Clients/Client2018/content/LuaPackages/TestEZImpl/TestRunner.lua

152 lines
4.6 KiB
Lua

--[[
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