mirror of
https://github.com/imezx/Warp.git
synced 2026-06-02 12:18:32 +00:00
Delete TestEZ directory
This commit is contained in:
parent
e7a0803b7b
commit
4dbe9e3ddf
15 changed files with 0 additions and 1873 deletions
|
|
@ -1,26 +0,0 @@
|
||||||
--[[
|
|
||||||
The Context object implements a write-once key-value store. It also allows
|
|
||||||
for a new Context object to inherit the entries from an existing one.
|
|
||||||
]]
|
|
||||||
local Context = {}
|
|
||||||
|
|
||||||
function Context.new(parent)
|
|
||||||
local meta = {}
|
|
||||||
local index = {}
|
|
||||||
meta.__index = index
|
|
||||||
|
|
||||||
if parent then
|
|
||||||
for key, value in pairs(getmetatable(parent).__index) do
|
|
||||||
index[key] = value
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function meta.__newindex(_obj, key, value)
|
|
||||||
assert(index[key] == nil, string.format("Cannot reassign %s in context", tostring(key)))
|
|
||||||
index[key] = value
|
|
||||||
end
|
|
||||||
|
|
||||||
return setmetatable({}, meta)
|
|
||||||
end
|
|
||||||
|
|
||||||
return Context
|
|
||||||
|
|
@ -1,311 +0,0 @@
|
||||||
--[[
|
|
||||||
Allows creation of expectation statements designed for behavior-driven
|
|
||||||
testing (BDD). See Chai (JS) or RSpec (Ruby) for examples of other BDD
|
|
||||||
frameworks.
|
|
||||||
|
|
||||||
The Expectation class is exposed to tests as a function called `expect`:
|
|
||||||
|
|
||||||
expect(5).to.equal(5)
|
|
||||||
expect(foo()).to.be.ok()
|
|
||||||
|
|
||||||
Expectations can be negated using .never:
|
|
||||||
|
|
||||||
expect(true).never.to.equal(false)
|
|
||||||
|
|
||||||
Expectations throw errors when their conditions are not met.
|
|
||||||
]]
|
|
||||||
|
|
||||||
local Expectation = {}
|
|
||||||
|
|
||||||
--[[
|
|
||||||
These keys don't do anything except make expectations read more cleanly
|
|
||||||
]]
|
|
||||||
local SELF_KEYS = {
|
|
||||||
to = true,
|
|
||||||
be = true,
|
|
||||||
been = true,
|
|
||||||
have = true,
|
|
||||||
was = true,
|
|
||||||
at = true,
|
|
||||||
}
|
|
||||||
|
|
||||||
--[[
|
|
||||||
These keys invert the condition expressed by the Expectation.
|
|
||||||
]]
|
|
||||||
local NEGATION_KEYS = {
|
|
||||||
never = true,
|
|
||||||
}
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Extension of Lua's 'assert' that lets you specify an error level.
|
|
||||||
]]
|
|
||||||
local function assertLevel(condition, message, level)
|
|
||||||
message = message or "Assertion failed!"
|
|
||||||
level = level or 1
|
|
||||||
|
|
||||||
if not condition then
|
|
||||||
error(message, level + 1)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Returns a version of the given method that can be called with either . or :
|
|
||||||
]]
|
|
||||||
local function bindSelf(self, method)
|
|
||||||
return function(firstArg, ...)
|
|
||||||
if firstArg == self then
|
|
||||||
return method(self, ...)
|
|
||||||
else
|
|
||||||
return method(self, firstArg, ...)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function formatMessage(result, trueMessage, falseMessage)
|
|
||||||
if result then
|
|
||||||
return trueMessage
|
|
||||||
else
|
|
||||||
return falseMessage
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Create a new expectation
|
|
||||||
]]
|
|
||||||
function Expectation.new(value)
|
|
||||||
local self = {
|
|
||||||
value = value,
|
|
||||||
successCondition = true,
|
|
||||||
condition = false,
|
|
||||||
matchers = {},
|
|
||||||
_boundMatchers = {},
|
|
||||||
}
|
|
||||||
|
|
||||||
setmetatable(self, Expectation)
|
|
||||||
|
|
||||||
self.a = bindSelf(self, self.a)
|
|
||||||
self.an = self.a
|
|
||||||
self.ok = bindSelf(self, self.ok)
|
|
||||||
self.equal = bindSelf(self, self.equal)
|
|
||||||
self.throw = bindSelf(self, self.throw)
|
|
||||||
self.near = bindSelf(self, self.near)
|
|
||||||
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
function Expectation.checkMatcherNameCollisions(name)
|
|
||||||
if SELF_KEYS[name] or NEGATION_KEYS[name] or Expectation[name] then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
function Expectation:extend(matchers)
|
|
||||||
self.matchers = matchers or {}
|
|
||||||
|
|
||||||
for name, implementation in pairs(self.matchers) do
|
|
||||||
self._boundMatchers[name] = bindSelf(self, function(_self, ...)
|
|
||||||
local result = implementation(self.value, ...)
|
|
||||||
local pass = result.pass == self.successCondition
|
|
||||||
|
|
||||||
assertLevel(pass, result.message, 3)
|
|
||||||
self:_resetModifiers()
|
|
||||||
return self
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
function Expectation.__index(self, key)
|
|
||||||
-- Keys that don't do anything except improve readability
|
|
||||||
if SELF_KEYS[key] then
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Invert your assertion
|
|
||||||
if NEGATION_KEYS[key] then
|
|
||||||
local newExpectation = Expectation.new(self.value):extend(self.matchers)
|
|
||||||
newExpectation.successCondition = not self.successCondition
|
|
||||||
|
|
||||||
return newExpectation
|
|
||||||
end
|
|
||||||
|
|
||||||
if self._boundMatchers[key] then
|
|
||||||
return self._boundMatchers[key]
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Fall back to methods provided by Expectation
|
|
||||||
return Expectation[key]
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Called by expectation terminators to reset modifiers in a statement.
|
|
||||||
|
|
||||||
This makes chains like:
|
|
||||||
|
|
||||||
expect(5)
|
|
||||||
.never.to.equal(6)
|
|
||||||
.to.equal(5)
|
|
||||||
|
|
||||||
Work as expected.
|
|
||||||
]]
|
|
||||||
function Expectation:_resetModifiers()
|
|
||||||
self.successCondition = true
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Assert that the expectation value is the given type.
|
|
||||||
|
|
||||||
expect(5).to.be.a("number")
|
|
||||||
]]
|
|
||||||
function Expectation:a(typeName)
|
|
||||||
local result = (type(self.value) == typeName) == self.successCondition
|
|
||||||
|
|
||||||
local message = formatMessage(self.successCondition,
|
|
||||||
("Expected value of type %q, got value %q of type %s"):format(
|
|
||||||
typeName,
|
|
||||||
tostring(self.value),
|
|
||||||
type(self.value)
|
|
||||||
),
|
|
||||||
("Expected value not of type %q, got value %q of type %s"):format(
|
|
||||||
typeName,
|
|
||||||
tostring(self.value),
|
|
||||||
type(self.value)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
assertLevel(result, message, 3)
|
|
||||||
self:_resetModifiers()
|
|
||||||
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Make alias public on class
|
|
||||||
Expectation.an = Expectation.a
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Assert that our expectation value is truthy
|
|
||||||
]]
|
|
||||||
function Expectation:ok()
|
|
||||||
local result = (self.value ~= nil) == self.successCondition
|
|
||||||
|
|
||||||
local message = formatMessage(self.successCondition,
|
|
||||||
("Expected value %q to be non-nil"):format(
|
|
||||||
tostring(self.value)
|
|
||||||
),
|
|
||||||
("Expected value %q to be nil"):format(
|
|
||||||
tostring(self.value)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
assertLevel(result, message, 3)
|
|
||||||
self:_resetModifiers()
|
|
||||||
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Assert that our expectation value is equal to another value
|
|
||||||
]]
|
|
||||||
function Expectation:equal(otherValue)
|
|
||||||
local result = (self.value == otherValue) == self.successCondition
|
|
||||||
|
|
||||||
local message = formatMessage(self.successCondition,
|
|
||||||
("Expected value %q (%s), got %q (%s) instead"):format(
|
|
||||||
tostring(otherValue),
|
|
||||||
type(otherValue),
|
|
||||||
tostring(self.value),
|
|
||||||
type(self.value)
|
|
||||||
),
|
|
||||||
("Expected anything but value %q (%s)"):format(
|
|
||||||
tostring(otherValue),
|
|
||||||
type(otherValue)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
assertLevel(result, message, 3)
|
|
||||||
self:_resetModifiers()
|
|
||||||
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Assert that our expectation value is equal to another value within some
|
|
||||||
inclusive limit.
|
|
||||||
]]
|
|
||||||
function Expectation:near(otherValue, limit)
|
|
||||||
assert(type(self.value) == "number", "Expectation value must be a number to use 'near'")
|
|
||||||
assert(type(otherValue) == "number", "otherValue must be a number")
|
|
||||||
assert(type(limit) == "number" or limit == nil, "limit must be a number or nil")
|
|
||||||
|
|
||||||
limit = limit or 1e-7
|
|
||||||
|
|
||||||
local result = (math.abs(self.value - otherValue) <= limit) == self.successCondition
|
|
||||||
|
|
||||||
local message = formatMessage(self.successCondition,
|
|
||||||
("Expected value to be near %f (within %f) but got %f instead"):format(
|
|
||||||
otherValue,
|
|
||||||
limit,
|
|
||||||
self.value
|
|
||||||
),
|
|
||||||
("Expected value to not be near %f (within %f) but got %f instead"):format(
|
|
||||||
otherValue,
|
|
||||||
limit,
|
|
||||||
self.value
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
assertLevel(result, message, 3)
|
|
||||||
self:_resetModifiers()
|
|
||||||
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Assert that our functoid expectation value throws an error when called.
|
|
||||||
An optional error message can be passed to assert that the error message
|
|
||||||
contains the given value.
|
|
||||||
]]
|
|
||||||
function Expectation:throw(messageSubstring)
|
|
||||||
local ok, err = pcall(self.value)
|
|
||||||
local result = ok ~= self.successCondition
|
|
||||||
|
|
||||||
if messageSubstring and not ok then
|
|
||||||
if self.successCondition then
|
|
||||||
result = err:find(messageSubstring, 1, true) ~= nil
|
|
||||||
else
|
|
||||||
result = err:find(messageSubstring, 1, true) == nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local message
|
|
||||||
|
|
||||||
if messageSubstring then
|
|
||||||
message = formatMessage(self.successCondition,
|
|
||||||
("Expected function to throw an error containing %q, but it %s"):format(
|
|
||||||
messageSubstring,
|
|
||||||
err and ("threw: %s"):format(err) or "did not throw."
|
|
||||||
),
|
|
||||||
("Expected function to never throw an error containing %q, but it threw: %s"):format(
|
|
||||||
messageSubstring,
|
|
||||||
tostring(err)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else
|
|
||||||
message = formatMessage(self.successCondition,
|
|
||||||
"Expected function to throw an error, but it did not throw.",
|
|
||||||
("Expected function to succeed, but it threw an error: %s"):format(
|
|
||||||
tostring(err)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
assertLevel(result, message, 3)
|
|
||||||
self:_resetModifiers()
|
|
||||||
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
return Expectation
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
local Expectation = require(script.Parent.Expectation)
|
|
||||||
local checkMatcherNameCollisions = Expectation.checkMatcherNameCollisions
|
|
||||||
|
|
||||||
local function copy(t)
|
|
||||||
local result = {}
|
|
||||||
|
|
||||||
for key, value in pairs(t) do
|
|
||||||
result[key] = value
|
|
||||||
end
|
|
||||||
|
|
||||||
return result
|
|
||||||
end
|
|
||||||
|
|
||||||
local ExpectationContext = {}
|
|
||||||
ExpectationContext.__index = ExpectationContext
|
|
||||||
|
|
||||||
function ExpectationContext.new(parent)
|
|
||||||
local self = {
|
|
||||||
_extensions = parent and copy(parent._extensions) or {},
|
|
||||||
}
|
|
||||||
|
|
||||||
return setmetatable(self, ExpectationContext)
|
|
||||||
end
|
|
||||||
|
|
||||||
function ExpectationContext:startExpectationChain(...)
|
|
||||||
return Expectation.new(...):extend(self._extensions)
|
|
||||||
end
|
|
||||||
|
|
||||||
function ExpectationContext:extend(config)
|
|
||||||
for key, value in pairs(config) do
|
|
||||||
assert(self._extensions[key] == nil, string.format("Cannot reassign %q in expect.extend", key))
|
|
||||||
assert(checkMatcherNameCollisions(key), string.format("Cannot overwrite matcher %q; it already exists", key))
|
|
||||||
|
|
||||||
self._extensions[key] = value
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return ExpectationContext
|
|
||||||
|
|
@ -1,89 +0,0 @@
|
||||||
local TestEnum = require(script.Parent.TestEnum)
|
|
||||||
|
|
||||||
local LifecycleHooks = {}
|
|
||||||
LifecycleHooks.__index = LifecycleHooks
|
|
||||||
|
|
||||||
function LifecycleHooks.new()
|
|
||||||
local self = {
|
|
||||||
_stack = {},
|
|
||||||
}
|
|
||||||
return setmetatable(self, LifecycleHooks)
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Returns an array of `beforeEach` hooks in FIFO order
|
|
||||||
]]
|
|
||||||
function LifecycleHooks:getBeforeEachHooks()
|
|
||||||
local key = TestEnum.NodeType.BeforeEach
|
|
||||||
local hooks = {}
|
|
||||||
|
|
||||||
for _, level in ipairs(self._stack) do
|
|
||||||
for _, hook in ipairs(level[key]) do
|
|
||||||
table.insert(hooks, hook)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return hooks
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Returns an array of `afterEach` hooks in FILO order
|
|
||||||
]]
|
|
||||||
function LifecycleHooks:getAfterEachHooks()
|
|
||||||
local key = TestEnum.NodeType.AfterEach
|
|
||||||
local hooks = {}
|
|
||||||
|
|
||||||
for _, level in ipairs(self._stack) do
|
|
||||||
for _, hook in ipairs(level[key]) do
|
|
||||||
table.insert(hooks, 1, hook)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return hooks
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Pushes uncalled beforeAll and afterAll hooks back up the stack
|
|
||||||
]]
|
|
||||||
function LifecycleHooks:popHooks()
|
|
||||||
table.remove(self._stack, #self._stack)
|
|
||||||
end
|
|
||||||
|
|
||||||
function LifecycleHooks:pushHooksFrom(planNode)
|
|
||||||
assert(planNode ~= nil)
|
|
||||||
|
|
||||||
table.insert(self._stack, {
|
|
||||||
[TestEnum.NodeType.BeforeAll] = self:_getHooksOfType(planNode.children, TestEnum.NodeType.BeforeAll),
|
|
||||||
[TestEnum.NodeType.AfterAll] = self:_getHooksOfType(planNode.children, TestEnum.NodeType.AfterAll),
|
|
||||||
[TestEnum.NodeType.BeforeEach] = self:_getHooksOfType(planNode.children, TestEnum.NodeType.BeforeEach),
|
|
||||||
[TestEnum.NodeType.AfterEach] = self:_getHooksOfType(planNode.children, TestEnum.NodeType.AfterEach),
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Get the beforeAll hooks from the current level.
|
|
||||||
]]
|
|
||||||
function LifecycleHooks:getBeforeAllHooks()
|
|
||||||
return self._stack[#self._stack][TestEnum.NodeType.BeforeAll]
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Get the afterAll hooks from the current level.
|
|
||||||
]]
|
|
||||||
function LifecycleHooks:getAfterAllHooks()
|
|
||||||
return self._stack[#self._stack][TestEnum.NodeType.AfterAll]
|
|
||||||
end
|
|
||||||
|
|
||||||
function LifecycleHooks:_getHooksOfType(nodes, key)
|
|
||||||
local hooks = {}
|
|
||||||
|
|
||||||
for _, node in ipairs(nodes) do
|
|
||||||
if node.type == key then
|
|
||||||
table.insert(hooks, node.callback)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return hooks
|
|
||||||
end
|
|
||||||
|
|
||||||
return LifecycleHooks
|
|
||||||
|
|
@ -1,102 +0,0 @@
|
||||||
local TestService = game:GetService("TestService")
|
|
||||||
|
|
||||||
local TestEnum = require(script.Parent.Parent.TestEnum)
|
|
||||||
|
|
||||||
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 node.status == TestEnum.TestStatus.Skipped then
|
|
||||||
return buffer
|
|
||||||
end
|
|
||||||
if node.planNode.type == TestEnum.NodeType.Describe then
|
|
||||||
table.insert(buffer, teamCityEnterSuite(node.planNode.phrase))
|
|
||||||
for _, child in ipairs(node.children) do
|
|
||||||
reportNode(child, buffer, level + 1)
|
|
||||||
end
|
|
||||||
table.insert(buffer, teamCityLeaveSuite(node.planNode.phrase))
|
|
||||||
else
|
|
||||||
table.insert(buffer, teamCityEnterCase(node.planNode.phrase))
|
|
||||||
if node.status == TestEnum.TestStatus.Failure then
|
|
||||||
table.insert(buffer, teamCityFailCase(node.planNode.phrase, table.concat(node.errors,"\n")))
|
|
||||||
end
|
|
||||||
table.insert(buffer, teamCityLeaveCase(node.planNode.phrase))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function reportRoot(node)
|
|
||||||
local buffer = {}
|
|
||||||
|
|
||||||
for _, child in ipairs(node.children) do
|
|
||||||
reportNode(child, buffer, 0)
|
|
||||||
end
|
|
||||||
|
|
||||||
return buffer
|
|
||||||
end
|
|
||||||
|
|
||||||
local function report(root)
|
|
||||||
local buffer = reportRoot(root)
|
|
||||||
|
|
||||||
return table.concat(buffer, "\n")
|
|
||||||
end
|
|
||||||
|
|
||||||
function TeamCityReporter.report(results)
|
|
||||||
local resultBuffer = {
|
|
||||||
"Test results:",
|
|
||||||
report(results),
|
|
||||||
("%d passed, %d failed, %d skipped"):format(
|
|
||||||
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:")
|
|
||||||
print("")
|
|
||||||
|
|
||||||
for _, message in ipairs(results.errors) do
|
|
||||||
TestService:Error(message)
|
|
||||||
|
|
||||||
-- Insert a blank line after each error
|
|
||||||
print("")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return TeamCityReporter
|
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
--[[
|
|
||||||
The TextReporter uses the results from a completed test to output text to
|
|
||||||
standard output and TestService.
|
|
||||||
]]
|
|
||||||
|
|
||||||
local TestService = game:GetService("TestService")
|
|
||||||
|
|
||||||
local TestEnum = require(script.Parent.Parent.TestEnum)
|
|
||||||
|
|
||||||
local INDENT = (" "):rep(3)
|
|
||||||
local STATUS_SYMBOLS = {
|
|
||||||
[TestEnum.TestStatus.Success] = "+",
|
|
||||||
[TestEnum.TestStatus.Failure] = "-",
|
|
||||||
[TestEnum.TestStatus.Skipped] = "~"
|
|
||||||
}
|
|
||||||
local UNKNOWN_STATUS_SYMBOL = "?"
|
|
||||||
|
|
||||||
local TextReporter = {}
|
|
||||||
|
|
||||||
local function compareNodes(a, b)
|
|
||||||
return a.planNode.phrase:lower() < b.planNode.phrase:lower()
|
|
||||||
end
|
|
||||||
|
|
||||||
local function reportNode(node, buffer, level)
|
|
||||||
buffer = buffer or {}
|
|
||||||
level = level or 0
|
|
||||||
|
|
||||||
if node.status == TestEnum.TestStatus.Skipped then
|
|
||||||
return buffer
|
|
||||||
end
|
|
||||||
|
|
||||||
local line
|
|
||||||
|
|
||||||
if node.status then
|
|
||||||
local symbol = STATUS_SYMBOLS[node.status] or UNKNOWN_STATUS_SYMBOL
|
|
||||||
|
|
||||||
line = ("%s[%s] %s"):format(
|
|
||||||
INDENT:rep(level),
|
|
||||||
symbol,
|
|
||||||
node.planNode.phrase
|
|
||||||
)
|
|
||||||
else
|
|
||||||
line = ("%s%s"):format(
|
|
||||||
INDENT:rep(level),
|
|
||||||
node.planNode.phrase
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
table.insert(buffer, line)
|
|
||||||
table.sort(node.children, compareNodes)
|
|
||||||
|
|
||||||
for _, child in ipairs(node.children) do
|
|
||||||
reportNode(child, buffer, level + 1)
|
|
||||||
end
|
|
||||||
|
|
||||||
return buffer
|
|
||||||
end
|
|
||||||
|
|
||||||
local function reportRoot(node)
|
|
||||||
local buffer = {}
|
|
||||||
table.sort(node.children, compareNodes)
|
|
||||||
|
|
||||||
for _, child in ipairs(node.children) do
|
|
||||||
reportNode(child, buffer, 0)
|
|
||||||
end
|
|
||||||
|
|
||||||
return buffer
|
|
||||||
end
|
|
||||||
|
|
||||||
local function report(root)
|
|
||||||
local buffer = reportRoot(root)
|
|
||||||
|
|
||||||
return table.concat(buffer, "\n")
|
|
||||||
end
|
|
||||||
|
|
||||||
function TextReporter.report(results)
|
|
||||||
local resultBuffer = {
|
|
||||||
"Test results:",
|
|
||||||
report(results),
|
|
||||||
("%d passed, %d failed, %d skipped"):format(
|
|
||||||
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:")
|
|
||||||
print("")
|
|
||||||
|
|
||||||
for _, message in ipairs(results.errors) do
|
|
||||||
TestService:Error(message)
|
|
||||||
|
|
||||||
-- Insert a blank line after each error
|
|
||||||
print("")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return TextReporter
|
|
||||||
|
|
@ -1,97 +0,0 @@
|
||||||
--[[
|
|
||||||
Copy of TextReporter that doesn't output successful tests.
|
|
||||||
|
|
||||||
This should be temporary, it's just a workaround to make CI environments
|
|
||||||
happy in the short-term.
|
|
||||||
]]
|
|
||||||
|
|
||||||
local TestService = game:GetService("TestService")
|
|
||||||
|
|
||||||
local TestEnum = require(script.Parent.Parent.TestEnum)
|
|
||||||
|
|
||||||
local INDENT = (" "):rep(3)
|
|
||||||
local STATUS_SYMBOLS = {
|
|
||||||
[TestEnum.TestStatus.Success] = "+",
|
|
||||||
[TestEnum.TestStatus.Failure] = "-",
|
|
||||||
[TestEnum.TestStatus.Skipped] = "~"
|
|
||||||
}
|
|
||||||
local UNKNOWN_STATUS_SYMBOL = "?"
|
|
||||||
|
|
||||||
local TextReporterQuiet = {}
|
|
||||||
|
|
||||||
local function reportNode(node, buffer, level)
|
|
||||||
buffer = buffer or {}
|
|
||||||
level = level or 0
|
|
||||||
|
|
||||||
if node.status == TestEnum.TestStatus.Skipped then
|
|
||||||
return buffer
|
|
||||||
end
|
|
||||||
|
|
||||||
local line
|
|
||||||
|
|
||||||
if node.status ~= TestEnum.TestStatus.Success then
|
|
||||||
local symbol = STATUS_SYMBOLS[node.status] or UNKNOWN_STATUS_SYMBOL
|
|
||||||
|
|
||||||
line = ("%s[%s] %s"):format(
|
|
||||||
INDENT:rep(level),
|
|
||||||
symbol,
|
|
||||||
node.planNode.phrase
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
table.insert(buffer, line)
|
|
||||||
|
|
||||||
for _, child in ipairs(node.children) do
|
|
||||||
reportNode(child, buffer, level + 1)
|
|
||||||
end
|
|
||||||
|
|
||||||
return buffer
|
|
||||||
end
|
|
||||||
|
|
||||||
local function reportRoot(node)
|
|
||||||
local buffer = {}
|
|
||||||
|
|
||||||
for _, child in ipairs(node.children) do
|
|
||||||
reportNode(child, buffer, 0)
|
|
||||||
end
|
|
||||||
|
|
||||||
return buffer
|
|
||||||
end
|
|
||||||
|
|
||||||
local function report(root)
|
|
||||||
local buffer = reportRoot(root)
|
|
||||||
|
|
||||||
return table.concat(buffer, "\n")
|
|
||||||
end
|
|
||||||
|
|
||||||
function TextReporterQuiet.report(results)
|
|
||||||
local resultBuffer = {
|
|
||||||
"Test results:",
|
|
||||||
report(results),
|
|
||||||
("%d passed, %d failed, %d skipped"):format(
|
|
||||||
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:")
|
|
||||||
print("")
|
|
||||||
|
|
||||||
for _, message in ipairs(results.errors) do
|
|
||||||
TestService:Error(message)
|
|
||||||
|
|
||||||
-- Insert a blank line after each error
|
|
||||||
print("")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return TextReporterQuiet
|
|
||||||
|
|
@ -1,147 +0,0 @@
|
||||||
--[[
|
|
||||||
Provides an interface to quickly run and report tests from a given object.
|
|
||||||
]]
|
|
||||||
|
|
||||||
local TestPlanner = require(script.Parent.TestPlanner)
|
|
||||||
local TestRunner = require(script.Parent.TestRunner)
|
|
||||||
local TextReporter = require(script.Parent.Reporters.TextReporter)
|
|
||||||
|
|
||||||
local TestBootstrap = {}
|
|
||||||
|
|
||||||
local function stripSpecSuffix(name)
|
|
||||||
return (name:gsub("%.spec$", ""))
|
|
||||||
end
|
|
||||||
local function isSpecScript(aScript)
|
|
||||||
return aScript:IsA("ModuleScript") and aScript.Name:match("%.spec$")
|
|
||||||
end
|
|
||||||
|
|
||||||
local function getPath(module, root)
|
|
||||||
root = root or game
|
|
||||||
|
|
||||||
local path = {}
|
|
||||||
local last = module
|
|
||||||
|
|
||||||
if last.Name == "init.spec" then
|
|
||||||
-- Use the directory's node for init.spec files.
|
|
||||||
last = last.Parent
|
|
||||||
end
|
|
||||||
|
|
||||||
while last ~= nil and last ~= root do
|
|
||||||
table.insert(path, stripSpecSuffix(last.Name))
|
|
||||||
last = last.Parent
|
|
||||||
end
|
|
||||||
table.insert(path, stripSpecSuffix(root.Name))
|
|
||||||
|
|
||||||
return path
|
|
||||||
end
|
|
||||||
|
|
||||||
local function toStringPath(tablePath)
|
|
||||||
local stringPath = ""
|
|
||||||
local first = true
|
|
||||||
for _, element in ipairs(tablePath) do
|
|
||||||
if first then
|
|
||||||
stringPath = element
|
|
||||||
first = false
|
|
||||||
else
|
|
||||||
stringPath = element .. " " .. stringPath
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return stringPath
|
|
||||||
end
|
|
||||||
|
|
||||||
function TestBootstrap:getModulesImpl(root, modules, current)
|
|
||||||
modules = modules or {}
|
|
||||||
current = current or root
|
|
||||||
|
|
||||||
if isSpecScript(current) then
|
|
||||||
local method = require(current)
|
|
||||||
local path = getPath(current, root)
|
|
||||||
local pathString = toStringPath(path)
|
|
||||||
|
|
||||||
table.insert(modules, {
|
|
||||||
method = method,
|
|
||||||
path = path,
|
|
||||||
pathStringForSorting = pathString:lower()
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Find all the ModuleScripts in this tree that are tests.
|
|
||||||
]]
|
|
||||||
function TestBootstrap:getModules(root)
|
|
||||||
local modules = {}
|
|
||||||
|
|
||||||
self:getModulesImpl(root, modules)
|
|
||||||
|
|
||||||
for _, child in ipairs(root:GetDescendants()) do
|
|
||||||
self:getModulesImpl(root, modules, child)
|
|
||||||
end
|
|
||||||
|
|
||||||
return modules
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Runs all test and reports the results using the given test reporter.
|
|
||||||
|
|
||||||
If no reporter is specified, a reasonable default is provided.
|
|
||||||
|
|
||||||
This function demonstrates the expected workflow with this testing system:
|
|
||||||
1. Locate test modules
|
|
||||||
2. Generate test plan
|
|
||||||
3. Run test plan
|
|
||||||
4. Report test results
|
|
||||||
|
|
||||||
This means we could hypothetically present a GUI to the developer that shows
|
|
||||||
the test plan before we execute it, allowing them to toggle specific tests
|
|
||||||
before they're run, but after they've been identified!
|
|
||||||
]]
|
|
||||||
function TestBootstrap:run(roots, reporter, otherOptions)
|
|
||||||
reporter = reporter or TextReporter
|
|
||||||
|
|
||||||
otherOptions = otherOptions or {}
|
|
||||||
local showTimingInfo = otherOptions["showTimingInfo"] or false
|
|
||||||
local testNamePattern = otherOptions["testNamePattern"]
|
|
||||||
local extraEnvironment = otherOptions["extraEnvironment"] or {}
|
|
||||||
|
|
||||||
if type(roots) ~= "table" then
|
|
||||||
error(("Bad argument #1 to TestBootstrap:run. Expected table, got %s"):format(typeof(roots)), 2)
|
|
||||||
end
|
|
||||||
|
|
||||||
local startTime = tick()
|
|
||||||
|
|
||||||
local modules = {}
|
|
||||||
for _, subRoot in ipairs(roots) do
|
|
||||||
local newModules = self:getModules(subRoot)
|
|
||||||
|
|
||||||
for _, newModule in ipairs(newModules) do
|
|
||||||
table.insert(modules, newModule)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local afterModules = tick()
|
|
||||||
|
|
||||||
local plan = TestPlanner.createPlan(modules, testNamePattern, extraEnvironment)
|
|
||||||
local afterPlan = tick()
|
|
||||||
|
|
||||||
local results = TestRunner.runPlan(plan)
|
|
||||||
local afterRun = tick()
|
|
||||||
|
|
||||||
reporter.report(results)
|
|
||||||
local afterReport = tick()
|
|
||||||
|
|
||||||
if showTimingInfo then
|
|
||||||
local timing = {
|
|
||||||
("Took %f seconds to locate test modules"):format(afterModules - startTime),
|
|
||||||
("Took %f seconds to create test plan"):format(afterPlan - afterModules),
|
|
||||||
("Took %f seconds to run tests"):format(afterRun - afterPlan),
|
|
||||||
("Took %f seconds to report tests"):format(afterReport - afterRun),
|
|
||||||
}
|
|
||||||
|
|
||||||
print(table.concat(timing, "\n"))
|
|
||||||
end
|
|
||||||
|
|
||||||
return results
|
|
||||||
end
|
|
||||||
|
|
||||||
return TestBootstrap
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
--[[
|
|
||||||
Constants used throughout the testing framework.
|
|
||||||
]]
|
|
||||||
|
|
||||||
local TestEnum = {}
|
|
||||||
|
|
||||||
TestEnum.TestStatus = {
|
|
||||||
Success = "Success",
|
|
||||||
Failure = "Failure",
|
|
||||||
Skipped = "Skipped"
|
|
||||||
}
|
|
||||||
|
|
||||||
TestEnum.NodeType = {
|
|
||||||
Describe = "Describe",
|
|
||||||
It = "It",
|
|
||||||
BeforeAll = "BeforeAll",
|
|
||||||
AfterAll = "AfterAll",
|
|
||||||
BeforeEach = "BeforeEach",
|
|
||||||
AfterEach = "AfterEach"
|
|
||||||
}
|
|
||||||
|
|
||||||
TestEnum.NodeModifier = {
|
|
||||||
None = "None",
|
|
||||||
Skip = "Skip",
|
|
||||||
Focus = "Focus"
|
|
||||||
}
|
|
||||||
|
|
||||||
return TestEnum
|
|
||||||
|
|
@ -1,304 +0,0 @@
|
||||||
--[[
|
|
||||||
Represents a tree of tests that have been loaded but not necessarily
|
|
||||||
executed yet.
|
|
||||||
|
|
||||||
TestPlan objects are produced by TestPlanner.
|
|
||||||
]]
|
|
||||||
|
|
||||||
local TestEnum = require(script.Parent.TestEnum)
|
|
||||||
local Expectation = require(script.Parent.Expectation)
|
|
||||||
|
|
||||||
local function newEnvironment(currentNode, extraEnvironment)
|
|
||||||
local env = {}
|
|
||||||
|
|
||||||
if extraEnvironment then
|
|
||||||
if type(extraEnvironment) ~= "table" then
|
|
||||||
error(("Bad argument #2 to newEnvironment. Expected table, got %s"):format(
|
|
||||||
typeof(extraEnvironment)), 2)
|
|
||||||
end
|
|
||||||
|
|
||||||
for key, value in pairs(extraEnvironment) do
|
|
||||||
env[key] = value
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function addChild(phrase, callback, nodeType, nodeModifier)
|
|
||||||
local node = currentNode:addChild(phrase, nodeType, nodeModifier)
|
|
||||||
node.callback = callback
|
|
||||||
if nodeType == TestEnum.NodeType.Describe then
|
|
||||||
node:expand()
|
|
||||||
end
|
|
||||||
return node
|
|
||||||
end
|
|
||||||
|
|
||||||
function env.describeFOCUS(phrase, callback)
|
|
||||||
addChild(phrase, callback, TestEnum.NodeType.Describe, TestEnum.NodeModifier.Focus)
|
|
||||||
end
|
|
||||||
|
|
||||||
function env.describeSKIP(phrase, callback)
|
|
||||||
addChild(phrase, callback, TestEnum.NodeType.Describe, TestEnum.NodeModifier.Skip)
|
|
||||||
end
|
|
||||||
|
|
||||||
function env.describe(phrase, callback, nodeModifier)
|
|
||||||
addChild(phrase, callback, TestEnum.NodeType.Describe, TestEnum.NodeModifier.None)
|
|
||||||
end
|
|
||||||
|
|
||||||
function env.itFOCUS(phrase, callback)
|
|
||||||
addChild(phrase, callback, TestEnum.NodeType.It, TestEnum.NodeModifier.Focus)
|
|
||||||
end
|
|
||||||
|
|
||||||
function env.itSKIP(phrase, callback)
|
|
||||||
addChild(phrase, callback, TestEnum.NodeType.It, TestEnum.NodeModifier.Skip)
|
|
||||||
end
|
|
||||||
|
|
||||||
function env.itFIXME(phrase, callback)
|
|
||||||
local node = addChild(phrase, callback, TestEnum.NodeType.It, TestEnum.NodeModifier.Skip)
|
|
||||||
warn("FIXME: broken test", node:getFullName())
|
|
||||||
end
|
|
||||||
|
|
||||||
function env.it(phrase, callback, nodeModifier)
|
|
||||||
addChild(phrase, callback, TestEnum.NodeType.It, TestEnum.NodeModifier.None)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Incrementing counter used to ensure that beforeAll, afterAll, beforeEach, afterEach have unique phrases
|
|
||||||
local lifecyclePhaseId = 0
|
|
||||||
|
|
||||||
local lifecycleHooks = {
|
|
||||||
[TestEnum.NodeType.BeforeAll] = "beforeAll",
|
|
||||||
[TestEnum.NodeType.AfterAll] = "afterAll",
|
|
||||||
[TestEnum.NodeType.BeforeEach] = "beforeEach",
|
|
||||||
[TestEnum.NodeType.AfterEach] = "afterEach"
|
|
||||||
}
|
|
||||||
|
|
||||||
for nodeType, name in pairs(lifecycleHooks) do
|
|
||||||
env[name] = function(callback)
|
|
||||||
addChild(name .. "_" .. tostring(lifecyclePhaseId), callback, nodeType, TestEnum.NodeModifier.None)
|
|
||||||
lifecyclePhaseId = lifecyclePhaseId + 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function env.FIXME(optionalMessage)
|
|
||||||
warn("FIXME: broken test", currentNode:getFullName(), optionalMessage or "")
|
|
||||||
|
|
||||||
currentNode.modifier = TestEnum.NodeModifier.Skip
|
|
||||||
end
|
|
||||||
|
|
||||||
function env.FOCUS()
|
|
||||||
currentNode.modifier = TestEnum.NodeModifier.Focus
|
|
||||||
end
|
|
||||||
|
|
||||||
function env.SKIP()
|
|
||||||
currentNode.modifier = TestEnum.NodeModifier.Skip
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
This function is deprecated. Calling it is a no-op beyond generating a
|
|
||||||
warning.
|
|
||||||
]]
|
|
||||||
function env.HACK_NO_XPCALL()
|
|
||||||
warn("HACK_NO_XPCALL is deprecated. It is now safe to yield in an " ..
|
|
||||||
"xpcall, so this is no longer necessary. It can be safely deleted.")
|
|
||||||
end
|
|
||||||
|
|
||||||
env.fit = env.itFOCUS
|
|
||||||
env.xit = env.itSKIP
|
|
||||||
env.fdescribe = env.describeFOCUS
|
|
||||||
env.xdescribe = env.describeSKIP
|
|
||||||
|
|
||||||
env.expect = setmetatable({
|
|
||||||
extend = function(...)
|
|
||||||
error("Cannot call \"expect.extend\" from within a \"describe\" node.")
|
|
||||||
end,
|
|
||||||
}, {
|
|
||||||
__call = function(_self, ...)
|
|
||||||
return Expectation.new(...)
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
|
|
||||||
return env
|
|
||||||
end
|
|
||||||
|
|
||||||
local TestNode = {}
|
|
||||||
TestNode.__index = TestNode
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Create a new test node. A pointer to the test plan, a phrase to describe it
|
|
||||||
and the type of node it is are required. The modifier is optional and will
|
|
||||||
be None if left blank.
|
|
||||||
]]
|
|
||||||
function TestNode.new(plan, phrase, nodeType, nodeModifier)
|
|
||||||
nodeModifier = nodeModifier or TestEnum.NodeModifier.None
|
|
||||||
|
|
||||||
local node = {
|
|
||||||
plan = plan,
|
|
||||||
phrase = phrase,
|
|
||||||
type = nodeType,
|
|
||||||
modifier = nodeModifier,
|
|
||||||
children = {},
|
|
||||||
callback = nil,
|
|
||||||
parent = nil,
|
|
||||||
}
|
|
||||||
|
|
||||||
node.environment = newEnvironment(node, plan.extraEnvironment)
|
|
||||||
return setmetatable(node, TestNode)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function getModifier(name, pattern, modifier)
|
|
||||||
if pattern and (modifier == nil or modifier == TestEnum.NodeModifier.None) then
|
|
||||||
if name:match(pattern) then
|
|
||||||
return TestEnum.NodeModifier.Focus
|
|
||||||
else
|
|
||||||
return TestEnum.NodeModifier.Skip
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return modifier
|
|
||||||
end
|
|
||||||
|
|
||||||
function TestNode:addChild(phrase, nodeType, nodeModifier)
|
|
||||||
if nodeType == TestEnum.NodeType.It then
|
|
||||||
for _, child in pairs(self.children) do
|
|
||||||
if child.phrase == phrase then
|
|
||||||
error("Duplicate it block found: " .. child:getFullName())
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local childName = self:getFullName() .. " " .. phrase
|
|
||||||
nodeModifier = getModifier(childName, self.plan.testNamePattern, nodeModifier)
|
|
||||||
local child = TestNode.new(self.plan, phrase, nodeType, nodeModifier)
|
|
||||||
child.parent = self
|
|
||||||
table.insert(self.children, child)
|
|
||||||
return child
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Join the names of all the nodes back to the parent.
|
|
||||||
]]
|
|
||||||
function TestNode:getFullName()
|
|
||||||
if self.parent then
|
|
||||||
local parentPhrase = self.parent:getFullName()
|
|
||||||
if parentPhrase then
|
|
||||||
return parentPhrase .. " " .. self.phrase
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return self.phrase
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Expand a node by setting its callback environment and then calling it. Any
|
|
||||||
further it and describe calls within the callback will be added to the tree.
|
|
||||||
]]
|
|
||||||
function TestNode:expand()
|
|
||||||
local originalEnv = getfenv(self.callback)
|
|
||||||
local callbackEnv = setmetatable({}, { __index = originalEnv })
|
|
||||||
for key, value in pairs(self.environment) do
|
|
||||||
callbackEnv[key] = value
|
|
||||||
end
|
|
||||||
-- Copy 'script' directly to new env to make Studio debugger happy.
|
|
||||||
-- Studio debugger does not look into __index, because of security reasons
|
|
||||||
callbackEnv.script = originalEnv.script
|
|
||||||
setfenv(self.callback, callbackEnv)
|
|
||||||
|
|
||||||
local success, result = xpcall(self.callback, function(message)
|
|
||||||
return debug.traceback(tostring(message), 2)
|
|
||||||
end)
|
|
||||||
|
|
||||||
if not success then
|
|
||||||
self.loadError = result
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local TestPlan = {}
|
|
||||||
TestPlan.__index = TestPlan
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Create a new, empty TestPlan.
|
|
||||||
]]
|
|
||||||
function TestPlan.new(testNamePattern, extraEnvironment)
|
|
||||||
local plan = {
|
|
||||||
children = {},
|
|
||||||
testNamePattern = testNamePattern,
|
|
||||||
extraEnvironment = extraEnvironment,
|
|
||||||
}
|
|
||||||
|
|
||||||
return setmetatable(plan, TestPlan)
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Add a new child under the test plan's root node.
|
|
||||||
]]
|
|
||||||
function TestPlan:addChild(phrase, nodeType, nodeModifier)
|
|
||||||
nodeModifier = getModifier(phrase, self.testNamePattern, nodeModifier)
|
|
||||||
local child = TestNode.new(self, phrase, nodeType, nodeModifier)
|
|
||||||
table.insert(self.children, child)
|
|
||||||
return child
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Add a new describe node with the given method as a callback. Generates or
|
|
||||||
reuses all the describe nodes along the path.
|
|
||||||
]]
|
|
||||||
function TestPlan:addRoot(path, method)
|
|
||||||
local curNode = self
|
|
||||||
for i = #path, 1, -1 do
|
|
||||||
local nextNode = nil
|
|
||||||
|
|
||||||
for _, child in ipairs(curNode.children) do
|
|
||||||
if child.phrase == path[i] then
|
|
||||||
nextNode = child
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if nextNode == nil then
|
|
||||||
nextNode = curNode:addChild(path[i], TestEnum.NodeType.Describe)
|
|
||||||
end
|
|
||||||
|
|
||||||
curNode = nextNode
|
|
||||||
end
|
|
||||||
|
|
||||||
curNode.callback = method
|
|
||||||
curNode:expand()
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Calls the given callback on all nodes in the tree, traversed depth-first.
|
|
||||||
]]
|
|
||||||
function TestPlan:visitAllNodes(callback, root, level)
|
|
||||||
root = root or self
|
|
||||||
level = level or 0
|
|
||||||
|
|
||||||
for _, child in ipairs(root.children) do
|
|
||||||
callback(child, level)
|
|
||||||
|
|
||||||
self:visitAllNodes(callback, child, level + 1)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Visualizes the test plan in a simple format, suitable for debugging the test
|
|
||||||
plan's structure.
|
|
||||||
]]
|
|
||||||
function TestPlan:visualize()
|
|
||||||
local buffer = {}
|
|
||||||
self:visitAllNodes(function(node, level)
|
|
||||||
table.insert(buffer, (" "):rep(3 * level) .. node.phrase)
|
|
||||||
end)
|
|
||||||
return table.concat(buffer, "\n")
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Gets a list of all nodes in the tree for which the given callback returns
|
|
||||||
true.
|
|
||||||
]]
|
|
||||||
function TestPlan:findNodes(callback)
|
|
||||||
local results = {}
|
|
||||||
self:visitAllNodes(function(node)
|
|
||||||
if callback(node) then
|
|
||||||
table.insert(results, node)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
return results
|
|
||||||
end
|
|
||||||
|
|
||||||
return TestPlan
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
--[[
|
|
||||||
Turns a series of specification functions into a test plan.
|
|
||||||
|
|
||||||
Uses a TestPlanBuilder to keep track of the state of the tree being built.
|
|
||||||
]]
|
|
||||||
local TestPlan = require(script.Parent.TestPlan)
|
|
||||||
|
|
||||||
local TestPlanner = {}
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Create a new TestPlan from a list of specification functions.
|
|
||||||
|
|
||||||
These functions should call a combination of `describe` and `it` (and their
|
|
||||||
variants), which will be turned into a test plan to be executed.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
- modulesList - list of tables describing test modules {
|
|
||||||
method, -- specification function described above
|
|
||||||
path, -- array of parent entires, first element is the leaf that owns `method`
|
|
||||||
pathStringForSorting -- a string representation of `path`, used for sorting of the test plan
|
|
||||||
}
|
|
||||||
- testNamePattern - Only tests matching this Lua pattern string will run. Pass empty or nil to run all tests
|
|
||||||
- extraEnvironment - Lua table holding additional functions and variables to be injected into the specification
|
|
||||||
function during execution
|
|
||||||
]]
|
|
||||||
function TestPlanner.createPlan(modulesList, testNamePattern, extraEnvironment)
|
|
||||||
local plan = TestPlan.new(testNamePattern, extraEnvironment)
|
|
||||||
|
|
||||||
table.sort(modulesList, function(a, b)
|
|
||||||
return a.pathStringForSorting < b.pathStringForSorting
|
|
||||||
end)
|
|
||||||
|
|
||||||
for _, module in ipairs(modulesList) do
|
|
||||||
plan:addRoot(module.path, module.method)
|
|
||||||
end
|
|
||||||
|
|
||||||
return plan
|
|
||||||
end
|
|
||||||
|
|
||||||
return TestPlanner
|
|
||||||
|
|
@ -1,112 +0,0 @@
|
||||||
--[[
|
|
||||||
Represents a tree of test results.
|
|
||||||
|
|
||||||
Each node in the tree corresponds directly to a node in a corresponding
|
|
||||||
TestPlan, accessible via the 'planNode' field.
|
|
||||||
|
|
||||||
TestResults objects are produced by TestRunner using TestSession as state.
|
|
||||||
]]
|
|
||||||
|
|
||||||
local TestEnum = require(script.Parent.TestEnum)
|
|
||||||
|
|
||||||
local STATUS_SYMBOLS = {
|
|
||||||
[TestEnum.TestStatus.Success] = "+",
|
|
||||||
[TestEnum.TestStatus.Failure] = "-",
|
|
||||||
[TestEnum.TestStatus.Skipped] = "~"
|
|
||||||
}
|
|
||||||
|
|
||||||
local TestResults = {}
|
|
||||||
|
|
||||||
TestResults.__index = TestResults
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Create a new TestResults tree that's linked to the given TestPlan.
|
|
||||||
]]
|
|
||||||
function TestResults.new(plan)
|
|
||||||
local self = {
|
|
||||||
successCount = 0,
|
|
||||||
failureCount = 0,
|
|
||||||
skippedCount = 0,
|
|
||||||
planNode = plan,
|
|
||||||
children = {},
|
|
||||||
errors = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
setmetatable(self, TestResults)
|
|
||||||
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Create a new result node that can be inserted into a TestResult tree.
|
|
||||||
]]
|
|
||||||
function TestResults.createNode(planNode)
|
|
||||||
local node = {
|
|
||||||
planNode = planNode,
|
|
||||||
children = {},
|
|
||||||
errors = {},
|
|
||||||
status = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return node
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Visit all test result nodes, depth-first.
|
|
||||||
]]
|
|
||||||
function TestResults:visitAllNodes(callback, root)
|
|
||||||
root = root or self
|
|
||||||
|
|
||||||
for _, child in ipairs(root.children) do
|
|
||||||
callback(child)
|
|
||||||
|
|
||||||
self:visitAllNodes(callback, child)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Creates a debug visualization of the test results.
|
|
||||||
]]
|
|
||||||
function TestResults:visualize(root, level)
|
|
||||||
root = root or self
|
|
||||||
level = level or 0
|
|
||||||
|
|
||||||
local buffer = {}
|
|
||||||
|
|
||||||
for _, child in ipairs(root.children) do
|
|
||||||
if child.planNode.type == TestEnum.NodeType.It then
|
|
||||||
local symbol = STATUS_SYMBOLS[child.status] or "?"
|
|
||||||
local str = ("%s[%s] %s"):format(
|
|
||||||
(" "):rep(3 * level),
|
|
||||||
symbol,
|
|
||||||
child.planNode.phrase
|
|
||||||
)
|
|
||||||
|
|
||||||
if child.messages and #child.messages > 0 then
|
|
||||||
str = str .. "\n " .. (" "):rep(3 * level) .. table.concat(child.messages, "\n " .. (" "):rep(3 * level))
|
|
||||||
end
|
|
||||||
|
|
||||||
table.insert(buffer, str)
|
|
||||||
else
|
|
||||||
local str = ("%s%s"):format(
|
|
||||||
(" "):rep(3 * level),
|
|
||||||
child.planNode.phrase or ""
|
|
||||||
)
|
|
||||||
|
|
||||||
if child.status then
|
|
||||||
str = str .. (" (%s)"):format(child.status)
|
|
||||||
end
|
|
||||||
|
|
||||||
table.insert(buffer, str)
|
|
||||||
|
|
||||||
if #child.children > 0 then
|
|
||||||
local text = self:visualize(child, level + 1)
|
|
||||||
table.insert(buffer, text)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return table.concat(buffer, "\n")
|
|
||||||
end
|
|
||||||
|
|
||||||
return TestResults
|
|
||||||
|
|
@ -1,188 +0,0 @@
|
||||||
--[[
|
|
||||||
Contains the logic to run a test plan and gather test results from it.
|
|
||||||
|
|
||||||
TestRunner accepts a TestPlan object, executes the planned tests, and
|
|
||||||
produces a TestResults object. While the tests are running, the system's
|
|
||||||
state is contained inside a TestSession object.
|
|
||||||
]]
|
|
||||||
|
|
||||||
local TestEnum = require(script.Parent.TestEnum)
|
|
||||||
local TestSession = require(script.Parent.TestSession)
|
|
||||||
local LifecycleHooks = require(script.Parent.LifecycleHooks)
|
|
||||||
|
|
||||||
local RUNNING_GLOBAL = "__TESTEZ_RUNNING_TEST__"
|
|
||||||
|
|
||||||
local TestRunner = {
|
|
||||||
environment = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
local function wrapExpectContextWithPublicApi(expectationContext)
|
|
||||||
return setmetatable({
|
|
||||||
extend = function(...)
|
|
||||||
expectationContext:extend(...)
|
|
||||||
end,
|
|
||||||
}, {
|
|
||||||
__call = function(_self, ...)
|
|
||||||
return expectationContext:startExpectationChain(...)
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Runs the given TestPlan and returns a TestResults object representing the
|
|
||||||
results of the run.
|
|
||||||
]]
|
|
||||||
function TestRunner.runPlan(plan)
|
|
||||||
local session = TestSession.new(plan)
|
|
||||||
local lifecycleHooks = LifecycleHooks.new()
|
|
||||||
|
|
||||||
local exclusiveNodes = plan:findNodes(function(node)
|
|
||||||
return node.modifier == TestEnum.NodeModifier.Focus
|
|
||||||
end)
|
|
||||||
|
|
||||||
session.hasFocusNodes = #exclusiveNodes > 0
|
|
||||||
|
|
||||||
TestRunner.runPlanNode(session, plan, lifecycleHooks)
|
|
||||||
|
|
||||||
return session:finalize()
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Run the given test plan node and its descendants, using the given test
|
|
||||||
session to store all of the results.
|
|
||||||
]]
|
|
||||||
function TestRunner.runPlanNode(session, planNode, lifecycleHooks)
|
|
||||||
local function runCallback(callback, messagePrefix)
|
|
||||||
local success = true
|
|
||||||
local errorMessage
|
|
||||||
-- Any code can check RUNNING_GLOBAL to fork behavior based on
|
|
||||||
-- whether a test is running. We use this to avoid accessing
|
|
||||||
-- protected APIs; it's a workaround that will go away someday.
|
|
||||||
_G[RUNNING_GLOBAL] = true
|
|
||||||
|
|
||||||
messagePrefix = messagePrefix or ""
|
|
||||||
|
|
||||||
local testEnvironment = getfenv(callback)
|
|
||||||
|
|
||||||
for key, value in pairs(TestRunner.environment) do
|
|
||||||
testEnvironment[key] = value
|
|
||||||
end
|
|
||||||
|
|
||||||
testEnvironment.fail = function(message)
|
|
||||||
if message == nil then
|
|
||||||
message = "fail() was called."
|
|
||||||
end
|
|
||||||
|
|
||||||
success = false
|
|
||||||
errorMessage = messagePrefix .. debug.traceback(tostring(message), 2)
|
|
||||||
end
|
|
||||||
|
|
||||||
testEnvironment.expect = wrapExpectContextWithPublicApi(session:getExpectationContext())
|
|
||||||
|
|
||||||
local context = session:getContext()
|
|
||||||
|
|
||||||
local nodeSuccess, nodeResult = xpcall(
|
|
||||||
function()
|
|
||||||
callback(context)
|
|
||||||
end,
|
|
||||||
function(message)
|
|
||||||
return messagePrefix .. debug.traceback(tostring(message), 2)
|
|
||||||
end
|
|
||||||
)
|
|
||||||
|
|
||||||
-- If a node threw an error, we prefer to use that message over
|
|
||||||
-- one created by fail() if it was set.
|
|
||||||
if not nodeSuccess then
|
|
||||||
success = false
|
|
||||||
errorMessage = nodeResult
|
|
||||||
end
|
|
||||||
|
|
||||||
_G[RUNNING_GLOBAL] = nil
|
|
||||||
|
|
||||||
return success, errorMessage
|
|
||||||
end
|
|
||||||
|
|
||||||
local function runNode(childPlanNode)
|
|
||||||
-- Errors can be set either via `error` propagating upwards or
|
|
||||||
-- by a test calling fail([message]).
|
|
||||||
|
|
||||||
for _, hook in ipairs(lifecycleHooks:getBeforeEachHooks()) do
|
|
||||||
local success, errorMessage = runCallback(hook, "beforeEach hook: ")
|
|
||||||
if not success then
|
|
||||||
return false, errorMessage
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local testSuccess, testErrorMessage = runCallback(childPlanNode.callback)
|
|
||||||
|
|
||||||
for _, hook in ipairs(lifecycleHooks:getAfterEachHooks()) do
|
|
||||||
local success, errorMessage = runCallback(hook, "afterEach hook: ")
|
|
||||||
if not success then
|
|
||||||
if not testSuccess then
|
|
||||||
return false, testErrorMessage .. "\nWhile cleaning up the failed test another error was found:\n" .. errorMessage
|
|
||||||
end
|
|
||||||
return false, errorMessage
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if not testSuccess then
|
|
||||||
return false, testErrorMessage
|
|
||||||
end
|
|
||||||
|
|
||||||
return true, nil
|
|
||||||
end
|
|
||||||
|
|
||||||
lifecycleHooks:pushHooksFrom(planNode)
|
|
||||||
|
|
||||||
local halt = false
|
|
||||||
for _, hook in ipairs(lifecycleHooks:getBeforeAllHooks()) do
|
|
||||||
local success, errorMessage = runCallback(hook, "beforeAll hook: ")
|
|
||||||
if not success then
|
|
||||||
session:addDummyError("beforeAll", errorMessage)
|
|
||||||
halt = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if not halt then
|
|
||||||
for _, childPlanNode in ipairs(planNode.children) do
|
|
||||||
if childPlanNode.type == TestEnum.NodeType.It then
|
|
||||||
session:pushNode(childPlanNode)
|
|
||||||
if session:shouldSkip() then
|
|
||||||
session:setSkipped()
|
|
||||||
else
|
|
||||||
local success, errorMessage = runNode(childPlanNode)
|
|
||||||
|
|
||||||
if success then
|
|
||||||
session:setSuccess()
|
|
||||||
else
|
|
||||||
session:setError(errorMessage)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
session:popNode()
|
|
||||||
elseif childPlanNode.type == TestEnum.NodeType.Describe then
|
|
||||||
session:pushNode(childPlanNode)
|
|
||||||
TestRunner.runPlanNode(session, childPlanNode, lifecycleHooks)
|
|
||||||
|
|
||||||
-- Did we have an error trying build a test plan?
|
|
||||||
if childPlanNode.loadError then
|
|
||||||
local message = "Error during planning: " .. childPlanNode.loadError
|
|
||||||
session:setError(message)
|
|
||||||
else
|
|
||||||
session:setStatusFromChildren()
|
|
||||||
end
|
|
||||||
session:popNode()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
for _, hook in ipairs(lifecycleHooks:getAfterAllHooks()) do
|
|
||||||
local success, errorMessage = runCallback(hook, "afterAll hook: ")
|
|
||||||
if not success then
|
|
||||||
session:addDummyError("afterAll", errorMessage)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
lifecycleHooks:popHooks()
|
|
||||||
end
|
|
||||||
|
|
||||||
return TestRunner
|
|
||||||
|
|
@ -1,243 +0,0 @@
|
||||||
--[[
|
|
||||||
Represents the state relevant while executing a test plan.
|
|
||||||
|
|
||||||
Used by TestRunner to produce a TestResults object.
|
|
||||||
|
|
||||||
Uses the same tree building structure as TestPlanBuilder; TestSession keeps
|
|
||||||
track of a stack of nodes that represent the current path through the tree.
|
|
||||||
]]
|
|
||||||
|
|
||||||
local TestEnum = require(script.Parent.TestEnum)
|
|
||||||
local TestResults = require(script.Parent.TestResults)
|
|
||||||
local Context = require(script.Parent.Context)
|
|
||||||
local ExpectationContext = require(script.Parent.ExpectationContext)
|
|
||||||
|
|
||||||
local TestSession = {}
|
|
||||||
|
|
||||||
TestSession.__index = TestSession
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Create a TestSession related to the given TestPlan.
|
|
||||||
|
|
||||||
The resulting TestResults object will be linked to this TestPlan.
|
|
||||||
]]
|
|
||||||
function TestSession.new(plan)
|
|
||||||
local self = {
|
|
||||||
results = TestResults.new(plan),
|
|
||||||
nodeStack = {},
|
|
||||||
contextStack = {},
|
|
||||||
expectationContextStack = {},
|
|
||||||
hasFocusNodes = false
|
|
||||||
}
|
|
||||||
|
|
||||||
setmetatable(self, TestSession)
|
|
||||||
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Calculate success, failure, and skipped test counts in the tree at the
|
|
||||||
current point in the execution.
|
|
||||||
]]
|
|
||||||
function TestSession:calculateTotals()
|
|
||||||
local results = self.results
|
|
||||||
|
|
||||||
results.successCount = 0
|
|
||||||
results.failureCount = 0
|
|
||||||
results.skippedCount = 0
|
|
||||||
|
|
||||||
results:visitAllNodes(function(node)
|
|
||||||
local status = node.status
|
|
||||||
local nodeType = node.planNode.type
|
|
||||||
|
|
||||||
if nodeType == TestEnum.NodeType.It then
|
|
||||||
if status == TestEnum.TestStatus.Success then
|
|
||||||
results.successCount = results.successCount + 1
|
|
||||||
elseif status == TestEnum.TestStatus.Failure then
|
|
||||||
results.failureCount = results.failureCount + 1
|
|
||||||
elseif status == TestEnum.TestStatus.Skipped then
|
|
||||||
results.skippedCount = results.skippedCount + 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Gathers all of the errors reported by tests and puts them at the top level
|
|
||||||
of the TestResults object.
|
|
||||||
]]
|
|
||||||
function TestSession:gatherErrors()
|
|
||||||
local results = self.results
|
|
||||||
|
|
||||||
results.errors = {}
|
|
||||||
|
|
||||||
results:visitAllNodes(function(node)
|
|
||||||
if #node.errors > 0 then
|
|
||||||
for _, message in ipairs(node.errors) do
|
|
||||||
table.insert(results.errors, message)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Calculates test totals, verifies the tree is valid, and returns results.
|
|
||||||
]]
|
|
||||||
function TestSession:finalize()
|
|
||||||
if #self.nodeStack ~= 0 then
|
|
||||||
error("Cannot finalize TestResults with nodes still on the stack!", 2)
|
|
||||||
end
|
|
||||||
|
|
||||||
self:calculateTotals()
|
|
||||||
self:gatherErrors()
|
|
||||||
|
|
||||||
return self.results
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Create a new test result node and push it onto the navigation stack.
|
|
||||||
]]
|
|
||||||
function TestSession:pushNode(planNode)
|
|
||||||
local node = TestResults.createNode(planNode)
|
|
||||||
local lastNode = self.nodeStack[#self.nodeStack] or self.results
|
|
||||||
table.insert(lastNode.children, node)
|
|
||||||
table.insert(self.nodeStack, node)
|
|
||||||
|
|
||||||
local lastContext = self.contextStack[#self.contextStack]
|
|
||||||
local context = Context.new(lastContext)
|
|
||||||
table.insert(self.contextStack, context)
|
|
||||||
|
|
||||||
local lastExpectationContext = self.expectationContextStack[#self.expectationContextStack]
|
|
||||||
local expectationContext = ExpectationContext.new(lastExpectationContext)
|
|
||||||
table.insert(self.expectationContextStack, expectationContext)
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Pops a node off of the navigation stack.
|
|
||||||
]]
|
|
||||||
function TestSession:popNode()
|
|
||||||
assert(#self.nodeStack > 0, "Tried to pop from an empty node stack!")
|
|
||||||
table.remove(self.nodeStack, #self.nodeStack)
|
|
||||||
table.remove(self.contextStack, #self.contextStack)
|
|
||||||
table.remove(self.expectationContextStack, #self.expectationContextStack)
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Gets the Context object for the current node.
|
|
||||||
]]
|
|
||||||
function TestSession:getContext()
|
|
||||||
assert(#self.contextStack > 0, "Tried to get context from an empty stack!")
|
|
||||||
return self.contextStack[#self.contextStack]
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
function TestSession:getExpectationContext()
|
|
||||||
assert(#self.expectationContextStack > 0, "Tried to get expectationContext from an empty stack!")
|
|
||||||
return self.expectationContextStack[#self.expectationContextStack]
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Tells whether the current test we're in should be skipped.
|
|
||||||
]]
|
|
||||||
function TestSession:shouldSkip()
|
|
||||||
-- If our test tree had any exclusive tests, then normal tests are skipped!
|
|
||||||
if self.hasFocusNodes then
|
|
||||||
for i = #self.nodeStack, 1, -1 do
|
|
||||||
local node = self.nodeStack[i]
|
|
||||||
|
|
||||||
-- Skipped tests are still skipped
|
|
||||||
if node.planNode.modifier == TestEnum.NodeModifier.Skip then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Focused tests are the only ones that aren't skipped
|
|
||||||
if node.planNode.modifier == TestEnum.NodeModifier.Focus then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return true
|
|
||||||
else
|
|
||||||
for i = #self.nodeStack, 1, -1 do
|
|
||||||
local node = self.nodeStack[i]
|
|
||||||
|
|
||||||
if node.planNode.modifier == TestEnum.NodeModifier.Skip then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Set the current node's status to Success.
|
|
||||||
]]
|
|
||||||
function TestSession:setSuccess()
|
|
||||||
assert(#self.nodeStack > 0, "Attempting to set success status on empty stack")
|
|
||||||
self.nodeStack[#self.nodeStack].status = TestEnum.TestStatus.Success
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Set the current node's status to Skipped.
|
|
||||||
]]
|
|
||||||
function TestSession:setSkipped()
|
|
||||||
assert(#self.nodeStack > 0, "Attempting to set skipped status on empty stack")
|
|
||||||
self.nodeStack[#self.nodeStack].status = TestEnum.TestStatus.Skipped
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Set the current node's status to Failure and adds a message to its list of
|
|
||||||
errors.
|
|
||||||
]]
|
|
||||||
function TestSession:setError(message)
|
|
||||||
assert(#self.nodeStack > 0, "Attempting to set error status on empty stack")
|
|
||||||
local last = self.nodeStack[#self.nodeStack]
|
|
||||||
last.status = TestEnum.TestStatus.Failure
|
|
||||||
table.insert(last.errors, message)
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Add a dummy child node to the current node to hold the given error. This
|
|
||||||
allows an otherwise empty describe node to report an error in a more natural
|
|
||||||
way.
|
|
||||||
]]
|
|
||||||
function TestSession:addDummyError(phrase, message)
|
|
||||||
self:pushNode({type = TestEnum.NodeType.It, phrase = phrase})
|
|
||||||
self:setError(message)
|
|
||||||
self:popNode()
|
|
||||||
self.nodeStack[#self.nodeStack].status = TestEnum.TestStatus.Failure
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Set the current node's status based on that of its children. If all children
|
|
||||||
are skipped, mark it as skipped. If any are fails, mark it as failed.
|
|
||||||
Otherwise, mark it as success.
|
|
||||||
]]
|
|
||||||
function TestSession:setStatusFromChildren()
|
|
||||||
assert(#self.nodeStack > 0, "Attempting to set status from children on empty stack")
|
|
||||||
|
|
||||||
local last = self.nodeStack[#self.nodeStack]
|
|
||||||
local status = TestEnum.TestStatus.Success
|
|
||||||
local skipped = true
|
|
||||||
|
|
||||||
-- If all children were skipped, then we were skipped
|
|
||||||
-- If any child failed, then we failed!
|
|
||||||
for _, child in ipairs(last.children) do
|
|
||||||
if child.status ~= TestEnum.TestStatus.Skipped then
|
|
||||||
skipped = false
|
|
||||||
|
|
||||||
if child.status == TestEnum.TestStatus.Failure then
|
|
||||||
status = TestEnum.TestStatus.Failure
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if skipped then
|
|
||||||
status = TestEnum.TestStatus.Skipped
|
|
||||||
end
|
|
||||||
|
|
||||||
last.status = status
|
|
||||||
end
|
|
||||||
|
|
||||||
return TestSession
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
--!native
|
|
||||||
--!optimize 2
|
|
||||||
local Expectation = require(script.Expectation)
|
|
||||||
local TestBootstrap = require(script.TestBootstrap)
|
|
||||||
local TestEnum = require(script.TestEnum)
|
|
||||||
local TestPlan = require(script.TestPlan)
|
|
||||||
local TestPlanner = require(script.TestPlanner)
|
|
||||||
local TestResults = require(script.TestResults)
|
|
||||||
local TestRunner = require(script.TestRunner)
|
|
||||||
local TestSession = require(script.TestSession)
|
|
||||||
local TextReporter = require(script.Reporters.TextReporter)
|
|
||||||
local TextReporterQuiet = require(script.Reporters.TextReporterQuiet)
|
|
||||||
local TeamCityReporter = require(script.Reporters.TeamCityReporter)
|
|
||||||
|
|
||||||
local function run(testRoot, callback)
|
|
||||||
local modules = TestBootstrap:getModules(testRoot)
|
|
||||||
local plan = TestPlanner.createPlan(modules)
|
|
||||||
local results = TestRunner.runPlan(plan)
|
|
||||||
|
|
||||||
callback(results)
|
|
||||||
end
|
|
||||||
|
|
||||||
local TestEZ = {
|
|
||||||
run = run,
|
|
||||||
|
|
||||||
Expectation = Expectation,
|
|
||||||
TestBootstrap = TestBootstrap,
|
|
||||||
TestEnum = TestEnum,
|
|
||||||
TestPlan = TestPlan,
|
|
||||||
TestPlanner = TestPlanner,
|
|
||||||
TestResults = TestResults,
|
|
||||||
TestRunner = TestRunner,
|
|
||||||
TestSession = TestSession,
|
|
||||||
|
|
||||||
Reporters = {
|
|
||||||
TextReporter = TextReporter,
|
|
||||||
TextReporterQuiet = TextReporterQuiet,
|
|
||||||
TeamCityReporter = TeamCityReporter,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return TestEZ
|
|
||||||
Loading…
Reference in a new issue