Files
metabuilder/packages/lua_test/seed/scripts/runner.lua
JohnDoe6345789 aa01e42ae8 feat(lua_test): add unit testing framework with BDD-style organization
- Implemented core testing functionalities including describe/it blocks, before/after hooks, and assertion methods.
- Added support for mocks and spies to facilitate testing of functions and methods.
- Introduced helper utilities for generating test data, parameterized tests, and snapshot testing.
- Developed a test runner that executes suites and generates detailed reports in both text and JSON formats.
- Created a manifest for the lua_test package to define scripts and their purposes.

feat(screenshot_analyzer): introduce screenshot analysis package

- Added metadata for the screenshot_analyzer package, detailing its components and scripts.
- Defined dependencies and bindings for browser interactions.
2025-12-30 01:15:59 +00:00

331 lines
9.1 KiB
Lua

-- Test runner and reporter
-- Executes test suites and generates reports
local M = {}
-- Result types
M.STATUS = {
PASSED = "passed",
FAILED = "failed",
SKIPPED = "skipped",
PENDING = "pending"
}
-- Run a single test
function M.runTest(test, hooks)
local result = {
name = test.name,
status = M.STATUS.PENDING,
error = nil,
duration = 0
}
if test.skipped then
result.status = M.STATUS.SKIPPED
return result
end
local startTime = os.clock()
-- Run beforeEach hook
if hooks.beforeEach then
local success, err = pcall(hooks.beforeEach)
if not success then
result.status = M.STATUS.FAILED
result.error = "beforeEach failed: " .. tostring(err)
result.duration = (os.clock() - startTime) * 1000
return result
end
end
-- Run the test
local success, err = pcall(test.fn)
-- Run afterEach hook
if hooks.afterEach then
pcall(hooks.afterEach)
end
result.duration = (os.clock() - startTime) * 1000
if success then
result.status = M.STATUS.PASSED
else
result.status = M.STATUS.FAILED
if type(err) == "table" and err.type == "AssertionError" then
result.error = err.message
result.expected = err.expected
result.actual = err.actual
else
result.error = tostring(err)
end
end
return result
end
-- Run a test suite
function M.runSuite(suite, config, parentHooks)
local results = {
name = suite.name,
tests = {},
nested = {},
stats = {
total = 0,
passed = 0,
failed = 0,
skipped = 0,
duration = 0
}
}
config = config or {}
parentHooks = parentHooks or {}
local startTime = os.clock()
-- Combine hooks with parent hooks
local hooks = {
beforeEach = suite.beforeEach or parentHooks.beforeEach,
afterEach = suite.afterEach or parentHooks.afterEach
}
-- Run beforeAll hook
if suite.beforeAll then
local success, err = pcall(suite.beforeAll)
if not success then
-- Mark all tests as failed
for _, test in ipairs(suite.tests) do
results.tests[#results.tests + 1] = {
name = test.name,
status = M.STATUS.FAILED,
error = "beforeAll failed: " .. tostring(err),
duration = 0
}
results.stats.failed = results.stats.failed + 1
results.stats.total = results.stats.total + 1
end
results.stats.duration = (os.clock() - startTime) * 1000
return results
end
end
-- Check for .only tests
local hasOnly = false
for _, test in ipairs(suite.tests) do
if test.only then
hasOnly = true
break
end
end
-- Run tests
for _, test in ipairs(suite.tests) do
-- Skip if there's an "only" test and this isn't it
if hasOnly and not test.only then
local skipResult = {
name = test.name,
status = M.STATUS.SKIPPED,
duration = 0
}
results.tests[#results.tests + 1] = skipResult
results.stats.skipped = results.stats.skipped + 1
-- Apply filter if configured
elseif config.filter and not string.find(test.name, config.filter, 1, true) then
local skipResult = {
name = test.name,
status = M.STATUS.SKIPPED,
duration = 0
}
results.tests[#results.tests + 1] = skipResult
results.stats.skipped = results.stats.skipped + 1
else
local testResult = M.runTest(test, hooks)
results.tests[#results.tests + 1] = testResult
if testResult.status == M.STATUS.PASSED then
results.stats.passed = results.stats.passed + 1
elseif testResult.status == M.STATUS.FAILED then
results.stats.failed = results.stats.failed + 1
if config.stopOnFirstFailure then
break
end
elseif testResult.status == M.STATUS.SKIPPED then
results.stats.skipped = results.stats.skipped + 1
end
end
results.stats.total = results.stats.total + 1
end
-- Run nested suites
for _, nestedSuite in ipairs(suite.nested) do
local nestedResults = M.runSuite(nestedSuite, config, hooks)
results.nested[#results.nested + 1] = nestedResults
-- Aggregate stats
results.stats.total = results.stats.total + nestedResults.stats.total
results.stats.passed = results.stats.passed + nestedResults.stats.passed
results.stats.failed = results.stats.failed + nestedResults.stats.failed
results.stats.skipped = results.stats.skipped + nestedResults.stats.skipped
end
-- Run afterAll hook
if suite.afterAll then
pcall(suite.afterAll)
end
results.stats.duration = (os.clock() - startTime) * 1000
return results
end
-- Run all suites
function M.runAll(suites, config)
local allResults = {
suites = {},
stats = {
total = 0,
passed = 0,
failed = 0,
skipped = 0,
duration = 0,
suiteCount = 0
},
timestamp = os.date("%Y-%m-%dT%H:%M:%S")
}
local startTime = os.clock()
for _, suite in ipairs(suites) do
local suiteResults = M.runSuite(suite, config)
allResults.suites[#allResults.suites + 1] = suiteResults
allResults.stats.suiteCount = allResults.stats.suiteCount + 1
-- Aggregate stats
allResults.stats.total = allResults.stats.total + suiteResults.stats.total
allResults.stats.passed = allResults.stats.passed + suiteResults.stats.passed
allResults.stats.failed = allResults.stats.failed + suiteResults.stats.failed
allResults.stats.skipped = allResults.stats.skipped + suiteResults.stats.skipped
end
allResults.stats.duration = (os.clock() - startTime) * 1000
allResults.stats.success = allResults.stats.failed == 0
return allResults
end
-- Format results as text report
function M.formatReport(results, options)
options = options or {}
local indent = options.indent or ""
local verbose = options.verbose ~= false
local lines = {}
local function add(line)
lines[#lines + 1] = line
end
local function formatSuite(suite, depth)
local prefix = string.rep(" ", depth)
add(prefix .. "📦 " .. suite.name)
for _, test in ipairs(suite.tests) do
local icon = ""
if test.status == M.STATUS.PASSED then
icon = ""
elseif test.status == M.STATUS.FAILED then
icon = ""
elseif test.status == M.STATUS.SKIPPED then
icon = "⏭️"
end
local duration = string.format("(%.2fms)", test.duration)
add(prefix .. " " .. icon .. " " .. test.name .. " " .. duration)
if test.status == M.STATUS.FAILED and verbose then
add(prefix .. " Error: " .. (test.error or "Unknown error"))
if test.expected then
add(prefix .. " Expected: " .. tostring(test.expected))
end
if test.actual then
add(prefix .. " Actual: " .. tostring(test.actual))
end
end
end
for _, nested in ipairs(suite.nested) do
formatSuite(nested, depth + 1)
end
end
add("═══════════════════════════════════════")
add(" TEST RESULTS REPORT ")
add("═══════════════════════════════════════")
add("")
for _, suite in ipairs(results.suites) do
formatSuite(suite, 0)
add("")
end
add("───────────────────────────────────────")
add("Summary:")
add(string.format(" Total: %d tests", results.stats.total))
add(string.format(" Passed: %d ✅", results.stats.passed))
add(string.format(" Failed: %d ❌", results.stats.failed))
add(string.format(" Skipped: %d ⏭️", results.stats.skipped))
add(string.format(" Duration: %.2fms", results.stats.duration))
add("")
if results.stats.success then
add("🎉 All tests passed!")
else
add("💥 Some tests failed!")
end
add("═══════════════════════════════════════")
return table.concat(lines, "\n")
end
-- Format results as JSON
function M.formatJSON(results)
-- Simple JSON serialization
local function serialize(value, indent)
indent = indent or 0
local t = type(value)
if t == "nil" then
return "null"
elseif t == "boolean" then
return value and "true" or "false"
elseif t == "number" then
return tostring(value)
elseif t == "string" then
return '"' .. value:gsub('"', '\\"'):gsub("\n", "\\n") .. '"'
elseif t == "table" then
local parts = {}
local isArray = #value > 0 or next(value) == nil
if isArray then
for _, v in ipairs(value) do
parts[#parts + 1] = serialize(v, indent + 1)
end
return "[" .. table.concat(parts, ",") .. "]"
else
for k, v in pairs(value) do
parts[#parts + 1] = '"' .. tostring(k) .. '":' .. serialize(v, indent + 1)
end
return "{" .. table.concat(parts, ",") .. "}"
end
end
return '"<' .. t .. '>"'
end
return serialize(results)
end
return M