mirror of
https://github.com/centau/vide.git
synced 2026-08-20 23:01:37 +00:00
321 lines
7.9 KiB
Lua
321 lines
7.9 KiB
Lua
------------------------------------------------------------------------------------------
|
|
-- testkit.luau
|
|
-- v0.2.0
|
|
------------------------------------------------------------------------------------------
|
|
|
|
--[[
|
|
|
|
EXAMPLE USAGE:
|
|
|
|
local testkit = require "path-to-testkit"
|
|
|
|
local TEST, CASE, CHECK = testkit.getUnitTestTools()
|
|
|
|
TEST("test name", function()
|
|
do CASE "A"
|
|
CHECK(condition)
|
|
end
|
|
end)
|
|
|
|
local BENCH, START = testkit.getBenchmarkTools()
|
|
|
|
BENCH("benchmark name", function()
|
|
local x = 0
|
|
|
|
for i = 1, START(1e6) do
|
|
x += 1
|
|
end
|
|
end)
|
|
]]
|
|
|
|
------------------------------------------------------------------------------------------
|
|
-- Unit Testing
|
|
------------------------------------------------------------------------------------------
|
|
|
|
type Test = {
|
|
name: string,
|
|
activeCase: Case?,
|
|
cases: { Case },
|
|
duration: number,
|
|
error: string?
|
|
}
|
|
|
|
type Case = {
|
|
name: string,
|
|
result: number,
|
|
line: number?
|
|
}
|
|
|
|
local PASS = 1
|
|
local FAIL = 2
|
|
local NONE = 3
|
|
local ERROR = 4
|
|
|
|
local activeTest: Test?
|
|
local tests: { Test } = {}
|
|
|
|
local function outputTestResults(test: Test)
|
|
print("\27[1;4m"..test.name.."\27[0m")
|
|
for _, case in test.cases do
|
|
print(
|
|
"[" ..
|
|
(if case.result == PASS then
|
|
"\27[32;1mPASS\27[0m"
|
|
elseif case.result == FAIL then
|
|
"\27[31;1mFAIL:"..assert(case.line).."\27[0m"
|
|
elseif case.result == NONE then
|
|
"\27[33;1mNONE\27[0m"
|
|
else
|
|
"\27[41;1;30mERROR\27[0m")
|
|
.. "] " .. case.name
|
|
)
|
|
end
|
|
|
|
if test.error then
|
|
print("\27[31;1;30merror: " .. test.error .. "\27[0m")
|
|
end
|
|
|
|
print ""
|
|
end
|
|
|
|
local function CASE(name: string)
|
|
assert(activeTest, "no active test")
|
|
|
|
local case: Case = {
|
|
name = name,
|
|
result = NONE
|
|
}
|
|
|
|
activeTest.activeCase = case
|
|
table.insert(activeTest.cases, case)
|
|
end
|
|
|
|
local function CHECK(value: any): boolean
|
|
assert(activeTest, "no active test")
|
|
local activeCase = activeTest.activeCase
|
|
|
|
if not activeCase then
|
|
CASE ""
|
|
activeCase = activeTest.activeCase
|
|
end; assert(activeCase, "no active case")
|
|
|
|
local result = value and PASS or FAIL
|
|
|
|
if activeCase.result == NONE or activeCase.result == PASS then
|
|
activeCase.result = result
|
|
activeCase.line = debug.info(2, "l")
|
|
end
|
|
|
|
return result == PASS
|
|
end
|
|
|
|
local function TEST(name: string, fn: () -> ())
|
|
assert(not activeTest, "new test was started while a test was in progress")
|
|
local test: Test = {
|
|
name = name,
|
|
cases = {},
|
|
duration = 0
|
|
}
|
|
|
|
activeTest = test
|
|
table.insert(tests, test)
|
|
|
|
local start = os.clock()
|
|
local msg: string?
|
|
local success = xpcall(fn, function(m: string) msg = m .. debug.traceback("", 2) end)
|
|
test.duration = os.clock() - start
|
|
|
|
if not test.activeCase then CASE "" end
|
|
assert(test.activeCase, "no active case")
|
|
|
|
if not success then
|
|
test.activeCase.result = ERROR
|
|
test.error = msg
|
|
end
|
|
|
|
activeTest = nil
|
|
outputTestResults(test)
|
|
end
|
|
|
|
local function FINISH(): boolean
|
|
local success = true
|
|
local totalCases = 0
|
|
local passedCases = 0
|
|
local duration = 0
|
|
|
|
for _, test in tests do
|
|
duration += test.duration
|
|
for _, case in test.cases do
|
|
totalCases += 1
|
|
if case.result == PASS or case.result == NONE then
|
|
passedCases += 1
|
|
else
|
|
success = false
|
|
end
|
|
end
|
|
end
|
|
|
|
print(string.format("%d/%d test cases passed in %.3f ms.", passedCases, totalCases, duration*1e3))
|
|
|
|
local fails = totalCases - passedCases
|
|
|
|
print(string.format("\27[%d;1;30m%d fail%s\27[0m", fails > 0 and 41 or 42, fails, fails == 1 and "" or "s"))
|
|
|
|
return success, table.clear(tests)
|
|
end
|
|
|
|
------------------------------------------------------------------------------------------
|
|
-- Benchmarking
|
|
------------------------------------------------------------------------------------------
|
|
|
|
type Bench = {
|
|
timeStart: number?,
|
|
memStart: number?,
|
|
iterations: number?
|
|
}
|
|
|
|
local activeBench: Bench? = nil
|
|
|
|
function START(iter: number?): number
|
|
local n = iter or 1
|
|
if n < 1 then error("iteration count must be greater than 0", 2) end
|
|
assert(activeBench, "no active benchmark")
|
|
assert(not activeBench.timeStart, "clock was already started")
|
|
|
|
activeBench.iterations = n
|
|
activeBench.memStart = gcinfo()
|
|
activeBench.timeStart = os.clock()
|
|
return n
|
|
end
|
|
|
|
local function BENCH(name: string, fn: () -> ())
|
|
assert(not activeBench, "cannot run benchmark, a benchmark is already in progress")
|
|
|
|
local bench: Bench = {}
|
|
activeBench = bench
|
|
|
|
local memStart = gcinfo()
|
|
local timeStart = os.clock()
|
|
local msg: string?
|
|
local success = xpcall(fn, function(m: string) msg = m .. debug.traceback("", 2) end)
|
|
local timeStop = os.clock()
|
|
local memStop = gcinfo()
|
|
|
|
if not success then
|
|
print("[\27[41;1mERROR\27[0m] " .. name)
|
|
print("\27[31;1m" .. "error: " .. msg :: string .. "\27[0m")
|
|
activeBench = nil
|
|
return
|
|
end
|
|
|
|
timeStart = bench.timeStart or timeStart
|
|
memStart = bench.memStart or memStart
|
|
|
|
local n = bench.iterations or 1
|
|
local duration = timeStop - timeStart
|
|
local allocated = memStop - memStart
|
|
|
|
print(string.format("[ %.3f us | %4.0f B ] %s", duration/n * 1e6, allocated/n * 1e3, name))
|
|
|
|
activeBench = nil
|
|
end
|
|
|
|
------------------------------------------------------------------------------------------
|
|
-- Printing
|
|
------------------------------------------------------------------------------------------
|
|
|
|
local function printa(v: unknown)
|
|
type Buffer = { n: number, [number]: string }
|
|
|
|
-- overkill concatenationless string buffer
|
|
local function tos(value: any, stack: number, str: Buffer)
|
|
local TAB = " "
|
|
local indent = table.concat(table.create(stack, TAB))
|
|
|
|
if type(value) == "string" then
|
|
local n = str.n
|
|
str[n + 1] = "\""
|
|
str[n + 2] = value
|
|
str[n + 3] = "\""
|
|
str.n = n + 3
|
|
elseif type(value) ~= "table" then
|
|
local n = str.n
|
|
str[n + 1] = value == nil and "nil" or tostring(value)
|
|
str.n = n + 1
|
|
elseif next(value) == nil then
|
|
local n = str.n
|
|
str[n + 1] = "{}"
|
|
str.n = n + 1
|
|
else
|
|
local tabbed_indent = indent .. TAB
|
|
|
|
str.n += 1
|
|
str[str.n] = "{\n"
|
|
|
|
local i, v = next(value, nil)
|
|
while v ~= nil do
|
|
local n = str.n
|
|
str[n + 1] = tabbed_indent
|
|
|
|
if type(i) ~= "string" then
|
|
str[n + 2] = "["
|
|
str[n + 3] = tostring(i)
|
|
str[n + 4] = "]"
|
|
n += 4
|
|
else
|
|
str[n + 2] = tostring(i)
|
|
n += 2
|
|
end
|
|
|
|
str[n + 1] = " = "
|
|
str.n = n + 1
|
|
|
|
tos(v, stack + 1, str)
|
|
|
|
i, v = next(value, i)
|
|
|
|
n = str.n
|
|
str[n + 1] = v ~= nil and ",\n" or "\n"
|
|
str.n = n + 1
|
|
end
|
|
|
|
local n = str.n
|
|
str[n + 1] = indent
|
|
str[n + 2] = "}"
|
|
str.n = n + 2
|
|
end
|
|
end
|
|
|
|
local str = { n = 0 }
|
|
tos(v, 0, str)
|
|
print(table.concat(str))
|
|
end
|
|
|
|
printa "string"
|
|
|
|
printa(1)
|
|
|
|
printa {
|
|
hello = 1,
|
|
bye = "ok",
|
|
|
|
test = {
|
|
1, 2, 3
|
|
}
|
|
}
|
|
|
|
------------------------------------------------------------------------------------------
|
|
-- Return
|
|
------------------------------------------------------------------------------------------
|
|
|
|
return {
|
|
getUnitTestTools = function()
|
|
return TEST, CASE, CHECK, FINISH
|
|
end,
|
|
|
|
getBenchmarkTools = function()
|
|
return BENCH, START
|
|
end,
|
|
|
|
printa = printa
|
|
}
|