Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .agents/skills/ship-pr/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ worktree. Same **commands** as the required jobs, not the GitHub Actions

| When | Run |
|------|-----|
| Inner loop (Red / Green) | Targeted `cargo test -p <crate> … -- --quiet`. `cargo check -p <crate> --lib`. Not `--tests` after every edit |
| Inner loop (Red / Green) | Targeted `cargo test -p <crate> --test suite … -- --quiet`. `cargo check -p <crate>`. Not a workspace `--tests` after every edit |
| After Green | `cargo test --workspace --quiet` |
| After Refactor, before commit | Local CI except coverage (below) |
| Docs, comments, formatting only | Skip the suite. `cargo fmt` if rustfmt would touch the tree |
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@ jobs:
run: df -h /dev/shm && free -m
- name: cargo test
run: cargo test --workspace --exclude rbitcoin-bench
# The workspace exclude stays (coverage.test.sh). This one lib test is
# the address-corpus pin: sha256::Hash Display is internal order.
- name: bench scripthash pin
run: cargo test -p rbitcoin-bench --lib hot_corpus_includes_p2a -- --quiet

# Windows IOCP store + mmap fuse + query confirm + --smoke. Required. Not a CRT-static snapshot.
windows:
Expand Down Expand Up @@ -181,7 +185,7 @@ jobs:
- name: store + node smoke
run: ./scripts/ci-os-smoke.sh

