Skip to content

std/test

std/test — pure-Lang unit-test runner. Designed to replace the Go-side *_test.go harness once the compiler itself is self-hosted (see docs/ROADMAP-AND-SELF-HOSTING.md). The runner takes no compiler dependency: it’s an ordinary Lang program that uses the helpers exported here, threads a TestRunner accumulator through each test case, and exits 0 on success / 1 on any failure. Output is TAP-13 (https://testanything.org/) so existing CI tooling (prove, tape, jUnit converters) can consume it directly while we transition off Go. std/test is part of the auto-prelude (see internal/prelude/prelude.fern), so test programs reach for the helpers by bare name — no test.assert_eq_i32(...) qualification, no import "std/test"; line needed. A user program that opts out of the prelude with import "core/no_prelude"; would have to import this module explicitly and qualify its names. function test_addition(): Option[string] { return assert_eq_i32(2 + 2, 4); } function test_strings(): Option[string] { return assert_eq_string(“foo” + “bar”, “foobar”); } function main(): i32 { var r: TestRunner = test_new(“arithmetic”); r = r.it(“addition”, test_addition()); r = r.it(“strings”, test_strings()); return r.finish(); } Tests are functions returning Option[string]: None — the case passed Some(msg) — the case failed; msg is the human-readable diff The runner is value-semantic — r.it(...) returns a fresh TestRunner with the result folded in. That matches the rest of lang’s stdlib (Array.push, struct receiver methods) and sidesteps the lack of interior mutability. (case would have been the obvious method name but it’s a reserved keyword for switch arms; it matches the BDD describe / it convention from JS / Ruby test frameworks.) Discovery is intentionally explicit: lang has no reflection, so main lists the tests it runs. That’s the same shape Zig’s test blocks compile down to and the same shape any future macro / build-step driven discovery would lower to.

pub struct TestRunner { suite: string, passed: i32, failed: i32, skipped: i32, failures: string[], skip_reasons: string[], verbose: boolean, prefix: string, base_idx: i32, cleanup_paths: string[], filter: string, fail_fast: boolean, quiet: boolean }

TestRunner — per-suite accumulator. Threaded through each .it(name, result) call. failures keeps the formatted ”: ” line for every failing case so finish() can repeat them at the bottom of the run (TAP consumers can look at the per-line not-ok output; humans want a summary).

skipped mirrors failures for cases that bailed out before running their assertion — toolchain missing, OS-conditional, etc. They print as ok N - name # SKIP reason per TAP-13, stay counted separately from passes / fails, and don’t affect the exit code (skipping isn’t a failure).

prefix lets nested subsuites prepend ” / ” to each case name without losing the flat TAP stream. The parent runner threads a child through (r) subsuite(name) which inherits the parent’s counters by reference (via the child = parent value copy + a merge at the end), then (parent) merge(child) folds the child’s counts back. The flat stream makes the output legible under any TAP consumer while the name prefix preserves the hierarchy.

pub function test_new(suite: string): TestRunner

test_new(suite) — start a new run. suite labels the run in the TAP # Suite: comment line. Tests print as they execute so a hanging case is obvious in the output stream.

pub function test_new_filtered(suite: string, filter: string): TestRunner

test_new_filtered(suite, filter) — same as test_new but pre-installs a case-name filter. Cases whose (prefix + name) don’t contain the filter substring skip instead of running. Use parse_filter_from_args(args()) to lift the filter from a --filter PATTERN command-line flag without hand-rolling the argv plumbing.

pub function parse_filter_from_args(argv: string[]): string

parse_filter_from_args(args) — scan a args()-style string array for --filter PATTERN or --filter=PATTERN and return the pattern. Returns "" if no flag was given, which matches the “no filter” sentinel test_new_filtered uses.

var r: TestRunner = test_new_filtered(“my suite”, parse_filter_from_args(args()));

Skip patterns that are CLI-flag-shaped beyond the basic --filter — anything fancier should reach for a real argv-parser module once one lands.

pub function test_new_fail_fast(suite: string): TestRunner

test_new_fail_fast(suite) — same as test_new but enables fail-fast mode. Once any case fails, subsequent it() calls auto-skip with the reason “fail-fast: prior case failed” rather than running. Use for long suites where you want to fix the first failure before learning about the next 50.

Pair with parse_fail_fast_from_args(args()) to lift the setting from a --fail-fast command-line flag:

var r: TestRunner = test_new(“my suite”); if (parse_fail_fast_from_args(args())) { r = r.with_fail_fast(); }

pub function (r: TestRunner) with_fail_fast(): TestRunner

(r).with_fail_fast() — flips fail-fast mode on for an existing runner. Lets a test that already constructed r via test_new(...) opt into fail-fast based on a CLI flag without having to discard the runner.

pub function parse_fail_fast_from_args(argv: string[]): boolean

parse_fail_fast_from_args(argv) — true iff --fail-fast appears anywhere in argv. Use with with_fail_fast():

var r: TestRunner = test_new(“my suite”); if (parse_fail_fast_from_args(args())) { r = r.with_fail_fast(); }

pub function test_new_quiet(suite: string): TestRunner

test_new_quiet(suite) — same as test_new but enables quiet mode. In quiet mode, the per-case ok N - name line is suppressed for passes + skips; only failures emit per-case detail. The 1..N plan line and the summary at the bottom still print, so TAP consumers that need the case count reconstruct it from there.

Use for the developer loop where seeing every passing test is more noise than signal. CI logs are usually better off WITHOUT this (full TAP makes regressions easier to triage).

Pair with parse_quiet_from_args(args()) to lift the setting from a --quiet command-line flag:

var r = test_new(“my suite”); if (parse_quiet_from_args(args())) { r = r.with_quiet(); }

pub function (r: TestRunner) with_quiet(): TestRunner

(r).with_quiet() — flips quiet mode on for an existing runner. Lets a test that already constructed r via test_new(...) opt into quiet based on a CLI flag without having to discard the runner.

pub function parse_quiet_from_args(argv: string[]): boolean

parse_quiet_from_args(argv) — true iff --quiet appears anywhere in argv. Use with with_quiet():

var r = test_new(“my suite”); if (parse_quiet_from_args(args())) { r = r.with_quiet(); }

pub function test_new_verbose(suite: string): TestRunner

test_new_verbose(suite) — same as test_new but flags the runner for future verbose output. Currently identical to test_new because TAP-13 already prints every case; the flag is reserved for a future “—verbose” mode that adds timing or extra context to passing cases.

pub function (r: TestRunner) it(name: string, result: Option[string]): TestRunner

(r) it(name, result) — record the outcome of one test. result is the value returned by the test function: None prints “ok N - name” Some(msg) prints “not ok N - name” + the YAML-ish --- block TAP uses for diagnostic detail

The displayed name is r.prefix + name, so a subsuite’s cases land as “parent / child / actual case” in the flat TAP stream. The bare name is what surfaces in the failures summary at the bottom of finish() — typically the prefix duplicates the suite line so the bottom summary stays scannable.

pub function (r: TestRunner) skip(name: string, reason: string): TestRunner

(r) skip(name, reason) — record a case that didn’t run. Emits TAP-13’s # SKIP <reason> directive on the ok line so consumers (prove, tape, jUnit converters) can distinguish it from a real pass. The skipped count is tracked separately and doesn’t influence the exit code: missing toolchains / OS-conditional cases shouldn’t fail CI on dev laptops that lack the optional dep.

Typical use:

if (env(“WASMTIME_PATH”).is_none()) { r = r.skip(“wasm e2e”, “wasmtime not on $PATH”); } else { r = r.it(“wasm e2e”, run_wasm_test()); }

pub function (r: TestRunner) skip_if(cond: boolean, name: string, reason: string, result: Option[string]): TestRunner

(r) skip_if(cond, name, reason, result) — conditional run. If cond is true, the case skips with reason; otherwise result is recorded as usual. Lets callers keep the test list flat without an if/else per gated case.

r = r.skip_if(env(“CI”).is_none(), “ci-only”, “not CI”, run_test());

result is evaluated eagerly (lang has no lazy params), so any setup the test does runs whether or not it’s skipped. For setup that’s expensive or has side effects, branch on cond yourself and use skip / it directly.

pub function (r: TestRunner) subsuite(name: string): TestRunner

(r) subsuite(name) — start a nested grouping. Returns a fresh TestRunner that inherits the parent’s prefix and counts. After the subsuite’s cases land via child.it(...) / child.skip(...), the parent calls parent.merge(child) to fold the counters back. The child’s case names print with the combined prefix so the flat TAP stream stays hierarchical:

r = r.it(“standalone”, run_standalone()); var arm = r.subsuite(“arm64”); arm = arm.it(“exit-code”, run_arm64_exit()); arm = arm.it(“stdout”, run_arm64_stdout()); r = r.merge(arm); r = r.it(“after subsuite”, run_after());

The child runner does NOT print a # Suite: line — that’s reserved for top-level test_new. The prefix string is the only nesting signal. Two-level nesting works too (arm.subsuite("regress")) by chaining the prefixes.

pub function (r: TestRunner) merge(child: TestRunner): TestRunner

(r) merge(child) — fold a subsuite’s counters and failure notes back into the parent. The child’s passed / failed / skipped get added; its failures / skip_reasons get concatenated. The parent’s prefix and verbose flag are preserved.

pub function (r: TestRunner) defer_cleanup(path: string): TestRunner

(r) defer_cleanup(path) — register a filesystem path that should be removed when finish() runs, regardless of test outcome. Designed for the temp_dir(...) pattern: a test creates a fresh directory, registers it for cleanup immediately, then writes fixtures + spawns subprocesses against it.

var dir: string = expect_ok(temp_dir(“foo”)); r = r.defer_cleanup(dir);

Each registered path goes through remove_dir_all at finish() time. Errors from cleanup are recorded as test failures (so a bug that leaves the tmpfs unwritable surfaces in CI), but they don’t replace the test exit code’s “real” cause — fast-failing on a cleanup error would hide the actual test failure.

pub function (r: TestRunner) log(msg: string): TestRunner

(r) log(msg) — emit a TAP comment (# msg) without recording a test outcome. Useful for debug breadcrumbs in long-running suites — TAP consumers ignore unknown comment lines, humans grep for them. Returns the runner unchanged so the call chains cleanly between .it(...) records:

r = r.log(“about to spawn child”); r = r.it(“spawned”, spawn_test());

Newlines in msg get printed verbatim — multiple TAP comment lines if msg contains \n. Lang’s print already handles the trailing newline.

pub function (r: TestRunner) log_kv_string(key: string, value: string): TestRunner

(r).log_kv_string(key, value) — formatted key=value TAP comment. Quotes the value so embedded spaces / special chars stay visually separable from the key.

r = r.log_kv_string(“session_id”, id);

Use over plain r.log(...) when emitting a structured breadcrumb the post-run log scraper might want to parse (e.g., “what tempdir did this test use”, “what trace id did the subprocess produce”) — the key=value shape is grep | awk -F= friendly.

pub function (r: TestRunner) log_kv_i32(key: string, value: i32): TestRunner

(r).log_kv_i32(key, value) — integer-valued breadcrumb. Value rendered unquoted so numeric grep / awk filters (e.g., awk -F= '$1=="bytes" && $2>1024') work without stripping quotes.

pub function (r: TestRunner) log_kv_i64(key: string, value: i64): TestRunner

(r).log_kv_i64(key, value) — wider-int breadcrumb. Use for byte counts, timestamps, anything beyond i32 range.

pub function (r: TestRunner) finish(): i32

(r) finish() — print the TAP plan line + per-suite summary and return the process exit code (0 = all passed, 1 = any failed). Call this exactly once at the end of main().

TAP plan goes at the bottom of the output (the “lazy plan” shape) because the test count isn’t known up-front — we’d have to count cases in main() before running them, which every test file would have to remember to keep in sync. Bottom-plan is permitted by TAP-13 and most consumers (prove, tape) handle it.

pub function pass(): Option[string]

pass() — explicit None for cases that need an early successful return (eg a test that walks a list and bails out on the first interesting state).

pub function fail(msg: string): Option[string]

fail(msg) — explicit failure with a custom message. Useful when a test computes its own diff or wants to flag an unreachable branch (“expected match to have hit the first arm”).

pub function unreachable(label: string): Option[string]

unreachable(label) — sugar for fail("unreachable: " + label). Used in match-default arms / impossible-branch dispatchers that the test logic claims can’t fire. If the branch DOES fire (a refactor expanded the input space), the failure message names the label so the bug is grep-able in CI logs.

pub function assert_true(cond: boolean): Option[string]

assert_true(cond) / assert_false(cond) — boolean predicates. The failure message names which direction failed so a glance at the log makes it obvious.

pub function assert_false(cond: boolean): Option[string]
pub function assert_eq[T](actual: T, expected: T): Option[string]

Generic equality + relational assertions over the core/cmp traits. assert_eq / assert_neq accept any Eq + Display type (i32 / i64 / u32 / u64 / string / boolean, and any user type that implements both traits); assert_lt / _le / _gt / _ge accept any Ord + Display type. These replaced the old per-width assert_eq_i32 / assert_lt_u64 / … families once traits landed (docs/TRAITS.md). The failure messages embed both values via to_string so you don’t have to re-run a test to see what came out.

pub function assert_neq[T](actual: T, expected: T): Option[string]
pub function assert_lt[T](a: T, b: T): Option[string]
pub function assert_le[T](a: T, b: T): Option[string]
pub function assert_gt[T](a: T, b: T): Option[string]
pub function assert_ge[T](a: T, b: T): Option[string]
pub function assert_eq_f64_near(actual: f64, expected: f64, epsilon: f64): Option[string]

assert_eq_f64_near(actual, expected, epsilon) — passes if |actual - expected| <= epsilon. NaN inputs always fail (NaN != anything, including itself); the message names “NaN” so the failure is obvious. The helper returns None for ±Inf-matching-±Inf, since float subtraction would yield NaN there.

pub function assert_eq_f32_near(actual: f32, expected: f32, epsilon: f32): Option[string]

assert_eq_f32_near(actual, expected, epsilon) — f32 version of the same idea. Widens both sides to f64 for the difference calc to avoid the precision-loss artifact where (small_f32 - small_other_f32) underflows to zero in f32 arithmetic but the values were genuinely different.

pub function assert_eq_f64_rel(actual: f64, expected: f64, rel_tol: f64): Option[string]

assert_eq_f64_rel(actual, expected, rel_tol) — relative- tolerance float compare. Passes when |actual - expected| / |expected| <= rel_tol. Use this when the scale of the expected value varies by many orders of magnitude across test cases — an absolute epsilon that’s generous for expected=1e6 is wildly too loose for expected=1e-6. NaN inputs always fail. Falls back to absolute compare when expected == 0.0 (the rel formula would divide by zero).

pub function assert_eq_f32_rel(actual: f32, expected: f32, rel_tol: f32): Option[string]

assert_eq_f32_rel(actual, expected, rel_tol) — f32 mirror of assert_eq_f64_rel. Same widen-to-f64 trick as assert_eq_f32_near to avoid precision-loss surprises.

pub function assert_eq_f64_exact(actual: f64, expected: f64): Option[string]

assert_eq_f64_exact(actual, expected) — bitwise-equal floats. Use for f32_bits round-trip tests / canonical-NaN checks where epsilons would smuggle bugs through. Most other callers want _near.

pub function assert_is_nan_f64(v: f64): Option[string]

assert_is_nan_f64(v) — explicit NaN check. v != v is the textbook NaN predicate (the only float that doesn’t compare equal to itself). Helper exists because writing v != v in a test reads like a typo.

pub function assert_is_nan_f32(v: f32): Option[string]
pub function assert_contains(haystack: string, needle: string): Option[string]

assert_contains(haystack, needle) — substring search. Empty needle always passes (mirrors (s).contains("") which returns true everywhere). Failure embeds the haystack so the diff against your fixture is one glance.

pub function assert_not_contains(haystack: string, needle: string): Option[string]

assert_not_contains(haystack, needle) — the inverse. Empty needle always fails because every string contains the empty string.

pub function assert_starts_with(s: string, prefix: string): Option[string]
pub function assert_ends_with(s: string, suffix: string): Option[string]
pub function assert_eq_string_ci(actual: string, expected: string): Option[string]

assert_eq_string_ci(actual, expected) — ASCII case- insensitive equality. Failure embeds both raw values (no case-folding applied for display) so the reader sees exactly which bytes differ.

pub function assert_neq_string_ci(actual: string, expected: string): Option[string]

assert_neq_string_ci(actual, expected) — must differ even after case-folding.

pub function assert_contains_ci(haystack: string, needle: string): Option[string]

assert_contains_ci(haystack, needle) — case- insensitive substring search.

pub function assert_starts_with_ci(s: string, prefix: string): Option[string]

assert_starts_with_ci(s, prefix) — case-insensitive prefix check.

pub function assert_ends_with_ci(s: string, suffix: string): Option[string]

assert_ends_with_ci(s, suffix) — case-insensitive suffix check.

pub function assert_empty_string(s: string): Option[string]

assert_empty_string(s) — sugar for assert_eq_i32(len(s), 0) that names the assertion in the failure message.

pub function assert_non_empty_string(s: string): Option[string]
pub function assert_string_count(haystack: string, needle: string, n: i32): Option[string]

assert_string_count(haystack, needle, n)needle appears exactly n times (non-overlapping) in haystack. Use when the test cares about cardinality (e.g., “the log has 3 ERROR lines”, “the output has exactly one newline”) without committing to where the occurrences sit. Delegates to std/string’s .count(sub) receiver method so the “non-overlapping” semantics stay consistent with other string library callers.

pub function assert_sorted_asc[T](arr: T[]): Option[string]

assert_len_i32(arr, n) / assert_len_string(arr, n) — array length checks. Split per element type because lang doesn’t have a T[] generic at the function-signature level for the call sites we currently want. Generic ordered / set array assertions over core/cmp. assert_sorted_* need Ord + Display; assert_set_eq / assert_subset / assert_unique need Eq + Display. These replaced the per-element-type families once traits landed. (set_eq / unique compare in O(n^2) via .eq() rather than sorting, since there is no generic sort — same result.) See docs/TRAITS.md.

pub function assert_sorted_desc[T](arr: T[]): Option[string]
pub function assert_strictly_sorted_asc[T](arr: T[]): Option[string]
pub function assert_unique[T](arr: T[]): Option[string]
pub function assert_subset[T](superset: T[], subset: T[]): Option[string]
pub function assert_set_eq[T](actual: T[], expected: T[]): Option[string]
pub function assert_len_i32(arr: i32[], n: i32): Option[string]
pub function assert_len_string(arr: string[], n: i32): Option[string]
pub function assert_eq_array[T](actual: T[], expected: T[]): Option[string]

assert_at_f64(arr, idx, expected, epsilon) — f64 element with tolerance (mirrors assert_eq_f64_near’s semantics). NaN inputs always fail. The epsilon parameter is mandatory because exact float equality is almost never what tests actually want. Generic array assertions over core/cmp. assert_eq_array / assert_at / assert_array_contains / assert_array_not_contains accept any Eq + Display element type — they replaced the old per-element-width families (assert_eq_i32_array, assert_at_string, …) once traits landed. See docs/TRAITS.md.

pub function assert_at[T](arr: T[], idx: i32, expected: T): Option[string]
pub function assert_array_contains[T](arr: T[], needle: T): Option[string]
pub function assert_array_not_contains[T](arr: T[], needle: T): Option[string]
pub function assert_at_f64(arr: f64[], idx: i32, expected: f64, epsilon: f64): Option[string]
pub function assert_at_f32(arr: f32[], idx: i32, expected: f32, epsilon: f32): Option[string]

assert_at_f32(arr, idx, expected, epsilon) — f32 mirror. Widens both sides per-element to f64 for the difference calc (same precision-safety trick as the scalar assert_eq_f32_near).

pub function assert_eq_f64_array_near(actual: f64[], expected: f64[], epsilon: f64): Option[string]

assert_eq_f64_array_near(actual, expected, epsilon) — element-wise float array compare. Each pair must satisfy |actual[i] - expected[i]| <= epsilon. NaN anywhere fails (NaN never compares within tolerance). Use when a function returns f64[] and the test wants to pin the whole vector; the _near semantics mean cumulative rounding error in vector ops doesn’t fail the test spuriously.

pub function assert_eq_f32_array_near(actual: f32[], expected: f32[], epsilon: f32): Option[string]

assert_eq_f32_array_near(actual, expected, epsilon) — f32 mirror. Widens both sides per element to f64 for the difference calc, same precision-safety trick as the scalar assert_eq_f32_near.

pub function assert_exit(actual: ProcessResult, expected: i32): Option[string]

assert_exit(actual: ProcessResult, expected: i32) — verify the spawned process exited with the given code. Failure message embeds stdout + stderr (truncated to keep CI logs readable) so the diff against your fixture is one glance.

pub function assert_stdout_eq(actual: ProcessResult, expected: string): Option[string]

assert_stdout_eq / assert_stderr_eq — exact-byte match against the captured stream. Whitespace counts; trim the inputs before calling if you want to ignore trailing newlines.

pub function assert_stderr_eq(actual: ProcessResult, expected: string): Option[string]
pub function assert_stdout_contains(actual: ProcessResult, needle: string): Option[string]

assert_stdout_contains / assert_stderr_contains — substring search. The expected-substring search is way friendlier than exact equality for diagnostic output (where the wording changes more often than the substantive content).

pub function assert_stderr_contains(actual: ProcessResult, needle: string): Option[string]
pub function assert_process(actual: ProcessResult, expected_exit: i32, stdout_substr: string): Option[string]

assert_process(actual, exit, stdout_substr) — fold the common “exit + stdout-contains” triple into one helper so the typical e2e shape stays a one-liner. stdout_substr is treated as a substring search (the most-used direction); callers wanting exact match should use assert_stdout_eq separately.

pub function assert_exit_zero(actual: ProcessResult): Option[string]

assert_exit_zero(actual) — sugar for assert_exit(.., 0). By far the most common shape (the “successful CLI invocation” path), and naming it explicitly reads better in a test summary than assert_exit(_, 0).

pub function assert_exit_nonzero(actual: ProcessResult): Option[string]

assert_exit_nonzero(actual) — must have exited with some non-zero code (the “expected failure” path). Doesn’t pin a specific code; use assert_exit(_, N) when the test cares which non-zero code surfaced.

pub function assert_stdout_lines(actual: ProcessResult, expected_lines: string[]): Option[string]

assert_stdout_lines(actual, expected_lines) — split the captured stdout on \n and compare to a string array. Delegates to assert_lines_eq so the failure message names the first differing line (same wording as the file-side / in-memory variants); a trailing newline does NOT count as an extra empty line.

pub function assert_stderr_lines(actual: ProcessResult, expected_lines: string[]): Option[string]

assert_stderr_lines(actual, expected_lines) — stderr mirror.

pub function assert_stdout_line_count(actual: ProcessResult, n: i32): Option[string]

assert_stdout_line_count(actual, n) — line cardinality on stdout. Use when the contract is “produce N records” and the lines themselves vary too much to pin exactly (timestamps, random ids, etc.). Same line definition as .lines() — trailing newline doesn’t overcount.

pub function assert_stderr_line_count(actual: ProcessResult, n: i32): Option[string]

assert_stderr_line_count(actual, n) — stderr mirror.

pub function assert_contains_all(haystack: string, needles: string[]): Option[string]

assert_contains_all(haystack, needles) — every entry in needles must appear somewhere in haystack (order is NOT required; if order matters, use a sequence of substring index_of checks). Empty needles list always passes.

pub function assert_contains_any(haystack: string, needles: string[]): Option[string]

assert_contains_any(haystack, needles) — at least one entry in needles must appear. Useful for “the diagnostic mentions EITHER X or Y” cases where the exact wording is incidental. Empty needles list always fails (vacuously: nothing to match).

pub function assert_contains_in_order(haystack: string, needles: string[]): Option[string]

assert_contains_in_order(haystack, needles) — every needle must appear AND the matches must be in the order given. Useful for asserting on a structured diagnostic (“expected X at line N, got Y”) where the file/line/expected/actual sequence is contractual.

pub function assert_eq_string_diff(actual: string, expected: string): Option[string]

assert_eq_string_diff(actual, expected) — exact equality but the failure message reports the first differing line + its line number + the values on each side. The diff is friendlier than the plain assert_eq_string shape for multi-line stdout or generated source.

pub function assert_lines_eq(actual: string, expected_lines: string[]): Option[string]

assert_lines_eq(actual, expected_lines) — split actual on \n and compare line-by-line against expected_lines. The expected side is an explicit string[], which reads better in tests than wrapping the expected output in one long backslash-escaped literal. Failure message names the first differing line + its 1-based index + both values, same shape as assert_eq_string_diff.

pub function assert_in_range_i32(v: i32, lo: i32, hi: i32): Option[string]

assert_in_range_i32(v, lo, hi)lo <= v <= hi.

pub function assert_in_range_i64(v: i64, lo: i64, hi: i64): Option[string]

assert_in_range_i64(v, lo, hi) — same for wider ints.

pub function assert_in_range_f64(v: f64, lo: f64, hi: f64): Option[string]

assert_in_range_f64(v, lo, hi) — inclusive float range. NaN inputs always fail (NaN never satisfies an ordering comparison; pretending it’s in-range would mask bugs).

pub function assert_in_range_f32(v: f32, lo: f32, hi: f32): Option[string]

assert_in_range_f32(v, lo, hi) — f32 mirror. Direct implementation (not delegating to _f64) so the failure message names this helper, keeping diagnostics greppable.

pub function assert_file_exists(path: string): Option[string]

assert_file_exists(path) — the path must be a readable file. The check goes through read_file, so a path that exists but isn’t readable surfaces as a failure too.

pub function assert_file_not_exists(path: string): Option[string]

assert_file_not_exists(path) — the path must NOT resolve to a readable file. Useful for asserting that a cleanup actually scrubbed something.

pub function assert_file_contents(path: string, expected: string): Option[string]

assert_file_contents(path, expected) — read the file and compare its full contents (byte-exact) to expected. Routes the long-string diff through assert_eq_string_diff so the failure message localises the first differing line.

pub function assert_file_lines(path: string, expected_lines: string[]): Option[string]

assert_file_lines(path, expected_lines) — read the file and compare its line-decomposed contents to a string array. Common shape: a subprocess writes N lines to its output file, the test wants to pin each line WITHOUT escaping a long multi-line string literal. Delegates to assert_lines_eq so the line-by-line diff and failure message wording stay identical to the in-memory version.

pub function assert_file_line_count(path: string, n: i32): Option[string]

assert_file_line_count(path, n) — count lines in a file. Uses the same definition of “line” as lines(): a final trailing newline does NOT add an extra empty line. Use when the contract is “produce N records” and the test cares about cardinality more than content.

pub function assert_is_file(path: string): Option[string]

assert_is_file(path) / assert_is_dir(path)stat-backed type predicates. assert_file_exists (above) goes through read_file which succeeds for regular files; these are the explicit pair for callers that need to distinguish files from directories without inspecting the raw FileStat themselves.

pub function assert_is_dir(path: string): Option[string]
pub function assert_file_size(path: string, expected: i64): Option[string]

assert_file_size(path, expected) — match the file’s byte size against an i64 expectation. Useful for fixture tests that copy / generate files of a known size.

pub function assert_eq_dir_listing(dir: string, expected_names: string[]): Option[string]

assert_eq_dir_listing(dir, expected_names) — list the directory and verify its contents (file/subdir names, not paths) form the same multiset as expected_names. readdir order isn’t observable, so the helper compares by sorting both sides and delegating to assert_eq_string_array — the failure message localises the first mismatching name + its index in sorted order.

Pair with must_temp_dir(r, prefix) + a fixture- creation step to pin “the operation produced exactly these files”.

pub function assert_file_contains(path: string, needle: string): Option[string]

assert_file_contains(path, needle) — file contents include the given substring. Faster than assert_file_contents when the test only cares that the output mentions something specific, not that it matches a full golden.

pub function must_temp_dir(r: TestRunner, prefix: string): (string, TestRunner)

=================== TEMPDIR CONVENIENCE ===================

must_temp_dir(r, prefix) — single-shot tempdir + cleanup registration. Returns the path (empty string on failure) and a runner with the cleanup hook already wired. Callers that expect tempdir creation to always succeed (the typical case — /tmp is writable on every supported platform) can skip the match boilerplate.

var dir: string = ""; var rr: TestRunner = r; match (must_temp_dir(rr, “my-fixture”)) { (p, nr) => { dir = p; rr = nr; } }

Returns a 2-tuple (path, runner). When tempdir creation fails the runner gets a skip recorded and the path is empty — callers should check dir != "" before using it.

pub function (r: TestRunner) bench(name: string, iterations: i32, fn: () => void): TestRunner

(r) bench(name, iterations, fn) — run fn repeatedly, emit a TAP comment with the per-iteration timing summary. Always passes; use bench_max_us for a regression bound.

pub function (r: TestRunner) bench_max_us(name: string, iterations: i32, fn: () => void, max_us: i64): TestRunner

(r) bench_max_us(name, iterations, fn, max_us) — same as bench but fails when the MEDIAN per-iteration time exceeds max_us. Median (not mean) is the right regression signal: one GC pause inflates the mean but leaves the median honest.

pub function (r: TestRunner) bench_max_ms(name: string, iterations: i32, fn: () => void, max_ms: i64): TestRunner

(r) bench_max_ms(name, iterations, fn, max_ms) — millisecond-scale companion to bench_max_us. Use when the budget is naturally expressed in ms (e.g. “rendering a frame in under 16ms”) so test authors don’t have to hand-multiply by 1000. Internally just delegates: 1 ms = 1000 us.

pub function assert_elapsed_lt_ms(start_ns: i64, max_ms: i64): Option[string]

assert_elapsed_lt_ms(start_ns, max_ms) — passes if the elapsed time since start_ns (a monotonic_ns() stamp) is below max_ms. The failure message embeds both the observed elapsed and the deadline so a flaky-bench failure logs enough context to decide whether to bump the bound.

pub function assert_elapsed_lt_us(start_ns: i64, max_us: i64): Option[string]

assert_elapsed_lt_us(start_ns, max_us) — finer-grained for sub-millisecond budgets. Same shape as the _ms variant; the failure message reports microseconds.

pub function assert_close_to_now_ms(actual_ms: i64, max_skew_ms: i64): Option[string]

assert_close_to_now_ms(actual_ms, max_skew_ms) — compare a wall-clock timestamp against now_unix_ms() with a tolerance. Used for tests that produce a “this happened around now” timestamp (e.g., a log entry, a session cookie’s issued-at) and want to verify the value is recent without pinning it exactly. The skew is bidirectional — anything from now - max_skew_ms to now + max_skew_ms passes. Failure message names the observed skew (signed; negative means the actual is in the future) and the bound.

pub function assert_matches_golden(path: string, actual: string): Option[string]

assert_matches_golden(path, actual) — if path exists, compare byte-exactly via the line-diff helper. If path doesn’t exist, write actual to it and pass with a TAP comment noting the bootstrap. Use this during development; run assert_matches_golden_strict in CI to catch missing- golden cases that would silently bootstrap themselves.

pub function assert_matches_golden_strict(path: string, actual: string): Option[string]

assert_matches_golden_strict(path, actual) — same compare behaviour but a missing golden file is a FAILURE rather than a bootstrap. Use in CI; reach for the auto-bootstrap variant only during development.

pub function assert_env_set(name: string): Option[string]

assert_env_set(name)env(name) returns Some(_). The value isn’t checked; use assert_env_eq when the exact content matters.

pub function assert_env_unset(name: string): Option[string]

assert_env_unset(name) — the inverse. Useful for tests that exercise the “use the default when env var missing” fallback path.

pub function assert_env_eq(name: string, expected: string): Option[string]

assert_env_eq(name, expected) — set AND equal to expected. Failure message distinguishes “var missing” from “var present but wrong value” so the diagnostic is unambiguous.

pub function assert_map_len[K, V](m: Map[K, V], n: i32): Option[string]

assert_map_len(m, n) — map length matches.

pub function assert_map_has[K, V](m: Map[K, V], k: K, v: V): Option[string]

assert_map_has(m, k, v) — key k is present with value v. Failure message distinguishes “key missing” from “key present with wrong value”.

pub function assert_map_lacks[K, V](m: Map[K, V], k: K): Option[string]

assert_map_lacks(m, k) — key k is NOT present. Used after a delete or in negative-result tests.

pub function assert_eq_map[K, V](actual: Map[K, V], expected: Map[K, V]): Option[string]

assert_eq_map(actual, expected) — full map deep equality. Same length AND every key in actual is present in expected with an equal value (by pigeonhole this implies the reverse direction too, so we only walk one side). Map iteration order isn’t observable, so the helper walks actual.keys() rather than iter — order-independent by construction. Values are compared with .eq and the per-key lookup goes through expected.get(k) / actual.get(k) so the body never needs a V-typed default literal. Use this when the test wants to pin the WHOLE map state; reach for _has / _lacks / _len when only individual entries matter.

pub function assert_all_i32(arr: i32[], pred: (i32) => boolean): Option[string]

assert_all_i32(arr, pred) — every element returns true. Vacuously holds for an empty array (matches the mathematical convention ∀ x ∈ ∅ : P(x)).

pub function assert_all_string(arr: string[], pred: (string) => boolean): Option[string]

assert_all_string(arr, pred) — string-typed mirror.

pub function assert_any_i32(arr: i32[], pred: (i32) => boolean): Option[string]

assert_any_i32(arr, pred) — at least one element returns true. Vacuously FAILS on an empty array (∃ x ∈ ∅ : P(x) is always false — same convention as classical logic). Failure message names the array length so an empty-array failure is obvious.

pub function assert_any_string(arr: string[], pred: (string) => boolean): Option[string]

assert_any_string(arr, pred) — string-typed mirror.

pub function assert_count_i32(arr: i32[], pred: (i32) => boolean, expected_count: i32): Option[string]

assert_count_i32(arr, pred, expected_count) — exactly expected_count elements of arr satisfy pred. Sits between assert_all (every element) and assert_any (at least one) — pin the exact cardinality. Failure message embeds the observed count so the diff is obvious.

pub function assert_count_string(arr: string[], pred: (string) => boolean, expected_count: i32): Option[string]

assert_count_string(arr, pred, expected_count) — string- typed mirror.

pub function assert_one_of_i32(actual: i32, allowed: i32[]): Option[string]

assert_one_of_i32(actual, allowed)actual ∈ allowed (linear scan on the allowed list — fine for the typical 2–10 element allowed sets these helpers target). Failure message embeds both the actual value AND the full allowed list so the diagnostic carries enough context to fix the call site without re-reading the test source.

pub function assert_one_of_string(actual: string, allowed: string[]): Option[string]

assert_one_of_string(actual, allowed) — string-typed mirror.

pub function assert_none_of_i32(actual: i32, forbidden: i32[]): Option[string]

assert_none_of_i32(actual, forbidden)actual ∉ forbidden. Use for negative-list checks: “the returned exit code is not any of these failure sentinels”. Failure message names the forbidden value the actual matched against, so a refactor that accidentally returns the bad value points at the right case immediately.

pub function assert_none_of_string(actual: string, forbidden: string[]): Option[string]

assert_none_of_string(actual, forbidden) — string-typed mirror.

pub function assert_is_some_i32(opt: Option[i32]): Option[string]

assert_is_some_i32(opt) — Option must be Some(_), payload value is irrelevant (use _eq variant when the value also matters).

pub function assert_is_some_string(opt: Option[string]): Option[string]

assert_is_some_string(opt) — string-typed mirror.

pub function assert_is_none_i32(opt: Option[i32]): Option[string]

assert_is_none_i32(opt) — Option must be None. Failure message embeds the unexpected payload so the regression case has its bad value in the log.

pub function assert_is_none_string(opt: Option[string]): Option[string]

assert_is_none_string(opt) — string-typed mirror.

pub function assert_is_some_eq_i32(opt: Option[i32], expected: i32): Option[string]

assert_is_some_eq_i32(opt, expected) — Option must be Some(expected). The single most common shape: a function returns Option[i32] indicating “found” / “not found”, and the test wants to pin both the found-ness AND the value. Distinguishes None failures from value mismatches in the failure message.

pub function assert_is_some_eq_string(opt: Option[string], expected: string): Option[string]

assert_is_some_eq_string(opt, expected) — string-typed mirror. Quotes both sides in the failure message.

pub function assert_is_ok_string(res: Result[string, IoError]): Option[string]

assert_is_ok_string(res) — Result must be Ok(_), payload string is irrelevant.

pub function assert_is_err_string(res: Result[string, IoError]): Option[string]

assert_is_err_string(res) — Result must be Err(_). Used after a negative-path operation (read a missing file, parse invalid input) to confirm the failure surfaced. Payload of the Err is irrelevant — pair with a downstream substring check on the formatted error if the test cares about the error message text.

pub function assert_is_ok_eq_string(res: Result[string, IoError], expected: string): Option[string]

assert_is_ok_eq_string(res, expected) — Result must be Ok(expected). The single most common shape: a function returns Result[string, IoError] and the test wants to pin both the success AND the value. Failure messages distinguish “got Err when Ok expected” from “got Ok with wrong value” so the regression mode is readable.

pub function assert_is_ok_string_array(res: Result[string[], IoError]): Option[string]

assert_is_ok_string_array(res) — string-array variant (e.g., read_dir returns Result[string[], IoError]).

pub function assert_is_err_string_array(res: Result[string[], IoError]): Option[string]

assert_is_err_string_array(res) — Err variant for the string-array shape.

pub function assert_array_intersects_i32(a: i32[], b: i32[]): Option[string]

assert_array_intersects_i32(a, b) — at least one element of a also appears in b. Empty array on either side always fails (intersection with empty is empty). Linear-time O(|a| * |b|) scan — fine for the typical short test arrays.

pub function assert_array_intersects_string(a: string[], b: string[]): Option[string]

assert_array_intersects_string(a, b) — string-typed mirror.

pub function assert_array_disjoint_i32(a: i32[], b: i32[]): Option[string]

assert_array_disjoint_i32(a, b) — NO element of a appears in b. The complementary test to _intersects; either array empty vacuously passes (the intersection of empty with anything is empty). Failure message names the first shared element.

pub function assert_array_disjoint_string(a: string[], b: string[]): Option[string]

assert_array_disjoint_string(a, b) — string-typed mirror.

pub function assert_array_starts_with_i32(arr: i32[], prefix: i32[]): Option[string]

assert_array_starts_with_i32(arr, prefix)arr[0..len(prefix)] == prefix (element-wise). Empty prefix vacuously passes (every array starts with the empty prefix). Failure either reports too-short OR names the first non-matching index.

pub function assert_array_starts_with_string(arr: string[], prefix: string[]): Option[string]

assert_array_starts_with_string(arr, prefix) — string- typed mirror.

pub function assert_array_ends_with_i32(arr: i32[], suffix: i32[]): Option[string]

assert_array_ends_with_i32(arr, suffix)arr[len(arr)-len(suffix)..] == suffix. Empty suffix vacuously passes. Failure reports too-short or names the first mismatching index (offset into arr).

pub function assert_array_ends_with_string(arr: string[], suffix: string[]): Option[string]

assert_array_ends_with_string(arr, suffix) — string- typed mirror.

pub function assert_array_contains_subseq_i32(arr: i32[], needle: i32[]): Option[string]

assert_array_contains_subseq_i32(arr, needle)needle appears anywhere in arr as a contiguous sub-array (order matters). Empty needle vacuously passes. Failure message just notes the absence (the offending side is the needle, already given to the helper — embedding arr would bloat the diagnostic without adding signal).

pub function assert_array_contains_subseq_string(arr: string[], needle: string[]): Option[string]

assert_array_contains_subseq_string(arr, needle) — string-typed mirror.

pub function assert_all_starts_with(arr: string[], prefix: string): Option[string]

assert_all_starts_with(arr, prefix) — every element of arr has prefix at byte 0. Empty array vacuously passes (∀ over ∅). Empty prefix vacuously passes for every non-null string.

pub function assert_all_ends_with(arr: string[], suffix: string): Option[string]

assert_all_ends_with(arr, suffix) — every element ends with suffix. Same shape as the _starts_with mirror.

pub function assert_all_contain(arr: string[], needle: string): Option[string]

assert_all_contain(arr, needle) — every element contains needle as a substring. Use when the contract is “every error message mentions the operation name” or “every emitted log line has the trace id”.

pub function assert_starts_with_any(s: string, prefixes: string[]): Option[string]

assert_starts_with_any(s, prefixes)s starts with at least one prefix in prefixes. Empty prefixes list always fails (nothing to match).

pub function assert_ends_with_any(s: string, suffixes: string[]): Option[string]

assert_ends_with_any(s, suffixes) — same pattern for the suffix family. Common shape: “the file extension is one of [.fern, .ll, .s]”.

pub function assert_json_eq(actual: string, expected: string): Option[string]

assert_json_eq(actual, expected) — both inputs are JSON text. Parses each via std/json’s json_parse, walks the resulting trees in order-independent fashion, and reports a failure with both raw documents quoted when they differ. Invalid JSON on either side surfaces as a failure with the offender named so the diagnostic is unambiguous.

Uses the qualified json.json_parse(...) form rather than bare json_parse(...) so modload’s rewriter routes the call correctly under both the auto-prelude flat-load path AND the user-side mangled-load path (where the user’s own import "std/json"; would have already loaded json’s decls under the json__ prefix; bare-name lookup would miss them).

pub function assert_json_has_key(json_text: string, key: string): Option[string]

assert_json_has_key(json_text, key) — top-level JObject contains key. Fails if json_text doesn’t parse OR the top-level value isn’t a JObject OR the key is absent. Distinct diagnostics for each so the regression case names the actual failure mode.

pub function assert_json_lacks_key(json_text: string, key: string): Option[string]

assert_json_lacks_key(json_text, key) — top-level JObject does NOT contain key. Use after a delete / filter operation, or to assert a sensitive field (e.g., “password_hash”) was stripped from a response.

pub function assert_json_array_len(json_text: string, n: i32): Option[string]

assert_json_array_len(json_text, n) — top-level JArray has exactly n elements. Use when the test cares about cardinality (e.g., “the endpoint returned 5 records”) without pinning each element.

pub function assert_json_object_size(json_text: string, n: i32): Option[string]

assert_json_object_size(json_text, n) — top-level JObject has exactly n keys. Cardinality complement to _array_len.

pub function assert_json_eq_field_string(json_text: string, key: string, expected: string): Option[string]

assert_json_eq_field_string(json_text, key, expected) — top-level object’s key is a JString equal to expected. Failure cases (each distinct in the diagnostic): invalid JSON, top-level not object, missing key, key not a string, value mismatch.

pub function assert_json_eq_field_i32(json_text: string, key: string, expected: i32): Option[string]

assert_json_eq_field_i32(json_text, key, expected) — top-level object’s key is a JNumber parseable to i32 and equal to expected. JNumber stores numbers verbatim as strings; we delegate to parse_int here so “non-numeric” JNumbers (the parser accepts decimals + exponents) surface as a parse failure rather than a silent miscompare.

pub function assert_json_eq_field_bool(json_text: string, key: string, expected: boolean): Option[string]

assert_json_eq_field_bool(json_text, key, expected) — top-level object’s key is a JBool equal to expected.