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); + } }