From a19e80aa196c5151f773cec80cfda49479661a2c Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Thu, 9 Jul 2026 21:56:35 +0200 Subject: [PATCH] feat(reports): add --report-tap to write a TAP v13 file Writes results in TAP version 13 to a file (parallels --report-html and --report-junit): ok/not ok per test with a YAML diagnostic on failure, and # SKIP / # TODO directives for skipped/incomplete/risky tests. Complements the existing streaming '--output tap'. Closes #740 --- CHANGELOG.md | 1 + docs/command-line.md | 1 + src/console_header.sh | 1 + src/env.sh | 3 + src/main.sh | 8 +++ src/reports.sh | 60 ++++++++++++++++- tests/acceptance/bashunit_report_tap_test.sh | 22 +++++++ tests/unit/reports_test.sh | 68 ++++++++++++++++++++ 8 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 tests/acceptance/bashunit_report_tap_test.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 556c0196..68cbad22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased ### Added +- `--report-tap ` writes a TAP version 13 report to a file (complements the streaming `--output tap`) (#740) - `assert_within_delta ` asserts a number is within a tolerance of another; supports floats (#744) - `assert_array_length ` asserts an array has exactly `n` elements (#743) - `assert_file_permissions ` asserts a file's octal permission mode (`644`, `0755`); portable across GNU/BSD `stat` (#742) diff --git a/docs/command-line.md b/docs/command-line.md index ada1daf1..8ae5dd9d 100644 --- a/docs/command-line.md +++ b/docs/command-line.md @@ -71,6 +71,7 @@ bashunit test tests/ --parallel --simple | `-p, --parallel` | Run tests in parallel | | `--no-parallel` | Run tests sequentially | | `-r, --report-html ` | Write HTML report | +| `--report-tap ` | Write TAP version 13 report to a file | | `-R, --run-all` | Run all assertions (don't stop on first failure) | | `-s, --simple` | Simple output (dots) | | `--detailed` | Detailed output (default) | diff --git a/src/console_header.sh b/src/console_header.sh index 48f4fb2a..947fdc66 100644 --- a/src/console_header.sh +++ b/src/console_header.sh @@ -113,6 +113,7 @@ Options: -p, --parallel Run tests in parallel (unlimited concurrency) --no-parallel Run tests sequentially -r, --report-html Write HTML report + --report-tap Write TAP version 13 report -s, --simple Simple output (dots) --detailed Detailed output (default) --output Output format: tap (TAP version 13) diff --git a/src/env.sh b/src/env.sh index 784ccd2a..a20e7c5e 100644 --- a/src/env.sh +++ b/src/env.sh @@ -61,6 +61,7 @@ _BASHUNIT_DEFAULT_DEV_LOG="" _BASHUNIT_DEFAULT_LOG_JUNIT="" _BASHUNIT_DEFAULT_LOG_GHA="" _BASHUNIT_DEFAULT_REPORT_HTML="" +_BASHUNIT_DEFAULT_REPORT_TAP="" # Coverage defaults (following kcov, bashcov, SimpleCov conventions) _BASHUNIT_DEFAULT_COVERAGE="false" @@ -79,6 +80,7 @@ _BASHUNIT_DEFAULT_COVERAGE_THRESHOLD_HIGH="80" : "${BASHUNIT_LOG_JUNIT:=${LOG_JUNIT:=$_BASHUNIT_DEFAULT_LOG_JUNIT}}" : "${BASHUNIT_LOG_GHA:=${LOG_GHA:=$_BASHUNIT_DEFAULT_LOG_GHA}}" : "${BASHUNIT_REPORT_HTML:=${REPORT_HTML:=$_BASHUNIT_DEFAULT_REPORT_HTML}}" +: "${BASHUNIT_REPORT_TAP:=${REPORT_TAP:=$_BASHUNIT_DEFAULT_REPORT_TAP}}" # Coverage : "${BASHUNIT_COVERAGE:=${COVERAGE:=$_BASHUNIT_DEFAULT_COVERAGE}}" @@ -340,6 +342,7 @@ function bashunit::env::print_verbose() { "BASHUNIT_LOG_JUNIT" "BASHUNIT_LOG_GHA" "BASHUNIT_REPORT_HTML" + "BASHUNIT_REPORT_TAP" "BASHUNIT_PARALLEL_RUN" "BASHUNIT_SHOW_HEADER" "BASHUNIT_HEADER_ASCII_ART" diff --git a/src/main.sh b/src/main.sh index 42d264cc..84af70d8 100644 --- a/src/main.sh +++ b/src/main.sh @@ -108,6 +108,10 @@ function bashunit::main::cmd_test() { export BASHUNIT_REPORT_HTML="$2" shift ;; + --report-tap) + export BASHUNIT_REPORT_TAP="$2" + shift + ;; --no-output) export BASHUNIT_NO_OUTPUT=true ;; @@ -737,6 +741,10 @@ function bashunit::main::exec_tests() { bashunit::reports::generate_report_html "$BASHUNIT_REPORT_HTML" fi + if [ -n "$BASHUNIT_REPORT_TAP" ]; then + bashunit::reports::generate_report_tap "$BASHUNIT_REPORT_TAP" + fi + # Generate coverage report if enabled if bashunit::env::is_coverage_enabled; then # Aggregate per-process coverage data from parallel runs diff --git a/src/reports.sh b/src/reports.sh index 95cf6080..0fb97e76 100755 --- a/src/reports.sh +++ b/src/reports.sh @@ -38,7 +38,8 @@ function bashunit::reports::add_test() { { [ -n "${BASHUNIT_LOG_JUNIT:-}" ] || [ -n "${BASHUNIT_REPORT_HTML:-}" ] || - [ -n "${BASHUNIT_LOG_GHA:-}" ] + [ -n "${BASHUNIT_LOG_GHA:-}" ] || + [ -n "${BASHUNIT_REPORT_TAP:-}" ] } || return 0 local file="$1" @@ -128,6 +129,63 @@ function bashunit::reports::generate_junit_xml() { } >"$output_file" } +## +# Prepares a failure message for a TAP YAML diagnostic block: strips ANSI escape +# sequences, collapses newlines to spaces and doubles single quotes so the value +# is safe inside a YAML single-quoted scalar. Bash 3.0+ compatible. +## +function bashunit::reports::__tap_message() { + echo "$1" \ + | sed -e 's/\x1b\[[0-9;]*[a-zA-Z]//g' \ + | tr '\n' ' ' \ + | sed -e "s/'/''/g" +} + +## +# Writes results in TAP version 13 format (https://testanything.org). +# Passing/snapshot -> "ok", failed -> "not ok" with a YAML diagnostic, +# skipped/risky -> "# SKIP", incomplete -> "# TODO". +# Arguments: $1 - output file +## +function bashunit::reports::generate_report_tap() { + local output_file="$1" + local total="${#_BASHUNIT_REPORTS_TEST_NAMES[@]}" + + { + echo "TAP version 13" + echo "1..$total" + + local i seq=0 + for i in "${!_BASHUNIT_REPORTS_TEST_NAMES[@]}"; do + seq=$((seq + 1)) + local name="${_BASHUNIT_REPORTS_TEST_NAMES[$i]:-}" + local status="${_BASHUNIT_REPORTS_TEST_STATUSES[$i]:-}" + local failure_message="${_BASHUNIT_REPORTS_TEST_FAILURES[$i]:-}" + + case "$status" in + failed) + echo "not ok $seq - $name" + echo " ---" + echo " message: '$(bashunit::reports::__tap_message "$failure_message")'" + echo " ..." + ;; + skipped) + echo "ok $seq - $name # SKIP" + ;; + risky) + echo "ok $seq - $name # SKIP risky (no assertions)" + ;; + incomplete) + echo "ok $seq - $name # TODO" + ;; + *) + echo "ok $seq - $name" + ;; + esac + done + } >"$output_file" +} + function bashunit::reports::__gha_encode() { local text="$1" # Strip ANSI escape sequences first (one sed call) diff --git a/tests/acceptance/bashunit_report_tap_test.sh b/tests/acceptance/bashunit_report_tap_test.sh new file mode 100644 index 00000000..387a91db --- /dev/null +++ b/tests/acceptance/bashunit_report_tap_test.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +function set_up_before_script() { + TEST_ENV_FILE="tests/acceptance/fixtures/.env.default" +} + +function test_bashunit_report_tap_writes_a_valid_tap_file() { + local test_file=./tests/acceptance/fixtures/test_bashunit_when_log_junit.sh + local report=report.tap + + # The fixture contains failing tests by design, so the run exits non-zero; + # swallow it (we only care that --report-tap produced a valid TAP file). + ./bashunit --no-parallel --env "$TEST_ENV_FILE" --report-tap "$report" "$test_file" >/dev/null 2>&1 || true + + assert_file_exists "$report" + assert_file_contains "$report" "TAP version 13" + assert_file_contains "$report" "1.." + assert_file_contains "$report" "ok " + assert_file_contains "$report" "not ok " + + rm "$report" +} diff --git a/tests/unit/reports_test.sh b/tests/unit/reports_test.sh index 9811c8f9..3b454640 100644 --- a/tests/unit/reports_test.sh +++ b/tests/unit/reports_test.sh @@ -21,6 +21,7 @@ function set_up() { unset BASHUNIT_LOG_JUNIT unset BASHUNIT_REPORT_HTML unset BASHUNIT_LOG_GHA + unset BASHUNIT_REPORT_TAP # Create temp file for output tests _TEMP_OUTPUT_FILE=$(mktemp) @@ -452,6 +453,73 @@ function test_generate_gha_log_strips_ansi_color_codes() { assert_not_contains $'\033[' "$content" } +function test_add_test_tracks_when_tap_report_enabled() { + BASHUNIT_REPORT_TAP="report.tap" + + bashunit::reports::add_test "file.sh" "test_name" "100" "3" "passed" + + assert_same "1" "${#_BASHUNIT_REPORTS_TEST_NAMES[@]}" +} + +function test_generate_report_tap_creates_valid_header_and_plan() { + _mock_state_functions + BASHUNIT_REPORT_TAP="report.tap" + + bashunit::reports::add_test "test.sh" "test_one" "100" "2" "passed" + bashunit::reports::add_test "test.sh" "test_two" "100" "2" "passed" + bashunit::reports::generate_report_tap "$_TEMP_OUTPUT_FILE" + + local content + content=$(cat "$_TEMP_OUTPUT_FILE") + + assert_contains "TAP version 13" "$content" + assert_contains "1..2" "$content" +} + +function test_generate_report_tap_ok_for_passed_test() { + _mock_state_functions + BASHUNIT_REPORT_TAP="report.tap" + + bashunit::reports::add_test "test.sh" "test_one" "100" "2" "passed" + bashunit::reports::generate_report_tap "$_TEMP_OUTPUT_FILE" + + assert_contains "ok 1 - test_one" "$(cat "$_TEMP_OUTPUT_FILE")" +} + +function test_generate_report_tap_not_ok_for_failed_test() { + _mock_state_functions + BASHUNIT_REPORT_TAP="report.tap" + + bashunit::reports::add_test "test.sh" "test_bad" "100" "2" "failed" "expected 1 got 2" + bashunit::reports::generate_report_tap "$_TEMP_OUTPUT_FILE" + + local content + content=$(cat "$_TEMP_OUTPUT_FILE") + + assert_contains "not ok 1 - test_bad" "$content" + assert_contains "expected 1 got 2" "$content" +} + +function test_generate_report_tap_skip_directive_for_skipped_test() { + _mock_state_functions + BASHUNIT_REPORT_TAP="report.tap" + + bashunit::reports::add_test "test.sh" "test_skip" "0" "0" "skipped" + bashunit::reports::generate_report_tap "$_TEMP_OUTPUT_FILE" + + assert_contains "ok 1 - test_skip # SKIP" "$(cat "$_TEMP_OUTPUT_FILE")" +} + +function test_generate_report_tap_todo_directive_for_incomplete_test() { + _mock_state_functions + BASHUNIT_REPORT_TAP="report.tap" + + bashunit::reports::add_test "test.sh" "test_todo" "0" "0" "incomplete" + bashunit::reports::generate_report_tap "$_TEMP_OUTPUT_FILE" + + assert_contains "ok 1 - test_todo # TODO" "$(cat "$_TEMP_OUTPUT_FILE")" +} + function test_generate_report_html_applies_status_css_classes() { _mock_state_functions BASHUNIT_REPORT_HTML="report.html"