Skip to content

Measure test coverage in CI and cover the app and tools - #19

Merged
schreck61 merged 1 commit into
mainfrom
coverage-80
Nov 11, 2026
Merged

schreck61 merged 1 commit into
mainfrom
coverage-80

Conversation

@schreck61

Copy link
Copy Markdown
Owner

What

  • Tests: 38 new tests in four files raise line coverage from 54% to 96%, counting every file (measured on macOS):
    • tests/gui.rs (16) runs the app without a window using egui_kittest. It drives the app with the keyboard, the pointer, and the names its controls give screen readers. It covers every layout, control mode and controller, the side-by-side comparison, corridors and the green wave, every setting, overlay and coloring, placing and selecting vehicles, pan and zoom, both benchmark windows, and About. It also checks that every control has a name.
    • tests/scripted.rs (8) opens each TS_* scene in a separate process, because the app reads those variables when it starts and tests that run in parallel can't set them. It also checks that screenshots are valid PNG files.
    • tests/tools.rs (7) runs bench and train briefly as programs. Each runs in a scratch directory that also serves as its home directory, so it never touches the user's saved weights. Code that the programs run counts toward coverage.
    • tests/networks.rs (7) checks backpropagation against the numerical gradient. It also covers Adam, the shape checks that reject saved weights that don't fit, and Double DQN.
  • CI: a new Coverage job on ubuntu-22.04 runs cargo llvm-cov --locked --release --fail-under-lines 80 -- --test-threads=1, with no file excluded. It installs cargo-llvm-cov 0.9.1 with taiki-e/install-action, pinned by SHA.
  • Docs: CONTRIBUTING.md adds the 80% floor to Checks your change must pass and explains how to measure coverage locally. docs/architecture.md adds the job to §11 and the new test files to §10.3. The README mentions the new tests.
  • Dependencies: one new dev-dependency, egui_kittest 0.37 from the egui project, which brings in kittest 0.5. Its toml dependency needs toml_datetime 1.1.3 rather than 1.1.2. Some of the app's proc macros also use that crate, so THIRD_PARTY_LICENSES.md now lists the new version. Nothing else in the shipped binaries changes.

Production code is unchanged: nothing under src/ changed. Apart from tests, CI and docs, the only change is in Cargo.toml: the dev-dependency and a [lints.rust] entry that declares the coverage cfg so the compiler recognizes it.

Why

The tests ran 54% of the crate's lines. Nothing exercised the app, the bench and train programs, or the neural network on its own. This meets the test_statement_coverage80 criterion of the OpenSSF Best Practices silver badge, and the new job keeps it met.

Why the job runs the tests one at a time

Code built for coverage counts what it runs in counters that every thread updates without synchronization. When several threads run the same code at once, they keep taking those cache lines from one another, and everything slows down many times over:

Normal build Built for coverage
tests/safety.rs 6.8 s (24 s CPU) 51 s (524 s CPU)
tests/gui.rs, tests in parallel 0.34 s 103 s

Promoting the counters to registers (-C llvm-args=-do-counter-promotion) made no difference.

So the job passes --test-threads=1. The tests that run many simulations at once now start them with spawn from tests/common/mod.rs. Under cfg(coverage), which cargo-llvm-cov sets, spawn runs the simulations in turn; otherwise it runs them on threads as before. runs_are_reproducible keeps its two real threads, because it checks that runs agree when their hash maps iterate differently.

Run one at a time under coverage, the tests take about 140 s locally: cooperative 59 s, tools 37 s, gui 26 s and safety 13 s.

Results

Coverage as measured by cargo llvm-cov --release -- --test-threads=1 on macOS. Linux will differ a little, because some branches are platform-specific.

Area Lines Missed Covered
src/gui/ 3,586 167 95.3%
src/bin/ (bench, train) 513 12 97.7%
src/rl/ 224 3 98.7%
src/control/ 1,070 62 94.2%
src/sim/ 5,562 225 96.0%
src/lib.rs 18 0 100%
src/main.rs 18 18 0%
Total 10,991 487 95.57%

Before this change, coverage was 53.81%, with 5,914 lines missed. Three things stay uncovered:

  • src/main.rs, which only opens a native window.
  • The callback in shot::request (7 lines). It needs a renderer, which egui_kittest provides only with its wgpu feature, so the tests check write_png directly instead.
  • Branches for other platforms, such as where weights looks for saved weights.

Required check?

Coverage isn't a required check yet, and the ruleset is unchanged. It should become required once it has passed on main and its runtime on CI is known. That needs a ruleset change. It also needs an update to the list of required checks in GOVERNANCE.md, and to §11 of docs/architecture.md: its "10 are required" and its table.

Testing

  • cargo fmt --all --check and cargo fmt --manifest-path fuzz/Cargo.toml --check are clean.
  • cargo test --locked --release passes. One test is ignored on purpose: scene in tests/scripted.rs, which the other scripted tests run in a child process.
  • cargo clippy --locked --release --all-targets -- -D warnings and its wasm32 counterpart pass.
  • cargo deny --locked check, packaging/licenses.sh --check and cargo package --locked pass.
    • cargo-deny warns that async-executor 1.14.0 has been yanked. That warning isn't new: main has the same version, which eframe's accessibility support on Linux uses.
  • cargo llvm-cov --locked --release --fail-under-lines 80 -- --test-threads=1 passes locally, at 95.57%. The Coverage job hasn't run on Linux yet.

Follow-ups

  • The line counts in §3 of docs/architecture.md predate the rustfmt reformat. This change refreshes only the tests/ row.
  • async-executor 1.14.0 has been yanked and should be updated in a separate change.

The tests ran about 54% of the crate's lines: nothing exercised the app,
the bench and train tools, or the neural network directly. New tests
bring line coverage, every file counted, above 80%:

* tests/gui.rs drives the app without a window (egui_kittest) through
  its accessibility tree, the keyboard and the pointer: every layout,
  control mode and controller, the comparison, corridors and the green
  wave, every setting, overlay and coloring, placing and selecting
  vehicles, pan and zoom, both benchmark windows and About. It also
  checks that every control has a name a screen reader can say.
* tests/scripted.rs opens each TS_* scene in a process of its own, as
  the app reads the variables when it starts, and checks that
  screenshots are valid PNG files.
* tests/tools.rs runs bench and train briefly, in scratch directories
  with their own home directories.
* tests/networks.rs checks backpropagation against the numerical
  gradient, Adam, the shape checks and Double DQN.

A new Coverage job runs cargo-llvm-cov and fails below 80% of lines.
Code built for coverage counts what it runs in counters that every
thread updates, so threads running the same code at once slow one
another down many times over: tests/safety.rs took 51 s instead of 7 s,
with 21 times the CPU time. The job runs the tests one at a time, and
under cfg(coverage) the tests that run many simulations at once run
them in turn (tests/common/mod.rs). Promoting counters to registers
(-do-counter-promotion) didn't help.

egui_kittest's toml needs toml_datetime 1.1.3 rather than 1.1.2. Some
of the app's proc macros use that crate too, so THIRD_PARTY_LICENSES.md
now lists the new version.

CONTRIBUTING.md says how to measure coverage locally, and
docs/architecture.md and the README describe the new tests and job.

Signed-off-by: Jim Schreckengast <schreck@gmail.com>
@schreck61
schreck61 enabled auto-merge (rebase) November 11, 2026 16:29
@schreck61
schreck61 merged commit 176c1fd into main Nov 11, 2026
13 checks passed
@schreck61
schreck61 deleted the coverage-80 branch November 11, 2026 16:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant