hare

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

commit 2459709ee594bb99c0ebc759c36d6a3a8d89df03
parent 3dd966977c33a89c9bb85662a4e131f892125d9b
Author: Drew DeVault <sir@cmpwn.com>
Date:   Wed,  1 Jan 2025 20:00:59 +0100

test: store backtrace on abort

And print it with the test summary.

Signed-off-by: Drew DeVault <sir@cmpwn.com>

Diffstat:
Drt/abort+test.ha | 61-------------------------------------------------------------
Mrt/abort.ha | 6++++--
Mtest/+test.ha | 96++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Mtest/util+test.ha | 10+++++-----
4 files changed, 95 insertions(+), 78 deletions(-)

diff --git a/rt/abort+test.ha b/rt/abort+test.ha @@ -1,61 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// (c) Hare authors <https://harelang.org> - -// Signature for abort handler function. -export type abort_handler = fn( - path: *str, - line: u64, - col: u64, - msg: str, -) never; - -// Sets a new global runtime abort handler. -export fn onabort(handler: *abort_handler) void = { - return; // no-op on +test (XXX: Do something here?) -}; - -export type abort_reason = struct { - path: nullable *str, - line: u64, - col: u64, - msg: str, -}; - -export let jmp: nullable *jmp_buf = null; -export let reason: abort_reason = abort_reason { ... }; - -export @symbol("rt.abort") fn _abort( - path: *str, - line: u64, - col: u64, - msg: str, -) void = { - match (jmp) { - case let j: *jmp_buf => - reason = abort_reason { - path = path, - line = line, - col = col, - msg = msg, - }; - longjmp(j, 1); // test::status::ABORT - case null => - platform_abort(path, line, col, msg); - }; -}; - -// See harec:include/gen.h -const reasons: [_]str = [ - "slice or array access out of bounds", // 0 - "type assertion failed", // 1 - "out of memory", // 2 - "static insert/append exceeds slice capacity", // 3 - "execution reached unreachable code (compiler bug)", // 4 - "slice allocation capacity smaller than initializer", // 5 - "assertion failed", // 6 - "error occurred", // 7 -]; - -export fn abort_fixed(path: *str, line: u64, col: u64, i: u64) void = { - _abort(path, line, col, reasons[i]); -}; diff --git a/rt/abort.ha b/rt/abort.ha @@ -11,9 +11,11 @@ export type abort_handler = fn( let handle_abort: *abort_handler = &platform_abort; -// Sets a new global runtime abort handler. -export fn onabort(handler: *abort_handler) void = { +// Sets a new global runtime abort handler, returning the previous handler. +export fn onabort(handler: *abort_handler) *abort_handler = { + const prev = handle_abort; handle_abort = handler; + return prev; }; export @symbol("rt.abort") fn _abort( diff --git a/test/+test.ha b/test/+test.ha @@ -3,6 +3,8 @@ use ascii; use bufio; +use debug; +use debug::image; use encoding::hex; use encoding::utf8; use fmt; @@ -30,9 +32,17 @@ type status = enum { SEGV, }; +type abort_reason = struct { + path: nullable *str, + line: u64, + col: u64, + msg: str, +}; + type failure = struct { test: str, - reason: rt::abort_reason, + reason: abort_reason, + trace: u64, }; type skipped = struct { @@ -80,8 +90,6 @@ fn colored() bool = { && tty::isatty(os::stdout_file); }; -let jmp_buf = rt::jmp_buf { ... }; - const @symbol("__test_array_start") test_start: [*]test; const @symbol("__test_array_end") test_end: [*]test; @@ -140,6 +148,17 @@ export @symbol("__test_main") fn main() size = { }; if (len(ctx.failures) > 0) { + const image = match (image::self()) { + case let img: image::image => + yield img; + case => yield; + }; + defer match (&image) { + case let img: *image::image => + image::close(img); + case void => void; + }; + fmt::println("Failures:")!; for (let failure .. ctx.failures) { match (failure.reason.path) { @@ -155,6 +174,22 @@ export @symbol("__test_main") fn main() size = { failure.reason.col, failure.reason.msg)!; }; + + if (failure.trace == 0) continue; + + const image = match (&image) { + case let img: *image::image => + yield img; + case void => continue; + }; + + const trace = match (debug::trace_by_id(failure.trace)) { + case let frame: debug::stackframe => + yield frame; + case void => continue; + }; + + debug::backtrace(image, trace); }; fmt::println()!; }; @@ -243,10 +278,16 @@ fn run_test(ctx: *context, test: test) status = { let orig_stderr = os::stderr; os::stdout = &ctx.stdout; os::stderr = &ctx.stderr; - defer rt::jmp = null; + + trace = 0u64; + + default_abort = rt::onabort(&onabort); + defer rt::onabort(default_abort); + + defer jmp = null; const n = rt::setjmp(&jmp_buf): status; if (n == status::RETURN) { - rt::jmp = &jmp_buf; + jmp = &jmp_buf; test.func(); }; @@ -290,10 +331,11 @@ fn interpret_status(ctx: *context, test: str, status: status) bool = { styled_print(91, "FAIL"); append(ctx.failures, failure { test = test, - reason = rt::abort_reason { + reason = abort_reason { msg = "Expected test to abort", ... }, + trace = 0, }); return true; } else { @@ -308,7 +350,8 @@ fn interpret_status(ctx: *context, test: str, status: status) bool = { styled_print(91, "FAIL"); append(ctx.failures, failure { test = test, - reason = rt::reason, + reason = reason, + trace = trace, }); return true; }; @@ -316,17 +359,18 @@ fn interpret_status(ctx: *context, test: str, status: status) bool = { styled_print(37, "SKIP"); append(ctx.skipped, skipped { test = test, - reason = rt::reason.msg, + reason = reason.msg, }); return false; case status::SEGV => styled_print(91, "FAIL"); append(ctx.failures, failure { test = test, - reason = rt::abort_reason { + reason = abort_reason { msg = "Segmentation fault", ... }, + trace = trace, }); return true; }; @@ -340,10 +384,42 @@ fn styled_print(color: int, result: fmt::formattable) void = { }; }; +let jmp_buf = rt::jmp_buf { ... }; +let jmp: nullable *rt::jmp_buf = null; +let reason: abort_reason = abort_reason { ... }; +let trace = 0u64; +let default_abort = null: *rt::abort_handler; + +fn onabort( + path: *str, + line: u64, + col: u64, + msg: str, +) never = { + match (jmp) { + case let j: *rt::jmp_buf => + let frame = debug::walk(); + // Skip rt:: and test:: frames + frame = debug::next(frame) as debug::stackframe; + frame = debug::next(frame) as debug::stackframe; + + trace = debug::trace_store(frame); + reason = abort_reason { + path = path, + line = line, + col = col, + msg = msg, + }; + rt::longjmp(j, status::ABORT); + case null => + default_abort(path, line, col, msg); + }; +}; + fn handle_segv( sig: signal::sig, info: *signal::siginfo, - ucontext: *opaque, + uctx: *opaque, ) void = { rt::longjmp(&jmp_buf, status::SEGV); }; diff --git a/test/util+test.ha b/test/util+test.ha @@ -11,19 +11,19 @@ let want_abort = false; // Expect the currently running test to abort. The test will fail if it doesn't // abort. export fn expectabort() void = { - if (rt::jmp == null) { + if (jmp == null) { abort("Attempted to call test::expectabort outside of @test function"); }; want_abort = true; }; // Skip the currently running test. -export fn skip(reason: str) never = { - if (rt::jmp == null) { +export fn skip(why: str) never = { + if (jmp == null) { abort("Attempted to call test::skip outside of @test function"); }; - rt::reason = rt::abort_reason { - msg = reason, + reason = abort_reason { + msg = why, ... }; rt::longjmp(&jmp_buf, status::SKIP);