diff --git a/resources/schema.json b/resources/schema.json index 85e65a5ba..1082128a9 100644 --- a/resources/schema.json +++ b/resources/schema.json @@ -65,12 +65,22 @@ }, "badge": { "type": "object", + "description": "Mutation score badge for your GitHub project. If provided, infection will report results for maching branches to an upstream reporting dashboard/collector.", "additionalProperties": false, "required": ["branch"], "properties": { "branch": { "type": "string", - "description": "Mutation score badge for your GitHub project." + "description": "Mutation score badge for your GitHub project. If this value starts and ends with \"/\", it will be considered a regular expression.", + "exaples": [ + "main", + "master", + "develop", + "latest", + "/1\\.\\d+/", + "/release-.*/", + "/feature\\/.*/" + ] } } }, diff --git a/src/Configuration/Entry/Badge.php b/src/Configuration/Entry/Badge.php index 118d8377a..76a573f84 100644 --- a/src/Configuration/Entry/Badge.php +++ b/src/Configuration/Entry/Badge.php @@ -35,20 +35,46 @@ namespace Infection\Configuration\Entry; +use InvalidArgumentException; +use function preg_quote; +use Safe\Exceptions\PcreException; +use function Safe\preg_match; +use function Safe\sprintf; + /** * @internal */ final class Badge { - private string $branch; + private string $branchMatch; + /** + * @throws InvalidArgumentException when the provided $branch looks like a regular expression, but is not a valid one + */ public function __construct(string $branch) { - $this->branch = $branch; + if (preg_match('#^/.+/$#', $branch) === 0) { + $this->branchMatch = '/^' . preg_quote($branch, '/') . '$/'; + + return; + } + + try { + // Yes, the `@` is intentional. For some reason, `thecodingmachine/safe` does not suppress the warnings here + @preg_match($branch, ''); + } catch (PcreException $invalidRegex) { + throw new InvalidArgumentException( + sprintf('Provided branchMatchRegex "%s" is not a valid regex', $branch), + 0, + $invalidRegex + ); + } + + $this->branchMatch = $branch; } - public function getBranch(): string + public function applicableForBranch(string $branchName): bool { - return $this->branch; + return preg_match($this->branchMatch, $branchName) === 1; } } diff --git a/src/Logger/BadgeLogger.php b/src/Logger/BadgeLogger.php index ee6130618..cff4a95d2 100644 --- a/src/Logger/BadgeLogger.php +++ b/src/Logger/BadgeLogger.php @@ -36,6 +36,7 @@ namespace Infection\Logger; use function getenv; +use Infection\Configuration\Entry\Badge; use Infection\Environment\BuildContextResolver; use Infection\Environment\CouldNotResolveBuildContext; use Infection\Environment\CouldNotResolveStrykerApiKey; @@ -54,7 +55,7 @@ final class BadgeLogger implements MutationTestingResultsLogger private StrykerApiKeyResolver $strykerApiKeyResolver; private StrykerDashboardClient $strykerDashboardClient; private MetricsCalculator $metricsCalculator; - private string $branch; + private Badge $badge; private LoggerInterface $logger; public function __construct( @@ -62,14 +63,14 @@ public function __construct( StrykerApiKeyResolver $strykerApiKeyResolver, StrykerDashboardClient $strykerDashboardClient, MetricsCalculator $metricsCalculator, - string $branch, + Badge $badge, LoggerInterface $logger ) { $this->buildContextResolver = $buildContextResolver; $this->strykerApiKeyResolver = $strykerApiKeyResolver; $this->strykerDashboardClient = $strykerDashboardClient; $this->metricsCalculator = $metricsCalculator; - $this->branch = $branch; + $this->badge = $badge; $this->logger = $logger; } @@ -83,11 +84,12 @@ public function log(): void return; } - if ($buildContext->branch() !== $this->branch) { + $branch = $buildContext->branch(); + + if (!$this->badge->applicableForBranch($branch)) { $this->logReportWasNotSent(sprintf( - 'Expected branch "%s", found "%s"', - $this->branch, - $buildContext->branch() + 'Branch "%s" does not match expected badge configuration', + $branch )); return; @@ -106,7 +108,7 @@ public function log(): void $this->strykerDashboardClient->sendReport( 'github.com/' . $buildContext->repositorySlug(), - $buildContext->branch(), + $branch, $apiKey, $this->metricsCalculator->getMutationScoreIndicator() ); diff --git a/src/Logger/BadgeLoggerFactory.php b/src/Logger/BadgeLoggerFactory.php index fc138be0a..e8c369670 100644 --- a/src/Logger/BadgeLoggerFactory.php +++ b/src/Logger/BadgeLoggerFactory.php @@ -66,7 +66,9 @@ public function __construct( public function createFromLogEntries(Logs $logConfig): ?MutationTestingResultsLogger { - if ($logConfig->getBadge() === null) { + $badge = $logConfig->getBadge(); + + if ($badge === null) { return null; } @@ -78,7 +80,7 @@ public function createFromLogEntries(Logs $logConfig): ?MutationTestingResultsLo $this->logger ), $this->metricsCalculator, - $logConfig->getBadge()->getBranch(), + $badge, $this->logger ); } diff --git a/tests/benchmark/MutationGenerator/generate-mutations-closure.php b/tests/benchmark/MutationGenerator/generate-mutations-closure.php index 0f13ee00d..e5803b774 100644 --- a/tests/benchmark/MutationGenerator/generate-mutations-closure.php +++ b/tests/benchmark/MutationGenerator/generate-mutations-closure.php @@ -65,7 +65,8 @@ static function (SplFileInfo $fileInfo): Trace { ); $mutators = $container->getMutatorFactory()->create( - $container->getMutatorResolver()->resolve(['@default' => true]) + $container->getMutatorResolver()->resolve(['@default' => true]), + true ); $fileMutationGenerator = $container->getFileMutationGenerator(); diff --git a/tests/benchmark/Tracing/provide-traces-closure.php b/tests/benchmark/Tracing/provide-traces-closure.php index cfe4b7b0c..b6d7ed9de 100644 --- a/tests/benchmark/Tracing/provide-traces-closure.php +++ b/tests/benchmark/Tracing/provide-traces-closure.php @@ -69,7 +69,8 @@ Container::DEFAULT_DRY_RUN, Container::DEFAULT_GIT_DIFF_FILTER, Container::DEFAULT_GIT_DIFF_BASE, - Container::DEFAULT_USE_GITHUB_LOGGER + Container::DEFAULT_USE_GITHUB_LOGGER, + true ); $generateTraces = static function (?int $maxCount) use ($container): iterable { diff --git a/tests/phpunit/Configuration/Entry/BadgeAssertions.php b/tests/phpunit/Configuration/Entry/BadgeAssertions.php deleted file mode 100644 index 68c49dae7..000000000 --- a/tests/phpunit/Configuration/Entry/BadgeAssertions.php +++ /dev/null @@ -1,48 +0,0 @@ -assertSame($expectedBranch, $badge->getBranch()); - } -} diff --git a/tests/phpunit/Configuration/Entry/BadgeTest.php b/tests/phpunit/Configuration/Entry/BadgeTest.php index 884915a14..61f850f72 100644 --- a/tests/phpunit/Configuration/Entry/BadgeTest.php +++ b/tests/phpunit/Configuration/Entry/BadgeTest.php @@ -36,16 +36,54 @@ namespace Infection\Tests\Configuration\Entry; use Infection\Configuration\Entry\Badge; +use InvalidArgumentException; use PHPUnit\Framework\TestCase; final class BadgeTest extends TestCase { - use BadgeAssertions; + /** @dataProvider branch_names_to_be_matched */ + public function test_branch_match(string $branchName, string $branchMatch, bool $willMatch): void + { + $this->assertSame( + $willMatch, + (new Badge($branchMatch)) + ->applicableForBranch($branchName) + ); + } + + /** @return non-empty-list */ + public function branch_names_to_be_matched(): array + { + return [ + ['master', 'master', true], + ['main', 'main', true], + ['main', 'master', false], + ['mast', 'master', false], + ['master ', 'master', false], + [' master', 'master', false], + [' master ', 'master', false], + ['master1', 'master', false], + ['foo', '/^(foo|bar)$/', true], + ['bar', '/^(foo|bar)$/', true], + ['foobar', '/^(foo|bar)$/', false], + ['fo', '/^(foo|bar)$/', false], + ['ba', '/^(foo|bar)$/', false], + ['foo ', '/^(foo|bar)$/', false], + [' foo', '/^(foo|bar)$/', false], + ['foo1', '/^(foo|bar)$/', false], + ]; + } - public function test_it_can_be_instantiated(): void + public function test_it_rejects_invalid_regex(): void { - $badge = new Badge('master'); + try { + new Badge('/[/'); - $this->assertBadgeStateIs($badge, 'master'); + $this->fail(); + } catch (InvalidArgumentException $invalid) { + $this->assertSame('Provided branchMatchRegex "/[/" is not a valid regex', $invalid->getMessage()); + $this->assertSame(0, $invalid->getCode()); + $this->assertNotNull($invalid->getPrevious()); + } } } diff --git a/tests/phpunit/Configuration/Entry/LogsAssertions.php b/tests/phpunit/Configuration/Entry/LogsAssertions.php index bbd779165..b3c918457 100644 --- a/tests/phpunit/Configuration/Entry/LogsAssertions.php +++ b/tests/phpunit/Configuration/Entry/LogsAssertions.php @@ -40,8 +40,6 @@ trait LogsAssertions { - use BadgeAssertions; - private function assertLogsStateIs( Logs $logs, ?string $expectedTextLogFilePath, @@ -65,7 +63,8 @@ private function assertLogsStateIs( $this->assertNull($badge); } else { $this->assertNotNull($badge); - $this->assertBadgeStateIs($badge, $expectedBadge->getBranch()); + + self::assertEquals($expectedBadge, $badge); } } } diff --git a/tests/phpunit/Configuration/Schema/SchemaConfigurationFactoryTest.php b/tests/phpunit/Configuration/Schema/SchemaConfigurationFactoryTest.php index 1c54c17db..7e3e05a48 100644 --- a/tests/phpunit/Configuration/Schema/SchemaConfigurationFactoryTest.php +++ b/tests/phpunit/Configuration/Schema/SchemaConfigurationFactoryTest.php @@ -368,6 +368,34 @@ public function provideRawConfig(): iterable ]), ]; + yield '[logs][badge] regex' => [ + <<<'JSON' +{ + "source": { + "directories": ["src"] + }, + "logs": { + "badge": { + "branch": "/^foo$/" + } + } +} +JSON + , + self::createConfig([ + 'source' => new Source(['src'], []), + 'logs' => new Logs( + null, + null, + null, + null, + null, + false, + new Badge('/^foo$/') + ), + ]), + ]; + yield '[logs] nominal' => [ <<<'JSON' { @@ -426,6 +454,30 @@ public function provideRawConfig(): iterable ]), ]; + yield '[logs] empty branch match regex' => [ + <<<'JSON' +{ + "source": { + "directories": ["src"] + }, + "logs": { + "text": "", + "summary": "", + "debug": "", + "perMutator": "", + "badge": { + "branch": "" + } + } +} +JSON + , + self::createConfig([ + 'source' => new Source(['src'], []), + 'logs' => Logs::createEmpty(), + ]), + ]; + yield '[logs] empty & untrimmed strings' => [ <<<'JSON' { diff --git a/tests/phpunit/Logger/BadgeLoggerTest.php b/tests/phpunit/Logger/BadgeLoggerTest.php index a2c9abc07..52d3a0375 100644 --- a/tests/phpunit/Logger/BadgeLoggerTest.php +++ b/tests/phpunit/Logger/BadgeLoggerTest.php @@ -35,6 +35,7 @@ namespace Infection\Tests\Logger; +use Infection\Configuration\Entry\Badge; use Infection\Environment\BuildContextResolver; use Infection\Environment\StrykerApiKeyResolver; use Infection\Logger\BadgeLogger; @@ -91,7 +92,7 @@ protected function setUp(): void new StrykerApiKeyResolver(), $this->badgeApiClientMock, $this->metricsCalculatorMock, - 'master', + new Badge('master'), $this->logger ); } @@ -227,7 +228,44 @@ public function test_it_skips_logging_when_it_is_branch_not_from_config(): void [ [ LogLevel::WARNING, - 'Dashboard report has not been sent: Expected branch "master", found "foo"', + 'Dashboard report has not been sent: Branch "foo" does not match expected badge configuration', + [], + ], + ], + $this->logger->getLogs() + ); + } + + public function test_it_skips_logging_when_it_is_branch_not_from_config_regex(): void + { + $this->ciDetectorEnv->setVariables([ + 'TRAVIS' => 'true', + 'TRAVIS_PULL_REQUEST' => 'false', + 'TRAVIS_REPO_SLUG' => 'a/b', + 'TRAVIS_BRANCH' => '1.x-mismatch', + ]); + + $this->badgeApiClientMock + ->expects($this->never()) + ->method('sendReport') + ; + + $badgeLogger = new BadgeLogger( + new BuildContextResolver(CiDetector::fromEnvironment($this->ciDetectorEnv)), + new StrykerApiKeyResolver(), + $this->badgeApiClientMock, + $this->metricsCalculatorMock, + new Badge('/^\d+\\.x$/'), + $this->logger + ); + + $badgeLogger->log(); + + $this->assertSame( + [ + [ + LogLevel::WARNING, + 'Dashboard report has not been sent: Branch "1.x-mismatch" does not match expected badge configuration', [], ], ], @@ -302,6 +340,51 @@ public function test_it_sends_report_when_everything_is_ok_with_stryker_key(): v ); } + public function test_it_sends_report_when_everything_is_ok_with_stryker_key_and_matching_branch_regex(): void + { + $this->ciDetectorEnv->setVariables([ + 'TRAVIS' => 'true', + 'TRAVIS_PULL_REQUEST' => 'false', + 'TRAVIS_REPO_SLUG' => 'a/b', + 'TRAVIS_BRANCH' => '7.x', + ]); + + putenv('STRYKER_DASHBOARD_API_KEY=abc'); + + $this->badgeApiClientMock + ->expects($this->once()) + ->method('sendReport') + ->with('github.com/a/b', '7.x', 'abc', 33.3) + ; + + $this->metricsCalculatorMock + ->method('getMutationScoreIndicator') + ->willReturn(33.3) + ; + + $badgeLogger = new BadgeLogger( + new BuildContextResolver(CiDetector::fromEnvironment($this->ciDetectorEnv)), + new StrykerApiKeyResolver(), + $this->badgeApiClientMock, + $this->metricsCalculatorMock, + new Badge('/^\d+\\.x$/'), + $this->logger + ); + + $badgeLogger->log(); + + $this->assertSame( + [ + [ + LogLevel::WARNING, + 'Sending dashboard report...', + [], + ], + ], + $this->logger->getLogs() + ); + } + public function test_it_sends_report_when_everything_is_ok_with_our_key(): void { $this->ciDetectorEnv->setVariables([