// memio's tests indirectly cover alloc/free; the direct row here pins // the FFI shape under lib/os itself so future bindings refactors can't // quietly drift. // // getenv env contract (ww ships no setenv primitive, deliberately): // // WW_TEST_GETENV = "hello-world" // WW_TEST_EMPTY = "" (set, but value is empty) // WW_TEST_NOT_SET unset // // The rows that need a var PRESENT skip loudly when it is absent, so // the suite passes bare; the arranged leg (env constructed per row, // rows asserted `ok`, not SKIP) is test/libenv/libenv_test.ww. // // 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 + alloc/free are // the only primitives needed. package os_test; import os; import rt; import test; 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; }; fn ostestwait(pid: i32, status: *i32) i32 = { for (true) { let r: i32 = os.wait4(pid, status, 0i32, nil: *void); if (r != -4i32) { return r; }; }; return -1; }; @test fn test_getenv_set() void = { let r = os.getenv("WW_TEST_GETENV"); match (r) { case void => test.skip("WW_TEST_GETENV unset (arranged leg: test/libenv)"); case let s: str => { assert(!(!streq(s, "hello-world"))); }; }; }; // POSIX permits an env var with an empty value (`name=` in environ). // getenv must return the empty str, NOT void — void is reserved for // "name not present at all". @test fn test_getenv_empty() void = { let r = os.getenv("WW_TEST_EMPTY"); match (r) { case void => test.skip("WW_TEST_EMPTY unset (arranged leg: test/libenv)"); case let s: str => { assert(!(s.len != 0)); }; }; }; @test fn test_getenv_unset() void = { let r = os.getenv("WW_TEST_NOT_SET"); match (r) { case void => {}; case let s: str => abort(); }; }; // Probes that "WW_TEST_GETEN" (a prefix of WW_TEST_GETENV) doesn't // match. Without the explicit `entry[name.len] == '='` check in // getenv, a naive prefix matcher would return the value of any // longer-named var that starts with the queried name. The row's // premise is that the longer var exists, so it skips with it. @test fn test_getenv_prefix_no_match() void = { match (os.getenv("WW_TEST_GETENV")) { case void => test.skip("WW_TEST_GETENV unset (arranged leg: test/libenv)"); case let s: str => {}; }; let r = os.getenv("WW_TEST_GETEN"); match (r) { case void => {}; case let s: str => abort(); }; }; // getenvs() is public API (task #28) and the single env walker getenv // routes through. getenv's cases exercise the walk transitively, but // none asserts the []str RETURN shape — its count or "NAME=VALUE" entry // text. This row pins both: the slice must be non-empty, and every // expected entry must appear verbatim (name, '=', value duped intact). // Fails if getenvs miscounts (len 0 → the inner search finds nothing) // or dups the wrong bytes (off-by-one truncation → no streq match). // The "WW_TEST_EMPTY=" row also pins the empty-value entry text. @test fn test_getenvs_entries() void = { let env: []str = os.getenvs(); assert(!(env.len == 0)); match (os.getenv("WW_TEST_GETENV")) { case void => test.skip("WW_TEST_GETENV unset (arranged leg: test/libenv)"); case let s: str => {}; }; match (os.getenv("WW_TEST_EMPTY")) { case void => test.skip("WW_TEST_EMPTY unset (arranged leg: test/libenv)"); case let s: str => {}; }; let want: [2]str = ["WW_TEST_GETENV=hello-world", "WW_TEST_EMPTY="]; let w: i32 = 0; for (w < 2) { let found: bool = false; let i: i32 = 0; for (i < env.len) { if (streq(env[i], want[w])) { found = true; }; i += 1; }; assert(found); w += 1; }; }; // Direct round-trip. Write-then-read-back proves the returned page is // dereferenceable. A miscompiled binding (wrong arg order, wrong ABI, // etc.) would either fault or return zero here. @test fn test_alloc_free_roundtrip() void = { let p: []u8 = alloc([], 4096u64)!; p.len = 4096; p[0] = 90u8; // 0x5a p[4095] = 165u8; // 0xa5 assert(!(p[0] != 90u8)); assert(!(p[4095] != 165u8)); os.free(p.ptr: *void, 4096u64); }; @test fn test_pipe_roundtrip() void = { let fds: [2]i32; assert(!(os.pipe(&fds) != 0)); let want: [1]u8 = [90u8]; let got: [1]u8; assert(!(os.write(fds[1], &want[0], 1u64) != 1i64)); assert(!(os.read(fds[0], &got[0], 1u64) != 1i64)); assert(!(got[0] != want[0])); assert(!(os.close(fds[0]) != 0)); assert(!(os.close(fds[1]) != 0)); }; @test fn test_pipe2_exec_hold() void = { let aa: []str = os.args(); if (len(aa) < 3 || !streq(aa[2], "pipe2-cloexec-helper")) { return; }; let b: [1]u8 = [1u8]; assert(!(os.write(101i32, &b[0], 1u64) != 1i64)); os.close(101i32); assert(!(os.read(100i32, &b[0], 1u64) != 1i64)); os.close(100i32); }; @test fn test_pipe2_cloexec() void = { let fds: [2]i32; assert(!(os.pipe2(&fds, os.O_CLOEXEC) != 0)); let hold: [2]i32; if (os.pipe(&hold) != 0) { os.close(fds[0]); os.close(fds[1]); abort(); }; let ready: [2]i32; if (os.pipe(&ready) != 0) { os.close(fds[0]); os.close(fds[1]); os.close(hold[0]); os.close(hold[1]); abort(); }; let pid: i32 = os.fork(); if (pid < 0) { os.close(fds[0]); os.close(fds[1]); os.close(hold[0]); os.close(hold[1]); os.close(ready[0]); os.close(ready[1]); abort(); }; if (pid == 0) { os.close(fds[0]); os.close(hold[1]); os.close(ready[0]); if (os.dup2(hold[0], 100i32) != 100i32) { os.exit(120); }; if (os.dup2(ready[1], 101i32) != 101i32) { os.exit(121); }; os.close(hold[0]); os.close(ready[1]); let aa: []str = os.args(); if (len(aa) == 0) { os.exit(122); }; let av: [4]*u8 = [aa[0].ptr, "test_pipe2_exec_hold\0".ptr, "pipe2-cloexec-helper\0".ptr, nil: *u8]; os.execve(aa[0], &av[0], nil: **u8); let mark: [1]u8 = ['E']; for (true) { let wr: i64 = os.write(fds[1], &mark[0], 1u64); if (wr == 1i64) { break; }; if (wr != -4i64) { os.exit(123); }; }; os.close(fds[1]); os.exit(124); }; os.close(fds[1]); os.close(hold[0]); os.close(ready[1]); let b: [1]u8; let rr: i64 = 0; for (true) { rr = os.read(ready[0], &b[0], 1u64); if (rr != -4i64) { break; }; }; if (rr != 1i64) { os.close(fds[0]); os.close(hold[1]); os.close(ready[0]); os.kill(pid, os.SIGKILL); let failedstatus: i32 = 0; ostestwait(pid, &failedstatus); abort(); }; os.close(ready[0]); for (true) { rr = os.read(fds[0], &b[0], 1u64); if (rr != -4i64) { break; }; }; os.close(fds[0]); let release: [1]u8 = [1u8]; let wr: i64 = os.write(hold[1], &release[0], 1u64); os.close(hold[1]); let status: i32 = 0; let r: i32 = ostestwait(pid, &status); assert(!(wr != 1i64)); assert(!(r != pid)); assert(os.wifexited(status)); assert(!(os.wexitstatus(status) != 0)); assert(!(rr != 0i64)); }; @test fn test_wait_status_decode() void = { let exited: i32 = 7i32 << 8i32; assert(os.wifexited(exited)); assert(!os.wifsignaled(exited)); assert(!(os.wexitstatus(exited) != 7)); assert(!(os.wtermsig(exited) != 0)); let signaled: i32 = os.SIGKILL | 128i32; assert(!os.wifexited(signaled)); assert(os.wifsignaled(signaled)); assert(!(os.wtermsig(signaled) != os.SIGKILL)); assert(!os.wifsignaled(127i32)); assert(!(os.WNOHANG != 1)); }; @test fn test_wait4_nohang_normal_exit() void = { let gate: [2]i32; assert(!(os.pipe(&gate) != 0)); let pid: i32 = os.fork(); assert(!(pid < 0)); if (pid == 0) { os.close(gate[1]); let b: [1]u8; if (os.read(gate[0], &b[0], 1u64) != 1i64) { os.exit(120); }; os.close(gate[0]); os.exit(7); }; os.close(gate[0]); let status: i32 = 0; let r: i32 = os.wait4(pid, &status, os.WNOHANG, nil: *void); if (r != 0) { os.close(gate[1]); if (r < 0) { os.kill(pid, os.SIGKILL); ostestwait(pid, &status); }; abort(); }; let b: [1]u8 = [1u8]; assert(!(os.write(gate[1], &b[0], 1u64) != 1i64)); os.close(gate[1]); r = ostestwait(pid, &status); assert(!(r != pid)); assert(os.wifexited(status)); assert(!(os.wexitstatus(status) != 7)); }; @test fn test_process_group_signal() void = { let hold: [2]i32; assert(!(os.pipe(&hold) != 0)); let pid: i32 = os.fork(); assert(!(pid < 0)); if (pid == 0) { os.close(hold[1]); if (os.setpgid(0i32, 0i32) != 0) { os.exit(120); }; let b: [1]u8; os.read(hold[0], &b[0], 1u64); os.exit(120); }; os.close(hold[0]); let pg: i32 = os.setpgid(pid, pid); if (pg != 0) { os.close(hold[1]); os.kill(pid, os.SIGKILL); let failedstatus: i32 = 0; ostestwait(pid, &failedstatus); abort(); }; let kr: i32 = os.kill(-pid, os.SIGTERM); os.close(hold[1]); if (kr != 0) { os.kill(pid, os.SIGKILL); let failedstatus: i32 = 0; ostestwait(pid, &failedstatus); abort(); }; let status: i32 = 0; let r: i32 = ostestwait(pid, &status); assert(!(r != pid)); assert(os.wifsignaled(status)); assert(!(os.wtermsig(status) != os.SIGTERM)); }; @test fn test_process_group_term_escalation() void = { let ready: [2]i32; assert(!(os.pipe(&ready) != 0)); let hold: [2]i32; if (os.pipe(&hold) != 0) { os.close(ready[0]); os.close(ready[1]); abort(); }; let pid: i32 = os.fork(); if (pid < 0) { os.close(ready[0]); os.close(ready[1]); os.close(hold[0]); os.close(hold[1]); abort(); }; if (pid == 0) { os.close(ready[0]); os.close(hold[1]); if (os.setpgid(0i32, 0i32) != 0) { os.exit(120); }; let mask: u64 = 1u64 << ((os.SIGTERM - 1): u64); if (os.sigprocmask(os.SIG_BLOCK, &mask, nil: *u64) != 0) { os.exit(121); }; let b: [1]u8 = [1u8]; if (os.write(ready[1], &b[0], 1u64) != 1i64) { os.exit(122); }; os.close(ready[1]); os.read(hold[0], &b[0], 1u64); os.exit(123); }; os.close(ready[1]); os.close(hold[0]); let pg: i32 = os.setpgid(pid, pid); if (pg != 0) { os.close(ready[0]); os.close(hold[1]); os.kill(pid, os.SIGKILL); let failedstatus: i32 = 0; ostestwait(pid, &failedstatus); abort(); }; let b: [1]u8; if (os.read(ready[0], &b[0], 1u64) != 1i64) { os.close(ready[0]); os.close(hold[1]); os.kill(-pid, os.SIGKILL); let failedstatus: i32 = 0; ostestwait(pid, &failedstatus); abort(); }; os.close(ready[0]); if (os.kill(-pid, os.SIGTERM) != 0) { os.close(hold[1]); os.kill(-pid, os.SIGKILL); let failedstatus: i32 = 0; ostestwait(pid, &failedstatus); abort(); }; let status: i32 = 0; let r: i32 = os.wait4(pid, &status, os.WNOHANG, nil: *void); if (r != 0) { os.close(hold[1]); if (r < 0) { os.kill(-pid, os.SIGKILL); ostestwait(pid, &status); }; abort(); }; let kr: i32 = os.kill(-pid, os.SIGKILL); os.close(hold[1]); if (kr != 0) { os.kill(pid, os.SIGKILL); ostestwait(pid, &status); abort(); }; r = ostestwait(pid, &status); assert(!(r != pid)); assert(os.wifsignaled(status)); assert(!(os.wtermsig(status) != os.SIGKILL)); assert(!(os.SIG_UNBLOCK != 1)); assert(!(os.SIG_SETMASK != 2)); }; @test fn test_chdir_child() void = { let pid: i32 = os.fork(); assert(!(pid < 0)); if (pid == 0) { if (os.chdir("/") != 0) { os.exit(120); }; let buf: [8]u8; let n: i64 = os.getcwd(&buf[0], size([8]u8)); if (n != 2i64 || buf[0] != '/' || buf[1] != 0u8) { os.exit(121); }; os.exit(0); }; let status: i32 = 0; assert(!(ostestwait(pid, &status) != pid)); assert(os.wifexited(status)); assert(!(os.wexitstatus(status) != 0)); }; @test fn test_signalfd_nonblocking() void = { let mask: u64 = 1u64 << ((os.SIGINT - 1): u64); let oldmask: u64 = 0u64; assert(!(os.sigprocmask(os.SIG_BLOCK, &mask, &oldmask) != 0)); let fd: i32 = os.signalfd(-1i32, &mask, os.SFD_CLOEXEC | os.SFD_NONBLOCK); if (fd < 0) { os.sigprocmask(os.SIG_SETMASK, &oldmask, nil: *u64); abort(); }; if (os.kill(os.getpid(), os.SIGINT) != 0) { os.close(fd); os.sigprocmask(os.SIG_SETMASK, &oldmask, nil: *u64); abort(); }; let info: [128]u8; let n: i64 = 0i64; for (true) { n = os.read(fd, &info[0], size([128]u8)); if (n != -4i64 && n != -11i64) { break; }; }; let cr: i32 = os.close(fd); let mr: i32 = os.sigprocmask(os.SIG_SETMASK, &oldmask, nil: *u64); assert(!(n != size([128]u8): i64)); assert(!(info[0] != os.SIGINT: u8)); assert(!(info[1] != 0u8 || info[2] != 0u8 || info[3] != 0u8)); assert(!(cr != 0)); assert(!(mr != 0)); assert(!(os.SFD_CLOEXEC != os.O_CLOEXEC)); assert(!(os.SFD_NONBLOCK != os.O_NONBLOCK)); };