Skip to content

Commit b28dcb9

Browse files
committedNov 14, 2014
[unit tests] Add multirenderchecker
The multirenderchecker allows to have several images, each with its own set of anomalies distributed in several subdirectories. With the help of multiple reference images, it is possible to apply a color tolerance to each of these
1 parent 3528661 commit b28dcb9

File tree

9 files changed

+349
-35
lines changed

9 files changed

+349
-35
lines changed
 

‎python/core/core.sip

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
%Include qgsmessagelog.sip
6767
%Include qgsmessageoutput.sip
6868
%Include qgsmimedatautils.sip
69+
%Include qgsmultirenderchecker.sip
6970
%Include qgsnetworkaccessmanager.sip
7071
%Include qgsnetworkcontentfetcher.sip
7172
%Include qgsofflineediting.sip

‎python/core/qgsmultirenderchecker.sip

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/***************************************************************************
2+
qgsmultirenderchecker.sip
3+
--------------------------------------
4+
Date : 6.11.2014
5+
Copyright : (C) 2014 Matthias Kuhn
6+
Email : matthias dot kuhn at gmx 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+
16+
/**
17+
*
18+
* @note added in 2.8
19+
*/
20+
21+
class QgsMultiRenderChecker
22+
{
23+
%TypeHeaderCode
24+
#include <qgsmultirenderchecker.h>
25+
%End
26+
public:
27+
QgsMultiRenderChecker();
28+
29+
/**
30+
* Base directory name for the control image (with control image path
31+
* suffixed) the path to the image will be constructed like this:
32+
* controlImagePath + '/' + mControlName + '/' + mControlName + '.png'
33+
*/
34+
void setControlName( const QString& theName );
35+
36+
void setControlPathPrefix( const QString& prefix );
37+
38+
/**
39+
* Set the path to the rendered image. If this is not set or set to QString::Null, an image
40+
* will be rendered based on the provided mapsettings
41+
*
42+
* @param renderedImagePath A path to the rendered image with which control images will be compared
43+
*/
44+
void setRenderedImage( const QString& renderedImagePath );
45+
46+
/**
47+
* Set the map settings to use to render the image
48+
*
49+
* @param mapSettings The map settings
50+
*/
51+
void setMapSettings( const QgsMapSettings& mapSettings );
52+
53+
/**
54+
* Set tolerance for color components used by runTest()
55+
* Default value is 0.
56+
*
57+
* @param theColorTolerance The maximum difference for each color component
58+
* including alpha to be considered correct.
59+
*/
60+
void setColorTolerance( unsigned int theColorTolerance );
61+
62+
/**
63+
* Test using renderer to generate the image to be compared.
64+
*
65+
* @param theTestName - to be used as the basis for writing a file to
66+
* e.g. /tmp/theTestName.png
67+
*
68+
* @param theMismatchCount - defaults to 0 - the number of pixels that
69+
* are allowed to be different from the control image. In some cases
70+
* rendering may be non-deterministic. This parameter allows you to account
71+
* for that by providing a tolerance.
72+
*
73+
* @note make sure to call setExpectedImage and setMapSettings first
74+
*/
75+
bool runTest( const QString& theTestName, unsigned int theMismatchCount = 0 );
76+
77+
/**
78+
* Returns a report for this test
79+
*
80+
* @return A report
81+
*/
82+
const QString& report() const;
83+
84+
/**
85+
* @brief controlImagePath
86+
* @return
87+
*/
88+
const QString controlImagePath() const;
89+
90+
};
91+

