Wado

Testing

Tests are first-class syntax: a test block declares one, and wado test finds and runs them. The runner's flags are described by wado test --help. A test checks its results with assert.

Test Declaration Syntax

Tests are declared using the test keyword followed by an optional name and a block:

// Named test
test "addition works" {
    assert 1 + 1 == 2;
}

// Unnamed test
test {
    let result = compute_something();
    assert result > 0;
}

// Test with multiple assertions
test "string operations" {
    let s = "hello";
    assert s.len() == 5;
    assert s + " world" == "hello world";
}

// Expect-trap test: the test passes when the body traps
#[expect_trap]
test "panics on bad input" {
    panic("intentional panic");
}

#[expect_trap]
test {
    unreachable();
}

// Custom timeout: override the default 5000ms limit
#[timeout_ms(30000)]
test "slow computation" {
    let result = expensive_computation();
    assert result == 42;
}

// Synopsis test: runs like any test; `wado doc` renders its body as the
// module's `## Synopsis` section.
#[synopsis]
test {
    let p = Point { x: 3, y: 4 };
    assert p.length() == 5.0;
}

Syntax Rules

Test Semantics

Execution

#[expect_trap] Attribute

The #[expect_trap] attribute inverts a test's pass/fail condition: the test passes only if its body traps, by a panic, an unreachable, a failed assert or any other trap (Regular Tests):

#[expect_trap]
test "panics on null dereference" {
    let opt: Option<i32> = null;
    opt.unwrap();
}

#[TODO] Attribute

The #[TODO] attribute marks a test for a feature not yet implemented. Its outcome is reported on a separate axis from pass and fail (TODO Tests):

#[TODO]
test "not yet implemented feature" {
    panic("TODO: this feature is not yet implemented");
}

#[timeout_ms(N)] Attribute

The #[timeout_ms(N)] attribute overrides the default test timeout of 5000ms for one test. N is an integer literal, in milliseconds. A test that runs longer is interrupted and fails. Time spent inside core:eval's eval does not count against it:

#[timeout_ms(30000)]
test "large data processing" {
    let result = process_large_dataset();
    assert result.len() > 0;
}

Test Outcome Model

Test results are classified into two independent axes: the pass/fail axis for regular tests, and the TODO axis for tests marked with #[TODO].

Regular Tests

Condition Outcome
Body completes normally pass
Body traps (panic, assert failure, unreachable) fail
Body traps and #[expect_trap] is present pass
Body completes normally and #[expect_trap] is present fail

TODO Tests

Tests marked with #[TODO] are reported separately from regular tests. They do not contribute to the pass or fail count.

Condition Outcome Action Required
Body traps todo (pending) None — the feature is still unimplemented
Body completes normally todo (unexpectedly passed) Remove #[TODO] — the feature now works

A TODO test that unexpectedly passes fails the run. This enforces cleanup: once the underlying feature is implemented, the #[TODO] attribute must be removed so the test joins the regular pass/fail pool.

A pending TODO test never causes a failure. So fixing a compiler bug cannot raise the failure count: a TODO test the fix makes pass shows as unexpectedly passed on the TODO axis, not as a failure on the pass/fail axis.

#![TODO] Modules

The #![TODO] inner attribute applies TODO semantics to an entire module:

Run Result

A run reports a third axis beside the two above: compile (passed / failed), over every discovered file. It exits non-zero when any test fails, any TODO test unexpectedly passes, or any file fails to compile other than a #![TODO] module, which counts as a pending TODO instead.

Test Discovery

wado test with no path argument walks the current directory for *.wado files. It skips:

Symbolic links are followed, and a directory reached twice is walked once. A symbolic link that points nowhere is skipped. A #![generated] file is not skipped.

A file needs neither a test block nor a particular world. Every discovered file compiles under the test world, where an entry point written for another world still type-checks, and a file with no test block is compiled and not run.

[test] in wado.toml

[test]
exclude = ["tests/fixtures/**"]
include = ["lib/**/*_test.wado"]

exclude and include are lists of glob patterns, matched against a path relative to the package root. * and ? stop at a /, and ** spans directories. dir/** also matches dir itself, so the walk does not enter it.

include carves files back out of what exclude removes: a file both match is discovered. While include has any pattern, the walk still enters an excluded directory to look for one, but a wado.toml inside an excluded directory does not start a run of its own.

Path Arguments

A path argument replaces the walk of the current directory. The files the arguments name are reported as one run, whichever packages they belong to.

File Naming

*_test.wado is the recommended name for a file that contains only tests. The suffix is not required: any file with test blocks contributes its tests.

src/
  math.wado
  math_test.wado      # Tests for math.wado (recommended convention)
  string.wado
  string_test.wado    # Tests for string.wado

Tests can also be placed in a separate tests/ directory:

src/
  lib.wado
tests/
  integration_test.wado

Rationale: WEP: Test Discovery.

Coverage

wado test --coverage reports which code the tests ran, and changes no test outcome.

wado test --coverage                        # summary on stdout, build/coverage/lcov.info
wado test --coverage=lcov,json,baseline     # the files written to build/coverage/
wado test --coverage --coverage-include=deps,stdlib
wado test --coverage --coverage-baseline FILE

The unit is a region: a span of source that runs as a unit. A function, method or closure body is one, and so is each if branch (an else the source omits included), each match arm, a loop body, the else of let … else, the right operand of && and ||, the early return of ?, and the statements after one that can leave its block with return, break, continue or ?. A region counts as run once any test runs its start.

Three measures derive from regions:

A test block is not measured, nor is a global's initializer. What is measured is the package's own modules; --coverage-include=deps adds its dependencies', and --coverage-include=stdlib the core: modules, for the standard library's own tests. #[coverage(off)] leaves a function, an impl or a module unmeasured.

--coverage-baseline FILE fails the run unless the regions no test ran are exactly those FILE lists. --coverage=baseline writes such a file.

Rationale: WEP: Test Coverage.

Example Test File

// spec_testing_math_test.wado
use {add, multiply} from "./sub/spec_testing_math.wado";

test "add positive numbers" {
    assert add(2, 3) == 5;
    assert add(0, 0) == 0;
}

test "add negative numbers" {
    assert add(-1, -1) == -2;
    assert add(-5, 3) == -2;
}