mirror of
https://github.com/littensy/charm.git
synced 2026-10-20 17:51:37 +00:00
refactor: improve logic
This commit is contained in:
parent
210e948225
commit
72f936591e
48 changed files with 434 additions and 349 deletions
12
test/init.luau
Normal file
12
test/init.luau
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
local suite = require("@test/suite")
|
||||
|
||||
if not table.isfrozen(_G) then
|
||||
_G.__DEV__ = true
|
||||
end
|
||||
|
||||
require("@pkgs/charm/tests/spec")
|
||||
|
||||
suite.finish({
|
||||
colors = true,
|
||||
verbose = false,
|
||||
})
|
||||
16
test/spec.server.luau
Normal file
16
test/spec.server.luau
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
local suite = require("./suite")
|
||||
|
||||
if not script then
|
||||
return
|
||||
end
|
||||
|
||||
_G.__DEV__ = true
|
||||
|
||||
require(script.Parent.CharmSync.spec)
|
||||
require(script.Parent.ReactCharm.spec)
|
||||
require(script.Parent.VideCharm.spec)
|
||||
|
||||
suite.finish({
|
||||
colors = false,
|
||||
verbose = true,
|
||||
})
|
||||
438
test/suite.luau
Normal file
438
test/suite.luau
Normal file
|
|
@ -0,0 +1,438 @@
|
|||
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 `\u{1F7E2} {text}`
|
||||
end,
|
||||
red = function(text: string)
|
||||
return if options.colors then `\27[31;1m{text}\27[0m` else `\u{1F534} {text}`
|
||||
end,
|
||||
blue = function(text: string)
|
||||
return if options.colors then `\27[34;1m{text}\27[0m` else `\u{1F535} {text}`
|
||||
end,
|
||||
}
|
||||
|
||||
local PENDING = 0
|
||||
local PASS = 1
|
||||
local FAIL = 2
|
||||
local SKIP = 3
|
||||
local ERROR = 4
|
||||
|
||||
local tests: { Test } = {}
|
||||
local descriptions: { Description } = {}
|
||||
local root: Description = {
|
||||
status = PENDING,
|
||||
name = "root",
|
||||
only = false,
|
||||
skip = false,
|
||||
children = {},
|
||||
passes = 0,
|
||||
fails = 0,
|
||||
skips = 0,
|
||||
runtime = 0,
|
||||
}
|
||||
local parent: Description = root
|
||||
|
||||
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
|
||||
root.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
|
||||
root.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
|
||||
root.only = true
|
||||
return this
|
||||
end
|
||||
|
||||
local function isOnly(test: Test): boolean
|
||||
if not root.only or test.only then
|
||||
return true
|
||||
end
|
||||
|
||||
local description = test.parent
|
||||
while description and description ~= root 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 ~= root 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 ~= root 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 ~= root 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(`{root.passes} passed`, root.fails == 0)} {color.gray(`({#tests})`)}`
|
||||
local duration = `{color.blue(`{string.format("%.2fms", root.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 root.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 root.children do
|
||||
reportNode(child)
|
||||
end
|
||||
|
||||
reportSummary()
|
||||
reportErrors()
|
||||
|
||||
print(table.concat(output, "\n"))
|
||||
|
||||
if root.fails > 0 then
|
||||
error(color.red(`{root.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(root)
|
||||
report()
|
||||
end
|
||||
|
||||
local testCtor = setmetatable({
|
||||
skip = testSkip,
|
||||
only = testOnly,
|
||||
}, {
|
||||
__call = function(_, ...)
|
||||
return test(...)
|
||||
end,
|
||||
})
|
||||
|
||||
local describeCtor = setmetatable({
|
||||
skip = describeSkip,
|
||||
only = describeOnly,
|
||||
}, {
|
||||
__call = function(_, ...)
|
||||
return describe(...)
|
||||
end,
|
||||
})
|
||||
|
||||
return {
|
||||
test = testCtor :: ((name: string, callback: () -> ()) -> ()) & {
|
||||
skip: (name: string, callback: () -> ()) -> (),
|
||||
only: (name: string, callback: () -> ()) -> (),
|
||||
},
|
||||
describe = describeCtor :: ((name: string, callback: () -> ()) -> ()) & {
|
||||
skip: (name: string, callback: () -> ()) -> (),
|
||||
only: (name: string, callback: () -> ()) -> (),
|
||||
},
|
||||
skip = skip,
|
||||
only = only,
|
||||
finish = finish,
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue