summaryrefslogtreecommitdiffstats
path: root/tests/baselineserver/src
diff options
context:
space:
mode:
Diffstat (limited to 'tests/baselineserver/src')
-rw-r--r--tests/baselineserver/src/baselineserver.cpp422
-rw-r--r--tests/baselineserver/src/baselineserver.h32
-rw-r--r--tests/baselineserver/src/report.cpp270
-rw-r--r--tests/baselineserver/src/report.h27
-rw-r--r--tests/baselineserver/src/templates/view.html9
5 files changed, 632 insertions, 128 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;
}
diff --git a/tests/baselineserver/src/baselineserver.h b/tests/baselineserver/src/baselineserver.h
index c8ce3860fb..d30a74195d 100644
--- a/tests/baselineserver/src/baselineserver.h
+++ b/tests/baselineserver/src/baselineserver.h
@@ -53,9 +53,14 @@
#include "baselineprotocol.h"
#include "report.h"
-// #seconds between update checks
+// #seconds between checks for update of the executable
#define HEARTBEAT 10
+// Timeout if no activity received from client, #seconds
+#define IDLE_CLIENT_TIMEOUT 3*60
+
#define MetadataFileExt "metadata"
+#define ThumbnailExt "thumbnail.jpg"
+
class BaselineServer : public QTcpServer
{
@@ -66,7 +71,7 @@ public:
static QString storagePath();
static QString baseUrl();
- static QString settingsFilePath();
+ static QStringList defaultPathKeys();
protected:
void incomingConnection(qintptr socketDescriptor);
@@ -81,7 +86,7 @@ private:
int lastRunIdIdx;
static QString storage;
static QString url;
- static QString settingsFile;
+ static QStringList pathKeys;
};
@@ -106,28 +111,38 @@ class BaselineHandler : public QObject
public:
BaselineHandler(const QString &runId, int socketDescriptor = -1);
- void testPathMapping();
+ QString projectPath(bool absolute = true) const;
QString pathForItem(const ImageItem &item, bool isBaseline = true, bool absolute = true) const;
// CGI callbacks:
static QString view(const QString &baseline, const QString &rendered, const QString &compared);
+ static QString diffstats(const QString &baseline, const QString &rendered);
static QString clearAllBaselines(const QString &context);
static QString updateBaselines(const QString &context, const QString &mismatchContext, const QString &itemFile);
static QString blacklistTest(const QString &context, const QString &itemId, bool removeFromBlacklist = false);
+ // for debugging
+ void testPathMapping();
+
private slots:
void receiveRequest();
void receiveDisconnect();
+ void idleClientTimeout();
private:
+ bool checkClient(QByteArray *errMsg, bool *dryRunMode = 0);
bool establishConnection();
void provideBaselineChecksums(const QByteArray &itemListBlock);
+ void recordMatch(const QByteArray &itemBlock);
void storeImage(const QByteArray &itemBlock, bool isBaseline);
void storeItemMetadata(const PlatformInfo &metadata, const QString &path);
PlatformInfo fetchItemMetadata(const QString &path);
PlatformInfo mapPlatformInfo(const PlatformInfo& orig) const;
const char *logtime();
- QString computeMismatchScore(const QImage& baseline, const QImage& rendered);
+ void issueMismatchNotification();
+ bool fuzzyCompare(const QString& baselinePath, const QString& mismatchPath);
+
+ static QString computeMismatchScore(const QImage& baseline, const QImage& rendered);
BaselineProtocol proto;
PlatformInfo clientInfo;
@@ -137,6 +152,13 @@ private:
bool connectionEstablished;
Report report;
QSettings *settings;
+ QString ruleName;
+ int fuzzLevel;
+ QTimer *idleTimer;
};
+
+// Make an identifer safer for use as filename and URL
+QString safeName(const QString& name);
+
#endif // BASELINESERVER_H
diff --git a/tests/baselineserver/src/report.cpp b/tests/baselineserver/src/report.cpp
index d29f22e2a3..cd544a6c78 100644
--- a/tests/baselineserver/src/report.cpp
+++ b/tests/baselineserver/src/report.cpp
@@ -44,9 +44,11 @@
#include <QDir>
#include <QProcess>
#include <QUrl>
+#include <QXmlStreamWriter>
Report::Report()
- : written(false), numItems(0), numMismatches(0)
+ : initialized(false), handler(0), written(false), numItems(0), numMismatches(0), settings(0),
+ hasStats(false)
{
}
@@ -60,19 +62,31 @@ QString Report::filePath()
return path;
}
-void Report::init(const BaselineHandler *h, const QString &r, const PlatformInfo &p)
+int Report::numberOfMismatches()
+{
+ return numMismatches;
+}
+
+bool Report::reportProduced()
+{
+ return written;
+}
+
+void Report::init(const BaselineHandler *h, const QString &r, const PlatformInfo &p, const QSettings *s)
{
handler = h;
runId = r;
plat = p;
+ settings = s;
rootDir = BaselineServer::storagePath() + QLC('/');
- reportDir = plat.value(PI_TestCase) + QLC('/') + (plat.isAdHocRun() ? QLS("reports/adhoc/") : QLS("reports/pulse/"));
- QString dir = rootDir + reportDir;
+ baseDir = handler->pathForItem(ImageItem(), true, false).remove(QRegExp("/baselines/.*$"));
+ QString dir = baseDir + (plat.isAdHocRun() ? QLS("/adhoc-reports") : QLS("/auto-reports"));
QDir cwd;
- if (!cwd.exists(dir))
- cwd.mkpath(dir);
- path = reportDir + QLS("Report_") + runId + QLS(".html");
+ if (!cwd.exists(rootDir + dir))
+ cwd.mkpath(rootDir + dir);
+ path = dir + QLS("/Report_") + runId + QLS(".html");
hasOverride = !plat.overrides().isEmpty();
+ initialized = true;
}
void Report::addItems(const ImageItemList &items)
@@ -83,38 +97,110 @@ void Report::addItems(const ImageItemList &items)
QString func = items.at(0).testFunction;
if (!testFunctions.contains(func))
testFunctions.append(func);
- itemLists[func] += items;
+ ImageItemList list = items;
+ if (settings->value("ReportMissingResults").toBool()) {
+ for (ImageItemList::iterator it = list.begin(); it != list.end(); ++it) {
+ if (it->status == ImageItem::Ok)
+ it->status = ImageItem::Error; // Status should be set by report from client, else report as error
+ }
+ }
+ itemLists[func] += list;
}
-void Report::addMismatch(const ImageItem &item)
+void Report::addResult(const ImageItem &item)
{
if (!testFunctions.contains(item.testFunction)) {
- qWarning() << "Report::addMismatch: unknown testfunction" << item.testFunction;
+ qWarning() << "Report::addResult: unknown testfunction" << item.testFunction;
return;
}
bool found = false;
ImageItemList &list = itemLists[item.testFunction];
for (ImageItemList::iterator it = list.begin(); it != list.end(); ++it) {
if (it->itemName == item.itemName && it->itemChecksum == item.itemChecksum) {
- it->status = ImageItem::Mismatch;
+ it->status = item.status;
found = true;
break;
}
}
- if (found)
- numMismatches++;
- else
- qWarning() << "Report::addMismatch: unknown item" << item.itemName << "in testfunction" << item.testFunction;
+ if (found) {
+ if (item.status == ImageItem::Mismatch)
+ numMismatches++;
+ } else {
+ qWarning() << "Report::addResult: unknown item" << item.itemName << "in testfunction" << item.testFunction;
+ }
}
void Report::end()
{
- if (written || !numMismatches)
+ if (!initialized || written)
+ return;
+ // Make report iff (#mismatches>0) || (#fuzzymatches>0) || (#errors>0 && settings say report errors)
+ bool doReport = (numMismatches > 0);
+ if (!doReport) {
+ bool reportErrors = settings->value("ReportMissingResults").toBool();
+ computeStats();
+ foreach (const QString &func, itemLists.keys()) {
+ FuncStats stat = stats.value(func);
+ if (stat.value(ImageItem::FuzzyMatch) > 0) {
+ doReport = true;
+ break;
+ }
+ foreach (const ImageItem &item, itemLists.value(func)) {
+ if (reportErrors && item.status == ImageItem::Error) {
+ doReport = true;
+ break;
+ }
+ }
+ if (doReport)
+ break;
+ }
+ }
+ if (!doReport)
return;
write();
written = true;
}
+void Report::computeStats()
+{
+ if (hasStats)
+ return;
+ foreach (const QString &func, itemLists.keys()) {
+ FuncStats funcStat;
+ funcStat[ImageItem::Ok] = 0;
+ funcStat[ImageItem::BaselineNotFound] = 0;
+ funcStat[ImageItem::IgnoreItem] = 0;
+ funcStat[ImageItem::Mismatch] = 0;
+ funcStat[ImageItem::FuzzyMatch] = 0;
+ funcStat[ImageItem::Error] = 0;
+ foreach (const ImageItem &item, itemLists.value(func)) {
+ funcStat[item.status]++;
+ }
+ stats[func] = funcStat;
+ }
+ hasStats = true;
+}
+
+QString Report::summary()
+{
+ computeStats();
+ QString res;
+ foreach (const QString &func, itemLists.keys()) {
+ FuncStats stat = stats.value(func);
+ QString s = QString("%1 %3 mismatch(es), %4 error(s), %5 fuzzy match(es)\n");
+ s = s.arg(QString("%1() [%2 items]:").arg(func).arg(itemLists.value(func).size()).leftJustified(40));
+ s = s.arg(stat.value(ImageItem::Mismatch));
+ s = s.arg(stat.value(ImageItem::Error));
+ s = s.arg(stat.value(ImageItem::FuzzyMatch));
+ res += s;
+ }
+#if 0
+ qDebug() << "***************************** Summary *************************";
+ qDebug() << res;
+ qDebug() << "***************************************************************";
+#endif
+ return res;
+}
void Report::write()
{
@@ -131,24 +217,27 @@ void Report::write()
}
writeFooter();
file.close();
+ updateLatestPointer();
}
void Report::writeHeader()
{
- QString title = plat.value(PI_TestCase) + QLS(" Qt Baseline Test Report");
- out << "<head><title>" << title << "</title></head>\n"
- << "<html><body><h1>" << title << "</h1>\n"
+ QString title = plat.value(PI_Project) + QLC(':') + plat.value(PI_TestCase) + QLS(" Lancelot Test Report");
+ out << "<!DOCTYPE html>\n"
+ << "<html><head><title>" << title << "</title></head>\n"
+ << "<body bgcolor=""#ddeeff""><h1>" << title << "</h1>\n"
<< "<p>Note: This is a <i>static</i> page, generated at " << QDateTime::currentDateTime().toString()
<< " for the test run with id " << runId << "</p>\n"
- << "<p>Summary: <b><span style=\"color:red\">" << numMismatches << " of " << numItems << "</b></span> items reported mismatching</p>\n\n";
+ << "<p>Summary: <b><span style=\"color:red\">" << numMismatches << " of " << numItems << "</span></b> items reported mismatching</p>\n";
+ out << "<pre>\n" << summary() << "</pre>\n\n";
out << "<h3>Testing Client Platform Info:</h3>\n"
<< "<table>\n";
foreach (QString key, plat.keys())
out << "<tr><td>" << key << ":</td><td>" << plat.value(key) << "</td></tr>\n";
out << "</table>\n\n";
if (hasOverride) {
- out << "<span style=\"color:red\"><h4>Note! Platform Override Info:</h4></span>\n"
+ out << "<span style=\"color:red\"><h4>Note! Override Platform Info:</h4></span>\n"
<< "<p>The client's output has been compared to baselines created on a different platform. Differences:</p>\n"
<< "<table>\n";
for (int i = 0; i < plat.overrides().size()-1; i+=2)
@@ -184,14 +273,25 @@ void Report::writeFunctionResults(const ImageItemList &list)
"</tr>\n\n";
foreach (const ImageItem &item, list) {
+ QString mmPrefix = handler->pathForItem(item, false, false);
+ QString blPrefix = handler->pathForItem(item, true, false);
+
+ // Make hard links to the current baseline, so that the report is static even if the baseline changes
+ generateThumbnail(blPrefix + QLS(FileFormat), rootDir); // Make sure baseline thumbnail is up to date
+ QString lnPrefix = mmPrefix + QLS("baseline.");
+ QByteArray blPrefixBa = (rootDir + blPrefix).toLatin1();
+ QByteArray lnPrefixBa = (rootDir + lnPrefix).toLatin1();
+ ::link((blPrefixBa + FileFormat).constData(), (lnPrefixBa + FileFormat).constData());
+ ::link((blPrefixBa + MetadataFileExt).constData(), (lnPrefixBa + MetadataFileExt).constData());
+ ::link((blPrefixBa + ThumbnailExt).constData(), (lnPrefixBa + ThumbnailExt).constData());
+
+ QString baseline = lnPrefix + QLS(FileFormat);
+ QString metadata = lnPrefix + QLS(MetadataFileExt);
out << "<tr>\n";
out << "<td>" << item.itemName << "</td>\n";
- QString prefix = handler->pathForItem(item, true, false);
- QString baseline = prefix + QLS(FileFormat);
- QString metadata = prefix + QLS(MetadataFileExt);
- if (item.status == ImageItem::Mismatch) {
- QString rendered = handler->pathForItem(item, false, false) + QLS(FileFormat);
- QString itemFile = prefix.section(QLC('/'), -1);
+ if (item.status == ImageItem::Mismatch || item.status == ImageItem::FuzzyMatch) {
+ QString rendered = mmPrefix + QLS(FileFormat);
+ QString itemFile = mmPrefix.section(QLC('/'), -1);
writeItem(baseline, rendered, item, itemFile, ctx, misCtx, metadata);
}
else {
@@ -210,6 +310,9 @@ void Report::writeFunctionResults(const ImageItemList &list)
<< "\">Whitelist this item</a>";
}
break;
+ case ImageItem::Error:
+ out << "<span style=\"background-color:red\">Error: No result reported!</span>";
+ break;
case ImageItem::Ok:
out << "<span style=\"color:green\"><small>No mismatch reported</small></span>";
break;
@@ -233,11 +336,14 @@ void Report::writeItem(const QString &baseline, const QString &rendered, const I
QStringList images = QStringList() << baseline << rendered << compared;
foreach (const QString& img, images)
- out << "<td height=246 align=center><a href=\"/" << img << "\"><img src=\"/" << generateThumbnail(img) << "\"></a></td>\n";
+ out << "<td height=246 align=center><a href=\"/" << img << "\"><img src=\"/" << generateThumbnail(img, rootDir) << "\"></a></td>\n";
- out << "<td align=center>\n"
- << "<p><span style=\"color:red\">Mismatch reported</span></p>\n"
- << "<p><a href=\"/" << metadata << "\">Baseline Info</a>\n";
+ out << "<td align=center>\n";
+ if (item.status == ImageItem::FuzzyMatch)
+ out << "<p><span style=\"color:orange\">Fuzzy match</span></p>\n";
+ else
+ out << "<p><span style=\"color:red\">Mismatch reported</span></p>\n";
+ out << "<p><a href=\"/" << metadata << "\">Baseline Info</a>\n";
if (!hasOverride) {
out << "<p><a href=\"/cgi-bin/server.cgi?cmd=updateSingleBaseline&context=" << ctx << "&mismatchContext=" << misCtx
<< "&itemFile=" << itemFile << "&url=" << pageUrl << "\">Let this be the new baseline</a></p>\n"
@@ -245,8 +351,14 @@ void Report::writeItem(const QString &baseline, const QString &rendered, const I
<< "&itemId=" << item.itemName << "&url=" << pageUrl << "\">Blacklist this item</a></p>\n";
}
out << "<p><a href=\"/cgi-bin/server.cgi?cmd=view&baseline=" << baseline << "&rendered=" << rendered
- << "&compared=" << compared << "&url=" << pageUrl << "\">Inspect</a></p>\n"
- << "</td>\n";
+ << "&compared=" << compared << "&url=" << pageUrl << "\">Inspect</a></p>\n";
+
+#if 0
+ out << "<p><a href=\"/cgi-bin/server.cgi?cmd=diffstats&baseline=" << baseline << "&rendered=" << rendered
+ << "&url=" << pageUrl << "\">Diffstats</a></p>\n";
+#endif
+
+ out << "</td>\n";
}
void Report::writeFooter()
@@ -259,8 +371,8 @@ QString Report::generateCompared(const QString &baseline, const QString &rendere
{
QString res = rendered;
QFileInfo fi(res);
- res.chop(fi.suffix().length() + 1);
- res += QLS(fuzzy ? "_fuzzycompared.png" : "_compared.png");
+ res.chop(fi.suffix().length());
+ res += QLS(fuzzy ? "fuzzycompared.png" : "compared.png");
QStringList args;
if (fuzzy)
args << QLS("-fuzz") << QLS("5%");
@@ -270,12 +382,14 @@ QString Report::generateCompared(const QString &baseline, const QString &rendere
}
-QString Report::generateThumbnail(const QString &image)
+QString Report::generateThumbnail(const QString &image, const QString &rootDir)
{
QString res = image;
QFileInfo imgFI(rootDir+image);
- res.chop(imgFI.suffix().length() + 1);
- res += QLS("_thumbnail.jpg");
+ if (!imgFI.exists())
+ return res;
+ res.chop(imgFI.suffix().length());
+ res += ThumbnailExt;
QFileInfo resFI(rootDir+res);
if (resFI.exists() && resFI.lastModified() > imgFI.lastModified())
return res;
@@ -286,12 +400,80 @@ QString Report::generateThumbnail(const QString &image)
}
+QString Report::writeResultsXmlFiles()
+{
+ if (!itemLists.size())
+ return QString();
+ QString dir = rootDir + baseDir + QLS("/xml-reports/") + runId;
+ QDir cwd;
+ if (!cwd.exists(dir))
+ cwd.mkpath(dir);
+ foreach (const QString &func, itemLists.keys()) {
+ QFile f(dir + "/" + func + "-results.xml");
+ if (!f.open(QIODevice::WriteOnly))
+ continue;
+ QXmlStreamWriter s(&f);
+ s.setAutoFormatting(true);
+ s.writeStartDocument();
+ foreach (QString key, plat.keys()) {
+ QString cmt = " " + key + "=\"" + plat.value(key) +"\" ";
+ s.writeComment(cmt.replace("--", "[-]"));
+ }
+ s.writeStartElement("testsuite");
+ s.writeAttribute("name", func);
+ foreach (const ImageItem &item, itemLists.value(func)) {
+ QString res;
+ switch (item.status) {
+ case ImageItem::Ok:
+ case ImageItem::FuzzyMatch:
+ res = "pass";
+ break;
+ case ImageItem::Mismatch:
+ case ImageItem::Error:
+ res = "fail";
+ break;
+ case ImageItem::BaselineNotFound:
+ case ImageItem::IgnoreItem:
+ default:
+ res = "skip";
+ }
+ s.writeStartElement("testcase");
+ s.writeAttribute("name", item.itemName);
+ s.writeAttribute("result", res);
+ s.writeEndElement();
+ }
+ s.writeEndElement();
+ s.writeEndDocument();
+ }
+ return dir;
+}
+
+
+
+void Report::updateLatestPointer()
+{
+ QString linkPath = rootDir + baseDir + QLS("/latest_report.html");
+ QString reportPath = path.mid(baseDir.size()+1);
+ QFile::remove(linkPath); // possible race with another thread, yada yada yada
+ QFile::link(reportPath, linkPath);
+
+#if 0
+ QByteArray fwd = "<!DOCTYPE html><html><head><meta HTTP-EQUIV=\"refresh\" CONTENT=\"0;URL=%1\"></meta></head><body></body></html>\n";
+ fwd.replace("%1", filePath().prepend(QLC('/')).toLatin1());
+
+ QFile file(rootDir + baseDir + "/latest_report.html");
+ if (file.open(QIODevice::WriteOnly | QIODevice::Truncate))
+ file.write(fwd);
+#endif
+}
+
+
void Report::handleCGIQuery(const QString &query)
{
QUrl cgiUrl(QLS("http://dummy/cgi-bin/dummy.cgi?") + query);
QTextStream s(stdout);
s << "Content-Type: text/html\r\n\r\n"
- << "<HTML>";
+ << "<!DOCTYPE html>\n<HTML>\n<body bgcolor=""#ddeeff"">\n"; // Lancelot blue
QString command(cgiUrl.queryItemValue("cmd"));
@@ -300,6 +482,12 @@ void Report::handleCGIQuery(const QString &query)
cgiUrl.queryItemValue(QLS("rendered")),
cgiUrl.queryItemValue(QLS("compared")));
}
+#if 0
+ else if (command == QLS("diffstats")) {
+ s << BaselineHandler::diffstats(cgiUrl.queryItemValue(QLS("baseline")),
+ cgiUrl.queryItemValue(QLS("rendered")));
+ }
+#endif
else if (command == QLS("updateSingleBaseline")) {
s << BaselineHandler::updateBaselines(cgiUrl.queryItemValue(QLS("context")),
cgiUrl.queryItemValue(QLS("mismatchContext")),
@@ -321,6 +509,6 @@ void Report::handleCGIQuery(const QString &query)
} else {
s << "Unknown query:<br>" << query << "<br>";
}
- s << "<p><a href=\"" << cgiUrl.queryItemValue(QLS("url")) << "\">Back to report</a>";
- s << "</HTML>";
+ s << "<p><a href=\"" << cgiUrl.queryItemValue(QLS("url")) << "\">Back to report</a>\n";
+ s << "</body>\n</HTML>";
}
diff --git a/tests/baselineserver/src/report.h b/tests/baselineserver/src/report.h
index 660593784e..918d09d420 100644
--- a/tests/baselineserver/src/report.h
+++ b/tests/baselineserver/src/report.h
@@ -46,6 +46,7 @@
#include <QTextStream>
#include <QMap>
#include <QStringList>
+#include <QSettings>
class BaselineHandler;
@@ -55,15 +56,24 @@ public:
Report();
~Report();
- void init(const BaselineHandler *h, const QString &r, const PlatformInfo &p);
+ void init(const BaselineHandler *h, const QString &r, const PlatformInfo &p, const QSettings *s);
void addItems(const ImageItemList& items);
- void addMismatch(const ImageItem& item);
+ void addResult(const ImageItem& item);
void end();
+ bool reportProduced();
+
+ int numberOfMismatches();
+ QString summary();
+
QString filePath();
+ QString writeResultsXmlFiles();
+
static void handleCGIQuery(const QString &query);
+ static QString generateThumbnail(const QString &image, const QString &rootDir = QString());
+
private:
void write();
void writeFunctionResults(const ImageItemList &list);
@@ -72,13 +82,17 @@ private:
void writeHeader();
void writeFooter();
QString generateCompared(const QString &baseline, const QString &rendered, bool fuzzy = false);
- QString generateThumbnail(const QString &image);
+ void updateLatestPointer();
+
+ void computeStats();
+
+ bool initialized;
const BaselineHandler *handler;
QString runId;
PlatformInfo plat;
QString rootDir;
- QString reportDir;
+ QString baseDir;
QString path;
QStringList testFunctions;
QMap<QString, ImageItemList> itemLists;
@@ -87,6 +101,11 @@ private:
int numMismatches;
QTextStream out;
bool hasOverride;
+ const QSettings *settings;
+
+ typedef QMap<ImageItem::ItemStatus, int> FuncStats;
+ QMap<QString, FuncStats> stats;
+ bool hasStats;
};
#endif // REPORT_H
diff --git a/tests/baselineserver/src/templates/view.html b/tests/baselineserver/src/templates/view.html
index c048f4781c..f0971010f2 100644
--- a/tests/baselineserver/src/templates/view.html
+++ b/tests/baselineserver/src/templates/view.html
@@ -23,9 +23,14 @@ Zoom:
</table></p>
-<p>
+<p><table cellspacing="25"><tr>
+<td valign="top">
<canvas id="c" width="800" height="800"></canvas>
-</p>
+</td>
+<td valign="top">
+%4
+</td>
+</tr></table></p>
<script>
var canvas = document.getElementById("c");