From aa9728450cc515c66545323646c66d826a1af50a Mon Sep 17 00:00:00 2001 From: aavit Date: Mon, 13 Aug 2012 14:13:40 +0200 Subject: 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 --- tests/baselineserver/src/baselineserver.cpp | 422 +++++++++++++++++++++++----- 1 file changed, 346 insertions(+), 76 deletions(-) (limited to 'tests/baselineserver/src/baselineserver.cpp') 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 #include #include +#include // 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 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 = "\n"; + QString item = "\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 += "
%1%2
\n"; + res += "

(Distances are normalized to the range 0-255)

\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; } -- cgit v1.2.3