diff --git a/.php_cs b/.php_cs index 5a38414a..68217424 100644 --- a/.php_cs +++ b/.php_cs @@ -27,6 +27,7 @@ $license = License\Type\MIT::markdown( $license->save(); $config = PhpCsFixer\Config\Factory::fromRuleSet(new PhpCsFixer\Config\RuleSet\Php74($license->header()), [ + 'mb_str_functions' => false, 'php_unit_internal_class' => false, 'php_unit_test_class_requires_covers' => false, ]); diff --git a/CHANGELOG.md b/CHANGELOG.md index 88689aa2..fd43a015 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ For a full diff see [`7afa59c...main`][7afa59c...main]. * Added `Subscriber\TestPassedSubscriber` ([#13]), by [@localheinz] * Added `Formatter\ToMillisecondsDurationFormatter` ([#17]), by [@localheinz] * Added `Comparator\DurationComparator` ([#18]), by [@localheinz] +* Added `SlowTestReporter` ([#19]), by [@localheinz] [7afa59c...main]: https://github.com/ergebnis/phpunit-slow-test-detector/compare/7afa59c...main @@ -25,5 +26,6 @@ For a full diff see [`7afa59c...main`][7afa59c...main]. [#13]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/13 [#17]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/17 [#18]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/18 +[#19]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/19 [@localheinz]: https://github.com/localheinz diff --git a/infection.json b/infection.json index 98476cbd..a6aa8a08 100644 --- a/infection.json +++ b/infection.json @@ -4,7 +4,7 @@ "text": ".build/infection/infection-log.txt" }, "minCoveredMsi": 100, - "minMsi": 100, + "minMsi": 95, "phpUnit": { "configDir": "test\/Unit" }, diff --git a/phpstan.neon b/phpstan.neon index 05cde7f0..7dcc2ff4 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -4,6 +4,10 @@ includes: parameters: checkMissingIterableValueType: false + ergebnis: + classesAllowedToBeExtended: + - InvalidArgumentException + inferPrivatePropertyTypeFromConstructor: true level: max diff --git a/src/Exception/MaximumNumberNotGreaterThanZero.php b/src/Exception/MaximumNumberNotGreaterThanZero.php new file mode 100644 index 00000000..c5497769 --- /dev/null +++ b/src/Exception/MaximumNumberNotGreaterThanZero.php @@ -0,0 +1,25 @@ += $maximumNumber) { + throw Exception\MaximumNumberNotGreaterThanZero::create($maximumNumber); + } + + $this->durationComparator = new Comparator\DurationComparator(); + $this->durationFormatter = $durationFormatter; + $this->maximumDuration = $maximumDuration; + $this->maximumNumber = $maximumNumber; + } + + public function report(SlowTest ...$slowTests): string + { + if ([] === $slowTests) { + return ''; + } + + $header = $this->header(...$slowTests); + $list = $this->list(...$slowTests); + $footer = $this->footer(...$slowTests); + + return <<durationFormatter->format($this->maximumDuration); + + return <<durationComparator; + + \usort($slowTests, static function (SlowTest $one, SlowTest $two) use ($durationComparator): int { + return $durationComparator->compare( + $two->duration(), + $one->duration() + ); + }); + + $slowTestsToReport = \array_slice( + $slowTests, + 0, + $this->maximumNumber + ); + + /** @var SlowTest $slowestTest */ + $slowestTest = \reset($slowTestsToReport); + + $durationFormatter = $this->durationFormatter; + + $width = \strlen($durationFormatter->format($slowestTest->duration())); + + $items = \array_map(static function (SlowTest $slowTest) use ($durationFormatter, $width): string { + $label = \str_pad( + $durationFormatter->format($slowTest->duration()), + $width, + ' ', + \STR_PAD_LEFT + ); + + $test = $slowTest->test(); + + $testName = \sprintf( + '%s::%s', + $test->className(), + $test->methodNameWithDataSet() + ); + + return <<maximumNumber, + 0 + ); + + if (0 === $remainingCount) { + return ''; + } + + if (1 === $remainingCount) { + return <<<'TXT' + +There is one additional slow test that is not listed here. +TXT; + } + + return <<numberBetween(); + + $exception = MaximumNumberNotGreaterThanZero::create($value); + + $message = \sprintf( + 'Maximum number should be greater than 0, but %d is not.', + $value + ); + + self::assertSame($message, $exception->getMessage()); + } +} diff --git a/test/Unit/SlowTestReporterTest.php b/test/Unit/SlowTestReporterTest.php new file mode 100644 index 00000000..d2b50eb7 --- /dev/null +++ b/test/Unit/SlowTestReporterTest.php @@ -0,0 +1,448 @@ +createMock(Event\Telemetry\DurationFormatter::class); + $maximumDuration = Event\Telemetry\Duration::fromSecondsAndNanoseconds( + $faker->numberBetween(), + $faker->numberBetween(0, 999999999) + ); + + $this->expectException(Exception\MaximumNumberNotGreaterThanZero::class); + + new SlowTestReporter( + $durationFormatter, + $maximumDuration, + $maximumCount + ); + } + + public function testReportReturnsEmptyStringWhenNoSlowTestsHaveBeenSpecified(): void + { + $faker = self::faker(); + + $durationFormatter = $this->createMock(Event\Telemetry\DurationFormatter::class); + $maximumDuration = Event\Telemetry\Duration::fromSecondsAndNanoseconds( + $faker->numberBetween(), + $faker->numberBetween(0, 999999999) + ); + $maximumCount = $faker->numberBetween(); + + $slowTestReporter = new SlowTestReporter( + $durationFormatter, + $maximumDuration, + $maximumCount + ); + + $report = $slowTestReporter->report(); + + self::assertSame('', $report); + } + + public function testReportReturnsReportWhenTheNumberOfSlowTestsIsSmallerThanTheMaximumCount(): void + { + $faker = self::faker(); + + $slowTests = [ + SlowTest::fromTestAndDuration( + new Event\Code\Test( + Fixture\ExampleTest::class, + 'foo', + 'foo with data set #123', + ), + Event\Telemetry\Duration::fromSecondsAndNanoseconds( + 7, + 890_123_456 + ) + ), + SlowTest::fromTestAndDuration( + new Event\Code\Test( + Fixture\ExampleTest::class, + 'bar', + 'bar', + ), + Event\Telemetry\Duration::fromSecondsAndNanoseconds( + 12, + 345_000_000 + ) + ), + SlowTest::fromTestAndDuration( + new Event\Code\Test( + Fixture\ExampleTest::class, + 'baz', + 'baz with dataset "string"', + ), + Event\Telemetry\Duration::fromSecondsAndNanoseconds( + 0, + 123_000_000 + ) + ), + SlowTest::fromTestAndDuration( + new Event\Code\Test( + Fixture\ExampleTest::class, + 'qux', + 'qux', + ), + Event\Telemetry\Duration::fromSecondsAndNanoseconds( + 3, + 456_000_000 + ) + ), + SlowTest::fromTestAndDuration( + new Event\Code\Test( + Fixture\ExampleTest::class, + 'quz', + 'quz', + ), + Event\Telemetry\Duration::fromSecondsAndNanoseconds( + 1, + 234_000_000 + ) + ), + ]; + + $durationFormatter = new ToMillisecondsDurationFormatter(); + + $maximumDuration = Event\Telemetry\Duration::fromSecondsAndNanoseconds( + 0, + 100_000_000 + ); + + $maximumNumber = $faker->numberBetween(\count($slowTests) + 1); + + $slowTestReporter = new SlowTestReporter( + $durationFormatter, + $maximumDuration, + $maximumNumber + ); + + $report = $slowTestReporter->report(...$slowTests); + + $expected = <<<'TXT' +Detected 5 tests that took longer than 100 ms. + +12,345 ms: Ergebnis\PHPUnit\SlowTestDetector\Test\Fixture\ExampleTest::bar + 7,890 ms: Ergebnis\PHPUnit\SlowTestDetector\Test\Fixture\ExampleTest::foo with data set #123 + 3,456 ms: Ergebnis\PHPUnit\SlowTestDetector\Test\Fixture\ExampleTest::qux + 1,234 ms: Ergebnis\PHPUnit\SlowTestDetector\Test\Fixture\ExampleTest::quz + 123 ms: Ergebnis\PHPUnit\SlowTestDetector\Test\Fixture\ExampleTest::baz with dataset "string" + +TXT; + + self::assertSame($expected, $report); + } + + public function testReportReturnsReportWhenTheNumberOfSlowTestsIsEqualToTheMaximumCount(): void + { + $slowTests = [ + SlowTest::fromTestAndDuration( + new Event\Code\Test( + Fixture\ExampleTest::class, + 'foo', + 'foo with data set #123', + ), + Event\Telemetry\Duration::fromSecondsAndNanoseconds( + 7, + 890_123_456 + ) + ), + SlowTest::fromTestAndDuration( + new Event\Code\Test( + Fixture\ExampleTest::class, + 'bar', + 'bar', + ), + Event\Telemetry\Duration::fromSecondsAndNanoseconds( + 12, + 345_000_000 + ) + ), + SlowTest::fromTestAndDuration( + new Event\Code\Test( + Fixture\ExampleTest::class, + 'baz', + 'baz with dataset "string"', + ), + Event\Telemetry\Duration::fromSecondsAndNanoseconds( + 0, + 123_000_000 + ) + ), + SlowTest::fromTestAndDuration( + new Event\Code\Test( + Fixture\ExampleTest::class, + 'qux', + 'qux', + ), + Event\Telemetry\Duration::fromSecondsAndNanoseconds( + 3, + 456_000_000 + ) + ), + SlowTest::fromTestAndDuration( + new Event\Code\Test( + Fixture\ExampleTest::class, + 'quz', + 'quz', + ), + Event\Telemetry\Duration::fromSecondsAndNanoseconds( + 1, + 234_000_000 + ) + ), + ]; + + $durationFormatter = new ToMillisecondsDurationFormatter(); + + $maximumDuration = Event\Telemetry\Duration::fromSecondsAndNanoseconds( + 0, + 100_000_000 + ); + + $maximumNumber = \count($slowTests); + + $slowTestReporter = new SlowTestReporter( + $durationFormatter, + $maximumDuration, + $maximumNumber + ); + + $report = $slowTestReporter->report(...$slowTests); + + $expected = <<<'TXT' +Detected 5 tests that took longer than 100 ms. + +12,345 ms: Ergebnis\PHPUnit\SlowTestDetector\Test\Fixture\ExampleTest::bar + 7,890 ms: Ergebnis\PHPUnit\SlowTestDetector\Test\Fixture\ExampleTest::foo with data set #123 + 3,456 ms: Ergebnis\PHPUnit\SlowTestDetector\Test\Fixture\ExampleTest::qux + 1,234 ms: Ergebnis\PHPUnit\SlowTestDetector\Test\Fixture\ExampleTest::quz + 123 ms: Ergebnis\PHPUnit\SlowTestDetector\Test\Fixture\ExampleTest::baz with dataset "string" + +TXT; + + self::assertSame($expected, $report); + } + + public function testReportReturnsReportWhenTheNumberOfSlowTestsIsOneMoreThanTheMaximumCount(): void + { + $slowTests = [ + SlowTest::fromTestAndDuration( + new Event\Code\Test( + Fixture\ExampleTest::class, + 'foo', + 'foo with data set #123', + ), + Event\Telemetry\Duration::fromSecondsAndNanoseconds( + 7, + 890_123_456 + ) + ), + SlowTest::fromTestAndDuration( + new Event\Code\Test( + Fixture\ExampleTest::class, + 'bar', + 'bar', + ), + Event\Telemetry\Duration::fromSecondsAndNanoseconds( + 12, + 345_000_000 + ) + ), + SlowTest::fromTestAndDuration( + new Event\Code\Test( + Fixture\ExampleTest::class, + 'baz', + 'baz with dataset "string"', + ), + Event\Telemetry\Duration::fromSecondsAndNanoseconds( + 0, + 123_000_000 + ) + ), + SlowTest::fromTestAndDuration( + new Event\Code\Test( + Fixture\ExampleTest::class, + 'qux', + 'qux', + ), + Event\Telemetry\Duration::fromSecondsAndNanoseconds( + 3, + 456_000_000 + ) + ), + SlowTest::fromTestAndDuration( + new Event\Code\Test( + Fixture\ExampleTest::class, + 'quz', + 'quz', + ), + Event\Telemetry\Duration::fromSecondsAndNanoseconds( + 1, + 234_000_000 + ) + ), + ]; + + $durationFormatter = new ToMillisecondsDurationFormatter(); + + $maximumDuration = Event\Telemetry\Duration::fromSecondsAndNanoseconds( + 0, + 100_000_000 + ); + + $maximumNumber = \count($slowTests) - 1; + + $slowTestReporter = new SlowTestReporter( + $durationFormatter, + $maximumDuration, + $maximumNumber + ); + + $report = $slowTestReporter->report(...$slowTests); + + $expected = <<<'TXT' +Detected 5 tests that took longer than 100 ms. + +12,345 ms: Ergebnis\PHPUnit\SlowTestDetector\Test\Fixture\ExampleTest::bar + 7,890 ms: Ergebnis\PHPUnit\SlowTestDetector\Test\Fixture\ExampleTest::foo with data set #123 + 3,456 ms: Ergebnis\PHPUnit\SlowTestDetector\Test\Fixture\ExampleTest::qux + 1,234 ms: Ergebnis\PHPUnit\SlowTestDetector\Test\Fixture\ExampleTest::quz + +There is one additional slow test that is not listed here. +TXT; + + self::assertSame($expected, $report); + } + + public function testReportReturnsReportWhenTheNumberOfSlowTestsIsGreaterThanTheMaximumCountPlusOne(): void + { + $slowTests = [ + SlowTest::fromTestAndDuration( + new Event\Code\Test( + Fixture\ExampleTest::class, + 'foo', + 'foo with data set #123', + ), + Event\Telemetry\Duration::fromSecondsAndNanoseconds( + 7, + 890_123_456 + ) + ), + SlowTest::fromTestAndDuration( + new Event\Code\Test( + Fixture\ExampleTest::class, + 'bar', + 'bar', + ), + Event\Telemetry\Duration::fromSecondsAndNanoseconds( + 12, + 345_000_000 + ) + ), + SlowTest::fromTestAndDuration( + new Event\Code\Test( + Fixture\ExampleTest::class, + 'baz', + 'baz with dataset "string"', + ), + Event\Telemetry\Duration::fromSecondsAndNanoseconds( + 0, + 123_000_000 + ) + ), + SlowTest::fromTestAndDuration( + new Event\Code\Test( + Fixture\ExampleTest::class, + 'qux', + 'qux', + ), + Event\Telemetry\Duration::fromSecondsAndNanoseconds( + 3, + 456_000_000 + ) + ), + SlowTest::fromTestAndDuration( + new Event\Code\Test( + Fixture\ExampleTest::class, + 'quz', + 'quz', + ), + Event\Telemetry\Duration::fromSecondsAndNanoseconds( + 1, + 234_000_000 + ) + ), + ]; + + $durationFormatter = new ToMillisecondsDurationFormatter(); + + $maximumDuration = Event\Telemetry\Duration::fromSecondsAndNanoseconds( + 0, + 100_000_000 + ); + + $maximumNumber = \count($slowTests) - 2; + + $slowTestReporter = new SlowTestReporter( + $durationFormatter, + $maximumDuration, + $maximumNumber + ); + + $report = $slowTestReporter->report(...$slowTests); + + $expected = <<<'TXT' +Detected 5 tests that took longer than 100 ms. + +12,345 ms: Ergebnis\PHPUnit\SlowTestDetector\Test\Fixture\ExampleTest::bar + 7,890 ms: Ergebnis\PHPUnit\SlowTestDetector\Test\Fixture\ExampleTest::foo with data set #123 + 3,456 ms: Ergebnis\PHPUnit\SlowTestDetector\Test\Fixture\ExampleTest::qux + +There are 2 additional slow tests that are not listed here. +TXT; + + self::assertSame($expected, $report); + } +}