plumbob 0.3.1

FRL link training state machine for HDMI 2.1
Documentation
# Testing Strategy

plumbob's test suite is built around deterministic state machine tests. All tests run
against scripted implementations of `ScdcClient` and `HdmiPhy`; no real hardware is
required at any point.

## Test structure

Tests live alongside the code they cover: `src/types.rs` for the protocol types, and
`src/training/tests.rs` for the state machine, using the simulated sink and PHY in
`src/training/sim.rs`.

### Type tests (`src/types.rs`)

Tests for the owned protocol types cover:

- `LtpReq` values matching the `Status_Flags` encoding, and `LtpReq::pattern` mapping
  0x1–0x8 to the `LtpPattern` of the same value (0x0, 0xE and 0xF have none)
- `FfeLevels` accepting 0–7, rejecting higher values, and `limited_to` capping it at 3 for
  every rate up to 12 Gbps
- Constructor invariants: `CedCount::new` strips the validity bit (`bits[14:0]` preserved,
  bit 15 masked off)
- Defaults and equality for `UpdateFlags`, `SourceTestConfig`, `LtpRequests` and
  `CedCount`
- The constructors that take fields in register or lane order (`UpdateFlags::new`,
  `ClearableUpdateFlags::new`, `SourceTestConfig::new`, `LtpRequests::new`,
  `CedCounters::new`, `FrlConfig::new`): each field lands where its bit or lane says, since
  positional `bool` and lane arguments are easy to swap; `clearable()` drops only
  `RR_Test`; `LtpRequests::all_trained` counts lane 3.

### The simulated sink and PHY (`src/training/sim.rs`)

- `SimSink` — a scripted sink, set up with builder methods so a test reads like its
  scenario: `FLT_ready` after N polls; a queue of LTS:3 *rounds*, each raising
  `FLT_update` with a set of per-lane requests after some polls (counted from the last
  `Config_1` write with an FRL rate); `FRL_start` after N polls once the rounds are used
  up; an optional source test configuration, also changed with a round or on its own
  (`source_test_after`, setting `Source_Test_Update` without `FLT_update`); the `Update_0`
  flags training does not act on (`other_flags`). A rate drop is a round of `RateChange`, and
  a retrain during LTS:P is a round after the all-`None` one. It records every call in
  order and can fail any one operation, on every call or only on its N-th call.
- `SimPhy` — records every PHY call in order and can fail any one operation, on every
  call or only on its N-th call.
- A shared `Log` (`log_to` on both) records the sink's and the PHY's calls interleaved, in
  the order they happened, for tests of step order across the two.

The sim has its own tests, so it is fully covered before the state machine uses it.

### Warning tests (`src/warning.rs`)

Merging: repeats on a lane keep the last value and a count, lane 3 in and out of use are
kept apart, and the five distinct warnings an attempt can produce fit without `alloc`.

### State machine tests (`src/training/tests.rs`)

- The main path LTS:2 → LTS:3 → LTS:P, asserting the complete SCDC and PHY call logs
- Each poll limit: timing out after exactly N polls, and continuing on the last allowed
  poll
- LTS:3's per-lane rules: a pattern per lane, 0x0 keeping a lane's pattern, 0x3 driven
  only under `FLT_no_timeout`, 0xE raising and holding a lane's TxFFE level, a partial
  0xF, lane 3 ignored at the 3-lane rates (a pattern, 0xE or an undefined value on it
  changes nothing), both 3-lane rates driving three lanes, and undefined values
  (`Reserved`) leaving a
  lane as it was without counting as a pass or a rate change
- `FLT_no_timeout` suspending the limits (from LTS:2 or LTS:3) up to its cap, holding for
  every attempt while it stays set, and the cap ending LTS:2, LTS:3 or LTS:P with
  `NoTimeoutHold` and no LTS:L (while LTS:P without it still falls back)
- `FRL_start` and `FLT_update` set together in LTS:P: the retrain wins, within
  `max_retrains`
- Retraining from LTS:P: resuming LTS:3 with no pattern on any lane and the TxFFE levels
  kept — no reset sent, and plumbob's own model holding them, so a further 0xE after the
  retrain raises a lane from its level, not from 0 — bounded by `max_retrains` (including 0), counted across a rate drop, and still
  falling back when exhausted under `FLT_no_timeout`
- Warnings: undefined requests returned as `TrainingWarning`s, merged per lane with a
  count, on a success and on a fallback, and none for a clean attempt
- The rate list: `NotSupported` and rates that are not strictly descending rejected as
  `InvalidRates`, and an empty list as `NoRates`, with no SCDC or PHY call; every FRL
  rate, in order, walked through
- LTS:4: stepping down through the list, resetting the lanes, moving to a 3-lane rate, a
  fresh poll limit per rate, and running out of rates
- LTS:L: the exact exit sequence, clearing a pending `FLT_update`, every timeout
  ending in TMDS, and every step attempted when one of them fails, with the first error
  returned
