Skip to content

Commit

Permalink
v1.0.8
Browse files Browse the repository at this point in the history
  • Loading branch information
imezx committed Mar 14, 2024
1 parent ae1754d commit 4b56835
Show file tree
Hide file tree
Showing 26 changed files with 2,101 additions and 13 deletions.
26 changes: 26 additions & 0 deletions TestEZ/Context.lua
Original file line number Diff line number Diff line change
@@ -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 changes: 311 additions & 0 deletions TestEZ/Expectation.lua
Original file line number Diff line number Diff line change
@@ -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
38 changes: 38 additions & 0 deletions TestEZ/ExpectationContext.lua
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 4b56835

Please sign in to comment.