# Labels Labels are sentinel values for tagging and filtering tests. ## Defining labels Use `#[skuld::label]` on a constant declaration. The label's string name is the identifier lowercased — `DOCKER` becomes the name `"docker"`. ```rust #[skuld::label] pub const DOCKER: skuld::Label; #[skuld::label] pub const SLOW: skuld::Label; #[skuld::label] pub const UNIT: skuld::Label; ``` The const name must be a valid Rust identifier (ASCII letters, digits, and underscores; must not start with a digit) — it's a plain Rust `const`, so the compiler enforces the usual identifier rules on the symbol. The runtime label string is the identifier lowercased. ### Cross-crate sharing To use a label defined in another crate, just `use` it: ```rust use other_crate::DOCKER; #[skuld::test(labels = [DOCKER])] fn my_test() { /* ... */ } ``` At startup, skuld panics (with both source locations) if two `#[skuld::label]` declarations in the binary produce the same lowercased name. ## Labeling tests Pass label constants to the `labels` option: ```rust #[skuld::test(labels = [DOCKER, SLOW])] fn heavy_test() { /* ... */ } #[skuld::test(labels = [UNIT])] fn fast_test() { /* ... */ } ``` ## Filtering with `SKULD_LABELS` Set the `SKULD_LABELS` environment variable to a boolean expression to filter tests at collection time. Tests not matching the filter do not appear at all (not ignored — absent): ```bash SKULD_LABELS=docker cargo test # only tests labeled "docker" SKULD_LABELS="docker | slow" cargo test # tests labeled "docker" OR "slow" SKULD_LABELS="docker & slow" cargo test # tests labeled "docker" AND "slow" SKULD_LABELS="!slow" cargo test # all tests except "slow" SKULD_LABELS="(docker | integration) & !slow" cargo test # combined ``` Label names in `SKULD_LABELS` are matched case-insensitively. `SKULD_LABELS=DOCKER`, `SKULD_LABELS=Docker`, and `SKULD_LABELS=docker` are equivalent. ### Expression syntax | Operator | Meaning | Example | | -------- | ---------- | --------------------------------- | | (none) | bare label | `docker` | | `true` | constant | `true` (matches every test) | | `false` | constant | `false` (matches no test) | | `!` | NOT | `!slow` | | `&` | AND | `docker & slow` | | `\|` | OR | `docker \| slow` | | `()` | grouping | `(docker \| integration) & !slow` | **Precedence** (highest to lowest): `!` > `&` > `|` Whitespace between tokens is optional. Quote the value in shell when using `|`. **Unset** `SKULD_LABELS` → no filtering, all tests run. **Empty or whitespace-only** `SKULD_LABELS` (e.g. `SKULD_LABELS=""` or `SKULD_LABELS=" "`) → panic at startup with `skuld: SKULD_LABELS: ...`. Shell expansions like `SKULD_LABELS="$MAYBE_UNSET"` that produce an empty string will therefore panic, not be treated as "no filter." If you want a conditional filter, use `if [ -n "$VAR" ]; then ... fi` or similar. The names `true` and `false` are reserved by the grammar. Attempting to define a label with one of those names (`#[skuld::label] pub const TRUE: skuld::Label`) is a compile-time error. ### Canonical form Filters are stored in a canonical (BDD-simplified, sort-normalized) form. Two semantically-equivalent expressions are interchangeable: - `LabelFilter::parse("a & b") == LabelFilter::parse("b & a")` — operator order does not matter. - `LabelFilter::parse("!!a") == LabelFilter::parse("a")` — double negation collapses. - `LabelFilter::parse("a | !a") == LabelFilter::parse("true")` — tautologies fold to `true`. - Merged fixture filters dedup automatically — `(a) | (a) | (b)` displays as `a | b`. Display output reflects the canonical form, so `parse(filter.to_string())` round-trips. Note that the canonical form uses sum-of-products with sorted children, so `(a | b) & c` displays as `a & c | b & c`. ## Module-level defaults Use `default_labels!` to set default labels for all `#[skuld::test]` functions in a module: ```rust #[skuld::label] pub const SMOKE: skuld::Label; #[skuld::label] pub const UNIT: skuld::Label; #[skuld::label] pub const SLOW: skuld::Label; skuld::default_labels!(SMOKE, UNIT); #[skuld::test] // inherits [SMOKE, UNIT] fn test_a() { /* ... */ } #[skuld::test(labels = [SLOW])] // gets [SLOW], NOT [SMOKE, UNIT, SLOW] fn test_b() { /* ... */ } #[skuld::test(labels = [])] // gets nothing (explicit opt-out) fn test_c() { /* ... */ } ``` Explicit `labels = [...]` (including empty) **fully replaces** the module defaults — there is no merging. Default labels are matched by module path prefix, so a `default_labels!` in a parent module applies to all children unless overridden.