Files
ww/lib/test/run.ww

648 lines
15 KiB
Plaintext

package test;
// 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;
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 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 = tstqualified(pkgprefix, row.0);
if (!tstenabled(name, av)) { i += 1; continue; };
selected += 1;
if (list) {
tstputs(name);
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;
};
if (list) {
return 0;
};
if (selected == 0) {
tsterrputs("testing: warning: no tests to run\n");
tstputaccounting(tests.len, 0, 0, 0);
return 0;
};
tstputuint(passed);
tstputs(" passed, ");
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;
};
// 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 > 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;
};
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;
};
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 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) {
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 = {
tstfdwriteall(os.STDOUT_FILENO, s.ptr, s.len: u64);
};
fn tsterrputs(s: str) void = {
tstfdwriteall(os.STDERR_FILENO, s.ptr, s.len: u64);
};
fn tstputuint(n: i32) void = {
let buf: [16]u8;
let i: i32 = 16;
let v: i32 = n;
if (v == 0) {
i -= 1;
buf[i] = 48u8;
} else {
for (v > 0) {
i -= 1;
buf[i] = (48 + v % 10): u8;
v = v / 10;
};
};
tstfdwriteall(os.STDOUT_FILENO, &buf[i], (16 - i): u64);
};