File indexing completed on 2024-05-05 16:15:13

0001 /*
0002  *  This file is part of the KDE libraries
0003  *
0004  *  Copyright (C) 2012 Rolf Eike Beer <kde@opensource.sf-tec.de>
0005  *
0006  *  This library is free software; you can redistribute it and/or
0007  *  modify it under the terms of the GNU Lesser General Public
0008  *  License as published by the Free Software Foundation; either
0009  *  version 2 of the License, or (at your option) any later version.
0010  */
0011 
0012 #include "ecmatest.h"
0013 #include <QTest>
0014 
0015 #include <wtf/HashTraits.h>
0016 #include "JSLock.h"
0017 #include "object.h"
0018 #include "JSVariableObject.h"
0019 #include "Parser.h"
0020 
0021 #include <QMap>
0022 #include <QDebug>
0023 #include <QRegularExpression>
0024 #include <QDirIterator>
0025 
0026 // Let the interpreter create its own global Object instead of using our selfcreated
0027 #define USE_KJS_GLOBAL 1
0028 
0029 // from khtml/ecma/kjs_binding.cpp"
0030 KJS::UString::UString(const QString &d)
0031 {
0032     unsigned int len = d.length();
0033     KJS::UChar *dat = static_cast<KJS::UChar *>(fastMalloc(sizeof(KJS::UChar) * len));
0034     memcpy(dat, reinterpret_cast<const void*>(d.unicode()), len * sizeof(KJS::UChar));
0035     m_rep = KJS::UString::Rep::create(dat, len);
0036 }
0037 
0038 QString KJS::UString::qstring() const
0039 {
0040     return QString((QChar *) data(), size());
0041 }
0042 
0043 // from khtml/ecma/debugger/value2string.cpp
0044 QString valueToString(KJS::JSValue *value)
0045 {
0046     switch (KJS::JSValue::type(value)) {
0047     case KJS::NumberType: {
0048         double v = 0.0;
0049         KJS::JSValue::getNumber(value, v);
0050         return QString::number(v);
0051     }
0052     case KJS::BooleanType:
0053         return KJS::JSValue::getBoolean(value) ? "true" : "false";
0054     case KJS::StringType: {
0055         KJS::UString s;
0056         KJS::JSValue::getString(value, s);
0057         return '"' + s.qstring() + '"';
0058     }
0059     case KJS::UndefinedType:
0060         return "undefined";
0061     case KJS::NullType:
0062         return "null";
0063     case KJS::ObjectType:
0064         return "[object " + static_cast<KJS::JSObject *>(value)->className().qstring() + "]";
0065     case KJS::GetterSetterType:
0066     case KJS::UnspecifiedType:
0067     default:
0068         return QString();
0069     }
0070 }
0071 
0072 // from khtml/ecma/debugger/debugwindow.cpp
0073 static QString exceptionToString(KJS::ExecState *exec, KJS::JSValue *exceptionObj)
0074 {
0075     QString exceptionMsg = valueToString(exceptionObj);
0076 
0077     // Since we purposefully bypass toString, we need to figure out
0078     // string serialization ourselves.
0079     //### might be easier to export class info for ErrorInstance ---
0080 
0081     KJS::JSObject *valueObj = KJS::JSValue::getObject(exceptionObj);
0082     KJS::JSValue  *protoObj = valueObj ? valueObj->prototype() : nullptr;
0083 
0084     bool exception   = false;
0085     bool syntaxError = false;
0086     if (protoObj == exec->lexicalInterpreter()->builtinSyntaxErrorPrototype()) {
0087         exception   = true;
0088         syntaxError = true;
0089     }
0090 
0091     if (protoObj == exec->lexicalInterpreter()->builtinErrorPrototype()          ||
0092             protoObj == exec->lexicalInterpreter()->builtinEvalErrorPrototype()      ||
0093             protoObj == exec->lexicalInterpreter()->builtinReferenceErrorPrototype() ||
0094             protoObj == exec->lexicalInterpreter()->builtinRangeErrorPrototype()     ||
0095             protoObj == exec->lexicalInterpreter()->builtinTypeErrorPrototype()      ||
0096             protoObj == exec->lexicalInterpreter()->builtinURIErrorPrototype()) {
0097         exception = true;
0098     }
0099 
0100     if (!exception) {
0101         return exceptionMsg;
0102     }
0103 
0104     // Clear exceptions temporarily so we can get/call a few things.
0105     // We memorize the old exception first, of course. Note that
0106     // This is not always the same as exceptionObj since we may be
0107     //  asked to translate a non-active exception
0108     KJS::JSValue *oldExceptionObj = exec->exception();
0109     exec->clearException();
0110 
0111     // We want to serialize the syntax errors ourselves, to provide the line number.
0112     // The URL is in "sourceURL" and the line is in "line"
0113     // ### TODO: Perhaps we want to use 'sourceId' in case of eval contexts.
0114     if (syntaxError) {
0115         KJS::JSValue *lineValue = valueObj->get(exec, "line");
0116         KJS::JSValue *urlValue  = valueObj->get(exec, "sourceURL");
0117 
0118         int      line = KJS::JSValue::toNumber(lineValue, exec);
0119         QString  url  = KJS::JSValue::toString(urlValue, exec).qstring();
0120         exceptionMsg = QString::fromLatin1("Parse error at %1 line %2").arg(url).arg(line + 1);
0121     } else {
0122         // ### it's still not 100% safe to call toString here, even on
0123         // native exception objects, since someone might have changed the toString property
0124         // of the exception prototype, but I'll punt on this case for now.
0125         exceptionMsg = KJS::JSValue::toString(exceptionObj, exec).qstring();
0126     }
0127     exec->setException(oldExceptionObj);
0128     return exceptionMsg;
0129 }
0130 
0131 #ifndef USE_KJS_GLOBAL
0132 class GlobalImp : public KJS::JSGlobalObject
0133 {
0134 public:
0135     virtual KJS::UString className() const
0136     {
0137         return "global";
0138     }
0139 };
0140 
0141 static GlobalImp *global;
0142 #endif
0143 static QString basedir("");
0144 static QByteArray testrunner;
0145 static QMap<QByteArray, QByteArray> includes;
0146 static QStringList expectedBroken;  // list of tests we know that will fail
0147 
0148 /**
0149  * load the given file from the harness directory
0150  * @param fn filename
0151  * @return if operation succeeded
0152  *
0153  * Will load the given file into the "includes" map
0154  */
0155 static bool loadInclude(const QByteArray &fn)
0156 {
0157     QFile runnerfile(basedir + QLatin1String("test/harness/") + QString::fromLatin1(fn.constData()));
0158 
0159     if (!runnerfile.open(QIODevice::ReadOnly)) {
0160         return false;
0161     }
0162 
0163     includes[ fn ] = runnerfile.readAll();
0164 
0165     return true;
0166 }
0167 
0168 QTEST_MAIN(ECMAscriptTest)
0169 
0170 void ECMAscriptTest::initTestCase()
0171 {
0172     basedir = QString::fromUtf8(qgetenv("ECMATEST_BASEDIR").constData());
0173 
0174     if (basedir.isEmpty()) {
0175         qFatal("ECMATEST_BASEDIR not set");
0176     }
0177 
0178     if (!basedir.endsWith(QLatin1Char('/'))) {
0179         basedir += QLatin1Char('/');
0180     }
0181 
0182     QVERIFY(loadInclude("sta.js"));
0183     QVERIFY(loadInclude("ed.js"));
0184 
0185     testrunner = includes[ "sta.js" ] + includes[ "ed.js" ] + '\n';
0186 
0187     const QString brokenFn = QString::fromLatin1(qgetenv("ECMATEST_BROKEN").constData());
0188     if (!brokenFn.isEmpty()) {
0189         QFile brokenF(brokenFn);
0190         if (!brokenF.open(QIODevice::ReadOnly)) {
0191             const QByteArray errmsg = QByteArray("cannot open ") + QFile::encodeName(brokenFn);
0192             QWARN(errmsg.constData());
0193         } else {
0194             expectedBroken = QString::fromLatin1(brokenF.readAll().constData()).split(QLatin1Char('\n'))
0195                              .filter(QRegularExpression(QStringLiteral("^[^#].*")));
0196         }
0197     }
0198 
0199     m_passed = 0;
0200     m_failed = 0;
0201 }
0202 
0203 static QByteArray getTextProperty(const QByteArray &property, const QByteArray &code)
0204 {
0205     int from = code.indexOf(property);
0206     if (from == -1) {
0207         return QByteArray();
0208     }
0209 
0210     from += property.length();
0211     while (code[ from ] == ' ') {
0212         from++;
0213     }
0214 
0215     int to = code.indexOf('\n', from);
0216     if (code[to - 1] == '\r') {
0217         to--;
0218     }
0219     // poor mans escaping
0220     return code.mid(from, to - from).replace("\\", "\\\\").replace("\"", "\\\"");
0221 }
0222 
0223 #define ECMATEST_VERIFY( expr ) \
0224     do { \
0225         const bool tmp_result = ( expr ); \
0226         if ( tmp_result ) \
0227             m_passed++; \
0228         else \
0229             m_failed++; \
0230         if ( knownBroken ) \
0231             QEXPECT_FAIL(QTest::currentDataTag(), "It is known that KJS doesn't pass this test", Abort); \
0232         QVERIFY( tmp_result ); \
0233     } while (0)
0234 
0235 static QMap< QByteArray, QByteArray > skips;
0236 
0237 void ECMAscriptTest::runAllTests()
0238 {
0239     static const QByteArray include = "$INCLUDE(\"";
0240 
0241     QFETCH(QString, filename);
0242     QByteArray expectedError;
0243 
0244     QFile input(filename);
0245 
0246     Q_FOREACH (const QByteArray &skip, skips.keys()) {
0247         if (skip == QTest::currentDataTag()) {
0248             QSKIP(skips[ skip ].constData());
0249         }
0250     }
0251 
0252     QVERIFY(input.open(QIODevice::ReadOnly));
0253 
0254     const QByteArray testdata = input.readAll();
0255 
0256     QVERIFY(! testdata.isEmpty());
0257 
0258 #ifdef USE_KJS_GLOBAL
0259     RefPtr<KJS::Interpreter> interp = new KJS::Interpreter();
0260 #else
0261     RefPtr<KJS::Interpreter> interp = new KJS::Interpreter(global);
0262 #endif
0263 
0264     KJS::Interpreter::setShouldPrintExceptions(true);
0265 
0266     QByteArray testscript;
0267 
0268     // test is expected to fail
0269     if (testdata.indexOf("@negative") >= 0) {
0270         expectedError = getTextProperty("@negative", testdata);
0271         if (expectedError.isEmpty()) {
0272             expectedError = ".";
0273         }
0274     }
0275 
0276     int from = 0;
0277     while ((from = testdata.indexOf(include, from)) >= 0) {
0278         int endq = testdata.indexOf("\"", from + include.length());
0279         QVERIFY(endq >= 0);
0280 
0281         const QByteArray includeFile = testdata.mid(from + include.length(), endq - from - include.length());
0282 
0283         if (! includes.contains(includeFile)) {
0284             QVERIFY(loadInclude(includeFile));
0285         }
0286 
0287         testscript += includes[ includeFile ];
0288         from = endq;
0289     }
0290 
0291     testscript += testrunner;
0292 
0293     testscript += testdata;
0294 
0295     const QFileInfo info(input);
0296 
0297     const QString scriptutf = QString::fromUtf8(testscript.constData());
0298 
0299 //     QWARN(filename.toAscii().data());
0300     KJS::Completion completion = interp->evaluate(info.fileName().toLatin1().constData(), 0, scriptutf);
0301 
0302     const bool knownBroken = expectedBroken.contains(QString::fromLatin1(QTest::currentDataTag()));
0303 
0304     if (expectedError.isEmpty()) {
0305         ECMATEST_VERIFY(completion.complType() != KJS::Throw);
0306     } else {
0307         if (knownBroken && completion.complType() != KJS::Throw) {
0308             QEXPECT_FAIL(QTest::currentDataTag(), "It is known that KJS doesn't pass this test", Abort);
0309             m_failed++;
0310         }
0311 
0312         QCOMPARE(completion.complType(), KJS::Throw);
0313         QVERIFY(completion.value() != nullptr);
0314 
0315         const QString eMsg = exceptionToString(interp->execState(), completion.value());
0316 
0317         if (expectedError == "^((?!NotEarlyError).)*$") {
0318             ECMATEST_VERIFY(eMsg.indexOf("NotEarlyError") == -1);
0319         } else if (expectedError == ".") {
0320             // means "every exception passes
0321         } else {
0322             ECMATEST_VERIFY(eMsg.indexOf(expectedError) >= 0);
0323         }
0324     }
0325 }
0326 
0327 void ECMAscriptTest::runAllTests_data()
0328 {
0329 #ifndef USE_KJS_GLOBAL
0330     global = new GlobalImp();
0331 #endif
0332 
0333     QTest::addColumn<QString>("filename");
0334 
0335     const QStringList js(QLatin1String("*.js"));
0336     const QStringList all(QLatin1String("*"));
0337     const QString chapter = QString::fromLatin1(qgetenv("ECMATEST_CHAPTER").constData());
0338 
0339     if (!chapter.isEmpty()) {
0340         QWARN(QByteArray("===> Testing chapter " + chapter.toLatin1()).constData());
0341     }
0342 
0343     if (chapter.isEmpty() || chapter.startsWith("ch15")) {
0344         const QByteArray timeZoneDepend = "this test depends on the timezone and may or may not fail, avoid it for the moment";
0345         // The tests are timezone dependent because of the Date implementation in kjs.
0346         // It only affects the limit by +/- 24h (or less, depending on your timezone),
0347         // the "normal" use is not affected.
0348         // It requires a complete Date rewrite to fix this, see ECMA Edition 5.1r6 15.9.1.1
0349         skips[ "15.9.5.43-0-8" ] = timeZoneDepend;
0350         skips[ "15.9.5.43-0-9" ] = timeZoneDepend;
0351         skips[ "15.9.5.43-0-10" ] = timeZoneDepend;
0352         skips[ "15.9.5.43-0-11" ] = timeZoneDepend;
0353         skips[ "15.9.5.43-0-12" ] = timeZoneDepend;
0354     }
0355 
0356 #ifndef USE_KJS_GLOBAL
0357     // some tests fail when the suite is run as a whole
0358     if (chapter.isEmpty() || chapter.startsWith("ch15") || chapter.startsWith("ch12")) {
0359         const QByteArray endlessLoop = "this test causes an endless loop, avoid it for the moment";
0360         const QByteArray crashTest = "this test causes a crash when run as part of the whole suite";
0361         skips[ "S12.7_A9_T1" ] = endlessLoop;
0362         skips[ "S12.7_A9_T2" ] = endlessLoop;
0363         skips[ "S15.1.2.3_A6" ] = endlessLoop;
0364         skips[ "S15.1.3.1_A2.5_T1" ] = endlessLoop;
0365         skips[ "S15.1.3.2_A2.4_T1" ] = endlessLoop;
0366         skips[ "S15.1.3.2_A2.5_T1" ] = endlessLoop;
0367         skips[ "15.2.3.4-4-1" ] = crashTest;
0368         skips[ "15.2.3.11-4-1" ] = crashTest;
0369         skips[ "15.2.3.12-3-1" ] = crashTest;
0370     }
0371 #endif
0372 
0373     QDirIterator it(basedir + QLatin1String("test/suite/") + chapter, QDirIterator::Subdirectories);
0374     while (it.hasNext()) {
0375         it.next();
0376 
0377         const QFileInfo info = it.fileInfo();
0378 
0379         if (!info.isFile()) {
0380             continue;
0381         }
0382 
0383         QString filename = info.fileName();
0384 
0385         filename.chop(3); // .js
0386 
0387         QTest::newRow(filename.toLatin1().constData()) << info.absoluteFilePath();
0388     }
0389 }
0390 
0391 void ECMAscriptTest::cleanup()
0392 {
0393 #ifndef USE_KJS_GLOBAL
0394     global->clearProperties();
0395 #endif
0396 }
0397 
0398 void ECMAscriptTest::cleanupTestCase()
0399 {
0400     qDebug() << "passed testcases:" << m_passed << "failed testcases:" << m_failed;
0401 }
0402 
0403 #include "moc_ecmatest.cpp"