Skip to content

feat(coverage): function records, uncovered hotspots, subshell+parallel tests#660

Merged
Chemaclass merged 19 commits intomainfrom
feat/coverage-improvements
May 4, 2026
Merged

feat(coverage): function records, uncovered hotspots, subshell+parallel tests#660
Chemaclass merged 19 commits intomainfrom
feat/coverage-improvements

Conversation

@Chemaclass
Copy link
Copy Markdown
Member

Summary

Tightens the coverage feature on three fronts: surfacing data we already collect but never showed, locking down behavior with tests, and documenting the contract.

  • LCOV function records (FN/FNDA/FNF/FNH) so genhtml, Codecov and Coveralls show per-function coverage. Reuses the existing extract_functions walker — no new analysis.
  • Opt-in text report blocks gated on env vars to keep default output compact:
    • BASHUNIT_COVERAGE_SHOW_FUNCTIONS=true — per-function coverage table after the file list.
    • BASHUNIT_COVERAGE_SHOW_UNCOVERED=true — "Uncovered Lines" block listing missed line numbers per file, with consecutive lines compressed into ranges (src/foo.sh:12-14,18,22-25).
  • HTML report tests for report_html index/per-file output, covered/uncovered/non-executable row classification, and HTML escaping of source content.
  • Test attribution tooltip tests for the existing HTML feature: dedup of repeated hits, absence when no test data is present.
  • Subshell tracking contract pinned by tests/unit/coverage_subshell_test.sh for $( ... ), ( ... ), pipelines, < <( ... ), and functions invoked from $( ... ). Documents the limitation (subshell-local hits are lost) instead of the prior vague "may have edge cases".
  • Parallel aggregation tests for aggregate_parallel: per-PID merge, dedup of the tracked-files index, test-hits merge, no-op behavior, and empty per-PID files.

Test plan

  • ./bashunit tests/unit/coverage_reporting_test.sh — 29 tests
  • ./bashunit tests/unit/coverage_subshell_test.sh — 5 tests
  • ./bashunit tests/unit/coverage_parallel_aggregation_test.sh — 5 tests
  • ./bashunit --parallel tests/ — 1043 passed, no regressions
  • make sa (ShellCheck)
  • make lint (EditorConfig)
  • Bash 3.0+ compat preserved (no associative arrays, no [[, etc.)

Commits

One commit per improvement point:

  1. feat(coverage): emit function records in LCOV and optional text summary
  2. test(coverage): cover HTML per-line test attribution tooltip
  3. test(coverage): add HTML report generation tests
  4. test(coverage): pin subshell tracking behavior
  5. test(coverage): cover parallel data aggregation
  6. feat(coverage): list uncovered executable lines in text report
  7. docs(coverage): document new env vars and pin subshell contract

Chemaclass added 7 commits May 4, 2026 09:19
Adds FN/FNDA/FNF/FNH function records to the LCOV report (consumed by
genhtml, Codecov, Coveralls). Adds an opt-in per-function summary to the
text report, gated on BASHUNIT_COVERAGE_SHOW_FUNCTIONS=true to keep the
default output compact.
Adds tests for the HTML report's tooltip that lists which tests hit each
covered line: render path, dedup of repeated hits, and absence when no
test attribution data is present.
Covers index.html overall metrics rendering, per-file HTML page creation,
covered/uncovered/non-executable row classification, and HTML escaping of
special characters in source content.
Documents and verifies how the DEBUG-trap recorder behaves across
subshell forms via fixtures: command substitution, explicit ( ... ),
pipelines, process substitution, and functions invoked inside $(...).
Locks in the known limitation that hits produced inside a subshell are
not propagated back to the parent's data file.
Adds direct tests for aggregate_parallel: merging per-PID hits files,
deduplicating the tracked-files index, merging test-hit attribution
files, no-op behavior when nothing to merge, and graceful handling of
empty per-PID files.
Adds an opt-in "Uncovered Lines" section to the text report, gated on
BASHUNIT_COVERAGE_SHOW_UNCOVERED=true. Each tracked file contributes a
single line listing its missed executable lines, with consecutive line
numbers compressed into ranges (e.g. "src/foo.sh:12-14,18,22-25") so
sparse misses stay scannable. Files with full coverage are skipped.
Updates CHANGELOG with the new LCOV function records and the opt-in
text-report blocks (BASHUNIT_COVERAGE_SHOW_FUNCTIONS,
BASHUNIT_COVERAGE_SHOW_UNCOVERED). Replaces the vague "subshell may have
edge cases" note with the precise tracked-vs-lost behavior pinned by
the new subshell test fixtures.
Chemaclass and others added 9 commits May 4, 2026 10:11
Enabling the DEBUG trap inside a parallel test worker process makes the
worker fire the trap on every internal coordination command, which
combines with /tmp file-I/O contention to deadlock CI runners (15-minute
timeouts on Ubuntu/Alpine parallel jobs and on Windows Git Bash).

The contracts these tests pin are deterministic in single-process mode,
so the parallel run is not a useful execution context. Adds a guard
that calls bashunit::skip when BASHUNIT_PARALLEL_RUN=true or when
running on CYGWIN/MINGW/MSYS, and applies it to all five subshell
tests.
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.
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 self-assigned this May 4, 2026
@Chemaclass Chemaclass added the enhancement New feature or request label May 4, 2026
Chemaclass added 3 commits May 4, 2026 15:31
Replaces verbose "%%|*" / "%%|*" parameter-expansion peels with a
single IFS='|' read across the three pipe-delimited record formats
this module emits (function, branch, function-coverage row), and
folds the comma-separated arms split in compute_branch_hits into a
one-line IFS=',' read -ra. Net -26 lines, no behavior change.

Verified on /bin/bash 3.2.57 with the full unit suite in sequential
and parallel modes, plus make sa and make lint.
Converts _arm_taken from echo-via-subshell to a global-out
(_BASHUNIT_ARM_TAKEN_OUT) so compute_branch_hits no longer pays a
subshell per arm. Pulls the consecutive-line range compression in
the uncovered-lines text report into _compress_ranges so the loop
body stays focused on filtering and the formatting concern lives in
one named helper.

Bash 3.0+ verified on /bin/bash 3.2.57: 40 reporting+branch tests
pass, 809 unit tests pass under --parallel, make sa and make lint
stay green.
Removes the dead get_function_coverage helper that relied on Bash 4.3+
nameref (local -n with a silent fall-through that left _hits_ref
unset on Bash 3, returning all-zero coverage). The function had no
callers; HTML and LCOV reports already inline the same hit-walk.

Replaces the [[ ... ]] test operator with [ ... ] in the four
coverage test files (coverage_core_test.sh, coverage_executable_test.sh,
coverage_helpers_test.sh, coverage_reporting_test.sh) so the project's
documented prohibition is uniformly applied.

The whole coverage subtree now passes the Bash 3.0+ audit: zero [[,
declare -A, ${var,,}, ${var^^}, ${arr[-1]}, &>>, or local -n outside
string literals. Verified on /bin/bash 3.2.57 with 119 coverage tests
plus the full unit suite (809 passed) under --parallel.
@Chemaclass Chemaclass merged commit cac931d into main May 4, 2026
30 checks passed
@Chemaclass Chemaclass deleted the feat/coverage-improvements branch May 4, 2026 14:17
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