This commit is contained in:
aaron 2023-07-30 14:35:46 +01:00
parent 1aa74310f3
commit 676bbbcc00
15 changed files with 665 additions and 873 deletions

View file

@ -1,43 +1,101 @@
------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- testkit.luau
-- v0.2.0
------------------------------------------------------------------------------------------
-- v0.7.0
--------------------------------------------------------------------------------
--[[
local color = {
white_underline = function(s: string)
return `\27[1;4m{s}\27[0m`
end,
EXAMPLE USAGE:
white = function(s: string)
return `\27[37;1m{s}\27[0m`
end,
local testkit = require "path-to-testkit"
green = function(s: string)
return `\27[32;1m{s}\27[0m`
end,
local TEST, CASE, CHECK = testkit.getUnitTestTools()
red = function(s: string)
return `\27[31;1m{s}\27[0m`
end,
TEST("test name", function()
do CASE "A"
CHECK(condition)
yellow = function(s: string)
return `\27[33;1m{s}\27[0m`
end,
red_highlight = function(s: string)
return `\27[41;1;30m{s}\27[0m`
end,
green_highlight = function(s: string)
return `\27[42;1;30m{s}\27[0m`
end,
gray = function(s: string)
return `\27[30;1m{s}\27[0m`
end,
}
local function convert_units(unit: string, value: number): (number, string)
local prefix_colors = {
[3] = color.red,
[2] = color.yellow,
[1] = color.yellow,
[0] = color.green,
[-1] = color.red,
[-2] = color.yellow,
[-3] = color.green
}
local prefixes = {
[3] ="G",
[2] ="M",
[1] = "k",
[0] = " ",
[-1] = "m",
[-2] = "u",
[-3] = "n"
}
local order = 0
while value >= 1000 do
order += 1
value /= 1000
end
end)
local BENCH, START = testkit.getBenchmarkTools()
BENCH("benchmark name", function()
local x = 0
for i = 1, START(1e6) do
x += 1
while value ~= 0 and value < 1 do
order -= 1
value *= 1000
end
end)
]]
------------------------------------------------------------------------------------------
-- Unit Testing
------------------------------------------------------------------------------------------
if value >= 100 then
value = math.floor(value)
elseif value >= 10 then
value = math.floor(value * 1e1) / 1e1
elseif value >= 1 then
value = math.floor(value * 1e2) / 1e2
end
return value, prefix_colors[order](prefixes[order] .. unit)
end
local WALL = color.gray ""
--------------------------------------------------------------------------------
-- Testing
--------------------------------------------------------------------------------
type Test = {
name: string,
activeCase: Case?,
case: Case?,
cases: { Case },
duration: number,
error: string?
error: {
message: string,
trace: string
}?
}
type Case = {
@ -46,185 +104,220 @@ type Case = {
line: number?
}
local PASS = 1
local FAIL = 2
local NONE = 3
local ERROR = 4
local PASS, FAIL, NONE, ERROR = 1, 2, 3, 4
local activeTest: Test?
local skip: string?
local test: Test?
local tests: { Test } = {}
local function outputTestResults(test: Test)
print("\27[1;4m"..test.name.."\27[0m")
local function output_test_result(test: Test)
print(color.white(test.name))
for _, case in test.cases do
print(
"[" ..
(if case.result == PASS then
"\27[32;1mPASS\27[0m"
elseif case.result == FAIL then
"\27[31;1mFAIL:"..assert(case.line).."\27[0m"
elseif case.result == NONE then
"\27[33;1mNONE\27[0m"
else
"\27[41;1;30mERROR\27[0m")
.. "] " .. case.name
)
local status = ({
[PASS] = color.green "PASS",
[FAIL] = color.red "FAIL",
[NONE] = color.yellow "NONE",
[ERROR] = color.red "FAIL"
})[case.result]
local line = case.result == FAIL and color.red(`{case.line}:`) or ""
print(`{status}{WALL} {line}{color.gray(case.name)}`)
end
if test.error then
print("\27[31;1;30merror: " .. test.error .. "\27[0m")
print(color.gray "error: " .. color.red(test.error.message))
print(color.gray "trace: " .. color.red(test.error.trace))
else
print()
end
print ""
end
local function CASE(name: string)
assert(activeTest, "no active test")
assert(test, "no active test")
local case: Case = {
local case = {
name = name,
result = NONE
}
activeTest.activeCase = case
table.insert(activeTest.cases, case)
test.case = case
table.insert(test.cases, case)
end
local function CHECK(value: any): boolean
assert(activeTest, "no active test")
local activeCase = activeTest.activeCase
local function CHECK<T>(value: T, stack: number?): T
assert(test, "no active test")
local case = test.case
if not activeCase then
if not case then
CASE ""
activeCase = activeTest.activeCase
end; assert(activeCase, "no active case")
local result = value and PASS or FAIL
if activeCase.result == NONE or activeCase.result == PASS then
activeCase.result = result
activeCase.line = debug.info(2, "l")
case = test.case
end
return result == PASS
assert(case, "no active case")
if case.result ~= FAIL then
case.result = value and PASS or FAIL
case.line = debug.info(stack and stack + 1 or 2, "l")
end
return value
end
local function TEST(name: string, fn: () -> ())
assert(not activeTest, "new test was started while a test was in progress")
local test: Test = {
if skip and name ~= skip then return end
local active = test
assert(not active, "cannot start test while another test is in progress")
test = {
name = name,
cases = {},
duration = 0
}
}; assert(test)
activeTest = test
table.insert(tests, test)
local start = os.clock()
local msg: string?
local success = xpcall(fn, function(m: string) msg = m .. debug.traceback("", 2) end)
local err
local success = xpcall(fn, function(m: string)
err = { message = m, trace = debug.traceback(nil, 2) }
end)
test.duration = os.clock() - start
if not test.activeCase then CASE "" end
assert(test.activeCase, "no active case")
if not test.case then CASE "" end
assert(test.case, "no active case")
if not success then
test.activeCase.result = ERROR
test.error = msg
test.case.result = ERROR
test.error = err
end
activeTest = nil
outputTestResults(test)
test = nil
end
local function FINISH(): boolean
local success = true
local totalCases = 0
local passedCases = 0
local total_cases = 0
local passed_cases = 0
local duration = 0
for _, test in tests do
duration += test.duration
for _, case in test.cases do
totalCases += 1
total_cases += 1
if case.result == PASS or case.result == NONE then
passedCases += 1
passed_cases += 1
else
success = false
end
end
output_test_result(test)
end
print(string.format("%d/%d test cases passed in %.3f ms.", passedCases, totalCases, duration*1e3))
print(color.gray(string.format(
`{passed_cases}/{total_cases} test cases passed in %.3f ms.`,
duration*1e3
)))
local fails = totalCases - passedCases
local fails = total_cases - passed_cases
print(string.format("\27[%d;1;30m%d fail%s\27[0m", fails > 0 and 41 or 42, fails, fails == 1 and "" or "s"))
print(
(
fails > 0
and color.red
or color.green
)(`{fails} {fails == 1 and "fail" or "fails"}`)
)
return success, table.clear(tests)
end
------------------------------------------------------------------------------------------
local function SKIP(name: string)
assert(not test, "cannot skip during test")
skip = name
end
--------------------------------------------------------------------------------
-- Benchmarking
------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------
type Bench = {
timeStart: number?,
memStart: number?,
time_start: number?,
memory_start: number?,
iterations: number?
}
local activeBench: Bench? = nil
local bench: Bench?
function START(iter: number?): number
local n = iter or 1
if n < 1 then error("iteration count must be greater than 0", 2) end
assert(activeBench, "no active benchmark")
assert(not activeBench.timeStart, "clock was already started")
assert(n > 0, "iterations must be greater than 0")
assert(bench, "no active benchmark")
assert(not bench.time_start, "clock was already started")
activeBench.iterations = n
activeBench.memStart = gcinfo()
activeBench.timeStart = os.clock()
bench.iterations = n
bench.memory_start = gcinfo()
bench.time_start = os.clock()
return n
end
local function BENCH(name: string, fn: () -> ())
assert(not activeBench, "cannot run benchmark, a benchmark is already in progress")
local active = bench
assert(not active, "a benchmark is already in progress")
local bench: Bench = {}
activeBench = bench
bench = {}; assert(bench)
local memStart = gcinfo()
local timeStart = os.clock()
local msg: string?
local success = xpcall(fn, function(m: string) msg = m .. debug.traceback("", 2) end)
local timeStop = os.clock()
local memStop = gcinfo()
;(collectgarbage :: any)("collect")
local mem_start = gcinfo()
local time_start = os.clock()
local err_msg: string?
local success = xpcall(fn, function(m: string)
err_msg = m .. debug.traceback(nil, 2)
end)
local time_stop = os.clock()
local mem_stop = gcinfo()
if not success then
print("[\27[41;1mERROR\27[0m] " .. name)
print("\27[31;1m" .. "error: " .. msg :: string .. "\27[0m")
activeBench = nil
return
print(`{WALL}{color.red("ERROR")}{WALL} {name}`)
print(color.gray(err_msg :: string))
else
time_start = bench.time_start or time_start
mem_start = bench.memory_start or mem_start
local n = bench.iterations or 1
local d, d_unit = convert_units("s", (time_stop - time_start) / n)
local a, a_unit = convert_units("B", math.floor((mem_stop - mem_start) / n * 1e3))
local function round(x: number): string
return x > 0 and x < 10 and (x - math.floor(x)) > 0
and string.format("%2.1f", x)
or string.format("%3.f", x)
end
print(string.format(
`%s %s %s %s{WALL} %s`,
color.gray(tostring(round(d))),
d_unit,
color.gray(tostring(round(a))),
a_unit,
color.gray(name)
))
end
timeStart = bench.timeStart or timeStart
memStart = bench.memStart or memStart
local n = bench.iterations or 1
local duration = timeStop - timeStart
local allocated = memStop - memStart
print(string.format("[ %.3f us | %4.0f B ] %s", duration/n * 1e6, allocated/n * 1e3, name))
activeBench = nil
bench = nil
end
------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Printing
------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function printa(v: unknown)
local function print2(v: unknown)
type Buffer = { n: number, [number]: string }
-- overkill concatenationless string buffer
@ -291,31 +384,67 @@ local function printa(v: unknown)
print(table.concat(str))
end
printa "string"
--------------------------------------------------------------------------------
-- Equality
--------------------------------------------------------------------------------
printa(1)
local function shallow_eq(a: {}, b: {}): boolean
if #a ~= #b then return false end
printa {
hello = 1,
bye = "ok",
for i, v in next, a do
if b[i] ~= v then
return false
end
end
test = {
1, 2, 3
}
}
for i, v in next, b do
if a[i] ~= v then
return false
end
end
------------------------------------------------------------------------------------------
return true
end
local function deep_eq(a: {}, b: {}): boolean
if #a ~= #b then return false end
for i, v in next, a do
if type(b[i]) == "table" and type(v) == "table" then
if deep_eq(b[i], v) == false then return false end
elseif b[i] ~= v then
return false
end
end
for i, v in next, b do
if type(a[i]) == "table" and type(v) == "table" then
if deep_eq(a[i], v) == false then return false end
elseif a[i] ~= v then
return false
end
end
return true
end
--------------------------------------------------------------------------------
-- Return
------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------
return {
getUnitTestTools = function()
return TEST, CASE, CHECK, FINISH
test = function()
return TEST, CASE, CHECK, FINISH, SKIP
end,
getBenchmarkTools = function()
benchmark = function()
return BENCH, START
end,
printa = printa
print2 = print2,
seq = shallow_eq,
deq = deep_eq,
color = color
}

View file

@ -2,7 +2,7 @@
-- unit.lua
----------------------------------------------------------------------------------------------------------------------
local TEST, CASE, CHECK, FINISH = require("test/testkit").getUnitTestTools()
local TEST, CASE, CHECK, FINISH, SKIP = require("test/testkit").test()
local mock = require "test/mock"
local Signal = require "test/goodsignal"
@ -17,19 +17,23 @@ local function gc(n: number?)
end
end
local function weak<T>(t: T & {}): T
setmetatable(t :: {}, { __mode = "kv" })
return t
end
-- weak reference table used for gc tests
local wref = setmetatable({}, { __mode = "kv" }) :: any
--local wref = setmetatable({}, { __mode = "kv" }) :: any
TEST("graph", function()
local graph = require "src/graph"
local create = graph.create
local get = graph.get
local unwrap = graph.unwrap
local set = graph.set
local capture = graph.capture
local captureAndLink = graph.captureAndLink
local capture_and_link = graph.capture_and_link
local link = graph.link
local setEffect = graph.setEffect
local set_effect = graph.set_effect
do CASE "Create"
local node = create(1)
@ -47,8 +51,8 @@ TEST("graph", function()
do CASE "Capture"
local node1 = create(nil)
local node2 = create(nil)
local nodes = capture(function(from)
return from(node1), from(node2)
local nodes = capture(function()
return get(node1), get(node2)
end)
CHECK(nodes[1] == node1)
CHECK(nodes[2] == node2)
@ -56,7 +60,7 @@ TEST("graph", function()
do CASE "Link"
local parent = create(1)
local child = create(nil)
local child = create(0)
link(parent, child, function()
return get(parent)
end)
@ -68,14 +72,15 @@ TEST("graph", function()
local parent = create(1)
local child = create(nil :: any)
captureAndLink(child, function(from)
return tostring(from(parent))
capture_and_link(child, function()
return tostring(get(parent))
end)
set(parent, get(parent) + 1)
CHECK(get(child) == "2")
end
--[[
do CASE "Scoped captures"
-- table.find but uses `rawequal` since nodes have overloaded __eq metamethod
local function rawfind(t, x)
@ -100,52 +105,54 @@ TEST("graph", function()
CHECK(not rawfind(captures, a))
CHECK(rawfind(captures, b))
-- repeat for `captureAndLink`
-- repeat for `capture_and_link`
local c = graph.create(1)
set(a, get(a) + 1) -- mark `b` for recomputation again
captureAndLink(c, function(from) return from(b) end)
capture_and_link(c, function(from) return from(b) end)
-- check that only `b` was linked
CHECK(not rawfind(assert(a.__children), c))
CHECK(rawfind(assert(b.__children), c))
end
]]
do CASE "Nodes garbage collection"
local node = create(1) :: Node?
wref.node, node = node, nil
local wref
do
wref = weak { create(1) }
end
gc()
CHECK(not wref.node)
CHECK(not wref[1])
end
do CASE "Node effect garbage collection"
local wref
do
local function factory(p) -- factory function to prevent closure caching
return function()
return unwrap(p)
return get(p)
end
end
local node = create(1)
local effect1 = factory(node)
local effect2 = factory(node)
wref.node = node
wref.effect1 = effect1
wref.effect2 = effect2
do
local effect1 = factory(node)
local effect2 = factory(node)
local t = {}
setEffect(node, effect1, t)
setEffect(node, effect2, true)
wref = weak { effect1, effect2, node :: any }
t = nil :: any
effect1, effect2 = nil :: any, nil :: any
set_effect(node, effect1, {})
set_effect(node, effect2, true)
end
gc()
CHECK(not wref.effect1) -- effect1 should gc since nothing is referencing table `t`
CHECK(wref.effect2) -- effect2 should not gc as `true` is not garbage collectable
CHECK(not wref[1]) -- effect1 should gc since nothing is referencing table `t`
CHECK(wref[2]) -- effect2 should not gc as `true` is not garbage collectable
end
gc()
CHECK(not wref.node and not wref.effect2) -- node should now gc along with effect2
CHECK(not wref[3] and not wref[2]) -- node should now gc along with effect2
do -- same test but for multiple nodes referenced by watcher
local function factory(a, b)
@ -162,8 +169,8 @@ TEST("graph", function()
wref.effect = effect
local t1 = {}
setEffect(node1, effect, t1)
setEffect(node2, effect, t1)
set_effect(node1, effect, t1)
set_effect(node2, effect, t1)
t1 = nil :: any
effect = nil :: any
@ -182,171 +189,69 @@ end)
TEST("wrap()", function()
local wrap = vide.wrap
local unwrap = vide.unwrap
local wrapped = vide.wrapped
local watch = vide.watch
do CASE "Wrap value"
local state = wrap(1)
CHECK(wrapped(state))
CHECK(unwrap(state) == 1)
CHECK(state() == 1)
end
do CASE "Setter"
local state, set = wrap(1)
set(2) -- set directly
CHECK(unwrap(state) == 2)
set(function(x) return x + 1 end) -- set via function
CHECK(unwrap(state) == 3)
set((wrap(4))) -- set using value of another state
CHECK(unwrap(state) == 4)
local state = wrap(1)
state(2) -- set directly
CHECK(state() == 2)
end
do CASE "Does not update if same value"
local state, set = wrap(1)
local state = wrap(1)
local updates = -1
watch(function(from)
from(state)
watch(function()
state()
updates += 1
end)
set(1)
CHECK(unwrap(state) == 1)
state(1)
CHECK(updates == 0)
set(1, true)
state(2)
CHECK(updates == 1)
end
do CASE "Does not rewrap state"
local state = wrap((wrap(1)))
CHECK(not wrapped(unwrap(state)))
end
end)
TEST("unwrap()", function()
local wrap = vide.wrap
local unwrap = vide.unwrap
do CASE "Gets state value"
local a = wrap(1)
CHECK(unwrap(a) == 1)
end
do CASE "Allow passthrough of non-state"
CHECK(unwrap(5) == 5)
end
end)
TEST("wrapped()", function()
local wrap = vide.wrap
local wrapped = vide.wrapped
do CASE "Check if value is a state object"
local state = wrap()
CHECK(wrapped(state))
end
do CASE "Refuse non-state"
CHECK(not wrapped(0))
end
end)
TEST("derive()", function()
local wrap = vide.wrap
local unwrap = vide.unwrap
local derive = vide.derive
do CASE "Derive new value on state change"
local state, set = wrap(1)
local state = wrap(1)
local derived = derive(function(from)
return tostring(from(state))
local derived = derive(function()
return tostring(state())
end)
CHECK(unwrap(derived) == "1") -- check initial run during detection
set(function(x) return x + 1 end)
CHECK(unwrap(derived) == "2") -- check actually updates
end
do CASE "Shorthand derivation"
do
local count, set = wrap(1 :: any)
local text = "Count: " .. count
CHECK(unwrap(text) == "Count: 1")
set(2)
CHECK(unwrap(text) == "Count: 2")
end
do
local count, set = wrap(1 :: any)
local text = count .. "x"
CHECK(unwrap(text) == "1x")
set(2)
CHECK(unwrap(text) == "2x")
end
do
local count, set = wrap(1 :: any)
local text = count .. count
CHECK(unwrap(text) == "11")
set(2)
CHECK(unwrap(text) == "22")
end
do
local a, seta = wrap { b = { c = 1 } }
local b = a.b
local c = b.c
CHECK(unwrap(c) == 1)
seta { b = { c = 2 } }
CHECK(unwrap(c) == 2)
end
do
local state, set = wrap { profiles = { decimal = { level = 1 }}}
local stringified = "Level: " .. state.profiles.decimal.level
set(function(state)
state.profiles.decimal.level += 1
return state
end, true)
CHECK(unwrap(stringified) == "Level: 2")
end
CHECK(derived() == "1") -- check initial run during detection
state(state() + 1)
CHECK(derived() == "2") -- check updates
end
do CASE "Derive from updated"
do
local a, set = wrap(1)
local a = wrap(1)
local b = derive(function(from)
return from(a) + 1
local b = derive(function()
return a() + 1
end)
set(2)
a(2)
local c = derive(function(from)
return from(b) + 1
local c = derive(function()
return b() + 1
end)
CHECK(unwrap(c) == 4)
end
do
local a, set = wrap(1)
local b = a + 1
set(2)
local c = b + 1
CHECK(unwrap(c) == 4)
CHECK(c() == 4)
end
end
--[[
do CASE "Cleanup"
local count, set = wrap(1)
@ -356,69 +261,67 @@ TEST("derive()", function()
v.Destroyed = true
end)
local first = unwrap(derived)
local first = derived()
CHECK(first.Destroyed == false)
set(2)
local _ = unwrap(derived) -- trigger recalc
local _ = derived() -- trigger recalc
CHECK(first.Destroyed == true)
end
]]
do CASE "Garbage collection"
do -- check that `derived` does not allow gc of `state`
local state = wrap(1) :: State?
do -- check that `b` does not allow gc of `a`
local a = wrap(1)
local _derived = derive(function(from)
return from(state)
local _b = derive(function()
return a()
end)
wref.state, state = state, nil
local wref = weak { a }
a = nil :: any
gc()
CHECK(not wref.state)
CHECK(not wref[1])
end
do -- check that `state` allows gc of `derived`
local state = wrap(1)
do -- check that `a` allows gc of `b`
local a = wrap(1)
local derived = derive(function(from)
return from(state)
end) :: State?
local b = derive(function()
return a()
end)
wref.derived, derived = derived, nil
local wref = weak { b }
b = nil :: any
gc()
CHECK(not wref.derived)
CHECK(not wref[1])
end
end
do CASE "Garbage collection 2"
-- creats a chain `a -> b -> c` where `a` is the root
local function setup()
local weak = setmetatable({}, { __mode = "v" })
local a = wrap(1)
local a, setA = wrap(1)
local b = derive(function(from)
return from(a)
local b = derive(function()
return a()
end)
local c = derive(function(from)
return from(b)
local c = derive(function()
return b()
end)
weak.a = a
weak.b = b
weak.c = c
return weak, a, b, c, setA
return weak { a, b, c }, a, b, c
end
do -- check that b and c can gc if a is referenced
local weak, _a = setup()
local wref, _a = setup()
gc()
CHECK(not weak.b)
CHECK(not weak.c)
CHECK(not wref[2])
CHECK(not wref[3])
end
do -- check that a and b wont gc if c is referenced
@ -427,21 +330,21 @@ TEST("derive()", function()
_a, _b = nil :: any, nil :: any
gc()
CHECK(weak.a)
CHECK(weak.b)
CHECK(weak[1])
CHECK(weak[2])
end
do -- check that b wont gc if a and c are referenced
local weak, _a, _b, c, setA = setup()
local weak, a, _b, c = setup()
_b = nil :: any
gc()
setA(2)
a(2)
CHECK(weak.b)
CHECK(unwrap(c) == 2)
CHECK(weak[2])
CHECK(c() == 2)
end
end
end)
@ -451,54 +354,54 @@ TEST("watch()", function()
local watch = vide.watch
do CASE "Capture states"
local a, setA = wrap(1)
local b, setB = wrap(1)
local a = wrap(1)
local b = wrap(1)
local runcount = 0
watch(function(from)
local _ = from(a) + from(b)
watch(function()
a()
b()
runcount += 1
end)
CHECK(runcount == 1) -- immediate callback execution
setA(2)
a(2)
CHECK(runcount == 2)
setB(2)
b(2)
CHECK(runcount == 3)
end
do CASE "Stop watch"
local a, setA = wrap(1)
local b, setB = wrap(1)
local a = wrap(1)
local b = wrap(1)
local runcount = 0
local unwatch = watch(function(from)
local _ = from(a) + from(b)
local unwatch = watch(function()
a()
runcount += 1
end)
CHECK(runcount == 1)
unwatch()
setA(2)
setB(2)
a(2)
CHECK(runcount == 1)
end
do CASE "Side-effect cleanup"
local state, set = wrap(1)
local state = wrap(1)
local effect_runcount = 0
local cleanup_runcount = 0
local unwatch = watch(function(from)
local _ = from(state)
local unwatch = watch(function()
state()
effect_runcount += 1
return function() cleanup_runcount += 1 end
end)
CHECK(effect_runcount == 1)
CHECK(cleanup_runcount == 0)
set(2)
state(2)
CHECK(effect_runcount == 2)
CHECK(cleanup_runcount == 1)
unwatch()
@ -508,22 +411,24 @@ TEST("watch()", function()
do CASE "Garbage collection"
local function factory(p)
return function(from: any)
from(p)
return function()
p()
end
end
do -- state prevents gc of watcher
local state = wrap(1)
local effect = factory(state)
watch(effect)
wref.effect = effect
effect = nil :: any
local wref
do
local effect = factory(state)
watch(effect)
wref = { effect }
end
gc()
CHECK(wref.effect) -- should still exist
CHECK(wref[1]) -- should still exist
end
@ -533,42 +438,42 @@ TEST("watch()", function()
local unwatch = watch(effect)
wref.effect = effect
local wref = weak { effect }
effect = nil :: any
gc()
CHECK(wref.effect) -- should still exist
CHECK(wref[1]) -- should still exist
unwatch(); unwatch = nil :: any
gc()
CHECK(not wref.effect) -- should gc
CHECK(not wref[1]) -- should gc
end
do -- state can gc with watcher
local state = wrap(1)
local effect = factory(state)
watch(effect)
local wref
wref.state = state
state = nil :: any
effect = nil :: any
do
local state = wrap(1)
local effect = factory(state)
watch(effect)
wref = weak { state }
end
gc()
CHECK(not wref.state) -- should gc
CHECK(not wref[1]) -- should gc
end
do -- watcher can gc if no state captured
local effect = factory()
local effect = factory(function() end)
watch(effect)
wref.effect = effect
local wref = weak { effect }
effect = nil :: any
gc()
CHECK(not wref.effect) -- should gc
CHECK(not wref[1]) -- should gc
end
end
end)
@ -576,7 +481,6 @@ end)
TEST("create()", function()
local create = vide.create
local wrap = vide.wrap
local Children: "Children" = vide.Children :: "Children"
do CASE "Apply default properties"
local defaults = require("src/defaults")
@ -601,15 +505,13 @@ TEST("create()", function()
do CASE "Assign children"
local frame = create "Frame" {
[Children] = {
create "TextLabel" { Name = "A" } :: any,
create "TextLabel" { Name = "B" },
create "TextLabel" { Name = "A" } :: any,
create "TextLabel" { Name = "B" },
{
create "TextLabel" { Name = "C" } :: any,
create "TextLabel" { Name = "D" },
{
create "TextLabel" { Name = "C" } :: any,
create "TextLabel" { Name = "D" },
{
create "TextLabel" { Name = "E" }
}
create "TextLabel" { Name = "E" }
}
}
}
@ -620,14 +522,14 @@ TEST("create()", function()
CHECK(frame:FindFirstChild "E")
local image = create "ImageLabel" {
[Children] = create "TextLabel" { Name = "A" }
create "TextLabel" { Name = "A" }
}
CHECK(image:FindFirstChild "A")
end
do CASE "Binding properties to state"
local name, setName = wrap("Hi")
local text, setText = wrap("Bye")
local name = wrap("Hi")
local text = wrap("Bye")
local label = create "TextLabel" {
Name = name,
@ -637,8 +539,8 @@ TEST("create()", function()
CHECK(label.Name == "Hi")
CHECK(label.Text == "Bye")
setName "Foo"
setText "Bar"
name "Foo"
text "Bar"
CHECK(label.Name == "Foo")
CHECK(label.Text == "Bar")
@ -648,14 +550,14 @@ TEST("create()", function()
do -- instance should gc despite property bound to state
local state = wrap("Hi")
do
wref.instance = create "TextLabel" {
local wref = weak {
create "TextLabel" {
Text = state,
}
end
}
gc()
CHECK(not wref.instance)
CHECK(not wref[1])
end
do -- instance should NOT gc despite property bound to state when parented
@ -663,45 +565,47 @@ TEST("create()", function()
local parent = create "Frame" {}
do
wref.instance = create "TextLabel" {
local wref = weak {
create "TextLabel" {
Parent = parent,
Text = state,
}
end
}
gc()
CHECK(wref.instance)
CHECK(wref[1])
wref.instance.Parent = nil
wref.instance.Parent = parent
wref[1].Parent = nil
wref[1].Parent = parent
gc()
CHECK(wref.instance)
CHECK(wref[1])
wref.instance:Destroy()
wref[1]:Destroy()
gc()
CHECK(not wref.instance)
CHECK(not wref[1])
end
do -- state should not gc once exits scope while instance still exists
local _label
local wref
do
local state = wrap("Hi")
wref.state = state
wref = weak { state }
_label = create "TextLabel" {
Name = state,
}
end
gc()
CHECK(wref.state)
CHECK(wref[1])
end
do -- state and instance should gc once both exit scope
local wref
do
local text = wrap("Hi")
@ -709,16 +613,16 @@ TEST("create()", function()
Text = text,
}
wref.text = text
wref.box = box
wref = weak { text = text, box = box}
end
gc()
CHECK(not wref.state)
CHECK(not wref.text)
CHECK(not wref.box)
end
--[[
do -- binding should gc despite state still existing after instance is gc
local state = wrap("Hi")
@ -727,8 +631,13 @@ TEST("create()", function()
Text = state,
}
local wref = {
instance = instance,
brinding = next((state :: any).effects
}
wref.instance = instance
wref.binding = next((state :: any).__effects)
wref.binding = )
end
CHECK(wref.binding)
@ -737,11 +646,11 @@ TEST("create()", function()
CHECK(not wref.instance)
CHECK(not wref.binding)
end
end]]
end
do CASE "Bind same state to multiple instance properties"
local state, set = wrap "1"
local state = wrap "1"
local text = create "TextBox" {
Name = state,
@ -749,7 +658,7 @@ TEST("create()", function()
PlaceholderText = state
}
set "2"
state "2"
CHECK(text.Name == "2")
CHECK(text.Text == "2")
@ -757,7 +666,7 @@ TEST("create()", function()
end
do CASE "Bind children"
local state, set = wrap({} :: {}?)
local state = wrap({} :: {}?)
local a, b, c =
create "TextLabel" { Name = "A" },
@ -765,31 +674,32 @@ TEST("create()", function()
create "TextLabel" { Name = "C" }
local frame = create "Frame" {
[Children] = state
state
}
set { a, b }
state { a, b }
CHECK(frame:FindFirstChild "A")
CHECK(frame:FindFirstChild "B")
-- check that b is removed and c is added while a remains untouched
set { a, c }
state { a, c }
CHECK(frame:FindFirstChild "A")
CHECK(frame:FindFirstChild "C")
CHECK(not frame:FindFirstChild "B")
set(nil)
state(nil)
CHECK(#frame:GetChildren() == 0)
end
--[[
do CASE "Parent set to nil by state does not allow gc"
-- this is technically a bug but we test for this anyways to confirm behavior
local parent = create "Frame" { Name = "Parent" }
local state, set = wrap(parent :: Frame?)
local state = wrap(parent :: Frame?)
do
wref.child = create "Frame" { Parent = state, Name = "Child" } :: Frame?
@ -808,6 +718,7 @@ TEST("create()", function()
gc()
CHECK(not wref.child)
end
]]
do CASE "GC test"
local data = setmetatable({}, {})
@ -872,9 +783,9 @@ TEST("map()", function()
return tostring(v)
end)
local t = unwrap(derived)
local t = derived()
for i, v in next, unwrap(state) do
for i, v in next, state() do
CHECK(tostring(v) == t[i])
end
end
@ -889,10 +800,10 @@ TEST("map()", function()
return tostring(v)
end)
local _ = unwrap(derived) -- trigger evaluation (so the next set is forced to be re-calculated)
local _ = derived() -- trigger evaluation (so the next set is forced to be re-calculated)
set { 1, 2, 4 }
local t = unwrap(derived)
local t = derived()
CHECK(t[1] == "1")
CHECK(t[2] == "2")
@ -910,10 +821,10 @@ TEST("map()", function()
return tostring(v)
end)
local _ = unwrap(derived) -- trigger evaluation (so the next set is forced to be re-calculated)
local _ = derived() -- trigger evaluation (so the next set is forced to be re-calculated)
set { 1, 2 }
local t = unwrap(derived)
local t = derived()
CHECK(t[1] == "1")
CHECK(t[2] == "2")
@ -962,13 +873,13 @@ TEST("map()", function()
v.Destroyed = true
end)
local first = unwrap(derived)
local first = derived()
CHECK(first[1].Destroyed == false)
set { 1, 2, 4 }
local _ = unwrap(derived)
local _ = derived()
CHECK(first[1].Destroyed == false)
CHECK(first[2].Destroyed == false)
@ -1033,10 +944,10 @@ TEST("spring()", function()
local springed = spring(number, 1, 1)
set(20)
CHECK(unwrap(springed) == 10)
CHECK(springed() == 10)
vide.step(1/60)
CHECK(unwrap(springed) ~= 10)
CHECK(unwrap(springed) > 10)
CHECK(springed() ~= 10)
CHECK(springed() > 10)
end
do CASE "Garbage collection"
@ -1080,23 +991,6 @@ TEST("spring()", function()
end
end)
TEST("Layout", function()
local create = vide.create
local Layout = vide.Layout
do CASE "Apply layout properties"
local frame = create "Frame" {
[Layout] = {
AnchorPoint = Vector2.new(0, 0.5),
Position = UDim2.fromScale(0.5, 0.5)
}
}
CHECK(frame.AnchorPoint == Vector2.new(0, 0.5))
CHECK(frame.Position == UDim2.fromScale(0.5, 0.5))
end
end)
TEST("Event", function()
local create = vide.create
local Event = vide.Event
@ -1199,6 +1093,7 @@ TEST("Changed", function()
end
end)
--[[
TEST("Created", function()
local create = vide.create
local Created = vide.Created
@ -1218,7 +1113,7 @@ TEST("Created", function()
CHECK(ran)
end
end)
end)]]
TEST("strict", function()
vide.strict = true
@ -1265,7 +1160,7 @@ TEST("strict", function()
CHECK(runcount == 2)
set(2)
local _ = unwrap(derived)
local _ = derived()
CHECK(runcount == 4)
end