- `TrainingError::Scdc` and `TrainingError::Phy` propagating from every SCDC and PHY
  operation the state machine uses, with LTS:L afterwards leaving both ends in TMDS — at
  every call site, not only an operation's first: three scenarios (a success with a
  source test configuration, a TxFFE raise, a rate drop and a retrain; a fallback through
  LTS:L; and a `Source_Test_Update` in LTS:3 that changes the TxFFE restrictions, then
  another change on LTS:P's first poll together with `FRL_start` and `FLT_update`) are each
  run once per call they make, failing that one call, and every failure must be reported.
  A site no scenario reaches is not checked, so a branch with I/O of its own needs a
  scenario that takes it; region coverage shows the three reach every one
- The step order across sink and PHY in LTS:2 (rate, then TxFFE reset), LTS:3 (the
  requested pattern and level reach the PHY before `FLT_update` is cleared), LTS:P (the
  patterns stopped and gap characters sent before `FLT_update` is cleared) and LTS:4
  (patterns stopped, then LTS:2's end at the new rate: `FLT_update` cleared, rate, TxFFE
  reset, patterns and gap characters restored, `Config_0`, `Config_1`), from a log of both
  in order
- `FLT_no_timeout` holding across a rate drop and a retrain within one call, and a TxFFE
  raise followed under it
- After an error: a failed LTS:L reported per end (`TmdsExit::Failed`), the
  `ExitedToTmds` and `ExitToTmdsFailed` events, and `exit_to_tmds_on_error: false`
  leaving both ends untouched (`TmdsExit::Skipped`); under `FLT_no_timeout` an error
  holding the link (`HeldUnderTest`), and exiting once the tester has cleared the flag
- A fallback whose LTS:L fails returning `TrainingError::ExitFailed` with each end's error
- `train_with_events` reporting each event as it occurs, without `alloc`
- `exit_to_tmds` on demand: the LTS:L sequence, taking down a link an error left in FRL,
  each end's error on failure, and `lts::exit_to_tmds` recording its event
- `Update_0`: plumbob clears only the three flags it acts on, one at a time, and a sink's
  `Status_Update`, `CED_Update`, `RR_Test` and `RSED_Update` stay set through training
- `Source_Test_Configuration`: every bit recorded in `SourceTestConfigRead`; a
  `Source_Test_Update` handled on the poll that sees it, without `FLT_update` in LTS:3 and
  before `FRL_start` in LTS:P
- TxFFE restrictions: sent with every equalization; kept across a rate drop and a retrain;
  a change in LTS:3 sent at once with the levels held, before the round's requests are
  acted on; an unchanged re-read not sent again
- The deadline pattern in architecture.md: one failed polled read in LTS:3 stops training,
  and LTS:L, whose read is not a poll, still returns the link to TMDS; a client following
  the rule (no failed read after `write_frl_config(NotSupported)`) lets an attempt whose
  deadline lands on LTS:L's read end with its own result, and one that does not turns it
  into `ExitFailed`
- Every new test above was checked against a deliberate bug (a reset of the restrictions
  on a rate drop or a retrain, a trip on LTS:2's read, an extra cleared flag, a late
  re-send): each fails its test

### Trace tests (requires `alloc` feature)

The `alloc`-gated tests exercise `train_traced` and `train_at_rate_traced` and assert on:

- The two example traces in [`architecture.md`]architecture.md, event for event
- Each timeout recording its limit and ending with `ExitedToTmds`, `RatesExhausted`, and
  `RetrainsExhausted`
- `SourceTestConfigRead` and `RetrainRequested` events, and no `FfeRaised` for a level
  already at the maximum
- `UndefinedLtpRequest` for an undefined value on every lane, with lane 3 at a 3-lane rate
  marked as not in use
- `TrainingTrace` carrying the rates and the `TrainingConfig`, so poll counts in events
  are interpretable against the configured limits
- Traced and untraced runs agreeing on the outcome
- `train_with_events` delivering exactly the events `train_traced` collects
- An error returned with its trace: the events up to the error, then LTS:L's event

### The sync driver

The state machine tests run through `FrlTrainer`, so they exercise `lts::run` driven
synchronously. One more test checks the driver's guard by running `block_on` over a
future that waits once — impossible over the sync adapter — with the trainer's own output
type: it panics rather than dropping the attempt.

A size test keeps the training future within a budget (456 bytes without `alloc`, 432
with it, over the simulator's zero-sized error types), so a change that grows it fails until the budget is raised on purpose. CI runs
the tests with the `std` feature and again with no features, so both budgets are
checked.

## Coverage

CI measures line coverage with `cargo-llvm-cov` over the `std` feature set. The baseline
is stored in `.coverage-baseline`; CI fails if coverage drops more than 0.1% below it.
New logic without tests will likely trip this.

## Philosophy

The training loop runs identically against simulated and real `ScdcClient` / `HdmiPhy`
implementations. A test that cannot run with a scripted `ScdcClient` does not belong in
this repository. Hardware is never a test dependency.