mirror of
				https://github.com/Ukendio/jecs.git
				synced 2025-11-04 02:49:18 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			566 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			Text
		
	
	
		
			Executable file
		
	
	
	
	
			
		
		
	
	
			566 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			Text
		
	
	
		
			Executable file
		
	
	
	
	
--------------------------------------------------------------------------------
 | 
						|
-- testkit.luau
 | 
						|
-- v0.7.3
 | 
						|
-- MIT License
 | 
						|
-- Copyright (c) 2022 centau
 | 
						|
--
 | 
						|
-- Some changes that I have made to this module is to evaluate the tests lazily,
 | 
						|
-- this way only focused tests will actually be ran rather than just focusing their output.
 | 
						|
--
 | 
						|
--------------------------------------------------------------------------------
 | 
						|
 | 
						|
local disable_ansi = false
 | 
						|
 | 
						|
local color = {
 | 
						|
	white_underline = function(s: string): string
 | 
						|
		return if disable_ansi then s else `\27[1;4m{s}\27[0m`
 | 
						|
	end,
 | 
						|
 | 
						|
	white = function(s: string): string
 | 
						|
		return if disable_ansi then s else `\27[37;1m{s}\27[0m`
 | 
						|
	end,
 | 
						|
 | 
						|
	green = function(s: string): string
 | 
						|
		return if disable_ansi then s else `\27[32;1m{s}\27[0m`
 | 
						|
	end,
 | 
						|
 | 
						|
	red = function(s: string): string
 | 
						|
		return if disable_ansi then s else `\27[31;1m{s}\27[0m`
 | 
						|
	end,
 | 
						|
 | 
						|
	yellow = function(s: string): string
 | 
						|
		return if disable_ansi then s else `\27[33;1m{s}\27[0m`
 | 
						|
	end,
 | 
						|
 | 
						|
	red_highlight = function(s: string): string
 | 
						|
		return if disable_ansi then s else `\27[41;1;30m{s}\27[0m`
 | 
						|
	end,
 | 
						|
 | 
						|
	green_highlight = function(s: string): string
 | 
						|
		return if disable_ansi then s else `\27[42;1;30m{s}\27[0m`
 | 
						|
	end,
 | 
						|
 | 
						|
	gray = function(s: string): string
 | 
						|
		return if disable_ansi then s else `\27[38;1m{s}\27[0m`
 | 
						|
	end,
 | 
						|
 | 
						|
	orange = function(s: string): string
 | 
						|
		return if disable_ansi then s else `\27[38;5;208m{s}\27[0m`
 | 
						|
	end,
 | 
						|
}
 | 
						|
 | 
						|
local function convert_units(unit: string, value: number): (number, string)
 | 
						|
	local sign = math.sign(value)
 | 
						|
	value = math.abs(value)
 | 
						|
 | 
						|
	local prefix_colors = {
 | 
						|
		[4] = color.red,
 | 
						|
		[3] = color.red,
 | 
						|
		[2] = color.yellow,
 | 
						|
		[1] = color.yellow,
 | 
						|
		[0] = color.green,
 | 
						|
		[-1] = color.red,
 | 
						|
		[-2] = color.yellow,
 | 
						|
		[-3] = color.green,
 | 
						|
		[-4] = color.red,
 | 
						|
	}
 | 
						|
 | 
						|
	local prefixes = {
 | 
						|
		[4] = "T",
 | 
						|
		[3] = "G",
 | 
						|
		[2] = "M",
 | 
						|
		[1] = "k",
 | 
						|
		[0] = " ",
 | 
						|
		[-1] = "m",
 | 
						|
		[-2] = "u",
 | 
						|
		[-3] = "n",
 | 
						|
		[-4] = "p",
 | 
						|
	}
 | 
						|
 | 
						|
	local order = 0
 | 
						|
 | 
						|
	while value >= 1000 do
 | 
						|
		order += 1
 | 
						|
		value /= 1000
 | 
						|
	end
 | 
						|
 | 
						|
	while value ~= 0 and value < 1 do
 | 
						|
		order -= 1
 | 
						|
		value *= 1000
 | 
						|
	end
 | 
						|
 | 
						|
	if value >= 100 then
 | 
						|
		value = math.floor(value)
 | 
						|
	elseif value >= 10 then
 | 
						|
		value = math.floor(value * 1e1) / 1e1
 | 
						|
	elseif value >= 1 then
 | 
						|
		value = math.floor(value * 1e2) / 1e2
 | 
						|
	end
 | 
						|
 | 
						|
	return value * sign, prefix_colors[order](prefixes[order] .. unit)
 | 
						|
