1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2016 The Qt Company Ltd. |
4 | ** Contact: https://www.qt.io/licensing/ |
5 | ** |
6 | ** This file is part of the test suite of the Qt Toolkit. |
7 | ** |
8 | ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ |
9 | ** Commercial License Usage |
10 | ** Licensees holding valid commercial Qt licenses may use this file in |
11 | ** accordance with the commercial license agreement provided with the |
12 | ** Software or, alternatively, in accordance with the terms contained in |
13 | ** a written agreement between you and The Qt Company. For licensing terms |
14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
15 | ** information use the contact form at https://www.qt.io/contact-us. |
16 | ** |
17 | ** GNU General Public License Usage |
18 | ** Alternatively, this file may be used under the terms of the GNU |
19 | ** General Public License version 3 as published by the Free Software |
20 | ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT |
21 | ** included in the packaging of this file. Please review the following |
22 | ** information to ensure the GNU General Public License requirements will |
23 | ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
24 | ** |
25 | ** $QT_END_LICENSE$ |
26 | ** |
27 | ****************************************************************************/ |
28 | |
29 | #include "qbaselinetest.h" |
30 | #include "baselineprotocol.h" |
31 | #include <QtCore/QDir> |
32 | #include <QFile> |
33 | |
34 | #define MAXCMDLINEARGS 128 |
35 | |
36 | namespace QBaselineTest { |
37 | |
38 | static char *fargv[MAXCMDLINEARGS]; |
39 | static bool simfail = false; |
40 | static PlatformInfo customInfo; |
41 | static bool customAutoModeSet = false; |
42 | |
43 | static BaselineProtocol proto; |
44 | static bool connected = false; |
45 | static bool triedConnecting = false; |
46 | static bool dryRunMode = false; |
47 | static enum { UploadMissing, UploadAll, UploadNone } baselinePolicy = UploadMissing; |
48 | |
49 | static QByteArray curFunction; |
50 | static ImageItemList itemList; |
51 | static bool gotBaselines; |
52 | |
53 | static QString definedTestProject; |
54 | static QString definedTestCase; |
55 | |
56 | |
57 | void handleCmdLineArgs(int *argcp, char ***argvp) |
58 | { |
59 | if (!argcp || !argvp) |
60 | return; |
61 | |
62 | bool showHelp = false; |
63 | |
64 | int fargc = 0; |
65 | int numArgs = *argcp; |
66 | |
67 | for (int i = 0; i < numArgs; i++) { |
68 | QByteArray arg = (*argvp)[i]; |
69 | QByteArray nextArg = (i+1 < numArgs) ? (*argvp)[i+1] : 0; |
70 | |
71 | if (arg == "-simfail" ) { |
72 | simfail = true; |
73 | } else if (arg == "-fuzzlevel" ) { |
74 | i++; |
75 | bool ok = false; |
76 | (void)nextArg.toInt(ok: &ok); |
77 | if (!ok) { |
78 | qWarning() << "-fuzzlevel requires integer parameter" ; |
79 | showHelp = true; |
80 | break; |
81 | } |
82 | customInfo.insert(akey: "FuzzLevel" , avalue: QString::fromLatin1(str: nextArg)); |
83 | } else if (arg == "-auto" ) { |
84 | customAutoModeSet = true; |
85 | customInfo.setAdHocRun(false); |
86 | } else if (arg == "-adhoc" ) { |
87 | customAutoModeSet = true; |
88 | customInfo.setAdHocRun(true); |
89 | } else if (arg == "-setbaselines" ) { |
90 | baselinePolicy = UploadAll; |
91 | } else if (arg == "-nosetbaselines" ) { |
92 | baselinePolicy = UploadNone; |
93 | } else if (arg == "-compareto" ) { |
94 | i++; |
95 | int split = qMax(a: 0, b: nextArg.indexOf(c: '=')); |
96 | QByteArray key = nextArg.left(len: split).trimmed(); |
97 | QByteArray value = nextArg.mid(index: split+1).trimmed(); |
98 | if (key.isEmpty() || value.isEmpty()) { |
99 | qWarning() << "-compareto requires parameter of the form <key>=<value>" ; |
100 | showHelp = true; |
101 | break; |
102 | } |
103 | customInfo.addOverride(key, value); |
104 | } else { |
105 | if ( (arg == "-help" ) || (arg == "--help" ) ) |
106 | showHelp = true; |
107 | if (fargc >= MAXCMDLINEARGS) { |
108 | qWarning() << "Too many command line arguments!" ; |
109 | break; |
110 | } |
111 | fargv[fargc++] = (*argvp)[i]; |
112 | } |
113 | } |
114 | *argcp = fargc; |
115 | *argvp = fargv; |
116 | |
117 | if (showHelp) { |
118 | // TBD: arrange for this to be printed *after* QTest's help |
119 | QTextStream out(stdout); |
120 | out << "\n Baseline testing (lancelot) options:\n" ; |
121 | out << " -simfail : Force an image comparison mismatch. For testing purposes.\n" ; |
122 | out << " -fuzzlevel <int> : Specify the percentage of fuzziness in comparison. Overrides server default. 0 means exact match.\n" ; |
123 | out << " -auto : Inform server that this run is done by a daemon, CI system or similar.\n" ; |
124 | out << " -adhoc (default) : The inverse of -auto; this run is done by human, e.g. for testing.\n" ; |
125 | out << " -setbaselines : Store ALL rendered images as new baselines. Forces replacement of previous baselines.\n" ; |
126 | out << " -nosetbaselines : Do not store rendered images as new baselines when previous baselines are missing.\n" ; |
127 | out << " -compareto KEY=VAL : Force comparison to baselines from a different client,\n" ; |
128 | out << " for example: -compareto QtVersion=4.8.0\n" ; |
129 | out << " Multiple -compareto client specifications may be given.\n" ; |
130 | out << "\n" ; |
131 | } |
132 | } |
133 | |
134 | |
135 | void addClientProperty(const QString& key, const QString& value) |
136 | { |
137 | customInfo.insert(akey: key, avalue: value); |
138 | } |
139 | |
140 | |
141 | /* |
142 | If a client property script is present, run it and accept its output |
143 | in the form of one 'key: value' property per line |
144 | */ |
145 | void fetchCustomClientProperties() |
146 | { |
147 | QFile file("hostinfo.txt" ); |
148 | if (!file.open(flags: QIODevice::ReadOnly | QIODevice::Text)) |
149 | return; |
150 | QTextStream in(&file); |
151 | |
152 | while (!in.atEnd()) { |
153 | QString line = in.readLine().trimmed(); // ###local8bit? utf8? |
154 | if (line.startsWith(c: QLatin1Char('#'))) // Ignore comments in file |
155 | continue; |
156 | QString key, val; |
157 | int colonPos = line.indexOf(c: ':'); |
158 | if (colonPos > 0) { |
159 | key = line.left(n: colonPos).simplified().replace(before: ' ', after: '_'); |
160 | val = line.mid(position: colonPos+1).trimmed(); |
161 | } |
162 | if (!key.isEmpty() && key.length() < 64 && val.length() < 256) // ###TBD: maximum 256 chars in value? |
163 | addClientProperty(key, value: val); |
164 | else |
165 | qDebug() << "Unparseable script output ignored:" << line; |
166 | } |
167 | } |
168 | |
169 | |
170 | bool connect(QByteArray *msg, bool *error) |
171 | { |
172 | if (connected) { |
173 | return true; |
174 | } |
175 | else if (triedConnecting) { |
176 | // Avoid repeated connection attempts, to avoid the program using Timeout * #testItems seconds before giving up |
177 | *msg = "Not connected to baseline server." ; |
178 | *error = true; |
179 | return false; |
180 | } |
181 | |
182 | triedConnecting = true; |
183 | fetchCustomClientProperties(); |
184 | // Merge the platform info set by the program with the protocols default info |
185 | PlatformInfo clientInfo = customInfo; |
186 | PlatformInfo defaultInfo = PlatformInfo::localHostInfo(); |
187 | foreach (QString key, defaultInfo.keys()) { |
188 | if (!clientInfo.contains(akey: key)) |
189 | clientInfo.insert(akey: key, avalue: defaultInfo.value(akey: key)); |
190 | } |
191 | if (!customAutoModeSet) |
192 | clientInfo.setAdHocRun(defaultInfo.isAdHocRun()); |
193 | |
194 | if (!definedTestProject.isEmpty()) |
195 | clientInfo.insert(akey: PI_Project, avalue: definedTestProject); |
196 | |
197 | QString testCase = definedTestCase; |
198 | if (testCase.isEmpty() && QTest::testObject() && QTest::testObject()->metaObject()) { |
199 | //qDebug() << "Trying to Read TestCaseName from Testlib!"; |
200 | testCase = QTest::testObject()->metaObject()->className(); |
201 | } |
202 | if (testCase.isEmpty()) { |
203 | qWarning(msg: "QBaselineTest::connect: No test case name specified, cannot connect." ); |
204 | return false; |
205 | } |
206 | |
207 | if (!proto.connect(testCase, dryrun: &dryRunMode, clientInfo)) { |
208 | *msg += "Failed to connect to baseline server: " + proto.errorMessage().toLatin1(); |
209 | *error = true; |
210 | return false; |
211 | } |
212 | connected = true; |
213 | return true; |
214 | } |
215 | |
216 | bool disconnectFromBaselineServer() |
217 | { |
218 | if (proto.disconnect()) { |
219 | connected = false; |
220 | triedConnecting = false; |
221 | return true; |
222 | } |
223 | |
224 | return false; |
225 | } |
226 | |
227 | bool connectToBaselineServer(QByteArray *msg, const QString &testProject, const QString &testCase) |
228 | { |
229 | bool dummy; |
230 | QByteArray dummyMsg; |
231 | |
232 | definedTestProject = testProject; |
233 | definedTestCase = testCase; |
234 | |
235 | return connect(msg: msg ? msg : &dummyMsg, error: &dummy); |
236 | } |
237 | |
238 | void setAutoMode(bool mode) |
239 | { |
240 | customInfo.setAdHocRun(!mode); |
241 | customAutoModeSet = true; |
242 | } |
243 | |
244 | void setSimFail(bool fail) |
245 | { |
246 | simfail = fail; |
247 | } |
248 | |
249 | |
250 | void modifyImage(QImage *img) |
251 | { |
252 | uint c0 = 0x0000ff00; |
253 | uint c1 = 0x0080ff00; |
254 | img->setPixel(x: 1,y: 1,index_or_rgb: c0); |
255 | img->setPixel(x: 2,y: 1,index_or_rgb: c1); |
256 | img->setPixel(x: 3,y: 1,index_or_rgb: c0); |
257 | img->setPixel(x: 1,y: 2,index_or_rgb: c1); |
258 | img->setPixel(x: 1,y: 3,index_or_rgb: c0); |
259 | img->setPixel(x: 2,y: 3,index_or_rgb: c1); |
260 | img->setPixel(x: 3,y: 3,index_or_rgb: c0); |
261 | img->setPixel(x: 1,y: 4,index_or_rgb: c1); |
262 | img->setPixel(x: 1,y: 5,index_or_rgb: c0); |
263 | } |
264 | |
265 | |
266 | bool compareItem(const ImageItem &baseline, const QImage &img, QByteArray *msg, bool *error) |
267 | { |
268 | ImageItem item = baseline; |
269 | if (simfail) { |
270 | // Simulate test failure by forcing image mismatch; for testing purposes |
271 | QImage misImg = img; |
272 | modifyImage(img: &misImg); |
273 | item.image = misImg; |
274 | simfail = false; // One failure is typically enough |
275 | } else { |
276 | item.image = img; |
277 | } |
278 | item.imageChecksums.clear(); |
279 | item.imageChecksums.prepend(t: ImageItem::computeChecksum(image: item.image)); |
280 | QByteArray srvMsg; |
281 | switch (baseline.status) { |
282 | case ImageItem::Ok: |
283 | break; |
284 | case ImageItem::IgnoreItem : |
285 | qDebug() << msg->constData() << "Ignored, blacklisted on server." ; |
286 | return true; |
287 | break; |
288 | case ImageItem::BaselineNotFound: |
289 | if (!customInfo.overrides().isEmpty() || baselinePolicy == UploadNone) { |
290 | qWarning() << "Cannot compare to baseline: No such baseline found on server." ; |
291 | return true; |
292 | } |
293 | if (proto.submitNewBaseline(item, serverMsg: &srvMsg)) |
294 | qDebug() << msg->constData() << "Baseline not found on server. New baseline uploaded." ; |
295 | else |
296 | qDebug() << msg->constData() << "Baseline not found on server. Uploading of new baseline failed:" << srvMsg; |
297 | return true; |
298 | break; |
299 | default: |
300 | qWarning() << "Unexpected reply from baseline server." ; |
301 | return true; |
302 | break; |
303 | } |
304 | *error = false; |
305 | // The actual comparison of the given image with the baseline: |
306 | if (baseline.imageChecksums.contains(t: item.imageChecksums.at(i: 0))) { |
307 | if (!proto.submitMatch(item, serverMsg: &srvMsg)) |
308 | qWarning() << "Failed to report image match to server:" << srvMsg; |
309 | return true; |
310 | } |
311 | // At this point, we have established a legitimate mismatch |
312 | if (baselinePolicy == UploadAll) { |
313 | if (proto.submitNewBaseline(item, serverMsg: &srvMsg)) |
314 | qDebug() << msg->constData() << "Forcing new baseline; uploaded ok." ; |
315 | else |
316 | qDebug() << msg->constData() << "Forcing new baseline; uploading failed:" << srvMsg; |
317 | return true; |
318 | } |
319 | bool fuzzyMatch = false; |
320 | bool res = proto.submitMismatch(item, serverMsg: &srvMsg, fuzzyMatch: &fuzzyMatch); |
321 | if (res && fuzzyMatch) { |
322 | *error = true; // To force a QSKIP/debug output; somewhat kludgy |
323 | *msg += srvMsg; |
324 | return true; // The server decides: a fuzzy match means no mismatch |
325 | } |
326 | *msg += "Mismatch. See report:\n " + srvMsg; |
327 | if (dryRunMode) { |
328 | qDebug() << "Dryrun, so ignoring" << *msg; |
329 | return true; |
330 | } |
331 | return false; |
332 | } |
333 | |
334 | bool checkImage(const QImage &img, const char *name, quint16 checksum, QByteArray *msg, bool *error, int manualdatatag) |
335 | { |
336 | if (!connected && !connect(msg, error)) |
337 | return true; |
338 | |
339 | QByteArray itemName; |
340 | bool hasName = qstrlen(str: name); |
341 | |
342 | const char *tag = QTest::currentDataTag(); |
343 | if (qstrlen(str: tag)) { |
344 | itemName = tag; |
345 | if (hasName) |
346 | itemName.append(c: '_').append(s: name); |
347 | } else { |
348 | itemName = hasName ? name : "default_name" ; |
349 | } |
350 | |
351 | if (manualdatatag > 0) |
352 | { |
353 | itemName.prepend(s: "_" ); |
354 | itemName.prepend(a: QByteArray::number(manualdatatag)); |
355 | } |
356 | |
357 | *msg = "Baseline check of image '" + itemName + "': " ; |
358 | |
359 | |
360 | ImageItem item; |
361 | item.itemName = QString::fromLatin1(str: itemName); |
362 | item.itemChecksum = checksum; |
363 | item.testFunction = QString::fromLatin1(str: QTest::currentTestFunction()); |
364 | ImageItemList list; |
365 | list.append(t: item); |
366 | if (!proto.requestBaselineChecksums(testFunction: QLatin1String(QTest::currentTestFunction()), itemList: &list) || list.isEmpty()) { |
367 | *msg = "Communication with baseline server failed: " + proto.errorMessage().toLatin1(); |
368 | *error = true; |
369 | return true; |
370 | } |
371 | |
372 | return compareItem(baseline: list.at(i: 0), img, msg, error); |
373 | } |
374 | |
375 | |
376 | QTestData &newRow(const char *dataTag, quint16 checksum) |
377 | { |
378 | if (QTest::currentTestFunction() != curFunction) { |
379 | curFunction = QTest::currentTestFunction(); |
380 | itemList.clear(); |
381 | gotBaselines = false; |
382 | } |
383 | ImageItem item; |
384 | item.itemName = QString::fromLatin1(str: dataTag); |
385 | item.itemChecksum = checksum; |
386 | item.testFunction = QString::fromLatin1(str: QTest::currentTestFunction()); |
387 | itemList.append(t: item); |
388 | |
389 | return QTest::newRow(dataTag); |
390 | } |
391 | |
392 | |
393 | bool testImage(const QImage& img, QByteArray *msg, bool *error) |
394 | { |
395 | if (!connected && !connect(msg, error)) |
396 | return true; |
397 | |
398 | if (QTest::currentTestFunction() != curFunction || itemList.isEmpty()) { |
399 | qWarning() << "Usage error: QBASELINE_TEST used without corresponding QBaselineTest::newRow()" ; |
400 | return true; |
401 | } |
402 | |
403 | if (!gotBaselines) { |
404 | if (!proto.requestBaselineChecksums(testFunction: QString::fromLatin1(str: QTest::currentTestFunction()), itemList: &itemList) || itemList.isEmpty()) { |
405 | *msg = "Communication with baseline server failed: " + proto.errorMessage().toLatin1(); |
406 | *error = true; |
407 | return true; |
408 | } |
409 | gotBaselines = true; |
410 | } |
411 | |
412 | QString curTag = QString::fromLatin1(str: QTest::currentDataTag()); |
413 | ImageItemList::const_iterator it = itemList.constBegin(); |
414 | while (it != itemList.constEnd() && it->itemName != curTag) |
415 | ++it; |
416 | if (it == itemList.constEnd()) { |
417 | qWarning() << "Usage error: QBASELINE_TEST used without corresponding QBaselineTest::newRow() for row" << curTag; |
418 | return true; |
419 | } |
420 | return compareItem(baseline: *it, img, msg, error); |
421 | } |
422 | |
423 | } |
424 | |