From 6c36a862a24f4b2d858f991f2582a8c8603e2da1 Mon Sep 17 00:00:00 2001 From: MagiMaster Date: Thu, 7 May 2020 13:58:46 -0700 Subject: [PATCH 01/28] Collapse TestPlan/TestPlanner/TestPlanBuilder/TestEnvironment --- src/TestEnvironment.lua | 144 ----------------------- src/TestPlan.lua | 244 +++++++++++++++++++++++++++++++++------ src/TestPlanBuilder.lua | 96 --------------- src/TestPlanner.lua | 45 +------- src/init.lua | 2 - tests/lifecycleHooks.lua | 4 + 6 files changed, 219 insertions(+), 316 deletions(-) delete mode 100644 src/TestEnvironment.lua delete mode 100644 src/TestPlanBuilder.lua diff --git a/src/TestEnvironment.lua b/src/TestEnvironment.lua deleted file mode 100644 index f2433e8..0000000 --- a/src/TestEnvironment.lua +++ /dev/null @@ -1,144 +0,0 @@ ---[[ - 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. -]] -local TestEnum = require(script.Parent.TestEnum) - -local TestEnvironment = {} - -function TestEnvironment.new(builder, extraEnvironment) - local env = {} - - if extraEnvironment then - if type(extraEnvironment) ~= "table" then - error(("Bad argument #2 to TestEnvironment.new. Expected table, got %s"):format( - typeof(extraEnvironment)), 2) - end - - for key, value in pairs(extraEnvironment) do - env[key] = value - end - end - - 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.it(phrase, callback) - local node = builder:pushNode(phrase, TestEnum.NodeType.It) - - node.callback = callback - - builder:popNode() - 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) - local node = builder:pushNode(name .. "_" .. tostring(lifecyclePhaseId), nodeType) - lifecyclePhaseId = lifecyclePhaseId + 1 - - node.callback = callback - - builder:popNode() - end - 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() - 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 - - setmetatable(env, TestEnvironment) - return env -end - -return TestEnvironment \ No newline at end of file diff --git a/src/TestPlan.lua b/src/TestPlan.lua index ce6759e..3a4de71 100644 --- a/src/TestPlan.lua +++ b/src/TestPlan.lua @@ -7,6 +7,111 @@ local TestEnum = require(script.Parent.TestEnum) +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 + + 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 = currentNode:addChild(phrase, TestEnum.NodeType.Describe, nodeModifier) + node.callback = callback + node:expand() + return node + end + + function env.it(phrase, callback, nodeModifier) + local node = currentNode:addChild(phrase, TestEnum.NodeType.It, nodeModifier) + node.callback = callback + return node + 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) + local node = currentNode:addChild(name .. "_" .. tostring(lifecyclePhaseId), nodeType) + lifecyclePhaseId = lifecyclePhaseId + 1 + + node.callback = callback + return node + end + end + + function env.itFOCUS(phrase, callback) + return env.it(phrase, callback, TestEnum.NodeModifier.Focus) + end + + function env.itSKIP(phrase, callback) + return env.it(phrase, callback, TestEnum.NodeModifier.Skip) + end + + function env.itFIXME(phrase, callback) + local node = env.it(phrase, callback, TestEnum.NodeModifier.Skip) + warn("FIXME: broken test", node:getFullName()) + return node + 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 + + --[[ + 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() + 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 + + return env +end + local TestPlan = {} TestPlan.__index = TestPlan @@ -14,56 +119,129 @@ TestPlan.__index = TestPlan --[[ Create a new, empty TestPlan. ]] -function TestPlan.new() - local self = { - children = {} +function TestPlan.new(testNamePattern, extraEnvironment) + local plan = { + children = {}, + testNamePattern = testNamePattern, } - setmetatable(self, TestPlan) + local Node = {} + Node.__index = Node - return self -end + function Node.new(phrase, nodeType, nodeModifier) + nodeModifier = nodeModifier or TestEnum.NodeModifier.None ---[[ - 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 + local node = { + phrase = phrase, + type = nodeType, + modifier = nodeModifier, + children = {}, + callback = nil, + } - for _, child in ipairs(root.children) do - callback(child, level) + node.environment = newEnvironment(node, extraEnvironment) + return setmetatable(node, Node) + end - self:visitAllNodes(callback, child, level + 1) + function Node:addChild(phrase, nodeType, nodeModifier) + if testNamePattern and (nodeModifier == nil or nodeModifier == TestEnum.NodeModifier.None) then + local name = self:getFullName() .. " " .. phrase + if name:match(testNamePattern) then + nodeModifier = TestEnum.NodeModifier.Focus + else + nodeModifier = TestEnum.NodeModifier.Skip + end + end + local child = Node.new(phrase, nodeType, nodeModifier) + child.parent = self + table.insert(self.children, child) + return child + end + + function Node:getFullName() + if self.parent and self.parent.getFullName then + local parentPhrase = self.parent:getFullName() + if parentPhrase then + return parentPhrase .. " " .. self.phrase + end + end + return self.phrase end + + function Node:expand() + local originalEnv = getfenv(self.callback) + local callbackEnv = setmetatable({}, { __index = originalEnv }) + for key, value in pairs(self.environment) do + callbackEnv[key] = value + end + setfenv(self.callback, callbackEnv) + + local success, result = xpcall(self.callback, function(err) + return err .. "\n" .. debug.traceback() + end) + + if not success then + self.loadError = result + end + end + + plan.Node = Node + + return setmetatable(plan, TestPlan) end -local function constructNodeFullName(node) - if node.parent then - local parentPhrase = constructNodeFullName(node.parent) - if parentPhrase then - return parentPhrase .. " " .. node.phrase +function TestPlan:addChild(phrase, nodeType, nodeModifier) + if self.testNamePattern and (nodeModifier == nil or nodeModifier == TestEnum.NodeModifier.None) then + if phrase:match(self.testNamePattern) then + nodeModifier = TestEnum.NodeModifier.Focus + else + nodeModifier = TestEnum.NodeModifier.Skip end end - return node.phrase + local child = self.Node.new(phrase, nodeType, nodeModifier) + child.parent = self + table.insert(self.children, child) + return child 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 +function TestPlan:addRoot(path, method) + local curNode = self + for i = #path, 1, -1 do + local nextNode = nil - local node = { - phrase = phrase, - type = nodeType, - modifier = nodeModifier, - children = {}, - callback = nil, - getFullName = constructNodeFullName - } + for _, child in ipairs(curNode.children) do + if child.phrase == path[i] then + nextNode = child + break + end + end - return node + 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 --[[ diff --git a/src/TestPlanBuilder.lua b/src/TestPlanBuilder.lua deleted file mode 100644 index 841874c..0000000 --- a/src/TestPlanBuilder.lua +++ /dev/null @@ -1,96 +0,0 @@ ---[[ - 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 = {}, - 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 - - 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 \ No newline at end of file diff --git a/src/TestPlanner.lua b/src/TestPlanner.lua index da39b5d..4b9772a 100644 --- a/src/TestPlanner.lua +++ b/src/TestPlanner.lua @@ -3,45 +3,10 @@ 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 TestEnvironment = require(script.Parent.TestEnvironment) +local TestPlan = require(script.Parent.TestPlan) 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 TestPlan from a list of specification functions. @@ -49,15 +14,13 @@ end variants), which will be turned into a test plan to be executed. ]] function TestPlanner.createPlan(specFunctions, testNamePattern, extraEnvironment) - local builder = TestPlanBuilder.new() - builder.testNamePattern = testNamePattern - local env = TestEnvironment.new(builder, extraEnvironment) + local plan = TestPlan.new(testNamePattern, extraEnvironment) for _, module in ipairs(specFunctions) do - buildPlan(builder, module, env) + plan:addRoot(module.path, module.method) end - return builder:finalize() + return plan end return TestPlanner \ No newline at end of file diff --git a/src/init.lua b/src/init.lua index ac3ce3d..9c702a1 100644 --- a/src/init.lua +++ b/src/init.lua @@ -2,7 +2,6 @@ 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) @@ -26,7 +25,6 @@ local TestEZ = { TestBootstrap = TestBootstrap, TestEnum = TestEnum, TestPlan = TestPlan, - TestPlanBuilder = TestPlanBuilder, TestPlanner = TestPlanner, TestResults = TestResults, TestRunner = TestRunner, diff --git a/tests/lifecycleHooks.lua b/tests/lifecycleHooks.lua index 10acd63..1d5225c 100644 --- a/tests/lifecycleHooks.lua +++ b/tests/lifecycleHooks.lua @@ -39,6 +39,10 @@ local function runTestPlan(testPlan) local plan = TestEZ.TestPlanner.createPlan({ { method = function() + -- This function environment hack is needed because the testPlan + -- function is not defined or required from within a test. This + -- shouldn't come up in real tests. + setfenv(testPlan, getfenv()) testPlan(insertLifecycleEvent) end, path = {'lifecycleHooksTest'} From b7e1877c0e6e5a8a833e8d9c2191de1f926043d9 Mon Sep 17 00:00:00 2001 From: MagiMaster Date: Fri, 8 May 2020 18:04:59 -0700 Subject: [PATCH 02/28] Fix test name. Now it works as expected the first time --- .../duplicateDescribe.spec.lua} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename tests/{passing/duplicateDescribe.lua => failing/duplicateDescribe.spec.lua} (61%) diff --git a/tests/passing/duplicateDescribe.lua b/tests/failing/duplicateDescribe.spec.lua similarity index 61% rename from tests/passing/duplicateDescribe.lua rename to tests/failing/duplicateDescribe.spec.lua index 8aae411..31a440b 100644 --- a/tests/passing/duplicateDescribe.lua +++ b/tests/failing/duplicateDescribe.spec.lua @@ -2,13 +2,13 @@ return function() describe("with the same description", function() - it("should not run this", function() - error("this won't happen") + it("should run this", function() + error("this won't get overwritten") end) end) describe("with the same description", function() - it("should only run this test", function() + it("should also run this", function() end) end) end \ No newline at end of file From ab4a9acfb7f8b1bcfeb0cb9a2d2d29430be13e3f Mon Sep 17 00:00:00 2001 From: MagiMaster Date: Fri, 8 May 2020 18:09:49 -0700 Subject: [PATCH 03/28] Add a more explicit check of duplicate nodes --- tests/planner.lua | 10 ++++++++++ tests/planning/d.spec.lua | 21 +++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 tests/planning/d.spec.lua diff --git a/tests/planner.lua b/tests/planner.lua index fb0bfdf..60ca32b 100644 --- a/tests/planner.lua +++ b/tests/planner.lua @@ -62,6 +62,11 @@ return { "planning b test1", "planning b test2", "planning b test2 test3", + "planning d", + "planning d test4", + "planning d test4 test5", + "planning d test4 test6", + "planning d test4 test7", })) end, ["it should mark skipped tests as skipped"] = function() @@ -74,6 +79,11 @@ return { "planning b", "planning b test1", "planning b test2 test3", -- This isn't marked skip, its parent is + "planning d", + "planning d test4", + "planning d test4 test5", + "planning d test4 test6", + "planning d test4 test7", }, true)) end, ["it should skip tests that don't match the filter"] = function() diff --git a/tests/planning/d.spec.lua b/tests/planning/d.spec.lua new file mode 100644 index 0000000..0f88430 --- /dev/null +++ b/tests/planning/d.spec.lua @@ -0,0 +1,21 @@ +-- luacheck: globals describe it SKIP + +return function() + describe("test4", function() + it("test5", function() + end) + + it("test6", function() + end) + end) + + describe("test4", function() + -- Duplicate describe blocks should get merged. + it("test5", function() + -- Duplicate it blocks will get overwritten. + end) + + it("test7", function() + end) + end) +end From ff58302d7bab5e1b29457f6e396983736c3af05f Mon Sep 17 00:00:00 2001 From: MagiMaster Date: Fri, 8 May 2020 18:17:40 -0700 Subject: [PATCH 04/28] Update tests of duplicate it blocks to match new code --- tests/failing/duplicateIt.spec.lua | 13 +++++++++++++ tests/passing/duplicateIt.spec.lua | 18 ------------------ 2 files changed, 13 insertions(+), 18 deletions(-) create mode 100644 tests/failing/duplicateIt.spec.lua delete mode 100644 tests/passing/duplicateIt.spec.lua diff --git a/tests/failing/duplicateIt.spec.lua b/tests/failing/duplicateIt.spec.lua new file mode 100644 index 0000000..6960077 --- /dev/null +++ b/tests/failing/duplicateIt.spec.lua @@ -0,0 +1,13 @@ +-- luacheck: globals describe it + +return function() + describe("multiple it blocks with the same description", function() + it("all get run", function() + end) + it("all get run", function() + error("this shouldn't get overwritten") + end) + it("all get run", function() + end) + end) +end \ No newline at end of file diff --git a/tests/passing/duplicateIt.spec.lua b/tests/passing/duplicateIt.spec.lua deleted file mode 100644 index 1d55c75..0000000 --- a/tests/passing/duplicateIt.spec.lua +++ /dev/null @@ -1,18 +0,0 @@ --- luacheck: globals describe it - -return function() - describe("multiple it blocks with the same description", function() - it("only the last runs", function() - error("first") - end) - it("only the last runs", function() - error("second") - end) - it("only the last runs", function() - error("third") - end) - it("only the last runs", function() - -- This is the only one that will run - end) - end) -end \ No newline at end of file From 79c68b770396fbb8011995c030be5e4d9bf5b558 Mon Sep 17 00:00:00 2001 From: MagiMaster Date: Wed, 13 May 2020 13:01:20 -0700 Subject: [PATCH 05/28] Add expectation to environment --- src/TestPlan.lua | 3 +++ tests/passing/expect.spec.lua | 11 +++++++++++ 2 files changed, 14 insertions(+) create mode 100644 tests/passing/expect.spec.lua diff --git a/src/TestPlan.lua b/src/TestPlan.lua index 3a4de71..beb5664 100644 --- a/src/TestPlan.lua +++ b/src/TestPlan.lua @@ -6,6 +6,7 @@ ]] local TestEnum = require(script.Parent.TestEnum) +local Expectation = require(script.Parent.Expectation) local function newEnvironment(currentNode, extraEnvironment) local env = {} @@ -109,6 +110,8 @@ local function newEnvironment(currentNode, extraEnvironment) env.fdescribe = env.describeFOCUS env.xdescribe = env.describeSKIP + env.expect = Expectation.new + return env end diff --git a/tests/passing/expect.spec.lua b/tests/passing/expect.spec.lua new file mode 100644 index 0000000..7929352 --- /dev/null +++ b/tests/passing/expect.spec.lua @@ -0,0 +1,11 @@ +return function() + local function helper() + expect(1).to.never.equal(2) + end + + describe("from within a describe block", function() + it("helpers should be able to access expect", function() + helper() + end) + end) +end \ No newline at end of file From ed47b2c24d04c3bb9a79584283ec8920be43faed Mon Sep 17 00:00:00 2001 From: MagiMaster Date: Wed, 13 May 2020 13:04:40 -0700 Subject: [PATCH 06/28] Add luacheck globals for new test --- tests/passing/expect.spec.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/passing/expect.spec.lua b/tests/passing/expect.spec.lua index 7929352..726b267 100644 --- a/tests/passing/expect.spec.lua +++ b/tests/passing/expect.spec.lua @@ -1,3 +1,4 @@ +-- luacheck: globals describe it expect return function() local function helper() expect(1).to.never.equal(2) From 04afd682b79fc450510f205406b24d51ba42c361 Mon Sep 17 00:00:00 2001 From: MagiMaster Date: Thu, 14 May 2020 13:47:42 -0700 Subject: [PATCH 07/28] Refactor TestNode to keep a pointer back to plan tree --- src/TestPlan.lua | 114 +++++++++++++++++++++++------------------------ 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/src/TestPlan.lua b/src/TestPlan.lua index beb5664..8cd2086 100644 --- a/src/TestPlan.lua +++ b/src/TestPlan.lua @@ -115,80 +115,80 @@ local function newEnvironment(currentNode, extraEnvironment) return env end +local TestNode = {} +TestNode.__index = TestNode + local TestPlan = {} TestPlan.__index = TestPlan ---[[ - Create a new, empty TestPlan. -]] -function TestPlan.new(testNamePattern, extraEnvironment) - local plan = { +function TestNode.new(plan, phrase, nodeType, nodeModifier) + nodeModifier = nodeModifier or TestEnum.NodeModifier.None + + local node = { + plan = plan, + phrase = phrase, + type = nodeType, + modifier = nodeModifier, children = {}, - testNamePattern = testNamePattern, + callback = nil, } - local Node = {} - Node.__index = Node - - function Node.new(phrase, nodeType, nodeModifier) - nodeModifier = nodeModifier or TestEnum.NodeModifier.None - - local node = { - phrase = phrase, - type = nodeType, - modifier = nodeModifier, - children = {}, - callback = nil, - } - - node.environment = newEnvironment(node, extraEnvironment) - return setmetatable(node, Node) - end + node.environment = newEnvironment(node, plan.extraEnvironment) + return setmetatable(node, TestNode) +end - function Node:addChild(phrase, nodeType, nodeModifier) - if testNamePattern and (nodeModifier == nil or nodeModifier == TestEnum.NodeModifier.None) then - local name = self:getFullName() .. " " .. phrase - if name:match(testNamePattern) then - nodeModifier = TestEnum.NodeModifier.Focus - else - nodeModifier = TestEnum.NodeModifier.Skip - end +function TestNode:addChild(phrase, nodeType, nodeModifier) + if self.plan.testNamePattern and (nodeModifier == nil or nodeModifier == TestEnum.NodeModifier.None) then + local name = self:getFullName() .. " " .. phrase + if name:match(self.plan.testNamePattern) then + nodeModifier = TestEnum.NodeModifier.Focus + else + nodeModifier = TestEnum.NodeModifier.Skip end - local child = Node.new(phrase, nodeType, nodeModifier) - child.parent = self - table.insert(self.children, child) - return child end + local child = TestNode.new(self.plan, phrase, nodeType, nodeModifier) + child.parent = self + table.insert(self.children, child) + return child +end - function Node:getFullName() - if self.parent and self.parent.getFullName then - local parentPhrase = self.parent:getFullName() - if parentPhrase then - return parentPhrase .. " " .. self.phrase - end +function TestNode:getFullName() + if self.parent and self.parent.getFullName then + local parentPhrase = self.parent:getFullName() + if parentPhrase then + return parentPhrase .. " " .. self.phrase end - return self.phrase end + return self.phrase +end - function Node:expand() - local originalEnv = getfenv(self.callback) - local callbackEnv = setmetatable({}, { __index = originalEnv }) - for key, value in pairs(self.environment) do - callbackEnv[key] = value - end - setfenv(self.callback, callbackEnv) +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 + setfenv(self.callback, callbackEnv) - local success, result = xpcall(self.callback, function(err) - return err .. "\n" .. debug.traceback() - end) + local success, result = xpcall(self.callback, function(err) + return err .. "\n" .. debug.traceback() + end) - if not success then - self.loadError = result - end + if not success then + self.loadError = result end +end - plan.Node = Node +--[[ + Create a new, empty TestPlan. +]] +function TestPlan.new(testNamePattern, extraEnvironment) + local plan = { + children = {}, + testNamePattern = testNamePattern, + extraEnvironment = extraEnvironment, + } return setmetatable(plan, TestPlan) end @@ -201,7 +201,7 @@ function TestPlan:addChild(phrase, nodeType, nodeModifier) nodeModifier = TestEnum.NodeModifier.Skip end end - local child = self.Node.new(phrase, nodeType, nodeModifier) + local child = TestNode.new(self, phrase, nodeType, nodeModifier) child.parent = self table.insert(self.children, child) return child From a33e15869520f271ce7d74242cfaef39ff873e8f Mon Sep 17 00:00:00 2001 From: MagiMaster Date: Thu, 14 May 2020 13:55:50 -0700 Subject: [PATCH 08/28] Update comments --- src/TestPlan.lua | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/src/TestPlan.lua b/src/TestPlan.lua index 8cd2086..4c3ec30 100644 --- a/src/TestPlan.lua +++ b/src/TestPlan.lua @@ -92,13 +92,8 @@ local function newEnvironment(currentNode, extraEnvironment) 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. + 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 " .. @@ -118,10 +113,11 @@ end local TestNode = {} TestNode.__index = TestNode -local TestPlan = {} - -TestPlan.__index = TestPlan - +--[[ + 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 option and will be + None if left blank. +]] function TestNode.new(plan, phrase, nodeType, nodeModifier) nodeModifier = nodeModifier or TestEnum.NodeModifier.None @@ -153,6 +149,9 @@ function TestNode:addChild(phrase, nodeType, nodeModifier) return child end +--[[ + Join the names of all the nodes back to the parent. +]] function TestNode:getFullName() if self.parent and self.parent.getFullName then local parentPhrase = self.parent:getFullName() @@ -163,6 +162,10 @@ function TestNode:getFullName() 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 }) @@ -180,6 +183,9 @@ function TestNode:expand() end end +local TestPlan = {} +TestPlan.__index = TestPlan + --[[ Create a new, empty TestPlan. ]] @@ -193,6 +199,9 @@ function TestPlan.new(testNamePattern, extraEnvironment) return setmetatable(plan, TestPlan) end +--[[ + Add a new child under the test plan's root node. +]] function TestPlan:addChild(phrase, nodeType, nodeModifier) if self.testNamePattern and (nodeModifier == nil or nodeModifier == TestEnum.NodeModifier.None) then if phrase:match(self.testNamePattern) then @@ -208,7 +217,8 @@ function TestPlan:addChild(phrase, nodeType, nodeModifier) 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 From 90d935a8cf33154a37953b5db81a3d88a865c7ad Mon Sep 17 00:00:00 2001 From: MagiMaster Date: Thu, 14 May 2020 14:04:42 -0700 Subject: [PATCH 09/28] Typo --- src/TestPlan.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/TestPlan.lua b/src/TestPlan.lua index 4c3ec30..c9086d7 100644 --- a/src/TestPlan.lua +++ b/src/TestPlan.lua @@ -115,8 +115,8 @@ 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 option and will be - None if left blank. + 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 From 5ed50deafe60febf3393186ade172d400a1e848e Mon Sep 17 00:00:00 2001 From: MagiMaster Date: Thu, 14 May 2020 14:15:45 -0700 Subject: [PATCH 10/28] Remove odd plan-as-parent pointer since the plan is stored explicitly --- src/TestPlan.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/TestPlan.lua b/src/TestPlan.lua index c9086d7..b61a479 100644 --- a/src/TestPlan.lua +++ b/src/TestPlan.lua @@ -128,6 +128,7 @@ function TestNode.new(plan, phrase, nodeType, nodeModifier) modifier = nodeModifier, children = {}, callback = nil, + parent = nil, } node.environment = newEnvironment(node, plan.extraEnvironment) @@ -153,7 +154,7 @@ end Join the names of all the nodes back to the parent. ]] function TestNode:getFullName() - if self.parent and self.parent.getFullName then + if self.parent then local parentPhrase = self.parent:getFullName() if parentPhrase then return parentPhrase .. " " .. self.phrase @@ -211,7 +212,6 @@ function TestPlan:addChild(phrase, nodeType, nodeModifier) end end local child = TestNode.new(self, phrase, nodeType, nodeModifier) - child.parent = self table.insert(self.children, child) return child end From 0a37fb85fc90cf3ceb5979f317ea961a4e0aa614 Mon Sep 17 00:00:00 2001 From: MagiMaster Date: Thu, 14 May 2020 14:24:36 -0700 Subject: [PATCH 11/28] Remove reference to TestPlanBuilder --- src/TestPlan.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/TestPlan.lua b/src/TestPlan.lua index b61a479..9f36ede 100644 --- a/src/TestPlan.lua +++ b/src/TestPlan.lua @@ -2,7 +2,7 @@ Represents a tree of tests that have been loaded but not necessarily executed yet. - TestPlan objects are produced by TestPlanner and TestPlanBuilder. + TestPlan objects are produced by TestPlanner. ]] local TestEnum = require(script.Parent.TestEnum) From e3249cb286d1e95ce4ea226694fe2b9d373d4fd0 Mon Sep 17 00:00:00 2001 From: MagiMaster Date: Mon, 18 May 2020 18:10:56 -0700 Subject: [PATCH 12/28] Tests for init.spec ordering --- tests/expandOrder.lua | 25 +++++++++++++++++++++++++ tests/planning/init.spec.lua | 4 ++++ 2 files changed, 29 insertions(+) create mode 100644 tests/expandOrder.lua create mode 100644 tests/planning/init.spec.lua diff --git a/tests/expandOrder.lua b/tests/expandOrder.lua new file mode 100644 index 0000000..6e70ae0 --- /dev/null +++ b/tests/expandOrder.lua @@ -0,0 +1,25 @@ +local TestEZ = require(script.Parent.Parent.TestEZ) + +return { + ["init.spec.lua is run before children are expanded"] = function() + local initialized = false + + local plan = TestEZ.TestPlanner.createPlan({ + { + method = function() + assert(initialized, "init.spec was not called before bar.spec") + end, + path = {'bar.spec', 'foo'} + }, + { + method = function() + initialized = true + end, + path = {'foo'} + }, + }) + + local results = TestEZ.TestRunner.runPlan(plan) + assert(#results.errors == 0, "init test failed: " .. tostring(results.errors[1])) + end, +} \ No newline at end of file diff --git a/tests/planning/init.spec.lua b/tests/planning/init.spec.lua new file mode 100644 index 0000000..eef2433 --- /dev/null +++ b/tests/planning/init.spec.lua @@ -0,0 +1,4 @@ +-- This should be added to the "planning" node of the tree instead of creating +-- a new node. +return function() +end From 062408ae9e6d60bc6c0082f9771c6dab98a9e806 Mon Sep 17 00:00:00 2001 From: MagiMaster Date: Mon, 18 May 2020 18:14:14 -0700 Subject: [PATCH 13/28] Use a finalize function to be sure order is correct --- src/TestPlan.lua | 25 +++++++++++++++++++++---- src/TestPlanner.lua | 1 + 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/TestPlan.lua b/src/TestPlan.lua index 9f36ede..a869a0d 100644 --- a/src/TestPlan.lua +++ b/src/TestPlan.lua @@ -33,7 +33,7 @@ local function newEnvironment(currentNode, extraEnvironment) function env.describe(phrase, callback, nodeModifier) local node = currentNode:addChild(phrase, TestEnum.NodeType.Describe, nodeModifier) node.callback = callback - node:expand() + --node:expand() return node end @@ -135,6 +135,9 @@ function TestNode.new(plan, phrase, nodeType, nodeModifier) return setmetatable(node, TestNode) end +--[[ + Add a child to this node, applying the test name pattern in the process. +]] function TestNode:addChild(phrase, nodeType, nodeModifier) if self.plan.testNamePattern and (nodeModifier == nil or nodeModifier == TestEnum.NodeModifier.None) then local name = self:getFullName() .. " " .. phrase @@ -164,10 +167,14 @@ function TestNode:getFullName() 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. + Expand a node by setting its callback environment and then calling it. Only + expands this one node. ]] function TestNode:expand() + if not self.callback then + return + end + local originalEnv = getfenv(self.callback) local callbackEnv = setmetatable({}, { __index = originalEnv }) for key, value in pairs(self.environment) do @@ -240,7 +247,17 @@ function TestPlan:addRoot(path, method) end curNode.callback = method - curNode:expand() +end + +--[[ + Expands all describe nodes, leaving the plan in a runnable state. +]] +function TestPlan:finalize() + self:visitAllNodes(function(node) + if node.type == TestEnum.NodeType.Describe then + node:expand() + end + end) end --[[ diff --git a/src/TestPlanner.lua b/src/TestPlanner.lua index 4b9772a..484157d 100644 --- a/src/TestPlanner.lua +++ b/src/TestPlanner.lua @@ -20,6 +20,7 @@ function TestPlanner.createPlan(specFunctions, testNamePattern, extraEnvironment plan:addRoot(module.path, module.method) end + plan:finalize() return plan end From 5ae438d499b4df09c4ab251729dbcfe6f309ae05 Mon Sep 17 00:00:00 2001 From: MagiMaster Date: Mon, 18 May 2020 18:23:11 -0700 Subject: [PATCH 14/28] Another test to make sure init.spec and afterAll work together --- tests/expandOrder.lua | 46 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/expandOrder.lua b/tests/expandOrder.lua index 6e70ae0..6d582a4 100644 --- a/tests/expandOrder.lua +++ b/tests/expandOrder.lua @@ -22,4 +22,50 @@ return { local results = TestEZ.TestRunner.runPlan(plan) assert(#results.errors == 0, "init test failed: " .. tostring(results.errors[1])) end, + ["init.spec.lua afterAll can correctly undo changes"] = function() + local initialized = false + + local plan = TestEZ.TestPlanner.createPlan({ + { + method = function() + assert(not initialized, "initialized was true in foo/a.spec") + end, + path = {'a.spec', 'foo'} + }, + { + method = function() + assert(initialized, "initialized was false in foo/bar/b.spec") + end, + path = {'b.spec', 'bar', 'foo'} + }, + { + method = function() + initialized = true + + -- luacheck: globals afterAll + afterAll(function() + print("!!!!!!!!!!!!!") + initialized = false + end) + end, + path = {'bar', 'foo'} + }, + { + method = function() + assert(initialized, "initialized was false in foo/bar/c.spec") + end, + path = {'c.spec', 'bar', 'foo'} + }, + { + method = function() + assert(not initialized, "initialized was true in foo/d.spec") + end, + path = {'d.spec', 'foo'} + }, + }) + + local results = TestEZ.TestRunner.runPlan(plan) + assert(#results.errors == 0, "init test failed:\n" .. + table.concat(results.errors, "\n")) + end, } \ No newline at end of file From e90c1df424a4d0995489e6fc9005dee0d184a5df Mon Sep 17 00:00:00 2001 From: MagiMaster Date: Mon, 18 May 2020 18:23:53 -0700 Subject: [PATCH 15/28] Remove debug print --- tests/expandOrder.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/expandOrder.lua b/tests/expandOrder.lua index 6d582a4..0aea59b 100644 --- a/tests/expandOrder.lua +++ b/tests/expandOrder.lua @@ -44,7 +44,6 @@ return { -- luacheck: globals afterAll afterAll(function() - print("!!!!!!!!!!!!!") initialized = false end) end, From 4b5a677bb74d1c4529ab17f7a324a34b977fd1fd Mon Sep 17 00:00:00 2001 From: MagiMaster Date: Tue, 19 May 2020 11:49:24 -0700 Subject: [PATCH 16/28] Expand test of lifecycle hooks --- tests/lifecycleHooks.lua | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/lifecycleHooks.lua b/tests/lifecycleHooks.lua index 1d5225c..ee387f6 100644 --- a/tests/lifecycleHooks.lua +++ b/tests/lifecycleHooks.lua @@ -137,6 +137,10 @@ return { end) end) end) + + it("runs root again", function() + insertLifecycleEvent("1 - another test") + end) end) expectShallowEquals(lifecycleOrder, { @@ -151,6 +155,9 @@ return { "2 - afterEach", "1 - afterEach", "2 - afterAll", + "1 - beforeEach", + "1 - another test", + "1 - afterEach", "1 - afterAll", }) expectNoFailures(results) From 5265c39b96ec698e7345149240be2e757d0ec8fb Mon Sep 17 00:00:00 2001 From: MagiMaster Date: Tue, 19 May 2020 11:49:24 -0700 Subject: [PATCH 17/28] Expand test of lifecycle hooks --- tests/lifecycleHooks.lua | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/lifecycleHooks.lua b/tests/lifecycleHooks.lua index 10acd63..02e76c7 100644 --- a/tests/lifecycleHooks.lua +++ b/tests/lifecycleHooks.lua @@ -133,6 +133,10 @@ return { end) end) end) + + it("runs root again", function() + insertLifecycleEvent("1 - another test") + end) end) expectShallowEquals(lifecycleOrder, { @@ -147,6 +151,9 @@ return { "2 - afterEach", "1 - afterEach", "2 - afterAll", + "1 - beforeEach", + "1 - another test", + "1 - afterEach", "1 - afterAll", }) expectNoFailures(results) From 815f0eeaa7a2512c4468f9e4942a42971b33fc11 Mon Sep 17 00:00:00 2001 From: MagiMaster Date: Tue, 19 May 2020 14:04:07 -0700 Subject: [PATCH 18/28] Fix #101, change beforeAll and afterAll hook implementation --- src/LifecycleHooks.lua | 105 +++++++++------------------------------ tests/lifecycleHooks.lua | 7 +++ 2 files changed, 30 insertions(+), 82 deletions(-) diff --git a/src/LifecycleHooks.lua b/src/LifecycleHooks.lua index 3ef05fe..288a680 100644 --- a/src/LifecycleHooks.lua +++ b/src/LifecycleHooks.lua @@ -7,8 +7,7 @@ function LifecycleHooks.new() local self = { _stack = {}, } - setmetatable(self, LifecycleHooks) - return self + return setmetatable(self, LifecycleHooks) end --[[ @@ -33,6 +32,7 @@ end 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) @@ -46,119 +46,60 @@ end Pushes uncalled beforeAll and afterAll hooks back up the stack ]] function LifecycleHooks:popHooks() - local popped = self._stack[#self._stack] table.remove(self._stack, #self._stack) - - local function pushHooksUp(type) - - local back = self:_getBackOfStack() - - if not back then - return - end - - back[type] = popped[type] - end - - pushHooksUp(TestEnum.NodeType.BeforeAll) - pushHooksUp(TestEnum.NodeType.AfterAll) end function LifecycleHooks:pushHooksFrom(planNode) assert(planNode ~= nil) table.insert(self._stack, { - [TestEnum.NodeType.BeforeAll] = self:_getBeforeAllHooksUncalledAtCurrentLevel(planNode.children), - [TestEnum.NodeType.AfterAll] = self:_getAfterAllHooksUncalledAtCurrentLevel(planNode.children), + [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 -function LifecycleHooks:getPendingBeforeAllHooks() - return self:_getAndClearPendingHooks(TestEnum.NodeType.BeforeAll) -end - -function LifecycleHooks:getAfterAllHooks() - if #self._stack > 0 then - return self:_getAndClearPendingHooks(TestEnum.NodeType.AfterAll) - else - return {} - end -end - --[[ - Return any hooks that have not yet been returned for this key and clear those hooks + Get all currently uncalled beforeAll hooks, and remove them from the stack. ]] -function LifecycleHooks:_getAndClearPendingHooks(key) - assert(key ~= nil) - - if #self._stack > 0 then - - local back = self._stack[#self._stack] - - local hooks = back[key] - - back[key] = {} - - return hooks - else - return {} - end -end - ---[[ - Transfers uncalled beforeAll and afterAll hooks down the stack -]] -function LifecycleHooks:_getBeforeAllHooksUncalledAtCurrentLevel(childNodes) - local hookType = TestEnum.NodeType.BeforeAll - local hooks = self:_getHooksOfTypeFromBackOfStack(hookType) - - for _, hook in pairs(self:_getHooksOfType(childNodes, hookType)) do - table.insert(hooks, hook) - end - - return hooks -end - -function LifecycleHooks:_getAfterAllHooksUncalledAtCurrentLevel(childNodes) - local hookType = TestEnum.NodeType.AfterAll - local hooks = self:_getHooksOfTypeFromBackOfStack(hookType) +function LifecycleHooks:getPendingBeforeAllHooks() + local key = TestEnum.NodeType.BeforeAll + local hooks = {} - for _, hook in pairs(self:_getHooksOfType(childNodes, hookType)) do - table.insert(hooks, 1, hook) + for _, level in ipairs(self._stack) do + for _, hook in ipairs(level[key]) do + table.insert(hooks, hook) + end + level[key] = {} end return hooks end -function LifecycleHooks:_getHooksOfTypeFromBackOfStack(hookType) - assert(hookType, "Expected hookType to be an argument") - - local currentBack = self:_getBackOfStack() - +--[[ + Get all uncalled afterAll hooks from the back of the stack and remove them. +]] +function LifecycleHooks:getAfterAllHooks() + local key = TestEnum.NodeType.AfterAll local hooks = {} + local currentBack = self._stack[#self._stack] or nil if currentBack then - for _, hook in pairs(currentBack[hookType]) do + for _, hook in pairs(currentBack[key]) do table.insert(hooks, hook) end - - currentBack[hookType] = {} + currentBack[key] = {} end return hooks end -function LifecycleHooks:_getBackOfStack() - return self._stack[#self._stack] or nil -end - -function LifecycleHooks:_getHooksOfType(nodes, type) +function LifecycleHooks:_getHooksOfType(nodes, key) local hooks = {} for _, node in pairs(nodes) do - if node.type == type then + if node.type == key then table.insert(hooks, node.callback) end end diff --git a/tests/lifecycleHooks.lua b/tests/lifecycleHooks.lua index 02e76c7..1f1d813 100644 --- a/tests/lifecycleHooks.lua +++ b/tests/lifecycleHooks.lua @@ -71,6 +71,10 @@ return { it("runs root", function() insertLifecycleEvent("1 - test") end) + + it("runs root again", function() + insertLifecycleEvent("1 - another test") + end) end) expectShallowEquals(lifecycleOrder, { @@ -78,6 +82,9 @@ return { "1 - beforeEach", "1 - test", "1 - afterEach", + "1 - beforeEach", + "1 - another test", + "1 - afterEach", "1 - afterAll", }) From 9060717064d6897f0bf22e450f3f37b1ee81c966 Mon Sep 17 00:00:00 2001 From: MagiMaster Date: Tue, 19 May 2020 16:47:38 -0700 Subject: [PATCH 19/28] Mention change in changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0d936e..f3fc852 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * Remove the `step` alias for `it` since that's meant for use with `try`. * Remove the `include` global function. * Remove `HACK_NO_XPCALL`. With recent changes to the definition of xpcall, this is no longer necessary. Since people are still using it, it will now print out a warning asking them to delete that call instead. +* Guarantee that `init.spec.lua` will run before any `it` or `describe` blocks in the folder under it. ## 0.2.0 (2020-03-04) * Added support for init.spec.lua. Code in this file is treated as belonging to the directory's node in the test tree. This allows for lifecycle hooks to be attached to all files in a directory. From 5b0d20e1d5c65002ac8d38d7aaa4573530fb6331 Mon Sep 17 00:00:00 2001 From: MagiMaster Date: Wed, 20 May 2020 11:37:50 -0700 Subject: [PATCH 20/28] Add it blocks for expand test --- tests/expandOrder.lua | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/tests/expandOrder.lua b/tests/expandOrder.lua index 0aea59b..2dbf01e 100644 --- a/tests/expandOrder.lua +++ b/tests/expandOrder.lua @@ -25,25 +25,36 @@ return { ["init.spec.lua afterAll can correctly undo changes"] = function() local initialized = false + -- luacheck: globals it local plan = TestEZ.TestPlanner.createPlan({ { method = function() - assert(not initialized, "initialized was true in foo/a.spec") + print("Ad") + it("A", function() + print("Ai") + assert(not initialized, "initialized was true in foo/a.spec") + end) end, path = {'a.spec', 'foo'} }, { method = function() - assert(initialized, "initialized was false in foo/bar/b.spec") + print("Bd") + it("B", function() + print("Bi") + assert(initialized, "initialized was false in foo/bar/b.spec") + end) end, path = {'b.spec', 'bar', 'foo'} }, { method = function() + print("Init") initialized = true -- luacheck: globals afterAll afterAll(function() + print("After") initialized = false end) end, @@ -51,13 +62,21 @@ return { }, { method = function() - assert(initialized, "initialized was false in foo/bar/c.spec") + print("Cd") + it("C", function() + print("Ci") + assert(initialized, "initialized was false in foo/bar/c.spec") + end) end, path = {'c.spec', 'bar', 'foo'} }, { method = function() - assert(not initialized, "initialized was true in foo/d.spec") + print("Dd") + it("D", function() + print("Di") + assert(not initialized, "initialized was true in foo/d.spec") + end) end, path = {'d.spec', 'foo'} }, From 2a2a579bba25ddd444c4642f610e10a49a09dc5f Mon Sep 17 00:00:00 2001 From: MagiMaster Date: Wed, 17 Jun 2020 16:07:42 -0700 Subject: [PATCH 21/28] First pass at adding loadmodule --- modules/lemur | 2 +- src/TestBootstrap.lua | 2 +- src/TestPlan.lua | 17 ++++++++++++++--- tests/modules/a.lua | 3 +++ tests/modules/b.lua | 3 +++ tests/passing/requires.spec.lua | 17 +++++++++++++++++ 6 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 tests/modules/a.lua create mode 100644 tests/modules/b.lua create mode 100644 tests/passing/requires.spec.lua diff --git a/modules/lemur b/modules/lemur index f7ad797..3e4ff7d 160000 --- a/modules/lemur +++ b/modules/lemur @@ -1 +1 @@ -Subproject commit f7ad797a70940f937eb16a98f6b6dc6e539fc3ba +Subproject commit 3e4ff7d8f09e57164ad0614c1b81cc9338caf006 diff --git a/src/TestBootstrap.lua b/src/TestBootstrap.lua index e3641a5..7bed034 100644 --- a/src/TestBootstrap.lua +++ b/src/TestBootstrap.lua @@ -54,7 +54,7 @@ function TestBootstrap:getModulesImpl(root, modules, current) current = current or root if isSpecScript(current) then - local method = require(current) + local method = debug.loadmodule(current) local path = getPath(current, root) local pathString = toStringPath(path) diff --git a/src/TestPlan.lua b/src/TestPlan.lua index 6529ec2..35711da 100644 --- a/src/TestPlan.lua +++ b/src/TestPlan.lua @@ -8,12 +8,12 @@ local TestEnum = require(script.Parent.TestEnum) local Expectation = require(script.Parent.Expectation) -local function newEnvironment(currentNode, extraEnvironment) +local function newEnvironment(currentNode, parentEnvironment, extraEnvironment) local env = {} if extraEnvironment then if type(extraEnvironment) ~= "table" then - error(("Bad argument #2 to newEnvironment. Expected table, got %s"):format( + error(("Bad argument #3 to newEnvironment. Expected table, got %s"):format( typeof(extraEnvironment)), 2) end @@ -107,6 +107,10 @@ local function newEnvironment(currentNode, extraEnvironment) env.expect = Expectation.new + function env.require(module) + return debug.loadmodule(module)() + end + return env end @@ -131,7 +135,7 @@ function TestNode.new(plan, phrase, nodeType, nodeModifier) parent = nil, } - node.environment = newEnvironment(node, plan.extraEnvironment) + node.environment = newEnvironment(node, nil, plan.extraEnvironment) return setmetatable(node, TestNode) end @@ -193,6 +197,13 @@ function TestNode:expand() if not success then self.loadError = result end + + if typeof(result) == "function" then + success, result = xpcall(result, debug.traceback) + if not success then + self.loadError = result + end + end end local TestPlan = {} diff --git a/tests/modules/a.lua b/tests/modules/a.lua new file mode 100644 index 0000000..5a7657c --- /dev/null +++ b/tests/modules/a.lua @@ -0,0 +1,3 @@ +local b = require(script.Parent.b) + +return b(1) -- 2 diff --git a/tests/modules/b.lua b/tests/modules/b.lua new file mode 100644 index 0000000..01d579b --- /dev/null +++ b/tests/modules/b.lua @@ -0,0 +1,3 @@ +return function(x) + return x + 1 +end diff --git a/tests/passing/requires.spec.lua b/tests/passing/requires.spec.lua new file mode 100644 index 0000000..c9639d8 --- /dev/null +++ b/tests/passing/requires.spec.lua @@ -0,0 +1,17 @@ +-- luacheck: globals it expect + +return function() + local a = require(script.Parent.Parent.modules.a) + local b = require(script.Parent.Parent.modules.b) + + it("requires should work properly", function() + expect(a).to.equal(2) + expect(b(2)).to.equal(3) + end) + + it("require cache works under normal circumstances", function() + local b2 = require(script.Parent.Parent.modules.b) + + expect(b).to.equal(b2) + end) +end From 8b1200abce7cd7939e3b0d4ad02d3b9de98d4a55 Mon Sep 17 00:00:00 2001 From: MagiMaster Date: Wed, 17 Jun 2020 17:30:47 -0700 Subject: [PATCH 22/28] Add recursive require definition --- src/TestPlan.lua | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/TestPlan.lua b/src/TestPlan.lua index 35711da..860c5c8 100644 --- a/src/TestPlan.lua +++ b/src/TestPlan.lua @@ -8,9 +8,19 @@ local TestEnum = require(script.Parent.TestEnum) local Expectation = require(script.Parent.Expectation) -local function newEnvironment(currentNode, parentEnvironment, extraEnvironment) +local REQUIRE_CACHE_KEY = {} + +local function newEnvironment(parentEnvironment, currentNode, extraEnvironment) local env = {} + local requireMeta = {} + if parentEnvironment then + requireMeta = {__index = parentEnvironment[REQUIRE_CACHE_KEY]} + end + + local requireCache = setmetatable({}, requireMeta) + env[REQUIRE_CACHE_KEY] = requireCache + if extraEnvironment then if type(extraEnvironment) ~= "table" then error(("Bad argument #3 to newEnvironment. Expected table, got %s"):format( @@ -108,7 +118,14 @@ local function newEnvironment(currentNode, parentEnvironment, extraEnvironment) env.expect = Expectation.new function env.require(module) - return debug.loadmodule(module)() + if not requireCache[module] then + local chunk = debug.loadmodule(module) + local originalEnv = getfenv(chunk) + local newEnv = setmetatable({require = env.require}, {__index = originalEnv}) + setfenv(chunk, newEnv) + requireCache[module] = chunk() + end + return requireCache[module] end return env @@ -122,7 +139,7 @@ TestNode.__index = TestNode 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) +function TestNode.new(parent, plan, phrase, nodeType, nodeModifier) nodeModifier = nodeModifier or TestEnum.NodeModifier.None local node = { @@ -135,7 +152,8 @@ function TestNode.new(plan, phrase, nodeType, nodeModifier) parent = nil, } - node.environment = newEnvironment(node, nil, plan.extraEnvironment) + local parentEnvironment = parent and parent.Environment + node.environment = newEnvironment(parentEnvironment, node, plan.extraEnvironment) return setmetatable(node, TestNode) end @@ -161,7 +179,7 @@ function TestNode:addChild(phrase, nodeType, nodeModifier) local childName = self:getFullName() .. " " .. phrase nodeModifier = getModifier(childName, self.plan.testNamePattern, nodeModifier) - local child = TestNode.new(self.plan, phrase, nodeType, nodeModifier) + local child = self:new(self.plan, phrase, nodeType, nodeModifier) child.parent = self table.insert(self.children, child) return child @@ -227,7 +245,7 @@ end ]] function TestPlan:addChild(phrase, nodeType, nodeModifier) nodeModifier = getModifier(phrase, self.testNamePattern, nodeModifier) - local child = TestNode.new(self, phrase, nodeType, nodeModifier) + local child = TestNode.new(nil, self, phrase, nodeType, nodeModifier) table.insert(self.children, child) return child end From dac8a70e1e7666fb302344806fddf277dddf14e1 Mon Sep 17 00:00:00 2001 From: MagiMaster Date: Thu, 16 Jul 2020 13:26:46 -0700 Subject: [PATCH 23/28] Remove hooks from session tree --- CHANGELOG.md | 1 + src/TestRunner.lua | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4742189..ab3d76d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # TestEZ Changelog ## Unreleased Changes +* Remove the lifecycle hooks from the session tree. This prevents the `[?]` spam from the reporter not recognizing these nodes. ## 0.3.1 (2020-06-22) * Further simplify `beforeAll` handling. diff --git a/src/TestRunner.lua b/src/TestRunner.lua index c3f4467..33018d8 100644 --- a/src/TestRunner.lua +++ b/src/TestRunner.lua @@ -134,9 +134,8 @@ function TestRunner.runPlanNode(session, planNode, lifecycleHooks) if not halt then for _, childPlanNode in ipairs(planNode.children) do - session:pushNode(childPlanNode) - if childPlanNode.type == TestEnum.NodeType.It then + session:pushNode(childPlanNode) if session:shouldSkip() then session:setSkipped() else @@ -148,7 +147,9 @@ function TestRunner.runPlanNode(session, planNode, lifecycleHooks) 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? @@ -158,9 +159,8 @@ function TestRunner.runPlanNode(session, planNode, lifecycleHooks) else session:setStatusFromChildren() end + session:popNode() end - - session:popNode() end end From cd847ee2b698718258bb54b53fb41bbf3cd0df92 Mon Sep 17 00:00:00 2001 From: MagiMaster Date: Thu, 16 Jul 2020 18:42:21 -0700 Subject: [PATCH 24/28] Stringify errors before processing --- CHANGELOG.md | 3 +++ src/TestPlan.lua | 4 +++- src/TestRunner.lua | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4742189..2ff6cca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # TestEZ Changelog ## Unreleased Changes +* Change the way errors are collected to call tostring on them before further processing. + * Luau allows non-string errors, but not concatenating non-strings or passing non-strings to `debug.traceback` as a message, so TestRunner needs to do that step. This is a temporary fix as the better solution would be to retain the error in object form for as long as possible to give the reporter more to work with. + * This also makes a slight change to what's in the traceback to eliminate the unnecessary line mentioning the error collection function. ## 0.3.1 (2020-06-22) * Further simplify `beforeAll` handling. diff --git a/src/TestPlan.lua b/src/TestPlan.lua index 6529ec2..f3e6083 100644 --- a/src/TestPlan.lua +++ b/src/TestPlan.lua @@ -188,7 +188,9 @@ function TestNode:expand() end setfenv(self.callback, callbackEnv) - local success, result = xpcall(self.callback, debug.traceback) + local success, result = xpcall(self.callback, function(message) + return debug.traceback(tostring(message), 2) + end) if not success then self.loadError = result diff --git a/src/TestRunner.lua b/src/TestRunner.lua index c3f4467..488114a 100644 --- a/src/TestRunner.lua +++ b/src/TestRunner.lua @@ -67,7 +67,7 @@ function TestRunner.runPlanNode(session, planNode, lifecycleHooks) end success = false - errorMessage = messagePrefix .. message .. "\n" .. debug.traceback() + errorMessage = messagePrefix .. debug.traceback(tostring(message)) end local context = session:getContext() @@ -77,7 +77,7 @@ function TestRunner.runPlanNode(session, planNode, lifecycleHooks) callback(context) end, function(message) - return messagePrefix .. message .. "\n" .. debug.traceback() + return messagePrefix .. debug.traceback(tostring(message)) end ) From 4e9d0c6bfee7146653fc1aafd231f503f2cd91d4 Mon Sep 17 00:00:00 2001 From: MagiMaster Date: Thu, 16 Jul 2020 18:45:28 -0700 Subject: [PATCH 25/28] Add missing 2s --- src/TestRunner.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/TestRunner.lua b/src/TestRunner.lua index 488114a..16df8e8 100644 --- a/src/TestRunner.lua +++ b/src/TestRunner.lua @@ -67,7 +67,7 @@ function TestRunner.runPlanNode(session, planNode, lifecycleHooks) end success = false - errorMessage = messagePrefix .. debug.traceback(tostring(message)) + errorMessage = messagePrefix .. debug.traceback(tostring(message), 2) end local context = session:getContext() @@ -77,7 +77,7 @@ function TestRunner.runPlanNode(session, planNode, lifecycleHooks) callback(context) end, function(message) - return messagePrefix .. debug.traceback(tostring(message)) + return messagePrefix .. debug.traceback(tostring(message), 2) end ) From 5c7c912dedb07c2ad5bb59bca6032dfcd9d59b81 Mon Sep 17 00:00:00 2001 From: MagiMaster Date: Fri, 17 Jul 2020 11:50:49 -0700 Subject: [PATCH 26/28] Deprecate extraEnvironment --- CHANGELOG.md | 1 + src/TestPlan.lua | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4742189..15569c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # TestEZ Changelog ## Unreleased Changes +* Add a deprecation notice for uses of extraEnvironment. ## 0.3.1 (2020-06-22) * Further simplify `beforeAll` handling. diff --git a/src/TestPlan.lua b/src/TestPlan.lua index 6529ec2..2f599ea 100644 --- a/src/TestPlan.lua +++ b/src/TestPlan.lua @@ -202,6 +202,14 @@ TestPlan.__index = TestPlan Create a new, empty TestPlan. ]] function TestPlan.new(testNamePattern, extraEnvironment) + if extraEnvironment and next(extraEnvironment) then + warn( + "extraEnvironment is deprecated and will be removed in the near future. " .. + "Please use an init.spec.lua file and the test context to pass in anything " .. + "that is currently in extraEnvironment." + ) + end + local plan = { children = {}, testNamePattern = testNamePattern, From f234a5e46bfaf1cf84a3f153ff06804139ccd4e3 Mon Sep 17 00:00:00 2001 From: MagiMaster Date: Wed, 22 Jul 2020 13:29:15 -0700 Subject: [PATCH 27/28] Use a single require cache for each plan tree for now --- CHANGELOG.md | 2 +- src/TestPlan.lua | 10 ++++------ tests/expandOrder.lua | 41 ++++++++++++++++++++--------------------- 3 files changed, 25 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b8e0cc..c82db6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * Luau allows non-string errors, but not concatenating non-strings or passing non-strings to `debug.traceback` as a message, so TestRunner needs to do that step. This is a temporary fix as the better solution would be to retain the error in object form for as long as possible to give the reporter more to work with. * This also makes a slight change to what's in the traceback to eliminate the unnecessary line mentioning the error collection function. * Add a deprecation notice for uses of extraEnvironment. +* Guarantee that `init.spec.lua` will run before any `it` or `describe` blocks in the folder under it. ## 0.3.1 (2020-06-22) * Further simplify `beforeAll` handling. @@ -21,7 +22,6 @@ * Remove the `step` alias for `it` since that's meant for use with `try`. * Remove the `include` global function. * Remove `HACK_NO_XPCALL`. With recent changes to the definition of xpcall, this is no longer necessary. Since people are still using it, it will now print out a warning asking them to delete that call instead. -* Guarantee that `init.spec.lua` will run before any `it` or `describe` blocks in the folder under it. * Major changes to the internals of test planning. * The major visible change is that `describe` and `it` blocks with duplicate descriptions will now not overwrite the earlier copies of those nodes. * Duplicate `it` nodes within one `describe` will raise an error. diff --git a/src/TestPlan.lua b/src/TestPlan.lua index 1e8aae1..131397d 100644 --- a/src/TestPlan.lua +++ b/src/TestPlan.lua @@ -9,21 +9,19 @@ local TestEnum = require(script.Parent.TestEnum) local Expectation = require(script.Parent.Expectation) local REQUIRE_CACHE_KEY = {} +local requireCache = {} local function newEnvironment(parentEnvironment, currentNode, extraEnvironment) local env = {} - local requireMeta = {} - if parentEnvironment then - requireMeta = {__index = parentEnvironment[REQUIRE_CACHE_KEY]} - end - - local requireCache = setmetatable({}, requireMeta) + -- All nodes in a tree currently share a cache + local requireCache = parentEnvironment and parentEnvironment[REQUIRE_CACHE_KEY] or {} env[REQUIRE_CACHE_KEY] = requireCache if extraEnvironment then if type(extraEnvironment) ~= "table" then error(("Bad argument #3 to newEnvironment. Expected table, got %s"):format( + typeof(extraEnvironment)), 2) end diff --git a/tests/expandOrder.lua b/tests/expandOrder.lua index 2dbf01e..e502c6f 100644 --- a/tests/expandOrder.lua +++ b/tests/expandOrder.lua @@ -1,3 +1,5 @@ +-- luacheck: globals it beforeAll afterAll + local TestEZ = require(script.Parent.Parent.TestEZ) return { @@ -9,13 +11,15 @@ return { method = function() assert(initialized, "init.spec was not called before bar.spec") end, - path = {'bar.spec', 'foo'} + path = {'bar.spec', 'foo'}, + pathStringForSorting = "foo bar.spec", }, { method = function() initialized = true end, - path = {'foo'} + path = {'foo'}, + pathStringForSorting = "foo", }, }) @@ -25,60 +29,55 @@ return { ["init.spec.lua afterAll can correctly undo changes"] = function() local initialized = false - -- luacheck: globals it local plan = TestEZ.TestPlanner.createPlan({ { method = function() - print("Ad") it("A", function() - print("Ai") assert(not initialized, "initialized was true in foo/a.spec") end) end, - path = {'a.spec', 'foo'} + path = {'a.spec', 'foo'}, + pathStringForSorting = "foo a.spec", }, { method = function() - print("Bd") it("B", function() - print("Bi") assert(initialized, "initialized was false in foo/bar/b.spec") end) end, - path = {'b.spec', 'bar', 'foo'} + path = {'b.spec', 'bar', 'foo'}, + pathStringForSorting = "foo bar b.spec", }, { method = function() - print("Init") - initialized = true + beforeAll(function() + initialized = true + end) - -- luacheck: globals afterAll afterAll(function() - print("After") initialized = false end) end, - path = {'bar', 'foo'} + path = {'bar', 'foo'}, + pathStringForSorting = "foo bar", }, { method = function() - print("Cd") it("C", function() - print("Ci") assert(initialized, "initialized was false in foo/bar/c.spec") end) end, - path = {'c.spec', 'bar', 'foo'} + path = {'c.spec', 'bar', 'foo'}, + pathStringForSorting = "foor bar c.spec", }, { method = function() - print("Dd") it("D", function() - print("Di") assert(not initialized, "initialized was true in foo/d.spec") end) end, - path = {'d.spec', 'foo'} + path = {'d.spec', 'foo'}, + pathStringForSorting = "foo d.spec", }, }) @@ -86,4 +85,4 @@ return { assert(#results.errors == 0, "init test failed:\n" .. table.concat(results.errors, "\n")) end, -} \ No newline at end of file +} From afb973ea976eb5d1d701021dc4888c548a188b1c Mon Sep 17 00:00:00 2001 From: MagiMaster Date: Wed, 22 Jul 2020 13:52:18 -0700 Subject: [PATCH 28/28] Removed unused variable --- src/TestPlan.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/src/TestPlan.lua b/src/TestPlan.lua index 131397d..04acda3 100644 --- a/src/TestPlan.lua +++ b/src/TestPlan.lua @@ -9,7 +9,6 @@ local TestEnum = require(script.Parent.TestEnum) local Expectation = require(script.Parent.Expectation) local REQUIRE_CACHE_KEY = {} -local requireCache = {} local function newEnvironment(parentEnvironment, currentNode, extraEnvironment) local env = {}