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