diff --git a/Api/Adapter/ReactPHP/ChildProcess/ProcessFactoryInterface.php b/Api/Adapter/ReactPHP/ChildProcess/ProcessFactoryInterface.php new file mode 100644 index 0000000..c953b0b --- /dev/null +++ b/Api/Adapter/ReactPHP/ChildProcess/ProcessFactoryInterface.php @@ -0,0 +1,27 @@ + + * @copyright Copyright (C) 2018 Lizard Media (http://lizardmedia.pl) + */ + +namespace LizardMedia\AdminIndexer\Api\Adapter\ReactPHP\ChildProcess; + +use React\ChildProcess\Process; + +/** + * Interface ProcessFactoryInterface + * @package LizardMedia\AdminIndexer\Api\Adapter\ReactPHP + */ +interface ProcessFactoryInterface +{ + /** + * @param string $command + * @return Process + */ + public function create(string $command): Process; +} diff --git a/Api/Adapter/ReactPHP/EventLoop/LoopFactoryInterface.php b/Api/Adapter/ReactPHP/EventLoop/LoopFactoryInterface.php new file mode 100644 index 0000000..94cbc04 --- /dev/null +++ b/Api/Adapter/ReactPHP/EventLoop/LoopFactoryInterface.php @@ -0,0 +1,27 @@ + + * @copyright Copyright (C) 2018 Lizard Media (http://lizardmedia.pl) + */ + +namespace LizardMedia\AdminIndexer\Api\Adapter\ReactPHP\EventLoop; + +use React\EventLoop\Factory; +use React\EventLoop\LoopInterface; + +/** + * Interface LoopFactoryInterface + * @package LizardMedia\AdminIndexer\Api\Adapter\ReactPHP\EventLoop + */ +interface LoopFactoryInterface +{ + /** + * @return LoopInterface + */ + public function create(): LoopInterface; +} \ No newline at end of file diff --git a/Api/IndexerProcessorInterface.php b/Api/IndexerProcessorInterface.php index 8a8e8c6..ff6b6c9 100644 --- a/Api/IndexerProcessorInterface.php +++ b/Api/IndexerProcessorInterface.php @@ -12,7 +12,7 @@ namespace LizardMedia\AdminIndexer\Api; -use LizardMedia\AdminIndexer\Exception\ReindexFailureException; +use LizardMedia\AdminIndexer\Exception\ReindexFailureAggregateException; /** * Interface IndexerProcessorInterface @@ -23,7 +23,7 @@ interface IndexerProcessorInterface /** * @param string[] ...$indexerIds * @return void - * @throws ReindexFailureException + * @throws ReindexFailureAggregateException */ public function process(string ...$indexerIds): void; } diff --git a/Api/ReindexRunner/AsyncReindexRunnerInterface.php b/Api/ReindexRunner/AsyncReindexRunnerInterface.php deleted file mode 100644 index 333701f..0000000 --- a/Api/ReindexRunner/AsyncReindexRunnerInterface.php +++ /dev/null @@ -1,23 +0,0 @@ - - * @author Paweł Papke - * @copyright Copyright (C) 2018 Lizard Media (http://lizardmedia.pl) - */ - -namespace LizardMedia\AdminIndexer\Api\ReindexRunner; - -use LizardMedia\AdminIndexer\Api\ReindexRunnerInterface; - -/** - * Interface AsyncReindexRunnerInterface - * @package LizardMedia\AdminIndexer\Api\ReindexRunner - */ -interface AsyncReindexRunnerInterface extends ReindexRunnerInterface -{ -} diff --git a/Api/ReindexRunner/SyncReindexRunnerInterface.php b/Api/ReindexRunner/SyncReindexRunnerInterface.php deleted file mode 100644 index 4a09c08..0000000 --- a/Api/ReindexRunner/SyncReindexRunnerInterface.php +++ /dev/null @@ -1,23 +0,0 @@ - - * @author Paweł Papke - * @copyright Copyright (C) 2018 Lizard Media (http://lizardmedia.pl) - */ - -namespace LizardMedia\AdminIndexer\Api\ReindexRunner; - -use LizardMedia\AdminIndexer\Api\ReindexRunnerInterface; - -/** - * Interface SyncReindexRunnerInterface - * @package LizardMedia\AdminIndexer\Api\ReindexRunner - */ -interface SyncReindexRunnerInterface extends ReindexRunnerInterface -{ -} diff --git a/Api/ReindexRunnerInterface.php b/Api/ReindexRunnerInterface.php index 72c18d5..a281315 100644 --- a/Api/ReindexRunnerInterface.php +++ b/Api/ReindexRunnerInterface.php @@ -12,6 +12,8 @@ namespace LizardMedia\AdminIndexer\Api; +use LizardMedia\AdminIndexer\Exception\ReindexFailureAggregateException; + /** * Interface ReindexRunnerInterface * @package LizardMedia\AdminIndexer\Api @@ -19,9 +21,9 @@ interface ReindexRunnerInterface { /** - * @param string $indexerId + * @param string[] ...$indexerIds * @return void - * @throws \Exception + * @throws ReindexFailureAggregateException */ - public function run(string $indexerId): void; + public function run(string ...$indexerIds): void; } diff --git a/Controller/Adminhtml/Indexer/MassReindex.php b/Controller/Adminhtml/Indexer/MassReindex.php index 825a054..7814299 100644 --- a/Controller/Adminhtml/Indexer/MassReindex.php +++ b/Controller/Adminhtml/Indexer/MassReindex.php @@ -14,7 +14,7 @@ use LizardMedia\AdminIndexer\Api\IndexerProcessorInterface; use LizardMedia\AdminIndexer\Api\ReindexRunner\MessageBagInterface; -use LizardMedia\AdminIndexer\Exception\ReindexFailureException; +use LizardMedia\AdminIndexer\Exception\ReindexFailureAggregateException; use Magento\Backend\App\Action; use Magento\Backend\App\Action\Context; use Magento\Framework\Controller\Result\Redirect; @@ -60,7 +60,6 @@ public function __construct( IndexerProcessorInterface $indexerProcessor ) { parent::__construct($context); - $this->messageBag = $messageBag; $this->redirectFactory = $redirectFactory; $this->indexerProcessor = $indexerProcessor; @@ -69,7 +68,7 @@ public function __construct( /** * @return Redirect */ - public function execute() : Redirect + public function execute(): Redirect { $indexerIds = $this->getRequest()->getParam('indexer_ids'); @@ -77,13 +76,14 @@ public function execute() : Redirect $this->messageManager->addErrorMessage(__('Please select at least one index.')); return $this->getRedirect(); } + $indexerIds = $this->castValuesToString($indexerIds); try { $this->indexerProcessor->process(...$indexerIds); - $this->displayMessages(); - } catch (ReindexFailureException $exception) { - $this->messageManager->addErrorMessage(__('Reindex failed on indexer %1.', $exception->getIndexerName())); + $this->displayMessagesAboutRunningIndexers(); + } catch (ReindexFailureAggregateException $exception) { + $this->displayErrors($exception); } return $this->getRedirect(); @@ -121,17 +121,31 @@ private function validateParam($indexerIds): bool private function castValuesToString(array $indexerIds): array { return array_filter($indexerIds, function ($indexerId) { - return (string)$indexerId; + return (string) $indexerId; }); } /** * @return void */ - private function displayMessages(): void + private function displayMessagesAboutRunningIndexers(): void { foreach ($this->messageBag->getMessages() as $message) { $this->messageManager->addNoticeMessage($message); } } + + /** + * @param ReindexFailureAggregateException $exception + * @return void + */ + private function displayErrors(ReindexFailureAggregateException $exception): void + { + $this->messageManager->addErrorMessage($exception->getMessage()); + $errors = $exception->getErrors(); + + foreach ($errors as $error) { + $this->messageManager->addErrorMessage($error->getMessage()); + } + } } diff --git a/Exception/ReindexFailureAggregateException.php b/Exception/ReindexFailureAggregateException.php new file mode 100644 index 0000000..5490c2d --- /dev/null +++ b/Exception/ReindexFailureAggregateException.php @@ -0,0 +1,22 @@ + + * @copyright Copyright (C) 2018 Lizard Media (http://lizardmedia.pl) + */ + +namespace LizardMedia\AdminIndexer\Exception; + +use Magento\Framework\Exception\AbstractAggregateException; + +/** + * Class ReindexFailureAggregateException + * @package LizardMedia\AdminIndexer\Exception + */ +class ReindexFailureAggregateException extends AbstractAggregateException +{ +} \ No newline at end of file diff --git a/Exception/ReindexFailureException.php b/Exception/ReindexFailureException.php deleted file mode 100644 index f8d8164..0000000 --- a/Exception/ReindexFailureException.php +++ /dev/null @@ -1,49 +0,0 @@ - - * @author Paweł Papke - * @copyright Copyright (C) 2018 Lizard Media (http://lizardmedia.pl) - */ - -namespace LizardMedia\AdminIndexer\Exception; - -use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\Phrase; - -/** - * Class ReindexFailureException - * @package LizardMedia\AdminIndexer\Exception - */ -class ReindexFailureException extends LocalizedException -{ - /** - * @var string - */ - private $indexerName; - - /** - * ReindexFailureException constructor. - * @param Phrase $phrase - * @param string $indexerName - * @param \Exception|null $cause - * @param int $code - */ - public function __construct(Phrase $phrase, string $indexerName, \Exception $cause = null, int $code = 0) - { - parent::__construct($phrase, $cause, $code); - $this->indexerName = $indexerName; - } - - /** - * @return string - */ - public function getIndexerName(): string - { - return $this->indexerName; - } -} diff --git a/Model/Adapter/ReactPHP/ChildProcess/ProcessFactory.php b/Model/Adapter/ReactPHP/ChildProcess/ProcessFactory.php new file mode 100644 index 0000000..d078c6f --- /dev/null +++ b/Model/Adapter/ReactPHP/ChildProcess/ProcessFactory.php @@ -0,0 +1,31 @@ + + * @copyright Copyright (C) 2018 Lizard Media (http://lizardmedia.pl) + */ + +namespace LizardMedia\AdminIndexer\Model\Adapter\ReactPHP\ChildProcess; + +use LizardMedia\AdminIndexer\Api\Adapter\ReactPHP\ChildProcess\ProcessFactoryInterface; +use React\ChildProcess\Process; + +/** + * Class ProcessFactory + * @package LizardMedia\AdminIndexer\Model\Adapter\ReactPHP + */ +class ProcessFactory implements ProcessFactoryInterface +{ + /** + * @param string $command + * @return Process + */ + public function create(string $command): Process + { + return new Process($command); + } +} diff --git a/Model/Adapter/ReactPHP/EventLoop/LoopFactory.php b/Model/Adapter/ReactPHP/EventLoop/LoopFactory.php new file mode 100644 index 0000000..cc53e55 --- /dev/null +++ b/Model/Adapter/ReactPHP/EventLoop/LoopFactory.php @@ -0,0 +1,46 @@ + + * @copyright Copyright (C) 2018 Lizard Media (http://lizardmedia.pl) + */ + +namespace LizardMedia\AdminIndexer\Model\Adapter\ReactPHP\EventLoop; + +use LizardMedia\AdminIndexer\Api\Adapter\ReactPHP\EventLoop\LoopFactoryInterface; +use React\EventLoop\LoopInterface; +use React\EventLoop\Factory; + +/** + * Class LoopFactory + * @package LizardMedia\AdminIndexer\Model\Adapter\ReactPHP\EventLoop + */ +class LoopFactory implements LoopFactoryInterface +{ + /** + * @var Factory + */ + private $factory; + + /** + * LoopFactory constructor. + * @param Factory $factory + */ + public function __construct( + Factory $factory + ) { + $this->factory = $factory; + } + + /** + * @return LoopInterface + */ + public function create(): LoopInterface + { + return $this->factory->create(); + } +} \ No newline at end of file diff --git a/Model/IndexerProcessor.php b/Model/IndexerProcessor.php index a775a53..15ecd19 100644 --- a/Model/IndexerProcessor.php +++ b/Model/IndexerProcessor.php @@ -14,7 +14,7 @@ use LizardMedia\AdminIndexer\Api\IndexerProcessorInterface; use LizardMedia\AdminIndexer\Api\ReindexRunnerInterface; -use LizardMedia\AdminIndexer\Exception\ReindexFailureException; +use LizardMedia\AdminIndexer\Exception\ReindexFailureAggregateException; /** * Class IndexerProcessor @@ -42,17 +42,6 @@ public function __construct( */ public function process(string ...$indexerIds): void { - foreach ($indexerIds as $indexerId) { - try { - $this->reindexRunner->run($indexerId); - } catch (\Exception $exception) { - throw new ReindexFailureException( - __($exception->getMessage()), - $indexerId, - $exception, - $exception->getCode() - ); - } - } + $this->reindexRunner->run(...$indexerIds); } } diff --git a/Model/ReindexRunner/AsyncReindexRunner.php b/Model/ReindexRunner/AsyncReindexRunner.php index 881e755..bab8d3b 100644 --- a/Model/ReindexRunner/AsyncReindexRunner.php +++ b/Model/ReindexRunner/AsyncReindexRunner.php @@ -12,23 +12,36 @@ namespace LizardMedia\AdminIndexer\Model\ReindexRunner; -use LizardMedia\AdminIndexer\Api\ReindexRunner\AsyncReindexRunnerInterface; +use LizardMedia\AdminIndexer\Api\Adapter\ReactPHP\ChildProcess\ProcessFactoryInterface; +use LizardMedia\AdminIndexer\Api\Adapter\ReactPHP\EventLoop\LoopFactoryInterface; +use LizardMedia\AdminIndexer\Api\ReindexRunnerInterface; use LizardMedia\AdminIndexer\Api\ReindexRunner\MessageBagInterface; +use LizardMedia\AdminIndexer\Exception\ReindexFailureAggregateException; use Magento\Framework\Filesystem\DirectoryList; use Magento\Framework\Indexer\IndexerRegistry; -use Symfony\Component\Process\Process; -use Symfony\Component\Process\ProcessFactory; +use React\EventLoop\LoopInterface; +use React\ChildProcess\Process; /** * Class AsyncReindexRunner * @package LizardMedia\AdminIndexer\Model\ReindexRunner */ -class AsyncReindexRunner implements AsyncReindexRunnerInterface +class AsyncReindexRunner implements ReindexRunnerInterface { /** - * @const string + * @var string */ - const INDEXER_REINDEX_COMMAND = 'bin/magento indexer:reindex'; + private const INDEXER_REINDEX_COMMAND = 'bin/magento indexer:reindex'; + + /** + * @var ProcessFactoryInterface + */ + private $childProcessFactory; + + /** + * @var LoopFactoryInterface + */ + private $loopFactory; /** * @var MessageBagInterface @@ -46,47 +59,86 @@ class AsyncReindexRunner implements AsyncReindexRunnerInterface private $indexerRegistry; /** - * @var ProcessFactory - */ - private $processFactory; - - /** - * ReindexRunner constructor. + * AsyncReindexRunner constructor. + * @param ProcessFactoryInterface $childProcessFactory + * @param LoopFactoryInterface $loopFactory * @param MessageBagInterface $messageBag * @param DirectoryList $directoryList * @param IndexerRegistry $indexerRegistry - * @param ProcessFactory $processFactory */ public function __construct( + ProcessFactoryInterface $childProcessFactory, + LoopFactoryInterface $loopFactory, MessageBagInterface $messageBag, DirectoryList $directoryList, - IndexerRegistry $indexerRegistry, - ProcessFactory $processFactory + IndexerRegistry $indexerRegistry ) { + $this->childProcessFactory = $childProcessFactory; + $this->loopFactory = $loopFactory; $this->messageBag = $messageBag; $this->directoryList = $directoryList; $this->indexerRegistry = $indexerRegistry; - $this->processFactory = $processFactory; } /** * {@inheritDoc} */ - public function run(string $indexerId): void + public function run(string ...$indexerIds): void + { + $this->informAboutIndexing($indexerIds); + $indexerIds = $this->formatIndexersToBeReindex(...$indexerIds); + $loop = $this->instantiateLoop(); + + try { + $command = $this->buildCommand($indexerIds); + $process = $this->instantiateNewProcess($command); + $process->start($loop); + $loop->run(); + } catch (\Exception $exception) { + $this->handleException($exception); + } + } + + /** + * @param string[] ...$indexerIds + * @return string + */ + private function formatIndexersToBeReindex(string ...$indexerIds): string { - $command = $this->buildCommand($indexerId); - $process = $this->instantiateNewProcess($command); - $process->start(); - $this->messageBag->addMessage(__('Indexing of indexer %1 has been executed', $indexerId)->render()); + return implode(' ', $indexerIds); } /** - * @param string $indexerId + * @param array $indexerIds + * @return void + */ + private function informAboutIndexing(array $indexerIds): void + { + foreach ($indexerIds as $indexerId) { + $this->messageBag->addMessage(__('Indexing of indexer %1 has been executed', $indexerId)->render()); + } + } + + /** + * @return LoopInterface + */ + private function instantiateLoop(): LoopInterface + { + return $this->loopFactory->create(); + } + + /** + * @param string $indexerIds * @return string */ - private function buildCommand(string $indexerId): string + private function buildCommand(string $indexerIds): string { - return sprintf('php %s/%s %s', $this->getRootDir(), self::INDEXER_REINDEX_COMMAND, $indexerId); + return sprintf( + 'php %s/%s %s > /dev/null 2>&1 &', + $this->getRootDir(), + self::INDEXER_REINDEX_COMMAND, + $indexerIds + ); } /** @@ -103,6 +155,21 @@ private function getRootDir(): string */ private function instantiateNewProcess(string $command): Process { - return $this->processFactory->create(['commandline' => $command]); + return $this->childProcessFactory->create($command); + } + + + /** + * @param \Exception $exception + * @return void + * @throws ReindexFailureAggregateException + */ + private function handleException(\Exception $exception): void + { + throw new ReindexFailureAggregateException( + __($exception->getMessage()), + $exception, + $exception->getCode() + ); } } diff --git a/Model/ReindexRunner/SyncReindexRunner.php b/Model/ReindexRunner/SyncReindexRunner.php index fd5dbca..368acba 100644 --- a/Model/ReindexRunner/SyncReindexRunner.php +++ b/Model/ReindexRunner/SyncReindexRunner.php @@ -12,15 +12,21 @@ namespace LizardMedia\AdminIndexer\Model\ReindexRunner; -use LizardMedia\AdminIndexer\Api\ReindexRunner\SyncReindexRunnerInterface; +use LizardMedia\AdminIndexer\Api\ReindexRunnerInterface; +use LizardMedia\AdminIndexer\Exception\ReindexFailureAggregateException; use Magento\Framework\Indexer\IndexerRegistry; /** * Class SyncReindexRunner * @package LizardMedia\AdminIndexer\Model\ReindexRunner */ -class SyncReindexRunner implements SyncReindexRunnerInterface +class SyncReindexRunner implements ReindexRunnerInterface { + /** + * @var ReindexFailureAggregateException + */ + private $reindexFailureAggregateException; + /** * @var IndexerRegistry */ @@ -39,9 +45,49 @@ public function __construct( /** * {@inheritDoc} */ - public function run(string $indexerId): void + public function run(string ...$indexerIds): void + { + foreach ($indexerIds as $indexerId) { + try { + $indexer = $this->indexerRegistry->get($indexerId); + $indexer->reindexAll(); + } catch (\Exception $exception) { + $this->addReindexFailureException(); + $this->reindexFailureAggregateException->addError( + __( + 'Indexing of %1 has failed: %2', + $indexerId, + $exception->getMessage() + ) + ); + + continue; + } + } + + $this->handleExceptions(); + } + + /** + * @return void + */ + private function addReindexFailureException(): void + { + if (!$this->reindexFailureAggregateException instanceof ReindexFailureAggregateException) { + $this->reindexFailureAggregateException = new ReindexFailureAggregateException( + __('Following indexing errors has occurred: ') + ); + } + } + + /** + * @throws ReindexFailureAggregateException + * @return void + */ + private function handleExceptions(): void { - $indexer = $this->indexerRegistry->get($indexerId); - $indexer->reindexAll(); + if ($this->reindexFailureAggregateException instanceof ReindexFailureAggregateException) { + throw $this->reindexFailureAggregateException; + } } } diff --git a/README.md b/README.md index 99a13f7..3fcd23a 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ bin/magento setup:upgrade ## For developers -Indexing is performed in background, each index in separate process. +Indexing is performed in background, using reactPHP child process component ## Contributing @@ -73,5 +73,5 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file ## To do -* write unit test for module -* add possibility to track indexing progress \ No newline at end of file +* add possibility to track indexing progress +* add integration tests \ No newline at end of file diff --git a/Test/Unit/Controller/Adminhtml/Indexer/MassReindexTest.php b/Test/Unit/Controller/Adminhtml/Indexer/MassReindexTest.php new file mode 100644 index 0000000..9460a28 --- /dev/null +++ b/Test/Unit/Controller/Adminhtml/Indexer/MassReindexTest.php @@ -0,0 +1,223 @@ + + * @copyright Copyright (C) 2018 Lizard Media (http://lizardmedia.pl) + */ + +namespace LizardMedia\AdminIndexer\Test\Unit\Controller\Adminhtml\Indexer; + +use LizardMedia\AdminIndexer\Api\IndexerProcessorInterface; +use LizardMedia\AdminIndexer\Api\ReindexRunner\MessageBagInterface; +use LizardMedia\AdminIndexer\Controller\Adminhtml\Indexer\MassReindex; +use LizardMedia\AdminIndexer\Exception\ReindexFailureAggregateException; +use Magento\Backend\App\Action\Context; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\Controller\Result\Redirect; +use Magento\Framework\Controller\Result\RedirectFactory; +use Magento\Framework\Message\ManagerInterface; +use PHPUnit_Framework_MockObject_MockObject as MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Class MassReindexTest + * @package LizardMedia\AdminIndexer\Test\Unit\Controller\Adminhtml\Indexer + */ +class MassReindexTest extends TestCase +{ + /** + * @var IndexerProcessorInterface | MockObject + */ + private $indexerProcessorMock; + + /** + * @var MessageBagInterface | MockObject + */ + private $messageBagMock; + + /** + * @var MassReindex + */ + private $massReindex; + + /** + * @var ReindexFailureAggregateException + */ + private $reindexFailureAggregateException; + + /** + * @var Redirect | MockObject + */ + private $redirectMock; + + /** + * @var RequestInterface | MockObject + */ + private $requestMock; + + /** + * @var RedirectFactory | MockObject + */ + private $redirectFactoryMock; + + /** + * @var ManagerInterface | MockObject + */ + private $messageManager; + + /** + * @return void + */ + protected function setUp(): void + { + //Internal mocks + $this->reindexFailureAggregateException = new ReindexFailureAggregateException(__('Some message')); + $context = $this->getMockBuilder(Context::class)->disableOriginalConstructor()->getMock(); + $this->requestMock = $this->getMockBuilder(RequestInterface::class)->getMock(); + $this->redirectMock = $this->getMockBuilder(Redirect::class) + ->disableOriginalConstructor() + ->getMock(); + $this->messageManager = $this->getMockBuilder(ManagerInterface::class)->getMock(); + + //Dependencies mocks + $this->messageBagMock = $this->getMockBuilder(MessageBagInterface::class)->getMock(); + $this->redirectFactoryMock = $this->getMockBuilder(RedirectFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->indexerProcessorMock = $this->getMockBuilder(IndexerProcessorInterface::class)->getMock(); + + $context->expects($this->once()) + ->method('getRequest') + ->willReturn($this->requestMock); + $context->expects($this->once()) + ->method('getMessageManager') + ->willReturn($this->messageManager); + + $this->massReindex = new MassReindex( + $this->messageBagMock, + $context, + $this->redirectFactoryMock, + $this->indexerProcessorMock + ); + } + + + /** + * @test + * @dataProvider provideInvalidRequestParamsExamples + * @param $invalidParam + * @return void + */ + public function testExecuteWhenParamsAreInvalid($invalidParam): void + { + $this->requestMock->expects($this->once()) + ->method('getParam') + ->with('indexer_ids') + ->willReturn($invalidParam); + $this->gettingRedirectExpectations(); + $this->massReindex->execute(); + } + + + /** + * @return array + */ + public function provideInvalidRequestParamsExamples(): array + { + return [ + [[]], + [''], + ['test'], + [1], + [null] + ]; + } + + /** + * @test + * @return void + */ + public function testExecuteWhenIndexerProcessorThrowsException(): void + { + $this->requestMock->expects($this->once()) + ->method('getParam') + ->with('indexer_ids') + ->willReturn(['catalog_product', 'customer_grid']); + $this->indexerProcessorMock->expects($this->once()) + ->method('process') + ->with('catalog_product', 'customer_grid') + ->willThrowException($this->reindexFailureAggregateException); + $this->simulateTwoIndexerErros(); + + $this->messageBagMock->expects($this->never())->method('getMessages'); + + $this->messageManager->expects($this->exactly(3)) + ->method('addErrorMessage') + ->withConsecutive( + [$this->reindexFailureAggregateException->getMessage()], + ['sth'], + ['sth else'] + ); + $this->gettingRedirectExpectations(); + $this->massReindex->execute(); + } + + /** + * @test + * @return void + */ + public function testExecuteWhenSucceed(): void + { + $this->requestMock->expects($this->once()) + ->method('getParam') + ->with('indexer_ids') + ->willReturn(['catalog_product', 'customer_grid']); + $this->indexerProcessorMock->expects($this->once()) + ->method('process') + ->with('catalog_product', 'customer_grid'); + $this->expectationsForInformingAboutIndexersRunning(); + $this->gettingRedirectExpectations(); + $this->massReindex->execute(); + } + + + /** + * @return void + */ + private function gettingRedirectExpectations(): void + { + $this->redirectFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->redirectMock); + $this->redirectMock->expects($this->once()) + ->method('setPath') + ->with('indexer/indexer/list') + ->willReturnSelf(); + } + + /** + * @return void + */ + private function expectationsForInformingAboutIndexersRunning(): void + { + $this->messageBagMock->expects($this->once()) + ->method('getMessages') + ->willReturn(['one', 'two']); + $this->messageManager->expects($this->exactly(2)) + ->method('addNoticeMessage') + ->withConsecutive(['one'], ['two']); + } + + /** + * @return void + */ + private function simulateTwoIndexerErros(): void + { + $this->reindexFailureAggregateException->addError(__('sth')); + $this->reindexFailureAggregateException->addError(__('sth else')); + } +} diff --git a/Test/Unit/Model/Adapter/ReactPHP/ChildProcess/ProcessFactoryTest.php b/Test/Unit/Model/Adapter/ReactPHP/ChildProcess/ProcessFactoryTest.php new file mode 100644 index 0000000..1d0dd90 --- /dev/null +++ b/Test/Unit/Model/Adapter/ReactPHP/ChildProcess/ProcessFactoryTest.php @@ -0,0 +1,36 @@ + + * @copyright Copyright (C) 2018 Lizard Media (http://lizardmedia.pl) + */ + +namespace LizardMedia\AdminIndexer\Test\Unit\Model\Adapter\ReactPHP; + +use LizardMedia\AdminIndexer\Model\Adapter\ReactPHP\ChildProcess\ProcessFactory; +use PHPUnit\Framework\TestCase; +use React\ChildProcess\Process; + +/** + * Class ProcessFactoryTest + * @package LizardMedia\AdminIndexer\Test\Unit\Model\Adapter\ReactPHP\ChildProcess + */ +class ProcessFactoryTest extends TestCase +{ + /** + * @test + * @return void + */ + public function testCreateReturnsCorrectInstance(): void + { + $processFactory = new ProcessFactory(); + $this->assertInstanceOf( + Process::class, + $processFactory->create('ls') + ); + } +} diff --git a/Test/Unit/Model/Adapter/ReactPHP/EventLoop/LoopFactoryTest.php b/Test/Unit/Model/Adapter/ReactPHP/EventLoop/LoopFactoryTest.php new file mode 100644 index 0000000..db0ebeb --- /dev/null +++ b/Test/Unit/Model/Adapter/ReactPHP/EventLoop/LoopFactoryTest.php @@ -0,0 +1,37 @@ + + * @copyright Copyright (C) 2018 Lizard Media (http://lizardmedia.pl) + */ + +namespace LizardMedia\AdminIndexer\Test\Unit\Model\Adapter\ReactPHP\EventLoop; + +use LizardMedia\AdminIndexer\Model\Adapter\ReactPHP\EventLoop\LoopFactory; +use PHPUnit\Framework\TestCase; +use React\EventLoop\Factory; +use React\EventLoop\LoopInterface; + +/** + * Class LoopFactoryTest + * @package LizardMedia\AdminIndexer\Test\Unit\Model\Adapter\ReactPHP\EventLoop + */ +class LoopFactoryTest extends TestCase +{ + /** + * @test + * @return void + */ + public function testCreateReturnsCorrectInstance(): void + { + $loopFactory = new LoopFactory(new Factory()); + $this->assertInstanceOf( + LoopInterface::class, + $loopFactory->create() + ); + } +} \ No newline at end of file diff --git a/Test/Unit/Model/IndexerProcessorTest.php b/Test/Unit/Model/IndexerProcessorTest.php new file mode 100644 index 0000000..0024143 --- /dev/null +++ b/Test/Unit/Model/IndexerProcessorTest.php @@ -0,0 +1,72 @@ + + * @copyright Copyright (C) 2018 Lizard Media (http://lizardmedia.pl) + */ + +namespace LizardMedia\AdminIndexer\Unit\Test\Model; + +use LizardMedia\AdminIndexer\Api\ReindexRunnerInterface; +use LizardMedia\AdminIndexer\Exception\ReindexFailureAggregateException; +use LizardMedia\AdminIndexer\Model\IndexerProcessor; +use PHPUnit_Framework_MockObject_MockObject as MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Class IndexerProcessorTest + * @package LizardMedia\AdminIndexer\Test\Unit\Model + */ +class IndexerProcessorTest extends TestCase +{ + /** + * @var ReindexRunnerInterface | MockObject + */ + private $reindexRunnerMock; + + /** + * @var IndexerProcessor + */ + private $indexerProcessor; + + /** + * @return void + */ + protected function setUp(): void + { + $this->reindexRunnerMock = $this->getMockBuilder(ReindexRunnerInterface::class)->getMock(); + $this->indexerProcessor = new IndexerProcessor($this->reindexRunnerMock); + } + + /** + * @test + * @return void + */ + public function testWhenReindexeRunnerThrowsException(): void + { + $this->reindexRunnerMock->expects($this->once()) + ->method('run') + ->withAnyParameters() + ->willThrowException(new ReindexFailureAggregateException(__())); + $this->expectException(ReindexFailureAggregateException::class); + $this->indexerProcessor->process('catalog_category_product', 'catalog_product_category'); + } + + + /** + * @test + * @return void + */ + public function testWhenReindexed(): void + { + $exampleIndexers = ['catalog_category_product', 'catalog_product_category']; + $this->reindexRunnerMock->expects($this->once()) + ->method('run') + ->with(...$exampleIndexers); + $this->indexerProcessor->process('catalog_category_product', 'catalog_product_category'); + } +} diff --git a/Test/Unit/Model/ReindexRunner/AsyncReindexRunnerTest.php b/Test/Unit/Model/ReindexRunner/AsyncReindexRunnerTest.php new file mode 100644 index 0000000..76e648f --- /dev/null +++ b/Test/Unit/Model/ReindexRunner/AsyncReindexRunnerTest.php @@ -0,0 +1,174 @@ + + * @copyright Copyright (C) 2018 Lizard Media (http://lizardmedia.pl) + */ + +namespace LizardMedia\AdminIndexer\Test\Unit\Model\ReindexRunner; + +use LizardMedia\AdminIndexer\Api\Adapter\ReactPHP\ChildProcess\ProcessFactoryInterface; +use LizardMedia\AdminIndexer\Api\Adapter\ReactPHP\EventLoop\LoopFactoryInterface; +use LizardMedia\AdminIndexer\Api\ReindexRunner\MessageBagInterface; +use LizardMedia\AdminIndexer\Exception\ReindexFailureAggregateException; +use LizardMedia\AdminIndexer\Model\ReindexRunner\AsyncReindexRunner; +use Magento\Framework\Filesystem\DirectoryList; +use Magento\Framework\Indexer\IndexerInterface; +use Magento\Framework\Indexer\IndexerRegistry; +use PHPUnit_Framework_MockObject_MockObject as MockObject; +use PHPUnit\Framework\TestCase; +use React\ChildProcess\Process as ChildProcess; +use React\EventLoop\LoopInterface; + +/** + * Class AsyncReindexRunnerTest + * @package LizardMedia\AdminIndexer\Test\Unit\Model\ReindexRunner + */ +class AsyncReindexRunnerTest extends TestCase +{ + /** + * @var array + */ + private $exampleIndexes; + + /** + * @var ProcessFactoryInterface | MockObject + */ + private $childProcessFactoryMock; + + /** + * @var LoopFactoryInterface | MockObject + */ + private $loopFactory; + + /** + * @var MessageBagInterface | MockObject + */ + private $messageBagMock; + + /** + * @var ReindexFailureAggregateException | MockObject + */ + private $reindexFailureAggregateExceptionMock; + + /** + * @var AsyncReindexRunner + */ + private $asyncReindexRunner; + + /** + * @var DirectoryList | MockObject + */ + private $directoryListMock; + + /** + * @var IndexerInterface | MockObject + */ + private $indexerMock; + + /** + * @var IndexerRegistry | MockObject + */ + private $indexerRegistryMock; + + /** + * @var ChildProcess | MockObject + */ + private $childProcess; + + /** + * @var LoopInterface | MockObject + */ + private $loopMock; + + /** + * @return void + */ + protected function setUp(): void + { + //Internal mocks + $this->exampleIndexes = ['catalog_product', 'customer_grid']; + $this->reindexFailureAggregateExceptionMock = $this->getMockBuilder(ReindexFailureAggregateException::class) + ->disableOriginalConstructor() + ->getMock(); + $this->indexerMock = $this->getMockBuilder(IndexerInterface::class)->getMock(); + $this->loopMock = $this->getMockBuilder(LoopInterface::class)->getMock(); + + //Dependencies mocks + $this->childProcessFactoryMock = $this->getMockBuilder(ProcessFactoryInterface::class)->getMock(); + $this->loopFactory = $this->getMockBuilder(LoopFactoryInterface::class)->getMock(); + $this->messageBagMock = $this->getMockBuilder(MessageBagInterface::class)->getMock(); + $this->directoryListMock = $this->getMockBuilder(DirectoryList::class) + ->disableOriginalConstructor() + ->getMock(); + $this->childProcess = $this->getMockBuilder(ChildProcess::class) + ->disableOriginalConstructor() + ->getMock(); + $this->indexerRegistryMock = $this->getMockBuilder(IndexerRegistry::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->asyncReindexRunner = new AsyncReindexRunner( + $this->childProcessFactoryMock, + $this->loopFactory, + $this->messageBagMock, + $this->directoryListMock, + $this->indexerRegistryMock + ); + } + + /** + * @test + * @return void + */ + public function testRunWhenProcessStartingThrownException(): void + { + $this->initialExpectations(); + $this->childProcess->expects($this->once()) + ->method('start') + ->with($this->loopMock) + ->willThrowException(new \RuntimeException); + $this->loopMock->expects($this->never())->method('run'); + + $this->expectException(ReindexFailureAggregateException::class); + $this->asyncReindexRunner->run(...$this->exampleIndexes); + } + + /** + * @test + * @return void + */ + public function testRunWhenSucceed(): void + { + $this->initialExpectations(); + $this->childProcess->expects($this->once()) + ->method('start'); + $this->loopMock->expects($this->once()) + ->method('run'); + + $this->asyncReindexRunner->run(...$this->exampleIndexes); + } + + /** + * @return void + */ + private function initialExpectations(): void + { + $this->messageBagMock->expects($this->exactly(2)) + ->method('addMessage'); + $this->loopFactory->expects($this->once()) + ->method('create') + ->willReturn($this->loopMock); + $this->directoryListMock->expects($this->once()) + ->method('getRoot') + ->willReturn('/var/www/html'); + $this->childProcessFactoryMock->expects($this->once()) + ->method('create') + ->with(sprintf('php /var/www/html/bin/magento indexer:reindex %s > /dev/null 2>&1 &', implode(' ', $this->exampleIndexes))) + ->willReturn($this->childProcess); + } +} diff --git a/Test/Unit/Model/ReindexRunner/MessageBagTest.php b/Test/Unit/Model/ReindexRunner/MessageBagTest.php new file mode 100644 index 0000000..4758d7f --- /dev/null +++ b/Test/Unit/Model/ReindexRunner/MessageBagTest.php @@ -0,0 +1,46 @@ + + * @copyright Copyright (C) 2018 Lizard Media (http://lizardmedia.pl) + */ + +namespace LizardMedia\AdminIndexer\Unit\Test\Model\ReindexRunner; + +use LizardMedia\AdminIndexer\Model\ReindexRunner\MessageBag; +use PHPUnit\Framework\TestCase; + +/** + * Class MessageBagTest + * @package LizardMedia\AdminIndexer\Unit\Test\Model\ReindexRunner + */ +class MessageBagTest extends TestCase +{ + /** + * @test + * @return void + */ + public function testAddMessage(): void + { + $messageBag = new MessageBag(); + $messageBag->addMessage('test'); + $this->assertAttributeSame(['test'], 'messages', $messageBag); + } + + /** + * @test + * @return void + */ + public function testGetMessage(): void + { + $messageBag = new MessageBag(); + $messageBag->addMessage('test'); + $messageBag->addMessage('test1'); + + $this->assertSame(['test', 'test1'], $messageBag->getMessages()); + } +} diff --git a/Test/Unit/Model/ReindexRunner/SyncReindexRunnerTest.php b/Test/Unit/Model/ReindexRunner/SyncReindexRunnerTest.php new file mode 100644 index 0000000..9ea09e9 --- /dev/null +++ b/Test/Unit/Model/ReindexRunner/SyncReindexRunnerTest.php @@ -0,0 +1,117 @@ + + * @copyright Copyright (C) 2018 Lizard Media (http://lizardmedia.pl) + */ + +namespace LizardMedia\AdminIndexer\Unit\Test\Model\ReindexRunner; + +use LizardMedia\AdminIndexer\Exception\ReindexFailureAggregateException; +use LizardMedia\AdminIndexer\Model\ReindexRunner\SyncReindexRunner; +use Magento\Framework\Indexer\IndexerInterface; +use Magento\Framework\Indexer\IndexerRegistry; +use PHPUnit_Framework_MockObject_MockObject as MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Class SyncReindexRunnerTest + * @package LizardMedia\AdminIndexer\Unit\Test\Model\ReindexRunner + */ +class SyncReindexRunnerTest extends TestCase +{ + /** + * @var array + */ + private $exampleIndexes; + + /** + * @var ReindexFailureAggregateException | MockObject + */ + private $reindexFailureAggregateExceptionMock; + + /** + * @var SyncReindexRunner + */ + private $syncReindexRunner; + + /** + * @var IndexerInterface | MockObject + */ + private $indexerMock; + + /** + * @var IndexerRegistry | MockObject + */ + private $indexerRegistryMock; + + /** + * @return void + */ + protected function setUp(): void + { + //Internal mocks + $this->exampleIndexes = ['catalog_product', 'customer_grid']; + $this->reindexFailureAggregateExceptionMock = $this->getMockBuilder(ReindexFailureAggregateException::class) + ->disableOriginalConstructor() + ->setMethodsExcept(['addError', 'getErrors']) + ->getMock(); + $this->indexerMock = $this->getMockBuilder(IndexerInterface::class)->getMock(); + + //Dependencies mocks + $this->indexerRegistryMock = $this->getMockBuilder(IndexerRegistry::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->syncReindexRunner = new SyncReindexRunner($this->indexerRegistryMock); + } + + /** + * @test + * @return void + */ + public function testRunWhenIndexerThrowsException(): void + { + $this->indexerRegistryMock->expects($this->exactly(2)) + ->method('get') + ->withConsecutive( + [$this->exampleIndexes[0]], + [$this->exampleIndexes[1]] + )->willReturn($this->indexerMock); + + $indexerException = new \Exception('Something went wrong'); + + $this->indexerMock->expects($this->at(0)) + ->method('reindexAll') + ->willThrowException($indexerException); + $this->indexerMock->expects($this->at(1)) + ->method('reindexAll'); + + $this->expectException(ReindexFailureAggregateException::class); + $this->syncReindexRunner->run(...$this->exampleIndexes); + $this->assertCount(1, $this->reindexFailureAggregateExceptionMock->getErrors()); + } + + /** + * @test + * @return void + */ + public function testRunWhenSucceed(): void + { + $this->indexerRegistryMock->expects($this->exactly(2)) + ->method('get') + ->withConsecutive( + [$this->exampleIndexes[0]], + [$this->exampleIndexes[1]] + )->willReturn($this->indexerMock); + + $this->indexerMock->expects($this->exactly(2)) + ->method('reindexAll'); + + $this->syncReindexRunner->run(...$this->exampleIndexes); + } +} diff --git a/composer.json b/composer.json index 29c97e8..c0817e5 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "lizardmedia/module-admin-indexer", "description": "Magento2 module adding indexation from the admin panel", "type": "magento2-module", - "version": "1.0.2", + "version": "1.1.0", "license": [ "MIT" ], @@ -19,7 +19,7 @@ "require": { "php": "^7.1.0", "magento/module-indexer": "^100.2.3", - "symfony/process": "^2.7 || ^3.0" + "react/child-process": "^0.5.2" }, "require-dev": { "phpunit/phpunit": ">=7.0.0" diff --git a/etc/di.xml b/etc/di.xml index cb6795a..0561769 100644 --- a/etc/di.xml +++ b/etc/di.xml @@ -10,12 +10,12 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + + - - @@ -23,7 +23,7 @@ - LizardMedia\AdminIndexer\Api\ReindexRunner\AsyncReindexRunnerInterface + LizardMedia\AdminIndexer\Model\ReindexRunner\AsyncReindexRunner diff --git a/etc/module.xml b/etc/module.xml index 30c0e21..ac0815a 100644 --- a/etc/module.xml +++ b/etc/module.xml @@ -7,7 +7,7 @@ --> - + diff --git a/i18n/en_US.csv b/i18n/en_US.csv index ae628af..9801df9 100644 --- a/i18n/en_US.csv +++ b/i18n/en_US.csv @@ -2,4 +2,6 @@ "Please select at least one index.","Please select at least one index." "LizardMedia admin indexer","LizardMedia admin indexer" "Reindex failed on indexer %1","Reindex failed on indexer %1" -"Indexing of indexer %1 has been executed","Indexing of indexer %1 has been executed" \ No newline at end of file +"Indexing of indexer %1 has been executed","Indexing of indexer %1 has been executed" +"Following indexing errors has occured: ","Following indexing errors has occured: " +"Indexing of %1 has failed: %2","Indexing of %1 has failed: %2" \ No newline at end of file diff --git a/i18n/pl_PL.csv b/i18n/pl_PL.csv index ac8a8f3..7be3248 100644 --- a/i18n/pl_PL.csv +++ b/i18n/pl_PL.csv @@ -3,3 +3,5 @@ "LizardMedia admin indexer","LizardMedia admin indexer" "Reindex failed on indexer %1","Błąd indeksowania na indeksie %1" "Indexing of indexer %1 has been executed","Rozpoczęto indeksowanie indeksu %1" +"Following indexing errors has occured: ","Następujące błędy indeksowanie miały miejsce: " +"Indexing of %1 has failed: %2","Indeksowanie %1 nie udało się: %2"