end
 | 
						|
 | 
						|
local WALL = color.gray("│")
 | 
						|
 | 
						|
--------------------------------------------------------------------------------
 | 
						|
-- Testing
 | 
						|
--------------------------------------------------------------------------------
 | 
						|
 | 
						|
type Test = {
 | 
						|
	name: string,
 | 
						|
	case: Case?,
 | 
						|
	cases: { Case },
 | 
						|
	duration: number,
 | 
						|
	error: {
 | 
						|
		message: string,
 | 
						|
		trace: string,
 | 
						|
	}?,
 | 
						|
	focus: boolean,
 | 
						|
	fn: () -> ()
 | 
						|
}
 | 
						|
 | 
						|
type Case = {
 | 
						|
	name: string,
 | 
						|
	result: number,
 | 
						|
	line: number?,
 | 
						|
	focus: boolean,
 | 
						|
}
 | 
						|
 | 
						|
local PASS, FAIL, NONE, ERROR, SKIPPED = 1, 2, 3, 4, 5
 | 
						|
 | 
						|
local check_for_focused = false
 | 
						|
local skip = false
 | 
						|
local test: Test?
 | 
						|
local tests: { Test } = {}
 | 
						|
 | 
						|
local function output_test_result(test: Test)
 | 
						|
	if check_for_focused then
 | 
						|
		local any_focused = test.focus
 | 
						|
		for _, case in test.cases do
 | 
						|
			any_focused = any_focused or case.focus
 | 
						|
		end
 | 
						|
 | 
						|
		if not any_focused then
 | 
						|
			return
 | 
						|
		end
 | 
						|
	end
 | 
						|
 | 
						|
	print(color.white(test.name))
 | 
						|
 | 
						|
	for _, case in test.cases do
 | 
						|
		local status = ({
 | 
						|
			[PASS] = color.green("PASS"),
 | 
						|
			[FAIL] = color.red("FAIL"),
 | 
						|
			[NONE] = color.orange("NONE"),
 | 
						|
			[ERROR] = color.red("FAIL"),
 | 
						|
			[SKIPPED] = color.yellow("SKIP"),
 | 
						|
		})[case.result]
 | 
						|
 | 
						|
		local line = case.result == FAIL and color.red(`{case.line}:`) or ""
 | 
						|
		if check_for_focused and case.focus == false and test.focus == false then
 | 
						|
			continue
 | 
						|
		end
 | 
						|
		print(`{status}{WALL} {line}{color.gray(case.name)}`)
 | 
						|
	end
 | 
						|
 | 
						|
	if test.error then
 | 
						|
		print(color.gray("error: ") .. color.red(test.error.message))
 | 
						|
		print(color.gray("trace: ") .. color.red(test.error.trace))
 | 
						|
	else
 | 
						|
		print()
 | 
						|
	end
 | 
						|
end
 | 
						|
 | 
						|
local function CASE(name: string)
 | 
						|
	skip = false
 | 
						|
	assert(test, "no active test")
 | 
						|
 | 
						|
	local case = {
 | 
						|
		name = name,
 | 
						|
		result = NONE,
 | 
						|
		focus = false,
 | 
						|
	}
 | 
						|
 | 
						|
	test.case = case
 | 
						|
	table.insert(test.cases, case)
 | 
						|
end
 | 
						|
 | 
						|
local function CHECK_EXPECT_ERR(fn, ...)
 | 
						|
	assert(test, "no active test")
 | 
						|
	local case = test.case
 | 
						|
	if not case then
 | 
						|
		CASE("")
 | 
						|
		case = test.case
 | 
						|
	end
 | 
						|
	assert(case, "no active case")
 | 
						|
	if case.result ~= FAIL then
 | 
						|
		local ok, err = pcall(fn, ...)
 | 
						|
		case.result = if ok then FAIL else PASS
 | 
						|
		if skip then
 | 
						|
			case.result = SKIPPED
 | 
						|
		end
 | 
						|
		case.line = debug.info(stack and stack + 1 or 2, "l")
 | 
						|
	end
 | 
						|
end
 | 
						|
 | 
						|
local function CHECK<T>(value: T, stack: number?): T?
 | 
						|
	assert(test, "no active test")
 | 
						|
 | 
						|
	local case = test.case
 | 
						|
 | 
						|
	if not case then
 | 
						|
		CASE("")
 | 
						|
		case = test.case
 | 
						|
	end
 | 
						|
 | 
						|
	assert(case, "no active case")
 | 
						|
 | 
						|
	if case.result ~= FAIL then
 | 
						|
		case.result = value and PASS or FAIL
 | 
						|
		if skip then
 | 
						|
			case.result = SKIPPED
 | 
						|
		end
 | 
						|
		case.line = debug.info(stack and stack + 1 or 2, "l")
 | 
						|
	end
 | 
						|
 | 
						|
	return value
 | 
						|
