From 682c16cad0cc5d0a29a5b3e9cc4022100fba8852 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sat, 8 Aug 2026 14:35:30 +0900 Subject: [PATCH] lib/os: stat_test self-arranges its scratch tree; retire 976_stat_run.c Each row builds regfile/symlink/subdir under temp.dir (Hare's os-test idiom) and asserts exact cleanup, so the suite passes bare and the carrier's env+tree arrangement job disappears. Every carrier assertion maps to an in-test row. --- lib/os/stat_test.ww | 122 ++++++++++++++++++++++++++++------------ test/wcc/976_stat_run.c | 112 ------------------------------------ 2 files changed, 85 insertions(+), 149 deletions(-) delete mode 100644 test/wcc/976_stat_run.c diff --git a/lib/os/stat_test.ww b/lib/os/stat_test.ww index 2e995cae..bfbde8a5 100644 --- a/lib/os/stat_test.ww +++ b/lib/os/stat_test.ww @@ -1,33 +1,63 @@ // stattest — exercises [[os.stat]] / [[os.lstat]] / [[os.fstat]] / -// [[os.exists]] against a scratch tree pre-arranged by -// test/wcc/976_stat_run.c. The driver mkdtemp's a tmp dir, creates a -// regfile + symlink + subdir, exports paths through env vars, then -// exec's `ww run lib/os/stattest.ww`. We read the paths back via -// os.getenv and stat the targets. +// [[os.exists]] against a scratch tree each row arranges itself +// under [[temp.dir]] (Hare's own idiom: os tests self-arrange in a +// temp dir), so the suite passes bare with no external driver: // -// Pre-arranged scratch tree (set by 976_stat_run.c): +// /regfile regular file, 11 bytes "hello world", 0644 +// /symlink → ./regfile (relative symlink) +// /subdir directory, mode 0700 +// /does-not-exist guaranteed absent // -// WW_TEST_STAT_REGFILE = "/regfile" regular file, 11 bytes -// WW_TEST_STAT_SYMLINK = "/symlink" symlink → ./regfile -// WW_TEST_STAT_SUBDIR = "/subdir" directory, 0700 -// WW_TEST_STAT_NOENT = "/does-not-exist" +// Trees are per-row because the runner forks one child per test — +// module state arranged in one row never reaches the next. Each row +// removes exactly what it created; a cleanup miss is a row failure +// (doubles as a layout assertion: the stat calls created nothing). // // A failing row aborts via the assert/abort builtin (task #5 @test -// conversion). +// conversion); the failure path deliberately leaves the tree for +// inspection (no defer in ww). package os_test; import os; +import strings; +import temp; -// envpath — fetch an env var or abort; we want a clean signal if -// 976_stat_run.c didn't prime the env state. -fn envpath(name: str) str = { - match (os.getenv(name)) { - case void => { abort(); return "": str; }; - case let s: str => return s; +type tree = struct { + root: str, + regfile: str, + symlink: str, + subdir: str, + noent: str, +}; + +fn mktree(t: *tree) void = { + // temp.dir returns a view of temp's static pathbuf; dup before + // deriving children so later temp calls can't clobber it. + t.root = strings.dup(temp.dir()); + t.regfile = strings.concat(t.root, "/regfile"); + t.symlink = strings.concat(t.root, "/symlink"); + t.subdir = strings.concat(t.root, "/subdir"); + t.noent = strings.concat(t.root, "/does-not-exist"); + let fd: i32 = os.open(t.regfile, + os.flag.WRONLY | os.flag.CREATE | os.flag.EXCL, 420i32); // 0o644 + assert(!(fd < 0)); + match (os.writeall(fd, "hello world".ptr, 11u64)) { + case let n: i64 => assert(!(n != 11i64)); + case let e: os.oserror => abort(); }; + assert(!(os.close(fd) != 0)); + assert(!(os.symlink("./regfile", t.symlink) != 0)); + assert(!(os.mkdir(t.subdir, 448i32) != 0)); // 0o700 +}; + +fn rmtree(t: *tree) void = { + assert(!(os.remove(t.symlink) != 0)); + assert(!(os.remove(t.regfile) != 0)); + assert(!(os.rmdir(t.subdir) != 0)); + assert(!(os.rmdir(t.root) != 0)); }; // istype — mask the file-type bits (S_IFMT = 0o170000 = 61440) @@ -45,7 +75,8 @@ fn istype(m: os.mode, t: os.mode) bool = { // - inode is non-zero (real fs entry, not synthetic) @test fn test_stat_regfile() void = { - let p = envpath("WW_TEST_STAT_REGFILE"); + let t: tree; + mktree(&t); let fi: os.filestat; // newfstatat populates every field, so the mask is the // OR-fold of all 7 Hare stat_mask bits — mirrors what @@ -54,15 +85,15 @@ fn istype(m: os.mode, t: os.mode) bool = { | os.stat_mask.SIZE | os.stat_mask.INODE | os.stat_mask.ATIME | os.stat_mask.MTIME | os.stat_mask.CTIME) as u32; - match (os.stat(&fi, p)) { + match (os.stat(&fi, t.regfile)) { case let e: os.oserror => abort(); case void => { assert(!((fi.mask as u32) != wantmask)); assert(!(!istype(fi.mode, os.mode.REG))); assert(!(fi.sz != 11u64)); assert(!(fi.inode == 0u64)); - // Permission-bit smoke test — 976_stat_run.c open(2)s with - // mode 0644 so USER_R survives any reasonable umask. Catches + // Permission-bit smoke test — mktree open(2)s with mode + // 0644 so USER_R survives any reasonable umask. Catches // a struct-field-offset miscompile on `fi.mode` that the // type-bit istype() check could miss if perm bits aliased a // neighbouring u32 (uid/gid). @@ -78,33 +109,38 @@ fn istype(m: os.mode, t: os.mode) bool = { assert(!(fi.ctime.sec <= 0i64)); }; }; + rmtree(&t); }; // ---- stat: directory ------------------------------------------------ @test fn test_stat_subdir() void = { - let p = envpath("WW_TEST_STAT_SUBDIR"); + let t: tree; + mktree(&t); let fi: os.filestat; - match (os.stat(&fi, p)) { + match (os.stat(&fi, t.subdir)) { case let e: os.oserror => abort(); case void => { assert(!(!istype(fi.mode, os.mode.DIR))); }; }; + rmtree(&t); }; // ---- stat: missing path → oserror ENOENT --------------------------- @test fn test_stat_noent() void = { - let p = envpath("WW_TEST_STAT_NOENT"); + let t: tree; + mktree(&t); let fi: os.filestat; - match (os.stat(&fi, p)) { + match (os.stat(&fi, t.noent)) { case void => abort(); case let e: os.oserror => { // ENOENT = 2 → raw errno is -2. assert(!((e: i64) != -2i64)); }; }; + rmtree(&t); }; // ---- stat (follow) vs lstat (no-follow) on a symlink ---------------- @@ -113,33 +149,38 @@ fn istype(m: os.mode, t: os.mode) bool = { // lstat does NOT follow → reports the link itself (LINK). @test fn test_stat_symlink_follow() void = { - let p = envpath("WW_TEST_STAT_SYMLINK"); + let t: tree; + mktree(&t); let fi: os.filestat; - match (os.stat(&fi, p)) { + match (os.stat(&fi, t.symlink)) { case let e: os.oserror => abort(); case void => { assert(!(!istype(fi.mode, os.mode.REG))); assert(!(fi.sz != 11u64)); }; }; + rmtree(&t); }; @test fn test_lstat_symlink_nofollow() void = { - let p = envpath("WW_TEST_STAT_SYMLINK"); + let t: tree; + mktree(&t); let fi: os.filestat; - match (os.lstat(&fi, p)) { + match (os.lstat(&fi, t.symlink)) { case let e: os.oserror => abort(); case void => { assert(!(!istype(fi.mode, os.mode.LINK))); }; }; + rmtree(&t); }; // ---- fstat: open a file and stat by fd ------------------------------ @test fn test_fstat_regfile() void = { - let p = envpath("WW_TEST_STAT_REGFILE"); - let fd: i32 = os.open(p, os.flag.RDONLY, 0i32); + let t: tree; + mktree(&t); + let fd: i32 = os.open(t.regfile, os.flag.RDONLY, 0i32); assert(!(fd < 0)); let fi: os.filestat; match (os.fstat(&fi, fd)) { @@ -150,23 +191,30 @@ fn istype(m: os.mode, t: os.mode) bool = { }; }; os.close(fd); + rmtree(&t); }; // ---- exists: true on regfile/dir/symlink, false on noent ------------ @test fn test_exists_regfile() void = { - let p = envpath("WW_TEST_STAT_REGFILE"); - assert(!(!os.exists(p))); + let t: tree; + mktree(&t); + assert(!(!os.exists(t.regfile))); + rmtree(&t); }; @test fn test_exists_subdir() void = { - let p = envpath("WW_TEST_STAT_SUBDIR"); - assert(!(!os.exists(p))); + let t: tree; + mktree(&t); + assert(!(!os.exists(t.subdir))); + rmtree(&t); }; @test fn test_exists_noent() void = { - let p = envpath("WW_TEST_STAT_NOENT"); - assert(!(os.exists(p))); + let t: tree; + mktree(&t); + assert(!(os.exists(t.noent))); + rmtree(&t); }; // ---- ENAMETOOLONG: kpath rejects paths >= PATH_MAX ------------------- diff --git a/test/wcc/976_stat_run.c b/test/wcc/976_stat_run.c deleted file mode 100644 index 6498e2a2..00000000 --- a/test/wcc/976_stat_run.c +++ /dev/null @@ -1,112 +0,0 @@ -/* - * 976_stat_run — execute the lib/os stat fixture under the C-side - * `ww test` driver and assert exit 0. - * - * Workflow (mirrors 975_dirs_run's mkdtemp + exact-cleanup shape): - * 1. mkdtemp /tmp/wwstat-XXXXXX — fresh per run, no parallel-test - * contention. - * 2. Populate the scratch tree: - * /regfile regular file, 11 bytes "hello world" - * /symlink → ./regfile (relative symlink) - * /subdir directory, mode 0700 - * 3. setenv WW_TEST_STAT_REGFILE / SYMLINK / SUBDIR / NOENT with - * the absolute paths; lib/os/stat_test.ww reads them via - * os.getenv and exercises stat/lstat/fstat/exists. - * 4. exec `ww test lib/os/stat_test.ww`. - * 5. Remove the exact children and then the mkdtemp-owned root. - * - * Same wrapper shape as 970-975; cleanup runs on success and failure. - */ -#include -#include -#include -#include -#include -#include -#include -#include - -static int -runwait(const char *cmd) -{ - int rc = system(cmd); - if (rc == -1) return -1; - if (WIFEXITED(rc)) return WEXITSTATUS(rc); - return 1; -} - -int -main(void) -{ - const char *bin = getenv("BIN"); - if (!bin) bin = "out/bin"; - char absbin[1024]; - if (bin[0] != '/') { - char cwd[1024]; - if (getcwd(cwd, sizeof cwd) == NULL) return 1; - snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); - bin = absbin; - } - char cwd[1024]; - if (getcwd(cwd, sizeof cwd) == NULL) return 1; - - char tmpl[64]; - strcpy(tmpl, "/tmp/wwstat-XXXXXX"); - if (mkdtemp(tmpl) == NULL) { - perror("mkdtemp"); - return 1; - } - - char regfile[256], symlink_path[256], subdir[256], noent[256]; - snprintf(regfile, sizeof regfile, "%s/regfile", tmpl); - snprintf(symlink_path, sizeof symlink_path, "%s/symlink", tmpl); - snprintf(subdir, sizeof subdir, "%s/subdir", tmpl); - snprintf(noent, sizeof noent, "%s/does-not-exist", tmpl); - const char *src = "lib/os/stat_test.ww"; - int fail = 0, subdir_owned = 0; - - /* Populate. */ - int fd = open(regfile, O_WRONLY | O_CREAT | O_EXCL, 0644); - if (fd < 0) { perror("open regfile"); fail = 1; goto cleanup; } - if (write(fd, "hello world", 11) != 11) { - perror("write regfile"); close(fd); fail = 1; goto cleanup; - } - close(fd); - if (symlink("./regfile", symlink_path) != 0) { - perror("symlink"); fail = 1; goto cleanup; - } - if (mkdir(subdir, 0700) != 0) { - perror("mkdir subdir"); fail = 1; goto cleanup; - } - subdir_owned = 1; - - setenv("WW_TEST_STAT_REGFILE", regfile, 1); - setenv("WW_TEST_STAT_SYMLINK", symlink_path, 1); - setenv("WW_TEST_STAT_SUBDIR", subdir, 1); - setenv("WW_TEST_STAT_NOENT", noent, 1); - - char path[1024], cmd[2048]; - snprintf(path, sizeof path, "%s/%s", cwd, src); - snprintf(cmd, sizeof cmd, "%s/ww test %s", bin, path); - int rc = runwait(cmd); - - if (rc != 0) { - fprintf(stderr, "stat_run FAIL: %s exited %d\n", src, rc); - fail = 1; - } - -cleanup: { - int cleanbad = 0; - if (unlink(symlink_path) != 0 && errno != ENOENT) cleanbad = 1; - if (unlink(regfile) != 0 && errno != ENOENT) cleanbad = 1; - if (subdir_owned && rmdir(subdir) != 0) cleanbad = 1; - if (rmdir(tmpl) != 0) cleanbad = 1; - if (cleanbad) { - fprintf(stderr, "stat_run FAIL: temporary cleanup failed\n"); - fail = 1; - } -} - if (fail) return 1; - printf("stat_run: %s ok\n", src); - return 0; -}