diff --git a/.claude/skills/linting-code/SKILL.md b/.claude/skills/linting-code/SKILL.md index d81d5b9..2680faf 100644 --- a/.claude/skills/linting-code/SKILL.md +++ b/.claude/skills/linting-code/SKILL.md @@ -10,10 +10,10 @@ description: > ## Tools and where configuration lives -| Tool | Checks | Config | -| --- | --- | --- | -| ShellCheck | Shell syntax, quoting, portability, common bugs | `.pre-commit-config.yaml` (args: `--severity=warning --external-sources`) | -| MarkdownLint | Markdown formatting, list style, line length | `.markdownlint.yaml` | +| Tool | Checks | Config | +| ------------ | ----------------------------------------------- | ------------------------------------------------------------------------- | +| ShellCheck | Shell syntax, quoting, portability, common bugs | `.pre-commit-config.yaml` (args: `--severity=warning --external-sources`) | +| MarkdownLint | Markdown formatting, list style, line length | `.markdownlint.yaml` | Both run through `pre-commit` so versions are pinned. Always use the Make targets or `pre-commit` directly — do not invoke `shellcheck` or `markdownlint` as standalone commands. @@ -74,8 +74,8 @@ pre-commit run markdownlint --all-files * Blank line above and below every heading (MD022) * Ordered lists: use `1.` for every item, or true sequential numbers; never an arbitrary starting number (MD029) -* Table separator rows: always `| --- | --- |`, **never** `|---|---|` — MD060 compact style - requires a space to the left and right of every `---` cell (MD060) +* Table column style: `"aligned"` (MD060) — all column widths must be padded uniformly so every + pipe is vertically aligned; the separator row dashes must span the full column width * Specify a language on all fenced code blocks where possible (optional but preferred) ## Interpreting failures @@ -88,10 +88,10 @@ Look up unknown codes at `https://www.shellcheck.net/wiki/SCXXX`. Most common codes in this project: -| Code | Cause | Fix | -| --- | --- | --- | -| SC2086 | Unquoted variable | Wrap in `"$var"` | -| SC2155 | Combined `local`/assign | Split: `local x; x=$(...)` | +| Code | Cause | Fix | +| ------ | --------------------------------- | ------------------------------- | +| SC2086 | Unquoted variable | Wrap in `"$var"` | +| SC2155 | Combined `local`/assign | Split: `local x; x=$(...)` | | SC2181 | Check `$?` instead of direct `if` | Use `if command; then` directly | ### MarkdownLint diff --git a/.claude/skills/pre-pr-checks/SKILL.md b/.claude/skills/pre-pr-checks/SKILL.md index c28a53a..f3df5a1 100644 --- a/.claude/skills/pre-pr-checks/SKILL.md +++ b/.claude/skills/pre-pr-checks/SKILL.md @@ -93,11 +93,11 @@ Resolve conflicts, then re-run the full test suite. ## Quick-reference: common pre-PR failures -| Symptom | Cause | Fix | -| --- | --- | --- | -| ShellCheck SC2086 | Unquoted variable | `"$var"` | -| ShellCheck SC2155 | Combined declare/assign | `local x; x=$(...)` | -| MarkdownLint MD004 | List marker is `-` | Change to `*` | -| Test not run | Call missing at bottom of suite | Add the call | -| Assertion not reached | Missing `\|\| return` on prior assertion | Add `\|\| return` | -| No release triggered | Commit type not release-triggering | Check `.releaserc.json` | +| Symptom | Cause | Fix | +| --------------------- | ---------------------------------------- | ----------------------- | +| ShellCheck SC2086 | Unquoted variable | `"$var"` | +| ShellCheck SC2155 | Combined declare/assign | `local x; x=$(...)` | +| MarkdownLint MD004 | List marker is `-` | Change to `*` | +| Test not run | Call missing at bottom of suite | Add the call | +| Assertion not reached | Missing `\|\| return` on prior assertion | Add `\|\| return` | +| No release triggered | Commit type not release-triggering | Check `.releaserc.json` | diff --git a/.claude/skills/running-tests/SKILL.md b/.claude/skills/running-tests/SKILL.md index 4b7e273..9e2ab3d 100644 --- a/.claude/skills/running-tests/SKILL.md +++ b/.claude/skills/running-tests/SKILL.md @@ -23,15 +23,15 @@ This lists every runnable suite name. `test_example.sh` is a contributor templat Default to targeted. Only run the full suite for broad changes or as a pre-PR gate. -| What changed | Run | -| --- | --- | -| A specific functional area (e.g. format handling, log levels) | The matching suite(s) only | -| `logging.sh` core logic (init, output routing, sanitization) | Full suite | -| `install.sh` | `test_install` only | -| Config file parsing | `test_config` and `test_config_security` | -| Security-related code | Relevant security suites + full suite | -| New test file added | New suite only, then full suite | -| Pre-PR / CI gate | Full suite | +| What changed | Run | +| ------------------------------------------------------------- | ---------------------------------------- | +| A specific functional area (e.g. format handling, log levels) | The matching suite(s) only | +| `logging.sh` core logic (init, output routing, sanitization) | Full suite | +| `install.sh` | `test_install` only | +| Config file parsing | `test_config` and `test_config_security` | +| Security-related code | Relevant security suites + full suite | +| New test file added | New suite only, then full suite | +| Pre-PR / CI gate | Full suite | ### Mapping changed code to suites @@ -94,6 +94,42 @@ make test make test-junit ``` +### Stop at first failure (fail-fast) + +Use this when a test run is failing and you want to isolate the broken suite quickly, +especially during `make sonar-analysis` debugging: + +```bash +# Via Make +make test-fail-fast + +# Or directly +./tests/run_tests.sh --fail-fast + +# Short form +./tests/run_tests.sh -x +``` + +In sequential mode the runner stops immediately after the first failing suite. +In parallel mode it stops before any subsequent pipeline step. + +### Debug a failing `make sonar-analysis` + +kcov (used by `make coverage`) changes the execution environment — it sets `BASH_ENV` +and installs a `DEBUG` trap, which can cause tests to fail that pass normally. To isolate +the kcov-specific failure quickly, use `coverage-debug` which runs kcov in sequential +fail-fast mode: + +```bash +make coverage-debug +``` + +Once the failing suite is identified, re-run it without kcov to confirm: + +```bash +./tests/run_tests.sh +``` + ## Reading output ``` diff --git a/.claude/skills/security-review/SKILL.md b/.claude/skills/security-review/SKILL.md index 4636202..5dbc2ce 100644 --- a/.claude/skills/security-review/SKILL.md +++ b/.claude/skills/security-review/SKILL.md @@ -204,16 +204,16 @@ value, and `SCRIPT_NAME` at the end of `init_logger` regardless of source. ## Security test suite map -| Area | Test suite(s) | -| --- | --- | -| ANSI injection | `test_ansi_injection` | -| Log injection (newlines) | `test_unsafe_newlines` | -| Path traversal | `test_path_traversal` | -| TOCTOU / symlink attacks | `test_toctou_protection` | -| Config file injection | `test_config_security` | -| Environment variable attacks | `test_environment_security` | -| Sensitive data isolation | `test_sensitive_data` | -| Script name sanitization | `test_script_name_sanitization` | +| Area | Test suite(s) | +| ---------------------------- | ------------------------------- | +| ANSI injection | `test_ansi_injection` | +| Log injection (newlines) | `test_unsafe_newlines` | +| Path traversal | `test_path_traversal` | +| TOCTOU / symlink attacks | `test_toctou_protection` | +| Config file injection | `test_config_security` | +| Environment variable attacks | `test_environment_security` | +| Sensitive data isolation | `test_sensitive_data` | +| Script name sanitization | `test_script_name_sanitization` | Run all security suites together: diff --git a/.claude/skills/writing-commits/SKILL.md b/.claude/skills/writing-commits/SKILL.md index 2b6122b..3a200a2 100644 --- a/.claude/skills/writing-commits/SKILL.md +++ b/.claude/skills/writing-commits/SKILL.md @@ -22,33 +22,33 @@ Commit history drives automated versioning via `semantic-release`. Incorrect for ## Type — required -| Type | Version bump | Use for | -| --- | --- | --- | -| `feat` | minor | New user-visible functionality | -| `fix` | patch | Bug fixes | -| `perf` | patch | Performance improvements | -| `revert` | patch | Reverting a prior commit | -| `refactor` | patch | Internal restructuring, no behaviour change | -| `docs` | none | Documentation-only changes | -| `style` | none | Whitespace/formatting, no logic change | -| `test` | none | Adding or updating tests | -| `chore` | none | Build, tooling, dependency changes | -| `ci` | none | GitHub Actions or CI config changes | +| Type | Version bump | Use for | +| ---------- | ------------ | ------------------------------------------- | +| `feat` | minor | New user-visible functionality | +| `fix` | patch | Bug fixes | +| `perf` | patch | Performance improvements | +| `revert` | patch | Reverting a prior commit | +| `refactor` | patch | Internal restructuring, no behaviour change | +| `docs` | none | Documentation-only changes | +| `style` | none | Whitespace/formatting, no logic change | +| `test` | none | Adding or updating tests | +| `chore` | none | Build, tooling, dependency changes | +| `ci` | none | GitHub Actions or CI config changes | A `BREAKING CHANGE` footer triggers a **major** bump regardless of type. ## Scope — optional but strongly preferred -| Scope | Use for | -| --- | --- | -| `logging` | Core `logging.sh` functions | -| `config` | Configuration file handling | +| Scope | Use for | +| --------- | ----------------------------------------- | +| `logging` | Core `logging.sh` functions | +| `config` | Configuration file handling | | `install` | `install.sh` and Makefile install targets | -| `tests` | Test files or test infrastructure | -| `docs` | Documentation files | -| `scripts` | Utility or demo scripts | -| `ci` | GitHub Actions workflows | -| `deps` | Dependency / Dependabot changes | +| `tests` | Test files or test infrastructure | +| `docs` | Documentation files | +| `scripts` | Utility or demo scripts | +| `ci` | GitHub Actions workflows | +| `deps` | Dependency / Dependabot changes | ## Subject — required diff --git a/.markdownlint.yaml b/.markdownlint.yaml index 1cd2ff9..459ca70 100644 --- a/.markdownlint.yaml +++ b/.markdownlint.yaml @@ -50,6 +50,6 @@ # Table pipe style - require spaces around cell content "MD060": { - "style": "spaced" + "style": "aligned" } } diff --git a/AGENTS.md b/AGENTS.md index 7cc5374..c024f8f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -103,7 +103,8 @@ All Markdown files must satisfy the rules in `.markdownlint.yaml`: * **MD013** — line length: maximum 200 characters; code blocks and tables are exempt * **MD022** — headings: one blank line above and below every heading * **MD029** — ordered list style: use `1.` for every item, or true sequential numbers -* **MD060** — table separators: `| --- | --- |` with spaces, never `|---|---|` +* **MD060** — table column style: `"aligned"` — all column widths must be padded uniformly so + every pipe is vertically aligned; separator dashes must span the full column width Rules explicitly disabled (permitted in this project): raw HTML (MD033), duplicate headings across sections (MD024), emphasis as heading (MD036), language tag on fenced blocks (MD040), diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8c20ac6..0440707 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -119,12 +119,14 @@ Please be respectful and professional in all interactions. Treat others with kin The project supports additional code quality tools for local development: -| Command | Description | Requirements | -| --------------------- | ------------------------------- | --------------------------------------------- | -| `make test` | Run test suite | Bash 4.0+ | -| `make test-junit` | Run tests with JUnit XML output | Bash 4.0+ | -| `make coverage` | Run tests with code coverage | [kcov](https://github.com/SimonKagstrom/kcov) | -| `make sonar-analysis` | Full coverage + SonarQube scan | kcov, sonar-scanner, secret-tool | +| Command | Description | Requirements | +| --------------------- | --------------------------------------------- | --------------------------------------------- | +| `make test` | Run test suite | Bash 4.0+ | +| `make test-junit` | Run tests with JUnit XML output | Bash 4.0+ | +| `make test-fail-fast` | Run tests, stop at first failure | Bash 4.0+ | +| `make coverage` | Run tests with code coverage | [kcov](https://github.com/SimonKagstrom/kcov) | +| `make coverage-debug` | Run kcov coverage, stop at first failure | kcov | +| `make sonar-analysis` | Full coverage + SonarQube scan | kcov, sonar-scanner, secret-tool | > **Note:** The `sonar` and `sonar-analysis` targets are configured for the maintainer's > private SonarQube instance. See [Testing Documentation](docs/testing.md#code-coverage-and-static-analysis) diff --git a/Makefile b/Makefile index 94431b4..1c398bd 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,7 @@ DOCS = README.md LICENSE CHANGELOG.md CONTRIBUTING.md CODE_OF_CONDUCT.md SECURIT # Shell for execution SHELL := /bin/bash -.PHONY: all help install install-user uninstall uninstall-user test test-quiet test-junit coverage sonar sonar-analysis lint lint-shell lint-markdown check demo demos clean pre-commit +.PHONY: all help install install-user uninstall uninstall-user test test-quiet test-junit test-fail-fast coverage coverage-debug sonar sonar-analysis lint lint-shell lint-markdown check demo demos clean pre-commit all: help @@ -34,7 +34,9 @@ help: @echo "Development targets:" @echo " make test Run test suite" @echo " make test-junit Run tests with JUnit XML output" + @echo " make test-fail-fast Run tests, stop at first failure" @echo " make coverage Run tests with kcov coverage" + @echo " make coverage-debug Run tests with kcov, stop at first failure" @echo " make sonar Run SonarQube scanner (syncs version from logging.sh)" @echo " make sonar-analysis Run coverage, JUnit tests, and SonarQube scan" @echo " make demo Run all demo scripts" @@ -133,6 +135,13 @@ test: test-quiet: @output=$$(./tests/run_tests.sh 2>&1) || { echo "$$output"; exit 1; } +test-fail-fast: + @echo "Running tests (fail-fast mode)..." + @if [ ! -x tests/run_tests.sh ]; then \ + chmod +x tests/run_tests.sh || { echo "Error: Cannot make test runner executable"; exit 1; }; \ + fi + @./tests/run_tests.sh --fail-fast + test-junit: @echo "Running tests with JUnit XML output..." @if [ ! -x tests/run_tests.sh ]; then \ @@ -154,6 +163,18 @@ coverage: @echo "" @echo "✓ Coverage report generated in coverage-report/" +coverage-debug: + @if ! command -v kcov >/dev/null 2>&1; then \ + echo "Error: kcov not found."; \ + echo "Install with: sudo dnf install kcov (Fedora) or sudo apt install kcov (Debian/Ubuntu)"; \ + exit 1; \ + fi + @echo "Running tests with kcov coverage (fail-fast mode)..." + @if [ ! -x tests/run_tests.sh ]; then \ + chmod +x tests/run_tests.sh || { echo "Error: Cannot make test runner executable"; exit 1; }; \ + fi + @TEST_PARALLEL_JOBS=1 kcov --include-path=./logging.sh coverage-report ./tests/run_tests.sh --fail-fast + sonar: @if ! command -v sonar-scanner >/dev/null 2>&1; then \ echo "Error: sonar-scanner not found."; \ diff --git a/docs/testing.md b/docs/testing.md index 0b46e0f..315da3e 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -97,6 +97,25 @@ export TEST_PARALLEL_JOBS=4 TEST_PARALLEL_JOBS=4 ./run_tests.sh ``` +**Stopping at the first failure:** + +Use `--fail-fast` (or `-x`) to stop the runner immediately after the first suite that +contains failures. This is the fastest way to isolate a broken test during development +or when diagnosing `make sonar-analysis` failures: + +```bash +# Stop at first failing suite +./tests/run_tests.sh --fail-fast + +# Or via Make +make test-fail-fast +``` + +In sequential mode the runner stops right after the failing suite and prints the "Failed Tests" +summary immediately. `--fail-fast` always runs sequentially: if parallel jobs are configured +(via `-j N` or `TEST_PARALLEL_JOBS`), the flag overrides them to ensure the run truly stops +at the first failure. + **Performance Impact:** * **Sequential** (`-j 1`): ~5+ minutes for full suite @@ -322,6 +341,24 @@ This runs the following targets in order: 2. `test-junit` - Generates JUnit XML test report 3. `sonar` - Uploads everything to SonarQube +**Debugging a failing `sonar-analysis`:** + +When `make sonar-analysis` fails it can be hard to spot the failing test in 500+ lines. +Use `make coverage-debug` to run the kcov pass in fail-fast mode — it stops at the first +failure and prints the test name immediately: + +```bash +make coverage-debug +``` + +Once the failing test is identified, run that suite alone without kcov to confirm whether +the failure is kcov-specific (e.g. due to kcov setting `BASH_ENV` or the DEBUG trap +slowing execution): + +```bash +./tests/run_tests.sh +``` + **Cleaning up reports:** ```bash @@ -676,26 +713,35 @@ test_custom_log_level # Add your new test When a test fails: -1. **Review the failure message** - includes test name and reason: +1. **Use fail-fast to isolate the suite** - re-run with `--fail-fast` so the runner stops + immediately after the first failure, keeping it at the top of the output: + + ```bash + make test-fail-fast + # or + ./tests/run_tests.sh --fail-fast + ``` + +2. **Review the failure message** - includes test name and reason: ``` ✗ test_feature Reason: Expected 'value1' but got 'value2' ``` -2. **Check test artifacts** - failed tests preserve temporary directories: +3. **Check test artifacts** - failed tests preserve temporary directories: ``` Test artifacts in: /tmp/bash-logger-tests.XXXXXX/timestamp ``` -3. **Run specific test** for faster iteration: +4. **Run specific test** for faster iteration: ```bash ./run_tests.sh test_specific_suite ``` -4. **Add debug output** temporarily: +5. **Add debug output** temporarily: ```bash test_feature() { @@ -711,7 +757,7 @@ When a test fails: } ``` -5. **Check the logging module** - source it interactively: +6. **Check the logging module** - source it interactively: ```bash source logging.sh diff --git a/tests/run_tests.sh b/tests/run_tests.sh index bc4a52f..dbe510e 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -26,6 +26,9 @@ PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" JUNIT_OUTPUT=false OUTPUT_DIR="$PROJECT_ROOT/test-reports" +# Fail-fast: stop after the first suite that has failures +FAIL_FAST=false + # Parallel execution options # Auto-detect available cores as a baseline system_cores=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo "1") @@ -203,6 +206,11 @@ run_test_suite() { echo "SUITE_TESTCASES_START" echo "$CURRENT_SUITE_TESTCASES" echo "SUITE_TESTCASES_END" + echo "FAILED_DETAILS_START" + for _detail in "${FAILED_TEST_DETAILS[@]+"${FAILED_TEST_DETAILS[@]}"}"; do + printf '%s\n' "$_detail" + done + echo "FAILED_DETAILS_END" } > "$results_file" # Restore output @@ -235,7 +243,7 @@ aggregate_parallel_results() { [[ -e "$result_file" ]] || continue local test_name suite_tests suite_passed suite_failed suite_skipped suite_duration - local in_testcases=false + local in_testcases=false in_failed_details=false local testcases_content="" while IFS= read -r line; do @@ -245,10 +253,18 @@ aggregate_parallel_results() { elif [[ "$line" == "SUITE_TESTCASES_END" ]]; then in_testcases=false continue + elif [[ "$line" == "FAILED_DETAILS_START" ]]; then + in_failed_details=true + continue + elif [[ "$line" == "FAILED_DETAILS_END" ]]; then + in_failed_details=false + continue fi if [[ "$in_testcases" == "true" ]]; then testcases_content+="$line" + elif [[ "$in_failed_details" == "true" ]]; then + [[ -n "$line" ]] && FAILED_TEST_DETAILS+=("$line") else case "$line" in TEST_NAME=*) test_name="${line#TEST_NAME=}" ;; @@ -367,6 +383,10 @@ main() { fi shift 2 ;; + --fail-fast|-x) + FAIL_FAST=true + shift + ;; --help|-h) echo "Usage: $0 [options] [test_suite...]" echo "" @@ -374,11 +394,13 @@ main() { echo " --junit Generate JUnit XML report for CI/SonarQube" echo " --output-dir DIR Directory for reports (default: ../test-reports)" echo " -j, --parallel N Run N test suites in parallel (auto-detect cores by default, max 8)" + echo " -x, --fail-fast Stop after the first suite that contains a failure" echo " --help, -h Show this help message" echo "" echo "Examples:" - echo " $0 # Run all tests sequentially" + echo " $0 # Run all tests" echo " $0 -j 4 # Run tests with 4 parallel jobs" + echo " $0 --fail-fast # Stop at first failure (useful for debugging)" echo " $0 --junit # Run tests with JUnit XML output" echo " $0 test_log_levels # Run specific test suite" exit 0 @@ -401,11 +423,23 @@ main() { echo "" fi + # Fail-fast requires sequential execution to guarantee stopping at the first failure + if [[ "$FAIL_FAST" == "true" && "$PARALLEL_JOBS" -gt 1 ]]; then + echo -e "${COLOR_YELLOW}Fail-fast mode: forcing sequential execution (overrides -j $PARALLEL_JOBS)${COLOR_RESET}" + echo "" + PARALLEL_JOBS=1 + fi + if [[ "$PARALLEL_JOBS" -gt 1 ]]; then echo -e "${COLOR_BLUE}Running tests with $PARALLEL_JOBS parallel jobs${COLOR_RESET}" echo "" fi + if [[ "$FAIL_FAST" == "true" ]]; then + echo -e "${COLOR_YELLOW}Fail-fast enabled: will stop after the first suite with failures${COLOR_RESET}" + echo "" + fi + # Check if logging.sh exists if [[ ! -f "$PROJECT_ROOT/logging.sh" ]]; then echo -e "${COLOR_RED}Error: logging.sh not found at $PROJECT_ROOT/logging.sh${COLOR_RESET}" @@ -485,11 +519,17 @@ main() { # Clean up results directory rm -rf "$RESULTS_DIR" + else # Sequential execution for test_file in "${test_files[@]}"; do if [[ -f "$test_file" ]]; then run_test_suite "$test_file" + if [[ "$FAIL_FAST" == "true" && $FAILED_TESTS -gt 0 ]]; then + echo -e "${COLOR_RED}Fail-fast: stopping after first failed suite${COLOR_RESET}" + echo "" + break + fi fi done fi diff --git a/tests/test_junit_output.sh b/tests/test_junit_output.sh index a9c87be..1efb3a5 100755 --- a/tests/test_junit_output.sh +++ b/tests/test_junit_output.sh @@ -375,6 +375,94 @@ test_junit_output_contains_test_results() { pass_test } +# Test that --fail-fast stops the runner after the first failing suite +test_fail_fast_stops_after_first_failure() { + start_test "--fail-fast stops after first suite with failures" + + local unique_suffix failing_suite_name second_suite_name + local failing_suite_path second_suite_path + local failing_marker second_marker + local output exit_code second_marker_count + + unique_suffix="fail_fast_$$" + failing_suite_name="test_${unique_suffix}_failing" + second_suite_name="test_${unique_suffix}_second" + failing_suite_path="$PROJECT_ROOT/tests/${failing_suite_name}.sh" + second_suite_path="$PROJECT_ROOT/tests/${second_suite_name}.sh" + failing_marker="FAIL_FAST_TEMP_FAILURE_MARKER_${unique_suffix}" + second_marker="FAIL_FAST_TEMP_SECOND_MARKER_${unique_suffix}" + + cat >"$failing_suite_path" <"$second_suite_path" <&1) + exit_code=$? + + rm -f "$failing_suite_path" "$second_suite_path" + + assert_equals "1" "$exit_code" "--fail-fast should exit non-zero after the first failing suite" || return + assert_contains "$output" "$failing_marker" "The failing suite should run before execution stops" || return + + second_marker_count=$(printf '%s\n' "$output" | grep -F -c "$second_marker" || true) + assert_equals "0" "$second_marker_count" "The second suite should not run when --fail-fast is enabled" || return + + pass_test +} + +# Test that --fail-fast message is shown when enabled +test_fail_fast_banner_shown() { + start_test "--fail-fast banner is printed when flag is set" + + local output + output=$(cd "$PROJECT_ROOT" && ./tests/run_tests.sh --fail-fast test_log_levels 2>&1) + + assert_contains "$output" "Fail-fast enabled" || return + + pass_test +} + +# Test that failed test details appear in summary for parallel runs +test_parallel_failed_details_in_summary() { + start_test "Failed test details are shown in summary for parallel runs" + + local suite_file suite_name output + suite_file=$(mktemp "$PROJECT_ROOT/tests/test_parallel_failed_details_XXXXXX.sh") || { + fail_test "Failed to create temp suite file" + return + } + suite_name=$(basename "${suite_file%.sh}") + + cat > "$suite_file" <<'SUITE_EOF' +#!/usr/bin/env bash +current_test="Intentional parallel failure" +suite_tests=$((suite_tests + 1)) +fail_test "expected parallel failure details" +SUITE_EOF + chmod +x "$suite_file" + + output=$(cd "$PROJECT_ROOT" && ./tests/run_tests.sh -j 2 "$suite_name" 2>&1 || true) + + rm -f "$suite_file" + + assert_contains "$output" "Failed Tests" "Summary should include Failed Tests section" || return + assert_contains "$output" "Intentional parallel failure" "Summary should include the failed test name" || return + assert_contains "$output" "expected parallel failure details" "Summary should include the propagated failure details" || return + + pass_test +} + # Run all tests test_junit_xml_escape_ampersand test_junit_xml_escape_less_than @@ -396,3 +484,6 @@ test_junit_testcase_escapes_special_chars_in_message test_junit_flag_creates_output_file test_junit_output_has_valid_xml_structure test_junit_output_contains_test_results +test_fail_fast_stops_after_first_failure +test_fail_fast_banner_shown +test_parallel_failed_details_in_summary