Skip to content

Commit 3bb696f

Browse files
Add color-coded output for test execution times with configurable thresholds (#2)
- Introduced `warningThreshold` and `dangerThreshold` parameters to customize test execution time thresholds. - Updated README to reflect new features and usage instructions. - Enhanced `ExecutionTimeReportPrinter` to display test results in color based on execution time. - Added unit tests to verify threshold functionality and output formatting.
1 parent 10d1d11 commit 3bb696f

File tree

6 files changed

+265
-10
lines changed

6 files changed

+265
-10
lines changed

README.md

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ A PHPUnit extension that tracks and reports test execution times, helping you id
88
- Displays a summary of the slowest tests after test execution
99
- Configurable number of slowest tests to display
1010
- Optional per-test timing output
11+
- Color-coded output based on configurable thresholds (yellow for warnings, red for danger)
1112
- Aligned column formatting for easy reading
1213
- Compatible with PHPUnit 10, 11, and 12
1314

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

4447
- **`topN`** (default: `10`): Number of slowest tests to display in the summary report
4548
- **`showIndividualTimings`** (default: `false`): Whether to display timing for each test as it runs
49+
- **`warningThreshold`** (default: `1.0`): Time in seconds at which tests will be colored yellow (warning). Tests with execution time >= this value will be highlighted.
50+
- **`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.
4651

4752
## Usage
4853

49-
After running your tests, you'll see a summary report at the end showing the slowest tests:
54+
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:
55+
56+
- **Yellow**: Tests that exceed the warning threshold (default: 1 second)
57+
- **Red**: Tests that exceed the danger threshold (default: 5 seconds)
58+
- **Normal**: Tests below the warning threshold
5059

5160
```
5261
Top 10 slowest tests:
5362
54-
1. MyTest::testSlowOperation : 1234.56 ms (1.235 s)
55-
2. AnotherTest::testComplexCalculation : 987.65 ms (0.988 s)
56-
3. DatabaseTest::testLargeQuery : 654.32 ms (0.654 s)
63+
1. MyTest::testSlowOperation : 1234.56 ms (1.235 s) [colored red]
64+
2. AnotherTest::testComplexCalculation : 987.65 ms (0.988 s) [colored yellow]
65+
3. DatabaseTest::testLargeQuery : 654.32 ms (0.654 s) [colored yellow]
5766
...
5867
```
5968

69+
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.
70+
6071
### Example Output
6172

6273
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
93104
</phpunit>
94105
```
95106

107+
### With Custom Thresholds
108+
109+
```xml
110+
<phpunit>
111+
<extensions>
112+
<bootstrap class="Phauthentic\PHPUnit\ExecutionTiming\ExecutionTimeExtension">
113+
<parameter name="topN" value="10"/>
114+
<parameter name="warningThreshold" value="0.5"/>
115+
<parameter name="dangerThreshold" value="2.0"/>
116+
</bootstrap>
117+
</extensions>
118+
</phpunit>
119+
```
120+
121+
This configuration will:
122+
- Show yellow for tests taking 0.5 seconds or more
123+
- Show red for tests taking 2.0 seconds or more
124+
96125
## How It Works
97126

98127
The extension subscribes to PHPUnit events:

phpcs.xml.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@
1818
<rule ref="PSR12"/>
1919
</ruleset>
2020

21+

src/ExecutionTimingExtension/ExecutionTimeExtension.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ final class ExecutionTimeExtension implements Extension
3030
private float $testStartTime = 0.0;
3131
private int $topN = 10;
3232
private bool $showIndividualTimings = false;
33+
private float $warningThreshold = 1.0;
34+
private float $dangerThreshold = 5.0;
3335

3436
public function bootstrap(
3537
Configuration $configuration,
@@ -64,7 +66,9 @@ public function onExecutionFinished(): void
6466
{
6567
$printer = new ExecutionTimeReportPrinter(
6668
$this->testTimes,
67-
$this->topN
69+
$this->topN,
70+
$this->warningThreshold,
71+
$this->dangerThreshold
6872
);
6973

7074
$printer->print();
@@ -97,5 +101,13 @@ public function extractConfigurationFromParameters(ParameterCollection $paramete
97101
FILTER_VALIDATE_BOOLEAN
98102
);
99103
}
104+
105+
if ($parameters->has('warningThreshold')) {
106+
$this->warningThreshold = (float)$parameters->get('warningThreshold');
107+
}
108+
109+
if ($parameters->has('dangerThreshold')) {
110+
$this->dangerThreshold = (float)$parameters->get('dangerThreshold');
111+
}
100112
}
101113
}

