/**************************************************************************** ** ** Copyright (C) 2021 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 #include #include #include #include #if QT_CONFIG(process) # include #endif #include #include #include #include #include #include const QString PI_Project(QLS("Project")); const QString PI_ProjectImageKeys(QLS("ProjectImageKeys")); 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_GitBranch(QLS("GitBranch")); PlatformInfo PlatformInfo::localHostInfo() { PlatformInfo pi; pi.insert(PI_HostName, QHostInfo::localHostName()); pi.insert(PI_QtVersion, QLS(qVersion())); pi.insert(PI_QtBuildMode, QLibraryInfo::isDebugBuild() ? QLS("QtDebug") : QLS("QtRelease")); #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")); #endif // QT_CONFIG(process) if (qEnvironmentVariableIsSet("JENKINS_HOME")) pi.setAdHocRun(false); QString gb = qEnvironmentVariable("GIT_BRANCH"); if (!gb.isEmpty()) pi.insert(PI_GitBranch, gb); return pi; } 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&>(pi); stream << pi.orides << pi.adHoc; return stream; } QDataStream & operator>> (QDataStream &stream, PlatformInfo &pi) { stream >> static_cast&>(pi); stream >> pi.orides >> pi.adHoc; return stream; } // 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 qsizetype bpl = img.bytesPerLine(); const int padBytes = bpl - (qsizetype(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(image.constBits()), 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)) { QThread::msleep(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 bandwidth 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; }