coverage.capture

The per-board capture.json artifact (spec §3).

A capture stores line/branch data in committed-code coordinates, pinned to the commit whose numbering they mean, with per-file blob SHAs as the rebase-tolerant validity anchor.

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: BaseModel

Coverage for one source file, keyed in pin coordinates.

blob : str | None
lines : dict[int, int]
branches : dict[int, list[tuple[int, int, int | None]]]
class otto.coverage.capture.model.Capture(*, schema: int = 1, tier: str, pin: 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 = '', files: dict[str, ~otto.coverage.capture.model.CaptureFileCov] = <factory>)

Bases: BaseModel

One board’s retrieval result — the universal capture artifact.

schema_version : int
tier : str
pin : str
dirty_remap : bool
captured_at : str
tester : dict[str, str] | None
ticket : str | None
note : str | None
labs : list[str]
board : str
files : dict[str, CaptureFileCov]
save(path: Path) None

Serialize this capture to path as indented JSON (by alias).

classmethod load(path: Path) Capture

Deserialize a Capture from path (unknown keys rejected).

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) where taken is None for lcov’s - (branch never reached) and an int count 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, now: datetime | None = None) Capture

Build a pinned Capture from an lcov .info file.

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 = pinned capture → 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: object

One @@ -a,b +c,d @@ header (counts default to 1 when omitted).

old_start : int
old_count : int
new_start : int
new_count : int
otto.coverage.capture.remap.parse_u0_hunks(diff_text: str) list[Hunk]

Parse hunk headers out of a single-file -U0 diff.

class otto.coverage.capture.remap.LineRemapper(hunks: list[Hunk])

Bases: object

Bidirectional 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 has old_count == 0; a pure deletion has new_count == 0): git’s “position is the line before” convention means a zero-count side’s own start is not a reliable same-side boundary marker (some hunks encode it as old_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.

new_to_old(line: int) int | None

OLD-side line for NEW-side line, or None inside a changed hunk.

old_to_new(line: int) int | None

NEW-side line for OLD-side line, or None when changed/deleted.

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.

exception otto.coverage.capture.gitio.GitUnavailableError

Bases: RuntimeError

Raised when git cannot answer (not a repo / git missing).

otto.coverage.capture.gitio.head_commit(repo_root: Path) str

Return the current HEAD commit SHA.

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 against repo_root (our cwd) — a bare REV:<path> resolves against the repo toplevel, which is wrong whenever repo_root is 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) of HEAD vs worktree file.

otto.coverage.capture.gitio.diff_no_index_u0(path_a: Path, path_b: Path) str

Return unified diff (U0) between two files outside a repo.

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 pinned Capture per board. For each board directory under cov_dir this:

  1. Resolves the board’s toolchain and gcno (build) source root from the .otto_cov_meta.json sidecar via the otto.coverage.reporter helpers.

  2. Runs capture() for that board alone, producing <board>/board.info.

  3. Auto-discovers path mappings and rewrites the embedded SF: paths to their local, repo_root-relative form, producing <board>/board.resolved.info.

  4. Builds and saves <board>/capture.json via build_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) list[Path]

Produce a pinned capture.json for each board dir under cov_dir.

A board dir is any direct subdirectory of cov_dir containing at least one .gcda file (recursively). Boards with no .gcda files are skipped with a warning.

Parameters:
  • cov_dir – Coverage directory written by otto test --cov, containing per-board subdirs and a .otto_cov_meta.json sidecar.

  • tier – Coverage tier name to stamp onto each capture.

  • repo_root – SUT git repo root, used for pin/blob resolution and as the path-correlation target.

  • labs – Lab identifiers to stamp onto each capture.

  • tester – Optional tester identity to stamp onto each capture.

  • ticket – Optional ticket reference to stamp onto each capture.

  • note – Optional free-text note to stamp onto each capture.

Returns:

Paths of the capture.json files written, one per board, in board-name sort order.

Raises:

otto.coverage.capture.gitio.GitUnavailableError – If repo_root is not a git repository.