QGIS API Documentation 3.37.0-Master (fdefdf9c27f)
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 for ( const QString &suffix : std::as_const( subDirs ) )
82 {
83 if ( subDirs.count() > 1 )
84 {
85 qDebug() << "Checking subdir " << suffix;
86 }
87 bool result;
88 QgsRenderChecker checker;
89 checker.enableDashBuffering( true );
90 checker.setColorTolerance( mColorTolerance );
91 checker.setSizeTolerance( mMaxSizeDifferenceX, mMaxSizeDifferenceY );
92 checker.setControlPathPrefix( mControlPathPrefix );
93 checker.setControlPathSuffix( suffix );
94 checker.setControlName( mControlName );
95 checker.setMapSettings( mMapSettings );
96 checker.setExpectFail( mExpectFail );
97
98 if ( !mRenderedImage.isNull() )
99 {
100 checker.setRenderedImage( mRenderedImage );
101 result = checker.compareImages( testName, mismatchCount, mRenderedImage, QgsRenderChecker::Flag::AvoidExportingRenderedImage );
102 }
103 else
104 {
105 result = checker.runTest( testName, mismatchCount, QgsRenderChecker::Flag::AvoidExportingRenderedImage );
106 mRenderedImage = checker.renderedImage();
107 }
108
109 mResult |= result;
110
111 dartMeasurements << checker.dartMeasurements();
112
113 mReport += checker.report( false );
114 if ( subDirs.count() > 1 )
115 mMarkdownReport += QStringLiteral( "* " ) + checker.markdownReport( false );
116 else
117 mMarkdownReport += checker.markdownReport( false );
118
119 if ( !mResult && diffImageFile.isEmpty() )
120 {
121 diffImageFile = checker.mDiffImageFile;
122 }
123 }
124
125 if ( !mResult && !mExpectFail && mIsCiRun )
126 {
127 const auto constDartMeasurements = dartMeasurements;
128 for ( const QgsDartMeasurement &measurement : constDartMeasurements )
129 measurement.send();
130
131 QgsDartMeasurement msg( QStringLiteral( "Image not accepted by test" ), QgsDartMeasurement::Text, "This may be caused because the test is supposed to fail or rendering inconsistencies."
132 "If this is a rendering inconsistency, please add another control image folder, add an anomaly image or increase the color tolerance." );
133 msg.send();
134
135#if DUMP_BASE64_IMAGES
136 QFile fileSource( mRenderedImage );
137 fileSource.open( QIODevice::ReadOnly );
138
139 const QByteArray blob = fileSource.readAll();
140 const QByteArray encoded = blob.toBase64();
141 qDebug() << "Dumping rendered image " << mRenderedImage << " as base64\n";
142 qDebug() << "################################################################";
143 qDebug() << encoded;
144 qDebug() << "################################################################";
145 qDebug() << "End dump";
146#endif
147 }
148
149 if ( !mResult && !mExpectFail )
150 {
151 const QDir reportDir = QgsRenderChecker::testReportDir();
152 if ( !reportDir.exists() )
153 {
154 if ( !QDir().mkpath( reportDir.path() ) )
155 {
156 qDebug() << "!!!!! cannot create " << reportDir.path();
157 }
158 }
159 if ( QFile::exists( mRenderedImage ) )
160 {
161 QFileInfo fi( mRenderedImage );
162 const QString destPath = reportDir.filePath( fi.fileName() );
163 if ( QFile::exists( destPath ) )
164 QFile::remove( destPath );
165
166 if ( !QFile::copy( mRenderedImage, destPath ) )
167 {
168 qDebug() << "!!!!! could not copy " << mRenderedImage << " to " << destPath;
169 }
170 }
171
172 if ( !diffImageFile.isEmpty() && QFile::exists( diffImageFile ) )
173 {
174 QFileInfo fi( diffImageFile );
175 const QString destPath = reportDir.filePath( fi.fileName() );
176 if ( QFile::exists( destPath ) )
177 QFile::remove( destPath );
178
179 if ( !QFile::copy( diffImageFile, destPath ) )
180 {
181 qDebug() << "!!!!! could not copy " << diffImageFile << " to " << destPath;
182 }
183 }
184 }
185
186 return mResult;
187}
188
190{
191 if ( mResult )
192 return QString();
193
194 QString report = mReportHeader;
195 if ( mSourceLine >= 0 )
196 {
197 const QString githubSha = qgetenv( "GITHUB_SHA" );
198 if ( !githubSha.isEmpty() )
199 {
200 const QString githubBlobUrl = QStringLiteral( "https://github.com/qgis/QGIS/blob/%1/%2#L%3" ).arg(
201 githubSha, mSourceFile ).arg( mSourceLine );
202 report += QStringLiteral( "<b style=\"color: red\">Test failed in %1 at <a href=\"%2\">%3:%4</a></b>\n" ).arg(
203 mSourceFunction,
204 githubBlobUrl,
205 mSourceFile ).arg( mSourceLine );
206 }
207 else
208 {
209 report += QStringLiteral( "<b style=\"color: red\">Test failed in %1 at %2:%3</b>\n" ).arg( mSourceFunction, mSourceFile ).arg( mSourceLine );
210 }
211 }
212
213 report += mReport;
214 return report;
215}
216
218{
219 if ( mResult )
220 return QString();
221
222 QString report = mMarkdownReportHeader;
223
224 if ( mSourceLine >= 0 )
225 {
226 const QString githubSha = qgetenv( "GITHUB_SHA" );
227 QString fileLink;
228 if ( !githubSha.isEmpty() )
229 {
230 fileLink = QStringLiteral( "https://github.com/qgis/QGIS/blob/%1/%2#L%3" ).arg(
231 githubSha, mSourceFile ).arg( mSourceLine );
232 }
233 else
234 {
235 fileLink = QUrl::fromLocalFile( QDir( QgsRenderChecker::sourcePath() ).filePath( mSourceFile ) ).toString();
236 }
237 report += QStringLiteral( "**Test failed at %1 at [%2:%3](%4)**\n\n" ).arg( mSourceFunction, mSourceFile ).arg( mSourceLine ).arg( fileLink );
238 }
239 report += mMarkdownReport;
240 return report;
241}
242
244{
245 QString myDataDir( TEST_DATA_DIR ); //defined in CmakeLists.txt
246 QString myControlImageDir = myDataDir + QDir::separator() + "control_images" +
247 QDir::separator() + mControlPathPrefix + QDir::separator() + mControlName + QDir::separator();
248 return myControlImageDir;
249}
250
251//
252// QgsLayoutChecker
253//
254
256
257QgsLayoutChecker::QgsLayoutChecker( const QString &testName, QgsLayout *layout )
258 : mTestName( testName )
259 , mLayout( layout )
260 , mSize( 1122, 794 )
261 , mDotsPerMeter( 96 / 25.4 * 1000 )
262{
263 // Qt has some slight render inconsistencies on the whole image sometimes
265}
266
267bool QgsLayoutChecker::testLayout( QString &checkedReport, int page, int pixelDiff, bool createReferenceImage )
268{
269 if ( !mLayout )
270 {
271 return false;
272 }
273
274 setControlName( "expected_" + mTestName );
275
276
277 if ( createReferenceImage )
278 {
279 //fake mode to generate expected image
280 //assume 96 dpi
281
282
283 QImage _outputImage( mSize, QImage::Format_RGB32 );
284 _outputImage.setDotsPerMeterX( 96 / 25.4 * 1000 );
285 _outputImage.setDotsPerMeterY( 96 / 25.4 * 1000 );
286 QPainter _p( &_outputImage );
287 QgsLayoutExporter _exporter( mLayout );
288 _exporter.renderPage( &_p, page );
289 _p.end();
290
291 if ( ! QDir( controlImagePath() ).exists() )
292 {
293 QDir().mkdir( controlImagePath() );
294 }
295 _outputImage.save( controlImagePath() + QDir::separator() + "expected_" + mTestName + ".png", "PNG" );
296 qDebug( ) << "Reference image saved to : " + controlImagePath() + QDir::separator() + "expected_" + mTestName + ".png";
297
298 }
299
300 QImage outputImage( mSize, QImage::Format_RGB32 );
301 outputImage.setDotsPerMeterX( mDotsPerMeter );
302 outputImage.setDotsPerMeterY( mDotsPerMeter );
303 drawBackground( &outputImage );
304 QPainter p( &outputImage );
305 QgsLayoutExporter exporter( mLayout );
306 exporter.renderPage( &p, page );
307 p.end();
308
309 QString renderedFilePath = QDir::tempPath() + '/' + QFileInfo( mTestName ).baseName() + "_rendered.png";
310 if ( QFile::exists( renderedFilePath ) )
311 QFile::remove( renderedFilePath );
312
313 outputImage.save( renderedFilePath, "PNG" );
314
315 setRenderedImage( renderedFilePath );
316
317 bool testResult = runTest( mTestName, pixelDiff );
318
319 checkedReport += report();
320
321 return testResult;
322}
323
324
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.
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.
@ 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.
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().