mirror of
				https://github.com/imezx/Warp.git
				synced 2025-10-24 23:39:18 +00:00 
			
		
		
		
	v1.0.8
This commit is contained in:
		
							parent
							
								
									ae1754d616
								
							
						
					
					
						commit
						4b568356c8
					
				
					 26 changed files with 2101 additions and 13 deletions
				
			
		
							
								
								
									
										26
									
								
								TestEZ/Context.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								TestEZ/Context.lua
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										311
									
								
								TestEZ/Expectation.lua
									
									
									
									
									
										Normal 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 | ||||||
							
								
								
									
										38
									
								
								TestEZ/ExpectationContext.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								TestEZ/ExpectationContext.lua
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										89
									
								
								TestEZ/LifecycleHooks.lua
									
									
									
									
									
										Normal 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 | ||||||
							
								
								
									
										102
									
								
								TestEZ/Reporters/TeamCityReporter.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								TestEZ/Reporters/TeamCityReporter.lua
									
									
									
									
									
										Normal 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 | ||||||
							
								
								
									
										106
									
								
								TestEZ/Reporters/TextReporter.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								TestEZ/Reporters/TextReporter.lua
									
									
									
									
									
										Normal 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 | ||||||
							
								
								
									
										97
									
								
								TestEZ/Reporters/TextReporterQuiet.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								TestEZ/Reporters/TextReporterQuiet.lua
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										147
									
								
								TestEZ/TestBootstrap.lua
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										28
									
								
								TestEZ/TestEnum.lua
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										304
									
								
								TestEZ/TestPlan.lua
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										40
									
								
								TestEZ/TestPlanner.lua
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										112
									
								
								TestEZ/TestResults.lua
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										188
									
								
								TestEZ/TestRunner.lua
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										243
									
								
								TestEZ/TestSession.lua
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										42
									
								
								TestEZ/init.lua
									
									
									
									
									
										Normal 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
									
									
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Warp.rbxm
									
									
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										3
									
								
								runTests.server.luau
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								runTests.server.luau
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | ||||||
|  | require(script.Parent.TestEZ).TestBootstrap:run({ | ||||||
|  | 	game:GetService("ServerScriptService").Test | ||||||
|  | }) | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| --!strict | --!strict | ||||||
| --!native | --!native | ||||||
|  | --!optimize 2 | ||||||
| local Client = {} | local Client = {} | ||||||
| Client.__index = Client | Client.__index = Client | ||||||
| 
 | 
 | ||||||
|  | @ -10,12 +11,17 @@ local ClientProcess = require(script.Parent.ClientProcess) | ||||||
| local Assert = require(Util.Assert) | local Assert = require(Util.Assert) | ||||||
| local Key = require(Util.Key) | local Key = require(Util.Key) | ||||||
| local Serdes = require(Util.Serdes) | local Serdes = require(Util.Serdes) | ||||||
|  | local Buffer = require(Util.Buffer) | ||||||
| 
 | 
 | ||||||
| function Client.new(Identifier: string) | function Client.new(Identifier: string) | ||||||
| 	local self = setmetatable({}, Client) | 	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.fn = {} | ||||||
|  | 	self.IsConnected = false | ||||||
| 	ClientProcess.add(self.id, Identifier) | 	ClientProcess.add(self.id, Identifier) | ||||||
|  | 	self._buffer:remove() | ||||||
| 	return self | 	return self | ||||||
| end | end | ||||||
| 
 | 
 | ||||||
