diff --git a/config/services.xml b/config/services.xml index 52ca97c..6c668fb 100644 --- a/config/services.xml +++ b/config/services.xml @@ -27,6 +27,11 @@ + + + + + @@ -76,5 +81,11 @@ + + + + + + diff --git a/src/Command/Process/Action/RevertAction.php b/src/Command/Process/Action/RevertAction.php index 5c47259..3ebd64e 100644 --- a/src/Command/Process/Action/RevertAction.php +++ b/src/Command/Process/Action/RevertAction.php @@ -179,7 +179,7 @@ private function printPatchRevertingFailed(OutputInterface $output, PatchInterfa 'Reverting patch %s (%s) failed.%s', $patch->getId(), $patch->getPath(), - $this->renderer->formatErrorOutput($errorOutput) + PHP_EOL . $errorOutput ); $this->logger->error($errorMessage); diff --git a/src/Command/Process/ApplyLocal.php b/src/Command/Process/ApplyLocal.php index 3ba8ebd..6316fab 100644 --- a/src/Command/Process/ApplyLocal.php +++ b/src/Command/Process/ApplyLocal.php @@ -89,13 +89,13 @@ public function run(InputInterface $input, OutputInterface $output) $this->printInfo($output, $message); array_push($appliedPatches, $patch); } catch (ApplierException $exception) { - $this->printError($output, 'Error: patch conflict happened'); + $this->printError($output, 'Error: patch can\'t be applied'); $messages = $this->rollbackProcessor->process($appliedPatches); $output->writeln($messages); $errorMessage = sprintf( 'Applying patch %s failed.%s', $patch->getPath(), - $this->renderer->formatErrorOutput($exception->getMessage()) + PHP_EOL . $exception->getMessage() ); throw new RuntimeException($errorMessage, $exception->getCode()); diff --git a/src/Command/Process/Ece/Revert.php b/src/Command/Process/Ece/Revert.php index b2b93b4..876267f 100644 --- a/src/Command/Process/Ece/Revert.php +++ b/src/Command/Process/Ece/Revert.php @@ -121,7 +121,7 @@ function ($patch) { $errorMessage = sprintf( 'Reverting patch %s failed.%s', $patch->getPath(), - $this->renderer->formatErrorOutput($exception->getMessage()) + PHP_EOL . $exception->getMessage() ); $this->printError($output, $errorMessage); } diff --git a/src/Command/Process/Renderer.php b/src/Command/Process/Renderer.php index 319ce7b..8c39bc9 100644 --- a/src/Command/Process/Renderer.php +++ b/src/Command/Process/Renderer.php @@ -140,21 +140,6 @@ public function printPatchInfo( $output->writeln(''); } - /** - * Format error output. - * - * @param string $errorOutput - * @return string - */ - public function formatErrorOutput(string $errorOutput): string - { - if (preg_match('#^.*?Error Output:(?.*?)$#is', $errorOutput, $matches)) { - $errorOutput = PHP_EOL . 'Error Output:' . $matches['errors']; - } - - return $errorOutput; - } - /** * Asks a confirmation question to the user. * diff --git a/src/Command/Process/ShowStatus.php b/src/Command/Process/ShowStatus.php index 181366e..0ae5294 100644 --- a/src/Command/Process/ShowStatus.php +++ b/src/Command/Process/ShowStatus.php @@ -111,9 +111,14 @@ function ($patch) { */ private function printDetailsInfo(OutputInterface $output) { + $supportUrl = 'https://support.magento.com'; + $releaseNotesUrl = 'https://devdocs.magento.com/quality-patches/release-notes.html'; + $output->writeln( - 'More detailed information about patches you can find on ' . - 'https://support.magento.com' + 'Patch details you can find on ' . + sprintf('%1$s (search for patch id, ex. MDVA-30265)', $supportUrl) . + PHP_EOL . + sprintf('Release notes %1$s', $releaseNotesUrl) ); } diff --git a/src/Patch/Applier.php b/src/Patch/Applier.php index e5f2d1e..93ecfb6 100644 --- a/src/Patch/Applier.php +++ b/src/Patch/Applier.php @@ -10,8 +10,6 @@ use Magento\CloudPatches\Composer\MagentoVersion; use Magento\CloudPatches\Filesystem\Filesystem; use Magento\CloudPatches\Patch\Status\StatusPool; -use Magento\CloudPatches\Shell\ProcessFactory; -use Symfony\Component\Process\Exception\ProcessFailedException; /** * Applies and reverts patches. @@ -19,9 +17,9 @@ class Applier { /** - * @var ProcessFactory + * @var PatchCommandInterface */ - private $processFactory; + private $patchCommand; /** * @var GitConverter @@ -39,18 +37,18 @@ class Applier private $filesystem; /** - * @param ProcessFactory $processFactory + * @param PatchCommandInterface $patchCommand * @param GitConverter $gitConverter * @param MagentoVersion $magentoVersion * @param Filesystem $filesystem */ public function __construct( - ProcessFactory $processFactory, + PatchCommandInterface $patchCommand, GitConverter $gitConverter, MagentoVersion $magentoVersion, Filesystem $filesystem ) { - $this->processFactory = $processFactory; + $this->patchCommand = $patchCommand; $this->gitConverter = $gitConverter; $this->magentoVersion = $magentoVersion; $this->filesystem = $filesystem; @@ -69,13 +67,11 @@ public function apply(string $path, string $id): string { $content = $this->readContent($path); try { - $this->processFactory->create(['git', 'apply'], $content) - ->mustRun(); - } catch (ProcessFailedException $exception) { + $this->patchCommand->apply($content); + } catch (PatchCommandException $exception) { try { - $this->processFactory->create(['git', 'apply', '--check', '--reverse'], $content) - ->mustRun(); - } catch (ProcessFailedException $reverseException) { + $this->patchCommand->revertCheck($content); + } catch (PatchCommandException $reverseException) { throw new ApplierException($exception->getMessage(), $exception->getCode()); } @@ -98,13 +94,11 @@ public function revert(string $path, string $id): string { $content = $this->readContent($path); try { - $this->processFactory->create(['git', 'apply', '--reverse'], $content) - ->mustRun(); - } catch (ProcessFailedException $exception) { + $this->patchCommand->revert($content); + } catch (PatchCommandException $exception) { try { - $this->processFactory->create(['git', 'apply', '--check'], $content) - ->mustRun(); - } catch (ProcessFailedException $applyException) { + $this->patchCommand->applyCheck($content); + } catch (PatchCommandException $applyException) { throw new ApplierException($exception->getMessage(), $exception->getCode()); } @@ -124,13 +118,11 @@ public function status(string $patchContent): string { $patchContent = $this->prepareContent($patchContent); try { - $this->processFactory->create(['git', 'apply', '--check'], $patchContent) - ->mustRun(); - } catch (ProcessFailedException $exception) { + $this->patchCommand->applyCheck($patchContent); + } catch (PatchCommandException $exception) { try { - $this->processFactory->create(['git', 'apply', '--check', '--reverse'], $patchContent) - ->mustRun(); - } catch (ProcessFailedException $reverseException) { + $this->patchCommand->revertCheck($patchContent); + } catch (PatchCommandException $reverseException) { return StatusPool::NA; } @@ -150,9 +142,8 @@ public function checkApply(string $patchContent): bool { $patchContent = $this->prepareContent($patchContent); try { - $this->processFactory->create(['git', 'apply', '--check'], $patchContent) - ->mustRun(); - } catch (ProcessFailedException $exception) { + $this->patchCommand->applyCheck($patchContent); + } catch (PatchCommandException $exception) { return false; } diff --git a/src/Patch/Conflict/Processor.php b/src/Patch/Conflict/Processor.php index 2927899..68b99de 100644 --- a/src/Patch/Conflict/Processor.php +++ b/src/Patch/Conflict/Processor.php @@ -73,7 +73,7 @@ public function process( array $appliedPatches, string $exceptionMessage ) { - $errorMessage = 'Error: patch conflict happened'; + $errorMessage = 'Error: patch can\'t be applied'; $this->logger->error($errorMessage); $output->writeln('' . $errorMessage . ''); @@ -84,7 +84,7 @@ public function process( 'Applying patch %s (%s) failed.%s%s', $patch->getId(), $patch->getPath(), - $this->renderer->formatErrorOutput($exceptionMessage), + PHP_EOL . $exceptionMessage, $conflictDetails ? PHP_EOL . $conflictDetails : '' ); diff --git a/src/Patch/PatchCommand.php b/src/Patch/PatchCommand.php new file mode 100644 index 0000000..a96baf4 --- /dev/null +++ b/src/Patch/PatchCommand.php @@ -0,0 +1,89 @@ +drivers = $drivers; + } + + /** + * @inheritDoc + */ + public function apply(string $patch) + { + $this->getDriver()->apply($patch); + } + + /** + * @inheritDoc + */ + public function revert(string $patch) + { + $this->getDriver()->revert($patch); + } + + /** + * @inheritDoc + */ + public function applyCheck(string $patch) + { + $this->getDriver()->applyCheck($patch); + } + + /** + * @inheritDoc + */ + public function revertCheck(string $patch) + { + $this->getDriver()->revertCheck($patch); + } + + /** + * Returns first available driver + * + * @return DriverInterface + * @throws PatchCommandNotFound + */ + private function getDriver(): DriverInterface + { + if ($this->driver === null) { + foreach ($this->drivers as $driver) { + if ($driver->isInstalled()) { + $this->driver = $driver; + break; + } + } + if ($this->driver === null) { + throw new PatchCommandNotFound(); + } + } + return $this->driver; + } +} diff --git a/src/Patch/PatchCommandException.php b/src/Patch/PatchCommandException.php new file mode 100644 index 0000000..3bcda3d --- /dev/null +++ b/src/Patch/PatchCommandException.php @@ -0,0 +1,17 @@ +getMessage(); + if ($previous instanceof ProcessFailedException) { + $message = $previous->getProcess()->getErrorOutput() ?: ($previous->getProcess()->getOutput() ?: $message); + } + parent::__construct($message, $previous->getCode(), $previous); + } +} diff --git a/src/Shell/Command/DriverInterface.php b/src/Shell/Command/DriverInterface.php new file mode 100644 index 0000000..9ac8d86 --- /dev/null +++ b/src/Shell/Command/DriverInterface.php @@ -0,0 +1,23 @@ +processFactory = $processFactory; + } + + /** + * @inheritDoc + */ + public function apply(string $patch) + { + try { + $this->processFactory->create(['git', 'apply'], $patch) + ->mustRun(); + } catch (ProcessFailedException $exception) { + throw new DriverException($exception); + } + } + + /** + * @inheritDoc + */ + public function revert(string $patch) + { + try { + $this->processFactory->create(['git', 'apply', '--reverse'], $patch) + ->mustRun(); + } catch (ProcessFailedException $exception) { + throw new DriverException($exception); + } + } + + /** + * @inheritDoc + */ + public function applyCheck(string $patch) + { + try { + $this->processFactory->create(['git', 'apply', '--check'], $patch) + ->mustRun(); + } catch (ProcessFailedException $exception) { + throw new DriverException($exception); + } + } + + /** + * @inheritDoc + */ + public function revertCheck(string $patch) + { + try { + $this->processFactory->create(['git', 'apply', '--reverse', '--check'], $patch) + ->mustRun(); + } catch (ProcessFailedException $exception) { + throw new DriverException($exception); + } + } + + /** + * @inheritDoc + */ + public function isInstalled(): bool + { + try { + $this->processFactory->create(['git', '--version'])->mustRun(); + $result = true; + } catch (ProcessFailedException $exception) { + $result = false; + } + + return $result; + } +} diff --git a/src/Shell/Command/PatchDriver.php b/src/Shell/Command/PatchDriver.php new file mode 100644 index 0000000..b22c4cc --- /dev/null +++ b/src/Shell/Command/PatchDriver.php @@ -0,0 +1,100 @@ +processFactory = $processFactory; + } + + /** + * @inheritDoc + */ + public function apply(string $patch) + { + try { + $this->applyCheck($patch); + $this->processFactory->create(['patch', '--silent', '-p1', '--no-backup-if-mismatch'], $patch) + ->mustRun(); + } catch (ProcessFailedException $exception) { + throw new DriverException($exception); + } + } + + /** + * @inheritDoc + */ + public function revert(string $patch) + { + try { + $this->revertCheck($patch); + $this->processFactory->create(['patch', '--silent', '-p1', '--no-backup-if-mismatch', '--reverse'], $patch) + ->mustRun(); + } catch (ProcessFailedException $exception) { + throw new DriverException($exception); + } + } + + /** + * @inheritDoc + */ + public function applyCheck(string $patch) + { + try { + $this->processFactory->create(['patch', '--silent', '-p1', '--dry-run'], $patch) + ->mustRun(); + } catch (ProcessFailedException $exception) { + throw new DriverException($exception); + } + } + + /** + * @inheritDoc + */ + public function revertCheck(string $patch) + { + try { + $this->processFactory->create(['patch', '--silent', '-p1', '--reverse', '--dry-run'], $patch) + ->mustRun(); + } catch (ProcessFailedException $exception) { + throw new DriverException($exception); + } + } + + /** + * @inheritDoc + */ + public function isInstalled(): bool + { + try { + $this->processFactory->create(['patch', '--version'])->mustRun(); + $result = true; + } catch (ProcessFailedException $exception) { + $result = false; + } + + return $result; + } +} diff --git a/src/Test/Unit/Command/Process/Action/RevertActionTest.php b/src/Test/Unit/Command/Process/Action/RevertActionTest.php index 4c0e3c5..9ef6b34 100644 --- a/src/Test/Unit/Command/Process/Action/RevertActionTest.php +++ b/src/Test/Unit/Command/Process/Action/RevertActionTest.php @@ -184,9 +184,6 @@ public function testRevertWithException() $this->applier->method('revert') ->willThrowException(new ApplierException('Error')); - $this->renderer->expects($this->once()) - ->method('formatErrorOutput') - ->with('Error'); $outputMock->expects($this->once()) ->method('writeln') ->withConsecutive( diff --git a/src/Test/Unit/Command/Process/ApplyLocalTest.php b/src/Test/Unit/Command/Process/ApplyLocalTest.php index cc6490c..b9f7e85 100644 --- a/src/Test/Unit/Command/Process/ApplyLocalTest.php +++ b/src/Test/Unit/Command/Process/ApplyLocalTest.php @@ -171,9 +171,6 @@ function ($path, $title) { ->method('process') ->withConsecutive([[$patch1]]) ->willReturn($rollbackMessages); - $this->renderer->expects($this->once()) - ->method('formatErrorOutput') - ->with('Applier error message'); $this->expectException(RuntimeException::class); $this->manager->run($inputMock, $outputMock); diff --git a/src/Test/Unit/Command/Process/Ece/RevertTest.php b/src/Test/Unit/Command/Process/Ece/RevertTest.php index 82dacb0..1197ba2 100644 --- a/src/Test/Unit/Command/Process/Ece/RevertTest.php +++ b/src/Test/Unit/Command/Process/Ece/RevertTest.php @@ -165,10 +165,6 @@ function ($path, $title) { } ); - $this->renderer->expects($this->once()) - ->method('formatErrorOutput') - ->with('Applier error message'); - $this->revertAction->expects($this->once()) ->method('execute') ->withConsecutive([$inputMock, $outputMock, []]); diff --git a/src/Test/Unit/Command/Process/RendererTest.php b/src/Test/Unit/Command/Process/RendererTest.php index f78e161..6a944b4 100644 --- a/src/Test/Unit/Command/Process/RendererTest.php +++ b/src/Test/Unit/Command/Process/RendererTest.php @@ -122,53 +122,6 @@ public function printPatchInfoDataProvider(): array ]; } - /** - * Tests error output formatting. - * - * @param string $errorOutput - * @param string $expectedOutput - * @dataProvider formatErrorOutputDataProvider - */ - public function testFormatErrorOutput(string $errorOutput, string $expectedOutput) - { - $this->assertEquals($expectedOutput, $this->renderer->formatErrorOutput($errorOutput)); - } - - /** - * @return array - */ - public function formatErrorOutputDataProvider(): array - { - return [ - [ - 'error' => 'The command "\'git\' \'apply\' \'/path/to/patch/MC-1111_test_patch_1.1.1_ce.patch\'" failed. - -Exit Code: 1(General error) - -Working directory: /path/to/patch - -Output: -================ - - -Error Output: -================ -error: patch failed: vendor/magento/module-admin-analytics/Controller/Adminhtml/Config/DisableAdminUsage.php:23 -error: vendor/magento/module-admin-analytics/Controller/Adminhtml/Config/DisableAdminUsage.php: patch does not apply', - - 'expectedOutput' => ' -Error Output: -================ -error: patch failed: vendor/magento/module-admin-analytics/Controller/Adminhtml/Config/DisableAdminUsage.php:23 -error: vendor/magento/module-admin-analytics/Controller/Adminhtml/Config/DisableAdminUsage.php: patch does not apply' - ], - [ - 'error' => 'Some other output', - 'expectedOutput' => 'Some other output' - ], - ]; - } - /** * Creates patch mock. * diff --git a/src/Test/Unit/Patch/ApplierTest.php b/src/Test/Unit/Patch/ApplierTest.php index d62d0c0..cd08b37 100644 --- a/src/Test/Unit/Patch/ApplierTest.php +++ b/src/Test/Unit/Patch/ApplierTest.php @@ -12,12 +12,11 @@ use Magento\CloudPatches\Patch\Applier; use Magento\CloudPatches\Patch\ApplierException; use Magento\CloudPatches\Patch\GitConverter; +use Magento\CloudPatches\Patch\PatchCommandException; +use Magento\CloudPatches\Patch\PatchCommandInterface; use Magento\CloudPatches\Patch\Status\StatusPool; -use Magento\CloudPatches\Shell\ProcessFactory; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -use Symfony\Component\Process\Exception\ProcessFailedException; -use Symfony\Component\Process\Process; /** * @inheritDoc @@ -30,9 +29,9 @@ class ApplierTest extends TestCase private $applier; /** - * @var ProcessFactory|MockObject + * @var PatchCommandInterface|MockObject */ - private $processFactory; + private $patchCommand; /** * @var GitConverter|MockObject @@ -54,13 +53,13 @@ class ApplierTest extends TestCase */ protected function setUp() { - $this->processFactory = $this->createMock(ProcessFactory::class); + $this->patchCommand = $this->createMock(PatchCommandInterface::class); $this->gitConverter = $this->createMock(GitConverter::class); $this->magentoVersion = $this->createMock(MagentoVersion::class); $this->filesystem = $this->createMock(Filesystem::class); $this->applier = new Applier( - $this->processFactory, + $this->patchCommand, $this->gitConverter, $this->magentoVersion, $this->filesystem @@ -86,14 +85,10 @@ public function testApply() $this->gitConverter->expects($this->once()) ->method('convert') ->willReturn('gitContent'); - $processMock = $this->createMock(Process::class); - $this->processFactory->expects($this->once()) - ->method('create') - ->withConsecutive([['git', 'apply'], 'gitContent']) - ->willReturn($processMock); - $processMock->expects($this->once()) - ->method('mustRun'); + $this->patchCommand->expects($this->once()) + ->method('apply') + ->with('gitContent'); $this->assertSame($expectedMessage, $this->applier->apply($path, $patchId)); } @@ -106,14 +101,13 @@ public function testApplyFailed() $path = 'path/to/patch'; $patchId = 'MC-11111'; - /** @var Process|MockObject $result */ - $processMock = $this->createMock(Process::class); - $processMock->method('mustRun') - ->willThrowException(new ProcessFailedException($processMock)); + $this->patchCommand->expects($this->once()) + ->method('apply') + ->willThrowException(new PatchCommandException('Patch cannot be applied')); - $this->processFactory->expects($this->exactly(2)) - ->method('create') - ->willReturn($processMock); + $this->patchCommand->expects($this->once()) + ->method('revertCheck') + ->willThrowException(new PatchCommandException('Patch cannot be reverted')); $this->expectException(ApplierException::class); $this->applier->apply($path, $patchId); @@ -139,45 +133,16 @@ public function testApplyPatchAlreadyApplied() $this->gitConverter->expects($this->never()) ->method('convert'); - $this->processFactory->expects($this->exactly(2)) - ->method('create') - ->willReturnMap([ - [['git', 'apply'], 'patchContent'], - [['git', 'apply', '--check', '--reverse'], 'patchContent'] - ])->willReturnCallback([$this, 'shellApplyRevertCallback']); + $this->patchCommand->expects($this->once()) + ->method('apply') + ->with('patchContent') + ->willThrowException(new PatchCommandException('Patch cannot be applied')); - $this->assertSame($expectedMessage, $this->applier->apply($path, $patchId)); - } + $this->patchCommand->expects($this->once()) + ->method('revertCheck') + ->with('patchContent'); - /** - * Callback for 'apply' and 'revert' operations. - * - * @param array $command - * @return Process - * - * @throws ProcessFailedException when the command isn't a reverse - */ - public function shellApplyRevertCallback(array $command): Process - { - if (in_array('--reverse', $command, true) && in_array('--check', $command, true) || - !in_array('--reverse', $command, true) && in_array('--check', $command, true) - ) { - // Command was the reverse check, it's all good. - /** @var Process|MockObject $result */ - $result = $this->createMock(Process::class); - $result->expects($this->once()) - ->method('mustRun'); - - return $result; - } - - /** @var Process|MockObject $result */ - $result = $this->createMock(Process::class); - $result->expects($this->once()) - ->method('mustRun') - ->willThrowException(new ProcessFailedException($result)); - - return $result; + $this->assertSame($expectedMessage, $this->applier->apply($path, $patchId)); } /** @@ -201,14 +166,9 @@ public function testRevert() ->method('convert') ->willReturn('gitContent'); - $processMock = $this->createMock(Process::class); - - $this->processFactory->expects($this->once()) - ->method('create') - ->withConsecutive([['git', 'apply', '--reverse'], 'gitContent']) - ->willReturn($processMock); - $processMock->expects($this->once()) - ->method('mustRun'); + $this->patchCommand->expects($this->once()) + ->method('revert') + ->with('gitContent'); $this->assertSame($expectedMessage, $this->applier->revert($path, $patchId)); } @@ -221,14 +181,13 @@ public function testRevertFailed() $path = 'path/to/patch'; $patchId = 'MC-11111'; - /** @var Process|MockObject $result */ - $processMock = $this->createMock(Process::class); - $processMock->method('mustRun') - ->willThrowException(new ProcessFailedException($processMock)); + $this->patchCommand->expects($this->once()) + ->method('revert') + ->willThrowException(new PatchCommandException('Patch cannot be reverted')); - $this->processFactory->expects($this->exactly(2)) - ->method('create') - ->willReturn($processMock); + $this->patchCommand->expects($this->once()) + ->method('applyCheck') + ->willThrowException(new PatchCommandException('Patch cannot be applied')); $this->expectException(ApplierException::class); $this->applier->revert($path, $patchId); @@ -255,12 +214,14 @@ public function testRevertPatchWasntApplied() $this->gitConverter->expects($this->never()) ->method('convert'); - $this->processFactory->expects($this->exactly(2)) - ->method('create') - ->willReturnMap([ - [['git', 'apply'], $patchContent], - [['git', 'apply', '--check'], $patchContent] - ])->willReturnCallback([$this, 'shellApplyRevertCallback']); + $this->patchCommand->expects($this->once()) + ->method('revert') + ->with($patchContent) + ->willThrowException(new PatchCommandException('Patch cannot be reverted')); + + $this->patchCommand->expects($this->once()) + ->method('applyCheck') + ->with($patchContent); $this->assertSame($expectedMessage, $this->applier->revert($path, $patchId)); } @@ -271,14 +232,10 @@ public function testRevertPatchWasntApplied() public function testStatusNotApplied() { $patchContent = 'patch content'; - $processMock = $this->createMock(Process::class); - $this->processFactory->expects($this->once()) - ->method('create') - ->withConsecutive([['git', 'apply', '--check'], $patchContent]) - ->willReturn($processMock); - $processMock->expects($this->once()) - ->method('mustRun'); + $this->patchCommand->expects($this->once()) + ->method('applyCheck') + ->with($patchContent); $this->assertSame(StatusPool::NOT_APPLIED, $this->applier->status($patchContent)); } @@ -290,14 +247,15 @@ public function testStatusNotAvailable() { $patchContent = 'patch content'; - /** @var Process|MockObject $result */ - $processMock = $this->createMock(Process::class); - $processMock->method('mustRun') - ->willThrowException(new ProcessFailedException($processMock)); + $this->patchCommand->expects($this->once()) + ->method('applyCheck') + ->with($patchContent) + ->willThrowException(new PatchCommandException('Patch cannot be applied')); - $this->processFactory->expects($this->exactly(2)) - ->method('create') - ->willReturn($processMock); + $this->patchCommand->expects($this->once()) + ->method('revertCheck') + ->with($patchContent) + ->willThrowException(new PatchCommandException('Patch cannot be reverted')); $this->assertSame(StatusPool::NA, $this->applier->status($patchContent)); } @@ -309,42 +267,15 @@ public function testStatusApplied() { $patchContent = 'patch content'; - $this->processFactory->expects($this->exactly(2)) - ->method('create') - ->willReturnMap([ - [['git', 'apply', '--check']], - [['git', 'apply', '--check', '--reverse']] - ])->willReturnCallback([$this, 'shellStatusCallback']); + $this->patchCommand->expects($this->once()) + ->method('applyCheck') + ->with($patchContent) + ->willThrowException(new PatchCommandException('Patch cannot be applied')); - $this->assertSame(StatusPool::APPLIED, $this->applier->status($patchContent)); - } + $this->patchCommand->expects($this->once()) + ->method('revertCheck') + ->with($patchContent); - /** - * Callback for 'status' operations. - * - * @param array $command - * @return Process - * - * @throws ProcessFailedException when the command isn't a reverse - */ - public function shellStatusCallback(array $command): Process - { - if (in_array('--reverse', $command, true) && in_array('--check', $command, true)) { - // Command was the reverse check, it's all good. - /** @var Process|MockObject $result */ - $result = $this->createMock(Process::class); - $result->expects($this->once()) - ->method('mustRun'); - - return $result; - } - - /** @var Process|MockObject $result */ - $result = $this->createMock(Process::class); - $result->expects($this->once()) - ->method('mustRun') - ->willThrowException(new ProcessFailedException($result)); - - return $result; + $this->assertSame(StatusPool::APPLIED, $this->applier->status($patchContent)); } } diff --git a/src/Test/Unit/Patch/Conflict/ProcessorTest.php b/src/Test/Unit/Patch/Conflict/ProcessorTest.php index 00569d6..8f09459 100644 --- a/src/Test/Unit/Patch/Conflict/ProcessorTest.php +++ b/src/Test/Unit/Patch/Conflict/ProcessorTest.php @@ -76,7 +76,6 @@ public function testProcess() $failedPatch = $this->createPatch('MC-3', 'path3'); $exceptionMessage = 'exceptionMessage'; $conflictDetails = 'Conflict details'; - $formattedOutput = 'formattedOutput'; $rollbackMessages = ['Patch 1 has been reverted', 'Patch 2 has been reverted']; /** @var OutputInterface|MockObject $outputMock */ @@ -90,14 +89,10 @@ public function testProcess() ->method('analyze') ->withConsecutive([$failedPatch]) ->willReturn($conflictDetails); - $this->renderer->expects($this->once()) - ->method('formatErrorOutput') - ->withConsecutive([$exceptionMessage]) - ->willReturn($formattedOutput); $outputMock->expects($this->exactly(2)) ->method('writeln') ->withConsecutive( - [$this->stringContains('Error: patch conflict happened')], + [$this->stringContains('Error: patch can\'t be applied')], [$rollbackMessages] ); @@ -105,7 +100,7 @@ public function testProcess() 'Applying patch %s (%s) failed.%s%s', $failedPatch->getId(), $failedPatch->getPath(), - $formattedOutput, + PHP_EOL .$exceptionMessage, PHP_EOL . $conflictDetails ); diff --git a/src/Test/Unit/Shell/Command/PatchDriverTest.php b/src/Test/Unit/Shell/Command/PatchDriverTest.php new file mode 100644 index 0000000..d1462d5 --- /dev/null +++ b/src/Test/Unit/Shell/Command/PatchDriverTest.php @@ -0,0 +1,187 @@ +baseDir = dirname(__DIR__, 5) . '/tests/unit/'; + $this->cwd = $this->baseDir . 'var/'; + $processFactory = $this->createMock(ProcessFactory::class); + $processFactory->method('create') + ->willReturnCallback( + function (array $cmd, string $input = null) { + return new Process( + $cmd, + $this->cwd, + null, + $input + ); + } + ); + $this->command = new PatchDriver( + $processFactory + ); + } + + /** + * @inheritDoc + */ + protected function tearDown() + { + foreach (glob($this->cwd . '*') as $file) { + if (is_file($file)) { + unlink($file); + } + } + parent::tearDown(); + } + + /** + * Tests that patch is applied + */ + public function testApply() + { + $this->copyFileToWorkingDir($this->getFixtureFile('file1.md')); + $patchContent = $this->getFileContent($this->getFixtureFile('file1.patch')); + $this->command->apply($patchContent); + $expected = $this->getFileContent($this->getFixtureFile('file1_applied_patch.md')); + $actual = $this->getFileContent($this->getVarFile('file1.md')); + $this->assertEquals($expected, $actual); + } + + /** + * Tests that patch is not applied to any target files if an error occurs + */ + public function testApplyFailure() + { + $this->copyFileToWorkingDir($this->getFixtureFile('file1.md')); + $this->copyFileToWorkingDir($this->getFixtureFile('file2_applied_patch.md'), 'file2.md'); + $patchContent = $this->getFileContent($this->getFixtureFile('file1_and_file2.patch')); + $exception = null; + try { + $this->command->apply($patchContent); + } catch (PatchCommandException $e) { + $exception = $e; + } + $this->assertNotNull($exception); + $expected = $this->getFileContent($this->getFixtureFile('file1.md')); + $actual = $this->getFileContent($this->getVarFile('file1.md')); + $this->assertEquals($expected, $actual); + $expected = $this->getFileContent($this->getFixtureFile('file2_applied_patch.md')); + $actual = $this->getFileContent($this->getVarFile('file2.md')); + $this->assertEquals($expected, $actual); + } + + /** + * Tests that patch is reverted + */ + public function testRevert() + { + $this->copyFileToWorkingDir($this->getFixtureFile('file1_applied_patch.md'), 'file1.md'); + $patchContent = $this->getFileContent($this->getFixtureFile('file1.patch')); + $this->command->revert($patchContent); + $expected = $this->getFileContent($this->getFixtureFile('file1.md')); + $actual = $this->getFileContent($this->getVarFile('file1.md')); + $this->assertEquals($expected, $actual); + } + + /** + * Tests that patch is not reverted in any target files if an error occurs + */ + public function testRevertFailure() + { + $this->copyFileToWorkingDir($this->getFixtureFile('file1_applied_patch.md'), 'file1.md'); + $this->copyFileToWorkingDir($this->getFixtureFile('file2.md')); + $patchContent = $this->getFileContent($this->getFixtureFile('file1_and_file2.patch')); + $exception = null; + try { + $this->command->revert($patchContent); + } catch (PatchCommandException $e) { + $exception = $e; + } + $this->assertNotNull($exception); + $expected = $this->getFileContent($this->getFixtureFile('file1_applied_patch.md')); + $actual = $this->getFileContent($this->getVarFile('file1.md')); + $this->assertEquals($expected, $actual); + $expected = $this->getFileContent($this->getFixtureFile('file2.md')); + $actual = $this->getFileContent($this->getVarFile('file2.md')); + $this->assertEquals($expected, $actual); + } + + /** + * Get file path in var directory + * + * @param string $name + * @return string + */ + private function getVarFile(string $name): string + { + return $this->cwd . $name; + } + + /** + * Get file path in files directory + * + * @param string $name + * @return string + */ + private function getFixtureFile(string $name): string + { + return $this->baseDir . '_data/files/' . $name; + } + + /** + * Get the file content + * + * @param string $path + * @return string + */ + private function getFileContent(string $path): string + { + return file_get_contents($path); + } + + /** + * Copy file to working directory + * + * @param string $path + * @param string|null $name + */ + private function copyFileToWorkingDir(string $path, string $name = null) + { + $name = $name ?? basename($path); + copy($path, $this->getVarFile($name)); + } +} diff --git a/tests/unit/_data/files/file1.md b/tests/unit/_data/files/file1.md new file mode 100644 index 0000000..abe4d18 --- /dev/null +++ b/tests/unit/_data/files/file1.md @@ -0,0 +1 @@ +## File 1 diff --git a/tests/unit/_data/files/file1.patch b/tests/unit/_data/files/file1.patch new file mode 100644 index 0000000..9c259fe --- /dev/null +++ b/tests/unit/_data/files/file1.patch @@ -0,0 +1,9 @@ +diff --git a/file1.md b/file1.md +index abe4d18..2f8298f 100644 +--- a/file1.md ++++ b/file1.md +@@ -1 +1,3 @@ +-## File 1 ++# File One ++ ++## Description diff --git a/tests/unit/_data/files/file1_and_file2.patch b/tests/unit/_data/files/file1_and_file2.patch new file mode 100644 index 0000000..f2dcb41 --- /dev/null +++ b/tests/unit/_data/files/file1_and_file2.patch @@ -0,0 +1,18 @@ +diff --git a/file1.md b/file1.md +index abe4d18..2f8298f 100644 +--- a/file1.md ++++ b/file1.md +@@ -1 +1,3 @@ +-## File 1 ++# File One ++ ++## Description +diff --git a/file2.md b/file2.md +index 37ae4d5..c49d210 100644 +--- a/file2.md ++++ b/file2.md +@@ -1 +1,3 @@ +-## File 2 ++# File Two ++ ++## Description diff --git a/tests/unit/_data/files/file1_applied_patch.md b/tests/unit/_data/files/file1_applied_patch.md new file mode 100644 index 0000000..2f8298f --- /dev/null +++ b/tests/unit/_data/files/file1_applied_patch.md @@ -0,0 +1,3 @@ +# File One + +## Description diff --git a/tests/unit/_data/files/file2.md b/tests/unit/_data/files/file2.md new file mode 100644 index 0000000..37ae4d5 --- /dev/null +++ b/tests/unit/_data/files/file2.md @@ -0,0 +1 @@ +## File 2 diff --git a/tests/unit/_data/files/file2_applied_patch.md b/tests/unit/_data/files/file2_applied_patch.md new file mode 100644 index 0000000..c49d210 --- /dev/null +++ b/tests/unit/_data/files/file2_applied_patch.md @@ -0,0 +1,3 @@ +# File Two + +## Description diff --git a/tests/unit/var/.gitignore b/tests/unit/var/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/tests/unit/var/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore