test: port 989_lib_byteid to ww; birth test/testenv + test/byteid
test/testenv (package testenv) is the shared plumbing for ww-native test drivers — subprocess launch with captured output, file IO, string search, byte-sorted directory listing, scratch ownership — lifted from the proven package_test.ww idiom; a utility, not a framework. test/byteid/libbyteid_test.ww carries every assertion of the retired C carrier: the 44-entry roster (fixtures + sentinel-guarded import probes + the zero-dep root-only build), both-stage sep builds with the resolved-unit proof, per-package .s concat compare with the empty-concat guard, the ID/DIVERGE/WWREJECT pin discipline, and the lib/ corpus completeness scan (negative-verified against a planted un-enrolled module). Concat order strengthened from shell-glob to explicit byte-lexicographic. The roster fills by cursor-driven field writes because append() rejects struct-call-result sources (#34). Byteid carriers 11 -> 10; docs counts move; test-byteid runs the ww test via the wwtest/ pattern with WW_TEST_REPO.
This commit is contained in:
235
test/testenv/testenv.ww
Normal file
235
test/testenv/testenv.ww
Normal file
@@ -0,0 +1,235 @@
|
||||
package testenv;
|
||||
|
||||
// Shared plumbing for ww-native test drivers under test/: subprocess
|
||||
// launch with captured output, file IO, string search, and scratch
|
||||
// ownership. A utility surface lifted from the proven
|
||||
// test/package/package_test.ww idiom — not a framework: callers own
|
||||
// discovery, assertions, and verdicts.
|
||||
|
||||
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 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 = {
|
||||
let c: exec.command;
|
||||
c.path = argv[0];
|
||||
c.argv = argv;
|
||||
c.env = os.getenvs();
|
||||
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;
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
// 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;
|
||||
};
|
||||
Reference in New Issue
Block a user