Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/workflows/reusable-phpunit-tests-v3.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ jobs:
# - Logs debug information about what's installed within the WordPress Docker containers.
# - Install WordPress within the Docker container.
# - Run the PHPUnit tests.
# - Flags slow PHPUnit tests with GitHub Actions annotations and a run summary.
# - Publish PHPUnit timing metrics to CodeVitals.
# - Upload the code coverage report to Codecov.io.
# - Ensures version-controlled files are not modified or deleted.
Expand Down Expand Up @@ -273,6 +274,28 @@ jobs:
TEST_GROUPS: ${{ inputs.phpunit-test-groups }}
MULTISITE_FLAG: ${{ inputs.multisite && 'multisite' || 'single' }}

- name: Flag slow PHPUnit tests
# Runs when the test step succeeds or fails, but skips cancelled jobs.
# This step never fails the job itself (continue-on-error), because the
# slow-test signal is advisory only.
if: >-
Comment thread
lancewillett marked this conversation as resolved.
${{
! cancelled() && inputs.report && inputs.php == '8.5' &&
( github.event_name == 'pull_request' ||
( github.event_name == 'push' && github.ref == 'refs/heads/trunk' ) )
}}
continue-on-error: true
run: |
if [ -f tests/phpunit/build/logs/junit.xml ]; then
php tests/phpunit/prepare-slow-test-annotations.php \
tests/phpunit/build/logs/junit.xml \
1.0 \
20 \
"$RUNNER_TEMP/phpunit-timing-metrics.json"
else
echo 'PHPUnit JUnit report not found; skipping slow-test annotations.'
fi

- name: Publish PHPUnit timing metrics
continue-on-error: true
if: |
Expand All @@ -295,6 +318,7 @@ jobs:
trunk \
"$GITHUB_SHA" \
"$COMMITTED_AT" \
"$RUNNER_TEMP/phpunit-timing-metrics.json" \
| curl --fail-with-body --silent --show-error \
--request POST \
--header 'Content-Type: application/json' \
Expand Down
89 changes: 66 additions & 23 deletions tests/phpunit/includes/class-wp-phpunit-timing-metrics.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,50 +8,93 @@ final class WP_PHPUnit_Timing_Metrics {
/**
* Extracts timing metrics from a JUnit XML file.
*
* @param string $file Path to the JUnit XML file.
* @param string $file Path to the JUnit XML file.
* @param callable|null $testcase_callback Optional callback invoked for each timed testcase.
* @return array<string, float|int> Timing metrics keyed for CodeVitals.
* @throws RuntimeException If the file cannot be read or contains invalid timing data.
* @throws RuntimeException If the file cannot be read or contains invalid XML.
*/
public static function from_file( $file ) {
public static function from_file( $file, $testcase_callback = null ) {
if ( ! is_readable( $file ) ) {
throw new RuntimeException( 'The JUnit timing report could not be read.' );
}

$reader = new XMLReader();
if ( ! $reader->open( $file, null, LIBXML_NONET | LIBXML_COMPACT ) ) {
throw new RuntimeException( 'The JUnit timing report could not be opened.' );
if ( null !== $testcase_callback && ! is_callable( $testcase_callback ) ) {
throw new InvalidArgumentException( 'The testcase callback must be callable.' );
}

$suite_time = null;
$test_times = array();
$reader = new XMLReader();
$previous_libxml_state = libxml_use_internal_errors( true );
$reader_is_open = false;
$suite_time = null;
$test_times = array();
$xml_errors = array();

while ( $reader->read() ) {
if ( XMLReader::ELEMENT !== $reader->nodeType ) {
continue;
libxml_clear_errors();

try {
if ( ! $reader->open( $file, null, LIBXML_NONET | LIBXML_COMPACT ) ) {
throw new RuntimeException( 'The JUnit timing report could not be opened.' );
}

if ( null === $suite_time && 'testsuite' === $reader->name ) {
$reader_is_open = true;

while ( $reader->read() ) {
if ( XMLReader::ELEMENT !== $reader->nodeType ) {
continue;
}

if ( null === $suite_time && 'testsuite' === $reader->name ) {
$time = $reader->getAttribute( 'time' );
if ( is_numeric( $time ) ) {
$suite_time = (float) $time;
}
continue;
}

if ( 'testcase' !== $reader->name ) {
continue;
}

$time = $reader->getAttribute( 'time' );
if ( is_numeric( $time ) ) {
$suite_time = (float) $time;

// A testcase without numeric timing, such as a skipped test, carries
// no timing signal and is excluded from the aggregate metrics.
if ( ! is_numeric( $time ) ) {
continue;
}
continue;
}

if ( 'testcase' !== $reader->name ) {
continue;
$test_time = (float) $time;
$test_times[] = $test_time;

if ( null !== $testcase_callback ) {
$testcase_callback(
array(
'name' => (string) $reader->getAttribute( 'name' ),
'class' => (string) $reader->getAttribute( 'class' ),
'file' => (string) $reader->getAttribute( 'file' ),
'line' => (string) $reader->getAttribute( 'line' ),
'time' => $test_time,
'time_display' => (string) $time,
)
);
}
}

$time = $reader->getAttribute( 'time' );
if ( ! is_numeric( $time ) ) {
$xml_errors = libxml_get_errors();
} finally {
if ( $reader_is_open ) {
$reader->close();
throw new RuntimeException( 'A JUnit testcase is missing numeric timing data.' );
}

$test_times[] = (float) $time;
libxml_clear_errors();
libxml_use_internal_errors( $previous_libxml_state );
}

$reader->close();
foreach ( $xml_errors as $xml_error ) {
if ( LIBXML_ERR_WARNING < $xml_error->level ) {
throw new RuntimeException( 'The JUnit timing report contains invalid XML.' );
}
}

if ( ! $test_times ) {
throw new RuntimeException( 'The JUnit timing report contains no testcases.' );
Expand Down
Loading
Loading