src/ExecutionTimingExtension/ExecutionTimeReportPrinter.php

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,18 @@
1616

1717
namespace Phauthentic\PHPUnit\ExecutionTiming;
1818

19+
use PHPUnit\Util\Color;
20+
1921
final class ExecutionTimeReportPrinter implements ExecutionTimeReportPrinterInterface
2022
{
2123
/**
2224
* @param array<int, array{name: string, time: float}> $testTimes
2325
*/
2426
public function __construct(
2527
private readonly array $testTimes,
26-
private readonly int $topN
28+
private readonly int $topN,
29+
private readonly float $warningThreshold = 1.0,
30+
private readonly float $dangerThreshold = 5.0
2731
) {
2832
}
2933

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

120+
$color = $this->determineColor($test['time']);
121+
122+
$nameDisplay = $color !== '' ? Color::colorize($color, $nameFormatted) : $nameFormatted;
123+
$timeMsDisplay = $color !== '' ? Color::colorize($color, sprintf('%.2f ms', $timeMs)) : sprintf('%.2f ms', $timeMs);
124+
$timeSecDisplay = $color !== '' ? Color::colorize($color, sprintf('(%.3f s)', $timeSec)) : sprintf('(%.3f s)', $timeSec);
125+
116126
printf(
117-
" %s. %s : %.2f ms (%.3f s)" . PHP_EOL,
127+
" %s. %s : %s %s" . PHP_EOL,
118128
$rankFormatted,
119-
$nameFormatted,
120-
$timeMs,
121-
$timeSec
129+
$nameDisplay,
130+
$timeMsDisplay,
131+
$timeSecDisplay
122132
);
123133
}
124134