‎src/core/CMakeLists.txt

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -114,12 +114,13 @@ SET(QGIS_CORE_SRCS
114114
qgsmapsettings.cpp
115115
qgsmaptopixel.cpp
116116
qgsmaptopixelgeometrysimplifier.cpp
117+
qgsmessagelog.cpp
117118
qgsmessageoutput.cpp
118119
qgsmimedatautils.cpp
119-
qgsmessagelog.cpp
120+
qgsmultirenderchecker.cpp
120121
qgsnetworkaccessmanager.cpp
121-
qgsnetworkreplyparser.cpp
122122
qgsnetworkcontentfetcher.cpp
123+
qgsnetworkreplyparser.cpp
123124
qgsobjectcustomproperties.cpp
124125
qgsofflineediting.cpp
125126
qgsogcutils.cpp
@@ -131,17 +132,17 @@ SET(QGIS_CORE_SRCS
131132
qgspoint.cpp
132133
qgsproject.cpp
133134
qgsprojectfiletransform.cpp
134-
qgsprojectversion.cpp
135135
qgsprojectproperty.cpp
136+
qgsprojectversion.cpp
136137
qgsprovidercountcalcevent.cpp
137138
qgsproviderextentcalcevent.cpp
138139
qgsprovidermetadata.cpp
139140
qgsproviderregistry.cpp
140141
qgspythonrunner.cpp
141142
qgsrelation.cpp
142143
qgsrelationmanager.cpp
143-
qgsrendercontext.cpp
144144
qgsrenderchecker.cpp
145+
qgsrendercontext.cpp
145146
qgsrectangle.cpp
146147
qgsrunprocess.cpp
147148
qgsscalecalculator.cpp
@@ -513,8 +514,9 @@ SET(QGIS_CORE_HDRS
513514
qgsmapunitscale.h
514515
qgsmessageoutput.h
515516
qgsmimedatautils.h
516-
qgsnetworkreplyparser.h
517+
qgsmultirenderchecker.h
517518
qgsnetworkcontentfetcher.h
519+
qgsnetworkreplyparser.h
518520
qgsobjectcustomproperties.h
519521
qgsofflineediting.h
520522
qgsogcutils.h
@@ -534,10 +536,10 @@ SET(QGIS_CORE_HDRS
534536
qgsproviderregistry.h
535537
qgspythonrunner.h
536538
qgsrectangle.h
537-
qgsrendercontext.h
538-
qgsrenderchecker.h
539539
qgsrelation.h
540540
qgsrelationmanager.h
541+
qgsrenderchecker.h
542+
qgsrendercontext.h
541543
qgsrunprocess.h
542544
qgsscalecalculator.h
543545
qgsscaleutils.h

‎src/core/qgsmultirenderchecker.cpp

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/***************************************************************************
2+
qgsmultirenderchecker.cpp
3+
--------------------------------------
4+
Date : 6.11.2014
5+
Copyright : (C) 2014 Matthias Kuhn
6+
Email : matthias dot kuhn at gmx 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+
16+
#include "qgsmultirenderchecker.h"
17+
18+
#include <QDebug>
19+
20+
QgsMultiRenderChecker::QgsMultiRenderChecker()
21+
{
22+
}
23+
24+
void QgsMultiRenderChecker::setControlName( const QString& theName )
25+
{
26+
mControlName = theName;
27+
}
28+
29+
void QgsMultiRenderChecker::setControlPathPrefix( const QString& prefix )
30+
{
31+
mControlPathPrefix = prefix;
32+
}
33+
34+
void QgsMultiRenderChecker::setMapSettings( const QgsMapSettings& mapSettings )
35+
{
36+
mMapSettings = mapSettings;
37+
}
38+
39+
bool QgsMultiRenderChecker::runTest( const QString& theTestName, unsigned int theMismatchCount )
40+
{
41+
bool successful = false;
42+
43+
const QString baseDir = controlImagePath();
44+
45+
QStringList subDirs = QDir( baseDir ).entryList( QDir::Dirs | QDir::NoDotAndDotDot );
46+
47+
if ( subDirs.count() == 0 )
48+
{
49+
subDirs << "";
50+
}
51+
52+
Q_FOREACH( const QString& suffix, subDirs )
53+
{
54+
qDebug() << "Checking subdir " << suffix;
55+
bool result;
56+
QgsRenderChecker checker;
57+
checker.setColorTolerance( mColorTolerance );
58+
checker.setControlPathPrefix( mControlPathPrefix );
59+
checker.setControlPathSuffix( suffix );
60+
checker.setControlName( mControlName );
61+
checker.setMapSettings( mMapSettings );
62+
63+
if ( !mRenderedImage.isNull() )
64+
{
65+
checker.setRenderedImage( mRenderedImage );
66+
result = checker.compareImages( theTestName, theMismatchCount, mRenderedImage );
67+
}
68+
else
69+
{
70+
result = checker.runTest( theTestName, theMismatchCount );
71+
mRenderedImage = checker.renderedImage();
72+
}
73+
74+
qDebug() << " * Subdir check " << suffix << ": " << result;
75+
successful |= result;
76+
77+
mReport += checker.report();
78+
}
79+
80+
if ( !successful )
81+
qDebug() << "No matching image found. If you think that this result should be considered ok, please copy it into a new subdirectory inside " << baseDir;
82+
83+
return successful;
84+
}
85+
86+
const QString QgsMultiRenderChecker::controlImagePath() const
87+
{
88+
QString myDataDir( TEST_DATA_DIR ); //defined in CmakeLists.txt
89+
QString myControlImageDir = myDataDir + QDir::separator() + "control_images" +
90+
QDir::separator() + mControlPathPrefix + QDir::separator() + mControlName + QDir::separator();
91+
return myControlImageDir;
92+
}

‎src/core/qgsmultirenderchecker.h

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/***************************************************************************
2+
qgsmultirenderchecker.h
3+
--------------------------------------
4+
Date : 6.11.2014
5+
Copyright : (C) 2014 Matthias Kuhn
6+
Email : matthias dot kuhn at gmx 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+
16+
#ifndef QGSMULTIRENDERCHECKER_H
17+
#define QGSMULTIRENDERCHECKER_H
18+
19+
#include "qgsrenderchecker.h"
20+
21+
/**
22+
* This class allows to check rendered images against comparison images.
23+
* Its main purpose is for the unit testing framework.
24+
*
25+
* It will either
26+
* <ul>
27+
* <li>take an externally rendered image (setRenderedImage())</li>
28+
* <li>render the image based on provided mapSettings (setMapSettings())</li>
29+
* </ul>
30+
*
31+
* This image will then be compared against one or several images in a folder inside
32+
* the control directory (tests/testdata/control_images/{controlName}).
33+
*
34+
* There are modes for single and for multiple reference images.
35+
* <ul>
36+
* <li>If there are no subfolders in the control directory, it will assume an image
37+
* with the name {controlImage}.png in the control directory itself.</li>
38+
*
39+
* <li>If there are subfolders inside the control directory, it will search for images
40+
* with the name {controlImage}.png in every subfolder.</li>
41+
* </ul>
42+
*
43+
* For every control image there may be one or several randomly named anomaly images defining
44+
* allowed anomalies.
45+
* For every control image, the allowed mismatch and color tolerance values will be calculated
46+
* individually.
47+
*
48+
* @note added in 2.8
49+
*/
50+
51+
class CORE_EXPORT QgsMultiRenderChecker
52+
{
53+
public:
54+
QgsMultiRenderChecker();
55+
56+
/**
57+
* Base directory name for the control image (with control image path
58+
* suffixed) the path to the image will be constructed like this:
59+
* controlImagePath + '/' + mControlName + '/' + mControlName + '.png'
60+
*/
61+
void setControlName( const QString& theName );
62+
63+
void setControlPathPrefix( const QString& prefix );
64+
65+
/**
66+
* Set the path to the rendered image. If this is not set or set to QString::Null, an image
67+
* will be rendered based on the provided mapsettings
68+
*
69+
* @param renderedImagePath A path to the rendered image with which control images will be compared
70+
*/
71+
void setRenderedImage( const QString& renderedImagePath ) { mRenderedImage = renderedImagePath; }
72+
73+
/**
74+
* Set the map settings to use to render the image
75+
*
76+
* @param mapSettings The map settings
77+
*/
78+
void setMapSettings( const QgsMapSettings& mapSettings );
79+
80+
/**
81+
* Set tolerance for color components used by runTest()
82+
* Default value is 0.
83+
*
84+
* @param theColorTolerance The maximum difference for each color component
85+
* including alpha to be considered correct.
86+
*/
87+
void setColorTolerance( unsigned int theColorTolerance ) { mColorTolerance = theColorTolerance; }
88+
89+
/**
90+
* Test using renderer to generate the image to be compared.
91+
*
92+
* @param theTestName - to be used as the basis for writing a file to
93+
* e.g. /tmp/theTestName.png
94+
*
95+
* @param theMismatchCount - defaults to 0 - the number of pixels that
96+
* are allowed to be different from the control image. In some cases
97+
* rendering may be non-deterministic. This parameter allows you to account
98+
* for that by providing a tolerance.
99+
*
100+
* @note make sure to call setExpectedImage and setMapSettings first
101+
*/
102+
bool runTest( const QString& theTestName, unsigned int theMismatchCount = 0 );
103+
104+
/**
105+
* Returns a report for this test
106+
*
107+
* @return A report
108+
*/
109+
const QString& report() const { return mReport; }
110+
111+
/**
112+
* @brief controlImagePath
113+
* @return
114+
*/
115+
const QString controlImagePath() const;
116+
117+
private:
118+
QString mReport;
119+
QString mRenderedImage;
120+
QString mControlName;
121+
QString mControlPathPrefix;
122+
unsigned int mColorTolerance;
123+
QgsMapSettings mMapSettings;
124+
};
125+
126+
#endif // QGSMULTIRENDERCHECKER_H

‎src/core/qgsrenderchecker.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ QString QgsRenderChecker::controlImagePath() const
5151
void QgsRenderChecker::setControlName( const QString theName )
5252
{
5353
mControlName = theName;
54-
mExpectedImageFile = controlImagePath() + theName + QDir::separator()
54+
mExpectedImageFile = controlImagePath() + theName + QDir::separator() + mControlPathSuffix
5555
+ theName + ".png";
5656
}
5757

‎src/core/qgsrenderchecker.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,19 +55,32 @@ class CORE_EXPORT QgsRenderChecker
5555
//only records time for actual render part
5656
int elapsedTime() { return mElapsedTime; }
5757
void setElapsedTimeTarget( int theTarget ) { mElapsedTimeTarget = theTarget; };
58+
5859
/** Base directory name for the control image (with control image path
5960
* suffixed) the path to the image will be constructed like this:
6061
* controlImagePath + '/' + mControlName + '/' + mControlName + '.png'
6162
*/
6263
void setControlName( const QString theName );
64+
6365
/** Prefix where the control images are kept.
6466
* This will be appended to controlImagePath
6567
*/
6668
void setControlPathPrefix( const QString theName ) { mControlPathPrefix = theName + QDir::separator(); }
69+
70+
void setControlPathSuffix( const QString& theName ) { mControlPathSuffix = theName + QDir::separator(); }
71+
6772
/** Get an md5 hash that uniquely identifies an image */
6873
QString imageToHash( QString theImageFile );
6974

7075
void setRenderedImage( QString theImageFileName ) { mRenderedImageFile = theImageFileName; }
76+
77+
/**
78+
* The path of the rendered image can be retrieved through that method.
79+
* Will return the path set with setRenderedImage() or generated in runTest()
80+
*
81+
* @return The path to the rendered image
82+
*/
83+
const QString& renderedImage() { return mRenderedImageFile; }
7184
//! @deprecated since 2.4 - use setMapSettings()
7285
Q_DECL_DEPRECATED void setMapRenderer( QgsMapRenderer * thepMapRenderer );
7386

@@ -130,6 +143,7 @@ class CORE_EXPORT QgsRenderChecker
130143
int mElapsedTimeTarget;
131144
QgsMapSettings mMapSettings;
132145
QString mControlPathPrefix;
146+
QString mControlPathSuffix;
133147

134148
}; // class QgsRenderChecker
135149

‎tests/src/core/qgscompositionchecker.cpp

Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@
2222
#include <QPainter>
2323

2424
QgsCompositionChecker::QgsCompositionChecker( const QString& testName, QgsComposition* composition )
25-
: QgsRenderChecker(),
25+
: QgsMultiRenderChecker(),
2626
mTestName( testName ),
27-
mComposition( composition )
27+
mComposition( composition ),
28+
mSize( 1122, 794 ),
29+
mDotsPerMeter( 96 / 25.4 * 1000 )
2830
{
2931
}
3032

@@ -36,7 +38,7 @@ QgsCompositionChecker::~QgsCompositionChecker()
3638
{
3739
}
3840

39-
bool QgsCompositionChecker::testComposition( QString &report, int page, int pixelDiff )
41+
bool QgsCompositionChecker::testComposition( QString &theReport, int page, int pixelDiff )
4042
{
4143
if ( !mComposition )
4244
{
@@ -62,17 +64,11 @@ bool QgsCompositionChecker::testComposition( QString &report, int page, int pixe
6264
return true;
6365
#endif //0
6466

65-
//load expected image
66-
QImage expectedImage( mExpectedImageFile );
67-
68-
//get width/height, create image and render the composition to it
69-
int width = expectedImage.width();
70-
int height = expectedImage.height();
71-
QImage outputImage( QSize( width, height ), QImage::Format_ARGB32 );
67+
QImage outputImage( mSize, QImage::Format_ARGB32 );
7268

7369
mComposition->setPlotStyle( QgsComposition::Print );
74-
outputImage.setDotsPerMeterX( expectedImage.dotsPerMeterX() );
75-
outputImage.setDotsPerMeterY( expectedImage.dotsPerMeterX() );
70+
outputImage.setDotsPerMeterX( mDotsPerMeter );
71+
outputImage.setDotsPerMeterY( mDotsPerMeter );
7672
outputImage.fill( 0 );
7773
QPainter p( &outputImage );
7874
mComposition->renderPage( &p, page );
@@ -81,19 +77,11 @@ bool QgsCompositionChecker::testComposition( QString &report, int page, int pixe
8177
QString renderedFilePath = QDir::tempPath() + QDir::separator() + QFileInfo( mTestName ).baseName() + "_rendered.png";
8278
outputImage.save( renderedFilePath, "PNG" );
8379

84-
QString diffFilePath = QDir::tempPath() + QDir::separator() + QFileInfo( mTestName ).baseName() + "_result_diff.png";
80+
setRenderedImage( renderedFilePath );
8581

86-
bool testResult = compareImages( mTestName, pixelDiff, renderedFilePath );
82+
bool testResult = runTest( mTestName, pixelDiff );
8783

88-
QString myDashMessage = "<DartMeasurementFile name=\"Rendered Image " + mTestName + "\""
89-
" type=\"image/png\">" + renderedFilePath +
90-
"</DartMeasurementFile>"
91-
"<DartMeasurementFile name=\"Expected Image " + mTestName + "\" type=\"image/png\">" +
92-
mExpectedImageFile + "</DartMeasurementFile>"
93-
"<DartMeasurementFile name=\"Difference Image " + mTestName + "\" type=\"image/png\">" +
94-
diffFilePath + "</DartMeasurementFile>";
95-
qDebug() << myDashMessage;
84+
theReport += report();
9685

97-
report += mReport;
9886
return testResult;
9987
}

‎tests/src/core/qgscompositionchecker.h

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@
1616
#ifndef QGSCOMPOSITIONCHECKER_H
1717
#define QGSCOMPOSITIONCHECKER_H
1818

19-
#include "qgsrenderchecker.h"
19+
#include "qgsmultirenderchecker.h"
2020
#include <QString>
2121

2222
class QgsComposition;
2323
class QImage;
2424

2525
/**Renders a composition to an image and compares with an expected output*/
26-
class QgsCompositionChecker : public QgsRenderChecker
26+
class QgsCompositionChecker : public QgsMultiRenderChecker
2727
{
2828
public:
2929
QgsCompositionChecker( const QString& testName, QgsComposition* composition );
@@ -36,8 +36,8 @@ class QgsCompositionChecker : public QgsRenderChecker
3636

3737
QString mTestName;
3838
QgsComposition* mComposition;
39-
40-
39+
QSize mSize;
40+
int mDotsPerMeter;
4141
};
4242

4343
#endif // QGSCOMPOSITIONCHECKER_H

0 commit comments

Comments
 (0)
Please sign in to comment.