This commit is contained in:
marked 2025-03-12 21:34:03 +01:00
parent 005b7bcfbb
commit dfc887561e
3 changed files with 5 additions and 555 deletions

View file

@ -1,555 +0,0 @@
--------------------------------------------------------------------------------
-- testkit.luau
-- v0.7.3
-- MIT License
-- Copyright (c) 2022 centau
--------------------------------------------------------------------------------
local disable_ansi = false
local color = {
white_underline = function(s: string): string
return if disable_ansi then s else `\27[1;4m{s}\27[0m`
end,
white = function(s: string): string
return if disable_ansi then s else `\27[37;1m{s}\27[0m`
end,
green = function(s: string): string
return if disable_ansi then s else `\27[32;1m{s}\27[0m`
end,
red = function(s: string): string
return if disable_ansi then s else `\27[31;1m{s}\27[0m`
end,
yellow = function(s: string): string
return if disable_ansi then s else `\27[33;1m{s}\27[0m`
end,
red_highlight = function(s: string): string
return if disable_ansi then s else `\27[41;1;30m{s}\27[0m`
end,
green_highlight = function(s: string): string
return if disable_ansi then s else `\27[42;1;30m{s}\27[0m`
end,
gray = function(s: string): string
return if disable_ansi then s else `\27[38;1m{s}\27[0m`
end,
orange = function(s: string): string
return if disable_ansi then s else `\27[38;5;208m{s}\27[0m`
end,
}
local function convert_units(unit: string, value: number): (number, string)
local sign = math.sign(value)
value = math.abs(value)
local prefix_colors = {
[4] = color.red,
[3] = color.red,
[2] = color.yellow,
[1] = color.yellow,
[0] = color.green,
[-1] = color.red,
[-2] = color.yellow,
[-3] = color.green,
[-4] = color.red,
}
local prefixes = {
[4] = "T",
[3] = "G",
[2] = "M",
[1] = "k",
[0] = " ",
[-1] = "m",
[-2] = "u",
[-3] = "n",
[-4] = "p",
}
local order = 0
while value >= 1000 do
order += 1
value /= 1000
end
while value ~= 0 and value < 1 do
order -= 1
value *= 1000
end
if value >= 100 then
value = math.floor(value)
elseif value >= 10 then
value = math.floor(value * 1e1) / 1e1
elseif value >= 1 then
value = math.floor(value * 1e2) / 1e2
end
return value * sign, prefix_colors[order](prefixes[order] .. unit)
end
local WALL = color.gray("│")
--------------------------------------------------------------------------------
-- Testing
--------------------------------------------------------------------------------
type Test = {
name: string,
case: Case?,
cases: { Case },
duration: number,
error: {
message: string,
trace: string,
}?,
focus: boolean,
}
type Case = {
name: string,
result: number,
line: number?,
focus: boolean,
}
local PASS, FAIL, NONE, ERROR, SKIPPED = 1, 2, 3, 4, 5
local check_for_focused = false
local skip = false
local test: Test?
local tests: { Test } = {}
local function output_test_result(test: Test)
if check_for_focused then
local any_focused = test.focus
for _, case in test.cases do
any_focused = any_focused or case.focus
end
if not any_focused then
return
end
end
print(color.white(test.name))
for _, case in test.cases do
local status = ({
[PASS] = color.green("PASS"),
[FAIL] = color.red("FAIL"),
[NONE] = color.orange("NONE"),
[ERROR] = color.red("FAIL"),
[SKIPPED] = color.yellow("SKIP"),
})[case.result]
local line = case.result == FAIL and color.red(`{case.line}:`) or ""
if check_for_focused and case.focus == false and test.focus == false then
continue
end
print(`{status}{WALL} {line}{color.gray(case.name)}`)
end
if test.error then
print(color.gray("error: ") .. color.red(test.error.message))
print(color.gray("trace: ") .. color.red(test.error.trace))
else
print()
end
end
local function CASE(name: string)
skip = false
assert(test, "no active test")
local case = {
name = name,
result = NONE,
focus = false,
}
test.case = case
table.insert(test.cases, case)
end
local function CHECK_EXPECT_ERR(fn, ...)
assert(test, "no active test")
local case = test.case
if not case then
CASE("")
case = test.case
end
assert(case, "no active case")
if case.result ~= FAIL then
local ok, err = pcall(fn, ...)
case.result = if ok then FAIL else PASS
if skip then
case.result = SKIPPED
end
case.line = debug.info(stack and stack + 1 or 2, "l")
end
end
local function CHECK<T>(value: T, stack: number?): T?
assert(test, "no active test")
local case = test.case
if not case then
CASE("")
case = test.case
end
assert(case, "no active case")
if case.result ~= FAIL then
case.result = value and PASS or FAIL
if skip then
case.result = SKIPPED
end
case.line = debug.info(stack and stack + 1 or 2, "l")
end
return value
end
local function TEST(name: string, fn: () -> ())
local active = test
assert(not active, "cannot start test while another test is in progress")
test = {
name = name,
cases = {},
duration = 0,
focus = false,
}
assert(test)
table.insert(tests, test)
local start = os.clock()
local err
local success = xpcall(fn, function(m: string)
err = { message = m, trace = debug.traceback(nil, 2) }
end)
test.duration = os.clock() - start
if not test.case then
CASE("")
end
assert(test.case, "no active case")
if not success then
test.case.result = ERROR
test.error = err
end
test = nil
end
local function FOCUS()
assert(test, "no active test")
check_for_focused = true
if test.case then
test.case.focus = true
else
test.focus = true
end
end
local function FINISH(): boolean
local success = true
local total_cases = 0
local passed_cases = 0
local passed_focus_cases = 0
local total_focus_cases = 0
local duration = 0
for _, test in tests do
duration += test.duration
for _, case in test.cases do
total_cases += 1
if case.focus or test.focus then
total_focus_cases += 1
end
if case.result == PASS or case.result == NONE or case.result == SKIPPED then
if case.focus or test.focus then
passed_focus_cases += 1
end
passed_cases += 1
else
success = false
end
end
output_test_result(test)
end
print(color.gray(string.format(`{passed_cases}/{total_cases} test cases passed in %.3f ms.`, duration * 1e3)))
if check_for_focused then
print(color.gray(`{passed_focus_cases}/{total_focus_cases} focused test cases passed`))
end
local fails = total_cases - passed_cases
print((fails > 0 and color.red or color.green)(`{fails} {fails == 1 and "fail" or "fails"}`))
check_for_focused = false
return success, table.clear(tests)
end
local function SKIP()
skip = true
end
--------------------------------------------------------------------------------
-- Benchmarking
--------------------------------------------------------------------------------
type Bench = {
time_start: number?,
memory_start: number?,
iterations: number?,
}
local bench: Bench?
function START(iter: number?): number
local n = iter or 1
assert(n > 0, "iterations must be greater than 0")
assert(bench, "no active benchmark")
assert(not bench.time_start, "clock was already started")
bench.iterations = n
bench.memory_start = gcinfo()
bench.time_start = os.clock()
return n
end
local function BENCH(name: string, fn: () -> ())
local active = bench
assert(not active, "a benchmark is already in progress")
bench = {}
assert(bench);
(collectgarbage :: any)("collect")
local mem_start = gcinfo()
local time_start = os.clock()
local err_msg: string?
local success = xpcall(fn, function(m: string)
err_msg = m .. debug.traceback(nil, 2)
end)
local time_stop = os.clock()
local mem_stop = gcinfo()
if not success then
print(`{WALL}{color.red("ERROR")}{WALL} {name}`)
print(color.gray(err_msg :: string))
else
time_start = bench.time_start or time_start
mem_start = bench.memory_start or mem_start
local n = bench.iterations or 1
local d, d_unit = convert_units("s", (time_stop - time_start) / n)
local a, a_unit = convert_units("B", math.round((mem_stop - mem_start) / n * 1e3))
local function round(x: number): string
return x > 0 and x < 10 and (x - math.floor(x)) > 0 and string.format("%2.1f", x)
or string.format("%3.f", x)
end
print(
string.format(
`%s %s %s %s{WALL} %s`,
color.gray(round(d)),
d_unit,
color.gray(round(a)),
a_unit,
color.gray(name)
)
)
end
bench = nil
end
--------------------------------------------------------------------------------
-- Printing
--------------------------------------------------------------------------------
local function print2(v: unknown)
type Buffer = { n: number, [number]: string }
type Cyclic = { n: number, [{}]: number }
-- overkill concatenationless string buffer
local function tos(value: any, stack: number, str: Buffer, cyclic: Cyclic)
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 -- is table
local tabbed_indent = indent .. TAB
if cyclic[value] then
str.n += 1
str[str.n] = color.gray(`CYCLIC REF {cyclic[value]}`)
return
else
cyclic.n += 1
cyclic[value] = cyclic.n
end
str.n += 3
str[str.n - 2] = "{ "
str[str.n - 1] = color.gray(tostring(cyclic[value]))
str[str.n - 0] = "\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, cyclic)
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 }
local cyclic = { n = 0 }
tos(v, 0, str, cyclic)
print(table.concat(str))
end
--------------------------------------------------------------------------------
-- Equality
--------------------------------------------------------------------------------
local function shallow_eq(a: {}, b: {}): boolean
if #a ~= #b then
return false
end
for i, v in next, a do
if b[i] ~= v then
return false
end
end
for i, v in next, b do
if a[i] ~= v then
return false
end
end
return true
end
local function deep_eq(a: {}, b: {}): boolean
if #a ~= #b then
return false
end
for i, v in next, a do
if type(b[i]) == "table" and type(v) == "table" then
if deep_eq(b[i], v) == false then
return false
end
elseif b[i] ~= v then
return false
end
end
for i, v in next, b do
if type(a[i]) == "table" and type(v) == "table" then
if deep_eq(a[i], v) == false then
return false
end
elseif a[i] ~= v then
return false
end
end
return true
end
--------------------------------------------------------------------------------
-- Return
--------------------------------------------------------------------------------
return {
test = function()
return {
TEST = TEST,
CASE = CASE,
CHECK = CHECK,
FINISH = FINISH,
SKIP = SKIP,
FOCUS = FOCUS,
CHECK_EXPECT_ERR = CHECK_EXPECT_ERR
}
end,
benchmark = function()
return BENCH, START
end,
disable_formatting = function()
disable_ansi = true
end,
print = print2,
seq = shallow_eq,
deq = deep_eq,
color = color,
}

View file

@ -177,6 +177,10 @@ local function release(origin: string, scopes: { wally: string?, pesde: string?
print(`-- Pesde out:\nstdout\n{res_pesde.stdout}\nstderr\n{res_pesde.stderr}`) print(`-- Pesde out:\nstdout\n{res_pesde.stdout}\nstderr\n{res_pesde.stderr}`)
end end
if res_pesde_rbx then
print(`-- Pesde rbx out:\nstdout\n{res_pesde.stdout}\nstderr\n{res_pesde.stderr}`)
end
if res_wally then if res_wally then
print(`-- Wally out:\nstdout\n{res_wally.stdout}\nstderr\n{res_wally.stderr}`) print(`-- Wally out:\nstdout\n{res_wally.stdout}\nstderr\n{res_wally.stderr}`)
end end

View file

@ -82,6 +82,7 @@ local function test(origin: string): result.Identity<boolean>
progress:nextStage() -- cleanup progress:nextStage() -- cleanup
fs.removeDir(`{origin}/test`) fs.removeDir(`{origin}/test`)
fs.removeDir(`{origin}/tools`)
progress:nextStage() -- metadata progress:nextStage() -- metadata