mirror of
https://github.com/littensy/charm.git
synced 2026-10-20 17:51:37 +00:00
431 lines
8.9 KiB
Text
431 lines
8.9 KiB
Text
type Config = {
|
|
colors: boolean?,
|
|
verbose: boolean?,
|
|
}
|
|
|
|
type Description = {
|
|
status: number,
|
|
name: string,
|
|
only: boolean,
|
|
skip: boolean,
|
|
parent: Description?,
|
|
children: { Description | Test },
|
|
passes: number,
|
|
fails: number,
|
|
skips: number,
|
|
runtime: number,
|
|
}
|
|
|
|
type Test = {
|
|
status: number,
|
|
name: string,
|
|
only: boolean,
|
|
skip: boolean,
|
|
parent: Description?,
|
|
children: nil,
|
|
run: () -> (),
|
|
result: unknown?,
|
|
line: number?,
|
|
runtime: number,
|
|
}
|
|
|
|
local options = {
|
|
colors = true,
|
|
verbose = false,
|
|
}
|
|
|
|
local color = {
|
|
white = function(text: string)
|
|
return if options.colors then `\27[37;1m{text}\27[0m` else text
|
|
end,
|
|
gray = function(text: string)
|
|
return if options.colors then `\27[90;1m{text}\27[0m` else text
|
|
end,
|
|
green = function(text: string)
|
|
return if options.colors then `\27[32;1m{text}\27[0m` else text
|
|
end,
|
|
red = function(text: string)
|
|
return if options.colors then `\27[31;1m{text}\27[0m` else text
|
|
end,
|
|
blue = function(text: string)
|
|
return if options.colors then `\27[34;1m{text}\27[0m` else text
|
|
end,
|
|
}
|
|
|
|
local PENDING = 0
|
|
local PASS = 1
|
|
local FAIL = 2
|
|
local SKIP = 3
|
|
local ERROR = 4
|
|
|
|
local tests: { Test } = {}
|
|
local descriptions: { Description } = {}
|
|
local parent: Description = {
|
|
status = PENDING,
|
|
name = "root",
|
|
only = false,
|
|
skip = false,
|
|
children = {},
|
|
passes = 0,
|
|
fails = 0,
|
|
skips = 0,
|
|
runtime = 0,
|
|
}
|
|
|
|
local function describe(name: string, run: () -> ()): Description
|
|
local this: Description = {
|
|
status = PENDING,
|
|
name = name,
|
|
only = false,
|
|
skip = false,
|
|
parent = parent,
|
|
children = {},
|
|
passes = 0,
|
|
fails = 0,
|
|
skips = 0,
|
|
runtime = 0,
|
|
}
|
|
|
|
local prevDescription = parent
|
|
parent = this
|
|
run()
|
|
parent = prevDescription
|
|
|
|
table.insert(parent.children, this)
|
|
table.insert(descriptions, this)
|
|
|
|
return this
|
|
end
|
|
|
|
local function test(name: string, run: () -> ()): Test
|
|
local this: Test = {
|
|
status = PENDING,
|
|
name = name,
|
|
only = false,
|
|
skip = false,
|
|
parent = parent,
|
|
run = run,
|
|
runtime = 0,
|
|
}
|
|
|
|
table.insert(parent.children, this)
|
|
table.insert(tests, this)
|
|
|
|
return this
|
|
end
|
|
|
|
local function skip()
|
|
parent.skip = true
|
|
end
|
|
|
|
local function only()
|
|
parent.only = true
|
|
parent.only = true
|
|
end
|
|
|
|
local function testSkip(name: string, run: () -> ()): Test
|
|
local this = test(name, run)
|
|
this.skip = true
|
|
return this
|
|
end
|
|
|
|
local function testOnly(name: string, run: () -> ()): Test
|
|
local this = test(name, run)
|
|
this.only = true
|
|
parent.only = true
|
|
return this
|
|
end
|
|
|
|
local function describeSkip(name: string, run: () -> ()): Description
|
|
local this = describe(name, run)
|
|
this.skip = true
|
|
return this
|
|
end
|
|
|
|
local function describeOnly(name: string, run: () -> ()): Description
|
|
local this = describe(name, run)
|
|
this.only = true
|
|
parent.only = true
|
|
return this
|
|
end
|
|
|
|
local function isOnly(test: Test): boolean
|
|
if not parent.only or test.only then
|
|
return true
|
|
end
|
|
|
|
local description = test.parent
|
|
while description and description ~= parent do
|
|
if description.only then
|
|
return true
|
|
elseif description.skip then
|
|
return false
|
|
end
|
|
description = description.parent
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
local function isSkip(test: Test): boolean
|
|
if test.only then
|
|
return false
|
|
elseif test.skip then
|
|
return true
|
|
end
|
|
|
|
local description = test.parent
|
|
while description do
|
|
if description.only and description ~= parent then
|
|
return false
|
|
elseif description.skip then
|
|
return true
|
|
end
|
|
description = description.parent
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
local function runTests()
|
|
for _, test in tests do
|
|
if isSkip(test) or not isOnly(test) then
|
|
test.status = SKIP
|
|
continue
|
|
end
|
|
|
|
local start = os.clock()
|
|
local success = xpcall(test.run, function(result)
|
|
test.status = FAIL
|
|
test.result = result
|
|
test.line = debug.info(3, "l")
|
|
end)
|
|
|
|
test.runtime = os.clock() - start
|
|
|
|
if success then
|
|
test.status = PASS
|
|
end
|
|
end
|
|
end
|
|
|
|
local function updateDescription(description: Description)
|
|
if not description.children[1] then
|
|
description.status = PASS
|
|
return
|
|
end
|
|
|
|
description.status = SKIP
|
|
|
|
for _, child in description.children do
|
|
if child.children then
|
|
updateDescription(child)
|
|
description.passes += child.passes
|
|
description.fails += child.fails
|
|
elseif child.status == PASS then
|
|
description.passes += 1
|
|
elseif child.status == FAIL then
|
|
description.fails += 1
|
|
end
|
|
|
|
-- Only count skips in direct children
|
|
if child.status == SKIP then
|
|
description.skips += 1
|
|
end
|
|
|
|
description.runtime += child.runtime
|
|
|
|
if child.status == FAIL then
|
|
description.status = FAIL
|
|
elseif description.status == SKIP and child.status == PASS then
|
|
description.status = PASS
|
|
end
|
|
end
|
|
end
|
|
|
|
local function getIndent(node: Description | Test): string
|
|
local depth = 0
|
|
local description = node.parent
|
|
while description and description ~= parent do
|
|
depth += 1
|
|
description = description.parent
|
|
end
|
|
return if depth > 0 then string.rep(" ", depth - 1) .. color.gray("· ") else ""
|
|
end
|
|
|
|
local function getPath(node: Description | Test): string
|
|
local path = { "" }
|
|
local description = node.parent
|
|
while description and description ~= parent do
|
|
table.insert(path, 1, description.name)
|
|
description = description.parent
|
|
end
|
|
return table.concat(path, " > ")
|
|
end
|
|
|
|
local function greenOrRed(text: string, good: boolean): string
|
|
return if good then color.green(text) else color.red(text)
|
|
end
|
|
|
|
local function colorStatus(status: number): string
|
|
local wall = color.gray("│")
|
|
|
|
if status == PASS then
|
|
return color.green("PASS") .. wall
|
|
elseif status == FAIL then
|
|
return color.red("FAIL") .. wall
|
|
elseif status == SKIP then
|
|
return color.blue("SKIP") .. wall
|
|
else
|
|
return color.red("ERROR") .. wall
|
|
end
|
|
end
|
|
|
|
local function report()
|
|
local output = {}
|
|
|
|
local function reportDescription(description: Description)
|
|
local name = color.white(description.name)
|
|
local text = `{colorStatus(description.status)} {getIndent(description)}{name}`
|
|
|
|
if description.fails > 0 then
|
|
text ..= " " .. color.red(`({description.fails})`)
|
|
end
|
|
|
|
if description.skips > 0 then
|
|
text ..= " " .. color.blue(`({description.skips} skipped)`)
|
|
elseif description.fails == 0 then
|
|
text ..= " " .. color.gray(`({string.format("%.2fms", description.runtime * 1000)})`)
|
|
end
|
|
|
|
table.insert(output, text)
|
|
end
|
|
|
|
local function reportTest(test: Test, reportError: boolean?)
|
|
if test.status ~= FAIL and not options.verbose then
|
|
return
|
|
end
|
|
|
|
local line = color.red(`{test.line}:`)
|
|
|
|
if reportError then
|
|
local name = color.white(test.name)
|
|
local path = color.gray(getPath(test))
|
|
|
|
table.insert(output, `{colorStatus(ERROR)} {path}{line}{name}`)
|
|
table.insert(output, `{test.result}`)
|
|
else
|
|
local name = color.gray(test.name)
|
|
local indent = getIndent(test)
|
|
|
|
if test.status == FAIL then
|
|
table.insert(output, `{colorStatus(FAIL)} {indent}{line}{name}`)
|
|
else
|
|
table.insert(output, `{colorStatus(test.status)} {indent}{name}`)
|
|
end
|
|
end
|
|
end
|
|
|
|
local function reportNode(node: Description | Test)
|
|
if not node.children then
|
|
return reportTest(node :: Test)
|
|
end
|
|
|
|
reportDescription(node)
|
|
|
|
for _, child in node.children do
|
|
reportNode(child)
|
|
end
|
|
end
|
|
|
|
local function reportSummary()
|
|
local describePasses = 0
|
|
local describeFails = 0
|
|
|
|
for _, description in descriptions do
|
|
if description.status == PASS then
|
|
describePasses += 1
|
|
elseif description.status == FAIL then
|
|
describeFails += 1
|
|
end
|
|
end
|
|
|
|
local describes =
|
|
`{greenOrRed(`{describePasses} passed`, describeFails == 0)} {color.gray(`({#descriptions})`)}`
|
|
local tests = `{greenOrRed(`{parent.passes} passed`, parent.fails == 0)} {color.gray(`({#tests})`)}`
|
|
local duration = `{color.blue(`{string.format("%.2fms", parent.runtime * 1000)}`)}`
|
|
|
|
table.insert(output, "")
|
|
table.insert(output, `Describe {describes}`)
|
|
table.insert(output, ` Tests {tests}`)
|
|
table.insert(output, `Duration {duration}`)
|
|
table.insert(output, "")
|
|
end
|
|
|
|
local function reportErrors()
|
|
if parent.fails == 0 then
|
|
return
|
|
end
|
|
|
|
for _, test in tests do
|
|
if test.status == FAIL then
|
|
reportTest(test, true)
|
|
table.insert(output, "")
|
|
end
|
|
end
|
|
end
|
|
|
|
table.insert(output, "")
|
|
table.insert(output, color.blue("Test Results"))
|
|
table.insert(output, "")
|
|
|
|
for _, child in parent.children do
|
|
reportNode(child)
|
|
end
|
|
|
|
reportSummary()
|
|
reportErrors()
|
|
|
|
print(table.concat(output, "\n"))
|
|
|
|
if parent.fails > 0 then
|
|
error(color.red(`{parent.fails} test(s) failed`))
|
|
end
|
|
end
|
|
|
|
local function finish(config: Config?)
|
|
if config then
|
|
options.colors = if config.colors ~= nil then config.colors else options.colors
|
|
options.verbose = if config.verbose ~= nil then config.verbose else options.verbose
|
|
end
|
|
|
|
runTests()
|
|
updateDescription(parent)
|
|
report()
|
|
end
|
|
|
|
local testImpl = setmetatable({
|
|
skip = testSkip,
|
|
only = testOnly,
|
|
}, {
|
|
__call = function(_, name: string, run: () -> ())
|
|
return test(name, run)
|
|
end,
|
|
})
|
|
|
|
local describeImpl = setmetatable({
|
|
skip = describeSkip,
|
|
only = describeOnly,
|
|
}, {
|
|
__call = function(_, name: string, run: () -> ())
|
|
return describe(name, run)
|
|
end,
|
|
})
|
|
|
|
return {
|
|
test = testImpl,
|
|
describe = describeImpl,
|
|
skip = skip,
|
|
only = only,
|
|
finish = finish,
|
|
}
|