From 706bf1719db256dc62c779e0607006464bb3ab0b Mon Sep 17 00:00:00 2001 From: Simon Gilli <25326036+gilbertsoft@users.noreply.github.com> Date: Fri, 10 Jun 2022 16:17:55 +0200 Subject: [PATCH] [TASK] Refactor Composer scripts --- src/Composer/Scripts.php | 222 +++++++++++++++++------ src/Extensions/ExtensionManager.php | 3 +- tests/unit/Composer/ScriptsTest.php | 269 ++++++++++++++++++++++++++-- tests/unit/Composer/TestScripts.php | 71 ++++++++ tests/unit/TestCase.php | 10 ++ 5 files changed, 506 insertions(+), 69 deletions(-) create mode 100644 tests/unit/Composer/TestScripts.php diff --git a/src/Composer/Scripts.php b/src/Composer/Scripts.php index 3c6af65..964d830 100644 --- a/src/Composer/Scripts.php +++ b/src/Composer/Scripts.php @@ -27,60 +27,158 @@ use Composer\Script\Event; use Composer\Semver\VersionParser; +use Composer\Util\Filesystem; +use Composer\Util\Platform; +use InvalidArgumentException; use RuntimeException; +use Throwable; use UnexpectedValueException; /** - * @internal + * @noRector \Rector\Privatization\Rector\Class_\FinalizeClassesWithoutChildrenRector */ -final class Scripts +class Scripts { + private static ?Filesystem $filesystem = null; + + protected static function getFilesystem(): Filesystem + { + if (self::$filesystem === null) { + self::$filesystem = new Filesystem(); + } + + return self::$filesystem; + } + + /** + * @param bool $forceConfig Forces to read the path from the config instead of cwd. + */ + protected static function getRootPath(Event $event, bool $forceConfig = false): string + { + if (!$forceConfig) { + // @todo replace with Platform::getCwd(true) once Composer lower 2.3 is not supported anymore + // return Platform::getCwd(true); + if (($cwd = getcwd()) === false) { + return ''; + } + + return $cwd; + } + + return dirname($event->getComposer()->getConfig()->getConfigSource()->getName()); + } + /** * @throws UnexpectedValueException */ - private static function extractVersions(string $rawVersion, string &$version, string &$branchVersion): void + protected static function getAbsoluteFilename(Event $event, string $filename): string { - if ($rawVersion === '') { + if (self::getFilesystem()->isAbsolutePath($filename)) { throw new UnexpectedValueException( - 'A valid version number must be provided as argument e.g. `composer set-version 1.2.3`.', + sprintf( + 'The parameter filename should be relative to the root composer.json, "%s" was given.', + $filename + ), + 1_654_777_710 + ); + } + + return self::getRootPath($event) . '/' . $filename; + } + + /** + * @throws RuntimeException + */ + protected static function fileGetContents(string $filename): string + { + try { + if (($content = file_get_contents($filename)) === false) { + // @codeCoverageIgnoreStart + throw new RuntimeException(); + // @codeCoverageIgnoreEnd + } + + return $content; + } catch (Throwable $throwable) { + throw new RuntimeException( + sprintf('Failed to read file "%s".', $filename), + 1_654_777_708, + $throwable + ); + } + } + + /** + * @throws RuntimeException + */ + protected static function filePutContents(string $filename, string $content): void + { + try { + if (file_put_contents($filename, $content) === false) { + // @codeCoverageIgnoreStart + throw new RuntimeException(); + // @codeCoverageIgnoreEnd + } + } catch (Throwable $throwable) { + throw new RuntimeException( + sprintf('Failed to write file "%s".', $filename), + 1_654_777_709, + $throwable + ); + } + } + + /** + * @throws InvalidArgumentException + * @throws UnexpectedValueException + */ + protected static function extractVersions(string $rawVersion, string &$version, string &$branchVersion): void + { + if ($rawVersion === '') { + throw new InvalidArgumentException( + 'The parameter rawVersion must be not be empty.', 1_654_777_706 ); } $normalizedVersion = (new VersionParser())->normalize($rawVersion); - if (preg_match('#^(\d+)\.(\d+)\.(\d+)#', $normalizedVersion, $matches) === false) { - // @codeCoverageIgnoreStart + if (preg_match('#^(\d+)\.(\d+)\.(\d+)#', $normalizedVersion, $matches) === false || count($matches) !== 4) { throw new UnexpectedValueException(sprintf('"%s" is no valid version number.', $rawVersion), 1_654_777_707); - // @codeCoverageIgnoreEnd } $version = sprintf('%d.%d.%d', $matches[1], $matches[2], $matches[3]); - $branchVersion = sprintf('%d.%d.x-dev', $matches[1], $matches[2]); + $branchVersion = sprintf('%d.%d', $matches[1], $matches[2]); } /** + * @param string $filename File name relative to the root composer.json. * @throws RuntimeException + * @throws UnexpectedValueException */ - private static function replaceVersion(string $filename, string $pattern, string $version): void + protected static function replaceVersion(Event $event, string $filename, string $pattern, string $version): void { - if (($currentContent = file_get_contents($filename)) === false) { - // @codeCoverageIgnoreStart - throw new RuntimeException(sprintf('"%s" could not be read.', $filename), 1_654_777_708); - // @codeCoverageIgnoreEnd - } + $currentContent = self::fileGetContents($filename); - $content = preg_replace($pattern, '${1}' . $version . '${2}', $currentContent); + try { + $content = preg_replace($pattern, '${1}' . $version . '${2}', $currentContent); + + if ($content === null) { + throw new UnexpectedValueException(); + } + } catch (Throwable $throwable) { + throw new UnexpectedValueException( + sprintf('Failed to replace version in "%s" with pattern "%s".', $filename, $pattern), + 1_654_777_711, + $throwable + ); + } if ($currentContent === $content) { return; } - if (file_put_contents($filename, $content) === false) { - // @codeCoverageIgnoreStart - throw new RuntimeException(sprintf('"%s" could not be written.', $filename), 1_654_777_709); - // @codeCoverageIgnoreEnd - } + self::filePutContents($filename, $content); } /** @@ -92,42 +190,50 @@ public static function setVersion(Event $event): void $version = ''; $branchVersion = ''; - self::extractVersions($event->getArguments()[0] ?? '', $version, $branchVersion); - - self::replaceVersion( - __DIR__ . '/../../README.md', - '/("gilbertsoft\/typo3-config-handling-extensions": "\^)\d+.\d+.\d+(")/', - $version - ); - - self::replaceVersion( - __DIR__ . '/../../tests/extensions/test/composer.json', - '/("gilbertsoft\/typo3-config-handling-extensions": "\^)\d+.\d+.\d+(")/', - $version - ); - - self::replaceVersion( - __DIR__ . '/../../tests/unit/Fixtures/composer.json', - '/("gilbertsoft\/typo3-config-handling-extensions": "\^)\d+.\d+.\d+(")/', - $version - ); - - self::replaceVersion( - __DIR__ . '/../../.ddev/config.yaml', - '/(- COMPOSER_ROOT_VERSION=)\d+.\d+.\d+()/', - $version - ); - - self::replaceVersion( - __DIR__ . '/../../.github/workflows/continuous-integration.yml', - '/(COMPOSER_ROOT_VERSION: )\d+.\d+.\d+()/', - $version - ); - - self::replaceVersion( - __DIR__ . '/../../composer.json', - '/("dev-main": ")\d+.\d+.x-dev(")/', - $branchVersion - ); + try { + self::extractVersions($event->getArguments()[0] ?? '', $version, $branchVersion); + } catch (InvalidArgumentException $invalidArgumentException) { + throw new UnexpectedValueException( + 'A valid version number must be provided as argument e.g. `composer set-version 1.2.3`.', + 1_654_777_706, + $invalidArgumentException + ); + } + + foreach ( + [ + 'README.md' => + '/("gilbertsoft\/typo3-config-handling-extensions": "\^)\d+.\d+.\d+(")/', + 'tests/extensions/test/composer.json' => + '/("gilbertsoft\/typo3-config-handling-extensions": "\^)\d+.\d+.\d+(")/', + 'tests/unit/Fixtures/composer.json' => + '/("gilbertsoft\/typo3-config-handling-extensions": "\^)\d+.\d+.\d+(")/', + '.ddev/config.yaml' => + '/(- COMPOSER_ROOT_VERSION=)\d+.\d+.\d+()/', + '.github/workflows/continuous-integration.yml' => + '/(COMPOSER_ROOT_VERSION: )\d+.\d+.\d+()/', + ] as $filename => $pattern + ) { + self::replaceVersion( + $event, + $filename, + $pattern, + $version + ); + } + + foreach ( + [ + 'composer.json' => + '/("dev-main": ")\d+.\d+(.x-dev")/', + ] as $filename => $pattern + ) { + self::replaceVersion( + $event, + $filename, + $pattern, + $branchVersion + ); + } } } diff --git a/src/Extensions/ExtensionManager.php b/src/Extensions/ExtensionManager.php index 545f64f..432c79a 100644 --- a/src/Extensions/ExtensionManager.php +++ b/src/Extensions/ExtensionManager.php @@ -26,6 +26,7 @@ namespace Gilbertsoft\TYPO3\ConfigHandling\Extensions; use RuntimeException; +use Throwable; final class ExtensionManager { @@ -35,7 +36,7 @@ public function getExtension(string $name): ExtensionInterface { try { return $this->getExtensions()[$name]; - } catch (\Throwable $throwable) { + } catch (Throwable $throwable) { throw new RuntimeException(sprintf('Extension "%s" not found.', $name), 1_654_429_424, $throwable); } } diff --git a/tests/unit/Composer/ScriptsTest.php b/tests/unit/Composer/ScriptsTest.php index c64025a..3dd4c2a 100644 --- a/tests/unit/Composer/ScriptsTest.php +++ b/tests/unit/Composer/ScriptsTest.php @@ -25,10 +25,17 @@ namespace Gilbertsoft\TYPO3\ConfigHandling\Tests\Unit\Composer; +use Composer\Composer; +use Composer\Config; +use Composer\Config\ConfigSourceInterface; +use Composer\Config\JsonConfigSource; use Composer\Script\Event; use Gilbertsoft\TYPO3\ConfigHandling\Composer\Scripts; use Gilbertsoft\TYPO3\ConfigHandling\Tests\Unit\TestCase; +use InvalidArgumentException; +use Iterator; use RuntimeException; +use UnexpectedValueException; /** * @covers \Gilbertsoft\TYPO3\ConfigHandling\Composer\Scripts @@ -47,37 +54,34 @@ private function getFileContents(string $filename): string public function testSetVersion(): void { $eventProphecy = $this->prophesize(Event::class); - $eventProphecy->getArguments()->willReturn(['v999.999.999-dev']); + $eventProphecy->getArguments()->willReturn(['1.2.3']); Scripts::setVersion($eventProphecy->reveal()); self::assertStringContainsString( - '999.999.999', + '"gilbertsoft/typo3-config-handling-extensions": "^1.2.3"', $this->getFileContents('README.md') ); self::assertStringContainsString( - '999.999.999', + '"gilbertsoft/typo3-config-handling-extensions": "^1.2.3"', $this->getFileContents('tests/extensions/test/composer.json') ); self::assertStringContainsString( - '999.999.999', + '"gilbertsoft/typo3-config-handling-extensions": "^1.2.3"', $this->getFileContents('tests/unit/Fixtures/composer.json') ); self::assertStringContainsString( - '999.999.999', + '- COMPOSER_ROOT_VERSION=1.2.3', $this->getFileContents('.ddev/config.yaml') ); self::assertStringContainsString( - '999.999.999', + 'COMPOSER_ROOT_VERSION: 1.2.3', $this->getFileContents('.github/workflows/continuous-integration.yml') ); self::assertStringContainsString( - '999.999.x-dev', + '"dev-main": "1.2.x-dev"', $this->getFileContents('composer.json') ); - - // Test early return - Scripts::setVersion($eventProphecy->reveal()); } public function testSetVersionThrowsOnMissingVersion(): void @@ -92,4 +96,249 @@ public function testSetVersionThrowsOnMissingVersion(): void Scripts::setVersion($eventProphecy->reveal()); } + + public function testGetFilesystem(): void + { + self::assertSame(TestScripts::testGetFilesystem(), TestScripts::testGetFilesystem()); + } + + public function testGetRootPath(): void + { + $jsonConfigSourceProphecy = $this->prophesize(JsonConfigSource::class); + $jsonConfigSourceProphecy->willImplement(ConfigSourceInterface::class); + $jsonConfigSourceProphecy->getName()->willReturn( + self::getComposerFilesystem()->normalizePath(__DIR__ . '/../../../composer.json') + ); + + $configProphecy = $this->prophesize(Config::class); + $configProphecy->getConfigSource()->willReturn($jsonConfigSourceProphecy->reveal()); + + $composerProphecy = $this->prophesize(Composer::class); + $composerProphecy->getConfig()->willReturn($configProphecy->reveal()); + + $eventProphecy = $this->prophesize(Event::class); + $eventProphecy->getComposer()->willReturn($composerProphecy->reveal()); + + self::assertSame( + self::getComposerFilesystem()->normalizePath(__DIR__ . '/../../..'), + TestScripts::testGetRootPath($eventProphecy->reveal(), false) + ); + + self::assertSame( + self::getComposerFilesystem()->normalizePath(__DIR__ . '/../../..'), + TestScripts::testGetRootPath($eventProphecy->reveal(), true) + ); + } + + public function testGetAbsoluteFilename(): void + { + $eventProphecy = $this->prophesize(Event::class); + + self::assertSame( + self::getComposerFilesystem()->normalizePath(__DIR__ . '/../../../test'), + TestScripts::testGetAbsoluteFilename($eventProphecy->reveal(), 'test') + ); + } + + public function testGetAbsoluteFilenameThrowsOnAbsoluteFilename(): void + { + $jsonConfigSourceProphecy = $this->prophesize(JsonConfigSource::class); + $jsonConfigSourceProphecy->willImplement(ConfigSourceInterface::class); + $jsonConfigSourceProphecy->getName()->willReturn(__DIR__ . '/../../../composer.json'); + + $configProphecy = $this->prophesize(Config::class); + $configProphecy->getConfigSource()->willReturn($jsonConfigSourceProphecy->reveal()); + + $composerProphecy = $this->prophesize(Composer::class); + $composerProphecy->getConfig()->willReturn($configProphecy->reveal()); + + $eventProphecy = $this->prophesize(Event::class); + $eventProphecy->getComposer()->willReturn($composerProphecy->reveal()); + + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionCode(1_654_777_710); + $this->expectExceptionMessage( + 'The parameter filename should be relative to the root composer.json, "/tmp/test" was given.' + ); + + TestScripts::testGetAbsoluteFilename($eventProphecy->reveal(), '/tmp/test'); + } + + public function testFileGetContents(): void + { + foreach (['README.md', 'src/Extensions.php'] as $filename) { + self::assertStringEqualsFile($filename, TestScripts::testFileGetContents($filename)); + } + } + + public function testFileGetContentsThrowsOnInvalidFile(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionCode(1_654_777_708); + $this->expectExceptionMessage( + 'Failed to read file "invalid".' + ); + + TestScripts::testFileGetContents('invalid'); + } + + public function testFilePutContents(): void + { + foreach ( + [ + 'test.txt' => 'test.txt content', + 'src/test.txt' => 'src/test.txt content', + ] as $filename => $content + ) { + self::assertFileDoesNotExist($filename); + TestScripts::testFilePutContents($filename, $content); + self::assertFileExists($filename); + self::assertStringEqualsFile($filename, $content); + } + } + + public function testFilePutContentsThrowsOnInvalidFile(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionCode(1_654_777_709); + $this->expectExceptionMessage( + 'Failed to write file "invalid/invalid".' + ); + + TestScripts::testFilePutContents('invalid/invalid', ''); + } + + /** + * @dataProvider versionProvider + */ + public function testExtractVersions( + string $rawVersion, + string $expectedVersion, + string $expectedBranchVersion + ): void { + $version = ''; + $branchVersion = ''; + + TestScripts::testExtractVersions($rawVersion, $version, $branchVersion); + + self::assertSame($expectedVersion, $version); + self::assertSame($expectedBranchVersion, $branchVersion); + } + + /** + * @return Iterator> + */ + public function versionProvider(): Iterator + { + yield 'simple version' => [ + 'rawVersion' => '1.2.3', + 'expectedVersion' => '1.2.3', + 'expectedBranchVersion' => '1.2', + ]; + yield 'sem ver with prefix' => [ + 'rawVersion' => 'v1.2.3-alpha', + 'expectedVersion' => '1.2.3', + 'expectedBranchVersion' => '1.2', + ]; + yield 'high version' => [ + 'rawVersion' => '999999.999999.999999', + 'expectedVersion' => '999999.999999.999999', + 'expectedBranchVersion' => '999999.999999', + ]; + } + + public function testExtractVersionsThrowsOnMissingVersion(): void + { + $version = ''; + $branchVersion = ''; + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionCode(1_654_777_706); + $this->expectExceptionMessage( + 'The parameter rawVersion must be not be empty.' + ); + + TestScripts::testExtractVersions('', $version, $branchVersion); + } + + public function testExtractVersionsThrowsOnInvalidVersion(): void + { + $version = ''; + $branchVersion = ''; + + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionCode(1_654_777_707); + $this->expectExceptionMessage( + '"v20100102" is no valid version number.' + ); + + TestScripts::testExtractVersions('v20100102', $version, $branchVersion); + } + + public function testReplaceVersion(): void + { + $jsonConfigSourceProphecy = $this->prophesize(JsonConfigSource::class); + $jsonConfigSourceProphecy->willImplement(ConfigSourceInterface::class); + $jsonConfigSourceProphecy->getName()->willReturn(__DIR__ . '/../../../composer.json'); + + $configProphecy = $this->prophesize(Config::class); + $configProphecy->getConfigSource()->willReturn($jsonConfigSourceProphecy->reveal()); + + $composerProphecy = $this->prophesize(Composer::class); + $composerProphecy->getConfig()->willReturn($configProphecy->reveal()); + + $eventProphecy = $this->prophesize(Event::class); + $eventProphecy->getComposer()->willReturn($composerProphecy->reveal()); + + TestScripts::testReplaceVersion( + $eventProphecy->reveal(), + 'tests/unit/Fixtures/composer.json', + '/("gilbertsoft\/typo3-config-handling-extensions": "\^)\d+.\d+.\d+(")/', + '999.888.777' + ); + + self::assertStringContainsString( + '999.888.777', + $this->getFileContents('tests/unit/Fixtures/composer.json') + ); + + // Test early return + TestScripts::testReplaceVersion( + $eventProphecy->reveal(), + 'tests/unit/Fixtures/composer.json', + '/("gilbertsoft\/typo3-config-handling-extensions": "\^)\d+.\d+.\d+(")/', + '999.888.777' + ); + } + + public function testReplaceVersionThrowsOnInvalidPattern(): void + { + /* + $jsonConfigSourceProphecy = $this->prophesize(JsonConfigSource::class); + $jsonConfigSourceProphecy->willImplement(ConfigSourceInterface::class); + $jsonConfigSourceProphecy->getName()->willReturn(__DIR__ . '/../../../composer.json'); + + $configProphecy = $this->prophesize(Config::class); + $configProphecy->getConfigSource()->willReturn($jsonConfigSourceProphecy->reveal()); + + $composerProphecy = $this->prophesize(Composer::class); + $composerProphecy->getConfig()->willReturn($configProphecy->reveal()); + */ + + $eventProphecy = $this->prophesize(Event::class); + //$eventProphecy->getComposer()->willReturn($composerProphecy->reveal()); + + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionCode(1_654_777_711); + $this->expectExceptionMessage( + 'Failed to replace version in "tests/unit/Fixtures/composer.json" with pattern "/.^*/".' + ); + + TestScripts::testReplaceVersion( + $eventProphecy->reveal(), + 'tests/unit/Fixtures/composer.json', + '/.^*/', + '' + ); + } } diff --git a/tests/unit/Composer/TestScripts.php b/tests/unit/Composer/TestScripts.php new file mode 100644 index 0000000..b251449 --- /dev/null +++ b/tests/unit/Composer/TestScripts.php @@ -0,0 +1,71 @@ +. + * + * The TYPO3 project - inspiring people to share! + */ + +namespace Gilbertsoft\TYPO3\ConfigHandling\Tests\Unit\Composer; + +use Composer\Script\Event; +use Composer\Util\Filesystem; +use Gilbertsoft\TYPO3\ConfigHandling\Composer\Scripts; + +/** + * @internal + */ +final class TestScripts extends Scripts +{ + public static function testGetFilesystem(): Filesystem + { + return self::getFilesystem(); + } + + public static function testGetRootPath(Event $event, bool $forceConfig): string + { + return self::getRootPath($event, $forceConfig); + } + + public static function testGetAbsoluteFilename(Event $event, string $filename): string + { + return self::getAbsoluteFilename($event, $filename); + } + + public static function testFileGetContents(string $filename): string + { + return self::fileGetContents($filename); + } + + public static function testFilePutContents(string $filename, string $content): void + { + self::filePutContents($filename, $content); + } + + public static function testExtractVersions(string $rawVersion, string &$version, string &$branchVersion): void + { + self::extractVersions($rawVersion, $version, $branchVersion); + } + + public static function testReplaceVersion(Event $event, string $filename, string $pattern, string $version): void + { + self::replaceVersion($event, $filename, $pattern, $version); + } +} diff --git a/tests/unit/TestCase.php b/tests/unit/TestCase.php index 607629b..922d371 100644 --- a/tests/unit/TestCase.php +++ b/tests/unit/TestCase.php @@ -25,6 +25,7 @@ namespace Gilbertsoft\TYPO3\ConfigHandling\Tests\Unit; +use Composer\Util\Filesystem as ComposerFilesystem; use PHPUnit\Framework\TestCase as BaseTestCase; use Prophecy\PhpUnit\ProphecyTrait; use RuntimeException; @@ -44,6 +45,8 @@ abstract class TestCase extends BaseTestCase private static Filesystem $filesystem; + private static ComposerFilesystem $composerFilesystem; + public static function setUpBeforeClass(): void { self::$rootPath = \dirname(__DIR__, 2); @@ -54,6 +57,8 @@ public static function setUpBeforeClass(): void self::$filesystem = new Filesystem(); //unlink(self::$testPath); self::$filesystem->mkdir(self::$testPath); + + self::$composerFilesystem = new ComposerFilesystem(); } protected function setUp(): void @@ -129,6 +134,11 @@ protected static function getFilesystem(): Filesystem return self::$filesystem; } + protected static function getComposerFilesystem(): ComposerFilesystem + { + return self::$composerFilesystem; + } + /** * @param array $files */