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
43const QString PI_Project(QLS("Project"));
44const QString PI_TestCase(QLS("TestCase"));
45const QString PI_HostName(QLS("HostName"));
46const QString PI_HostAddress(QLS("HostAddress"));
47const QString PI_OSName(QLS("OSName"));
48const QString PI_OSVersion(QLS("OSVersion"));
49const QString PI_QtVersion(QLS("QtVersion"));
50const QString PI_QtBuildMode(QLS("QtBuildMode"));
51const QString PI_GitCommit(QLS("GitCommit"));
52const QString PI_QMakeSpec(QLS("QMakeSpec"));
53const QString PI_PulseGitBranch(QLS("PulseGitBranch"));
54const 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
66void 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
80PlatformInfo::PlatformInfo()
81 : QMap<QString, QString>(), adHoc(true)
82{
83}
84
85PlatformInfo 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
147PlatformInfo::PlatformInfo(const PlatformInfo &other)
148 : QMap<QString, QString>(other)
149{
150 orides = other.orides;
151 adHoc = other.adHoc;
152}
153
154
155PlatformInfo &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
164void PlatformInfo::addOverride(const QString& key, const QString& value)
165{
166 orides.append(t: key);
167 orides.append(t: value);
168}
169
170
171QStringList PlatformInfo::overrides() const
172{
173 return orides;
174}
175
176
177void PlatformInfo::setAdHocRun(bool isAdHoc)
178{
179 adHoc = isAdHoc;
180}
181
182
183bool PlatformInfo::isAdHocRun() const
184{
185 return adHoc;
186}
187
188
189QDataStream & 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
197QDataStream & 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
205ImageItem &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:
217void hashword2 (
218const quint32 *k, /* the key, an array of quint32 values */
219size_t length, /* the length of the key, in quint32s */
220quint32 *pc, /* IN: seed OUT: primary hash value */
221quint32 *pb); /* IN: more seed OUT: secondary hash value */
222
223quint64 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
244QString 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
259QString 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
286void 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
300void 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
328QDataStream & 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
335QDataStream & 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
344BaselineProtocol::BaselineProtocol()
345{
346}
347
348BaselineProtocol::~BaselineProtocol()
349{
350 disconnect();
351}
352
353bool BaselineProtocol::disconnect()
354{
355 socket.close();
356 return (socket.state() == QTcpSocket::UnconnectedState) ? true : socket.waitForDisconnected(msecs: Timeout);
357}
358
359
360bool 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
409bool 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
428bool 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
453bool 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
462bool 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
469bool 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
481bool 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
496bool 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
506bool 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
545QString 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

source code of qtbase/tests/baselineserver/shared/baselineprotocol.cpp