# Line-coverage 93% floor + CRAP --fail-above 30 (allowlist in .cargo-crap.toml).
# Line-coverage 91% floor + CRAP --fail-above 30 (allowlist in .cargo-crap.toml).
# Slow; waits for qc and test so a red check does not start
# this job. Required. Job+step timeouts so a hung llvm-cov test binary
# cannot sit until the runner's 6h default.
Expand Down
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ You can also run the script yourself before the first consensus test:
export CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-$PWD/target/dev}"
cargo fmt --all # includes nested fuzz/ crate
cargo clippy --workspace --all-targets -- -D warnings
cargo test -p rbitcoin-primitives --lib # fast sanity
cargo test -p rbitcoin-store --lib # store (tests pass Tiny at open; production default is Mainnet)
cargo test -p rbitcoin-primitives --test suite # fast sanity
cargo test -p rbitcoin-store --test suite # store (tests pass Tiny at open; production default is Mainnet)
cargo test --workspace # default suite — Linux / macOS; see table
./scripts/ci-os-smoke.sh # Windows/macOS PR surface
```
Expand Down Expand Up @@ -164,7 +164,7 @@ engineering.
2. Prefer **high-level functional/integration tests** over unit tests
([`TESTING.md`](./TESTING.md)). A branch no peer, client, or operator can
trigger is removed rather than covered by a unit.
3. Every PR must keep production line coverage **≥ 93%** (`LH*100 >= LF*93`).
3. Every PR must keep production line coverage **≥ 91%** (`LH*100 >= LF*91`).
Nightly branch coverage stays ≥90% when measured. Same bar as CI via
`./scripts/coverage.sh`. CRAP `--fail-above 30` with the allowlist in
`.cargo-crap.toml`. Hold the floor by covering from the surface journey
Expand Down
23 changes: 20 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ members = [
"crates/rbitcoin-consensus",
"crates/rbitcoin-mempool",
"crates/rbitcoin-net",
"crates/rbitcoin-ibd",
"crates/rbitcoin-rpc",
"crates/rbitcoin-electrum",
"crates/rbitcoin-esplora",
Expand All @@ -32,9 +33,12 @@ rbitcoin-primitives = { path = "crates/rbitcoin-primitives" }
rbitcoin-log = { path = "crates/rbitcoin-log" }
rbitcoin-store = { path = "crates/rbitcoin-store" }
rbitcoin-query = { path = "crates/rbitcoin-query" }
rbitcoin-consensus = { path = "crates/rbitcoin-consensus" }
# `default-features = false`: the `corpus` default is only for this
# package's own suite. A node build does not compile the script assembler.
rbitcoin-consensus = { path = "crates/rbitcoin-consensus", default-features = false }
rbitcoin-mempool = { path = "crates/rbitcoin-mempool" }
rbitcoin-net = { path = "crates/rbitcoin-net" }
rbitcoin-ibd = { path = "crates/rbitcoin-ibd" }
rbitcoin-rpc = { path = "crates/rbitcoin-rpc" }
rbitcoin-electrum = { path = "crates/rbitcoin-electrum" }
rbitcoin-esplora = { path = "crates/rbitcoin-esplora" }
Expand Down
22 changes: 12 additions & 10 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ When adding or folding a pin:
|----|--------|
| Extend an existing [catalog](#scenario-catalog) journey (same `/tmp` pad, more asserts) | A new skinny scenario that remine-pads the same chain |
| Fold a twin unit once the journey hits the same shipped path | Twin unit + scenario for the same reject string |
| Delete a lower test only in a commit where a surface journey already hits those lines, so the 93% ratio holds. When no surface can hit the lines, delete the production branch and the test that only painted it in that same commit | A new small test for a path a peer, client, or operator can already hit, or a private-helper test written to turn coverage or CRAP green. Handshake **format** needles, `decode_rpc_subset`, and BIP324 encode vectors stay only until the live v2 journey observes those bytes |
| Delete a lower test only in a commit where a surface journey already hits those lines, so the 91% ratio holds. When no surface can hit the lines, delete the production branch and the test that only painted it in that same commit | A new small test for a path a peer, client, or operator can already hit, or a private-helper test written to turn coverage or CRAP green. Handshake **format** needles, `decode_rpc_subset`, and BIP324 encode vectors stay only until the live v2 journey observes those bytes |
| Live P2P/RPC on `cross_surface` / `integration_multinode` catalog tests | Grow `node_cli_and_surface_smoke` into a second live node |
| New P2P behavior on `p2p_timeout_*` / compact / feeler / inbound-full | Stuff more asserts onto `two_node` |

Expand Down Expand Up @@ -178,10 +178,10 @@ absolute path. Keep default-tier fixtures small: on tmpfs, test bytes are RAM.

| Package / binary (warm, order-of-magnitude) | Budget | Notes |
|---------------------------------------------|-------:|-------|
| `rbitcoin-store --lib` | **&lt;45 s** | Catalog-run fixtures stay tens of tiny files, not thousands |
| `rbitcoin-consensus --lib` | **&lt;30 s** | Pure-result rule: [True journeys](#true-journeys). Mainnet 866342 (~1.6 s) is the historical prevout pin — one zstd decode, overweight on a clone. |
| `rbitcoin-query --lib` | **&lt;20 s** | |
| `rbitcoin-test --test journeys` | **&lt;15 s** for the old scenarios module | Prefer `pad_empty_from` / shared mature helpers. Default journeys are one binary (`scenarios::` and the rest). |
| `rbitcoin-store --test suite` | **&lt;45 s** | Catalog-run fixtures stay tens of tiny files, not thousands. The suite links the normal rlib. |
| `rbitcoin-consensus --test suite` | **&lt;30 s** | Pure-result rule: [True journeys](#true-journeys). Mainnet 866342 (~1.6 s) is the historical prevout pin — one zstd decode, overweight on a clone. |
| `rbitcoin-query --test suite` | **&lt;20 s** | |
| `rbitcoin-test --test suite scenarios::` | **&lt;15 s** for the old scenarios module | Prefer `pad_empty_from` / shared mature helpers. Default journeys are one binary (`scenarios::` and the rest). |
| **Full** `cargo test --workspace` | **≤3 min** warm | Stretch **&lt;2 min** |

**New default-suite test rule:** if a new or expanded default test routinely takes **&gt;2 s wall** on a warm tree, the PR must **justify** it (what contract needs that cost, why a smaller N / unit cannot hit the branch). Prefer `#[ignore]` + reason string for true microbenches / host-only forensics.
Expand All @@ -203,14 +203,16 @@ absolute path. Keep default-tier fixtures small: on tmpfs, test bytes are RAM.

| Metric | Required |
|--------|----------|
| Line coverage | Production LCOV `LH`/`LF` from `./scripts/coverage.sh` **≥ 93%** (unrounded `LH*100 >= LF*93`). No never-falls ratchet — llvm-cov hit counts jitter tens of lines on the same tree. |
| Line coverage | Production LCOV `LH`/`LF` from `./scripts/coverage.sh` **≥ 91%** (unrounded `LH*100 >= LF*91`). No never-falls ratchet — llvm-cov hit counts jitter tens of lines on the same tree. |
| Branch coverage | **≥ 90%** when measured on nightly with `--branch`; on stable, region-partial lines in the text report may remain — still close large gaps via scenarios |

Test modules (`*_tests.rs`, `/tests/`, `testutil.rs`, crate `rbitcoin-test`)
are omitted from `LH`/`LF`. Those tests still run. A journey that executes
a production line already counts that line, so a second in-crate `#[test]`
is not required to make LCOV see it. `#[cfg(test)]` arms inside production
files still count. llvm-cov hit counts jitter tens of lines on the same
is not required to make LCOV see it. Library tests live in each crate's
`tests/suite` and link the normal rlib, so a `#[cfg(test)]` arm in `src/`
is no longer how those tests compile. `#[cfg(debug_assertions)]` hooks in
production files still count. llvm-cov hit counts jitter tens of lines on the same
tree; the floor is the gate, not a never-falls ratchet vs master.

