🤔 Background
--output promises stdout is the document. A file with a tear_down_after_script writes a blank line to stdout first, so the XML declaration is no longer at byte 0 and the report does not parse.
The hook does not have to fail. An ordinary one is enough:
functiontest_ok() { assert_true true; }
functiontear_down_after_script() { :; }$ bashunit --no-parallel --output junit tests/ | od -c | head -10000000 \n < ? x m l v e r s i o n = "
$ ... | python3 -c 'import sys,xml.dom.minidom as m; m.parseString(sys.stdin.read())'ExpatError: XML or text declaration not at start of entity: line 2, column 0
--parallel is unaffected, so this is also a sequential/parallel divergence. JSON tolerates leading whitespace, so --output json still parses — but the stray byte is in that stream too.
💡 Cause
src/runner/hooks.sh prints this blank line from two sibling paths. The early-return one (no tear_down_after_script defined) guards on the machine formats:
! bashunit::env::is_json_output_enabled &&! bashunit::env::is_junit_output_enabled &&
The path taken when the hook exists and ran omits both. Adding them there matches the sibling. tap stays excluded on purpose: it streams line by line, so a blank line between files is valid and its snapshots record it.
💡 Found by
Extending the sequential-vs-parallel sweep from #1295 to assert the document contract rather than the console summary: every pathological fixture x {sequential, parallel} x {json, junit}, each parsed. That table also still shows two unrelated cases, filed separately.
🤔 Background
--outputpromises stdout is the document. A file with atear_down_after_scriptwrites a blank line to stdout first, so the XML declaration is no longer at byte 0 and the report does not parse.The hook does not have to fail. An ordinary one is enough:
--parallelis unaffected, so this is also a sequential/parallel divergence. JSON tolerates leading whitespace, so--output jsonstill parses — but the stray byte is in that stream too.💡 Cause
src/runner/hooks.shprints this blank line from two sibling paths. The early-return one (notear_down_after_scriptdefined) guards on the machine formats:The path taken when the hook exists and ran omits both. Adding them there matches the sibling.
tapstays excluded on purpose: it streams line by line, so a blank line between files is valid and its snapshots record it.💡 Found by
Extending the sequential-vs-parallel sweep from #1295 to assert the document contract rather than the console summary: every pathological fixture x {sequential, parallel} x {json, junit}, each parsed. That table also still shows two unrelated cases, filed separately.