| /**************************************************************************** |
| ** |
| ** Copyright (C) 2016 The Qt Company Ltd. |
| ** Contact: https://www.qt.io/licensing/ |
| ** |
| ** This file is part of the test suite of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ |
| ** Commercial License Usage |
| ** Licensees holding valid commercial Qt licenses may use this file in |
| ** accordance with the commercial license agreement provided with the |
| ** Software or, alternatively, in accordance with the terms contained in |
| ** a written agreement between you and The Qt Company. For licensing terms |
| ** and conditions see https://www.qt.io/terms-conditions. For further |
| ** information use the contact form at https://www.qt.io/contact-us. |
| ** |
| ** GNU General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU |
| ** General Public License version 3 as published by the Free Software |
| ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT |
| ** included in the packaging of this file. Please review the following |
| ** information to ensure the GNU General Public License requirements will |
| ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
| ** |
| ** $QT_END_LICENSE$ |
| ** |
| ****************************************************************************/ |
| #include "baselineprotocol.h" |
| #include <QLibraryInfo> |
| #include <QImage> |
| #include <QBuffer> |
| #include <QHostInfo> |
| #include <QSysInfo> |
| #if QT_CONFIG(process) |
| # include <QProcess> |
| #endif |
| #include <QFileInfo> |
| #include <QDir> |
| #include <QTime> |
| #include <QPointer> |
| #include <QRegExp> |
| |
| const QString PI_Project(QLS("Project")); |
| const QString PI_TestCase(QLS("TestCase")); |
| const QString PI_HostName(QLS("HostName")); |
| const QString PI_HostAddress(QLS("HostAddress")); |
| const QString PI_OSName(QLS("OSName")); |
| const QString PI_OSVersion(QLS("OSVersion")); |
| const QString PI_QtVersion(QLS("QtVersion")); |
| const QString PI_QtBuildMode(QLS("QtBuildMode")); |
| const QString PI_GitCommit(QLS("GitCommit")); |
| const QString PI_QMakeSpec(QLS("QMakeSpec")); |
| const QString PI_PulseGitBranch(QLS("PulseGitBranch")); |
| const QString PI_PulseTestrBranch(QLS("PulseTestrBranch")); |
| |
| #ifndef QMAKESPEC |
| #define QMAKESPEC "Unknown" |
| #endif |
| |
| #if defined(Q_OS_WIN) |
| #include <QtCore/qt_windows.h> |
| #endif |
| #if defined(Q_OS_UNIX) |
| #include <time.h> |
| #endif |
| void BaselineProtocol::sysSleep(int ms) |
| { |
| #if defined(Q_OS_WIN) |
| # ifndef Q_OS_WINRT |
| Sleep(DWORD(ms)); |
| # else |
| WaitForSingleObjectEx(GetCurrentThread(), ms, false); |
| # endif |
| #else |
| struct timespec ts = { ms / 1000, (ms % 1000) * 1000 * 1000 }; |
| nanosleep(&ts, NULL); |
| #endif |
| } |
| |
| PlatformInfo::PlatformInfo() |
| : QMap<QString, QString>(), adHoc(true) |
| { |
| } |
| |
| PlatformInfo PlatformInfo::localHostInfo() |
| { |
| PlatformInfo pi; |
| pi.insert(PI_HostName, QHostInfo::localHostName()); |
| pi.insert(PI_QtVersion, QLS(qVersion())); |
| pi.insert(PI_QMakeSpec, QString(QLS(QMAKESPEC)).remove(QRegExp(QLS("^.*mkspecs/")))); |
| #if QT_VERSION >= 0x050000 |
| pi.insert(PI_QtBuildMode, QLibraryInfo::isDebugBuild() ? QLS("QtDebug") : QLS("QtRelease")); |
| #endif |
| #if defined(Q_OS_LINUX) && QT_CONFIG(process) |
| pi.insert(PI_OSName, QLS("Linux")); |
| #elif defined(Q_OS_WIN) |
| pi.insert(PI_OSName, QLS("Windows")); |
| #elif defined(Q_OS_DARWIN) |
| pi.insert(PI_OSName, QLS("Darwin")); |
| #else |
| pi.insert(PI_OSName, QLS("Other")); |
| #endif |
| pi.insert(PI_OSVersion, QSysInfo::kernelVersion()); |
| |
| #if QT_CONFIG(process) |
| QProcess git; |
| QString cmd; |
| QStringList args; |
| #if defined(Q_OS_WIN) |
| cmd = QLS("cmd.exe"); |
| args << QLS("/c") << QLS("git"); |
| #else |
| cmd = QLS("git"); |
| #endif |
| args << QLS("log") << QLS("--max-count=1") << QLS("--pretty=%H [%an] [%ad] %s"); |
| git.start(cmd, args); |
| git.waitForFinished(3000); |
| if (!git.exitCode()) |
| pi.insert(PI_GitCommit, QString::fromLocal8Bit(git.readAllStandardOutput().constData()).simplified()); |
| else |
| pi.insert(PI_GitCommit, QLS("Unknown")); |
| |
| QByteArray gb = qgetenv("PULSE_GIT_BRANCH"); |
| if (!gb.isEmpty()) { |
| pi.insert(PI_PulseGitBranch, QString::fromLatin1(gb)); |
| pi.setAdHocRun(false); |
| } |
| QByteArray tb = qgetenv("PULSE_TESTR_BRANCH"); |
| if (!tb.isEmpty()) { |
| pi.insert(PI_PulseTestrBranch, QString::fromLatin1(tb)); |
| pi.setAdHocRun(false); |
| } |
| if (!qgetenv("JENKINS_HOME").isEmpty()) { |
| pi.setAdHocRun(false); |
| gb = qgetenv("GIT_BRANCH"); |
| if (!gb.isEmpty()) { |
| // FIXME: the string "Pulse" should be eliminated, since that is not the used tool. |
| pi.insert(PI_PulseGitBranch, QString::fromLatin1(gb)); |
| } |
| } |
| #endif // QT_CONFIG(process) |
| |
| return pi; |
| } |
| |
| |
| PlatformInfo::PlatformInfo(const PlatformInfo &other) |
| : QMap<QString, QString>(other) |
| { |
| orides = other.orides; |
| adHoc = other.adHoc; |
| } |
| |
| |
| PlatformInfo &PlatformInfo::operator=(const PlatformInfo &other) |
| { |
| QMap<QString, QString>::operator=(other); |
| orides = other.orides; |
| adHoc = other.adHoc; |
| return *this; |
| } |
| |
| |
| void PlatformInfo::addOverride(const QString& key, const QString& value) |
| { |
| orides.append(key); |
| orides.append(value); |
| } |
| |
| |
| QStringList PlatformInfo::overrides() const |
| { |
| return orides; |
| } |
| |
| |
| void PlatformInfo::setAdHocRun(bool isAdHoc) |
| { |
| adHoc = isAdHoc; |
| } |
| |
| |
| bool PlatformInfo::isAdHocRun() const |
| { |
| return adHoc; |
| } |
| |
| |
| QDataStream & operator<< (QDataStream &stream, const PlatformInfo &pi) |
| { |
| stream << static_cast<const QMap<QString, QString>&>(pi); |
| stream << pi.orides << pi.adHoc; |
| return stream; |
| } |
| |
| |
| QDataStream & operator>> (QDataStream &stream, PlatformInfo &pi) |
| { |
| stream >> static_cast<QMap<QString, QString>&>(pi); |
| stream >> pi.orides >> pi.adHoc; |
| return stream; |
| } |
| |
| |
| ImageItem &ImageItem::operator=(const ImageItem &other) |
| { |
| testFunction = other.testFunction; |
| itemName = other.itemName; |
| itemChecksum = other.itemChecksum; |
| status = other.status; |
| image = other.image; |
| imageChecksums = other.imageChecksums; |
| return *this; |
| } |
| |
| // Defined in lookup3.c: |
| void hashword2 ( |
| const quint32 *k, /* the key, an array of quint32 values */ |
| size_t length, /* the length of the key, in quint32s */ |
| quint32 *pc, /* IN: seed OUT: primary hash value */ |
| quint32 *pb); /* IN: more seed OUT: secondary hash value */ |
| |
| quint64 ImageItem::computeChecksum(const QImage &image) |
| { |
| QImage img(image); |
| const int bpl = img.bytesPerLine(); |
| const int padBytes = bpl - (img.width() * img.depth() / 8); |
| if (padBytes) { |
| uchar *p = img.bits() + bpl - padBytes; |
| const int h = img.height(); |
| for (int y = 0; y < h; ++y) { |
| memset(p, 0, padBytes); |
| p += bpl; |
| } |
| } |
| |
| quint32 h1 = 0xfeedbacc; |
| quint32 h2 = 0x21604894; |
| hashword2((const quint32 *)img.constBits(), img.sizeInBytes()/4, &h1, &h2); |
| return (quint64(h1) << 32) | h2; |
| } |
| |
| #if 0 |
| QString ImageItem::engineAsString() const |
| { |
| switch (engine) { |
| case Raster: |
| return QLS("Raster"); |
| break; |
| case OpenGL: |
| return QLS("OpenGL"); |
| break; |
| default: |
| break; |
| } |
| return QLS("Unknown"); |
| } |
| |
| QString ImageItem::formatAsString() const |
| { |
| static const int numFormats = 16; |
| static const char *formatNames[numFormats] = { |
| "Invalid", |
| "Mono", |
| "MonoLSB", |
| "Indexed8", |
| "RGB32", |
| "ARGB32", |
| "ARGB32-Premult", |
| "RGB16", |
| "ARGB8565-Premult", |
| "RGB666", |
| "ARGB6666-Premult", |
| "RGB555", |
| "ARGB8555-Premult", |
| "RGB888", |
| "RGB444", |
| "ARGB4444-Premult" |
| }; |
| if (renderFormat < 0 || renderFormat >= numFormats) |
| return QLS("UnknownFormat"); |
| return QLS(formatNames[renderFormat]); |
| } |
| #endif |
| |
| void ImageItem::writeImageToStream(QDataStream &out) const |
| { |
| if (image.isNull() || image.format() == QImage::Format_Invalid) { |
| out << quint8(0); |
| return; |
| } |
| out << quint8('Q') << quint8(image.format()); |
| out << quint8(QSysInfo::ByteOrder) << quint8(0); // pad to multiple of 4 bytes |
| out << quint32(image.width()) << quint32(image.height()) << quint32(image.bytesPerLine()); |
| out << qCompress(reinterpret_cast<const uchar *>(image.constBits()), |
| int(image.sizeInBytes())); |
| //# can be followed by colormap for formats that use it |
| } |
| |
| void ImageItem::readImageFromStream(QDataStream &in) |
| { |
| quint8 hdr, fmt, endian, pad; |
| quint32 width, height, bpl; |
| QByteArray data; |
| |
| in >> hdr; |
| if (hdr != 'Q') { |
| image = QImage(); |
| return; |
| } |
| in >> fmt >> endian >> pad; |
| if (!fmt || fmt >= QImage::NImageFormats) { |
| image = QImage(); |
| return; |
| } |
| if (endian != QSysInfo::ByteOrder) { |
| qWarning("ImageItem cannot read streamed image with different endianness"); |
| image = QImage(); |
| return; |
| } |
| in >> width >> height >> bpl; |
| in >> data; |
| data = qUncompress(data); |
| QImage res((const uchar *)data.constData(), width, height, bpl, QImage::Format(fmt)); |
| image = res.copy(); //# yuck, seems there is currently no way to avoid data copy |
| } |
| |
| QDataStream & operator<< (QDataStream &stream, const ImageItem &ii) |
| { |
| stream << ii.testFunction << ii.itemName << ii.itemChecksum << quint8(ii.status) << ii.imageChecksums << ii.misc; |
| ii.writeImageToStream(stream); |
| return stream; |
| } |
| |
| QDataStream & operator>> (QDataStream &stream, ImageItem &ii) |
| { |
| quint8 encStatus; |
| stream >> ii.testFunction >> ii.itemName >> ii.itemChecksum >> encStatus >> ii.imageChecksums >> ii.misc; |
| ii.status = ImageItem::ItemStatus(encStatus); |
| ii.readImageFromStream(stream); |
| return stream; |
| } |
| |
| BaselineProtocol::BaselineProtocol() |
| { |
| } |
| |
| BaselineProtocol::~BaselineProtocol() |
| { |
| disconnect(); |
| } |
| |
| bool BaselineProtocol::disconnect() |
| { |
| socket.close(); |
| return (socket.state() == QTcpSocket::UnconnectedState) ? true : socket.waitForDisconnected(Timeout); |
| } |
| |
| |
| bool BaselineProtocol::connect(const QString &testCase, bool *dryrun, const PlatformInfo& clientInfo) |
| { |
| errMsg.clear(); |
| QByteArray serverName(qgetenv("QT_LANCELOT_SERVER")); |
| if (serverName.isNull()) |
| serverName = "lancelot.test.qt-project.org"; |
| |
| socket.connectToHost(serverName, ServerPort); |
| if (!socket.waitForConnected(Timeout)) { |
| sysSleep(3000); // Wait a bit and try again, the server might just be restarting |
| if (!socket.waitForConnected(Timeout)) { |
| errMsg += QLS("TCP connectToHost failed. Host:") + QLS(serverName) + QLS(" port:") + QString::number(ServerPort); |
| return false; |
| } |
| } |
| |
| PlatformInfo pi = clientInfo.isEmpty() ? PlatformInfo::localHostInfo() : clientInfo; |
| pi.insert(PI_TestCase, testCase); |
| QByteArray block; |
| QDataStream ds(&block, QIODevice::ReadWrite); |
| ds << pi; |
| if (!sendBlock(AcceptPlatformInfo, block)) { |
| errMsg += QLS("Failed to send data to server."); |
| return false; |
| } |
| |
| Command cmd = UnknownError; |
| if (!receiveBlock(&cmd, &block)) { |
| errMsg.prepend(QLS("Failed to get response from server. ")); |
| return false; |
| } |
| |
| if (cmd == Abort) { |
| errMsg += QLS("Server rejected connection. Reason: ") + QString::fromLatin1(block); |
| return false; |
| } |
| |
| if (dryrun) |
| *dryrun = (cmd == DoDryRun); |
| |
| if (cmd != Ack && cmd != DoDryRun) { |
| errMsg += QLS("Unexpected response from server."); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| |
| bool BaselineProtocol::acceptConnection(PlatformInfo *pi) |
| { |
| errMsg.clear(); |
| |
| QByteArray block; |
| Command cmd = AcceptPlatformInfo; |
| if (!receiveBlock(&cmd, &block) || cmd != AcceptPlatformInfo) |
| return false; |
| |
| if (pi) { |
| QDataStream ds(block); |
| ds >> *pi; |
| pi->insert(PI_HostAddress, socket.peerAddress().toString()); |
| } |
| |
| return true; |
| } |
| |
| |
| bool BaselineProtocol::requestBaselineChecksums(const QString &testFunction, ImageItemList *itemList) |
| { |
| errMsg.clear(); |
| if (!itemList) |
| return false; |
| |
| for(ImageItemList::iterator it = itemList->begin(); it != itemList->end(); it++) |
| it->testFunction = testFunction; |
| |
| QByteArray block; |
| QDataStream ds(&block, QIODevice::WriteOnly); |
| ds << *itemList; |
| if (!sendBlock(RequestBaselineChecksums, block)) |
| return false; |
| |
| Command cmd; |
| QByteArray rcvBlock; |
| if (!receiveBlock(&cmd, &rcvBlock) || cmd != BaselineProtocol::Ack) |
| return false; |
| QDataStream rds(&rcvBlock, QIODevice::ReadOnly); |
| rds >> *itemList; |
| return true; |
| } |
| |
| |
| bool BaselineProtocol::submitMatch(const ImageItem &item, QByteArray *serverMsg) |
| { |
| Command cmd; |
| ImageItem smallItem = item; |
| smallItem.image = QImage(); // No need to waste bandwith sending image (identical to baseline) to server |
| return (sendItem(AcceptMatch, smallItem) && receiveBlock(&cmd, serverMsg) && cmd == Ack); |
| } |
| |
| |
| bool BaselineProtocol::submitNewBaseline(const ImageItem &item, QByteArray *serverMsg) |
| { |
| Command cmd; |
| return (sendItem(AcceptNewBaseline, item) && receiveBlock(&cmd, serverMsg) && cmd == Ack); |
| } |
| |
| |
| bool BaselineProtocol::submitMismatch(const ImageItem &item, QByteArray *serverMsg, bool *fuzzyMatch) |
| { |
| Command cmd; |
| if (sendItem(AcceptMismatch, item) && receiveBlock(&cmd, serverMsg) && (cmd == Ack || cmd == FuzzyMatch)) { |
| if (fuzzyMatch) |
| *fuzzyMatch = (cmd == FuzzyMatch); |
| return true; |
| } |
| return false; |
| } |
| |
| |
| bool BaselineProtocol::sendItem(Command cmd, const ImageItem &item) |
| { |
| errMsg.clear(); |
| QBuffer buf; |
| buf.open(QIODevice::WriteOnly); |
| QDataStream ds(&buf); |
| ds << item; |
| if (!sendBlock(cmd, buf.data())) { |
| errMsg.prepend(QLS("Failed to submit image to server. ")); |
| return false; |
| } |
| return true; |
| } |
| |
| |
| bool BaselineProtocol::sendBlock(Command cmd, const QByteArray &block) |
| { |
| QDataStream s(&socket); |
| // TBD: set qds version as a constant |
| s << quint16(ProtocolVersion) << quint16(cmd); |
| s.writeBytes(block.constData(), block.size()); |
| return true; |
| } |
| |
| |
| bool BaselineProtocol::receiveBlock(Command *cmd, QByteArray *block) |
| { |
| while (socket.bytesAvailable() < int(2*sizeof(quint16) + sizeof(quint32))) { |
| if (!socket.waitForReadyRead(Timeout)) |
| return false; |
| } |
| QDataStream ds(&socket); |
| quint16 rcvProtocolVersion, rcvCmd; |
| ds >> rcvProtocolVersion >> rcvCmd; |
| if (rcvProtocolVersion != ProtocolVersion) { |
| errMsg = QLS("Baseline protocol version mismatch, received:") + QString::number(rcvProtocolVersion) |
| + QLS(" expected:") + QString::number(ProtocolVersion); |
| return false; |
| } |
| if (cmd) |
| *cmd = Command(rcvCmd); |
| |
| QByteArray uMsg; |
| quint32 remaining; |
| ds >> remaining; |
| uMsg.resize(remaining); |
| int got = 0; |
| char* uMsgBuf = uMsg.data(); |
| do { |
| got = ds.readRawData(uMsgBuf, remaining); |
| remaining -= got; |
| uMsgBuf += got; |
| } while (remaining && got >= 0 && socket.waitForReadyRead(Timeout)); |
| |
| if (got < 0) |
| return false; |
| |
| if (block) |
| *block = uMsg; |
| |
| return true; |
| } |
| |
| |
| QString BaselineProtocol::errorMessage() |
| { |
| QString ret = errMsg; |
| if (socket.error() >= 0) |
| ret += QLS(" Socket state: ") + socket.errorString(); |
| return ret; |
| } |
| |