Skip to content

feat(coverage): branch coverage MVP via static branch-point detection#661

Merged
Chemaclass merged 7 commits intofeat/coverage-improvementsfrom
feat/coverage-branch-mvp
May 4, 2026
Merged

feat(coverage): branch coverage MVP via static branch-point detection#661
Chemaclass merged 7 commits intofeat/coverage-improvementsfrom
feat/coverage-branch-mvp

Conversation

@Chemaclass
Copy link
Copy Markdown
Member

Summary

Adds branch coverage to the LCOV report so reviewers see whether else/elif arms and individual case patterns were exercised. Stacked on top of #660 (function records + uncovered hotspots) and will rebase to main once #660 merges.

  • bashunit::coverage::extract_branches — single-pass parser that finds if/elif/else chains and case patterns, emitting one record per decision with the line ranges of each arm. Same shape as the existing extract_functions walker.
  • bashunit::coverage::compute_branch_hits — reuses the existing line-hit data file. An arm is taken iff at least one executable line inside its range was hit. Output: <decision_line>|<block>|<branch_index>|<taken>.
  • LCOV emit — adds BRDA:<line>,<block>,<branch>,<taken>, BRF:<count> and BRH:<count> per file, consumed unchanged by genhtml, Codecov and Coveralls.
  • ADR-007 — records the decision, the considered alternatives (DEBUG-trap decision tracing, source rewriting), and the explicit MVP scope vs. deferred follow-ups.

Why static + line-hit inference, not runtime tracing

DEBUG-trap decision tracing via BASH_COMMAND was rejected because:

  • BASH_COMMAND semantics diverge across Bash 3.x and 5.x for ((...)), [[...]] and pipelines.
  • Per-line overhead would compound the existing tracker cost.
  • Subshell context loss (already documented for line coverage) extends to runtime branch decisions, with no fix path.

Static parsing of branch points plus line-hit lookup keeps zero runtime overhead and a deterministic, testable contract.

Known limitations (documented)

  • An arm whose body has no executable lines (only comments/braces) registers as not-taken even when the conditional fired.
  • Implicit else (an if/elif chain without an explicit else) reports only the explicit arms.
  • Compound conditionals (if A && B) are reported as a single binary decision, not per sub-expression.
  • &&/|| short-circuit branches outside if and loop-entry decisions (while/until) are not tracked.

Each is listed in docs/coverage.md and adrs/adr-007-branch-coverage-mvp.md with a path to a follow-up if/when prioritized.

Test plan

  • ./bashunit tests/unit/coverage_branches_test.sh — 8 tests for extractor + hit computation
  • ./bashunit tests/unit/coverage_reporting_test.sh — 3 new BRDA tests (if/else, case, no-branches), 32 total
  • ./bashunit --parallel tests/unit/ — 814 passed, no regressions
  • make sa (ShellCheck), make lint (EditorConfig)
  • Bash 3.0+ compat (parallel indexed arrays only)

Commits

  1. docs(adr): branch coverage MVP design
  2. feat(coverage): branch extractor and hit computation
  3. test(coverage): cover BRDA/BRF/BRH emission in LCOV report
  4. docs(coverage): document branch coverage MVP

@Chemaclass Chemaclass self-assigned this May 4, 2026
@Chemaclass Chemaclass added the enhancement New feature or request label May 4, 2026
Chemaclass added 5 commits May 4, 2026 10:12
Records the decision to use static branch-point detection plus line-hit
inference for the branch-coverage MVP, scoping included constructs
(if/elif/else, case) and listing deferred items (implicit-else,
short-circuit branches, loop-entry decisions).
Adds bashunit::coverage::extract_branches, a single-pass parser that
discovers if/elif/else chains and case patterns and emits one record per
decision with the line ranges of each arm. Pairs it with
bashunit::coverage::compute_branch_hits, which walks the existing
line-hit data and marks each arm taken iff at least one executable line
inside its range was hit. Both functions are Bash 3.0+ compatible
(parallel indexed arrays in place of associative arrays).

See adrs/adr-007-branch-coverage-mvp.md for the design and known
limitations.
Verifies that the LCOV report emits branch records produced by
compute_branch_hits: one BRDA per arm for if/else and case, the BRF
total, the BRH count of taken arms, and that BRF/BRH are still emitted
(as zeros) for files with no branch points.
Adds the BRDA/BRF/BRH entry to the changelog and a "Branch Coverage
Scope" section to docs/coverage.md spelling out the limitations
(no-executable-line arms, implicit-else omission, compound conditional
folding, untracked short-circuit and loop-entry decisions).
Eliminates duplication in extract_branches by extracting two helpers:
- _append_arm: shared arm-close logic, returns via global to avoid
  per-line subshell cost.
- _is_case_pattern_line: case-pattern opener detection.

Folds the elif/else clauses (identical except for keyword) into one
branch. Replaces the IFS+set-- arm split in compute_branch_hits with
a parameter-expansion loop, and pulls the per-arm taken check into
_arm_taken. LCOV BRDA parsing now uses IFS='|' read for clarity.

Verified on /bin/bash 3.2.57 (macOS default): 814 unit tests pass,
parallel mode included. No new Bash 4+ constructs introduced.
@Chemaclass Chemaclass force-pushed the feat/coverage-branch-mvp branch from b018d86 to 21adf54 Compare May 4, 2026 08:12
Chemaclass added 2 commits May 4, 2026 10:20
Promotes branch coverage to a top-level section with: a what-counts
table, the two opt-in env vars (SHOW_FUNCTIONS, SHOW_UNCOVERED), a
worked example showing the full LCOV output for a partially-tested
if/elif/else chain, genhtml integration command, and a Codecov gate
recipe. Adds FN/FNDA/FNF/FNH and BRDA/BRF/BRH rows to the LCOV field
reference table.
Extracts six small handlers (_branch_push_if, _branch_close_if_arm,
_branch_emit_if, _branch_push_case, _branch_close_case_arm,
_branch_emit_case, _branch_open_case_pattern) that operate on the
state arrays kept as locals in extract_branches via Bash's dynamic
scoping. The main loop becomes a straightforward dispatch over the
first token of each line.

Bash 3.0+ compatibility preserved (no namerefs, no associative
arrays); verified on /bin/bash 3.2.57 with the full unit suite in
both sequential and parallel modes.
@Chemaclass Chemaclass merged commit fee2da3 into feat/coverage-improvements May 4, 2026
30 checks passed
@Chemaclass Chemaclass deleted the feat/coverage-branch-mvp branch May 4, 2026 13:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants