hare

[hare] The Hare programming language
git clone https://git.torresjrjr.com/hare.git
Log | Files | Refs | README | LICENSE

commit 01b8aaa3c137a017962af0436c7ada432583cbc8
parent 2003bf9b541e58e6d86d2e0df9f940cbc66b4c77
Author: Sebastian <sebastian@sebsite.pw>
Date:   Mon,  6 Nov 2023 23:27:27 -0500

test: implement skip function

The test runner itself is also refactored to more neatly accomodate for
skipped tests.

Signed-off-by: Sebastian <sebastian@sebsite.pw>

Diffstat:
Mrt/abort+test.ha | 2+-
Mtest/+test.ha | 169++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Mtest/util+test.ha | 12++++++++++++
Mtest/util.ha | 5+++++
4 files changed, 112 insertions(+), 76 deletions(-)

diff --git a/rt/abort+test.ha b/rt/abort+test.ha @@ -25,7 +25,7 @@ export @symbol("rt.abort") fn _abort( col = col, msg = msg, }; - longjmp(j, 1); + longjmp(j, 1); // test::status::ABORT case null => platform_abort(path, line, col, msg); }; diff --git a/test/+test.ha b/test/+test.ha @@ -20,11 +20,24 @@ type test = struct { func: *fn() void, }; +// RETURN and ABORT must be 0 and 1 respectively +type status = enum { + RETURN = 0, + ABORT = 1, + SKIP, + SEGV, +}; + type failure = struct { test: str, reason: rt::abort_reason, }; +type skipped = struct { + test: str, + reason: str, +}; + type output = struct { test: str, stdout: str, @@ -40,6 +53,7 @@ type context = struct { stdout: memio::stream, stderr: memio::stream, failures: []failure, + skipped: []skipped, output: []output, maxname: size, total_time: time::duration, @@ -51,6 +65,7 @@ fn finish_context(ctx: *context) void = { io::close(&ctx.stdout)!; io::close(&ctx.stderr)!; free(ctx.failures); + free(ctx.skipped); for (let i = 0z; i < len(ctx.output); i += 1) { finish_output(&ctx.output[i]); }; @@ -68,7 +83,6 @@ let jmpbuf = rt::jmpbuf { ... }; const @symbol("__test_array_start") test_start: [*]test; const @symbol("__test_array_end") test_end: [*]test; - export @symbol("__test_main") fn main() size = { const ntest = (&test_end: uintptr - &test_start: uintptr): size / size(test); const tests = test_start[..ntest]; @@ -109,6 +123,14 @@ export @symbol("__test_main") fn main() size = { }; fmt::println()!; + for (let i = 0z; i < len(ctx.skipped); i += 1) { + fmt::printfln("Skipped {}: {}", ctx.skipped[i].test, + ctx.skipped[i].reason)!; + }; + if (len(ctx.skipped) > 0) { + fmt::println()!; + }; + if (len(ctx.failures) > 0) { fmt::println("Failures:")!; for (let i = 0z; i < len(ctx.failures); i += 1) { @@ -144,24 +166,21 @@ export @symbol("__test_main") fn main() size = { }; // XXX: revisit once time::format_duration is implemented - const passed_cnt = len(enabled_tests) - len(ctx.failures); - const failed_cnt = len(ctx.failures); const total_cnt = len(enabled_tests); + const failed_cnt = len(ctx.failures); + const skipped_cnt = len(ctx.skipped); + const passed_cnt = total_cnt - failed_cnt - skipped_cnt; const elapsed_whole = ctx.total_time / time::SECOND; const elapsed_fraction = ctx.total_time % time::SECOND; - if (colored()) { - fmt::printfln("\x1b[{}m" "{}" "\x1b[m" " passed; " - "\x1b[{}m" "{}" "\x1b[m" - " failed; {} completed in {}.{:09}s", - if (len(enabled_tests) != len(ctx.failures)) "92" else "37", - passed_cnt, - if (len(ctx.failures) > 0) "91" else "37", - failed_cnt, total_cnt, elapsed_whole, elapsed_fraction)!; - } else { - fmt::printfln("{} passed; {} failed; {} completed in {}.{:09}s", - passed_cnt, failed_cnt, total_cnt, elapsed_whole, - elapsed_fraction)!; + styled_print(if (passed_cnt > 0) 92 else 37, passed_cnt); + fmt::print(" passed; ")!; + styled_print(if (len(ctx.failures) > 0) 91 else 37, failed_cnt); + fmt::print(" failed; ")!; + if (len(ctx.skipped) > 0) { + fmt::print(len(ctx.skipped), "skipped; ")!; }; + fmt::printfln("{} completed in {}.{:09}s", total_cnt, + elapsed_whole, elapsed_fraction)!; easter_egg(ctx.failures, enabled_tests); @@ -173,6 +192,7 @@ fn reset(ctx: *context) void = { rt::feclearexcept(~0u); signal::resetall(); os::chdir(ctx.cwd)!; + want_abort = false; }; fn do_test(ctx: *context, test: test) void = { @@ -182,13 +202,8 @@ fn do_test(ctx: *context, test: test) void = { const start_time = time::now(time::clock::MONOTONIC); - const failed = match (run_test(ctx, test)) { - case void => - yield false; - case let f: failure => - append(ctx.failures, f); - yield true; - }; + const status = run_test(ctx, test); + const failed = interpret_status(ctx, test.name, status); const end_time = time::now(time::clock::MONOTONIC); const time_diff = time::diff(start_time, end_time); @@ -227,7 +242,7 @@ fn do_test(ctx: *context, test: test) void = { reset(ctx); }; -fn run_test(ctx: *context, test: test) (void | failure) = { +fn run_test(ctx: *context, test: test) status = { fmt::print(test.name)!; dots(ctx.maxname - len(test.name) + 3); bufio::flush(os::stdout)!; // write test name before test runs @@ -237,74 +252,78 @@ fn run_test(ctx: *context, test: test) (void | failure) = { os::stdout = &ctx.stdout; os::stderr = &ctx.stderr; defer rt::jmp = null; - const n = rt::setjmp(&jmpbuf); - if (n != 0) { - os::stdout = orig_stdout; - os::stderr = orig_stderr; - if (n == 1 && want_abort) { - want_abort = false; - pass(); - return; - }; - return fail(test, n); + const n = rt::setjmp(&jmpbuf): status; + if (n == status::RETURN) { + rt::jmp = &jmpbuf; + test.func(); }; - rt::jmp = &jmpbuf; - test.func(); os::stdout = orig_stdout; os::stderr = orig_stderr; - if (want_abort) { - want_abort = false; - return fail(test, 1); - }; - pass(); + return n; }; -fn pass() void = { - if (colored()) { - fmt::print("\x1b[92m" "PASS" "\x1b[m")!; - } else { - fmt::print("PASS")!; +fn dots(n: size) void = { + for (let i = 0z; i < n; i += 1) { + fmt::print(".")!; }; }; -fn fail(test: test, n: int) failure = { - if (colored()) { - fmt::print("\x1b[91m" "FAIL" "\x1b[m")!; - } else { - fmt::print("FAIL")!; - }; - switch (n) { - case 1 => - // assertion failed - return failure { - test = test.name, - reason = rt::reason, +// returns true if test failed, false if it passed or was skipped +fn interpret_status(ctx: *context, test: str, status: status) bool = { + switch (status) { + case status::RETURN => + if (want_abort) { + styled_print(91, "FAIL"); + append(ctx.failures, failure { + test = test, + reason = rt::abort_reason { + msg = "Expected test to abort", + ... + }, + }); + return true; + } else { + styled_print(92, "PASS"); + return false; }; - case 2 => - // segmentation fault - return failure { - test = test.name, - reason = rt::abort_reason { - msg = "Segmentation fault", - ... - }, + case status::ABORT => + if (want_abort) { + styled_print(92, "PASS"); + return false; + } else { + styled_print(91, "FAIL"); + append(ctx.failures, failure { + test = test, + reason = rt::reason, + }); + return true; }; - case => - // unrecognized failure - return failure { - test = test.name, + case status::SKIP => + styled_print(37, "SKIP"); + append(ctx.skipped, skipped { + test = test, + reason = rt::reason.msg, + }); + return false; + case status::SEGV => + styled_print(91, "FAIL"); + append(ctx.failures, failure { + test = test, reason = rt::abort_reason { - msg = "Reason unknown", + msg = "Segmentation fault", ... }, - }; + }); + return true; }; }; -fn dots(n: size) void = { - for (let i = 0z; i < n; i += 1) { - fmt::print(".")!; +fn styled_print(color: int, result: fmt::formattable) void = { + if (colored()) { + fmt::printf("\x1b[{}m" "{}" "\x1b[m", color, result)!; + } else { + fmt::print(result)!; }; }; @@ -313,7 +332,7 @@ fn handle_segv( info: *signal::siginfo, ucontext: *opaque, ) void = { - rt::longjmp(&jmpbuf, 2); + rt::longjmp(&jmpbuf, status::SEGV); }; fn easter_egg(fails: []failure, tests: []test) void = { diff --git a/test/util+test.ha b/test/util+test.ha @@ -13,3 +13,15 @@ export fn expectabort() void = { }; want_abort = true; }; + +// Skip the currently running test. +export fn skip(reason: str) never = { + if (rt::jmp == null) { + abort("Attempted to call test::skip outside of @test function"); + }; + rt::reason = rt::abort_reason { + msg = reason, + ... + }; + rt::longjmp(&jmpbuf, status::SKIP); +}; diff --git a/test/util.ha b/test/util.ha @@ -6,3 +6,8 @@ export fn expectabort() void = { abort("Attempted to call test::expectabort outside of @test function"); }; + +// Skip the currently running test. +export fn skip(reason: str) never = { + abort("Attempted to call test::skip outside of @test function"); +};