// Each row arranges its own scratch tree under [[temp.dir]] (Hare's // own idiom: os tests self-arrange in a temp dir), so the suite // passes bare with no external driver: // // /regfile regular file, 11 bytes "hello world", 0644 // /symlink → ./regfile (relative symlink) // /subdir directory, mode 0700 // /does-not-exist guaranteed absent // // 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); the failure path deliberately leaves the tree for // inspection (no defer in ww). package os_test; import os; import strings; import temp; 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) // out of a mode and compare against the requested type bit. fn istype(m: os.mode, t: os.mode) bool = { return ((m as u32) & 61440u32) == (t as u32); }; @test fn test_stat_regfile() void = { 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 // fillfilestat assigns. let wantmask: u32 = (os.stat_mask.UID | os.stat_mask.GID | 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, 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 — 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). if (((fi.mode as u32) & (os.mode.USER_R as u32)) == 0u32) { abort(); }; // atime/mtime/ctime — kernel-set at create time, all // post-epoch (>0). Three distinct kstat offsets (72/88/104) // so a fillfilestat field-copy miscompile or a filestat // time.instant offset bug surfaces here, not silently. assert(!(fi.atime.sec <= 0i64)); assert(!(fi.mtime.sec <= 0i64)); assert(!(fi.ctime.sec <= 0i64)); }; }; rmtree(&t); }; @test fn test_stat_subdir() void = { let t: tree; mktree(&t); let fi: os.filestat; match (os.stat(&fi, t.subdir)) { case let e: os.oserror => abort(); case void => { assert(!(!istype(fi.mode, os.mode.DIR))); }; }; rmtree(&t); }; @test fn test_stat_noent() void = { let t: tree; mktree(&t); let fi: os.filestat; 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); }; @test fn test_stat_symlink_follow() void = { let t: tree; mktree(&t); let fi: os.filestat; 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 t: tree; mktree(&t); let fi: os.filestat; match (os.lstat(&fi, t.symlink)) { case let e: os.oserror => abort(); case void => { assert(!(!istype(fi.mode, os.mode.LINK))); }; }; rmtree(&t); }; @test fn test_readlink_verbatim() void = { let t: tree; mktree(&t); let buf: [16]u8; let i: i32 = 0; for (i < 16) { buf[i] = 90u8; i += 1; }; // 'Z' let n: i64 = os.readlink(t.symlink, &buf[0], 16u64); assert(!(n != 9i64)); let want: str = "./regfile"; i = 0; for (i < want.len) { assert(!(buf[i] != want[i])); i += 1; }; let ni: i32 = n: i32; assert(!(buf[ni] != 90u8)); rmtree(&t); }; @test fn test_fstat_regfile() void = { 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)) { case let e: os.oserror => { os.close(fd); abort(); }; case void => { if (!istype(fi.mode, os.mode.REG)) { os.close(fd); abort(); }; if (fi.sz != 11u64) { os.close(fd); abort(); }; }; }; os.close(fd); rmtree(&t); }; @test fn test_exists_regfile() void = { let t: tree; mktree(&t); assert(!(!os.exists(t.regfile))); rmtree(&t); }; @test fn test_exists_subdir() void = { let t: tree; mktree(&t); assert(!(!os.exists(t.subdir))); rmtree(&t); }; @test fn test_exists_noent() void = { let t: tree; mktree(&t); assert(!(os.exists(t.noent))); rmtree(&t); }; // kpath copies into a single [PATH_MAX]u8 buffer and reserves one byte // for the NUL terminator (`p.len + 1 > PATH_MAX` → reject). The // rejection surfaces as `oserror = -36` (ENAMETOOLONG) on (... | // oserror)-returning wrappers, and as `false` on [[os.exists]] // (Hare's os::exists doc: "true if a node exists at the given path, // or false if not."). let bigbuf: [4200]u8; fn makebig(n: i32) str = { let i: i32 = 0; for (i < n) { bigbuf[i] = 97u8; i += 1; }; // 'a' let r: str; r.ptr = &bigbuf[0]; r.len = n; return r; }; @test fn test_stat_toolong() void = { let bp = makebig(os.PATH_MAX); let fi: os.filestat; match (os.stat(&fi, bp)) { case void => abort(); case let e: os.oserror => { assert(!((e: i64) != -36i64)); // ENAMETOOLONG }; }; }; @test fn test_exists_toolong() void = { let bp = makebig(os.PATH_MAX); assert(!(os.exists(bp))); };