~sircmpwn/hare-dev

hare: Report the backtraces on tests failures v1 NEEDS REVISION

Willow Barraco: 1
 Report the backtraces on tests failures

 4 files changed, 35 insertions(+), 72 deletions(-)
#1292252 alpine.yml success
#1292253 freebsd.yml failed
#1292254 netbsd.yml failed
#1292255 openbsd.yml failed
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~sircmpwn/hare-dev/patches/54258/mbox | git am -3
Learn more about email & git

[PATCH hare] Report the backtraces on tests failures Export this patch

This is a minimum patch to stderr the backtrace while running the tests.

We basically backport the global management from rt/abort+test to
debug/abort.

debug is now always included while building for tests, even when tests
are run with the release mode. But I think it is completely ok.

We could iterate over this, if we want to report the backtrace in a
dedicated output part by example. This would require more globals, and
some other minimal changes.

Signed-off-by: Willow Barraco <contact@willowbarraco.fr>
---
 debug/abort.ha    | 24 ++++++++++++++++++-
 rt/abort+test.ha  | 61 -----------------------------------------------
 test/+test.ha     | 15 ++++++------
 test/util+test.ha |  7 +++---
 4 files changed, 35 insertions(+), 72 deletions(-)
 delete mode 100644 rt/abort+test.ha

diff --git a/debug/abort.ha b/debug/abort.ha
index 3b52f260..c4d005d5 100644
--- a/debug/abort.ha
+++ b/debug/abort.ha
@@ -5,6 +5,16 @@ use debug::image;
use fmt;
use rt;

export type abort_reason = struct {
	path: nullable *str,
	line: u64,
	col: u64,
	msg: str,
};

export let jmp: nullable *rt::jmpbuf = null;
export let reason: abort_reason = abort_reason { ... };

@init fn init_abort() void = {
	rt::onabort(&debug_abort);
};
@@ -36,7 +46,19 @@ fn debug_abort(
	};

	backtrace(&self, frame);
	halt();

	match (jmp) {
	case let j: *rt::jmpbuf =>
		reason = abort_reason {
			path = path,
			line = line,
			col = col,
			msg = msg,
		};
		rt::longjmp(j, 1); // test::status::ABORT
	case null =>
		halt();
	};
};

fn halt() never = {
diff --git a/rt/abort+test.ha b/rt/abort+test.ha
deleted file mode 100644
index c38f88f8..00000000
--- a/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 *jmpbuf = 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: *jmpbuf =>
		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/test/+test.ha b/test/+test.ha
index 6863f6ba..ef10f613 100644
--- a/test/+test.ha
+++ b/test/+test.ha
@@ -15,6 +15,7 @@ use strings;
use time;
use unix::signal;
use unix::tty;
use debug;

type test = struct {
	name: str,
@@ -31,7 +32,7 @@ type status = enum {

type failure = struct {
	test: str,
	reason: rt::abort_reason,
	reason: debug::abort_reason,
};

type skipped = struct {
@@ -242,10 +243,10 @@ 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;
	defer debug::jmp = null;
	const n = rt::setjmp(&jmpbuf): status;
	if (n == status::RETURN) {
		rt::jmp = &jmpbuf;
		debug::jmp = &jmpbuf;
		test.func();
	};

@@ -289,7 +290,7 @@ 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 = debug::abort_reason {
					msg = "Expected test to abort",
					...
				},
@@ -307,7 +308,7 @@ fn interpret_status(ctx: *context, test: str, status: status) bool = {
			styled_print(91, "FAIL");
			append(ctx.failures, failure {
				test = test,
				reason = rt::reason,
				reason = debug::reason,
			});
			return true;
		};
@@ -315,14 +316,14 @@ 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 = debug::reason.msg,
		});
		return false;
	case status::SEGV =>
		styled_print(91, "FAIL");
		append(ctx.failures, failure {
			test = test,
			reason = rt::abort_reason {
			reason = debug::abort_reason {
				msg = "Segmentation fault",
				...
			},
diff --git a/test/util+test.ha b/test/util+test.ha
index 353de281..6177d9fe 100644
--- a/test/util+test.ha
+++ b/test/util+test.ha
@@ -5,13 +5,14 @@ use fmt;
use os;
use rt;
use strings;
use debug;

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 (debug::jmp == null) {
		abort("Attempted to call test::expectabort outside of @test function");
	};
	want_abort = true;
@@ -19,10 +20,10 @@ export fn expectabort() void = {

// Skip the currently running test.
export fn skip(reason: str) never = {
	if (rt::jmp == null) {
	if (debug::jmp == null) {
		abort("Attempted to call test::skip outside of @test function");
	};
	rt::reason = rt::abort_reason {
	debug::reason = debug::abort_reason {
		msg = reason,
		...
	};
-- 
2.46.0
hare/patches: FAILED in 5m35s

[Report the backtraces on tests failures][0] from [Willow Barraco][1]

[0]: https://lists.sr.ht/~sircmpwn/hare-dev/patches/54258
[1]: mailto:contact@willowbarraco.fr

✗ #1292255 FAILED  hare/patches/openbsd.yml https://builds.sr.ht/~sircmpwn/job/1292255
✗ #1292254 FAILED  hare/patches/netbsd.yml  https://builds.sr.ht/~sircmpwn/job/1292254
✓ #1292252 SUCCESS hare/patches/alpine.yml  https://builds.sr.ht/~sircmpwn/job/1292252
✗ #1292253 FAILED  hare/patches/freebsd.yml https://builds.sr.ht/~sircmpwn/job/1292253
I think I would rather take an approach where we drop rt+test and
instead have test:: register its own abort handler which does the
longjmp and taps debug:: to retrieve a backtrace, storing it to be
printed with the failed test summary at the end.