std/test
std/test
Section titled “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.
struct TestRunner
Section titled “struct TestRunner”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
”
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 ”(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.
test_new
Section titled “test_new”pub function test_new(suite: string): TestRunnertest_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.
test_new_filtered
Section titled “test_new_filtered”pub function test_new_filtered(suite: string, filter: string): TestRunnertest_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.
parse_filter_from_args
Section titled “parse_filter_from_args”pub function parse_filter_from_args(argv: string[]): stringparse_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.
test_new_fail_fast
Section titled “test_new_fail_fast”pub function test_new_fail_fast(suite: string): TestRunnertest_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(); }
with_fail_fast
Section titled “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.
parse_fail_fast_from_args
Section titled “parse_fail_fast_from_args”pub function parse_fail_fast_from_args(argv: string[]): booleanparse_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(); }
test_new_quiet
Section titled “test_new_quiet”pub function test_new_quiet(suite: string): TestRunnertest_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(); }
with_quiet
Section titled “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.
parse_quiet_from_args
Section titled “parse_quiet_from_args”pub function parse_quiet_from_args(argv: string[]): booleanparse_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(); }
test_new_verbose
Section titled “test_new_verbose”pub function test_new_verbose(suite: string): TestRunnertest_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()); }
skip_if
Section titled “skip_if”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.
subsuite
Section titled “subsuite”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.
defer_cleanup
Section titled “defer_cleanup”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.
log_kv_string
Section titled “log_kv_string”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);
session_id=“abc-123”
Section titled “session_id=“abc-123””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.
log_kv_i32
Section titled “log_kv_i32”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.
log_kv_i64
Section titled “log_kv_i64”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.
finish
Section titled “finish”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”).
unreachable
Section titled “unreachable”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.
assert_true
Section titled “assert_true”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.
assert_false
Section titled “assert_false”pub function assert_false(cond: boolean): Option[string]assert_eq
Section titled “assert_eq”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.
assert_neq
Section titled “assert_neq”pub function assert_neq[T](actual: T, expected: T): Option[string]assert_lt
Section titled “assert_lt”pub function assert_lt[T](a: T, b: T): Option[string]assert_le
Section titled “assert_le”pub function assert_le[T](a: T, b: T): Option[string]assert_gt
Section titled “assert_gt”pub function assert_gt[T](a: T, b: T): Option[string]assert_ge
Section titled “assert_ge”pub function assert_ge[T](a: T, b: T): Option[string]assert_eq_f64_near
Section titled “assert_eq_f64_near”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.
assert_eq_f32_near
Section titled “assert_eq_f32_near”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.
assert_eq_f64_rel
Section titled “assert_eq_f64_rel”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).
assert_eq_f32_rel
Section titled “assert_eq_f32_rel”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.
assert_eq_f64_exact
Section titled “assert_eq_f64_exact”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.
assert_is_nan_f64
Section titled “assert_is_nan_f64”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.
assert_is_nan_f32
Section titled “assert_is_nan_f32”pub function assert_is_nan_f32(v: f32): Option[string]assert_contains
Section titled “assert_contains”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.
assert_not_contains
Section titled “assert_not_contains”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.
assert_starts_with
Section titled “assert_starts_with”pub function assert_starts_with(s: string, prefix: string): Option[string]assert_ends_with
Section titled “assert_ends_with”pub function assert_ends_with(s: string, suffix: string): Option[string]assert_eq_string_ci
Section titled “assert_eq_string_ci”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.
assert_neq_string_ci
Section titled “assert_neq_string_ci”pub function assert_neq_string_ci(actual: string, expected: string): Option[string]assert_neq_string_ci(actual, expected) — must differ
even after case-folding.
assert_contains_ci
Section titled “assert_contains_ci”pub function assert_contains_ci(haystack: string, needle: string): Option[string]assert_contains_ci(haystack, needle) — case-
insensitive substring search.
assert_starts_with_ci
Section titled “assert_starts_with_ci”pub function assert_starts_with_ci(s: string, prefix: string): Option[string]assert_starts_with_ci(s, prefix) — case-insensitive
prefix check.
assert_ends_with_ci
Section titled “assert_ends_with_ci”pub function assert_ends_with_ci(s: string, suffix: string): Option[string]assert_ends_with_ci(s, suffix) — case-insensitive
suffix check.
assert_empty_string
Section titled “assert_empty_string”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.
assert_non_empty_string
Section titled “assert_non_empty_string”pub function assert_non_empty_string(s: string): Option[string]assert_string_count
Section titled “assert_string_count”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.
assert_sorted_asc
Section titled “assert_sorted_asc”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.
assert_sorted_desc
Section titled “assert_sorted_desc”pub function assert_sorted_desc[T](arr: T[]): Option[string]assert_strictly_sorted_asc
Section titled “assert_strictly_sorted_asc”pub function assert_strictly_sorted_asc[T](arr: T[]): Option[string]assert_unique
Section titled “assert_unique”pub function assert_unique[T](arr: T[]): Option[string]assert_subset
Section titled “assert_subset”pub function assert_subset[T](superset: T[], subset: T[]): Option[string]assert_set_eq
Section titled “assert_set_eq”pub function assert_set_eq[T](actual: T[], expected: T[]): Option[string]assert_len_i32
Section titled “assert_len_i32”pub function assert_len_i32(arr: i32[], n: i32): Option[string]assert_len_string
Section titled “assert_len_string”pub function assert_len_string(arr: string[], n: i32): Option[string]assert_eq_array
Section titled “assert_eq_array”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.
assert_at
Section titled “assert_at”pub function assert_at[T](arr: T[], idx: i32, expected: T): Option[string]assert_array_contains
Section titled “assert_array_contains”pub function assert_array_contains[T](arr: T[], needle: T): Option[string]assert_array_not_contains
Section titled “assert_array_not_contains”pub function assert_array_not_contains[T](arr: T[], needle: T): Option[string]assert_at_f64
Section titled “assert_at_f64”pub function assert_at_f64(arr: f64[], idx: i32, expected: f64, epsilon: f64): Option[string]assert_at_f32
Section titled “assert_at_f32”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).
assert_eq_f64_array_near
Section titled “assert_eq_f64_array_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.
assert_eq_f32_array_near
Section titled “assert_eq_f32_array_near”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.
assert_exit
Section titled “assert_exit”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.
assert_stdout_eq
Section titled “assert_stdout_eq”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.
assert_stderr_eq
Section titled “assert_stderr_eq”pub function assert_stderr_eq(actual: ProcessResult, expected: string): Option[string]assert_stdout_contains
Section titled “assert_stdout_contains”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).
assert_stderr_contains
Section titled “assert_stderr_contains”pub function assert_stderr_contains(actual: ProcessResult, needle: string): Option[string]assert_process
Section titled “assert_process”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.
assert_exit_zero
Section titled “assert_exit_zero”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).
assert_exit_nonzero
Section titled “assert_exit_nonzero”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.
assert_stdout_lines
Section titled “assert_stdout_lines”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.
assert_stderr_lines
Section titled “assert_stderr_lines”pub function assert_stderr_lines(actual: ProcessResult, expected_lines: string[]): Option[string]assert_stderr_lines(actual, expected_lines) — stderr
mirror.
assert_stdout_line_count
Section titled “assert_stdout_line_count”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.
assert_stderr_line_count
Section titled “assert_stderr_line_count”pub function assert_stderr_line_count(actual: ProcessResult, n: i32): Option[string]assert_stderr_line_count(actual, n) — stderr mirror.
assert_contains_all
Section titled “assert_contains_all”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.
assert_contains_any
Section titled “assert_contains_any”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).
assert_contains_in_order
Section titled “assert_contains_in_order”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.
assert_eq_string_diff
Section titled “assert_eq_string_diff”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.
assert_lines_eq
Section titled “assert_lines_eq”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.
assert_in_range_i32
Section titled “assert_in_range_i32”pub function assert_in_range_i32(v: i32, lo: i32, hi: i32): Option[string]assert_in_range_i32(v, lo, hi) — lo <= v <= hi.
assert_in_range_i64
Section titled “assert_in_range_i64”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.
assert_in_range_f64
Section titled “assert_in_range_f64”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).
assert_in_range_f32
Section titled “assert_in_range_f32”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.
assert_file_exists
Section titled “assert_file_exists”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.
assert_file_not_exists
Section titled “assert_file_not_exists”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.
assert_file_contents
Section titled “assert_file_contents”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.
assert_file_lines
Section titled “assert_file_lines”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.
assert_file_line_count
Section titled “assert_file_line_count”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.
assert_is_file
Section titled “assert_is_file”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.
assert_is_dir
Section titled “assert_is_dir”pub function assert_is_dir(path: string): Option[string]assert_file_size
Section titled “assert_file_size”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.
assert_eq_dir_listing
Section titled “assert_eq_dir_listing”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”.
assert_file_contains
Section titled “assert_file_contains”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.
must_temp_dir
Section titled “must_temp_dir”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.
bench_max_us
Section titled “bench_max_us”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.
bench_max_ms
Section titled “bench_max_ms”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.
assert_elapsed_lt_ms
Section titled “assert_elapsed_lt_ms”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.
assert_elapsed_lt_us
Section titled “assert_elapsed_lt_us”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.
assert_close_to_now_ms
Section titled “assert_close_to_now_ms”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.
assert_matches_golden
Section titled “assert_matches_golden”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.
assert_matches_golden_strict
Section titled “assert_matches_golden_strict”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.
assert_env_set
Section titled “assert_env_set”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.
assert_env_unset
Section titled “assert_env_unset”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.
assert_env_eq
Section titled “assert_env_eq”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.
assert_map_len
Section titled “assert_map_len”pub function assert_map_len[K, V](m: Map[K, V], n: i32): Option[string]assert_map_len(m, n) — map length matches.
assert_map_has
Section titled “assert_map_has”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”.
assert_map_lacks
Section titled “assert_map_lacks”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.
assert_eq_map
Section titled “assert_eq_map”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.
assert_all_i32
Section titled “assert_all_i32”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)).
assert_all_string
Section titled “assert_all_string”pub function assert_all_string(arr: string[], pred: (string) => boolean): Option[string]assert_all_string(arr, pred) — string-typed mirror.
assert_any_i32
Section titled “assert_any_i32”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.
assert_any_string
Section titled “assert_any_string”pub function assert_any_string(arr: string[], pred: (string) => boolean): Option[string]assert_any_string(arr, pred) — string-typed mirror.
assert_count_i32
Section titled “assert_count_i32”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.
assert_count_string
Section titled “assert_count_string”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.
assert_one_of_i32
Section titled “assert_one_of_i32”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.
assert_one_of_string
Section titled “assert_one_of_string”pub function assert_one_of_string(actual: string, allowed: string[]): Option[string]assert_one_of_string(actual, allowed) — string-typed
mirror.
assert_none_of_i32
Section titled “assert_none_of_i32”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.
assert_none_of_string
Section titled “assert_none_of_string”pub function assert_none_of_string(actual: string, forbidden: string[]): Option[string]assert_none_of_string(actual, forbidden) — string-typed
mirror.
assert_is_some_i32
Section titled “assert_is_some_i32”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).
assert_is_some_string
Section titled “assert_is_some_string”pub function assert_is_some_string(opt: Option[string]): Option[string]assert_is_some_string(opt) — string-typed mirror.
assert_is_none_i32
Section titled “assert_is_none_i32”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.
assert_is_none_string
Section titled “assert_is_none_string”pub function assert_is_none_string(opt: Option[string]): Option[string]assert_is_none_string(opt) — string-typed mirror.
assert_is_some_eq_i32
Section titled “assert_is_some_eq_i32”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.
assert_is_some_eq_string
Section titled “assert_is_some_eq_string”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.
assert_is_ok_string
Section titled “assert_is_ok_string”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.
assert_is_err_string
Section titled “assert_is_err_string”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.
assert_is_ok_eq_string
Section titled “assert_is_ok_eq_string”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.
assert_is_ok_string_array
Section titled “assert_is_ok_string_array”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]).
assert_is_err_string_array
Section titled “assert_is_err_string_array”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.
assert_array_intersects_i32
Section titled “assert_array_intersects_i32”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.
assert_array_intersects_string
Section titled “assert_array_intersects_string”pub function assert_array_intersects_string(a: string[], b: string[]): Option[string]assert_array_intersects_string(a, b) — string-typed
mirror.
assert_array_disjoint_i32
Section titled “assert_array_disjoint_i32”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.
assert_array_disjoint_string
Section titled “assert_array_disjoint_string”pub function assert_array_disjoint_string(a: string[], b: string[]): Option[string]assert_array_disjoint_string(a, b) — string-typed
mirror.
assert_array_starts_with_i32
Section titled “assert_array_starts_with_i32”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.
assert_array_starts_with_string
Section titled “assert_array_starts_with_string”pub function assert_array_starts_with_string(arr: string[], prefix: string[]): Option[string]assert_array_starts_with_string(arr, prefix) — string-
typed mirror.
assert_array_ends_with_i32
Section titled “assert_array_ends_with_i32”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).
assert_array_ends_with_string
Section titled “assert_array_ends_with_string”pub function assert_array_ends_with_string(arr: string[], suffix: string[]): Option[string]assert_array_ends_with_string(arr, suffix) — string-
typed mirror.
assert_array_contains_subseq_i32
Section titled “assert_array_contains_subseq_i32”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).
assert_array_contains_subseq_string
Section titled “assert_array_contains_subseq_string”pub function assert_array_contains_subseq_string(arr: string[], needle: string[]): Option[string]assert_array_contains_subseq_string(arr, needle) —
string-typed mirror.
assert_all_starts_with
Section titled “assert_all_starts_with”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.
assert_all_ends_with
Section titled “assert_all_ends_with”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.
assert_all_contain
Section titled “assert_all_contain”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”.
assert_starts_with_any
Section titled “assert_starts_with_any”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).
assert_ends_with_any
Section titled “assert_ends_with_any”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]”.
assert_json_eq
Section titled “assert_json_eq”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).
assert_json_has_key
Section titled “assert_json_has_key”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.
assert_json_lacks_key
Section titled “assert_json_lacks_key”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.
assert_json_array_len
Section titled “assert_json_array_len”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.
assert_json_object_size
Section titled “assert_json_object_size”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.
assert_json_eq_field_string
Section titled “assert_json_eq_field_string”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.
assert_json_eq_field_i32
Section titled “assert_json_eq_field_i32”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.
assert_json_eq_field_bool
Section titled “assert_json_eq_field_bool”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.