Files
ww/lib/log/logtest.ww
Hojun-Cho e7f173cde0 lib+test: add log over io.stream sink
Hare-shaped lib/log v1: logger vtable carrying one println slot,
stdlogger forwarding to a *io.stream, plus *logger globals (silent
/ default / global), setlogger, lprintln / println, lfatal / fatal.
Default sink is stderr through a private rt_syscall write callback;
graduates with #17 (cgen mod-mangle for fn labels), at which point
the rt_syscall stub disappears the same way fmt's will.

Module-scope struct/pointer-literal init isn't constexpr in cstage
emit_lets, so silent/default/global are wired lazily by ensureinit
on the first exported-fn entry (lib/temp rnginit pattern). Callers
that read the globals directly must call some lib/log fn first.

Skipped this round: printfln / lprintfln / fatalf / lfatalf and the
matching printfln vtable slot — they need a fmt {n}-placeholder
parser that isn't shipped yet. fatal / lfatal's exit(255) arm has
no test fixture (needs fork+wait for WEXITSTATUS); left as TODO.

971_log_run wraps the @test fixture under `ww run`, mirroring
970_fmt_run. make test: 54/54; bootstrap fixed-point (990-997)
holds.
2026-05-15 14:05:55 +09:00

190 lines
5.6 KiB
Plaintext

// logtest — exercises lib/log. Run with `out/bin/ww run lib/log/logtest.ww`.
//
// One scenario per @test fn (the ww-stdlib idiom): each scenario
// constructs a stdlogger over a memio.stream and asserts the bytes
// the logger produced. The test surface mirrors what drew called out
// in the design read: lprintln-to-memio, silent-writes-nothing, and
// setlogger-swap. The process-terminating arm ([[log.fatal]] /
// [[log.lfatal]]) needs a subprocess to verify exit(255) without
// killing the test driver — left as a TODO until the project grows
// a subprocess fixture.
use fmt;
use io;
use log;
use memio;
// Direct exit(2) binding rather than `use os;` — os exports
// read/write/close, which collide with io.read/write/close under
// the driver's flat-scope concat. Same workaround as bufiotest /
// fmttest / memiotest.
@symbol("rt_syscall") fn syscall1ww(num: i64, a: i64) i64;
fn doexit(code: i32) void = {
syscall1ww(60i64, code: i64);
};
// signalled — bumped before each scenario so a failing exit code
// pinpoints the offending case.
let signalled: i32 = 0;
fn fail() void = { doexit(signalled + 10); };
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;
};
// ---- default points at global after init (must run first) ------------
// log.default writes to stderr through the private stderrsink — we
// can't easily capture stderr from inside the test, so this scenario
// verifies the initial wiring only: after lazy [[ensureinit]] runs,
// [[log.default]] / [[log.silent]] are non-nil and [[log.global]]
// equals [[log.default]]. The check is order-sensitive: any later
// scenario that calls [[log.setlogger]] mutates the global, so this
// scenario must run before [[setloggerswap]].
@test fn defaultwiredtoglobal() void = {
let buf: [4]u8;
let mem: memio.state;
let s: io.stream;
memio.fixed(&mem, &s, buf[0:4]);
let sl: log.stdlogger;
log.new(&sl, &s); // triggers ensureinit
if (log.silent == nil) { fail(); };
if (log.default == nil) { fail(); };
if (log.global == nil) { fail(); };
if (log.global != log.default) { fail(); };
};
// ---- lprintln to a memio sink: exact-byte assertion -------------------
@test fn lprintlnbasic() void = {
let buf: [32]u8;
let mem: memio.state;
let s: io.stream;
memio.fixed(&mem, &s, buf[0:32]);
let sl: log.stdlogger;
log.new(&sl, &s);
log.lprintln(&sl.logger, "hello", 42i64);
if (!streq(memio.string(&mem), "hello 42\n")) { fail(); };
};
// ---- lprintln single-arg: no leading space, just trailing newline -----
@test fn lprintlnsingle() void = {
let buf: [16]u8;
let mem: memio.state;
let s: io.stream;
memio.fixed(&mem, &s, buf[0:16]);
let sl: log.stdlogger;
log.new(&sl, &s);
log.lprintln(&sl.logger, "only");
if (!streq(memio.string(&mem), "only\n")) { fail(); };
};
// ---- lprintln zero args: bare newline ---------------------------------
@test fn lprintlnempty() void = {
let buf: [4]u8;
let mem: memio.state;
let s: io.stream;
memio.fixed(&mem, &s, buf[0:4]);
let sl: log.stdlogger;
log.new(&sl, &s);
log.lprintln(&sl.logger);
if (!streq(memio.string(&mem), "\n")) { fail(); };
};
// ---- silent: lprintln through log.silent writes no bytes --------------
// The silent logger's callback discards args without touching fmt or
// any sink. Verified indirectly: we wire a memio.stream and never
// pass it to log.silent — but the @test fn calling log.new first
// triggers [[log.ensureinit]] so log.silent is non-nil here. Then
// lprintln(silent, ...) must not crash and the memio sink must stay
// empty (silent has no path to it anyway).
@test fn silentwritesnothing() void = {
let buf: [16]u8;
let mem: memio.state;
let s: io.stream;
memio.fixed(&mem, &s, buf[0:16]);
let sl: log.stdlogger;
log.new(&sl, &s); // triggers log.ensureinit; populates log.silent
if (log.silent == nil) { fail(); };
log.lprintln(log.silent, "ignored", 1i64, true);
if (mem.pos != 0) { fail(); };
};
// ---- setlogger swap: global redirects to a new sink, then to silent --
// Three-phase: install sl1 as global, println writes there; swap to
// sl2, println writes there only; swap to silent, println discards.
// Each memio sink starts empty and is asserted at each phase to pin
// the swap semantics.
@test fn setloggerswap() void = {
let buf1: [32]u8;
let mem1: memio.state;
let s1: io.stream;
memio.fixed(&mem1, &s1, buf1[0:32]);
let sl1: log.stdlogger;
log.new(&sl1, &s1);
let buf2: [32]u8;
let mem2: memio.state;
let s2: io.stream;
memio.fixed(&mem2, &s2, buf2[0:32]);
let sl2: log.stdlogger;
log.new(&sl2, &s2);
log.setlogger(&sl1.logger);
log.println("first");
if (!streq(memio.string(&mem1), "first\n")) { fail(); };
if (mem2.pos != 0) { fail(); };
log.setlogger(&sl2.logger);
log.println("second");
if (!streq(memio.string(&mem2), "second\n")) { fail(); };
// mem1 must be unchanged.
if (!streq(memio.string(&mem1), "first\n")) { fail(); };
log.setlogger(log.silent);
log.println("dropped");
// Both sinks unchanged.
if (!streq(memio.string(&mem1), "first\n")) { fail(); };
if (!streq(memio.string(&mem2), "second\n")) { fail(); };
};
// TODO subprocess: log.fatal / log.lfatal exit(255). Verifying them
// needs a fork+wait fixture so the parent can assert WEXITSTATUS ==
// 255 without the test process itself terminating. lib/os doesn't
// ship process spawning yet; revisit when that lands.
export fn main() i32 = {
signalled = 1; defaultwiredtoglobal();
signalled = 2; lprintlnbasic();
signalled = 3; lprintlnsingle();
signalled = 4; lprintlnempty();
signalled = 5; silentwritesnothing();
signalled = 6; setloggerswap();
return 0;
};