Files
ww/lib/temp/temptest.ww
Hojun-Cho deaa777eb8 lib/os+selfhost: *u8→str path migration (#23)
Path-shaped entrypoints now take str: open, tryopen, access, remove,
mkdir, rmdir, mkdirs, stat, lstat, exists, execve (path arg only).
Each cites its Hare source (ref/hare/os/*.ha, ref/hare/sys/+linux/
*.ha).

New internal kpath(str) *u8 copies into module-level pathbuf: [4096]u8
and NUL-terminates; mirrors ref/hare/sys/+linux/syscalls.ha:25,53.
Non-reentrant — graduates with thread story. mkdirs flattens to one
kpath at entry then walks pathbuf invoking raw SYS_mkdir to avoid
nested kpath clobber.

One Hare divergence at kpath: ships *u8 with nil ENAMETOOLONG sentinel
instead of (*const u8 | errno). Reason: wwstage over-allocates
1-word-payload tagged returns to 24B (cstage emits 16B); filed as
follow-up. Repro at .ai/probe_tagged_return_pointer_payload.ww;
graduates when fix lands.

Each selfhost cmd grew a private pathstr(*u8) str (cstrlen + bs) for
remaining *u8 path sites; w6l shares via obj.ww. Probe 7 in smoke
updated.

Tests 975/976/981 cover migrated entrypoints; 976 extended with two
ENAMETOOLONG rows (-36 for stat, false for exists).
2026-05-16 23:36:41 +09:00

268 lines
7.9 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.
use os;
use 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;
};