commit 576f6408009c08a2845b24e583e651acc90dcc63
parent 02f5e354e79f38e6a9663f6e938850739b54efad
Author: Sebastian <sebastian@sebsite.pw>
Date: Wed, 10 May 2023 17:35:15 -0400
Rewrite test runner
The test runner is moved to a new module named "test", which the build
driver links with when running `hare test`. The rewritten test driver
contains the following improvements:
- Liberal use of the rest of the stdlib, as opposed to needing to
reimplement everything in rt.
- stdout and stderr are captured on a per-test basis, and only displayed
at the very end for failing tests (invalid UTF-8 is displayed with
hex::dump).
- Each test is individually timed, in addition to the total time shown
at the end.
- Tests are matched by fnmatch, so it's now possible to, for instance,
run all tests in module foo by running `hare test 'foo::*'`.
- The old test runner didn't properly handle OOM conditions, since the
abort handler assumed that all aborts would come from failing tests.
This is now handled gracefully.
- os::exit is caught, resulting in a test failure and continuing other
tests.
- Segfaults are caught, resulting in a test failure and continuing other
tests.
It should also be much easier now to add to the test driver, since the
rest of the stdlib can be used.
Closes: https://todo.sr.ht/~sircmpwn/hare/520
Closes: https://todo.sr.ht/~sircmpwn/hare/188
Closes: https://todo.sr.ht/~sircmpwn/hare/204
Signed-off-by: Sebastian <sebastian@sebsite.pw>
Diffstat:
18 files changed, 392 insertions(+), 226 deletions(-)
diff --git a/Makefile b/Makefile
@@ -67,7 +67,7 @@ $(BINOUT)/hare-tests: $(TESTCACHE)/hare.o
@mkdir -p $(BINOUT)
@printf 'LD\t%s\n' "$@"
@$(LD) $(LDLINKFLAGS) -T $(rtscript) -o $@ \
- $(TESTCACHE)/hare.o $(testlib_deps_any) $(testlib_deps_$(PLATFORM))
+ $(testlib_deps_any) $(testlib_deps_$(PLATFORM))
$(BINOUT)/harec2: $(BINOUT)/hare $(harec_srcs)
@mkdir -p $(BINOUT)
diff --git a/cmd/hare/subcmds.ha b/cmd/hare/subcmds.ha
@@ -508,15 +508,6 @@ fn test(cmd: *getopt::command) void = {
};
};
- let input = "";
- let runargs: []str = [];
- if (len(cmd.args) == 0) {
- input = os::getcwd();
- } else {
- input = cmd.args[0];
- runargs = cmd.args[1..];
- };
-
if (len(libs) > 0) {
append(tags, module::tag {
mode = module::tag_mode::INCLUSIVE,
@@ -531,9 +522,9 @@ fn test(cmd: *getopt::command) void = {
defer plan_finish(&plan);
let depends: []*task = [];
- sched_module(&plan, ["rt"], &depends);
+ sched_module(&plan, ["test"], &depends);
- let items = match (module::walk(&ctx, input)) {
+ let items = match (module::walk(&ctx, ".")) {
case let items: []ast::ident =>
yield items;
case let err: module::error =>
@@ -570,13 +561,13 @@ fn test(cmd: *getopt::command) void = {
return;
};
- const cmd = match (exec::cmd(output, runargs...)) {
+ const cmd = match (exec::cmd(output, cmd.args...)) {
case let err: exec::error =>
fmt::fatal("exec:", exec::strerror(err));
case let cmd: exec::command =>
yield cmd;
};
- exec::setname(&cmd, input);
+ exec::setname(&cmd, os::getcwd());
exec::exec(&cmd);
};
diff --git a/docs/hare.scd b/docs/hare.scd
@@ -53,9 +53,10 @@ Hare program which is run. os::args[0] is set to the _path_ argument.
*hare test* compiles and runs tests for Hare code. All Hare modules in the
current working directory are recursively discovered, built, and their tests
-made eligible for the test run. If the _test_ argument is omitted, all tests are
-run. Otherwise, the list of named tests are run. *hare test* adds the +test and
-+debug tags to the default build tags.
+made eligible for the test run. If the _tests_ argument is omitted, all tests
+are run. Otherwise, each argument is interpreted as a *glob*(7) pattern, giving
+the names of the tests that should be run. *hare test* adds the +test tag to the
+default build tags.
*hare version* prints version information for the *hare* program. If *-v* is
supplied, it also prints information about the build parameters. The output
diff --git a/os/+freebsd/exit+test.ha b/os/+freebsd/exit+test.ha
@@ -0,0 +1,7 @@
+// License: MPL-2.0
+// (c) 2023 Sebastian <sebastian@sebsite.pw>
+
+// Exit the program with the provided status code.
+export @noreturn fn exit(status: int) void = {
+ abort("os::exit disabled in +test");
+};
diff --git a/os/+linux/exit+test.ha b/os/+linux/exit+test.ha
@@ -0,0 +1,7 @@
+// License: MPL-2.0
+// (c) 2023 Sebastian <sebastian@sebsite.pw>
+
+// Exit the program with the provided status code.
+export @noreturn fn exit(status: int) void = {
+ abort("os::exit disabled in +test");
+};
diff --git a/rt/+test/+freebsd.ha b/rt/+test/+freebsd.ha
@@ -1,21 +0,0 @@
-// License: MPL-2.0
-// (c) 2021 Drew DeVault <sir@cmpwn.com>
-
-let start: timespec = timespec { ... };
-
-fn time_start() void = {
- clock_gettime(CLOCK_MONOTONIC, &start) as void;
-};
-
-// Returns elapsed time as (seconds, milliseconds)
-fn time_stop() (size, size) = {
- let end: timespec = timespec { ... };
- clock_gettime(CLOCK_MONOTONIC, &end) as void;
- let sec_diff = end.tv_sec - start.tv_sec;
- let nsec_diff = end.tv_nsec - start.tv_nsec;
- if (nsec_diff < 0) {
- nsec_diff += 1000000000;
- sec_diff -= 1;
- };
- return (sec_diff: size, nsec_diff: size / 1000000z);
-};
diff --git a/rt/+test/+linux.ha b/rt/+test/+linux.ha
@@ -1,22 +0,0 @@
-// License: MPL-2.0
-// (c) 2021 Drew DeVault <sir@cmpwn.com>
-// (c) 2021 Ember Sawady <ecs@d2evs.net>
-
-let start: timespec = timespec { ... };
-
-fn time_start() void = {
- clock_gettime(CLOCK_MONOTONIC, &start) as void;
-};
-
-// Returns elapsed time as (seconds, milliseconds)
-fn time_stop() (size, size) = {
- let end: timespec = timespec { ... };
- clock_gettime(CLOCK_MONOTONIC, &end) as void;
- let sec_diff = end.tv_sec - start.tv_sec;
- let nsec_diff = end.tv_nsec - start.tv_nsec;
- if (nsec_diff < 0) {
- nsec_diff += 1000000000;
- sec_diff -= 1;
- };
- return (sec_diff: size, nsec_diff: size / 1000000z);
-};
diff --git a/rt/+test/cstring.ha b/rt/+test/cstring.ha
@@ -1,19 +0,0 @@
-// License: MPL-2.0
-// (c) 2021 Drew DeVault <sir@cmpwn.com>
-
-fn c_strlen(cstr: *const u8) size = {
- const ptr = cstr: *[*]u8;
- let ln = 0z;
- for (ptr[ln] != 0; ln += 1) void;
- return ln;
-};
-
-fn from_c_unsafe(cstr: *const u8) const str = {
- const l = c_strlen(cstr);
- const s = struct {
- data: *[*]u8 = cstr: *[*]u8,
- length: size = l,
- capacity: size = l + 1,
- };
- return *(&s: *const str);
-};
diff --git a/rt/+test/run.ha b/rt/+test/run.ha
@@ -1,125 +0,0 @@
-// License: MPL-2.0
-// (c) 2022 Bor Grošelj Simić <bor.groseljsimic@telemach.net>
-// (c) 2021 Drew DeVault <sir@cmpwn.com>
-// (c) 2021 Ember Sawady <ecs@d2evs.net>
-
-type test = struct {
- name: str,
- func: *fn() void,
-};
-
-type abort_reason = struct {
- loc: str,
- msg: str,
-};
-
-const @symbol("__test_array_start") test_start: [*]test;
-const @symbol("__test_array_end") test_end: [*]test;
-
-let jmp: jmpbuf = jmpbuf { ... };
-let reason: abort_reason = abort_reason { ... };
-
-export fn tests_main() size = {
- const ntest = (&test_end: uintptr - &test_start: uintptr): size / size(test);
- let maxname = 0z;
- for (let i = 0z; i < ntest; i += 1) {
- if (len(test_start[i].name) > maxname) {
- maxname = len(test_start[i].name);
- };
- };
-
- let failures: [](str, abort_reason) = [];
- let npass = 0z, nfail = 0z;
- let default_round = fegetround();
- print("Running ");
- print(ztos(ntest));
- print(" tests:\n\n");
- time_start();
- for (let i = 0z; i < ntest; i += 1) {
- if (!should_test(test_start[i].name)) {
- continue;
- };
- print(test_start[i].name);
- dots(maxname - len(test_start[i].name) + 3);
- print(" ");
-
- if (setjmp(&jmp) != 0) {
- nfail += 1;
- append(failures, (test_start[i].name, reason));
- print("FAIL\n");
- continue;
- };
-
- fesetround(default_round);
- feclearexcept(~0u);
-
- test_start[i].func();
-
- npass += 1;
- print("OK\n");
- };
- let end = time_stop();
-
- if (nfail != 0) {
- print("\n");
- print(ztos(nfail));
- if (nfail == 1) {
- print(" test failed:\n");
- } else {
- print(" tests failed:\n");
- };
- for (let i = 0z; i < nfail; i += 1) {
- print(failures[i].0);
- print(": ");
- if (len(failures[i].1.loc) != 0) {
- print(failures[i].1.loc);
- print(": ");
- };
- print(failures[i].1.msg);
- print("\n");
- };
- };
-
- print("\n");
- print(ztos(npass));
- print(" passed; ");
- print(ztos(nfail));
- print(" failed; ");
- print(ztos(ntest));
- print(" tests completed in ");
- print(ztos(end.0));
- print(".");
- if (end.1 < 10) {
- print("00");
- } else if (end.1 < 100) {
- print("0");
- };
- print(ztos(end.1));
- print("s\n");
-
- return nfail;
-};
-
-fn print(msg: str) void = {
- write(STDOUT_FILENO, *(&msg: **void): *const u8, len(msg))!;
-};
-
-fn dots(n: size) void = {
- // XXX: this is slow, I guess
- for (let i = 0z; i < n; i += 1) {
- print(".");
- };
-};
-
-fn should_test(name: str) bool = {
- if (argc == 1) {
- return true;
- };
- for (let i = 1z; i < argc; i += 1) {
- let s = from_c_unsafe(argv[i]);
- if (name == s) {
- return true;
- };
- };
- return false;
-};
diff --git a/rt/abort+test.ha b/rt/abort+test.ha
@@ -2,9 +2,22 @@
// (c) 2021 Drew DeVault <sir@cmpwn.com>
// (c) 2021 Ember Sawady <ecs@d2evs.net>
+export type abort_reason = struct {
+ loc: str,
+ msg: str,
+};
+
+export let jmp: nullable *jmpbuf = null;
+export let reason: abort_reason = abort_reason { ... };
+
export @noreturn @symbol("rt.abort") fn _abort(msg: str) void = {
- reason = abort_reason { loc = "", msg = msg };
- longjmp(&jmp, 1);
+ match (jmp) {
+ case let j: *jmpbuf =>
+ reason = abort_reason { loc = "", msg = msg };
+ longjmp(j, 1);
+ case null =>
+ platform_abort(msg);
+ };
};
// See harec:include/gen.h
@@ -18,6 +31,11 @@ const reasons: [_]str = [
];
export @noreturn fn abort_fixed(loc: str, i: int) void = {
- reason = abort_reason { loc = loc, msg = reasons[i] };
- longjmp(&jmp, 1);
+ match (jmp) {
+ case let j: *jmpbuf =>
+ reason = abort_reason { loc = loc, msg = reasons[i] };
+ longjmp(j, 1);
+ case null =>
+ platform_abort(reasons[i]);
+ };
};
diff --git a/rt/start+test+libc.ha b/rt/start+test+libc.ha
@@ -1,6 +1,8 @@
// License: MPL-2.0
// (c) 2021 Alexey Yerin <yyp@disroot.org>
+@symbol("__test_main") fn test_main() size;
+
export fn init() void = void;
const @symbol("__fini_array_start") fini_start: [*]*fn() void;
@@ -16,6 +18,6 @@ export fn fini() void = {
};
export @symbol("main") fn main() int = {
- const nfail = tests_main();
+ const nfail = test_main();
return if (nfail > 0) 1 else 0;
};
diff --git a/rt/start+test.ha b/rt/start+test.ha
@@ -1,6 +1,8 @@
// License: MPL-2.0
// (c) 2021 Drew DeVault <sir@cmpwn.com>
+@symbol("__test_main") fn test_main() size;
+
const @symbol("__init_array_start") init_start: [*]*fn() void;
const @symbol("__init_array_end") init_end: [*]*fn() void;
const @symbol("__fini_array_start") fini_start: [*]*fn() void;
@@ -26,7 +28,7 @@ export fn fini() void = {
export @noreturn fn start_ha() void = {
init();
- let nfail = tests_main();
+ const nfail = test_main();
fini();
exit(if (nfail > 0) 1 else 0);
};
diff --git a/scripts/gen-stdlib b/scripts/gen-stdlib
@@ -81,11 +81,8 @@ rt() {
start.ha
else
gensrcs_rt \
- start+test.ha \
abort+test.ha \
- '+test/+$(PLATFORM).ha' \
- +test/cstring.ha \
- +test/run.ha \
+ start+test.ha \
+test/signal.ha \
+test/ztos.ha
fi
@@ -157,6 +154,17 @@ ${stdlib}_deps_any += \$(${stdlib}_rt)
EOF
}
+test() {
+ if [ $testing -eq 0 ]; then
+ gen_srcs test common.ha
+ gen_ssa test
+ else
+ gen_srcs test common.ha +test.ha fail+test.ha
+ gen_ssa test bufio encoding::hex encoding::utf8 fmt fnmatch io \
+ os rt strings strio time unix::signal
+ fi
+}
+
ascii() {
gen_srcs ascii \
ctype.ha \
@@ -1168,10 +1176,17 @@ math_random() {
}
os() {
+ if [ $testing -eq 0 ]
+ then
+ exit=exit.ha
+ else
+ exit=exit+test.ha
+ fi
+
gen_srcs -plinux os \
+linux/dirfdfs.ha \
+linux/environ.ha \
- +linux/exit.ha \
+ +linux/$exit \
+linux/fs.ha \
+linux/memory.ha \
+linux/stdfd.ha \
@@ -1181,7 +1196,7 @@ os() {
gen_srcs -pfreebsd os \
+freebsd/environ.ha \
- +freebsd/exit.ha \
+ +freebsd/$exit \
+freebsd/dirfdfs.ha \
+freebsd/stdfd.ha \
+freebsd/fs.ha \
@@ -1599,6 +1614,7 @@ strings
strings::template
strio
temp linux freebsd
+test
time linux freebsd
time::chrono linux freebsd
types
diff --git a/scripts/install-mods b/scripts/install-mods
@@ -33,6 +33,7 @@ strconv
strings
strio
temp
+test
time
types
unix
diff --git a/stdlib.mk b/stdlib.mk
@@ -670,6 +670,12 @@ stdlib_deps_linux += $(stdlib_temp_linux)
stdlib_temp_freebsd = $(HARECACHE)/temp/temp-freebsd.o
stdlib_deps_freebsd += $(stdlib_temp_freebsd)
+# gen_lib test (any)
+stdlib_test_any = $(HARECACHE)/test/test-any.o
+stdlib_deps_any += $(stdlib_test_any)
+stdlib_test_linux = $(stdlib_test_any)
+stdlib_test_freebsd = $(stdlib_test_any)
+
# gen_lib time (linux)
stdlib_time_linux = $(HARECACHE)/time/time-linux.o
stdlib_deps_linux += $(stdlib_time_linux)
@@ -1995,6 +2001,16 @@ $(HARECACHE)/temp/temp-freebsd.ssa: $(stdlib_temp_freebsd_srcs) $(stdlib_rt) $(s
@HARECACHE=$(HARECACHE) $(HAREC) $(HAREFLAGS) -o $@ -Ntemp \
-t$(HARECACHE)/temp/temp.td $(stdlib_temp_freebsd_srcs)
+# test (+any)
+stdlib_test_any_srcs = \
+ $(STDLIB)/test/common.ha
+
+$(HARECACHE)/test/test-any.ssa: $(stdlib_test_any_srcs) $(stdlib_rt)
+ @printf 'HAREC \t$@\n'
+ @mkdir -p $(HARECACHE)/test
+ @HARECACHE=$(HARECACHE) $(HAREC) $(HAREFLAGS) -o $@ -Ntest \
+ -t$(HARECACHE)/test/test.td $(stdlib_test_any_srcs)
+
# time (+linux)
stdlib_time_linux_srcs = \
$(STDLIB)/time/+linux/functions.ha \
@@ -2278,11 +2294,8 @@ testlib_rt_linux_srcs = \
$(STDLIB)/rt/memmove.ha \
$(STDLIB)/rt/memset.ha \
$(STDLIB)/rt/strcmp.ha \
- $(STDLIB)/rt/start+test.ha \
$(STDLIB)/rt/abort+test.ha \
- $(STDLIB)/rt/+test/+$(PLATFORM).ha \
- $(STDLIB)/rt/+test/cstring.ha \
- $(STDLIB)/rt/+test/run.ha \
+ $(STDLIB)/rt/start+test.ha \
$(STDLIB)/rt/+test/signal.ha \
$(STDLIB)/rt/+test/ztos.ha
@@ -2310,11 +2323,8 @@ testlib_rt_freebsd_srcs = \
$(STDLIB)/rt/memmove.ha \
$(STDLIB)/rt/memset.ha \
$(STDLIB)/rt/strcmp.ha \
- $(STDLIB)/rt/start+test.ha \
$(STDLIB)/rt/abort+test.ha \
- $(STDLIB)/rt/+test/+$(PLATFORM).ha \
- $(STDLIB)/rt/+test/cstring.ha \
- $(STDLIB)/rt/+test/run.ha \
+ $(STDLIB)/rt/start+test.ha \
$(STDLIB)/rt/+test/signal.ha \
$(STDLIB)/rt/+test/ztos.ha
@@ -2930,6 +2940,12 @@ testlib_deps_linux += $(testlib_temp_linux)
testlib_temp_freebsd = $(TESTCACHE)/temp/temp-freebsd.o
testlib_deps_freebsd += $(testlib_temp_freebsd)
+# gen_lib test (any)
+testlib_test_any = $(TESTCACHE)/test/test-any.o
+testlib_deps_any += $(testlib_test_any)
+testlib_test_linux = $(testlib_test_any)
+testlib_test_freebsd = $(testlib_test_any)
+
# gen_lib time (linux)
testlib_time_linux = $(TESTCACHE)/time/time-linux.o
testlib_deps_linux += $(testlib_time_linux)
@@ -4111,7 +4127,7 @@ $(TESTCACHE)/net/uri/net_uri-any.ssa: $(testlib_net_uri_any_srcs) $(testlib_rt)
testlib_os_linux_srcs = \
$(STDLIB)/os/+linux/dirfdfs.ha \
$(STDLIB)/os/+linux/environ.ha \
- $(STDLIB)/os/+linux/exit.ha \
+ $(STDLIB)/os/+linux/exit+test.ha \
$(STDLIB)/os/+linux/fs.ha \
$(STDLIB)/os/+linux/memory.ha \
$(STDLIB)/os/+linux/stdfd.ha \
@@ -4126,7 +4142,7 @@ $(TESTCACHE)/os/os-linux.ssa: $(testlib_os_linux_srcs) $(testlib_rt) $(testlib_i
# os (+freebsd)
testlib_os_freebsd_srcs = \
$(STDLIB)/os/+freebsd/environ.ha \
- $(STDLIB)/os/+freebsd/exit.ha \
+ $(STDLIB)/os/+freebsd/exit+test.ha \
$(STDLIB)/os/+freebsd/dirfdfs.ha \
$(STDLIB)/os/+freebsd/stdfd.ha \
$(STDLIB)/os/+freebsd/fs.ha \
@@ -4316,6 +4332,18 @@ $(TESTCACHE)/temp/temp-freebsd.ssa: $(testlib_temp_freebsd_srcs) $(testlib_rt) $
@HARECACHE=$(TESTCACHE) $(HAREC) $(TESTHAREFLAGS) -o $@ -Ntemp \
-t$(TESTCACHE)/temp/temp.td $(testlib_temp_freebsd_srcs)
+# test (+any)
+testlib_test_any_srcs = \
+ $(STDLIB)/test/common.ha \
+ $(STDLIB)/test/+test.ha \
+ $(STDLIB)/test/fail+test.ha
+
+$(TESTCACHE)/test/test-any.ssa: $(testlib_test_any_srcs) $(testlib_rt) $(testlib_bufio_$(PLATFORM)) $(testlib_encoding_hex_$(PLATFORM)) $(testlib_encoding_utf8_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_fnmatch_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_os_$(PLATFORM)) $(testlib_rt_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_strio_$(PLATFORM)) $(testlib_time_$(PLATFORM)) $(testlib_unix_signal_$(PLATFORM))
+ @printf 'HAREC \t$@\n'
+ @mkdir -p $(TESTCACHE)/test
+ @HARECACHE=$(TESTCACHE) $(HAREC) $(TESTHAREFLAGS) -o $@ -Ntest \
+ -t$(TESTCACHE)/test/test.td $(testlib_test_any_srcs)
+
# time (+linux)
testlib_time_linux_srcs = \
$(STDLIB)/time/+linux/functions.ha \
diff --git a/test/+test.ha b/test/+test.ha
@@ -0,0 +1,264 @@
+use bufio;
+use encoding::hex;
+use encoding::utf8;
+use fmt;
+use fnmatch;
+use io;
+use os;
+use rt;
+use strings;
+use strio;
+use time;
+use unix::signal;
+
+type test = struct {
+ name: str,
+ func: *fn() void,
+};
+
+type failure = struct {
+ test: str,
+ reason: rt::abort_reason,
+};
+
+type output = struct {
+ test: str,
+ stdout: str,
+ stderr: str,
+};
+
+fn finish_output(output: *output) void = {
+ free(output.stdout);
+ free(output.stderr);
+};
+
+type context = struct {
+ stdout: bufio::memstream,
+ stderr: bufio::memstream,
+ failures: []failure,
+ output: []output,
+ maxname: size,
+ total_time: time::duration,
+ default_round: uint,
+};
+
+fn finish_context(ctx: *context) void = {
+ io::close(&ctx.stdout)!;
+ io::close(&ctx.stderr)!;
+ free(ctx.failures);
+ for (let i = 0z; i < len(ctx.output); i += 1) {
+ finish_output(&ctx.output[i]);
+ };
+ free(ctx.output);
+};
+
+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];
+ let enabled_tests: []test = [];
+ defer free(enabled_tests);
+ if (len(os::args) == 1) {
+ append(enabled_tests, tests...);
+ } else for (let i = 0z; i < ntest; i += 1) {
+ for (let j = 1z; j < len(os::args); j += 1) {
+ if (fnmatch::fnmatch(os::args[j], tests[i].name)) {
+ append(enabled_tests, tests[i]);
+ break;
+ };
+ };
+ };
+
+ let maxname = 0z;
+ for (let i = 0z; i < len(enabled_tests); i += 1) {
+ if (len(enabled_tests[i].name) > maxname) {
+ maxname = len(enabled_tests[i].name);
+ };
+ };
+
+ let ctx = context {
+ stdout = bufio::dynamic(io::mode::WRITE),
+ stderr = bufio::dynamic(io::mode::WRITE),
+ maxname = maxname,
+ default_round = rt::fegetround(),
+ ...
+ };
+ defer finish_context(&ctx);
+
+ fmt::printfln("Running {}/{} tests:\n", len(enabled_tests), ntest)!;
+ for (let i = 0z; i < len(enabled_tests); i += 1) {
+ do_test(&ctx, enabled_tests[i]);
+ };
+ fmt::println()!;
+
+ if (len(ctx.failures) > 0) {
+ fmt::println("Failures:")!;
+ for (let i = 0z; i < len(ctx.failures); i += 1) {
+ fmt::printfln("{}: {}", ctx.failures[i].test,
+ ctx.failures[i].reason.msg)!;
+ };
+ fmt::println()!;
+ };
+
+ for (let i = 0z; i < len(ctx.output); i += 1) {
+ if (ctx.output[i].stdout != "") {
+ fmt::println(ctx.output[i].test, "stdout:")!;
+ fmt::println(ctx.output[i].stdout)!;
+ };
+ if (ctx.output[i].stderr != "") {
+ fmt::println(ctx.output[i].test, "stderr:")!;
+ fmt::println(ctx.output[i].stderr)!;
+ };
+ if (i == len(ctx.output) - 1) {
+ fmt::println()!;
+ };
+ };
+
+ // XXX: revisit once time::format_duration is implemented
+ 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",
+ len(enabled_tests) - len(ctx.failures),
+ if (len(ctx.failures) > 0) "91" else "37",
+ len(ctx.failures),
+ len(enabled_tests),
+ ctx.total_time / 1000000000,
+ ctx.total_time % 1000000000)!;
+
+ return len(ctx.failures);
+};
+
+fn do_test(ctx: *context, test: test) void = {
+ signal::handle(signal::SIGSEGV, &handle_segv, signal::flags::NODEFER);
+ bufio::reset(&ctx.stdout);
+ bufio::reset(&ctx.stderr);
+
+ 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 end_time = time::now(time::clock::MONOTONIC);
+ const time_diff = time::diff(start_time, end_time);
+ assert(time_diff >= 0);
+ ctx.total_time += time_diff;
+ fmt::printfln(" in {}.{:09}s",
+ time_diff / 1000000000,
+ time_diff % 1000000000)!;
+
+ const stdout = bufio::buffer(&ctx.stdout);
+ const stdout = match (strings::fromutf8(stdout)) {
+ case let s: str =>
+ yield strings::dup(s);
+ case utf8::invalid =>
+ let s = strio::dynamic();
+ hex::dump(&s, stdout)!;
+ yield strio::string(&s);
+ };
+ const stderr = bufio::buffer(&ctx.stderr);
+ const stderr = match (strings::fromutf8(stderr)) {
+ case let s: str =>
+ yield strings::dup(s);
+ case utf8::invalid =>
+ let s = strio::dynamic();
+ hex::dump(&s, stderr)!;
+ yield strio::string(&s);
+ };
+ if (failed && (stdout != "" || stderr != "")) {
+ append(ctx.output, output {
+ test = test.name,
+ stdout = stdout,
+ stderr = stderr,
+ });
+ };
+
+ rt::fesetround(ctx.default_round);
+ rt::feclearexcept(~0u);
+ signal::resetall();
+};
+
+fn run_test(ctx: *context, test: test) (void | failure) = {
+ fmt::print(test.name)!;
+ dots(ctx.maxname - len(test.name) + 3);
+ bufio::flush(os::stdout)!; // write test name before test runs
+
+ let orig_stdout = os::stdout;
+ let orig_stderr = os::stderr;
+ 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);
+ };
+ rt::jmp = &jmpbuf;
+
+ test.func();
+ os::stdout = orig_stdout;
+ os::stderr = orig_stderr;
+ if (want_abort) {
+ want_abort = false;
+ return fail(test, 1);
+ };
+ pass();
+};
+
+fn pass() void = {
+ fmt::print("\x1b[92m" "PASS" "\x1b[m")!;
+};
+
+fn fail(test: test, n: int) failure = {
+ fmt::print("\x1b[91m" "FAIL" "\x1b[m")!;
+ switch (n) {
+ case 1 =>
+ // assertion failed
+ return failure {
+ test = test.name,
+ reason = rt::reason,
+ };
+ case 2 =>
+ // segmentation fault
+ return failure {
+ test = test.name,
+ reason = rt::abort_reason {
+ loc = "",
+ msg = "Segmentation fault",
+ },
+ };
+ case =>
+ // unrecognized failure
+ return failure {
+ test = test.name,
+ reason = rt::abort_reason {
+ loc = "",
+ msg = "Reason unknown",
+ },
+ };
+ };
+};
+
+fn dots(n: size) void = {
+ for (let i = 0z; i < n; i += 1) {
+ fmt::print(".")!;
+ };
+};
+
+fn handle_segv(sig: int, info: *signal::siginfo, ucontext: *void) void = {
+ rt::longjmp(&jmpbuf, 2);
+};
diff --git a/test/common.ha b/test/common.ha
@@ -0,0 +1,5 @@
+let want_abort = false;
+
+// Expect the currently running test to abort. The test will fail if it doesn't
+// abort.
+export fn expectabort() void = want_abort = true;
diff --git a/test/fail+test.ha b/test/fail+test.ha
@@ -0,0 +1,11 @@
+use os;
+
+@test fn _abort() void = {
+ expectabort();
+ abort("Intentional failure");
+};
+
+@test fn exit() void = {
+ expectabort();
+ os::exit(1);
+};