The README badge and rbitcoin.org figure are the last **green `master`**
Expand Down Expand Up @@ -291,7 +293,7 @@ counts that line.
5. The narrow pure-result case in [True journeys](#true-journeys): one unit
on that shipped function, reason in the test. A reopened store that can
show the result is case 3.
6. Stop when the ratio is **≥ 93%**. Do not add a private-helper test to
6. Stop when the ratio is **≥ 91%**. Do not add a private-helper test to
get there.

## Structural lints, CRAP, Miri, mutants
Expand All @@ -308,7 +310,7 @@ matches scalar in default tests). Owner: [`docs/quality.md`](./docs/quality.md).
|------|------------|----|
| **ast-grep** | `./scripts/ast-grep.sh` (needs `ast-grep` on `PATH`; `nix-shell` / `nix develop` provide it). Fixture self-test: `./scripts/ast-grep.test.sh` | Step in required job `qc` |
| **cargo-crap** | After LCOV, `./scripts/coverage.sh` calls `./scripts/coverage-crap.sh` (skip if `cargo-crap` missing). `--fail-above --threshold 30`; `.cargo-crap.toml` allowlists today's production CRAP>30 functions (remove a name when it scores ≤30). A new failure is answered by deleting branches until one path remains, or by extending the catalog journey that owns the feature so those branches run. Allowlist additions follow [`docs/code-shape.md`](docs/code-shape.md): do not split a Core-faithful opcode loop or an io_uring machine to beat a score. A test whose only effect is to change the CRAP input is not the fix. Dry-run: `CRAP_DRY_RUN=1 ./scripts/coverage-crap.sh`. Self-test: `./scripts/coverage-crap.test.sh` | Rides required `coverage`. No `--fail-regression` (llvm-cov coverage % jitters per function) |
| **coverage ignore / badge** | `./scripts/coverage.test.sh` (filename ignore, Tier A IBD not skipped, 93% floor, Shields JSON). Publish dry-run: `BADGE_DRY_RUN=1 ./scripts/publish-coverage-badge.sh` | `test` job self-test; `coverage` job writes `coverage/badge.json` and, on green `master`, pushes `badges/coverage.json` |
| **coverage ignore / badge** | `./scripts/coverage.test.sh` (filename ignore, Tier A IBD not skipped, 91% floor, Shields JSON). Publish dry-run: `BADGE_DRY_RUN=1 ./scripts/publish-coverage-badge.sh` | `test` job self-test; `coverage` job writes `coverage/badge.json` and, on green `master`, pushes `badges/coverage.json` |
| **Miri** | `./scripts/miri.sh` → `cargo +nightly miri test -p rbitcoin-primitives`. Dry-run: `MIRI_DRY_RUN=1 ./scripts/miri.sh`. Self-test: `./scripts/miri.test.sh` | Nightly `miri.yml` (not required). Never `--workspace` |
| **cargo-mutants** | Nightly, not a PR check. `./scripts/mutants-nightly.sh` lists `--workspace` mutants, runs new diff lines first, then walks a cursor through the rest. `rbitcoin-bench` is excluded (`.cargo/mutants.toml` and `--exclude` on both invocations; CLI replaces the config glob). `#[mutants::skip]` on an expression is the won't-test list; those mutants are omitted, not `MISSED`. Each mutant uses `--test-workspace=true` so a higher journey counts, `--baseline=skip`, `-j 1`, test binaries on tmpfs (runner above; the mutants copy stays on disk), 20 minute mutant timeout, 8 hour budget across two jobs of 4 hours (a hosted job dies at 6). While both the new queue and the backlog have mutants, a job starts new batches only until half of its own budget has elapsed, then walks the backlog. Time the new queue does not use goes to the backlog. The second job resumes the cursor and does not open another new window after that half is used. An empty side does not block the other. Cursor and `MISSED` lines are pushed to the `mutants-state` branch; that branch wins over the `mutants-cursor` artifact, which is still uploaded and expires after 14 days. Resume continues at the saved backlog mutant name, or at the saved index modulo the current backlog length when that name is gone. A failed push fails the run. `MISSED` is uploaded and does not fail the run. Each invocation passes `--file` for one source file and at most `MUTANTS_BATCH` queued names from that file (default 200). cargo-mutants 27.1.0 emits `..` struct-field deletes without applying `--re`; one file keeps that repeat inside the file under test instead of retesting every such delete in the workspace on every batch. The miss list is `missed.txt` on the `mutants-state` branch. | `mutants.yml` daily `47 0 * * *` (17:47 Pacific during PDT) and `workflow_dispatch`. Each job timeout 270 minutes. |

