docs(knowledge-base): prune stale content and add agent-facing index (#7035)

This commit is contained in:
Zhengchao An
2026-09-02 08:26:59 +08:00
committed by GitHub
parent ceeff52229
commit 0a975f2fe2
99 changed files with 3312 additions and 10590 deletions
+49 -189
View File
@@ -1,87 +1,30 @@
# RustFS Testing
> **Owner: backlog#1153 (infra-11).** This file is the authoritative home for
> the test-layer taxonomy, naming conventions, and serial/nextest rules. The
> event × budget × required-status matrix is owned separately by
> `docs/testing/ci-gates.md` (backlog#1149 ci-15); this file links to it rather
> than duplicating counts, timeouts, or required-check names.
**Use this when:** you need to pick a test layer for a change, name a test so a gate keeps selecting it, understand why `#[serial]` does nothing under nextest, or handle a flaky test.
**Source of truth:** `.config/nextest.toml` (profiles, test-groups, quarantine), `.config/make/tests.mak` (`make test`), `.github/workflows/*.yml` (what runs when; matrix in [ci-gates.md](ci-gates.md)).
## Test taxonomy
RustFS layers its tests from cheap-and-narrow to expensive-and-broad. Higher
layers catch what lower layers cannot but cost more wall-clock and setup, so
each layer has a clear entry point and a clear "when". Pick the lowest layer
that can prove your change; add a higher-layer test only when the behaviour is
not observable below it.
Pick the lowest layer that can prove the change; add a higher-layer test only when the behaviour is not observable below it.
| Layer | What it covers | Entry command | When it runs |
| Layer | What it covers | Entry command | When it runs (details: [ci-gates.md](ci-gates.md)) |
|---|---|---|---|
| Unit & crate integration | Per-crate logic and in-process integration tests, run under nextest | `cargo nextest run --all --exclude e2e_test` (or `-p <crate>`) | Every PR (required) |
| ecstore black-box | Erasure-coded read/write/recovery validation of the ecstore stack | `scripts/run_ecstore_validation_suite.sh --profile quick` | Local / release validation (not in CI workflows) |
| e2e (`e2e_test` crate) | Full server spun up per test, driven over the S3 API | `cargo nextest run --profile e2e-smoke -p e2e_test` | PR smoke lane + scheduled full/nightly lanes |
| s3s-e2e conformance | External S3 conformance tool run against a live rustfs server | `./scripts/e2e-run.sh ./target/debug/rustfs /tmp/rustfs-e2e-data` | Per-PR e2e gate (`e2e-tests` job) |
| S3 compatibility | Third-party suites: `ceph/s3-tests` (boto3) and MinIO `mint` (many SDKs) | `scripts/s3-tests/run.sh` (mint: `.github/workflows/mint.yml`) | s3-tests: per-PR gate; mint: scheduled, report-only |
| Chaos / fault-injection | Multi-node, power-loss, and disk-fault harness | — (harness planned) | Planned — tracked in backlog#1100 |
| Fuzz | `cargo-fuzz` targets over untrusted parsing/validation surfaces | `./scripts/fuzz/run.sh` (or `cd fuzz && cargo +nightly fuzz run <target>`) | PR smoke + nightly corpus (`.github/workflows/fuzz.yml`) |
| Benchmarks | Criterion micro/throughput benchmarks | `cargo bench -p <crate>` | On-demand / local |
| Unit & crate integration | Per-crate logic and in-process integration tests | `cargo nextest run --all --exclude e2e_test` (or `-p <crate>`); `make test` wraps it | Every PR, required (`Test and Lint`, `ci` profile) |
| ecstore black-box | Erasure-coded read/write/recovery validation; profiles `quick` / `full` / `destructive` / `fuzz` | `scripts/run_ecstore_validation_suite.sh --profile quick` | Local and release validation only; not wired into any workflow. Contract: [ecstore-validation-suite-design.md](ecstore-validation-suite-design.md) |
| e2e (`e2e_test` crate) | A real `rustfs` binary per test, driven over the S3, admin, and protocol APIs | `cargo nextest run --profile e2e-smoke -p e2e_test` | PR: `e2e-smoke` (report-only); merge queue / main push: `e2e-full`; nightly: `e2e-repl-nightly`, `e2e-nightly`, `e2e-protocols`. Guide: [`crates/e2e_test/README.md`](../../crates/e2e_test/README.md) |
| s3s-e2e conformance | External S3 conformance tool against a live server | `./scripts/e2e-run.sh ./target/debug/rustfs <data-dir>` | PR, report-only (second half of the `End-to-End Tests` job) |
| S3 compatibility | `ceph/s3-tests` (boto3; allow-list `scripts/s3-tests/implemented_tests.txt`) and MinIO `mint` | `scripts/s3-tests/run.sh`; mint via `.github/workflows/mint.yml` | s3-tests: PR report-only plus a weekly full sweep; mint: weekly, report-only |
| Chaos / fault-injection | Single-node disk fault injection (`crates/e2e_test/src/chaos.rs`, `crates/e2e_test/src/fault_proxy.rs`) used by the reliability and heal e2e modules | Part of the e2e crate (`e2e-reliability` test-group) | With the `e2e-full` and nightly e2e lanes. A multi-node power-loss harness is not in tree |
| Fuzz | `cargo-fuzz` targets over untrusted parsing surfaces; isolated sub-workspace under `fuzz/` | `./scripts/fuzz/run.sh` (see [`fuzz/README.md`](../../fuzz/README.md)) | PR smoke on the paths listed in `.github/workflows/fuzz.yml`, plus nightly corpus |
| Benchmarks | Criterion benches under each crate's `benches/` | `cargo bench -p <crate>` | On demand; never a gate |
> The **When it runs** column is a qualitative pointer only. The authoritative
> event × timeout × required-status matrix lives in `docs/testing/ci-gates.md`
> (ci-15) — do not duplicate its numbers here.
Layer notes:
- **Unit & crate integration** — the primary gate. `make test` wraps
`cargo nextest run --all --exclude e2e_test`; CI runs the same set under the
strict `ci` profile (`.config/nextest.toml`). Requires `cargo-nextest` (see
[Serial execution & nextest profiles](#serial-execution--nextest-profiles)).
- **ecstore black-box** — `scripts/run_ecstore_validation_suite.sh` has four
profiles (`quick` / `full` / `destructive` / `fuzz`); `quick` is the
PR-smoke-sized core read/write/recovery pass. It is a local and
release-validation tool, not wired into a CI workflow.
- **e2e** — each test spawns its own single-node rustfs server on a random port
with an isolated temp dir, so the suite is parallel-safe. The `e2e-smoke`
profile is the single PR wiring mechanism; slow/cross-process suites run in
scheduled lanes (`e2e-repl-nightly`, and the reliability group). Full
contributor guide: [`crates/e2e_test/README.md`](../../crates/e2e_test/README.md);
per-module counts: [`e2e-suite-inventory.md`](e2e-suite-inventory.md).
- **s3s-e2e** — an external black-box conformance tool installed in CI; locally
run it against a freshly built binary with `scripts/e2e-run.sh <binary>
<data-dir>`.
- **S3 compatibility** — `ceph/s3-tests` exercises S3 semantics through boto3
and is gated on a committed allow-list
(`scripts/s3-tests/implemented_tests.txt`); MinIO `mint` runs many real
client SDKs and is report-only until suites pass reliably (see the header of
`.github/workflows/mint.yml`).
- **Chaos / fault-injection** — multi-node, power-loss, and disk-fault
scenarios are out of scope for this repo's in-tree suites; the harness is
tracked in backlog#1100. (Single-node disk-fault e2e tests already live in the
e2e crate's `e2e-reliability` group.)
- **Fuzz** — the `cargo-fuzz` harness is an isolated sub-workspace under
`fuzz/` (kept out of the root workspace on purpose); see
[`fuzz/README.md`](../../fuzz/README.md) for targets and corpus rules.
- **Benchmarks** — Criterion benches live under each crate's `benches/`. They
are not a gate; run them locally to compare before/after on a specific crate.
Script inventory: every entry under `scripts/` — including the runners above —
is indexed in [`scripts/README.md`](../../scripts/README.md) with its status
(ci-gate / dev-tool / archived) and wiring.
### Security advisory regression tests
Fixed GHSA advisories map to named, discoverable regression tests. The
advisory -> test map lives in
[`docs/testing/security-regressions.md`](security-regressions.md). sec-14
(backlog#1151) formalizes the written admission policy in `AGENTS.md`.
Every script named above is indexed with status and wiring in [`scripts/README.md`](../../scripts/README.md). Fixed GHSA advisories map to named regression tests in [security-regressions.md](security-regressions.md).
## Naming conventions
### Reserved test-name substrings (migration gate)
The migration-critical CI gate selects tests **by name substring** rather than
by module path, so a rename that drops the substring silently thins the gate.
These substrings are therefore reserved: keep them in the test name when a test
proves migration-critical behaviour.
`scripts/check_migration_gate_count.sh` (runs in `Test and Lint`) selects migration-critical tests by name substring and fails when the count drops below `.config/migration-gate-floor.txt`. A rename that drops a substring silently thins the gate, so these substrings are reserved:
| Substring | Guards |
|---|---|
@@ -91,138 +34,55 @@ proves migration-critical behaviour.
| `source_cleanup` | Post-migration source cleanup |
| `delete_marker` | Delete-marker handling across migration |
The gate is enforced by
[`scripts/check_migration_gate_count.sh`](../../scripts/check_migration_gate_count.sh),
which counts the selected tests and fails if the count drops below the committed
floor in
[`.config/migration-gate-floor.txt`](../../.config/migration-gate-floor.txt).
A deliberate reduction must lower the floor in the same PR, so the change is
reviewable in the diff (backlog#1153 infra-12). The substring list above is the
same one the gate uses; keep the two in sync when adding a reserved word.
A deliberate reduction lowers the floor in the same PR. The list above mirrors the script; change both together.
### General naming
- Name a regression test after what it pins: the issue or advisory number
(`..._regression_test`, `..._issue_NNNN_...`) or the invariant it protects, so
a reviewer can find the guard for a past bug by grepping.
- e2e lane membership is driven by test-name patterns in `.config/nextest.toml`
(for example the `_real_dual_node` / `_real_single_node` markers route
replication tests into the nightly lane). Follow the existing marker when
adding a test to an established suite; see
[`crates/e2e_test/README.md`](../../crates/e2e_test/README.md).
- Follow the Rust API Guidelines for symbol naming (see `AGENTS.md`).
- Name a regression test after what it pins (issue or advisory number, or the invariant) so `rg` finds the guard for a past bug.
- e2e lane membership is selected by test-name patterns in `.config/nextest.toml` (for example `_real_dual_node` / `_real_single_node` route replication tests to the nightly lane). Follow the existing marker of the suite you extend.
- Symbol naming follows the Rust API Guidelines (see `AGENTS.md`).
## Serial execution & nextest profiles
## nextest and `#[serial]`
**`cargo-nextest` is the runner.** `make test` requires it and CI installs it;
plain `cargo test` is not a faithful substitute because the two runners execute
tests differently.
`cargo-nextest` is the runner: `make test` requires it and CI installs it. nextest runs every test in its own process, so `serial_test`'s in-process `#[serial]` mutex does **not** serialize tests against each other; it only affects the plain `cargo test` fallback. Cross-test serialization under nextest comes from a `[test-groups]` entry with `max-threads = 1` in `.config/nextest.toml` (for example `ecstore-serial-flaky`, `e2e-reliability`) or from a `-j 1` lane. Prefer making tests self-isolating (per-test instance context, random port, own temp dir) over adding serialization. `RUSTFS_ALLOW_CARGO_TEST_FALLBACK=1 make test` runs plain `cargo test`; its results are not authoritative because `[test-groups]` do not apply.
- **Install:** `cargo install cargo-nextest --locked`, or a prebuilt binary
(faster) from <https://nexte.st/docs/installation/>.
- **Escape hatch:** `RUSTFS_ALLOW_CARGO_TEST_FALLBACK=1 make test` runs the
plain `cargo test` fallback, but the results are **not authoritative**
serialization semantics differ from CI and `[test-groups]` do not apply.
Time-driven tests use paused tokio time (`start_paused` plus `tokio::time::advance`) or explicit synchronization instead of fixed `sleep` windows.
**Why `#[serial]` is mostly a no-op under nextest.** nextest runs every test in
its own process, so `serial_test`'s in-process `#[serial]` mutex does **not**
serialize tests against each other. The mechanism that actually serializes
across nextest's process boundary is a nextest `[test-groups]` entry with
`max-threads = 1` (for example `ecstore-serial-flaky` and `e2e-reliability` in
`.config/nextest.toml`). Consequences:
### Profiles
- Do not add `#[serial]` expecting cross-test isolation under nextest. If two
tests genuinely share process/global state or a fixed external resource,
serialize them with a `[test-groups]` entry, or make each test self-isolating
(per-test instance context — see backlog#1153 infra-7 / infra-8).
- A large share of the repo's existing `#[serial]` markers only affect the
`cargo test` fallback. The serial-debt census and removal plan are tracked in
backlog#1153 infra-7.
All profiles are defined in `.config/nextest.toml`; its block comments hold the filters and rationale.
**Profiles** (all defined in `.config/nextest.toml`):
| Profile | Role |
|---|---|
| `default` | Local runs; never retries |
| `ci` | PR gate for everything except `e2e_test`; global `retries = 0` plus the quarantine list |
| `e2e-smoke` | PR subset of `e2e_test` |
| `e2e-full` | Merge-queue / main-push single-node e2e lane |
| `e2e-repl-nightly` | Nightly slow / cross-process replication lane |
| `e2e-nightly` | Nightly serial multi-process cluster fault lane |
| `e2e-protocols` | Nightly fixed-port FTPS/SFTP/WebDAV lane, run with `-j 1` |
- `default` — local runs. **Never retries**: a red test locally is a real
failure to investigate, not noise to retry away.
- `ci` — the strict CI gate: global `retries = 0` plus a narrowly-scoped
quarantine list (`retries = 2`) for tests with a tracked OPEN flake issue.
- `e2e-smoke` — the PR smoke subset of the `e2e_test` crate (the single wiring
mechanism for e2e in PR CI).
- `e2e-repl-nightly` — the scheduled slow/cross-process replication lane.
Membership of each e2e profile is pinned by a digest in `.config/e2e-<profile>-selection.txt` and checked by `scripts/check_test_wiring.py --check-profile <profile>` before the lane runs. To list what a profile selects on your platform (the result is platform-dependent because some modules are linux-only):
### Time control (paused vs real clock)
Time-driven tests should prefer paused time (`tokio::time` with `start_paused`
and `advance`) or explicit event synchronization over fixed `sleep` race
windows. The written convention and the `docs/testing/time-control.md` guide are
added by backlog#1153 infra-4.
## Coverage
Workspace line coverage is measured weekly. Pull requests that touch iam, kms,
policy, or crypto also run a non-required, report-only comparison against
`.config/coverage-baselines.toml`. During calibration, a regression is recorded
in the job summary without failing the job; missing or malformed coverage
evidence still fails closed (backlog#1153 infra-6).
- **CI**: `.github/workflows/coverage.yml` runs every Sunday and on manual
dispatch: `cargo llvm-cov nextest --workspace --exclude e2e_test` under the
`ci` nextest profile — the same scope and profile as the PR test gate. The
per-crate line-coverage table lands in the run's job summary; the lcov +
JSON exports are uploaded as a `coverage-lcov-<run>` artifact kept for
90 days. Scheduled failures open/append the `[scheduled-failure] coverage`
issue via the shared alert action (ci-8).
- **Local**: `make coverage` is the equivalent (slow — instrumented rebuild
plus the full suite). It prints the same per-crate table via
`scripts/coverage_per_crate.py` and writes `target/llvm-cov/lcov.info` and
`coverage.json`.
- **Security-critical ratchet**: relevant pull requests compare iam / kms /
policy / crypto line coverage with the versioned baseline. Drops greater than
the configured one-percentage-point calibration threshold are marked
`REGRESSION (report-only)`. The weekly summary runs the same comparison so
calibration continues even when no relevant pull request is open. Baseline
changes require a linked coverage run and a reviewed explanation.
- **Trend comparison**: each run's job summary is the weekly per-crate
snapshot — open two runs from the Actions history (workflow "coverage") and
compare their tables. For line-level diffs, download the two runs'
`coverage-lcov-*` artifacts and compare the `lcov.info` files with your lcov
tooling of choice.
- **Not measured**: doctests (ci.yml runs them uninstrumented; covering them
would require a nightly toolchain) and the `e2e_test` crate (excluded from
the unit gate; its lanes are described in the taxonomy above).
```bash
cargo nextest list -p e2e_test --profile e2e-smoke --message-format json \
| jq -r '.["rust-suites"][].testcases | to_entries[] | select(.value["filter-match"].status == "matches") | .key | split("::")[0]' \
| sort | uniq -c
```
## Flake policy
A flaky test is one that fails non-deterministically without a corresponding
code change. Flakes erode trust in the gate and block tightening required
checks, so they are handled on a strict, time-boxed loop.
A flaky test fails non-deterministically without a corresponding code change. Retry semantics live in `.config/nextest.toml`: `default` never retries, `ci` has global `retries = 0`, and only quarantined tests get `retries = 2` under `ci`. A quarantined test that passes on retry is marked `flaky` in `target/nextest/ci/junit.xml` (uploaded as a CI artifact); that marker, not a green check, is how a live flake stays visible.
**Retry semantics (source of truth: `.config/nextest.toml`):**
1. **Discover** — a non-deterministic failure (CI or local) or a `flaky` JUnit marker.
2. **Open an issue within 24h** describing symptom, suspected cause, and affected suite. No silent re-runs.
3. **Quarantine** — add a `[[profile.ci.overrides]]` entry with `retries = 2` and a comment linking exactly one OPEN issue. The current quarantine list is the `[[profile.ci.overrides]]` block in `.config/nextest.toml`.
4. **Fix or delete within 30 days** — make the test robust and remove the entry, or delete the test. An entry without a live OPEN issue link is a policy violation.
- The **local `default` profile never retries.** A red test on your machine is
a real failure to investigate, not noise to paper over.
- The **CI `ci` profile runs with global `retries = 0`.** A new race must fail
on its first occurrence so the first crime scene is never masked.
- Only tests on the **quarantine list** get `retries = 2`, and only under the
`ci` profile. Each quarantine entry MUST link exactly one OPEN issue.
- **JUnit flaky markers are the observable.** A quarantined test that passes
only after a retry is marked `flaky` in `target/nextest/ci/junit.xml`
(uploaded as a CI artifact). That marker — not a green check — is how we see
a flake is still live.
## Coverage
**Lifecycle of a flake:**
1. **Discover** — a test fails non-deterministically (CI or local), or shows a
`flaky` marker in the JUnit report.
2. **Open an issue within 24h** — file/track an issue describing the flake
(symptom, suspected cause, affected suite). No silent re-runs.
3. **Quarantine** — add the test to the quarantine override block in
`.config/nextest.toml` with a comment linking that OPEN issue. This grants
`retries = 2` under CI so the flake stops reddening unrelated PRs, while the
`flaky` marker keeps it visible.
4. **Fix or delete within 30 days** — make the test robust (then remove the
quarantine entry) or delete the test. A quarantine entry may not outlive its
fix window; an entry without a live OPEN issue link is a policy violation.
First quarantine members: the two backlog#937 ecstore groups
(`concurrent_resend_same_part_commits_one_generation` and
`store::bucket::tests::bucket_delete_*`).
- `.github/workflows/coverage.yml` measures workspace line coverage on its schedule and on manual dispatch: `cargo llvm-cov nextest --workspace --exclude e2e_test` under the `ci` profile, the same scope as the PR gate. The per-crate table lands in the job summary; lcov and JSON exports are uploaded as an artifact (retention set in the workflow).
- PRs touching the paths listed in `coverage.yml` also run a report-only comparison against `.config/coverage-baselines.toml` via `scripts/check_security_coverage.py`: a regression is recorded in the summary without failing the job; missing or malformed coverage evidence fails closed.
- `make coverage` (`.config/make/coverage.mak`) is the local equivalent; it writes `target/llvm-cov/lcov.info` and `coverage.json` and prints the same table via `scripts/coverage_per_crate.py`.
- Not measured: doctests (`ci.yml` runs them uninstrumented) and the `e2e_test` crate.
- A baseline change needs a linked coverage run and a reviewed explanation in the PR.
+68 -120
View File
@@ -1,154 +1,102 @@
# CI gate matrix
This file is the source of truth for which validation runs on each event, its
configured wall-clock budget, and whether it can block a merge. Test taxonomy,
naming, and nextest serialization rules remain in [README.md](README.md); e2e
membership and counts remain in
[e2e-suite-inventory.md](e2e-suite-inventory.md).
**Use this when:** a check is red and you need to know whether it blocks the merge, which workflow and job produced it, and how to reproduce it locally.
**Source of truth:** the live `main` ruleset (command below) for required status; `.github/workflows/<file>.yml` for triggers, `paths`, `timeout-minutes`, and cron; `.config/nextest.toml` for e2e profile filters; `.github/scheduled-validations.json` for the freshness-watchdog list.
The distinction between **required** and **report-only** is load-bearing:
a failing job blocks a merge only when its exact check name is present in the
live `main` ruleset. A workflow name, a `merge_group` trigger, or a red PR check
does not make a job required by itself.
A job blocks a merge only when its exact check name is in the live `main` ruleset. A workflow name, a `merge_group` trigger, or a red PR check does not make a job required by itself.
## Required merge checks
The live `main` ruleset (`6436880`) currently requires exactly these contexts:
The `main` ruleset (`6436880`) requires exactly these contexts, with `strict_required_status_checks_policy=false`:
| Required context | Producer | Validation |
|---|---|---|
| `CLA Check` | `.github/workflows/cla.yml` | Contributor agreement |
| `Quick Checks` | `.github/workflows/ci.yml` | Formatting and repository guard scripts |
| `Test and Lint` | `.github/workflows/ci.yml` | Clippy, workspace nextest excluding `e2e_test`, doctests, and migration proofs |
| `CLA Check` | `cla.yml` | Contributor agreement |
| `Quick Checks` | `ci.yml` job `quick-checks` | Formatting and repository guard scripts |
| `Test and Lint` | `ci.yml` job `test-and-lint` | Clippy, workspace nextest (`ci` profile, excluding `e2e_test`), doctests, migration-gate count (`scripts/check_migration_gate_count.sh`) |
For pull requests limited to the paths excluded by the main CI workflow,
`.github/workflows/ci-docs-only.yml` reports `Quick Checks` and
`Test and Lint` under the same names. It runs the real quick checks and the
planning-document guard; it does not claim that Rust compilation or runtime
tests ran. Despite the workflow name, these paths also include selected deploy,
workflow, and lock files.
For PRs limited to the `paths-ignore` list in `ci.yml`, `ci-docs-only.yml` reports `Quick Checks` and `Test and Lint` under the same names; it runs the quick checks and `scripts/check_no_planning_docs.sh`, not a Rust build or tests. `scripts/check_ci_paths_sync.sh` keeps the two path lists aligned.
Verify the live rule rather than trusting this snapshot before changing merge
policy:
Verify the live rule before changing merge policy:
```bash
gh api repos/rustfs/rustfs/rulesets/6436880 \
--jq '.rules[] | select(.type == "required_status_checks") | .parameters'
```
The ruleset currently has `strict_required_status_checks_policy=false`.
`Continuous Integration` accepts `merge_group` events and runs `e2e-full` for
them, but `End-to-End Tests (full merge gate)` is not currently a required
context. Therefore the repository is prepared to test a merge-queue SHA, but
the workflow alone does not prove that every merge passed that lane.
Promotion rule: never promote a report-only lane to required from one green run. Require at least 14 days and 30 representative PRs with at least 99% complete execution, then update the ruleset and this file together.
## Pull request and merge matrix
Budgets below are job `timeout-minutes`, not typical runtimes. “Report-only”
means the result is visible and actionable but is not in the live required
context list.
"Report-only" means visible and actionable but not in the required list. Budgets are each job's `timeout-minutes` in the named workflow and are not copied here.
| Event | Validation | Budget | Merge status | Reproduction |
|---|---|---:|---|---|
| PR, non-doc change | `Quick Checks` | 10 min | Required | `make pre-commit` (broader local umbrella) |
| PR, non-doc change | `Test and Lint` | 90 min | Required | `cargo nextest run --profile ci --all --exclude e2e_test` |
| PR, non-doc change | `Typos` | 10 min | Report-only | `typos` |
| PR, non-doc change | `ILM Integration (serial)` | 90 min | Report-only | Use the exact command in `.github/workflows/ci.yml` |
| PR, non-doc change | rio-v2 / swift / sftp test-and-lint variants | 90 min each | Report-only | `cargo nextest run` with the workflow's feature set |
| PR, non-doc change | `Build RustFS Debug Binary` | 30 min | Report-only; prerequisite for black-box lanes | `cargo build -p rustfs --bins` |
| PR, non-doc change | `io_uring Integration (real)` | 30 min | Report-only | `cargo test -p rustfs-ecstore --lib uring_ -- --test-threads=1 --nocapture` |
| PR, non-doc change | `End-to-End Tests` (`e2e-smoke` plus `s3s-e2e`) | 30 min | Report-only | `cargo nextest run --profile e2e-smoke -p e2e_test`; then `./scripts/e2e-run.sh ./target/debug/rustfs <data-dir>` |
| PR, non-doc change | `S3 Implemented Tests` | 60 min | Report-only | Build `rustfs`, then run `scripts/s3-tests/run.sh` with `DEPLOY_MODE=binary`, `TEST_MODE=single`, and `MAXFAIL=0` |
| PR, non-doc change | `S3 Lifecycle Behavior Tests` | 30 min | Report-only | Use the accelerated scanner environment in `.github/workflows/ci.yml` with `scripts/s3-tests/run.sh` |
| PR touching dependency or workflow inputs | Cargo Deny / Workflow Pin Report / Dependency Review | 20 / 5 / 30 min | Report-only | `cargo deny check`; `scripts/security/check_workflow_pins.sh` |
| PR touching architecture rules or architecture docs | `Architecture Migration Rules` | 10 min | Report-only | `scripts/check_architecture_migration_rules.sh` |
| PR touching Nix or workspace manifests | `Nix Build & Check` | 60 min | Report-only | `nix flake check` |
| PR limited to main-CI-excluded paths | companion `Quick Checks` and `Test and Lint` | 10 min each | Required | `git diff --check`; `make doc-paths-check` when documentation paths changed |
| `merge_group` | Standard CI plus `e2e-full` | 55 min for `e2e-full` | Standard required contexts only; `e2e-full` report-only | `cargo nextest run --profile e2e-full -p e2e_test` |
| Push to `main` | Standard CI plus `e2e-full` | 55 min for `e2e-full` | Post-merge detection | Same as `merge_group` |
| PR touching fuzz inputs or harness paths | Build plus five 60-second fuzz smoke targets | 60 min build; 30 min per target | Report-only | `MAX_TOTAL_TIME=60 ./scripts/fuzz/run.sh` |
| PR touching selected ecstore disk/format paths | `Rename Safety` on Windows | 60 min | Report-only | Run the four `cargo test -p rustfs-ecstore --lib <filter>` commands in `windows-filesystem.yml` on Windows |
| Event | Check name | Workflow / job | Merge status | Reproduce |
|---|---|---|---|---|
| PR, non-doc change | `Quick Checks` | `ci.yml` `quick-checks` | Required | `make pre-commit` |
| PR, non-doc change | `Test and Lint` | `ci.yml` `test-and-lint` | Required | `cargo clippy --all-targets -- -D warnings`; `cargo nextest run --profile ci --all --exclude e2e_test`; `cargo test --all --doc`; `scripts/check_migration_gate_count.sh` |
| PR, non-doc change | `Typos` | `ci.yml` `typos` | Report-only | `typos` |
| PR, non-doc change | `ILM Integration (serial)` | `ci.yml` `test-ilm-integration-serial` | Report-only | exact command in the job |
| PR, non-doc change | `Test and Lint (rio-v2)`, `Test and Lint (swift)`, `Test and Lint (sftp)` | `ci.yml` `test-and-lint-rio-v2`, `test-and-lint-protocols` | Report-only | `cargo nextest run` with the job's `--features` |
| PR, non-doc change | `Connect Short Credential Boundary` | `ci.yml` `connect-short-credential-boundary` | Report-only | `cargo test -p rustfs --test connect_registration --features connect-e2e-short-credentials`; `cargo check -p rustfs --release --features connect-e2e-short-credentials` must fail |
| PR, non-doc change | `Build RustFS Debug Binary` | `ci.yml` `build-rustfs-debug-binary` | Report-only; prerequisite for the black-box jobs | `cargo build -p rustfs --bins` |
| PR, non-doc change | `io_uring Integration (real)` | `ci.yml` `uring-integration` | Report-only | `cargo test -p rustfs-ecstore --lib uring_ -- --test-threads=1 --nocapture` |
| PR, non-doc change | `End-to-End Tests` | `ci.yml` `e2e-tests` | Report-only | `cargo nextest run --profile e2e-smoke -p e2e_test`, then `./scripts/e2e-run.sh ./target/debug/rustfs <data-dir>`; membership guards `scripts/check_test_wiring.py --check-profile e2e-smoke <listing.json>` and `scripts/check_security_smoke_count.sh check <listing.json>` |
| PR, non-doc change | `S3 Implemented Tests` | `ci.yml` `s3-implemented-tests` | Report-only | build `rustfs`, then `scripts/s3-tests/run.sh` with the job's `DEPLOY_MODE` / `TEST_MODE` / `MAXFAIL` env |
| PR, non-doc change | `S3 Lifecycle Behavior Tests` | `ci.yml` `s3-lifecycle-behavior-tests` | Report-only | `scripts/s3-tests/run.sh` with the job's accelerated-scanner env |
| PR touching `paths` in `audit.yml` | `Cargo Deny`, `Workflow Pin Report`, `Dependency Review` | `audit.yml` `cargo-deny`, `workflow-pin-report`, `dependency-review` | Report-only | `cargo deny check`; `scripts/security/check_workflow_pins.sh` |
| PR touching `paths` in `architecture-migration-rules.yml` | `Architecture Migration Rules` | `architecture-migration-rules.yml` `architecture-migration-rules` | Report-only | `scripts/check_architecture_migration_rules.sh` |
| PR touching `paths` in `nix.yml` | `Nix Build & Check` | `nix.yml` `nix-validation` | Report-only | `nix flake check` |
| PR touching `paths` in `fuzz.yml` | `Build Fuzz Harness`, `Smoke / <target>` | `fuzz.yml` `fuzz-build`, `pr-fuzz-smoke` | Report-only | `MAX_TOTAL_TIME=60 ./scripts/fuzz/run.sh` |
| PR touching `paths` in `windows-filesystem.yml` | `Rename Safety` | `windows-filesystem.yml` `rename-safety` | Report-only | the `cargo test -p rustfs-ecstore --lib <filter>` commands in the job, on Windows |
| PR touching `paths` in `coverage.yml` | `Workspace line coverage` | `coverage.yml` `coverage` | Report-only | `make coverage`; `python3 scripts/check_security_coverage.py target/llvm-cov/coverage.json` |
| PR touching `paths` in `e2e-upgrade.yml` | `Direct upgrade from rc.2` | `e2e-upgrade.yml` `direct-upgrade` | Report-only | the `cargo test --locked -p e2e_test` command in the job with `RUSTFS_UPGRADE_SOURCE_BINARY` pointing at the pinned previous release |
| PR touching `paths` in `oidc-keycloak.yml` | `OIDC Keycloak live gate` | `oidc-keycloak.yml` `oidc-keycloak-live` | Report-only | `cargo build --locked -p rustfs --bin rustfs`, then `bash scripts/test/oidc_keycloak_live.sh ./target/debug/rustfs` |
| PR touching `paths` in `targets-integration.yml` | `PostgreSQL, MySQL, AMQP, and NATS` | `targets-integration.yml` `targets-live` | Report-only | start the containers as in the job, export the `RUSTFS_TEST_*` DSNs, then the job's `cargo test --locked -p rustfs-targets --test <name> -- --ignored --test-threads=1` commands |
| PR limited to main-CI-excluded paths | `Quick Checks`, `Test and Lint` | `ci-docs-only.yml` `quick-checks`, `test-and-lint` | Required | `git diff --check`; `make doc-paths-check`; `scripts/check_no_planning_docs.sh` |
| `merge_group`; push to `main` | `End-to-End Tests (full merge gate)` | `ci.yml` `e2e-full` | Report-only | `cargo nextest run --profile e2e-full -p e2e_test` |
The authoritative e2e filters live in `.config/nextest.toml`; extend a profile
instead of adding a second ad-hoc selector. Before a profile runs,
`scripts/check_test_wiring.py` compares its exact membership to the committed
digest so a silent test drop fails closed.
e2e filters live in `.config/nextest.toml`; extend a profile instead of adding a second selector. Before a profile runs, `scripts/check_test_wiring.py` compares its listing to the committed digest in `.config/e2e-<profile>-selection.txt`, so a silent test drop fails closed.
## Scheduled and manual validation
## Scheduled validation
Scheduled lanes are independent fault domains. They do not block a pull
request, but their workflow-local gate can fail the run and scheduled failures
are routed to the shared failure-issue action. The scheduled-validation
watchdog and freshness workflow separately detect incomplete runs and missing
schedules.
Scheduled lanes never block a PR. Their workflow-local gate fails the run, scheduled failures route to the shared failure-issue action, and `scheduled-validation-freshness.yml` fails when a workflow listed in `.github/scheduled-validations.json` has not run within its `max_age_hours` (a `never_ran_grace_until` entry covers the window before a newly enabled cron's first slot). Cadence is qualitative here; the cron lives in each workflow's `on.schedule`.
| Cadence (UTC unless noted) | Workflow / validation | Budget | Verdict and artifacts | Reproduction |
|---|---|---:|---|---|
| Daily 02:17 | Fuzz: five nightly corpus targets | 60 min build; 60 min per target | Gate; corpus/crash artifacts, scheduled failure alert | `MAX_TOTAL_TIME=<seconds> ./scripts/fuzz/run.sh` |
| Dormant (cron 03:17 once re-enabled) | MinIO interop (EC + SSE read parity) | 40 min | Manually disabled in the Actions settings (backlog#1603) and therefore outside the freshness list; re-add it to `.github/scheduled-validations.json` when re-enabling | Follow the pinned Docker fixture steps in `minio-interop.yml` |
| Daily 04:29 | Replication / cluster-fault / protocol e2e | 45 / 90 / 90 min | Three independent gates; JUnit, membership, and server logs | `cargo nextest run --profile e2e-repl-nightly -p e2e_test`; `--profile e2e-nightly`; `-j 1 --profile e2e-protocols` |
| Daily 06:31 | Warp performance A/B | 180 min | Regression budget gate; A/B summaries and server logs | `bash scripts/run_hotpath_warp_abba.sh --help` |
| Daily 00:07 Asia/Shanghai (16:07 UTC previous day) | Nightly GNU build and Vault lanes | 150 / 90 / 60 min | Build, live Vault, and HA failover gates | Use the commands and pinned Vault images in `nightly-gnu.yml` |
| Daily 03:23 | Security Audit | 20 / 5 min, plus 30 min on PR dependency review | Cargo Deny and workflow-pin gates; scheduled failure alert | `cargo deny check`; `scripts/security/check_workflow_pins.sh` |
| Daily 23:47 | Scheduled Validation Freshness | 10 min | Fails when a critical schedule was never created or is stale; an entry may carry `never_ran_grace_until` to cover the window before a newly enabled cron's first slot | Dispatch `scheduled-validation-freshness.yml` |
| Sunday 00:11 | Full `Continuous Integration` matrix | Per-job budgets above | Weekly variant coverage, including dormant rio-v2 binary/e2e lanes | Dispatch `ci.yml` |
| Sunday 01:13 | Seven-platform build matrix | 150 min per platform | Build/package integrity; scheduled failure alert | Dispatch `build.yml` with an exact platform set |
| Sunday 02:19 | Ceph s3-tests full sweep: single and real four-node, four shards each | 180 min per shard | Compatibility gate; report, JUnit, exact node IDs, and server logs | `scripts/s3-tests/run.sh` against an existing single or distributed target |
| Sunday 06:41 | Mint | 120 min | **Report-only by design**; per-suite PASS/FAIL/NA and raw `log.json` | Reproduce the pinned Docker sequence in `mint.yml` or dispatch it |
| Sunday 07:43 | Workspace line coverage | 120 min | Report-only trend; lcov and JSON retained 90 days | `make coverage` |
| Monthly, day 1 06:37 | Runner Hygiene | 15 min | Validates runner ephemerality; scheduled failure alert | Dispatch `runner-hygiene.yml` |
| Workflow (cadence) | Jobs | Verdict and artifacts | In freshness list | Reproduce |
|---|---|---|---|---|
| `ci.yml` (weekly) | full matrix, including the schedule/dispatch-only rio-v2 jobs `build-rustfs-debug-binary-rio-v2` and `e2e-tests-rio-v2` | per-job | yes | dispatch `ci.yml` |
| `build.yml` (weekly) | `build-rustfs` over the six-target platform matrix in `prepare-platform-matrix` (four Linux, macOS aarch64, Windows x86_64) | build/package integrity | yes | dispatch `build.yml` with an exact platform set |
| `e2e-replication-nightly.yml` (nightly) | `repl-nightly`, `cluster-nightly`, `protocols-nightly` | three independent gates; JUnit, membership listing, server logs | yes | `cargo nextest run --profile e2e-repl-nightly -p e2e_test`; `--profile e2e-nightly`; `-j 1 --profile e2e-protocols` |
| `e2e-s3tests.yml` (weekly) | `s3tests` (single and distributed, four shards each), `upstream-head-canary` | compatibility gate; report, JUnit, node IDs, server logs | yes | `scripts/s3-tests/run.sh` against an existing single or distributed target |
| `fuzz.yml` (nightly) | `nightly-fuzz-corpus` per target | gate; corpus and crash artifacts | yes | `MAX_TOTAL_TIME=<seconds> ./scripts/fuzz/run.sh` |
| `minio-interop.yml` (nightly) | `minio-interop` | EC + SSE read-parity gate | yes, with `never_ran_grace_until` | pinned Docker fixture steps in the workflow |
| `performance-ab.yml` (nightly) | `warp-ab` | regression-budget gate; A/B summaries and server logs | yes | `bash scripts/run_hotpath_warp_abba.sh --help` |
| `nightly-gnu.yml` (nightly) | `build`, `kms-vault-lane`, `kms-vault-ha-failover` | build, live Vault, and HA failover gates | yes | commands and pinned Vault images in the workflow |
| `audit.yml` (nightly) | `cargo-deny`, `workflow-pin-report` | dependency and workflow-pin gates | yes | `cargo deny check`; `scripts/security/check_workflow_pins.sh` |
| `mint.yml` (weekly) | `mint` | report-only by design; per-suite PASS/FAIL/NA and raw `log.json` | yes | pinned Docker sequence in the workflow |
| `coverage.yml` (weekly) | `coverage` | report-only trend; lcov and JSON artifact | yes | `make coverage` |
| `runner-hygiene.yml` (monthly) | `check-ephemerality` | runner ephemerality | yes | dispatch |
| `e2e-upgrade.yml` (weekly) | `direct-upgrade` | upgrade gate; server logs | no | see the PR row |
| `oidc-keycloak.yml` (weekly) | `oidc-keycloak-live` | live OIDC gate | no | see the PR row |
| `targets-integration.yml` (nightly) | `targets-live` | live target gate; container logs | no | see the PR row |
| `scheduled-validation-freshness.yml` (nightly) | `check-freshness` | fails on a never-created or stale schedule | n/a | dispatch |
Manual `workflow_dispatch` exists for the scheduled workflows above. Manual
runs are debugging evidence and intentionally do not open scheduled-failure
issues. A manual performance run may explicitly allow a known regression; that
override must not be treated as an ordinary passing baseline.
Manual `workflow_dispatch` runs are debugging evidence and do not open scheduled-failure issues. A manual performance run may explicitly allow a known regression; that override is not a passing baseline.
## Release validation
Release validation is post-merge and tag-driven; it does not substitute for a
pull-request gate.
Post-merge and tag-driven; not a substitute for a PR gate.
| Event | Validation | Budget | Result |
|---|---|---:|---|
| Push to `main` or weekly schedule | `Build and Release` platform matrix | 150 min per platform | Build artifacts for all selected targets; no release publication on a main push |
| Valid release or preview tag | `Build and Release` plus asset checks | 150 min per platform | Draft release, checksummed assets, and publish step |
| Successful non-preview release-tag build | Docker image build and image scan | 60 min build; 30 min scan | Multi-architecture images plus vulnerability report |
| Successful release-tag build | DEB/RPM packaging | 30 min per architecture | Packages and checksum files uploaded to the release |
| Successful non-preview release-tag build | Helm template test and package | 30 min build; 30 min publish | Versioned chart and repository index |
| Trigger | Workflow / job | Result |
|---|---|---|
| Push to `main`, weekly schedule, dispatch | `build.yml` `build-rustfs` (a development build on a main push restricts the matrix to the Linux targets) | build artifacts; no release publication |
| Valid release or preview tag | `build.yml` `build-rustfs`, `create-release`, `upload-release-assets`, `publish-release` | draft release, checksummed assets, publish |
| Successful non-preview release-tag build (`workflow_run`) | `docker.yml` `build-docker`, `scan-docker-image` | multi-architecture images and vulnerability report |
| Successful release-tag build (`workflow_run`) | `package.yml` `package` | DEB/RPM packages and checksums uploaded to the release |
| Successful non-preview release-tag build (`workflow_run`) | `helm-package.yml` `build-helm-package`, `publish-helm-package` | versioned chart and repository index |
| Final tag's release published | `build.yml` `cleanup-preview-releases` | deletes every `<target>-preview.<N>` Release for that target; the tags are kept |
Use an exact preview tag for end-to-end release rehearsal. Manual dispatches
are backfill/debug paths and do not prove the automatic `workflow_run` chain.
A preview Release is internal validation state, not a deliverable: after the
final tag's release is published, `cleanup-preview-releases` deletes every
`<target>-preview.<N>` Release for that target. The tags themselves are kept, so
the validated commit stays traceable.
## Evidence requirements
A green check is useful only when it proves the intended behavior ran:
- Record the exact commit SHA and run URL.
- Separate product failure from runner prerequisites, service readiness, and
cancellation. Repair the precondition, then rerun the exact workload.
- Preserve membership manifests, JUnit, raw compatibility logs, seeds, and
server logs where the workflow provides them.
- For a bug fix or a new fault checker, provide sensitivity evidence: the old
behavior or an intentional mutation must fail the new oracle, and the fixed
behavior must pass it.
- Never promote a report-only lane to required from one green run. Require at
least 14 days and 30 representative pull requests with at least 99% complete
execution, then update the ruleset and this table together.
Use an exact preview tag for an end-to-end release rehearsal. Manual dispatches are backfill/debug paths and do not prove the automatic `workflow_run` chain.
## Change checklist
Update this file in the same pull request when any of these change:
- workflow triggers, job names, timeouts, or nextest profile ownership;
- required status contexts or strict/merge-queue policy;
- scheduled cadence, alert routing, artifact contract, or local reproduction;
- report-only versus gating semantics.
Do not copy per-module test counts here. Update
[e2e-suite-inventory.md](e2e-suite-inventory.md) and its enforced membership
digest instead.
Update this file in the same PR when a job or check name changes, a workflow gains or loses a `pull_request` or `schedule` trigger, required contexts or strict/merge-queue policy change, report-only vs gating semantics change, or `.github/scheduled-validations.json` membership changes. Do not copy timeouts, crons, or test counts here.
-107
View File
@@ -1,107 +0,0 @@
# e2e_test suite inventory
> Authoritative per-module test counts for the `e2e_test` crate (backlog#1149
> ci-4), generated from `cargo nextest list -p e2e_test`. Regenerate with:
> ```bash
> cargo nextest list -p e2e_test --message-format json | jq -r '.["rust-suites"][]?.testcases | to_entries[] | select(.value.ignored == false) | .key | split("::")[0]' | sort | uniq -c
> ```
> Modules marked ✅ are in the PR smoke profile `e2e-smoke`; 🌙 marks the
> cluster, protocol, and replication subsets in the consolidated nightly
> workflow. The `e2e-full` merge/main profile covers the remaining default
> single-node tests. Committed test-ID digests are enforced before each run.
> Note: counts exclude `#[ignore]`d tests (nextest lists them separately).
> Managed-SSE (SSE-S3/SSE-KMS) replication contracts assert successful
> re-encryption on the target (backlog#1783); SSE-C replication still pins a
> fail-closed FAILED contract until ciphertext passthrough lands.
| module | tests | PR smoke |
|---|---|---|
| admin_auth_test | 4 | ✅ |
| admin_iam_crud_test | 3 | ✅ |
| admin_mfa_test | 4 | |
| admin_pools_test | 1 | ✅ |
| admin_timeout_regression_test | 1 | 🌙 |
| anonymous_access_test | 4 | ✅ |
| api_rate_limit_test | 3 | |
| archive_download_integrity_test | 13 | |
| bucket_logging_test | 3 | |
| bucket_policy_check_test | 1 | ✅ |
| bucket_stats_regression_test | 3 | |
| chaos | 2 | |
| checksum_upload_test | 7 | |
| cluster_concurrency_test | 3 | 🌙 |
| cluster_multidrive_pool_test | 4 | 🌙 |
| common | 17 | |
| compression_test | 6 | ✅ |
| connection_cap_test | 2 | |
| console_smoke_test | 1 | ✅ |
| content_encoding_test | 3 | ✅ |
| copy_object_checksum_test | 7 | |
| copy_object_metadata_test | 4 | ✅ |
| copy_object_tagging_test | 2 | ✅ |
| copy_object_version_restore_test | 2 | |
| copy_source_invalid_date_test | 1 | ✅ |
| create_bucket_region_test | 2 | ✅ |
| data_usage_test | 2 | |
| degraded_listing_availability_test | 1 | 🌙 |
| degraded_read_eof_regression_test | 3 | |
| delete_marker_migration_semantics_test | 2 | ✅ |
| delete_object_no_content_length_test | 1 | |
| delete_objects_versioning_test | 2 | ✅ |
| delete_regression_test | 5 | |
| distributed_startup_regression_test | 3 | |
| existing_object_tag_policy_test | 4 | |
| fake_s3_target | 6 | ✅ |
| fault_proxy | 7 | |
| get_codec_streaming_compat_test | 1 | |
| get_stream_failure_observability_test | 1 | |
| group_delete_test | 4 | |
| head_object_consistency_test | 1 | ✅ |
| head_object_range_test | 1 | ✅ |
| heal_erasure_disk_rebuild_test | 6 | 🌙 |
| inline_fast_path_cluster_test | 16 | |
| internode_rpc_signature_e2e_test | 5 | |
| kms | 50 | |
| leading_slash_key_test | 2 | ✅ |
| lifecycle_regression_test | 4 | |
| list_buckets_auth_test | 1 | ✅ |
| list_buckets_double_slash_test | 3 | ✅ |
| list_buckets_iam_filter_test | 1 | ✅ |
| list_object_versions_metadata_extension_test | 1 | |
| list_object_versions_regression_test | 2 | ✅ |
| list_objects_duplicates_test | 3 | ✅ |
| list_objects_v2_metadata_extension_test | 1 | |
| list_objects_v2_pagination_test | 12 | ✅ |
| listing_regression_test | 4 | |
| mc_mirror_small_bucket_test | 1 | |
| multipart_auth_test | 75 | |
| multipart_storage_class_test | 3 | ✅ |
| namespace_lock_quorum_test | 2 | 🌙 |
| negative_sigv4_test | 7 | ✅ |
| notification_startup_regression_test | 2 | |
| notification_webhook_test | 3 | ✅ |
| object_lambda_test | 16 | 🌙 |
| object_lock | 34 | |
| overwrite_cleanup_regression_test | 1 | |
| policy | 6 | |
| presigned_negative_test | 7 | ✅ |
| protocols | 16 | 🌙 |
| quota_test | 14 | |
| reliability_disk_fault_test | 4 | |
| reliant | 43 | 20 ✅ |
| replication_extension_test | 76 | 20 ✅ +56 🌙 |
| replication_lww_receiver_test | 1 | |
| security_boundary_test | 4 | |
| server_startup_failfast_test | 1 | |
| snowball_auto_extract_test | 6 | |
| special_chars_test | 14 | ✅ |
| ssec_copy_test | 2 | ✅ |
| stale_multipart_cleanup_cluster_test | 1 | 🌙 |
| storage_class_capability_test | 4 | ✅ |
| sts_query_compat_test | 6 | ✅ |
| tier_transition_regression_test | 3 | |
| tls_gen | 3 | |
| tls_hot_reload_test | 1 | ✅ |
| version_id_regression_test | 10 | ✅ |
**Total listed: 622 tests across 86 modules · PR smoke: 165 tests / 36 modules · merge/main full: 495 tests / 77 modules · nightly replication: 56 tests · nightly cluster faults: 32 tests / 7 modules · nightly protocols: 16 tests** · updated 2026-08-31.
+131 -339
View File
@@ -1,294 +1,62 @@
# ECStore Validation Suite Design
# ECStore Validation Suite
This document defines the validation suite RustFS should use before claiming
ECStore erasure-coding correctness, durability, and fault-resilience coverage.
It is a suite design, not a claim that all tests already exist.
**Use this when:** you run or extend `scripts/run_ecstore_validation_suite.sh`, or add an erasure-coding test and need the scenario row it must satisfy.
**Source of truth:** `scripts/run_ecstore_validation_suite.sh` (profiles, flags, step commands, coverage scope, default thresholds); `docs/architecture/erasure-coding.md` (the invariants the rows enforce).
## Goal
Provide one command that runs the full ECStore confidence suite and produces a
single pass/fail result plus artifacts. The command should exercise both:
- white-box invariants in `rustfs-ecstore`, `rustfs-filemeta`, and related
helpers;
- black-box S3 and admin behavior against real single-node and distributed
erasure deployments.
The suite can reduce release risk; it cannot prove the absence of all defects.
It must therefore combine deterministic matrices, negative tests, fuzz/corpus
tests, chaos tests, and explicit artifact review.
## Proposed Entry Point
Use the top-level runner for repeatable local, CI, and release checks:
## Runner contract
```bash
scripts/run_ecstore_validation_suite.sh --profile full
scripts/run_ecstore_validation_suite.sh --profile <quick|full|destructive|fuzz> \
[--out-dir <path>] [--skip-e2e] [--skip-s3-tests] [--skip-coverage] [--require-fixtures] \
[--unit-coverage-min <percent>] [--unit-coverage-scope <ec-critical|crate>] [--dry-run]
```
Profiles:
The runner is a local and release-validation tool; no CI workflow invokes it.
| Profile | Purpose | Expected cost |
| --- | --- | --- |
| `quick` | PR smoke for EC logic and existing single-node reliability tests. | minutes |
| `full` | Release gate: white-box, e2e, chaos, S3 compatibility subset, coverage. | hours |
| `destructive` | Manual/nightly gate: distributed 4-node/16-disk, crash/restart, rebalance/decommission fault injection. | hours+ |
| `fuzz` | Malformed metadata/RPC/corpus fuzzing with fixed seed output. | bounded by budget |
| Profile | Steps (each profile includes the one above it) | Cost |
|---|---|---|
| `quick` | `rustfs-filemeta` lib tests; focused `rustfs-ecstore` lib filters (`erasure`, `set_disk::read`, `set_disk::core::io_primitives`, the rename-rollback test, `disk::local`); the two isolated global-state tests named at the top of the runner; the whole `rustfs-ecstore --lib` with `--test-threads=1`; e2e `reliability_disk_fault_test`, `heal_erasure_disk_rebuild_test`, `namespace_lock_quorum_test` | minutes |
| `full` | fixture gate; legacy-bitrot and MinIO generated-read fixture tests; s3-tests subset `TESTEXPR="multipart or range or versioning or delete"` with `DEPLOY_MODE=build MAXFAIL=0`; unit coverage gate | hours |
| `destructive` | `disk::local::test::crash_consistency` (power loss at each pre-commit step; object reopens as old or new, never mixed); e2e `cluster_concurrency_test`, `stale_multipart_cleanup_cluster_test`, `delete_marker_migration_semantics_test` | hours+ |
| `fuzz` | `scripts/fuzz/run.sh` bounded by `MAX_TOTAL_TIME` (default in the runner) | bounded |
The current runner wires the existing high-signal checks. The matrix below
still tracks required follow-up coverage before `full` can be treated as a
complete release gate.
Artifacts under `<out-dir>` (default `target/ecstore-validation/<timestamp>/`):
The runner writes artifacts under
`target/ecstore-validation/<timestamp>/`:
| File | Content |
|---|---|
| `run-metadata.env` | profile, flags, thresholds, environment |
| `summary.tsv` | one row per step: pass / fail / skip with reason |
| `blackbox-matrix.tsv` | every black-box and fixture row with command, fixture env, and status `enabled` / `disabled` / `missing-optional` / `missing-required`; written on every invocation regardless of profile |
| `logs/*.log` | per-step transcripts |
| `coverage/ecstore/lcov.info`, `summary.tsv`, `files.tsv` | coverage export, gate summary, per-file table (`full` and `destructive`) |
- command transcript and environment;
- randomized seeds;
- object manifests with SHA256;
- per-disk layout snapshots before and after mutation;
- server logs;
- junit/nextest output;
- `blackbox-matrix.tsv` with selected black-box and fixture gates;
- coverage report;
- failure reproducer instructions.
Fixture rows:
## Acceptance Rules
| Row | Profile | Fixture env |
|---|---|---|
| legacy bitrot read (`crates/ecstore/tests/legacy_bitrot_read_test.rs`) | `full` | `RUSTFS_LEGACY_TEST_ROOT`, `RUSTFS_LEGACY_TEST_DISK` |
| MinIO generated encrypted read and negative restore (`storage::minio_generated_read_test` in `rustfs/src/storage/mod.rs`, `--features rio-v2`, `--ignored`) | `full` | `RUSTFS_MINIO_FIXTURE_ROOT`, `RUSTFS_MINIO_STATIC_KMS_KEY_B64` |
The suite passes only when all selected profile commands pass and every
scenario asserts both API-visible behavior and on-disk state where applicable.
A missing fixture is recorded as a `missing-optional` skip. `--require-fixtures` turns it into an early `ecstore-fixture-gate` failure before the expensive black-box steps run.
Required fail-closed rules:
## Acceptance rules
A run passes only when every selected step passes. Every scenario asserts API-visible behaviour and, where applicable, on-disk state; a test that only checks constants, helper calls, deleted branches, or implementation details does not satisfy a row.
Fail-closed invariants every row enforces:
- never return corrupted object bytes;
- never silently accept forged or split-brain metadata;
- never downgrade write quorum to read quorum;
- never leave a mixed old/new object after partial commit;
- never leave a mixed old/new object after a partial commit;
- never panic on malformed EC metadata;
- return typed errors or quorum failures for invalid states.
Unit-test coverage is a hard release-gate input: `rustfs-ecstore` unit line
coverage for the EC-critical scope must be at least 95%, with 100% as the
target for EC read, write, decode, heal, metadata quorum, and rollback paths.
A lower threshold is only acceptable for a temporary, explicitly documented
exception tied to missing testability or unreachable code. Full-crate coverage
is still reported as an observation metric, but it must not hide EC regressions
behind unrelated modules.
Fault injection is explicit and deterministic: local disk mocks for unit tests, process-level disk manipulation (`crates/e2e_test/src/chaos.rs`) for e2e tests. Property tests replay a fixed seed for payload, range, and missing-shard selection.
Tests must not pass by only checking constants, helper calls, deleted branches,
or implementation details. Every test needs a reader-facing or storage-state
assertion.
### Coverage gate
Fixture-backed checks are optional for local smoke runs, but explicit in the
artifact stream. `--require-fixtures` turns missing legacy or MinIO generated
fixtures into an early `ecstore-fixture-gate` failure before expensive black-box
steps run. The MinIO generated fixture gate requires both
`RUSTFS_MINIO_FIXTURE_ROOT` and `RUSTFS_MINIO_STATIC_KMS_KEY_B64`.
## White-Box Matrix
### Erasure Algorithm
Target files:
- `crates/ecstore/src/erasure/coding/erasure.rs`
- `crates/ecstore/src/erasure/coding/encode.rs`
- `crates/ecstore/src/erasure/coding/decode.rs`
- `crates/ecstore/src/erasure/coding/decode_reader.rs`
- `crates/ecstore/src/erasure/codec/bridge.rs`
Coverage:
| Area | Scenarios | Assertions |
| --- | --- | --- |
| Shard geometry | legacy/current shard-size formulas; lengths `0`, `1`, `block-1`, `block`, `block+1`, multi-block tail | no divide-by-zero; shard/file/range offsets match expected |
| Encode/decode | `(data, parity)` sets `2+2`, `4+2`, `8+8`; random payloads; missing shards up to parity | reconstructed data equals original |
| Negative reconstruction | missing shards above parity; inconsistent shard lengths; corrupt surplus parity | typed error, no partial success |
| Source verification | missing data shard plus extra parity source | rebuilt parity must match source parity |
| Legacy compatibility | old shard formula and legacy checksum data | legacy files decode and heal correctly |
| Streaming decode | legacy engine vs RustFS codec engine on same stripe stream | bytes and errors are equivalent |
| Range output | head/middle/tail/suffix; cross-block and final-short-stripe ranges | exact byte range, no over-read/under-read |
Add property tests with fixed replay seeds for payload, range, and missing-shard
selection.
### Bitrot and Reader Alignment
Target files:
- `crates/ecstore/src/erasure/coding/bitrot.rs`
- `crates/ecstore/src/erasure/coding/decode.rs`
- `crates/ecstore/src/set_disk/core/io_primitives.rs`
- `crates/ecstore/src/set_disk/shard_source.rs`
Coverage:
| Area | Scenarios | Assertions |
| --- | --- | --- |
| Hash framing | valid hash+data; wrong hash; truncated hash; truncated data | invalid data never succeeds |
| Short shard | short read under normal hash, `skip_verify`, and hash-none | `UnexpectedEof` or equivalent typed error |
| Lockstep reads | mid-stream data shard failure; pending/timeout reader; final short stripe | each live reader advances exactly one stripe; failed reader retires |
| Adaptive reads | hedged parity fallback and timeout retirement | no shard desync; reconstructed bytes match original |
| Shard source order | out-of-order read completion and missing slots | slots resolve by shard index |
| Deferred readers | data-blocks-first setup opens deferred parity at correct offset | parity fallback uses aligned data |
Use instrumented readers that record shard index, stripe index, read count,
offset, and retirement reason.
### Metadata, Quorum, and Commit Atomicity
Target files:
- `crates/filemeta/src/filemeta/version.rs`
- `crates/ecstore/src/set_disk/read.rs`
- `crates/ecstore/src/set_disk/metadata.rs`
- `crates/ecstore/src/set_disk/ops/object.rs`
- `crates/ecstore/src/set_disk/core/io_primitives.rs`
- `crates/ecstore/src/disk/local.rs`
Coverage:
| Area | Scenarios | Assertions |
| --- | --- | --- |
| Metadata tamper | same `version_id`/`mod_time`, divergent data dir, parts, ETag, size, checksum, inline flag, erasure distribution | previous committed version or read quorum error; no arbitrary latest |
| Early stop | valid quorum, stale quorum, corrupt trailing disks, slow trailing disks | early-stop only on safe identity |
| Quorum downgrade | read quorum vs write quorum; delete marker quorum; version-not-found quorum | no mutation below write quorum |
| Rename atomicity | failure before data rename, after data rename, after metadata rename, cleanup failure | object is old or new; never mixed |
| Rollback | failed commit quorum and stale temp data | rollback preserves old metadata/data |
| Malformed metadata | oversized lengths, bad CRC, invalid versions, invalid UUID/timestamp/enum, huge parts | bounded memory; typed error; no panic |
Fault injection should be explicit and deterministic, preferably through local
disk mocks for unit tests and process-level disk manipulation for e2e tests.
## Black-Box Matrix
Use `crates/e2e_test` for real S3/admin behavior and extend
`crates/e2e_test/src/chaos.rs` rather than duplicating ad hoc helpers.
The current runner emits `blackbox-matrix.tsv` for every invocation. It is a
machine-readable manifest of selected black-box scenarios, their commands,
required fixture environment, and whether each row is enabled, disabled, or
missing an optional/required fixture.
Current runner rows:
| Profile | Scenario | Gate | Fixture env |
| --- | --- | --- | --- |
| `quick` | single-node disk fault read/write | e2e black box | none |
| `quick` | degraded erasure disk rebuild | e2e black box | none |
| `quick` | namespace lock quorum under EC ops | e2e black box | none |
| `full` | legacy bitrot read fixture restore | fixture black box | `RUSTFS_LEGACY_TEST_ROOT`, `RUSTFS_LEGACY_TEST_DISK` |
| `full` | MinIO generated encrypted read and negative restore fixture | fixture black box | `RUSTFS_MINIO_FIXTURE_ROOT`, `RUSTFS_MINIO_STATIC_KMS_KEY_B64` |
| `full` | S3 multipart/range/versioning/delete subset | S3 black box | none |
| `destructive` | distributed cluster concurrency | e2e black box | none |
| `destructive` | stale multipart cleanup cluster | e2e black box | none |
| `destructive` | delete marker migration semantics | e2e black box | none |
### Single-Node 4-Disk EC
| Scenario | Required assertions |
| --- | --- |
| baseline PUT/GET/HEAD/List for tiny, inline, block-boundary, multi-block, multipart objects | SHA256 manifest matches; metadata is consistent on all disks |
| one disk offline during read | existing objects readable; no corrupted bytes |
| one disk offline during write | write succeeds only when write quorum holds; restored disk is healed |
| above-parity disk loss | GET/PUT fails with quorum error; no partial bytes accepted |
| corrupt data shard and parity shard | GET returns original bytes or fails closed; read-repair/heal restores |
| corrupt inline `xl.meta` | fail closed or heal; no forged inline data |
| range read with offline/corrupt shard | exact range bytes; invalid ranges produce expected S3 errors |
| multipart part resend and concurrent same-part writes | final complete object matches chosen committed parts |
| crash during multipart complete/put/delete | after restart only old or new full version is visible |
Existing anchors:
- `crates/e2e_test/src/reliability_disk_fault_test.rs`
- `crates/e2e_test/src/heal_erasure_disk_rebuild_test.rs`
- `crates/e2e_test/src/chaos.rs`
### Distributed 4-Node / 16-Disk EC
| Scenario | Required assertions |
| --- | --- |
| node/disk outage while reading large objects | no EOF/truncation; SHA256 manifest matches |
| write while one remote node is down | write follows quorum; later heal reconstructs remote disk |
| remote shard bitrot | degraded read uses clean shards; no bad bytes |
| concurrent GET/PUT/DELETE/List on same key | no 500 for expected conflicts; no dirty reads |
| range GET matrix for large objects | sequential and parallel ranges match full-object hash |
| internode timeout/slow disk | typed error or fallback; no desync |
This layer is mandatory because single-process unit tests cannot prove RPC,
HTTP/2, timeout, and distributed quorum behavior.
### Versioning, Delete Markers, and Migration
| Scenario | Required assertions |
| --- | --- |
| latest delete marker | GET/HEAD/ListObjectVersions match S3 semantics |
| explicit `versionId` for old versions | exact old bytes and metadata |
| suspended/null version | no version ordering regression |
| delete marker during heal/rebalance/decommission | marker visibility and history are preserved |
| orphan directory cleanup | real objects are not purged; tombstones behave correctly |
### Heal, Rebalance, and Decommission
| Scenario | Required assertions |
| --- | --- |
| auto heal and admin deep heal | data hash unchanged; `xl.meta` and format data rebuilt |
| heal interruption/restart | idempotent recovery; no dangling temp objects |
| two-pool rebalance with versioned/multipart objects | source and target pools have consistent versions |
| decommission cancel/restart/finalize | target readable; source cleanup safe |
| rebalance/decommission with node outage | progress resumes; no duplicate or missing versions |
Existing scripts under `scripts/test/decommission_*.sh` should be wrapped into
the `destructive` profile only after they emit machine-readable pass/fail
artifacts.
## S3 Compatibility and Large Object Gates
The full profile should include a targeted S3 compatibility subset, not the
entire compatibility suite by default:
```bash
TESTEXPR="multipart or range or versioning or delete" \
DEPLOY_MODE=build \
MAXFAIL=0 \
./scripts/s3-tests/run.sh
```
Large object gates:
- `scripts/run_get_codec_streaming_smoke.sh` for legacy/codec GET parity;
- `scripts/run_gt1g_get_http_matrix.sh` for sequential and parallel range GET;
- `scripts/run_gt1g_multipart_put_matrix.sh` for multipart PUT paths.
These should be artifact-producing optional stages in `full` or `destructive`
profiles, not hidden local-only commands.
## Fuzz and Corpus Gates
Add bounded fuzz/corpus tests for:
- `xl.meta` MessagePack and legacy filemeta versions;
- protobuf/RPC payload decoding;
- checksum and bitrot headers;
- range offset/length overflow;
- object names with path traversal, encoded separators, and symlink components;
- huge inline metadata and huge part counts.
Each corpus failure must save the input bytes and the minimized reproducer under
the suite artifact directory.
## Coverage Snapshot Target
The runner enforces unit line coverage for `rustfs-ecstore` in `full` and
`destructive` profiles. The default threshold is 95%, and the target remains
100% for EC-critical paths:
```bash
scripts/run_ecstore_validation_suite.sh --profile full --unit-coverage-min 95
scripts/run_ecstore_validation_suite.sh --profile full --unit-coverage-min 100
scripts/run_ecstore_validation_suite.sh --profile full --unit-coverage-scope crate
```
The default hard gate scope is `ec-critical`, covering:
`full` and `destructive` run `cargo llvm-cov -p rustfs-ecstore --lib` and fail when line coverage of the gate scope is below `--unit-coverage-min`. The default minimum and the 100% target for EC read, write, decode, heal, metadata-quorum, and rollback paths are the `UNIT_COVERAGE_*` constants at the top of the runner. `cargo-llvm-cov` must be installed unless `--skip-coverage` is passed explicitly. The default scope `ec-critical` is:
- `crates/ecstore/src/erasure/**`
- `crates/ecstore/src/set_disk/read.rs`
@@ -298,79 +66,103 @@ The default hard gate scope is `ec-critical`, covering:
- `crates/ecstore/src/set_disk/core/io_primitives.rs`
- `crates/ecstore/src/disk/local.rs`
Minimum release-gate target:
`--unit-coverage-scope crate` measures the whole crate instead; that number is an observation metric and must not hide EC regressions behind unrelated modules. Lowering the minimum requires a documented exception tied to missing testability or unreachable code. Uncovered branches in reconstruction, quorum, and error paths are either intentionally unreachable or tracked.
- `cargo-llvm-cov` must be installed unless `--skip-coverage` is explicitly set;
- `rustfs-ecstore` EC-critical unit line coverage is at least 95%;
- EC read/write/decode/heal/quorum/rollback code should trend toward 100%;
- full-crate unit line coverage is recorded for visibility but is not the EC
hard gate;
- all HIGH rows in this document have positive and negative tests;
- changed EC/read/write/heal lines are covered;
- branch coverage is reviewed for reconstruction, quorum, and error paths;
- uncovered branches are either intentionally unreachable or tracked.
## White-box scenario matrix
The `quick` profile also runs
`cargo test -p rustfs-ecstore --lib -- --test-threads=1` so the white-box smoke
includes all current `rustfs-ecstore` library unit tests without parallel test
context cross-talk, not only focused EC filters.
Each row is a scenario the unit layer covers; a new EC unit test names the row it satisfies.
Coverage artifacts:
### Erasure algorithm (`crates/ecstore/src/erasure/`)
- `target/ecstore-validation/<timestamp>/coverage/ecstore/lcov.info`
- `target/ecstore-validation/<timestamp>/coverage/ecstore/summary.tsv`
- `target/ecstore-validation/<timestamp>/coverage/ecstore/files.tsv`
Local validation on 2026-07-07 found that the current suite is not yet at the
target: full-crate unit line coverage was 69.32%; the EC-critical scope was
84.32% after the first negative read/write/recovery additions. The lowest
EC-critical files were `set_disk/ops/object.rs`, `io_primitives.rs`,
`disk/local.rs`, `bitrot.rs`, and `encode.rs`. These are gaps to close before
the default 95% gate can pass.
## Initial Command Set
Use the runner first:
```bash
scripts/run_ecstore_validation_suite.sh --profile quick
scripts/run_ecstore_validation_suite.sh --profile full
scripts/run_ecstore_validation_suite.sh --profile destructive
scripts/run_ecstore_validation_suite.sh --profile fuzz
```
The split-run equivalent for the quick profile is:
```bash
cargo test -p rustfs-filemeta --lib
cargo test -p rustfs-ecstore --lib erasure
cargo test -p rustfs-ecstore --lib set_disk::read
cargo test -p rustfs-ecstore --lib set_disk::core::io_primitives
cargo test -p rustfs-ecstore --lib set_disk::tests::test_rename_data_quorum_failure_rolls_back_destination_object
cargo test -p rustfs-ecstore --lib disk::local
cargo test -p rustfs-ecstore --lib -- --test-threads=1
cargo test --package e2e_test reliability_disk_fault_test -- --nocapture
cargo test --package e2e_test heal_erasure_disk_rebuild_test -- --nocapture
cargo test --package e2e_test namespace_lock_quorum_test -- --nocapture
```
Fixture-backed tests should run when the fixture path is present:
```bash
cargo test -p rustfs-ecstore --test legacy_bitrot_read_test -- --nocapture
cargo test -p rustfs --features rio-v2 storage::minio_generated_read_test --lib -- --ignored --nocapture
```
## Multi-Expert Adversarial Review Summary
Three independent read-only reviews were run for this design:
| Reviewer | Main challenge | Resulting requirement |
| Area | Scenarios | Assertions |
| --- | --- | --- |
| Algorithm correctness | A single run only samples one timing/layout/hash combination. | matrix/property tests plus instrumented reader alignment checks |
| Black-box reliability | Existing tests miss distributed, shard-desync, range+fault, and migration-fault combinations. | add 4-node/16-disk and destructive profiles |
| Security and fault review | Metadata tamper, quorum downgrade, bitrot bypass, and rename atomicity must be end-to-end. | HIGH matrix rows block completion claims |
| Shard geometry | legacy/current shard-size formulas; lengths `0`, `1`, `block-1`, `block`, `block+1`, multi-block tail | no divide-by-zero; shard/file/range offsets match expected |
| Encode/decode | `(data, parity)` sets `2+2`, `4+2`, `8+8`; random payloads; missing shards up to parity | reconstructed data equals original |
| Negative reconstruction | missing shards above parity; inconsistent shard lengths; corrupt surplus parity | typed error, no partial success |
| Source verification | missing data shard plus extra parity source | rebuilt parity matches source parity |
| Legacy compatibility | old shard formula and legacy checksum data | legacy files decode and heal correctly |
| Streaming decode | legacy engine vs RustFS codec engine on the same stripe stream | bytes and errors are equivalent |
| Range output | head/middle/tail/suffix; cross-block and final-short-stripe ranges | exact byte range, no over-read or under-read |
Current verdict: design accepted as a target. The runner exists and captures
artifacts; implementation remains incomplete until the missing matrix rows have
artifact-backed evidence.
### Bitrot and reader alignment (`erasure/coding/bitrot.rs`, `decode.rs`, `set_disk/core/io_primitives.rs`, `set_disk/shard_source.rs`)
| Area | Scenarios | Assertions |
| --- | --- | --- |
| Hash framing | valid hash+data; wrong hash; truncated hash; truncated data | invalid data never succeeds |
| Short shard | short read under normal hash, `skip_verify`, and hash-none | `UnexpectedEof` or equivalent typed error |
| Lockstep reads | mid-stream data shard failure; pending/timeout reader; final short stripe | each live reader advances exactly one stripe; failed reader retires |
| Adaptive reads | hedged parity fallback and timeout retirement | no shard desync; reconstructed bytes match original |
| Shard source order | out-of-order read completion and missing slots | slots resolve by shard index |
| Deferred readers | data-blocks-first setup opens deferred parity at the correct offset | parity fallback uses aligned data |
Instrumented readers record shard index, stripe index, read count, offset, and retirement reason.
### Metadata, quorum, and commit atomicity (`crates/filemeta/src/filemeta/version.rs`, `set_disk/read.rs`, `set_disk/metadata.rs`, `set_disk/ops/object.rs`, `disk/local.rs`)
| Area | Scenarios | Assertions |
| --- | --- | --- |
| Metadata tamper | same `version_id`/`mod_time`, divergent data dir, parts, ETag, size, checksum, inline flag, erasure distribution | previous committed version or read-quorum error; no arbitrary latest |
| Early stop | valid quorum, stale quorum, corrupt trailing disks, slow trailing disks | early-stop only on safe identity |
| Quorum downgrade | read quorum vs write quorum; delete-marker quorum; version-not-found quorum | no mutation below write quorum |
| Rename atomicity | failure before data rename, after data rename, after metadata rename, cleanup failure | object is old or new; never mixed |
| Rollback | failed commit quorum and stale temp data | rollback preserves old metadata and data |
| Malformed metadata | oversized lengths, bad CRC, invalid versions, invalid UUID/timestamp/enum, huge parts | bounded memory; typed error; no panic |
## Black-box scenario matrix
Real S3 and admin behaviour runs through `crates/e2e_test`. Extend `crates/e2e_test/src/chaos.rs` rather than adding ad hoc fault helpers.
### Single-node 4-disk EC (`reliability_disk_fault_test.rs`, `heal_erasure_disk_rebuild_test.rs`, `chaos.rs`)
| Scenario | Required assertions |
| --- | --- |
| baseline PUT/GET/HEAD/List for tiny, inline, block-boundary, multi-block, multipart objects | SHA256 manifest matches; metadata is consistent on all disks |
| one disk offline during read | existing objects readable; no corrupted bytes |
| one disk offline during write | write succeeds only when write quorum holds; restored disk is healed |
| above-parity disk loss | GET/PUT fails with a quorum error; no partial bytes accepted |
| corrupt data shard and parity shard | GET returns original bytes or fails closed; read-repair/heal restores |
| corrupt inline `xl.meta` | fail closed or heal; no forged inline data |
| range read with offline/corrupt shard | exact range bytes; invalid ranges produce the expected S3 errors |
| multipart part resend and concurrent same-part writes | final object matches the chosen committed parts |
| crash during multipart complete/put/delete | after restart only the old or the new full version is visible |
### Distributed 4-node / 16-disk EC (`cluster_concurrency_test.rs`, `namespace_lock_quorum_test.rs`, `stale_multipart_cleanup_cluster_test.rs`)
Single-process unit tests cannot prove RPC, HTTP/2, timeout, or distributed-quorum behaviour, so this layer is mandatory.
| Scenario | Required assertions |
| --- | --- |
| node/disk outage while reading large objects | no EOF/truncation; SHA256 manifest matches |
| write while one remote node is down | write follows quorum; later heal reconstructs the remote disk |
| remote shard bitrot | degraded read uses clean shards; no bad bytes |
| concurrent GET/PUT/DELETE/List on the same key | no 500 for expected conflicts; no dirty reads |
| range GET matrix for large objects | sequential and parallel ranges match the full-object hash |
| internode timeout / slow disk | typed error or fallback; no desync |
### Versioning, delete markers, and migration (`delete_marker_migration_semantics_test.rs`)
| Scenario | Required assertions |
| --- | --- |
| latest delete marker | GET/HEAD/ListObjectVersions match S3 semantics |
| explicit `versionId` for old versions | exact old bytes and metadata |
| suspended/null version | no version-ordering regression |
| delete marker during heal/rebalance/decommission | marker visibility and history are preserved |
| orphan directory cleanup | real objects are not purged; tombstones behave correctly |
### Heal, rebalance, and decommission
| Scenario | Required assertions |
| --- | --- |
| auto heal and admin deep heal | data hash unchanged; `xl.meta` and format data rebuilt |
| heal interruption/restart | idempotent recovery; no dangling temp objects |
| two-pool rebalance with versioned/multipart objects | source and target pools have consistent versions |
| decommission cancel/restart/finalize | target readable; source cleanup safe |
| rebalance/decommission with node outage | progress resumes; no duplicate or missing versions |
`scripts/test/decommission_*.sh` cover parts of this table but are not runner steps because they emit no machine-readable pass/fail artifact.
## Large-object and fuzz gates
Stand-alone harnesses indexed in `scripts/README.md`, not runner steps: `scripts/run_get_codec_streaming_smoke.sh` (legacy vs codec GET parity), `scripts/run_gt1g_get_http_matrix.sh` (sequential and parallel range GET above 1 GiB), `scripts/run_gt1g_multipart_put_matrix.sh` (multipart PUT above 1 GiB).
The `fuzz` profile delegates to `scripts/fuzz/run.sh`; targets, corpus rules, and crash-reproducer locations are in `fuzz/README.md`. Malformed-storage-input surfaces the corpus must cover: `xl.meta` MessagePack and legacy filemeta versions, RPC payload decoding, checksum and bitrot headers, range offset/length overflow, object names with path traversal or encoded separators, huge inline metadata and part counts.
@@ -1,58 +0,0 @@
# Backlog #2007 Coalescer Delay Validation
`scripts/issue_2007_coalescer_prometheus_report.py` is a read-only Prometheus
report helper for validating whether the GET metadata `ReadVersion` coalescer
default can move from `200us` to `50us`.
The benchmark itself is intentionally external to this helper: use the same
main build, bucket/object set, workload, and
`RUSTFS_BATCH_READ_VERSION_SERVER_PARALLELISM=4` for both cells. Only switch:
```bash
RUSTFS_GET_METADATA_READ_VERSION_COALESCE=auto
RUSTFS_GET_METADATA_READ_VERSION_COALESCE_DELAY_MICROS=200
RUSTFS_GET_METADATA_READ_VERSION_COALESCE_DELAY_MICROS=50
```
After each measured workload window, collect a report from Prometheus:
```bash
scripts/issue_2007_coalescer_prometheus_report.py \
--query-url http://prometheus.example:9090 \
--profile delay-200us \
--window 180s \
--rustfs-selector 'server=~"node[5-8]"' \
--node-selector 'instance=~"node[5-8].*"'
scripts/issue_2007_coalescer_prometheus_report.py \
--query-url http://prometheus.example:9090 \
--profile delay-50us \
--window 180s \
--rustfs-selector 'server=~"node[5-8]"' \
--node-selector 'instance=~"node[5-8].*"'
```
The output is Markdown and is suitable for attaching to the issue alongside the
warp throughput, average latency, p95, p99, and TTFB p99 from the fixed
workload run.
Required RustFS signals:
- `grpc_read_version` and `grpc_batch_read_version` outgoing request increases.
- Coalescer batch distribution from
`rustfs_get_metadata_read_version_coalescer_total{event="attempted_batch"}`.
- `batch_read_version_coalescer_wait`, `batch_read_version_rpc_roundtrip`,
`batch_read_version_disk_read`, and `batch_read_version_response_map` p99.
Required host-cost signals:
- CPU busy from `node_cpu_seconds_total`.
- Network RX/TX from `node_network_receive_bytes_total` and
`node_network_transmit_bytes_total`.
- Disk read await, average queue depth, and utilization from node-exporter disk
counters.
If a section reports `UNAVAILABLE`, treat that evidence as missing rather than
zero. Do not use a default-change PR until the `50us` cell has stable
throughput/latency benefit and CPU, network, and disk cost are available and
acceptable.
@@ -1,44 +0,0 @@
# Backlog #1649 Prometheus smoke
`scripts/prometheus_metrics_1649_smoke.py` is a read-only environment check for
the metric dimensions delivered by backlog #1649 and issues #1650-#1653. It
uses Prometheus' instant-query API and does not start, stop, reconfigure, or
load RustFS nodes.
Run the parser and selector self-test without a live environment:
```bash
python3 scripts/prometheus_metrics_1649_smoke.py --self-test
```
For a live cluster, pass a Prometheus base URL (or its `/api/v1/query`
endpoint), one or more expected server label values, and the built-in profile:
```bash
python3 scripts/prometheus_metrics_1649_smoke.py \
--query-url http://prometheus.example:9090 \
--profile backlog-1649 \
--server rustfs-node1 \
--server rustfs-node2
```
The profile checks the disk, scanner, ILM, audit, and notification series and
their required labels. It also requires the legacy aggregate audit and
notification series, so an additive label change cannot silently break
existing dashboards.
Dynamic series retirement is checked with an exact label set after the
scheduler retirement window has elapsed:
```bash
python3 scripts/prometheus_metrics_1649_smoke.py \
--query-url http://prometheus.example:9090 \
--retired 'rustfs_scanner_bucket_drive_result_total|server=node1,bucket=removed,drive=d1,result=success' \
--retired 'rustfs_audit_total_messages_by_server|server=node1,target_id=removed'
```
`--require METRIC|key=value,...` requires a matching series;
`--require-labels METRIC|key1,key2` requires every returned series to carry
the named labels. Use `--bearer` for a bearer token or `--basic` for a
`username:password` credential when Prometheus is protected. Do not put
credentials in committed commands, logs, or issue comments.
+25 -77
View File
@@ -1,93 +1,41 @@
# Security Advisory Regression Tests
Every fixed RustFS GitHub Security Advisory (GHSA) should map to at least one
named, discoverable regression test. The convention is: name the test (or a
helper / doc comment on the exact assertion) after the advisory so that
**Use this when:** fixing or reviewing a GHSA, or checking that an advisory's guard actually executes in CI.
**Source of truth:** the test files named below (`rg -i ghsa` finds them); `.config/nextest.toml` for lane membership; `.github/workflows/ci.yml` and `.github/workflows/e2e-replication-nightly.yml` for execution.
```bash
rg -i "ghsa|3p3x|r5qv"
```
Every fixed RustFS GitHub Security Advisory maps to at least one named regression test. Name the test (or the helper / doc comment on the exact assertion) after the advisory so `rg -i "ghsa|<id>"` finds the guard, and a future fix of a still-open advisory is forced to update its pinned test (red -> green).
finds the guard for any advisory, and a future fix of a still-open advisory is
forced to update its pinned test (red -> green).
## Advisory -> test map
> This is the lightweight inventory. sec-14 (backlog#1151) formalizes the
> written admission policy in `AGENTS.md`; keep this file as the map.
## Advisory -> test mapping
| Advisory | Class | Fix PR | Named regression tests | Layer |
| Advisory | Class | Fix | Named regression tests | Layer |
| --- | --- | --- | --- | --- |
| [GHSA-3p3x-734c-h5vx](https://github.com/rustfs/rustfs/security/advisories/GHSA-3p3x-734c-h5vx) | Constant-time secret comparison on WebDAV/FTPS password login | rustfs/rustfs#4403 | `assert_ftps_ghsa_3p3x_wrong_credentials_rejected` (`crates/e2e_test/src/protocols/ftps_core.rs`); `GHSA-3p3x` auth-failure block in `test_webdav_core_operations` (`crates/e2e_test/src/protocols/webdav_core.rs`) | e2e (protocols suite) |
| [GHSA-3p3x-734c-h5vx](https://github.com/rustfs/rustfs/security/advisories/GHSA-3p3x-734c-h5vx) | Constant-time secret comparison on WebDAV/FTPS password login | rustfs/rustfs#4403 | `assert_ftps_ghsa_3p3x_wrong_credentials_rejected` (`crates/e2e_test/src/protocols/ftps_core.rs`); `GHSA-3p3x` auth-failure block in `test_webdav_core_operations` (`crates/e2e_test/src/protocols/webdav_core.rs`) | e2e (`e2e-protocols`) |
| [GHSA-r5qv-rc46-hv8q](https://github.com/rustfs/rustfs/security/advisories/GHSA-r5qv-rc46-hv8q) | Internode RPC authentication must fail closed | rustfs/rustfs#4402 | `ghsa_r5qv_resolve_shared_secret_rejects_default_fallback`, `ghsa_r5qv_verify_rpc_signature_fails_closed_on_missing_or_invalid_auth` (`crates/ecstore/src/cluster/rpc/http_auth.rs`) | unit |
| [GHSA-m77q-r63m-pj89](https://github.com/rustfs/rustfs/security/advisories/GHSA-m77q-r63m-pj89) | STS JWTs signed with shared root secret (intentionally unfixed) | n/a | `test_ghsa_m77q_sts_session_token_signed_with_root_secret` (flow-level pin: signing key == root secret, root-only decode, authorizes) and `test_created_sts_credentials_authorize_with_session_token_claims` (`crates/iam/src/sys.rs`); `token_signing_key` doc (`crates/iam/src/root_credentials.rs`) — pin current by-design behavior; fixing m77q must update red -> green | unit |
| [GHSA-5354-r3w2-34m8](https://github.com/rustfs/rustfs/security/advisories/GHSA-5354-r3w2-34m8) | Service-account parent must stay within caller scope a non-owner holding `CreateServiceAccountAdminAction` could parent a service account to the root credential and authenticate as owner | rustfs/rustfs#5141 | `ghsa_5354_non_owner_service_account_parent_confined_to_scope` and the `add_service_account_parent_within_scope` invariant it pins (`rustfs/src/admin/handlers/service_account.rs`) | unit |
| [GHSA-3ppv-fx5m-m749](https://github.com/rustfs/rustfs/security/advisories/GHSA-3ppv-fx5m-m749) | Versioned object reads must be authorized against `s3:GetObjectVersion`, not `s3:GetObject` (`get_object`, CopyObject source, UploadPartCopy source) | rustfs/rustfs#5142 | `ghsa_3ppv_versioned_read_selects_get_object_version_action` and the `versioned_read_action` helper it pins (`rustfs/src/storage/access.rs`) | unit |
| [GHSA-v9cp-qfw9-9pfp](https://github.com/rustfs/rustfs/security/advisories/GHSA-v9cp-qfw9-9pfp) | `ForAllValues:`/`ForAnyValue:` negated string operators were transposed — negation was applied to the aggregate quantified result instead of the per-value predicate | pending | `ghsa_v9cp_for_all_values_not_equals_partial_overlap`, `ghsa_v9cp_for_any_value_not_equals_partial_overlap` and the absent-key/positive-quantifier cases beside them (`crates/policy/tests/quantified_negation.rs`) the value set must partially overlap the policy set, contained or disjoint sets cannot tell the two quantifiers apart | unit (crate test) |
| [GHSA-6r96-hmgc-726c](https://github.com/rustfs/rustfs/security/advisories/GHSA-6r96-hmgc-726c) | Request headers must not populate server-derived IAM condition keys (`userid`, `groups`, `jwt:`/`ldap:` claim names) | pending | `ghsa_6r96_identity_condition_keys_ignore_spoofed_headers`, `ghsa_6r96_claim_condition_keys_ignore_spoofed_headers`, and `test_request_headers_still_reach_conditions` which guards the reserved set from growing too broad (`rustfs/src/auth.rs`) | unit |
| [GHSA-x298-9x87-fvjq](https://github.com/rustfs/rustfs/security/advisories/GHSA-x298-9x87-fvjq) | The anonymous ListObjectVersions -> `s3:ListBucket` fallback must reach the same public-access gates as a direct grant | pending | `ghsa_x298_anonymous_list_object_versions_denied_when_restrict_public_buckets_enabled` (`crates/e2e_test/src/anonymous_access_test.rs`) asserts 200 before the public-access block is applied so the test proves the gate rather than a broken fallback | e2e |
| [GHSA-g3vq-vv42-f647](https://github.com/rustfs/rustfs/security/advisories/GHSA-g3vq-vv42-f647) | FTPS `MKD` must clear the `s3:CreateBucket` authorization boundary before reaching the backend | pending | `ghsa_g3vq_mkd_denied_before_reaching_backend` (`crates/protocols/src/ftps/driver.rs`) primes `create_bucket` to succeed so the assertion distinguishes "denied at authorization" from "backend refused" | unit |
| [GHSA-m77q-r63m-pj89](https://github.com/rustfs/rustfs/security/advisories/GHSA-m77q-r63m-pj89) | STS JWTs signed with the shared root secret (intentionally unfixed) | n/a; tests pin the by-design behaviour and must flip red -> green when m77q is fixed | `test_ghsa_m77q_sts_session_token_signed_with_root_secret`, `test_created_sts_credentials_authorize_with_session_token_claims` (`crates/iam/src/sys.rs`); `token_signing_key` doc (`crates/iam/src/root_credentials.rs`) | unit |
| [GHSA-5354-r3w2-34m8](https://github.com/rustfs/rustfs/security/advisories/GHSA-5354-r3w2-34m8) | Service-account parent must stay within caller scope; a non-owner with `CreateServiceAccountAdminAction` could parent a service account to root | rustfs/rustfs#5141 | `ghsa_5354_non_owner_service_account_parent_confined_to_scope`, `ghsa_5354_scope_guard_matches_owner_or_self_scope_for_derived_credentials`, and the `add_service_account_parent_within_scope` invariant they pin (`rustfs/src/admin/handlers/service_account.rs`) | unit |
| [GHSA-3ppv-fx5m-m749](https://github.com/rustfs/rustfs/security/advisories/GHSA-3ppv-fx5m-m749) | Versioned reads (`get_object`, CopyObject source, UploadPartCopy source) authorize against `s3:GetObjectVersion`, not `s3:GetObject` | rustfs/rustfs#5142 | `ghsa_3ppv_versioned_read_selects_get_object_version_action` and the `versioned_read_action` helper it pins (`rustfs/src/storage/access.rs`) | unit |
| [GHSA-v9cp-qfw9-9pfp](https://github.com/rustfs/rustfs/security/advisories/GHSA-v9cp-qfw9-9pfp) | `ForAllValues:`/`ForAnyValue:` negated string operators applied negation to the aggregate instead of the per-value predicate | fixed, GHSA private-fork merge | `ghsa_v9cp_for_all_values_not_equals_partial_overlap`, `ghsa_v9cp_for_any_value_not_equals_partial_overlap` and the absent-key/positive-quantifier cases beside them (`crates/policy/tests/quantified_negation.rs`); the value set must partially overlap the policy set, since contained or disjoint sets cannot tell the quantifiers apart | crate test |
| [GHSA-6r96-hmgc-726c](https://github.com/rustfs/rustfs/security/advisories/GHSA-6r96-hmgc-726c) | Request headers must not populate server-derived IAM condition keys (`userid`, `groups`, `jwt:`/`ldap:` claims) | fixed, GHSA private-fork merge | `ghsa_6r96_identity_condition_keys_ignore_spoofed_headers`, `ghsa_6r96_claim_condition_keys_ignore_spoofed_headers`, and `test_request_headers_still_reach_conditions`, which keeps the reserved set from growing too broad (`rustfs/src/auth.rs`) | unit |
| [GHSA-x298-9x87-fvjq](https://github.com/rustfs/rustfs/security/advisories/GHSA-x298-9x87-fvjq) | Anonymous ListObjectVersions -> `s3:ListBucket` fallback must reach the same public-access gates as a direct grant | fixed, GHSA private-fork merge | `ghsa_x298_anonymous_list_object_versions_denied_when_restrict_public_buckets_enabled` (`crates/e2e_test/src/anonymous_access_test.rs`); asserts 200 before the public-access block is applied so it proves the gate, not a broken fallback | e2e (`e2e-smoke`) |
| [GHSA-g3vq-vv42-f647](https://github.com/rustfs/rustfs/security/advisories/GHSA-g3vq-vv42-f647) | FTPS `MKD` must clear the `s3:CreateBucket` authorization boundary before reaching the backend | fixed, GHSA private-fork merge | `ghsa_g3vq_mkd_denied_before_reaching_backend` (`crates/protocols/src/ftps/driver.rs`); primes `create_bucket` to succeed so the assertion distinguishes "denied at authorization" from "backend refused" | unit (`ftps` feature) |
## Where these run (CI-execution map)
## Where these run
Every security regression must land where CI actually runs it — a named test in
an unexecuted suite is theater. The suites split across three execution paths by
topology:
| Layer | Command | Lane | Guard |
| --- | --- | --- | --- |
| Unit and crate tests (`ghsa_r5qv_*`, the m77q pins, `ghsa_5354_*`, `ghsa_3ppv_*`, `ghsa_6r96_*`, `ghsa_v9cp_*`, `ghsa_g3vq_*`) | `cargo nextest run --profile ci --all --exclude e2e_test` | every PR, `Test and Lint` (required) | none needed; the workspace pass runs every unit and crate test |
| S3-API negative-auth e2e (`negative_sigv4_test`, `presigned_negative_test`, `admin_auth_test`) | `cargo nextest run --profile e2e-smoke -p e2e_test` | every PR, `End-to-End Tests` (report-only) | `scripts/check_security_smoke_count.sh` with the floor in `.config/security-smoke-floor.txt`, run in the `e2e-tests` job; fails when a rename drops one of these modules out of the smoke filter |
| Other S3 e2e guards (`anonymous_access_test`) | `cargo nextest run --profile e2e-smoke -p e2e_test` | every PR, `End-to-End Tests` (report-only) | `scripts/check_test_wiring.py --check-profile e2e-smoke` digest |
| Protocol e2e (`protocols::test_protocol_core_suite`, GHSA-3p3x) | `RUSTFS_BUILD_FEATURES=ftps,webdav,sftp cargo nextest run -j 1 --profile e2e-protocols -p e2e_test` | nightly, `e2e-replication-nightly.yml` job `protocols-nightly`; not PR-gated | `scripts/check_test_wiring.py --check-profile e2e-protocols` digest |
- **Unit tests** — `ghsa_r5qv_*` (`crates/ecstore`), the GHSA-m77q STS
pinning (`crates/iam`), `ghsa_5354_*` / `ghsa_3ppv_*` / `ghsa_6r96_*`
(`rustfs` lib), `ghsa_v9cp_*` (`crates/policy` crate test), and
`ghsa_g3vq_*` (`crates/protocols`) run automatically in the default CI pass
(`cargo nextest run --profile ci --all --exclude e2e_test`) — no special
wiring. This is the CI-executed regression for the RPC fail-closed (r5qv),
STS-signing (m77q), service-account parent-scope (5354), versioned-read
authorization (3ppv), condition-key injection (6r96), quantified-negation
(v9cp), and FTPS MKD authorization (g3vq) advisories.
Notes:
`ghsa_g3vq_*` sits behind `rustfs-protocols`' `ftps` feature, which is off by
default for that crate alone. It still runs in the workspace pass because the
`rustfs` crate defaults to `["ftps", "webdav"]` and cargo unifies features
across the build. Running `cargo test -p rustfs-protocols` on its own silently
skips it — use `--features ftps` in that case. The `protocol-features` CI
matrix covers `swift` and `sftp` only, for the same reason: those are not in
any default feature set, so they need explicit jobs.
- **S3-API negative-auth e2e (e2e-smoke, PR-gated)** — the attacker-facing S3
auth-rejection suites run on every PR via the `e2e-smoke` nextest profile
(`.config/nextest.toml`), which each spawns its own server on a random port
and is parallel-safe:
- `negative_sigv4_test` — tampered/wrong-key/skewed header SigV4 (sec-1)
- `presigned_negative_test` — expired/tampered/wrong-key presigned URLs (sec-2)
- `admin_auth_test` — non-admin denial + root-credential lifecycle (sec-4)
A count-floor guard (`scripts/check_security_smoke_count.sh`, floor in
`.config/security-smoke-floor.txt`) runs in the `e2e-tests` CI job and fails if
a rename drops any of these out of the smoke filter (infra-12 mechanism). This
is the sec-5 wiring: those merged suites were dead weight until listed here.
- **Protocol e2e (WebDAV/FTPS constant-time login, GHSA-3p3x)** — lives in the
`e2e_test` protocols suite (`test_protocol_core_suite`), which binds **fixed
ports**, needs the `ftps,webdav` build features, and is `#[serial]`. Those
three properties make it **structurally incompatible** with the random-port,
default-feature, parallel `e2e-smoke` profile: it cannot be a filterset change,
so sec-5 (scoped to a filterset change, global ruling G5) does not wire it.
It runs manually today:
```bash
RUSTFS_BUILD_FEATURES=ftps,webdav cargo test --package e2e_test \
test_protocol_core_suite -- --test-threads=1 --nocapture
```
**Open gap:** GHSA-3p3x's *e2e* layer has no PR-gated CI execution. A protocols
e2e CI lane is a ci-domain concern (a new profile/job, not a filterset change);
it is deliberately out of sec-5's boundary and left as a follow-up.
- `test_protocol_core_suite` is a single `#[tokio::test]` (not `#[serial]`) that binds fixed ports and needs the `ftps,webdav` build features; the nightly job serializes it with `-j 1`. It cannot join the random-port, default-feature `e2e-smoke` profile. Targeted local run: `crates/e2e_test/src/protocols/README.md`.
- `ghsa_g3vq_*` sits behind the `ftps` feature of `rustfs-protocols`, which is off by default for that crate alone. It still runs in the workspace pass because the `rustfs` crate defaults to `["ftps", "webdav"]` and cargo unifies features across the build; `cargo test -p rustfs-protocols` on its own skips it, so pass `--features ftps`. The `protocol-features` matrix in `ci.yml` covers only `swift` and `sftp` for the same reason.
## Adding a new advisory guard
1. Reproduce the advisory's bypass form as a focused negative test.
2. Name the test (or the helper/assertion) `ghsa_<id>_*`, or attach a
`GHSA-<id>` doc comment with the advisory URL and fix PR.
2. Name the test (or the helper/assertion) `ghsa_<id>_*`, or attach a `GHSA-<id>` doc comment with the advisory URL.
3. Add a row to the table above.
4. Land it where CI runs it (see the map above): a unit guard in the default CI
pass; an S3-API negative-auth e2e in the `e2e-smoke` filter (add the module to
`.config/nextest.toml` and bump `.config/security-smoke-floor.txt`); a
fixed-port protocol e2e in the protocols suite (still manual — see the open
gap above).
4. Land it where it runs (table above): a unit guard needs nothing extra; an S3 e2e guard joins the `e2e-smoke` filter in `.config/nextest.toml` with its digest updated, and a negative-auth module also bumps `.config/security-smoke-floor.txt`; a fixed-port protocol guard goes into the protocols suite (nightly lane).