chore(core): use pre-processed files for coverage
What changed, and why it matters
This is a routine developer tooling change that improves how code-coverage reports are generated for the Trezor firmware. It makes coverage measurements more accurate by using the same preprocessed source files that are actually compiled into test builds, and avoids redundant rebuilds in CI. There is no user-facing change and no security relevance.
No security action required. Treat as normal maintenance.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit modifies the coverage workflow to reuse preprocessed .i files from the frozen build instead of rebuilding or mapping against original .py sources. Changes include: (1) CI artifact upload/download of build/unix/src/**/*.i files; (2) Makefile logic to skip the frozen build when .i files already exist; (3) coverage-collect.py remapping src/<rel>.py to build/unix/src/<rel>.i; (4) .coveragerc excluding if False: and elif False: lines created by feature-flag preprocessing; and (5) coverage-report updating exclude paths to .i extensions. This is purely a build/CI/coverage infrastructure change.
Changed components
core/.coveragerccore/Makefilecore/tools/coverage-collect.pycore/tools/coverage-report.github/workflows/core.ymlInspect captured patch +54 / −11
diff --git a/.github/workflows/core.yml b/.github/workflows/core.yml
index ab4e5bec..a53f751f 100644
--- a/.github/workflows/core.yml
+++ b/.github/workflows/core.yml
@@ -148,6 +148,7 @@ jobs:
path: |
core/build/unix/trezor-emu-core*
core/build/bootloader_emu/bootloader.elf
+ core/build/unix/src/**/*.i
retention-days: 7
core_emu_arm:
@@ -670,6 +671,7 @@ jobs:
name: Coverage report
runs-on: ubuntu-latest
needs:
+ - core_emu
- core_click_test
- core_persistence_test
- core_device_test
@@ -691,7 +693,15 @@ jobs:
pattern: core-coverage-${{ matrix.model }}-*
path: core
merge-multiple: true
+ # Reuse the frozen-build artifact produced by `core_emu` (noasan variant
+ # matches the test jobs that set TREZOR_PROFILING=1) so the preprocessed
+ # .i sources are available without rebuilding.
+ - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # actions/download-artifact@v8.0.0
+ with:
+ name: core-emu-${{ matrix.model }}-universal-debuglink-noasan
+ path: core/build
- uses: ./.github/actions/environment
+ # `make coverage` uses .i files downloaded above
- run: nix-shell --run "uv run make -C core coverage"
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # actions/upload-artifact@v7.0.0
with:
diff --git a/core/.coveragerc b/core/.coveragerc
index 17b39303..e10d10c8 100644
--- a/core/.coveragerc
+++ b/core/.coveragerc
@@ -3,6 +3,9 @@
exclude_lines =
from typing import
if TYPE_CHECKING:
+ # frozen-build preprocessing rewrites dead feature-flag branches to `if False:`
+ if False:
+ elif False:
# local const variables, e.g. _FIELD_TYPE_VL = const(7)
^_.*const\(\d+
assert False
diff --git a/core/Makefile b/core/Makefile
index caf780fb..0da3351d 100644
--- a/core/Makefile
+++ b/core/Makefile
@@ -537,7 +537,13 @@ upload: ## upload firmware using trezorctl
upload_prodtest: ## upload prodtest using trezorctl
trezorctl firmware_update -s -f $(PRODTEST_BUILD_DIR)/prodtest.bin
-coverage: ## generate coverage report
+# Skip the frozen-build rebuild when preprocessed `.i` files are already
+# present (e.g. downloaded as a CI artifact); otherwise build first so local
+# `make coverage` is self-contained.
+COVERAGE_I_SENTINEL := $(UNIX_BUILD_DIR)/src/trezor/utils.i
+COVERAGE_BUILD_DEP := $(if $(wildcard $(COVERAGE_I_SENTINEL)),,build_unix_frozen)
+
+coverage: $(COVERAGE_BUILD_DEP) ## generate coverage report
./tools/coverage-report
./tools/coverage-annotate.py $(shell find . -name '.coverage*.json*') > htmlcov/hits.md
diff --git a/core/tools/coverage-collect.py b/core/tools/coverage-collect.py
index cc84964f..56370578 100755
--- a/core/tools/coverage-collect.py
+++ b/core/tools/coverage-collect.py
@@ -1,6 +1,8 @@
#!/usr/bin/env python3
import json
+import os
import sys
+from pathlib import Path
import coverage
@@ -8,13 +10,32 @@ result_filename, *coverage_filenames = sys.argv[1:]
data = coverage.CoverageData(result_filename)
+
+def to_preprocessed_path(py_path: Path) -> Path:
+ # Remap <prefix>/src/<rel>.py → <prefix>/build/unix/src/<rel>.i so coverage
+ # reports against the preprocessed source actually compiled into the frozen
+ # build (dead feature-flag branches already rewritten to `if False:`).
+ # Fall back to the original .py path when no .i exists (e.g. unfrozen
+ # debug-only modules loaded directly from src/).
+ path_parts = py_path.parts
+ if py_path.suffix != ".py" or "src" not in path_parts:
+ return py_path
+ src_index = path_parts.index("src")
+ prefix = path_parts[:src_index]
+ rest = path_parts[src_index + 1 :]
+ i_path = Path(*prefix) / "build/unix/src" / Path(*rest).with_suffix(".i")
+ return i_path if os.path.exists(i_path) else py_path
+
+
for filename in coverage_filenames:
with open(filename) as f:
file_map = json.load(f)
lines = {}
for file_path, values in file_map.items():
# coverage doesn't support per-line counters
- lines[file_path] = [line for (line, _count) in values]
+ lines[str(to_preprocessed_path(Path(file_path)))] = [
+ line for (line, _count) in values
+ ]
data.add_lines(lines)
data.write()
diff --git a/core/tools/coverage-report b/core/tools/coverage-report
index 9aba7a54..ea1e4c61 100755
--- a/core/tools/coverage-report
+++ b/core/tools/coverage-report
@@ -9,19 +9,22 @@ fi
# Moving coverage files from src (tests usually save it there).
mv -v src/.coverage* . 2>/dev/null
-# Collect JSON coverage files (generated by `prof`) into a single .coverage file
+# Collect JSON coverage files (generated by `prof`) into a single .coverage file.
+# coverage-collect.py remaps src/foo/bar.py → build/unix/src/foo/bar.i so the
+# report is generated against the frozen-build preprocessed sources, where
+# model-/feature-specific dead branches are already `if False:` and caught by
+# the standard exclusions in .coveragerc.
ls -l .coverage*
./tools/coverage-collect.py .coverage .coverage*.json* || exit 1
EXCLUDES="\
-src/typing.py,\
-src/apps/ethereum/tokens.py,\
-src/apps/webauthn/knownapps.py,\
-src/apps/common/coininfo.py,\
-src/trezor/messages.py,\
-src/trezor/enums/__init__.py"
-
-# Uses core/.coveragerc configuration file
+*/typing.i,\
+*/apps/ethereum/tokens.i,\
+*/apps/webauthn/knownapps.i,\
+*/apps/common/coininfo.i,\
+*/trezor/messages.i,\
+*/trezor/enums/__init__.i"
+
coverage html \
--omit="$EXCLUDES" \
--fail-under=${COVERAGE_THRESHOLD}
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.