Skip to content
Merged
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
37 changes: 33 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ A PHPUnit extension that tracks and reports test execution times, helping you id
- Displays a summary of the slowest tests after test execution
- Configurable number of slowest tests to display
- Optional per-test timing output
- Color-coded output based on configurable thresholds (yellow for warnings, red for danger)
- Aligned column formatting for easy reading
- Compatible with PHPUnit 10, 11, and 12

Expand All @@ -34,6 +35,8 @@ Add the extension to your `phpunit.xml.dist` or `phpunit.xml` file:
<bootstrap class="Phauthentic\PHPUnit\ExecutionTiming\ExecutionTimeExtension">
<parameter name="topN" value="10"/>
<parameter name="showIndividualTimings" value="false"/>
<parameter name="warningThreshold" value="1.0"/>
<parameter name="dangerThreshold" value="5.0"/>
</bootstrap>
</extensions>
</phpunit>
Expand All @@ -43,20 +46,28 @@ Add the extension to your `phpunit.xml.dist` or `phpunit.xml` file:

- **`topN`** (default: `10`): Number of slowest tests to display in the summary report
- **`showIndividualTimings`** (default: `false`): Whether to display timing for each test as it runs
- **`warningThreshold`** (default: `1.0`): Time in seconds at which tests will be colored yellow (warning). Tests with execution time >= this value will be highlighted.
- **`dangerThreshold`** (default: `5.0`): Time in seconds at which tests will be colored red (danger). Tests with execution time >= this value will be highlighted in red. Tests between `warningThreshold` and `dangerThreshold` will be colored yellow.

## Usage

After running your tests, you'll see a summary report at the end showing the slowest tests:
After running your tests, you'll see a summary report at the end showing the slowest tests. Tests are color-coded based on their execution time:

- **Yellow**: Tests that exceed the warning threshold (default: 1 second)
- **Red**: Tests that exceed the danger threshold (default: 5 seconds)
- **Normal**: Tests below the warning threshold

```
Top 10 slowest tests:

1. MyTest::testSlowOperation : 1234.56 ms (1.235 s)
2. AnotherTest::testComplexCalculation : 987.65 ms (0.988 s)
3. DatabaseTest::testLargeQuery : 654.32 ms (0.654 s)
1. MyTest::testSlowOperation : 1234.56 ms (1.235 s) [colored red]
2. AnotherTest::testComplexCalculation : 987.65 ms (0.988 s) [colored yellow]
3. DatabaseTest::testLargeQuery : 654.32 ms (0.654 s) [colored yellow]
...
```

Note: The actual output will show ANSI color codes when viewed in a terminal that supports colors. The colors help quickly identify tests that may need optimization.

### Example Output

With `showIndividualTimings` set to `true`, you'll also see timing for each test as it executes:
Expand Down Expand Up @@ -93,6 +104,24 @@ With `showIndividualTimings` set to `true`, you'll also see timing for each test
</phpunit>
```

### With Custom Thresholds

```xml
<phpunit>
<extensions>
<bootstrap class="Phauthentic\PHPUnit\ExecutionTiming\ExecutionTimeExtension">
<parameter name="topN" value="10"/>
<parameter name="warningThreshold" value="0.5"/>
<parameter name="dangerThreshold" value="2.0"/>
</bootstrap>
</extensions>
</phpunit>
```

This configuration will:
- Show yellow for tests taking 0.5 seconds or more
- Show red for tests taking 2.0 seconds or more

## How It Works

The extension subscribes to PHPUnit events:
Expand Down
1 change: 1 addition & 0 deletions phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@
<rule ref="PSR12"/>
</ruleset>


14 changes: 13 additions & 1 deletion src/ExecutionTimingExtension/ExecutionTimeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ final class ExecutionTimeExtension implements Extension
private float $testStartTime = 0.0;
private int $topN = 10;
private bool $showIndividualTimings = false;
private float $warningThreshold = 1.0;
private float $dangerThreshold = 5.0;

public function bootstrap(
Configuration $configuration,
Expand Down Expand Up @@ -64,7 +66,9 @@ public function onExecutionFinished(): void
{
$printer = new ExecutionTimeReportPrinter(
$this->testTimes,
$this->topN
$this->topN,
$this->warningThreshold,
$this->dangerThreshold
);

$printer->print();
Expand Down Expand Up @@ -97,5 +101,13 @@ public function extractConfigurationFromParameters(ParameterCollection $paramete
FILTER_VALIDATE_BOOLEAN
);
}

if ($parameters->has('warningThreshold')) {
$this->warningThreshold = (float)$parameters->get('warningThreshold');
}

if ($parameters->has('dangerThreshold')) {
$this->dangerThreshold = (float)$parameters->get('dangerThreshold');
}
}
}
33 changes: 28 additions & 5 deletions src/ExecutionTimingExtension/ExecutionTimeReportPrinter.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,18 @@

namespace Phauthentic\PHPUnit\ExecutionTiming;

use PHPUnit\Util\Color;

final class ExecutionTimeReportPrinter implements ExecutionTimeReportPrinterInterface
{
/**
* @param array<int, array{name: string, time: float}> $testTimes
*/
public function __construct(
private readonly array $testTimes,
private readonly int $topN
private readonly int $topN,
private readonly float $warningThreshold = 1.0,
private readonly float $dangerThreshold = 5.0
) {
}

Expand Down Expand Up @@ -113,15 +117,34 @@ private function printTestLine(array $test, int $rank, array $columnWidths): voi
$rankFormatted = $this->formatRank($rank, $columnWidths['rank']);
$nameFormatted = $this->formatTestName($test['name'], $columnWidths['name']);

$color = $this->determineColor($test['time']);

$nameDisplay = $color !== '' ? Color::colorize($color, $nameFormatted) : $nameFormatted;
$timeMsDisplay = $color !== '' ? Color::colorize($color, sprintf('%.2f ms', $timeMs)) : sprintf('%.2f ms', $timeMs);
$timeSecDisplay = $color !== '' ? Color::colorize($color, sprintf('(%.3f s)', $timeSec)) : sprintf('(%.3f s)', $timeSec);

printf(
" %s. %s : %.2f ms (%.3f s)" . PHP_EOL,
" %s. %s : %s %s" . PHP_EOL,
$rankFormatted,
$nameFormatted,
$timeMs,
$timeSec
$nameDisplay,
$timeMsDisplay,
$timeSecDisplay
);
}

private function determineColor(float $time): string
{
if ($time >= $this->dangerThreshold) {
return 'fg-red';
}

if ($time >= $this->warningThreshold) {
return 'fg-yellow';
}

return '';
}

private function formatRank(int $rank, int $width): string
{
return str_pad(
Expand Down
74 changes: 74 additions & 0 deletions tests/Unit/ExecutionTimeExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

use Phauthentic\PHPUnit\ExecutionTiming\ExecutionTimeExtension;
use PHPUnit\Framework\TestCase;
use PHPUnit\Runner\Extension\ParameterCollection;

final class ExecutionTimeExtensionTest extends TestCase
{
Expand Down Expand Up @@ -51,6 +52,79 @@ public function testDefaultShowIndividualTimingsIsFalse(): void
$this->assertFalse($property->getValue($this->extension));
}

public function testDefaultWarningThresholdIsOneSecond(): void
{
$reflection = new \ReflectionClass($this->extension);
$property = $reflection->getProperty('warningThreshold');
$property->setAccessible(true);

$this->assertEquals(1.0, $property->getValue($this->extension));
}

public function testDefaultDangerThresholdIsFiveSeconds(): void
{
$reflection = new \ReflectionClass($this->extension);
$property = $reflection->getProperty('dangerThreshold');
$property->setAccessible(true);

$this->assertEquals(5.0, $property->getValue($this->extension));
}

public function testExtractConfigurationFromParametersWithWarningThreshold(): void
{
$parameters = ParameterCollection::fromArray([
'warningThreshold' => '2.5',
]);

$reflection = new \ReflectionClass($this->extension);
$method = $reflection->getMethod('extractConfigurationFromParameters');
$method->setAccessible(true);
$method->invoke($this->extension, $parameters);

$property = $reflection->getProperty('warningThreshold');
$property->setAccessible(true);

$this->assertEquals(2.5, $property->getValue($this->extension));
}

public function testExtractConfigurationFromParametersWithDangerThreshold(): void
{
$parameters = ParameterCollection::fromArray([
'dangerThreshold' => '10.0',
]);

$reflection = new \ReflectionClass($this->extension);
$method = $reflection->getMethod('extractConfigurationFromParameters');
$method->setAccessible(true);
$method->invoke($this->extension, $parameters);

$property = $reflection->getProperty('dangerThreshold');
$property->setAccessible(true);

$this->assertEquals(10.0, $property->getValue($this->extension));
}

public function testExtractConfigurationFromParametersWithBothThresholds(): void
{
$parameters = ParameterCollection::fromArray([
'warningThreshold' => '1.5',
'dangerThreshold' => '7.5',
]);

$reflection = new \ReflectionClass($this->extension);
$method = $reflection->getMethod('extractConfigurationFromParameters');
$method->setAccessible(true);
$method->invoke($this->extension, $parameters);

$warningProperty = $reflection->getProperty('warningThreshold');
$warningProperty->setAccessible(true);
$dangerProperty = $reflection->getProperty('dangerThreshold');
$dangerProperty->setAccessible(true);

$this->assertEquals(1.5, $warningProperty->getValue($this->extension));
$this->assertEquals(7.5, $dangerProperty->getValue($this->extension));
}

public function testOnExecutionFinishedWithNoTests(): void
{
ob_start();
Expand Down
116 changes: 116 additions & 0 deletions tests/Unit/ExecutionTimeReportPrinterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -155,4 +155,120 @@ public function testPrintFormatsTimeCorrectly(): void
$this->assertStringContainsString('12345.00 ms', $output);
$this->assertStringContainsString('12.345 s', $output);
}

public function testPrintDoesNotColorTestBelowThreshold(): void
{
$testTimes = [
['name' => 'FastTest', 'time' => 0.5],
];
$printer = new ExecutionTimeReportPrinter($testTimes, 10, 1.0, 5.0);

ob_start();
$printer->print();
$output = ob_get_clean() ?: '';

$this->assertStringContainsString('FastTest', $output);
// Should not contain ANSI color codes
$this->assertStringNotContainsString("\x1b[", $output);
}

public function testPrintColorsTestWithWarningThreshold(): void
{
$testTimes = [
['name' => 'WarningTest', 'time' => 1.5],
];
$printer = new ExecutionTimeReportPrinter($testTimes, 10, 1.0, 5.0);

ob_start();
$printer->print();
$output = ob_get_clean() ?: '';

$this->assertStringContainsString('WarningTest', $output);
// Should contain yellow ANSI color code (fg-yellow = 33)
$this->assertStringContainsString("\x1b[33m", $output);
// Should not contain red color code
$this->assertStringNotContainsString("\x1b[31m", $output);
}

public function testPrintColorsTestWithDangerThreshold(): void
{
$testTimes = [
['name' => 'DangerTest', 'time' => 6.0],
];
$printer = new ExecutionTimeReportPrinter($testTimes, 10, 1.0, 5.0);

ob_start();
$printer->print();
$output = ob_get_clean() ?: '';

$this->assertStringContainsString('DangerTest', $output);
// Should contain red ANSI color code (fg-red = 31)
$this->assertStringContainsString("\x1b[31m", $output);
}

public function testPrintColorsCorrectlyWithMultipleThresholds(): void
{
$testTimes = [
['name' => 'FastTest', 'time' => 0.5],
['name' => 'WarningTest', 'time' => 2.0],
['name' => 'DangerTest', 'time' => 6.0],
];
$printer = new ExecutionTimeReportPrinter($testTimes, 10, 1.0, 5.0);

ob_start();
$printer->print();
$output = ob_get_clean() ?: '';

// FastTest should not be colored
$fastTestPos = strpos($output, 'FastTest');
$this->assertNotFalse($fastTestPos);
$fastTestLine = substr($output, $fastTestPos, 100);
$this->assertStringNotContainsString("\x1b[", $fastTestLine);

// WarningTest should be yellow
$warningTestPos = strpos($output, 'WarningTest');
$this->assertNotFalse($warningTestPos);
$warningTestLine = substr($output, $warningTestPos, 200);
$this->assertStringContainsString("\x1b[33m", $warningTestLine);
$this->assertStringNotContainsString("\x1b[31m", $warningTestLine);

// DangerTest should be red
$dangerTestPos = strpos($output, 'DangerTest');
$this->assertNotFalse($dangerTestPos);
$dangerTestLine = substr($output, $dangerTestPos, 200);
$this->assertStringContainsString("\x1b[31m", $dangerTestLine);
}

public function testPrintRespectsThresholdConfiguration(): void
{
$testTimes = [
['name' => 'Test1', 'time' => 0.8],
['name' => 'Test2', 'time' => 1.2],
['name' => 'Test3', 'time' => 3.0],
];
// Custom thresholds: warning at 1.0, danger at 2.0
$printer = new ExecutionTimeReportPrinter($testTimes, 10, 1.0, 2.0);

ob_start();
$printer->print();
$output = ob_get_clean() ?: '';

// Test1 (0.8s) should not be colored
$test1Pos = strpos($output, 'Test1');
$this->assertNotFalse($test1Pos);
$test1Line = substr($output, $test1Pos, 100);
$this->assertStringNotContainsString("\x1b[", $test1Line);

// Test2 (1.2s) should be yellow (>= 1.0 but < 2.0)
$test2Pos = strpos($output, 'Test2');
$this->assertNotFalse($test2Pos);
$test2Line = substr($output, $test2Pos, 200);
$this->assertStringContainsString("\x1b[33m", $test2Line);

// Test3 (3.0s) should be red (>= 2.0)
$test3Pos = strpos($output, 'Test3');
$this->assertNotFalse($test3Pos);
$test3Line = substr($output, $test3Pos, 200);
$this->assertStringContainsString("\x1b[31m", $test3Line);
}
}