-- Enhanced Test Framework - Global Functions -- Provides better assert reporting and test runner functionality -- Test state local passed = 0 local total = 0 -- Enhanced assert function with better error reporting 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:match("@?(.+)") or "unknown" 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 -- Assert with tolerance for floating point comparisons function assert_close(a, b, tolerance, message) tolerance = tolerance or 1e-10 local diff = math.abs(a - b) if diff <= tolerance then return true end local msg = message or string.format("Expected %g, got %g (diff: %g, tolerance: %g)", a, b, diff, tolerance) assert(false, msg, 3) end -- Assert equality with better error messages function assert_equal(a, b, message) if a == b then return true end local msg = message or string.format("Expected %s, got %s", tostring(a), tostring(b)) assert(false, msg, 3) end -- Assert table equality (deep comparison) function assert_table_equal(a, b, message, path) path = path or "root" if type(a) ~= type(b) then local msg = message or string.format("Type mismatch at %s: expected %s, got %s", path, type(a), type(b)) assert(false, msg, 3) end if type(a) ~= "table" then if a ~= b then local msg = message or string.format("Value mismatch at %s: expected %s, got %s", path, tostring(a), tostring(b)) assert(false, msg, 3) end return true end -- Check all keys in a exist in b with same values for k, v in pairs(a) do local new_path = path .. "." .. tostring(k) if b[k] == nil then local msg = message or string.format("Missing key at %s", new_path) assert(false, msg, 3) end assert_table_equal(v, b[k], message, new_path) end -- Check all keys in b exist in a for k, v in pairs(b) do if a[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 -- Test runner function 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 -- Test suite runner 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 -- Reset test counters function reset_tests() passed = 0 total = 0 end -- Get test statistics function test_stats() return { passed = passed, total = total, failed = total - passed, success_rate = total > 0 and (passed / total) or 0 } end -- Print test summary and return success status 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 -- Exit with appropriate code based on test results function test_exit() local success = passed == total os.exit(success and 0 or 1) end -- Convenience function to run and exit function run_and_exit(tests) local success = run_tests(tests) os.exit(success and 0 or 1) end -- Benchmark function 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 -- Helper to check if file exists function file_exists(filename) local file = io.open(filename, "r") if file then file:close() return true end return false end