Expand Down
4 changes: 4 additions & 0 deletions changelog.d/crate-layout-compile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Changed

- **Library tests compile against the normal rlib.** Each library package except `rbitcoin-bench` has one `tests/suite` target (`[lib] test = false` and `doctest = false`, including `rbitcoin-ibd`), so `cargo test` no longer rebuilds the crate under `--cfg test`. Bench still runs with `cargo test -p rbitcoin-bench --lib`. `test-hooks` is gone. Hooks a test arms stay `cfg(debug_assertions)`. A debug node still uses the 8 s outbound connect timeout and still aborts when an io_uring drain leaves SQEs. Initial block download is `rbitcoin-ibd`, and RPC, Electrum, Esplora, and SV2 do not depend on it. The consensus suite is the package's default `corpus` feature (the Core script assembler). Product crates depend on consensus with `default-features = false`, so a node build leaves that assembler out. `rbitcoin-test`'s default scenarios are one `suite` binary; `overlay` stays feature-gated. `NODE_COMPACT_FILTERS` is advertised per peer hub, so one process can run more than one node.
- **Production line-coverage floor is 91%.** Suite tests link the normal rlib, so `#[cfg(test)]` lines are no longer in the LCOV denominator. The measured ratio on this layout is 91.26% (93410/102361). The gate is still unrounded `LH*100 >= LF*91`.
4 changes: 2 additions & 2 deletions changelog.d/suite-under-150s.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ Changed
- **The hub `OP_TRUE` signet generate test uses regtest difficulty.** Accept still requires the BIP325 solution. The real signet target stays covered by the consensus miner test.
- **`NODE_COMPACT_FILTERS` lives on the peer hub.** Version, feeler, addr, and `getnetworkinfo` read that node's bit. There is no process-wide flag.
- **`getblocktemplate` stays off while the catch-up pool still holds transactions.** That returns `-10`. An empty pool is served, and a node with no mempool still serves `getblocktemplate` on the chain tip and an empty `getrawmempool`. `-blocksonly` returns a permanent error for `getblocktemplate` so a miner does not retry. `generate` during catch-up mines the coinbase only. Once relay is on, `getblocktemplate` and Stratum v2 build on the tip recorded after the pool strip. A Stratum v2 session that is still holding tries again on the template interval, so the tip that ends catch-up is not missed when relay turns on after that event. A block connect and an initial-download confirm batch hold that mark from before the chain tip moves until the drop is stored. A disconnect, including a header rewind, moves it to the fork point the pool was put back on. There is no chain scan under the mempool lock.
- **A node built with the journey `test-hooks` feature warns at startup.** That build uses the short reconnect and SV2 cooldown. Release, nix, and `cargo build -p rbitcoin-node` do not.
- **A debug node warns at startup.** That build uses the short reconnect and SV2 cooldown. Release does not.
- **`cargo llvm-cov` no longer builds the workspace twice.** The coverage script was compiling plain node and cli bins before the instrumented test build. The suite already builds the bins it execs.

Fixed

- **Default journeys link and run as one test binary.** Doctests are off on library crates that have none. The node, cli, and journey-node bins do not build an empty test harness. Journeys compile a 50ms SAM retry, a 250ms connect redial, and a 100ms SV2 budget cooldown. The node binary keeps 500ms, 2s, and 1s.
- **Default journeys link and run as one test binary.** Doctests are off on library crates that have none. The node, cli, and journey-node bins do not build an empty test harness. Debug builds use a 50ms SAM retry, a 250ms connect redial, and a 100ms SV2 budget cooldown. Release keeps 500ms, 2s, and 1s.
10 changes: 8 additions & 2 deletions crates/rbitcoin-cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
[package]
name = "rbitcoin-cli"
autotests = false
version.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
repository.workspace = true
description = "rbitcoin RPC CLI client"

[lib]
test = false
doctest = false

[[bin]]
name = "rbitcoin-cli"
path = "src/main.rs"
test = false

[lib]
doctest = false

[features]
mutants = []
Expand All @@ -24,6 +27,9 @@ mimalloc = { workspace = true }
serde_json = { workspace = true }
socket2 = { version = "0.6", features = ["all"] }

[[test]]
name = "suite"
path = "tests/suite.rs"

[lints]
workspace = true
Loading
Loading