/**************************************************************************** ** ** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). ** All rights reserved. ** Contact: Nokia Corporation (qt-info@nokia.com) ** ** This file is part of QtUiTest. ** ** $QT_BEGIN_LICENSE:LGPL$ ** No Commercial Usage ** This file contains pre-release code and may not be distributed. ** You may use this file in accordance with the terms and conditions ** contained in the Technology Preview License Agreement accompanying ** this package. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 2.1 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 2.1 requirements ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. ** ** In addition, as a special exception, Nokia gives you certain additional ** rights. These rights are described in the Nokia Qt LGPL Exception ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. ** ** If you have questions regarding the use of this file, please contact ** Nokia at qt-info@nokia.com. ** ** ** ** ** ** ** ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "qscriptsystemtest.h" #include "scriptpreprocessor.h" #include #include #include #include //#include #include #include #include "qtscript_bindings.h" #include "qtuitestengineagent.h" #include QStringList builtins; Q_DECLARE_METATYPE(QVariant) Q_DECLARE_METATYPE(QList) template QScriptValue qScriptValueFromQObject(QScriptEngine *engine, Tp const &qobject) { return engine->newQObject(qobject, QScriptEngine::AutoOwnership); } template void qScriptValueToQObject(const QScriptValue &value, Tp &qobject) { qobject = qobject_cast(value.toQObject()); } template int qScriptRegisterQObjectMetaType( QScriptEngine *engine, const QScriptValue &prototype = QScriptValue(), Tp * /* dummy */ = 0 ) { return qScriptRegisterMetaType(engine, qScriptValueFromQObject, qScriptValueToQObject, prototype); } void setupEnums(QScriptEngine *engine) { QScriptValue qsvObject = engine->newObject(); #define S(val) qsvObject.setProperty(#val, engine->toScriptValue((int)QScriptValue::val)); S(PropertySetter);S(ReadOnly);S(Undeletable);S(SkipInEnumeration); S(PropertyGetter);S(QObjectMember);S(KeepExistingFlags);S(UserRange); #undef S engine->globalObject().setProperty("QScriptValue", qsvObject ); } static QScriptValue dateToString(QScriptContext *ctx, QScriptEngine *eng) { QDateTime dt = ctx->thisObject().toDateTime(); QString fmt = ctx->argument(0).toString(); return eng->toScriptValue(dt.toString(fmt)); } static QString findIncludeScript(const QString& name) { QList includeDirs; foreach (QByteArray const& split, qgetenv("QTUITEST_INCLUDE_PATH").split(':')) { if (split.isEmpty()) continue; QDir dir(split); if (dir.exists()) includeDirs << dir; } foreach (QDir const& dir, includeDirs) { QString file = dir.canonicalPath() + "/" + name; if (QFile::exists(file)) { return file; } } return QString(); } // Include a single script into an engine. static QScriptValue includeScriptFunction (QScriptEngine *engine, const QString& name) { // Find the script, relative to the entry script. QString script; QtScript::getLocation(engine->currentContext(), &script, 0); QString directory = QFileInfo(QFileInfo(script).canonicalFilePath()).dir().canonicalPath(); QDir dir(directory); QFileInfo file(dir, name); QString filename = name; if (file.exists()) filename = file.canonicalFilePath(); // Check if the script has already been loaded. static const char includesProperty[] = "_qtuitest_includes"; QStringList included = qscriptvalue_cast(engine->globalObject().property(includesProperty)); if (included.contains(filename)) { return QScriptValue(); } // Try to load the script into memory. QFile scriptFile(filename); if (!scriptFile.exists() || !scriptFile.open(QIODevice::ReadOnly)) { int pos = directory.indexOf("/interpreter"); if (pos > 0) { directory = directory.left(pos) + "/tests/shared"; QDir dir(directory); QFileInfo file(dir, name); filename = file.filePath(); scriptFile.setFileName(filename); } else { scriptFile.setFileName(findIncludeScript(filename)); } } if (!scriptFile.exists() || (!scriptFile.isOpen() && !scriptFile.open(QIODevice::ReadOnly))) { return engine->currentContext()->throwError("Could not find " + name + " in either the shared or testcase directory"); } QString contents = QTextStream(&scriptFile).readAll(); scriptFile.close(); contents.prepend("with(ParentTestMetaObject) {"); contents.append("\n}"); ScriptPreprocessor().preprocess(contents); // Evaluate the contents of the script. engine->pushContext()->setActivationObject(engine->globalObject()); // Note that we have included this script. if (!engine->globalObject().property(includesProperty).isValid()) { engine->globalObject().setProperty(includesProperty, engine->newArray()); } engine->globalObject().property(includesProperty).setProperty( engine->globalObject().property(includesProperty).property("length").toUInt32(), qScriptValueFromValue(engine, filename)); QScriptValue ret = engine->evaluate(contents, filename); engine->popContext(); return ret; } // Implementation of the "include()" function in scripts, which imports scripts. static QScriptValue includeFunction (QScriptContext *context, QScriptEngine *engine) { QScriptValue value; if (context->argumentCount() == 0) { return context->throwError ("script name must be supplied to include()"); } value = context->argument(0); if (!value.isString()) { return context->throwError ("invalid script name supplied to include()"); } QString name = value.toString(); if (name.contains(QChar('*'))) { //FIXME: This path doesn't look in the tests/shared/ directory. // Expand the wildcard and include all matching scripts. QString script; QtScript::getLocation(context, &script, 0); QDir dir(QFileInfo(script).dir()); QStringList files = dir.entryList(QStringList(name)); foreach (QString filename, files) { value = includeScriptFunction(engine, dir.filePath(filename)); if (engine->hasUncaughtException()) return value; } return engine->undefinedValue(); } else { // Include a single script file. return includeScriptFunction(engine, name); } } static QScriptValue setFlags (QScriptContext *context, QScriptEngine* /*engine*/) { Q_ASSERT(context); if (context->argumentCount() != 3) { return context->throwError ("setFlags() needs three arguments"); } QScriptValue o = context->argument(0); if (!o.isObject()) return QScriptValue(); QString name = context->argument(1).toString(); int flags = context->argument(2).toInt32(); o.setProperty(name, o.property(name), QFlag(flags)); return QScriptValue(); } QScriptSystemTest::QScriptSystemTest() : m_agent(new QtUiTestEngineAgent(&m_engine, this)), m_contextDepth(0), testObject(0), m_checkOnly(false) { // Ensure we process events periodically. m_engine.setProcessEventsInterval(100); } QScriptSystemTest::~QScriptSystemTest() { } QString QScriptSystemTest::testCaseName() const { if (testObject) return testObject->metaObject()->className(); return QAbstractTest::testCaseName(); } void QScriptSystemTest::loadBuiltins(QScriptEngine &engine) { QScriptEngine configEngine; QScriptSystemTest::loadInternalScript("config.js", configEngine); for (int i = 0; i < configEngine.globalObject().property("builtin_files").property("length").toInt32(); ++i) { QString file = configEngine.globalObject().property("builtin_files").property(i).toString(); QtScript::addInternalFile( QScriptSystemTest::loadInternalScript(file, engine, true) ); } } void QScriptSystemTest::importIntoGlobalNamespace(QScriptEngine& engine, QString const& object) { QScriptValue obj = engine.globalObject().property(object); QScriptValueIterator it(obj); while (it.hasNext()) { it.next(); QString name = it.name(); // Transform name of enter(QString,QString) to enter if (it.value().isFunction() && name.contains('(')) { name = name.left(name.indexOf('(')); } // Import this property into the global object iff one doesn't already // exist with this name if (engine.globalObject().property(name).isValid()) continue; // For functions, to keep the QObject slot resolution working right, we // must wrap the property instead of simply copying it. if (it.value().isFunction()) { engine.evaluate(QString("%1 = function(){ return %2.%1.apply(this, arguments); };") .arg(name) .arg(object)); } else { engine.globalObject().setProperty(name, it.value()); } } } QString QScriptSystemTest::loadInternalScript(QString const &name, QScriptEngine &engine, bool withParentObject) { QString filename = QFileInfo(QString::fromAscii(__FILE__)).absolutePath() + "/" + name; if (!QFileInfo(filename).exists()) filename = ":/" + name; QFile f(filename); QString data; if (!f.open(QIODevice::ReadOnly) || (data = QTextStream(&f).readAll()).isEmpty()) { qWarning("QtUiTest: Couldn't load config file '%s' (using '%s')", qPrintable(name), qPrintable(filename)); return QString(); } if (withParentObject) { data.prepend("with(ParentTestMetaObject) {"); data.append("\n}"); } QScriptValue e = engine.evaluate(data, filename); if (e.isError()) { QString backtrace = engine.uncaughtExceptionBacktrace().join("\n"); qWarning("In QtUiTest config file %s:\n%s\n%s", qPrintable(filename), qPrintable(e.toString()), qPrintable(backtrace)); } builtins << filename; return filename; } QScriptValue variantToScriptValue(QScriptEngine *engine, const QVariant &v) { QScriptValue ret; if (v.isNull()) { ret = QScriptValue( engine, QScriptValue::NullValue ); } else if (v.canConvert()) { ret = engine->toScriptValue(v.value()); } else if (v.canConvert()) { ret = engine->toScriptValue(v.value()); } else if (v.canConvert()) { ret = engine->toScriptValue(v.value()); } else if (v.canConvert()) { ret = engine->toScriptValue(v.value()); } else { ret = engine->newVariant(v); } return ret; } void variantFromScriptValue(const QScriptValue &obj, QVariant &v) { v = obj.toVariant(); } QString QScriptSystemTest::currentFile() { QString fileName = QString(); int lineNumber = 0; QtScript::getLocation(m_engine.currentContext(), &fileName, &lineNumber); return fileName; } int QScriptSystemTest::currentLine() { QString fileName = QString(); int lineNumber = 0; QtScript::getLocation(m_engine.currentContext(), &fileName, &lineNumber); return lineNumber; } void QScriptSystemTest::outputBacktrace() { QScriptContext *ctx = m_engine.currentContext(); QString bt("Backtrace:"); while (ctx) { QScriptContextInfo ctxInfo(ctx); QString fn = ctxInfo.fileName(); int ln = ctxInfo.lineNumber(); if (!fn.isEmpty() && ln != -1) bt += "\n " + fn + "(" + QString::number(ln) + ")"; ctx = ctx->parentContext(); } QDebug(QtDebugMsg) << qPrintable(bt); } void QScriptSystemTest::skip(QString const &message, QSystemTest::SkipMode mode) { QSystemTest::skip(message, mode); m_engine.evaluate("throw new QTestFailure('QSKIP');"); } bool QScriptSystemTest::fail(QString const &message) { bool ret = QSystemTest::fail( message ); if (!ret) { outputBacktrace(); m_engine.evaluate("throw new QTestFailure('QFAIL');"); } return ret; } void QScriptSystemTest::verify(bool statement, QString const &message) { if (!QTest::qVerify(statement, "", qPrintable(message), qPrintable(currentFile()), currentLine() )) { outputBacktrace(); m_engine.evaluate("throw new QTestFailure('QFAIL');"); } } void QScriptSystemTest::compare(const QString &actual, const QString &expected) { if (QSystemTest::runAsManualTest()) { QString act; if (actual == "MAGIC_DATA" || actual.contains("'")) act = actual; else act = "'" + actual + "'"; QString exp; if (expected == "MAGIC_DATA" || expected.contains("'")) exp = expected; else exp = "'" + expected + "'"; manualTest( QString("verify that %1 is equal to %2").arg(act).arg(exp)); return; } if (!QTest::qCompare( actual, expected, qPrintable(actual), qPrintable(expected), qPrintable(currentFile()), currentLine() )) { outputBacktrace(); m_engine.evaluate("throw new QTestFailure('QFAIL');"); } } void QScriptSystemTest::compare(const QStringList &actual, const QStringList &expected) { if (QSystemTest::runAsManualTest()) { QString act = actual.count() > 0 ? actual[0] : ""; QString exp = expected.count() > 0 ? expected[0] : ""; manualTest( QString("verify that %1 is equal to %2").arg(act).arg(exp)); return; } if (!QTest::qCompare( actual.count(), expected.count(), qPrintable(actual.join("\n")), qPrintable(expected.join("\n")), qPrintable(currentFile()), currentLine() )) { outputBacktrace(); m_engine.evaluate("throw new QTestFailure('QFAIL');"); return; } for (int i=0; isetProperty("expectFailLineNumber", line); } bool ok = QTest::qExpectFail(currentDataTag().toLatin1(), reason.toLatin1(), QTest::TestFailMode(1),//mode), qPrintable(currentFile()), line); if (!ok) m_engine.evaluate("throw new QTestFailure('QFAIL');"); } bool QScriptSystemTest::setQueryError( const QTestMessage &message ) { QString errorString = message["status"].toString(); QVariant response = message["_q_inResponseTo"]; if (response.isValid()) { errorString += QString("\nIn response to message: %2").arg(response.toString()); } return setQueryError( errorString ); } bool QScriptSystemTest::setQueryError( const QString &errString ) { if (queryFailed()) return false; QSystemTest::setQueryError(errString); bool ret = fail(errString); if (!ret) { m_engine.evaluate("throw new QTestFailure('QFAIL');"); } return ret; } int QScriptSystemTest::runTest(const QString &fname, const QStringList ¶meters, const QStringList &environment) { m_env = environment; filename = fname; QFile file(filename); if (!file.open(QFile::ReadOnly)) { qDebug() << "Can't open " << filename; return -1; } QTextStream stream(&file); QString script = stream.readAll(); ScriptPreprocessor().preprocess(script); setupEnums(&m_engine); // include() imports scripts directly into the parent script. m_engine.globalObject().setProperty ("include", m_engine.newFunction(includeFunction, 1)); m_engine.globalObject().setProperty ("setFlags", m_engine.newFunction(setFlags, 3)); m_engine.globalObject().setProperty("_dateToString", m_engine.newFunction(dateToString)); m_engine.evaluate("_old_date_toString = Date.prototype.toString;" "Date.prototype.toString = function() {" " if (arguments[0] == undefined)" " return _old_date_toString.apply(this, arguments);" " return _dateToString.apply(this, arguments);" "}"); m_engine.globalObject().setProperty("ParentTestObject", m_engine.newQObject(this)); m_engine.globalObject().setProperty("ParentTestMetaObject", m_engine.newQMetaObject(metaObject())); loadBuiltins(m_engine); importIntoGlobalNamespace(m_engine, "ParentTestObject"); // Allow shebangs without giving syntax errors. if (script.startsWith("#!")) script.prepend("//"); script.prepend("with(ParentTestMetaObject){"); script.append("\n}"); QtScriptTest tc(filename, script, &m_engine); if (tc.status() != QtScriptTest::StatusNormal) { return -1; } testObject = &tc; qScriptRegisterMetaType(&m_engine, variantToScriptValue, variantFromScriptValue); qScriptRegisterSequenceMetaType >(&m_engine); // Only set up the test data path if not explicitly set by user if (!QCoreApplication::arguments().contains("-data")) { setupTestDataPath(qPrintable(filename)); } enableQueryWarnings(false); // If we get here, the syntax of the script is definitely OK // (a syntax error causes a qFatal in the QtScriptTest ctor). if (m_checkOnly) return 0; // If an IDE is connected, set the agent to enable script debugging if (QTestIDE::instance()->isConnected()) { m_engine.setAgent(m_agent); } int retval = QTest::qExec(&tc, parameters); testObject = 0; // After a full test run, QTestLib sometimes returns 0 or sometimes returns // the number of test failures, depending on how it was compiled. In both // cases, a negative number denotes an error. // We don't want test failures to affect the exit code. return (retval < 0) ? retval : 0; } /*! \internal Send a raw message from within script to the connected system. This can be used to extend the API for new messages on the target device without modifying QSystemTest. If the message doesn't elicit a response of "OK", the current test fails. */ QVariantMap QScriptSystemTest::sendRaw(const QString& event, const QScriptValue& object) { QVariantMap ret; if (object.isValid() && !object.isObject()) { setQueryError("Second argument to sendRaw must be an object."); return ret; } if (event.isEmpty()) { setQueryError("First argument to sendRaw cannot be an empty string."); return ret; } QTestMessage message(event); // Treat object like a variant map and load it into message. QScriptValueIterator it(object); while (it.hasNext()) { it.next(); QScriptValue v = it.value(); // Map must be flat; we don't handle objects within objects. if (v.isObject() && !v.isArray()) { setQueryError("Object passed to sendRaw must not have any child objects."); return ret; } // toVariant appears to flatten stringlists into strings, which we don't want. if (v.isArray()) { QVariantList list; for (int i = 0, max = qscriptvalue_cast(v.property("length")); i < max; ++i) list << v.property(i).toVariant(); message[it.name()] = list; } else { message[it.name()] = v.toVariant(); } } QTestMessage reply; if (!doQuery(message, QString(), &reply)) { setQueryError("Raw " + event + " message failed: " + reply["status"].toString()); return ret; } foreach (QString const& key, reply.keys()) ret[key] = reply[key]; return ret; } //#ifndef QTCREATOR_QTEST /*! \internal Print any special usage information which should be shown when test is launched with -help. */ void QScriptSystemTest::printUsage() const { QSystemTest::printUsage(); qWarning( " Script options:\n" " -c : Check the syntax of the test only. Returns a non-zero exit code if the test\n" " contains any syntax errors.\n" ); } //#endif /*! \internal If \a func is a function, install it as a handler for all messages received from the test system. Whenever a new message is received, \a func will be called with two arguments. The first is the message event as a string. The second is an object containing one property for each parameter of the message. If \a func returns true, processing of the message ends. */ void QScriptSystemTest::installMessageHandler(const QScriptValue& func) { if (!func.isFunction()) { setQueryError("Argument to installMessageHandler must be a function."); return; } m_messageHandlers << func; } QString qDumpScriptValue(QString const& name, QScriptValue const& v, int indent = 0) { const QString spc = QString().fill(' ', indent); QString ret; ret += spc + name + ": "; if (name != "global" && v.engine()->globalObject().strictlyEquals(v)) ret += "global"; else if (v.isBoolean()) ret += "Boolean:" + v.toString(); else if (v.isDate()) ret += "Date:" + v.toString(); else if (v.isFunction()) ret += "Function"; else if (v.isNull()) ret += "null"; else if (v.isNumber()) ret += "Number:" + v.toString(); else if (v.isString()) ret += "String:" + v.toString(); else if (v.isUndefined()) ret += "undef"; else { QString inner; QScriptValueIterator it(v); QString sep; while (it.hasNext()) { it.next(); inner += sep + qDumpScriptValue(it.name(), it.value(), indent+2); sep = ",\n"; } if (inner.isEmpty()) { ret += "{}"; } else { ret += "{\n" + inner + "\n" + spc + "} /* " + name + " */"; } } return ret; } /*! \internal Write out most of the state of the script engine to stderr. */ void QScriptSystemTest::dumpEngine() { QString state; { QScriptContext* ctx = m_engine.currentContext(); state += "context: {"; int i = 0; QString sep; while (ctx) { state += QString("%1\n %2: {\n").arg(sep).arg(i++); state += " toString: " + ctx->toString() + "\n"; state += qDumpScriptValue("activationObject", ctx->activationObject(), 4) + ",\n"; state += qDumpScriptValue("thisObject", ctx->thisObject(), 4) + "\n"; state += " }"; sep = ","; ctx = ctx->parentContext(); } state += "\n};\n"; } state += qDumpScriptValue("global", m_engine.globalObject()); state += ";"; fprintf(stderr, "%s\n", qPrintable(state)); } /*! \internal Passes the test message through any installed QtScript message handlers. If none of them handle the message, it will be passed to the superclass. */ void QScriptSystemTest::processMessage(const QTestMessage& message) { if (m_messageHandlers.count()) { QVariantMap map; foreach (QString const& key, message.keys()) map[key] = message[key]; QScriptValueList args; args << m_engine.toScriptValue(message.event()); args << m_engine.toScriptValue(map); for (int i = 0; i < m_messageHandlers.count(); ++i) { QScriptValue out = m_messageHandlers[i].call(QScriptValue(), args); if (out.isBoolean() && out.toBoolean()) return; } } QSystemTest::processMessage(message); } /*! \internal Processes the command line parameters. */ void QScriptSystemTest::processCommandLine( QStringList &args ) { QMutableStringListIterator it(args); while (it.hasNext()) { QString arg = it.next(); if (!arg.compare("-c", Qt::CaseInsensitive)) { m_checkOnly = true; it.remove(); } } QSystemTest::processCommandLine(args); } void QScriptSystemTest::scriptPositionChange(qint64 scriptId, int line, int column) { Q_UNUSED(scriptId); Q_UNUSED(line); Q_UNUSED(column); QScriptContextInfo ctxInfo(m_engine.currentContext()); if (!ctxInfo.fileName().isEmpty() && !builtins.contains(ctxInfo.fileName())) { QString functionName; if (ctxInfo.functionName().isEmpty()) { functionName = currentTestFunction(); } else { functionName = ctxInfo.functionName(); } if (QTestIDE::instance()->queryBreakpoint(ctxInfo.fileName(), ctxInfo.lineNumber(), functionName, m_contextDepth)) { QTestIDE::instance()->breakpointContext(m_engine.currentContext()); } } } void QScriptSystemTest::scriptContextChange(bool isNew) { if (isNew) ++m_contextDepth; else --m_contextDepth; }