#test Region

Purpose

The #test region contains test cases for the module. These are compiled and run via tape test <file> — they’re not included in normal builds.

tape
#test
test "addition works" {
    @assert(1 + 1 == 2);
}

test "string concatenation" {
    let result = "hello" + " " + "world";
    @assert(result == "hello world");
}

Test syntax

Each test is declared with test "name" { body }. The name becomes part of the generated function name (spaces and hyphens become underscores).

tape
test "descriptive name" {
    // test body — regular tape code
    @assert(some_condition);
}

Tests return 0 on success. If any assertion fails, the test returns -1. If skipped, it returns -2.

Assertions

AssertionPurposeDesugars to
@assert(cond)Fails if condition is falseif (!cond) { report; return -1; }
@assert_err(expr)Expects expression to be non-zero (error)if (expr != 0) { report; return -1; }
@assert_trap(expr)Expects expression to cause a traptrap_begin; expr; if (!trap_end) fail
@fail(msg)Unconditional failure with messagereport(msg); return -1;
@skip(cond)Skip test if condition is trueif (cond) { return -2; }

@assert with comparison operators (==, !=, <, >, <=, >=) reports both left and right values on failure via __test_assert_fail_cmp. Other conditions use __test_assert_fail with the expression text.

Setup and teardown

tape
#test
setup {
    // runs before each test
}

teardown {
    // runs after each test
}

test "example" {
    @assert(1 == 1);
}

setup and teardown are optional. They run before/after every test function in the file.

Table-driven tests

Generate multiple test cases from data rows:

tape
test "squares" with [1, 1], [2, 4], [3, 9], [4, 16] as (input, expected) {
    @assert(input * input == expected);
}

Each row [values...] becomes a separate test function (__test_squares_0, __test_squares_1, etc.). The as (names...) clause binds row values to local variables in the body.

Helper functions

Regular function declarations are allowed inside #test:

tape
#test
fn make_test_data() -> i64 {
    return 42;
}

test "uses helper" {
    let x = make_test_data();
    @assert(x == 42);
}

Module substitution

For dependency injection in tests, use --substitute:

bash
tape test app.tape --substitute io=./mock_io.tape

This replaces the io module with a mock implementation during test compilation.

PlannedImport statements inside #test are parsed but skipped. The design allows #test to import modules directly — either for test-only utilities, or to override a module that #code imports (acting as inline substitution without the --substitute flag).

Running tests

bash
tape test module.tape                          # run all tests in file
tape test module.tape --substitute M=path      # with module substitution

Output

Tests print results as they run:

plaintext
  test "addition_works" ... ok
  test "string_concatenation" ... FAILED
  test "skipped_example" ... skipped

results: 1 passed, 1 failed, 1 skipped, 3 total

Exit code is 0 if all pass, 1 if any fail.

What #test supports

FeatureStatus
test "name" { ... }Implemented
setup { ... } / teardown { ... }Implemented
@assert, @fail, @skipImplemented
@assert_err, @assert_trapImplemented
Table-driven tests (with [...] as (...))Implemented
Helper fn declarationsImplemented
--substitute for mockingImplemented
--filter for test name matchingNot implemented
Test isolation (state reset between tests)Not implemented

Last modified: