anthonyoteri / anthonyoteri/cargo-rigtest
Destructive test markers with profile-gated execution
- Linguagem predominante
- Rust
- Estrelas
- 0
- Forks
- 0
- Merge médio
- 3d 18h
- PRs com merge (30d)
- 8
Descrição
## Problem
Acceptance suites mix two kinds of tests: read-only verifications (does `/health` return 200, does login succeed) and tests that mutate state (creating records, deleting users, exercising write paths). Read-only tests are safe to run against any environment including production; destructive tests are not. Today rigtest has no way to express this distinction, which means either the suite is restricted to the lowest-common-denominator set of tests that are safe everywhere, or operators manually maintain separate test files per safety class.
## Proposed solution
Tests opt into a `destructive` marker, and profiles can opt *out* of permitting destructive writes. The runner refuses to execute destructive tests against profiles that have opted out.
```rust
#[testcase(destructive)]
async fn deletes_old_records(ctx: Arc) -> Result<(), BoxError> {
// ...
}
```
```toml
[profile.staging]
# destructive omitted — defaults to true (writes allowed)
[profile.prod]
destructive = false # explicit opt-out — read-only
```
**Default behavior:** the `destructive` field on a profile is **optional and defaults to `true`** (writes allowed). Profiles must explicitly opt out by setting `destructive = false`. This keeps the feature additive — existing profiles and tests continue to work without any config changes, and the marker only restricts behavior for users who have deliberately classified at least one profile as read-only.
When the active profile has `destructive = false`, marked tests appear in the run summary as `SKIP destructive (profile 'prod' is read-only)`. They aren't silently dropped — the operator sees exactly which tests were skipped and why.
The three policy layers stay orthogonal and compose cleanly:
| Layer | Question it answers | Default |
|---|---|---|
| `#[testcase(profiles = [...])]` | Which environments is this test valid against? | All profiles (no restriction) |
| `#[testcase(destructive)]` | Does this test mutate state? | Not destructive (no restriction) |
| `[profile.X] destructive = ...` | Does this environment allow mutation? | `true` (writes allowed) |
All three defaults are permissive. The feature only restricts behavior when the user explicitly opts into a restriction.
## Alternatives considered
Defaulting `destructive` to `false` (read-only) would be safer in the abstract but would break every existing suite the moment the field is introduced. The opt-out model trades a slightly less safe default for a non-breaking introduction, which matches rigtest's additive-feature-additions approach. Users who want safer defaults can set `destructive = false` on every profile and only flip it to `true` where needed — the same outcome, just expressed by the user rather than imposed by the framework.
A CLI flag (`--allow-destructive`) without per-profile policy would gate writes globally per run. Simpler but weaker: the operator has to remember the flag, and one suite invocation can't safely target a write-permitted profile and a read-only profile at the same time. The per-profile field puts the policy in the same place as the rest of the environment's truth, where it belongs.
A test-naming convention (`test_destructive_*` runs only with a flag) works but is brittle, invisible to tooling, and doesn't survive refactors.
## Additional context
Depends on the profiles feature (filed separately) — destructive policy is a field on the profile block, and the gate fires at runner time using the active profile's policy. The marker itself is a small addition to `TestCase`, made non-breaking by #40 (`#[non_exhaustive]`).
**Related issues:** #40 (`#[non_exhaustive]` — keeps adding the `destructive` field non-breaking), #41 (tags — orthogonal but often combined: `tags = ["smoke"], destructive`). Profiles feature issue to follow.
Guia de contribuição
Avaliação
Esta issue ainda não foi avaliada.