26using namespace Qt::StringLiterals;
30 if ( qgetenv(
"QGIS_CONTINUOUS_INTEGRATION_RUN" ) == u
"true"_s )
47 mSourceFunction = function;
53 mControlPathPrefix = prefix;
58 mMapSettings = mapSettings;
65 mReportHeader =
"<h2>" + testName +
"</h2>\n";
66 mMarkdownReportHeader = u
"### %1\n\n"_s.arg( testName );
69 if ( !QFile::exists( baseDir ) )
71 qDebug() <<
"Control image path " << baseDir <<
" does not exist!";
75 QStringList subDirs = QDir( baseDir ).entryList( QDir::Dirs | QDir::NoDotAndDotDot );
77 if ( subDirs.isEmpty() )
82 QVector<QgsDartMeasurement> dartMeasurements;
85 QString diffImageFile;
87 QMap< QString, int > variantMismatchCount;
88 QMap< QString, int > variantSize;
90 for (
const QString &suffix : std::as_const( subDirs ) )
103 if ( !mRenderedImage.isNull() )
118 mReport += checker.
report(
false );
119 if ( subDirs.count() > 1 )
124 if ( !mResult && diffImageFile.isEmpty() )
126 diffImageFile = checker.mDiffImageFile;
130 variantMismatchCount.insert( suffix, checker.
mismatchCount() );
131 variantSize.insert( suffix, checker.
matchTarget() );
135 if ( !mResult && !mExpectFail && mIsCiRun )
137 const auto constDartMeasurements = dartMeasurements;
142 "If this is a rendering inconsistency, please add another control image folder, add an anomaly image or increase the color tolerance." );
145#if DUMP_BASE64_IMAGES
146 QFile fileSource( mRenderedImage );
147 fileSource.open( QIODevice::ReadOnly );
149 const QByteArray blob = fileSource.readAll();
150 const QByteArray encoded = blob.toBase64();
151 qDebug() <<
"Dumping rendered image " << mRenderedImage <<
" as base64\n";
152 qDebug() <<
"################################################################";
154 qDebug() <<
"################################################################";
155 qDebug() <<
"End dump";
159 if ( !mResult && !mExpectFail )
161 for (
auto it = variantMismatchCount.constBegin(); it != variantMismatchCount.constEnd(); it++ )
163 if ( subDirs.size() > 1 )
165 qDebug() << u
"Variant %1: %2/%3 pixels mismatched (%4 allowed)"_s.arg( it.key() ).arg( it.value() ).arg( variantSize.value( it.key() ) ).arg( mismatchCount );
169 qDebug() << u
"%1/%2 pixels mismatched (%4 allowed)"_s.arg( it.value() ).arg( variantSize.value( it.key() ) ).arg( mismatchCount );
173 if ( !reportDir.exists() )
175 if ( !QDir().mkpath( reportDir.path() ) )
177 qDebug() <<
"!!!!! cannot create " << reportDir.path();
180 if ( QFile::exists( mRenderedImage ) )
182 QFileInfo fi( mRenderedImage );
183 const QString destPath = reportDir.filePath( fi.fileName() );
184 if ( QFile::exists( destPath ) )
185 QFile::remove( destPath );
187 if ( !QFile::copy( mRenderedImage, destPath ) )
189 qDebug() <<
"!!!!! could not copy " << mRenderedImage <<
" to " << destPath;
193 if ( !diffImageFile.isEmpty() && QFile::exists( diffImageFile ) )
195 QFileInfo fi( diffImageFile );
196 const QString destPath = reportDir.filePath( fi.fileName() );
197 if ( QFile::exists( destPath ) )
198 QFile::remove( destPath );
200 if ( !QFile::copy( diffImageFile, destPath ) )
202 qDebug() <<
"!!!!! could not copy " << diffImageFile <<
" to " << destPath;
215 QString
report = mReportHeader;
216 if ( mSourceLine >= 0 )
218 const QString githubSha = qgetenv(
"GITHUB_SHA" );
219 if ( !githubSha.isEmpty() )
221 const QString githubBlobUrl = u
"https://github.com/qgis/QGIS/blob/%1/%2#L%3"_s.arg(
222 githubSha, mSourceFile ).arg( mSourceLine );
223 report += u
"<b style=\"color: red\">Test failed in %1 at <a href=\"%2\">%3:%4</a></b>\n"_s.arg(
226 mSourceFile ).arg( mSourceLine );
230 report += u
"<b style=\"color: red\">Test failed in %1 at %2:%3</b>\n"_s.arg( mSourceFunction, mSourceFile ).arg( mSourceLine );
243 QString
report = mMarkdownReportHeader;
245 if ( mSourceLine >= 0 )
247 const QString githubSha = qgetenv(
"GITHUB_SHA" );
249 if ( !githubSha.isEmpty() )
251 fileLink = u
"https://github.com/qgis/QGIS/blob/%1/%2#L%3"_s.arg(
252 githubSha, mSourceFile ).arg( mSourceLine );
258 report += u
"**Test failed at %1 at [%2:%3](%4)**\n\n"_s.arg( mSourceFunction, mSourceFile ).arg( mSourceLine ).arg( fileLink );
260 report += mMarkdownReport;
266 QString myDataDir( TEST_DATA_DIR );
267 QString myControlImageDir = myDataDir + QDir::separator() +
"control_images" +
268 QDir::separator() + mControlPathPrefix + QDir::separator() + mControlName + QDir::separator();
269 return myControlImageDir;
278QgsLayoutChecker::QgsLayoutChecker(
const QString &testName,
QgsLayout *layout )
279 : mTestName( testName )
282 , mDotsPerMeter( 96 / 25.4 * 1000 )
288bool QgsLayoutChecker::testLayout( QString &checkedReport,
int page,
int pixelDiff,
bool createReferenceImage )
295 setControlName(
"expected_" + mTestName );
298 if ( createReferenceImage )
304 QImage _outputImage( mSize, QImage::Format_RGB32 );
305 _outputImage.setDotsPerMeterX( 96 / 25.4 * 1000 );
306 _outputImage.setDotsPerMeterY( 96 / 25.4 * 1000 );
307 QPainter _p( &_outputImage );
309 _exporter.renderPage( &_p, page );
312 if ( ! QDir( controlImagePath() ).exists() )
314 QDir().mkdir( controlImagePath() );
316 _outputImage.save( controlImagePath() + QDir::separator() +
"expected_" + mTestName +
".png",
"PNG" );
317 qDebug( ) <<
"Reference image saved to : " + controlImagePath() + QDir::separator() +
"expected_" + mTestName +
".png";
321 QImage outputImage( mSize, QImage::Format_RGB32 );
322 outputImage.setDotsPerMeterX( mDotsPerMeter );
323 outputImage.setDotsPerMeterY( mDotsPerMeter );
324 drawBackground( &outputImage );
325 QPainter p( &outputImage );
327 exporter.renderPage( &p, page );
330 QString renderedFilePath = QDir::tempPath() +
'/' + QFileInfo( mTestName ).baseName() +
"_rendered.png";
331 if ( QFile::exists( renderedFilePath ) )
332 QFile::remove( renderedFilePath );
334 outputImage.save( renderedFilePath,
"PNG" );
336 setRenderedImage( renderedFilePath );
338 bool testResult = runTest( mTestName, pixelDiff );
340 checkedReport += report();
Emits dart measurements for display in CDash reports.
Handles rendering and exports of layouts to various formats.
Base class for layouts, which can contain items such as maps, labels, scalebars, etc.
Contains configuration for rendering maps.
bool runTest(const QString &testName, unsigned int mismatchCount=0)
Test using renderer to generate the image to be compared.
void setControlName(const QString &name)
Base directory name for the control image (with control image path suffixed) the path to the image wi...
QString controlImagePath() const
Returns the path to the control images.
void setControlPathPrefix(const QString &prefix)
Sets the path prefix where the control images are kept.
void setColorTolerance(unsigned int colorTolerance)
Set tolerance for color components used by runTest() Default value is 0.
void setMapSettings(const QgsMapSettings &mapSettings)
Set the map settings to use to render the image.
void setFileFunctionLine(const QString &file, const QString &function, int line)
Sets the source file, function and line from where the test originates.
QString report() const
Returns a HTML report for this test.
QgsMultiRenderChecker()
Constructor for QgsMultiRenderChecker.
QString markdownReport() const
Returns a markdown report for this test.
Helper class for unit tests that need to write an image and compare it to an expected result or rende...
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 QString sourcePath()
Returns the path to the QGIS source code.
QString markdownReport(bool ignoreSuccess=true) const
Returns the markdown report describing the results of the test run.
unsigned int matchTarget() const
Returns the total number of pixels in the control image.
void setMapSettings(const QgsMapSettings &mapSettings)
void setControlPathSuffix(const QString &name)
bool runTest(const QString &testName, unsigned int mismatchCount=0, QgsRenderChecker::Flags flags=QgsRenderChecker::Flags())
Test using renderer to generate the image to be compared.
@ Silent
Don't output non-critical messages to console.
@ AvoidExportingRenderedImage
Avoids exporting rendered images to reports.
QString renderedImage() const
Returns the path of the rendered image generated by the test.
QVector< QgsDartMeasurement > dartMeasurements() const
Gets access to buffered dash messages.
void setControlPathPrefix(const QString &name)
Sets the path prefix where the control images are kept.
QString report(bool ignoreSuccess=true) const
Returns the HTML report describing the results of the test run.
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).
void setRenderedImage(const QString &imageFileName)
Sets the file name of the rendered image generated by the test.
void setSizeTolerance(int xTolerance, int yTolerance)
Sets the largest allowable difference in size between the rendered and the expected image.
void enableDashBuffering(bool enable)
Call this to enable internal buffering of dash messages.
unsigned int mismatchCount() const
Returns the number of pixels which did not match the control image.
void setExpectFail(bool expectFail)
Sets whether the comparison is expected to fail.
void setColorTolerance(unsigned int colorTolerance)
Set tolerance for color components used by runTest() and compareImages().