end
 | 
						|
 | 
						|
local test_focused = false
 | 
						|
 | 
						|
local function TEST(name: string, fn: () -> ())
 | 
						|
	test = {
 | 
						|
		name = name,
 | 
						|
		cases = {},
 | 
						|
		duration = 0,
 | 
						|
		focus = false,
 | 
						|
		fn = fn
 | 
						|
	}
 | 
						|
	
 | 
						|
	local t = test
 | 
						|
 | 
						|
	if check_for_focused and not test_focused then 
 | 
						|
		test.focus = true
 | 
						|
		test_focused = true
 | 
						|
	end
 | 
						|
 | 
						|
	table.insert(tests, t)
 | 
						|
end
 | 
						|
 | 
						|
local function FOCUS()
 | 
						|
	check_for_focused = true
 | 
						|
	test_focused = false
 | 
						|
end
 | 
						|
 | 
						|
local function FINISH(): number
 | 
						|
	local total_cases = 0
 | 
						|
	local passed_cases = 0
 | 
						|
	local passed_focus_cases = 0
 | 
						|
	local total_focus_cases = 0
 | 
						|
	local duration = 0
 | 
						|
 | 
						|
	for _, t in tests do
 | 
						|
		if check_for_focused and not t.focus then
 | 
						|
			continue
 | 
						|
		end
 | 
						|
		test = t
 | 
						|
		local fn = t.fn
 | 
						|
		local start = os.clock()
 | 
						|
		local err
 | 
						|
		local ok = xpcall(fn, function(m: string)
 | 
						|
			err = { message = m, trace = debug.traceback(nil, 2) }
 | 
						|
		end)
 | 
						|
		t.duration = os.clock() - start
 | 
						|
 | 
						|
		if not t.case then
 | 
						|
			CASE("")
 | 
						|
		end
 | 
						|
		assert(t.case, "no active case")
 | 
						|
 | 
						|
		if not ok then
 | 
						|
			t.case.result = ERROR
 | 
						|
			t.error = err
 | 
						|
		end
 | 
						|
		collectgarbage()
 | 
						|
	end
 | 
						|
 | 
						|
	for _, test in tests do
 | 
						|
		duration += test.duration
 | 
						|
		for _, case in test.cases do
 | 
						|
			total_cases += 1
 | 
						|
			if case.focus or test.focus then
 | 
						|
				total_focus_cases += 1
 | 
						|
			end
 | 
						|
			if case.result == PASS or case.result == NONE or case.result == SKIPPED then
 | 
						|
				if case.focus or test.focus then
 | 
						|
					passed_focus_cases += 1
 | 
						|
				end
 | 
						|
				passed_cases += 1
 | 
						|
			else
 | 
						|
				success = false
 | 
						|
			end
 | 
						|
		end
 | 
						|
 | 
						|
		output_test_result(test)
 | 
						|
	end
 | 
						|
 | 
						|
	print(color.gray(string.format(`{passed_cases}/{total_cases} test cases passed in %.3f ms.`, duration * 1e3)))
 | 
						|
	if check_for_focused then
 | 
						|
		print(color.gray(`{passed_focus_cases}/{total_focus_cases} focused test cases passed`))
 | 
						|
	end
 | 
						|
 | 
						|
	local fails = total_cases - passed_cases
 | 
						|
 | 
						|
	print((fails > 0 and color.red or color.green)(`{fails} {fails == 1 and "fail" or "fails"}`))
 | 
						|
 | 
						|
	check_for_focused = false
 | 
						|
	table.clear(tests)
 | 
						|
	return math.clamp(fails, 0, 1)
 | 
						|
end
 | 
						|
 | 
						|
local function SKIP()
 | 
						|
	skip = true
 | 
						|
end
 | 
						|
 | 
						|
--------------------------------------------------------------------------------
 | 
						|
-- Benchmarking
 | 
						|
--------------------------------------------------------------------------------
 | 
						|
 | 
						|
type Bench = {
 | 
						|
	time_start: number?,
 | 
						|
	memory_start: number?,
 | 
						|
	iterations: number?,
 | 
						|
}
 | 
						|
 | 
						|
local bench: Bench?
 | 
						|
 | 
						|
