mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-26 06:44:58 +00:00
feat: add testing utilities including context, mock functions, and snapshot handling
This commit is contained in:
57
packages/lua_test/seed/scripts/context.lua
Normal file
57
packages/lua_test/seed/scripts/context.lua
Normal file
@@ -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
|
||||
102
packages/lua_test/seed/scripts/fake_timers.lua
Normal file
102
packages/lua_test/seed/scripts/fake_timers.lua
Normal file
@@ -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
|
||||
@@ -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 "<circular>"
|
||||
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
|
||||
|
||||
123
packages/lua_test/seed/scripts/mock_fn.lua
Normal file
123
packages/lua_test/seed/scripts/mock_fn.lua
Normal file
@@ -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
|
||||
36
packages/lua_test/seed/scripts/mock_object.lua
Normal file
36
packages/lua_test/seed/scripts/mock_object.lua
Normal file
@@ -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<string, MockFunction> Internal mock storage
|
||||
---@field _resetAll fun() Reset all mocks on this object
|
||||
|
||||
---Create a mock object with multiple mock functions
|
||||
---@param methods? table<string, function> 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
|
||||
@@ -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
|
||||
102
packages/lua_test/seed/scripts/snapshot.lua
Normal file
102
packages/lua_test/seed/scripts/snapshot.lua
Normal file
@@ -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 "<circular>"
|
||||
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<string, string> 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<string, string>
|
||||
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
|
||||
37
packages/lua_test/seed/scripts/spy.lua
Normal file
37
packages/lua_test/seed/scripts/spy.lua
Normal file
@@ -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
|
||||
43
packages/lua_test/seed/scripts/string_utils.lua
Normal file
43
packages/lua_test/seed/scripts/string_utils.lua
Normal file
@@ -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
|
||||
67
packages/lua_test/seed/scripts/table_utils.lua
Normal file
67
packages/lua_test/seed/scripts/table_utils.lua
Normal file
@@ -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
|
||||
50
packages/lua_test/seed/scripts/test_data.lua
Normal file
50
packages/lua_test/seed/scripts/test_data.lua
Normal file
@@ -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
|
||||
36
packages/lua_test/seed/scripts/wait.lua
Normal file
36
packages/lua_test/seed/scripts/wait.lua
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user