Native controller coverage
controller_coverage.json binds source hashes
to GCC and LLVM coverage reports. Both controller kernels and their streaming CLIs have
100% executable line and branch coverage in the recorded snapshot. The retained reports
include the earlier Rust 1.98.1 build; they are historical evidence and do not certify a new
compiler. These counts exclude benchmark harnesses and SystemVerilog. They establish
host execution coverage, not board qualification.
Production Rust builds use 1.99.0. Stable LLVM coverage additionally reports complete line,
region and function coverage for the library and CLI, but does not instrument branches.
The separate branch analysis uses nightly-2026-08-21, with llvm-tools-preview, without
changing the production compiler or disabling any branch. GCC 13.3.0 uses --coverage.
Build outputs and raw reports remain under ignored build/ and the crate's target/.
Python coverage report in CI
The CI test job requires 100% Python statement and branch coverage for
src/loop_timing_witness, tools and controller_test_support.py. It also writes
build/coverage/coverage.xml through export_python_coverage.py.
The export preserves repository-relative paths, including equal basenames in different
source directories, without changing the collected source set or coverage threshold. A successful test job retains that report, its SHA-256
and the tested commit as the python-coverage-<commit> artifact for seven days.
A separate job runs only on main, after the test job succeeds. It downloads the
artifact from the same workflow run and verifies its digest and commit against the
exact checkout before uploading only that XML report to Codecov with the python
flag. OIDC authentication belongs to the codecov environment; test execution has
only repository read permission. An upload error fails the CI category and its
required gate. Pull requests retain the test report without publishing it.
This report describes the configured Python source set. It does not measure C, C++, Rust, SystemVerilog or physical hardware. Those surfaces need their separate instrumented evidence below.
Compile instrumented public interfaces
Run from the repository root with its development environment installed. Use a fresh output directory so profiles from a different source revision cannot be merged.
mkdir -p build/controller-coverage
gcc -std=gnu11 -O0 --coverage -Icontrollers/c -c \
controllers/c/witness_controller.c -o build/controller-coverage/kernel.o
gcc -std=gnu11 -O0 --coverage -Icontrollers/c build/controller-coverage/kernel.o \
controllers/c/controller_cli.c -o build/controller-coverage/controller_cli
gcc -std=gnu11 -O0 --coverage -Icontrollers/c build/controller-coverage/kernel.o \
tests/native/controller_api_test.c -o build/controller-coverage/controller_api_test
build/controller-coverage/controller_api_test
rustup component add --toolchain nightly-2026-08-21 llvm-tools-preview
export RUSTUP_TOOLCHAIN=nightly-2026-08-21
export RUSTFLAGS='-C instrument-coverage -Z coverage-options=branch -C opt-level=0'
export LLVM_PROFILE_FILE="$PWD/build/controller-coverage/rust-%p-%m.profraw"
export CARGO_TARGET_DIR="$PWD/controllers/rust/target/controller-coverage"
cargo test --offline --locked --manifest-path controllers/rust/Cargo.toml
cargo build --offline --locked --bins --manifest-path controllers/rust/Cargo.toml
Execute the existing behavioral corpus
This calls the same parameterized tests against the instrumented executables. The RTL runner still invokes Icarus; the read, write, pipe and device tests still use real OS interfaces. It does not replace a controller, plant or simulator with a mock.
import importlib
import inspect
import itertools
import os
from pathlib import Path
import sys
root = Path.cwd()
sys.path.insert(0, str(root / "tests"))
import conftest
work = root / "build/controller-coverage"
programs = (
work / "controller_cli",
Path(os.environ["CARGO_TARGET_DIR"]) / "debug/witness-controller",
)
fixtures = {
"native_controllers": programs,
"tmp_path": work,
"run_rtl": conftest.run_rtl.__wrapped__(work),
}
for module_name in ("test_controller_inputs", "test_controller_parity"):
module = importlib.import_module(module_name)
for name, function in inspect.getmembers(module, inspect.isfunction):
if not name.startswith("test_"):
continue
marks = [m for m in getattr(function, "pytestmark", []) if m.name == "parametrize"]
for values in itertools.product(*(m.args[1] for m in marks)):
parameters = dict(zip((m.args[0] for m in marks), values, strict=True))
parameters.update(
{
key: fixtures[key]
for key in inspect.signature(function).parameters
if key in fixtures
}
)
function(**parameters)
Save or execute the Python block with .venv/bin/python from the repository root.
The corpus includes full-range default/maximal coefficients, deterministic random states,
negative fractions, reset, integrator rail recovery, malformed input, LF/CRLF, streaming
before EOF, /dev/full, invalid encoding and an actual nonblocking-pipe read failure.
Export and inspect every outcome
Run gcov -b -c -j build/controller-coverage/kernel.gcno and
gcov -b -c -j build/controller-coverage/controller_cli-controller_cli.gcno.
For each production C file, every executable line and every branch must have a positive
execution count in the resulting JSON report. A percentage rounded to 100 is insufficient.
Resolve the pinned toolchain's llvm-profdata and llvm-cov below
rustc --print sysroot, in lib/rustlib/x86_64-unknown-linux-gnu/bin/.
Merge all build/controller-coverage/*.profraw with llvm-profdata merge -sparse.
Use llvm-cov export --instr-profile=<merged.profdata> with the CLI ELF and -object
for every actual controller_api-* ELF below the coverage target directory; exclude
dependency metadata files. Inspect the lib.rs and main.rs file summaries: line,
region, function and branch covered counts must equal their totals. Preserve source hashes,
compiler versions, flags and report hashes with the result. Reset the coverage environment
variables before ordinary production builds or benchmarks.
Freestanding AMP C optimized trace coverage
The production RV64 firmware is built at -O2 without instrumentation. A separate debug-map
link recompiles only runtime/bare_metal/amp_controller.c with the exact production compiler
and flags plus -g, then links the other original objects unchanged. Coverage is admissible only
when the production and debug-map .text sections have equal sizes and SHA-256 digests.
Real Spike -l logs from the production lifecycle and the dedicated refusal tests are mapped to
that byte-identical optimized image. tests/test_amp_startup_admission_fault.py supplies the
remaining real MMIO, mailbox, overload, submission and post-entry hart observations on both
production plants. Exact-function or exact-branch traces from wrapper images are included only
after their linked instruction bytes and addresses match production. The retained profile covers
all 112 executable source lines and all 100 conditional machine-code edges. It executes 520 of
522 controller-attributed instructions; the remaining second wfi and loop-back occur after the
real logger has already terminated on the first published refusal.
These optimized instruction and edge counts are functional ISA evidence. They are not GCC source-branch counters and do not qualify physical U54 timing, cache policy, PMP isolation, HSS/Linux ownership or board acceptance.
Freestanding Rust C ABI coverage
tests/test_amp_rust_branch_coverage.py builds the original safe core and C ABI adapter
with Rust 1.99.0 source coverage, then runs the real C API and PID/LQR stream clients.
It also runs the safe core's public Rust API tests and links a test-owned no-std panic
producer to the adapter's unchanged panic handler. The host-only panic sink flushes the
actual profile before exiting. The test merges those profiles with the matching toolchain's
llvm-profdata and requires positive, fully covered line, region, function and branch
counts for both original Rust source files. RUSTC_BOOTSTRAP=1 is scoped to the profile
builds solely to enable Rust 1.99.0's unstable branch instrumentation option; production
builds and their warning gates retain their normal compiler settings. This is host source
coverage. The separately compiled RV64 fault in tests/test_amp_rust_panic.py exercises
the public target refusal path, but does not export target coverage counters or qualify
board timing.
Native run controller instrumentation
Kernel coverage above does not cover the Linux run lifecycle, artifact hashing, scheduling or transport. Measure those C++ paths through the actual native/RTL entry points in a new build allocation:
WITNESS_NATIVE_BUILD_ROOT="$PWD/build/native-run-coverage" \
RUN_SIMULATION_CFLAGS='-std=c++17 -Wall -Wextra -Werror --coverage -O2' \
RUN_SIMULATION_LDFLAGS='--coverage' \
.venv/bin/python -m pytest tests/test_native_run.py tests/test_native_run_output.py \
tests/test_native_run_metadata.py tests/test_native_configuration.py \
tests/test_native_configuration_mutation.py tests/test_native_hash_mutation.py \
tests/test_native_file_digest.py tests/test_native_scheduling.py tests/test_power_journal.py \
tests/test_native_final_drain.py
WITNESS_NATIVE_BUILD_ROOT selects separate mechanical and thermal build directories.
The Makefile creates nested output directories before invoking Verilator. Compiler and linker
instrumentation flags are explicit; their defaults retain ordinary builds. Record optimization flags with coverage; optimized reports may omit eliminated paths. Choose a new
allocation for each measurement to avoid merging old execution counts. After all native
processes exit, use gcov -b -c -j on run_simulation.gcno, run_configuration.gcno and
file_digest.gcno in each plant directory. Included runtime headers appear under their actual
source paths in the resulting JSON; filter by those paths, not generated Verilator or system
library totals. Inspect uncovered lines and branch counts before making any coverage claim.
CLI file-size-limit tests relocate their child counters with
GCOV_PREFIX.
The limit also affects profiling files, so those child counter files may be truncated and must
not be merged into the baseline. Their exit status and retained data prove behavior; they do
not supply valid compiler coverage counts. Never use a corrupted gcov export as coverage evidence.
C++ exception paths and positive UIO/PAC1934 acquisition need separate real evidence. Native simulation execution does not qualify hardware, and generated model C++ coverage does not prove SystemVerilog branch or CDC coverage. Retain each scope and compiler's evidence separately.
The native lifecycle API corpus builds both production plants with strict warnings and
--coverage -O2 in a separate allocation:
WITNESS_LIFECYCLE_BUILD_ROOT="$PWD/build/native-lifecycle-coverage" \
.venv/bin/python -m pytest tests/test_native_lifecycle_api.py tests/test_native_configuration_api.py
Its C++ entry point calls the public Simulation and run lifecycle APIs against the actual
AXI decoder, clocks, CDC and FIFO. It verifies register refusals, active-run reset refusal,
unread-record reset refusal, drain followed by successful bank reset, and start/check/finish
callback ordering during a real run. Completed-output cases use actual saved event/sample data
to verify repeated completion and writes after close are refused. Additional sample cases verify repeated/out-of-range
observations and invalid public API coefficient inputs for both controller selections, using
actual RTL samples. These callbacks observe the simulation lifecycle; they
do not provide physical PAC1934 evidence. Export run_lifecycle_test.gcno with gcov after all
cases exit and retain source/report hashes. This corpus supplements the production run CLI
coverage; keep its scope and counters separate rather than reporting a combined percentage.
The period-mismatch case supplies a valid configuration with a period different from the
compiled RTL register and requires refusal before START. The unstarted-sample case calls
the public sample operation before START and requires the actual AXI decoder to refuse
the sample read before the native controller or command commit can run. Both cases complete a
healthy run on the same device; the refused unstarted sample retains an empty event file
and a header-only trace separately from the recovery capture.
The read case also refuses the upper timebase word before the lower-word latch, then reads
the latched pair through the real AXI path.
The C++ lifecycle corpus additionally applies reversible soft RLIMIT_FSIZE caps with
SIGXFSZ ignored in its own process. Real header, event, sample and final-flush failures occur
while the cap is active; output cleanup finishes before the original limit and signal disposition
are restored. GCC writes profiling data only after that restoration. Those API counters can
therefore provide compiler evidence without truncating .gcda files. The CLI tests retain their
original hard limits and isolated profiling directories. Do not merge their damaged counters
with the API reports or substitute the API corpus for production CLI behavior tests.
For direct RTL source-flow coverage, set WITNESS_RTL_COVERAGE=1 for the same lifecycle and
configuration API corpus, choose a fresh WITNESS_LIFECYCLE_BUILD_ROOT, and set pytest's
--basetemp to a separate retained directory. The fixture builds the actual production RTL
with Verilator --coverage-line; each successful lifecycle executable writes a nonempty
events.bin.coverage.dat next to its real test outputs. Merge those files with
verilator_coverage --write merged.dat <files...> and retain the raw files, source hashes,
merged report and compiler version. Verilator's v_branch counters describe instrumented RTL
source-flow branches; they are separate from C++ gcov and do not prove CDC timing, synthesis
timing or physical board acceptance. Keep zero-count branches open for real stimulus or a
documented unreachable-invariant review.
The existing production AXI process corpus adds register, controller feedback and full-aperture
transport paths. Set WITNESS_RTL_COVERAGE=1, choose a fresh
WITNESS_AXI_COVERAGE_BUILD_ROOT, and run tests/test_axi_simulator.py with
tests/test_axi_transport_progress.py. The Make target builds the same RTL with
--coverage-line; each process that finishes through the public Q request writes a
axi_simulator.<pid>.coverage.dat beside its executable. Invalid-request cases exit through
their real error path and do not export a profile. Merge these raw files with the separately
retained lifecycle profiles only when source hashes and Verilator versions match. The AXI
build's FIFO parameter differs from the lifecycle build, so report the union of instrumented
points and its actual denominator rather than adding percentages.
The AXI process corpus also commits a command for cycle one while the actual RTL remains on
cycle zero. It requires one retained ACT_LATE FIFO record and unchanged miss and safe-state
registers, exercising accepted-but-untimely command handling through the real register path.
The same process corpus configures ramp, sine and clipped step references through AXI writes,
then reads successive reference snapshots and acknowledges each retained sample interrupt.
The module-specific arithmetic simulation checks both saturated reference values and their
clip flags. The register contract refuses reference mode 3, so that generator default case
is not a reachable AXI runtime path.
ExclusiveOutput supplies the same exclusive FILE ownership to controller outputs and the
power journal. The actual lifecycle corpus copies a captured FIFO record through that public
stream API, closes it, refuses a second close and borrow, and refuses recreating the same path.
Existing real output failures exercise constructor unwinding and partial-file cleanup. These
cases verify shared file ownership through simulation and real host files. They do not exercise
physical power acquisition or the logger's hardware-dependent worker start/stop sequence.
Public configuration API cases first complete an actual run, snapshot its finished/configuration registers and nonzero retained IRQ generation, then submit an invalid C++ configuration. The refusal must leave every observed register unchanged; a second actual healthy run must succeed. This supplements file-parser refusal tests and exercises the shared semantic validator before reset rather than relying on a transport refusal after configuration has already changed.
Final-backlog cases model a five-millisecond CPU delay during an actual eight-cycle run. The real 32-record FIFO retains 20 events without overflow; two regular drain batches cannot empty it, so final producer-quiescence drain must retrieve the remainder. Native PID/LQR cases replay the observed command through both public kernels, and frozen captures from both plants reach the importer/report path with eight observed deadlines and one partial tracking sample. This exercises final-drain execution; it does not fabricate a timeout or qualify host-wall latency.
The Linux scheduling API corpus builds a separate native executable with strict warnings and
gcov, uses actual unprivileged SCHED_BATCH in its own children, and checks unchanged real
policy after invalid public requests. It also exercises preserving inherited policy and applying
explicit normal policy/affinity. No syscall substitution or parent-thread scheduling change is used:
WITNESS_POLICY_BUILD_ROOT="$PWD/build/native-policy-coverage" \
.venv/bin/python -m pytest tests/test_native_policy_api.py
Export its actual policy_api_test.gcno with gcov and retain this scope separately from RTL
controller profiles. Positive privileged FIFO application still requires a permitted host policy.
The same test module launches the production simulation CLI through chrt --batch 0 for
both plants. Complete healthy runs must retain the inherited batch scheduler and affinity
in native metadata, with actual event counts and sample traces matching the receipt.
tests/test_axi_transport_progress.py exercises the production AXI process across idle,
held-bank-reset and completed-run states for both plants. Each case reads all 256 byte offsets
and submits every partial strobe mask at every offset, requiring actual SLVERR responses for
partial writes and unchanged configuration/staging readback. Request and response files retain
the actual model time for each access. The fixed 7 ns bus and 5 ns capture half-periods must
complete each serial access within 280 modeled nanoseconds; this is a simulation regression
bound, not a host or board latency guarantee.
Bank reset does not reset the AXI mailbox: axi_control_witness supplies the external reset to
the transport and the separate bank reset to the register banks. Remote decode still returns
SLVERR while the bank is held, so a held bank cannot by itself exercise the simulation's
100,000 ns AXI timeout. The test verifies response progress through those real states; it does
not exercise stopped clocks, an external-reset race or either timeout exception. Those guards
remain in the compiler report without a coverage exclusion. Physical UIO reads/writes cannot
return destination AXI response signals and require separate board acceptance.
tests/test_native_metadata_api.py uses the lifecycle executable after an actual healthy run.
It submits invalid public metadata configurations, requires refusal before file creation, then
writes the original run's receipt to the same path. Schema validation and independently computed
hashes bind that receipt to the actual completed configuration, event and tracking files. Both
plants exercise the shared validator before the metadata writer's fault-name array lookup.
tests/test_power_configuration_api.py builds the actual PAC1934 constructor and submits typed
configurations that bypass file parsing. Malformed identity, polling, CPU, sample-rate and rail
fields must refuse before IIO selection with the same semantic contract as the parser. These
compiler profiles cover configuration validation and early constructor refusal, not physical
sensor reads or journal worker execution. The journal's validation-before-output ordering is
source-inspected and compiled; invoking that constructor requires actual IIO and UIO resources.
RTL source-flow closure
The completed Verilator 5.020 measurement retains every source and elaboration
identity, omitting only instance hierarchy when taking the union. It covers all
24 production RTL files: 20 contain measured procedural source-flow points;
control_plant_witness.sv, fabric_control_witness.sv and icicle_witness.sv
contain continuous assignments and module wiring, and event_codes_pkg.sv
contains constants. Those four files have no procedural counter in this scope.
The completed union has 826 of 834 points executed, with 8 remaining raw zero counters accepted as source invariants. No point is excluded and no unexplained zero remains. The eight proofs do not count as runtime hits. The union combines actual public-port testbench, native lifecycle and AXI process profiles; it does not assert independent completeness for each plant.
tests/test_retained_interrupt.py reads the real 0xa8 IRQ status register
across three bus/capture clock ratios. It checks an older snapshot acknowledgement
while a newer generation is pending, coalesced safe/finish events, invalid
acknowledgements and common reset. This exercises retained_interrupt.sv:50;
the unknown-address default is at line 51 and was already executed.
Run the full original 32-bit counter programme separately from the ordinary suite:
make rtl-counter-saturation RTL_SATURATION_DIRECTORY=build/rtl-counter-full
make rtl-invariants RTL_INVARIANT_DIRECTORY=build/rtl-invariants-full
The first command drives public clocks and inputs for 2^32 + 8 ticks after
warmup. Six control_cycle period bindings (64, 256, 512, 16384, 32768 and
100000), three deadline_monitor miss limits (1, 2 and 3), and an actual full
FIFO reach UINT32_MAX, remain saturated and clear on common reset. The sources
and their 32-bit widths stay unchanged. The ordinary pytest regression runs
only one million ticks and checks progression/reset; it does not claim saturation.
A full run takes 50 minutes on the recorded shared host. Verilator retains
the original constant unsigned-threshold warning for miss limit 1, so this
programme does not certify strict RTL lint. Its C++ build uses strict warnings.
Coverage snapshots are cumulative observations. Verilator's unsigned 32-bit
coverage counters can wrap during this long run; preserve the intermediate
profiles and use positive observations to determine execution. Do not add these
snapshots as independent cases or treat their sum as an execution frequency.
Keep raw elaboration pages distinct: event_witness__A3 and
event_witness__A3_O20 are separate measured identities. Both original 32-bit
bindings were executed, including an exact explicit-parameter FIFO run.
| Raw zero source points | Accepted invariant and reproduction |
|---|---|
run_configuration_registers.sv:111 |
A successful public write can only select a listed sequential address; the default cannot follow an accepted write. The configuration proof runs six periods and both plant selectors. |
control_io_registers.sv:83,127 |
The nested read case has the same six addresses as its outer case; successful writes select the three sequential addresses. The public decoder assertions support this exact source inspection. |
axi_lite_clock_bridge.sv:147,148 (three points) |
The original ENABLE_LOCAL=0 binding makes local_valid=0; the local response branches cannot execute. Enabled-local elaborations remain separate runtime points. |
fixed_point_math.sv:28,29 (two points) |
The original thermal velocity-step binding has A=B=C=0, so neither negative clamp nor clipping can occur. This applies only to that verified hierarchy and parameter binding. |
make rtl-invariants runs 16 positive checks with Yosys: four default wrappers
and 12 configuration variants. The recorded proof review also checked four
intentionally false assertions and retained their concrete counterexamples.
The accepted argument uses two-state, one-step SAT and exact source/parameter
binding with unconstrained initial states. It makes no reset-reachability,
four-state, signal-toggle, MC/DC, CDC-timing, synthesis or physical-board claim.
Native C++ compiler edges and hardware acquisition remain separate scopes.
Native lifecycle guard proof
The lifecycle corpus retains the original production RTL, GCC 13.3 optimisation
level and nanosecond timers. commit_boundary delays a real one-cycle run through
Simulation::advance; the live-status read still sees an active run, while the
subsequent full-word command commit receives the actual finished-run SLVERR.
The trace retains one observed sample with submitted=0, and all three real
FIFO records are drained. This exercises the second status check in control_sample;
it does not replace the transport or inject a response.
final_drain_no_hooks exercises the real backlog without callbacks. run_timeout
and final_drain_timeout use acquisition callbacks to advance both production
clocks past the original timer limits. They require the exact timeout exception,
no completion callback, and the retained actual event/trace bytes. These tests
advance more than one billion simulated nanoseconds and therefore take longer
than ordinary lifecycle cases. They establish timer supervision in the simulation
time domain, rather than host-wall or board timing.
WITNESS_LIFECYCLE_BUILD_ROOT="$PWD/build/native-lifecycle-proof" \
.venv/bin/python -m pytest tests/test_native_lifecycle_api.py \
tests/test_native_configuration_api.py tests/test_native_metadata_api.py
make native-runtime-invariants
The Yosys wrappers elaborate the unchanged production control_io_registers,
run_configuration_registers, control_cycle, deadline_monitor and
clock_reset_release modules.
Four-step proofs check full-word cycle/value staging followed by commit, and
preservation of enabled, finished and safe state across non-reset transitions,
constant ABI/Q-format readback, and reset release after two running local edges.
Each intentionally false variant must produce a counterexample; parser errors
and solver timeouts cannot satisfy the negative control. JSON netlists and
counterexample waveforms are retained in build/native-invariants.
For the serial Simulation lifecycle, sample-read success establishes enabled
state; both staged writes succeed or the public API throws before commit.
There is no reset or competing bus writer inside control_sample. Under these
premises, the production decoder can refuse the commit only when finished or
safe is asserted, and those flags persist until reset. The subsequent status read
therefore handles that refusal. The defensive exception for refusal without
either flag remains in the source and raw coverage report. This compositional
proof applies to the declared serial simulation path; it does not assert that an
unqualified physical device, external reset or competing writer obeys the premises.
GCC JSON reports mark exception arcs with throw=true. Keep those arcs and their
counts, and inspect the accompanying -fdump-tree-cfg-lineno files rather than
renaming them as source decisions. An unexecuted allocation/unwind edge is not an
executed error path. Constant ABI/Q-format mismatches, deterministic reset release,
and the defensive commit guard require their own source evidence; the successful
period-mismatch test covers the independent variable period check. Report raw
executable lines, compiler arcs, proved source invariants and actual resource
limitations separately. Do not publish an unconditional 100% C++ compiler-edge claim.