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.
Split one tree-wide git diff -M -w -U0 stream into per-file diffs.
One subprocess answers the whole anchor chain for a capture (spec §9):
rename detection (-M), whitespace immunity (-w), and per-file
hunks arrive in a single pass instead of one git diff per file.
Keys are OLD paths — the coordinates a capture is anchored in.
- class otto.coverage.capture.treediff.FileDiff(old_path: str, new_path: str | None, hunks: list[~otto.coverage.capture.remap.Hunk] = <factory>)¶
Bases:
objectOne file’s slice of a tree diff, in capture (old-path) terms.
- otto.coverage.capture.treediff.parse_multifile_u0(diff_text: str) dict[str, FileDiff]¶
Parse a multi-file
-U0diff into{old_path: FileDiff}.Sections are delimited by
diff --githeaders. Old/new paths come from---/+++lines when present (they carry rename targets and/dev/nullmarkers); clean renames have no hunk block, so their paths come fromrename from/rename tolines instead. Pure additions (old side/dev/null) are dropped — no capture is anchored in a file that did not exist at base.
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.hash_objects(repo_root: Path, paths: list[Path]) list[str]¶
Hash many files in one spawn via
git hash-object --stdin-paths.Output order matches paths order. Reads paths from stdin instead of argv so a whole capture’s worth of files costs one process instead of one per file (spec §9 batched fallback); applies the same attribute-driven filters as
hash_object(), so results are identical file-for-file.
- 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.blobs_exist(repo_root: Path, shas: list[str]) set[str]¶
Return the subset of shas present in the object database, in one spawn.
git cat-file --batch-checkanswers one line per input sha:<sha> <type> <size>when present,<sha> missingwhen absent.
- otto.coverage.capture.gitio.cat_blob(repo_root: Path, sha: str) bytes¶
Return the contents of a blob.
- otto.coverage.capture.gitio.cat_blobs(repo_root: Path, shas: list[str]) dict[str, bytes]¶
Fetch many blobs’ contents in one spawn via
git cat-file --batch.The batch stream is
<sha> <type> <size>\nfollowed by exactly size content bytes and a trailing\n, repeated per input sha (a sha git can’t find instead emits<sha> missing\n, skipped here). Duplicate shas in shas are fine — later entries just overwrite earlier ones in the returned dict.
- 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.
- otto.coverage.capture.gitio.diff_no_index_dir_u0(dir_a: Path, dir_b: Path) str¶
Return a unified diff (U0, whitespace-insensitive) between two sibling dirs.
One spawn answers every changed file’s diff for the batched blob- fallback index (spec §9 amendment) instead of one
--no-indexspawn per file, mirroringdiff_no_index_u0()’s-w/exit-code contract. dir_a and dir_b must be siblings (share a parent) — passing their bare names withcwd=dir_a.parentmakes git report paths as<dir_a.name>/<rel>/<dir_b.name>/<rel>(after its own standarda//b/prefix, whichparse_multifile_u0()already strips); the caller strips the remaining<dir_a.name>/prefix to recover capture-relative paths.
- otto.coverage.capture.gitio.diff_tree_u0(repo_root: Path, base: str) str¶
Tree-wide
-U0diff of base vs the working tree, rename-detecting.One subprocess per capture replaces the per-file anchor diffs (spec §9).
-Mreports renames (spec §8.2: git’s tracking is the whole policy);-wmatchesdiff_worktree_file_u0()’s whitespace immunity (spec §8.1);--relative -- .scopes and re-roots paths whenrepo_rootis nested inside a larger repository.- Raises:¶
GitUnavailableError – base is not a resolvable commit here (GC’d after a squash-merge, or absent from a shallow clone). Callers fall back to the per-file blob chain.
- otto.coverage.capture.gitio.is_shallow(repo_root: Path) bool¶
Return whether the repo is a shallow clone (affects anchor degradation hints).
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.
Same-context re-capture supersedes (spec §8.5): newest wins, never accumulate.
Accumulating two captures of the same (tier, label, host) context would
double-count that context’s coverage; the superseded capture drops out
of the runs table entirely. The third key component is the capture’s
board field — the host id — the same value carried into
RunRecord.host at report time (store v4).
- otto.coverage.capture.supersede.select_manual_captures(captures: list[Capture]) list[Capture]¶
Return winners (input order), newest
captured_atper context key.
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.