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