FEATURE: Add comparison command
Compare current crawl against saved base.
This commit is contained in:
parent
5d2e8a934d
commit
eb6ff42f0c
5 changed files with 245 additions and 21 deletions
|
@ -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();
|
||||||
|
|
|
@ -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",
|
||||||
|
|
119
src/Command/CompareCommand.php
Normal file
119
src/Command/CompareCommand.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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',
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue