coverage.capture¶
The per-board capture.json artifact (spec §3).
A capture stores line/branch data in committed-code coordinates,
anchored to the base_commit whose numbering they mean, with
per-file blob SHAs as the rebase-tolerant validity anchor.
-
otto.coverage.capture.model.CAPTURE_FORMAT_VERSION =
2¶ capture.jsonschema version, bumped on breaking on-disk changes.Version 2 renames the git-anchor field from
pintobase_commit(JSON key and Python attribute alike) — clearer terminology for the commit whose line numbering a capture’s coordinates mean. There is no migration shim: a file that does not declare this exact version fails loud inCapture.load()with a message telling the caller to re-capture, rather than silently mis-reading the renamed key under its old name (or, worse, tripping pydantic’sextra="forbid"on the now-unknownpinkey with an unfriendly traceback).
- class otto.coverage.capture.model.CaptureFileCov(*, blob: str | None = None, lines: dict[int, int] = <factory>, branches: dict[int, list[tuple[int, int, int | None]]] = <factory>)¶
Bases:
BaseModelCoverage for one source file, keyed in
base_commitcoordinates.
- class otto.coverage.capture.model.Capture(*, schema: int = 2, tier: str, base_commit: str, dirty_remap: bool = False, captured_at: str = '', tester: dict[str, str] | None = None, ticket: str | None = None, note: str | None = None, labs: list[str] = <factory>, board: str = '', display_name: str | None = None, files: dict[str, ~otto.coverage.capture.model.CaptureFileCov] = <factory>)¶
Bases:
BaseModelOne board’s retrieval result — the universal capture artifact.
- files : dict[str, CaptureFileCov]¶
- classmethod load(path: Path) Capture¶
Deserialize a
Capturefrom path (unknown keys rejected).- Raises:¶
ValueError – The file’s
"schema"key does not matchCAPTURE_FORMAT_VERSION(including files with no"schema"key at all — everything predating this capture-format version). There is no migration shim; the message tells the caller to re-capture.
- otto.coverage.capture.model.parse_info(info_path: Path) dict[str, tuple[dict[int, int], dict[int, list[tuple[int, int, int | None]]]]]¶
Minimal SF/DA/BRDA parser: source path → (line hits, branch triples).
Branch triples are
(block, branch, taken)wheretakenisNonefor lcov’s-(branch never reached) and anintcount otherwise — preserving the never-reached/reached-but-not-taken distinction through to the capture file.
-
otto.coverage.capture.model.build_capture(*, info_path: Path, tier: str, repo_root: Path, board: str, labs: list[str], tester: dict[str, str] | None =
None, ticket: str | None =None, note: str | None =None, display_name: str | None =None, now: datetime | None =None) Capture¶ Build a
Captureanchored tobase_commitfrom an lcov.infofile.- Parameters:¶
display_name – Host display name to annotate onto the capture;
boardstays the staging-dir/host-id name.
Map line numbers across a -U0 unified diff.
One engine, two uses (spec §6): retrieval-time dirty-tree correction
(NEW = modified working tree → OLD = HEAD) and report-time manual
validity (OLD = capture anchored at base_commit → NEW = current
source). Lines inside a hunk on either side have no counterpart and
map to None.
- class otto.coverage.capture.remap.Hunk(old_start: int, old_count: int, new_start: int, new_count: int)¶
Bases:
objectOne
@@ -a,b +c,d @@header (counts default to 1 when omitted).
- otto.coverage.capture.remap.parse_u0_hunks(diff_text: str) list[Hunk]¶
Parse hunk headers out of a single-file
-U0diff.
- class otto.coverage.capture.remap.LineRemapper(hunks: list[Hunk])¶
Bases:
objectBidirectional line mapping across the hunks of one file’s diff.
For each hunk, “has line moved past this hunk yet” is decided by a threshold computed from the other side’s fields translated through the cumulative offset – not from this hunk’s own same-side
start + count. That distinction only matters for a zero-count side (a pure insertion hasold_count == 0; a pure deletion hasnew_count == 0): git’s “position is the line before” convention means a zero-count side’s ownstartis not a reliable same-side boundary marker (some hunks encode it asold_start + offset, the line right at the gap; others as one less, the last untouched line before the gap – both occur in practice). Translating the always- unambiguous opposite side through the running offset sidesteps the ambiguity entirely:old_start + old_count(resp.new_start + new_count) is well-defined regardless of which convention the other side’s zero-count marker happens to follow.
Thin subprocess wrappers around the git plumbing coverage needs.
Everything is synchronous and side-effect-free on the repo (read-only
commands only). Callers pass the sut repo root; a non-repo raises
GitUnavailableError with a clean message.
Bases:
RuntimeErrorRaised when git cannot answer (not a repo / git missing).
- otto.coverage.capture.gitio.is_dirty(repo_root: Path) bool¶
Return True if there are uncommitted changes.
-
otto.coverage.capture.gitio.blob_sha(repo_root: Path, relpath: Path, rev: str =
'HEAD') str | None¶ Return the SHA of a blob at a path/revision, or None if not found.
The
./prefix makes git resolve the path againstrepo_root(our cwd) — a bareREV:<path>resolves against the repo toplevel, which is wrong wheneverrepo_rootis a subdirectory of a larger repo (e.g. a sut checked out inside another project).
- otto.coverage.capture.gitio.hash_object(repo_root: Path, path: Path) str¶
Return the SHA1 hash of a file object.
- otto.coverage.capture.gitio.blob_exists(repo_root: Path, sha: str) bool¶
Return True if a blob exists in the repository.
- otto.coverage.capture.gitio.cat_blob(repo_root: Path, sha: str) bytes¶
Return the contents of a blob.
- otto.coverage.capture.gitio.diff_worktree_file_u0(repo_root: Path, relpath: Path) str¶
Return unified diff (U0, whitespace-insensitive) of HEAD vs worktree file.
-w(--ignore-all-space) suppresses whitespace-only line modifications so a reformat/reindent does not invalidate manual coverage anchored to the untouched code. Safe for the line remapper: a whitespace-only modification is 1 line -> 1 line and shifts no counts, so hiding it loses no hunk-offset information (unlike--ignore-blank-lines, which would hide count-changing hunks). The SUTs are C/C++, where intra-string whitespace is not coverage- relevant, so the one behavioural case-walso equates (spacing inside a string literal) is an accepted, immaterial false-valid.
- otto.coverage.capture.gitio.diff_no_index_u0(path_a: Path, path_b: Path) str¶
Return unified diff (U0, whitespace-insensitive) between two files outside a repo.
-wmatchesdiff_worktree_file_u0()so the report-time anchor chain (base_commit blob vs current file) ignores whitespace-only edits the same way the dirty-tree remap does.git diff --no-indexexits 1 when the files differ — that is success here; with-wa whitespace-only difference exits 0 with empty output (hunkless), which the remapper treats as verbatim.
The committed in-repo manual-capture store (spec §3).
- otto.coverage.capture.store_dir.manual_store_dir(repo_root: Path) Path¶
Directory storing committed manual capture records.
- otto.coverage.capture.store_dir.write_manual_capture(capture: Capture, repo_root: Path) Path¶
Write a capture to the manual store and return its path.
- otto.coverage.capture.store_dir.load_manual_captures(repo_root: Path) list[Capture]¶
Load all manual captures from repo_root, sorted by filename.
Per-board capture.json production from fetched .gcda counters.
Turns the raw .gcda counters collected by otto test --cov into a
Capture per board, anchored to
base_commit. For each board directory under cov_dir this:
Resolves the board’s toolchain and gcno (build) source root from the
.otto_cov_meta.jsonsidecar via theotto.coverage.reporterhelpers.Runs
capture()for that board alone, producing<board>/board.info.Auto-discovers path mappings and rewrites the embedded
SF:paths to their local,repo_root-relative form, producing<board>/board.resolved.info.Builds and saves
<board>/capture.jsonviabuild_capture().
The raw .gcda, board.info, and board.resolved.info all stay
on disk as debug artifacts (spec decision 18).
-
async otto.coverage.capture.produce.produce_captures(cov_dir: Path, *, tier: str, repo_root: Path, labs: list[str], tester: dict[str, str] | None =
None, ticket: str | None =None, note: str | None =None, display_names: dict[str, str] | None =None) list[Path]¶ Produce a
capture.jsonanchored tobase_commitfor each board dir under cov_dir.A board dir is any direct subdirectory of cov_dir containing at least one
.gcdafile (recursively). Boards with no.gcdafiles are skipped with a warning.- Parameters:¶
cov_dir – Coverage directory written by
otto test --cov, containing per-board subdirs and a.otto_cov_meta.jsonsidecar.tier – Coverage tier name to annotate onto each capture.
repo_root – SUT git repo root, used for base_commit/blob resolution and as the path-remapping target.
labs – Lab identifiers to annotate onto each capture.
tester – Optional tester identity to annotate onto each capture.
ticket – Optional ticket reference to annotate onto each capture.
note – Optional free-text note to annotate onto each capture.
display_names – Board-dir name (host id) → host display name; boards without an entry are annotated
None.
- Returns:¶
Paths of the
capture.jsonfiles written, one per board, in board-name sort order.- Raises:¶
otto.coverage.capture.gitio.GitUnavailableError – If repo_root is not a git repository.