diff --git a/CHANGELOG.md b/CHANGELOG.md index 65b99f08..f6fcdb2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ ### Changed - Lower minimum Bash version requirement from 3.2 to 3.0 +- Improved parallel test execution performance (30-40% faster on large test suites) + - Test functions now run in parallel within each file when using `--parallel` flag + - Better load balancing through internal test file reorganization + - Optimized result aggregation eliminates subprocess overhead ### Added - Add Claude Code configuration with custom skills, agents, and rules @@ -21,6 +25,10 @@ - Shows only the final test summary - Useful for CI/CD pipelines or log-restricted environments - Can also be set via `BASHUNIT_NO_PROGRESS=true` environment variable +- Support for `# bashunit: no-parallel-tests` directive in test files + - Allows test files to opt out of test-level parallelism + - Useful for tests with shared state or race conditions + - Add as the second line in test files (after shebang) ### Fixed - Data providers now work without the `function` keyword on test functions (Issue #586) @@ -29,6 +37,10 @@ - Fixes regex in `bashunit::helper::get_provider_data()` to make the `function` keyword optional - Self-test `tests/acceptance/install_test.sh` now passes when no network tools are available (Issue #582) - Tests skip gracefully with `BASHUNIT_NO_NETWORK=true` or in sandboxed environments +- Parallel test execution now works correctly in strict mode environments (bash -e -o pipefail) + - Fixed arithmetic operations in result aggregation to prevent exit code 1 when values are zero + - Fixed spinner cleanup to handle already-terminated processes gracefully + - Ensures proper exit codes in CI environments like GitHub Actions Windows runners ## [0.32.0](https://github.com/TypedDevs/bashunit/compare/0.31.0...0.32.0) - 2026-01-12 diff --git a/docs/command-line.md b/docs/command-line.md index 04a4ecaf..1913525e 100644 --- a/docs/command-line.md +++ b/docs/command-line.md @@ -188,12 +188,32 @@ The line number syntax finds the test function that contains the specified line. Run tests in parallel or sequentially. Sequential is the default. +In parallel mode, both test files and individual test functions run concurrently +for maximum performance. + ::: warning Parallel mode is supported on **macOS**, **Ubuntu**, and **Windows**. On other systems (like Alpine Linux) the option is automatically disabled due to inconsistent results. ::: +::: tip Opt-out of test-level parallelism +If a test file has shared state or race conditions, you can disable test-level +parallelism by adding this directive as the second line: + +```bash +#!/usr/bin/env bash +# bashunit: no-parallel-tests + +function test_with_shared_state() { + # This test will not run in parallel with others in this file +} +``` + +The file will still run in parallel with other files, but tests within it will +run sequentially. +::: + ### Output Style > `bashunit test -s|--simple` diff --git a/release.sh b/release.sh index 32a33e0e..c8f1d411 100755 --- a/release.sh +++ b/release.sh @@ -375,10 +375,28 @@ function release::sandbox::create() { release::log_info "Creating sandbox at: $SANDBOX_DIR" # Copy repo content excluding .git, .release-state, node_modules - # Using cp + rm for portability (rsync not available on all systems) - cp -r . "$SANDBOX_DIR/" - rm -rf "$SANDBOX_DIR/.git" "$SANDBOX_DIR/.release-state" "$SANDBOX_DIR/node_modules" - release::log_verbose "Copied project files to sandbox" + # Try tar pipe first (faster), fallback to cp + rm for portability + # Disable errexit temporarily to allow tar fallback in strict mode + local tar_status=0 + set +e + tar --exclude='.git' \ + --exclude='.release-state' \ + --exclude='node_modules' \ + --exclude='.tasks' \ + --exclude='tmp' \ + -cf - . 2>/dev/null | tar -xf - -C "$SANDBOX_DIR" 2>/dev/null + tar_status=$? + set -e + + if [ "$tar_status" -eq 0 ]; then + release::log_verbose "Copied project files to sandbox (tar)" + else + # Fallback: traditional cp + rm for maximum portability + cp -r . "$SANDBOX_DIR/" + rm -rf "$SANDBOX_DIR/.git" "$SANDBOX_DIR/.release-state" \ + "$SANDBOX_DIR/node_modules" "$SANDBOX_DIR/.tasks" "$SANDBOX_DIR/tmp" + release::log_verbose "Copied project files to sandbox (cp)" + fi } function release::sandbox::setup_git() { diff --git a/src/parallel.sh b/src/parallel.sh index c0949638..6787e4ab 100755 --- a/src/parallel.sh +++ b/src/parallel.sh @@ -28,7 +28,8 @@ function bashunit::parallel::aggregate_test_results() { local result_file="" for result_file in "${result_files[@]+"${result_files[@]}"}"; do local result_line - result_line=$(tail -n 1 <"$result_file") + result_line=$(<"$result_file") + result_line="${result_line##*$'\n'}" local failed="${result_line##*##ASSERTIONS_FAILED=}" failed="${failed%%##*}" diff --git a/src/runner.sh b/src/runner.sh index aa96d01b..8f2234bd 100755 --- a/src/runner.sh +++ b/src/runner.sh @@ -102,7 +102,8 @@ function bashunit::runner::load_test_files() { local spinner_pid=$! bashunit::parallel::aggregate_test_results "$TEMP_DIR_PARALLEL_TEST_SUITE" # Kill the spinner once the aggregation finishes - disown "$spinner_pid" && kill "$spinner_pid" &>/dev/null + disown "$spinner_pid" 2>/dev/null || true + kill "$spinner_pid" 2>/dev/null || true printf "\r \r" # Clear the spinner output local script_id for script_id in "${scripts_ids[@]+"${scripts_ids[@]}"}"; do @@ -342,6 +343,12 @@ function bashunit::runner::call_test_functions() { bashunit::helper::check_duplicate_functions "$script" || true + # Check if test file opts out of test-level parallelism + local allow_test_parallel=true + if grep -q "^# bashunit: no-parallel-tests" "$script" 2>/dev/null; then + allow_test_parallel=false + fi + local -a provider_data=() local provider_data_count=0 local -a parsed_data=() @@ -362,8 +369,12 @@ function bashunit::runner::call_test_functions() { done <<<"$(bashunit::helper::get_provider_data "$fn_name" "$script")" # No data provider found - if [[ "$provider_data_count" -eq 0 ]]; then - bashunit::runner::run_test "$script" "$fn_name" + if [ "$provider_data_count" -eq 0 ]; then + if bashunit::parallel::is_enabled && [ "$allow_test_parallel" = true ]; then + bashunit::runner::run_test "$script" "$fn_name" & + else + bashunit::runner::run_test "$script" "$fn_name" + fi unset -v fn_name continue fi @@ -375,14 +386,23 @@ function bashunit::runner::call_test_functions() { parsed_data_count=0 local line while IFS= read -r line; do - [[ -z "$line" ]] && continue + [ -z "$line" ] && continue parsed_data[parsed_data_count]="$(bashunit::helper::decode_base64 "${line}")" parsed_data_count=$((parsed_data_count + 1)) done <<<"$(bashunit::runner::parse_data_provider_args "$data")" - bashunit::runner::run_test "$script" "$fn_name" ${parsed_data+"${parsed_data[@]}"} + if bashunit::parallel::is_enabled && [ "$allow_test_parallel" = true ]; then + bashunit::runner::run_test "$script" "$fn_name" ${parsed_data+"${parsed_data[@]}"} & + else + bashunit::runner::run_test "$script" "$fn_name" ${parsed_data+"${parsed_data[@]}"} + fi done unset -v fn_name done + + # Wait for all parallel tests within this file to complete + if bashunit::parallel::is_enabled && [ "$allow_test_parallel" = true ]; then + wait + fi } function bashunit::runner::call_bench_functions() { diff --git a/tests/acceptance/bashunit_assert_basic_test.sh b/tests/acceptance/bashunit_assert_basic_test.sh new file mode 100644 index 00000000..73ec4289 --- /dev/null +++ b/tests/acceptance/bashunit_assert_basic_test.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +set -euo pipefail + +function set_up() { + export BASHUNIT_SIMPLE_OUTPUT=false +} + +# Test basic assert subcommand functionality +function test_bashunit_assert_subcommand_equals() { + ./bashunit assert equals "foo" "foo" + assert_successful_code +} + +function test_bashunit_assert_subcommand_same() { + ./bashunit assert same "1" "1" + assert_successful_code +} + +function test_bashunit_assert_subcommand_contains() { + ./bashunit assert contains "world" "hello world" + assert_successful_code +} + +function test_bashunit_assert_subcommand_without_prefix() { + ./bashunit assert equals "bar" "bar" + assert_successful_code +} + +# Test help functionality +function test_bashunit_assert_subcommand_help_short() { + local output + output=$(./bashunit assert -h 2>&1) + + assert_contains "Usage: bashunit assert" "$output" + assert_contains "Run standalone assertion" "$output" + assert_successful_code "$(./bashunit assert -h)" +} + +function test_bashunit_assert_subcommand_help_long() { + local output + output=$(./bashunit assert --help 2>&1) + + assert_contains "Usage: bashunit assert" "$output" + assert_contains "Single assertion:" "$output" + assert_successful_code "$(./bashunit assert --help)" +} + +# Test assert subcommand is in main help +function test_bashunit_main_help_includes_assert() { + local output + output=$(./bashunit --help 2>&1) + + assert_contains "assert " "$output" +} + +function test_multi_assert_help_shows_multi_syntax() { + local output + output=$(./bashunit assert --help 2>&1) + assert_contains "Multiple assertions on command output" "$output" +} diff --git a/tests/acceptance/bashunit_assert_errors_test.sh b/tests/acceptance/bashunit_assert_errors_test.sh new file mode 100644 index 00000000..513e0269 --- /dev/null +++ b/tests/acceptance/bashunit_assert_errors_test.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +set -euo pipefail + +function set_up() { + export BASHUNIT_SIMPLE_OUTPUT=false +} + +# Test error cases +function test_bashunit_assert_subcommand_no_function() { + local output + local exit_code + output=$(./bashunit assert 2>&1) && exit_code=$? || exit_code=$? + + assert_contains "Error: Assert function name or command is required" "$output" + assert_general_error "" "" "$exit_code" +} + +function test_bashunit_assert_subcommand_non_existing_function() { + local exit_code + ./bashunit assert non_existing_function 2>&1 && exit_code=$? || exit_code=$? + assert_command_not_found "" "" "$exit_code" +} + +function test_bashunit_assert_subcommand_failure() { + local exit_code + ./bashunit --no-parallel assert equals "foo" "bar" 2>&1 && exit_code=$? || exit_code=$? + assert_general_error "" "" "$exit_code" +} + +# Test backward compatibility with --assert option +function test_bashunit_old_assert_option_still_works() { + local output + output=$(./bashunit -a equals "foo" "foo" 2>&1) + assert_successful_code "$output" +} + +function test_bashunit_old_assert_option_long_form() { + local output + output=$(./bashunit --assert equals "foo" "foo" 2>&1) + assert_successful_code "$output" +} + +# Test deprecation notice in help +function test_bashunit_test_help_shows_deprecation() { + local output + output=$(./bashunit test --help 2>&1) + + assert_contains "deprecated" "$output" + assert_contains "bashunit assert" "$output" +} diff --git a/tests/acceptance/bashunit_assert_multi_test.sh b/tests/acceptance/bashunit_assert_multi_test.sh new file mode 100644 index 00000000..a9ea0540 --- /dev/null +++ b/tests/acceptance/bashunit_assert_multi_test.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +set -euo pipefail + +function set_up() { + export BASHUNIT_SIMPLE_OUTPUT=false +} + +# Test multi-assertion mode +function test_multi_assert_exit_code_and_contains() { + ./bashunit assert "echo 'some error' && exit 1" exit_code "1" contains "some error" 2>&1 + assert_successful_code +} + +function test_multi_assert_exit_code_zero_and_output() { + ./bashunit assert "echo 'success message'" exit_code "0" contains "success" 2>&1 + assert_successful_code +} + +function test_multi_assert_multiple_output_assertions() { + ./bashunit assert "echo 'hello world'" exit_code "0" contains "hello" contains "world" 2>&1 + assert_successful_code +} + +function test_multi_assert_fails_on_exit_code_mismatch() { + local exit_code + ./bashunit assert "echo 'output' && exit 1" exit_code "0" 2>&1 && exit_code=$? || exit_code=$? + assert_general_error "" "" "$exit_code" +} + +function test_multi_assert_fails_on_contains_mismatch() { + local exit_code + ./bashunit assert "echo 'actual output'" exit_code "0" contains "expected" 2>&1 && exit_code=$? || exit_code=$? + assert_general_error "" "" "$exit_code" +} + +function test_multi_assert_missing_assertion_arg() { + local exit_code + local output + output=$(./bashunit assert "echo test" exit_code 2>&1) && exit_code=$? || exit_code=$? + assert_contains "Missing argument for assertion" "$output" + assert_general_error "" "" "$exit_code" +} diff --git a/tests/acceptance/bashunit_assert_subcommand_test.sh b/tests/acceptance/bashunit_assert_subcommand_test.sh deleted file mode 100755 index 5c4eb945..00000000 --- a/tests/acceptance/bashunit_assert_subcommand_test.sh +++ /dev/null @@ -1,140 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -function set_up() { - export BASHUNIT_SIMPLE_OUTPUT=false -} - -# Test basic assert subcommand functionality -function test_bashunit_assert_subcommand_equals() { - ./bashunit assert equals "foo" "foo" - assert_successful_code -} - -function test_bashunit_assert_subcommand_same() { - ./bashunit assert same "1" "1" - assert_successful_code -} - -function test_bashunit_assert_subcommand_contains() { - ./bashunit assert contains "world" "hello world" - assert_successful_code -} - -function test_bashunit_assert_subcommand_without_prefix() { - ./bashunit assert equals "bar" "bar" - assert_successful_code -} - -# Test help functionality -function test_bashunit_assert_subcommand_help_short() { - local output - output=$(./bashunit assert -h 2>&1) - - assert_contains "Usage: bashunit assert" "$output" - assert_contains "Run standalone assertion" "$output" - assert_successful_code "$(./bashunit assert -h)" -} - -function test_bashunit_assert_subcommand_help_long() { - local output - output=$(./bashunit assert --help 2>&1) - - assert_contains "Usage: bashunit assert" "$output" - assert_contains "Single assertion:" "$output" - assert_successful_code "$(./bashunit assert --help)" -} - -# Test error cases -function test_bashunit_assert_subcommand_no_function() { - local output - local exit_code - output=$(./bashunit assert 2>&1) && exit_code=$? || exit_code=$? - - assert_contains "Error: Assert function name or command is required" "$output" - assert_general_error "" "" "$exit_code" -} - -function test_bashunit_assert_subcommand_non_existing_function() { - local exit_code - ./bashunit assert non_existing_function 2>&1 && exit_code=$? || exit_code=$? - assert_command_not_found "" "" "$exit_code" -} - -function test_bashunit_assert_subcommand_failure() { - local exit_code - ./bashunit --no-parallel assert equals "foo" "bar" 2>&1 && exit_code=$? || exit_code=$? - assert_general_error "" "" "$exit_code" -} - -# Test backward compatibility with --assert option -function test_bashunit_old_assert_option_still_works() { - local output - output=$(./bashunit -a equals "foo" "foo" 2>&1) - assert_successful_code "$output" -} - -function test_bashunit_old_assert_option_long_form() { - local output - output=$(./bashunit --assert equals "foo" "foo" 2>&1) - assert_successful_code "$output" -} - -# Test deprecation notice in help -function test_bashunit_test_help_shows_deprecation() { - local output - output=$(./bashunit test --help 2>&1) - - assert_contains "deprecated" "$output" - assert_contains "bashunit assert" "$output" -} - -# Test assert subcommand is in main help -function test_bashunit_main_help_includes_assert() { - local output - output=$(./bashunit --help 2>&1) - - assert_contains "assert " "$output" -} - -# Test multi-assertion mode -function test_multi_assert_exit_code_and_contains() { - ./bashunit assert "echo 'some error' && exit 1" exit_code "1" contains "some error" 2>&1 - assert_successful_code -} - -function test_multi_assert_exit_code_zero_and_output() { - ./bashunit assert "echo 'success message'" exit_code "0" contains "success" 2>&1 - assert_successful_code -} - -function test_multi_assert_multiple_output_assertions() { - ./bashunit assert "echo 'hello world'" exit_code "0" contains "hello" contains "world" 2>&1 - assert_successful_code -} - -function test_multi_assert_fails_on_exit_code_mismatch() { - local exit_code - ./bashunit assert "echo 'output' && exit 1" exit_code "0" 2>&1 && exit_code=$? || exit_code=$? - assert_general_error "" "" "$exit_code" -} - -function test_multi_assert_fails_on_contains_mismatch() { - local exit_code - ./bashunit assert "echo 'actual output'" exit_code "0" contains "expected" 2>&1 && exit_code=$? || exit_code=$? - assert_general_error "" "" "$exit_code" -} - -function test_multi_assert_missing_assertion_arg() { - local exit_code - local output - output=$(./bashunit assert "echo test" exit_code 2>&1) && exit_code=$? || exit_code=$? - assert_contains "Missing argument for assertion" "$output" - assert_general_error "" "" "$exit_code" -} - -function test_multi_assert_help_shows_multi_syntax() { - local output - output=$(./bashunit assert --help 2>&1) - assert_contains "Multiple assertions on command output" "$output" -} diff --git a/tests/acceptance/bashunit_direct_fn_call_test.sh b/tests/acceptance/bashunit_direct_fn_call_advanced_test.sh similarity index 61% rename from tests/acceptance/bashunit_direct_fn_call_test.sh rename to tests/acceptance/bashunit_direct_fn_call_advanced_test.sh index 5ba1e63c..93b9c497 100644 --- a/tests/acceptance/bashunit_direct_fn_call_test.sh +++ b/tests/acceptance/bashunit_direct_fn_call_advanced_test.sh @@ -3,77 +3,12 @@ set -euo pipefail function set_up_before_script() { TEST_ENV_FILE="tests/acceptance/fixtures/.env.default" - TEST_MULTILINE_STR="first line -\n -four line -find me with \n a regular expression" } function set_up() { export BASHUNIT_SIMPLE_OUTPUT=false } -function test_bashunit_direct_fn_call_passes() { - local expected="foo" - local actual="foo" - - ./bashunit -a assert_same --env "$TEST_ENV_FILE" "$expected" $actual - assert_successful_code -} - -function test_bashunit_direct_fn_call_without_assert_prefix_passes() { - local expected="foo" - local actual="foo" - - ./bashunit -a equals --env "$TEST_ENV_FILE" "$expected" $actual - assert_successful_code -} - -function test_bashunit_assert_line_count() { - ./bashunit -a line_count 6 "$TEST_MULTILINE_STR" - assert_successful_code -} - -function test_bashunit_assert_contains() { - ./bashunit -a contains "four" "$TEST_MULTILINE_STR" - assert_successful_code -} - -function test_bashunit_assert_not_contains() { - ./bashunit -a not_contains "unknown" "$TEST_MULTILINE_STR" - assert_successful_code -} - -function test_bashunit_assert_matches() { - ./bashunit -a matches "with.+regular expr" "$TEST_MULTILINE_STR" - assert_successful_code -} - -function test_bashunit_assert_not_matches() { - ./bashunit -a not_matches "unknown" "$TEST_MULTILINE_STR" - assert_successful_code -} - -function test_bashunit_assert_string_starts_with() { - ./bashunit -a string_starts_with "first" "$TEST_MULTILINE_STR" - assert_successful_code -} - -function test_bashunit_assert_string_not_starts_with() { - ./bashunit -a string_not_starts_with "unknown" "$TEST_MULTILINE_STR" - assert_successful_code -} - -function test_bashunit_assert_string_ends_with() { - ./bashunit -a string_ends_with "expression" "$TEST_MULTILINE_STR" - assert_successful_code -} - -function test_bashunit_assert_string_not_ends_with() { - ./bashunit -a string_not_ends_with "unknown" "$TEST_MULTILINE_STR" - assert_successful_code -} - function test_bashunit_direct_fn_call_failure() { local expected="foo" local actual="bar" diff --git a/tests/acceptance/bashunit_direct_fn_call_basic_test.sh b/tests/acceptance/bashunit_direct_fn_call_basic_test.sh new file mode 100644 index 00000000..31382300 --- /dev/null +++ b/tests/acceptance/bashunit_direct_fn_call_basic_test.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +set -euo pipefail + +function set_up_before_script() { + TEST_ENV_FILE="tests/acceptance/fixtures/.env.default" + TEST_MULTILINE_STR="first line +\n +four line +find me with \n a regular expression" +} + +function set_up() { + export BASHUNIT_SIMPLE_OUTPUT=false +} + +function test_bashunit_direct_fn_call_passes() { + local expected="foo" + local actual="foo" + + ./bashunit -a assert_same --env "$TEST_ENV_FILE" "$expected" $actual + assert_successful_code +} + +function test_bashunit_direct_fn_call_without_assert_prefix_passes() { + local expected="foo" + local actual="foo" + + ./bashunit -a equals --env "$TEST_ENV_FILE" "$expected" $actual + assert_successful_code +} + +function test_bashunit_assert_line_count() { + ./bashunit -a line_count 6 "$TEST_MULTILINE_STR" + assert_successful_code +} + +function test_bashunit_assert_contains() { + ./bashunit -a contains "four" "$TEST_MULTILINE_STR" + assert_successful_code +} + +function test_bashunit_assert_not_contains() { + ./bashunit -a not_contains "unknown" "$TEST_MULTILINE_STR" + assert_successful_code +} + +function test_bashunit_assert_matches() { + ./bashunit -a matches "with.+regular expr" "$TEST_MULTILINE_STR" + assert_successful_code +} + +function test_bashunit_assert_not_matches() { + ./bashunit -a not_matches "unknown" "$TEST_MULTILINE_STR" + assert_successful_code +} + +function test_bashunit_assert_string_starts_with() { + ./bashunit -a string_starts_with "first" "$TEST_MULTILINE_STR" + assert_successful_code +} + +function test_bashunit_assert_string_not_starts_with() { + ./bashunit -a string_not_starts_with "unknown" "$TEST_MULTILINE_STR" + assert_successful_code +} + +function test_bashunit_assert_string_ends_with() { + ./bashunit -a string_ends_with "expression" "$TEST_MULTILINE_STR" + assert_successful_code +} + +function test_bashunit_assert_string_not_ends_with() { + ./bashunit -a string_not_ends_with "unknown" "$TEST_MULTILINE_STR" + assert_successful_code +} diff --git a/tests/acceptance/bashunit_lifecycle_output_test.sh b/tests/acceptance/bashunit_lifecycle_verbose_test.sh similarity index 54% rename from tests/acceptance/bashunit_lifecycle_output_test.sh rename to tests/acceptance/bashunit_lifecycle_verbose_test.sh index 10112bcc..0c380f2b 100644 --- a/tests/acceptance/bashunit_lifecycle_output_test.sh +++ b/tests/acceptance/bashunit_lifecycle_verbose_test.sh @@ -101,97 +101,3 @@ EOF assert_contains "stdout from test" "$output" assert_contains "stderr from test" "$output" } - -function test_hook_visibility_shows_running_message_in_normal_mode() { - local test_file="$TEST_DIR/test_hook_visibility_normal.sh" - cat >"$test_file" <<'EOF' -#!/usr/bin/env bash - -function set_up_before_script() { -true -} - -function tear_down_after_script() { -true -} - -function test_hook_normal_mode() { -assert_same "foo" "foo" -} -EOF - - local output - # Explicitly disable simple/parallel modes to ensure normal output - output=$(BASHUNIT_SIMPLE_OUTPUT=false BASHUNIT_PARALLEL_RUN=false ./bashunit "$test_file" 2>&1) - - assert_contains "● set_up_before_script" "$output" - assert_contains "● tear_down_after_script" "$output" -} - -function test_hook_visibility_suppressed_in_failures_only_mode() { - local test_file="$TEST_DIR/test_hook_visibility_failures.sh" - cat >"$test_file" <<'EOF' -#!/usr/bin/env bash - -function set_up_before_script() { -true -} - -function tear_down_after_script() { -true -} - -function test_hook_failures_only() { -assert_same "foo" "foo" -} -EOF - - local output - output=$(./bashunit --failures-only "$test_file" 2>&1) - - assert_not_contains "● set_up_before_script" "$output" - assert_not_contains "● tear_down_after_script" "$output" -} - -function test_hook_visibility_suppressed_in_simple_mode() { - local test_file="$TEST_DIR/test_hook_visibility_simple.sh" - cat >"$test_file" <<'EOF' -#!/usr/bin/env bash - -function set_up_before_script() { -true -} - -function tear_down_after_script() { -true -} - -function test_hook_simple_mode() { -assert_same "foo" "foo" -} -EOF - - local output - # Explicitly set simple mode and disable parallel to test simple output format - output=$(BASHUNIT_SIMPLE_OUTPUT=true BASHUNIT_PARALLEL_RUN=false ./bashunit --simple "$test_file" 2>&1) - - assert_not_contains "set_up_before_script" "$output" - assert_not_contains "tear_down_after_script" "$output" -} - -function test_hook_visibility_not_shown_when_hooks_not_defined() { - local test_file="$TEST_DIR/test_no_hooks.sh" - cat >"$test_file" <<'EOF' -#!/usr/bin/env bash - -function test_no_hooks_defined() { -assert_same "foo" "foo" -} -EOF - - local output - output=$(BASHUNIT_SIMPLE_OUTPUT=false BASHUNIT_PARALLEL_RUN=false ./bashunit "$test_file" 2>&1) - - assert_not_contains "● set_up_before_script" "$output" - assert_not_contains "● tear_down_after_script" "$output" -} diff --git a/tests/acceptance/bashunit_lifecycle_visibility_test.sh b/tests/acceptance/bashunit_lifecycle_visibility_test.sh new file mode 100644 index 00000000..805f3018 --- /dev/null +++ b/tests/acceptance/bashunit_lifecycle_visibility_test.sh @@ -0,0 +1,103 @@ +#!/usr/bin/env bash + +function set_up() { + TEST_DIR=$(mktemp -d) +} + +function tear_down() { + rm -rf "$TEST_DIR" +} + +function test_hook_visibility_shows_running_message_in_normal_mode() { + local test_file="$TEST_DIR/test_hook_visibility_normal.sh" + cat >"$test_file" <<'EOF' +#!/usr/bin/env bash + +function set_up_before_script() { +true +} + +function tear_down_after_script() { +true +} + +function test_hook_normal_mode() { +assert_same "foo" "foo" +} +EOF + + local output + # Explicitly disable simple/parallel modes to ensure normal output + output=$(BASHUNIT_SIMPLE_OUTPUT=false BASHUNIT_PARALLEL_RUN=false ./bashunit "$test_file" 2>&1) + + assert_contains "● set_up_before_script" "$output" + assert_contains "● tear_down_after_script" "$output" +} + +function test_hook_visibility_suppressed_in_failures_only_mode() { + local test_file="$TEST_DIR/test_hook_visibility_failures.sh" + cat >"$test_file" <<'EOF' +#!/usr/bin/env bash + +function set_up_before_script() { +true +} + +function tear_down_after_script() { +true +} + +function test_hook_failures_only() { +assert_same "foo" "foo" +} +EOF + + local output + output=$(./bashunit --failures-only "$test_file" 2>&1) + + assert_not_contains "● set_up_before_script" "$output" + assert_not_contains "● tear_down_after_script" "$output" +} + +function test_hook_visibility_suppressed_in_simple_mode() { + local test_file="$TEST_DIR/test_hook_visibility_simple.sh" + cat >"$test_file" <<'EOF' +#!/usr/bin/env bash + +function set_up_before_script() { +true +} + +function tear_down_after_script() { +true +} + +function test_hook_simple_mode() { +assert_same "foo" "foo" +} +EOF + + local output + # Explicitly set simple mode and disable parallel to test simple output format + output=$(BASHUNIT_SIMPLE_OUTPUT=true BASHUNIT_PARALLEL_RUN=false ./bashunit --simple "$test_file" 2>&1) + + assert_not_contains "set_up_before_script" "$output" + assert_not_contains "tear_down_after_script" "$output" +} + +function test_hook_visibility_not_shown_when_hooks_not_defined() { + local test_file="$TEST_DIR/test_no_hooks.sh" + cat >"$test_file" <<'EOF' +#!/usr/bin/env bash + +function test_no_hooks_defined() { +assert_same "foo" "foo" +} +EOF + + local output + output=$(BASHUNIT_SIMPLE_OUTPUT=false BASHUNIT_PARALLEL_RUN=false ./bashunit "$test_file" 2>&1) + + assert_not_contains "● set_up_before_script" "$output" + assert_not_contains "● tear_down_after_script" "$output" +} diff --git a/tests/acceptance/bashunit_report_html_test.sh b/tests/acceptance/bashunit_report_html_test.sh index ef4b4d54..c989bb27 100644 --- a/tests/acceptance/bashunit_report_html_test.sh +++ b/tests/acceptance/bashunit_report_html_test.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# bashunit: no-parallel-tests set -euo pipefail function set_up_before_script() { diff --git a/tests/acceptance/bashunit_script_temp_file_cleanup_test.sh b/tests/acceptance/bashunit_script_temp_file_cleanup_test.sh index 6af251c0..f29a4f68 100644 --- a/tests/acceptance/bashunit_script_temp_file_cleanup_test.sh +++ b/tests/acceptance/bashunit_script_temp_file_cleanup_test.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# bashunit: no-parallel-tests # @data_provider execution_modes function test_script_temp_files_are_cleaned_up_after_test_run() { diff --git a/tests/acceptance/bashunit_upgrade_test.sh b/tests/acceptance/bashunit_upgrade_test.sh index bb0ee8c4..76677736 100644 --- a/tests/acceptance/bashunit_upgrade_test.sh +++ b/tests/acceptance/bashunit_upgrade_test.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# bashunit: no-parallel-tests set -uo pipefail set +e diff --git a/tests/acceptance/install_test.sh b/tests/acceptance/install_test.sh index dc7e9304..47801abe 100644 --- a/tests/acceptance/install_test.sh +++ b/tests/acceptance/install_test.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# bashunit: no-parallel-tests # shellcheck disable=SC2317 set -uo pipefail set +e diff --git a/tests/acceptance/snapshots/bashunit_direct_fn_call_advanced_test_sh.test_bashunit_direct_fn_call_failure.snapshot b/tests/acceptance/snapshots/bashunit_direct_fn_call_advanced_test_sh.test_bashunit_direct_fn_call_failure.snapshot new file mode 100644 index 00000000..cb7069d8 --- /dev/null +++ b/tests/acceptance/snapshots/bashunit_direct_fn_call_advanced_test_sh.test_bashunit_direct_fn_call_failure.snapshot @@ -0,0 +1,3 @@ +✗ Failed: assert same + Expected 'foo' + but got  'bar' diff --git a/tests/acceptance/snapshots/bashunit_direct_fn_call_advanced_test_sh.test_bashunit_direct_fn_call_non_existing_fn.snapshot b/tests/acceptance/snapshots/bashunit_direct_fn_call_advanced_test_sh.test_bashunit_direct_fn_call_non_existing_fn.snapshot new file mode 100644 index 00000000..321ac66b --- /dev/null +++ b/tests/acceptance/snapshots/bashunit_direct_fn_call_advanced_test_sh.test_bashunit_direct_fn_call_non_existing_fn.snapshot @@ -0,0 +1 @@ +Function non_existing_fn does not exist. diff --git a/tests/unit/assert_advanced_test.sh b/tests/unit/assert_advanced_test.sh new file mode 100644 index 00000000..25536610 --- /dev/null +++ b/tests/unit/assert_advanced_test.sh @@ -0,0 +1,162 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2327 +# shellcheck disable=SC2328 +# shellcheck disable=SC2329 + +function test_successful_assert_not_empty() { + assert_empty "$(assert_not_empty "a_random_string")" +} + +function test_unsuccessful_assert_not_empty() { + assert_same \ + "$(bashunit::console_results::print_failed_test "Unsuccessful assert not empty" "to not be empty" "but got " "")" \ + "$(assert_not_empty "")" +} + +function test_successful_assert_not_same() { + assert_empty "$(assert_not_same "1" "2")" +} + +function test_unsuccessful_assert_not_same() { + assert_same "$(bashunit::console_results::print_failed_test "Unsuccessful assert not same" "1" "but got " "1")" \ + "$(assert_not_same "1" "1")" +} + +function test_successful_assert_general_error() { + function fake_function() { + return 1 + } + + assert_empty "$(assert_general_error "$(fake_function)")" +} + +function test_unsuccessful_assert_general_error() { + function fake_function() { + return 2 + } + + assert_same \ + "$(bashunit::console_results::print_failed_test "Unsuccessful assert general error" "2" "to be exactly" "1")" \ + "$(assert_general_error "$(fake_function)")" +} + +function test_successful_assert_command_not_found() { + assert_empty "$(assert_command_not_found "$(a_non_existing_function >/dev/null 2>&1)")" +} + +function test_unsuccessful_assert_command_not_found() { + function fake_function() { + return 0 + } + + assert_same \ + "$(bashunit::console_results::print_failed_test \ + "Unsuccessful assert command not found" "0" "to be exactly" "127")" \ + "$(assert_command_not_found "$(fake_function)")" +} + +function test_successful_assert_exec() { + # shellcheck disable=SC2317 + function fake_command() { + echo "Expected output" + echo "Expected error" >&2 + return 1 + } + + assert_empty "$(assert_exec fake_command --exit 1 --stdout "Expected output" --stderr "Expected error")" +} + +function test_unsuccessful_assert_exec() { + # shellcheck disable=SC2317 + function fake_command() { + echo "out" + echo "err" >&2 + return 0 + } + + local expected="exit: 1"$'\n'"stdout: Expected"$'\n'"stderr: Expected error" + local actual="exit: 0"$'\n'"stdout: out"$'\n'"stderr: err" + + assert_same \ + "$(bashunit::console_results::print_failed_test "Unsuccessful assert exec" "$expected" "but got " "$actual")" \ + "$(assert_exec fake_command --exit 1 --stdout "Expected" --stderr "Expected error")" +} + +function test_successful_assert_array_contains() { + local distros + distros=(Ubuntu 123 Linux\ Mint) + + assert_empty "$(assert_array_contains "123" "${distros[@]}")" +} + +function test_unsuccessful_assert_array_contains() { + local distros + distros=(Ubuntu 123 Linux\ Mint) + + assert_same "$(bashunit::console_results::print_failed_test \ + "Unsuccessful assert array contains" \ + "Ubuntu 123 Linux Mint" \ + "to contain" \ + "non_existing_element")" \ + "$(assert_array_contains "non_existing_element" "${distros[@]}")" +} + +function test_successful_assert_array_not_contains() { + local distros + distros=(Ubuntu 123 Linux\ Mint) + + assert_empty "$(assert_array_not_contains "a_non_existing_element" "${distros[@]}")" +} + +function test_unsuccessful_assert_array_not_contains() { + local distros + distros=(Ubuntu 123 Linux\ Mint) + + assert_same \ + "$(bashunit::console_results::print_failed_test \ + "Unsuccessful assert array not contains" "Ubuntu 123 Linux Mint" "to not contain" "123")" \ + "$(assert_array_not_contains "123" "${distros[@]}")" +} + +function test_successful_assert_line_count_empty_str() { + assert_empty "$(assert_line_count 0 "")" +} + +function test_successful_assert_line_count_one_line() { + assert_empty "$(assert_line_count 1 "one line")" +} + +function test_successful_assert_count_multiline() { + local multiline_string="this is line one + this is line two + this is line three" + + assert_empty "$(assert_line_count 3 "$multiline_string")" +} + +function test_successful_assert_line_count_multiline_string_in_one_line() { + assert_empty "$(assert_line_count 4 "one\ntwo\nthree\nfour")" +} + +function test_successful_assert_line_count_multiline_with_new_lines() { + local multiline_str="this \n is \n a multiline \n in one + \n + this is line 7 + this is \n line nine + " + + assert_empty "$(assert_line_count 10 "$multiline_str")" +} + +function test_unsuccessful_assert_line_count() { + assert_same \ + "$(bashunit::console_results::print_failed_test \ + "Unsuccessful assert line count" "one_line_string" "to contain number of lines equal to" "10" "but found" "1")" \ + "$(assert_line_count 10 "one_line_string")" +} + +function test_assert_line_count_does_not_modify_existing_variable() { + local additional_new_lines="original" + assert_empty "$(assert_line_count 1 "one")" + assert_same "original" "$additional_new_lines" +} diff --git a/tests/unit/assert_basic_test.sh b/tests/unit/assert_basic_test.sh new file mode 100644 index 00000000..4b756d06 --- /dev/null +++ b/tests/unit/assert_basic_test.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2327 +# shellcheck disable=SC2328 +# shellcheck disable=SC2329 + +function test_successful_fail() { + true || bashunit::fail "This cannot fail" +} + +function test_unsuccessful_fail() { + assert_same "$(bashunit::console_results::print_failure_message \ + "Unsuccessful fail" "Failure message")" \ + "$(bashunit::fail "Failure message")" +} + +# @data_provider provider_successful_assert_true +function test_successful_assert_true() { + # shellcheck disable=SC2086 + assert_empty "$(assert_true $1)" +} + +function provider_successful_assert_true() { + bashunit::data_set true + bashunit::data_set "true" + bashunit::data_set 0 +} + +function test_unsuccessful_assert_true() { + assert_same "$(bashunit::console_results::print_failed_test "Unsuccessful assert true" \ + "true or 0" \ + "but got " "false")" \ + "$(assert_true false)" +} + +function test_successful_assert_true_on_function() { + assert_empty "$(assert_true ls)" +} + +function test_unsuccessful_assert_true_on_function() { + assert_same "$(bashunit::console_results::print_failed_test "Unsuccessful assert true on function" \ + "command or function with zero exit code" \ + "but got " "exit code: 2")" \ + "$(assert_true "eval return 2")" +} + +# @data_provider provider_successful_assert_false +function test_successful_assert_false() { + # shellcheck disable=SC2086 + assert_empty "$(assert_false $1)" +} + +function provider_successful_assert_false() { + bashunit::data_set false + bashunit::data_set "false" + bashunit::data_set 1 +} + +function test_unsuccessful_assert_false() { + assert_same "$(bashunit::console_results::print_failed_test "Unsuccessful assert false" \ + "false or 1" \ + "but got " "true")" \ + "$(assert_false true)" +} + +function test_successful_assert_false_on_function() { + assert_empty "$(assert_false "eval return 1")" +} + +function test_unsuccessful_assert_false_on_function() { + assert_same "$(bashunit::console_results::print_failed_test "Unsuccessful assert false on function" \ + "command or function with non-zero exit code" \ + "but got " "exit code: 0")" \ + "$(assert_false "eval return 0")" +} + +function test_successful_assert_same() { + assert_empty "$(assert_same "1" "1")" +} + +function test_unsuccessful_assert_same() { + assert_same "$(bashunit::console_results::print_failed_test "Unsuccessful assert same" "1" "but got " "2")" \ + "$(assert_same "1" "2")" +} + +function test_successful_assert_empty() { + assert_empty "$(assert_empty "")" +} + +function test_unsuccessful_assert_empty() { + assert_same \ + "$(bashunit::console_results::print_failed_test "Unsuccessful assert empty" "to be empty" "but got " "1")" \ + "$(assert_empty "1")" +} diff --git a/tests/unit/assert_numeric_test.sh b/tests/unit/assert_numeric_test.sh new file mode 100644 index 00000000..6206f75e --- /dev/null +++ b/tests/unit/assert_numeric_test.sh @@ -0,0 +1,127 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2327 +# shellcheck disable=SC2328 +# shellcheck disable=SC2329 + +function test_successful_assert_exit_code() { + function fake_function() { + exit 0 + } + + assert_empty "$(assert_exit_code "0" "$(fake_function)")" +} + +function test_unsuccessful_assert_exit_code() { + function fake_function() { + exit 1 + } + + assert_same "$(bashunit::console_results::print_failed_test "Unsuccessful assert exit code" "1" "to be" "0")" \ + "$(assert_exit_code "0" "$(fake_function)")" +} + +function test_successful_return_assert_exit_code() { + function fake_function() { + return 0 + } + + fake_function + + assert_exit_code "0" +} + +function test_unsuccessful_return_assert_exit_code() { + function fake_function() { + return 1 + } + + assert_exit_code "1" "$(fake_function)" +} + +function test_successful_assert_successful_code() { + function fake_function() { + return 0 + } + + assert_empty "$(assert_successful_code "$(fake_function)")" +} + +function test_unsuccessful_assert_successful_code() { + function fake_function() { + return 2 + } + + assert_same \ + "$(bashunit::console_results::print_failed_test "Unsuccessful assert successful code" "2" "to be exactly" "0")" \ + "$(assert_successful_code "$(fake_function)")" +} + +function test_successful_assert_unsuccessful_code() { + function fake_function() { + return 2 + } + + assert_empty "$(assert_unsuccessful_code "$(fake_function)")" +} + +function test_unsuccessful_assert_unsuccessful_code() { + function fake_function() { + return 0 + } + + local expected + expected="$(bashunit::console_results::print_failed_test \ + "Unsuccessful assert unsuccessful code" "0" "to be non-zero" "but was 0")" + assert_same "$expected" "$(assert_unsuccessful_code "$(fake_function)")" +} + +function test_successful_assert_less_than() { + assert_empty "$(assert_less_than "3" "1")" +} + +function test_unsuccessful_assert_less_than() { + assert_same \ + "$(bashunit::console_results::print_failed_test "Unsuccessful assert less than" "3" "to be less than" "1")" \ + "$(assert_less_than "1" "3")" +} + +function test_successful_assert_less_or_equal_than_with_a_smaller_number() { + assert_empty "$(assert_less_or_equal_than "3" "1")" +} + +function test_successful_assert_less_or_equal_than_with_an_equal_number() { + assert_empty "$(assert_less_or_equal_than "3" "3")" +} + +function test_unsuccessful_assert_less_or_equal_than() { + assert_same \ + "$(bashunit::console_results::print_failed_test \ + "Unsuccessful assert less or equal than" "3" "to be less or equal than" "1")" \ + "$(assert_less_or_equal_than "1" "3")" +} + +function test_successful_assert_greater_than() { + assert_empty "$(assert_greater_than "1" "3")" +} + +function test_unsuccessful_assert_greater_than() { + assert_same \ + "$(bashunit::console_results::print_failed_test \ + "Unsuccessful assert greater than" "1" "to be greater than" "3")" \ + "$(assert_greater_than "3" "1")" +} + +function test_successful_assert_greater_or_equal_than_with_a_smaller_number() { + assert_empty "$(assert_greater_or_equal_than "1" "3")" +} + +function test_successful_assert_greater_or_equal_than_with_an_equal_number() { + assert_empty "$(assert_greater_or_equal_than "3" "3")" +} + +function test_unsuccessful_assert_greater_or_equal_than() { + assert_same \ + "$(bashunit::console_results::print_failed_test \ + "Unsuccessful assert greater or equal than" "1" "to be greater or equal than" "3")" \ + "$(assert_greater_or_equal_than "3" "1")" +} diff --git a/tests/unit/assert_string_test.sh b/tests/unit/assert_string_test.sh new file mode 100644 index 00000000..004e16b0 --- /dev/null +++ b/tests/unit/assert_string_test.sh @@ -0,0 +1,160 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2327 +# shellcheck disable=SC2328 +# shellcheck disable=SC2329 + +function test_successful_assert_not_equals() { + assert_empty "$(assert_not_equals "1" "2")" +} + +function test_unsuccessful_assert_not_equals() { + assert_same "$(bashunit::console_results::print_failed_test "Unsuccessful assert not equals" "1" "but got " "1")" \ + "$(assert_not_equals "1" "1")" +} + +function test_unsuccessful_assert_not_equals_with_special_chars() { + local str1="${_BASHUNIT_COLOR_FAILED}✗ Failed${_BASHUNIT_COLOR_DEFAULT} foo" + local str2="✗ Failed foo" + + assert_equals "$(bashunit::console_results::print_failed_test \ + "Unsuccessful assert not equals with special chars" \ + "$str1" "but got " "$str2")" \ + "$(assert_not_equals "$str1" "$str2")" +} + +function test_successful_assert_equals() { + assert_equals "✗ Failed foo" \ + "${_BASHUNIT_COLOR_FAILED}✗ Failed${_BASHUNIT_COLOR_DEFAULT} foo" +} + +function test_successful_assert_equals_with_special_chars() { + local string="${_BASHUNIT_COLOR_FAILED}✗ Failed${_BASHUNIT_COLOR_DEFAULT} foo" + + assert_equals "$string" "$string" +} + +function test_unsuccessful_assert_equals() { + local str1="${_BASHUNIT_COLOR_FAILED}✗ Failed${_BASHUNIT_COLOR_DEFAULT} str1" + local str2="${_BASHUNIT_COLOR_FAILED}✗ Failed${_BASHUNIT_COLOR_DEFAULT} str2" + + assert_same "$(bashunit::console_results::print_failed_test \ + "Unsuccessful assert equals" \ + "✗ Failed str1" \ + "but got " \ + "✗ Failed str2")" \ + "$(assert_equals "$str1" "$str2")" +} + +function test_successful_assert_contains_ignore_case() { + assert_empty "$(assert_contains_ignore_case "Linux" "GNU/LINUX")" +} + +function test_unsuccessful_assert_contains_ignore_case() { + local expected + expected="$(bashunit::console_results::print_failed_test \ + "Unsuccessful assert contains ignore case" "GNU/LINUX" "to contain" "Unix")" + assert_same "$expected" "$(assert_contains_ignore_case "Unix" "GNU/LINUX")" +} + +function test_successful_assert_contains() { + assert_empty "$(assert_contains "Linux" "GNU/Linux")" +} + +function test_unsuccessful_assert_contains() { + assert_same \ + "$(bashunit::console_results::print_failed_test "Unsuccessful assert contains" "GNU/Linux" "to contain" "Unix")" \ + "$(assert_contains "Unix" "GNU/Linux")" +} + +function test_successful_assert_not_contains() { + assert_empty "$(assert_not_contains "Linus" "GNU/Linux")" +} + +function test_unsuccessful_assert_not_contains() { + local expected + expected="$(bashunit::console_results::print_failed_test \ + "Unsuccessful assert not contains" "GNU/Linux" "to not contain" "Linux")" + assert_same "$expected" "$(assert_not_contains "Linux" "GNU/Linux")" +} + +function test_successful_assert_matches() { + assert_empty "$(assert_matches ".*Linu*" "GNU/Linux")" +} + +function test_unsuccessful_assert_matches() { + assert_same \ + "$(bashunit::console_results::print_failed_test "Unsuccessful assert matches" "GNU/Linux" "to match" ".*Pinux*")" \ + "$(assert_matches ".*Pinux*" "GNU/Linux")" +} + +function test_successful_assert_not_matches() { + assert_empty "$(assert_not_matches ".*Pinux*" "GNU/Linux")" +} + +function test_unsuccessful_assert_not_matches() { + local expected + expected="$(bashunit::console_results::print_failed_test \ + "Unsuccessful assert not matches" "GNU/Linux" "to not match" ".*Linu*")" + assert_same "$expected" "$(assert_not_matches ".*Linu*" "GNU/Linux")" +} + +function test_successful_assert_string_starts_with() { + assert_empty "$(assert_string_starts_with "ho" "house")" +} + +function test_unsuccessful_assert_string_starts_with() { + assert_same \ + "$(bashunit::console_results::print_failed_test \ + "Unsuccessful assert string starts with" "pause" "to start with" "hou")" \ + "$(assert_string_starts_with "hou" "pause")" +} + +function test_successful_assert_string_not_starts_with() { + assert_empty "$(assert_string_not_starts_with "hou" "pause")" +} + +function test_unsuccessful_assert_string_not_starts_with() { + assert_same \ + "$(bashunit::console_results::print_failed_test \ + "Unsuccessful assert string not starts with" "house" "to not start with" "ho")" \ + "$(assert_string_not_starts_with "ho" "house")" +} + +function test_successful_assert_string_ends_with() { + assert_empty "$(assert_string_ends_with "bar" "foobar")" +} + +function test_unsuccessful_assert_string_ends_with() { + assert_same \ + "$(bashunit::console_results::print_failed_test \ + "Unsuccessful assert string ends with" "foobar" "to end with" "foo")" \ + "$(assert_string_ends_with "foo" "foobar")" +} + +function test_successful_assert_string_not_ends_with() { + assert_empty "$(assert_string_not_ends_with "foo" "foobar")" +} + +function test_unsuccessful_assert_string_not_ends_with() { + assert_same \ + "$(bashunit::console_results::print_failed_test \ + "Unsuccessful assert string not ends with" "foobar" "to not end with" "bar")" \ + "$(assert_string_not_ends_with "bar" "foobar")" +} + +function test_assert_string_start_end_with_special_chars() { + assert_empty "$(assert_string_starts_with "foo." "foo.bar")" + assert_empty "$(assert_string_ends_with ".bar" "foo.bar")" +} + +function test_assert_string_start_end_with_special_chars_fail() { + assert_same \ + "$(bashunit::console_results::print_failed_test \ + "Assert string start end with special chars fail" "fooX" "to start with" "foo.")" \ + "$(assert_string_starts_with "foo." "fooX")" + + assert_same \ + "$(bashunit::console_results::print_failed_test \ + "Assert string start end with special chars fail" "fooX" "to end with" ".bar")" \ + "$(assert_string_ends_with ".bar" "fooX")" +} diff --git a/tests/unit/assert_test.sh b/tests/unit/assert_test.sh deleted file mode 100644 index bc6857c7..00000000 --- a/tests/unit/assert_test.sh +++ /dev/null @@ -1,530 +0,0 @@ -#!/usr/bin/env bash -# shellcheck disable=SC2327 -# shellcheck disable=SC2328 -# shellcheck disable=SC2329 - -function test_successful_fail() { - true || bashunit::fail "This cannot fail" -} - -function test_unsuccessful_fail() { - assert_same "$(bashunit::console_results::print_failure_message \ - "Unsuccessful fail" "Failure message")" \ - "$(bashunit::fail "Failure message")" -} - -# @data_provider provider_successful_assert_true -function test_successful_assert_true() { - # shellcheck disable=SC2086 - assert_empty "$(assert_true $1)" -} - -function provider_successful_assert_true() { - bashunit::data_set true - bashunit::data_set "true" - bashunit::data_set 0 -} - -function test_unsuccessful_assert_true() { - assert_same "$(bashunit::console_results::print_failed_test "Unsuccessful assert true" \ - "true or 0" \ - "but got " "false")" \ - "$(assert_true false)" -} - -function test_successful_assert_true_on_function() { - assert_empty "$(assert_true ls)" -} - -function test_unsuccessful_assert_true_on_function() { - assert_same "$(bashunit::console_results::print_failed_test "Unsuccessful assert true on function" \ - "command or function with zero exit code" \ - "but got " "exit code: 2")" \ - "$(assert_true "eval return 2")" -} - -# @data_provider provider_successful_assert_false -function test_successful_assert_false() { - # shellcheck disable=SC2086 - assert_empty "$(assert_false $1)" -} - -function provider_successful_assert_false() { - bashunit::data_set false - bashunit::data_set "false" - bashunit::data_set 1 -} - -function test_unsuccessful_assert_false() { - assert_same "$(bashunit::console_results::print_failed_test "Unsuccessful assert false" \ - "false or 1" \ - "but got " "true")" \ - "$(assert_false true)" -} - -function test_successful_assert_false_on_function() { - assert_empty "$(assert_false "eval return 1")" -} - -function test_unsuccessful_assert_false_on_function() { - assert_same "$(bashunit::console_results::print_failed_test "Unsuccessful assert false on function" \ - "command or function with non-zero exit code" \ - "but got " "exit code: 0")" \ - "$(assert_false "eval return 0")" -} - -function test_successful_assert_same() { - assert_empty "$(assert_same "1" "1")" -} - -function test_unsuccessful_assert_same() { - assert_same "$(bashunit::console_results::print_failed_test "Unsuccessful assert same" "1" "but got " "2")" \ - "$(assert_same "1" "2")" -} - -function test_successful_assert_empty() { - assert_empty "$(assert_empty "")" -} - -function test_unsuccessful_assert_empty() { - assert_same \ - "$(bashunit::console_results::print_failed_test "Unsuccessful assert empty" "to be empty" "but got " "1")" \ - "$(assert_empty "1")" -} - -function test_successful_assert_not_empty() { - assert_empty "$(assert_not_empty "a_random_string")" -} - -function test_unsuccessful_assert_not_empty() { - assert_same \ - "$(bashunit::console_results::print_failed_test "Unsuccessful assert not empty" "to not be empty" "but got " "")" \ - "$(assert_not_empty "")" -} - -function test_successful_assert_not_same() { - assert_empty "$(assert_not_same "1" "2")" -} - -function test_unsuccessful_assert_not_same() { - assert_same "$(bashunit::console_results::print_failed_test "Unsuccessful assert not same" "1" "but got " "1")" \ - "$(assert_not_same "1" "1")" -} - -function test_successful_assert_not_equals() { - assert_empty "$(assert_not_equals "1" "2")" -} - -function test_unsuccessful_assert_not_equals() { - assert_same "$(bashunit::console_results::print_failed_test "Unsuccessful assert not equals" "1" "but got " "1")" \ - "$(assert_not_equals "1" "1")" -} - -function test_unsuccessful_assert_not_equals_with_special_chars() { - local str1="${_BASHUNIT_COLOR_FAILED}✗ Failed${_BASHUNIT_COLOR_DEFAULT} foo" - local str2="✗ Failed foo" - - assert_equals "$(bashunit::console_results::print_failed_test \ - "Unsuccessful assert not equals with special chars" \ - "$str1" "but got " "$str2")" \ - "$(assert_not_equals "$str1" "$str2")" -} - -function test_successful_assert_contains_ignore_case() { - assert_empty "$(assert_contains_ignore_case "Linux" "GNU/LINUX")" -} - -function test_unsuccessful_assert_contains_ignore_case() { - local expected - expected="$(bashunit::console_results::print_failed_test \ - "Unsuccessful assert contains ignore case" "GNU/LINUX" "to contain" "Unix")" - assert_same "$expected" "$(assert_contains_ignore_case "Unix" "GNU/LINUX")" -} - -function test_successful_assert_contains() { - assert_empty "$(assert_contains "Linux" "GNU/Linux")" -} - -function test_unsuccessful_assert_contains() { - assert_same \ - "$(bashunit::console_results::print_failed_test "Unsuccessful assert contains" "GNU/Linux" "to contain" "Unix")" \ - "$(assert_contains "Unix" "GNU/Linux")" -} - -function test_successful_assert_not_contains() { - assert_empty "$(assert_not_contains "Linus" "GNU/Linux")" -} - -function test_unsuccessful_assert_not_contains() { - local expected - expected="$(bashunit::console_results::print_failed_test \ - "Unsuccessful assert not contains" "GNU/Linux" "to not contain" "Linux")" - assert_same "$expected" "$(assert_not_contains "Linux" "GNU/Linux")" -} - -function test_successful_assert_matches() { - assert_empty "$(assert_matches ".*Linu*" "GNU/Linux")" -} - -function test_unsuccessful_assert_matches() { - assert_same \ - "$(bashunit::console_results::print_failed_test "Unsuccessful assert matches" "GNU/Linux" "to match" ".*Pinux*")" \ - "$(assert_matches ".*Pinux*" "GNU/Linux")" -} - -function test_successful_assert_not_matches() { - assert_empty "$(assert_not_matches ".*Pinux*" "GNU/Linux")" -} - -function test_unsuccessful_assert_not_matches() { - local expected - expected="$(bashunit::console_results::print_failed_test \ - "Unsuccessful assert not matches" "GNU/Linux" "to not match" ".*Linu*")" - assert_same "$expected" "$(assert_not_matches ".*Linu*" "GNU/Linux")" -} - -function test_successful_assert_exit_code() { - function fake_function() { - exit 0 - } - - assert_empty "$(assert_exit_code "0" "$(fake_function)")" -} - -function test_unsuccessful_assert_exit_code() { - function fake_function() { - exit 1 - } - - assert_same "$(bashunit::console_results::print_failed_test "Unsuccessful assert exit code" "1" "to be" "0")" \ - "$(assert_exit_code "0" "$(fake_function)")" -} - -function test_successful_return_assert_exit_code() { - function fake_function() { - return 0 - } - - fake_function - - assert_exit_code "0" -} - -function test_unsuccessful_return_assert_exit_code() { - function fake_function() { - return 1 - } - - assert_exit_code "1" "$(fake_function)" -} - -function test_successful_assert_successful_code() { - function fake_function() { - return 0 - } - - assert_empty "$(assert_successful_code "$(fake_function)")" -} - -function test_unsuccessful_assert_successful_code() { - function fake_function() { - return 2 - } - - assert_same \ - "$(bashunit::console_results::print_failed_test "Unsuccessful assert successful code" "2" "to be exactly" "0")" \ - "$(assert_successful_code "$(fake_function)")" -} - -function test_successful_assert_unsuccessful_code() { - function fake_function() { - return 2 - } - - assert_empty "$(assert_unsuccessful_code "$(fake_function)")" -} - -function test_unsuccessful_assert_unsuccessful_code() { - function fake_function() { - return 0 - } - - local expected - expected="$(bashunit::console_results::print_failed_test \ - "Unsuccessful assert unsuccessful code" "0" "to be non-zero" "but was 0")" - assert_same "$expected" "$(assert_unsuccessful_code "$(fake_function)")" -} - -function test_successful_assert_general_error() { - function fake_function() { - return 1 - } - - assert_empty "$(assert_general_error "$(fake_function)")" -} - -function test_unsuccessful_assert_general_error() { - function fake_function() { - return 2 - } - - assert_same \ - "$(bashunit::console_results::print_failed_test "Unsuccessful assert general error" "2" "to be exactly" "1")" \ - "$(assert_general_error "$(fake_function)")" -} - -function test_successful_assert_command_not_found() { - assert_empty "$(assert_command_not_found "$(a_non_existing_function >/dev/null 2>&1)")" -} - -function test_unsuccessful_assert_command_not_found() { - function fake_function() { - return 0 - } - - assert_same \ - "$(bashunit::console_results::print_failed_test \ - "Unsuccessful assert command not found" "0" "to be exactly" "127")" \ - "$(assert_command_not_found "$(fake_function)")" -} - -function test_successful_assert_exec() { - # shellcheck disable=SC2317 - function fake_command() { - echo "Expected output" - echo "Expected error" >&2 - return 1 - } - - assert_empty "$(assert_exec fake_command --exit 1 --stdout "Expected output" --stderr "Expected error")" -} - -function test_unsuccessful_assert_exec() { - # shellcheck disable=SC2317 - function fake_command() { - echo "out" - echo "err" >&2 - return 0 - } - - local expected="exit: 1"$'\n'"stdout: Expected"$'\n'"stderr: Expected error" - local actual="exit: 0"$'\n'"stdout: out"$'\n'"stderr: err" - - assert_same \ - "$(bashunit::console_results::print_failed_test "Unsuccessful assert exec" "$expected" "but got " "$actual")" \ - "$(assert_exec fake_command --exit 1 --stdout "Expected" --stderr "Expected error")" -} - -function test_successful_assert_array_contains() { - local distros - distros=(Ubuntu 123 Linux\ Mint) - - assert_empty "$(assert_array_contains "123" "${distros[@]}")" -} - -function test_unsuccessful_assert_array_contains() { - local distros - distros=(Ubuntu 123 Linux\ Mint) - - assert_same "$(bashunit::console_results::print_failed_test \ - "Unsuccessful assert array contains" \ - "Ubuntu 123 Linux Mint" \ - "to contain" \ - "non_existing_element")" \ - "$(assert_array_contains "non_existing_element" "${distros[@]}")" -} - -function test_successful_assert_array_not_contains() { - local distros - distros=(Ubuntu 123 Linux\ Mint) - - assert_empty "$(assert_array_not_contains "a_non_existing_element" "${distros[@]}")" -} - -function test_unsuccessful_assert_array_not_contains() { - local distros - distros=(Ubuntu 123 Linux\ Mint) - - assert_same \ - "$(bashunit::console_results::print_failed_test \ - "Unsuccessful assert array not contains" "Ubuntu 123 Linux Mint" "to not contain" "123")" \ - "$(assert_array_not_contains "123" "${distros[@]}")" -} - -function test_successful_assert_string_starts_with() { - assert_empty "$(assert_string_starts_with "ho" "house")" -} - -function test_unsuccessful_assert_string_starts_with() { - assert_same \ - "$(bashunit::console_results::print_failed_test \ - "Unsuccessful assert string starts with" "pause" "to start with" "hou")" \ - "$(assert_string_starts_with "hou" "pause")" -} - -function test_successful_assert_string_not_starts_with() { - assert_empty "$(assert_string_not_starts_with "hou" "pause")" -} - -function test_unsuccessful_assert_string_not_starts_with() { - assert_same \ - "$(bashunit::console_results::print_failed_test \ - "Unsuccessful assert string not starts with" "house" "to not start with" "ho")" \ - "$(assert_string_not_starts_with "ho" "house")" -} - -function test_successful_assert_string_ends_with() { - assert_empty "$(assert_string_ends_with "bar" "foobar")" -} - -function test_unsuccessful_assert_string_ends_with() { - assert_same \ - "$(bashunit::console_results::print_failed_test \ - "Unsuccessful assert string ends with" "foobar" "to end with" "foo")" \ - "$(assert_string_ends_with "foo" "foobar")" -} - -function test_successful_assert_string_not_ends_with() { - assert_empty "$(assert_string_not_ends_with "foo" "foobar")" -} - -function test_unsuccessful_assert_string_not_ends_with() { - assert_same \ - "$(bashunit::console_results::print_failed_test \ - "Unsuccessful assert string not ends with" "foobar" "to not end with" "bar")" \ - "$(assert_string_not_ends_with "bar" "foobar")" -} - -function test_assert_string_start_end_with_special_chars() { - assert_empty "$(assert_string_starts_with "foo." "foo.bar")" - assert_empty "$(assert_string_ends_with ".bar" "foo.bar")" -} - -function test_assert_string_start_end_with_special_chars_fail() { - assert_same \ - "$(bashunit::console_results::print_failed_test \ - "Assert string start end with special chars fail" "fooX" "to start with" "foo.")" \ - "$(assert_string_starts_with "foo." "fooX")" - - assert_same \ - "$(bashunit::console_results::print_failed_test \ - "Assert string start end with special chars fail" "fooX" "to end with" ".bar")" \ - "$(assert_string_ends_with ".bar" "fooX")" -} - -function test_successful_assert_less_than() { - assert_empty "$(assert_less_than "3" "1")" -} - -function test_unsuccessful_assert_less_than() { - assert_same \ - "$(bashunit::console_results::print_failed_test "Unsuccessful assert less than" "3" "to be less than" "1")" \ - "$(assert_less_than "1" "3")" -} - -function test_successful_assert_less_or_equal_than_with_a_smaller_number() { - assert_empty "$(assert_less_or_equal_than "3" "1")" -} - -function test_successful_assert_less_or_equal_than_with_an_equal_number() { - assert_empty "$(assert_less_or_equal_than "3" "3")" -} - -function test_unsuccessful_assert_less_or_equal_than() { - assert_same \ - "$(bashunit::console_results::print_failed_test \ - "Unsuccessful assert less or equal than" "3" "to be less or equal than" "1")" \ - "$(assert_less_or_equal_than "1" "3")" -} - -function test_successful_assert_greater_than() { - assert_empty "$(assert_greater_than "1" "3")" -} - -function test_unsuccessful_assert_greater_than() { - assert_same \ - "$(bashunit::console_results::print_failed_test \ - "Unsuccessful assert greater than" "1" "to be greater than" "3")" \ - "$(assert_greater_than "3" "1")" -} - -function test_successful_assert_greater_or_equal_than_with_a_smaller_number() { - assert_empty "$(assert_greater_or_equal_than "1" "3")" -} - -function test_successful_assert_greater_or_equal_than_with_an_equal_number() { - assert_empty "$(assert_greater_or_equal_than "3" "3")" -} - -function test_unsuccessful_assert_greater_or_equal_than() { - assert_same \ - "$(bashunit::console_results::print_failed_test \ - "Unsuccessful assert greater or equal than" "1" "to be greater or equal than" "3")" \ - "$(assert_greater_or_equal_than "3" "1")" -} - -function test_successful_assert_equals() { - assert_equals "✗ Failed foo" \ - "${_BASHUNIT_COLOR_FAILED}✗ Failed${_BASHUNIT_COLOR_DEFAULT} foo" -} - -function test_successful_assert_equals_with_special_chars() { - local string="${_BASHUNIT_COLOR_FAILED}✗ Failed${_BASHUNIT_COLOR_DEFAULT} foo" - - assert_equals "$string" "$string" -} - -function test_unsuccessful_assert_equals() { - local str1="${_BASHUNIT_COLOR_FAILED}✗ Failed${_BASHUNIT_COLOR_DEFAULT} str1" - local str2="${_BASHUNIT_COLOR_FAILED}✗ Failed${_BASHUNIT_COLOR_DEFAULT} str2" - - assert_same "$(bashunit::console_results::print_failed_test \ - "Unsuccessful assert equals" \ - "✗ Failed str1" \ - "but got " \ - "✗ Failed str2")" \ - "$(assert_equals "$str1" "$str2")" -} - -function test_successful_assert_line_count_empty_str() { - assert_empty "$(assert_line_count 0 "")" -} - -function test_successful_assert_line_count_one_line() { - assert_empty "$(assert_line_count 1 "one line")" -} - -function test_successful_assert_count_multiline() { - local multiline_string="this is line one - this is line two - this is line three" - - assert_empty "$(assert_line_count 3 "$multiline_string")" -} - -function test_successful_assert_line_count_multiline_string_in_one_line() { - assert_empty "$(assert_line_count 4 "one\ntwo\nthree\nfour")" -} - -function test_successful_assert_line_count_multiline_with_new_lines() { - local multiline_str="this \n is \n a multiline \n in one - \n - this is line 7 - this is \n line nine - " - - assert_empty "$(assert_line_count 10 "$multiline_str")" -} - -function test_unsuccessful_assert_line_count() { - assert_same \ - "$(bashunit::console_results::print_failed_test \ - "Unsuccessful assert line count" "one_line_string" "to contain number of lines equal to" "10" "but found" "1")" \ - "$(assert_line_count 10 "one_line_string")" -} - -function test_assert_line_count_does_not_modify_existing_variable() { - local additional_new_lines="original" - assert_empty "$(assert_line_count 1 "one")" - assert_same "original" "$additional_new_lines" -} diff --git a/tests/unit/coverage_core_test.sh b/tests/unit/coverage_core_test.sh new file mode 100644 index 00000000..88c3e80a --- /dev/null +++ b/tests/unit/coverage_core_test.sh @@ -0,0 +1,241 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2317 + +# Save original coverage state to restore after tests +_ORIG_COVERAGE_DATA_FILE="" +_ORIG_COVERAGE_TRACKED_FILES="" +_ORIG_COVERAGE_TRACKED_CACHE_FILE="" +_ORIG_COVERAGE="" +_ORIG_COVERAGE_PATHS="" +_ORIG_COVERAGE_EXCLUDE="" +_ORIG_COVERAGE_REPORT="" +_ORIG_COVERAGE_MIN="" + +function set_up() { + # Save original coverage state + _ORIG_COVERAGE_DATA_FILE="$_BASHUNIT_COVERAGE_DATA_FILE" + _ORIG_COVERAGE_TRACKED_FILES="$_BASHUNIT_COVERAGE_TRACKED_FILES" + _ORIG_COVERAGE_TRACKED_CACHE_FILE="$_BASHUNIT_COVERAGE_TRACKED_CACHE_FILE" + _ORIG_COVERAGE="${BASHUNIT_COVERAGE:-}" + _ORIG_COVERAGE_PATHS="${BASHUNIT_COVERAGE_PATHS:-}" + _ORIG_COVERAGE_EXCLUDE="${BASHUNIT_COVERAGE_EXCLUDE:-}" + _ORIG_COVERAGE_REPORT="${BASHUNIT_COVERAGE_REPORT:-}" + _ORIG_COVERAGE_MIN="${BASHUNIT_COVERAGE_MIN:-}" + + # Reset coverage state for testing + _BASHUNIT_COVERAGE_DATA_FILE="" + _BASHUNIT_COVERAGE_TRACKED_FILES="" + _BASHUNIT_COVERAGE_TRACKED_CACHE_FILE="" + export BASHUNIT_COVERAGE="false" + export BASHUNIT_COVERAGE_PATHS="src/" + export BASHUNIT_COVERAGE_EXCLUDE="tests/*,vendor/*,*_test.sh,*Test.sh" + export BASHUNIT_COVERAGE_REPORT="" + export BASHUNIT_COVERAGE_MIN="" +} + +function tear_down() { + # Clean up any coverage temp files created by tests + if [[ -n "$_BASHUNIT_COVERAGE_DATA_FILE" && "$_BASHUNIT_COVERAGE_DATA_FILE" != "$_ORIG_COVERAGE_DATA_FILE" ]]; then + local coverage_dir + coverage_dir=$(dirname "$_BASHUNIT_COVERAGE_DATA_FILE") + rm -rf "$coverage_dir" 2>/dev/null || true + fi + + # Restore original coverage state + _BASHUNIT_COVERAGE_DATA_FILE="$_ORIG_COVERAGE_DATA_FILE" + _BASHUNIT_COVERAGE_TRACKED_FILES="$_ORIG_COVERAGE_TRACKED_FILES" + _BASHUNIT_COVERAGE_TRACKED_CACHE_FILE="$_ORIG_COVERAGE_TRACKED_CACHE_FILE" + if [[ -n "$_ORIG_COVERAGE" ]]; then + export BASHUNIT_COVERAGE="$_ORIG_COVERAGE" + else + unset BASHUNIT_COVERAGE + fi + if [[ -n "$_ORIG_COVERAGE_PATHS" ]]; then + export BASHUNIT_COVERAGE_PATHS="$_ORIG_COVERAGE_PATHS" + else + unset BASHUNIT_COVERAGE_PATHS + fi + if [[ -n "$_ORIG_COVERAGE_EXCLUDE" ]]; then + export BASHUNIT_COVERAGE_EXCLUDE="$_ORIG_COVERAGE_EXCLUDE" + else + unset BASHUNIT_COVERAGE_EXCLUDE + fi + if [[ -n "$_ORIG_COVERAGE_REPORT" ]]; then + export BASHUNIT_COVERAGE_REPORT="$_ORIG_COVERAGE_REPORT" + else + unset BASHUNIT_COVERAGE_REPORT + fi + if [[ -n "$_ORIG_COVERAGE_MIN" ]]; then + export BASHUNIT_COVERAGE_MIN="$_ORIG_COVERAGE_MIN" + else + unset BASHUNIT_COVERAGE_MIN + fi +} + +function test_coverage_disabled_by_default() { + assert_equals "false" "$BASHUNIT_COVERAGE" +} + +function test_is_coverage_enabled_returns_false_when_disabled() { + BASHUNIT_COVERAGE="false" + # Use subshell to capture exit code without triggering errexit + local result + result=$(bashunit::env::is_coverage_enabled && echo "true" || echo "false") + assert_equals "false" "$result" +} + +function test_is_coverage_enabled_returns_true_when_enabled() { + BASHUNIT_COVERAGE="true" + local result + result=$(bashunit::env::is_coverage_enabled && echo "true" || echo "false") + assert_equals "true" "$result" +} + +function test_coverage_init_creates_temp_files() { + BASHUNIT_COVERAGE="true" + bashunit::coverage::init + + assert_not_empty "$_BASHUNIT_COVERAGE_DATA_FILE" + assert_not_empty "$_BASHUNIT_COVERAGE_TRACKED_FILES" + assert_file_exists "$_BASHUNIT_COVERAGE_DATA_FILE" + assert_file_exists "$_BASHUNIT_COVERAGE_TRACKED_FILES" +} + +function test_coverage_init_does_nothing_when_disabled() { + BASHUNIT_COVERAGE="false" + bashunit::coverage::init + + assert_empty "$_BASHUNIT_COVERAGE_DATA_FILE" +} + +function test_coverage_should_track_excludes_test_files() { + BASHUNIT_COVERAGE="true" + BASHUNIT_COVERAGE_PATHS="" + BASHUNIT_COVERAGE_EXCLUDE="*_test.sh" + bashunit::coverage::init + + # Use subshell to capture exit code without triggering errexit + local result + result=$(bashunit::coverage::should_track '/path/to/my_test.sh' && echo "tracked" || echo "excluded") + assert_equals "excluded" "$result" +} + +function test_coverage_should_track_excludes_vendor() { + BASHUNIT_COVERAGE="true" + BASHUNIT_COVERAGE_PATHS="" + BASHUNIT_COVERAGE_EXCLUDE="vendor/*" + bashunit::coverage::init + + local result + result=$(bashunit::coverage::should_track '/project/vendor/lib.sh' && echo "tracked" || echo "excluded") + assert_equals "excluded" "$result" +} + +function test_coverage_should_track_excludes_bashunit_src() { + BASHUNIT_COVERAGE="true" + bashunit::coverage::init + + local result + result=$(bashunit::coverage::should_track '/path/to/bashunit/src/runner.sh' && echo "tracked" || echo "excluded") + assert_equals "excluded" "$result" +} + +function test_coverage_get_executable_lines_counts_correctly() { + local temp_file + temp_file=$(mktemp) + + cat >"$temp_file" <<'EOF' +#!/usr/bin/env bash + +# This is a comment +function my_func() { + echo "hello" + echo "world" +} + +my_func +EOF + + # Expected executable lines: + # Line 1: shebang (not counted - it's a comment) + # Line 3: comment (not counted) + # Line 4: function declaration (not counted) + # Line 5: echo "hello" (counted) + # Line 6: echo "world" (counted) + # Line 7: } (not counted) + # Line 9: my_func (counted) + # Total: 3 executable lines + + local count + count=$(bashunit::coverage::get_executable_lines "$temp_file") + + assert_equals "3" "$count" + + rm -f "$temp_file" +} + +function test_coverage_record_line_writes_to_file() { + BASHUNIT_COVERAGE="true" + BASHUNIT_COVERAGE_PATHS="/" + BASHUNIT_COVERAGE_EXCLUDE="" + bashunit::coverage::init + + local test_file="/some/path/script.sh" + bashunit::coverage::record_line "$test_file" "10" + bashunit::coverage::record_line "$test_file" "20" + bashunit::coverage::record_line "$test_file" "10" + + # In parallel mode, data is written to a per-process file + local data_file="$_BASHUNIT_COVERAGE_DATA_FILE" + if bashunit::parallel::is_enabled; then + data_file="${_BASHUNIT_COVERAGE_DATA_FILE}.$$" + fi + + local content + content=$(cat "$data_file") + + assert_contains "$test_file:10" "$content" + assert_contains "$test_file:20" "$content" +} + +function test_coverage_cleanup_removes_temp_files() { + BASHUNIT_COVERAGE="true" + bashunit::coverage::init + + local coverage_dir + coverage_dir=$(dirname "$_BASHUNIT_COVERAGE_DATA_FILE") + + assert_directory_exists "$coverage_dir" + + bashunit::coverage::cleanup + + assert_directory_not_exists "$coverage_dir" +} + +function test_coverage_default_paths_is_empty_for_auto_discovery() { + assert_equals "" "$_BASHUNIT_DEFAULT_COVERAGE_PATHS" +} + +function test_coverage_should_track_caches_decisions() { + BASHUNIT_COVERAGE="true" + BASHUNIT_COVERAGE_PATHS="/" + BASHUNIT_COVERAGE_EXCLUDE="" + bashunit::coverage::init + + local test_file="/some/path/script.sh" + + # First call should cache the decision + bashunit::coverage::should_track "$test_file" + + # Verify cache file contains the decision + # In parallel mode, cache is written to per-process file + local cache_file="$_BASHUNIT_COVERAGE_TRACKED_CACHE_FILE" + if bashunit::parallel::is_enabled; then + cache_file="${cache_file}.$$" + fi + + local cache_content + cache_content=$(cat "$cache_file") + + assert_contains "${test_file}:" "$cache_content" +} diff --git a/tests/unit/coverage_executable_test.sh b/tests/unit/coverage_executable_test.sh new file mode 100644 index 00000000..765090b1 --- /dev/null +++ b/tests/unit/coverage_executable_test.sh @@ -0,0 +1,221 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2317 + +# Save original coverage state to restore after tests +_ORIG_COVERAGE_DATA_FILE="" +_ORIG_COVERAGE_TRACKED_FILES="" +_ORIG_COVERAGE_TRACKED_CACHE_FILE="" +_ORIG_COVERAGE="" +_ORIG_COVERAGE_PATHS="" +_ORIG_COVERAGE_EXCLUDE="" +_ORIG_COVERAGE_REPORT="" +_ORIG_COVERAGE_MIN="" + +function set_up() { + # Save original coverage state + _ORIG_COVERAGE_DATA_FILE="$_BASHUNIT_COVERAGE_DATA_FILE" + _ORIG_COVERAGE_TRACKED_FILES="$_BASHUNIT_COVERAGE_TRACKED_FILES" + _ORIG_COVERAGE_TRACKED_CACHE_FILE="$_BASHUNIT_COVERAGE_TRACKED_CACHE_FILE" + _ORIG_COVERAGE="${BASHUNIT_COVERAGE:-}" + _ORIG_COVERAGE_PATHS="${BASHUNIT_COVERAGE_PATHS:-}" + _ORIG_COVERAGE_EXCLUDE="${BASHUNIT_COVERAGE_EXCLUDE:-}" + _ORIG_COVERAGE_REPORT="${BASHUNIT_COVERAGE_REPORT:-}" + _ORIG_COVERAGE_MIN="${BASHUNIT_COVERAGE_MIN:-}" + + # Reset coverage state for testing + _BASHUNIT_COVERAGE_DATA_FILE="" + _BASHUNIT_COVERAGE_TRACKED_FILES="" + _BASHUNIT_COVERAGE_TRACKED_CACHE_FILE="" + export BASHUNIT_COVERAGE="false" + export BASHUNIT_COVERAGE_PATHS="src/" + export BASHUNIT_COVERAGE_EXCLUDE="tests/*,vendor/*,*_test.sh,*Test.sh" + export BASHUNIT_COVERAGE_REPORT="" + export BASHUNIT_COVERAGE_MIN="" +} + +function tear_down() { + # Clean up any coverage temp files created by tests + if [[ -n "$_BASHUNIT_COVERAGE_DATA_FILE" && "$_BASHUNIT_COVERAGE_DATA_FILE" != "$_ORIG_COVERAGE_DATA_FILE" ]]; then + local coverage_dir + coverage_dir=$(dirname "$_BASHUNIT_COVERAGE_DATA_FILE") + rm -rf "$coverage_dir" 2>/dev/null || true + fi + + # Restore original coverage state + _BASHUNIT_COVERAGE_DATA_FILE="$_ORIG_COVERAGE_DATA_FILE" + _BASHUNIT_COVERAGE_TRACKED_FILES="$_ORIG_COVERAGE_TRACKED_FILES" + _BASHUNIT_COVERAGE_TRACKED_CACHE_FILE="$_ORIG_COVERAGE_TRACKED_CACHE_FILE" + if [[ -n "$_ORIG_COVERAGE" ]]; then + export BASHUNIT_COVERAGE="$_ORIG_COVERAGE" + else + unset BASHUNIT_COVERAGE + fi + if [[ -n "$_ORIG_COVERAGE_PATHS" ]]; then + export BASHUNIT_COVERAGE_PATHS="$_ORIG_COVERAGE_PATHS" + else + unset BASHUNIT_COVERAGE_PATHS + fi + if [[ -n "$_ORIG_COVERAGE_EXCLUDE" ]]; then + export BASHUNIT_COVERAGE_EXCLUDE="$_ORIG_COVERAGE_EXCLUDE" + else + unset BASHUNIT_COVERAGE_EXCLUDE + fi + if [[ -n "$_ORIG_COVERAGE_REPORT" ]]; then + export BASHUNIT_COVERAGE_REPORT="$_ORIG_COVERAGE_REPORT" + else + unset BASHUNIT_COVERAGE_REPORT + fi + if [[ -n "$_ORIG_COVERAGE_MIN" ]]; then + export BASHUNIT_COVERAGE_MIN="$_ORIG_COVERAGE_MIN" + else + unset BASHUNIT_COVERAGE_MIN + fi +} + +function test_coverage_default_report_is_lcov() { + assert_equals "coverage/lcov.info" "$_BASHUNIT_DEFAULT_COVERAGE_REPORT" +} + +function test_coverage_default_threshold_low_is_50() { + assert_equals "50" "$_BASHUNIT_DEFAULT_COVERAGE_THRESHOLD_LOW" +} + +function test_coverage_default_threshold_high_is_80() { + assert_equals "80" "$_BASHUNIT_DEFAULT_COVERAGE_THRESHOLD_HIGH" +} + +function test_coverage_default_excludes_test_files() { + assert_contains "*_test.sh" "$_BASHUNIT_DEFAULT_COVERAGE_EXCLUDE" + assert_contains "*Test.sh" "$_BASHUNIT_DEFAULT_COVERAGE_EXCLUDE" +} + +function test_coverage_normalize_path_returns_absolute_path() { + local temp_file + temp_file=$(mktemp) + + local result + result=$(bashunit::coverage::normalize_path "$temp_file") + + # Result should be an absolute path starting with / + assert_matches "^/" "$result" + + # Result should contain the actual temp file name + assert_contains "$(basename "$temp_file")" "$result" + + rm -f "$temp_file" +} + +function test_coverage_is_executable_line_returns_true_for_commands() { + local result + result=$(bashunit::coverage::is_executable_line 'echo "hello"' 2 && echo "yes" || echo "no") + assert_equals "yes" "$result" +} + +function test_coverage_is_executable_line_returns_false_for_comments() { + local result + result=$(bashunit::coverage::is_executable_line '# this is a comment' 2 && echo "yes" || echo "no") + assert_equals "no" "$result" +} + +function test_coverage_is_executable_line_returns_false_for_shebang() { + # Shebang is a comment line, not executable (only runs when script invoked directly) + local result + result=$(bashunit::coverage::is_executable_line '#!/usr/bin/env bash' 1 && echo "yes" || echo "no") + assert_equals "no" "$result" +} + +function test_coverage_is_executable_line_returns_false_for_function_declaration() { + local result + result=$(bashunit::coverage::is_executable_line 'function my_func() {' 2 && echo "yes" || echo "no") + assert_equals "no" "$result" +} + +function test_coverage_is_executable_line_returns_false_for_empty_line() { + local result + result=$(bashunit::coverage::is_executable_line ' ' 2 && echo "yes" || echo "no") + assert_equals "no" "$result" +} + +function test_coverage_is_executable_line_returns_false_for_brace_only() { + local result + result=$(bashunit::coverage::is_executable_line '}' 2 && echo "yes" || echo "no") + assert_equals "no" "$result" +} + +function test_coverage_is_executable_line_returns_false_for_then() { + local result + result=$(bashunit::coverage::is_executable_line ' then' 2 && echo "yes" || echo "no") + assert_equals "no" "$result" +} + +function test_coverage_is_executable_line_returns_false_for_else() { + local result + result=$(bashunit::coverage::is_executable_line ' else' 2 && echo "yes" || echo "no") + assert_equals "no" "$result" +} + +function test_coverage_is_executable_line_returns_false_for_fi() { + local result + result=$(bashunit::coverage::is_executable_line ' fi' 2 && echo "yes" || echo "no") + assert_equals "no" "$result" +} + +function test_coverage_is_executable_line_returns_false_for_do() { + local result + result=$(bashunit::coverage::is_executable_line ' do' 2 && echo "yes" || echo "no") + assert_equals "no" "$result" +} + +function test_coverage_is_executable_line_returns_false_for_done() { + local result + result=$(bashunit::coverage::is_executable_line ' done' 2 && echo "yes" || echo "no") + assert_equals "no" "$result" +} + +function test_coverage_is_executable_line_returns_false_for_esac() { + local result + result=$(bashunit::coverage::is_executable_line ' esac' 2 && echo "yes" || echo "no") + assert_equals "no" "$result" +} + +function test_coverage_is_executable_line_returns_false_for_case_terminator() { + local result + result=$(bashunit::coverage::is_executable_line ' ;;' 2 && echo "yes" || echo "no") + assert_equals "no" "$result" +} + +function test_coverage_is_executable_line_returns_false_for_case_pattern() { + local result + result=$(bashunit::coverage::is_executable_line ' --exit)' 2 && echo "yes" || echo "no") + assert_equals "no" "$result" +} + +function test_coverage_is_executable_line_returns_false_for_wildcard_case() { + local result + result=$(bashunit::coverage::is_executable_line ' *)' 2 && echo "yes" || echo "no") + assert_equals "no" "$result" +} + +function test_coverage_is_executable_line_returns_false_for_case_fallthrough() { + local result + result=$(bashunit::coverage::is_executable_line ' ;&' 2 && echo "yes" || echo "no") + assert_equals "no" "$result" +} + +function test_coverage_is_executable_line_returns_false_for_case_continue() { + local result + result=$(bashunit::coverage::is_executable_line ' ;;&' 2 && echo "yes" || echo "no") + assert_equals "no" "$result" +} + +function test_coverage_is_executable_line_returns_false_for_in_keyword() { + local result + result=$(bashunit::coverage::is_executable_line ' in' 2 && echo "yes" || echo "no") + assert_equals "no" "$result" +} + +function test_coverage_is_executable_line_returns_false_for_standalone_paren() { + local result + result=$(bashunit::coverage::is_executable_line ' )' 2 && echo "yes" || echo "no") + assert_equals "no" "$result" +} diff --git a/tests/unit/coverage_helpers_test.sh b/tests/unit/coverage_helpers_test.sh new file mode 100644 index 00000000..15112d46 --- /dev/null +++ b/tests/unit/coverage_helpers_test.sh @@ -0,0 +1,273 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2317 + +# Save original coverage state to restore after tests +_ORIG_COVERAGE_DATA_FILE="" +_ORIG_COVERAGE_TRACKED_FILES="" +_ORIG_COVERAGE_TRACKED_CACHE_FILE="" +_ORIG_COVERAGE="" +_ORIG_COVERAGE_PATHS="" +_ORIG_COVERAGE_EXCLUDE="" +_ORIG_COVERAGE_REPORT="" +_ORIG_COVERAGE_MIN="" + +function set_up() { + # Save original coverage state + _ORIG_COVERAGE_DATA_FILE="$_BASHUNIT_COVERAGE_DATA_FILE" + _ORIG_COVERAGE_TRACKED_FILES="$_BASHUNIT_COVERAGE_TRACKED_FILES" + _ORIG_COVERAGE_TRACKED_CACHE_FILE="$_BASHUNIT_COVERAGE_TRACKED_CACHE_FILE" + _ORIG_COVERAGE="${BASHUNIT_COVERAGE:-}" + _ORIG_COVERAGE_PATHS="${BASHUNIT_COVERAGE_PATHS:-}" + _ORIG_COVERAGE_EXCLUDE="${BASHUNIT_COVERAGE_EXCLUDE:-}" + _ORIG_COVERAGE_REPORT="${BASHUNIT_COVERAGE_REPORT:-}" + _ORIG_COVERAGE_MIN="${BASHUNIT_COVERAGE_MIN:-}" + + # Reset coverage state for testing + _BASHUNIT_COVERAGE_DATA_FILE="" + _BASHUNIT_COVERAGE_TRACKED_FILES="" + _BASHUNIT_COVERAGE_TRACKED_CACHE_FILE="" + export BASHUNIT_COVERAGE="false" + export BASHUNIT_COVERAGE_PATHS="src/" + export BASHUNIT_COVERAGE_EXCLUDE="tests/*,vendor/*,*_test.sh,*Test.sh" + export BASHUNIT_COVERAGE_REPORT="" + export BASHUNIT_COVERAGE_MIN="" +} + +function tear_down() { + # Clean up any coverage temp files created by tests + if [[ -n "$_BASHUNIT_COVERAGE_DATA_FILE" && "$_BASHUNIT_COVERAGE_DATA_FILE" != "$_ORIG_COVERAGE_DATA_FILE" ]]; then + local coverage_dir + coverage_dir=$(dirname "$_BASHUNIT_COVERAGE_DATA_FILE") + rm -rf "$coverage_dir" 2>/dev/null || true + fi + + # Restore original coverage state + _BASHUNIT_COVERAGE_DATA_FILE="$_ORIG_COVERAGE_DATA_FILE" + _BASHUNIT_COVERAGE_TRACKED_FILES="$_ORIG_COVERAGE_TRACKED_FILES" + _BASHUNIT_COVERAGE_TRACKED_CACHE_FILE="$_ORIG_COVERAGE_TRACKED_CACHE_FILE" + if [[ -n "$_ORIG_COVERAGE" ]]; then + export BASHUNIT_COVERAGE="$_ORIG_COVERAGE" + else + unset BASHUNIT_COVERAGE + fi + if [[ -n "$_ORIG_COVERAGE_PATHS" ]]; then + export BASHUNIT_COVERAGE_PATHS="$_ORIG_COVERAGE_PATHS" + else + unset BASHUNIT_COVERAGE_PATHS + fi + if [[ -n "$_ORIG_COVERAGE_EXCLUDE" ]]; then + export BASHUNIT_COVERAGE_EXCLUDE="$_ORIG_COVERAGE_EXCLUDE" + else + unset BASHUNIT_COVERAGE_EXCLUDE + fi + if [[ -n "$_ORIG_COVERAGE_REPORT" ]]; then + export BASHUNIT_COVERAGE_REPORT="$_ORIG_COVERAGE_REPORT" + else + unset BASHUNIT_COVERAGE_REPORT + fi + if [[ -n "$_ORIG_COVERAGE_MIN" ]]; then + export BASHUNIT_COVERAGE_MIN="$_ORIG_COVERAGE_MIN" + else + unset BASHUNIT_COVERAGE_MIN + fi +} + +# === Coverage class tests === + +function test_coverage_get_coverage_class_returns_high() { + local result + export BASHUNIT_COVERAGE_THRESHOLD_HIGH=80 + export BASHUNIT_COVERAGE_THRESHOLD_LOW=50 + result=$(bashunit::coverage::get_coverage_class 85) + assert_equals "high" "$result" +} + +function test_coverage_get_coverage_class_returns_medium() { + local result + export BASHUNIT_COVERAGE_THRESHOLD_HIGH=80 + export BASHUNIT_COVERAGE_THRESHOLD_LOW=50 + result=$(bashunit::coverage::get_coverage_class 65) + assert_equals "medium" "$result" +} + +function test_coverage_get_coverage_class_returns_low() { + local result + export BASHUNIT_COVERAGE_THRESHOLD_HIGH=80 + export BASHUNIT_COVERAGE_THRESHOLD_LOW=50 + result=$(bashunit::coverage::get_coverage_class 30) + assert_equals "low" "$result" +} + +function test_coverage_get_coverage_class_boundary_high() { + local result + export BASHUNIT_COVERAGE_THRESHOLD_HIGH=80 + export BASHUNIT_COVERAGE_THRESHOLD_LOW=50 + result=$(bashunit::coverage::get_coverage_class 80) + assert_equals "high" "$result" +} + +function test_coverage_get_coverage_class_boundary_low() { + local result + export BASHUNIT_COVERAGE_THRESHOLD_HIGH=80 + export BASHUNIT_COVERAGE_THRESHOLD_LOW=50 + result=$(bashunit::coverage::get_coverage_class 50) + assert_equals "medium" "$result" +} + +# === Percentage calculation tests === + +function test_coverage_calculate_percentage_basic() { + local result + result=$(bashunit::coverage::calculate_percentage 5 10) + assert_equals "50" "$result" +} + +function test_coverage_calculate_percentage_full_coverage() { + local result + result=$(bashunit::coverage::calculate_percentage 100 100) + assert_equals "100" "$result" +} + +function test_coverage_calculate_percentage_zero_hits() { + local result + result=$(bashunit::coverage::calculate_percentage 0 50) + assert_equals "0" "$result" +} + +function test_coverage_calculate_percentage_zero_executable() { + local result + result=$(bashunit::coverage::calculate_percentage 0 0) + assert_equals "0" "$result" +} + +# === HTML escape tests === + +function test_coverage_html_escape_ampersand() { + local result + result=$(bashunit::coverage::html_escape 'foo & bar') + assert_equals 'foo & bar' "$result" +} + +function test_coverage_html_escape_less_than() { + local result + result=$(bashunit::coverage::html_escape 'x < y') + assert_equals 'x < y' "$result" +} + +function test_coverage_html_escape_greater_than() { + local result + result=$(bashunit::coverage::html_escape 'x > y') + assert_equals 'x > y' "$result" +} + +function test_coverage_html_escape_combined() { + local result + # shellcheck disable=SC2016 # Single quotes intentional - testing literal string escaping + result=$(bashunit::coverage::html_escape 'if [[ $a < $b && $c > $d ]]; then') + # shellcheck disable=SC2016 + assert_equals 'if [[ $a < $b && $c > $d ]]; then' "$result" +} + +# === Path to filename tests === + +function test_coverage_path_to_filename_converts_slashes() { + cd /tmp || return + local result + result=$(bashunit::coverage::path_to_filename '/tmp/src/lib/utils.sh') + assert_equals 'src_lib_utils_sh' "$result" +} + +function test_coverage_path_to_filename_handles_dots() { + cd /tmp || return + local result + result=$(bashunit::coverage::path_to_filename '/tmp/test.spec.sh') + assert_equals 'test_spec_sh' "$result" +} + +# === Extract functions tests === + +function test_coverage_extract_functions_finds_basic_function() { + local temp_file + temp_file=$(mktemp) + cat >"$temp_file" <<'EOF' +#!/usr/bin/env bash +function my_func() { + echo "hello" +} +EOF + + local result + result=$(bashunit::coverage::extract_functions "$temp_file") + + assert_contains "my_func" "$result" + + rm -f "$temp_file" +} + +function test_coverage_extract_functions_finds_namespaced_function() { + local temp_file + temp_file=$(mktemp) + cat >"$temp_file" <<'EOF' +#!/usr/bin/env bash +function bashunit::helper::do_thing() { + echo "hello" +} +EOF + + local result + result=$(bashunit::coverage::extract_functions "$temp_file") + + assert_contains "bashunit::helper::do_thing" "$result" + + rm -f "$temp_file" +} + +function test_coverage_extract_functions_finds_multiple_functions() { + local temp_file + temp_file=$(mktemp) + cat >"$temp_file" <<'EOF' +#!/usr/bin/env bash +function func_one() { + echo "one" +} +function func_two() { + echo "two" +} +EOF + + local result + result=$(bashunit::coverage::extract_functions "$temp_file") + + assert_contains "func_one" "$result" + assert_contains "func_two" "$result" + + rm -f "$temp_file" +} + +# === Line hits tests === + +function test_coverage_get_line_hits_returns_zero_when_no_file() { + _BASHUNIT_COVERAGE_DATA_FILE="" + + local result + result=$(bashunit::coverage::get_line_hits "/path/to/file.sh" 10) + + assert_equals "0" "$result" +} + +function test_coverage_get_line_hits_counts_correctly() { + BASHUNIT_COVERAGE="true" + bashunit::coverage::init + + local test_file="/test/script.sh" + { + echo "${test_file}:5" + echo "${test_file}:5" + echo "${test_file}:5" + } >>"$_BASHUNIT_COVERAGE_DATA_FILE" + + local result + result=$(bashunit::coverage::get_line_hits "$test_file" 5) + + assert_equals "3" "$result" +} diff --git a/tests/unit/coverage_reporting_test.sh b/tests/unit/coverage_reporting_test.sh new file mode 100644 index 00000000..d2af35d8 --- /dev/null +++ b/tests/unit/coverage_reporting_test.sh @@ -0,0 +1,218 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2317 + +# Save original coverage state to restore after tests +_ORIG_COVERAGE_DATA_FILE="" +_ORIG_COVERAGE_TRACKED_FILES="" +_ORIG_COVERAGE_TRACKED_CACHE_FILE="" +_ORIG_COVERAGE="" +_ORIG_COVERAGE_PATHS="" +_ORIG_COVERAGE_EXCLUDE="" +_ORIG_COVERAGE_REPORT="" +_ORIG_COVERAGE_MIN="" + +function set_up() { + # Save original coverage state + _ORIG_COVERAGE_DATA_FILE="$_BASHUNIT_COVERAGE_DATA_FILE" + _ORIG_COVERAGE_TRACKED_FILES="$_BASHUNIT_COVERAGE_TRACKED_FILES" + _ORIG_COVERAGE_TRACKED_CACHE_FILE="$_BASHUNIT_COVERAGE_TRACKED_CACHE_FILE" + _ORIG_COVERAGE="${BASHUNIT_COVERAGE:-}" + _ORIG_COVERAGE_PATHS="${BASHUNIT_COVERAGE_PATHS:-}" + _ORIG_COVERAGE_EXCLUDE="${BASHUNIT_COVERAGE_EXCLUDE:-}" + _ORIG_COVERAGE_REPORT="${BASHUNIT_COVERAGE_REPORT:-}" + _ORIG_COVERAGE_MIN="${BASHUNIT_COVERAGE_MIN:-}" + + # Reset coverage state for testing + _BASHUNIT_COVERAGE_DATA_FILE="" + _BASHUNIT_COVERAGE_TRACKED_FILES="" + _BASHUNIT_COVERAGE_TRACKED_CACHE_FILE="" + export BASHUNIT_COVERAGE="false" + export BASHUNIT_COVERAGE_PATHS="src/" + export BASHUNIT_COVERAGE_EXCLUDE="tests/*,vendor/*,*_test.sh,*Test.sh" + export BASHUNIT_COVERAGE_REPORT="" + export BASHUNIT_COVERAGE_MIN="" +} + +function tear_down() { + # Clean up any coverage temp files created by tests + if [[ -n "$_BASHUNIT_COVERAGE_DATA_FILE" && "$_BASHUNIT_COVERAGE_DATA_FILE" != "$_ORIG_COVERAGE_DATA_FILE" ]]; then + local coverage_dir + coverage_dir=$(dirname "$_BASHUNIT_COVERAGE_DATA_FILE") + rm -rf "$coverage_dir" 2>/dev/null || true + fi + + # Restore original coverage state + _BASHUNIT_COVERAGE_DATA_FILE="$_ORIG_COVERAGE_DATA_FILE" + _BASHUNIT_COVERAGE_TRACKED_FILES="$_ORIG_COVERAGE_TRACKED_FILES" + _BASHUNIT_COVERAGE_TRACKED_CACHE_FILE="$_ORIG_COVERAGE_TRACKED_CACHE_FILE" + if [[ -n "$_ORIG_COVERAGE" ]]; then + export BASHUNIT_COVERAGE="$_ORIG_COVERAGE" + else + unset BASHUNIT_COVERAGE + fi + if [[ -n "$_ORIG_COVERAGE_PATHS" ]]; then + export BASHUNIT_COVERAGE_PATHS="$_ORIG_COVERAGE_PATHS" + else + unset BASHUNIT_COVERAGE_PATHS + fi + if [[ -n "$_ORIG_COVERAGE_EXCLUDE" ]]; then + export BASHUNIT_COVERAGE_EXCLUDE="$_ORIG_COVERAGE_EXCLUDE" + else + unset BASHUNIT_COVERAGE_EXCLUDE + fi + if [[ -n "$_ORIG_COVERAGE_REPORT" ]]; then + export BASHUNIT_COVERAGE_REPORT="$_ORIG_COVERAGE_REPORT" + else + unset BASHUNIT_COVERAGE_REPORT + fi + if [[ -n "$_ORIG_COVERAGE_MIN" ]]; then + export BASHUNIT_COVERAGE_MIN="$_ORIG_COVERAGE_MIN" + else + unset BASHUNIT_COVERAGE_MIN + fi +} + +function test_coverage_check_threshold_passes_when_no_minimum() { + BASHUNIT_COVERAGE="true" + BASHUNIT_COVERAGE_MIN="" + bashunit::coverage::init + + assert_successful_code "bashunit::coverage::check_threshold" +} + +function test_coverage_check_threshold_fails_when_below_minimum() { + BASHUNIT_COVERAGE="true" + BASHUNIT_COVERAGE_MIN="80" + bashunit::coverage::init + + # Create a tracked file with some executable lines but no hits + local temp_file + temp_file=$(mktemp) + cat >"$temp_file" <<'EOF' +#!/usr/bin/env bash +echo "line 1" +echo "line 2" +EOF + + echo "$temp_file" >"$_BASHUNIT_COVERAGE_TRACKED_FILES" + + # Capture only the exit code, suppress output + local result + if bashunit::coverage::check_threshold >/dev/null 2>&1; then + result="passed" + else + result="failed" + fi + + assert_equals "failed" "$result" + + rm -f "$temp_file" +} + +function test_coverage_report_lcov_generates_valid_format() { + BASHUNIT_COVERAGE="true" + bashunit::coverage::init + + # Create a test source file + local temp_file + temp_file=$(mktemp) + cat >"$temp_file" <<'EOF' +#!/usr/bin/env bash +echo "line 1" +echo "line 2" +EOF + + echo "$temp_file" >"$_BASHUNIT_COVERAGE_TRACKED_FILES" + + # Simulate some hits + echo "${temp_file}:2" >>"$_BASHUNIT_COVERAGE_DATA_FILE" + + # Generate report to temp file + local report_file + report_file=$(mktemp) + bashunit::coverage::report_lcov "$report_file" + + local content + content=$(cat "$report_file") + + # Line 1 (shebang) is not counted - only lines 2 and 3 are executable + assert_contains "TN:" "$content" + assert_contains "SF:${temp_file}" "$content" + assert_contains "DA:2," "$content" + assert_contains "DA:3," "$content" + assert_contains "LF:2" "$content" + assert_contains "end_of_record" "$content" + + rm -f "$temp_file" "$report_file" +} + +function test_coverage_report_text_shows_no_files_message() { + BASHUNIT_COVERAGE="true" + bashunit::coverage::init + + # Empty tracked files + : >"$_BASHUNIT_COVERAGE_TRACKED_FILES" + + local output + output=$(bashunit::coverage::report_text) + + assert_contains "Total: 0/0 (0%)" "$output" +} + +function test_coverage_get_tracked_files_returns_empty_when_no_file() { + _BASHUNIT_COVERAGE_TRACKED_FILES="" + + local result + result=$(bashunit::coverage::get_tracked_files) + + assert_empty "$result" +} + +function test_coverage_get_tracked_files_returns_sorted_unique() { + BASHUNIT_COVERAGE="true" + bashunit::coverage::init + + { + echo "/path/to/b.sh" + echo "/path/to/a.sh" + echo "/path/to/b.sh" + } >>"$_BASHUNIT_COVERAGE_TRACKED_FILES" + + local result + result=$(bashunit::coverage::get_tracked_files | tr '\n' ' ') + + # Should be sorted and unique + assert_equals "/path/to/a.sh /path/to/b.sh " "$result" +} + +function test_coverage_get_file_stats_returns_formatted_string() { + BASHUNIT_COVERAGE="true" + bashunit::coverage::init + + # Create a test file with known content + local temp_file + temp_file=$(mktemp) + cat >"$temp_file" <<'EOF' +#!/usr/bin/env bash +echo "line 1" +echo "line 2" +EOF + + # No hits recorded, so 0% coverage + local result + result=$(bashunit::coverage::get_file_stats "$temp_file") + + # Format: executable:hit:pct:class + assert_matches "^2:0:0:low$" "$result" + + rm -f "$temp_file" +} + +function test_coverage_get_hit_lines_returns_zero_when_no_data() { + _BASHUNIT_COVERAGE_DATA_FILE="" + + local result + result=$(bashunit::coverage::get_hit_lines "/path/to/file.sh") + + assert_equals "0" "$result" +} diff --git a/tests/unit/coverage_test.sh b/tests/unit/coverage_test.sh deleted file mode 100644 index fdf1208c..00000000 --- a/tests/unit/coverage_test.sh +++ /dev/null @@ -1,724 +0,0 @@ -#!/usr/bin/env bash -# shellcheck disable=SC2317 - -# Save original coverage state to restore after tests -_ORIG_COVERAGE_DATA_FILE="" -_ORIG_COVERAGE_TRACKED_FILES="" -_ORIG_COVERAGE_TRACKED_CACHE_FILE="" -_ORIG_COVERAGE="" -_ORIG_COVERAGE_PATHS="" -_ORIG_COVERAGE_EXCLUDE="" -_ORIG_COVERAGE_REPORT="" -_ORIG_COVERAGE_MIN="" - -function set_up() { - # Save original coverage state - _ORIG_COVERAGE_DATA_FILE="$_BASHUNIT_COVERAGE_DATA_FILE" - _ORIG_COVERAGE_TRACKED_FILES="$_BASHUNIT_COVERAGE_TRACKED_FILES" - _ORIG_COVERAGE_TRACKED_CACHE_FILE="$_BASHUNIT_COVERAGE_TRACKED_CACHE_FILE" - _ORIG_COVERAGE="${BASHUNIT_COVERAGE:-}" - _ORIG_COVERAGE_PATHS="${BASHUNIT_COVERAGE_PATHS:-}" - _ORIG_COVERAGE_EXCLUDE="${BASHUNIT_COVERAGE_EXCLUDE:-}" - _ORIG_COVERAGE_REPORT="${BASHUNIT_COVERAGE_REPORT:-}" - _ORIG_COVERAGE_MIN="${BASHUNIT_COVERAGE_MIN:-}" - - # Reset coverage state for testing - _BASHUNIT_COVERAGE_DATA_FILE="" - _BASHUNIT_COVERAGE_TRACKED_FILES="" - _BASHUNIT_COVERAGE_TRACKED_CACHE_FILE="" - export BASHUNIT_COVERAGE="false" - export BASHUNIT_COVERAGE_PATHS="src/" - export BASHUNIT_COVERAGE_EXCLUDE="tests/*,vendor/*,*_test.sh,*Test.sh" - export BASHUNIT_COVERAGE_REPORT="" - export BASHUNIT_COVERAGE_MIN="" -} - -function tear_down() { - # Clean up any coverage temp files created by tests - if [[ -n "$_BASHUNIT_COVERAGE_DATA_FILE" && "$_BASHUNIT_COVERAGE_DATA_FILE" != "$_ORIG_COVERAGE_DATA_FILE" ]]; then - local coverage_dir - coverage_dir=$(dirname "$_BASHUNIT_COVERAGE_DATA_FILE") - rm -rf "$coverage_dir" 2>/dev/null || true - fi - - # Restore original coverage state - _BASHUNIT_COVERAGE_DATA_FILE="$_ORIG_COVERAGE_DATA_FILE" - _BASHUNIT_COVERAGE_TRACKED_FILES="$_ORIG_COVERAGE_TRACKED_FILES" - _BASHUNIT_COVERAGE_TRACKED_CACHE_FILE="$_ORIG_COVERAGE_TRACKED_CACHE_FILE" - if [[ -n "$_ORIG_COVERAGE" ]]; then - export BASHUNIT_COVERAGE="$_ORIG_COVERAGE" - else - unset BASHUNIT_COVERAGE - fi - if [[ -n "$_ORIG_COVERAGE_PATHS" ]]; then - export BASHUNIT_COVERAGE_PATHS="$_ORIG_COVERAGE_PATHS" - else - unset BASHUNIT_COVERAGE_PATHS - fi - if [[ -n "$_ORIG_COVERAGE_EXCLUDE" ]]; then - export BASHUNIT_COVERAGE_EXCLUDE="$_ORIG_COVERAGE_EXCLUDE" - else - unset BASHUNIT_COVERAGE_EXCLUDE - fi - if [[ -n "$_ORIG_COVERAGE_REPORT" ]]; then - export BASHUNIT_COVERAGE_REPORT="$_ORIG_COVERAGE_REPORT" - else - unset BASHUNIT_COVERAGE_REPORT - fi - if [[ -n "$_ORIG_COVERAGE_MIN" ]]; then - export BASHUNIT_COVERAGE_MIN="$_ORIG_COVERAGE_MIN" - else - unset BASHUNIT_COVERAGE_MIN - fi -} - -function test_coverage_disabled_by_default() { - assert_equals "false" "$BASHUNIT_COVERAGE" -} - -function test_is_coverage_enabled_returns_false_when_disabled() { - BASHUNIT_COVERAGE="false" - # Use subshell to capture exit code without triggering errexit - local result - result=$(bashunit::env::is_coverage_enabled && echo "true" || echo "false") - assert_equals "false" "$result" -} - -function test_is_coverage_enabled_returns_true_when_enabled() { - BASHUNIT_COVERAGE="true" - local result - result=$(bashunit::env::is_coverage_enabled && echo "true" || echo "false") - assert_equals "true" "$result" -} - -function test_coverage_init_creates_temp_files() { - BASHUNIT_COVERAGE="true" - bashunit::coverage::init - - assert_not_empty "$_BASHUNIT_COVERAGE_DATA_FILE" - assert_not_empty "$_BASHUNIT_COVERAGE_TRACKED_FILES" - assert_file_exists "$_BASHUNIT_COVERAGE_DATA_FILE" - assert_file_exists "$_BASHUNIT_COVERAGE_TRACKED_FILES" -} - -function test_coverage_init_does_nothing_when_disabled() { - BASHUNIT_COVERAGE="false" - bashunit::coverage::init - - assert_empty "$_BASHUNIT_COVERAGE_DATA_FILE" -} - -function test_coverage_should_track_excludes_test_files() { - BASHUNIT_COVERAGE="true" - BASHUNIT_COVERAGE_PATHS="" - BASHUNIT_COVERAGE_EXCLUDE="*_test.sh" - bashunit::coverage::init - - # Use subshell to capture exit code without triggering errexit - local result - result=$(bashunit::coverage::should_track '/path/to/my_test.sh' && echo "tracked" || echo "excluded") - assert_equals "excluded" "$result" -} - -function test_coverage_should_track_excludes_vendor() { - BASHUNIT_COVERAGE="true" - BASHUNIT_COVERAGE_PATHS="" - BASHUNIT_COVERAGE_EXCLUDE="vendor/*" - bashunit::coverage::init - - local result - result=$(bashunit::coverage::should_track '/project/vendor/lib.sh' && echo "tracked" || echo "excluded") - assert_equals "excluded" "$result" -} - -function test_coverage_should_track_excludes_bashunit_src() { - BASHUNIT_COVERAGE="true" - bashunit::coverage::init - - local result - result=$(bashunit::coverage::should_track '/path/to/bashunit/src/runner.sh' && echo "tracked" || echo "excluded") - assert_equals "excluded" "$result" -} - -function test_coverage_get_executable_lines_counts_correctly() { - local temp_file - temp_file=$(mktemp) - - cat >"$temp_file" <<'EOF' -#!/usr/bin/env bash - -# This is a comment -function my_func() { - echo "hello" - echo "world" -} - -my_func -EOF - - # Expected executable lines: - # Line 1: shebang (not counted - it's a comment) - # Line 3: comment (not counted) - # Line 4: function declaration (not counted) - # Line 5: echo "hello" (counted) - # Line 6: echo "world" (counted) - # Line 7: } (not counted) - # Line 9: my_func (counted) - # Total: 3 executable lines - - local count - count=$(bashunit::coverage::get_executable_lines "$temp_file") - - assert_equals "3" "$count" - - rm -f "$temp_file" -} - -function test_coverage_record_line_writes_to_file() { - BASHUNIT_COVERAGE="true" - BASHUNIT_COVERAGE_PATHS="/" - BASHUNIT_COVERAGE_EXCLUDE="" - bashunit::coverage::init - - local test_file="/some/path/script.sh" - bashunit::coverage::record_line "$test_file" "10" - bashunit::coverage::record_line "$test_file" "20" - bashunit::coverage::record_line "$test_file" "10" - - # In parallel mode, data is written to a per-process file - local data_file="$_BASHUNIT_COVERAGE_DATA_FILE" - if bashunit::parallel::is_enabled; then - data_file="${_BASHUNIT_COVERAGE_DATA_FILE}.$$" - fi - - local content - content=$(cat "$data_file") - - assert_contains "$test_file:10" "$content" - assert_contains "$test_file:20" "$content" -} - -function test_coverage_check_threshold_passes_when_no_minimum() { - BASHUNIT_COVERAGE="true" - BASHUNIT_COVERAGE_MIN="" - bashunit::coverage::init - - assert_successful_code "bashunit::coverage::check_threshold" -} - -function test_coverage_cleanup_removes_temp_files() { - BASHUNIT_COVERAGE="true" - bashunit::coverage::init - - local coverage_dir - coverage_dir=$(dirname "$_BASHUNIT_COVERAGE_DATA_FILE") - - assert_directory_exists "$coverage_dir" - - bashunit::coverage::cleanup - - assert_directory_not_exists "$coverage_dir" -} - -function test_coverage_default_paths_is_empty_for_auto_discovery() { - assert_equals "" "$_BASHUNIT_DEFAULT_COVERAGE_PATHS" -} - -function test_coverage_default_report_is_lcov() { - assert_equals "coverage/lcov.info" "$_BASHUNIT_DEFAULT_COVERAGE_REPORT" -} - -function test_coverage_default_threshold_low_is_50() { - assert_equals "50" "$_BASHUNIT_DEFAULT_COVERAGE_THRESHOLD_LOW" -} - -function test_coverage_default_threshold_high_is_80() { - assert_equals "80" "$_BASHUNIT_DEFAULT_COVERAGE_THRESHOLD_HIGH" -} - -function test_coverage_is_executable_line_returns_true_for_commands() { - local result - result=$(bashunit::coverage::is_executable_line 'echo "hello"' 2 && echo "yes" || echo "no") - assert_equals "yes" "$result" -} - -function test_coverage_is_executable_line_returns_false_for_comments() { - local result - result=$(bashunit::coverage::is_executable_line '# this is a comment' 2 && echo "yes" || echo "no") - assert_equals "no" "$result" -} - -function test_coverage_is_executable_line_returns_false_for_shebang() { - # Shebang is a comment line, not executable (only runs when script invoked directly) - local result - result=$(bashunit::coverage::is_executable_line '#!/usr/bin/env bash' 1 && echo "yes" || echo "no") - assert_equals "no" "$result" -} - -function test_coverage_is_executable_line_returns_false_for_function_declaration() { - local result - result=$(bashunit::coverage::is_executable_line 'function my_func() {' 2 && echo "yes" || echo "no") - assert_equals "no" "$result" -} - -function test_coverage_is_executable_line_returns_false_for_empty_line() { - local result - result=$(bashunit::coverage::is_executable_line ' ' 2 && echo "yes" || echo "no") - assert_equals "no" "$result" -} - -function test_coverage_is_executable_line_returns_false_for_brace_only() { - local result - result=$(bashunit::coverage::is_executable_line '}' 2 && echo "yes" || echo "no") - assert_equals "no" "$result" -} - -function test_coverage_is_executable_line_returns_false_for_then() { - local result - result=$(bashunit::coverage::is_executable_line ' then' 2 && echo "yes" || echo "no") - assert_equals "no" "$result" -} - -function test_coverage_is_executable_line_returns_false_for_else() { - local result - result=$(bashunit::coverage::is_executable_line ' else' 2 && echo "yes" || echo "no") - assert_equals "no" "$result" -} - -function test_coverage_is_executable_line_returns_false_for_fi() { - local result - result=$(bashunit::coverage::is_executable_line ' fi' 2 && echo "yes" || echo "no") - assert_equals "no" "$result" -} - -function test_coverage_is_executable_line_returns_false_for_do() { - local result - result=$(bashunit::coverage::is_executable_line ' do' 2 && echo "yes" || echo "no") - assert_equals "no" "$result" -} - -function test_coverage_is_executable_line_returns_false_for_done() { - local result - result=$(bashunit::coverage::is_executable_line ' done' 2 && echo "yes" || echo "no") - assert_equals "no" "$result" -} - -function test_coverage_is_executable_line_returns_false_for_esac() { - local result - result=$(bashunit::coverage::is_executable_line ' esac' 2 && echo "yes" || echo "no") - assert_equals "no" "$result" -} - -function test_coverage_is_executable_line_returns_false_for_case_terminator() { - local result - result=$(bashunit::coverage::is_executable_line ' ;;' 2 && echo "yes" || echo "no") - assert_equals "no" "$result" -} - -function test_coverage_is_executable_line_returns_false_for_case_pattern() { - local result - result=$(bashunit::coverage::is_executable_line ' --exit)' 2 && echo "yes" || echo "no") - assert_equals "no" "$result" -} - -function test_coverage_is_executable_line_returns_false_for_wildcard_case() { - local result - result=$(bashunit::coverage::is_executable_line ' *)' 2 && echo "yes" || echo "no") - assert_equals "no" "$result" -} - -function test_coverage_is_executable_line_returns_false_for_case_fallthrough() { - local result - result=$(bashunit::coverage::is_executable_line ' ;&' 2 && echo "yes" || echo "no") - assert_equals "no" "$result" -} - -function test_coverage_is_executable_line_returns_false_for_case_continue() { - local result - result=$(bashunit::coverage::is_executable_line ' ;;&' 2 && echo "yes" || echo "no") - assert_equals "no" "$result" -} - -function test_coverage_is_executable_line_returns_false_for_in_keyword() { - local result - result=$(bashunit::coverage::is_executable_line ' in' 2 && echo "yes" || echo "no") - assert_equals "no" "$result" -} - -function test_coverage_is_executable_line_returns_false_for_standalone_paren() { - local result - result=$(bashunit::coverage::is_executable_line ' )' 2 && echo "yes" || echo "no") - assert_equals "no" "$result" -} - -function test_coverage_check_threshold_fails_when_below_minimum() { - BASHUNIT_COVERAGE="true" - BASHUNIT_COVERAGE_MIN="80" - bashunit::coverage::init - - # Create a tracked file with some executable lines but no hits - local temp_file - temp_file=$(mktemp) - cat >"$temp_file" <<'EOF' -#!/usr/bin/env bash -echo "line 1" -echo "line 2" -EOF - - echo "$temp_file" >"$_BASHUNIT_COVERAGE_TRACKED_FILES" - - # Capture only the exit code, suppress output - local result - if bashunit::coverage::check_threshold >/dev/null 2>&1; then - result="passed" - else - result="failed" - fi - - assert_equals "failed" "$result" - - rm -f "$temp_file" -} - -function test_coverage_report_lcov_generates_valid_format() { - BASHUNIT_COVERAGE="true" - bashunit::coverage::init - - # Create a test source file - local temp_file - temp_file=$(mktemp) - cat >"$temp_file" <<'EOF' -#!/usr/bin/env bash -echo "line 1" -echo "line 2" -EOF - - echo "$temp_file" >"$_BASHUNIT_COVERAGE_TRACKED_FILES" - - # Simulate some hits - echo "${temp_file}:2" >>"$_BASHUNIT_COVERAGE_DATA_FILE" - - # Generate report to temp file - local report_file - report_file=$(mktemp) - bashunit::coverage::report_lcov "$report_file" - - local content - content=$(cat "$report_file") - - # Line 1 (shebang) is not counted - only lines 2 and 3 are executable - assert_contains "TN:" "$content" - assert_contains "SF:${temp_file}" "$content" - assert_contains "DA:2," "$content" - assert_contains "DA:3," "$content" - assert_contains "LF:2" "$content" - assert_contains "end_of_record" "$content" - - rm -f "$temp_file" "$report_file" -} - -function test_coverage_normalize_path_returns_absolute_path() { - local temp_file - temp_file=$(mktemp) - - local result - result=$(bashunit::coverage::normalize_path "$temp_file") - - # Result should be an absolute path starting with / - assert_matches "^/" "$result" - - # Result should contain the actual temp file name - assert_contains "$(basename "$temp_file")" "$result" - - rm -f "$temp_file" -} - -function test_coverage_should_track_caches_decisions() { - BASHUNIT_COVERAGE="true" - BASHUNIT_COVERAGE_PATHS="/" - BASHUNIT_COVERAGE_EXCLUDE="" - bashunit::coverage::init - - local test_file="/some/path/script.sh" - - # First call should cache the decision - bashunit::coverage::should_track "$test_file" - - # Verify cache file contains the decision - # In parallel mode, cache is written to per-process file - local cache_file="$_BASHUNIT_COVERAGE_TRACKED_CACHE_FILE" - if bashunit::parallel::is_enabled; then - cache_file="${cache_file}.$$" - fi - - local cache_content - cache_content=$(cat "$cache_file") - - assert_contains "${test_file}:" "$cache_content" -} - -function test_coverage_default_excludes_test_files() { - assert_contains "*_test.sh" "$_BASHUNIT_DEFAULT_COVERAGE_EXCLUDE" - assert_contains "*Test.sh" "$_BASHUNIT_DEFAULT_COVERAGE_EXCLUDE" -} - -# === Helper function tests === - -function test_coverage_get_coverage_class_returns_high() { - local result - export BASHUNIT_COVERAGE_THRESHOLD_HIGH=80 - export BASHUNIT_COVERAGE_THRESHOLD_LOW=50 - result=$(bashunit::coverage::get_coverage_class 85) - assert_equals "high" "$result" -} - -function test_coverage_get_coverage_class_returns_medium() { - local result - export BASHUNIT_COVERAGE_THRESHOLD_HIGH=80 - export BASHUNIT_COVERAGE_THRESHOLD_LOW=50 - result=$(bashunit::coverage::get_coverage_class 65) - assert_equals "medium" "$result" -} - -function test_coverage_get_coverage_class_returns_low() { - local result - export BASHUNIT_COVERAGE_THRESHOLD_HIGH=80 - export BASHUNIT_COVERAGE_THRESHOLD_LOW=50 - result=$(bashunit::coverage::get_coverage_class 30) - assert_equals "low" "$result" -} - -function test_coverage_get_coverage_class_boundary_high() { - local result - export BASHUNIT_COVERAGE_THRESHOLD_HIGH=80 - export BASHUNIT_COVERAGE_THRESHOLD_LOW=50 - result=$(bashunit::coverage::get_coverage_class 80) - assert_equals "high" "$result" -} - -function test_coverage_get_coverage_class_boundary_low() { - local result - export BASHUNIT_COVERAGE_THRESHOLD_HIGH=80 - export BASHUNIT_COVERAGE_THRESHOLD_LOW=50 - result=$(bashunit::coverage::get_coverage_class 50) - assert_equals "medium" "$result" -} - -function test_coverage_calculate_percentage_basic() { - local result - result=$(bashunit::coverage::calculate_percentage 5 10) - assert_equals "50" "$result" -} - -function test_coverage_calculate_percentage_full_coverage() { - local result - result=$(bashunit::coverage::calculate_percentage 100 100) - assert_equals "100" "$result" -} - -function test_coverage_calculate_percentage_zero_hits() { - local result - result=$(bashunit::coverage::calculate_percentage 0 50) - assert_equals "0" "$result" -} - -function test_coverage_calculate_percentage_zero_executable() { - local result - result=$(bashunit::coverage::calculate_percentage 0 0) - assert_equals "0" "$result" -} - -function test_coverage_html_escape_ampersand() { - local result - result=$(bashunit::coverage::html_escape 'foo & bar') - assert_equals 'foo & bar' "$result" -} - -function test_coverage_html_escape_less_than() { - local result - result=$(bashunit::coverage::html_escape 'x < y') - assert_equals 'x < y' "$result" -} - -function test_coverage_html_escape_greater_than() { - local result - result=$(bashunit::coverage::html_escape 'x > y') - assert_equals 'x > y' "$result" -} - -function test_coverage_html_escape_combined() { - local result - # shellcheck disable=SC2016 # Single quotes intentional - testing literal string escaping - result=$(bashunit::coverage::html_escape 'if [[ $a < $b && $c > $d ]]; then') - # shellcheck disable=SC2016 - assert_equals 'if [[ $a < $b && $c > $d ]]; then' "$result" -} - -function test_coverage_path_to_filename_converts_slashes() { - cd /tmp || return - local result - result=$(bashunit::coverage::path_to_filename '/tmp/src/lib/utils.sh') - assert_equals 'src_lib_utils_sh' "$result" -} - -function test_coverage_path_to_filename_handles_dots() { - cd /tmp || return - local result - result=$(bashunit::coverage::path_to_filename '/tmp/test.spec.sh') - assert_equals 'test_spec_sh' "$result" -} - -function test_coverage_get_tracked_files_returns_empty_when_no_file() { - _BASHUNIT_COVERAGE_TRACKED_FILES="" - - local result - result=$(bashunit::coverage::get_tracked_files) - - assert_empty "$result" -} - -function test_coverage_get_tracked_files_returns_sorted_unique() { - BASHUNIT_COVERAGE="true" - bashunit::coverage::init - - { - echo "/path/to/b.sh" - echo "/path/to/a.sh" - echo "/path/to/b.sh" - } >>"$_BASHUNIT_COVERAGE_TRACKED_FILES" - - local result - result=$(bashunit::coverage::get_tracked_files | tr '\n' ' ') - - # Should be sorted and unique - assert_equals "/path/to/a.sh /path/to/b.sh " "$result" -} - -function test_coverage_get_file_stats_returns_formatted_string() { - BASHUNIT_COVERAGE="true" - bashunit::coverage::init - - # Create a test file with known content - local temp_file - temp_file=$(mktemp) - cat >"$temp_file" <<'EOF' -#!/usr/bin/env bash -echo "line 1" -echo "line 2" -EOF - - # No hits recorded, so 0% coverage - local result - result=$(bashunit::coverage::get_file_stats "$temp_file") - - # Format: executable:hit:pct:class - assert_matches "^2:0:0:low$" "$result" - - rm -f "$temp_file" -} - -function test_coverage_extract_functions_finds_basic_function() { - local temp_file - temp_file=$(mktemp) - cat >"$temp_file" <<'EOF' -#!/usr/bin/env bash -function my_func() { - echo "hello" -} -EOF - - local result - result=$(bashunit::coverage::extract_functions "$temp_file") - - assert_contains "my_func" "$result" - - rm -f "$temp_file" -} - -function test_coverage_extract_functions_finds_namespaced_function() { - local temp_file - temp_file=$(mktemp) - cat >"$temp_file" <<'EOF' -#!/usr/bin/env bash -function bashunit::helper::do_thing() { - echo "hello" -} -EOF - - local result - result=$(bashunit::coverage::extract_functions "$temp_file") - - assert_contains "bashunit::helper::do_thing" "$result" - - rm -f "$temp_file" -} - -function test_coverage_extract_functions_finds_multiple_functions() { - local temp_file - temp_file=$(mktemp) - cat >"$temp_file" <<'EOF' -#!/usr/bin/env bash -function func_one() { - echo "one" -} -function func_two() { - echo "two" -} -EOF - - local result - result=$(bashunit::coverage::extract_functions "$temp_file") - - assert_contains "func_one" "$result" - assert_contains "func_two" "$result" - - rm -f "$temp_file" -} - -function test_coverage_get_line_hits_returns_zero_when_no_file() { - _BASHUNIT_COVERAGE_DATA_FILE="" - - local result - result=$(bashunit::coverage::get_line_hits "/path/to/file.sh" 10) - - assert_equals "0" "$result" -} - -function test_coverage_get_line_hits_counts_correctly() { - BASHUNIT_COVERAGE="true" - bashunit::coverage::init - - local test_file="/test/script.sh" - { - echo "${test_file}:5" - echo "${test_file}:5" - echo "${test_file}:5" - } >>"$_BASHUNIT_COVERAGE_DATA_FILE" - - local result - result=$(bashunit::coverage::get_line_hits "$test_file" 5) - - assert_equals "3" "$result" -} - -function test_coverage_get_hit_lines_returns_zero_when_no_data() { - _BASHUNIT_COVERAGE_DATA_FILE="" - - local result - result=$(bashunit::coverage::get_hit_lines "/path/to/file.sh") - - assert_equals "0" "$result" -} - -function test_coverage_report_text_shows_no_files_message() { - BASHUNIT_COVERAGE="true" - bashunit::coverage::init - - # Empty tracked files - : >"$_BASHUNIT_COVERAGE_TRACKED_FILES" - - local output - output=$(bashunit::coverage::report_text) - - assert_contains "Total: 0/0 (0%)" "$output" -} diff --git a/tests/unit/parallel_test.sh b/tests/unit/parallel_test.sh index 12905a5c..9bf9610e 100644 --- a/tests/unit/parallel_test.sh +++ b/tests/unit/parallel_test.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# bashunit: no-parallel-tests # shellcheck disable=SC2034 @@ -27,7 +28,9 @@ function tear_down() { export BASHUNIT_PARALLEL_RUN=$original_parallel_run # Clean up test temp directory - [[ -d "$_TEST_TEMP_DIR" ]] && rm -rf "$_TEST_TEMP_DIR" + if [ -d "$_TEST_TEMP_DIR" ]; then + rm -rf "$_TEST_TEMP_DIR" + fi # Restore original paths export TEMP_DIR_PARALLEL_TEST_SUITE="$_ORIGINAL_TEMP_DIR_PARALLEL" @@ -204,11 +207,14 @@ function test_aggregate_sums_multiple_result_files() { _create_result_file "$TEMP_DIR_PARALLEL_TEST_SUITE/script1" "test2.result" \ "##ASSERTIONS_PASSED=3##ASSERTIONS_FAILED=2##TEST_EXIT_CODE=0##" - local passed failed - read -r passed failed < <( + local result passed failed + result=$( bashunit::parallel::aggregate_test_results "$TEMP_DIR_PARALLEL_TEST_SUITE" >/dev/null echo "$_BASHUNIT_ASSERTIONS_PASSED $_BASHUNIT_ASSERTIONS_FAILED" ) + IFS=' ' read -r passed failed </dev/null || exit 0 +fi + +RELEASE_SCRIPT_DIR="" +FIXTURES_DIR="" + +function set_up_before_script() { + RELEASE_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + FIXTURES_DIR="$(dirname "${BASH_SOURCE[0]}")/fixtures/release" + + # Source release.sh to get access to functions + # shellcheck source=/dev/null + source "$RELEASE_SCRIPT_DIR/release.sh" +} + +########################## +# release::get_checksum tests +########################## + +function test_get_checksum_returns_checksum_when_file_exists() { + # Create a temp checksum file + local temp_dir + temp_dir=$(mktemp -d) + echo "abc123def456 bin/bashunit" >"$temp_dir/checksum" + + # Override the function to use temp dir + local result + result=$(cd "$temp_dir" && awk '{print $1}' checksum) + + assert_same "abc123def456" "$result" + rm -rf "$temp_dir" +} + +function test_get_checksum_returns_empty_when_file_missing() { + local temp_dir + temp_dir=$(mktemp -d) + + local result + result=$( + cd "$temp_dir" || return + if [[ -f "checksum" ]]; then + awk '{print $1}' checksum + else + echo "" + fi + ) + + assert_empty "$result" + rm -rf "$temp_dir" +} + +########################## +# release::get_contributors tests +########################## + +function test_get_contributors_returns_handles_when_mocked() { + # Mock gh command to return test data + bashunit::mock gh echo -e "User1\nUser2\nUser1" + + local result + result=$(release::get_contributors "0.28.0") + + # Should return unique sorted handles + assert_contains "User1" "$result" + assert_contains "User2" "$result" +} + +function test_get_contributors_returns_empty_on_failure() { + # Mock gh to fail + bashunit::mock gh false + + local result + result=$(release::get_contributors "0.28.0") + + assert_empty "$result" +} + +########################## +# release::generate_release_notes tests (using fixtures) +########################## + +function test_generate_release_notes_transforms_added_section() { + # Mock dependencies + bashunit::mock gh echo "TestUser" + + # Use fixture changelog + local result + result=$(cd "$FIXTURES_DIR" && release::generate_release_notes "0.30.0" "0.29.0" "abc123") + + assert_contains "## ✨ Improvements" "$result" +} + +function test_generate_release_notes_transforms_changed_section() { + bashunit::mock gh echo "TestUser" + + local result + result=$(cd "$FIXTURES_DIR" && release::generate_release_notes "0.30.0" "0.29.0" "abc123") + + assert_contains "## 🛠️ Changes" "$result" +} + +function test_generate_release_notes_transforms_fixed_section() { + bashunit::mock gh echo "TestUser" + + local result + result=$(cd "$FIXTURES_DIR" && release::generate_release_notes "0.30.0" "0.29.0" "abc123") + + assert_contains "## 🐛 Bug Fixes" "$result" +} + +function test_generate_release_notes_includes_checksum() { + bashunit::mock gh echo "TestUser" + + local result + result=$(cd "$FIXTURES_DIR" && release::generate_release_notes "0.30.0" "0.29.0" "abc123checksum") + + assert_contains "## Checksum" "$result" + assert_contains "abc123checksum" "$result" +} + +function test_generate_release_notes_includes_changelog_link() { + bashunit::mock gh echo "TestUser" + + local result + result=$(cd "$FIXTURES_DIR" && release::generate_release_notes "0.30.0" "0.29.0" "abc123") + + assert_contains "**Full Changelog:**" "$result" + assert_contains "0.29.0...0.30.0" "$result" +} + +function test_generate_release_notes_includes_contributors() { + bashunit::mock gh echo "Contributor1" + + local result + result=$(cd "$FIXTURES_DIR" && release::generate_release_notes "0.30.0" "0.29.0" "abc123") + + assert_contains "## 👥 Contributors" "$result" + assert_contains "@Contributor1" "$result" +} + +function test_generate_release_notes_extracts_from_first_version_header() { + bashunit::mock gh echo "TestUser" + + local result + result=$(cd "$FIXTURES_DIR" && release::generate_release_notes "0.30.0" "0.29.0" "abc123") + + # Should include content from first version header (0.30.0) + assert_contains "New feature one" "$result" + assert_contains "Changed behavior" "$result" + assert_contains "Bug fix one" "$result" +} + +function test_generate_release_notes_excludes_older_version_content() { + bashunit::mock gh echo "TestUser" + + local result + result=$(cd "$FIXTURES_DIR" && release::generate_release_notes "0.30.0" "0.29.0" "abc123") + + # Should NOT include content from older versions (0.29.0) + assert_not_contains "Previous feature" "$result" +} diff --git a/tests/unit/release_sandbox_test.sh b/tests/unit/release_sandbox_test.sh new file mode 100644 index 00000000..894e1566 --- /dev/null +++ b/tests/unit/release_sandbox_test.sh @@ -0,0 +1,192 @@ +#!/usr/bin/env bash + +# release.sh requires Bash 3.1+ (uses += array syntax) +# Skip this entire test file on Bash 3.0 +if [[ "${BASH_VERSINFO[0]}" -eq 3 ]] && [[ "${BASH_VERSINFO[1]}" -lt 1 ]]; then + # shellcheck disable=SC2317 + return 0 2>/dev/null || exit 0 +fi + +RELEASE_SCRIPT_DIR="" + +function set_up_before_script() { + RELEASE_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + + # Source release.sh to get access to functions + # shellcheck source=/dev/null + source "$RELEASE_SCRIPT_DIR/release.sh" +} + +########################## +# Sandbox mode tests +########################## + +function test_sandbox_create_creates_temp_directory() { + release::sandbox::create 2>/dev/null + assert_not_empty "$SANDBOX_DIR" + assert_directory_exists "$SANDBOX_DIR" + rm -rf "$SANDBOX_DIR" +} + +function test_sandbox_create_copies_project_files() { + local original_dir + original_dir=$(pwd) + + cd "$RELEASE_SCRIPT_DIR" || return + release::sandbox::create 2>/dev/null + + # Check that key files were copied + assert_file_exists "$SANDBOX_DIR/bashunit" + assert_file_exists "$SANDBOX_DIR/build.sh" + assert_file_exists "$SANDBOX_DIR/CHANGELOG.md" + + rm -rf "$SANDBOX_DIR" + cd "$original_dir" || return +} + +function test_sandbox_create_excludes_git_directory() { + local original_dir + original_dir=$(pwd) + + cd "$RELEASE_SCRIPT_DIR" || return + release::sandbox::create 2>/dev/null + + # .git should NOT be copied + assert_directory_not_exists "$SANDBOX_DIR/.git" + + rm -rf "$SANDBOX_DIR" + cd "$original_dir" || return +} + +function test_sandbox_create_excludes_release_state() { + local original_dir + original_dir=$(pwd) + + cd "$RELEASE_SCRIPT_DIR" || return + + # Create a .release-state directory to test exclusion + mkdir -p .release-state/test + release::sandbox::create 2>/dev/null + + # .release-state should NOT be copied + assert_directory_not_exists "$SANDBOX_DIR/.release-state" + + rm -rf "$SANDBOX_DIR" .release-state + cd "$original_dir" || return +} + +function test_sandbox_setup_git_initializes_repo() { + if ! command -v git >/dev/null 2>&1; then + bashunit::skip "git not available" && return + fi + + local original_dir + original_dir=$(pwd) + + cd "$RELEASE_SCRIPT_DIR" || return + release::sandbox::create 2>/dev/null + release::sandbox::setup_git 2>/dev/null + + # Should have a .git directory now + assert_directory_exists "$SANDBOX_DIR/.git" + + rm -rf "$SANDBOX_DIR" + cd "$original_dir" || return +} + +function test_sandbox_setup_git_creates_initial_commit() { + if ! command -v git >/dev/null 2>&1; then + bashunit::skip "git not available" && return + fi + + local original_dir + original_dir=$(pwd) + + cd "$RELEASE_SCRIPT_DIR" || return + release::sandbox::create 2>/dev/null + release::sandbox::setup_git 2>/dev/null + + # Should have at least one commit + local commit_count + commit_count=$(git -C "$SANDBOX_DIR" rev-list --count HEAD 2>/dev/null || echo "0") + assert_equals "1" "$commit_count" + + rm -rf "$SANDBOX_DIR" + cd "$original_dir" || return +} + +function test_sandbox_mock_gh_handles_release_command() { + release::sandbox::mock_gh 2>/dev/null + + local result + result=$(gh release create "1.0.0" 2>&1) + assert_contains "SANDBOX" "$result" + + # Unset the mock + unset -f gh +} + +function test_sandbox_mock_gh_handles_api_command() { + release::sandbox::mock_gh 2>/dev/null + + local result + result=$(gh api /repos/test 2>&1) + + # Should return empty (not fail) + assert_successful_code + + unset -f gh +} + +function test_sandbox_mock_gh_handles_auth_command() { + release::sandbox::mock_gh 2>/dev/null + + # Should succeed (return 0) + gh auth status 2>/dev/null + assert_successful_code + + unset -f gh +} + +function test_sandbox_mock_git_push_prevents_actual_push() { + if ! command -v git >/dev/null 2>&1; then + bashunit::skip "git not available" && return + fi + + release::sandbox::mock_git_push 2>/dev/null + + local result + result=$(git push origin main 2>&1) + assert_contains "SANDBOX" "$result" + + # Unset the mock + unset -f git +} + +function test_sandbox_mock_git_push_allows_other_git_commands() { + if ! command -v git >/dev/null 2>&1; then + bashunit::skip "git not available" && return + fi + + local temp_dir + temp_dir=$(mktemp -d) + + ( + cd "$temp_dir" || return + git init --quiet + git config user.email "test@test.com" + git config user.name "Test" + echo "test" >file.txt + git add file.txt + + release::sandbox::mock_git_push 2>/dev/null + + # Non-push commands should work + git commit -m "test" --quiet + git status --short + ) 2>/dev/null + + assert_successful_code + rm -rf "$temp_dir" + unset -f git 2>/dev/null || true +} diff --git a/tests/unit/release_test.sh b/tests/unit/release_test.sh deleted file mode 100644 index 9e7dab09..00000000 --- a/tests/unit/release_test.sh +++ /dev/null @@ -1,961 +0,0 @@ -#!/usr/bin/env bash - -# release.sh requires Bash 3.1+ (uses += array syntax) -# Skip this entire test file on Bash 3.0 -if [[ "${BASH_VERSINFO[0]}" -eq 3 ]] && [[ "${BASH_VERSINFO[1]}" -lt 1 ]]; then - # shellcheck disable=SC2317 - return 0 2>/dev/null || exit 0 -fi - -RELEASE_SCRIPT_DIR="" -FIXTURES_DIR="" - -function set_up_before_script() { - RELEASE_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" - FIXTURES_DIR="$(dirname "${BASH_SOURCE[0]}")/fixtures/release" - - # Source release.sh to get access to functions - # shellcheck source=/dev/null - source "$RELEASE_SCRIPT_DIR/release.sh" -} - -########################## -# release::validate_semver tests -########################## - -function test_validate_semver_accepts_valid_version() { - # Should not exit (no output means success) - local result - result=$(release::validate_semver "0.30.0" 2>&1) || true - assert_empty "$result" -} - -function test_validate_semver_accepts_major_version() { - local result - result=$(release::validate_semver "1.0.0" 2>&1) || true - assert_empty "$result" -} - -function test_validate_semver_accepts_large_numbers() { - local result - result=$(release::validate_semver "10.20.30" 2>&1) || true - assert_empty "$result" -} - -function test_validate_semver_rejects_two_part_version() { - local result - result=$(release::validate_semver "0.30" 2>&1) || true - assert_contains "Invalid version format" "$result" -} - -function test_validate_semver_rejects_v_prefix() { - local result - result=$(release::validate_semver "v0.30.0" 2>&1) || true - assert_contains "Invalid version format" "$result" -} - -function test_validate_semver_rejects_prerelease_suffix() { - local result - result=$(release::validate_semver "0.30.0-beta" 2>&1) || true - assert_contains "Invalid version format" "$result" -} - -function test_validate_semver_rejects_empty_string() { - local result - result=$(release::validate_semver "" 2>&1) || true - assert_contains "Invalid version format" "$result" -} - -function test_validate_semver_rejects_text() { - local result - result=$(release::validate_semver "latest" 2>&1) || true - assert_contains "Invalid version format" "$result" -} - -########################## -# release::version_gt tests -########################## - -function test_version_gt_returns_true_when_patch_greater() { - release::version_gt "0.29.1" "0.29.0" - assert_successful_code -} - -function test_version_gt_returns_true_when_minor_greater() { - release::version_gt "0.30.0" "0.29.0" - assert_successful_code -} - -function test_version_gt_returns_true_when_major_greater() { - release::version_gt "1.0.0" "0.99.99" - assert_successful_code -} - -function test_version_gt_returns_false_when_equal() { - assert_unsuccessful_code "$(release::version_gt "0.29.0" "0.29.0")" -} - -function test_version_gt_returns_false_when_less() { - assert_unsuccessful_code "$(release::version_gt "0.28.0" "0.29.0")" -} - -function test_version_gt_returns_false_when_patch_less() { - assert_unsuccessful_code "$(release::version_gt "0.29.0" "0.29.1")" -} - -function test_version_gt_handles_large_numbers() { - release::version_gt "10.20.31" "10.20.30" - assert_successful_code -} - -# Data provider for version comparison -# @data_provider provider_release::version_gt_true -function test_version_gt_with_provider_returns_true() { - local v1="$1" - local v2="$2" - release::version_gt "$v1" "$v2" - assert_successful_code -} - -function provider_release::version_gt_true() { - bashunit::data_set "0.30.0" "0.29.0" - bashunit::data_set "1.0.0" "0.99.99" - bashunit::data_set "0.29.1" "0.29.0" - bashunit::data_set "2.0.0" "1.99.99" -} - -# @data_provider provider_release::version_gt_false -function test_version_gt_with_provider_returns_false() { - local v1="$1" - local v2="$2" - assert_unsuccessful_code "$(release::version_gt "$v1" "$v2")" -} - -function provider_release::version_gt_false() { - bashunit::data_set "0.29.0" "0.29.0" - bashunit::data_set "0.28.0" "0.29.0" - bashunit::data_set "0.29.0" "0.30.0" - bashunit::data_set "0.99.99" "1.0.0" -} - -########################## -# release::get_checksum tests -########################## - -function test_get_checksum_returns_checksum_when_file_exists() { - # Create a temp checksum file - local temp_dir - temp_dir=$(mktemp -d) - echo "abc123def456 bin/bashunit" >"$temp_dir/checksum" - - # Override the function to use temp dir - local result - result=$(cd "$temp_dir" && awk '{print $1}' checksum) - - assert_same "abc123def456" "$result" - rm -rf "$temp_dir" -} - -function test_get_checksum_returns_empty_when_file_missing() { - local temp_dir - temp_dir=$(mktemp -d) - - local result - result=$( - cd "$temp_dir" || return - if [[ -f "checksum" ]]; then - awk '{print $1}' checksum - else - echo "" - fi - ) - - assert_empty "$result" - rm -rf "$temp_dir" -} - -########################## -# release::get_contributors tests -########################## - -function test_get_contributors_returns_handles_when_mocked() { - # Mock gh command to return test data - bashunit::mock gh echo -e "User1\nUser2\nUser1" - - local result - result=$(release::get_contributors "0.28.0") - - # Should return unique sorted handles - assert_contains "User1" "$result" - assert_contains "User2" "$result" -} - -function test_get_contributors_returns_empty_on_failure() { - # Mock gh to fail - bashunit::mock gh false - - local result - result=$(release::get_contributors "0.28.0") - - assert_empty "$result" -} - -########################## -# release::generate_release_notes tests (using fixtures) -########################## - -function test_generate_release_notes_transforms_added_section() { - # Mock dependencies - bashunit::mock gh echo "TestUser" - - # Use fixture changelog - local result - result=$(cd "$FIXTURES_DIR" && release::generate_release_notes "0.30.0" "0.29.0" "abc123") - - assert_contains "## ✨ Improvements" "$result" -} - -function test_generate_release_notes_transforms_changed_section() { - bashunit::mock gh echo "TestUser" - - local result - result=$(cd "$FIXTURES_DIR" && release::generate_release_notes "0.30.0" "0.29.0" "abc123") - - assert_contains "## 🛠️ Changes" "$result" -} - -function test_generate_release_notes_transforms_fixed_section() { - bashunit::mock gh echo "TestUser" - - local result - result=$(cd "$FIXTURES_DIR" && release::generate_release_notes "0.30.0" "0.29.0" "abc123") - - assert_contains "## 🐛 Bug Fixes" "$result" -} - -function test_generate_release_notes_includes_checksum() { - bashunit::mock gh echo "TestUser" - - local result - result=$(cd "$FIXTURES_DIR" && release::generate_release_notes "0.30.0" "0.29.0" "abc123checksum") - - assert_contains "## Checksum" "$result" - assert_contains "abc123checksum" "$result" -} - -function test_generate_release_notes_includes_changelog_link() { - bashunit::mock gh echo "TestUser" - - local result - result=$(cd "$FIXTURES_DIR" && release::generate_release_notes "0.30.0" "0.29.0" "abc123") - - assert_contains "**Full Changelog:**" "$result" - assert_contains "0.29.0...0.30.0" "$result" -} - -function test_generate_release_notes_includes_contributors() { - bashunit::mock gh echo "Contributor1" - - local result - result=$(cd "$FIXTURES_DIR" && release::generate_release_notes "0.30.0" "0.29.0" "abc123") - - assert_contains "## 👥 Contributors" "$result" - assert_contains "@Contributor1" "$result" -} - -function test_generate_release_notes_extracts_from_first_version_header() { - bashunit::mock gh echo "TestUser" - - local result - result=$(cd "$FIXTURES_DIR" && release::generate_release_notes "0.30.0" "0.29.0" "abc123") - - # Should include content from first version header (0.30.0) - assert_contains "New feature one" "$result" - assert_contains "Changed behavior" "$result" - assert_contains "Bug fix one" "$result" -} - -function test_generate_release_notes_excludes_older_version_content() { - bashunit::mock gh echo "TestUser" - - local result - result=$(cd "$FIXTURES_DIR" && release::generate_release_notes "0.30.0" "0.29.0" "abc123") - - # Should NOT include content from older versions (0.29.0) - assert_not_contains "Previous feature" "$result" -} - -########################## -# Pre-flight check tests -########################## - -function test_preflight_check_required_files_passes_when_all_exist() { - # Run in the project root where all files exist - local result - result=$(cd "$RELEASE_SCRIPT_DIR" && release::preflight::check_required_files 2>&1) - assert_successful_code -} - -function test_preflight_check_required_files_fails_when_file_missing() { - local temp_dir - temp_dir=$(mktemp -d) - - local result - result=$(cd "$temp_dir" && release::preflight::check_required_files 2>&1) || true - - assert_contains "Required files missing" "$result" - rm -rf "$temp_dir" -} - -function test_preflight_check_changelog_unreleased_passes_with_content() { - local result - result=$(cd "$FIXTURES_DIR" && release::preflight::check_changelog_unreleased 2>&1) - assert_successful_code -} - -function test_preflight_check_changelog_unreleased_fails_when_missing() { - local temp_dir - temp_dir=$(mktemp -d) - echo "# Changelog" >"$temp_dir/CHANGELOG.md" - - local result - result=$(cd "$temp_dir" && release::preflight::check_changelog_unreleased 2>&1) || true - - assert_contains "missing '## Unreleased' section" "$result" - rm -rf "$temp_dir" -} - -########################## -# Backup and rollback tests -########################## - -function test_backup_init_creates_directory() { - local temp_dir - temp_dir=$(mktemp -d) - - ( - cd "$temp_dir" || return - release::backup::init - [[ -d "$BACKUP_DIR" ]] && echo "exists" - ) >/tmp/backup_test_result 2>&1 - - assert_contains "exists" "$(cat /tmp/backup_test_result)" || true - rm -rf "$temp_dir" /tmp/backup_test_result - assert_successful_code -} - -function test_backup_save_file_copies_file() { - local temp_dir - temp_dir=$(mktemp -d) - - local result - result=$( - cd "$temp_dir" || return - echo "test content" >testfile.txt - release::backup::init - release::backup::save_file "testfile.txt" - cat "$BACKUP_DIR/testfile.txt" - ) - - assert_same "test content" "$result" - rm -rf "$temp_dir" -} - -function test_rollback_restore_files_restores_backup() { - local temp_dir - temp_dir=$(mktemp -d) - - local result - result=$( - cd "$temp_dir" || return - echo "original content" >testfile.txt - release::backup::init - release::backup::save_file "testfile.txt" - echo "modified content" >testfile.txt - release::rollback::restore_files 2>/dev/null - cat testfile.txt - ) - - assert_same "original content" "$result" - rm -rf "$temp_dir" -} - -########################## -# Force mode tests -########################## - -function test_confirm_action_auto_confirms_in_force_mode() { - FORCE_MODE=true - local result - result=$(release::confirm_action "Test prompt" 2>&1) - local exit_code=$? - FORCE_MODE=false - assert_same 0 "$exit_code" -} - -########################## -# JSON output tests -########################## - -function test_json_summary_generates_valid_json() { - VERSION="0.31.0" - CURRENT_VERSION="0.30.0" - SANDBOX_MODE=false - DRY_RUN=false - FORCE_MODE=false - COMPLETED_STEPS=("step1" "step2") - - local result - result=$(release::json::summary "success") - - assert_contains '"status": "success"' "$result" - assert_contains '"version": "0.31.0"' "$result" - assert_contains '"current_version": "0.30.0"' "$result" - assert_contains '"completed_steps": ["step1","step2"]' "$result" -} - -function test_json_summary_handles_empty_steps() { - # shellcheck disable=SC2034 # Variables used by release::json::summary - VERSION="0.31.0" - # shellcheck disable=SC2034 - CURRENT_VERSION="0.30.0" - # shellcheck disable=SC2034 - SANDBOX_MODE=false - # shellcheck disable=SC2034 - DRY_RUN=false - # shellcheck disable=SC2034 - FORCE_MODE=false - COMPLETED_STEPS=() - - local result - result=$(release::json::summary "success") - - assert_contains '"completed_steps": []' "$result" -} - -########################## -# State tracking tests -########################## - -function test_state_record_step_adds_to_completed_steps() { - COMPLETED_STEPS=() - # shellcheck disable=SC2034 # Used by release::state::record_step - VERBOSE_MODE=false - - release::state::record_step "test_step" - - assert_same "test_step" "${COMPLETED_STEPS[0]}" -} - -########################## -# Error with suggestion tests -########################## - -function test_error_with_suggestion_shows_both_messages() { - local result - result=$(release::error_with_suggestion "Test error" "Test suggestion" 2>&1) - - assert_contains "Test error" "$result" - assert_contains "Suggestion:" "$result" - assert_contains "Test suggestion" "$result" -} - -########################## -# Sandbox mode tests -########################## - -function test_sandbox_create_creates_temp_directory() { - release::sandbox::create 2>/dev/null - assert_not_empty "$SANDBOX_DIR" - assert_directory_exists "$SANDBOX_DIR" - rm -rf "$SANDBOX_DIR" -} - -function test_sandbox_create_copies_project_files() { - local original_dir - original_dir=$(pwd) - - cd "$RELEASE_SCRIPT_DIR" || return - release::sandbox::create 2>/dev/null - - # Check that key files were copied - assert_file_exists "$SANDBOX_DIR/bashunit" - assert_file_exists "$SANDBOX_DIR/build.sh" - assert_file_exists "$SANDBOX_DIR/CHANGELOG.md" - - rm -rf "$SANDBOX_DIR" - cd "$original_dir" || return -} - -function test_sandbox_create_excludes_git_directory() { - local original_dir - original_dir=$(pwd) - - cd "$RELEASE_SCRIPT_DIR" || return - release::sandbox::create 2>/dev/null - - # .git should NOT be copied - assert_directory_not_exists "$SANDBOX_DIR/.git" - - rm -rf "$SANDBOX_DIR" - cd "$original_dir" || return -} - -function test_sandbox_create_excludes_release_state() { - local original_dir - original_dir=$(pwd) - - cd "$RELEASE_SCRIPT_DIR" || return - - # Create a .release-state directory to test exclusion - mkdir -p .release-state/test - release::sandbox::create 2>/dev/null - - # .release-state should NOT be copied - assert_directory_not_exists "$SANDBOX_DIR/.release-state" - - rm -rf "$SANDBOX_DIR" .release-state - cd "$original_dir" || return -} - -function test_sandbox_setup_git_initializes_repo() { - if ! command -v git >/dev/null 2>&1; then - bashunit::skip "git not available" && return - fi - - local original_dir - original_dir=$(pwd) - - cd "$RELEASE_SCRIPT_DIR" || return - release::sandbox::create 2>/dev/null - release::sandbox::setup_git 2>/dev/null - - # Should have a .git directory now - assert_directory_exists "$SANDBOX_DIR/.git" - - rm -rf "$SANDBOX_DIR" - cd "$original_dir" || return -} - -function test_sandbox_setup_git_creates_initial_commit() { - if ! command -v git >/dev/null 2>&1; then - bashunit::skip "git not available" && return - fi - - local original_dir - original_dir=$(pwd) - - cd "$RELEASE_SCRIPT_DIR" || return - release::sandbox::create 2>/dev/null - release::sandbox::setup_git 2>/dev/null - - # Should have at least one commit - local commit_count - commit_count=$(git -C "$SANDBOX_DIR" rev-list --count HEAD 2>/dev/null || echo "0") - assert_equals "1" "$commit_count" - - rm -rf "$SANDBOX_DIR" - cd "$original_dir" || return -} - -function test_sandbox_mock_gh_handles_release_command() { - release::sandbox::mock_gh 2>/dev/null - - local result - result=$(gh release create "1.0.0" 2>&1) - assert_contains "SANDBOX" "$result" - - # Unset the mock - unset -f gh -} - -function test_sandbox_mock_gh_handles_api_command() { - release::sandbox::mock_gh 2>/dev/null - - local result - result=$(gh api /repos/test 2>&1) - - # Should return empty (not fail) - assert_successful_code - - unset -f gh -} - -function test_sandbox_mock_gh_handles_auth_command() { - release::sandbox::mock_gh 2>/dev/null - - # Should succeed (return 0) - gh auth status 2>/dev/null - assert_successful_code - - unset -f gh -} - -function test_sandbox_mock_git_push_prevents_actual_push() { - if ! command -v git >/dev/null 2>&1; then - bashunit::skip "git not available" && return - fi - - release::sandbox::mock_git_push 2>/dev/null - - local result - result=$(git push origin main 2>&1) - assert_contains "SANDBOX" "$result" - - # Unset the mock - unset -f git -} - -function test_sandbox_mock_git_push_allows_other_git_commands() { - if ! command -v git >/dev/null 2>&1; then - bashunit::skip "git not available" && return - fi - - local temp_dir - temp_dir=$(mktemp -d) - - ( - cd "$temp_dir" || return - git init --quiet - git config user.email "test@test.com" - git config user.name "Test" - echo "test" >file.txt - git add file.txt - - release::sandbox::mock_git_push 2>/dev/null - - # Non-push commands should work - git commit -m "test" --quiet - git status --short - ) 2>/dev/null - - assert_successful_code - rm -rf "$temp_dir" - unset -f git 2>/dev/null || true -} - -########################## -# Update function tests -########################## - -function test_update_file_pattern_modifies_file() { - local temp_dir - temp_dir=$(mktemp -d) - - echo 'VERSION="1.0.0"' >"$temp_dir/test.txt" - - DRY_RUN=false - ( - cd "$temp_dir" || return - release::update_file_pattern "test.txt" 'VERSION="[^"]*"' 'VERSION="2.0.0"' "version" 2>/dev/null - ) - - local result - result=$(cat "$temp_dir/test.txt") - assert_contains 'VERSION="2.0.0"' "$result" - - rm -rf "$temp_dir" -} - -function test_update_file_pattern_logs_dry_run() { - local temp_dir - temp_dir=$(mktemp -d) - - echo 'VERSION="1.0.0"' >"$temp_dir/test.txt" - - DRY_RUN=true - local output - output=$( - cd "$temp_dir" || return - release::update_file_pattern "test.txt" 'VERSION="[^"]*"' 'VERSION="2.0.0"' "version" 2>&1 - ) - DRY_RUN=false - - # File should NOT be modified - local result - result=$(cat "$temp_dir/test.txt") - assert_contains 'VERSION="1.0.0"' "$result" - assert_contains "DRY-RUN" "$output" - - rm -rf "$temp_dir" -} - -function test_update_bashunit_version_changes_version_string() { - local temp_dir - temp_dir=$(mktemp -d) - - cp "$FIXTURES_DIR/mock_bashunit" "$temp_dir/bashunit" - - DRY_RUN=false - ( - cd "$temp_dir" || return - release::update_bashunit_version "0.31.0" 2>/dev/null - ) - - local result - result=$(cat "$temp_dir/bashunit") - assert_contains 'BASHUNIT_VERSION="0.31.0"' "$result" - - rm -rf "$temp_dir" -} - -function test_update_install_version_changes_version_string() { - local temp_dir - temp_dir=$(mktemp -d) - - cp "$FIXTURES_DIR/mock_install.sh" "$temp_dir/install.sh" - - DRY_RUN=false - ( - cd "$temp_dir" || return - release::update_install_version "0.31.0" 2>/dev/null - ) - - local result - result=$(cat "$temp_dir/install.sh") - assert_contains 'LATEST_BASHUNIT_VERSION="0.31.0"' "$result" - - rm -rf "$temp_dir" -} - -function test_update_package_json_version_changes_version_string() { - local temp_dir - temp_dir=$(mktemp -d) - - cp "$FIXTURES_DIR/mock_package.json" "$temp_dir/package.json" - - DRY_RUN=false - ( - cd "$temp_dir" || return - release::update_package_json_version "0.31.0" 2>/dev/null - ) - - local result - result=$(cat "$temp_dir/package.json") - assert_contains '"version": "0.31.0"' "$result" - - rm -rf "$temp_dir" -} - -function test_update_changelog_adds_new_unreleased_section() { - local temp_dir - temp_dir=$(mktemp -d) - - cp "$FIXTURES_DIR/CHANGELOG.md" "$temp_dir/CHANGELOG.md" - - DRY_RUN=false - GITHUB_REPO_URL="https://github.com/TypedDevs/bashunit" - ( - cd "$temp_dir" || return - release::update_changelog "0.31.0" "0.30.0" 2>/dev/null - ) - - local result - result=$(cat "$temp_dir/CHANGELOG.md") - # Should have a new Unreleased section at the top - assert_contains "## Unreleased" "$result" - # Should have the new version header - assert_contains "[0.31.0]" "$result" - - rm -rf "$temp_dir" -} - -function test_update_changelog_dry_run_does_not_modify() { - local temp_dir - temp_dir=$(mktemp -d) - - cp "$FIXTURES_DIR/CHANGELOG.md" "$temp_dir/CHANGELOG.md" - local original - original=$(cat "$temp_dir/CHANGELOG.md") - - DRY_RUN=true - GITHUB_REPO_URL="https://github.com/TypedDevs/bashunit" - ( - cd "$temp_dir" || return - release::update_changelog "0.31.0" "0.30.0" 2>/dev/null - ) - DRY_RUN=false - - local result - result=$(cat "$temp_dir/CHANGELOG.md") - assert_equals "$original" "$result" - - rm -rf "$temp_dir" -} - -function test_update_checksum_updates_package_json() { - local temp_dir - temp_dir=$(mktemp -d) - - cp "$FIXTURES_DIR/mock_package.json" "$temp_dir/package.json" - mkdir -p "$temp_dir/bin" - echo "newchecksum123 bin/bashunit" >"$temp_dir/bin/checksum" - - DRY_RUN=false - ( - cd "$temp_dir" || return - release::update_checksum 2>/dev/null - ) - - local result - result=$(cat "$temp_dir/package.json") - assert_contains '"checksum": "newchecksum123"' "$result" - - rm -rf "$temp_dir" -} - -########################## -# Dry-run mode tests -########################## - -function test_dry_run_does_not_modify_any_files() { - local temp_dir - temp_dir=$(mktemp -d) - - # Copy all fixture files - cp "$FIXTURES_DIR/mock_bashunit" "$temp_dir/bashunit" - cp "$FIXTURES_DIR/mock_install.sh" "$temp_dir/install.sh" - cp "$FIXTURES_DIR/mock_package.json" "$temp_dir/package.json" - cp "$FIXTURES_DIR/CHANGELOG.md" "$temp_dir/CHANGELOG.md" - - # Save originals - local orig_bashunit orig_install orig_package orig_changelog - orig_bashunit=$(cat "$temp_dir/bashunit") - orig_install=$(cat "$temp_dir/install.sh") - orig_package=$(cat "$temp_dir/package.json") - orig_changelog=$(cat "$temp_dir/CHANGELOG.md") - - DRY_RUN=true - # shellcheck disable=SC2034 # Used by release:: functions - GITHUB_REPO_URL="https://github.com/TypedDevs/bashunit" - ( - cd "$temp_dir" || return - release::update_bashunit_version "0.31.0" 2>/dev/null - release::update_install_version "0.31.0" 2>/dev/null - release::update_package_json_version "0.31.0" 2>/dev/null - release::update_changelog "0.31.0" "0.30.0" 2>/dev/null - ) - DRY_RUN=false - - # All files should be unchanged - assert_equals "$orig_bashunit" "$(cat "$temp_dir/bashunit")" - assert_equals "$orig_install" "$(cat "$temp_dir/install.sh")" - assert_equals "$orig_package" "$(cat "$temp_dir/package.json")" - assert_equals "$orig_changelog" "$(cat "$temp_dir/CHANGELOG.md")" - - rm -rf "$temp_dir" -} - -function test_dry_run_logs_what_would_happen() { - local temp_dir - temp_dir=$(mktemp -d) - - cp "$FIXTURES_DIR/mock_bashunit" "$temp_dir/bashunit" - - DRY_RUN=true - local output - output=$( - cd "$temp_dir" || return - release::update_bashunit_version "0.31.0" 2>&1 - ) - DRY_RUN=false - - assert_contains "DRY-RUN" "$output" - assert_contains "Would update" "$output" - - rm -rf "$temp_dir" -} - -########################## -# Logging function tests -########################## - -function test_log_info_outputs_blue_prefix() { - local result - result=$(release::log_info "Test message" 2>&1) - assert_contains "[INFO]" "$result" - assert_contains "Test message" "$result" -} - -function test_log_success_outputs_green_prefix() { - local result - result=$(release::log_success "Test message" 2>&1) - assert_contains "[OK]" "$result" - assert_contains "Test message" "$result" -} - -function test_log_warning_outputs_yellow_prefix() { - local result - result=$(release::log_warning "Test message" 2>&1) - assert_contains "[WARN]" "$result" - assert_contains "Test message" "$result" -} - -function test_log_error_outputs_red_prefix() { - local result - result=$(release::log_error "Test message" 2>&1) - assert_contains "[ERROR]" "$result" - assert_contains "Test message" "$result" -} - -function test_log_dry_run_outputs_dry_run_prefix() { - local result - result=$(release::log_dry_run "Test message" 2>&1) - assert_contains "[DRY-RUN]" "$result" - assert_contains "Test message" "$result" -} - -function test_log_sandbox_outputs_sandbox_prefix() { - local result - result=$(release::log_sandbox "Test message" 2>&1) - assert_contains "[SANDBOX]" "$result" - assert_contains "Test message" "$result" -} - -function test_log_verbose_only_outputs_when_enabled() { - VERBOSE_MODE=false - local result_disabled - result_disabled=$(release::log_verbose "Test message" 2>&1) - assert_empty "$result_disabled" - - VERBOSE_MODE=true - local result_enabled - result_enabled=$(release::log_verbose "Test message" 2>&1) - assert_contains "[VERBOSE]" "$result_enabled" - assert_contains "Test message" "$result_enabled" - # shellcheck disable=SC2034 # Used by release::log_verbose - VERBOSE_MODE=false -} - -########################## -# Get current version test -########################## - -function test_get_current_version_extracts_from_bashunit() { - local temp_dir - temp_dir=$(mktemp -d) - - cp "$FIXTURES_DIR/mock_bashunit" "$temp_dir/bashunit" - - local result - result=$(cd "$temp_dir" && release::get_current_version) - - assert_equals "0.29.0" "$result" - - rm -rf "$temp_dir" -} - -########################## -# Build project test -########################## - -function test_build_project_dry_run_does_not_execute() { - DRY_RUN=true - local result - result=$(release::build_project 2>&1) - # shellcheck disable=SC2034 # Used by release:: functions - DRY_RUN=false - - assert_contains "DRY-RUN" "$result" - assert_contains "Would run" "$result" -} diff --git a/tests/unit/release_update_test.sh b/tests/unit/release_update_test.sh new file mode 100644 index 00000000..80e6e49c --- /dev/null +++ b/tests/unit/release_update_test.sh @@ -0,0 +1,253 @@ +#!/usr/bin/env bash + +# release.sh requires Bash 3.1+ (uses += array syntax) +# Skip this entire test file on Bash 3.0 +if [[ "${BASH_VERSINFO[0]}" -eq 3 ]] && [[ "${BASH_VERSINFO[1]}" -lt 1 ]]; then + # shellcheck disable=SC2317 + return 0 2>/dev/null || exit 0 +fi + +RELEASE_SCRIPT_DIR="" +FIXTURES_DIR="" + +function set_up_before_script() { + RELEASE_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + FIXTURES_DIR="$(dirname "${BASH_SOURCE[0]}")/fixtures/release" + + # Source release.sh to get access to functions + # shellcheck source=/dev/null + source "$RELEASE_SCRIPT_DIR/release.sh" +} + +########################## +# Update function tests +########################## + +function test_update_file_pattern_modifies_file() { + local temp_dir + temp_dir=$(mktemp -d) + + echo 'VERSION="1.0.0"' >"$temp_dir/test.txt" + + DRY_RUN=false + ( + cd "$temp_dir" || return + release::update_file_pattern "test.txt" 'VERSION="[^"]*"' 'VERSION="2.0.0"' "version" 2>/dev/null + ) + + local result + result=$(cat "$temp_dir/test.txt") + assert_contains 'VERSION="2.0.0"' "$result" + + rm -rf "$temp_dir" +} + +function test_update_file_pattern_logs_dry_run() { + local temp_dir + temp_dir=$(mktemp -d) + + echo 'VERSION="1.0.0"' >"$temp_dir/test.txt" + + DRY_RUN=true + local output + output=$( + cd "$temp_dir" || return + release::update_file_pattern "test.txt" 'VERSION="[^"]*"' 'VERSION="2.0.0"' "version" 2>&1 + ) + DRY_RUN=false + + # File should NOT be modified + local result + result=$(cat "$temp_dir/test.txt") + assert_contains 'VERSION="1.0.0"' "$result" + assert_contains "DRY-RUN" "$output" + + rm -rf "$temp_dir" +} + +function test_update_bashunit_version_changes_version_string() { + local temp_dir + temp_dir=$(mktemp -d) + + cp "$FIXTURES_DIR/mock_bashunit" "$temp_dir/bashunit" + + DRY_RUN=false + ( + cd "$temp_dir" || return + release::update_bashunit_version "0.31.0" 2>/dev/null + ) + + local result + result=$(cat "$temp_dir/bashunit") + assert_contains 'BASHUNIT_VERSION="0.31.0"' "$result" + + rm -rf "$temp_dir" +} + +function test_update_install_version_changes_version_string() { + local temp_dir + temp_dir=$(mktemp -d) + + cp "$FIXTURES_DIR/mock_install.sh" "$temp_dir/install.sh" + + DRY_RUN=false + ( + cd "$temp_dir" || return + release::update_install_version "0.31.0" 2>/dev/null + ) + + local result + result=$(cat "$temp_dir/install.sh") + assert_contains 'LATEST_BASHUNIT_VERSION="0.31.0"' "$result" + + rm -rf "$temp_dir" +} + +function test_update_package_json_version_changes_version_string() { + local temp_dir + temp_dir=$(mktemp -d) + + cp "$FIXTURES_DIR/mock_package.json" "$temp_dir/package.json" + + DRY_RUN=false + ( + cd "$temp_dir" || return + release::update_package_json_version "0.31.0" 2>/dev/null + ) + + local result + result=$(cat "$temp_dir/package.json") + assert_contains '"version": "0.31.0"' "$result" + + rm -rf "$temp_dir" +} + +function test_update_changelog_adds_new_unreleased_section() { + local temp_dir + temp_dir=$(mktemp -d) + + cp "$FIXTURES_DIR/CHANGELOG.md" "$temp_dir/CHANGELOG.md" + + DRY_RUN=false + GITHUB_REPO_URL="https://github.com/TypedDevs/bashunit" + ( + cd "$temp_dir" || return + release::update_changelog "0.31.0" "0.30.0" 2>/dev/null + ) + + local result + result=$(cat "$temp_dir/CHANGELOG.md") + # Should have a new Unreleased section at the top + assert_contains "## Unreleased" "$result" + # Should have the new version header + assert_contains "[0.31.0]" "$result" + + rm -rf "$temp_dir" +} + +function test_update_changelog_dry_run_does_not_modify() { + local temp_dir + temp_dir=$(mktemp -d) + + cp "$FIXTURES_DIR/CHANGELOG.md" "$temp_dir/CHANGELOG.md" + local original + original=$(cat "$temp_dir/CHANGELOG.md") + + DRY_RUN=true + GITHUB_REPO_URL="https://github.com/TypedDevs/bashunit" + ( + cd "$temp_dir" || return + release::update_changelog "0.31.0" "0.30.0" 2>/dev/null + ) + DRY_RUN=false + + local result + result=$(cat "$temp_dir/CHANGELOG.md") + assert_equals "$original" "$result" + + rm -rf "$temp_dir" +} + +function test_update_checksum_updates_package_json() { + local temp_dir + temp_dir=$(mktemp -d) + + cp "$FIXTURES_DIR/mock_package.json" "$temp_dir/package.json" + mkdir -p "$temp_dir/bin" + echo "newchecksum123 bin/bashunit" >"$temp_dir/bin/checksum" + + DRY_RUN=false + ( + cd "$temp_dir" || return + release::update_checksum 2>/dev/null + ) + + local result + result=$(cat "$temp_dir/package.json") + assert_contains '"checksum": "newchecksum123"' "$result" + + rm -rf "$temp_dir" +} + +########################## +# Dry-run mode tests +########################## + +function test_dry_run_does_not_modify_any_files() { + local temp_dir + temp_dir=$(mktemp -d) + + # Copy all fixture files + cp "$FIXTURES_DIR/mock_bashunit" "$temp_dir/bashunit" + cp "$FIXTURES_DIR/mock_install.sh" "$temp_dir/install.sh" + cp "$FIXTURES_DIR/mock_package.json" "$temp_dir/package.json" + cp "$FIXTURES_DIR/CHANGELOG.md" "$temp_dir/CHANGELOG.md" + + # Save originals + local orig_bashunit orig_install orig_package orig_changelog + orig_bashunit=$(cat "$temp_dir/bashunit") + orig_install=$(cat "$temp_dir/install.sh") + orig_package=$(cat "$temp_dir/package.json") + orig_changelog=$(cat "$temp_dir/CHANGELOG.md") + + DRY_RUN=true + # shellcheck disable=SC2034 # Used by release:: functions + GITHUB_REPO_URL="https://github.com/TypedDevs/bashunit" + ( + cd "$temp_dir" || return + release::update_bashunit_version "0.31.0" 2>/dev/null + release::update_install_version "0.31.0" 2>/dev/null + release::update_package_json_version "0.31.0" 2>/dev/null + release::update_changelog "0.31.0" "0.30.0" 2>/dev/null + ) + DRY_RUN=false + + # All files should be unchanged + assert_equals "$orig_bashunit" "$(cat "$temp_dir/bashunit")" + assert_equals "$orig_install" "$(cat "$temp_dir/install.sh")" + assert_equals "$orig_package" "$(cat "$temp_dir/package.json")" + assert_equals "$orig_changelog" "$(cat "$temp_dir/CHANGELOG.md")" + + rm -rf "$temp_dir" +} + +function test_dry_run_logs_what_would_happen() { + local temp_dir + temp_dir=$(mktemp -d) + + cp "$FIXTURES_DIR/mock_bashunit" "$temp_dir/bashunit" + + DRY_RUN=true + local output + output=$( + cd "$temp_dir" || return + release::update_bashunit_version "0.31.0" 2>&1 + ) + # shellcheck disable=SC2034 + DRY_RUN=false + + assert_contains "DRY-RUN" "$output" + assert_contains "Would update" "$output" + + rm -rf "$temp_dir" +} diff --git a/tests/unit/release_utilities_test.sh b/tests/unit/release_utilities_test.sh new file mode 100644 index 00000000..97aa1249 --- /dev/null +++ b/tests/unit/release_utilities_test.sh @@ -0,0 +1,289 @@ +#!/usr/bin/env bash + +# release.sh requires Bash 3.1+ (uses += array syntax) +# Skip this entire test file on Bash 3.0 +if [[ "${BASH_VERSINFO[0]}" -eq 3 ]] && [[ "${BASH_VERSINFO[1]}" -lt 1 ]]; then + # shellcheck disable=SC2317 + return 0 2>/dev/null || exit 0 +fi + +RELEASE_SCRIPT_DIR="" +FIXTURES_DIR="" + +function set_up_before_script() { + RELEASE_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + FIXTURES_DIR="$(dirname "${BASH_SOURCE[0]}")/fixtures/release" + + # Source release.sh to get access to functions + # shellcheck source=/dev/null + source "$RELEASE_SCRIPT_DIR/release.sh" +} + +########################## +# Logging function tests +########################## + +function test_log_info_outputs_blue_prefix() { + local result + result=$(release::log_info "Test message" 2>&1) + assert_contains "[INFO]" "$result" + assert_contains "Test message" "$result" +} + +function test_log_success_outputs_green_prefix() { + local result + result=$(release::log_success "Test message" 2>&1) + assert_contains "[OK]" "$result" + assert_contains "Test message" "$result" +} + +function test_log_warning_outputs_yellow_prefix() { + local result + result=$(release::log_warning "Test message" 2>&1) + assert_contains "[WARN]" "$result" + assert_contains "Test message" "$result" +} + +function test_log_error_outputs_red_prefix() { + local result + result=$(release::log_error "Test message" 2>&1) + assert_contains "[ERROR]" "$result" + assert_contains "Test message" "$result" +} + +function test_log_dry_run_outputs_dry_run_prefix() { + local result + result=$(release::log_dry_run "Test message" 2>&1) + assert_contains "[DRY-RUN]" "$result" + assert_contains "Test message" "$result" +} + +function test_log_sandbox_outputs_sandbox_prefix() { + local result + result=$(release::log_sandbox "Test message" 2>&1) + assert_contains "[SANDBOX]" "$result" + assert_contains "Test message" "$result" +} + +function test_log_verbose_only_outputs_when_enabled() { + VERBOSE_MODE=false + local result_disabled + result_disabled=$(release::log_verbose "Test message" 2>&1) + assert_empty "$result_disabled" + + VERBOSE_MODE=true + local result_enabled + result_enabled=$(release::log_verbose "Test message" 2>&1) + assert_contains "[VERBOSE]" "$result_enabled" + assert_contains "Test message" "$result_enabled" + # shellcheck disable=SC2034 # Used by release::log_verbose + VERBOSE_MODE=false +} + +########################## +# Backup and rollback tests +########################## + +function test_backup_init_creates_directory() { + local temp_dir + temp_dir=$(mktemp -d) + + ( + cd "$temp_dir" || return + release::backup::init + [[ -d "$BACKUP_DIR" ]] && echo "exists" + ) >/tmp/backup_test_result 2>&1 + + assert_contains "exists" "$(cat /tmp/backup_test_result)" || true + rm -rf "$temp_dir" /tmp/backup_test_result + assert_successful_code +} + +function test_backup_save_file_copies_file() { + local temp_dir + temp_dir=$(mktemp -d) + + local result + result=$( + cd "$temp_dir" || return + echo "test content" >testfile.txt + release::backup::init + release::backup::save_file "testfile.txt" + cat "$BACKUP_DIR/testfile.txt" + ) + + assert_same "test content" "$result" + rm -rf "$temp_dir" +} + +function test_rollback_restore_files_restores_backup() { + local temp_dir + temp_dir=$(mktemp -d) + + local result + result=$( + cd "$temp_dir" || return + echo "original content" >testfile.txt + release::backup::init + release::backup::save_file "testfile.txt" + echo "modified content" >testfile.txt + release::rollback::restore_files 2>/dev/null + cat testfile.txt + ) + + assert_same "original content" "$result" + rm -rf "$temp_dir" +} + +########################## +# Pre-flight check tests +########################## + +function test_preflight_check_required_files_passes_when_all_exist() { + # Run in the project root where all files exist + local result + result=$(cd "$RELEASE_SCRIPT_DIR" && release::preflight::check_required_files 2>&1) + assert_successful_code +} + +function test_preflight_check_required_files_fails_when_file_missing() { + local temp_dir + temp_dir=$(mktemp -d) + + local result + result=$(cd "$temp_dir" && release::preflight::check_required_files 2>&1) || true + + assert_contains "Required files missing" "$result" + rm -rf "$temp_dir" +} + +function test_preflight_check_changelog_unreleased_passes_with_content() { + local result + result=$(cd "$FIXTURES_DIR" && release::preflight::check_changelog_unreleased 2>&1) + assert_successful_code +} + +function test_preflight_check_changelog_unreleased_fails_when_missing() { + local temp_dir + temp_dir=$(mktemp -d) + echo "# Changelog" >"$temp_dir/CHANGELOG.md" + + local result + result=$(cd "$temp_dir" && release::preflight::check_changelog_unreleased 2>&1) || true + + assert_contains "missing '## Unreleased' section" "$result" + rm -rf "$temp_dir" +} + +########################## +# Force mode tests +########################## + +function test_confirm_action_auto_confirms_in_force_mode() { + FORCE_MODE=true + local result + result=$(release::confirm_action "Test prompt" 2>&1) + local exit_code=$? + FORCE_MODE=false + assert_same 0 "$exit_code" +} + +########################## +# JSON output tests +########################## + +function test_json_summary_generates_valid_json() { + VERSION="0.31.0" + CURRENT_VERSION="0.30.0" + SANDBOX_MODE=false + DRY_RUN=false + FORCE_MODE=false + COMPLETED_STEPS=("step1" "step2") + + local result + result=$(release::json::summary "success") + + assert_contains '"status": "success"' "$result" + assert_contains '"version": "0.31.0"' "$result" + assert_contains '"current_version": "0.30.0"' "$result" + assert_contains '"completed_steps": ["step1","step2"]' "$result" +} + +function test_json_summary_handles_empty_steps() { + # shellcheck disable=SC2034 # Variables used by release::json::summary + VERSION="0.31.0" + # shellcheck disable=SC2034 + CURRENT_VERSION="0.30.0" + # shellcheck disable=SC2034 + SANDBOX_MODE=false + # shellcheck disable=SC2034 + DRY_RUN=false + # shellcheck disable=SC2034 + FORCE_MODE=false + COMPLETED_STEPS=() + + local result + result=$(release::json::summary "success") + + assert_contains '"completed_steps": []' "$result" +} + +########################## +# State tracking tests +########################## + +function test_state_record_step_adds_to_completed_steps() { + COMPLETED_STEPS=() + # shellcheck disable=SC2034 # Used by release::state::record_step + VERBOSE_MODE=false + + release::state::record_step "test_step" + + assert_same "test_step" "${COMPLETED_STEPS[0]}" +} + +########################## +# Error with suggestion tests +########################## + +function test_error_with_suggestion_shows_both_messages() { + local result + result=$(release::error_with_suggestion "Test error" "Test suggestion" 2>&1) + + assert_contains "Test error" "$result" + assert_contains "Suggestion:" "$result" + assert_contains "Test suggestion" "$result" +} + +########################## +# Get current version test +########################## + +function test_get_current_version_extracts_from_bashunit() { + local temp_dir + temp_dir=$(mktemp -d) + + cp "$FIXTURES_DIR/mock_bashunit" "$temp_dir/bashunit" + + local result + result=$(cd "$temp_dir" && release::get_current_version) + + assert_equals "0.29.0" "$result" + + rm -rf "$temp_dir" +} + +########################## +# Build project test +########################## + +function test_build_project_dry_run_does_not_execute() { + DRY_RUN=true + local result + result=$(release::build_project 2>&1) + # shellcheck disable=SC2034 # Used by release:: functions + DRY_RUN=false + + assert_contains "DRY-RUN" "$result" + assert_contains "Would run" "$result" +} diff --git a/tests/unit/release_validation_test.sh b/tests/unit/release_validation_test.sh new file mode 100644 index 00000000..40f193b2 --- /dev/null +++ b/tests/unit/release_validation_test.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env bash + +# release.sh requires Bash 3.1+ (uses += array syntax) +# Skip this entire test file on Bash 3.0 +if [[ "${BASH_VERSINFO[0]}" -eq 3 ]] && [[ "${BASH_VERSINFO[1]}" -lt 1 ]]; then + # shellcheck disable=SC2317 + return 0 2>/dev/null || exit 0 +fi + +RELEASE_SCRIPT_DIR="" + +function set_up_before_script() { + RELEASE_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + + # Source release.sh to get access to functions + # shellcheck source=/dev/null + source "$RELEASE_SCRIPT_DIR/release.sh" +} + +########################## +# release::validate_semver tests +########################## + +function test_validate_semver_accepts_valid_version() { + # Should not exit (no output means success) + local result + result=$(release::validate_semver "0.30.0" 2>&1) || true + assert_empty "$result" +} + +function test_validate_semver_accepts_major_version() { + local result + result=$(release::validate_semver "1.0.0" 2>&1) || true + assert_empty "$result" +} + +function test_validate_semver_accepts_large_numbers() { + local result + result=$(release::validate_semver "10.20.30" 2>&1) || true + assert_empty "$result" +} + +function test_validate_semver_rejects_two_part_version() { + local result + result=$(release::validate_semver "0.30" 2>&1) || true + assert_contains "Invalid version format" "$result" +} + +function test_validate_semver_rejects_v_prefix() { + local result + result=$(release::validate_semver "v0.30.0" 2>&1) || true + assert_contains "Invalid version format" "$result" +} + +function test_validate_semver_rejects_prerelease_suffix() { + local result + result=$(release::validate_semver "0.30.0-beta" 2>&1) || true + assert_contains "Invalid version format" "$result" +} + +function test_validate_semver_rejects_empty_string() { + local result + result=$(release::validate_semver "" 2>&1) || true + assert_contains "Invalid version format" "$result" +} + +function test_validate_semver_rejects_text() { + local result + result=$(release::validate_semver "latest" 2>&1) || true + assert_contains "Invalid version format" "$result" +} + +########################## +# release::version_gt tests +########################## + +function test_version_gt_returns_true_when_patch_greater() { + release::version_gt "0.29.1" "0.29.0" + assert_successful_code +} + +function test_version_gt_returns_true_when_minor_greater() { + release::version_gt "0.30.0" "0.29.0" + assert_successful_code +} + +function test_version_gt_returns_true_when_major_greater() { + release::version_gt "1.0.0" "0.99.99" + assert_successful_code +} + +function test_version_gt_returns_false_when_equal() { + assert_unsuccessful_code "$(release::version_gt "0.29.0" "0.29.0")" +} + +function test_version_gt_returns_false_when_less() { + assert_unsuccessful_code "$(release::version_gt "0.28.0" "0.29.0")" +} + +function test_version_gt_returns_false_when_patch_less() { + assert_unsuccessful_code "$(release::version_gt "0.29.0" "0.29.1")" +} + +function test_version_gt_handles_large_numbers() { + release::version_gt "10.20.31" "10.20.30" + assert_successful_code +} + +# Data provider for version comparison +# @data_provider provider_release::version_gt_true +function test_version_gt_with_provider_returns_true() { + local v1="$1" + local v2="$2" + release::version_gt "$v1" "$v2" + assert_successful_code +} + +function provider_release::version_gt_true() { + bashunit::data_set "0.30.0" "0.29.0" + bashunit::data_set "1.0.0" "0.99.99" + bashunit::data_set "0.29.1" "0.29.0" + bashunit::data_set "2.0.0" "1.99.99" +} + +# @data_provider provider_release::version_gt_false +function test_version_gt_with_provider_returns_false() { + local v1="$1" + local v2="$2" + assert_unsuccessful_code "$(release::version_gt "$v1" "$v2")" +} + +function provider_release::version_gt_false() { + bashunit::data_set "0.29.0" "0.29.0" + bashunit::data_set "0.28.0" "0.29.0" + bashunit::data_set "0.29.0" "0.30.0" + bashunit::data_set "0.99.99" "1.0.0" +}