Serial Tests¶
Some test resources are process-global — environment variables, the current working directory, and similar shared state. Tests that modify them cannot safely run in parallel.
Marking tests as serial¶
Serial with everything¶
A bare serial flag blocks the test against all other tests — both serial and non-serial:
#[skuld::test(serial)]
fn test_with_global_state() {
std::env::set_var("UNSAFE_BUT_SERIAL", "ok");
}
While a serial test is running, no other test executes. This is the safest option when your test touches truly process-global state.
Serial with a filter expression¶
When only a subset of tests conflict, use serial = <expr> to restrict mutual exclusion to tests whose labels match the expression:
#[skuld::label] const DATABASE: skuld::Label;
#[skuld::label] const FAST: skuld::Label;
#[skuld::test(labels = [DATABASE], serial = DATABASE)]
fn migrate_schema() {
// Blocks other tests that are serial with DATABASE,
// but non-DATABASE tests can run in parallel.
}
The expression supports boolean operators with bare operator syntax:
Syntax |
Meaning |
|---|---|
|
Serial with tests labeled DATABASE |
|
Serial with DATABASE tests that are not FAST |
|
Serial with DATABASE or CACHE tests |
|
Grouped expression |
Operator precedence: ! > & > |. Parentheses override precedence.
Labels used in serial expressions must be Label constants in scope (defined with #[skuld::label]). The expression matches label names case-insensitively, so serial = DATABASE and serial = database are equivalent.
How coordination works¶
Serial tests are coordinated through a SQLite database, automatically managed by skuld. This works across multiple test processes — if two test binaries run concurrently, their serial constraints are respected across process boundaries.
Serial fixtures¶
Fixtures can declare serial too. Any test that uses a serial fixture automatically inherits the serial constraint:
#[skuld::label] const DATABASE: skuld::Label;
#[skuld::fixture(scope = test, serial = DATABASE)]
fn db_conn() -> Result<DbConn, String> {
Ok(DbConn::new())
}
#[skuld::test]
fn my_test(#[fixture] db_conn: &DbConn) {
// No `serial` on the test, but db_conn is serial = DATABASE → test is serial with DATABASE.
}
A bare serial on a fixture works the same as on a test — serial with everything:
#[skuld::fixture(scope = test, serial)]
fn env() -> Result<EnvGuard, String> {
Ok(EnvGuard::new())
}
This propagation is transitive: if fixture A depends on fixture B, and B has serial = X, then any test using A is also serial with X. When multiple fixtures contribute different serial filters, they are combined with OR — the test is serial with the union of all constraints.
Built-in serial fixtures¶
The env and cwd fixtures are both serial because they modify process-global state. You don’t need to add serial to your tests when using them — it’s inherited automatically.
LabelFilter type¶
For programmatic use (e.g. in dynamic tests), the LabelFilter type supports the same operators via Rust’s operator overloads:
use skuld::LabelFilter;
#[skuld::label] const DATABASE: skuld::Label;
#[skuld::label] const FAST: skuld::Label;
let filter: LabelFilter = DATABASE.into();
let filter = DATABASE & !FAST;
let filter = DATABASE | FAST;
See Dynamic Tests for using LabelFilter with TestRunner::add_serial_with.