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.
This commit is contained in:
714
lib/test/run.ww
714
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
|
||||
// <file> [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);
|
||||
};
|
||||
|
||||
@@ -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 <none>\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 = {
|
||||
|
||||
Reference in New Issue
Block a user