feat(coverage): function records, uncovered hotspots, subshell+parallel tests#660
Merged
Chemaclass merged 19 commits intomainfrom May 4, 2026
Merged
feat(coverage): function records, uncovered hotspots, subshell+parallel tests#660Chemaclass merged 19 commits intomainfrom
Chemaclass merged 19 commits intomainfrom
Conversation
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.
5 tasks
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.
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.
JesusValeraDev
approved these changes
May 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
FN/FNDA/FNF/FNH) sogenhtml, Codecov and Coveralls show per-function coverage. Reuses the existingextract_functionswalker — no new analysis.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).report_htmlindex/per-file output, covered/uncovered/non-executable row classification, and HTML escaping of source content.tests/unit/coverage_subshell_test.shfor$( ... ),( ... ), pipelines,< <( ... ), and functions invoked from$( ... ). Documents the limitation (subshell-local hits are lost) instead of the prior vague "may have edge cases".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 regressionsmake sa(ShellCheck)make lint(EditorConfig)[[, etc.)Commits
One commit per improvement point:
feat(coverage): emit function records in LCOV and optional text summarytest(coverage): cover HTML per-line test attribution tooltiptest(coverage): add HTML report generation teststest(coverage): pin subshell tracking behaviortest(coverage): cover parallel data aggregationfeat(coverage): list uncovered executable lines in text reportdocs(coverage): document new env vars and pin subshell contract