add gs
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
--[[
|
||||
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
|
||||
}
|
||||
|
||||
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.__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)
|
||||
newExpectation.successCondition = not self.successCondition
|
||||
|
||||
return newExpectation
|
||||
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
|
||||
|
||||
--[[
|
||||
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
|
||||
]]
|
||||
function Expectation:throw()
|
||||
local ok, err = pcall(self.value)
|
||||
local result = ok ~= self.successCondition
|
||||
|
||||
local message = formatMessage(self.successCondition,
|
||||
("Expected function to succeed, but it threw an error: %s"):format(
|
||||
tostring(err)
|
||||
),
|
||||
"Expected function to throw an error, but it did not."
|
||||
)
|
||||
|
||||
assertLevel(result, message, 3)
|
||||
self:_resetModifiers()
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
return Expectation
|
||||
@@ -0,0 +1,102 @@
|
||||
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
|
||||
@@ -0,0 +1,100 @@
|
||||
--[[
|
||||
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 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)
|
||||
|
||||
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 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
|
||||
@@ -0,0 +1,38 @@
|
||||
local Stack = {}
|
||||
Stack.__index = Stack
|
||||
|
||||
function Stack.new()
|
||||
local self = {}
|
||||
setmetatable(self, Stack)
|
||||
self.data = {}
|
||||
return self
|
||||
end
|
||||
|
||||
function Stack:size()
|
||||
return #self.data
|
||||
end
|
||||
|
||||
function Stack:push(obj)
|
||||
self.data[self:size()+1] = obj
|
||||
return self
|
||||
end
|
||||
|
||||
function Stack:pop()
|
||||
local result = self:getBack()
|
||||
table.remove(self.data,self:size())
|
||||
return result
|
||||
end
|
||||
|
||||
function Stack:getBack()
|
||||
if self:size() == 0 then error("stack is empty") end
|
||||
local result = self.data[self:size()]
|
||||
return result
|
||||
end
|
||||
|
||||
function Stack:setBack(obj)
|
||||
if self:size() == 0 then error("stack is empty") end
|
||||
self.data[self:size()] = obj
|
||||
return self
|
||||
end
|
||||
|
||||
return Stack
|
||||
@@ -0,0 +1,120 @@
|
||||
--[[
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
--[[
|
||||
Find all the ModuleScripts in this tree that are tests.
|
||||
]]
|
||||
function TestBootstrap:getModules(root, modules, current)
|
||||
modules = modules or {}
|
||||
current = current or root
|
||||
|
||||
if isSpecScript(current) then
|
||||
local method = require(current)
|
||||
local path = getPath(current, root)
|
||||
|
||||
table.insert(modules, {
|
||||
method = method,
|
||||
path = path
|
||||
})
|
||||
end
|
||||
|
||||
for _, child in ipairs(current:GetChildren()) do
|
||||
self:getModules(root, modules, child)
|
||||
end
|
||||
|
||||
table.sort(modules, function(a, b)
|
||||
return a.path[#a.path]:lower() < b.path[#b.path]:lower()
|
||||
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 noXpcallByDefault = otherOptions["noXpcallByDefault"] or false
|
||||
local testNamePattern = otherOptions["testNamePattern"]
|
||||
|
||||
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
|
||||
modules = self:getModules(subRoot, modules)
|
||||
end
|
||||
|
||||
local afterModules = tick()
|
||||
|
||||
local plan = TestPlanner.createPlan(modules, noXpcallByDefault, testNamePattern)
|
||||
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
|
||||
@@ -0,0 +1,25 @@
|
||||
--[[
|
||||
Constants used throughout the testing framework.
|
||||
]]
|
||||
|
||||
local TestEnum = {}
|
||||
|
||||
TestEnum.TestStatus = {
|
||||
Success = "Success",
|
||||
Failure = "Failure",
|
||||
Skipped = "Skipped"
|
||||
}
|
||||
|
||||
TestEnum.NodeType = {
|
||||
Try = "Try",
|
||||
Describe = "Describe",
|
||||
It = "It"
|
||||
}
|
||||
|
||||
TestEnum.NodeModifier = {
|
||||
None = "None",
|
||||
Skip = "Skip",
|
||||
Focus = "Focus"
|
||||
}
|
||||
|
||||
return TestEnum
|
||||
@@ -0,0 +1,113 @@
|
||||
--[[
|
||||
Represents a tree of tests that have been loaded but not necessarily
|
||||
executed yet.
|
||||
|
||||
TestPlan objects are produced by TestPlanner and TestPlanBuilder.
|
||||
]]
|
||||
|
||||
local TestEnum = require(script.Parent.TestEnum)
|
||||
|
||||
local TestPlan = {}
|
||||
|
||||
TestPlan.__index = TestPlan
|
||||
|
||||
--[[
|
||||
Create a new, empty TestPlan.
|
||||
]]
|
||||
function TestPlan.new()
|
||||
local self = {
|
||||
children = {}
|
||||
}
|
||||
|
||||
setmetatable(self, TestPlan)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--[[
|
||||
Calls the given callback on all nodes in the tree, traversed depth-first.
|
||||
]]
|
||||
function TestPlan:visitAllNodes(callback, root)
|
||||
root = root or self
|
||||
|
||||
for _, child in ipairs(root.children) do
|
||||
callback(child)
|
||||
|
||||
self:visitAllNodes(callback, child)
|
||||
end
|
||||
end
|
||||
|
||||
local function constructNodeFullName(node)
|
||||
if node.parent then
|
||||
local parentPhrase = constructNodeFullName(node.parent)
|
||||
if parentPhrase then
|
||||
return parentPhrase .. " " .. node.phrase
|
||||
end
|
||||
end
|
||||
return node.phrase
|
||||
end
|
||||
|
||||
--[[
|
||||
Creates a new node that would be suitable to insert into the TestPlan.
|
||||
]]
|
||||
function TestPlan.createNode(phrase, nodeType, nodeModifier)
|
||||
nodeModifier = nodeModifier or TestEnum.NodeModifier.None
|
||||
|
||||
local node = {
|
||||
phrase = phrase,
|
||||
type = nodeType,
|
||||
modifier = nodeModifier,
|
||||
children = {},
|
||||
callback = nil,
|
||||
getFullName = constructNodeFullName
|
||||
}
|
||||
|
||||
return node
|
||||
end
|
||||
|
||||
--[[
|
||||
Visualizes the test plan in a simple format, suitable for debugging the test
|
||||
plan's structure.
|
||||
]]
|
||||
function TestPlan:visualize(root, level)
|
||||
root = root or self
|
||||
level = level or 0
|
||||
|
||||
local buffer = {}
|
||||
|
||||
for _, child in ipairs(root.children) do
|
||||
if child.type == TestEnum.NodeType.It then
|
||||
table.insert(buffer, (" "):rep(3 * level) .. child.phrase)
|
||||
else
|
||||
table.insert(buffer, (" "):rep(3 * level) .. child.phrase)
|
||||
end
|
||||
|
||||
if #child.children > 0 then
|
||||
local text = self:visualize(child, level + 1)
|
||||
table.insert(buffer, text)
|
||||
end
|
||||
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, results, node)
|
||||
node = node or self
|
||||
results = results or {}
|
||||
|
||||
for _, childNode in ipairs(node.children) do
|
||||
if callback(childNode) then
|
||||
table.insert(results, childNode)
|
||||
end
|
||||
|
||||
self:findNodes(callback, results, childNode)
|
||||
end
|
||||
|
||||
return results
|
||||
end
|
||||
|
||||
return TestPlan
|
||||
@@ -0,0 +1,98 @@
|
||||
--[[
|
||||
Represents the ephermal state used for building a TestPlan from some other
|
||||
representation.
|
||||
|
||||
TestPlanBuilder keeps track of a stack of nodes that represents the current
|
||||
position in the hierarchy, allowing the consumer to move up and down the
|
||||
tree as new nodes are discovered.
|
||||
]]
|
||||
|
||||
local TestPlan = require(script.Parent.TestPlan)
|
||||
local TestEnum = require(script.Parent.TestEnum)
|
||||
|
||||
local TestPlanBuilder = {}
|
||||
|
||||
TestPlanBuilder.__index = TestPlanBuilder
|
||||
|
||||
--[[
|
||||
Create a new TestPlanBuilder, used for creating a TestPlan.
|
||||
]]
|
||||
function TestPlanBuilder.new()
|
||||
local self = {
|
||||
plan = TestPlan.new(),
|
||||
nodeStack = {},
|
||||
noXpcallByDefault = false,
|
||||
testNamePattern = nil,
|
||||
}
|
||||
|
||||
setmetatable(self, TestPlanBuilder)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--[[
|
||||
Verify that the TestPlanBuilder's state is valid and get a TestPlan from it.
|
||||
]]
|
||||
function TestPlanBuilder:finalize()
|
||||
if #self.nodeStack ~= 0 then
|
||||
error("Cannot finalize a TestPlan with nodes still on the stack!", 2)
|
||||
end
|
||||
|
||||
return self.plan
|
||||
end
|
||||
|
||||
--[[
|
||||
Grab the current node being worked on by the TestPlanBuilder.
|
||||
]]
|
||||
function TestPlanBuilder:getCurrentNode()
|
||||
return self.nodeStack[#self.nodeStack] or self.plan
|
||||
end
|
||||
|
||||
--[[
|
||||
Creates and pushes a node onto the navigation stack.
|
||||
]]
|
||||
function TestPlanBuilder:pushNode(phrase, nodeType, nodeModifier)
|
||||
local lastNode = self.nodeStack[#self.nodeStack] or self.plan
|
||||
|
||||
-- Find an existing node with this phrase to use
|
||||
local useNode
|
||||
for _, child in ipairs(lastNode.children) do
|
||||
if child.phrase == phrase then
|
||||
useNode = child
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
-- Didn't find one, create a new node
|
||||
if not useNode then
|
||||
useNode = TestPlan.createNode(phrase, nodeType, nodeModifier)
|
||||
useNode.parent = lastNode
|
||||
|
||||
table.insert(lastNode.children, useNode)
|
||||
end
|
||||
|
||||
table.insert(self.nodeStack, useNode)
|
||||
|
||||
local nodeModifierNotSet = useNode.modifier == nil or useNode.modifier == TestEnum.NodeModifier.None
|
||||
if self.testNamePattern and nodeModifierNotSet then
|
||||
local fullName = useNode:getFullName()
|
||||
if fullName:match(self.testNamePattern) then
|
||||
useNode.modifier = TestEnum.NodeModifier.Focus
|
||||
else
|
||||
useNode.modifier = TestEnum.NodeModifier.Skip
|
||||
end
|
||||
end
|
||||
useNode.HACK_NO_XPCALL = self.noXpcallByDefault
|
||||
|
||||
return useNode
|
||||
end
|
||||
|
||||
--[[
|
||||
Pops a node off of the node navigation stack.
|
||||
]]
|
||||
function TestPlanBuilder:popNode()
|
||||
assert(#self.nodeStack > 0, "Tried to pop from an empty node stack!")
|
||||
return table.remove(self.nodeStack, #self.nodeStack)
|
||||
end
|
||||
|
||||
return TestPlanBuilder
|
||||
@@ -0,0 +1,198 @@
|
||||
--[[
|
||||
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 TestEnum = require(script.Parent.TestEnum)
|
||||
local TestPlanBuilder = require(script.Parent.TestPlanBuilder)
|
||||
|
||||
local TestPlanner = {}
|
||||
|
||||
local function buildPlan(builder, module, env)
|
||||
local currentEnv = getfenv(module.method)
|
||||
|
||||
for key, value in pairs(env) do
|
||||
currentEnv[key] = value
|
||||
end
|
||||
|
||||
local nodeCount = #module.path
|
||||
|
||||
-- Dive into auto-named nodes for this module
|
||||
for i = nodeCount, 1, -1 do
|
||||
local name = module.path[i]
|
||||
builder:pushNode(name, TestEnum.NodeType.Describe)
|
||||
end
|
||||
|
||||
local ok, err = xpcall(module.method, function(err)
|
||||
return err .. "\n" .. debug.traceback()
|
||||
end)
|
||||
|
||||
-- This is an error outside of any describe/it blocks.
|
||||
-- We attach it to the node we generate automatically per-file.
|
||||
if not ok then
|
||||
local node = builder:getCurrentNode()
|
||||
node.loadError = err
|
||||
end
|
||||
|
||||
-- Back out of auto-named nodes
|
||||
for _ = 1, nodeCount do
|
||||
builder:popNode()
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
Create a new environment with functions for defining the test plan structure
|
||||
using the given TestPlanBuilder.
|
||||
|
||||
These functions illustrate the advantage of the stack-style tree navigation
|
||||
as state doesn't need to be passed around between functions or explicitly
|
||||
global.
|
||||
]]
|
||||
function TestPlanner.createEnvironment(builder)
|
||||
local env = {}
|
||||
|
||||
function env.describeFOCUS(phrase, callback)
|
||||
return env.describe(phrase, callback, TestEnum.NodeModifier.Focus)
|
||||
end
|
||||
|
||||
function env.describeSKIP(phrase, callback)
|
||||
return env.describe(phrase, callback, TestEnum.NodeModifier.Skip)
|
||||
end
|
||||
|
||||
function env.describe(phrase, callback, nodeModifier)
|
||||
local node = builder:pushNode(phrase, TestEnum.NodeType.Describe, nodeModifier)
|
||||
|
||||
local ok, err = pcall(callback)
|
||||
|
||||
-- loadError on a TestPlan node is an automatic failure
|
||||
if not ok then
|
||||
node.loadError = err
|
||||
end
|
||||
|
||||
builder:popNode()
|
||||
end
|
||||
|
||||
function env.try(phrase, callback)
|
||||
local node = builder:pushNode(phrase, TestEnum.NodeType.Try)
|
||||
|
||||
local ok, err = pcall(callback)
|
||||
|
||||
-- loadError on a TestPlan node is an automatic failure
|
||||
if not ok then
|
||||
node.loadError = err
|
||||
end
|
||||
|
||||
builder:popNode()
|
||||
end
|
||||
|
||||
function env.it(phrase, callback)
|
||||
local node = builder:pushNode(phrase, TestEnum.NodeType.It)
|
||||
|
||||
node.callback = callback
|
||||
|
||||
builder:popNode()
|
||||
end
|
||||
|
||||
function env.itFOCUS(phrase, callback)
|
||||
local node = builder:pushNode(phrase, TestEnum.NodeType.It, TestEnum.NodeModifier.Focus)
|
||||
|
||||
node.callback = callback
|
||||
|
||||
builder:popNode()
|
||||
end
|
||||
|
||||
function env.itSKIP(phrase, callback)
|
||||
local node = builder:pushNode(phrase, TestEnum.NodeType.It, TestEnum.NodeModifier.Skip)
|
||||
|
||||
node.callback = callback
|
||||
|
||||
builder:popNode()
|
||||
end
|
||||
|
||||
function env.itFIXME(phrase, callback)
|
||||
local node = builder:pushNode(phrase, TestEnum.NodeType.It, TestEnum.NodeModifier.Skip)
|
||||
|
||||
warn("FIXME: broken test", node:getFullName())
|
||||
node.callback = callback
|
||||
|
||||
builder:popNode()
|
||||
end
|
||||
|
||||
function env.FIXME(optionalMessage)
|
||||
local currentNode = builder:getCurrentNode()
|
||||
warn("FIXME: broken test", currentNode:getFullName(), optionalMessage or "")
|
||||
|
||||
currentNode.modifier = TestEnum.NodeModifier.Skip
|
||||
end
|
||||
|
||||
function env.FOCUS()
|
||||
local currentNode = builder:getCurrentNode()
|
||||
|
||||
currentNode.modifier = TestEnum.NodeModifier.Focus
|
||||
end
|
||||
|
||||
function env.SKIP()
|
||||
local currentNode = builder:getCurrentNode()
|
||||
|
||||
currentNode.modifier = TestEnum.NodeModifier.Skip
|
||||
end
|
||||
|
||||
--[[
|
||||
These method is intended to disable the use of xpcall when running
|
||||
nodes contained in the same node that this function is called in.
|
||||
This is because xpcall breaks badly if the method passed yields.
|
||||
|
||||
This function is intended to be hideous and seldom called.
|
||||
|
||||
Once xpcall is able to yield, this function is obsolete.
|
||||
]]
|
||||
function env.HACK_NO_XPCALL()
|
||||
local currentNode = builder:getCurrentNode()
|
||||
|
||||
currentNode.HACK_NO_XPCALL = true
|
||||
end
|
||||
|
||||
env.step = env.it
|
||||
|
||||
env.fit = env.itFOCUS
|
||||
env.xit = env.itSKIP
|
||||
env.fdescribe = env.describeFOCUS
|
||||
env.xdescribe = env.describeSKIP
|
||||
|
||||
function env.include(...)
|
||||
local args = {...}
|
||||
local method, path
|
||||
if #args == 1 then
|
||||
method = args[1]
|
||||
path = {}
|
||||
elseif #args == 2 then
|
||||
method = args[2]
|
||||
path = {args[1]}
|
||||
end
|
||||
buildPlan(builder, {path = path, method = method}, env)
|
||||
end
|
||||
|
||||
return env
|
||||
end
|
||||
|
||||
--[[
|
||||
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.
|
||||
]]
|
||||
function TestPlanner.createPlan(specFunctions, noXpcallByDefault, testNamePattern)
|
||||
local builder = TestPlanBuilder.new()
|
||||
builder.noXpcallByDefault = noXpcallByDefault
|
||||
builder.testNamePattern = testNamePattern
|
||||
local env = TestPlanner.createEnvironment(builder)
|
||||
|
||||
for _, module in ipairs(specFunctions) do
|
||||
buildPlan(builder, module, env)
|
||||
end
|
||||
|
||||
return builder:finalize()
|
||||
end
|
||||
|
||||
return TestPlanner
|
||||
@@ -0,0 +1,112 @@
|
||||
--[[
|
||||
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 > 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
|
||||
)
|
||||
|
||||
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
|
||||
@@ -0,0 +1,152 @@
|
||||
--[[
|
||||
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 Expectation = require(script.Parent.Expectation)
|
||||
local TestEnum = require(script.Parent.TestEnum)
|
||||
local TestSession = require(script.Parent.TestSession)
|
||||
local Stack = require(script.Parent.Stack)
|
||||
|
||||
local RUNNING_GLOBAL = "__TESTEZ_RUNNING_TEST__"
|
||||
|
||||
local TestRunner = {
|
||||
environment = {}
|
||||
}
|
||||
|
||||
function TestRunner.environment.expect(...)
|
||||
return Expectation.new(...)
|
||||
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 tryStack = Stack.new()
|
||||
|
||||
local exclusiveNodes = plan:findNodes(function(node)
|
||||
return node.modifier == TestEnum.NodeModifier.Focus
|
||||
end)
|
||||
|
||||
session.hasFocusNodes = #exclusiveNodes > 0
|
||||
|
||||
TestRunner.runPlanNode(session, plan, tryStack)
|
||||
|
||||
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, tryStack, noXpcall)
|
||||
for _, childPlanNode in ipairs(planNode.children) do
|
||||
local childResultNode = session:pushNode(childPlanNode)
|
||||
|
||||
if childPlanNode.type == TestEnum.NodeType.It then
|
||||
if session:shouldSkip() then
|
||||
childResultNode.status = TestEnum.TestStatus.Skipped
|
||||
else
|
||||
if tryStack:size() > 0 and tryStack:getBack().isOk == false then
|
||||
childResultNode.status = TestEnum.TestStatus.Failure
|
||||
table.insert(childResultNode.errors,
|
||||
string.format("%q failed without trying, because test case %q failed",
|
||||
childPlanNode.phrase, tryStack:getBack().failedNode.phrase))
|
||||
else
|
||||
-- Errors can be set either via `error` propagating upwards or
|
||||
-- by a test calling fail([message]).
|
||||
local success = true
|
||||
local errorMessage
|
||||
|
||||
local testEnvironment = getfenv(childPlanNode.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 = message .. "\n" .. debug.traceback()
|
||||
end
|
||||
|
||||
-- We prefer xpcall, but yielding doesn't work from xpcall.
|
||||
-- As a workaround, you can mark nodes as "not xpcallable"
|
||||
local call = noXpcall and pcall or xpcall
|
||||
|
||||
-- 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
|
||||
|
||||
local nodeSuccess, nodeResult = call(childPlanNode.callback, function(message)
|
||||
return message .. "\n" .. debug.traceback()
|
||||
end)
|
||||
|
||||
_G[RUNNING_GLOBAL] = nil
|
||||
|
||||
-- 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
|
||||
|
||||
if success then
|
||||
childResultNode.status = TestEnum.TestStatus.Success
|
||||
else
|
||||
childResultNode.status = TestEnum.TestStatus.Failure
|
||||
table.insert(childResultNode.errors, errorMessage)
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif childPlanNode.type == TestEnum.NodeType.Describe or childPlanNode.type == TestEnum.NodeType.Try then
|
||||
if childPlanNode.type == TestEnum.NodeType.Try then tryStack:push({isOk = true, failedNode = nil}) end
|
||||
TestRunner.runPlanNode(session, childPlanNode, tryStack, childPlanNode.HACK_NO_XPCALL)
|
||||
if childPlanNode.type == TestEnum.NodeType.Try then tryStack:pop() end
|
||||
|
||||
local status = TestEnum.TestStatus.Success
|
||||
|
||||
-- Did we have an error trying build a test plan?
|
||||
if childPlanNode.loadError then
|
||||
status = TestEnum.TestStatus.Failure
|
||||
|
||||
local message = "Error during planning: " .. childPlanNode.loadError
|
||||
|
||||
table.insert(childResultNode.errors, message)
|
||||
else
|
||||
local skipped = true
|
||||
|
||||
-- If all children were skipped, then we were skipped
|
||||
-- If any child failed, then we failed!
|
||||
for _, child in ipairs(childResultNode.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
|
||||
end
|
||||
|
||||
childResultNode.status = status
|
||||
end
|
||||
|
||||
session:popNode()
|
||||
end
|
||||
end
|
||||
|
||||
return TestRunner
|
||||
@@ -0,0 +1,149 @@
|
||||
--[[
|
||||
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 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 = {},
|
||||
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)
|
||||
|
||||
return node
|
||||
end
|
||||
|
||||
--[[
|
||||
Pops a node off of the navigation stack.
|
||||
]]
|
||||
function TestSession:popNode()
|
||||
assert(#self.nodeStack > 0, "Tried to pop from an empty node stack!")
|
||||
return table.remove(self.nodeStack, #self.nodeStack)
|
||||
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
|
||||
|
||||
return TestSession
|
||||
@@ -0,0 +1,40 @@
|
||||
local Expectation = require(script.Expectation)
|
||||
local TestBootstrap = require(script.TestBootstrap)
|
||||
local TestEnum = require(script.TestEnum)
|
||||
local TestPlan = require(script.TestPlan)
|
||||
local TestPlanBuilder = require(script.TestPlanBuilder)
|
||||
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 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,
|
||||
TestPlanBuilder = TestPlanBuilder,
|
||||
TestPlanner = TestPlanner,
|
||||
TestResults = TestResults,
|
||||
TestRunner = TestRunner,
|
||||
TestSession = TestSession,
|
||||
|
||||
Reporters = {
|
||||
TextReporter = TextReporter,
|
||||
TeamCityReporter = TeamCityReporter,
|
||||
},
|
||||
}
|
||||
|
||||
return TestEZ
|
||||
Reference in New Issue
Block a user