qt5base-lts/tests/arthur/baselineserver/src/baselineserver.cpp

559 lines
19 KiB
C++
Raw Normal View History

/****************************************************************************
**
** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies).
** All rights reserved.
** Contact: Nokia Corporation (qt-info@nokia.com)
**
** This file is part of the test suite of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** No Commercial Usage
** This file contains pre-release code and may not be distributed.
** You may use this file in accordance with the terms and conditions
** contained in the Technology Preview License Agreement accompanying
** this package.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 2.1 requirements
** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** In addition, as a special exception, Nokia gives you certain additional
** rights. These rights are described in the Nokia Qt LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
** If you have questions regarding the use of this file, please contact
** Nokia at qt-info@nokia.com.
**
**
**
**
**
**
**
**
** $QT_END_LICENSE$
**
****************************************************************************/
#define QT_USE_FAST_CONCATENATION
#define QT_USE_FAST_OPERATOR_PLUS
#include "baselineserver.h"
#include <QBuffer>
#include <QFile>
#include <QDir>
#include <QCoreApplication>
#include <QFileInfo>
#include <QHostInfo>
#include <QTextStream>
#include <QProcess>
#include <QDirIterator>
// extra fields, for use in image metadata storage
const QString PI_ImageChecksum(QLS("ImageChecksum"));
const QString PI_RunId(QLS("RunId"));
const QString PI_CreationDate(QLS("CreationDate"));
QString BaselineServer::storage;
QString BaselineServer::url;
BaselineServer::BaselineServer(QObject *parent)
: QTcpServer(parent), lastRunIdIdx(0)
{
QFileInfo me(QCoreApplication::applicationFilePath());
meLastMod = me.lastModified();
heartbeatTimer = new QTimer(this);
connect(heartbeatTimer, SIGNAL(timeout()), this, SLOT(heartbeat()));
heartbeatTimer->start(HEARTBEAT*1000);
}
QString BaselineServer::storagePath()
{
if (storage.isEmpty()) {
storage = QLS(qgetenv("QT_LANCELOT_DIR"));
if (storage.isEmpty())
storage = QLS("/var/www");
}
return storage;
}
QString BaselineServer::baseUrl()
{
if (url.isEmpty()) {
url = QLS("http://")
+ QHostInfo::localHostName().toLatin1() + '.'
+ QHostInfo::localDomainName().toLatin1() + '/';
}
return url;
}
void BaselineServer::incomingConnection(int socketDescriptor)
{
QString runId = QDateTime::currentDateTime().toString(QLS("MMMdd-hhmmss"));
if (runId == lastRunId) {
runId += QLC('-') + QString::number(++lastRunIdIdx);
} else {
lastRunId = runId;
lastRunIdIdx = 0;
}
qDebug() << "Server: New connection! RunId:" << runId;
BaselineThread *thread = new BaselineThread(runId, socketDescriptor, this);
connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater()));
thread->start();
}
void BaselineServer::heartbeat()
{
// The idea is to exit to be restarted when modified, as soon as not actually serving
QFileInfo me(QCoreApplication::applicationFilePath());
if (me.lastModified() == meLastMod)
return;
if (!me.exists() || !me.isExecutable())
return;
//# (could close() here to avoid accepting new connections, to avoid livelock)
//# also, could check for a timeout to force exit, to avoid hung threads blocking
bool isServing = false;
foreach(BaselineThread *thread, findChildren<BaselineThread *>()) {
if (thread->isRunning()) {
isServing = true;
break;
}
}
if (!isServing)
QCoreApplication::exit();
}
BaselineThread::BaselineThread(const QString &runId, int socketDescriptor, QObject *parent)
: QThread(parent), runId(runId), socketDescriptor(socketDescriptor)
{
}
void BaselineThread::run()
{
BaselineHandler handler(runId, socketDescriptor);
exec();
}
BaselineHandler::BaselineHandler(const QString &runId, int socketDescriptor)
: QObject(), runId(runId), connectionEstablished(false)
{
if (socketDescriptor == -1)
return;
connect(&proto.socket, SIGNAL(readyRead()), this, SLOT(receiveRequest()));
connect(&proto.socket, SIGNAL(disconnected()), this, SLOT(receiveDisconnect()));
proto.socket.setSocketDescriptor(socketDescriptor);
}
const char *BaselineHandler::logtime()
{
return 0;
//return QTime::currentTime().toString(QLS("mm:ss.zzz"));
}
bool BaselineHandler::establishConnection()
{
if (!proto.acceptConnection(&plat)) {
qWarning() << runId << logtime() << "Accepting new connection from" << proto.socket.peerAddress().toString() << "failed." << proto.errorMessage();
proto.sendBlock(BaselineProtocol::Abort, proto.errorMessage().toLatin1()); // In case the client can hear us, tell it what's wrong.
proto.socket.disconnectFromHost();
return false;
}
QString logMsg;
foreach (QString key, plat.keys()) {
if (key != PI_HostName && key != PI_HostAddress)
logMsg += key + QLS(": '") + plat.value(key) + QLS("', ");
}
qDebug() << runId << logtime() << "Connection established with" << plat.value(PI_HostName)
<< "[" << qPrintable(plat.value(PI_HostAddress)) << "]" << logMsg;
// Filter on branch
QString branch = plat.value(PI_PulseGitBranch);
if (branch.isEmpty()) {
// Not run by Pulse, i.e. ad hoc run: Ok.
}
else if (branch != QLS("master-integration") || !plat.value(PI_GitCommit).contains(QLS("Merge branch 'master' of scm.dev.nokia.troll.no:qt/qt-fire-staging into master-integration"))) {
qDebug() << runId << logtime() << "Did not pass branch/staging repo filter, disconnecting.";
proto.sendBlock(BaselineProtocol::Abort, QByteArray("This branch/staging repo is not assigned to be tested."));
proto.socket.disconnectFromHost();
return false;
}
proto.sendBlock(BaselineProtocol::Ack, QByteArray());
report.init(this, runId, plat);
return true;
}
void BaselineHandler::receiveRequest()
{
if (!connectionEstablished) {
connectionEstablished = establishConnection();
return;
}
QByteArray block;
BaselineProtocol::Command cmd;
if (!proto.receiveBlock(&cmd, &block)) {
qWarning() << runId << logtime() << "Command reception failed. "<< proto.errorMessage();
QThread::currentThread()->exit(1);
return;
}
switch(cmd) {
case BaselineProtocol::RequestBaselineChecksums:
provideBaselineChecksums(block);
break;
case BaselineProtocol::AcceptNewBaseline:
storeImage(block, true);
break;
case BaselineProtocol::AcceptMismatch:
storeImage(block, false);
break;
default:
qWarning() << runId << logtime() << "Unknown command received. " << proto.errorMessage();
proto.sendBlock(BaselineProtocol::UnknownError, QByteArray());
}
}
void BaselineHandler::provideBaselineChecksums(const QByteArray &itemListBlock)
{
ImageItemList itemList;
QDataStream ds(itemListBlock);
ds >> itemList;
qDebug() << runId << logtime() << "Received request for checksums for" << itemList.count()
<< "items in test function" << itemList.at(0).testFunction;
for (ImageItemList::iterator i = itemList.begin(); i != itemList.end(); ++i) {
i->imageChecksums.clear();
i->status = ImageItem::BaselineNotFound;
QString prefix = pathForItem(*i, true);
PlatformInfo itemData = fetchItemMetadata(prefix);
if (itemData.contains(PI_ImageChecksum)) {
bool ok = false;
quint64 checksum = itemData.value(PI_ImageChecksum).toULongLong(&ok, 16);
if (ok) {
i->imageChecksums.prepend(checksum);
i->status = ImageItem::Ok;
}
}
}
// Find and mark blacklisted items
QString context = pathForItem(itemList.at(0), true, false).section(QLC('/'), 0, -2);
if (itemList.count() > 0) {
QFile file(BaselineServer::storagePath() + QLC('/') + context + QLS("/BLACKLIST"));
if (file.open(QIODevice::ReadOnly)) {
QTextStream in(&file);
do {
QString itemName = in.readLine();
if (!itemName.isNull()) {
for (ImageItemList::iterator i = itemList.begin(); i != itemList.end(); ++i) {
if (i->itemName == itemName)
i->status = ImageItem::IgnoreItem;
}
}
} while (!in.atEnd());
}
}
QByteArray block;
QDataStream ods(&block, QIODevice::WriteOnly);
ods << itemList;
proto.sendBlock(BaselineProtocol::Ack, block);
report.addItems(itemList);
}
void BaselineHandler::storeImage(const QByteArray &itemBlock, bool isBaseline)
{
QDataStream ds(itemBlock);
ImageItem item;
ds >> item;
QString prefix = pathForItem(item, isBaseline);
qDebug() << runId << logtime() << "Received" << (isBaseline ? "baseline" : "mismatched") << "image for:" << item.itemName << "Storing in" << prefix;
QString msg;
if (isBaseline)
msg = QLS("New baseline image stored: ") + pathForItem(item, true, true) + QLS(FileFormat);
else
msg = BaselineServer::baseUrl() + report.filePath();
proto.sendBlock(BaselineProtocol::Ack, msg.toLatin1());
QString dir = prefix.section(QLC('/'), 0, -2);
QDir cwd;
if (!cwd.exists(dir))
cwd.mkpath(dir);
item.image.save(prefix + QLS(FileFormat), FileFormat);
PlatformInfo itemData = plat;
itemData.insert(PI_ImageChecksum, QString::number(item.imageChecksums.at(0), 16)); //# Only the first is stored. TBD: get rid of list
itemData.insert(PI_RunId, runId);
itemData.insert(PI_CreationDate, QDateTime::currentDateTime().toString());
storeItemMetadata(itemData, prefix);
if (!isBaseline)
report.addMismatch(item);
}
void BaselineHandler::storeItemMetadata(const PlatformInfo &metadata, const QString &path)
{
QFile file(path + QLS(MetadataFileExt));
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
qWarning() << runId << logtime() << "ERROR: could not write to file" << file.fileName();
return;
}
QTextStream out(&file);
PlatformInfo::const_iterator it = metadata.constBegin();
while (it != metadata.constEnd()) {
out << it.key() << ": " << it.value() << endl;
++it;
}
file.close();
}
PlatformInfo BaselineHandler::fetchItemMetadata(const QString &path)
{
PlatformInfo res;
QFile file(path + QLS(MetadataFileExt));
if (!file.open(QIODevice::ReadOnly))
return res;
QTextStream in(&file);
do {
QString line = in.readLine();
int idx = line.indexOf(QLS(": "));
if (idx > 0)
res.insert(line.left(idx), line.mid(idx+2));
} while (!in.atEnd());
return res;
}
void BaselineHandler::receiveDisconnect()
{
qDebug() << runId << logtime() << "Client disconnected.";
report.end();
QThread::currentThread()->exit(0);
}
void BaselineHandler::mapPlatformInfo() const
{
mapped = plat;
// Map hostname
QString host = plat.value(PI_HostName).section(QLC('.'), 0, 0); // Filter away domain, if any
if (host.isEmpty() || host == QLS("localhost")) {
host = plat.value(PI_HostAddress);
} else {
if (!plat.value(PI_PulseGitBranch).isEmpty()) {
// i.e. pulse run, so remove index postfix typical of vm hostnames
host.remove(QRegExp(QLS("\\d+$")));
if (host.endsWith(QLC('-')))
host.chop(1);
}
}
if (host.isEmpty())
host = QLS("unknownhost");
mapped.insert(PI_HostName, host);
// Map qmakespec
QString mkspec = plat.value(PI_QMakeSpec);
mapped.insert(PI_QMakeSpec, mkspec.replace(QLC('/'), QLC('_')));
// Map Qt version
QString ver = plat.value(PI_QtVersion);
mapped.insert(PI_QtVersion, ver.prepend(QLS("Qt-")));
}
QString BaselineHandler::pathForItem(const ImageItem &item, bool isBaseline, bool absolute) const
{
if (mapped.isEmpty())
mapPlatformInfo();
QString itemName = item.itemName.simplified();
itemName.replace(QLC(' '), QLC('_'));
itemName.replace(QLC('.'), QLC('_'));
itemName.append(QLC('_'));
itemName.append(QString::number(item.itemChecksum, 16).rightJustified(4, QLC('0')));
QStringList path;
if (absolute)
path += BaselineServer::storagePath();
path += mapped.value(PI_TestCase);
path += QLS(isBaseline ? "baselines" : "mismatches");
path += item.testFunction;
path += mapped.value(PI_QtVersion);
path += mapped.value(PI_QMakeSpec);
path += mapped.value(PI_HostName);
if (!isBaseline)
path += runId;
path += itemName + QLC('.');
return path.join(QLS("/"));
}
QString BaselineHandler::view(const QString &baseline, const QString &rendered, const QString &compared)
{
QFile f(":/templates/view.html");
f.open(QIODevice::ReadOnly);
return QString::fromLatin1(f.readAll()).arg('/'+baseline, '/'+rendered, '/'+compared);
}
QString BaselineHandler::clearAllBaselines(const QString &context)
{
int tot = 0;
int failed = 0;
QDirIterator it(BaselineServer::storagePath() + QLC('/') + context,
QStringList() << QLS("*.") + QLS(FileFormat) << QLS("*.") + QLS(MetadataFileExt));
while (it.hasNext()) {
tot++;
if (!QFile::remove(it.next()))
failed++;
}
return QString(QLS("%1 of %2 baselines cleared from context ")).arg((tot-failed)/2).arg(tot/2) + context;
}
QString BaselineHandler::updateBaselines(const QString &context, const QString &mismatchContext, const QString &itemFile)
{
int tot = 0;
int failed = 0;
QString storagePrefix = BaselineServer::storagePath() + QLC('/');
// If itemId is set, update just that one, otherwise, update all:
QString filter = itemFile.isEmpty() ? QLS("*_????.") : itemFile;
QDirIterator it(storagePrefix + mismatchContext, QStringList() << filter + QLS(FileFormat) << filter + QLS(MetadataFileExt));
while (it.hasNext()) {
tot++;
it.next();
QString oldFile = storagePrefix + context + QLC('/') + it.fileName();
QFile::remove(oldFile); // Remove existing baseline file
if (!QFile::copy(it.filePath(), oldFile)) // and replace it with the mismatch
failed++;
}
return QString(QLS("%1 of %2 baselines updated in context %3 from context %4")).arg((tot-failed)/2).arg(tot/2).arg(context, mismatchContext);
}
QString BaselineHandler::blacklistTest(const QString &context, const QString &itemId, bool removeFromBlacklist)
{
QFile file(BaselineServer::storagePath() + QLC('/') + context + QLS("/BLACKLIST"));
QStringList blackList;
if (file.open(QIODevice::ReadWrite)) {
while (!file.atEnd())
blackList.append(file.readLine().trimmed());
if (removeFromBlacklist)
blackList.removeAll(itemId);
else if (!blackList.contains(itemId))
blackList.append(itemId);
file.resize(0);
foreach (QString id, blackList)
file.write(id.toLatin1() + '\n');
file.close();
return QLS(removeFromBlacklist ? "Whitelisted " : "Blacklisted ") + itemId + QLS(" in context ") + context;
} else {
return QLS("Unable to update blacklisted tests, failed to open ") + file.fileName();
}
}
void BaselineHandler::testPathMapping()
{
qDebug() << "Storage prefix:" << BaselineServer::storagePath();
QStringList hosts;
hosts << QLS("bq-ubuntu910-x86-01")
<< QLS("bq-ubuntu910-x86-15")
<< QLS("osl-mac-master-5.test.qt.nokia.com")
<< QLS("osl-mac-master-6.test.qt.nokia.com")
<< QLS("sv-xp-vs-010")
<< QLS("sv-xp-vs-011")
<< QLS("sv-solaris-sparc-008")
<< QLS("macbuilder-02.test.troll.no")
<< QLS("bqvm1164")
<< QLS("chimera")
<< QLS("localhost")
<< QLS("");
ImageItem item;
item.testFunction = QLS("testPathMapping");
item.itemName = QLS("arcs.qps");
item.imageChecksums << 0x0123456789abcdefULL;
item.itemChecksum = 0x0123;
plat.insert(PI_QtVersion, QLS("4.8.0"));
plat.insert(PI_BuildKey, QLS("(nobuildkey)"));
plat.insert(PI_QMakeSpec, QLS("linux-g++"));
plat.insert(PI_PulseGitBranch, QLS("somebranch"));
foreach(const QString& host, hosts) {
mapped.clear();
plat.insert(PI_HostName, host);
qDebug() << "Baseline from" << host << "->" << pathForItem(item, true);
qDebug() << "Mismatch from" << host << "->" << pathForItem(item, false);
}
}
QString BaselineHandler::computeMismatchScore(const QImage &baseline, const QImage &rendered)
{
if (baseline.size() != rendered.size() || baseline.format() != rendered.format())
return QLS("[No score, incomparable images.]");
if (baseline.depth() != 32)
return QLS("[Score computation not implemented for format.]");
int w = baseline.width();
int h = baseline.height();
uint ncd = 0; // number of differing color pixels
uint nad = 0; // number of differing alpha pixels
uint scd = 0; // sum of color pixel difference
uint sad = 0; // sum of alpha pixel difference
for (int y=0; y<h; ++y) {
const QRgb *bl = (const QRgb *) baseline.constScanLine(y);
const QRgb *rl = (const QRgb *) rendered.constScanLine(y);
for (int x=0; x<w; ++x) {
QRgb b = bl[x];
QRgb r = rl[x];
if (r != b) {
int dr = qAbs(qRed(b) - qRed(r));
int dg = qAbs(qGreen(b) - qGreen(r));
int db = qAbs(qBlue(b) - qBlue(r));
int ds = dr + dg + db;
int da = qAbs(qAlpha(b) - qAlpha(r));
if (ds) {
ncd++;
scd += ds;
}
if (da) {
nad++;
sad += da;
}
}
}
}
double pcd = 100.0 * ncd / (w*h); // percent of pixels that differ
double acd = ncd ? double(scd) / (3*ncd) : 0; // avg. difference
QString res = QString(QLS("Diffscore: %1% (Num:%2 Avg:%3)")).arg(pcd, 0, 'g', 2).arg(ncd).arg(acd, 0, 'g', 2);
if (baseline.hasAlphaChannel()) {
double pad = 100.0 * nad / (w*h); // percent of pixels that differ
double aad = nad ? double(sad) / (3*nad) : 0; // avg. difference
res += QString(QLS(" Alpha-diffscore: %1% (Num:%2 Avg:%3)")).arg(pad, 0, 'g', 2).arg(nad).arg(aad, 0, 'g', 2);
}
return res;
}