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
// — the second clobbers the first's view.
//
// `signalled` global bumps before each test so a failing exit code
// pinpoints the offending row (WEXITSTATUS = signalled + 10). Same
// shape as temptest / shlextest / fnmatchtest / ostest.
// A failing row aborts via the assert/abort builtin (task #5 @test
// conversion).
//
// 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;
import dirs;
import os;
let signalled: i32 = 0;
fn fail() void = { os.exit(signalled + 10); };
fn streq(a: str, b: str) bool = {
if (a.len != b.len) { return false; };
@@ -87,7 +84,7 @@ fn gettmpdir() str = {
match (os.getenv("WW_TEST_TMPDIR")) {
case let s: str => return s;
case void => {
fail();
abort();
let empty: str;
return empty;
};
@@ -100,7 +97,7 @@ fn gettmpdir() str = {
let tmpl = gettmpdir();
let exp = expected(tmpl, "cfg", "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 --
@@ -112,7 +109,7 @@ fn gettmpdir() str = {
let tmpl = gettmpdir();
let exp = expected(tmpl, ".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 -------
@@ -121,7 +118,7 @@ fn gettmpdir() str = {
let tmpl = gettmpdir();
let exp = expected(tmpl, ".local/share", "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 ------
@@ -130,13 +127,13 @@ fn gettmpdir() str = {
let tmpl = gettmpdir();
let exp = expected(tmpl, ".local/state", "myapp");
let got = dirs.state("myapp");
if (!streq(got, exp)) { fail(); };
assert(!(!streq(got, exp)));
};
export fn main() i32 = {
signalled = 1; test_config_xdg_set();
signalled = 2; test_cache_xdg_relative_fallback();
signalled = 3; test_data_unset_fallback();
signalled = 4; test_state_unset_fallback();
test_config_xdg_set();
test_cache_xdg_relative_fallback();
test_data_unset_fallback();
test_state_unset_fallback();
return 0;
};

View File

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

View File

@@ -12,24 +12,20 @@
// WW_TEST_STAT_SUBDIR = "<tmp>/subdir" directory, 0700
// WW_TEST_STAT_NOENT = "<tmp>/does-not-exist"
//
// Test exit code follows the stdlib `_run` convention: a `signalled`
// global the main fn bumps before each row, so `WEXITSTATUS = signalled
// + 10` tells the harness which row tripped. Same shape as
// ostest / temptest / shlextest.
// A failing row aborts via the assert/abort builtin (task #5 @test
// conversion).
package os_test;
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
// 976_stat_run.c didn't prime the env state.
fn envpath(name: str) str = {
match (os.getenv(name)) {
case void => { fail(); return "": str; };
case void => { abort(); return "": str; };
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.CTIME) as u32;
match (os.stat(&fi, p)) {
case let e: os.oserror => fail();
case let e: os.oserror => abort();
case void => {
if ((fi.mask as u32) != wantmask) { fail(); };
if (!istype(fi.mode, os.mode.REG)) { fail(); };
if (fi.sz != 11u64) { fail(); };
if (fi.inode == 0u64) { fail(); };
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
// 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) {
fail();
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.
if (fi.atime.sec <= 0i64) { fail(); };
if (fi.mtime.sec <= 0i64) { fail(); };
if (fi.ctime.sec <= 0i64) { fail(); };
assert(!(fi.atime.sec <= 0i64));
assert(!(fi.mtime.sec <= 0i64));
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 fi: os.filestat;
match (os.stat(&fi, p)) {
case let e: os.oserror => fail();
case let e: os.oserror => abort();
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 fi: os.filestat;
match (os.stat(&fi, p)) {
case void => fail();
case void => abort();
case let e: os.oserror => {
// 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 fi: os.filestat;
match (os.stat(&fi, p)) {
case let e: os.oserror => fail();
case let e: os.oserror => abort();
case void => {
if (!istype(fi.mode, os.mode.REG)) { fail(); };
if (fi.sz != 11u64) { fail(); };
assert(!(!istype(fi.mode, os.mode.REG)));
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 fi: os.filestat;
match (os.lstat(&fi, p)) {
case let e: os.oserror => fail();
case let e: os.oserror => abort();
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 = {
let p = envpath("WW_TEST_STAT_REGFILE");
let fd: i32 = os.open(p, os.flag.RDONLY, 0i32);
if (fd < 0) { fail(); };
assert(!(fd < 0));
let fi: os.filestat;
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 => {
if (!istype(fi.mode, os.mode.REG)) { os.close(fd); fail(); };
if (fi.sz != 11u64) { os.close(fd); fail(); };
if (!istype(fi.mode, os.mode.REG)) { os.close(fd); abort(); };
if (fi.sz != 11u64) { os.close(fd); abort(); };
};
};
os.close(fd);
@@ -160,17 +156,17 @@ fn istype(m: os.mode, t: os.mode) bool = {
@test fn test_exists_regfile() void = {
let p = envpath("WW_TEST_STAT_REGFILE");
if (!os.exists(p)) { fail(); };
assert(!(!os.exists(p)));
};
@test fn test_exists_subdir() void = {
let p = envpath("WW_TEST_STAT_SUBDIR");
if (!os.exists(p)) { fail(); };
assert(!(!os.exists(p)));
};
@test fn test_exists_noent() void = {
let p = envpath("WW_TEST_STAT_NOENT");
if (os.exists(p)) { fail(); };
assert(!(os.exists(p)));
};
// ---- ENAMETOOLONG: kpath rejects paths >= PATH_MAX -------------------
@@ -197,29 +193,29 @@ fn makebig(n: i32) str = {
let bp = makebig(os.PATH_MAX);
let fi: os.filestat;
match (os.stat(&fi, bp)) {
case void => fail();
case void => abort();
case let e: os.oserror => {
if ((e: i64) != -36i64) { fail(); }; // ENAMETOOLONG
assert(!((e: i64) != -36i64)); // ENAMETOOLONG
};
};
};
@test fn test_exists_toolong() void = {
let bp = makebig(os.PATH_MAX);
if (os.exists(bp)) { fail(); };
assert(!(os.exists(bp)));
};
export fn main() i32 = {
signalled = 1; test_stat_regfile();
signalled = 2; test_stat_subdir();
signalled = 3; test_stat_noent();
signalled = 4; test_stat_symlink_follow();
signalled = 5; test_lstat_symlink_nofollow();
signalled = 6; test_fstat_regfile();
signalled = 7; test_exists_regfile();
signalled = 8; test_exists_subdir();
signalled = 9; test_exists_noent();
signalled = 10; test_stat_toolong();
signalled = 11; test_exists_toolong();
test_stat_regfile();
test_stat_subdir();
test_stat_noent();
test_stat_symlink_follow();
test_lstat_symlink_nofollow();
test_fstat_regfile();
test_exists_regfile();
test_exists_subdir();
test_exists_noent();
test_stat_toolong();
test_exists_toolong();
return 0;
};