This commit is contained in:
EternityDev 2024-03-14 11:58:08 +07:00
parent ae1754d616
commit 4b568356c8
26 changed files with 2101 additions and 13 deletions

26
TestEZ/Context.lua Normal file
View file

@ -0,0 +1,26 @@
--[[
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

311
TestEZ/Expectation.lua Normal file
View file

@ -0,0 +1,311 @@
--[[
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

View file

@ -0,0 +1,38 @@
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

89
TestEZ/LifecycleHooks.lua Normal file
View file

@ -0,0 +1,89 @@
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

View file

@ -0,0 +1,102 @@
local TestService = game:GetService("TestService")
local TestEnum = require(script.Parent.Parent.TestEnum)
local TeamCityReporter = {}
local function teamCityEscape(str)
str = string.gsub(str, "([]|'[])","|%1")
str = string.gsub(str, "\r", "|r")
str = string.gsub(str, "\n", "|n")
return str
end
local function teamCityEnterSuite(suiteName)
return string.format("##teamcity[testSuiteStarted name='%s']", teamCityEscape(suiteName))
end
local function teamCityLeaveSuite(suiteName)
return string.format("##teamcity[testSuiteFinished name='%s']", teamCityEscape(suiteName))
end
local function teamCityEnterCase(caseName)
return string.format("##teamcity[testStarted name='%s']", teamCityEscape(caseName))
end
local function teamCityLeaveCase(caseName)
return string.format("##teamcity[testFinished name='%s']", teamCityEscape(caseName))
end
local function teamCityFailCase(caseName, errorMessage)
return string.format("##teamcity[testFailed name='%s' message='%s']",
teamCityEscape(caseName), teamCityEscape(errorMessage))
end
local function reportNode(node, buffer, level)
buffer = buffer or {}
level = level or 0
if node.status == TestEnum.TestStatus.Skipped then
return buffer
end
if node.planNode.type == TestEnum.NodeType.Describe then
table.insert(buffer, teamCityEnterSuite(node.planNode.phrase))
for _, child in ipairs(node.children) do
reportNode(child, buffer, level + 1)
end
table.insert(buffer, teamCityLeaveSuite(node.planNode.phrase))
else
table.insert(buffer, teamCityEnterCase(node.planNode.phrase))
if node.status == TestEnum.TestStatus.Failure then
table.insert(buffer, teamCityFailCase(node.planNode.phrase, table.concat(node.errors,"\n")))
end
table.insert(buffer, teamCityLeaveCase(node.planNode.phrase))
end
end
local function reportRoot(node)
local buffer = {}
for _, child in ipairs(node.children) do
reportNode(child, buffer, 0)
end
return buffer
end
local function report(root)
local buffer = reportRoot(root)
return table.concat(buffer, "\n")
end
function TeamCityReporter.report(results)
local resultBuffer = {
"Test results:",
report(results),
("%d passed, %d failed, %d skipped"):format(
results.successCount,
results.failureCount,
results.skippedCount
)
}
print(table.concat(resultBuffer, "\n"))
if results.failureCount > 0 then
print(("%d test nodes reported failures."):format(results.failureCount))
end
if #results.errors > 0 then
print("Errors reported by tests:")
print("")
for _, message in ipairs(results.errors) do
TestService:Error(message)
-- Insert a blank line after each error
print("")
end
end
end
return TeamCityReporter

View file

@ -0,0 +1,106 @@
--[[
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

View file

@ -0,0 +1,97 @@
--[[
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

147
TestEZ/TestBootstrap.lua Normal file
View file

@ -0,0 +1,147 @@
--[[
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

28
TestEZ/TestEnum.lua Normal file
View file

@ -0,0 +1,28 @@
--[[
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

304
TestEZ/TestPlan.lua Normal file
View file

@ -0,0 +1,304 @@
--[[
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

40
TestEZ/TestPlanner.lua Normal file
View file

@ -0,0 +1,40 @@
--[[
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

112
TestEZ/TestResults.lua Normal file
View file

@ -0,0 +1,112 @@
--[[
Represents a tree of test results.
Each node in the tree corresponds directly to a node in a corresponding
TestPlan, accessible via the 'planNode' field.
TestResults objects are produced by TestRunner using TestSession as state.
]]
local TestEnum = require(script.Parent.TestEnum)
local STATUS_SYMBOLS = {
[TestEnum.TestStatus.Success] = "+",
[TestEnum.TestStatus.Failure] = "-",
[TestEnum.TestStatus.Skipped] = "~"
}
local TestResults = {}
TestResults.__index = TestResults
--[[
Create a new TestResults tree that's linked to the given TestPlan.
]]
function TestResults.new(plan)
local self = {
successCount = 0,
failureCount = 0,
skippedCount = 0,
planNode = plan,
children = {},
errors = {}
}
setmetatable(self, TestResults)
return self
end
--[[
Create a new result node that can be inserted into a TestResult tree.
]]
function TestResults.createNode(planNode)
local node = {
planNode = planNode,
children = {},
errors = {},
status = nil
}
return node
end
--[[
Visit all test result nodes, depth-first.
]]
function TestResults:visitAllNodes(callback, root)
root = root or self
for _, child in ipairs(root.children) do
callback(child)
self:visitAllNodes(callback, child)
end
end
--[[
Creates a debug visualization of the test results.
]]
function TestResults:visualize(root, level)
root = root or self
level = level or 0
local buffer = {}
for _, child in ipairs(root.children) do
if child.planNode.type == TestEnum.NodeType.It then
local symbol = STATUS_SYMBOLS[child.status] or "?"
local str = ("%s[%s] %s"):format(
(" "):rep(3 * level),
symbol,
child.planNode.phrase
)
if child.messages 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

188
TestEZ/TestRunner.lua Normal file
View file

@ -0,0 +1,188 @@
--[[
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

243
TestEZ/TestSession.lua Normal file
View file

@ -0,0 +1,243 @@
--[[
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

42
TestEZ/init.lua Normal file
View file

@ -0,0 +1,42 @@
--!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

BIN
Warp.rbxm

Binary file not shown.

3
runTests.server.luau Normal file
View file

@ -0,0 +1,3 @@
require(script.Parent.TestEZ).TestBootstrap:run({
game:GetService("ServerScriptService").Test
})

View file

@ -1,5 +1,6 @@
--!strict
--!native
--!optimize 2
local Client = {}
Client.__index = Client
@ -10,12 +11,17 @@ local ClientProcess = require(script.Parent.ClientProcess)
local Assert = require(Util.Assert)
local Key = require(Util.Key)
local Serdes = require(Util.Serdes)
local Buffer = require(Util.Buffer)
function Client.new(Identifier: string)
local self = setmetatable({}, Client)
self.id = Serdes(Identifier)
self._buffer = Buffer.new()
self._buffer:writeu8(Serdes(Identifier))
self.id = Buffer.convert(self._buffer:build())
self.fn = {}
self.IsConnected = false
ClientProcess.add(self.id, Identifier)
self._buffer:remove()
return self
end
@ -30,6 +36,7 @@ end
function Client:Connect(callback: (args: any) -> ()): string
local key = tostring(Key())
table.insert(self.fn, key)
self.IsConnected = #self.fn > 0
ClientProcess.addCallback(self.id, key, callback)
return key
end
@ -37,6 +44,7 @@ end
function Client:Once(callback: (args: any) -> ()): string
local key = tostring(Key())
table.insert(self.fn, key)
self.IsConnected = #self.fn > 0
ClientProcess.addCallback(self.id, key, function(...)
self:Disconnect(key)
task.spawn(callback, ...)
@ -53,15 +61,17 @@ function Client:Wait()
end
function Client:DisconnectAll()
for idx, key: string in self.fn do
ClientProcess.removeCallback(self.id, key)
table.remove(self.fn, idx)
for _, key: string in self.fn do
self:Disconnect(key)
end
end
function Client:Disconnect(key: string)
function Client:Disconnect(key: string): boolean
Assert(typeof(key) == "string", "Key must be a string type.")
ClientProcess.removeCallback(self.id, key)
table.remove(self.fn, table.find(self.fn, key))
self.IsConnected = #self.fn > 0
return table.find(self.fn, key) == nil
end
function Client:Destroy()

View file

@ -20,6 +20,7 @@ function Server.new(Identifier: string, rateLimit: Type.rateLimitArg?)
self._buffer:writeu8(Serdes(Identifier))
self.id = Buffer.convert(self._buffer:build())
self.fn = {}
self.IsConnected = false
ServerProcess.add(self.id, Identifier, rateLimit or { maxEntrance = 200, interval = 2 })
self._buffer:remove()
return self
@ -50,12 +51,14 @@ function Server:Connect(callback: (plyer: Player, args: any) -> ()): string
local key = tostring(Key())
table.insert(self.fn, key)
ServerProcess.addCallback(self.id, key, callback)
self.IsConnected = #self.fn > 0
return key
end
function Server:Once(callback: (plyer: Player, args: any) -> ()): string
local key = tostring(Key())
table.insert(self.fn, key)
self.IsConnected = #self.fn > 0
ServerProcess.addCallback(self.id, key, function(...)
self:Disconnect(key)
task.spawn(callback, ...)
@ -72,15 +75,17 @@ function Server:Wait()
end
function Server:DisconnectAll()
for idx, key: string in self.fn do
ServerProcess.removeCallback(self.id, key)
table.remove(self.fn, idx)
for _, key: string in self.fn do
self:Disconnect(key)
end
end
function Server:Disconnect(key: string)
function Server:Disconnect(key: string): boolean
Assert(typeof(key) == "string", "Key must be a string type.")
ServerProcess.removeCallback(self.id, key)
table.remove(self.fn, table.find(self.fn, key))
self.IsConnected = #self.fn > 0
return table.find(self.fn, key) == nil
end
function Server:Destroy()

View file

@ -132,7 +132,6 @@ function ServerProcess.add(Identifier: string, originId: string, ratelimit: Type
end
function ServerProcess.addCallback(Identifier: string, key: string, callback)
print(serverCallback, Identifier)
serverCallback[Identifier][key] = callback
end

View file

@ -73,7 +73,7 @@ end
function Signal:InvokeTo(signal: string, key: string, ...: any): ()
if not Signals[signal] then return end
return Signal.Invoke(Signals[signal], ...)
return Signal.Invoke(Signals[signal], key, ...)
end
function Signal:Destroy(): ()

View file

@ -1,4 +1,5 @@
--!strict
--!optimize 2
local Index = {}
local RunService = game:GetService("RunService")

View file

@ -1,7 +1,8 @@
-- Warp Library (@Eternity_Devs)
-- version 1.0.6
-- version 1.0.8
--!strict
--!native
--!optimize 2
local Index = require(script.Index)
return {

30
test.project.json Normal file
View file

@ -0,0 +1,30 @@
{
"name": "warp-test",
"tree": {
"$className": "DataModel",
"ServerScriptService": {
"$className": "ServerScriptService",
"Test": {
"$path": "test"
}
},
"ReplicatedStorage": {
"$className": "ReplicatedStorage",
"Warp": {
"$path": "src"
}
},
"TestService": {
"$className": "TestService",
"$properties": {
"ExecuteWithStudioRun": true
},
"TestEZ": {
"$path": "TestEZ"
},
"run": {
"$path": "runTests.server.luau"
}
}
}
}

166
test/init.spec.luau Normal file
View file

@ -0,0 +1,166 @@
return function()
local Warp = require(game:GetService("ReplicatedStorage").Warp)
describe("Warp.Server", function()
it("should be able to create a new server event", function()
local test = Warp.Server("Test")
expect(test).to.be.ok()
end)
it("should be able to create a new server event with ratelimit configuration", function()
local test = Warp.Server("Test", {
maxEntrance = 10,
interval = 1,
})
expect(test).to.be.ok()
end)
end)
describe("Warp.fromServerArray", function()
it("should be able to create a new server event with arrays", function()
local test = Warp.fromServerArray({
"Test1",
"Test2",
})
expect(test).to.be.ok()
expect(test.Test1).to.be.ok()
expect(test.Test2).to.be.ok()
end)
it("should be able to create a new server event with arrays & ratelimit configuration", function()
local test = Warp.fromServerArray({
"Test1",
"Test2",
["Test3"] = {
maxEntrance = 10,
interval = 0.75,
},
})
expect(test).to.be.ok()
expect(test.Test1).to.be.ok()
expect(test.Test2).to.be.ok()
expect(test.Test3).to.be.ok()
end)
end)
describe("Event.Connect", function()
it("should be able to connect the event", function()
local test = Warp.Server("Test")
test:Connect(function() end)
expect(test.IsConnected).to.be.ok()
end)
end)
describe("Multi Event.Connect", function()
it("should be able to multiple connect the event", function()
local test = Warp.Server("Test")
test:Connect(function() end)
test:Connect(function() end)
expect(test.IsConnected).to.be.ok()
end)
end)
describe("Event.DisconnectAll", function()
it("should be able to disconnect all the event connections", function()
local test = Warp.Server("Test")
test:DisconnectAll()
expect(#test.fn).to.equal(0)
end)
end)
describe("Event.Disconnect", function()
it("should be able to disconnect the event connection", function()
local test = Warp.Server("Test")
local connection = test:Connect(function() end)
expect(test:Disconnect(connection)).to.be.ok()
end)
end)
describe("Warp.Signal", function()
it("should be able to create a new signal", function()
local test = Warp.Signal("Test")
expect(test).to.be.ok()
end)
end)
describe("Warp.fromSignalArray", function()
it("should be able to create a new signal with arrays", function()
local test = Warp.fromSignalArray({
"Test1",
"Test2"
})
expect(test).to.be.ok()
expect(test.Test1).to.be.ok()
expect(test.Test2).to.be.ok()
end)
end)
describe("Signal.Connect", function()
it("should be able to connect the signal", function()
local test = Warp.Signal("Test")
expect(test:Connect(function() end)).to.be.ok()
test:DisconnectAll()
end)
end)
describe("Multi Signal.Connect", function()
it("should be able to multiple connect the signal", function()
local test = Warp.Signal("Test")
expect(test:Connect(function() end)).to.be.ok()
expect(test:Connect(function() end)).to.be.ok()
test:DisconnectAll()
end)
end)
describe("Signal.Fire", function()
it("should be able to fire the signal", function()
local test = Warp.Signal("Test")
test:Once(function(arg)
expect(arg).to.equal("hello world!")
end)
test:Fire("hello world!")
end)
end)
describe("Signal.Invoke", function()
it("should be able to invoke the signal", function()
local test = Warp.Signal("Test")
local connection = test:Connect(function(arg)
if arg ~= "test" then
return
end
return "hello world!"
end)
local receive = test:Invoke(connection, "test")
expect(receive).to.equal("hello world!")
end)
end)
describe("Signal.InvokeTo", function()
it("should be able to invoke to a signal", function()
local test = Warp.Signal("Test")
local test2 = Warp.Signal("Test2")
local connection = test2:Connect(function(arg)
if arg ~= "test" then
return
end
return "hello world!"
end)
local receive = test:InvokeTo("Test2", connection, "test")
expect(receive).to.equal("hello world!")
end)
end)
describe("Signal.Wait", function()
it("should be able to wait for the signal", function()
local test = Warp.Signal("Test")
test:Connect(function() end)
task.spawn(function()
local time = test:Wait()
expect(time).to.be.ok()
expect(time).to.be.a("number")
end)
test:Fire()
end)
end)
end

View file

@ -1,6 +1,6 @@
[package]
name = "imezx/warp"
version = "1.0.7"
version = "1.0.8"
registry = "https://github.com/UpliftGames/wally-index"
realm = "shared"
license = "MIT"