Modules

Testing

moduleextensionSource

Available on macOS, Linux, Windows, Wasm·since 0.28.0

#import "Extensions/Testing";

A test framework. A test is a procedure with a @test note, found at compile time and run by jacket test. The expect* checks report a failure and let the test go on; the require* ones report it and return from the test. assert_that checks a value against a matcher, @cases and @property run a test over a list or over generated inputs, and expect_snapshot compares output with a stored file.

Example
#import "Extensions/Testing";

parses_numbers :: (t: *Test) {
    value, ok := string_to_int("42");
    require(ok);
    expect_equal(value, 42);
} @test

This one is only checked to compile, not run.

A test runs with a context allocator that fails it on a leak or a bad free, and with temporary storage reset first.

Procedures#

General#

logsince 0.28.0Source
(t: *Test, format: string, args: ..Any)

Prints a line to the test's output, which jacket test shows when the test fails.

reports_progress :: (t: *Test) {
    log(t, "step % of %", 1, 3);
}
↗

This one is only checked to compile, not run.

skipsince 0.28.0Source
(t: *Test, reason := "")

Ends the test as skipped, from the procedure it is written in.

needs_a_gpu :: (t: *Test) {
    skip(t, "no GPU on this machine");
} @test
↗

This one is only checked to compile, not run.

