Files
ww/test/testenv/testenv.ww

316 lines
8.6 KiB
Plaintext

package testenv;
// A utility surface lifted from the proven test/package/package_test.ww
// idiom — not a framework: callers own discovery, assertions, and
// verdicts.
import endian;
import os;
import os.exec;
import strings;
import temp;
import time;
export type commandout = struct {
termination: exec.termination,
code: i32,
stdout: str,
stderr: str,
};
fn envrequired(name: str) str = {
match (os.getenv(name)) {
case let value: str => {
assert(value.len != 0);
return strings.dup(value);
};
case void => abort("missing test environment");
};
};
// The repository root, provided by the Make target as WW_TEST_REPO.
export fn repo() str = { return envrequired("WW_TEST_REPO"); };
export fn driver(name: str) str = {
return strings.concat(repo(), "/out/bin/", name);
};
export fn fresh() str = { return strings.dup(temp.dir()); };
export fn readfile(path: str) str = {
let fd: i32 = os.open(path, os.flag.RDONLY, 0i32);
assert(fd >= 0);
let sr: (i64 | os.oserror) = os.filesize(fd);
let n: i64 = -1i64;
match (sr) {
case let v: i64 => n = v;
case let e: os.oserror => abort("filesize failed");
};
assert(n >= 0i64);
let b: []u8 = alloc([], (n + 1i64): u64)!;
b.len = (n + 1i64): i32;
let rr: (i64 | os.oserror) = os.readall(fd, b.ptr, n: u64);
os.close(fd);
let got: i64 = -1i64;
match (rr) {
case let v: i64 => got = v;
case let e: os.oserror => abort("read failed");
};
assert(got == n);
let ni: i32 = n: i32;
b[ni] = 0u8;
let out: str;
out.ptr = b.ptr;
out.len = n: i32;
return out;
};
export fn writefile(path: str, content: str) void = {
let fd: i32 = os.open(path,
os.flag.WRONLY | os.flag.CREATE | os.flag.EXCL, 384i32);
assert(fd >= 0);
match (os.writeall(fd, content.ptr, content.len: u64)) {
case let n: i64 => assert(n == content.len: i64);
case let e: os.oserror => abort("write failed");
};
assert(os.close(fd) == 0);
};
export fn writeexecutable(path: str, content: str) void = {
let fd: i32 = os.open(path,
os.flag.WRONLY | os.flag.CREATE | os.flag.EXCL, 448i32);
assert(fd >= 0);
match (os.writeall(fd, content.ptr, content.len: u64)) {
case let n: i64 => assert(n == content.len: i64);
case let e: os.oserror => abort("write failed");
};
assert(os.close(fd) == 0);
};
export fn exists(path: str) bool = {
return os.access(path, 0i32) == 0;
};
export fn isdir(path: str) bool = {
let st: os.filestat;
match (os.stat(&st, path)) {
case void => {
return ((st.mode: u32) & 61440u32) == (os.mode.DIR: u32);
};
case let e: os.oserror => return false;
};
};
// Run argv[0] with `dir` as the working directory, capturing stdout and
// stderr into `<root>/<name>.{stdout,stderr}`. Launch and cleanup
// failures abort; termination and exit code are the caller's verdict.
export fn runcommand(dir: str, root: str, name: str, argv: []str,
lifetime: time.duration, out: *commandout) void = {
runcommandenv(dir, root, name, argv, os.getenvs(), lifetime, out);
};
// Explicit-env variant of [[runcommand]] for arranger observers that
// must control the child's environment per row (exec.command.env is
// caller-built; ww ships no setenv primitive, deliberately).
export fn runcommandenv(dir: str, root: str, name: str, argv: []str,
env: []str, lifetime: time.duration, out: *commandout) void = {
let c: exec.command;
c.path = argv[0];
c.argv = argv;
c.env = env;
c.dir = dir;
c.stdoutpath = strings.concat(root, "/", name, ".stdout");
c.stderrpath = strings.concat(root, "/", name, ".stderr");
c.deadline = time.add(time.now(time.clock.monotonic), lifetime);
c.grace = (100i64 * (time.millisecond: i64)): time.duration;
let r: exec.result;
exec.run(&c, &r);
assert(r.errno == 0 && r.cleanuperrno == 0);
out.termination = r.termination;
out.code = r.code;
out.stdout = readfile(c.stdoutpath);
out.stderr = readfile(c.stderrpath);
};
export fn clean(root: str) void = {
let av: []str = ["/bin/rm", "-rf", "--", root];
let c: exec.command;
c.path = av[0];
c.argv = av;
c.env = os.getenvs();
c.dir = "/";
c.stdoutpath = strings.concat(root, ".cleanup.stdout");
c.stderrpath = strings.concat(root, ".cleanup.stderr");
c.deadline = time.add(time.now(time.clock.monotonic), time.second);
c.grace = (50i64 * (time.millisecond: i64)): time.duration;
let r: exec.result;
exec.run(&c, &r);
assert(r.termination == exec.termination.EXIT && r.code == 0);
assert(os.remove(c.stdoutpath) == 0);
assert(os.remove(c.stderrpath) == 0);
};
export fn pos(haystack: str, needle: str) i32 = {
match (strings.index(haystack, needle)) {
case let n: i32 => return n;
case void => return -1;
};
};
export fn has(haystack: str, needle: str) bool = {
return pos(haystack, needle) >= 0;
};
export fn same(a: str, b: str) bool = {
if (a.len != b.len) { return false; };
let i: i32 = 0;
for (i < a.len) {
if (a[i] != b[i]) { return false; };
i += 1;
};
return true;
};
// The ELF observers under test/object read header fields straight out
// of readfile's byte-carrying str; these getters reinterpret it as
// bytes and route through endian's canonical little-endian readers
// instead of re-deriving the bit math. u64 uniformly so one call
// shape covers u16/u32/u64 fields; callers narrow for offset math.
export fn leu16(s: str, off: i32) u64 = {
let b: []u8;
b.ptr = &s.ptr[off];
b.len = 2;
b.cap = 2;
return endian.legetu16(b): u64;
};
export fn leu32(s: str, off: i32) u64 = {
let b: []u8;
b.ptr = &s.ptr[off];
b.len = 4;
b.cap = 4;
return endian.legetu32(b): u64;
};
export fn leu64(s: str, off: i32) u64 = {
let b: []u8;
b.ptr = &s.ptr[off];
b.len = 8;
b.cap = 8;
return endian.legetu64(b);
};
export fn occurrences(haystack: str, needle: str) i32 = {
if (needle.len == 0 || haystack.len < needle.len) { return 0; };
let count: i32 = 0;
let i: i32 = 0;
for (i + needle.len <= haystack.len) {
let j: i32 = 0;
let hit: bool = true;
for (j < needle.len) {
if (haystack[i + j] != needle[j]) { hit = false; j = needle.len; }
else { j += 1; };
};
if (hit) { count += 1; };
i += 1;
};
return count;
};
// Expected semantic identity for a canonical absolute directory that is not
// representable below an active source root. Keep this independent observer
// byte-for-byte aligned with the documented driver protocol.
export fn localpackageidentity(dir: str, leaf: str) str = {
let out: []u8 = alloc([], (dir.len * 4 + leaf.len + 16): u64)!;
let prefix: str = "__wwlocal.p";
let i: i32 = 0;
for (i < prefix.len) { append(out, prefix[i]); i += 1; };
let hex: str = "0123456789abcdef";
i = 0;
for (i < dir.len) {
let c: u8 = dir[i];
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
|| (c >= '0' && c <= '9')) {
append(out, c);
} else if (c == '_') {
append(out, '_'); append(out, 'u');
} else if (c == '/') {
append(out, '_'); append(out, 's');
} else {
let high: i32 = (c / 16u8): i32;
let low: i32 = (c % 16u8): i32;
append(out, '_'); append(out, 'x');
append(out, hex[high]); append(out, hex[low]);
};
i += 1;
};
append(out, '.');
i = 0;
for (i < leaf.len) { append(out, leaf[i]); i += 1; };
return strings.frombytes(out);
};
// Byte-lexicographic order, so directory listings sort identically on
// every host regardless of locale.
export fn lexless(a: str, b: str) bool = {
let n: i32 = a.len;
if (b.len < n) { n = b.len; };
let i: i32 = 0;
for (i < n) {
if (a[i] != b[i]) { return a[i] < b[i]; };
i += 1;
};
return a.len < b.len;
};
def DIRBUF: i32 = 16384;
// Entry names of `path` (excluding . and ..), byte-sorted. Aborts on an
// unreadable directory or a malformed dirent stream.
export fn listdir(path: str) []str = {
let names: []str = alloc([], 16u64)!;
let fd: i32 = os.open(path, os.flag.RDONLY, 0i32);
assert(fd >= 0);
let buf: []u8 = alloc([], DIRBUF: u64)!;
buf.len = DIRBUF;
let n: i64 = os.getdents64(fd, buf.ptr, buf.len: u64);
for (n > 0i64) {
let off: i32 = 0;
for (off < n: i32) {
// linux_dirent64: d_reclen at 16..17, name at 19.
let reclen: i32 = (buf[off + 16]: i32)
+ ((buf[off + 17]: i32) * 256);
assert(reclen >= 20 && off + reclen <= n: i32);
let nb: []u8 = alloc([], 256u64)!;
let k: i32 = 0;
for (buf[off + 19 + k] != 0u8) {
append(nb, buf[off + 19 + k]);
k += 1;
};
let name: str = strings.frombytes(nb);
if (!same(name, ".") && !same(name, "..")) {
append(names, name);
};
off += reclen;
};
n = os.getdents64(fd, buf.ptr, buf.len: u64);
};
assert(n == 0i64);
assert(os.close(fd) == 0);
// insertion sort; listings are small
let i: i32 = 1;
for (i < names.len) {
let v: str = names[i];
let j: i32 = i;
for (j > 0) {
if (lexless(v, names[j - 1])) {
names[j] = names[j - 1];
j -= 1;
} else { break; };
};
names[j] = v;
i += 1;
};
return names;
};