24#include <QCryptographicHash>
31 : mBasePath( QStringLiteral( TEST_DATA_DIR ) + QStringLiteral(
"/control_images/" ) )
33 if ( qgetenv(
"QGIS_CONTINUOUS_INTEGRATION_RUN" ) == QStringLiteral(
"true" ) )
39 if ( qgetenv(
"QGIS_CONTINUOUS_INTEGRATION_RUN" ) == QStringLiteral(
"true" ) )
40 return QDir( QDir(
"/root/QGIS" ).filePath( QStringLiteral(
"qgis_test_report" ) ) );
42 return QDir( QDir::temp().filePath( QStringLiteral(
"qgis_test_report" ) ) );
52 return mBasePath + ( mBasePath.endsWith(
'/' ) ? QString() : QStringLiteral(
"/" ) ) + mControlPathPrefix;
62 return ( ignoreSuccess && mResult ) ? QString() :
mReport;
73 if ( !name.isEmpty() )
74 mControlPathSuffix = name +
'/';
76 mControlPathSuffix.clear();
82 myImage.load( imageFile );
83 QByteArray myByteArray;
84 QBuffer myBuffer( &myByteArray );
85 myImage.save( &myBuffer,
"PNG" );
86 const QString myImageString = QString::fromUtf8( myByteArray.toBase64().data() );
87 QCryptographicHash myHash( QCryptographicHash::Md5 );
88 myHash.addData( myImageString.toUtf8() );
89 return myHash.result().toHex().constData();
94 mMapSettings = mapSettings;
100 uchar pixDataRGB[] = { 255, 255, 255, 255,
106 const QImage img( pixDataRGB, 2, 2, 8, QImage::Format_ARGB32 );
107 const QPixmap pix = QPixmap::fromImage( img.scaled( 20, 20 ) );
111 brush.setTexture( pix );
113 p.setRenderHint( QPainter::Antialiasing,
false );
114 p.fillRect( QRect( 0, 0, image->width(), image->height() ), brush );
121 const QDir myDirectory = QDir( myControlImageDir );
123 const QString myFilename = QStringLiteral(
"*" );
124 myList = myDirectory.entryList( QStringList( myFilename ),
125 QDir::Files | QDir::NoSymLinks );
130 const QString myImageHash =
imageToHash( diffImageFile );
133 for (
int i = 0; i < myList.size(); ++i )
135 const QString myFile = myList.at( i );
136 mReport +=
"<tr><td colspan=3>"
137 "Checking if " + myFile +
" is a known anomaly.";
138 mReport += QLatin1String(
"</td></tr>" );
140 QString myHashMessage = QStringLiteral(
141 "Checking if anomaly %1 (hash %2)<br>" )
144 myHashMessage += QStringLiteral(
" matches %1 (hash %2)" )
150 mReport +=
"<tr><td colspan=3>" + myHashMessage +
"</td></tr>";
151 if ( myImageHash == myAnomalyHash )
153 mReport +=
"<tr><td colspan=3>"
154 "Anomaly found! " + myFile;
155 mReport += QLatin1String(
"</td></tr>" );
159 mReport +=
"<tr><td colspan=3>"
160 "No anomaly found! ";
161 mReport += QLatin1String(
"</td></tr>" );
170 if ( mBufferDashMessages )
171 mDashMessages << dashMessage;
181#if DUMP_BASE64_IMAGES
182void QgsRenderChecker::dumpRenderedImageAsBase64()
185 if ( !fileSource.open( QIODevice::ReadOnly ) )
190 const QByteArray blob = fileSource.readAll();
191 const QByteArray encoded = blob.toBase64();
193 qDebug() <<
"################################################################";
195 qDebug() <<
"################################################################";
196 qDebug() <<
"End dump";
200void QgsRenderChecker::performPostTestActions( Flags flags )
202 if ( mResult || mExpectFail )
205#if DUMP_BASE64_IMAGES
207 dumpRenderedImageAsBase64();
213 if ( !reportDir.exists() )
215 if ( !QDir().mkpath( reportDir.path() ) )
217 qDebug() <<
"!!!!! cannot create " << reportDir.path();
224 const QString destPath = reportDir.filePath( fi.fileName() );
225 if ( QFile::exists( destPath ) )
226 QFile::remove( destPath );
234 QFileInfo fi( mDiffImageFile );
235 const QString destPath = reportDir.filePath( fi.fileName() );
236 if ( QFile::exists( destPath ) )
237 QFile::remove( destPath );
238 QFile::copy( mDiffImageFile, destPath );
244 unsigned int mismatchCount,
245 QgsRenderChecker::Flags flags )
250 qDebug(
"QgsRenderChecker::runTest failed - Expected Image File not set." );
252 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
253 "<tr><td>Nothing rendered</td>\n<td>Failed because Expected "
254 "Image File not set.</td></tr></table>\n";
255 performPostTestActions( flags );
262 if ( myExpectedImage.isNull() )
264 qDebug() <<
"QgsRenderChecker::runTest failed - Could not load expected image from " <<
mExpectedImageFile;
266 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
267 "<tr><td>Nothing rendered</td>\n<td>Failed because Expected "
268 "Image File could not be loaded.</td></tr></table>\n";
269 performPostTestActions( flags );
272 mMatchTarget = myExpectedImage.width() * myExpectedImage.height();
280 QElapsedTimer myTime;
298 myImage.setDotsPerMeterX( myExpectedImage.dotsPerMeterX() );
299 myImage.setDotsPerMeterY( myExpectedImage.dotsPerMeterY() );
302 qDebug() <<
"QgsRenderChecker::runTest failed - Could not save rendered image to " <<
mRenderedImageFile;
304 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
305 "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered "
306 "Image File could not be saved.</td></tr></table>\n";
307 performPostTestActions( flags );
313 QFile wldFile( QDir::tempPath() +
'/' + testName +
"_result.wld" );
314 if ( wldFile.open( QIODevice::WriteOnly | QIODevice::Truncate ) )
318 QTextStream stream( &wldFile );
319 stream << QStringLiteral(
"%1\r\n0 \r\n0 \r\n%2\r\n%3\r\n%4\r\n" )
331 unsigned int mismatchCount,
332 const QString &renderedImageFile,
333 QgsRenderChecker::Flags flags )
338 qDebug(
"QgsRenderChecker::runTest failed - Expected Image (control) File not set." );
340 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
341 "<tr><td>Nothing rendered</td>\n<td>Failed because Expected "
342 "Image File not set.</td></tr></table>\n";
343 performPostTestActions( flags );
350bool QgsRenderChecker::compareImages(
const QString &testName,
const QString &referenceImageFile,
const QString &renderedImageFile,
unsigned int mismatchCount, QgsRenderChecker::Flags flags )
353 if ( ! renderedImageFile.isEmpty() )
363 qDebug(
"QgsRenderChecker::runTest failed - Rendered Image File not set." );
365 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
366 "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered "
367 "Image File not set.</td></tr></table>\n";
368 performPostTestActions( flags );
375 QImage expectedImage( referenceImageFile );
376 if ( expectedImage.isNull() )
378 qDebug() <<
"QgsRenderChecker::runTest failed - Could not load control image from " << referenceImageFile;
380 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
381 "<tr><td>Nothing rendered</td>\n<td>Failed because control "
382 "image file could not be loaded.</td></tr></table>\n";
383 performPostTestActions( flags );
388 if ( myResultImage.isNull() )
390 qDebug() <<
"QgsRenderChecker::runTest failed - Could not load rendered image from " <<
mRenderedImageFile;
392 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
393 "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered "
394 "Image File could not be loaded.</td></tr></table>\n";
395 performPostTestActions( flags );
398 QImage myDifferenceImage( expectedImage.width(),
399 expectedImage.height(),
400 QImage::Format_RGB32 );
401 mDiffImageFile = QDir::tempPath() +
'/' + testName +
"_result_diff.png";
402 myDifferenceImage.fill( qRgb( 152, 219, 249 ) );
405 QString maskImagePath = referenceImageFile;
406 maskImagePath.chop( 4 );
407 maskImagePath += QLatin1String(
"_mask.png" );
408 const QImage maskImage( maskImagePath );
409 const bool hasMask = !maskImage.isNull();
414 mMatchTarget = expectedImage.width() * expectedImage.height();
415 const unsigned int myPixelCount = myResultImage.width() * myResultImage.height();
419 mReport = QStringLiteral(
"<script src=\"file://%1/../renderchecker.js\"></script>\n" ).arg( TEST_DATA_DIR );
420 mReport += QLatin1String(
"<table>" );
421 mReport += QLatin1String(
"<tr><td colspan=2>" );
422 mReport += QString(
"<tr><td colspan=2>"
423 "Test image and result image for %1<br>"
424 "Expected size: %2 w x %3 h (%4 pixels)<br>"
425 "Actual size: %5 w x %6 h (%7 pixels)"
428 .arg( expectedImage.width() ).arg( expectedImage.height() ).arg(
mMatchTarget )
429 .arg( myResultImage.width() ).arg( myResultImage.height() ).arg( myPixelCount );
430 mReport += QString(
"<tr><td colspan=2>\n"
431 "Expected Duration : <= %1 (0 indicates not specified)<br>"
432 "Actual Duration : %2 ms<br></td></tr>" )
433 .arg( mElapsedTimeTarget )
439 if ( ! expectedImage.isNull() )
441 imgWidth = std::min( expectedImage.width(), imgWidth );
442 imgHeight = expectedImage.height() * imgWidth / expectedImage.width();
446 const QString diffImageFileName = QFileInfo( mDiffImageFile ).fileName();
447 const QString myImagesString = QString(
449 "<td colspan=2>Compare expected and actual result</td>"
450 "<td>Difference (all blue is good, any red is bad)</td>"
452 "<td colspan=2 id=\"td-%1-%7\"></td>\n"
453 "<td align=center><img width=%5 height=%6 src=\"%2\"></td>\n"
456 "<script>\naddComparison(\"td-%1-%7\",\"%3\",\"file://%4\",%5,%6);\n</script>\n"
457 "<p>If the new image looks good, create or update a test mask with<br>"
458 "<code>scripts/generate_test_mask_image.py \"%8\" \"%9\"</code>" )
461 renderedImageFileName,
463 .arg( imgWidth ).arg( imgHeight )
464 .arg( QUuid::createUuid().toString().mid( 1, 6 ),
470 if ( !mControlPathPrefix.isNull() )
472 prefix = QStringLiteral(
" (prefix %1)" ).arg( mControlPathPrefix );
479 if ( expectedImage.width() != myResultImage.width() || expectedImage.height() != myResultImage.height() )
481 qDebug(
"Expected size: %dw x %dh", expectedImage.width(), expectedImage.height() );
482 qDebug(
"Actual size: %dw x %dh", myResultImage.width(), myResultImage.height() );
484 qDebug(
"Mask size: %dw x %dh", maskImage.width(), maskImage.height() );
489 qDebug(
"Test image and result image for %s are different dimensions", testName.toLocal8Bit().constData() );
491 if ( std::abs( expectedImage.width() - myResultImage.width() ) > mMaxSizeDifferenceX ||
492 std::abs( expectedImage.height() - myResultImage.height() ) > mMaxSizeDifferenceY )
497 mReport += QLatin1String(
"<tr><td colspan=3>" );
498 mReport +=
"<font color=red>Expected image and result image for " + testName +
" are different dimensions - FAILING!</font>";
499 mReport += QLatin1String(
"</td></tr>" );
501 const QString diffSizeImagesString = QString(
503 "<td colspan=3>Compare expected and actual result</td>"
505 "<td align=center><img src=\"%1\"></td>\n"
506 "<td align=center><img width=%3 height=%4 src=\"%2\"></td>\n"
510 renderedImageFileName,
512 .arg( imgWidth ).arg( imgHeight );
514 mReport += diffSizeImagesString;
515 performPostTestActions( flags );
520 mReport += QLatin1String(
"<tr><td colspan=3>" );
521 mReport +=
"Expected image and result image for " + testName +
" are different dimensions, but within tolerance";
522 mReport += QLatin1String(
"</td></tr>" );
526 if ( expectedImage.format() == QImage::Format_Indexed8 )
528 if ( myResultImage.format() != QImage::Format_Indexed8 )
533 qDebug() <<
"Expected image and result image for " << testName <<
" have different formats (8bit format is expected) - FAILING!";
535 mReport += QLatin1String(
"<tr><td colspan=3>" );
536 mReport +=
"<font color=red>Expected image and result image for " + testName +
" have different formats (8bit format is expected) - FAILING!</font>";
537 mReport += QLatin1String(
"</td></tr>" );
539 performPostTestActions( flags );
546 myResultImage = myResultImage.convertToFormat( QImage::Format_ARGB32 );
547 expectedImage = expectedImage.convertToFormat( QImage::Format_ARGB32 );
556 const int maxHeight = std::min( expectedImage.height(), myResultImage.height() );
557 const int maxWidth = std::min( expectedImage.width(), myResultImage.width() );
560 const int colorTolerance =
static_cast< int >( mColorTolerance );
561 for (
int y = 0; y < maxHeight; ++y )
563 const QRgb *expectedScanline =
reinterpret_cast< const QRgb *
>( expectedImage.constScanLine( y ) );
564 const QRgb *resultScanline =
reinterpret_cast< const QRgb *
>( myResultImage.constScanLine( y ) );
565 const QRgb *maskScanline = ( hasMask && maskImage.height() > y ) ?
reinterpret_cast< const QRgb *
>( maskImage.constScanLine( y ) ) :
nullptr;
566 QRgb *diffScanline =
reinterpret_cast< QRgb *
>( myDifferenceImage.scanLine( y ) );
568 for (
int x = 0; x < maxWidth; ++x )
570 const int maskTolerance = ( maskScanline && maskImage.width() > x ) ? qRed( maskScanline[ x ] ) : 0;
571 const int pixelTolerance = std::max( colorTolerance, maskTolerance );
572 if ( pixelTolerance == 255 )
578 const QRgb myExpectedPixel = expectedScanline[x];
579 const QRgb myActualPixel = resultScanline[x];
580 if ( pixelTolerance == 0 )
582 if ( myExpectedPixel != myActualPixel )
585 diffScanline[ x ] = qRgb( 255, 0, 0 );
590 if ( std::abs( qRed( myExpectedPixel ) - qRed( myActualPixel ) ) > pixelTolerance ||
591 std::abs( qGreen( myExpectedPixel ) - qGreen( myActualPixel ) ) > pixelTolerance ||
592 std::abs( qBlue( myExpectedPixel ) - qBlue( myActualPixel ) ) > pixelTolerance ||
593 std::abs( qAlpha( myExpectedPixel ) - qAlpha( myActualPixel ) ) > pixelTolerance )
596 diffScanline[ x ] = qRgb( 255, 0, 0 );
615 myDifferenceImage.save( mDiffImageFile );
622 mReport += QStringLiteral(
"<tr><td colspan=3>%1/%2 pixels mismatched (allowed threshold: %3, allowed color component tolerance: %4)</td></tr>" )
628 if ( mMismatchCount > 0 )
635 mReport += QLatin1String(
"<tr><td colspan = 3>\n" );
636 mReport +=
"Test image and result image for " + testName +
" are matched<br>";
637 mReport += QLatin1String(
"</td></tr>" );
638 if ( mElapsedTimeTarget != 0 && mElapsedTimeTarget <
mElapsedTime )
641 qDebug(
"Test failed because render step took too long" );
642 mReport += QLatin1String(
"<tr><td colspan = 3>\n" );
643 mReport += QLatin1String(
"<font color=red>Test failed because render step took too long</font>" );
644 mReport += QLatin1String(
"</td></tr>" );
646 performPostTestActions( flags );
653 performPostTestActions( flags );
658 mReport += QLatin1String(
"<tr><td colspan=3></td></tr>" );
659 emitDashMessage( QStringLiteral(
"Image mismatch" ),
QgsDartMeasurement::Text,
"Difference image did not match any known anomaly or mask."
660 " If you feel the difference image should be considered an anomaly "
661 "you can do something like this\n"
663 "/\nIf it should be included in the mask run\n"
664 "scripts/generate_test_mask_image.py '" + referenceImageFile +
"' '" +
mRenderedImageFile +
"'\n" );
666 mReport += QLatin1String(
"<tr><td colspan = 3>\n" );
667 mReport +=
"<font color=red>Test image and result image for " + testName +
" are mismatched</font><br>";
668 mReport += QLatin1String(
"</td></tr>" );
671 performPostTestActions( flags );
@ Antialiasing
Enable anti-aliasing for map rendering.
void start()
Start the rendering job and immediately return.
Job implementation that renders everything sequentially in one thread.
QImage renderedImage() override
Gets a preview/resulting image.
void waitForFinished() override
Block until the job has finished.
The QgsMapSettings class contains configuration for rendering of the map.
double mapUnitsPerPixel() const
Returns the distance in geographical coordinates that equals to one pixel in the map.
float devicePixelRatio() const
Returns the device pixel ratio.
QgsRectangle extent() const
Returns geographical coordinates of the rectangle that should be rendered.
void setOutputSize(QSize size)
Sets the size of the resulting map image, in pixels.
void setBackgroundColor(const QColor &color)
Sets the background color of the map.
void setFlag(Qgis::MapSettingsFlag flag, bool on=true)
Enable or disable a particular flag (other flags are not affected)
A rectangle specified with double values.
double yMaximum() const SIP_HOLDGIL
Returns the y maximum value (top side of rectangle).
double xMinimum() const SIP_HOLDGIL
Returns the x minimum value (left side of rectangle).
Q_DECL_DEPRECATED bool isKnownAnomaly(const QString &diffImageFile)
Gets a list of all the anomalies.
void setControlName(const QString &name)
Sets the base directory name for the control image (with control image path suffixed).
static QDir testReportDir()
Returns the directory to use for generating a test report.
static bool shouldGenerateReport()
Returns true if a test report should be generated given the current environment.
unsigned int mMatchTarget
void setControlImagePath(const QString &path)
Sets the base path containing the reference images.
void setMapSettings(const QgsMapSettings &mapSettings)
void setControlPathSuffix(const QString &name)
bool runTest(const QString &testName, unsigned int mismatchCount=0, QgsRenderChecker::Flags flags=QgsRenderChecker::Flags())
Render checker flags.
QString imageToHash(const QString &imageFile)
Gets an md5 hash that uniquely identifies an image.
@ AvoidExportingRenderedImage
Avoids exporting rendered images to reports.
QString mRenderedImageFile
QString report(bool ignoreSuccess=true) const
Returns the HTML report describing the results of the test run.
static void drawBackground(QImage *image)
Draws a checkboard pattern for image backgrounds, so that opacity is visible without requiring a tran...
bool compareImages(const QString &testName, unsigned int mismatchCount=0, const QString &renderedImageFile=QString(), QgsRenderChecker::Flags flags=QgsRenderChecker::Flags())
Test using two arbitrary images (map renderer will not be used)
QgsRenderChecker()
Constructor for QgsRenderChecker.
QString mExpectedImageFile
QString controlImagePath() const
Returns the base path containing the reference images.
unsigned int mismatchCount() const
Returns the number of pixels which did not match the control image.
QString qgsDoubleToString(double a, int precision=17)
Returns a string representation of a double.