30 lib test files: package <mod> -> <mod>_test, the sanctioned Go-over-Hare departure (CLAUDE.md rule-9 carve-out; white-box testing deferred, not forbidden). decimaltest retired per .ai/rob-16-decimaltest-ruling.md: Hare ships no decimal_test.ha (engine covered transitively); coverage migrated losslessly into stoftest rows (all-9s carry, nd>19 pure-decimal) + ftostest f64_roundtrip mirroring ref/hare strconv ftos_test.ha tcsf64; k=0 micro-gap documented at site. regex_test keeps its white-box internal probes under a documented rule-7 divergence (rehome = task #9). 922_decimal_run + its 989 byte-id row removed with the fixture.
270 lines
8.0 KiB
Plaintext
270 lines
8.0 KiB
Plaintext
// temptest — exercises lib/temp. Run with
|
|
// `out/bin/ww build lib/temp/temptest.ww && ./temptest`.
|
|
//
|
|
// Every @test enumerates parallel `[N]T` arrays of inputs and
|
|
// expectations, then iterates one body across them. Parallel arrays
|
|
// (rather than `[N]struct{...}`) sidestep the cstage cgen's chained
|
|
// `arr[i].field` store gap (task #6).
|
|
//
|
|
// Cleanup is the tests' responsibility — temp ships no defer hook
|
|
// (deliberate divergence from Hare; see lib/temp/temp.ww header).
|
|
// Each test [[os.close]]s every fd it opens and [[os.remove]] /
|
|
// [[os.rmdir]]s every path it returns, so a regression here would
|
|
// leave detritus under /tmp. `ls /tmp` before/after each run should
|
|
// match.
|
|
|
|
package temp_test;
|
|
|
|
import os;
|
|
import temp;
|
|
|
|
// Direct exit(2) binding rather than mixing `use io;` and `use os;` —
|
|
// they share read/write/close names under the driver's flat-scope
|
|
// concat (task #7). We don't need lib/io here at all (raw fd ops
|
|
// via os.read/os.write/os.close suffice), but the binding shape
|
|
// mirrors memio/getopt for parity.
|
|
fn doexit(code: i32) void = { os.exit(code); };
|
|
|
|
// signalled — bumped by main before each test so a failing exit
|
|
// code pinpoints the offending case.
|
|
let signalled: i32 = 0;
|
|
|
|
fn fail() void = { doexit(signalled + 10); };
|
|
|
|
fn streq(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;
|
|
};
|
|
|
|
// ---- namedroundtrip: write + read-back across payload sizes ------------
|
|
|
|
@test fn namedroundtrip() void = {
|
|
// (payload size, fill byte). Row 0 covers the zero-byte edge.
|
|
let sz: [4]i32;
|
|
let fill: [4]u8;
|
|
sz[0]=0; fill[0]=0u8;
|
|
sz[1]=1; fill[1]=33u8; // '!'
|
|
sz[2]=4; fill[2]=65u8; // 'A'
|
|
sz[3]=64; fill[3]=90u8; // 'Z'
|
|
|
|
let i: i32 = 0;
|
|
for (i < 4) {
|
|
let fd: i32 = 0;
|
|
let p: str;
|
|
match (temp.named(&fd, &p, "/tmp", temp.mode.RDWR, 384i32)) { // 0o600
|
|
case void => {};
|
|
case let e: os.oserror => fail();
|
|
};
|
|
if (fd < 0) { fail(); };
|
|
if (p.len < 10) { fail(); }; // at minimum "/tmp/temp.<1 hex>"
|
|
|
|
// Path bytes must start with "/tmp/temp." and live in temp's
|
|
// static buffer (NUL-terminated for syscall handoff).
|
|
if (p[0] != 47u8) { fail(); }; // '/'
|
|
if (!streq(strslice(p, 0, 10), "/tmp/temp.")) { fail(); };
|
|
if (p.ptr[p.len] != 0u8) { fail(); };
|
|
|
|
// Build fill payload, write it, lseek to 0, read it back.
|
|
let wbuf: [64]u8;
|
|
let k: i32 = 0;
|
|
for (k < sz[i]) { wbuf[k] = fill[i]; k += 1; };
|
|
if (sz[i] > 0) {
|
|
let wr: i64 = os.write(fd, &wbuf[0], sz[i]: u64);
|
|
if (wr != sz[i]: i64) { fail(); };
|
|
};
|
|
|
|
let r: i64 = os.lseek(fd, 0i64, os.whence.SET);
|
|
if (r != 0i64) { fail(); };
|
|
|
|
let rbuf: [64]u8;
|
|
let z: i32 = 0;
|
|
for (z < 64) { rbuf[z] = 0u8; z += 1; };
|
|
let rd: i64 = 0i64;
|
|
if (sz[i] > 0) {
|
|
rd = os.read(fd, &rbuf[0], sz[i]: u64);
|
|
if (rd != sz[i]: i64) { fail(); };
|
|
};
|
|
let k2: i32 = 0;
|
|
for (k2 < sz[i]) {
|
|
if (rbuf[k2] != fill[i]) { fail(); };
|
|
k2 += 1;
|
|
};
|
|
|
|
// File exists pre-cleanup.
|
|
if (os.access(p, 0i32) != 0) { fail(); };
|
|
|
|
if (os.close(fd) != 0) { fail(); };
|
|
if (os.remove(p) != 0) { fail(); };
|
|
|
|
// Cleanup landed.
|
|
if (os.access(p, 0i32) == 0) { fail(); };
|
|
|
|
i += 1;
|
|
};
|
|
};
|
|
|
|
// strslice — borrow `p[lo:hi]`. Inline because lib/strings.sub
|
|
// returns an allocated copy in some shapes; here we want a view.
|
|
fn strslice(p: str, lo: i32, hi: i32) str = {
|
|
let r: str;
|
|
r.ptr = p.ptr + (lo: u64);
|
|
r.len = hi - lo;
|
|
return r;
|
|
};
|
|
|
|
// ---- namedoverwrite: static buffer is reused across calls --------------
|
|
//
|
|
// Hare docs: "The name is statically allocated, and will be
|
|
// overwritten on subsequent calls." Match that contract — the second
|
|
// named() call lands in the same buffer, so p1.ptr == p2.ptr.
|
|
@test fn namedoverwrite() void = {
|
|
let fd1: i32 = 0;
|
|
let p1: str;
|
|
match (temp.named(&fd1, &p1, "/tmp", temp.mode.WRITE, 384i32)) {
|
|
case void => {};
|
|
case let e: os.oserror => fail();
|
|
};
|
|
|
|
// Snapshot p1's bytes BEFORE the second call clobbers the buffer,
|
|
// so we can compare p2 against the original p1 content and
|
|
// remove() the first file after closing it.
|
|
let snap: [128]u8;
|
|
let snaplen: i32 = p1.len;
|
|
let i: i32 = 0;
|
|
for (i < p1.len) { snap[i] = p1[i]; i += 1; };
|
|
snap[p1.len] = 0u8;
|
|
let psnap: str;
|
|
psnap.ptr = &snap[0];
|
|
psnap.len = snaplen;
|
|
|
|
let fd2: i32 = 0;
|
|
let p2: str;
|
|
match (temp.named(&fd2, &p2, "/tmp", temp.mode.WRITE, 384i32)) {
|
|
case void => {};
|
|
case let e: os.oserror => fail();
|
|
};
|
|
|
|
// Same buffer (Hare docs: "overwritten on subsequent calls"),
|
|
// distinct path bytes (random suffix differs).
|
|
if (p1.ptr != p2.ptr) { fail(); };
|
|
if (streq(strslice(p2, 0, p2.len), psnap)) { fail(); };
|
|
|
|
// Both fds are distinct.
|
|
if (fd1 == fd2) { fail(); };
|
|
|
|
if (os.close(fd2) != 0) { fail(); };
|
|
if (os.remove(p2) != 0) { fail(); };
|
|
|
|
if (os.close(fd1) != 0) { fail(); };
|
|
if (os.remove(psnap) != 0) { fail(); };
|
|
};
|
|
|
|
// NOTE: temp.file() has no test of its own. The function is a thin
|
|
// wrapper around temp.named() that DROPS the returned path, and the
|
|
// on-disk entry would leak until external cleanup (no O_TMPFILE in
|
|
// lib/os yet — see lib/temp/temp.ww header). Hare's +freebsd.ha
|
|
// has the same leak; the Hare +linux.ha fallback path does too.
|
|
// Re-add a file() test once O_TMPFILE lands and the leak goes away.
|
|
// Coverage for the underlying open+create+EXCL path lives in
|
|
// [[namedroundtrip]] / [[namedoverwrite]].
|
|
|
|
// ---- dirlifecycle: empty dir, then dir + one child file ----------------
|
|
|
|
@test fn dirlifecycle() void = {
|
|
// (child count). Row 0: empty dir. Row 1: dir + one file.
|
|
let childn: [2]i32;
|
|
childn[0] = 0;
|
|
childn[1] = 1;
|
|
|
|
let i: i32 = 0;
|
|
for (i < 2) {
|
|
let d: str = temp.dir();
|
|
if (d.len < 6) { fail(); };
|
|
if (!streq(strslice(d, 0, 5), "/tmp/")) { fail(); };
|
|
if (d.ptr[d.len] != 0u8) { fail(); };
|
|
|
|
// Dir exists.
|
|
if (os.access(d, 0i32) != 0) { fail(); };
|
|
|
|
// Snapshot the dir path into a local NUL-terminated buffer:
|
|
// the os.* path entrypoints now copy through lib/os.pathbuf
|
|
// (kpath), so d's view into temp.pathbuf is safe; the
|
|
// snapshot still buys robustness against future helpers
|
|
// that might share temp's buffer.
|
|
let dsnap: [128]u8;
|
|
let dlen: i32 = d.len;
|
|
let s: i32 = 0;
|
|
for (s < d.len) { dsnap[s] = d[s]; s += 1; };
|
|
dsnap[d.len] = 0u8;
|
|
let dview: str;
|
|
dview.ptr = &dsnap[0]; dview.len = dlen;
|
|
|
|
if (childn[i] > 0) {
|
|
// Build "<d>/x\0" in a local buffer.
|
|
let cbuf: [144]u8;
|
|
let off: i32 = 0;
|
|
let j: i32 = 0;
|
|
for (j < dlen) { cbuf[off] = dsnap[j]; off += 1; j += 1; };
|
|
cbuf[off] = 47u8; off += 1; // '/'
|
|
cbuf[off] = 120u8; off += 1; // 'x'
|
|
cbuf[off] = 0u8;
|
|
let cview: str;
|
|
cview.ptr = &cbuf[0]; cview.len = off;
|
|
let cflags: os.flag = os.flag.WRONLY | os.flag.CREATE | os.flag.EXCL;
|
|
let fd: i32 = os.open(cview, cflags, 384i32);
|
|
if (fd < 0) { fail(); };
|
|
let payload: [3]u8;
|
|
payload[0] = 88u8; payload[1] = 89u8; payload[2] = 90u8; // "XYZ"
|
|
let wr: i64 = os.write(fd, &payload[0], 3u64);
|
|
if (wr != 3i64) { fail(); };
|
|
if (os.close(fd) != 0) { fail(); };
|
|
if (os.remove(cview) != 0) { fail(); };
|
|
};
|
|
|
|
// Rmdir uses dview (more robust if the static buffer were
|
|
// touched between dir() and here).
|
|
if (os.rmdir(dview) != 0) { fail(); };
|
|
|
|
// Cleanup landed.
|
|
if (os.access(dview, 0i32) == 0) { fail(); };
|
|
|
|
i += 1;
|
|
};
|
|
};
|
|
|
|
// ---- diruniqueness: two dir() calls produce different paths ------------
|
|
|
|
@test fn diruniqueness() void = {
|
|
let d1: str = temp.dir();
|
|
let snap: [128]u8;
|
|
let snaplen: i32 = d1.len;
|
|
let i: i32 = 0;
|
|
for (i < d1.len) { snap[i] = d1[i]; i += 1; };
|
|
snap[d1.len] = 0u8;
|
|
|
|
let dsnap: str;
|
|
dsnap.ptr = &snap[0];
|
|
dsnap.len = snaplen;
|
|
|
|
let d2: str = temp.dir();
|
|
if (d1.ptr != d2.ptr) { fail(); }; // same static buffer
|
|
if (streq(strslice(d2, 0, d2.len), dsnap)) { fail(); };
|
|
|
|
// Cleanup both, using snap for d1's old contents.
|
|
if (os.rmdir(d2) != 0) { fail(); };
|
|
if (os.rmdir(dsnap) != 0) { fail(); };
|
|
};
|
|
|
|
export fn main() i32 = {
|
|
signalled = 1; namedroundtrip();
|
|
signalled = 2; namedoverwrite();
|
|
signalled = 3; dirlifecycle();
|
|
signalled = 4; diruniqueness();
|
|
return 0;
|
|
};
|