# 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:

```rust
#[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:

```rust
#[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>`:

```rust
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](fixtures.md) for details.

## Custom display names

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

```rust
#[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):

```rust
#[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
```
