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@@ -11,6 +11,7 @@
- Performance: `--coverage` is roughly 5x faster and `--coverage-report-html` roughly 19x β€” a run over this repo went from 16.2s to 2.9s, and a 128-file HTML report from 58.7s to 3.1s. The report phase emits each format in one awk invocation per run instead of Bash loops and forks per file and per row, and the capture path writes records straight to disk, normalizes a path with one fork instead of four, and reads each cache once (#1092, #1096, #1098, #1099, #1102, #1104, #1110, #1117)

### Fixed
- A run survives its scratch directory going missing instead of reporting a valid test file as unsourceable: `source "$file" 2>"$dir/source_err"` fails on the *redirect* when the directory is gone, which bash reports as the command failing, so the runner printed `Failed to source '<file>' (exit 1, ..., no stderr)` and named the one thing that was not at fault (#1163)
- Duplicate test functions are detected under `--parallel` again: the check ran inside the per-file loop, which `--parallel` backgrounds, so the state it set died with the subshell and a file where one of two same-named tests never ran reported "All tests passed" and exited 0. The report also names the line of every definition now, instead of leaving the second one to be found by hand (#1147)
- A `@data_provider` naming a function that is not defined no longer makes the test vanish: the run reported "No tests found", blaming a missing test rather than the missing provider and never printing the annotation's name. Both that and a provider that yields no data are now errors naming the provider, and under `--parallel` the failure reaches the aggregate instead of the run printing the error and exiting 0 (#1145)
- `mock`, `mock_sequence` and `spy` report a usable-name error instead of a raw bash syntax error when the command name carries whitespace or shell syntax β€” `mock "ls -l" echo hi` used to print `syntax error near unexpected token '-l'` from inside bashunit (#1136)
Expand Down
10 changes: 10 additions & 0 deletions src/runner/discovery.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -67,6 +67,16 @@ function bashunit::runner::load_test_files() {
# run-dir cleanup removes it, saving a mktemp and an rm fork per file.
local source_err_file source_err source_status
source_err_file="$_BASHUNIT_RUN_OUTPUT_DIR/source_err"
# A missing scratch dir makes the redirect below fail, and bash reports that
# as the *command* failing: exit 1 with nothing written to the capture file,
# which read as "this test file failed to source" against a file that was
# complete and valid (#1137). Restore the directory, and fall back to
# /dev/null if even that is refused -- losing a file's stderr capture is
# worth far less than failing the file for a reason that is not its own.
if [ ! -d "$_BASHUNIT_RUN_OUTPUT_DIR" ] &&
! mkdir -p "$_BASHUNIT_RUN_OUTPUT_DIR" 2>/dev/null; then
source_err_file=/dev/null
fi
# shellcheck source=/dev/null
source "$test_file" 2>"$source_err_file"
source_status=$?
Expand Down
54 changes: 54 additions & 0 deletions tests/acceptance/bashunit_source_redirect_test.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
set -euo pipefail

# `source "$file" 2>"$_BASHUNIT_RUN_OUTPUT_DIR/source_err"` fails when the run's
# scratch directory is missing -- and it fails on the *redirect*, so bash
# returns 1 having written nothing to the capture file. The runner read that as
# the test file failing to source and reported
#
# Failed to source 'x_test.sh' (exit 1, 52 bytes, no stderr)
#
# against a file that was complete and valid. That message sent three separate
# investigations at the fixture (#1137). The directory going missing mid-run is
# a separate open question; a run should survive it either way.

function set_up_before_script() {
TEST_ENV_FILE="tests/acceptance/fixtures/.env.default"
}

# Two files: the first removes the run's scratch directory while it is being
# sourced, which is exactly the state the flake produces; the second must still
# be sourced and run.
function test_a_file_still_runs_when_the_run_scratch_dir_disappeared() {
local dir
dir="$(bashunit::temp_dir)"
printf '%s\n' 'rm -rf "$_BASHUNIT_RUN_OUTPUT_DIR"
function test_removes_the_scratch_dir() { assert_same 1 1; }' >"$dir/a_killer_test.sh"
printf '%s\n' 'function test_runs_after_the_scratch_dir_went_away() { assert_same 2 2; }' \
>"$dir/b_victim_test.sh"

local output code=0
output="$(./bashunit --no-parallel --env "$TEST_ENV_FILE" \
"$dir/a_killer_test.sh" "$dir/b_victim_test.sh" 2>&1)" || code=$?
output="$(printf '%s' "$output" | strip_ansi)"

assert_same 0 "$code"
assert_contains "2 passed" "$output"
assert_not_contains "Failed to source" "$output"
}

# The genuine case must keep reporting: a file whose top level returns non-zero
# is still a source failure, and the message still says so.
function test_a_real_source_failure_is_still_reported() {
local fixture
fixture="$(bashunit::temp_file real_source_failure).sh"
printf '%s\n' 'function test_never_runs() { assert_same 1 1; }
false' >"$fixture"

local output code=0
output="$(./bashunit --no-parallel --env "$TEST_ENV_FILE" "$fixture" 2>&1)" || code=$?
output="$(printf '%s' "$output" | strip_ansi)"

assert_same 1 "$code"
assert_contains "Failed to source" "$output"
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n 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;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} 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@@ -11,6 +11,7 @@
- Performance: `--coverage` is roughly 5x faster and `--coverage-report-html` roughly 19x β€” a run over this repo went from 16.2s to 2.9s, and a 128-file HTML report from 58.7s to 3.1s. The report phase emits each format in one awk invocation per run instead of Bash loops and forks per file and per row, and the capture path writes records straight to disk, normalizes a path with one fork instead of four, and reads each cache once (#1092, #1096, #1098, #1099, #1102, #1104, #1110, #1117)

### Fixed
- A run survives its scratch directory going missing instead of reporting a valid test file as unsourceable: `source "$file" 2>"$dir/source_err"` fails on the *redirect* when the directory is gone, which bash reports as the command failing, so the runner printed `Failed to source '<file>' (exit 1, ..., no stderr)` and named the one thing that was not at fault (#1163)
- Duplicate test functions are detected under `--parallel` again: the check ran inside the per-file loop, which `--parallel` backgrounds, so the state it set died with the subshell and a file where one of two same-named tests never ran reported "All tests passed" and exited 0. The report also names the line of every definition now, instead of leaving the second one to be found by hand (#1147)
- A `@data_provider` naming a function that is not defined no longer makes the test vanish: the run reported "No tests found", blaming a missing test rather than the missing provider and never printing the annotation's name. Both that and a provider that yields no data are now errors naming the provider, and under `--parallel` the failure reaches the aggregate instead of the run printing the error and exiting 0 (#1145)
- `mock`, `mock_sequence` and `spy` report a usable-name error instead of a raw bash syntax error when the command name carries whitespace or shell syntax β€” `mock "ls -l" echo hi` used to print `syntax error near unexpected token '-l'` from inside bashunit (#1136)
Expand Down
10 changes: 10 additions & 0 deletions src/runner/discovery.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -67,6 +67,16 @@ function bashunit::runner::load_test_files() {
# run-dir cleanup removes it, saving a mktemp and an rm fork per file.
local source_err_file source_err source_status
source_err_file="$_BASHUNIT_RUN_OUTPUT_DIR/source_err"
# A missing scratch dir makes the redirect below fail, and bash reports that
# as the *command* failing: exit 1 with nothing written to the capture file,
# which read as "this test file failed to source" against a file that was
# complete and valid (#1137). Restore the directory, and fall back to
# /dev/null if even that is refused -- losing a file's stderr capture is
# worth far less than failing the file for a reason that is not its own.
if [ ! -d "$_BASHUNIT_RUN_OUTPUT_DIR" ] &&
! mkdir -p "$_BASHUNIT_RUN_OUTPUT_DIR" 2>/dev/null; then
source_err_file=/dev/null
fi
# shellcheck source=/dev/null
source "$test_file" 2>"$source_err_file"
source_status=$?
Expand Down
54 changes: 54 additions & 0 deletions tests/acceptance/bashunit_source_redirect_test.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
set -euo pipefail

# `source "$file" 2>"$_BASHUNIT_RUN_OUTPUT_DIR/source_err"` fails when the run's
# scratch directory is missing -- and it fails on the *redirect*, so bash
# returns 1 having written nothing to the capture file. The runner read that as
# the test file failing to source and reported
#
# Failed to source 'x_test.sh' (exit 1, 52 bytes, no stderr)
#
# against a file that was complete and valid. That message sent three separate
# investigations at the fixture (#1137). The directory going missing mid-run is
# a separate open question; a run should survive it either way.

function set_up_before_script() {
TEST_ENV_FILE="tests/acceptance/fixtures/.env.default"
}

# Two files: the first removes the run's scratch directory while it is being
# sourced, which is exactly the state the flake produces; the second must still
# be sourced and run.
function test_a_file_still_runs_when_the_run_scratch_dir_disappeared() {
local dir
dir="$(bashunit::temp_dir)"
printf '%s\n' 'rm -rf "$_BASHUNIT_RUN_OUTPUT_DIR"
function test_removes_the_scratch_dir() { assert_same 1 1; }' >"$dir/a_killer_test.sh"
printf '%s\n' 'function test_runs_after_the_scratch_dir_went_away() { assert_same 2 2; }' \
>"$dir/b_victim_test.sh"

local output code=0
output="$(./bashunit --no-parallel --env "$TEST_ENV_FILE" \
"$dir/a_killer_test.sh" "$dir/b_victim_test.sh" 2>&1)" || code=$?
output="$(printf '%s' "$output" | strip_ansi)"

assert_same 0 "$code"
assert_contains "2 passed" "$output"
assert_not_contains "Failed to source" "$output"
}

# The genuine case must keep reporting: a file whose top level returns non-zero
# is still a source failure, and the message still says so.
function test_a_real_source_failure_is_still_reported() {
local fixture
fixture="$(bashunit::temp_file real_source_failure).sh"
printf '%s\n' 'function test_never_runs() { assert_same 1 1; }
false' >"$fixture"

local output code=0
output="$(./bashunit --no-parallel --env "$TEST_ENV_FILE" "$fixture" 2>&1)" || code=$?
output="$(printf '%s' "$output" | strip_ansi)"

assert_same 1 "$code"
assert_contains "Failed to source" "$output"
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } 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@@ -11,6 +11,7 @@
- Performance: `--coverage` is roughly 5x faster and `--coverage-report-html` roughly 19x β€” a run over this repo went from 16.2s to 2.9s, and a 128-file HTML report from 58.7s to 3.1s. The report phase emits each format in one awk invocation per run instead of Bash loops and forks per file and per row, and the capture path writes records straight to disk, normalizes a path with one fork instead of four, and reads each cache once (#1092, #1096, #1098, #1099, #1102, #1104, #1110, #1117)

### Fixed
- A run survives its scratch directory going missing instead of reporting a valid test file as unsourceable: `source "$file" 2>"$dir/source_err"` fails on the *redirect* when the directory is gone, which bash reports as the command failing, so the runner printed `Failed to source '<file>' (exit 1, ..., no stderr)` and named the one thing that was not at fault (#1163)
- Duplicate test functions are detected under `--parallel` again: the check ran inside the per-file loop, which `--parallel` backgrounds, so the state it set died with the subshell and a file where one of two same-named tests never ran reported "All tests passed" and exited 0. The report also names the line of every definition now, instead of leaving the second one to be found by hand (#1147)
- A `@data_provider` naming a function that is not defined no longer makes the test vanish: the run reported "No tests found", blaming a missing test rather than the missing provider and never printing the annotation's name. Both that and a provider that yields no data are now errors naming the provider, and under `--parallel` the failure reaches the aggregate instead of the run printing the error and exiting 0 (#1145)
- `mock`, `mock_sequence` and `spy` report a usable-name error instead of a raw bash syntax error when the command name carries whitespace or shell syntax β€” `mock "ls -l" echo hi` used to print `syntax error near unexpected token '-l'` from inside bashunit (#1136)
Expand Down
10 changes: 10 additions & 0 deletions src/runner/discovery.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -67,6 +67,16 @@ function bashunit::runner::load_test_files() {
# run-dir cleanup removes it, saving a mktemp and an rm fork per file.
local source_err_file source_err source_status
source_err_file="$_BASHUNIT_RUN_OUTPUT_DIR/source_err"
# A missing scratch dir makes the redirect below fail, and bash reports that
# as the *command* failing: exit 1 with nothing written to the capture file,
# which read as "this test file failed to source" against a file that was
# complete and valid (#1137). Restore the directory, and fall back to
# /dev/null if even that is refused -- losing a file's stderr capture is
# worth far less than failing the file for a reason that is not its own.
if [ ! -d "$_BASHUNIT_RUN_OUTPUT_DIR" ] &&
! mkdir -p "$_BASHUNIT_RUN_OUTPUT_DIR" 2>/dev/null; then
source_err_file=/dev/null
fi
# shellcheck source=/dev/null
source "$test_file" 2>"$source_err_file"
source_status=$?
Expand Down
54 changes: 54 additions & 0 deletions tests/acceptance/bashunit_source_redirect_test.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
set -euo pipefail

# `source "$file" 2>"$_BASHUNIT_RUN_OUTPUT_DIR/source_err"` fails when the run's
# scratch directory is missing -- and it fails on the *redirect*, so bash
# returns 1 having written nothing to the capture file. The runner read that as
# the test file failing to source and reported
#
# Failed to source 'x_test.sh' (exit 1, 52 bytes, no stderr)
#
# against a file that was complete and valid. That message sent three separate
# investigations at the fixture (#1137). The directory going missing mid-run is
# a separate open question; a run should survive it either way.

function set_up_before_script() {
TEST_ENV_FILE="tests/acceptance/fixtures/.env.default"
}

# Two files: the first removes the run's scratch directory while it is being
# sourced, which is exactly the state the flake produces; the second must still
# be sourced and run.
function test_a_file_still_runs_when_the_run_scratch_dir_disappeared() {
local dir
dir="$(bashunit::temp_dir)"
printf '%s\n' 'rm -rf "$_BASHUNIT_RUN_OUTPUT_DIR"
function test_removes_the_scratch_dir() { assert_same 1 1; }' >"$dir/a_killer_test.sh"
printf '%s\n' 'function test_runs_after_the_scratch_dir_went_away() { assert_same 2 2; }' \
>"$dir/b_victim_test.sh"

local output code=0
output="$(./bashunit --no-parallel --env "$TEST_ENV_FILE" \
"$dir/a_killer_test.sh" "$dir/b_victim_test.sh" 2>&1)" || code=$?
output="$(printf '%s' "$output" | strip_ansi)"

assert_same 0 "$code"
assert_contains "2 passed" "$output"
assert_not_contains "Failed to source" "$output"
}

# The genuine case must keep reporting: a file whose top level returns non-zero
# is still a source failure, and the message still says so.
function test_a_real_source_failure_is_still_reported() {
local fixture
fixture="$(bashunit::temp_file real_source_failure).sh"
printf '%s\n' 'function test_never_runs() { assert_same 1 1; }
false' >"$fixture"

local output code=0
output="$(./bashunit --no-parallel --env "$TEST_ENV_FILE" "$fixture" 2>&1)" || code=$?
output="$(printf '%s' "$output" | strip_ansi)"

assert_same 1 "$code"
assert_contains "Failed to source" "$output"
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } 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@@ -11,6 +11,7 @@
- Performance: `--coverage` is roughly 5x faster and `--coverage-report-html` roughly 19x β€” a run over this repo went from 16.2s to 2.9s, and a 128-file HTML report from 58.7s to 3.1s. The report phase emits each format in one awk invocation per run instead of Bash loops and forks per file and per row, and the capture path writes records straight to disk, normalizes a path with one fork instead of four, and reads each cache once (#1092, #1096, #1098, #1099, #1102, #1104, #1110, #1117)

### Fixed
- A run survives its scratch directory going missing instead of reporting a valid test file as unsourceable: `source "$file" 2>"$dir/source_err"` fails on the *redirect* when the directory is gone, which bash reports as the command failing, so the runner printed `Failed to source '<file>' (exit 1, ..., no stderr)` and named the one thing that was not at fault (#1163)
- Duplicate test functions are detected under `--parallel` again: the check ran inside the per-file loop, which `--parallel` backgrounds, so the state it set died with the subshell and a file where one of two same-named tests never ran reported "All tests passed" and exited 0. The report also names the line of every definition now, instead of leaving the second one to be found by hand (#1147)
- A `@data_provider` naming a function that is not defined no longer makes the test vanish: the run reported "No tests found", blaming a missing test rather than the missing provider and never printing the annotation's name. Both that and a provider that yields no data are now errors naming the provider, and under `--parallel` the failure reaches the aggregate instead of the run printing the error and exiting 0 (#1145)
- `mock`, `mock_sequence` and `spy` report a usable-name error instead of a raw bash syntax error when the command name carries whitespace or shell syntax β€” `mock "ls -l" echo hi` used to print `syntax error near unexpected token '-l'` from inside bashunit (#1136)
Expand Down
10 changes: 10 additions & 0 deletions src/runner/discovery.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -67,6 +67,16 @@ function bashunit::runner::load_test_files() {
# run-dir cleanup removes it, saving a mktemp and an rm fork per file.
local source_err_file source_err source_status
source_err_file="$_BASHUNIT_RUN_OUTPUT_DIR/source_err"
# A missing scratch dir makes the redirect below fail, and bash reports that
# as the *command* failing: exit 1 with nothing written to the capture file,
# which read as "this test file failed to source" against a file that was
# complete and valid (#1137). Restore the directory, and fall back to
# /dev/null if even that is refused -- losing a file's stderr capture is
# worth far less than failing the file for a reason that is not its own.
if [ ! -d "$_BASHUNIT_RUN_OUTPUT_DIR" ] &&
! mkdir -p "$_BASHUNIT_RUN_OUTPUT_DIR" 2>/dev/null; then
source_err_file=/dev/null
fi
# shellcheck source=/dev/null
source "$test_file" 2>"$source_err_file"
source_status=$?
Expand Down
54 changes: 54 additions & 0 deletions tests/acceptance/bashunit_source_redirect_test.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
set -euo pipefail

# `source "$file" 2>"$_BASHUNIT_RUN_OUTPUT_DIR/source_err"` fails when the run's
# scratch directory is missing -- and it fails on the *redirect*, so bash
# returns 1 having written nothing to the capture file. The runner read that as
# the test file failing to source and reported
#
# Failed to source 'x_test.sh' (exit 1, 52 bytes, no stderr)
#
# against a file that was complete and valid. That message sent three separate
# investigations at the fixture (#1137). The directory going missing mid-run is
# a separate open question; a run should survive it either way.

function set_up_before_script() {
TEST_ENV_FILE="tests/acceptance/fixtures/.env.default"
}

# Two files: the first removes the run's scratch directory while it is being
# sourced, which is exactly the state the flake produces; the second must still
# be sourced and run.
function test_a_file_still_runs_when_the_run_scratch_dir_disappeared() {
local dir
dir="$(bashunit::temp_dir)"
printf '%s\n' 'rm -rf "$_BASHUNIT_RUN_OUTPUT_DIR"
function test_removes_the_scratch_dir() { assert_same 1 1; }' >"$dir/a_killer_test.sh"
printf '%s\n' 'function test_runs_after_the_scratch_dir_went_away() { assert_same 2 2; }' \
>"$dir/b_victim_test.sh"

local output code=0
output="$(./bashunit --no-parallel --env "$TEST_ENV_FILE" \
"$dir/a_killer_test.sh" "$dir/b_victim_test.sh" 2>&1)" || code=$?
output="$(printf '%s' "$output" | strip_ansi)"

assert_same 0 "$code"
assert_contains "2 passed" "$output"
assert_not_contains "Failed to source" "$output"
}

# The genuine case must keep reporting: a file whose top level returns non-zero
# is still a source failure, and the message still says so.
function test_a_real_source_failure_is_still_reported() {
local fixture
fixture="$(bashunit::temp_file real_source_failure).sh"
printf '%s\n' 'function test_never_runs() { assert_same 1 1; }
false' >"$fixture"

local output code=0
output="$(./bashunit --no-parallel --env "$TEST_ENV_FILE" "$fixture" 2>&1)" || code=$?
output="$(printf '%s' "$output" | strip_ansi)"

assert_same 1 "$code"
assert_contains "Failed to source" "$output"
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } 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@@ -11,6 +11,7 @@
- Performance: `--coverage` is roughly 5x faster and `--coverage-report-html` roughly 19x β€” a run over this repo went from 16.2s to 2.9s, and a 128-file HTML report from 58.7s to 3.1s. The report phase emits each format in one awk invocation per run instead of Bash loops and forks per file and per row, and the capture path writes records straight to disk, normalizes a path with one fork instead of four, and reads each cache once (#1092, #1096, #1098, #1099, #1102, #1104, #1110, #1117)

### Fixed
- A run survives its scratch directory going missing instead of reporting a valid test file as unsourceable: `source "$file" 2>"$dir/source_err"` fails on the *redirect* when the directory is gone, which bash reports as the command failing, so the runner printed `Failed to source '<file>' (exit 1, ..., no stderr)` and named the one thing that was not at fault (#1163)
- Duplicate test functions are detected under `--parallel` again: the check ran inside the per-file loop, which `--parallel` backgrounds, so the state it set died with the subshell and a file where one of two same-named tests never ran reported "All tests passed" and exited 0. The report also names the line of every definition now, instead of leaving the second one to be found by hand (#1147)
- A `@data_provider` naming a function that is not defined no longer makes the test vanish: the run reported "No tests found", blaming a missing test rather than the missing provider and never printing the annotation's name. Both that and a provider that yields no data are now errors naming the provider, and under `--parallel` the failure reaches the aggregate instead of the run printing the error and exiting 0 (#1145)
- `mock`, `mock_sequence` and `spy` report a usable-name error instead of a raw bash syntax error when the command name carries whitespace or shell syntax β€” `mock "ls -l" echo hi` used to print `syntax error near unexpected token '-l'` from inside bashunit (#1136)
Expand Down
10 changes: 10 additions & 0 deletions src/runner/discovery.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -67,6 +67,16 @@ function bashunit::runner::load_test_files() {
# run-dir cleanup removes it, saving a mktemp and an rm fork per file.
local source_err_file source_err source_status
source_err_file="$_BASHUNIT_RUN_OUTPUT_DIR/source_err"
# A missing scratch dir makes the redirect below fail, and bash reports that
# as the *command* failing: exit 1 with nothing written to the capture file,
# which read as "this test file failed to source" against a file that was
# complete and valid (#1137). Restore the directory, and fall back to
# /dev/null if even that is refused -- losing a file's stderr capture is
# worth far less than failing the file for a reason that is not its own.
if [ ! -d "$_BASHUNIT_RUN_OUTPUT_DIR" ] &&
! mkdir -p "$_BASHUNIT_RUN_OUTPUT_DIR" 2>/dev/null; then
source_err_file=/dev/null
fi
# shellcheck source=/dev/null
source "$test_file" 2>"$source_err_file"
source_status=$?
Expand Down
54 changes: 54 additions & 0 deletions tests/acceptance/bashunit_source_redirect_test.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
set -euo pipefail

# `source "$file" 2>"$_BASHUNIT_RUN_OUTPUT_DIR/source_err"` fails when the run's
# scratch directory is missing -- and it fails on the *redirect*, so bash
# returns 1 having written nothing to the capture file. The runner read that as
# the test file failing to source and reported
#
# Failed to source 'x_test.sh' (exit 1, 52 bytes, no stderr)
#
# against a file that was complete and valid. That message sent three separate
# investigations at the fixture (#1137). The directory going missing mid-run is
# a separate open question; a run should survive it either way.

function set_up_before_script() {
TEST_ENV_FILE="tests/acceptance/fixtures/.env.default"
}

# Two files: the first removes the run's scratch directory while it is being
# sourced, which is exactly the state the flake produces; the second must still
# be sourced and run.
function test_a_file_still_runs_when_the_run_scratch_dir_disappeared() {
local dir
dir="$(bashunit::temp_dir)"
printf '%s\n' 'rm -rf "$_BASHUNIT_RUN_OUTPUT_DIR"
function test_removes_the_scratch_dir() { assert_same 1 1; }' >"$dir/a_killer_test.sh"
printf '%s\n' 'function test_runs_after_the_scratch_dir_went_away() { assert_same 2 2; }' \
>"$dir/b_victim_test.sh"

local output code=0
output="$(./bashunit --no-parallel --env "$TEST_ENV_FILE" \
"$dir/a_killer_test.sh" "$dir/b_victim_test.sh" 2>&1)" || code=$?
output="$(printf '%s' "$output" | strip_ansi)"

assert_same 0 "$code"
assert_contains "2 passed" "$output"
assert_not_contains "Failed to source" "$output"
}

# The genuine case must keep reporting: a file whose top level returns non-zero
# is still a source failure, and the message still says so.
function test_a_real_source_failure_is_still_reported() {
local fixture
fixture="$(bashunit::temp_file real_source_failure).sh"
printf '%s\n' 'function test_never_runs() { assert_same 1 1; }
false' >"$fixture"

local output code=0
output="$(./bashunit --no-parallel --env "$TEST_ENV_FILE" "$fixture" 2>&1)" || code=$?
output="$(printf '%s' "$output" | strip_ansi)"

assert_same 1 "$code"
assert_contains "Failed to source" "$output"
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } 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@@ -11,6 +11,7 @@
- Performance: `--coverage` is roughly 5x faster and `--coverage-report-html` roughly 19x β€” a run over this repo went from 16.2s to 2.9s, and a 128-file HTML report from 58.7s to 3.1s. The report phase emits each format in one awk invocation per run instead of Bash loops and forks per file and per row, and the capture path writes records straight to disk, normalizes a path with one fork instead of four, and reads each cache once (#1092, #1096, #1098, #1099, #1102, #1104, #1110, #1117)

### Fixed
- A run survives its scratch directory going missing instead of reporting a valid test file as unsourceable: `source "$file" 2>"$dir/source_err"` fails on the *redirect* when the directory is gone, which bash reports as the command failing, so the runner printed `Failed to source '<file>' (exit 1, ..., no stderr)` and named the one thing that was not at fault (#1163)
- Duplicate test functions are detected under `--parallel` again: the check ran inside the per-file loop, which `--parallel` backgrounds, so the state it set died with the subshell and a file where one of two same-named tests never ran reported "All tests passed" and exited 0. The report also names the line of every definition now, instead of leaving the second one to be found by hand (#1147)
- A `@data_provider` naming a function that is not defined no longer makes the test vanish: the run reported "No tests found", blaming a missing test rather than the missing provider and never printing the annotation's name. Both that and a provider that yields no data are now errors naming the provider, and under `--parallel` the failure reaches the aggregate instead of the run printing the error and exiting 0 (#1145)
- `mock`, `mock_sequence` and `spy` report a usable-name error instead of a raw bash syntax error when the command name carries whitespace or shell syntax β€” `mock "ls -l" echo hi` used to print `syntax error near unexpected token '-l'` from inside bashunit (#1136)
Expand Down
10 changes: 10 additions & 0 deletions src/runner/discovery.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -67,6 +67,16 @@ function bashunit::runner::load_test_files() {
# run-dir cleanup removes it, saving a mktemp and an rm fork per file.
local source_err_file source_err source_status
source_err_file="$_BASHUNIT_RUN_OUTPUT_DIR/source_err"
# A missing scratch dir makes the redirect below fail, and bash reports that
# as the *command* failing: exit 1 with nothing written to the capture file,
# which read as "this test file failed to source" against a file that was
# complete and valid (#1137). Restore the directory, and fall back to
# /dev/null if even that is refused -- losing a file's stderr capture is
# worth far less than failing the file for a reason that is not its own.
if [ ! -d "$_BASHUNIT_RUN_OUTPUT_DIR" ] &&
! mkdir -p "$_BASHUNIT_RUN_OUTPUT_DIR" 2>/dev/null; then
source_err_file=/dev/null
fi
# shellcheck source=/dev/null
source "$test_file" 2>"$source_err_file"
source_status=$?
Expand Down
54 changes: 54 additions & 0 deletions tests/acceptance/bashunit_source_redirect_test.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
set -euo pipefail

# `source "$file" 2>"$_BASHUNIT_RUN_OUTPUT_DIR/source_err"` fails when the run's
# scratch directory is missing -- and it fails on the *redirect*, so bash
# returns 1 having written nothing to the capture file. The runner read that as
# the test file failing to source and reported
#
# Failed to source 'x_test.sh' (exit 1, 52 bytes, no stderr)
#
# against a file that was complete and valid. That message sent three separate
# investigations at the fixture (#1137). The directory going missing mid-run is
# a separate open question; a run should survive it either way.

function set_up_before_script() {
TEST_ENV_FILE="tests/acceptance/fixtures/.env.default"
}

# Two files: the first removes the run's scratch directory while it is being
# sourced, which is exactly the state the flake produces; the second must still
# be sourced and run.
function test_a_file_still_runs_when_the_run_scratch_dir_disappeared() {
local dir
dir="$(bashunit::temp_dir)"
printf '%s\n' 'rm -rf "$_BASHUNIT_RUN_OUTPUT_DIR"
function test_removes_the_scratch_dir() { assert_same 1 1; }' >"$dir/a_killer_test.sh"
printf '%s\n' 'function test_runs_after_the_scratch_dir_went_away() { assert_same 2 2; }' \
>"$dir/b_victim_test.sh"

local output code=0
output="$(./bashunit --no-parallel --env "$TEST_ENV_FILE" \
"$dir/a_killer_test.sh" "$dir/b_victim_test.sh" 2>&1)" || code=$?
output="$(printf '%s' "$output" | strip_ansi)"

assert_same 0 "$code"
assert_contains "2 passed" "$output"
assert_not_contains "Failed to source" "$output"
}

# The genuine case must keep reporting: a file whose top level returns non-zero
# is still a source failure, and the message still says so.
function test_a_real_source_failure_is_still_reported() {
local fixture
fixture="$(bashunit::temp_file real_source_failure).sh"
printf '%s\n' 'function test_never_runs() { assert_same 1 1; }
false' >"$fixture"

local output code=0
output="$(./bashunit --no-parallel --env "$TEST_ENV_FILE" "$fixture" 2>&1)" || code=$?
output="$(printf '%s' "$output" | strip_ansi)"

assert_same 1 "$code"
assert_contains "Failed to source" "$output"
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } 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@@ -11,6 +11,7 @@
- Performance: `--coverage` is roughly 5x faster and `--coverage-report-html` roughly 19x β€” a run over this repo went from 16.2s to 2.9s, and a 128-file HTML report from 58.7s to 3.1s. The report phase emits each format in one awk invocation per run instead of Bash loops and forks per file and per row, and the capture path writes records straight to disk, normalizes a path with one fork instead of four, and reads each cache once (#1092, #1096, #1098, #1099, #1102, #1104, #1110, #1117)

### Fixed
- A run survives its scratch directory going missing instead of reporting a valid test file as unsourceable: `source "$file" 2>"$dir/source_err"` fails on the *redirect* when the directory is gone, which bash reports as the command failing, so the runner printed `Failed to source '<file>' (exit 1, ..., no stderr)` and named the one thing that was not at fault (#1163)
- Duplicate test functions are detected under `--parallel` again: the check ran inside the per-file loop, which `--parallel` backgrounds, so the state it set died with the subshell and a file where one of two same-named tests never ran reported "All tests passed" and exited 0. The report also names the line of every definition now, instead of leaving the second one to be found by hand (#1147)
- A `@data_provider` naming a function that is not defined no longer makes the test vanish: the run reported "No tests found", blaming a missing test rather than the missing provider and never printing the annotation's name. Both that and a provider that yields no data are now errors naming the provider, and under `--parallel` the failure reaches the aggregate instead of the run printing the error and exiting 0 (#1145)
- `mock`, `mock_sequence` and `spy` report a usable-name error instead of a raw bash syntax error when the command name carries whitespace or shell syntax β€” `mock "ls -l" echo hi` used to print `syntax error near unexpected token '-l'` from inside bashunit (#1136)
Expand Down
10 changes: 10 additions & 0 deletions src/runner/discovery.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -67,6 +67,16 @@ function bashunit::runner::load_test_files() {
# run-dir cleanup removes it, saving a mktemp and an rm fork per file.
local source_err_file source_err source_status
source_err_file="$_BASHUNIT_RUN_OUTPUT_DIR/source_err"
# A missing scratch dir makes the redirect below fail, and bash reports that
# as the *command* failing: exit 1 with nothing written to the capture file,
# which read as "this test file failed to source" against a file that was
# complete and valid (#1137). Restore the directory, and fall back to
# /dev/null if even that is refused -- losing a file's stderr capture is
# worth far less than failing the file for a reason that is not its own.
if [ ! -d "$_BASHUNIT_RUN_OUTPUT_DIR" ] &&
! mkdir -p "$_BASHUNIT_RUN_OUTPUT_DIR" 2>/dev/null; then
source_err_file=/dev/null
fi
# shellcheck source=/dev/null
source "$test_file" 2>"$source_err_file"
source_status=$?
Expand Down
54 changes: 54 additions & 0 deletions tests/acceptance/bashunit_source_redirect_test.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
set -euo pipefail

# `source "$file" 2>"$_BASHUNIT_RUN_OUTPUT_DIR/source_err"` fails when the run's
# scratch directory is missing -- and it fails on the *redirect*, so bash
# returns 1 having written nothing to the capture file. The runner read that as
# the test file failing to source and reported
#
# Failed to source 'x_test.sh' (exit 1, 52 bytes, no stderr)
#
# against a file that was complete and valid. That message sent three separate
# investigations at the fixture (#1137). The directory going missing mid-run is
# a separate open question; a run should survive it either way.

function set_up_before_script() {
TEST_ENV_FILE="tests/acceptance/fixtures/.env.default"
}

# Two files: the first removes the run's scratch directory while it is being
# sourced, which is exactly the state the flake produces; the second must still
# be sourced and run.
function test_a_file_still_runs_when_the_run_scratch_dir_disappeared() {
local dir
dir="$(bashunit::temp_dir)"
printf '%s\n' 'rm -rf "$_BASHUNIT_RUN_OUTPUT_DIR"
function test_removes_the_scratch_dir() { assert_same 1 1; }' >"$dir/a_killer_test.sh"
printf '%s\n' 'function test_runs_after_the_scratch_dir_went_away() { assert_same 2 2; }' \
>"$dir/b_victim_test.sh"

local output code=0
output="$(./bashunit --no-parallel --env "$TEST_ENV_FILE" \
"$dir/a_killer_test.sh" "$dir/b_victim_test.sh" 2>&1)" || code=$?
output="$(printf '%s' "$output" | strip_ansi)"

assert_same 0 "$code"
assert_contains "2 passed" "$output"
assert_not_contains "Failed to source" "$output"
}

# The genuine case must keep reporting: a file whose top level returns non-zero
# is still a source failure, and the message still says so.
function test_a_real_source_failure_is_still_reported() {
local fixture
fixture="$(bashunit::temp_file real_source_failure).sh"
printf '%s\n' 'function test_never_runs() { assert_same 1 1; }
false' >"$fixture"

local output code=0
output="$(./bashunit --no-parallel --env "$TEST_ENV_FILE" "$fixture" 2>&1)" || code=$?
output="$(printf '%s' "$output" | strip_ansi)"

assert_same 1 "$code"
assert_contains "Failed to source" "$output"
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } 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@@ -11,6 +11,7 @@
- Performance: `--coverage` is roughly 5x faster and `--coverage-report-html` roughly 19x β€” a run over this repo went from 16.2s to 2.9s, and a 128-file HTML report from 58.7s to 3.1s. The report phase emits each format in one awk invocation per run instead of Bash loops and forks per file and per row, and the capture path writes records straight to disk, normalizes a path with one fork instead of four, and reads each cache once (#1092, #1096, #1098, #1099, #1102, #1104, #1110, #1117)

### Fixed
- A run survives its scratch directory going missing instead of reporting a valid test file as unsourceable: `source "$file" 2>"$dir/source_err"` fails on the *redirect* when the directory is gone, which bash reports as the command failing, so the runner printed `Failed to source '<file>' (exit 1, ..., no stderr)` and named the one thing that was not at fault (#1163)
- Duplicate test functions are detected under `--parallel` again: the check ran inside the per-file loop, which `--parallel` backgrounds, so the state it set died with the subshell and a file where one of two same-named tests never ran reported "All tests passed" and exited 0. The report also names the line of every definition now, instead of leaving the second one to be found by hand (#1147)
- A `@data_provider` naming a function that is not defined no longer makes the test vanish: the run reported "No tests found", blaming a missing test rather than the missing provider and never printing the annotation's name. Both that and a provider that yields no data are now errors naming the provider, and under `--parallel` the failure reaches the aggregate instead of the run printing the error and exiting 0 (#1145)
- `mock`, `mock_sequence` and `spy` report a usable-name error instead of a raw bash syntax error when the command name carries whitespace or shell syntax β€” `mock "ls -l" echo hi` used to print `syntax error near unexpected token '-l'` from inside bashunit (#1136)
Expand Down
10 changes: 10 additions & 0 deletions src/runner/discovery.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -67,6 +67,16 @@ function bashunit::runner::load_test_files() {
# run-dir cleanup removes it, saving a mktemp and an rm fork per file.
local source_err_file source_err source_status
source_err_file="$_BASHUNIT_RUN_OUTPUT_DIR/source_err"
# A missing scratch dir makes the redirect below fail, and bash reports that
# as the *command* failing: exit 1 with nothing written to the capture file,
# which read as "this test file failed to source" against a file that was
# complete and valid (#1137). Restore the directory, and fall back to
# /dev/null if even that is refused -- losing a file's stderr capture is
# worth far less than failing the file for a reason that is not its own.
if [ ! -d "$_BASHUNIT_RUN_OUTPUT_DIR" ] &&
! mkdir -p "$_BASHUNIT_RUN_OUTPUT_DIR" 2>/dev/null; then
source_err_file=/dev/null
fi
# shellcheck source=/dev/null
source "$test_file" 2>"$source_err_file"
source_status=$?
Expand Down
54 changes: 54 additions & 0 deletions tests/acceptance/bashunit_source_redirect_test.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
set -euo pipefail

# `source "$file" 2>"$_BASHUNIT_RUN_OUTPUT_DIR/source_err"` fails when the run's
# scratch directory is missing -- and it fails on the *redirect*, so bash
# returns 1 having written nothing to the capture file. The runner read that as
# the test file failing to source and reported
#
# Failed to source 'x_test.sh' (exit 1, 52 bytes, no stderr)
#
# against a file that was complete and valid. That message sent three separate
# investigations at the fixture (#1137). The directory going missing mid-run is
# a separate open question; a run should survive it either way.

function set_up_before_script() {
TEST_ENV_FILE="tests/acceptance/fixtures/.env.default"
}

# Two files: the first removes the run's scratch directory while it is being
# sourced, which is exactly the state the flake produces; the second must still
# be sourced and run.
function test_a_file_still_runs_when_the_run_scratch_dir_disappeared() {
local dir
dir="$(bashunit::temp_dir)"
printf '%s\n' 'rm -rf "$_BASHUNIT_RUN_OUTPUT_DIR"
function test_removes_the_scratch_dir() { assert_same 1 1; }' >"$dir/a_killer_test.sh"
printf '%s\n' 'function test_runs_after_the_scratch_dir_went_away() { assert_same 2 2; }' \
>"$dir/b_victim_test.sh"

local output code=0
output="$(./bashunit --no-parallel --env "$TEST_ENV_FILE" \
"$dir/a_killer_test.sh" "$dir/b_victim_test.sh" 2>&1)" || code=$?
output="$(printf '%s' "$output" | strip_ansi)"

assert_same 0 "$code"
assert_contains "2 passed" "$output"
assert_not_contains "Failed to source" "$output"
}

# The genuine case must keep reporting: a file whose top level returns non-zero
# is still a source failure, and the message still says so.
function test_a_real_source_failure_is_still_reported() {
local fixture
fixture="$(bashunit::temp_file real_source_failure).sh"
printf '%s\n' 'function test_never_runs() { assert_same 1 1; }
false' >"$fixture"

local output code=0
output="$(./bashunit --no-parallel --env "$TEST_ENV_FILE" "$fixture" 2>&1)" || code=$?
output="$(printf '%s' "$output" | strip_ansi)"

assert_same 1 "$code"
assert_contains "Failed to source" "$output"
}
Loading