lib/os+dirs tests: hand-main plumbing -> assert (@test conversion B6)

WW_TEST_* env arrangement logic untouched; only the fail/exit plumbing
converts.
This commit is contained in:
2026-06-10 18:52:53 +09:00
parent e8096e68e1
commit eb891f4007
3 changed files with 72 additions and 83 deletions

View File

@@ -26,21 +26,18 @@
// asserts immediately. Don't call two dirs.* fns and compare across // asserts immediately. Don't call two dirs.* fns and compare across
// — the second clobbers the first's view. // — the second clobbers the first's view.
// //
// `signalled` global bumps before each test so a failing exit code // A failing row aborts via the assert/abort builtin (task #5 @test
// pinpoints the offending row (WEXITSTATUS = signalled + 10). Same // conversion).
// shape as temptest / shlextest / fnmatchtest / ostest.
// //
// Don't `use io;` here — lib/os and lib/io collide on read/write/ // Don't `use io;` here — lib/os and lib/io collide on read/write/
// close (task #17). We need only os.getenv + os.exit. // close (task #17). We need only os.getenv.
package dirs_test; package dirs_test;
import dirs; import dirs;
import os; import os;
let signalled: i32 = 0;
fn fail() void = { os.exit(signalled + 10); };
fn streq(a: str, b: str) bool = { fn streq(a: str, b: str) bool = {
if (a.len != b.len) { return false; }; if (a.len != b.len) { return false; };
@@ -87,7 +84,7 @@ fn gettmpdir() str = {
match (os.getenv("WW_TEST_TMPDIR")) { match (os.getenv("WW_TEST_TMPDIR")) {
case let s: str => return s; case let s: str => return s;
case void => { case void => {
fail(); abort();
let empty: str; let empty: str;
return empty; return empty;
}; };
@@ -100,7 +97,7 @@ fn gettmpdir() str = {
let tmpl = gettmpdir(); let tmpl = gettmpdir();
let exp = expected(tmpl, "cfg", "myapp"); let exp = expected(tmpl, "cfg", "myapp");
let got = dirs.config("myapp"); let got = dirs.config("myapp");
if (!streq(got, exp)) { fail(); }; assert(!(!streq(got, exp)));
}; };
// ---- 2: XDG_CACHE_HOME="relative/path" → fallback to $HOME/.cache -- // ---- 2: XDG_CACHE_HOME="relative/path" → fallback to $HOME/.cache --
@@ -112,7 +109,7 @@ fn gettmpdir() str = {
let tmpl = gettmpdir(); let tmpl = gettmpdir();
let exp = expected(tmpl, ".cache", "myapp"); let exp = expected(tmpl, ".cache", "myapp");
let got = dirs.cache("myapp"); let got = dirs.cache("myapp");
if (!streq(got, exp)) { fail(); }; assert(!(!streq(got, exp)));
}; };
// ---- 3: XDG_DATA_HOME unset → fallback to $HOME/.local/share ------- // ---- 3: XDG_DATA_HOME unset → fallback to $HOME/.local/share -------
@@ -121,7 +118,7 @@ fn gettmpdir() str = {
let tmpl = gettmpdir(); let tmpl = gettmpdir();
let exp = expected(tmpl, ".local/share", "myapp"); let exp = expected(tmpl, ".local/share", "myapp");
let got = dirs.data("myapp"); let got = dirs.data("myapp");
if (!streq(got, exp)) { fail(); }; assert(!(!streq(got, exp)));
}; };
// ---- 4: XDG_STATE_HOME unset → fallback to $HOME/.local/state ------ // ---- 4: XDG_STATE_HOME unset → fallback to $HOME/.local/state ------
@@ -130,13 +127,13 @@ fn gettmpdir() str = {
let tmpl = gettmpdir(); let tmpl = gettmpdir();
let exp = expected(tmpl, ".local/state", "myapp"); let exp = expected(tmpl, ".local/state", "myapp");
let got = dirs.state("myapp"); let got = dirs.state("myapp");
if (!streq(got, exp)) { fail(); }; assert(!(!streq(got, exp)));
}; };
export fn main() i32 = { export fn main() i32 = {
signalled = 1; test_config_xdg_set(); test_config_xdg_set();
signalled = 2; test_cache_xdg_relative_fallback(); test_cache_xdg_relative_fallback();
signalled = 3; test_data_unset_fallback(); test_data_unset_fallback();
signalled = 4; test_state_unset_fallback(); test_state_unset_fallback();
return 0; return 0;
}; };

View File

@@ -5,11 +5,9 @@
// cover alloc/free; the direct row here pins the FFI shape under // cover alloc/free; the direct row here pins the FFI shape under
// lib/os itself so future bindings refactors can't quietly drift. // lib/os itself so future bindings refactors can't quietly drift.
// //
// Convention follows the stdlib `_run` test fixtures: hand-rolled // Hand-rolled @test fns dispatched from `main()` in source order; a
// @test fns dispatched from `main()` in numeric order, with a // failing case aborts via the assert/abort builtin (task #5 @test
// `signalled` global the main fn bumps so a failing case exits with // conversion).
// `WEXITSTATUS = signalled + 10` — the harness then knows which
// scenario tripped. Same shape as temptest / shlextest / fnmatchtest.
// //
// Pre-arranged env state (set by 974_getenv_run.c via setenv/unsetenv // Pre-arranged env state (set by 974_getenv_run.c via setenv/unsetenv
// before exec'ing `ww run lib/os/ostest.ww`): // before exec'ing `ww run lib/os/ostest.ww`):
@@ -20,17 +18,15 @@
// //
// Don't `use io;` here — lib/os and lib/io both export read/write/ // Don't `use io;` here — lib/os and lib/io both export read/write/
// close as C symbols that collide under the driver's flat-scope // close as C symbols that collide under the driver's flat-scope
// concat (task #17). os alone is sufficient: getenv + exit are the // concat (task #17). os alone is sufficient: getenv + alloc/free are
// only primitives needed. // the only primitives needed.
package os_test; package os_test;
import os; import os;
import rt; import rt;
let signalled: i32 = 0;
fn fail() void = { os.exit(signalled + 10); };
fn streq(a: str, b: str) bool = { fn streq(a: str, b: str) bool = {
if (a.len != b.len) { return false; }; if (a.len != b.len) { return false; };
@@ -47,9 +43,9 @@ fn streq(a: str, b: str) bool = {
@test fn test_getenv_set() void = { @test fn test_getenv_set() void = {
let r = os.getenv("WW_TEST_GETENV"); let r = os.getenv("WW_TEST_GETENV");
match (r) { match (r) {
case void => fail(); case void => abort();
case let s: str => { case let s: str => {
if (!streq(s, "hello-world")) { fail(); }; assert(!(!streq(s, "hello-world")));
}; };
}; };
}; };
@@ -63,9 +59,9 @@ fn streq(a: str, b: str) bool = {
@test fn test_getenv_empty() void = { @test fn test_getenv_empty() void = {
let r = os.getenv("WW_TEST_EMPTY"); let r = os.getenv("WW_TEST_EMPTY");
match (r) { match (r) {
case void => fail(); case void => abort();
case let s: str => { case let s: str => {
if (s.len != 0) { fail(); }; assert(!(s.len != 0));
}; };
}; };
}; };
@@ -76,7 +72,7 @@ fn streq(a: str, b: str) bool = {
let r = os.getenv("WW_TEST_NOT_SET"); let r = os.getenv("WW_TEST_NOT_SET");
match (r) { match (r) {
case void => {}; case void => {};
case let s: str => fail(); case let s: str => abort();
}; };
}; };
@@ -91,7 +87,7 @@ fn streq(a: str, b: str) bool = {
let r = os.getenv("WW_TEST_GETEN"); let r = os.getenv("WW_TEST_GETEN");
match (r) { match (r) {
case void => {}; case void => {};
case let s: str => fail(); case let s: str => abort();
}; };
}; };
@@ -106,16 +102,16 @@ fn streq(a: str, b: str) bool = {
p.len = 4096; p.len = 4096;
p[0] = 90u8; // 0x5a p[0] = 90u8; // 0x5a
p[4095] = 165u8; // 0xa5 p[4095] = 165u8; // 0xa5
if (p[0] != 90u8) { fail(); }; assert(!(p[0] != 90u8));
if (p[4095] != 165u8) { fail(); }; assert(!(p[4095] != 165u8));
os.free(p.ptr: *void, 4096u64); os.free(p.ptr: *void, 4096u64);
}; };
export fn main() i32 = { export fn main() i32 = {
signalled = 1; test_getenv_set(); test_getenv_set();
signalled = 2; test_getenv_empty(); test_getenv_empty();
signalled = 3; test_getenv_unset(); test_getenv_unset();
signalled = 4; test_getenv_prefix_no_match(); test_getenv_prefix_no_match();
signalled = 5; test_alloc_free_roundtrip(); test_alloc_free_roundtrip();
return 0; return 0;
}; };

View File

@@ -12,24 +12,20 @@
// WW_TEST_STAT_SUBDIR = "<tmp>/subdir" directory, 0700 // WW_TEST_STAT_SUBDIR = "<tmp>/subdir" directory, 0700
// WW_TEST_STAT_NOENT = "<tmp>/does-not-exist" // WW_TEST_STAT_NOENT = "<tmp>/does-not-exist"
// //
// Test exit code follows the stdlib `_run` convention: a `signalled` // A failing row aborts via the assert/abort builtin (task #5 @test
// global the main fn bumps before each row, so `WEXITSTATUS = signalled // conversion).
// + 10` tells the harness which row tripped. Same shape as
// ostest / temptest / shlextest.
package os_test; package os_test;
import os; import os;
let signalled: i32 = 0;
fn fail() void = { os.exit(signalled + 10); };
// envpath — fetch an env var or abort; we want a clean signal if // envpath — fetch an env var or abort; we want a clean signal if
// 976_stat_run.c didn't prime the env state. // 976_stat_run.c didn't prime the env state.
fn envpath(name: str) str = { fn envpath(name: str) str = {
match (os.getenv(name)) { match (os.getenv(name)) {
case void => { fail(); return "": str; }; case void => { abort(); return "": str; };
case let s: str => return s; case let s: str => return s;
}; };
}; };
@@ -59,27 +55,27 @@ fn istype(m: os.mode, t: os.mode) bool = {
| os.stat_mask.ATIME | os.stat_mask.MTIME | os.stat_mask.ATIME | os.stat_mask.MTIME
| os.stat_mask.CTIME) as u32; | os.stat_mask.CTIME) as u32;
match (os.stat(&fi, p)) { match (os.stat(&fi, p)) {
case let e: os.oserror => fail(); case let e: os.oserror => abort();
case void => { case void => {
if ((fi.mask as u32) != wantmask) { fail(); }; assert(!((fi.mask as u32) != wantmask));
if (!istype(fi.mode, os.mode.REG)) { fail(); }; assert(!(!istype(fi.mode, os.mode.REG)));
if (fi.sz != 11u64) { fail(); }; assert(!(fi.sz != 11u64));
if (fi.inode == 0u64) { fail(); }; assert(!(fi.inode == 0u64));
// Permission-bit smoke test — 976_stat_run.c open(2)s with // Permission-bit smoke test — 976_stat_run.c open(2)s with
// mode 0644 so USER_R survives any reasonable umask. Catches // mode 0644 so USER_R survives any reasonable umask. Catches
// a struct-field-offset miscompile on `fi.mode` that the // a struct-field-offset miscompile on `fi.mode` that the
// type-bit istype() check could miss if perm bits aliased a // type-bit istype() check could miss if perm bits aliased a
// neighbouring u32 (uid/gid). // neighbouring u32 (uid/gid).
if (((fi.mode as u32) & (os.mode.USER_R as u32)) == 0u32) { if (((fi.mode as u32) & (os.mode.USER_R as u32)) == 0u32) {
fail(); abort();
}; };
// atime/mtime/ctime — kernel-set at create time, all // atime/mtime/ctime — kernel-set at create time, all
// post-epoch (>0). Three distinct kstat offsets (72/88/104) // post-epoch (>0). Three distinct kstat offsets (72/88/104)
// so a fillfilestat field-copy miscompile or a filestat // so a fillfilestat field-copy miscompile or a filestat
// time.instant offset bug surfaces here, not silently. // time.instant offset bug surfaces here, not silently.
if (fi.atime.sec <= 0i64) { fail(); }; assert(!(fi.atime.sec <= 0i64));
if (fi.mtime.sec <= 0i64) { fail(); }; assert(!(fi.mtime.sec <= 0i64));
if (fi.ctime.sec <= 0i64) { fail(); }; assert(!(fi.ctime.sec <= 0i64));
}; };
}; };
}; };
@@ -90,9 +86,9 @@ fn istype(m: os.mode, t: os.mode) bool = {
let p = envpath("WW_TEST_STAT_SUBDIR"); let p = envpath("WW_TEST_STAT_SUBDIR");
let fi: os.filestat; let fi: os.filestat;
match (os.stat(&fi, p)) { match (os.stat(&fi, p)) {
case let e: os.oserror => fail(); case let e: os.oserror => abort();
case void => { case void => {
if (!istype(fi.mode, os.mode.DIR)) { fail(); }; assert(!(!istype(fi.mode, os.mode.DIR)));
}; };
}; };
}; };
@@ -103,10 +99,10 @@ fn istype(m: os.mode, t: os.mode) bool = {
let p = envpath("WW_TEST_STAT_NOENT"); let p = envpath("WW_TEST_STAT_NOENT");
let fi: os.filestat; let fi: os.filestat;
match (os.stat(&fi, p)) { match (os.stat(&fi, p)) {
case void => fail(); case void => abort();
case let e: os.oserror => { case let e: os.oserror => {
// ENOENT = 2 → raw errno is -2. // ENOENT = 2 → raw errno is -2.
if ((e: i64) != -2i64) { fail(); }; assert(!((e: i64) != -2i64));
}; };
}; };
}; };
@@ -120,10 +116,10 @@ fn istype(m: os.mode, t: os.mode) bool = {
let p = envpath("WW_TEST_STAT_SYMLINK"); let p = envpath("WW_TEST_STAT_SYMLINK");
let fi: os.filestat; let fi: os.filestat;
match (os.stat(&fi, p)) { match (os.stat(&fi, p)) {
case let e: os.oserror => fail(); case let e: os.oserror => abort();
case void => { case void => {
if (!istype(fi.mode, os.mode.REG)) { fail(); }; assert(!(!istype(fi.mode, os.mode.REG)));
if (fi.sz != 11u64) { fail(); }; assert(!(fi.sz != 11u64));
}; };
}; };
}; };
@@ -132,9 +128,9 @@ fn istype(m: os.mode, t: os.mode) bool = {
let p = envpath("WW_TEST_STAT_SYMLINK"); let p = envpath("WW_TEST_STAT_SYMLINK");
let fi: os.filestat; let fi: os.filestat;
match (os.lstat(&fi, p)) { match (os.lstat(&fi, p)) {
case let e: os.oserror => fail(); case let e: os.oserror => abort();
case void => { case void => {
if (!istype(fi.mode, os.mode.LINK)) { fail(); }; assert(!(!istype(fi.mode, os.mode.LINK)));
}; };
}; };
}; };
@@ -144,13 +140,13 @@ fn istype(m: os.mode, t: os.mode) bool = {
@test fn test_fstat_regfile() void = { @test fn test_fstat_regfile() void = {
let p = envpath("WW_TEST_STAT_REGFILE"); let p = envpath("WW_TEST_STAT_REGFILE");
let fd: i32 = os.open(p, os.flag.RDONLY, 0i32); let fd: i32 = os.open(p, os.flag.RDONLY, 0i32);
if (fd < 0) { fail(); }; assert(!(fd < 0));
let fi: os.filestat; let fi: os.filestat;
match (os.fstat(&fi, fd)) { match (os.fstat(&fi, fd)) {
case let e: os.oserror => { os.close(fd); fail(); }; case let e: os.oserror => { os.close(fd); abort(); };
case void => { case void => {
if (!istype(fi.mode, os.mode.REG)) { os.close(fd); fail(); }; if (!istype(fi.mode, os.mode.REG)) { os.close(fd); abort(); };
if (fi.sz != 11u64) { os.close(fd); fail(); }; if (fi.sz != 11u64) { os.close(fd); abort(); };
}; };
}; };
os.close(fd); os.close(fd);
@@ -160,17 +156,17 @@ fn istype(m: os.mode, t: os.mode) bool = {
@test fn test_exists_regfile() void = { @test fn test_exists_regfile() void = {
let p = envpath("WW_TEST_STAT_REGFILE"); let p = envpath("WW_TEST_STAT_REGFILE");
if (!os.exists(p)) { fail(); }; assert(!(!os.exists(p)));
}; };
@test fn test_exists_subdir() void = { @test fn test_exists_subdir() void = {
let p = envpath("WW_TEST_STAT_SUBDIR"); let p = envpath("WW_TEST_STAT_SUBDIR");
if (!os.exists(p)) { fail(); }; assert(!(!os.exists(p)));
}; };
@test fn test_exists_noent() void = { @test fn test_exists_noent() void = {
let p = envpath("WW_TEST_STAT_NOENT"); let p = envpath("WW_TEST_STAT_NOENT");
if (os.exists(p)) { fail(); }; assert(!(os.exists(p)));
}; };
// ---- ENAMETOOLONG: kpath rejects paths >= PATH_MAX ------------------- // ---- ENAMETOOLONG: kpath rejects paths >= PATH_MAX -------------------
@@ -197,29 +193,29 @@ fn makebig(n: i32) str = {
let bp = makebig(os.PATH_MAX); let bp = makebig(os.PATH_MAX);
let fi: os.filestat; let fi: os.filestat;
match (os.stat(&fi, bp)) { match (os.stat(&fi, bp)) {
case void => fail(); case void => abort();
case let e: os.oserror => { case let e: os.oserror => {
if ((e: i64) != -36i64) { fail(); }; // ENAMETOOLONG assert(!((e: i64) != -36i64)); // ENAMETOOLONG
}; };
}; };
}; };
@test fn test_exists_toolong() void = { @test fn test_exists_toolong() void = {
let bp = makebig(os.PATH_MAX); let bp = makebig(os.PATH_MAX);
if (os.exists(bp)) { fail(); }; assert(!(os.exists(bp)));
}; };
export fn main() i32 = { export fn main() i32 = {
signalled = 1; test_stat_regfile(); test_stat_regfile();
signalled = 2; test_stat_subdir(); test_stat_subdir();
signalled = 3; test_stat_noent(); test_stat_noent();
signalled = 4; test_stat_symlink_follow(); test_stat_symlink_follow();
signalled = 5; test_lstat_symlink_nofollow(); test_lstat_symlink_nofollow();
signalled = 6; test_fstat_regfile(); test_fstat_regfile();
signalled = 7; test_exists_regfile(); test_exists_regfile();
signalled = 8; test_exists_subdir(); test_exists_subdir();
signalled = 9; test_exists_noent(); test_exists_noent();
signalled = 10; test_stat_toolong(); test_stat_toolong();
signalled = 11; test_exists_toolong(); test_exists_toolong();
return 0; return 0;
}; };