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