// Copyright (C) 2017 Crimson AS // Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "quicktestresult_p.h" #include "quicktest.h" #include "quicktest_p.h" #include #include #include #include #include #include #include "qtestoptions_p.h" #include #include #include #include #include #include #include #include #include #include #if QT_CONFIG(regularexpression) #include #endif #include #include #include #include #include #include #include #include QT_BEGIN_NAMESPACE static const char *globalProgramName = nullptr; static bool loggingStarted = false; static QBenchmarkGlobalData globalBenchmarkData; class Q_QMLTEST_EXPORT QuickTestImageObject : public QObject { Q_OBJECT Q_PROPERTY(int width READ width CONSTANT) Q_PROPERTY(int height READ height CONSTANT) Q_PROPERTY(QSize size READ size CONSTANT) public: QuickTestImageObject(const QImage& img, QObject *parent = nullptr) : QObject(parent) , m_image(img) { } ~QuickTestImageObject() {} public Q_SLOTS: int red(int x, int y) const { return pixel(x, y).value().red(); } int green(int x, int y) const { return pixel(x, y).value().green(); } int blue(int x, int y) const { return pixel(x, y).value().blue(); } int alpha(int x, int y) const { return pixel(x, y).value().alpha(); } QVariant pixel(int x, int y) const { if (m_image.isNull() || x >= m_image.width() || y >= m_image.height() || x < 0 || y < 0 || x * y >= m_image.width() * m_image.height()) return QVariant(); return QColor::fromRgba(m_image.pixel(QPoint(x, y))); } bool equals(QuickTestImageObject *other) const { if (!other) return m_image.isNull(); return m_image == other->m_image; } void save(const QString &filePath) { QImageWriter writer(filePath); if (!writer.write(m_image)) { QQmlEngine *engine = qmlContext(this)->engine(); QV4::ExecutionEngine *v4 = engine->handle(); v4->throwError(QStringLiteral("Can't save to %1: %2").arg(filePath, writer.errorString())); } } public: int width() const { return m_image.width(); } int height() const { return m_image.height(); } QSize size() const { return m_image.size(); } private: QImage m_image; }; class QuickTestResultPrivate { public: QuickTestResultPrivate() : table(nullptr) , benchmarkIter(nullptr) , benchmarkData(nullptr) , iterCount(0) { } ~QuickTestResultPrivate() { delete table; delete benchmarkIter; delete benchmarkData; } QByteArray intern(const QString &str); QString testCaseName; QString functionName; QSet internedStrings; QTestTable *table; QTest::QBenchmarkIterationController *benchmarkIter; QBenchmarkTestMethodData *benchmarkData; int iterCount; QList> resultsList; }; QByteArray QuickTestResultPrivate::intern(const QString &str) { QByteArray bstr = str.toUtf8(); return *(internedStrings.insert(bstr)); } QuickTestResult::QuickTestResult(QObject *parent) : QObject(parent), d_ptr(new QuickTestResultPrivate) { if (!QBenchmarkGlobalData::current) QBenchmarkGlobalData::current = &globalBenchmarkData; } QuickTestResult::~QuickTestResult() { } /*! \qmlproperty string TestResult::testCaseName This property defines the name of current TestCase element that is running test cases. \sa functionName */ QString QuickTestResult::testCaseName() const { Q_D(const QuickTestResult); return d->testCaseName; } void QuickTestResult::setTestCaseName(const QString &name) { Q_D(QuickTestResult); d->testCaseName = name; emit testCaseNameChanged(); } /*! \qmlproperty string TestResult::functionName This property defines the name of current test function within a TestCase element that is running. If this string is empty, then no function is currently running. \sa testCaseName */ QString QuickTestResult::functionName() const { Q_D(const QuickTestResult); return d->functionName; } void QuickTestResult::setFunctionName(const QString &name) { Q_D(QuickTestResult); if (!name.isEmpty()) { if (d->testCaseName.isEmpty()) { QTestResult::setCurrentTestFunction (d->intern(name).constData()); } else { QString fullName = d->testCaseName + QLatin1String("::") + name; QTestResult::setCurrentTestFunction (d->intern(fullName).constData()); if (QTestPrivate::checkBlackLists(fullName.toUtf8().constData(), nullptr)) QTestResult::setBlacklistCurrentTest(true); } } else { QTestResult::setCurrentTestFunction(nullptr); } d->functionName = name; emit functionNameChanged(); } /*! \qmlproperty string TestResult::dataTag This property defines the tag for the current row in a data-driven test, or an empty string if not a data-driven test. */ QString QuickTestResult::dataTag() const { const char *tag = QTestResult::currentDataTag(); if (tag) return QString::fromUtf8(tag); else return QString(); } void QuickTestResult::setDataTag(const QString &tag) { if (!tag.isEmpty()) { QTestData *data = &(QTest::newRow(tag.toUtf8().constData())); QTestResult::setCurrentTestData(data); if (QTestPrivate::checkBlackLists((testCaseName() + QLatin1String("::") + functionName()).toUtf8().constData(), tag.toUtf8().constData())) { QTestResult::setBlacklistCurrentTest(true); } emit dataTagChanged(); } else { QTestResult::setCurrentTestData(nullptr); } } /*! \qmlproperty bool TestResult::failed This property returns true if the current test function (or current test data row for a data-driven test) has failed; false otherwise. The fail state is reset when functionName is changed or finishTestDataCleanup() is called. \sa skipped */ bool QuickTestResult::isFailed() const { return QTestResult::currentTestFailed(); } /*! \qmlproperty bool TestResult::skipped This property returns true if the current test function was marked as skipped; false otherwise. \sa failed */ bool QuickTestResult::isSkipped() const { return QTestResult::skipCurrentTest(); } void QuickTestResult::setSkipped(bool skip) { QTestResult::setSkipCurrentTest(skip); if (!skip) QTestResult::setBlacklistCurrentTest(false); emit skippedChanged(); } /*! \qmlproperty int TestResult::passCount This property returns the number of tests that have passed. \sa failCount, skipCount */ int QuickTestResult::passCount() const { return QTestLog::passCount(); } /*! \qmlproperty int TestResult::failCount This property returns the number of tests that have failed. \sa passCount, skipCount */ int QuickTestResult::failCount() const { return QTestLog::failCount(); } /*! \qmlproperty int TestResult::skipCount This property returns the number of tests that have been skipped. \sa passCount, failCount */ int QuickTestResult::skipCount() const { return QTestLog::skipCount(); } /*! \qmlproperty list TestResult::functionsToRun This property returns the list of function names to be run. */ QStringList QuickTestResult::functionsToRun() const { return QTest::testFunctions; } /*! \qmlproperty list TestResult::tagsToRun This property returns the list of test function's data tags to be run */ QStringList QuickTestResult::tagsToRun() const { return QTest::testTags; } /*! \qmlmethod TestResult::reset() Resets all pass/fail/skip counters and prepare for testing. */ void QuickTestResult::reset() { if (!globalProgramName) // Only if run via qmlviewer. QTestResult::reset(); } /*! \qmlmethod TestResult::startLogging() Starts logging to the test output stream and writes the test header. \sa stopLogging() */ void QuickTestResult::startLogging() { // The program name is used for logging headers and footers if it // is set. Otherwise the test case name is used. if (loggingStarted) return; QTestLog::startLogging(); loggingStarted = true; } /*! \qmlmethod TestResult::stopLogging() Writes the test footer to the test output stream and then stops logging. \sa startLogging() */ void QuickTestResult::stopLogging() { Q_D(QuickTestResult); if (globalProgramName) return; // Logging will be stopped by setProgramName(0). QTestResult::setCurrentTestObject(d->intern(d->testCaseName).constData()); QTestLog::stopLogging(); } void QuickTestResult::initTestTable() { Q_D(QuickTestResult); delete d->table; d->table = new QTestTable; //qmltest does not really need the column for data driven test //add this to avoid warnings. d->table->addColumn(qMetaTypeId(), "qmltest_dummy_data_column"); } void QuickTestResult::clearTestTable() { Q_D(QuickTestResult); delete d->table; d->table = nullptr; } void QuickTestResult::finishTestData() { QTestResult::finishedCurrentTestData(); } void QuickTestResult::finishTestDataCleanup() { QTestResult::finishedCurrentTestDataCleanup(); } void QuickTestResult::finishTestFunction() { QTestResult::finishedCurrentTestFunction(); } static QString qtestFixUrl(const QUrl &location) { if (location.isLocalFile()) // Use QUrl's logic for Windows drive letters. return QDir::toNativeSeparators(location.toLocalFile()); return location.toString(); } void QuickTestResult::fail (const QString &message, const QUrl &location, int line) { QTestResult::addFailure(message.toUtf8().constData(), qtestFixUrl(location).toLatin1().constData(), line); } bool QuickTestResult::verify (bool success, const QString &message, const QUrl &location, int line) { if (!success && message.isEmpty()) { return QTestResult::verify (success, "verify()", "", qtestFixUrl(location).toLatin1().constData(), line); } else { return QTestResult::verify (success, message.toUtf8().constData(), "", qtestFixUrl(location).toLatin1().constData(), line); } } bool QuickTestResult::fuzzyCompare(const QVariant &actual, const QVariant &expected, qreal delta) { if (actual.userType() == QMetaType::QColor || expected.userType() == QMetaType::QColor) { if (!actual.canConvert(QMetaType(QMetaType::QColor)) || !expected.canConvert(QMetaType(QMetaType::QColor))) { return false; } //fuzzy color comparison QColor act; QColor exp; bool ok(false); QVariant var = QQml_colorProvider()->colorFromString(actual.toString(), &ok); if (!ok) return false; act = var.value(); var = QQml_colorProvider()->colorFromString(expected.toString(), &ok); if (!ok) return false; exp = var.value(); return ( qAbs(act.red() - exp.red()) <= delta && qAbs(act.green() - exp.green()) <= delta && qAbs(act.blue() - exp.blue()) <= delta && qAbs(act.alpha() - exp.alpha()) <= delta); } else { //number comparison bool ok = true; qreal act = actual.toFloat(&ok); if (!ok) return false; qreal exp = expected.toFloat(&ok); if (!ok) return false; return (qAbs(act - exp) <= delta); } return false; } void QuickTestResult::stringify(QQmlV4FunctionPtr args) { if (args->length() < 1) args->setReturnValue(QV4::Encode::null()); QV4::Scope scope(args->v4engine()); QV4::ScopedValue value(scope, (*args)[0]); QString result; //Check for Object Type if (value->isObject() && !value->as() && !value->as()) { QVariant v = QV4::ExecutionEngine::toVariant(value, QMetaType {}); if (v.isValid()) { switch (v.userType()) { case QMetaType::QVector3D: { QVector3D v3d = v.value(); result = QString::fromLatin1("Qt.vector3d(%1, %2, %3)").arg(v3d.x()).arg(v3d.y()).arg(v3d.z()); break; } case QMetaType::QUrl: { QUrl url = v.value(); result = QString::fromLatin1("Qt.url(%1)").arg(url.toString()); break; } case QMetaType::QDateTime: { QDateTime dt = v.value(); result = dt.toString(Qt::ISODateWithMs); break; } default: result = v.toString(); } } else { result = QLatin1String("Object"); } } if (result.isEmpty()) { QString tmp = value->toQStringNoThrow(); if (value->as()) result += QLatin1Char('[') + tmp + QLatin1Char(']'); else result.append(tmp); } args->setReturnValue(QV4::Encode(args->v4engine()->newString(result))); } bool QuickTestResult::compare (bool success, const QString &message, const QVariant &val1, const QVariant &val2, const QUrl &location, int line) { return QTestResult::compare (success, message.toUtf8().constData(), QTest::toString(val1.toString().toLatin1().constData()), QTest::toString(val2.toString().toLatin1().constData()), "", "", qtestFixUrl(location).toLatin1().constData(), line); } void QuickTestResult::skip (const QString &message, const QUrl &location, int line) { QTestResult::addSkip(message.toUtf8().constData(), qtestFixUrl(location).toLatin1().constData(), line); QTestResult::setSkipCurrentTest(true); } bool QuickTestResult::expectFail (const QString &tag, const QString &comment, const QUrl &location, int line) { return QTestResult::expectFail (tag.toLatin1().constData(), QTest::toString(comment.toLatin1().constData()), QTest::Abort, qtestFixUrl(location).toLatin1().constData(), line); } bool QuickTestResult::expectFailContinue (const QString &tag, const QString &comment, const QUrl &location, int line) { return QTestResult::expectFail (tag.toLatin1().constData(), QTest::toString(comment.toUtf8().constData()), QTest::Continue, qtestFixUrl(location).toLatin1().constData(), line); } void QuickTestResult::warn(const QString &message, const QUrl &location, int line) { QTestLog::warn(message.toUtf8().constData(), qtestFixUrl(location).toLatin1().constData(), line); } void QuickTestResult::ignoreWarning(const QJSValue &message) { if (message.isRegExp()) { #if QT_CONFIG(regularexpression) QTestLog::ignoreMessage(QtWarningMsg, qjsvalue_cast(message)); #endif } else { QTestLog::ignoreMessage(QtWarningMsg, message.toString().toUtf8()); } } void QuickTestResult::failOnWarning(const QJSValue &message) { if (message.isRegExp()) { #if QT_CONFIG(regularexpression) QTestLog::failOnWarning(qjsvalue_cast(message)); #endif } else { QTestLog::failOnWarning(message.toString().toUtf8()); } } void QuickTestResult::wait(int ms) { QTest::qWait(ms); } void QuickTestResult::sleep(int ms) { QTest::qSleep(ms); } bool QuickTestResult::waitForRendering(QQuickItem *item, int timeout) { Q_ASSERT(item); return qWaitForSignal(item->window(), SIGNAL(frameSwapped()), timeout); } void QuickTestResult::startMeasurement() { Q_D(QuickTestResult); delete d->benchmarkData; d->benchmarkData = new QBenchmarkTestMethodData(); QBenchmarkTestMethodData::current = d->benchmarkData; d->iterCount = (QBenchmarkGlobalData::current->measurer->needsWarmupIteration()) ? -1 : 0; d->resultsList.clear(); } void QuickTestResult::beginDataRun() { QBenchmarkTestMethodData::current->beginDataRun(); } void QuickTestResult::endDataRun() { Q_D(QuickTestResult); QBenchmarkTestMethodData::current->endDataRun(); const QList &results = QBenchmarkTestMethodData::current->results; if (results.isEmpty()) return; // shouldn't happen if (d->iterCount > -1) // iteration -1 is the warmup iteration. d->resultsList.append(results); if (QBenchmarkGlobalData::current->verboseOutput) { if (d->iterCount == -1) { qDebug() << "warmup stage result :" << results.first().measurement.value; } else { qDebug() << "accumulation stage result:" << results.first().measurement.value; } } } bool QuickTestResult::measurementAccepted() { return QBenchmarkTestMethodData::current->resultsAccepted(); } static QList qMedian(const QList> &container) { const int count = container.size(); if (count == 0) return {}; if (count == 1) return container.at(0); QList> containerCopy = container; std::sort(containerCopy.begin(), containerCopy.end(), [](const QList &a, const QList &b) { return a.first() < b.first(); }); const int middle = count / 2; // ### handle even-sized containers here by doing an aritmetic mean of the two middle items. return containerCopy.at(middle); } bool QuickTestResult::needsMoreMeasurements() { Q_D(QuickTestResult); ++(d->iterCount); if (d->iterCount < QBenchmarkGlobalData::current->adjustMedianIterationCount()) return true; if (QBenchmarkTestMethodData::current->resultsAccepted()) QTestLog::addBenchmarkResults(qMedian(d->resultsList)); return false; } void QuickTestResult::startBenchmark(RunMode runMode, const QString &tag) { QBenchmarkTestMethodData::current->results = {}; QBenchmarkTestMethodData::current->resultAccepted = false; QBenchmarkGlobalData::current->context.tag = tag; QBenchmarkGlobalData::current->context.slotName = functionName(); Q_D(QuickTestResult); delete d->benchmarkIter; d->benchmarkIter = new QTest::QBenchmarkIterationController (QTest::QBenchmarkIterationController::RunMode(runMode)); } bool QuickTestResult::isBenchmarkDone() const { Q_D(const QuickTestResult); if (d->benchmarkIter) return d->benchmarkIter->isDone(); else return true; } void QuickTestResult::nextBenchmark() { Q_D(QuickTestResult); if (d->benchmarkIter) d->benchmarkIter->next(); } void QuickTestResult::stopBenchmark() { Q_D(QuickTestResult); delete d->benchmarkIter; d->benchmarkIter = nullptr; } QObject *QuickTestResult::grabImage(QQuickItem *item) { if (item && item->window()) { QQuickWindow *window = item->window(); QImage grabbed = window->grabWindow(); const auto dpi = grabbed.devicePixelRatio(); QRectF rf(item->x() * dpi, item->y() * dpi, item->width() * dpi, item->height() * dpi); rf = rf.intersected(QRectF(0, 0, grabbed.width(), grabbed.height())); QObject *o = new QuickTestImageObject(grabbed.copy(rf.toAlignedRect())); QQmlEngine::setContextForObject(o, qmlContext(this)); return o; } return nullptr; } QObject *QuickTestResult::findChild(QObject *parent, const QString &objectName) { return parent ? parent->findChild(objectName) : 0; } bool QuickTestResult::isPolishScheduled(QObject *itemOrWindow) const { if (auto item = qobject_cast(itemOrWindow)) return QQuickTest::qIsPolishScheduled(item); if (auto window = qobject_cast(itemOrWindow)) return QQuickTest::qIsPolishScheduled(window); qmlWarning(this) << "isPolishScheduled() expects either an Item or Window, but got" << QDebug::toString(itemOrWindow); return false; } bool QuickTestResult::waitForPolish(QObject *itemOrWindow, int timeout) const { if (auto item = qobject_cast(itemOrWindow)) return QQuickTest::qWaitForPolish(item, timeout); if (auto window = qobject_cast(itemOrWindow)) return QQuickTest::qWaitForPolish(window, timeout); qmlWarning(this) << "waitForItemPolish() expects either an Item or Window, but got" << QDebug::toString(itemOrWindow); return false; } namespace QTest { void qtest_qParseArgs(int argc, char *argv[], bool qml); }; void QuickTestResult::parseArgs(int argc, char *argv[]) { if (!QBenchmarkGlobalData::current) QBenchmarkGlobalData::current = &globalBenchmarkData; QTest::qtest_qParseArgs(argc, argv, true); } void QuickTestResult::setProgramName(const char *name) { if (name) { QTestPrivate::parseBlackList(); QTestResult::reset(); } else if (!name && loggingStarted) { QTestResult::setCurrentTestObject(globalProgramName); QTestLog::stopLogging(); QTestResult::setCurrentTestObject(nullptr); } globalProgramName = name; QTestResult::setCurrentTestObject(globalProgramName); } void QuickTestResult::setCurrentAppname(const char *appname) { QTestResult::setCurrentAppName(appname); } int QuickTestResult::exitCode() { #if defined(QTEST_NOEXITCODE) return 0; #else // make sure our exit code is never going above 127 // since that could wrap and indicate 0 test fails return qMin(QTestLog::failCount(), 127); #endif } QT_END_NAMESPACE #include "quicktestresult.moc" #include "moc_quicktestresult_p.cpp"