function START(iter: number?): number
 | 
						|
	local n = iter or 1
 | 
						|
	assert(n > 0, "iterations must be greater than 0")
 | 
						|
	assert(bench, "no active benchmark")
 | 
						|
	assert(not bench.time_start, "clock was already started")
 | 
						|
 | 
						|
	bench.iterations = n
 | 
						|
	bench.memory_start = gcinfo()
 | 
						|
	bench.time_start = os.clock()
 | 
						|
	return n
 | 
						|
end
 | 
						|
 | 
						|
local function BENCH(name: string, fn: () -> ())
 | 
						|
	local active = bench
 | 
						|
	assert(not active, "a benchmark is already in progress")
 | 
						|
 | 
						|
	bench = {}
 | 
						|
	assert(bench);
 | 
						|
	(collectgarbage :: any)("collect")
 | 
						|
 | 
						|
	local mem_start = gcinfo()
 | 
						|
	local time_start = os.clock()
 | 
						|
	local err_msg: string?
 | 
						|
 | 
						|
	local success = xpcall(fn, function(m: string)
 | 
						|
		err_msg = m .. debug.traceback(nil, 2)
 | 
						|
	end)
 | 
						|
 | 
						|
	local time_stop = os.clock()
 | 
						|
	local mem_stop = gcinfo()
 | 
						|
 | 
						|
	if not success then
 | 
						|
		print(`{WALL}{color.red("ERROR")}{WALL} {name}`)
 | 
						|
		print(color.gray(err_msg :: string))
 | 
						|
	else
 | 
						|
		time_start = bench.time_start or time_start
 | 
						|
		mem_start = bench.memory_start or mem_start
 | 
						|
 | 
						|
		local n = bench.iterations or 1
 | 
						|
		local d, d_unit = convert_units("s", (time_stop - time_start) / n)
 | 
						|
		local a, a_unit = convert_units("B", math.round((mem_stop - mem_start) / n * 1e3))
 | 
						|
 | 
						|
		local function round(x: number): string
 | 
						|
			return x > 0 and x < 10 and (x - math.floor(x)) > 0 and string.format("%2.1f", x)
 | 
						|
				or string.format("%3.f", x)
 | 
						|
		end
 | 
						|
 | 
						|
		print(
 | 
						|
			string.format(
 | 
						|
				`%s %s %s %s{WALL} %s`,
 | 
						|
				color.gray(round(d)),
 | 
						|
				d_unit,
 | 
						|
				color.gray(round(a)),
 | 
						|
				a_unit,
 | 
						|
				color.gray(name)
 | 
						|
			)
 | 
						|
		)
 | 
						|
	end
 | 
						|
 | 
						|
	bench = nil
 | 
						|
end
 | 
						|
 | 
						|
--------------------------------------------------------------------------------
 | 
						|
-- Printing
 | 
						|
--------------------------------------------------------------------------------
 | 
						|
 | 
						|
local function print2(v: unknown)
 | 
						|
	type Buffer = { n: number, [number]: string }
 | 
						|
	type Cyclic = { n: number, [{}]: number }
 | 
						|
 | 
						|
	-- overkill concatenationless string buffer
 | 
						|
	local function tos(value: any, stack: number, str: Buffer, cyclic: Cyclic)
 | 
						|
		local TAB = "    "
 | 
						|
		local indent = table.concat(table.create(stack, TAB))
 | 
						|
 | 
						|
		if type(value) == "string" then
 | 
						|
			local n = str.n
 | 
						|
			str[n + 1] = "\""
 | 
						|
			str[n + 2] = value
 | 
						|
			str[n + 3] = "\""
 | 
						|
			str.n = n + 3
 | 
						|
		elseif type(value) ~= "table" then
 | 
						|
			local n = str.n
 | 
						|
			str[n + 1] = value == nil and "nil" or tostring(value)
 | 
						|
			str.n = n + 1
 | 
						|
		elseif next(value) == nil then
 | 
						|
			local n = str.n
 | 
						|
			str[n + 1] = "{}"
 | 
						|
			str.n = n + 1
 | 
						|
		else -- is table
 | 
						|
			local tabbed_indent = indent .. TAB
 | 
						|
 | 
						|
			if cyclic[value] then
 | 
						|
				str.n += 1
 | 
						|
				str[str.n] = color.gray(`CYCLIC REF {cyclic[value]}`)
 | 
						|
				return
 | 
						|
			else
 | 
						|
				cyclic.n += 1
 | 
						|
				cyclic[value] = cyclic.n
 | 
						|
			end
 | 
						|
 | 
						|
			str.n += 3
 | 
						|
			str[str.n - 2] = "{ "
 | 
						|
			str[str.n - 1] = color.gray(tostring(cyclic[value]))
 | 
						|
			str[str.n - 0] = "\n"
 | 
						|
 | 
						|
			local i, v = next(value, nil)
 | 
						|
			while v ~= nil do
 | 
						|
				local n = str.n
 | 
						|
				str[n + 1] = tabbed_indent
 | 
						|
 | 
						|
				if type(i) ~= "string" then
 | 
						|
					str[n + 2] = "["
 | 
						|
					str[n + 3] = tostring(i)
 | 
						|
					str[n + 4] = "]"
 | 
						|
					n += 4
 | 
						|
				else
 | 
						|
					str[n + 2] = tostring(i)
 | 
						|
					n += 2
 | 
						|
				end
 | 
						|
 | 
						|
				str[n + 1] = " = "
 | 
						|
				str.n = n + 1
 | 
						|
 | 
						|
				tos(v, stack + 1, str, cyclic)
 | 
						|
 | 
						|
				i, v = next(value, i)
 | 
						|
 | 
						|
				n = str.n
 | 
						|
				str[n + 1] = v ~= nil and ",\n" or "\n"
 | 
						|
				str.n = n + 1
 | 
						|
			end
 | 
						|
 | 
						|
			local n = str.n
 | 
						|
			str[n + 1] = indent
 | 
						|
			str[n + 2] = "}"
 | 
						|
			str.n = n + 2
 | 
						|
		end
 | 
						|
	end
 | 
						|
 | 
						|
	local str = { n = 0 }
 | 
						|
	local cyclic = { n = 0 }
 | 
						|
	tos(v, 0, str, cyclic)
 | 
						|
	print(table.concat(str))
 | 
						|
