FEATURE: Add comparison command

Compare current crawl against saved base.
This commit is contained in:
Daniel Siepmann 2018-09-03 10:37:23 +02:00
parent 5d2e8a934d
commit eb6ff42f0c
Signed by: Daniel Siepmann
GPG key ID: 33D6629915560EF4
5 changed files with 245 additions and 21 deletions

View file

@ -3,9 +3,11 @@
require __DIR__ . '/vendor/autoload.php'; require __DIR__ . '/vendor/autoload.php';
use Codappix\WebsiteComparison\Command\CompareCommand;
use Codappix\WebsiteComparison\Command\CreateBaseCommand; use Codappix\WebsiteComparison\Command\CreateBaseCommand;
use Symfony\Component\Console\Application; use Symfony\Component\Console\Application;
$application = new Application(); $application = new Application();
$application->add(new CreateBaseCommand()); $application->add(new CreateBaseCommand());
$application->add(new CompareCommand());
$application->run(); $application->run();

View file

@ -15,6 +15,8 @@
} }
}, },
"require": { "require": {
"php": "^7.2",
"php-imagick": "*",
"facebook/webdriver": "^1.6", "facebook/webdriver": "^1.6",
"symfony/console": "^4.1", "symfony/console": "^4.1",
"symfony/process": "^4.1", "symfony/process": "^4.1",

View file

@ -0,0 +1,119 @@
<?php
namespace Codappix\WebsiteComparison\Command;
/*
* Copyright (C) 2018 Daniel Siepmann <coding@daniel-siepmann.de>
*
* 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;
}
}

View file

@ -32,9 +32,6 @@ use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process; use Symfony\Component\Process\Process;
/**
*
*/
class CreateBaseCommand extends Command class CreateBaseCommand extends Command
{ {
/** /**
@ -54,7 +51,7 @@ class CreateBaseCommand extends Command
null, null,
InputOption::VALUE_OPTIONAL, InputOption::VALUE_OPTIONAL,
'Define the sub directory to use for storing created Screenshots.', 'Define the sub directory to use for storing created Screenshots.',
'output' 'output/base'
) )
->addOption( ->addOption(
'screenshotWidth', 'screenshotWidth',

View file

@ -59,6 +59,21 @@ class ScreenshotCrawlerService
*/ */
protected $screenshotWidth = 3840; protected $screenshotWidth = 3840;
/**
* @var string
*/
protected $compareDirectory = '';
/**
* @var string
*/
protected $diffResultDir = '';
/**
* @var bool
*/
protected $hasDifferences = false;
public function __construct( public function __construct(
OutputInterface $output, OutputInterface $output,
RemoteWebDriver $driver, RemoteWebDriver $driver,
@ -69,16 +84,13 @@ class ScreenshotCrawlerService
$this->output = $output; $this->output = $output;
$this->driver = $driver; $this->driver = $driver;
$this->baseUrl = rtrim($baseUrl, '/') . '/'; $this->baseUrl = rtrim($baseUrl, '/') . '/';
$this->screenshotDir = implode(DIRECTORY_SEPARATOR, [ $this->screenshotDir = $this->convertReltiveFolder($screenshotDir);
dirname(dirname(dirname(__FILE__))),
rtrim($screenshotDir, '/')
]) . DIRECTORY_SEPARATOR;
$this->screenshotWidth = $screenshotWidth; $this->screenshotWidth = $screenshotWidth;
} }
public function crawl() public function crawl()
{ {
$this->createScreenshotDirIfNecessary(); $this->createDir($this->screenshotDir);
$linkList = new UrlListDto(); $linkList = new UrlListDto();
$linkList->addUrl($this->baseUrl); $linkList->addUrl($this->baseUrl);
@ -88,7 +100,11 @@ class ScreenshotCrawlerService
$screenshotHeight = $this->driver->findElement(WebDriverBy::cssSelector('body')) $screenshotHeight = $this->driver->findElement(WebDriverBy::cssSelector('body'))
->getSize() ->getSize()
->getHeight(); ->getHeight();
$this->createScreenshot($this->driver->getCurrentURL(), $screenshotHeight); $createdScreenshot = $this->createScreenshot($this->driver->getCurrentURL(), $screenshotHeight);
if ($this->compareDirectory !== '') {
$this->compareScreenshot($createdScreenshot);
}
$linkList->markUrlAsFinished($url); $linkList->markUrlAsFinished($url);
array_map([$linkList, 'addUrl'], $this->fetchFurtherLinks( 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. * @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)) { if (!is_dir($dir)) {
mkdir($dir, 0777, true); mkdir($dir, 0777, true);
} }
if (!is_dir($this->screenshotDir)) { if (!is_dir($dir)) {
throw new \Exception('Could not create screenshot dir: "' . $dir . '".', 1535528875); 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); $screenshotTarget = $this->getScreenshotTarget($url);
$this->createScreenshotDirIfNecessary(dirname($screenshotTarget)); $this->createDir($this->screenshotDir . dirname($screenshotTarget));
$completeScreenshotTarget = $this->screenshotDir . $screenshotTarget;
$screenshotProcess = new Process([ $screenshotProcess = new Process([
'chromium-browser', 'chromium-browser',
'--headless', '--headless',
'--disable-gpu', '--disable-gpu',
'--window-size=' . $this->screenshotWidth . ',' . $height, '--window-size=' . $this->screenshotWidth . ',' . $height,
'--screenshot=' . $this->screenshotDir . $screenshotTarget, '--screenshot=' . $completeScreenshotTarget,
$url $url
]); ]);
// TODO: Check for success // TODO: Check for success
@ -134,10 +159,76 @@ class ScreenshotCrawlerService
if ($this->output->isVerbose()) { if ($this->output->isVerbose()) {
$this->output->writeln(sprintf( $this->output->writeln(sprintf(
'<info>Created screenshot "%s" for url "%s".</info>', '<info>Created screenshot "%s" for url "%s".</info>',
$this->screenshotDir . $screenshotTarget, $completeScreenshotTarget,
$url $url
)); ));
} }
return $completeScreenshotTarget;
}
protected function compareScreenshot(string $screenshot)
{
try {
if ($this->doScreenshotsDiffer($screenshot)) {
$this->hasDifferences = true;
$this->output->writeln(sprintf(
'<error>Screenshot "%s" is different then "%s". Diff was written to "%s".</error>',
$screenshot,
$this->getBaseScreenshot($screenshot),
$this->getDiffFileName($screenshot)
));
return;
}
} catch (\ImagickException $e) {
$this->output->writeln('<error>' . $e->getMessage() . '</error>');
return;
}
if ($this->output->isVerbose()) {
$this->output->writeln('<info>Screenshot is same.</info>');
}
}
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) protected function getScreenshotTarget(string $url)
@ -204,4 +295,17 @@ class ScreenshotCrawlerService
return in_array($uri->getHost(), $validHosts); 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);
}
} }