# testkit.luau Coverage

## Total Execution Hits: 1826

## Function Coverage Overview: 64.52%

## Function Coverage:

Function | Hits
--- | ---
| 1
white_underline:11 | 0
white:15 | 24
green:19 | 77
red:23 | 146
yellow:27 | 76
red_highlight:31 | 0
green_highlight:35 | 0
gray:39 | 84
orange:43 | 73
convert_units:48 | 6
output_test_result:131 | 24
CASE:169 | 73
CHECK_EXPECT_ERR:183 | 9
CHECK:201 | 1195
TEST:224 | 24
FOCUS:237 | 0
FINISH:248 | 1
:264 | 0
SKIP:314 | 1
START:330 | 1
BENCH:342 | 3
:354 | 0
round:372 | 6
print2:396 | 0
tos:401 | 0
shallow_eq:480 | 0
deep_eq:500 | 0
test:533 | 1
benchmark:545 | 1
disable_formatting:549 | 0

## Source Code:

Line | Hits | Code
--- | --- | ---
1 | N/A | -------------------------------------------------------------------------------->
2 | N/A | -- testkit.luau>
3 | N/A | -- v0.7.3>
4 | N/A | -- MIT License>
5 | N/A | -- Copyright (c) 2022 centau>
6 | N/A | -------------------------------------------------------------------------------->
7 | N/A | >
8 | 1 | local disable_ansi = false
9 | N/A | >
10 | 1 | local color = {
11 | 1 | white_underline = function(s: string): string
12 | 0 | return if disable_ansi then s else `\27[1;4m{s}\27[0m`
13 | N/A | end,>
14 | N/A | >
15 | 1 | white = function(s: string): string
16 | 24 | return if disable_ansi then s else `\27[37;1m{s}\27[0m`
17 | N/A | end,>
18 | N/A | >
19 | 1 | green = function(s: string): string
20 | 77 | return if disable_ansi then s else `\27[32;1m{s}\27[0m`
21 | N/A | end,>
22 | N/A | >
23 | 1 | red = function(s: string): string
24 | 146 | return if disable_ansi then s else `\27[31;1m{s}\27[0m`
25 | N/A | end,>
26 | N/A | >
27 | 1 | yellow = function(s: string): string
28 | 76 | return if disable_ansi then s else `\27[33;1m{s}\27[0m`
29 | N/A | end,>
30 | N/A | >
31 | 1 | red_highlight = function(s: string): string
32 | 0 | return if disable_ansi then s else `\27[41;1;30m{s}\27[0m`
33 | N/A | end,>
34 | N/A | >
35 | 1 | green_highlight = function(s: string): string
36 | 0 | return if disable_ansi then s else `\27[42;1;30m{s}\27[0m`
37 | N/A | end,>
38 | N/A | >
39 | 1 | gray = function(s: string): string
40 | 84 | return if disable_ansi then s else `\27[38;1m{s}\27[0m`
41 | N/A | end,>
42 | N/A | >
43 | 1 | orange = function(s: string): string
44 | 73 | return if disable_ansi then s else `\27[38;5;208m{s}\27[0m`
45 | N/A | end,>
46 | 0 | }
47 | N/A | >
48 | 1 | local function convert_units(unit: string, value: number): (number, string)
49 | 6 | local sign = math.sign(value)
50 | 6 | value = math.abs(value)
51 | N/A | >
52 | 6 | local prefix_colors = {
53 | 6 | [4] = color.red,
54 | 6 | [3] = color.red,
55 | 6 | [2] = color.yellow,
56 | 6 | [1] = color.yellow,
57 | 6 | [0] = color.green,
58 | 6 | [-1] = color.red,
59 | 6 | [-2] = color.yellow,
60 | 6 | [-3] = color.green,
61 | 6 | [-4] = color.red,
62 | 0 | }
63 | N/A | >
64 | 6 | local prefixes = {
65 | 6 | [4] = "T",
66 | 6 | [3] = "G",
67 | 6 | [2] = "M",
68 | 6 | [1] = "k",
69 | 6 | [0] = " ",
70 | 6 | [-1] = "m",
71 | 6 | [-2] = "u",
72 | 6 | [-3] = "n",
73 | 6 | [-4] = "p",
74 | 0 | }
75 | N/A | >
76 | 6 | local order = 0
77 | N/A | >
78 | 7 | while value >= 1000 do
79 | 1 | order += 1
80 | 1 | value /= 1000
81 | N/A | end>
82 | N/A | >
83 | 11 | while value ~= 0 and value < 1 do
84 | 7 | order -= 1
85 | 7 | value *= 1000
86 | N/A | end>
87 | N/A | >
88 | 6 | if value >= 100 then
89 | 1 | value = math.floor(value)
90 | 5 | elseif value >= 10 then
91 | 2 | value = math.floor(value * 1e1) / 1e1
92 | 3 | elseif value >= 1 then
93 | 1 | value = math.floor(value * 1e2) / 1e2
94 | N/A | end>
95 | N/A | >
96 | 6 | return value * sign, prefix_colors[order](prefixes[order] .. unit)
97 | N/A | end>
98 | N/A | >
99 | 1 | local WALL = color.gray("│")
100 | N/A | >
101 | N/A | -------------------------------------------------------------------------------->
102 | N/A | -- Testing>
103 | N/A | -------------------------------------------------------------------------------->
104 | N/A | >
105 | 0 | type Test = {
106 | 0 | name: string,
107 | 0 | case: Case?,
108 | 0 | cases: { Case },
109 | 0 | duration: number,
110 | 0 | error: {
111 | 0 | message: string,
112 | 0 | trace: string,
113 | 0 | }?,
114 | 0 | focus: boolean,
115 | 0 | }
116 | N/A | >
117 | 0 | type Case = {
118 | 0 | name: string,
119 | 0 | result: number,
120 | 0 | line: number?,
121 | 0 | focus: boolean,
122 | 0 | }
123 | N/A | >
124 | 1 | local PASS, FAIL, NONE, ERROR, SKIPPED = 1, 2, 3, 4, 5
125 | N/A | >
126 | 1 | local check_for_focused = false
127 | 1 | local skip = false
128 | 1 | local test: Test?
129 | 1 | local tests: { Test } = {}
130 | N/A | >
131 | 1 | local function output_test_result(test: Test)
132 | 24 | if check_for_focused then
133 | 0 | local any_focused = test.focus
134 | 0 | for _, case in test.cases do
135 | 0 | any_focused = any_focused or case.focus
136 | N/A | end>
137 | N/A | >
138 | 0 | if not any_focused then
139 | 0 | return
140 | N/A | end>
141 | N/A | end>
142 | N/A | >
143 | 24 | print(color.white(test.name))
144 | N/A | >
145 | 24 | for _, case in test.cases do
146 | 73 | local status = ({
147 | 73 | [PASS] = color.green("PASS"),
148 | 73 | [FAIL] = color.red("FAIL"),
149 | 73 | [NONE] = color.orange("NONE"),
150 | 73 | [ERROR] = color.red("FAIL"),
151 | 73 | [SKIPPED] = color.yellow("SKIP"),
152 | 73 | })[case.result]
153 | N/A | >
154 | 73 | local line = case.result == FAIL and color.red(`{case.line}:`) or ""
155 | 73 | if check_for_focused and case.focus == false and test.focus == false then
156 | 0 | continue
157 | N/A | end>
158 | 73 | print(`{status}{WALL} {line}{color.gray(case.name)}`)
159 | N/A | end>
160 | N/A | >
161 | 24 | if test.error then
162 | 0 | print(color.gray("error: ") .. color.red(test.error.message))
163 | 0 | print(color.gray("trace: ") .. color.red(test.error.trace))
164 | 0 | else
165 | 24 | print()
166 | N/A | end>
167 | N/A | end>
168 | N/A | >
169 | 1 | local function CASE(name: string)
170 | 73 | skip = false
171 | 73 | assert(test, "no active test")
172 | N/A | >
173 | 73 | local case = {
174 | 73 | name = name,
175 | 73 | result = NONE,
176 | 73 | focus = false,
177 | 0 | }
178 | N/A | >
179 | 73 | test.case = case
180 | 73 | table.insert(test.cases, case)
181 | N/A | end>
182 | N/A | >
183 | 1 | local function CHECK_EXPECT_ERR(fn, ...)
184 | 9 | assert(test, "no active test")
185 | 9 | local case = test.case
186 | 9 | if not case then
187 | 0 | CASE("")
188 | 0 | case = test.case
189 | N/A | end>
190 | 9 | assert(case, "no active case")
191 | 9 | if case.result ~= FAIL then
192 | 9 | local ok, err = pcall(fn, ...)
193 | 9 | case.result = if ok then FAIL else PASS
194 | 9 | if skip then
195 | 0 | case.result = SKIPPED
196 | N/A | end>
197 | 9 | case.line = debug.info(stack and stack + 1 or 2, "l")
198 | N/A | end>
199 | N/A | end>
200 | N/A | >
201 | 1 | local function CHECK(value: T, stack: number?): T?
202 | 1195 | assert(test, "no active test")
203 | N/A | >
204 | 1195 | local case = test.case
205 | N/A | >
206 | 1195 | if not case then
207 | 9 | CASE("")
208 | 9 | case = test.case
209 | N/A | end>
210 | N/A | >
211 | 1195 | assert(case, "no active case")
212 | N/A | >
213 | 1195 | if case.result ~= FAIL then
214 | 1195 | case.result = value and PASS or FAIL
215 | 1195 | if skip then
216 | 1 | case.result = SKIPPED
217 | N/A | end>
218 | 1195 | case.line = debug.info(stack and stack + 1 or 2, "l")
219 | N/A | end>
220 | N/A | >
221 | 1195 | return value
222 | N/A | end>
223 | N/A | >
224 | 1 | local function TEST(name: string, fn: () -> ())
225 | N/A | >
226 | 24 | test = {
227 | 24 | name = name,
228 | 24 | cases = {},
229 | 24 | duration = 0,
230 | 24 | focus = false,
231 | 24 | fn = fn
232 | 0 | }
233 | N/A | >
234 | 24 | table.insert(tests, test)
235 | N/A | end>
236 | N/A | >
237 | 1 | local function FOCUS()
238 | 0 | assert(test, "no active test")
239 | N/A | >
240 | 0 | check_for_focused = true
241 | 0 | if test.case then
242 | 0 | test.case.focus = true
243 | 0 | else
244 | 0 | test.focus = true
245 | N/A | end>
246 | N/A | end>
247 | N/A | >
248 | 1 | local function FINISH(): boolean
249 | 1 | local success = true
250 | 1 | local total_cases = 0
251 | 1 | local passed_cases = 0
252 | 1 | local passed_focus_cases = 0
253 | 1 | local total_focus_cases = 0
254 | 1 | local duration = 0
255 | N/A | >
256 | 1 | for _, t in tests do
257 | 24 | if check_for_focused and not t.focus then
258 | 0 | continue
259 | N/A | end>
260 | 24 | test = t
261 | 24 | fn = t.fn
262 | 24 | local start = os.clock()
263 | 24 | local err
264 | 24 | local success = xpcall(fn, function(m: string)
265 | 0 | err = { message = m, trace = debug.traceback(nil, 2) }
266 | N/A | end)>
267 | 24 | test.duration = os.clock() - start
268 | N/A | >
269 | 24 | if not test.case then
270 | 0 | CASE("")
271 | N/A | end>
272 | 24 | assert(test.case, "no active case")
273 | N/A | >
274 | 24 | if not success then
275 | 0 | test.case.result = ERROR
276 | 0 | test.error = err
277 | N/A | end>
278 | 24 | collectgarbage()
279 | N/A | end>
280 | N/A | >
281 | 1 | for _, test in tests do
282 | 24 | duration += test.duration
283 | 24 | for _, case in test.cases do
284 | 73 | total_cases += 1
285 | 73 | if case.focus or test.focus then
286 | 0 | total_focus_cases += 1
287 | N/A | end>
288 | 73 | if case.result == PASS or case.result == NONE or case.result == SKIPPED then
289 | 73 | if case.focus or test.focus then
290 | 0 | passed_focus_cases += 1
291 | N/A | end>
292 | 73 | passed_cases += 1
293 | 0 | else
294 | 0 | success = false
295 | N/A | end>
296 | N/A | end>
297 | N/A | >
298 | 24 | output_test_result(test)
299 | N/A | end>
300 | N/A | >
301 | 1 | print(color.gray(string.format(`{passed_cases}/{total_cases} test cases passed in %.3f ms.`, duration * 1e3)))
302 | 1 | if check_for_focused then
303 | 0 | print(color.gray(`{passed_focus_cases}/{total_focus_cases} focused test cases passed`))
304 | N/A | end>
305 | N/A | >
306 | 1 | local fails = total_cases - passed_cases
307 | N/A | >
308 | 1 | print((fails > 0 and color.red or color.green)(`{fails} {fails == 1 and "fail" or "fails"}`))
309 | N/A | >
310 | 1 | check_for_focused = false
311 | 1 | return success, table.clear(tests)
312 | N/A | end>
313 | N/A | >
314 | 1 | local function SKIP()
315 | 1 | skip = true
316 | N/A | end>
317 | N/A | >
318 | N/A | -------------------------------------------------------------------------------->
319 | N/A | -- Benchmarking>
320 | N/A | -------------------------------------------------------------------------------->
321 | N/A | >
322 | 0 | type Bench = {
323 | 0 | time_start: number?,
324 | 0 | memory_start: number?,
325 | 0 | iterations: number?,
326 | 0 | }
327 | N/A | >
328 | 1 | local bench: Bench?
329 | N/A | >
330 | 1 | function START(iter: number?): number
331 | 1 | local n = iter or 1
332 | 1 | assert(n > 0, "iterations must be greater than 0")
333 | 1 | assert(bench, "no active benchmark")
334 | 1 | assert(not bench.time_start, "clock was already started")
335 | N/A | >
336 | 1 | bench.iterations = n
337 | 1 | bench.memory_start = gcinfo()
338 | 1 | bench.time_start = os.clock()
339 | 1 | return n
340 | N/A | end>
341 | N/A | >
342 | 1 | local function BENCH(name: string, fn: () -> ())
343 | 3 | local active = bench
344 | 3 | assert(not active, "a benchmark is already in progress")
345 | N/A | >
346 | 3 | bench = {}
347 | 3 | assert(bench);
348 | 3 | (collectgarbage :: any)("collect")
349 | N/A | >
350 | 3 | local mem_start = gcinfo()
351 | 3 | local time_start = os.clock()
352 | 3 | local err_msg: string?
353 | N/A | >
354 | 3 | local success = xpcall(fn, function(m: string)
355 | 0 | err_msg = m .. debug.traceback(nil, 2)
356 | N/A | end)>
357 | N/A | >
358 | 3 | local time_stop = os.clock()
359 | 3 | local mem_stop = gcinfo()
360 | N/A | >
361 | 3 | if not success then
362 | 0 | print(`{WALL}{color.red("ERROR")}{WALL} {name}`)
363 | 0 | print(color.gray(err_msg :: string))
364 | 0 | else
365 | 3 | time_start = bench.time_start or time_start
366 | 3 | mem_start = bench.memory_start or mem_start
367 | N/A | >
368 | 3 | local n = bench.iterations or 1
369 | 3 | local d, d_unit = convert_units("s", (time_stop - time_start) / n)
370 | 3 | local a, a_unit = convert_units("B", math.round((mem_stop - mem_start) / n * 1e3))
371 | N/A | >
372 | 3 | local function round(x: number): string
373 | 6 | return x > 0 and x < 10 and (x - math.floor(x)) > 0 and string.format("%2.1f", x)
374 | 6 | or string.format("%3.f", x)
375 | N/A | end>
376 | N/A | >
377 | 3 | print(
378 | 3 | string.format(
379 | 3 | `%s %s %s %s{WALL} %s`,
380 | 3 | color.gray(round(d)),
381 | 3 | d_unit,
382 | 3 | color.gray(round(a)),
383 | 3 | a_unit,
384 | 3 | color.gray(name)
385 | 0 | )
386 | 0 | )
387 | N/A | end>
388 | N/A | >
389 | 3 | bench = nil
390 | N/A | end>
391 | N/A | >
392 | N/A | -------------------------------------------------------------------------------->
393 | N/A | -- Printing>
394 | N/A | -------------------------------------------------------------------------------->
395 | N/A | >
396 | 1 | local function print2(v: unknown)
397 | 0 | type Buffer = { n: number, [number]: string }
398 | 0 | type Cyclic = { n: number, [{}]: number }
399 | N/A | >
400 | N/A | -- overkill concatenationless string buffer>
401 | 0 | local function tos(value: any, stack: number, str: Buffer, cyclic: Cyclic)
402 | 0 | local TAB = " "
403 | 0 | local indent = table.concat(table.create(stack, TAB))
404 | N/A | >
405 | 0 | if type(value) == "string" then
406 | 0 | local n = str.n
407 | 0 | str[n + 1] = "\""
408 | 0 | str[n + 2] = value
409 | 0 | str[n + 3] = "\""
410 | 0 | str.n = n + 3
411 | 0 | elseif type(value) ~= "table" then
412 | 0 | local n = str.n
413 | 0 | str[n + 1] = value == nil and "nil" or tostring(value)
414 | 0 | str.n = n + 1
415 | 0 | elseif next(value) == nil then
416 | 0 | local n = str.n
417 | 0 | str[n + 1] = "{}"
418 | 0 | str.n = n + 1
419 | 0 | else -- is table
420 | 0 | local tabbed_indent = indent .. TAB
421 | N/A | >
422 | 0 | if cyclic[value] then
423 | 0 | str.n += 1
424 | 0 | str[str.n] = color.gray(`CYCLIC REF {cyclic[value]}`)
425 | 0 | return
426 | 0 | else
427 | 0 | cyclic.n += 1
428 | 0 | cyclic[value] = cyclic.n
429 | N/A | end>
430 | N/A | >
431 | 0 | str.n += 3
432 | 0 | str[str.n - 2] = "{ "
433 | 0 | str[str.n - 1] = color.gray(tostring(cyclic[value]))
434 | 0 | str[str.n - 0] = "\n"
435 | N/A | >
436 | 0 | local i, v = next(value, nil)
437 | 0 | while v ~= nil do
438 | 0 | local n = str.n
439 | 0 | str[n + 1] = tabbed_indent
440 | N/A | >
441 | 0 | if type(i) ~= "string" then
442 | 0 | str[n + 2] = "["
443 | 0 | str[n + 3] = tostring(i)
444 | 0 | str[n + 4] = "]"
445 | 0 | n += 4
446 | 0 | else
447 | 0 | str[n + 2] = tostring(i)
448 | 0 | n += 2
449 | N/A | end>
450 | N/A | >
451 | 0 | str[n + 1] = " = "
452 | 0 | str.n = n + 1
453 | N/A | >
454 | 0 | tos(v, stack + 1, str, cyclic)
455 | N/A | >
456 | 0 | i, v = next(value, i)
457 | N/A | >
458 | 0 | n = str.n
459 | 0 | str[n + 1] = v ~= nil and ",\n" or "\n"
460 | 0 | str.n = n + 1
461 | N/A | end>
462 | N/A | >
463 | 0 | local n = str.n
464 | 0 | str[n + 1] = indent
465 | 0 | str[n + 2] = "}"
466 | 0 | str.n = n + 2
467 | N/A | end>
468 | N/A | end>
469 | N/A | >
470 | 0 | local str = { n = 0 }
471 | 0 | local cyclic = { n = 0 }
472 | 0 | tos(v, 0, str, cyclic)
473 | 0 | print(table.concat(str))
474 | N/A | end>
475 | N/A | >
476 | N/A | -------------------------------------------------------------------------------->
477 | N/A | -- Equality>
478 | N/A | -------------------------------------------------------------------------------->
479 | N/A | >
480 | 1 | local function shallow_eq(a: {}, b: {}): boolean
481 | 0 | if #a ~= #b then
482 | 0 | return false
483 | N/A | end>
484 | N/A | >
485 | 0 | for i, v in next, a do
486 | 0 | if b[i] ~= v then
487 | 0 | return false
488 | N/A | end>
489 | N/A | end>
490 | N/A | >
491 | 0 | for i, v in next, b do
492 | 0 | if a[i] ~= v then
493 | 0 | return false
494 | N/A | end>
495 | N/A | end>
496 | N/A | >
497 | 0 | return true
498 | N/A | end>
499 | N/A | >
500 | 1 | local function deep_eq(a: {}, b: {}): boolean
501 | 0 | if #a ~= #b then
502 | 0 | return false
503 | N/A | end>
504 | N/A | >
505 | 0 | for i, v in next, a do
506 | 0 | if type(b[i]) == "table" and type(v) == "table" then
507 | 0 | if deep_eq(b[i], v) == false then
508 | 0 | return false
509 | N/A | end>
510 | 0 | elseif b[i] ~= v then
511 | 0 | return false
512 | N/A | end>
513 | N/A | end>
514 | N/A | >
515 | 0 | for i, v in next, b do
516 | 0 | if type(a[i]) == "table" and type(v) == "table" then
517 | 0 | if deep_eq(a[i], v) == false then
518 | 0 | return false
519 | N/A | end>
520 | 0 | elseif a[i] ~= v then
521 | 0 | return false
522 | N/A | end>
523 | N/A | end>
524 | N/A | >
525 | 0 | return true
526 | N/A | end>
527 | N/A | >
528 | N/A | -------------------------------------------------------------------------------->
529 | N/A | -- Return>
530 | N/A | -------------------------------------------------------------------------------->
531 | N/A | >
532 | 1 | return {
533 | 1 | test = function()
534 | 1 | return {
535 | 1 | TEST = TEST,
536 | 1 | CASE = CASE,
537 | 1 | CHECK = CHECK,
538 | 1 | FINISH = FINISH,
539 | 1 | SKIP = SKIP,
540 | 1 | FOCUS = FOCUS,
541 | 1 | CHECK_EXPECT_ERR = CHECK_EXPECT_ERR,
542 | 0 | }
543 | N/A | end,>
544 | N/A | >
545 | 1 | benchmark = function()
546 | 1 | return BENCH, START
547 | N/A | end,>
548 | N/A | >
549 | 1 | disable_formatting = function()
550 | 0 | disable_ansi = true
551 | N/A | end,>
552 | N/A | >
553 | 1 | print = print2,
554 | N/A | >
555 | 1 | seq = shallow_eq,
556 | 1 | deq = deep_eq,
557 | N/A | >
558 | 1 | color = color,
559 | 0 | }