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
2 changes: 1 addition & 1 deletion .github/workflows/code_analysis.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
4 changes: 4 additions & 0 deletions phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,8 @@
<php>
<ini name="memory_limit" value="-1"/>
</php>

<extensions>
<bootstrap class="Rector\Utils\PHPUnit\SlowestTests\SlowTestExtension"/>
</extensions>
</phpunit>
21 changes: 21 additions & 0 deletions utils/PHPUnit/SlowestTests/RunnerFinishedSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Rector\Utils\PHPUnit\SlowestTests;

use PHPUnit\Event\TestRunner\ExecutionFinished;
use PHPUnit\Event\TestRunner\ExecutionFinishedSubscriber;

final readonly class RunnerFinishedSubscriber implements ExecutionFinishedSubscriber
{
public function __construct(
private SlowTestReporter $slowTestReporter
) {
}

public function notify(ExecutionFinished $event): void
{
$this->slowTestReporter->printSlowest();
}
}
24 changes: 24 additions & 0 deletions utils/PHPUnit/SlowestTests/SlowTestExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace Rector\Utils\PHPUnit\SlowestTests;

use PHPUnit\Runner\Extension\Extension;
use PHPUnit\Runner\Extension\Facade;
use PHPUnit\Runner\Extension\ParameterCollection;
use PHPUnit\TextUI\Configuration\Configuration;

final class SlowTestExtension implements Extension
{
public function bootstrap(Configuration $configuration, Facade $facade, ParameterCollection $parameters): void
{
$slowTestReporter = new SlowTestReporter();

$facade->registerSubscribers(
new TestPreparedSubscriber($slowTestReporter),
new TestFinishedSubscriber($slowTestReporter),
new RunnerFinishedSubscriber($slowTestReporter),
);
}
}
67 changes: 67 additions & 0 deletions utils/PHPUnit/SlowestTests/SlowTestReporter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

declare(strict_types=1);

namespace Rector\Utils\PHPUnit\SlowestTests;

final class SlowTestReporter
{
private const int LIMIT = 25;

/**
* Only report tests slower than this, to keep the list signal-heavy.
*/
private const float THRESHOLD_SECONDS = 0.5;

/**
* Seconds since run start, captured when a test is prepared, keyed by test id.
* @var array<string, float>
*/
private array $startedAt = [];

/**
* Test name => accumulated duration in seconds (summed across data-provider fixtures).
* @var array<string, float>
*/
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;
}
}
31 changes: 31 additions & 0 deletions utils/PHPUnit/SlowestTests/TestFinishedSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace Rector\Utils\PHPUnit\SlowestTests;

use PHPUnit\Event\Code\TestMethod;
use PHPUnit\Event\Test\Finished;
use PHPUnit\Event\Test\FinishedSubscriber;

final readonly class TestFinishedSubscriber implements FinishedSubscriber
{
public function __construct(
private SlowTestReporter $slowTestReporter
) {
}

public function notify(Finished $event): void
{
$test = $event->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()
);
}
}
24 changes: 24 additions & 0 deletions utils/PHPUnit/SlowestTests/TestPreparedSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace Rector\Utils\PHPUnit\SlowestTests;

use PHPUnit\Event\Test\Prepared;
use PHPUnit\Event\Test\PreparedSubscriber;

final readonly class TestPreparedSubscriber implements PreparedSubscriber
{
public function __construct(
private SlowTestReporter $slowTestReporter
) {
}

public function notify(Prepared $event): void
{
$this->slowTestReporter->testPrepared(
$event->test()->id(),
$event->telemetryInfo()->durationSinceStart()->asFloat()
);
}
}
Loading