hare

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

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:
MMakefile | 2+-
Mcmd/hare/subcmds.ha | 17++++-------------
Mdocs/hare.scd | 7++++---
Aos/+freebsd/exit+test.ha | 7+++++++
Aos/+linux/exit+test.ha | 7+++++++
Drt/+test/+freebsd.ha | 21---------------------
Drt/+test/+linux.ha | 22----------------------
Drt/+test/cstring.ha | 19-------------------
Drt/+test/run.ha | 125-------------------------------------------------------------------------------
Mrt/abort+test.ha | 26++++++++++++++++++++++----
Mrt/start+test+libc.ha | 4+++-
Mrt/start+test.ha | 4+++-
Mscripts/gen-stdlib | 28++++++++++++++++++++++------
Mscripts/install-mods | 1+
Mstdlib.mk | 48++++++++++++++++++++++++++++++++++++++----------
Atest/+test.ha | 264+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/common.ha | 5+++++
Atest/fail+test.ha | 11+++++++++++
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); +};