Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@
- Internal: `src/runner.sh` and `src/coverage.sh` are split into `src/runner/` and `src/coverage/` modules of single-responsibility files, each behind a `source`-only `index.sh` aggregator. A pure relocation, no behavior change; see [ADR-010](adrs/adr-010-src-module-directories.md) (#924, #925)

### Fixed
- Parallel runs no longer lose results when two test files in different directories share a filename. Per-test results were bucketed by basename, so the second file overwrote the first β€” silently, with the run still green. A tests/ tree mirroring a src/ tree makes that layout ordinary (#959)
- Coverage no longer counts variable assignments as functions. A line like `URL="https://${host}/api"` was reported as a function, inflating `FNF`/`FNH` in the LCOV report (17 phantom entries in bashunit's own run); when the value also contained a `|`, the malformed record aborted the LCOV writer with a raw bash arithmetic error (#936)
- `build.sh` dedupes embedded files by repo-relative path. The previous basename key compared the top-level loop's relative paths against the recursion's absolute ones, so a file reached from two places could be bundled twice in the released binary; it also collided for same-named files in different directories (#923)
- `bashunit doc` no longer errors when the default bootstrap file is missing (#929)
Expand Down
4 changes: 2 additions & 2 deletions src/runner/exec.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -64,8 +64,8 @@ function bashunit::runner::call_test_functions() {
# publish into it, and checking `[ -d ]` inside a worker races its siblings
# (every worker would still pay the mkdir fork).
if bashunit::parallel::is_enabled && [ "$allow_test_parallel" = true ]; then
local _suite_base="${script##*/}"
mkdir -p "${TEMP_DIR_PARALLEL_TEST_SUITE}/${_suite_base%.sh}" 2>/dev/null || true
bashunit::runner::parallel_suite_dir_to_slot "$script"
mkdir -p "$_BASHUNIT_RUNNER_SUITE_DIR_OUT" 2>/dev/null || true
fi

for fn_name in "${functions_to_run[@]+"${functions_to_run[@]}"}"; do
Expand Down
28 changes: 26 additions & 2 deletions src/runner/result.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,30 @@ function bashunit::runner::parse_result() {
fi
}

_BASHUNIT_RUNNER_SUITE_DIR_OUT=""

##
# Writes the parallel result directory for a test file into
# _BASHUNIT_RUNNER_SUITE_DIR_OUT.
#
# Keyed on the whole path with separators folded, never on the basename: two
# files sharing a basename in different directories used to land in one
# directory, where their per-suite ordinals collided and the second file's
# results overwrote the first's -- silently, with the run still green (#959).
# That is the same defect #923 fixed in build.sh. Mirroring a source tree in
# tests/ makes duplicate basenames ordinary, so this is a normal layout.
#
# Pure parameter expansion: this runs once per test in every parallel worker and
# the path must stay fork-free (.claude/rules/perf-fork-budget.md).
# Arguments: $1 - the test file path
##
function bashunit::runner::parallel_suite_dir_to_slot() {
local key="${1#./}"
key="${key%.sh}"
key="${key//\//_}"
_BASHUNIT_RUNNER_SUITE_DIR_OUT="${TEMP_DIR_PARALLEL_TEST_SUITE}/${key}"
}

function bashunit::runner::parse_result_parallel() {
local fn_name=$1
shift
Expand All@@ -25,8 +49,8 @@ function bashunit::runner::parse_result_parallel() {
# mkdir when the dir is missing (first test of the file wins the race,
# `-p` makes the losers no-ops), and name the result file by the per-suite
# ordinal the dispatcher assigned β€” unique without forking mktemp or mv.
local test_suite_base="${test_file##*/}"
local test_suite_dir="${TEMP_DIR_PARALLEL_TEST_SUITE}/${test_suite_base%.sh}"
bashunit::runner::parallel_suite_dir_to_slot "$test_file"
local test_suite_dir=$_BASHUNIT_RUNNER_SUITE_DIR_OUT
[ -d "$test_suite_dir" ] || mkdir -p "$test_suite_dir"

local unique_test_result_file="${test_suite_dir}/${_BASHUNIT_RUNNER_RESULT_ORDINAL}.result"
Expand Down
28 changes: 28 additions & 0 deletions tests/acceptance/bashunit_parallel_consistency_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,3 +42,31 @@ function test_jobs_auto_caps_at_detected_cores_and_matches_sequential() {

assert_equals "$sequential_summary" "$auto_summary"
}

# Per-test results used to be bucketed by the test file's BASENAME, so two files
# sharing one in different directories wrote into the same bucket and their
# per-suite ordinals collided -- the second file's results overwrote the first's,
# silently, with the run still green (#959). Mirroring a source tree in tests/
# makes duplicate basenames normal, so this is a realistic layout, not a corner.
function test_parallel_does_not_lose_same_named_files_in_different_dirs() {
local one=tests/acceptance/fixtures/dup_basename/one/test_dup.sh
local two=tests/acceptance/fixtures/dup_basename/two/test_dup.sh

local sequential_output
sequential_output=$(./bashunit --no-parallel --env "$TEST_ENV_FILE" "$one" "$two" 2>&1) || true

local parallel_output
parallel_output=$(./bashunit --parallel --env "$TEST_ENV_FILE" "$one" "$two" 2>&1) || true

# Counts only, not the rendered line: the parallel spinner leaves control bytes
# and indentation on the summary, which is cosmetic and not what this asserts.
local sequential_counts
sequential_counts=$(echo "$sequential_output" | grep -oE '[0-9]+ (passed|total)' | tr '\n' ' ') || true

local parallel_counts
parallel_counts=$(echo "$parallel_output" | grep -oE '[0-9]+ (passed|total)' | tr '\n' ' ') || true

assert_same "$sequential_counts" "$parallel_counts"
# Guard against both sides collapsing to nothing and matching vacuously.
assert_contains "2 total" "$sequential_counts"
}
5 changes: 5 additions & 0 deletions tests/acceptance/fixtures/dup_basename/one/test_dup.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash

function test_dup_in_dir_one() {
assert_same "one" "one"
}
5 changes: 5 additions & 0 deletions tests/acceptance/fixtures/dup_basename/two/test_dup.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash

function test_dup_in_dir_two() {
assert_same "two" "two"
}
Loading
, '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" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@
- Internal: `src/runner.sh` and `src/coverage.sh` are split into `src/runner/` and `src/coverage/` modules of single-responsibility files, each behind a `source`-only `index.sh` aggregator. A pure relocation, no behavior change; see [ADR-010](adrs/adr-010-src-module-directories.md) (#924, #925)

### Fixed
- Parallel runs no longer lose results when two test files in different directories share a filename. Per-test results were bucketed by basename, so the second file overwrote the first β€” silently, with the run still green. A tests/ tree mirroring a src/ tree makes that layout ordinary (#959)
- Coverage no longer counts variable assignments as functions. A line like `URL="https://${host}/api"` was reported as a function, inflating `FNF`/`FNH` in the LCOV report (17 phantom entries in bashunit's own run); when the value also contained a `|`, the malformed record aborted the LCOV writer with a raw bash arithmetic error (#936)
- `build.sh` dedupes embedded files by repo-relative path. The previous basename key compared the top-level loop's relative paths against the recursion's absolute ones, so a file reached from two places could be bundled twice in the released binary; it also collided for same-named files in different directories (#923)
- `bashunit doc` no longer errors when the default bootstrap file is missing (#929)
Expand Down
4 changes: 2 additions & 2 deletions src/runner/exec.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -64,8 +64,8 @@ function bashunit::runner::call_test_functions() {
# publish into it, and checking `[ -d ]` inside a worker races its siblings
# (every worker would still pay the mkdir fork).
if bashunit::parallel::is_enabled && [ "$allow_test_parallel" = true ]; then
local _suite_base="${script##*/}"
mkdir -p "${TEMP_DIR_PARALLEL_TEST_SUITE}/${_suite_base%.sh}" 2>/dev/null || true
bashunit::runner::parallel_suite_dir_to_slot "$script"
mkdir -p "$_BASHUNIT_RUNNER_SUITE_DIR_OUT" 2>/dev/null || true
fi

for fn_name in "${functions_to_run[@]+"${functions_to_run[@]}"}"; do
Expand Down
28 changes: 26 additions & 2 deletions src/runner/result.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,30 @@ function bashunit::runner::parse_result() {
fi
}

_BASHUNIT_RUNNER_SUITE_DIR_OUT=""

##
# Writes the parallel result directory for a test file into
# _BASHUNIT_RUNNER_SUITE_DIR_OUT.
#
# Keyed on the whole path with separators folded, never on the basename: two
# files sharing a basename in different directories used to land in one
# directory, where their per-suite ordinals collided and the second file's
# results overwrote the first's -- silently, with the run still green (#959).
# That is the same defect #923 fixed in build.sh. Mirroring a source tree in
# tests/ makes duplicate basenames ordinary, so this is a normal layout.
#
# Pure parameter expansion: this runs once per test in every parallel worker and
# the path must stay fork-free (.claude/rules/perf-fork-budget.md).
# Arguments: $1 - the test file path
##
function bashunit::runner::parallel_suite_dir_to_slot() {
local key="${1#./}"
key="${key%.sh}"
key="${key//\//_}"
_BASHUNIT_RUNNER_SUITE_DIR_OUT="${TEMP_DIR_PARALLEL_TEST_SUITE}/${key}"
}

function bashunit::runner::parse_result_parallel() {
local fn_name=$1
shift
Expand All@@ -25,8 +49,8 @@ function bashunit::runner::parse_result_parallel() {
# mkdir when the dir is missing (first test of the file wins the race,
# `-p` makes the losers no-ops), and name the result file by the per-suite
# ordinal the dispatcher assigned β€” unique without forking mktemp or mv.
local test_suite_base="${test_file##*/}"
local test_suite_dir="${TEMP_DIR_PARALLEL_TEST_SUITE}/${test_suite_base%.sh}"
bashunit::runner::parallel_suite_dir_to_slot "$test_file"
local test_suite_dir=$_BASHUNIT_RUNNER_SUITE_DIR_OUT
[ -d "$test_suite_dir" ] || mkdir -p "$test_suite_dir"

local unique_test_result_file="${test_suite_dir}/${_BASHUNIT_RUNNER_RESULT_ORDINAL}.result"
Expand Down
28 changes: 28 additions & 0 deletions tests/acceptance/bashunit_parallel_consistency_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,3 +42,31 @@ function test_jobs_auto_caps_at_detected_cores_and_matches_sequential() {

assert_equals "$sequential_summary" "$auto_summary"
}

# Per-test results used to be bucketed by the test file's BASENAME, so two files
# sharing one in different directories wrote into the same bucket and their
# per-suite ordinals collided -- the second file's results overwrote the first's,
# silently, with the run still green (#959). Mirroring a source tree in tests/
# makes duplicate basenames normal, so this is a realistic layout, not a corner.
function test_parallel_does_not_lose_same_named_files_in_different_dirs() {
local one=tests/acceptance/fixtures/dup_basename/one/test_dup.sh
local two=tests/acceptance/fixtures/dup_basename/two/test_dup.sh

local sequential_output
sequential_output=$(./bashunit --no-parallel --env "$TEST_ENV_FILE" "$one" "$two" 2>&1) || true

local parallel_output
parallel_output=$(./bashunit --parallel --env "$TEST_ENV_FILE" "$one" "$two" 2>&1) || true

# Counts only, not the rendered line: the parallel spinner leaves control bytes
# and indentation on the summary, which is cosmetic and not what this asserts.
local sequential_counts
sequential_counts=$(echo "$sequential_output" | grep -oE '[0-9]+ (passed|total)' | tr '\n' ' ') || true

local parallel_counts
parallel_counts=$(echo "$parallel_output" | grep -oE '[0-9]+ (passed|total)' | tr '\n' ' ') || true

assert_same "$sequential_counts" "$parallel_counts"
# Guard against both sides collapsing to nothing and matching vacuously.
assert_contains "2 total" "$sequential_counts"
}
5 changes: 5 additions & 0 deletions tests/acceptance/fixtures/dup_basename/one/test_dup.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash

function test_dup_in_dir_one() {
assert_same "one" "one"
}
5 changes: 5 additions & 0 deletions tests/acceptance/fixtures/dup_basename/two/test_dup.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash

function test_dup_in_dir_two() {
assert_same "two" "two"
}
Loading
, '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('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@
- Internal: `src/runner.sh` and `src/coverage.sh` are split into `src/runner/` and `src/coverage/` modules of single-responsibility files, each behind a `source`-only `index.sh` aggregator. A pure relocation, no behavior change; see [ADR-010](adrs/adr-010-src-module-directories.md) (#924, #925)

### Fixed
- Parallel runs no longer lose results when two test files in different directories share a filename. Per-test results were bucketed by basename, so the second file overwrote the first β€” silently, with the run still green. A tests/ tree mirroring a src/ tree makes that layout ordinary (#959)
- Coverage no longer counts variable assignments as functions. A line like `URL="https://${host}/api"` was reported as a function, inflating `FNF`/`FNH` in the LCOV report (17 phantom entries in bashunit's own run); when the value also contained a `|`, the malformed record aborted the LCOV writer with a raw bash arithmetic error (#936)
- `build.sh` dedupes embedded files by repo-relative path. The previous basename key compared the top-level loop's relative paths against the recursion's absolute ones, so a file reached from two places could be bundled twice in the released binary; it also collided for same-named files in different directories (#923)
- `bashunit doc` no longer errors when the default bootstrap file is missing (#929)
Expand Down
4 changes: 2 additions & 2 deletions src/runner/exec.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -64,8 +64,8 @@ function bashunit::runner::call_test_functions() {
# publish into it, and checking `[ -d ]` inside a worker races its siblings
# (every worker would still pay the mkdir fork).
if bashunit::parallel::is_enabled && [ "$allow_test_parallel" = true ]; then
local _suite_base="${script##*/}"
mkdir -p "${TEMP_DIR_PARALLEL_TEST_SUITE}/${_suite_base%.sh}" 2>/dev/null || true
bashunit::runner::parallel_suite_dir_to_slot "$script"
mkdir -p "$_BASHUNIT_RUNNER_SUITE_DIR_OUT" 2>/dev/null || true
fi

for fn_name in "${functions_to_run[@]+"${functions_to_run[@]}"}"; do
Expand Down
28 changes: 26 additions & 2 deletions src/runner/result.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,30 @@ function bashunit::runner::parse_result() {
fi
}

_BASHUNIT_RUNNER_SUITE_DIR_OUT=""

##
# Writes the parallel result directory for a test file into
# _BASHUNIT_RUNNER_SUITE_DIR_OUT.
#
# Keyed on the whole path with separators folded, never on the basename: two
# files sharing a basename in different directories used to land in one
# directory, where their per-suite ordinals collided and the second file's
# results overwrote the first's -- silently, with the run still green (#959).
# That is the same defect #923 fixed in build.sh. Mirroring a source tree in
# tests/ makes duplicate basenames ordinary, so this is a normal layout.
#
# Pure parameter expansion: this runs once per test in every parallel worker and
# the path must stay fork-free (.claude/rules/perf-fork-budget.md).
# Arguments: $1 - the test file path
##
function bashunit::runner::parallel_suite_dir_to_slot() {
local key="${1#./}"
key="${key%.sh}"
key="${key//\//_}"
_BASHUNIT_RUNNER_SUITE_DIR_OUT="${TEMP_DIR_PARALLEL_TEST_SUITE}/${key}"
}

function bashunit::runner::parse_result_parallel() {
local fn_name=$1
shift
Expand All@@ -25,8 +49,8 @@ function bashunit::runner::parse_result_parallel() {
# mkdir when the dir is missing (first test of the file wins the race,
# `-p` makes the losers no-ops), and name the result file by the per-suite
# ordinal the dispatcher assigned β€” unique without forking mktemp or mv.
local test_suite_base="${test_file##*/}"
local test_suite_dir="${TEMP_DIR_PARALLEL_TEST_SUITE}/${test_suite_base%.sh}"
bashunit::runner::parallel_suite_dir_to_slot "$test_file"
local test_suite_dir=$_BASHUNIT_RUNNER_SUITE_DIR_OUT
[ -d "$test_suite_dir" ] || mkdir -p "$test_suite_dir"

local unique_test_result_file="${test_suite_dir}/${_BASHUNIT_RUNNER_RESULT_ORDINAL}.result"
Expand Down
28 changes: 28 additions & 0 deletions tests/acceptance/bashunit_parallel_consistency_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,3 +42,31 @@ function test_jobs_auto_caps_at_detected_cores_and_matches_sequential() {

assert_equals "$sequential_summary" "$auto_summary"
}

# Per-test results used to be bucketed by the test file's BASENAME, so two files
# sharing one in different directories wrote into the same bucket and their
# per-suite ordinals collided -- the second file's results overwrote the first's,
# silently, with the run still green (#959). Mirroring a source tree in tests/
# makes duplicate basenames normal, so this is a realistic layout, not a corner.
function test_parallel_does_not_lose_same_named_files_in_different_dirs() {
local one=tests/acceptance/fixtures/dup_basename/one/test_dup.sh
local two=tests/acceptance/fixtures/dup_basename/two/test_dup.sh

local sequential_output
sequential_output=$(./bashunit --no-parallel --env "$TEST_ENV_FILE" "$one" "$two" 2>&1) || true

local parallel_output
parallel_output=$(./bashunit --parallel --env "$TEST_ENV_FILE" "$one" "$two" 2>&1) || true

# Counts only, not the rendered line: the parallel spinner leaves control bytes
# and indentation on the summary, which is cosmetic and not what this asserts.
local sequential_counts
sequential_counts=$(echo "$sequential_output" | grep -oE '[0-9]+ (passed|total)' | tr '\n' ' ') || true

local parallel_counts
parallel_counts=$(echo "$parallel_output" | grep -oE '[0-9]+ (passed|total)' | tr '\n' ' ') || true

assert_same "$sequential_counts" "$parallel_counts"
# Guard against both sides collapsing to nothing and matching vacuously.
assert_contains "2 total" "$sequential_counts"
}
5 changes: 5 additions & 0 deletions tests/acceptance/fixtures/dup_basename/one/test_dup.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash

function test_dup_in_dir_one() {
assert_same "one" "one"
}
5 changes: 5 additions & 0 deletions tests/acceptance/fixtures/dup_basename/two/test_dup.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash

function test_dup_in_dir_two() {
assert_same "two" "two"
}
Loading
, '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('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@
- Internal: `src/runner.sh` and `src/coverage.sh` are split into `src/runner/` and `src/coverage/` modules of single-responsibility files, each behind a `source`-only `index.sh` aggregator. A pure relocation, no behavior change; see [ADR-010](adrs/adr-010-src-module-directories.md) (#924, #925)

### Fixed
- Parallel runs no longer lose results when two test files in different directories share a filename. Per-test results were bucketed by basename, so the second file overwrote the first β€” silently, with the run still green. A tests/ tree mirroring a src/ tree makes that layout ordinary (#959)
- Coverage no longer counts variable assignments as functions. A line like `URL="https://${host}/api"` was reported as a function, inflating `FNF`/`FNH` in the LCOV report (17 phantom entries in bashunit's own run); when the value also contained a `|`, the malformed record aborted the LCOV writer with a raw bash arithmetic error (#936)
- `build.sh` dedupes embedded files by repo-relative path. The previous basename key compared the top-level loop's relative paths against the recursion's absolute ones, so a file reached from two places could be bundled twice in the released binary; it also collided for same-named files in different directories (#923)
- `bashunit doc` no longer errors when the default bootstrap file is missing (#929)
Expand Down
4 changes: 2 additions & 2 deletions src/runner/exec.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -64,8 +64,8 @@ function bashunit::runner::call_test_functions() {
# publish into it, and checking `[ -d ]` inside a worker races its siblings
# (every worker would still pay the mkdir fork).
if bashunit::parallel::is_enabled && [ "$allow_test_parallel" = true ]; then
local _suite_base="${script##*/}"
mkdir -p "${TEMP_DIR_PARALLEL_TEST_SUITE}/${_suite_base%.sh}" 2>/dev/null || true
bashunit::runner::parallel_suite_dir_to_slot "$script"
mkdir -p "$_BASHUNIT_RUNNER_SUITE_DIR_OUT" 2>/dev/null || true
fi

for fn_name in "${functions_to_run[@]+"${functions_to_run[@]}"}"; do
Expand Down
28 changes: 26 additions & 2 deletions src/runner/result.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,30 @@ function bashunit::runner::parse_result() {
fi
}

_BASHUNIT_RUNNER_SUITE_DIR_OUT=""

##
# Writes the parallel result directory for a test file into
# _BASHUNIT_RUNNER_SUITE_DIR_OUT.
#
# Keyed on the whole path with separators folded, never on the basename: two
# files sharing a basename in different directories used to land in one
# directory, where their per-suite ordinals collided and the second file's
# results overwrote the first's -- silently, with the run still green (#959).
# That is the same defect #923 fixed in build.sh. Mirroring a source tree in
# tests/ makes duplicate basenames ordinary, so this is a normal layout.
#
# Pure parameter expansion: this runs once per test in every parallel worker and
# the path must stay fork-free (.claude/rules/perf-fork-budget.md).
# Arguments: $1 - the test file path
##
function bashunit::runner::parallel_suite_dir_to_slot() {
local key="${1#./}"
key="${key%.sh}"
key="${key//\//_}"
_BASHUNIT_RUNNER_SUITE_DIR_OUT="${TEMP_DIR_PARALLEL_TEST_SUITE}/${key}"
}

function bashunit::runner::parse_result_parallel() {
local fn_name=$1
shift
Expand All@@ -25,8 +49,8 @@ function bashunit::runner::parse_result_parallel() {
# mkdir when the dir is missing (first test of the file wins the race,
# `-p` makes the losers no-ops), and name the result file by the per-suite
# ordinal the dispatcher assigned β€” unique without forking mktemp or mv.
local test_suite_base="${test_file##*/}"
local test_suite_dir="${TEMP_DIR_PARALLEL_TEST_SUITE}/${test_suite_base%.sh}"
bashunit::runner::parallel_suite_dir_to_slot "$test_file"
local test_suite_dir=$_BASHUNIT_RUNNER_SUITE_DIR_OUT
[ -d "$test_suite_dir" ] || mkdir -p "$test_suite_dir"

local unique_test_result_file="${test_suite_dir}/${_BASHUNIT_RUNNER_RESULT_ORDINAL}.result"
Expand Down
28 changes: 28 additions & 0 deletions tests/acceptance/bashunit_parallel_consistency_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,3 +42,31 @@ function test_jobs_auto_caps_at_detected_cores_and_matches_sequential() {

assert_equals "$sequential_summary" "$auto_summary"
}

# Per-test results used to be bucketed by the test file's BASENAME, so two files
# sharing one in different directories wrote into the same bucket and their
# per-suite ordinals collided -- the second file's results overwrote the first's,
# silently, with the run still green (#959). Mirroring a source tree in tests/
# makes duplicate basenames normal, so this is a realistic layout, not a corner.
function test_parallel_does_not_lose_same_named_files_in_different_dirs() {
local one=tests/acceptance/fixtures/dup_basename/one/test_dup.sh
local two=tests/acceptance/fixtures/dup_basename/two/test_dup.sh

local sequential_output
sequential_output=$(./bashunit --no-parallel --env "$TEST_ENV_FILE" "$one" "$two" 2>&1) || true

local parallel_output
parallel_output=$(./bashunit --parallel --env "$TEST_ENV_FILE" "$one" "$two" 2>&1) || true

# Counts only, not the rendered line: the parallel spinner leaves control bytes
# and indentation on the summary, which is cosmetic and not what this asserts.
local sequential_counts
sequential_counts=$(echo "$sequential_output" | grep -oE '[0-9]+ (passed|total)' | tr '\n' ' ') || true

local parallel_counts
parallel_counts=$(echo "$parallel_output" | grep -oE '[0-9]+ (passed|total)' | tr '\n' ' ') || true

assert_same "$sequential_counts" "$parallel_counts"
# Guard against both sides collapsing to nothing and matching vacuously.
assert_contains "2 total" "$sequential_counts"
}
5 changes: 5 additions & 0 deletions tests/acceptance/fixtures/dup_basename/one/test_dup.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash

function test_dup_in_dir_one() {
assert_same "one" "one"
}
5 changes: 5 additions & 0 deletions tests/acceptance/fixtures/dup_basename/two/test_dup.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash

function test_dup_in_dir_two() {
assert_same "two" "two"
}
Loading
, '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" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@
- Internal: `src/runner.sh` and `src/coverage.sh` are split into `src/runner/` and `src/coverage/` modules of single-responsibility files, each behind a `source`-only `index.sh` aggregator. A pure relocation, no behavior change; see [ADR-010](adrs/adr-010-src-module-directories.md) (#924, #925)

### Fixed
- Parallel runs no longer lose results when two test files in different directories share a filename. Per-test results were bucketed by basename, so the second file overwrote the first β€” silently, with the run still green. A tests/ tree mirroring a src/ tree makes that layout ordinary (#959)
- Coverage no longer counts variable assignments as functions. A line like `URL="https://${host}/api"` was reported as a function, inflating `FNF`/`FNH` in the LCOV report (17 phantom entries in bashunit's own run); when the value also contained a `|`, the malformed record aborted the LCOV writer with a raw bash arithmetic error (#936)
- `build.sh` dedupes embedded files by repo-relative path. The previous basename key compared the top-level loop's relative paths against the recursion's absolute ones, so a file reached from two places could be bundled twice in the released binary; it also collided for same-named files in different directories (#923)
- `bashunit doc` no longer errors when the default bootstrap file is missing (#929)
Expand Down
4 changes: 2 additions & 2 deletions src/runner/exec.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -64,8 +64,8 @@ function bashunit::runner::call_test_functions() {
# publish into it, and checking `[ -d ]` inside a worker races its siblings
# (every worker would still pay the mkdir fork).
if bashunit::parallel::is_enabled && [ "$allow_test_parallel" = true ]; then
local _suite_base="${script##*/}"
mkdir -p "${TEMP_DIR_PARALLEL_TEST_SUITE}/${_suite_base%.sh}" 2>/dev/null || true
bashunit::runner::parallel_suite_dir_to_slot "$script"
mkdir -p "$_BASHUNIT_RUNNER_SUITE_DIR_OUT" 2>/dev/null || true
fi

for fn_name in "${functions_to_run[@]+"${functions_to_run[@]}"}"; do
Expand Down
28 changes: 26 additions & 2 deletions src/runner/result.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,30 @@ function bashunit::runner::parse_result() {
fi
}

_BASHUNIT_RUNNER_SUITE_DIR_OUT=""

##
# Writes the parallel result directory for a test file into
# _BASHUNIT_RUNNER_SUITE_DIR_OUT.
#
# Keyed on the whole path with separators folded, never on the basename: two
# files sharing a basename in different directories used to land in one
# directory, where their per-suite ordinals collided and the second file's
# results overwrote the first's -- silently, with the run still green (#959).
# That is the same defect #923 fixed in build.sh. Mirroring a source tree in
# tests/ makes duplicate basenames ordinary, so this is a normal layout.
#
# Pure parameter expansion: this runs once per test in every parallel worker and
# the path must stay fork-free (.claude/rules/perf-fork-budget.md).
# Arguments: $1 - the test file path
##
function bashunit::runner::parallel_suite_dir_to_slot() {
local key="${1#./}"
key="${key%.sh}"
key="${key//\//_}"
_BASHUNIT_RUNNER_SUITE_DIR_OUT="${TEMP_DIR_PARALLEL_TEST_SUITE}/${key}"
}

function bashunit::runner::parse_result_parallel() {
local fn_name=$1
shift
Expand All@@ -25,8 +49,8 @@ function bashunit::runner::parse_result_parallel() {
# mkdir when the dir is missing (first test of the file wins the race,
# `-p` makes the losers no-ops), and name the result file by the per-suite
# ordinal the dispatcher assigned β€” unique without forking mktemp or mv.
local test_suite_base="${test_file##*/}"
local test_suite_dir="${TEMP_DIR_PARALLEL_TEST_SUITE}/${test_suite_base%.sh}"
bashunit::runner::parallel_suite_dir_to_slot "$test_file"
local test_suite_dir=$_BASHUNIT_RUNNER_SUITE_DIR_OUT
[ -d "$test_suite_dir" ] || mkdir -p "$test_suite_dir"

local unique_test_result_file="${test_suite_dir}/${_BASHUNIT_RUNNER_RESULT_ORDINAL}.result"
Expand Down
28 changes: 28 additions & 0 deletions tests/acceptance/bashunit_parallel_consistency_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,3 +42,31 @@ function test_jobs_auto_caps_at_detected_cores_and_matches_sequential() {

assert_equals "$sequential_summary" "$auto_summary"
}

# Per-test results used to be bucketed by the test file's BASENAME, so two files
# sharing one in different directories wrote into the same bucket and their
# per-suite ordinals collided -- the second file's results overwrote the first's,
# silently, with the run still green (#959). Mirroring a source tree in tests/
# makes duplicate basenames normal, so this is a realistic layout, not a corner.
function test_parallel_does_not_lose_same_named_files_in_different_dirs() {
local one=tests/acceptance/fixtures/dup_basename/one/test_dup.sh
local two=tests/acceptance/fixtures/dup_basename/two/test_dup.sh

local sequential_output
sequential_output=$(./bashunit --no-parallel --env "$TEST_ENV_FILE" "$one" "$two" 2>&1) || true

local parallel_output
parallel_output=$(./bashunit --parallel --env "$TEST_ENV_FILE" "$one" "$two" 2>&1) || true

# Counts only, not the rendered line: the parallel spinner leaves control bytes
# and indentation on the summary, which is cosmetic and not what this asserts.
local sequential_counts
sequential_counts=$(echo "$sequential_output" | grep -oE '[0-9]+ (passed|total)' | tr '\n' ' ') || true

local parallel_counts
parallel_counts=$(echo "$parallel_output" | grep -oE '[0-9]+ (passed|total)' | tr '\n' ' ') || true

assert_same "$sequential_counts" "$parallel_counts"
# Guard against both sides collapsing to nothing and matching vacuously.
assert_contains "2 total" "$sequential_counts"
}
5 changes: 5 additions & 0 deletions tests/acceptance/fixtures/dup_basename/one/test_dup.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash

function test_dup_in_dir_one() {
assert_same "one" "one"
}
5 changes: 5 additions & 0 deletions tests/acceptance/fixtures/dup_basename/two/test_dup.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash

function test_dup_in_dir_two() {
assert_same "two" "two"
}
Loading
, '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('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@
- Internal: `src/runner.sh` and `src/coverage.sh` are split into `src/runner/` and `src/coverage/` modules of single-responsibility files, each behind a `source`-only `index.sh` aggregator. A pure relocation, no behavior change; see [ADR-010](adrs/adr-010-src-module-directories.md) (#924, #925)

### Fixed
- Parallel runs no longer lose results when two test files in different directories share a filename. Per-test results were bucketed by basename, so the second file overwrote the first β€” silently, with the run still green. A tests/ tree mirroring a src/ tree makes that layout ordinary (#959)
- Coverage no longer counts variable assignments as functions. A line like `URL="https://${host}/api"` was reported as a function, inflating `FNF`/`FNH` in the LCOV report (17 phantom entries in bashunit's own run); when the value also contained a `|`, the malformed record aborted the LCOV writer with a raw bash arithmetic error (#936)
- `build.sh` dedupes embedded files by repo-relative path. The previous basename key compared the top-level loop's relative paths against the recursion's absolute ones, so a file reached from two places could be bundled twice in the released binary; it also collided for same-named files in different directories (#923)
- `bashunit doc` no longer errors when the default bootstrap file is missing (#929)
Expand Down
4 changes: 2 additions & 2 deletions src/runner/exec.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -64,8 +64,8 @@ function bashunit::runner::call_test_functions() {
# publish into it, and checking `[ -d ]` inside a worker races its siblings
# (every worker would still pay the mkdir fork).
if bashunit::parallel::is_enabled && [ "$allow_test_parallel" = true ]; then
local _suite_base="${script##*/}"
mkdir -p "${TEMP_DIR_PARALLEL_TEST_SUITE}/${_suite_base%.sh}" 2>/dev/null || true
bashunit::runner::parallel_suite_dir_to_slot "$script"
mkdir -p "$_BASHUNIT_RUNNER_SUITE_DIR_OUT" 2>/dev/null || true
fi

for fn_name in "${functions_to_run[@]+"${functions_to_run[@]}"}"; do
Expand Down
28 changes: 26 additions & 2 deletions src/runner/result.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,30 @@ function bashunit::runner::parse_result() {
fi
}

_BASHUNIT_RUNNER_SUITE_DIR_OUT=""

##
# Writes the parallel result directory for a test file into
# _BASHUNIT_RUNNER_SUITE_DIR_OUT.
#
# Keyed on the whole path with separators folded, never on the basename: two
# files sharing a basename in different directories used to land in one
# directory, where their per-suite ordinals collided and the second file's
# results overwrote the first's -- silently, with the run still green (#959).
# That is the same defect #923 fixed in build.sh. Mirroring a source tree in
# tests/ makes duplicate basenames ordinary, so this is a normal layout.
#
# Pure parameter expansion: this runs once per test in every parallel worker and
# the path must stay fork-free (.claude/rules/perf-fork-budget.md).
# Arguments: $1 - the test file path
##
function bashunit::runner::parallel_suite_dir_to_slot() {
local key="${1#./}"
key="${key%.sh}"
key="${key//\//_}"
_BASHUNIT_RUNNER_SUITE_DIR_OUT="${TEMP_DIR_PARALLEL_TEST_SUITE}/${key}"
}

function bashunit::runner::parse_result_parallel() {
local fn_name=$1
shift
Expand All@@ -25,8 +49,8 @@ function bashunit::runner::parse_result_parallel() {
# mkdir when the dir is missing (first test of the file wins the race,
# `-p` makes the losers no-ops), and name the result file by the per-suite
# ordinal the dispatcher assigned β€” unique without forking mktemp or mv.
local test_suite_base="${test_file##*/}"
local test_suite_dir="${TEMP_DIR_PARALLEL_TEST_SUITE}/${test_suite_base%.sh}"
bashunit::runner::parallel_suite_dir_to_slot "$test_file"
local test_suite_dir=$_BASHUNIT_RUNNER_SUITE_DIR_OUT
[ -d "$test_suite_dir" ] || mkdir -p "$test_suite_dir"

local unique_test_result_file="${test_suite_dir}/${_BASHUNIT_RUNNER_RESULT_ORDINAL}.result"
Expand Down
28 changes: 28 additions & 0 deletions tests/acceptance/bashunit_parallel_consistency_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,3 +42,31 @@ function test_jobs_auto_caps_at_detected_cores_and_matches_sequential() {

assert_equals "$sequential_summary" "$auto_summary"
}

# Per-test results used to be bucketed by the test file's BASENAME, so two files
# sharing one in different directories wrote into the same bucket and their
# per-suite ordinals collided -- the second file's results overwrote the first's,
# silently, with the run still green (#959). Mirroring a source tree in tests/
# makes duplicate basenames normal, so this is a realistic layout, not a corner.
function test_parallel_does_not_lose_same_named_files_in_different_dirs() {
local one=tests/acceptance/fixtures/dup_basename/one/test_dup.sh
local two=tests/acceptance/fixtures/dup_basename/two/test_dup.sh

local sequential_output
sequential_output=$(./bashunit --no-parallel --env "$TEST_ENV_FILE" "$one" "$two" 2>&1) || true

local parallel_output
parallel_output=$(./bashunit --parallel --env "$TEST_ENV_FILE" "$one" "$two" 2>&1) || true

# Counts only, not the rendered line: the parallel spinner leaves control bytes
# and indentation on the summary, which is cosmetic and not what this asserts.
local sequential_counts
sequential_counts=$(echo "$sequential_output" | grep -oE '[0-9]+ (passed|total)' | tr '\n' ' ') || true

local parallel_counts
parallel_counts=$(echo "$parallel_output" | grep -oE '[0-9]+ (passed|total)' | tr '\n' ' ') || true

assert_same "$sequential_counts" "$parallel_counts"
# Guard against both sides collapsing to nothing and matching vacuously.
assert_contains "2 total" "$sequential_counts"
}
5 changes: 5 additions & 0 deletions tests/acceptance/fixtures/dup_basename/one/test_dup.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash

function test_dup_in_dir_one() {
assert_same "one" "one"
}
5 changes: 5 additions & 0 deletions tests/acceptance/fixtures/dup_basename/two/test_dup.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash

function test_dup_in_dir_two() {
assert_same "two" "two"
}
Loading
, '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('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@
- Internal: `src/runner.sh` and `src/coverage.sh` are split into `src/runner/` and `src/coverage/` modules of single-responsibility files, each behind a `source`-only `index.sh` aggregator. A pure relocation, no behavior change; see [ADR-010](adrs/adr-010-src-module-directories.md) (#924, #925)

### Fixed
- Parallel runs no longer lose results when two test files in different directories share a filename. Per-test results were bucketed by basename, so the second file overwrote the first β€” silently, with the run still green. A tests/ tree mirroring a src/ tree makes that layout ordinary (#959)
- Coverage no longer counts variable assignments as functions. A line like `URL="https://${host}/api"` was reported as a function, inflating `FNF`/`FNH` in the LCOV report (17 phantom entries in bashunit's own run); when the value also contained a `|`, the malformed record aborted the LCOV writer with a raw bash arithmetic error (#936)
- `build.sh` dedupes embedded files by repo-relative path. The previous basename key compared the top-level loop's relative paths against the recursion's absolute ones, so a file reached from two places could be bundled twice in the released binary; it also collided for same-named files in different directories (#923)
- `bashunit doc` no longer errors when the default bootstrap file is missing (#929)
Expand Down
4 changes: 2 additions & 2 deletions src/runner/exec.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -64,8 +64,8 @@ function bashunit::runner::call_test_functions() {
# publish into it, and checking `[ -d ]` inside a worker races its siblings
# (every worker would still pay the mkdir fork).
if bashunit::parallel::is_enabled && [ "$allow_test_parallel" = true ]; then
local _suite_base="${script##*/}"
mkdir -p "${TEMP_DIR_PARALLEL_TEST_SUITE}/${_suite_base%.sh}" 2>/dev/null || true
bashunit::runner::parallel_suite_dir_to_slot "$script"
mkdir -p "$_BASHUNIT_RUNNER_SUITE_DIR_OUT" 2>/dev/null || true
fi

for fn_name in "${functions_to_run[@]+"${functions_to_run[@]}"}"; do
Expand Down
28 changes: 26 additions & 2 deletions src/runner/result.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,30 @@ function bashunit::runner::parse_result() {
fi
}

_BASHUNIT_RUNNER_SUITE_DIR_OUT=""

##
# Writes the parallel result directory for a test file into
# _BASHUNIT_RUNNER_SUITE_DIR_OUT.
#
# Keyed on the whole path with separators folded, never on the basename: two
# files sharing a basename in different directories used to land in one
# directory, where their per-suite ordinals collided and the second file's
# results overwrote the first's -- silently, with the run still green (#959).
# That is the same defect #923 fixed in build.sh. Mirroring a source tree in
# tests/ makes duplicate basenames ordinary, so this is a normal layout.
#
# Pure parameter expansion: this runs once per test in every parallel worker and
# the path must stay fork-free (.claude/rules/perf-fork-budget.md).
# Arguments: $1 - the test file path
##
function bashunit::runner::parallel_suite_dir_to_slot() {
local key="${1#./}"
key="${key%.sh}"
key="${key//\//_}"
_BASHUNIT_RUNNER_SUITE_DIR_OUT="${TEMP_DIR_PARALLEL_TEST_SUITE}/${key}"
}

function bashunit::runner::parse_result_parallel() {
local fn_name=$1
shift
Expand All@@ -25,8 +49,8 @@ function bashunit::runner::parse_result_parallel() {
# mkdir when the dir is missing (first test of the file wins the race,
# `-p` makes the losers no-ops), and name the result file by the per-suite
# ordinal the dispatcher assigned β€” unique without forking mktemp or mv.
local test_suite_base="${test_file##*/}"
local test_suite_dir="${TEMP_DIR_PARALLEL_TEST_SUITE}/${test_suite_base%.sh}"
bashunit::runner::parallel_suite_dir_to_slot "$test_file"
local test_suite_dir=$_BASHUNIT_RUNNER_SUITE_DIR_OUT
[ -d "$test_suite_dir" ] || mkdir -p "$test_suite_dir"

local unique_test_result_file="${test_suite_dir}/${_BASHUNIT_RUNNER_RESULT_ORDINAL}.result"
Expand Down
28 changes: 28 additions & 0 deletions tests/acceptance/bashunit_parallel_consistency_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,3 +42,31 @@ function test_jobs_auto_caps_at_detected_cores_and_matches_sequential() {

assert_equals "$sequential_summary" "$auto_summary"
}

# Per-test results used to be bucketed by the test file's BASENAME, so two files
# sharing one in different directories wrote into the same bucket and their
# per-suite ordinals collided -- the second file's results overwrote the first's,
# silently, with the run still green (#959). Mirroring a source tree in tests/
# makes duplicate basenames normal, so this is a realistic layout, not a corner.
function test_parallel_does_not_lose_same_named_files_in_different_dirs() {
local one=tests/acceptance/fixtures/dup_basename/one/test_dup.sh
local two=tests/acceptance/fixtures/dup_basename/two/test_dup.sh

local sequential_output
sequential_output=$(./bashunit --no-parallel --env "$TEST_ENV_FILE" "$one" "$two" 2>&1) || true

local parallel_output
parallel_output=$(./bashunit --parallel --env "$TEST_ENV_FILE" "$one" "$two" 2>&1) || true

# Counts only, not the rendered line: the parallel spinner leaves control bytes
# and indentation on the summary, which is cosmetic and not what this asserts.
local sequential_counts
sequential_counts=$(echo "$sequential_output" | grep -oE '[0-9]+ (passed|total)' | tr '\n' ' ') || true

local parallel_counts
parallel_counts=$(echo "$parallel_output" | grep -oE '[0-9]+ (passed|total)' | tr '\n' ' ') || true

assert_same "$sequential_counts" "$parallel_counts"
# Guard against both sides collapsing to nothing and matching vacuously.
assert_contains "2 total" "$sequential_counts"
}
5 changes: 5 additions & 0 deletions tests/acceptance/fixtures/dup_basename/one/test_dup.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash

function test_dup_in_dir_one() {
assert_same "one" "one"
}
5 changes: 5 additions & 0 deletions tests/acceptance/fixtures/dup_basename/two/test_dup.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash

function test_dup_in_dir_two() {
assert_same "two" "two"
}
Loading
, '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); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@
- Internal: `src/runner.sh` and `src/coverage.sh` are split into `src/runner/` and `src/coverage/` modules of single-responsibility files, each behind a `source`-only `index.sh` aggregator. A pure relocation, no behavior change; see [ADR-010](adrs/adr-010-src-module-directories.md) (#924, #925)

### Fixed
- Parallel runs no longer lose results when two test files in different directories share a filename. Per-test results were bucketed by basename, so the second file overwrote the first β€” silently, with the run still green. A tests/ tree mirroring a src/ tree makes that layout ordinary (#959)
- Coverage no longer counts variable assignments as functions. A line like `URL="https://${host}/api"` was reported as a function, inflating `FNF`/`FNH` in the LCOV report (17 phantom entries in bashunit's own run); when the value also contained a `|`, the malformed record aborted the LCOV writer with a raw bash arithmetic error (#936)
- `build.sh` dedupes embedded files by repo-relative path. The previous basename key compared the top-level loop's relative paths against the recursion's absolute ones, so a file reached from two places could be bundled twice in the released binary; it also collided for same-named files in different directories (#923)
- `bashunit doc` no longer errors when the default bootstrap file is missing (#929)
Expand Down
4 changes: 2 additions & 2 deletions src/runner/exec.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -64,8 +64,8 @@ function bashunit::runner::call_test_functions() {
# publish into it, and checking `[ -d ]` inside a worker races its siblings
# (every worker would still pay the mkdir fork).
if bashunit::parallel::is_enabled && [ "$allow_test_parallel" = true ]; then
local _suite_base="${script##*/}"
mkdir -p "${TEMP_DIR_PARALLEL_TEST_SUITE}/${_suite_base%.sh}" 2>/dev/null || true
bashunit::runner::parallel_suite_dir_to_slot "$script"
mkdir -p "$_BASHUNIT_RUNNER_SUITE_DIR_OUT" 2>/dev/null || true
fi

for fn_name in "${functions_to_run[@]+"${functions_to_run[@]}"}"; do
Expand Down
28 changes: 26 additions & 2 deletions src/runner/result.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,30 @@ function bashunit::runner::parse_result() {
fi
}

_BASHUNIT_RUNNER_SUITE_DIR_OUT=""

##
# Writes the parallel result directory for a test file into
# _BASHUNIT_RUNNER_SUITE_DIR_OUT.
#
# Keyed on the whole path with separators folded, never on the basename: two
# files sharing a basename in different directories used to land in one
# directory, where their per-suite ordinals collided and the second file's
# results overwrote the first's -- silently, with the run still green (#959).
# That is the same defect #923 fixed in build.sh. Mirroring a source tree in
# tests/ makes duplicate basenames ordinary, so this is a normal layout.
#
# Pure parameter expansion: this runs once per test in every parallel worker and
# the path must stay fork-free (.claude/rules/perf-fork-budget.md).
# Arguments: $1 - the test file path
##
function bashunit::runner::parallel_suite_dir_to_slot() {
local key="${1#./}"
key="${key%.sh}"
key="${key//\//_}"
_BASHUNIT_RUNNER_SUITE_DIR_OUT="${TEMP_DIR_PARALLEL_TEST_SUITE}/${key}"
}

function bashunit::runner::parse_result_parallel() {
local fn_name=$1
shift
Expand All@@ -25,8 +49,8 @@ function bashunit::runner::parse_result_parallel() {
# mkdir when the dir is missing (first test of the file wins the race,
# `-p` makes the losers no-ops), and name the result file by the per-suite
# ordinal the dispatcher assigned β€” unique without forking mktemp or mv.
local test_suite_base="${test_file##*/}"
local test_suite_dir="${TEMP_DIR_PARALLEL_TEST_SUITE}/${test_suite_base%.sh}"
bashunit::runner::parallel_suite_dir_to_slot "$test_file"
local test_suite_dir=$_BASHUNIT_RUNNER_SUITE_DIR_OUT
[ -d "$test_suite_dir" ] || mkdir -p "$test_suite_dir"

local unique_test_result_file="${test_suite_dir}/${_BASHUNIT_RUNNER_RESULT_ORDINAL}.result"
Expand Down
28 changes: 28 additions & 0 deletions tests/acceptance/bashunit_parallel_consistency_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,3 +42,31 @@ function test_jobs_auto_caps_at_detected_cores_and_matches_sequential() {

assert_equals "$sequential_summary" "$auto_summary"
}

# Per-test results used to be bucketed by the test file's BASENAME, so two files
# sharing one in different directories wrote into the same bucket and their
# per-suite ordinals collided -- the second file's results overwrote the first's,
# silently, with the run still green (#959). Mirroring a source tree in tests/
# makes duplicate basenames normal, so this is a realistic layout, not a corner.
function test_parallel_does_not_lose_same_named_files_in_different_dirs() {
local one=tests/acceptance/fixtures/dup_basename/one/test_dup.sh
local two=tests/acceptance/fixtures/dup_basename/two/test_dup.sh

local sequential_output
sequential_output=$(./bashunit --no-parallel --env "$TEST_ENV_FILE" "$one" "$two" 2>&1) || true

local parallel_output
parallel_output=$(./bashunit --parallel --env "$TEST_ENV_FILE" "$one" "$two" 2>&1) || true

# Counts only, not the rendered line: the parallel spinner leaves control bytes
# and indentation on the summary, which is cosmetic and not what this asserts.
local sequential_counts
sequential_counts=$(echo "$sequential_output" | grep -oE '[0-9]+ (passed|total)' | tr '\n' ' ') || true

local parallel_counts
parallel_counts=$(echo "$parallel_output" | grep -oE '[0-9]+ (passed|total)' | tr '\n' ' ') || true

assert_same "$sequential_counts" "$parallel_counts"
# Guard against both sides collapsing to nothing and matching vacuously.
assert_contains "2 total" "$sequential_counts"
}
5 changes: 5 additions & 0 deletions tests/acceptance/fixtures/dup_basename/one/test_dup.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash

function test_dup_in_dir_one() {
assert_same "one" "one"
}
5 changes: 5 additions & 0 deletions tests/acceptance/fixtures/dup_basename/two/test_dup.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash

function test_dup_in_dir_two() {
assert_same "two" "two"
}
Loading