/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- * vim: set ts=8 sts=2 et sw=2 tw=80: * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "jsapi-tests/tests.h" #include "mozilla/ScopeExit.h" #include "mozilla/Sprintf.h" #include "mozilla/Utf8.h" // mozilla::Utf8Unit #include #include #include #include #include #include "js/CharacterEncoding.h" #include "js/CompilationAndEvaluation.h" // JS::Evaluate #include "js/Conversions.h" #include "js/Equality.h" // JS::SameValue #include "js/GlobalObject.h" // JS::DefaultGlobalClassOps, JS_NewGlobalObject #include "js/Initialization.h" #include "js/Prefs.h" #include "js/PropertyAndElement.h" // JS_DefineFunction #include "js/RootingAPI.h" #include "js/SourceText.h" // JS::Source{Ownership,Text} #include "js/Warnings.h" // JS::SetWarningReporter using namespace jsapitest; // A singly linked list of tests which doesn't require runtime intialization. // // We rely on the runtime initialization of global variables containing test // instances to add themselves to these lists, which depends on them already // being in a valid state. template class TestList { T* first = nullptr; T* last = nullptr; public: T* getFirst() const { return first; } void pushBack(T* element) { MOZ_ASSERT(!element->next); MOZ_ASSERT(bool(first) == bool(last)); if (!first) { first = element; last = element; return; } last->next = element; last = element; } }; static TestList testList; TestBase::TestBase(TestKind kind) : kind(kind) { testList.pushBack(this); } [[noreturn]] static void Die(const char* format, ...) { fprintf(stderr, "TEST-UNEXPECTED-FAIL | jsapi-tests | "); va_list ap; va_start(ap, format); vfprintf(stderr, format, ap); va_end(ap); fprintf(stderr, "\n"); exit(1); } bool TestBase::fail(const std::string& msg, const char* filename, int lineno) { char location[256]; SprintfLiteral(location, "%s:%d:", filename, lineno); std::string message(location); message += msg; maybeAppendException(message); fprintf(stderr, "%s\n", message.c_str()); if (msgs.length() != 0) { msgs += " | "; } msgs += message; return false; } /* static */ RuntimeTest* RuntimeTest::From(TestBase* test) { MOZ_ASSERT(test->isRuntimeTest()); return static_cast(test); } RuntimeTest::RuntimeTest() : TestBase(TestKind::Runtime) {} RuntimeTest::~RuntimeTest() { MOZ_RELEASE_ASSERT(!cx); MOZ_RELEASE_ASSERT(!global); } bool RuntimeTest::initContext(JSContext* maybeReusableContext) { if (maybeReusableContext && reuseGlobal) { cx = maybeReusableContext; global.init(cx, JS::CurrentGlobalOrNull(cx)); return true; } MaybeFreeContext(maybeReusableContext); cx = createContext(); if (!cx) { return false; } js::UseInternalJobQueues(cx); if (!JS::InitSelfHostedCode(cx)) { return false; } global.init(cx); createGlobal(); if (!global) { return false; } JS::EnterRealm(cx, global); return true; } JSContext* RuntimeTest::maybeForgetContext() { if (!reuseGlobal) { return nullptr; } JSContext* reusableCx = cx; global.reset(); cx = nullptr; return reusableCx; } /* static */ void RuntimeTest::MaybeFreeContext(JSContext* maybeCx) { if (maybeCx) { JS::LeaveRealm(maybeCx, nullptr); JS_DestroyContext(maybeCx); } } void RuntimeTest::uninit() { global.reset(); MaybeFreeContext(cx); cx = nullptr; msgs.clear(); } bool RuntimeTest::exec(const char* utf8, const char* filename, int lineno) { JS::CompileOptions opts(cx); opts.setFileAndLine(filename, lineno); JS::SourceText srcBuf; JS::RootedValue v(cx); return (srcBuf.init(cx, utf8, strlen(utf8), JS::SourceOwnership::Borrowed) && JS::Evaluate(cx, opts, srcBuf, &v)) || fail(utf8, filename, lineno); } bool RuntimeTest::execDontReport(const char* utf8, const char* filename, int lineno) { JS::CompileOptions opts(cx); opts.setFileAndLine(filename, lineno); JS::SourceText srcBuf; JS::RootedValue v(cx); return srcBuf.init(cx, utf8, strlen(utf8), JS::SourceOwnership::Borrowed) && JS::Evaluate(cx, opts, srcBuf, &v); } bool RuntimeTest::evaluate(const char* utf8, const char* filename, int lineno, JS::MutableHandleValue vp) { JS::CompileOptions opts(cx); opts.setFileAndLine(filename, lineno); JS::SourceText srcBuf; return (srcBuf.init(cx, utf8, strlen(utf8), JS::SourceOwnership::Borrowed) && JS::Evaluate(cx, opts, srcBuf, vp)) || fail(utf8, filename, lineno); } std::string RuntimeTest::jsvalToSource(JS::HandleValue v) { JS::Rooted str(cx, JS_ValueToSource(cx, v)); if (str) { if (JS::UniqueChars bytes = JS_EncodeStringToUTF8(cx, str)) { return bytes.get(); } } JS_ClearPendingException(cx); return "<>"; } std::string RuntimeTest::toSource(char c) { char buf[2] = {c, '\0'}; return buf; } std::string RuntimeTest::toSource(long v) { char buf[40]; SprintfLiteral(buf, "%ld", v); return buf; } std::string RuntimeTest::toSource(unsigned long v) { char buf[40]; SprintfLiteral(buf, "%lu", v); return buf; } std::string RuntimeTest::toSource(long long v) { char buf[40]; SprintfLiteral(buf, "%lld", v); return buf; } std::string RuntimeTest::toSource(unsigned long long v) { char buf[40]; SprintfLiteral(buf, "%llu", v); return buf; } std::string RuntimeTest::toSource(double d) { char buf[40]; SprintfLiteral(buf, "%17lg", d); return buf; } std::string RuntimeTest::toSource(unsigned int v) { return toSource((unsigned long)v); } std::string RuntimeTest::toSource(int v) { return toSource((long)v); } std::string RuntimeTest::toSource(bool v) { return v ? "true" : "false"; } std::string RuntimeTest::toSource(JS::RegExpFlags flags) { std::string str; if (flags.hasIndices()) { str += "d"; } if (flags.global()) { str += "g"; } if (flags.ignoreCase()) { str += "i"; } if (flags.multiline()) { str += "m"; } if (flags.dotAll()) { str += "s"; } if (flags.unicode()) { str += "u"; } if (flags.unicodeSets()) { str += "v"; } if (flags.sticky()) { str += "y"; } return str; } std::string RuntimeTest::toSource(JSAtom* v) { JS::RootedValue val(cx, JS::StringValue((JSString*)v)); return jsvalToSource(val); } bool RuntimeTest::checkSame(const JS::Value& actualArg, const JS::Value& expectedArg, const char* actualExpr, const char* expectedExpr, const char* filename, int lineno) { bool same = false; JS::RootedValue actual(cx, actualArg); JS::RootedValue expected(cx, expectedArg); if (JS::SameValue(cx, actual, expected, &same) && same) { return true; } return false; } void RuntimeTest::maybeAppendException(std::string& message) { if (JS_IsExceptionPending(cx)) { message += " -- "; js::gc::AutoSuppressGC gcoff(cx); JS::RootedValue v(cx); JS_GetPendingException(cx, &v); JS_ClearPendingException(cx); JS::Rooted s(cx, JS::ToString(cx, v)); if (s) { if (JS::UniqueChars bytes = JS_EncodeStringToLatin1(cx, s)) { message += bytes.get(); } } } } /* static */ const JSClass* RuntimeTest::basicGlobalClass() { static const JSClass c = { "global", JSCLASS_GLOBAL_FLAGS, &JS::DefaultGlobalClassOps, }; return &c; } /* static */ void RuntimeTest::reportWarning(JSContext* cx, JSErrorReport* report) { MOZ_RELEASE_ASSERT(report->isWarning()); fprintf(stderr, "%s:%u:%s\n", report->filename ? report->filename.c_str() : "", (unsigned int)report->lineno, report->message().c_str()); } /* static */ bool RuntimeTest::print(JSContext* cx, unsigned argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); JS::Rooted str(cx); for (unsigned i = 0; i < args.length(); i++) { str = JS::ToString(cx, args[i]); if (!str) { return false; } JS::UniqueChars bytes = JS_EncodeStringToUTF8(cx, str); if (!bytes) { return false; } printf("%s%s", i ? " " : "", bytes.get()); } putchar('\n'); fflush(stdout); args.rval().setUndefined(); return true; } bool RuntimeTest::definePrint() { return JS_DefineFunction(cx, global, "print", (JSNative)print, 0, 0); } JSContext* RuntimeTest::createContext() { JSContext* cx = JS_NewContext(8L * 1024 * 1024); if (!cx) { return nullptr; } JS::SetWarningReporter(cx, &reportWarning); return cx; } JSObject* RuntimeTest::createGlobal(JSPrincipals* principals) { /* Create the global object. */ JS::RootedObject newGlobal(cx); JS::RealmOptions options; options.creationOptions().setSharedMemoryAndAtomicsEnabled(true); newGlobal = JS_NewGlobalObject(cx, getGlobalClass(), principals, JS::FireOnNewGlobalHook, options); if (!newGlobal) { return nullptr; } global = newGlobal; return newGlobal; } FrontendTest::FrontendTest() : TestBase(TestKind::Frontend) {} TempFile::TempFile() : name(), stream() {} TempFile::~TempFile() { if (stream) { close(); } if (name) { remove(); } } FILE* TempFile::open(const char* fileName) { stream = fopen(fileName, "wb+"); if (!stream) { Die("error opening temporary file '%s': %s", fileName, strerror(errno)); } name = fileName; return stream; } void TempFile::close() { if (fclose(stream) == EOF) { Die("error closing temporary file '%s': %s", name, strerror(errno)); } stream = nullptr; } void TempFile::remove() { if (::remove(name) != 0) { Die("error deleting temporary file '%s': %s", name, strerror(errno)); } name = nullptr; } TestJSPrincipals::TestJSPrincipals(int rc) { refcount = rc; } bool TestJSPrincipals::write(JSContext* cx, JSStructuredCloneWriter* writer) { MOZ_CRASH("TestJSPrincipals::write not implemented"); } ExternalData::ExternalData(const char* str) : contents_(strdup(str)), len_(strlen(str) + 1) {} void ExternalData::free() { MOZ_ASSERT(!wasFreed()); ::free(contents_); contents_ = nullptr; } mozilla::UniquePtr ExternalData::pointer() { MOZ_ASSERT(!uniquePointerCreated_, "Not allowed to create multiple unique pointers to contents"); uniquePointerCreated_ = true; return {contents_, {ExternalData::freeCallback, this}}; } /* static */ void ExternalData::freeCallback(void* contents, void* userData) { auto self = static_cast(userData); MOZ_ASSERT(self->contents() == contents); self->free(); } AutoGCParameter::AutoGCParameter(JSContext* cx, JSGCParamKey key, uint32_t value) : cx_(cx), key_(key), value_() { value_ = JS_GetGCParameter(cx, key); JS_SetGCParameter(cx, key, value); } AutoGCParameter::~AutoGCParameter() { JS_SetGCParameter(cx_, key_, value_); } #ifdef JS_GC_ZEAL AutoLeaveZeal::AutoLeaveZeal(JSContext* cx) : cx_(cx), zealBits_(0), frequency_(0) { uint32_t dummy; JS::GetGCZealBits(cx_, &zealBits_, &frequency_, &dummy); JS::SetGCZeal(cx_, 0, 0); JS::PrepareForFullGC(cx_); JS::NonIncrementalGC(cx_, JS::GCOptions::Normal, JS::GCReason::DEBUG_GC); } AutoLeaveZeal::~AutoLeaveZeal() { JS::SetGCZeal(cx_, 0, 0); for (size_t i = 0; i < sizeof(zealBits_) * 8; i++) { if (zealBits_ & (1 << i)) { JS::SetGCZeal(cx_, i, frequency_); } } # ifdef DEBUG uint32_t zealBitsAfter, frequencyAfter, dummy; JS::GetGCZealBits(cx_, &zealBitsAfter, &frequencyAfter, &dummy); MOZ_ASSERT(zealBitsAfter == zealBits_); MOZ_ASSERT(frequencyAfter == frequency_); # endif } #else AutoLeaveZeal::AutoLeaveZeal(JSContext* cx) {} AutoLeaveZeal::~AutoLeaveZeal() {} #endif struct CommandOptions { bool list = false; bool runRuntimeTests = true; bool runFrontendTests = true; bool help = false; const char* filter = nullptr; }; static void PrintUsage() { printf("Usage: jsapi-tests [OPTIONS] [FILTER]\n"); printf("\n"); printf("Options:\n"); printf(" -h, --help Display this message\n"); printf(" --list List all tests\n"); printf( " --frontend-only Run tests for frontend-only APIs, with " "light-weight entry point\n"); } static void ParseArgs(int argc, char* argv[], CommandOptions& options) { for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { options.help = true; continue; } if (strcmp(argv[i], "--list") == 0) { options.list = true; continue; } if (strcmp(argv[i], "--frontend-only") == 0) { options.runRuntimeTests = false; continue; } if (!options.filter) { options.filter = argv[i]; continue; } printf("error: Unrecognized option: %s\n", argv[i]); options.help = true; } } static void NewHandler() { std::set_new_handler(nullptr); Die("Out of memory."); } template void PrintTests(TestList list) { for (TestT* test = list.getFirst(); test; test = test->next) { printf("%s\n", test->name()); } } void RunTests(int& total, int& failures, CommandOptions& options, TestList list) { // Reinitializing the global for every test is quite slow, due to having to // recompile all self-hosted builtins. Allow tests to opt-in to reusing the // context and global. JSContext* maybeReusedContext = nullptr; auto guard = mozilla::MakeScopeExit( [&]() { RuntimeTest::MaybeFreeContext(maybeReusedContext); }); for (TestBase* test = list.getFirst(); test; test = test->next) { if ((test->isRuntimeTest() && !options.runRuntimeTests) || (!test->isRuntimeTest() && !options.runFrontendTests)) { continue; } const char* name = test->name(); if (options.filter && strstr(name, options.filter) == nullptr) { continue; } total += 1; printf("%s\n", name); // Make sure the test name is printed before we enter the test that can // crash on failure. fflush(stdout); if (test->isRuntimeTest() && !RuntimeTest::From(test)->initContext(maybeReusedContext)) { printf("TEST-UNEXPECTED-FAIL | %s | Failed to set context.\n", name); failures++; continue; } if (!test->init()) { printf("TEST-UNEXPECTED-FAIL | %s | Failed to initialize.\n", name); failures++; test->uninit(); continue; } if (test->run()) { printf("TEST-PASS | %s | ok\n", name); } else { std::string messages = test->messages(); printf("%s | %s | %s\n", (test->knownFail ? "TEST-KNOWN-FAIL" : "TEST-UNEXPECTED-FAIL"), name, messages.c_str()); if (!test->knownFail) { failures++; } } if (test->isRuntimeTest()) { // Return a non-nullptr pointer if the context & global can safely be // reused for the next test. maybeReusedContext = RuntimeTest::From(test)->maybeForgetContext(); } test->uninit(); } } int main(int argc, char* argv[]) { int total = 0; int failures = 0; CommandOptions options; ParseArgs(argc, argv, options); if (options.help) { PrintUsage(); return 0; } // Ensure allocation failure in std::string gets reported as test failure. std::set_new_handler(NewHandler); // Override prefs for jsapi-tests. JS::Prefs::setAtStartup_experimental_weakrefs_expose_cleanupSome(true); JS::Prefs::setAtStartup_experimental_symbols_as_weakmap_keys(true); if (options.runRuntimeTests) { if (!JS_Init()) { Die("JS_Init() failed."); } } else if (options.runFrontendTests) { if (!JS_FrontendOnlyInit()) { Die("JS_FrontendOnlyInit() failed."); } } if (options.list) { PrintTests(testList); return 0; } RunTests(total, failures, options, testList); MOZ_RELEASE_ASSERT(!JSRuntime::hasLiveRuntimes()); if (options.runRuntimeTests) { JS_ShutDown(); } else if (options.runFrontendTests) { JS_FrontendOnlyShutDown(); } if (failures) { printf("\n%d unexpected failure%s.\n", failures, (failures == 1 ? "" : "s")); return 1; } printf("\nPassed: ran %d tests.\n", total); return 0; }