skipper-rust
Test-gating for Rust via Google Spreadsheet. Enable or disable tests without changing code — just update a date in a Google Sheet.
A Rust port of get-skipper/skipper, supporting the built-in test harness (cargo test), rstest, #[tokio::test] and cucumber-rs.
How it works
A Google Spreadsheet stores test IDs with optional disabledUntil dates:
| testId | disabledUntil | notes |
|---|---|---|
tests/auth.rs > can_log_in |
||
tests/payment.rs > checkout::with_valid_card |
2099-12-31 |
Flaky on CI |
tests/features/auth.feature > Auth > Login works |
2026-06-01 |
Under investigation |
- Empty
disabledUntil→ test runs normally - Past date → test runs normally
- Future date → test is skipped (it stays disabled through the whole
disabledUntilday, UTC)
Tests not listed in the spreadsheet always run (opt-out model).
Crates
| Crate | Use it for |
|---|---|
get-skipper |
#[skipper] attribute for #[test], #[rstest], #[tokio::test] |
skipper-cucumber |
cucumber-rs scenarios |
cargo-skipper |
cargo skipper test: one fetch per run, quarantine report, sync mode |
skipper-core |
Shared core (resolver, Sheets client, writer, caches) |
skipper-macros |
Proc-macro behind #[skipper] (re-exported by get-skipper) |
The facade crate is get-skipper because skipper is taken on crates.io.
Installation
Google Sheets setup
-
Create a Google Spreadsheet with the following columns in row 1:
testIddisabledUntilnotes(optional)
-
Create a Google Cloud service account and download the JSON key file.
-
Share the spreadsheet with the service account's email (
client_emailin the JSON). -
Note the spreadsheet ID from the URL:
https://docs.google.com/spreadsheets/d/YOUR_SPREADSHEET_ID/edit
Configuration
Rust's test harness has no global setup hook, so Skipper is configured with environment variables — conveniently set in .cargo/config.toml:
[]
= "your-spreadsheet-id"
= { = "service-account.json", = true }
= "my-project" # optional, default: first sheet
Credentials, in order of precedence:
| Variable | Use case |
|---|---|
SKIPPER_CREDENTIALS_FILE |
Path to the service-account JSON (local development) |
GOOGLE_CREDS_B64 |
Base64-encoded service-account JSON (CI secrets) |
Without a spreadsheet ID and credentials, Skipper does nothing and every test runs.
For programmatic use, skipper_core::SkipperConfig also accepts
Credentials::ServiceAccount(...) with inline fields.
Usage
cargo test (libtest), rstest, tokio
use skipper;
async
Put #[skipper] first, above the test attribute. Where an attribute does not fit, call the macro at the top of the test body:
Test ID format: {file} > {module path} > {fn name}, with the crate name dropped:
| Test | ID |
|---|---|
fn can_log_in in tests/auth.rs |
tests/auth.rs > can_log_in |
fn works in mod tests of src/lib.rs (crate in a workspace) |
crates/foo/src/lib.rs > tests > works |
Note: libtest cannot mark a test as skipped at runtime, so a disabled test returns early and is reported as
ok. Skipper prints[skipper] SKIP <id>: test disabled until <date>on stderr (visible even without--nocapture), and the quarantine report lists it.
cargo skipper (recommended in CI)
cargo skipper fetches the spreadsheet once, hands the result to every test binary, then:
- writes the quarantine report to
skipper-report.jsonand$GITHUB_STEP_SUMMARY(or stdout); - with
SKIPPER_MODE=sync, reconciles the spreadsheet with the tests that ran.
Plain cargo test works too (read-only): each test binary fetches the sheet, with a
.skipper-cache.json disk cache shared between them.
cucumber-rs
// tests/bdd.rs (with `harness = false` for this [[test]] in Cargo.toml)
use World;
;
async
Disabled scenarios are filtered out of the run. For a custom Cucumber builder:
let skipper = from_env;
cucumber
.max_concurrent_scenarios
.filter_run
.await;
skipper.finish; // report + sync
Test ID format: tests/features/auth.feature > Feature > [Rule >] Scenario
Modes
| Mode | Env var | Behavior |
|---|---|---|
read-only (default) |
— | Fetch spreadsheet, skip disabled tests |
sync |
SKIPPER_MODE=sync |
Same as above, then reconcile spreadsheet with discovered tests |
Sync runs from cargo skipper (or from skipper_cucumber::run). New tests are added to the
spreadsheet; existing rows are never modified. Deletion of orphaned rows requires
SKIPPER_SYNC_ALLOW_DELETE=true. Only tests that actually run are discovered, so run the
full suite when syncing with deletion enabled.
Environment variables
| Variable | Default | Description |
|---|---|---|
SKIPPER_SPREADSHEET_ID |
— | Spreadsheet to read. Required to enable Skipper |
SKIPPER_CREDENTIALS_FILE |
— | Service-account JSON file (wins over GOOGLE_CREDS_B64) |
GOOGLE_CREDS_B64 |
— | Base64-encoded service-account JSON |
SKIPPER_SHEET_NAME |
first sheet | Primary sheet name |
SKIPPER_REFERENCE_SHEETS |
— | Comma-separated read-only sheets to merge in |
SKIPPER_MODE |
read-only |
Set to sync to enable sync mode |
SKIPPER_DEBUG |
— | Any non-empty value enables verbose debug logging |
SKIPPER_FAIL_OPEN |
true |
When the API is unreachable and no fresh disk cache exists, run all tests. Set to false to fail instead |
SKIPPER_CACHE_TTL |
300 |
Seconds the on-disk cache stays valid as an API fallback. 0 disables it |
SKIPPER_CACHE_FILE |
.skipper-cache.json |
Location of the on-disk cache |
SKIPPER_SYNC_ALLOW_DELETE |
false |
In sync mode, also delete rows of tests that no longer exist |
SKIPPER_WORKER_CACHE_FILE and SKIPPER_DISCOVERED_DIR are set by cargo skipper for the test binaries.
Test ID matching
- Paths are relative to the workspace root (the outermost
Cargo.tomlwith[workspace]), using/ - Title parts are separated by
> - IDs are case-insensitive and whitespace-collapsed for matching
Reference sheets
Inherit disabled tests from additional read-only sheets:
SKIPPER_REFERENCE_SHEETS=GlobalDisabled,FlakyTests
When the same test ID appears in multiple sheets, the most restrictive (latest) disabledUntil date wins.
CI example
- run: cargo install cargo-skipper
- name: Run tests
run: cargo skipper test --workspace
env:
SKIPPER_SPREADSHEET_ID: ${{ secrets.SKIPPER_SPREADSHEET_ID }}
GOOGLE_CREDS_B64: ${{ secrets.GOOGLE_CREDS_B64 }}