diff --git a/.env.example b/.env.example index d57324f1..f3682d1b 100644 --- a/.env.example +++ b/.env.example @@ -7,54 +7,54 @@ #─────────────────────────────────────────────────────────────────────────────── # Test Discovery #─────────────────────────────────────────────────────────────────────────────── -BASHUNIT_DEFAULT_PATH= # Default: tests -BASHUNIT_BOOTSTRAP= # Default: tests/bootstrap.sh -BASHUNIT_BOOTSTRAP_ARGS= # Arguments passed to bootstrap script +BASHUNIT_DEFAULT_PATH= # Default: tests +BASHUNIT_BOOTSTRAP= # Default: tests/bootstrap.sh +BASHUNIT_BOOTSTRAP_ARGS= # Arguments passed to bootstrap script #─────────────────────────────────────────────────────────────────────────────── # Output Display #─────────────────────────────────────────────────────────────────────────────── -BASHUNIT_SHOW_HEADER= # Default: true -BASHUNIT_HEADER_ASCII_ART= # Default: false -BASHUNIT_SIMPLE_OUTPUT= # Default: false (use dots instead of test names) -BASHUNIT_VERBOSE= # Default: false (show environment variables) -BASHUNIT_NO_OUTPUT= # Default: false (suppress all output) -BASHUNIT_SHOW_EXECUTION_TIME= # Default: true -BASHUNIT_SHOW_SKIPPED= # Default: false (show skipped test details) -BASHUNIT_SHOW_INCOMPLETE= # Default: false (show incomplete test details) -BASHUNIT_FAILURES_ONLY= # Default: false (only show failures) -BASHUNIT_NO_COLOR= # Default: false (disable colors) +BASHUNIT_SHOW_HEADER= # Default: true +BASHUNIT_HEADER_ASCII_ART= # Default: false +BASHUNIT_SIMPLE_OUTPUT= # Default: false (use dots instead of test names) +BASHUNIT_VERBOSE= # Default: false (show environment variables) +BASHUNIT_NO_OUTPUT= # Default: false (suppress all output) +BASHUNIT_SHOW_EXECUTION_TIME= # Default: true +BASHUNIT_SHOW_SKIPPED= # Default: false (show skipped test details) +BASHUNIT_SHOW_INCOMPLETE= # Default: false (show incomplete test details) +BASHUNIT_FAILURES_ONLY= # Default: false (only show failures) +BASHUNIT_NO_COLOR= # Default: false (disable colors) #─────────────────────────────────────────────────────────────────────────────── # Test Execution #─────────────────────────────────────────────────────────────────────────────── -BASHUNIT_PARALLEL_RUN= # Default: false -BASHUNIT_STOP_ON_FAILURE= # Default: false (stop suite on first failure) +BASHUNIT_PARALLEL_RUN= # Default: false +BASHUNIT_STOP_ON_FAILURE= # Default: false (stop suite on first failure) BASHUNIT_STOP_ON_ASSERTION_FAILURE= # Default: true (stop test on first assertion fail) -BASHUNIT_STRICT_MODE= # Default: false (enable set -euo pipefail) -BASHUNIT_LOGIN_SHELL= # Default: false (source login shell profiles) +BASHUNIT_STRICT_MODE= # Default: false (enable set -euo pipefail) +BASHUNIT_LOGIN_SHELL= # Default: false (source login shell profiles) #─────────────────────────────────────────────────────────────────────────────── # Reports #─────────────────────────────────────────────────────────────────────────────── -BASHUNIT_LOG_JUNIT= # JUnit XML report path (e.g., report.xml) -BASHUNIT_REPORT_HTML= # HTML test report path (e.g., report.html) +BASHUNIT_LOG_JUNIT= # JUnit XML report path (e.g., report.xml) +BASHUNIT_REPORT_HTML= # HTML test report path (e.g., report.html) #─────────────────────────────────────────────────────────────────────────────── # Code Coverage #─────────────────────────────────────────────────────────────────────────────── -BASHUNIT_COVERAGE= # Default: false -BASHUNIT_COVERAGE_PATHS= # Default: src/ (comma-separated paths to track) -BASHUNIT_COVERAGE_EXCLUDE= # Default: tests/*,vendor/*,*_test.sh,*Test.sh -BASHUNIT_COVERAGE_REPORT= # Default: coverage/lcov.info -BASHUNIT_COVERAGE_REPORT_HTML= # HTML coverage report directory (e.g., coverage/html) -BASHUNIT_COVERAGE_MIN= # Minimum coverage % (fails if below) -BASHUNIT_COVERAGE_THRESHOLD_LOW= # Default: 50 (red below this) -BASHUNIT_COVERAGE_THRESHOLD_HIGH=# Default: 80 (green above this) +BASHUNIT_COVERAGE= # Default: false +BASHUNIT_COVERAGE_PATHS= # Default: src/ (comma-separated paths to track) +BASHUNIT_COVERAGE_EXCLUDE= # Default: tests/*,vendor/*,*_test.sh,*Test.sh +BASHUNIT_COVERAGE_REPORT= # Default: coverage/lcov.info +BASHUNIT_COVERAGE_REPORT_HTML= # HTML coverage report directory (e.g., coverage/html) +BASHUNIT_COVERAGE_MIN= # Minimum coverage % (fails if below) +BASHUNIT_COVERAGE_THRESHOLD_LOW= # Default: 50 (red below this) +BASHUNIT_COVERAGE_THRESHOLD_HIGH= # Default: 80 (green above this) #─────────────────────────────────────────────────────────────────────────────── # Advanced / Debug #─────────────────────────────────────────────────────────────────────────────── -BASHUNIT_DEV_LOG= # Developer log file path -BASHUNIT_BENCH_MODE= # Default: false (benchmark mode) -BASHUNIT_INTERNAL_LOG= # Default: false (internal debug logging) +BASHUNIT_DEV_LOG= # Developer log file path +BASHUNIT_BENCH_MODE= # Default: false (benchmark mode) +BASHUNIT_INTERNAL_LOG= # Default: false (internal debug logging) diff --git a/.github/workflows/tests-bash-3.0.yml b/.github/workflows/tests-bash-3.0.yml index 51fc0370..0087945b 100644 --- a/.github/workflows/tests-bash-3.0.yml +++ b/.github/workflows/tests-bash-3.0.yml @@ -80,6 +80,9 @@ jobs: with: fetch-depth: 1 + - name: Setup Config + run: cp .env.example .env + - name: Download Docker image artifact uses: actions/download-artifact@v4 with: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7173d2c1..c5e95996 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,9 +17,54 @@ jobs: with: fetch-depth: 1 + - name: Setup Config + run: cp .env.example .env + - name: Run Tests run: make test + localized-ubuntu: + name: "Ubuntu - ${{ matrix.name }} Locale" + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + include: + - name: "Spanish" + lang: "es_ES.UTF-8" + tz: "Europe/Madrid" + language_pack: "language-pack-es-base" + - name: "Brazilian" + lang: "pt_BR.UTF-8" + tz: "America/Sao_Paulo" + language_pack: "language-pack-pt-base" + - name: "Japanese" + lang: "ja_JP.UTF-8" + tz: "Asia/Tokyo" + language_pack: "language-pack-ja-base" + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Install locales + run: | + sudo apt-get update + sudo apt-get install -y locales "${{ matrix.language_pack }}" + sudo locale-gen "${{ matrix.lang }}" + + - name: Setup Config + run: cp .env.example .env + + - name: Run Tests + env: + LANG: ${{ matrix.lang }} + LC_ALL: ${{ matrix.lang }} + TZ: ${{ matrix.tz }} + run: make test + macos: name: "macOS - latest" runs-on: macos-latest @@ -30,6 +75,9 @@ jobs: with: fetch-depth: 1 + - name: Setup Config + run: cp .env.example .env + - name: Run Tests run: make test @@ -63,6 +111,10 @@ jobs: with: fetch-depth: 1 + - name: Setup Config + shell: bash + run: cp .env.example .env + - name: Run tests shell: bash run: | @@ -78,6 +130,9 @@ jobs: with: fetch-depth: 1 + - name: Setup Config + run: cp .env.example .env + - name: Run Tests run: | docker run --rm -v "$(pwd)":/project alpine:latest /bin/sh -c " \ @@ -97,6 +152,9 @@ jobs: with: fetch-depth: 1 + - name: Setup Config + run: cp .env.example .env + - name: Run Tests run: | ./bashunit --simple tests/ @@ -111,6 +169,9 @@ jobs: with: fetch-depth: 1 + - name: Setup Config + run: cp .env.example .env + - name: Run Tests run: | ./bashunit --parallel --simple tests/ @@ -125,6 +186,9 @@ jobs: with: fetch-depth: 1 + - name: Setup Config + run: cp .env.example .env + - name: Run Tests run: | ./bashunit --parallel tests/ @@ -139,6 +203,9 @@ jobs: with: fetch-depth: 1 + - name: Setup Config + run: cp .env.example .env + - name: Run Tests with strict mode run: | ./bashunit --parallel --simple --strict tests/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ff9f45b..0b2f5927 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ ### Fixed - Fix spying on `echo` or `printf` causing bashunit to hang due to infinite recursion (#607) +- Fix invalid `.env.example` coverage threshold entry and copy `.env.example` to `.env` in CI test workflows so configuration parse errors are caught during automated test runs +- Fix `clock::now` shell-time parsing when `EPOCHREALTIME` uses a comma decimal separator ## [0.34.1](https://github.com/TypedDevs/bashunit/compare/0.34.0...0.34.1) - 2026-03-20 diff --git a/src/assert_dates.sh b/src/assert_dates.sh index fa33486a..849aff9d 100644 --- a/src/assert_dates.sh +++ b/src/assert_dates.sh @@ -12,10 +12,25 @@ function bashunit::date::to_epoch() { ;; esac - # Normalize ISO 8601: replace T with space, strip Z suffix, strip tz offset + # Handle Z (UTC) suffix explicitly: BusyBox needs TZ=UTC, BSD needs +0000 + case "$input" in + *Z) + local utc_input="${input%Z}" + local utc_norm="${utc_input/T/ }" + local epoch + # GNU/BusyBox: parse in explicit UTC + epoch=$(TZ=UTC date -d "$utc_input" +%s 2>/dev/null) && { echo "$epoch"; return 0; } + epoch=$(TZ=UTC date -d "$utc_norm" +%s 2>/dev/null) && { echo "$epoch"; return 0; } + # BSD: use +0000 offset which %z understands + epoch=$(date -j -f "%Y-%m-%dT%H:%M:%S%z" "${utc_input}+0000" +%s 2>/dev/null) && { echo "$epoch"; return 0; } + echo "$input" + return 1 + ;; + esac + + # Normalize ISO 8601: replace T with space, strip tz offset local normalized="$input" normalized="${normalized/T/ }" - normalized="${normalized%Z}" # Strip timezone offset (+HHMM or -HHMM) at end for initial parsing case "$normalized" in *[+-][0-9][0-9][0-9][0-9]) @@ -30,6 +45,24 @@ function bashunit::date::to_epoch() { echo "$epoch" return 0 } + # If input has timezone offset, parse in UTC and adjust manually (BusyBox) + case "$input" in + *[+-][0-9][0-9][0-9][0-9]) + epoch=$(TZ=UTC date -d "$normalized" +%s 2>/dev/null) && { + local ilen=${#input} + local ostart=$((ilen - 5)) + local osign="${input:$ostart:1}" + local ohh="${input:$((ostart + 1)):2}" + local omm="${input:$((ostart + 3)):2}" + local osecs=$(( (10#$ohh * 3600) + (10#$omm * 60) )) + if [ "$osign" = "+" ]; then + osecs=$(( -osecs )) + fi + echo $(( epoch + osecs )) + return 0 + } + ;; + esac # Try GNU date with normalized (space-separated) input if [ "$normalized" != "$input" ]; then epoch=$(date -d "$normalized" +%s 2>/dev/null) && { diff --git a/src/clock.sh b/src/clock.sh index 3a3b9281..51641a71 100644 --- a/src/clock.sh +++ b/src/clock.sh @@ -113,8 +113,11 @@ EOF shell) # shellcheck disable=SC2155 local shell_time="$(bashunit::clock::shell_time)" - local seconds="${shell_time%%.*}" - local microseconds="${shell_time#*.}" + local seconds="${shell_time%%[.,]*}" + local microseconds="${shell_time#*[.,]}" + if [ "$seconds" = "$shell_time" ]; then + microseconds="" + fi # Pad to 6 digits and strip leading zeros for arithmetic microseconds="${microseconds}000000" microseconds="${microseconds:0:6}" diff --git a/tests/acceptance/bashunit_bootstrap_args_test.sh b/tests/acceptance/bashunit_bootstrap_args_test.sh index 4f54dbd0..dc91a087 100644 --- a/tests/acceptance/bashunit_bootstrap_args_test.sh +++ b/tests/acceptance/bashunit_bootstrap_args_test.sh @@ -26,7 +26,7 @@ function test_bootstrap_args_via_env_variable() { # Use --env flag to set the bootstrap file (avoiding .env override), # but use BASHUNIT_BOOTSTRAP_ARGS from environment output=$(BASHUNIT_BOOTSTRAP_ARGS="hello world" \ - ./bashunit --no-parallel --simple \ + ./bashunit --no-parallel --simple --skip-env-file \ --env "tests/acceptance/fixtures/bootstrap_with_args.sh" \ tests/acceptance/fixtures/test_bootstrap_args.sh 2>&1) || true diff --git a/tests/acceptance/bashunit_execution_error_test.sh b/tests/acceptance/bashunit_execution_error_test.sh index 59eb3c48..d59687f1 100644 --- a/tests/acceptance/bashunit_execution_error_test.sh +++ b/tests/acceptance/bashunit_execution_error_test.sh @@ -25,7 +25,6 @@ function test_bashunit_when_a_execution_error() { printf "%sRunning ./tests/acceptance/fixtures/test_bashunit_when_a_execution_error.sh%s\n" \ "${color_bold}" "${color_default}" printf "%s✗ Error%s: Error\n" "${color_red}" "${color_default}" - printf " %sline 4: invalid_function_name: command not found%s\n" "${color_dim}" "${color_default}" ) local fixture_end=$( format_summary_title "Tests: " @@ -39,6 +38,7 @@ function test_bashunit_when_a_execution_error() { local actual="$(./bashunit --no-parallel --detailed --env "$TEST_ENV_FILE" "$test_file")" assert_contains "$fixture_start" "$actual" + assert_contains "invalid_function_name" "$actual" assert_contains "$fixture_end" "$actual" assert_general_error "$(./bashunit --no-parallel --env "$TEST_ENV_FILE" "$test_file")" } diff --git a/tests/acceptance/bashunit_no_color_test.sh b/tests/acceptance/bashunit_no_color_test.sh index 52647362..1bb7329c 100644 --- a/tests/acceptance/bashunit_no_color_test.sh +++ b/tests/acceptance/bashunit_no_color_test.sh @@ -8,7 +8,7 @@ function set_up_before_script() { function test_bashunit_no_color_flag_disables_colors() { local test_file=./tests/acceptance/fixtures/test_bashunit_when_a_test_passes.sh local output - output=$(./bashunit --no-parallel --env "$TEST_ENV_FILE" "$test_file" --no-color --simple) + output=$(./bashunit --no-parallel --skip-env-file --env "$TEST_ENV_FILE" "$test_file" --no-color --simple) # ANSI escape codes start with \x1b[ (ESC[) - should not be present assert_not_contains $'\e[' "$output" diff --git a/tests/acceptance/bashunit_setup_before_script_error_test.sh b/tests/acceptance/bashunit_setup_before_script_error_test.sh index 1fa15819..b6f25071 100644 --- a/tests/acceptance/bashunit_setup_before_script_error_test.sh +++ b/tests/acceptance/bashunit_setup_before_script_error_test.sh @@ -15,7 +15,7 @@ function test_bashunit_when_set_up_before_script_errors() { local header_line="Running $fixture" local error_line="✗ Error: Set up before script" - local message_line=" $fixture: line 4: invalid_function_name: command not found" + local message_line="invalid_function_name" local tests_summary="Tests: 1 failed, 1 total" local assertions_summary="Assertions: 0 failed, 0 total" diff --git a/tests/acceptance/bashunit_setup_error_test.sh b/tests/acceptance/bashunit_setup_error_test.sh index 70d4ebf8..8f924366 100644 --- a/tests/acceptance/bashunit_setup_error_test.sh +++ b/tests/acceptance/bashunit_setup_error_test.sh @@ -15,7 +15,7 @@ function test_bashunit_when_set_up_errors() { local header_line="Running $fixture" local error_line="✗ Error: Set up" - local message_line=" $fixture: line 4: invalid_function_name: command not found" + local message_line="invalid_function_name" local tests_summary="Tests: 1 failed, 1 total" local assertions_summary="Assertions: 0 failed, 0 total" diff --git a/tests/acceptance/bashunit_teardown_after_script_error_test.sh b/tests/acceptance/bashunit_teardown_after_script_error_test.sh index 03878918..955b1633 100644 --- a/tests/acceptance/bashunit_teardown_after_script_error_test.sh +++ b/tests/acceptance/bashunit_teardown_after_script_error_test.sh @@ -15,7 +15,7 @@ function test_bashunit_when_tear_down_after_script_errors() { local header_line="Running $fixture" local error_line="✗ Error: Tear down after script" - local message_line=" $fixture: line 4: missing_cleanup_command: command not found" + local message_line="missing_cleanup_command" local tests_summary="Tests: 1 passed, 1 failed, 2 total" local assertions_summary="Assertions: 1 passed, 0 failed, 1 total" diff --git a/tests/acceptance/bashunit_teardown_error_test.sh b/tests/acceptance/bashunit_teardown_error_test.sh index d127a12e..ee65205f 100644 --- a/tests/acceptance/bashunit_teardown_error_test.sh +++ b/tests/acceptance/bashunit_teardown_error_test.sh @@ -15,7 +15,7 @@ function test_bashunit_when_tear_down_errors() { local header_line="Running $fixture" local error_line="✗ Error: Tear down" - local message_line=" $fixture: line 4: invalid_function_name: command not found" + local message_line="invalid_function_name" local tests_summary="Tests: 1 failed, 1 total" local assertions_summary="Assertions: 0 failed, 0 total" diff --git a/tests/unit/assert_dates_test.sh b/tests/unit/assert_dates_test.sh index 57f9cff5..169763e8 100644 --- a/tests/unit/assert_dates_test.sh +++ b/tests/unit/assert_dates_test.sh @@ -152,10 +152,10 @@ function test_successful_assert_date_equals_epoch_vs_space_separated() { assert_empty "$(assert_date_equals "$epoch" "2023-11-14 12:00:00")" } -# UTC Z suffix test (documents existing behavior) +# UTC Z suffix test function test_successful_assert_date_equals_with_utc_z_suffix() { - assert_empty "$(assert_date_equals "2023-11-14T12:00:00" "2023-11-14T12:00:00Z")" + assert_empty "$(assert_date_equals "2023-11-14T14:00:00+0200" "2023-11-14T12:00:00Z")" } # Timezone offset tests diff --git a/tests/unit/clock_test.sh b/tests/unit/clock_test.sh index 39f52c70..86f5befb 100644 --- a/tests/unit/clock_test.sh +++ b/tests/unit/clock_test.sh @@ -124,6 +124,12 @@ function test_now_prefers_shell_time_over_perl() { assert_same "1234567890000" "$(bashunit::clock::now)" } +function test_now_handles_shell_time_with_comma_decimal_separator() { + bashunit::mock bashunit::clock::shell_time <<<"1234,567890" + + assert_same "1234567890000" "$(bashunit::clock::now)" +} + function test_now_prefers_python_over_node() { bashunit::mock bashunit::clock::shell_time mock_non_existing_fn bashunit::mock date mock_non_existing_fn