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:
2026-08-07 23:00:57 +09:00
parent 25fd1958d3
commit 8bc7a622cc
2 changed files with 608 additions and 117 deletions

View File

@@ -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);
};

View File

@@ -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 = {