Skip to content

perf(coverage): reject untracked files inside the DEBUG trap #1060

Closed

Description

@Chemaclass

Problem

The DEBUG trap calls a shell function for every executed line, including every line of bashunit's own framework code running inside the test body and the lifecycle hooks:

trap 'bashunit::coverage::record_line "${BASH_SOURCE[0]:-}" "${LINENO:-}"' DEBUG

record_line (src/coverage/engine.sh:215) then decides whether the file is tracked at all. Most events are framework lines that lose that decision and return early, after paying a function call, two local assignments and a cache scan.

Measured on Bash 3.2 arm64, macOS, running tests/unit/assert/basic_test.sh:

run wall
no coverage 0.39s
--coverage --coverage-paths /nonexistent (trap fires, nothing tracked) 1.63s
--coverage --coverage-paths src (11 files tracked) 6.42s

Splitting the 6.42s: 0.39s test work, roughly 1.24s raw trap dispatch, roughly 1.27s report phase (measured separately at 11 files), leaving roughly 3.5s of capture bookkeeping. That bookkeeping is the target here.

Proposal

Reject untracked files inside the trap command, before any function call.

  • Build a glob from BASHUNIT_COVERAGE_PATHS once in bashunit::coverage::init and export it.
  • Make the trap dispatch on it: trap 'case "${BASH_SOURCE[0]:-}" in $glob) bashunit::coverage::record_line "${BASH_SOURCE[0]}" "${LINENO}";; esac' DEBUG.
  • The exclude patterns stay in should_track, which still runs for the files the glob admits. The glob is a cheap pre-filter, not a replacement for the decision.

Be honest about the ceiling: 1.24s of the run is trap dispatch that no filter can remove. This issue targets the per-line work layered on top of it. The two hot-path caches are the other half of that work and have their own issue.

Where to change

  • src/coverage/engine.sh:34-55 enable_trap
  • src/coverage/config.sh:61-112 init, build and export the glob
  • src/coverage/paths.sh:29-125 should_track stays authoritative for exclusions

Acceptance criteria

  • Coverage totals and LCOV output are identical to the current implementation for the same run
  • A relative coverage path (src), an absolute one, and several comma-separated paths all still track their files
  • BASHUNIT_COVERAGE_EXCLUDE still excludes a file that the glob admits
  • A wall-clock comparison at a fixed test file, before and after, in the PR description
  • The trap string stays valid under set -u in subshells, which is why the current one uses ${VAR:-}
  • A test asserting the glob cannot be broken by a path containing a space or a glob metacharacter
  • xtrace engine unaffected, since it never enters record_line

Repo checklist (agent)

  • TDD: RED then GREEN then REFACTOR. Write the failing test first.
  • Bash 3.0+ only: no printf -v, no += append, no declare -A, no [[ ]], no ${var,,}, no &>>, no ${arr[-1]}.
  • Gates: make sa, make lint, ./bashunit tests/, ./bashunit --parallel tests/. Never run shfmt -w.
  • CHANGELOG.md: one line under ## Unreleased (performance entry with the measured factor).
  • One issue = one PR.

Activity

  1. self-assigned this
    on Aug 12, 2026
  2. Chemaclass commented on Aug 12, 2026

    @Chemaclass
    MemberAuthor

    Done in #1078 (merged).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

  • Status
    Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions