Skip to content

Commit 3165276

Browse files
author
Mohamed Hedi DRIDI
committed
fix: junit report test
1 parent 2a2e24f commit 3165276

File tree

4 files changed

+131
-20
lines changed

4 files changed

+131
-20
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@
1616
### Changed
1717
- Split Windows CI test jobs into parallel chunks to avoid timeouts
1818

19+
### Fixed
20+
- JUnit XML report now conforms to the standard schema
21+
- Remove non-standard `passed`, `incomplete`, `snapshot` attributes from `<testsuite>` and `status`, `assertions` from `<testcase>`
22+
- Add `errors="0"` attribute and `<failure>`/`<skipped>` child elements per the JUnit spec
23+
- `skipped` count now includes both skipped and incomplete tests to match emitted `<skipped/>` elements
24+
- Convert `time` values from milliseconds to seconds (float) as expected by CI tools
25+
- Strip ANSI escape sequences and invalid XML control characters from failure messages
26+
- Include actual failure messages in `<failure>` body instead of hard-coded placeholders
27+
1928
## [0.33.0](https://github.com/TypedDevs/bashunit/compare/0.32.0...0.33.0) - 2026-02-15
2029

2130
### Changed

src/reports.sh

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ _BASHUNIT_REPORTS_TEST_NAMES=()
66
_BASHUNIT_REPORTS_TEST_STATUSES=()
77
_BASHUNIT_REPORTS_TEST_DURATIONS=()
88
_BASHUNIT_REPORTS_TEST_ASSERTIONS=()
9+
_BASHUNIT_REPORTS_TEST_FAILURES=()
910

