From 8bc7a622cc5bb77a86e1be5a188939854e9d51c7 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 7 Aug 2026 23:00:57 +0900 Subject: [PATCH] test: fork-per-test runtime Each @test forks into its own process group with a nonblocking control pipe carrying framed ready/complete/skip/expect-abort records; timeouts escalate SIGTERM then SIGKILL over the group. New exports skip, expectabort, current; new flags -list, -timeout-ms=, -package=. A clean exit without a completion frame is a harness error, and the exit code is 0/1 with a discovered/selected/started/completed accounting line. toktest drops its fixed /tmp path for temp.named so parallel invocations cannot collide. --- lib/test/run.ww | 714 ++++++++++++++++++++++++++++++++------- lib/ww/syntax/toktest.ww | 11 +- 2 files changed, 608 insertions(+), 117 deletions(-) diff --git a/lib/test/run.ww b/lib/test/run.ww index 3bb5bcca..76f1641b 100644 --- a/lib/test/run.ww +++ b/lib/test/run.ww @@ -1,148 +1,634 @@ package test; -// Plan-9-lean port of Hare's @test runner (ref/hare/test/+test.ha -// __test_main:97-115, run_test:284, do_test:250). MECHANISM DIVERGENCE -// (drew-17-attest-spec.md §a; rob ruling 2026-06-10, task #17): Hare -// isolates each test in ONE process via arch::setjmp + an rt::onabort -// hook + a SIGSEGV signal handler; ww has none of those primitives, so -// we fork per test and read the child's wait-status. A clean child -// exit(0) is a pass; abort/div0/SIGSEGV/nonzero-exit all surface as a -// failing child status, so the fork boundary catches EVERY fault class -// the setjmp+signal path catches, at a process boundary. The proven -// fork+wait4+WEXITSTATUS decode is procrun (selfhost/cmd/ww/main.ww: -// 154-177). CONSEQUENCE (sanctioned): module globals do NOT persist -// test-to-test — each test runs in a fresh fork snapshot. Hare's tests -// share one process, so its globals persist; ww's are hermetic. -// -// D1/D4/D5 reductions also retained from the -T synth era: no -// __test_array linker section (the synth hands us a value table), no -// sort, no file:line reflection. -// -// fnmatch NAME-FILTER (task #17 commit-3, drew spec §d): when the test -// binary is invoked with trailing argv (the driver passes `ww test -// [pattern]` through as argv[1..]), each pattern is a shell glob -// matched against the test name via [[fnmatch.fnmatch]]; a test runs iff -// some pattern matches. No pattern (argv.len == 1) runs ALL tests — the -// no-filter path is byte-for-byte the pre-filter behavior. Zero matches -// prints "No tests run" and exits 0, mirroring Hare's __test_main -// (ref/hare/test/+test.ha:104-117). The synth (`run(__wwtests)`) and the -// value table are UNCHANGED — the pattern reaches us via os.args, so -// 990-997 byte-id and the no-pattern path hold. -// -// BUNDLE-FLOOR CONSEQUENCE (sanctioned, task #17): importing fnmatch -// raises lib/test's auto-bundle floor from os-only to -// os+fnmatch+ascii+strings (+their transitive bytes/encoding.utf8/types/ -// rt) in EVERY -T build. fnmatch's module-private symbols co-bundle into -// the same flat "" scope as the test's modules (pre-#8, no package -// isolation); a same-named module-private fn in a tested module would -// collide. Accepted as the cost of name-filtering; #8 (real packages) -// retires it. +// WW uses one fork per test because it does not yet have Hare's +// setjmp/onabort machinery (ref/hare/test/+test.ha:250-322). The pipe +// makes a normal return distinguishable from a test calling os.exit(0). import fnmatch; import os; +import time; -// run — execute each test in `tests` in a forked subprocess, report a -// per-test status line, and return the failure count (the synthesized -// `export fn main()` returns this, so the process exits nonzero iff any -// test failed). The table is `[](str, *fn() void)` — Hare models a test -// as `struct { name: str, func: *fn() void }` (+test.ha:23-26); the -T -// synth emits the value-table tuple form (rob ruling), so the runner -// reads `.0`/`.1` off each row rather than named fields. -// -// The private helpers carry a `tst` prefix: until real packages (#8) give -// lib/test symbol isolation, every -T build flat-bundles this module into -// the SAME bare-leaf scope as the test's own modules, so an un-prefixed -// `puts`/`runone` collides with a same-named module-private fn (lib/dirs -// and lib/temp both ship a private `puts(off, s)`). `run` is the bound -// runner name (the synth's callee), reached module-qualified as `test.run`: -// the -T synth prepends `use test;` before name-binding (#80), so this -// runner keys into the `test` module namespace — a symbol distinct from any -// bare user `fn run` in the @test unit. The two COEXIST: `run` and -// `test.run` are separate symbols (ref/hare/test/+test.ha:97 — the runner -// is its own `test` module), so a user `fn run` is no longer a duplicate and -// both stages accept it under @test/-T. +def TST_PASS: i32 = 0; +def TST_FAIL: i32 = 1; +def TST_SKIP: i32 = 2; +def TST_HARNESS: i32 = 3; + +def TST_COMPLETE: u8 = 80u8; // P +def TST_SKIPPED: u8 = 83u8; // S +def TST_EXPECT_ABORT: u8 = 65u8; // A +def TST_GROUP_READY: u8 = 71u8; // G +def TST_CONTROL_MAX: i32 = 4096; +def TST_FRAME_HEADER: i32 = 3; +def TST_EINTR: i64 = -4i64; +def TST_EAGAIN: i64 = -11i64; +def TST_ESRCH: i32 = -3; +def TST_DEFAULT_TIMEOUT_MS: i64 = 30000i64; +def TST_MAX_TIMEOUT_MS: i64 = 3600000i64; +def TST_POLL_MS: i64 = 1i64; +def TST_TERM_GRACE_MS: i64 = 100i64; +def TST_TIMEOUT_PREFIX_LEN: i32 = 12; +def TST_PACKAGE_PREFIX_LEN: i32 = 9; + +type tstresult = struct { + kind: i32, + completed: bool, + timedout: bool, + exited: bool, + detail: i32, + control: [TST_CONTROL_MAX]u8, + ncontrol: i32, +}; + +let tstactive: bool = false; +let tstcontrolfd: i32 = -1; +let tstname: str; +let tstwantabort: bool = false; + +// run preserves the compiler-generated [](str, *fn() void) descriptor. +// Package assembly can therefore aggregate tests without another registry +// format or a compiler invocation per assertion. export fn run(tests: [](str, *fn() void)) i32 = { let av: []str = os.args(); - let nfail: i32 = 0; - let nrun: i32 = 0; + let list: bool = tstlistmode(av); + let timeoutms: i64 = tsttimeout(av); + let pkgprefix: str = ""; + if (timeoutms <= 0) { + tstputs("test: invalid or duplicate -timeout-ms option\n"); + return 1; + }; + if (!tstpackage(av, &pkgprefix)) { + tstputs("test: invalid or duplicate -package option\n"); + return 1; + }; + let selected: i32 = 0; + let started: i32 = 0; + let completed: i32 = 0; + let passed: i32 = 0; + let failed: i32 = 0; + let skipped: i32 = 0; + let harness: i32 = 0; let i: i32 = 0; for (i < tests.len) { let row: (str, *fn() void) = tests[i]; - let name: str = row.0; - let f: *fn() void = row.1; - if (tstenabled(name, av)) { + let name: str = tstqualified(pkgprefix, row.0); + if (!tstenabled(name, av)) { i += 1; continue; }; + selected += 1; + if (list) { tstputs(name); - tstputs(" ... "); - let st: i32 = tstrunone(f); - if (st == 0) { - tstputs("ok\n"); - } else { - tstputs("FAIL\n"); - nfail += 1; - }; - nrun += 1; + tstputs("\n"); + i += 1; + continue; + }; + + tstputs(name); + tstputs(" ... "); + let result: tstresult; + if (!tstrunone(row.1, name, timeoutms, &result)) { + harness += 1; + tstputs("HARNESS (could not start)\n"); + i += 1; + continue; + }; + started += 1; + if (result.completed) { completed += 1; }; + switch (result.kind) { + case TST_PASS: + passed += 1; + tstputs("ok\n"); + case TST_SKIP: + skipped += 1; + tstputs("SKIP: "); + tstfdwriteall(os.STDOUT_FILENO, + &result.control[TST_FRAME_HEADER], + (result.ncontrol - TST_FRAME_HEADER): u64); + tstputs("\n"); + case TST_FAIL: + failed += 1; + tstputfailure(&result); + case TST_HARNESS: + harness += 1; + tstputharness(&result); }; i += 1; }; - // Zero tests selected (an over-narrow pattern, or none matched): Hare - // prints "No tests run" and returns 0 regardless of whether a pattern - // was given (ref/hare/test/+test.ha:114-117). - if (nrun == 0) { - tstputs("No tests run\n"); + + if (list) { + if (selected == 0 && pkgprefix.len != 0) { + tstputs("[no matches]\n"); + }; return 0; }; - tstputuint(nrun - nfail); + if (selected == 0) { + if (pkgprefix.len != 0) { tstputs("[no matches]\n"); } + else { tstputs("No tests run\n"); }; + tstputaccounting(tests.len, 0, 0, 0); + return 0; + }; + tstputuint(passed); tstputs(" passed, "); - tstputuint(nfail); - tstputs(" failed\n"); - return nfail; + tstputuint(failed); + tstputs(" failed, "); + tstputuint(skipped); + tstputs(" skipped, "); + tstputuint(harness); + tstputs(" harness errors\n"); + tstputaccounting(tests.len, selected, started, completed); + if (failed != 0 || harness != 0 || started != selected || + completed != started) { + return 1; + }; + return 0; }; -// tstenabled — does `name` pass the name-filter? `av` is os.args(): -// av[0] is the program name, av[1..] the glob patterns the driver -// forwarded. No patterns (av.len <= 1) enables every test; otherwise a -// test is enabled iff SOME pattern fnmatch-matches (Hare's OR-over-args, -// ref/hare/test/+test.ha:106-113). DIVERGENCE: Hare iterates all of -// os::args including [0]; ww skips [0] — the test binary path is never a -// test-name pattern (the driver passes the pattern as the first -// post-program arg, never argv[0]). -fn tstenabled(name: str, av: []str) bool = { - if (av.len <= 1) { return true; }; - let j: i32 = 1; - for (j < av.len) { - if (fnmatch.fnmatch(av[j], name, fnmatch.flag.NONE)) { - return true; +// skip and expectabort are records rather than exit-code conventions: an +// exit status alone cannot say whether user code returned, skipped, or died. +export fn skip(reason: str) never = { + if (!tstactive) { abort("test.skip called outside a test"); }; + if (reason.len == 0 || reason.len > TST_CONTROL_MAX - TST_FRAME_HEADER) { + tstwriteframe(0u8, ""); + os.exit(0); + }; + tstwriteframe(TST_SKIPPED, reason); + os.close(tstcontrolfd); + os.exit(0); +}; + +export fn expectabort() void = { + if (!tstactive) { abort("test.expectabort called outside a test"); }; + if (!tstwantabort) { + tstwriteframe(TST_EXPECT_ABORT, ""); + tstwantabort = true; + }; +}; + +export fn current() str = { + if (!tstactive) { abort("test.current called outside a test"); }; + return tstname; +}; + +fn tstlistarg(s: str) bool = { + return s == "-list" || s == "--list"; +}; + +fn tsttimeoutarg(s: str) bool = { + let prefix: str = "-timeout-ms="; + if (s.len < prefix.len) { return false; }; + let i: i32 = 0; + for (i < prefix.len) { + if (s[i] != prefix[i]) { return false; }; + i += 1; + }; + return true; +}; + +fn tstpackagearg(s: str) bool = { + let prefix: str = "-package="; + if (s.len < prefix.len) { return false; }; + let i: i32 = 0; + for (i < prefix.len) { + if (s[i] != prefix[i]) { return false; }; + i += 1; + }; + return true; +}; + +fn tstpackage(av: []str, out: *str) bool = { + let seen: bool = false; + let i: i32 = 1; + for (i < av.len) { + if (tstpackagearg(av[i])) { + if (seen || av[i].len == TST_PACKAGE_PREFIX_LEN) { + return false; + }; + seen = true; + out.ptr = av[i].ptr + (TST_PACKAGE_PREFIX_LEN: u64); + out.len = av[i].len - TST_PACKAGE_PREFIX_LEN; }; - j += 1; + i += 1; + }; + return true; +}; + +fn tstqualified(pkgprefix: str, name: str) str = { + if (pkgprefix.len == 0) { return name; }; + let b: []u8 = alloc([], (pkgprefix.len + name.len + 1): u64)!; + b.len = pkgprefix.len + name.len + 1; + let i: i32 = 0; + for (i < pkgprefix.len) { b[i] = pkgprefix[i]; i += 1; }; + b[pkgprefix.len] = '.'; + i = 0; + for (i < name.len) { + b[pkgprefix.len + 1 + i] = name[i]; + i += 1; + }; + let out: str; + out.ptr = b.ptr; + out.len = b.len; + return out; +}; + +fn tsttimeout(av: []str) i64 = { + let timeoutms: i64 = TST_DEFAULT_TIMEOUT_MS; + let seen: bool = false; + let i: i32 = 1; + for (i < av.len) { + if (tsttimeoutarg(av[i])) { + if (seen || av[i].len == TST_TIMEOUT_PREFIX_LEN) { + return -1i64; + }; + seen = true; + let n: i64 = 0i64; + let j: i32 = TST_TIMEOUT_PREFIX_LEN; + for (j < av[i].len) { + let c: u8 = av[i][j]; + if (c < 48u8 || c > 57u8) { return -1i64; }; + let digit: i64 = (c - 48u8): i64; + if (n > (TST_MAX_TIMEOUT_MS - digit) / 10i64) { + return -1i64; + }; + n = n * 10i64 + digit; + j += 1; + }; + if (n == 0) { return -1i64; }; + timeoutms = n; + }; + i += 1; + }; + return timeoutms; +}; + +fn tstlistmode(av: []str) bool = { + let i: i32 = 1; + for (i < av.len) { + if (tstlistarg(av[i])) { return true; }; + i += 1; }; return false; }; -// runone — fork, run `f` in the child, decode the child's wait-status. -// Byte-for-byte the procrun (main.ww:154-177) status decode: low 7 bits -// are the killing signal (abort=SIGABRT, div0/SIGSEGV), 0 if the child -// exited; next byte is the exit code. -fn tstrunone(f: *fn() void) i32 = { - let pid: i32 = os.fork(); - if (pid < 0) { return -1; }; - if (pid == 0) { - (*f)(); - os.exit(0i32); +fn tstenabled(name: str, av: []str) bool = { + let havefilter: bool = false; + let leaf: str = tstleaf(name); + let i: i32 = 1; + for (i < av.len) { + if (!tstlistarg(av[i]) && !tsttimeoutarg(av[i]) + && !tstpackagearg(av[i])) { + havefilter = true; + if (fnmatch.fnmatch(av[i], name, fnmatch.flag.NONE) || + fnmatch.fnmatch(av[i], leaf, fnmatch.flag.NONE)) { + return true; + }; + }; + i += 1; }; + return !havefilter; +}; + +fn tstleaf(name: str) str = { + let off: i32 = 0; + let i: i32 = 0; + for (i < name.len) { + if (name[i] == 46u8) { off = i + 1; }; + i += 1; + }; + let leaf: str; + leaf.ptr = name.ptr + (off: u64); + leaf.len = name.len - off; + return leaf; +}; + +// One atomic bounded frame is enough after the leader is reaped. Descendants +// may inherit the writer, so record collection never waits for EOF. +fn tstrunone(f: *fn() void, name: str, timeoutms: i64, + result: *tstresult) bool = { + result.kind = TST_HARNESS; + result.completed = false; + result.timedout = false; + result.exited = false; + result.detail = 0; + result.ncontrol = 0; + let fds: [2]i32; + if (os.pipe2(&fds, os.O_CLOEXEC | os.O_NONBLOCK) < 0) { + return false; + }; + let pid: i32 = os.fork(); + if (pid < 0) { + os.close(fds[0]); + os.close(fds[1]); + return false; + }; + if (pid == 0) { + os.close(fds[0]); + tstactive = true; + tstcontrolfd = fds[1]; + tstname = name; + tstwantabort = false; + if (os.setpgid(0, 0) != 0) { + tstwritebyte(0u8); + os.exit(1); + }; + tstwritebyte(TST_GROUP_READY); + (*f)(); + if (!tstwantabort) { tstwriteframe(TST_COMPLETE, ""); }; + os.close(tstcontrolfd); + os.exit(0); + }; + if (pid <= 1) { + os.close(fds[0]); + os.close(fds[1]); + return false; + }; + os.close(fds[1]); + let parentgroup: i32 = os.setpgid(pid, pid); + let ready: u8 = 0u8; + let began: time.instant = time.now(time.clock.monotonic); + let readied: i32 = tstreadready(fds[0], &ready, began, timeoutms); + if (readied == 0) { + result.timedout = true; + if (parentgroup == 0) { os.kill(-pid, os.SIGKILL); } + else { os.kill(pid, os.SIGKILL); }; + let status: i32 = 0; + if (tstwait(pid, &status, 0) == pid) { result.completed = true; }; + result.kind = TST_FAIL; + os.close(fds[0]); + return true; + }; + if (readied < 0 || ready != TST_GROUP_READY) { + if (pid > 1) { os.kill(pid, os.SIGKILL); }; + let ignored: i32 = 0; + tstwait(pid, &ignored, 0); + os.close(fds[0]); + return true; + }; + // The child byte validates the group even when it wins the setpgid race. let status: i32 = 0; - let r: i32 = os.wait4(pid, &status, 0i32, nil: *void); - if (r < 0) { return -1; }; - if ((status & 127i32) != 0) { return 1; }; - return (status >> 8i32) & 255i32; + let waited: i32 = 0; + for (true) { + waited = tstwait(pid, &status, os.WNOHANG); + if (waited == pid || waited < 0) { break; }; + if (tstelapsedms(began) >= timeoutms) { + result.timedout = true; + os.kill(-pid, os.SIGTERM); + let termbegan: time.instant = time.now(time.clock.monotonic); + for (tstelapsedms(termbegan) < TST_TERM_GRACE_MS) { + waited = tstwait(pid, &status, os.WNOHANG); + if (waited == pid || waited < 0) { break; }; + tstsleep(); + }; + if (waited == 0) { + os.kill(-pid, os.SIGKILL); + waited = tstwait(pid, &status, 0); + }; + break; + }; + tstsleep(); + }; + if (waited != pid) { + os.kill(-pid, os.SIGKILL); + waited = tstwait(pid, &status, 0); + if (waited == pid) { + result.completed = true; + tstcleargroup(pid); + }; + os.close(fds[0]); + return true; + }; + result.completed = true; + if (!tstcleargroup(pid)) { + os.close(fds[0]); + return true; + }; + let full: bool = false; + for (result.ncontrol < TST_CONTROL_MAX) { + let n: i64 = os.read(fds[0], &result.control[result.ncontrol], + (TST_CONTROL_MAX - result.ncontrol): u64); + if (n == TST_EINTR) { continue; }; + if (n == TST_EAGAIN || n == 0) { break; }; + if (n < 0) { + os.close(fds[0]); + return true; + }; + result.ncontrol += n: i32; + }; + if (result.ncontrol == TST_CONTROL_MAX) { + let extra: u8 = 0u8; + let n: i64 = os.read(fds[0], &extra, 1u64); + full = n != TST_EAGAIN && n != 0; + }; + os.close(fds[0]); + if (full) { return true; }; + + result.exited = os.wifexited(status); + if (result.exited) { + result.detail = os.wexitstatus(status); + } else if (os.wifsignaled(status)) { + result.detail = os.wtermsig(status); + } else { + return true; + }; + tstinterpret(result); + return true; +}; + +fn tstinterpret(result: *tstresult) void = { + if (result.timedout) { + result.kind = TST_FAIL; + return; + }; + if (result.ncontrol == 0) { + if (result.exited && result.detail == 0) { + result.kind = TST_HARNESS; + } else { + result.kind = TST_FAIL; + }; + return; + }; + if (result.ncontrol < TST_FRAME_HEADER) { + result.kind = TST_HARNESS; + return; + }; + let code: u8 = result.control[0]; + let payload: i32 = (result.control[1]: i32) | + ((result.control[2]: i32) << 8i32); + if (result.ncontrol != TST_FRAME_HEADER + payload) { + result.kind = TST_HARNESS; + return; + }; + if (code == TST_COMPLETE && payload == 0) { + if (result.exited && result.detail == 0) { + result.kind = TST_PASS; + } else { + result.kind = TST_HARNESS; + }; + return; + }; + if (code == TST_SKIPPED && payload > 0) { + if (result.exited && result.detail == 0) { + result.kind = TST_SKIP; + } else { + result.kind = TST_HARNESS; + }; + return; + }; + if (code == TST_EXPECT_ABORT && payload == 0) { + if (result.exited && result.detail != 0) { + result.kind = TST_PASS; + } else { + result.kind = TST_FAIL; + }; + return; + }; + result.kind = TST_HARNESS; +}; + +fn tstwritebyte(b: u8) void = { + if (!tstwriteall(&b, 1u64)) { + os.exit(1); + }; +}; + +fn tstwriteframe(code: u8, payload: str) void = { + let frame: [TST_CONTROL_MAX]u8; + frame[0] = code; + frame[1] = (payload.len & 255): u8; + frame[2] = ((payload.len >> 8) & 255): u8; + let i: i32 = 0; + for (i < payload.len) { + frame[TST_FRAME_HEADER + i] = payload[i]; + i += 1; + }; + if (!tstwriteall(&frame[0], (TST_FRAME_HEADER + payload.len): u64)) { + os.exit(1); + }; +}; + +fn tstwriteall(buf: *u8, n: u64) bool = { + return tstfdwriteall(tstcontrolfd, buf, n); +}; + +fn tstfdwriteall(fd: i32, buf: *u8, n: u64) bool = { + let off: u64 = 0u64; + for (off < n) { + let wrote: i64 = os.write(fd, buf + off, n - off); + if (wrote == TST_EINTR) { continue; }; + if (wrote <= 0) { return false; }; + off += wrote: u64; + }; + return true; +}; + +fn tstreadready(fd: i32, b: *u8, began: time.instant, + timeoutms: i64) i32 = { + for (true) { + let n: i64 = os.read(fd, b, 1u64); + if (n == TST_EINTR) { continue; }; + if (n == 1i64) { return 1; }; + if (n == TST_EAGAIN) { + if (tstelapsedms(began) >= timeoutms) { return 0; }; + tstsleep(); + continue; + }; + return -1; + }; +}; + +fn tstwait(pid: i32, status: *i32, options: i32) i32 = { + for (true) { + let waited: i32 = os.wait4(pid, status, options, nil: *void); + if (waited: i64 != TST_EINTR) { return waited; }; + }; +}; + +fn tstelapsedms(began: time.instant) i64 = { + let now: time.instant = time.now(time.clock.monotonic); + return (time.diff(began, now): i64) / (time.millisecond: i64); +}; + +fn tstsleep() void = { + time.sleep((TST_POLL_MS * (time.millisecond: i64)): time.duration, + time.clock.monotonic); +}; + +fn tstgroupexists(pgid: i32) i32 = { + if (pgid <= 1) { return -1; }; + let r: i32 = os.kill(-pgid, 0); + if (r == 0) { return 1; }; + if (r == TST_ESRCH) { return 0; }; + return -1; +}; + +fn tstcleargroup(pgid: i32) bool = { + let state: i32 = tstgroupexists(pgid); + if (state == 0) { return true; }; + if (state < 0) { return false; }; + os.kill(-pgid, os.SIGTERM); + let began: time.instant = time.now(time.clock.monotonic); + for (tstelapsedms(began) < TST_TERM_GRACE_MS) { + state = tstgroupexists(pgid); + if (state == 0) { return true; }; + if (state < 0) { return false; }; + tstsleep(); + }; + os.kill(-pgid, os.SIGKILL); + began = time.now(time.clock.monotonic); + for (tstelapsedms(began) < TST_TERM_GRACE_MS) { + state = tstgroupexists(pgid); + if (state == 0) { return true; }; + if (state < 0) { return false; }; + tstsleep(); + }; + return tstgroupexists(pgid) == 0; +}; + +fn tstputfailure(result: *tstresult) void = { + if (result.timedout) { + tstputs("FAIL(timeout)\n"); + return; + }; + if (result.ncontrol == TST_FRAME_HEADER && + result.control[0] == TST_EXPECT_ABORT && + result.exited && result.detail == 0) { + tstputs("FAIL (expected abort)\n"); + return; + }; + if (result.exited) { + tstputs("FAIL (exit "); + tstputuint(result.detail); + tstputs(")\n"); + } else { + tstputs("FAIL (signal "); + tstputuint(result.detail); + tstputs(")\n"); + }; +}; + +fn tstputharness(result: *tstresult) void = { + if (result.completed && result.ncontrol == 0 && result.exited && + result.detail == 0) { + tstputs("HARNESS (incomplete result)\n"); + } else if (result.ncontrol == 0) { + tstputs("HARNESS (wait or read error)\n"); + } else { + tstputs("HARNESS (malformed or contradictory result)\n"); + }; +}; + +fn tstputaccounting(discovered: i32, selected: i32, started: i32, + completed: i32) void = { + tstputuint(discovered); + tstputs(" discovered, "); + tstputuint(selected); + tstputs(" selected, "); + tstputuint(started); + tstputs(" started, "); + tstputuint(completed); + tstputs(" completed\n"); }; fn tstputs(s: str) void = { - os.write(os.STDOUT_FILENO, s.ptr, s.len: u64); + tstfdwriteall(os.STDOUT_FILENO, s.ptr, s.len: u64); }; -// tstputuint — write a non-negative i32 in decimal. Lean substitute for -// fmt::printf (Hare's test uses fmt); keeping lib/test's bundle floor at -// os-only avoids pulling the io/strconv stack into every -T build. fn tstputuint(n: i32) void = { let buf: [16]u8; let i: i32 = 16; @@ -157,5 +643,5 @@ fn tstputuint(n: i32) void = { v = v / 10; }; }; - os.write(os.STDOUT_FILENO, &buf[i], (16 - i): u64); + tstfdwriteall(os.STDOUT_FILENO, &buf[i], (16 - i): u64); }; diff --git a/lib/ww/syntax/toktest.ww b/lib/ww/syntax/toktest.ww index 9ae74fba..3b5dcc28 100644 --- a/lib/ww/syntax/toktest.ww +++ b/lib/ww/syntax/toktest.ww @@ -23,6 +23,7 @@ package main; import os; import syntax; +import temp; fn checkname(k: tkind, want: str) void = { @@ -228,8 +229,12 @@ fn checkprint(fd: i32, t: *tok, want: str) void = { // every fputq escape: \\ " \n \t \r, a c<0x20 byte (0x01), c==0x7f, // and a printable ('A'). `want` spells the exact emitted line. @test fn tokprint_cases() void = { - let flags: os.flag = os.flag.RDWR | os.flag.CREATE | os.flag.TRUNC; - let fd: i32 = os.open("/tmp/ww_s9_tok.tmp", flags, 384i32); + let fd: i32 = 0; + let path: str; + match (temp.named(&fd, &path, "/tmp", temp.mode.RDWR, 384i32)) { + case void => {}; + case let e: os.oserror => abort(); + }; assert(!(fd < 0)); let t: tok; @@ -262,7 +267,7 @@ fn checkprint(fd: i32, t: *tok, want: str) void = { checkprint(fd, &t, "t:1:1 \n"); assert(!(os.close(fd) != 0)); - assert(!(os.remove("/tmp/ww_s9_tok.tmp") != 0)); + assert(!(os.remove(path) != 0)); }; fn checkfloat(src: str, want: u64) void = {