From acd1cc55bf6b1df62ed39d111576dfb33de4b753 Mon Sep 17 00:00:00 2001 From: Omar Gallo Date: Sun, 28 Dec 2025 09:56:09 +0100 Subject: [PATCH 1/8] test: improve coverage --- src/coverage.sh | 19 ++++++++++++- src/runner.sh | 20 +++++++++++++ tests/acceptance/coverage_hooks_test.sh | 28 +++++++++++++++++++ .../fixtures/test_coverage_hooks.sh | 15 ++++++++++ 4 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 tests/acceptance/coverage_hooks_test.sh create mode 100644 tests/acceptance/fixtures/test_coverage_hooks.sh diff --git a/src/coverage.sh b/src/coverage.sh index 34f96cd8..0a2286d9 100644 --- a/src/coverage.sh +++ b/src/coverage.sh @@ -82,6 +82,12 @@ function bashunit::coverage::init() { export _BASHUNIT_COVERAGE_TRACKED_FILES export _BASHUNIT_COVERAGE_TRACKED_CACHE_FILE export _BASHUNIT_COVERAGE_TEST_HITS_FILE + + # Ensure we have inclusion paths; if none provided or auto-discovered, + # default to tracking the src/ folder to avoid empty reports + if [[ -z "${BASHUNIT_COVERAGE_PATHS:-}" ]]; then + BASHUNIT_COVERAGE_PATHS="src/" + fi } function bashunit::coverage::enable_trap() { @@ -95,7 +101,18 @@ function bashunit::coverage::enable_trap() { # Set DEBUG trap to record line execution # Use ${VAR:-} to handle unset variables when set -u is active (in subshells) # shellcheck disable=SC2154 - trap 'bashunit::coverage::record_line "${BASH_SOURCE:-}" "${LINENO:-}"' DEBUG + trap ' + # Prefer immediate callee frame; fall back to caller when current file is excluded + local __cov_file="${BASH_SOURCE[0]:-}" + local __cov_line="${LINENO:-}" + if [[ -n "$__cov_file" ]] && ! bashunit::coverage::should_track "$__cov_file"; then + # Try next stack frame if available (e.g., called function from a tracked src file) + if [[ -n "${BASH_SOURCE[1]:-}" ]]; then + __cov_file="${BASH_SOURCE[1]}" + fi + fi + bashunit::coverage::record_line "$__cov_file" "$__cov_line" + ' DEBUG } function bashunit::coverage::disable_trap() { diff --git a/src/runner.sh b/src/runner.sh index f625a796..c6c7d75d 100755 --- a/src/runner.sh +++ b/src/runner.sh @@ -23,6 +23,10 @@ function bashunit::runner::load_test_files() { # Auto-discover coverage paths if not explicitly set if [[ -z "$BASHUNIT_COVERAGE_PATHS" ]]; then BASHUNIT_COVERAGE_PATHS=$(bashunit::coverage::auto_discover_paths "${files[@]}") + # Fallback: if auto-discovery yields no paths, track the src/ folder + if [[ -z "$BASHUNIT_COVERAGE_PATHS" ]]; then + BASHUNIT_COVERAGE_PATHS="src/" + fi fi bashunit::coverage::init fi @@ -850,8 +854,16 @@ function bashunit::runner::execute_file_hook() { trap '_BASHUNIT_HOOK_ERR_STATUS=$?; set +eE; trap - ERR; return $_BASHUNIT_HOOK_ERR_STATUS' ERR { + # Enable coverage trap inside hooks to attribute lines executed during setup/teardown + if bashunit::env::is_coverage_enabled; then + bashunit::coverage::enable_trap + fi "$hook_name" } >"$hook_output_file" 2>&1 + # Disable coverage trap after hook execution + if bashunit::env::is_coverage_enabled; then + bashunit::coverage::disable_trap + fi # Capture exit status from global variable and clean up status=$_BASHUNIT_HOOK_ERR_STATUS @@ -943,8 +955,16 @@ function bashunit::runner::execute_test_hook() { trap '_BASHUNIT_HOOK_ERR_STATUS=$?; set +eE; trap - ERR; return $_BASHUNIT_HOOK_ERR_STATUS' ERR { + # Enable coverage trap inside test-level hooks + if bashunit::env::is_coverage_enabled; then + bashunit::coverage::enable_trap + fi "$hook_name" } >"$hook_output_file" 2>&1 + # Disable coverage trap after hook execution + if bashunit::env::is_coverage_enabled; then + bashunit::coverage::disable_trap + fi # Capture exit status from global variable and clean up status=$_BASHUNIT_HOOK_ERR_STATUS diff --git a/tests/acceptance/coverage_hooks_test.sh b/tests/acceptance/coverage_hooks_test.sh new file mode 100644 index 00000000..554e9e82 --- /dev/null +++ b/tests/acceptance/coverage_hooks_test.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +function set_up_before_script() { + TEST_ENV_FILE="tests/acceptance/fixtures/.env.default" + LCOV_FILE="$(bashunit::temp_file "lcov-output")" +} + +function test_coverage_includes_src_hits_from_setup_hook() { + # Enable coverage in-process and exercise code in a hook-like context + BASHUNIT_COVERAGE=true + BASHUNIT_COVERAGE_PATHS="" # force auto-discovery / fallback + bashunit::coverage::init + + # Simulate hook execution with coverage trap enabled + bashunit::coverage::enable_trap + # Call a src function to generate attributable hits + local f + f="$(bashunit::temp_file "cov-hooks")" + [[ -n "${f:-}" ]] && echo "tmp created" > /dev/null + bashunit::coverage::disable_trap + + # Generate LCOV and assert presence of src entries + bashunit::coverage::report_lcov "$LCOV_FILE" + local lcov + lcov="$(cat "$LCOV_FILE" 2>/dev/null)" + assert_contains "SF:$(pwd)/src/" "$lcov" + assert_contains "DA:" "$lcov" +} diff --git a/tests/acceptance/fixtures/test_coverage_hooks.sh b/tests/acceptance/fixtures/test_coverage_hooks.sh new file mode 100644 index 00000000..dc9fb642 --- /dev/null +++ b/tests/acceptance/fixtures/test_coverage_hooks.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +# This fixture exercises coverage attribution inside lifecycle hooks. + +function set_up() { + # Invoke src functions to generate attributable coverage hits + local f + f="$(bashunit::temp_file \"cov-hooks\")" + [[ -n "${f:-}" ]] && echo "tmp created" > /dev/null +} + +function test_noop() { + # No-op test; coverage should still attribute lines from set_up + assert_true true +} From 97df5d8436cf24ffe0cca941c24141302c666356 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Sun, 28 Dec 2025 23:34:06 +0100 Subject: [PATCH 2/8] fix: correct escaped quotes in coverage hooks fixture --- tests/acceptance/fixtures/test_coverage_hooks.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/acceptance/fixtures/test_coverage_hooks.sh b/tests/acceptance/fixtures/test_coverage_hooks.sh index dc9fb642..ad43e126 100644 --- a/tests/acceptance/fixtures/test_coverage_hooks.sh +++ b/tests/acceptance/fixtures/test_coverage_hooks.sh @@ -5,7 +5,7 @@ function set_up() { # Invoke src functions to generate attributable coverage hits local f - f="$(bashunit::temp_file \"cov-hooks\")" + f="$(bashunit::temp_file "cov-hooks")" [[ -n "${f:-}" ]] && echo "tmp created" > /dev/null } From 78535cfcc28f6a9c25059955879bd416ebab5455 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Sun, 28 Dec 2025 23:34:44 +0100 Subject: [PATCH 3/8] refactor(coverage): remove duplicate default path logic from init The src/ fallback is already handled in runner.sh after auto-discovery. Having it in both places is redundant since runner.sh calls init() after setting BASHUNIT_COVERAGE_PATHS. --- src/coverage.sh | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/coverage.sh b/src/coverage.sh index 0a2286d9..1cef5011 100644 --- a/src/coverage.sh +++ b/src/coverage.sh @@ -82,12 +82,6 @@ function bashunit::coverage::init() { export _BASHUNIT_COVERAGE_TRACKED_FILES export _BASHUNIT_COVERAGE_TRACKED_CACHE_FILE export _BASHUNIT_COVERAGE_TEST_HITS_FILE - - # Ensure we have inclusion paths; if none provided or auto-discovered, - # default to tracking the src/ folder to avoid empty reports - if [[ -z "${BASHUNIT_COVERAGE_PATHS:-}" ]]; then - BASHUNIT_COVERAGE_PATHS="src/" - fi } function bashunit::coverage::enable_trap() { From fddd252a6566111312241668074ef5deb28058cd Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Sun, 28 Dec 2025 23:35:35 +0100 Subject: [PATCH 4/8] perf(coverage): simplify DEBUG trap to avoid redundant calls The complex trap was: - Redundant: record_line already calls should_track internally - Semantically incorrect: used LINENO from frame 0 with BASH_SOURCE[1] - Adding overhead: extra function call on every line executed Revert to simple, efficient trap. --- src/coverage.sh | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/coverage.sh b/src/coverage.sh index 1cef5011..88bbd023 100644 --- a/src/coverage.sh +++ b/src/coverage.sh @@ -95,18 +95,7 @@ function bashunit::coverage::enable_trap() { # Set DEBUG trap to record line execution # Use ${VAR:-} to handle unset variables when set -u is active (in subshells) # shellcheck disable=SC2154 - trap ' - # Prefer immediate callee frame; fall back to caller when current file is excluded - local __cov_file="${BASH_SOURCE[0]:-}" - local __cov_line="${LINENO:-}" - if [[ -n "$__cov_file" ]] && ! bashunit::coverage::should_track "$__cov_file"; then - # Try next stack frame if available (e.g., called function from a tracked src file) - if [[ -n "${BASH_SOURCE[1]:-}" ]]; then - __cov_file="${BASH_SOURCE[1]}" - fi - fi - bashunit::coverage::record_line "$__cov_file" "$__cov_line" - ' DEBUG + trap 'bashunit::coverage::record_line "${BASH_SOURCE[0]:-}" "${LINENO:-}"' DEBUG } function bashunit::coverage::disable_trap() { From 3d8b8ec2f35976d8e889c2c9fd2bd6eb3432768f Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Sun, 28 Dec 2025 23:36:27 +0100 Subject: [PATCH 5/8] test: add shellcheck directive and use explicit coverage paths - Add SC2034 disable for environment variables used by bashunit - Use explicit BASHUNIT_COVERAGE_PATHS="src/" instead of relying on fallback --- tests/acceptance/coverage_hooks_test.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/acceptance/coverage_hooks_test.sh b/tests/acceptance/coverage_hooks_test.sh index 554e9e82..ed299336 100644 --- a/tests/acceptance/coverage_hooks_test.sh +++ b/tests/acceptance/coverage_hooks_test.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# shellcheck disable=SC2034 function set_up_before_script() { TEST_ENV_FILE="tests/acceptance/fixtures/.env.default" @@ -8,7 +9,7 @@ function set_up_before_script() { function test_coverage_includes_src_hits_from_setup_hook() { # Enable coverage in-process and exercise code in a hook-like context BASHUNIT_COVERAGE=true - BASHUNIT_COVERAGE_PATHS="" # force auto-discovery / fallback + BASHUNIT_COVERAGE_PATHS="src/" bashunit::coverage::init # Simulate hook execution with coverage trap enabled From 2521779e38979420ed3c0ce70eaadd1dbbcd8f7a Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Sun, 28 Dec 2025 23:37:39 +0100 Subject: [PATCH 6/8] test: add end-to-end test for coverage hooks Runs bashunit on fixture file with coverage enabled to verify the complete flow works correctly, including hook coverage attribution. Skip test in parallel mode as coverage state is not shared across workers. --- tests/acceptance/coverage_hooks_test.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/acceptance/coverage_hooks_test.sh b/tests/acceptance/coverage_hooks_test.sh index ed299336..23c189b4 100644 --- a/tests/acceptance/coverage_hooks_test.sh +++ b/tests/acceptance/coverage_hooks_test.sh @@ -7,6 +7,12 @@ function set_up_before_script() { } function test_coverage_includes_src_hits_from_setup_hook() { + # Skip in parallel mode - coverage state is not shared across workers + if bashunit::env::is_parallel_run_enabled; then + bashunit::skip "Coverage tests require sequential execution" + return + fi + # Enable coverage in-process and exercise code in a hook-like context BASHUNIT_COVERAGE=true BASHUNIT_COVERAGE_PATHS="src/" From c2f2830571fb638c8a359d388abf5e83068406a3 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Mon, 29 Dec 2025 16:27:11 +0100 Subject: [PATCH 7/8] test: remove coverage hooks tests that cause CI hangs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The coverage hooks acceptance tests cause parallel test runs to hang in GitHub Actions CI, resulting in 6-hour timeouts. This happens on Ubuntu runners but not on macOS locally. The runner.sh changes for enabling coverage in hooks remain in place - only the problematic test files are removed until the root cause can be identified. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- tests/acceptance/coverage_hooks_test.sh | 35 ------------------- .../fixtures/test_coverage_hooks.sh | 15 -------- 2 files changed, 50 deletions(-) delete mode 100644 tests/acceptance/coverage_hooks_test.sh delete mode 100644 tests/acceptance/fixtures/test_coverage_hooks.sh diff --git a/tests/acceptance/coverage_hooks_test.sh b/tests/acceptance/coverage_hooks_test.sh deleted file mode 100644 index 23c189b4..00000000 --- a/tests/acceptance/coverage_hooks_test.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash -# shellcheck disable=SC2034 - -function set_up_before_script() { - TEST_ENV_FILE="tests/acceptance/fixtures/.env.default" - LCOV_FILE="$(bashunit::temp_file "lcov-output")" -} - -function test_coverage_includes_src_hits_from_setup_hook() { - # Skip in parallel mode - coverage state is not shared across workers - if bashunit::env::is_parallel_run_enabled; then - bashunit::skip "Coverage tests require sequential execution" - return - fi - - # Enable coverage in-process and exercise code in a hook-like context - BASHUNIT_COVERAGE=true - BASHUNIT_COVERAGE_PATHS="src/" - bashunit::coverage::init - - # Simulate hook execution with coverage trap enabled - bashunit::coverage::enable_trap - # Call a src function to generate attributable hits - local f - f="$(bashunit::temp_file "cov-hooks")" - [[ -n "${f:-}" ]] && echo "tmp created" > /dev/null - bashunit::coverage::disable_trap - - # Generate LCOV and assert presence of src entries - bashunit::coverage::report_lcov "$LCOV_FILE" - local lcov - lcov="$(cat "$LCOV_FILE" 2>/dev/null)" - assert_contains "SF:$(pwd)/src/" "$lcov" - assert_contains "DA:" "$lcov" -} diff --git a/tests/acceptance/fixtures/test_coverage_hooks.sh b/tests/acceptance/fixtures/test_coverage_hooks.sh deleted file mode 100644 index ad43e126..00000000 --- a/tests/acceptance/fixtures/test_coverage_hooks.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash - -# This fixture exercises coverage attribution inside lifecycle hooks. - -function set_up() { - # Invoke src functions to generate attributable coverage hits - local f - f="$(bashunit::temp_file "cov-hooks")" - [[ -n "${f:-}" ]] && echo "tmp created" > /dev/null -} - -function test_noop() { - # No-op test; coverage should still attribute lines from set_up - assert_true true -} From 65520a41b24ab27340a93ad7e3a84a77828a17c0 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Mon, 29 Dec 2025 16:43:37 +0100 Subject: [PATCH 8/8] revert: remove coverage hooks feature that causes CI hangs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The coverage tracking in lifecycle hooks feature causes parallel test runs to hang indefinitely in GitHub Actions CI. This reverts all changes from PR #574 until the root cause of the parallel test hangs can be identified and fixed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/coverage.sh | 2 +- src/runner.sh | 20 -------------------- 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/src/coverage.sh b/src/coverage.sh index 88bbd023..34f96cd8 100644 --- a/src/coverage.sh +++ b/src/coverage.sh @@ -95,7 +95,7 @@ function bashunit::coverage::enable_trap() { # Set DEBUG trap to record line execution # Use ${VAR:-} to handle unset variables when set -u is active (in subshells) # shellcheck disable=SC2154 - trap 'bashunit::coverage::record_line "${BASH_SOURCE[0]:-}" "${LINENO:-}"' DEBUG + trap 'bashunit::coverage::record_line "${BASH_SOURCE:-}" "${LINENO:-}"' DEBUG } function bashunit::coverage::disable_trap() { diff --git a/src/runner.sh b/src/runner.sh index c6c7d75d..f625a796 100755 --- a/src/runner.sh +++ b/src/runner.sh @@ -23,10 +23,6 @@ function bashunit::runner::load_test_files() { # Auto-discover coverage paths if not explicitly set if [[ -z "$BASHUNIT_COVERAGE_PATHS" ]]; then BASHUNIT_COVERAGE_PATHS=$(bashunit::coverage::auto_discover_paths "${files[@]}") - # Fallback: if auto-discovery yields no paths, track the src/ folder - if [[ -z "$BASHUNIT_COVERAGE_PATHS" ]]; then - BASHUNIT_COVERAGE_PATHS="src/" - fi fi bashunit::coverage::init fi @@ -854,16 +850,8 @@ function bashunit::runner::execute_file_hook() { trap '_BASHUNIT_HOOK_ERR_STATUS=$?; set +eE; trap - ERR; return $_BASHUNIT_HOOK_ERR_STATUS' ERR { - # Enable coverage trap inside hooks to attribute lines executed during setup/teardown - if bashunit::env::is_coverage_enabled; then - bashunit::coverage::enable_trap - fi "$hook_name" } >"$hook_output_file" 2>&1 - # Disable coverage trap after hook execution - if bashunit::env::is_coverage_enabled; then - bashunit::coverage::disable_trap - fi # Capture exit status from global variable and clean up status=$_BASHUNIT_HOOK_ERR_STATUS @@ -955,16 +943,8 @@ function bashunit::runner::execute_test_hook() { trap '_BASHUNIT_HOOK_ERR_STATUS=$?; set +eE; trap - ERR; return $_BASHUNIT_HOOK_ERR_STATUS' ERR { - # Enable coverage trap inside test-level hooks - if bashunit::env::is_coverage_enabled; then - bashunit::coverage::enable_trap - fi "$hook_name" } >"$hook_output_file" 2>&1 - # Disable coverage trap after hook execution - if bashunit::env::is_coverage_enabled; then - bashunit::coverage::disable_trap - fi # Capture exit status from global variable and clean up status=$_BASHUNIT_HOOK_ERR_STATUS