From ffd7ac15f6b80fbacf31764395c11004709ed01f Mon Sep 17 00:00:00 2001 From: Ryan Ward Date: Mon, 23 Mar 2026 21:52:56 -0700 Subject: [PATCH] added new unit tests --- init.lua | 19 +- tests/conf.lua | 39 -- tests/main.lua | 10 - tests/multi_test.lua | 1095 ++++++++++++++++++++++++++++++++++ tests/runtests.lua | 271 --------- tests/units/connections.lua | 71 --- tests/vscode-debuggee.lua | 1102 ----------------------------------- 7 files changed, 1107 insertions(+), 1500 deletions(-) delete mode 100644 tests/conf.lua delete mode 100644 tests/main.lua create mode 100644 tests/multi_test.lua delete mode 100644 tests/runtests.lua delete mode 100644 tests/units/connections.lua delete mode 100644 tests/vscode-debuggee.lua diff --git a/init.lua b/init.lua index d862647..d6db3ea 100644 --- a/init.lua +++ b/init.lua @@ -1421,7 +1421,9 @@ function multi:newProcessor(name, opts, priority) function c:Destroy() Active = false - c.process:Destroy() + if c.process then + c.process:Destroy() + end end function c:setTaskDelay(delay) @@ -2647,8 +2649,8 @@ function multi:benchMark(sec,p,pt) end function multi.Round(num, numDecimalPlaces) - local mult = 10^(numDecimalPlaces or 0) - return math.floor(num * mult + 0.5) / mult + local mult = 10 ^ (numDecimalPlaces or 0) + return math.floor((num * mult) + 0.5 + 1e-10) / mult end function multi.AlignTable(tab) @@ -2697,6 +2699,12 @@ end function multi:reallocate(processor, index) index=index or #processor.Mainloop+1 local int=self.Parent + for i = #int.Mainloop, 1, -1 do + if int.Mainloop[i] == self then + table.remove(int.Mainloop,i) + break + end + end self.Parent=processor if index then table.insert(processor.Mainloop, index, self) @@ -2794,11 +2802,8 @@ local function random_hex(len) end return result end - +math.randomseed(os.time()) multi.generate_uuid7 = function() - -- Seed random number generator with current time - math.randomseed(os.time()) - -- Get timestamp in milliseconds local timestamp_ms = get_timestamp_ms() diff --git a/tests/conf.lua b/tests/conf.lua deleted file mode 100644 index 4d2c4ee..0000000 --- a/tests/conf.lua +++ /dev/null @@ -1,39 +0,0 @@ -function love.conf(t) - t.identity = nil -- The name of the save directory (string) - t.version = "12.0" -- The LOVE version this game was made for (string) - t.console = true -- Attach a console (boolean, Windows only) - - t.window.title = "MultiThreadTest" -- The window title (string) - t.window.icon = nil -- Filepath to an image to use as the window's icon (string) - t.window.width = 1280 -- The window width (number) - t.window.height = 720 -- The window height (number) - - t.window.borderless = false -- Remove all border visuals from the window (boolean) - t.window.resizable = true -- Let the window be user-resizable (boolean) - t.window.minwidth = 1 -- Minimum window width if the window is resizable (number) - t.window.minheight = 1 -- Minimum window height if the window is resizable (number) - t.window.fullscreen = false -- Enable fullscreen (boolean) - t.window.fullscreentype = "desktop" -- Standard fullscreen or desktop fullscreen mode (string) - t.window.vsync = false -- Enable vertical sync (boolean) - t.window.fsaa = 2 -- The number of samples to use with multi-sampled antialiasing (number) - t.window.display = 1 -- Index of the monitor to show the window in (number) - t.window.highdpi = false -- Enable high-dpi mode for the window on a Retina display (boolean) - t.window.srgb = false -- Enable sRGB gamma correction when drawing to the screen (boolean) - t.window.x = nil -- The x-coordinate of the window's position in the specified display (number) - t.window.y = nil -- The y-coordinate of the window's position in the specified display (number) - - t.modules.audio = false -- Enable the audio module (boolean) - t.modules.event = false -- Enable the event module (boolean) - t.modules.graphics = false -- Enable the graphics module (boolean) - t.modules.image = false -- Enable the image module (boolean) - t.modules.joystick = false -- Enable the joystick module (boolean) - t.modules.keyboard = false -- Enable the keyboard module (boolean) - t.modules.math = false -- Enable the math module (boolean) - t.modules.mouse = false -- Enable the mouse module (boolean) - t.modules.physics = false -- Enable the physics module (boolean) - t.modules.sound = false -- Enable the sound module (boolean) - t.modules.system = false -- Enable the system module (boolean) - t.modules.timer = false -- Enable the timer module (boolean) - t.modules.window = false -- Enable the window module (boolean) - t.modules.thread = true -- Enable the thread module (boolean) -end \ No newline at end of file diff --git a/tests/main.lua b/tests/main.lua deleted file mode 100644 index edeee3a..0000000 --- a/tests/main.lua +++ /dev/null @@ -1,10 +0,0 @@ -package.path = "../?/init.lua;../?.lua;"..package.path - -if os.getenv("LOCAL_LUA_DEBUGGER_VSCODE") == "1" then - require("lldebugger").start() -end - -GLOBAL, THREAD = require("multi.integration.loveManager"):init() - -require("runtests") -require("threadtests") diff --git a/tests/multi_test.lua b/tests/multi_test.lua new file mode 100644 index 0000000..1e64819 --- /dev/null +++ b/tests/multi_test.lua @@ -0,0 +1,1095 @@ +--[[ + 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) diff --git a/tests/runtests.lua b/tests/runtests.lua deleted file mode 100644 index f7b5109..0000000 --- a/tests/runtests.lua +++ /dev/null @@ -1,271 +0,0 @@ -package.path = "../?/init.lua;../?.lua;./init.lua;./?.lua;" .. package.path - -local multi, thread = require("multi"):init{print=true,warn=true,error=true}--{priority=true} -local good = false -local proc = multi:newProcessor("Test") - -local testing = {} - -function testing.False(value) - if value ~= false then - thread.kill("Expected value to be false") - end -end - -function testing.True(value) - if value ~= true then - thread.kill("Expected value to be true") - end -end - -function testing.IsNil(value) - if value ~= nil then - thread.kill("Expected nil, but got: " .. tostring(value) .."") - end -end - -function testing.NotNil(value) - if value == nil then - thread.kill("Expected value not to be nil.") - end -end - -function testing.Equal(expected, actual) - if expected ~= actual then - thread.kill("Not equal: \n" .. - "expected: " .. tostring(expected) .. "\n" .. - "actual : " .. tostring(actual) .. "\n") - end -end - -function testing.NotEqual(expected, actual) - if expected == actual then - thread.kill("Should not be: values are equal") - end -end - -function getOS() - -- ask LuaJIT first - if jit then - return jit.os - end - - -- Unix, Linux variants - local fh,err = assert(io.popen("uname -o 2>/dev/null","r")) - if fh then - osname = fh:read() - end - - return osname or "Windows" -end - -function ListFiles(path) - files = {} - if getOS() == "Windows" then - for dir in io.popen([[dir /A-d "]] .. path .. [[" /b]]):lines() do table.insert(files,dir) end - else - for dir in io.popen([[ls -pa "]] .. path .. [[" | grep -v /]]):lines() do table.insert(files,dir) end - end - return files -end - -print("Version: "..multi.Version) - -proc.Start() - -proc:newAlarm(3):OnRing(function() - good = true -end) - -runTest = thread:newFunction(function() - -- local alarms,tsteps,steps,loops,tloops,updaters,events=false,0,0,0,0,0,false - -- multi.print("Testing Basic Features. If this fails most other features will probably not work!") - -- proc:newAlarm(2):OnRing(function(a) - -- alarms = true - -- a:Destroy() - -- end) - -- proc:newTStep(1,10,1,.1):OnStep(function(t) - -- tsteps = tsteps + 1 - -- end):OnEnd(function(step) - -- step:Destroy() - -- end) - -- proc:newStep(1,10):OnStep(function(s) - -- steps = steps + 1 - -- end):OnEnd(function(step) - -- step:Destroy() - -- end) - -- local loop = proc:newLoop(function(l) - -- loops = loops + 1 - -- end) - -- proc:newTLoop(function(t) - -- tloops = tloops + 1 - -- end,.1) - -- local updater = proc:newUpdater(1):OnUpdate(function() - -- updaters = updaters + 1 - -- end) - -- local event = proc:newEvent(function() - -- return alarms - -- end) - -- event.OnEvent(function(evnt) - -- evnt:Destroy() - -- events = true - -- multi.success("Alarms: Ok") - -- multi.success("Events: Ok") - -- if tsteps == 10 then multi.success("TSteps: Ok") else multi.error("TSteps: Bad!") end - -- if steps == 10 then multi.success("Steps: Ok") else multi.error("Steps: Bad!") end - -- if loops > 100 then multi.success("Loops: Ok") else multi.error("Loops: Bad!") end - -- if tloops > 10 then multi.success("TLoops: Ok") else multi.error("TLoops: Bad!") end - -- if updaters > 100 then multi.success("Updaters: Ok") else multi.error("Updaters: Bad!") end - -- end) - -- thread.hold(event.OnEvent) - -- multi.print("Starting Connection and Thread tests!") - -- func = thread:newFunction(function(count) - -- multi.print("Starting Status test: ",count) - -- local a = 0 - -- while true do - -- a = a + 1 - -- thread.sleep(.1) - -- thread.pushStatus(a,count) - -- if a == count then break end - -- end - -- return "Done", true, math.random(1,10000) - -- end) - -- local ret = func(10) - -- local ret2 = func(15) - -- local ret3 = func(20) - -- local s1,s2,s3 = 0,0,0 - -- ret.OnError(function(...) - -- multi.error("Func 1:",...) - -- end) - -- ret2.OnError(function(...) - -- multi.error("Func 2:",...) - -- end) - -- ret3.OnError(function(...) - -- multi.error("Func 3:",...) - -- end) - -- ret.OnStatus(function(part,whole) - -- s1 = math.ceil((part/whole)*1000)/10 - -- end) - -- ret2.OnStatus(function(part,whole) - -- s2 = math.ceil((part/whole)*1000)/10 - -- end) - -- ret3.OnStatus(function(part,whole) - -- s3 = math.ceil((part/whole)*1000)/10 - -- end) - - -- ret.OnReturn(function(...) - -- multi.success("Done 1",...) - -- end) - -- ret2.OnReturn(function(...) - -- multi.success("Done 2",...) - -- end) - -- ret3.OnReturn(function(...) - -- multi.success("Done 3",...) - -- end) - - -- local err, timeout = thread.hold(ret.OnReturn * ret2.OnReturn * ret3.OnReturn) - - -- if s1 == 100 and s2 == 100 and s3 == 100 then - -- multi.success("Threads: All tests Ok") - -- else - -- if s1>0 and s2>0 and s3 > 0 then - -- multi.success("Thread OnStatus: Ok") - -- else - -- multi.error("Threads OnStatus or thread.hold(conn) Error!") - -- end - -- if timeout then - -- multi.error("Connection Error!") - -- else - -- multi.success("Connection Test 1: Ok") - -- end - -- multi.error("Connection holding Error!") - -- end - - -- conn1 = proc:newConnection() - -- conn2 = proc:newConnection() - -- conn3 = proc:newConnection() - -- local c1,c2,c3,c4 = false,false,false,false - - -- local a = conn1(function() - -- c1 = true - -- end) - - -- local b = conn2(function() - -- c2 = true - -- end) - - -- local c = conn3(function() - -- c3 = true - -- end) - - -- local d = conn3(function() - -- c4 = true - -- end) - - -- conn1:Fire() - -- conn2:Fire() - -- conn3:Fire() - - -- if c1 and c2 and c3 and c4 then - -- multi.success("Connection Test 2: Ok") - -- else - -- multi.error("Connection Test 2: Error") - -- end - -- c3 = false - -- c4 = false - -- conn3:Unconnect(d) - -- conn3:Fire() - -- if c3 and not(c4) then - -- multi.success("Connection Test 3: Ok") - -- else - -- multi.error("Connection Test 3: Error removing connection") - -- end - units = ListFiles("tests/units") - for _, file in pairs(units) do - multi.print("Running tests in: " .. file) - unit = loadfile("tests/units/" .. file)() - function Test(...) - unit.Test(...) - multi.success(file) - end - func = thread:newFunction(Test, true) - func(multi:newProcessor(file), thread, testing) - end - if not love then - local ec = 0 - multi.print("Testing pseudo threading") - capture = io.popen("lua tests/threadtests.lua p"):read("*a") - if capture:lower():match("error") then - ec = ec + 1 - os.exit(1) - else - io.write(capture) - end - multi.print("Testing lanes threading") - capture = io.popen("lua tests/threadtests.lua l"):read("*a") - if capture:lower():match("error") then - ec = ec + 1 - os.exit(1) - else - io.write(capture) - end - os.exit(0) - end -end) - -local handle = runTest() - -handle.OnError(function(...) - multi.error("Something went wrong with the test!") - print(...) -end) - -if not love then - multi:mainloop() -else - local hold = thread:newFunction(function() - thread.hold(handle.OnError + handle.OnReturn) - end, true) - hold() - multi.print("Starting Threading tests!") -end \ No newline at end of file diff --git a/tests/units/connections.lua b/tests/units/connections.lua deleted file mode 100644 index bfbf8f8..0000000 --- a/tests/units/connections.lua +++ /dev/null @@ -1,71 +0,0 @@ -function Test(multi, thread, t) - multi.print("Testing Connection operators") - - do - multi.print("Testing: conn1 + conn2") - local conn1 = multi:newConnection() - local conn2 = multi:newConnection() - local conn3 = conn1 + conn2 - local count = 0 - - conn3(function() - count = count + 1 - end) - - conn1:Fire() - t.Equal(1, count) - conn2:Fire() - t.Equal(2, count) - end - - do - multi.print("Testing: conn1 * conn2") - local conn1 = multi:newConnection() - local conn2 = multi:newConnection() - local conn3 = conn1 * conn2 - local count = 0 - - conn3(function() - count = count + 1 - end) - - conn1:Fire() - t.Equal(0, count) - conn2:Fire() - t.Equal(1, count) - end - - do - multi.print("Testing: conn .. function") - local called = false - local conn1 = multi:newConnection() - local conn2 = conn1 .. function() called = true end - - conn1(function() - t.False(called) - end) - - conn2:Fire() - t.True(called) - end - - do - multi.print("function .. conn") - local status = false - local conn1 = multi:newConnection() - local conn2 = function(test) return test end .. conn1 - - conn1(function() - status = true - end) - - conn2:Fire(false) - t.False(status) - conn2:Fire(true) - t.True(status) - end -end - -return { - Test = Test -} \ No newline at end of file diff --git a/tests/vscode-debuggee.lua b/tests/vscode-debuggee.lua deleted file mode 100644 index 9b76276..0000000 --- a/tests/vscode-debuggee.lua +++ /dev/null @@ -1,1102 +0,0 @@ -local debuggee = {} - -local socket = require 'socket.core' -local json -local handlers = {} -local sock -local directorySeperator = package.config:sub(1,1) -local sourceBasePath = '.' -local storedVariables = {} -local nextVarRef = 1 -local baseDepth -local breaker -local sendEvent -local dumpCommunication = false -local ignoreFirstFrameInC = false -local debugTargetCo = nil -local redirectedPrintFunction = nil - -local onError = nil -local addUserdataVar = nil - -local function defaultOnError(e) - print('****************************************************') - print(e) - print('****************************************************') -end - -local function valueToString(value, depth) - local str = '' - depth = depth or 0 - local t = type(value) - if t == 'table' then - str = str .. '{\n' - for k, v in pairs(value) do - str = str .. string.rep(' ', depth + 1) .. '[' .. valueToString(k) ..']' .. ' = ' .. valueToString(v, depth + 1) .. ',\n' - end - str = str .. string.rep(' ', depth) .. '}' - elseif t == 'string' then - str = str .. '"' .. tostring(value) .. '"' - else - str = str .. tostring(value) - end - return str -end - -------------------------------------------------------------------------------- -local sethook = debug.sethook -debug.sethook = nil - -local cocreate = coroutine.create -coroutine.create = function(f) - local c = cocreate(f) - debuggee.addCoroutine(c) - return c -end - -------------------------------------------------------------------------------- -local function debug_getinfo(depth, what) - if debugTargetCo then - return debug.getinfo(debugTargetCo, depth, what) - else - return debug.getinfo(depth + 1, what) - end -end - -------------------------------------------------------------------------------- -local function debug_getlocal(depth, i) - if debugTargetCo then - return debug.getlocal(debugTargetCo, depth, i) - else - return debug.getlocal(depth + 1, i) - end -end - -------------------------------------------------------------------------------- -local DO_TEST = false - -------------------------------------------------------------------------------- --- chunkname matching {{{ -local function getMatchCount(a, b) - local n = math.min(#a, #b) - for i = 0, n - 1 do - if a[#a - i] == b[#b - i] then - -- pass - else - return i - end - end - return n -end -if DO_TEST then - assert(getMatchCount({'a','b','c'}, {'a','b','c'}) == 3) - assert(getMatchCount({'b','c'}, {'a','b','c'}) == 2) - assert(getMatchCount({'a','b','c'}, {'b','c'}) == 2) - assert(getMatchCount({}, {'a','b','c'}) == 0) - assert(getMatchCount({'a','b','c'}, {}) == 0) - assert(getMatchCount({'a','b','c'}, {'a','b','c','d'}) == 0) -end - -local function splitChunkName(s) - if string.sub(s, 1, 1) == '@' then - s = string.sub(s, 2) - end - - local a = {} - for word in string.gmatch(s, '[^/\\]+') do - a[#a + 1] = string.lower(word) - end - return a -end -if DO_TEST then - local a = splitChunkName('@.\\vscode-debuggee.lua') - assert(#a == 2) - assert(a[1] == '.') - assert(a[2] == 'vscode-debuggee.lua') - - local a = splitChunkName('@C:\\dev\\VSCodeLuaDebug\\debuggee/lua\\socket.lua') - assert(#a == 6) - assert(a[1] == 'c:') - assert(a[2] == 'dev') - assert(a[3] == 'vscodeluadebug') - assert(a[4] == 'debuggee') - assert(a[5] == 'lua') - assert(a[6] == 'socket.lua') - - local a = splitChunkName('@main.lua') - assert(#a == 1) - assert(a[1] == 'main.lua') -end --- chunkname matching }}} - --- path control {{{ -local Path = {} - -function Path.isAbsolute(a) - local firstChar = string.sub(a, 1, 1) - if firstChar == '/' or firstChar == '\\' then - return true - end - - if string.match(a, '^%a%:[/\\]') then - return true - end - - return false -end - -local np_pat1, np_pat2 = ('[^SEP:]+SEP%.%.SEP?'):gsub('SEP', directorySeperator), ('SEP+%.?SEP'):gsub('SEP', directorySeperator) -function Path.normpath(path) - path = path:gsub('[/\\]', directorySeperator) - - if directorySeperator == '\\' then - local unc = ('SEPSEP'):gsub('SEP', directorySeperator) -- UNC - if path:match('^'..unc) then - return unc..Path.normpath(path:sub(3)) - end - end - - local k - repeat -- /./ -> / - path,k = path:gsub(np_pat2, directorySeperator) - until k == 0 - repeat -- A/../ -> (empty) - path,k = path:gsub(np_pat1, '', 1) - until k == 0 - if path == '' then - path = '.' - end - return path -end - -function Path.concat(a, b) - -- normalize a - local lastChar = string.sub(a, #a, #a) - if not (lastChar == '/' or lastChar == '\\') then - a = a .. directorySeperator - end - - -- normalize b - if string.match(b, '^%.%\\') or string.match(b, '^%.%/') then - b = string.sub(b, 3) - end - - return a .. b -end - -function Path.toAbsolute(base, sub) - if Path.isAbsolute(sub) then - return Path.normpath(sub) - else - return Path.normpath(Path.concat(base, sub)) - end -end - -if DO_TEST then - assert(Path.isAbsolute('c:\\asdf\\afsd')) - assert(Path.isAbsolute('c:/asdf/afsd')) - if directorySeperator == '\\' then - assert(Path.toAbsolute('c:\\asdf', 'fdsf') == 'c:\\asdf\\fdsf') - assert(Path.toAbsolute('c:\\asdf', '.\\fdsf') == 'c:\\asdf\\fdsf') - assert(Path.toAbsolute('c:\\asdf', '..\\fdsf') == 'c:\\fdsf') - assert(Path.toAbsolute('c:\\asdf', 'c:\\fdsf') == 'c:\\fdsf') - assert(Path.toAbsolute('c:/asdf', '../fdsf') == 'c:\\fdsf') - assert(Path.toAbsolute('\\\\HOST\\asdf', '..\\fdsf') == '\\\\HOST\\fdsf') - elseif directorySeperator == '/' then - assert(Path.toAbsolute('/usr/bin/asdf', 'fdsf') == '/usr/bin/asdf/fdsf') - assert(Path.toAbsolute('/usr/bin/asdf', './fdsf') == '/usr/bin/asdf/fdsf') - assert(Path.toAbsolute('/usr/bin/asdf', '../fdsf') == '/usr/bin/fdsf') - assert(Path.toAbsolute('/usr/bin/asdf', '/usr/bin/fdsf') == '/usr/bin/fdsf') - assert(Path.toAbsolute('\\usr\\bin\\asdf', '..\\fdsf') == '/usr/bin/fdsf') - end -end --- path control }}} - -local coroutineSet = {} -setmetatable(coroutineSet, { __mode = 'v' }) - -------------------------------------------------------------------------------- --- network utility {{{ -local function sendFully(str) - local first = 1 - while first <= #str do - local sent = sock:send(str, first) - if sent and sent > 0 then - first = first + sent; - else - error('sock:send() returned < 0') - end - end -end - --- send log to debug console -local function logToDebugConsole(output, category) - local dumpMsg = { - event = 'output', - type = 'event', - body = { - category = category or 'console', - output = output - } - } - local dumpBody = json.encode(dumpMsg) - sendFully('#' .. #dumpBody .. '\n' .. dumpBody) -end - --- pure mode {{{ -local function createHaltBreaker() - -- chunkname matching { - local loadedChunkNameMap = {} - for chunkname, _ in pairs(debug.getchunknames()) do - loadedChunkNameMap[chunkname] = splitChunkName(chunkname) - end - - local function findMostSimilarChunkName(path) - local splitedReqPath = splitChunkName(path) - local maxMatchCount = 0 - local foundChunkName = nil - for chunkName, splitted in pairs(loadedChunkNameMap) do - local count = getMatchCount(splitedReqPath, splitted) - if (count > maxMatchCount) then - maxMatchCount = count - foundChunkName = chunkName - end - end - return foundChunkName - end - -- chunkname matching } - - local lineBreakCallback = nil - local function updateCoroutineHook(c) - if lineBreakCallback then - sethook(c, lineBreakCallback, 'l') - else - sethook(c) - end - end - local function sethalt(cname, ln) - for i = ln, ln + 10 do - if debug.sethalt(cname, i) then - return i - end - end - return nil - end - return { - setBreakpoints = function(path, lines) - local foundChunkName = findMostSimilarChunkName(path) - local verifiedLines = {} - - if foundChunkName then - debug.clearhalt(foundChunkName) - for _, ln in ipairs(lines) do - verifiedLines[ln] = sethalt(foundChunkName, ln) - end - end - - return verifiedLines - end, - - setLineBreak = function(callback) - if callback then - sethook(callback, 'l') - else - sethook() - end - - lineBreakCallback = callback - for cid, c in pairs(coroutineSet) do - updateCoroutineHook(c) - end - end, - - coroutineAdded = function(c) - updateCoroutineHook(c) - end, - - stackOffset = - { - enterDebugLoop = 6, - halt = 6, - step = 4, - stepDebugLoop = 6 - } - } -end - -local function createPureBreaker() - local lineBreakCallback = nil - local breakpointsPerPath = {} - local chunknameToPathCache = {} - - local function chunkNameToPath(chunkname) - local cached = chunknameToPathCache[chunkname] - if cached then - return cached - end - - local splitedReqPath = splitChunkName(chunkname) - local maxMatchCount = 0 - local foundPath = nil - for path, _ in pairs(breakpointsPerPath) do - local splitted = splitChunkName(path) - local count = getMatchCount(splitedReqPath, splitted) - if (count > maxMatchCount) then - maxMatchCount = count - foundPath = path - end - end - - if foundPath then - chunknameToPathCache[chunkname] = foundPath - end - return foundPath - end - - local entered = false - local function hookfunc() - if entered then return false end - entered = true - - if lineBreakCallback then - lineBreakCallback() - end - - local info = debug_getinfo(2, 'Sl') - if info then - local path = chunkNameToPath(info.source) - if path then - path = string.lower(path) - end - local bpSet = breakpointsPerPath[path] - if bpSet and bpSet[info.currentline] then - _G.__halt__() - end - end - - entered = false - end - sethook(hookfunc, 'l') - - return { - setBreakpoints = function(path, lines) - local t = {} - local verifiedLines = {} - for _, ln in ipairs(lines) do - t[ln] = true - verifiedLines[ln] = ln - end - if path then - path = string.lower(path) - end - breakpointsPerPath[path] = t - return verifiedLines - end, - - setLineBreak = function(callback) - lineBreakCallback = callback - end, - - coroutineAdded = function(c) - sethook(c, hookfunc, 'l') - end, - - stackOffset = - { - enterDebugLoop = 6, - halt = 7, - step = 4, - stepDebugLoop = 7 - } - } -end --- pure mode }}} - - --- 센드는 블럭이어도 됨. -local function sendMessage(msg) - local body = json.encode(msg) - - if dumpCommunication then - logToDebugConsole('[SENDING] ' .. valueToString(msg)) - end - - sendFully('#' .. #body .. '\n' .. body) -end - --- 리시브는 블럭이 아니어야 할 거 같은데... 음... 블럭이어도 괜찮나? -local function recvMessage() - local header = sock:receive('*l') - if (header == nil) then - -- 디버거가 떨어진 상황 - return nil - end - if (string.sub(header, 1, 1) ~= '#') then - error('헤더 이상함:' .. header) - end - - local bodySize = tonumber(header:sub(2)) - local body = sock:receive(bodySize) - - return json.decode(body) -end --- network utility }}} - -------------------------------------------------------------------------------- -local function debugLoop() - storedVariables = {} - nextVarRef = 1 - while true do - local msg = recvMessage() - if msg then - if dumpCommunication then - logToDebugConsole('[RECEIVED] ' .. valueToString(msg), 'stderr') - end - - local fn = handlers[msg.command] - if fn then - local rv = fn(msg) - - -- continue인데 break하는 게 역설적으로 느껴지지만 - -- 디버그 루프를 탈출(break)해야 정상 실행 흐름을 계속(continue)할 수 있지.. - if (rv == 'CONTINUE') then - break; - end - else - --print('UNKNOWN DEBUG COMMAND: ' .. tostring(msg.command)) - end - else - -- 디버그 중에 디버거가 떨어졌다. - -- print펑션을 리다이렉트 한경우에는 원래대로 돌려놓는다 - if redirectedPrintFunction then - _G.print = redirectedPrintFunction - end - break - end - end - storedVariables = {} - nextVarRef = 1 -end - -------------------------------------------------------------------------------- -local sockArray = {} -function debuggee.start(jsonLib, config) - json = jsonLib - assert(jsonLib) - - config = config or {} - local connectTimeout = config.connectTimeout or 5.0 - local controllerHost = config.controllerHost or 'localhost' - local controllerPort = config.controllerPort or 56789 - onError = config.onError or defaultOnError - addUserdataVar = config.addUserdataVar or function() return end - local redirectPrint = config.redirectPrint or false - dumpCommunication = config.dumpCommunication or false - ignoreFirstFrameInC = config.ignoreFirstFrameInC or false - if not config.luaStyleLog then - valueToString = function(value) return json.encode(value) end - end - - local breakerType - if debug.sethalt then - breaker = createHaltBreaker() - breakerType = 'halt' - else - breaker = createPureBreaker() - breakerType = 'pure' - end - - local err - sock, err = socket.tcp() - if not sock then error(err) end - sockArray = { sock } - if sock.settimeout then sock:settimeout(connectTimeout) end - local res, err = sock:connect(controllerHost, tostring(controllerPort)) - if not res then - sock:close() - sock = nil - return false, breakerType - end - - if sock.settimeout then sock:settimeout() end - sock:setoption('tcp-nodelay', true) - - local initMessage = recvMessage() - assert(initMessage and initMessage.command == 'welcome') - sourceBasePath = initMessage.sourceBasePath - directorySeperator = initMessage.directorySeperator - - if redirectPrint then - redirectedPrintFunction = _G.print -- 디버거가 떨어질때를 대비해서 보관한다 - _G.print = function(...) - local t = { n = select("#", ...), ... } - for i = 1, #t do - t[i] = tostring(t[i]) - end - sendEvent( - 'output', - { - category = 'stdout', - output = table.concat(t, '\t') .. '\n' -- Same as default "print" output end new line. - }) - end - end - - debugLoop() - return true, breakerType -end - -------------------------------------------------------------------------------- -function debuggee.poll() - if not sock then return end - - -- Processes commands in the queue. - -- Immediately returns when the queue is/became empty. - while true do - local r, w, e = socket.select(sockArray, nil, 0) - if e == 'timeout' then break end - - local msg = recvMessage() - if msg then - if dumpCommunication then - logToDebugConsole('[POLL-RECEIVED] ' .. valueToString(msg), 'stderr') - end - - if msg.command == 'pause' then - debuggee.enterDebugLoop(1) - return - end - - local fn = handlers[msg.command] - if fn then - local rv = fn(msg) - -- Ignores rv, because this loop never blocks except explicit pause command. - else - --print('POLL-UNKNOWN DEBUG COMMAND: ' .. tostring(msg.command)) - end - else - break - end - end -end - -------------------------------------------------------------------------------- -local function getCoroutineId(c) - -- 'thread: 011DD5B0' - -- 12345678^ - local threadIdHex = string.sub(tostring(c), 9) - return tonumber(threadIdHex, 16) -end - -------------------------------------------------------------------------------- -function debuggee.addCoroutine(c) - local cid = getCoroutineId(c) - coroutineSet[cid] = c - breaker.coroutineAdded(c) -end - -------------------------------------------------------------------------------- -local function sendSuccess(req, body) - sendMessage({ - command = req.command, - success = true, - request_seq = req.seq, - type = "response", - body = body - }) -end - -------------------------------------------------------------------------------- -local function sendFailure(req, msg) - sendMessage({ - command = req.command, - success = false, - request_seq = req.seq, - type = "response", - message = msg - }) -end - -------------------------------------------------------------------------------- -sendEvent = function(eventName, body) - sendMessage({ - event = eventName, - type = "event", - body = body - }) -end - -------------------------------------------------------------------------------- -local function currentThreadId() ---[[ - local threadId = 0 - if coroutine.running() then - end - return threadId -]] - return 0 -end - -------------------------------------------------------------------------------- -local function startDebugLoop() - sendEvent( - 'stopped', - { - reason = 'breakpoint', - threadId = currentThreadId(), - allThreadsStopped = true - }) - - local status, err = pcall(debugLoop) - if not status then - onError(err) - end -end - -------------------------------------------------------------------------------- -_G.__halt__ = function() - baseDepth = breaker.stackOffset.halt - startDebugLoop() -end - -------------------------------------------------------------------------------- -function debuggee.enterDebugLoop(depthOrCo, what) - if sock == nil then - return false - end - - if what then - sendEvent( - 'output', - { - category = 'stderr', - output = what, - }) - end - - if type(depthOrCo) == 'thread' then - baseDepth = 0 - debugTargetCo = depthOrCo - elseif type(depthOrCo) == 'table' then - baseDepth = (depthOrCo.depth or 0) - debugTargetCo = depthOrCo.co - else - baseDepth = (depthOrCo or 0) + breaker.stackOffset.enterDebugLoop - debugTargetCo = nil - end - startDebugLoop() - return true -end - -------------------------------------------------------------------------------- --- Function for printing on vscode debug console --- First parameter 'category' can colorizes print text -function debuggee.print(category, ...) - if sock == nil then - return false - end - local t = { ... } - for i = 1, #t do - t[i] = tostring(t[i]) - end - - local categoryVscodeConsole = 'stdout' - if category == 'warning' then - categoryVscodeConsole = 'console' -- yellow - elseif category == 'error' then - categoryVscodeConsole = 'stderr' -- red - elseif category == 'log' then - categoryVscodeConsole = 'stdout' -- white - end - - sendEvent( - 'output', - { - category = categoryVscodeConsole, - output = table.concat(t, '\t') .. '\n' -- Same as default "print" output end new line. - }) -end - -------------------------------------------------------------------------------- --- ★★★ https://github.com/Microsoft/vscode-debugadapter-node/blob/master/protocol/src/debugProtocol.ts -------------------------------------------------------------------------------- - -------------------------------------------------------------------------------- -function handlers.setBreakpoints(req) - local bpLines = {} - for _, bp in ipairs(req.arguments.breakpoints) do - bpLines[#bpLines + 1] = bp.line - end - - local verifiedLines = breaker.setBreakpoints( - req.arguments.source.path, - bpLines) - - local breakpoints = {} - for i, ln in ipairs(bpLines) do - breakpoints[i] = { - verified = (verifiedLines[ln] ~= nil), - line = verifiedLines[ln] - } - end - - sendSuccess(req, { - breakpoints = breakpoints - }) -end - -------------------------------------------------------------------------------- -function handlers.configurationDone(req) - sendSuccess(req, {}) - return 'CONTINUE' -end - -------------------------------------------------------------------------------- -function handlers.threads(req) - local c = coroutine.running() - - local mainThread = { - id = currentThreadId(), - name = (c and tostring(c)) or "main" - } - - sendSuccess(req, { - threads = { mainThread } - }) -end - -------------------------------------------------------------------------------- -function handlers.stackTrace(req) - assert(req.arguments.threadId == 0) - - local stackFrames = {} - local firstFrame = (req.arguments.startFrame or 0) + baseDepth - local lastFrame = (req.arguments.levels and (req.arguments.levels ~= 0)) - and (firstFrame + req.arguments.levels - 1) - or (9999) - - -- if firstframe function of stack is C function, ignore it. - if ignoreFirstFrameInC then - local info = debug_getinfo(firstFrame, 'lnS') - if info and info.what == "C" then - firstFrame = firstFrame + 1 - end - end - - for i = firstFrame, lastFrame do - local info = debug_getinfo(i, 'lnS') - if (info == nil) then break end - --print(json.encode(info)) - - local src = info.source - if string.sub(src, 1, 1) == '@' then - src = string.sub(src, 2) -- 앞의 '@' 떼어내기 - end - - local name - if info.name then - name = info.name .. ' (' .. (info.namewhat or '?') .. ')' - else - name = '?' - end - - local sframe = { - name = name, - source = { - name = nil, - path = Path.toAbsolute(sourceBasePath, src) - }, - column = 1, - line = info.currentline or 1, - id = i, - } - stackFrames[#stackFrames + 1] = sframe - end - - sendSuccess(req, { - stackFrames = stackFrames - }) -end - -------------------------------------------------------------------------------- -local scopeTypes = { - Locals = 1, - Upvalues = 2, - Globals = 3, -} -function handlers.scopes(req) - local depth = req.arguments.frameId - - local scopes = {} - local function addScope(name) - scopes[#scopes + 1] = { - name = name, - expensive = false, - variablesReference = depth * 1000000 + scopeTypes[name] - } - end - - addScope('Locals') - addScope('Upvalues') - addScope('Globals') - - sendSuccess(req, { - scopes = scopes - }) -end - -------------------------------------------------------------------------------- -local function registerVar(varNameCount, name_, value, noQuote) - local ty = type(value) - local name - if type(name_) == 'number' then - name = '[' .. name_ .. ']' - else - name = tostring(name_) - end - if varNameCount[name] then - varNameCount[name] = varNameCount[name] + 1 - name = name .. ' (' .. varNameCount[name] .. ')' - else - varNameCount[name] = 1 - end - - local item = { - name = name, - type = ty - } - - if (ty == 'string' and (not noQuote)) then - item.value = '"' .. value .. '"' - else - item.value = tostring(value) - end - - if (ty == 'table') or - (ty == 'function') or - (ty == 'userdata') then - storedVariables[nextVarRef] = value - item.variablesReference = nextVarRef - nextVarRef = nextVarRef + 1 - else - item.variablesReference = -1 - end - - return item -end - -------------------------------------------------------------------------------- -function handlers.variables(req) - local varRef = req.arguments.variablesReference - local variables = {} - local varNameCount = {} - local function addVar(name, value, noQuote) - variables[#variables + 1] = registerVar(varNameCount, name, value, noQuote) - end - - if (varRef >= 1000000) then - -- Scope. - local depth = math.floor(varRef / 1000000) - local scopeType = varRef % 1000000 - if scopeType == scopeTypes.Locals then - for i = 1, 9999 do - local name, value = debug_getlocal(depth, i) - if name == nil then break end - addVar(name, value, nil) - end - elseif scopeType == scopeTypes.Upvalues then - local info = debug_getinfo(depth, 'f') - if info and info.func then - for i = 1, 9999 do - local name, value = debug.getupvalue(info.func, i) - if name == nil then break end - addVar(name, value, nil) - end - end - elseif scopeType == scopeTypes.Globals then - for name, value in pairs(_G) do - addVar(name, value) - end - table.sort(variables, function(a, b) return a.name < b.name end) - end - else - -- Expansion. - local var = storedVariables[varRef] - if type(var) == 'table' then - for k, v in pairs(var) do - addVar(k, v) - end - table.sort(variables, function(a, b) - local aNum, aMatched = string.gsub(a.name, '^%[(%d+)%]$', '%1') - local bNum, bMatched = string.gsub(b.name, '^%[(%d+)%]$', '%1') - - if (aMatched == 1) and (bMatched == 1) then - -- both are numbers. compare numerically. - return tonumber(aNum) < tonumber(bNum) - elseif aMatched == bMatched then - -- both are strings. compare alphabetically. - return a.name < b.name - else - -- string comes first. - return aMatched < bMatched - end - end) - elseif type(var) == 'function' then - local info = debug.getinfo(var, 'S') - addVar('(source)', tostring(info.short_src), true) - addVar('(line)', info.linedefined) - - for i = 1, 9999 do - local name, value = debug.getupvalue(var, i) - if name == nil then break end - addVar(name, value) - end - elseif type(var) == 'userdata' then - addUserdataVar(var, addVar) - end - - local mt = getmetatable(var) - if mt then - addVar("(metatable)", mt) - end - end - - sendSuccess(req, { - variables = variables - }) -end - -------------------------------------------------------------------------------- -function handlers.continue(req) - sendSuccess(req, {}) - return 'CONTINUE' -end - -------------------------------------------------------------------------------- -local function stackHeight() - for i = 1, 9999999 do - if (debug_getinfo(i, '') == nil) then - return i - end - end -end - -------------------------------------------------------------------------------- -local stepTargetHeight = nil -local function step() - if (stepTargetHeight == nil) or (stackHeight() <= stepTargetHeight) then - breaker.setLineBreak(nil) - baseDepth = breaker.stackOffset.stepDebugLoop - startDebugLoop() - end -end - -------------------------------------------------------------------------------- -function handlers.next(req) - stepTargetHeight = stackHeight() - breaker.stackOffset.step - breaker.setLineBreak(step) - sendSuccess(req, {}) - return 'CONTINUE' -end - -------------------------------------------------------------------------------- -function handlers.stepIn(req) - stepTargetHeight = nil - breaker.setLineBreak(step) - sendSuccess(req, {}) - return 'CONTINUE' -end - -------------------------------------------------------------------------------- -function handlers.stepOut(req) - stepTargetHeight = stackHeight() - (breaker.stackOffset.step + 1) - breaker.setLineBreak(step) - sendSuccess(req, {}) - return 'CONTINUE' -end - -------------------------------------------------------------------------------- -function handlers.evaluate(req) - -- 실행할 소스 코드 준비 - local sourceCode = req.arguments.expression - if string.sub(sourceCode, 1, 1) == '!' then - sourceCode = string.sub(sourceCode, 2) - else - sourceCode = 'return (' .. sourceCode .. ')' - end - - -- 환경 준비. - -- 뭘 요구할지 모르니까 로컬, 업밸류, 글로벌을 죄다 복사해둔다. - -- 우선순위는 글로벌-업밸류-로컬 순서니까 - -- 그 반대로 갖다놓아서 나중 것이 앞의 것을 덮어쓰게 한다. - local depth = req.arguments.frameId - local tempG = {} - local declared = {} - local function set(k, v) - tempG[k] = v - declared[k] = true - end - - for name, value in pairs(_G) do - set(name, value) - end - - if depth then - local info = debug_getinfo(depth, 'f') - if info and info.func then - for i = 1, 9999 do - local name, value = debug.getupvalue(info.func, i) - if name == nil then break end - set(name, value) - end - end - - for i = 1, 9999 do - local name, value = debug_getlocal(depth, i) - if name == nil then break end - set(name, value) - end - else - -- VSCode가 depth를 안 보낼 수도 있다. - -- 특정 스택 프레임을 선택하지 않은, 전역 이름만 조회하는 경우이다. - end - local mt = { - __newindex = function() error('assignment not allowed', 2) end, - __index = function(t, k) if not declared[k] then error('not declared', 2) end end - } - setmetatable(tempG, mt) - - -- 파싱 - -- loadstring for Lua 5.1 - -- load for Lua 5.2 and 5.3(supports the private environment's load function) - local fn, err = (loadstring or load)(sourceCode, 'X', nil, tempG) - if fn == nil then - sendFailure(req, string.gsub(err, '^%[string %"X%"%]%:%d+%: ', '')) - return - end - - -- 실행하고 결과 송신 - if setfenv ~= nil then - -- Only for Lua 5.1 - setfenv(fn, tempG) - end - - local success, aux = pcall(fn) - if not success then - aux = aux or '' -- Execution of 'error()' returns nil as aux - sendFailure(req, string.gsub(aux, '^%[string %"X%"%]%:%d+%: ', '')) - return - end - - local varNameCount = {} - local item = registerVar(varNameCount, '', aux) - - sendSuccess(req, { - result = item.value, - type = item.type, - variablesReference = item.variablesReference - }) -end - -------------------------------------------------------------------------------- -return debuggee