# 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
<anonymous>:264 | 0
SKIP:314 | 1
START:330 | 1
BENCH:342 | 3
<anonymous>: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<T>(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 | }