135+
private function determineColor(float $time): string
136+
{
137+
if ($time >= $this->dangerThreshold) {
138+
return 'fg-red';
139+
}
140+
141+
if ($time >= $this->warningThreshold) {
142+
return 'fg-yellow';
143+
}
144+
145+
return '';
146+
}
147+
125148
private function formatRank(int $rank, int $width): string
126149
{
127150
return str_pad(

tests/Unit/ExecutionTimeExtensionTest.php

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
use Phauthentic\PHPUnit\ExecutionTiming\ExecutionTimeExtension;
2020
use PHPUnit\Framework\TestCase;
21+
use PHPUnit\Runner\Extension\ParameterCollection;
2122

2223
final class ExecutionTimeExtensionTest extends TestCase
2324
{
@@ -51,6 +52,79 @@ public function testDefaultShowIndividualTimingsIsFalse(): void
5152
$this->assertFalse($property->getValue($this->extension));
5253
}
5354

55+
public function testDefaultWarningThresholdIsOneSecond(): void
56+
{
57+
$reflection = new \ReflectionClass($this->extension);
58+
$property = $reflection->getProperty('warningThreshold');
59+
$property->setAccessible(true);
60+
61+
$this->assertEquals(1.0, $property->getValue($this->extension));
62+
}
63+
64+
public function testDefaultDangerThresholdIsFiveSeconds(): void
65+
{
66+
$reflection = new \ReflectionClass($this->extension);
67+
$property = $reflection->getProperty('dangerThreshold');
68+
$property->setAccessible(true);
69+
70+
$this->assertEquals(5.0, $property->getValue($this->extension));
71+
}
72+
73+
public function testExtractConfigurationFromParametersWithWarningThreshold(): void
74+
{
75+
$parameters = ParameterCollection::fromArray([
76+
'warningThreshold' => '2.5',
77+
]);
78+
79+
$reflection = new \ReflectionClass($this->extension);
80+
$method = $reflection->getMethod('extractConfigurationFromParameters');
81+
$method->setAccessible(true);
82+
$method->invoke($this->extension, $parameters);
83+
84+
$property = $reflection->getProperty('warningThreshold');
85+
$property->setAccessible(true);
86+
87+
$this->assertEquals(2.5, $property->getValue($this->extension));
88+
}
89+
90+
public function testExtractConfigurationFromParametersWithDangerThreshold(): void
91+
{
92+
$parameters = ParameterCollection::fromArray([
93+
'dangerThreshold' => '10.0',
94+
]);
95+
96+
$reflection = new \ReflectionClass($this->extension);
97+
$method = $reflection->getMethod('extractConfigurationFromParameters');
98+
$method->setAccessible(true);
99+
$method->invoke($this->extension, $parameters);
100+
101+
$property = $reflection->getProperty('dangerThreshold');
102+
$property->setAccessible(true);
103+
104+
$this->assertEquals(10.0, $property->getValue($this->extension));
105+
}
106+
107+
public function testExtractConfigurationFromParametersWithBothThresholds(): void
108+
{
109+
$parameters = ParameterCollection::fromArray([
110+
'warningThreshold' => '1.5',
111+
'dangerThreshold' => '7.5',
112+
]);
113+
114+
$reflection = new \ReflectionClass($this->extension);
115+
$method = $reflection->getMethod('extractConfigurationFromParameters');
116+
$method->setAccessible(true);
117+
$method->invoke($this->extension, $parameters);
118+
119+
$warningProperty = $reflection->getProperty('warningThreshold');
120+
$warningProperty->setAccessible(true);
121+
$dangerProperty = $reflection->getProperty('dangerThreshold');
122+
$dangerProperty->setAccessible(true);
123+
124+
$this->assertEquals(1.5, $warningProperty->getValue($this->extension));
125+
$this->assertEquals(7.5, $dangerProperty->getValue($this->extension));
126+
}
127+
54128
public function testOnExecutionFinishedWithNoTests(): void
55129
{
56130
ob_start();

tests/Unit/ExecutionTimeReportPrinterTest.php

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,4 +155,120 @@ public function testPrintFormatsTimeCorrectly(): void
155155
$this->assertStringContainsString('12345.00 ms', $output);
156156
$this->assertStringContainsString('12.345 s', $output);
157157
}
158+
159+
public function testPrintDoesNotColorTestBelowThreshold(): void
160+
{
161+
$testTimes = [
162+
['name' => 'FastTest', 'time' => 0.5],
163+
];
164+
$printer = new ExecutionTimeReportPrinter($testTimes, 10, 1.0, 5.0);
165+
166+
ob_start();
167+
$printer->print();
168+
$output = ob_get_clean() ?: '';
169+
170+
$this->assertStringContainsString('FastTest', $output);
171+
// Should not contain ANSI color codes
172+
$this->assertStringNotContainsString("\x1b[", $output);
173+
}
174+
175+
public function testPrintColorsTestWithWarningThreshold(): void
176+
{
177+
$testTimes = [
178+
['name' => 'WarningTest', 'time' => 1.5],
179+
];
180+
$printer = new ExecutionTimeReportPrinter($testTimes, 10, 1.0, 5.0);
181+
182+
ob_start();
183+
$printer->print();
184+
$output = ob_get_clean() ?: '';
185+
186+
$this->assertStringContainsString('WarningTest', $output);
187+
// Should contain yellow ANSI color code (fg-yellow = 33)
188+
$this->assertStringContainsString("\x1b[33m", $output);
189+
// Should not contain red color code
190+
$this->assertStringNotContainsString("\x1b[31m", $output);
191+
}
192+
193+
public function testPrintColorsTestWithDangerThreshold(): void
194+
{
195+
$testTimes = [
196+
['name' => 'DangerTest', 'time' => 6.0],
197+
];
198+
$printer = new ExecutionTimeReportPrinter($testTimes, 10, 1.0, 5.0);
199+
200+
ob_start();
201+
$printer->print();
202+
$output = ob_get_clean() ?: '';
203+
204+
$this->assertStringContainsString('DangerTest', $output);
205+
// Should contain red ANSI color code (fg-red = 31)
206+
$this->assertStringContainsString("\x1b[31m", $output);
207+
}
208+
209+
public function testPrintColorsCorrectlyWithMultipleThresholds(): void
210+
{
211+
$testTimes = [
212+
['name' => 'FastTest', 'time' => 0.5],
213+
['name' => 'WarningTest', 'time' => 2.0],
214+
['name' => 'DangerTest', 'time' => 6.0],
215+
];
216+
$printer = new ExecutionTimeReportPrinter($testTimes, 10, 1.0, 5.0);
217+
218+
ob_start();
219+
$printer->print();
220+
$output = ob_get_clean() ?: '';
221+
222+
// FastTest should not be colored
223+
$fastTestPos = strpos($output, 'FastTest');
224+
$this->assertNotFalse($fastTestPos);
225+
$fastTestLine = substr($output, $fastTestPos, 100);
226+
$this->assertStringNotContainsString("\x1b[", $fastTestLine);
227+
228+
// WarningTest should be yellow
229+
$warningTestPos = strpos($output, 'WarningTest');
230+
$this->assertNotFalse($warningTestPos);
231+
$warningTestLine = substr($output, $warningTestPos, 200);
232+
$this->assertStringContainsString("\x1b[33m", $warningTestLine);
233+
$this->assertStringNotContainsString("\x1b[31m", $warningTestLine);
234+
235+
// DangerTest should be red
236+
$dangerTestPos = strpos($output, 'DangerTest');
237+
$this->assertNotFalse($dangerTestPos);
238+
$dangerTestLine = substr($output, $dangerTestPos, 200);
239+
$this->assertStringContainsString("\x1b[31m", $dangerTestLine);
240+
}
241+
242+
public function testPrintRespectsThresholdConfiguration(): void
243+
{
244+
$testTimes = [
245+
['name' => 'Test1', 'time' => 0.8],
246+
['name' => 'Test2', 'time' => 1.2],
247+
['name' => 'Test3', 'time' => 3.0],
248+
];
249+
// Custom thresholds: warning at 1.0, danger at 2.0
250+
$printer = new ExecutionTimeReportPrinter($testTimes, 10, 1.0, 2.0);
251+
252+
ob_start();
253+
$printer->print();
254+
$output = ob_get_clean() ?: '';
255+
256+
// Test1 (0.8s) should not be colored
257+
$test1Pos = strpos($output, 'Test1');
258+
$this->assertNotFalse($test1Pos);
259+
$test1Line = substr($output, $test1Pos, 100);
260+
$this->assertStringNotContainsString("\x1b[", $test1Line);
261+
262+
// Test2 (1.2s) should be yellow (>= 1.0 but < 2.0)
263+
$test2Pos = strpos($output, 'Test2');
264+
$this->assertNotFalse($test2Pos);
265+
$test2Line = substr($output, $test2Pos, 200);
266+
$this->assertStringContainsString("\x1b[33m", $test2Line);
267+
268+
// Test3 (3.0s) should be red (>= 2.0)
269+
$test3Pos = strpos($output, 'Test3');
270+
$this->assertNotFalse($test3Pos);
271+
$test3Line = substr($output, $test3Pos, 200);
272+
$this->assertStringContainsString("\x1b[31m", $test3Line);
273+
}
158274
}

0 commit comments

Comments
 (0)