1011
function bashunit::reports::add_test_snapshot() {
1112
bashunit::reports::add_test "$1" "$2" "$3" "$4" "snapshot"
@@ -24,7 +25,7 @@ function bashunit::reports::add_test_passed() {
2425
}
2526

2627
function bashunit::reports::add_test_failed() {
27-
bashunit::reports::add_test "$1" "$2" "$3" "$4" "failed"
28+
bashunit::reports::add_test "$1" "$2" "$3" "$4" "failed" "$5"
2829
}
2930

3031
function bashunit::reports::add_test() {
@@ -36,45 +37,69 @@ function bashunit::reports::add_test() {
3637
local duration="$3"
3738
local assertions="$4"
3839
local status="$5"
40+
local failure_message="${6:-}"
3941

4042
_BASHUNIT_REPORTS_TEST_FILES[${#_BASHUNIT_REPORTS_TEST_FILES[@]}]="$file"
4143
_BASHUNIT_REPORTS_TEST_NAMES[${#_BASHUNIT_REPORTS_TEST_NAMES[@]}]="$test_name"
4244
_BASHUNIT_REPORTS_TEST_STATUSES[${#_BASHUNIT_REPORTS_TEST_STATUSES[@]}]="$status"
4345
_BASHUNIT_REPORTS_TEST_ASSERTIONS[${#_BASHUNIT_REPORTS_TEST_ASSERTIONS[@]}]="$assertions"
4446
_BASHUNIT_REPORTS_TEST_DURATIONS[${#_BASHUNIT_REPORTS_TEST_DURATIONS[@]}]="$duration"
47+
_BASHUNIT_REPORTS_TEST_FAILURES[${#_BASHUNIT_REPORTS_TEST_FAILURES[@]}]="$failure_message"
48+
}
49+
50+
function bashunit::reports::__xml_escape() {
51+
local text="$1"
52+
# Strip ANSI escape sequences and control characters invalid in XML 1.0,
53+
# then escape XML special characters (& first to avoid double-escaping)
54+
echo "$text" \
55+
| sed -e 's/\x1b\[[0-9;]*[a-zA-Z]//g' \
56+
| tr -d '\000-\010\013\014\016-\037' \
57+
| sed -e 's/&/\&amp;/g' -e 's/</\&lt;/g' -e 's/>/\&gt;/g' -e 's/"/\&quot;/g' -e "s/'/\&apos;/g"
4558
}
4659

4760
function bashunit::reports::generate_junit_xml() {
4861
local output_file="$1"
4962

50-
local test_passed=$(bashunit::state::get_tests_passed)
5163
local tests_skipped=$(bashunit::state::get_tests_skipped)
5264
local tests_incomplete=$(bashunit::state::get_tests_incomplete)
53-
local tests_snapshot=$(bashunit::state::get_tests_snapshot)
5465
local tests_failed=$(bashunit::state::get_tests_failed)
55-
local time=$(bashunit::clock::total_runtime_in_milliseconds)
66+
local time_ms=$(bashunit::clock::total_runtime_in_milliseconds)
67+
local time
68+
time=$(LC_ALL=C awk -v ms="$time_ms" 'BEGIN {printf "%.3f", ms/1000}')
5669

5770
{
5871
echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
5972
echo "<testsuites>"
6073
echo " <testsuite name=\"bashunit\" tests=\"${#_BASHUNIT_REPORTS_TEST_NAMES[@]}\""
61-
echo " passed=\"$test_passed\" failures=\"$tests_failed\" incomplete=\"$tests_incomplete\""
62-
echo " skipped=\"$tests_skipped\" snapshot=\"$tests_snapshot\""
74+
echo " failures=\"$tests_failed\" errors=\"0\""
75+
echo " skipped=\"$(( tests_skipped + tests_incomplete ))\""
6376
echo " time=\"$time\">"
6477

6578
local i
6679
for i in "${!_BASHUNIT_REPORTS_TEST_NAMES[@]}"; do
6780
local file="${_BASHUNIT_REPORTS_TEST_FILES[$i]:-}"
6881
local name="${_BASHUNIT_REPORTS_TEST_NAMES[$i]:-}"
69-
local assertions="${_BASHUNIT_REPORTS_TEST_ASSERTIONS[$i]:-}"
7082
local status="${_BASHUNIT_REPORTS_TEST_STATUSES[$i]:-}"
71-
local test_time="${_BASHUNIT_REPORTS_TEST_DURATIONS[$i]:-}"
83+
local test_time_ms="${_BASHUNIT_REPORTS_TEST_DURATIONS[$i]:-}"
84+
local failure_message="${_BASHUNIT_REPORTS_TEST_FAILURES[$i]:-}"
85+
local test_time
86+
test_time=$(LC_ALL=C awk -v ms="$test_time_ms" 'BEGIN {printf "%.3f", ms/1000}')
7287

7388
echo " <testcase file=\"$file\""
7489
echo " name=\"$name\""
75-
echo " status=\"$status\""
76-
echo " assertions=\"$assertions\""
7790
echo " time=\"$test_time\">"
91+
92+
# Add failure element for failed tests with actual failure message
93+
if [[ "$status" == "failed" ]]; then
94+
local escaped_message
95+
escaped_message=$(bashunit::reports::__xml_escape "$failure_message")
96+
echo " <failure message=\"Test failed\">$escaped_message</failure>"
97+
elif [[ "$status" == "skipped" ]]; then
98+
echo " <skipped/>"
99+
elif [[ "$status" == "incomplete" ]]; then
100+
echo " <skipped message=\"Test incomplete\"/>"
101+
fi
102+
78103
echo " </testcase>"
79104
done
80105

src/runner.sh

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -702,15 +702,15 @@ function bashunit::runner::run_test() {
702702
error_message="$hook_message"
703703
fi
704704
bashunit::console_results::print_error_test "$failure_function" "$error_message" "$runtime_output"
705-
bashunit::reports::add_test_failed "$test_file" "$failure_label" "$duration" "$total_assertions"
705+
bashunit::reports::add_test_failed "$test_file" "$failure_label" "$duration" "$total_assertions" "$error_message"
706706
bashunit::runner::write_failure_result_output "$test_file" "$failure_function" "$error_message" "$runtime_output"
707707
bashunit::internal_log "Test error" "$failure_label" "$error_message"
708708
return
709709
fi
710710

711711
if [[ "$current_assertions_failed" != "$(bashunit::state::get_assertions_failed)" ]]; then
712712
bashunit::state::add_tests_failed
713-
bashunit::reports::add_test_failed "$test_file" "$label" "$duration" "$total_assertions"
713+
bashunit::reports::add_test_failed "$test_file" "$label" "$duration" "$total_assertions" "$subshell_output"
714714
bashunit::runner::write_failure_result_output "$test_file" "$fn_name" "$subshell_output"
715715

716716
bashunit::internal_log "Test failed" "$label"
@@ -965,7 +965,7 @@ function bashunit::runner::record_file_hook_failure() {
965965

966966
bashunit::state::add_tests_failed
967967
bashunit::console_results::print_error_test "$hook_name" "$hook_output"
968-
bashunit::reports::add_test_failed "$test_file" "$(bashunit::helper::normalize_test_function_name "$hook_name")" 0 0
968+
bashunit::reports::add_test_failed "$test_file" "$(bashunit::helper::normalize_test_function_name "$hook_name")" 0 0 "$hook_output"
969969
bashunit::runner::write_failure_result_output "$test_file" "$hook_name" "$hook_output"
970970

971971
return "$status"

tests/unit/reports_test.sh

Lines changed: 84 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ function set_up() {
1313
_BASHUNIT_REPORTS_TEST_STATUSES=()
1414
_BASHUNIT_REPORTS_TEST_DURATIONS=()
1515
_BASHUNIT_REPORTS_TEST_ASSERTIONS=()
16+
_BASHUNIT_REPORTS_TEST_FAILURES=()
1617

1718
# Unset report env vars by default
1819
unset BASHUNIT_LOG_JUNIT
@@ -92,9 +93,10 @@ function test_add_test_passed_sets_passed_status() {
9293
function test_add_test_failed_sets_failed_status() {
9394
BASHUNIT_LOG_JUNIT="report.xml"
9495

95-
bashunit::reports::add_test_failed "test.sh" "my_test" "100" "2"
96+
bashunit::reports::add_test_failed "test.sh" "my_test" "100" "2" "some error"
9697

9798
assert_same "failed" "${_BASHUNIT_REPORTS_TEST_STATUSES[0]}"
99+
assert_same "some error" "${_BASHUNIT_REPORTS_TEST_FAILURES[0]}"
98100
}
99101

100102
# === Core add_test tests ===
@@ -118,13 +120,14 @@ function test_add_test_tracks_when_html_report_enabled() {
118120
function test_add_test_populates_all_arrays() {
119121
BASHUNIT_LOG_JUNIT="report.xml"
120122

121-
bashunit::reports::add_test "my_file.sh" "my_test_name" "250" "5" "failed"
123+
bashunit::reports::add_test "my_file.sh" "my_test_name" "250" "5" "failed" "expected X got Y"
122124

123125
assert_same "my_file.sh" "${_BASHUNIT_REPORTS_TEST_FILES[0]}"
124126
assert_same "my_test_name" "${_BASHUNIT_REPORTS_TEST_NAMES[0]}"
125127
assert_same "failed" "${_BASHUNIT_REPORTS_TEST_STATUSES[0]}"
126128
assert_same "250" "${_BASHUNIT_REPORTS_TEST_DURATIONS[0]}"
127129
assert_same "5" "${_BASHUNIT_REPORTS_TEST_ASSERTIONS[0]}"
130+
assert_same "expected X got Y" "${_BASHUNIT_REPORTS_TEST_FAILURES[0]}"
128131
}
129132

130133
# === JUnit XML generation tests ===
@@ -156,9 +159,10 @@ function test_generate_junit_xml_includes_testsuite_attributes() {
156159

157160
assert_contains '<testsuite name="bashunit"' "$content"
158161
assert_contains 'tests="1"' "$content"
159-
assert_contains 'passed="5"' "$content"
160162
assert_contains 'failures="1"' "$content"
161-
assert_contains 'time="1234"' "$content"
163+
assert_contains 'skipped="3"' "$content"
164+
assert_contains 'errors="0"' "$content"
165+
assert_contains 'time="1.234"' "$content"
162166
}
163167

164168
function test_generate_junit_xml_includes_testcase_elements() {
@@ -173,9 +177,82 @@ function test_generate_junit_xml_includes_testcase_elements() {
173177

174178
assert_contains '<testcase file="my_test.sh"' "$content"
175179
assert_contains 'name="test_example"' "$content"
176-
assert_contains 'status="passed"' "$content"
177-
assert_contains 'assertions="3"' "$content"
178-
assert_contains 'time="500"' "$content"
180+
assert_contains 'time="0.500"' "$content"
181+
assert_not_contains 'status=' "$content"
182+
assert_not_contains 'assertions=' "$content"
183+
}
184+
185+
function test_generate_junit_xml_passed_has_no_children() {
186+
_mock_state_functions
187+
BASHUNIT_LOG_JUNIT="report.xml"
188+
189+
bashunit::reports::add_test "test.sh" "test_ok" "200" "1" "passed"
190+
bashunit::reports::generate_junit_xml "$_TEMP_OUTPUT_FILE"
191+
192+
local content
193+
content=$(cat "$_TEMP_OUTPUT_FILE")
194+
195+
assert_not_contains '<failure' "$content"
196+
assert_not_contains '<skipped' "$content"
197+
}
198+
199+
function test_generate_junit_xml_skipped_testcase() {
200+
_mock_state_functions
201+
BASHUNIT_LOG_JUNIT="report.xml"
202+
203+
bashunit::reports::add_test "test.sh" "test_skip" "0" "0" "skipped"
204+
bashunit::reports::generate_junit_xml "$_TEMP_OUTPUT_FILE"
205+
206+
local content
207+
content=$(cat "$_TEMP_OUTPUT_FILE")
208+
209+
assert_contains '<skipped/>' "$content"
210+
assert_not_contains '<failure' "$content"
211+
}
212+
213+
function test_generate_junit_xml_incomplete_testcase() {
214+
_mock_state_functions
215+
BASHUNIT_LOG_JUNIT="report.xml"
216+
217+
bashunit::reports::add_test "test.sh" "test_todo" "0" "0" "incomplete"
218+
bashunit::reports::generate_junit_xml "$_TEMP_OUTPUT_FILE"
219+
220+
local content
221+
content=$(cat "$_TEMP_OUTPUT_FILE")
222+
223+
assert_contains '<skipped message="Test incomplete"/>' "$content"
224+
assert_not_contains '<failure' "$content"
225+
}
226+
227+
function test_generate_junit_xml_failure_element_without_type() {
228+
_mock_state_functions
229+
BASHUNIT_LOG_JUNIT="report.xml"
230+
231+
local failure_msg="Assertion failed: expected 42 but got 0"
232+
bashunit::reports::add_test "test_fail.sh" "test_failure" "1000" "5" "failed" "$failure_msg"
233+
bashunit::reports::generate_junit_xml "$_TEMP_OUTPUT_FILE"
234+
235+
local content
236+
content=$(cat "$_TEMP_OUTPUT_FILE")
237+
238+
assert_contains '<failure message="Test failed">' "$content"
239+
assert_contains "$failure_msg</failure>" "$content"
240+
assert_not_contains 'type=' "$content"
241+
}
242+
243+
function test_generate_junit_xml_failure_element_with_xml_escaping() {
244+
_mock_state_functions
245+
BASHUNIT_LOG_JUNIT="report.xml"
246+
247+
local failure_msg='Expected "value1" & "value2" to be > other'
248+
bashunit::reports::add_test "test_fail.sh" "test_xml_escape" "500" "2" "failed" "$failure_msg"
249+
bashunit::reports::generate_junit_xml "$_TEMP_OUTPUT_FILE"
250+
251+
local content
252+
content=$(cat "$_TEMP_OUTPUT_FILE")
253+
254+
# Verify XML escaping is applied
255+
assert_contains 'Expected &quot;value1&quot; &amp; &quot;value2&quot; to be &gt; other</failure>' "$content"
179256
}
180257

181258
# === HTML report generation tests ===

0 commit comments

Comments
 (0)