{"id":46736598,"url":"https://github.com/bindreams/skuld","last_synced_at":"2026-04-18T22:00:57.770Z","repository":{"id":342065933,"uuid":"1172536658","full_name":"bindreams/skuld","owner":"bindreams","description":null,"archived":false,"fork":false,"pushed_at":"2026-04-11T00:39:33.000Z","size":164,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-11T02:09:23.761Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bindreams.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-04T12:25:11.000Z","updated_at":"2026-04-11T00:39:37.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/bindreams/skuld","commit_stats":null,"previous_names":["bindreams/skuld"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/bindreams/skuld","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bindreams%2Fskuld","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bindreams%2Fskuld/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bindreams%2Fskuld/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bindreams%2Fskuld/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bindreams","download_url":"https://codeload.github.com/bindreams/skuld/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bindreams%2Fskuld/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31982756,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-18T17:30:12.329Z","status":"ssl_error","status_checked_at":"2026-04-18T17:29:59.069Z","response_time":103,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-03-09T16:53:00.780Z","updated_at":"2026-04-18T22:00:57.761Z","avatar_url":"https://github.com/bindreams.png","language":"Rust","readme":"# skuld\n\nTest harness for Rust with runtime preconditions, fixture injection, and label filtering.\n\nRust's built-in test framework has no way to mark a test as \"ignored with reason\" at runtime. Tests that need external tools (valgrind, docker, a built binary) either silently pass when the tool is missing, or hard-fail. `skuld` replaces the built-in harness with one that checks preconditions at runtime, reports unmet ones as `ignored`, and prints a summary showing exactly what's missing.\n\n## Setup\n\nAdd a `[[test]]` target with `harness = false` in your `Cargo.toml`:\n\n```toml\n[dev-dependencies]\nskuld = { path = \"skuld\" }\n\n[[test]]\nname = \"my_tests\"\npath = \"tests/my_tests.rs\"\nharness = false\n```\n\nCreate the test entry point:\n\n```rust\n// tests/my_tests.rs\n#[path = \"my_tests_support/mod.rs\"]\nmod support;\n\nfn main() {\n    skuld::run_all();\n}\n```\n\n## Unit tests\n\nTo use `skuld` for tests inside `src/`, disable the default harness for the library target and add the entry point:\n\n```toml\n[lib]\nharness = false\n```\n\n```rust\n// lib.rs\n#[cfg(test)]\nfn main() {\n    skuld::run_all();\n}\n```\n\nNow `#[skuld::test]` works in any `#[cfg(test)]` module under `src/`:\n\n```rust\n// src/my_module.rs\n#[cfg(test)]\nmod tests {\n    #[skuld::test]\n    fn unit_test_example() {\n        assert_eq!(2 + 2, 4);\n    }\n}\n```\n\n\u003e **Note:** Without `[lib] harness = false`, the default Rust test harness runs instead of skuld, silently reporting `running 0 tests` with no error.\n\n## Writing tests\n\nAnnotate test functions with `#[skuld::test]`. The attribute supports several options:\n\n```rust\nfn valgrind() -\u003e Result\u003c(), String\u003e {\n    use std::process::{Command, Stdio};\n    Command::new(\"valgrind\")\n        .arg(\"--version\")\n        .stdout(Stdio::null())\n        .stderr(Stdio::null())\n        .status()\n        .is_ok_and(|s| s.success())\n        .then_some(())\n        .ok_or_else(|| \"valgrind not installed\".into())\n}\n\n#[skuld::test(requires = [valgrind], labels = [SLOW])]\nfn smoke_test() {\n    // Runs only if valgrind is available.\n}\n\n#[skuld::test(name = \"custom display name\")]\nfn internal_name() { /* ... */ }\n\n#[skuld::test(ignore)]\nfn wip() { /* ... */ }\n\n#[skuld::test(ignore = \"blocked on #123\")]\nfn blocked_test() { /* ... */ }\n\n// Standard outer attributes also work (must appear after #[skuld::test]):\n#[skuld::test]\n#[ignore]\nfn wip_outer() { /* ... */ }\n\n#[skuld::test]\n#[ignore = \"blocked on #456\"]\nfn blocked_outer() { /* ... */ }\n\n#[skuld::test(serial)]\nfn modifies_global_state() { /* ... */ }\n\n#[skuld::test(should_panic)]\nfn panics_on_bad_input() {\n    my_function(invalid_input);\n}\n\n#[skuld::test(should_panic = \"out of range\")]\nfn panics_with_message() {\n    my_function(too_large);\n}\n\n// Standard outer attribute form (must appear after #[skuld::test]):\n#[skuld::test]\n#[should_panic(expected = \"out of range\")]\nfn panics_outer() {\n    my_function(too_large);\n}\n```\n\nEvery `#[skuld::test]` function is registered with the harness. Functions without `#[skuld::test]` are invisible to skuld.\n\n## Async tests\n\nEnable the `tokio` feature to use `async fn` test bodies:\n\n```toml\n[dev-dependencies]\nskuld = { path = \"skuld\", features = [\"tokio\"] }\n```\n\n```rust\n#[skuld::test]\nasync fn connects_to_server() {\n    let stream = tokio::net::TcpStream::connect(\"127.0.0.1:8080\").await.unwrap();\n    // ...\n}\n```\n\nAsync tests run on a single-threaded tokio runtime (`current_thread` with `enable_all()`). All existing features — fixtures, `requires`, `should_panic`, `serial`, labels — work with async tests.\n\nTests may also return `Result\u003c(), E\u003e` where `E: Debug`. An `Err` return fails the test:\n\n```rust\n#[skuld::test]\nasync fn parses_config() -\u003e Result\u003c(), Box\u003cdyn std::error::Error\u003e\u003e {\n    let config = load_config().await?;\n    assert_eq!(config.port, 8080);\n    Ok(())\n}\n```\n\n## Fixtures\n\nFixtures provide dependency-injected values to test functions. Define a fixture with `#[skuld::fixture]` and inject it with `#[fixture]` on a test parameter:\n\n```rust\nuse std::path::Path;\n\n#[skuld::fixture(deref)]\nfn temp_dir(#[fixture(test_name)] name: \u0026str) -\u003e Result\u003cskuld::TempDir, String\u003e {\n    // skuld provides TempDir and TestName as built-in fixtures.\n    // This example shows how custom fixtures work.\n    todo!()\n}\n\n#[skuld::test]\nfn my_test(#[fixture(temp_dir)] dir: \u0026Path) {\n    assert!(dir.exists());\n}\n```\n\n### Scopes\n\nEach fixture has a lifetime scope:\n\n| Scope                | Behaviour                                                           |\n| -------------------- | ------------------------------------------------------------------- |\n| `variable` (default) | Fresh instance per request. Dropped when the `FixtureHandle` drops. |\n| `test`               | Cached per test. Dropped when the test ends.                        |\n| `process`            | Cached globally. Dropped after all tests finish (LIFO).             |\n\n```rust\n#[skuld::fixture(scope = process, requires = [docker_available])]\nfn corpus_image() -\u003e Result\u003cCorpusImage, String\u003e { /* ... */ }\n```\n\nA fixture may only depend on fixtures of the **same or wider** scope. Dependency cycles are detected at startup.\n\n### Built-in fixtures\n\n| Fixture     | Scope    | Type                         | Serial | Description                                    |\n| ----------- | -------- | ---------------------------- | ------ | ---------------------------------------------- |\n| `test_name` | test     | `TestName` (deref to `\u0026str`) | no     | Current test function name                     |\n| `temp_dir`  | variable | `TempDir` (deref to `\u0026Path`) | no     | Temporary directory named after the test       |\n| `env`       | test     | `EnvGuard`                   | yes    | Set/remove env vars with automatic revert      |\n| `cwd`       | test     | `CwdGuard`                   | yes    | Change working directory with automatic revert |\n\n### Deref coercion\n\nFixtures annotated with `deref` can be injected as their `Deref::Target` type:\n\n```rust\n// TempDir implements Deref\u003cTarget = Path\u003e, so both work:\nfn example1(#[fixture(temp_dir)] dir: \u0026skuld::TempDir) { /* ... */ }\nfn example2(#[fixture(temp_dir)] dir: \u0026Path) { /* ... */ }\n```\n\n## Labels\n\nLabels are sentinel values for tagging and filtering tests. Define them with `#[skuld::label]`:\n\n```rust\n#[skuld::label] pub const DOCKER: skuld::Label;\n#[skuld::label] pub const SLOW: skuld::Label;\n\n#[skuld::test(labels = [DOCKER, SLOW])]\nfn heavy_test() { /* ... */ }\n```\n\nThe label's string name is the identifier lowercased (`DOCKER` → `\"docker\"`). To reuse a label from another crate, just `use` it: `use other_crate::DOCKER;`.\n\nFilter with the `SKULD_LABELS` environment variable using boolean expressions (`\u0026` AND, `|` OR, `!` NOT, parentheses, plus the `true` and `false` literals):\n\n```bash\nSKULD_LABELS=docker cargo test                         # only \"docker\"\nSKULD_LABELS=\"docker | slow\" cargo test                # \"docker\" OR \"slow\"\nSKULD_LABELS=\"(docker | integration) \u0026 !slow\" cargo test  # combined\n```\n\nUnset `SKULD_LABELS` runs all tests. Precedence: `!` \u003e `\u0026` \u003e `|`. Label names are matched case-insensitively, so `SKULD_LABELS=DOCKER` is equivalent to `SKULD_LABELS=docker`. Filters are stored canonically, so `parse(\"a \u0026 b\") == parse(\"b \u0026 a\")`.\n\n### Module-level defaults\n\n```rust\n#[skuld::label] pub const SMOKE: skuld::Label;\n#[skuld::label] pub const UNIT: skuld::Label;\n#[skuld::label] pub const SLOW: skuld::Label;\nskuld::default_labels!(SMOKE, UNIT);\n\n#[skuld::test]                      // inherits [SMOKE, UNIT]\nfn test_a() { /* ... */ }\n\n#[skuld::test(labels = [SLOW])]     // gets [SLOW], NOT [SMOKE, UNIT, SLOW]\nfn test_b() { /* ... */ }\n\n#[skuld::test(labels = [])]         // gets nothing (explicit opt-out)\nfn test_c() { /* ... */ }\n```\n\n## Serial tests\n\nTests that modify process-global state (environment variables, current directory) must not run in parallel with other such tests. Mark them with `serial`:\n\n```rust\n#[skuld::test(serial)]\nfn test_with_global_state() { /* ... */ }\n```\n\nFixtures can also declare `serial`. Any test using a serial fixture automatically inherits the flag:\n\n```rust\n#[skuld::fixture(scope = test, serial)]\nfn env() -\u003e Result\u003cEnvGuard, String\u003e { /* ... */ }\n\n#[skuld::test]\nfn my_test(#[fixture] env: \u0026EnvGuard) {\n    // Automatically serial — env fixture declares it.\n    env.set(\"MY_VAR\", \"value\");\n}\n```\n\nAll serial tests run under a cross-process file lock (`target/{profile}/.skuld-serial.lock`). Under `cargo test` the lock is trivially uncontended; under `cargo nextest run` (process-per-test) it serializes across processes automatically. Non-serial tests are unaffected and may still run in parallel.\n\n## Dynamic tests\n\nUse `TestRunner` to mix inventory-registered and runtime-generated tests:\n\n```rust\nfn main() {\n    #[skuld::label] const DATA: skuld::Label;\n\n    let mut runner = skuld::TestRunner::new();\n    for file in std::fs::read_dir(\"test_data\").unwrap() {\n        let path = file.unwrap().path();\n        runner.add(\n            path.display().to_string(),\n            \u0026[DATA],\n            false,\n            move || { /* test body */ },\n        );\n    }\n    runner.run();\n}\n```\n\n## Running tests\n\n### Capture model\n\nUnder `cargo test`, skuld captures each test's `stdout` and `stderr` via a file-descriptor redirect (`dup2` on Unix; `SetStdHandle` + `_dup2` on Windows). On pass the captured bytes are discarded; on failure they are dumped to the real `stderr` between `---- captured ----` markers, followed by the panic. The capture intercepts at the FD level, so every write — `println!`, `eprintln!`, raw `io::stdout().write_all`, FFI output, tracing subscribers installed by the test body, and even output from spawned child processes — is captured. Tests are free to install their own `tracing_subscriber::registry().try_init()` and skuld stays out of the dispatch path entirely.\n\nBecause FD redirect is a process-wide operation, capture mode forces `--test-threads=1`. For parallel execution, either run with `--nocapture` or use `cargo nextest run` (recommended for large suites — nextest runs each test in its own subprocess and captures via OS pipes externally, so skuld's in-process redirect is unnecessary and disabled automatically). Serial tests are safe under nextest: the `serial` lock uses a cross-process file lock, so `#[skuld::test(serial)]` correctly serializes even when nextest spawns separate processes.\n\n```bash\ncargo test                      # default: FD capture, serial, silent on pass\ncargo test -- --nocapture       # no capture, default parallelism, all output visible\ncargo nextest run               # process-per-test parallelism via nextest\n```\n\n### `SKULD_DEBUG=1`\n\nSet `SKULD_DEBUG=1` to get diagnostic lines around each test's execution, useful for debugging capture setup or runner behavior:\n\n```bash\nSKULD_DEBUG=1 cargo test\n# ...\n# [skuld] my_test: starting\n# [skuld-debug] my_test: entering test scope\n# [skuld-debug] my_test: capture enabled (fd redirect)\n# [skuld-debug] my_test: capture disabled\n# [skuld] my_test: pass (3 ms)\n```\n\n### A note on `tracing-subscriber`'s `tracing-log` feature\n\nIf your test code or code under test pulls in `tracing-subscriber` directly, **do not enable its `tracing-log` feature**. The feature auto-installs a `log::Log` shim on the first subscriber `init`, which mutates `log::max_level` globally. Downstream projects have hit Windows CI timeout regressions from this — see bindreams/hole#147. If you need the `log`→`tracing` bridge, call `tracing_log::LogTracer::init()` yourself in the test that needs it, and accept that doing so is a process-wide, one-time operation.\n\n## Output\n\nWhen all requirements are met:\n\n```\nrunning 2 tests\ntest smoke_test     ... ok\ntest full_pipeline  ... ok\n\ntest result: ok. 2 passed; 0 failed; 0 ignored\n```\n\nWhen a requirement is missing:\n\n```\nrunning 2 tests\ntest smoke_test     ... ignored\ntest full_pipeline  ... ignored\n\ntest result: ok. 0 passed; 0 failed; 2 ignored\n\n--- Unavailable (2) ---\n  smoke_test:     valgrind not installed\n  full_pipeline:  valgrind not installed\n```\n\n## How it works\n\n1. `#[skuld::test]` is a proc macro that preserves the original function and appends an `inventory::submit!` call to register it with the harness.\n1. `run_all()` (or `TestRunner::run_tests()`) iterates all registered tests, checks preconditions and fixture requirements at runtime, and builds `libtest-mimic::Trial`s — marking unmet tests as ignored.\n1. After `libtest-mimic::run()` completes, the unavailability summary is printed to stderr.\n\n## License\n\n\u003cimg align=\"right\" width=\"150px\" height=\"150px\" src=\"https://www.apache.org/foundation/press/kit/img/the-apache-way-badge/Indigo-THE_APACHE_WAY_BADGE-rgb.svg\"\u003e\n\nCopyright 2026, Anna Zhukova\n\nThis project is licensed under the Apache 2.0 license. The license text can be found at [LICENSE.md](/LICENSE.md).\n\n## About\n\n\u003cimg align=\"right\" width=\"122px\" height=\"180px\" src=\"docs/src/_static/norns.jpg\"\u003e\n\n**Skuld** is the youngest of the three Norns in Norse mythology — the weavers of fate who sit beneath the world-tree Yggdrasil. While her sisters Urðr and Verðanði govern the past and the present, Skuld presides over _what shall be_: obligations yet unfulfilled, debts yet unpaid. Her name shares its root with the English word _should_.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbindreams%2Fskuld","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbindreams%2Fskuld","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbindreams%2Fskuld/lists"}