blob: 3587cd01ea4783f2adbad7df4217c279e1a6ad3a [file] [log] [blame]
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the test suite of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:GPL-EXCEPT$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include "qbaselinetest.h"
#include "baselineprotocol.h"
#include <QtCore/QDir>
#include <QFile>
#define MAXCMDLINEARGS 128
namespace QBaselineTest {
static char *fargv[MAXCMDLINEARGS];
static bool simfail = false;
static PlatformInfo customInfo;
static bool customAutoModeSet = false;
static BaselineProtocol proto;
static bool connected = false;
static bool triedConnecting = false;
static bool dryRunMode = false;
static enum { UploadMissing, UploadAll, UploadNone } baselinePolicy = UploadMissing;
static QByteArray curFunction;
static ImageItemList itemList;
static bool gotBaselines;
static QString definedTestProject;
static QString definedTestCase;
void handleCmdLineArgs(int *argcp, char ***argvp)
{
if (!argcp || !argvp)
return;
bool showHelp = false;
int fargc = 0;
int numArgs = *argcp;
for (int i = 0; i < numArgs; i++) {
QByteArray arg = (*argvp)[i];
QByteArray nextArg = (i+1 < numArgs) ? (*argvp)[i+1] : 0;
if (arg == "-simfail") {
simfail = true;
} else if (arg == "-fuzzlevel") {
i++;
bool ok = false;
(void)nextArg.toInt(&ok);
if (!ok) {
qWarning() << "-fuzzlevel requires integer parameter";
showHelp = true;
break;
}
customInfo.insert("FuzzLevel", QString::fromLatin1(nextArg));
} else if (arg == "-auto") {
customAutoModeSet = true;
customInfo.setAdHocRun(false);
} else if (arg == "-adhoc") {
customAutoModeSet = true;
customInfo.setAdHocRun(true);
} else if (arg == "-setbaselines") {
baselinePolicy = UploadAll;
} else if (arg == "-nosetbaselines") {
baselinePolicy = UploadNone;
} else if (arg == "-compareto") {
i++;
int split = qMax(0, nextArg.indexOf('='));
QByteArray key = nextArg.left(split).trimmed();
QByteArray value = nextArg.mid(split+1).trimmed();
if (key.isEmpty() || value.isEmpty()) {
qWarning() << "-compareto requires parameter of the form <key>=<value>";
showHelp = true;
break;
}
customInfo.addOverride(key, value);
} else {
if ( (arg == "-help") || (arg == "--help") )
showHelp = true;
if (fargc >= MAXCMDLINEARGS) {
qWarning() << "Too many command line arguments!";
break;
}
fargv[fargc++] = (*argvp)[i];
}
}
*argcp = fargc;
*argvp = fargv;
if (showHelp) {
// TBD: arrange for this to be printed *after* QTest's help
QTextStream out(stdout);
out << "\n Baseline testing (lancelot) options:\n";
out << " -simfail : Force an image comparison mismatch. For testing purposes.\n";
out << " -fuzzlevel <int> : Specify the percentage of fuzziness in comparison. Overrides server default. 0 means exact match.\n";
out << " -auto : Inform server that this run is done by a daemon, CI system or similar.\n";
out << " -adhoc (default) : The inverse of -auto; this run is done by human, e.g. for testing.\n";
out << " -setbaselines : Store ALL rendered images as new baselines. Forces replacement of previous baselines.\n";
out << " -nosetbaselines : Do not store rendered images as new baselines when previous baselines are missing.\n";
out << " -compareto KEY=VAL : Force comparison to baselines from a different client,\n";
out << " for example: -compareto QtVersion=4.8.0\n";
out << " Multiple -compareto client specifications may be given.\n";
out << "\n";
}
}
void addClientProperty(const QString& key, const QString& value)
{
customInfo.insert(key, value);
}
/*
If a client property script is present, run it and accept its output
in the form of one 'key: value' property per line
*/
void fetchCustomClientProperties()
{
QFile file("hostinfo.txt");
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
return;
QTextStream in(&file);
while (!in.atEnd()) {
QString line = in.readLine().trimmed(); // ###local8bit? utf8?
if (line.startsWith(QLatin1Char('#'))) // Ignore comments in file
continue;
QString key, val;
int colonPos = line.indexOf(':');
if (colonPos > 0) {
key = line.left(colonPos).simplified().replace(' ', '_');
val = line.mid(colonPos+1).trimmed();
}
if (!key.isEmpty() && key.length() < 64 && val.length() < 256) // ###TBD: maximum 256 chars in value?
addClientProperty(key, val);
else
qDebug() << "Unparseable script output ignored:" << line;
}
}
bool connect(QByteArray *msg, bool *error)
{
if (connected) {
return true;
}
else if (triedConnecting) {
// Avoid repeated connection attempts, to avoid the program using Timeout * #testItems seconds before giving up
*msg = "Not connected to baseline server.";
*error = true;
return false;
}
triedConnecting = true;
fetchCustomClientProperties();
// Merge the platform info set by the program with the protocols default info
PlatformInfo clientInfo = customInfo;
PlatformInfo defaultInfo = PlatformInfo::localHostInfo();
foreach (QString key, defaultInfo.keys()) {
if (!clientInfo.contains(key))
clientInfo.insert(key, defaultInfo.value(key));
}
if (!customAutoModeSet)
clientInfo.setAdHocRun(defaultInfo.isAdHocRun());
if (!definedTestProject.isEmpty())
clientInfo.insert(PI_Project, definedTestProject);
QString testCase = definedTestCase;
if (testCase.isEmpty() && QTest::testObject() && QTest::testObject()->metaObject()) {
//qDebug() << "Trying to Read TestCaseName from Testlib!";
testCase = QTest::testObject()->metaObject()->className();
}
if (testCase.isEmpty()) {
qWarning("QBaselineTest::connect: No test case name specified, cannot connect.");
return false;
}
if (!proto.connect(testCase, &dryRunMode, clientInfo)) {
*msg += "Failed to connect to baseline server: " + proto.errorMessage().toLatin1();
*error = true;
return false;
}
connected = true;
return true;
}
bool disconnectFromBaselineServer()
{
if (proto.disconnect()) {
connected = false;
triedConnecting = false;
return true;
}
return false;
}
bool connectToBaselineServer(QByteArray *msg, const QString &testProject, const QString &testCase)
{
bool dummy;
QByteArray dummyMsg;
definedTestProject = testProject;
definedTestCase = testCase;
return connect(msg ? msg : &dummyMsg, &dummy);
}
void setAutoMode(bool mode)
{
customInfo.setAdHocRun(!mode);
customAutoModeSet = true;
}
void setSimFail(bool fail)
{
simfail = fail;
}
void modifyImage(QImage *img)
{
uint c0 = 0x0000ff00;
uint c1 = 0x0080ff00;
img->setPixel(1,1,c0);
img->setPixel(2,1,c1);
img->setPixel(3,1,c0);
img->setPixel(1,2,c1);
img->setPixel(1,3,c0);
img->setPixel(2,3,c1);
img->setPixel(3,3,c0);
img->setPixel(1,4,c1);
img->setPixel(1,5,c0);
}
bool compareItem(const ImageItem &baseline, const QImage &img, QByteArray *msg, bool *error)
{
ImageItem item = baseline;
if (simfail) {
// Simulate test failure by forcing image mismatch; for testing purposes
QImage misImg = img;
modifyImage(&misImg);
item.image = misImg;
simfail = false; // One failure is typically enough
} else {
item.image = img;
}
item.imageChecksums.clear();
item.imageChecksums.prepend(ImageItem::computeChecksum(item.image));
QByteArray srvMsg;
switch (baseline.status) {
case ImageItem::Ok:
break;
case ImageItem::IgnoreItem :
qDebug() << msg->constData() << "Ignored, blacklisted on server.";
return true;
break;
case ImageItem::BaselineNotFound:
if (!customInfo.overrides().isEmpty() || baselinePolicy == UploadNone) {
qWarning() << "Cannot compare to baseline: No such baseline found on server.";
return true;
}
if (proto.submitNewBaseline(item, &srvMsg))
qDebug() << msg->constData() << "Baseline not found on server. New baseline uploaded.";
else
qDebug() << msg->constData() << "Baseline not found on server. Uploading of new baseline failed:" << srvMsg;
return true;
break;
default:
qWarning() << "Unexpected reply from baseline server.";
return true;
break;
}
*error = false;
// The actual comparison of the given image with the baseline:
if (baseline.imageChecksums.contains(item.imageChecksums.at(0))) {
if (!proto.submitMatch(item, &srvMsg))
qWarning() << "Failed to report image match to server:" << srvMsg;
return true;
}
// At this point, we have established a legitimate mismatch
if (baselinePolicy == UploadAll) {
if (proto.submitNewBaseline(item, &srvMsg))
qDebug() << msg->constData() << "Forcing new baseline; uploaded ok.";
else
qDebug() << msg->constData() << "Forcing new baseline; uploading failed:" << srvMsg;
return true;
}
bool fuzzyMatch = false;
bool res = proto.submitMismatch(item, &srvMsg, &fuzzyMatch);
if (res && fuzzyMatch) {
*error = true; // To force a QSKIP/debug output; somewhat kludgy
*msg += srvMsg;
return true; // The server decides: a fuzzy match means no mismatch
}
*msg += "Mismatch. See report:\n " + srvMsg;
if (dryRunMode) {
qDebug() << "Dryrun, so ignoring" << *msg;
return true;
}
return false;
}
bool checkImage(const QImage &img, const char *name, quint16 checksum, QByteArray *msg, bool *error, int manualdatatag)
{
if (!connected && !connect(msg, error))
return true;
QByteArray itemName;
bool hasName = qstrlen(name);
const char *tag = QTest::currentDataTag();
if (qstrlen(tag)) {
itemName = tag;
if (hasName)
itemName.append('_').append(name);
} else {
itemName = hasName ? name : "default_name";
}
if (manualdatatag > 0)
{
itemName.prepend("_");
itemName.prepend(QByteArray::number(manualdatatag));
}
*msg = "Baseline check of image '" + itemName + "': ";
ImageItem item;
item.itemName = QString::fromLatin1(itemName);
item.itemChecksum = checksum;
item.testFunction = QString::fromLatin1(QTest::currentTestFunction());
ImageItemList list;
list.append(item);
if (!proto.requestBaselineChecksums(QLatin1String(QTest::currentTestFunction()), &list) || list.isEmpty()) {
*msg = "Communication with baseline server failed: " + proto.errorMessage().toLatin1();
*error = true;
return true;
}
return compareItem(list.at(0), img, msg, error);
}
QTestData &newRow(const char *dataTag, quint16 checksum)
{
if (QTest::currentTestFunction() != curFunction) {
curFunction = QTest::currentTestFunction();
itemList.clear();
gotBaselines = false;
}
ImageItem item;
item.itemName = QString::fromLatin1(dataTag);
item.itemChecksum = checksum;
item.testFunction = QString::fromLatin1(QTest::currentTestFunction());
itemList.append(item);
return QTest::newRow(dataTag);
}
bool testImage(const QImage& img, QByteArray *msg, bool *error)
{
if (!connected && !connect(msg, error))
return true;
if (QTest::currentTestFunction() != curFunction || itemList.isEmpty()) {
qWarning() << "Usage error: QBASELINE_TEST used without corresponding QBaselineTest::newRow()";
return true;
}
if (!gotBaselines) {
if (!proto.requestBaselineChecksums(QString::fromLatin1(QTest::currentTestFunction()), &itemList) || itemList.isEmpty()) {
*msg = "Communication with baseline server failed: " + proto.errorMessage().toLatin1();
*error = true;
return true;
}
gotBaselines = true;
}
QString curTag = QString::fromLatin1(QTest::currentDataTag());
ImageItemList::const_iterator it = itemList.constBegin();
while (it != itemList.constEnd() && it->itemName != curTag)
++it;
if (it == itemList.constEnd()) {
qWarning() << "Usage error: QBASELINE_TEST used without corresponding QBaselineTest::newRow() for row" << curTag;
return true;
}
return compareItem(*it, img, msg, error);
}
}