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 | #include "baselineprotocol.h" |
29 | #include <QLibraryInfo> |
30 | #include <QImage> |
31 | #include <QBuffer> |
32 | #include <QHostInfo> |
33 | #include <QSysInfo> |
34 | #if QT_CONFIG(process) |
35 | # include <QProcess> |
36 | #endif |
37 | #include <QFileInfo> |
38 | #include <QDir> |
39 | #include <QTime> |
40 | #include <QPointer> |
41 | #include <QRegExp> |
42 | |
43 | const QString PI_Project(QLS("Project" )); |
44 | const QString PI_TestCase(QLS("TestCase" )); |
45 | const QString PI_HostName(QLS("HostName" )); |
46 | const QString PI_HostAddress(QLS("HostAddress" )); |
47 | const QString PI_OSName(QLS("OSName" )); |
48 | const QString PI_OSVersion(QLS("OSVersion" )); |
49 | const QString PI_QtVersion(QLS("QtVersion" )); |
50 | const QString PI_QtBuildMode(QLS("QtBuildMode" )); |
51 | const QString PI_GitCommit(QLS("GitCommit" )); |
52 | const QString PI_QMakeSpec(QLS("QMakeSpec" )); |
53 | const QString PI_PulseGitBranch(QLS("PulseGitBranch" )); |
54 | const QString PI_PulseTestrBranch(QLS("PulseTestrBranch" )); |
55 | |
56 | #ifndef QMAKESPEC |
57 | #define QMAKESPEC "Unknown" |
58 | #endif |
59 | |
60 | #if defined(Q_OS_WIN) |
61 | #include <QtCore/qt_windows.h> |
62 | #endif |
63 | #if defined(Q_OS_UNIX) |
64 | #include <time.h> |
65 | #endif |
66 | void BaselineProtocol::sysSleep(int ms) |
67 | { |
68 | #if defined(Q_OS_WIN) |
69 | # ifndef Q_OS_WINRT |
70 | Sleep(DWORD(ms)); |
71 | # else |
72 | WaitForSingleObjectEx(GetCurrentThread(), ms, false); |
73 | # endif |
74 | #else |
75 | struct timespec ts = { .tv_sec: ms / 1000, .tv_nsec: (ms % 1000) * 1000 * 1000 }; |
76 | nanosleep(requested_time: &ts, NULL); |
77 | #endif |
78 | } |
79 | |
80 | PlatformInfo::PlatformInfo() |
81 | : QMap<QString, QString>(), adHoc(true) |
82 | { |
83 | } |
84 | |
85 | PlatformInfo PlatformInfo::localHostInfo() |
86 | { |
87 | PlatformInfo pi; |
88 | pi.insert(akey: PI_HostName, avalue: QHostInfo::localHostName()); |
89 | pi.insert(akey: PI_QtVersion, QLS(qVersion())); |
90 | pi.insert(akey: PI_QMakeSpec, avalue: QString(QLS(QMAKESPEC)).remove(rx: QRegExp(QLS("^.*mkspecs/" )))); |
91 | #if QT_VERSION >= 0x050000 |
92 | pi.insert(akey: PI_QtBuildMode, avalue: QLibraryInfo::isDebugBuild() ? QLS("QtDebug" ) : QLS("QtRelease" )); |
93 | #endif |
94 | #if defined(Q_OS_LINUX) && QT_CONFIG(process) |
95 | pi.insert(akey: PI_OSName, QLS("Linux" )); |
96 | #elif defined(Q_OS_WIN) |
97 | pi.insert(PI_OSName, QLS("Windows" )); |
98 | #elif defined(Q_OS_DARWIN) |
99 | pi.insert(PI_OSName, QLS("Darwin" )); |
100 | #else |
101 | pi.insert(PI_OSName, QLS("Other" )); |
102 | #endif |
103 | pi.insert(akey: PI_OSVersion, avalue: QSysInfo::kernelVersion()); |
104 | |
105 | #if QT_CONFIG(process) |
106 | QProcess git; |
107 | QString cmd; |
108 | QStringList args; |
109 | #if defined(Q_OS_WIN) |
110 | cmd = QLS("cmd.exe" ); |
111 | args << QLS("/c" ) << QLS("git" ); |
112 | #else |
113 | cmd = QLS("git" ); |
114 | #endif |
115 | args << QLS("log" ) << QLS("--max-count=1" ) << QLS("--pretty=%H [%an] [%ad] %s" ); |
116 | git.start(program: cmd, arguments: args); |
117 | git.waitForFinished(msecs: 3000); |
118 | if (!git.exitCode()) |
119 | pi.insert(akey: PI_GitCommit, avalue: QString::fromLocal8Bit(str: git.readAllStandardOutput().constData()).simplified()); |
120 | else |
121 | pi.insert(akey: PI_GitCommit, QLS("Unknown" )); |
122 | |
123 | QByteArray gb = qgetenv(varName: "PULSE_GIT_BRANCH" ); |
124 | if (!gb.isEmpty()) { |
125 | pi.insert(akey: PI_PulseGitBranch, avalue: QString::fromLatin1(str: gb)); |
126 | pi.setAdHocRun(false); |
127 | } |
128 | QByteArray tb = qgetenv(varName: "PULSE_TESTR_BRANCH" ); |
129 | if (!tb.isEmpty()) { |
130 | pi.insert(akey: PI_PulseTestrBranch, avalue: QString::fromLatin1(str: tb)); |
131 | pi.setAdHocRun(false); |
132 | } |
133 | if (!qgetenv(varName: "JENKINS_HOME" ).isEmpty()) { |
134 | pi.setAdHocRun(false); |
135 | gb = qgetenv(varName: "GIT_BRANCH" ); |
136 | if (!gb.isEmpty()) { |
137 | // FIXME: the string "Pulse" should be eliminated, since that is not the used tool. |
138 | pi.insert(akey: PI_PulseGitBranch, avalue: QString::fromLatin1(str: gb)); |
139 | } |
140 | } |
141 | #endif // QT_CONFIG(process) |
142 | |
143 | return pi; |
144 | } |
145 | |
146 | |
147 | PlatformInfo::PlatformInfo(const PlatformInfo &other) |
148 | : QMap<QString, QString>(other) |
149 | { |
150 | orides = other.orides; |
151 | adHoc = other.adHoc; |
152 | } |
153 | |
154 | |
155 | PlatformInfo &PlatformInfo::operator=(const PlatformInfo &other) |
156 | { |
157 | QMap<QString, QString>::operator=(other); |
158 | orides = other.orides; |
159 | adHoc = other.adHoc; |
160 | return *this; |
161 | } |
162 | |
163 | |
164 | void PlatformInfo::addOverride(const QString& key, const QString& value) |
165 | { |
166 | orides.append(t: key); |
167 | orides.append(t: value); |
168 | } |
169 | |
170 | |
171 | QStringList PlatformInfo::overrides() const |
172 | { |
173 | return orides; |
174 | } |
175 | |
176 | |
177 | void PlatformInfo::setAdHocRun(bool isAdHoc) |
178 | { |
179 | adHoc = isAdHoc; |
180 | } |
181 | |
182 | |
183 | bool PlatformInfo::isAdHocRun() const |
184 | { |
185 | return adHoc; |
186 | } |
187 | |
188 | |
189 | QDataStream & operator<< (QDataStream &stream, const PlatformInfo &pi) |
190 | { |
191 | stream << static_cast<const QMap<QString, QString>&>(pi); |
192 | stream << pi.orides << pi.adHoc; |
193 | return stream; |
194 | } |
195 | |
196 | |
197 | QDataStream & operator>> (QDataStream &stream, PlatformInfo &pi) |
198 | { |
199 | stream >> static_cast<QMap<QString, QString>&>(pi); |
200 | stream >> pi.orides >> pi.adHoc; |
201 | return stream; |
202 | } |
203 | |
204 | |
205 | ImageItem &ImageItem::operator=(const ImageItem &other) |
206 | { |
207 | testFunction = other.testFunction; |
208 | itemName = other.itemName; |
209 | itemChecksum = other.itemChecksum; |
210 | status = other.status; |
211 | image = other.image; |
212 | imageChecksums = other.imageChecksums; |
213 | return *this; |
214 | } |
215 | |
216 | // Defined in lookup3.c: |
217 | void hashword2 ( |
218 | const quint32 *k, /* the key, an array of quint32 values */ |
219 | size_t length, /* the length of the key, in quint32s */ |
220 | quint32 *pc, /* IN: seed OUT: primary hash value */ |
221 | quint32 *pb); /* IN: more seed OUT: secondary hash value */ |
222 | |
223 | quint64 ImageItem::computeChecksum(const QImage &image) |
224 | { |
225 | QImage img(image); |
226 | const int bpl = img.bytesPerLine(); |
227 | const int padBytes = bpl - (img.width() * img.depth() / 8); |
228 | if (padBytes) { |
229 | uchar *p = img.bits() + bpl - padBytes; |
230 | const int h = img.height(); |
231 | for (int y = 0; y < h; ++y) { |
232 | memset(s: p, c: 0, n: padBytes); |
233 | p += bpl; |
234 | } |
235 | } |
236 | |
237 | quint32 h1 = 0xfeedbacc; |
238 | quint32 h2 = 0x21604894; |
239 | hashword2(k: (const quint32 *)img.constBits(), length: img.sizeInBytes()/4, pc: &h1, pb: &h2); |
240 | return (quint64(h1) << 32) | h2; |
241 | } |
242 | |
243 | #if 0 |
244 | QString ImageItem::engineAsString() const |
245 | { |
246 | switch (engine) { |
247 | case Raster: |
248 | return QLS("Raster" ); |
249 | break; |
250 | case OpenGL: |
251 | return QLS("OpenGL" ); |
252 | break; |
253 | default: |
254 | break; |
255 | } |
256 | return QLS("Unknown" ); |
257 | } |
258 | |
259 | QString ImageItem::formatAsString() const |
260 | { |
261 | static const int numFormats = 16; |
262 | static const char *formatNames[numFormats] = { |
263 | "Invalid" , |
264 | "Mono" , |
265 | "MonoLSB" , |
266 | "Indexed8" , |
267 | "RGB32" , |
268 | "ARGB32" , |
269 | "ARGB32-Premult" , |
270 | "RGB16" , |
271 | "ARGB8565-Premult" , |
272 | "RGB666" , |
273 | "ARGB6666-Premult" , |
274 | "RGB555" , |
275 | "ARGB8555-Premult" , |
276 | "RGB888" , |
277 | "RGB444" , |
278 | "ARGB4444-Premult" |
279 | }; |
280 | if (renderFormat < 0 || renderFormat >= numFormats) |
281 | return QLS("UnknownFormat" ); |
282 | return QLS(formatNames[renderFormat]); |
283 | } |
284 | #endif |
285 | |
286 | void ImageItem::writeImageToStream(QDataStream &out) const |
287 | { |
288 | if (image.isNull() || image.format() == QImage::Format_Invalid) { |
289 | out << quint8(0); |
290 | return; |
291 | } |
292 | out << quint8('Q') << quint8(image.format()); |
293 | out << quint8(QSysInfo::ByteOrder) << quint8(0); // pad to multiple of 4 bytes |
294 | out << quint32(image.width()) << quint32(image.height()) << quint32(image.bytesPerLine()); |
295 | out << qCompress(data: reinterpret_cast<const uchar *>(image.constBits()), |
296 | nbytes: int(image.sizeInBytes())); |
297 | //# can be followed by colormap for formats that use it |
298 | } |
299 | |
300 | void ImageItem::readImageFromStream(QDataStream &in) |
301 | { |
302 | quint8 hdr, fmt, endian, pad; |
303 | quint32 width, height, bpl; |
304 | QByteArray data; |
305 | |
306 | in >> hdr; |
307 | if (hdr != 'Q') { |
308 | image = QImage(); |
309 | return; |
310 | } |
311 | in >> fmt >> endian >> pad; |
312 | if (!fmt || fmt >= QImage::NImageFormats) { |
313 | image = QImage(); |
314 | return; |
315 | } |
316 | if (endian != QSysInfo::ByteOrder) { |
317 | qWarning(msg: "ImageItem cannot read streamed image with different endianness" ); |
318 | image = QImage(); |
319 | return; |
320 | } |
321 | in >> width >> height >> bpl; |
322 | in >> data; |
323 | data = qUncompress(data); |
324 | QImage res((const uchar *)data.constData(), width, height, bpl, QImage::Format(fmt)); |
325 | image = res.copy(); //# yuck, seems there is currently no way to avoid data copy |
326 | } |
327 | |
328 | QDataStream & operator<< (QDataStream &stream, const ImageItem &ii) |
329 | { |
330 | stream << ii.testFunction << ii.itemName << ii.itemChecksum << quint8(ii.status) << ii.imageChecksums << ii.misc; |
331 | ii.writeImageToStream(out&: stream); |
332 | return stream; |
333 | } |
334 | |
335 | QDataStream & operator>> (QDataStream &stream, ImageItem &ii) |
336 | { |
337 | quint8 encStatus; |
338 | stream >> ii.testFunction >> ii.itemName >> ii.itemChecksum >> encStatus >> ii.imageChecksums >> ii.misc; |
339 | ii.status = ImageItem::ItemStatus(encStatus); |
340 | ii.readImageFromStream(in&: stream); |
341 | return stream; |
342 | } |
343 | |
344 | BaselineProtocol::BaselineProtocol() |
345 | { |
346 | } |
347 | |
348 | BaselineProtocol::~BaselineProtocol() |
349 | { |
350 | disconnect(); |
351 | } |
352 | |
353 | bool BaselineProtocol::disconnect() |
354 | { |
355 | socket.close(); |
356 | return (socket.state() == QTcpSocket::UnconnectedState) ? true : socket.waitForDisconnected(msecs: Timeout); |
357 | } |
358 | |
359 | |
360 | bool BaselineProtocol::connect(const QString &testCase, bool *dryrun, const PlatformInfo& clientInfo) |
361 | { |
362 | errMsg.clear(); |
363 | QByteArray serverName(qgetenv(varName: "QT_LANCELOT_SERVER" )); |
364 | if (serverName.isNull()) |
365 | serverName = "lancelot.test.qt-project.org" ; |
366 | |
367 | socket.connectToHost(hostName: serverName, port: ServerPort); |
368 | if (!socket.waitForConnected(msecs: Timeout)) { |
369 | sysSleep(ms: 3000); // Wait a bit and try again, the server might just be restarting |
370 | if (!socket.waitForConnected(msecs: Timeout)) { |
371 | errMsg += QLS("TCP connectToHost failed. Host:" ) + QLS(serverName) + QLS(" port:" ) + QString::number(ServerPort); |
372 | return false; |
373 | } |
374 | } |
375 | |
376 | PlatformInfo pi = clientInfo.isEmpty() ? PlatformInfo::localHostInfo() : clientInfo; |
377 | pi.insert(akey: PI_TestCase, avalue: testCase); |
378 | QByteArray block; |
379 | QDataStream ds(&block, QIODevice::ReadWrite); |
380 | ds << pi; |
381 | if (!sendBlock(cmd: AcceptPlatformInfo, block)) { |
382 | errMsg += QLS("Failed to send data to server." ); |
383 | return false; |
384 | } |
385 | |
386 | Command cmd = UnknownError; |
387 | if (!receiveBlock(cmd: &cmd, block: &block)) { |
388 | errMsg.prepend(QLS("Failed to get response from server. " )); |
389 | return false; |
390 | } |
391 | |
392 | if (cmd == Abort) { |
393 | errMsg += QLS("Server rejected connection. Reason: " ) + QString::fromLatin1(str: block); |
394 | return false; |
395 | } |
396 | |
397 | if (dryrun) |
398 | *dryrun = (cmd == DoDryRun); |
399 | |
400 | if (cmd != Ack && cmd != DoDryRun) { |
401 | errMsg += QLS("Unexpected response from server." ); |
402 | return false; |
403 | } |
404 | |
405 | return true; |
406 | } |
407 | |
408 | |
409 | bool BaselineProtocol::acceptConnection(PlatformInfo *pi) |
410 | { |
411 | errMsg.clear(); |
412 | |
413 | QByteArray block; |
414 | Command cmd = AcceptPlatformInfo; |
415 | if (!receiveBlock(cmd: &cmd, block: &block) || cmd != AcceptPlatformInfo) |
416 | return false; |
417 | |
418 | if (pi) { |
419 | QDataStream ds(block); |
420 | ds >> *pi; |
421 | pi->insert(akey: PI_HostAddress, avalue: socket.peerAddress().toString()); |
422 | } |
423 | |
424 | return true; |
425 | } |
426 | |
427 | |
428 | bool BaselineProtocol::requestBaselineChecksums(const QString &testFunction, ImageItemList *itemList) |
429 | { |
430 | errMsg.clear(); |
431 | if (!itemList) |
432 | return false; |
433 | |
434 | for(ImageItemList::iterator it = itemList->begin(); it != itemList->end(); it++) |
435 | it->testFunction = testFunction; |
436 | |
437 | QByteArray block; |
438 | QDataStream ds(&block, QIODevice::WriteOnly); |
439 | ds << *itemList; |
440 | if (!sendBlock(cmd: RequestBaselineChecksums, block)) |
441 | return false; |
442 | |
443 | Command cmd; |
444 | QByteArray rcvBlock; |
445 | if (!receiveBlock(cmd: &cmd, block: &rcvBlock) || cmd != BaselineProtocol::Ack) |
446 | return false; |
447 | QDataStream rds(&rcvBlock, QIODevice::ReadOnly); |
448 | rds >> *itemList; |
449 | return true; |
450 | } |
451 | |
452 | |
453 | bool BaselineProtocol::submitMatch(const ImageItem &item, QByteArray *serverMsg) |
454 | { |
455 | Command cmd; |
456 | ImageItem smallItem = item; |
457 | smallItem.image = QImage(); // No need to waste bandwith sending image (identical to baseline) to server |
458 | return (sendItem(cmd: AcceptMatch, item: smallItem) && receiveBlock(cmd: &cmd, block: serverMsg) && cmd == Ack); |
459 | } |
460 | |
461 | |
462 | bool BaselineProtocol::submitNewBaseline(const ImageItem &item, QByteArray *serverMsg) |
463 | { |
464 | Command cmd; |
465 | return (sendItem(cmd: AcceptNewBaseline, item) && receiveBlock(cmd: &cmd, block: serverMsg) && cmd == Ack); |
466 | } |
467 | |
468 | |
469 | bool BaselineProtocol::submitMismatch(const ImageItem &item, QByteArray *serverMsg, bool *fuzzyMatch) |
470 | { |
471 | Command cmd; |
472 | if (sendItem(cmd: AcceptMismatch, item) && receiveBlock(cmd: &cmd, block: serverMsg) && (cmd == Ack || cmd == FuzzyMatch)) { |
473 | if (fuzzyMatch) |
474 | *fuzzyMatch = (cmd == FuzzyMatch); |
475 | return true; |
476 | } |
477 | return false; |
478 | } |
479 | |
480 | |
481 | bool BaselineProtocol::sendItem(Command cmd, const ImageItem &item) |
482 | { |
483 | errMsg.clear(); |
484 | QBuffer buf; |
485 | buf.open(openMode: QIODevice::WriteOnly); |
486 | QDataStream ds(&buf); |
487 | ds << item; |
488 | if (!sendBlock(cmd, block: buf.data())) { |
489 | errMsg.prepend(QLS("Failed to submit image to server. " )); |
490 | return false; |
491 | } |
492 | return true; |
493 | } |
494 | |
495 | |
496 | bool BaselineProtocol::sendBlock(Command cmd, const QByteArray &block) |
497 | { |
498 | QDataStream s(&socket); |
499 | // TBD: set qds version as a constant |
500 | s << quint16(ProtocolVersion) << quint16(cmd); |
501 | s.writeBytes(block.constData(), len: block.size()); |
502 | return true; |
503 | } |
504 | |
505 | |
506 | bool BaselineProtocol::receiveBlock(Command *cmd, QByteArray *block) |
507 | { |
508 | while (socket.bytesAvailable() < int(2*sizeof(quint16) + sizeof(quint32))) { |
509 | if (!socket.waitForReadyRead(msecs: Timeout)) |
510 | return false; |
511 | } |
512 | QDataStream ds(&socket); |
513 | quint16 rcvProtocolVersion, rcvCmd; |
514 | ds >> rcvProtocolVersion >> rcvCmd; |
515 | if (rcvProtocolVersion != ProtocolVersion) { |
516 | errMsg = QLS("Baseline protocol version mismatch, received:" ) + QString::number(rcvProtocolVersion) |
517 | + QLS(" expected:" ) + QString::number(ProtocolVersion); |
518 | return false; |
519 | } |
520 | if (cmd) |
521 | *cmd = Command(rcvCmd); |
522 | |
523 | QByteArray uMsg; |
524 | quint32 remaining; |
525 | ds >> remaining; |
526 | uMsg.resize(size: remaining); |
527 | int got = 0; |
528 | char* uMsgBuf = uMsg.data(); |
529 | do { |
530 | got = ds.readRawData(uMsgBuf, len: remaining); |
531 | remaining -= got; |
532 | uMsgBuf += got; |
533 | } while (remaining && got >= 0 && socket.waitForReadyRead(msecs: Timeout)); |
534 | |
535 | if (got < 0) |
536 | return false; |
537 | |
538 | if (block) |
539 | *block = uMsg; |
540 | |
541 | return true; |
542 | } |
543 | |
544 | |
545 | QString BaselineProtocol::errorMessage() |
546 | { |
547 | QString ret = errMsg; |
548 | if (socket.error() >= 0) |
549 | ret += QLS(" Socket state: " ) + socket.errorString(); |
550 | return ret; |
551 | } |
552 | |
553 | |