20Testing
Cryo ships a built-in unit-test framework. Tests live in <project>/tests/ files whose namespace declaration carries the ![config(testing)] directive. Inside such a file, every function marked ![test] is auto-discovered by the compiler and run fork-per-test by cryo test.
![config(testing)]
namespace MyApp::Tests;
import std::test::assert::{ expect_eq, expect, bail };
import std::test::error::{ TestError };
![test]
function addition_is_commutative() -> Result<(), TestError> {
return expect_eq(1 + 2, 2 + 1);
}
![test]
![ignore]
function expensive_integration_test() -> Result<(), TestError> {
// Run only when the user passes --ignored.
return expect(setup_real_environment(), "environment ready");
}
![test]
![should_panic]
function unwrap_none_panics() -> Result<(), TestError> {
const empty: Option<int> = Option::None;
const _value: int = empty.unwrap(); // panics
return bail("unwrap should have panicked");
}
A test function returns Result<(), TestError>. Ok(()) is success; Err(...) is failure with a message. The assert module provides:
expect(condition, message): fails if the condition is false.expect_eq(a, b): fails ifa != b(requiresT: Eq + Display).expect_ne(a, b): fails ifa == b.bail(message): unconditional failure.bail_other(message): non-assertion failure (treated as an error rather than a test fail).
The runner forks a child process per test, captures its output, and applies the ![should_panic] inversion if requested. Run with:
cryo test # run every discoverable test
cryo test some_filter # run tests whose name contains "some_filter"
cryo test --list # discover only; print and exit
cryo test --ignored # also run ![ignore]-marked tests
cryo test --exact # treat the filter as an exact match
cryo test -q, --quiet # suppress per-test ok / ignored lines
Output format
The runner ships three output formats; --format=<mode> picks per-run:
| Format | Layout |
|---|---|
plain | One test NAME ... ok line per test; CI-friendly. Default. |
pretty | Tests grouped under their namespace, indented leaves, [PASS] / [FAIL] / [skip] chips. Colored on a TTY. |
compact | One line per namespace with a dot-stream of results (. pass, F fail, s skip, P did-not-panic, E runner error) and a trailing per-group tally. |
Color follows --color=<auto|always|never>. auto (default) emits ANSI escapes only when stdout is a terminal; the NO_COLOR environment variable forces it off per https://no-color.org/.
Configuration precedence is CLI flag > environment variable > cryoconfig > built-in default:
cryo test --format=pretty --color=always
CRYO_TEST_FORMAT=compact CRYO_TEST_COLOR=never cryo test
# cryoconfig
[test]
format = "pretty" # one of: plain | pretty | compact
color = "auto" # one of: auto | always | never
cryo test forwards [test] format = "..." to the spawned test binary as --format=..., so the cryoconfig defaults travel without needing exported environment variables.
namespace app::module;Every source file opens with one: its position in the module hierarchy. The compiler uses it to resolve imports and to mangle symbol names, so two parse functions in different modules never collide at link time.
import a::b;
import a::b::{ X, Y };
import a::b as c;
import a::b::*;Brings a module, or selected items from it, into scope. One path per declaration: use the brace form for several items, as to alias, and * for everything public (prefer the brace form; wildcards invite name collisions).
function expect_eq<T>(a: T, b: T) -> Result<(), TestError>
where T: Eq + DisplayOk(()) if a.equals(&b), otherwise Err carrying a multi-line "assertion left == right failed" message that Display-formats both values. Allocates the message on the heap; on allocation failure during message building, returns a generic fallback message.
function expect(condition: boolean, message: Str) -> Result<(), TestError>Return Ok(()) if condition is true, Err(TestError::Failed(msg)) otherwise. The cheap, no-format assertion.
function bail(message: Str) -> Result<(), TestError>Always return Err(TestError::Failed(message)). Use this when the failure condition is implicit in reaching this code path.
type enum TestError {
Failed(String);
Other(String);
}Why a test failed. The runner prints the variant's message verbatim to stderr from the child process before exiting non-zero, so the parent can show the failing message in the test summary.
std::test::errorfunction name<T>(param: Type) -> Ret { ... }Declares a function. Parameters are written name: Type and are never inferred; the return type follows -> and defaults to void. Declaration order does not matter: every signature is collected before any body is checked, so functions may call each other freely and recurse.
function addition_is_commutative() -> Result<(), TestError>primitive ()The unit type: a value that carries no information, and the empty tuple. Distinct from void, which is the absence of a value - Result<(), E> succeeds with nothing to return. 0 bytes.
return value;Leaves the current function with the given value. A function whose return type is not void must return one; a bare return; is for void. In an async function it completes the future with the value.
function expensive_integration_test() -> Result<(), TestError>function unwrap_none_panics() -> Result<(), TestError>const name: Type = value;An immutable binding: it cannot be reassigned after initialisation. The annotation may be omitted when an initialiser is present - the binding takes the initialiser's concrete type, inferred locally. Mutability is opt-in through mut.
const empty: Option<int>Option::Noneconst _value: intconst empty: Option<int>Option::unwrap(&this) -> TReturn the inner value, aborting on None. Prefer unwrap_or or a match in code that must not panic.
Result::Ok(T)Result::Err(E)function name(count: i32, args...)A variadic parameter: the trailing bucket receives any number of further arguments with C's calling convention, so the function stays ABI-compatible with the printf family. Wrap the bucket in VaArgs to read typed values from it.
function assert(condition: boolean, message: string) -> voidPanic if condition is false. Always checked.
type trait Eq {
equals(&this, other: &This) -> boolean;
}Types that support value equality. equals(a, b) must be reflexive, symmetric, and transitive.
type trait Display {
fmt<W>(&this, f: mut &Formatter<W>) -> Result<(), FmtError>
where W: FmtWrite;
}Display / Debug / Formatter.
std::fmt::displayfunction expect_ne<T>(a: T, b: T) -> Result<(), TestError>
where T: Eq + DisplayOk(()) if !a.equals(&b), otherwise Err carrying a multi-line "assertion left != right failed" message that Display-formats both values. Allocates as expect_eq does.
function bail_other(message: Str) -> Result<(), TestError>Same as bail but produces a TestError::Other, for non-assertion failures (I/O setup, environment problems, etc.) that the runner should display under "test error" rather than "assertion failed".
// reservedReserved for future use: the lexer recognises it, but no current syntax gives it meaning.
language reference