wwfixture: declarative fixture runner
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.
This commit is contained in:
8
cmd/wwfixture/wwfixture.ww
Normal file
8
cmd/wwfixture/wwfixture.ww
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package main;
|
||||||
|
|
||||||
|
import os;
|
||||||
|
import wwfixture;
|
||||||
|
|
||||||
|
export fn main() int = {
|
||||||
|
return wwfixture.command(os.args());
|
||||||
|
};
|
||||||
150
internal/wwfixture/command.ww
Normal file
150
internal/wwfixture/command.ww
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
package wwfixture;
|
||||||
|
|
||||||
|
import fmt;
|
||||||
|
import os;
|
||||||
|
import strings;
|
||||||
|
|
||||||
|
type options = struct {
|
||||||
|
root: str,
|
||||||
|
patterns: []str,
|
||||||
|
jobs: i32,
|
||||||
|
list: bool,
|
||||||
|
keep: bool,
|
||||||
|
timeoutms: i64,
|
||||||
|
input: str,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn usage() void = {
|
||||||
|
fmt.errorln("usage: wwfixture [-root path] [-run glob]... [-list] [-j N] [-keep] [-timeout-ms N]");
|
||||||
|
fmt.errorln(" wwfixture validate [-root path] [-run glob]... STREAM");
|
||||||
|
};
|
||||||
|
|
||||||
|
fn positive(s: str, limit: i64, out: *i64) bool = {
|
||||||
|
if (s.len == 0) { return false; };
|
||||||
|
let n: i64 = 0i64;
|
||||||
|
let i: i32 = 0;
|
||||||
|
for (i < s.len) {
|
||||||
|
let c: u8 = s[i];
|
||||||
|
if (c < '0': u8 || c > '9': u8) { return false; };
|
||||||
|
let digit: i64 = (c - '0': u8): i64;
|
||||||
|
if (n > (limit - digit) / 10i64) { return false; };
|
||||||
|
n = n * 10i64 + digit;
|
||||||
|
i += 1;
|
||||||
|
};
|
||||||
|
if (n == 0i64) { return false; };
|
||||||
|
*out = n;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn initoptions(out: *options) void = {
|
||||||
|
out.root = "";
|
||||||
|
let patterns: []str = alloc([], 16u64)!;
|
||||||
|
out.patterns = patterns;
|
||||||
|
out.jobs = 4;
|
||||||
|
out.list = false;
|
||||||
|
out.keep = false;
|
||||||
|
out.timeoutms = 30000i64;
|
||||||
|
out.input = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
fn parseoptions(args: []str, start: i32, validate: bool,
|
||||||
|
out: *options) bool = {
|
||||||
|
initoptions(out);
|
||||||
|
let i: i32 = start;
|
||||||
|
for (i < args.len) {
|
||||||
|
let a: str = args[i];
|
||||||
|
if (a == "-root" || a == "-run") {
|
||||||
|
if (i + 1 >= args.len) { return false; };
|
||||||
|
if (a == "-root") {
|
||||||
|
if (out.root.len != 0) { return false; };
|
||||||
|
out.root = args[i + 1];
|
||||||
|
} else { append(out.patterns, args[i + 1]); };
|
||||||
|
i += 2;
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if (!validate && a == "-list") {
|
||||||
|
if (out.list) { return false; };
|
||||||
|
out.list = true; i += 1; continue;
|
||||||
|
};
|
||||||
|
if (!validate && a == "-keep") {
|
||||||
|
if (out.keep) { return false; };
|
||||||
|
out.keep = true; i += 1; continue;
|
||||||
|
};
|
||||||
|
if (!validate && (a == "-j" || a == "-timeout-ms")) {
|
||||||
|
if (i + 1 >= args.len) { return false; };
|
||||||
|
let n: i64 = 0i64;
|
||||||
|
let limit: i64 = 64i64;
|
||||||
|
if (a == "-timeout-ms") { limit = 3600000i64; };
|
||||||
|
if (!positive(args[i + 1], limit, &n)) { return false; };
|
||||||
|
if (a == "-j") { out.jobs = n: i32; }
|
||||||
|
else { out.timeoutms = n; };
|
||||||
|
i += 2;
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if (validate && a.len > 0 && a[0] != '-': u8 && out.input.len == 0) {
|
||||||
|
out.input = a; i += 1; continue;
|
||||||
|
};
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if (validate && out.input.len == 0) { return false; };
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn listfixtures(c: *corpus, ids: []identity) bool = {
|
||||||
|
let i: i32 = 0;
|
||||||
|
for (i < ids.len) {
|
||||||
|
let f: *fixture = &c.fixtures[ids[i].fixtureindex];
|
||||||
|
let line: str = strings.concat(f.id, "\t", stageword(ids[i].stage), "\n");
|
||||||
|
if (!writeall(os.STDOUT_FILENO, line)) { return false; };
|
||||||
|
i += 1;
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn validatecommand(opts: *options) int = {
|
||||||
|
let c: corpus;
|
||||||
|
if (!loadcorpus(opts.root, &c)) {
|
||||||
|
fmt.errorln("wwfixture validate: ", error());
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
let ids: []identity;
|
||||||
|
selectidentities(&c, opts.patterns, &ids);
|
||||||
|
let data: str;
|
||||||
|
if (!readfile(opts.input, &data)) {
|
||||||
|
fmt.errorln("wwfixture validate: ", error());
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
if (!validatestream(data, &c, ids)) {
|
||||||
|
fmt.errorln("wwfixture validate: ", protocolerror());
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
export fn command(args: []str) int = {
|
||||||
|
if (args.len < 1) { usage(); return 2; };
|
||||||
|
let validate: bool = args.len > 1 && args[1] == "validate";
|
||||||
|
let start: i32 = 1;
|
||||||
|
if (validate) { start = 2; };
|
||||||
|
let opts: options;
|
||||||
|
if (!parseoptions(args, start, validate, &opts)) {
|
||||||
|
usage();
|
||||||
|
return 2;
|
||||||
|
};
|
||||||
|
if (validate) { return validatecommand(&opts); };
|
||||||
|
let c: corpus;
|
||||||
|
if (!loadcorpus(opts.root, &c)) {
|
||||||
|
fmt.errorln("wwfixture: ", error());
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
let ids: []identity;
|
||||||
|
selectidentities(&c, opts.patterns, &ids);
|
||||||
|
if (opts.list) {
|
||||||
|
if (!listfixtures(&c, ids)) {
|
||||||
|
fmt.errorln("wwfixture: result output failed");
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
return runfixtures(&opts, &c, ids);
|
||||||
|
};
|
||||||
464
internal/wwfixture/corpus.ww
Normal file
464
internal/wwfixture/corpus.ww
Normal file
@@ -0,0 +1,464 @@
|
|||||||
|
package wwfixture;
|
||||||
|
|
||||||
|
import crypto.sha256;
|
||||||
|
import encoding.hex;
|
||||||
|
import fnmatch;
|
||||||
|
import hash;
|
||||||
|
import os;
|
||||||
|
import strings;
|
||||||
|
|
||||||
|
def dirbufsize: i32 = 8192;
|
||||||
|
def typemask: u32 = 61440u32;
|
||||||
|
|
||||||
|
let fixtureerror: str = "";
|
||||||
|
|
||||||
|
export fn error() str = { return fixtureerror; };
|
||||||
|
|
||||||
|
fn seterror(message: str, detail: str) bool = {
|
||||||
|
if (detail.len == 0) { fixtureerror = strings.dup(message); }
|
||||||
|
else { fixtureerror = strings.concat(message, ": ", detail); };
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn startswith(s: str, prefix: str) bool = {
|
||||||
|
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 safeatom(s: str) bool = {
|
||||||
|
if (s.len == 0) { return false; };
|
||||||
|
let i: i32 = 0;
|
||||||
|
for (i < s.len) {
|
||||||
|
let c: u8 = s[i];
|
||||||
|
if (!((c >= 'a': u8 && c <= 'z': u8)
|
||||||
|
|| (c >= '0': u8 && c <= '9': u8) || c == '_': u8)) {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
i += 1;
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn join(a: str, b: str) str = {
|
||||||
|
if (a.len == 0) { return strings.dup(b); };
|
||||||
|
if (a[a.len - 1] == '/': u8) { return strings.concat(a, b); };
|
||||||
|
return strings.concat(a, "/", b);
|
||||||
|
};
|
||||||
|
|
||||||
|
fn currentdir(out: *str) bool = {
|
||||||
|
let b: []u8 = alloc([], os.PATH_MAX: u64)!;
|
||||||
|
b.len = os.PATH_MAX;
|
||||||
|
let n: i64 = os.getcwd(b.ptr, b.len: u64);
|
||||||
|
if (n <= 1i64 || n > b.len: i64) { return seterror("cannot resolve current directory", ""); };
|
||||||
|
b.len = (n - 1i64): i32;
|
||||||
|
*out = strings.dup(strings.frombytes(b));
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn absoluteroot(root: str, out: *str) bool = {
|
||||||
|
if (root.len == 0) { return currentdir(out); };
|
||||||
|
if (root[0] == '/': u8) { *out = strings.dup(root); return true; };
|
||||||
|
let cwd: str;
|
||||||
|
if (!currentdir(&cwd)) { return false; };
|
||||||
|
*out = join(cwd, root);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn readfile(path: str, out: *str) bool = {
|
||||||
|
let st: os.filestat;
|
||||||
|
match (os.stat(&st, path)) {
|
||||||
|
case void => void;
|
||||||
|
case let e: os.oserror => return seterror("cannot stat file", path);
|
||||||
|
};
|
||||||
|
if (st.sz > 2147483647u64) { return seterror("file is too large", path); };
|
||||||
|
let fd: i32 = os.open(path, os.flag.RDONLY, 0i32);
|
||||||
|
if (fd < 0) { return seterror("cannot open file", path); };
|
||||||
|
if (st.sz == 0u64) {
|
||||||
|
if (os.close(fd) != 0) { return seterror("cannot close file", path); };
|
||||||
|
let empty: str;
|
||||||
|
empty.ptr = nil: *u8; empty.len = 0;
|
||||||
|
*out = empty;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
let b: []u8 = alloc([], st.sz)!;
|
||||||
|
b.len = st.sz: i32;
|
||||||
|
let got: i64 = match (os.readall(fd, b.ptr, st.sz)) {
|
||||||
|
case let n: i64 => yield n;
|
||||||
|
case let e: os.oserror => {
|
||||||
|
os.close(fd);
|
||||||
|
return seterror("cannot read file", path);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
if (os.close(fd) != 0) { return seterror("cannot close file", path); };
|
||||||
|
if (got != st.sz: i64) { return seterror("short read", path); };
|
||||||
|
*out = strings.dup(strings.frombytes(b));
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn writeall(fd: i32, s: str) bool = {
|
||||||
|
let off: i32 = 0;
|
||||||
|
for (off < s.len) {
|
||||||
|
let n: i64 = os.write(fd, s.ptr + (off: u64), (s.len - off): u64);
|
||||||
|
if (n == -4i64) { continue; };
|
||||||
|
if (n <= 0i64) { return false; };
|
||||||
|
off += n: i32;
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn writefile(path: str, data: str) bool = {
|
||||||
|
let fd: i32 = os.open(path,
|
||||||
|
os.flag.WRONLY | os.flag.CREATE | os.flag.EXCL, 384i32);
|
||||||
|
if (fd < 0) { return false; };
|
||||||
|
let ok: bool = writeall(fd, data);
|
||||||
|
if (os.close(fd) != 0) { ok = false; };
|
||||||
|
return ok;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn writeharness(dir: str, message: str) bool = {
|
||||||
|
let path: str = join(dir, "harness.txt");
|
||||||
|
return writefile(path, strings.concat(message, "\n"));
|
||||||
|
};
|
||||||
|
|
||||||
|
fn direntname(buf: []u8, off: i32, reclen: i32, out: *str) bool = {
|
||||||
|
if (reclen < 20 || off < 0 || off + reclen > buf.len) { return false; };
|
||||||
|
let first: i32 = off + 19;
|
||||||
|
let last: i32 = first;
|
||||||
|
let limit: i32 = off + reclen;
|
||||||
|
for (last < limit && buf[last] != 0u8) { last += 1; };
|
||||||
|
if (last == limit) { return false; };
|
||||||
|
out.ptr = buf.ptr + (first: u64);
|
||||||
|
out.len = last - first;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn filekind(path: str, out: *u32) i32 = {
|
||||||
|
let st: os.filestat;
|
||||||
|
match (os.lstat(&st, path)) {
|
||||||
|
case void => { *out = (st.mode: u32) & typemask; return 1; };
|
||||||
|
case let e: os.oserror => {
|
||||||
|
if ((e: i64) == -2i64) { return 0; };
|
||||||
|
seterror("cannot inspect path", path);
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
fn sortnames(names: []str) void = {
|
||||||
|
let i: i32 = 1;
|
||||||
|
for (i < names.len) {
|
||||||
|
let v: str = names[i];
|
||||||
|
let j: i32 = i;
|
||||||
|
for (j > 0 && strings.compare(names[j - 1], v) > 0) {
|
||||||
|
names[j] = names[j - 1];
|
||||||
|
j -= 1;
|
||||||
|
};
|
||||||
|
names[j] = v;
|
||||||
|
i += 1;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
fn parsenumber(s: str, out: *i32) bool = {
|
||||||
|
if (s.len == 0 || (s.len > 1 && s[0] == '0': u8)) { return false; };
|
||||||
|
let value: i32 = 0;
|
||||||
|
let i: i32 = 0;
|
||||||
|
for (i < s.len) {
|
||||||
|
let c: u8 = s[i];
|
||||||
|
if (c < '0': u8 || c > '9': u8) { return false; };
|
||||||
|
value = value * 10 + ((c - '0': u8): i32);
|
||||||
|
if (value > 255) { return false; };
|
||||||
|
i += 1;
|
||||||
|
};
|
||||||
|
*out = value;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn parsedirective(path: str, data: str, out: *fixture) bool = {
|
||||||
|
if (data.len == 0) { return seterror("empty fixture", path); };
|
||||||
|
let end: i32 = 0;
|
||||||
|
for (end < data.len && data[end] != 10u8) { end += 1; };
|
||||||
|
if (end == data.len) { return seterror("fixture directive line is not LF terminated", path); };
|
||||||
|
let line: str;
|
||||||
|
line.ptr = data.ptr; line.len = end;
|
||||||
|
out.diagnostic = "";
|
||||||
|
out.wwdiagnostic = "";
|
||||||
|
out.exitcode = 0;
|
||||||
|
if (line == "//ww:run") { out.directive = directive.RUN; return true; };
|
||||||
|
let runexit: str = "//ww:run-exit ";
|
||||||
|
if (startswith(line, runexit)) {
|
||||||
|
let tail: str;
|
||||||
|
tail.ptr = line.ptr + (runexit.len: u64);
|
||||||
|
tail.len = line.len - runexit.len;
|
||||||
|
if (!parsenumber(tail, &out.exitcode)) {
|
||||||
|
return seterror("malformed run-exit directive", path);
|
||||||
|
};
|
||||||
|
out.directive = directive.RUNEXIT;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
let stagedprefix: str = "//ww:error c \"";
|
||||||
|
if (startswith(line, stagedprefix)) {
|
||||||
|
let cend: i32 = stagedprefix.len;
|
||||||
|
for (cend < line.len && line[cend] != 34u8) { cend += 1; };
|
||||||
|
if (cend == stagedprefix.len || cend + 6 >= line.len
|
||||||
|
|| line[cend] != 34u8 || line[cend + 1] != ' ': u8
|
||||||
|
|| line[cend + 2] != 'w': u8 || line[cend + 3] != 'w': u8
|
||||||
|
|| line[cend + 4] != ' ': u8 || line[cend + 5] != 34u8
|
||||||
|
|| line[line.len - 1] != 34u8 || cend + 6 >= line.len - 1) {
|
||||||
|
return seterror("malformed stage-specific error directive", path);
|
||||||
|
};
|
||||||
|
let cbody: str;
|
||||||
|
cbody.ptr = line.ptr + (stagedprefix.len: u64);
|
||||||
|
cbody.len = cend - stagedprefix.len;
|
||||||
|
let wwbody: str;
|
||||||
|
wwbody.ptr = line.ptr + ((cend + 6): u64);
|
||||||
|
wwbody.len = line.len - cend - 7;
|
||||||
|
let i: i32 = 0;
|
||||||
|
for (i < cbody.len) {
|
||||||
|
if (cbody[i] == 34u8 || cbody[i] == 9u8 || cbody[i] == 13u8) {
|
||||||
|
return seterror("malformed stage-specific error directive", path);
|
||||||
|
};
|
||||||
|
i += 1;
|
||||||
|
};
|
||||||
|
i = 0;
|
||||||
|
for (i < wwbody.len) {
|
||||||
|
if (wwbody[i] == 34u8 || wwbody[i] == 9u8 || wwbody[i] == 13u8) {
|
||||||
|
return seterror("malformed stage-specific error directive", path);
|
||||||
|
};
|
||||||
|
i += 1;
|
||||||
|
};
|
||||||
|
out.directive = directive.ERROR;
|
||||||
|
out.diagnostic = strings.dup(cbody);
|
||||||
|
out.wwdiagnostic = strings.dup(wwbody);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
let errprefix: str = "//ww:error \"";
|
||||||
|
if (startswith(line, errprefix) && line.len > errprefix.len + 1
|
||||||
|
&& line[line.len - 1] == 34u8) {
|
||||||
|
let body: str;
|
||||||
|
body.ptr = line.ptr + (errprefix.len: u64);
|
||||||
|
body.len = line.len - errprefix.len - 1;
|
||||||
|
let i: i32 = 0;
|
||||||
|
for (i < body.len) {
|
||||||
|
if (body[i] == 34u8 || body[i] == 9u8 || body[i] == 13u8) {
|
||||||
|
return seterror("malformed error directive", path);
|
||||||
|
};
|
||||||
|
i += 1;
|
||||||
|
};
|
||||||
|
out.directive = directive.ERROR;
|
||||||
|
out.diagnostic = strings.dup(body);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
if (line == "//ww:compile") {
|
||||||
|
out.directive = directive.COMPILE;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
return seterror("malformed fixture directive", path);
|
||||||
|
};
|
||||||
|
|
||||||
|
fn discovernames(root: str, out: *[]str) bool = {
|
||||||
|
let datadir: str = join(root, "test/wcc/data");
|
||||||
|
let fd: i32 = os.open(datadir, os.flag.RDONLY, 0i32);
|
||||||
|
if (fd < 0) { return seterror("cannot open fixture root", datadir); };
|
||||||
|
let names: []str = alloc([], 128u64)!;
|
||||||
|
let buf: []u8 = alloc([], dirbufsize: u64)!;
|
||||||
|
buf.len = dirbufsize;
|
||||||
|
for (true) {
|
||||||
|
let n: i64 = os.getdents64(fd, buf.ptr, buf.len: u64);
|
||||||
|
if (n < 0i64) { os.close(fd); return seterror("cannot enumerate fixture root", datadir); };
|
||||||
|
if (n == 0i64) { break; };
|
||||||
|
let off: i32 = 0;
|
||||||
|
for (off < n: i32) {
|
||||||
|
if (off + 18 >= n: i32) { os.close(fd); return seterror("malformed directory record", datadir); };
|
||||||
|
let reclen: i32 = (buf[off + 16]: i32) + ((buf[off + 17]: i32) * 256);
|
||||||
|
if (reclen <= 0 || off + reclen > n: i32) {
|
||||||
|
os.close(fd); return seterror("malformed directory record", datadir);
|
||||||
|
};
|
||||||
|
let name: str;
|
||||||
|
if (!direntname(buf, off, reclen, &name)) {
|
||||||
|
os.close(fd); return seterror("malformed directory record", datadir);
|
||||||
|
};
|
||||||
|
if (name != "." && name != "..") {
|
||||||
|
let dir: str = join(datadir, name);
|
||||||
|
let kind: u32 = 0u32;
|
||||||
|
let status: i32 = filekind(dir, &kind);
|
||||||
|
if (status < 0) { os.close(fd); return false; };
|
||||||
|
if (status == 1 && kind == (os.mode.LINK: u32)) {
|
||||||
|
os.close(fd); return seterror("symlink fixture directory is forbidden", dir);
|
||||||
|
};
|
||||||
|
if (status == 1 && kind == (os.mode.DIR: u32)) {
|
||||||
|
let source: str = join(dir, "case.ww");
|
||||||
|
let sourcekind: u32 = 0u32;
|
||||||
|
let sourcestatus: i32 = filekind(source, &sourcekind);
|
||||||
|
if (sourcestatus < 0) { os.close(fd); return false; };
|
||||||
|
if (sourcestatus == 1 && sourcekind == (os.mode.LINK: u32)) {
|
||||||
|
os.close(fd); return seterror("symlink fixture source is forbidden", source);
|
||||||
|
};
|
||||||
|
if (sourcestatus == 1 && sourcekind == (os.mode.REG: u32)) {
|
||||||
|
if (!safeatom(name)) { os.close(fd); return seterror("invalid fixture directory name", name); };
|
||||||
|
append(names, strings.dup(name));
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
off += reclen;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
if (os.close(fd) != 0) { return seterror("cannot close fixture root", datadir); };
|
||||||
|
sortnames(names);
|
||||||
|
*out = names;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn identityhash(names: []str) str = {
|
||||||
|
let state: sha256.state = sha256.sha256();
|
||||||
|
let h: *hash.hash = (&state): *hash.hash;
|
||||||
|
let nl: [1]u8 = [10u8];
|
||||||
|
let i: i32 = 0;
|
||||||
|
for (i < names.len) {
|
||||||
|
hash.write(h, strings.toutf8(names[i]));
|
||||||
|
hash.write(h, nl[0:1]);
|
||||||
|
i += 1;
|
||||||
|
};
|
||||||
|
let digest: [32]u8;
|
||||||
|
hash.sum(h, digest[0:32]);
|
||||||
|
return strings.dup(hex.encodestr(digest[0:32]));
|
||||||
|
};
|
||||||
|
|
||||||
|
fn loadcorpus(rootarg: str, out: *corpus) bool = {
|
||||||
|
fixtureerror = "";
|
||||||
|
let root: str;
|
||||||
|
if (!absoluteroot(rootarg, &root)) { return false; };
|
||||||
|
let names: []str;
|
||||||
|
if (!discovernames(root, &names)) { return false; };
|
||||||
|
let digest: str = identityhash(names);
|
||||||
|
if (names.len != corpuscount || digest != corpushash) {
|
||||||
|
return seterror("compiler corpus identity drift",
|
||||||
|
strings.concat("expected ", rundecimal(corpuscount), "/",
|
||||||
|
corpushash, ", observed ", rundecimal(names.len), "/", digest));
|
||||||
|
};
|
||||||
|
let fixtures: []fixture = alloc([], names.len: u64)!;
|
||||||
|
let errors: i32 = 0;
|
||||||
|
let compiles: i32 = 0;
|
||||||
|
let runs: i32 = 0;
|
||||||
|
let runexits: i32 = 0;
|
||||||
|
let i: i32 = 0;
|
||||||
|
for (i < names.len) {
|
||||||
|
let f: fixture;
|
||||||
|
f.name = names[i];
|
||||||
|
f.id = strings.concat("compiler.", f.name);
|
||||||
|
f.source = join(join(join(root, "test/wcc/data"), f.name), "case.ww");
|
||||||
|
let data: str;
|
||||||
|
if (!readfile(f.source, &data)) { return false; };
|
||||||
|
if (!parsedirective(f.source, data, &f)) { return false; };
|
||||||
|
if (f.directive == directive.ERROR) { errors += 1; }
|
||||||
|
else { if (f.directive == directive.COMPILE) { compiles += 1; }
|
||||||
|
else { if (f.directive == directive.RUN) { runs += 1; }
|
||||||
|
else { if (f.directive == directive.RUNEXIT) { runexits += 1; };
|
||||||
|
}; }; };
|
||||||
|
append(fixtures, f);
|
||||||
|
i += 1;
|
||||||
|
};
|
||||||
|
let derivednative: i32 = fixtures.len * 2;
|
||||||
|
if (errors != errorcount || compiles != compilecount
|
||||||
|
|| runs != runcount || runexits != runexitcount
|
||||||
|
|| derivednative != nativecount) {
|
||||||
|
return seterror("compiler stage matrix drift",
|
||||||
|
strings.concat("expected ", rundecimal(errorcount), " errors, ",
|
||||||
|
rundecimal(compilecount), " compiles, ",
|
||||||
|
rundecimal(runcount), " runs, and ",
|
||||||
|
rundecimal(runexitcount), " run-exits"));
|
||||||
|
};
|
||||||
|
out.root = root;
|
||||||
|
out.fixtures = fixtures;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn cellname(f: *fixture, s: stage) str = {
|
||||||
|
return strings.concat(f.id, "/", stageword(s));
|
||||||
|
};
|
||||||
|
|
||||||
|
fn selectedby(patterns: []str, f: *fixture, s: stage) bool = {
|
||||||
|
if (patterns.len == 0) { return true; };
|
||||||
|
let full: str = cellname(f, s);
|
||||||
|
let i: i32 = 0;
|
||||||
|
for (i < patterns.len) {
|
||||||
|
if (fnmatch.fnmatch(patterns[i], f.id, fnmatch.flag.NONE)
|
||||||
|
|| fnmatch.fnmatch(patterns[i], full, fnmatch.flag.NONE)) {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
i += 1;
|
||||||
|
};
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn selectidentities(c: *corpus, patterns: []str, out: *[]identity) bool = {
|
||||||
|
let ids: []identity = alloc([], nativecount: u64)!;
|
||||||
|
let ordinal: i32 = 0;
|
||||||
|
let i: i32 = 0;
|
||||||
|
for (i < c.fixtures.len) {
|
||||||
|
let si: i32 = 0;
|
||||||
|
for (si < 2) {
|
||||||
|
let s: stage = stage.C;
|
||||||
|
if (si == 1) { s = stage.WW; };
|
||||||
|
if (selectedby(patterns, &c.fixtures[i], s)) {
|
||||||
|
ordinal += 1;
|
||||||
|
let id: identity;
|
||||||
|
id.ordinal = ordinal;
|
||||||
|
id.fixtureindex = i;
|
||||||
|
id.stage = s;
|
||||||
|
append(ids, id);
|
||||||
|
};
|
||||||
|
si += 1;
|
||||||
|
};
|
||||||
|
i += 1;
|
||||||
|
};
|
||||||
|
*out = ids;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn isdot(name: str) bool = { return name == "." || name == ".."; };
|
||||||
|
|
||||||
|
// Callers restrict this to directories minted beneath one temp.dir result;
|
||||||
|
// lstat plus the opened-directory inode check keeps cleanup from following a
|
||||||
|
// replaced path outside that ownership boundary.
|
||||||
|
fn removeall(path: str) bool = {
|
||||||
|
let st: os.filestat;
|
||||||
|
match (os.lstat(&st, path)) {
|
||||||
|
case void => void;
|
||||||
|
case let e: os.oserror => return false;
|
||||||
|
};
|
||||||
|
let kind: u32 = (st.mode: u32) & typemask;
|
||||||
|
if (kind != (os.mode.DIR: u32)) { return os.remove(path) == 0; };
|
||||||
|
let fd: i32 = os.open(path, os.flag.RDONLY, 0i32);
|
||||||
|
if (fd < 0) { return false; };
|
||||||
|
let opened: os.filestat;
|
||||||
|
match (os.fstat(&opened, fd)) {
|
||||||
|
case void => void;
|
||||||
|
case let e: os.oserror => { os.close(fd); return false; };
|
||||||
|
};
|
||||||
|
if (((opened.mode: u32) & typemask) != (os.mode.DIR: u32)
|
||||||
|
|| opened.inode != st.inode) { os.close(fd); return false; };
|
||||||
|
let ok: bool = true;
|
||||||
|
let buf: []u8 = alloc([], dirbufsize: u64)!;
|
||||||
|
buf.len = dirbufsize;
|
||||||
|
let n: i64 = os.getdents64(fd, buf.ptr, buf.len: u64);
|
||||||
|
for (n > 0i64) {
|
||||||
|
let off: i32 = 0;
|
||||||
|
for (off < n: i32) {
|
||||||
|
let reclen: i32 = (buf[off + 16]: i32) + ((buf[off + 17]: i32) * 256);
|
||||||
|
if (reclen < 20 || off + reclen > n: i32) { ok = false; break; };
|
||||||
|
let name: str;
|
||||||
|
if (!direntname(buf, off, reclen, &name)) { ok = false; break; };
|
||||||
|
if (!isdot(name) && !removeall(join(path, name))) { ok = false; };
|
||||||
|
off += reclen;
|
||||||
|
};
|
||||||
|
n = os.getdents64(fd, buf.ptr, buf.len: u64);
|
||||||
|
};
|
||||||
|
if (n < 0i64 || os.close(fd) != 0) { ok = false; };
|
||||||
|
if (os.rmdir(path) != 0) { ok = false; };
|
||||||
|
return ok;
|
||||||
|
};
|
||||||
524
internal/wwfixture/protocol.ww
Normal file
524
internal/wwfixture/protocol.ww
Normal file
@@ -0,0 +1,524 @@
|
|||||||
|
package wwfixture;
|
||||||
|
|
||||||
|
import os;
|
||||||
|
import strings;
|
||||||
|
|
||||||
|
let protocolerr: str = "";
|
||||||
|
|
||||||
|
export fn protocolerror() str = { return protocolerr; };
|
||||||
|
|
||||||
|
fn protocolfail(message: str) bool = {
|
||||||
|
protocolerr = strings.dup(message);
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn protocolput(b: *[]u8, s: str) void = {
|
||||||
|
let i: i32 = 0;
|
||||||
|
for (i < s.len) {
|
||||||
|
append(*b, s[i]);
|
||||||
|
i += 1;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
fn protocolfield(b: *[]u8, s: str) void = {
|
||||||
|
append(*b, '\t': u8);
|
||||||
|
protocolput(b, s);
|
||||||
|
};
|
||||||
|
|
||||||
|
fn protocolu64(b: *[]u8, value: u64) void = {
|
||||||
|
let digits: [20]u8;
|
||||||
|
let n: i32 = 0;
|
||||||
|
let v: u64 = value;
|
||||||
|
if (v == 0u64) {
|
||||||
|
append(*b, '0': u8);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
for (v != 0u64) {
|
||||||
|
digits[n] = '0': u8 + (v % 10u64): u8;
|
||||||
|
n += 1;
|
||||||
|
v /= 10u64;
|
||||||
|
};
|
||||||
|
for (n > 0) {
|
||||||
|
n -= 1;
|
||||||
|
append(*b, digits[n]);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
fn protocoli64(b: *[]u8, value: i64) void = {
|
||||||
|
if (value < 0i64) {
|
||||||
|
append(*b, '-': u8);
|
||||||
|
protocolu64(b, (-value): u64);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
protocolu64(b, value: u64);
|
||||||
|
};
|
||||||
|
|
||||||
|
fn protocolintfield(b: *[]u8, value: i64) void = {
|
||||||
|
append(*b, '\t': u8);
|
||||||
|
protocoli64(b, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
fn protocolnewline(b: *[]u8) void = { append(*b, '\n': u8); };
|
||||||
|
|
||||||
|
fn protocolwriteall(fd: i32, b: []u8) bool = {
|
||||||
|
let off: i32 = 0;
|
||||||
|
for (off < b.len) {
|
||||||
|
let n: i64 = os.write(fd, b.ptr + (off: u64),
|
||||||
|
(b.len - off): u64);
|
||||||
|
if (n == -4i64) { continue; };
|
||||||
|
if (n <= 0i64) { return protocolfail("cannot write terminal stream"); };
|
||||||
|
off += n: i32;
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn safeprotocolfield(s: str) bool = {
|
||||||
|
if (s.len == 0) { return false; };
|
||||||
|
let i: i32 = 0;
|
||||||
|
for (i < s.len) {
|
||||||
|
if (s[i] < 32u8 || s[i] == 127u8) { return false; };
|
||||||
|
i += 1;
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn validrunid(s: str) bool = {
|
||||||
|
if (s.len != 16) { return false; };
|
||||||
|
let i: i32 = 0;
|
||||||
|
for (i < s.len) {
|
||||||
|
let c: u8 = s[i];
|
||||||
|
if (!((c >= '0': u8 && c <= '9': u8)
|
||||||
|
|| (c >= 'a': u8 && c <= 'f': u8))) { return false; };
|
||||||
|
i += 1;
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn validstage(s: stage) bool = { return s == stage.C || s == stage.WW; };
|
||||||
|
|
||||||
|
fn validphase(p: phase) bool = {
|
||||||
|
return p == phase.COMPILE || p == phase.BUILD || p == phase.RUN;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn validverdict(v: verdict) bool = {
|
||||||
|
return v == verdict.PASS || v == verdict.FAIL || v == verdict.ERROR;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn validtermination(t: termination) bool = {
|
||||||
|
return t == termination.EXIT || t == termination.SIGNAL
|
||||||
|
|| t == termination.TIMEOUT || t == termination.SETUP;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn validexpected(c: *corpus, ids: []identity) bool = {
|
||||||
|
if (ids.len > nativecount) {
|
||||||
|
return protocolfail("selected count exceeds native matrix");
|
||||||
|
};
|
||||||
|
let i: i32 = 0;
|
||||||
|
for (i < ids.len) {
|
||||||
|
let id: *identity = &ids[i];
|
||||||
|
if (id.ordinal != i + 1) {
|
||||||
|
return protocolfail("expected ordinals are not consecutive");
|
||||||
|
};
|
||||||
|
if (id.fixtureindex < 0 || id.fixtureindex >= c.fixtures.len) {
|
||||||
|
return protocolfail("expected fixture index is invalid");
|
||||||
|
};
|
||||||
|
if (!validstage(id.stage)) {
|
||||||
|
return protocolfail("expected stage is invalid");
|
||||||
|
};
|
||||||
|
let j: i32 = 0;
|
||||||
|
for (j < i) {
|
||||||
|
if (ids[j].fixtureindex == id.fixtureindex
|
||||||
|
&& ids[j].stage == id.stage) {
|
||||||
|
return protocolfail("duplicate expected identity");
|
||||||
|
};
|
||||||
|
j += 1;
|
||||||
|
};
|
||||||
|
i += 1;
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn validcapture(r: *cellresult) bool = {
|
||||||
|
if (!safeprotocolfield(r.capture)) {
|
||||||
|
return protocolfail("unsafe or empty capture field");
|
||||||
|
};
|
||||||
|
if (r.verdict != verdict.PASS && r.capture == "-") {
|
||||||
|
return protocolfail("non-pass result has no retained capture");
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn validterminationcode(r: *cellresult) bool = {
|
||||||
|
if (r.termination == termination.EXIT) {
|
||||||
|
if (r.code < 0 || r.code > 255) {
|
||||||
|
return protocolfail("exit code is out of range");
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
if (r.termination == termination.SIGNAL) {
|
||||||
|
if (r.code < 1 || r.code > 64) {
|
||||||
|
return protocolfail("signal number is out of range");
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
if (r.termination == termination.TIMEOUT) {
|
||||||
|
if (r.code != 0) { return protocolfail("timeout code is not zero"); };
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
if (r.code >= 0 || r.code < -4095) {
|
||||||
|
return protocolfail("setup code is not a negative errno");
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn validresult(f: *fixture, r: *cellresult) bool = {
|
||||||
|
if (!safeprotocolfield(r.id)) {
|
||||||
|
return protocolfail("unsafe or empty result identity");
|
||||||
|
};
|
||||||
|
if (!validstage(r.stage) || !validphase(r.phase)
|
||||||
|
|| !validverdict(r.verdict) || !validtermination(r.termination)) {
|
||||||
|
return protocolfail("invalid result enum");
|
||||||
|
};
|
||||||
|
if (r.durationns < 0i64) { return protocolfail("negative duration"); };
|
||||||
|
if (!validcapture(r) || !validterminationcode(r)) { return false; };
|
||||||
|
if (r.verdict == verdict.PASS && r.termination != termination.EXIT) {
|
||||||
|
return protocolfail("pass is not a normal exit");
|
||||||
|
};
|
||||||
|
if (r.termination == termination.SETUP && r.verdict != verdict.ERROR) {
|
||||||
|
return protocolfail("setup termination is not an error");
|
||||||
|
};
|
||||||
|
if (r.verdict == verdict.FAIL && r.termination == termination.SETUP) {
|
||||||
|
return protocolfail("fail result has setup termination");
|
||||||
|
};
|
||||||
|
if (f.directive == directive.ERROR) {
|
||||||
|
if (r.phase != phase.COMPILE) {
|
||||||
|
return protocolfail("error fixture has non-compile phase");
|
||||||
|
};
|
||||||
|
if (r.verdict == verdict.PASS
|
||||||
|
&& (r.termination != termination.EXIT || r.code == 0)) {
|
||||||
|
return protocolfail("compile-fail pass is not a normal nonzero exit");
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
if (f.directive == directive.COMPILE) {
|
||||||
|
if (r.phase != phase.COMPILE) {
|
||||||
|
return protocolfail("compile fixture has non-compile phase");
|
||||||
|
};
|
||||||
|
if (r.verdict == verdict.PASS
|
||||||
|
&& (r.termination != termination.EXIT || r.code != 0)) {
|
||||||
|
return protocolfail("compile pass is not a normal exit zero");
|
||||||
|
};
|
||||||
|
if (r.verdict != verdict.PASS && r.termination == termination.EXIT
|
||||||
|
&& r.code == 0) {
|
||||||
|
return protocolfail("non-pass compile is a normal exit zero");
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
if (f.directive != directive.RUN && f.directive != directive.RUNEXIT) {
|
||||||
|
return protocolfail("invalid fixture directive");
|
||||||
|
};
|
||||||
|
if (r.phase == phase.COMPILE) {
|
||||||
|
return protocolfail("positive fixture has compile phase");
|
||||||
|
};
|
||||||
|
if (r.phase == phase.BUILD) {
|
||||||
|
if (r.verdict == verdict.PASS) {
|
||||||
|
return protocolfail("positive build cannot be a terminal pass");
|
||||||
|
};
|
||||||
|
if (r.termination == termination.EXIT && r.code == 0) {
|
||||||
|
return protocolfail("positive build stopped after normal exit zero");
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
let expected: i32 = 0;
|
||||||
|
if (f.directive == directive.RUNEXIT) { expected = f.exitcode; };
|
||||||
|
if (r.verdict == verdict.PASS
|
||||||
|
&& (r.termination != termination.EXIT || r.code != expected)) {
|
||||||
|
return protocolfail("runtime pass does not match expected exit");
|
||||||
|
};
|
||||||
|
if (r.verdict == verdict.FAIL && r.termination == termination.EXIT
|
||||||
|
&& r.code == expected) {
|
||||||
|
return protocolfail("runtime fail matches expected exit");
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn validrow(c: *corpus, expected: *identity, r: *cellresult) bool = {
|
||||||
|
if (r.ordinal != expected.ordinal) {
|
||||||
|
return protocolfail("missing, duplicate, or out-of-order ordinal");
|
||||||
|
};
|
||||||
|
let f: *fixture = &c.fixtures[expected.fixtureindex];
|
||||||
|
if (r.id != f.id || r.stage != expected.stage) {
|
||||||
|
return protocolfail("unexpected result identity");
|
||||||
|
};
|
||||||
|
return validresult(f, r);
|
||||||
|
};
|
||||||
|
|
||||||
|
fn putheader(b: *[]u8, runid: str, selected: i32) void = {
|
||||||
|
protocolput(b, "wwfix");
|
||||||
|
protocolintfield(b, protocolversion: i64);
|
||||||
|
protocolfield(b, runid);
|
||||||
|
protocolintfield(b, corpuscount: i64);
|
||||||
|
protocolintfield(b, nativecount: i64);
|
||||||
|
protocolfield(b, corpushash);
|
||||||
|
protocolintfield(b, selected: i64);
|
||||||
|
protocolnewline(b);
|
||||||
|
};
|
||||||
|
|
||||||
|
fn putrow(b: *[]u8, r: *cellresult) void = {
|
||||||
|
protocolput(b, "case");
|
||||||
|
protocolintfield(b, r.ordinal: i64);
|
||||||
|
protocolfield(b, r.id);
|
||||||
|
protocolfield(b, stageword(r.stage));
|
||||||
|
protocolfield(b, phaseword(r.phase));
|
||||||
|
protocolfield(b, verdictword(r.verdict));
|
||||||
|
protocolfield(b, terminationword(r.termination));
|
||||||
|
protocolintfield(b, r.code: i64);
|
||||||
|
protocolintfield(b, r.durationns);
|
||||||
|
protocolfield(b, r.capture);
|
||||||
|
protocolnewline(b);
|
||||||
|
};
|
||||||
|
|
||||||
|
fn putfooter(b: *[]u8, selected: i32, failures: i32) void = {
|
||||||
|
protocolput(b, "end");
|
||||||
|
protocolintfield(b, selected: i64);
|
||||||
|
protocolintfield(b, failures: i64);
|
||||||
|
protocolnewline(b);
|
||||||
|
};
|
||||||
|
|
||||||
|
fn writestream(fd: i32, runid: str, c: *corpus, ids: []identity,
|
||||||
|
results: []cellresult) bool = {
|
||||||
|
protocolerr = "";
|
||||||
|
if (!validrunid(runid)) { return protocolfail("invalid run ID"); };
|
||||||
|
if (!validexpected(c, ids)) { return false; };
|
||||||
|
if (results.len != ids.len) {
|
||||||
|
return protocolfail("selected result count mismatch");
|
||||||
|
};
|
||||||
|
let failures: i32 = 0;
|
||||||
|
let i: i32 = 0;
|
||||||
|
for (i < results.len) {
|
||||||
|
if (!validrow(c, &ids[i], &results[i])) { return false; };
|
||||||
|
if (results[i].verdict != verdict.PASS) { failures += 1; };
|
||||||
|
i += 1;
|
||||||
|
};
|
||||||
|
let b: []u8;
|
||||||
|
putheader(&b, runid, ids.len);
|
||||||
|
i = 0;
|
||||||
|
for (i < results.len) {
|
||||||
|
putrow(&b, &results[i]);
|
||||||
|
i += 1;
|
||||||
|
};
|
||||||
|
putfooter(&b, ids.len, failures);
|
||||||
|
return protocolwriteall(fd, b);
|
||||||
|
};
|
||||||
|
|
||||||
|
fn splitfields(line: str, out: *[]str) bool = {
|
||||||
|
let fields: []str;
|
||||||
|
let start: i32 = 0;
|
||||||
|
let i: i32 = 0;
|
||||||
|
for (i <= line.len) {
|
||||||
|
if (i == line.len || line[i] == '\t': u8) {
|
||||||
|
append(fields, line[start:i]);
|
||||||
|
start = i + 1;
|
||||||
|
};
|
||||||
|
i += 1;
|
||||||
|
};
|
||||||
|
*out = fields;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn nextline(data: str, off: *i32, out: *str) bool = {
|
||||||
|
if (*off >= data.len) { return false; };
|
||||||
|
let end: i32 = *off;
|
||||||
|
for (end < data.len && data[end] != '\n': u8) { end += 1; };
|
||||||
|
if (end == data.len) { return false; };
|
||||||
|
*out = data[*off:end];
|
||||||
|
*off = end + 1;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn parseu64field(s: str, out: *u64) bool = {
|
||||||
|
if (s.len == 0) { return protocolfail("empty integer"); };
|
||||||
|
if (s.len > 1 && s[0] == '0': u8) {
|
||||||
|
return protocolfail("non-canonical integer");
|
||||||
|
};
|
||||||
|
let n: u64 = 0u64;
|
||||||
|
let i: i32 = 0;
|
||||||
|
for (i < s.len) {
|
||||||
|
let c: u8 = s[i];
|
||||||
|
if (c < '0': u8 || c > '9': u8) {
|
||||||
|
return protocolfail("malformed integer");
|
||||||
|
};
|
||||||
|
let d: u64 = (c - '0': u8): u64;
|
||||||
|
if (n > 1844674407370955161u64
|
||||||
|
|| (n == 1844674407370955161u64 && d > 5u64)) {
|
||||||
|
return protocolfail("integer overflow");
|
||||||
|
};
|
||||||
|
n = n * 10u64 + d;
|
||||||
|
i += 1;
|
||||||
|
};
|
||||||
|
*out = n;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn parsei32nonnegative(s: str, out: *i32) bool = {
|
||||||
|
let n: u64 = 0u64;
|
||||||
|
if (!parseu64field(s, &n)) { return false; };
|
||||||
|
if (n > 2147483647u64) { return protocolfail("i32 overflow"); };
|
||||||
|
*out = n: i32;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn parsei64nonnegative(s: str, out: *i64) bool = {
|
||||||
|
let n: u64 = 0u64;
|
||||||
|
if (!parseu64field(s, &n)) { return false; };
|
||||||
|
if (n > 9223372036854775807u64) { return protocolfail("i64 overflow"); };
|
||||||
|
*out = n: i64;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn parsei32field(s: str, out: *i32) bool = {
|
||||||
|
if (s.len == 0) { return protocolfail("empty integer"); };
|
||||||
|
if (s[0] != '-': u8) { return parsei32nonnegative(s, out); };
|
||||||
|
if (s.len == 1 || (s.len > 2 && s[1] == '0': u8)) {
|
||||||
|
return protocolfail("malformed negative integer");
|
||||||
|
};
|
||||||
|
let magnitude: str;
|
||||||
|
magnitude.ptr = s.ptr + 1u64;
|
||||||
|
magnitude.len = s.len - 1;
|
||||||
|
let n: u64 = 0u64;
|
||||||
|
if (!parseu64field(magnitude, &n)) { return false; };
|
||||||
|
if (n == 0u64 || n > 2147483648u64) {
|
||||||
|
return protocolfail("i32 overflow");
|
||||||
|
};
|
||||||
|
*out = -(n: i32);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn parsestagefield(s: str, out: *stage) bool = {
|
||||||
|
if (s == "c") { *out = stage.C; return true; };
|
||||||
|
if (s == "ww") { *out = stage.WW; return true; };
|
||||||
|
return protocolfail("unknown stage");
|
||||||
|
};
|
||||||
|
|
||||||
|
fn parsephasefield(s: str, out: *phase) bool = {
|
||||||
|
if (s == "compile") { *out = phase.COMPILE; return true; };
|
||||||
|
if (s == "build") { *out = phase.BUILD; return true; };
|
||||||
|
if (s == "run") { *out = phase.RUN; return true; };
|
||||||
|
return protocolfail("unknown phase");
|
||||||
|
};
|
||||||
|
|
||||||
|
fn parseverdictfield(s: str, out: *verdict) bool = {
|
||||||
|
if (s == "pass") { *out = verdict.PASS; return true; };
|
||||||
|
if (s == "fail") { *out = verdict.FAIL; return true; };
|
||||||
|
if (s == "error") { *out = verdict.ERROR; return true; };
|
||||||
|
return protocolfail("unknown verdict");
|
||||||
|
};
|
||||||
|
|
||||||
|
fn parseterminationfield(s: str, out: *termination) bool = {
|
||||||
|
if (s == "exit") { *out = termination.EXIT; return true; };
|
||||||
|
if (s == "signal") { *out = termination.SIGNAL; return true; };
|
||||||
|
if (s == "timeout") { *out = termination.TIMEOUT; return true; };
|
||||||
|
if (s == "setup") { *out = termination.SETUP; return true; };
|
||||||
|
return protocolfail("unknown termination");
|
||||||
|
};
|
||||||
|
|
||||||
|
fn parseheader(line: str, selected: i32) bool = {
|
||||||
|
let fields: []str;
|
||||||
|
splitfields(line, &fields);
|
||||||
|
if (fields.len != 7) { return protocolfail("malformed header field count"); };
|
||||||
|
if (fields[0] != "wwfix") { return protocolfail("missing or malformed header"); };
|
||||||
|
let version: i32 = 0;
|
||||||
|
let observedcorpus: i32 = 0;
|
||||||
|
let observednative: i32 = 0;
|
||||||
|
let observedselected: i32 = 0;
|
||||||
|
if (!parsei32nonnegative(fields[1], &version)
|
||||||
|
|| !parsei32nonnegative(fields[3], &observedcorpus)
|
||||||
|
|| !parsei32nonnegative(fields[4], &observednative)
|
||||||
|
|| !parsei32nonnegative(fields[6], &observedselected)) { return false; };
|
||||||
|
if (version != protocolversion) { return protocolfail("wrong protocol version"); };
|
||||||
|
if (!validrunid(fields[2])) { return protocolfail("invalid run ID"); };
|
||||||
|
if (observedcorpus != corpuscount || observednative != nativecount
|
||||||
|
|| strings.compare(fields[5], corpushash) != 0) {
|
||||||
|
return protocolfail("wrong corpus identity");
|
||||||
|
};
|
||||||
|
if (observedselected != selected) {
|
||||||
|
return protocolfail("header selected count mismatch");
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn parserow(line: str, out: *cellresult) bool = {
|
||||||
|
let fields: []str;
|
||||||
|
splitfields(line, &fields);
|
||||||
|
if (fields.len != 10) { return protocolfail("malformed case field count"); };
|
||||||
|
if (fields[0] != "case") { return protocolfail("missing case record"); };
|
||||||
|
if (!parsei32nonnegative(fields[1], &out.ordinal)
|
||||||
|
|| !parsestagefield(fields[3], &out.stage)
|
||||||
|
|| !parsephasefield(fields[4], &out.phase)
|
||||||
|
|| !parseverdictfield(fields[5], &out.verdict)
|
||||||
|
|| !parseterminationfield(fields[6], &out.termination)
|
||||||
|
|| !parsei32field(fields[7], &out.code)
|
||||||
|
|| !parsei64nonnegative(fields[8], &out.durationns)) { return false; };
|
||||||
|
out.id = fields[2];
|
||||||
|
out.capture = fields[9];
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn parsefooter(line: str, selected: i32, failures: i32) bool = {
|
||||||
|
let fields: []str;
|
||||||
|
splitfields(line, &fields);
|
||||||
|
if (fields.len != 3 || fields[0] != "end") {
|
||||||
|
return protocolfail("missing or malformed footer");
|
||||||
|
};
|
||||||
|
let observedselected: i32 = 0;
|
||||||
|
let observedfailures: i32 = 0;
|
||||||
|
if (!parsei32nonnegative(fields[1], &observedselected)
|
||||||
|
|| !parsei32nonnegative(fields[2], &observedfailures)) { return false; };
|
||||||
|
if (observedselected != selected || observedfailures != failures) {
|
||||||
|
return protocolfail("inconsistent footer");
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn validatestream(data: str, c: *corpus, ids: []identity) bool = {
|
||||||
|
protocolerr = "";
|
||||||
|
if (data.len == 0) { return protocolfail("missing header"); };
|
||||||
|
if (data[data.len - 1] != '\n': u8) {
|
||||||
|
return protocolfail("incomplete final line");
|
||||||
|
};
|
||||||
|
let off: i32 = 0;
|
||||||
|
let line: str;
|
||||||
|
if (!nextline(data, &off, &line)) { return protocolfail("missing header"); };
|
||||||
|
if (!parseheader(line, ids.len)) { return false; };
|
||||||
|
if (!validexpected(c, ids)) { return false; };
|
||||||
|
let failures: i32 = 0;
|
||||||
|
let i: i32 = 0;
|
||||||
|
for (i < ids.len) {
|
||||||
|
if (!nextline(data, &off, &line)) {
|
||||||
|
return protocolfail("missing case record");
|
||||||
|
};
|
||||||
|
let r: cellresult;
|
||||||
|
if (!parserow(line, &r) || !validrow(c, &ids[i], &r)) { return false; };
|
||||||
|
if (r.verdict != verdict.PASS) { failures += 1; };
|
||||||
|
i += 1;
|
||||||
|
};
|
||||||
|
if (!nextline(data, &off, &line)) { return protocolfail("missing footer"); };
|
||||||
|
if (!parsefooter(line, ids.len, failures)) { return false; };
|
||||||
|
if (off != data.len) { return protocolfail("trailing records"); };
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export fn validate(data: str, root: str, patterns: []str) bool = {
|
||||||
|
protocolerr = "";
|
||||||
|
let c: corpus;
|
||||||
|
if (!loadcorpus(root, &c)) {
|
||||||
|
protocolerr = strings.concat("cannot load corpus: ", error());
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let ids: []identity;
|
||||||
|
if (!selectidentities(&c, patterns, &ids)) {
|
||||||
|
protocolerr = strings.concat("cannot select corpus: ", error());
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
return validatestream(data, &c, ids);
|
||||||
|
};
|
||||||
566
internal/wwfixture/run.ww
Normal file
566
internal/wwfixture/run.ww
Normal file
@@ -0,0 +1,566 @@
|
|||||||
|
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;
|
||||||
|
};
|
||||||
98
internal/wwfixture/types.ww
Normal file
98
internal/wwfixture/types.ww
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
package wwfixture;
|
||||||
|
|
||||||
|
def protocolversion: i32 = 1;
|
||||||
|
def corpuscount: i32 = 1224;
|
||||||
|
def errorcount: i32 = 314;
|
||||||
|
def compilecount: i32 = 12;
|
||||||
|
def runcount: i32 = 136;
|
||||||
|
def runexitcount: i32 = 762;
|
||||||
|
def nativecount: i32 = 2448;
|
||||||
|
def corpushash: str = "8b522e42cbce8b389e33e917229dede8045d85f1af0c54630cfef83c4ef242be";
|
||||||
|
|
||||||
|
type directive = enum i32 {
|
||||||
|
ERROR = 0,
|
||||||
|
RUN = 1,
|
||||||
|
RUNEXIT = 2,
|
||||||
|
COMPILE = 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
type stage = enum i32 {
|
||||||
|
C = 0,
|
||||||
|
WW = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
type phase = enum i32 {
|
||||||
|
COMPILE = 0,
|
||||||
|
BUILD = 1,
|
||||||
|
RUN = 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
type verdict = enum i32 {
|
||||||
|
PASS = 0,
|
||||||
|
FAIL = 1,
|
||||||
|
ERROR = 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
type termination = enum i32 {
|
||||||
|
EXIT = 0,
|
||||||
|
SIGNAL = 1,
|
||||||
|
TIMEOUT = 2,
|
||||||
|
SETUP = 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
type fixture = struct {
|
||||||
|
name: str,
|
||||||
|
id: str,
|
||||||
|
source: str,
|
||||||
|
directive: directive,
|
||||||
|
diagnostic: str,
|
||||||
|
wwdiagnostic: str,
|
||||||
|
exitcode: i32,
|
||||||
|
};
|
||||||
|
|
||||||
|
type corpus = struct {
|
||||||
|
root: str,
|
||||||
|
fixtures: []fixture,
|
||||||
|
};
|
||||||
|
|
||||||
|
type identity = struct {
|
||||||
|
ordinal: i32,
|
||||||
|
fixtureindex: i32,
|
||||||
|
stage: stage,
|
||||||
|
};
|
||||||
|
|
||||||
|
type cellresult = struct {
|
||||||
|
ordinal: i32,
|
||||||
|
id: str,
|
||||||
|
stage: stage,
|
||||||
|
phase: phase,
|
||||||
|
verdict: verdict,
|
||||||
|
termination: termination,
|
||||||
|
code: i32,
|
||||||
|
durationns: i64,
|
||||||
|
capture: str,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn stageword(s: stage) str = {
|
||||||
|
if (s == stage.C) { return "c"; };
|
||||||
|
return "ww";
|
||||||
|
};
|
||||||
|
|
||||||
|
fn phaseword(p: phase) str = {
|
||||||
|
if (p == phase.COMPILE) { return "compile"; };
|
||||||
|
if (p == phase.BUILD) { return "build"; };
|
||||||
|
return "run";
|
||||||
|
};
|
||||||
|
|
||||||
|
fn verdictword(v: verdict) str = {
|
||||||
|
if (v == verdict.PASS) { return "pass"; };
|
||||||
|
if (v == verdict.FAIL) { return "fail"; };
|
||||||
|
return "error";
|
||||||
|
};
|
||||||
|
|
||||||
|
fn terminationword(t: termination) str = {
|
||||||
|
if (t == termination.EXIT) { return "exit"; };
|
||||||
|
if (t == termination.SIGNAL) { return "signal"; };
|
||||||
|
if (t == termination.TIMEOUT) { return "timeout"; };
|
||||||
|
return "setup";
|
||||||
|
};
|
||||||
442
test/wwfixture/integration.sh
Normal file
442
test/wwfixture/integration.sh
Normal file
@@ -0,0 +1,442 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
LC_ALL=C
|
||||||
|
export LC_ALL
|
||||||
|
|
||||||
|
repo=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
|
||||||
|
tool=$repo/out/bin/wwfixture
|
||||||
|
work=$(mktemp -d "${TMPDIR:-/tmp}/wwfixture-test.XXXXXXXX")
|
||||||
|
coordinator=
|
||||||
|
|
||||||
|
cleanup()
|
||||||
|
{
|
||||||
|
if [ -n "$coordinator" ]; then
|
||||||
|
kill -KILL "$coordinator" 2> /dev/null || :
|
||||||
|
wait "$coordinator" 2> /dev/null || :
|
||||||
|
fi
|
||||||
|
for stream in "$work"/*.tab; do
|
||||||
|
[ -f "$stream" ] || continue
|
||||||
|
awk -F '\t' '$1 == "case" && $10 ~ /^\/tmp\/[0-9a-f]{16}\/[0-9]{4}$/ {
|
||||||
|
p=$10; sub("/[0-9]{4}$", "", p); print p
|
||||||
|
}' "$stream"
|
||||||
|
done | sort -u | while IFS= read -r run; do
|
||||||
|
if [ -d "$run" ]; then
|
||||||
|
find "$run" -depth -delete
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
find "$work" -depth -delete
|
||||||
|
}
|
||||||
|
|
||||||
|
trap cleanup EXIT HUP INT TERM
|
||||||
|
|
||||||
|
fail()
|
||||||
|
{
|
||||||
|
echo "test/wwfixture: $*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
clone_corpus()
|
||||||
|
{
|
||||||
|
dest=$1
|
||||||
|
mkdir -p "$dest/test/wcc/data"
|
||||||
|
for source in "$repo"/test/wcc/data/*/case.ww; do
|
||||||
|
name=${source%/case.ww}
|
||||||
|
name=${name##*/}
|
||||||
|
mkdir "$dest/test/wcc/data/$name"
|
||||||
|
cp "$source" "$dest/test/wcc/data/$name/case.ww"
|
||||||
|
done
|
||||||
|
ln -s "$repo/out" "$dest/out"
|
||||||
|
ln -s "$repo/lib" "$dest/lib"
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_capture()
|
||||||
|
{
|
||||||
|
stream=$1
|
||||||
|
capture=$(awk -F '\t' '$1 == "case" && $10 != "-" { print $10; exit }' \
|
||||||
|
"$stream")
|
||||||
|
[ -n "$capture" ] || return 0
|
||||||
|
run=${capture%/*}
|
||||||
|
id=${run#/tmp/}
|
||||||
|
[ "$run" != "$id" ] || fail "capture outside /tmp"
|
||||||
|
[ "${#id}" -eq 16 ] || fail "malformed capture run ID"
|
||||||
|
case $id in *[!0-9a-f]*) fail "unsafe capture run ID" ;; esac
|
||||||
|
[ -d "$run" ] || fail "missing retained capture"
|
||||||
|
find "$run" -depth -delete
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_run()
|
||||||
|
{
|
||||||
|
run=$1
|
||||||
|
id=${run#/tmp/}
|
||||||
|
[ "$run" != "$id" ] || fail "retained run outside /tmp"
|
||||||
|
[ "${#id}" -eq 16 ] || fail "malformed retained run ID"
|
||||||
|
case $id in *[!0-9a-f]*) fail "unsafe retained run ID" ;; esac
|
||||||
|
[ -d "$run" ] || fail "missing retained run"
|
||||||
|
find "$run" -depth -delete
|
||||||
|
}
|
||||||
|
|
||||||
|
replace_directive()
|
||||||
|
{
|
||||||
|
path=$1
|
||||||
|
line=$2
|
||||||
|
tmp=$path.new
|
||||||
|
{
|
||||||
|
printf '%s\n' "$line"
|
||||||
|
sed -n '2,$p' "$path"
|
||||||
|
} > "$tmp"
|
||||||
|
mv "$tmp" "$path"
|
||||||
|
}
|
||||||
|
|
||||||
|
expect_invalid()
|
||||||
|
{
|
||||||
|
root=$1
|
||||||
|
pattern=$2
|
||||||
|
stream=$3
|
||||||
|
if "$tool" validate -root "$root" -run "$pattern" "$stream" \
|
||||||
|
> /dev/null 2>&1; then
|
||||||
|
fail "validator accepted $(basename -- "$stream")"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
base=$work/base
|
||||||
|
clone_corpus "$base"
|
||||||
|
|
||||||
|
# Filtering and listing publish identities only; no synthetic SKIP records.
|
||||||
|
list=$work/list.tab
|
||||||
|
"$tool" -root "$base" -run 'compiler.runww_dup_main_reject/c' -list > "$list"
|
||||||
|
[ "$(wc -l < "$list")" -eq 1 ] || fail "filtered list count"
|
||||||
|
[ "$(sed -n '1p' "$list")" = "compiler.runww_dup_main_reject c" ] \
|
||||||
|
|| fail "filtered list identity"
|
||||||
|
|
||||||
|
# Real positive fixtures must build successfully before their programs run.
|
||||||
|
runstream=$work/run-pass.tab
|
||||||
|
"$tool" -root "$base" -run 'compiler.idx_dot_aggret_subtail_run/c' \
|
||||||
|
-j 1 > "$runstream"
|
||||||
|
"$tool" validate -root "$base" \
|
||||||
|
-run 'compiler.idx_dot_aggret_subtail_run/c' "$runstream"
|
||||||
|
awk -F '\t' 'NR == 2 && $5 == "run" && $6 == "pass" && \
|
||||||
|
$7 == "exit" && $8 == "0" && $10 == "-" { ok=1 } \
|
||||||
|
END { exit !ok }' "$runstream" || fail "run fixture terminal row"
|
||||||
|
|
||||||
|
runexitstream=$work/run-exit-pass.tab
|
||||||
|
"$tool" -root "$base" -run 'compiler.runww_enum_run/c' -j 1 \
|
||||||
|
> "$runexitstream"
|
||||||
|
"$tool" validate -root "$base" -run 'compiler.runww_enum_run/c' \
|
||||||
|
"$runexitstream"
|
||||||
|
awk -F '\t' 'NR == 2 && $5 == "run" && $6 == "pass" && \
|
||||||
|
$7 == "exit" && $8 == "6" && $10 == "-" { ok=1 } \
|
||||||
|
END { exit !ok }' "$runexitstream" || fail "run-exit fixture terminal row"
|
||||||
|
|
||||||
|
# Compile fixtures stop at the frontend and publish one terminal row per stage.
|
||||||
|
compilepattern=compiler.r78_strict_package_main_accept
|
||||||
|
compilestream=$work/compile-pass.tab
|
||||||
|
"$tool" -root "$base" -run "$compilepattern" -j 1 > "$compilestream"
|
||||||
|
"$tool" validate -root "$base" -run "$compilepattern" "$compilestream"
|
||||||
|
awk -F '\t' 'NR == 1 && $7 == "2" { h=1 } \
|
||||||
|
$1 == "case" && $4 == "c" && $5 == "compile" && $6 == "pass" && \
|
||||||
|
$7 == "exit" && $8 == "0" && $10 == "-" { c=1 } \
|
||||||
|
$1 == "case" && $4 == "ww" && $5 == "compile" && $6 == "pass" && \
|
||||||
|
$7 == "exit" && $8 == "0" && $10 == "-" { w=1 } \
|
||||||
|
END { exit !(h && c && w) }' "$compilestream" \
|
||||||
|
|| fail "compile fixture terminal rows"
|
||||||
|
|
||||||
|
# A compile directive cannot turn a frontend rejection into a pass.
|
||||||
|
compilefailroot=$work/compile-failure
|
||||||
|
cp -R "$base" "$compilefailroot"
|
||||||
|
compilecase=$compilefailroot/test/wcc/data/r78_strict_package_main_accept/case.ww
|
||||||
|
printf '%s\n' '//ww:compile' 'package main;' 'fn main( {' > "$compilecase"
|
||||||
|
compilefailstream=$work/compile-failure.tab
|
||||||
|
if "$tool" -root "$compilefailroot" -run "$compilepattern/c" -j 1 \
|
||||||
|
> "$compilefailstream"; then
|
||||||
|
fail "compile rejection satisfied compile directive"
|
||||||
|
fi
|
||||||
|
"$tool" validate -root "$compilefailroot" -run "$compilepattern/c" \
|
||||||
|
"$compilefailstream"
|
||||||
|
awk -F '\t' 'NR == 2 && $5 == "compile" && $6 == "fail" && \
|
||||||
|
$7 == "exit" && $8 != "0" && $10 != "-" { ok=1 } \
|
||||||
|
END { exit !ok }' "$compilefailstream" \
|
||||||
|
|| fail "compile failure terminal row"
|
||||||
|
cleanup_capture "$compilefailstream"
|
||||||
|
|
||||||
|
# A build exit equal to the requested runtime exit is still a build failure.
|
||||||
|
buildroot=$work/build-failure
|
||||||
|
cp -R "$base" "$buildroot"
|
||||||
|
buildpattern=compiler.r816_one_elem
|
||||||
|
buildcase=$buildroot/test/wcc/data/r816_one_elem/case.ww
|
||||||
|
printf '%s\n' '//ww:run-exit 1' 'package main;' \
|
||||||
|
'fn main( {' > "$buildcase"
|
||||||
|
buildstream=$work/build-failure.tab
|
||||||
|
if "$tool" -root "$buildroot" \
|
||||||
|
-run "$buildpattern/c" -j 1 > "$buildstream"; then
|
||||||
|
fail "build failure satisfied runtime expectation"
|
||||||
|
fi
|
||||||
|
"$tool" validate -root "$buildroot" \
|
||||||
|
-run "$buildpattern/c" "$buildstream"
|
||||||
|
awk -F '\t' 'NR == 2 && $5 == "build" && $6 == "fail" && \
|
||||||
|
$7 == "exit" && $8 == "1" { ok=1 } END { exit !ok }' "$buildstream" \
|
||||||
|
|| fail "build failure terminal row"
|
||||||
|
cleanup_capture "$buildstream"
|
||||||
|
|
||||||
|
# A normal rejecting compile without the required stderr fragment fails.
|
||||||
|
diagroot=$work/diagnostic-mismatch
|
||||||
|
cp -R "$base" "$diagroot"
|
||||||
|
replace_directive "$diagroot/test/wcc/data/runww_dup_main_reject/case.ww" \
|
||||||
|
'//ww:error "fragment that is deliberately absent"'
|
||||||
|
diagstream=$work/diagnostic-mismatch.tab
|
||||||
|
if "$tool" -root "$diagroot" -run 'compiler.runww_dup_main_reject/c' \
|
||||||
|
-j 1 > "$diagstream"; then
|
||||||
|
fail "diagnostic mismatch passed"
|
||||||
|
fi
|
||||||
|
"$tool" validate -root "$diagroot" -run 'compiler.runww_dup_main_reject/c' \
|
||||||
|
"$diagstream"
|
||||||
|
awk -F '\t' 'NR == 2 && $5 == "compile" && $6 == "fail" && \
|
||||||
|
$7 == "exit" && $8 != "0" && $10 != "-" { ok=1 } \
|
||||||
|
END { exit !ok }' "$diagstream" || fail "diagnostic mismatch terminal row"
|
||||||
|
cleanup_capture "$diagstream"
|
||||||
|
|
||||||
|
# A labeled error directive selects the exact diagnostic for each stage.
|
||||||
|
stagediagpattern=compiler.r828_arrlit_ret_overlong
|
||||||
|
stagediagstream=$work/stage-diagnostic-pass.tab
|
||||||
|
"$tool" -root "$base" -run "$stagediagpattern" -j 1 > "$stagediagstream"
|
||||||
|
"$tool" validate -root "$base" -run "$stagediagpattern" "$stagediagstream"
|
||||||
|
awk -F '\t' '$1 == "case" && $4 == "c" && $5 == "compile" && \
|
||||||
|
$6 == "pass" && $7 == "exit" && $8 != "0" && $10 == "-" { c=1 } \
|
||||||
|
$1 == "case" && $4 == "ww" && $5 == "compile" && \
|
||||||
|
$6 == "pass" && $7 == "exit" && $8 != "0" && $10 == "-" { w=1 } \
|
||||||
|
END { exit !(c && w) }' "$stagediagstream" \
|
||||||
|
|| fail "stage-specific diagnostic terminal rows"
|
||||||
|
|
||||||
|
# Swapping the labeled fragments must fail both cells. This catches either
|
||||||
|
# fragment being shared across stages or treated as an unordered alternative.
|
||||||
|
stageswaproot=$work/stage-diagnostic-swapped
|
||||||
|
cp -R "$base" "$stageswaproot"
|
||||||
|
replace_directive \
|
||||||
|
"$stageswaproot/test/wcc/data/r828_arrlit_ret_overlong/case.ww" \
|
||||||
|
'//ww:error c "array literal has 3 elements but declared array holds 2" ww "return [3]int not assignable to [2]int"'
|
||||||
|
stageswapstream=$work/stage-diagnostic-swapped.tab
|
||||||
|
if "$tool" -root "$stageswaproot" -run "$stagediagpattern" -j 1 \
|
||||||
|
> "$stageswapstream"; then
|
||||||
|
fail "swapped stage-specific diagnostics passed"
|
||||||
|
fi
|
||||||
|
"$tool" validate -root "$stageswaproot" \
|
||||||
|
-run "$stagediagpattern" "$stageswapstream"
|
||||||
|
awk -F '\t' '$1 == "case" && $4 == "c" && $5 == "compile" && \
|
||||||
|
$6 == "fail" && $7 == "exit" && $8 != "0" && $10 != "-" { c=1 } \
|
||||||
|
$1 == "case" && $4 == "ww" && $5 == "compile" && \
|
||||||
|
$6 == "fail" && $7 == "exit" && $8 != "0" && $10 != "-" { w=1 } \
|
||||||
|
END { exit !(c && w) }' "$stageswapstream" \
|
||||||
|
|| fail "stage-specific diagnostics were shared across stages"
|
||||||
|
cleanup_capture "$stageswapstream"
|
||||||
|
|
||||||
|
# Signals remain signals and can never satisfy a numeric runtime expectation.
|
||||||
|
signalroot=$work/signal
|
||||||
|
cp -R "$base" "$signalroot"
|
||||||
|
signalcase=$signalroot/test/wcc/data/idx_dot_aggret_subtail_run/case.ww
|
||||||
|
printf '%s\n' '//ww:run' 'package main;' 'import os;' \
|
||||||
|
'export fn main() int = {' \
|
||||||
|
' os.kill(os.getpid(), os.SIGKILL);' ' return 0;' '};' > "$signalcase"
|
||||||
|
signalstream=$work/signal.tab
|
||||||
|
if "$tool" -root "$signalroot" -run 'compiler.idx_dot_aggret_subtail_run/c' \
|
||||||
|
-j 1 > "$signalstream"; then
|
||||||
|
fail "signaled fixture passed"
|
||||||
|
fi
|
||||||
|
"$tool" validate -root "$signalroot" \
|
||||||
|
-run 'compiler.idx_dot_aggret_subtail_run/c' "$signalstream"
|
||||||
|
awk -F '\t' 'NR == 2 && $5 == "run" && $6 == "fail" && \
|
||||||
|
$7 == "signal" && $8 == "9" { ok=1 } END { exit !ok }' "$signalstream" \
|
||||||
|
|| fail "signal terminal row"
|
||||||
|
cleanup_capture "$signalstream"
|
||||||
|
|
||||||
|
# One total deadline covers build and run; the busy program is terminated.
|
||||||
|
timeoutroot=$work/timeout
|
||||||
|
cp -R "$base" "$timeoutroot"
|
||||||
|
timeoutcase=$timeoutroot/test/wcc/data/idx_dot_aggret_subtail_run/case.ww
|
||||||
|
printf '%s\n' '//ww:run' 'package main;' 'export fn main() int = {' \
|
||||||
|
' for (true) { };' ' return 0;' '};' > "$timeoutcase"
|
||||||
|
timeoutstream=$work/timeout.tab
|
||||||
|
if "$tool" -root "$timeoutroot" -run 'compiler.idx_dot_aggret_subtail_run/c' \
|
||||||
|
-j 1 -timeout-ms 250 > "$timeoutstream"; then
|
||||||
|
fail "timed out fixture passed"
|
||||||
|
fi
|
||||||
|
"$tool" validate -root "$timeoutroot" \
|
||||||
|
-run 'compiler.idx_dot_aggret_subtail_run/c' "$timeoutstream"
|
||||||
|
awk -F '\t' 'NR == 2 && $6 == "fail" && $7 == "timeout" && \
|
||||||
|
$8 == "0" && $9 >= 200000000 { ok=1 } END { exit !ok }' "$timeoutstream" \
|
||||||
|
|| fail "timeout terminal row"
|
||||||
|
cleanup_capture "$timeoutstream"
|
||||||
|
|
||||||
|
# The real coordinator consumes SIGTERM, owns active cleanup, and still emits
|
||||||
|
# exactly one terminal record for the active cell and the queued cell.
|
||||||
|
interruptstream=$work/interrupted.tab
|
||||||
|
"$tool" -root "$timeoutroot" -run 'compiler.idx_dot_aggret_subtail_run' \
|
||||||
|
-j 1 -timeout-ms 10000 > "$interruptstream" &
|
||||||
|
coordinator=$!
|
||||||
|
program=
|
||||||
|
tries=0
|
||||||
|
while [ "$tries" -lt 300 ]; do
|
||||||
|
if [ -r "/proc/$coordinator/task/$coordinator/children" ]; then
|
||||||
|
for child in $(sed -n '1p' "/proc/$coordinator/task/$coordinator/children"); do
|
||||||
|
exe=$(readlink "/proc/$child/exe" 2> /dev/null || :)
|
||||||
|
case $exe in /tmp/*/program) program=$child ;; esac
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
[ -z "$program" ] || break
|
||||||
|
tries=$((tries + 1))
|
||||||
|
sleep 0.01
|
||||||
|
done
|
||||||
|
[ -n "$program" ] || fail "coordinator runtime child did not start"
|
||||||
|
kill -TERM "$coordinator"
|
||||||
|
if wait "$coordinator"; then
|
||||||
|
fail "interrupted coordinator returned success"
|
||||||
|
fi
|
||||||
|
coordinator=
|
||||||
|
[ ! -e "/proc/$program" ] || fail "interrupted leader was not reaped"
|
||||||
|
"$tool" validate -root "$timeoutroot" \
|
||||||
|
-run 'compiler.idx_dot_aggret_subtail_run' "$interruptstream"
|
||||||
|
awk -F '\t' 'NR == 1 && $7 == "2" { h=1 } \
|
||||||
|
NR == 2 && $2 == "1" && $5 == "run" && $6 == "error" && \
|
||||||
|
$7 == "signal" && $8 == "15" { a=1 } \
|
||||||
|
NR == 3 && $2 == "2" && $5 == "build" && $6 == "error" && \
|
||||||
|
$7 == "setup" && $8 == "-4" && $9 == "0" { q=1 } \
|
||||||
|
NR == 4 && $1 == "end" && $2 == "2" && $3 == "2" { e=1 } \
|
||||||
|
END { exit !(h && a && q && e) }' "$interruptstream" \
|
||||||
|
|| fail "interrupted coordinator terminal accounting"
|
||||||
|
cleanup_capture "$interruptstream"
|
||||||
|
|
||||||
|
# Directive parsing and corpus pinning happen before filtering or execution.
|
||||||
|
malformed=$work/malformed
|
||||||
|
cp -R "$base" "$malformed"
|
||||||
|
replace_directive "$malformed/test/wcc/data/runww_dup_main_reject/case.ww" \
|
||||||
|
'//ww:run-exit 01'
|
||||||
|
if "$tool" -root "$malformed" -list > "$work/malformed.out" \
|
||||||
|
2> "$work/malformed.err"; then
|
||||||
|
fail "malformed directive accepted"
|
||||||
|
fi
|
||||||
|
[ ! -s "$work/malformed.out" ] || fail "malformed directive published output"
|
||||||
|
|
||||||
|
stagemalformed=$work/stage-malformed
|
||||||
|
cp -R "$base" "$stagemalformed"
|
||||||
|
stagemalformedcase=$stagemalformed/test/wcc/data/r828_arrlit_ret_overlong/case.ww
|
||||||
|
stagemalformedn=0
|
||||||
|
for directive in \
|
||||||
|
'//ww:error c "" ww "WW fragment"' \
|
||||||
|
'//ww:error c "C fragment" ww ""' \
|
||||||
|
'//ww:error c "C fragment" c "WW fragment"' \
|
||||||
|
'//ww:error c "C fragment" ww "WW fragment" trailing'
|
||||||
|
do
|
||||||
|
stagemalformedn=$((stagemalformedn + 1))
|
||||||
|
replace_directive "$stagemalformedcase" "$directive"
|
||||||
|
out=$work/stage-malformed-$stagemalformedn.out
|
||||||
|
err=$work/stage-malformed-$stagemalformedn.err
|
||||||
|
if "$tool" -root "$stagemalformed" -list > "$out" 2> "$err"; then
|
||||||
|
fail "malformed stage-specific directive accepted"
|
||||||
|
fi
|
||||||
|
[ ! -s "$out" ] \
|
||||||
|
|| fail "malformed stage-specific directive published output"
|
||||||
|
grep -F 'malformed stage-specific error directive' "$err" > /dev/null \
|
||||||
|
|| fail "wrong malformed stage-specific diagnostic"
|
||||||
|
done
|
||||||
|
|
||||||
|
countdrift=$work/count-drift
|
||||||
|
cp -R "$base" "$countdrift"
|
||||||
|
find "$countdrift/test/wcc/data/a2s_mismatch_callarg" -depth -delete
|
||||||
|
if "$tool" -root "$countdrift" -list > "$work/count.out" \
|
||||||
|
2> "$work/count.err"; then
|
||||||
|
fail "corpus count drift accepted"
|
||||||
|
fi
|
||||||
|
[ ! -s "$work/count.out" ] || fail "count drift published output"
|
||||||
|
|
||||||
|
hashdrift=$work/hash-drift
|
||||||
|
cp -R "$base" "$hashdrift"
|
||||||
|
mv "$hashdrift/test/wcc/data/a2s_mismatch_callarg" \
|
||||||
|
"$hashdrift/test/wcc/data/a2s_mismatch_callarg_changed"
|
||||||
|
if "$tool" -root "$hashdrift" -list > "$work/hash.out" \
|
||||||
|
2> "$work/hash.err"; then
|
||||||
|
fail "corpus hash drift accepted"
|
||||||
|
fi
|
||||||
|
[ ! -s "$work/hash.out" ] || fail "hash drift published output"
|
||||||
|
|
||||||
|
# A terminal-stream sink failure must force a nonzero producer status.
|
||||||
|
if "$tool" -root "$base" -run 'compiler.runww_dup_main_reject/c' -j 1 \
|
||||||
|
> /dev/full 2> "$work/full.err"; then
|
||||||
|
fail "result publication failure returned success"
|
||||||
|
fi
|
||||||
|
publication=$(sed -n 's/^wwfixture: .*; capture: \(\/tmp\/[0-9a-f][0-9a-f]*\)$/\1/p' \
|
||||||
|
"$work/full.err")
|
||||||
|
[ -n "$publication" ] || fail "publication failure capture was not retained"
|
||||||
|
[ -f "$publication/publication.txt" ] \
|
||||||
|
|| fail "publication failure diagnostic was not retained"
|
||||||
|
cleanup_run "$publication"
|
||||||
|
|
||||||
|
# Strict decoder: start with an independently produced, valid two-cell stream.
|
||||||
|
good=$work/good.tab
|
||||||
|
pattern=compiler.runww_dup_main_reject
|
||||||
|
"$tool" -root "$base" -run "$pattern" -j 1 > "$good"
|
||||||
|
"$tool" validate -root "$base" -run "$pattern" "$good"
|
||||||
|
|
||||||
|
bad=$work/bad-no-header.tab
|
||||||
|
sed '1d' "$good" > "$bad"
|
||||||
|
expect_invalid "$base" "$pattern" "$bad"
|
||||||
|
|
||||||
|
bad=$work/bad-version.tab
|
||||||
|
awk -F '\t' 'BEGIN { OFS="\t" } NR == 1 { $2=2 } { print }' "$good" > "$bad"
|
||||||
|
expect_invalid "$base" "$pattern" "$bad"
|
||||||
|
|
||||||
|
bad=$work/bad-corpus.tab
|
||||||
|
awk -F '\t' 'BEGIN { OFS="\t" } NR == 1 { $4=105 } { print }' "$good" > "$bad"
|
||||||
|
expect_invalid "$base" "$pattern" "$bad"
|
||||||
|
|
||||||
|
bad=$work/bad-missing-ordinal.tab
|
||||||
|
sed '2d' "$good" > "$bad"
|
||||||
|
expect_invalid "$base" "$pattern" "$bad"
|
||||||
|
|
||||||
|
bad=$work/bad-duplicate-ordinal.tab
|
||||||
|
awk 'NR == 2 { print; print; next } { print }' "$good" > "$bad"
|
||||||
|
expect_invalid "$base" "$pattern" "$bad"
|
||||||
|
|
||||||
|
bad=$work/bad-identity.tab
|
||||||
|
awk -F '\t' 'BEGIN { OFS="\t" } NR == 2 { $3="compiler.not_selected" } \
|
||||||
|
{ print }' "$good" > "$bad"
|
||||||
|
expect_invalid "$base" "$pattern" "$bad"
|
||||||
|
|
||||||
|
bad=$work/bad-stage.tab
|
||||||
|
awk -F '\t' 'BEGIN { OFS="\t" } NR == 2 { $4="ww" } { print }' \
|
||||||
|
"$good" > "$bad"
|
||||||
|
expect_invalid "$base" "$pattern" "$bad"
|
||||||
|
|
||||||
|
bad=$work/bad-field-count.tab
|
||||||
|
awk -F '\t' 'BEGIN { OFS="\t" } NR == 2 { print $0, "extra"; next } \
|
||||||
|
{ print }' "$good" > "$bad"
|
||||||
|
expect_invalid "$base" "$pattern" "$bad"
|
||||||
|
|
||||||
|
bad=$work/bad-enum.tab
|
||||||
|
awk -F '\t' 'BEGIN { OFS="\t" } NR == 2 { $6="bogus" } { print }' \
|
||||||
|
"$good" > "$bad"
|
||||||
|
expect_invalid "$base" "$pattern" "$bad"
|
||||||
|
|
||||||
|
bad=$work/bad-contradiction.tab
|
||||||
|
awk -F '\t' 'BEGIN { OFS="\t" } NR == 2 { $8=0 } { print }' "$good" > "$bad"
|
||||||
|
expect_invalid "$base" "$pattern" "$bad"
|
||||||
|
|
||||||
|
bad=$work/bad-incomplete-line.tab
|
||||||
|
bytes=$(wc -c < "$good")
|
||||||
|
dd if="$good" of="$bad" bs=1 count=$((bytes - 1)) 2> /dev/null
|
||||||
|
expect_invalid "$base" "$pattern" "$bad"
|
||||||
|
|
||||||
|
bad=$work/bad-missing-footer.tab
|
||||||
|
sed '$d' "$good" > "$bad"
|
||||||
|
expect_invalid "$base" "$pattern" "$bad"
|
||||||
|
|
||||||
|
bad=$work/bad-footer.tab
|
||||||
|
awk -F '\t' 'BEGIN { OFS="\t" } $1 == "end" { $3=1 } { print }' \
|
||||||
|
"$good" > "$bad"
|
||||||
|
expect_invalid "$base" "$pattern" "$bad"
|
||||||
|
|
||||||
|
bad=$work/bad-trailing.tab
|
||||||
|
cp "$good" "$bad"
|
||||||
|
printf '%s\n' 'case 3 compiler.trailing c compile pass exit 1 0 -' \
|
||||||
|
>> "$bad"
|
||||||
|
expect_invalid "$base" "$pattern" "$bad"
|
||||||
|
|
||||||
|
echo "wwfixture integration: PASS"
|
||||||
Reference in New Issue
Block a user