From 90215616b1d6c3679729dd5a92de5c245b2f15aa Mon Sep 17 00:00:00 2001
From: Luke Rodgers <lukerodgers90@gmail.com>
Date: Sun, 5 Nov 2017 20:51:31 +0000
Subject: [PATCH] Add command to view mview state and queue

This is similar to the magerun1 command here: https://github.com/netz98/n98-magerun/pull/891

I like the ability to view the mview queue in realtime as its being processed, it can be quite helpful when debugging indexing issues.

This command will actually show how many items are in the list pending processing, as well information from the `mview_state` table.

```
php bin/magento indexer:status:mview
+---------------------------+----------+--------+---------------------+------------+---------+
| ID                        | Mode     | Status | Updated             | Version ID | Backlog |
+---------------------------+----------+--------+---------------------+------------+---------+
| catalog_category_product  | enabled  | idle   | 2017-11-02 10:00:00 | 1          | 0       |
| catalog_product_attribute | enabled  | idle   | 2017-11-02 10:00:00 | 1          | 1       |
| catalog_product_category  | disabled | idle   | 2017-11-02 10:00:00 | 1          | 0       |
| catalog_product_price     | enabled  | idle   | 2017-11-02 10:00:00 | 1          | 0       |
+---------------------------+----------+--------+---------------------+------------+---------+
```

I'll point this PR into 2.1.x and raise a separate PR to pop it into 2.2.x.
---
 .../Command/IndexerStatusMviewCommand.php     |  95 +++++++
 .../Command/IndexerStatusMviewCommandTest.php | 233 ++++++++++++++++++
 app/code/Magento/Indexer/etc/di.xml           |   1 +
 3 files changed, 329 insertions(+)
 create mode 100644 app/code/Magento/Indexer/Console/Command/IndexerStatusMviewCommand.php
 create mode 100644 app/code/Magento/Indexer/Test/Unit/Console/Command/IndexerStatusMviewCommandTest.php