|  | @ -30,6 +36,7 @@ end | ||||||
| function Client:Connect(callback: (args: any) -> ()): string | function Client:Connect(callback: (args: any) -> ()): string | ||||||
| 	local key = tostring(Key()) | 	local key = tostring(Key()) | ||||||
| 	table.insert(self.fn, key) | 	table.insert(self.fn, key) | ||||||
|  | 	self.IsConnected = #self.fn > 0 | ||||||
| 	ClientProcess.addCallback(self.id, key, callback) | 	ClientProcess.addCallback(self.id, key, callback) | ||||||
| 	return key | 	return key | ||||||
| end | end | ||||||
|  | @ -37,6 +44,7 @@ end | ||||||
| function Client:Once(callback: (args: any) -> ()): string | function Client:Once(callback: (args: any) -> ()): string | ||||||
| 	local key = tostring(Key()) | 	local key = tostring(Key()) | ||||||
| 	table.insert(self.fn, key) | 	table.insert(self.fn, key) | ||||||
|  | 	self.IsConnected = #self.fn > 0 | ||||||
| 	ClientProcess.addCallback(self.id, key, function(...) | 	ClientProcess.addCallback(self.id, key, function(...) | ||||||
| 		self:Disconnect(key) | 		self:Disconnect(key) | ||||||
| 		task.spawn(callback, ...) | 		task.spawn(callback, ...) | ||||||
|  | @ -53,15 +61,17 @@ function Client:Wait() | ||||||
| end | end | ||||||
| 
 | 
 | ||||||
| function Client:DisconnectAll() | function Client:DisconnectAll() | ||||||
| 	for idx, key: string in self.fn do | 	for _, key: string in self.fn do | ||||||
| 		ClientProcess.removeCallback(self.id, key) | 		self:Disconnect(key) | ||||||
| 		table.remove(self.fn, idx) |  | ||||||
| 	end | 	end | ||||||
| end | end | ||||||
| 
 | 
 | ||||||
| function Client:Disconnect(key: string) | function Client:Disconnect(key: string): boolean | ||||||
| 	Assert(typeof(key) == "string", "Key must be a string type.") | 	Assert(typeof(key) == "string", "Key must be a string type.") | ||||||
| 	ClientProcess.removeCallback(self.id, key) | 	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 | end | ||||||
| 
 | 
 | ||||||
| function Client:Destroy() | function Client:Destroy() | ||||||
|  |  | ||||||
|  | @ -20,6 +20,7 @@ function Server.new(Identifier: string, rateLimit: Type.rateLimitArg?) | ||||||
| 	self._buffer:writeu8(Serdes(Identifier)) | 	self._buffer:writeu8(Serdes(Identifier)) | ||||||
| 	self.id = Buffer.convert(self._buffer:build()) | 	self.id = Buffer.convert(self._buffer:build()) | ||||||
| 	self.fn = {} | 	self.fn = {} | ||||||
|  | 	self.IsConnected = false | ||||||
| 	ServerProcess.add(self.id, Identifier, rateLimit or { maxEntrance = 200, interval = 2 }) | 	ServerProcess.add(self.id, Identifier, rateLimit or { maxEntrance = 200, interval = 2 }) | ||||||
| 	self._buffer:remove() | 	self._buffer:remove() | ||||||
| 	return self | 	return self | ||||||
|  | @ -50,12 +51,14 @@ function Server:Connect(callback: (plyer: Player, args: any) -> ()): string | ||||||
| 	local key = tostring(Key()) | 	local key = tostring(Key()) | ||||||
| 	table.insert(self.fn, key) | 	table.insert(self.fn, key) | ||||||
| 	ServerProcess.addCallback(self.id, key, callback) | 	ServerProcess.addCallback(self.id, key, callback) | ||||||
|  | 	self.IsConnected = #self.fn > 0 | ||||||
| 	return key | 	return key | ||||||
| end | end | ||||||
| 
 | 
 | ||||||
| function Server:Once(callback: (plyer: Player, args: any) -> ()): string | function Server:Once(callback: (plyer: Player, args: any) -> ()): string | ||||||
| 	local key = tostring(Key()) | 	local key = tostring(Key()) | ||||||
| 	table.insert(self.fn, key) | 	table.insert(self.fn, key) | ||||||
|  | 	self.IsConnected = #self.fn > 0 | ||||||
| 	ServerProcess.addCallback(self.id, key, function(...) | 	ServerProcess.addCallback(self.id, key, function(...) | ||||||
| 		self:Disconnect(key) | 		self:Disconnect(key) | ||||||
| 		task.spawn(callback, ...) | 		task.spawn(callback, ...) | ||||||
|  | @ -72,15 +75,17 @@ function Server:Wait() | ||||||
| end | end | ||||||
| 
 | 
 | ||||||
| function Server:DisconnectAll() | function Server:DisconnectAll() | ||||||
| 	for idx, key: string in self.fn do | 	for _, key: string in self.fn do | ||||||
| 		ServerProcess.removeCallback(self.id, key) | 		self:Disconnect(key) | ||||||
| 		table.remove(self.fn, idx) |  | ||||||
| 	end | 	end | ||||||
| end | end | ||||||
| 
 | 
 | ||||||
| function Server:Disconnect(key: string) | function Server:Disconnect(key: string): boolean | ||||||
| 	Assert(typeof(key) == "string", "Key must be a string type.") | 	Assert(typeof(key) == "string", "Key must be a string type.") | ||||||
| 	ServerProcess.removeCallback(self.id, key) | 	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 | end | ||||||
| 
 | 
 | ||||||
| function Server:Destroy() | function Server:Destroy() | ||||||
|  |  | ||||||
|  | @ -132,7 +132,6 @@ function ServerProcess.add(Identifier: string, originId: string, ratelimit: Type | ||||||
| end | end | ||||||
| 
 | 
 | ||||||
| function ServerProcess.addCallback(Identifier: string, key: string, callback) | function ServerProcess.addCallback(Identifier: string, key: string, callback) | ||||||
| 	print(serverCallback, Identifier) |  | ||||||
| 	serverCallback[Identifier][key] = callback | 	serverCallback[Identifier][key] = callback | ||||||
| end | end | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -73,7 +73,7 @@ end | ||||||
| 
 | 
 | ||||||
| function Signal:InvokeTo(signal: string, key: string, ...: any): () | function Signal:InvokeTo(signal: string, key: string, ...: any): () | ||||||
| 	if not Signals[signal] then return end | 	if not Signals[signal] then return end | ||||||
| 	return Signal.Invoke(Signals[signal], ...) | 	return Signal.Invoke(Signals[signal], key, ...) | ||||||
| end | end | ||||||
| 
 | 
 | ||||||
| function Signal:Destroy(): () | function Signal:Destroy(): () | ||||||
|  |  | ||||||
|  | @ -1,4 +1,5 @@ | ||||||
| --!strict | --!strict | ||||||
|  | --!optimize 2 | ||||||
| local Index = {} | local Index = {} | ||||||
| 
 | 
 | ||||||
| local RunService = game:GetService("RunService") | local RunService = game:GetService("RunService") | ||||||
|  |  | ||||||
|  | @ -1,7 +1,8 @@ | ||||||
| -- Warp Library (@Eternity_Devs) | -- Warp Library (@Eternity_Devs) | ||||||
| -- version 1.0.6 | -- version 1.0.8 | ||||||
| --!strict | --!strict | ||||||
| --!native | --!native | ||||||
|  | --!optimize 2 | ||||||
| local Index = require(script.Index) | local Index = require(script.Index) | ||||||
| 
 | 
 | ||||||
| return { | return { | ||||||
|  |  | ||||||
							
								
								
									
										30
									
								
								test.project.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								test.project.json
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										166
									
								
								test/init.spec.luau
									
									
									
									
									
										Normal 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 | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| [package] | [package] | ||||||
| name = "imezx/warp" | name = "imezx/warp" | ||||||
| version = "1.0.7" | version = "1.0.8" | ||||||
| registry = "https://github.com/UpliftGames/wally-index" | registry = "https://github.com/UpliftGames/wally-index" | ||||||
| realm = "shared" | realm = "shared" | ||||||
| license = "MIT" | license = "MIT" | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue