Discovers test/wcc/data, hard-pins corpus identity (1,224 fixtures, 314 error / 12 compile / 136 run / 762 run-exit, sha256 name hash), schedules both frontend cells per fixture through os.exec with per-cell capture dirs, and emits the strict wwfix TSV result stream. integration.sh is the black-box owner of the CLI/protocol boundary.
567 lines
16 KiB
Plaintext
567 lines
16 KiB
Plaintext
package wwfixture;
|
|
|
|
import fmt;
|
|
import os;
|
|
import os.exec;
|
|
import strings;
|
|
import temp;
|
|
import time;
|
|
|
|
def fixturepoll: time.duration = 1000000i64: time.duration;
|
|
def fixturegrace: time.duration = 250000000i64: time.duration;
|
|
|
|
type cellstate = enum i32 {
|
|
QUEUED = 0,
|
|
COMPILE = 1,
|
|
BUILD = 2,
|
|
RUN = 3,
|
|
DONE = 4,
|
|
};
|
|
|
|
type fixturecell = struct {
|
|
fixtureindex: i32,
|
|
stage: stage,
|
|
state: cellstate,
|
|
dir: str,
|
|
began: time.instant,
|
|
deadline: time.instant,
|
|
result: cellresult,
|
|
};
|
|
|
|
fn rundecimal(value: i32) str = {
|
|
let digits: [11]u8;
|
|
let n: i32 = 0;
|
|
let v: i64 = value: i64;
|
|
let negative: bool = v < 0i64;
|
|
if (negative) { v = -v; };
|
|
if (v == 0i64) { digits[0] = '0': u8; n = 1; };
|
|
for (v > 0i64) {
|
|
digits[n] = '0': u8 + (v % 10i64): u8;
|
|
n += 1;
|
|
v /= 10i64;
|
|
};
|
|
let capacity: i32 = n;
|
|
if (negative) { capacity += 1; };
|
|
let out: []u8 = alloc([], capacity: u64)!;
|
|
if (negative) { append(out, '-': u8); };
|
|
let i: i32 = n;
|
|
for (i > 0) { i -= 1; append(out, digits[i]); };
|
|
return strings.frombytes(out);
|
|
};
|
|
|
|
fn numbered(ordinal: i32) str = {
|
|
let b: []u8 = alloc([], 4u64)!;
|
|
b.len = 4;
|
|
let v: i32 = ordinal;
|
|
let i: i32 = 3;
|
|
for (i >= 0) {
|
|
b[i] = '0': u8 + (v % 10): u8;
|
|
v /= 10;
|
|
i -= 1;
|
|
};
|
|
return strings.frombytes(b);
|
|
};
|
|
|
|
fn runid(path: str) str = {
|
|
let start: i32 = 0;
|
|
let i: i32 = 0;
|
|
for (i < path.len) {
|
|
if (path[i] == '/': u8) { start = i + 1; };
|
|
i += 1;
|
|
};
|
|
return strings.dup(path[start:path.len]);
|
|
};
|
|
|
|
fn makeenv(root: str, s: stage) []str = {
|
|
let env: []str = alloc([], 7u64)!;
|
|
append(env, "PATH=/usr/bin:/bin");
|
|
append(env, "LC_ALL=C");
|
|
append(env, strings.concat("WW_SRCLIB=", join(root, "lib")));
|
|
append(env, strings.concat("WW_LIB=", join(root, "out/lib")));
|
|
let suffix: str = "";
|
|
if (s == stage.WW) { suffix = "_ww"; };
|
|
append(env, strings.concat("WW_W6C=", join(root,
|
|
strings.concat("out/bin/w6c", suffix))));
|
|
append(env, strings.concat("WW_W6A=", join(root,
|
|
strings.concat("out/bin/w6a", suffix))));
|
|
append(env, strings.concat("WW_W6L=", join(root,
|
|
strings.concat("out/bin/w6l", suffix))));
|
|
return env;
|
|
};
|
|
|
|
fn initialresult(id: *identity, f: *fixture, dir: str) cellresult = {
|
|
let r: cellresult;
|
|
r.ordinal = id.ordinal;
|
|
r.id = f.id;
|
|
r.stage = id.stage;
|
|
r.phase = phase.COMPILE;
|
|
if (f.directive == directive.RUN || f.directive == directive.RUNEXIT) {
|
|
r.phase = phase.BUILD;
|
|
};
|
|
r.verdict = verdict.ERROR;
|
|
r.termination = termination.SETUP;
|
|
r.code = -5;
|
|
r.durationns = 0i64;
|
|
r.capture = dir;
|
|
return r;
|
|
};
|
|
|
|
fn procmessage(reason: str, p: *exec.result) str = {
|
|
return strings.concat(reason,
|
|
"\ntermination=", rundecimal(p.termination as i32),
|
|
" code=", rundecimal(p.code),
|
|
" errno=", rundecimal(p.errno),
|
|
" cleanup-errno=", rundecimal(p.cleanuperrno));
|
|
};
|
|
|
|
fn maptermination(p: *exec.result, r: *cellresult) void = {
|
|
if (p.termination == exec.termination.TIMEOUT) {
|
|
r.termination = termination.TIMEOUT;
|
|
r.code = 0;
|
|
return;
|
|
};
|
|
if (p.errno != 0 || p.cleanuperrno != 0
|
|
|| p.termination == exec.termination.ERROR) {
|
|
r.termination = termination.SETUP;
|
|
r.code = -p.errno;
|
|
if (r.code == 0) { r.code = -p.cleanuperrno; };
|
|
if (r.code == 0) { r.code = -5; };
|
|
return;
|
|
};
|
|
if (p.termination == exec.termination.EXIT) {
|
|
r.termination = termination.EXIT;
|
|
r.code = p.code;
|
|
return;
|
|
};
|
|
if (p.termination == exec.termination.SIGNAL) {
|
|
r.termination = termination.SIGNAL;
|
|
r.code = p.code;
|
|
return;
|
|
};
|
|
r.termination = termination.SETUP;
|
|
r.code = -5;
|
|
};
|
|
|
|
fn processerror(p: *exec.result) bool = {
|
|
return p.errno != 0 || p.cleanuperrno != 0
|
|
|| p.termination == exec.termination.ERROR;
|
|
};
|
|
|
|
fn finishmessage(c: *fixturecell, p: *exec.result, reason: str) bool = {
|
|
let message: str = procmessage(reason, p);
|
|
if (writeharness(c.dir, message)) { return true; };
|
|
c.result.verdict = verdict.ERROR;
|
|
return false;
|
|
};
|
|
|
|
fn startcommand(root: str, f: *fixture, c: *fixturecell,
|
|
h: *exec.process) void = {
|
|
let argv: []str;
|
|
let out: str;
|
|
let err: str;
|
|
if (c.state == cellstate.COMPILE) {
|
|
let frontend: str = join(root, "out/bin/w6c");
|
|
if (c.stage == stage.WW) { frontend = join(root, "out/bin/w6c_ww"); };
|
|
let compileargv: []str = alloc([], 4u64)!;
|
|
argv = compileargv;
|
|
append(argv, frontend); append(argv, "-o");
|
|
append(argv, join(c.dir, "case.s")); append(argv, f.source);
|
|
out = join(c.dir, "compile.stdout");
|
|
err = join(c.dir, "compile.stderr");
|
|
} else if (c.state == cellstate.BUILD) {
|
|
let driver: str = join(root, "out/bin/ww");
|
|
if (c.stage == stage.WW) { driver = join(root, "out/bin/ww_ww"); };
|
|
let buildargv: []str = alloc([], 5u64)!;
|
|
argv = buildargv;
|
|
append(argv, driver); append(argv, "build"); append(argv, "-o");
|
|
append(argv, join(c.dir, "program")); append(argv, f.source);
|
|
out = join(c.dir, "build.stdout");
|
|
err = join(c.dir, "build.stderr");
|
|
} else {
|
|
let program: str = join(c.dir, "program");
|
|
let runargv: []str = alloc([], 1u64)!;
|
|
argv = runargv;
|
|
append(argv, program);
|
|
out = join(c.dir, "run.stdout");
|
|
err = join(c.dir, "run.stderr");
|
|
};
|
|
let env: []str = makeenv(root, c.stage);
|
|
let cmd: exec.command;
|
|
cmd.path = argv[0];
|
|
cmd.argv = argv;
|
|
cmd.env = env;
|
|
cmd.dir = c.dir;
|
|
cmd.stdoutpath = out;
|
|
cmd.stderrpath = err;
|
|
cmd.deadline = c.deadline;
|
|
cmd.grace = fixturegrace;
|
|
exec.start(h, &cmd);
|
|
};
|
|
|
|
// Returns true only when the cell reached a terminal result. A successful
|
|
// positive build starts its program directly and keeps the scheduler slot.
|
|
fn collect(root: str, f: *fixture, c: *fixturecell,
|
|
h: *exec.process, captureok: *bool) bool = {
|
|
let p: *exec.result = &h.result;
|
|
c.result.durationns = time.diff(c.began,
|
|
time.now(time.clock.monotonic)): i64;
|
|
maptermination(p, &c.result);
|
|
if (c.state == cellstate.COMPILE) {
|
|
c.result.phase = phase.COMPILE;
|
|
if (processerror(p)) {
|
|
c.result.verdict = verdict.ERROR;
|
|
if (!finishmessage(c, p, "compiler process failed")) { *captureok = false; };
|
|
} else if (f.directive == directive.COMPILE) {
|
|
if (c.result.termination == termination.EXIT && c.result.code == 0) {
|
|
c.result.verdict = verdict.PASS;
|
|
} else {
|
|
c.result.verdict = verdict.FAIL;
|
|
if (!finishmessage(c, p,
|
|
"compiler did not exit normally with status 0")) {
|
|
*captureok = false;
|
|
};
|
|
};
|
|
} else if (c.result.termination == termination.EXIT && c.result.code != 0) {
|
|
let diagnostic: str;
|
|
let required: str = f.diagnostic;
|
|
if (c.stage == stage.WW && f.wwdiagnostic.len != 0) {
|
|
required = f.wwdiagnostic;
|
|
};
|
|
let path: str = join(c.dir, "compile.stderr");
|
|
if (!readfile(path, &diagnostic)) {
|
|
c.result.verdict = verdict.ERROR;
|
|
if (!finishmessage(c, p, "cannot read compiler diagnostics")) {
|
|
*captureok = false;
|
|
};
|
|
} else if (strings.contains(diagnostic, required)) {
|
|
c.result.verdict = verdict.PASS;
|
|
} else {
|
|
c.result.verdict = verdict.FAIL;
|
|
if (!finishmessage(c, p, "required diagnostic fragment was not observed")) {
|
|
*captureok = false;
|
|
};
|
|
};
|
|
} else {
|
|
c.result.verdict = verdict.FAIL;
|
|
if (!finishmessage(c, p, "compiler did not exit normally with a nonzero status")) {
|
|
*captureok = false;
|
|
};
|
|
};
|
|
c.state = cellstate.DONE;
|
|
return true;
|
|
};
|
|
if (c.state == cellstate.BUILD) {
|
|
c.result.phase = phase.BUILD;
|
|
if (!processerror(p) && c.result.termination == termination.EXIT
|
|
&& c.result.code == 0) {
|
|
c.state = cellstate.RUN;
|
|
startcommand(root, f, c, h);
|
|
return false;
|
|
};
|
|
if (processerror(p)) { c.result.verdict = verdict.ERROR; }
|
|
else { c.result.verdict = verdict.FAIL; };
|
|
if (!finishmessage(c, p, "positive fixture build did not exit normally with status 0")) {
|
|
*captureok = false;
|
|
};
|
|
c.state = cellstate.DONE;
|
|
return true;
|
|
};
|
|
c.result.phase = phase.RUN;
|
|
let expected: i32 = 0;
|
|
if (f.directive == directive.RUNEXIT) { expected = f.exitcode; };
|
|
if (processerror(p)) {
|
|
c.result.verdict = verdict.ERROR;
|
|
if (!finishmessage(c, p, "fixture program process failed")) { *captureok = false; };
|
|
} else if (c.result.termination == termination.EXIT
|
|
&& c.result.code == expected) {
|
|
c.result.verdict = verdict.PASS;
|
|
} else {
|
|
c.result.verdict = verdict.FAIL;
|
|
if (!finishmessage(c, p, "fixture program did not exit normally with the expected status")) {
|
|
*captureok = false;
|
|
};
|
|
};
|
|
c.state = cellstate.DONE;
|
|
return true;
|
|
};
|
|
|
|
fn finishinterrupted(c: *fixturecell, p: *exec.result,
|
|
signo: i32, captureok: *bool) void = {
|
|
c.result.phase = phase.COMPILE;
|
|
if (c.state == cellstate.BUILD) { c.result.phase = phase.BUILD; }
|
|
else if (c.state == cellstate.RUN) { c.result.phase = phase.RUN; };
|
|
c.result.durationns = time.diff(c.began,
|
|
time.now(time.clock.monotonic)): i64;
|
|
maptermination(p, &c.result);
|
|
c.result.verdict = verdict.ERROR;
|
|
let why: str = strings.concat("coordinator interrupted by signal ",
|
|
rundecimal(signo));
|
|
if (signo < 0) { why = "coordinator interrupt monitor failed"; };
|
|
if (!finishmessage(c, p, why)) { *captureok = false; };
|
|
c.state = cellstate.DONE;
|
|
};
|
|
|
|
fn finishqueued(c: *fixturecell, code: i32, reason: str,
|
|
captureok: *bool) void = {
|
|
c.result.verdict = verdict.ERROR;
|
|
c.result.termination = termination.SETUP;
|
|
c.result.code = -code;
|
|
if (c.result.code == 0) { c.result.code = -4; };
|
|
c.result.durationns = 0i64;
|
|
let p: exec.result;
|
|
p.termination = exec.termination.ERROR;
|
|
p.errno = code;
|
|
if (!finishmessage(c, &p, reason)) { *captureok = false; };
|
|
c.state = cellstate.DONE;
|
|
};
|
|
|
|
fn ensurecapture(dir: str, reason: str) bool = {
|
|
let rc: i32 = os.mkdir(dir, 448i32);
|
|
if (rc != 0 && rc != -17) { return false; };
|
|
return writeharness(dir, reason);
|
|
};
|
|
|
|
fn cleanuppassingcapture(c: *fixturecell) bool = {
|
|
let r: *cellresult = &c.result;
|
|
if (removeall(c.dir)) {
|
|
r.capture = "-";
|
|
return true;
|
|
};
|
|
r.verdict = verdict.ERROR;
|
|
r.termination = termination.SETUP;
|
|
r.code = -5;
|
|
return ensurecapture(c.dir, "passing capture cleanup failed");
|
|
};
|
|
|
|
fn cleanupcaptures(opts: *options, cells: []fixturecell,
|
|
runroot: str) bool = {
|
|
let ok: bool = true;
|
|
let kept: bool = false;
|
|
let i: i32 = 0;
|
|
for (i < cells.len) {
|
|
let r: *cellresult = &cells[i].result;
|
|
if (r.verdict == verdict.PASS && !opts.keep) {
|
|
if (r.capture != "-" && !cleanuppassingcapture(&cells[i])) {
|
|
ok = false;
|
|
};
|
|
if (r.capture != "-") { kept = true; };
|
|
} else { kept = true; };
|
|
i += 1;
|
|
};
|
|
if (!kept) {
|
|
if (os.rmdir(runroot) != 0) {
|
|
ok = false;
|
|
if (cells.len > 0) {
|
|
let c: *fixturecell = &cells[0];
|
|
c.result.verdict = verdict.ERROR;
|
|
c.result.termination = termination.SETUP;
|
|
c.result.code = -5;
|
|
c.result.capture = c.dir;
|
|
ensurecapture(c.dir, "run directory cleanup failed");
|
|
};
|
|
};
|
|
};
|
|
return ok;
|
|
};
|
|
|
|
fn preparecells(opts: *options, c: *corpus, ids: []identity,
|
|
runroot: str, outcells: *[]fixturecell,
|
|
outhandles: *[]exec.process) bool = {
|
|
let cells: []fixturecell = alloc([], ids.len: u64)!;
|
|
let handles: []exec.process = alloc([], ids.len: u64)!;
|
|
let i: i32 = 0;
|
|
for (i < ids.len) {
|
|
let f: *fixture = &c.fixtures[ids[i].fixtureindex];
|
|
let cell: fixturecell;
|
|
cell.fixtureindex = ids[i].fixtureindex;
|
|
cell.stage = ids[i].stage;
|
|
cell.state = cellstate.QUEUED;
|
|
cell.dir = join(runroot, numbered(ids[i].ordinal));
|
|
cell.result = initialresult(&ids[i], f, cell.dir);
|
|
append(cells, cell);
|
|
let handle: exec.process;
|
|
append(handles, handle);
|
|
i += 1;
|
|
};
|
|
*outcells = cells;
|
|
*outhandles = handles;
|
|
let ok: bool = true;
|
|
i = 0;
|
|
for (i < cells.len) {
|
|
if (os.mkdir(cells[i].dir, 448i32) != 0) {
|
|
ok = false;
|
|
i += 1;
|
|
continue;
|
|
};
|
|
i += 1;
|
|
};
|
|
if (!ok) { fmt.errorln("wwfixture: cannot create numbered capture directory"); };
|
|
return ok;
|
|
};
|
|
|
|
fn cancelall(handles: []exec.process) bool = {
|
|
let i: i32 = 0;
|
|
for (i < handles.len) { exec.cancel(&handles[i]); i += 1; };
|
|
let pending: bool = true;
|
|
for (pending) {
|
|
pending = false;
|
|
i = 0;
|
|
for (i < handles.len) {
|
|
if (!exec.poll(&handles[i])) { pending = true; };
|
|
i += 1;
|
|
};
|
|
if (pending) { time.sleep(fixturepoll, time.clock.monotonic); };
|
|
};
|
|
let ok: bool = true;
|
|
i = 0;
|
|
for (i < handles.len) {
|
|
if (handles[i].pid > 0 && (handles[i].result.errno != 0
|
|
|| handles[i].result.cleanuperrno != 0)) { ok = false; };
|
|
i += 1;
|
|
};
|
|
return ok;
|
|
};
|
|
|
|
fn runfixtures(opts: *options, c: *corpus, ids: []identity) int = {
|
|
let runroot: str = strings.dup(temp.dir());
|
|
let rid: str = runid(runroot);
|
|
let cells: []fixturecell;
|
|
let handles: []exec.process;
|
|
let prepared: bool = preparecells(opts, c, ids, runroot, &cells, &handles);
|
|
let watch: exec.interrupt;
|
|
let captureok: bool = true;
|
|
let watchok: bool = false;
|
|
let interrupted: i32 = 0;
|
|
let active: i32 = 0;
|
|
let complete: i32 = 0;
|
|
let next: i32 = 0;
|
|
if (!prepared) {
|
|
let rootcaptureok: bool = writeharness(runroot,
|
|
"cannot create one or more numbered capture directories");
|
|
let i: i32 = 0;
|
|
for (i < cells.len) {
|
|
let cellcaptureok: bool = true;
|
|
finishqueued(&cells[i], 5,
|
|
"fixture setup stopped before process launch", &cellcaptureok);
|
|
if (!cellcaptureok) {
|
|
cells[i].result.capture = runroot;
|
|
if (!rootcaptureok) { captureok = false; };
|
|
};
|
|
i += 1;
|
|
};
|
|
complete = cells.len;
|
|
} else if (cells.len > 0) {
|
|
watchok = exec.interruptopen(&watch);
|
|
};
|
|
if (prepared && cells.len > 0 && !watchok) {
|
|
let code: i32 = watch.errno;
|
|
let i: i32 = 0;
|
|
for (i < cells.len) {
|
|
finishqueued(&cells[i], code,
|
|
"cannot establish coordinator interrupt monitor", &captureok);
|
|
i += 1;
|
|
};
|
|
complete = cells.len;
|
|
};
|
|
for (complete < cells.len) {
|
|
let signo: i32 = exec.interruptpoll(&watch);
|
|
if (signo != 0) {
|
|
interrupted = signo;
|
|
let cleanupok: bool = cancelall(handles);
|
|
if (!cleanupok) { captureok = false; };
|
|
let i: i32 = 0;
|
|
for (i < cells.len) {
|
|
if (cells[i].state == cellstate.DONE) { i += 1; continue; };
|
|
if (cells[i].state == cellstate.QUEUED) {
|
|
let ec: i32 = 4;
|
|
if (signo < 0) { ec = -signo; };
|
|
finishqueued(&cells[i], ec,
|
|
"coordinator stopped before cell launch", &captureok);
|
|
} else {
|
|
finishinterrupted(&cells[i], &handles[i].result,
|
|
signo, &captureok);
|
|
};
|
|
i += 1;
|
|
};
|
|
complete = cells.len;
|
|
break;
|
|
};
|
|
for (next < cells.len && active < opts.jobs) {
|
|
let current: *fixturecell = &cells[next];
|
|
let f: *fixture = &c.fixtures[current.fixtureindex];
|
|
current.state = cellstate.COMPILE;
|
|
if (f.directive == directive.RUN || f.directive == directive.RUNEXIT) {
|
|
current.state = cellstate.BUILD;
|
|
};
|
|
current.began = time.now(time.clock.monotonic);
|
|
current.deadline = time.add(current.began,
|
|
(opts.timeoutms * (time.millisecond: i64)): time.duration);
|
|
startcommand(c.root, f, current, &handles[next]);
|
|
active += 1;
|
|
next += 1;
|
|
};
|
|
let i: i32 = 0;
|
|
for (i < next) {
|
|
if (cells[i].state == cellstate.DONE
|
|
|| cells[i].state == cellstate.QUEUED) { i += 1; continue; };
|
|
if (exec.poll(&handles[i])) {
|
|
let f: *fixture = &c.fixtures[cells[i].fixtureindex];
|
|
if (collect(c.root, f, &cells[i], &handles[i], &captureok)) {
|
|
if (cells[i].result.verdict == verdict.PASS && !opts.keep
|
|
&& !cleanuppassingcapture(&cells[i])) {
|
|
captureok = false;
|
|
};
|
|
active -= 1;
|
|
complete += 1;
|
|
};
|
|
};
|
|
i += 1;
|
|
};
|
|
if (active > 0) { time.sleep(fixturepoll, time.clock.monotonic); };
|
|
};
|
|
if (watchok && !exec.interruptclose(&watch)) {
|
|
captureok = false;
|
|
if (cells.len > 0) {
|
|
let c0: *fixturecell = &cells[0];
|
|
if (!writefile(join(c0.dir, "coordinator.txt"),
|
|
"cannot close coordinator interrupt monitor\n")) {
|
|
captureok = false;
|
|
};
|
|
if (c0.result.verdict == verdict.PASS) {
|
|
c0.result.verdict = verdict.ERROR;
|
|
c0.result.termination = termination.SETUP;
|
|
c0.result.code = -watch.errno;
|
|
if (c0.result.code == 0) { c0.result.code = -5; };
|
|
};
|
|
};
|
|
};
|
|
if (!cleanupcaptures(opts, cells, runroot)) { captureok = false; };
|
|
let results: []cellresult = alloc([], cells.len: u64)!;
|
|
let failures: i32 = 0;
|
|
let i: i32 = 0;
|
|
for (i < cells.len) {
|
|
append(results, cells[i].result);
|
|
if (cells[i].result.verdict != verdict.PASS) { failures += 1; };
|
|
i += 1;
|
|
};
|
|
if (!writestream(os.STDOUT_FILENO, rid, c, ids, results)) {
|
|
let retained: bool = true;
|
|
let rc: i32 = os.mkdir(runroot, 448i32);
|
|
if (rc != 0 && rc != -17) { retained = false; };
|
|
if (retained && !writefile(join(runroot, "publication.txt"),
|
|
strings.concat("result publication failed: ", protocolerror(), "\n"))) {
|
|
retained = false;
|
|
};
|
|
if (retained) {
|
|
fmt.errorln(strings.concat("wwfixture: ", protocolerror(),
|
|
"; capture: ", runroot));
|
|
} else {
|
|
fmt.errorln(strings.concat("wwfixture: ", protocolerror(),
|
|
"; cannot retain publication capture"));
|
|
};
|
|
return 1;
|
|
};
|
|
if (!captureok || interrupted != 0 || failures != 0) { return 1; };
|
|
return 0;
|
|
};
|