This commit is contained in:
lx
2026-07-06 14:01:11 -04:00
commit c4f97d729d
16468 changed files with 935321 additions and 0 deletions
@@ -0,0 +1,12 @@
local Enums = {
Type = {
Describe = "Describe",
Step = "Step",
},
Modifier = {
Protect = "Protect",
Skip = "Skip"
},
}
return Enums
@@ -0,0 +1,89 @@
local TestService = game:GetService("TestService")
local Enums = require(script.Parent.Parent.Enums)
local TeamCityReporter = {}
local function teamCityEscape(str)
str = string.gsub(str, "([]|'[])","|%1")
str = string.gsub(str, "\r", "|r")
str = string.gsub(str, "\n", "|n")
return str
end
local function teamCityEnterSuite(suiteName)
return string.format("##teamcity[testSuiteStarted name='%s']", teamCityEscape(suiteName))
end
local function teamCityLeaveSuite(suiteName)
return string.format("##teamcity[testSuiteFinished name='%s']", teamCityEscape(suiteName))
end
local function teamCityEnterCase(caseName)
return string.format("##teamcity[testStarted name='%s']", teamCityEscape(caseName))
end
local function teamCityLeaveCase(caseName)
return string.format("##teamcity[testFinished name='%s']", teamCityEscape(caseName))
end
local function teamCityFailCase(caseName, errorMessage)
return string.format("##teamcity[testFailed name='%s' message='%s']",
teamCityEscape(caseName), teamCityEscape(errorMessage))
end
local function reportNode(node, buffer, level)
buffer = buffer or {}
level = level or 0
if not node.testTouched then
return buffer
end
if node.type == Enums.Type.Describe then
table.insert(buffer, teamCityEnterSuite(node.text))
for _, child in ipairs(node.children) do
reportNode(child, buffer, level + 1)
end
table.insert(buffer, teamCityLeaveSuite(node.text))
elseif node.type == Enums.Type.Step then
table.insert(buffer, teamCityEnterCase(node.text))
if not node.testSuccess then
table.insert(buffer, teamCityFailCase(node.text, node.errorMessage))
end
table.insert(buffer, teamCityLeaveCase(node.text))
end
end
local function treeToString(testNode)
local result = {}
for _, child in ipairs(testNode.children) do
reportNode(child, result)
end
return table.concat(result, "\n")
end
function TeamCityReporter.report(results)
local resultBuffer = {
"Test results:",
treeToString(results.testNode),
string.format(
"%d passed, %d failed, %d skipped",
results.successCount,
results.failureCount,
results.skippedCount
)
}
print(table.concat(resultBuffer, "\n"))
if results.failureCount > 0 then
print(("%d test nodes reported failures."):format(results.failureCount))
end
if #results.errors > 0 then
print("Errors reported by tests:\n")
for _, message in ipairs(results.errors) do
TestService:Error(message)
print("")
end
end
end
return TeamCityReporter
@@ -0,0 +1,49 @@
local TestService = game:GetService("TestService")
local TextReporter = {}
local INDENT = (" "):rep(3)
local function treeToString(testNode)
local result = {}
local function callback(node, level)
local symbol = node.testTouched and (node.testSuccess and "+" or "-") or "~"
local line = ("%s[%s] %s"):format(
INDENT:rep(level),
symbol,
node.text
)
table.insert(result, line)
end
testNode:visit(callback)
return table.concat(result, "\n")
end
function TextReporter.report(results)
local resultBuffer = {
"Test results:",
treeToString(results.testNode),
string.format(
"%d passed, %d failed, %d skipped",
results.successCount,
results.failureCount,
results.skippedCount
)
}
print(table.concat(resultBuffer, "\n"))
if results.failureCount > 0 then
print(("%d test nodes reported failures."):format(results.failureCount))
end
if #results.errors > 0 then
print("Errors reported by tests:\n")
for _, message in ipairs(results.errors) do
TestService:Error(message)
print("")
end
end
end
return TextReporter
@@ -0,0 +1,39 @@
local TestNode = {}
TestNode.__index = TestNode
function TestNode.new(text, type, modifiers)
local self = {
parent = nil,
children = {},
text = text,
type = type,
modifiers = modifiers or {},
callback = nil,
testSuccess = true,
testTouched = false,
loadSuccess = true,
errorMessage = "",
}
setmetatable(self, TestNode)
return self
end
function TestNode:addChild(node)
node.parent = self
table.insert(self.children, node)
end
function TestNode:visit(callback, level)
level = level or 0
callback(self, level);
for _, child in ipairs(self.children) do
child:visit(callback, level + 1)
end
end
function TestNode:isRoot()
return self.parent == nil
end
return TestNode
@@ -0,0 +1,70 @@
local Enums = require(script.Parent.Enums)
local TestNode = require(script.Parent.TestNode)
local createModdableFunction = require(script.Parent.createModdableFunction)
local TestPlanner = {}
TestPlanner.__index = TestPlanner
local function doPlan(currentNode, method, env)
local currentEnv = getfenv(method)
for key, value in pairs(env) do
currentEnv[key] = value
end
local success, result = xpcall(method, function(err)
return err .. "\n" .. debug.traceback()
end)
if not success then
currentNode.loadSuccess = false
currentNode.errorMessage = result
end
end
local validModifiers = {
protected = Enums.Modifier.Protect,
skipped = Enums.Modifier.Skip,
}
local function createEnv(currentNode)
local function describe(modifiers, text, callback)
local testNode = TestNode.new(text, Enums.Type.Describe, modifiers)
currentNode:addChild(testNode)
currentNode = testNode
local success, result = xpcall(callback, function(err)
return err .. "\n" .. debug.traceback()
end)
if not success then
testNode.loadSuccess = false
testNode.errorMessage = result
end
currentNode = currentNode.parent
end
local function step(modifiers, text, callback)
local testNode = TestNode.new(text, Enums.Type.Step, modifiers)
testNode.callback = callback
currentNode:addChild(testNode)
end
local env = {}
env.describe = createModdableFunction(validModifiers, describe)
env.step = createModdableFunction(validModifiers, step)
env.skip = function()
currentNode.modifiers[Enums.Modifier.Skip] = true
end
env.include = function(method)
doPlan(currentNode, method, env)
end
return env
end
function TestPlanner.plan(method)
local currentNode = TestNode.new("Root", Enums.Type.Describe)
local env = createEnv(currentNode)
doPlan(currentNode, method, env)
return currentNode
end
return TestPlanner
@@ -0,0 +1,36 @@
local Enums = require(script.Parent.Enums)
local TestResult = {}
TestResult.__index = TestResult
function TestResult.getResult(testNode)
local result = {
testNode = testNode,
successCount = 0,
failureCount = 0,
skippedCount = 0,
errors = {}
}
local function callback(node, level)
if node.type == Enums.Type.Describe then
if not node.loadSuccess then
result.failureCount = result.failureCount + 1
table.insert(result.errors, node.errorMessage)
end
elseif node.type == Enums.Type.Step then
if not node.testTouched then
result.skippedCount = result.skippedCount + 1
elseif node.testSuccess then
result.successCount = result.successCount + 1
else
result.failureCount = result.failureCount + 1
table.insert(result.errors, node.errorMessage)
end
end
end
testNode:visit(callback)
return result
end
return TestResult
@@ -0,0 +1,72 @@
local CorePackages = game:GetService("CorePackages")
local Enums = require(script.Parent.Enums)
local Expectation = require(CorePackages.TestEZ).Expectation
local TestRunner = {}
local testEnv = {expect = Expectation.new}
local function assign(to, from)
for key, value in pairs(from) do
to[key] = value
end
end
local function setTestEnv(method)
assign(getfenv(method), testEnv)
end
local function run(testNode, protectionState)
if testNode.modifiers[Enums.Modifier.Skip] then
return
end
testNode.testTouched = true
if testNode.modifiers[Enums.Modifier.Protect] then
-- Update the protection value so that descendant nodes will reference this value
protectionState = {
failedNode = protectionState.failedNode
}
end
if testNode.type == Enums.Type.Describe then
for _, child in ipairs(testNode.children) do
run(child, protectionState)
end
elseif testNode.type == Enums.Type.Step then
if protectionState and protectionState.failedNode then
testNode.testSuccess = false
testNode.errorMessage = string.format(
"%q failed without execution, because %q failed",
testNode.text, protectionState.failedNode.text
)
else
setTestEnv(testNode.callback)
local success, result = pcall(testNode.callback)
if not success then
testNode.testSuccess = false
testNode.errorMessage = result
protectionState.failedNode = testNode
end
end
end
if not testNode.loadSuccess then
testNode.errorMessage = string.format("Error during planning: %q\n%s", testNode.text, testNode.errorMessage)
testNode.testSuccess = false
end
if testNode.parent and not testNode.testSuccess then
testNode.parent.testSuccess = false
end
end
function TestRunner.run(testNode)
-- protected by default, which means when one step failed, all steps after will be skipped by default
run(testNode, {
failedNode = nil
})
return testNode
end
return TestRunner
@@ -0,0 +1,39 @@
--[[createModdableFunction can be used to build a Moddable function in a chain style
for example:
validModifiers = {
protected = Enums.Modifier.Protect,
skipped = Enums.Modifier.Skip,
}
describe = createModdableFunction(validModifiers, callback)
describe.protected.skipped("description", function() end)
step.skipped("description", function() end)
]]
local function createModdableFunction(validModifiers, method, appliedModifiers)
if appliedModifiers == nil then
appliedModifiers = {}
end
return setmetatable({}, {
__call = function(_, ...)
return method(appliedModifiers, ...)
end,
__index = function(_, key)
if not validModifiers[key] then
error(("%q is not a valid modifier"):format(tostring(key)), 2)
end
local newAppliedModifiers = {}
for key, value in pairs(appliedModifiers) do
newAppliedModifiers[key] = value
end
newAppliedModifiers[validModifiers[key]] = true
return createModdableFunction(validModifiers, method, newAppliedModifiers)
end,
})
end
return createModdableFunction
@@ -0,0 +1,12 @@
local TestPlanner = require(script.Parent.TestPlanner)
local TestRunner = require(script.Parent.TestRunner)
local TestResult = require(script.Parent.TestResult)
local function startTest(method, reporter)
local testNode = TestPlanner.plan(method)
testNode = TestRunner.run(testNode)
local result = TestResult.getResult(testNode)
reporter.report(result)
end
return startTest