Chapter 32 5 min read

Testing and benchmarks

A program you can't test is a program you can't change with confidence. Glide builds testing into the toolchain — no framework to install, no config. You write test_-prefixed functions in *_test.glide files and run glide test.

Your first test

Tests live next to your code (commonly in a tests/ directory) in files ending _test.glide. A test is a function whose name starts with test_, returns i32, and returns 0 to mean pass:

tests/math_test.glide
import stdlib::testing::*;
import stdlib::math::*;

fn test_ipow_basic() -> i32 {
    assert_eq!(ipow(2, 10), 1024);
    return 0;
}

fn test_gcd() -> i32 {
    assert_eq!(gcd(48, 36), 12);
    return 0;
}

Run them:

shell
glide test tests/math_test.glide
# running tests/math_test.glide ( 2 tests )
#
# 2 passed, 0 failed

Each test file is compiled into its own binary and run, so global state never leaks between files. A failed assertion marks the test failed and prints where; the runner exits non-zero if anything failed, which is exactly what CI wants.

The assertion macros

stdlib::testing gives you five macros. Pick the right one — the specific ones print better failure messages:

assert!(cond);                  // cond must be true

assert_not!(cond);              // cond must be false

assert_eq!(a, b);               // equality for i32, bool, char

assert_str_eq!(a, b);           // equality for strings — use this for text

assert_msg!(cond, "context");   // boolean check with a custom message

A test with several assertions fails on the first one that breaks:

import stdlib::testing::*;

fn slug(s: string) -> string {
    return s.trim().to_lower().replace(" ", "-");
}

fn test_slug() -> i32 {
    assert_str_eq!(slug("  Hello World  "), "hello-world");
    assert_not!(slug("A B").contains(" "));
    return 0;
}

Running the suite

glide test takes a path, or runs everything in the current directory:

shell
glide test                       # every *_test.glide under the cwd
glide test tests/                # every test file in a directory
glide test tests/math_test.glide # one file

The three layers

The runner supports tests at three granularities — the same glide test, different scope:

  • Unit — one function or struct method in isolation: pure logic, validation, a parser rule. The test_ipow_basic above is a unit test.
  • Integration — several modules together: fs + os, or your library using another. Same shape, just exercising more at once.
  • Golden — a whole program whose stdout is compared against a recorded .expected file. The runner builds the program, runs it, and diffs the output.

Golden tests run with a flag:

shell
glide test --golden tests/golden

Each program under tests/golden/ is paired with a .expected file holding the output it should produce. The runner normalizes CRLF to LF, so a .expected written with Unix line endings works on Windows too. Golden tests are perfect for CLIs and code generators where the contract is the output.

Benchmarks

Correctness is one axis; speed is another. glide bench runs microbenchmarks declared as bench_-prefixed functions taking a *Bencher:

sum_bench.glide
import stdlib::bench::*;

pub fn bench_sum(b: *Bencher) {
    let mut total: i32 = 0;
    for let i: i32 = 0; i < b.n; i++ {
        total = total + i;
    }
    b.consume(&total);     // keep `total` alive so the loop isn't optimized away

}

Run it:

shell
glide bench sum_bench.glide

Two things make a benchmark honest:

  • `b.n` is the iteration count, and the runner auto-tunes it — it scales n up until the benchmark runs long enough to time reliably, then reports nanoseconds per operation. Your loop must run exactly b.n times.
  • `b.consume(&x)` tells the optimizer the result is observed. Without it, an -O2 build would notice total is never used and delete your whole loop — you'd be timing nothing. Pass &x for primitives, or the value directly for pointer-shaped types (b.consume(my_vector)).

Recap

Where to next

You can build, test, and measure Glide programs. The final chapter — FFI and escape hatches — goes the other direction: calling C, embedding raw C, inline assembly, and per-platform code for when you need to reach below the language.