diff --git a/resources/schema.json b/resources/schema.json index 4fa7bd102..0059baed0 100644 --- a/resources/schema.json +++ b/resources/schema.json @@ -95,6 +95,10 @@ "github": { "type": "boolean", "description": "GitHub Annotations for escaped Mutants in the added/modifies files" + }, + "summaryJson": { + "type": "string", + "definition": "Summary JSON log file, which contains only the statistics from the complete JSON file." } } }, diff --git a/src/Configuration/Entry/Logs.php b/src/Configuration/Entry/Logs.php index 510a2e816..22e958a2f 100644 --- a/src/Configuration/Entry/Logs.php +++ b/src/Configuration/Entry/Logs.php @@ -49,6 +49,7 @@ class Logs private ?string $perMutatorFilePath; private bool $useGitHubAnnotationsLogger; private ?StrykerConfig $strykerConfig; + private ?string $summaryJsonLogFilePath; public function __construct( ?string $textLogFilePath, @@ -58,7 +59,8 @@ public function __construct( ?string $debugLogFilePath, ?string $perMutatorFilePath, bool $useGitHubAnnotationsLogger, - ?StrykerConfig $strykerConfig + ?StrykerConfig $strykerConfig, + ?string $summaryJsonLogFilePath ) { $this->textLogFilePath = $textLogFilePath; $this->htmlLogFilePath = $htmlLogFilePath; @@ -68,6 +70,7 @@ public function __construct( $this->perMutatorFilePath = $perMutatorFilePath; $this->useGitHubAnnotationsLogger = $useGitHubAnnotationsLogger; $this->strykerConfig = $strykerConfig; + $this->summaryJsonLogFilePath = $summaryJsonLogFilePath; } public static function createEmpty(): self @@ -80,6 +83,7 @@ public static function createEmpty(): self null, null, false, + null, null ); } @@ -133,4 +137,9 @@ public function getStrykerConfig(): ?StrykerConfig { return $this->strykerConfig; } + + public function getSummaryJsonLogFilePath(): ?string + { + return $this->summaryJsonLogFilePath; + } } diff --git a/src/Configuration/Schema/SchemaConfigurationFactory.php b/src/Configuration/Schema/SchemaConfigurationFactory.php index fa3fda17e..c26c2390f 100644 --- a/src/Configuration/Schema/SchemaConfigurationFactory.php +++ b/src/Configuration/Schema/SchemaConfigurationFactory.php @@ -88,7 +88,8 @@ private static function createLogs(stdClass $logs): Logs self::normalizeString($logs->debug ?? null), self::normalizeString($logs->perMutator ?? null), $logs->github ?? false, - self::createStrykerConfig($logs->stryker ?? null) + self::createStrykerConfig($logs->stryker ?? null), + self::normalizeString($logs->summaryJson ?? null), ); } diff --git a/src/Logger/FileLoggerFactory.php b/src/Logger/FileLoggerFactory.php index 51554e808..0f4bf0586 100644 --- a/src/Logger/FileLoggerFactory.php +++ b/src/Logger/FileLoggerFactory.php @@ -90,6 +90,8 @@ private function createLineLoggers(Logs $logConfig): iterable yield $logConfig->getPerMutatorFilePath() => $this->createPerMutatorLogger(); + yield $logConfig->getSummaryJsonLogFilePath() => $this->createSummaryJsonLogger(); + if ($logConfig->getUseGitHubAnnotationsLogger()) { yield GitHubAnnotationsLogger::DEFAULT_OUTPUT => $this->createGitHubAnnotationsLogger(); } @@ -157,4 +159,9 @@ private function createPerMutatorLogger(): LineMutationTestingResultsLogger $this->resultsCollector ); } + + private function createSummaryJsonLogger(): LineMutationTestingResultsLogger + { + return new SummaryJsonLogger($this->metricsCalculator); + } } diff --git a/src/Logger/SummaryJsonLogger.php b/src/Logger/SummaryJsonLogger.php new file mode 100644 index 000000000..bb278a150 --- /dev/null +++ b/src/Logger/SummaryJsonLogger.php @@ -0,0 +1,75 @@ + [ + 'totalMutantsCount' => $this->metricsCalculator->getTotalMutantsCount(), + 'killedCount' => $this->metricsCalculator->getKilledCount(), + 'notCoveredCount' => $this->metricsCalculator->getNotTestedCount(), + 'escapedCount' => $this->metricsCalculator->getEscapedCount(), + 'errorCount' => $this->metricsCalculator->getErrorCount(), + 'syntaxErrorCount' => $this->metricsCalculator->getSyntaxErrorCount(), + 'skippedCount' => $this->metricsCalculator->getSkippedCount(), + 'ignoredCount' => $this->metricsCalculator->getIgnoredCount(), + 'timeOutCount' => $this->metricsCalculator->getTimedOutCount(), + 'msi' => $this->metricsCalculator->getMutationScoreIndicator(), + 'mutationCodeCoverage' => $this->metricsCalculator->getCoverageRate(), + 'coveredCodeMsi' => $this->metricsCalculator->getCoveredCodeMutationScoreIndicator(), + ], + ]; + + return [json_encode($data, JSON_THROW_ON_ERROR)]; + } +} diff --git a/tests/phpunit/Configuration/ConfigurationAssertions.php b/tests/phpunit/Configuration/ConfigurationAssertions.php index 356801177..6bbbf8d32 100644 --- a/tests/phpunit/Configuration/ConfigurationAssertions.php +++ b/tests/phpunit/Configuration/ConfigurationAssertions.php @@ -105,7 +105,8 @@ private function assertConfigurationStateIs( $expectedLogs->getDebugLogFilePath(), $expectedLogs->getPerMutatorFilePath(), $expectedLogs->getUseGitHubAnnotationsLogger(), - $expectedLogs->getStrykerConfig() + $expectedLogs->getStrykerConfig(), + $expectedLogs->getSummaryJsonLogFilePath() ); $this->assertSame($expectedLogVerbosity, $configuration->getLogVerbosity()); $this->assertSame($expectedTmpDir, $configuration->getTmpDir()); diff --git a/tests/phpunit/Configuration/ConfigurationFactoryTest.php b/tests/phpunit/Configuration/ConfigurationFactoryTest.php index 44869725d..1d5c78400 100644 --- a/tests/phpunit/Configuration/ConfigurationFactoryTest.php +++ b/tests/phpunit/Configuration/ConfigurationFactoryTest.php @@ -806,7 +806,8 @@ public function valueProvider(): iterable 'debug.log', 'mutator.log', true, - StrykerConfig::forFullReport('master') + StrykerConfig::forFullReport('master'), + 'summary.json' ), 'config/tmp', new PhpUnit( @@ -862,7 +863,8 @@ public function valueProvider(): iterable 'debug.log', 'mutator.log', true, - StrykerConfig::forFullReport('master') + StrykerConfig::forFullReport('master'), + 'summary.json' ), 'none', '/path/to/config/tmp/infection', @@ -1290,6 +1292,7 @@ private static function createValueForGithubActionsDetected( null, $useGitHubAnnotationsLogger, null, + null, ); return [ @@ -2079,6 +2082,7 @@ private static function createValueForHtmlLogFilePath(?string $htmlFileLogPathIn null, true, null, + null, ); return [ @@ -2097,6 +2101,7 @@ private static function createValueForHtmlLogFilePath(?string $htmlFileLogPathIn null, false, null, + null, ), '', new PhpUnit(null, null), diff --git a/tests/phpunit/Configuration/ConfigurationTest.php b/tests/phpunit/Configuration/ConfigurationTest.php index 5c17252f6..d726e4ad5 100644 --- a/tests/phpunit/Configuration/ConfigurationTest.php +++ b/tests/phpunit/Configuration/ConfigurationTest.php @@ -215,7 +215,8 @@ public function valueProvider(): iterable 'debug.log', 'mutator.log', true, - StrykerConfig::forBadge('master') + StrykerConfig::forBadge('master'), + 'summary.json' ), 'default', 'custom-dir', diff --git a/tests/phpunit/Configuration/Entry/LogsAssertions.php b/tests/phpunit/Configuration/Entry/LogsAssertions.php index 549cfe05b..27b00a221 100644 --- a/tests/phpunit/Configuration/Entry/LogsAssertions.php +++ b/tests/phpunit/Configuration/Entry/LogsAssertions.php @@ -49,7 +49,8 @@ private function assertLogsStateIs( ?string $expectedDebugLogFilePath, ?string $expectedPerMutatorFilePath, bool $expectedUseGitHubAnnotationsLogger, - ?StrykerConfig $expectedStrykerConfig + ?StrykerConfig $expectedStrykerConfig, + ?string $expectedSummaryJsonLogFilePath, ): void { $this->assertSame($expectedTextLogFilePath, $logs->getTextLogFilePath()); $this->assertSame($expectedHtmlLogFilePath, $logs->getHtmlLogFilePath()); @@ -58,6 +59,7 @@ private function assertLogsStateIs( $this->assertSame($expectedDebugLogFilePath, $logs->getDebugLogFilePath()); $this->assertSame($expectedPerMutatorFilePath, $logs->getPerMutatorFilePath()); $this->assertSame($expectedUseGitHubAnnotationsLogger, $logs->getUseGitHubAnnotationsLogger(), 'Use GithubAnnotationLogger is incorrect'); + $this->assertSame($expectedSummaryJsonLogFilePath, $logs->getSummaryJsonLogFilePath()); $strykerConfig = $logs->getStrykerConfig(); diff --git a/tests/phpunit/Configuration/Entry/LogsTest.php b/tests/phpunit/Configuration/Entry/LogsTest.php index 46eeb1b80..5c61d1f73 100644 --- a/tests/phpunit/Configuration/Entry/LogsTest.php +++ b/tests/phpunit/Configuration/Entry/LogsTest.php @@ -54,7 +54,8 @@ public function test_it_can_be_instantiated( ?string $debugLogFilePath, ?string $perMutatorFilePath, bool $useGitHubAnnotationsLogger, - ?StrykerConfig $strykerConfig + ?StrykerConfig $strykerConfig, + ?string $summaryJsonLogFilePath ): void { $logs = new Logs( $textLogFilePath, @@ -64,7 +65,8 @@ public function test_it_can_be_instantiated( $debugLogFilePath, $perMutatorFilePath, $useGitHubAnnotationsLogger, - $strykerConfig + $strykerConfig, + $summaryJsonLogFilePath ); $this->assertLogsStateIs( @@ -76,7 +78,8 @@ public function test_it_can_be_instantiated( $debugLogFilePath, $perMutatorFilePath, $useGitHubAnnotationsLogger, - $strykerConfig + $strykerConfig, + $summaryJsonLogFilePath ); } @@ -93,6 +96,7 @@ public function test_it_can_be_instantiated_without_any_values(): void null, null, false, + null, null ); } @@ -108,6 +112,7 @@ public function valuesProvider(): iterable null, false, null, + null, ]; yield 'complete' => [ @@ -119,6 +124,7 @@ public function valuesProvider(): iterable 'perMutator.log', true, StrykerConfig::forBadge('master'), + 'summary.json', ]; } } diff --git a/tests/phpunit/Configuration/Schema/SchemaConfigurationFactoryTest.php b/tests/phpunit/Configuration/Schema/SchemaConfigurationFactoryTest.php index 4ca1472b4..9e19f3c9b 100644 --- a/tests/phpunit/Configuration/Schema/SchemaConfigurationFactoryTest.php +++ b/tests/phpunit/Configuration/Schema/SchemaConfigurationFactoryTest.php @@ -232,6 +232,7 @@ public function provideRawConfig(): iterable null, null, false, + null, null ), ]), @@ -259,6 +260,7 @@ public function provideRawConfig(): iterable null, null, false, + null, null ), ]), @@ -286,6 +288,7 @@ public function provideRawConfig(): iterable null, null, false, + null, null ), ]), @@ -313,6 +316,7 @@ public function provideRawConfig(): iterable null, null, false, + null, null ), ]), @@ -340,6 +344,7 @@ public function provideRawConfig(): iterable 'debug.log', null, false, + null, null ), ]), @@ -367,6 +372,7 @@ public function provideRawConfig(): iterable null, 'perMutator.log', false, + null, null ), ]), @@ -396,7 +402,8 @@ public function provideRawConfig(): iterable null, null, false, - StrykerConfig::forBadge('master') + StrykerConfig::forBadge('master'), + null ), ]), ]; @@ -425,7 +432,8 @@ public function provideRawConfig(): iterable null, null, false, - StrykerConfig::forFullReport('master') + StrykerConfig::forFullReport('master'), + null ), ]), ]; @@ -454,7 +462,8 @@ public function provideRawConfig(): iterable null, null, false, - StrykerConfig::forBadge('/^foo$/') + StrykerConfig::forBadge('/^foo$/'), + null ), ]), ]; @@ -483,7 +492,36 @@ public function provideRawConfig(): iterable null, null, false, - StrykerConfig::forFullReport('/^foo$/') + StrykerConfig::forFullReport('/^foo$/'), + null + ), + ]), + ]; + + yield '[logs][summaryJson] nominal' => [ + <<<'JSON' +{ + "source": { + "directories": ["src"] + }, + "logs": { + "summaryJson": "summary.json" + } +} +JSON + , + self::createConfig([ + 'source' => new Source(['src'], []), + 'logs' => new Logs( + null, + null, + null, + null, + null, + null, + false, + null, + 'summary.json' ), ]), ]; @@ -504,7 +542,8 @@ public function provideRawConfig(): iterable "github": true, "stryker": { "badge": "master" - } + }, + "summaryJson": "summary.json" } } JSON @@ -519,7 +558,8 @@ public function provideRawConfig(): iterable 'debug.log', 'perMutator.log', true, - StrykerConfig::forBadge('master') + StrykerConfig::forBadge('master'), + 'summary.json' ), ]), ]; @@ -588,7 +628,8 @@ public function provideRawConfig(): iterable "github": true , "stryker": { "badge": " master " - } + }, + "summaryJson": " summary.json " } } JSON @@ -603,7 +644,8 @@ public function provideRawConfig(): iterable 'debug.log', 'perMutator.log', true, - StrykerConfig::forBadge('master') + StrykerConfig::forBadge('master'), + 'summary.json' ), ]), ]; @@ -2238,7 +2280,8 @@ public function provideRawConfig(): iterable "github": true, "stryker": { "badge": "master" - } + }, + "summaryJson": "summary.json" }, "tmpDir": "custom-tmp", "phpUnit": { @@ -2475,7 +2518,8 @@ public function provideRawConfig(): iterable 'debug.log', 'perMutator.log', true, - StrykerConfig::forBadge('master') + StrykerConfig::forBadge('master'), + 'summary.json' ), 'tmpDir' => 'custom-tmp', 'phpunit' => new PhpUnit( diff --git a/tests/phpunit/Configuration/Schema/SchemaConfigurationTest.php b/tests/phpunit/Configuration/Schema/SchemaConfigurationTest.php index d51ee48f6..cecc0aa79 100644 --- a/tests/phpunit/Configuration/Schema/SchemaConfigurationTest.php +++ b/tests/phpunit/Configuration/Schema/SchemaConfigurationTest.php @@ -127,7 +127,8 @@ public function valueProvider(): iterable 'debug.log', 'mutator.log', true, - StrykerConfig::forFullReport('master') + StrykerConfig::forFullReport('master'), + 'summary.json' ), 'path/to/tmp', new PhpUnit('dist/phpunit', 'bin/phpunit'), diff --git a/tests/phpunit/Logger/FileLoggerFactoryTest.php b/tests/phpunit/Logger/FileLoggerFactoryTest.php index b54d9bf8b..1a9233b22 100644 --- a/tests/phpunit/Logger/FileLoggerFactoryTest.php +++ b/tests/phpunit/Logger/FileLoggerFactoryTest.php @@ -51,6 +51,7 @@ use Infection\Logger\MutationTestingResultsLogger; use Infection\Logger\PerMutatorLogger; use Infection\Logger\SummaryFileLogger; +use Infection\Logger\SummaryJsonLogger; use Infection\Logger\TextFileLogger; use Infection\Metrics\MetricsCalculator; use Infection\Metrics\ResultsCollector; @@ -105,7 +106,8 @@ public function test_it_does_not_create_any_logger_for_no_verbosity_level_and_no '/a/file', '/a/file', true, - null + null, + '/a/file', ) ); @@ -146,6 +148,7 @@ public function logsProvider(): iterable null, null, false, + null, null ), [TextFileLogger::class], @@ -160,6 +163,7 @@ public function logsProvider(): iterable null, null, false, + null, null ), [HtmlFileLogger::class], @@ -174,6 +178,7 @@ public function logsProvider(): iterable null, null, false, + null, null ), [SummaryFileLogger::class], @@ -188,6 +193,7 @@ public function logsProvider(): iterable 'debug_file', null, false, + null, null ), [DebugFileLogger::class], @@ -202,6 +208,7 @@ public function logsProvider(): iterable null, null, false, + null, null ), [JsonLogger::class], @@ -216,6 +223,7 @@ public function logsProvider(): iterable null, 'per_muator', false, + null, null ), [PerMutatorLogger::class], @@ -230,11 +238,27 @@ public function logsProvider(): iterable null, null, true, + null, null ), [GitHubAnnotationsLogger::class], ]; + yield 'summary-json logger' => [ + new Logs( + null, + null, + null, + null, + null, + null, + false, + null, + 'summary-json' + ), + [SummaryJsonLogger::class], + ]; + yield 'all loggers' => [ new Logs( 'text', @@ -244,7 +268,8 @@ public function logsProvider(): iterable 'debug', 'per_mutator', true, - StrykerConfig::forBadge('branch') + StrykerConfig::forBadge('branch'), + 'summary-json' ), [ TextFileLogger::class, @@ -253,6 +278,7 @@ public function logsProvider(): iterable JsonLogger::class, DebugFileLogger::class, PerMutatorLogger::class, + SummaryJsonLogger::class, GitHubAnnotationsLogger::class, ], ]; diff --git a/tests/phpunit/Logger/StrykerLoggerFactoryTest.php b/tests/phpunit/Logger/StrykerLoggerFactoryTest.php index bd05efb63..502160dc4 100644 --- a/tests/phpunit/Logger/StrykerLoggerFactoryTest.php +++ b/tests/phpunit/Logger/StrykerLoggerFactoryTest.php @@ -70,7 +70,8 @@ public function test_it_does_not_create_any_logger_for_no_verbosity_level_and_no '/a/file', '/a/file', true, - null + null, + '/a/file', ) ); @@ -90,7 +91,8 @@ public function test_it_creates_a_stryker_logger_on_no_verbosity(): void null, null, false, - StrykerConfig::forBadge('master') + StrykerConfig::forBadge('master'), + null ) ); @@ -133,7 +135,8 @@ public function logsProvider(): iterable null, null, false, - StrykerConfig::forBadge('foo') + StrykerConfig::forBadge('foo'), + null ), StrykerLogger::class, ]; @@ -147,7 +150,8 @@ public function logsProvider(): iterable null, null, false, - StrykerConfig::forFullReport('foo') + StrykerConfig::forFullReport('foo'), + null ), StrykerLogger::class, ]; @@ -161,7 +165,8 @@ public function logsProvider(): iterable 'debug', 'per_mutator', true, - StrykerConfig::forBadge('branch') + StrykerConfig::forBadge('branch'), + 'summary_json' ), StrykerLogger::class, ]; diff --git a/tests/phpunit/Logger/SummaryJsonLoggerTest.php b/tests/phpunit/Logger/SummaryJsonLoggerTest.php new file mode 100644 index 000000000..25f172796 --- /dev/null +++ b/tests/phpunit/Logger/SummaryJsonLoggerTest.php @@ -0,0 +1,195 @@ +assertLoggedContentIs($expectedContents, $logger); + } + + public function metricsProvider(): iterable + { + yield 'no mutations; only covered' => [ + new MetricsCalculator(2), + [ + 'stats' => [ + 'totalMutantsCount' => 0, + 'killedCount' => 0, + 'notCoveredCount' => 0, + 'escapedCount' => 0, + 'errorCount' => 0, + 'syntaxErrorCount' => 0, + 'skippedCount' => 0, + 'ignoredCount' => 0, + 'timeOutCount' => 0, + 'msi' => 0, + 'mutationCodeCoverage' => 0, + 'coveredCodeMsi' => 0, + ], + ], + ]; + + yield 'all mutations; only covered' => [ + $this->createCompleteMetricsCalculator(), + [ + 'stats' => [ + 'totalMutantsCount' => 16, + 'killedCount' => 2, + 'notCoveredCount' => 2, + 'escapedCount' => 2, + 'errorCount' => 2, + 'syntaxErrorCount' => 2, + 'skippedCount' => 2, + 'ignoredCount' => 2, + 'timeOutCount' => 2, + 'msi' => 66.67, + 'mutationCodeCoverage' => 83.33, + 'coveredCodeMsi' => 80, + ], + ], + ]; + + yield 'uncovered mutations' => [ + $this->createUncoveredMetricsCalculator(), + [ + 'stats' => [ + 'totalMutantsCount' => 1, + 'killedCount' => 0, + 'notCoveredCount' => 1, + 'escapedCount' => 0, + 'errorCount' => 0, + 'syntaxErrorCount' => 0, + 'skippedCount' => 0, + 'ignoredCount' => 0, + 'timeOutCount' => 0, + 'msi' => 0, + 'mutationCodeCoverage' => 0, + 'coveredCodeMsi' => 0, + ], + ], + ]; + + yield 'Ignored mutations' => [ + $this->createIgnoredMetricsCalculator(), + [ + 'stats' => [ + 'totalMutantsCount' => 1, + 'killedCount' => 0, + 'notCoveredCount' => 0, + 'escapedCount' => 0, + 'errorCount' => 0, + 'syntaxErrorCount' => 0, + 'skippedCount' => 0, + 'ignoredCount' => 1, + 'timeOutCount' => 0, + 'msi' => 0, + 'mutationCodeCoverage' => 0, + 'coveredCodeMsi' => 0, + ], + ], + ]; + } + + private function assertLoggedContentIs(array $expectedJson, SummaryJsonLogger $logger): void + { + $this->assertSame($expectedJson, json_decode($logger->getLogLines()[0], true, JSON_THROW_ON_ERROR)); + } + + private function createUncoveredMetricsCalculator(): MetricsCalculator + { + $collector = new MetricsCalculator(2); + + $this->initUncoveredCollector($collector); + + return $collector; + } + + private function initUncoveredCollector(Collector $collector): void + { + $collector->collect( + $this->createMutantExecutionResult( + 0, + For_::class, + DetectionStatus::NOT_COVERED, + 'uncovered#0' + ), + ); + } + + private function createIgnoredMetricsCalculator(): MetricsCalculator + { + $collector = new MetricsCalculator(2); + + $this->initIgnoredCollector($collector); + + return $collector; + } + + private function initIgnoredCollector(Collector $collector): void + { + $collector->collect( + $this->createMutantExecutionResult( + 0, + For_::class, + DetectionStatus::IGNORED, + 'ignored#0' + ), + ); + } +}