3"""The coverage census: what is enrolled, what measures it, why a row is unmeasured.
5``check_tree_coverage.py`` is the ENFORCER -- it reads the measured traces and
6judges them against the committed baseline. This module is the MODEL it judges
7against: which translation units are enrolled, which host projects measure
8them, what an unmeasured row is allowed to say, and how small any one root may
9legitimately get before the enumeration itself is the defect.
11Keeping the two apart is the same split ``lint_coverage_rules.py`` /
12``check_lint_coverage.py`` and ``tier_layers.py`` / ``check_tier_imports.py``
13already use here: the tables below are the part a human edits when a new
14measurement project or a new source root lands, and that edit is reviewable
15without reading the trace plumbing.
19Every first-party ``.c`` / ``.cc`` / ``.cpp`` under ``libs/``, ``src/``,
20``port/``, ``tools/``, ``apps/`` and ``examples/`` is enrolled -- firmware,
21platform, host tool and product alike. There is one quality bar for the tree
22and no tier gets a softer one, so there is no per-tier scope list to fall out
23of date. The enumeration itself comes from ``lint_targets.first_party_paths``,
24i.e. from ``git ls-files``, so a directory added tomorrow is enrolled the day
27Only three things are subtracted, and each is subtracted somewhere else first:
29* vendored SOUP and generated tables -- ``lint_targets`` already drops
30 ``libs/third_party/``, ``libs/ra8_fonts/``, ``tools/vela/generated/`` and
32* the individually registered generated sources in
33 ``lint_coverage_rules.PATH_CLASS`` -- a protobuf-c codec is its generator's
34 output, not hand-authored code;
35* test sources. A file under a ``tests/`` directory is the INSTRUMENT, not the
36 thing measured, and the ``tests/`` root is already outside the census by the
37 same reasoning. Applying it at any depth is what keeps the rule uniform
38 instead of a per-product carve-out: ``apps/shared_libs/mdl/tests/`` and
39 ``tools/ra8_emulator/tests/`` are test code exactly as ``tests/`` is.
41Headers carry no row. Inline code in a header is measured through the TUs that
42include it, and a header row would double-count it against whichever TU
43happened to be compiled first.
46from __future__
import annotations
49from dataclasses
import dataclass
50from pathlib
import Path
52sys.path.insert(0, str(Path(__file__).resolve().parent))
54from lint_coverage_rules
import PATH_CLASS
55from lint_targets
import first_party_paths
57REPO_ROOT = Path(__file__).resolve().parents[2]
61CENSUS_ROOTS: tuple[str, ...] = (
70CENSUS_SUFFIXES: tuple[str, ...] = (
".c",
".cc",
".cpp",
".cxx")
73TEST_DIR_COMPONENT =
"tests"
76GENERATED_CLASS =
"generated-source"
79def root_of(rel: str) -> str:
80 """Return the census root a repo-relative path belongs to, without its slash."""
81 return rel.split(
"/", 1)[0]
84def is_test_source(rel: str) -> bool:
85 """True when `rel` sits under a ``tests/`` directory at any depth."""
86 return TEST_DIR_COMPONENT
in rel.split(
"/")[:-1]
89def in_census(rel: str) -> bool:
90 """True when `rel` is an enrolled first-party translation unit.
92 The caller is expected to have filtered SOUP, generated tables and build
93 output already (``lint_targets`` does), so this adds only the three
94 subtractions this module owns: root, generated registry, test source.
96 if not rel.startswith(CENSUS_ROOTS)
or not rel.endswith(CENSUS_SUFFIXES):
98 if PATH_CLASS.get(rel) == GENERATED_CLASS:
100 return not is_test_source(rel)
103def census_paths(paths: list[str] |
None =
None) -> list[str]:
104 """Every enrolled translation unit, sorted.
107 paths: Repo-relative candidates to filter. Defaults to the tracked
108 first-party tree; the parameter exists so a selftest can drive the
109 rule with a fixture instead of the live checkout.
112 The enrolled repo-relative paths, sorted.
115 paths = first_party_paths(CENSUS_SUFFIXES)
116 return sorted(rel
for rel
in paths
if in_census(rel))
137@dataclass(frozen=True)
138class MeasurementProject:
139 """One host build that produces coverage data for the census."""
142 """Trace file stem and build subdirectory name."""
145 """Repo-relative directory handed to ``cmake -S``."""
147 subsumes: tuple[str, ...]
148 """Coverage-capable source roots this project configures as subdirectories."""
151 """Non-vacuity floor: census units this project's own report must carry. A
152 project whose build silently stopped instrumenting reports a handful of
153 files and the merged total still looks healthy, so the floor is per
154 project rather than on the merge."""
157 def claimed_dirs(self) -> tuple[str, ...]:
158 """Every source root whose coverage option this project is responsible for."""
159 return (self.cmake_dir, *self.subsumes)
162PROJECTS: tuple[MeasurementProject, ...] = (
164 MeasurementProject(
"host-tests",
"tests", (), 400),
166 MeasurementProject(
"mdl",
"apps/host/mdl", (
"apps/shared_libs/mdl",), 50),
170COVERAGE_OPTION_DECLARATION =
"option(RA8_COVERAGE"
173def coverage_capable_dirs(listfiles: dict[str, str]) -> list[str]:
174 """Return the directories whose listfiles declare the coverage option.
177 listfiles: Repo-relative listfile path -> its text.
180 The owning directories, sorted and de-duplicated.
183 rel.rsplit(
"/", 1)[0]
184 for rel, text
in listfiles.items()
185 if COVERAGE_OPTION_DECLARATION
in text
190def unclaimed_coverage_projects(dirs: list[str]) -> list[str]:
191 """Return coverage-capable directories no measurement project claims.
193 A directory is claimed when it IS a project's claimed root or sits under
194 one. Anything left over can produce coverage data that nothing collects,
195 which is how a whole product stays invisible while the gate reports a
198 claimed = {d
for project
in PROJECTS
for d
in project.claimed_dirs}
199 prefixes = tuple(f
"{d}/" for d
in sorted(claimed))
200 return [d
for d
in dirs
if d
not in claimed
and not d.startswith(prefixes)]
212REASON_FIRMWARE =
"firmware-composition"
213"""Only ever cross-compiled into an image: ``examples/`` and the firmware
214products under ``apps/``. There is no host process to run and no exit status to
215read, so no host coverage build can reach it."""
217REASON_PLATFORM =
"platform-cross-only"
218"""Platform code (``libs/``, ``src/``, ``port/``) that no host coverage build
219compiles at all -- board boot code, RTOS/USB stack ports, and drivers with no
220host double. It is compiled only by the ARM toolchain."""
222REASON_HOSTED =
"hosted-no-coverage-build"
223"""Host-side tool or product code whose CMake project is not wired into any
224measurement project. This is the one class that is pure debt: the code IS host
225executable, so the fix is to add the project to ``PROJECTS``, not to keep the
228REASON_COMPILED =
"compiled-not-executed"
229"""A measurement project COMPILED the unit and no test ever executed it, so
230gcov wrote a .gcno and never a .gcda. Usually a static-archive member no test
231binary pulls in. Named separately because it is invisible to a report-driven
232gate -- the unit simply does not appear -- which is how three of these sat
233outside a floor advertised as having no allowlist."""
235REASONS: tuple[str, ...] = (
242PLATFORM_ROOTS: tuple[str, ...] = (
"libs/",
"src/",
"port/")
243HOSTED_ROOTS: tuple[str, ...] = (
"tools/",
"apps/")
244FIRMWARE_ROOTS: tuple[str, ...] = (
"examples/",)
250PLATFORM_CROSS_ONLY_UNITS: frozenset[str] = frozenset(
251 {
"apps/shared_libs/reflow/v2/src/reflow_v2.cpp"}
255def is_firmware_composition(rel: str, firmware_dirs: tuple[str, ...]) -> bool:
256 """True when `rel` is only ever linked into a cross-compiled image.
258 ``examples/`` is firmware by root. Under ``apps/`` the root answers
259 nothing -- mdl is a host program and the e-reader is a TrustZone
260 image -- so the discriminator is ``lint_targets.firmware_app_dirs()``: an
261 app directory carrying BOTH a linker script and a vector table.
263 if rel.startswith(FIRMWARE_ROOTS):
265 return any(rel.startswith(f
"{d}/")
for d
in firmware_dirs)
268def structural_reason(rel: str, *, compiled: bool, firmware_dirs: tuple[str, ...]) -> str:
269 """Return the one reason class the tree says an unmeasured `rel` may carry.
272 rel: Repo-relative census path with no execution data.
273 compiled: Whether a measurement project's build compiled it anyway.
274 firmware_dirs: ``lint_targets.firmware_app_dirs()`` for this tree.
277 One member of ``REASONS``.
280 return REASON_COMPILED
281 if is_firmware_composition(rel, firmware_dirs):
282 return REASON_FIRMWARE
283 if rel
in PLATFORM_CROSS_ONLY_UNITS:
284 return REASON_PLATFORM
285 if rel.startswith(PLATFORM_ROOTS):
286 return REASON_PLATFORM
302ROOT_CENSUS_FLOORS: dict[str, int] = {
314"""Census units carrying execution data. Measured 507 when the gate landed; a
315drop past this means the measurement, not the tests, came apart."""
318def census_floor_failures(paths: list[str]) -> list[str]:
319 """Return one message per root whose census fell below its floor."""
320 counts = dict.fromkeys(ROOT_CENSUS_FLOORS, 0)
326 f
"census for root {root}/ collapsed to {counts[root]} unit(s), floor is {floor}"
327 for root, floor
in sorted(ROOT_CENSUS_FLOORS.items())
328 if counts[root] < floor