diff --git a/app/code/Magento/Indexer/Console/Command/IndexerStatusMviewCommand.php b/app/code/Magento/Indexer/Console/Command/IndexerStatusMviewCommand.php
new file mode 100644
index 00000000000..4fb0c0bcb56
--- /dev/null
+++ b/app/code/Magento/Indexer/Console/Command/IndexerStatusMviewCommand.php
@@ -0,0 +1,95 @@
+<?php
+/**
+ * Copyright © 2013-2017 Magento, Inc. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+namespace Magento\Indexer\Console\Command;
+
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Command\Command;
+use Magento\Framework\Mview\View;
+
+/**
+ * Command for displaying status of mview indexers.
+ */
+class IndexerStatusMviewCommand extends Command
+{
+    /** @var \Magento\Framework\Mview\View\CollectionInterface $mviewIndexersCollection */
+    private $mviewIndexersCollection;
+
+    public function __construct(
+        \Magento\Framework\Mview\View\CollectionInterface $collection
+    ) {
+        $this->mviewIndexersCollection = $collection;
+        parent::__construct();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function configure()
+    {
+        $this->setName('indexer:status:mview')
+            ->setDescription('Shows status of Mview Indexers and their queue status');
+
+        parent::configure();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function execute(InputInterface $input, OutputInterface $output)
+    {
+        try {
+            $table = $this->getHelperSet()->get('table');
+            $table->setHeaders(['ID', 'Mode', 'Status', 'Updated', 'Version ID', 'Backlog']);
+
+            $rows = [];
+
+            /** @var \Magento\Framework\Mview\View $indexer */
+            foreach ($this->mviewIndexersCollection as $indexer) {
+                $state = $indexer->getState();
+                $changelog = $indexer->getChangelog();
+
+                try {
+                    $currentVersionId = $changelog->getVersion();
+                } catch (View\ChangelogTableNotExistsException $e) {
+                    continue;
+                }
+
+                $pendingCount = count($changelog->getList($state->getVersionId(), $currentVersionId));
+
+                $pendingString = "<error>$pendingCount</error>";
+                if ($pendingCount <= 0) {
+                    $pendingString = "<info>$pendingCount</info>";
+                }
+
+                $rows[] = [
+                    $indexer->getData('view_id'),
+                    $state->getData('mode'),
+                    $state->getData('status'),
+                    $state->getData('updated'),
+                    $state->getData('version_id'),
+                    $pendingString,
+                ];
+            }
+
+            usort($rows, function($a, $b) {
+                return $a[0] <=> $b[0];
+            });
+
+            $table->addRows($rows);
+            $table->render($output);
+
+            return \Magento\Framework\Console\Cli::RETURN_SUCCESS;
+        } catch (\Exception $e) {
+            $output->writeln('<error>' . $e->getMessage() . '</error>');
+            if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
+                $output->writeln($e->getTraceAsString());
+            }
+
+            return \Magento\Framework\Console\Cli::RETURN_FAILURE;
+        }
+    }
+}
diff --git a/app/code/Magento/Indexer/Test/Unit/Console/Command/IndexerStatusMviewCommandTest.php b/app/code/Magento/Indexer/Test/Unit/Console/Command/IndexerStatusMviewCommandTest.php
new file mode 100644
index 00000000000..7266d009a5e
--- /dev/null
+++ b/app/code/Magento/Indexer/Test/Unit/Console/Command/IndexerStatusMviewCommandTest.php
@@ -0,0 +1,233 @@
+<?php
+/**
+ * Copyright © 2013-2017 Magento, Inc. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+namespace Magento\Indexer\Test\Unit\Console\Command;
+
+use \Magento\Framework\Mview;
+use Magento\Indexer\Console\Command\IndexerStatusMviewCommand;
+use Symfony\Component\Console\Tester\CommandTester;
+use Symfony\Component\Console\Helper\HelperSet;
+use Symfony\Component\Console\Helper\TableHelper;
+use Magento\Store\Model\Website;
+use Magento\Framework\Console\Cli;
+
+class IndexerStatusMviewCommandTest extends \PHPUnit_Framework_TestCase
+{
+    /**
+     * @var IndexerStatusMviewCommand
+     */
+    private $command;
+
+    /**
+     * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager
+     */
+    private $objectManager;
+
+    /**
+     * @var \Magento\Framework\Mview\View\Collection
+     */
+    private $collection;
+
+    protected function setUp()
+    {
+        $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+
+        /** @var \Magento\Framework\Mview\View\Collection $collection */
+        $this->collection = $this->objectManager->getObject(Mview\View\Collection::class);
+
+        $reflectedCollection = new \ReflectionObject($this->collection);
+        $isLoadedProperty = $reflectedCollection->getProperty('_isCollectionLoaded');
+        $isLoadedProperty->setAccessible(true);
+        $isLoadedProperty->setValue($this->collection, true);
+
+        $this->command = $this->objectManager->getObject(
+            IndexerStatusMviewCommand::class,
+            ['collection' => $this->collection]
+        );
+
+        /** @var HelperSet $helperSet */
+        $helperSet = $this->objectManager->getObject(
+            HelperSet::class,
+            ['helpers' => [$this->objectManager->getObject(TableHelper::class)]]
+        );
+
+        //Inject table helper for output
+        $this->command->setHelperSet($helperSet);
+    }
+
+    public function testExecute()
+    {
+        $mviews = [
+            [
+                'view' => [
+                    'view_id' => 'catalog_category_product',
+                    'mode' => 'enabled',
+                    'status' => 'idle',
+                    'updated' => '2017-01-01 11:11:11',
+                    'version_id' => 100,
+                ],
+                'changelog' => [
+                    'version_id' => 110
+                ],
+            ],
+            [
+                'view' => [
+                    'view_id' => 'catalog_product_category',
+                    'mode' => 'disabled',
+                    'status' => 'idle',
+                    'updated' => '2017-01-01 11:11:11',
+                    'version_id' => 100,
+                ],
+                'changelog' => [
+                    'version_id' => 200
+                ],
+            ],
+            [
+                'view' => [
+                    'view_id' => 'catalog_product_attribute',
+                    'mode' => 'enabled',
+                    'status' => 'idle',
+                    'updated' => '2017-01-01 11:11:11',
+                    'version_id' => 100,
+                ],
+                'changelog' => [
+                    'version_id' => 100
+                ],
+            ],
+        ];
+
+        foreach ($mviews as $data) {
+            $this->collection->addItem($this->generateMviewStub($data['view'], $data['changelog']));
+        }
+
+        /** @var Mview\View\Changelog|\PHPUnit_Framework_MockObject_MockObject $stub */
+        $changelog = $this->getMockBuilder(\Magento\Framework\Mview\View\Changelog::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $changelog->expects($this->any())
+            ->method('getVersion')
+            ->willThrowException(
+                new Mview\View\ChangelogTableNotExistsException(new \Magento\Framework\Phrase("Do not render"))
+            );
+
+        /** @var Mview\View|\PHPUnit_Framework_MockObject_MockObject $notInitiatedMview */
+        $notInitiatedMview = $this->getMockBuilder(\Magento\Framework\Mview\View::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $notInitiatedMview->expects($this->any())
+            ->method('getChangelog')
+            ->willReturn($changelog);
+
+        $this->collection->addItem($notInitiatedMview);
+
+        $tester = new CommandTester($this->command);
+        $this->assertEquals(Cli::RETURN_SUCCESS, $tester->execute([]));
+
+        $linesOutput = array_filter(explode(PHP_EOL, $tester->getDisplay()));
+        $this->assertCount(7, $linesOutput, 'There should be 7 lines output. 3 Spacers, 1 header, 3 content.');
+        $this->assertEquals($linesOutput[0], $linesOutput[2], "Lines 0, 2, 7 should be spacer lines");
+        $this->assertEquals($linesOutput[2], $linesOutput[6], "Lines 0, 2, 6 should be spacer lines");
+
+        $headerValues = array_values(array_filter(explode('|', $linesOutput[1])));
+        $this->assertEquals('ID', trim($headerValues[0]));
+        $this->assertEquals('Mode', trim($headerValues[1]));
+        $this->assertEquals('Status', trim($headerValues[2]));
+        $this->assertEquals('Updated', trim($headerValues[3]));
+        $this->assertEquals('Version ID', trim($headerValues[4]));
+        $this->assertEquals('Backlog', trim($headerValues[5]));
+
+        $catalogCategoryProductMviewData = array_values(array_filter(explode('|', $linesOutput[3])));
+        $this->assertEquals('catalog_category_product', trim($catalogCategoryProductMviewData[0]));
+        $this->assertEquals('enabled', trim($catalogCategoryProductMviewData[1]));
+        $this->assertEquals('idle', trim($catalogCategoryProductMviewData[2]));
+        $this->assertEquals('2017-01-01 11:11:11', trim($catalogCategoryProductMviewData[3]));
+        $this->assertEquals('100', trim($catalogCategoryProductMviewData[4]));
+        $this->assertEquals('10', trim($catalogCategoryProductMviewData[5]));
+        unset($catalogCategoryProductMviewData);
+
+        $catalogProductAttributeMviewData = array_values(array_filter(explode('|', $linesOutput[4])));
+        $this->assertEquals('catalog_product_attribute', trim($catalogProductAttributeMviewData[0]));
+        $this->assertEquals('enabled', trim($catalogProductAttributeMviewData[1]));
+        $this->assertEquals('idle', trim($catalogProductAttributeMviewData[2]));
+        $this->assertEquals('2017-01-01 11:11:11', trim($catalogProductAttributeMviewData[3]));
+        $this->assertEquals('100', trim($catalogProductAttributeMviewData[4]));
+        $this->assertEquals('0', trim($catalogProductAttributeMviewData[5]));
+        unset($catalogProductAttributeMviewData);
+
+        $catalogCategoryProductMviewData = array_values(array_filter(explode('|', $linesOutput[5])));
+        $this->assertEquals('catalog_product_category', trim($catalogCategoryProductMviewData[0]));
+        $this->assertEquals('disabled', trim($catalogCategoryProductMviewData[1]));
+        $this->assertEquals('idle', trim($catalogCategoryProductMviewData[2]));
+        $this->assertEquals('2017-01-01 11:11:11', trim($catalogCategoryProductMviewData[3]));
+        $this->assertEquals('100', trim($catalogCategoryProductMviewData[4]));
+        $this->assertEquals('100', trim($catalogCategoryProductMviewData[5]));
+        unset($catalogCategoryProductMviewData);
+    }
+
+    /**
+     * @param array $viewData
+     * @param array $changelogData
+     * @return Mview\View|Mview\View\Changelog|\PHPUnit_Framework_MockObject_MockObject
+     */
+    protected function generateMviewStub(array $viewData, array $changelogData)
+    {
+        /** @var Mview\View\Changelog|\PHPUnit_Framework_MockObject_MockObject $stub */
+        $changelog = $this->getMockBuilder(\Magento\Framework\Mview\View\Changelog::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $list = [];
+        if ($changelogData['version_id'] !== $viewData['version_id']) {
+            $list = range($viewData['version_id']+1, $changelogData['version_id']);
+        }
+
+        $changelog->expects($this->any())
+            ->method('getList')
+            ->willReturn($list);
+
+        $changelog->expects($this->any())
+            ->method('getVersion')
+            ->willReturn($changelogData['version_id']);
+
+        /** @var Mview\View|\PHPUnit_Framework_MockObject_MockObject $stub */
+        $stub = $this->getMockBuilder(\Magento\Framework\Mview\View::class)
+            ->disableOriginalConstructor()
+            ->setMethods(['getChangelog', 'getState'])
+            ->getMock();
+
+        $stub->expects($this->any())
+            ->method('getChangelog')
+            ->willReturn($changelog);
+
+        $stub->expects($this->any())
+            ->method('getState')
+            ->willReturnSelf();
+
+        $stub->setData($viewData);
+
+        return $stub;
+    }
+
+    public function testExecuteExceptionNoVerbosity()
+    {
+        /** @var \Magento\Framework\Mview\View|\PHPUnit_Framework_MockObject_MockObject $stub */
+        $stub = $this->getMockBuilder(Mview\View::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $stub->expects($this->any())
+            ->method('getChangelog')
+            ->willThrowException(new \Exception("Dummy test exception"));
+
+        $this->collection->addItem($stub);
+
+        $tester = new CommandTester($this->command);
+        $this->assertEquals(Cli::RETURN_FAILURE, $tester->execute([]));
+        $linesOutput = array_filter(explode(PHP_EOL, $tester->getDisplay()));
+        $this->assertEquals('Dummy test exception', $linesOutput[0]);
+    }
+}
diff --git a/app/code/Magento/Indexer/etc/di.xml b/app/code/Magento/Indexer/etc/di.xml
index 610f08fac3a..266cf72c50d 100644
--- a/app/code/Magento/Indexer/etc/di.xml
+++ b/app/code/Magento/Indexer/etc/di.xml
@@ -51,6 +51,7 @@
                 <item name="set-mode" xsi:type="object">Magento\Indexer\Console\Command\IndexerSetModeCommand</item>
                 <item name="show-mode" xsi:type="object">Magento\Indexer\Console\Command\IndexerShowModeCommand</item>
                 <item name="status" xsi:type="object">Magento\Indexer\Console\Command\IndexerStatusCommand</item>
+                <item name="status-mview" xsi:type="object">Magento\Indexer\Console\Command\IndexerStatusMviewCommand</item>
                 <item name="reset" xsi:type="object">Magento\Indexer\Console\Command\IndexerResetStateCommand</item>
             </argument>
         </arguments>
-- 
GitLab