culvert 0.2.0

Typed access to the HDMI 2.1 SCDC register map
Documentation
# Testing Strategy

culvert's test suite is built around deterministic register-access tests. All tests run
against in-memory transport implementations; no real hardware is required at any point.

## Test structure

Tests are split between inline unit tests in each `src/client/` module and integration
tests in `tests/scdc.rs`.

### Unit tests (`src/client/`)

Each client module contains tests immediately below the code it covers. They use
`TestTransport`, a 256-byte register array backed transport defined in
`src/client/test_transport.rs`.

`TestTransport` has two constructors:

- `TestTransport::new()` — succeeds on all operations; used for happy-path tests.
- `TestTransport::failing_after(n)` — succeeds for the first `n` operations then returns
  `Err(())`; used to exercise every `?` error branch.

The single generic instantiation (`Scdc<TestTransport>`) is intentional: using one
concrete type avoids LLVM counting per-monomorphisation `?` branches as uncovered, which
would inflate the coverage denominator without corresponding tests.

Each register group has tests covering:

- **Encoding correctness** — every field of a write config maps to the correct bit
  position in the output register(s). One assertion per field, not per struct.
- **Decoding correctness** — every field of a read result is extracted from the correct
  bit position. Register values are crafted to isolate individual bits where necessary.
- **Protocol errors** — what culvert refuses produces the correct `ProtocolError`
  variant before anything is written: FFE levels above the rate's maximum. A clear of
  `Update_0` cannot ask for `RR_Test` (`ClearableUpdateFlags` has no field for it); a test
  reads `RR_Test` and `FLT_update` set and clears what it read, which writes `FLT_update`
  only. Undefined
  values the sink reports are not errors: each undefined `LtpReq` nibble (0x9–0xD) decodes
  to `LtpReq::Reserved` with its raw value.
- **Transport error propagation** — every read and write call site has a
  `TestTransport::failing_after(n)` test that triggers failure at that exact operation
  and asserts the error bubbles through as `ScdcError::Transport`.

The `register` module also contains unit tests for the `CedCount` newtype (validity bit
masking, 15-bit value preservation) and `UpdateFlags::new` field ordering.

### Integration tests (`tests/scdc.rs`)

The integration tests use `SimulatedScdc`, a separate infallible transport
(`Error = Infallible`) that exercises the public API through the crate boundary. These
tests confirm that the full round-trip — pre-load registers, call an `Scdc` method,
assert on decoded output or written register state — works correctly when going through
`pub use` re-exports.

Coverage is complementary: unit tests cover all branches and error paths; integration
tests confirm end-to-end bit patterns for each register group with realistic multi-field
values.

### plumbob feature tests (`src/client/plumbob_client.rs`, `tests/plumbob.rs`)

When compiled with `--features plumbob`, two more sets of tests run:

- **Unit tests** call each method through the `plumbob::ScdcClient` trait against
  `TestTransport` and assert on register bits: every request value (and 0x9 passed on
  as `LtpReq::Reserved`), each flag clear writing only its own bit, `Config_1` encoding
  (including FRL off), culvert's FFE-levels check, CED validity bits, and transport errors
  from every method.
- **End-to-end tests** run plumbob's `FrlTrainer` through `Scdc<T>` against a sink
  simulated at the register level: `FLT_ready` after N reads, request nibbles posted in
  `Status_Flags_1/2` with `FLT_update`, write-1-to-clear `Update_0`, and `Config_1`
  starting and ending training. They cover a full training run, a rate drop, a timeout and
  exhausted rates ending in TMDS, `FLT_no_timeout`, an undefined request ignored and
  returned as a `TrainingWarning`, a 3-lane link whose unused lane 3 reports an undefined
  value, `FRL_start` and `FLT_update` set together (the retrain wins, and
  `FrlStartWithRetrain` is recorded), a DDC NACK mid-LTS:3 still ending in LTS:L
  (`Config_1` back to 0, the PHY in TMDS, `FLT_update` cleared), a retrain resuming from
  no pattern, the `Update_0` flags plumbob does not handle left set, and the FFE maximum
  limited per rate in the `Config_1` byte. They check the whole chain — plumbob's
  sequencing, culvert's encoding, and the register bits in between.

CI's coverage run measures the default features and `--features plumbob` together, so
the feature's module is part of the coverage ratchet.

## Coverage

CI measures line coverage with `cargo-llvm-cov`, over the default features and the
`plumbob` feature merged into one report. The baseline is stored in
`.coverage-baseline` (currently 100%); CI fails if coverage drops more than 0.1% below
it. New register coverage without tests will trip this.

## Philosophy

`Scdc<T>` runs identically against simulated and real `ScdcTransport` implementations.
A test that cannot run with an in-memory transport does not belong in this repository.
Hardware is never a test dependency.