From eb6ff42f0c40be4e4ae90bdcaba375cf49777f4d Mon Sep 17 00:00:00 2001 From: Daniel Siepmann Date: Mon, 3 Sep 2018 10:37:23 +0200 Subject: [PATCH] FEATURE: Add comparison command Compare current crawl against saved base. --- comparison | 2 + composer.json | 2 + src/Command/CompareCommand.php | 119 +++++++++++++++++++ src/Command/CreateBaseCommand.php | 5 +- src/Service/ScreenshotCrawlerService.php | 138 ++++++++++++++++++++--- 5 files changed, 245 insertions(+), 21 deletions(-) create mode 100644 src/Command/CompareCommand.php diff --git a/comparison b/comparison index 9385d64..ee16f65 100755 --- a/comparison +++ b/comparison @@ -3,9 +3,11 @@ require __DIR__ . '/vendor/autoload.php'; +use Codappix\WebsiteComparison\Command\CompareCommand; use Codappix\WebsiteComparison\Command\CreateBaseCommand; use Symfony\Component\Console\Application; $application = new Application(); $application->add(new CreateBaseCommand()); +$application->add(new CompareCommand()); $application->run(); diff --git a/composer.json b/composer.json index c8bc4bb..950de22 100644 --- a/composer.json +++ b/composer.json @@ -15,6 +15,8 @@ } }, "require": { + "php": "^7.2", + "php-imagick": "*", "facebook/webdriver": "^1.6", "symfony/console": "^4.1", "symfony/process": "^4.1", diff --git a/src/Command/CompareCommand.php b/src/Command/CompareCommand.php new file mode 100644 index 0000000..a5a7699 --- /dev/null +++ b/src/Command/CompareCommand.php @@ -0,0 +1,119 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +use Codappix\WebsiteComparison\Service\ScreenshotCrawlerService; +use Facebook\WebDriver\Chrome\ChromeDriver; +use Facebook\WebDriver\Chrome\ChromeDriverService; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Process\Exception\ProcessFailedException; +use Symfony\Component\Process\Process; + +class CompareCommand extends Command +{ + /** + * @var Process + */ + protected $chromeProcess; + + protected function configure() + { + $this + ->setName('comparison:comparetobase') + ->setDescription('Compare curent state against saved base.') + ->setHelp('Crawls and screenshots the original website, as a base for future comparison.') + + ->addOption( + 'screenshotDir', + null, + InputOption::VALUE_OPTIONAL, + 'Define the sub directory containing original Screenshots for comparison.', + 'output/base' + ) + ->addOption( + 'compareDir', + null, + InputOption::VALUE_OPTIONAL, + 'Define the sub directory to use for storing created Screenshots.', + 'output/compare' + ) + ->addOption( + 'diffResultDir', + null, + InputOption::VALUE_OPTIONAL, + 'Define the sub directory to use for storing created diffs.', + 'output/diffResult' + ) + ->addOption( + 'screenshotWidth', + null, + InputOption::VALUE_OPTIONAL, + 'The width for screen resolution and screenshots.', + 3840 + ) + + ->addArgument( + 'baseUrl', + InputArgument::REQUIRED, + 'E.g. https://typo3.org/ the base url of the website to crawl.' + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $screenshotCrawler = new ScreenshotCrawlerService( + $output, + $this->getDriver(), + $input->getArgument('baseUrl'), + $input->getOption('compareDir'), + $input->getOption('screenshotWidth') + ); + $hasDifferences = $screenshotCrawler->compare( + $input->getOption('screenshotDir'), + $input->getOption('diffResultDir') + ); + + if ($hasDifferences) { + return 255; + } + } + + protected function getDriver(): ChromeDriver + { + $chromeDriverService = new ChromeDriverService( + '/usr/lib/chromium-browser/chromedriver', + 9515, + [ + '--port=9515', + '--headless', + ] + ); + $driver = ChromeDriver::start(null, $chromeDriverService); + + return $driver; + } +} diff --git a/src/Command/CreateBaseCommand.php b/src/Command/CreateBaseCommand.php index b56da72..9a054de 100644 --- a/src/Command/CreateBaseCommand.php +++ b/src/Command/CreateBaseCommand.php @@ -32,9 +32,6 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\Process; -/** - * - */ class CreateBaseCommand extends Command { /** @@ -54,7 +51,7 @@ class CreateBaseCommand extends Command null, InputOption::VALUE_OPTIONAL, 'Define the sub directory to use for storing created Screenshots.', - 'output' + 'output/base' ) ->addOption( 'screenshotWidth', diff --git a/src/Service/ScreenshotCrawlerService.php b/src/Service/ScreenshotCrawlerService.php index c72b931..3cd5c88 100644 --- a/src/Service/ScreenshotCrawlerService.php +++ b/src/Service/ScreenshotCrawlerService.php @@ -59,6 +59,21 @@ class ScreenshotCrawlerService */ protected $screenshotWidth = 3840; + /** + * @var string + */ + protected $compareDirectory = ''; + + /** + * @var string + */ + protected $diffResultDir = ''; + + /** + * @var bool + */ + protected $hasDifferences = false; + public function __construct( OutputInterface $output, RemoteWebDriver $driver, @@ -69,16 +84,13 @@ class ScreenshotCrawlerService $this->output = $output; $this->driver = $driver; $this->baseUrl = rtrim($baseUrl, '/') . '/'; - $this->screenshotDir = implode(DIRECTORY_SEPARATOR, [ - dirname(dirname(dirname(__FILE__))), - rtrim($screenshotDir, '/') - ]) . DIRECTORY_SEPARATOR; + $this->screenshotDir = $this->convertReltiveFolder($screenshotDir); $this->screenshotWidth = $screenshotWidth; } public function crawl() { - $this->createScreenshotDirIfNecessary(); + $this->createDir($this->screenshotDir); $linkList = new UrlListDto(); $linkList->addUrl($this->baseUrl); @@ -88,7 +100,11 @@ class ScreenshotCrawlerService $screenshotHeight = $this->driver->findElement(WebDriverBy::cssSelector('body')) ->getSize() ->getHeight(); - $this->createScreenshot($this->driver->getCurrentURL(), $screenshotHeight); + $createdScreenshot = $this->createScreenshot($this->driver->getCurrentURL(), $screenshotHeight); + + if ($this->compareDirectory !== '') { + $this->compareScreenshot($createdScreenshot); + } $linkList->markUrlAsFinished($url); array_map([$linkList, 'addUrl'], $this->fetchFurtherLinks( @@ -97,35 +113,44 @@ class ScreenshotCrawlerService } } + public function compare(string $compareDirectory, string $diffResultDir): bool + { + // TODO: Check for existence of directory + $this->compareDirectory = $this->convertReltiveFolder($compareDirectory); + $this->diffResultDir = $this->convertReltiveFolder($diffResultDir); + $this->crawl(); + + return $this->hasDifferences; + } + /** * @throws \Exception If folder could not be created. */ - protected function createScreenshotDirIfNecessary(string $subPath = '') + protected function createDir(string $dir) { - $dir = $this->screenshotDir; - if ($subPath !== '') { - $dir = $dir . DIRECTORY_SEPARATOR . trim($subPath, DIRECTORY_SEPARATOR); - } if (!is_dir($dir)) { mkdir($dir, 0777, true); } - if (!is_dir($this->screenshotDir)) { - throw new \Exception('Could not create screenshot dir: "' . $dir . '".', 1535528875); + if (!is_dir($dir)) { + throw new \Exception('Could not create directory: "' . $dir . '".', 1535528875); } } - protected function createScreenshot(string $url, int $height) + protected function createScreenshot(string $url, int $height): string { + // TODO: Include width in screenshot dir. + // This enables to compare different resolutions $screenshotTarget = $this->getScreenshotTarget($url); - $this->createScreenshotDirIfNecessary(dirname($screenshotTarget)); + $this->createDir($this->screenshotDir . dirname($screenshotTarget)); + $completeScreenshotTarget = $this->screenshotDir . $screenshotTarget; $screenshotProcess = new Process([ 'chromium-browser', '--headless', '--disable-gpu', '--window-size=' . $this->screenshotWidth . ',' . $height, - '--screenshot=' . $this->screenshotDir . $screenshotTarget, + '--screenshot=' . $completeScreenshotTarget, $url ]); // TODO: Check for success @@ -134,10 +159,76 @@ class ScreenshotCrawlerService if ($this->output->isVerbose()) { $this->output->writeln(sprintf( 'Created screenshot "%s" for url "%s".', - $this->screenshotDir . $screenshotTarget, + $completeScreenshotTarget, $url )); } + + return $completeScreenshotTarget; + } + + protected function compareScreenshot(string $screenshot) + { + try { + if ($this->doScreenshotsDiffer($screenshot)) { + $this->hasDifferences = true; + $this->output->writeln(sprintf( + 'Screenshot "%s" is different then "%s". Diff was written to "%s".', + $screenshot, + $this->getBaseScreenshot($screenshot), + $this->getDiffFileName($screenshot) + )); + return; + } + } catch (\ImagickException $e) { + $this->output->writeln('' . $e->getMessage() . ''); + return; + } + + if ($this->output->isVerbose()) { + $this->output->writeln('Screenshot is same.'); + } + } + + protected function doScreenshotsDiffer(string $screenshot): bool + { + $actualScreenshot = new \Imagick($screenshot); + $actualGeometry = $actualScreenshot->getImageGeometry(); + $compareScreenshot = new \Imagick($this->getBaseScreenshot($screenshot)); + $compareGeometry = $compareScreenshot->getImageGeometry(); + + if ($actualGeometry !== $compareGeometry) { + throw new \ImagickException(sprintf( + "Screenshots don't have an equal geometry. Should be %sx%s but is %sx%s", + $compareGeometry['width'], + $compareGeometry['height'], + $actualGeometry['width'], + $actualGeometry['height'] + )); + } + + $result = $actualScreenshot->compareImages($compareScreenshot, \Imagick::METRIC_ROOTMEANSQUAREDERROR); + if ($result[1] > 0) { + /** @var \Imagick $diffScreenshot */ + $diffScreenshot = $result[0]; + $diffScreenshot->setImageFormat('png'); + $fileName = $this->getDiffFileName($screenshot); + $this->createDir(dirname($fileName)); + file_put_contents($fileName, $diffScreenshot); + + return true; + } + + return false; + } + + protected function getBaseScreenshot(string $compareScreenshot): string + { + return str_replace( + $this->screenshotDir, + $this->compareDirectory, + $compareScreenshot + ); } protected function getScreenshotTarget(string $url) @@ -204,4 +295,17 @@ class ScreenshotCrawlerService return in_array($uri->getHost(), $validHosts); } + + protected function convertReltiveFolder(string $folder): string + { + return implode(DIRECTORY_SEPARATOR, [ + dirname(dirname(dirname(__FILE__))), + rtrim($folder, '/'), + ]) . DIRECTORY_SEPARATOR; + } + + protected function getDiffFileName(string $screenshot): string + { + return str_replace($this->screenshotDir, $this->diffResultDir, $screenshot); + } }