failsince 0.28.0Source
(t: *Test, message := "failed", loc := #caller_location)

Marks the test as failed and lets it go on.

fail(t, "this never worked");

This one is meant to fail, so there is nothing to run.

temp_dirsince 0.28.0Source
(t: *Test, loc := #caller_location) -> string

A directory for this test's files. jacket test creates it and deletes it after a pass (--keep-temp keeps it); subtests share their test's directory.

#import "File";

writes_a_file :: (t: *Test) {
    path := tprint("%/out.txt", temp_dir(t));
    expect(write_entire_file(path, "hi"));
} @test

This one is only checked to compile, not run.

cleanupsince 0.28.0Source
(t: *Test, callback: (data: *void), data: *void = null)

Calls callback(data) when the test ends, after its body; the last registered runs first.

uses_a_buffer :: (t: *Test) {
    buffer := alloc(1024);
    cleanup(t, (data: *void) { free(data); }, buffer);
} @test
↗

This one is only checked to compile, not run.

run2 overloadssince 0.28.0Source
(t: *Test, name: string, body: (t: *Test)) -> bool
(t: *Test, name: string, body: (t: *Test, data: *$T), data: *T) -> bool

Runs body as the subtest name, reported and filtered as test/name. Returns whether it passed.

passed := run(t, "addition", (t: *Test) {
    expect_equal(1 + 1, 2);
});
assert(passed);

Checks#

expectsince 0.28.0Source
(condition: bool, message := "", loc := #caller_location) -> bool

Checks condition, reports a failure at the caller's line and returns whether it held. The other expect* checks work the same way; jacket test shows the line's source next to the message.

expect(1 + 1 == 2);
expect(1 + 1 == 2, "the arithmetic is off"); // the message replaces the default one
expect_equalsince 0.28.0Source
(got: $T, want: $U, message := "", loc := #caller_location) -> bool

got and want are compared by value: numbers of any width, strings, enums, arrays element by element (fixed, view and resizable alike) and structs member by member. Strings and arrays that differ get a diff.

expect_equal(2 + 2, 4);
expect_equal("abc", "abc");
expect_equal(s32.[1, 2, 3], s32.[1, 2, 3]);
expect_equal(2 + 2, 4, "cut point 3"); // on failure, the message leads the difference
expect_nearsince 0.28.0Source
(got: float64, want: float64, tolerance: float64, loc := #caller_location) -> bool

|got - want| <= tolerance. NaN is never near anything.

expect_near(0.5, 0.25, 0.1);

This one is meant to fail, so there is nothing to run.

expect_nullsince 0.28.0Source
(pointer: *$T, loc := #caller_location) -> bool

Checks that pointer is null.

p: *int;
expect_null(p);
requiresince 0.28.0Source
(condition: bool, message := "", loc := #caller_location)

The require forms check the same way and, when the check fails, return from the procedure they are written in (which must return nothing). Use them when the rest of the test needs the check to hold. In a helper that returns a value, use the expect form instead.

items: [..] int;
array_add(*items, 1);
require(items.count == 1);
expect_equal(items[0], 1);
(got: $T, want: $U, loc := #caller_location)

expect_equal, then returns from the calling procedure on a failure.

require_equal(1 + 1, 2);
↗
require_nearsince 0.28.0Source
(got: float64, want: float64, tolerance: float64, loc := #caller_location)

expect_near, then returns from the calling procedure on a failure.

require_near(0.1 + 0.2, 0.3, 0.0001);
↗

Cases#

run_casessince 0.30.0Source
(t: *Test, cases: [] $T, $body: $B) -> bool

Runs body once per entry of cases, each as the subtest test/<values>; returns whether every case passed. An entry is the argument of body, or, when body takes several, a struct whose members are its arguments in order. body may take a leading *Test. This is what a @test procedure with a @cases(LIST) note runs; call it by hand to build the list at run time or to run a helper per case:

Case :: struct { input: string; length: s64; }

checks_each_case :: (t: *Test) {
    lengths :: (input: string, length: s64) {
        expect_equal(input.count, length);
    }

    passed := run_cases(t, Case.[.{ "", 0 }, .{ "abc", 3 }], lengths);
    expect(passed);
}
all_valuessince 0.30.0Source
($T: Type) -> [] T

Every member of the enum T, in declaration order, as a list for @cases:

Color :: enum { RED; GREEN; BLUE; }

values := all_values(Color);

assert(values.count == 3);
assert(values[0] == .RED && values[2] == .BLUE);
↗
case_namessince 0.30.0Source
(cases: [] $T, body: $B) -> [] string

The names run_cases gives the subtests of cases: the entry's values, without /, shortened, repeats numbered. In temporary storage.

Matchers#

assert_thatsince 0.30.0Source
(value: $T, matcher: Matcher, loc := #caller_location) -> bool

Checks value against matcher; a failure is reported at the caller's line. Returns whether it held.

assert_that(4, greater_than(5));

This one is meant to fail, so there is nothing to run.

require_thatsince 0.30.0Source
(value: $T, matcher: Matcher, loc := #caller_location)

assert_that, then returns from the procedure it is written in (which must return nothing) when the check fails.

require_that("hello", has_length(5));
↗
issince 0.30.0Source
(want: $T) -> Matcher

Equal by value, as expect_equal compares; strings, arrays and structs fail with its diff.

assert_that("abc", is("abc"));
↗
nearsince 0.30.0Source
(want: float64, tolerance: float64) -> Matcher

|value - want| <= tolerance. NaN is never near anything.

assert_that(0.1 + 0.2, near(0.3, 0.0001));
↗

Snapshots#

expect_snapshot2 overloadssince 0.30.0Source
(t: *Test, value: Any, name := "", loc := #caller_location) -> bool
(t: *Test, text: string, name := "", loc := #caller_location) -> bool

Compares the printed form of value with its stored snapshot. Strings are compared as they are; any other value is printed with one member or element to a line, so a change shows as a line diff. A test that takes several snapshots numbers them, or names them with name.

Point :: struct { x, y: s32; }

prints_a_point :: (t: *Test) {
    expect_snapshot(t, Point.{ 1, 2 });
    expect_snapshot(t, "a block of text\nwith two lines", "text");
} @test

This one is only checked to compile, not run.

Property#

rng_rangesince 0.30.0Source
(rng: *Rng, low: s64, high: s64) -> s64

A number from low to high, both included.

rng := rng_seeded(1);
n := rng_range(*rng, -5, 5);
assert(n >= -5 && n <= 5);
↗
assumesince 0.30.0Source
(condition: bool)

Checks the arguments of a property: skips the trial, without failing it, when condition does not hold. A property that returns a value writes if !condition return true;.

run_property(t, (divisor: s64) {
    assume(divisor != 0);
    expect_equal(10 / divisor * divisor + 10 % divisor, 10);
});
run_propertysince 0.30.0Source
(t: *Test, $body: $B, count: s64 = DEFAULT_TRIALS, generators: [] Custom_Generator = .[])

What a @property test runs: body on count generated inputs. Call it by hand to give the arguments custom generators or another count.

run_property(t, (a: s32, b: s32) {
    expect_equal(cast(s64) a + b, cast(s64) b + a);
}, 50);

Compile time#

Runner#

run_examplesince 0.28.0Source
(body: (t: *Test))

Runs body as a test that prints nothing of its own: a failed check prints its message, and the program exits with status 1. jacket test runs a doc comment's example with it.

Bench#

black_boxsince 0.31.0Source
(value: $T) -> T

Returns value and hides it from the optimizer: the compiler cannot know the result, and cannot drop the work that made it, so wrap what a bench computes and the inputs it must not see through. It is an intrinsic that costs no instruction: an empty assembly block takes the value and may change it (and any memory), see docs/language/intrinsics.md.

set_bytessince 0.31.0Source
(b: *Bench, count: s64)

Each iteration handles count bytes, so the report shows a rate in bytes per second.

set_itemssince 0.31.0Source
(b: *Bench, count: s64)

Each iteration handles count items (requests, elements), shown as items per second.

reset_timersince 0.31.0Source
(b: *Bench)

Starts the measured time and the count of allocations again, for a bench that sets up its input before the loop.

Types#

Teststructsince 0.28.0Source
:: struct

The test that is running. name is parent/child inside a subtest.

name: string
failed: bool
skipped: bool
parent: *Test
Private to the module.
cleanups: [..] Cleanup
temp_dir: string
snapshots: s64
Benchstructsince 0.28.0Source
:: struct

The benchmark that is running (bench.jai). A @bench procedure loops iterations times.

iterations: s64
bytes_per_iteration: s64
items_per_iteration: s64
started: Apollo_Time
Private to the module.
counter: *Counter
Matcher_Kindenumsince 0.30.0Source
:: enum

What a Matcher checks.

EQUAL
GREATER
AT_LEAST
LESS
AT_MOST
BETWEEN
NEAR
CONTAINS
STARTS_WITH
ENDS_WITH
IS_EMPTY
HAS_LENGTH
HAS_ITEM
EACH
IS_NULL
IS_TRUE
IS_FALSE
OF_TYPE
NOT
ALL_OF
ANY_OF
Matcherstructsince 0.30.0Source
:: struct

What a value should be. Build one with the procedures below; the operands live in temporary storage, so a matcher is for the test that makes it. Use it with assert_that.

first: Any
second: Any
type: *Type_Info
children: [] Matcher
Rngstructsince 0.30.0Source
:: struct

A source of pseudo-random numbers (splitmix64). The same seed gives the same numbers on every OS and in every mode, which is what makes a failing run repeatable. Custom generators take one and draw from it.

state: u64
size: s64
How big the values of this trial may be, from 1 on the first trial to 100 on the last.
Custom_Generatorstructsince 0.30.0Source
:: struct

A generator the property uses for every argument (or part of one) of the type it returns, in place of the built-in one. Named in a @generate(NAME) note; values it makes are not shrunk.

info: *Type_Info
make: (rng: *Rng, destination: *void)
even :: (rng: *Rng) -> s64 { return rng_range(rng, -500, 500) * 2; }

doubles :: (n: s64) { expect_equal((n * 3) % 2, 0); } @test @property @generate(even)
Test_Casestructsince 0.28.0Source
:: struct

One entry of the table run_tests takes: a test's name, procedure, source location and flags.

name: string
body: (t: *Test)
location: Source_Code_Location
flags: Test_Flags
Test_Flagsenum flagssince 0.28.0Source
:: enum_flags

How a test is run.

ALLOW_LEAKS
CASES
a @cases test: its body is run_cases
Bench_CasestructforeignSource
:: struct

One @bench the Discovery plugin found.

name: string
body: (b: *Bench)
samples: s64
From @samples(N) and @min_time(MS); 0 means the default.
min_time_ms: s64
:: struct

Settings of a run from the command line (--samples, --warmup, --measure-ms); 0 keeps what the bench or the defaults say.

samples: s64
warmup_ms: s64
measure_ms: s64

Constants#