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
182 void 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";
200 void 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() );
232 QFileInfo fi( mDiffImageFile );
233 const QString destPath = reportDir.filePath( fi.fileName() );
234 QFile::copy( mDiffImageFile, destPath );
240 unsigned int mismatchCount,
241 QgsRenderChecker::Flags flags )
246 qDebug(
"QgsRenderChecker::runTest failed - Expected Image File not set." );
248 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
249 "<tr><td>Nothing rendered</td>\n<td>Failed because Expected "
250 "Image File not set.</td></tr></table>\n";
251 performPostTestActions( flags );
258 if ( myExpectedImage.isNull() )
260 qDebug() <<
"QgsRenderChecker::runTest failed - Could not load expected image from " <<
mExpectedImageFile;
262 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
263 "<tr><td>Nothing rendered</td>\n<td>Failed because Expected "
264 "Image File could not be loaded.</td></tr></table>\n";
265 performPostTestActions( flags );
268 mMatchTarget = myExpectedImage.width() * myExpectedImage.height();
276 QElapsedTimer myTime;
294 myImage.setDotsPerMeterX( myExpectedImage.dotsPerMeterX() );
295 myImage.setDotsPerMeterY( myExpectedImage.dotsPerMeterY() );
298 qDebug() <<
"QgsRenderChecker::runTest failed - Could not save rendered image to " <<
mRenderedImageFile;
300 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
301 "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered "
302 "Image File could not be saved.</td></tr></table>\n";
303 performPostTestActions( flags );
309 QFile wldFile( QDir::tempPath() +
'/' + testName +
"_result.wld" );
310 if ( wldFile.open( QIODevice::WriteOnly | QIODevice::Truncate ) )
314 QTextStream stream( &wldFile );
315 stream << QStringLiteral(
"%1\r\n0 \r\n0 \r\n%2\r\n%3\r\n%4\r\n" )
327 unsigned int mismatchCount,
328 const QString &renderedImageFile,
329 QgsRenderChecker::Flags flags )
334 qDebug(
"QgsRenderChecker::runTest failed - Expected Image (control) File not set." );
336 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
337 "<tr><td>Nothing rendered</td>\n<td>Failed because Expected "
338 "Image File not set.</td></tr></table>\n";
339 performPostTestActions( flags );
346 bool QgsRenderChecker::compareImages(
const QString &testName,
const QString &referenceImageFile,
const QString &renderedImageFile,
unsigned int mismatchCount, QgsRenderChecker::Flags flags )
349 if ( ! renderedImageFile.isEmpty() )
359 qDebug(
"QgsRenderChecker::runTest failed - Rendered Image File not set." );
361 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
362 "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered "
363 "Image File not set.</td></tr></table>\n";
364 performPostTestActions( flags );
371 QImage myExpectedImage( referenceImageFile );
373 if ( myResultImage.isNull() )
375 qDebug() <<
"QgsRenderChecker::runTest failed - Could not load rendered image from " <<
mRenderedImageFile;
377 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
378 "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered "
379 "Image File could not be loaded.</td></tr></table>\n";
380 performPostTestActions( flags );
383 QImage myDifferenceImage( myExpectedImage.width(),
384 myExpectedImage.height(),
385 QImage::Format_RGB32 );
386 mDiffImageFile = QDir::tempPath() +
'/' + testName +
"_result_diff.png";
387 myDifferenceImage.fill( qRgb( 152, 219, 249 ) );
390 QString maskImagePath = referenceImageFile;
391 maskImagePath.chop( 4 );
392 maskImagePath += QLatin1String(
"_mask.png" );
393 const QImage maskImage( maskImagePath );
394 const bool hasMask = !maskImage.isNull();
399 mMatchTarget = myExpectedImage.width() * myExpectedImage.height();
400 const unsigned int myPixelCount = myResultImage.width() * myResultImage.height();
404 mReport = QStringLiteral(
"<script src=\"file://%1/../renderchecker.js\"></script>\n" ).arg( TEST_DATA_DIR );
405 mReport += QLatin1String(
"<table>" );
406 mReport += QLatin1String(
"<tr><td colspan=2>" );
407 mReport += QString(
"<tr><td colspan=2>"
408 "Test image and result image for %1<br>"
409 "Expected size: %2 w x %3 h (%4 pixels)<br>"
410 "Actual size: %5 w x %6 h (%7 pixels)"
413 .arg( myExpectedImage.width() ).arg( myExpectedImage.height() ).arg(
mMatchTarget )
414 .arg( myResultImage.width() ).arg( myResultImage.height() ).arg( myPixelCount );
415 mReport += QString(
"<tr><td colspan=2>\n"
416 "Expected Duration : <= %1 (0 indicates not specified)<br>"
417 "Actual Duration : %2 ms<br></td></tr>" )
418 .arg( mElapsedTimeTarget )
424 if ( ! myExpectedImage.isNull() )
426 imgWidth = std::min( myExpectedImage.width(), imgWidth );
427 imgHeight = myExpectedImage.height() * imgWidth / myExpectedImage.width();
431 const QString diffImageFileName = QFileInfo( mDiffImageFile ).fileName();
432 const QString myImagesString = QString(
434 "<td colspan=2>Compare actual and expected result</td>"
435 "<td>Difference (all blue is good, any red is bad)</td>"
437 "<td colspan=2 id=\"td-%1-%7\"></td>\n"
438 "<td align=center><img width=%5 height=%6 src=\"%2\"></td>\n"
441 "<script>\naddComparison(\"td-%1-%7\",\"%3\",\"file://%4\",%5,%6);\n</script>\n"
442 "<p>If the new image looks good, create or update a test mask with<br>"
443 "<code>scripts/generate_test_mask_image.py \"%8\" \"%9\"</code>" )
446 renderedImageFileName,
448 .arg( imgWidth ).arg( imgHeight )
449 .arg( QUuid::createUuid().toString().mid( 1, 6 ),
455 if ( !mControlPathPrefix.isNull() )
457 prefix = QStringLiteral(
" (prefix %1)" ).arg( mControlPathPrefix );
469 if ( myExpectedImage.width() != myResultImage.width() || myExpectedImage.height() != myResultImage.height() )
471 qDebug(
"Expected size: %dw x %dh", myExpectedImage.width(), myExpectedImage.height() );
472 qDebug(
"Actual size: %dw x %dh", myResultImage.width(), myResultImage.height() );
474 qDebug(
"Mask size: %dw x %dh", maskImage.width(), maskImage.height() );
479 qDebug(
"Test image and result image for %s are different dimensions", testName.toLocal8Bit().constData() );
481 if ( std::abs( myExpectedImage.width() - myResultImage.width() ) > mMaxSizeDifferenceX ||
482 std::abs( myExpectedImage.height() - myResultImage.height() ) > mMaxSizeDifferenceY )
484 mReport += QLatin1String(
"<tr><td colspan=3>" );
485 mReport +=
"<font color=red>Expected image and result image for " + testName +
" are different dimensions - FAILING!</font>";
486 mReport += QLatin1String(
"</td></tr>" );
488 performPostTestActions( flags );
493 mReport += QLatin1String(
"<tr><td colspan=3>" );
494 mReport +=
"Expected image and result image for " + testName +
" are different dimensions, but within tolerance";
495 mReport += QLatin1String(
"</td></tr>" );
499 if ( myExpectedImage.format() == QImage::Format_Indexed8 )
501 if ( myResultImage.format() != QImage::Format_Indexed8 )
503 qDebug() <<
"Expected image and result image for " << testName <<
" have different formats (8bit format is expected) - FAILING!";
505 mReport += QLatin1String(
"<tr><td colspan=3>" );
506 mReport +=
"<font color=red>Expected image and result image for " + testName +
" have different formats (8bit format is expected) - FAILING!</font>";
507 mReport += QLatin1String(
"</td></tr>" );
509 performPostTestActions( flags );
516 myResultImage = myResultImage.convertToFormat( QImage::Format_ARGB32 );
517 myExpectedImage = myExpectedImage.convertToFormat( QImage::Format_ARGB32 );
526 const int maxHeight = std::min( myExpectedImage.height(), myResultImage.height() );
527 const int maxWidth = std::min( myExpectedImage.width(), myResultImage.width() );
530 const int colorTolerance =
static_cast< int >( mColorTolerance );
531 for (
int y = 0; y < maxHeight; ++y )
533 const QRgb *expectedScanline =
reinterpret_cast< const QRgb *
>( myExpectedImage.constScanLine( y ) );
534 const QRgb *resultScanline =
reinterpret_cast< const QRgb *
>( myResultImage.constScanLine( y ) );
535 const QRgb *maskScanline = ( hasMask && maskImage.height() > y ) ?
reinterpret_cast< const QRgb *
>( maskImage.constScanLine( y ) ) :
nullptr;
536 QRgb *diffScanline =
reinterpret_cast< QRgb *
>( myDifferenceImage.scanLine( y ) );
538 for (
int x = 0; x < maxWidth; ++x )
540 const int maskTolerance = ( maskScanline && maskImage.width() > x ) ? qRed( maskScanline[ x ] ) : 0;
541 const int pixelTolerance = std::max( colorTolerance, maskTolerance );
542 if ( pixelTolerance == 255 )
548 const QRgb myExpectedPixel = expectedScanline[x];
549 const QRgb myActualPixel = resultScanline[x];
550 if ( pixelTolerance == 0 )
552 if ( myExpectedPixel != myActualPixel )
555 diffScanline[ x ] = qRgb( 255, 0, 0 );
560 if ( std::abs( qRed( myExpectedPixel ) - qRed( myActualPixel ) ) > pixelTolerance ||
561 std::abs( qGreen( myExpectedPixel ) - qGreen( myActualPixel ) ) > pixelTolerance ||
562 std::abs( qBlue( myExpectedPixel ) - qBlue( myActualPixel ) ) > pixelTolerance ||
563 std::abs( qAlpha( myExpectedPixel ) - qAlpha( myActualPixel ) ) > pixelTolerance )
566 diffScanline[ x ] = qRgb( 255, 0, 0 );
574 myDifferenceImage.save( mDiffImageFile );
588 mReport += QStringLiteral(
"<tr><td colspan=3>%1/%2 pixels mismatched (allowed threshold: %3, allowed color component tolerance: %4)</td></tr>" )
598 mReport += QLatin1String(
"<tr><td colspan = 3>\n" );
599 mReport +=
"Test image and result image for " + testName +
" are matched<br>";
600 mReport += QLatin1String(
"</td></tr>" );
601 if ( mElapsedTimeTarget != 0 && mElapsedTimeTarget <
mElapsedTime )
604 qDebug(
"Test failed because render step took too long" );
605 mReport += QLatin1String(
"<tr><td colspan = 3>\n" );
606 mReport += QLatin1String(
"<font color=red>Test failed because render step took too long</font>" );
607 mReport += QLatin1String(
"</td></tr>" );
609 performPostTestActions( flags );
616 performPostTestActions( flags );
622 if ( myAnomalyMatchFlag )
624 mReport +=
"<tr><td colspan=3>"
625 "Difference image matched a known anomaly - passing test! "
628 performPostTestActions( flags );
632 mReport += QLatin1String(
"<tr><td colspan=3></td></tr>" );
633 emitDashMessage( QStringLiteral(
"Image mismatch" ),
QgsDartMeasurement::Text,
"Difference image did not match any known anomaly or mask."
634 " If you feel the difference image should be considered an anomaly "
635 "you can do something like this\n"
637 "/\nIf it should be included in the mask run\n"
638 "scripts/generate_test_mask_image.py '" + referenceImageFile +
"' '" +
mRenderedImageFile +
"'\n" );
640 mReport += QLatin1String(
"<tr><td colspan = 3>\n" );
641 mReport +=
"<font color=red>Test image and result image for " + testName +
" are mismatched</font><br>";
642 mReport += QLatin1String(
"</td></tr>" );
645 performPostTestActions( flags );