--[[ Test suite for multi.lua Run with: lua multi_test.lua Requires multi.lua to be in the same directory or on the Lua path. Compatible with Lua 5.1, 5.2, 5.3, 5.4, and LuaJIT. ]] package.path = "?/init.lua;?.lua;./init.lua;./?.lua;" .. package.path -- ───────────────────────────────────────────── -- Minimal test runner -- ───────────────────────────────────────────── local passed, failed, skipped = 0, 0, 0 local failures = {} local function test(name, fn) local ok, err = pcall(fn) if ok then passed = passed + 1 io.write("\x1b[92m ✓\x1b[0m " .. name .. "\n") else failed = failed + 1 table.insert(failures, {name = name, err = tostring(err)}) io.write("\x1b[91m ✗\x1b[0m " .. name .. "\n") io.write(" " .. tostring(err) .. "\n") end end local function skip(name, reason) skipped = skipped + 1 io.write("\x1b[93m -\x1b[0m " .. name .. " [SKIPPED: " .. (reason or "") .. "]\n") end local function section(name) io.write("\n\x1b[97m── " .. name .. " ──\x1b[0m\n") end local function assert_eq(a, b, msg) if a ~= b then error((msg or "assert_eq failed") .. ": expected " .. tostring(b) .. ", got " .. tostring(a), 2) end end local function assert_truthy(v, msg) if not v then error((msg or "expected truthy value, got falsy") .. ": " .. tostring(v), 2) end end local function assert_falsy(v, msg) if v then error((msg or "expected falsy value, got truthy") .. ": " .. tostring(v), 2) end end local function assert_type(v, t, msg) if type(v) ~= t then error((msg or "type mismatch") .. ": expected " .. t .. ", got " .. type(v), 2) end end -- ───────────────────────────────────────────── -- Load the library -- ───────────────────────────────────────────── local multi, thread local ok, err = pcall(function() multi, thread = require("multi"):init() end) if not ok then io.write("\x1b[91mFATAL: Could not load multi.lua: " .. tostring(err) .. "\x1b[0m\n") io.write("Make sure multi.lua is in the same directory or on package.path.\n") os.exit(1) end -- Helper: run the scheduler for up to `max_ticks` ticks or until `done()` returns true. local function run_until(done, max_ticks) max_ticks = max_ticks or 10000 for _ = 1, max_ticks do multi:uManager() if done and done() then return true end end return done == nil end -- ═════════════════════════════════════════════ -- 1. LIBRARY METADATA -- ═════════════════════════════════════════════ section("Library Metadata") test("multi is a table", function() assert_type(multi, "table") end) test("multi.Version is a string", function() assert_type(multi.Version, "string") assert_truthy(#multi.Version > 0, "Version string should not be empty") end) test("multi.Name is 'root'", function() assert_eq(multi.Name, "root") end) test("multi.Type is registered rootprocess type", function() assert_truthy(multi:isType(multi.registerType("rootprocess")), "Type should be rootprocess") end) test("$multi global is populated", function() assert_truthy(_G["$multi"], "$multi global should exist") assert_truthy(_G["$multi"].multi, "$multi.multi should exist") assert_truthy(_G["$multi"].thread, "$multi.thread should exist") end) -- ═════════════════════════════════════════════ -- 2. TYPE SYSTEM -- ═════════════════════════════════════════════ section("Type System") test("registerType returns the type string", function() local t = multi.registerType("test_custom_type_xyz") assert_eq(t, "test_custom_type_xyz") end) test("registerType is idempotent (re-register same type)", function() local t1 = multi.registerType("idempotent_type") local t2 = multi.registerType("idempotent_type") assert_eq(t1, t2) end) test("hasType finds registered types", function() multi.registerType("findable_type") assert_truthy(multi.hasType("findable_type"), "Should find registered type") end) test("hasType returns nil for unknown types", function() local result = multi.hasType("definitely_not_registered_xyzzy") assert_falsy(result, "Should return nil for unknown type") end) test("getTypes returns a table", function() assert_type(multi.getTypes(), "table") assert_truthy(#multi.getTypes() > 0, "Should have at least one registered type") end) test("multi:isType() works correctly", function() assert_truthy(multi:isType(multi.registerType("rootprocess"))) assert_falsy(multi:isType("not_root")) end) test("DestroyedObj sentinels are tables", function() assert_type(multi.DestroyedObj, "table") assert_eq(multi.DESTROYED, multi.DestroyedObj) end) test("setType converts object to DestroyedObj", function() local obj = {foo = "bar", baz = 42} multi.setType(obj, multi.DestroyedObj) -- After destruction, accessing fields should return DestroyedObj-family values assert_truthy(obj.foo ~= nil or obj.foo == nil) -- should not error end) -- ═════════════════════════════════════════════ -- 3. UTILITY FUNCTIONS -- ═════════════════════════════════════════════ section("Utility Functions") test("multi.randomString returns a string of the right length", function() for _, n in ipairs({1, 5, 10, 32}) do local s = multi.randomString(n) assert_type(s, "string") assert_eq(#s, n, "randomString(" .. n .. ") length") end end) test("multi.randomString produces alphanumeric characters only", function() local s = multi.randomString(100) assert_truthy(s:match("^[a-zA-Z0-9]+$"), "Should only contain alphanumeric chars") end) test("multi.ForEach iterates all elements", function() local collected = {} multi.ForEach({10, 20, 30}, function(v) table.insert(collected, v) end) assert_eq(#collected, 3) assert_eq(collected[1], 10) assert_eq(collected[3], 30) end) test("multi.ForEach on empty table does nothing", function() local count = 0 multi.ForEach({}, function() count = count + 1 end) assert_eq(count, 0) end) test("multi.isMulitObj returns true for multi objects", function() local alarm = multi:newAlarm(999) assert_truthy(multi.isMulitObj(alarm)) alarm:Destroy() end) test("multi.isMulitObj returns false for plain tables", function() assert_falsy(multi.isMulitObj({foo = "bar"})) end) test("multi.isMulitObj returns false for non-tables", function() assert_falsy(multi.isMulitObj("string")) assert_falsy(multi.isMulitObj(42)) assert_falsy(multi.isMulitObj(nil)) end) test("multi.Round rounds correctly", function() assert_eq(multi.Round(3.14159, 2), 3.14) assert_eq(multi.Round(2.5, 0), 3) assert_eq(multi.Round(1.005, 2), 1.01) end) test("multi.AlignTable returns a string", function() local result = multi.AlignTable({ {"Name", "Age", "City"}, {"Alice", "30", "NYC"}, {"Bob", "4", "LA"}, }) assert_type(result, "string") assert_truthy(result:find("Alice"), "Should contain 'Alice'") assert_truthy(result:find("Bob"), "Should contain 'Bob'") end) test("multi.timer returns elapsed time and results", function() local t, result = multi.timer(function() return 42 end) assert_type(t, "number") assert_truthy(t >= 0, "elapsed time should be non-negative") assert_eq(result, 42) end) test("multi.isTimeout returns true for TIMEOUT sentinel", function() assert_truthy(multi.isTimeout(multi.TIMEOUT)) assert_truthy(multi.isTimeout("TIMEOUT")) -- For backwards compat end) test("multi.isTimeout returns false for non-TIMEOUT values", function() assert_falsy(multi.isTimeout(nil)) assert_falsy(multi.isTimeout(42)) end) -- ═════════════════════════════════════════════ -- 4. UUID GENERATION -- ═════════════════════════════════════════════ section("UUID Generation") test("generate_uuid7 returns a string", function() local uuid = multi.generate_uuid7() assert_type(uuid, "string") end) test("generate_uuid7 has correct format (8-4-4-4-12)", function() local uuid = multi.generate_uuid7() assert_truthy(uuid:match("^%x%x%x%x%x%x%x%x%-%x%x%x%x%-%x%x%x%x%-%x%x%x%x%-%x%x%x%x%x%x%x%x%x%x%x%x$"), "UUID format mismatch: " .. uuid) end) test("generate_uuid7 version nibble is '7'", function() local uuid = multi.generate_uuid7() -- The 15th character (after removing hyphens at positions 9,14,19,24) is the version nibble -- UUID format: xxxxxxxx-xxxx-7xxx-xxxx-xxxxxxxxxxxx -> char 15 is '7' assert_eq(uuid:sub(15, 15), "7", "Version nibble should be '7', got: " .. uuid) end) test("generate_uuid7 produces unique IDs", function() local ids = {} for i = 1, 20 do ids[i] = multi.generate_uuid7() end -- Check a sample for uniqueness local seen = {} for _, id in ipairs(ids) do assert_falsy(seen[id], "Duplicate UUID generated: " .. id) seen[id] = true end end) test("extract_uuid7_timestamp returns a table with expected fields", function() local uuid = multi.generate_uuid7() local result = multi.extract_uuid7_timestamp(uuid) assert_type(result, "table") assert_truthy(result.milliseconds, "Should have milliseconds") assert_truthy(result.seconds, "Should have seconds") assert_truthy(result.date, "Should have date") assert_truthy(result.iso8601, "Should have iso8601") end) test("extract_uuid7_timestamp iso8601 ends with 'Z'", function() local uuid = multi.generate_uuid7() local result = multi.extract_uuid7_timestamp(uuid) assert_eq(result.iso8601:sub(-1), "Z") end) test("extract_uuid7_timestamp returns nil for invalid UUID", function() local result = multi.extract_uuid7_timestamp("not-a-valid-uuid") assert_falsy(result, "Should return nil for invalid UUID") end) -- ═════════════════════════════════════════════ -- 5. CONNECTION SYSTEM -- ═════════════════════════════════════════════ section("Connection System") test("newConnection returns a connection object", function() local conn = multi:newConnection() assert_type(conn, "table") assert_eq(conn.Type, multi.registerType("connector", "connections")) conn:Destroy() end) test("connection Fire calls connected function", function() local conn = multi:newConnection() local fired = false conn:Connect(function() fired = true end) conn:Fire() assert_truthy(fired, "Connected function should have been called") conn:Destroy() end) test("connection Fire passes arguments", function() local conn = multi:newConnection() local got_a, got_b conn:Connect(function(a, b) got_a = a; got_b = b end) conn:Fire(10, 20) assert_eq(got_a, 10) assert_eq(got_b, 20) conn:Destroy() end) test("connection supports multiple subscribers", function() local conn = multi:newConnection() local count = 0 conn:Connect(function() count = count + 1 end) conn:Connect(function() count = count + 1 end) conn:Connect(function() count = count + 1 end) conn:Fire() assert_eq(count, 3, "All three subscribers should have been called") conn:Destroy() end) test("hasConnections is false before Connect", function() local conn = multi:newConnection() assert_falsy(conn:hasConnections()) conn:Destroy() end) test("hasConnections is true after Connect", function() local conn = multi:newConnection() conn:Connect(function() end) assert_truthy(conn:hasConnections()) conn:Destroy() end) test("Unconnect removes the subscription", function() local conn = multi:newConnection() local count = 0 local ref = conn:Connect(function() count = count + 1 end) conn:Fire() assert_eq(count, 1) conn:Unconnect(ref) conn:Fire() assert_eq(count, 1, "Should not fire after Unconnect") conn:Destroy() end) test("Lock prevents Fire from calling subscribers", function() local conn = multi:newConnection() local count = 0 conn:Connect(function() count = count + 1 end) conn:Lock() conn:Fire() assert_eq(count, 0, "Locked connection should not fire") conn:Unlock() conn:Fire() assert_eq(count, 1, "Unlocked connection should fire") conn:Destroy() end) test("Bind replaces the subscriber list", function() local conn = multi:newConnection() local old_count, new_count = 0, 0 conn:Connect(function() old_count = old_count + 1 end) local old_fast = conn:Bind({function() new_count = new_count + 1 end}) conn:Fire() assert_eq(old_count, 0, "Old subscriber should not be called after Bind") assert_eq(new_count, 1, "New subscriber should be called") assert_type(old_fast, "table") conn:Destroy() end) test("Remove clears all subscribers", function() local conn = multi:newConnection() local count = 0 conn:Connect(function() count = count + 1 end) conn:Connect(function() count = count + 1 end) conn:Remove() conn:Fire() assert_eq(count, 0, "No subscribers should remain after Remove") conn:Destroy() end) test("connection__add operator creates OR connection", function() local c1 = multi:newConnection() local c2 = multi:newConnection() local or_conn = c1 + c2 local count = 0 or_conn:Connect(function() count = count + 1 end) c1:Fire() c2:Fire() assert_eq(count, 2, "OR connection should fire for each source") or_conn:Destroy() c1:Destroy() c2:Destroy() end) test("Destroy makes connection inert", function() local conn = multi:newConnection() local count = 0 conn:Connect(function() count = count + 1 end) conn:Destroy() -- Fire on a destroyed connection should be a no-op conn:Fire() assert_eq(count, 0, "Destroyed connection should not fire") assert_truthy(conn.destroyed, "destroyed flag should be set") end) test("connection_count increments on newConnection", function() local before = multi.connection_count local c = multi:newConnection() assert_eq(multi.connection_count, before + 1) c:Destroy() end) test("connection_subscriptions tracks Connect and Unconnect", function() local conn = multi:newConnection() local before = multi.connection_subscriptions local ref = conn:Connect(function() end) assert_eq(multi.connection_subscriptions, before + 1) conn:Unconnect(ref) assert_eq(multi.connection_subscriptions, before, "Should decrement on Unconnect") conn:Destroy() end) test("getConnections returns the subscriber list", function() local conn = multi:newConnection() conn:Connect(function() end) conn:Connect(function() end) local conns = conn:getConnections() assert_type(conns, "table") assert_truthy(#conns >= 2) conn:Destroy() end) -- ═════════════════════════════════════════════ -- 6. TIMER -- ═════════════════════════════════════════════ section("Timer") test("newTimer returns a timer object", function() local t = multi:newTimer() assert_eq(t.Type, multi.registerType("timer", "timers")) end) test("timer Get returns a non-negative number after Start", function() local t = multi:newTimer() t:Start() local elapsed = t:Get() assert_type(elapsed, "number") assert_truthy(elapsed >= 0) end) test("timer Pause freezes elapsed time", function() local t = multi:newTimer() t:Start() -- busy-wait briefly local deadline = os.clock() + 0.02 while os.clock() < deadline do end t:Pause() local frozen = t:Get() local deadline2 = os.clock() + 0.02 while os.clock() < deadline2 do end local after = t:Get() assert_eq(frozen, after, "Paused timer should not advance") assert_truthy(t:isPaused(), "isPaused should return true") end) test("timer Resume resumes counting", function() local t = multi:newTimer() t:Start() local deadline = os.clock() + 0.01 while os.clock() < deadline do end t:Pause() local before = t:Get() t:Resume() local deadline2 = os.clock() + 0.02 while os.clock() < deadline2 do end local after = t:Get() assert_truthy(after > before, "Resumed timer should advance") assert_falsy(t:isPaused()) end) test("timer Reset restarts counting from zero", function() local t = multi:newTimer() t:Start() local deadline = os.clock() + 0.02 while os.clock() < deadline do end t:Reset() local after_reset = t:Get() assert_truthy(after_reset < 0.05, "Timer should be near zero after Reset") end) -- ═════════════════════════════════════════════ -- 7. SCHEDULER ACTORS -- ═════════════════════════════════════════════ section("Scheduler Actors") test("newLoop creates a loop object and fires OnLoop", function() local fires = 0 local loop = multi:newLoop(function() fires = fires + 1 end) run_until(function() return fires >= 3 end, 1000) assert_truthy(fires >= 3, "Loop should have fired at least 3 times, got " .. fires) loop:Destroy() end) test("newLoop Pause stops firing", function() local fires = 0 local loop = multi:newLoop(function() fires = fires + 1 end) run_until(function() return fires >= 2 end, 1000) loop:Pause() local snapshot = fires run_until(nil, 100) assert_eq(fires, snapshot, "Paused loop should not fire") loop:Destroy() end) test("newLoop Resume restarts firing", function() local fires = 0 local loop = multi:newLoop(function() fires = fires + 1 end) run_until(function() return fires >= 2 end, 1000) loop:Pause() local before = fires run_until(nil, 50) loop:Resume() run_until(function() return fires >= before + 2 end, 1000) assert_truthy(fires >= before + 2, "Resumed loop should fire again") loop:Destroy() end) test("newUpdater fires OnUpdate based on skip interval", function() local fires = 0 local updater = multi:newUpdater(1, function() fires = fires + 1 end) run_until(function() return fires >= 3 end, 2000) assert_truthy(fires >= 3) updater:Destroy() end) test("newAlarm fires OnRing after timeout", function() local rang = false local alarm = multi:newAlarm(0, function() rang = true end) -- 0-second alarm fires immediately run_until(function() return rang end, 500) assert_truthy(rang, "Alarm should have rung") end) test("newAlarm does not ring before timeout", function() local rang = false local alarm = multi:newAlarm(9999, function() rang = true end) run_until(nil, 100) assert_falsy(rang, "Alarm should not have rung yet") alarm:Destroy() end) test("newAlarm Reset re-arms the alarm", function() local ring_count = 0 local alarm = multi:newAlarm(0, function() ring_count = ring_count + 1 end) run_until(function() return ring_count >= 1 end, 500) alarm:Reset() run_until(function() return ring_count >= 2 end, 500) assert_truthy(ring_count >= 2, "Alarm should ring again after Reset") end) test("newTLoop fires periodically", function() local fires = 0 local tloop = multi:newTLoop(function() fires = fires + 1 end, 0) run_until(function() return fires >= 3 end, 5000) assert_truthy(fires >= 3, "TLoop should fire multiple times, got " .. fires) tloop:Destroy() end) test("newStep fires OnStep for each step", function() local steps = {} local s = multi:newStep(1, 4, 1) s.OnStep(function(self, pos) table.insert(steps, pos) end) run_until(function() return #steps >= 3 end, 5000) assert_truthy(#steps >= 3) assert_eq(steps[1], 1) assert_eq(steps[2], 2) s:Destroy() end) test("newStep fires OnEnd when reaching the end", function() local ended = false local s = multi:newStep(1, 3, 1) s.OnEnd(function() ended = true end) run_until(function() return ended end, 5000) assert_truthy(ended, "Step should have fired OnEnd") end) test("newEvent fires OnEvent when task returns truthy", function() local done = false local tick = 0 local ev = multi:newEvent(function() tick = tick + 1 if tick >= 3 then return true end end, function() done = true end) run_until(function() return done end, 5000) assert_truthy(done, "Event should have fired") end) test("newEvent does not fire when task returns falsy", function() local done = false local ev = multi:newEvent(function() return false end, function() done = true end) run_until(nil, 200) assert_falsy(done, "Event should not have fired") ev:Destroy() end) -- ═════════════════════════════════════════════ -- 8. OBJECT LIFECYCLE (Pause / Resume / Destroy) -- ═════════════════════════════════════════════ section("Object Lifecycle") test("Pause sets Active to false", function() local loop = multi:newLoop(function() end) assert_truthy(loop.Active) loop:Pause() assert_falsy(loop.Active) loop:Destroy() end) test("isPaused returns correct state", function() local loop = multi:newLoop(function() end) assert_falsy(loop:isPaused()) loop:Pause() assert_truthy(loop:isPaused()) loop:Resume() assert_falsy(loop:isPaused()) loop:Destroy() end) test("isActive returns correct state", function() local loop = multi:newLoop(function() end) assert_truthy(loop:isActive()) loop:Pause() assert_falsy(loop:isActive()) loop:Destroy() end) test("Destroy removes object from Mainloop", function() local before = #multi.Mainloop local loop = multi:newLoop(function() end) assert_eq(#multi.Mainloop, before + 1) loop:Destroy() assert_eq(#multi.Mainloop, before, "Destroyed object should be removed from Mainloop") end) test("isDone returns true after Pause", function() local loop = multi:newLoop(function() end) loop:Pause() assert_truthy(loop:isDone()) loop:Destroy() end) test("setName sets the Name field", function() local loop = multi:newLoop(function() end) loop:setName("MyTestLoop") assert_eq(loop.Name, "MyTestLoop") loop:Destroy() end) test("reallocate moves object to another processor", function() local proc = multi:newProcessor("TestReallocProc", {Start = true}) local loop = multi:newLoop(function() end) local before_main = #multi.Mainloop loop:reallocate(proc) assert_eq(#multi.Mainloop, before_main - 1, "Loop should be removed from main Mainloop") assert_truthy(#proc.Mainloop >= 1, "Loop should be in proc Mainloop") proc:Destroy() end) -- ═════════════════════════════════════════════ -- 9. PROCESSOR -- ═════════════════════════════════════════════ section("Processor") test("newProcessor returns a process object", function() local proc = multi:newProcessor("TestProc1", {Start = false}) assert_eq(proc.Type, multi.registerType("process", "processes")) assert_type(proc.Mainloop, "table") proc:Destroy() end) test("processor Start/Stop toggles active state", function() local proc = multi:newProcessor("TestProc2", {Start = false}) assert_falsy(proc.isActive()) proc.Start() assert_truthy(proc.isActive()) proc.Stop() assert_falsy(proc.isActive()) proc:Destroy() end) test("processor run() executes when active", function() local fires = 0 local proc = multi:newProcessor("TestProc3", {Start = true}) proc:newLoop(function() fires = fires + 1 end) for _ = 1, 10 do proc.run() end assert_truthy(fires > 0, "Processor run() should execute objects") proc:Destroy() end) test("processor run() is a no-op when stopped", function() local fires = 0 local proc = multi:newProcessor("TestProc4", {Start = false}) proc:newLoop(function() fires = fires + 1 end) for _ = 1, 10 do proc.run() end assert_eq(fires, 0, "Stopped processor should not run objects") proc:Destroy() end) test("processor getName and getFullName", function() local proc = multi:newProcessor("MyNamedProc", {Start = false}) assert_eq(proc:getName(), "MyNamedProc") local full = proc:getFullName() assert_truthy(full:find("MyNamedProc"), "getFullName should contain processor name") proc:Destroy() end) test("processor MaxObjects constraint", function() local proc = multi:newProcessor("MaxObjProc", {Start = false, MaxObjects = 2}) local a = proc:newLoop(function() end) local b = proc:newLoop(function() end) local c, err = proc:newLoop(function() end) assert_truthy(err, "Should return error when MaxObjects exceeded") proc:Destroy() end) test("getProcessors returns a list containing created processors", function() local procs_before = #multi:getProcessors() local proc = multi:newProcessor("GetProcsTest", {Start = false}) local procs_after = #multi:getProcessors() assert_truthy(procs_after > procs_before) proc:Destroy() end) -- ═════════════════════════════════════════════ -- 10. THREADING -- ═════════════════════════════════════════════ section("Threading") test("thread.isThread returns false outside a thread", function() assert_falsy(thread.isThread(), "Should return false in main coroutine") end) test("newThread creates a thread and it runs", function() local done = false thread:newThread("TestThread1", function() done = true end) run_until(function() return done end, 5000) assert_truthy(done, "Thread should have run") end) test("thread OnDeath fires when thread finishes", function() local death_fired = false local t = thread:newThread("TestThread_Death", function() return "finished" end) t.OnDeath(function(val) death_fired = true end) run_until(function() return death_fired end, 5000) assert_truthy(death_fired) end) test("thread OnDeath receives return values", function() local result local t = thread:newThread("TestThread_Ret", function() return 42, "hello" end) t.OnDeath(function(a, b) result = {a, b} end) run_until(function() return result ~= nil end, 5000) assert_truthy(result) assert_eq(result[1], 42) assert_eq(result[2], "hello") end) test("thread.sleep suspends for approximately the given time", function() local start_t = os.clock() local done = false thread:newThread("SleepThread", function() thread.sleep(0.05) done = true end) run_until(function() return done end, 100000) local elapsed = os.clock() - start_t assert_truthy(done) assert_truthy(elapsed >= 0.04, "Should have slept at least ~0.05s, elapsed=" .. elapsed) end) test("thread.hold waits for condition", function() local flag = false local saw_flag = false thread:newThread("HoldThread", function() thread.hold(function() return flag end) saw_flag = true end) -- Run without setting flag first run_until(nil, 200) assert_falsy(saw_flag, "Thread should still be waiting") flag = true run_until(function() return saw_flag end, 5000) assert_truthy(saw_flag, "Thread should have proceeded after flag set") end) test("thread.hold on a connection waits for Fire", function() local conn = multi:newConnection() local received thread:newThread("HoldConnThread", function() local val = thread.hold(conn) received = val end) run_until(nil, 100) assert_falsy(received, "Should still be waiting") conn:Fire(99) run_until(function() return received ~= nil end, 5000) assert_eq(received, 99) conn:Destroy() end) test("thread.skip skips N scheduler ticks", function() local done = false local ticks_before = 0 local loop = multi:newLoop(function() ticks_before = ticks_before + 1 end) thread:newThread("SkipThread", function() thread.skip(5) done = true end) run_until(function() return done end, 5000) assert_truthy(done) loop:Destroy() end) test("thread GlobalVariables set and get", function() thread.set("test_gvar_key", "hello_global") assert_eq(thread.get("test_gvar_key"), "hello_global") end) test("thread.waitFor blocks until variable is set", function() local got local key = "waitfor_test_" .. multi.randomString(6) thread:newThread("WaitForSetter", function() thread.sleep(0) thread.set(key, "ready") end) thread:newThread("WaitForWaiter", function() got = thread.waitFor(key) end) run_until(function() return got ~= nil end, 5000) assert_eq(got, "ready") end) test("newFunction wraps a thread and supports wait()", function() local fn = thread:newFunction(function(x) return x * 2 end, true) -- holdme = true, so calling blocks until result local result thread:newThread("newFnCaller", function() result = fn(21) end) run_until(function() return result ~= nil end, 5000) assert_eq(result, 42) end) test("newFunction with holdme=false returns future table", function() local fn = thread:newFunction(function() return "async_result" end, false) local future = fn() assert_type(future, "table") assert_truthy(future.wait, "Future should have a wait method") end) -- ═════════════════════════════════════════════ -- 11. PRIORITY SYSTEM -- ═════════════════════════════════════════════ section("Priority System") test("setPriority accepts string 'normal'", function() local loop = multi:newLoop(function() end) loop:setPriority("normal") assert_eq(loop.Priority, multi.Priority_Normal) loop:Destroy() end) test("setPriority accepts string shortcuts", function() local loop = multi:newLoop(function() end) local cases = { {"c", multi.Priority_Core}, {"vh", multi.Priority_Very_High}, {"h", multi.Priority_High}, {"a", multi.Priority_Above_Normal}, {"n", multi.Priority_Normal}, {"b", multi.Priority_Below_Normal}, {"l", multi.Priority_Low}, {"vl", multi.Priority_Very_Low}, {"i", multi.Priority_Idle}, } for _, case in ipairs(cases) do loop:setPriority(case[1]) assert_eq(loop.Priority, case[2], "Priority mismatch for shortcut '" .. case[1] .. "'") end loop:Destroy() end) test("setPriority accepts numeric values", function() local loop = multi:newLoop(function() end) loop:setPriority(64) assert_eq(loop.Priority, 64) loop:Destroy() end) test("ResetPriority restores the default priority", function() local loop = multi:newLoop(function() end) loop:setPriority("normal") local default = loop.Priority loop:setPriority("idle") assert_eq(loop.Priority, multi.Priority_Idle) loop:ResetPriority() assert_eq(loop.Priority, default, "Priority should be restored to default") loop:Destroy() end) test("PriorityResolve maps priority values to names", function() assert_eq(multi.PriorityResolve[multi.Priority_Normal], "Normal") assert_eq(multi.PriorityResolve[multi.Priority_High], "High") assert_eq(multi.PriorityResolve[multi.Priority_Idle], "Idle") end) -- ═════════════════════════════════════════════ -- 12. STATS & INTROSPECTION -- ═════════════════════════════════════════════ section("Stats & Introspection") test("getStats returns a table with root entry", function() local stats = multi:getStats() assert_type(stats, "table") assert_truthy(stats["root"], "Stats should contain 'root' entry") assert_type(stats["root"].connections, "number") assert_type(stats["root"].subscriptions, "number") end) test("getChildren returns the Mainloop", function() local children = multi:getChildren() assert_eq(children, multi.Mainloop) end) test("getVersion returns the library version string", function() assert_eq(multi:getVersion(), multi.Version) end) test("getRunners excludes internal process threads", function() local runners = multi:getRunners() assert_type(runners, "table") for _, r in ipairs(runners) do assert_falsy(r.__ignore, "Runners should not include __ignore objects") end end) test("getCurrentProcess returns multi at top level", function() assert_eq(multi.getCurrentProcess(), multi) end) -- ═════════════════════════════════════════════ -- 13. OS UTILITIES -- ═════════════════════════════════════════════ section("OS Utilities") test("os.getOS returns 'windows' or 'unix'", function() local os_name = os.getOS() assert_truthy(os_name == "windows" or os_name == "unix", "os.getOS should return 'windows' or 'unix', got: " .. tostring(os_name)) end) test("os.sleep is defined (just check it exists)", function() assert_type(os.sleep, "function") end) -- ═════════════════════════════════════════════ -- 14. TABLE UTILITIES -- ═════════════════════════════════════════════ section("Table Utilities (table.merge)") test("table.merge merges non-overlapping keys", function() local t1 = {a = 1} local t2 = {b = 2} table.merge(t1, t2) assert_eq(t1.a, 1) assert_eq(t1.b, 2) end) test("table.merge overwrites with t2 values on conflict", function() local t1 = {a = 1, b = "old"} local t2 = {b = "new", c = 3} table.merge(t1, t2) assert_eq(t1.b, "new") assert_eq(t1.c, 3) end) test("table.merge recurses into nested tables", function() local t1 = {nested = {x = 1}} local t2 = {nested = {y = 2}} table.merge(t1, t2) assert_eq(t1.nested.x, 1) assert_eq(t1.nested.y, 2) end) test("table.merge returns t1", function() local t1 = {a = 1} local result = table.merge(t1, {b = 2}) assert_eq(result, t1) end) -- ═════════════════════════════════════════════ -- 15. SCHEDULED JOBS / TASKS -- ═════════════════════════════════════════════ section("Task Queue") test("newTask adds a function to the task queue", function() local ran = false multi:newTask(function() ran = true end) -- Tasks are processed by the Task Handler thread inside threadManager run_until(function() return ran end, 5000) assert_truthy(ran, "Task should have been run") end) test("multiple tasks run in order", function() local order = {} multi:newTask(function() table.insert(order, 1) end) multi:newTask(function() table.insert(order, 2) end) multi:newTask(function() table.insert(order, 3) end) run_until(function() return #order >= 3 end, 5000) assert_eq(#order, 3) assert_eq(order[1], 1) assert_eq(order[2], 2) assert_eq(order[3], 3) end) -- ═════════════════════════════════════════════ -- 16. NIL SENTINEL -- ═════════════════════════════════════════════ section("NIL Sentinel") test("multi.NIL is a table with Type='NIL'", function() assert_type(multi.NIL, "table") assert_eq(multi.NIL.Type, "NIL") end) test("multi.NIL is distinct from Lua nil", function() assert_truthy(multi.NIL ~= nil) end) -- ───────────────────────────────────────────── -- SUMMARY -- ───────────────────────────────────────────── io.write(string.format( "\n\x1b[97m────────────────────────────────\x1b[0m\n" .. "\x1b[92m Passed : %d\x1b[0m\n" .. "\x1b[91m Failed : %d\x1b[0m\n" .. "\x1b[93m Skipped: %d\x1b[0m\n" .. "\x1b[97m────────────────────────────────\x1b[0m\n", passed, failed, skipped )) if #failures > 0 then io.write("\n\x1b[91mFailed tests:\x1b[0m\n") for _, f in ipairs(failures) do io.write(string.format(" • %s\n %s\n", f.name, f.err)) end end os.exit(failed == 0 and 0 or 1)