From 52accbd969ad334c0694a8cc68308f79d35711f9 Mon Sep 17 00:00:00 2001 From: Kamran Abdul Aziz Date: Mon, 31 Aug 2026 10:47:41 +0530 Subject: [PATCH] Fail loudly when junit.xml is missing, empty, or unusable process_junit_xml() had several weak failure paths: empty input returned an empty string, which the reporting API only rejected much later with a confusing JSON validation error and a wasted upload; valid but unusable XML, such as an empty testsuites element or a root level testsuite, produced a payload with empty counts that uploaded cleanly and rendered with no diagnostics; a payload carrying a test count without failure and error counts casts to zero downstream and displays as Passed; and invalid XML was not checked before later property and xpath access, producing a fatal error with no explanation of the cause. That last case is how reporting broke for every host in April 2026 (#310), when email test data containing raw invalid UTF-8 bytes made junit.xml unparseable. In every case the host saw either a clean exit or an unexplained crash, so nothing pointed at the real problem. This is issue #311. report.php now stops with a clear error when junit.xml is missing or unreadable, and process_junit_xml() stops when the XML is empty, cannot be parsed, or parses without the tests, failures, and errors counts together. The unparseable case includes the first libxml parser error and line number, since that is the hardest one to diagnose from a host. The libxml error handler state is restored either way. A root level testsuite element, a legitimate JUnit shape from producers other than PHPUnit, now parses to correct counts and failure details where it previously produced empty strings. A junit file that genuinely reports zero tests still passes through unchanged, and the reporter displays that as Errored rather than Passed. Also removes an unreachable duplicated return statement. Fixes #311. --- functions.php | 45 +++++++++++++++++++++++++++++++++++++++------ report.php | 9 ++++++++- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/functions.php b/functions.php index b8367c0..c0fee35 100644 --- a/functions.php +++ b/functions.php @@ -216,6 +216,12 @@ function trailingslashit( $str ) { * - Number of errors. * - Overall execution time. * + * When the XML data is missing, empty, unparseable, or carries no usable + * result counts, execution stops with an error instead of returning an + * empty or hollow result, because such a result is either rejected much + * later with a confusing error or displayed in a misleading way. See + * issue #311. + * * @param string $xml_string The JUnit XML data as a string. This should be * well-formed XML representing the results of test executions, typically * generated by testing frameworks compatible with JUnit reporting. @@ -237,14 +243,41 @@ function trailingslashit( $str ) { */ function process_junit_xml( $xml_string ) { if ( empty( $xml_string ) ) { - return ''; - return ''; + error_message( 'junit.xml is missing or empty, so there are no test results to report. Aborting instead of reporting an empty result as a success. See https://github.com/WordPress/phpunit-test-runner/issues/311.' ); + } + + $previous_libxml_setting = libxml_use_internal_errors( true ); + libxml_clear_errors(); + + $xml = simplexml_load_string( $xml_string ); + + $errors = libxml_get_errors(); + libxml_clear_errors(); + libxml_use_internal_errors( $previous_libxml_setting ); + + if ( false === $xml ) { + $detail = ''; + if ( ! empty( $errors ) ) { + $first = $errors[0]; + $detail = ' First parser error: ' . trim( $first->message ) . ' at line ' . (int) $first->line . '.'; + } + error_message( 'junit.xml could not be parsed as XML, so the test results are unreadable. Aborting instead of reporting an unreadable result as a success.' . $detail . ' See https://github.com/WordPress/phpunit-test-runner/issues/311.' ); } - $xml = simplexml_load_string( $xml_string ); $xml_string = null; - $project = $xml->testsuite; - $results = array(); + + // PHPUnit wraps everything in , but a root + // element is also a legitimate JUnit shape from other producers. + $project = 'testsuite' === $xml->getName() ? $xml : $xml->testsuite; + $results = array(); + + if ( + ! isset( $project['tests'] ) || '' === (string) $project['tests'] || + ! isset( $project['failures'] ) || '' === (string) $project['failures'] || + ! isset( $project['errors'] ) || '' === (string) $project['errors'] + ) { + error_message( 'junit.xml parsed but contains no usable test result counts, so the test results are unusable. A result without failure and error counts would be displayed as a success. Aborting instead. See https://github.com/WordPress/phpunit-test-runner/issues/311.' ); + } $results = array( 'tests' => (string) $project['tests'], @@ -255,7 +288,7 @@ function process_junit_xml( $xml_string ) { $results['testsuites'] = array(); - $testsuites = $xml->xpath( '//testsuites//testsuite[ ( count( testcase ) > 0 ) and ( @errors > 0 or @failures > 0 ) ]' ); + $testsuites = $xml->xpath( '//testsuite[ ( count( testcase ) > 0 ) and ( @errors > 0 or @failures > 0 ) ]' ); foreach ( $testsuites as $testsuite ) { $result = array( 'name' => (string) $testsuite['name'], diff --git a/report.php b/report.php index 764ec54..b45ee39 100644 --- a/report.php +++ b/report.php @@ -84,7 +84,14 @@ // Process the junit.xml file. log_message( 'Processing and uploading junit.xml' ); -$xml = file_get_contents( $runner_vars['WPT_PREPARE_DIR'] . '/junit.xml' ); +$junit_path = $runner_vars['WPT_PREPARE_DIR'] . '/junit.xml'; +if ( ! file_exists( $junit_path ) ) { + error_message( 'junit.xml was not found at ' . $junit_path . ', so there are no test results to report. Aborting instead of reporting an empty result as a success. See https://github.com/WordPress/phpunit-test-runner/issues/311.' ); +} +if ( ! is_readable( $junit_path ) ) { + error_message( 'junit.xml exists at ' . $junit_path . ' but is not readable. Fix the file permissions and run the report again. See https://github.com/WordPress/phpunit-test-runner/issues/311.' ); +} +$xml = file_get_contents( $junit_path ); $results = process_junit_xml( $xml ); /*