diff --git a/README.md b/README.md index a41b3b93..a93f8f1e 100644 --- a/README.md +++ b/README.md @@ -121,4 +121,19 @@ vendor/bin/roave-backward-compatibility-check --help ## Configuration -There are currently no configuration options available. +The file `.roave-backward-compatibility-check.json` is read from the current working directory (when it exists) and sets configuration for the command. + +It's expected to be a JSON encoded file that, optionally, contains the following properties: + +* `baseline`: list of regexes used to filter detected changes; useful to avoid detection of known BC-breaks + +**Example:** + +```json +{ + "baseline": [ + "#\\[BC\\] CHANGED: The parameter \\$a of TestArtifact\\\\TheClass\\#method()#", + "#\\[BC\\] CHANGED: The parameter \\$b of TestArtifact\\\\TheClass\\#method2()#" + ] +} +``` diff --git a/src/Command/AssertBackwardsCompatible.php b/src/Command/AssertBackwardsCompatible.php index af4ef615..48809215 100644 --- a/src/Command/AssertBackwardsCompatible.php +++ b/src/Command/AssertBackwardsCompatible.php @@ -6,6 +6,7 @@ use Psl; use Psl\Env; +use Psl\File; use Psl\Iter; use Psl\Str; use Psl\Type; @@ -34,6 +35,8 @@ final class AssertBackwardsCompatible extends Command { + private const CONFIGURATION_FILENAME = '.roave-backward-compatibility-check.json'; + /** @throws LogicException */ public function __construct( private PerformCheckoutOfRevision $git, @@ -113,7 +116,9 @@ public function execute(InputInterface $input, OutputInterface $output): int $stdErr = $output->getErrorOutput(); // @todo fix flaky assumption about the path of the source repo... - $sourceRepo = CheckedOutRepository::fromPath(Env\current_dir()); + $currentDirectory = Env\current_dir(); + + $sourceRepo = CheckedOutRepository::fromPath($currentDirectory); $fromRevision = $input->getOption('from') !== null ? $this->parseRevisionFromInput($input, $sourceRepo) @@ -125,6 +130,8 @@ public function execute(InputInterface $input, OutputInterface $output): int $toRevision = $this->parseRevision->fromStringForRepository($to, $sourceRepo); + $configuration = $this->determineConfiguration($currentDirectory, $stdErr); + $stdErr->writeln(Str\format( 'Comparing from %s to %s...', Type\string()->coerce($fromRevision), @@ -148,7 +155,7 @@ public function execute(InputInterface $input, OutputInterface $output): int $toPath->__toString(), ($this->locateDependencies)($toPath->__toString(), $includeDevelopmentDependencies), ), - ); + )->applyBaseline($configuration->baseline); $formatters = [ 'console' => new SymfonyConsoleTextFormatter($stdErr), @@ -213,4 +220,24 @@ private function determineFromRevisionFromRepository( $repository, ); } + + private function determineConfiguration( + string $currentDirectory, + OutputInterface $stdErr, + ): Configuration { + $fileName = $currentDirectory . '/' . self::CONFIGURATION_FILENAME; + + try { + $configContents = File\read($fileName); + } catch (File\Exception\InvalidArgumentException) { + return Configuration::default(); + } + + $stdErr->writeln(Str\format( + 'Using "%s" as configuration file', + Type\string()->coerce($fileName), + )); + + return Configuration::fromJson($configContents); + } } diff --git a/src/Command/Configuration.php b/src/Command/Configuration.php new file mode 100644 index 00000000..23d90c72 --- /dev/null +++ b/src/Command/Configuration.php @@ -0,0 +1,43 @@ + Type\optional(Type\vec(Type\string()))], + ), + ); + } catch (Json\Exception\DecodeException $exception) { + throw new RuntimeException( + 'It was not possible to parse the configuration', + previous: $exception, + ); + } + + $baseline = $configuration['baseline'] ?? []; + + return new self(Baseline::fromList(...$baseline)); + } +} diff --git a/test/e2e/Command/AssertBackwardsCompatibleTest.php b/test/e2e/Command/AssertBackwardsCompatibleTest.php index 5bd62902..8222755c 100644 --- a/test/e2e/Command/AssertBackwardsCompatibleTest.php +++ b/test/e2e/Command/AssertBackwardsCompatibleTest.php @@ -28,6 +28,13 @@ final class AssertBackwardsCompatibleTest extends TestCase ] } +JSON; + + private const BASELINE_CONFIGURATION = <<<'JSON' +{ + "baseline": ["#\\[BC\\] CHANGED: The parameter \\$a of TestArtifact\\\\TheClass\\#method()#"] +} + JSON; private const CLASS_VERSIONS = [ @@ -264,6 +271,21 @@ public function testWillPickLatestTaggedVersionOnNoGivenFrom(): void } } + public function testWillAllowSpecifyingBaselineConfiguration(): void + { + File\write($this->sourcesRepository . '/.roave-backward-compatibility-check.json', self::BASELINE_CONFIGURATION); + + $output = Shell\execute(__DIR__ . '/../../../bin/roave-backward-compatibility-check', [ + '--from=' . $this->versions[0], + '--to=' . $this->versions[1], + ], $this->sourcesRepository, [], Shell\ErrorOutputBehavior::Append); + + self::assertStringContainsString( + '.roave-backward-compatibility-check.json" as configuration file', + $output, + ); + } + private function tagOnVersion(string $tagName, int $version): void { Shell\execute('git', ['checkout', $this->versions[$version]], $this->sourcesRepository); diff --git a/test/unit/Command/ConfigurationTest.php b/test/unit/Command/ConfigurationTest.php new file mode 100644 index 00000000..c1957261 --- /dev/null +++ b/test/unit/Command/ConfigurationTest.php @@ -0,0 +1,82 @@ +baseline); + } + + /** @dataProvider validConfigurations */ + public function testBaselineShouldBeReadFromJsonContents( + string $jsonContents, + Baseline $expectedBaseline, + ): void { + $config = Configuration::fromJson($jsonContents); + + self::assertEquals($expectedBaseline, $config->baseline); + } + + /** @psalm-return iterable */ + public function validConfigurations(): iterable + { + yield 'empty object' => ['{}', Baseline::empty()]; + yield 'empty array' => ['[]', Baseline::empty()]; + yield 'empty baseline property' => ['{"baseline":[]}', Baseline::empty()]; + + yield 'baseline with strings' => [ + <<<'JSON' +{"baseline": ["#\\[BC\\] CHANGED: The parameter \\$a#"]} +JSON, + Baseline::fromList('#\[BC\] CHANGED: The parameter \$a#'), + ]; + + yield 'random properties are ignored' => [ + <<<'JSON' +{ + "baseline": ["#\\[BC\\] CHANGED: The parameter \\$a#"], + "random": false +} +JSON, + Baseline::fromList('#\[BC\] CHANGED: The parameter \$a#'), + ]; + } + + /** @dataProvider invalidConfigurations */ + public function testExceptionShouldBeTriggeredOnInvalidConfiguration( + string $jsonContents, + ): void { + $this->expectException(RuntimeException::class); + + Configuration::fromJson($jsonContents); + } + + /** @psalm-return iterable */ + public function invalidConfigurations(): iterable + { + yield 'empty content' => ['']; + yield 'empty string' => ['""']; + yield 'int' => ['0']; + yield 'float' => ['0.1']; + yield 'boolean' => ['false']; + yield 'baseline with string' => ['{"baseline": "this should be a list"}']; + yield 'baseline with int' => ['{"baseline": 0}']; + yield 'baseline with float' => ['{"baseline": 0.0}']; + yield 'baseline with bool' => ['{"baseline": true}']; + yield 'baseline with array of float' => ['{"baseline": [0.0]}']; + yield 'baseline with array of bool' => ['{"baseline": [false]}']; + yield 'baseline with array of object' => ['{"baseline": [{}]}']; + } +}