From e8612cd76081dacc5d8325dc5357fb616ee92fba Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Thu, 8 Oct 2026 19:48:35 +0200 Subject: [PATCH 1/3] add PHPUnit extension to report slowest tests --- phpunit.xml | 4 ++ .../SlowestTests/RunnerFinishedSubscriber.php | 21 ++++++ .../SlowestTests/SlowTestExtension.php | 24 +++++++ .../PHPUnit/SlowestTests/SlowTestReporter.php | 71 +++++++++++++++++++ .../SlowestTests/TestFinishedSubscriber.php | 31 ++++++++ .../SlowestTests/TestPreparedSubscriber.php | 24 +++++++ 6 files changed, 175 insertions(+) create mode 100644 utils/PHPUnit/SlowestTests/RunnerFinishedSubscriber.php create mode 100644 utils/PHPUnit/SlowestTests/SlowTestExtension.php create mode 100644 utils/PHPUnit/SlowestTests/SlowTestReporter.php create mode 100644 utils/PHPUnit/SlowestTests/TestFinishedSubscriber.php create mode 100644 utils/PHPUnit/SlowestTests/TestPreparedSubscriber.php diff --git a/phpunit.xml b/phpunit.xml index 37c4eb12d7f..c134897098b 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -21,4 +21,8 @@ + + + + diff --git a/utils/PHPUnit/SlowestTests/RunnerFinishedSubscriber.php b/utils/PHPUnit/SlowestTests/RunnerFinishedSubscriber.php new file mode 100644 index 00000000000..42452ea0809 --- /dev/null +++ b/utils/PHPUnit/SlowestTests/RunnerFinishedSubscriber.php @@ -0,0 +1,21 @@ +slowTestReporter->printSlowest(); + } +} diff --git a/utils/PHPUnit/SlowestTests/SlowTestExtension.php b/utils/PHPUnit/SlowestTests/SlowTestExtension.php new file mode 100644 index 00000000000..fde3cf46b27 --- /dev/null +++ b/utils/PHPUnit/SlowestTests/SlowTestExtension.php @@ -0,0 +1,24 @@ +registerSubscribers( + new TestPreparedSubscriber($slowTestReporter), + new TestFinishedSubscriber($slowTestReporter), + new RunnerFinishedSubscriber($slowTestReporter), + ); + } +} diff --git a/utils/PHPUnit/SlowestTests/SlowTestReporter.php b/utils/PHPUnit/SlowestTests/SlowTestReporter.php new file mode 100644 index 00000000000..6a727b23567 --- /dev/null +++ b/utils/PHPUnit/SlowestTests/SlowTestReporter.php @@ -0,0 +1,71 @@ + + */ + private array $startedAt = []; + + /** + * Test name => accumulated duration in seconds (summed across data-provider fixtures). + * @var array + */ + private array $durations = []; + + public function testPrepared(string $testId, float $secondsSinceStart): void + { + $this->startedAt[$testId] = $secondsSinceStart; + } + + public function testFinished(string $testId, string $testName, float $secondsSinceStart): void + { + if (! isset($this->startedAt[$testId])) { + return; + } + + $this->durations[$testName] = ($this->durations[$testName] ?? 0.0) + ($secondsSinceStart - $this->startedAt[$testId]); + unset($this->startedAt[$testId]); + } + + public function printSlowest(): void + { + $slowDurations = array_filter( + $this->durations, + static fn (float $seconds): bool => $seconds >= self::THRESHOLD_SECONDS + ); + + if ($slowDurations === []) { + return; + } + + arsort($slowDurations); + $slowDurations = array_slice($slowDurations, 0, self::LIMIT, true); + + echo PHP_EOL . sprintf('%d slowest tests (>= %.1fs):', count($slowDurations), self::THRESHOLD_SECONDS) . PHP_EOL; + + $position = 1; + foreach ($slowDurations as $testName => $seconds) { + echo sprintf('%3d. %6.2fs %s', $position, $seconds, $testName) . PHP_EOL; + ++$position; + } + + echo PHP_EOL; + } +} diff --git a/utils/PHPUnit/SlowestTests/TestFinishedSubscriber.php b/utils/PHPUnit/SlowestTests/TestFinishedSubscriber.php new file mode 100644 index 00000000000..79dc10d26d1 --- /dev/null +++ b/utils/PHPUnit/SlowestTests/TestFinishedSubscriber.php @@ -0,0 +1,31 @@ +test(); + + // group all data-provider fixtures under one test method, so a slow rule shows as a single entry + $reportName = $test instanceof TestMethod ? $test->className() . '::' . $test->methodName() : $test->name(); + + $this->slowTestReporter->testFinished( + $test->id(), + $reportName, + $event->telemetryInfo()->durationSinceStart()->asFloat() + ); + } +} diff --git a/utils/PHPUnit/SlowestTests/TestPreparedSubscriber.php b/utils/PHPUnit/SlowestTests/TestPreparedSubscriber.php new file mode 100644 index 00000000000..49426b9de78 --- /dev/null +++ b/utils/PHPUnit/SlowestTests/TestPreparedSubscriber.php @@ -0,0 +1,24 @@ +slowTestReporter->testPrepared( + $event->test()->id(), + $event->telemetryInfo()->durationSinceStart()->asFloat() + ); + } +} From 524c9fc0d17ffddeaede1c8941b8414f3b624705 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 8 Oct 2026 17:49:55 +0000 Subject: [PATCH 2/3] [ci-review] Rector Rectify --- utils/PHPUnit/SlowestTests/SlowTestReporter.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/utils/PHPUnit/SlowestTests/SlowTestReporter.php b/utils/PHPUnit/SlowestTests/SlowTestReporter.php index 6a727b23567..8e7e0cb8c7a 100644 --- a/utils/PHPUnit/SlowestTests/SlowTestReporter.php +++ b/utils/PHPUnit/SlowestTests/SlowTestReporter.php @@ -6,16 +6,12 @@ final class SlowTestReporter { - /** - * @var int - */ - private const LIMIT = 25; + private const int LIMIT = 25; /** * Only report tests slower than this, to keep the list signal-heavy. - * @var float */ - private const THRESHOLD_SECONDS = 0.5; + private const float THRESHOLD_SECONDS = 0.5; /** * Seconds since run start, captured when a test is prepared, keyed by test id. From 058044e91d94646d04ed5cf90f744d656119b617 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Thu, 8 Oct 2026 19:52:57 +0200 Subject: [PATCH 3/3] skip SlowTestExtension in class-leak check --- .github/workflows/code_analysis.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code_analysis.yaml b/.github/workflows/code_analysis.yaml index d4869162c92..33831a4260f 100644 --- a/.github/workflows/code_analysis.yaml +++ b/.github/workflows/code_analysis.yaml @@ -53,7 +53,7 @@ jobs: - name: 'Active Classes' run: | - vendor/bin/class-leak check bin config src rules utils --skip-suffix "Rector" --skip-type="Rector\\Utils\\Compiler\\Unprefixer" --skip-type="Rector\\NodeCollector\\BinaryOpConditionsCollector" --skip-type="Rector\\Set\\Contract\\SetListInterface" --skip-type="Rector\\DependencyInjection\\PHPStan\\RichParserFactory" --skip-type="Rector\\Utils\\PHPStan\\Tests\\Rule\\SingleServiceRegistrationRule\\Source\\DuplicateRegistrationFactory" --skip-type="Rector\\Utils\\PHPStan\\Tests\\Rule\\SingleServiceRegistrationRule\\Source\\SingleRegistrationFactory" --skip-type="Rector\\ChangesReporting\\Contract\\Output\\OutputFormatterInterface" --skip-type="Rector\\NodeTypeResolver\\Contract\\NodeTypeResolverInterface" --skip-type="Rector\\CodingStyle\\Contract\\ClassNameImport\\ClassNameImportSkipVoterInterface" --skip-type="Rector\\Php80\\Contract\\ConverterAttributeDecoratorInterface" --skip-type="Rector\\NodeNameResolver\\Contract\\NodeNameResolverInterface" --skip-type="Rector\\StaticTypeMapper\\Contract\\PhpDocParser\\PhpDocTypeMapperInterface" --skip-type="Rector\\PhpAttribute\\Contract\\AnnotationToAttributeMapperInterface" --skip-type="Rector\\PHPStanStaticTypeMapper\\Contract\\TypeMapperInterface" --skip-type="Rector\\StaticTypeMapper\\Contract\\PhpParser\\PhpParserNodeMapperInterface" + vendor/bin/class-leak check bin config src rules utils --skip-suffix "Rector" --skip-type="Rector\\Utils\\Compiler\\Unprefixer" --skip-type="Rector\\NodeCollector\\BinaryOpConditionsCollector" --skip-type="Rector\\Set\\Contract\\SetListInterface" --skip-type="Rector\\DependencyInjection\\PHPStan\\RichParserFactory" --skip-type="Rector\\Utils\\PHPStan\\Tests\\Rule\\SingleServiceRegistrationRule\\Source\\DuplicateRegistrationFactory" --skip-type="Rector\\Utils\\PHPStan\\Tests\\Rule\\SingleServiceRegistrationRule\\Source\\SingleRegistrationFactory" --skip-type="Rector\\ChangesReporting\\Contract\\Output\\OutputFormatterInterface" --skip-type="Rector\\NodeTypeResolver\\Contract\\NodeTypeResolverInterface" --skip-type="Rector\\CodingStyle\\Contract\\ClassNameImport\\ClassNameImportSkipVoterInterface" --skip-type="Rector\\Php80\\Contract\\ConverterAttributeDecoratorInterface" --skip-type="Rector\\NodeNameResolver\\Contract\\NodeNameResolverInterface" --skip-type="Rector\\StaticTypeMapper\\Contract\\PhpDocParser\\PhpDocTypeMapperInterface" --skip-type="Rector\\PhpAttribute\\Contract\\AnnotationToAttributeMapperInterface" --skip-type="Rector\\PHPStanStaticTypeMapper\\Contract\\TypeMapperInterface" --skip-type="Rector\\StaticTypeMapper\\Contract\\PhpParser\\PhpParserNodeMapperInterface" --skip-type="Rector\\Utils\\PHPUnit\\SlowestTests\\SlowTestExtension" - name: 'Compatible PHPStan versions'