From 09482b0d0bf4cb221c84ae7786f48c3988fa8d7f Mon Sep 17 00:00:00 2001 From: JohnDoe6345789 Date: Tue, 30 Dec 2025 11:03:15 +0000 Subject: [PATCH] feat: add testing utilities including context, mock functions, and snapshot handling --- packages/lua_test/seed/scripts/context.lua | 57 ++++ .../lua_test/seed/scripts/fake_timers.lua | 102 +++++++ packages/lua_test/seed/scripts/helpers.lua | 269 +++--------------- packages/lua_test/seed/scripts/mock_fn.lua | 123 ++++++++ .../lua_test/seed/scripts/mock_object.lua | 36 +++ packages/lua_test/seed/scripts/mocks.lua | 248 ---------------- packages/lua_test/seed/scripts/snapshot.lua | 102 +++++++ packages/lua_test/seed/scripts/spy.lua | 37 +++ .../lua_test/seed/scripts/string_utils.lua | 43 +++ .../lua_test/seed/scripts/table_utils.lua | 67 +++++ packages/lua_test/seed/scripts/test_data.lua | 50 ++++ packages/lua_test/seed/scripts/wait.lua | 36 +++ 12 files changed, 695 insertions(+), 475 deletions(-) create mode 100644 packages/lua_test/seed/scripts/context.lua create mode 100644 packages/lua_test/seed/scripts/fake_timers.lua create mode 100644 packages/lua_test/seed/scripts/mock_fn.lua create mode 100644 packages/lua_test/seed/scripts/mock_object.lua delete mode 100644 packages/lua_test/seed/scripts/mocks.lua create mode 100644 packages/lua_test/seed/scripts/snapshot.lua create mode 100644 packages/lua_test/seed/scripts/spy.lua create mode 100644 packages/lua_test/seed/scripts/string_utils.lua create mode 100644 packages/lua_test/seed/scripts/table_utils.lua create mode 100644 packages/lua_test/seed/scripts/test_data.lua create mode 100644 packages/lua_test/seed/scripts/wait.lua diff --git a/packages/lua_test/seed/scripts/context.lua b/packages/lua_test/seed/scripts/context.lua new file mode 100644 index 000000000..6362cb961 --- /dev/null +++ b/packages/lua_test/seed/scripts/context.lua @@ -0,0 +1,57 @@ +-- Test context utilities +-- Provides context builder for sharing state in tests + +local table_utils = require("table_utils") + +---@class TestContextModule +local M = {} + +---@class TestContext +---@field get fun(key: string): any Get a context value +---@field set fun(key: string, value: any) Set a context value +---@field with fun(overrides: table): table Get context merged with overrides +---@field reset fun() Reset context to initial state +---@field getAll fun(): table Get a clone of all context values + +---Create a test context for sharing state +---@param initial? table Initial context values +---@return TestContext Context object with get/set/with/reset methods +function M.createContext(initial) + local ctx = initial or {} + + return { + ---Get a context value by key + ---@param key string Key to look up + ---@return any Value at key + get = function(key) + return ctx[key] + end, + + ---Set a context value + ---@param key string Key to set + ---@param value any Value to store + set = function(key, value) + ctx[key] = value + end, + + ---Get context merged with overrides + ---@param overrides table Values to merge + ---@return table Merged context + with = function(overrides) + return table_utils.merge(ctx, overrides) + end, + + ---Reset context to initial state + reset = function() + ctx = initial and table_utils.clone(initial) or {} + end, + + ---Get a clone of all context values + ---@return table Clone of context + getAll = function() + return table_utils.clone(ctx) + end + } +end + +return M diff --git a/packages/lua_test/seed/scripts/fake_timers.lua b/packages/lua_test/seed/scripts/fake_timers.lua new file mode 100644 index 000000000..f8449582b --- /dev/null +++ b/packages/lua_test/seed/scripts/fake_timers.lua @@ -0,0 +1,102 @@ +-- Fake timer utilities for testing +-- Allows controlling time in tests + +---@class FakeTimersModule +local M = {} + +---@class FakeTimers +---@field now fun(): number Get current fake time +---@field schedule fun(callback: function, delay: number): number Schedule callback +---@field cancel fun(id: number) Cancel scheduled callback +---@field advance fun(ms: number) Advance time by milliseconds +---@field runAll fun() Run all pending timers +---@field reset fun() Reset timer state + +---Create a fake timer system for testing time-dependent code +---@return FakeTimers Timer control object +function M.useFakeTimers() + local timers = { + now = 0, + scheduled = {} + } + + return { + ---Get current fake time + ---@return number Current fake timestamp + now = function() + return timers.now + end, + + ---Schedule a callback (like setTimeout) + ---@param callback function Function to call + ---@param delay number Delay in milliseconds + ---@return number Timer ID for cancellation + schedule = function(callback, delay) + local id = #timers.scheduled + 1 + timers.scheduled[id] = { + callback = callback, + time = timers.now + delay, + id = id + } + return id + end, + + ---Cancel a scheduled callback + ---@param id number Timer ID to cancel + cancel = function(id) + timers.scheduled[id] = nil + end, + + ---Advance time and run scheduled callbacks + ---@param ms number Milliseconds to advance + advance = function(ms) + local targetTime = timers.now + ms + + local pending = {} + for _, timer in pairs(timers.scheduled) do + if timer.time <= targetTime then + pending[#pending + 1] = timer + end + end + + table.sort(pending, function(a, b) return a.time < b.time end) + + for _, timer in ipairs(pending) do + timers.now = timer.time + timer.callback() + timers.scheduled[timer.id] = nil + end + + timers.now = targetTime + end, + + ---Run all pending timers + runAll = function() + while next(timers.scheduled) do + local nextTimer + local nextTime = math.huge + + for id, timer in pairs(timers.scheduled) do + if timer.time < nextTime then + nextTime = timer.time + nextTimer = timer + end + end + + if nextTimer then + timers.now = nextTimer.time + nextTimer.callback() + timers.scheduled[nextTimer.id] = nil + end + end + end, + + ---Reset timer state + reset = function() + timers.now = 0 + timers.scheduled = {} + end + } +end + +return M diff --git a/packages/lua_test/seed/scripts/helpers.lua b/packages/lua_test/seed/scripts/helpers.lua index c9db457a6..c95c2043e 100644 --- a/packages/lua_test/seed/scripts/helpers.lua +++ b/packages/lua_test/seed/scripts/helpers.lua @@ -1,46 +1,32 @@ --- Test helper utilities --- Additional utilities for writing tests +-- Test helper utilities facade +-- Re-exports all helper modules for backward compatibility +-- +-- Split into focused modules: +-- test_data.lua - Test data generation (generateTestData) +-- wait.lua - Async wait utilities (waitFor) +-- snapshot.lua - Snapshot testing (createSnapshot, serialize) +-- table_utils.lua - Table utilities (clone, merge, keys, values, size) +-- string_utils.lua - String utilities (trim, split, startsWith, endsWith) +-- context.lua - Test context builder (createContext) +local test_data = require("test_data") +local wait = require("wait") +local snapshot = require("snapshot") +local table_utils = require("table_utils") +local string_utils = require("string_utils") +local context = require("context") + +---@class HelpersModule local M = {} --- Generate test data -function M.generateTestData(template, count) - local data = {} - count = count or 10 - - for i = 1, count do - local item = {} - for k, v in pairs(template) do - if type(v) == "function" then - item[k] = v(i) - elseif type(v) == "string" and v:match("^%$") then - -- Template variables - local varName = v:sub(2) - if varName == "index" then - item[k] = i - elseif varName == "random" then - item[k] = math.random(1, 1000) - elseif varName == "uuid" then - item[k] = string.format("%08x-%04x-%04x-%04x-%012x", - math.random(0, 0xffffffff), - math.random(0, 0xffff), - math.random(0, 0xffff), - math.random(0, 0xffff), - math.random(0, 0xffffffffffff)) - else - item[k] = v - end - else - item[k] = v - end - end - data[#data + 1] = item - end - - return data -end +-- Re-export test data generation +M.generateTestData = test_data.generateTestData --- Create parameterized test cases +-- Re-export parameterized helper +---Create parameterized test cases +---@param cases table[] Array of test case objects +---@param testFn fun(testCase: table) Test function receiving each case +---@return function Runner function that executes all cases function M.parameterized(cases, testFn) return function() for _, testCase in ipairs(cases) do @@ -49,173 +35,27 @@ function M.parameterized(cases, testFn) end end --- Wait for condition (for async-like testing) -function M.waitFor(condition, options) - options = options or {} - local timeout = options.timeout or 1000 - local interval = options.interval or 10 - local startTime = os.clock() * 1000 - - while (os.clock() * 1000 - startTime) < timeout do - if condition() then - return true - end - -- Note: In sandbox, we can't actually sleep, but this provides the pattern - end - - if options.throwOnTimeout ~= false then - error("waitFor timed out after " .. timeout .. "ms") - end - - return false -end +-- Re-export wait utilities +M.waitFor = wait.waitFor --- Snapshot testing helper -function M.createSnapshot(name) - local snapshots = {} - - return { - -- Record a snapshot - record = function(key, value) - snapshots[key] = M.serialize(value) - end, - - -- Match against recorded snapshot - match = function(key, value) - local serialized = M.serialize(value) - if snapshots[key] then - return snapshots[key] == serialized - end - -- First run - record the snapshot - snapshots[key] = serialized - return true - end, - - -- Get all snapshots - getSnapshots = function() - return snapshots - end, - - -- Update a snapshot - update = function(key, value) - snapshots[key] = M.serialize(value) - end - } -end +-- Re-export snapshot utilities +M.createSnapshot = snapshot.createSnapshot +M.serialize = snapshot.serialize --- Serialize value for comparison -function M.serialize(value, seen) - seen = seen or {} - local t = type(value) - - if t == "nil" then - return "nil" - elseif t == "boolean" then - return value and "true" or "false" - elseif t == "number" then - return tostring(value) - elseif t == "string" then - return string.format("%q", value) - elseif t == "table" then - if seen[value] then - return "" - end - seen[value] = true - - local parts = {} - local keys = {} - - for k in pairs(value) do - keys[#keys + 1] = k - end - - table.sort(keys, function(a, b) - return tostring(a) < tostring(b) - end) - - for _, k in ipairs(keys) do - parts[#parts + 1] = "[" .. M.serialize(k, seen) .. "]=" .. M.serialize(value[k], seen) - end - - return "{" .. table.concat(parts, ",") .. "}" - else - return "<" .. t .. ">" - end -end +-- Re-export table utilities +M.table = table_utils --- Table utilities for testing -M.table = {} +-- Re-export string utilities +M.string = string_utils -function M.table.clone(t) - if type(t) ~= "table" then return t end - local copy = {} - for k, v in pairs(t) do - copy[k] = M.table.clone(v) - end - return setmetatable(copy, getmetatable(t)) -end - -function M.table.merge(...) - local result = {} - for _, t in ipairs({...}) do - if type(t) == "table" then - for k, v in pairs(t) do - result[k] = v - end - end - end - return result -end - -function M.table.keys(t) - local keys = {} - for k in pairs(t) do - keys[#keys + 1] = k - end - return keys -end - -function M.table.values(t) - local values = {} - for _, v in pairs(t) do - values[#values + 1] = v - end - return values -end - -function M.table.size(t) - local count = 0 - for _ in pairs(t) do - count = count + 1 - end - return count -end - --- String utilities for testing -M.string = {} - -function M.string.trim(s) - return s:match("^%s*(.-)%s*$") -end - -function M.string.split(s, delimiter) - delimiter = delimiter or "%s" - local result = {} - for match in (s .. delimiter):gmatch("(.-)" .. delimiter) do - result[#result + 1] = match - end - return result -end - -function M.string.startsWith(s, prefix) - return s:sub(1, #prefix) == prefix -end - -function M.string.endsWith(s, suffix) - return suffix == "" or s:sub(-#suffix) == suffix -end +-- Re-export context builder +M.createContext = context.createContext -- Assertion shortcuts for common patterns +---Assert that a function throws an error +---@param fn function Function expected to throw +---@param expectedMessage? string Optional message to check for +---@return any The error that was thrown function M.assertThrows(fn, expectedMessage) local success, err = pcall(fn) if success then @@ -230,6 +70,8 @@ function M.assertThrows(fn, expectedMessage) return err end +---Assert that a function does not throw +---@param fn function Function that should not throw function M.assertDoesNotThrow(fn) local success, err = pcall(fn) if not success then @@ -237,31 +79,4 @@ function M.assertDoesNotThrow(fn) end end --- Test context builder -function M.createContext(initial) - local ctx = initial or {} - - return { - get = function(key) - return ctx[key] - end, - - set = function(key, value) - ctx[key] = value - end, - - with = function(overrides) - return M.table.merge(ctx, overrides) - end, - - reset = function() - ctx = initial and M.table.clone(initial) or {} - end, - - getAll = function() - return M.table.clone(ctx) - end - } -end - return M diff --git a/packages/lua_test/seed/scripts/mock_fn.lua b/packages/lua_test/seed/scripts/mock_fn.lua new file mode 100644 index 000000000..4ca7d97a4 --- /dev/null +++ b/packages/lua_test/seed/scripts/mock_fn.lua @@ -0,0 +1,123 @@ +-- Mock function creator +-- Creates trackable mock functions with call history + +---@class MockFn +local M = {} + +---@class MockFunction +---@field getCallCount fun(): number Get number of times called +---@field wasCalled fun(): boolean Check if function was called +---@field wasCalledWith fun(...): boolean Check if called with specific args +---@field getCall fun(index: number): any[] Get args from specific call +---@field getLastCall fun(): any[] Get args from last call +---@field getCalls fun(): any[][] Get all call args +---@field getResults fun(): any[][] Get all return values +---@field reset fun() Clear call history +---@field mockReturnValue fun(value: any) Set return value +---@field mockReturnValueOnce fun(value: any) Set one-time return value +---@field mockImplementation fun(fn: function) Set implementation +---@field mockRestore fun() Restore to original state + +---Create a mock function that tracks calls +---@param implementation? function Optional implementation +---@return MockFunction Callable mock with tracking methods +function M.fn(implementation) + local mock = { + calls = {}, + results = {}, + implementation = implementation + } + + local callable = function(...) + local args = {...} + mock.calls[#mock.calls + 1] = args + + local result + if mock.implementation then + result = {mock.implementation(...)} + else + result = {} + end + + mock.results[#mock.results + 1] = result + return table.unpack(result) + end + + return setmetatable({}, { + __call = function(_, ...) return callable(...) end, + __index = { + getCallCount = function() + return #mock.calls + end, + + wasCalled = function() + return #mock.calls > 0 + end, + + wasCalledWith = function(...) + local expectedArgs = {...} + for _, callArgs in ipairs(mock.calls) do + local match = true + for i, expected in ipairs(expectedArgs) do + if callArgs[i] ~= expected then + match = false + break + end + end + if match then return true end + end + return false + end, + + getCall = function(index) + return mock.calls[index] + end, + + getLastCall = function() + return mock.calls[#mock.calls] + end, + + getCalls = function() + return mock.calls + end, + + getResults = function() + return mock.results + end, + + reset = function() + mock.calls = {} + mock.results = {} + end, + + mockReturnValue = function(value) + mock.implementation = function() return value end + end, + + mockReturnValueOnce = function(value) + local originalImpl = mock.implementation + local called = false + mock.implementation = function(...) + if not called then + called = true + return value + elseif originalImpl then + return originalImpl(...) + end + end + end, + + mockImplementation = function(fn) + mock.implementation = fn + end, + + mockRestore = function() + mock.implementation = nil + mock.calls = {} + mock.results = {} + end + } + }) +end + +return M diff --git a/packages/lua_test/seed/scripts/mock_object.lua b/packages/lua_test/seed/scripts/mock_object.lua new file mode 100644 index 000000000..02c905307 --- /dev/null +++ b/packages/lua_test/seed/scripts/mock_object.lua @@ -0,0 +1,36 @@ +-- Mock object creator +-- Creates objects with multiple mock methods + +local mock_fn = require("mock_fn") + +---@class MockObjectModule +local M = {} + +---@class MockObject +---@field _mocks table Internal mock storage +---@field _resetAll fun() Reset all mocks on this object + +---Create a mock object with multiple mock functions +---@param methods? table Method names to implementations +---@return MockObject Object with mock methods +function M.mockObject(methods) + local obj = {} + local mocks = {} + + for name, impl in pairs(methods or {}) do + mocks[name] = mock_fn.fn(impl) + obj[name] = function(...) return mocks[name](...) end + end + + obj._mocks = mocks + + obj._resetAll = function() + for _, mock in pairs(mocks) do + mock.reset() + end + end + + return obj +end + +return M diff --git a/packages/lua_test/seed/scripts/mocks.lua b/packages/lua_test/seed/scripts/mocks.lua deleted file mode 100644 index cd76295b8..000000000 --- a/packages/lua_test/seed/scripts/mocks.lua +++ /dev/null @@ -1,248 +0,0 @@ --- Mock and spy utilities for testing --- Allows tracking function calls and replacing implementations - -local M = {} - --- Create a mock function -function M.fn(implementation) - local mock = { - calls = {}, - results = {}, - implementation = implementation - } - - -- The callable mock function - local callable = function(...) - local args = {...} - mock.calls[#mock.calls + 1] = args - - local result - if mock.implementation then - result = {mock.implementation(...)} - else - result = {} - end - - mock.results[#mock.results + 1] = result - return table.unpack(result) - end - - -- Attach mock metadata to the function via a metatable - return setmetatable({}, { - __call = function(_, ...) return callable(...) end, - __index = { - -- Get call count - getCallCount = function() - return #mock.calls - end, - - -- Check if called - wasCalled = function() - return #mock.calls > 0 - end, - - -- Check if called with specific args - wasCalledWith = function(...) - local expectedArgs = {...} - for _, callArgs in ipairs(mock.calls) do - local match = true - for i, expected in ipairs(expectedArgs) do - if callArgs[i] ~= expected then - match = false - break - end - end - if match then return true end - end - return false - end, - - -- Get specific call args - getCall = function(index) - return mock.calls[index] - end, - - -- Get last call args - getLastCall = function() - return mock.calls[#mock.calls] - end, - - -- Get all calls - getCalls = function() - return mock.calls - end, - - -- Get all results - getResults = function() - return mock.results - end, - - -- Clear call history - reset = function() - mock.calls = {} - mock.results = {} - end, - - -- Set return value - mockReturnValue = function(value) - mock.implementation = function() return value end - end, - - -- Set return values in sequence - mockReturnValueOnce = function(value) - local originalImpl = mock.implementation - local called = false - mock.implementation = function(...) - if not called then - called = true - return value - elseif originalImpl then - return originalImpl(...) - end - end - end, - - -- Set implementation - mockImplementation = function(fn) - mock.implementation = fn - end, - - -- Restore original (for spies) - mockRestore = function() - mock.implementation = nil - mock.calls = {} - mock.results = {} - end - } - }) -end - --- Create a spy on an existing object method -function M.spyOn(obj, methodName) - local original = obj[methodName] - if type(original) ~= "function" then - error("Cannot spy on non-function: " .. methodName) - end - - local spy = M.fn(original) - - -- Add restore functionality - local meta = getmetatable(spy).__index - local originalRestore = meta.mockRestore - meta.mockRestore = function() - obj[methodName] = original - originalRestore() - end - - -- Replace the method - obj[methodName] = function(...) - return spy(...) - end - - return spy -end - --- Create a mock object with multiple mock functions -function M.mockObject(methods) - local obj = {} - local mocks = {} - - for name, impl in pairs(methods or {}) do - mocks[name] = M.fn(impl) - obj[name] = function(...) return mocks[name](...) end - end - - obj._mocks = mocks - - obj._resetAll = function() - for _, mock in pairs(mocks) do - mock.reset() - end - end - - return obj -end - --- Timer mocks for testing time-dependent code -function M.useFakeTimers() - local timers = { - now = 0, - scheduled = {} - } - - return { - -- Get current fake time - now = function() - return timers.now - end, - - -- Schedule a callback (like setTimeout) - schedule = function(callback, delay) - local id = #timers.scheduled + 1 - timers.scheduled[id] = { - callback = callback, - time = timers.now + delay, - id = id - } - return id - end, - - -- Cancel a scheduled callback - cancel = function(id) - timers.scheduled[id] = nil - end, - - -- Advance time and run scheduled callbacks - advance = function(ms) - local targetTime = timers.now + ms - - -- Sort by scheduled time - local pending = {} - for _, timer in pairs(timers.scheduled) do - if timer.time <= targetTime then - pending[#pending + 1] = timer - end - end - - table.sort(pending, function(a, b) return a.time < b.time end) - - -- Run each callback at its scheduled time - for _, timer in ipairs(pending) do - timers.now = timer.time - timer.callback() - timers.scheduled[timer.id] = nil - end - - timers.now = targetTime - end, - - -- Run all pending timers - runAll = function() - while next(timers.scheduled) do - local nextTimer - local nextTime = math.huge - - for id, timer in pairs(timers.scheduled) do - if timer.time < nextTime then - nextTime = timer.time - nextTimer = timer - end - end - - if nextTimer then - timers.now = nextTimer.time - nextTimer.callback() - timers.scheduled[nextTimer.id] = nil - end - end - end, - - -- Reset timers - reset = function() - timers.now = 0 - timers.scheduled = {} - end - } -end - -return M diff --git a/packages/lua_test/seed/scripts/snapshot.lua b/packages/lua_test/seed/scripts/snapshot.lua new file mode 100644 index 000000000..797555fa8 --- /dev/null +++ b/packages/lua_test/seed/scripts/snapshot.lua @@ -0,0 +1,102 @@ +-- Snapshot testing utilities +-- Provides record/match snapshot functionality + +---@class SnapshotModule +local M = {} + +-- Forward declaration for serialize +local serialize + +---Serialize a value to a deterministic string representation +---@param value any Value to serialize +---@param seen? table Circular reference tracker +---@return string Serialized string +function M.serialize(value, seen) + seen = seen or {} + local t = type(value) + + if t == "nil" then + return "nil" + elseif t == "boolean" then + return value and "true" or "false" + elseif t == "number" then + return tostring(value) + elseif t == "string" then + return string.format("%q", value) + elseif t == "table" then + if seen[value] then + return "" + end + seen[value] = true + + local parts = {} + local keys = {} + + for k in pairs(value) do + keys[#keys + 1] = k + end + + table.sort(keys, function(a, b) + return tostring(a) < tostring(b) + end) + + for _, k in ipairs(keys) do + parts[#parts + 1] = "[" .. M.serialize(k, seen) .. "]=" .. M.serialize(value[k], seen) + end + + return "{" .. table.concat(parts, ",") .. "}" + else + return "<" .. t .. ">" + end +end + +---@class Snapshot +---@field record fun(key: string, value: any) Record a snapshot value +---@field match fun(key: string, value: any): boolean Match against recorded snapshot +---@field getSnapshots fun(): table Get all recorded snapshots +---@field update fun(key: string, value: any) Update a snapshot value + +---Create a snapshot testing helper +---@param name string Name for this snapshot set +---@return Snapshot Snapshot object with record/match methods +function M.createSnapshot(name) + local snapshots = {} + + return { + ---Record a snapshot value + ---@param key string Snapshot identifier + ---@param value any Value to record + record = function(key, value) + snapshots[key] = M.serialize(value) + end, + + ---Match value against recorded snapshot + ---@param key string Snapshot identifier + ---@param value any Value to compare + ---@return boolean True if matches or first run + match = function(key, value) + local serialized = M.serialize(value) + if snapshots[key] then + return snapshots[key] == serialized + end + -- First run - record the snapshot + snapshots[key] = serialized + return true + end, + + ---Get all recorded snapshots + ---@return table + getSnapshots = function() + return snapshots + end, + + ---Update a snapshot value + ---@param key string Snapshot identifier + ---@param value any New value to record + update = function(key, value) + snapshots[key] = M.serialize(value) + end + } +end + +return M diff --git a/packages/lua_test/seed/scripts/spy.lua b/packages/lua_test/seed/scripts/spy.lua new file mode 100644 index 000000000..1a5fecc46 --- /dev/null +++ b/packages/lua_test/seed/scripts/spy.lua @@ -0,0 +1,37 @@ +-- Spy utilities for testing +-- Allows spying on existing object methods + +local mock_fn = require("mock_fn") + +---@class SpyModule +local M = {} + +---Create a spy on an existing object method +---@param obj table Object containing the method +---@param methodName string Name of method to spy on +---@return MockFunction Spy with tracking methods and mockRestore +function M.spyOn(obj, methodName) + local original = obj[methodName] + if type(original) ~= "function" then + error("Cannot spy on non-function: " .. methodName) + end + + local spy = mock_fn.fn(original) + + -- Add restore functionality + local meta = getmetatable(spy).__index + local originalRestore = meta.mockRestore + meta.mockRestore = function() + obj[methodName] = original + originalRestore() + end + + -- Replace the method + obj[methodName] = function(...) + return spy(...) + end + + return spy +end + +return M diff --git a/packages/lua_test/seed/scripts/string_utils.lua b/packages/lua_test/seed/scripts/string_utils.lua new file mode 100644 index 000000000..f5c0e8c18 --- /dev/null +++ b/packages/lua_test/seed/scripts/string_utils.lua @@ -0,0 +1,43 @@ +-- String utilities for testing +-- Provides trim, split, startsWith, endsWith functions + +---@class StringUtils +local M = {} + +---Trim whitespace from both ends of a string +---@param s string String to trim +---@return string Trimmed string +function M.trim(s) + return s:match("^%s*(.-)%s*$") +end + +---Split a string by delimiter +---@param s string String to split +---@param delimiter? string Delimiter pattern (default whitespace) +---@return string[] Array of parts +function M.split(s, delimiter) + delimiter = delimiter or "%s" + local result = {} + for match in (s .. delimiter):gmatch("(.-)" .. delimiter) do + result[#result + 1] = match + end + return result +end + +---Check if string starts with prefix +---@param s string String to check +---@param prefix string Prefix to look for +---@return boolean True if s starts with prefix +function M.startsWith(s, prefix) + return s:sub(1, #prefix) == prefix +end + +---Check if string ends with suffix +---@param s string String to check +---@param suffix string Suffix to look for +---@return boolean True if s ends with suffix +function M.endsWith(s, suffix) + return suffix == "" or s:sub(-#suffix) == suffix +end + +return M diff --git a/packages/lua_test/seed/scripts/table_utils.lua b/packages/lua_test/seed/scripts/table_utils.lua new file mode 100644 index 000000000..eb79d77cc --- /dev/null +++ b/packages/lua_test/seed/scripts/table_utils.lua @@ -0,0 +1,67 @@ +-- Table utilities for testing +-- Provides clone, merge, keys, values, size functions + +---@class TableUtils +local M = {} + +---Deep clone a table +---@param t table Table to clone +---@return table Cloned table +function M.clone(t) + if type(t) ~= "table" then return t end + local copy = {} + for k, v in pairs(t) do + copy[k] = M.clone(v) + end + return setmetatable(copy, getmetatable(t)) +end + +---Merge multiple tables into one +---@vararg table Tables to merge (later tables override earlier) +---@return table Merged table +function M.merge(...) + local result = {} + for _, t in ipairs({...}) do + if type(t) == "table" then + for k, v in pairs(t) do + result[k] = v + end + end + end + return result +end + +---Get all keys from a table +---@param t table Source table +---@return any[] Array of keys +function M.keys(t) + local keys = {} + for k in pairs(t) do + keys[#keys + 1] = k + end + return keys +end + +---Get all values from a table +---@param t table Source table +---@return any[] Array of values +function M.values(t) + local values = {} + for _, v in pairs(t) do + values[#values + 1] = v + end + return values +end + +---Get the number of entries in a table (including non-array keys) +---@param t table Table to count +---@return number Number of key-value pairs +function M.size(t) + local count = 0 + for _ in pairs(t) do + count = count + 1 + end + return count +end + +return M diff --git a/packages/lua_test/seed/scripts/test_data.lua b/packages/lua_test/seed/scripts/test_data.lua new file mode 100644 index 000000000..ab0f7010d --- /dev/null +++ b/packages/lua_test/seed/scripts/test_data.lua @@ -0,0 +1,50 @@ +-- Test data generation utilities +-- Provides functions for generating test fixtures + +---@class TestDataGenerator +local M = {} + +---@class TestDataTemplate +---@field [string] any Field template (value, function, or $variable) + +---Generate test data from a template +---@param template TestDataTemplate Template defining field patterns +---@param count? number Number of items to generate (default 10) +---@return table[] Array of generated test data items +function M.generateTestData(template, count) + local data = {} + count = count or 10 + + for i = 1, count do + local item = {} + for k, v in pairs(template) do + if type(v) == "function" then + item[k] = v(i) + elseif type(v) == "string" and v:match("^%$") then + -- Template variables + local varName = v:sub(2) + if varName == "index" then + item[k] = i + elseif varName == "random" then + item[k] = math.random(1, 1000) + elseif varName == "uuid" then + item[k] = string.format("%08x-%04x-%04x-%04x-%012x", + math.random(0, 0xffffffff), + math.random(0, 0xffff), + math.random(0, 0xffff), + math.random(0, 0xffff), + math.random(0, 0xffffffffffff)) + else + item[k] = v + end + else + item[k] = v + end + end + data[#data + 1] = item + end + + return data +end + +return M diff --git a/packages/lua_test/seed/scripts/wait.lua b/packages/lua_test/seed/scripts/wait.lua new file mode 100644 index 000000000..437091fb7 --- /dev/null +++ b/packages/lua_test/seed/scripts/wait.lua @@ -0,0 +1,36 @@ +-- Wait utilities for async-like testing +-- Provides waitFor condition polling + +---@class WaitModule +local M = {} + +---@class WaitForOptions +---@field timeout? number Timeout in milliseconds (default 1000) +---@field interval? number Polling interval in ms (default 10) +---@field throwOnTimeout? boolean Whether to throw on timeout (default true) + +---Wait for a condition to become true +---@param condition fun(): boolean Function returning true when condition is met +---@param options? WaitForOptions Configuration options +---@return boolean True if condition was met, false if timed out (when throwOnTimeout=false) +function M.waitFor(condition, options) + options = options or {} + local timeout = options.timeout or 1000 + local interval = options.interval or 10 + local startTime = os.clock() * 1000 + + while (os.clock() * 1000 - startTime) < timeout do + if condition() then + return true + end + -- Note: In sandbox, we can't actually sleep, but this provides the pattern + end + + if options.throwOnTimeout ~= false then + error("waitFor timed out after " .. timeout .. "ms") + end + + return false +end + +return M