diff --git a/.gitignore b/.gitignore index 238a9c6..2f94176 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /phpunit.xml /vendor /composer.lock +/.phpunit.result.cache +/.idea diff --git a/README.md b/README.md index 89349c0..c319815 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,11 @@ SpeedTrap also supports these parameters: * **slowThreshold** - Number of milliseconds when a test is considered "slow" (Default: 500ms) * **reportLength** - Number of slow tests included in the report (Default: 10 tests) * **stopOnSlow** - Stop execution upon first slow test (Default: false). Activate by setting "true". +* **reportRenderer** - Used to specify the report renderer to use (ConsoleRenderer is used by default if omitted) + * **class** the class to be used in order to render the result of SpeedTrap, for the moment 2 renderers are available + * \JohnKary\PHPUnit\Listener\Renderer\NgWarningsRenderer + * \JohnKary\PHPUnit\Listener\Renderer\ConsoleRenderer + * **options** options to pass to the renderer (this argument can be omitted if not options are needed) Each parameter is set in `phpunit.xml`: @@ -59,6 +64,23 @@ Each parameter is set in `phpunit.xml`: false + + + + \JohnKary\PHPUnit\Listener\Renderer\NgWarningsRenderer + + + + + /tmp/phpunit-speedtrap-report.json + + + /project + + + + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 5665b88..6d05c03 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,40 +1,47 @@ - - - - - tests - - - - - - - - - 500 - - - 5 - - - false - - - - - + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd" + colors="true" + bootstrap="vendor/autoload.php"> - - - src - - + + + src + + + + + tests + + + + + + + + 500 + + + 5 + + + false + + + + + \JohnKary\PHPUnit\Listener\Renderer\ConsoleRenderer + + + + + + + + + + diff --git a/src/Renderer/ConsoleRenderer.php b/src/Renderer/ConsoleRenderer.php new file mode 100644 index 0000000..fdb0aac --- /dev/null +++ b/src/Renderer/ConsoleRenderer.php @@ -0,0 +1,57 @@ +speedTrapReport = $speedTrapReport; + } + + /** + * @see ReportRendererInterface::renderHeader + */ + public function renderHeader(): void + { + echo sprintf( + "\n\nYou should really speed up these slow tests (>%sms)...\n", + $this->speedTrapReport->getSlowThreshold() + ); + } + + /** + * @see ReportRendererInterface::renderBody + */ + public function renderBody(): void + { + $slowTests = $this->speedTrapReport->getSlow(); + + $length = $this->speedTrapReport->getReportLength(); + for ($i = 1; $i <= $length; ++$i) { + [$testCase, $time] = array_shift($slowTests); + $label = sprintf('%s::%s', addslashes(get_class($testCase)), $testCase->getName()); + + echo sprintf(" %s. %sms to run %s\n", $i, $time, $label); + } + } + + /** + * @see ReportRendererInterface::renderFooter + */ + public function renderFooter(): void + { + if ($hidden = $this->speedTrapReport->getHiddenCount()) { + printf("...and there %s %s more above your threshold hidden from view\n", $hidden == 1 ? 'is' : 'are', $hidden); + } + } +} diff --git a/src/Renderer/ReportRendererInterface.php b/src/Renderer/ReportRendererInterface.php new file mode 100644 index 0000000..9595051 --- /dev/null +++ b/src/Renderer/ReportRendererInterface.php @@ -0,0 +1,31 @@ +speedTrapReport = $speedTrapReport; + $this->data = []; + if ( + !isset($options['file']) + || !is_string($options['file']) + ) { + throw new Exception( + 'WarningsNgRenderer - invalid filepath provided' + ); + } + if (!is_writable(dirname($options['file']))) + { + throw new Exception( + 'WarningsNgRenderer - parent directory should be writable ' + . $options['file'] + ); + } + $this->targetFile = $options['file']; + $this->projectBaseDir = $options['projectBaseDir'] ?? false; + if ($this->projectBaseDir) { + if (is_dir($this->projectBaseDir)) { + $this->projectBaseDir = realpath($this->projectBaseDir); + } else { + throw new Exception( + 'WarningsNgRenderer - project base directory should exist' + .$options['file'] + ); + } + } + } + + /** + * @see ReportRendererInterface::renderHeader + */ + public function renderHeader(): void + { + $this->data['_class'] = 'io.jenkins.plugins.analysis.core.restapi.ReportApi'; + $this->data['issues'] = []; + } + + /** + * @see ReportRendererInterface::renderBody + */ + public function renderBody(): void + { + $slowTests = $this->speedTrapReport->getSlow(); + + $length = count($slowTests); + $this->data['size'] = $length; + for ($i = 1; $i <= $length; ++$i) { + /** @var TestCase $testCase */ + [$testCase, $time] = array_shift($slowTests); + $label = sprintf("%sms to run %s", $time, $testCase->getName(true)); + + $testCaseClass = new \ReflectionClass(get_class($testCase)); + $fileName = $testCaseClass->getFileName(); + if ($this->projectBaseDir) { + $fileName = str_replace($this->projectBaseDir, '.', $fileName); + } + + $this->data['issues'][] = [ + "fileName" => $fileName, + "packageName" => $testCaseClass->getNamespaceName(), + "message" => $label, + "severity" => "HIGH", + "duration" => $time, + ]; + } + } + + /** + * @see ReportRendererInterface::renderFooter + */ + public function renderFooter(): void + { + // write the file + file_put_contents($this->targetFile, json_encode($this->data, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES)); + } + +} diff --git a/src/SpeedTrapListener.php b/src/SpeedTrapListener.php index 310631e..082d89c 100644 --- a/src/SpeedTrapListener.php +++ b/src/SpeedTrapListener.php @@ -3,6 +3,9 @@ namespace JohnKary\PHPUnit\Listener; +use Exception; +use JohnKary\PHPUnit\Listener\Renderer\ConsoleRenderer; +use JohnKary\PHPUnit\Listener\Renderer\ReportRendererInterface; use PHPUnit\Framework\{TestListener, TestListenerDefaultImplementation, TestSuite, Test, TestCase}; use PHPUnit\Util\Test as TestUtil; @@ -41,13 +44,6 @@ class SpeedTrapListener implements TestListener */ protected $slowThreshold; - /** - * Number of tests to print in slowness report. - * - * @var int - */ - protected $reportLength; - /** * Whether the test runner should halt running additional tests after * finding a slow test. @@ -56,16 +52,24 @@ class SpeedTrapListener implements TestListener */ protected $stopOnSlow; + + /** + * @var SpeedTrapReport instance responsible to grab statistics + */ + protected $report; + /** - * Collection of slow tests. - * Keys (string) => Printable label describing the test - * Values (int) => Test execution time, in milliseconds + * @var ReportRendererInterface instance responsible to render report statistics */ - protected $slow = []; + protected $reportRenderer; + + /** + * @throws Exception + */ public function __construct(array $options = []) { - $this->enabled = getenv('PHPUNIT_SPEEDTRAP') === 'disabled' ? false : true; + $this->enabled = !(getenv('PHPUNIT_SPEEDTRAP') === 'disabled'); $this->loadOptions($options); } @@ -85,7 +89,10 @@ public function endTest(Test $test, float $time): void $threshold = $this->getSlowThreshold($test); if ($this->isSlow($timeInMilliseconds, $threshold)) { - $this->addSlowTest($test, $timeInMilliseconds); + $this->report->addSlowTest($test, $timeInMilliseconds); + if ($this->stopOnSlow) { + $test->getTestResultObject()->stop(); + } } } @@ -112,12 +119,10 @@ public function endTestSuite(TestSuite $suite): void $this->suites--; - if (0 === $this->suites && $this->hasSlowTests()) { - arsort($this->slow); // Sort longest running tests to the top - - $this->renderHeader(); - $this->renderBody(); - $this->renderFooter(); + if (0 === $this->suites && $this->report->hasSlowTests()) { + $this->reportRenderer->renderHeader(); + $this->reportRenderer->renderBody(); + $this->reportRenderer->renderFooter(); } } @@ -132,28 +137,6 @@ protected function isSlow(int $time, int $slowThreshold): bool return $slowThreshold && $time >= $slowThreshold; } - /** - * Stores a test as slow. - */ - protected function addSlowTest(TestCase $test, int $time) - { - $label = $this->makeLabel($test); - - $this->slow[$label] = $time; - - if ($this->stopOnSlow) { - $test->getTestResultObject()->stop(); - } - } - - /** - * Whether at least one test has been considered slow. - */ - protected function hasSlowTests(): bool - { - return !empty($this->slow); - } - /** * Convert PHPUnit's reported test time (microseconds) to milliseconds. */ @@ -162,84 +145,39 @@ protected function toMilliseconds(float $time): int return (int) round($time * 1000); } - /** - * Label describing a slow test case. Formatted to support copy/paste with - * PHPUnit's --filter CLI option: - * - * vendor/bin/phpunit --filter 'JohnKary\\PHPUnit\\Listener\\Tests\\SomeSlowTest::testWithDataProvider with data set "Rock"' - */ - protected function makeLabel(TestCase $test): string - { - return sprintf('%s::%s', addslashes(get_class($test)), $test->getName()); - } - - /** - * Calculate number of tests to include in slowness report. - */ - protected function getReportLength(): int - { - return min(count($this->slow), $this->reportLength); - } - - /** - * Calculate number of slow tests to be hidden from the slowness report - * due to list length. - */ - protected function getHiddenCount(): int - { - $total = count($this->slow); - $showing = $this->getReportLength(); - - $hidden = 0; - if ($total > $showing) { - $hidden = $total - $showing; - } - - return $hidden; - } - - /** - * Renders slowness report header. - */ - protected function renderHeader() - { - echo sprintf("\n\nYou should really speed up these slow tests (>%sms)...\n", $this->slowThreshold); - } - - /** - * Renders slowness report body. - */ - protected function renderBody() - { - $slowTests = $this->slow; - - $length = $this->getReportLength(); - for ($i = 1; $i <= $length; ++$i) { - $label = key($slowTests); - $time = array_shift($slowTests); - - echo sprintf(" %s. %sms to run %s\n", $i, $time, $label); - } - } - - /** - * Renders slowness report footer. - */ - protected function renderFooter() - { - if ($hidden = $this->getHiddenCount()) { - printf("...and there %s %s more above your threshold hidden from view\n", $hidden == 1 ? 'is' : 'are', $hidden); - } - } - /** * Populate options into class internals. + * + * @throws Exception */ protected function loadOptions(array $options) { $this->slowThreshold = $options['slowThreshold'] ?? 500; - $this->reportLength = $options['reportLength'] ?? 10; $this->stopOnSlow = $options['stopOnSlow'] ?? false; + $this->report = new SpeedTrapReport( + $options['reportLength'] ?? 10, + $this->slowThreshold + ); + + if (isset($options['reportRenderer']) && is_array($options['reportRenderer'])) { + if (empty($options['reportRenderer']['class'])) { + throw new Exception('option reportRenderer - missing class option'); + } + $reportRendererClass = $options['reportRenderer']['class']; + if (!class_exists($reportRendererClass)) { + throw new Exception("option reportRenderer class - class $reportRendererClass does not exists"); + } + $reportRendererClassInterfaces = class_implements($reportRendererClass); + if (!isset($reportRendererClassInterfaces[ReportRendererInterface::class])) { + throw new Exception( + "option reportRenderer class - class $reportRendererClass does not implement interface " + . ReportRendererInterface::class + ); + } + $this->reportRenderer = new $reportRendererClass($this->report, $options['reportRenderer']['options'] ?? []); + } else { + $this->reportRenderer = new ConsoleRenderer($this->report, []); + } } /** diff --git a/src/SpeedTrapReport.php b/src/SpeedTrapReport.php new file mode 100644 index 0000000..d791804 --- /dev/null +++ b/src/SpeedTrapReport.php @@ -0,0 +1,103 @@ + Printable label describing the test + * Values (int) => Test execution time, in milliseconds + */ + protected $slow = []; + + + /** + * Number of tests to print in slowness report. + * + * @var int + */ + protected $reportLength; + + /** + * Test execution time (milliseconds) after which a test will be considered + * "slow" and be included in the slowness report. + * + * @var int + */ + protected $slowThreshold; + + /** + * @param int $reportLength Number of tests to print in slowness report. + */ + public function __construct(int $reportLength, int $slowThreshold) { + $this->reportLength = $reportLength; + $this->slowThreshold = $slowThreshold; + } + + /** + * Whether at least one test has been considered slow. + */ + public function hasSlowTests(): bool + { + return !empty($this->slow); + } + + /** + * @return array + */ + public function getSlow(): array + { + arsort($this->slow); // Sort longest running tests to the top + return $this->slow; + } + + /** + * Stores a test as slow. + */ + public function addSlowTest(TestCase $test, int $time): void + { + $this->slow[] = [$test, $time]; + } + + /** + * Calculate number of slow tests to be hidden from the slowness report + * due to list length. + */ + public function getHiddenCount(): int + { + $total = count($this->slow); + $showing = $this->getReportLength(); + + $hidden = 0; + if ($total > $showing) { + $hidden = $total - $showing; + } + + return $hidden; + } + + /** + * Calculate number of tests to include in slowness report. + */ + public function getReportLength(): int + { + if ($this->reportLength === -1) { + return count($this->slow); + } + return min(count($this->slow), $this->reportLength); + } + + + /** + * @return int + */ + public function getSlowThreshold(): int + { + return $this->slowThreshold; + } +}