193 lines
4.3 KiB
Lua
193 lines
4.3 KiB
Lua
local passed = 0
|
|
local total = 0
|
|
|
|
function assert(condition, message, level)
|
|
if condition then
|
|
return true
|
|
end
|
|
|
|
level = level or 2
|
|
local info = debug.getinfo(level, "Sl")
|
|
local file = info.source
|
|
|
|
-- Extract filename from source or use generic name
|
|
if file:sub(1,1) == "@" then
|
|
file = file:sub(2) -- Remove @ prefix for files
|
|
else
|
|
file = "<script>" -- Generic name for inline scripts
|
|
end
|
|
|
|
local line = info.currentline or "unknown"
|
|
local error_msg = message or "assertion failed"
|
|
local full_msg = string.format("%s:%s: %s", file, line, error_msg)
|
|
|
|
error(full_msg, 0)
|
|
end
|
|
|
|
function assert_equal(expected, actual, message)
|
|
if expected == actual then
|
|
return true
|
|
end
|
|
|
|
local msg = message or string.format("Expected %s, got %s", tostring(expected), tostring(actual))
|
|
assert(false, msg, 3)
|
|
end
|
|
|
|
function assert_table_equal(expected, actual, message, path)
|
|
path = path or "root"
|
|
|
|
if type(expected) ~= type(actual) then
|
|
local msg = message or string.format("Type mismatch at %s: expected %s, got %s", path, type(expected), type(actual))
|
|
assert(false, msg, 3)
|
|
end
|
|
|
|
if type(expected) ~= "table" then
|
|
if expected ~= actual then
|
|
local msg = message or string.format("Value mismatch at %s: expected %s, got %s", path, tostring(expected), tostring(actual))
|
|
assert(false, msg, 3)
|
|
end
|
|
return true
|
|
end
|
|
|
|
-- Check all keys in a exist in b with same values
|
|
for k, v in pairs(expected) do
|
|
local new_path = path .. "." .. tostring(k)
|
|
if actual[k] == nil then
|
|
local msg = message or string.format("Missing key at %s", new_path)
|
|
assert(false, msg, 3)
|
|
end
|
|
assert_table_equal(v, actual[k], message, new_path)
|
|
end
|
|
|
|
-- Check all keys in b exist in a
|
|
for k, v in pairs(actual) do
|
|
if expected[k] == nil then
|
|
local new_path = path .. "." .. tostring(k)
|
|
local msg = message or string.format("Extra key at %s", new_path)
|
|
assert(false, msg, 3)
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
function assert_close(expected, actual, tolerance, message)
|
|
tolerance = tolerance or 1e-10
|
|
local diff = math.abs(expected - actual)
|
|
if diff <= tolerance then
|
|
return true
|
|
end
|
|
local msg = message or string.format("Expected %g, got %g (diff: %g > %g)", expected, actual, diff, tolerance)
|
|
assert(false, msg, 3)
|
|
end
|
|
|
|
function test(name, fn)
|
|
print("Testing " .. name .. "...")
|
|
total = total + 1
|
|
|
|
local start_time = os.clock()
|
|
local ok, err = pcall(fn)
|
|
local end_time = os.clock()
|
|
local duration = end_time - start_time
|
|
|
|
if ok then
|
|
passed = passed + 1
|
|
print(string.format(" ✓ PASS (%.3fs)", duration))
|
|
return true
|
|
else
|
|
print(" ✗ FAIL: " .. err)
|
|
if duration > 0.001 then
|
|
print(string.format(" (%.3fs)", duration))
|
|
end
|
|
return false
|
|
end
|
|
end
|
|
|
|
function run_tests(tests)
|
|
print("Running test suite...")
|
|
print("=" .. string.rep("=", 50))
|
|
|
|
for name, test_fn in pairs(tests) do
|
|
test(name, test_fn)
|
|
end
|
|
|
|
return summary()
|
|
end
|
|
|
|
function reset_tests()
|
|
passed = 0
|
|
total = 0
|
|
end
|
|
|
|
function test_stats()
|
|
return {
|
|
passed = passed,
|
|
total = total,
|
|
failed = total - passed,
|
|
success_rate = total > 0 and (passed / total) or 0
|
|
}
|
|
end
|
|
|
|
function summary()
|
|
print("=" .. string.rep("=", 50))
|
|
print(string.format("Test Results: %d/%d passed", passed, total))
|
|
|
|
local success = passed == total
|
|
if success then
|
|
print("🎉 All tests passed!")
|
|
else
|
|
local failed = total - passed
|
|
local rate = total > 0 and (passed / total * 100) or 0
|
|
print(string.format("❌ %d test(s) failed! (%.1f%% success rate)", failed, rate))
|
|
end
|
|
|
|
return success
|
|
end
|
|
|
|
function test_exit()
|
|
local success = passed == total
|
|
os.exit(success and 0 or 1)
|
|
end
|
|
|
|
function run_and_exit(tests)
|
|
local success = run_tests(tests)
|
|
os.exit(success and 0 or 1)
|
|
end
|
|
|
|
function benchmark(name, fn, iterations)
|
|
iterations = iterations or 1000
|
|
print("Benchmarking " .. name .. " (" .. iterations .. " iterations)...")
|
|
|
|
-- Warmup
|
|
for i = 1, math.min(10, iterations) do
|
|
fn()
|
|
end
|
|
|
|
-- Actual benchmark
|
|
local start = os.clock()
|
|
for i = 1, iterations do
|
|
fn()
|
|
end
|
|
local total_time = os.clock() - start
|
|
local avg_time = total_time / iterations
|
|
|
|
print(string.format(" Total: %.3fs, Average: %.6fs, Rate: %.0f ops/sec",
|
|
total_time, avg_time, 1/avg_time))
|
|
|
|
return {
|
|
total_time = total_time,
|
|
avg_time = avg_time,
|
|
ops_per_sec = 1/avg_time,
|
|
iterations = iterations
|
|
}
|
|
end
|
|
|
|
function file_exists(filename)
|
|
local file = io.open(filename, "r")
|
|
if file then
|
|
file:close()
|
|
return true
|
|
end
|
|
return false
|
|
end
|