end
 | 
						|
 | 
						|
--------------------------------------------------------------------------------
 | 
						|
-- Equality
 | 
						|
--------------------------------------------------------------------------------
 | 
						|
 | 
						|
local function shallow_eq(a: {}, b: {}): boolean
 | 
						|
	if #a ~= #b then
 | 
						|
		return false
 | 
						|
	end
 | 
						|
 | 
						|
	for i, v in next, a do
 | 
						|
		if b[i] ~= v then
 | 
						|
			return false
 | 
						|
		end
 | 
						|
	end
 | 
						|
 | 
						|
	for i, v in next, b do
 | 
						|
		if a[i] ~= v then
 | 
						|
			return false
 | 
						|
		end
 | 
						|
	end
 | 
						|
 | 
						|
	return true
 | 
						|
end
 | 
						|
 | 
						|
local function deep_eq(a: {}, b: {}): boolean
 | 
						|
	if #a ~= #b then
 | 
						|
		return false
 | 
						|
	end
 | 
						|
 | 
						|
	for i, v in next, a do
 | 
						|
		if type(b[i]) == "table" and type(v) == "table" then
 | 
						|
			if deep_eq(b[i], v) == false then
 | 
						|
				return false
 | 
						|
			end
 | 
						|
		elseif b[i] ~= v then
 | 
						|
			return false
 | 
						|
		end
 | 
						|
	end
 | 
						|
 | 
						|
	for i, v in next, b do
 | 
						|
		if type(a[i]) == "table" and type(v) == "table" then
 | 
						|
			if deep_eq(a[i], v) == false then
 | 
						|
				return false
 | 
						|
			end
 | 
						|
		elseif a[i] ~= v then
 | 
						|
			return false
 | 
						|
		end
 | 
						|
	end
 | 
						|
 | 
						|
	return true
 | 
						|
end
 | 
						|
 | 
						|
--------------------------------------------------------------------------------
 | 
						|
-- Return
 | 
						|
--------------------------------------------------------------------------------
 | 
						|
 | 
						|
return {
 | 
						|
	test = function()
 | 
						|
		return {
 | 
						|
			TEST = TEST,
 | 
						|
			CASE = CASE,
 | 
						|
			CHECK = CHECK,
 | 
						|
			FINISH = FINISH,
 | 
						|
			SKIP = SKIP,
 | 
						|
			FOCUS = FOCUS,
 | 
						|
			CHECK_EXPECT_ERR = CHECK_EXPECT_ERR,
 | 
						|
		}
 | 
						|
	end,
 | 
						|
 | 
						|
	benchmark = function()
 | 
						|
		return BENCH, START
 | 
						|
	end,
 | 
						|
 | 
						|
	disable_formatting = function()
 | 
						|
		disable_ansi = true
 | 
						|
	end,
 | 
						|
 | 
						|
	print = print2,
 | 
						|
 | 
						|
	seq = shallow_eq,
 | 
						|
	deq = deep_eq,
 | 
						|
 | 
						|
	color = color,
 | 
						|
}
 |