Skip to content

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

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:

runwall
no coverage0.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-55enable_trap
  • src/coverage/config.sh:61-112init, build and export the glob
  • src/coverage/paths.sh:29-125should_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.

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

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Issue · GitHub
Skip to content

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

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:

runwall
no coverage0.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-55enable_trap
  • src/coverage/config.sh:61-112init, build and export the glob
  • src/coverage/paths.sh:29-125should_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.

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

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Issue · GitHub
Skip to content

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

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:

runwall
no coverage0.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-55enable_trap
  • src/coverage/config.sh:61-112init, build and export the glob
  • src/coverage/paths.sh:29-125should_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.

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

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Issue · GitHub
Skip to content

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

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:

runwall
no coverage0.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-55enable_trap
  • src/coverage/config.sh:61-112init, build and export the glob
  • src/coverage/paths.sh:29-125should_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.

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

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Issue · GitHub
Skip to content

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

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:

runwall
no coverage0.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-55enable_trap
  • src/coverage/config.sh:61-112init, build and export the glob
  • src/coverage/paths.sh:29-125should_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.

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

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Issue · GitHub
Skip to content

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

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:

runwall
no coverage0.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-55enable_trap
  • src/coverage/config.sh:61-112init, build and export the glob
  • src/coverage/paths.sh:29-125should_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.

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

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Issue · GitHub
Skip to content

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

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:

runwall
no coverage0.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-55enable_trap
  • src/coverage/config.sh:61-112init, build and export the glob
  • src/coverage/paths.sh:29-125should_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.

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

, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Issue · GitHub
Skip to content

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

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:

runwall
no coverage0.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-55enable_trap
  • src/coverage/config.sh:61-112init, build and export the glob
  • src/coverage/paths.sh:29-125should_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.

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