| 1 | /**************************************************************************** |
| 2 | ** |
| 3 | ** Copyright (C) 2018 The Qt Company Ltd. |
| 4 | ** Contact: https://www.qt.io/licensing/ |
| 5 | ** |
| 6 | ** This file is part of the test suite of the Qt Toolkit. |
| 7 | ** |
| 8 | ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ |
| 9 | ** Commercial License Usage |
| 10 | ** Licensees holding valid commercial Qt licenses may use this file in |
| 11 | ** accordance with the commercial license agreement provided with the |
| 12 | ** Software or, alternatively, in accordance with the terms contained in |
| 13 | ** a written agreement between you and The Qt Company. For licensing terms |
| 14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
| 15 | ** information use the contact form at https://www.qt.io/contact-us. |
| 16 | ** |
| 17 | ** GNU General Public License Usage |
| 18 | ** Alternatively, this file may be used under the terms of the GNU |
| 19 | ** General Public License version 3 as published by the Free Software |
| 20 | ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT |
| 21 | ** included in the packaging of this file. Please review the following |
| 22 | ** information to ensure the GNU General Public License requirements will |
| 23 | ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
| 24 | ** |
| 25 | ** $QT_END_LICENSE$ |
| 26 | ** |
| 27 | ****************************************************************************/ |
| 28 | |
| 29 | #include "abstracttestsuite.h" |
| 30 | #include <QtTest/QtTest> |
| 31 | #include <QtCore/qset.h> |
| 32 | #include <QtCore/QSysInfo> |
| 33 | #include <QtCore/qtextstream.h> |
| 34 | #include <private/qmetaobjectbuilder_p.h> |
| 35 | |
| 36 | /*! |
| 37 | AbstractTestSuite provides a way of building QtTest test objects |
| 38 | dynamically. The use case is integration of JavaScript test suites |
| 39 | into QtTest autotests. |
| 40 | |
| 41 | Subclasses add their tests functions with addTestFunction() in the |
| 42 | constructor, and must reimplement runTestFunction(). Additionally, |
| 43 | subclasses can reimplement initTestCase() and cleanupTestCase() |
| 44 | (but make sure to call the base implementation). |
| 45 | |
| 46 | AbstractTestSuite uses configuration files for getting information |
| 47 | about skipped tests (skip.txt) and expected test failures |
| 48 | (expect_fail.txt). Subclasses must reimplement |
| 49 | createSkipConfigFile() and createExpectFailConfigFile() for |
| 50 | creating these files, and configData() for processing an entry of |
| 51 | such a file. |
| 52 | |
| 53 | The config file format is as follows: |
| 54 | - Lines starting with '#' are skipped. |
| 55 | - Lines of the form [SYMBOL] means that the upcoming data |
| 56 | should only be processed if the given SYMBOL is defined on |
| 57 | this platform. |
| 58 | - Any other line is split on ' | ' and handed off to the client. |
| 59 | |
| 60 | Subclasses must provide a default tests directory (where the |
| 61 | subclass expects to find the script files to run as tests), and a |
| 62 | default config file directory. Some environment variables can be |
| 63 | used to affect where AbstractTestSuite will look for files: |
| 64 | |
| 65 | - QTSCRIPT_TEST_CONFIG_DIR: Overrides the default test config path. |
| 66 | |
| 67 | - QTSCRIPT_TEST_CONFIG_SUFFIX: Is appended to "skip" and |
| 68 | "expect_fail" to create the test config name. This makes it easy to |
| 69 | maintain skip- and expect_fail-files corresponding to different |
| 70 | revisions of a test suite, and switch between them. |
| 71 | |
| 72 | - QTSCRIPT_TEST_DIR: Overrides the default test dir. |
| 73 | |
| 74 | AbstractTestSuite does _not_ define how the test dir itself is |
| 75 | processed or how tests are run; this is left up to the subclass. |
| 76 | |
| 77 | If no config files are found, AbstractTestSuite will ask the |
| 78 | subclass to create a default skip file. Also, the |
| 79 | shouldGenerateExpectedFailures variable will be set to true. The |
| 80 | subclass should check for this when a test fails, and add an entry |
| 81 | to its set of expected failures. When all tests have been run, |
| 82 | AbstractTestSuite will ask the subclass to create the expect_fail |
| 83 | file based on the tests that failed. The next time the autotest is |
| 84 | run, the created config files will be used. |
| 85 | |
| 86 | The reason for skipping a test is usually that it takes a very long |
| 87 | time to complete (or even hangs completely), or it crashes. It's |
| 88 | not possible for the test runner to know in advance which tests are |
| 89 | problematic, which is why the entries to the skip file are |
| 90 | typically added manually. When running tests for the first time, it |
| 91 | can be useful to run the autotest with the -v1 command line option, |
| 92 | so you can see the name of each test before it's run, and can add a |
| 93 | skip entry if appropriate. |
| 94 | */ |
| 95 | |
| 96 | class TestConfigClientInterface; |
| 97 | // For parsing information about skipped tests and expected failures. |
| 98 | class TestConfigParser |
| 99 | { |
| 100 | public: |
| 101 | static void parse(const QString &path, |
| 102 | TestConfig::Mode mode, |
| 103 | TestConfigClientInterface *client); |
| 104 | |
| 105 | private: |
| 106 | static QString unescape(const QString &); |
| 107 | static bool isKnownSymbol(const QString &); |
| 108 | static bool isDefined(const QString &); |
| 109 | |
| 110 | static QSet<QString> knownSymbols; |
| 111 | static QSet<QString> definedSymbols; |
| 112 | }; |
| 113 | |
| 114 | QSet<QString> TestConfigParser::knownSymbols; |
| 115 | QSet<QString> TestConfigParser::definedSymbols; |
| 116 | |
| 117 | /** |
| 118 | Parses the config file at the given \a path in the given \a mode. |
| 119 | Handling of errors and data is delegated to the given \a client. |
| 120 | */ |
| 121 | void TestConfigParser::parse(const QString &path, |
| 122 | TestConfig::Mode mode, |
| 123 | TestConfigClientInterface *client) |
| 124 | { |
| 125 | QFile file(path); |
| 126 | if (!file.open(flags: QIODevice::ReadOnly)) |
| 127 | return; |
| 128 | QTextStream stream(&file); |
| 129 | int lineNumber = 0; |
| 130 | QString predicate; |
| 131 | const QString separator = QString::fromLatin1(str: " | " ); |
| 132 | while (!stream.atEnd()) { |
| 133 | ++lineNumber; |
| 134 | QString line = stream.readLine(); |
| 135 | if (line.isEmpty()) |
| 136 | continue; |
| 137 | if (line.startsWith(c: '#')) // Comment |
| 138 | continue; |
| 139 | if (line.startsWith(c: '[')) { // Predicate |
| 140 | if (!line.endsWith(c: ']')) { |
| 141 | client->configError(path, message: "malformed predicate" , lineNumber); |
| 142 | return; |
| 143 | } |
| 144 | QString symbol = line.mid(position: 1, n: line.size()-2); |
| 145 | if (isKnownSymbol(symbol)) { |
| 146 | predicate = symbol; |
| 147 | } else { |
| 148 | qWarning(msg: "symbol %s is not known -- add it to TestConfigParser!" , qPrintable(symbol)); |
| 149 | predicate = QString(); |
| 150 | } |
| 151 | } else { |
| 152 | if (predicate.isEmpty() || isDefined(predicate)) { |
| 153 | QStringList parts = line.split(sep: separator, behavior: Qt::KeepEmptyParts); |
| 154 | for (int i = 0; i < parts.size(); ++i) |
| 155 | parts[i] = unescape(parts[i]); |
| 156 | client->configData(mode, parts); |
| 157 | } |
| 158 | } |
| 159 | } |
| 160 | } |
| 161 | |
| 162 | QString TestConfigParser::unescape(const QString &str) |
| 163 | { |
| 164 | return QString(str).replace(before: "\\n" , after: "\n" ); |
| 165 | } |
| 166 | |
| 167 | bool TestConfigParser::isKnownSymbol(const QString &symbol) |
| 168 | { |
| 169 | if (knownSymbols.isEmpty()) { |
| 170 | knownSymbols |
| 171 | // If you add a symbol here, add a case for it in |
| 172 | // isDefined() as well. |
| 173 | << "Q_OS_LINUX" |
| 174 | << "Q_OS_SOLARIS" |
| 175 | << "Q_OS_WINCE" |
| 176 | << "Q_OS_SYMBIAN" |
| 177 | << "Q_OS_MAC" |
| 178 | << "Q_OS_WIN" |
| 179 | << "Q_CC_MSVC" |
| 180 | << "Q_CC_MSVC32" |
| 181 | << "Q_CC_MSVC64" |
| 182 | << "Q_CC_MINGW" |
| 183 | << "Q_CC_MINGW32" |
| 184 | << "Q_CC_MINGW64" |
| 185 | << "Q_CC_INTEL" |
| 186 | << "Q_CC_INTEL32" |
| 187 | << "Q_CC_INTEL64" |
| 188 | ; |
| 189 | } |
| 190 | return knownSymbols.contains(value: symbol); |
| 191 | } |
| 192 | |
| 193 | bool TestConfigParser::isDefined(const QString &symbol) |
| 194 | { |
| 195 | if (definedSymbols.isEmpty()) { |
| 196 | definedSymbols |
| 197 | #ifdef Q_OS_LINUX |
| 198 | << "Q_OS_LINUX" |
| 199 | #endif |
| 200 | #ifdef Q_OS_SOLARIS |
| 201 | << "Q_OS_SOLARIS" |
| 202 | #endif |
| 203 | #ifdef Q_OS_WINCE |
| 204 | << "Q_OS_WINCE" |
| 205 | #endif |
| 206 | #ifdef Q_OS_SYMBIAN |
| 207 | << "Q_OS_SYMBIAN" |
| 208 | #endif |
| 209 | #ifdef Q_OS_MAC |
| 210 | << "Q_OS_MAC" |
| 211 | #endif |
| 212 | #ifdef Q_OS_WIN |
| 213 | << "Q_OS_WIN" |
| 214 | #endif |
| 215 | #ifdef Q_CC_MSVC |
| 216 | << "Q_CC_MSVC" |
| 217 | << (QStringLiteral("Q_CC_MSVC" ) + QString::number(QSysInfo::WordSize)) |
| 218 | #endif |
| 219 | #ifdef Q_CC_MINGW |
| 220 | << "Q_CC_MINGW" |
| 221 | << (QStringLiteral("Q_CC_MINGW" ) + QString::number(QSysInfo::WordSize)) |
| 222 | #endif |
| 223 | #ifdef Q_CC_INTEL |
| 224 | << "Q_CC_INTEL" |
| 225 | << (QStringLiteral("Q_CC_INTEL" ) + QString::number(QSysInfo::WordSize)) |
| 226 | #endif |
| 227 | ; |
| 228 | } |
| 229 | return definedSymbols.contains(value: symbol); |
| 230 | } |
| 231 | |
| 232 | |
| 233 | const QMetaObject *AbstractTestSuite::metaObject() const |
| 234 | { |
| 235 | return dynamicMetaObject; |
| 236 | } |
| 237 | |
| 238 | void *AbstractTestSuite::qt_metacast(const char *_clname) |
| 239 | { |
| 240 | if (!_clname) return 0; |
| 241 | if (!strcmp(s1: _clname, s2: dynamicMetaObject->className())) |
| 242 | return static_cast<void*>(const_cast<AbstractTestSuite*>(this)); |
| 243 | return QObject::qt_metacast(_clname); |
| 244 | } |
| 245 | |
| 246 | void AbstractTestSuite::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a) |
| 247 | { |
| 248 | Q_UNUSED(_a); |
| 249 | if (_c == QMetaObject::InvokeMetaMethod) { |
| 250 | AbstractTestSuite *_t = static_cast<AbstractTestSuite *>(_o); |
| 251 | switch (_id) { |
| 252 | case 0: |
| 253 | _t->initTestCase(); |
| 254 | break; |
| 255 | case 1: |
| 256 | _t->cleanupTestCase(); |
| 257 | break; |
| 258 | default: |
| 259 | // If another method is added above, this offset must be adjusted. |
| 260 | _t->runTestFunction(index: _id - 2); |
| 261 | } |
| 262 | } |
| 263 | } |
| 264 | |
| 265 | int AbstractTestSuite::qt_metacall(QMetaObject::Call _c, int _id, void **_a) |
| 266 | { |
| 267 | _id = QObject::qt_metacall(_c, _id, _a); |
| 268 | if (_id < 0) |
| 269 | return _id; |
| 270 | if (_c == QMetaObject::InvokeMetaMethod) { |
| 271 | Q_ASSERT(dynamicMetaObject->cast(this)); |
| 272 | int ownMethodCount = dynamicMetaObject->methodCount() - dynamicMetaObject->methodOffset(); |
| 273 | if (_id < ownMethodCount) |
| 274 | qt_static_metacall(o: this, _c, _id, _a); |
| 275 | _id -= ownMethodCount; |
| 276 | } |
| 277 | return _id; |
| 278 | } |
| 279 | |
| 280 | void AbstractTestSuite::addPrivateSlot(const QByteArray &signature) |
| 281 | { |
| 282 | QMetaMethodBuilder slot = metaBuilder->addSlot(signature); |
| 283 | slot.setAccess(QMetaMethod::Private); |
| 284 | } |
| 285 | |
| 286 | AbstractTestSuite::AbstractTestSuite(const QByteArray &className, |
| 287 | const QString &defaultTestsPath, |
| 288 | const QString &defaultConfigPath) |
| 289 | : shouldGenerateExpectedFailures(false), |
| 290 | dynamicMetaObject(0), |
| 291 | metaBuilder(new QMetaObjectBuilder) |
| 292 | { |
| 293 | metaBuilder->setSuperClass(&QObject::staticMetaObject); |
| 294 | metaBuilder->setClassName(className); |
| 295 | metaBuilder->setStaticMetacallFunction(qt_static_metacall); |
| 296 | |
| 297 | QString testConfigPath = qgetenv(varName: "QTSCRIPT_TEST_CONFIG_DIR" ); |
| 298 | if (testConfigPath.isEmpty()) |
| 299 | testConfigPath = defaultConfigPath; |
| 300 | QString configSuffix = qgetenv(varName: "QTSCRIPT_TEST_CONFIG_SUFFIX" ); |
| 301 | skipConfigPath = QString::fromLatin1(str: "%0/skip%1.txt" ) |
| 302 | .arg(a: testConfigPath).arg(a: configSuffix); |
| 303 | expectFailConfigPath = QString::fromLatin1(str: "%0/expect_fail%1.txt" ) |
| 304 | .arg(a: testConfigPath).arg(a: configSuffix); |
| 305 | |
| 306 | QString testsPath = qgetenv(varName: "QTSCRIPT_TEST_DIR" ); |
| 307 | if (testsPath.isEmpty()) |
| 308 | testsPath = defaultTestsPath; |
| 309 | testsDir = QDir(testsPath); |
| 310 | |
| 311 | addTestFunction("initTestCase" ); |
| 312 | addTestFunction("cleanupTestCase" ); |
| 313 | |
| 314 | // Subclass constructors should add their custom test functions to |
| 315 | // the meta-object and call finalizeMetaObject(). |
| 316 | } |
| 317 | |
| 318 | AbstractTestSuite::~AbstractTestSuite() |
| 319 | { |
| 320 | free(ptr: dynamicMetaObject); |
| 321 | } |
| 322 | |
| 323 | void AbstractTestSuite::addTestFunction(const QString &name, |
| 324 | DataFunctionCreation dfc) |
| 325 | { |
| 326 | if (dfc == CreateDataFunction) { |
| 327 | QString dataSignature = QString::fromLatin1(str: "%0_data()" ).arg(a: name); |
| 328 | addPrivateSlot(signature: dataSignature.toLatin1()); |
| 329 | } |
| 330 | QString signature = QString::fromLatin1(str: "%0()" ).arg(a: name); |
| 331 | addPrivateSlot(signature: signature.toLatin1()); |
| 332 | } |
| 333 | |
| 334 | void AbstractTestSuite::finalizeMetaObject() |
| 335 | { |
| 336 | dynamicMetaObject = metaBuilder->toMetaObject(); |
| 337 | } |
| 338 | |
| 339 | void AbstractTestSuite::initTestCase() |
| 340 | { |
| 341 | if (!testsDir.exists()) { |
| 342 | QString message = QString::fromLatin1(str: "tests directory (%0) doesn't exist." ) |
| 343 | .arg(a: testsDir.path()); |
| 344 | QFAIL(qPrintable(message)); |
| 345 | return; |
| 346 | } |
| 347 | |
| 348 | if (QFileInfo(skipConfigPath).exists()) |
| 349 | TestConfigParser::parse(path: skipConfigPath, mode: TestConfig::Skip, client: this); |
| 350 | else |
| 351 | createSkipConfigFile(); |
| 352 | |
| 353 | if (QFileInfo(expectFailConfigPath).exists()) |
| 354 | TestConfigParser::parse(path: expectFailConfigPath, mode: TestConfig::ExpectFail, client: this); |
| 355 | else |
| 356 | shouldGenerateExpectedFailures = true; |
| 357 | } |
| 358 | |
| 359 | void AbstractTestSuite::cleanupTestCase() |
| 360 | { |
| 361 | if (shouldGenerateExpectedFailures) |
| 362 | createExpectFailConfigFile(); |
| 363 | } |
| 364 | |
| 365 | void AbstractTestSuite::configError(const QString &path, const QString &message, int lineNumber) |
| 366 | { |
| 367 | QString output; |
| 368 | output.append(s: path); |
| 369 | if (lineNumber != -1) |
| 370 | output.append(s: ":" ).append(s: QString::number(lineNumber)); |
| 371 | output.append(s: ": " ).append(s: message); |
| 372 | QFAIL(qPrintable(output)); |
| 373 | } |
| 374 | |
| 375 | void AbstractTestSuite::createSkipConfigFile() |
| 376 | { |
| 377 | QFile file(skipConfigPath); |
| 378 | if (!file.open(flags: QIODevice::WriteOnly)) |
| 379 | return; |
| 380 | QWARN(qPrintable(QString::fromLatin1("creating %0" ).arg(skipConfigPath))); |
| 381 | QTextStream stream(&file); |
| 382 | |
| 383 | writeSkipConfigFile(stream); |
| 384 | |
| 385 | file.close(); |
| 386 | } |
| 387 | |
| 388 | void AbstractTestSuite::createExpectFailConfigFile() |
| 389 | { |
| 390 | QFile file(expectFailConfigPath); |
| 391 | if (!file.open(flags: QFile::WriteOnly)) |
| 392 | return; |
| 393 | QWARN(qPrintable(QString::fromLatin1("creating %0" ).arg(expectFailConfigPath))); |
| 394 | QTextStream stream(&file); |
| 395 | |
| 396 | writeExpectFailConfigFile(stream); |
| 397 | |
| 398 | file.close(); |
| 399 | } |
| 400 | |
| 401 | /*! |
| 402 | Convenience function for reading all contents of a file. |
| 403 | */ |
| 404 | QString AbstractTestSuite::readFile(const QString &filename) |
| 405 | { |
| 406 | QFile file(filename); |
| 407 | if (!file.open(flags: QFile::ReadOnly)) |
| 408 | return QString(); |
| 409 | QTextStream stream(&file); |
| 410 | stream.setCodec("UTF-8" ); |
| 411 | return stream.readAll(); |
| 412 | } |
| 413 | |
| 414 | /*! |
| 415 | Escapes characters in the string \a str so it's suitable for writing |
| 416 | to a config file. |
| 417 | */ |
| 418 | QString AbstractTestSuite::escape(const QString &str) |
| 419 | { |
| 420 | return QString(str).replace(before: "\n" , after: "\\n" ); |
| 421 | } |
| 422 | |