mirror of
https://github.com/imezx/Warp.git
synced 2025-04-24 15:10:03 +00:00
305 lines
7.7 KiB
Lua
305 lines
7.7 KiB
Lua
|
--[[
|
||
|
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
|