QGIS API Documentation 3.99.0-Master (2fe06baccd8)
Loading...
Searching...
No Matches
qgsmultirenderchecker.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsmultirenderchecker.cpp
3 --------------------------------------
4 Date : 6.11.2014
5 Copyright : (C) 2014 Matthias Kuhn
6 Email : matthias at opengis dot ch
7 ***************************************************************************
8 * *
9 * This program is free software; you can redistribute it and/or modify *
10 * it under the terms of the GNU General Public License as published by *
11 * the Free Software Foundation; either version 2 of the License, or *
12 * (at your option) any later version. *
13 * *
14 ***************************************************************************/
15
17
18#include <mutex>
19
20#include "qgslayout.h"
21#include "qgslayoutexporter.h"
22
23#include <QDebug>
24
26{
27 if ( qgetenv( "QGIS_CONTINUOUS_INTEGRATION_RUN" ) == QStringLiteral( "true" ) )
28 mIsCiRun = true;
29}
30
31void QgsMultiRenderChecker::setControlName( const QString &name )
32{
33 mControlName = name;
34}
35
36void QgsMultiRenderChecker::setFileFunctionLine( const QString &file, const QString &function, int line )
37{
38#ifndef _MSC_VER
39 mSourceFile = QDir( QgsRenderChecker::sourcePath() ).relativeFilePath( file );
40#else
41 mSourceFile = file;
42#endif
43
44 mSourceFunction = function;
45 mSourceLine = line;
46}
47
49{
50 mControlPathPrefix = prefix;
51}
52
54{
55 mMapSettings = mapSettings;
56}
57
58bool QgsMultiRenderChecker::runTest( const QString &testName, unsigned int mismatchCount )
59{
60 mResult = false;
61
62 mReportHeader = "<h2>" + testName + "</h2>\n";
63 mMarkdownReportHeader = QStringLiteral( "### %1\n\n" ).arg( testName );
64
65 const QString baseDir = controlImagePath();
66 if ( !QFile::exists( baseDir ) )
67 {
68 qDebug() << "Control image path " << baseDir << " does not exist!";
69 return mResult;
70 }
71
72 QStringList subDirs = QDir( baseDir ).entryList( QDir::Dirs | QDir::NoDotAndDotDot );
73
74 if ( subDirs.isEmpty() )
75 {
76 subDirs << QString();
77 }
78
79 QVector<QgsDartMeasurement> dartMeasurements;
80
81 // we can only report one diff image, so just use the first
82 QString diffImageFile;
83
84 QMap< QString, int > variantMismatchCount;
85 QMap< QString, int > variantSize;
86
87 for ( const QString &suffix : std::as_const( subDirs ) )
88 {
89 QgsRenderChecker checker;
90 checker.enableDashBuffering( true );
91 checker.setColorTolerance( mColorTolerance );
92 checker.setSizeTolerance( mMaxSizeDifferenceX, mMaxSizeDifferenceY );
93 checker.setControlPathPrefix( mControlPathPrefix );
94 checker.setControlPathSuffix( suffix );
95 checker.setControlName( mControlName );
96 checker.setMapSettings( mMapSettings );
97 checker.setExpectFail( mExpectFail );
98
99 bool result = false;
100 if ( !mRenderedImage.isNull() )
101 {
102 checker.setRenderedImage( mRenderedImage );
103 result = checker.compareImages( testName, mismatchCount, mRenderedImage, QgsRenderChecker::Flag::AvoidExportingRenderedImage | QgsRenderChecker::Flag::Silent );
104 }
105 else
106 {
108 mRenderedImage = checker.renderedImage();
109 }
110
111 mResult |= result;
112
113 dartMeasurements << checker.dartMeasurements();
114
115 mReport += checker.report( false );
116 if ( subDirs.count() > 1 )
117 mMarkdownReport += QStringLiteral( "* " ) + checker.markdownReport( false );
118 else
119 mMarkdownReport += checker.markdownReport( false );
120
121 if ( !mResult && diffImageFile.isEmpty() )
122 {
123 diffImageFile = checker.mDiffImageFile;
124 }
125 if ( !mResult )
126 {
127 variantMismatchCount.insert( suffix, checker.mismatchCount() );
128 variantSize.insert( suffix, checker.matchTarget() );
129 }
130 }
131
132 if ( !mResult && !mExpectFail && mIsCiRun )
133 {
134 const auto constDartMeasurements = dartMeasurements;
135 for ( const QgsDartMeasurement &measurement : constDartMeasurements )
136 measurement.send();
137
138 QgsDartMeasurement msg( QStringLiteral( "Image not accepted by test" ), QgsDartMeasurement::Text, "This may be caused because the test is supposed to fail or rendering inconsistencies."
139 "If this is a rendering inconsistency, please add another control image folder, add an anomaly image or increase the color tolerance." );
140 msg.send();
141
142#if DUMP_BASE64_IMAGES
143 QFile fileSource( mRenderedImage );
144 fileSource.open( QIODevice::ReadOnly );
145
146 const QByteArray blob = fileSource.readAll();
147 const QByteArray encoded = blob.toBase64();
148 qDebug() << "Dumping rendered image " << mRenderedImage << " as base64\n";
149 qDebug() << "################################################################";
150 qDebug() << encoded;
151 qDebug() << "################################################################";
152 qDebug() << "End dump";
153#endif
154 }
155
156 if ( !mResult && !mExpectFail )
157 {
158 for ( auto it = variantMismatchCount.constBegin(); it != variantMismatchCount.constEnd(); it++ )
159 {
160 if ( subDirs.size() > 1 )
161 {
162 qDebug() << QStringLiteral( "Variant %1: %2/%3 pixels mismatched (%4 allowed)" ).arg( it.key() ).arg( it.value() ).arg( variantSize.value( it.key() ) ).arg( mismatchCount );
163 }
164 else
165 {
166 qDebug() << QStringLiteral( "%1/%2 pixels mismatched (%4 allowed)" ).arg( it.value() ).arg( variantSize.value( it.key() ) ).arg( mismatchCount );
167 }
168 }
169 const QDir reportDir = QgsRenderChecker::testReportDir();
170 if ( !reportDir.exists() )
171 {
172 if ( !QDir().mkpath( reportDir.path() ) )
173 {
174 qDebug() << "!!!!! cannot create " << reportDir.path();
175 }
176 }
177 if ( QFile::exists( mRenderedImage ) )
178 {
179 QFileInfo fi( mRenderedImage );
180 const QString destPath = reportDir.filePath( fi.fileName() );
181 if ( QFile::exists( destPath ) )
182 QFile::remove( destPath );
183
184 if ( !QFile::copy( mRenderedImage, destPath ) )
185 {
186 qDebug() << "!!!!! could not copy " << mRenderedImage << " to " << destPath;
187 }
188 }
189
190 if ( !diffImageFile.isEmpty() && QFile::exists( diffImageFile ) )
191 {
192 QFileInfo fi( diffImageFile );
193 const QString destPath = reportDir.filePath( fi.fileName() );
194 if ( QFile::exists( destPath ) )
195 QFile::remove( destPath );
196
197 if ( !QFile::copy( diffImageFile, destPath ) )
198 {
199 qDebug() << "!!!!! could not copy " << diffImageFile << " to " << destPath;
200 }
201 }
202 }
203
204 return mResult;
205}
206
208{
209 if ( mResult )
210 return QString();
211
212 QString report = mReportHeader;
213 if ( mSourceLine >= 0 )
214 {
215 const QString githubSha = qgetenv( "GITHUB_SHA" );
216 if ( !githubSha.isEmpty() )
217 {
218 const QString githubBlobUrl = QStringLiteral( "https://github.com/qgis/QGIS/blob/%1/%2#L%3" ).arg(
219 githubSha, mSourceFile ).arg( mSourceLine );
220 report += QStringLiteral( "<b style=\"color: red\">Test failed in %1 at <a href=\"%2\">%3:%4</a></b>\n" ).arg(
221 mSourceFunction,
222 githubBlobUrl,
223 mSourceFile ).arg( mSourceLine );
224 }
225 else
226 {
227 report += QStringLiteral( "<b style=\"color: red\">Test failed in %1 at %2:%3</b>\n" ).arg( mSourceFunction, mSourceFile ).arg( mSourceLine );
228 }
229 }
230
231 report += mReport;
232 return report;
233}
234
236{
237 if ( mResult )
238 return QString();
239
240 QString report = mMarkdownReportHeader;
241
242 if ( mSourceLine >= 0 )
243 {
244 const QString githubSha = qgetenv( "GITHUB_SHA" );
245 QString fileLink;
246 if ( !githubSha.isEmpty() )
247 {
248 fileLink = QStringLiteral( "https://github.com/qgis/QGIS/blob/%1/%2#L%3" ).arg(
249 githubSha, mSourceFile ).arg( mSourceLine );
250 }
251 else
252 {
253 fileLink = QUrl::fromLocalFile( QDir( QgsRenderChecker::sourcePath() ).filePath( mSourceFile ) ).toString();
254 }
255 report += QStringLiteral( "**Test failed at %1 at [%2:%3](%4)**\n\n" ).arg( mSourceFunction, mSourceFile ).arg( mSourceLine ).arg( fileLink );
256 }
257 report += mMarkdownReport;
258 return report;
259}
260
262{
263 QString myDataDir( TEST_DATA_DIR ); //defined in CmakeLists.txt
264 QString myControlImageDir = myDataDir + QDir::separator() + "control_images" +
265 QDir::separator() + mControlPathPrefix + QDir::separator() + mControlName + QDir::separator();
266 return myControlImageDir;
267}
268
269//
270// QgsLayoutChecker
271//
272
274
275QgsLayoutChecker::QgsLayoutChecker( const QString &testName, QgsLayout *layout )
276 : mTestName( testName )
277 , mLayout( layout )
278 , mSize( 1122, 794 )
279 , mDotsPerMeter( 96 / 25.4 * 1000 )
280{
281 // Qt has some slight render inconsistencies on the whole image sometimes
283}
284
285bool QgsLayoutChecker::testLayout( QString &checkedReport, int page, int pixelDiff, bool createReferenceImage )
286{
287 if ( !mLayout )
288 {
289 return false;
290 }
291
292 setControlName( "expected_" + mTestName );
293
294
295 if ( createReferenceImage )
296 {
297 //fake mode to generate expected image
298 //assume 96 dpi
299
300
301 QImage _outputImage( mSize, QImage::Format_RGB32 );
302 _outputImage.setDotsPerMeterX( 96 / 25.4 * 1000 );
303 _outputImage.setDotsPerMeterY( 96 / 25.4 * 1000 );
304 QPainter _p( &_outputImage );
305 QgsLayoutExporter _exporter( mLayout );
306 _exporter.renderPage( &_p, page );
307 _p.end();
308
309 if ( ! QDir( controlImagePath() ).exists() )
310 {
311 QDir().mkdir( controlImagePath() );
312 }
313 _outputImage.save( controlImagePath() + QDir::separator() + "expected_" + mTestName + ".png", "PNG" );
314 qDebug( ) << "Reference image saved to : " + controlImagePath() + QDir::separator() + "expected_" + mTestName + ".png";
315
316 }
317
318 QImage outputImage( mSize, QImage::Format_RGB32 );
319 outputImage.setDotsPerMeterX( mDotsPerMeter );
320 outputImage.setDotsPerMeterY( mDotsPerMeter );
321 drawBackground( &outputImage );
322 QPainter p( &outputImage );
323 QgsLayoutExporter exporter( mLayout );
324 exporter.renderPage( &p, page );
325 p.end();
326
327 QString renderedFilePath = QDir::tempPath() + '/' + QFileInfo( mTestName ).baseName() + "_rendered.png";
328 if ( QFile::exists( renderedFilePath ) )
329 QFile::remove( renderedFilePath );
330
331 outputImage.save( renderedFilePath, "PNG" );
332
333 setRenderedImage( renderedFilePath );
334
335 bool testResult = runTest( mTestName, pixelDiff );
336
337 checkedReport += report();
338
339 return testResult;
340}
341
342
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.
Definition qgslayout.h:50
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().