diff --git a/bin/ece-patches b/bin/ece-patches
index 99d5e7d..3efc871 100755
--- a/bin/ece-patches
+++ b/bin/ece-patches
@@ -4,7 +4,8 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+define('IS_CLOUD', true);
$container = require __DIR__ . '/../bootstrap.php';
-$application = new Magento\CloudPatches\Application($container);
+$application = new Magento\CloudPatches\ApplicationEce($container);
$application->run();
diff --git a/composer.json b/composer.json
index 49d6fab..5c08720 100644
--- a/composer.json
+++ b/composer.json
@@ -2,7 +2,7 @@
"name": "magento/magento-cloud-patches",
"description": "Provides critical fixes for Magento 2 Enterprise Edition",
"type": "magento2-component",
- "version": "1.0.5",
+ "version": "1.0.6",
"license": "OSL-3.0",
"require": {
"php": "^7.0",
@@ -14,6 +14,7 @@
"symfony/dependency-injection": "^3.3||^4.3",
"symfony/process": "^2.1||^4.1",
"symfony/proxy-manager-bridge": "^3.3||^4.3",
+ "symfony/yaml": "^3.3||^4.0",
"monolog/monolog": "^1.16",
"magento/quality-patches": "^1.0.0"
},
diff --git a/config/services.xml b/config/services.xml
index 958607b..52ca97c 100644
--- a/config/services.xml
+++ b/config/services.xml
@@ -9,7 +9,6 @@
-
@@ -28,11 +27,6 @@
-
-
-
-
-
@@ -42,26 +36,29 @@
+
+
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -75,6 +72,9 @@
+
+
+
diff --git a/src/ApplicationEce.php b/src/ApplicationEce.php
new file mode 100644
index 0000000..fc6e4c2
--- /dev/null
+++ b/src/ApplicationEce.php
@@ -0,0 +1,48 @@
+container = $container;
+
+ parent::__construct(
+ $container->get(Composer::class)->getPackage()->getPrettyName(),
+ $container->get(Composer::class)->getPackage()->getPrettyVersion()
+ );
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function getDefaultCommands()
+ {
+ return array_merge(parent::getDefaultCommands(), [
+ $this->container->get(Command\Ece\Apply::class),
+ $this->container->get(Command\Ece\Revert::class),
+ $this->container->get(Command\Status::class)
+ ]);
+ }
+}
diff --git a/src/Command/Apply.php b/src/Command/Apply.php
index 3c681af..ce77f79 100644
--- a/src/Command/Apply.php
+++ b/src/Command/Apply.php
@@ -8,19 +8,15 @@
namespace Magento\CloudPatches\Command;
use Magento\CloudPatches\App\RuntimeException;
-use Magento\CloudPatches\Command\Process\ApplyLocal;
use Magento\CloudPatches\Command\Process\ApplyOptional;
-use Magento\CloudPatches\Command\Process\ApplyRequired;
use Magento\CloudPatches\Composer\MagentoVersion;
-use Magento\CloudPatches\Patch\Environment;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
-use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
- * Patch apply command.
+ * Patch apply command (OnPrem).
*/
class Apply extends AbstractCommand
{
@@ -30,35 +26,15 @@ class Apply extends AbstractCommand
const NAME = 'apply';
/**
- * Defines whether Magento is installed from Git.
+ * List of patches to apply.
*/
- const OPT_GIT_INSTALLATION = 'git-installation';
-
- /**
- * List of quality patches to apply.
- */
- const ARG_QUALITY_PATCHES = 'quality-patches';
+ const ARG_LIST_OF_PATCHES = 'list-of-patches';
/**
* @var ApplyOptional
*/
private $applyOptional;
- /**
- * @var ApplyRequired
- */
- private $applyRequired;
-
- /**
- * @var ApplyLocal
- */
- private $applyLocal;
-
- /**
- * @var Environment
- */
- private $environment;
-
/**
* @var LoggerInterface
*/
@@ -70,25 +46,16 @@ class Apply extends AbstractCommand
private $magentoVersion;
/**
- * @param ApplyRequired $applyRequired
* @param ApplyOptional $applyOptional
- * @param ApplyLocal $applyLocal
- * @param Environment $environment
* @param LoggerInterface $logger
* @param MagentoVersion $magentoVersion
*/
public function __construct(
- ApplyRequired $applyRequired,
ApplyOptional $applyOptional,
- ApplyLocal $applyLocal,
- Environment $environment,
LoggerInterface $logger,
MagentoVersion $magentoVersion
) {
- $this->applyRequired = $applyRequired;
$this->applyOptional = $applyOptional;
- $this->applyLocal = $applyLocal;
- $this->environment = $environment;
$this->logger = $logger;
$this->magentoVersion = $magentoVersion;
@@ -101,17 +68,11 @@ public function __construct(
protected function configure()
{
$this->setName(self::NAME)
- ->setDescription('Apply patches')
+ ->setDescription('Applies patches. The list of patches should pass as a command argument')
->addArgument(
- self::ARG_QUALITY_PATCHES,
- InputArgument::IS_ARRAY,
- 'List of quality patches to apply'
- )->addOption(
- self::OPT_GIT_INSTALLATION,
- null,
- InputOption::VALUE_OPTIONAL,
- 'Is git installation',
- false
+ self::ARG_LIST_OF_PATCHES,
+ InputArgument::IS_ARRAY | InputArgument::REQUIRED,
+ 'List of patches to apply'
);
parent::configure();
@@ -122,23 +83,10 @@ protected function configure()
*/
public function execute(InputInterface $input, OutputInterface $output)
{
- $deployedFromGit = $input->getOption(Apply::OPT_GIT_INSTALLATION);
- if ($deployedFromGit) {
- $output->writeln('Git-based installation. Skipping patches applying.');
-
- return self::RETURN_SUCCESS;
- }
-
$this->logger->notice($this->magentoVersion->get());
try {
- if ($this->environment->isCloud()) {
- $this->applyRequired->run($input, $output);
- $this->applyOptional->run($input, $output);
- $this->applyLocal->run($input, $output);
- } else {
- $this->applyOptional->run($input, $output);
- }
+ $this->applyOptional->run($input, $output);
} catch (RuntimeException $e) {
$output->writeln('' . $e->getMessage() . '');
$this->logger->error($e->getMessage());
diff --git a/src/Command/Ece/Apply.php b/src/Command/Ece/Apply.php
new file mode 100644
index 0000000..6090a38
--- /dev/null
+++ b/src/Command/Ece/Apply.php
@@ -0,0 +1,113 @@
+applyRequired = $applyRequired;
+ $this->applyOptional = $applyOptional;
+ $this->applyLocal = $applyLocal;
+ $this->logger = $logger;
+ $this->magentoVersion = $magentoVersion;
+
+ parent::__construct(self::NAME);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function configure()
+ {
+ $this->setName(self::NAME)
+ ->setDescription('Applies patches (Magento Cloud only)');
+
+ parent::configure();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function execute(InputInterface $input, OutputInterface $output)
+ {
+ $this->logger->notice($this->magentoVersion->get());
+
+ try {
+ $this->applyRequired->run($input, $output);
+ $this->applyOptional->run($input, $output);
+ $this->applyLocal->run($input, $output);
+ } catch (RuntimeException $e) {
+ $output->writeln('' . $e->getMessage() . '');
+ $this->logger->error($e->getMessage());
+
+ return self::RETURN_FAILURE;
+ } catch (\Exception $e) {
+ $this->logger->critical($e);
+
+ throw $e;
+ }
+
+ return self::RETURN_SUCCESS;
+ }
+}
diff --git a/src/Command/Ece/Revert.php b/src/Command/Ece/Revert.php
new file mode 100644
index 0000000..bd8cde3
--- /dev/null
+++ b/src/Command/Ece/Revert.php
@@ -0,0 +1,93 @@
+revert = $revert;
+ $this->logger = $logger;
+ $this->magentoVersion = $magentoVersion;
+
+ parent::__construct(self::NAME);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function configure()
+ {
+ $this->setName(self::NAME)
+ ->setDescription('Reverts patches (Magento Cloud only)');
+
+ parent::configure();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function execute(InputInterface $input, OutputInterface $output)
+ {
+ $this->logger->notice($this->magentoVersion->get());
+
+ try {
+ $this->revert->run($input, $output);
+ } catch (RuntimeException $e) {
+ $output->writeln('' . $e->getMessage() . '');
+ $this->logger->error($e->getMessage());
+
+ return self::RETURN_FAILURE;
+ } catch (\Exception $e) {
+ $this->logger->critical($e);
+
+ throw $e;
+ }
+
+ return self::RETURN_SUCCESS;
+ }
+}
diff --git a/src/Command/Process/Action/ApplyOptionalAction.php b/src/Command/Process/Action/ApplyOptionalAction.php
index 5bc6a95..461d6c3 100644
--- a/src/Command/Process/Action/ApplyOptionalAction.php
+++ b/src/Command/Process/Action/ApplyOptionalAction.php
@@ -9,6 +9,7 @@
use Magento\CloudPatches\App\RuntimeException;
use Magento\CloudPatches\Command\Process\Renderer;
+use Magento\CloudPatches\Patch\Conflict\Processor as ConflictProcessor;
use Magento\CloudPatches\Patch\Pool\OptionalPool;
use Magento\CloudPatches\Patch\Applier;
use Magento\CloudPatches\Patch\ApplierException;
@@ -51,25 +52,33 @@ class ApplyOptionalAction implements ActionInterface
*/
private $logger;
+ /**
+ * @var ConflictProcessor
+ */
+ private $conflictProcessor;
+
/**
* @param Applier $applier
* @param OptionalPool $optionalPool
* @param StatusPool $statusPool
* @param Renderer $renderer
* @param LoggerInterface $logger
+ * @param ConflictProcessor $conflictProcessor
*/
public function __construct(
Applier $applier,
OptionalPool $optionalPool,
StatusPool $statusPool,
Renderer $renderer,
- LoggerInterface $logger
+ LoggerInterface $logger,
+ ConflictProcessor $conflictProcessor
) {
$this->applier = $applier;
$this->optionalPool = $optionalPool;
$this->statusPool = $statusPool;
$this->renderer = $renderer;
$this->logger = $logger;
+ $this->conflictProcessor = $conflictProcessor;
}
/**
@@ -93,13 +102,7 @@ public function execute(InputInterface $input, OutputInterface $output, array $p
$this->logger->info($message, ['file' => $patch->getPath()]);
array_push($appliedPatches, $patch);
} catch (ApplierException $exception) {
- $this->printPatchApplyingFailed($output, $patch, $exception->getMessage());
- $this->rollback($output, $appliedPatches);
-
- throw new RuntimeException(
- 'Applying optional patches ' . implode(' ', $patchFilter) . ' failed.',
- $exception->getCode()
- );
+ $this->conflictProcessor->process($output, $patch, $appliedPatches, $exception->getMessage());
}
}
}
@@ -124,28 +127,6 @@ private function printPatchWasApplied(OutputInterface $output, PatchInterface $p
$this->logger->info($message);
}
- /**
- * Prints and logs 'applying patch failed' message.
- *
- * @param OutputInterface $output
- * @param PatchInterface $patch
- * @param string $errorOutput
- *
- * @return void
- */
- private function printPatchApplyingFailed(OutputInterface $output, PatchInterface $patch, string $errorOutput)
- {
- $errorMessage = sprintf(
- 'Applying patch %s (%s) failed.%s',
- $patch->getId(),
- $patch->getPath(),
- $this->renderer->formatErrorOutput($errorOutput)
- );
-
- $output->writeln('' . $errorMessage . '' . PHP_EOL);
- $this->logger->error($errorMessage);
- }
-
/**
* Returns a list of patches according to the filter.
*
@@ -171,25 +152,4 @@ function ($patch) {
throw new RuntimeException($e->getMessage(), $e->getCode());
}
}
-
- /**
- * Rollback applied patches.
- *
- * @param OutputInterface $output
- * @param PatchInterface[] $appliedPatches
- *
- * @return void
- */
- private function rollback(OutputInterface $output, array $appliedPatches)
- {
- $this->logger->info('Start rollback');
-
- foreach (array_reverse($appliedPatches) as $appliedPatch) {
- $message = $this->applier->revert($appliedPatch->getPath(), $appliedPatch->getId());
- $this->renderer->printPatchInfo($output, $appliedPatch, $message);
- $this->logger->info($message, ['file' => $appliedPatch->getPath()]);
- }
-
- $this->logger->info('End rollback');
- }
}
diff --git a/src/Command/Process/ApplyLocal.php b/src/Command/Process/ApplyLocal.php
index e1418cf..3ba8ebd 100644
--- a/src/Command/Process/ApplyLocal.php
+++ b/src/Command/Process/ApplyLocal.php
@@ -11,6 +11,7 @@
use Magento\CloudPatches\Patch\Pool\LocalPool;
use Magento\CloudPatches\Patch\Applier;
use Magento\CloudPatches\Patch\ApplierException;
+use Magento\CloudPatches\Patch\RollbackProcessor;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
@@ -40,22 +41,30 @@ class ApplyLocal implements ProcessInterface
*/
private $logger;
+ /**
+ * @var RollbackProcessor
+ */
+ private $rollbackProcessor;
+
/**
* @param Applier $applier
- * @param LocalPool $localPatchPool
+ * @param LocalPool $localPool
* @param Renderer $renderer
* @param LoggerInterface $logger
+ * @param RollbackProcessor $rollbackProcessor
*/
public function __construct(
Applier $applier,
- LocalPool $localPatchPool,
+ LocalPool $localPool,
Renderer $renderer,
- LoggerInterface $logger
+ LoggerInterface $logger,
+ RollbackProcessor $rollbackProcessor
) {
$this->applier = $applier;
- $this->localPool = $localPatchPool;
+ $this->localPool = $localPool;
$this->renderer = $renderer;
$this->logger = $logger;
+ $this->rollbackProcessor = $rollbackProcessor;
}
/**
@@ -73,11 +82,16 @@ public function run(InputInterface $input, OutputInterface $output)
$this->logger->notice('Start of applying hot-fixes');
$output->writeln('Applying hot-fixes');
+ $appliedPatches = [];
foreach ($patches as $patch) {
try {
$message = $this->applier->apply($patch->getPath(), $patch->getTitle());
$this->printInfo($output, $message);
+ array_push($appliedPatches, $patch);
} catch (ApplierException $exception) {
+ $this->printError($output, 'Error: patch conflict happened');
+ $messages = $this->rollbackProcessor->process($appliedPatches);
+ $output->writeln($messages);
$errorMessage = sprintf(
'Applying patch %s failed.%s',
$patch->getPath(),
@@ -102,4 +116,16 @@ private function printInfo(OutputInterface $output, string $message)
$output->writeln('' . $message . '');
$this->logger->info($message);
}
+
+ /**
+ * Prints and logs error message.
+ *
+ * @param OutputInterface $output
+ * @param string $message
+ */
+ private function printError(OutputInterface $output, string $message)
+ {
+ $output->writeln('' . $message . '');
+ $this->logger->error($message);
+ }
}
diff --git a/src/Command/Process/ApplyOptional.php b/src/Command/Process/ApplyOptional.php
index d4b8ce1..9275736 100644
--- a/src/Command/Process/ApplyOptional.php
+++ b/src/Command/Process/ApplyOptional.php
@@ -15,7 +15,7 @@
use Symfony\Component\Console\Output\OutputInterface;
/**
- * Applies optional patches (Cloud & OnPrem).
+ * Applies optional patches (OnPrem).
*/
class ApplyOptional implements ProcessInterface
{
@@ -54,7 +54,7 @@ public function __construct(
*/
public function run(InputInterface $input, OutputInterface $output)
{
- $argPatches = $input->getArgument(Apply::ARG_QUALITY_PATCHES);
+ $argPatches = $input->getArgument(Apply::ARG_LIST_OF_PATCHES);
$patchFilter = $this->filterFactory->createApplyFilter($argPatches);
if ($patchFilter === null) {
return;
diff --git a/src/Command/Process/ApplyRequired.php b/src/Command/Process/ApplyRequired.php
index 51f057d..f4d5eed 100644
--- a/src/Command/Process/ApplyRequired.php
+++ b/src/Command/Process/ApplyRequired.php
@@ -7,7 +7,7 @@
namespace Magento\CloudPatches\Command\Process;
-use Magento\CloudPatches\App\RuntimeException;
+use Magento\CloudPatches\Patch\Conflict\Processor as ConflictProcessor;
use Magento\CloudPatches\Patch\Pool\RequiredPool;
use Magento\CloudPatches\Patch\Applier;
use Magento\CloudPatches\Patch\ApplierException;
@@ -42,22 +42,30 @@ class ApplyRequired implements ProcessInterface
*/
private $logger;
+ /**
+ * @var ConflictProcessor
+ */
+ private $conflictProcessor;
+
/**
* @param Applier $applier
* @param RequiredPool $requiredPool
* @param Renderer $renderer
* @param LoggerInterface $logger
+ * @param ConflictProcessor $conflictProcessor
*/
public function __construct(
Applier $applier,
RequiredPool $requiredPool,
Renderer $renderer,
- LoggerInterface $logger
+ LoggerInterface $logger,
+ ConflictProcessor $conflictProcessor
) {
$this->applier = $applier;
$this->requiredPool = $requiredPool;
$this->renderer = $renderer;
$this->logger = $logger;
+ $this->conflictProcessor = $conflictProcessor;
}
/**
@@ -67,21 +75,16 @@ public function run(InputInterface $input, OutputInterface $output)
{
$this->logger->notice('Start of applying required patches');
+ $appliedPatches = [];
$patches = $this->requiredPool->getList();
foreach ($patches as $patch) {
try {
$message = $this->applier->apply($patch->getPath(), $patch->getId());
$this->renderer->printPatchInfo($output, $patch, $message);
$this->logger->info($message, ['file' => $patch->getPath()]);
+ array_push($appliedPatches, $patch);
} catch (ApplierException $exception) {
- $errorMessage = sprintf(
- 'Applying patch %s %s failed.%s',
- $patch->getId(),
- $patch->getPath(),
- $this->renderer->formatErrorOutput($exception->getMessage())
- );
-
- throw new RuntimeException($errorMessage, $exception->getCode());
+ $this->conflictProcessor->process($output, $patch, $appliedPatches, $exception->getMessage());
}
}
diff --git a/src/Command/Process/Ece/ApplyOptional.php b/src/Command/Process/Ece/ApplyOptional.php
new file mode 100644
index 0000000..dc02f1d
--- /dev/null
+++ b/src/Command/Process/Ece/ApplyOptional.php
@@ -0,0 +1,77 @@
+filterFactory = $filterFactory;
+ $this->actionPool = $actionPool;
+ $this->logger = $logger;
+ $this->config = $config;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function run(InputInterface $input, OutputInterface $output)
+ {
+ $envQualityPatches = $this->config->getQualityPatches();
+ $patchFilter = $this->filterFactory->createApplyFilter($envQualityPatches);
+ if ($patchFilter === null) {
+ return;
+ }
+
+ $this->logger->notice('Start of applying optional patches');
+ $this->logger->info('QUALITY_PATCHES env variable: ' . implode(' ', $envQualityPatches));
+ $this->actionPool->execute($input, $output, $patchFilter);
+ $this->logger->notice('End of applying optional patches');
+ }
+}
diff --git a/src/Command/Process/Ece/Revert.php b/src/Command/Process/Ece/Revert.php
new file mode 100644
index 0000000..b2b93b4
--- /dev/null
+++ b/src/Command/Process/Ece/Revert.php
@@ -0,0 +1,156 @@
+revertAction = $revertAction;
+ $this->logger = $logger;
+ $this->applier = $applier;
+ $this->localPool = $localPool;
+ $this->renderer = $renderer;
+ $this->statusPool = $statusPool;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function run(InputInterface $input, OutputInterface $output)
+ {
+ $this->logger->notice('Start of reverting all patches');
+
+ $this->revertLocalPatches($output);
+ $this->revertAction->execute($input, $output, []);
+
+ $this->logger->notice('End of reverting all patches');
+ }
+
+ /**
+ * Reverts local custom patches.
+
+ * @param OutputInterface $output
+ * @return void
+ * @throws RuntimeException
+ */
+ private function revertLocalPatches(OutputInterface $output)
+ {
+ $patches = array_filter(
+ $this->localPool->getList(),
+ function ($patch) {
+ return !$this->statusPool->isNotApplied($patch->getId());
+ }
+ );
+
+ if (empty($patches)) {
+ return;
+ }
+
+ $output->writeln('Start of reverting hot-fixes');
+
+ foreach (array_reverse($patches) as $patch) {
+ try {
+ $message = $this->applier->revert($patch->getPath(), $patch->getTitle());
+ $this->printInfo($output, $message);
+ } catch (ApplierException $exception) {
+ $errorMessage = sprintf(
+ 'Reverting patch %s failed.%s',
+ $patch->getPath(),
+ $this->renderer->formatErrorOutput($exception->getMessage())
+ );
+ $this->printError($output, $errorMessage);
+ }
+ }
+
+ $output->writeln('End of reverting hot-fixes');
+ }
+
+ /**
+ * Prints and logs info message.
+ *
+ * @param OutputInterface $output
+ * @param string $message
+ */
+ private function printInfo(OutputInterface $output, string $message)
+ {
+ $output->writeln('' . $message . '');
+ $this->logger->info($message);
+ }
+
+ /**
+ * Prints and logs error message.
+ *
+ * @param OutputInterface $output
+ * @param string $message
+ */
+ private function printError(OutputInterface $output, string $message)
+ {
+ $output->writeln('' . $message . '');
+ $this->logger->error($message);
+ }
+}
diff --git a/src/Command/Process/Renderer.php b/src/Command/Process/Renderer.php
index 764a86a..319ce7b 100644
--- a/src/Command/Process/Renderer.php
+++ b/src/Command/Process/Renderer.php
@@ -82,15 +82,17 @@ public function printTable(OutputInterface $output, array $patchList)
$table = $this->tableFactory->create($output);
$table->setHeaders([self::ID, self::TITLE, self::TYPE, self::STATUS, self::DETAILS]);
$table->setStyle('box-double');
- $table->setColumnMaxWidth(1, 50);
$rows = [];
foreach ($patchList as $patch) {
$rows[] = $this->createRow($patch);
- $rows[] = new TableSeparator();
}
- array_pop($rows);
+ usort($rows, function ($a, $b) {
+ return strcmp($a[self::STATUS], $b[self::STATUS]);
+ });
+
+ $rows = $this->addTableSeparator($rows);
$table->addRows($rows);
$table->render();
}
@@ -109,7 +111,6 @@ public function printPatchInfo(
string $prependedMessage = ''
) {
$info = [
- sprintf('Id: %s', $patch->getId()),
sprintf('Title: %s', $patch->getTitle()),
sprintf('File: %s', $patch->getFilename()),
sprintf(
@@ -179,26 +180,53 @@ public function printQuestion(InputInterface $input, OutputInterface $output, st
*/
private function createRow(AggregatedPatchInterface $patch): array
{
- $glue = PHP_EOL . ' - ';
$details = '';
if ($patch->getReplacedWith()) {
- $details .= 'Recommended replacement: ' . $patch->getReplacedWith() . PHP_EOL . '';
+ $details .= 'Recommended replacement: ' . $patch->getReplacedWith() . '' . PHP_EOL;
}
+
if ($patch->getRequire()) {
- $details .= 'Required patches:' .
- '' . $glue . implode($glue, $patch->getRequire()) . PHP_EOL . '';
+ $wrappedRequire = array_map(
+ function ($item) {
+ return sprintf(' - %s', $item);
+ },
+ $patch->getRequire()
+ );
+ $details .= 'Required patches:' . PHP_EOL . implode(PHP_EOL, $wrappedRequire) . PHP_EOL;
}
+
if ($patch->getAffectedComponents()) {
+ $glue = PHP_EOL . ' - ';
$details .= 'Affected components:' . $glue . implode($glue, $patch->getAffectedComponents());
}
+
$id = $patch->getType() === PatchInterface::TYPE_CUSTOM ? 'N/A' : $patch->getId();
+ $title = chunk_split($patch->getTitle(), 60, PHP_EOL);
return [
self::ID => '' . $id . '',
- self::TITLE => $patch->getTitle(),
+ self::TITLE => $title,
self::TYPE => $patch->isDeprecated() ? 'DEPRECATED' : $patch->getType(),
self::STATUS => $this->statusPool->get($patch->getId()),
self::DETAILS => $details
];
}
+
+ /**
+ * Adds table separator.
+ *
+ * @param array $rowItems
+ * @return array
+ */
+ private function addTableSeparator(array $rowItems): array
+ {
+ $result = [];
+ foreach ($rowItems as $row) {
+ $result[] = $row;
+ $result[] = new TableSeparator();
+ }
+ array_pop($result);
+
+ return $result;
+ }
}
diff --git a/src/Command/Process/Revert.php b/src/Command/Process/Revert.php
index 1c92b01..d9f07cf 100644
--- a/src/Command/Process/Revert.php
+++ b/src/Command/Process/Revert.php
@@ -15,7 +15,7 @@
use Symfony\Component\Console\Output\OutputInterface;
/**
- * Reverts patches.
+ * Reverts patches (OnPrem).
*
* Patches are reverting from bottom to top of config list.
*/
@@ -56,7 +56,7 @@ public function __construct(
*/
public function run(InputInterface $input, OutputInterface $output)
{
- $argPatches = $input->getArgument(RevertCommand::ARG_QUALITY_PATCHES);
+ $argPatches = $input->getArgument(RevertCommand::ARG_LIST_OF_PATCHES);
$optAll = $input->getOption(RevertCommand::OPT_ALL);
$patchFilter = $this->filterFactory->createRevertFilter($optAll, $argPatches);
diff --git a/src/Command/Revert.php b/src/Command/Revert.php
index 2b971cd..8d6f99e 100644
--- a/src/Command/Revert.php
+++ b/src/Command/Revert.php
@@ -10,7 +10,6 @@
use Magento\CloudPatches\App\RuntimeException;
use Magento\CloudPatches\Command\Process\Revert as RevertProcess;
use Magento\CloudPatches\Composer\MagentoVersion;
-use Magento\CloudPatches\Patch\Environment;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
@@ -18,7 +17,7 @@
use Symfony\Component\Console\Output\OutputInterface;
/**
- * @inheritDoc
+ * Patch revert command (OnPrem).
*/
class Revert extends AbstractCommand
{
@@ -28,9 +27,9 @@ class Revert extends AbstractCommand
const NAME = 'revert';
/**
- * List of quality patches to revert.
+ * List of patches to revert.
*/
- const ARG_QUALITY_PATCHES = 'quality-patches';
+ const ARG_LIST_OF_PATCHES = 'list-of-patches';
/**
* Revert all patches.
@@ -42,11 +41,6 @@ class Revert extends AbstractCommand
*/
private $revert;
- /**
- * @var Environment
- */
- private $environment;
-
/**
* @var LoggerInterface
*/
@@ -59,18 +53,15 @@ class Revert extends AbstractCommand
/**
* @param RevertProcess $revert
- * @param Environment $environment
* @param LoggerInterface $logger
* @param MagentoVersion $magentoVersion
*/
public function __construct(
RevertProcess $revert,
- Environment $environment,
LoggerInterface $logger,
MagentoVersion $magentoVersion
) {
$this->revert = $revert;
- $this->environment = $environment;
$this->logger = $logger;
$this->magentoVersion = $magentoVersion;
@@ -83,16 +74,18 @@ public function __construct(
protected function configure()
{
$this->setName(self::NAME)
- ->setDescription('Revert patches')
- ->addArgument(
- self::ARG_QUALITY_PATCHES,
+ ->setDescription(
+ 'Reverts patches. The list of patches should pass as a command argument' .
+ ' or use option --all to revert all patches'
+ )->addArgument(
+ self::ARG_LIST_OF_PATCHES,
InputArgument::IS_ARRAY,
- 'List of quality patches to revert'
+ 'List of patches to revert'
)->addOption(
self::OPT_ALL,
'a',
InputOption::VALUE_NONE,
- 'Revert all patches'
+ 'Reverts all patches'
);
parent::configure();
@@ -103,12 +96,6 @@ protected function configure()
*/
public function execute(InputInterface $input, OutputInterface $output)
{
- if ($this->environment->isCloud()) {
- $output->writeln('Revert command is unavailable on Magento Cloud');
-
- return self::RETURN_FAILURE;
- }
-
$this->logger->notice($this->magentoVersion->get());
try {
diff --git a/src/Command/Status.php b/src/Command/Status.php
index 8054de1..857b297 100644
--- a/src/Command/Status.php
+++ b/src/Command/Status.php
@@ -59,7 +59,7 @@ public function __construct(
protected function configure()
{
$this->setName(self::NAME)
- ->setDescription('Shows status of patches');
+ ->setDescription('Shows the list of available patches and their statuses');
parent::configure();
}
diff --git a/src/Composer/MagentoVersion.php b/src/Composer/MagentoVersion.php
index 6495f57..e7126a5 100644
--- a/src/Composer/MagentoVersion.php
+++ b/src/Composer/MagentoVersion.php
@@ -9,7 +9,9 @@
use Composer;
use Composer\Package\PackageInterface;
+use Composer\Package\RootPackageInterface;
use Composer\Repository\RepositoryInterface;
+use Composer\Semver\Semver;
/**
* Defines version of Magento.
@@ -21,17 +23,32 @@ class MagentoVersion
*/
private $repository;
+ /**
+ * @var RootPackageInterface
+ */
+ private $rootPackage;
+
/**
* @var string
*/
private $version;
/**
- * @var string[]
+ * @var array
*/
private $editionMap = [
'magento/magento2-b2b-base' => 'B2B Edition',
- 'magento/magento2-ee-base' => 'Enterprise Edition'
+ 'magento/magento2-ee-base' => 'Enterprise Edition',
+ 'magento/magento2ee' => 'Enterprise Edition',
+ 'magento/magento2ce' => 'Community Edition'
+ ];
+
+ /**
+ * @var array
+ */
+ private $gitToComposerMap = [
+ 'magento/magento2ce' => ['magento/magento2-base'],
+ 'magento/magento2ee' => ['magento/magento2-base', 'magento/magento2-ee-base']
];
/**
@@ -40,6 +57,7 @@ class MagentoVersion
public function __construct(
Composer\Composer $composer
) {
+ $this->rootPackage = $composer->getPackage();
$this->repository = $composer->getRepositoryManager()->getLocalRepository();
}
@@ -60,11 +78,38 @@ public function get(): string
$version = $basePackage->getVersion();
$edition = $this->getEdition();
$this->version = 'Magento 2 ' . $edition . ', version ' . $version;
+ } elseif ($this->isGitBased()) {
+ $edition = $this->editionMap[$this->rootPackage->getName()];
+ $this->version = 'Git-based: Magento 2 ' . $edition . ', version ' . $this->rootPackage->getVersion();
}
return $this->version;
}
+ /**
+ * Checks if it's git-based installation.
+ *
+ * @return boolean
+ */
+ public function isGitBased(): bool
+ {
+ return isset($this->gitToComposerMap[$this->rootPackage->getName()]);
+ }
+
+ /**
+ * Matches package on git-based Magento instance
+ *
+ * @param string $name
+ * @param string $constraint
+ * @return boolean
+ */
+ public function matchPackageGit(string $name, string $constraint): bool
+ {
+ return $this->isGitBased()
+ && in_array($name, $this->gitToComposerMap[$this->rootPackage->getName()])
+ && Semver::satisfies($this->rootPackage->getVersion(), $constraint);
+ }
+
/**
* Returns Magento edition.
*
diff --git a/src/Composer/Package.php b/src/Composer/Package.php
index cac6982..8f919d4 100644
--- a/src/Composer/Package.php
+++ b/src/Composer/Package.php
@@ -21,13 +21,21 @@ class Package
*/
private $repository;
+ /**
+ * @var MagentoVersion
+ */
+ private $magentoVersion;
+
/**
* @param Composer\Composer $composer
+ * @param MagentoVersion $magentoVersion
*/
public function __construct(
- Composer\Composer $composer
+ Composer\Composer $composer,
+ MagentoVersion $magentoVersion
) {
$this->repository = $composer->getRepositoryManager()->getLocalRepository();
+ $this->magentoVersion = $magentoVersion;
}
/**
@@ -39,6 +47,7 @@ public function __construct(
*/
public function matchConstraint(string $packageName, string $packageConstraint): bool
{
- return $this->repository->findPackage($packageName, $packageConstraint) instanceof PackageInterface;
+ return $this->magentoVersion->matchPackageGit($packageName, $packageConstraint) ||
+ $this->repository->findPackage($packageName, $packageConstraint) instanceof PackageInterface;
}
}
diff --git a/src/Composer/QualityPackage.php b/src/Composer/QualityPackage.php
new file mode 100644
index 0000000..9cc28e9
--- /dev/null
+++ b/src/Composer/QualityPackage.php
@@ -0,0 +1,56 @@
+patchesDirectory = $info->getPatchesDirectory();
+ $this->patchesConfig = $info->getPatchesConfig();
+ }
+ }
+
+ /**
+ * Returns path to patches directory.
+ *
+ * @return string|null
+ */
+ public function getPatchesDirectory()
+ {
+ return $this->patchesDirectory;
+ }
+
+ /**
+ * Returns path to patches configuration file.
+ *
+ * @return string|null
+ */
+ public function getPatchesConfig()
+ {
+ return $this->patchesConfig;
+ }
+}
diff --git a/src/Environment/Config.php b/src/Environment/Config.php
new file mode 100644
index 0000000..0109d2d
--- /dev/null
+++ b/src/Environment/Config.php
@@ -0,0 +1,82 @@
+configReader = $configReader;
+ }
+ /**
+ * Checks if it's Cloud environment.
+ *
+ * @return bool
+ */
+ public function isCloud(): bool
+ {
+ return (bool)$this->getEnv(self::ENV_VAR_CLOUD) || defined(self::CONST_IS_CLOUD);
+ }
+
+ /**
+ * Returns quality patches env variable.
+ *
+ * @return array
+ * @throws FileSystemException
+ */
+ public function getQualityPatches(): array
+ {
+ $result = $this->getEnv(self::ENV_VAR_QUALITY_PATCHES);
+ if ($result === false) {
+ $result = $this->configReader->read()['stage']['build'][self::ENV_VAR_QUALITY_PATCHES] ?? [];
+ }
+
+ return $result ?: [];
+ }
+
+ /**
+ * 'getEnv' method is an abstraction for _ENV and getenv.
+ * If _ENV is enabled in php.ini, use that. If not, fall back to use getenv.
+ * returns false if not found
+ *
+ * @param string $key
+ * @return array|string|int|null|bool
+ */
+ private function getEnv(string $key)
+ {
+ return $_ENV[$key] ?? getenv($key);
+ }
+}
diff --git a/src/Environment/ConfigReader.php b/src/Environment/ConfigReader.php
new file mode 100644
index 0000000..255f860
--- /dev/null
+++ b/src/Environment/ConfigReader.php
@@ -0,0 +1,70 @@
+fileList = $fileList;
+ $this->filesystem = $filesystem;
+ }
+
+ /**
+ * Returns config.
+ *
+ * @return array
+ * @throws ParseException
+ * @throws FileSystemException
+ */
+ public function read(): array
+ {
+ if ($this->config === null) {
+ $path = $this->fileList->getEnvConfig();
+
+ if (!$this->filesystem->exists($path)) {
+ $this->config = [];
+ } else {
+ $parseFlag = defined(Yaml::class . '::PARSE_CONSTANT') ? Yaml::PARSE_CONSTANT : 0;
+ $this->config = (array)Yaml::parse($this->filesystem->get($path), $parseFlag);
+ }
+ }
+
+ return $this->config;
+ }
+}
diff --git a/src/Filesystem/FileList.php b/src/Filesystem/FileList.php
index 07e07bd..7c95b40 100644
--- a/src/Filesystem/FileList.php
+++ b/src/Filesystem/FileList.php
@@ -44,8 +44,8 @@ public function getPatchLog(): string
/**
* @return string
*/
- public function getInitPatchLog(): string
+ public function getEnvConfig(): string
{
- return $this->directoryList->getMagentoRoot() . '/init/var/log/patch.log';
+ return $this->directoryList->getMagentoRoot() . '/.magento.env.yaml';
}
}
diff --git a/src/Patch/Applier.php b/src/Patch/Applier.php
index 568cbf9..e5f2d1e 100644
--- a/src/Patch/Applier.php
+++ b/src/Patch/Applier.php
@@ -7,6 +7,8 @@
namespace Magento\CloudPatches\Patch;
+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;
@@ -21,13 +23,37 @@ class Applier
*/
private $processFactory;
+ /**
+ * @var GitConverter
+ */
+ private $gitConverter;
+
+ /**
+ * @var MagentoVersion
+ */
+ private $magentoVersion;
+
+ /**
+ * @var Filesystem
+ */
+ private $filesystem;
+
/**
* @param ProcessFactory $processFactory
+ * @param GitConverter $gitConverter
+ * @param MagentoVersion $magentoVersion
+ * @param Filesystem $filesystem
*/
public function __construct(
- ProcessFactory $processFactory
+ ProcessFactory $processFactory,
+ GitConverter $gitConverter,
+ MagentoVersion $magentoVersion,
+ Filesystem $filesystem
) {
$this->processFactory = $processFactory;
+ $this->gitConverter = $gitConverter;
+ $this->magentoVersion = $magentoVersion;
+ $this->filesystem = $filesystem;
}
/**
@@ -41,12 +67,13 @@ public function __construct(
*/
public function apply(string $path, string $id): string
{
+ $content = $this->readContent($path);
try {
- $this->processFactory->create(['git', 'apply', $path])
+ $this->processFactory->create(['git', 'apply'], $content)
->mustRun();
} catch (ProcessFailedException $exception) {
try {
- $this->processFactory->create(['git', 'apply', '--check', '--reverse', $path])
+ $this->processFactory->create(['git', 'apply', '--check', '--reverse'], $content)
->mustRun();
} catch (ProcessFailedException $reverseException) {
throw new ApplierException($exception->getMessage(), $exception->getCode());
@@ -69,12 +96,13 @@ public function apply(string $path, string $id): string
*/
public function revert(string $path, string $id): string
{
+ $content = $this->readContent($path);
try {
- $this->processFactory->create(['git', 'apply', '--reverse', $path])
+ $this->processFactory->create(['git', 'apply', '--reverse'], $content)
->mustRun();
} catch (ProcessFailedException $exception) {
try {
- $this->processFactory->create(['git', 'apply', '--check', $path])
+ $this->processFactory->create(['git', 'apply', '--check'], $content)
->mustRun();
} catch (ProcessFailedException $applyException) {
throw new ApplierException($exception->getMessage(), $exception->getCode());
@@ -94,6 +122,7 @@ public function revert(string $path, string $id): string
*/
public function status(string $patchContent): string
{
+ $patchContent = $this->prepareContent($patchContent);
try {
$this->processFactory->create(['git', 'apply', '--check'], $patchContent)
->mustRun();
@@ -110,4 +139,51 @@ public function status(string $patchContent): string
return StatusPool::NOT_APPLIED;
}
+
+ /**
+ * Checks if the patch can be applied.
+ *
+ * @param string $patchContent
+ * @return boolean
+ */
+ public function checkApply(string $patchContent): bool
+ {
+ $patchContent = $this->prepareContent($patchContent);
+ try {
+ $this->processFactory->create(['git', 'apply', '--check'], $patchContent)
+ ->mustRun();
+ } catch (ProcessFailedException $exception) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns patch content.
+ *
+ * @param string $path
+ * @return string
+ */
+ private function readContent(string $path): string
+ {
+ $content = $this->filesystem->get($path);
+
+ return $this->prepareContent($content);
+ }
+
+ /**
+ * Prepares patch content.
+ *
+ * @param string $content
+ * @return string
+ */
+ private function prepareContent(string $content): string
+ {
+ if ($this->magentoVersion->isGitBased()) {
+ $content = $this->gitConverter->convert($content);
+ }
+
+ return $content;
+ }
}
diff --git a/src/Patch/Collector/CloudCollector.php b/src/Patch/Collector/CloudCollector.php
index 07b18f3..eb81889 100644
--- a/src/Patch/Collector/CloudCollector.php
+++ b/src/Patch/Collector/CloudCollector.php
@@ -7,10 +7,10 @@
namespace Magento\CloudPatches\Patch\Collector;
+use Magento\CloudPatches\Environment\Config;
use Magento\CloudPatches\Filesystem\DirectoryList;
use Magento\CloudPatches\Patch\Data\PatchInterface;
use Magento\CloudPatches\Composer\Package;
-use Magento\CloudPatches\Patch\Environment;
use Magento\CloudPatches\Patch\PatchBuilder;
use Magento\CloudPatches\Patch\PatchIntegrityException;
use Magento\CloudPatches\Patch\SourceProvider;
@@ -37,9 +37,9 @@ class CloudCollector
private $directoryList;
/**
- * @var Environment
+ * @var Config
*/
- private $environment;
+ private $envConfig;
/**
* @var PatchBuilder
@@ -50,20 +50,20 @@ class CloudCollector
* @param SourceProvider $sourceProvider
* @param Package $package
* @param DirectoryList $directoryList
- * @param Environment $environment
+ * @param Config $envConfig
* @param PatchBuilder $patchBuilder
*/
public function __construct(
SourceProvider $sourceProvider,
Package $package,
DirectoryList $directoryList,
- Environment $environment,
+ Config $envConfig,
PatchBuilder $patchBuilder
) {
$this->sourceProvider = $sourceProvider;
$this->package = $package;
$this->directoryList = $directoryList;
- $this->environment = $environment;
+ $this->envConfig = $envConfig;
$this->patchBuilder = $patchBuilder;
}
@@ -91,7 +91,7 @@ public function collect(): array
if ($this->package->matchConstraint($packageName, $packageConstraint)) {
try {
$patchPath = $this->directoryList->getPatches() . '/' . $patchFile;
- $patchType = $this->environment->isCloud()
+ $patchType = $this->envConfig->isCloud()
? PatchInterface::TYPE_REQUIRED : PatchInterface::TYPE_OPTIONAL;
$this->patchBuilder->setId($patchId);
diff --git a/src/Patch/Collector/LocalCollector.php b/src/Patch/Collector/LocalCollector.php
index 2da4c49..8f2b35c 100644
--- a/src/Patch/Collector/LocalCollector.php
+++ b/src/Patch/Collector/LocalCollector.php
@@ -48,8 +48,9 @@ public function collect(): array
$files = $this->sourceProvider->getLocalPatches();
$result = [];
foreach ($files as $file) {
- $this->patchBuilder->setId(md5($file));
- $this->patchBuilder->setTitle('../' . SourceProvider::HOT_FIXES_DIR . '/' . basename($file));
+ $shortPath = '../' . SourceProvider::HOT_FIXES_DIR . '/' . basename($file);
+ $this->patchBuilder->setId($shortPath);
+ $this->patchBuilder->setTitle($shortPath);
$this->patchBuilder->setFilename(basename($file));
$this->patchBuilder->setPath($file);
$this->patchBuilder->setType(PatchInterface::TYPE_CUSTOM);
diff --git a/src/Patch/Collector/QualityCollector.php b/src/Patch/Collector/QualityCollector.php
index 61c5991..1671119 100644
--- a/src/Patch/Collector/QualityCollector.php
+++ b/src/Patch/Collector/QualityCollector.php
@@ -7,13 +7,13 @@
namespace Magento\CloudPatches\Patch\Collector;
+use Magento\CloudPatches\Composer\QualityPackage;
use Magento\CloudPatches\Patch\Data\PatchInterface;
use Magento\CloudPatches\Composer\Package;
use Magento\CloudPatches\Patch\PatchBuilder;
use Magento\CloudPatches\Patch\PatchIntegrityException;
use Magento\CloudPatches\Patch\SourceProvider;
use Magento\CloudPatches\Patch\SourceProviderException;
-use Magento\QualityPatches\Info as QualityPatchesInfo;
/**
* Collects patches.
@@ -59,9 +59,9 @@ class QualityCollector
private $package;
/**
- * @var QualityPatchesInfo
+ * @var QualityPackage
*/
- private $qualityPatchesInfo;
+ private $qualityPackage;
/**
* @var array|null
@@ -76,18 +76,18 @@ class QualityCollector
/**
* @param SourceProvider $sourceProvider
* @param Package $package
- * @param QualityPatchesInfo $qualityPatchesInfo
+ * @param QualityPackage $qualityPackage
* @param PatchBuilder $patchBuilder
*/
public function __construct(
SourceProvider $sourceProvider,
Package $package,
- QualityPatchesInfo $qualityPatchesInfo,
+ QualityPackage $qualityPackage,
PatchBuilder $patchBuilder
) {
$this->sourceProvider = $sourceProvider;
$this->package = $package;
- $this->qualityPatchesInfo = $qualityPatchesInfo;
+ $this->qualityPackage = $qualityPackage;
$this->patchBuilder = $patchBuilder;
}
@@ -204,7 +204,7 @@ private function createPatch(
bool $patchDeprecated
): PatchInterface {
try {
- $patchPath = $this->qualityPatchesInfo->getPatchesDirectory() . '/' . $patchFile;
+ $patchPath = $this->qualityPackage->getPatchesDirectory() . '/' . $patchFile;
$this->patchBuilder->setId($patchId);
$this->patchBuilder->setTitle($patchTitle);
$this->patchBuilder->setFilename($patchFile);
diff --git a/src/Patch/Conflict/Analyzer.php b/src/Patch/Conflict/Analyzer.php
new file mode 100644
index 0000000..3862823
--- /dev/null
+++ b/src/Patch/Conflict/Analyzer.php
@@ -0,0 +1,188 @@
+optionalPool = $optionalPool;
+ $this->envConfig = $envConfig;
+ $this->rollbackProcessor = $rollbackProcessor;
+ $this->applyChecker = $applyChecker;
+ }
+
+ /**
+ * Returns details about patch conflict.
+ *
+ * Identifies which particular patch(es) leads to conflict.
+ * Works only on Cloud since we need to have a clean Magento instance before analyzing.
+ *
+ * @param PatchInterface $failedPatch
+ * @param array $patchFilter
+ * @return string
+ */
+ public function analyze(PatchInterface $failedPatch, array $patchFilter = []): string
+ {
+ if (!$this->envConfig->isCloud()) {
+ return '';
+ }
+
+ if ($failedPatch->getType() !== PatchInterface::TYPE_REQUIRED) {
+ $this->cleanupInstance();
+ }
+ $id = $failedPatch->getId();
+
+ return $this->analyzeRequired($id) ?: $this->analyzeOptional($id, $patchFilter);
+ }
+
+ /**
+ * Returns details about conflict with optional patches.
+ *
+ * @param string $failedPatchId
+ * @param array $patchFilter
+ * @return string
+ */
+ private function analyzeOptional(string $failedPatchId, array $patchFilter = []): string
+ {
+ $optionalPatchIds = $patchFilter ?: $this->optionalPool->getIdsByType(PatchInterface::TYPE_OPTIONAL);
+ $ids = $this->getIncompatiblePatches($optionalPatchIds, $failedPatchId);
+ if ($ids) {
+ $errorMessage = sprintf(
+ 'Patch %s is not compatible with optional: %s',
+ $failedPatchId,
+ implode(' ', $ids)
+ );
+ }
+
+ return $errorMessage;
+ }
+
+ /**
+ * Returns details about conflict with required patch.
+ *
+ * @param string $failedPatchId
+ * @return string
+ */
+ private function analyzeRequired(string $failedPatchId): string
+ {
+ $requiredPatchIds = $this->optionalPool->getIdsByType(PatchInterface::TYPE_REQUIRED);
+ $poolToCompare = array_diff($requiredPatchIds, [$failedPatchId]);
+ if ($this->applyChecker->check(array_merge($poolToCompare, [$failedPatchId]))) {
+ return '';
+ }
+
+ while (count($poolToCompare)) {
+ $patchId = array_pop($poolToCompare);
+ if ($this->applyChecker->check(array_merge($poolToCompare, [$failedPatchId]))) {
+ return sprintf(
+ 'Patch %s is not compatible with required: %s',
+ $failedPatchId,
+ $patchId
+ );
+ }
+ }
+
+ if (!$this->applyChecker->check([$failedPatchId])) {
+ return 'Patch ' . $failedPatchId . ' can\'t be applied to clean Magento instance';
+ }
+
+ return '';
+ }
+
+ /**
+ * Returns ids of incompatible patches.
+ *
+ * @param string[] $patchesToCompare
+ * @param string $patchId
+ * @return array
+ */
+ private function getIncompatiblePatches(array $patchesToCompare, string $patchId): array
+ {
+ $result = [];
+ $patchesToCompare = array_diff($patchesToCompare, [$patchId]);
+ foreach ($patchesToCompare as $compareId) {
+ if (!$this->applyChecker->check([$compareId, $patchId])) {
+ $result[] = $compareId;
+ }
+ }
+
+ foreach ($result as $key => $patchId) {
+ $dependencies = $this->optionalPool->getDependencies($patchId);
+ if (array_intersect($result, $dependencies)) {
+ unset($result[$key]);
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Cleanup instance from applied patches.
+ *
+ * @return void
+ */
+ private function cleanupInstance()
+ {
+ $requiredPatches = $this->getRequiredPatches();
+ $this->rollbackProcessor->process($requiredPatches);
+ }
+
+ /**
+ * Returns all patches of type 'Required'.
+ *
+ * @return PatchInterface[]
+ */
+ private function getRequiredPatches(): array
+ {
+ return array_filter(
+ $this->optionalPool->getList(),
+ function ($patch) {
+ return $patch->getType() === PatchInterface::TYPE_REQUIRED;
+ }
+ );
+ }
+}
diff --git a/src/Patch/Conflict/ApplyChecker.php b/src/Patch/Conflict/ApplyChecker.php
new file mode 100644
index 0000000..a126fe6
--- /dev/null
+++ b/src/Patch/Conflict/ApplyChecker.php
@@ -0,0 +1,79 @@
+applier = $applier;
+ $this->optionalPool = $optionalPool;
+ $this->filesystem = $filesystem;
+ }
+
+ /**
+ * Returns true if listed patches with all dependencies can be applied to clean Magento instance.
+ *
+ * @param string[] $patchIds
+ * @return boolean
+ */
+ public function check(array $patchIds): bool
+ {
+ $patchItems = $this->optionalPool->getList($patchIds);
+ $content = $this->getContent($patchItems);
+
+ return $this->applier->checkApply($content);
+ }
+
+ /**
+ * Returns aggregated patch content.
+ *
+ * @param PatchInterface[] $patches
+ * @return string
+ */
+ private function getContent(array $patches): string
+ {
+ $result = '';
+ foreach ($patches as $patch) {
+ $result .= $this->filesystem->get($patch->getPath());
+ }
+
+ return $result;
+ }
+}
diff --git a/src/Patch/Conflict/Processor.php b/src/Patch/Conflict/Processor.php
new file mode 100644
index 0000000..2927899
--- /dev/null
+++ b/src/Patch/Conflict/Processor.php
@@ -0,0 +1,93 @@
+renderer = $renderer;
+ $this->logger = $logger;
+ $this->conflictAnalyzer = $conflictAnalyzer;
+ $this->rollbackProcessor = $rollbackProcessor;
+ }
+
+ /**
+ * Makes rollback of applied patches and provides conflict details.
+ *
+ * @param OutputInterface $output
+ * @param PatchInterface $patch
+ * @param array $appliedPatches
+ * @param string $exceptionMessage
+ * @throws RuntimeException
+ */
+ public function process(
+ OutputInterface $output,
+ PatchInterface $patch,
+ array $appliedPatches,
+ string $exceptionMessage
+ ) {
+ $errorMessage = 'Error: patch conflict happened';
+ $this->logger->error($errorMessage);
+ $output->writeln('' . $errorMessage . '');
+
+ $messages = $this->rollbackProcessor->process($appliedPatches);
+ $output->writeln($messages);
+ $conflictDetails = $this->conflictAnalyzer->analyze($patch);
+ $errorMessage = sprintf(
+ 'Applying patch %s (%s) failed.%s%s',
+ $patch->getId(),
+ $patch->getPath(),
+ $this->renderer->formatErrorOutput($exceptionMessage),
+ $conflictDetails ? PHP_EOL . $conflictDetails : ''
+ );
+
+ throw new RuntimeException($errorMessage);
+ }
+}
diff --git a/src/Patch/Environment.php b/src/Patch/Environment.php
deleted file mode 100644
index 2f52073..0000000
--- a/src/Patch/Environment.php
+++ /dev/null
@@ -1,31 +0,0 @@
- 'app/code/Magento/',
+ self::ADMINHTML_DESIGN => 'app/design/adminhtml/Magento/',
+ self::FRONTEND_DESIGN => 'app/design/frontend/Magento/',
+ self::LIBRARY_AMPQ => 'lib/internal/Magento/Framework/Amqp/',
+ self::LIBRARY_BULK => 'lib/internal/Magento/Framework/Bulk/',
+ self::LIBRARY_FOREIGN_KEY => 'lib/internal/Magento/Framework/ForeignKey/',
+ self::LIBRARY_MESSAGE_QUEUE => 'lib/internal/Magento/Framework/MessageQueue/',
+ self::LIBRARY => 'lib/internal/Magento/Framework/'
+ ];
+
+ /**
+ * @var string[]
+ */
+ private $composerPath = [
+ self::MODULE => 'vendor/magento/module-',
+ self::ADMINHTML_DESIGN => 'vendor/magento/theme-adminhtml-',
+ self::FRONTEND_DESIGN => 'vendor/magento/theme-frontend-',
+ self::LIBRARY_AMPQ => 'vendor/magento/framework-ampq/',
+ self::LIBRARY_BULK => 'vendor/magento/framework-bulk/',
+ self::LIBRARY_FOREIGN_KEY => 'vendor/magento/framework-foreign-key/',
+ self::LIBRARY_MESSAGE_QUEUE => 'vendor/magento/framework-message-queue/',
+ self::LIBRARY => 'vendor/magento/framework/'
+ ];
+
+ /**
+ * Converts patch content from composer-based to git-based.
+ *
+ * @param string $content
+ * @return string
+ */
+ public function convert(string $content): string
+ {
+ foreach ($this->composerPath as $type => $path) {
+ $escapedPath = addcslashes($path, '/');
+ $needProcess = $type !== self::FRONTEND_DESIGN && $type !== self::ADMINHTML_DESIGN;
+
+ /**
+ * phpcs:disable
+ * Example:
+ * ( 1 ) ( 2 )( 3 ) ( 4 )( 5 )
+ * diff --git a/vendor/magento/module-some-module/Some/Path/File.ext b/vendor/magento/module-some-module/Some/Path/File.ext
+ *
+ * ( 1 ) ()( 3 ) ()( 5 )
+ * diff --git a/vendor/magento/framework-message-queue/Config.php b/vendor/magento/framework-message-queue/Config.php
+ * phpcs:enable
+ */
+ $regex = '~(^diff -(?:.*?)\s+(?:a\/)?)' . $escapedPath . '([-\w]+\/)?([^\s]+\s+(?:b\/)?)' .
+ $escapedPath . '([-\w]+\/)?([^\s]+)$~m';
+ $content = preg_replace_callback(
+ $regex,
+ function ($matches) use ($type, $needProcess) {
+ return $matches[1] . $this->nonComposerPath[$type]
+ . ($needProcess ? $this->dashedStringToCamelCase($matches[2]) : $matches[2])
+ . $matches[3] . $this->nonComposerPath[$type]
+ . ($needProcess ? $this->dashedStringToCamelCase($matches[4]) : $matches[4])
+ . $matches[5];
+ },
+ $content
+ );
+
+ // ( 1 ) ( 2 )
+ // +++ b/vendor/magento/module-some-module...
+ $content = preg_replace_callback(
+ '~(^(?:---|\+\+\+|Index:)\s+(?:a\/|b\/)?)' . $escapedPath . '([-\w]+)~m',
+ function ($matches) use ($type, $needProcess) {
+ return $matches[1] . $this->nonComposerPath[$type]
+ . ($needProcess ? $this->dashedStringToCamelCase($matches[2]) : $matches[2]);
+ },
+ $content
+ );
+
+ // ( 1 ) ( 2 )
+ // rename from vendor/magento/module-some-module...
+ $content = preg_replace_callback(
+ '~(^rename\s+(?:from|to)\s+)' . $escapedPath . '([-\w]+)~m',
+ function ($matches) use ($type, $needProcess) {
+ return $matches[1] . $this->nonComposerPath[$type]
+ . ($needProcess ? $this->dashedStringToCamelCase($matches[2]) : $matches[2]);
+ },
+ $content
+ );
+ }
+
+ return $content;
+ }
+
+ /**
+ * Converts string to camel case.
+ *
+ * @param string $string
+ * @return string
+ */
+ private function dashedStringToCamelCase(string $string): string
+ {
+ return str_replace('-', '', ucwords($string, '-'));
+ }
+}
diff --git a/src/Patch/Pool/OptionalPool.php b/src/Patch/Pool/OptionalPool.php
index f26a1b7..80c3cf0 100644
--- a/src/Patch/Pool/OptionalPool.php
+++ b/src/Patch/Pool/OptionalPool.php
@@ -121,6 +121,24 @@ public function getDependentOn($patchId)
return $result;
}
+ /**
+ * Returns patch dependency ids.
+ *
+ * @param string $patchId
+ * @return string[]
+ */
+ public function getDependencies($patchId)
+ {
+ $result = array_map(
+ function (PatchInterface $patch) {
+ return $patch->getId();
+ },
+ $this->getAdditionalRequiredPatches([$patchId])
+ );
+
+ return array_unique($result);
+ }
+
/**
* Returns required patches which are not included in patch filter.
*
@@ -167,6 +185,31 @@ public function getReplacedBy($patchId)
return array_unique($result);
}
+ /**
+ * Returns not deprecated patch ids by type.
+ *
+ * @param string $type
+ * @return string[]
+ */
+ public function getIdsByType($type)
+ {
+ $items = array_filter(
+ $this->items,
+ function ($patch) use ($type) {
+ return !$patch->isDeprecated() && $patch->getType() === $type;
+ }
+ );
+
+ $result = array_map(
+ function (PatchInterface $patch) {
+ return $patch->getId();
+ },
+ $items
+ );
+
+ return array_unique($result);
+ }
+
/**
* Validates search result.
*
diff --git a/src/Patch/RollbackProcessor.php b/src/Patch/RollbackProcessor.php
new file mode 100644
index 0000000..f6cbfb1
--- /dev/null
+++ b/src/Patch/RollbackProcessor.php
@@ -0,0 +1,68 @@
+applier = $applier;
+ $this->logger = $logger;
+ }
+
+ /**
+ * Rollback applied patches.
+ *
+ * @param PatchInterface[] $appliedPatches
+ * @return string[]
+ */
+ public function process(array $appliedPatches): array
+ {
+ if (empty($appliedPatches)) {
+ return [];
+ }
+
+ $message = 'Start of rollback';
+ $this->logger->info($message);
+ $messages[] = $message;
+
+ foreach (array_reverse($appliedPatches) as $appliedPatch) {
+ $message = $this->applier->revert($appliedPatch->getPath(), $appliedPatch->getId());
+ $messages[] = $message;
+ $this->logger->info($message, ['file' => $appliedPatch->getPath()]);
+ }
+
+ $message = 'End of rollback';
+ $this->logger->info($message);
+ $messages[] = $message;
+
+ return $messages;
+ }
+}
diff --git a/src/Patch/SourceProvider.php b/src/Patch/SourceProvider.php
index a837318..743fdbd 100644
--- a/src/Patch/SourceProvider.php
+++ b/src/Patch/SourceProvider.php
@@ -7,10 +7,10 @@
namespace Magento\CloudPatches\Patch;
+use Magento\CloudPatches\Composer\QualityPackage;
use Magento\CloudPatches\Filesystem\DirectoryList;
use Magento\CloudPatches\Filesystem\FileList;
use Magento\CloudPatches\Filesystem\FileSystemException;
-use Magento\QualityPatches\Info as QualityPatchesInfo;
use Magento\CloudPatches\Filesystem\Filesystem;
/**
@@ -39,26 +39,26 @@ class SourceProvider
private $directoryList;
/**
- * @var QualityPatchesInfo
+ * @var QualityPackage
*/
- private $qualityPatchesInfo;
+ private $qualityPackage;
/**
* @param Filesystem $filesystem
* @param FileList $fileList
* @param DirectoryList $directoryList
- * @param QualityPatchesInfo $qualityPatchesInfo
+ * @param QualityPackage $qualityPackage
*/
public function __construct(
Filesystem $filesystem,
FileList $fileList,
DirectoryList $directoryList,
- QualityPatchesInfo $qualityPatchesInfo
+ QualityPackage $qualityPackage
) {
$this->filesystem = $filesystem;
$this->fileList = $fileList;
$this->directoryList = $directoryList;
- $this->qualityPatchesInfo = $qualityPatchesInfo;
+ $this->qualityPackage = $qualityPackage;
}
/**
@@ -93,9 +93,9 @@ public function getCloudPatches(): array
*/
public function getQualityPatches(): array
{
- $configSupportPath = $this->qualityPatchesInfo->getPatchesConfig();
+ $configPath = $this->qualityPackage->getPatchesConfig();
- return $this->readConfiguration($configSupportPath);
+ return $configPath ? $this->readConfiguration($configPath) : [];
}
/**
diff --git a/src/Test/Unit/Command/ApplyTest.php b/src/Test/Unit/Command/ApplyTest.php
index 85f3578..b88db0d 100644
--- a/src/Test/Unit/Command/ApplyTest.php
+++ b/src/Test/Unit/Command/ApplyTest.php
@@ -10,11 +10,8 @@
use Magento\CloudPatches\App\RuntimeException;
use Magento\CloudPatches\Command\AbstractCommand;
use Magento\CloudPatches\Command\Apply;
-use Magento\CloudPatches\Command\Process\ApplyLocal;
use Magento\CloudPatches\Command\Process\ApplyOptional;
-use Magento\CloudPatches\Command\Process\ApplyRequired;
use Magento\CloudPatches\Composer\MagentoVersion;
-use Magento\CloudPatches\Patch\Environment;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
@@ -31,26 +28,11 @@ class ApplyTest extends TestCase
*/
private $command;
- /**
- * @var ApplyLocal|MockObject
- */
- private $applyLocal;
-
/**
* @var ApplyOptional|MockObject
*/
private $applyOptional;
- /**
- * @var ApplyRequired|MockObject
- */
- private $applyRequired;
-
- /**
- * @var Environment|MockObject
- */
- private $environment;
-
/**
* @var LoggerInterface|MockObject
*/
@@ -66,68 +48,29 @@ class ApplyTest extends TestCase
*/
protected function setUp()
{
- $this->applyLocal = $this->createMock(ApplyLocal::class);
$this->applyOptional = $this->createMock(ApplyOptional::class);
- $this->applyRequired = $this->createMock(ApplyRequired::class);
- $this->environment = $this->createMock(Environment::class);
$this->logger = $this->getMockForAbstractClass(LoggerInterface::class);
$this->magentoVersion = $this->createMock(MagentoVersion::class);
$this->command = new Apply(
- $this->applyRequired,
$this->applyOptional,
- $this->applyLocal,
- $this->environment,
$this->logger,
$this->magentoVersion
);
}
/**
- * Tests successful command execution on Cloud environment.
+ * Tests successful command execution.
*/
- public function testCloudEnvironmentSuccess()
+ public function testExecute()
{
/** @var InputInterface|MockObject $inputMock */
$inputMock = $this->getMockForAbstractClass(InputInterface::class);
/** @var OutputInterface|MockObject $outputMock */
$outputMock = $this->getMockForAbstractClass(OutputInterface::class);
- $this->environment->method('isCloud')
- ->willReturn(true);
-
- $this->applyLocal->expects($this->once())
- ->method('run');
$this->applyOptional->expects($this->once())
->method('run');
- $this->applyRequired->expects($this->once())
- ->method('run');
-
- $this->assertEquals(
- AbstractCommand::RETURN_SUCCESS,
- $this->command->execute($inputMock, $outputMock)
- );
- }
-
- /**
- * Tests successful command execution on OnPrem environment.
- */
- public function testOnPremEnvironmentSuccess()
- {
- /** @var InputInterface|MockObject $inputMock */
- $inputMock = $this->getMockForAbstractClass(InputInterface::class);
- /** @var OutputInterface|MockObject $outputMock */
- $outputMock = $this->getMockForAbstractClass(OutputInterface::class);
-
- $this->environment->method('isCloud')
- ->willReturn(false);
-
- $this->applyLocal->expects($this->never())
- ->method('run');
- $this->applyOptional->expects($this->once())
- ->method('run');
- $this->applyRequired->expects($this->never())
- ->method('run');
$this->assertEquals(
AbstractCommand::RETURN_SUCCESS,
@@ -176,32 +119,4 @@ public function testCriticalError()
$this->expectException(\InvalidArgumentException::class);
$this->command->execute($inputMock, $outputMock);
}
-
- /**
- * Tests when Magento is installed from Git.
- */
- public function testGitBasedInstallation()
- {
- /** @var InputInterface|MockObject $inputMock */
- $inputMock = $this->getMockForAbstractClass(InputInterface::class);
- /** @var OutputInterface|MockObject $outputMock */
- $outputMock = $this->getMockForAbstractClass(OutputInterface::class);
-
- $inputMock->expects($this->once())
- ->method('getOption')
- ->with(Apply::OPT_GIT_INSTALLATION)
- ->willReturn(1);
-
- $this->applyLocal->expects($this->never())
- ->method('run');
- $this->applyOptional->expects($this->never())
- ->method('run');
- $this->applyRequired->expects($this->never())
- ->method('run');
-
- $this->assertEquals(
- AbstractCommand::RETURN_SUCCESS,
- $this->command->execute($inputMock, $outputMock)
- );
- }
}
diff --git a/src/Test/Unit/Command/Ece/ApplyTest.php b/src/Test/Unit/Command/Ece/ApplyTest.php
new file mode 100644
index 0000000..8b9906c
--- /dev/null
+++ b/src/Test/Unit/Command/Ece/ApplyTest.php
@@ -0,0 +1,142 @@
+applyLocal = $this->createMock(ApplyLocal::class);
+ $this->applyOptionalEce = $this->createMock(ApplyOptional::class);
+ $this->applyRequired = $this->createMock(ApplyRequired::class);
+ $this->logger = $this->getMockForAbstractClass(LoggerInterface::class);
+ $this->magentoVersion = $this->createMock(MagentoVersion::class);
+
+ $this->command = new Apply(
+ $this->applyRequired,
+ $this->applyOptionalEce,
+ $this->applyLocal,
+ $this->logger,
+ $this->magentoVersion
+ );
+ }
+
+ /**
+ * Tests successful command execution - Cloud environment.
+ */
+ public function testExecute()
+ {
+ /** @var InputInterface|MockObject $inputMock */
+ $inputMock = $this->getMockForAbstractClass(InputInterface::class);
+ /** @var OutputInterface|MockObject $outputMock */
+ $outputMock = $this->getMockForAbstractClass(OutputInterface::class);
+
+ $this->applyLocal->expects($this->once())
+ ->method('run');
+ $this->applyOptionalEce->expects($this->once())
+ ->method('run');
+ $this->applyRequired->expects($this->once())
+ ->method('run');
+
+ $this->assertEquals(
+ AbstractCommand::RETURN_SUCCESS,
+ $this->command->execute($inputMock, $outputMock)
+ );
+ }
+
+ /**
+ * Tests when runtime error happens during command execution.
+ */
+ public function testRuntimeError()
+ {
+ /** @var InputInterface|MockObject $inputMock */
+ $inputMock = $this->getMockForAbstractClass(InputInterface::class);
+ /** @var OutputInterface|MockObject $outputMock */
+ $outputMock = $this->getMockForAbstractClass(OutputInterface::class);
+
+ $this->applyOptionalEce->expects($this->once())
+ ->method('run')
+ ->willThrowException(new RuntimeException('Error!'));
+ $this->logger->expects($this->once())
+ ->method('error');
+
+ $this->assertEquals(
+ AbstractCommand::RETURN_FAILURE,
+ $this->command->execute($inputMock, $outputMock)
+ );
+ }
+
+ /**
+ * Tests when critical error happens during command execution.
+ */
+ public function testCriticalError()
+ {
+ /** @var InputInterface|MockObject $inputMock */
+ $inputMock = $this->getMockForAbstractClass(InputInterface::class);
+ /** @var OutputInterface|MockObject $outputMock */
+ $outputMock = $this->getMockForAbstractClass(OutputInterface::class);
+
+ $this->applyOptionalEce->expects($this->once())
+ ->method('run')
+ ->willThrowException(new \InvalidArgumentException('Critical error!'));
+ $this->logger->expects($this->once())
+ ->method('critical');
+
+ $this->expectException(\InvalidArgumentException::class);
+ $this->command->execute($inputMock, $outputMock);
+ }
+}
diff --git a/src/Test/Unit/Command/Ece/RevertTest.php b/src/Test/Unit/Command/Ece/RevertTest.php
new file mode 100644
index 0000000..55f9075
--- /dev/null
+++ b/src/Test/Unit/Command/Ece/RevertTest.php
@@ -0,0 +1,119 @@
+revertEce = $this->createMock(RevertProcess::class);
+ $this->logger = $this->getMockForAbstractClass(LoggerInterface::class);
+
+ /** @var MagentoVersion|MockObject $magentoVersion */
+ $magentoVersion = $this->createMock(MagentoVersion::class);
+
+ $this->command = new Revert(
+ $this->revertEce,
+ $this->logger,
+ $magentoVersion
+ );
+ }
+
+ /**
+ * Tests successful command execution.
+ */
+ public function testRevertSuccess()
+ {
+ /** @var InputInterface|MockObject $inputMock */
+ $inputMock = $this->getMockForAbstractClass(InputInterface::class);
+ /** @var OutputInterface|MockObject $outputMock */
+ $outputMock = $this->getMockForAbstractClass(OutputInterface::class);
+
+ $this->revertEce->expects($this->once())
+ ->method('run');
+
+ $this->assertEquals(
+ AbstractCommand::RETURN_SUCCESS,
+ $this->command->execute($inputMock, $outputMock)
+ );
+ }
+
+ /**
+ * Tests when runtime error happens during command execution.
+ */
+ public function testRuntimeError()
+ {
+ /** @var InputInterface|MockObject $inputMock */
+ $inputMock = $this->getMockForAbstractClass(InputInterface::class);
+ /** @var OutputInterface|MockObject $outputMock */
+ $outputMock = $this->getMockForAbstractClass(OutputInterface::class);
+
+ $this->revertEce->expects($this->once())
+ ->method('run')
+ ->willThrowException(new RuntimeException('Error!'));
+ $this->logger->expects($this->once())
+ ->method('error');
+
+ $this->assertEquals(
+ AbstractCommand::RETURN_FAILURE,
+ $this->command->execute($inputMock, $outputMock)
+ );
+ }
+
+ /**
+ * Tests when critical error happens during command execution.
+ */
+ public function testCriticalError()
+ {
+ /** @var InputInterface|MockObject $inputMock */
+ $inputMock = $this->getMockForAbstractClass(InputInterface::class);
+ /** @var OutputInterface|MockObject $outputMock */
+ $outputMock = $this->getMockForAbstractClass(OutputInterface::class);
+
+ $this->revertEce->expects($this->once())
+ ->method('run')
+ ->willThrowException(new \InvalidArgumentException('Critical error!'));
+ $this->logger->expects($this->once())
+ ->method('critical');
+
+ $this->expectException(\InvalidArgumentException::class);
+ $this->command->execute($inputMock, $outputMock);
+ }
+}
diff --git a/src/Test/Unit/Command/Process/Action/ApplyOptionalActionTest.php b/src/Test/Unit/Command/Process/Action/ApplyOptionalActionTest.php
index 5a264a6..dabaa7f 100644
--- a/src/Test/Unit/Command/Process/Action/ApplyOptionalActionTest.php
+++ b/src/Test/Unit/Command/Process/Action/ApplyOptionalActionTest.php
@@ -12,6 +12,7 @@
use Magento\CloudPatches\Command\Process\Renderer;
use Magento\CloudPatches\Patch\Applier;
use Magento\CloudPatches\Patch\ApplierException;
+use Magento\CloudPatches\Patch\Conflict\Processor as ConflictProcessor;
use Magento\CloudPatches\Patch\Data\PatchInterface;
use Magento\CloudPatches\Patch\Pool\OptionalPool;
use Magento\CloudPatches\Patch\Status\StatusPool;
@@ -56,6 +57,11 @@ class ApplyOptionalActionTest extends TestCase
*/
private $optionalPool;
+ /**
+ * @var ConflictProcessor|MockObject
+ */
+ private $conflictProcessor;
+
/**
* @inheritdoc
*/
@@ -66,13 +72,15 @@ protected function setUp()
$this->statusPool = $this->createMock(StatusPool::class);
$this->optionalPool = $this->createMock(OptionalPool::class);
$this->renderer = $this->createMock(Renderer::class);
+ $this->conflictProcessor = $this->createMock(ConflictProcessor::class);
$this->action = new ApplyOptionalAction(
$this->applier,
$this->optionalPool,
$this->statusPool,
$this->renderer,
- $this->logger
+ $this->logger,
+ $this->conflictProcessor
);
}
@@ -229,22 +237,21 @@ public function testApplyWithException()
])->willReturnCallback(
function ($path, $id) {
if ($id === 'MC-22222') {
- throw new ApplierException('Error');
+ throw new ApplierException('Applier error message');
}
return "Patch {$path} {$id} has been applied";
}
);
- $this->renderer->expects($this->once())
- ->method('formatErrorOutput')
- ->with('Error');
-
- $this->applier->expects($this->once())
- ->method('revert')
- ->withConsecutive([$patch1->getPath(), $patch1->getId()]);
+ $this->conflictProcessor->expects($this->once())
+ ->method('process')
+ ->withConsecutive([$outputMock, $patch2, [$patch1], 'Applier error message'])
+ ->willThrowException(new RuntimeException('Error message'));
$this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('Error message');
+
$this->action->execute($inputMock, $outputMock, $patchFilter);
}
diff --git a/src/Test/Unit/Command/Process/ApplyLocalTest.php b/src/Test/Unit/Command/Process/ApplyLocalTest.php
index 7396710..cc6490c 100644
--- a/src/Test/Unit/Command/Process/ApplyLocalTest.php
+++ b/src/Test/Unit/Command/Process/ApplyLocalTest.php
@@ -14,6 +14,7 @@
use Magento\CloudPatches\Patch\ApplierException;
use Magento\CloudPatches\Patch\Data\PatchInterface;
use Magento\CloudPatches\Patch\Pool\LocalPool;
+use Magento\CloudPatches\Patch\RollbackProcessor;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
@@ -45,6 +46,11 @@ class ApplyLocalTest extends TestCase
*/
private $localPool;
+ /**
+ * @var RollbackProcessor|MockObject
+ */
+ private $rollbackProcessor;
+
/**
* @var Renderer|MockObject
*/
@@ -59,12 +65,14 @@ protected function setUp()
$this->logger = $this->getMockForAbstractClass(LoggerInterface::class);
$this->localPool = $this->createMock(LocalPool::class);
$this->renderer = $this->createMock(Renderer::class);
+ $this->rollbackProcessor = $this->createMock(RollbackProcessor::class);
$this->manager = new ApplyLocal(
$this->applier,
$this->localPool,
$this->renderer,
- $this->logger
+ $this->logger,
+ $this->rollbackProcessor
);
}
@@ -134,21 +142,38 @@ public function testApplySuccessful()
*/
public function testApplyWithException()
{
- $patch = $this->createPatch('/path/patch.patch', '../m2-hotfixes/patch.patch');
+ $patch1 = $this->createPatch('/path/patch1.patch', '../m2-hotfixes/patch1.patch');
+ $patch2 = $this->createPatch('/path/patch2.patch', '../m2-hotfixes/patch2.patch');
+ $rollbackMessages = ['Patch 1 has been reverted'];
/** @var InputInterface|MockObject $inputMock */
$inputMock = $this->getMockForAbstractClass(InputInterface::class);
/** @var OutputInterface|MockObject $outputMock */
$outputMock = $this->getMockForAbstractClass(OutputInterface::class);
$this->localPool->method('getList')
- ->willReturn([$patch]);
+ ->willReturn([$patch1, $patch2]);
$this->applier->method('apply')
- ->withConsecutive([$patch->getPath(), $patch->getTitle()])
- ->willThrowException(new ApplierException('Error'));
+ ->willReturnMap([
+ [$patch1->getPath(), $patch1->getTitle()],
+ [$patch2->getPath(), $patch2->getTitle()]
+ ])->willReturnCallback(
+ function ($path, $title) {
+ if (strpos($title, 'patch2') !== false) {
+ throw new ApplierException('Applier error message');
+ }
+
+ return "Patch {$path} {$title} has been applied";
+ }
+ );
+
+ $this->rollbackProcessor->expects($this->once())
+ ->method('process')
+ ->withConsecutive([[$patch1]])
+ ->willReturn($rollbackMessages);
$this->renderer->expects($this->once())
->method('formatErrorOutput')
- ->with('Error');
+ ->with('Applier error message');
$this->expectException(RuntimeException::class);
$this->manager->run($inputMock, $outputMock);
diff --git a/src/Test/Unit/Command/Process/ApplyOptionalTest.php b/src/Test/Unit/Command/Process/ApplyOptionalTest.php
index 38ebba3..a32b68c 100644
--- a/src/Test/Unit/Command/Process/ApplyOptionalTest.php
+++ b/src/Test/Unit/Command/Process/ApplyOptionalTest.php
@@ -26,7 +26,7 @@ class ApplyOptionalTest extends TestCase
/**
* @var ApplyOptional
*/
- private $manager;
+ private $applyOptional;
/**
* @var LoggerInterface|MockObject
@@ -52,7 +52,7 @@ protected function setUp()
$this->actionPool = $this->createMock(ActionPool::class);
$this->logger = $this->getMockForAbstractClass(LoggerInterface::class);
- $this->manager = new ApplyOptional(
+ $this->applyOptional = new ApplyOptional(
$this->filterFactory,
$this->actionPool,
$this->logger
@@ -74,7 +74,7 @@ public function testApplyWithPatchArgumentProvided()
$cliPatchArgument = ['MC-1111', 'MC-22222'];
$inputMock->expects($this->once())
->method('getArgument')
- ->with(Apply::ARG_QUALITY_PATCHES)
+ ->with(Apply::ARG_LIST_OF_PATCHES)
->willReturn($cliPatchArgument);
$this->filterFactory->method('createApplyFilter')
->with($cliPatchArgument)
@@ -84,7 +84,7 @@ public function testApplyWithPatchArgumentProvided()
->method('execute')
->withConsecutive([$inputMock, $outputMock, $cliPatchArgument]);
- $this->manager->run($inputMock, $outputMock);
+ $this->applyOptional->run($inputMock, $outputMock);
}
/**
@@ -102,7 +102,7 @@ public function testApplyWithEmptyPatchArgument()
$cliPatchArgument = [];
$inputMock->expects($this->once())
->method('getArgument')
- ->with(Apply::ARG_QUALITY_PATCHES)
+ ->with(Apply::ARG_LIST_OF_PATCHES)
->willReturn($cliPatchArgument);
$this->filterFactory->method('createApplyFilter')
->with($cliPatchArgument)
@@ -111,6 +111,6 @@ public function testApplyWithEmptyPatchArgument()
$this->actionPool->expects($this->never())
->method('execute');
- $this->manager->run($inputMock, $outputMock);
+ $this->applyOptional->run($inputMock, $outputMock);
}
}
diff --git a/src/Test/Unit/Command/Process/ApplyRequiredTest.php b/src/Test/Unit/Command/Process/ApplyRequiredTest.php
index 04562de..9675f5c 100644
--- a/src/Test/Unit/Command/Process/ApplyRequiredTest.php
+++ b/src/Test/Unit/Command/Process/ApplyRequiredTest.php
@@ -12,6 +12,7 @@
use Magento\CloudPatches\Command\Process\Renderer;
use Magento\CloudPatches\Patch\Applier;
use Magento\CloudPatches\Patch\ApplierException;
+use Magento\CloudPatches\Patch\Conflict\Processor as ConflictProcessor;
use Magento\CloudPatches\Patch\Data\PatchInterface;
use Magento\CloudPatches\Patch\Pool\RequiredPool;
use PHPUnit\Framework\MockObject\MockObject;
@@ -50,6 +51,11 @@ class ApplyRequiredTest extends TestCase
*/
private $renderer;
+ /**
+ * @var ConflictProcessor|MockObject
+ */
+ private $conflictProcessor;
+
/**
* @inheritdoc
*/
@@ -59,12 +65,14 @@ protected function setUp()
$this->logger = $this->getMockForAbstractClass(LoggerInterface::class);
$this->requiredPool = $this->createMock(RequiredPool::class);
$this->renderer = $this->createMock(Renderer::class);
+ $this->conflictProcessor = $this->createMock(ConflictProcessor::class);
$this->manager = new ApplyRequired(
$this->applier,
$this->requiredPool,
$this->renderer,
- $this->logger
+ $this->logger,
+ $this->conflictProcessor
);
}
@@ -122,12 +130,15 @@ public function testApplyWithException()
$this->applier->method('apply')
->withConsecutive([$patch->getPath(), $patch->getId()])
- ->willThrowException(new ApplierException('Error'));
- $this->renderer->expects($this->once())
- ->method('formatErrorOutput')
- ->with('Error');
+ ->willThrowException(new ApplierException('Applier error message'));
+ $this->conflictProcessor->expects($this->once())
+ ->method('process')
+ ->withConsecutive([$outputMock, $patch, [], 'Applier error message'])
+ ->willThrowException(new RuntimeException('Error message'));
$this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('Error message');
+
$this->manager->run($inputMock, $outputMock);
}
diff --git a/src/Test/Unit/Command/Process/Ece/ApplyOptionalTest.php b/src/Test/Unit/Command/Process/Ece/ApplyOptionalTest.php
new file mode 100644
index 0000000..f269a98
--- /dev/null
+++ b/src/Test/Unit/Command/Process/Ece/ApplyOptionalTest.php
@@ -0,0 +1,121 @@
+filterFactory = $this->createMock(FilterFactory::class);
+ $this->actionPool = $this->createMock(ActionPool::class);
+ $this->logger = $this->getMockForAbstractClass(LoggerInterface::class);
+ $this->config = $this->createMock(Config::class);
+
+ $this->applyOptionalEce = new ApplyOptional(
+ $this->filterFactory,
+ $this->actionPool,
+ $this->logger,
+ $this->config
+ );
+ }
+
+ /**
+ * Tests successful optional patches applying.
+ *
+ * @throws RuntimeException
+ */
+ public function testApplyWithPatchEnvVariableProvided()
+ {
+ /** @var InputInterface|MockObject $inputMock */
+ $inputMock = $this->getMockForAbstractClass(InputInterface::class);
+ /** @var OutputInterface|MockObject $outputMock */
+ $outputMock = $this->getMockForAbstractClass(OutputInterface::class);
+
+ $configQualityPatches = ['MC-1111', 'MC-22222'];
+ $this->config->expects($this->once())
+ ->method('getQualityPatches')
+ ->willReturn($configQualityPatches);
+ $this->filterFactory->method('createApplyFilter')
+ ->with($configQualityPatches)
+ ->willReturn($configQualityPatches);
+
+ $this->actionPool->expects($this->once())
+ ->method('execute')
+ ->withConsecutive([$inputMock, $outputMock, $configQualityPatches]);
+
+ $this->applyOptionalEce->run($inputMock, $outputMock);
+ }
+
+ /**
+ * Tests optional patches applying when QUALITY_PATCHES env variable is empty.
+ *
+ * @throws RuntimeException
+ */
+ public function testApplyWithEmptyPatchEnvVariable()
+ {
+ /** @var InputInterface|MockObject $inputMock */
+ $inputMock = $this->getMockForAbstractClass(InputInterface::class);
+ /** @var OutputInterface|MockObject $outputMock */
+ $outputMock = $this->getMockForAbstractClass(OutputInterface::class);
+
+ $configQualityPatches = [];
+ $this->config->expects($this->once())
+ ->method('getQualityPatches')
+ ->willReturn($configQualityPatches);
+ $this->filterFactory->method('createApplyFilter')
+ ->with($configQualityPatches)
+ ->willReturn(null);
+
+ $this->actionPool->expects($this->never())
+ ->method('execute');
+
+ $this->applyOptionalEce->run($inputMock, $outputMock);
+ }
+}
diff --git a/src/Test/Unit/Command/Process/Ece/RevertTest.php b/src/Test/Unit/Command/Process/Ece/RevertTest.php
new file mode 100644
index 0000000..82dacb0
--- /dev/null
+++ b/src/Test/Unit/Command/Process/Ece/RevertTest.php
@@ -0,0 +1,196 @@
+revertAction = $this->createMock(RevertAction::class);
+ $this->logger = $this->getMockForAbstractClass(LoggerInterface::class);
+ $this->applier = $this->createMock(Applier::class);
+ $this->localPool = $this->createMock(LocalPool::class);
+ $this->renderer = $this->createMock(Renderer::class);
+ $this->statusPool = $this->createMock(StatusPool::class);
+
+ $this->revertEce = new Revert(
+ $this->revertAction,
+ $this->logger,
+ $this->applier,
+ $this->localPool,
+ $this->renderer,
+ $this->statusPool
+ );
+ }
+
+ /**
+ * Tests successful patches reverting.
+ *
+ * @throws RuntimeException
+ */
+ public function testRevertSuccessful()
+ {
+ $patch1 = $this->createPatch('/path/patch1.patch', '../m2-hotfixes/patch1.patch');
+ $patch2 = $this->createPatch('/path/patch2.patch', '../m2-hotfixes/patch2.patch');
+ $patch3 = $this->createPatch('/path/patch3.patch', '../m2-hotfixes/patch3.patch');
+ $this->statusPool->method('isNotApplied')
+ ->willReturnMap([
+ ['../m2-hotfixes/patch1.patch', false],
+ ['../m2-hotfixes/patch2.patch', false],
+ ['../m2-hotfixes/patch3.patch', true]
+ ]);
+
+ /** @var InputInterface|MockObject $inputMock */
+ $inputMock = $this->getMockForAbstractClass(InputInterface::class);
+ /** @var OutputInterface|MockObject $outputMock */
+ $outputMock = $this->getMockForAbstractClass(OutputInterface::class);
+ $this->localPool->method('getList')
+ ->willReturn([$patch1, $patch2, $patch3]);
+
+ $this->applier->method('revert')
+ ->willReturnMap([
+ [$patch2->getPath(), $patch2->getTitle(), 'Patch ' . $patch2->getTitle() .' has been reverted'],
+ [$patch1->getPath(), $patch1->getTitle(), 'Patch ' . $patch1->getTitle() .' has been reverted'],
+ ]);
+
+ $outputMock->expects($this->exactly(4))
+ ->method('writeln')
+ ->withConsecutive(
+ [$this->anything()],
+ [$this->stringContains('Patch ' . $patch2->getTitle() .' has been reverted')],
+ [$this->stringContains('Patch ' . $patch1->getTitle() .' has been reverted')]
+ );
+
+ $this->revertAction->expects($this->once())
+ ->method('execute')
+ ->withConsecutive([$inputMock, $outputMock, []]);
+
+ $this->revertEce->run($inputMock, $outputMock);
+ }
+
+ /**
+ * Tests patches reverting with exception.
+ *
+ * @throws RuntimeException
+ */
+ public function testRevertWithError()
+ {
+ $patch1 = $this->createPatch('/path/patch1.patch', '../m2-hotfixes/patch1.patch');
+ $patch2 = $this->createPatch('/path/patch2.patch', '../m2-hotfixes/patch2.patch');
+ $this->statusPool->method('isNotApplied')
+ ->willReturnMap([
+ ['../m2-hotfixes/patch1.patch', false],
+ ['../m2-hotfixes/patch2.patch', false]
+ ]);
+
+ /** @var InputInterface|MockObject $inputMock */
+ $inputMock = $this->getMockForAbstractClass(InputInterface::class);
+ /** @var OutputInterface|MockObject $outputMock */
+ $outputMock = $this->getMockForAbstractClass(OutputInterface::class);
+ $this->localPool->method('getList')
+ ->willReturn([$patch1, $patch2]);
+
+ $this->applier->method('revert')
+ ->willReturnMap([
+ [$patch1->getPath(), $patch1->getTitle()],
+ [$patch2->getPath(), $patch2->getTitle()]
+ ])->willReturnCallback(
+ function ($path, $title) {
+ if (strpos($title, 'patch2') !== false) {
+ throw new ApplierException('Applier error message');
+ }
+
+ return "Patch {$path} {$title} has been reverted";
+ }
+ );
+
+ $this->renderer->expects($this->once())
+ ->method('formatErrorOutput')
+ ->with('Applier error message');
+
+ $this->revertAction->expects($this->once())
+ ->method('execute')
+ ->withConsecutive([$inputMock, $outputMock, []]);
+
+ $this->revertEce->run($inputMock, $outputMock);
+ }
+
+ /**
+ * Creates patch mock.
+ *
+ * @param string $path
+ * @param string $title
+ *
+ * @return PatchInterface|MockObject
+ */
+ private function createPatch(string $path, string $title)
+ {
+ $patch = $this->getMockForAbstractClass(PatchInterface::class);
+ $patch->method('getPath')->willReturn($path);
+ $patch->method('getTitle')->willReturn($title);
+ $patch->method('getId')->willReturn($title);
+
+ return $patch;
+ }
+}
diff --git a/src/Test/Unit/Command/Process/RendererTest.php b/src/Test/Unit/Command/Process/RendererTest.php
index 41c0753..f78e161 100644
--- a/src/Test/Unit/Command/Process/RendererTest.php
+++ b/src/Test/Unit/Command/Process/RendererTest.php
@@ -91,7 +91,6 @@ public function printPatchInfoDataProvider(): array
'patch' => $this->createPatch(false),
'prependedMessage' => '',
'expectedArray' => [
- 'Id: ' . self::PATCH_ID,
'Title: ' . self::PATCH_TITLE,
'File: ' . self::PATCH_FILENAME,
'Affected components: ' . implode(' ', $this->affectedComponents)
@@ -102,7 +101,6 @@ public function printPatchInfoDataProvider(): array
'prependedMessage' => 'Prepended message',
'expectedArray' => [
'Prepended message',
- 'Id: ' . self::PATCH_ID,
'Title: ' . self::PATCH_TITLE,
'File: ' . self::PATCH_FILENAME,
'Affected components: ' . implode(' ', $this->affectedComponents),
@@ -114,7 +112,6 @@ public function printPatchInfoDataProvider(): array
'prependedMessage' => 'Prepended message',
'expectedArray' => [
'Prepended message',
- 'Id: ' . self::PATCH_ID,
'Title: ' . self::PATCH_TITLE,
'File: ' . self::PATCH_FILENAME,
'Affected components: ' . implode(' ', $this->affectedComponents),
diff --git a/src/Test/Unit/Command/Process/RevertTest.php b/src/Test/Unit/Command/Process/RevertTest.php
index c0a2067..37bb7a5 100644
--- a/src/Test/Unit/Command/Process/RevertTest.php
+++ b/src/Test/Unit/Command/Process/RevertTest.php
@@ -76,7 +76,7 @@ public function testRevertWithPatchArgumentProvided()
$inputMock->expects($this->once())
->method('getArgument')
- ->with(RevertCommand::ARG_QUALITY_PATCHES)
+ ->with(RevertCommand::ARG_LIST_OF_PATCHES)
->willReturn($cliPatchArgument);
$inputMock->expects($this->once())
->method('getOption')
@@ -110,7 +110,7 @@ public function testRevertWithEmptyPatchArgument()
$inputMock->expects($this->once())
->method('getArgument')
- ->with(RevertCommand::ARG_QUALITY_PATCHES)
+ ->with(RevertCommand::ARG_LIST_OF_PATCHES)
->willReturn($cliPatchArgument);
$inputMock->expects($this->once())
->method('getOption')
diff --git a/src/Test/Unit/Command/RevertTest.php b/src/Test/Unit/Command/RevertTest.php
index 5f8ea6a..f732ea8 100644
--- a/src/Test/Unit/Command/RevertTest.php
+++ b/src/Test/Unit/Command/RevertTest.php
@@ -12,7 +12,6 @@
use Magento\CloudPatches\Command\Process\Revert as RevertProcess;
use Magento\CloudPatches\Command\Revert;
use Magento\CloudPatches\Composer\MagentoVersion;
-use Magento\CloudPatches\Patch\Environment;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
@@ -34,11 +33,6 @@ class RevertTest extends TestCase
*/
private $revert;
- /**
- * @var Environment|MockObject
- */
- private $environment;
-
/**
* @var LoggerInterface|MockObject
*/
@@ -50,7 +44,6 @@ class RevertTest extends TestCase
protected function setUp()
{
$this->revert = $this->createMock(RevertProcess::class);
- $this->environment = $this->createMock(Environment::class);
$this->logger = $this->getMockForAbstractClass(LoggerInterface::class);
/** @var MagentoVersion|MockObject $magentoVersion */
@@ -58,47 +51,21 @@ protected function setUp()
$this->command = new Revert(
$this->revert,
- $this->environment,
$this->logger,
$magentoVersion
);
}
/**
- * Tests that command is not available on Cloud environment.
+ * Tests successful command execution.
*/
- public function testCloudEnvironmentNotAvailable()
+ public function testRevertSuccess()
{
/** @var InputInterface|MockObject $inputMock */
$inputMock = $this->getMockForAbstractClass(InputInterface::class);
/** @var OutputInterface|MockObject $outputMock */
$outputMock = $this->getMockForAbstractClass(OutputInterface::class);
- $this->environment->method('isCloud')
- ->willReturn(true);
-
- $this->revert->expects($this->never())
- ->method('run');
-
- $this->assertEquals(
- AbstractCommand::RETURN_FAILURE,
- $this->command->execute($inputMock, $outputMock)
- );
- }
-
- /**
- * Tests successful command execution on OnPrem environment.
- */
- public function testOnPremEnvironmentSuccess()
- {
- /** @var InputInterface|MockObject $inputMock */
- $inputMock = $this->getMockForAbstractClass(InputInterface::class);
- /** @var OutputInterface|MockObject $outputMock */
- $outputMock = $this->getMockForAbstractClass(OutputInterface::class);
-
- $this->environment->method('isCloud')
- ->willReturn(false);
-
$this->revert->expects($this->once())
->method('run');
diff --git a/src/Test/Unit/Composer/MagentoVersionTest.php b/src/Test/Unit/Composer/MagentoVersionTest.php
index 181fd7f..615b679 100644
--- a/src/Test/Unit/Composer/MagentoVersionTest.php
+++ b/src/Test/Unit/Composer/MagentoVersionTest.php
@@ -9,6 +9,7 @@
use Composer\Composer;
use Composer\Package\PackageInterface;
+use Composer\Package\RootPackageInterface;
use Composer\Repository\RepositoryManager;
use Composer\Repository\WritableRepositoryInterface;
use Magento\CloudPatches\Composer\MagentoVersion;
@@ -32,12 +33,18 @@ class MagentoVersionTest extends TestCase
*/
private $magentoVersion;
+ /**
+ * @var RootPackageInterface|MockObject
+ */
+ private $rootPackage;
+
/**
* @inheritDoc
*/
protected function setUp()
{
$this->repository = $this->getMockForAbstractClass(WritableRepositoryInterface::class);
+ $this->rootPackage = $this->getMockForAbstractClass(RootPackageInterface::class);
$repositoryManager = $this->createMock(RepositoryManager::class);
$repositoryManager->method('getLocalRepository')
->willReturn($this->repository);
@@ -46,6 +53,8 @@ protected function setUp()
$composer = $this->createMock(Composer::class);
$composer->method('getRepositoryManager')
->willReturn($repositoryManager);
+ $composer->method('getPackage')
+ ->willReturn($this->rootPackage);
$this->magentoVersion = new MagentoVersion($composer);
}
@@ -56,12 +65,18 @@ protected function setUp()
* @param bool $ce
* @param bool $ee
* @param bool $b2b
+ * @param string $rootPackage
* @param string $expectedResult
*
* @dataProvider getDataProvider
*/
- public function testGet(bool $ce, bool $ee, bool $b2b, string $expectedResult)
+ public function testGet(bool $ce, bool $ee, bool $b2b, string $rootPackage, string $expectedResult)
{
+ $this->rootPackage->method('getName')
+ ->willReturn($rootPackage);
+ $this->rootPackage->method('getVersion')
+ ->willReturn(self::VERSION);
+
$package = $this->getMockForAbstractClass(PackageInterface::class);
$package->method('getVersion')
->willReturn(self::VERSION);
@@ -81,10 +96,140 @@ public function testGet(bool $ce, bool $ee, bool $b2b, string $expectedResult)
public function getDataProvider(): array
{
return [
- ['CE' => false, 'EE' => false, 'B2B' => false, 'Magento 2 is not installed'],
- ['CE' => true, 'EE' => true, 'B2B' => false, 'Magento 2 Enterprise Edition, version ' . self::VERSION],
- ['CE' => true, 'EE' => false, 'B2B' => true, 'Magento 2 B2B Edition, version ' . self::VERSION],
- ['CE' => true, 'EE' => false, 'B2B' => false, 'Magento 2 Community Edition, version ' . self::VERSION],
+ [
+ 'CE' => false,
+ 'EE' => false,
+ 'B2B' => false,
+ 'gitPackage' => '',
+ 'Magento 2 is not installed'
+ ],
+ [
+ 'CE' => true,
+ 'EE' => true,
+ 'B2B' => false,
+ 'gitPackage' => '',
+ 'Magento 2 Enterprise Edition, version ' . self::VERSION
+ ],
+ [
+ 'CE' => true,
+ 'EE' => false,
+ 'B2B' => true,
+ 'gitPackage' => '',
+ 'Magento 2 B2B Edition, version ' . self::VERSION
+ ],
+ [
+ 'CE' => true,
+ 'EE' => false,
+ 'B2B' => false,
+ 'gitPackage' => '',
+ 'Magento 2 Community Edition, version ' . self::VERSION
+ ],
+ [
+ 'CE' => false,
+ 'EE' => false,
+ 'B2B' => false,
+ 'gitPackage' => 'magento/magento2ce',
+ 'Git-based: Magento 2 Community Edition, version ' . self::VERSION
+ ],
+ [
+ 'CE' => false,
+ 'EE' => false,
+ 'B2B' => false,
+ 'gitPackage' => 'magento/magento2ee',
+ 'Git-based: Magento 2 Enterprise Edition, version ' . self::VERSION
+ ],
+ ];
+ }
+
+ /**
+ * Tests Magento git-version identifying .
+ *
+ * @param string $rootPackageName
+ * @param bool $expectedResult
+ * @dataProvider isGitBasedDataProvider
+ */
+ public function testIsGitBased(string $rootPackageName, bool $expectedResult)
+ {
+ $this->rootPackage->method('getName')
+ ->willReturn($rootPackageName);
+
+ $this->assertEquals($expectedResult, $this->magentoVersion->isGitBased());
+ }
+
+ /**
+ * @return array
+ */
+ public function isGitBasedDataProvider(): array
+ {
+ return [
+ ['rootPackageName' => 'magento/magento2ce', 'expectedResult' => true],
+ ['rootPackageName' => 'magento/magento2ee', 'expectedResult' => true],
+ ['rootPackageName' => 'magento/magento2-ce-base', 'expectedResult' => false]
+ ];
+ }
+
+ /**
+ * Tests package matching using composer root package.
+ *
+ * @param string $rootPackageName
+ * @param string $rootPackageVersion
+ * @param string $testPackageName
+ * @param string $testPackageVersion
+ * @param bool $expectedResult
+ * @dataProvider matchPackageGitProvider
+ */
+ public function testMatchPackageGit(
+ string $rootPackageName,
+ string $rootPackageVersion,
+ string $testPackageName,
+ string $testPackageVersion,
+ bool $expectedResult
+ ) {
+ $this->rootPackage->method('getName')
+ ->willReturn($rootPackageName);
+ $this->rootPackage->method('getVersion')
+ ->willReturn($rootPackageVersion);
+
+ $this->assertEquals(
+ $expectedResult,
+ $this->magentoVersion->matchPackageGit($testPackageName, $testPackageVersion)
+ );
+ }
+
+ /**
+ * @return array
+ */
+ public function matchPackageGitProvider(): array
+ {
+ return [
+ [
+ 'magento/magento2ce',
+ '2.3.5',
+ 'magento/magento2-base',
+ '<=2.3.5 <2.3.6',
+ 'expectedResult' => true
+ ],
+ [
+ 'magento/magento2ce',
+ '2.3.5',
+ 'magento/magento2-base',
+ '<2.3.5',
+ 'expectedResult' => false
+ ],
+ [
+ 'magento/magento2ce',
+ '2.3.5',
+ 'magento/magento2-ee-base',
+ '<=2.3.5 <2.3.6',
+ 'expectedResult' => false
+ ],
+ [
+ 'magento/magento2ee',
+ '2.3.5',
+ 'magento/magento2-ee-base',
+ '<=2.3.5 <2.3.6',
+ 'expectedResult' => true
+ ],
];
}
}
diff --git a/src/Test/Unit/Environment/ConfigReaderTest.php b/src/Test/Unit/Environment/ConfigReaderTest.php
new file mode 100644
index 0000000..ddd0f5a
--- /dev/null
+++ b/src/Test/Unit/Environment/ConfigReaderTest.php
@@ -0,0 +1,94 @@
+fileList = $this->createMock(FileList::class);
+ $this->filesystem = $this->createPartialMock(Filesystem::class, ['exists']);
+
+ $this->configReader = new ConfigReader(
+ $this->fileList,
+ $this->filesystem
+ );
+ }
+
+ /**
+ * @throws FileSystemException
+ */
+ public function testRead()
+ {
+ $baseDir = __DIR__ . '/_file/';
+
+ $this->fileList->expects($this->once())
+ ->method('getEnvConfig')
+ ->willReturn($baseDir . '/.magento.env.yaml');
+ $this->filesystem->expects($this->once())
+ ->method('exists')
+ ->willReturn(true);
+
+ $this->configReader->read();
+ $this->assertEquals(
+ [
+ 'stage' => [
+ 'build' => [
+ 'QUALITY_PATCHES' => ['MC-1', 'MC-2']
+ ]
+ ]
+ ],
+ $this->configReader->read()
+ );
+ }
+
+ /**
+ * @throws FileSystemException
+ */
+ public function testReadNotExist()
+ {
+ $baseDir = __DIR__ . '/_file/';
+
+ $this->fileList->expects($this->once())
+ ->method('getEnvConfig')
+ ->willReturn($baseDir . '/.magento.env.yaml');
+ $this->filesystem->expects($this->once())
+ ->method('exists')
+ ->willReturn(false);
+
+ $this->assertEquals([], $this->configReader->read());
+ }
+}
diff --git a/src/Test/Unit/Environment/ConfigTest.php b/src/Test/Unit/Environment/ConfigTest.php
new file mode 100644
index 0000000..79d3329
--- /dev/null
+++ b/src/Test/Unit/Environment/ConfigTest.php
@@ -0,0 +1,86 @@
+configReader = $this->createMock(ConfigReader::class);
+
+ $this->config = new Config($this->configReader);
+ }
+
+ /**
+ * Tests Cloud environment.
+ */
+ public function testIsCloud()
+ {
+ $_ENV[Config::ENV_VAR_CLOUD] = '';
+ $this->assertFalse($this->config->isCloud());
+
+ $_ENV[Config::ENV_VAR_CLOUD] = '123';
+ $this->assertTrue($this->config->isCloud());
+ }
+
+ /**
+ * Tests retrieving QUALITY_PATCHES from env variable.
+ */
+ public function testGetQualityPatchesEnv()
+ {
+ $_ENV[Config::ENV_VAR_QUALITY_PATCHES] = ['MC-1', 'MC-2'];
+
+ $this->configReader->expects($this->never())
+ ->method('read');
+
+ $this->assertEquals(
+ ['MC-1', 'MC-2'],
+ $this->config->getQualityPatches()
+ );
+ }
+
+ /**
+ * Tests retrieving QUALITY_PATCHES from env config.
+ */
+ public function testGetQualityPatchesConfig()
+ {
+ unset($_ENV[Config::ENV_VAR_QUALITY_PATCHES]);
+ $this->assertArrayNotHasKey(Config::ENV_VAR_QUALITY_PATCHES, $_ENV);
+
+ $config['stage']['build'][Config::ENV_VAR_QUALITY_PATCHES] = ['MC-1', 'MC-2'];
+ $this->configReader->expects($this->once())
+ ->method('read')
+ ->willReturn($config);
+
+ $this->assertEquals(
+ ['MC-1', 'MC-2'],
+ $this->config->getQualityPatches()
+ );
+ }
+}
diff --git a/src/Test/Unit/Environment/_file/.magento.env.yaml b/src/Test/Unit/Environment/_file/.magento.env.yaml
new file mode 100644
index 0000000..de70300
--- /dev/null
+++ b/src/Test/Unit/Environment/_file/.magento.env.yaml
@@ -0,0 +1,5 @@
+stage:
+ build:
+ QUALITY_PATCHES:
+ - MC-1
+ - MC-2
diff --git a/src/Test/Unit/Filesystem/FileListTest.php b/src/Test/Unit/Filesystem/FileListTest.php
index 194ab01..f0f019a 100644
--- a/src/Test/Unit/Filesystem/FileListTest.php
+++ b/src/Test/Unit/Filesystem/FileListTest.php
@@ -59,12 +59,4 @@ public function testGetPatchLog()
$this->fileList->getPatchLog()
);
}
-
- public function testGetInitPatchLog()
- {
- $this->assertSame(
- 'magento_root/init/var/log/patch.log',
- $this->fileList->getInitPatchLog()
- );
- }
}
diff --git a/src/Test/Unit/Patch/ApplierTest.php b/src/Test/Unit/Patch/ApplierTest.php
index f2efb35..d62d0c0 100644
--- a/src/Test/Unit/Patch/ApplierTest.php
+++ b/src/Test/Unit/Patch/ApplierTest.php
@@ -7,8 +7,11 @@
namespace Magento\CloudPatches\Test\Unit\Patch;
+use Magento\CloudPatches\Composer\MagentoVersion;
+use Magento\CloudPatches\Filesystem\Filesystem;
use Magento\CloudPatches\Patch\Applier;
use Magento\CloudPatches\Patch\ApplierException;
+use Magento\CloudPatches\Patch\GitConverter;
use Magento\CloudPatches\Patch\Status\StatusPool;
use Magento\CloudPatches\Shell\ProcessFactory;
use PHPUnit\Framework\MockObject\MockObject;
@@ -31,14 +34,37 @@ class ApplierTest extends TestCase
*/
private $processFactory;
+ /**
+ * @var GitConverter|MockObject
+ */
+ private $gitConverter;
+
+ /**
+ * @var MagentoVersion|MockObject
+ */
+ private $magentoVersion;
+
+ /**
+ * @var Filesystem|MockObject
+ */
+ private $filesystem;
+
/**
* @inheritDoc
*/
protected function setUp()
{
$this->processFactory = $this->createMock(ProcessFactory::class);
-
- $this->applier = new Applier($this->processFactory);
+ $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->gitConverter,
+ $this->magentoVersion,
+ $this->filesystem
+ );
}
/**
@@ -51,12 +77,20 @@ public function testApply()
$path = 'path/to/patch';
$patchId = 'MC-11111';
$expectedMessage = 'Patch ' . $patchId . ' has been applied';
-
+ $this->filesystem->expects($this->once())
+ ->method('get')
+ ->willReturn('patchContent');
+ $this->magentoVersion->expects($this->once())
+ ->method('isGitBased')
+ ->willReturn(true);
+ $this->gitConverter->expects($this->once())
+ ->method('convert')
+ ->willReturn('gitContent');
$processMock = $this->createMock(Process::class);
$this->processFactory->expects($this->once())
->method('create')
- ->with(['git', 'apply', $path])
+ ->withConsecutive([['git', 'apply'], 'gitContent'])
->willReturn($processMock);
$processMock->expects($this->once())
->method('mustRun');
@@ -96,11 +130,20 @@ public function testApplyPatchAlreadyApplied()
$patchId = 'MC-11111';
$expectedMessage = 'Patch ' . $patchId . ' was already applied';
+ $this->filesystem->expects($this->once())
+ ->method('get')
+ ->willReturn('patchContent');
+ $this->magentoVersion->expects($this->once())
+ ->method('isGitBased')
+ ->willReturn(false);
+ $this->gitConverter->expects($this->never())
+ ->method('convert');
+
$this->processFactory->expects($this->exactly(2))
->method('create')
->willReturnMap([
- [['git', 'apply', $path]],
- [['git', 'apply', $path, '--check', '--reverse']]
+ [['git', 'apply'], 'patchContent'],
+ [['git', 'apply', '--check', '--reverse'], 'patchContent']
])->willReturnCallback([$this, 'shellApplyRevertCallback']);
$this->assertSame($expectedMessage, $this->applier->apply($path, $patchId));
@@ -148,11 +191,21 @@ public function testRevert()
$patchId = 'MC-11111';
$expectedMessage = 'Patch ' . $patchId . ' has been reverted';
+ $this->filesystem->expects($this->once())
+ ->method('get')
+ ->willReturn('patchContent');
+ $this->magentoVersion->expects($this->once())
+ ->method('isGitBased')
+ ->willReturn(true);
+ $this->gitConverter->expects($this->once())
+ ->method('convert')
+ ->willReturn('gitContent');
+
$processMock = $this->createMock(Process::class);
$this->processFactory->expects($this->once())
->method('create')
- ->with(['git', 'apply', '--reverse', $path])
+ ->withConsecutive([['git', 'apply', '--reverse'], 'gitContent'])
->willReturn($processMock);
$processMock->expects($this->once())
->method('mustRun');
@@ -190,13 +243,23 @@ public function testRevertPatchWasntApplied()
{
$path = 'path/to/patch';
$patchId = 'MC-11111';
+ $patchContent = 'patch content';
$expectedMessage = 'Patch ' . $patchId . ' wasn\'t applied';
+ $this->filesystem->expects($this->once())
+ ->method('get')
+ ->willReturn($patchContent);
+ $this->magentoVersion->expects($this->once())
+ ->method('isGitBased')
+ ->willReturn(false);
+ $this->gitConverter->expects($this->never())
+ ->method('convert');
+
$this->processFactory->expects($this->exactly(2))
->method('create')
->willReturnMap([
- [['git', 'apply', $path]],
- [['git', 'apply', $path, '--check']]
+ [['git', 'apply'], $patchContent],
+ [['git', 'apply', '--check'], $patchContent]
])->willReturnCallback([$this, 'shellApplyRevertCallback']);
$this->assertSame($expectedMessage, $this->applier->revert($path, $patchId));
diff --git a/src/Test/Unit/Patch/Collector/CloudCollectorTest.php b/src/Test/Unit/Patch/Collector/CloudCollectorTest.php
index 55e6569..b83df61 100644
--- a/src/Test/Unit/Patch/Collector/CloudCollectorTest.php
+++ b/src/Test/Unit/Patch/Collector/CloudCollectorTest.php
@@ -8,12 +8,12 @@
namespace Magento\CloudPatches\Test\Unit\Patch\Collector;
use Magento\CloudPatches\Composer\Package;
+use Magento\CloudPatches\Environment\Config;
use Magento\CloudPatches\Filesystem\DirectoryList;
use Magento\CloudPatches\Patch\Collector\CloudCollector;
use Magento\CloudPatches\Patch\Collector\CollectorException;
use Magento\CloudPatches\Patch\Data\Patch;
use Magento\CloudPatches\Patch\Data\PatchInterface;
-use Magento\CloudPatches\Patch\Environment;
use Magento\CloudPatches\Patch\PatchBuilder;
use Magento\CloudPatches\Patch\PatchIntegrityException;
use Magento\CloudPatches\Patch\SourceProvider;
@@ -49,9 +49,9 @@ class CloudCollectorTest extends TestCase
private $package;
/**
- * @var Environment|MockObject
+ * @var Config|MockObject
*/
- private $environment;
+ private $envConfig;
/**
* @var DirectoryList|MockObject
@@ -65,7 +65,7 @@ protected function setUp()
{
$this->sourceProvider = $this->createMock(SourceProvider::class);
$this->package = $this->createMock(Package::class);
- $this->environment = $this->createMock(Environment::class);
+ $this->envConfig = $this->createMock(Config::class);
$this->directoryList = $this->createMock(DirectoryList::class);
$this->patchBuilder = $this->createMock(PatchBuilder::class);
@@ -73,7 +73,7 @@ protected function setUp()
$this->sourceProvider,
$this->package,
$this->directoryList,
- $this->environment,
+ $this->envConfig,
$this->patchBuilder
);
}
@@ -93,7 +93,7 @@ public function testCollectSuccessful(bool $isCloud, string $expectedType)
->willReturn($validConfig);
$this->directoryList->method('getPatches')
->willReturn(self::CLOUD_PATCH_DIR);
- $this->environment->method('isCloud')
+ $this->envConfig->method('isCloud')
->willReturn($isCloud);
$this->package->method('matchConstraint')
diff --git a/src/Test/Unit/Patch/Collector/LocalCollectorTest.php b/src/Test/Unit/Patch/Collector/LocalCollectorTest.php
index cd297c5..972436d 100644
--- a/src/Test/Unit/Patch/Collector/LocalCollectorTest.php
+++ b/src/Test/Unit/Patch/Collector/LocalCollectorTest.php
@@ -56,6 +56,8 @@ public function testCollect()
{
$file1 = __DIR__ . SourceProvider::HOT_FIXES_DIR . '/patch1.patch';
$file2 = __DIR__ . SourceProvider::HOT_FIXES_DIR . '/patch2.patch';
+ $shortPath1 = '../' . SourceProvider::HOT_FIXES_DIR . '/patch1.patch';
+ $shortPath2 = '../' . SourceProvider::HOT_FIXES_DIR . '/patch2.patch';
$this->sourceProvider->expects($this->once())
->method('getLocalPatches')
@@ -63,12 +65,12 @@ public function testCollect()
$this->patchBuilder->expects($this->exactly(2))
->method('setId')
- ->withConsecutive([md5($file1)], [md5($file2)]);
+ ->withConsecutive([$shortPath1], [$shortPath2]);
$this->patchBuilder->expects($this->exactly(2))
->method('setTitle')
->withConsecutive(
- ['../' . SourceProvider::HOT_FIXES_DIR . '/patch1.patch'],
- ['../' . SourceProvider::HOT_FIXES_DIR . '/patch2.patch']
+ [$shortPath1],
+ [$shortPath2]
);
$this->patchBuilder->expects($this->exactly(2))
->method('setFilename')
diff --git a/src/Test/Unit/Patch/Collector/QualityCollectorTest.php b/src/Test/Unit/Patch/Collector/QualityCollectorTest.php
index 5d4bdf1..b9e3853 100644
--- a/src/Test/Unit/Patch/Collector/QualityCollectorTest.php
+++ b/src/Test/Unit/Patch/Collector/QualityCollectorTest.php
@@ -8,6 +8,7 @@
namespace Magento\CloudPatches\Test\Unit\Patch\Collector;
use Magento\CloudPatches\Composer\Package;
+use Magento\CloudPatches\Composer\QualityPackage;
use Magento\CloudPatches\Patch\Collector\CollectorException;
use Magento\CloudPatches\Patch\Collector\QualityCollector;
use Magento\CloudPatches\Patch\Data\Patch;
@@ -16,7 +17,6 @@
use Magento\CloudPatches\Patch\PatchIntegrityException;
use Magento\CloudPatches\Patch\SourceProvider;
use Magento\CloudPatches\Patch\SourceProviderException;
-use Magento\QualityPatches\Info as QualityPatchesInfo;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
@@ -48,9 +48,9 @@ class QualityCollectorTest extends TestCase
private $package;
/**
- * @var QualityPatchesInfo|MockObject
+ * @var QualityPackage|MockObject
*/
- private $qualityPatchesInfo;
+ private $qualityPackage;
/**
* @inheritDoc
@@ -59,13 +59,13 @@ protected function setUp()
{
$this->sourceProvider = $this->createMock(SourceProvider::class);
$this->package = $this->createMock(Package::class);
- $this->qualityPatchesInfo = $this->createMock(QualityPatchesInfo::class);
+ $this->qualityPackage = $this->createMock(QualityPackage::class);
$this->patchBuilder = $this->createMock(PatchBuilder::class);
$this->collector = new QualityCollector(
$this->sourceProvider,
$this->package,
- $this->qualityPatchesInfo,
+ $this->qualityPackage,
$this->patchBuilder
);
}
@@ -79,7 +79,7 @@ public function testCollectSuccessful()
$this->sourceProvider->expects($this->once())
->method('getQualityPatches')
->willReturn($validConfig);
- $this->qualityPatchesInfo->method('getPatchesDirectory')
+ $this->qualityPackage->method('getPatchesDirectory')
->willReturn(self::QUALITY_PATCH_DIR);
$this->package->method('matchConstraint')
diff --git a/src/Test/Unit/Patch/Conflict/AnalyzerTest.php b/src/Test/Unit/Patch/Conflict/AnalyzerTest.php
new file mode 100644
index 0000000..f518d8f
--- /dev/null
+++ b/src/Test/Unit/Patch/Conflict/AnalyzerTest.php
@@ -0,0 +1,182 @@
+optionalPool = $this->createMock(OptionalPool::class);
+ $this->config = $this->createMock(Config::class);
+ $this->rollbackProcessor = $this->createMock(RollbackProcessor::class);
+ $this->applyChecker = $this->createMock(ApplyChecker::class);
+
+ $this->conflictAnalyzer = new ConflictAnalyzer(
+ $this->optionalPool,
+ $this->config,
+ $this->rollbackProcessor,
+ $this->applyChecker
+ );
+ }
+
+ /**
+ * Tests patch conflict analyzing.
+ *
+ * @param array $checkApplyMap
+ * @param string $expectedMessage
+ * @dataProvider analyzeDataProvider
+ */
+ public function testAnalyze(array $checkApplyMap, string $expectedMessage)
+ {
+ $failedPatch = $this->createPatch('MC-1', 'path1', PatchInterface::TYPE_OPTIONAL);
+ $requiredPool = ['REQUIRED-1', 'REQUIRED-2'];
+ $optionalPool = ['OPTIONAL-1', 'OPTIONAL-2'];
+
+ $this->config->expects($this->once())
+ ->method('isCloud')
+ ->willReturn(true);
+ $this->optionalPool->expects($this->once())
+ ->method('getList')
+ ->willReturn([]);
+ $this->optionalPool->method('getDependencies')
+ ->willReturn([]);
+ $this->rollbackProcessor->expects($this->once())
+ ->method('process');
+
+ $this->optionalPool->expects($this->atLeastOnce())
+ ->method('getIdsByType')
+ ->willReturnMap([
+ [PatchInterface::TYPE_REQUIRED, $requiredPool],
+ [PatchInterface::TYPE_OPTIONAL, $optionalPool]
+ ]);
+
+ $this->applyChecker->method('check')
+ ->willReturnMap($checkApplyMap);
+
+ $this->assertEquals(
+ $expectedMessage,
+ $this->conflictAnalyzer->analyze($failedPatch, [])
+ );
+ }
+
+ /**
+ * @return array
+ */
+ public function analyzeDataProvider(): array
+ {
+ return [
+ [
+ 'checkApplyMap' => [
+ [['REQUIRED-1', 'REQUIRED-2', 'MC-1'], true],
+ [['OPTIONAL-1', 'MC-1'], false],
+ [['OPTIONAL-2', 'MC-1'], false],
+ ],
+ 'expectedMessage' => 'Patch MC-1 is not compatible with optional: OPTIONAL-1 OPTIONAL-2'
+ ],
+ [
+ 'checkApplyMap' => [
+ [['REQUIRED-1', 'REQUIRED-2', 'MC-1'], true],
+ [['OPTIONAL-1', 'MC-1'], true],
+ [['OPTIONAL-2', 'MC-1'], false],
+ ],
+ 'expectedMessage' => 'Patch MC-1 is not compatible with optional: OPTIONAL-2'
+ ],
+ [
+ 'checkApplyMap' => [
+ [['REQUIRED-1', 'REQUIRED-2', 'MC-1'], false],
+ [['REQUIRED-2', 'MC-1'], false],
+ [['REQUIRED-1', 'MC-1'], true],
+ ],
+ 'expectedMessage' => 'Patch MC-1 is not compatible with required: REQUIRED-2'
+ ],
+ [
+ 'checkApplyMap' => [
+ [['REQUIRED-1', 'REQUIRED-2', 'MC-1'], false],
+ [['REQUIRED-2', 'MC-1'], false],
+ [['REQUIRED-1', 'MC-1'], false],
+ [['MC-1'], false],
+ ],
+ 'expectedMessage' => 'Patch MC-1 can\'t be applied to clean Magento instance'
+ ],
+ ];
+ }
+
+ /**
+ * Tests with non-Cloud environment.
+ */
+ public function testAnalyzeWithNonCloudEnv()
+ {
+ $patch = $this->createPatch('MC-1', 'path1');
+
+ $this->config->expects($this->once())
+ ->method('isCloud')
+ ->willReturn(false);
+ $this->optionalPool->expects($this->never())
+ ->method('getIdsByType');
+
+ $this->assertEmpty($this->conflictAnalyzer->analyze($patch));
+ }
+
+ /**
+ * Creates patch mock.
+ *
+ * @param string $id
+ * @param string $path
+ * @param string $type
+ * @return PatchInterface|MockObject
+ */
+ private function createPatch(string $id, string $path, string $type = '')
+ {
+ $patch = $this->getMockForAbstractClass(PatchInterface::class);
+ $patch->method('getId')->willReturn($id);
+ $patch->method('getPath')->willReturn($path);
+ $patch->method('getType')->willReturn($type);
+
+ return $patch;
+ }
+}
diff --git a/src/Test/Unit/Patch/Conflict/ApplyCheckerTest.php b/src/Test/Unit/Patch/Conflict/ApplyCheckerTest.php
new file mode 100644
index 0000000..f59d651
--- /dev/null
+++ b/src/Test/Unit/Patch/Conflict/ApplyCheckerTest.php
@@ -0,0 +1,105 @@
+optionalPool = $this->createMock(OptionalPool::class);
+ $this->filesystem = $this->createMock(Filesystem::class);
+ $this->applier = $this->createMock(Applier::class);
+
+ $this->applyChecker = new ApplyChecker(
+ $this->applier,
+ $this->optionalPool,
+ $this->filesystem
+ );
+ }
+
+ /**
+ * Tests patch apply checker.
+ */
+ public function testCheck()
+ {
+ $patchIds = ['MC-1', 'MC-2', 'MC-3'];
+ $patch1 = $this->createPatch('MC-1', 'path1');
+ $patch2 = $this->createPatch('MC-2', 'path2');
+ $patch3 = $this->createPatch('MC-3', 'path3');
+
+ $this->optionalPool->expects($this->once())
+ ->method('getList')
+ ->withConsecutive([$patchIds])
+ ->willReturn([$patch1, $patch2, $patch3]);
+ $this->filesystem->expects($this->exactly(3))
+ ->method('get')
+ ->willReturnMap([
+ [$patch1->getPath(), 'content1'],
+ [$patch2->getPath(), 'content2'],
+ [$patch3->getPath(), 'content3'],
+ ]);
+ $this->applier->expects($this->once())
+ ->method('checkApply')
+ ->with('content1content2content3')
+ ->willReturn(true);
+
+ $this->assertTrue(
+ $this->applyChecker->check($patchIds)
+ );
+ }
+
+ /**
+ * Creates patch mock.
+ *
+ * @param string $id
+ * @param string $path
+ * @return PatchInterface|MockObject
+ */
+ private function createPatch(string $id, string $path)
+ {
+ $patch = $this->getMockForAbstractClass(PatchInterface::class);
+ $patch->method('getId')->willReturn($id);
+ $patch->method('getPath')->willReturn($path);
+
+ return $patch;
+ }
+}
diff --git a/src/Test/Unit/Patch/Conflict/ProcessorTest.php b/src/Test/Unit/Patch/Conflict/ProcessorTest.php
new file mode 100644
index 0000000..00569d6
--- /dev/null
+++ b/src/Test/Unit/Patch/Conflict/ProcessorTest.php
@@ -0,0 +1,138 @@
+renderer = $this->createMock(Renderer::class);
+ $this->logger = $this->getMockForAbstractClass(LoggerInterface::class);
+ $this->conflictAnalyzer = $this->createMock(ConflictAnalyzer::class);
+ $this->rollbackProcessor = $this->createMock(RollbackProcessor::class);
+
+ $this->conflictProcessor = new ConflictProcessor(
+ $this->renderer,
+ $this->logger,
+ $this->conflictAnalyzer,
+ $this->rollbackProcessor
+ );
+ }
+
+ /**
+ * Tests patch conflict processing.
+ */
+ public function testProcess()
+ {
+ $appliedPatch1 = $this->createPatch('MC-1', 'path1');
+ $appliedPatch2 = $this->createPatch('MC-2', 'path2');
+ $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 */
+ $outputMock = $this->getMockForAbstractClass(OutputInterface::class);
+
+ $this->rollbackProcessor->expects($this->once())
+ ->method('process')
+ ->withConsecutive([[$appliedPatch1, $appliedPatch2]])
+ ->willReturn($rollbackMessages);
+ $this->conflictAnalyzer->expects($this->once())
+ ->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')],
+ [$rollbackMessages]
+ );
+
+ $expectedErrorMessage = sprintf(
+ 'Applying patch %s (%s) failed.%s%s',
+ $failedPatch->getId(),
+ $failedPatch->getPath(),
+ $formattedOutput,
+ PHP_EOL . $conflictDetails
+ );
+
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage($expectedErrorMessage);
+
+ $this->conflictProcessor->process(
+ $outputMock,
+ $failedPatch,
+ [$appliedPatch1, $appliedPatch2],
+ $exceptionMessage
+ );
+ }
+
+ /**
+ * Creates patch mock.
+ *
+ * @param string $id
+ * @param string $path
+ * @return PatchInterface|MockObject
+ */
+ private function createPatch(string $id, string $path)
+ {
+ $patch = $this->getMockForAbstractClass(PatchInterface::class);
+ $patch->method('getId')->willReturn($id);
+ $patch->method('getPath')->willReturn($path);
+
+ return $patch;
+ }
+}
diff --git a/src/Test/Unit/Patch/EnvironmentTest.php b/src/Test/Unit/Patch/EnvironmentTest.php
deleted file mode 100644
index 21741af..0000000
--- a/src/Test/Unit/Patch/EnvironmentTest.php
+++ /dev/null
@@ -1,31 +0,0 @@
-assertFalse($environment->isCloud());
-
- $_ENV[Environment::ENV_VAR_CLOUD] = '123';
- $this->assertTrue($environment->isCloud());
- }
-}
diff --git a/src/Test/Unit/Patch/GitConverterTest.php b/src/Test/Unit/Patch/GitConverterTest.php
new file mode 100644
index 0000000..a250edf
--- /dev/null
+++ b/src/Test/Unit/Patch/GitConverterTest.php
@@ -0,0 +1,87 @@
+gitConverter = new GitConverter();
+ }
+
+ /**
+ * Tests patch converting from composer-based to git-based.
+ *
+ * @param string $composerContent
+ * @param string $expectedContent
+ * @dataProvider convertDataProvider
+ */
+ public function testConvert(string $composerContent, string $expectedContent)
+ {
+ $this->assertEquals(
+ $expectedContent,
+ $this->gitConverter->convert($composerContent)
+ );
+ }
+
+ /**
+ * phpcs:disable
+ * @return array
+ */
+ public function convertDataProvider()
+ {
+ return [
+ [
+ 'composerContent' => 'diff -Naur a/vendor/magento/framework/View/Asset/File/FallbackContext.php b/vendor/magento/framework/View/Asset/File/FallbackContext.php
+--- a/vendor/magento/framework/View/Asset/File/FallbackContext.php
++++ b/vendor/magento/framework/View/Asset/File/FallbackContext.php',
+ 'expectedContent' => 'diff -Naur a/lib/internal/Magento/Framework/View/Asset/File/FallbackContext.php b/lib/internal/Magento/Framework/View/Asset/File/FallbackContext.php
+--- a/lib/internal/Magento/Framework/View/Asset/File/FallbackContext.php
++++ b/lib/internal/Magento/Framework/View/Asset/File/FallbackContext.php'
+ ],
+
+ [
+ 'composerContent' => 'diff -Naur a/app/etc/di.xml b/app/etc/di.xml
+--- a/app/etc/di.xml
++++ b/app/etc/di.xml',
+ 'expectedContent' => 'diff -Naur a/app/etc/di.xml b/app/etc/di.xml
+--- a/app/etc/di.xml
++++ b/app/etc/di.xml'
+ ],
+
+ [
+ 'composerContent' => 'diff --git a/vendor/magento/module-deploy/Process/Queue.php b/vendor/magento/module-deploy/Process/Queue.php
+--- a/vendor/magento/module-deploy/Process/Queue.php
++++ b/vendor/magento/module-deploy/Process/Queue.php',
+ 'expectedContent' => 'diff --git a/app/code/Magento/Deploy/Process/Queue.php b/app/code/Magento/Deploy/Process/Queue.php
+--- a/app/code/Magento/Deploy/Process/Queue.php
++++ b/app/code/Magento/Deploy/Process/Queue.php'
+ ],
+
+ [
+ 'composerContent' => 'rename from vendor/magento/module-some-module',
+ 'expectedContent' => 'rename from app/code/Magento/SomeModule'
+ ]
+ ];
+ }
+ /** phpcs:enable */
+}
diff --git a/src/Test/Unit/Patch/Pool/OptionalPoolTest.php b/src/Test/Unit/Patch/Pool/OptionalPoolTest.php
index 53c711e..58c68cc 100644
--- a/src/Test/Unit/Patch/Pool/OptionalPoolTest.php
+++ b/src/Test/Unit/Patch/Pool/OptionalPoolTest.php
@@ -140,6 +140,24 @@ public function testGetDependentOn()
$this->assertEquals(['MC-2', 'MC-3'], $pool->getDependentOn('MC-1'));
}
+ /**
+ * Tests retrieving ids of patch dependencies.
+ */
+ public function testGetDependencies()
+ {
+ $patch1 = $this->createPatch('MC-1');
+ $patch2 = $this->createPatch('MC-2', ['MC-1']);
+ $patch3 = $this->createPatch('MC-3', ['MC-2']);
+ $patch4 = $this->createPatch('MC-4', ['MC-3']);
+
+ $pool = $this->createPool([$patch1, $patch2, $patch3, $patch4]);
+
+ $this->assertEquals(
+ ['MC-1', 'MC-2', 'MC-3'],
+ array_values($pool->getDependencies('MC-4'))
+ );
+ }
+
/**
* Tests retrieving additional required patches which are not included in patch filter.
*/
@@ -178,6 +196,34 @@ public function testGetReplacedBy()
);
}
+ /**
+ * Tests retrieving not deprecated patch ids by type.
+ */
+ public function testGetIdsByType()
+ {
+ $patch1 = $this->createPatch('OPTIONAL-1');
+ $patch1->method('getType')->willReturn(PatchInterface::TYPE_OPTIONAL);
+ $patch2 = $this->createPatch('OPTIONAL-2');
+ $patch2->method('getType')->willReturn(PatchInterface::TYPE_OPTIONAL);
+ $patch2->method('isDeprecated')->willReturn(true);
+ $patch3 = $this->createPatch('REQUIRED-3');
+ $patch3->method('getType')->willReturn(PatchInterface::TYPE_REQUIRED);
+ $patch4 = $this->createPatch('REQUIRED-4');
+ $patch4->method('getType')->willReturn(PatchInterface::TYPE_REQUIRED);
+
+ $pool = $this->createPool([$patch1, $patch2, $patch3, $patch4]);
+
+ $this->assertEquals(
+ ['OPTIONAL-1'],
+ array_values($pool->getIdsByType(PatchInterface::TYPE_OPTIONAL))
+ );
+
+ $this->assertEquals(
+ ['REQUIRED-3', 'REQUIRED-4'],
+ array_values($pool->getIdsByType(PatchInterface::TYPE_REQUIRED))
+ );
+ }
+
/**
* Filter is empty, Cloud + Quality patches expected to return.
*
diff --git a/src/Test/Unit/Patch/RollbackProcessorTest.php b/src/Test/Unit/Patch/RollbackProcessorTest.php
new file mode 100644
index 0000000..60a8cfd
--- /dev/null
+++ b/src/Test/Unit/Patch/RollbackProcessorTest.php
@@ -0,0 +1,103 @@
+applier = $this->createMock(Applier::class);
+ $this->logger = $this->getMockForAbstractClass(LoggerInterface::class);
+
+ $this->rollbackProcessor = new RollbackProcessor(
+ $this->applier,
+ $this->logger
+ );
+ }
+
+ /**
+ * Tests patch conflict processing.
+ */
+ public function testProcess()
+ {
+ $patch1 = $this->createPatch('MC-1', 'path1');
+ $patch2 = $this->createPatch('MC-2', 'path2');
+ $expectedMessages = [
+ 'Start of rollback',
+ 'Patch MC-2 has been reverted',
+ 'Patch MC-1 has been reverted',
+ 'End of rollback'
+ ];
+
+ $this->applier->method('revert')
+ ->willReturnMap([
+ [$patch1->getPath(), $patch1->getId(), 'Patch ' . $patch1->getId() .' has been reverted'],
+ [$patch2->getPath(), $patch2->getId(), 'Patch ' . $patch2->getId() .' has been reverted'],
+ ]);
+
+ $this->assertEquals(
+ $expectedMessages,
+ $this->rollbackProcessor->process([$patch1, $patch2])
+ );
+ }
+
+ /**
+ * Tests with empty passing argument.
+ */
+ public function testProcessWithEmptyArray()
+ {
+ $this->applier->expects($this->never())
+ ->method('revert');
+
+ $this->assertEmpty($this->rollbackProcessor->process([]));
+ }
+
+ /**
+ * Creates patch mock.
+ *
+ * @param string $id
+ * @param string $path
+ * @return PatchInterface|MockObject
+ */
+ private function createPatch(string $id, string $path)
+ {
+ $patch = $this->getMockForAbstractClass(PatchInterface::class);
+ $patch->method('getId')->willReturn($id);
+ $patch->method('getPath')->willReturn($path);
+
+ return $patch;
+ }
+}
diff --git a/src/Test/Unit/Patch/SourceProviderTest.php b/src/Test/Unit/Patch/SourceProviderTest.php
index f4ad453..3ad8ca5 100644
--- a/src/Test/Unit/Patch/SourceProviderTest.php
+++ b/src/Test/Unit/Patch/SourceProviderTest.php
@@ -7,13 +7,13 @@
namespace Magento\CloudPatches\Test\Unit\Patch;
+use Magento\CloudPatches\Composer\QualityPackage;
use Magento\CloudPatches\Filesystem\DirectoryList;
use Magento\CloudPatches\Filesystem\FileList;
use Magento\CloudPatches\Filesystem\Filesystem;
use Magento\CloudPatches\Filesystem\FileSystemException;
use Magento\CloudPatches\Patch\SourceProvider;
use Magento\CloudPatches\Patch\SourceProviderException;
-use Magento\QualityPatches\Info as QualityPatchesInfo;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
@@ -38,9 +38,9 @@ class SourceProviderTest extends TestCase
private $directoryList;
/**
- * @var QualityPatchesInfo|MockObject
+ * @var QualityPackage|MockObject
*/
- private $qualityPatchesInfo;
+ private $qualityPackage;
/**
* @var FileList|MockObject
@@ -55,13 +55,13 @@ protected function setUp()
$this->filesystem = $this->createMock(Filesystem::class);
$this->filelist = $this->createMock(FileList::class);
$this->directoryList = $this->createMock(DirectoryList::class);
- $this->qualityPatchesInfo = $this->createMock(QualityPatchesInfo::class);
+ $this->qualityPackage = $this->createMock(QualityPackage::class);
$this->sourceProvider = new SourceProvider(
$this->filesystem,
$this->filelist,
$this->directoryList,
- $this->qualityPatchesInfo
+ $this->qualityPackage
);
}
@@ -95,7 +95,7 @@ public function testGetQualityPatches()
$configSource = require __DIR__ . '/Collector/Fixture/quality_config_valid.php';
$jsonConfig = json_encode($configSource);
- $this->qualityPatchesInfo->expects($this->once())
+ $this->qualityPackage->expects($this->once())
->method('getPatchesConfig')
->willReturn($configPath);
@@ -107,6 +107,23 @@ public function testGetQualityPatches()
$this->assertEquals($configSource, $this->sourceProvider->getQualityPatches());
}
+ /**
+ * Tests retrieving Quality patch configuration when config path is null.
+ *
+ * Case when magento/quality-patches package is not installed.
+ */
+ public function testGetQualityPatchesWithNullConfigPath()
+ {
+ $this->qualityPackage->expects($this->once())
+ ->method('getPatchesConfig')
+ ->willReturn(null);
+
+ $this->filesystem->expects($this->never())
+ ->method('get');
+
+ $this->assertEquals([], $this->sourceProvider->getQualityPatches());
+ }
+
/**
* Tests retrieving Local patch configuration.
*/
@@ -130,7 +147,7 @@ public function testGetQualityPatchesFilesystemException()
{
$configPath = '/quality/patches.json';
- $this->qualityPatchesInfo->expects($this->once())
+ $this->qualityPackage->expects($this->once())
->method('getPatchesConfig')
->willReturn($configPath);
@@ -149,7 +166,7 @@ public function testGetQualityPatchesJsonException()
{
$configPath = '/quality/patches.json';
- $this->qualityPatchesInfo->expects($this->once())
+ $this->qualityPackage->expects($this->once())
->method('getPatchesConfig')
->willReturn($configPath);