diff --git a/README.md b/README.md
index e2c1d19..08fb666 100644
--- a/README.md
+++ b/README.md
@@ -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
@@ -34,6 +35,8 @@ Add the extension to your `phpunit.xml.dist` or `phpunit.xml` file:
+
+
@@ -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:
@@ -93,6 +104,24 @@ With `showIndividualTimings` set to `true`, you'll also see timing for each test
```
+### With Custom Thresholds
+
+```xml
+
+
+
+
+
+
+
+
+
+```
+
+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:
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
index 7294098..c98027d 100644
--- a/phpcs.xml.dist
+++ b/phpcs.xml.dist
@@ -18,3 +18,4 @@
+
diff --git a/src/ExecutionTimingExtension/ExecutionTimeExtension.php b/src/ExecutionTimingExtension/ExecutionTimeExtension.php
index 63f9e1b..2b98ad8 100644
--- a/src/ExecutionTimingExtension/ExecutionTimeExtension.php
+++ b/src/ExecutionTimingExtension/ExecutionTimeExtension.php
@@ -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,
@@ -64,7 +66,9 @@ public function onExecutionFinished(): void
{
$printer = new ExecutionTimeReportPrinter(
$this->testTimes,
- $this->topN
+ $this->topN,
+ $this->warningThreshold,
+ $this->dangerThreshold
);
$printer->print();
@@ -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');
+ }
}
}
diff --git a/src/ExecutionTimingExtension/ExecutionTimeReportPrinter.php b/src/ExecutionTimingExtension/ExecutionTimeReportPrinter.php
index b961ef3..5802fee 100644
--- a/src/ExecutionTimingExtension/ExecutionTimeReportPrinter.php
+++ b/src/ExecutionTimingExtension/ExecutionTimeReportPrinter.php
@@ -16,6 +16,8 @@
namespace Phauthentic\PHPUnit\ExecutionTiming;
+use PHPUnit\Util\Color;
+
final class ExecutionTimeReportPrinter implements ExecutionTimeReportPrinterInterface
{
/**
@@ -23,7 +25,9 @@ final class ExecutionTimeReportPrinter implements ExecutionTimeReportPrinterInte
*/
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
) {
}
@@ -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(
diff --git a/tests/Unit/ExecutionTimeExtensionTest.php b/tests/Unit/ExecutionTimeExtensionTest.php
index f58439b..d8405c8 100644
--- a/tests/Unit/ExecutionTimeExtensionTest.php
+++ b/tests/Unit/ExecutionTimeExtensionTest.php
@@ -18,6 +18,7 @@
use Phauthentic\PHPUnit\ExecutionTiming\ExecutionTimeExtension;
use PHPUnit\Framework\TestCase;
+use PHPUnit\Runner\Extension\ParameterCollection;
final class ExecutionTimeExtensionTest extends TestCase
{
@@ -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();
diff --git a/tests/Unit/ExecutionTimeReportPrinterTest.php b/tests/Unit/ExecutionTimeReportPrinterTest.php
index 08a7023..a69ecdb 100644
--- a/tests/Unit/ExecutionTimeReportPrinterTest.php
+++ b/tests/Unit/ExecutionTimeReportPrinterTest.php
@@ -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);
+ }
}