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 = { if (ids.len > 9999) { fmt.errorln("wwfixture: selected matrix exceeds four-digit capture namespace"); return 1; }; 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; };