Testing
Silt has a built-in test runner invoked with silt test. It discovers and
runs test functions without any configuration.
File Conventions
Test files must use one of these naming patterns to be auto-discovered:
*_test.silt(e.g.math_test.silt)*.test.silt(e.g.math.test.silt)
Without a path argument, silt test searches the current directory
recursively. You can also pass a specific file or directory:
silt test -- search current directory recursively
silt test tests/ -- search tests/ directory recursively
silt test math_test.silt -- run a single file
A test file is a module of its own, which imports the module it tests. So it cannot write an impl for a type of that module (an impl goes in the module of its trait or of its type, see Where an Impl Is Written): put the impl beside the type, or declare the trait in the test file and implement it there, or wrap the value in a type the test file declares.
Function Conventions
Within a test file, functions are recognized by their name prefix:
test_*— recognized as a test and executedskip_test_*— shown as skipped, not executed- Any other function name — ignored by the test runner (available as helpers)
import string
import test
fn test_addition() {
test.assert_eq(1 + 1, 2)
}
fn test_string_length() {
test.assert_eq(string.length("hello"), 5)
}
fn skip_test_not_ready_yet() {
test.assert(false)
}
fn helper(x) {
x * 2
}
fn test_with_helper() {
test.assert_eq(helper(3), 6)
}
Running silt test in a directory containing this file produces:
PASS ./math_test.silt::test_addition
PASS ./math_test.silt::test_string_length
SKIP ./math_test.silt::skip_test_not_ready_yet
PASS ./math_test.silt::test_with_helper
4 tests: 3 passed, 0 failed, 1 skipped
(When a single file is passed directly — silt test math_test.silt — the
./ prefix is omitted and you see PASS math_test.silt::test_addition
instead.)
Filtering Tests
Use --filter <pattern> to run only tests whose names contain the pattern:
silt test --filter addition -- runs only test_addition
silt test --filter string -- runs only test_string_length
The filter matches against the function name (not the file name). A file with no matching test is skipped entirely: it is not compiled and nothing is reported for it. A file whose tests cannot be listed, because it cannot be read or has a syntax error, is still reported, and its error fails the run.
Assertions
The test module provides assertion functions. Hover over any
test.* call in your editor for the full signature and per-function
documentation (the LSP surfaces it from the inlined builtin docs);
the LSP’s completion popup on test. also enumerates every assertion
entry point.
| Function | Description |
|---|---|
test.assert(condition) | Fails if condition is false |
test.assert_eq(left, right) | Fails if left != right |
test.assert_ne(left, right) | Fails if left == right |
Spawned Tasks
A test that spawns tasks is responsible for them. If a task fails and the
test never joins it (task.join) or cancels it (task.cancel), the failure
fails the test that spawned the task — also when the task was spawned by
another task of that test. The failure is reported under the test’s result
line, rendered like any runtime error:
FAIL spawn_test.silt::test_worker
error[runtime]: task <handle:0> failed and was never joined: division by zero
--> spawn_test.silt:5:29
...
= help: join the task with task.join to handle its error, or cancel it with task.cancel
A test has ended when its test function has returned and the tasks it spawned can do no more: each has ended or waits, with no timer and no I/O of theirs pending (the rule of When a program ends, applied to the test’s own tasks). The runner waits for that before it gives the test’s result, so:
- a task that fails after the test function has returned fails that test, not a later one;
- a task that never stops keeps its test, and the run, from ending: cancel it before the test returns;
- a task left waiting for something nobody will give is dropped with its test, and is neither waited for nor reported by a later test.
A test whose function fails (a failed assertion, a panic, a returned
Err) is not waited for: its tasks are stopped where they are, as a program’s are when main
fails. A ticker that the test would have cancelled on its last line does
not keep the run going.
The tasks that a file’s top-level code spawns are the file’s. The runner
waits for them in the same way before the first test, and those that wait
then stay for the tests: a top-level task that answers requests on a channel
serves every test of the file, and a test that waits for its answer is not
deadlocked. A test has ended when neither its own tasks nor the file’s can
do more. If a task of the top-level code fails and nobody joins it, the file
has failed: FAIL <file> (a task spawned by the file's top-level code failed), once, after the file’s last test, with the reports under it.
Exit Code
silt test exits with code 0 if all tests pass and code 1 if any test fails,
including a test that failed because a task it spawned failed.