Writing Tests

The #[skuld::test] attribute

Every test function must be annotated with #[skuld::test]. The attribute registers the function with the harness and supports the following options:

#[skuld::test]                                     // no options
#[skuld::test(requires = [valgrind, my_binary])]   // runtime preconditions
#[skuld::test(name = "custom display name")]        // custom name in output
#[skuld::test(labels = [DOCKER, SLOW])]             // labels for filtering
#[skuld::test(ignore)]                              // statically ignored
#[skuld::test(ignore = "blocked on #123")]          // ignored with reason
#[skuld::test(serial)]                              // serial with everything
#[skuld::test(serial = DATABASE & !FAST)]            // serial with a filter

Options can be combined:

#[skuld::test(requires = [docker], labels = [INTEGRATION], serial)]
fn heavy_test() { /* ... */ }

Note

Do not add #[test] alongside #[skuld::test] — the skuld macro already registers the function. Adding both will produce a compile error.

Preconditions

Each entry in requires = [...] must be a function with signature fn() -> Result<(), String>:

fn docker() -> Result<(), String> {
    use std::process::{Command, Stdio};
    Command::new("docker")
        .arg("--version")
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .status()
        .is_ok_and(|s| s.success())
        .then_some(())
        .ok_or_else(|| "docker not installed".into())
}

fn corpus_exists() -> Result<(), String> {
    std::path::Path::new("test_data/corpus")
        .exists()
        .then_some(())
        .ok_or_else(|| "test_data/corpus not found".into())
}

#[skuld::test(requires = [docker, corpus_exists])]
fn integration_test() {
    // Runs only if both checks pass.
}

If any requirement returns Err, the test is marked ignored (not failed) and the reason is collected for the unavailability summary.

Fixture requirements also propagate: if a test uses a fixture that has requires = [...], those requirements are checked too. See Fixtures for details.

Custom display names

By default, the test name in output is the function name. Override it with name:

#[skuld::test(name = "arithmetic: 2 + 2 = 4")]
fn test_add() {
    assert_eq!(2 + 2, 4);
}

Static ignore

Mark a test as statically ignored (always skipped, no precondition check):

#[skuld::test(ignore)]
fn work_in_progress() { /* ... */ }

#[skuld::test(ignore = "blocked on #123")]
fn blocked_test() { /* ... */ }

Statically ignored tests do not appear in the unavailability summary.

Output

When all requirements are met:

running 3 tests
test smoke_test     ... ok
test full_pipeline  ... ok
test unit_test      ... ok

test result: ok. 3 passed; 0 failed; 0 ignored

When requirements are missing:

running 3 tests
test smoke_test     ... ignored
test full_pipeline  ... ignored
test unit_test      ... ok

test result: ok. 1 passed; 0 failed; 2 ignored

--- Unavailable (2) ---
  smoke_test:     valgrind not installed
  full_pipeline:  valgrind not installed