summaryrefslogtreecommitdiffstats
path: root/tests/baselineserver/src/baselineserver.cpp
diff options
context:
space:
mode:
authoraavit <qt_aavit@ovi.com>2012-08-13 14:13:40 +0200
committerThe Qt Project <gerrit-noreply@qt-project.org>2012-09-26 04:03:48 +0200
commitaa9728450cc515c66545323646c66d826a1af50a (patch)
treee309abb926ca9fe8da2d1784d0db4a8db9305c1e /tests/baselineserver/src/baselineserver.cpp
parentbf05abddfd542a0568138d533d1f401d32b65e8c (diff)
Misc. updates to the lancelot autotest framework
Moving more logic into the protocol and framework, easening the burden on the autotest implementation. Implementing several new features in the server and report, like fuzzy matching and static baselines. Change-Id: Iaf070918195ae05767808a548f019d09d9d5f8c0 Reviewed-by: Paul Olav Tvete <paul.tvete@digia.com>
Diffstat (limited to 'tests/baselineserver/src/baselineserver.cpp')
-rw-r--r--tests/baselineserver/src/baselineserver.cpp422
1 files changed, 346 insertions, 76 deletions
diff --git a/tests/baselineserver/src/baselineserver.cpp b/tests/baselineserver/src/baselineserver.cpp
index 1653754333..a85dca375a 100644
--- a/tests/baselineserver/src/baselineserver.cpp
+++ b/tests/baselineserver/src/baselineserver.cpp
@@ -52,6 +52,7 @@
#include <QTextStream>
#include <QProcess>
#include <QDirIterator>
+#include <QUrl>
// extra fields, for use in image metadata storage
const QString PI_ImageChecksum(QLS("ImageChecksum"));
@@ -60,7 +61,7 @@ const QString PI_CreationDate(QLS("CreationDate"));
QString BaselineServer::storage;
QString BaselineServer::url;
-QString BaselineServer::settingsFile;
+QStringList BaselineServer::pathKeys;
BaselineServer::BaselineServer(QObject *parent)
: QTcpServer(parent), lastRunIdIdx(0)
@@ -92,13 +93,11 @@ QString BaselineServer::baseUrl()
return url;
}
-QString BaselineServer::settingsFilePath()
+QStringList BaselineServer::defaultPathKeys()
{
- if (settingsFile.isEmpty()) {
- QString exeName = QCoreApplication::applicationFilePath().section(QLC('/'), -1);
- settingsFile = storagePath() + QLC('/') + exeName + QLS(".ini");
- }
- return settingsFile;
+ if (pathKeys.isEmpty())
+ pathKeys << PI_QtVersion << PI_QMakeSpec << PI_HostName;
+ return pathKeys;
}
void BaselineServer::incomingConnection(qintptr socketDescriptor)
@@ -152,9 +151,13 @@ void BaselineThread::run()
BaselineHandler::BaselineHandler(const QString &runId, int socketDescriptor)
- : QObject(), runId(runId), connectionEstablished(false)
+ : QObject(), runId(runId), connectionEstablished(false), settings(0), fuzzLevel(0)
{
- settings = new QSettings(BaselineServer::settingsFilePath(), QSettings::IniFormat, this);
+ idleTimer = new QTimer(this);
+ idleTimer->setSingleShot(true);
+ idleTimer->setInterval(IDLE_CLIENT_TIMEOUT * 1000);
+ connect(idleTimer, SIGNAL(timeout()), this, SLOT(idleClientTimeout()));
+ idleTimer->start();
if (socketDescriptor == -1)
return;
@@ -162,6 +165,7 @@ BaselineHandler::BaselineHandler(const QString &runId, int socketDescriptor)
connect(&proto.socket, SIGNAL(readyRead()), this, SLOT(receiveRequest()));
connect(&proto.socket, SIGNAL(disconnected()), this, SLOT(receiveDisconnect()));
proto.socket.setSocketDescriptor(socketDescriptor);
+ proto.socket.setSocketOption(QAbstractSocket::KeepAliveOption, 1);
}
const char *BaselineHandler::logtime()
@@ -170,6 +174,85 @@ const char *BaselineHandler::logtime()
//return QTime::currentTime().toString(QLS("mm:ss.zzz"));
}
+QString BaselineHandler::projectPath(bool absolute) const
+{
+ QString p = clientInfo.value(PI_Project);
+ return absolute ? BaselineServer::storagePath() + QLC('/') + p : p;
+}
+
+bool BaselineHandler::checkClient(QByteArray *errMsg, bool *dryRunMode)
+{
+ if (!errMsg)
+ return false;
+ if (clientInfo.value(PI_Project).isEmpty() || clientInfo.value(PI_TestCase).isEmpty()) {
+ *errMsg = "No Project and/or TestCase specified in client info.";
+ return false;
+ }
+
+ // Determine ad-hoc state ### hardcoded for now
+ if (clientInfo.value(PI_TestCase) == QLS("tst_Lancelot")) {
+ //### Todo: push this stuff out in a script
+ if (!clientInfo.isAdHocRun()) {
+ // ### comp. with earlier versions still running (4.8) (?)
+ clientInfo.setAdHocRun(clientInfo.value(PI_PulseGitBranch).isEmpty() && clientInfo.value(PI_PulseTestrBranch).isEmpty());
+ }
+ }
+ else {
+ // TBD
+ }
+
+ if (clientInfo.isAdHocRun()) {
+ if (dryRunMode)
+ *dryRunMode = false;
+ return true;
+ }
+
+ // Not ad hoc: filter the client
+ settings->beginGroup("ClientFilters");
+ bool matched = false;
+ bool dryRunReq = false;
+ foreach (const QString &rule, settings->childKeys()) {
+ //qDebug() << " > RULE" << rule;
+ dryRunReq = false;
+ QString ruleMode = settings->value(rule).toString().toLower();
+ if (ruleMode == QLS("dryrun"))
+ dryRunReq = true;
+ else if (ruleMode != QLS("enabled"))
+ continue;
+ settings->beginGroup(rule);
+ bool ruleMatched = true;
+ foreach (const QString &filterKey, settings->childKeys()) {
+ //qDebug() << " > FILTER" << filterKey;
+ QString filter = settings->value(filterKey).toString();
+ if (filter.isEmpty())
+ continue;
+ QString platVal = clientInfo.value(filterKey);
+ if (!platVal.contains(filter)) {
+ ruleMatched = false;
+ break;
+ }
+ }
+ if (ruleMatched) {
+ ruleName = rule;
+ matched = true;
+ break;
+ }
+ settings->endGroup();
+ }
+
+ if (!matched && errMsg)
+ *errMsg = "Non-adhoc client did not match any filter rule in " + settings->fileName().toLatin1();
+
+ if (matched && dryRunMode)
+ *dryRunMode = dryRunReq;
+
+ // NB! Must reset the settings object before returning
+ while (!settings->group().isEmpty())
+ settings->endGroup();
+
+ return matched;
+}
+
bool BaselineHandler::establishConnection()
{
if (!proto.acceptConnection(&clientInfo)) {
@@ -187,35 +270,46 @@ bool BaselineHandler::establishConnection()
<< "[" << qPrintable(clientInfo.value(PI_HostAddress)) << "]" << logMsg
<< "Overrides:" << clientInfo.overrides() << "AdHoc-Run:" << clientInfo.isAdHocRun();
- //### Temporarily override the client setting, for client compatibility:
- if (!clientInfo.isAdHocRun())
- clientInfo.setAdHocRun(clientInfo.value(PI_PulseGitBranch).isEmpty() && clientInfo.value(PI_PulseTestrBranch).isEmpty());
+ // ### Hardcoded backwards compatibility: add project field for certain existing clients that lack it
+ if (clientInfo.value(PI_Project).isEmpty()) {
+ QString tc = clientInfo.value(PI_TestCase);
+ if (tc == QLS("tst_Lancelot"))
+ clientInfo.insert(PI_Project, QLS("Raster"));
+ else if (tc == QLS("tst_Scenegraph"))
+ clientInfo.insert(PI_Project, QLS("SceneGraph"));
+ else
+ clientInfo.insert(PI_Project, QLS("Other"));
+ }
- settings->beginGroup("ClientFilters");
- if (!clientInfo.isAdHocRun()) { // for CI runs, allow filtering of clients. TBD: different filters (settings file) per testCase
- foreach (QString filterKey, settings->childKeys()) {
- QString filter = settings->value(filterKey).toString();
- QString platVal = clientInfo.value(filterKey);
- if (filter.isEmpty())
- continue; // tbd: add a syntax for specifying a "value-must-be-present" filter
- if (!platVal.contains(filter)) {
- qDebug() << runId << logtime() << "Did not pass client filter on" << filterKey << "; disconnecting.";
- proto.sendBlock(BaselineProtocol::Abort, QByteArray("Configured to not do testing for this client or repo, ref. ") + BaselineServer::settingsFilePath().toLatin1());
- proto.socket.disconnectFromHost();
- return false;
- }
- }
+ QString settingsFile = projectPath() + QLS("/config.ini");
+ settings = new QSettings(settingsFile, QSettings::IniFormat, this);
+
+ QByteArray errMsg;
+ bool dryRunMode = false;
+ if (!checkClient(&errMsg, &dryRunMode)) {
+ qDebug() << runId << logtime() << "Rejecting connection:" << errMsg;
+ proto.sendBlock(BaselineProtocol::Abort, errMsg);
+ proto.socket.disconnectFromHost();
+ return false;
}
- settings->endGroup();
- proto.sendBlock(BaselineProtocol::Ack, QByteArray());
+ fuzzLevel = qBound(0, settings->value("FuzzLevel").toInt(), 100);
+ if (!clientInfo.isAdHocRun()) {
+ qDebug() << runId << logtime() << "Client matches filter rule" << ruleName
+ << "Dryrun:" << dryRunMode
+ << "FuzzLevel:" << fuzzLevel
+ << "ReportMissingResults:" << settings->value("ReportMissingResults").toBool();
+ }
- report.init(this, runId, clientInfo);
+ proto.sendBlock(dryRunMode ? BaselineProtocol::DoDryRun : BaselineProtocol::Ack, QByteArray());
+ report.init(this, runId, clientInfo, settings);
return true;
}
void BaselineHandler::receiveRequest()
{
+ idleTimer->start(); // Restart idle client timeout
+
if (!connectionEstablished) {
connectionEstablished = establishConnection();
return;
@@ -233,6 +327,9 @@ void BaselineHandler::receiveRequest()
case BaselineProtocol::RequestBaselineChecksums:
provideBaselineChecksums(block);
break;
+ case BaselineProtocol::AcceptMatch:
+ recordMatch(block);
+ break;
case BaselineProtocol::AcceptNewBaseline:
storeImage(block, true);
break;
@@ -295,6 +392,16 @@ void BaselineHandler::provideBaselineChecksums(const QByteArray &itemListBlock)
}
+void BaselineHandler::recordMatch(const QByteArray &itemBlock)
+{
+ QDataStream ds(itemBlock);
+ ImageItem item;
+ ds >> item;
+ report.addResult(item);
+ proto.sendBlock(BaselineProtocol::Ack, QByteArray());
+}
+
+
void BaselineHandler::storeImage(const QByteArray &itemBlock, bool isBaseline)
{
QDataStream ds(itemBlock);
@@ -307,16 +414,23 @@ void BaselineHandler::storeImage(const QByteArray &itemBlock, bool isBaseline)
return;
}
- QString prefix = pathForItem(item, isBaseline);
+ QString blPrefix = pathForItem(item, true);
+ QString mmPrefix = pathForItem(item, false);
+ QString prefix = isBaseline ? blPrefix : mmPrefix;
+
qDebug() << runId << logtime() << "Received" << (isBaseline ? "baseline" : "mismatched") << "image for:" << item.itemName << "Storing in" << prefix;
+ // Reply to the client
QString msg;
if (isBaseline)
- msg = QLS("New baseline image stored: ") + pathForItem(item, true, true) + QLS(FileFormat);
+ msg = QLS("New baseline image stored: ") + blPrefix + QLS(FileFormat);
else
msg = BaselineServer::baseUrl() + report.filePath();
- proto.sendBlock(BaselineProtocol::Ack, msg.toLatin1());
+ if (isBaseline || !fuzzLevel)
+ proto.sendBlock(BaselineProtocol::Ack, msg.toLatin1()); // Do early reply if possible: don't make the client wait longer than necessary
+
+ // Store the image
QString dir = prefix.section(QLC('/'), 0, -2);
QDir cwd;
if (!cwd.exists(dir))
@@ -329,8 +443,23 @@ void BaselineHandler::storeImage(const QByteArray &itemBlock, bool isBaseline)
itemData.insert(PI_CreationDate, QDateTime::currentDateTime().toString());
storeItemMetadata(itemData, prefix);
- if (!isBaseline)
- report.addMismatch(item);
+ if (!isBaseline) {
+ // Do fuzzy matching
+ bool fuzzyMatch = false;
+ if (fuzzLevel) {
+ BaselineProtocol::Command cmd = BaselineProtocol::Ack;
+ fuzzyMatch = fuzzyCompare(blPrefix, mmPrefix);
+ if (fuzzyMatch) {
+ msg.prepend(QString("Fuzzy match at fuzzlevel %1%. Report: ").arg(fuzzLevel));
+ cmd = BaselineProtocol::FuzzyMatch;
+ }
+ proto.sendBlock(cmd, msg.toLatin1()); // We didn't reply earlier
+ }
+
+ // Add to report
+ item.status = fuzzyMatch ? ImageItem::FuzzyMatch : ImageItem::Mismatch;
+ report.addResult(item);
+ }
}
@@ -355,7 +484,7 @@ PlatformInfo BaselineHandler::fetchItemMetadata(const QString &path)
{
PlatformInfo res;
QFile file(path + QLS(MetadataFileExt));
- if (!file.open(QIODevice::ReadOnly))
+ if (!file.open(QIODevice::ReadOnly) || !QFile::exists(path + QLS(FileFormat)))
return res;
QTextStream in(&file);
do {
@@ -368,20 +497,50 @@ PlatformInfo BaselineHandler::fetchItemMetadata(const QString &path)
}
+void BaselineHandler::idleClientTimeout()
+{
+ qWarning() << runId << logtime() << "Idle client timeout: no request received for" << IDLE_CLIENT_TIMEOUT << "seconds, terminating connection.";
+ proto.socket.disconnectFromHost();
+}
+
+
void BaselineHandler::receiveDisconnect()
{
qDebug() << runId << logtime() << "Client disconnected.";
report.end();
+ if (report.reportProduced() && !clientInfo.isAdHocRun())
+ issueMismatchNotification();
+ if (settings && settings->value("ProcessXmlResults").toBool() && !clientInfo.isAdHocRun()) {
+ // ### TBD: actually execute the processing command. For now, just generate the xml files.
+ QString xmlDir = report.writeResultsXmlFiles();
+ }
QThread::currentThread()->exit(0);
}
PlatformInfo BaselineHandler::mapPlatformInfo(const PlatformInfo& orig) const
{
- PlatformInfo mapped = orig;
+ PlatformInfo mapped;
+ foreach (const QString &key, orig.uniqueKeys()) {
+ QString val = orig.value(key).simplified();
+ val.replace(QLC('/'), QLC('_'));
+ val.replace(QLC(' '), QLC('_'));
+ mapped.insert(key, QUrl::toPercentEncoding(val, "+"));
+ //qDebug() << "MAPPED" << key << "FROM" << orig.value(key) << "TO" << mapped.value(key);
+ }
+
+ // Special fixup for OS version
+ if (mapped.value(PI_OSName) == QLS("MacOS")) {
+ int ver = mapped.value(PI_OSVersion).toInt();
+ if (ver > 1)
+ mapped.insert(PI_OSVersion, QString("MV_10_%1").arg(ver-2));
+ }
+ else if (mapped.value(PI_OSName) == QLS("Windows")) {
+ // TBD: map windows version numbers to names
+ }
- // Map hostname
- QString host = orig.value(PI_HostName).section(QLC('.'), 0, 0); // Filter away domain, if any
+ // Special fixup for hostname
+ QString host = mapped.value(PI_HostName).section(QLC('.'), 0, 0); // Filter away domain, if any
if (host.isEmpty() || host == QLS("localhost")) {
host = orig.value(PI_HostAddress);
} else {
@@ -392,16 +551,15 @@ PlatformInfo BaselineHandler::mapPlatformInfo(const PlatformInfo& orig) const
}
}
if (host.isEmpty())
- host = QLS("unknownhost");
+ host = QLS("UNKNOWN-HOST");
+ if (mapped.value(PI_OSName) == QLS("MacOS")) // handle multiple os versions on same host
+ host += QLC('-') + mapped.value(PI_OSVersion);
mapped.insert(PI_HostName, host);
- // Map qmakespec
- QString mkspec = orig.value(PI_QMakeSpec);
- mapped.insert(PI_QMakeSpec, mkspec.replace(QLC('/'), QLC('_')));
-
- // Map Qt version
- QString ver = orig.value(PI_QtVersion);
- mapped.insert(PI_QtVersion, ver.prepend(QLS("Qt-")));
+ // Special fixup for Qt version
+ QString ver = mapped.value(PI_QtVersion);
+ if (!ver.isEmpty())
+ mapped.insert(PI_QtVersion, ver.prepend(QLS("Qt-")));
return mapped;
}
@@ -412,6 +570,7 @@ QString BaselineHandler::pathForItem(const ImageItem &item, bool isBaseline, boo
if (mappedClientInfo.isEmpty()) {
mappedClientInfo = mapPlatformInfo(clientInfo);
PlatformInfo oraw = clientInfo;
+ // ### simplify: don't map if no overrides!
for (int i = 0; i < clientInfo.overrides().size()-1; i+=2)
oraw.insert(clientInfo.overrides().at(i), clientInfo.overrides().at(i+1));
overriddenMappedClientInfo = mapPlatformInfo(oraw);
@@ -419,21 +578,21 @@ QString BaselineHandler::pathForItem(const ImageItem &item, bool isBaseline, boo
const PlatformInfo& mapped = isBaseline ? overriddenMappedClientInfo : mappedClientInfo;
- 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')));
+ QString itemName = safeName(item.itemName);
+ itemName.append(QLC('_') + QString::number(item.itemChecksum, 16).rightJustified(4, QLC('0')));
QStringList path;
- if (absolute)
- path += BaselineServer::storagePath();
+ path += projectPath(absolute);
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);
+ QStringList itemPathKeys;
+ if (settings)
+ itemPathKeys = settings->value("ItemPathKeys").toStringList();
+ if (itemPathKeys.isEmpty())
+ itemPathKeys = BaselineServer::defaultPathKeys();
+ foreach (const QString &key, itemPathKeys)
+ path += mapped.value(key, QLS("UNSET-")+key);
if (!isBaseline)
path += runId;
path += itemName + QLC('.');
@@ -446,19 +605,33 @@ QString BaselineHandler::view(const QString &baseline, const QString &rendered,
{
QFile f(":/templates/view.html");
f.open(QIODevice::ReadOnly);
- return QString::fromLatin1(f.readAll()).arg('/'+baseline, '/'+rendered, '/'+compared);
+ return QString::fromLatin1(f.readAll()).arg('/'+baseline, '/'+rendered, '/'+compared, diffstats(baseline, rendered));
}
+QString BaselineHandler::diffstats(const QString &baseline, const QString &rendered)
+{
+ QImage blImg(BaselineServer::storagePath() + QLC('/') + baseline);
+ QImage mmImg(BaselineServer::storagePath() + QLC('/') + rendered);
+ if (blImg.isNull() || mmImg.isNull())
+ return QLS("Could not compute diffstats: image loading failed.");
+
+ // ### TBD: cache the results
+ return computeMismatchScore(blImg, mmImg);
+}
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));
+ QStringList() << QLS("*.") + QLS(FileFormat)
+ << QLS("*.") + QLS(MetadataFileExt)
+ << QLS("*.") + QLS(ThumbnailExt));
while (it.hasNext()) {
- tot++;
- if (!QFile::remove(it.next()))
+ bool counting = !it.next().endsWith(QLS(ThumbnailExt));
+ if (counting)
+ tot++;
+ if (!QFile::remove(it.filePath()) && counting)
failed++;
}
return QString(QLS("%1 of %2 baselines cleared from context ")).arg((tot-failed)/2).arg(tot/2) + context;
@@ -471,13 +644,17 @@ QString BaselineHandler::updateBaselines(const QString &context, const QString &
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));
+ QDirIterator it(storagePrefix + mismatchContext,
+ QStringList() << filter + QLS(FileFormat)
+ << filter + QLS(MetadataFileExt)
+ << filter + QLS(ThumbnailExt));
while (it.hasNext()) {
- tot++;
- it.next();
+ bool counting = !it.next().endsWith(QLS(ThumbnailExt));
+ if (counting)
+ tot++;
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
+ QFile::remove(oldFile); // Remove existing baseline file
+ if (!QFile::copy(it.filePath(), oldFile) && counting) // 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);
@@ -534,7 +711,7 @@ void BaselineHandler::testPathMapping()
clientInfo.insert(PI_QtVersion, QLS("5.0.0"));
clientInfo.insert(PI_QMakeSpec, QLS("linux-g++"));
clientInfo.insert(PI_PulseGitBranch, QLS("somebranch"));
-
+ clientInfo.setAdHocRun(false);
foreach(const QString& host, hosts) {
mappedClientInfo.clear();
clientInfo.insert(PI_HostName, host);
@@ -547,9 +724,9 @@ void BaselineHandler::testPathMapping()
QString BaselineHandler::computeMismatchScore(const QImage &baseline, const QImage &rendered)
{
if (baseline.size() != rendered.size() || baseline.format() != rendered.format())
- return QLS("[No score, incomparable images.]");
+ return QLS("[No diffstats, incomparable images.]");
if (baseline.depth() != 32)
- return QLS("[Score computation not implemented for format.]");
+ return QLS("[Diffstats computation not implemented for format.]");
int w = baseline.width();
int h = baseline.height();
@@ -558,6 +735,8 @@ QString BaselineHandler::computeMismatchScore(const QImage &baseline, const QIma
uint nad = 0; // number of differing alpha pixels
uint scd = 0; // sum of color pixel difference
uint sad = 0; // sum of alpha pixel difference
+ uint mind = 0; // minimum difference
+ uint maxd = 0; // maximum difference
for (int y=0; y<h; ++y) {
const QRgb *bl = (const QRgb *) baseline.constScanLine(y);
@@ -566,14 +745,18 @@ QString BaselineHandler::computeMismatchScore(const QImage &baseline, const QIma
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));
+ uint dr = qAbs(qRed(b) - qRed(r));
+ uint dg = qAbs(qGreen(b) - qGreen(r));
+ uint db = qAbs(qBlue(b) - qBlue(r));
+ uint ds = (dr + dg + db) / 3;
+ uint da = qAbs(qAlpha(b) - qAlpha(r));
if (ds) {
ncd++;
scd += ds;
+ if (!mind || ds < mind)
+ mind = ds;
+ if (ds > maxd)
+ maxd = ds;
}
if (da) {
nad++;
@@ -583,13 +766,100 @@ QString BaselineHandler::computeMismatchScore(const QImage &baseline, const QIma
}
}
+
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);
+ double acd = ncd ? double(scd) / (ncd) : 0; // avg. difference
+/*
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);
}
+*/
+ QString res = "<table>\n";
+ QString item = "<tr><td>%1</td><td align=right>%2</td></tr>\n";
+ res += item.arg("Number of mismatching pixels").arg(ncd);
+ res += item.arg("Percentage mismatching pixels").arg(pcd, 0, 'g', 2);
+ res += item.arg("Minimum pixel distance").arg(mind);
+ res += item.arg("Maximum pixel distance").arg(maxd);
+ if (acd >= 10.0)
+ res += item.arg("Average pixel distance").arg(qRound(acd));
+ else
+ res += item.arg("Average pixel distance").arg(acd, 0, 'g', 2);
+
+ if (baseline.hasAlphaChannel())
+ res += item.arg("Number of mismatching alpha values").arg(nad);
+
+ res += "</table>\n";
+ res += "<p>(Distances are normalized to the range 0-255)</p>\n";
+ return res;
+}
+
+
+bool BaselineHandler::fuzzyCompare(const QString &baselinePath, const QString &mismatchPath)
+{
+ QProcess compareProc;
+ QStringList args;
+ args << "-fuzz" << QString("%1%").arg(fuzzLevel) << "-metric" << "AE";
+ args << baselinePath + QLS(FileFormat) << mismatchPath + QLS(FileFormat) << "/dev/null"; // TBD: Should save output image, so report won't have to regenerate it
+
+ compareProc.setProcessChannelMode(QProcess::MergedChannels);
+ compareProc.start("compare", args, QIODevice::ReadOnly);
+ if (compareProc.waitForFinished(3000) && compareProc.error() == QProcess::UnknownError) {
+ bool ok = false;
+ int metric = compareProc.readAll().trimmed().toInt(&ok);
+ if (ok && metric == 0)
+ return true;
+ }
+ return false;
+}
+
+
+void BaselineHandler::issueMismatchNotification()
+{
+ // KISS: hardcoded use of the "sendemail" utility. Make this configurable if and when demand arises.
+ if (!settings)
+ return;
+
+ settings->beginGroup("Notification");
+ QStringList receivers = settings->value("Receivers").toStringList();
+ QString sender = settings->value("Sender").toString();
+ QString server = settings->value("SMTPserver").toString();
+ settings->endGroup();
+ if (receivers.isEmpty() || sender.isEmpty() || server.isEmpty())
+ return;
+
+ QString msg = QString("\nResult summary for test run %1:\n").arg(runId);
+ msg += report.summary();
+ msg += "\nReport: " + BaselineServer::baseUrl() + report.filePath() + "\n";
+
+ msg += "\nTest run platform properties:\n------------------\n";
+ foreach (const QString &key, clientInfo.keys())
+ msg += key + ": " + clientInfo.value(key) + '\n';
+ msg += "\nCheers,\n- Your friendly Lancelot Baseline Server\n";
+
+ QProcess proc;
+ QString cmd = "sendemail";
+ QStringList args;
+ args << "-s" << server << "-f" << sender << "-t" << receivers;
+ args << "-u" << "[Lancelot] Mismatch report for project " + clientInfo.value(PI_Project) + ", test case " + clientInfo.value(PI_TestCase);
+ args << "-m" << msg;
+
+ //proc.setProcessChannelMode(QProcess::MergedChannels);
+ proc.start(cmd, args);
+ if (!proc.waitForFinished(10 * 1000) || (proc.exitStatus() != QProcess::NormalExit) || proc.exitCode()) {
+ qWarning() << "FAILED to issue notification. Command:" << cmd << args.mid(0, args.size()-2);
+ qWarning() << " Command standard output:" << proc.readAllStandardOutput();
+ qWarning() << " Command error output:" << proc.readAllStandardError();
+ }
+}
+
+
+// Make an identifer safer for use as filename and URL
+QString safeName(const QString& name)
+{
+ QString res = name.simplified();
+ res.replace(QLC(' '), QLC('_'));
+ res.replace(QLC('.'), QLC('_'));
+ res.replace(QLC('/'), QLC('^'));
return res;
}