diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /js/src/jsapi-tests | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'js/src/jsapi-tests')
157 files changed, 29801 insertions, 0 deletions
diff --git a/js/src/jsapi-tests/Makefile.in b/js/src/jsapi-tests/Makefile.in new file mode 100644 index 0000000000..2aac798393 --- /dev/null +++ b/js/src/jsapi-tests/Makefile.in @@ -0,0 +1,9 @@ +# -*- Mode: makefile -*- +# +# 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/. + +ifdef QEMU_EXE +MOZ_POST_PROGRAM_COMMAND = $(topsrcdir)/build/qemu-wrap --qemu $(QEMU_EXE) --libdir $(CROSS_LIB) +endif diff --git a/js/src/jsapi-tests/README b/js/src/jsapi-tests/README new file mode 100644 index 0000000000..5e03748f0e --- /dev/null +++ b/js/src/jsapi-tests/README @@ -0,0 +1,173 @@ +# JSAPI Test Suite + +The tests in this directory exercise the JSAPI. + + +## Building and running the tests + +If you built JS, you already built the tests. + +The tests are built by default when you build JS. All the tests are compiled +into a single binary named jsapi-tests. They all run in a single process. +They must be run from the js/src directory. + +To run the tests: + + $OBJDIR/dist/bin/jsapi-tests + +To run the tests in a debugger: + + gdb $OBJDIR/dist/bin/jsapi-tests + + +## Creating new tests + +1. You can either add to an existing test*.cpp file or make a new one. + Copy an existing test and replace the body with your test code. + The test harness provides `cx`, `rt`, and `global` for your use. + +2. If you made a new .cpp file, add it to the UNIFIED_SOURCES list + in moz.build. + + +## Writing test code + +Here is a sample test: + + #include "jsapi-tests/tests.h" + + BEGIN_TEST(testIntString_bug515273) + { + RootedValue v(cx); + + EVAL("'42';", &v); + JSString *str = v.toString(); + CHECK(JS_StringHasBeenInterned(cx, str)); + CHECK(JS_FlatStringEqualsLiteral(JS_ASSERT_STRING_IS_FLAT(str), "42")); + return true; + } + END_TEST(testIntString_bug515273) + +The BEGIN_TEST and END_TEST macros bracket each test. By convention, the test +name is <testFilename>_<detail>. (The above test is in testIntString.cpp.) + +The curly braces are required. This block is the body of a C++ member function +that returns bool. The test harness calls this member function +automatically. If the function returns true, the test passes. False, it fails. + +JSAPI tests often need extra global C/C++ code: a JSClass, a getter or setter +function, a resolve hook. Put these before the BEGIN_TEST macro. + +The body of the test can use these member variables and macros, defined in +tests.h: + + JSRuntime *rt; + JSContext *cx; + JSObject *global; + + The test framework creates these fresh for each test. The default + environment has reasonable default settings, including + JSOPTION_VAROBJFIX, JSOPTION_JIT, a global object of a class with + JSCLASS_GLOBAL_FLAGS, and an error reporter that prints to stderr. + See also "Custom test setup" below. + + EXEC(const char *code); + + Execute some JS code in global scope, using JS::Evaluate. Return + false if that fails. (This means that if the code throws an uncaught JS + exception, the test fails.) + + EVAL(const char *code, jsval *vp); + + Same as EXEC, but store the result value in *vp. + + CHECK(bool cond); + + If the condition is not true, print an error message and return false, + failing the test. + + CHECK_SAME(jsval a, jsval b); + + If a and b are different values, print an error message and return + false, failing the test. + + This is like CHECK(sameValue(a, b)) but with a more detailed error + message on failure. See sameValue below. + + CHECK_EQUAL(const T &a, const U &b); + + CHECK(a == b), but with a more detailed error message. + + CHECK_NULL(const T *ptr); + + CHECK(ptr == nullptr), but with a more detailed error message. + + (This is here because CHECK_EQUAL(ptr, nullptr) fails to compile on GCC + 2.5 and before.) + + + bool knownFail; + + Set this to true if your test is known to fail. The test runner will + print a TEST-KNOWN-FAIL line rather than a TEST-UNEXPECTED-FAIL + line. This way you can check in a test illustrating a bug ahead of the + fix. + + If your test actually crashes the process or triggers an assertion, + this of course will not help, so you should add something like + + knownFail = true; // see bug 123456 + return false; // the code below crashes! + + as the first two lines of the test. + + bool isNegativeZero(jsval v); + bool isNaN(jsval v); + + Self-explanatory. + + bool sameValue(jsval v1, jsval v2); + + True if v1 and v2 are the same value according to the ES5 SameValue() + function, to wit: + + SameValue(NaN, NaN) is true. + SameValue(-0, 0) is false. + Otherwise SameValue(a, b) iff a === b. + + +## Custom test setup + +Before executing each test, the test framework calls the tests' init() member +function, which populates the rt, cx, and global member variables. + +A test can customize the test setup process by overloading virtual member +functions from the JSAPIRuntimeTest class, like this: + + const JSClass globalClassWithResolve = { ... }; + + BEGIN_TEST(testGlobalResolveHook) + { + RootedValue v; + EVAL("v", v.address()); + CHECK_SAME(v, JSVAL_VOID); + return true; + } + + // Other class members can go here. + + // This one overloads a base-class method. + virtual JSClass *getGlobalJSClass() { + return &globalClassWithResolve; + } + END_TEST(testGlobalResolveHook) + +The overloadable member functions are: + + virtual bool init(); + virtual void uninit(); + virtual JSRuntime * createRuntime(); + virtual JSContext * createContext(); + virtual JSClass * getGlobalClass(); + virtual JSObject * createGlobal(); + diff --git a/js/src/jsapi-tests/hidePointer.cpp b/js/src/jsapi-tests/hidePointer.cpp new file mode 100644 index 0000000000..144c890359 --- /dev/null +++ b/js/src/jsapi-tests/hidePointer.cpp @@ -0,0 +1,25 @@ +/* -*- 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 "mozilla/Atomics.h" + +// This is an attempt to hide pointer values from the C++ compiler. This might +// not stand up in the presence of PGO but that's probably not important. + +// g_hidden_pointer is public so that it's limited what the compiler can assume +// about it, and atomic so that we don't run afoul of the compiler's UB +// analysis. + +mozilla::Atomic<void*> g_hidden_pointer; + +// Call this to install a pointer into the global. + +MOZ_NEVER_INLINE void setHiddenPointer(void* p) { g_hidden_pointer = p; } + +// Call this to retrieve the pointer. + +MOZ_NEVER_INLINE void* getHiddenPointer() { return g_hidden_pointer; } diff --git a/js/src/jsapi-tests/jsapi-tests-gdb.py.in b/js/src/jsapi-tests/jsapi-tests-gdb.py.in new file mode 100644 index 0000000000..c5a12f38e0 --- /dev/null +++ b/js/src/jsapi-tests/jsapi-tests-gdb.py.in @@ -0,0 +1,8 @@ +"""GDB Python customization auto-loader for jsapi-tests executable""" +#filter substitution + +import os.path +sys.path[0:0] = [os.path.join('@topsrcdir@', 'gdb')] + +import mozilla.autoload +mozilla.autoload.register(gdb.current_objfile()) diff --git a/js/src/jsapi-tests/moz.build b/js/src/jsapi-tests/moz.build new file mode 100644 index 0000000000..9e2c99cfa6 --- /dev/null +++ b/js/src/jsapi-tests/moz.build @@ -0,0 +1,201 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +GeckoProgram("jsapi-tests", linkage=None) + +include("../js-cxxflags.mozbuild") +include("../js-standalone.mozbuild") + +UNIFIED_SOURCES += [ + "selfTest.cpp", + "testAddPropertyPropcache.cpp", + "testArgumentsObject.cpp", + "testArrayBuffer.cpp", + "testArrayBufferOrViewAPI.cpp", + "testArrayBufferView.cpp", + "testArrayBufferWithUserOwnedContents.cpp", + "testAtomicOperations.cpp", + "testAtomizeUtf8NonAsciiLatin1CodePoint.cpp", + "testAtomizeWithoutActiveZone.cpp", + "testAvlTree.cpp", + "testBigInt.cpp", + "testBoundFunction.cpp", + "testBug604087.cpp", + "testCallArgs.cpp", + "testCallNonGenericMethodOnProxy.cpp", + "testCharacterEncoding.cpp", + "testChromeBuffer.cpp", + "testCompileNonSyntactic.cpp", + "testCompileScript.cpp", + "testCompileUtf8.cpp", + "testDateToLocaleString.cpp", + "testDebugger.cpp", + "testDeduplication.cpp", + "testDeepFreeze.cpp", + "testDefineGetterSetterNonEnumerable.cpp", + "testDefineProperty.cpp", + "testDeflateStringToUTF8Buffer.cpp", + "testDeleteProperty.cpp", + "testDifferentNewTargetInvokeConstructor.cpp", + "testEmptyWindowIsOmitted.cpp", + "testErrorCopying.cpp", + "testErrorLineOfContext.cpp", + "testException.cpp", + "testExecuteInJSMEnvironment.cpp", + "testExternalStrings.cpp", + "testFindSCCs.cpp", + "testForceLexicalInitialization.cpp", + "testForOfIterator.cpp", + "testForwardSetProperty.cpp", + "testFreshGlobalEvalRedefinition.cpp", + "testFrontendCompileStencil.cpp", + "testFrontendErrors.cpp", + "testFrontendJSON.cpp", + "testFunctionBinding.cpp", + "testFunctionNonSyntactic.cpp", + "testFunctionProperties.cpp", + "testGCAllocator.cpp", + "testGCCellPtr.cpp", + "testGCChunkPool.cpp", + "testGCExactRooting.cpp", + "testGCFinalizeCallback.cpp", + "testGCGrayMarking.cpp", + "testGCHeapBarriers.cpp", + "testGCHooks.cpp", + "testGCMarking.cpp", + "testGCOutOfMemory.cpp", + "testGCStoreBufferRemoval.cpp", + "testGCUniqueId.cpp", + "testGCWeakCache.cpp", + "testGetPropertyDescriptor.cpp", + "testHashTable.cpp", + "testIndexToString.cpp", + "testInformalValueTypeName.cpp", + "testIntern.cpp", + "testIntlAvailableLocales.cpp", + "testIntString.cpp", + "testIsInsideNursery.cpp", + "testIsISOStyleDate.cpp", + "testIteratorObject.cpp", + "testJSEvaluateScript.cpp", + "testJSON.cpp", + "testLargeArrayBuffers.cpp", + "testLookup.cpp", + "testLooselyEqual.cpp", + "testMappedArrayBuffer.cpp", + "testMemoryAssociation.cpp", + "testMutedErrors.cpp", + "testNewObject.cpp", + "testNewTargetInvokeConstructor.cpp", + "testNullRoot.cpp", + "testNumberToString.cpp", + "testObjectEmulatingUndefined.cpp", + "testObjectSwap.cpp", + "testOOM.cpp", + "testParseJSON.cpp", + "testParserAtom.cpp", + "testPersistentRooted.cpp", + "testPreserveJitCode.cpp", + "testPrintf.cpp", + "testPrivateGCThingValue.cpp", + "testProfileStrings.cpp", + "testPromise.cpp", + "testPropCache.cpp", + "testPropertyKey.cpp", + "testRecordTupleToSource.cpp", + "testRegExp.cpp", + "testResolveRecursion.cpp", + "testResult.cpp", + "tests.cpp", + "testSABAccounting.cpp", + "testSameValue.cpp", + "testSavedStacks.cpp", + "testScriptInfo.cpp", + "testScriptObject.cpp", + "testScriptSourceCompression.cpp", + "testSetProperty.cpp", + "testSetPropertyIgnoringNamedGetter.cpp", + "testSharedImmutableStringsCache.cpp", + "testSinglyLinkedList.cpp", + "testSliceBudget.cpp", + "testSlowScript.cpp", + "testSourcePolicy.cpp", + "testSparseBitmap.cpp", + "testStencil.cpp", + "testStringBuffer.cpp", + "testStringIsArrayIndex.cpp", + "testStructuredClone.cpp", + "testSymbol.cpp", + "testThreadingConditionVariable.cpp", + "testThreadingExclusiveData.cpp", + "testThreadingMutex.cpp", + "testThreadingThread.cpp", + "testToSignedOrUnsignedInteger.cpp", + "testTypedArrays.cpp", + "testUbiNode.cpp", + "testUncaughtSymbol.cpp", + "testUTF8.cpp", + "testWasmLEB128.cpp", + "testWasmReturnCalls.cpp", + "testWeakMap.cpp", + "testWindowNonConfigurable.cpp", +] + +SOURCES += [ + # There are clashing definitions of js::jit::AssemblerBuffer. + "testAssemblerBuffer.cpp", +] + +SOURCES += [ + # We don't want this in the C++ files with the test cases. + "hidePointer.cpp", +] + +if not CONFIG["JS_CODEGEN_NONE"]: + UNIFIED_SOURCES += [ + "testJitABIcalls.cpp", + "testJitDCEinGVN.cpp", + "testJitFoldsTo.cpp", + "testJitGVN.cpp", + "testJitMacroAssembler.cpp", + "testJitMoveEmitterCycles-mips32.cpp", + "testJitMoveEmitterCycles.cpp", + "testJitRangeAnalysis.cpp", + "testJitRegisterSet.cpp", + "testJitRValueAlloc.cpp", + "testsJit.cpp", + ] + +if CONFIG["NIGHTLY_BUILD"]: + # The Error interceptor only exists on Nightly. + UNIFIED_SOURCES += [ + "testErrorInterceptor.cpp", + "testErrorInterceptorGC.cpp", + ] + +if CONFIG["OS_ARCH"] not in ("WINNT", "Darwin") and CONFIG["OS_TARGET"] != "Android": + # open_memstream() not available on Windows, macOS, or Android + UNIFIED_SOURCES += [ + "testPrintError.cpp", + ] + +if CONFIG["MOZ_DEBUG"] or CONFIG["NIGHTLY_BUILD"]: + DEFINES["JS_CACHEIR_SPEW"] = True + DEFINES["JS_STRUCTURED_SPEW"] = True + +DEFINES["EXPORT_JS_API"] = True + +LOCAL_INCLUDES += [ + "!..", + "..", +] + +USE_LIBS += [ + "static:js", +] + +DEFINES["topsrcdir"] = "%s/js/src" % TOPSRCDIR +OBJDIR_PP_FILES.js.src["jsapi-tests"] += ["jsapi-tests-gdb.py.in"] diff --git a/js/src/jsapi-tests/selfTest.cpp b/js/src/jsapi-tests/selfTest.cpp new file mode 100644 index 0000000000..aefd6982d9 --- /dev/null +++ b/js/src/jsapi-tests/selfTest.cpp @@ -0,0 +1,19 @@ +/* -*- 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" + +BEGIN_TEST(selfTest_NaNsAreSame) { + JS::RootedValue v1(cx), v2(cx); + EVAL("0/0", &v1); // NaN + CHECK_SAME(v1, v1); + + EVAL("Math.sin('no')", &v2); // also NaN + CHECK_SAME(v1, v2); + return true; +} +END_TEST(selfTest_NaNsAreSame) diff --git a/js/src/jsapi-tests/testAddPropertyPropcache.cpp b/js/src/jsapi-tests/testAddPropertyPropcache.cpp new file mode 100644 index 0000000000..7fa1e86df9 --- /dev/null +++ b/js/src/jsapi-tests/testAddPropertyPropcache.cpp @@ -0,0 +1,67 @@ +/* -*- 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 "js/Array.h" // JS::NewArrayObject +#include "js/PropertyAndElement.h" // JS_DefineElement, JS_DefineProperty +#include "jsapi-tests/tests.h" + +static int callCount = 0; + +static bool AddProperty(JSContext* cx, JS::HandleObject obj, JS::HandleId id, + JS::HandleValue v) { + callCount++; + return true; +} + +static const JSClassOps AddPropertyClassOps = { + AddProperty, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + nullptr, // finalize + nullptr, // call + nullptr, // construct + nullptr, // trace +}; + +static const JSClass AddPropertyClass = {"AddPropertyTester", 0, + &AddPropertyClassOps}; + +BEGIN_TEST(testAddPropertyHook) { + /* + * Do the test a bunch of times, because sometimes we seem to randomly + * miss the propcache. + */ + static const int ExpectedCount = 100; + + JS::RootedObject obj(cx, JS::NewArrayObject(cx, 0)); + CHECK(obj); + JS::RootedValue arr(cx, JS::ObjectValue(*obj)); + + CHECK(JS_DefineProperty(cx, global, "arr", arr, JSPROP_ENUMERATE)); + + JS::RootedObject arrObj(cx, &arr.toObject()); + for (int i = 0; i < ExpectedCount; ++i) { + obj = JS_NewObject(cx, &AddPropertyClass); + CHECK(obj); + CHECK(JS_DefineElement(cx, arrObj, i, obj, JSPROP_ENUMERATE)); + } + + // Now add a prop to each of the objects, but make sure to do + // so at the same bytecode location so we can hit the propcache. + EXEC( + "'use strict'; \n" + "for (var i = 0; i < arr.length; ++i) \n" + " arr[i].prop = 42; \n"); + + CHECK(callCount == ExpectedCount); + + return true; +} +END_TEST(testAddPropertyHook) diff --git a/js/src/jsapi-tests/testArgumentsObject.cpp b/js/src/jsapi-tests/testArgumentsObject.cpp new file mode 100644 index 0000000000..8378de7e08 --- /dev/null +++ b/js/src/jsapi-tests/testArgumentsObject.cpp @@ -0,0 +1,91 @@ +/* -*- 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 "vm/ArgumentsObject-inl.h" +#include "vm/JSObject-inl.h" + +using namespace js; + +static const char NORMAL_ZERO[] = "function f() { return arguments; }"; +static const char NORMAL_ONE[] = "function f(a) { return arguments; }"; +static const char NORMAL_TWO[] = "function f(a, b) { return arguments; }"; +static const char NORMAL_THREE[] = "function f(a, b, c) { return arguments; }"; + +static const char STRICT_ZERO[] = + "function f() { 'use strict'; return arguments; }"; +static const char STRICT_ONE[] = + "function f() { 'use strict'; return arguments; }"; +static const char STRICT_TWO[] = + "function f() { 'use strict'; return arguments; }"; +static const char STRICT_THREE[] = + "function f() { 'use strict'; return arguments; }"; + +static const char* const CALL_CODES[] = {"f()", "f(0)", + "f(0, 1)", "f(0, 1, 2)", + "f(0, 1, 2, 3)", "f(0, 1, 2, 3, 4)"}; + +BEGIN_TEST(testArgumentsObject) { + return ExhaustiveTest<0>(NORMAL_ZERO) && ExhaustiveTest<1>(NORMAL_ZERO) && + ExhaustiveTest<2>(NORMAL_ZERO) && ExhaustiveTest<0>(NORMAL_ONE) && + ExhaustiveTest<1>(NORMAL_ONE) && ExhaustiveTest<2>(NORMAL_ONE) && + ExhaustiveTest<3>(NORMAL_ONE) && ExhaustiveTest<0>(NORMAL_TWO) && + ExhaustiveTest<1>(NORMAL_TWO) && ExhaustiveTest<2>(NORMAL_TWO) && + ExhaustiveTest<3>(NORMAL_TWO) && ExhaustiveTest<4>(NORMAL_TWO) && + ExhaustiveTest<0>(NORMAL_THREE) && ExhaustiveTest<1>(NORMAL_THREE) && + ExhaustiveTest<2>(NORMAL_THREE) && ExhaustiveTest<3>(NORMAL_THREE) && + ExhaustiveTest<4>(NORMAL_THREE) && ExhaustiveTest<5>(NORMAL_THREE) && + ExhaustiveTest<0>(STRICT_ZERO) && ExhaustiveTest<1>(STRICT_ZERO) && + ExhaustiveTest<2>(STRICT_ZERO) && ExhaustiveTest<0>(STRICT_ONE) && + ExhaustiveTest<1>(STRICT_ONE) && ExhaustiveTest<2>(STRICT_ONE) && + ExhaustiveTest<3>(STRICT_ONE) && ExhaustiveTest<0>(STRICT_TWO) && + ExhaustiveTest<1>(STRICT_TWO) && ExhaustiveTest<2>(STRICT_TWO) && + ExhaustiveTest<3>(STRICT_TWO) && ExhaustiveTest<4>(STRICT_TWO) && + ExhaustiveTest<0>(STRICT_THREE) && ExhaustiveTest<1>(STRICT_THREE) && + ExhaustiveTest<2>(STRICT_THREE) && ExhaustiveTest<3>(STRICT_THREE) && + ExhaustiveTest<4>(STRICT_THREE) && ExhaustiveTest<5>(STRICT_THREE); +} + +static const size_t MAX_ELEMS = 6; + +template <size_t ArgCount> +bool ExhaustiveTest(const char funcode[]) { + RootedValue v(cx); + EVAL(funcode, &v); + + EVAL(CALL_CODES[ArgCount], &v); + Rooted<ArgumentsObject*> argsobj(cx, + &v.toObjectOrNull()->as<ArgumentsObject>()); + + JS::RootedValueArray<MAX_ELEMS> elems(cx); + + for (size_t i = 0; i <= ArgCount; i++) { + for (size_t j = 0; j <= ArgCount - i; j++) { + ClearElements(elems); + CHECK(argsobj->maybeGetElements(i, j, elems.begin())); + for (size_t k = 0; k < j; k++) { + CHECK(elems[k].isInt32(i + k)); + } + for (size_t k = j; k < MAX_ELEMS - 1; k++) { + CHECK(elems[k].isNull()); + } + CHECK(elems[MAX_ELEMS - 1].isInt32(42)); + } + } + + return true; +} + +template <size_t N> +static void ClearElements(JS::RootedValueArray<N>& elems) { + for (size_t i = 0; i < elems.length() - 1; i++) { + elems[i].setNull(); + } + elems[elems.length() - 1].setInt32(42); +} +END_TEST(testArgumentsObject) diff --git a/js/src/jsapi-tests/testArrayBuffer.cpp b/js/src/jsapi-tests/testArrayBuffer.cpp new file mode 100644 index 0000000000..a32ab27741 --- /dev/null +++ b/js/src/jsapi-tests/testArrayBuffer.cpp @@ -0,0 +1,449 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + */ + +#include "builtin/TestingFunctions.h" +#include "js/Array.h" // JS::NewArrayObject +#include "js/ArrayBuffer.h" // JS::{GetArrayBuffer{ByteLength,Data},IsArrayBufferObject,NewArrayBuffer{,WithContents},StealArrayBufferContents} +#include "js/ArrayBufferMaybeShared.h" +#include "js/CallAndConstruct.h" +#include "js/Exception.h" +#include "js/experimental/TypedData.h" // JS_New{Int32,Uint8}ArrayWithBuffer +#include "js/friend/ErrorMessages.h" // JSMSG_* +#include "js/MemoryFunctions.h" +#include "js/PropertyAndElement.h" // JS_GetElement, JS_GetProperty, JS_SetElement +#include "js/Realm.h" +#include "jsapi-tests/tests.h" + +#include "vm/Realm-inl.h" + +BEGIN_TEST(testArrayBuffer_bug720949_steal) { + static const unsigned NUM_TEST_BUFFERS = 2; + static const unsigned MAGIC_VALUE_1 = 3; + static const unsigned MAGIC_VALUE_2 = 17; + + JS::RootedObject buf_len1(cx), buf_len200(cx); + JS::RootedObject tarray_len1(cx), tarray_len200(cx); + + uint32_t sizes[NUM_TEST_BUFFERS] = {sizeof(uint32_t), 200 * sizeof(uint32_t)}; + JS::HandleObject testBuf[NUM_TEST_BUFFERS] = {buf_len1, buf_len200}; + JS::HandleObject testArray[NUM_TEST_BUFFERS] = {tarray_len1, tarray_len200}; + + // Single-element ArrayBuffer (uses fixed slots for storage) + CHECK(buf_len1 = JS::NewArrayBuffer(cx, sizes[0])); + CHECK(tarray_len1 = JS_NewInt32ArrayWithBuffer(cx, testBuf[0], 0, -1)); + + CHECK(JS_SetElement(cx, testArray[0], 0, MAGIC_VALUE_1)); + + // Many-element ArrayBuffer (uses dynamic storage) + CHECK(buf_len200 = JS::NewArrayBuffer(cx, 200 * sizeof(uint32_t))); + CHECK(tarray_len200 = JS_NewInt32ArrayWithBuffer(cx, testBuf[1], 0, -1)); + + for (unsigned i = 0; i < NUM_TEST_BUFFERS; i++) { + JS::HandleObject obj = testBuf[i]; + JS::HandleObject view = testArray[i]; + uint32_t size = sizes[i]; + JS::RootedValue v(cx); + + // Byte lengths should all agree + CHECK(JS::IsArrayBufferObject(obj)); + CHECK_EQUAL(JS::GetArrayBufferByteLength(obj), size); + CHECK(JS_GetProperty(cx, obj, "byteLength", &v)); + CHECK(v.isInt32(size)); + CHECK(JS_GetProperty(cx, view, "byteLength", &v)); + CHECK(v.isInt32(size)); + + // Modifying the underlying data should update the value returned through + // the view + { + JS::AutoCheckCannotGC nogc; + bool sharedDummy; + uint8_t* data = JS::GetArrayBufferData(obj, &sharedDummy, nogc); + CHECK(data != nullptr); + *reinterpret_cast<uint32_t*>(data) = MAGIC_VALUE_2; + } + CHECK(JS_GetElement(cx, view, 0, &v)); + CHECK(v.isInt32(MAGIC_VALUE_2)); + + // Steal the contents + mozilla::UniquePtr<void, JS::FreePolicy> contents{ + JS::StealArrayBufferContents(cx, obj)}; + CHECK(contents != nullptr); + + CHECK(JS::IsDetachedArrayBufferObject(obj)); + + // Transfer to a new ArrayBuffer + JS::RootedObject dst( + cx, JS::NewArrayBufferWithContents(cx, size, std::move(contents))); + CHECK(JS::IsArrayBufferObject(dst)); + { + JS::AutoCheckCannotGC nogc; + bool sharedDummy; + (void)JS::GetArrayBufferData(obj, &sharedDummy, nogc); + } + + JS::RootedObject dstview(cx, JS_NewInt32ArrayWithBuffer(cx, dst, 0, -1)); + CHECK(dstview != nullptr); + + CHECK_EQUAL(JS::GetArrayBufferByteLength(dst), size); + { + JS::AutoCheckCannotGC nogc; + bool sharedDummy; + uint8_t* data = JS::GetArrayBufferData(dst, &sharedDummy, nogc); + CHECK(data != nullptr); + CHECK_EQUAL(*reinterpret_cast<uint32_t*>(data), MAGIC_VALUE_2); + } + CHECK(JS_GetElement(cx, dstview, 0, &v)); + CHECK(v.isInt32(MAGIC_VALUE_2)); + } + + return true; +} +END_TEST(testArrayBuffer_bug720949_steal) + +// Varying number of views of a buffer, to test the detachment weak pointers +BEGIN_TEST(testArrayBuffer_bug720949_viewList) { + JS::RootedObject buffer(cx); + + // No views + buffer = JS::NewArrayBuffer(cx, 2000); + buffer = nullptr; + GC(cx); + + // One view. + { + buffer = JS::NewArrayBuffer(cx, 2000); + JS::RootedObject view(cx, JS_NewUint8ArrayWithBuffer(cx, buffer, 0, -1)); + void* contents = JS::StealArrayBufferContents(cx, buffer); + CHECK(contents != nullptr); + JS_free(nullptr, contents); + GC(cx); + CHECK(hasDetachedBuffer(view)); + CHECK(JS::IsDetachedArrayBufferObject(buffer)); + view = nullptr; + GC(cx); + buffer = nullptr; + GC(cx); + } + + // Two views + { + buffer = JS::NewArrayBuffer(cx, 2000); + + JS::RootedObject view1(cx, JS_NewUint8ArrayWithBuffer(cx, buffer, 0, -1)); + JS::RootedObject view2(cx, JS_NewUint8ArrayWithBuffer(cx, buffer, 1, 200)); + + // Remove, re-add a view + view2 = nullptr; + GC(cx); + view2 = JS_NewUint8ArrayWithBuffer(cx, buffer, 1, 200); + + // Detach + void* contents = JS::StealArrayBufferContents(cx, buffer); + CHECK(contents != nullptr); + JS_free(nullptr, contents); + + CHECK(hasDetachedBuffer(view1)); + CHECK(hasDetachedBuffer(view2)); + CHECK(JS::IsDetachedArrayBufferObject(buffer)); + + view1 = nullptr; + GC(cx); + view2 = nullptr; + GC(cx); + buffer = nullptr; + GC(cx); + } + + return true; +} + +static void GC(JSContext* cx) { + JS_GC(cx); + JS_GC(cx); // Trigger another to wait for background finalization to end +} + +bool hasDetachedBuffer(JS::HandleObject obj) { + JS::RootedValue v(cx); + return JS_GetProperty(cx, obj, "byteLength", &v) && v.toInt32() == 0; +} + +END_TEST(testArrayBuffer_bug720949_viewList) + +BEGIN_TEST(testArrayBuffer_customFreeFunc) { + ExternalData data("One two three four"); + auto dataPointer = data.pointer(); + + // The buffer takes ownership of the data. + JS::RootedObject buffer( + cx, JS::NewExternalArrayBuffer(cx, data.len(), std::move(dataPointer))); + CHECK(buffer); + CHECK(!data.wasFreed()); + + size_t len; + bool isShared; + uint8_t* bufferData; + JS::GetArrayBufferLengthAndData(buffer, &len, &isShared, &bufferData); + CHECK_EQUAL(len, data.len()); + CHECK(bufferData == data.contents()); + CHECK(strcmp(reinterpret_cast<char*>(bufferData), data.asString()) == 0); + + buffer = nullptr; + JS_GC(cx); + JS_GC(cx); + CHECK(data.wasFreed()); + + return true; +} +END_TEST(testArrayBuffer_customFreeFunc) + +BEGIN_TEST(testArrayBuffer_staticContents) { + ExternalData data("One two three four"); + + JS::RootedObject buffer(cx, JS::NewArrayBufferWithUserOwnedContents( + cx, data.len(), data.contents())); + CHECK(buffer); + CHECK(!data.wasFreed()); + + size_t len; + bool isShared; + uint8_t* bufferData; + JS::GetArrayBufferLengthAndData(buffer, &len, &isShared, &bufferData); + CHECK_EQUAL(len, data.len()); + CHECK(bufferData == data.contents()); + CHECK(strcmp(reinterpret_cast<char*>(bufferData), data.asString()) == 0); + + buffer = nullptr; + JS_GC(cx); + JS_GC(cx); + CHECK(!data.wasFreed()); + + data.free(); + return true; +} +END_TEST(testArrayBuffer_staticContents) + +BEGIN_TEST(testArrayBuffer_stealDetachExternal) { + static const char dataBytes[] = "One two three four"; + ExternalData data(dataBytes); + auto dataPointer = data.pointer(); + JS::RootedObject buffer( + cx, JS::NewExternalArrayBuffer(cx, data.len(), std::move(dataPointer))); + CHECK(buffer); + CHECK(!data.wasFreed()); + + void* stolenContents = JS::StealArrayBufferContents(cx, buffer); + + // External buffers are stealable: the data is copied into freshly allocated + // memory, and the buffer's data pointer is cleared (immediately freeing the + // data) and the buffer is marked as detached. + CHECK(stolenContents != data.contents()); + CHECK(strcmp(reinterpret_cast<char*>(stolenContents), dataBytes) == 0); + CHECK(data.wasFreed()); + CHECK(JS::IsDetachedArrayBufferObject(buffer)); + + JS_free(cx, stolenContents); + return true; +} +END_TEST(testArrayBuffer_stealDetachExternal) + +BEGIN_TEST(testArrayBuffer_serializeExternal) { + JS::RootedValue serializeValue(cx); + + { + JS::RootedFunction serialize(cx); + serialize = + JS_NewFunction(cx, js::testingFunc_serialize, 1, 0, "serialize"); + CHECK(serialize); + + serializeValue.setObject(*JS_GetFunctionObject(serialize)); + } + + ExternalData data("One two three four"); + auto dataPointer = data.pointer(); + JS::RootedObject externalBuffer( + cx, JS::NewExternalArrayBuffer(cx, data.len(), std::move(dataPointer))); + CHECK(externalBuffer); + CHECK(!data.wasFreed()); + + JS::RootedValue v(cx, JS::ObjectValue(*externalBuffer)); + JS::RootedObject transferMap(cx, + JS::NewArrayObject(cx, JS::HandleValueArray(v))); + CHECK(transferMap); + + JS::RootedValueArray<2> args(cx); + args[0].setObject(*externalBuffer); + args[1].setObject(*transferMap); + + // serialize(externalBuffer, [externalBuffer]) should throw for an unhandled + // BufferContents kind. + CHECK(!JS::Call(cx, JS::UndefinedHandleValue, serializeValue, + JS::HandleValueArray(args), &v)); + + JS::ExceptionStack exnStack(cx); + CHECK(JS::StealPendingExceptionStack(cx, &exnStack)); + + JS::ErrorReportBuilder report(cx); + CHECK(report.init(cx, exnStack, JS::ErrorReportBuilder::NoSideEffects)); + + CHECK_EQUAL(report.report()->errorNumber, + static_cast<unsigned int>(JSMSG_SC_NOT_TRANSFERABLE)); + + // Data should have been left alone. + CHECK(!data.wasFreed()); + + v.setNull(); + transferMap = nullptr; + args[0].setNull(); + args[1].setNull(); + externalBuffer = nullptr; + + JS_GC(cx); + JS_GC(cx); + CHECK(data.wasFreed()); + + return true; +} +END_TEST(testArrayBuffer_serializeExternal) + +BEGIN_TEST(testArrayBuffer_copyData) { + ExternalData data1("One two three four"); + JS::RootedObject buffer1(cx, JS::NewArrayBufferWithUserOwnedContents( + cx, data1.len(), data1.contents())); + + CHECK(buffer1); + + ExternalData data2("Six"); + JS::RootedObject buffer2(cx, JS::NewArrayBufferWithUserOwnedContents( + cx, data2.len(), data2.contents())); + + CHECK(buffer2); + + // Check we can't copy from a larger to a smaller buffer. + CHECK(!JS::ArrayBufferCopyData(cx, buffer2, 0, buffer1, 0, data1.len())); + + // Verify expected exception is thrown. + { + JS::ExceptionStack exnStack(cx); + CHECK(JS::StealPendingExceptionStack(cx, &exnStack)); + + JS::ErrorReportBuilder report(cx); + CHECK(report.init(cx, exnStack, JS::ErrorReportBuilder::NoSideEffects)); + + CHECK_EQUAL(report.report()->errorNumber, + static_cast<unsigned int>(JSMSG_ARRAYBUFFER_COPY_RANGE)); + } + + CHECK(JS::ArrayBufferCopyData( + cx, buffer1, 0, buffer2, 0, + data2.len() - 1 /* don't copy null terminator */)); + + { + size_t len; + bool isShared; + uint8_t* bufferData; + JS::GetArrayBufferLengthAndData(buffer1, &len, &isShared, &bufferData); + + ExternalData expected1("Six two three four"); + + fprintf(stderr, "expected %s actual %s\n", expected1.asString(), + bufferData); + + CHECK_EQUAL(len, expected1.len()); + CHECK_EQUAL(memcmp(expected1.contents(), bufferData, expected1.len()), 0); + } + + return true; +} +END_TEST(testArrayBuffer_copyData) + +BEGIN_TEST(testArrayBuffer_copyDataAcrossGlobals) { + JS::RootedObject otherGlobal(cx, createGlobal(nullptr)); + if (!otherGlobal) { + return false; + } + + ExternalData data1("One two three four"); + JS::RootedObject buffer1(cx); + { + js::AutoRealm realm(cx, otherGlobal); + buffer1 = JS::NewArrayBufferWithUserOwnedContents(cx, data1.len(), + data1.contents()); + } + CHECK(buffer1); + CHECK(JS_WrapObject(cx, &buffer1)); + + ExternalData data2("Six"); + JS::RootedObject buffer2(cx, JS::NewArrayBufferWithUserOwnedContents( + cx, data2.len(), data2.contents())); + + CHECK(buffer2); + + // Check we can't copy from a larger to a smaller buffer. + CHECK(!JS::ArrayBufferCopyData(cx, buffer2, 0, buffer1, 0, data1.len())); + + // Verify expected exception is thrown. + { + JS::ExceptionStack exnStack(cx); + CHECK(JS::StealPendingExceptionStack(cx, &exnStack)); + + JS::ErrorReportBuilder report(cx); + CHECK(report.init(cx, exnStack, JS::ErrorReportBuilder::NoSideEffects)); + + CHECK_EQUAL(report.report()->errorNumber, + static_cast<unsigned int>(JSMSG_ARRAYBUFFER_COPY_RANGE)); + } + + CHECK(JS::ArrayBufferCopyData( + cx, buffer1, 0, buffer2, 0, + data2.len() - 1 /* don't copy null terminator */)); + + { + JS::RootedObject unwrappedBuffer1( + cx, JS::UnwrapArrayBufferMaybeShared(buffer1)); + CHECK(unwrappedBuffer1); + + size_t len; + bool isShared; + uint8_t* bufferData; + JS::GetArrayBufferLengthAndData(unwrappedBuffer1, &len, &isShared, + &bufferData); + + ExternalData expected1("Six two three four"); + + fprintf(stderr, "expected %s actual %s\n", expected1.asString(), + bufferData); + + CHECK_EQUAL(len, expected1.len()); + CHECK_EQUAL(memcmp(expected1.contents(), bufferData, expected1.len()), 0); + } + + return true; +} +END_TEST(testArrayBuffer_copyDataAcrossGlobals) + +BEGIN_TEST(testArrayBuffer_ArrayBufferClone) { + ExternalData data("One two three four"); + JS::RootedObject externalBuffer(cx, JS::NewArrayBufferWithUserOwnedContents( + cx, data.len(), data.contents())); + + CHECK(externalBuffer); + + size_t lengthToCopy = 3; + JS::RootedObject clonedBuffer( + cx, JS::ArrayBufferClone(cx, externalBuffer, 4, lengthToCopy)); + CHECK(clonedBuffer); + + size_t len; + bool isShared; + uint8_t* bufferData; + JS::GetArrayBufferLengthAndData(clonedBuffer, &len, &isShared, &bufferData); + + CHECK_EQUAL(len, lengthToCopy); + + ExternalData expectedData("two"); + CHECK_EQUAL(memcmp(expectedData.contents(), bufferData, len), 0); + + return true; +} +END_TEST(testArrayBuffer_ArrayBufferClone) diff --git a/js/src/jsapi-tests/testArrayBufferOrViewAPI.cpp b/js/src/jsapi-tests/testArrayBufferOrViewAPI.cpp new file mode 100644 index 0000000000..6b9189af04 --- /dev/null +++ b/js/src/jsapi-tests/testArrayBufferOrViewAPI.cpp @@ -0,0 +1,111 @@ +/* -*- 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 <type_traits> + +#include "jsfriendapi.h" + +#include "js/ArrayBuffer.h" // JS::NewArrayBuffer +#include "js/experimental/TypedData.h" +#include "js/ScalarType.h" // JS::Scalar::Type +#include "jsapi-tests/tests.h" + +#include "vm/Realm-inl.h" + +using namespace js; + +template <class ViewType> +static JSObject* CreateObj(JSContext* cx, size_t len) { + return ViewType::create(cx, len).asObject(); +} + +template <> +JSObject* CreateObj<JS::DataView>(JSContext* cx, size_t len) { + JS::Rooted<JSObject*> buffer(cx, JS::NewArrayBuffer(cx, len)); + if (!buffer) { + return nullptr; + } + return JS_NewDataView(cx, buffer, 0, len); +} + +BEGIN_TEST(testArrayBufferOrView_type) { + JS::RealmOptions options; + JS::RootedObject otherGlobal( + cx, JS_NewGlobalObject(cx, basicGlobalClass(), nullptr, + JS::DontFireOnNewGlobalHook, options)); + CHECK(otherGlobal); + + CHECK((TestType<JS::TypedArray<Scalar::Uint8>>(cx, otherGlobal))); + CHECK((TestType<JS::TypedArray<Scalar::Int8>>(cx, otherGlobal))); + CHECK((TestType<JS::TypedArray<Scalar::Uint8Clamped>>(cx, otherGlobal))); + CHECK((TestType<JS::TypedArray<Scalar::Uint16>>(cx, otherGlobal))); + CHECK((TestType<JS::TypedArray<Scalar::Int16>>(cx, otherGlobal))); + CHECK((TestType<JS::TypedArray<Scalar::Uint32>>(cx, otherGlobal))); + CHECK((TestType<JS::TypedArray<Scalar::Int32>>(cx, otherGlobal))); + CHECK((TestType<JS::TypedArray<Scalar::Float32>>(cx, otherGlobal))); + CHECK((TestType<JS::TypedArray<Scalar::Float64>>(cx, otherGlobal))); + CHECK((TestType<JS::DataView>(cx, otherGlobal))); + CHECK((TestType<JS::ArrayBuffer>(cx, otherGlobal))); + + return true; +} + +template <class APIType> +bool TestType(JSContext* cx, Handle<JSObject*> otherGlobal) { + JS::Rooted<JSObject*> obj(cx, CreateObj<APIType>(cx, 8)); + CHECK(obj); + + // Any of these should be creatable as an ArrayBufferOrView. + JS::Rooted<JS::ArrayBufferOrView> abov( + cx, JS::ArrayBufferOrView::fromObject(obj)); + CHECK(abov); + + // And that should allow unwrapping as well. + abov = JS::ArrayBufferOrView::unwrap(obj); + CHECK(abov); + + if constexpr (!std::is_same_v<APIType, JS::Uint16Array>) { + // Check that we can't make an API object of a different type. + JS::Rooted<JS::Uint16Array> nope(cx, JS::Uint16Array::unwrap(obj)); + CHECK(!nope); + + // And that we can't make an API object from an object of a different type. + JS::Rooted<JSObject*> u16array(cx, CreateObj<JS::Uint16Array>(cx, 10)); + CHECK(u16array); + auto deny = APIType::fromObject(u16array); + CHECK(!deny); + deny = APIType::unwrap(u16array); + CHECK(!deny); + } + + CHECK_EQUAL(abov.asObject(), obj); + + JS::Rooted<JSObject*> wrapped(cx); + { + AutoRealm ar(cx, otherGlobal); + wrapped = CreateObj<APIType>(cx, 8); // Not wrapped yet! + CHECK(wrapped); + } + CHECK(wrapped->compartment() == otherGlobal->compartment()); + CHECK(JS_WrapObject(cx, &wrapped)); // Now it's wrapped. + CHECK(wrapped->compartment() == global->compartment()); + + abov = JS::ArrayBufferOrView::fromObject(wrapped); + CHECK(!abov); + abov = JS::ArrayBufferOrView::unwrap(wrapped); + CHECK(abov); + + JS::Rooted<APIType> dummy(cx, APIType::fromObject(obj)); + CHECK(obj); + CHECK(dummy); + CHECK(dummy.asObject()); + + return true; +} + +END_TEST(testArrayBufferOrView_type) diff --git a/js/src/jsapi-tests/testArrayBufferView.cpp b/js/src/jsapi-tests/testArrayBufferView.cpp new file mode 100644 index 0000000000..c2f3687266 --- /dev/null +++ b/js/src/jsapi-tests/testArrayBufferView.cpp @@ -0,0 +1,162 @@ +/* -*- 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 "jsfriendapi.h" + +#include "js/ArrayBuffer.h" // JS::NewArrayBuffer +#include "js/experimental/TypedData.h" // JS_GetArrayBufferView{Type,ByteLength,Data}, JS_GetObjectAsArrayBufferView, JS_GetObjectAs{{Ui,I}nt{8,16,32},Float{32,64}}Array, JS_IsArrayBufferViewObject, JS_NewDataView, JS_New{{Ui,I}nt{8,16,32},Float{32,64},Uint8Clamped}Array +#include "js/GlobalObject.h" // JS_NewGlobalObject +#include "js/PropertyAndElement.h" // JS_SetProperty +#include "js/ScalarType.h" // js::Scalar::Type +#include "jsapi-tests/tests.h" +#include "vm/ProxyObject.h" +#include "vm/Realm.h" +#include "vm/Uint8Clamped.h" // js::uint8_clamped_t + +#include "vm/JSObject-inl.h" +#include "vm/Realm-inl.h" +#include "vm/TypedArrayObject-inl.h" // TypeIDOfType + +using namespace js; + +template <class ViewType> +static JSObject* Create(JSContext* cx, size_t len) { + return ViewType::create(cx, len).asObject(); +} + +template <> +JSObject* Create<JS::DataView>(JSContext* cx, size_t len) { + JS::Rooted<JSObject*> buffer(cx, JS::NewArrayBuffer(cx, len)); + if (!buffer) { + return nullptr; + } + return JS_NewDataView(cx, buffer, 0, len); +} + +template <class T> +struct InternalType { + using Type = uint8_t; +}; + +#define INT_TYPE(ExternalType, NativeType, Name) \ + template <> \ + struct InternalType<JS::TypedArray<js::Scalar::Name>> { \ + using Type = NativeType; \ + }; +JS_FOR_EACH_TYPED_ARRAY(INT_TYPE) +#undef INT_TYPE + +BEGIN_TEST(testArrayBufferView_type) { + CHECK((TestViewType<JS::TypedArray<Scalar::Uint8>, 7, 7>(cx))); + CHECK((TestViewType<JS::TypedArray<Scalar::Int8>, 33, 33>(cx))); + CHECK((TestViewType<JS::TypedArray<Scalar::Uint8Clamped>, 7, 7>(cx))); + CHECK((TestViewType<JS::TypedArray<Scalar::Uint16>, 3, 6>(cx))); + CHECK((TestViewType<JS::TypedArray<Scalar::Int16>, 17, 34>(cx))); + CHECK((TestViewType<JS::TypedArray<Scalar::Uint32>, 15, 60>(cx))); + CHECK((TestViewType<JS::TypedArray<Scalar::Int32>, 8, 32>(cx))); + CHECK((TestViewType<JS::TypedArray<Scalar::Float32>, 7, 28>(cx))); + CHECK((TestViewType<JS::TypedArray<Scalar::Float64>, 9, 72>(cx))); + CHECK((TestViewType<JS::DataView, 8, 8>(cx))); + + return true; +} + +template <class T> +struct ScalarTypeOf { + static constexpr js::Scalar::Type value = js::Scalar::MaxTypedArrayViewType; +}; + +template <js::Scalar::Type EType> +struct ScalarTypeOf<JS::TypedArray<EType>> { + static constexpr js::Scalar::Type value = EType; +}; +template <class ViewType, uint32_t ExpectedLength, uint32_t ExpectedByteLength> +bool TestViewType(JSContext* cx) { + JS::Rooted<JSObject*> obj(cx, Create<ViewType>(cx, ExpectedLength)); + CHECK(obj); + + CHECK(JS_IsArrayBufferViewObject(obj)); + + CHECK(JS_GetArrayBufferViewByteLength(obj) == ExpectedByteLength); + + { + JS::AutoCheckCannotGC nogc; + bool shared1; + JSObject* unwrapped = js::UnwrapArrayBufferView(obj); + uint8_t* data1 = + (uint8_t*)JS_GetArrayBufferViewData(unwrapped, &shared1, nogc); + + auto view = ViewType::unwrap(obj); + CHECK(JS_GetArrayBufferViewType(obj) == ScalarTypeOf<ViewType>::value); + + if (JS_IsTypedArrayObject(unwrapped)) { + CHECK(unwrapped->as<TypedArrayObject>().type() == + TypeIDOfType<typename InternalType<ViewType>::Type>::id); + } + + bool shared2; + mozilla::Span<typename ViewType::DataType> span2 = + view.getData(&shared2, nogc); + CHECK(obj == view.asObject()); + CHECK(data1 == reinterpret_cast<uint8_t*>(span2.data())); + CHECK(shared1 == shared2); + CHECK(span2.Length() == ExpectedLength); + + JS::Heap<ViewType> hv(view); + + bool shared3; + mozilla::Span<typename ViewType::DataType> span3 = + hv.getData(&shared3, nogc); + CHECK(obj == hv.asObject()); + CHECK(data1 == reinterpret_cast<uint8_t*>(span3.data())); + CHECK(shared1 == shared3); + CHECK(span3.Length() == ExpectedLength); + } + + JS::RealmOptions options; + JS::RootedObject otherGlobal( + cx, JS_NewGlobalObject(cx, basicGlobalClass(), nullptr, + JS::DontFireOnNewGlobalHook, options)); + CHECK(otherGlobal); + + JS::Rooted<JSObject*> buffer(cx); + { + AutoRealm ar(cx, otherGlobal); + buffer = JS::NewArrayBuffer(cx, 8); + CHECK(buffer); + CHECK(buffer->as<ArrayBufferObject>().byteLength() == 8); + } + CHECK(buffer->compartment() == otherGlobal->compartment()); + CHECK(JS_WrapObject(cx, &buffer)); + CHECK(buffer->compartment() == global->compartment()); + + JS::Rooted<JSObject*> dataview(cx, JS_NewDataView(cx, buffer, 4, 4)); + CHECK(dataview); + CHECK(dataview->is<ProxyObject>()); + + JS::Rooted<JS::Value> val(cx); + + val = ObjectValue(*dataview); + CHECK(JS_SetProperty(cx, global, "view", val)); + + EVAL("view.buffer", &val); + CHECK(val.toObject().is<ProxyObject>()); + + CHECK(dataview->compartment() == global->compartment()); + JS::Rooted<JSObject*> otherView(cx, js::UncheckedUnwrap(dataview)); + CHECK(otherView->compartment() == otherGlobal->compartment()); + JS::Rooted<JSObject*> otherBuffer(cx, js::UncheckedUnwrap(&val.toObject())); + CHECK(otherBuffer->compartment() == otherGlobal->compartment()); + + EVAL("Object.getPrototypeOf(view) === DataView.prototype", &val); + CHECK(val.toBoolean() == true); + + return true; +} + +END_TEST(testArrayBufferView_type) diff --git a/js/src/jsapi-tests/testArrayBufferWithUserOwnedContents.cpp b/js/src/jsapi-tests/testArrayBufferWithUserOwnedContents.cpp new file mode 100644 index 0000000000..9a360e38be --- /dev/null +++ b/js/src/jsapi-tests/testArrayBufferWithUserOwnedContents.cpp @@ -0,0 +1,54 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + */ + +#include <stdint.h> // uint32_t + +#include "js/ArrayBuffer.h" // JS::{DetachArrayBuffer,GetArrayBuffer{ByteLength,Data},IsArrayBufferObject,NewArrayBufferWithUserOwnedContents} +#include "js/GCAPI.h" // JS::AutoCheckCannotGC, JS_GC +#include "js/RootingAPI.h" // JS::Rooted +#include "jsapi-tests/tests.h" +#include "vm/ArrayBufferObject.h" + +char testData[] = + "1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + +constexpr size_t testDataLength = sizeof(testData); + +static void GC(JSContext* cx) { + JS_GC(cx); + // Trigger another to wait for background finalization to end. + JS_GC(cx); +} + +BEGIN_TEST(testArrayBufferWithUserOwnedContents) { + JS::Rooted<JSObject*> obj(cx, JS::NewArrayBufferWithUserOwnedContents( + cx, testDataLength, testData)); + GC(cx); + CHECK(VerifyObject(obj, testDataLength)); + GC(cx); + JS::DetachArrayBuffer(cx, obj); + GC(cx); + CHECK(VerifyObject(obj, 0)); + + return true; +} + +bool VerifyObject(JS::HandleObject obj, uint32_t length) { + JS::AutoCheckCannotGC nogc; + + CHECK(obj); + CHECK(JS::IsArrayBufferObject(obj)); + CHECK_EQUAL(JS::GetArrayBufferByteLength(obj), length); + bool sharedDummy; + const char* data = reinterpret_cast<const char*>( + JS::GetArrayBufferData(obj, &sharedDummy, nogc)); + if (length == testDataLength) { + CHECK(data); + CHECK(testData == data); + } + + return true; +} + +END_TEST(testArrayBufferWithUserOwnedContents) diff --git a/js/src/jsapi-tests/testAssemblerBuffer.cpp b/js/src/jsapi-tests/testAssemblerBuffer.cpp new file mode 100644 index 0000000000..50fc5b6043 --- /dev/null +++ b/js/src/jsapi-tests/testAssemblerBuffer.cpp @@ -0,0 +1,587 @@ +/* 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 <stdlib.h> + +#include "jit/shared/IonAssemblerBufferWithConstantPools.h" +#include "jsapi-tests/tests.h" + +// Tests for classes in: +// +// jit/shared/IonAssemblerBuffer.h +// jit/shared/IonAssemblerBufferWithConstantPools.h +// +// Classes in js::jit tested: +// +// BufferOffset +// BufferSlice (implicitly) +// AssemblerBuffer +// +// BranchDeadlineSet +// Pool (implicitly) +// AssemblerBufferWithConstantPools +// + +BEGIN_TEST(testAssemblerBuffer_BufferOffset) { + using js::jit::BufferOffset; + + BufferOffset off1; + BufferOffset off2(10); + + CHECK(!off1.assigned()); + CHECK(off2.assigned()); + CHECK_EQUAL(off2.getOffset(), 10); + off1 = off2; + CHECK(off1.assigned()); + CHECK_EQUAL(off1.getOffset(), 10); + + return true; +} +END_TEST(testAssemblerBuffer_BufferOffset) + +BEGIN_TEST(testAssemblerBuffer_AssemblerBuffer) { + using js::jit::BufferOffset; + typedef js::jit::AssemblerBuffer<5 * sizeof(uint32_t), uint32_t> AsmBuf; + + AsmBuf ab; + CHECK(ab.isAligned(16)); + CHECK_EQUAL(ab.size(), 0u); + CHECK_EQUAL(ab.nextOffset().getOffset(), 0); + CHECK(!ab.oom()); + + BufferOffset off1 = ab.putInt(1000017); + CHECK_EQUAL(off1.getOffset(), 0); + CHECK_EQUAL(ab.size(), 4u); + CHECK_EQUAL(ab.nextOffset().getOffset(), 4); + CHECK(!ab.isAligned(16)); + CHECK(ab.isAligned(4)); + CHECK(ab.isAligned(1)); + CHECK_EQUAL(*ab.getInst(off1), 1000017u); + + BufferOffset off2 = ab.putInt(1000018); + CHECK_EQUAL(off2.getOffset(), 4); + + BufferOffset off3 = ab.putInt(1000019); + CHECK_EQUAL(off3.getOffset(), 8); + + BufferOffset off4 = ab.putInt(1000020); + CHECK_EQUAL(off4.getOffset(), 12); + CHECK_EQUAL(ab.size(), 16u); + CHECK_EQUAL(ab.nextOffset().getOffset(), 16); + + // Last one in the slice. + BufferOffset off5 = ab.putInt(1000021); + CHECK_EQUAL(off5.getOffset(), 16); + CHECK_EQUAL(ab.size(), 20u); + CHECK_EQUAL(ab.nextOffset().getOffset(), 20); + + BufferOffset off6 = ab.putInt(1000022); + CHECK_EQUAL(off6.getOffset(), 20); + CHECK_EQUAL(ab.size(), 24u); + CHECK_EQUAL(ab.nextOffset().getOffset(), 24); + + // Reference previous slice. Excercise the finger. + CHECK_EQUAL(*ab.getInst(off1), 1000017u); + CHECK_EQUAL(*ab.getInst(off6), 1000022u); + CHECK_EQUAL(*ab.getInst(off1), 1000017u); + CHECK_EQUAL(*ab.getInst(off5), 1000021u); + + // Too much data for one slice. + const uint32_t fixdata[] = {2000036, 2000037, 2000038, + 2000039, 2000040, 2000041}; + + // Split payload across multiple slices. + CHECK_EQUAL(ab.nextOffset().getOffset(), 24); + BufferOffset good1 = ab.putBytesLarge(sizeof(fixdata), fixdata); + CHECK_EQUAL(good1.getOffset(), 24); + CHECK_EQUAL(ab.nextOffset().getOffset(), 48); + CHECK_EQUAL(*ab.getInst(good1), 2000036u); + CHECK_EQUAL(*ab.getInst(BufferOffset(32)), 2000038u); + CHECK_EQUAL(*ab.getInst(BufferOffset(36)), 2000039u); + CHECK_EQUAL(*ab.getInst(BufferOffset(40)), 2000040u); + CHECK_EQUAL(*ab.getInst(BufferOffset(44)), 2000041u); + + return true; +} +END_TEST(testAssemblerBuffer_AssemblerBuffer) + +BEGIN_TEST(testAssemblerBuffer_BranchDeadlineSet) { + typedef js::jit::BranchDeadlineSet<3> DLSet; + using js::jit::BufferOffset; + + js::LifoAlloc alloc(1024); + DLSet dls(alloc); + + CHECK(dls.empty()); + CHECK(alloc.isEmpty()); // Constructor must be infallible. + CHECK_EQUAL(dls.size(), 0u); + CHECK_EQUAL(dls.maxRangeSize(), 0u); + + // Removing non-existant deadline is OK. + dls.removeDeadline(1, BufferOffset(7)); + + // Add deadlines in increasing order as intended. This is optimal. + dls.addDeadline(1, BufferOffset(10)); + CHECK(!dls.empty()); + CHECK_EQUAL(dls.size(), 1u); + CHECK_EQUAL(dls.maxRangeSize(), 1u); + CHECK_EQUAL(dls.earliestDeadline().getOffset(), 10); + CHECK_EQUAL(dls.earliestDeadlineRange(), 1u); + + // Removing non-existant deadline is OK. + dls.removeDeadline(1, BufferOffset(7)); + dls.removeDeadline(1, BufferOffset(17)); + dls.removeDeadline(0, BufferOffset(10)); + CHECK_EQUAL(dls.size(), 1u); + CHECK_EQUAL(dls.maxRangeSize(), 1u); + + // Two identical deadlines for different ranges. + dls.addDeadline(2, BufferOffset(10)); + CHECK(!dls.empty()); + CHECK_EQUAL(dls.size(), 2u); + CHECK_EQUAL(dls.maxRangeSize(), 1u); + CHECK_EQUAL(dls.earliestDeadline().getOffset(), 10); + + // It doesn't matter which range earliestDeadlineRange() reports first, + // but it must report both. + if (dls.earliestDeadlineRange() == 1) { + dls.removeDeadline(1, BufferOffset(10)); + CHECK_EQUAL(dls.earliestDeadline().getOffset(), 10); + CHECK_EQUAL(dls.earliestDeadlineRange(), 2u); + } else { + CHECK_EQUAL(dls.earliestDeadlineRange(), 2u); + dls.removeDeadline(2, BufferOffset(10)); + CHECK_EQUAL(dls.earliestDeadline().getOffset(), 10); + CHECK_EQUAL(dls.earliestDeadlineRange(), 1u); + } + + // Add deadline which is the front of range 0, but not the global earliest. + dls.addDeadline(0, BufferOffset(20)); + CHECK_EQUAL(dls.earliestDeadline().getOffset(), 10); + CHECK(dls.earliestDeadlineRange() > 0); + + // Non-optimal add to front of single-entry range 0. + dls.addDeadline(0, BufferOffset(15)); + CHECK_EQUAL(dls.earliestDeadline().getOffset(), 10); + CHECK(dls.earliestDeadlineRange() > 0); + + // Append to 2-entry range 0. + dls.addDeadline(0, BufferOffset(30)); + CHECK_EQUAL(dls.earliestDeadline().getOffset(), 10); + CHECK(dls.earliestDeadlineRange() > 0); + + // Add penultimate entry. + dls.addDeadline(0, BufferOffset(25)); + CHECK_EQUAL(dls.earliestDeadline().getOffset(), 10); + CHECK(dls.earliestDeadlineRange() > 0); + + // Prepend, stealing earliest from other range. + dls.addDeadline(0, BufferOffset(5)); + CHECK_EQUAL(dls.earliestDeadline().getOffset(), 5); + CHECK_EQUAL(dls.earliestDeadlineRange(), 0u); + + // Remove central element. + dls.removeDeadline(0, BufferOffset(20)); + CHECK_EQUAL(dls.earliestDeadline().getOffset(), 5); + CHECK_EQUAL(dls.earliestDeadlineRange(), 0u); + + // Remove front, giving back the lead. + dls.removeDeadline(0, BufferOffset(5)); + CHECK_EQUAL(dls.earliestDeadline().getOffset(), 10); + CHECK(dls.earliestDeadlineRange() > 0); + + // Remove front, giving back earliest to range 0. + dls.removeDeadline(dls.earliestDeadlineRange(), BufferOffset(10)); + CHECK_EQUAL(dls.earliestDeadline().getOffset(), 15); + CHECK_EQUAL(dls.earliestDeadlineRange(), 0u); + + // Remove tail. + dls.removeDeadline(0, BufferOffset(30)); + CHECK_EQUAL(dls.earliestDeadline().getOffset(), 15); + CHECK_EQUAL(dls.earliestDeadlineRange(), 0u); + + // Now range 0 = [15, 25]. + CHECK_EQUAL(dls.size(), 2u); + dls.removeDeadline(0, BufferOffset(25)); + dls.removeDeadline(0, BufferOffset(15)); + CHECK(dls.empty()); + + return true; +} +END_TEST(testAssemblerBuffer_BranchDeadlineSet) + +// Mock Assembler class for testing the AssemblerBufferWithConstantPools +// callbacks. +namespace { + +struct TestAssembler; + +typedef js::jit::AssemblerBufferWithConstantPools< + /* SliceSize */ 5 * sizeof(uint32_t), + /* InstSize */ 4, + /* Inst */ uint32_t, + /* Asm */ TestAssembler, + /* NumShortBranchRanges */ 3> + AsmBufWithPool; + +struct TestAssembler { + // Mock instruction set: + // + // 0x1111xxxx - align filler instructions. + // 0x2222xxxx - manually inserted 'arith' instructions. + // 0xaaaaxxxx - noop filler instruction. + // 0xb0bbxxxx - branch xxxx bytes forward. (Pool guard). + // 0xb1bbxxxx - branch xxxx bytes forward. (Short-range branch). + // 0xb2bbxxxx - branch xxxx bytes forward. (Veneer branch). + // 0xb3bbxxxx - branch xxxx bytes forward. (Patched short-range branch). + // 0xc0ccxxxx - constant pool load (uninitialized). + // 0xc1ccxxxx - constant pool load to index xxxx. + // 0xc2ccxxxx - constant pool load xxxx bytes ahead. + // 0xffffxxxx - pool header with xxxx bytes. + + static const unsigned BranchRange = 36; + + static void InsertIndexIntoTag(uint8_t* load_, uint32_t index) { + uint32_t* load = reinterpret_cast<uint32_t*>(load_); + MOZ_ASSERT(*load == 0xc0cc0000, + "Expected uninitialized constant pool load"); + MOZ_ASSERT(index < 0x10000); + *load = 0xc1cc0000 + index; + } + + static void PatchConstantPoolLoad(void* loadAddr, void* constPoolAddr) { + uint32_t* load = reinterpret_cast<uint32_t*>(loadAddr); + uint32_t index = *load & 0xffff; + MOZ_ASSERT(*load == (0xc1cc0000 | index), + "Expected constant pool load(index)"); + ptrdiff_t offset = reinterpret_cast<uint8_t*>(constPoolAddr) - + reinterpret_cast<uint8_t*>(loadAddr); + offset += index * 4; + MOZ_ASSERT(offset % 4 == 0, "Unaligned constant pool"); + MOZ_ASSERT(offset > 0 && offset < 0x10000, "Pool out of range"); + *load = 0xc2cc0000 + offset; + } + + static void WritePoolGuard(js::jit::BufferOffset branch, uint32_t* dest, + js::jit::BufferOffset afterPool) { + MOZ_ASSERT(branch.assigned()); + MOZ_ASSERT(afterPool.assigned()); + size_t branchOff = branch.getOffset(); + size_t afterPoolOff = afterPool.getOffset(); + MOZ_ASSERT(afterPoolOff > branchOff); + uint32_t delta = afterPoolOff - branchOff; + *dest = 0xb0bb0000 + delta; + } + + static void WritePoolHeader(void* start, js::jit::Pool* p, bool isNatural) { + MOZ_ASSERT(!isNatural, "Natural pool guards not implemented."); + uint32_t* hdr = reinterpret_cast<uint32_t*>(start); + *hdr = 0xffff0000 + p->getPoolSize(); + } + + static void PatchShortRangeBranchToVeneer(AsmBufWithPool* buffer, + unsigned rangeIdx, + js::jit::BufferOffset deadline, + js::jit::BufferOffset veneer) { + size_t branchOff = deadline.getOffset() - BranchRange; + size_t veneerOff = veneer.getOffset(); + uint32_t* branch = buffer->getInst(js::jit::BufferOffset(branchOff)); + + MOZ_ASSERT((*branch & 0xffff0000) == 0xb1bb0000, + "Expected short-range branch instruction"); + // Copy branch offset to veneer. A real instruction set would require + // some adjustment of the label linked-list. + *buffer->getInst(veneer) = 0xb2bb0000 | (*branch & 0xffff); + MOZ_ASSERT(veneerOff > branchOff, "Veneer should follow branch"); + *branch = 0xb3bb0000 + (veneerOff - branchOff); + } +}; +} // namespace + +BEGIN_TEST(testAssemblerBuffer_AssemblerBufferWithConstantPools) { + using js::jit::BufferOffset; + + AsmBufWithPool ab(/* guardSize= */ 1, + /* headerSize= */ 1, + /* instBufferAlign(unused)= */ 0, + /* poolMaxOffset= */ 17, + /* pcBias= */ 0, + /* alignFillInst= */ 0x11110000, + /* nopFillInst= */ 0xaaaa0000, + /* nopFill= */ 0); + + CHECK(ab.isAligned(16)); + CHECK_EQUAL(ab.size(), 0u); + CHECK_EQUAL(ab.nextOffset().getOffset(), 0); + CHECK(!ab.oom()); + + // Each slice holds 5 instructions. Trigger a constant pool inside the slice. + uint32_t poolLoad[] = {0xc0cc0000}; + uint32_t poolData[] = {0xdddd0000, 0xdddd0001, 0xdddd0002, 0xdddd0003}; + AsmBufWithPool::PoolEntry pe; + BufferOffset load = + ab.allocEntry(1, 1, (uint8_t*)poolLoad, (uint8_t*)poolData, &pe); + CHECK_EQUAL(pe.index(), 0u); + CHECK_EQUAL(load.getOffset(), 0); + + // Pool hasn't been emitted yet. Load has been patched by + // InsertIndexIntoTag. + CHECK_EQUAL(*ab.getInst(load), 0xc1cc0000); + + // Expected layout: + // + // 0: load [pc+16] + // 4: 0x22220001 + // 8: guard branch pc+12 + // 12: pool header + // 16: poolData + // 20: 0x22220002 + // + ab.putInt(0x22220001); + // One could argue that the pool should be flushed here since there is no + // more room. However, the current implementation doesn't dump pool until + // asked to add data: + ab.putInt(0x22220002); + + CHECK_EQUAL(*ab.getInst(BufferOffset(0)), 0xc2cc0010u); + CHECK_EQUAL(*ab.getInst(BufferOffset(4)), 0x22220001u); + CHECK_EQUAL(*ab.getInst(BufferOffset(8)), 0xb0bb000cu); + CHECK_EQUAL(*ab.getInst(BufferOffset(12)), 0xffff0004u); + CHECK_EQUAL(*ab.getInst(BufferOffset(16)), 0xdddd0000u); + CHECK_EQUAL(*ab.getInst(BufferOffset(20)), 0x22220002u); + + // allocEntry() overwrites the load instruction! Restore the original. + poolLoad[0] = 0xc0cc0000; + + // Now try with load and pool data on separate slices. + load = ab.allocEntry(1, 1, (uint8_t*)poolLoad, (uint8_t*)poolData, &pe); + CHECK_EQUAL(pe.index(), 1u); // Global pool entry index. + CHECK_EQUAL(load.getOffset(), 24); + CHECK_EQUAL(*ab.getInst(load), 0xc1cc0000); // Index into current pool. + ab.putInt(0x22220001); + ab.putInt(0x22220002); + CHECK_EQUAL(*ab.getInst(BufferOffset(24)), 0xc2cc0010u); + CHECK_EQUAL(*ab.getInst(BufferOffset(28)), 0x22220001u); + CHECK_EQUAL(*ab.getInst(BufferOffset(32)), 0xb0bb000cu); + CHECK_EQUAL(*ab.getInst(BufferOffset(36)), 0xffff0004u); + CHECK_EQUAL(*ab.getInst(BufferOffset(40)), 0xdddd0000u); + CHECK_EQUAL(*ab.getInst(BufferOffset(44)), 0x22220002u); + + // Two adjacent loads to the same pool. + poolLoad[0] = 0xc0cc0000; + load = ab.allocEntry(1, 1, (uint8_t*)poolLoad, (uint8_t*)poolData, &pe); + CHECK_EQUAL(pe.index(), 2u); // Global pool entry index. + CHECK_EQUAL(load.getOffset(), 48); + CHECK_EQUAL(*ab.getInst(load), 0xc1cc0000); // Index into current pool. + + poolLoad[0] = 0xc0cc0000; + load = ab.allocEntry(1, 1, (uint8_t*)poolLoad, (uint8_t*)(poolData + 1), &pe); + CHECK_EQUAL(pe.index(), 3u); // Global pool entry index. + CHECK_EQUAL(load.getOffset(), 52); + CHECK_EQUAL(*ab.getInst(load), 0xc1cc0001); // Index into current pool. + + ab.putInt(0x22220005); + + CHECK_EQUAL(*ab.getInst(BufferOffset(48)), 0xc2cc0010u); // load pc+16. + CHECK_EQUAL(*ab.getInst(BufferOffset(52)), 0xc2cc0010u); // load pc+16. + CHECK_EQUAL(*ab.getInst(BufferOffset(56)), + 0xb0bb0010u); // guard branch pc+16. + CHECK_EQUAL(*ab.getInst(BufferOffset(60)), 0xffff0008u); // header 8 bytes. + CHECK_EQUAL(*ab.getInst(BufferOffset(64)), 0xdddd0000u); // datum 1. + CHECK_EQUAL(*ab.getInst(BufferOffset(68)), 0xdddd0001u); // datum 2. + CHECK_EQUAL(*ab.getInst(BufferOffset(72)), + 0x22220005u); // putInt(0x22220005) + + // Two loads as above, but the first load has an 8-byte pool entry, and the + // second load wouldn't be able to reach its data. This must produce two + // pools. + poolLoad[0] = 0xc0cc0000; + load = ab.allocEntry(1, 2, (uint8_t*)poolLoad, (uint8_t*)(poolData + 2), &pe); + CHECK_EQUAL(pe.index(), 4u); // Global pool entry index. + CHECK_EQUAL(load.getOffset(), 76); + CHECK_EQUAL(*ab.getInst(load), 0xc1cc0000); // Index into current pool. + + poolLoad[0] = 0xc0cc0000; + load = ab.allocEntry(1, 1, (uint8_t*)poolLoad, (uint8_t*)poolData, &pe); + CHECK_EQUAL(pe.index(), + 6u); // Global pool entry index. (Prev one is two indexes). + CHECK_EQUAL(load.getOffset(), 96); + CHECK_EQUAL(*ab.getInst(load), 0xc1cc0000); // Index into current pool. + + CHECK_EQUAL(*ab.getInst(BufferOffset(76)), 0xc2cc000cu); // load pc+12. + CHECK_EQUAL(*ab.getInst(BufferOffset(80)), + 0xb0bb0010u); // guard branch pc+16. + CHECK_EQUAL(*ab.getInst(BufferOffset(84)), 0xffff0008u); // header 8 bytes. + CHECK_EQUAL(*ab.getInst(BufferOffset(88)), 0xdddd0002u); // datum 1. + CHECK_EQUAL(*ab.getInst(BufferOffset(92)), 0xdddd0003u); // datum 2. + + // Second pool is not flushed yet, and there is room for one instruction + // after the load. Test the keep-together feature. + ab.enterNoPool(2); + ab.putInt(0x22220006); + ab.putInt(0x22220007); + ab.leaveNoPool(); + + CHECK_EQUAL(*ab.getInst(BufferOffset(96)), 0xc2cc000cu); // load pc+16. + CHECK_EQUAL(*ab.getInst(BufferOffset(100)), + 0xb0bb000cu); // guard branch pc+12. + CHECK_EQUAL(*ab.getInst(BufferOffset(104)), 0xffff0004u); // header 4 bytes. + CHECK_EQUAL(*ab.getInst(BufferOffset(108)), 0xdddd0000u); // datum 1. + CHECK_EQUAL(*ab.getInst(BufferOffset(112)), 0x22220006u); + CHECK_EQUAL(*ab.getInst(BufferOffset(116)), 0x22220007u); + + return true; +} +END_TEST(testAssemblerBuffer_AssemblerBufferWithConstantPools) + +BEGIN_TEST(testAssemblerBuffer_AssemblerBufferWithConstantPools_ShortBranch) { + using js::jit::BufferOffset; + + AsmBufWithPool ab(/* guardSize= */ 1, + /* headerSize= */ 1, + /* instBufferAlign(unused)= */ 0, + /* poolMaxOffset= */ 17, + /* pcBias= */ 0, + /* alignFillInst= */ 0x11110000, + /* nopFillInst= */ 0xaaaa0000, + /* nopFill= */ 0); + + // Insert short-range branch. + BufferOffset br1 = ab.putInt(0xb1bb00cc); + ab.registerBranchDeadline( + 1, BufferOffset(br1.getOffset() + TestAssembler::BranchRange)); + ab.putInt(0x22220001); + BufferOffset off = ab.putInt(0x22220002); + ab.registerBranchDeadline( + 1, BufferOffset(off.getOffset() + TestAssembler::BranchRange)); + ab.putInt(0x22220003); + ab.putInt(0x22220004); + + // Second short-range branch that will be swiped up by hysteresis. + BufferOffset br2 = ab.putInt(0xb1bb0d2d); + ab.registerBranchDeadline( + 1, BufferOffset(br2.getOffset() + TestAssembler::BranchRange)); + + // Branch should not have been patched yet here. + CHECK_EQUAL(*ab.getInst(br1), 0xb1bb00cc); + CHECK_EQUAL(*ab.getInst(br2), 0xb1bb0d2d); + + // Cancel one of the pending branches. + // This is what will happen to most branches as they are bound before + // expiring by Assembler::bind(). + ab.unregisterBranchDeadline( + 1, BufferOffset(off.getOffset() + TestAssembler::BranchRange)); + + off = ab.putInt(0x22220006); + // Here we may or may not have patched the branch yet, but it is inevitable + // now: + // + // 0: br1 pc+36 + // 4: 0x22220001 + // 8: 0x22220002 (unpatched) + // 12: 0x22220003 + // 16: 0x22220004 + // 20: br2 pc+20 + // 24: 0x22220006 + CHECK_EQUAL(off.getOffset(), 24); + // 28: guard branch pc+16 + // 32: pool header + // 36: veneer1 + // 40: veneer2 + // 44: 0x22220007 + + off = ab.putInt(0x22220007); + CHECK_EQUAL(off.getOffset(), 44); + + // Now the branch must have been patched. + CHECK_EQUAL(*ab.getInst(br1), 0xb3bb0000 + 36); // br1 pc+36 (patched) + CHECK_EQUAL(*ab.getInst(BufferOffset(8)), + 0x22220002u); // 0x22220002 (unpatched) + CHECK_EQUAL(*ab.getInst(br2), 0xb3bb0000 + 20); // br2 pc+20 (patched) + CHECK_EQUAL(*ab.getInst(BufferOffset(28)), 0xb0bb0010u); // br pc+16 (guard) + CHECK_EQUAL(*ab.getInst(BufferOffset(32)), + 0xffff0000u); // pool header 0 bytes. + CHECK_EQUAL(*ab.getInst(BufferOffset(36)), + 0xb2bb00ccu); // veneer1 w/ original 'cc' offset. + CHECK_EQUAL(*ab.getInst(BufferOffset(40)), + 0xb2bb0d2du); // veneer2 w/ original 'd2d' offset. + CHECK_EQUAL(*ab.getInst(BufferOffset(44)), 0x22220007u); + + return true; +} +END_TEST(testAssemblerBuffer_AssemblerBufferWithConstantPools_ShortBranch) + +// Test that everything is put together correctly in the ARM64 assembler. +#if defined(JS_CODEGEN_ARM64) + +# include "jit/MacroAssembler-inl.h" + +BEGIN_TEST(testAssemblerBuffer_ARM64) { + using namespace js::jit; + + js::LifoAlloc lifo(4096); + TempAllocator alloc(&lifo); + JitContext jc(cx); + StackMacroAssembler masm(cx, alloc); + AutoCreatedBy acb(masm, __func__); + + // Branches to an unbound label. + Label lab1; + masm.branch(Assembler::Equal, &lab1); + masm.branch(Assembler::LessThan, &lab1); + masm.bind(&lab1); + masm.branch(Assembler::Equal, &lab1); + + CHECK_EQUAL(masm.getInstructionAt(BufferOffset(0))->InstructionBits(), + vixl::B_cond | vixl::Assembler::ImmCondBranch(2) | vixl::eq); + CHECK_EQUAL(masm.getInstructionAt(BufferOffset(4))->InstructionBits(), + vixl::B_cond | vixl::Assembler::ImmCondBranch(1) | vixl::lt); + CHECK_EQUAL(masm.getInstructionAt(BufferOffset(8))->InstructionBits(), + vixl::B_cond | vixl::Assembler::ImmCondBranch(0) | vixl::eq); + + // Branches can reach the label, but the linked list of uses needs to be + // rearranged. The final conditional branch cannot reach the first branch. + Label lab2a; + Label lab2b; + masm.bind(&lab2a); + masm.B(&lab2b); + // Generate 1,100,000 bytes of NOPs. + for (unsigned n = 0; n < 1100000; n += 4) { + masm.Nop(); + } + masm.branch(Assembler::LessThan, &lab2b); + masm.bind(&lab2b); + CHECK_EQUAL( + masm.getInstructionAt(BufferOffset(lab2a.offset()))->InstructionBits(), + vixl::B | vixl::Assembler::ImmUncondBranch(1100000 / 4 + 2)); + CHECK_EQUAL(masm.getInstructionAt(BufferOffset(lab2b.offset() - 4)) + ->InstructionBits(), + vixl::B_cond | vixl::Assembler::ImmCondBranch(1) | vixl::lt); + + // Generate a conditional branch that can't reach its label. + Label lab3a; + Label lab3b; + masm.bind(&lab3a); + masm.branch(Assembler::LessThan, &lab3b); + for (unsigned n = 0; n < 1100000; n += 4) { + masm.Nop(); + } + masm.bind(&lab3b); + masm.B(&lab3a); + Instruction* bcond3 = masm.getInstructionAt(BufferOffset(lab3a.offset())); + CHECK_EQUAL(bcond3->BranchType(), vixl::CondBranchType); + ptrdiff_t delta = bcond3->ImmPCRawOffset() * 4; + Instruction* veneer = + masm.getInstructionAt(BufferOffset(lab3a.offset() + delta)); + CHECK_EQUAL(veneer->BranchType(), vixl::UncondBranchType); + delta += veneer->ImmPCRawOffset() * 4; + CHECK_EQUAL(delta, lab3b.offset() - lab3a.offset()); + Instruction* b3 = masm.getInstructionAt(BufferOffset(lab3b.offset())); + CHECK_EQUAL(b3->BranchType(), vixl::UncondBranchType); + CHECK_EQUAL(4 * b3->ImmPCRawOffset(), -delta); + + return true; +} +END_TEST(testAssemblerBuffer_ARM64) +#endif /* JS_CODEGEN_ARM64 */ diff --git a/js/src/jsapi-tests/testAtomicOperations.cpp b/js/src/jsapi-tests/testAtomicOperations.cpp new file mode 100644 index 0000000000..1a59fec08f --- /dev/null +++ b/js/src/jsapi-tests/testAtomicOperations.cpp @@ -0,0 +1,304 @@ +/* -*- 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 "mozilla/Alignment.h" +#include "mozilla/Assertions.h" + +#include "jit/AtomicOperations.h" +#include "jsapi-tests/tests.h" +#include "vm/ArrayBufferObject.h" +#include "vm/SharedMem.h" +#include "vm/Uint8Clamped.h" +#include "wasm/WasmFeatures.h" + +using namespace js; + +// Machinery to disguise pointer addresses to the C++ compiler -- quite possibly +// not thread-safe. + +extern void setHiddenPointer(void* p); +extern void* getHiddenPointer(); + +void* hidePointerValue(void* p) { + setHiddenPointer(p); + return getHiddenPointer(); +} + +////////////////////////////////////////////////////////////////////// +// +// Lock-freedom predicates + +BEGIN_REUSABLE_TEST(testAtomicLockFree8) { + // isLockfree8() must not return true if there are no 8-byte atomics + + CHECK(!jit::AtomicOperations::isLockfree8() || + jit::AtomicOperations::hasAtomic8()); + + // We must have lock-free 8-byte atomics on every platform where we support + // wasm, but we don't care otherwise. + + CHECK(!wasm::HasSupport(cx) || jit::AtomicOperations::isLockfree8()); + return true; +} +END_TEST(testAtomicLockFree8) + +// The JS spec requires specific behavior for all but 1 and 2. + +BEGIN_REUSABLE_TEST(testAtomicLockFreeJS) { + static_assert(jit::AtomicOperations::isLockfreeJS(1) == + true); // false is allowed by spec but not in SpiderMonkey + static_assert(jit::AtomicOperations::isLockfreeJS(2) == true); // ditto + static_assert(jit::AtomicOperations::isLockfreeJS(8) == true); // ditto + static_assert(jit::AtomicOperations::isLockfreeJS(3) == false); // required + static_assert(jit::AtomicOperations::isLockfreeJS(4) == true); // required + static_assert(jit::AtomicOperations::isLockfreeJS(5) == false); // required + static_assert(jit::AtomicOperations::isLockfreeJS(6) == false); // required + static_assert(jit::AtomicOperations::isLockfreeJS(7) == false); // required + return true; +} +END_TEST(testAtomicLockFreeJS) + +////////////////////////////////////////////////////////////////////// +// +// Fence + +// This only tests that fenceSeqCst is defined and that it doesn't crash if we +// call it, but it has no return value and its effect is not observable here. + +BEGIN_REUSABLE_TEST(testAtomicFence) { + jit::AtomicOperations::fenceSeqCst(); + return true; +} +END_TEST(testAtomicFence) + +////////////////////////////////////////////////////////////////////// +// +// Memory access primitives + +// These tests for the atomic load and store primitives ascertain that the +// primitives are defined and that they load and store the values they should, +// but not that the primitives are actually atomic wrt to the memory subsystem. + +// Memory for testing atomics. This must be aligned to the natural alignment of +// the type we're testing; for now, use 8-byte alignment for all. + +MOZ_ALIGNED_DECL(8, static uint8_t atomicMem[8]); +MOZ_ALIGNED_DECL(8, static uint8_t atomicMem2[8]); + +// T is the primitive type we're testing, and A and B are references to constant +// bindings holding values of that type. +// +// No bytes of A and B should be 0 or FF. A+B and A-B must not overflow. + +#define ATOMIC_TESTS(T, A, B) \ + T* q = (T*)hidePointerValue((void*)atomicMem); \ + *q = A; \ + SharedMem<T*> p = \ + SharedMem<T*>::shared((T*)hidePointerValue((T*)atomicMem)); \ + CHECK(*q == A); \ + CHECK(jit::AtomicOperations::loadSeqCst(p) == A); \ + CHECK(*q == A); \ + jit::AtomicOperations::storeSeqCst(p, B); \ + CHECK(*q == B); \ + CHECK(jit::AtomicOperations::exchangeSeqCst(p, A) == B); \ + CHECK(*q == A); \ + CHECK(jit::AtomicOperations::compareExchangeSeqCst(p, (T)0, (T)1) == \ + A); /*failure*/ \ + CHECK(*q == A); \ + CHECK(jit::AtomicOperations::compareExchangeSeqCst(p, A, B) == \ + A); /*success*/ \ + CHECK(*q == B); \ + *q = A; \ + CHECK(jit::AtomicOperations::fetchAddSeqCst(p, B) == A); \ + CHECK(*q == A + B); \ + *q = A; \ + CHECK(jit::AtomicOperations::fetchSubSeqCst(p, B) == A); \ + CHECK(*q == A - B); \ + *q = A; \ + CHECK(jit::AtomicOperations::fetchAndSeqCst(p, B) == A); \ + CHECK(*q == (A & B)); \ + *q = A; \ + CHECK(jit::AtomicOperations::fetchOrSeqCst(p, B) == A); \ + CHECK(*q == (A | B)); \ + *q = A; \ + CHECK(jit::AtomicOperations::fetchXorSeqCst(p, B) == A); \ + CHECK(*q == (A ^ B)); \ + *q = A; \ + CHECK(jit::AtomicOperations::loadSafeWhenRacy(p) == A); \ + jit::AtomicOperations::storeSafeWhenRacy(p, B); \ + CHECK(*q == B); \ + T* q2 = (T*)hidePointerValue((void*)atomicMem2); \ + SharedMem<T*> p2 = \ + SharedMem<T*>::shared((T*)hidePointerValue((void*)atomicMem2)); \ + *q = A; \ + *q2 = B; \ + jit::AtomicOperations::memcpySafeWhenRacy(p2, p, sizeof(T)); \ + CHECK(*q2 == A); \ + *q = A; \ + *q2 = B; \ + jit::AtomicOperations::memcpySafeWhenRacy(p2, p.unwrap(), sizeof(T)); \ + CHECK(*q2 == A); \ + *q = A; \ + *q2 = B; \ + jit::AtomicOperations::memcpySafeWhenRacy(p2.unwrap(), p, sizeof(T)); \ + CHECK(*q2 == A); \ + *q = A; \ + *q2 = B; \ + jit::AtomicOperations::memmoveSafeWhenRacy(p2, p, sizeof(T)); \ + CHECK(*q2 == A); \ + *q = A; \ + *q2 = B; \ + jit::AtomicOperations::podCopySafeWhenRacy(p2, p, 1); \ + CHECK(*q2 == A); \ + *q = A; \ + *q2 = B; \ + jit::AtomicOperations::podMoveSafeWhenRacy(p2, p, 1); \ + CHECK(*q2 == A); \ + return true + +BEGIN_REUSABLE_TEST(testAtomicOperationsU8) { + const uint8_t A = 0xab; + const uint8_t B = 0x37; + ATOMIC_TESTS(uint8_t, A, B); +} +END_TEST(testAtomicOperationsU8) + +BEGIN_REUSABLE_TEST(testAtomicOperationsI8) { + const int8_t A = 0x3b; + const int8_t B = 0x27; + ATOMIC_TESTS(int8_t, A, B); +} +END_TEST(testAtomicOperationsI8) + +BEGIN_REUSABLE_TEST(testAtomicOperationsU16) { + const uint16_t A = 0xabdc; + const uint16_t B = 0x3789; + ATOMIC_TESTS(uint16_t, A, B); +} +END_TEST(testAtomicOperationsU16) + +BEGIN_REUSABLE_TEST(testAtomicOperationsI16) { + const int16_t A = 0x3bdc; + const int16_t B = 0x2737; + ATOMIC_TESTS(int16_t, A, B); +} +END_TEST(testAtomicOperationsI16) + +BEGIN_REUSABLE_TEST(testAtomicOperationsU32) { + const uint32_t A = 0xabdc0588; + const uint32_t B = 0x37891942; + ATOMIC_TESTS(uint32_t, A, B); +} +END_TEST(testAtomicOperationsU32) + +BEGIN_REUSABLE_TEST(testAtomicOperationsI32) { + const int32_t A = 0x3bdc0588; + const int32_t B = 0x27371843; + ATOMIC_TESTS(int32_t, A, B); +} +END_TEST(testAtomicOperationsI32) + +BEGIN_REUSABLE_TEST(testAtomicOperationsU64) { + if (!jit::AtomicOperations::hasAtomic8()) { + return true; + } + + const uint64_t A(0x9aadf00ddeadbeef); + const uint64_t B(0x4eedbead1337f001); + ATOMIC_TESTS(uint64_t, A, B); +} +END_TEST(testAtomicOperationsU64) + +BEGIN_REUSABLE_TEST(testAtomicOperationsI64) { + if (!jit::AtomicOperations::hasAtomic8()) { + return true; + } + + const int64_t A(0x2aadf00ddeadbeef); + const int64_t B(0x4eedbead1337f001); + ATOMIC_TESTS(int64_t, A, B); +} +END_TEST(testAtomicOperationsI64) + +// T is the primitive float type we're testing, and A and B are references to +// constant bindings holding values of that type. +// +// Stay away from 0, NaN, infinities, and denormals. + +#define ATOMIC_FLOAT_TESTS(T, A, B) \ + T* q = (T*)hidePointerValue((void*)atomicMem); \ + *q = A; \ + SharedMem<T*> p = \ + SharedMem<T*>::shared((T*)hidePointerValue((T*)atomicMem)); \ + CHECK(*q == A); \ + CHECK(jit::AtomicOperations::loadSafeWhenRacy(p) == A); \ + jit::AtomicOperations::storeSafeWhenRacy(p, B); \ + CHECK(*q == B); \ + T* q2 = (T*)hidePointerValue((void*)atomicMem2); \ + SharedMem<T*> p2 = \ + SharedMem<T*>::shared((T*)hidePointerValue((void*)atomicMem2)); \ + *q = A; \ + *q2 = B; \ + jit::AtomicOperations::memcpySafeWhenRacy(p2, p, sizeof(T)); \ + CHECK(*q2 == A); \ + *q = A; \ + *q2 = B; \ + jit::AtomicOperations::memcpySafeWhenRacy(p2, p.unwrap(), sizeof(T)); \ + CHECK(*q2 == A); \ + *q = A; \ + *q2 = B; \ + jit::AtomicOperations::memcpySafeWhenRacy(p2.unwrap(), p, sizeof(T)); \ + CHECK(*q2 == A); \ + *q = A; \ + *q2 = B; \ + jit::AtomicOperations::memmoveSafeWhenRacy(p2, p, sizeof(T)); \ + CHECK(*q2 == A); \ + *q = A; \ + *q2 = B; \ + jit::AtomicOperations::podCopySafeWhenRacy(p2, p, 1); \ + CHECK(*q2 == A); \ + *q = A; \ + *q2 = B; \ + jit::AtomicOperations::podMoveSafeWhenRacy(p2, p, 1); \ + CHECK(*q2 == A); \ + return true + +BEGIN_REUSABLE_TEST(testAtomicOperationsF32) { + const float A(123.25); + const float B(-987.75); + ATOMIC_FLOAT_TESTS(float, A, B); +} +END_TEST(testAtomicOperationsF32) + +BEGIN_REUSABLE_TEST(testAtomicOperationsF64) { + const double A(123.25); + const double B(-987.75); + ATOMIC_FLOAT_TESTS(double, A, B); +} +END_TEST(testAtomicOperationsF64) + +#define ATOMIC_CLAMPED_TESTS(T, A, B) \ + T* q = (T*)hidePointerValue((void*)atomicMem); \ + *q = A; \ + SharedMem<T*> p = \ + SharedMem<T*>::shared((T*)hidePointerValue((T*)atomicMem)); \ + CHECK(*q == A); \ + CHECK(jit::AtomicOperations::loadSafeWhenRacy(p) == A); \ + jit::AtomicOperations::storeSafeWhenRacy(p, B); \ + CHECK(*q == B); \ + return true + +BEGIN_REUSABLE_TEST(testAtomicOperationsU8Clamped) { + const uint8_clamped A(0xab); + const uint8_clamped B(0x37); + ATOMIC_CLAMPED_TESTS(uint8_clamped, A, B); +} +END_TEST(testAtomicOperationsU8Clamped) + +#undef ATOMIC_TESTS +#undef ATOMIC_FLOAT_TESTS +#undef ATOMIC_CLAMPED_TESTS diff --git a/js/src/jsapi-tests/testAtomizeUtf8NonAsciiLatin1CodePoint.cpp b/js/src/jsapi-tests/testAtomizeUtf8NonAsciiLatin1CodePoint.cpp new file mode 100644 index 0000000000..dd66318448 --- /dev/null +++ b/js/src/jsapi-tests/testAtomizeUtf8NonAsciiLatin1CodePoint.cpp @@ -0,0 +1,212 @@ +/* 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 "mozilla/Maybe.h" // mozilla::Maybe +#include "mozilla/Utf8.h" // mozilla::IsTrailingUnit, mozilla::Utf8Unit, mozilla::DecodeOneUtf8CodePoint + +#include <inttypes.h> // UINT8_MAX +#include <stdint.h> // uint16_t + +#include "js/Exception.h" // JS_IsExceptionPending, JS_ClearPendingException +#include "js/RootingAPI.h" // JS::Rooted, JS::MutableHandle +#include "jsapi-tests/tests.h" // BEGIN_TEST, END_TEST, CHECK +#include "vm/JSAtomUtils.h" // js::AtomizeChars, js::AtomizeUTF8Chars +#include "vm/StringType.h" // JSAtom + +using mozilla::DecodeOneUtf8CodePoint; +using mozilla::IsAscii; +using mozilla::IsTrailingUnit; +using mozilla::Maybe; +using mozilla::Utf8Unit; + +using JS::Latin1Char; +using JS::MutableHandle; +using JS::Rooted; + +BEGIN_TEST(testAtomizeTwoByteUTF8) { + Rooted<JSAtom*> atom16(cx); + Rooted<JSAtom*> atom8(cx); + + for (uint16_t i = 0; i <= UINT8_MAX; i++) { + // Test cases where the first unit is ASCII. + if (IsAscii(char16_t(i))) { + for (uint16_t j = 0; j <= UINT8_MAX; j++) { + if (IsAscii(char16_t(j))) { + // If both units are ASCII, the sequence encodes a two-code point + // string. + if (!shouldBeTwoCodePoints(i, j, &atom16, &atom8)) { + return false; + } + } else { + // ASCII followed by non-ASCII should be invalid. + if (!shouldBeInvalid(i, j)) { + return false; + } + } + } + + continue; + } + + // Test remaining cases where the first unit isn't a two-byte lead. + if ((i & 0b1110'0000) != 0b1100'0000) { + for (uint16_t j = 0; j <= UINT8_MAX; j++) { + // If the first unit isn't a two-byte lead, the sequence is invalid no + // matter what the second unit is. + if (!shouldBeInvalid(i, j)) { + return false; + } + } + + continue; + } + + // Test remaining cases where the first unit is the two-byte lead of a + // non-Latin-1 code point. + if (i >= 0b1100'0100) { + for (uint16_t j = 0; j <= UINT8_MAX; j++) { + if (IsTrailingUnit(Utf8Unit(static_cast<uint8_t>(j)))) { + if (!shouldBeSingleNonLatin1(i, j, &atom16, &atom8)) { + return false; + } + } else { + if (!shouldBeInvalid(i, j)) { + return false; + } + } + } + + continue; + } + + // Test remaining cases where the first unit is the two-byte lead of an + // overlong ASCII code point. + if (i < 0b1100'0010) { + for (uint16_t j = 0; j <= UINT8_MAX; j++) { + if (!shouldBeInvalid(i, j)) { + return false; + } + } + + continue; + } + + // Finally, test remaining cases where the first unit is the two-byte lead + // of a Latin-1 code point. + for (uint16_t j = 0; j <= UINT8_MAX; j++) { + if (IsTrailingUnit(Utf8Unit(static_cast<uint8_t>(j)))) { + if (!shouldBeSingleLatin1(i, j, &atom16, &atom8)) { + return false; + } + } else { + if (!shouldBeInvalid(i, j)) { + return false; + } + } + } + } + + return true; +} + +bool shouldBeTwoCodePoints(uint16_t first, uint16_t second, + MutableHandle<JSAtom*> atom16, + MutableHandle<JSAtom*> atom8) { + CHECK(first <= UINT8_MAX); + CHECK(second <= UINT8_MAX); + CHECK(IsAscii(char16_t(first))); + CHECK(IsAscii(char16_t(second))); + + const char16_t utf16[] = {static_cast<char16_t>(first), + static_cast<char16_t>(second)}; + atom16.set(js::AtomizeChars(cx, utf16, 2)); + CHECK(atom16); + CHECK(atom16->length() == 2); + CHECK(atom16->latin1OrTwoByteChar(0) == first); + CHECK(atom16->latin1OrTwoByteChar(1) == second); + + const char utf8[] = {static_cast<char>(first), static_cast<char>(second)}; + atom8.set(js::AtomizeUTF8Chars(cx, utf8, 2)); + CHECK(atom8); + CHECK(atom8->length() == 2); + CHECK(atom8->latin1OrTwoByteChar(0) == first); + CHECK(atom8->latin1OrTwoByteChar(1) == second); + + CHECK(atom16 == atom8); + + return true; +} + +bool shouldBeOneCodePoint(uint16_t first, uint16_t second, char32_t v, + MutableHandle<JSAtom*> atom16, + MutableHandle<JSAtom*> atom8) { + CHECK(first <= UINT8_MAX); + CHECK(second <= UINT8_MAX); + CHECK(v <= UINT16_MAX); + + const char16_t utf16[] = {static_cast<char16_t>(v)}; + atom16.set(js::AtomizeChars(cx, utf16, 1)); + CHECK(atom16); + CHECK(atom16->length() == 1); + CHECK(atom16->latin1OrTwoByteChar(0) == v); + + const char utf8[] = {static_cast<char>(first), static_cast<char>(second)}; + atom8.set(js::AtomizeUTF8Chars(cx, utf8, 2)); + CHECK(atom8); + CHECK(atom8->length() == 1); + CHECK(atom8->latin1OrTwoByteChar(0) == v); + + CHECK(atom16 == atom8); + + return true; +} + +bool shouldBeSingleNonLatin1(uint16_t first, uint16_t second, + MutableHandle<JSAtom*> atom16, + MutableHandle<JSAtom*> atom8) { + CHECK(first <= UINT8_MAX); + CHECK(second <= UINT8_MAX); + + const char bytes[] = {static_cast<char>(first), static_cast<char>(second)}; + const char* iter = &bytes[1]; + Maybe<char32_t> cp = + DecodeOneUtf8CodePoint(Utf8Unit(bytes[0]), &iter, bytes + 2); + CHECK(cp.isSome()); + + char32_t v = cp.value(); + CHECK(v > UINT8_MAX); + + return shouldBeOneCodePoint(first, second, v, atom16, atom8); +} + +bool shouldBeSingleLatin1(uint16_t first, uint16_t second, + MutableHandle<JSAtom*> atom16, + MutableHandle<JSAtom*> atom8) { + CHECK(first <= UINT8_MAX); + CHECK(second <= UINT8_MAX); + + const char bytes[] = {static_cast<char>(first), static_cast<char>(second)}; + const char* iter = &bytes[1]; + Maybe<char32_t> cp = + DecodeOneUtf8CodePoint(Utf8Unit(bytes[0]), &iter, bytes + 2); + CHECK(cp.isSome()); + + char32_t v = cp.value(); + CHECK(v <= UINT8_MAX); + + return shouldBeOneCodePoint(first, second, v, atom16, atom8); +} + +bool shouldBeInvalid(uint16_t first, uint16_t second) { + CHECK(first <= UINT8_MAX); + CHECK(second <= UINT8_MAX); + + const char invalid[] = {static_cast<char>(first), static_cast<char>(second)}; + CHECK(!js::AtomizeUTF8Chars(cx, invalid, 2)); + CHECK(JS_IsExceptionPending(cx)); + JS_ClearPendingException(cx); + + return true; +} +END_TEST(testAtomizeTwoByteUTF8) diff --git a/js/src/jsapi-tests/testAtomizeWithoutActiveZone.cpp b/js/src/jsapi-tests/testAtomizeWithoutActiveZone.cpp new file mode 100644 index 0000000000..5f4ff36f3e --- /dev/null +++ b/js/src/jsapi-tests/testAtomizeWithoutActiveZone.cpp @@ -0,0 +1,52 @@ +/* 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 "js/Realm.h" +#include "js/RootingAPI.h" +#include "jsapi-tests/tests.h" +#include "vm/CommonPropertyNames.h" +#include "vm/JSAtomState.h" +#include "vm/JSContext.h" + +BEGIN_TEST(testAtomizeWithoutActiveZone) { + // Tests for JS_AtomizeAndPinString when called without an active zone. + + MOZ_ASSERT(cx->zone()); + + JS::RootedString testAtom1(cx, JS_AtomizeString(cx, "test1234@!")); + CHECK(testAtom1); + + JS::RootedString testAtom2(cx); + { + JSAutoNullableRealm ar(cx, nullptr); + MOZ_ASSERT(!cx->zone()); + + // Permanent atom. + JSString* atom = JS_AtomizeAndPinString(cx, "boolean"); + CHECK(atom); + CHECK_EQUAL(atom, cx->names().boolean); + + // Static string. + atom = JS_AtomizeAndPinString(cx, "8"); + CHECK(atom); + CHECK_EQUAL(atom, cx->staticStrings().getUint(8)); + + // Existing atom. + atom = JS_AtomizeAndPinString(cx, "test1234@!"); + CHECK(atom); + CHECK_EQUAL(atom, testAtom1); + + // New atom. + testAtom2 = JS_AtomizeAndPinString(cx, "asdflkjsdf987_@"); + CHECK(testAtom2); + } + + MOZ_ASSERT(cx->zone()); + JSString* atom = JS_AtomizeString(cx, "asdflkjsdf987_@"); + CHECK(atom); + CHECK_EQUAL(atom, testAtom2); + + return true; +} +END_TEST(testAtomizeWithoutActiveZone) diff --git a/js/src/jsapi-tests/testAvlTree.cpp b/js/src/jsapi-tests/testAvlTree.cpp new file mode 100644 index 0000000000..8d6e849955 --- /dev/null +++ b/js/src/jsapi-tests/testAvlTree.cpp @@ -0,0 +1,383 @@ +/* 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 <set> +#include <stdio.h> + +#include "ds/AvlTree.h" + +#include "jsapi-tests/tests.h" + +using namespace js; + +//////////////////////////////////////////////////////////////////////// +// // +// AvlTree testing interface. // +// // +//////////////////////////////////////////////////////////////////////// + +// The "standard" AVL interface, `class AvlTree` at the end of +// js/src/ds/AvlTree.h, is too restrictive to allow proper testing of the AVL +// tree internals. In particular it disallows insertion of duplicate values +// and removal of non-present values, and lacks various secondary functions +// such as for counting the number of nodes. +// +// So, for testing, we wrap an alternative interface `AvlTreeTestIF` around +// the core implementation. + +template <class T, class C> +class AvlTreeTestIF : public AvlTreeImpl<T, C> { + // Shorthands for names in the implementation (parent) class. + using Impl = AvlTreeImpl<T, C>; + using ImplTag = typename AvlTreeImpl<T, C>::Tag; + using ImplNode = typename AvlTreeImpl<T, C>::Node; + using ImplResult = typename AvlTreeImpl<T, C>::Result; + using ImplNodeAndResult = typename AvlTreeImpl<T, C>::NodeAndResult; + + public: + explicit AvlTreeTestIF(LifoAlloc* alloc = nullptr) : Impl(alloc) {} + + // Insert `v` if it isn't already there, else leave the tree unchanged. + // Returns true iff an insertion happened. + bool testInsert(const T& v) { + ImplNode* new_root = Impl::insert_worker(v); + if (!new_root) { + // OOM + MOZ_CRASH(); + } + if (uintptr_t(new_root) == uintptr_t(1)) { + // Already present + return false; + } + Impl::root_ = new_root; + return true; + } + + // Remove `v` if it is present. Returns true iff a removal happened. + bool testRemove(const T& v) { + ImplNodeAndResult pair = Impl::delete_worker(Impl::root_, v); + ImplNode* new_root = pair.first; + ImplResult res = pair.second; + if (res == ImplResult::Error) { + // `v` isn't in the tree. + return false; + } else { + Impl::root_ = new_root; + return true; + } + } + + // Count number of elements + size_t testSize_worker(ImplNode* n) const { + if (n) { + return 1 + testSize_worker(n->left) + testSize_worker(n->getRight()); + } + return 0; + } + size_t testSize() const { return testSize_worker(Impl::root_); } + + size_t testDepth_worker(ImplNode* n) const { + if (n) { + size_t depthL = testDepth_worker(n->left); + size_t depthR = testDepth_worker(n->getRight()); + return 1 + (depthL > depthR ? depthL : depthR); + } + return 0; + } + size_t testDepth() const { return testDepth_worker(Impl::root_); } + + bool testContains(const T& v) const { + ImplNode* node = Impl::find_worker(v); + return node != nullptr; + } + + ImplNode* testGetRoot() const { return Impl::root_; } + ImplNode* testGetFreeList() const { return Impl::freeList_; } + + bool testFreeListLooksValid(size_t maxElems) { + size_t numElems = 0; + ImplNode* node = Impl::freeList_; + while (node) { + numElems++; + if (numElems > maxElems) { + return false; + } + if (node->getTag() != ImplTag::Free || node->getRight() != nullptr) { + return false; + } + node = node->left; + } + return true; + } + + // For debugging only + private: + void testShow_worker(int depth, const ImplNode* node) const { + if (node) { + testShow_worker(depth + 1, node->getRight()); + for (int i = 0; i < depth; i++) { + printf(" "); + } + char* str = node->item.show(); + printf("%s\n", str); + free(str); + testShow_worker(depth + 1, node->left); + } + } + + public: + // For debugging only + void testShow() const { testShow_worker(0, Impl::root_); } + + // AvlTree::Iter is also public; it's just pass-through from AvlTreeImpl. +}; + +//////////////////////////////////////////////////////////////////////// +// // +// AvlTree test cases. // +// // +//////////////////////////////////////////////////////////////////////// + +class CmpInt { + int x_; + + public: + explicit CmpInt(int x) : x_(x) {} + ~CmpInt() {} + static int compare(const CmpInt& me, const CmpInt& other) { + if (me.x_ < other.x_) return -1; + if (me.x_ > other.x_) return 1; + return 0; + } + int get() const { return x_; } + char* show() const { + const size_t length = 16; + char* str = (char*)calloc(length, 1); + snprintf(str, length, "%d", x_); + return str; + } +}; + +bool TreeIsPlausible(const AvlTreeTestIF<CmpInt, CmpInt>& tree, + const std::set<int>& should_be_in_tree, int UNIV_MIN, + int UNIV_MAX) { + // Same cardinality + size_t n_in_set = should_be_in_tree.size(); + size_t n_in_tree = tree.testSize(); + if (n_in_set != n_in_tree) { + return false; + } + + // Tree is not wildly out of balance. Depth should not exceed 1.44 * + // log2(size). + size_t tree_depth = tree.testDepth(); + size_t log2_size = 0; + { + size_t n = n_in_tree; + while (n > 0) { + n = n >> 1; + log2_size += 1; + } + } + // Actually a tighter limit than stated above. For these test cases, the + // tree is either perfectly balanced or within one level of being so (hence + // the +1). + if (tree_depth > log2_size + 1) { + return false; + } + + // Check that everything that should be in the tree is in it, and vice + // versa. + for (int i = UNIV_MIN; i < UNIV_MAX; i++) { + bool should_be_in = should_be_in_tree.find(i) != should_be_in_tree.end(); + + // Look it up with a null comparator (so `contains` compares + // directly) + bool is_in = tree.testContains(CmpInt(i)); + if (is_in != should_be_in) { + return false; + } + } + + return true; +} + +template <typename T> +bool SetContains(std::set<T> s, const T& v) { + return s.find(v) != s.end(); +} + +BEGIN_TEST(testAvlTree_main) { + static const int UNIV_MIN = 5000; + static const int UNIV_MAX = 5999; + static const int UNIV_SIZE = UNIV_MAX - UNIV_MIN + 1; + + LifoAlloc alloc(4096); + AvlTreeTestIF<CmpInt, CmpInt> tree(&alloc); + std::set<int> should_be_in_tree; + + // Add numbers to the tree, checking as we go. + for (int i = UNIV_MIN; i < UNIV_MAX; i++) { + // Idiotic but simple + if (i % 3 != 0) { + continue; + } + bool was_added = tree.testInsert(CmpInt(i)); + should_be_in_tree.insert(i); + CHECK(was_added); + CHECK(TreeIsPlausible(tree, should_be_in_tree, UNIV_MIN, UNIV_MAX)); + } + + // Then remove the middle half of the tree, also checking. + for (int i = UNIV_MIN + UNIV_SIZE / 4; i < UNIV_MIN + 3 * (UNIV_SIZE / 4); + i++) { + // Note that here, we're asking to delete a bunch of numbers that aren't + // in the tree. It should remain valid throughout. + bool was_removed = tree.testRemove(CmpInt(i)); + bool should_have_been_removed = SetContains(should_be_in_tree, i); + CHECK(was_removed == should_have_been_removed); + should_be_in_tree.erase(i); + CHECK(TreeIsPlausible(tree, should_be_in_tree, UNIV_MIN, UNIV_MAX)); + } + + // Now add some numbers which are already in the tree. + for (int i = UNIV_MIN; i < UNIV_MIN + UNIV_SIZE / 4; i++) { + if (i % 3 != 0) { + continue; + } + bool was_added = tree.testInsert(CmpInt(i)); + bool should_have_been_added = !SetContains(should_be_in_tree, i); + CHECK(was_added == should_have_been_added); + should_be_in_tree.insert(i); + CHECK(TreeIsPlausible(tree, should_be_in_tree, UNIV_MIN, UNIV_MAX)); + } + + // Then remove all numbers from the tree, in reverse order. + for (int ir = UNIV_MIN; ir < UNIV_MAX; ir++) { + int i = UNIV_MIN + (UNIV_MAX - ir); + bool was_removed = tree.testRemove(CmpInt(i)); + bool should_have_been_removed = SetContains(should_be_in_tree, i); + CHECK(was_removed == should_have_been_removed); + should_be_in_tree.erase(i); + CHECK(TreeIsPlausible(tree, should_be_in_tree, UNIV_MIN, UNIV_MAX)); + } + + // Now the tree should be empty. + CHECK(should_be_in_tree.empty()); + CHECK(tree.testSize() == 0); + + // Now delete some more stuff. Tree should still be empty :-) + for (int i = UNIV_MIN + 10; i < UNIV_MIN + 100; i++) { + CHECK(should_be_in_tree.empty()); + CHECK(tree.testSize() == 0); + bool was_removed = tree.testRemove(CmpInt(i)); + CHECK(!was_removed); + CHECK(TreeIsPlausible(tree, should_be_in_tree, UNIV_MIN, UNIV_MAX)); + } + + // The tree root should be NULL. + CHECK(tree.testGetRoot() == nullptr); + CHECK(tree.testGetFreeList() != nullptr); + + // Check the freelist to the extent we can: it's non-circular, and the + // elements look plausible. If it's not shorter than the specified length + // then it must have a cycle, since the tests above won't have resulted in + // more than 400 free nodes at the end. + CHECK(tree.testFreeListLooksValid(400 /*arbitrary*/)); + + // Test iteration, first in a tree with 9 nodes. This tests the general + // case. + { + CHECK(tree.testSize() == 0); + for (int i = 10; i < 100; i += 10) { + bool was_inserted = tree.testInsert(CmpInt(i)); + CHECK(was_inserted); + } + + // Test iteration across the whole tree. + AvlTreeTestIF<CmpInt, CmpInt>::Iter iter(&tree); + // `expect` produces (independently) the next expected number. `remaining` + // counts the number items of items remaining. + int expect = 10; + int remaining = 9; + while (iter.hasMore()) { + CmpInt ci = iter.next(); + CHECK(ci.get() == expect); + expect += 10; + remaining--; + } + CHECK(remaining == 0); + + // Test iteration from a specified start point. + for (int i = 10; i < 100; i += 10) { + for (int j = i - 1; j <= i + 1; j++) { + // Set up `expect` and `remaining`. + remaining = (100 - i) / 10; + switch (j % 10) { + case 0: + expect = j; + break; + case 1: + expect = j + 9; + remaining--; + break; + case 9: + expect = j + 1; + break; + default: + MOZ_CRASH(); + } + AvlTreeTestIF<CmpInt, CmpInt>::Iter iter(&tree, CmpInt(j)); + while (iter.hasMore()) { + CmpInt ci = iter.next(); + CHECK(ci.get() == expect); + expect += 10; + remaining--; + } + CHECK(remaining == 0); + } + } + } + + // Now with a completely empty tree. + { + AvlTreeTestIF<CmpInt, CmpInt> emptyTree(&alloc); + CHECK(emptyTree.testSize() == 0); + // Full tree iteration gets us nothing. + AvlTreeTestIF<CmpInt, CmpInt>::Iter iter1(&emptyTree); + CHECK(!iter1.hasMore()); + // Starting iteration with any number gets us nothing. + AvlTreeTestIF<CmpInt, CmpInt>::Iter iter2(&emptyTree, CmpInt(42)); + CHECK(!iter2.hasMore()); + } + + // Finally with a one-element tree. + { + AvlTreeTestIF<CmpInt, CmpInt> unitTree(&alloc); + bool was_inserted = unitTree.testInsert(CmpInt(1337)); + CHECK(was_inserted); + CHECK(unitTree.testSize() == 1); + // Try full tree iteration. + AvlTreeTestIF<CmpInt, CmpInt>::Iter iter3(&unitTree); + CHECK(iter3.hasMore()); + CmpInt ci = iter3.next(); + CHECK(ci.get() == 1337); + CHECK(!iter3.hasMore()); + for (int i = 1336; i <= 1338; i++) { + int remaining = i < 1338 ? 1 : 0; + int expect = i < 1338 ? 1337 : 99999 /*we'll never use this*/; + AvlTreeTestIF<CmpInt, CmpInt>::Iter iter4(&unitTree, CmpInt(i)); + while (iter4.hasMore()) { + CmpInt ci = iter4.next(); + CHECK(ci.get() == expect); + remaining--; + // expect doesn't change, we only expect it (or nothing) + } + CHECK(remaining == 0); + } + } + + return true; +} +END_TEST(testAvlTree_main) diff --git a/js/src/jsapi-tests/testBigInt.cpp b/js/src/jsapi-tests/testBigInt.cpp new file mode 100644 index 0000000000..1a1dbfe4ec --- /dev/null +++ b/js/src/jsapi-tests/testBigInt.cpp @@ -0,0 +1,768 @@ +/* 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 "mozilla/FloatingPoint.h" // mozilla::NumberIsInt32 +#include "mozilla/Range.h" // mozilla::Range +#include "mozilla/Span.h" // mozilla::MakeStringSpan + +#include <stdint.h> + +#include "js/BigInt.h" // JS::{,Number,String,SimpleString}ToBigInt, JS::ToBig{I,Ui}nt64 +#include "js/CharacterEncoding.h" // JS::Const{Latin1,TwoByte}Chars +#include "js/Conversions.h" // JS::ToString +#include "js/ErrorReport.h" // JS::ErrorReportBuilder, JSEXN_SYNTAXERR +#include "js/Exception.h" // JS::StealPendingExceptionStack, JS_IsExceptionPending +#include "js/friend/ErrorMessages.h" // JSMSG_* +#include "js/RootingAPI.h" // JS::Rooted +#include "js/String.h" // JS_StringEqualsLiteral +#include "js/Value.h" // JS::FalseValue, JS::Value + +#include "jsapi-tests/tests.h" +#include "util/Text.h" // js::InflateString + +struct JS_PUBLIC_API JSContext; +class JS_PUBLIC_API JSString; + +namespace JS { + +class JS_PUBLIC_API BigInt; + +} // namespace JS + +BEGIN_TEST(testToBigInt64) { + JS::Rooted<JS::Value> v(cx); + + EVAL("0n", &v); + CHECK(v.isBigInt()); + CHECK(JS::ToBigInt64(v.toBigInt()) == 0); + + EVAL("9223372036854775807n", &v); + CHECK(v.isBigInt()); + CHECK(JS::ToBigInt64(v.toBigInt()) == 9223372036854775807L); + + EVAL("-9223372036854775808n", &v); + CHECK(v.isBigInt()); + CHECK(JS::ToBigInt64(v.toBigInt()) == -9223372036854775807L - 1L); + + return true; +} +END_TEST(testToBigInt64) + +BEGIN_TEST(testToBigUint64) { + JS::Rooted<JS::Value> v(cx); + + EVAL("0n", &v); + CHECK(v.isBigInt()); + CHECK(JS::ToBigUint64(v.toBigInt()) == 0); + + EVAL("18446744073709551615n", &v); + CHECK(v.isBigInt()); + CHECK(JS::ToBigUint64(v.toBigInt()) == 18446744073709551615UL); + + return true; +} +END_TEST(testToBigUint64) + +#define GENERATE_INTTYPE_TEST(bits) \ + BEGIN_TEST(testNumberToBigInt_Int##bits) { \ + int##bits##_t i = INT##bits##_MIN; \ + JS::BigInt* bi = JS::NumberToBigInt(cx, i); \ + CHECK(bi); \ + CHECK(JS::ToBigInt64(bi) == i); \ + \ + i = INT##bits##_MAX; \ + bi = JS::NumberToBigInt(cx, i); \ + CHECK(bi); \ + CHECK(JS::ToBigInt64(bi) == i); \ + \ + uint##bits##_t u = 0; \ + bi = JS::NumberToBigInt(cx, u); \ + CHECK(bi); \ + CHECK(JS::ToBigUint64(bi) == 0); \ + \ + u = UINT##bits##_MAX; \ + bi = JS::NumberToBigInt(cx, u); \ + CHECK(bi); \ + CHECK(JS::ToBigUint64(bi) == u); \ + \ + return true; \ + } \ + END_TEST(testNumberToBigInt_Int##bits) + +GENERATE_INTTYPE_TEST(8); +GENERATE_INTTYPE_TEST(16); +GENERATE_INTTYPE_TEST(32); +GENERATE_INTTYPE_TEST(64); + +#undef GENERATE_INTTYPE_TEST + +#define GENERATE_SIGNED_VALUE_TEST(type, tag, val) \ + BEGIN_TEST(testNumberToBigInt_##type##_##tag) { \ + type v = val; \ + JS::BigInt* bi = JS::NumberToBigInt(cx, v); \ + CHECK(bi); \ + CHECK(JS::ToBigInt64(bi) == (val)); \ + return true; \ + } \ + END_TEST(testNumberToBigInt_##type##_##tag) + +#define GENERATE_UNSIGNED_VALUE_TEST(type, tag, val) \ + BEGIN_TEST(testNumberToBigInt_##type##_##tag) { \ + type v = val; \ + JS::BigInt* bi = JS::NumberToBigInt(cx, v); \ + CHECK(bi); \ + CHECK(JS::ToBigUint64(bi) == (val)); \ + return true; \ + } \ + END_TEST(testNumberToBigInt_##type##_##tag) + +GENERATE_SIGNED_VALUE_TEST(int, zero, 0); +GENERATE_SIGNED_VALUE_TEST(int, aValue, -42); +GENERATE_UNSIGNED_VALUE_TEST(unsigned, zero, 0); +GENERATE_UNSIGNED_VALUE_TEST(unsigned, aValue, 42); +GENERATE_SIGNED_VALUE_TEST(long, zero, 0); +GENERATE_SIGNED_VALUE_TEST(long, aValue, -42); +GENERATE_UNSIGNED_VALUE_TEST(uintptr_t, zero, 0); +GENERATE_UNSIGNED_VALUE_TEST(uintptr_t, aValue, 42); +GENERATE_UNSIGNED_VALUE_TEST(size_t, zero, 0); +GENERATE_UNSIGNED_VALUE_TEST(size_t, aValue, 42); +GENERATE_SIGNED_VALUE_TEST(double, zero, 0); +GENERATE_SIGNED_VALUE_TEST(double, aValue, -42); + +#undef GENERATE_SIGNED_VALUE_TEST +#undef GENERATE_UNSIGNED_VALUE_TEST + +BEGIN_TEST(testNumberToBigInt_bool) { + JS::BigInt* bi = JS::NumberToBigInt(cx, true); + CHECK(bi); + CHECK(JS::ToBigUint64(bi) == 1); + + bi = JS::NumberToBigInt(cx, false); + CHECK(bi); + CHECK(JS::ToBigUint64(bi) == 0); + + return true; +} +END_TEST(testNumberToBigInt_bool) + +BEGIN_TEST(testNumberToBigInt_NonIntegerValueFails) { + JS::BigInt* bi = JS::NumberToBigInt(cx, 3.1416); + CHECK_NULL(bi); + CHECK(JS_IsExceptionPending(cx)); + JS_ClearPendingException(cx); + return true; +} +END_TEST(testNumberToBigInt_NonIntegerValueFails) + +BEGIN_TEST(testStringToBigInt_FromTwoByteStringSpan) { + mozilla::Range<const char16_t> input{ + mozilla::MakeStringSpan(u"18446744073709551616")}; + JS::BigInt* bi = JS::StringToBigInt(cx, input); + CHECK(bi); + JS::Rooted<JS::Value> val(cx, JS::BigIntValue(bi)); + JS::Rooted<JSString*> str(cx, JS::ToString(cx, val)); + CHECK(str); + bool match; + CHECK(JS_StringEqualsLiteral(cx, str, "18446744073709551616", &match)); + CHECK(match); + return true; +} +END_TEST(testStringToBigInt_FromTwoByteStringSpan) + +BEGIN_TEST(testStringToBigInt_FromLatin1Range) { + const JS::Latin1Char string[] = "12345 and some junk at the end"; + JS::ConstLatin1Chars range(string, 5); + JS::BigInt* bi = JS::StringToBigInt(cx, range); + CHECK(bi); + CHECK(JS::ToBigInt64(bi) == 12345); + return true; +} +END_TEST(testStringToBigInt_FromLatin1Range) + +BEGIN_TEST(testStringToBigInt_FromTwoByteRange) { + const char16_t string[] = u"12345 and some junk at the end"; + JS::ConstTwoByteChars range(string, 5); + JS::BigInt* bi = JS::StringToBigInt(cx, range); + CHECK(bi); + CHECK(JS::ToBigInt64(bi) == 12345); + return true; +} +END_TEST(testStringToBigInt_FromTwoByteRange) + +BEGIN_TEST(testStringToBigInt_AcceptedInput) { + CHECK(Allowed(u"", 0)); + CHECK(Allowed(u"\n", 0)); + CHECK(Allowed(u" ", 0)); + CHECK(Allowed(u"0\n", 0)); + CHECK(Allowed(u"0 ", 0)); + CHECK(Allowed(u"\n1", 1)); + CHECK(Allowed(u" 1", 1)); + CHECK(Allowed(u"\n2 ", 2)); + CHECK(Allowed(u" 2\n", 2)); + CHECK(Allowed(u"0b11", 3)); + CHECK(Allowed(u"0x17", 23)); + CHECK(Allowed(u"-5", -5)); + CHECK(Allowed(u"+5", 5)); + CHECK(Allowed(u"-0", 0)); + + CHECK(Fails(u"!!!!!!111one1111one1!1!1!!")); + CHECK(Fails(u"3.1416")); + CHECK(Fails(u"6.022e23")); + CHECK(Fails(u"1e3")); + CHECK(Fails(u".25")); + CHECK(Fails(u".25e2")); + CHECK(Fails(u"1_000_000")); + CHECK(Fails(u"3n")); + CHECK(Fails(u"-0x3")); + CHECK(Fails(u"Infinity")); + + return true; +} + +template <size_t N> +inline bool Allowed(const char16_t (&str)[N], int64_t expected) { + JS::BigInt* bi = JS::StringToBigInt(cx, mozilla::MakeStringSpan(str)); + CHECK(bi); + CHECK(JS::ToBigInt64(bi) == expected); + return true; +} + +template <size_t N> +inline bool Fails(const char16_t (&str)[N]) { + JS::BigInt* bi = JS::StringToBigInt(cx, mozilla::MakeStringSpan(str)); + CHECK_NULL(bi); + CHECK(JS_IsExceptionPending(cx)); + + JS::ExceptionStack exnStack(cx); + CHECK(JS::StealPendingExceptionStack(cx, &exnStack)); + + JS::ErrorReportBuilder report(cx); + CHECK(report.init(cx, exnStack, JS::ErrorReportBuilder::NoSideEffects)); + CHECK(report.report()->exnType == JSEXN_SYNTAXERR); + CHECK(report.report()->errorNumber == JSMSG_BIGINT_INVALID_SYNTAX); + + CHECK(!JS_IsExceptionPending(cx)); + + return true; +} +END_TEST(testStringToBigInt_AcceptedInput) + +BEGIN_TEST(testSimpleStringToBigInt_AcceptedInput) { + CHECK(Allowed("12345", 10, 12345)); + CHECK(Allowed("+12345", 10, 12345)); + CHECK(Allowed("-12345", 10, -12345)); + CHECK(Allowed("775", 8, 0775)); + CHECK(Allowed("+775", 8, 0775)); + CHECK(Allowed("-775", 8, -0775)); + CHECK(Allowed("cAfE", 16, 0xCAFE)); + CHECK(Allowed("+cAfE", 16, +0xCAFE)); + CHECK(Allowed("-cAfE", 16, -0xCAFE)); + CHECK(Allowed("-0", 10, 0)); + + CHECK(Fails("", 10)); + CHECK(Fails("\n", 10)); + CHECK(Fails(" ", 10)); + CHECK(Fails("0\n", 10)); + CHECK(Fails("0 ", 10)); + CHECK(Fails("\n1", 10)); + CHECK(Fails(" 1", 10)); + CHECK(Fails("\n2 ", 10)); + CHECK(Fails(" 2\n", 10)); + CHECK(Fails("0b11", 2)); + CHECK(Fails("0x17", 16)); + CHECK(Fails("!!!!!!111one1111one1!1!1!!", 10)); + CHECK(Fails("3.1416", 10)); + CHECK(Fails("6.022e23", 10)); + CHECK(Fails("1e3", 10)); + CHECK(Fails(".25", 10)); + CHECK(Fails(".25e2", 10)); + CHECK(Fails("1_000_000", 10)); + CHECK(Fails("3n", 10)); + CHECK(Fails("-0x3", 10)); + CHECK(Fails("Infinity", 10)); + CHECK(Fails("555", 4)); + CHECK(Fails("fff", 15)); + + return true; +} + +template <size_t N> +inline bool Allowed(const char (&str)[N], unsigned radix, int64_t expected) { + JS::BigInt* bi = JS::SimpleStringToBigInt(cx, {str, N - 1}, radix); + CHECK(bi); + CHECK(JS::ToBigInt64(bi) == expected); + return true; +} + +template <size_t N> +inline bool Fails(const char (&str)[N], unsigned radix) { + JS::BigInt* bi = JS::SimpleStringToBigInt(cx, {str, N - 1}, radix); + CHECK_NULL(bi); + CHECK(JS_IsExceptionPending(cx)); + + JS::ExceptionStack exnStack(cx); + CHECK(JS::StealPendingExceptionStack(cx, &exnStack)); + + JS::ErrorReportBuilder report(cx); + CHECK(report.init(cx, exnStack, JS::ErrorReportBuilder::NoSideEffects)); + CHECK(report.report()->exnType == JSEXN_SYNTAXERR); + CHECK(report.report()->errorNumber == JSMSG_BIGINT_INVALID_SYNTAX); + + CHECK(!JS_IsExceptionPending(cx)); + + return true; +} +END_TEST(testSimpleStringToBigInt_AcceptedInput) + +BEGIN_TEST(testSimpleStringToBigInt_AllPossibleDigits) { + const char allPossible[] = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; + JS::BigInt* bi = + JS::SimpleStringToBigInt(cx, mozilla::MakeStringSpan(allPossible), 36); + CHECK(bi); + JS::Rooted<JS::Value> val(cx, JS::BigIntValue(bi)); + JS::Rooted<JSString*> str(cx, JS::ToString(cx, val)); + CHECK(str); + + // Answer calculated using Python: + // int('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890', 36) + // Do not trust online base-36 calculators for values > UINT32_MAX! + bool match; + CHECK( + JS_StringEqualsLiteral(cx, str, + "8870050151210747660007771095260505028056221996735" + "67534007158336222790086855213834764150805438340", + &match)); + CHECK(match); + return true; +} +END_TEST(testSimpleStringToBigInt_AllPossibleDigits) + +BEGIN_TEST(testSimpleStringToBigInt_RadixOutOfRange) { + CHECK(RadixOutOfRange(1)); + CHECK(RadixOutOfRange(37)); + return true; +} + +inline bool RadixOutOfRange(unsigned radix) { + JS::BigInt* bi = + JS::SimpleStringToBigInt(cx, mozilla::MakeStringSpan("1"), radix); + CHECK_NULL(bi); + CHECK(JS_IsExceptionPending(cx)); + + JS::ExceptionStack exnStack(cx); + CHECK(JS::StealPendingExceptionStack(cx, &exnStack)); + + JS::ErrorReportBuilder report(cx); + CHECK(report.init(cx, exnStack, JS::ErrorReportBuilder::NoSideEffects)); + CHECK(report.report()->exnType == JSEXN_RANGEERR); + CHECK(report.report()->errorNumber == JSMSG_BAD_RADIX); + + CHECK(!JS_IsExceptionPending(cx)); + + return true; +} +END_TEST(testSimpleStringToBigInt_RadixOutOfRange) + +BEGIN_TEST(testToBigInt_Undefined) { + JS::Rooted<JS::Value> v(cx, JS::UndefinedValue()); + JS::BigInt* bi = JS::ToBigInt(cx, v); + CHECK_NULL(bi); + CHECK(JS_IsExceptionPending(cx)); + JS_ClearPendingException(cx); + return true; +} +END_TEST(testToBigInt_Undefined) + +BEGIN_TEST(testToBigInt_Null) { + JS::Rooted<JS::Value> v(cx, JS::NullValue()); + JS::BigInt* bi = JS::ToBigInt(cx, v); + CHECK_NULL(bi); + CHECK(JS_IsExceptionPending(cx)); + JS_ClearPendingException(cx); + return true; +} +END_TEST(testToBigInt_Null) + +BEGIN_TEST(testToBigInt_Boolean) { + JS::Rooted<JS::Value> v(cx, JS::TrueValue()); + JS::BigInt* bi = JS::ToBigInt(cx, v); + CHECK(bi); + CHECK(JS::ToBigInt64(bi) == 1); + + v = JS::FalseValue(); + bi = JS::ToBigInt(cx, v); + CHECK(bi); + CHECK(JS::ToBigInt64(bi) == 0); + + return true; +} +END_TEST(testToBigInt_Boolean) + +BEGIN_TEST(testToBigInt_BigInt) { + JS::Rooted<JS::Value> v(cx); + EVAL("42n", &v); + JS::BigInt* bi = JS::ToBigInt(cx, v); + CHECK(bi); + CHECK(JS::ToBigInt64(bi) == 42); + return true; +} +END_TEST(testToBigInt_BigInt) + +BEGIN_TEST(testToBigInt_Number) { + JS::Rooted<JS::Value> v(cx, JS::Int32Value(42)); + JS::BigInt* bi = JS::ToBigInt(cx, v); + CHECK_NULL(bi); + CHECK(JS_IsExceptionPending(cx)); + JS_ClearPendingException(cx); + return true; +} +END_TEST(testToBigInt_Number) + +BEGIN_TEST(testToBigInt_String) { + JS::Rooted<JS::Value> v(cx); + EVAL("'42'", &v); + JS::BigInt* bi = JS::ToBigInt(cx, v); + CHECK(bi); + CHECK(JS::ToBigInt64(bi) == 42); + return true; +} +END_TEST(testToBigInt_String) + +BEGIN_TEST(testToBigInt_Symbol) { + JS::Rooted<JS::Value> v(cx); + EVAL("Symbol.toStringTag", &v); + JS::BigInt* bi = JS::ToBigInt(cx, v); + CHECK_NULL(bi); + CHECK(JS_IsExceptionPending(cx)); + JS_ClearPendingException(cx); + return true; +} +END_TEST(testToBigInt_Symbol) + +BEGIN_TEST(testBigIntToNumber) { + JS::BigInt* bi = JS::NumberToBigInt(cx, 0); + CHECK(bi); + int32_t result; + CHECK(mozilla::NumberIsInt32(JS::BigIntToNumber(bi), &result)); + CHECK_EQUAL(result, 0); + + bi = JS::NumberToBigInt(cx, 100); + CHECK(bi); + CHECK(JS::BigIntToNumber(bi) == 100); + + bi = JS::NumberToBigInt(cx, -100); + CHECK(bi); + CHECK(JS::BigIntToNumber(bi) == -100); + + JS::Rooted<JS::Value> v(cx); + + EVAL("18446744073709551615n", &v); + CHECK(v.isBigInt()); + double numberValue = JS::BigIntToNumber(v.toBigInt()); + EVAL("Number(18446744073709551615n)", &v); + CHECK(v.isNumber()); + CHECK(numberValue == v.toNumber()); + + EVAL((std::string(500, '9') + "n").c_str(), &v); + CHECK(v.isBigInt()); + CHECK(JS::BigIntToNumber(v.toBigInt()) == INFINITY); + + return true; +} +END_TEST(testBigIntToNumber) + +BEGIN_TEST(testBigIntIsNegative) { + JS::BigInt* bi = JS::NumberToBigInt(cx, 0); + CHECK(bi); + CHECK(!JS::BigIntIsNegative(bi)); + + bi = JS::NumberToBigInt(cx, 100); + CHECK(bi); + CHECK(!JS::BigIntIsNegative(bi)); + + bi = JS::NumberToBigInt(cx, -100); + CHECK(bi); + CHECK(JS::BigIntIsNegative(bi)); + + return true; +} +END_TEST(testBigIntIsNegative) + +#define GENERATE_INTTYPE_TEST(bits) \ + BEGIN_TEST(testBigIntFits_Int##bits) { \ + int64_t in = INT##bits##_MIN; \ + JS::BigInt* bi = JS::NumberToBigInt(cx, in); \ + CHECK(bi); \ + int##bits##_t i; \ + CHECK(JS::BigIntFits(bi, &i)); \ + CHECK_EQUAL(i, in); \ + \ + in = int64_t(INT##bits##_MIN) - 1; \ + bi = JS::NumberToBigInt(cx, in); \ + CHECK(bi); \ + CHECK(!JS::BigIntFits(bi, &i)); \ + \ + in = INT64_MIN; \ + bi = JS::NumberToBigInt(cx, in); \ + CHECK(bi); \ + CHECK(!JS::BigIntFits(bi, &i)); \ + \ + in = INT##bits##_MAX; \ + bi = JS::NumberToBigInt(cx, in); \ + CHECK(bi); \ + CHECK(JS::BigIntFits(bi, &i)); \ + CHECK_EQUAL(i, in); \ + \ + in = int64_t(INT##bits##_MAX) + 1; \ + bi = JS::NumberToBigInt(cx, in); \ + CHECK(bi); \ + CHECK(!JS::BigIntFits(bi, &i)); \ + \ + in = INT64_MAX; \ + bi = JS::NumberToBigInt(cx, in); \ + CHECK(bi); \ + CHECK(!JS::BigIntFits(bi, &i)); \ + \ + uint64_t uin = 0; \ + bi = JS::NumberToBigInt(cx, uin); \ + CHECK(bi); \ + uint##bits##_t u; \ + CHECK(JS::BigIntFits(bi, &u)); \ + CHECK_EQUAL(u, uin); \ + \ + uin = UINT##bits##_MAX; \ + bi = JS::NumberToBigInt(cx, uin); \ + CHECK(bi); \ + CHECK(JS::BigIntFits(bi, &u)); \ + CHECK_EQUAL(u, uin); \ + \ + uin = uint64_t(UINT##bits##_MAX) + 1; \ + bi = JS::NumberToBigInt(cx, uin); \ + CHECK(bi); \ + CHECK(!JS::BigIntFits(bi, &u)); \ + \ + uin = UINT64_MAX; \ + bi = JS::NumberToBigInt(cx, uin); \ + CHECK(bi); \ + CHECK(!JS::BigIntFits(bi, &u)); \ + \ + return true; \ + } \ + END_TEST(testBigIntFits_Int##bits) + +GENERATE_INTTYPE_TEST(8); +GENERATE_INTTYPE_TEST(16); +GENERATE_INTTYPE_TEST(32); + +#undef GENERATE_INTTYPE_TEST + +BEGIN_TEST(testBigIntFits_Int64) { + int64_t in = INT64_MIN; + JS::BigInt* bi = JS::NumberToBigInt(cx, in); + CHECK(bi); + int64_t i; + CHECK(JS::BigIntFits(bi, &i)); + CHECK_EQUAL(i, in); + + in = INT64_MAX; + bi = JS::NumberToBigInt(cx, in); + CHECK(bi); + CHECK(JS::BigIntFits(bi, &i)); + CHECK_EQUAL(i, in); + + JS::RootedValue v(cx); + + EVAL((std::string(500, '9') + "n").c_str(), &v); + CHECK(v.isBigInt()); + CHECK(!JS::BigIntFits(v.toBigInt(), &i)); + + EVAL(("-" + std::string(500, '9') + "n").c_str(), &v); + CHECK(v.isBigInt()); + CHECK(!JS::BigIntFits(v.toBigInt(), &i)); + + return true; +} +END_TEST(testBigIntFits_Int64) + +BEGIN_TEST(testBigIntFits_Uint64) { + uint64_t uin = 0; + JS::BigInt* bi = JS::NumberToBigInt(cx, uin); + CHECK(bi); + uint64_t u; + CHECK(JS::BigIntFits(bi, &u)); + CHECK_EQUAL(u, uin); + + uin = UINT64_MAX; + bi = JS::NumberToBigInt(cx, uin); + CHECK(bi); + CHECK(JS::BigIntFits(bi, &u)); + CHECK_EQUAL(u, uin); + + JS::RootedValue v(cx); + + EVAL((std::string(500, '9') + "n").c_str(), &v); + CHECK(v.isBigInt()); + CHECK(!JS::BigIntFits(v.toBigInt(), &u)); + + return true; +} +END_TEST(testBigIntFits_Uint64) + +#define GENERATE_SIGNED_VALUE_TEST(type, tag, val) \ + BEGIN_TEST(testBigIntFits_##type##_##tag) { \ + int64_t v = val; \ + JS::BigInt* bi = JS::NumberToBigInt(cx, v); \ + CHECK(bi); \ + type result; \ + CHECK(JS::BigIntFits(bi, &result)); \ + CHECK_EQUAL(v, result); \ + return true; \ + } \ + END_TEST(testBigIntFits_##type##_##tag) + +#define GENERATE_UNSIGNED_VALUE_TEST(type, tag, val) \ + BEGIN_TEST(testBigIntFits_##type##_##tag) { \ + uint64_t v = val; \ + JS::BigInt* bi = JS::NumberToBigInt(cx, v); \ + CHECK(bi); \ + type result; \ + CHECK(JS::BigIntFits(bi, &result)); \ + CHECK_EQUAL(v, result); \ + return true; \ + } \ + END_TEST(testBigIntFits_##type##_##tag) + +GENERATE_SIGNED_VALUE_TEST(int, zero, 0); +GENERATE_SIGNED_VALUE_TEST(int, aValue, -42); +GENERATE_UNSIGNED_VALUE_TEST(unsigned, zero, 0); +GENERATE_UNSIGNED_VALUE_TEST(unsigned, aValue, 42); +GENERATE_SIGNED_VALUE_TEST(long, zero, 0); +GENERATE_SIGNED_VALUE_TEST(long, aValue, -42); +GENERATE_UNSIGNED_VALUE_TEST(uintptr_t, zero, 0); +GENERATE_UNSIGNED_VALUE_TEST(uintptr_t, aValue, 42); +GENERATE_UNSIGNED_VALUE_TEST(size_t, zero, 0); +GENERATE_UNSIGNED_VALUE_TEST(size_t, aValue, 42); + +#undef GENERATE_SIGNED_VALUE_TEST +#undef GENERATE_UNSIGNED_VALUE_TEST + +BEGIN_TEST(testBigIntFitsNumber) { + JS::BigInt* bi = JS::NumberToBigInt(cx, 0); + CHECK(bi); + double num; + CHECK(JS::BigIntFitsNumber(bi, &num)); + int32_t result; + CHECK(mozilla::NumberIsInt32(num, &result)); + CHECK_EQUAL(result, 0); + + bi = JS::NumberToBigInt(cx, 100); + CHECK(bi); + CHECK(JS::BigIntFitsNumber(bi, &num)); + CHECK(num == 100); + + bi = JS::NumberToBigInt(cx, -100); + CHECK(bi); + CHECK(JS::BigIntFitsNumber(bi, &num)); + CHECK(num == -100); + + JS::Rooted<JS::Value> v(cx); + + EVAL("BigInt(Number.MAX_SAFE_INTEGER)", &v); + CHECK(v.isBigInt()); + CHECK(JS::BigIntFitsNumber(v.toBigInt(), &num)); + + EVAL("BigInt(Number.MIN_SAFE_INTEGER)", &v); + CHECK(v.isBigInt()); + CHECK(JS::BigIntFitsNumber(v.toBigInt(), &num)); + + EVAL("BigInt(Number.MAX_SAFE_INTEGER) + 1n", &v); + CHECK(v.isBigInt()); + CHECK(!JS::BigIntFitsNumber(v.toBigInt(), &num)); + + EVAL("BigInt(Number.MIN_SAFE_INTEGER) - 1n", &v); + CHECK(v.isBigInt()); + CHECK(!JS::BigIntFitsNumber(v.toBigInt(), &num)); + + EVAL((std::string(500, '9') + "n").c_str(), &v); + CHECK(v.isBigInt()); + CHECK(!JS::BigIntFitsNumber(v.toBigInt(), &num)); + + EVAL(("-" + std::string(500, '9') + "n").c_str(), &v); + CHECK(v.isBigInt()); + CHECK(!JS::BigIntFitsNumber(v.toBigInt(), &num)); + + return true; +} +END_TEST(testBigIntFitsNumber) + +BEGIN_TEST(testBigIntToString) { + CHECK(Convert(12345, 10, "12345")); + CHECK(Convert(-12345, 10, "-12345")); + CHECK(Convert(0775, 8, "775")); + CHECK(Convert(-0775, 8, "-775")); + CHECK(Convert(0xCAFE, 16, "cafe")); + CHECK(Convert(-0xCAFE, 16, "-cafe")); + + return true; +} + +template <size_t N> +inline bool Convert(int64_t input, uint8_t radix, const char (&expected)[N]) { + JS::Rooted<JS::BigInt*> bi(cx, JS::NumberToBigInt(cx, input)); + CHECK(bi); + JS::Rooted<JSString*> str(cx, JS::BigIntToString(cx, bi, radix)); + CHECK(str); + + bool match; + CHECK(JS_StringEqualsLiteral(cx, str, expected, &match)); + CHECK(match); + + return true; +} +END_TEST(testBigIntToString) + +BEGIN_TEST(testBigIntToString_AllPossibleDigits) { + const char allPossible[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; + JS::Rooted<JS::BigInt*> bi( + cx, + JS::SimpleStringToBigInt(cx, mozilla::MakeStringSpan(allPossible), 36)); + CHECK(bi); + JS::Rooted<JSString*> str(cx, JS::BigIntToString(cx, bi, 36)); + CHECK(str); + + bool match; + CHECK(JS_StringEqualsLiteral(cx, str, "abcdefghijklmnopqrstuvwxyz1234567890", + &match)); + CHECK(match); + return true; +} +END_TEST(testBigIntToString_AllPossibleDigits) + +BEGIN_TEST(testBigIntToString_RadixOutOfRange) { + CHECK(RadixOutOfRange(1)); + CHECK(RadixOutOfRange(37)); + return true; +} + +inline bool RadixOutOfRange(uint8_t radix) { + JS::Rooted<JS::BigInt*> bi(cx, JS::NumberToBigInt(cx, 1)); + CHECK(bi); + JSString* s = JS::BigIntToString(cx, bi, radix); + CHECK_NULL(s); + CHECK(JS_IsExceptionPending(cx)); + + JS::ExceptionStack exnStack(cx); + CHECK(JS::StealPendingExceptionStack(cx, &exnStack)); + + JS::ErrorReportBuilder report(cx); + CHECK(report.init(cx, exnStack, JS::ErrorReportBuilder::NoSideEffects)); + CHECK(report.report()->exnType == JSEXN_RANGEERR); + CHECK(report.report()->errorNumber == JSMSG_BAD_RADIX); + + CHECK(!JS_IsExceptionPending(cx)); + + return true; +} +END_TEST(testBigIntToString_RadixOutOfRange) diff --git a/js/src/jsapi-tests/testBoundFunction.cpp b/js/src/jsapi-tests/testBoundFunction.cpp new file mode 100644 index 0000000000..74f51ffd35 --- /dev/null +++ b/js/src/jsapi-tests/testBoundFunction.cpp @@ -0,0 +1,32 @@ +/* -*- 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" + +BEGIN_TEST(testBoundFunction) { + EXEC("function foo() {}"); + JS::RootedValue foo(cx); + EVAL("foo", &foo); + JS::RootedValue bound(cx); + EVAL("foo.bind(1)", &bound); + + JS::Rooted<JSObject*> foofun(cx, &foo.toObject()); + JS::Rooted<JSObject*> boundfun(cx, &bound.toObject()); + + CHECK(!JS_ObjectIsBoundFunction(foofun)); + CHECK(JS_ObjectIsBoundFunction(boundfun)); + + CHECK(!JS_GetBoundFunctionTarget(foofun)); + JSObject* target = JS_GetBoundFunctionTarget(boundfun); + CHECK(!!target); + CHECK(JS_ObjectIsFunction(target)); + JS::RootedValue targetVal(cx, JS::ObjectValue(*target)); + CHECK_SAME(foo, targetVal); + + return true; +} +END_TEST(testBoundFunction) diff --git a/js/src/jsapi-tests/testBug604087.cpp b/js/src/jsapi-tests/testBug604087.cpp new file mode 100644 index 0000000000..a237a58a19 --- /dev/null +++ b/js/src/jsapi-tests/testBug604087.cpp @@ -0,0 +1,91 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + * + * Tests JS_TransplantObject + */ +/* 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 "js/friend/WindowProxy.h" // js::SetWindowProxyClass +#include "js/GlobalObject.h" // JS_NewGlobalObject +#include "js/Wrapper.h" +#include "js/WrapperCallbacks.h" +#include "jsapi-tests/tests.h" +#include "vm/JSObject.h" +#include "vm/ProxyObject.h" + +const JSClass OuterWrapperClass = PROXY_CLASS_DEF( + "Proxy", JSCLASS_HAS_RESERVED_SLOTS(1) /* additional class flags */); + +static JSObject* wrap(JSContext* cx, JS::HandleObject toWrap, + JS::HandleObject target) { + JSAutoRealm ar(cx, target); + JS::RootedObject wrapper(cx, toWrap); + if (!JS_WrapObject(cx, &wrapper)) { + return nullptr; + } + return wrapper; +} + +static void PreWrap(JSContext* cx, JS::HandleObject scope, + JS::HandleObject origObj, JS::HandleObject obj, + JS::HandleObject objectPassedToWrap, + JS::MutableHandleObject retObj) { + JS_GC(cx); + retObj.set(obj); +} + +static JSObject* Wrap(JSContext* cx, JS::HandleObject existing, + JS::HandleObject obj) { + return js::Wrapper::New(cx, obj, &js::CrossCompartmentWrapper::singleton); +} + +static const JSWrapObjectCallbacks WrapObjectCallbacks = {Wrap, PreWrap}; + +BEGIN_TEST(testBug604087) { + js::SetWindowProxyClass(cx, &OuterWrapperClass); + + js::WrapperOptions options; + options.setClass(&OuterWrapperClass); + JS::RootedObject outerObj( + cx, js::Wrapper::New(cx, global, &js::Wrapper::singleton, options)); + JS::RealmOptions globalOptions; + JS::RootedObject compartment2( + cx, JS_NewGlobalObject(cx, getGlobalClass(), nullptr, + JS::FireOnNewGlobalHook, globalOptions)); + CHECK(compartment2 != nullptr); + JS::RootedObject compartment3( + cx, JS_NewGlobalObject(cx, getGlobalClass(), nullptr, + JS::FireOnNewGlobalHook, globalOptions)); + CHECK(compartment3 != nullptr); + JS::RootedObject compartment4( + cx, JS_NewGlobalObject(cx, getGlobalClass(), nullptr, + JS::FireOnNewGlobalHook, globalOptions)); + CHECK(compartment4 != nullptr); + + JS::RootedObject c2wrapper(cx, wrap(cx, outerObj, compartment2)); + CHECK(c2wrapper); + c2wrapper->as<js::ProxyObject>().setReservedSlot(0, js::Int32Value(2)); + + JS::RootedObject c3wrapper(cx, wrap(cx, outerObj, compartment3)); + CHECK(c3wrapper); + c3wrapper->as<js::ProxyObject>().setReservedSlot(0, js::Int32Value(3)); + + JS::RootedObject c4wrapper(cx, wrap(cx, outerObj, compartment4)); + CHECK(c4wrapper); + c4wrapper->as<js::ProxyObject>().setReservedSlot(0, js::Int32Value(4)); + compartment4 = c4wrapper = nullptr; + + JS::RootedObject next(cx); + { + JSAutoRealm ar(cx, compartment2); + next = js::Wrapper::New(cx, compartment2, &js::Wrapper::singleton, options); + CHECK(next); + } + + JS_SetWrapObjectCallbacks(cx, &WrapObjectCallbacks); + CHECK(JS_TransplantObject(cx, outerObj, next)); + return true; +} +END_TEST(testBug604087) diff --git a/js/src/jsapi-tests/testCallArgs.cpp b/js/src/jsapi-tests/testCallArgs.cpp new file mode 100644 index 0000000000..07a1a83cfc --- /dev/null +++ b/js/src/jsapi-tests/testCallArgs.cpp @@ -0,0 +1,85 @@ +/* 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 "mozilla/Utf8.h" // mozilla::Utf8Unit + +#include "js/CompilationAndEvaluation.h" // JS::Evaluate +#include "js/PropertyAndElement.h" // JS_DefineFunction +#include "js/SourceText.h" // JS::Source{Ownership,Text} +#include "jsapi-tests/tests.h" + +static bool CustomNative(JSContext* cx, unsigned argc, JS::Value* vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + MOZ_RELEASE_ASSERT(!JS_IsExceptionPending(cx)); + + MOZ_RELEASE_ASSERT(!args.isConstructing()); + args.rval().setUndefined(); + MOZ_RELEASE_ASSERT(!args.isConstructing()); + + return true; +} + +BEGIN_TEST(testCallArgs_isConstructing_native) { + CHECK(JS_DefineFunction(cx, global, "customNative", CustomNative, 0, 0)); + + JS::CompileOptions opts(cx); + opts.setFileAndLine(__FILE__, __LINE__ + 4); + + JS::RootedValue result(cx); + + static const char code[] = "new customNative();"; + JS::SourceText<mozilla::Utf8Unit> srcBuf; + CHECK(srcBuf.init(cx, code, strlen(code), JS::SourceOwnership::Borrowed)); + + CHECK(!JS::Evaluate(cx, opts, srcBuf, &result)); + + CHECK(JS_IsExceptionPending(cx)); + JS_ClearPendingException(cx); + + EVAL("customNative();", &result); + CHECK(result.isUndefined()); + + return true; +} +END_TEST(testCallArgs_isConstructing_native) + +static bool CustomConstructor(JSContext* cx, unsigned argc, JS::Value* vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + MOZ_RELEASE_ASSERT(!JS_IsExceptionPending(cx)); + + if (args.isConstructing()) { + JSObject* obj = JS_NewPlainObject(cx); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + + MOZ_RELEASE_ASSERT(args.isConstructing()); + } else { + args.rval().setUndefined(); + + MOZ_RELEASE_ASSERT(!args.isConstructing()); + } + + return true; +} + +BEGIN_TEST(testCallArgs_isConstructing_constructor) { + CHECK(JS_DefineFunction(cx, global, "customConstructor", CustomConstructor, 0, + JSFUN_CONSTRUCTOR)); + + JS::RootedValue result(cx); + + EVAL("new customConstructor();", &result); + CHECK(result.isObject()); + + EVAL("customConstructor();", &result); + CHECK(result.isUndefined()); + + return true; +} +END_TEST(testCallArgs_isConstructing_constructor) diff --git a/js/src/jsapi-tests/testCallNonGenericMethodOnProxy.cpp b/js/src/jsapi-tests/testCallNonGenericMethodOnProxy.cpp new file mode 100644 index 0000000000..54244ea768 --- /dev/null +++ b/js/src/jsapi-tests/testCallNonGenericMethodOnProxy.cpp @@ -0,0 +1,87 @@ +/* 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 "js/CallAndConstruct.h" +#include "js/GlobalObject.h" +#include "js/Object.h" // JS::GetClass, JS::GetReservedSlot +#include "jsapi-tests/tests.h" + +using namespace JS; + +static const JSClass CustomClass = {"CustomClass", + JSCLASS_HAS_RESERVED_SLOTS(1)}; + +static const uint32_t CUSTOM_SLOT = 0; + +static bool IsCustomClass(JS::Handle<JS::Value> v) { + return v.isObject() && JS::GetClass(&v.toObject()) == &CustomClass; +} + +static bool CustomMethodImpl(JSContext* cx, const CallArgs& args) { + MOZ_RELEASE_ASSERT(IsCustomClass(args.thisv())); + args.rval().set(JS::GetReservedSlot(&args.thisv().toObject(), CUSTOM_SLOT)); + return true; +} + +static bool CustomMethod(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, IsCustomClass, CustomMethodImpl, args); +} + +BEGIN_TEST(test_CallNonGenericMethodOnProxy) { + // Create the first global object and compartment + JS::RealmOptions options; + JS::RootedObject globalA( + cx, JS_NewGlobalObject(cx, getGlobalClass(), nullptr, + JS::FireOnNewGlobalHook, options)); + CHECK(globalA); + + JS::RootedObject customA(cx, JS_NewObject(cx, &CustomClass)); + CHECK(customA); + JS_SetReservedSlot(customA, CUSTOM_SLOT, Int32Value(17)); + + JS::RootedFunction customMethodA( + cx, JS_NewFunction(cx, CustomMethod, 0, 0, "customMethodA")); + CHECK(customMethodA); + + JS::RootedValue rval(cx); + CHECK(JS_CallFunction(cx, customA, customMethodA, + JS::HandleValueArray::empty(), &rval)); + CHECK_SAME(rval, Int32Value(17)); + + // Now create the second global object and compartment... + { + JS::RealmOptions options; + JS::RootedObject globalB( + cx, JS_NewGlobalObject(cx, getGlobalClass(), nullptr, + JS::FireOnNewGlobalHook, options)); + CHECK(globalB); + + // ...and enter it. + JSAutoRealm enter(cx, globalB); + JS::RootedObject customB(cx, JS_NewObject(cx, &CustomClass)); + CHECK(customB); + JS_SetReservedSlot(customB, CUSTOM_SLOT, Int32Value(42)); + + JS::RootedFunction customMethodB( + cx, JS_NewFunction(cx, CustomMethod, 0, 0, "customMethodB")); + CHECK(customMethodB); + + JS::RootedValue rval(cx); + CHECK(JS_CallFunction(cx, customB, customMethodB, + JS::HandleValueArray::empty(), &rval)); + CHECK_SAME(rval, Int32Value(42)); + + JS::RootedObject wrappedCustomA(cx, customA); + CHECK(JS_WrapObject(cx, &wrappedCustomA)); + + JS::RootedValue rval2(cx); + CHECK(JS_CallFunction(cx, wrappedCustomA, customMethodB, + JS::HandleValueArray::empty(), &rval2)); + CHECK_SAME(rval, Int32Value(42)); + } + + return true; +} +END_TEST(test_CallNonGenericMethodOnProxy) diff --git a/js/src/jsapi-tests/testCharacterEncoding.cpp b/js/src/jsapi-tests/testCharacterEncoding.cpp new file mode 100644 index 0000000000..c15b1c978b --- /dev/null +++ b/js/src/jsapi-tests/testCharacterEncoding.cpp @@ -0,0 +1,257 @@ +/* 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 "mozilla/TextUtils.h" + +#include <clocale> +#include <cstring> +#include <cwchar> +#include <initializer_list> +#include <iterator> +#include <string_view> + +#include "js/CharacterEncoding.h" +#include "jsapi-tests/tests.h" + +static bool EqualsIgnoreCase(const char* xs, const char* ys) { + while (*xs && *ys) { + char x = *xs++; + char y = *ys++; + + // Convert both to lower-case. + if (mozilla::IsAsciiAlpha(x) && mozilla::IsAsciiAlpha(y)) { + x |= 0x20; + y |= 0x20; + } + + // Fail if the characters aren't the same. + if (x != y) { + return false; + } + } + + // Both strings must be read to the end. + return !*xs && !*ys; +} + +class ToUTF8Locale { + const char* previousLocale_ = nullptr; + bool supported_ = false; + + public: + ToUTF8Locale() { + // Store the old locale so we can reset it in the destructor. + previousLocale_ = std::setlocale(LC_ALL, nullptr); + + // Query the system default locale. + const char* defaultLocale = std::setlocale(LC_ALL, ""); + if (!defaultLocale) { + // std::setlocale returns nullptr on failure. + return; + } + + // Switch the default locale to be UTF-8 aware. + const char* newLocale = std::setlocale(LC_ALL, "en_US.UTF-8"); + if (!newLocale) { + // std::setlocale returns nullptr on failure. + return; + } + + const char* defaultCodepage = std::strchr(defaultLocale, '.'); + const char* newCodepage = std::strchr(newLocale, '.'); + + // Return if either the default or new locale don't contain a code-page. + if (!defaultCodepage || !newCodepage) { + return; + } + + // Skip past the '.'. + defaultCodepage++; + newCodepage++; + + // UTF-8 is supported when the default locale and new locale support it: + // + // The default locale needs to support UTF-8, because this test is compiled + // using the default locale. + // + // The new locale needs to support UTF-8 to ensure UTF-8 encoding works at + // runtime. + supported_ = EqualsIgnoreCase(defaultCodepage, "UTF-8") && + EqualsIgnoreCase(newCodepage, "UTF-8"); + } + + bool supported() const { return supported_; } + + ~ToUTF8Locale() { + // Restore the previous locale. + if (previousLocale_) { + std::setlocale(LC_ALL, previousLocale_); + } + } +}; + +BEGIN_TEST(testCharacterEncoding_narrow_to_utf8) { + // Assume the narrow charset is ASCII-compatible. ASCII to UTF-8 conversion is + // a no-op. + for (std::string_view string : { + "", + "a", + "abc", + "abc\0def", + }) { + auto utf8 = JS::EncodeNarrowToUtf8(cx, string.data()); + CHECK(utf8 != nullptr); + CHECK_EQUAL(std::strlen(utf8.get()), string.length()); + CHECK(utf8.get() == string); + } + return true; +} +END_TEST(testCharacterEncoding_narrow_to_utf8) + +BEGIN_TEST(testCharacterEncoding_wide_to_utf8) { + // Assume the wide charset is ASCII-compatible. ASCII to UTF-8 conversion is + // a no-op. + for (std::wstring_view string : { + L"", + L"a", + L"abc", + L"abc\0def", + }) { + auto utf8 = JS::EncodeWideToUtf8(cx, string.data()); + CHECK(utf8 != nullptr); + CHECK_EQUAL(std::strlen(utf8.get()), string.length()); + CHECK(std::equal( + string.begin(), string.end(), utf8.get(), + [](wchar_t x, char y) { return char32_t(x) == char32_t(y); })); + } + return true; +} +END_TEST(testCharacterEncoding_wide_to_utf8) + +BEGIN_TEST(testCharacterEncoding_wide_to_utf8_non_ascii) { + // Change the locale to be UTF-8 aware for the emoji string. + ToUTF8Locale utf8locale; + + // Skip this test if UTF-8 isn't supported on this system. + if (!utf8locale.supported()) { + return true; + } + + { + std::wstring_view string = L"ä"; + auto utf8 = JS::EncodeWideToUtf8(cx, string.data()); + CHECK(utf8 != nullptr); + + CHECK_EQUAL(std::strlen(utf8.get()), 2U); + CHECK_EQUAL(utf8[0], char(0xC3)); + CHECK_EQUAL(utf8[1], char(0xA4)); + } + { + std::wstring_view string = L"💩"; + auto utf8 = JS::EncodeWideToUtf8(cx, string.data()); + CHECK(utf8 != nullptr); + + CHECK_EQUAL(std::strlen(utf8.get()), 4U); + CHECK_EQUAL(utf8[0], char(0xF0)); + CHECK_EQUAL(utf8[1], char(0x9F)); + CHECK_EQUAL(utf8[2], char(0x92)); + CHECK_EQUAL(utf8[3], char(0xA9)); + } + return true; +} +END_TEST(testCharacterEncoding_wide_to_utf8_non_ascii) + +BEGIN_TEST(testCharacterEncoding_utf8_to_narrow) { + // Assume the narrow charset is ASCII-compatible. ASCII to UTF-8 conversion is + // a no-op. + for (std::string_view string : { + "", + "a", + "abc", + "abc\0def", + }) { + auto narrow = JS::EncodeUtf8ToNarrow(cx, string.data()); + CHECK(narrow != nullptr); + CHECK_EQUAL(std::strlen(narrow.get()), string.length()); + CHECK(narrow.get() == string); + } + return true; +} +END_TEST(testCharacterEncoding_utf8_to_narrow) + +BEGIN_TEST(testCharacterEncoding_utf8_to_wide) { + // Assume the wide charset is ASCII-compatible. ASCII to UTF-8 conversion is + // a no-op. + for (std::string_view string : { + "", + "a", + "abc", + "abc\0def", + }) { + auto wide = JS::EncodeUtf8ToWide(cx, string.data()); + CHECK(wide != nullptr); + CHECK_EQUAL(std::wcslen(wide.get()), string.length()); + CHECK(std::equal( + string.begin(), string.end(), wide.get(), + [](char x, wchar_t y) { return char32_t(x) == char32_t(y); })); + } + return true; +} +END_TEST(testCharacterEncoding_utf8_to_wide) + +BEGIN_TEST(testCharacterEncoding_narrow_roundtrip) { + // Change the locale to be UTF-8 aware for the emoji string. + ToUTF8Locale utf8locale; + + // Skip this test if UTF-8 isn't supported on this system. + if (!utf8locale.supported()) { + return true; + } + + for (std::string_view string : { + "", + "a", + "abc", + "ä", + "💩", + }) { + auto utf8 = JS::EncodeNarrowToUtf8(cx, string.data()); + CHECK(utf8 != nullptr); + + auto narrow = JS::EncodeUtf8ToNarrow(cx, utf8.get()); + CHECK(narrow != nullptr); + + CHECK(narrow.get() == string); + } + return true; +} +END_TEST(testCharacterEncoding_narrow_roundtrip) + +BEGIN_TEST(testCharacterEncoding_wide_roundtrip) { + // Change the locale to be UTF-8 aware for the emoji string. + ToUTF8Locale utf8locale; + + // Skip this test if UTF-8 isn't supported on this system. + if (!utf8locale.supported()) { + return true; + } + + for (std::wstring_view string : { + L"", + L"a", + L"abc", + L"ä", + L"💩", + }) { + auto utf8 = JS::EncodeWideToUtf8(cx, string.data()); + CHECK(utf8 != nullptr); + + auto wide = JS::EncodeUtf8ToWide(cx, utf8.get()); + CHECK(wide != nullptr); + + CHECK(wide.get() == string); + } + return true; +} +END_TEST(testCharacterEncoding_wide_roundtrip) diff --git a/js/src/jsapi-tests/testChromeBuffer.cpp b/js/src/jsapi-tests/testChromeBuffer.cpp new file mode 100644 index 0000000000..91eafdcfbd --- /dev/null +++ b/js/src/jsapi-tests/testChromeBuffer.cpp @@ -0,0 +1,269 @@ +/* -*- 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 "mozilla/Utf8.h" // mozilla::Utf8Unit + +#include "js/CallAndConstruct.h" // JS_CallFunctionValue +#include "js/CompilationAndEvaluation.h" // JS::CompileFunction +#include "js/ContextOptions.h" +#include "js/GlobalObject.h" // JS_NewGlobalObject +#include "js/PropertyAndElement.h" // JS_DefineProperty +#include "js/SourceText.h" // JS::Source{Ownership,Text} +#include "jsapi-tests/tests.h" +#include "util/Text.h" + +static TestJSPrincipals system_principals(1); + +static const JSClass global_class = {"global", + JSCLASS_IS_GLOBAL | JSCLASS_GLOBAL_FLAGS, + &JS::DefaultGlobalClassOps}; + +static JS::PersistentRootedObject trusted_glob; +static JS::PersistentRootedObject trusted_fun; + +static bool CallTrusted(JSContext* cx, unsigned argc, JS::Value* vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + bool ok = false; + { + JSAutoRealm ar(cx, trusted_glob); + JS::RootedValue funVal(cx, JS::ObjectValue(*trusted_fun)); + ok = JS_CallFunctionValue(cx, nullptr, funVal, + JS::HandleValueArray::empty(), args.rval()); + } + return ok; +} + +BEGIN_TEST(testChromeBuffer) { + JS_SetTrustedPrincipals(cx, &system_principals); + + JS::RealmOptions options; + trusted_glob.init(cx, + JS_NewGlobalObject(cx, &global_class, &system_principals, + JS::FireOnNewGlobalHook, options)); + CHECK(trusted_glob); + + JS::RootedFunction fun(cx); + + /* + * Check that, even after untrusted content has exhausted the stack, code + * compiled with "trusted principals" can run using reserved trusted-only + * buffer space. + */ + { + // Disable the JIT because if we don't this test fails. See bug 1160414. + uint32_t oldBaselineInterpreterEnabled; + CHECK(JS_GetGlobalJitCompilerOption( + cx, JSJITCOMPILER_BASELINE_INTERPRETER_ENABLE, + &oldBaselineInterpreterEnabled)); + JS_SetGlobalJitCompilerOption(cx, JSJITCOMPILER_BASELINE_INTERPRETER_ENABLE, + 0); + uint32_t oldBaselineJitEnabled; + CHECK(JS_GetGlobalJitCompilerOption(cx, JSJITCOMPILER_BASELINE_ENABLE, + &oldBaselineJitEnabled)); + JS_SetGlobalJitCompilerOption(cx, JSJITCOMPILER_BASELINE_ENABLE, 0); +#ifdef ENABLE_PORTABLE_BASELINE_INTERP + uint32_t oldPortableBaselineInterpreterEnabled; + CHECK(JS_GetGlobalJitCompilerOption( + cx, JSJITCOMPILER_PORTABLE_BASELINE_ENABLE, + &oldPortableBaselineInterpreterEnabled)); + JS_SetGlobalJitCompilerOption(cx, JSJITCOMPILER_PORTABLE_BASELINE_ENABLE, + 0); +#endif + { + JSAutoRealm ar(cx, trusted_glob); + const char* paramName = "x"; + static const char bytes[] = "return x ? 1 + trusted(x-1) : 0"; + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + CHECK(srcBuf.init(cx, bytes, js_strlen(bytes), + JS::SourceOwnership::Borrowed)); + + JS::CompileOptions options(cx); + options.setFileAndLine("", 0); + + JS::RootedObjectVector emptyScopeChain(cx); + fun = JS::CompileFunction(cx, emptyScopeChain, options, "trusted", 1, + ¶mName, srcBuf); + CHECK(fun); + CHECK(JS_DefineProperty(cx, trusted_glob, "trusted", fun, + JSPROP_ENUMERATE)); + trusted_fun.init(cx, JS_GetFunctionObject(fun)); + } + + JS::RootedValue v(cx, JS::ObjectValue(*trusted_fun)); + CHECK(JS_WrapValue(cx, &v)); + + const char* paramName = "trusted"; + static const char bytes[] = + "try { " + " return untrusted(trusted); " + "} catch (e) { " + " try { " + " return trusted(100); " + " } catch(e) { " + " return -1; " + " } " + "} "; + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + CHECK(srcBuf.init(cx, bytes, js_strlen(bytes), + JS::SourceOwnership::Borrowed)); + + JS::CompileOptions options(cx); + options.setFileAndLine("", 0); + + JS::RootedObjectVector emptyScopeChain(cx); + fun = JS::CompileFunction(cx, emptyScopeChain, options, "untrusted", 1, + ¶mName, srcBuf); + CHECK(fun); + CHECK(JS_DefineProperty(cx, global, "untrusted", fun, JSPROP_ENUMERATE)); + + JS::RootedValue rval(cx); + CHECK(JS_CallFunction(cx, nullptr, fun, JS::HandleValueArray(v), &rval)); + CHECK(rval.toInt32() == 100); + JS_SetGlobalJitCompilerOption(cx, JSJITCOMPILER_BASELINE_INTERPRETER_ENABLE, + oldBaselineInterpreterEnabled); + JS_SetGlobalJitCompilerOption(cx, JSJITCOMPILER_BASELINE_ENABLE, + oldBaselineJitEnabled); +#ifdef ENABLE_PORTABLE_BASELINE_INTERP + JS_SetGlobalJitCompilerOption(cx, JSJITCOMPILER_PORTABLE_BASELINE_ENABLE, + oldPortableBaselineInterpreterEnabled); +#endif + } + + /* + * Check that content called from chrome in the reserved-buffer space + * immediately ooms. + */ + { + { + JSAutoRealm ar(cx, trusted_glob); + + const char* paramName = "untrusted"; + static const char bytes[] = + "try { " + " untrusted(); " + "} catch (e) { " + " /* " + " * Careful! We must not reenter JS " + " * that might try to push a frame. " + " */ " + " return 'From trusted: ' + " + " e.name + ': ' + e.message; " + "} "; + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + CHECK(srcBuf.init(cx, bytes, js_strlen(bytes), + JS::SourceOwnership::Borrowed)); + + JS::CompileOptions options(cx); + options.setFileAndLine("", 0); + + JS::RootedObjectVector emptyScopeChain(cx); + fun = JS::CompileFunction(cx, emptyScopeChain, options, "trusted", 1, + ¶mName, srcBuf); + CHECK(fun); + CHECK(JS_DefineProperty(cx, trusted_glob, "trusted", fun, + JSPROP_ENUMERATE)); + trusted_fun = JS_GetFunctionObject(fun); + } + + JS::RootedValue v(cx, JS::ObjectValue(*trusted_fun)); + CHECK(JS_WrapValue(cx, &v)); + + const char* paramName = "trusted"; + static const char bytes[] = + "try { " + " return untrusted(trusted); " + "} catch (e) { " + " return trusted(untrusted); " + "} "; + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + CHECK(srcBuf.init(cx, bytes, js_strlen(bytes), + JS::SourceOwnership::Borrowed)); + + JS::CompileOptions options(cx); + options.setFileAndLine("", 0); + + JS::RootedObjectVector emptyScopeChain(cx); + fun = JS::CompileFunction(cx, emptyScopeChain, options, "untrusted", 1, + ¶mName, srcBuf); + CHECK(fun); + CHECK(JS_DefineProperty(cx, global, "untrusted", fun, JSPROP_ENUMERATE)); + + JS::RootedValue rval(cx); + CHECK(JS_CallFunction(cx, nullptr, fun, JS::HandleValueArray(v), &rval)); +#ifndef JS_SIMULATOR_ARM64 + // The ARM64 simulator does not share a common implementation with the other + // simulators, and has slightly different end-of-stack behavior. Instead of + // failing with "too much recursion," it executes one more function call and + // fails with a type error. This behavior is not incorrect. + bool match; + CHECK(JS_StringEqualsAscii( + cx, rval.toString(), "From trusted: InternalError: too much recursion", + &match)); + CHECK(match); +#endif + } + + { + { + JSAutoRealm ar(cx, trusted_glob); + + static const char bytes[] = "return 42"; + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + CHECK(srcBuf.init(cx, bytes, js_strlen(bytes), + JS::SourceOwnership::Borrowed)); + + JS::CompileOptions options(cx); + options.setFileAndLine("", 0); + + JS::RootedObjectVector emptyScopeChain(cx); + fun = JS::CompileFunction(cx, emptyScopeChain, options, "trusted", 0, + nullptr, srcBuf); + CHECK(fun); + CHECK(JS_DefineProperty(cx, trusted_glob, "trusted", fun, + JSPROP_ENUMERATE)); + trusted_fun = JS_GetFunctionObject(fun); + } + + JS::RootedFunction fun( + cx, JS_NewFunction(cx, CallTrusted, 0, 0, "callTrusted")); + JS::RootedObject callTrusted(cx, JS_GetFunctionObject(fun)); + + const char* paramName = "f"; + static const char bytes[] = + "try { " + " return untrusted(trusted); " + "} catch (e) { " + " return f(); " + "} "; + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + CHECK(srcBuf.init(cx, bytes, js_strlen(bytes), + JS::SourceOwnership::Borrowed)); + + JS::CompileOptions options(cx); + options.setFileAndLine("", 0); + + JS::RootedObjectVector emptyScopeChain(cx); + fun = JS::CompileFunction(cx, emptyScopeChain, options, "untrusted", 1, + ¶mName, srcBuf); + CHECK(fun); + CHECK(JS_DefineProperty(cx, global, "untrusted", fun, JSPROP_ENUMERATE)); + + JS::RootedValue arg(cx, JS::ObjectValue(*callTrusted)); + JS::RootedValue rval(cx); + CHECK(JS_CallFunction(cx, nullptr, fun, JS::HandleValueArray(arg), &rval)); + CHECK(rval.toInt32() == 42); + } + + return true; +} +END_TEST(testChromeBuffer) diff --git a/js/src/jsapi-tests/testCompileNonSyntactic.cpp b/js/src/jsapi-tests/testCompileNonSyntactic.cpp new file mode 100644 index 0000000000..13985d90bb --- /dev/null +++ b/js/src/jsapi-tests/testCompileNonSyntactic.cpp @@ -0,0 +1,68 @@ +/* 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 "mozilla/RefPtr.h" // RefPtr +#include "mozilla/Utf8.h" // mozilla::Utf8Unit + +#include <string_view> + +#include "js/CompilationAndEvaluation.h" // JS::Compile +#include "js/CompileOptions.h" // JS::CompileOptions, JS::InstantiateOptions +#include "js/experimental/JSStencil.h" // JS::Stencil, JS::InstantiateGlobalStencil + +#include "js/SourceText.h" // JS::Source{Ownership,Text} +#include "jsapi-tests/tests.h" +#include "vm/HelperThreads.h" +#include "vm/Monitor.h" +#include "vm/MutexIDs.h" + +using namespace JS; +using js::AutoLockMonitor; + +BEGIN_TEST(testCompileNonsyntactic) { + CHECK(testCompile(true)); + + CHECK(testCompile(false)); + return true; +} + +bool testCompile(bool nonSyntactic) { + static constexpr std::string_view src = "42\n"; + static constexpr std::u16string_view src_16 = u"42\n"; + + static_assert(src.length() == src_16.length(), + "Source buffers must be same length"); + + JS::CompileOptions options(cx); + options.setNonSyntacticScope(nonSyntactic); + + JS::SourceText<char16_t> buf16; + CHECK(buf16.init(cx, src_16.data(), src_16.length(), + JS::SourceOwnership::Borrowed)); + + JS::SourceText<mozilla::Utf8Unit> buf8; + CHECK(buf8.init(cx, src.data(), src.length(), JS::SourceOwnership::Borrowed)); + + JS::RootedScript script(cx); + + script = Compile(cx, options, buf16); + CHECK(script); + CHECK_EQUAL(script->hasNonSyntacticScope(), nonSyntactic); + + script = Compile(cx, options, buf8); + CHECK(script); + CHECK_EQUAL(script->hasNonSyntacticScope(), nonSyntactic); + + { + JS::SourceText<char16_t> srcBuf; + CHECK(srcBuf.init(cx, src_16.data(), src_16.length(), + JS::SourceOwnership::Borrowed)); + + script = Compile(cx, options, srcBuf); + CHECK(script); + CHECK_EQUAL(script->hasNonSyntacticScope(), nonSyntactic); + } + return true; +} +END_TEST(testCompileNonsyntactic); diff --git a/js/src/jsapi-tests/testCompileScript.cpp b/js/src/jsapi-tests/testCompileScript.cpp new file mode 100644 index 0000000000..45f5e8301a --- /dev/null +++ b/js/src/jsapi-tests/testCompileScript.cpp @@ -0,0 +1,241 @@ +/* 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 "mozilla/RefPtr.h" // RefPtr +#include "mozilla/ScopeExit.h" // MakeScopeExit +#include "mozilla/Utf8.h" // mozilla::Utf8Unit + +#include "frontend/CompilationStencil.h" // JS::Stencil +#include "js/CompileOptions.h" // JS::CompileOptions, JS::InstantiateOptions +#include "js/experimental/CompileScript.h" // JS::NewFrontendContext +#include "js/SourceText.h" // JS::Source{Ownership,Text} +#include "jsapi-tests/tests.h" +#include "vm/ErrorReporting.h" +#include "vm/JSONPrinter.h" // js::JSONPrinter + +using namespace JS; + +template <typename T> +static void dump(const T& data) { + js::Fprinter printer(stderr); + js::JSONPrinter json(printer, true); + + data->dump(json); + printer.put("\n"); +} + +BEGIN_TEST(testCompileScript) { + CHECK(testCompile()); + CHECK(testNonsyntacticCompile()); + CHECK(testCompileModule()); + CHECK(testPrepareForInstantiate()); + + return true; +} + +bool testCompile() { + static constexpr std::string_view src = "42\n"; + static constexpr std::u16string_view src_16 = u"42\n"; + + static_assert(src.length() == src_16.length(), + "Source buffers must be same length"); + + JS::CompileOptions options(cx); + + JS::FrontendContext* fc = JS::NewFrontendContext(); + CHECK(fc); + auto destroyFc = + mozilla::MakeScopeExit([fc] { JS::DestroyFrontendContext(fc); }); + + { // 16-bit characters + JS::SourceText<char16_t> buf16; + CHECK(buf16.init(cx, src_16.data(), src_16.length(), + JS::SourceOwnership::Borrowed)); + + JS::CompilationStorage compileStorage; + RefPtr<JS::Stencil> stencil = + CompileGlobalScriptToStencil(fc, options, buf16, compileStorage); + CHECK(stencil); + CHECK(stencil->scriptExtra.size() == 1); + CHECK(stencil->scriptExtra[0].extent.sourceStart == 0); + CHECK(stencil->scriptExtra[0].extent.sourceEnd == 3); + CHECK(stencil->scriptData.size() == 1); + CHECK(stencil->scriptData[0].hasSharedData()); // has generated bytecode + CHECK(stencil->scriptData[0].gcThingsLength == 1); + CHECK(compileStorage.hasInput()); + } + + { // 8-bit characters + JS::SourceText<mozilla::Utf8Unit> buf8; + CHECK( + buf8.init(cx, src.data(), src.length(), JS::SourceOwnership::Borrowed)); + + JS::CompilationStorage compileStorage; + RefPtr<JS::Stencil> stencil = + CompileGlobalScriptToStencil(fc, options, buf8, compileStorage); + CHECK(stencil); + CHECK(stencil->scriptExtra.size() == 1); + CHECK(stencil->scriptExtra[0].extent.sourceStart == 0); + CHECK(stencil->scriptExtra[0].extent.sourceEnd == 3); + CHECK(stencil->scriptData.size() == 1); + CHECK(stencil->scriptData[0].hasSharedData()); // has generated bytecode + CHECK(stencil->scriptData[0].gcThingsLength == 1); + CHECK(compileStorage.hasInput()); + } + + { // propagates failures + static constexpr std::string_view badSrc = "{ a: 1, b: 3\n"; + JS::SourceText<mozilla::Utf8Unit> srcBuf; + CHECK(srcBuf.init(cx, badSrc.data(), badSrc.length(), + JS::SourceOwnership::Borrowed)); + + JS::CompilationStorage compileStorage; + RefPtr<JS::Stencil> stencil = + CompileGlobalScriptToStencil(fc, options, srcBuf, compileStorage); + CHECK(!stencil); + CHECK(fc->maybeError().isSome()); + const js::CompileError& error = fc->maybeError().ref(); + CHECK(JSEXN_SYNTAXERR == error.exnType); + CHECK(error.lineno == 1); + CHECK(error.column.oneOriginValue() == 10); + } + + return true; +} + +bool testNonsyntacticCompile() { + const char* chars = + "function f() { return x; }" + "f();"; + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + CHECK(srcBuf.init(cx, chars, strlen(chars), JS::SourceOwnership::Borrowed)); + + JS::CompileOptions options(cx); + options.setNonSyntacticScope(true); + + JS::FrontendContext* fc = JS::NewFrontendContext(); + CHECK(fc); + auto destroyFc = + mozilla::MakeScopeExit([fc] { JS::DestroyFrontendContext(fc); }); + + JS::CompilationStorage compileStorage; + RefPtr<JS::Stencil> stencil = + CompileGlobalScriptToStencil(fc, options, srcBuf, compileStorage); + CHECK(stencil); + + JS::InstantiateOptions instantiateOptions(options); + JS::RootedScript script( + cx, JS::InstantiateGlobalStencil(cx, instantiateOptions, stencil)); + CHECK(script); + CHECK(script->hasNonSyntacticScope()); + + return true; +} + +bool testCompileModule() { + static constexpr std::string_view src = "import 'a'; 42\n"; + static constexpr std::u16string_view src_16 = u"import 'a'; 42\n"; + + static_assert(src.length() == src_16.length(), + "Source buffers must be same length"); + + JS::CompileOptions options(cx); + + JS::FrontendContext* fc = JS::NewFrontendContext(); + CHECK(fc); + auto destroyFc = + mozilla::MakeScopeExit([fc] { JS::DestroyFrontendContext(fc); }); + + { // 16-bit characters + JS::SourceText<char16_t> buf16; + CHECK(buf16.init(cx, src_16.data(), src_16.length(), + JS::SourceOwnership::Borrowed)); + + JS::CompilationStorage compileStorage; + RefPtr<JS::Stencil> stencil = + CompileModuleScriptToStencil(fc, options, buf16, compileStorage); + CHECK(stencil); + CHECK(stencil->isModule()); + CHECK(stencil->scriptExtra.size() == 1); + CHECK(stencil->scriptExtra[0].extent.sourceStart == 0); + CHECK(stencil->scriptExtra[0].extent.sourceEnd == 15); + CHECK(stencil->scriptData.size() == 1); + CHECK(stencil->scriptData[0].hasSharedData()); // has generated bytecode + CHECK(stencil->scriptData[0].gcThingsLength == 1); + CHECK(compileStorage.hasInput()); + } + + { // 8-bit characters + JS::SourceText<mozilla::Utf8Unit> buf8; + CHECK( + buf8.init(cx, src.data(), src.length(), JS::SourceOwnership::Borrowed)); + + JS::CompilationStorage compileStorage; + RefPtr<JS::Stencil> stencil = + CompileModuleScriptToStencil(fc, options, buf8, compileStorage); + CHECK(stencil); + CHECK(stencil->scriptExtra.size() == 1); + CHECK(stencil->scriptExtra[0].extent.sourceStart == 0); + CHECK(stencil->scriptExtra[0].extent.sourceEnd == 15); + CHECK(stencil->scriptData.size() == 1); + CHECK(stencil->scriptData[0].hasSharedData()); // has generated bytecode + CHECK(stencil->scriptData[0].gcThingsLength == 1); + CHECK(compileStorage.hasInput()); + } + + { // propagates failures + static constexpr std::string_view badSrc = "{ a: 1, b: 3\n"; + JS::SourceText<mozilla::Utf8Unit> srcBuf; + CHECK(srcBuf.init(cx, badSrc.data(), badSrc.length(), + JS::SourceOwnership::Borrowed)); + + JS::CompilationStorage compileStorage; + RefPtr<JS::Stencil> stencil = + CompileModuleScriptToStencil(fc, options, srcBuf, compileStorage); + CHECK(!stencil); + CHECK(fc->maybeError().isSome()); + const js::CompileError& error = fc->maybeError().ref(); + CHECK(JSEXN_SYNTAXERR == error.exnType); + CHECK(error.lineno == 1); + CHECK(error.column.oneOriginValue() == 10); + } + + return true; +} + +bool testPrepareForInstantiate() { + static constexpr std::u16string_view src_16 = + u"function f() { return {'field': 42};}; f()['field']\n"; + + JS::CompileOptions options(cx); + + JS::SourceText<char16_t> buf16; + CHECK(buf16.init(cx, src_16.data(), src_16.length(), + JS::SourceOwnership::Borrowed)); + + JS::FrontendContext* fc = JS::NewFrontendContext(); + CHECK(fc); + auto destroyFc = + mozilla::MakeScopeExit([fc] { JS::DestroyFrontendContext(fc); }); + + JS::CompilationStorage compileStorage; + RefPtr<JS::Stencil> stencil = + CompileGlobalScriptToStencil(fc, options, buf16, compileStorage); + CHECK(stencil); + CHECK(stencil->scriptData.size() == 2); + CHECK(stencil->scopeData.size() == 1); // function f + CHECK(stencil->parserAtomData.size() == 1); // 'field' + CHECK(compileStorage.hasInput()); + CHECK(compileStorage.getInput().atomCache.empty()); + + JS::InstantiationStorage storage; + CHECK(JS::PrepareForInstantiate(fc, *stencil, storage)); + CHECK(storage.isValid()); + // TODO storage.gcOutput_ is private, so there isn't a good way to check the + // scriptData and scopeData capacities + + return true; +} +END_TEST(testCompileScript); diff --git a/js/src/jsapi-tests/testCompileUtf8.cpp b/js/src/jsapi-tests/testCompileUtf8.cpp new file mode 100644 index 0000000000..8799e70201 --- /dev/null +++ b/js/src/jsapi-tests/testCompileUtf8.cpp @@ -0,0 +1,300 @@ +/* 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 "mozilla/ArrayUtils.h" +#include "mozilla/TextUtils.h" +#include "mozilla/Utf8.h" + +#include <cstring> + +#include "js/CharacterEncoding.h" +#include "js/CompilationAndEvaluation.h" // JS::Compile +#include "js/Exception.h" +#include "js/friend/ErrorMessages.h" // JSMSG_* +#include "js/SourceText.h" +#include "jsapi-tests/tests.h" +#include "util/Text.h" +#include "vm/ErrorReporting.h" + +using mozilla::ArrayEqual; +using mozilla::IsAsciiHexDigit; +using mozilla::Utf8Unit; + +BEGIN_TEST(testUtf8BadBytes) { + static const char badLeadingUnit[] = "var x = \x80"; + CHECK(testBadUtf8( + badLeadingUnit, JSMSG_BAD_LEADING_UTF8_UNIT, + [this](JS::ConstUTF8CharsZ message) { + const char* chars = message.c_str(); + CHECK(startsWith(chars, "0x80")); + CHECK(isBadLeadUnitMessage(chars)); + return true; + }, + "0x80")); + + static const char badSecondInTwoByte[] = "var x = \xDF\x20"; + CHECK(testBadUtf8( + badSecondInTwoByte, JSMSG_BAD_TRAILING_UTF8_UNIT, + [this](JS::ConstUTF8CharsZ message) { + const char* chars = message.c_str(); + CHECK(isBadTrailingBytesMessage(chars)); + CHECK(contains(chars, "0x20")); + return true; + }, + "0xDF 0x20")); + + static const char badSecondInThreeByte[] = "var x = \xEF\x17\xA7"; + CHECK(testBadUtf8( + badSecondInThreeByte, JSMSG_BAD_TRAILING_UTF8_UNIT, + [this](JS::ConstUTF8CharsZ message) { + const char* chars = message.c_str(); + CHECK(isBadTrailingBytesMessage(chars)); + CHECK(contains(chars, "0x17")); + return true; + }, + // Validating stops with the first invalid code unit and + // shouldn't go beyond that. + "0xEF 0x17")); + + static const char lengthTwoTooShort[] = "var x = \xDF"; + CHECK(testBadUtf8( + lengthTwoTooShort, JSMSG_NOT_ENOUGH_CODE_UNITS, + [this](JS::ConstUTF8CharsZ message) { + const char* chars = message.c_str(); + CHECK(isNotEnoughUnitsMessage(chars)); + CHECK(contains(chars, "0xDF")); + CHECK(contains(chars, " 1 byte, but 0 bytes were present")); + return true; + }, + "0xDF")); + + static const char forbiddenHighSurrogate[] = "var x = \xED\xA2\x87"; + CHECK(testBadUtf8( + forbiddenHighSurrogate, JSMSG_FORBIDDEN_UTF8_CODE_POINT, + [this](JS::ConstUTF8CharsZ message) { + const char* chars = message.c_str(); + CHECK(isSurrogateMessage(chars)); + CHECK(contains(chars, "0xD887")); + return true; + }, + "0xED 0xA2 0x87")); + + static const char forbiddenLowSurrogate[] = "var x = \xED\xB7\xAF"; + CHECK(testBadUtf8( + forbiddenLowSurrogate, JSMSG_FORBIDDEN_UTF8_CODE_POINT, + [this](JS::ConstUTF8CharsZ message) { + const char* chars = message.c_str(); + CHECK(isSurrogateMessage(chars)); + CHECK(contains(chars, "0xDDEF")); + return true; + }, + "0xED 0xB7 0xAF")); + + static const char oneTooBig[] = "var x = \xF4\x90\x80\x80"; + CHECK(testBadUtf8( + oneTooBig, JSMSG_FORBIDDEN_UTF8_CODE_POINT, + [this](JS::ConstUTF8CharsZ message) { + const char* chars = message.c_str(); + CHECK(isTooBigMessage(chars)); + CHECK(contains(chars, "0x110000")); + return true; + }, + "0xF4 0x90 0x80 0x80")); + + static const char notShortestFormZero[] = "var x = \xC0\x80"; + CHECK(testBadUtf8( + notShortestFormZero, JSMSG_FORBIDDEN_UTF8_CODE_POINT, + [this](JS::ConstUTF8CharsZ message) { + const char* chars = message.c_str(); + CHECK(isNotShortestFormMessage(chars)); + CHECK(startsWith(chars, "0x0 isn't ")); + return true; + }, + "0xC0 0x80")); + + static const char notShortestFormNonzero[] = "var x = \xE0\x87\x80"; + CHECK(testBadUtf8( + notShortestFormNonzero, JSMSG_FORBIDDEN_UTF8_CODE_POINT, + [this](JS::ConstUTF8CharsZ message) { + const char* chars = message.c_str(); + CHECK(isNotShortestFormMessage(chars)); + CHECK(startsWith(chars, "0x1C0 isn't ")); + return true; + }, + "0xE0 0x87 0x80")); + + return true; +} + +static constexpr size_t LengthOfByte = js_strlen("0xFF"); + +static bool startsWithByte(const char* str) { + return str[0] == '0' && str[1] == 'x' && IsAsciiHexDigit(str[2]) && + IsAsciiHexDigit(str[3]); +} + +static bool startsWith(const char* str, const char* prefix) { + return std::strncmp(prefix, str, strlen(prefix)) == 0; +} + +static bool contains(const char* str, const char* substr) { + return std::strstr(str, substr) != nullptr; +} + +static bool equals(const char* str, const char* expected) { + return std::strcmp(str, expected) == 0; +} + +static bool isBadLeadUnitMessage(const char* str) { + return startsWithByte(str) && + equals(str + LengthOfByte, + " byte doesn't begin a valid UTF-8 code point"); +} + +static bool isBadTrailingBytesMessage(const char* str) { + return startsWith(str, "bad trailing UTF-8 byte "); +} + +static bool isNotEnoughUnitsMessage(const char* str) { + return startsWithByte(str) && + startsWith(str + LengthOfByte, " byte in UTF-8 must be followed by "); +} + +static bool isForbiddenCodePointMessage(const char* str) { + return contains(str, "isn't a valid code point because"); +} + +static bool isSurrogateMessage(const char* str) { + return isForbiddenCodePointMessage(str) && + contains(str, " it's a UTF-16 surrogate"); +} + +static bool isTooBigMessage(const char* str) { + return isForbiddenCodePointMessage(str) && + contains(str, "the maximum code point is U+10FFFF"); +} + +static bool isNotShortestFormMessage(const char* str) { + return isForbiddenCodePointMessage(str) && + contains(str, "it wasn't encoded in shortest possible form"); +} + +template <size_t N, typename TestMessage> +bool testBadUtf8(const char (&chars)[N], unsigned errorNumber, + TestMessage testMessage, const char* badBytes) { + JS::Rooted<JSScript*> script(cx); + { + JS::CompileOptions options(cx); + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + CHECK(srcBuf.init(cx, chars, N - 1, JS::SourceOwnership::Borrowed)); + + script = JS::Compile(cx, options, srcBuf); + CHECK(!script); + } + + JS::ExceptionStack exnStack(cx); + CHECK(JS::StealPendingExceptionStack(cx, &exnStack)); + + JS::ErrorReportBuilder report(cx); + CHECK(report.init(cx, exnStack, JS::ErrorReportBuilder::WithSideEffects)); + + const auto* errorReport = report.report(); + + CHECK(errorReport->errorNumber == errorNumber); + + CHECK(testMessage(errorReport->message())); + + { + const auto& notes = errorReport->notes; + CHECK(notes != nullptr); + + auto iter = notes->begin(); + CHECK(iter != notes->end()); + + const char* noteMessage = (*iter)->message().c_str(); + + // The prefix ought always be the same. + static constexpr char expectedPrefix[] = + "the code units comprising this invalid code point were: "; + constexpr size_t expectedPrefixLen = js_strlen(expectedPrefix); + + CHECK(startsWith(noteMessage, expectedPrefix)); + + // The end of the prefix is the bad bytes. + CHECK(equals(noteMessage + expectedPrefixLen, badBytes)); + + ++iter; + CHECK(iter == notes->end()); + } + + static constexpr char16_t expectedContext[] = u"var x = "; + constexpr size_t expectedContextLen = js_strlen(expectedContext); + + const char16_t* lineOfContext = errorReport->linebuf(); + size_t lineOfContextLength = errorReport->linebufLength(); + + CHECK(lineOfContext[lineOfContextLength] == '\0'); + CHECK(lineOfContextLength == expectedContextLen); + + CHECK(std::memcmp(lineOfContext, expectedContext, + expectedContextLen * sizeof(char16_t)) == 0); + + return true; +} +END_TEST(testUtf8BadBytes) + +BEGIN_TEST(testMultiUnitUtf8InWindow) { + static const char firstInWindowIsMultiUnit[] = + "\xCF\x80\xCF\x80 = 6.283185307; @ bad starts HERE:\x80\xFF\xFF"; + CHECK(testContext(firstInWindowIsMultiUnit, + u"ππ = 6.283185307; @ bad starts HERE:")); + + static const char atTokenOffsetIsMulti[] = "var z = 💯"; + CHECK(testContext(atTokenOffsetIsMulti, u"var z = 💯")); + + static const char afterTokenOffsetIsMulti[] = "var z = @💯💯💯X"; + CHECK(testContext(afterTokenOffsetIsMulti, u"var z = @💯💯💯X")); + + static const char atEndIsMulti[] = "var z = @@💯💯💯"; + CHECK(testContext(atEndIsMulti, u"var z = @@💯💯💯")); + + return true; +} + +template <size_t N, size_t ContextLenWithNull> +bool testContext(const char (&chars)[N], + const char16_t (&expectedContext)[ContextLenWithNull]) { + JS::Rooted<JSScript*> script(cx); + { + JS::CompileOptions options(cx); + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + CHECK(srcBuf.init(cx, chars, N - 1, JS::SourceOwnership::Borrowed)); + + script = JS::Compile(cx, options, srcBuf); + CHECK(!script); + } + + JS::ExceptionStack exnStack(cx); + CHECK(JS::StealPendingExceptionStack(cx, &exnStack)); + + JS::ErrorReportBuilder report(cx); + CHECK(report.init(cx, exnStack, JS::ErrorReportBuilder::WithSideEffects)); + + const auto* errorReport = report.report(); + + CHECK(errorReport->errorNumber == JSMSG_ILLEGAL_CHARACTER); + + const char16_t* lineOfContext = errorReport->linebuf(); + size_t lineOfContextLength = errorReport->linebufLength(); + + CHECK(lineOfContext[lineOfContextLength] == '\0'); + CHECK(lineOfContextLength == ContextLenWithNull - 1); + + CHECK(ArrayEqual(lineOfContext, expectedContext, ContextLenWithNull)); + + return true; +} +END_TEST(testMultiUnitUtf8InWindow) diff --git a/js/src/jsapi-tests/testDateToLocaleString.cpp b/js/src/jsapi-tests/testDateToLocaleString.cpp new file mode 100644 index 0000000000..3a425ab7a6 --- /dev/null +++ b/js/src/jsapi-tests/testDateToLocaleString.cpp @@ -0,0 +1,63 @@ +/* -*- 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 "js/LocaleSensitive.h" +#include "jsapi-tests/tests.h" + +BEGIN_TEST(testDateToLocaleString) { + JSRuntime* rt = JS_GetRuntime(cx); + + // This test should only attempt to run if we have Intl support: necessary + // to properly assume that changes to the default locale will predictably + // affect the behavior of the locale-sensitive Date methods tested here. + JS::Rooted<JS::Value> haveIntl(cx); + EVAL("typeof Intl !== 'undefined'", &haveIntl); + if (!haveIntl.toBoolean()) { + return true; + } + + // Pervasive assumption: our Intl support includes "de" (German) and + // "en" (English) and treats them differently for purposes of + // Date.prototype.toLocale{,Date,Time}String behavior. + + // Start with German. + CHECK(JS_SetDefaultLocale(rt, "de")); + + // The (constrained) Date object we'll use to test behavior. + EXEC("var d = new Date(Date.UTC(2015, 9 - 1, 17));"); + + // Test that toLocaleString behavior changes with default locale changes. + EXEC("var deAll = d.toLocaleString();"); + + CHECK(JS_SetDefaultLocale(rt, "en")); + EXEC( + "if (d.toLocaleString() === deAll) \n" + " throw 'toLocaleString results should have changed with system locale " + "change';"); + + // Test that toLocaleDateString behavior changes with default locale changes. + EXEC("var enDate = d.toLocaleDateString();"); + + CHECK(JS_SetDefaultLocale(rt, "de")); + EXEC( + "if (d.toLocaleDateString() === enDate) \n" + " throw 'toLocaleDateString results should have changed with system " + "locale change';"); + + // Test that toLocaleTimeString behavior changes with default locale changes. + EXEC("var deTime = d.toLocaleTimeString();"); + + CHECK(JS_SetDefaultLocale(rt, "en")); + EXEC( + "if (d.toLocaleTimeString() === deTime) \n" + " throw 'toLocaleTimeString results should have changed with system " + "locale change';"); + + JS_ResetDefaultLocale(rt); + return true; +} +END_TEST(testDateToLocaleString) diff --git a/js/src/jsapi-tests/testDebugger.cpp b/js/src/jsapi-tests/testDebugger.cpp new file mode 100644 index 0000000000..62735fa212 --- /dev/null +++ b/js/src/jsapi-tests/testDebugger.cpp @@ -0,0 +1,62 @@ +/* -*- 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 "js/CallAndConstruct.h" +#include "js/GlobalObject.h" // JS_NewGlobalObject +#include "js/PropertyAndElement.h" // JS_SetProperty +#include "jsapi-tests/tests.h" +#include "vm/JSContext.h" + +using namespace js; + +BEGIN_TEST(testDebugger_newScriptHook) { + // Test that top-level indirect eval fires the newScript hook. + CHECK(JS_DefineDebuggerObject(cx, global)); + JS::RealmOptions options; + JS::RootedObject g(cx, JS_NewGlobalObject(cx, getGlobalClass(), nullptr, + JS::FireOnNewGlobalHook, options)); + CHECK(g); + + JS::RootedObject gWrapper(cx, g); + CHECK(JS_WrapObject(cx, &gWrapper)); + JS::RootedValue v(cx, JS::ObjectValue(*gWrapper)); + CHECK(JS_SetProperty(cx, global, "g", v)); + + EXEC( + "var dbg = Debugger(g);\n" + "var hits = 0;\n" + "dbg.onNewScript = function (s) {\n" + " hits += Number(s instanceof Debugger.Script);\n" + "};\n"); + + // Since g is a debuggee, g.eval should trigger newScript, regardless of + // what scope object we use to enter the compartment. + // + // Scripts are associated with the global where they're compiled, so we + // deliver them only to debuggers that are watching that particular global. + // + return testIndirectEval(g, "Math.abs(0)"); +} + +bool testIndirectEval(JS::HandleObject global, const char* code) { + EXEC("hits = 0;"); + + { + JSAutoRealm ar(cx, global); + JSString* codestr = JS_NewStringCopyZ(cx, code); + CHECK(codestr); + JS::RootedValue arg(cx, JS::StringValue(codestr)); + JS::RootedValue v(cx); + CHECK(JS_CallFunctionName(cx, global, "eval", HandleValueArray(arg), &v)); + } + + JS::RootedValue hitsv(cx); + EVAL("hits", &hitsv); + CHECK(hitsv.isInt32(1)); + return true; +} +END_TEST(testDebugger_newScriptHook) diff --git a/js/src/jsapi-tests/testDeduplication.cpp b/js/src/jsapi-tests/testDeduplication.cpp new file mode 100644 index 0000000000..6ee3cb989d --- /dev/null +++ b/js/src/jsapi-tests/testDeduplication.cpp @@ -0,0 +1,126 @@ +/* -*- 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 <string.h> + +#include "gc/GC.h" + +#include "js/RootingAPI.h" +#include "js/StableStringChars.h" +#include "js/String.h" // JS::StringToLinearString + +#include "jsapi-tests/tests.h" + +#include "vm/JSContext.h" +#include "vm/StringType.h" + +#include "vm/JSContext-inl.h" + +static bool SameChars(JSContext* cx, JSString* str1, JSString* str2, + size_t offset) { + JS::AutoCheckCannotGC nogc(cx); + + const JS::Latin1Char* chars1 = + JS::StringToLinearString(cx, str1)->latin1Chars(nogc); + const JS::Latin1Char* chars2 = + JS::StringToLinearString(cx, str2)->latin1Chars(nogc); + + return chars1 == chars2 + offset; +} + +BEGIN_TEST(testDeduplication_ASSC) { + // Test with a long enough string to avoid inline chars allocation. + const char text[] = + "Andthebeastshallcomeforthsurroundedbyaroilingcloudofvengeance." + "Thehouseoftheunbelieversshallberazedandtheyshallbescorchedtoth" + "eearth.Theirtagsshallblinkuntiltheendofdays."; + + // Create a string to deduplicate later strings to. + JS::RootedString original(cx); + JS::RootedString str(cx); + JS::RootedString dep(cx); + JS::RootedString depdep(cx); + JS::RootedString str2(cx); + JS::RootedString dep2(cx); + JS::RootedString depdep2(cx); + + if (!cx->zone()->allocNurseryStrings()) { + // This test requires nursery-allocated strings, so that they will go + // through the deduplication pass during minor GC. + return true; + } + + { + // This test checks the behavior when GC is performed after allocating + // all the following strings. + // GC shouldn't happen in between them, even in compacting jobs. + js::gc::AutoSuppressGC suppress(cx); + + original = JS_NewStringCopyZ(cx, text); + CHECK(original); + + // Create a chain of dependent strings, with a base string whose contents + // match `original`'s. + str = JS_NewStringCopyZ(cx, text); + CHECK(str && !str->isTenured()); + + dep = JS_NewDependentString(cx, str, 10, 100); + CHECK(dep && !dep->isTenured()); + + depdep = JS_NewDependentString(cx, dep, 10, 80); + CHECK(depdep && !depdep->isTenured()); + + // Repeat. This one will not be prevented from deduplication. + str2 = JS_NewStringCopyZ(cx, text); + CHECK(str2 && !str2->isTenured()); + + dep2 = JS_NewDependentString(cx, str2, 10, 100); + CHECK(dep2 && !dep2->isTenured()); + + depdep2 = JS_NewDependentString(cx, dep2, 10, 80); + CHECK(depdep2 && !depdep2->isTenured()); + } + + // Initializing an AutoStableStringChars with `depdep` will prevent the + // owner of its chars (`str`) from being deduplicated, but only if the + // chars are stored in the malloc heap. Force `str` to be nondeduplicatable + // unconditionally to avoid depending on the exact set of things that are + // enabled. + str->setNonDeduplicatable(); + JS::AutoStableStringChars stable(cx); + CHECK(stable.init(cx, depdep)); + + const JS::Latin1Char* chars = stable.latin1Chars(); + CHECK(memcmp(chars, text + 20, 80 * sizeof(JS::Latin1Char)) == 0); + + // `depdep` should share chars with `str` but not with `original`. + CHECK(SameChars(cx, depdep, str, 20)); + CHECK(!SameChars(cx, depdep, original, 20)); + + // Same for `depdep2`. + CHECK(SameChars(cx, depdep2, str2, 20)); + CHECK(!SameChars(cx, depdep2, original, 20)); + + // Do a minor GC that will deduplicate `str2` to `original`, and would have + // deduplicated `str` as well if it weren't prevented by the + // AutoStableStringChars. + cx->minorGC(JS::GCReason::API); + + // `depdep` should still share chars with `str` but not with `original`. + CHECK(SameChars(cx, depdep, str, 20)); + CHECK(!SameChars(cx, depdep, original, 20)); + + // `depdep2` should now share chars with both `str2` and `original`. Or with + // `str`, since it could legitimately have been detected to be identical to + // the tenured `depdep` and deduplicated to that. + CHECK(SameChars(cx, depdep2, str2, 20) || SameChars(cx, depdep2, str, 20)); + CHECK(SameChars(cx, depdep2, original, 20) || + SameChars(cx, depdep2, str, 20)); + + return true; +} +END_TEST(testDeduplication_ASSC) diff --git a/js/src/jsapi-tests/testDeepFreeze.cpp b/js/src/jsapi-tests/testDeepFreeze.cpp new file mode 100644 index 0000000000..a1b62b8ca4 --- /dev/null +++ b/js/src/jsapi-tests/testDeepFreeze.cpp @@ -0,0 +1,58 @@ +/* -*- 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" + +BEGIN_TEST(testDeepFreeze_bug535703) { + JS::RootedValue v(cx); + EVAL("var x = {}; x;", &v); + JS::RootedObject obj(cx, v.toObjectOrNull()); + CHECK(JS_DeepFreezeObject(cx, obj)); // don't crash + EVAL("Object.isFrozen(x)", &v); + CHECK(v.isTrue()); + return true; +} +END_TEST(testDeepFreeze_bug535703) + +BEGIN_TEST(testDeepFreeze_deep) { + JS::RootedValue a(cx), o(cx); + EXEC( + "var a = {}, o = a;\n" + "for (var i = 0; i < 5000; i++)\n" + " a = {x: a, y: a};\n"); + EVAL("a", &a); + EVAL("o", &o); + + JS::RootedObject aobj(cx, a.toObjectOrNull()); + CHECK(JS_DeepFreezeObject(cx, aobj)); + + JS::RootedValue b(cx); + EVAL("Object.isFrozen(a)", &b); + CHECK(b.isTrue()); + EVAL("Object.isFrozen(o)", &b); + CHECK(b.isTrue()); + return true; +} +END_TEST(testDeepFreeze_deep) + +BEGIN_TEST(testDeepFreeze_loop) { + JS::RootedValue x(cx), y(cx); + EXEC("var x = [], y = {x: x}; y.y = y; x.push(x, y);"); + EVAL("x", &x); + EVAL("y", &y); + + JS::RootedObject xobj(cx, x.toObjectOrNull()); + CHECK(JS_DeepFreezeObject(cx, xobj)); + + JS::RootedValue b(cx); + EVAL("Object.isFrozen(x)", &b); + CHECK(b.isTrue()); + EVAL("Object.isFrozen(y)", &b); + CHECK(b.isTrue()); + return true; +} +END_TEST(testDeepFreeze_loop) diff --git a/js/src/jsapi-tests/testDefineGetterSetterNonEnumerable.cpp b/js/src/jsapi-tests/testDefineGetterSetterNonEnumerable.cpp new file mode 100644 index 0000000000..37c57fb3c3 --- /dev/null +++ b/js/src/jsapi-tests/testDefineGetterSetterNonEnumerable.cpp @@ -0,0 +1,51 @@ +/* -*- 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 "js/PropertyAndElement.h" // JS_DefineProperty +#include "js/PropertyDescriptor.h" // JS_GetOwnPropertyDescriptor +#include "jsapi-tests/tests.h" + +static bool NativeGetterSetter(JSContext* cx, unsigned argc, JS::Value* vp) { + return true; +} + +BEGIN_TEST(testDefineGetterSetterNonEnumerable) { + static const char PROPERTY_NAME[] = "foo"; + + JS::RootedValue vobj(cx); + JS::RootedObject obj(cx, JS_NewPlainObject(cx)); + CHECK(obj); + vobj.setObject(*obj); + + JSFunction* funGet = JS_NewFunction(cx, NativeGetterSetter, 0, 0, "get"); + CHECK(funGet); + JS::RootedObject funGetObj(cx, JS_GetFunctionObject(funGet)); + JS::RootedValue vget(cx, JS::ObjectValue(*funGetObj)); + + JSFunction* funSet = JS_NewFunction(cx, NativeGetterSetter, 1, 0, "set"); + CHECK(funSet); + JS::RootedObject funSetObj(cx, JS_GetFunctionObject(funSet)); + JS::RootedValue vset(cx, JS::ObjectValue(*funSetObj)); + + JS::RootedObject vObject(cx, vobj.toObjectOrNull()); + CHECK(JS_DefineProperty(cx, vObject, PROPERTY_NAME, funGetObj, funSetObj, + JSPROP_ENUMERATE)); + + CHECK(JS_DefineProperty(cx, vObject, PROPERTY_NAME, funGetObj, funSetObj, + JSPROP_PERMANENT)); + + JS::Rooted<mozilla::Maybe<JS::PropertyDescriptor>> desc(cx); + CHECK(JS_GetOwnPropertyDescriptor(cx, vObject, PROPERTY_NAME, &desc)); + CHECK(desc.isSome()); + CHECK(desc->hasGetter()); + CHECK(desc->hasSetter()); + CHECK(!desc->configurable()); + CHECK(!desc->enumerable()); + + return true; +} +END_TEST(testDefineGetterSetterNonEnumerable) diff --git a/js/src/jsapi-tests/testDefineProperty.cpp b/js/src/jsapi-tests/testDefineProperty.cpp new file mode 100644 index 0000000000..576e93e19f --- /dev/null +++ b/js/src/jsapi-tests/testDefineProperty.cpp @@ -0,0 +1,26 @@ +/* -*- 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 "js/PropertyAndElement.h" // JS_DefineProperty +#include "jsapi-tests/tests.h" + +BEGIN_TEST(testDefineProperty_bug564344) { + JS::RootedValue x(cx); + EVAL( + "function f() {}\n" + "var x = {p: f};\n" + "x.p(); // brand x's scope\n" + "x;", + &x); + + JS::RootedObject obj(cx, x.toObjectOrNull()); + for (int i = 0; i < 2; i++) { + CHECK(JS_DefineProperty(cx, obj, "q", JS::UndefinedHandleValue, 0)); + } + return true; +} +END_TEST(testDefineProperty_bug564344) diff --git a/js/src/jsapi-tests/testDeflateStringToUTF8Buffer.cpp b/js/src/jsapi-tests/testDeflateStringToUTF8Buffer.cpp new file mode 100644 index 0000000000..b451fc4e2b --- /dev/null +++ b/js/src/jsapi-tests/testDeflateStringToUTF8Buffer.cpp @@ -0,0 +1,162 @@ +/* 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" + +using namespace JS; + +BEGIN_TEST(test_DeflateStringToUTF8Buffer) { + JSString* str; + JSLinearString* linearStr; + + // DeflateStringToUTF8Buffer does not write a null terminator, so the byte + // following the last byte written to the |actual| buffer should retain + // the value it held before the call to DeflateStringToUTF8Buffer, which is + // initialized to 0x1. + + char actual[100]; + auto span = mozilla::Span(actual); + + // Test with an ASCII string, which calls JSLinearString::latin1Chars + // to retrieve the characters from the string and generates UTF-8 output + // that is identical to the ASCII input. + + str = JS_NewStringCopyZ(cx, "Ohai"); // { 0x4F, 0x68, 0x61, 0x69 } + MOZ_RELEASE_ASSERT(str); + linearStr = JS_EnsureLinearString(cx, str); + + { + const char expected[] = {0x4F, 0x68, 0x61, 0x69, 0x1}; + memset(actual, 0x1, 100); + size_t dstlen = JS::DeflateStringToUTF8Buffer(linearStr, span); + CHECK_EQUAL(memcmp(actual, expected, sizeof(expected)), 0); + CHECK_EQUAL(dstlen, 4u); + } + + { + const char expected[] = {0x4F, 0x68, 0x61, 0x1}; + memset(actual, 0x1, 100); + size_t dstlen = JS::DeflateStringToUTF8Buffer(linearStr, span.To(3)); + CHECK_EQUAL(memcmp(actual, expected, sizeof(expected)), 0); + CHECK_EQUAL(dstlen, 3u); + } + + { + const unsigned char expected[] = {0x1}; + memset(actual, 0x1, 100); + size_t dstlen = JS::DeflateStringToUTF8Buffer(linearStr, span.To(0)); + CHECK_EQUAL(memcmp(actual, expected, sizeof(expected)), 0); + CHECK_EQUAL(dstlen, 0u); + } + + // Test with a Latin-1 string, which calls JSLinearString::latin1Chars + // like with the ASCII string but generates UTF-8 output that is different + // from the ASCII input. + + str = JS_NewUCStringCopyZ(cx, u"\xD3\x68\xE3\xEF"); // u"Óhãï" + MOZ_RELEASE_ASSERT(str); + linearStr = JS_EnsureLinearString(cx, str); + + { + const unsigned char expected[] = {0xC3, 0x93, 0x68, 0xC3, + 0xA3, 0xC3, 0xAF, 0x1}; + memset(actual, 0x1, 100); + JS::DeflateStringToUTF8Buffer(linearStr, span); + CHECK_EQUAL(memcmp(actual, expected, sizeof(expected)), 0); + } + + { + const unsigned char expected[] = {0xC3, 0x93, 0x68, 0xC3, + 0xA3, 0xC3, 0xAF, 0x1}; + memset(actual, 0x1, 100); + size_t dstlen = JS::DeflateStringToUTF8Buffer(linearStr, span.To(7)); + CHECK_EQUAL(memcmp(actual, expected, sizeof(expected)), 0); + CHECK_EQUAL(dstlen, 7u); + } + + { + // Specify a destination buffer length of 3. That's exactly enough + // space to encode the first two characters, which takes three bytes. + const unsigned char expected[] = {0xC3, 0x93, 0x68, 0x1}; + memset(actual, 0x1, 100); + size_t dstlen = JS::DeflateStringToUTF8Buffer(linearStr, span.To(3)); + CHECK_EQUAL(memcmp(actual, expected, sizeof(expected)), 0); + CHECK_EQUAL(dstlen, 3u); + } + + { + // Specify a destination buffer length of 4. That's only enough space + // to encode the first two characters, which takes three bytes, because + // the third character would take another two bytes. + const unsigned char expected[] = {0xC3, 0x93, 0x68, 0x1}; + memset(actual, 0x1, 100); + size_t dstlen = JS::DeflateStringToUTF8Buffer(linearStr, span.To(4)); + CHECK_EQUAL(memcmp(actual, expected, sizeof(expected)), 0); + CHECK_EQUAL(dstlen, 3u); + } + + { + const unsigned char expected[] = {0x1}; + memset(actual, 0x1, 100); + size_t dstlen = JS::DeflateStringToUTF8Buffer(linearStr, span.To(0)); + CHECK_EQUAL(memcmp(actual, expected, sizeof(expected)), 0); + CHECK_EQUAL(dstlen, 0u); + } + + // Test with a UTF-16 string, which calls JSLinearString::twoByteChars + // to retrieve the characters from the string. + + str = JS_NewUCStringCopyZ(cx, u"\x038C\x0068\x0203\x0457"); // u"Όhȃї" + MOZ_RELEASE_ASSERT(str); + linearStr = JS_EnsureLinearString(cx, str); + + { + const unsigned char expected[] = {0xCE, 0x8C, 0x68, 0xC8, + 0x83, 0xD1, 0x97, 0x1}; + memset(actual, 0x1, 100); + JS::DeflateStringToUTF8Buffer(linearStr, span); + CHECK_EQUAL(memcmp(actual, expected, sizeof(expected)), 0); + } + + { + const unsigned char expected[] = {0xCE, 0x8C, 0x68, 0xC8, + 0x83, 0xD1, 0x97, 0x1}; + memset(actual, 0x1, 100); + size_t dstlen = JS::DeflateStringToUTF8Buffer(linearStr, span.To(7)); + CHECK_EQUAL(memcmp(actual, expected, sizeof(expected)), 0); + CHECK_EQUAL(dstlen, 7u); + } + + { + // Specify a destination buffer length of 3. That's exactly enough + // space to encode the first two characters, which takes three bytes. + const unsigned char expected[] = {0xCE, 0x8C, 0x68, 0x1}; + memset(actual, 0x1, 100); + size_t dstlen = JS::DeflateStringToUTF8Buffer(linearStr, span.To(3)); + CHECK_EQUAL(memcmp(actual, expected, sizeof(expected)), 0); + CHECK_EQUAL(dstlen, 3u); + } + + { + // Specify a destination buffer length of 4. That's only enough space + // to encode the first two characters, which takes three bytes, because + // the third character would take another two bytes. + const unsigned char expected[] = {0xCE, 0x8C, 0x68, 0x1}; + memset(actual, 0x1, 100); + size_t dstlen = JS::DeflateStringToUTF8Buffer(linearStr, span.To(4)); + CHECK_EQUAL(memcmp(actual, expected, sizeof(expected)), 0); + CHECK_EQUAL(dstlen, 3u); + } + + { + const unsigned char expected[] = {0x1}; + memset(actual, 0x1, 100); + size_t dstlen = JS::DeflateStringToUTF8Buffer(linearStr, span.To(0)); + CHECK_EQUAL(memcmp(actual, expected, sizeof(expected)), 0); + CHECK_EQUAL(dstlen, 0u); + } + + return true; +} +END_TEST(test_DeflateStringToUTF8Buffer) diff --git a/js/src/jsapi-tests/testDeleteProperty.cpp b/js/src/jsapi-tests/testDeleteProperty.cpp new file mode 100644 index 0000000000..263c858069 --- /dev/null +++ b/js/src/jsapi-tests/testDeleteProperty.cpp @@ -0,0 +1,53 @@ +/* -*- 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 "js/Id.h" +#include "js/PropertyAndElement.h" +#include "jsapi-tests/tests.h" + +BEGIN_TEST(testDeleteProperty) { + JS::RootedValue val(cx); + EVAL("var obj = {a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8}; obj", &val); + CHECK(val.isObject()); + JS::RootedObject obj(cx, &val.toObject()); + + JS::Rooted<JS::PropertyKey> key(cx); + + auto createPropertyKey = [](JSContext* cx, const char* s, + JS::MutableHandle<JS::PropertyKey> key) { + JSString* atom = JS_AtomizeString(cx, s); + if (!atom) { + return false; + } + key.set(JS::PropertyKey::NonIntAtom(atom)); + return true; + }; + + // Test delete APIs without an ObjectOpResult argument. + CHECK(JS_DeleteProperty(cx, obj, "b")); + CHECK(createPropertyKey(cx, "d", &key)); + CHECK(JS_DeletePropertyById(cx, obj, key)); + + // Test delete APIs with an ObjectOpResult argument. + JS::ObjectOpResult result; + CHECK(JS_DeleteProperty(cx, obj, "e", result)); + CHECK(result); + CHECK(createPropertyKey(cx, "f", &key)); + CHECK(JS_DeletePropertyById(cx, obj, key, result)); + CHECK(result); + + // Check properties were deleted. + EVAL("JSON.stringify(obj)", &val); + CHECK(val.isString()); + bool match = false; + CHECK(JS_StringEqualsAscii(cx, val.toString(), + "{\"a\":1,\"c\":3,\"g\":7,\"h\":8}", &match)); + CHECK(match); + + return true; +} +END_TEST(testDeleteProperty) diff --git a/js/src/jsapi-tests/testDifferentNewTargetInvokeConstructor.cpp b/js/src/jsapi-tests/testDifferentNewTargetInvokeConstructor.cpp new file mode 100644 index 0000000000..1d623a78c2 --- /dev/null +++ b/js/src/jsapi-tests/testDifferentNewTargetInvokeConstructor.cpp @@ -0,0 +1,38 @@ +/* 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 "js/CallAndConstruct.h" // JS::Construct +#include "jsapi-tests/tests.h" + +BEGIN_TEST(testDifferentNewTargetInvokeConstructor) { + JS::RootedValue func(cx); + JS::RootedValue otherFunc(cx); + + EVAL("(function() { /* This is a different new.target function */ })", + &otherFunc); + + EVAL( + "(function(expected) { if (expected !== new.target) throw new " + "Error('whoops'); })", + &func); + + JS::RootedValueArray<1> args(cx); + args[0].set(otherFunc); + + JS::RootedObject obj(cx); + + JS::RootedObject newTarget(cx, &otherFunc.toObject()); + + CHECK(JS::Construct(cx, func, newTarget, args, &obj)); + + // It should fail, though, if newTarget is not a constructor + JS::RootedValue plain(cx); + EVAL("({})", &plain); + args[0].set(plain); + newTarget = &plain.toObject(); + CHECK(!JS::Construct(cx, func, newTarget, args, &obj)); + + return true; +} +END_TEST(testDifferentNewTargetInvokeConstructor) diff --git a/js/src/jsapi-tests/testEmptyWindowIsOmitted.cpp b/js/src/jsapi-tests/testEmptyWindowIsOmitted.cpp new file mode 100644 index 0000000000..c03a8106c0 --- /dev/null +++ b/js/src/jsapi-tests/testEmptyWindowIsOmitted.cpp @@ -0,0 +1,155 @@ +/* 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 "mozilla/TextUtils.h" +#include "mozilla/Utf8.h" + +#include <cstring> + +#include "js/CharacterEncoding.h" +#include "js/CompilationAndEvaluation.h" // JS::Compile +#include "js/Exception.h" +#include "js/friend/ErrorMessages.h" // JSMSG_* +#include "js/SourceText.h" +#include "jsapi-tests/tests.h" +#include "util/Text.h" +#include "vm/ErrorReporting.h" + +using mozilla::IsAsciiHexDigit; +using mozilla::Utf8Unit; + +BEGIN_TEST(testEmptyWindow) { return testUtf8() && testUtf16(); } + +bool testUtf8() { + // Bad unit with nothing before it. + static const char badLeadingUnit[] = "\x80"; + CHECK(testOmittedWindow(badLeadingUnit, JSMSG_BAD_LEADING_UTF8_UNIT, "0x80")); + + // Bad unit at start of a fresh line. + static const char badStartingFreshLine[] = "var x = 5;\n\x98"; + CHECK(testOmittedWindow(badStartingFreshLine, JSMSG_BAD_LEADING_UTF8_UNIT, + "0x98")); + + // Bad trailing unit in initial code point. + static const char badTrailingUnit[] = "\xD8\x20"; + CHECK(testOmittedWindow(badTrailingUnit, JSMSG_BAD_TRAILING_UTF8_UNIT, + "0xD8 0x20")); + + // Bad trailing unit at start of a fresh line. + static const char badTrailingUnitFreshLine[] = "var x = 5;\n\xD8\x20"; + CHECK(testOmittedWindow(badTrailingUnitFreshLine, + JSMSG_BAD_TRAILING_UTF8_UNIT, "0xD8 0x20")); + + // Overlong in initial code point. + static const char overlongInitial[] = "\xC0\x80"; + CHECK(testOmittedWindow(overlongInitial, JSMSG_FORBIDDEN_UTF8_CODE_POINT, + "0xC0 0x80")); + + // Overlong at start of a fresh line. + static const char overlongFreshLine[] = "var x = 5;\n\xC0\x81"; + CHECK(testOmittedWindow(overlongFreshLine, JSMSG_FORBIDDEN_UTF8_CODE_POINT, + "0xC0 0x81")); + + // Not-enough in initial code point. + static const char notEnoughInitial[] = "\xF0"; + CHECK( + testOmittedWindow(notEnoughInitial, JSMSG_NOT_ENOUGH_CODE_UNITS, "0xF0")); + + // Not-enough at start of a fresh line. + static const char notEnoughFreshLine[] = "var x = 5;\n\xF0"; + CHECK(testOmittedWindow(notEnoughFreshLine, JSMSG_NOT_ENOUGH_CODE_UNITS, + "0xF0")); + + return true; +} + +bool testUtf16() { + // Bad unit with nothing before it. + static const char16_t badLeadingUnit[] = u"\xDFFF"; + CHECK(testOmittedWindow(badLeadingUnit, JSMSG_ILLEGAL_CHARACTER)); + + // Bad unit at start of a fresh line. + static const char16_t badStartingFreshLine[] = u"var x = 5;\n\xDFFF"; + CHECK(testOmittedWindow(badStartingFreshLine, JSMSG_ILLEGAL_CHARACTER)); + + return true; +} + +static bool startsWith(const char* str, const char* prefix) { + return std::strncmp(prefix, str, strlen(prefix)) == 0; +} + +static bool equals(const char* str, const char* expected) { + return std::strcmp(str, expected) == 0; +} + +JSScript* compile(const char16_t* chars, size_t len) { + JS::SourceText<char16_t> source; + MOZ_RELEASE_ASSERT( + source.init(cx, chars, len, JS::SourceOwnership::Borrowed)); + + JS::CompileOptions options(cx); + return JS::Compile(cx, options, source); +} + +JSScript* compile(const char* chars, size_t len) { + JS::SourceText<Utf8Unit> source; + MOZ_RELEASE_ASSERT( + source.init(cx, chars, len, JS::SourceOwnership::Borrowed)); + + JS::CompileOptions options(cx); + return JS::Compile(cx, options, source); +} + +template <typename CharT, size_t N> +bool testOmittedWindow(const CharT (&chars)[N], unsigned expectedErrorNumber, + const char* badCodeUnits = nullptr) { + JS::Rooted<JSScript*> script(cx, compile(chars, N - 1)); + CHECK(!script); + + JS::ExceptionStack exnStack(cx); + CHECK(JS::StealPendingExceptionStack(cx, &exnStack)); + + JS::ErrorReportBuilder report(cx); + CHECK(report.init(cx, exnStack, JS::ErrorReportBuilder::WithSideEffects)); + + const auto* errorReport = report.report(); + + CHECK(errorReport->errorNumber == expectedErrorNumber); + + if (const auto& notes = errorReport->notes) { + CHECK(sizeof(CharT) == 1); + CHECK(badCodeUnits != nullptr); + + auto iter = notes->begin(); + CHECK(iter != notes->end()); + + const char* noteMessage = (*iter)->message().c_str(); + + // The prefix ought always be the same. + static constexpr char expectedPrefix[] = + "the code units comprising this invalid code point were: "; + constexpr size_t expectedPrefixLen = js_strlen(expectedPrefix); + + CHECK(startsWith(noteMessage, expectedPrefix)); + + // The end of the prefix is the bad code units. + CHECK(equals(noteMessage + expectedPrefixLen, badCodeUnits)); + + ++iter; + CHECK(iter == notes->end()); + } else { + CHECK(sizeof(CharT) == 2); + + // UTF-16 encoding "errors" are not categorical errors, so the errors + // are just of the invalid-character sort, without an accompanying note + // spelling out a series of invalid code units. + CHECK(!badCodeUnits); + } + + CHECK(!errorReport->linebuf()); + + return true; +} +END_TEST(testEmptyWindow) diff --git a/js/src/jsapi-tests/testErrorCopying.cpp b/js/src/jsapi-tests/testErrorCopying.cpp new file mode 100644 index 0000000000..e6a85bfee9 --- /dev/null +++ b/js/src/jsapi-tests/testErrorCopying.cpp @@ -0,0 +1,33 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + * + * Tests that the column number of error reports is properly copied over from + * other reports when invoked from the C++ api. + */ +/* 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 "js/CallAndConstruct.h" +#include "js/Exception.h" +#include "jsapi-tests/tests.h" + +BEGIN_TEST(testErrorCopying_columnCopied) { + // 0 1 2 + // 1234567890123456789012345678 + EXEC("function check() { Object; foo; }"); + + JS::RootedValue rval(cx); + CHECK(!JS_CallFunctionName(cx, global, "check", JS::HandleValueArray::empty(), + &rval)); + JS::ExceptionStack exnStack(cx); + CHECK(JS::StealPendingExceptionStack(cx, &exnStack)); + + JS::ErrorReportBuilder report(cx); + CHECK(report.init(cx, exnStack, JS::ErrorReportBuilder::WithSideEffects)); + + CHECK_EQUAL(report.report()->column.oneOriginValue(), 28u); + return true; +} + +END_TEST(testErrorCopying_columnCopied) diff --git a/js/src/jsapi-tests/testErrorInterceptor.cpp b/js/src/jsapi-tests/testErrorInterceptor.cpp new file mode 100644 index 0000000000..031fa85afa --- /dev/null +++ b/js/src/jsapi-tests/testErrorInterceptor.cpp @@ -0,0 +1,145 @@ +#include <iterator> + +#include "js/ErrorInterceptor.h" +#include "jsapi-tests/tests.h" +#include "util/StringBuffer.h" + +// Tests for JS_GetErrorInterceptorCallback and JS_SetErrorInterceptorCallback. + +namespace { +static JS::PersistentRootedString gLatestMessage; + +// An interceptor that stores the error in `gLatestMessage`. +struct SimpleInterceptor : JSErrorInterceptor { + virtual void interceptError(JSContext* cx, JS::HandleValue val) override { + js::JSStringBuilder buffer(cx); + if (!ValueToStringBuffer(cx, val, buffer)) { + MOZ_CRASH("Could not convert to string buffer"); + } + gLatestMessage = buffer.finishString(); + if (!gLatestMessage) { + MOZ_CRASH("Could not convert to string"); + } + } +}; + +bool equalStrings(JSContext* cx, JSString* a, JSString* b) { + int32_t result = 0; + if (!JS_CompareStrings(cx, a, b, &result)) { + MOZ_CRASH("Could not compare strings"); + } + return result == 0; +} +} // namespace + +BEGIN_TEST(testErrorInterceptor) { + // Run the following snippets. + const char* SAMPLES[] = { + "throw new Error('I am an Error')\0", + "throw new TypeError('I am a TypeError')\0", + "throw new ReferenceError('I am a ReferenceError')\0", + "throw new SyntaxError('I am a SyntaxError')\0", + "throw 5\0", + "foo[0]\0", + "b[\0", + }; + // With the simpleInterceptor, we should end up with the following error: + const char* TO_STRING[] = { + "Error: I am an Error\0", + "TypeError: I am a TypeError\0", + "ReferenceError: I am a ReferenceError\0", + "SyntaxError: I am a SyntaxError\0", + "5\0", + "ReferenceError: foo is not defined\0", + "SyntaxError: expected expression, got end of script\0", + }; + static_assert(std::size(SAMPLES) == std::size(TO_STRING)); + + // Save original callback. + JSErrorInterceptor* original = JS_GetErrorInterceptorCallback(cx->runtime()); + gLatestMessage.init(cx); + + // Test without callback. + JS_SetErrorInterceptorCallback(cx->runtime(), nullptr); + CHECK(gLatestMessage == nullptr); + + for (auto sample : SAMPLES) { + if (execDontReport(sample, __FILE__, __LINE__)) { + MOZ_CRASH("This sample should have failed"); + } + CHECK(JS_IsExceptionPending(cx)); + CHECK(gLatestMessage == nullptr); + JS_ClearPendingException(cx); + } + + // Test with callback. + SimpleInterceptor simpleInterceptor; + JS_SetErrorInterceptorCallback(cx->runtime(), &simpleInterceptor); + + // Test that we return the right callback. + CHECK_EQUAL(JS_GetErrorInterceptorCallback(cx->runtime()), + &simpleInterceptor); + + // This shouldn't cause any error. + EXEC("function bar() {}"); + CHECK(gLatestMessage == nullptr); + + // Test error throwing with a callback that succeeds. + for (size_t i = 0; i < std::size(SAMPLES); ++i) { + // This should cause the appropriate error. + if (execDontReport(SAMPLES[i], __FILE__, __LINE__)) { + MOZ_CRASH("This sample should have failed"); + } + CHECK(JS_IsExceptionPending(cx)); + + // Check result of callback. + CHECK(gLatestMessage != nullptr); + CHECK(js::StringEqualsAscii(&gLatestMessage->asLinear(), TO_STRING[i])); + + // Check the final error. + JS::RootedValue exn(cx); + CHECK(JS_GetPendingException(cx, &exn)); + JS_ClearPendingException(cx); + + js::JSStringBuilder buffer(cx); + CHECK(ValueToStringBuffer(cx, exn, buffer)); + JS::Rooted<JSLinearString*> linear(cx, buffer.finishString()); + CHECK(equalStrings(cx, linear, gLatestMessage)); + + // Cleanup. + gLatestMessage = nullptr; + } + + // Test again without callback. + JS_SetErrorInterceptorCallback(cx->runtime(), nullptr); + for (size_t i = 0; i < std::size(SAMPLES); ++i) { + if (execDontReport(SAMPLES[i], __FILE__, __LINE__)) { + MOZ_CRASH("This sample should have failed"); + } + CHECK(JS_IsExceptionPending(cx)); + + // Check that the callback wasn't called. + CHECK(gLatestMessage == nullptr); + + // Check the final error. + JS::RootedValue exn(cx); + CHECK(JS_GetPendingException(cx, &exn)); + JS_ClearPendingException(cx); + + js::JSStringBuilder buffer(cx); + CHECK(ValueToStringBuffer(cx, exn, buffer)); + JS::Rooted<JSLinearString*> linear(cx, buffer.finishString()); + CHECK(js::StringEqualsAscii(linear, TO_STRING[i])); + + // Cleanup. + gLatestMessage = nullptr; + } + + // Cleanup + JS_SetErrorInterceptorCallback(cx->runtime(), original); + gLatestMessage = nullptr; + JS_ClearPendingException(cx); + + return true; +} +END_TEST(testErrorInterceptor) diff --git a/js/src/jsapi-tests/testErrorInterceptorGC.cpp b/js/src/jsapi-tests/testErrorInterceptorGC.cpp new file mode 100644 index 0000000000..67dac2f2dd --- /dev/null +++ b/js/src/jsapi-tests/testErrorInterceptorGC.cpp @@ -0,0 +1,32 @@ +#include "js/ErrorInterceptor.h" +#include "jsapi-tests/tests.h" + +namespace { + +// An interceptor that triggers GC: +struct ErrorInterceptorWithGC : JSErrorInterceptor { + void interceptError(JSContext* cx, JS::HandleValue val) override { + JS::PrepareForFullGC(cx); + JS::NonIncrementalGC(cx, JS::GCOptions::Shrink, JS::GCReason::DEBUG_GC); + } +}; + +} // namespace + +BEGIN_TEST(testErrorInterceptorGC) { + JSErrorInterceptor* original = JS_GetErrorInterceptorCallback(cx->runtime()); + + ErrorInterceptorWithGC interceptor; + JS_SetErrorInterceptorCallback(cx->runtime(), &interceptor); + + CHECK(!execDontReport("0 = 0;", __FILE__, __LINE__)); + + CHECK(JS_IsExceptionPending(cx)); + JS_ClearPendingException(cx); + + // Restore the original error interceptor. + JS_SetErrorInterceptorCallback(cx->runtime(), original); + + return true; +} +END_TEST(testErrorInterceptorGC) diff --git a/js/src/jsapi-tests/testErrorLineOfContext.cpp b/js/src/jsapi-tests/testErrorLineOfContext.cpp new file mode 100644 index 0000000000..ecfd895527 --- /dev/null +++ b/js/src/jsapi-tests/testErrorLineOfContext.cpp @@ -0,0 +1,74 @@ +/* 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 "js/CompilationAndEvaluation.h" +#include "js/Exception.h" +#include "js/GlobalObject.h" +#include "js/SourceText.h" +#include "jsapi-tests/tests.h" +#include "vm/ErrorReporting.h" + +BEGIN_TEST(testErrorLineOfContext) { + static const char16_t fullLineR[] = u"\n var x = @; \r "; + CHECK(testLineOfContextHasNoLineTerminator(fullLineR, ' ')); + + static const char16_t fullLineN[] = u"\n var x = @; !\n "; + CHECK(testLineOfContextHasNoLineTerminator(fullLineN, '!')); + + static const char16_t fullLineLS[] = u"\n var x = @; +\u2028 "; + CHECK(testLineOfContextHasNoLineTerminator(fullLineLS, '+')); + + static const char16_t fullLinePS[] = u"\n var x = @; #\u2029 "; + CHECK(testLineOfContextHasNoLineTerminator(fullLinePS, '#')); + + static_assert(js::ErrorMetadata::lineOfContextRadius == 60, + "current max count past offset is 60, hits 'X' below"); + + static const char16_t truncatedLine[] = + u"@ + 4567890123456789012345678901234567890123456789012345678XYZW\n"; + CHECK(testLineOfContextHasNoLineTerminator(truncatedLine, 'X')); + + return true; +} + +bool eval(const char16_t* chars, size_t len, JS::MutableHandleValue rval) { + JS::RealmOptions globalOptions; + JS::RootedObject global( + cx, JS_NewGlobalObject(cx, getGlobalClass(), nullptr, + JS::FireOnNewGlobalHook, globalOptions)); + CHECK(global); + + JSAutoRealm ar(cx, global); + + JS::SourceText<char16_t> srcBuf; + CHECK(srcBuf.init(cx, chars, len, JS::SourceOwnership::Borrowed)); + + JS::CompileOptions options(cx); + return JS::Evaluate(cx, options, srcBuf, rval); +} + +template <size_t N> +bool testLineOfContextHasNoLineTerminator(const char16_t (&chars)[N], + char16_t expectedLast) { + JS::RootedValue rval(cx); + CHECK(!eval(chars, N - 1, &rval)); + + JS::ExceptionStack exnStack(cx); + CHECK(JS::StealPendingExceptionStack(cx, &exnStack)); + + JS::ErrorReportBuilder report(cx); + CHECK(report.init(cx, exnStack, JS::ErrorReportBuilder::WithSideEffects)); + + const auto* errorReport = report.report(); + + const char16_t* lineOfContext = errorReport->linebuf(); + size_t lineOfContextLength = errorReport->linebufLength(); + + CHECK(lineOfContext[lineOfContextLength] == '\0'); + char16_t last = lineOfContext[lineOfContextLength - 1]; + CHECK(last == expectedLast); + + return true; +} +END_TEST(testErrorLineOfContext) diff --git a/js/src/jsapi-tests/testException.cpp b/js/src/jsapi-tests/testException.cpp new file mode 100644 index 0000000000..dbf5afbda1 --- /dev/null +++ b/js/src/jsapi-tests/testException.cpp @@ -0,0 +1,95 @@ +/* -*- 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 "js/CallAndConstruct.h" // JS_CallFunctionValue +#include "js/ColumnNumber.h" // JS::ColumnNumberOneOrigin +#include "js/PropertyAndElement.h" // JS_GetProperty +#include "jsapi-tests/tests.h" + +BEGIN_TEST(testException_bug860435) { + JS::RootedValue fun(cx); + + EVAL("ReferenceError", &fun); + CHECK(fun.isObject()); + + JS::RootedValue v(cx); + CHECK( + JS_CallFunctionValue(cx, global, fun, JS::HandleValueArray::empty(), &v)); + CHECK(v.isObject()); + JS::RootedObject obj(cx, &v.toObject()); + + CHECK(JS_GetProperty(cx, obj, "stack", &v)); + CHECK(v.isString()); + return true; +} +END_TEST(testException_bug860435) + +BEGIN_TEST(testException_getCause) { + JS::RootedValue err(cx); + EVAL("new Error('message', { cause: new Error('message 2') })", &err); + CHECK(err.isObject()); + + JS::RootedString msg(cx, JS::ToString(cx, err)); + CHECK(msg); + // Check that we have the outer error + bool match; + CHECK(JS_StringEqualsLiteral(cx, msg, "Error: message", &match)); + CHECK(match); + + JS::Rooted<mozilla::Maybe<JS::Value>> maybeCause( + cx, JS::GetExceptionCause(&err.toObject())); + CHECK(maybeCause.isSome()); + JS::RootedValue cause(cx, *maybeCause); + CHECK(cause.isObject()); + + msg = JS::ToString(cx, cause); + CHECK(msg); + // Check that we have the inner error + CHECK(JS_StringEqualsLiteral(cx, msg, "Error: message 2", &match)); + CHECK(match); + + maybeCause = JS::GetExceptionCause(&cause.toObject()); + CHECK(maybeCause.isNothing()); + + return true; +} +END_TEST(testException_getCause) + +BEGIN_TEST(testException_getCausePlainObject) { + JS::RootedObject plain(cx, JS_NewPlainObject(cx)); + CHECK(plain); + JS::Rooted<mozilla::Maybe<JS::Value>> maybeCause( + cx, JS::GetExceptionCause(plain)); + CHECK(maybeCause.isNothing()); + return true; +} +END_TEST(testException_getCausePlainObject) + +BEGIN_TEST(testException_createErrorWithCause) { + JS::RootedString empty(cx, JS_GetEmptyString(cx)); + JS::Rooted<mozilla::Maybe<JS::Value>> cause( + cx, mozilla::Some(JS::Int32Value(-1))); + JS::RootedValue err(cx); + CHECK(JS::CreateError(cx, JSEXN_ERR, nullptr, empty, 1, + JS::ColumnNumberOneOrigin(1), nullptr, empty, cause, + &err)); + CHECK(err.isObject()); + JS::Rooted<mozilla::Maybe<JS::Value>> maybeCause( + cx, JS::GetExceptionCause(&err.toObject())); + CHECK(maybeCause.isSome()); + CHECK_SAME(*cause, *maybeCause); + + CHECK(JS::CreateError(cx, JSEXN_ERR, nullptr, empty, 1, + JS::ColumnNumberOneOrigin(1), nullptr, empty, + JS::NothingHandleValue, &err)); + CHECK(err.isObject()); + maybeCause = JS::GetExceptionCause(&err.toObject()); + CHECK(maybeCause.isNothing()); + + return true; +} +END_TEST(testException_createErrorWithCause) diff --git a/js/src/jsapi-tests/testExecuteInJSMEnvironment.cpp b/js/src/jsapi-tests/testExecuteInJSMEnvironment.cpp new file mode 100644 index 0000000000..ee42667589 --- /dev/null +++ b/js/src/jsapi-tests/testExecuteInJSMEnvironment.cpp @@ -0,0 +1,107 @@ +/* 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 "mozilla/Utf8.h" // mozilla::Utf8Unit + +#include "js/CompilationAndEvaluation.h" // JS::Compile +#include "js/friend/JSMEnvironment.h" // JS::ExecuteInJSMEnvironment, JS::GetJSMEnvironmentOfScriptedCaller, JS::NewJSMEnvironment +#include "js/PropertyAndElement.h" // JS_DefineFunctions, JS_GetProperty, JS_SetProperty +#include "js/PropertySpec.h" +#include "js/SourceText.h" // JS::Source{Ownership,Text} +#include "jsapi-tests/tests.h" +#include "util/Text.h" +#include "vm/EnvironmentObject.h" +#include "vm/EnvironmentObject-inl.h" + +BEGIN_TEST(testExecuteInJSMEnvironment_Basic) { + static const char src[] = + "var output = input;\n" + "\n" + "a = 1;\n" + "var b = 2;\n" + "let c = 3;\n" + "this.d = 4;\n" + "eval('this.e = 5');\n" + "(0,eval)('this.f = 6');\n" + "(function() { this.g = 7; })();\n" + "function f_h() { this.h = 8; }; f_h();\n"; + + JS::CompileOptions options(cx); + options.setFileAndLine(__FILE__, __LINE__); + options.setNoScriptRval(true); + options.setNonSyntacticScope(true); + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + CHECK(srcBuf.init(cx, src, js_strlen(src), JS::SourceOwnership::Borrowed)); + + JS::RootedScript script(cx, JS::Compile(cx, options, srcBuf)); + CHECK(script); + + JS::RootedObject varEnv(cx, JS::NewJSMEnvironment(cx)); + JS::RootedObject lexEnv(cx, JS_ExtensibleLexicalEnvironment(varEnv)); + CHECK(varEnv && varEnv->is<js::NonSyntacticVariablesObject>()); + CHECK(lexEnv && js::IsExtensibleLexicalEnvironment(lexEnv)); + CHECK(lexEnv->enclosingEnvironment() == varEnv); + + JS::RootedValue vi(cx, JS::Int32Value(1000)); + CHECK(JS_SetProperty(cx, varEnv, "input", vi)); + + CHECK(JS::ExecuteInJSMEnvironment(cx, script, varEnv)); + + JS::RootedValue v(cx); + CHECK(JS_GetProperty(cx, varEnv, "output", &v) && v == vi); + CHECK(JS_GetProperty(cx, varEnv, "a", &v) && v == JS::Int32Value(1)); + CHECK(JS_GetProperty(cx, varEnv, "b", &v) && v == JS::Int32Value(2)); + CHECK(JS_GetProperty(cx, lexEnv, "c", &v) && v == JS::Int32Value(3)); + CHECK(JS_GetProperty(cx, varEnv, "d", &v) && v == JS::Int32Value(4)); + CHECK(JS_GetProperty(cx, varEnv, "e", &v) && v == JS::Int32Value(5)); + // TODO: Bug 1396050 will fix this + // CHECK(JS_GetProperty(cx, varEnv, "f", &v) && v == JS::Int32Value(6)); + CHECK(JS_GetProperty(cx, varEnv, "g", &v) && v == JS::Int32Value(7)); + CHECK(JS_GetProperty(cx, varEnv, "h", &v) && v == JS::Int32Value(8)); + + return true; +} +END_TEST(testExecuteInJSMEnvironment_Basic); + +static bool test_callback(JSContext* cx, unsigned argc, JS::Value* vp) { + JS::RootedObject env(cx, JS::GetJSMEnvironmentOfScriptedCaller(cx)); + if (!env) { + return false; + } + + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + args.rval().setObject(*env); + return true; +} + +static const JSFunctionSpec testFunctions[] = { + JS_FN("callback", test_callback, 0, 0), JS_FS_END}; + +BEGIN_TEST(testExecuteInJSMEnvironment_Callback) { + static const char src[] = "var output = callback();\n"; + + CHECK(JS_DefineFunctions(cx, global, testFunctions)); + + JS::CompileOptions options(cx); + options.setFileAndLine(__FILE__, __LINE__); + options.setNoScriptRval(true); + options.setNonSyntacticScope(true); + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + CHECK(srcBuf.init(cx, src, js_strlen(src), JS::SourceOwnership::Borrowed)); + + JS::RootedScript script(cx, JS::Compile(cx, options, srcBuf)); + CHECK(script); + + JS::RootedObject nsvo(cx, JS::NewJSMEnvironment(cx)); + CHECK(nsvo); + CHECK(JS::ExecuteInJSMEnvironment(cx, script, nsvo)); + + JS::RootedValue v(cx); + CHECK(JS_GetProperty(cx, nsvo, "output", &v) && v == JS::ObjectValue(*nsvo)); + + return true; +} +END_TEST(testExecuteInJSMEnvironment_Callback) diff --git a/js/src/jsapi-tests/testExternalStrings.cpp b/js/src/jsapi-tests/testExternalStrings.cpp new file mode 100644 index 0000000000..7b615f39c6 --- /dev/null +++ b/js/src/jsapi-tests/testExternalStrings.cpp @@ -0,0 +1,202 @@ +/* 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 "js/CharacterEncoding.h" // JS::FindSmallestEncoding, JS::SmallestEncoding +#include "js/GCAPI.h" // JSExternalStringCallbacks, JS_NewExternalUCString, JS_NewExternalStringLatin1, JS_NewMaybeExternalStringUTF8, JS::AutoRequireNoGC +#include "js/String.h" // JS::IsExternalStringLatin1 +#include "jsapi-tests/tests.h" +#include "util/Text.h" + +static const char16_t arr[] = u"hi, don't delete me"; +static const size_t arrlen = js_strlen(arr); + +static const char arr2[] = "hi, don't delete me"; +static const size_t arrlen2 = js_strlen(arr2); + +static int finalized1 = 0; +static int finalized2 = 0; +static int finalized3 = 0; +static int finalized4 = 0; + +struct ExternalStringCallbacks : public JSExternalStringCallbacks { + int* finalizedCount = nullptr; + bool isTwoBytes = false; + + explicit ExternalStringCallbacks(int* finalizedCount, bool isTwoBytes) + : finalizedCount(finalizedCount), isTwoBytes(isTwoBytes) {} + + void finalize(JS::Latin1Char* chars) const override { + MOZ_ASSERT(!isTwoBytes); + MOZ_ASSERT(chars == (JS::Latin1Char*)arr2); + (*finalizedCount)++; + } + + void finalize(char16_t* chars) const override { + MOZ_ASSERT(isTwoBytes); + MOZ_ASSERT(chars == arr); + (*finalizedCount)++; + } + + size_t sizeOfBuffer(const JS::Latin1Char* chars, + mozilla::MallocSizeOf mallocSizeOf) const override { + MOZ_CRASH("Unexpected call"); + } + + size_t sizeOfBuffer(const char16_t* chars, + mozilla::MallocSizeOf mallocSizeOf) const override { + MOZ_CRASH("Unexpected call"); + } +}; + +static const ExternalStringCallbacks callbacks1(&finalized1, true); +static const ExternalStringCallbacks callbacks2(&finalized2, true); +static const ExternalStringCallbacks callbacks3(&finalized3, false); +static const ExternalStringCallbacks callbacks4(&finalized4, false); + +BEGIN_TEST(testExternalStrings) { + const unsigned N = 1000; + + for (unsigned i = 0; i < N; ++i) { + CHECK(JS_NewExternalUCString(cx, arr, arrlen, &callbacks1)); + CHECK(JS_NewExternalUCString(cx, arr, arrlen, &callbacks2)); + CHECK(JS_NewExternalStringLatin1(cx, (JS::Latin1Char*)arr2, arrlen2, + &callbacks3)); + CHECK(JS_NewExternalStringLatin1(cx, (JS::Latin1Char*)arr2, arrlen2, + &callbacks4)); + } + + JS_GC(cx); + + CHECK((N - finalized1) == 0); + CHECK((N - finalized2) == 0); + CHECK((N - finalized3) == 0); + CHECK((N - finalized4) == 0); + + return true; +} +END_TEST(testExternalStrings) + +struct SimpleExternalStringCallbacks : public JSExternalStringCallbacks { + SimpleExternalStringCallbacks() = default; + + void finalize(JS::Latin1Char* chars) const override {} + + void finalize(char16_t* chars) const override { + MOZ_CRASH("Unexpected call"); + } + + size_t sizeOfBuffer(const JS::Latin1Char* chars, + mozilla::MallocSizeOf mallocSizeOf) const override { + MOZ_CRASH("Unexpected call"); + } + + size_t sizeOfBuffer(const char16_t* chars, + mozilla::MallocSizeOf mallocSizeOf) const override { + MOZ_CRASH("Unexpected call"); + } +}; + +static const SimpleExternalStringCallbacks simpleCallback; + +static const char utf8ASCII[] = "hi, I'm UTF-8 string"; +static const size_t utf8ASCIILen = js_strlen(utf8ASCII); + +static const char utf8Latin1[] = "hi, I'm \xC3\x9CTF-8 string"; +static const size_t utf8Latin1Len = js_strlen(utf8Latin1); + +static const char latin1[] = "hi, I'm \xDCTF-8 string"; +static const size_t latin1Len = js_strlen(latin1); + +static const char utf8UTF16[] = "hi, I'm UTF-\xEF\xBC\x98 string"; +static const size_t utf8UTF16Len = js_strlen(utf8UTF16); + +static const char16_t utf16[] = u"hi, I'm UTF-8 string"; +static const size_t utf16Len = js_strlen(utf16); + +BEGIN_TEST(testExternalStringsUTF8) { + // UTF-8 chars with ASCII range content should be converted into external + // string. + { + JS::UTF8Chars utf8Chars(utf8ASCII, utf8ASCIILen); + CHECK(JS::FindSmallestEncoding(utf8Chars) == JS::SmallestEncoding::ASCII); + bool allocatedExternal = false; + JS::Rooted<JSString*> str( + cx, JS_NewMaybeExternalStringUTF8(cx, utf8Chars, &simpleCallback, + &allocatedExternal)); + CHECK(str); + CHECK(allocatedExternal); + + const JSExternalStringCallbacks* callbacks = nullptr; + const JS::Latin1Char* chars = nullptr; + CHECK(JS::IsExternalStringLatin1(str, &callbacks, &chars)); + CHECK(callbacks == &simpleCallback); + CHECK((void*)chars == (void*)utf8ASCII); + + CHECK(StringHasLatin1Chars(str)); + + JS::AutoAssertNoGC nogc(cx); + size_t length; + chars = JS_GetLatin1StringCharsAndLength(cx, nogc, str, &length); + CHECK(length == utf8ASCIILen); + CHECK(memcmp(chars, utf8ASCII, length) == 0); + } + + // UTF-8 chars with latin-1 range content shouldn't be converted into external + // string, but regular latin-1 string. + { + JS::UTF8Chars utf8Chars(utf8Latin1, utf8Latin1Len); + CHECK(JS::FindSmallestEncoding(utf8Chars) == JS::SmallestEncoding::Latin1); + bool allocatedExternal = false; + JS::Rooted<JSString*> str( + cx, JS_NewMaybeExternalStringUTF8(cx, utf8Chars, &simpleCallback, + &allocatedExternal)); + CHECK(str); + CHECK(!allocatedExternal); + + const JSExternalStringCallbacks* callbacks = nullptr; + const JS::Latin1Char* chars = nullptr; + CHECK(!JS::IsExternalStringLatin1(str, &callbacks, &chars)); + CHECK(!callbacks); + CHECK(!chars); + + CHECK(StringHasLatin1Chars(str)); + + JS::AutoAssertNoGC nogc(cx); + size_t length; + chars = JS_GetLatin1StringCharsAndLength(cx, nogc, str, &length); + CHECK(length == latin1Len); + CHECK(memcmp(chars, latin1, length) == 0); + } + + // UTF-8 chars with UTF-16 range content shouldn't be converted into external + // string, but regular TwoBytes string. + { + JS::UTF8Chars utf8Chars(utf8UTF16, utf8UTF16Len); + CHECK(JS::FindSmallestEncoding(utf8Chars) == JS::SmallestEncoding::UTF16); + bool allocatedExternal = false; + JS::Rooted<JSString*> str( + cx, JS_NewMaybeExternalStringUTF8(cx, utf8Chars, &simpleCallback, + &allocatedExternal)); + CHECK(str); + CHECK(!allocatedExternal); + + const JSExternalStringCallbacks* callbacks = nullptr; + const JS::Latin1Char* chars = nullptr; + CHECK(!JS::IsExternalStringLatin1(str, &callbacks, &chars)); + CHECK(!callbacks); + CHECK(!chars); + + CHECK(!StringHasLatin1Chars(str)); + + JS::AutoAssertNoGC nogc(cx); + size_t length; + const char16_t* chars16 = nullptr; + chars16 = JS_GetTwoByteStringCharsAndLength(cx, nogc, str, &length); + CHECK(length == utf16Len); + CHECK(memcmp(chars16, utf16, length * sizeof(char16_t)) == 0); + } + + return true; +} +END_TEST(testExternalStringsUTF8) diff --git a/js/src/jsapi-tests/testFindSCCs.cpp b/js/src/jsapi-tests/testFindSCCs.cpp new file mode 100644 index 0000000000..2c0db7daa1 --- /dev/null +++ b/js/src/jsapi-tests/testFindSCCs.cpp @@ -0,0 +1,241 @@ +/* -*- 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 <stdarg.h> +#include <string.h> + +#include "gc/FindSCCs.h" +#include "jsapi-tests/tests.h" + +static const unsigned MaxVertices = 10; + +using js::gc::ComponentFinder; +using js::gc::GraphNodeBase; + +struct TestNode : public GraphNodeBase<TestNode> { + unsigned index; +}; + +using TestComponentFinder = ComponentFinder<TestNode>; + +static TestNode Vertex[MaxVertices]; + +BEGIN_TEST(testFindSCCs) { + // no vertices + + setup(0); + run(); + CHECK(end()); + + // no edges + + setup(1); + run(); + CHECK(group(0, -1)); + CHECK(end()); + + setup(3); + run(); + CHECK(group(2, -1)); + CHECK(group(1, -1)); + CHECK(group(0, -1)); + CHECK(end()); + + // linear + + setup(3); + CHECK(edge(0, 1)); + CHECK(edge(1, 2)); + run(); + CHECK(group(0, -1)); + CHECK(group(1, -1)); + CHECK(group(2, -1)); + CHECK(end()); + + // tree + + setup(3); + CHECK(edge(0, 1)); + CHECK(edge(0, 2)); + run(); + CHECK(group(0, -1)); + if (resultsList && resultsList->index == 1) { + CHECK(group(1, -1)); + CHECK(group(2, -1)); + } else { + CHECK(group(2, -1)); + CHECK(group(1, -1)); + } + CHECK(end()); + + // cycles + + setup(3); + CHECK(edge(0, 1)); + CHECK(edge(1, 2)); + CHECK(edge(2, 0)); + run(); + CHECK(group(0, 1, 2, -1)); + CHECK(end()); + + setup(4); + CHECK(edge(0, 1)); + CHECK(edge(1, 2)); + CHECK(edge(2, 1)); + CHECK(edge(2, 3)); + run(); + CHECK(group(0, -1)); + CHECK(group(1, 2, -1)); + CHECK(group(3, -1)); + CHECK(end()); + + // remaining + + setup(2); + CHECK(edge(0, 1)); + run(); + CHECK(remaining(0, 1, -1)); + CHECK(end()); + + setup(2); + CHECK(edge(0, 1)); + run(); + CHECK(group(0, -1)); + CHECK(remaining(1, -1)); + CHECK(end()); + + setup(2); + CHECK(edge(0, 1)); + run(); + CHECK(group(0, -1)); + CHECK(group(1, -1)); + CHECK(remaining(-1)); + CHECK(end()); + + return true; +} + +unsigned vertex_count; +TestComponentFinder* finder; +TestNode* resultsList; + +void setup(unsigned count) { + vertex_count = count; + for (unsigned i = 0; i < MaxVertices; ++i) { + TestNode& v = Vertex[i]; + v.gcGraphEdges.clear(); + v.gcNextGraphNode = nullptr; + v.index = i; + } +} + +bool edge(unsigned src_index, unsigned dest_index) { + return Vertex[src_index].gcGraphEdges.put(&Vertex[dest_index]); +} + +void run() { + finder = new TestComponentFinder(cx); + for (unsigned i = 0; i < vertex_count; ++i) { + finder->addNode(&Vertex[i]); + } + resultsList = finder->getResultsList(); +} + +bool group(int vertex, ...) { + TestNode* v = resultsList; + + va_list ap; + va_start(ap, vertex); + while (vertex != -1) { + CHECK(v != nullptr); + CHECK(v->index == unsigned(vertex)); + v = v->nextNodeInGroup(); + vertex = va_arg(ap, int); + } + va_end(ap); + + CHECK(v == nullptr); + resultsList = resultsList->nextGroup(); + return true; +} + +bool remaining(int vertex, ...) { + TestNode* v = resultsList; + + va_list ap; + va_start(ap, vertex); + while (vertex != -1) { + CHECK(v != nullptr); + CHECK(v->index == unsigned(vertex)); + v = (TestNode*)v->gcNextGraphNode; + vertex = va_arg(ap, int); + } + va_end(ap); + + CHECK(v == nullptr); + resultsList = nullptr; + return true; +} + +bool end() { + CHECK(resultsList == nullptr); + + delete finder; + finder = nullptr; + return true; +} +END_TEST(testFindSCCs) + +BEGIN_TEST(testFindSCCsStackLimit) { + /* + * Test what happens if recusion causes the stack to become full while + * traversing the graph. + * + * The test case is a large number of vertices, almost all of which are + * arranged in a linear chain. The last few are left unlinked to exercise + * adding vertices after the stack full condition has already been detected. + * + * Such an arrangement with no cycles would normally result in one group for + * each vertex, but since the stack is exhasted in processing a single group + * is returned containing all the vertices. + */ + const unsigned max = 1000000; + const unsigned initial = 10; + + TestNode* vertices = new TestNode[max](); + for (unsigned i = initial; i < (max - 10); ++i) { + CHECK(vertices[i].gcGraphEdges.put(&vertices[i + 1])); + } + + TestComponentFinder finder(cx); + for (unsigned i = 0; i < max; ++i) { + finder.addNode(&vertices[i]); + } + + TestNode* r = finder.getResultsList(); + CHECK(r); + TestNode* v = r; + + unsigned count = 0; + while (v) { + ++count; + v = v->nextNodeInGroup(); + } + CHECK(count == max - initial); + + count = 0; + v = r->nextGroup(); + while (v) { + ++count; + CHECK(!v->nextNodeInGroup()); + v = v->nextGroup(); + } + + delete[] vertices; + return true; +} +END_TEST(testFindSCCsStackLimit) diff --git a/js/src/jsapi-tests/testForOfIterator.cpp b/js/src/jsapi-tests/testForOfIterator.cpp new file mode 100644 index 0000000000..45618d037d --- /dev/null +++ b/js/src/jsapi-tests/testForOfIterator.cpp @@ -0,0 +1,50 @@ +/* -*- 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 "js/ForOfIterator.h" +#include "jsapi-tests/tests.h" + +BEGIN_TEST(testForOfIterator_basicNonIterable) { + JS::RootedValue v(cx); + // Hack to make it simple to produce an object that has a property + // named Symbol.iterator. + EVAL("({[Symbol.iterator]: 5})", &v); + JS::ForOfIterator iter(cx); + bool ok = iter.init(v); + CHECK(!ok); + JS_ClearPendingException(cx); + return true; +} +END_TEST(testForOfIterator_basicNonIterable) + +BEGIN_TEST(testForOfIterator_bug515273_part1) { + JS::RootedValue v(cx); + + // Hack to make it simple to produce an object that has a property + // named Symbol.iterator. + EVAL("({[Symbol.iterator]: 5})", &v); + + JS::ForOfIterator iter(cx); + bool ok = iter.init(v, JS::ForOfIterator::AllowNonIterable); + CHECK(!ok); + JS_ClearPendingException(cx); + return true; +} +END_TEST(testForOfIterator_bug515273_part1) + +BEGIN_TEST(testForOfIterator_bug515273_part2) { + JS::RootedObject obj(cx, JS_NewPlainObject(cx)); + CHECK(obj); + JS::RootedValue v(cx, JS::ObjectValue(*obj)); + + JS::ForOfIterator iter(cx); + bool ok = iter.init(v, JS::ForOfIterator::AllowNonIterable); + CHECK(ok); + CHECK(!iter.valueIsIterable()); + return true; +} +END_TEST(testForOfIterator_bug515273_part2) diff --git a/js/src/jsapi-tests/testForceLexicalInitialization.cpp b/js/src/jsapi-tests/testForceLexicalInitialization.cpp new file mode 100644 index 0000000000..be0468897d --- /dev/null +++ b/js/src/jsapi-tests/testForceLexicalInitialization.cpp @@ -0,0 +1,39 @@ +/* -*- 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 "jsfriendapi.h" +#include "jsapi-tests/tests.h" +#include "vm/EnvironmentObject.h" + +BEGIN_TEST(testForceLexicalInitialization) { + // Attach an uninitialized lexical to a scope and ensure that it's + // set to undefined + JS::Rooted<js::GlobalObject*> g(cx, cx->global()); + JS::Rooted<js::GlobalLexicalEnvironmentObject*> env( + cx, js::GlobalLexicalEnvironmentObject::create(cx, g)); + + JS::RootedValue uninitialized(cx, JS::MagicValue(JS_UNINITIALIZED_LEXICAL)); + JS::Rooted<js::PropertyName*> name(cx, + Atomize(cx, "foopi", 4)->asPropertyName()); + JS::RootedId id(cx, NameToId(name)); + unsigned attrs = JSPROP_ENUMERATE | JSPROP_PERMANENT; + + CHECK(NativeDefineDataProperty(cx, env, id, uninitialized, attrs)); + + // Verify that "foopi" is uninitialized + const JS::Value v = env->getSlot(env->lookup(cx, id)->slot()); + CHECK(v.isMagic(JS_UNINITIALIZED_LEXICAL)); + + ForceLexicalInitialization(cx, env); + + // Verify that "foopi" has been initialized to undefined + const JS::Value v2 = env->getSlot(env->lookup(cx, id)->slot()); + CHECK(v2.isUndefined()); + + return true; +} +END_TEST(testForceLexicalInitialization) diff --git a/js/src/jsapi-tests/testForwardSetProperty.cpp b/js/src/jsapi-tests/testForwardSetProperty.cpp new file mode 100644 index 0000000000..0ab221a0c1 --- /dev/null +++ b/js/src/jsapi-tests/testForwardSetProperty.cpp @@ -0,0 +1,98 @@ +/* -*- 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 "js/PropertyAndElement.h" // JS_ForwardSetPropertyTo +#include "jsapi-tests/tests.h" + +using namespace JS; + +BEGIN_TEST(testForwardSetProperty) { + RootedValue v1(cx); + EVAL( + "var foundValue; \n" + "var obj1 = { set prop(val) { foundValue = this; } }; \n" + "obj1;", + &v1); + + RootedValue v2(cx); + EVAL( + "var obj2 = Object.create(obj1); \n" + "obj2;", + &v2); + + RootedValue v3(cx); + EVAL( + "var obj3 = {}; \n" + "obj3;", + &v3); + + RootedObject obj1(cx, &v1.toObject()); + RootedObject obj2(cx, &v2.toObject()); + RootedObject obj3(cx, &v3.toObject()); + + RootedValue setval(cx, Int32Value(42)); + + RootedValue propkey(cx); + EVAL("'prop';", &propkey); + + RootedId prop(cx); + CHECK(JS_ValueToId(cx, propkey, &prop)); + + EXEC( + "function assertEq(a, b, msg) \n" + "{ \n" + " if (!Object.is(a, b)) \n" + " throw new Error('Assertion failure: ' + msg); \n" + "}"); + + // Non-strict setter + + JS::ObjectOpResult result; + CHECK(JS_ForwardSetPropertyTo(cx, obj2, prop, setval, v3, result)); + CHECK(result); + + EXEC("assertEq(foundValue, obj3, 'wrong receiver passed to setter');"); + + CHECK(JS_ForwardSetPropertyTo(cx, obj2, prop, setval, setval, result)); + CHECK(result); + + EXEC( + "assertEq(typeof foundValue === 'object', true, \n" + " 'passing 42 as receiver to non-strict setter ' + \n" + " 'must box');"); + + EXEC( + "assertEq(foundValue instanceof Number, true, \n" + " 'passing 42 as receiver to non-strict setter ' + \n" + " 'must box to a Number');"); + + EXEC( + "assertEq(foundValue.valueOf(), 42, \n" + " 'passing 42 as receiver to non-strict setter ' + \n" + " 'must box to new Number(42)');"); + + // Strict setter + + RootedValue v4(cx); + EVAL("({ set prop(val) { 'use strict'; foundValue = this; } })", &v4); + RootedObject obj4(cx, &v4.toObject()); + + CHECK(JS_ForwardSetPropertyTo(cx, obj4, prop, setval, v3, result)); + CHECK(result); + + EXEC("assertEq(foundValue, obj3, 'wrong receiver passed to strict setter');"); + + CHECK(JS_ForwardSetPropertyTo(cx, obj4, prop, setval, setval, result)); + CHECK(result); + + EXEC( + "assertEq(foundValue, 42, \n" + " '42 passed as receiver to strict setter was mangled');"); + + return true; +} +END_TEST(testForwardSetProperty) diff --git a/js/src/jsapi-tests/testFreshGlobalEvalRedefinition.cpp b/js/src/jsapi-tests/testFreshGlobalEvalRedefinition.cpp new file mode 100644 index 0000000000..645bc86c16 --- /dev/null +++ b/js/src/jsapi-tests/testFreshGlobalEvalRedefinition.cpp @@ -0,0 +1,63 @@ +/* -*- 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 "mozilla/Utf8.h" // mozilla::Utf8Unit + +#include "js/CompilationAndEvaluation.h" // JS::Evaluate +#include "js/GlobalObject.h" // JS_NewGlobalObject +#include "js/PropertyAndElement.h" // JS_GetProperty +#include "js/SourceText.h" // JS::Source{Ownership,Text} +#include "jsapi-tests/tests.h" +#include "util/Text.h" + +static bool GlobalResolve(JSContext* cx, JS::HandleObject obj, JS::HandleId id, + bool* resolvedp) { + return JS_ResolveStandardClass(cx, obj, id, resolvedp); +} + +BEGIN_TEST(testRedefineGlobalEval) { + static const JSClassOps clsOps = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + JS_NewEnumerateStandardClasses, // newEnumerate + GlobalResolve, // resolve + nullptr, // mayResolve + nullptr, // finalize + nullptr, // call + nullptr, // construct + JS_GlobalObjectTraceHook, // trace + }; + + static const JSClass cls = {"global", JSCLASS_GLOBAL_FLAGS, &clsOps}; + + /* Create the global object. */ + JS::RealmOptions options; + JS::Rooted<JSObject*> g( + cx, + JS_NewGlobalObject(cx, &cls, nullptr, JS::FireOnNewGlobalHook, options)); + if (!g) { + return false; + } + + JSAutoRealm ar(cx, g); + JS::Rooted<JS::Value> v(cx); + CHECK(JS_GetProperty(cx, g, "Object", &v)); + + static const char data[] = + "Object.defineProperty(this, 'eval', { configurable: false });"; + + JS::CompileOptions opts(cx); + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + CHECK(srcBuf.init(cx, data, js_strlen(data), JS::SourceOwnership::Borrowed)); + + CHECK(JS::Evaluate(cx, opts.setFileAndLine(__FILE__, __LINE__), srcBuf, &v)); + + return true; +} +END_TEST(testRedefineGlobalEval) diff --git a/js/src/jsapi-tests/testFrontendCompileStencil.cpp b/js/src/jsapi-tests/testFrontendCompileStencil.cpp new file mode 100644 index 0000000000..ba29b5aea2 --- /dev/null +++ b/js/src/jsapi-tests/testFrontendCompileStencil.cpp @@ -0,0 +1,69 @@ +/* -*- 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 "mozilla/RefPtr.h" +#include "mozilla/Utf8.h" + +#include <string> + +#include "frontend/FrontendContext.h" // js::FrontendContext +#include "js/CompileOptions.h" +#include "js/experimental/CompileScript.h" +#include "js/SourceText.h" +#include "js/Stack.h" +#include "jsapi-tests/tests.h" +#include "util/NativeStack.h" // js::GetNativeStackBase + +using namespace JS; + +BEGIN_FRONTEND_TEST(testFrontendContextCompileGlobalScriptToStencil) { + JS::FrontendContext* fc = JS::NewFrontendContext(); + CHECK(fc); + + static constexpr JS::NativeStackSize stackSize = 128 * sizeof(size_t) * 1024; + + JS::SetNativeStackQuota(fc, stackSize); + +#ifndef __wasi__ + CHECK(fc->stackLimit() == + JS::GetNativeStackLimit(js::GetNativeStackBase(), stackSize - 1)); +#endif + + JS::PrefableCompileOptions prefableOptions; + JS::CompileOptions options(prefableOptions); + + { + const char source[] = "var a = 10;"; + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + CHECK( + srcBuf.init(fc, source, strlen(source), JS::SourceOwnership::Borrowed)); + JS::CompilationStorage compileStorage; + RefPtr<JS::Stencil> stencil = + JS::CompileGlobalScriptToStencil(fc, options, srcBuf, compileStorage); + CHECK(stencil); + CHECK(compileStorage.hasInput()); + } + + { + const char16_t source[] = u"var a = 10;"; + + JS::SourceText<char16_t> srcBuf; + CHECK(srcBuf.init(fc, source, std::char_traits<char16_t>::length(source), + JS::SourceOwnership::Borrowed)); + JS::CompilationStorage compileStorage; + RefPtr<JS::Stencil> stencil = + JS::CompileGlobalScriptToStencil(fc, options, srcBuf, compileStorage); + CHECK(stencil); + CHECK(compileStorage.hasInput()); + } + + JS::DestroyFrontendContext(fc); + + return true; +} + +END_TEST(testFrontendContextCompileGlobalScriptToStencil) diff --git a/js/src/jsapi-tests/testFrontendErrors.cpp b/js/src/jsapi-tests/testFrontendErrors.cpp new file mode 100644 index 0000000000..c9d3ee1eb5 --- /dev/null +++ b/js/src/jsapi-tests/testFrontendErrors.cpp @@ -0,0 +1,353 @@ +/* -*- 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 "mozilla/RefPtr.h" // RefPtr +#include "mozilla/Utf8.h" // mozilla::Utf8Unit + +#include <string.h> // strcmp + +#include "frontend/FrontendContext.h" // js::FrontendContext +#include "js/AllocPolicy.h" // js::ReportOutOfMemory +#include "js/CompileOptions.h" // JS::PrefableCompileOptions, JS::CompileOptions +#include "js/Exception.h" // JS_IsExceptionPending, JS_IsThrowingOutOfMemory, JS_GetPendingException, JS_ClearPendingException, JS_ErrorFromException +#include "js/experimental/CompileScript.h" // JS::NewFrontendContext, JS::DestroyFrontendContext, JS::SetNativeStackQuota, JS::CompileGlobalScriptToStencil, JS::ConvertFrontendErrorsToRuntimeErrors, JS::HadFrontendErrors, JS::HadFrontendOverRecursed, JS::HadFrontendOutOfMemory, JS::HadFrontendAllocationOverflow, JS::GetFrontendWarningCount, JS::GetFrontendWarningAt +#include "js/friend/ErrorMessages.h" // JSMSG_* +#include "js/friend/StackLimits.h" // js::ReportOverRecursed +#include "js/RootingAPI.h" // JS::Rooted +#include "js/SourceText.h" // JS::SourceText +#include "js/Stack.h" // JS::NativeStackSize +#include "js/Warnings.h" // JS::SetWarningReporter +#include "jsapi-tests/tests.h" +#include "util/NativeStack.h" // js::GetNativeStackBase +#include "vm/ErrorObject.h" // js::ErrorObject +#include "vm/JSContext.h" // JSContext +#include "vm/JSObject.h" // JSObject +#include "vm/NativeObject.h" // js::NativeObject +#include "vm/Runtime.h" // js::ReportAllocationOverflow + +using namespace JS; + +BEGIN_TEST(testFrontendErrors_error) { + JS::FrontendContext* fc = JS::NewFrontendContext(); + CHECK(fc); + + static constexpr JS::NativeStackSize stackSize = 128 * sizeof(size_t) * 1024; + + JS::SetNativeStackQuota(fc, stackSize); + + JS::PrefableCompileOptions prefableOptions; + JS::CompileOptions options(prefableOptions); + const char* filename = "testFrontendErrors_error.js"; + options.setFile(filename); + + CHECK(!JS::HadFrontendErrors(fc)); + + { + const char source[] = "syntax error"; + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + CHECK( + srcBuf.init(fc, source, strlen(source), JS::SourceOwnership::Borrowed)); + JS::CompilationStorage compileStorage; + RefPtr<JS::Stencil> stencil = + JS::CompileGlobalScriptToStencil(fc, options, srcBuf, compileStorage); + CHECK(!stencil); + } + + CHECK(JS::HadFrontendErrors(fc)); + CHECK(!JS::HadFrontendOverRecursed(fc)); + CHECK(!JS::HadFrontendOutOfMemory(fc)); + CHECK(!JS::HadFrontendAllocationOverflow(fc)); + CHECK(JS::GetFrontendWarningCount(fc) == 0); + + { + const JSErrorReport* report = JS::GetFrontendErrorReport(fc, options); + CHECK(report); + + CHECK(report->errorNumber == JSMSG_UNEXPECTED_TOKEN_NO_EXPECT); + // FrontendContext's error report borrows the filename. + CHECK(report->filename.c_str() == filename); + } + + CHECK(!JS_IsExceptionPending(cx)); + + JS::SetWarningReporter(cx, warningReporter); + + bool result = JS::ConvertFrontendErrorsToRuntimeErrors(cx, fc, options); + CHECK(result); + + CHECK(JS_IsExceptionPending(cx)); + CHECK(!warningReporterCalled); + CHECK(!JS_IsThrowingOutOfMemory(cx)); + + { + JS::Rooted<JS::Value> exception(cx); + CHECK(JS_GetPendingException(cx, &exception)); + + CHECK(exception.isObject()); + JS::Rooted<JSObject*> exceptionObj(cx, &exception.toObject()); + + const JSErrorReport* report = JS_ErrorFromException(cx, exceptionObj); + CHECK(report); + + CHECK(report->errorNumber == JSMSG_UNEXPECTED_TOKEN_NO_EXPECT); + // Runtime's error report doesn't borrow the filename. + CHECK(report->filename.c_str() != filename); + CHECK(strcmp(report->filename.c_str(), filename) == 0); + } + + JS_ClearPendingException(cx); + + JS::DestroyFrontendContext(fc); + + return true; +} + +static bool warningReporterCalled; + +static void warningReporter(JSContext* cx, JSErrorReport* report) { + warningReporterCalled = true; +} +END_TEST(testFrontendErrors_error) + +/* static */ bool cls_testFrontendErrors_error::warningReporterCalled = false; + +BEGIN_TEST(testFrontendErrors_warning) { + JS::FrontendContext* fc = JS::NewFrontendContext(); + CHECK(fc); + + static constexpr JS::NativeStackSize stackSize = 128 * sizeof(size_t) * 1024; + + JS::SetNativeStackQuota(fc, stackSize); + + JS::PrefableCompileOptions prefableOptions; + JS::CompileOptions options(prefableOptions); + options.setFile(filename); + + { + const char source[] = "function f() { return; f(); }"; + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + CHECK( + srcBuf.init(fc, source, strlen(source), JS::SourceOwnership::Borrowed)); + JS::CompilationStorage compileStorage; + RefPtr<JS::Stencil> stencil = + JS::CompileGlobalScriptToStencil(fc, options, srcBuf, compileStorage); + CHECK(stencil); + } + + CHECK(!JS::HadFrontendErrors(fc)); + CHECK(!JS::HadFrontendOverRecursed(fc)); + CHECK(!JS::HadFrontendOutOfMemory(fc)); + CHECK(!JS::HadFrontendAllocationOverflow(fc)); + CHECK(JS::GetFrontendWarningCount(fc) == 1); + + { + const JSErrorReport* report = JS::GetFrontendWarningAt(fc, 0, options); + CHECK(report); + + CHECK(report->errorNumber == JSMSG_STMT_AFTER_RETURN); + // FrontendContext's error report borrows the filename. + CHECK(report->filename.c_str() == filename); + } + + CHECK(!JS_IsExceptionPending(cx)); + + JS::SetWarningReporter(cx, warningReporter); + + bool result = JS::ConvertFrontendErrorsToRuntimeErrors(cx, fc, options); + CHECK(result); + + CHECK(!JS_IsExceptionPending(cx)); + CHECK(warningReporterCalled); + CHECK(!JS_IsThrowingOutOfMemory(cx)); + + CHECK(errorNumberMatches); + CHECK(filenameMatches); + + JS::DestroyFrontendContext(fc); + + return true; +} + +static const char* filename; +static bool warningReporterCalled; +static bool errorNumberMatches; +static bool filenameMatches; + +static void warningReporter(JSContext* cx, JSErrorReport* report) { + warningReporterCalled = true; + + errorNumberMatches = report->errorNumber == JSMSG_STMT_AFTER_RETURN; + filenameMatches = report->filename.c_str() == filename; +} +END_TEST(testFrontendErrors_warning) + +/* static */ const char* cls_testFrontendErrors_warning::filename = + "testFrontendErrors_warning.js"; +/* static */ bool cls_testFrontendErrors_warning::warningReporterCalled = false; +/* static */ bool cls_testFrontendErrors_warning::errorNumberMatches = false; +/* static */ bool cls_testFrontendErrors_warning::filenameMatches = false; + +BEGIN_TEST(testFrontendErrors_oom) { + JS::FrontendContext* fc = JS::NewFrontendContext(); + CHECK(fc); + + JS::PrefableCompileOptions prefableOptions; + JS::CompileOptions options(prefableOptions); + + CHECK(!JS::HadFrontendErrors(fc)); + + js::ReportOutOfMemory(fc); + + CHECK(JS::HadFrontendErrors(fc)); + CHECK(!JS::HadFrontendOverRecursed(fc)); + CHECK(JS::HadFrontendOutOfMemory(fc)); + CHECK(!JS::HadFrontendAllocationOverflow(fc)); + CHECK(JS::GetFrontendWarningCount(fc) == 0); + + CHECK(!JS_IsExceptionPending(cx)); + + JS::SetWarningReporter(cx, warningReporter); + + bool result = JS::ConvertFrontendErrorsToRuntimeErrors(cx, fc, options); + CHECK(!result); + + CHECK(JS_IsExceptionPending(cx)); + CHECK(!warningReporterCalled); + CHECK(JS_IsThrowingOutOfMemory(cx)); + + JS_ClearPendingException(cx); + + JS::DestroyFrontendContext(fc); + + return true; +} + +static bool warningReporterCalled; + +static void warningReporter(JSContext* cx, JSErrorReport* report) { + warningReporterCalled = true; +} +END_TEST(testFrontendErrors_oom) + +/* static */ bool cls_testFrontendErrors_oom::warningReporterCalled = false; + +BEGIN_TEST(testFrontendErrors_overRecursed) { + JS::FrontendContext* fc = JS::NewFrontendContext(); + CHECK(fc); + + JS::PrefableCompileOptions prefableOptions; + JS::CompileOptions options(prefableOptions); + + CHECK(!JS::HadFrontendErrors(fc)); + + js::ReportOverRecursed(fc); + + CHECK(JS::HadFrontendErrors(fc)); + CHECK(JS::HadFrontendOverRecursed(fc)); + CHECK(!JS::HadFrontendOutOfMemory(fc)); + CHECK(!JS::HadFrontendAllocationOverflow(fc)); + CHECK(JS::GetFrontendWarningCount(fc) == 0); + + CHECK(!JS_IsExceptionPending(cx)); + + JS::SetWarningReporter(cx, warningReporter); + + bool result = JS::ConvertFrontendErrorsToRuntimeErrors(cx, fc, options); + CHECK(result); + + CHECK(JS_IsExceptionPending(cx)); + CHECK(!warningReporterCalled); + CHECK(!JS_IsThrowingOutOfMemory(cx)); + + { + JS::Rooted<JS::Value> exception(cx); + CHECK(JS_GetPendingException(cx, &exception)); + + CHECK(exception.isObject()); + JS::Rooted<JSObject*> exceptionObj(cx, &exception.toObject()); + + const JSErrorReport* report = JS_ErrorFromException(cx, exceptionObj); + CHECK(report); + + CHECK(report->errorNumber == JSMSG_OVER_RECURSED); + } + + JS_ClearPendingException(cx); + + JS::DestroyFrontendContext(fc); + + return true; +} + +static bool warningReporterCalled; + +static void warningReporter(JSContext* cx, JSErrorReport* report) { + warningReporterCalled = true; +} +END_TEST(testFrontendErrors_overRecursed) + +/* static */ bool cls_testFrontendErrors_overRecursed::warningReporterCalled = + false; + +BEGIN_TEST(testFrontendErrors_allocationOverflow) { + JS::FrontendContext* fc = JS::NewFrontendContext(); + CHECK(fc); + + JS::PrefableCompileOptions prefableOptions; + JS::CompileOptions options(prefableOptions); + + CHECK(!JS::HadFrontendErrors(fc)); + + js::ReportAllocationOverflow(fc); + + CHECK(JS::HadFrontendErrors(fc)); + CHECK(!JS::HadFrontendOverRecursed(fc)); + CHECK(!JS::HadFrontendOutOfMemory(fc)); + CHECK(JS::HadFrontendAllocationOverflow(fc)); + CHECK(JS::GetFrontendWarningCount(fc) == 0); + + CHECK(!JS_IsExceptionPending(cx)); + + JS::SetWarningReporter(cx, warningReporter); + + bool result = JS::ConvertFrontendErrorsToRuntimeErrors(cx, fc, options); + CHECK(result); + + CHECK(JS_IsExceptionPending(cx)); + CHECK(!warningReporterCalled); + CHECK(!JS_IsThrowingOutOfMemory(cx)); + + { + JS::Rooted<JS::Value> exception(cx); + CHECK(JS_GetPendingException(cx, &exception)); + + CHECK(exception.isObject()); + JS::Rooted<JSObject*> exceptionObj(cx, &exception.toObject()); + + const JSErrorReport* report = JS_ErrorFromException(cx, exceptionObj); + CHECK(report); + + CHECK(report->errorNumber == JSMSG_ALLOC_OVERFLOW); + } + + JS_ClearPendingException(cx); + + JS::DestroyFrontendContext(fc); + + return true; +} + +static bool warningReporterCalled; + +static void warningReporter(JSContext* cx, JSErrorReport* report) { + warningReporterCalled = true; +} +END_TEST(testFrontendErrors_allocationOverflow) + +/* static */ bool + cls_testFrontendErrors_allocationOverflow::warningReporterCalled = false; diff --git a/js/src/jsapi-tests/testFrontendJSON.cpp b/js/src/jsapi-tests/testFrontendJSON.cpp new file mode 100644 index 0000000000..b5315e6084 --- /dev/null +++ b/js/src/jsapi-tests/testFrontendJSON.cpp @@ -0,0 +1,646 @@ +/* -*- 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 "mozilla/Maybe.h" // mozilla::Maybe + +#include <string> + +#include "js/AllocPolicy.h" // js::SystemAllocPolicy +#include "js/JSON.h" +#include "js/Vector.h" // js::Vector +#include "jsapi-tests/tests.h" + +using namespace JS; + +BEGIN_FRONTEND_TEST(testIsValidJSONLatin1) { + const char* source; + + source = "true"; + CHECK(IsValidJSON(reinterpret_cast<const JS::Latin1Char*>(source), + strlen(source))); + + source = "false"; + CHECK(IsValidJSON(reinterpret_cast<const JS::Latin1Char*>(source), + strlen(source))); + + source = "null"; + CHECK(IsValidJSON(reinterpret_cast<const JS::Latin1Char*>(source), + strlen(source))); + + source = "0"; + CHECK(IsValidJSON(reinterpret_cast<const JS::Latin1Char*>(source), + strlen(source))); + + source = "1"; + CHECK(IsValidJSON(reinterpret_cast<const JS::Latin1Char*>(source), + strlen(source))); + + source = "-1"; + CHECK(IsValidJSON(reinterpret_cast<const JS::Latin1Char*>(source), + strlen(source))); + + source = "1.75"; + CHECK(IsValidJSON(reinterpret_cast<const JS::Latin1Char*>(source), + strlen(source))); + + source = "9000000000"; + CHECK(IsValidJSON(reinterpret_cast<const JS::Latin1Char*>(source), + strlen(source))); + + source = "\"foo\""; + CHECK(IsValidJSON(reinterpret_cast<const JS::Latin1Char*>(source), + strlen(source))); + + source = "[]"; + CHECK(IsValidJSON(reinterpret_cast<const JS::Latin1Char*>(source), + strlen(source))); + + source = "[1, true]"; + CHECK(IsValidJSON(reinterpret_cast<const JS::Latin1Char*>(source), + strlen(source))); + + source = "{}"; + CHECK(IsValidJSON(reinterpret_cast<const JS::Latin1Char*>(source), + strlen(source))); + + source = "{\"key\": 10}"; + CHECK(IsValidJSON(reinterpret_cast<const JS::Latin1Char*>(source), + strlen(source))); + + source = "{\"key\": 10, \"prop\": 20}"; + CHECK(IsValidJSON(reinterpret_cast<const JS::Latin1Char*>(source), + strlen(source))); + + source = "1 "; + CHECK(IsValidJSON(reinterpret_cast<const JS::Latin1Char*>(source), + strlen(source))); + + // Invalid cases. + + source = ""; + CHECK(!IsValidJSON(reinterpret_cast<const JS::Latin1Char*>(source), + strlen(source))); + + source = "1 1"; + CHECK(!IsValidJSON(reinterpret_cast<const JS::Latin1Char*>(source), + strlen(source))); + + source = ".1"; + CHECK(!IsValidJSON(reinterpret_cast<const JS::Latin1Char*>(source), + strlen(source))); + + source = "undefined"; + CHECK(!IsValidJSON(reinterpret_cast<const JS::Latin1Char*>(source), + strlen(source))); + + source = "TRUE"; + CHECK(!IsValidJSON(reinterpret_cast<const JS::Latin1Char*>(source), + strlen(source))); + + source = "'foo'"; + CHECK(!IsValidJSON(reinterpret_cast<const JS::Latin1Char*>(source), + strlen(source))); + + source = "["; + CHECK(!IsValidJSON(reinterpret_cast<const JS::Latin1Char*>(source), + strlen(source))); + + source = "{"; + CHECK(!IsValidJSON(reinterpret_cast<const JS::Latin1Char*>(source), + strlen(source))); + + source = "/a/"; + CHECK(!IsValidJSON(reinterpret_cast<const JS::Latin1Char*>(source), + strlen(source))); + + return true; +} + +END_TEST(testIsValidJSONLatin1) + +BEGIN_FRONTEND_TEST(testIsValidJSONTwoBytes) { + const char16_t* source; + + source = u"true"; + CHECK(IsValidJSON(source, std::char_traits<char16_t>::length(source))); + + source = u"false"; + CHECK(IsValidJSON(source, std::char_traits<char16_t>::length(source))); + + source = u"null"; + CHECK(IsValidJSON(source, std::char_traits<char16_t>::length(source))); + + source = u"0"; + CHECK(IsValidJSON(source, std::char_traits<char16_t>::length(source))); + + source = u"1"; + CHECK(IsValidJSON(source, std::char_traits<char16_t>::length(source))); + + source = u"-1"; + CHECK(IsValidJSON(source, std::char_traits<char16_t>::length(source))); + + source = u"1.75"; + CHECK(IsValidJSON(source, std::char_traits<char16_t>::length(source))); + + source = u"9000000000"; + CHECK(IsValidJSON(source, std::char_traits<char16_t>::length(source))); + + source = u"\"foo\""; + CHECK(IsValidJSON(source, std::char_traits<char16_t>::length(source))); + + source = u"[]"; + CHECK(IsValidJSON(source, std::char_traits<char16_t>::length(source))); + + source = u"[1, true]"; + CHECK(IsValidJSON(source, std::char_traits<char16_t>::length(source))); + + source = u"{}"; + CHECK(IsValidJSON(source, std::char_traits<char16_t>::length(source))); + + source = u"{\"key\": 10}"; + CHECK(IsValidJSON(source, std::char_traits<char16_t>::length(source))); + + source = u"{\"key\": 10, \"prop\": 20}"; + CHECK(IsValidJSON(source, std::char_traits<char16_t>::length(source))); + + source = u"1 "; + CHECK(IsValidJSON(source, std::char_traits<char16_t>::length(source))); + + // Invalid cases. + + source = u""; + CHECK(!IsValidJSON(source, std::char_traits<char16_t>::length(source))); + + source = u"1 1"; + CHECK(!IsValidJSON(source, std::char_traits<char16_t>::length(source))); + + source = u".1"; + CHECK(!IsValidJSON(source, std::char_traits<char16_t>::length(source))); + + source = u"undefined"; + CHECK(!IsValidJSON(source, std::char_traits<char16_t>::length(source))); + + source = u"TRUE"; + CHECK(!IsValidJSON(source, std::char_traits<char16_t>::length(source))); + + source = u"'foo'"; + CHECK(!IsValidJSON(source, std::char_traits<char16_t>::length(source))); + + source = u"["; + CHECK(!IsValidJSON(source, std::char_traits<char16_t>::length(source))); + + source = u"{"; + CHECK(!IsValidJSON(source, std::char_traits<char16_t>::length(source))); + + source = u"/a/"; + CHECK(!IsValidJSON(source, std::char_traits<char16_t>::length(source))); + + return true; +} + +END_TEST(testIsValidJSONTwoBytes) + +BEGIN_FRONTEND_TEST(testParseJSONWithHandler) { + { + MyHandler handler; + + const char* source = + "{ \"prop1\": 10.5, \"prop\\uff12\": [true, false, null, \"Ascii\", " + "\"\\u3042\\u3044\\u3046\", \"\\u0020\", \"\\u0080\"] }"; + CHECK(JS::ParseJSONWithHandler((const JS::Latin1Char*)source, + std::char_traits<char>::length(source), + &handler)); + + size_t i = 0; + + CHECK(handler.events[i++] == MyHandler::Event::StartObject); + + // Non-escaped ASCII property name in Latin1 input should be passed with + // Latin1. + CHECK(handler.events[i++] == MyHandler::Event::Latin1Prop1); + + CHECK(handler.events[i++] == MyHandler::Event::Number); + + // Escaped non-Latin1 property name in Latin1 input should be passed with + // TwoBytes. + CHECK(handler.events[i++] == MyHandler::Event::TwoBytesProp2); + CHECK(handler.events[i++] == MyHandler::Event::StartArray); + CHECK(handler.events[i++] == MyHandler::Event::True); + CHECK(handler.events[i++] == MyHandler::Event::False); + CHECK(handler.events[i++] == MyHandler::Event::Null); + + // Non-escaped ASCII string in Latin1 input should be passed with Latin1. + CHECK(handler.events[i++] == MyHandler::Event::Latin1Str1); + + // Escaped non-Latin1 string in Latin1 input should be passed with TwoBytes. + CHECK(handler.events[i++] == MyHandler::Event::TwoBytesStr2); + + // Escaped ASCII-range string in Latin1 input should be passed with Latin1. + CHECK(handler.events[i++] == MyHandler::Event::Latin1Str3); + + // Escaped Latin1-range string in Latin1 input should be passed with Latin1. + CHECK(handler.events[i++] == MyHandler::Event::Latin1Str4); + + CHECK(handler.events[i++] == MyHandler::Event::EndArray); + CHECK(handler.events[i++] == MyHandler::Event::EndObject); + CHECK(handler.events.length() == i); + } + { + MyHandler handler; + + const char* source = "{"; + CHECK(!JS::ParseJSONWithHandler((const JS::Latin1Char*)source, + std::char_traits<char>::length(source), + &handler)); + + size_t i = 0; + + CHECK(handler.events[i++] == MyHandler::Event::StartObject); + CHECK(handler.events[i++] == MyHandler::Event::Error); + CHECK(handler.events.length() == i); + } + + { + MyHandler handler; + + const char16_t* source = + u"{ \"prop1\": 10.5, \"prop\uff12\": [true, false, null, \"Ascii\", " + u"\"\\u3042\\u3044\\u3046\", \"\\u0020\", \"\\u0080\"] }"; + CHECK(JS::ParseJSONWithHandler( + source, std::char_traits<char16_t>::length(source), &handler)); + + size_t i = 0; + + CHECK(handler.events[i++] == MyHandler::Event::StartObject); + + // Non-escaped ASCII property name in TwoBytes input should be passed with + // TwoBytes. + CHECK(handler.events[i++] == MyHandler::Event::TwoBytesProp1); + + CHECK(handler.events[i++] == MyHandler::Event::Number); + + // Escaped non-Latin1 property name in TwoBytes input should be passed with + // TwoBytes. + CHECK(handler.events[i++] == MyHandler::Event::TwoBytesProp2); + + CHECK(handler.events[i++] == MyHandler::Event::StartArray); + CHECK(handler.events[i++] == MyHandler::Event::True); + CHECK(handler.events[i++] == MyHandler::Event::False); + CHECK(handler.events[i++] == MyHandler::Event::Null); + + // Non-escaped ASCII string in TwoBytes input should be passed with + // TwoBytes. + CHECK(handler.events[i++] == MyHandler::Event::TwoBytesStr1); + + // Escaped non-Latin1 string in TwoBytes input should be passed with + // TwoBytes. + CHECK(handler.events[i++] == MyHandler::Event::TwoBytesStr2); + + // Escaped ASCII-range string in TwoBytes input should be passed with + // Latin1. + CHECK(handler.events[i++] == MyHandler::Event::Latin1Str3); + + // Escaped Latin1-range string in TwoBytes input should be passed with + // Latin1. + CHECK(handler.events[i++] == MyHandler::Event::Latin1Str4); + + CHECK(handler.events[i++] == MyHandler::Event::EndArray); + CHECK(handler.events[i++] == MyHandler::Event::EndObject); + CHECK(handler.events.length() == i); + } + + { + MyHandler handler; + + const char16_t* source = u"{"; + CHECK(!JS::ParseJSONWithHandler( + source, std::char_traits<char16_t>::length(source), &handler)); + + size_t i = 0; + + CHECK(handler.events[i++] == MyHandler::Event::StartObject); + CHECK(handler.events[i++] == MyHandler::Event::Error); + CHECK(handler.events.length() == i); + } + + // Verify the failure case is handled properly and no methods are called + // after the failure. + + bool checkedLast = false; + for (size_t failAt = 1; !checkedLast; failAt++) { + MyHandler handler; + handler.failAt.emplace(failAt); + + const char* source = + "{ \"prop1\": 10.5, \"prop\\uff12\": [true, false, null, \"Ascii\", " + "\"\\u3042\\u3044\\u3046\", \"\\u0020\", \"\\u0080\"] }"; + CHECK(!JS::ParseJSONWithHandler((const JS::Latin1Char*)source, + std::char_traits<char>::length(source), + &handler)); + + CHECK(handler.events.length() == failAt); + + size_t i = 0; + + CHECK(handler.events[i++] == MyHandler::Event::StartObject); + if (i >= failAt) { + continue; + } + CHECK(handler.events[i++] == MyHandler::Event::Latin1Prop1); + if (i >= failAt) { + continue; + } + CHECK(handler.events[i++] == MyHandler::Event::Number); + if (i >= failAt) { + continue; + } + CHECK(handler.events[i++] == MyHandler::Event::TwoBytesProp2); + if (i >= failAt) { + continue; + } + CHECK(handler.events[i++] == MyHandler::Event::StartArray); + if (i >= failAt) { + continue; + } + CHECK(handler.events[i++] == MyHandler::Event::True); + if (i >= failAt) { + continue; + } + CHECK(handler.events[i++] == MyHandler::Event::False); + if (i >= failAt) { + continue; + } + CHECK(handler.events[i++] == MyHandler::Event::Null); + if (i >= failAt) { + continue; + } + CHECK(handler.events[i++] == MyHandler::Event::Latin1Str1); + if (i >= failAt) { + continue; + } + CHECK(handler.events[i++] == MyHandler::Event::TwoBytesStr2); + if (i >= failAt) { + continue; + } + CHECK(handler.events[i++] == MyHandler::Event::Latin1Str3); + if (i >= failAt) { + continue; + } + CHECK(handler.events[i++] == MyHandler::Event::Latin1Str4); + if (i >= failAt) { + continue; + } + CHECK(handler.events[i++] == MyHandler::Event::EndArray); + if (i >= failAt) { + continue; + } + CHECK(handler.events[i++] == MyHandler::Event::EndObject); + checkedLast = true; + } + + checkedLast = false; + for (size_t failAt = 1; !checkedLast; failAt++) { + MyHandler handler; + handler.failAt.emplace(failAt); + + const char16_t* source = + u"{ \"prop1\": 10.5, \"prop\uff12\": [true, false, null, \"Ascii\", " + u"\"\\u3042\\u3044\\u3046\", \"\\u0020\", \"\\u0080\"] }"; + CHECK(!JS::ParseJSONWithHandler( + source, std::char_traits<char16_t>::length(source), &handler)); + + CHECK(handler.events.length() == failAt); + + size_t i = 0; + + CHECK(handler.events[i++] == MyHandler::Event::StartObject); + if (i >= failAt) { + continue; + } + CHECK(handler.events[i++] == MyHandler::Event::TwoBytesProp1); + if (i >= failAt) { + continue; + } + CHECK(handler.events[i++] == MyHandler::Event::Number); + if (i >= failAt) { + continue; + } + CHECK(handler.events[i++] == MyHandler::Event::TwoBytesProp2); + if (i >= failAt) { + continue; + } + CHECK(handler.events[i++] == MyHandler::Event::StartArray); + if (i >= failAt) { + continue; + } + CHECK(handler.events[i++] == MyHandler::Event::True); + if (i >= failAt) { + continue; + } + CHECK(handler.events[i++] == MyHandler::Event::False); + if (i >= failAt) { + continue; + } + CHECK(handler.events[i++] == MyHandler::Event::Null); + if (i >= failAt) { + continue; + } + CHECK(handler.events[i++] == MyHandler::Event::TwoBytesStr1); + if (i >= failAt) { + continue; + } + CHECK(handler.events[i++] == MyHandler::Event::TwoBytesStr2); + if (i >= failAt) { + continue; + } + CHECK(handler.events[i++] == MyHandler::Event::Latin1Str3); + if (i >= failAt) { + continue; + } + CHECK(handler.events[i++] == MyHandler::Event::Latin1Str4); + if (i >= failAt) { + continue; + } + CHECK(handler.events[i++] == MyHandler::Event::EndArray); + if (i >= failAt) { + continue; + } + CHECK(handler.events[i++] == MyHandler::Event::EndObject); + checkedLast = true; + } + + return true; +} + +class MyHandler : public JS::JSONParseHandler { + public: + enum class Event { + Uninitialized = 0, + + StartObject, + Latin1Prop1, + TwoBytesProp1, + Number, + TwoBytesProp2, + StartArray, + True, + False, + Null, + Latin1Str1, + TwoBytesStr1, + TwoBytesStr2, + Latin1Str3, + Latin1Str4, + EndArray, + EndObject, + Error, + UnexpectedNumber, + UnexpectedLatin1Prop, + UnexpectedTwoBytesProp, + UnexpectedLatin1String, + UnexpectedTwoBytesString, + }; + js::Vector<Event, 0, js::SystemAllocPolicy> events; + mozilla::Maybe<size_t> failAt; + + MyHandler() {} + virtual ~MyHandler() {} + + bool startObject() override { + MOZ_ALWAYS_TRUE(events.append(Event::StartObject)); + if (failAt.isSome() && events.length() == *failAt) { + failAt.reset(); + return false; + } + return true; + } + bool propertyName(const JS::Latin1Char* name, size_t length) override { + if (length == 5 && name[0] == 'p' && name[1] == 'r' && name[2] == 'o' && + name[3] == 'p' && name[4] == '1') { + MOZ_ALWAYS_TRUE(events.append(Event::Latin1Prop1)); + } else { + MOZ_ALWAYS_TRUE(events.append(Event::UnexpectedLatin1Prop)); + } + if (failAt.isSome() && events.length() == *failAt) { + failAt.reset(); + return false; + } + return true; + } + bool propertyName(const char16_t* name, size_t length) override { + if (length == 5 && name[0] == 'p' && name[1] == 'r' && name[2] == 'o' && + name[3] == 'p' && name[4] == '1') { + MOZ_ALWAYS_TRUE(events.append(Event::TwoBytesProp1)); + } else if (length == 5 && name[0] == 'p' && name[1] == 'r' && + name[2] == 'o' && name[3] == 'p' && name[4] == 0xff12) { + MOZ_ALWAYS_TRUE(events.append(Event::TwoBytesProp2)); + } else { + MOZ_ALWAYS_TRUE(events.append(Event::UnexpectedTwoBytesProp)); + } + if (failAt.isSome() && events.length() == *failAt) { + failAt.reset(); + return false; + } + return true; + } + bool endObject() override { + MOZ_ALWAYS_TRUE(events.append(Event::EndObject)); + if (failAt.isSome() && events.length() == *failAt) { + failAt.reset(); + return false; + } + return true; + } + + bool startArray() override { + MOZ_ALWAYS_TRUE(events.append(Event::StartArray)); + if (failAt.isSome() && events.length() == *failAt) { + failAt.reset(); + return false; + } + return true; + } + bool endArray() override { + MOZ_ALWAYS_TRUE(events.append(Event::EndArray)); + if (failAt.isSome() && events.length() == *failAt) { + failAt.reset(); + return false; + } + return true; + } + + bool stringValue(const JS::Latin1Char* name, size_t length) override { + if (length == 5 && name[0] == 'A' && name[1] == 's' && name[2] == 'c' && + name[3] == 'i' && name[4] == 'i') { + MOZ_ALWAYS_TRUE(events.append(Event::Latin1Str1)); + } else if (length == 1 && name[0] == ' ') { + MOZ_ALWAYS_TRUE(events.append(Event::Latin1Str3)); + } else if (length == 1 && name[0] == 0x80) { + MOZ_ALWAYS_TRUE(events.append(Event::Latin1Str4)); + } else { + MOZ_ALWAYS_TRUE(events.append(Event::UnexpectedLatin1String)); + } + if (failAt.isSome() && events.length() == *failAt) { + failAt.reset(); + return false; + } + return true; + } + bool stringValue(const char16_t* name, size_t length) override { + if (length == 5 && name[0] == 'A' && name[1] == 's' && name[2] == 'c' && + name[3] == 'i' && name[4] == 'i') { + MOZ_ALWAYS_TRUE(events.append(Event::TwoBytesStr1)); + } else if (length == 3 && name[0] == 0x3042 && name[1] == 0x3044 && + name[2] == 0x3046) { + MOZ_ALWAYS_TRUE(events.append(Event::TwoBytesStr2)); + } else { + MOZ_ALWAYS_TRUE(events.append(Event::UnexpectedTwoBytesString)); + } + if (failAt.isSome() && events.length() == *failAt) { + failAt.reset(); + return false; + } + return true; + } + bool numberValue(double d) override { + if (d == 10.5) { + MOZ_ALWAYS_TRUE(events.append(Event::Number)); + } else { + MOZ_ALWAYS_TRUE(events.append(Event::UnexpectedNumber)); + } + if (failAt.isSome() && events.length() == *failAt) { + failAt.reset(); + return false; + } + return true; + } + bool booleanValue(bool v) override { + if (v) { + MOZ_ALWAYS_TRUE(events.append(Event::True)); + } else { + MOZ_ALWAYS_TRUE(events.append(Event::False)); + } + if (failAt.isSome() && events.length() == *failAt) { + failAt.reset(); + return false; + } + return true; + } + bool nullValue() override { + MOZ_ALWAYS_TRUE(events.append(Event::Null)); + if (failAt.isSome() && events.length() == *failAt) { + failAt.reset(); + return false; + } + return true; + } + + void error(const char* msg, uint32_t line, uint32_t column) override { + MOZ_ALWAYS_TRUE(events.append(Event::Error)); + } +}; + +END_TEST(testParseJSONWithHandler) diff --git a/js/src/jsapi-tests/testFunctionBinding.cpp b/js/src/jsapi-tests/testFunctionBinding.cpp new file mode 100644 index 0000000000..6a69e32349 --- /dev/null +++ b/js/src/jsapi-tests/testFunctionBinding.cpp @@ -0,0 +1,83 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + * + * Test function name binding. + */ +/* 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 "mozilla/Utf8.h" // mozilla::Utf8Unit + +#include "js/CallAndConstruct.h" +#include "js/CompilationAndEvaluation.h" // JS::CompileFunction +#include "js/SourceText.h" // JS::Source{Ownership,Text} +#include "jsapi-tests/tests.h" +#include "util/Text.h" + +using namespace js; + +BEGIN_TEST(test_functionBinding) { + RootedFunction fun(cx); + + JS::CompileOptions options(cx); + options.setFileAndLine(__FILE__, __LINE__); + + JS::RootedObjectVector emptyScopeChain(cx); + + // Named function shouldn't have it's binding. + { + static const char s1chars[] = "return (typeof s1) == 'undefined';"; + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + CHECK(srcBuf.init(cx, s1chars, js_strlen(s1chars), + JS::SourceOwnership::Borrowed)); + + fun = JS::CompileFunction(cx, emptyScopeChain, options, "s1", 0, nullptr, + srcBuf); + CHECK(fun); + } + + JS::RootedValueVector args(cx); + RootedValue rval(cx); + CHECK(JS::Call(cx, UndefinedHandleValue, fun, args, &rval)); + CHECK(rval.isBoolean()); + CHECK(rval.toBoolean()); + + // Named function shouldn't have `anonymous` binding. + { + static const char s2chars[] = "return (typeof anonymous) == 'undefined';"; + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + CHECK(srcBuf.init(cx, s2chars, js_strlen(s2chars), + JS::SourceOwnership::Borrowed)); + + fun = JS::CompileFunction(cx, emptyScopeChain, options, "s2", 0, nullptr, + srcBuf); + CHECK(fun); + } + + CHECK(JS::Call(cx, UndefinedHandleValue, fun, args, &rval)); + CHECK(rval.isBoolean()); + CHECK(rval.toBoolean()); + + // Anonymous function shouldn't have `anonymous` binding. + { + static const char s3chars[] = "return (typeof anonymous) == 'undefined';"; + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + CHECK(srcBuf.init(cx, s3chars, js_strlen(s3chars), + JS::SourceOwnership::Borrowed)); + + fun = JS::CompileFunction(cx, emptyScopeChain, options, nullptr, 0, nullptr, + srcBuf); + CHECK(fun); + } + + CHECK(JS::Call(cx, UndefinedHandleValue, fun, args, &rval)); + CHECK(rval.isBoolean()); + CHECK(rval.toBoolean()); + + return true; +} +END_TEST(test_functionBinding) diff --git a/js/src/jsapi-tests/testFunctionNonSyntactic.cpp b/js/src/jsapi-tests/testFunctionNonSyntactic.cpp new file mode 100644 index 0000000000..b58db08897 --- /dev/null +++ b/js/src/jsapi-tests/testFunctionNonSyntactic.cpp @@ -0,0 +1,95 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + * + * Test function with enclosing non-syntactic scope. + */ +/* 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 "mozilla/Utf8.h" // mozilla::Utf8Unit + +#include "js/CallAndConstruct.h" +#include "js/CompilationAndEvaluation.h" // JS::CompileFunction +#include "js/PropertyAndElement.h" // JS_DefineProperty +#include "js/SourceText.h" // JS::Source{Ownership,Text} +#include "jsapi-tests/tests.h" +#include "util/Text.h" +#include "vm/JSFunction.h" // JSFunction +#include "vm/Scope.h" // Scope +#include "vm/ScopeKind.h" // ScopeKind + +using namespace js; + +BEGIN_TEST(testFunctionNonSyntactic) { + JS::RootedObjectVector scopeChain(cx); + + { + JS::RootedObject scopeObj(cx, JS_NewPlainObject(cx)); + CHECK(scopeObj); + JS::RootedValue val(cx); + val.setNumber(1); + CHECK(JS_DefineProperty(cx, scopeObj, "foo", val, JSPROP_ENUMERATE)); + CHECK(scopeChain.append(scopeObj)); + } + + { + JS::RootedObject scopeObj(cx, JS_NewPlainObject(cx)); + CHECK(scopeObj); + JS::RootedValue val(cx); + val.setNumber(20); + CHECK(JS_DefineProperty(cx, scopeObj, "bar", val, JSPROP_ENUMERATE)); + CHECK(scopeChain.append(scopeObj)); + } + + { + static const char src[] = "return foo + bar;"; + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + CHECK(srcBuf.init(cx, src, js_strlen(src), JS::SourceOwnership::Borrowed)); + + JS::CompileOptions options(cx); + options.setFileAndLine(__FILE__, __LINE__); + RootedFunction fun(cx, JS::CompileFunction(cx, scopeChain, options, "test", + 0, nullptr, srcBuf)); + CHECK(fun); + + CHECK(fun->enclosingScope()->kind() == ScopeKind::NonSyntactic); + + JS::RootedValue funVal(cx, JS::ObjectValue(*fun)); + JS::RootedValue rval(cx); + CHECK(JS::Call(cx, JS::UndefinedHandleValue, funVal, + JS::HandleValueArray::empty(), &rval)); + CHECK(rval.isNumber()); + CHECK(rval.toNumber() == 21); + } + + // With extra body bar. + { + const char* args[] = { + "a = 300", + }; + static const char src[] = "var x = 4000; return a + x + foo + bar;"; + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + CHECK(srcBuf.init(cx, src, js_strlen(src), JS::SourceOwnership::Borrowed)); + + JS::CompileOptions options(cx); + options.setFileAndLine(__FILE__, __LINE__); + RootedFunction fun(cx, JS::CompileFunction(cx, scopeChain, options, "test", + 1, args, srcBuf)); + CHECK(fun); + + CHECK(fun->enclosingScope()->kind() == ScopeKind::NonSyntactic); + + JS::RootedValue funVal(cx, JS::ObjectValue(*fun)); + JS::RootedValue rval(cx); + CHECK(JS::Call(cx, JS::UndefinedHandleValue, funVal, + JS::HandleValueArray::empty(), &rval)); + CHECK(rval.isNumber()); + CHECK(rval.toNumber() == 4321); + } + + return true; +} +END_TEST(testFunctionNonSyntactic) diff --git a/js/src/jsapi-tests/testFunctionProperties.cpp b/js/src/jsapi-tests/testFunctionProperties.cpp new file mode 100644 index 0000000000..01031cc5ea --- /dev/null +++ b/js/src/jsapi-tests/testFunctionProperties.cpp @@ -0,0 +1,26 @@ +/* -*- 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 "js/PropertyAndElement.h" // JS_GetProperty +#include "jsapi-tests/tests.h" + +BEGIN_TEST(testFunctionProperties) { + JS::RootedValue x(cx); + EVAL("(function f() {})", &x); + + JS::RootedObject obj(cx, x.toObjectOrNull()); + + JS::RootedValue y(cx); + CHECK(JS_GetProperty(cx, obj, "arguments", &y)); + CHECK(y.isNull()); + + CHECK(JS_GetProperty(cx, obj, "caller", &y)); + CHECK(y.isNull()); + + return true; +} +END_TEST(testFunctionProperties) diff --git a/js/src/jsapi-tests/testGCAllocator.cpp b/js/src/jsapi-tests/testGCAllocator.cpp new file mode 100644 index 0000000000..c740a3b10b --- /dev/null +++ b/js/src/jsapi-tests/testGCAllocator.cpp @@ -0,0 +1,354 @@ +/* -*- 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 <cstdlib> + +#include "gc/Memory.h" +#include "jsapi-tests/tests.h" + +#if defined(XP_WIN) +# include "util/WindowsWrapper.h" +# include <psapi.h> +#elif defined(__wasi__) +// Nothing. +#else +# include <algorithm> +# include <errno.h> +# include <sys/mman.h> +# include <sys/resource.h> +# include <sys/stat.h> +# include <sys/types.h> +# include <unistd.h> +#endif + +BEGIN_TEST(testGCAllocator) { +#ifdef JS_64BIT + // If we're using the scattershot allocator, this test does not apply. + if (js::gc::UsingScattershotAllocator()) { + return true; + } +#endif + + size_t PageSize = js::gc::SystemPageSize(); + + /* Finish any ongoing background free activity. */ + js::gc::FinishGC(cx); + + bool growUp = false; + CHECK(addressesGrowUp(&growUp)); + + if (growUp) { + return testGCAllocatorUp(PageSize); + } else { + return testGCAllocatorDown(PageSize); + } +} + +static const size_t Chunk = 512 * 1024; +static const size_t Alignment = 2 * Chunk; +static const int MaxTempChunks = 4096; +static const size_t StagingSize = 16 * Chunk; + +bool addressesGrowUp(bool* resultOut) { + /* + * Try to detect whether the OS allocates memory in increasing or decreasing + * address order by making several allocations and comparing the addresses. + */ + + static const unsigned ChunksToTest = 20; + static const int ThresholdCount = 15; + + void* chunks[ChunksToTest]; + for (unsigned i = 0; i < ChunksToTest; i++) { + chunks[i] = mapMemory(2 * Chunk); + CHECK(chunks[i]); + } + + int upCount = 0; + int downCount = 0; + + for (unsigned i = 0; i < ChunksToTest - 1; i++) { + if (chunks[i] < chunks[i + 1]) { + upCount++; + } else { + downCount++; + } + } + + for (unsigned i = 0; i < ChunksToTest; i++) { + unmapPages(chunks[i], 2 * Chunk); + } + + /* Check results were mostly consistent. */ + CHECK(abs(upCount - downCount) >= ThresholdCount); + + *resultOut = upCount > downCount; + + return true; +} + +size_t offsetFromAligned(void* p) { return uintptr_t(p) % Alignment; } + +enum AllocType { UseNormalAllocator, UseLastDitchAllocator }; + +bool testGCAllocatorUp(const size_t PageSize) { + const size_t UnalignedSize = StagingSize + Alignment - PageSize; + void* chunkPool[MaxTempChunks]; + // Allocate a contiguous chunk that we can partition for testing. + void* stagingArea = mapMemory(UnalignedSize); + if (!stagingArea) { + return false; + } + // Ensure that the staging area is aligned. + unmapPages(stagingArea, UnalignedSize); + if (offsetFromAligned(stagingArea)) { + const size_t Offset = offsetFromAligned(stagingArea); + // Place the area at the lowest aligned address. + stagingArea = (void*)(uintptr_t(stagingArea) + (Alignment - Offset)); + } + mapMemoryAt(stagingArea, StagingSize); + // Make sure there are no available chunks below the staging area. + int tempChunks; + if (!fillSpaceBeforeStagingArea(tempChunks, stagingArea, chunkPool, false)) { + return false; + } + // Unmap the staging area so we can set it up for testing. + unmapPages(stagingArea, StagingSize); + // Check that the first chunk is used if it is aligned. + CHECK(positionIsCorrect("xxooxxx---------", stagingArea, chunkPool, + tempChunks)); + // Check that the first chunk is used if it can be aligned. + CHECK(positionIsCorrect("x-ooxxx---------", stagingArea, chunkPool, + tempChunks)); + // Check that an aligned chunk after a single unalignable chunk is used. + CHECK(positionIsCorrect("x--xooxxx-------", stagingArea, chunkPool, + tempChunks)); + // Check that we fall back to the slow path after two unalignable chunks. + CHECK(positionIsCorrect("x--xx--xoo--xxx-", stagingArea, chunkPool, + tempChunks)); + // Check that we also fall back after an unalignable and an alignable chunk. + CHECK(positionIsCorrect("x--xx---x-oo--x-", stagingArea, chunkPool, + tempChunks)); + // Check that the last ditch allocator works as expected. + CHECK(positionIsCorrect("x--xx--xx-oox---", stagingArea, chunkPool, + tempChunks, UseLastDitchAllocator)); + // Check that the last ditch allocator can deal with naturally aligned chunks. + CHECK(positionIsCorrect("x--xx--xoo------", stagingArea, chunkPool, + tempChunks, UseLastDitchAllocator)); + + // Clean up. + while (--tempChunks >= 0) { + unmapPages(chunkPool[tempChunks], 2 * Chunk); + } + return true; +} + +bool testGCAllocatorDown(const size_t PageSize) { + const size_t UnalignedSize = StagingSize + Alignment - PageSize; + void* chunkPool[MaxTempChunks]; + // Allocate a contiguous chunk that we can partition for testing. + void* stagingArea = mapMemory(UnalignedSize); + if (!stagingArea) { + return false; + } + // Ensure that the staging area is aligned. + unmapPages(stagingArea, UnalignedSize); + if (offsetFromAligned(stagingArea)) { + void* stagingEnd = (void*)(uintptr_t(stagingArea) + UnalignedSize); + const size_t Offset = offsetFromAligned(stagingEnd); + // Place the area at the highest aligned address. + stagingArea = (void*)(uintptr_t(stagingEnd) - Offset - StagingSize); + } + mapMemoryAt(stagingArea, StagingSize); + // Make sure there are no available chunks above the staging area. + int tempChunks; + if (!fillSpaceBeforeStagingArea(tempChunks, stagingArea, chunkPool, true)) { + return false; + } + // Unmap the staging area so we can set it up for testing. + unmapPages(stagingArea, StagingSize); + // Check that the first chunk is used if it is aligned. + CHECK(positionIsCorrect("---------xxxooxx", stagingArea, chunkPool, + tempChunks)); + // Check that the first chunk is used if it can be aligned. + CHECK(positionIsCorrect("---------xxxoo-x", stagingArea, chunkPool, + tempChunks)); + // Check that an aligned chunk after a single unalignable chunk is used. + CHECK(positionIsCorrect("-------xxxoox--x", stagingArea, chunkPool, + tempChunks)); + // Check that we fall back to the slow path after two unalignable chunks. + CHECK(positionIsCorrect("-xxx--oox--xx--x", stagingArea, chunkPool, + tempChunks)); + // Check that we also fall back after an unalignable and an alignable chunk. + CHECK(positionIsCorrect("-x--oo-x---xx--x", stagingArea, chunkPool, + tempChunks)); + // Check that the last ditch allocator works as expected. + CHECK(positionIsCorrect("---xoo-xx--xx--x", stagingArea, chunkPool, + tempChunks, UseLastDitchAllocator)); + // Check that the last ditch allocator can deal with naturally aligned chunks. + CHECK(positionIsCorrect("------oox--xx--x", stagingArea, chunkPool, + tempChunks, UseLastDitchAllocator)); + + // Clean up. + while (--tempChunks >= 0) { + unmapPages(chunkPool[tempChunks], 2 * Chunk); + } + return true; +} + +bool fillSpaceBeforeStagingArea(int& tempChunks, void* stagingArea, + void** chunkPool, bool addressesGrowDown) { + // Make sure there are no available chunks before the staging area. + tempChunks = 0; + chunkPool[tempChunks++] = mapMemory(2 * Chunk); + while (tempChunks < MaxTempChunks && chunkPool[tempChunks - 1] && + (chunkPool[tempChunks - 1] < stagingArea) ^ addressesGrowDown) { + chunkPool[tempChunks++] = mapMemory(2 * Chunk); + if (!chunkPool[tempChunks - 1]) { + break; // We already have our staging area, so OOM here is okay. + } + if ((chunkPool[tempChunks - 1] < chunkPool[tempChunks - 2]) ^ + addressesGrowDown) { + break; // The address growth direction is inconsistent! + } + } + // OOM also means success in this case. + if (!chunkPool[tempChunks - 1]) { + --tempChunks; + return true; + } + // Bail if we can't guarantee the right address space layout. + if ((chunkPool[tempChunks - 1] < stagingArea) ^ addressesGrowDown || + (tempChunks > 1 && + (chunkPool[tempChunks - 1] < chunkPool[tempChunks - 2]) ^ + addressesGrowDown)) { + while (--tempChunks >= 0) { + unmapPages(chunkPool[tempChunks], 2 * Chunk); + } + unmapPages(stagingArea, StagingSize); + return false; + } + return true; +} + +bool positionIsCorrect(const char* str, void* base, void** chunkPool, + int tempChunks, + AllocType allocator = UseNormalAllocator) { + // str represents a region of memory, with each character representing a + // region of Chunk bytes. str should contain only x, o and -, where + // x = mapped by the test to set up the initial conditions, + // o = mapped by the GC allocator, and + // - = unmapped. + // base should point to a region of contiguous free memory + // large enough to hold strlen(str) chunks of Chunk bytes. + int len = strlen(str); + int i; + // Find the index of the desired address. + for (i = 0; i < len && str[i] != 'o'; ++i) + ; + void* desired = (void*)(uintptr_t(base) + i * Chunk); + // Map the regions indicated by str. + for (i = 0; i < len; ++i) { + if (str[i] == 'x') { + mapMemoryAt((void*)(uintptr_t(base) + i * Chunk), Chunk); + } + } + // Allocate using the GC's allocator. + void* result; + if (allocator == UseNormalAllocator) { + result = js::gc::MapAlignedPages(2 * Chunk, Alignment); + } else { + result = js::gc::TestMapAlignedPagesLastDitch(2 * Chunk, Alignment); + } + // Clean up the mapped regions. + if (result) { + js::gc::UnmapPages(result, 2 * Chunk); + } + for (--i; i >= 0; --i) { + if (str[i] == 'x') { + js::gc::UnmapPages((void*)(uintptr_t(base) + i * Chunk), Chunk); + } + } + // CHECK returns, so clean up on failure. + if (result != desired) { + while (--tempChunks >= 0) { + js::gc::UnmapPages(chunkPool[tempChunks], 2 * Chunk); + } + } + return result == desired; +} + +#if defined(XP_WIN) + +void* mapMemoryAt(void* desired, size_t length) { + return VirtualAlloc(desired, length, MEM_COMMIT | MEM_RESERVE, + PAGE_READWRITE); +} + +void* mapMemory(size_t length) { + return VirtualAlloc(nullptr, length, MEM_COMMIT | MEM_RESERVE, + PAGE_READWRITE); +} + +void unmapPages(void* p, size_t size) { + MOZ_ALWAYS_TRUE(VirtualFree(p, 0, MEM_RELEASE)); +} + +#elif defined(__wasi__) + +void* mapMemoryAt(void* desired, size_t length) { return nullptr; } + +void* mapMemory(size_t length) { + void* addr = nullptr; + if (int err = posix_memalign(&addr, js::gc::SystemPageSize(), length)) { + MOZ_ASSERT(err == ENOMEM); + } + MOZ_ASSERT(addr); + memset(addr, 0, length); + return addr; +} + +void unmapPages(void* p, size_t size) { free(p); } + +#else + +void* mapMemoryAt(void* desired, size_t length) { + void* region = mmap(desired, length, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANON, -1, 0); + if (region == MAP_FAILED) { + return nullptr; + } + if (region != desired) { + if (munmap(region, length)) { + MOZ_RELEASE_ASSERT(errno == ENOMEM); + } + return nullptr; + } + return region; +} + +void* mapMemory(size_t length) { + int prot = PROT_READ | PROT_WRITE; + int flags = MAP_PRIVATE | MAP_ANON; + int fd = -1; + off_t offset = 0; + void* region = mmap(nullptr, length, prot, flags, fd, offset); + if (region == MAP_FAILED) { + return nullptr; + } + return region; +} + +void unmapPages(void* p, size_t size) { + if (munmap(p, size)) { + MOZ_RELEASE_ASSERT(errno == ENOMEM); + } +} + +#endif + +END_TEST(testGCAllocator) diff --git a/js/src/jsapi-tests/testGCCellPtr.cpp b/js/src/jsapi-tests/testGCCellPtr.cpp new file mode 100644 index 0000000000..ef74808b52 --- /dev/null +++ b/js/src/jsapi-tests/testGCCellPtr.cpp @@ -0,0 +1,58 @@ +/* -*- 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.h" +#include "jspubtd.h" + +#include "js/CompilationAndEvaluation.h" // JS::Compile +#include "js/SourceText.h" // JS::Source{Ownership,Text} +#include "jsapi-tests/tests.h" + +JS::GCCellPtr GivesAndTakesCells(JS::GCCellPtr cell) { return cell; } + +BEGIN_TEST(testGCCellPtr) { + JS::RootedObject obj(cx, JS_NewPlainObject(cx)); + CHECK(obj); + + JS::RootedString str(cx, JS_NewStringCopyZ(cx, "probably foobar")); + CHECK(str); + + const char* code = "function foo() { return 'bar'; }"; + + JS::CompileOptions opts(cx); + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + CHECK(srcBuf.init(cx, code, strlen(code), JS::SourceOwnership::Borrowed)); + + JS::RootedScript script(cx, JS::Compile(cx, opts, srcBuf)); + CHECK(script); + + CHECK(!JS::GCCellPtr(nullptr)); + + CHECK(JS::GCCellPtr(obj.get())); + CHECK(JS::GCCellPtr(obj.get()).kind() == JS::TraceKind::Object); + CHECK(JS::ObjectValue(*obj).toGCCellPtr().kind() == JS::TraceKind::Object); + + CHECK(JS::GCCellPtr(str.get())); + CHECK(JS::GCCellPtr(str.get()).kind() == JS::TraceKind::String); + CHECK(JS::StringValue(str).toGCCellPtr().kind() == JS::TraceKind::String); + + CHECK(JS::GCCellPtr(script.get())); + CHECK(!JS::GCCellPtr(nullptr)); + CHECK(JS::GCCellPtr(script.get()).kind() == JS::TraceKind::Script); + + JS::GCCellPtr objcell(obj.get()); + JS::GCCellPtr scriptcell = JS::GCCellPtr(script.get()); + CHECK(GivesAndTakesCells(objcell)); + CHECK(GivesAndTakesCells(scriptcell)); + + JS::GCCellPtr copy = objcell; + CHECK(copy == objcell); + + return true; +} +END_TEST(testGCCellPtr) diff --git a/js/src/jsapi-tests/testGCChunkPool.cpp b/js/src/jsapi-tests/testGCChunkPool.cpp new file mode 100644 index 0000000000..12b42330d4 --- /dev/null +++ b/js/src/jsapi-tests/testGCChunkPool.cpp @@ -0,0 +1,74 @@ +/* -*- 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 <utility> + +#include "gc/GCLock.h" +#include "gc/GCRuntime.h" +#include "jsapi-tests/tests.h" + +BEGIN_TEST(testGCChunkPool) { + using namespace js::gc; + + const int N = 10; + ChunkPool pool; + + // Create. + for (int i = 0; i < N; ++i) { + void* ptr = TenuredChunk::allocate(&cx->runtime()->gc); + CHECK(ptr); + TenuredChunk* chunk = TenuredChunk::emplace(ptr, &cx->runtime()->gc, true); + CHECK(chunk); + pool.push(chunk); + } + MOZ_ASSERT(pool.verify()); + + // Iterate. + uint32_t i = 0; + for (ChunkPool::Iter iter(pool); !iter.done(); iter.next(), ++i) { + CHECK(iter.get()); + } + CHECK(i == pool.count()); + MOZ_ASSERT(pool.verify()); + + // Push/Pop. + for (int i = 0; i < N; ++i) { + TenuredChunk* chunkA = pool.pop(); + TenuredChunk* chunkB = pool.pop(); + TenuredChunk* chunkC = pool.pop(); + pool.push(chunkA); + pool.push(chunkB); + pool.push(chunkC); + } + MOZ_ASSERT(pool.verify()); + + // Remove. + TenuredChunk* chunk = nullptr; + int offset = N / 2; + for (ChunkPool::Iter iter(pool); !iter.done(); iter.next(), --offset) { + if (offset == 0) { + chunk = pool.remove(iter.get()); + break; + } + } + CHECK(chunk); + MOZ_ASSERT(!pool.contains(chunk)); + MOZ_ASSERT(pool.verify()); + pool.push(chunk); + + // Destruct. + js::AutoLockGC lock(cx->runtime()); + for (ChunkPool::Iter iter(pool); !iter.done();) { + TenuredChunk* chunk = iter.get(); + iter.next(); + pool.remove(chunk); + UnmapPages(chunk, ChunkSize); + } + + return true; +} +END_TEST(testGCChunkPool) diff --git a/js/src/jsapi-tests/testGCExactRooting.cpp b/js/src/jsapi-tests/testGCExactRooting.cpp new file mode 100644 index 0000000000..79af0a886e --- /dev/null +++ b/js/src/jsapi-tests/testGCExactRooting.cpp @@ -0,0 +1,917 @@ +/* -*- 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 "mozilla/Maybe.h" +#include "mozilla/Result.h" +#include "mozilla/ResultVariant.h" + +#include "ds/TraceableFifo.h" +#include "gc/Policy.h" +#include "js/GCHashTable.h" +#include "js/GCVector.h" +#include "js/PropertyAndElement.h" // JS_DefineProperty, JS_GetProperty, JS_SetProperty +#include "js/RootingAPI.h" + +#include "jsapi-tests/tests.h" + +using namespace js; + +using mozilla::Maybe; +using mozilla::Some; + +BEGIN_TEST(testGCExactRooting) { + JS::RootedObject rootCx(cx, JS_NewPlainObject(cx)); + + JS_GC(cx); + + /* Use the objects we just created to ensure that they are still alive. */ + JS_DefineProperty(cx, rootCx, "foo", JS::UndefinedHandleValue, 0); + + return true; +} +END_TEST(testGCExactRooting) + +BEGIN_TEST(testGCSuppressions) { + JS::AutoAssertNoGC nogc; + JS::AutoCheckCannotGC checkgc; + JS::AutoSuppressGCAnalysis noanalysis; + + JS::AutoAssertNoGC nogcCx(cx); + JS::AutoCheckCannotGC checkgcCx(cx); + JS::AutoSuppressGCAnalysis noanalysisCx(cx); + + return true; +} +END_TEST(testGCSuppressions) + +struct MyContainer { + int whichConstructor; + HeapPtr<JSObject*> obj; + HeapPtr<JSString*> str; + + MyContainer() : whichConstructor(1), obj(nullptr), str(nullptr) {} + explicit MyContainer(double) : MyContainer() { whichConstructor = 2; } + explicit MyContainer(JSContext* cx) : MyContainer() { whichConstructor = 3; } + MyContainer(JSContext* cx, JSContext* cx2, JSContext* cx3) : MyContainer() { + whichConstructor = 4; + } + MyContainer(const MyContainer& rhs) + : whichConstructor(100 + rhs.whichConstructor), + obj(rhs.obj), + str(rhs.str) {} + void trace(JSTracer* trc) { + js::TraceNullableEdge(trc, &obj, "test container obj"); + js::TraceNullableEdge(trc, &str, "test container str"); + } +}; + +struct MyNonCopyableContainer { + int whichConstructor; + HeapPtr<JSObject*> obj; + HeapPtr<JSString*> str; + + MyNonCopyableContainer() : whichConstructor(1), obj(nullptr), str(nullptr) {} + explicit MyNonCopyableContainer(double) : MyNonCopyableContainer() { + whichConstructor = 2; + } + explicit MyNonCopyableContainer(JSContext* cx) : MyNonCopyableContainer() { + whichConstructor = 3; + } + explicit MyNonCopyableContainer(JSContext* cx, JSContext* cx2, JSContext* cx3) + : MyNonCopyableContainer() { + whichConstructor = 4; + } + + MyNonCopyableContainer(const MyNonCopyableContainer&) = delete; + MyNonCopyableContainer& operator=(const MyNonCopyableContainer&) = delete; + + void trace(JSTracer* trc) { + js::TraceNullableEdge(trc, &obj, "test container obj"); + js::TraceNullableEdge(trc, &str, "test container str"); + } +}; + +namespace js { +template <typename Wrapper> +struct MutableWrappedPtrOperations<MyContainer, Wrapper> { + HeapPtr<JSObject*>& obj() { return static_cast<Wrapper*>(this)->get().obj; } + HeapPtr<JSString*>& str() { return static_cast<Wrapper*>(this)->get().str; } + int constructor() { + return static_cast<Wrapper*>(this)->get().whichConstructor; + } +}; + +template <typename Wrapper> +struct MutableWrappedPtrOperations<MyNonCopyableContainer, Wrapper> { + HeapPtr<JSObject*>& obj() { return static_cast<Wrapper*>(this)->get().obj; } + HeapPtr<JSString*>& str() { return static_cast<Wrapper*>(this)->get().str; } + int constructor() { + return static_cast<Wrapper*>(this)->get().whichConstructor; + } +}; +} // namespace js + +BEGIN_TEST(testGCRootedStaticStructInternalStackStorageAugmented) { + // Test Rooted constructors for a copyable type. + JS::Rooted<MyContainer> r1(cx); + JS::Rooted<MyContainer> r2(cx, 3.4); + JS::Rooted<MyContainer> r3(cx, MyContainer(cx)); + JS::Rooted<MyContainer> r4(cx, cx); + JS::Rooted<MyContainer> r5(cx, cx, cx, cx); + + JS::Rooted<Value> rv(cx); + + CHECK_EQUAL(r1.constructor(), 1); // direct SafelyInitialized<T> + CHECK_EQUAL(r2.constructor(), 2); // direct MyContainer(3.4) + CHECK_EQUAL(r3.constructor(), 103); // copy of MyContainer(cx) + CHECK_EQUAL(r4.constructor(), 3); // direct MyContainer(cx) + CHECK_EQUAL(r5.constructor(), 4); // direct MyContainer(cx, cx, cx) + + // Test Rooted constructor forwarding for a non-copyable type. + JS::Rooted<MyNonCopyableContainer> nc1(cx); + JS::Rooted<MyNonCopyableContainer> nc2(cx, 3.4); + // Compile error: cannot copy + // JS::Rooted<MyNonCopyableContainer> nc3(cx, MyNonCopyableContainer(cx)); + JS::Rooted<MyNonCopyableContainer> nc4(cx, cx); + JS::Rooted<MyNonCopyableContainer> nc5(cx, cx, cx, cx); + + CHECK_EQUAL(nc1.constructor(), 1); // direct MyNonCopyableContainer() + CHECK_EQUAL(nc2.constructor(), 2); // direct MyNonCopyableContainer(3.4) + CHECK_EQUAL(nc4.constructor(), 3); // direct MyNonCopyableContainer(cx) + CHECK_EQUAL(nc5.constructor(), + 4); // direct MyNonCopyableContainer(cx, cx, cx) + + JS::Rooted<MyContainer> container(cx); + container.obj() = JS_NewObject(cx, nullptr); + container.str() = JS_NewStringCopyZ(cx, "Hello"); + + JS_GC(cx); + JS_GC(cx); + + JS::RootedObject obj(cx, container.obj()); + JS::RootedValue val(cx, StringValue(container.str())); + CHECK(JS_SetProperty(cx, obj, "foo", val)); + obj = nullptr; + val = UndefinedValue(); + + { + JS::RootedString actual(cx); + bool same; + + // Automatic move from stack to heap. + JS::PersistentRooted<MyContainer> heap(cx, container); + + // Copyable types in place. + JS::PersistentRooted<MyContainer> cp1(cx); + JS::PersistentRooted<MyContainer> cp2(cx, 7.8); + JS::PersistentRooted<MyContainer> cp3(cx, cx); + JS::PersistentRooted<MyContainer> cp4(cx, cx, cx, cx); + + CHECK_EQUAL(cp1.constructor(), 1); // direct SafelyInitialized<T> + CHECK_EQUAL(cp2.constructor(), 2); // direct MyContainer(double) + CHECK_EQUAL(cp3.constructor(), 3); // direct MyContainer(cx) + CHECK_EQUAL(cp4.constructor(), 4); // direct MyContainer(cx, cx, cx) + + // Construct uncopyable type in place. + JS::PersistentRooted<MyNonCopyableContainer> ncp1(cx); + JS::PersistentRooted<MyNonCopyableContainer> ncp2(cx, 7.8); + + // We're not just using a 1-arg constructor, right? + JS::PersistentRooted<MyNonCopyableContainer> ncp3(cx, cx); + JS::PersistentRooted<MyNonCopyableContainer> ncp4(cx, cx, cx, cx); + + CHECK_EQUAL(ncp1.constructor(), 1); // direct SafelyInitialized<T> + CHECK_EQUAL(ncp2.constructor(), 2); // direct Ctor(double) + CHECK_EQUAL(ncp3.constructor(), 3); // direct Ctor(cx) + CHECK_EQUAL(ncp4.constructor(), 4); // direct Ctor(cx, cx, cx) + + // clear prior rooting. + container.obj() = nullptr; + container.str() = nullptr; + + obj = heap.obj(); + CHECK(JS_GetProperty(cx, obj, "foo", &val)); + actual = val.toString(); + CHECK(JS_StringEqualsLiteral(cx, actual, "Hello", &same)); + CHECK(same); + obj = nullptr; + actual = nullptr; + + JS_GC(cx); + JS_GC(cx); + + obj = heap.obj(); + CHECK(JS_GetProperty(cx, obj, "foo", &val)); + actual = val.toString(); + CHECK(JS_StringEqualsLiteral(cx, actual, "Hello", &same)); + CHECK(same); + obj = nullptr; + actual = nullptr; + } + + return true; +} +END_TEST(testGCRootedStaticStructInternalStackStorageAugmented) + +static JS::PersistentRooted<JSObject*> sLongLived; +BEGIN_TEST(testGCPersistentRootedOutlivesRuntime) { + sLongLived.init(cx, JS_NewObject(cx, nullptr)); + CHECK(sLongLived); + return true; +} +END_TEST(testGCPersistentRootedOutlivesRuntime) + +// Unlike the above, the following test is an example of an invalid usage: for +// performance and simplicity reasons, PersistentRooted<Traceable> is not +// allowed to outlive the container it belongs to. The following commented out +// test can be used to verify that the relevant assertion fires as expected. +static JS::PersistentRooted<MyContainer> sContainer; +BEGIN_TEST(testGCPersistentRootedTraceableCannotOutliveRuntime) { + JS::Rooted<MyContainer> container(cx); + container.obj() = JS_NewObject(cx, nullptr); + container.str() = JS_NewStringCopyZ(cx, "Hello"); + sContainer.init(cx, container); + + // Commenting the following line will trigger an assertion that the + // PersistentRooted outlives the runtime it is attached to. + sContainer.reset(); + + return true; +} +END_TEST(testGCPersistentRootedTraceableCannotOutliveRuntime) + +using MyHashMap = js::GCHashMap<js::Shape*, JSObject*>; + +BEGIN_TEST(testGCRootedHashMap) { + JS::Rooted<MyHashMap> map(cx, MyHashMap(cx, 15)); + + for (size_t i = 0; i < 10; ++i) { + RootedObject obj(cx, JS_NewObject(cx, nullptr)); + RootedValue val(cx, UndefinedValue()); + // Construct a unique property name to ensure that the object creates a + // new shape. + char buffer[2]; + buffer[0] = 'a' + i; + buffer[1] = '\0'; + CHECK(JS_SetProperty(cx, obj, buffer, val)); + CHECK(map.putNew(obj->shape(), obj)); + } + + JS_GC(cx); + JS_GC(cx); + + for (auto r = map.all(); !r.empty(); r.popFront()) { + RootedObject obj(cx, r.front().value()); + CHECK(obj->shape() == r.front().key()); + } + + return true; +} +END_TEST(testGCRootedHashMap) + +// Repeat of the test above, but without rooting. This is a rooting hazard. The +// JS_EXPECT_HAZARDS annotation will cause the hazard taskcluster job to fail +// if the hazard below is *not* detected. +BEGIN_TEST_WITH_ATTRIBUTES(testUnrootedGCHashMap, JS_EXPECT_HAZARDS) { + AutoLeaveZeal noZeal(cx); + + MyHashMap map(cx, 15); + + for (size_t i = 0; i < 10; ++i) { + RootedObject obj(cx, JS_NewObject(cx, nullptr)); + RootedValue val(cx, UndefinedValue()); + // Construct a unique property name to ensure that the object creates a + // new shape. + char buffer[2]; + buffer[0] = 'a' + i; + buffer[1] = '\0'; + CHECK(JS_SetProperty(cx, obj, buffer, val)); + CHECK(map.putNew(obj->shape(), obj)); + } + + JS_GC(cx); + + // Access map to keep it live across the GC. + CHECK(map.count() == 10); + + return true; +} +END_TEST(testUnrootedGCHashMap) + +BEGIN_TEST(testSafelyUnrootedGCHashMap) { + // This is not rooted, but it doesn't use GC pointers as keys or values so + // it's ok. + js::GCHashMap<uint64_t, uint64_t> map(cx, 15); + + JS_GC(cx); + CHECK(map.putNew(12, 13)); + + return true; +} +END_TEST(testSafelyUnrootedGCHashMap) + +static bool FillMyHashMap(JSContext* cx, MutableHandle<MyHashMap> map) { + for (size_t i = 0; i < 10; ++i) { + RootedObject obj(cx, JS_NewObject(cx, nullptr)); + RootedValue val(cx, UndefinedValue()); + // Construct a unique property name to ensure that the object creates a + // new shape. + char buffer[2]; + buffer[0] = 'a' + i; + buffer[1] = '\0'; + if (!JS_SetProperty(cx, obj, buffer, val)) { + return false; + } + if (!map.putNew(obj->shape(), obj)) { + return false; + } + } + return true; +} + +static bool CheckMyHashMap(JSContext* cx, Handle<MyHashMap> map) { + for (auto r = map.all(); !r.empty(); r.popFront()) { + RootedObject obj(cx, r.front().value()); + if (obj->shape() != r.front().key()) { + return false; + } + } + return true; +} + +BEGIN_TEST(testGCHandleHashMap) { + JS::Rooted<MyHashMap> map(cx, MyHashMap(cx, 15)); + + CHECK(FillMyHashMap(cx, &map)); + + JS_GC(cx); + JS_GC(cx); + + CHECK(CheckMyHashMap(cx, map)); + + return true; +} +END_TEST(testGCHandleHashMap) + +using ShapeVec = GCVector<NativeShape*>; + +BEGIN_TEST(testGCRootedVector) { + JS::Rooted<ShapeVec> shapes(cx, cx); + + for (size_t i = 0; i < 10; ++i) { + RootedObject obj(cx, JS_NewObject(cx, nullptr)); + RootedValue val(cx, UndefinedValue()); + // Construct a unique property name to ensure that the object creates a + // new shape. + char buffer[2]; + buffer[0] = 'a' + i; + buffer[1] = '\0'; + CHECK(JS_SetProperty(cx, obj, buffer, val)); + CHECK(shapes.append(obj->as<NativeObject>().shape())); + } + + JS_GC(cx); + JS_GC(cx); + + for (size_t i = 0; i < 10; ++i) { + // Check the shape to ensure it did not get collected. + char letter = 'a' + i; + bool match; + ShapePropertyIter<NoGC> iter(shapes[i]); + CHECK(JS_StringEqualsAscii(cx, iter->key().toString(), &letter, 1, &match)); + CHECK(match); + } + + // Ensure iterator enumeration works through the rooted. + for (auto shape : shapes) { + CHECK(shape); + } + + CHECK(receiveConstRefToShapeVector(shapes)); + + // Ensure rooted converts to handles. + CHECK(receiveHandleToShapeVector(shapes)); + CHECK(receiveMutableHandleToShapeVector(&shapes)); + + return true; +} + +bool receiveConstRefToShapeVector( + const JS::Rooted<GCVector<NativeShape*>>& rooted) { + // Ensure range enumeration works through the reference. + for (auto shape : rooted) { + CHECK(shape); + } + return true; +} + +bool receiveHandleToShapeVector(JS::Handle<GCVector<NativeShape*>> handle) { + // Ensure range enumeration works through the handle. + for (auto shape : handle) { + CHECK(shape); + } + return true; +} + +bool receiveMutableHandleToShapeVector( + JS::MutableHandle<GCVector<NativeShape*>> handle) { + // Ensure range enumeration works through the handle. + for (auto shape : handle) { + CHECK(shape); + } + return true; +} +END_TEST(testGCRootedVector) + +BEGIN_TEST(testTraceableFifo) { + using ShapeFifo = TraceableFifo<NativeShape*>; + JS::Rooted<ShapeFifo> shapes(cx, ShapeFifo(cx)); + CHECK(shapes.empty()); + + for (size_t i = 0; i < 10; ++i) { + RootedObject obj(cx, JS_NewObject(cx, nullptr)); + RootedValue val(cx, UndefinedValue()); + // Construct a unique property name to ensure that the object creates a + // new shape. + char buffer[2]; + buffer[0] = 'a' + i; + buffer[1] = '\0'; + CHECK(JS_SetProperty(cx, obj, buffer, val)); + CHECK(shapes.pushBack(obj->as<NativeObject>().shape())); + } + + CHECK(shapes.length() == 10); + + JS_GC(cx); + JS_GC(cx); + + for (size_t i = 0; i < 10; ++i) { + // Check the shape to ensure it did not get collected. + char letter = 'a' + i; + bool match; + ShapePropertyIter<NoGC> iter(shapes.front()); + CHECK(JS_StringEqualsAscii(cx, iter->key().toString(), &letter, 1, &match)); + CHECK(match); + shapes.popFront(); + } + + CHECK(shapes.empty()); + return true; +} +END_TEST(testTraceableFifo) + +using ShapeVec = GCVector<NativeShape*>; + +static bool FillVector(JSContext* cx, MutableHandle<ShapeVec> shapes) { + for (size_t i = 0; i < 10; ++i) { + RootedObject obj(cx, JS_NewObject(cx, nullptr)); + RootedValue val(cx, UndefinedValue()); + // Construct a unique property name to ensure that the object creates a + // new shape. + char buffer[2]; + buffer[0] = 'a' + i; + buffer[1] = '\0'; + if (!JS_SetProperty(cx, obj, buffer, val)) { + return false; + } + if (!shapes.append(obj->as<NativeObject>().shape())) { + return false; + } + } + + // Ensure iterator enumeration works through the mutable handle. + for (auto shape : shapes) { + if (!shape) { + return false; + } + } + + return true; +} + +static bool CheckVector(JSContext* cx, Handle<ShapeVec> shapes) { + for (size_t i = 0; i < 10; ++i) { + // Check the shape to ensure it did not get collected. + char letter = 'a' + i; + bool match; + ShapePropertyIter<NoGC> iter(shapes[i]); + if (!JS_StringEqualsAscii(cx, iter->key().toString(), &letter, 1, &match)) { + return false; + } + if (!match) { + return false; + } + } + + // Ensure iterator enumeration works through the handle. + for (auto shape : shapes) { + if (!shape) { + return false; + } + } + + return true; +} + +BEGIN_TEST(testGCHandleVector) { + JS::Rooted<ShapeVec> vec(cx, ShapeVec(cx)); + + CHECK(FillVector(cx, &vec)); + + JS_GC(cx); + JS_GC(cx); + + CHECK(CheckVector(cx, vec)); + + return true; +} +END_TEST(testGCHandleVector) + +class Foo { + public: + Foo(int, int) {} + void trace(JSTracer*) {} +}; + +using FooVector = JS::GCVector<Foo>; + +BEGIN_TEST(testGCVectorEmplaceBack) { + JS::Rooted<FooVector> vector(cx, FooVector(cx)); + + CHECK(vector.emplaceBack(1, 2)); + + return true; +} +END_TEST(testGCVectorEmplaceBack) + +BEGIN_TEST(testRootedMaybeValue) { + JS::Rooted<Maybe<Value>> maybeNothing(cx); + CHECK(maybeNothing.isNothing()); + CHECK(!maybeNothing.isSome()); + + JS::Rooted<Maybe<Value>> maybe(cx, Some(UndefinedValue())); + CHECK(CheckConstOperations<Rooted<Maybe<Value>>&>(maybe)); + CHECK(CheckConstOperations<Handle<Maybe<Value>>>(maybe)); + CHECK(CheckConstOperations<MutableHandle<Maybe<Value>>>(&maybe)); + + maybe = Some(JS::TrueValue()); + CHECK(CheckMutableOperations<Rooted<Maybe<Value>>&>(maybe)); + + maybe = Some(JS::TrueValue()); + CHECK(CheckMutableOperations<MutableHandle<Maybe<Value>>>(&maybe)); + + CHECK(JS::NothingHandleValue.isNothing()); + + return true; +} + +template <typename T> +bool CheckConstOperations(T someUndefinedValue) { + CHECK(someUndefinedValue.isSome()); + CHECK(someUndefinedValue.value().isUndefined()); + CHECK(someUndefinedValue->isUndefined()); + CHECK((*someUndefinedValue).isUndefined()); + return true; +} + +template <typename T> +bool CheckMutableOperations(T maybe) { + CHECK(maybe->isTrue()); + + *maybe = JS::FalseValue(); + CHECK(maybe->isFalse()); + + maybe.reset(); + CHECK(maybe.isNothing()); + + return true; +} + +END_TEST(testRootedMaybeValue) + +struct TestErr {}; +struct OtherTestErr {}; + +struct SimpleTraceable { + // I'm using plain objects rather than Heap<T> because Heap<T> would get + // traced via the store buffer. Heap<T> would be a more realistic example, + // but would require compaction to test for tracing. + JSObject* obj; + JS::Value val; + + void trace(JSTracer* trc) { + TraceRoot(trc, &obj, "obj"); + TraceRoot(trc, &val, "val"); + } +}; + +namespace JS { +template <> +struct GCPolicy<TestErr> : public IgnoreGCPolicy<TestErr> {}; +} // namespace JS + +BEGIN_TEST_WITH_ATTRIBUTES(testGCRootedResult, JS_EXPECT_HAZARDS) { + AutoLeaveZeal noZeal(cx); + + JSObject* unrootedObj = JS_NewPlainObject(cx); + CHECK(js::gc::IsInsideNursery(unrootedObj)); + Value unrootedVal = ObjectValue(*unrootedObj); + + RootedObject obj(cx, unrootedObj); + RootedValue val(cx, unrootedVal); + + Result<Value, TestErr> unrootedValerr(val); + Rooted<Result<Value, TestErr>> valerr(cx, val); + + Result<mozilla::Ok, Value> unrootedOkval(val); + Rooted<Result<mozilla::Ok, Value>> okval(cx, val); + + Result<mozilla::Ok, TestErr> simple{mozilla::Ok()}; + + Result<Value, JSObject*> unrootedValobj1(val); + Rooted<Result<Value, JSObject*>> valobj1(cx, val); + Result<Value, JSObject*> unrootedValobj2(obj); + Rooted<Result<Value, JSObject*>> valobj2(cx, obj); + + // Test nested traceable structures. + Result<mozilla::Maybe<mozilla::Ok>, JSObject*> maybeobj( + mozilla::Some(mozilla::Ok())); + Rooted<Result<mozilla::Maybe<mozilla::Ok>, JSObject*>> rooted_maybeobj( + cx, mozilla::Some(mozilla::Ok())); + + // This would fail to compile because Result<> deletes its copy constructor, + // which prevents updating after tracing: + // + // Rooted<Result<Result<mozilla::Ok, JS::Value>, JSObject*>> + + // But this should be fine when no tracing is required. + Result<Result<mozilla::Ok, int>, double> dummy(3.4); + + // One thing I didn't realize initially about Result<>: unwrap() takes + // ownership of a value. In the case of Result<Maybe>, that means the + // contained Maybe is reset to Nothing. + Result<mozilla::Maybe<int>, int> confusing(mozilla::Some(7)); + CHECK(confusing.unwrap().isSome()); + CHECK(!confusing.unwrap().isSome()); + + Result<mozilla::Maybe<JS::Value>, JSObject*> maybevalobj( + mozilla::Some(val.get())); + Rooted<Result<mozilla::Maybe<JS::Value>, JSObject*>> rooted_maybevalobj( + cx, mozilla::Some(val.get())); + + // Custom types that haven't had GCPolicy explicitly specialized. + SimpleTraceable s1{obj, val}; + Result<SimpleTraceable, TestErr> custom(s1); + SimpleTraceable s2{obj, val}; + Rooted<Result<SimpleTraceable, TestErr>> rootedCustom(cx, s2); + + CHECK(obj == unrootedObj); + CHECK(val == unrootedVal); + CHECK(simple.isOk()); + CHECK(unrootedValerr.inspect() == unrootedVal); + CHECK(valerr.get().inspect() == val); + CHECK(unrootedOkval.inspectErr() == unrootedVal); + CHECK(okval.get().inspectErr() == val); + CHECK(unrootedValobj1.inspect() == unrootedVal); + CHECK(valobj1.get().inspect() == val); + CHECK(unrootedValobj2.inspectErr() == unrootedObj); + CHECK(valobj2.get().inspectErr() == obj); + CHECK(*maybevalobj.inspect() == unrootedVal); + CHECK(*rooted_maybevalobj.get().inspect() == val); + CHECK(custom.inspect().obj == unrootedObj); + CHECK(custom.inspect().val == unrootedVal); + CHECK(rootedCustom.get().inspect().obj == obj); + CHECK(rootedCustom.get().inspect().val == val); + + JS_GC(cx); + + CHECK(obj != unrootedObj); + CHECK(val != unrootedVal); + CHECK(unrootedValerr.inspect() == unrootedVal); + CHECK(valerr.get().inspect() == val); + CHECK(unrootedOkval.inspectErr() == unrootedVal); + CHECK(okval.get().inspectErr() == val); + CHECK(unrootedValobj1.inspect() == unrootedVal); + CHECK(valobj1.get().inspect() == val); + CHECK(unrootedValobj2.inspectErr() == unrootedObj); + CHECK(valobj2.get().inspectErr() == obj); + CHECK(*maybevalobj.inspect() == unrootedVal); + CHECK(*rooted_maybevalobj.get().inspect() == val); + MOZ_ASSERT(custom.inspect().obj == unrootedObj); + CHECK(custom.inspect().obj == unrootedObj); + CHECK(custom.inspect().val == unrootedVal); + CHECK(rootedCustom.get().inspect().obj == obj); + CHECK(rootedCustom.get().inspect().val == val); + + mozilla::Result<OtherTestErr, mozilla::Ok> r(mozilla::Ok{}); + (void)r; + + return true; +} +END_TEST(testGCRootedResult) + +static int copies = 0; + +struct DontCopyMe_Variant { + JSObject* obj; + explicit DontCopyMe_Variant(JSObject* objArg) : obj(objArg) {} + DontCopyMe_Variant(const DontCopyMe_Variant& other) : obj(other.obj) { + copies++; + } + DontCopyMe_Variant(DontCopyMe_Variant&& other) : obj(std::move(other.obj)) { + other.obj = nullptr; + } + void trace(JSTracer* trc) { TraceRoot(trc, &obj, "obj"); } +}; + +enum struct TestUnusedZeroEnum : int16_t { Ok = 0, NotOk = 1 }; + +namespace mozilla::detail { +template <> +struct UnusedZero<TestUnusedZeroEnum> : UnusedZeroEnum<TestUnusedZeroEnum> {}; +} // namespace mozilla::detail + +namespace JS { +template <> +struct GCPolicy<TestUnusedZeroEnum> + : public IgnoreGCPolicy<TestUnusedZeroEnum> {}; +} // namespace JS + +struct DontCopyMe_NullIsOk { + JS::Value val; + DontCopyMe_NullIsOk() : val(UndefinedValue()) {} + explicit DontCopyMe_NullIsOk(const JS::Value& valArg) : val(valArg) {} + DontCopyMe_NullIsOk(const DontCopyMe_NullIsOk& other) = delete; + DontCopyMe_NullIsOk(DontCopyMe_NullIsOk&& other) + : val(std::move(other.val)) {} + DontCopyMe_NullIsOk& operator=(DontCopyMe_NullIsOk&& other) { + val = std::move(other.val); + other.val = UndefinedValue(); + return *this; + } + void trace(JSTracer* trc) { TraceRoot(trc, &val, "val"); } +}; + +struct Failed {}; + +namespace mozilla::detail { +template <> +struct UnusedZero<Failed> { + using StorageType = uintptr_t; + + static constexpr bool value = true; + static constexpr StorageType nullValue = 0; + static constexpr StorageType GetDefaultValue() { return 2; } + + static constexpr void AssertValid(StorageType aValue) {} + static constexpr Failed Inspect(const StorageType& aValue) { + return Failed{}; + } + static constexpr Failed Unwrap(StorageType aValue) { return Failed{}; } + static constexpr StorageType Store(Failed aValue) { + return GetDefaultValue(); + } +}; +} // namespace mozilla::detail + +namespace JS { +template <> +struct GCPolicy<Failed> : public IgnoreGCPolicy<Failed> {}; +} // namespace JS + +struct TriviallyCopyable_LowBitTagIsError { + JSObject* obj; + TriviallyCopyable_LowBitTagIsError() : obj(nullptr) {} + explicit TriviallyCopyable_LowBitTagIsError(JSObject* objArg) : obj(objArg) {} + TriviallyCopyable_LowBitTagIsError( + const TriviallyCopyable_LowBitTagIsError& other) = default; + void trace(JSTracer* trc) { TraceRoot(trc, &obj, "obj"); } +}; + +namespace mozilla::detail { +template <> +struct HasFreeLSB<TriviallyCopyable_LowBitTagIsError> : HasFreeLSB<JSObject*> { +}; +} // namespace mozilla::detail + +BEGIN_TEST_WITH_ATTRIBUTES(testRootedResultCtors, JS_EXPECT_HAZARDS) { + JSObject* unrootedObj = JS_NewPlainObject(cx); + CHECK(unrootedObj); + Rooted<JSObject*> obj(cx, unrootedObj); + + using mozilla::detail::PackingStrategy; + + static_assert(Result<DontCopyMe_Variant, TestErr>::Strategy == + PackingStrategy::Variant); + Rooted<Result<DontCopyMe_Variant, TestErr>> vv(cx, DontCopyMe_Variant{obj}); + static_assert(Result<mozilla::Ok, DontCopyMe_Variant>::Strategy == + PackingStrategy::Variant); + Rooted<Result<mozilla::Ok, DontCopyMe_Variant>> ve(cx, + DontCopyMe_Variant{obj}); + + static_assert(Result<DontCopyMe_NullIsOk, TestUnusedZeroEnum>::Strategy == + PackingStrategy::NullIsOk); + Rooted<Result<DontCopyMe_NullIsOk, TestUnusedZeroEnum>> nv( + cx, DontCopyMe_NullIsOk{JS::ObjectValue(*obj)}); + + static_assert(Result<TriviallyCopyable_LowBitTagIsError, Failed>::Strategy == + PackingStrategy::LowBitTagIsError); + Rooted<Result<TriviallyCopyable_LowBitTagIsError, Failed>> lv( + cx, TriviallyCopyable_LowBitTagIsError{obj}); + + CHECK(obj == unrootedObj); + + CHECK(vv.get().inspect().obj == obj); + CHECK(ve.get().inspectErr().obj == obj); + CHECK(nv.get().inspect().val.toObjectOrNull() == obj); + CHECK(lv.get().inspect().obj == obj); + + JS_GC(cx); + CHECK(obj != unrootedObj); + + CHECK(vv.get().inspect().obj == obj); + CHECK(ve.get().inspectErr().obj == obj); + CHECK(nv.get().inspect().val.toObjectOrNull() == obj); + CHECK(lv.get().inspect().obj == obj); + CHECK(copies == 0); + return true; +} +END_TEST(testRootedResultCtors) + +#if defined(HAVE_64BIT_BUILD) && !defined(XP_WIN) + +// This depends on a pointer fitting in 48 bits, leaving space for an empty +// struct and a bool in a packed struct. Windows doesn't seem to do this +// packing, so we'll skip this test here. We're primarily checking whether +// copy constructors get called, which should be cross-platform, and +// secondarily making sure that the Rooted/tracing stuff is compiled and +// executed properly. There are certainly more clever ways to do this that +// would work cross-platform, but it doesn't seem worth the bother right now. + +struct __attribute__((packed)) DontCopyMe_PackedVariant { + uintptr_t obj : 48; + static JSObject* Unwrap(uintptr_t packed) { + return reinterpret_cast<JSObject*>(packed); + } + static uintptr_t Store(JSObject* obj) { + return reinterpret_cast<uintptr_t>(obj); + } + + DontCopyMe_PackedVariant() : obj(0) {} + explicit DontCopyMe_PackedVariant(JSObject* objArg) + : obj(reinterpret_cast<uintptr_t>(objArg)) {} + DontCopyMe_PackedVariant(const DontCopyMe_PackedVariant& other) + : obj(other.obj) { + copies++; + } + DontCopyMe_PackedVariant(DontCopyMe_PackedVariant&& other) : obj(other.obj) { + other.obj = 0; + } + void trace(JSTracer* trc) { + JSObject* realObj = Unwrap(obj); + TraceRoot(trc, &realObj, "obj"); + obj = Store(realObj); + } +}; + +static_assert(std::is_default_constructible_v<DontCopyMe_PackedVariant>); +static_assert(std::is_default_constructible_v<TestErr>); +static_assert(mozilla::detail::IsPackableVariant<DontCopyMe_PackedVariant, + TestErr>::value); + +BEGIN_TEST_WITH_ATTRIBUTES(testResultPackedVariant, JS_EXPECT_HAZARDS) { + JSObject* unrootedObj = JS_NewPlainObject(cx); + CHECK(unrootedObj); + Rooted<JSObject*> obj(cx, unrootedObj); + + using mozilla::detail::PackingStrategy; + + static_assert(Result<DontCopyMe_PackedVariant, TestErr>::Strategy == + PackingStrategy::PackedVariant); + Rooted<Result<DontCopyMe_PackedVariant, TestErr>> pv( + cx, DontCopyMe_PackedVariant{obj}); + static_assert(Result<mozilla::Ok, DontCopyMe_PackedVariant>::Strategy == + PackingStrategy::PackedVariant); + Rooted<Result<mozilla::Ok, DontCopyMe_PackedVariant>> pe( + cx, DontCopyMe_PackedVariant{obj}); + + CHECK(obj == unrootedObj); + + CHECK(DontCopyMe_PackedVariant::Unwrap(pv.get().inspect().obj) == obj); + CHECK(DontCopyMe_PackedVariant::Unwrap(pe.get().inspectErr().obj) == obj); + + JS_GC(cx); + CHECK(obj != unrootedObj); + + CHECK(DontCopyMe_PackedVariant::Unwrap(pv.get().inspect().obj) == obj); + CHECK(DontCopyMe_PackedVariant::Unwrap(pe.get().inspectErr().obj) == obj); + + return true; +} +END_TEST(testResultPackedVariant) + +#endif // HAVE_64BIT_BUILD diff --git a/js/src/jsapi-tests/testGCFinalizeCallback.cpp b/js/src/jsapi-tests/testGCFinalizeCallback.cpp new file mode 100644 index 0000000000..290356c421 --- /dev/null +++ b/js/src/jsapi-tests/testGCFinalizeCallback.cpp @@ -0,0 +1,202 @@ +/* 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 "js/GlobalObject.h" +#include "jsapi-tests/tests.h" + +using namespace js; + +static const unsigned BufSize = 20; +static unsigned FinalizeCalls = 0; +static JSFinalizeStatus StatusBuffer[BufSize]; + +BEGIN_TEST(testGCFinalizeCallback) { + AutoGCParameter param1(cx, JSGC_INCREMENTAL_GC_ENABLED, true); + AutoGCParameter param2(cx, JSGC_PER_ZONE_GC_ENABLED, true); + + /* Full GC, non-incremental. */ + FinalizeCalls = 0; + JS_GC(cx); + CHECK(cx->runtime()->gc.isFullGc()); + CHECK(checkSingleGroup()); + CHECK(checkFinalizeStatus()); + + /* Full GC, incremental. */ + FinalizeCalls = 0; + JS::PrepareForFullGC(cx); + SliceBudget startBudget(TimeBudget(1000000)); + JS::StartIncrementalGC(cx, JS::GCOptions::Normal, JS::GCReason::API, + startBudget); + while (cx->runtime()->gc.isIncrementalGCInProgress()) { + JS::PrepareForFullGC(cx); + SliceBudget budget(TimeBudget(1000000)); + JS::IncrementalGCSlice(cx, JS::GCReason::API, budget); + } + CHECK(!cx->runtime()->gc.isIncrementalGCInProgress()); + CHECK(cx->runtime()->gc.isFullGc()); + CHECK(checkMultipleGroups()); + CHECK(checkFinalizeStatus()); + +#ifdef JS_GC_ZEAL + // Bug 1377593 - the below tests want to control how many zones are GC'ing, + // and some zeal modes will convert them into all-zones GCs. + JS_SetGCZeal(cx, 0, 0); +#endif + + JS::RootedObject global1(cx, createTestGlobal()); + JS::RootedObject global2(cx, createTestGlobal()); + JS::RootedObject global3(cx, createTestGlobal()); + CHECK(global1); + CHECK(global2); + CHECK(global3); + + /* Zone GC, non-incremental, single zone. */ + FinalizeCalls = 0; + JS::PrepareZoneForGC(cx, global1->zone()); + JS::NonIncrementalGC(cx, JS::GCOptions::Normal, JS::GCReason::API); + CHECK(!cx->runtime()->gc.isFullGc()); + CHECK(checkSingleGroup()); + CHECK(checkFinalizeStatus()); + + /* Zone GC, non-incremental, multiple zones. */ + FinalizeCalls = 0; + JS::PrepareZoneForGC(cx, global1->zone()); + JS::PrepareZoneForGC(cx, global2->zone()); + JS::PrepareZoneForGC(cx, global3->zone()); + JS::NonIncrementalGC(cx, JS::GCOptions::Normal, JS::GCReason::API); + CHECK(!cx->runtime()->gc.isFullGc()); + CHECK(checkSingleGroup()); + CHECK(checkFinalizeStatus()); + + /* Zone GC, incremental, single zone. */ + FinalizeCalls = 0; + JS::PrepareZoneForGC(cx, global1->zone()); + SliceBudget budget(TimeBudget(1000000)); + JS::StartIncrementalGC(cx, JS::GCOptions::Normal, JS::GCReason::API, budget); + while (cx->runtime()->gc.isIncrementalGCInProgress()) { + JS::PrepareZoneForGC(cx, global1->zone()); + budget = SliceBudget(TimeBudget(1000000)); + JS::IncrementalGCSlice(cx, JS::GCReason::API, budget); + } + CHECK(!cx->runtime()->gc.isIncrementalGCInProgress()); + CHECK(!cx->runtime()->gc.isFullGc()); + CHECK(checkSingleGroup()); + CHECK(checkFinalizeStatus()); + + /* Zone GC, incremental, multiple zones. */ + FinalizeCalls = 0; + JS::PrepareZoneForGC(cx, global1->zone()); + JS::PrepareZoneForGC(cx, global2->zone()); + JS::PrepareZoneForGC(cx, global3->zone()); + budget = SliceBudget(TimeBudget(1000000)); + JS::StartIncrementalGC(cx, JS::GCOptions::Normal, JS::GCReason::API, budget); + while (cx->runtime()->gc.isIncrementalGCInProgress()) { + JS::PrepareZoneForGC(cx, global1->zone()); + JS::PrepareZoneForGC(cx, global2->zone()); + JS::PrepareZoneForGC(cx, global3->zone()); + budget = SliceBudget(TimeBudget(1000000)); + JS::IncrementalGCSlice(cx, JS::GCReason::API, budget); + } + CHECK(!cx->runtime()->gc.isIncrementalGCInProgress()); + CHECK(!cx->runtime()->gc.isFullGc()); + CHECK(checkMultipleGroups()); + CHECK(checkFinalizeStatus()); + +#ifdef JS_GC_ZEAL + + /* Full GC with reset due to new zone, becoming zone GC. */ + + FinalizeCalls = 0; + JS_SetGCZeal(cx, 9, 1000000); + JS::PrepareForFullGC(cx); + budget = SliceBudget(WorkBudget(1)); + cx->runtime()->gc.startDebugGC(JS::GCOptions::Normal, budget); + CHECK(cx->runtime()->gc.state() == gc::State::Mark); + CHECK(cx->runtime()->gc.isFullGc()); + + JS::RootedObject global4(cx, createTestGlobal()); + budget = SliceBudget(WorkBudget(1)); + cx->runtime()->gc.debugGCSlice(budget); + while (cx->runtime()->gc.isIncrementalGCInProgress()) { + cx->runtime()->gc.debugGCSlice(budget); + } + CHECK(!cx->runtime()->gc.isIncrementalGCInProgress()); + CHECK(checkSingleGroup()); + CHECK(checkFinalizeStatus()); + + JS_SetGCZeal(cx, 0, 0); + +#endif + + /* + * Make some use of the globals here to ensure the compiler doesn't optimize + * them away in release builds, causing the zones to be collected and + * the test to fail. + */ + CHECK(JS_IsGlobalObject(global1)); + CHECK(JS_IsGlobalObject(global2)); + CHECK(JS_IsGlobalObject(global3)); + + return true; +} + +JSObject* createTestGlobal() { + JS::RealmOptions options; + return JS_NewGlobalObject(cx, getGlobalClass(), nullptr, + JS::FireOnNewGlobalHook, options); +} + +virtual bool init() override { + if (!JSAPIRuntimeTest::init()) { + return false; + } + + JS_AddFinalizeCallback(cx, FinalizeCallback, nullptr); + return true; +} + +virtual void uninit() override { + JS_RemoveFinalizeCallback(cx, FinalizeCallback); + JSAPIRuntimeTest::uninit(); +} + +bool checkSingleGroup() { + CHECK(FinalizeCalls < BufSize); + CHECK(FinalizeCalls == 4); + return true; +} + +bool checkMultipleGroups() { + CHECK(FinalizeCalls < BufSize); + CHECK(FinalizeCalls % 3 == 1); + CHECK((FinalizeCalls - 1) / 3 > 1); + return true; +} + +bool checkFinalizeStatus() { + /* + * The finalize callback should be called twice for each sweep group + * finalized, with status JSFINALIZE_GROUP_START and JSFINALIZE_GROUP_END, + * and then once more with JSFINALIZE_COLLECTION_END. + */ + + for (unsigned i = 0; i < FinalizeCalls - 1; i += 3) { + CHECK(StatusBuffer[i] == JSFINALIZE_GROUP_PREPARE); + CHECK(StatusBuffer[i + 1] == JSFINALIZE_GROUP_START); + CHECK(StatusBuffer[i + 2] == JSFINALIZE_GROUP_END); + } + + CHECK(StatusBuffer[FinalizeCalls - 1] == JSFINALIZE_COLLECTION_END); + + return true; +} + +static void FinalizeCallback(JS::GCContext* gcx, JSFinalizeStatus status, + void* data) { + if (FinalizeCalls < BufSize) { + StatusBuffer[FinalizeCalls] = status; + } + ++FinalizeCalls; +} +END_TEST(testGCFinalizeCallback) diff --git a/js/src/jsapi-tests/testGCGrayMarking.cpp b/js/src/jsapi-tests/testGCGrayMarking.cpp new file mode 100644 index 0000000000..1ae1683a63 --- /dev/null +++ b/js/src/jsapi-tests/testGCGrayMarking.cpp @@ -0,0 +1,804 @@ +/* -*- 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 <algorithm> + +#include "gc/WeakMap.h" +#include "gc/Zone.h" +#include "js/PropertyAndElement.h" // JS_DefineProperty, JS_DefinePropertyById +#include "js/Proxy.h" +#include "js/WeakMap.h" +#include "jsapi-tests/tests.h" + +using namespace js; +using namespace js::gc; + +static constexpr CellColor AllCellColors[] = {CellColor::White, CellColor::Gray, + CellColor::Black}; + +static constexpr CellColor MarkedCellColors[] = {CellColor::Gray, + CellColor::Black}; + +namespace js { + +struct GCManagedObjectWeakMap : public ObjectWeakMap { + using ObjectWeakMap::ObjectWeakMap; +}; + +} // namespace js + +namespace JS { + +template <> +struct MapTypeToRootKind<js::GCManagedObjectWeakMap*> { + static const JS::RootKind kind = JS::RootKind::Traceable; +}; + +template <> +struct GCPolicy<js::GCManagedObjectWeakMap*> + : public NonGCPointerPolicy<js::GCManagedObjectWeakMap*> {}; + +} // namespace JS + +class AutoNoAnalysisForTest { + public: + AutoNoAnalysisForTest() {} +} JS_HAZ_GC_SUPPRESSED; + +BEGIN_TEST(testGCGrayMarking) { + AutoNoAnalysisForTest disableAnalysis; + AutoDisableCompactingGC disableCompactingGC(cx); + AutoLeaveZeal nozeal(cx); + + CHECK(InitGlobals()); + JSAutoRealm ar(cx, global1); + + InitGrayRootTracer(); + + // Enable incremental GC. + AutoGCParameter param1(cx, JSGC_INCREMENTAL_GC_ENABLED, true); + AutoGCParameter param2(cx, JSGC_PER_ZONE_GC_ENABLED, true); + + bool ok = TestMarking() && TestJSWeakMaps() && TestInternalWeakMaps() && + TestCCWs() && TestGrayUnmarking(); + + global1 = nullptr; + global2 = nullptr; + RemoveGrayRootTracer(); + + return ok; +} + +bool TestMarking() { + JSObject* sameTarget = AllocPlainObject(); + CHECK(sameTarget); + + JSObject* sameSource = AllocSameCompartmentSourceObject(sameTarget); + CHECK(sameSource); + + JSObject* crossTarget = AllocPlainObject(); + CHECK(crossTarget); + + JSObject* crossSource = GetCrossCompartmentWrapper(crossTarget); + CHECK(crossSource); + + // Test GC with black roots marks objects black. + + JS::RootedObject blackRoot1(cx, sameSource); + JS::RootedObject blackRoot2(cx, crossSource); + + JS_GC(cx); + + CHECK(IsMarkedBlack(sameSource)); + CHECK(IsMarkedBlack(crossSource)); + CHECK(IsMarkedBlack(sameTarget)); + CHECK(IsMarkedBlack(crossTarget)); + + // Test GC with black and gray roots marks objects black. + + grayRoots.grayRoot1 = sameSource; + grayRoots.grayRoot2 = crossSource; + + JS_GC(cx); + + CHECK(IsMarkedBlack(sameSource)); + CHECK(IsMarkedBlack(crossSource)); + CHECK(IsMarkedBlack(sameTarget)); + CHECK(IsMarkedBlack(crossTarget)); + + CHECK(!JS::ObjectIsMarkedGray(sameSource)); + + // Test GC with gray roots marks object gray. + + blackRoot1 = nullptr; + blackRoot2 = nullptr; + + JS_GC(cx); + + CHECK(IsMarkedGray(sameSource)); + CHECK(IsMarkedGray(crossSource)); + CHECK(IsMarkedGray(sameTarget)); + CHECK(IsMarkedGray(crossTarget)); + + CHECK(JS::ObjectIsMarkedGray(sameSource)); + + // Test ExposeToActiveJS marks gray objects black. + + JS::ExposeObjectToActiveJS(sameSource); + JS::ExposeObjectToActiveJS(crossSource); + CHECK(IsMarkedBlack(sameSource)); + CHECK(IsMarkedBlack(crossSource)); + CHECK(IsMarkedBlack(sameTarget)); + CHECK(IsMarkedBlack(crossTarget)); + + // Test a zone GC with black roots marks gray object in other zone black. + + JS_GC(cx); + + CHECK(IsMarkedGray(crossSource)); + CHECK(IsMarkedGray(crossTarget)); + + blackRoot1 = crossSource; + CHECK(ZoneGC(crossSource->zone())); + + CHECK(IsMarkedBlack(crossSource)); + CHECK(IsMarkedBlack(crossTarget)); + + blackRoot1 = nullptr; + blackRoot2 = nullptr; + grayRoots.grayRoot1 = nullptr; + grayRoots.grayRoot2 = nullptr; + + return true; +} + +static constexpr CellColor DontMark = CellColor::White; + +enum MarkKeyOrDelegate : bool { MarkKey = true, MarkDelegate = false }; + +bool TestJSWeakMaps() { + for (auto keyOrDelegateColor : MarkedCellColors) { + for (auto mapColor : MarkedCellColors) { + for (auto markKeyOrDelegate : {MarkKey, MarkDelegate}) { + CellColor expected = std::min(keyOrDelegateColor, mapColor); + CHECK(TestJSWeakMap(markKeyOrDelegate, keyOrDelegateColor, mapColor, + expected)); +#ifdef JS_GC_ZEAL + CHECK(TestJSWeakMapWithGrayUnmarking( + markKeyOrDelegate, keyOrDelegateColor, mapColor, expected)); +#endif + } + } + } + + return true; +} + +bool TestInternalWeakMaps() { + for (auto keyMarkColor : AllCellColors) { + for (auto delegateMarkColor : AllCellColors) { + if (keyMarkColor == CellColor::White && + delegateMarkColor == CellColor::White) { + continue; + } + + // The map is black. The delegate marks its key via wrapper preservation. + // The key maps its delegate and the value. Thus, all three end up the + // maximum of the key and delegate colors. + CellColor expected = std::max(keyMarkColor, delegateMarkColor); + CHECK(TestInternalWeakMap(keyMarkColor, delegateMarkColor, expected)); + +#ifdef JS_GC_ZEAL + CHECK(TestInternalWeakMapWithGrayUnmarking(keyMarkColor, + delegateMarkColor, expected)); +#endif + } + } + + return true; +} + +bool TestJSWeakMap(MarkKeyOrDelegate markKey, CellColor weakMapMarkColor, + CellColor keyOrDelegateMarkColor, + CellColor expectedValueColor) { + using std::swap; + + // Test marking a JS WeakMap object. + // + // This marks the map and one of the key or delegate. The key/delegate and the + // value can end up different colors depending on the color of the map. + + JSObject* weakMap; + JSObject* key; + JSObject* value; + + // If both map and key are marked the same color, test both possible + // orderings. + unsigned markOrderings = weakMapMarkColor == keyOrDelegateMarkColor ? 2 : 1; + + for (unsigned markOrder = 0; markOrder < markOrderings; markOrder++) { + CHECK(CreateJSWeakMapObjects(&weakMap, &key, &value)); + + JSObject* delegate = UncheckedUnwrapWithoutExpose(key); + JSObject* keyOrDelegate = markKey ? key : delegate; + + RootedObject blackRoot1(cx); + RootedObject blackRoot2(cx); + + RootObject(weakMap, weakMapMarkColor, blackRoot1, grayRoots.grayRoot1); + RootObject(keyOrDelegate, keyOrDelegateMarkColor, blackRoot2, + grayRoots.grayRoot2); + + if (markOrder != 0) { + swap(blackRoot1.get(), blackRoot2.get()); + swap(grayRoots.grayRoot1, grayRoots.grayRoot2); + } + + JS_GC(cx); + + ClearGrayRoots(); + + CHECK(weakMap->color() == weakMapMarkColor); + CHECK(keyOrDelegate->color() == keyOrDelegateMarkColor); + CHECK(value->color() == expectedValueColor); + } + + return true; +} + +#ifdef JS_GC_ZEAL + +bool TestJSWeakMapWithGrayUnmarking(MarkKeyOrDelegate markKey, + CellColor weakMapMarkColor, + CellColor keyOrDelegateMarkColor, + CellColor expectedValueColor) { + // This is like the previous test, but things are marked black by gray + // unmarking during incremental GC. + + JSObject* weakMap; + JSObject* key; + JSObject* value; + + // If both map and key are marked the same color, test both possible + // orderings. + unsigned markOrderings = weakMapMarkColor == keyOrDelegateMarkColor ? 2 : 1; + + JS_SetGCZeal(cx, uint8_t(ZealMode::YieldWhileGrayMarking), 0); + + for (unsigned markOrder = 0; markOrder < markOrderings; markOrder++) { + CHECK(CreateJSWeakMapObjects(&weakMap, &key, &value)); + + JSObject* delegate = UncheckedUnwrapWithoutExpose(key); + JSObject* keyOrDelegate = markKey ? key : delegate; + + grayRoots.grayRoot1 = keyOrDelegate; + grayRoots.grayRoot2 = weakMap; + + // Start an incremental GC and run until gray roots have been pushed onto + // the mark stack. + JS::PrepareForFullGC(cx); + js::SliceBudget budget(TimeBudget(1000000)); + JS::StartIncrementalGC(cx, JS::GCOptions::Normal, JS::GCReason::DEBUG_GC, + budget); + MOZ_ASSERT(cx->runtime()->gc.state() == gc::State::Sweep); + MOZ_ASSERT(cx->zone()->gcState() == Zone::MarkBlackAndGray); + + // Unmark gray things as specified. + if (markOrder != 0) { + MaybeExposeObject(weakMap, weakMapMarkColor); + MaybeExposeObject(keyOrDelegate, keyOrDelegateMarkColor); + } else { + MaybeExposeObject(keyOrDelegate, keyOrDelegateMarkColor); + MaybeExposeObject(weakMap, weakMapMarkColor); + } + + JS::FinishIncrementalGC(cx, JS::GCReason::API); + + ClearGrayRoots(); + + CHECK(weakMap->color() == weakMapMarkColor); + CHECK(keyOrDelegate->color() == keyOrDelegateMarkColor); + CHECK(value->color() == expectedValueColor); + } + + JS_UnsetGCZeal(cx, uint8_t(ZealMode::YieldWhileGrayMarking)); + + return true; +} + +static void MaybeExposeObject(JSObject* object, CellColor color) { + if (color == CellColor::Black) { + JS::ExposeObjectToActiveJS(object); + } +} + +#endif // JS_GC_ZEAL + +bool CreateJSWeakMapObjects(JSObject** weakMapOut, JSObject** keyOut, + JSObject** valueOut) { + RootedObject key(cx, AllocWeakmapKeyObject()); + CHECK(key); + + RootedObject value(cx, AllocPlainObject()); + CHECK(value); + + RootedObject weakMap(cx, JS::NewWeakMapObject(cx)); + CHECK(weakMap); + + JS::RootedValue keyValue(cx, ObjectValue(*key)); + JS::RootedValue valueValue(cx, ObjectValue(*value)); + CHECK(SetWeakMapEntry(cx, weakMap, keyValue, valueValue)); + + *weakMapOut = weakMap; + *keyOut = key; + *valueOut = value; + return true; +} + +bool TestInternalWeakMap(CellColor keyMarkColor, CellColor delegateMarkColor, + CellColor expectedColor) { + using std::swap; + + // Test marking for internal weakmaps (without an owning JSObject). + // + // All of the key, delegate and value are expected to end up the same color. + + UniquePtr<GCManagedObjectWeakMap> weakMap; + JSObject* key; + JSObject* value; + + // If both key and delegate are marked the same color, test both possible + // orderings. + unsigned markOrderings = keyMarkColor == delegateMarkColor ? 2 : 1; + + for (unsigned markOrder = 0; markOrder < markOrderings; markOrder++) { + CHECK(CreateInternalWeakMapObjects(&weakMap, &key, &value)); + + JSObject* delegate = UncheckedUnwrapWithoutExpose(key); + + RootedObject blackRoot1(cx); + RootedObject blackRoot2(cx); + + Rooted<GCManagedObjectWeakMap*> rootMap(cx, weakMap.get()); + RootObject(key, keyMarkColor, blackRoot1, grayRoots.grayRoot1); + RootObject(delegate, delegateMarkColor, blackRoot2, grayRoots.grayRoot2); + + if (markOrder != 0) { + swap(blackRoot1.get(), blackRoot2.get()); + swap(grayRoots.grayRoot1, grayRoots.grayRoot2); + } + + JS_GC(cx); + + ClearGrayRoots(); + + CHECK(key->color() == expectedColor); + CHECK(delegate->color() == expectedColor); + CHECK(value->color() == expectedColor); + } + + return true; +} + +#ifdef JS_GC_ZEAL + +bool TestInternalWeakMapWithGrayUnmarking(CellColor keyMarkColor, + CellColor delegateMarkColor, + CellColor expectedColor) { + UniquePtr<GCManagedObjectWeakMap> weakMap; + JSObject* key; + JSObject* value; + + // If both key and delegate are marked the same color, test both possible + // orderings. + unsigned markOrderings = keyMarkColor == delegateMarkColor ? 2 : 1; + + JS_SetGCZeal(cx, uint8_t(ZealMode::YieldWhileGrayMarking), 0); + + for (unsigned markOrder = 0; markOrder < markOrderings; markOrder++) { + CHECK(CreateInternalWeakMapObjects(&weakMap, &key, &value)); + + JSObject* delegate = UncheckedUnwrapWithoutExpose(key); + + Rooted<GCManagedObjectWeakMap*> rootMap(cx, weakMap.get()); + grayRoots.grayRoot1 = key; + grayRoots.grayRoot2 = delegate; + + // Start an incremental GC and run until gray roots have been pushed onto + // the mark stack. + JS::PrepareForFullGC(cx); + js::SliceBudget budget(TimeBudget(1000000)); + JS::StartIncrementalGC(cx, JS::GCOptions::Normal, JS::GCReason::DEBUG_GC, + budget); + MOZ_ASSERT(cx->runtime()->gc.state() == gc::State::Sweep); + MOZ_ASSERT(cx->zone()->gcState() == Zone::MarkBlackAndGray); + + // Unmark gray things as specified. + if (markOrder != 0) { + MaybeExposeObject(key, keyMarkColor); + MaybeExposeObject(delegate, delegateMarkColor); + } else { + MaybeExposeObject(key, keyMarkColor); + MaybeExposeObject(delegate, delegateMarkColor); + } + + JS::FinishIncrementalGC(cx, JS::GCReason::API); + + ClearGrayRoots(); + + CHECK(key->color() == expectedColor); + CHECK(delegate->color() == expectedColor); + CHECK(value->color() == expectedColor); + } + + JS_UnsetGCZeal(cx, uint8_t(ZealMode::YieldWhileGrayMarking)); + + return true; +} + +#endif // JS_GC_ZEAL + +bool CreateInternalWeakMapObjects(UniquePtr<GCManagedObjectWeakMap>* weakMapOut, + JSObject** keyOut, JSObject** valueOut) { + RootedObject key(cx, AllocWeakmapKeyObject()); + CHECK(key); + + RootedObject value(cx, AllocPlainObject()); + CHECK(value); + + auto weakMap = cx->make_unique<GCManagedObjectWeakMap>(cx); + CHECK(weakMap); + + CHECK(weakMap->add(cx, key, value)); + + *weakMapOut = std::move(weakMap); + *keyOut = key; + *valueOut = value; + return true; +} + +void RootObject(JSObject* object, CellColor color, RootedObject& blackRoot, + JS::Heap<JSObject*>& grayRoot) { + if (color == CellColor::Black) { + blackRoot = object; + } else if (color == CellColor::Gray) { + grayRoot = object; + } else { + MOZ_RELEASE_ASSERT(color == CellColor::White); + } +} + +bool TestCCWs() { + JSObject* target = AllocPlainObject(); + CHECK(target); + + // Test getting a new wrapper doesn't return a gray wrapper. + + RootedObject blackRoot(cx, target); + JSObject* wrapper = GetCrossCompartmentWrapper(target); + CHECK(wrapper); + CHECK(!IsMarkedGray(wrapper)); + + // Test getting an existing wrapper doesn't return a gray wrapper. + + grayRoots.grayRoot1 = wrapper; + grayRoots.grayRoot2 = nullptr; + JS_GC(cx); + CHECK(IsMarkedGray(wrapper)); + CHECK(IsMarkedBlack(target)); + + CHECK(GetCrossCompartmentWrapper(target) == wrapper); + CHECK(!IsMarkedGray(wrapper)); + + // Test getting an existing wrapper doesn't return a gray wrapper + // during incremental GC. + + JS_GC(cx); + CHECK(IsMarkedGray(wrapper)); + CHECK(IsMarkedBlack(target)); + + JSRuntime* rt = cx->runtime(); + JS::PrepareForFullGC(cx); + js::SliceBudget budget(js::WorkBudget(1)); + rt->gc.startDebugGC(JS::GCOptions::Normal, budget); + while (rt->gc.state() == gc::State::Prepare) { + rt->gc.debugGCSlice(budget); + } + CHECK(JS::IsIncrementalGCInProgress(cx)); + + CHECK(!IsMarkedBlack(wrapper)); + CHECK(wrapper->zone()->isGCMarkingBlackOnly()); + + CHECK(GetCrossCompartmentWrapper(target) == wrapper); + CHECK(IsMarkedBlack(wrapper)); + + JS::FinishIncrementalGC(cx, JS::GCReason::API); + + // Test behaviour of gray CCWs marked black by a barrier during incremental + // GC. + + // Initial state: source and target are gray. + blackRoot = nullptr; + grayRoots.grayRoot1 = wrapper; + grayRoots.grayRoot2 = nullptr; + JS_GC(cx); + CHECK(IsMarkedGray(wrapper)); + CHECK(IsMarkedGray(target)); + + // Incremental zone GC started: the source is now unmarked. + JS::PrepareZoneForGC(cx, wrapper->zone()); + budget = js::SliceBudget(js::WorkBudget(1)); + rt->gc.startDebugGC(JS::GCOptions::Normal, budget); + while (rt->gc.state() == gc::State::Prepare) { + rt->gc.debugGCSlice(budget); + } + CHECK(JS::IsIncrementalGCInProgress(cx)); + CHECK(wrapper->zone()->isGCMarkingBlackOnly()); + CHECK(!target->zone()->wasGCStarted()); + CHECK(!IsMarkedBlack(wrapper)); + CHECK(!IsMarkedGray(wrapper)); + CHECK(IsMarkedGray(target)); + + // Betweeen GC slices: source marked black by barrier, target is + // still gray. Target will be marked gray + // eventually. ObjectIsMarkedGray() is conservative and reports + // that target is not marked gray; AssertObjectIsNotGray() will + // assert. + grayRoots.grayRoot1.get(); + CHECK(IsMarkedBlack(wrapper)); + CHECK(IsMarkedGray(target)); + CHECK(!JS::ObjectIsMarkedGray(target)); + + // Final state: source and target are black. + JS::FinishIncrementalGC(cx, JS::GCReason::API); + CHECK(IsMarkedBlack(wrapper)); + CHECK(IsMarkedBlack(target)); + + grayRoots.grayRoot1 = nullptr; + grayRoots.grayRoot2 = nullptr; + + return true; +} + +bool TestGrayUnmarking() { + const size_t length = 2000; + + JSObject* chain = AllocObjectChain(length); + CHECK(chain); + + RootedObject blackRoot(cx, chain); + JS_GC(cx); + size_t count; + CHECK(IterateObjectChain(chain, ColorCheckFunctor(MarkColor::Black, &count))); + CHECK(count == length); + + blackRoot = nullptr; + grayRoots.grayRoot1 = chain; + JS_GC(cx); + CHECK(cx->runtime()->gc.areGrayBitsValid()); + CHECK(IterateObjectChain(chain, ColorCheckFunctor(MarkColor::Gray, &count))); + CHECK(count == length); + + JS::ExposeObjectToActiveJS(chain); + CHECK(cx->runtime()->gc.areGrayBitsValid()); + CHECK(IterateObjectChain(chain, ColorCheckFunctor(MarkColor::Black, &count))); + CHECK(count == length); + + grayRoots.grayRoot1 = nullptr; + + return true; +} + +struct ColorCheckFunctor { + MarkColor color; + size_t& count; + + ColorCheckFunctor(MarkColor colorArg, size_t* countArg) + : color(colorArg), count(*countArg) { + count = 0; + } + + bool operator()(JSObject* obj) { + if (!CheckCellColor(obj, color)) { + return false; + } + + NativeObject& nobj = obj->as<NativeObject>(); + if (!CheckCellColor(nobj.shape(), color)) { + return false; + } + + NativeShape* shape = nobj.shape(); + if (!CheckCellColor(shape, color)) { + return false; + } + + // Shapes and symbols are never marked gray. + ShapePropertyIter<NoGC> iter(shape); + jsid id = iter->key(); + if (id.isGCThing() && + !CheckCellColor(id.toGCCellPtr().asCell(), MarkColor::Black)) { + return false; + } + + count++; + return true; + } +}; + +JS::PersistentRootedObject global1; +JS::PersistentRootedObject global2; + +struct GrayRoots { + JS::Heap<JSObject*> grayRoot1; + JS::Heap<JSObject*> grayRoot2; +}; + +GrayRoots grayRoots; + +bool InitGlobals() { + global1.init(cx, global); + if (!createGlobal()) { + return false; + } + global2.init(cx, global); + return global2 != nullptr; +} + +void ClearGrayRoots() { + grayRoots.grayRoot1 = nullptr; + grayRoots.grayRoot2 = nullptr; +} + +void InitGrayRootTracer() { + ClearGrayRoots(); + JS_SetGrayGCRootsTracer(cx, TraceGrayRoots, &grayRoots); +} + +void RemoveGrayRootTracer() { + ClearGrayRoots(); + JS_SetGrayGCRootsTracer(cx, nullptr, nullptr); +} + +static bool TraceGrayRoots(JSTracer* trc, SliceBudget& budget, void* data) { + auto grayRoots = static_cast<GrayRoots*>(data); + TraceEdge(trc, &grayRoots->grayRoot1, "gray root 1"); + TraceEdge(trc, &grayRoots->grayRoot2, "gray root 2"); + return true; +} + +JSObject* AllocPlainObject() { + JS::RootedObject obj(cx, JS_NewPlainObject(cx)); + EvictNursery(); + + MOZ_ASSERT(obj->compartment() == global1->compartment()); + return obj; +} + +JSObject* AllocSameCompartmentSourceObject(JSObject* target) { + JS::RootedObject source(cx, JS_NewPlainObject(cx)); + if (!source) { + return nullptr; + } + + JS::RootedObject obj(cx, target); + if (!JS_DefineProperty(cx, source, "ptr", obj, 0)) { + return nullptr; + } + + EvictNursery(); + + MOZ_ASSERT(source->compartment() == global1->compartment()); + return source; +} + +JSObject* GetCrossCompartmentWrapper(JSObject* target) { + MOZ_ASSERT(target->compartment() == global1->compartment()); + JS::RootedObject obj(cx, target); + JSAutoRealm ar(cx, global2); + if (!JS_WrapObject(cx, &obj)) { + return nullptr; + } + + EvictNursery(); + + MOZ_ASSERT(obj->compartment() == global2->compartment()); + return obj; +} + +JSObject* AllocWeakmapKeyObject() { + JS::RootedObject delegate(cx, JS_NewPlainObject(cx)); + if (!delegate) { + return nullptr; + } + + JS::RootedObject key(cx, + js::Wrapper::New(cx, delegate, &js::Wrapper::singleton)); + + EvictNursery(); + return key; +} + +JSObject* AllocObjectChain(size_t length) { + // Allocate a chain of linked JSObjects. + + // Use a unique property name so the shape is not shared with any other + // objects. + RootedString nextPropName(cx, JS_NewStringCopyZ(cx, "unique14142135")); + RootedId nextId(cx); + if (!JS_StringToId(cx, nextPropName, &nextId)) { + return nullptr; + } + + RootedObject head(cx); + for (size_t i = 0; i < length; i++) { + RootedValue next(cx, ObjectOrNullValue(head)); + head = AllocPlainObject(); + if (!head) { + return nullptr; + } + if (!JS_DefinePropertyById(cx, head, nextId, next, 0)) { + return nullptr; + } + } + + return head; +} + +template <typename F> +bool IterateObjectChain(JSObject* chain, F f) { + RootedObject obj(cx, chain); + while (obj) { + if (!f(obj)) { + return false; + } + + // Access the 'next' property via the object's slots to avoid triggering + // gray marking assertions when calling JS_GetPropertyById. + NativeObject& nobj = obj->as<NativeObject>(); + MOZ_ASSERT(nobj.slotSpan() == 1); + obj = nobj.getSlot(0).toObjectOrNull(); + } + + return true; +} + +static bool IsMarkedBlack(Cell* cell) { + TenuredCell* tc = &cell->asTenured(); + return tc->isMarkedBlack(); +} + +static bool IsMarkedGray(Cell* cell) { + TenuredCell* tc = &cell->asTenured(); + bool isGray = tc->isMarkedGray(); + MOZ_ASSERT_IF(isGray, tc->isMarkedAny()); + return isGray; +} + +static bool CheckCellColor(Cell* cell, MarkColor color) { + MOZ_ASSERT(color == MarkColor::Black || color == MarkColor::Gray); + if (color == MarkColor::Black && !IsMarkedBlack(cell)) { + printf("Found non-black cell: %p\n", cell); + return false; + } else if (color == MarkColor::Gray && !IsMarkedGray(cell)) { + printf("Found non-gray cell: %p\n", cell); + return false; + } + + return true; +} + +void EvictNursery() { cx->runtime()->gc.evictNursery(); } + +bool ZoneGC(JS::Zone* zone) { + JS::PrepareZoneForGC(cx, zone); + cx->runtime()->gc.gc(JS::GCOptions::Normal, JS::GCReason::API); + CHECK(!cx->runtime()->gc.isFullGc()); + return true; +} + +END_TEST(testGCGrayMarking) diff --git a/js/src/jsapi-tests/testGCHeapBarriers.cpp b/js/src/jsapi-tests/testGCHeapBarriers.cpp new file mode 100644 index 0000000000..9493049e16 --- /dev/null +++ b/js/src/jsapi-tests/testGCHeapBarriers.cpp @@ -0,0 +1,828 @@ +/* -*- 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 "mozilla/Maybe.h" +#include "mozilla/UniquePtr.h" + +#include "gc/AllocKind.h" +#include "gc/Cell.h" +#include "gc/GCInternals.h" +#include "gc/GCRuntime.h" +#include "js/ArrayBuffer.h" // JS::NewArrayBuffer +#include "js/experimental/TypedData.h" +#include "js/PropertyAndElement.h" // JS_DefineProperty, JS_GetProperty +#include "js/RootingAPI.h" +#include "jsapi-tests/tests.h" +#include "vm/Runtime.h" +#include "vm/TypedArrayObject.h" + +#include "vm/JSContext-inl.h" + +using namespace js; + +static js::gc::CellColor GetColor(JSObject* obj) { return obj->color(); } +static js::gc::CellColor GetColor(const JS::ArrayBufferOrView& view) { + return view.asObjectUnbarriered()->color(); +} + +static MOZ_MAYBE_UNUSED bool IsInsideNursery(gc::Cell* cell) { + return !cell->isTenured(); +} +static MOZ_MAYBE_UNUSED bool IsInsideNursery( + const JS::ArrayBufferOrView& view) { + return IsInsideNursery(view.asObjectUnbarriered()); +} + +// A heap-allocated structure containing one of our barriered pointer wrappers +// to test. +template <typename W, typename T> +struct TestStruct { + W wrapper; + + void trace(JSTracer* trc) { + TraceNullableEdge(trc, &wrapper, "TestStruct::wrapper"); + } + + TestStruct() {} + explicit TestStruct(T init) : wrapper(init) {} +}; + +template <typename T> +static T CreateNurseryGCThing(JSContext* cx) = delete; + +template <> +JSObject* CreateNurseryGCThing(JSContext* cx) { + JS::RootedObject obj(cx, JS_NewPlainObject(cx)); + if (!obj) { + return nullptr; + } + JS_DefineProperty(cx, obj, "x", 42, 0); + MOZ_ASSERT(IsInsideNursery(obj)); + return obj; +} + +template <> +JSFunction* CreateNurseryGCThing(JSContext* cx) { + /* + * We don't actually use the function as a function, so here we cheat and + * cast a JSObject. + */ + return static_cast<JSFunction*>(CreateNurseryGCThing<JSObject*>(cx)); +} + +template <> +JS::Uint8Array CreateNurseryGCThing(JSContext* cx) { + JS::Rooted<JS::Uint8Array> arr(cx, JS::Uint8Array::create(cx, 100)); + JS::RootedObject obj(cx, arr.asObject()); + JS_DefineProperty(cx, obj, "x", 42, 0); + MOZ_ASSERT(IsInsideNursery(obj)); + return arr; +} + +template <typename T> +static T CreateTenuredGCThing(JSContext* cx) = delete; + +template <> +JSObject* CreateTenuredGCThing(JSContext* cx) { + // Use ArrayBuffers because they have finalizers, which allows using them in + // TenuredHeap<> without awkward conversations about nursery allocatability. + // Note that at some point ArrayBuffers might become nursery allocated at + // which point this test will have to change. + JSObject* obj = JS::NewArrayBuffer(cx, 20); + MOZ_ASSERT(!IsInsideNursery(obj)); + MOZ_ASSERT(obj->getClass()->hasFinalize() && + !(obj->getClass()->flags & JSCLASS_SKIP_NURSERY_FINALIZE)); + return obj; +} + +template <> +JS::ArrayBuffer CreateTenuredGCThing(JSContext* cx) { + return JS::ArrayBuffer::fromObject(CreateTenuredGCThing<JSObject*>(cx)); +} + +template <> +JS::Uint8Array CreateTenuredGCThing(JSContext* cx) { + // Use internal APIs that lets us specify the InitialHeap so we can ensure + // that this is tenured. + JSObject* obj = js::NewUint8ArrayWithLength(cx, 100, gc::Heap::Tenured); + MOZ_ASSERT(!IsInsideNursery(obj)); + return JS::Uint8Array::fromObject(obj); +} + +template <typename T> +void* CreateHiddenTenuredGCThing(JSContext* cx) { + return CreateTenuredGCThing<T>(cx); +} + +template <> +void* CreateHiddenTenuredGCThing<JS::ArrayBuffer>(JSContext* cx) { + return CreateTenuredGCThing<JS::ArrayBuffer>(cx).asObjectUnbarriered(); +} + +template <> +void* CreateHiddenTenuredGCThing<JS::Uint8Array>(JSContext* cx) { + return CreateTenuredGCThing<JS::Uint8Array>(cx).asObjectUnbarriered(); +} + +static uintptr_t UnbarrieredCastToInt(gc::Cell* cell) { + return reinterpret_cast<uintptr_t>(cell); +} +static uintptr_t UnbarrieredCastToInt(const JS::ArrayBufferOrView& view) { + return UnbarrieredCastToInt(view.asObjectUnbarriered()); +} + +template <typename T> +T RecoverHiddenGCThing(void* ptr) { + return reinterpret_cast<T>(ptr); +} + +template <> +JS::ArrayBuffer RecoverHiddenGCThing(void* ptr) { + return JS::ArrayBuffer::fromObject(RecoverHiddenGCThing<JSObject*>(ptr)); +} + +template <> +JS::Uint8Array RecoverHiddenGCThing(void* ptr) { + return JS::Uint8Array::fromObject(RecoverHiddenGCThing<JSObject*>(ptr)); +} + +static void MakeGray(JSObject* obj) { + gc::TenuredCell* cell = &obj->asTenured(); + cell->unmark(); + cell->markIfUnmarked(gc::MarkColor::Gray); + MOZ_ASSERT(obj->isMarkedGray()); +} + +static void MakeGray(const JS::ArrayBufferOrView& view) { + MakeGray(view.asObjectUnbarriered()); +} + +// Test post-barrier implementation on wrapper types. The following wrapper +// types have post barriers: +// - JS::Heap +// - GCPtr +// - HeapPtr +// - WeakHeapPtr +BEGIN_TEST(testGCHeapPostBarriers) { + AutoLeaveZeal nozeal(cx); + + /* Sanity check - objects start in the nursery and then become tenured. */ + JS_GC(cx); + JS::RootedObject obj(cx, CreateNurseryGCThing<JSObject*>(cx)); + CHECK(IsInsideNursery(obj.get())); + JS_GC(cx); + CHECK(!IsInsideNursery(obj.get())); + JS::RootedObject tenuredObject(cx, obj); + + /* JSObject and JSFunction objects are nursery allocated. */ + CHECK(TestHeapPostBarriersForType<JSObject*>()); + CHECK(TestHeapPostBarriersForType<JSFunction*>()); + CHECK(TestHeapPostBarriersForType<JS::Uint8Array>()); + // Bug 1599378: Add string tests. + + return true; +} + +bool CanAccessObject(JSObject* obj) { + JS::RootedObject rootedObj(cx, obj); + JS::RootedValue value(cx); + CHECK(JS_GetProperty(cx, rootedObj, "x", &value)); + CHECK(value.isInt32()); + CHECK(value.toInt32() == 42); + return true; +} +bool CanAccessObject(const JS::ArrayBufferOrView& view) { + return CanAccessObject(view.asObject()); +} + +template <typename T> +bool TestHeapPostBarriersForType() { + CHECK((TestHeapPostBarriersForWrapper<js::GCPtr, T>())); + CHECK((TestHeapPostBarriersForMovableWrapper<JS::Heap, T>())); + CHECK((TestHeapPostBarriersForMovableWrapper<js::HeapPtr, T>())); + CHECK((TestHeapPostBarriersForMovableWrapper<js::WeakHeapPtr, T>())); + return true; +} + +template <template <typename> class W, typename T> +bool TestHeapPostBarriersForMovableWrapper() { + CHECK((TestHeapPostBarriersForWrapper<W, T>())); + CHECK((TestHeapPostBarrierMoveConstruction<W<T>, T>())); + CHECK((TestHeapPostBarrierMoveAssignment<W<T>, T>())); + return true; +} + +template <template <typename> class W, typename T> +bool TestHeapPostBarriersForWrapper() { + CHECK((TestHeapPostBarrierConstruction<W<T>, T>())); + CHECK((TestHeapPostBarrierConstruction<const W<T>, T>())); + CHECK((TestHeapPostBarrierUpdate<W<T>, T>())); + if constexpr (!std::is_same_v<W<T>, GCPtr<T>>) { + // It is not allowed to delete heap memory containing GCPtrs on + // initialization failure like this and doing so will cause an assertion to + // fail in the GCPtr destructor (although we disable this in some places in + // this test). + CHECK((TestHeapPostBarrierInitFailure<W<T>, T>())); + CHECK((TestHeapPostBarrierInitFailure<const W<T>, T>())); + } + return true; +} + +template <typename W, typename T> +bool TestHeapPostBarrierConstruction() { + T initialObj = CreateNurseryGCThing<T>(cx); + CHECK(initialObj); + CHECK(IsInsideNursery(initialObj)); + uintptr_t initialObjAsInt = UnbarrieredCastToInt(initialObj); + + { + // We don't root our structure because that would end up tracing it on minor + // GC and we're testing that heap post barrier works for things that aren't + // roots. + JS::AutoSuppressGCAnalysis noAnalysis(cx); + + auto* testStruct = js_new<TestStruct<W, T>>(initialObj); + CHECK(testStruct); + + auto& wrapper = testStruct->wrapper; + CHECK(wrapper == initialObj); + + cx->minorGC(JS::GCReason::API); + + CHECK(UnbarrieredCastToInt(wrapper.get()) != initialObjAsInt); + CHECK(!IsInsideNursery(wrapper.get())); + CHECK(CanAccessObject(wrapper.get())); + + // Disable the check that GCPtrs are only destroyed by the GC. What happens + // on destruction isn't relevant to the test. + gc::AutoSetThreadIsFinalizing threadIsFinalizing; + + js_delete(testStruct); + } + + cx->minorGC(JS::GCReason::API); + + return true; +} + +template <typename W, typename T> +bool TestHeapPostBarrierUpdate() { + // Normal case - allocate a heap object, write a nursery pointer into it and + // check that it gets updated on minor GC. + + T initialObj = CreateNurseryGCThing<T>(cx); + CHECK(initialObj); + CHECK(IsInsideNursery(initialObj)); + uintptr_t initialObjAsInt = UnbarrieredCastToInt(initialObj); + + { + // We don't root our structure because that would end up tracing it on minor + // GC and we're testing that heap post barrier works for things that aren't + // roots. + JS::AutoSuppressGCAnalysis noAnalysis(cx); + + auto* testStruct = js_new<TestStruct<W, T>>(); + CHECK(testStruct); + + auto& wrapper = testStruct->wrapper; + CHECK(!wrapper.get()); + + wrapper = initialObj; + CHECK(wrapper == initialObj); + + cx->minorGC(JS::GCReason::API); + + CHECK(UnbarrieredCastToInt(wrapper.get()) != initialObjAsInt); + CHECK(!IsInsideNursery(wrapper.get())); + CHECK(CanAccessObject(wrapper.get())); + + // Disable the check that GCPtrs are only destroyed by the GC. What happens + // on destruction isn't relevant to the test. + gc::AutoSetThreadIsFinalizing threadIsFinalizing; + + js_delete(testStruct); + } + + cx->minorGC(JS::GCReason::API); + + return true; +} + +template <typename W, typename T> +bool TestHeapPostBarrierInitFailure() { + // Failure case - allocate a heap object, write a nursery pointer into it + // and fail to complete initialization. + + T initialObj = CreateNurseryGCThing<T>(cx); + CHECK(initialObj); + CHECK(IsInsideNursery(initialObj)); + + { + // We don't root our structure because that would end up tracing it on minor + // GC and we're testing that heap post barrier works for things that aren't + // roots. + JS::AutoSuppressGCAnalysis noAnalysis(cx); + + auto testStruct = cx->make_unique<TestStruct<W, T>>(initialObj); + CHECK(testStruct); + + auto& wrapper = testStruct->wrapper; + CHECK(wrapper == initialObj); + + // testStruct deleted here, as if we left this block due to an error. + } + + cx->minorGC(JS::GCReason::API); + + return true; +} + +template <typename W, typename T> +bool TestHeapPostBarrierMoveConstruction() { + T initialObj = CreateNurseryGCThing<T>(cx); + CHECK(initialObj); + CHECK(IsInsideNursery(initialObj)); + uintptr_t initialObjAsInt = UnbarrieredCastToInt(initialObj); + + { + // We don't root our structure because that would end up tracing it on minor + // GC and we're testing that heap post barrier works for things that aren't + // roots. + JS::AutoSuppressGCAnalysis noAnalysis(cx); + + W wrapper1(initialObj); + CHECK(wrapper1 == initialObj); + + W wrapper2(std::move(wrapper1)); + CHECK(wrapper2 == initialObj); + + cx->minorGC(JS::GCReason::API); + + CHECK(UnbarrieredCastToInt(wrapper1.get()) != initialObjAsInt); + CHECK(UnbarrieredCastToInt(wrapper2.get()) != initialObjAsInt); + CHECK(!IsInsideNursery(wrapper2.get())); + CHECK(CanAccessObject(wrapper2.get())); + } + + cx->minorGC(JS::GCReason::API); + + return true; +} + +template <typename W, typename T> +bool TestHeapPostBarrierMoveAssignment() { + T initialObj = CreateNurseryGCThing<T>(cx); + CHECK(initialObj); + CHECK(IsInsideNursery(initialObj)); + uintptr_t initialObjAsInt = UnbarrieredCastToInt(initialObj); + + { + // We don't root our structure because that would end up tracing it on minor + // GC and we're testing that heap post barrier works for things that aren't + // roots. + JS::AutoSuppressGCAnalysis noAnalysis(cx); + + W wrapper1(initialObj); + CHECK(wrapper1 == initialObj); + + W wrapper2; + wrapper2 = std::move(wrapper1); + CHECK(wrapper2 == initialObj); + + cx->minorGC(JS::GCReason::API); + + CHECK(UnbarrieredCastToInt(wrapper1.get()) != initialObjAsInt); + CHECK(UnbarrieredCastToInt(wrapper2.get()) != initialObjAsInt); + CHECK(!IsInsideNursery(wrapper2.get())); + CHECK(CanAccessObject(wrapper2.get())); + } + + cx->minorGC(JS::GCReason::API); + + return true; +} + +END_TEST(testGCHeapPostBarriers) + +// Test read barrier implementation on wrapper types. The following wrapper +// types have read barriers: +// - JS::Heap +// - JS::TenuredHeap +// - WeakHeapPtr +// +// Also check that equality comparisons on wrappers do not trigger the read +// barrier. +BEGIN_TEST(testGCHeapReadBarriers) { + AutoLeaveZeal nozeal(cx); + + CHECK((TestWrapperType<JS::Heap<JSObject*>, JSObject*>())); + CHECK((TestWrapperType<JS::TenuredHeap<JSObject*>, JSObject*>())); + CHECK((TestWrapperType<WeakHeapPtr<JSObject*>, JSObject*>())); + CHECK((TestWrapperType<JS::Heap<JS::ArrayBuffer>, JS::ArrayBuffer>())); + CHECK((TestWrapperType<JS::Heap<JS::Uint8Array>, JS::Uint8Array>())); + + // JS::Heap has an additional barrier on its move and copy constructors. + CHECK((TestConstructorBarrier<JS::Heap<JSObject*>, JSObject*>())); + CHECK((TestConstructorBarrier<JS::Heap<JS::ArrayBuffer>, JS::ArrayBuffer>())); + CHECK((TestConstructorBarrier<JS::Heap<JS::Uint8Array>, JS::Uint8Array>())); + + return true; +} + +template <typename WrapperT, typename ObjectT> +bool TestWrapperType() { + // Check that the read barrier normally marks gray things black. + CHECK((TestReadBarrierUnmarksGray<WrapperT, ObjectT>())); + + // Check that the read barrier marks gray and white things black during an + // incremental GC. + CHECK((TestReadBarrierMarksBlack<WrapperT, ObjectT>(true))); + CHECK((TestReadBarrierMarksBlack<WrapperT, ObjectT>(false))); + + // Allocate test objects and make them gray. We will make sure they stay + // gray. (For most reads, the barrier will unmark gray.) + Rooted<ObjectT> obj1(cx, CreateTenuredGCThing<ObjectT>(cx)); + Rooted<ObjectT> obj2(cx, CreateTenuredGCThing<ObjectT>(cx)); + MakeGray(obj1); + MakeGray(obj2); + + WrapperT wrapper1(obj1); + WrapperT wrapper2(obj2); + const ObjectT constobj1 = obj1; + const ObjectT constobj2 = obj2; + CHECK((TestUnbarrieredOperations<WrapperT, ObjectT>(obj1, obj2, wrapper1, + wrapper2))); + CHECK((TestUnbarrieredOperations<WrapperT, ObjectT>(constobj1, constobj2, + wrapper1, wrapper2))); + + return true; +} + +template <typename WrapperT, typename ObjectT> +void Access(const WrapperT& wrapper) { + if constexpr (std::is_base_of_v<JS::ArrayBufferOrView, ObjectT>) { + (void)wrapper.asObject(); + } else { + (void)*wrapper; + } +} + +template <typename WrapperT, typename ObjectT> +bool TestReadBarrierUnmarksGray() { + Rooted<ObjectT> obj(cx, CreateTenuredGCThing<ObjectT>(cx)); + WrapperT wrapper(obj); + + CHECK(GetColor(obj) == gc::CellColor::White); + + Access<WrapperT, ObjectT>(wrapper); + + CHECK(GetColor(obj) == gc::CellColor::White); + + MakeGray(obj); + Access<WrapperT, ObjectT>(wrapper); + + CHECK(GetColor(obj) == gc::CellColor::Black); + + return true; +} + +// Execute thunk |f| between two slices of an incremental GC controlled by zeal +// mode |mode|. +template <typename F> +bool CallDuringIncrementalGC(uint32_t mode, F&& f) { +#ifndef JS_GC_ZEAL + fprintf(stderr, "This test requires building with --enable-gczeal\n"); +#else + AutoGCParameter incremental(cx, JSGC_INCREMENTAL_GC_ENABLED, true); + + const int64_t BudgetMS = 10000; // 10S should be long enough for anyone. + + JS_SetGCZeal(cx, mode, 0); + JS::PrepareZoneForGC(cx, js::GetContextZone(cx)); + js::SliceBudget budget{TimeBudget(BudgetMS)}; + JS::StartIncrementalGC(cx, JS::GCOptions(), JS::GCReason::DEBUG_GC, budget); + CHECK(JS::IsIncrementalGCInProgress(cx)); + + CHECK(f()); + + JS::FinishIncrementalGC(cx, JS::GCReason::DEBUG_GC); +#endif + + return true; +} + +template <typename WrapperT, typename ObjectT> +bool TestReadBarrierMarksBlack(bool fromWhite) { + AutoLeaveZeal noZeal(cx); + + // Create an object and hide it from the hazard analysis. + void* ptr = CreateHiddenTenuredGCThing<ObjectT>(cx); + CHECK(ptr); + + CallDuringIncrementalGC(9 /* YieldBeforeSweeping */, [&]() -> bool { + CHECK(JS::IsIncrementalBarrierNeeded(cx)); + + auto obj = RecoverHiddenGCThing<ObjectT>(ptr); + + WrapperT wrapper(obj); + + CHECK(GetColor(obj) == gc::CellColor::White); + if (!fromWhite) { + MakeGray(obj); + } + + Access<WrapperT, ObjectT>(wrapper); + + CHECK(GetColor(obj) == gc::CellColor::Black); + + return true; + }); + + return true; +} + +template <typename WrapperT, typename ObjectT> +bool TestConstructorBarrier() { + AutoLeaveZeal noZeal(cx); + + // Create an object and hide it from the hazard analysis. + void* ptr = CreateHiddenTenuredGCThing<ObjectT>(cx); + CHECK(ptr); + + CallDuringIncrementalGC(9 /* YieldBeforeSweeping */, [&]() -> bool { + CHECK(JS::IsIncrementalBarrierNeeded(cx)); + + auto obj = RecoverHiddenGCThing<ObjectT>(ptr); + WrapperT wrapper(obj); + CHECK(GetColor(obj) == gc::CellColor::White); + + WrapperT copiedWrapper(wrapper); + CHECK(GetColor(obj) == gc::CellColor::Black); + + return true; + }); + + ptr = CreateHiddenTenuredGCThing<ObjectT>(cx); + CHECK(ptr); + + CallDuringIncrementalGC(9 /* YieldBeforeSweeping */, [&]() -> bool { + CHECK(JS::IsIncrementalBarrierNeeded(cx)); + + auto obj = RecoverHiddenGCThing<ObjectT>(ptr); + WrapperT wrapper(obj); + CHECK(GetColor(obj) == gc::CellColor::White); + + WrapperT movedWrapper(std::move(wrapper)); + CHECK(GetColor(obj) == gc::CellColor::Black); + + return true; + }); + + return true; +} + +template <typename WrapperT, typename ObjectT> +bool TestUnbarrieredOperations(ObjectT obj, ObjectT obj2, WrapperT& wrapper, + WrapperT& wrapper2) { + (void)bool(wrapper); + (void)bool(wrapper2); + CHECK(GetColor(obj) == gc::CellColor::Gray); + CHECK(GetColor(obj2) == gc::CellColor::Gray); + + int x = 0; + + CHECK(GetColor(obj) == gc::CellColor::Gray); + CHECK(GetColor(obj2) == gc::CellColor::Gray); + x += obj == obj2; + CHECK(GetColor(obj) == gc::CellColor::Gray); + CHECK(GetColor(obj2) == gc::CellColor::Gray); + x += obj == wrapper2; + CHECK(GetColor(obj) == gc::CellColor::Gray); + CHECK(GetColor(obj2) == gc::CellColor::Gray); + x += wrapper == obj2; + CHECK(GetColor(obj) == gc::CellColor::Gray); + CHECK(GetColor(obj2) == gc::CellColor::Gray); + x += wrapper == wrapper2; + CHECK(GetColor(obj) == gc::CellColor::Gray); + CHECK(GetColor(obj2) == gc::CellColor::Gray); + + CHECK(x == 0); + + x += obj != obj2; + CHECK(GetColor(obj) == gc::CellColor::Gray); + CHECK(GetColor(obj2) == gc::CellColor::Gray); + x += obj != wrapper2; + CHECK(GetColor(obj) == gc::CellColor::Gray); + CHECK(GetColor(obj2) == gc::CellColor::Gray); + x += wrapper != obj2; + CHECK(GetColor(obj) == gc::CellColor::Gray); + CHECK(GetColor(obj2) == gc::CellColor::Gray); + x += wrapper != wrapper2; + CHECK(GetColor(obj) == gc::CellColor::Gray); + CHECK(GetColor(obj2) == gc::CellColor::Gray); + + CHECK(x == 4); + + return true; +} + +END_TEST(testGCHeapReadBarriers) + +using ObjectVector = Vector<JSObject*, 0, SystemAllocPolicy>; + +// Test pre-barrier implementation on wrapper types. The following wrapper types +// have a pre-barrier: +// - GCPtr +// - HeapPtr +// - PreBarriered +BEGIN_TEST(testGCHeapPreBarriers) { + AutoLeaveZeal nozeal(cx); + + AutoGCParameter param1(cx, JSGC_INCREMENTAL_GC_ENABLED, true); + + // Create a bunch of objects. These are unrooted and will be used to test + // whether barriers have fired by checking whether they have been marked + // black. + size_t objectCount = 100; // Increase this if necessary when adding tests. + ObjectVector testObjects; + for (size_t i = 0; i < objectCount; i++) { + JSObject* obj = CreateTenuredGCThing<JSObject*>(cx); + CHECK(obj); + CHECK(testObjects.append(obj)); + } + + // Start an incremental GC so we can detect if we cause barriers to fire, as + // these will mark objects black. + JS::PrepareForFullGC(cx); + SliceBudget budget(WorkBudget(1)); + gc::GCRuntime* gc = &cx->runtime()->gc; + gc->startDebugGC(JS::GCOptions::Normal, budget); + while (gc->state() != gc::State::Mark) { + gc->debugGCSlice(budget); + } + MOZ_ASSERT(cx->zone()->needsIncrementalBarrier()); + + TestWrapper<HeapPtr<JSObject*>>(testObjects); + TestWrapper<PreBarriered<JSObject*>>(testObjects); + + // GCPtr is different because 1) it doesn't support move operations as it's + // supposed to be part of a GC thing and 2) it doesn't perform a pre-barrier + // in its destructor because these are only destroyed as part of a GC where + // the barrier is unnecessary. + TestGCPtr(testObjects); + + gc::FinishGC(cx, JS::GCReason::API); + + return true; +} + +template <typename Wrapper> +bool TestWrapper(ObjectVector& testObjects) { + CHECK(TestCopyConstruction<Wrapper>(testObjects.popCopy())); + CHECK(TestMoveConstruction<Wrapper>(testObjects.popCopy())); + CHECK(TestAssignment<Wrapper>(testObjects.popCopy(), testObjects.popCopy())); + CHECK(TestMoveAssignment<Wrapper>(testObjects.popCopy(), + testObjects.popCopy())); + return true; +} + +template <typename Wrapper> +bool TestCopyConstruction(JSObject* obj) { + CHECK(GetColor(obj) == gc::CellColor::White); + + { + Wrapper wrapper1(obj); + Wrapper wrapper2(wrapper1); + CHECK(wrapper1 == obj); + CHECK(wrapper2 == obj); + CHECK(GetColor(obj) == gc::CellColor::White); + } + + // Check destructor performs pre-barrier. + CHECK(GetColor(obj) == gc::CellColor::Black); + + return true; +} + +template <typename Wrapper> +bool TestMoveConstruction(JSObject* obj) { + CHECK(GetColor(obj) == gc::CellColor::White); + + { + Wrapper wrapper1(obj); + MakeGray(obj); // Check that we allow move of gray GC thing. + Wrapper wrapper2(std::move(wrapper1)); + CHECK(!wrapper1); + CHECK(wrapper2 == obj); + CHECK(GetColor(obj) == gc::CellColor::Gray); + } + + // Check destructor performs pre-barrier. + CHECK(GetColor(obj) == gc::CellColor::Black); + + return true; +} + +template <typename Wrapper> +bool TestAssignment(JSObject* obj1, JSObject* obj2) { + CHECK(GetColor(obj1) == gc::CellColor::White); + CHECK(GetColor(obj2) == gc::CellColor::White); + + { + Wrapper wrapper1(obj1); + Wrapper wrapper2(obj2); + + wrapper2 = wrapper1; + + CHECK(wrapper1 == obj1); + CHECK(wrapper2 == obj1); + CHECK(GetColor(obj1) == gc::CellColor::White); // No barrier fired. + CHECK(GetColor(obj2) == gc::CellColor::Black); // Pre barrier fired. + } + + // Check destructor performs pre-barrier. + CHECK(GetColor(obj1) == gc::CellColor::Black); + + return true; +} + +template <typename Wrapper> +bool TestMoveAssignment(JSObject* obj1, JSObject* obj2) { + CHECK(GetColor(obj1) == gc::CellColor::White); + CHECK(GetColor(obj2) == gc::CellColor::White); + + { + Wrapper wrapper1(obj1); + Wrapper wrapper2(obj2); + + MakeGray(obj1); // Check we allow move of gray thing. + wrapper2 = std::move(wrapper1); + + CHECK(!wrapper1); + CHECK(wrapper2 == obj1); + CHECK(GetColor(obj1) == gc::CellColor::Gray); // No barrier fired. + CHECK(GetColor(obj2) == gc::CellColor::Black); // Pre barrier fired. + } + + // Check destructor performs pre-barrier. + CHECK(GetColor(obj1) == gc::CellColor::Black); + + return true; +} + +bool TestGCPtr(ObjectVector& testObjects) { + CHECK(TestGCPtrCopyConstruction(testObjects.popCopy())); + CHECK(TestGCPtrAssignment(testObjects.popCopy(), testObjects.popCopy())); + return true; +} + +bool TestGCPtrCopyConstruction(JSObject* obj) { + CHECK(GetColor(obj) == gc::CellColor::White); + + { + // Let us destroy GCPtrs ourselves for testing purposes. + gc::AutoSetThreadIsFinalizing threadIsFinalizing; + + GCPtr<JSObject*> wrapper1(obj); + GCPtr<JSObject*> wrapper2(wrapper1); + CHECK(wrapper1 == obj); + CHECK(wrapper2 == obj); + CHECK(GetColor(obj) == gc::CellColor::White); + } + + // GCPtr doesn't perform pre-barrier in destructor. + CHECK(GetColor(obj) == gc::CellColor::White); + + return true; +} + +bool TestGCPtrAssignment(JSObject* obj1, JSObject* obj2) { + CHECK(GetColor(obj1) == gc::CellColor::White); + CHECK(GetColor(obj2) == gc::CellColor::White); + + { + // Let us destroy GCPtrs ourselves for testing purposes. + gc::AutoSetThreadIsFinalizing threadIsFinalizing; + + GCPtr<JSObject*> wrapper1(obj1); + GCPtr<JSObject*> wrapper2(obj2); + + wrapper2 = wrapper1; + + CHECK(wrapper1 == obj1); + CHECK(wrapper2 == obj1); + CHECK(GetColor(obj1) == gc::CellColor::White); // No barrier fired. + CHECK(GetColor(obj2) == gc::CellColor::Black); // Pre barrier fired. + } + + // GCPtr doesn't perform pre-barrier in destructor. + CHECK(GetColor(obj1) == gc::CellColor::White); + + return true; +} + +END_TEST(testGCHeapPreBarriers) diff --git a/js/src/jsapi-tests/testGCHooks.cpp b/js/src/jsapi-tests/testGCHooks.cpp new file mode 100644 index 0000000000..7c6a224995 --- /dev/null +++ b/js/src/jsapi-tests/testGCHooks.cpp @@ -0,0 +1,279 @@ +/* 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 "mozilla/ScopeExit.h" +#include "mozilla/UniquePtr.h" + +#include <iterator> + +#include "jsapi-tests/tests.h" + +static unsigned gSliceCallbackCount = 0; +static bool gSawAllSliceCallbacks; +static bool gSawAllGCCallbacks; + +static void NonIncrementalGCSliceCallback(JSContext* cx, + JS::GCProgress progress, + const JS::GCDescription& desc) { + using namespace JS; + static GCProgress expect[] = {GC_CYCLE_BEGIN, GC_SLICE_BEGIN, GC_SLICE_END, + GC_CYCLE_END}; + + MOZ_RELEASE_ASSERT(gSliceCallbackCount < std::size(expect)); + MOZ_RELEASE_ASSERT(progress == expect[gSliceCallbackCount++]); + MOZ_RELEASE_ASSERT(desc.isZone_ == false); + MOZ_RELEASE_ASSERT(desc.options_ == JS::GCOptions::Normal); + MOZ_RELEASE_ASSERT(desc.reason_ == JS::GCReason::API); + if (progress == GC_CYCLE_END) { + mozilla::UniquePtr<char16_t> summary(desc.formatSummaryMessage(cx)); + mozilla::UniquePtr<char16_t> message(desc.formatSliceMessage(cx)); + } +} + +BEGIN_TEST(testGCSliceCallback) { + gSliceCallbackCount = 0; + JS::SetGCSliceCallback(cx, NonIncrementalGCSliceCallback); + JS_GC(cx); + JS::SetGCSliceCallback(cx, nullptr); + CHECK(gSliceCallbackCount == 4); + return true; +} +END_TEST(testGCSliceCallback) + +static void RootsRemovedGCSliceCallback(JSContext* cx, JS::GCProgress progress, + const JS::GCDescription& desc) { + using namespace JS; + + static constexpr struct { + GCProgress progress; + GCReason reason; + } expect[] = { + // Explicitly requested a full GC. + {GC_CYCLE_BEGIN, GCReason::DEBUG_GC}, + {GC_SLICE_BEGIN, GCReason::DEBUG_GC}, + {GC_SLICE_END, GCReason::DEBUG_GC}, + {GC_SLICE_BEGIN, GCReason::DEBUG_GC}, + {GC_SLICE_END, GCReason::DEBUG_GC}, + {GC_CYCLE_END, GCReason::DEBUG_GC}, + // Shutdown GC with ROOTS_REMOVED. + {GC_CYCLE_BEGIN, GCReason::ROOTS_REMOVED}, + {GC_SLICE_BEGIN, GCReason::ROOTS_REMOVED}, + {GC_SLICE_END, GCReason::ROOTS_REMOVED}, + {GC_CYCLE_END, GCReason::ROOTS_REMOVED} + // All done. + }; + + MOZ_RELEASE_ASSERT(gSliceCallbackCount < std::size(expect)); + MOZ_RELEASE_ASSERT(progress == expect[gSliceCallbackCount].progress); + MOZ_RELEASE_ASSERT(desc.isZone_ == false); + MOZ_RELEASE_ASSERT(desc.options_ == JS::GCOptions::Shrink); + MOZ_RELEASE_ASSERT(desc.reason_ == expect[gSliceCallbackCount].reason); + gSliceCallbackCount++; +} + +BEGIN_TEST(testGCRootsRemoved) { + AutoLeaveZeal nozeal(cx); + + AutoGCParameter param1(cx, JSGC_INCREMENTAL_GC_ENABLED, true); + + gSliceCallbackCount = 0; + JS::SetGCSliceCallback(cx, RootsRemovedGCSliceCallback); + auto byebye = + mozilla::MakeScopeExit([=] { JS::SetGCSliceCallback(cx, nullptr); }); + + JS::RootedObject obj(cx, JS_NewPlainObject(cx)); + CHECK(obj); + + JS::PrepareForFullGC(cx); + js::SliceBudget budget(js::WorkBudget(1)); + cx->runtime()->gc.startDebugGC(JS::GCOptions::Shrink, budget); + CHECK(JS::IsIncrementalGCInProgress(cx)); + + // Trigger another GC after the current one in shrinking / shutdown GCs. + cx->runtime()->gc.notifyRootsRemoved(); + + JS::FinishIncrementalGC(cx, JS::GCReason::DEBUG_GC); + CHECK(!JS::IsIncrementalGCInProgress(cx)); + + return true; +} +END_TEST(testGCRootsRemoved) + +#define ASSERT_MSG(cond, ...) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, __VA_ARGS__); \ + MOZ_RELEASE_ASSERT(cond); \ + } \ + } while (false) + +// Trigger some nested GCs to ensure that they get their own reasons and +// fullGCRequested state. +// +// The callbacks will form the following tree: +// +// Begin(DEBUG_GC) +// Begin(API) +// End(API) +// End(DEBUG_GC) +// Begin(MEM_PRESSURE) +// End(MEM_PRESSURE) +// Begin(DOM_WINDOW_UTILS) +// End(DOM_WINDOW_UTILS) +// +// JSGC_BEGIN and JSGC_END callbacks will be observed as a preorder traversal +// of the above tree. +// +// Note that the ordering of the *slice* callbacks don't match up simply to the +// ordering above. If a JSGC_BEGIN triggers another GC, we won't see the outer +// GC's GC_CYCLE_BEGIN until the inner one is done. The slice callbacks are +// reporting the actual order that the GCs are happening in. +// +// JSGC_END, on the other hand, won't be emitted until the GC is complete and +// the GC_CYCLE_BEGIN callback has fired. So its ordering is straightforward. +// +static void GCTreeCallback(JSContext* cx, JSGCStatus status, + JS::GCReason reason, void* data) { + using namespace JS; + + static constexpr struct { + JSGCStatus expectedStatus; + JS::GCReason expectedReason; + bool fireGC; + JS::GCReason reason; + bool requestFullGC; + } invocations[] = { + {JSGC_BEGIN, GCReason::DEBUG_GC, true, GCReason::API, false}, + {JSGC_BEGIN, GCReason::API, false}, + {JSGC_END, GCReason::API, false}, + {JSGC_END, GCReason::DEBUG_GC, true, GCReason::MEM_PRESSURE, true}, + {JSGC_BEGIN, GCReason::MEM_PRESSURE, false}, + {JSGC_END, GCReason::MEM_PRESSURE, true, GCReason::DOM_WINDOW_UTILS, + false}, + {JSGC_BEGIN, GCReason::DOM_WINDOW_UTILS, false}, + {JSGC_END, GCReason::DOM_WINDOW_UTILS, false}}; + + static size_t i = 0; + MOZ_RELEASE_ASSERT(i < std::size(invocations)); + auto& invocation = invocations[i++]; + if (i == std::size(invocations)) { + gSawAllGCCallbacks = true; + } + ASSERT_MSG(status == invocation.expectedStatus, + "GC callback #%zu: got status %d expected %d\n", i, status, + invocation.expectedStatus); + ASSERT_MSG(reason == invocation.expectedReason, + "GC callback #%zu: got reason %s expected %s\n", i, + ExplainGCReason(reason), + ExplainGCReason(invocation.expectedReason)); + if (invocation.fireGC) { + if (invocation.requestFullGC) { + JS::PrepareForFullGC(cx); + } + js::SliceBudget budget = js::SliceBudget(js::WorkBudget(1)); + cx->runtime()->gc.startGC(GCOptions::Normal, invocation.reason, budget); + MOZ_RELEASE_ASSERT(JS::IsIncrementalGCInProgress(cx)); + + JS::FinishIncrementalGC(cx, invocation.reason); + MOZ_RELEASE_ASSERT(!JS::IsIncrementalGCInProgress(cx)); + } +} + +static void GCTreeSliceCallback(JSContext* cx, JS::GCProgress progress, + const JS::GCDescription& desc) { + using namespace JS; + + static constexpr struct { + GCProgress progress; + GCReason reason; + bool isZonal; + } expectations[] = { + // JSGC_BEGIN triggers a new GC before we get any slice callbacks from the + // original outer GC. So the very first reason observed is API, not + // DEBUG_GC. + {GC_CYCLE_BEGIN, GCReason::API, true}, + {GC_SLICE_BEGIN, GCReason::API, true}, + {GC_SLICE_END, GCReason::API, true}, + {GC_SLICE_BEGIN, GCReason::API, true}, + {GC_SLICE_END, GCReason::API, true}, + {GC_CYCLE_END, GCReason::API, true}, + // Now the "outer" GC runs. It requested a full GC. + {GC_CYCLE_BEGIN, GCReason::DEBUG_GC, false}, + {GC_SLICE_BEGIN, GCReason::DEBUG_GC, false}, + {GC_SLICE_END, GCReason::DEBUG_GC, false}, + {GC_SLICE_BEGIN, GCReason::DEBUG_GC, false}, + {GC_SLICE_END, GCReason::DEBUG_GC, false}, + {GC_CYCLE_END, GCReason::DEBUG_GC, false}, + // The outer JSGC_DEBUG GC's end callback triggers a full MEM_PRESSURE + // GC, which runs next. (Its JSGC_BEGIN does not run a GC.) + {GC_CYCLE_BEGIN, GCReason::MEM_PRESSURE, false}, + {GC_SLICE_BEGIN, GCReason::MEM_PRESSURE, false}, + {GC_SLICE_END, GCReason::MEM_PRESSURE, false}, + {GC_SLICE_BEGIN, GCReason::MEM_PRESSURE, false}, + {GC_SLICE_END, GCReason::MEM_PRESSURE, false}, + {GC_CYCLE_END, GCReason::MEM_PRESSURE, false}, + // The MEM_PRESSURE's GC's end callback now triggers a (zonal) + // DOM_WINDOW_UTILS GC. + {GC_CYCLE_BEGIN, GCReason::DOM_WINDOW_UTILS, true}, + {GC_SLICE_BEGIN, GCReason::DOM_WINDOW_UTILS, true}, + {GC_SLICE_END, GCReason::DOM_WINDOW_UTILS, true}, + {GC_SLICE_BEGIN, GCReason::DOM_WINDOW_UTILS, true}, + {GC_SLICE_END, GCReason::DOM_WINDOW_UTILS, true}, + {GC_CYCLE_END, GCReason::DOM_WINDOW_UTILS, true}, + // All done. + }; + + MOZ_RELEASE_ASSERT(gSliceCallbackCount < std::size(expectations)); + auto& expect = expectations[gSliceCallbackCount]; + ASSERT_MSG(progress == expect.progress, "iteration %d: wrong progress\n", + gSliceCallbackCount); + ASSERT_MSG(desc.reason_ == expect.reason, + "iteration %d: expected %s got %s\n", gSliceCallbackCount, + JS::ExplainGCReason(expect.reason), + JS::ExplainGCReason(desc.reason_)); + ASSERT_MSG(desc.isZone_ == expect.isZonal, "iteration %d: wrong zonal\n", + gSliceCallbackCount); + MOZ_RELEASE_ASSERT(desc.options_ == JS::GCOptions::Normal); + gSliceCallbackCount++; + if (gSliceCallbackCount == std::size(expectations)) { + gSawAllSliceCallbacks = true; + } +} + +BEGIN_TEST(testGCTree) { + AutoLeaveZeal nozeal(cx); + + AutoGCParameter param1(cx, JSGC_INCREMENTAL_GC_ENABLED, true); + + gSliceCallbackCount = 0; + gSawAllSliceCallbacks = false; + gSawAllGCCallbacks = false; + JS::SetGCSliceCallback(cx, GCTreeSliceCallback); + JS_SetGCCallback(cx, GCTreeCallback, nullptr); + + // Automate the callback clearing. Otherwise if a CHECK fails, it will get + // cluttered with additional failures from the callback unexpectedly firing + // during the final shutdown GC. + auto byebye = mozilla::MakeScopeExit([=] { + JS::SetGCSliceCallback(cx, nullptr); + JS_SetGCCallback(cx, nullptr, nullptr); + }); + + JS::RootedObject obj(cx, JS_NewPlainObject(cx)); + CHECK(obj); + + // Outer GC is a full GC. + JS::PrepareForFullGC(cx); + js::SliceBudget budget(js::WorkBudget(1)); + cx->runtime()->gc.startDebugGC(JS::GCOptions::Normal, budget); + CHECK(JS::IsIncrementalGCInProgress(cx)); + + JS::FinishIncrementalGC(cx, JS::GCReason::DEBUG_GC); + CHECK(!JS::IsIncrementalGCInProgress(cx)); + CHECK(gSawAllSliceCallbacks); + CHECK(gSawAllGCCallbacks); + + return true; +} +END_TEST(testGCTree) diff --git a/js/src/jsapi-tests/testGCMarking.cpp b/js/src/jsapi-tests/testGCMarking.cpp new file mode 100644 index 0000000000..dc2f1e0f4d --- /dev/null +++ b/js/src/jsapi-tests/testGCMarking.cpp @@ -0,0 +1,448 @@ +/* -*- 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 "js/GlobalObject.h" // JS_NewGlobalObject +#include "js/PropertyAndElement.h" // JS_DefineProperty, JS_GetProperty, JS_SetProperty +#include "js/RootingAPI.h" +#include "js/SliceBudget.h" +#include "jsapi-tests/tests.h" +#include "vm/Compartment.h" +#include "vm/Realm.h" + +using namespace js; + +static bool ConstructCCW(JSContext* cx, const JSClass* globalClasp, + JS::HandleObject global1, + JS::MutableHandleObject wrapper, + JS::MutableHandleObject global2, + JS::MutableHandleObject wrappee) { + if (!global1) { + fprintf(stderr, "null initial global"); + return false; + } + + // Define a second global in a different zone. + JS::RealmOptions options; + global2.set(JS_NewGlobalObject(cx, globalClasp, nullptr, + JS::FireOnNewGlobalHook, options)); + if (!global2) { + fprintf(stderr, "failed to create second global"); + return false; + } + + // This should always be false, regardless. + if (global1->compartment() == global2->compartment()) { + fprintf(stderr, "second global claims to be in global1's compartment"); + return false; + } + + // This checks that the API obeys the implicit zone request. + if (global1->zone() == global2->zone()) { + fprintf(stderr, "global2 is in global1's zone"); + return false; + } + + // Define an object in compartment 2, that is wrapped by a CCW into + // compartment 1. + { + JSAutoRealm ar(cx, global2); + wrappee.set(JS_NewPlainObject(cx)); + if (wrappee->compartment() != global2->compartment()) { + fprintf(stderr, "wrappee in wrong compartment"); + return false; + } + } + + wrapper.set(wrappee); + if (!JS_WrapObject(cx, wrapper)) { + fprintf(stderr, "failed to wrap"); + return false; + } + if (wrappee == wrapper) { + fprintf(stderr, "expected wrapping"); + return false; + } + if (wrapper->compartment() != global1->compartment()) { + fprintf(stderr, "wrapper in wrong compartment"); + return false; + } + + return true; +} + +class CCWTestTracer final : public JS::CallbackTracer { + void onChild(JS::GCCellPtr thing, const char* name) override { + numberOfThingsTraced++; + + printf("*thingp = %p\n", thing.asCell()); + printf("*expectedThingp = %p\n", *expectedThingp); + + printf("kind = %d\n", static_cast<int>(thing.kind())); + printf("expectedKind = %d\n", static_cast<int>(expectedKind)); + + if (thing.asCell() != *expectedThingp || thing.kind() != expectedKind) { + okay = false; + } + } + + public: + bool okay; + size_t numberOfThingsTraced; + void** expectedThingp; + JS::TraceKind expectedKind; + + CCWTestTracer(JSContext* cx, void** expectedThingp, + JS::TraceKind expectedKind) + : JS::CallbackTracer(cx), + okay(true), + numberOfThingsTraced(0), + expectedThingp(expectedThingp), + expectedKind(expectedKind) {} +}; + +BEGIN_TEST(testTracingIncomingCCWs) { +#ifdef JS_GC_ZEAL + // Disable zeal modes because this test needs to control exactly when the GC + // happens. + JS_SetGCZeal(cx, 0, 100); +#endif + JS_GC(cx); + + JS::RootedObject global1(cx, JS::CurrentGlobalOrNull(cx)); + JS::RootedObject wrapper(cx, JS::CurrentGlobalOrNull(cx)); + JS::RootedObject global2(cx, JS::CurrentGlobalOrNull(cx)); + JS::RootedObject wrappee(cx, JS::CurrentGlobalOrNull(cx)); + CHECK(ConstructCCW(cx, getGlobalClass(), global1, &wrapper, &global2, + &wrappee)); + JS_GC(cx); + CHECK(!js::gc::IsInsideNursery(wrappee)); + CHECK(!js::gc::IsInsideNursery(wrapper)); + + JS::RootedValue v(cx, JS::ObjectValue(*wrapper)); + CHECK(JS_SetProperty(cx, global1, "ccw", v)); + + // Ensure that |TraceIncomingCCWs| finds the object wrapped by the CCW. + + JS::CompartmentSet compartments; + CHECK(compartments.put(global2->compartment())); + + void* thing = wrappee.get(); + CCWTestTracer trc(cx, &thing, JS::TraceKind::Object); + js::gc::TraceIncomingCCWs(&trc, compartments); + CHECK(trc.numberOfThingsTraced == 1); + CHECK(trc.okay); + + return true; +} +END_TEST(testTracingIncomingCCWs) + +static size_t countObjectWrappers(JS::Compartment* comp) { + size_t count = 0; + for (JS::Compartment::ObjectWrapperEnum e(comp); !e.empty(); e.popFront()) { + ++count; + } + return count; +} + +BEGIN_TEST(testDeadNurseryCCW) { +#ifdef JS_GC_ZEAL + // Disable zeal modes because this test needs to control exactly when the GC + // happens. + JS_SetGCZeal(cx, 0, 100); +#endif + JS_GC(cx); + + JS::RootedObject global1(cx, JS::CurrentGlobalOrNull(cx)); + JS::RootedObject wrapper(cx, JS::CurrentGlobalOrNull(cx)); + JS::RootedObject global2(cx, JS::CurrentGlobalOrNull(cx)); + JS::RootedObject wrappee(cx, JS::CurrentGlobalOrNull(cx)); + CHECK(ConstructCCW(cx, getGlobalClass(), global1, &wrapper, &global2, + &wrappee)); + CHECK(js::gc::IsInsideNursery(wrappee)); + CHECK(js::gc::IsInsideNursery(wrapper)); + + // Now let the obj and wrapper die. + wrappee = wrapper = nullptr; + + // Now a GC should clear the CCW. + CHECK(countObjectWrappers(global1->compartment()) == 1); + cx->runtime()->gc.evictNursery(); + CHECK(countObjectWrappers(global1->compartment()) == 0); + + // Check for corruption of the CCW table by doing a full GC to force sweeping. + JS_GC(cx); + + return true; +} +END_TEST(testDeadNurseryCCW) + +BEGIN_TEST(testLiveNurseryCCW) { +#ifdef JS_GC_ZEAL + // Disable zeal modes because this test needs to control exactly when the GC + // happens. + JS_SetGCZeal(cx, 0, 100); +#endif + JS_GC(cx); + + JS::RootedObject global1(cx, JS::CurrentGlobalOrNull(cx)); + JS::RootedObject wrapper(cx, JS::CurrentGlobalOrNull(cx)); + JS::RootedObject global2(cx, JS::CurrentGlobalOrNull(cx)); + JS::RootedObject wrappee(cx, JS::CurrentGlobalOrNull(cx)); + CHECK(ConstructCCW(cx, getGlobalClass(), global1, &wrapper, &global2, + &wrappee)); + CHECK(js::gc::IsInsideNursery(wrappee)); + CHECK(js::gc::IsInsideNursery(wrapper)); + + // Now a GC should not kill the CCW. + CHECK(countObjectWrappers(global1->compartment()) == 1); + cx->runtime()->gc.evictNursery(); + CHECK(countObjectWrappers(global1->compartment()) == 1); + + CHECK(!js::gc::IsInsideNursery(wrappee)); + CHECK(!js::gc::IsInsideNursery(wrapper)); + + // Check for corruption of the CCW table by doing a full GC to force sweeping. + JS_GC(cx); + + return true; +} +END_TEST(testLiveNurseryCCW) + +BEGIN_TEST(testLiveNurseryWrapperCCW) { +#ifdef JS_GC_ZEAL + // Disable zeal modes because this test needs to control exactly when the GC + // happens. + JS_SetGCZeal(cx, 0, 100); +#endif + JS_GC(cx); + + JS::RootedObject global1(cx, JS::CurrentGlobalOrNull(cx)); + JS::RootedObject wrapper(cx, JS::CurrentGlobalOrNull(cx)); + JS::RootedObject global2(cx, JS::CurrentGlobalOrNull(cx)); + JS::RootedObject wrappee(cx, JS::CurrentGlobalOrNull(cx)); + CHECK(ConstructCCW(cx, getGlobalClass(), global1, &wrapper, &global2, + &wrappee)); + CHECK(js::gc::IsInsideNursery(wrappee)); + CHECK(js::gc::IsInsideNursery(wrapper)); + + // The wrapper contains a strong reference to the wrappee, so just dropping + // the reference to the wrappee will not drop the CCW table entry as long + // as the wrapper is held strongly. Thus, the minor collection here must + // tenure both the wrapper and the wrappee and keep both in the table. + wrappee = nullptr; + + // Now a GC should not kill the CCW. + CHECK(countObjectWrappers(global1->compartment()) == 1); + cx->runtime()->gc.evictNursery(); + CHECK(countObjectWrappers(global1->compartment()) == 1); + + CHECK(!js::gc::IsInsideNursery(wrapper)); + + // Check for corruption of the CCW table by doing a full GC to force sweeping. + JS_GC(cx); + + return true; +} +END_TEST(testLiveNurseryWrapperCCW) + +BEGIN_TEST(testLiveNurseryWrappeeCCW) { +#ifdef JS_GC_ZEAL + // Disable zeal modes because this test needs to control exactly when the GC + // happens. + JS_SetGCZeal(cx, 0, 100); +#endif + JS_GC(cx); + + JS::RootedObject global1(cx, JS::CurrentGlobalOrNull(cx)); + JS::RootedObject wrapper(cx, JS::CurrentGlobalOrNull(cx)); + JS::RootedObject global2(cx, JS::CurrentGlobalOrNull(cx)); + JS::RootedObject wrappee(cx, JS::CurrentGlobalOrNull(cx)); + CHECK(ConstructCCW(cx, getGlobalClass(), global1, &wrapper, &global2, + &wrappee)); + CHECK(js::gc::IsInsideNursery(wrappee)); + CHECK(js::gc::IsInsideNursery(wrapper)); + + // Let the wrapper die. The wrapper should drop from the table when we GC, + // even though there are other non-cross-compartment edges to it. + wrapper = nullptr; + + // Now a GC should not kill the CCW. + CHECK(countObjectWrappers(global1->compartment()) == 1); + cx->runtime()->gc.evictNursery(); + CHECK(countObjectWrappers(global1->compartment()) == 0); + + CHECK(!js::gc::IsInsideNursery(wrappee)); + + // Check for corruption of the CCW table by doing a full GC to force sweeping. + JS_GC(cx); + + return true; +} +END_TEST(testLiveNurseryWrappeeCCW) + +BEGIN_TEST(testIncrementalRoots) { + JSRuntime* rt = cx->runtime(); + +#ifdef JS_GC_ZEAL + // Disable zeal modes because this test needs to control exactly when the GC + // happens. + JS_SetGCZeal(cx, 0, 100); +#endif + + // Construct a big object graph to mark. In JS, the resulting object graph + // is equivalent to: + // + // leaf = {}; + // leaf2 = {}; + // root = { 'obj': { 'obj': ... { 'obj': leaf, 'leaf2': leaf2 } ... } } + // + // with leafOwner the object that has the 'obj' and 'leaf2' properties. + + JS::RootedObject obj(cx, JS_NewObject(cx, nullptr)); + if (!obj) { + return false; + } + + JS::RootedObject root(cx, obj); + + JS::RootedObject leaf(cx); + JS::RootedObject leafOwner(cx); + + for (size_t i = 0; i < 3000; i++) { + JS::RootedObject subobj(cx, JS_NewObject(cx, nullptr)); + if (!subobj) { + return false; + } + if (!JS_DefineProperty(cx, obj, "obj", subobj, 0)) { + return false; + } + leafOwner = obj; + obj = subobj; + leaf = subobj; + } + + // Give the leaf owner a second leaf. + { + JS::RootedObject leaf2(cx, JS_NewObject(cx, nullptr)); + if (!leaf2) { + return false; + } + if (!JS_DefineProperty(cx, leafOwner, "leaf2", leaf2, 0)) { + return false; + } + } + + // This is marked during markRuntime + JS::RootedObjectVector vec(cx); + if (!vec.append(root)) { + return false; + } + + // Tenure everything so intentionally unrooted objects don't move before we + // can use them. + cx->runtime()->gc.minorGC(JS::GCReason::API); + + // Release all roots except for the RootedObjectVector. + obj = root = nullptr; + + // We need to manipulate interior nodes, but the JSAPI understandably wants + // to make it difficult to do that without rooting things on the stack (by + // requiring Handle parameters). We can do it anyway by using + // fromMarkedLocation. The hazard analysis is OK with this because the + // unrooted variables are not live after they've been pointed to via + // fromMarkedLocation; you're essentially lying to the analysis, saying + // that the unrooted variables are rooted. + // + // The analysis will report this lie in its listing of "unsafe references", + // but we do not break the build based on those as there are too many false + // positives. + JSObject* unrootedLeaf = leaf; + JS::Value unrootedLeafValue = JS::ObjectValue(*leaf); + JSObject* unrootedLeafOwner = leafOwner; + JS::HandleObject leafHandle = + JS::HandleObject::fromMarkedLocation(&unrootedLeaf); + JS::HandleValue leafValueHandle = + JS::HandleValue::fromMarkedLocation(&unrootedLeafValue); + JS::HandleObject leafOwnerHandle = + JS::HandleObject::fromMarkedLocation(&unrootedLeafOwner); + leaf = leafOwner = nullptr; + + // Do the root marking slice. This should mark 'root' and a bunch of its + // descendants. It shouldn't make it all the way through (it gets a budget + // of 1000, and the graph is about 3000 objects deep). + js::SliceBudget budget(js::WorkBudget(1000)); + AutoGCParameter param(cx, JSGC_INCREMENTAL_GC_ENABLED, true); + rt->gc.startDebugGC(JS::GCOptions::Normal, budget); + while (rt->gc.state() != gc::State::Mark) { + rt->gc.debugGCSlice(budget); + } + + // We'd better be between iGC slices now. There's always a risk that + // something will decide that we need to do a full GC (such as gczeal, but + // that is turned off.) + MOZ_ASSERT(JS::IsIncrementalGCInProgress(cx)); + + // And assert that the mark bits are as we expect them to be. + MOZ_ASSERT(vec[0]->asTenured().isMarkedBlack()); + MOZ_ASSERT(!leafHandle->asTenured().isMarkedBlack()); + MOZ_ASSERT(!leafOwnerHandle->asTenured().isMarkedBlack()); + +#ifdef DEBUG + // Remember the current GC number so we can assert that no GC occurs + // between operations. + auto currentGCNumber = rt->gc.gcNumber(); +#endif + + // Now do the incremental GC's worst nightmare: rip an unmarked object + // 'leaf' out of the graph and stick it into an already-marked region (hang + // it off the un-prebarriered root, in fact). The pre-barrier on the + // overwrite of the source location should cause this object to be marked. + if (!JS_SetProperty(cx, leafOwnerHandle, "obj", JS::UndefinedHandleValue)) { + return false; + } + MOZ_ASSERT(rt->gc.gcNumber() == currentGCNumber); + if (!JS_SetProperty(cx, vec[0], "newobj", leafValueHandle)) { + return false; + } + MOZ_ASSERT(rt->gc.gcNumber() == currentGCNumber); + MOZ_ASSERT(leafHandle->asTenured().isMarkedBlack()); + + // Also take an unmarked object 'leaf2' from the graph and add an + // additional edge from the root to it. This will not be marked by any + // pre-barrier, but it is still in the live graph so it will eventually get + // marked. + // + // Note that the root->leaf2 edge will *not* be marked through, since the + // root is already marked, but that only matters if doing a compacting GC + // and the compacting GC repeats the whole marking phase to update + // pointers. + { + JS::RootedValue leaf2(cx); + if (!JS_GetProperty(cx, leafOwnerHandle, "leaf2", &leaf2)) { + return false; + } + MOZ_ASSERT(rt->gc.gcNumber() == currentGCNumber); + MOZ_ASSERT(!leaf2.toObject().asTenured().isMarkedBlack()); + if (!JS_SetProperty(cx, vec[0], "leafcopy", leaf2)) { + return false; + } + MOZ_ASSERT(rt->gc.gcNumber() == currentGCNumber); + MOZ_ASSERT(!leaf2.toObject().asTenured().isMarkedBlack()); + } + + // Finish the GC using an unlimited budget. + auto unlimited = js::SliceBudget::unlimited(); + rt->gc.debugGCSlice(unlimited); + + // Access the leaf object to try to trigger a crash if it is dead. + if (!JS_SetProperty(cx, leafHandle, "toes", JS::UndefinedHandleValue)) { + return false; + } + + return true; +} +END_TEST(testIncrementalRoots) diff --git a/js/src/jsapi-tests/testGCOutOfMemory.cpp b/js/src/jsapi-tests/testGCOutOfMemory.cpp new file mode 100644 index 0000000000..bfd7079584 --- /dev/null +++ b/js/src/jsapi-tests/testGCOutOfMemory.cpp @@ -0,0 +1,79 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + * + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/licenses/publicdomain/ + * Contributor: Igor Bukanov + */ + +#include "mozilla/Utf8.h" // mozilla::Utf8Unit + +#include "gc/GCEnum.h" // js::gc::ZealMode +#include "js/CompilationAndEvaluation.h" // JS::Evaluate +#include "js/SourceText.h" // JS::Source{Ownership,Text} +#include "jsapi-tests/tests.h" + +BEGIN_TEST(testGCOutOfMemory) { + // Count the number of allocations until we hit OOM, and store it in 'max'. + static const char source[] = + "var max = 0; (function() {" + " var array = [];" + " for (; ; ++max)" + " array.push({});" + " array = []; array.push(0);" + "})();"; + + JS::CompileOptions opts(cx); + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + CHECK(srcBuf.init(cx, source, strlen(source), JS::SourceOwnership::Borrowed)); + + JS::RootedValue root(cx); + bool ok = JS::Evaluate(cx, opts, srcBuf, &root); + + /* Check that we get OOM. */ + CHECK(!ok); + CHECK(JS_GetPendingException(cx, &root)); + CHECK(root.isString()); + bool match = false; + CHECK(JS_StringEqualsLiteral(cx, root.toString(), "out of memory", &match)); + CHECK(match); + JS_ClearPendingException(cx); + + JS_GC(cx); + + // The above GC should have discarded everything. Verify that we can now + // allocate half as many objects without OOMing. + EVAL( + "(function() {" + " var array = [];" + " for (var i = max >> 2; i != 0;) {" + " --i;" + " array.push({});" + " }" + "})();", + &root); + CHECK(!JS_IsExceptionPending(cx)); + return true; +} + +virtual JSContext* createContext() override { + // Note that the max nursery size must be less than the whole heap size, or + // the test will fail because 'max' (the number of allocations required for + // OOM) will be based on the nursery size, and that will overflow the + // tenured heap, which will cause the second pass with max/4 allocations to + // OOM. (Actually, this only happens with nursery zeal, because normally + // the nursery will start out with only a single chunk before triggering a + // major GC.) + JSContext* cx = JS_NewContext(4 * 1024 * 1024); + if (!cx) { + return nullptr; + } + JS_SetGCParameter(cx, JSGC_MAX_NURSERY_BYTES, js::gc::ChunkSize); +#ifdef JS_GC_ZEAL + JS_UnsetGCZeal(cx, uint8_t(js::gc::ZealMode::GenerationalGC)); +#endif + return cx; +} + +END_TEST(testGCOutOfMemory) diff --git a/js/src/jsapi-tests/testGCStoreBufferRemoval.cpp b/js/src/jsapi-tests/testGCStoreBufferRemoval.cpp new file mode 100644 index 0000000000..4a4ae1fe42 --- /dev/null +++ b/js/src/jsapi-tests/testGCStoreBufferRemoval.cpp @@ -0,0 +1,111 @@ +/* -*- 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 "gc/Barrier.h" +#include "js/GCAPI.h" +#include "jsapi-tests/tests.h" + +using namespace JS; +using namespace js; + +// Name this constant without creating a GC hazard. +#define BAD_OBJECT_PTR reinterpret_cast<JSObject*>(1) + +BEGIN_TEST(testGCStoreBufferRemoval) { + // Sanity check - objects start in the nursery and then become tenured. + JS_GC(cx); + JS::RootedObject obj(cx, NurseryObject()); + CHECK(js::gc::IsInsideNursery(obj.get())); + JS_GC(cx); + CHECK(!js::gc::IsInsideNursery(obj.get())); + JS::RootedObject tenuredObject(cx, obj); + + // Test removal of store buffer entries added by HeapPtr<T>. + { + JSObject* punnedPtr = nullptr; + HeapPtr<JSObject*>* relocPtr = + reinterpret_cast<HeapPtr<JSObject*>*>(&punnedPtr); + new (relocPtr) HeapPtr<JSObject*>; + *relocPtr = NurseryObject(); + relocPtr->~HeapPtr<JSObject*>(); + punnedPtr = BAD_OBJECT_PTR; + JS_GC(cx); + + new (relocPtr) HeapPtr<JSObject*>; + *relocPtr = NurseryObject(); + *relocPtr = tenuredObject; + relocPtr->~HeapPtr<JSObject*>(); + punnedPtr = BAD_OBJECT_PTR; + JS_GC(cx); + + new (relocPtr) HeapPtr<JSObject*>; + *relocPtr = NurseryObject(); + *relocPtr = nullptr; + relocPtr->~HeapPtr<JSObject*>(); + punnedPtr = BAD_OBJECT_PTR; + JS_GC(cx); + } + + // Test removal of store buffer entries added by HeapPtr<Value>. + { + Value punnedValue; + HeapPtr<Value>* relocValue = + reinterpret_cast<HeapPtr<Value>*>(&punnedValue); + new (relocValue) HeapPtr<Value>; + *relocValue = ObjectValue(*NurseryObject()); + relocValue->~HeapPtr<Value>(); + punnedValue = js::PoisonedObjectValue(0x48); + JS_GC(cx); + + new (relocValue) HeapPtr<Value>; + *relocValue = ObjectValue(*NurseryObject()); + *relocValue = ObjectValue(*tenuredObject); + relocValue->~HeapPtr<Value>(); + punnedValue = js::PoisonedObjectValue(0x48); + JS_GC(cx); + + new (relocValue) HeapPtr<Value>; + *relocValue = ObjectValue(*NurseryObject()); + *relocValue = NullValue(); + relocValue->~HeapPtr<Value>(); + punnedValue = js::PoisonedObjectValue(0x48); + JS_GC(cx); + } + + // Test removal of store buffer entries added by Heap<T>. + { + JSObject* punnedPtr = nullptr; + JS::Heap<JSObject*>* heapPtr = + reinterpret_cast<JS::Heap<JSObject*>*>(&punnedPtr); + new (heapPtr) JS::Heap<JSObject*>; + *heapPtr = NurseryObject(); + heapPtr->~Heap<JSObject*>(); + punnedPtr = BAD_OBJECT_PTR; + JS_GC(cx); + + new (heapPtr) JS::Heap<JSObject*>; + *heapPtr = NurseryObject(); + *heapPtr = tenuredObject; + heapPtr->~Heap<JSObject*>(); + punnedPtr = BAD_OBJECT_PTR; + JS_GC(cx); + + new (heapPtr) JS::Heap<JSObject*>; + *heapPtr = NurseryObject(); + *heapPtr = nullptr; + heapPtr->~Heap<JSObject*>(); + punnedPtr = BAD_OBJECT_PTR; + JS_GC(cx); + } + + return true; +} + +JSObject* NurseryObject() { return JS_NewPlainObject(cx); } +END_TEST(testGCStoreBufferRemoval) + +#undef BAD_OBJECT_PTR diff --git a/js/src/jsapi-tests/testGCUniqueId.cpp b/js/src/jsapi-tests/testGCUniqueId.cpp new file mode 100644 index 0000000000..1c5652f280 --- /dev/null +++ b/js/src/jsapi-tests/testGCUniqueId.cpp @@ -0,0 +1,125 @@ +/* -*- 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 "js/GCVector.h" + +#include "jsapi-tests/tests.h" + +#include "gc/StableCellHasher-inl.h" + +using namespace js; + +static void MinimizeHeap(JSContext* cx) { + // The second collection is to force us to wait for the background + // sweeping that the first GC started to finish. + JS_GC(cx); + JS_GC(cx); + js::gc::FinishGC(cx); +} + +BEGIN_TEST(testGCUID) { + AutoLeaveZeal nozeal(cx); + + uint64_t uid = 0; + uint64_t tmp = 0; + + // Ensure the heap is as minimal as it can get. + MinimizeHeap(cx); + + JS::RootedObject obj(cx, JS_NewPlainObject(cx)); + uintptr_t nurseryAddr = uintptr_t(obj.get()); + CHECK(obj); + CHECK(gc::IsInsideNursery(obj)); + + // Do not start with an ID. + CHECK(!gc::HasUniqueId(obj)); + + // Ensure we can get a new UID. + CHECK(gc::GetOrCreateUniqueId(obj, &uid)); + CHECK(uid > gc::LargestTaggedNullCellPointer); + + // We should now have an id. + CHECK(gc::HasUniqueId(obj)); + + // Calling again should get us the same thing. + CHECK(gc::GetOrCreateUniqueId(obj, &tmp)); + CHECK(uid == tmp); + + // Tenure the thing and check that the UID moved with it. + MinimizeHeap(cx); + uintptr_t tenuredAddr = uintptr_t(obj.get()); + CHECK(tenuredAddr != nurseryAddr); + CHECK(!gc::IsInsideNursery(obj)); + CHECK(gc::HasUniqueId(obj)); + CHECK(gc::GetOrCreateUniqueId(obj, &tmp)); + CHECK(uid == tmp); + + // Allocate a new nursery thing in the same location and check that we + // removed the prior uid that was attached to the location. + obj = JS_NewPlainObject(cx); + CHECK(obj); + CHECK(uintptr_t(obj.get()) == nurseryAddr); + CHECK(!gc::HasUniqueId(obj)); + + // Try to get another tenured object in the same location and check that + // the uid was removed correctly. + obj = nullptr; + MinimizeHeap(cx); + obj = JS_NewPlainObject(cx); + MinimizeHeap(cx); + CHECK(uintptr_t(obj.get()) == tenuredAddr); + CHECK(!gc::HasUniqueId(obj)); + CHECK(gc::GetOrCreateUniqueId(obj, &tmp)); + CHECK(uid != tmp); + uid = tmp; + + // Allocate a few arenas worth of objects to ensure we get some compaction. + const static size_t N = 2049; + using ObjectVector = JS::GCVector<JSObject*>; + JS::Rooted<ObjectVector> vec(cx, ObjectVector(cx)); + for (size_t i = 0; i < N; ++i) { + obj = JS_NewPlainObject(cx); + CHECK(obj); + CHECK(vec.append(obj)); + } + + // Transfer our vector to tenured if it isn't there already. + MinimizeHeap(cx); + + // Tear holes in the heap by unrooting the even objects and collecting. + JS::Rooted<ObjectVector> vec2(cx, ObjectVector(cx)); + for (size_t i = 0; i < N; ++i) { + if (i % 2 == 1) { + CHECK(vec2.append(vec[i])); + } + } + vec.clear(); + + // Grab the last object in the vector as our object of interest. + obj = vec2.back(); + CHECK(obj); + CHECK(!gc::IsInsideNursery(obj)); + tenuredAddr = uintptr_t(obj.get()); + CHECK(gc::GetOrCreateUniqueId(obj, &uid)); + + // Force a compaction to move the object and check that the uid moved to + // the new tenured heap location. + JS::PrepareForFullGC(cx); + JS::NonIncrementalGC(cx, JS::GCOptions::Shrink, JS::GCReason::API); + + // There's a very low probability that this check could fail, but it is + // possible. If it becomes an annoying intermittent then we should make + // this test more robust by recording IDs of many objects and then checking + // that some have moved. + CHECK(uintptr_t(obj.get()) != tenuredAddr); + CHECK(gc::HasUniqueId(obj)); + CHECK(gc::GetOrCreateUniqueId(obj, &tmp)); + CHECK(uid == tmp); + + return true; +} +END_TEST(testGCUID) diff --git a/js/src/jsapi-tests/testGCWeakCache.cpp b/js/src/jsapi-tests/testGCWeakCache.cpp new file mode 100644 index 0000000000..592487d284 --- /dev/null +++ b/js/src/jsapi-tests/testGCWeakCache.cpp @@ -0,0 +1,697 @@ +/* -*- 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 "gc/Policy.h" +#include "gc/SweepingAPI.h" +#include "gc/Zone.h" +#include "js/GCHashTable.h" +#include "js/RootingAPI.h" + +#include "jsapi-tests/tests.h" + +using namespace js; + +// Exercise WeakCache<GCHashSet>. +BEGIN_TEST(testWeakCacheSet) { + // Create two objects tenured and two in the nursery. If zeal is on, + // this may fail and we'll get more tenured objects. That's fine: + // the test will continue to work, it will just not test as much. + JS::RootedObject tenured1(cx, JS_NewPlainObject(cx)); + JS::RootedObject tenured2(cx, JS_NewPlainObject(cx)); + JS_GC(cx); + JS::RootedObject nursery1(cx, JS_NewPlainObject(cx)); + JS::RootedObject nursery2(cx, JS_NewPlainObject(cx)); + + using ObjectSet = + GCHashSet<HeapPtr<JSObject*>, StableCellHasher<HeapPtr<JSObject*>>, + SystemAllocPolicy>; + using Cache = WeakCache<ObjectSet>; + Cache cache(JS::GetObjectZone(tenured1)); + + cache.put(tenured1); + cache.put(tenured2); + cache.put(nursery1); + cache.put(nursery2); + + // Verify relocation and that we don't sweep too aggressively. + JS_GC(cx); + CHECK(cache.has(tenured1)); + CHECK(cache.has(tenured2)); + CHECK(cache.has(nursery1)); + CHECK(cache.has(nursery2)); + + // Unroot two entries and verify that they get removed. + tenured2 = nursery2 = nullptr; + JS_GC(cx); + CHECK(cache.has(tenured1)); + CHECK(cache.has(nursery1)); + CHECK(cache.count() == 2); + + return true; +} +END_TEST(testWeakCacheSet) + +// Exercise WeakCache<GCHashMap>. +BEGIN_TEST(testWeakCacheMap) { + // Create two objects tenured and two in the nursery. If zeal is on, + // this may fail and we'll get more tenured objects. That's fine: + // the test will continue to work, it will just not test as much. + JS::RootedObject tenured1(cx, JS_NewPlainObject(cx)); + JS::RootedObject tenured2(cx, JS_NewPlainObject(cx)); + JS_GC(cx); + JS::RootedObject nursery1(cx, JS_NewPlainObject(cx)); + JS::RootedObject nursery2(cx, JS_NewPlainObject(cx)); + + using ObjectMap = js::GCHashMap<HeapPtr<JSObject*>, uint32_t, + js::StableCellHasher<HeapPtr<JSObject*>>>; + using Cache = WeakCache<ObjectMap>; + Cache cache(JS::GetObjectZone(tenured1), cx); + + cache.put(tenured1, 1); + cache.put(tenured2, 2); + cache.put(nursery1, 3); + cache.put(nursery2, 4); + + JS_GC(cx); + CHECK(cache.has(tenured1)); + CHECK(cache.has(tenured2)); + CHECK(cache.has(nursery1)); + CHECK(cache.has(nursery2)); + + tenured2 = nursery2 = nullptr; + JS_GC(cx); + CHECK(cache.has(tenured1)); + CHECK(cache.has(nursery1)); + CHECK(cache.count() == 2); + + return true; +} +END_TEST(testWeakCacheMap) + +// Exercise WeakCache<GCVector>. +BEGIN_TEST(testWeakCacheGCVector) { + // Create two objects tenured and two in the nursery. If zeal is on, + // this may fail and we'll get more tenured objects. That's fine: + // the test will continue to work, it will just not test as much. + JS::RootedObject tenured1(cx, JS_NewPlainObject(cx)); + JS::RootedObject tenured2(cx, JS_NewPlainObject(cx)); + JS_GC(cx); + JS::RootedObject nursery1(cx, JS_NewPlainObject(cx)); + JS::RootedObject nursery2(cx, JS_NewPlainObject(cx)); + + using ObjectVector = WeakCache<GCVector<HeapPtr<JSObject*>>>; + ObjectVector cache(JS::GetObjectZone(tenured1), cx); + + CHECK(cache.append(tenured1)); + CHECK(cache.append(tenured2)); + CHECK(cache.append(nursery1)); + CHECK(cache.append(nursery2)); + + JS_GC(cx); + CHECK(cache.get().length() == 4); + CHECK(cache.get()[0] == tenured1); + CHECK(cache.get()[1] == tenured2); + CHECK(cache.get()[2] == nursery1); + CHECK(cache.get()[3] == nursery2); + + tenured2 = nursery2 = nullptr; + JS_GC(cx); + CHECK(cache.get().length() == 2); + CHECK(cache.get()[0] == tenured1); + CHECK(cache.get()[1] == nursery1); + + return true; +} +END_TEST(testWeakCacheGCVector) + +#ifdef JS_GC_ZEAL + +// A simple structure that embeds an object pointer. We cripple the hash +// implementation so that we can test hash table collisions. +struct ObjectEntry { + HeapPtr<JSObject*> obj; + explicit ObjectEntry(JSObject* o) : obj(o) {} + bool operator==(const ObjectEntry& other) const { return obj == other.obj; } + bool traceWeak(JSTracer* trc) { + return TraceWeakEdge(trc, &obj, "ObjectEntry::obj"); + } +}; + +namespace js { +template <> +struct StableCellHasher<ObjectEntry> { + using Key = ObjectEntry; + using Lookup = JSObject*; + + static bool maybeGetHash(const Lookup& l, HashNumber* hashOut) { + if (!StableCellHasher<JSObject*>::maybeGetHash(l, hashOut)) { + return false; + } + // Reduce hash code to single bit to generate hash collisions. + *hashOut &= 0x1; + return true; + } + static bool ensureHash(const Lookup& l, HashNumber* hashOut) { + if (!StableCellHasher<JSObject*>::ensureHash(l, hashOut)) { + return false; + } + // Reduce hash code to single bit to generate hash collisions. + *hashOut &= 0x1; + return true; + } + static HashNumber hash(const Lookup& l) { + // Reduce hash code to single bit to generate hash collisions. + return StableCellHasher<HeapPtr<JSObject*>>::hash(l) & 0x1; + } + static bool match(const Key& k, const Lookup& l) { + return StableCellHasher<HeapPtr<JSObject*>>::match(k.obj, l); + } +}; +} // namespace js + +// A structure that contains a pointer to a JSObject but is keyed based on an +// integer. This lets us test replacing dying entries in a set. +struct NumberAndObjectEntry { + uint32_t number; + HeapPtr<JSObject*> obj; + + NumberAndObjectEntry(uint32_t n, JSObject* o) : number(n), obj(o) {} + bool operator==(const NumberAndObjectEntry& other) const { + return number == other.number && obj == other.obj; + } + bool traceWeak(JSTracer* trc) { + return TraceWeakEdge(trc, &obj, "NumberAndObjectEntry::obj"); + } +}; + +struct NumberAndObjectLookup { + uint32_t number; + HeapPtr<JSObject*> obj; + + NumberAndObjectLookup(uint32_t n, JSObject* o) : number(n), obj(o) {} + MOZ_IMPLICIT NumberAndObjectLookup(const NumberAndObjectEntry& entry) + : number(entry.number), obj(entry.obj) {} +}; + +namespace js { +template <> +struct StableCellHasher<NumberAndObjectEntry> { + using Key = NumberAndObjectEntry; + using Lookup = NumberAndObjectLookup; + + static bool maybeGetHash(const Lookup& l, HashNumber* hashOut) { + if (!StableCellHasher<JSObject*>::maybeGetHash(l.obj, hashOut)) { + return false; + } + *hashOut ^= l.number; + return true; + } + static bool ensureHash(const Lookup& l, HashNumber* hashOut) { + if (!StableCellHasher<JSObject*>::ensureHash(l.obj, hashOut)) { + return false; + } + *hashOut ^= l.number; + return true; + } + static HashNumber hash(const Lookup& l) { + return StableCellHasher<HeapPtr<JSObject*>>::hash(l.obj) ^ l.number; + } + static bool match(const Key& k, const Lookup& l) { + return k.number == l.number && + StableCellHasher<HeapPtr<JSObject*>>::match(k.obj, l.obj); + } +}; +} // namespace js + +BEGIN_TEST(testIncrementalWeakCacheSweeping) { + AutoLeaveZeal nozeal(cx); + + JS_SetGCParameter(cx, JSGC_INCREMENTAL_GC_ENABLED, true); + JS_SetGCZeal(cx, 17, 1000000); + + CHECK(TestSet()); + CHECK(TestMap()); + CHECK(TestReplaceDyingInSet()); + CHECK(TestReplaceDyingInMap()); + CHECK(TestUniqueIDLookups()); + + JS_SetGCZeal(cx, 0, 0); + JS_SetGCParameter(cx, JSGC_INCREMENTAL_GC_ENABLED, false); + + return true; +} + +template <typename Cache> +bool GCUntilCacheSweep(JSContext* cx, const Cache& cache) { + CHECK(!IsIncrementalGCInProgress(cx)); + + JS::Zone* zone = JS::GetObjectZone(global); + JS::PrepareZoneForGC(cx, zone); + SliceBudget budget(WorkBudget(1)); + cx->runtime()->gc.startDebugGC(JS::GCOptions::Normal, budget); + + CHECK(IsIncrementalGCInProgress(cx)); + CHECK(zone->isGCSweeping()); + CHECK(cache.needsIncrementalBarrier()); + + return true; +} + +template <typename Cache> +bool SweepCacheAndFinishGC(JSContext* cx, const Cache& cache) { + CHECK(IsIncrementalGCInProgress(cx)); + + PrepareForIncrementalGC(cx); + IncrementalGCSlice(cx, JS::GCReason::API, SliceBudget::unlimited()); + + JS::Zone* zone = JS::GetObjectZone(global); + CHECK(!IsIncrementalGCInProgress(cx)); + CHECK(!zone->isCollecting()); + CHECK(!cache.needsIncrementalBarrier()); + + return true; +} + +bool TestSet() { + using ObjectSet = + GCHashSet<HeapPtr<JSObject*>, StableCellHasher<HeapPtr<JSObject*>>, + TempAllocPolicy>; + using Cache = WeakCache<ObjectSet>; + Cache cache(JS::GetObjectZone(global), cx); + + // Sweep empty cache. + + CHECK(cache.empty()); + JS_GC(cx); + CHECK(cache.empty()); + + // Add an entry while sweeping. + + JS::RootedObject obj1(cx, JS_NewPlainObject(cx)); + JS::RootedObject obj2(cx, JS_NewPlainObject(cx)); + JS::RootedObject obj3(cx, JS_NewPlainObject(cx)); + JS::RootedObject obj4(cx, JS_NewPlainObject(cx)); + CHECK(obj1); + CHECK(obj2); + CHECK(obj3); + CHECK(obj4); + + CHECK(!cache.has(obj1)); + CHECK(cache.put(obj1)); + CHECK(cache.count() == 1); + CHECK(cache.has(obj1)); + CHECK(*cache.lookup(obj1) == obj1); + + CHECK(GCUntilCacheSweep(cx, cache)); + + CHECK(!cache.has(obj2)); + CHECK(cache.put(obj2)); + CHECK(cache.has(obj2)); + CHECK(*cache.lookup(obj2) == obj2); + + CHECK(SweepCacheAndFinishGC(cx, cache)); + + CHECK(cache.count() == 2); + CHECK(cache.has(obj1)); + CHECK(cache.has(obj2)); + + // Test dying entries are not found while sweeping. + + CHECK(cache.put(obj3)); + CHECK(cache.put(obj4)); + void* old3 = obj3; + void* old4 = obj4; + obj3 = obj4 = nullptr; + + CHECK(GCUntilCacheSweep(cx, cache)); + + CHECK(cache.has(obj1)); + CHECK(cache.has(obj2)); + CHECK(!cache.has(static_cast<JSObject*>(old3))); + CHECK(!cache.has(static_cast<JSObject*>(old4))); + + size_t count = 0; + for (auto r = cache.all(); !r.empty(); r.popFront()) { + CHECK(r.front() == obj1 || r.front() == obj2); + count++; + } + CHECK(count == 2); + + CHECK(SweepCacheAndFinishGC(cx, cache)); + + CHECK(cache.count() == 2); + + // Test lookupForAdd while sweeping. + + obj3 = JS_NewPlainObject(cx); + obj4 = JS_NewPlainObject(cx); + CHECK(obj3); + CHECK(obj4); + + CHECK(cache.lookupForAdd(obj1)); + CHECK(*cache.lookupForAdd(obj1) == obj1); + + auto addp = cache.lookupForAdd(obj3); + CHECK(!addp); + CHECK(cache.add(addp, obj3)); + CHECK(cache.has(obj3)); + + CHECK(GCUntilCacheSweep(cx, cache)); + + addp = cache.lookupForAdd(obj4); + CHECK(!addp); + CHECK(cache.add(addp, obj4)); + CHECK(cache.has(obj4)); + + CHECK(SweepCacheAndFinishGC(cx, cache)); + + CHECK(cache.count() == 4); + CHECK(cache.has(obj3)); + CHECK(cache.has(obj4)); + + // Test remove while sweeping. + + cache.remove(obj3); + + CHECK(GCUntilCacheSweep(cx, cache)); + + cache.remove(obj4); + + CHECK(SweepCacheAndFinishGC(cx, cache)); + + CHECK(cache.count() == 2); + CHECK(!cache.has(obj3)); + CHECK(!cache.has(obj4)); + + // Test putNew while sweeping. + + CHECK(GCUntilCacheSweep(cx, cache)); + + CHECK(cache.putNew(obj3)); + CHECK(cache.putNew(obj4, obj4)); + + CHECK(SweepCacheAndFinishGC(cx, cache)); + + CHECK(cache.count() == 4); + CHECK(cache.has(obj3)); + CHECK(cache.has(obj4)); + + cache.clear(); + + return true; +} + +bool TestMap() { + using ObjectMap = + GCHashMap<HeapPtr<JSObject*>, uint32_t, + StableCellHasher<HeapPtr<JSObject*>>, TempAllocPolicy>; + using Cache = WeakCache<ObjectMap>; + Cache cache(JS::GetObjectZone(global), cx); + + // Sweep empty cache. + + CHECK(cache.empty()); + JS_GC(cx); + CHECK(cache.empty()); + + // Add an entry while sweeping. + + JS::RootedObject obj1(cx, JS_NewPlainObject(cx)); + JS::RootedObject obj2(cx, JS_NewPlainObject(cx)); + JS::RootedObject obj3(cx, JS_NewPlainObject(cx)); + JS::RootedObject obj4(cx, JS_NewPlainObject(cx)); + CHECK(obj1); + CHECK(obj2); + CHECK(obj3); + CHECK(obj4); + + CHECK(!cache.has(obj1)); + CHECK(cache.put(obj1, 1)); + CHECK(cache.count() == 1); + CHECK(cache.has(obj1)); + CHECK(cache.lookup(obj1)->key() == obj1); + + CHECK(GCUntilCacheSweep(cx, cache)); + CHECK(cache.needsIncrementalBarrier()); + + CHECK(!cache.has(obj2)); + CHECK(cache.put(obj2, 2)); + CHECK(cache.has(obj2)); + CHECK(cache.lookup(obj2)->key() == obj2); + + CHECK(SweepCacheAndFinishGC(cx, cache)); + CHECK(!cache.needsIncrementalBarrier()); + + CHECK(cache.count() == 2); + CHECK(cache.has(obj1)); + CHECK(cache.has(obj2)); + + // Test iteration. + + CHECK(cache.put(obj3, 3)); + CHECK(cache.put(obj4, 4)); + void* old3 = obj3; + void* old4 = obj4; + obj3 = obj4 = nullptr; + + CHECK(GCUntilCacheSweep(cx, cache)); + + CHECK(cache.has(obj1)); + CHECK(cache.has(obj2)); + CHECK(!cache.has(static_cast<JSObject*>(old3))); + CHECK(!cache.has(static_cast<JSObject*>(old4))); + + size_t count = 0; + for (auto r = cache.all(); !r.empty(); r.popFront()) { + CHECK(r.front().key() == obj1 || r.front().key() == obj2); + count++; + } + CHECK(count == 2); + + CHECK(SweepCacheAndFinishGC(cx, cache)); + + CHECK(cache.count() == 2); + + // Test lookupForAdd while sweeping. + + obj3 = JS_NewPlainObject(cx); + obj4 = JS_NewPlainObject(cx); + CHECK(obj3); + CHECK(obj4); + + CHECK(cache.lookupForAdd(obj1)); + CHECK(cache.lookupForAdd(obj1)->key() == obj1); + + auto addp = cache.lookupForAdd(obj3); + CHECK(!addp); + CHECK(cache.add(addp, obj3, 3)); + CHECK(cache.has(obj3)); + + CHECK(GCUntilCacheSweep(cx, cache)); + + addp = cache.lookupForAdd(obj4); + CHECK(!addp); + CHECK(cache.add(addp, obj4, 4)); + CHECK(cache.has(obj4)); + + CHECK(SweepCacheAndFinishGC(cx, cache)); + + CHECK(cache.count() == 4); + CHECK(cache.has(obj3)); + CHECK(cache.has(obj4)); + + // Test remove while sweeping. + + cache.remove(obj3); + + CHECK(GCUntilCacheSweep(cx, cache)); + + cache.remove(obj4); + + CHECK(SweepCacheAndFinishGC(cx, cache)); + + CHECK(cache.count() == 2); + CHECK(!cache.has(obj3)); + CHECK(!cache.has(obj4)); + + // Test putNew while sweeping. + + CHECK(GCUntilCacheSweep(cx, cache)); + + CHECK(cache.putNew(obj3, 3)); + CHECK(cache.putNew(obj4, 4)); + + CHECK(SweepCacheAndFinishGC(cx, cache)); + + CHECK(cache.count() == 4); + CHECK(cache.has(obj3)); + CHECK(cache.has(obj4)); + + cache.clear(); + + return true; +} + +bool TestReplaceDyingInSet() { + // Test replacing dying entries with ones that have the same key using the + // various APIs. + + using Cache = WeakCache< + GCHashSet<NumberAndObjectEntry, StableCellHasher<NumberAndObjectEntry>, + TempAllocPolicy>>; + Cache cache(JS::GetObjectZone(global), cx); + + RootedObject value1(cx, JS_NewPlainObject(cx)); + RootedObject value2(cx, JS_NewPlainObject(cx)); + CHECK(value1); + CHECK(value2); + + CHECK(cache.put(NumberAndObjectEntry(1, value1))); + CHECK(cache.put(NumberAndObjectEntry(2, value2))); + CHECK(cache.put(NumberAndObjectEntry(3, value2))); + CHECK(cache.put(NumberAndObjectEntry(4, value2))); + CHECK(cache.put(NumberAndObjectEntry(5, value2))); + CHECK(cache.put(NumberAndObjectEntry(6, value2))); + CHECK(cache.put(NumberAndObjectEntry(7, value2))); + + value2 = nullptr; + CHECK(GCUntilCacheSweep(cx, cache)); + + CHECK(!cache.has(NumberAndObjectLookup(2, value1))); + CHECK(!cache.has(NumberAndObjectLookup(3, value1))); + CHECK(!cache.has(NumberAndObjectLookup(4, value1))); + CHECK(!cache.has(NumberAndObjectLookup(5, value1))); + CHECK(!cache.has(NumberAndObjectLookup(6, value1))); + + auto ptr = cache.lookupForAdd(NumberAndObjectLookup(2, value1)); + CHECK(!ptr); + CHECK(cache.add(ptr, NumberAndObjectEntry(2, value1))); + + auto ptr2 = cache.lookupForAdd(NumberAndObjectLookup(3, value1)); + CHECK(!ptr2); + CHECK(cache.relookupOrAdd(ptr2, NumberAndObjectLookup(3, value1), + NumberAndObjectEntry(3, value1))); + + CHECK(cache.put(NumberAndObjectEntry(4, value1))); + CHECK(cache.putNew(NumberAndObjectEntry(5, value1))); + + CHECK(cache.putNew(NumberAndObjectLookup(6, value1), + NumberAndObjectEntry(6, value1))); + + CHECK(SweepCacheAndFinishGC(cx, cache)); + + CHECK(cache.count() == 6); + CHECK(cache.has(NumberAndObjectLookup(1, value1))); + CHECK(cache.has(NumberAndObjectLookup(2, value1))); + CHECK(cache.has(NumberAndObjectLookup(3, value1))); + CHECK(cache.has(NumberAndObjectLookup(4, value1))); + CHECK(cache.has(NumberAndObjectLookup(5, value1))); + CHECK(cache.has(NumberAndObjectLookup(6, value1))); + + return true; +} + +bool TestReplaceDyingInMap() { + // Test replacing dying entries with ones that have the same key using the + // various APIs. + + using Cache = WeakCache<GCHashMap<uint32_t, HeapPtr<JSObject*>, + DefaultHasher<uint32_t>, TempAllocPolicy>>; + Cache cache(JS::GetObjectZone(global), cx); + + RootedObject value1(cx, JS_NewPlainObject(cx)); + RootedObject value2(cx, JS_NewPlainObject(cx)); + CHECK(value1); + CHECK(value2); + + CHECK(cache.put(1, value1)); + CHECK(cache.put(2, value2)); + CHECK(cache.put(3, value2)); + CHECK(cache.put(4, value2)); + CHECK(cache.put(5, value2)); + CHECK(cache.put(6, value2)); + + value2 = nullptr; + CHECK(GCUntilCacheSweep(cx, cache)); + + CHECK(!cache.has(2)); + CHECK(!cache.has(3)); + CHECK(!cache.has(4)); + CHECK(!cache.has(5)); + CHECK(!cache.has(6)); + + auto ptr = cache.lookupForAdd(2); + CHECK(!ptr); + CHECK(cache.add(ptr, 2, value1)); + + auto ptr2 = cache.lookupForAdd(3); + CHECK(!ptr2); + CHECK(cache.add(ptr2, 3, HeapPtr<JSObject*>())); + + auto ptr3 = cache.lookupForAdd(4); + CHECK(!ptr3); + CHECK(cache.relookupOrAdd(ptr3, 4, value1)); + + CHECK(cache.put(5, value1)); + CHECK(cache.putNew(6, value1)); + + CHECK(SweepCacheAndFinishGC(cx, cache)); + + CHECK(cache.count() == 6); + CHECK(cache.lookup(1)->value() == value1); + CHECK(cache.lookup(2)->value() == value1); + CHECK(cache.lookup(3)->value() == nullptr); + CHECK(cache.lookup(4)->value() == value1); + CHECK(cache.lookup(5)->value() == value1); + CHECK(cache.lookup(6)->value() == value1); + + return true; +} + +bool TestUniqueIDLookups() { + // Test hash table lookups during incremental sweeping where the hash is + // generated based on a unique ID. The problem is that the unique ID table + // will have already been swept by this point so looking up a dead pointer + // in the table will fail. This lookup happens if we try to match a live key + // against a dead table entry with the same hash code. + + const size_t DeadFactor = 3; + const size_t ObjectCount = 100; + + using Cache = WeakCache< + GCHashSet<ObjectEntry, StableCellHasher<ObjectEntry>, TempAllocPolicy>>; + Cache cache(JS::GetObjectZone(global), cx); + + Rooted<GCVector<JSObject*, 0, SystemAllocPolicy>> liveObjects(cx); + + for (size_t j = 0; j < ObjectCount; j++) { + JSObject* obj = JS_NewPlainObject(cx); + CHECK(obj); + CHECK(cache.put(obj)); + if (j % DeadFactor == 0) { + CHECK(liveObjects.get().append(obj)); + } + } + + CHECK(cache.count() == ObjectCount); + + CHECK(GCUntilCacheSweep(cx, cache)); + + for (size_t j = 0; j < liveObjects.length(); j++) { + CHECK(cache.has(liveObjects[j])); + } + + CHECK(SweepCacheAndFinishGC(cx, cache)); + + CHECK(cache.count() == liveObjects.length()); + + return true; +} + +END_TEST(testIncrementalWeakCacheSweeping) + +#endif // defined JS_GC_ZEAL diff --git a/js/src/jsapi-tests/testGetPropertyDescriptor.cpp b/js/src/jsapi-tests/testGetPropertyDescriptor.cpp new file mode 100644 index 0000000000..f5a76b5b6a --- /dev/null +++ b/js/src/jsapi-tests/testGetPropertyDescriptor.cpp @@ -0,0 +1,54 @@ +/* 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 "js/CallAndConstruct.h" // JS::IsCallable +#include "js/PropertyAndElement.h" +#include "js/PropertyDescriptor.h" // JS::FromPropertyDescriptor, JS_GetPropertyDescriptor +#include "js/RootingAPI.h" +#include "jsapi-tests/tests.h" + +BEGIN_TEST(test_GetPropertyDescriptor) { + JS::RootedValue v(cx); + EVAL("({ somename : 123 })", &v); + CHECK(v.isObject()); + + JS::RootedObject obj(cx, &v.toObject()); + JS::Rooted<mozilla::Maybe<JS::PropertyDescriptor>> desc(cx); + JS::RootedObject holder(cx); + + CHECK(JS_GetPropertyDescriptor(cx, obj, "somename", &desc, &holder)); + CHECK(desc.isSome()); + CHECK_SAME(desc->value(), JS::Int32Value(123)); + + JS::RootedValue descValue(cx); + CHECK(JS::FromPropertyDescriptor(cx, desc, &descValue)); + CHECK(descValue.isObject()); + JS::RootedObject descObj(cx, &descValue.toObject()); + JS::RootedValue value(cx); + CHECK(JS_GetProperty(cx, descObj, "value", &value)); + CHECK_EQUAL(value.toInt32(), 123); + CHECK(JS_GetProperty(cx, descObj, "get", &value)); + CHECK(value.isUndefined()); + CHECK(JS_GetProperty(cx, descObj, "set", &value)); + CHECK(value.isUndefined()); + CHECK(JS_GetProperty(cx, descObj, "writable", &value)); + CHECK(value.isTrue()); + CHECK(JS_GetProperty(cx, descObj, "configurable", &value)); + CHECK(value.isTrue()); + CHECK(JS_GetProperty(cx, descObj, "enumerable", &value)); + CHECK(value.isTrue()); + + CHECK(JS_GetPropertyDescriptor(cx, obj, "not-here", &desc, &holder)); + CHECK(desc.isNothing()); + + CHECK(JS_GetPropertyDescriptor(cx, obj, "toString", &desc, &holder)); + JS::RootedObject objectProto(cx, JS::GetRealmObjectPrototype(cx)); + CHECK(objectProto); + CHECK_EQUAL(holder, objectProto); + CHECK(desc->value().isObject()); + CHECK(JS::IsCallable(&desc->value().toObject())); + + return true; +} +END_TEST(test_GetPropertyDescriptor) diff --git a/js/src/jsapi-tests/testHashTable.cpp b/js/src/jsapi-tests/testHashTable.cpp new file mode 100644 index 0000000000..4cd55f4cba --- /dev/null +++ b/js/src/jsapi-tests/testHashTable.cpp @@ -0,0 +1,564 @@ +/* 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 "mozilla/HashFunctions.h" + +#include <utility> + +#include "ds/OrderedHashTable.h" +#include "js/HashTable.h" +#include "js/Utility.h" +#include "jsapi-tests/tests.h" + +// #define FUZZ + +typedef js::HashMap<uint32_t, uint32_t, js::DefaultHasher<uint32_t>, + js::SystemAllocPolicy> + IntMap; +typedef js::HashSet<uint32_t, js::DefaultHasher<uint32_t>, + js::SystemAllocPolicy> + IntSet; + +/* + * The rekeying test as conducted by adding only keys masked with 0x0000FFFF + * that are unique. We rekey by shifting left 16 bits. + */ +#ifdef FUZZ +const size_t TestSize = 0x0000FFFF / 2; +const size_t TestIterations = SIZE_MAX; +#else +const size_t TestSize = 10000; +const size_t TestIterations = 10; +#endif + +static_assert(TestSize <= 0x0000FFFF / 2); + +struct LowToHigh { + static uint32_t rekey(uint32_t initial) { + if (initial > uint32_t(0x0000FFFF)) { + return initial; + } + return initial << 16; + } + + static bool shouldBeRemoved(uint32_t initial) { return false; } +}; + +struct LowToHighWithRemoval { + static uint32_t rekey(uint32_t initial) { + if (initial > uint32_t(0x0000FFFF)) { + return initial; + } + return initial << 16; + } + + static bool shouldBeRemoved(uint32_t initial) { + if (initial >= 0x00010000) { + return (initial >> 16) % 2 == 0; + } + return initial % 2 == 0; + } +}; + +static bool MapsAreEqual(IntMap& am, IntMap& bm) { + bool equal = true; + if (am.count() != bm.count()) { + equal = false; + fprintf(stderr, "A.count() == %u and B.count() == %u\n", am.count(), + bm.count()); + } + for (auto iter = am.iter(); !iter.done(); iter.next()) { + if (!bm.has(iter.get().key())) { + equal = false; + fprintf(stderr, "B does not have %x which is in A\n", iter.get().key()); + } + } + for (auto iter = bm.iter(); !iter.done(); iter.next()) { + if (!am.has(iter.get().key())) { + equal = false; + fprintf(stderr, "A does not have %x which is in B\n", iter.get().key()); + } + } + return equal; +} + +static bool SetsAreEqual(IntSet& am, IntSet& bm) { + bool equal = true; + if (am.count() != bm.count()) { + equal = false; + fprintf(stderr, "A.count() == %u and B.count() == %u\n", am.count(), + bm.count()); + } + for (auto iter = am.iter(); !iter.done(); iter.next()) { + if (!bm.has(iter.get())) { + equal = false; + fprintf(stderr, "B does not have %x which is in A\n", iter.get()); + } + } + for (auto iter = bm.iter(); !iter.done(); iter.next()) { + if (!am.has(iter.get())) { + equal = false; + fprintf(stderr, "A does not have %x which is in B\n", iter.get()); + } + } + return equal; +} + +static bool AddLowKeys(IntMap* am, IntMap* bm, int seed) { + size_t i = 0; + srand(seed); + while (i < TestSize) { + uint32_t n = rand() & 0x0000FFFF; + if (!am->has(n)) { + if (bm->has(n)) { + return false; + } + + if (!am->putNew(n, n) || !bm->putNew(n, n)) { + return false; + } + i++; + } + } + return true; +} + +static bool AddLowKeys(IntSet* as, IntSet* bs, int seed) { + size_t i = 0; + srand(seed); + while (i < TestSize) { + uint32_t n = rand() & 0x0000FFFF; + if (!as->has(n)) { + if (bs->has(n)) { + return false; + } + if (!as->putNew(n) || !bs->putNew(n)) { + return false; + } + i++; + } + } + return true; +} + +template <class NewKeyFunction> +static bool SlowRekey(IntMap* m) { + IntMap tmp; + + for (auto iter = m->iter(); !iter.done(); iter.next()) { + if (NewKeyFunction::shouldBeRemoved(iter.get().key())) { + continue; + } + uint32_t hi = NewKeyFunction::rekey(iter.get().key()); + if (tmp.has(hi)) { + return false; + } + if (!tmp.putNew(hi, iter.get().value())) { + return false; + } + } + + m->clear(); + for (auto iter = tmp.iter(); !iter.done(); iter.next()) { + if (!m->putNew(iter.get().key(), iter.get().value())) { + return false; + } + } + + return true; +} + +template <class NewKeyFunction> +static bool SlowRekey(IntSet* s) { + IntSet tmp; + + for (auto iter = s->iter(); !iter.done(); iter.next()) { + if (NewKeyFunction::shouldBeRemoved(iter.get())) { + continue; + } + uint32_t hi = NewKeyFunction::rekey(iter.get()); + if (tmp.has(hi)) { + return false; + } + if (!tmp.putNew(hi)) { + return false; + } + } + + s->clear(); + for (auto iter = tmp.iter(); !iter.done(); iter.next()) { + if (!s->putNew(iter.get())) { + return false; + } + } + + return true; +} + +BEGIN_TEST(testHashRekeyManual) { + IntMap am, bm; + for (size_t i = 0; i < TestIterations; ++i) { +#ifdef FUZZ + fprintf(stderr, "map1: %lu\n", i); +#endif + CHECK(AddLowKeys(&am, &bm, i)); + CHECK(MapsAreEqual(am, bm)); + + for (auto iter = am.modIter(); !iter.done(); iter.next()) { + uint32_t tmp = LowToHigh::rekey(iter.get().key()); + if (tmp != iter.get().key()) { + iter.rekey(tmp); + } + } + CHECK(SlowRekey<LowToHigh>(&bm)); + + CHECK(MapsAreEqual(am, bm)); + am.clear(); + bm.clear(); + } + + IntSet as, bs; + for (size_t i = 0; i < TestIterations; ++i) { +#ifdef FUZZ + fprintf(stderr, "set1: %lu\n", i); +#endif + CHECK(AddLowKeys(&as, &bs, i)); + CHECK(SetsAreEqual(as, bs)); + + for (auto iter = as.modIter(); !iter.done(); iter.next()) { + uint32_t tmp = LowToHigh::rekey(iter.get()); + if (tmp != iter.get()) { + iter.rekey(tmp); + } + } + CHECK(SlowRekey<LowToHigh>(&bs)); + + CHECK(SetsAreEqual(as, bs)); + as.clear(); + bs.clear(); + } + + return true; +} +END_TEST(testHashRekeyManual) + +BEGIN_TEST(testHashRekeyManualRemoval) { + IntMap am, bm; + for (size_t i = 0; i < TestIterations; ++i) { +#ifdef FUZZ + fprintf(stderr, "map2: %lu\n", i); +#endif + CHECK(AddLowKeys(&am, &bm, i)); + CHECK(MapsAreEqual(am, bm)); + + for (auto iter = am.modIter(); !iter.done(); iter.next()) { + if (LowToHighWithRemoval::shouldBeRemoved(iter.get().key())) { + iter.remove(); + } else { + uint32_t tmp = LowToHighWithRemoval::rekey(iter.get().key()); + if (tmp != iter.get().key()) { + iter.rekey(tmp); + } + } + } + CHECK(SlowRekey<LowToHighWithRemoval>(&bm)); + + CHECK(MapsAreEqual(am, bm)); + am.clear(); + bm.clear(); + } + + IntSet as, bs; + for (size_t i = 0; i < TestIterations; ++i) { +#ifdef FUZZ + fprintf(stderr, "set1: %lu\n", i); +#endif + CHECK(AddLowKeys(&as, &bs, i)); + CHECK(SetsAreEqual(as, bs)); + + for (auto iter = as.modIter(); !iter.done(); iter.next()) { + if (LowToHighWithRemoval::shouldBeRemoved(iter.get())) { + iter.remove(); + } else { + uint32_t tmp = LowToHighWithRemoval::rekey(iter.get()); + if (tmp != iter.get()) { + iter.rekey(tmp); + } + } + } + CHECK(SlowRekey<LowToHighWithRemoval>(&bs)); + + CHECK(SetsAreEqual(as, bs)); + as.clear(); + bs.clear(); + } + + return true; +} +END_TEST(testHashRekeyManualRemoval) + +// A type that is not copyable, only movable. +struct MoveOnlyType { + uint32_t val; + + explicit MoveOnlyType(uint32_t val) : val(val) {} + + MoveOnlyType(MoveOnlyType&& rhs) { val = rhs.val; } + + MoveOnlyType& operator=(MoveOnlyType&& rhs) { + MOZ_ASSERT(&rhs != this); + this->~MoveOnlyType(); + new (this) MoveOnlyType(std::move(rhs)); + return *this; + } + + struct HashPolicy { + typedef MoveOnlyType Lookup; + + static js::HashNumber hash(const Lookup& lookup) { return lookup.val; } + + static bool match(const MoveOnlyType& existing, const Lookup& lookup) { + return existing.val == lookup.val; + } + }; + + private: + MoveOnlyType(const MoveOnlyType&) = delete; + MoveOnlyType& operator=(const MoveOnlyType&) = delete; +}; + +BEGIN_TEST(testHashSetOfMoveOnlyType) { + typedef js::HashSet<MoveOnlyType, MoveOnlyType::HashPolicy, + js::SystemAllocPolicy> + Set; + + Set set; + + MoveOnlyType a(1); + + CHECK(set.put(std::move(a))); // This shouldn't generate a compiler error. + + return true; +} +END_TEST(testHashSetOfMoveOnlyType) + +#if defined(DEBUG) + +// Add entries to a HashMap until either we get an OOM, or the table has been +// resized a few times. +static bool GrowUntilResize() { + IntMap m; + + // Add entries until we've resized the table four times. + size_t lastCapacity = m.capacity(); + size_t resizes = 0; + uint32_t key = 0; + while (resizes < 4) { + auto p = m.lookupForAdd(key); + if (!p && !m.add(p, key, 0)) { + return false; // OOM'd in lookupForAdd() or add() + } + + size_t capacity = m.capacity(); + if (capacity != lastCapacity) { + resizes++; + lastCapacity = capacity; + } + key++; + } + + return true; +} + +BEGIN_TEST(testHashMapGrowOOM) { + uint32_t timeToFail; + for (timeToFail = 1; timeToFail < 1000; timeToFail++) { + js::oom::simulator.simulateFailureAfter( + js::oom::FailureSimulator::Kind::OOM, timeToFail, js::THREAD_TYPE_MAIN, + false); + GrowUntilResize(); + } + + js::oom::simulator.reset(); + return true; +} + +END_TEST(testHashMapGrowOOM) +#endif // defined(DEBUG) + +BEGIN_TEST(testHashTableMovableModIterator) { + IntSet set; + + // Exercise returning a hash table ModIterator object from a function. + + CHECK(set.put(1)); + for (auto iter = setModIter(set); !iter.done(); iter.next()) { + iter.remove(); + } + CHECK(set.count() == 0); + + // Test moving an ModIterator object explicitly. + + CHECK(set.put(1)); + CHECK(set.put(2)); + CHECK(set.put(3)); + CHECK(set.count() == 3); + { + auto i1 = set.modIter(); + CHECK(!i1.done()); + i1.remove(); + i1.next(); + + auto i2 = std::move(i1); + CHECK(!i2.done()); + i2.remove(); + i2.next(); + } + + CHECK(set.count() == 1); + return true; +} + +IntSet::ModIterator setModIter(IntSet& set) { return set.modIter(); } + +END_TEST(testHashTableMovableModIterator) + +BEGIN_TEST(testHashLazyStorage) { + // The following code depends on the current capacity computation, which + // could change in the future. + uint32_t defaultCap = 32; + uint32_t minCap = 4; + + IntSet set; + CHECK(set.capacity() == 0); + + CHECK(set.put(1)); + CHECK(set.capacity() == defaultCap); + + set.compact(); // effectively a no-op + CHECK(set.capacity() == minCap); + + set.clear(); + CHECK(set.capacity() == minCap); + + set.compact(); + CHECK(set.capacity() == 0); + + CHECK(set.putNew(1)); + CHECK(set.capacity() == minCap); + + set.clear(); + set.compact(); + CHECK(set.capacity() == 0); + + auto p = set.lookupForAdd(1); + CHECK(set.capacity() == 0); + CHECK(set.add(p, 1)); + CHECK(set.capacity() == minCap); + CHECK(set.has(1)); + + set.clear(); + set.compact(); + CHECK(set.capacity() == 0); + + p = set.lookupForAdd(1); + CHECK(set.putNew(2)); + CHECK(set.capacity() == minCap); + CHECK(set.relookupOrAdd(p, 1, 1)); + CHECK(set.capacity() == minCap); + CHECK(set.has(1)); + + set.clear(); + set.compact(); + CHECK(set.capacity() == 0); + + CHECK(set.putNew(1)); + p = set.lookupForAdd(1); + set.clear(); + set.compact(); + CHECK(set.count() == 0); + CHECK(set.relookupOrAdd(p, 1, 1)); + CHECK(set.count() == 1); + CHECK(set.capacity() == minCap); + + set.clear(); + set.compact(); + CHECK(set.capacity() == 0); + + CHECK(set.reserve(0)); // a no-op + CHECK(set.capacity() == 0); + + CHECK(set.reserve(1)); + CHECK(set.capacity() == minCap); + + CHECK(set.reserve(0)); // a no-op + CHECK(set.capacity() == minCap); + + CHECK(set.reserve(2)); // effectively a no-op + CHECK(set.capacity() == minCap); + + // No need to clear here because we didn't add anything. + set.compact(); + CHECK(set.capacity() == 0); + + CHECK(set.reserve(128)); + CHECK(set.capacity() == 256); + CHECK(set.reserve(3)); // effectively a no-op + CHECK(set.capacity() == 256); + for (int i = 0; i < 8; i++) { + CHECK(set.putNew(i)); + } + CHECK(set.count() == 8); + CHECK(set.capacity() == 256); + set.compact(); + CHECK(set.capacity() == 16); + set.compact(); // effectively a no-op + CHECK(set.capacity() == 16); + for (int i = 8; i < 16; i++) { + CHECK(set.putNew(i)); + } + CHECK(set.count() == 16); + CHECK(set.capacity() == 32); + set.clear(); + CHECK(set.capacity() == 32); + set.compact(); + CHECK(set.capacity() == 0); + + // Lowest length for which reserve() will fail. + static const uint32_t toobig = (1 << 29) + 1; + CHECK(!set.reserve(toobig)); + CHECK(set.capacity() == 0); // unchanged + CHECK(set.reserve(16)); + CHECK(set.capacity() == 32); + + return true; +} +END_TEST(testHashLazyStorage) + +BEGIN_TEST(testOrderedHashSetWithoutInit) { + { + struct NonzeroUint32HashPolicy { + using Lookup = uint32_t; + static js::HashNumber hash(const Lookup& v, + const mozilla::HashCodeScrambler& hcs) { + return mozilla::HashGeneric(v); + } + static bool match(const uint32_t& k, const Lookup& l) { return k == l; } + static bool isEmpty(const uint32_t& v) { return v == 0; } + static void makeEmpty(uint32_t* v) { *v = 0; } + }; + + using OHS = js::OrderedHashSet<uint32_t, NonzeroUint32HashPolicy, + js::SystemAllocPolicy>; + + OHS set(js::SystemAllocPolicy(), mozilla::HashCodeScrambler(17, 42)); + CHECK(set.count() == 0); + + // This test passes if the set is safely destructible even when |init()| is + // never called. + } + + return true; +} +END_TEST(testOrderedHashSetWithoutInit) diff --git a/js/src/jsapi-tests/testIndexToString.cpp b/js/src/jsapi-tests/testIndexToString.cpp new file mode 100644 index 0000000000..9074a4267c --- /dev/null +++ b/js/src/jsapi-tests/testIndexToString.cpp @@ -0,0 +1,120 @@ +/* -*- 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 "jsnum.h" + +#include "jsapi-tests/tests.h" +#include "vm/JSContext.h" +#include "vm/Realm.h" +#include "vm/StaticStrings.h" + +#include "vm/StringType-inl.h" + +static const struct TestPair { + uint32_t num; + const char* expected; +} tests[] = { + {0, "0"}, + {1, "1"}, + {2, "2"}, + {9, "9"}, + {10, "10"}, + {15, "15"}, + {16, "16"}, + {17, "17"}, + {99, "99"}, + {100, "100"}, + {255, "255"}, + {256, "256"}, + {257, "257"}, + {999, "999"}, + {1000, "1000"}, + {4095, "4095"}, + {4096, "4096"}, + {9999, "9999"}, + {1073741823, "1073741823"}, + {1073741824, "1073741824"}, + {1073741825, "1073741825"}, + {2147483647, "2147483647"}, + {2147483648u, "2147483648"}, + {2147483649u, "2147483649"}, + {4294967294u, "4294967294"}, +}; + +BEGIN_TEST(testIndexToString) { + for (const auto& test : tests) { + uint32_t u = test.num; + JSString* str = js::IndexToString(cx, u); + CHECK(str); + + if (!js::StaticStrings::hasUint(u)) { + CHECK(cx->realm()->dtoaCache.lookup(10, u) == str); + } + + bool match = false; + CHECK(JS_StringEqualsAscii(cx, str, test.expected, &match)); + CHECK(match); + } + + return true; +} +END_TEST(testIndexToString) + +BEGIN_TEST(testStringIsIndex) { + for (const auto& test : tests) { + uint32_t u = test.num; + JSLinearString* str = js::IndexToString(cx, u); + CHECK(str); + + uint32_t n; + CHECK(str->isIndex(&n)); + CHECK(u == n); + } + + return true; +} +END_TEST(testStringIsIndex) + +BEGIN_TEST(testStringToPropertyName) { + uint32_t index; + + static const char16_t hiChars[] = {'h', 'i'}; + JSLinearString* hiStr = NewString(cx, hiChars); + CHECK(hiStr); + CHECK(!hiStr->isIndex(&index)); + CHECK(hiStr->toPropertyName(cx) != nullptr); + + static const char16_t maxChars[] = {'4', '2', '9', '4', '9', + '6', '7', '2', '9', '4'}; + JSLinearString* maxStr = NewString(cx, maxChars); + CHECK(maxStr); + CHECK(maxStr->isIndex(&index)); + CHECK(index == UINT32_MAX - 1); + + static const char16_t maxPlusOneChars[] = {'4', '2', '9', '4', '9', + '6', '7', '2', '9', '5'}; + JSLinearString* maxPlusOneStr = NewString(cx, maxPlusOneChars); + CHECK(maxPlusOneStr); + CHECK(!maxPlusOneStr->isIndex(&index)); + CHECK(maxPlusOneStr->toPropertyName(cx) != nullptr); + + static const char16_t maxNonUint32Chars[] = {'4', '2', '9', '4', '9', + '6', '7', '2', '9', '6'}; + JSLinearString* maxNonUint32Str = NewString(cx, maxNonUint32Chars); + CHECK(maxNonUint32Str); + CHECK(!maxNonUint32Str->isIndex(&index)); + CHECK(maxNonUint32Str->toPropertyName(cx) != nullptr); + + return true; +} + +template <size_t N> +static JSLinearString* NewString(JSContext* cx, const char16_t (&chars)[N]) { + return js::NewStringCopyN<js::CanGC>(cx, chars, N); +} + +END_TEST(testStringToPropertyName) diff --git a/js/src/jsapi-tests/testInformalValueTypeName.cpp b/js/src/jsapi-tests/testInformalValueTypeName.cpp new file mode 100644 index 0000000000..37b73ccc08 --- /dev/null +++ b/js/src/jsapi-tests/testInformalValueTypeName.cpp @@ -0,0 +1,43 @@ +/* -*- 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 "js/Array.h" // JS::NewArrayObject +#include "js/Symbol.h" +#include "jsapi-tests/tests.h" + +BEGIN_TEST(testInformalValueTypeName) { + JS::RootedValue v1(cx, JS::ObjectOrNullValue(JS_NewPlainObject(cx))); + CHECK(strcmp(JS::InformalValueTypeName(v1), "Object") == 0); + + JS::RootedValue v2(cx, JS::ObjectOrNullValue(JS::NewArrayObject(cx, 0))); + CHECK(strcmp(JS::InformalValueTypeName(v2), "Array") == 0); + + JS::RootedValue v3(cx, JS::StringValue(JS_GetEmptyString(cx))); + CHECK(strcmp(JS::InformalValueTypeName(v3), "string") == 0); + + JS::RootedValue v4(cx, JS::SymbolValue(JS::NewSymbol(cx, nullptr))); + CHECK(strcmp(JS::InformalValueTypeName(v4), "symbol") == 0); + + JS::RootedValue v5(cx, JS::NumberValue(50.5)); + CHECK(strcmp(JS::InformalValueTypeName(v5), "number") == 0); + JS::RootedValue v6(cx, JS::Int32Value(50)); + CHECK(strcmp(JS::InformalValueTypeName(v6), "number") == 0); + JS::RootedValue v7(cx, JS::Float32Value(50.5)); + CHECK(strcmp(JS::InformalValueTypeName(v7), "number") == 0); + + JS::RootedValue v8(cx, JS::TrueValue()); + CHECK(strcmp(JS::InformalValueTypeName(v8), "boolean") == 0); + + JS::RootedValue v9(cx, JS::NullValue()); + CHECK(strcmp(JS::InformalValueTypeName(v9), "null") == 0); + + JS::RootedValue v10(cx, JS::UndefinedValue()); + CHECK(strcmp(JS::InformalValueTypeName(v10), "undefined") == 0); + + return true; +} +END_TEST(testInformalValueTypeName) diff --git a/js/src/jsapi-tests/testIntString.cpp b/js/src/jsapi-tests/testIntString.cpp new file mode 100644 index 0000000000..8733e669ec --- /dev/null +++ b/js/src/jsapi-tests/testIntString.cpp @@ -0,0 +1,49 @@ +/* -*- 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" + +BEGIN_TEST(testIntString_bug515273) { + JS::RootedValue v(cx); + + EVAL("'1';", &v); + JSString* str = v.toString(); + CHECK(JS_StringHasBeenPinned(cx, str)); + CHECK(JS_LinearStringEqualsLiteral(JS_ASSERT_STRING_IS_LINEAR(str), "1")); + + EVAL("'42';", &v); + str = v.toString(); + CHECK(JS_StringHasBeenPinned(cx, str)); + CHECK(JS_LinearStringEqualsLiteral(JS_ASSERT_STRING_IS_LINEAR(str), "42")); + + // Short string literal should use atom. + EVAL("'111';", &v); + str = v.toString(); + CHECK(JS_StringHasBeenPinned(cx, str)); + CHECK(JS_LinearStringEqualsLiteral(JS_ASSERT_STRING_IS_LINEAR(str), "111")); + + // Long string literal shouldn't use atom, but just linear string. + EVAL("'111222333';", &v); + str = v.toString(); + CHECK(!JS_StringHasBeenPinned(cx, str)); + CHECK(JS_LinearStringEqualsLiteral(JS_ASSERT_STRING_IS_LINEAR(str), + "111222333")); + + /* Test other types of static strings. */ + EVAL("'a';", &v); + str = v.toString(); + CHECK(JS_StringHasBeenPinned(cx, str)); + CHECK(JS_LinearStringEqualsLiteral(JS_ASSERT_STRING_IS_LINEAR(str), "a")); + + EVAL("'bc';", &v); + str = v.toString(); + CHECK(JS_StringHasBeenPinned(cx, str)); + CHECK(JS_LinearStringEqualsLiteral(JS_ASSERT_STRING_IS_LINEAR(str), "bc")); + + return true; +} +END_TEST(testIntString_bug515273) diff --git a/js/src/jsapi-tests/testIntern.cpp b/js/src/jsapi-tests/testIntern.cpp new file mode 100644 index 0000000000..4c3719ecf6 --- /dev/null +++ b/js/src/jsapi-tests/testIntern.cpp @@ -0,0 +1,51 @@ +/* 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 "gc/GCContext.h" +#include "gc/Marking.h" +#include "jsapi-tests/tests.h" +#include "util/Text.h" +#include "vm/JSAtomUtils.h" // Atomize +#include "vm/StringType.h" + +BEGIN_TEST(testAtomizedIsNotPinned) { + /* Try to pick a string that won't be interned by other tests in this runtime. + */ + static const char someChars[] = "blah blah blah? blah blah blah"; + JS::Rooted<JSAtom*> atom(cx, + js::Atomize(cx, someChars, js_strlen(someChars))); + CHECK(!JS_StringHasBeenPinned(cx, atom)); + + JS::RootedString string(cx, JS_AtomizeAndPinString(cx, someChars)); + CHECK(string); + CHECK(string == atom); + + CHECK(JS_StringHasBeenPinned(cx, atom)); + return true; +} +END_TEST(testAtomizedIsNotPinned) + +struct StringWrapperStruct { + JSString* str; + bool strOk; +} sw; + +BEGIN_TEST(testPinAcrossGC) { + sw.str = JS_AtomizeAndPinString( + cx, "wrapped chars that another test shouldn't be using"); + sw.strOk = false; + CHECK(sw.str); + JS_AddFinalizeCallback(cx, FinalizeCallback, nullptr); + JS_GC(cx); + CHECK(sw.strOk); + return true; +} + +static void FinalizeCallback(JS::GCContext* gcx, JSFinalizeStatus status, + void* data) { + if (status == JSFINALIZE_GROUP_START) { + sw.strOk = js::gc::IsMarkedUnbarriered(gcx->runtime(), sw.str); + } +} +END_TEST(testPinAcrossGC) diff --git a/js/src/jsapi-tests/testIntlAvailableLocales.cpp b/js/src/jsapi-tests/testIntlAvailableLocales.cpp new file mode 100644 index 0000000000..b3d16eb758 --- /dev/null +++ b/js/src/jsapi-tests/testIntlAvailableLocales.cpp @@ -0,0 +1,68 @@ +/* -*- 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 "js/LocaleSensitive.h" +#include "jsapi-tests/tests.h" + +BEGIN_TEST(testIntlAvailableLocales) { + JSRuntime* rt = JS_GetRuntime(cx); + + // This test should only attempt to run if we have Intl support. + JS::Rooted<JS::Value> haveIntl(cx); + EVAL("typeof Intl !== 'undefined'", &haveIntl); + if (!haveIntl.toBoolean()) { + return true; + } + + // Assumption: our Intl support always includes "az" (Azerbaijani) support, + // and our Intl support *does not* natively support az-Cyrl-AZ. + CHECK(JS_SetDefaultLocale(rt, "az-Cyrl-AZ")); + + EXEC( + "if (Intl.Collator().resolvedOptions().locale !== " + "'az-Cyrl-AZ') \n" + " throw 'unexpected default locale';"); + EXEC( + "var used = Intl.Collator('az-Cyrl').resolvedOptions().locale; \n" + "if (used !== 'az-Cyrl') \n" + " throw 'bad locale when using truncated default: ' + used;"); + EXEC( + "if (Intl.Collator('az').resolvedOptions().locale !== 'az') \n" + " throw 'bad locale when using more-truncated default';"); + EXEC( + "if (Intl.Collator('az-Cyrl-US').resolvedOptions().locale !== 'az-Cyrl') " + "\n" + " throw 'unexpected default locale';"); + + EXEC( + "if (Intl.Collator('az-Cyrl-AZ', { localeMatcher: 'lookup' " + "}).resolvedOptions().locale !== \n" + " 'az-Cyrl-AZ') \n" + "{ \n" + " throw 'unexpected default locale with lookup matcher'; \n" + "}"); + + CHECK(JS_SetDefaultLocale(rt, "en-US-u-co-phonebk")); + EXEC( + "if (Intl.Collator().resolvedOptions().locale !== 'en-US') \n" + " throw 'unexpected default locale where proposed default included a " + "Unicode extension';"); + + CHECK(JS_SetDefaultLocale(rt, "this is not a language tag at all, yo")); + + EXEC( + "if (Intl.Collator().resolvedOptions().locale !== 'en-GB') \n" + " throw 'unexpected last-ditch locale';"); + EXEC( + "if (Intl.Collator('en-GB').resolvedOptions().locale !== 'en-GB') \n" + " throw 'unexpected used locale when specified, with last-ditch " + "locale as default';"); + + JS_ResetDefaultLocale(rt); + return true; +} +END_TEST(testIntlAvailableLocales) diff --git a/js/src/jsapi-tests/testIsISOStyleDate.cpp b/js/src/jsapi-tests/testIsISOStyleDate.cpp new file mode 100644 index 0000000000..64f0516535 --- /dev/null +++ b/js/src/jsapi-tests/testIsISOStyleDate.cpp @@ -0,0 +1,79 @@ +/* -*- 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 "js/Date.h" +#include "jsapi-tests/tests.h" + +const char* VALID_DATES[] = { + "2009", + "2009-05", + "2009-05-19", + "2022-02-29", + "2009T15:00", + "2009-05T15:00", + "2022-06-31T15:00", + "2009-05-19T15:00", + "2009-05-19T15:00:15", + "2009-05-19T15:00-00:00", + "2009-05-19T15:00:15.452", + "2009-05-19T15:00:15.452Z", + "2009-05-19T15:00:15.452+02:00", + "2009-05-19T15:00:15.452-02:00", + "-271821-04-20T00:00:00Z", + "+000000-01-01T00:00:00Z", +}; + +const char* INVALID_DATES[] = { + "10", + "20009", + "+20009", + "2009-", + "2009-0", + "2009-15", + "2009-02-1", + "2009-02-50", + "15:00", + "T15:00", + "9-05-19T15:00", + "2009-5-19T15:00", + "2009-05-1T15:00", + "2009-02-10T15", + "2009-05-19T15:", + "2009-05-19T1:00", + "2009-05-19T10:1", + "2009-05-19T60:00", + "2009-05-19T15:70", + "2009-05-19T15:00.25", + "2009-05-19+10:00", + "2009-05-19Z", + "2009-05-19 15:00", + "2009-05-19t15:00Z", + "2009-05-19T15:00z", + "2009-05-19T15:00+01", + "2009-05-19T10:10+1:00", + "2009-05-19T10:10+01:1", + "2009-05-19T15:00+75:00", + "2009-05-19T15:00+02:80", + "02009-05-19T15:00", +}; + +BEGIN_TEST(testIsISOStyleDate_success) { + for (const char* date : VALID_DATES) { + CHECK(ValidDate(date)); + } + for (const char* date : INVALID_DATES) { + CHECK(!ValidDate(date)); + } + + return true; +} + +bool ValidDate(const char* str) { + return JS::IsISOStyleDate(cx, JS::Latin1Chars(str, strlen(str))); +} + +END_TEST(testIsISOStyleDate_success) diff --git a/js/src/jsapi-tests/testIsInsideNursery.cpp b/js/src/jsapi-tests/testIsInsideNursery.cpp new file mode 100644 index 0000000000..793a1ed3cc --- /dev/null +++ b/js/src/jsapi-tests/testIsInsideNursery.cpp @@ -0,0 +1,58 @@ +/* -*- 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 "vm/JSContext-inl.h" + +BEGIN_TEST(testIsInsideNursery) { + /* Non-GC things are never inside the nursery. */ + CHECK(!cx->nursery().isInside(cx)); + CHECK(!cx->nursery().isInside((void*)nullptr)); + + // Skip test if some part of the nursery is disabled (via env var, for + // example.) + if (!cx->zone()->allocNurseryObjects() || + !cx->zone()->allocNurseryStrings()) { + return true; + } + + JS_GC(cx); + + JS::Rooted<JSObject*> object(cx, JS_NewPlainObject(cx)); + const char oolstr[] = + "my hands are floppy dark red pieces of liver, large " + "enough to exceed the space of an inline string"; + JS::Rooted<JSString*> string(cx, JS_NewStringCopyZ(cx, oolstr)); + + /* Objects are initially allocated in the nursery. */ + CHECK(js::gc::IsInsideNursery(object)); + /* As are strings. */ + CHECK(js::gc::IsInsideNursery(string)); + /* And their contents. */ + { + JS::AutoCheckCannotGC nogc; + const JS::Latin1Char* strdata = + string->asLinear().nonInlineLatin1Chars(nogc); + CHECK(cx->nursery().isInside(strdata)); + } + + JS_GC(cx); + + /* And are tenured if still live after a GC. */ + CHECK(!js::gc::IsInsideNursery(object)); + CHECK(!js::gc::IsInsideNursery(string)); + { + JS::AutoCheckCannotGC nogc; + const JS::Latin1Char* strdata = + string->asLinear().nonInlineLatin1Chars(nogc); + CHECK(!cx->nursery().isInside(strdata)); + } + + return true; +} +END_TEST(testIsInsideNursery) diff --git a/js/src/jsapi-tests/testIteratorObject.cpp b/js/src/jsapi-tests/testIteratorObject.cpp new file mode 100644 index 0000000000..71ac16dabb --- /dev/null +++ b/js/src/jsapi-tests/testIteratorObject.cpp @@ -0,0 +1,34 @@ +/* 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 "js/Class.h" // js::ESClass +#include "js/Object.h" // JS::GetBuiltinClass +#include "js/RootingAPI.h" // JS::Rooted +#include "js/Value.h" // JS::Value +#include "jsapi-tests/tests.h" + +BEGIN_TEST(testIteratorObject) { + using js::ESClass; + + JS::Rooted<JS::Value> result(cx); + + EVAL("new Map([['key1', 'value1'], ['key2', 'value2']]).entries()", &result); + + CHECK(result.isObject()); + JS::Rooted<JSObject*> obj1(cx, &result.toObject()); + ESClass class1 = ESClass::Other; + CHECK(JS::GetBuiltinClass(cx, obj1, &class1)); + CHECK(class1 == ESClass::MapIterator); + + EVAL("new Set(['value1', 'value2']).entries()", &result); + + CHECK(result.isObject()); + JS::Rooted<JSObject*> obj2(cx, &result.toObject()); + ESClass class2 = ESClass::Other; + CHECK(JS::GetBuiltinClass(cx, obj2, &class2)); + CHECK(class2 == ESClass::SetIterator); + + return true; +} +END_TEST(testIteratorObject) diff --git a/js/src/jsapi-tests/testJSEvaluateScript.cpp b/js/src/jsapi-tests/testJSEvaluateScript.cpp new file mode 100644 index 0000000000..1dfc9ea3cb --- /dev/null +++ b/js/src/jsapi-tests/testJSEvaluateScript.cpp @@ -0,0 +1,38 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + */ + +#include "js/CompilationAndEvaluation.h" +#include "js/PropertyAndElement.h" // JS_AlreadyHasOwnProperty, JS_HasProperty +#include "js/SourceText.h" +#include "jsapi-tests/tests.h" +#include "util/Text.h" + +BEGIN_TEST(testJSEvaluateScript) { + JS::RootedObject obj(cx, JS_NewPlainObject(cx)); + CHECK(obj); + + static const char16_t src[] = u"var x = 5;"; + + JS::RootedValue retval(cx); + JS::CompileOptions opts(cx); + JS::RootedObjectVector scopeChain(cx); + CHECK(scopeChain.append(obj)); + + JS::SourceText<char16_t> srcBuf; + CHECK(srcBuf.init(cx, src, js_strlen(src), JS::SourceOwnership::Borrowed)); + + CHECK(JS::Evaluate(cx, scopeChain, opts.setFileAndLine(__FILE__, __LINE__), + srcBuf, &retval)); + + bool hasProp = true; + CHECK(JS_AlreadyHasOwnProperty(cx, obj, "x", &hasProp)); + CHECK(hasProp); + + hasProp = false; + CHECK(JS_HasProperty(cx, global, "x", &hasProp)); + CHECK(!hasProp); + + return true; +} +END_TEST(testJSEvaluateScript) diff --git a/js/src/jsapi-tests/testJSON.cpp b/js/src/jsapi-tests/testJSON.cpp new file mode 100644 index 0000000000..5c33e86368 --- /dev/null +++ b/js/src/jsapi-tests/testJSON.cpp @@ -0,0 +1,112 @@ +/* -*- 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 <limits> +#include <string.h> + +#include "js/Array.h" // JS::IsArrayObject +#include "js/Exception.h" +#include "js/friend/ErrorMessages.h" // JSMSG_* +#include "js/JSON.h" +#include "js/MemoryFunctions.h" +#include "js/Printf.h" +#include "js/PropertyAndElement.h" // JS_GetProperty +#include "jsapi-tests/tests.h" + +using namespace JS; + +static bool Cmp(const char16_t* buffer, uint32_t length, + const char16_t* expected) { + uint32_t i = 0; + while (*expected) { + if (i >= length || *expected != *buffer) { + return false; + } + ++expected; + ++buffer; + ++i; + } + return i == length; +} + +static bool Callback(const char16_t* buffer, uint32_t length, void* data) { + const char16_t** data_ = static_cast<const char16_t**>(data); + const char16_t* expected = *data_; + *data_ = nullptr; + return Cmp(buffer, length, expected); +} + +BEGIN_TEST(testToJSON_same) { + // Primitives + Rooted<Value> input(cx); + input = JS::TrueValue(); + CHECK(TryToJSON(cx, input, u"true")); + + input = JS::FalseValue(); + CHECK(TryToJSON(cx, input, u"false")); + + input = JS::NullValue(); + CHECK(TryToJSON(cx, input, u"null")); + + input.setInt32(0); + CHECK(TryToJSON(cx, input, u"0")); + + input.setInt32(1); + CHECK(TryToJSON(cx, input, u"1")); + + input.setInt32(-1); + CHECK(TryToJSON(cx, input, u"-1")); + + input.setDouble(1); + CHECK(TryToJSON(cx, input, u"1")); + + input.setDouble(1.75); + CHECK(TryToJSON(cx, input, u"1.75")); + + input.setDouble(9e9); + CHECK(TryToJSON(cx, input, u"9000000000")); + + input.setDouble(std::numeric_limits<double>::infinity()); + CHECK(TryToJSON(cx, input, u"null")); + return true; +} + +bool TryToJSON(JSContext* cx, Handle<Value> value, const char16_t* expected) { + { + Rooted<Value> v(cx, value); + const char16_t* expected_ = expected; + CHECK(JS_Stringify(cx, &v, nullptr, JS::NullHandleValue, Callback, + &expected_)); + CHECK_NULL(expected_); + } + { + const char16_t* expected_ = expected; + CHECK(JS::ToJSON(cx, value, nullptr, JS::NullHandleValue, Callback, + &expected_)); + CHECK_NULL(expected_); + } + return true; +} +END_TEST(testToJSON_same) + +BEGIN_TEST(testToJSON_different) { + Rooted<Symbol*> symbol(cx, NewSymbol(cx, nullptr)); + Rooted<Value> value(cx, SymbolValue(symbol)); + + CHECK(JS::ToJSON(cx, value, nullptr, JS::NullHandleValue, UnreachedCallback, + nullptr)); + const char16_t* expected = u"null"; + CHECK(JS_Stringify(cx, &value, nullptr, JS::NullHandleValue, Callback, + &expected)); + CHECK_NULL(expected); + return true; +} + +static bool UnreachedCallback(const char16_t*, uint32_t, void*) { + MOZ_CRASH("Should not call the callback"); +} + +END_TEST(testToJSON_different) diff --git a/js/src/jsapi-tests/testJitABIcalls.cpp b/js/src/jsapi-tests/testJitABIcalls.cpp new file mode 100644 index 0000000000..1bd0468aa5 --- /dev/null +++ b/js/src/jsapi-tests/testJitABIcalls.cpp @@ -0,0 +1,714 @@ +/* -*- 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 "mozilla/FloatingPoint.h" +#include "mozilla/IntegerTypeTraits.h" + +#include <iterator> + +#include "jit/ABIFunctions.h" +#include "jit/IonAnalysis.h" +#include "jit/Linker.h" +#include "jit/MacroAssembler.h" +#include "jit/MIRGenerator.h" +#include "jit/MIRGraph.h" +#include "jit/ValueNumbering.h" +#include "jit/VMFunctions.h" +#include "js/Value.h" + +#include "jsapi-tests/tests.h" +#include "jsapi-tests/testsJit.h" + +#include "jit/ABIFunctionList-inl.h" +#include "jit/MacroAssembler-inl.h" +#include "jit/VMFunctionList-inl.h" + +using namespace js; +using namespace js::jit; + +// This test case relies on VMFUNCTION_LIST, ABIFUNCTION_LIST, +// ABIFUNCTION_AND_TYPE_LIST and ABIFUNCTIONSIG_LIST, to create a test case for +// each function registered, in order to check if the arguments are properly +// being interpreted after a call from the JIT. +// +// This test checks that the interpretation of the C++ compiler matches the +// interpretation of the JIT. It works by generating a call to a function which +// has the same signature as the tested function. The function being called +// re-interprets the arguments' content to ensure that it matches the content +// given as arguments by the JIT. +// +// These tests cases succeed if the content provided by the JIT matches the +// content read by the C++ code. Otherwise, a failure implies that either the +// MacroAssembler is not used properly, or that the code used by the JIT to +// generate the function call does not match the ABI of the targeted system. + +// Convert the content of each macro list to a single and unique format which is +// (Name, Type). +#define ABIFUN_TO_ALLFUN(Fun) (#Fun, decltype(&::Fun)) +#define ABIFUN_AND_SIG_TO_ALLFUN(Fun, Sig) (#Fun " as " #Sig, Sig) +#define ABISIG_TO_ALLFUN(Sig) ("(none) as " #Sig, Sig) +#define VMFUN_TO_ALLFUN(Name, Fun, Pop...) (#Fun, decltype(&::Fun)) + +#define APPLY(A, B) A B + +// Generate macro calls for all the lists which are used to allow, directly or +// indirectly, calls performed with callWithABI. +// +// This macro will delegate to a different macro call based on the type of the +// list the element is extracted from. +#define ALL_FUNCTIONS(PREFIX) \ + ABIFUNCTION_LIST(PREFIX##_ABIFUN_TO_ALLFUN) \ + ABIFUNCTION_AND_TYPE_LIST(PREFIX##_ABIFUN_AND_SIG_TO_ALLFUN) \ + ABIFUNCTIONSIG_LIST(PREFIX##_ABISIG_TO_ALLFUN) \ + VMFUNCTION_LIST(PREFIX##_VMFUN_TO_ALLFUN) + +// sizeof(const T&) is not equal to sizeof(const T*), but references are passed +// as pointers. +// +// "When applied to a reference or a reference type, the result is the size of +// the referenced type." [expr.sizeof] (5.3.3.2) +// +// The following functions avoid this issue by wrapping the type in a structure +// which will share the same property, even if the wrapped type is a reference. +template <typename T> +constexpr size_t ActualSizeOf() { + struct Wrapper { + T _unused; + }; + return sizeof(Wrapper); +} + +template <typename T> +constexpr size_t ActualAlignOf() { + struct Wrapper { + T _unused; + }; + return alignof(Wrapper); +} + +// Given a type, return the integer type which has the same size. +template <typename T> +using IntTypeOf_t = + typename mozilla::UnsignedStdintTypeForSize<ActualSizeOf<T>()>::Type; + +// Concatenate 2 std::integer_sequence, and return an std::integer_sequence with +// the content of both parameters. +template <typename Before, typename After> +struct Concat; + +template <typename Int, Int... Before, Int... After> +struct Concat<std::integer_sequence<Int, Before...>, + std::integer_sequence<Int, After...>> { + using type = std::integer_sequence<Int, Before..., After...>; +}; + +template <typename Before, typename After> +using Concat_t = typename Concat<Before, After>::type; + +static_assert(std::is_same_v<Concat_t<std::integer_sequence<uint8_t, 1, 2>, + std::integer_sequence<uint8_t, 3, 4>>, + std::integer_sequence<uint8_t, 1, 2, 3, 4>>); + +// Generate an std::integer_sequence of `N` elements, where each element is an +// uint8_t integer with value `Value`. +template <size_t N, uint8_t Value> +constexpr auto CstSeq() { + if constexpr (N == 0) { + return std::integer_sequence<uint8_t>{}; + } else { + return Concat_t<std::integer_sequence<uint8_t, Value>, + decltype(CstSeq<N - 1, Value>())>{}; + } +} + +template <size_t N, uint8_t Value> +using CstSeq_t = decltype(CstSeq<N, Value>()); + +static_assert( + std::is_same_v<CstSeq_t<4, 2>, std::integer_sequence<uint8_t, 2, 2, 2, 2>>); + +// Computes the number of bytes to add before a type in order to align it in +// memory. +constexpr size_t PadBytes(size_t size, size_t align) { + return (align - (size % align)) % align; +} + +// Request a minimum alignment for the values added to a buffer in order to +// account for the read size used by the MoveOperand given as an argument of +// passWithABI. The MoveOperand does not take into consideration the size of +// the data being transfered, and might load a larger amount of data. +// +// This function ensures that the MoveOperand would read the 0x55 padding added +// after each value, when it reads too much. +constexpr size_t AtLeastSize() { return sizeof(uintptr_t); } + +// Returns the size which needs to be added in addition to the memory consumed +// by the type, from which the size if given as argument. +template <typename Type> +constexpr size_t BackPadBytes() { + return std::max(AtLeastSize(), ActualSizeOf<Type>()) - ActualSizeOf<Type>(); +} + +// Adds the padding and the reserved size for storing a value in a buffer which +// can be read by a MoveOperand. +template <typename Type> +constexpr size_t PadSize(size_t size) { + return PadBytes(size, ActualAlignOf<Type>()) + ActualSizeOf<Type>() + + BackPadBytes<Type>(); +} + +// Generate an std::integer_sequence of 0:uint8_t elements of the size of the +// padding needed to align a type in memory. +template <size_t Align, size_t CurrSize> +using PadSeq_t = decltype(CstSeq<PadBytes(CurrSize, Align), 0>()); + +static_assert(std::is_same_v<PadSeq_t<4, 0>, std::integer_sequence<uint8_t>>); +static_assert( + std::is_same_v<PadSeq_t<4, 3>, std::integer_sequence<uint8_t, 0>>); +static_assert( + std::is_same_v<PadSeq_t<4, 2>, std::integer_sequence<uint8_t, 0, 0>>); +static_assert( + std::is_same_v<PadSeq_t<4, 1>, std::integer_sequence<uint8_t, 0, 0, 0>>); + +// Spread an integer value `Value` into a new std::integer_sequence of `N` +// uint8_t elements, using Little Endian ordering of bytes. +template <size_t N, uint64_t Value, uint8_t... Rest> +constexpr auto FillLESeq() { + if constexpr (N == 0) { + return std::integer_sequence<uint8_t, Rest...>{}; + } else { + return FillLESeq<N - 1, (Value >> 8), Rest..., uint8_t(Value & 0xff)>(); + } +} + +template <size_t N, uint64_t Value> +using FillSeq_t = decltype(FillLESeq<N, Value>()); + +static_assert(std::is_same_v<FillSeq_t<4, 2>, + std::integer_sequence<uint8_t, 2, 0, 0, 0>>); + +// Given a list of template parameters, generate an std::integer_sequence of +// size_t, where each element is 1 larger than the previous one. The generated +// sequence starts at 0. +template <typename... Args> +using ArgsIndexes_t = + std::make_integer_sequence<uint64_t, uint64_t(sizeof...(Args))>; + +static_assert(std::is_same_v<ArgsIndexes_t<uint8_t, uint64_t>, + std::integer_sequence<uint64_t, 0, 1>>); + +// Extract a single bit for each element of an std::integer_sequence. This is +// used to work around some restrictions with providing boolean arguments, +// which might be truncated to a single bit. +template <size_t Bit, typename IntSeq> +struct ExtractBit; + +template <size_t Bit, uint64_t... Values> +struct ExtractBit<Bit, std::integer_sequence<uint64_t, Values...>> { + using type = std::integer_sequence<uint64_t, (Values >> Bit) & 1 ...>; +}; + +// Generate an std::integer_sequence of indexes which are filtered for a single +// bit, such that it can be used with boolean types. +template <size_t Bit, typename... Args> +using ArgsBitOfIndexes_t = + typename ExtractBit<Bit, ArgsIndexes_t<Args...>>::type; + +static_assert(std::is_same_v<ArgsBitOfIndexes_t<0, int, int, int, int>, + std::integer_sequence<uint64_t, 0, 1, 0, 1>>); +static_assert(std::is_same_v<ArgsBitOfIndexes_t<1, int, int, int, int>, + std::integer_sequence<uint64_t, 0, 0, 1, 1>>); + +// Compute the offset of each argument in a buffer produced by GenArgsBuffer, +// this is used to fill the MoveOperand displacement field when loading value +// out of the buffer produced by GenArgsBuffer. +template <uint64_t Size, typename... Args> +struct ArgsOffsets; + +template <uint64_t Size> +struct ArgsOffsets<Size> { + using type = std::integer_sequence<uint64_t>; +}; + +template <uint64_t Size, typename Arg, typename... Args> +struct ArgsOffsets<Size, Arg, Args...> { + using type = + Concat_t<std::integer_sequence< + uint64_t, Size + PadBytes(Size, ActualAlignOf<Arg>())>, + typename ArgsOffsets<Size + PadSize<Arg>(Size), Args...>::type>; +}; + +template <uint64_t Size, typename... Args> +using ArgsOffsets_t = typename ArgsOffsets<Size, Args...>::type; + +// Not all 32bits architecture align uint64_t type on 8 bytes, so check the +// validity of the stored content based on the alignment of the architecture. +static_assert(ActualAlignOf<uint64_t>() != 8 || + std::is_same_v<ArgsOffsets_t<0, uint8_t, uint64_t, bool>, + std::integer_sequence<uint64_t, 0, 8, 16>>); + +static_assert(ActualAlignOf<uint64_t>() != 4 || + std::is_same_v<ArgsOffsets_t<0, uint8_t, uint64_t, bool>, + std::integer_sequence<uint64_t, 0, 4, 12>>); + +// Generate an std::integer_sequence containing the size of each argument in +// memory. +template <typename... Args> +using ArgsSizes_t = std::integer_sequence<uint64_t, ActualSizeOf<Args>()...>; + +// Generate an std::integer_sequence containing values where all valid bits for +// each type are set to 1. +template <typename Type> +constexpr uint64_t FillBits() { + constexpr uint64_t topBit = uint64_t(1) << ((8 * ActualSizeOf<Type>()) - 1); + if constexpr (std::is_same_v<Type, bool>) { + return uint64_t(1); + } else if constexpr (std::is_same_v<Type, double> || + std::is_same_v<Type, float>) { + // A NaN has all the bits of its exponent set to 1. The CPU / C++ does not + // garantee keeping the payload of NaN values, when interpreted as floating + // point, which could cause some random failures. This removes one bit from + // the exponent, such that the floating point value is not converted to a + // canonicalized NaN by the time we compare it. + constexpr uint64_t lowExpBit = + uint64_t(1) << mozilla::FloatingPoint<Type>::kExponentShift; + return uint64_t((topBit - 1) + topBit - lowExpBit); + } else { + // Note: Keep parentheses to avoid unwanted overflow. + return uint64_t((topBit - 1) + topBit); + } +} + +template <typename... Args> +using ArgsFillBits_t = std::integer_sequence<uint64_t, FillBits<Args>()...>; + +// Given a type, return the ABIType used by passABIArg to know how to +// interpret the value which are given as arguments. +template <typename Type> +constexpr ABIType TypeToABIType() { + if constexpr (std::is_same_v<Type, float>) { + return ABIType::Float32; + } else if constexpr (std::is_same_v<Type, double>) { + return ABIType::Float64; + } else { + return ABIType::General; + } +} + +// Generate a sequence which contains the associated ABIType of each argument. +// Note, a new class is defined because C++ header of clang are rejecting the +// option of having an enumerated type as argument of std::integer_sequence. +template <ABIType... Val> +class ABITypeSequence {}; + +template <typename... Args> +using ArgsABITypes_t = ABITypeSequence<TypeToABIType<Args>()...>; + +// Generate an std::integer_sequence which corresponds to a buffer containing +// values which are spread at the location where each arguments type would be +// stored in a buffer. +template <typename Buffer, typename Values, typename... Args> +struct ArgsBuffer; + +template <uint8_t... Buffer, typename Arg, typename... Args, uint64_t Val, + uint64_t... Values> +struct ArgsBuffer<std::integer_sequence<uint8_t, Buffer...>, + std::integer_sequence<uint64_t, Val, Values...>, Arg, + Args...> { + using type = typename ArgsBuffer< + Concat_t<std::integer_sequence<uint8_t, Buffer...>, + Concat_t<PadSeq_t<ActualAlignOf<Arg>(), sizeof...(Buffer)>, + Concat_t<FillSeq_t<ActualSizeOf<Arg>(), Val>, + CstSeq_t<BackPadBytes<Arg>(), 0x55>>>>, + std::integer_sequence<uint64_t, Values...>, Args...>::type; +}; + +template <typename Buffer> +struct ArgsBuffer<Buffer, std::integer_sequence<uint64_t>> { + using type = Buffer; +}; + +template <typename Values, typename... Args> +using ArgsBuffer_t = + typename ArgsBuffer<std::integer_sequence<uint8_t>, Values, Args...>::type; + +// NOTE: The representation of the boolean might be surprising in this test +// case, see AtLeastSize function for an explanation. +#ifdef JS_64BIT +static_assert(sizeof(uintptr_t) == 8); +static_assert( + std::is_same_v< + ArgsBuffer_t<std::integer_sequence<uint64_t, 42, 51>, uint64_t, bool>, + std::integer_sequence<uint8_t, 42, 0, 0, 0, 0, 0, 0, 0, 51, 0x55, 0x55, + 0x55, 0x55, 0x55, 0x55, 0x55>>); +static_assert( + std::is_same_v< + ArgsBuffer_t<std::integer_sequence<uint64_t, 0xffffffff, 0xffffffff>, + uint8_t, uint16_t>, + std::integer_sequence<uint8_t, 0xff, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, + 0x55, 0xff, 0xff, 0x55, 0x55, 0x55, 0x55, 0x55, + 0x55>>); +#else +static_assert(sizeof(uintptr_t) == 4); +static_assert( + std::is_same_v< + ArgsBuffer_t<std::integer_sequence<uint64_t, 42, 51>, uint64_t, bool>, + std::integer_sequence<uint8_t, 42, 0, 0, 0, 0, 0, 0, 0, 51, 0x55, 0x55, + 0x55>>); +static_assert( + std::is_same_v< + ArgsBuffer_t<std::integer_sequence<uint64_t, 0xffffffff, 0xffffffff>, + uint8_t, uint16_t>, + std::integer_sequence<uint8_t, 0xff, 0x55, 0x55, 0x55, 0xff, 0xff, 0x55, + 0x55>>); +#endif + +// Test used to check if any of the types given as template parameters are a +// `bool`, which is a corner case where a raw integer might be truncated by the +// C++ compiler. +template <typename... Args> +constexpr bool AnyBool_v = (std::is_same_v<Args, bool> || ...); + +// Instantiate an std::integer_sequence as a buffer which is readable and +// addressable at runtime, for reading argument values from the generated code. +template <typename Seq> +struct InstanceSeq; + +template <typename Int, Int... Values> +struct InstanceSeq<std::integer_sequence<Int, Values...>> { + static constexpr Int table[sizeof...(Values)] = {Values...}; + static constexpr size_t size = sizeof...(Values); +}; + +// Instantiate a buffer for testing the position of arguments when calling a +// function. +template <typename... Args> +using TestArgsPositions = + InstanceSeq<ArgsBuffer_t<ArgsIndexes_t<Args...>, Args...>>; + +// Instantiate a buffer for testing the position of arguments, one bit at a +// time, when calling a function. +template <size_t Bit, typename... Args> +using TestArgsBitOfPositions = + InstanceSeq<ArgsBuffer_t<ArgsBitOfIndexes_t<Bit, Args...>, Args...>>; + +// Instantiate a buffer to check that the size of each argument is interpreted +// correctly when calling a function. +template <typename... Args> +using TestArgsSizes = InstanceSeq<ArgsBuffer_t<ArgsSizes_t<Args...>, Args...>>; + +// Instantiate a buffer to check that all bits of each argument goes through. +template <typename... Args> +using TestArgsFillBits = + InstanceSeq<ArgsBuffer_t<ArgsFillBits_t<Args...>, Args...>>; + +// ConvertToInt returns the raw value of any argument as an integer value which +// can be compared with the expected values. +template <typename Type> +IntTypeOf_t<Type> ConvertToInt(Type v) { + // Simplify working with types by casting the address of the value to the + // equivalent `const void*`. + auto address = reinterpret_cast<const void*>(&v); + // Convert the `void*` to an integer pointer of the same size as the input + // type, and return the raw value stored in the integer interpretation. + static_assert(ActualSizeOf<Type>() == ActualSizeOf<IntTypeOf_t<Type>>()); + if constexpr (std::is_reference_v<Type>) { + return reinterpret_cast<const IntTypeOf_t<Type>>(address); + } else { + return *reinterpret_cast<const IntTypeOf_t<Type>*>(address); + } +} + +// Attributes used to disable some parts of Undefined Behavior sanitizer. This +// is needed to keep the signature identical to what is used in production, +// instead of working around these limitations. +// +// * no_sanitize("enum"): Enumerated values given as arguments are checked to +// see if the value given as argument matches any of the enumerated values. +// The patterns used to check whether the values are correctly transmitted +// from the JIT to C++ might go beyond the set of enumerated values, and +// break this sanitizer check. +#if defined(__clang__) && defined(__has_attribute) && \ + __has_attribute(no_sanitize) +# define NO_ARGS_CHECKS __attribute__((no_sanitize("enum"))) +#else +# define NO_ARGS_CHECKS +#endif + +// Check if the raw values of arguments are equal to the numbers given in the +// std::integer_sequence given as the first argument. +template <typename... Args, typename Int, Int... Val> +NO_ARGS_CHECKS bool CheckArgsEqual(JSAPIRuntimeTest* instance, int lineno, + std::integer_sequence<Int, Val...>, + Args... args) { + return (instance->checkEqual(ConvertToInt<Args>(args), IntTypeOf_t<Args>(Val), + "ConvertToInt<Args>(args)", + "IntTypeOf_t<Args>(Val)", __FILE__, lineno) && + ...); +} + +// Generate code to register the value of each argument of the called function. +// Each argument's content is read from a buffer whose address is stored in the +// `base` register. The offsets of arguments are given as a third argument +// which is expected to be generated by `ArgsOffsets`. The ABIType types of +// arguments are given as the fourth argument and are expected to be generated +// by `ArgsABIType`. +template <uint64_t... Off, ABIType... Type> +static void passABIArgs(MacroAssembler& masm, Register base, + std::integer_sequence<uint64_t, Off...>, + ABITypeSequence<Type...>) { + (masm.passABIArg(MoveOperand(base, size_t(Off)), Type), ...); +} + +// For each function type given as a parameter, create a few functions with the +// given type, to be called by the JIT code produced by `generateCalls`. These +// functions report the result through the instance registered with the +// `set_instance` function, as we cannot add extra arguments to these functions. +template <typename Type> +struct DefineCheckArgs; + +template <typename Res, typename... Args> +struct DefineCheckArgs<Res (*)(Args...)> { + void set_instance(JSAPIRuntimeTest* instance, bool* reportTo) { + MOZ_ASSERT((!instance_) != (!instance)); + instance_ = instance; + MOZ_ASSERT((!reportTo_) != (!reportTo)); + reportTo_ = reportTo; + } + static void report(bool value) { *reportTo_ = *reportTo_ && value; } + + // Check that arguments are interpreted in the same order at compile time and + // at runtime. + static NO_ARGS_CHECKS Res CheckArgsPositions(Args... args) { + AutoUnsafeCallWithABI unsafe; + using Indexes = std::index_sequence_for<Args...>; + report(CheckArgsEqual<Args...>(instance_, __LINE__, Indexes(), + std::forward<Args>(args)...)); + return Res(); + } + + // This is the same test as above, but some compilers might clean the boolean + // values using `& 1` operations, which corrupt the operand index, thus to + // properly check for the position of boolean operands, we have to check the + // position of the boolean operand using a single bit at a time. + static NO_ARGS_CHECKS Res CheckArgsBitOfPositions0(Args... args) { + AutoUnsafeCallWithABI unsafe; + using Indexes = ArgsBitOfIndexes_t<0, Args...>; + report(CheckArgsEqual<Args...>(instance_, __LINE__, Indexes(), + std::forward<Args>(args)...)); + return Res(); + } + + static NO_ARGS_CHECKS Res CheckArgsBitOfPositions1(Args... args) { + AutoUnsafeCallWithABI unsafe; + using Indexes = ArgsBitOfIndexes_t<1, Args...>; + report(CheckArgsEqual<Args...>(instance_, __LINE__, Indexes(), + std::forward<Args>(args)...)); + return Res(); + } + + static NO_ARGS_CHECKS Res CheckArgsBitOfPositions2(Args... args) { + AutoUnsafeCallWithABI unsafe; + using Indexes = ArgsBitOfIndexes_t<2, Args...>; + report(CheckArgsEqual<Args...>(instance_, __LINE__, Indexes(), + std::forward<Args>(args)...)); + return Res(); + } + + static NO_ARGS_CHECKS Res CheckArgsBitOfPositions3(Args... args) { + AutoUnsafeCallWithABI unsafe; + using Indexes = ArgsBitOfIndexes_t<3, Args...>; + report(CheckArgsEqual<Args...>(instance_, __LINE__, Indexes(), + std::forward<Args>(args)...)); + return Res(); + } + + // Check that the compile time and run time sizes of each argument are the + // same. + static NO_ARGS_CHECKS Res CheckArgsSizes(Args... args) { + AutoUnsafeCallWithABI unsafe; + using Sizes = ArgsSizes_t<Args...>; + report(CheckArgsEqual<Args...>(instance_, __LINE__, Sizes(), + std::forward<Args>(args)...)); + return Res(); + } + + // Check that the compile time and run time all bits of each argument are + // correctly passed through. + static NO_ARGS_CHECKS Res CheckArgsFillBits(Args... args) { + AutoUnsafeCallWithABI unsafe; + using FillBits = ArgsFillBits_t<Args...>; + report(CheckArgsEqual<Args...>(instance_, __LINE__, FillBits(), + std::forward<Args>(args)...)); + return Res(); + } + + using FunType = Res (*)(Args...); + struct Test { + const uint8_t* buffer; + const size_t size; + const FunType fun; + }; + + // Generate JIT code for calling the above test functions where each argument + // is given a different raw value that can be compared by each called + // function. + void generateCalls(MacroAssembler& masm, Register base, Register setup) { + using ArgsPositions = TestArgsPositions<Args...>; + using ArgsBitOfPositions0 = TestArgsBitOfPositions<0, Args...>; + using ArgsBitOfPositions1 = TestArgsBitOfPositions<1, Args...>; + using ArgsBitOfPositions2 = TestArgsBitOfPositions<2, Args...>; + using ArgsBitOfPositions3 = TestArgsBitOfPositions<3, Args...>; + using ArgsSizes = TestArgsSizes<Args...>; + using ArgsFillBits = TestArgsFillBits<Args...>; + static const Test testsWithoutBoolArgs[3] = { + {ArgsPositions::table, ArgsPositions::size, CheckArgsPositions}, + {ArgsSizes::table, ArgsSizes::size, CheckArgsSizes}, + {ArgsFillBits::table, ArgsFillBits::size, CheckArgsFillBits}, + }; + static const Test testsWithBoolArgs[6] = { + {ArgsBitOfPositions0::table, ArgsBitOfPositions0::size, + CheckArgsBitOfPositions0}, + {ArgsBitOfPositions1::table, ArgsBitOfPositions1::size, + CheckArgsBitOfPositions1}, + {ArgsBitOfPositions2::table, ArgsBitOfPositions2::size, + CheckArgsBitOfPositions2}, + {ArgsBitOfPositions3::table, ArgsBitOfPositions3::size, + CheckArgsBitOfPositions3}, + {ArgsSizes::table, ArgsSizes::size, CheckArgsSizes}, + {ArgsFillBits::table, ArgsFillBits::size, CheckArgsFillBits}, + }; + const Test* tests = testsWithoutBoolArgs; + size_t numTests = std::size(testsWithoutBoolArgs); + if (AnyBool_v<Args...>) { + tests = testsWithBoolArgs; + numTests = std::size(testsWithBoolArgs); + } + + for (size_t i = 0; i < numTests; i++) { + const Test& test = tests[i]; + masm.movePtr(ImmPtr(test.buffer), base); + + masm.setupUnalignedABICall(setup); + using Offsets = ArgsOffsets_t<0, Args...>; + using ABITypes = ArgsABITypes_t<Args...>; + passABIArgs(masm, base, Offsets(), ABITypes()); + masm.callWithABI(DynFn{JS_FUNC_TO_DATA_PTR(void*, test.fun)}, + TypeToABIType<Res>(), + CheckUnsafeCallWithABI::DontCheckOther); + } + } + + private: + // As we are checking specific function signature, we cannot add extra + // parameters, thus we rely on static variables to pass the value of the + // instance that we are testing. + static JSAPIRuntimeTest* instance_; + static bool* reportTo_; +}; + +template <typename Res, typename... Args> +JSAPIRuntimeTest* DefineCheckArgs<Res (*)(Args...)>::instance_ = nullptr; + +template <typename Res, typename... Args> +bool* DefineCheckArgs<Res (*)(Args...)>::reportTo_ = nullptr; + +// This is a child class of JSAPIRuntimeTest, which is used behind the scenes to +// register test cases in jsapi-tests. Each instance of it creates a new test +// case. This class is specialized with the type of the function to check, and +// initialized with the name of the function with the given signature. +// +// When executed, it generates the JIT code to call functions with the same +// signature and checks that the JIT interpretation of arguments location +// matches the C++ interpretation. If it differs, the test case will fail. +template <typename Sig> +class JitABICall final : public JSAPIRuntimeTest, public DefineCheckArgs<Sig> { + public: + explicit JitABICall(const char* name) : name_(name) { reuseGlobal = true; } + virtual const char* name() override { return name_; } + virtual bool run(JS::HandleObject) override { + bool result = true; + this->set_instance(this, &result); + + TempAllocator temp(&cx->tempLifoAlloc()); + JitContext jcx(cx); + StackMacroAssembler masm(cx, temp); + AutoCreatedBy acb(masm, __func__); + PrepareJit(masm); + + AllocatableGeneralRegisterSet regs(GeneralRegisterSet::All()); + // Initialize the base register the same way this is done while reading + // arguments in generateVMWrapper, in order to avoid MOZ_RELEASE_ASSERT in + // the MoveResolver. +#if defined(JS_CODEGEN_X86) + Register base = regs.takeAny(); +#elif defined(JS_CODEGEN_X64) + Register base = r10; + regs.take(base); +#elif defined(JS_CODEGEN_ARM) + Register base = r5; + regs.take(base); +#elif defined(JS_CODEGEN_ARM64) + Register base = r8; + regs.take(base); +#elif defined(JS_CODEGEN_MIPS32) + Register base = t1; + regs.take(base); +#elif defined(JS_CODEGEN_MIPS64) + Register base = t1; + regs.take(base); +#elif defined(JS_CODEGEN_LOONG64) + Register base = t0; + regs.take(base); +#elif defined(JS_CODEGEN_RISCV64) + Register base = t0; + regs.take(base); +#else +# error "Unknown architecture!" +#endif + + Register setup = regs.takeAny(); + + this->generateCalls(masm, base, setup); + + if (!ExecuteJit(cx, masm)) { + return false; + } + this->set_instance(nullptr, nullptr); + return result; + }; + + private: + const char* name_; +}; + +// GCC warns when the signature does not have matching attributes (for example +// [[nodiscard]]). Squelch this warning to avoid a GCC-only footgun. +#if MOZ_IS_GCC +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wignored-attributes" +#endif + +// For each VMFunction and ABIFunction, create an instance of a JitABICall +// class to register a jsapi-tests test case. +#define TEST_INSTANCE(Name, Sig) \ + static JitABICall<Sig> MOZ_CONCAT(MOZ_CONCAT(cls_jitabicall, __COUNTER__), \ + _instance)("JIT ABI for " Name); +#define TEST_INSTANCE_ABIFUN_TO_ALLFUN(...) \ + APPLY(TEST_INSTANCE, ABIFUN_TO_ALLFUN(__VA_ARGS__)) +#define TEST_INSTANCE_ABIFUN_AND_SIG_TO_ALLFUN(...) \ + APPLY(TEST_INSTANCE, ABIFUN_AND_SIG_TO_ALLFUN(__VA_ARGS__)) +#define TEST_INSTANCE_ABISIG_TO_ALLFUN(...) \ + APPLY(TEST_INSTANCE, ABISIG_TO_ALLFUN(__VA_ARGS__)) +#define TEST_INSTANCE_VMFUN_TO_ALLFUN(...) \ + APPLY(TEST_INSTANCE, VMFUN_TO_ALLFUN(__VA_ARGS__)) + +ALL_FUNCTIONS(TEST_INSTANCE) + +#if MOZ_IS_GCC +# pragma GCC diagnostic pop +#endif diff --git a/js/src/jsapi-tests/testJitDCEinGVN.cpp b/js/src/jsapi-tests/testJitDCEinGVN.cpp new file mode 100644 index 0000000000..286d36aa14 --- /dev/null +++ b/js/src/jsapi-tests/testJitDCEinGVN.cpp @@ -0,0 +1,147 @@ +/* -*- 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 "jit/IonAnalysis.h" +#include "jit/MIRGenerator.h" +#include "jit/MIRGraph.h" +#include "jit/ValueNumbering.h" + +#include "jsapi-tests/testJitMinimalFunc.h" +#include "jsapi-tests/tests.h" + +using namespace js; +using namespace js::jit; + +BEGIN_TEST(testJitDCEinGVN_ins) { + MinimalFunc func; + MBasicBlock* block = func.createEntryBlock(); + + // mul0 = p * p + // mul1 = mul0 * mul0 + // return p + MParameter* p = func.createParameter(); + block->add(p); + MMul* mul0 = MMul::New(func.alloc, p, p, MIRType::Double); + block->add(mul0); + if (!mul0->typePolicy()->adjustInputs(func.alloc, mul0)) { + return false; + } + MMul* mul1 = MMul::New(func.alloc, mul0, mul0, MIRType::Double); + block->add(mul1); + if (!mul1->typePolicy()->adjustInputs(func.alloc, mul1)) { + return false; + } + MReturn* ret = MReturn::New(func.alloc, p); + block->end(ret); + + if (!func.runGVN()) { + return false; + } + + // mul0 and mul1 should be deleted. + for (MInstructionIterator ins = block->begin(); ins != block->end(); ins++) { + CHECK(!ins->isMul() || + (ins->getOperand(0) != p && ins->getOperand(1) != p)); + CHECK(!ins->isMul() || + (ins->getOperand(0) != mul0 && ins->getOperand(1) != mul0)); + } + return true; +} +END_TEST(testJitDCEinGVN_ins) + +BEGIN_TEST(testJitDCEinGVN_phi) { + MinimalFunc func; + MBasicBlock* block = func.createEntryBlock(); + MBasicBlock* thenBlock1 = func.createBlock(block); + MBasicBlock* thenBlock2 = func.createBlock(block); + MBasicBlock* elifBlock = func.createBlock(block); + MBasicBlock* elseBlock = func.createBlock(block); + MBasicBlock* joinBlock = func.createBlock(block); + + // if (p) { + // x = 1.0; + // y = 3.0; + // } else if (q) { + // x = 2.0; + // y = 4.0; + // } else { + // x = 1.0; + // y = 5.0; + // } + // x = phi(1.0, 2.0, 1.0); + // y = phi(3.0, 4.0, 5.0); + // z = x * y; + // return y; + + MConstant* c1 = MConstant::New(func.alloc, DoubleValue(1.0)); + block->add(c1); + MPhi* x = MPhi::New(func.alloc); + MPhi* y = MPhi::New(func.alloc); + + // if (p) { + MParameter* p = func.createParameter(); + block->add(p); + block->end(MTest::New(func.alloc, p, thenBlock1, elifBlock)); + + // x = 1.0 + // y = 3.0; + MOZ_RELEASE_ASSERT(x->addInputSlow(c1)); + MConstant* c3 = MConstant::New(func.alloc, DoubleValue(3.0)); + thenBlock1->add(c3); + MOZ_RELEASE_ASSERT(y->addInputSlow(c3)); + thenBlock1->end(MGoto::New(func.alloc, joinBlock)); + MOZ_ALWAYS_TRUE(joinBlock->addPredecessor(func.alloc, thenBlock1)); + + // } else if (q) { + MParameter* q = func.createParameter(); + elifBlock->add(q); + elifBlock->end(MTest::New(func.alloc, q, thenBlock2, elseBlock)); + + // x = 2.0 + // y = 4.0; + MConstant* c2 = MConstant::New(func.alloc, DoubleValue(2.0)); + thenBlock2->add(c2); + MOZ_RELEASE_ASSERT(x->addInputSlow(c2)); + MConstant* c4 = MConstant::New(func.alloc, DoubleValue(4.0)); + thenBlock2->add(c4); + MOZ_RELEASE_ASSERT(y->addInputSlow(c4)); + thenBlock2->end(MGoto::New(func.alloc, joinBlock)); + MOZ_ALWAYS_TRUE(joinBlock->addPredecessor(func.alloc, thenBlock2)); + + // } else { + // x = 1.0 + // y = 5.0; + // } + MOZ_RELEASE_ASSERT(x->addInputSlow(c1)); + MConstant* c5 = MConstant::New(func.alloc, DoubleValue(5.0)); + elseBlock->add(c5); + MOZ_RELEASE_ASSERT(y->addInputSlow(c5)); + elseBlock->end(MGoto::New(func.alloc, joinBlock)); + MOZ_ALWAYS_TRUE(joinBlock->addPredecessor(func.alloc, elseBlock)); + + // x = phi(1.0, 2.0, 1.0) + // y = phi(3.0, 4.0, 5.0) + // z = x * y + // return y + joinBlock->addPhi(x); + joinBlock->addPhi(y); + MMul* z = MMul::New(func.alloc, x, y, MIRType::Double); + joinBlock->add(z); + MReturn* ret = MReturn::New(func.alloc, y); + joinBlock->end(ret); + + if (!func.runGVN()) { + return false; + } + + // c1 should be deleted. + for (MInstructionIterator ins = block->begin(); ins != block->end(); ins++) { + CHECK(!ins->isConstant() || (ins->toConstant()->numberToDouble() != 1.0)); + } + return true; +} +END_TEST(testJitDCEinGVN_phi) diff --git a/js/src/jsapi-tests/testJitFoldsTo.cpp b/js/src/jsapi-tests/testJitFoldsTo.cpp new file mode 100644 index 0000000000..9537e1dcf7 --- /dev/null +++ b/js/src/jsapi-tests/testJitFoldsTo.cpp @@ -0,0 +1,262 @@ +/* -*- 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 "jit/IonAnalysis.h" +#include "jit/MIRGenerator.h" +#include "jit/MIRGraph.h" +#include "jit/ValueNumbering.h" + +#include "jsapi-tests/testJitMinimalFunc.h" +#include "jsapi-tests/tests.h" + +using namespace js; +using namespace js::jit; + +BEGIN_TEST(testJitFoldsTo_DivReciprocal) { + MinimalFunc func; + MBasicBlock* block = func.createEntryBlock(); + + // return p / 4.0 + MParameter* p = func.createParameter(); + block->add(p); + MConstant* c = MConstant::New(func.alloc, DoubleValue(4.0)); + block->add(c); + MDiv* div = MDiv::New(func.alloc, p, c, MIRType::Double); + block->add(div); + if (!div->typePolicy()->adjustInputs(func.alloc, div)) { + return false; + } + MDefinition* left = div->getOperand(0); + MReturn* ret = MReturn::New(func.alloc, div); + block->end(ret); + + if (!func.runGVN()) { + return false; + } + + // Test that the div got folded to p * 0.25. + MDefinition* op = ret->getOperand(0); + CHECK(op->isMul()); + CHECK(op->getOperand(0) == left); + CHECK(op->getOperand(1)->isConstant()); + CHECK(op->getOperand(1)->toConstant()->numberToDouble() == 0.25); + return true; +} +END_TEST(testJitFoldsTo_DivReciprocal) + +BEGIN_TEST(testJitFoldsTo_NoDivReciprocal) { + MinimalFunc func; + MBasicBlock* block = func.createEntryBlock(); + + // return p / 5.0 + MParameter* p = func.createParameter(); + block->add(p); + MConstant* c = MConstant::New(func.alloc, DoubleValue(5.0)); + block->add(c); + MDiv* div = MDiv::New(func.alloc, p, c, MIRType::Double); + block->add(div); + if (!div->typePolicy()->adjustInputs(func.alloc, div)) { + return false; + } + MDefinition* left = div->getOperand(0); + MDefinition* right = div->getOperand(1); + MReturn* ret = MReturn::New(func.alloc, div); + block->end(ret); + + if (!func.runGVN()) { + return false; + } + + // Test that the div didn't get folded. + MDefinition* op = ret->getOperand(0); + CHECK(op->isDiv()); + CHECK(op->getOperand(0) == left); + CHECK(op->getOperand(1) == right); + return true; +} +END_TEST(testJitFoldsTo_NoDivReciprocal) + +BEGIN_TEST(testJitNotNot) { + MinimalFunc func; + MBasicBlock* block = func.createEntryBlock(); + + // return Not(Not(p)) + MParameter* p = func.createParameter(); + block->add(p); + MNot* not0 = MNot::New(func.alloc, p); + block->add(not0); + MNot* not1 = MNot::New(func.alloc, not0); + block->add(not1); + MReturn* ret = MReturn::New(func.alloc, not1); + block->end(ret); + + if (!func.runGVN()) { + return false; + } + + // Test that the nots did not get folded. + MDefinition* op = ret->getOperand(0); + CHECK(op->isNot()); + CHECK(op->getOperand(0)->isNot()); + CHECK(op->getOperand(0)->getOperand(0) == p); + return true; +} +END_TEST(testJitNotNot) + +BEGIN_TEST(testJitNotNotNot) { + MinimalFunc func; + MBasicBlock* block = func.createEntryBlock(); + + // return Not(Not(Not(p))) + MParameter* p = func.createParameter(); + block->add(p); + MNot* not0 = MNot::New(func.alloc, p); + block->add(not0); + MNot* not1 = MNot::New(func.alloc, not0); + block->add(not1); + MNot* not2 = MNot::New(func.alloc, not1); + block->add(not2); + MReturn* ret = MReturn::New(func.alloc, not2); + block->end(ret); + + if (!func.runGVN()) { + return false; + } + + // Test that the nots got folded. + MDefinition* op = ret->getOperand(0); + CHECK(op->isNot()); + CHECK(op->getOperand(0) == p); + return true; +} +END_TEST(testJitNotNotNot) + +BEGIN_TEST(testJitNotTest) { + MinimalFunc func; + MBasicBlock* block = func.createEntryBlock(); + MBasicBlock* then = func.createBlock(block); + MBasicBlock* else_ = func.createBlock(block); + MBasicBlock* exit = func.createBlock(block); + + // MTest(Not(p)) + MParameter* p = func.createParameter(); + block->add(p); + MNot* not0 = MNot::New(func.alloc, p); + block->add(not0); + MTest* test = MTest::New(func.alloc, not0, then, else_); + block->end(test); + + then->end(MGoto::New(func.alloc, exit)); + + else_->end(MGoto::New(func.alloc, exit)); + + MReturn* ret = MReturn::New(func.alloc, p); + exit->end(ret); + + MOZ_ALWAYS_TRUE(exit->addPredecessorWithoutPhis(then)); + + if (!func.runGVN()) { + return false; + } + + // Test that the not got folded. + test = block->lastIns()->toTest(); + CHECK(test->getOperand(0) == p); + CHECK(test->getSuccessor(0) == else_); + CHECK(test->getSuccessor(1) == then); + return true; +} +END_TEST(testJitNotTest) + +BEGIN_TEST(testJitNotNotTest) { + MinimalFunc func; + MBasicBlock* block = func.createEntryBlock(); + MBasicBlock* then = func.createBlock(block); + MBasicBlock* else_ = func.createBlock(block); + MBasicBlock* exit = func.createBlock(block); + + // MTest(Not(Not(p))) + MParameter* p = func.createParameter(); + block->add(p); + MNot* not0 = MNot::New(func.alloc, p); + block->add(not0); + MNot* not1 = MNot::New(func.alloc, not0); + block->add(not1); + MTest* test = MTest::New(func.alloc, not1, then, else_); + block->end(test); + + then->end(MGoto::New(func.alloc, exit)); + + else_->end(MGoto::New(func.alloc, exit)); + + MReturn* ret = MReturn::New(func.alloc, p); + exit->end(ret); + + MOZ_ALWAYS_TRUE(exit->addPredecessorWithoutPhis(then)); + + if (!func.runGVN()) { + return false; + } + + // Test that the nots got folded. + test = block->lastIns()->toTest(); + CHECK(test->getOperand(0) == p); + CHECK(test->getSuccessor(0) == then); + CHECK(test->getSuccessor(1) == else_); + return true; +} +END_TEST(testJitNotNotTest) + +BEGIN_TEST(testJitFoldsTo_UnsignedDiv) { + MinimalFunc func; + MBasicBlock* block = func.createEntryBlock(); + + // return 1.0 / 0xffffffff + MConstant* c0 = MConstant::New(func.alloc, Int32Value(1)); + block->add(c0); + MConstant* c1 = MConstant::New(func.alloc, Int32Value(0xffffffff)); + block->add(c1); + MDiv* div = MDiv::New(func.alloc, c0, c1, MIRType::Int32, /*unsignd=*/true); + block->add(div); + MReturn* ret = MReturn::New(func.alloc, div); + block->end(ret); + + if (!func.runGVN()) { + return false; + } + + // Test that the div got folded to 0. + MConstant* op = ret->getOperand(0)->toConstant(); + CHECK(mozilla::NumbersAreIdentical(op->numberToDouble(), 0.0)); + return true; +} +END_TEST(testJitFoldsTo_UnsignedDiv) + +BEGIN_TEST(testJitFoldsTo_UnsignedMod) { + MinimalFunc func; + MBasicBlock* block = func.createEntryBlock(); + + // return 1.0 % 0xffffffff + MConstant* c0 = MConstant::New(func.alloc, Int32Value(1)); + block->add(c0); + MConstant* c1 = MConstant::New(func.alloc, Int32Value(0xffffffff)); + block->add(c1); + MMod* mod = MMod::New(func.alloc, c0, c1, MIRType::Int32, /*unsignd=*/true); + block->add(mod); + MReturn* ret = MReturn::New(func.alloc, mod); + block->end(ret); + + if (!func.runGVN()) { + return false; + } + + // Test that the mod got folded to 1. + MConstant* op = ret->getOperand(0)->toConstant(); + CHECK(mozilla::NumbersAreIdentical(op->numberToDouble(), 1.0)); + return true; +} +END_TEST(testJitFoldsTo_UnsignedMod) diff --git a/js/src/jsapi-tests/testJitGVN.cpp b/js/src/jsapi-tests/testJitGVN.cpp new file mode 100644 index 0000000000..4dd8d64fde --- /dev/null +++ b/js/src/jsapi-tests/testJitGVN.cpp @@ -0,0 +1,299 @@ +/* -*- 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 "jit/IonAnalysis.h" +#include "jit/MIRGenerator.h" +#include "jit/MIRGraph.h" +#include "jit/RangeAnalysis.h" +#include "jit/ValueNumbering.h" + +#include "jsapi-tests/testJitMinimalFunc.h" +#include "jsapi-tests/tests.h" + +using namespace js; +using namespace js::jit; + +static MBasicBlock* FollowTrivialGotos(MBasicBlock* block) { + while (block->phisEmpty() && *block->begin() == block->lastIns() && + block->lastIns()->isGoto()) { + block = block->lastIns()->toGoto()->getSuccessor(0); + } + return block; +} + +BEGIN_TEST(testJitGVN_FixupOSROnlyLoop) { + // This is a testcase which constructs the very rare circumstances that + // require the FixupOSROnlyLoop logic. + + MinimalFunc func; + + MBasicBlock* entry = func.createEntryBlock(); + MBasicBlock* osrEntry = func.createOsrEntryBlock(); + MBasicBlock* outerHeader = func.createBlock(entry); + MBasicBlock* merge = func.createBlock(outerHeader); + MBasicBlock* innerHeader = func.createBlock(merge); + MBasicBlock* innerBackedge = func.createBlock(innerHeader); + MBasicBlock* outerBackedge = func.createBlock(innerHeader); + MBasicBlock* exit = func.createBlock(outerHeader); + + MConstant* c = MConstant::New(func.alloc, BooleanValue(false)); + entry->add(c); + entry->end(MTest::New(func.alloc, c, outerHeader, exit)); + osrEntry->end(MGoto::New(func.alloc, merge)); + + merge->end(MGoto::New(func.alloc, innerHeader)); + + // Use Beta nodes to hide the constants and suppress folding. + MConstant* x = MConstant::New(func.alloc, BooleanValue(false)); + outerHeader->add(x); + MBeta* xBeta = + MBeta::New(func.alloc, x, Range::NewInt32Range(func.alloc, 0, 1)); + outerHeader->add(xBeta); + outerHeader->end(MTest::New(func.alloc, xBeta, merge, exit)); + + MConstant* y = MConstant::New(func.alloc, BooleanValue(false)); + innerHeader->add(y); + MBeta* yBeta = + MBeta::New(func.alloc, y, Range::NewInt32Range(func.alloc, 0, 1)); + innerHeader->add(yBeta); + innerHeader->end(MTest::New(func.alloc, yBeta, innerBackedge, outerBackedge)); + + innerBackedge->end(MGoto::New(func.alloc, innerHeader)); + outerBackedge->end(MGoto::New(func.alloc, outerHeader)); + + MConstant* u = MConstant::New(func.alloc, UndefinedValue()); + exit->add(u); + exit->end(MReturn::New(func.alloc, u)); + + MOZ_ALWAYS_TRUE(innerHeader->addPredecessorWithoutPhis(innerBackedge)); + MOZ_ALWAYS_TRUE(outerHeader->addPredecessorWithoutPhis(outerBackedge)); + MOZ_ALWAYS_TRUE(exit->addPredecessorWithoutPhis(entry)); + MOZ_ALWAYS_TRUE(merge->addPredecessorWithoutPhis(osrEntry)); + + outerHeader->setLoopHeader(outerBackedge); + innerHeader->setLoopHeader(innerBackedge); + + if (!func.runGVN()) { + return false; + } + + // The loops are no longer reachable from the normal entry. They are + // doinated by the osrEntry. + MOZ_RELEASE_ASSERT(func.graph.osrBlock() == osrEntry); + MBasicBlock* newInner = + FollowTrivialGotos(osrEntry->lastIns()->toGoto()->target()); + MBasicBlock* newOuter = + FollowTrivialGotos(newInner->lastIns()->toTest()->ifFalse()); + MBasicBlock* newExit = FollowTrivialGotos(entry); + MOZ_RELEASE_ASSERT(newInner->isLoopHeader()); + MOZ_RELEASE_ASSERT(newOuter->isLoopHeader()); + MOZ_RELEASE_ASSERT(newExit->lastIns()->isReturn()); + + // One more time. + ClearDominatorTree(func.graph); + if (!func.runGVN()) { + return false; + } + + // The loops are no longer reachable from the normal entry. They are + // doinated by the osrEntry. + MOZ_RELEASE_ASSERT(func.graph.osrBlock() == osrEntry); + newInner = FollowTrivialGotos(osrEntry->lastIns()->toGoto()->target()); + newOuter = FollowTrivialGotos(newInner->lastIns()->toTest()->ifFalse()); + newExit = FollowTrivialGotos(entry); + MOZ_RELEASE_ASSERT(newInner->isLoopHeader()); + MOZ_RELEASE_ASSERT(newOuter->isLoopHeader()); + MOZ_RELEASE_ASSERT(newExit->lastIns()->isReturn()); + + return true; +} +END_TEST(testJitGVN_FixupOSROnlyLoop) + +BEGIN_TEST(testJitGVN_FixupOSROnlyLoopNested) { + // Same as testJitGVN_FixupOSROnlyLoop but adds another level of loop + // nesting for added excitement. + + MinimalFunc func; + + MBasicBlock* entry = func.createEntryBlock(); + MBasicBlock* osrEntry = func.createOsrEntryBlock(); + MBasicBlock* outerHeader = func.createBlock(entry); + MBasicBlock* middleHeader = func.createBlock(outerHeader); + MBasicBlock* merge = func.createBlock(middleHeader); + MBasicBlock* innerHeader = func.createBlock(merge); + MBasicBlock* innerBackedge = func.createBlock(innerHeader); + MBasicBlock* middleBackedge = func.createBlock(innerHeader); + MBasicBlock* outerBackedge = func.createBlock(middleHeader); + MBasicBlock* exit = func.createBlock(outerHeader); + + MConstant* c = MConstant::New(func.alloc, BooleanValue(false)); + entry->add(c); + entry->end(MTest::New(func.alloc, c, outerHeader, exit)); + osrEntry->end(MGoto::New(func.alloc, merge)); + + merge->end(MGoto::New(func.alloc, innerHeader)); + + // Use Beta nodes to hide the constants and suppress folding. + MConstant* x = MConstant::New(func.alloc, BooleanValue(false)); + outerHeader->add(x); + MBeta* xBeta = + MBeta::New(func.alloc, x, Range::NewInt32Range(func.alloc, 0, 1)); + outerHeader->add(xBeta); + outerHeader->end(MTest::New(func.alloc, xBeta, middleHeader, exit)); + + MConstant* y = MConstant::New(func.alloc, BooleanValue(false)); + middleHeader->add(y); + MBeta* yBeta = + MBeta::New(func.alloc, y, Range::NewInt32Range(func.alloc, 0, 1)); + middleHeader->add(yBeta); + middleHeader->end(MTest::New(func.alloc, yBeta, merge, outerBackedge)); + + MConstant* w = MConstant::New(func.alloc, BooleanValue(false)); + innerHeader->add(w); + MBeta* wBeta = + MBeta::New(func.alloc, w, Range::NewInt32Range(func.alloc, 0, 1)); + innerHeader->add(wBeta); + innerHeader->end( + MTest::New(func.alloc, wBeta, innerBackedge, middleBackedge)); + + innerBackedge->end(MGoto::New(func.alloc, innerHeader)); + middleBackedge->end(MGoto::New(func.alloc, middleHeader)); + outerBackedge->end(MGoto::New(func.alloc, outerHeader)); + + MConstant* u = MConstant::New(func.alloc, UndefinedValue()); + exit->add(u); + exit->end(MReturn::New(func.alloc, u)); + + MOZ_ALWAYS_TRUE(innerHeader->addPredecessorWithoutPhis(innerBackedge)); + MOZ_ALWAYS_TRUE(middleHeader->addPredecessorWithoutPhis(middleBackedge)); + MOZ_ALWAYS_TRUE(outerHeader->addPredecessorWithoutPhis(outerBackedge)); + MOZ_ALWAYS_TRUE(exit->addPredecessorWithoutPhis(entry)); + MOZ_ALWAYS_TRUE(merge->addPredecessorWithoutPhis(osrEntry)); + + outerHeader->setLoopHeader(outerBackedge); + middleHeader->setLoopHeader(middleBackedge); + innerHeader->setLoopHeader(innerBackedge); + + if (!func.runGVN()) { + return false; + } + + // The loops are no longer reachable from the normal entry. They are + // doinated by the osrEntry. + MOZ_RELEASE_ASSERT(func.graph.osrBlock() == osrEntry); + MBasicBlock* newInner = + FollowTrivialGotos(osrEntry->lastIns()->toGoto()->target()); + MBasicBlock* newMiddle = + FollowTrivialGotos(newInner->lastIns()->toTest()->ifFalse()); + MBasicBlock* newOuter = + FollowTrivialGotos(newMiddle->lastIns()->toTest()->ifFalse()); + MBasicBlock* newExit = FollowTrivialGotos(entry); + MOZ_RELEASE_ASSERT(newInner->isLoopHeader()); + MOZ_RELEASE_ASSERT(newMiddle->isLoopHeader()); + MOZ_RELEASE_ASSERT(newOuter->isLoopHeader()); + MOZ_RELEASE_ASSERT(newExit->lastIns()->isReturn()); + + // One more time. + ClearDominatorTree(func.graph); + if (!func.runGVN()) { + return false; + } + + // The loops are no longer reachable from the normal entry. They are + // doinated by the osrEntry. + MOZ_RELEASE_ASSERT(func.graph.osrBlock() == osrEntry); + newInner = FollowTrivialGotos(osrEntry->lastIns()->toGoto()->target()); + newMiddle = FollowTrivialGotos(newInner->lastIns()->toTest()->ifFalse()); + newOuter = FollowTrivialGotos(newMiddle->lastIns()->toTest()->ifFalse()); + newExit = FollowTrivialGotos(entry); + MOZ_RELEASE_ASSERT(newInner->isLoopHeader()); + MOZ_RELEASE_ASSERT(newMiddle->isLoopHeader()); + MOZ_RELEASE_ASSERT(newOuter->isLoopHeader()); + MOZ_RELEASE_ASSERT(newExit->lastIns()->isReturn()); + + return true; +} +END_TEST(testJitGVN_FixupOSROnlyLoopNested) + +BEGIN_TEST(testJitGVN_PinnedPhis) { + // Set up a loop which gets optimized away, with phis which must be + // cleaned up, permitting more phis to be cleaned up. + + MinimalFunc func; + + MBasicBlock* entry = func.createEntryBlock(); + MBasicBlock* outerHeader = func.createBlock(entry); + MBasicBlock* outerBlock = func.createBlock(outerHeader); + MBasicBlock* innerHeader = func.createBlock(outerBlock); + MBasicBlock* innerBackedge = func.createBlock(innerHeader); + MBasicBlock* exit = func.createBlock(innerHeader); + + MPhi* phi0 = MPhi::New(func.alloc); + MPhi* phi1 = MPhi::New(func.alloc); + MPhi* phi2 = MPhi::New(func.alloc); + MPhi* phi3 = MPhi::New(func.alloc); + + MParameter* p = func.createParameter(); + entry->add(p); + MConstant* z0 = MConstant::New(func.alloc, Int32Value(0)); + MConstant* z1 = MConstant::New(func.alloc, Int32Value(1)); + MConstant* z2 = MConstant::New(func.alloc, Int32Value(2)); + MConstant* z3 = MConstant::New(func.alloc, Int32Value(2)); + MOZ_RELEASE_ASSERT(phi0->addInputSlow(z0)); + MOZ_RELEASE_ASSERT(phi1->addInputSlow(z1)); + MOZ_RELEASE_ASSERT(phi2->addInputSlow(z2)); + MOZ_RELEASE_ASSERT(phi3->addInputSlow(z3)); + entry->add(z0); + entry->add(z1); + entry->add(z2); + entry->add(z3); + entry->end(MGoto::New(func.alloc, outerHeader)); + + outerHeader->addPhi(phi0); + outerHeader->addPhi(phi1); + outerHeader->addPhi(phi2); + outerHeader->addPhi(phi3); + outerHeader->end(MGoto::New(func.alloc, outerBlock)); + + outerBlock->end(MGoto::New(func.alloc, innerHeader)); + + MConstant* true_ = MConstant::New(func.alloc, BooleanValue(true)); + innerHeader->add(true_); + innerHeader->end(MTest::New(func.alloc, true_, innerBackedge, exit)); + + innerBackedge->end(MGoto::New(func.alloc, innerHeader)); + + MInstruction* z4 = MAdd::New(func.alloc, phi0, phi1, MIRType::Int32); + MConstant* z5 = MConstant::New(func.alloc, Int32Value(4)); + MInstruction* z6 = MAdd::New(func.alloc, phi2, phi3, MIRType::Int32); + MConstant* z7 = MConstant::New(func.alloc, Int32Value(6)); + MOZ_RELEASE_ASSERT(phi0->addInputSlow(z4)); + MOZ_RELEASE_ASSERT(phi1->addInputSlow(z5)); + MOZ_RELEASE_ASSERT(phi2->addInputSlow(z6)); + MOZ_RELEASE_ASSERT(phi3->addInputSlow(z7)); + exit->add(z4); + exit->add(z5); + exit->add(z6); + exit->add(z7); + exit->end(MGoto::New(func.alloc, outerHeader)); + + MOZ_ALWAYS_TRUE(innerHeader->addPredecessorWithoutPhis(innerBackedge)); + MOZ_ALWAYS_TRUE(outerHeader->addPredecessorWithoutPhis(exit)); + + outerHeader->setLoopHeader(exit); + innerHeader->setLoopHeader(innerBackedge); + + if (!func.runGVN()) { + return false; + } + + MOZ_RELEASE_ASSERT(innerHeader->phisEmpty()); + MOZ_RELEASE_ASSERT(exit->isDead()); + + return true; +} +END_TEST(testJitGVN_PinnedPhis) diff --git a/js/src/jsapi-tests/testJitMacroAssembler.cpp b/js/src/jsapi-tests/testJitMacroAssembler.cpp new file mode 100644 index 0000000000..0cfa71700f --- /dev/null +++ b/js/src/jsapi-tests/testJitMacroAssembler.cpp @@ -0,0 +1,816 @@ +/* -*- 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 "jit/IonAnalysis.h" +#include "jit/Linker.h" +#include "jit/MacroAssembler.h" +#include "jit/MIRGenerator.h" +#include "jit/MIRGraph.h" +#include "jit/ValueNumbering.h" +#include "js/Value.h" + +#include "jsapi-tests/tests.h" +#include "jsapi-tests/testsJit.h" + +#include "jit/MacroAssembler-inl.h" + +using namespace js; +using namespace js::jit; + +using mozilla::NegativeInfinity; +using mozilla::PositiveInfinity; + +#if defined(JS_CODEGEN_X86) || defined(JS_CODEGEN_X64) + +BEGIN_TEST(testJitMacroAssembler_flexibleDivMod) { + TempAllocator tempAlloc(&cx->tempLifoAlloc()); + JitContext jcx(cx); + StackMacroAssembler masm(cx, tempAlloc); + AutoCreatedBy acb(masm, __func__); + + PrepareJit(masm); + + // Test case divides 9/2; + const uintptr_t quotient_result = 4; + const uintptr_t remainder_result = 1; + const uintptr_t dividend = 9; + const uintptr_t divisor = 2; + + AllocatableGeneralRegisterSet leftOutputHandSides(GeneralRegisterSet::All()); + + while (!leftOutputHandSides.empty()) { + Register lhsOutput = leftOutputHandSides.takeAny(); + + AllocatableGeneralRegisterSet rightHandSides(GeneralRegisterSet::All()); + while (!rightHandSides.empty()) { + Register rhs = rightHandSides.takeAny(); + + AllocatableGeneralRegisterSet remainders(GeneralRegisterSet::All()); + while (!remainders.empty()) { + Register remainderOutput = remainders.takeAny(); + if (lhsOutput == rhs || lhsOutput == remainderOutput || + rhs == remainderOutput) { + continue; + } + + AllocatableRegisterSet regs(RegisterSet::Volatile()); + LiveRegisterSet save(regs.asLiveSet()); + + Label next, fail; + masm.mov(ImmWord(dividend), lhsOutput); + masm.mov(ImmWord(divisor), rhs); + masm.flexibleDivMod32(rhs, lhsOutput, remainderOutput, false, save); + masm.branch32(Assembler::NotEqual, AbsoluteAddress("ient_result), + lhsOutput, &fail); + masm.branch32(Assembler::NotEqual, AbsoluteAddress(&remainder_result), + remainderOutput, &fail); + // Ensure RHS was not clobbered + masm.branch32(Assembler::NotEqual, AbsoluteAddress(&divisor), rhs, + &fail); + masm.jump(&next); + masm.bind(&fail); + masm.printf("Failed"); + masm.breakpoint(); + + masm.bind(&next); + } + } + } + + return ExecuteJit(cx, masm); +} +END_TEST(testJitMacroAssembler_flexibleDivMod) + +BEGIN_TEST(testJitMacroAssembler_flexibleRemainder) { + TempAllocator tempAlloc(&cx->tempLifoAlloc()); + JitContext jcx(cx); + StackMacroAssembler masm(cx, tempAlloc); + AutoCreatedBy acb(masm, __func__); + + PrepareJit(masm); + + // Test case divides 9/2; + const uintptr_t dividend = 9; + const uintptr_t divisor = 2; + const uintptr_t remainder_result = 1; + + AllocatableGeneralRegisterSet leftOutputHandSides(GeneralRegisterSet::All()); + + while (!leftOutputHandSides.empty()) { + Register lhsOutput = leftOutputHandSides.takeAny(); + + AllocatableGeneralRegisterSet rightHandSides(GeneralRegisterSet::All()); + while (!rightHandSides.empty()) { + Register rhs = rightHandSides.takeAny(); + + if (lhsOutput == rhs) { + continue; + } + + AllocatableRegisterSet regs(RegisterSet::Volatile()); + LiveRegisterSet save(regs.asLiveSet()); + + Label next, fail; + masm.mov(ImmWord(dividend), lhsOutput); + masm.mov(ImmWord(divisor), rhs); + masm.flexibleRemainder32(rhs, lhsOutput, false, save); + masm.branch32(Assembler::NotEqual, AbsoluteAddress(&remainder_result), + lhsOutput, &fail); + // Ensure RHS was not clobbered + masm.branch32(Assembler::NotEqual, AbsoluteAddress(&divisor), rhs, &fail); + masm.jump(&next); + masm.bind(&fail); + masm.printf("Failed\n"); + masm.breakpoint(); + + masm.bind(&next); + } + } + + return ExecuteJit(cx, masm); +} +END_TEST(testJitMacroAssembler_flexibleRemainder) + +BEGIN_TEST(testJitMacroAssembler_flexibleQuotient) { + TempAllocator tempAlloc(&cx->tempLifoAlloc()); + JitContext jcx(cx); + StackMacroAssembler masm(cx, tempAlloc); + AutoCreatedBy acb(masm, __func__); + + PrepareJit(masm); + + // Test case divides 9/2; + const uintptr_t dividend = 9; + const uintptr_t divisor = 2; + const uintptr_t quotient_result = 4; + + AllocatableGeneralRegisterSet leftOutputHandSides(GeneralRegisterSet::All()); + + while (!leftOutputHandSides.empty()) { + Register lhsOutput = leftOutputHandSides.takeAny(); + + AllocatableGeneralRegisterSet rightHandSides(GeneralRegisterSet::All()); + while (!rightHandSides.empty()) { + Register rhs = rightHandSides.takeAny(); + + if (lhsOutput == rhs) { + continue; + } + + AllocatableRegisterSet regs(RegisterSet::Volatile()); + LiveRegisterSet save(regs.asLiveSet()); + + Label next, fail; + masm.mov(ImmWord(dividend), lhsOutput); + masm.mov(ImmWord(divisor), rhs); + masm.flexibleQuotient32(rhs, lhsOutput, false, save); + masm.branch32(Assembler::NotEqual, AbsoluteAddress("ient_result), + lhsOutput, &fail); + // Ensure RHS was not clobbered + masm.branch32(Assembler::NotEqual, AbsoluteAddress(&divisor), rhs, &fail); + masm.jump(&next); + masm.bind(&fail); + masm.printf("Failed\n"); + masm.breakpoint(); + + masm.bind(&next); + } + } + + return ExecuteJit(cx, masm); +} +END_TEST(testJitMacroAssembler_flexibleQuotient) + +// To make sure ecx isn't being clobbered; globally scoped to ensure it has the +// right lifetime. +const uintptr_t guardEcx = 0xfeedbad; + +bool shiftTest(JSContext* cx, const char* name, + void (*operation)(StackMacroAssembler& masm, Register, Register), + const uintptr_t* lhsInput, const uintptr_t* rhsInput, + const uintptr_t* result) { + TempAllocator tempAlloc(&cx->tempLifoAlloc()); + JitContext jcx(cx); + StackMacroAssembler masm(cx, tempAlloc); + AutoCreatedBy acb(masm, __func__); + + PrepareJit(masm); + + JS::AutoSuppressGCAnalysis suppress; + AllocatableGeneralRegisterSet leftOutputHandSides(GeneralRegisterSet::All()); + + while (!leftOutputHandSides.empty()) { + Register lhsOutput = leftOutputHandSides.takeAny(); + + AllocatableGeneralRegisterSet rightHandSides(GeneralRegisterSet::All()); + while (!rightHandSides.empty()) { + Register rhs = rightHandSides.takeAny(); + + // You can only use shift as the same reg if the values are the same + if (lhsOutput == rhs && *lhsInput != *rhsInput) { + continue; + } + + Label next, outputFail, clobberRhs, clobberEcx, dump; + masm.mov(ImmWord(guardEcx), ecx); + masm.mov(ImmWord(*lhsInput), lhsOutput); + masm.mov(ImmWord(*rhsInput), rhs); + + operation(masm, rhs, lhsOutput); + + // Ensure Result is correct + masm.branch32(Assembler::NotEqual, AbsoluteAddress(result), lhsOutput, + &outputFail); + + // Ensure RHS was not clobbered, unless it's also the output register. + if (lhsOutput != rhs) { + masm.branch32(Assembler::NotEqual, AbsoluteAddress(rhsInput), rhs, + &clobberRhs); + } + + if (lhsOutput != ecx && rhs != ecx) { + // If neither lhsOutput nor rhs is ecx, make sure ecx has been + // preserved, otherwise it's expected to be covered by the RHS clobber + // check above, or intentionally clobbered as the output. + masm.branch32(Assembler::NotEqual, AbsoluteAddress(&guardEcx), ecx, + &clobberEcx); + } + + masm.jump(&next); + + masm.bind(&outputFail); + masm.printf("Incorrect output (got %d) ", lhsOutput); + masm.jump(&dump); + + masm.bind(&clobberRhs); + masm.printf("rhs clobbered %d", rhs); + masm.jump(&dump); + + masm.bind(&clobberEcx); + masm.printf("ecx clobbered"); + masm.jump(&dump); + + masm.bind(&dump); + masm.mov(ImmPtr(lhsOutput.name()), lhsOutput); + masm.printf("(lhsOutput/srcDest) %s ", lhsOutput); + masm.mov(ImmPtr(name), lhsOutput); + masm.printf("%s ", lhsOutput); + masm.mov(ImmPtr(rhs.name()), lhsOutput); + masm.printf("(shift/rhs) %s \n", lhsOutput); + // Breakpoint to force test failure. + masm.breakpoint(); + masm.bind(&next); + } + } + + return ExecuteJit(cx, masm); +} + +BEGIN_TEST(testJitMacroAssembler_flexibleRshift) { + { + // Test case 16 >> 2 == 4; + const uintptr_t lhsInput = 16; + const uintptr_t rhsInput = 2; + const uintptr_t result = 4; + + bool res = shiftTest( + cx, "flexibleRshift32", + [](StackMacroAssembler& masm, Register rhs, Register lhsOutput) { + masm.flexibleRshift32(rhs, lhsOutput); + }, + &lhsInput, &rhsInput, &result); + if (!res) { + return false; + } + } + + { + // Test case 16 >> 16 == 0 -- this helps cover the case where the same + // register can be passed for source and dest. + const uintptr_t lhsInput = 16; + const uintptr_t rhsInput = 16; + const uintptr_t result = 0; + + bool res = shiftTest( + cx, "flexibleRshift32", + [](StackMacroAssembler& masm, Register rhs, Register lhsOutput) { + masm.flexibleRshift32(rhs, lhsOutput); + }, + &lhsInput, &rhsInput, &result); + if (!res) { + return false; + } + } + + return true; +} +END_TEST(testJitMacroAssembler_flexibleRshift) + +BEGIN_TEST(testJitMacroAssembler_flexibleRshiftArithmetic) { + { + // Test case 4294967295 >> 2 == 4294967295; + const uintptr_t lhsInput = 4294967295; + const uintptr_t rhsInput = 2; + const uintptr_t result = 4294967295; + bool res = shiftTest( + cx, "flexibleRshift32Arithmetic", + [](StackMacroAssembler& masm, Register rhs, Register lhsOutput) { + masm.flexibleRshift32Arithmetic(rhs, lhsOutput); + }, + &lhsInput, &rhsInput, &result); + if (!res) { + return false; + } + } + + { + // Test case 16 >> 16 == 0 -- this helps cover the case where the same + // register can be passed for source and dest. + const uintptr_t lhsInput = 16; + const uintptr_t rhsInput = 16; + const uintptr_t result = 0; + + bool res = shiftTest( + cx, "flexibleRshift32Arithmetic", + [](StackMacroAssembler& masm, Register rhs, Register lhsOutput) { + masm.flexibleRshift32Arithmetic(rhs, lhsOutput); + }, + &lhsInput, &rhsInput, &result); + if (!res) { + return false; + } + } + + return true; +} +END_TEST(testJitMacroAssembler_flexibleRshiftArithmetic) + +BEGIN_TEST(testJitMacroAssembler_flexibleLshift) { + { + // Test case 16 << 2 == 64; + const uintptr_t lhsInput = 16; + const uintptr_t rhsInput = 2; + const uintptr_t result = 64; + + bool res = shiftTest( + cx, "flexibleLshift32", + [](StackMacroAssembler& masm, Register rhs, Register lhsOutput) { + masm.flexibleLshift32(rhs, lhsOutput); + }, + &lhsInput, &rhsInput, &result); + if (!res) { + return false; + } + } + + { + // Test case 4 << 4 == 64; duplicated input case + const uintptr_t lhsInput = 4; + const uintptr_t rhsInput = 4; + const uintptr_t result = 64; + + bool res = shiftTest( + cx, "flexibleLshift32", + [](StackMacroAssembler& masm, Register rhs, Register lhsOutput) { + masm.flexibleLshift32(rhs, lhsOutput); + }, + &lhsInput, &rhsInput, &result); + if (!res) { + return false; + } + } + + return true; +} +END_TEST(testJitMacroAssembler_flexibleLshift) + +BEGIN_TEST(testJitMacroAssembler_truncateDoubleToInt64) { + TempAllocator tempAlloc(&cx->tempLifoAlloc()); + JitContext jcx(cx); + StackMacroAssembler masm(cx, tempAlloc); + AutoCreatedBy acb(masm, __func__); + + PrepareJit(masm); + + AllocatableGeneralRegisterSet allRegs(GeneralRegisterSet::All()); + AllocatableFloatRegisterSet allFloatRegs(FloatRegisterSet::All()); + FloatRegister input = allFloatRegs.takeAny(); +# ifdef JS_NUNBOX32 + Register64 output(allRegs.takeAny(), allRegs.takeAny()); +# else + Register64 output(allRegs.takeAny()); +# endif + Register temp = allRegs.takeAny(); + + masm.reserveStack(sizeof(int32_t)); + +# define TEST(INPUT, OUTPUT) \ + { \ + Label next; \ + masm.loadConstantDouble(double(INPUT), input); \ + masm.storeDouble(input, Operand(esp, 0)); \ + masm.truncateDoubleToInt64(Address(esp, 0), Address(esp, 0), temp); \ + masm.branch64(Assembler::Equal, Address(esp, 0), Imm64(OUTPUT), &next); \ + masm.printf("truncateDoubleToInt64(" #INPUT ") failed\n"); \ + masm.breakpoint(); \ + masm.bind(&next); \ + } + + TEST(0, 0); + TEST(-0, 0); + TEST(1, 1); + TEST(9223372036854774784.0, 9223372036854774784); + TEST(-9223372036854775808.0, 0x8000000000000000); + TEST(9223372036854775808.0, 0x8000000000000000); + TEST(JS::GenericNaN(), 0x8000000000000000); + TEST(PositiveInfinity<double>(), 0x8000000000000000); + TEST(NegativeInfinity<double>(), 0x8000000000000000); +# undef TEST + + masm.freeStack(sizeof(int32_t)); + + return ExecuteJit(cx, masm); +} +END_TEST(testJitMacroAssembler_truncateDoubleToInt64) + +BEGIN_TEST(testJitMacroAssembler_truncateDoubleToUInt64) { + TempAllocator tempAlloc(&cx->tempLifoAlloc()); + JitContext jcx(cx); + StackMacroAssembler masm(cx, tempAlloc); + AutoCreatedBy acb(masm, __func__); + + PrepareJit(masm); + + AllocatableGeneralRegisterSet allRegs(GeneralRegisterSet::All()); + AllocatableFloatRegisterSet allFloatRegs(FloatRegisterSet::All()); + FloatRegister input = allFloatRegs.takeAny(); + FloatRegister floatTemp = allFloatRegs.takeAny(); +# ifdef JS_NUNBOX32 + Register64 output(allRegs.takeAny(), allRegs.takeAny()); +# else + Register64 output(allRegs.takeAny()); +# endif + Register temp = allRegs.takeAny(); + + masm.reserveStack(sizeof(int32_t)); + +# define TEST(INPUT, OUTPUT) \ + { \ + Label next; \ + masm.loadConstantDouble(double(INPUT), input); \ + masm.storeDouble(input, Operand(esp, 0)); \ + masm.truncateDoubleToUInt64(Address(esp, 0), Address(esp, 0), temp, \ + floatTemp); \ + masm.branch64(Assembler::Equal, Address(esp, 0), Imm64(OUTPUT), &next); \ + masm.printf("truncateDoubleToUInt64(" #INPUT ") failed\n"); \ + masm.breakpoint(); \ + masm.bind(&next); \ + } + + TEST(0, 0); + TEST(1, 1); + TEST(9223372036854774784.0, 9223372036854774784); + TEST((uint64_t)0x8000000000000000, 0x8000000000000000); + TEST((uint64_t)0x8000000000000001, 0x8000000000000000); + TEST((uint64_t)0x8006004000000001, 0x8006004000000000); + TEST(-0.0, 0); + TEST(-0.5, 0); + TEST(-0.99, 0); + TEST(JS::GenericNaN(), 0x8000000000000000); + TEST(PositiveInfinity<double>(), 0x8000000000000000); + TEST(NegativeInfinity<double>(), 0x8000000000000000); +# undef TEST + + masm.freeStack(sizeof(int32_t)); + + return ExecuteJit(cx, masm); +} +END_TEST(testJitMacroAssembler_truncateDoubleToUInt64) + +BEGIN_TEST(testJitMacroAssembler_branchDoubleNotInInt64Range) { + TempAllocator tempAlloc(&cx->tempLifoAlloc()); + JitContext jcx(cx); + StackMacroAssembler masm(cx, tempAlloc); + AutoCreatedBy acb(masm, __func__); + + PrepareJit(masm); + + AllocatableGeneralRegisterSet allRegs(GeneralRegisterSet::All()); + AllocatableFloatRegisterSet allFloatRegs(FloatRegisterSet::All()); + FloatRegister input = allFloatRegs.takeAny(); +# ifdef JS_NUNBOX32 + Register64 output(allRegs.takeAny(), allRegs.takeAny()); +# else + Register64 output(allRegs.takeAny()); +# endif + Register temp = allRegs.takeAny(); + + masm.reserveStack(sizeof(int32_t)); + +# define TEST(INPUT, OUTPUT) \ + { \ + Label next; \ + masm.loadConstantDouble(double(INPUT), input); \ + masm.storeDouble(input, Operand(esp, 0)); \ + if (OUTPUT) { \ + masm.branchDoubleNotInInt64Range(Address(esp, 0), temp, &next); \ + } else { \ + Label fail; \ + masm.branchDoubleNotInInt64Range(Address(esp, 0), temp, &fail); \ + masm.jump(&next); \ + masm.bind(&fail); \ + } \ + masm.printf("branchDoubleNotInInt64Range(" #INPUT ") failed\n"); \ + masm.breakpoint(); \ + masm.bind(&next); \ + } + + TEST(0, false); + TEST(-0, false); + TEST(1, false); + TEST(9223372036854774784.0, false); + TEST(-9223372036854775808.0, true); + TEST(9223372036854775808.0, true); + TEST(JS::GenericNaN(), true); + TEST(PositiveInfinity<double>(), true); + TEST(NegativeInfinity<double>(), true); +# undef TEST + + masm.freeStack(sizeof(int32_t)); + + return ExecuteJit(cx, masm); +} +END_TEST(testJitMacroAssembler_branchDoubleNotInInt64Range) + +BEGIN_TEST(testJitMacroAssembler_branchDoubleNotInUInt64Range) { + TempAllocator tempAlloc(&cx->tempLifoAlloc()); + JitContext jcx(cx); + StackMacroAssembler masm(cx, tempAlloc); + AutoCreatedBy acb(masm, __func__); + + PrepareJit(masm); + + AllocatableGeneralRegisterSet allRegs(GeneralRegisterSet::All()); + AllocatableFloatRegisterSet allFloatRegs(FloatRegisterSet::All()); + FloatRegister input = allFloatRegs.takeAny(); +# ifdef JS_NUNBOX32 + Register64 output(allRegs.takeAny(), allRegs.takeAny()); +# else + Register64 output(allRegs.takeAny()); +# endif + Register temp = allRegs.takeAny(); + + masm.reserveStack(sizeof(int32_t)); + +# define TEST(INPUT, OUTPUT) \ + { \ + Label next; \ + masm.loadConstantDouble(double(INPUT), input); \ + masm.storeDouble(input, Operand(esp, 0)); \ + if (OUTPUT) { \ + masm.branchDoubleNotInUInt64Range(Address(esp, 0), temp, &next); \ + } else { \ + Label fail; \ + masm.branchDoubleNotInUInt64Range(Address(esp, 0), temp, &fail); \ + masm.jump(&next); \ + masm.bind(&fail); \ + } \ + masm.printf("branchDoubleNotInUInt64Range(" #INPUT ") failed\n"); \ + masm.breakpoint(); \ + masm.bind(&next); \ + } + + TEST(0, false); + TEST(1, false); + TEST(9223372036854774784.0, false); + TEST((uint64_t)0x8000000000000000, false); + TEST((uint64_t)0x8000000000000001, false); + TEST((uint64_t)0x8006004000000001, false); + TEST(-0.0, true); + TEST(-0.5, true); + TEST(-0.99, true); + TEST(JS::GenericNaN(), true); + TEST(PositiveInfinity<double>(), true); + TEST(NegativeInfinity<double>(), true); +# undef TEST + + masm.freeStack(sizeof(int32_t)); + + return ExecuteJit(cx, masm); +} +END_TEST(testJitMacroAssembler_branchDoubleNotInUInt64Range) + +BEGIN_TEST(testJitMacroAssembler_lshift64) { + TempAllocator tempAlloc(&cx->tempLifoAlloc()); + JitContext jcx(cx); + StackMacroAssembler masm(cx, tempAlloc); + AutoCreatedBy acb(masm, __func__); + + PrepareJit(masm); + + AllocatableGeneralRegisterSet allRegs(GeneralRegisterSet::All()); + AllocatableFloatRegisterSet allFloatRegs(FloatRegisterSet::All()); +# if defined(JS_CODEGEN_X86) + Register shift = ecx; + allRegs.take(shift); +# elif defined(JS_CODEGEN_X64) + Register shift = rcx; + allRegs.take(shift); +# else + Register shift = allRegs.takeAny(); +# endif + +# ifdef JS_NUNBOX32 + Register64 input(allRegs.takeAny(), allRegs.takeAny()); +# else + Register64 input(allRegs.takeAny()); +# endif + + masm.reserveStack(sizeof(int32_t)); + +# define TEST(SHIFT, INPUT, OUTPUT) \ + { \ + Label next; \ + masm.move64(Imm64(INPUT), input); \ + masm.move32(Imm32(SHIFT), shift); \ + masm.lshift64(shift, input); \ + masm.branch64(Assembler::Equal, input, Imm64(OUTPUT), &next); \ + masm.printf("lshift64(" #SHIFT ", " #INPUT ") failed\n"); \ + masm.breakpoint(); \ + masm.bind(&next); \ + } \ + { \ + Label next; \ + masm.move64(Imm64(INPUT), input); \ + masm.lshift64(Imm32(SHIFT & 0x3f), input); \ + masm.branch64(Assembler::Equal, input, Imm64(OUTPUT), &next); \ + masm.printf("lshift64(Imm32(" #SHIFT "&0x3f), " #INPUT ") failed\n"); \ + masm.breakpoint(); \ + masm.bind(&next); \ + } + + TEST(0, 1, 1); + TEST(1, 1, 2); + TEST(2, 1, 4); + TEST(32, 1, 0x0000000100000000); + TEST(33, 1, 0x0000000200000000); + TEST(0, -1, 0xffffffffffffffff); + TEST(1, -1, 0xfffffffffffffffe); + TEST(2, -1, 0xfffffffffffffffc); + TEST(32, -1, 0xffffffff00000000); + TEST(0xffffffff, 1, 0x8000000000000000); + TEST(0xfffffffe, 1, 0x4000000000000000); + TEST(0xfffffffd, 1, 0x2000000000000000); + TEST(0x80000001, 1, 2); +# undef TEST + + masm.freeStack(sizeof(int32_t)); + + return ExecuteJit(cx, masm); +} +END_TEST(testJitMacroAssembler_lshift64) + +BEGIN_TEST(testJitMacroAssembler_rshift64Arithmetic) { + TempAllocator tempAlloc(&cx->tempLifoAlloc()); + JitContext jcx(cx); + StackMacroAssembler masm(cx, tempAlloc); + AutoCreatedBy acb(masm, __func__); + + PrepareJit(masm); + + AllocatableGeneralRegisterSet allRegs(GeneralRegisterSet::All()); + AllocatableFloatRegisterSet allFloatRegs(FloatRegisterSet::All()); +# if defined(JS_CODEGEN_X86) + Register shift = ecx; + allRegs.take(shift); +# elif defined(JS_CODEGEN_X64) + Register shift = rcx; + allRegs.take(shift); +# else + Register shift = allRegs.takeAny(); +# endif + +# ifdef JS_NUNBOX32 + Register64 input(allRegs.takeAny(), allRegs.takeAny()); +# else + Register64 input(allRegs.takeAny()); +# endif + + masm.reserveStack(sizeof(int32_t)); + +# define TEST(SHIFT, INPUT, OUTPUT) \ + { \ + Label next; \ + masm.move64(Imm64(INPUT), input); \ + masm.move32(Imm32(SHIFT), shift); \ + masm.rshift64Arithmetic(shift, input); \ + masm.branch64(Assembler::Equal, input, Imm64(OUTPUT), &next); \ + masm.printf("rshift64Arithmetic(" #SHIFT ", " #INPUT ") failed\n"); \ + masm.breakpoint(); \ + masm.bind(&next); \ + } \ + { \ + Label next; \ + masm.move64(Imm64(INPUT), input); \ + masm.rshift64Arithmetic(Imm32(SHIFT & 0x3f), input); \ + masm.branch64(Assembler::Equal, input, Imm64(OUTPUT), &next); \ + masm.printf("rshift64Arithmetic(Imm32(" #SHIFT "&0x3f), " #INPUT \ + ") failed\n"); \ + masm.breakpoint(); \ + masm.bind(&next); \ + } + + TEST(0, 0x4000000000000000, 0x4000000000000000); + TEST(1, 0x4000000000000000, 0x2000000000000000); + TEST(2, 0x4000000000000000, 0x1000000000000000); + TEST(32, 0x4000000000000000, 0x0000000040000000); + TEST(0, 0x8000000000000000, 0x8000000000000000); + TEST(1, 0x8000000000000000, 0xc000000000000000); + TEST(2, 0x8000000000000000, 0xe000000000000000); + TEST(32, 0x8000000000000000, 0xffffffff80000000); + TEST(0xffffffff, 0x8000000000000000, 0xffffffffffffffff); + TEST(0xfffffffe, 0x8000000000000000, 0xfffffffffffffffe); + TEST(0xfffffffd, 0x8000000000000000, 0xfffffffffffffffc); + TEST(0x80000001, 0x8000000000000000, 0xc000000000000000); +# undef TEST + + masm.freeStack(sizeof(int32_t)); + + return ExecuteJit(cx, masm); +} +END_TEST(testJitMacroAssembler_rshift64Arithmetic) + +BEGIN_TEST(testJitMacroAssembler_rshift64) { + TempAllocator tempAlloc(&cx->tempLifoAlloc()); + JitContext jcx(cx); + StackMacroAssembler masm(cx, tempAlloc); + AutoCreatedBy acb(masm, __func__); + + PrepareJit(masm); + + AllocatableGeneralRegisterSet allRegs(GeneralRegisterSet::All()); + AllocatableFloatRegisterSet allFloatRegs(FloatRegisterSet::All()); +# if defined(JS_CODEGEN_X86) + Register shift = ecx; + allRegs.take(shift); +# elif defined(JS_CODEGEN_X64) + Register shift = rcx; + allRegs.take(shift); +# else + Register shift = allRegs.takeAny(); +# endif + +# ifdef JS_NUNBOX32 + Register64 input(allRegs.takeAny(), allRegs.takeAny()); +# else + Register64 input(allRegs.takeAny()); +# endif + + masm.reserveStack(sizeof(int32_t)); + +# define TEST(SHIFT, INPUT, OUTPUT) \ + { \ + Label next; \ + masm.move64(Imm64(INPUT), input); \ + masm.move32(Imm32(SHIFT), shift); \ + masm.rshift64(shift, input); \ + masm.branch64(Assembler::Equal, input, Imm64(OUTPUT), &next); \ + masm.printf("rshift64(" #SHIFT ", " #INPUT ") failed\n"); \ + masm.breakpoint(); \ + masm.bind(&next); \ + } \ + { \ + Label next; \ + masm.move64(Imm64(INPUT), input); \ + masm.rshift64(Imm32(SHIFT & 0x3f), input); \ + masm.branch64(Assembler::Equal, input, Imm64(OUTPUT), &next); \ + masm.printf("rshift64(Imm32(" #SHIFT "&0x3f), " #INPUT ") failed\n"); \ + masm.breakpoint(); \ + masm.bind(&next); \ + } + + TEST(0, 0x4000000000000000, 0x4000000000000000); + TEST(1, 0x4000000000000000, 0x2000000000000000); + TEST(2, 0x4000000000000000, 0x1000000000000000); + TEST(32, 0x4000000000000000, 0x0000000040000000); + TEST(0, 0x8000000000000000, 0x8000000000000000); + TEST(1, 0x8000000000000000, 0x4000000000000000); + TEST(2, 0x8000000000000000, 0x2000000000000000); + TEST(32, 0x8000000000000000, 0x0000000080000000); + TEST(0xffffffff, 0x8000000000000000, 0x0000000000000001); + TEST(0xfffffffe, 0x8000000000000000, 0x0000000000000002); + TEST(0xfffffffd, 0x8000000000000000, 0x0000000000000004); + TEST(0x80000001, 0x8000000000000000, 0x4000000000000000); +# undef TEST + + masm.freeStack(sizeof(int32_t)); + + return ExecuteJit(cx, masm); +} +END_TEST(testJitMacroAssembler_rshift64) + +#endif diff --git a/js/src/jsapi-tests/testJitMinimalFunc.h b/js/src/jsapi-tests/testJitMinimalFunc.h new file mode 100644 index 0000000000..3a7dbd3183 --- /dev/null +++ b/js/src/jsapi-tests/testJitMinimalFunc.h @@ -0,0 +1,123 @@ +/* -*- 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/. */ + +#ifndef jsapi_tests_jitTestGVN_h +#define jsapi_tests_jitTestGVN_h + +#include "jit/IonAnalysis.h" +#include "jit/MIRGenerator.h" +#include "jit/MIRGraph.h" +#include "jit/RangeAnalysis.h" +#include "jit/ValueNumbering.h" + +namespace js { +namespace jit { + +struct MinimalAlloc { + LifoAlloc lifo; + TempAllocator alloc; + + // We are not testing the fallible allocator in these test cases, thus make + // the lifo alloc chunk extremely large for our test cases. + MinimalAlloc() : lifo(128 * 1024), alloc(&lifo) { + if (!alloc.ensureBallast()) { + MOZ_CRASH("[OOM] Not enough RAM for the test."); + } + } +}; + +struct MinimalFunc : MinimalAlloc { + JitCompileOptions options; + CompileInfo info; + MIRGraph graph; + MIRGenerator mir; + uint32_t numParams; + + MinimalFunc() + : options(), + info(0), + graph(&alloc), + mir(static_cast<CompileRealm*>(nullptr), options, &alloc, &graph, &info, + static_cast<const OptimizationInfo*>(nullptr)), + numParams(0) {} + + MBasicBlock* createEntryBlock() { + MBasicBlock* block = + MBasicBlock::New(graph, info, nullptr, MBasicBlock::NORMAL); + graph.addBlock(block); + return block; + } + + MBasicBlock* createOsrEntryBlock() { + MBasicBlock* block = + MBasicBlock::New(graph, info, nullptr, MBasicBlock::NORMAL); + graph.addBlock(block); + graph.setOsrBlock(block); + return block; + } + + MBasicBlock* createBlock(MBasicBlock* pred) { + MBasicBlock* block = + MBasicBlock::New(graph, info, pred, MBasicBlock::NORMAL); + graph.addBlock(block); + return block; + } + + MParameter* createParameter() { + MParameter* p = MParameter::New(alloc, numParams++); + return p; + } + + bool runGVN() { + if (!SplitCriticalEdges(graph)) { + return false; + } + RenumberBlocks(graph); + if (!BuildDominatorTree(graph)) { + return false; + } + if (!BuildPhiReverseMapping(graph)) { + return false; + } + ValueNumberer gvn(&mir, graph); + if (!gvn.run(ValueNumberer::DontUpdateAliasAnalysis)) { + return false; + } + return true; + } + + bool runRangeAnalysis() { + if (!SplitCriticalEdges(graph)) { + return false; + } + RenumberBlocks(graph); + if (!BuildDominatorTree(graph)) { + return false; + } + if (!BuildPhiReverseMapping(graph)) { + return false; + } + RangeAnalysis rangeAnalysis(&mir, graph); + if (!rangeAnalysis.addBetaNodes()) { + return false; + } + if (!rangeAnalysis.analyze()) { + return false; + } + if (!rangeAnalysis.addRangeAssertions()) { + return false; + } + if (!rangeAnalysis.removeBetaNodes()) { + return false; + } + return true; + } +}; + +} // namespace jit +} // namespace js + +#endif diff --git a/js/src/jsapi-tests/testJitMoveEmitterCycles-mips32.cpp b/js/src/jsapi-tests/testJitMoveEmitterCycles-mips32.cpp new file mode 100644 index 0000000000..bab11a22f4 --- /dev/null +++ b/js/src/jsapi-tests/testJitMoveEmitterCycles-mips32.cpp @@ -0,0 +1,480 @@ +/* -*- 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/. */ + +#if defined(JS_SIMULATOR_MIPS32) +# include "jit/Linker.h" +# include "jit/MacroAssembler.h" +# include "jit/mips32/Assembler-mips32.h" +# include "jit/mips32/MoveEmitter-mips32.h" +# include "jit/mips32/Simulator-mips32.h" +# include "jit/MoveResolver.h" + +# include "jsapi-tests/tests.h" + +# include "vm/Runtime.h" + +static const int LIFO_ALLOC_PRIMARY_CHUNK_SIZE = 4 * 1024; + +static constexpr js::jit::FloatRegister single0(0, + js::jit::FloatRegister::Single); +static constexpr js::jit::FloatRegister single1(1, + js::jit::FloatRegister::Single); +static constexpr js::jit::FloatRegister single2(2, + js::jit::FloatRegister::Single); +static constexpr js::jit::FloatRegister single3(3, + js::jit::FloatRegister::Single); +static constexpr js::jit::FloatRegister single4(4, + js::jit::FloatRegister::Single); +static constexpr js::jit::FloatRegister single5(5, + js::jit::FloatRegister::Single); +static constexpr js::jit::FloatRegister single6(6, + js::jit::FloatRegister::Single); +static constexpr js::jit::FloatRegister single7(7, + js::jit::FloatRegister::Single); +static constexpr js::jit::FloatRegister single8(8, + js::jit::FloatRegister::Single); +static constexpr js::jit::FloatRegister single9(9, + js::jit::FloatRegister::Single); +static constexpr js::jit::FloatRegister single10( + 10, js::jit::FloatRegister::Single); +static constexpr js::jit::FloatRegister single11( + 11, js::jit::FloatRegister::Single); +static constexpr js::jit::FloatRegister single12( + 12, js::jit::FloatRegister::Single); +static constexpr js::jit::FloatRegister single13( + 13, js::jit::FloatRegister::Single); +static constexpr js::jit::FloatRegister single14( + 14, js::jit::FloatRegister::Single); +static constexpr js::jit::FloatRegister single15( + 15, js::jit::FloatRegister::Single); +static constexpr js::jit::FloatRegister single16( + 16, js::jit::FloatRegister::Single); +static constexpr js::jit::FloatRegister single17( + 17, js::jit::FloatRegister::Single); +static constexpr js::jit::FloatRegister single18( + 18, js::jit::FloatRegister::Single); +static constexpr js::jit::FloatRegister single19( + 19, js::jit::FloatRegister::Single); +static constexpr js::jit::FloatRegister single20( + 20, js::jit::FloatRegister::Single); +static constexpr js::jit::FloatRegister single21( + 21, js::jit::FloatRegister::Single); +static constexpr js::jit::FloatRegister single22( + 22, js::jit::FloatRegister::Single); +static constexpr js::jit::FloatRegister single23( + 23, js::jit::FloatRegister::Single); +static constexpr js::jit::FloatRegister single24( + 24, js::jit::FloatRegister::Single); +static constexpr js::jit::FloatRegister single25( + 25, js::jit::FloatRegister::Single); +static constexpr js::jit::FloatRegister single26( + 26, js::jit::FloatRegister::Single); +static constexpr js::jit::FloatRegister single27( + 27, js::jit::FloatRegister::Single); +static constexpr js::jit::FloatRegister single28( + 28, js::jit::FloatRegister::Single); +static constexpr js::jit::FloatRegister single29( + 29, js::jit::FloatRegister::Single); +static constexpr js::jit::FloatRegister single30( + 30, js::jit::FloatRegister::Single); +static constexpr js::jit::FloatRegister single31( + 31, js::jit::FloatRegister::Single); + +static constexpr js::jit::FloatRegister double0(0, + js::jit::FloatRegister::Double); +static constexpr js::jit::FloatRegister double1(2, + js::jit::FloatRegister::Double); +static constexpr js::jit::FloatRegister double2(4, + js::jit::FloatRegister::Double); +static constexpr js::jit::FloatRegister double3(6, + js::jit::FloatRegister::Double); +static constexpr js::jit::FloatRegister double4(8, + js::jit::FloatRegister::Double); +static constexpr js::jit::FloatRegister double5(10, + js::jit::FloatRegister::Double); +static constexpr js::jit::FloatRegister double6(12, + js::jit::FloatRegister::Double); +static constexpr js::jit::FloatRegister double7(14, + js::jit::FloatRegister::Double); +static constexpr js::jit::FloatRegister double8(16, + js::jit::FloatRegister::Double); +static constexpr js::jit::FloatRegister double9(18, + js::jit::FloatRegister::Double); +static constexpr js::jit::FloatRegister double10( + 20, js::jit::FloatRegister::Double); +static constexpr js::jit::FloatRegister double11( + 22, js::jit::FloatRegister::Double); +static constexpr js::jit::FloatRegister double12( + 24, js::jit::FloatRegister::Double); +static constexpr js::jit::FloatRegister double13( + 26, js::jit::FloatRegister::Double); +static constexpr js::jit::FloatRegister double14( + 28, js::jit::FloatRegister::Double); +static constexpr js::jit::FloatRegister double15( + 30, js::jit::FloatRegister::Double); + +static js::jit::JitCode* linkAndAllocate(JSContext* cx, + js::jit::MacroAssembler* masm) { + using namespace js; + using namespace js::jit; + Linker l(*masm); + return l.newCode(cx, CodeKind::Ion); +} + +# define TRY(x) \ + if (!(x)) return false; + +BEGIN_TEST(testJitMoveEmitterCycles_simple) { + using namespace js; + using namespace js::jit; + LifoAlloc lifo(LIFO_ALLOC_PRIMARY_CHUNK_SIZE); + TempAllocator alloc(&lifo); + JitContext jc(cx, &alloc); + + StackMacroAssembler masm; + MoveEmitter mover(masm); + MoveResolver mr; + mr.setAllocator(alloc); + Simulator* sim = Simulator::Current(); + TRY(mr.addMove(MoveOperand(double0), MoveOperand(double2), MoveOp::DOUBLE)); + sim->setFpuRegisterDouble(double0.id(), 2.0); + TRY(mr.addMove(MoveOperand(double3), MoveOperand(double1), MoveOp::DOUBLE)); + sim->setFpuRegisterDouble(double3.id(), 1.0); + TRY(mr.addMove(MoveOperand(single4), MoveOperand(single0), MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single4.id(), 0.0f); + TRY(mr.addMove(MoveOperand(single5), MoveOperand(single6), MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single5.id(), 6.0f); + TRY(mr.addMove(MoveOperand(single2), MoveOperand(single1), MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single2.id(), 1.0f); + TRY(mr.addMove(MoveOperand(single3), MoveOperand(single7), MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single3.id(), 7.0f); + // don't explode! + TRY(mr.resolve()); + mover.emit(mr); + mover.finish(); + masm.abiret(); + JitCode* code = linkAndAllocate(cx, &masm); + sim->call(code->raw(), 1, 1); + CHECK(sim->getFpuRegisterDouble(double2.id()) == 2.0); + CHECK(int(sim->getFpuRegisterDouble(double1.id())) == 1.0); + CHECK(int(sim->getFpuRegisterFloat(single0.id())) == 0.0); + CHECK(int(sim->getFpuRegisterFloat(single6.id())) == 6.0); + CHECK(int(sim->getFpuRegisterFloat(single1.id())) == 1.0); + CHECK(int(sim->getFpuRegisterFloat(single7.id())) == 7.0); + return true; +} +END_TEST(testJitMoveEmitterCycles_simple) +BEGIN_TEST(testJitMoveEmitterCycles_autogen) { + using namespace js; + using namespace js::jit; + LifoAlloc lifo(LIFO_ALLOC_PRIMARY_CHUNK_SIZE); + TempAllocator alloc(&lifo); + JitContext jc(cx, &alloc); + StackMacroAssembler masm; + MoveEmitter mover(masm); + MoveResolver mr; + mr.setAllocator(alloc); + Simulator* sim = Simulator::Current(); + sim->setFpuRegisterDouble(double9.id(), 9.0); + TRY(mr.addMove(MoveOperand(single24), MoveOperand(single25), + MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single24.id(), 24.0f); + TRY(mr.addMove(MoveOperand(double3), MoveOperand(double0), MoveOp::DOUBLE)); + sim->setFpuRegisterDouble(double3.id(), 3.0); + TRY(mr.addMove(MoveOperand(single10), MoveOperand(single31), + MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single10.id(), 10.0f); + TRY(mr.addMove(MoveOperand(double1), MoveOperand(double10), MoveOp::DOUBLE)); + sim->setFpuRegisterDouble(double1.id(), 1.0); + TRY(mr.addMove(MoveOperand(single8), MoveOperand(single10), MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single8.id(), 8.0f); + TRY(mr.addMove(MoveOperand(double2), MoveOperand(double7), MoveOp::DOUBLE)); + sim->setFpuRegisterDouble(double2.id(), 2.0); + TRY(mr.addMove(MoveOperand(single1), MoveOperand(single3), MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single1.id(), 1.0f); + TRY(mr.addMove(MoveOperand(single17), MoveOperand(single11), + MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single17.id(), 17.0f); + TRY(mr.addMove(MoveOperand(single22), MoveOperand(single30), + MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single22.id(), 22.0f); + TRY(mr.addMove(MoveOperand(single31), MoveOperand(single7), MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single31.id(), 31.0f); + TRY(mr.addMove(MoveOperand(double3), MoveOperand(double13), MoveOp::DOUBLE)); + sim->setFpuRegisterDouble(double3.id(), 3.0); + TRY(mr.addMove(MoveOperand(single31), MoveOperand(single23), + MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single31.id(), 31.0f); + TRY(mr.addMove(MoveOperand(single13), MoveOperand(single8), MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single13.id(), 13.0f); + TRY(mr.addMove(MoveOperand(single28), MoveOperand(single5), MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single28.id(), 28.0f); + TRY(mr.addMove(MoveOperand(single20), MoveOperand(single6), MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single20.id(), 20.0f); + TRY(mr.addMove(MoveOperand(single0), MoveOperand(single2), MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single0.id(), 0.0f); + TRY(mr.addMove(MoveOperand(double7), MoveOperand(double6), MoveOp::DOUBLE)); + sim->setFpuRegisterDouble(double7.id(), 7.0); + TRY(mr.addMove(MoveOperand(single13), MoveOperand(single9), MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single13.id(), 13.0f); + TRY(mr.addMove(MoveOperand(single1), MoveOperand(single4), MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single1.id(), 1.0f); + TRY(mr.addMove(MoveOperand(single29), MoveOperand(single22), + MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single29.id(), 29.0f); + TRY(mr.addMove(MoveOperand(single25), MoveOperand(single24), + MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single25.id(), 25.0f); + TRY(mr.resolve()); + mover.emit(mr); + mover.finish(); + masm.abiret(); + JitCode* code = linkAndAllocate(cx, &masm); + sim->call(code->raw(), 1, 1); + CHECK(int(sim->getFpuRegisterFloat(single25.id())) == 24.0); + CHECK(int(sim->getFpuRegisterDouble(double0.id())) == 3.0); + CHECK(int(sim->getFpuRegisterFloat(single31.id())) == 10.0); + CHECK(int(sim->getFpuRegisterDouble(double10.id())) == 1.0); + CHECK(int(sim->getFpuRegisterFloat(single10.id())) == 8.0); + CHECK(int(sim->getFpuRegisterDouble(double7.id())) == 2.0); + CHECK(int(sim->getFpuRegisterFloat(single3.id())) == 1.0); + CHECK(int(sim->getFpuRegisterFloat(single11.id())) == 17.0); + CHECK(int(sim->getFpuRegisterFloat(single30.id())) == 22.0); + CHECK(int(sim->getFpuRegisterFloat(single7.id())) == 31.0); + CHECK(int(sim->getFpuRegisterDouble(double13.id())) == 3.0); + CHECK(int(sim->getFpuRegisterFloat(single23.id())) == 31.0); + CHECK(int(sim->getFpuRegisterFloat(single8.id())) == 13.0); + CHECK(int(sim->getFpuRegisterFloat(single5.id())) == 28.0); + CHECK(int(sim->getFpuRegisterFloat(single6.id())) == 20.0); + CHECK(int(sim->getFpuRegisterFloat(single2.id())) == 0.0); + CHECK(int(sim->getFpuRegisterDouble(double6.id())) == 7.0); + CHECK(int(sim->getFpuRegisterFloat(single9.id())) == 13.0); + CHECK(int(sim->getFpuRegisterFloat(single4.id())) == 1.0); + CHECK(int(sim->getFpuRegisterFloat(single22.id())) == 29.0); + CHECK(int(sim->getFpuRegisterFloat(single24.id())) == 25.0); + return true; +} +END_TEST(testJitMoveEmitterCycles_autogen) + +BEGIN_TEST(testJitMoveEmitterCycles_autogen2) { + using namespace js; + using namespace js::jit; + LifoAlloc lifo(LIFO_ALLOC_PRIMARY_CHUNK_SIZE); + TempAllocator alloc(&lifo); + JitContext jc(cx, &alloc); + StackMacroAssembler masm; + MoveEmitter mover(masm); + MoveResolver mr; + mr.setAllocator(alloc); + Simulator* sim = Simulator::Current(); + TRY(mr.addMove(MoveOperand(double10), MoveOperand(double0), MoveOp::DOUBLE)); + sim->setFpuRegisterDouble(double10.id(), 10.0); + TRY(mr.addMove(MoveOperand(single15), MoveOperand(single3), MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single15.id(), 15.0f); + TRY(mr.addMove(MoveOperand(single2), MoveOperand(single28), MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single2.id(), 2.0f); + TRY(mr.addMove(MoveOperand(single30), MoveOperand(single25), + MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single30.id(), 30.0f); + TRY(mr.addMove(MoveOperand(single16), MoveOperand(single2), MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single16.id(), 16.0f); + TRY(mr.addMove(MoveOperand(single2), MoveOperand(single29), MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single2.id(), 2.0f); + TRY(mr.addMove(MoveOperand(single17), MoveOperand(single10), + MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single17.id(), 17.0f); + TRY(mr.addMove(MoveOperand(single9), MoveOperand(single26), MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single9.id(), 9.0f); + TRY(mr.addMove(MoveOperand(single1), MoveOperand(single23), MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single1.id(), 1.0f); + TRY(mr.addMove(MoveOperand(single8), MoveOperand(single6), MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single8.id(), 8.0f); + TRY(mr.addMove(MoveOperand(single24), MoveOperand(single16), + MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single24.id(), 24.0f); + TRY(mr.addMove(MoveOperand(double5), MoveOperand(double6), MoveOp::DOUBLE)); + sim->setFpuRegisterDouble(double5.id(), 5.0f); + TRY(mr.addMove(MoveOperand(single23), MoveOperand(single30), + MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single23.id(), 23.0f); + TRY(mr.addMove(MoveOperand(single27), MoveOperand(single17), + MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single27.id(), 27.0f); + TRY(mr.addMove(MoveOperand(double3), MoveOperand(double4), MoveOp::DOUBLE)); + sim->setFpuRegisterDouble(double3.id(), 3.0f); + TRY(mr.addMove(MoveOperand(single14), MoveOperand(single27), + MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single14.id(), 14.0f); + TRY(mr.addMove(MoveOperand(single2), MoveOperand(single31), MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single2.id(), 2.0f); + TRY(mr.addMove(MoveOperand(single2), MoveOperand(single24), MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single2.id(), 2.0f); + TRY(mr.addMove(MoveOperand(single31), MoveOperand(single11), + MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single31.id(), 31.0f); + TRY(mr.addMove(MoveOperand(single24), MoveOperand(single7), MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single24.id(), 24.0f); + TRY(mr.addMove(MoveOperand(single0), MoveOperand(single21), MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single0.id(), 0.0f); + TRY(mr.addMove(MoveOperand(single27), MoveOperand(single20), + MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single27.id(), 27.0f); + TRY(mr.addMove(MoveOperand(single14), MoveOperand(single5), MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single14.id(), 14.0f); + TRY(mr.addMove(MoveOperand(single2), MoveOperand(single14), MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single2.id(), 2.0f); + TRY(mr.addMove(MoveOperand(single12), MoveOperand(single22), + MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single12.id(), 12.0f); + TRY(mr.resolve()); + mover.emit(mr); + mover.finish(); + masm.abiret(); + JitCode* code = linkAndAllocate(cx, &masm); + sim->call(code->raw(), 1, 1); + CHECK(int(sim->getFpuRegisterDouble(double0.id())) == 10); + CHECK(int(sim->getFpuRegisterFloat(single3.id())) == 15); + CHECK(int(sim->getFpuRegisterFloat(single28.id())) == 2); + CHECK(int(sim->getFpuRegisterFloat(single25.id())) == 30); + CHECK(int(sim->getFpuRegisterFloat(single2.id())) == 16); + CHECK(int(sim->getFpuRegisterFloat(single29.id())) == 2); + CHECK(int(sim->getFpuRegisterFloat(single10.id())) == 17); + CHECK(int(sim->getFpuRegisterFloat(single26.id())) == 9); + CHECK(int(sim->getFpuRegisterFloat(single23.id())) == 1); + CHECK(int(sim->getFpuRegisterFloat(single6.id())) == 8); + CHECK(int(sim->getFpuRegisterFloat(single16.id())) == 24); + CHECK(int(sim->getFpuRegisterDouble(double6.id())) == 5); + CHECK(int(sim->getFpuRegisterFloat(single30.id())) == 23); + CHECK(int(sim->getFpuRegisterFloat(single17.id())) == 27); + CHECK(int(sim->getFpuRegisterDouble(double4.id())) == 3); + CHECK(int(sim->getFpuRegisterFloat(single27.id())) == 14); + CHECK(int(sim->getFpuRegisterFloat(single31.id())) == 2); + CHECK(int(sim->getFpuRegisterFloat(single24.id())) == 2); + CHECK(int(sim->getFpuRegisterFloat(single11.id())) == 31); + CHECK(int(sim->getFpuRegisterFloat(single7.id())) == 24); + CHECK(int(sim->getFpuRegisterFloat(single21.id())) == 0); + CHECK(int(sim->getFpuRegisterFloat(single20.id())) == 27); + CHECK(int(sim->getFpuRegisterFloat(single5.id())) == 14); + CHECK(int(sim->getFpuRegisterFloat(single14.id())) == 2); + CHECK(int(sim->getFpuRegisterFloat(single22.id())) == 12); + return true; +} +END_TEST(testJitMoveEmitterCycles_autogen2) + +BEGIN_TEST(testJitMoveEmitterCycles_autogen3) { + using namespace js; + using namespace js::jit; + LifoAlloc lifo(LIFO_ALLOC_PRIMARY_CHUNK_SIZE); + TempAllocator alloc(&lifo); + JitContext jc(cx, &alloc); + StackMacroAssembler masm; + MoveEmitter mover(masm); + MoveResolver mr; + mr.setAllocator(alloc); + Simulator* sim = Simulator::Current(); + TRY(mr.addMove(MoveOperand(single0), MoveOperand(single21), MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single0.id(), 0.0f); + TRY(mr.addMove(MoveOperand(single2), MoveOperand(single26), MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single2.id(), 2.0f); + TRY(mr.addMove(MoveOperand(single4), MoveOperand(single24), MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single4.id(), 4.0f); + TRY(mr.addMove(MoveOperand(single22), MoveOperand(single9), MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single22.id(), 22.0f); + TRY(mr.addMove(MoveOperand(single5), MoveOperand(single28), MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single5.id(), 5.0f); + TRY(mr.addMove(MoveOperand(single15), MoveOperand(single7), MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single15.id(), 15.0f); + TRY(mr.addMove(MoveOperand(single26), MoveOperand(single14), + MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single26.id(), 26.0f); + TRY(mr.addMove(MoveOperand(single13), MoveOperand(single30), + MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single13.id(), 13.0f); + TRY(mr.addMove(MoveOperand(single26), MoveOperand(single22), + MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single26.id(), 26.0f); + TRY(mr.addMove(MoveOperand(single21), MoveOperand(single6), MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single21.id(), 21.0f); + TRY(mr.addMove(MoveOperand(single23), MoveOperand(single31), + MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single23.id(), 23.0f); + TRY(mr.addMove(MoveOperand(single7), MoveOperand(single12), MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single7.id(), 7.0f); + TRY(mr.addMove(MoveOperand(single14), MoveOperand(single10), + MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single14.id(), 14.0f); + TRY(mr.addMove(MoveOperand(double12), MoveOperand(double8), MoveOp::DOUBLE)); + sim->setFpuRegisterDouble(double12.id(), 12.0); + TRY(mr.addMove(MoveOperand(single5), MoveOperand(single1), MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single5.id(), 5.0f); + TRY(mr.addMove(MoveOperand(double12), MoveOperand(double2), MoveOp::DOUBLE)); + sim->setFpuRegisterDouble(double12.id(), 12.0); + TRY(mr.addMove(MoveOperand(single3), MoveOperand(single8), MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single3.id(), 3.0f); + TRY(mr.addMove(MoveOperand(single14), MoveOperand(single0), MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single14.id(), 14.0f); + TRY(mr.addMove(MoveOperand(single28), MoveOperand(single29), + MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single28.id(), 28.0f); + TRY(mr.addMove(MoveOperand(single29), MoveOperand(single2), MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single29.id(), 29.0f); + TRY(mr.addMove(MoveOperand(single22), MoveOperand(single27), + MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single22.id(), 22.0f); + TRY(mr.addMove(MoveOperand(single21), MoveOperand(single11), + MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single21.id(), 21.0f); + TRY(mr.addMove(MoveOperand(single22), MoveOperand(single13), + MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single22.id(), 22.0f); + TRY(mr.addMove(MoveOperand(single29), MoveOperand(single25), + MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single29.id(), 29.0f); + TRY(mr.addMove(MoveOperand(single29), MoveOperand(single15), + MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single29.id(), 29.0f); + TRY(mr.addMove(MoveOperand(single16), MoveOperand(single23), + MoveOp::FLOAT32)); + sim->setFpuRegisterFloat(single16.id(), 16.0f); + TRY(mr.resolve()); + mover.emit(mr); + mover.finish(); + masm.abiret(); + JitCode* code = linkAndAllocate(cx, &masm); + sim->call(code->raw(), 1, 1); + CHECK(int(sim->getFpuRegisterFloat(single21.id())) == 0); + CHECK(int(sim->getFpuRegisterFloat(single26.id())) == 2); + CHECK(int(sim->getFpuRegisterFloat(single24.id())) == 4); + CHECK(int(sim->getFpuRegisterFloat(single9.id())) == 22); + CHECK(int(sim->getFpuRegisterFloat(single28.id())) == 5); + CHECK(int(sim->getFpuRegisterFloat(single7.id())) == 15); + CHECK(int(sim->getFpuRegisterFloat(single14.id())) == 26); + CHECK(int(sim->getFpuRegisterFloat(single30.id())) == 13); + CHECK(int(sim->getFpuRegisterFloat(single22.id())) == 26); + CHECK(int(sim->getFpuRegisterFloat(single6.id())) == 21); + CHECK(int(sim->getFpuRegisterFloat(single31.id())) == 23); + CHECK(int(sim->getFpuRegisterFloat(single12.id())) == 7); + CHECK(int(sim->getFpuRegisterFloat(single10.id())) == 14); + CHECK(int(sim->getFpuRegisterDouble(double8.id())) == 12); + CHECK(int(sim->getFpuRegisterFloat(single1.id())) == 5); + CHECK(int(sim->getFpuRegisterDouble(double2.id())) == 12); + CHECK(int(sim->getFpuRegisterFloat(single8.id())) == 3); + CHECK(int(sim->getFpuRegisterFloat(single0.id())) == 14); + CHECK(int(sim->getFpuRegisterFloat(single29.id())) == 28); + CHECK(int(sim->getFpuRegisterFloat(single2.id())) == 29); + CHECK(int(sim->getFpuRegisterFloat(single27.id())) == 22); + CHECK(int(sim->getFpuRegisterFloat(single3.id())) == 3); + CHECK(int(sim->getFpuRegisterFloat(single11.id())) == 21); + CHECK(int(sim->getFpuRegisterFloat(single13.id())) == 22); + CHECK(int(sim->getFpuRegisterFloat(single25.id())) == 29); + CHECK(int(sim->getFpuRegisterFloat(single15.id())) == 29); + CHECK(int(sim->getFpuRegisterFloat(single23.id())) == 16); + return true; +} +END_TEST(testJitMoveEmitterCycles_autogen3) + +#endif diff --git a/js/src/jsapi-tests/testJitMoveEmitterCycles.cpp b/js/src/jsapi-tests/testJitMoveEmitterCycles.cpp new file mode 100644 index 0000000000..521fab7fa9 --- /dev/null +++ b/js/src/jsapi-tests/testJitMoveEmitterCycles.cpp @@ -0,0 +1,616 @@ +/* -*- 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/. */ + +#if defined(JS_SIMULATOR_ARM) + +# include "jit/arm/Assembler-arm.h" +# include "jit/arm/MoveEmitter-arm.h" +# include "jit/arm/Simulator-arm.h" +# include "jit/Linker.h" +# include "jit/MacroAssembler.h" +# include "jit/MoveResolver.h" + +# include "jsapi-tests/tests.h" + +# include "vm/Runtime.h" + +static const int LIFO_ALLOC_PRIMARY_CHUNK_SIZE = 4 * 1024; + +static constexpr js::jit::FloatRegister s0(0, js::jit::VFPRegister::Single); +static constexpr js::jit::FloatRegister s1(1, js::jit::VFPRegister::Single); +static constexpr js::jit::FloatRegister s2(2, js::jit::VFPRegister::Single); +static constexpr js::jit::FloatRegister s3(3, js::jit::VFPRegister::Single); +static constexpr js::jit::FloatRegister s4(4, js::jit::VFPRegister::Single); +static constexpr js::jit::FloatRegister s5(5, js::jit::VFPRegister::Single); +static constexpr js::jit::FloatRegister s6(6, js::jit::VFPRegister::Single); +static constexpr js::jit::FloatRegister s7(7, js::jit::VFPRegister::Single); +static constexpr js::jit::FloatRegister s8(8, js::jit::VFPRegister::Single); +static constexpr js::jit::FloatRegister s9(9, js::jit::VFPRegister::Single); +static constexpr js::jit::FloatRegister s10(10, js::jit::VFPRegister::Single); +static constexpr js::jit::FloatRegister s11(11, js::jit::VFPRegister::Single); +static constexpr js::jit::FloatRegister s12(12, js::jit::VFPRegister::Single); +static constexpr js::jit::FloatRegister s13(13, js::jit::VFPRegister::Single); +static constexpr js::jit::FloatRegister s14(14, js::jit::VFPRegister::Single); +static constexpr js::jit::FloatRegister s15(15, js::jit::VFPRegister::Single); +static constexpr js::jit::FloatRegister s16(16, js::jit::VFPRegister::Single); +static constexpr js::jit::FloatRegister s17(17, js::jit::VFPRegister::Single); +static constexpr js::jit::FloatRegister s18(18, js::jit::VFPRegister::Single); +static constexpr js::jit::FloatRegister s19(19, js::jit::VFPRegister::Single); +static constexpr js::jit::FloatRegister s20(20, js::jit::VFPRegister::Single); +static constexpr js::jit::FloatRegister s21(21, js::jit::VFPRegister::Single); +static constexpr js::jit::FloatRegister s22(22, js::jit::VFPRegister::Single); +static constexpr js::jit::FloatRegister s23(23, js::jit::VFPRegister::Single); +static constexpr js::jit::FloatRegister s24(24, js::jit::VFPRegister::Single); +static constexpr js::jit::FloatRegister s25(25, js::jit::VFPRegister::Single); +static constexpr js::jit::FloatRegister s26(26, js::jit::VFPRegister::Single); +static constexpr js::jit::FloatRegister s27(27, js::jit::VFPRegister::Single); +static constexpr js::jit::FloatRegister s28(28, js::jit::VFPRegister::Single); +static constexpr js::jit::FloatRegister s29(29, js::jit::VFPRegister::Single); +static constexpr js::jit::FloatRegister s30(30, js::jit::VFPRegister::Single); +static constexpr js::jit::FloatRegister s31(31, js::jit::VFPRegister::Single); + +static js::jit::JitCode* linkAndAllocate(JSContext* cx, + js::jit::MacroAssembler* masm) { + using namespace js; + using namespace js::jit; + Linker l(*masm); + return l.newCode(cx, CodeKind::Ion); +} + +# define TRY(x) \ + if (!(x)) return false; + +BEGIN_TEST(testJitMoveEmitterCycles_simple) { + using namespace js; + using namespace js::jit; + LifoAlloc lifo(LIFO_ALLOC_PRIMARY_CHUNK_SIZE); + TempAllocator alloc(&lifo); + JitContext jc(cx); + StackMacroAssembler masm(cx, alloc); + AutoCreatedBy acb(masm, __func__); + MoveEmitter mover(masm); + MoveResolver mr; + mr.setAllocator(alloc); + Simulator* sim = Simulator::Current(); + TRY(mr.addMove(MoveOperand(d0), MoveOperand(d2), MoveOp::DOUBLE)); + sim->set_d_register_from_double(0, 2); + TRY(mr.addMove(MoveOperand(d3), MoveOperand(d1), MoveOp::DOUBLE)); + sim->set_d_register_from_double(3, 1); + TRY(mr.addMove(MoveOperand(s4), MoveOperand(s0), MoveOp::FLOAT32)); + sim->set_s_register_from_float(4, 0); + TRY(mr.addMove(MoveOperand(s5), MoveOperand(s6), MoveOp::FLOAT32)); + sim->set_s_register_from_float(5, 6); + TRY(mr.addMove(MoveOperand(s2), MoveOperand(s1), MoveOp::FLOAT32)); + sim->set_s_register_from_float(2, 1); + TRY(mr.addMove(MoveOperand(s3), MoveOperand(s7), MoveOp::FLOAT32)); + sim->set_s_register_from_float(3, 7); + // don't explode! + TRY(mr.resolve()); + mover.emit(mr); + mover.finish(); + masm.abiret(); + JitCode* code = linkAndAllocate(cx, &masm); + sim->call(code->raw(), 1, 1); + float f; + double d; + sim->get_double_from_d_register(2, &d); + CHECK(d == 2); + sim->get_double_from_d_register(1, &d); + CHECK(int(d) == 1); + sim->get_float_from_s_register(0, &f); + CHECK(int(f) == 0); + sim->get_float_from_s_register(6, &f); + CHECK(int(f) == 6); + sim->get_float_from_s_register(1, &f); + CHECK(int(f) == 1); + sim->get_float_from_s_register(7, &f); + CHECK(int(f) == 7); + return true; +} +END_TEST(testJitMoveEmitterCycles_simple) +BEGIN_TEST(testJitMoveEmitterCycles_autogen) { + using namespace js; + using namespace js::jit; + LifoAlloc lifo(LIFO_ALLOC_PRIMARY_CHUNK_SIZE); + TempAllocator alloc(&lifo); + JitContext jc(cx); + StackMacroAssembler masm(cx, alloc); + AutoCreatedBy acb(masm, __func__); + MoveEmitter mover(masm); + MoveResolver mr; + mr.setAllocator(alloc); + Simulator* sim = Simulator::Current(); + TRY(mr.addMove(MoveOperand(d9), MoveOperand(d14), MoveOp::DOUBLE)); + sim->set_d_register_from_double(9, 9); + TRY(mr.addMove(MoveOperand(s24), MoveOperand(s25), MoveOp::FLOAT32)); + sim->set_s_register_from_float(24, 24); + TRY(mr.addMove(MoveOperand(d3), MoveOperand(d0), MoveOp::DOUBLE)); + sim->set_d_register_from_double(3, 3); + TRY(mr.addMove(MoveOperand(s10), MoveOperand(s31), MoveOp::FLOAT32)); + sim->set_s_register_from_float(10, 10); + TRY(mr.addMove(MoveOperand(d1), MoveOperand(d10), MoveOp::DOUBLE)); + sim->set_d_register_from_double(1, 1); + TRY(mr.addMove(MoveOperand(s8), MoveOperand(s10), MoveOp::FLOAT32)); + sim->set_s_register_from_float(8, 8); + TRY(mr.addMove(MoveOperand(d2), MoveOperand(d7), MoveOp::DOUBLE)); + sim->set_d_register_from_double(2, 2); + TRY(mr.addMove(MoveOperand(s20), MoveOperand(s18), MoveOp::FLOAT32)); + sim->set_s_register_from_float(20, 20); + TRY(mr.addMove(MoveOperand(s1), MoveOperand(s3), MoveOp::FLOAT32)); + sim->set_s_register_from_float(1, 1); + TRY(mr.addMove(MoveOperand(s17), MoveOperand(s11), MoveOp::FLOAT32)); + sim->set_s_register_from_float(17, 17); + TRY(mr.addMove(MoveOperand(s22), MoveOperand(s30), MoveOp::FLOAT32)); + sim->set_s_register_from_float(22, 22); + TRY(mr.addMove(MoveOperand(s31), MoveOperand(s7), MoveOp::FLOAT32)); + sim->set_s_register_from_float(31, 31); + TRY(mr.addMove(MoveOperand(d3), MoveOperand(d13), MoveOp::DOUBLE)); + sim->set_d_register_from_double(3, 3); + TRY(mr.addMove(MoveOperand(d9), MoveOperand(d8), MoveOp::DOUBLE)); + sim->set_d_register_from_double(9, 9); + TRY(mr.addMove(MoveOperand(s31), MoveOperand(s23), MoveOp::FLOAT32)); + sim->set_s_register_from_float(31, 31); + TRY(mr.addMove(MoveOperand(s13), MoveOperand(s8), MoveOp::FLOAT32)); + sim->set_s_register_from_float(13, 13); + TRY(mr.addMove(MoveOperand(s28), MoveOperand(s5), MoveOp::FLOAT32)); + sim->set_s_register_from_float(28, 28); + TRY(mr.addMove(MoveOperand(s31), MoveOperand(s19), MoveOp::FLOAT32)); + sim->set_s_register_from_float(31, 31); + TRY(mr.addMove(MoveOperand(s20), MoveOperand(s6), MoveOp::FLOAT32)); + sim->set_s_register_from_float(20, 20); + TRY(mr.addMove(MoveOperand(s0), MoveOperand(s2), MoveOp::FLOAT32)); + sim->set_s_register_from_float(0, 0); + TRY(mr.addMove(MoveOperand(d7), MoveOperand(d6), MoveOp::DOUBLE)); + sim->set_d_register_from_double(7, 7); + TRY(mr.addMove(MoveOperand(s13), MoveOperand(s9), MoveOp::FLOAT32)); + sim->set_s_register_from_float(13, 13); + TRY(mr.addMove(MoveOperand(s1), MoveOperand(s4), MoveOp::FLOAT32)); + sim->set_s_register_from_float(1, 1); + TRY(mr.addMove(MoveOperand(s29), MoveOperand(s22), MoveOp::FLOAT32)); + sim->set_s_register_from_float(29, 29); + TRY(mr.addMove(MoveOperand(s25), MoveOperand(s24), MoveOp::FLOAT32)); + sim->set_s_register_from_float(25, 25); + // don't explode! + TRY(mr.resolve()); + mover.emit(mr); + mover.finish(); + masm.abiret(); + JitCode* code = linkAndAllocate(cx, &masm); + sim->skipCalleeSavedRegsCheck = true; + sim->call(code->raw(), 1, 1); + double d; + float f; + sim->get_double_from_d_register(14, &d); + CHECK(int(d) == 9); + sim->get_float_from_s_register(25, &f); + CHECK(int(f) == 24); + sim->get_double_from_d_register(0, &d); + CHECK(int(d) == 3); + sim->get_float_from_s_register(31, &f); + CHECK(int(f) == 10); + sim->get_double_from_d_register(10, &d); + CHECK(int(d) == 1); + sim->get_float_from_s_register(10, &f); + CHECK(int(f) == 8); + sim->get_double_from_d_register(7, &d); + CHECK(int(d) == 2); + sim->get_float_from_s_register(18, &f); + CHECK(int(f) == 20); + sim->get_float_from_s_register(3, &f); + CHECK(int(f) == 1); + sim->get_float_from_s_register(11, &f); + CHECK(int(f) == 17); + sim->get_float_from_s_register(30, &f); + CHECK(int(f) == 22); + sim->get_float_from_s_register(7, &f); + CHECK(int(f) == 31); + sim->get_double_from_d_register(13, &d); + CHECK(int(d) == 3); + sim->get_double_from_d_register(8, &d); + CHECK(int(d) == 9); + sim->get_float_from_s_register(23, &f); + CHECK(int(f) == 31); + sim->get_float_from_s_register(8, &f); + CHECK(int(f) == 13); + sim->get_float_from_s_register(5, &f); + CHECK(int(f) == 28); + sim->get_float_from_s_register(19, &f); + CHECK(int(f) == 31); + sim->get_float_from_s_register(6, &f); + CHECK(int(f) == 20); + sim->get_float_from_s_register(2, &f); + CHECK(int(f) == 0); + sim->get_double_from_d_register(6, &d); + CHECK(int(d) == 7); + sim->get_float_from_s_register(9, &f); + CHECK(int(f) == 13); + sim->get_float_from_s_register(4, &f); + CHECK(int(f) == 1); + sim->get_float_from_s_register(22, &f); + CHECK(int(f) == 29); + sim->get_float_from_s_register(24, &f); + CHECK(int(f) == 25); + return true; +} +END_TEST(testJitMoveEmitterCycles_autogen) + +BEGIN_TEST(testJitMoveEmitterCycles_autogen2) { + using namespace js; + using namespace js::jit; + LifoAlloc lifo(LIFO_ALLOC_PRIMARY_CHUNK_SIZE); + TempAllocator alloc(&lifo); + JitContext jc(cx); + StackMacroAssembler masm(cx, alloc); + AutoCreatedBy acb(masm, __func__); + MoveEmitter mover(masm); + MoveResolver mr; + mr.setAllocator(alloc); + Simulator* sim = Simulator::Current(); + TRY(mr.addMove(MoveOperand(d10), MoveOperand(d0), MoveOp::DOUBLE)); + sim->set_d_register_from_double(10, 10); + TRY(mr.addMove(MoveOperand(s15), MoveOperand(s3), MoveOp::FLOAT32)); + sim->set_s_register_from_float(15, 15); + TRY(mr.addMove(MoveOperand(s2), MoveOperand(s28), MoveOp::FLOAT32)); + sim->set_s_register_from_float(2, 2); + TRY(mr.addMove(MoveOperand(s30), MoveOperand(s25), MoveOp::FLOAT32)); + sim->set_s_register_from_float(30, 30); + TRY(mr.addMove(MoveOperand(s16), MoveOperand(s2), MoveOp::FLOAT32)); + sim->set_s_register_from_float(16, 16); + TRY(mr.addMove(MoveOperand(s2), MoveOperand(s29), MoveOp::FLOAT32)); + sim->set_s_register_from_float(2, 2); + TRY(mr.addMove(MoveOperand(s17), MoveOperand(s10), MoveOp::FLOAT32)); + sim->set_s_register_from_float(17, 17); + TRY(mr.addMove(MoveOperand(s2), MoveOperand(s19), MoveOp::FLOAT32)); + sim->set_s_register_from_float(2, 2); + TRY(mr.addMove(MoveOperand(s9), MoveOperand(s26), MoveOp::FLOAT32)); + sim->set_s_register_from_float(9, 9); + TRY(mr.addMove(MoveOperand(s1), MoveOperand(s23), MoveOp::FLOAT32)); + sim->set_s_register_from_float(1, 1); + TRY(mr.addMove(MoveOperand(s8), MoveOperand(s6), MoveOp::FLOAT32)); + sim->set_s_register_from_float(8, 8); + TRY(mr.addMove(MoveOperand(s24), MoveOperand(s16), MoveOp::FLOAT32)); + sim->set_s_register_from_float(24, 24); + TRY(mr.addMove(MoveOperand(s19), MoveOperand(s4), MoveOp::FLOAT32)); + sim->set_s_register_from_float(19, 19); + TRY(mr.addMove(MoveOperand(d5), MoveOperand(d6), MoveOp::DOUBLE)); + sim->set_d_register_from_double(5, 5); + TRY(mr.addMove(MoveOperand(s18), MoveOperand(s15), MoveOp::FLOAT32)); + sim->set_s_register_from_float(18, 18); + TRY(mr.addMove(MoveOperand(s23), MoveOperand(s30), MoveOp::FLOAT32)); + sim->set_s_register_from_float(23, 23); + TRY(mr.addMove(MoveOperand(s27), MoveOperand(s17), MoveOp::FLOAT32)); + sim->set_s_register_from_float(27, 27); + TRY(mr.addMove(MoveOperand(d3), MoveOperand(d4), MoveOp::DOUBLE)); + sim->set_d_register_from_double(3, 3); + TRY(mr.addMove(MoveOperand(s14), MoveOperand(s27), MoveOp::FLOAT32)); + sim->set_s_register_from_float(14, 14); + TRY(mr.addMove(MoveOperand(s2), MoveOperand(s31), MoveOp::FLOAT32)); + sim->set_s_register_from_float(2, 2); + TRY(mr.addMove(MoveOperand(s2), MoveOperand(s24), MoveOp::FLOAT32)); + sim->set_s_register_from_float(2, 2); + TRY(mr.addMove(MoveOperand(s31), MoveOperand(s11), MoveOp::FLOAT32)); + sim->set_s_register_from_float(31, 31); + TRY(mr.addMove(MoveOperand(s0), MoveOperand(s18), MoveOp::FLOAT32)); + sim->set_s_register_from_float(0, 0); + TRY(mr.addMove(MoveOperand(s24), MoveOperand(s7), MoveOp::FLOAT32)); + sim->set_s_register_from_float(24, 24); + TRY(mr.addMove(MoveOperand(s0), MoveOperand(s21), MoveOp::FLOAT32)); + sim->set_s_register_from_float(0, 0); + TRY(mr.addMove(MoveOperand(s27), MoveOperand(s20), MoveOp::FLOAT32)); + sim->set_s_register_from_float(27, 27); + TRY(mr.addMove(MoveOperand(s14), MoveOperand(s5), MoveOp::FLOAT32)); + sim->set_s_register_from_float(14, 14); + TRY(mr.addMove(MoveOperand(s2), MoveOperand(s14), MoveOp::FLOAT32)); + sim->set_s_register_from_float(2, 2); + TRY(mr.addMove(MoveOperand(s12), MoveOperand(s22), MoveOp::FLOAT32)); + sim->set_s_register_from_float(12, 12); + // don't explode! + TRY(mr.resolve()); + mover.emit(mr); + mover.finish(); + masm.abiret(); + JitCode* code = linkAndAllocate(cx, &masm); + sim->skipCalleeSavedRegsCheck = true; + sim->call(code->raw(), 1, 1); + + double d; + float f; + sim->get_double_from_d_register(0, &d); + CHECK(int(d) == 10); + sim->get_float_from_s_register(3, &f); + CHECK(int(f) == 15); + sim->get_float_from_s_register(28, &f); + CHECK(int(f) == 2); + sim->get_float_from_s_register(25, &f); + CHECK(int(f) == 30); + sim->get_float_from_s_register(2, &f); + CHECK(int(f) == 16); + sim->get_float_from_s_register(29, &f); + CHECK(int(f) == 2); + sim->get_float_from_s_register(10, &f); + CHECK(int(f) == 17); + sim->get_float_from_s_register(19, &f); + CHECK(int(f) == 2); + sim->get_float_from_s_register(26, &f); + CHECK(int(f) == 9); + sim->get_float_from_s_register(23, &f); + CHECK(int(f) == 1); + sim->get_float_from_s_register(6, &f); + CHECK(int(f) == 8); + sim->get_float_from_s_register(16, &f); + CHECK(int(f) == 24); + sim->get_float_from_s_register(4, &f); + CHECK(int(f) == 19); + sim->get_double_from_d_register(6, &d); + CHECK(int(d) == 5); + sim->get_float_from_s_register(15, &f); + CHECK(int(f) == 18); + sim->get_float_from_s_register(30, &f); + CHECK(int(f) == 23); + sim->get_float_from_s_register(17, &f); + CHECK(int(f) == 27); + sim->get_double_from_d_register(4, &d); + CHECK(int(d) == 3); + sim->get_float_from_s_register(27, &f); + CHECK(int(f) == 14); + sim->get_float_from_s_register(31, &f); + CHECK(int(f) == 2); + sim->get_float_from_s_register(24, &f); + CHECK(int(f) == 2); + sim->get_float_from_s_register(11, &f); + CHECK(int(f) == 31); + sim->get_float_from_s_register(18, &f); + CHECK(int(f) == 0); + sim->get_float_from_s_register(7, &f); + CHECK(int(f) == 24); + sim->get_float_from_s_register(21, &f); + CHECK(int(f) == 0); + sim->get_float_from_s_register(20, &f); + CHECK(int(f) == 27); + sim->get_float_from_s_register(5, &f); + CHECK(int(f) == 14); + sim->get_float_from_s_register(14, &f); + CHECK(int(f) == 2); + sim->get_float_from_s_register(22, &f); + CHECK(int(f) == 12); + return true; +} +END_TEST(testJitMoveEmitterCycles_autogen2) + +BEGIN_TEST(testJitMoveEmitterCycles_autogen3) { + using namespace js; + using namespace js::jit; + LifoAlloc lifo(LIFO_ALLOC_PRIMARY_CHUNK_SIZE); + TempAllocator alloc(&lifo); + JitContext jc(cx); + StackMacroAssembler masm(cx, alloc); + AutoCreatedBy acb(masm, __func__); + MoveEmitter mover(masm); + MoveResolver mr; + mr.setAllocator(alloc); + Simulator* sim = Simulator::Current(); + TRY(mr.addMove(MoveOperand(s0), MoveOperand(s21), MoveOp::FLOAT32)); + sim->set_s_register_from_float(0, 0); + TRY(mr.addMove(MoveOperand(s2), MoveOperand(s26), MoveOp::FLOAT32)); + sim->set_s_register_from_float(2, 2); + TRY(mr.addMove(MoveOperand(s19), MoveOperand(s20), MoveOp::FLOAT32)); + sim->set_s_register_from_float(19, 19); + TRY(mr.addMove(MoveOperand(s4), MoveOperand(s24), MoveOp::FLOAT32)); + sim->set_s_register_from_float(4, 4); + TRY(mr.addMove(MoveOperand(s22), MoveOperand(s9), MoveOp::FLOAT32)); + sim->set_s_register_from_float(22, 22); + TRY(mr.addMove(MoveOperand(s5), MoveOperand(s28), MoveOp::FLOAT32)); + sim->set_s_register_from_float(5, 5); + TRY(mr.addMove(MoveOperand(s15), MoveOperand(s7), MoveOp::FLOAT32)); + sim->set_s_register_from_float(15, 15); + TRY(mr.addMove(MoveOperand(s26), MoveOperand(s14), MoveOp::FLOAT32)); + sim->set_s_register_from_float(26, 26); + TRY(mr.addMove(MoveOperand(s13), MoveOperand(s30), MoveOp::FLOAT32)); + sim->set_s_register_from_float(13, 13); + TRY(mr.addMove(MoveOperand(s26), MoveOperand(s22), MoveOp::FLOAT32)); + sim->set_s_register_from_float(26, 26); + TRY(mr.addMove(MoveOperand(s21), MoveOperand(s6), MoveOp::FLOAT32)); + sim->set_s_register_from_float(21, 21); + TRY(mr.addMove(MoveOperand(s23), MoveOperand(s31), MoveOp::FLOAT32)); + sim->set_s_register_from_float(23, 23); + TRY(mr.addMove(MoveOperand(s7), MoveOperand(s12), MoveOp::FLOAT32)); + sim->set_s_register_from_float(7, 7); + TRY(mr.addMove(MoveOperand(s14), MoveOperand(s10), MoveOp::FLOAT32)); + sim->set_s_register_from_float(14, 14); + TRY(mr.addMove(MoveOperand(d12), MoveOperand(d8), MoveOp::DOUBLE)); + sim->set_d_register_from_double(12, 12); + TRY(mr.addMove(MoveOperand(s5), MoveOperand(s1), MoveOp::FLOAT32)); + sim->set_s_register_from_float(5, 5); + TRY(mr.addMove(MoveOperand(d12), MoveOperand(d2), MoveOp::DOUBLE)); + sim->set_d_register_from_double(12, 12); + TRY(mr.addMove(MoveOperand(s3), MoveOperand(s8), MoveOp::FLOAT32)); + sim->set_s_register_from_float(3, 3); + TRY(mr.addMove(MoveOperand(s14), MoveOperand(s0), MoveOp::FLOAT32)); + sim->set_s_register_from_float(14, 14); + TRY(mr.addMove(MoveOperand(s28), MoveOperand(s29), MoveOp::FLOAT32)); + sim->set_s_register_from_float(28, 28); + TRY(mr.addMove(MoveOperand(d12), MoveOperand(d9), MoveOp::DOUBLE)); + sim->set_d_register_from_double(12, 12); + TRY(mr.addMove(MoveOperand(s29), MoveOperand(s2), MoveOp::FLOAT32)); + sim->set_s_register_from_float(29, 29); + TRY(mr.addMove(MoveOperand(s22), MoveOperand(s27), MoveOp::FLOAT32)); + sim->set_s_register_from_float(22, 22); + TRY(mr.addMove(MoveOperand(s19), MoveOperand(s3), MoveOp::FLOAT32)); + sim->set_s_register_from_float(19, 19); + TRY(mr.addMove(MoveOperand(s21), MoveOperand(s11), MoveOp::FLOAT32)); + sim->set_s_register_from_float(21, 21); + TRY(mr.addMove(MoveOperand(s22), MoveOperand(s13), MoveOp::FLOAT32)); + sim->set_s_register_from_float(22, 22); + TRY(mr.addMove(MoveOperand(s29), MoveOperand(s25), MoveOp::FLOAT32)); + sim->set_s_register_from_float(29, 29); + TRY(mr.addMove(MoveOperand(s29), MoveOperand(s15), MoveOp::FLOAT32)); + sim->set_s_register_from_float(29, 29); + TRY(mr.addMove(MoveOperand(s16), MoveOperand(s23), MoveOp::FLOAT32)); + sim->set_s_register_from_float(16, 16); + // don't explode! + TRY(mr.resolve()); + mover.emit(mr); + mover.finish(); + masm.abiret(); + JitCode* code = linkAndAllocate(cx, &masm); + sim->skipCalleeSavedRegsCheck = true; + sim->call(code->raw(), 1, 1); + + float f; + double d; + sim->get_float_from_s_register(21, &f); + CHECK(int(f) == 0); + sim->get_float_from_s_register(26, &f); + CHECK(int(f) == 2); + sim->get_float_from_s_register(20, &f); + CHECK(int(f) == 19); + sim->get_float_from_s_register(24, &f); + CHECK(int(f) == 4); + sim->get_float_from_s_register(9, &f); + CHECK(int(f) == 22); + sim->get_float_from_s_register(28, &f); + CHECK(int(f) == 5); + sim->get_float_from_s_register(7, &f); + CHECK(int(f) == 15); + sim->get_float_from_s_register(14, &f); + CHECK(int(f) == 26); + sim->get_float_from_s_register(30, &f); + CHECK(int(f) == 13); + sim->get_float_from_s_register(22, &f); + CHECK(int(f) == 26); + sim->get_float_from_s_register(6, &f); + CHECK(int(f) == 21); + sim->get_float_from_s_register(31, &f); + CHECK(int(f) == 23); + sim->get_float_from_s_register(12, &f); + CHECK(int(f) == 7); + sim->get_float_from_s_register(10, &f); + CHECK(int(f) == 14); + sim->get_double_from_d_register(8, &d); + CHECK(int(d) == 12); + sim->get_float_from_s_register(1, &f); + CHECK(int(f) == 5); + sim->get_double_from_d_register(2, &d); + CHECK(int(d) == 12); + sim->get_float_from_s_register(8, &f); + CHECK(int(f) == 3); + sim->get_float_from_s_register(0, &f); + CHECK(int(f) == 14); + sim->get_float_from_s_register(29, &f); + CHECK(int(f) == 28); + sim->get_double_from_d_register(9, &d); + CHECK(int(d) == 12); + sim->get_float_from_s_register(2, &f); + CHECK(int(f) == 29); + sim->get_float_from_s_register(27, &f); + CHECK(int(f) == 22); + sim->get_float_from_s_register(3, &f); + CHECK(int(f) == 19); + sim->get_float_from_s_register(11, &f); + CHECK(int(f) == 21); + sim->get_float_from_s_register(13, &f); + CHECK(int(f) == 22); + sim->get_float_from_s_register(25, &f); + CHECK(int(f) == 29); + sim->get_float_from_s_register(15, &f); + CHECK(int(f) == 29); + sim->get_float_from_s_register(23, &f); + CHECK(int(f) == 16); + return true; +} +END_TEST(testJitMoveEmitterCycles_autogen3) +BEGIN_TEST(testJitMoveEmitterCycles_bug1299147_1) { + using namespace js; + using namespace js::jit; + LifoAlloc lifo(LIFO_ALLOC_PRIMARY_CHUNK_SIZE); + TempAllocator alloc(&lifo); + JitContext jc(cx); + StackMacroAssembler masm(cx, alloc); + AutoCreatedBy acb(masm, __func__); + MoveEmitter mover(masm); + MoveResolver mr; + mr.setAllocator(alloc); + Simulator* sim = Simulator::Current(); + // S2 -> S0 + // S2 -> S6 + // S3 -> S1 + // S3 -> S7 + // D0 -> D1 + // D0 -> D2 + TRY(mr.addMove(MoveOperand(s2), MoveOperand(s0), MoveOp::FLOAT32)); + TRY(mr.addMove(MoveOperand(s2), MoveOperand(s6), MoveOp::FLOAT32)); + sim->set_s_register_from_float(2, 2); + TRY(mr.addMove(MoveOperand(s3), MoveOperand(s1), MoveOp::FLOAT32)); + TRY(mr.addMove(MoveOperand(s3), MoveOperand(s7), MoveOp::FLOAT32)); + sim->set_s_register_from_float(3, 4); + TRY(mr.addMove(MoveOperand(d0), MoveOperand(d1), MoveOp::FLOAT32)); + TRY(mr.addMove(MoveOperand(d0), MoveOperand(d2), MoveOp::FLOAT32)); + sim->set_d_register_from_double(0, 1); + // don't explode! + TRY(mr.resolve()); + mover.emit(mr); + mover.finish(); + masm.abiret(); + JitCode* code = linkAndAllocate(cx, &masm); + sim->call(code->raw(), 1, 1); + float f; + double d; + sim->get_double_from_d_register(1, &d); + CHECK(d == 1); + sim->get_double_from_d_register(2, &d); + CHECK(d == 1); + sim->get_float_from_s_register(0, &f); + CHECK(int(f) == 2); + sim->get_float_from_s_register(6, &f); + CHECK(int(f) == 2); + sim->get_float_from_s_register(1, &f); + CHECK(int(f) == 4); + sim->get_float_from_s_register(7, &f); + CHECK(int(f) == 4); + return true; +} +END_TEST(testJitMoveEmitterCycles_bug1299147_1) +BEGIN_TEST(testJitMoveEmitterCycles_bug1299147) { + using namespace js; + using namespace js::jit; + LifoAlloc lifo(LIFO_ALLOC_PRIMARY_CHUNK_SIZE); + TempAllocator alloc(&lifo); + JitContext jc(cx); + StackMacroAssembler masm(cx, alloc); + AutoCreatedBy acb(masm, __func__); + MoveEmitter mover(masm); + MoveResolver mr; + mr.setAllocator(alloc); + Simulator* sim = Simulator::Current(); + // S2 -> S5 + // S2 -> S6 + // D0 -> D1 + TRY(mr.addMove(MoveOperand(s2), MoveOperand(s5), MoveOp::FLOAT32)); + TRY(mr.addMove(MoveOperand(s2), MoveOperand(s6), MoveOp::FLOAT32)); + sim->set_s_register_from_float(2, 2); + TRY(mr.addMove(MoveOperand(d0), MoveOperand(d1), MoveOp::FLOAT32)); + sim->set_d_register_from_double(0, 1); + // don't explode! + TRY(mr.resolve()); + mover.emit(mr); + mover.finish(); + masm.abiret(); + JitCode* code = linkAndAllocate(cx, &masm); + sim->call(code->raw(), 1, 1); + float f; + double d; + sim->get_double_from_d_register(1, &d); + CHECK(d == 1); + sim->get_float_from_s_register(5, &f); + CHECK(int(f) == 2); + sim->get_float_from_s_register(6, &f); + CHECK(int(f) == 2); + return true; +} +END_TEST(testJitMoveEmitterCycles_bug1299147) + +#endif // JS_SIMULATOR_ARM diff --git a/js/src/jsapi-tests/testJitRValueAlloc.cpp b/js/src/jsapi-tests/testJitRValueAlloc.cpp new file mode 100644 index 0000000000..27242b7652 --- /dev/null +++ b/js/src/jsapi-tests/testJitRValueAlloc.cpp @@ -0,0 +1,256 @@ +/* -*- 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 "jit/Snapshots.h" + +#include "jsapi-tests/tests.h" + +using namespace js; +using namespace js::jit; + +// These tests are checking that all slots of the current architecture can all +// be encoded and decoded correctly. We iterate on all registers and on many +// fake stack locations (Fibonacci). +static RValueAllocation Read(const RValueAllocation& slot) { + CompactBufferWriter writer; + slot.write(writer); + + // Call hash to run its assertions. + slot.hash(); + + CompactBufferReader reader(writer); + return RValueAllocation::read(reader); +} + +class Fibonacci { + class Iterator { + public: + // std::iterator traits. + using iterator_category = std::input_iterator_tag; + using value_type = int32_t; + using difference_type = int32_t; + using pointer = value_type*; + using reference = value_type&; + + private: + uint32_t value_{}; + uint32_t last_{}; + + Iterator() = default; + Iterator(value_type value, value_type last) : value_(value), last_(last) {} + + friend class Fibonacci; + + public: + Iterator& operator++() { + auto next = value_ + last_; + if (next <= static_cast<uint32_t>(INT32_MAX)) { + last_ = value_; + value_ = next; + } else { + *this = Iterator{}; + } + return *this; + } + + bool operator==(const Iterator& other) const { + return value_ == other.value_ && last_ == other.last_; + } + + bool operator!=(const Iterator& other) const { return !(*this == other); } + + auto operator*() const { return static_cast<int32_t>(value_); } + }; + + public: + auto begin() { return Iterator{0, 1}; } + + auto end() { return Iterator{}; } +}; + +BEGIN_TEST(testJitRValueAlloc_Double) { + RValueAllocation s; + for (uint32_t i = 0; i < FloatRegisters::Total; i++) { + s = RValueAllocation::Double(FloatRegister::FromCode(i)); + CHECK(s == Read(s)); + } + return true; +} +END_TEST(testJitRValueAlloc_Double) + +BEGIN_TEST(testJitRValueAlloc_FloatReg) { + RValueAllocation s; + for (uint32_t i = 0; i < FloatRegisters::Total; i++) { + s = RValueAllocation::AnyFloat(FloatRegister::FromCode(i)); + CHECK(s == Read(s)); + } + return true; +} +END_TEST(testJitRValueAlloc_FloatReg) + +BEGIN_TEST(testJitRValueAlloc_FloatStack) { + RValueAllocation s; + for (auto i : Fibonacci{}) { + s = RValueAllocation::AnyFloat(i); + CHECK(s == Read(s)); + } + return true; +} +END_TEST(testJitRValueAlloc_FloatStack) + +BEGIN_TEST(testJitRValueAlloc_TypedReg) { + RValueAllocation s; + for (uint32_t i = 0; i < Registers::Total; i++) { +#define FOR_EACH_JSVAL(_) \ + /* _(JSVAL_TYPE_DOUBLE) */ \ + _(JSVAL_TYPE_INT32) \ + /* _(JSVAL_TYPE_UNDEFINED) */ \ + _(JSVAL_TYPE_BOOLEAN) \ + /* _(JSVAL_TYPE_MAGIC) */ \ + _(JSVAL_TYPE_STRING) \ + _(JSVAL_TYPE_SYMBOL) \ + _(JSVAL_TYPE_BIGINT) \ + /* _(JSVAL_TYPE_NULL) */ \ + _(JSVAL_TYPE_OBJECT) + +#define CHECK_WITH_JSVAL(jsval) \ + s = RValueAllocation::Typed(jsval, Register::FromCode(i)); \ + CHECK(s == Read(s)); + + FOR_EACH_JSVAL(CHECK_WITH_JSVAL) +#undef CHECK_WITH_JSVAL +#undef FOR_EACH_JSVAL + } + return true; +} +END_TEST(testJitRValueAlloc_TypedReg) + +BEGIN_TEST(testJitRValueAlloc_TypedStack) { + RValueAllocation s; + for (auto i : Fibonacci{}) { +#define FOR_EACH_JSVAL(_) \ + _(JSVAL_TYPE_DOUBLE) \ + _(JSVAL_TYPE_INT32) \ + /* _(JSVAL_TYPE_UNDEFINED) */ \ + _(JSVAL_TYPE_BOOLEAN) \ + /* _(JSVAL_TYPE_MAGIC) */ \ + _(JSVAL_TYPE_STRING) \ + _(JSVAL_TYPE_SYMBOL) \ + _(JSVAL_TYPE_BIGINT) \ + /* _(JSVAL_TYPE_NULL) */ \ + _(JSVAL_TYPE_OBJECT) + +#define CHECK_WITH_JSVAL(jsval) \ + s = RValueAllocation::Typed(jsval, i); \ + CHECK(s == Read(s)); + + FOR_EACH_JSVAL(CHECK_WITH_JSVAL) +#undef CHECK_WITH_JSVAL +#undef FOR_EACH_JSVAL + } + return true; +} +END_TEST(testJitRValueAlloc_TypedStack) + +#if defined(JS_NUNBOX32) + +BEGIN_TEST(testJitRValueAlloc_UntypedRegReg) { + RValueAllocation s; + for (uint32_t i = 0; i < Registers::Total; i++) { + for (uint32_t j = 0; j < Registers::Total; j++) { + if (i == j) { + continue; + } + s = RValueAllocation::Untyped(Register::FromCode(i), + Register::FromCode(j)); + MOZ_ASSERT(s == Read(s)); + CHECK(s == Read(s)); + } + } + return true; +} +END_TEST(testJitRValueAlloc_UntypedRegReg) + +BEGIN_TEST(testJitRValueAlloc_UntypedRegStack) { + RValueAllocation s; + for (uint32_t i = 0; i < Registers::Total; i++) { + for (auto j : Fibonacci{}) { + s = RValueAllocation::Untyped(Register::FromCode(i), j); + CHECK(s == Read(s)); + } + } + return true; +} +END_TEST(testJitRValueAlloc_UntypedRegStack) + +BEGIN_TEST(testJitRValueAlloc_UntypedStackReg) { + RValueAllocation s; + for (auto i : Fibonacci{}) { + for (uint32_t j = 0; j < Registers::Total; j++) { + s = RValueAllocation::Untyped(i, Register::FromCode(j)); + CHECK(s == Read(s)); + } + } + return true; +} +END_TEST(testJitRValueAlloc_UntypedStackReg) + +BEGIN_TEST(testJitRValueAlloc_UntypedStackStack) { + RValueAllocation s; + for (auto i : Fibonacci{}) { + for (auto j : Fibonacci{}) { + s = RValueAllocation::Untyped(i, j); + CHECK(s == Read(s)); + } + } + return true; +} +END_TEST(testJitRValueAlloc_UntypedStackStack) + +#else + +BEGIN_TEST(testJitRValueAlloc_UntypedReg) { + RValueAllocation s; + for (uint32_t i = 0; i < Registers::Total; i++) { + s = RValueAllocation::Untyped(Register::FromCode(i)); + CHECK(s == Read(s)); + } + return true; +} +END_TEST(testJitRValueAlloc_UntypedReg) + +BEGIN_TEST(testJitRValueAlloc_UntypedStack) { + RValueAllocation s; + for (auto i : Fibonacci{}) { + s = RValueAllocation::Untyped(i); + CHECK(s == Read(s)); + } + return true; +} +END_TEST(testJitRValueAlloc_UntypedStack) + +#endif + +BEGIN_TEST(testJitRValueAlloc_UndefinedAndNull) { + RValueAllocation s; + s = RValueAllocation::Undefined(); + CHECK(s == Read(s)); + s = RValueAllocation::Null(); + CHECK(s == Read(s)); + return true; +} +END_TEST(testJitRValueAlloc_UndefinedAndNull) + +BEGIN_TEST(testJitRValueAlloc_ConstantPool) { + RValueAllocation s; + for (auto i : Fibonacci{}) { + s = RValueAllocation::ConstantPool(i); + CHECK(s == Read(s)); + } + return true; +} +END_TEST(testJitRValueAlloc_ConstantPool) diff --git a/js/src/jsapi-tests/testJitRangeAnalysis.cpp b/js/src/jsapi-tests/testJitRangeAnalysis.cpp new file mode 100644 index 0000000000..3414ef2310 --- /dev/null +++ b/js/src/jsapi-tests/testJitRangeAnalysis.cpp @@ -0,0 +1,368 @@ +/* -*- 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 "jit/IonAnalysis.h" +#include "jit/MIRGenerator.h" +#include "jit/MIRGraph.h" +#include "jit/RangeAnalysis.h" + +#include "jsapi-tests/testJitMinimalFunc.h" +#include "jsapi-tests/tests.h" + +using namespace js; +using namespace js::jit; + +static bool EquivalentRanges(const Range* a, const Range* b) { + if (a->hasInt32UpperBound() != b->hasInt32UpperBound()) { + return false; + } + if (a->hasInt32LowerBound() != b->hasInt32LowerBound()) { + return false; + } + if (a->hasInt32UpperBound() && (a->upper() != b->upper())) { + return false; + } + if (a->hasInt32LowerBound() && (a->lower() != b->lower())) { + return false; + } + if (a->canHaveFractionalPart() != b->canHaveFractionalPart()) { + return false; + } + if (a->canBeNegativeZero() != b->canBeNegativeZero()) { + return false; + } + if (a->canBeNaN() != b->canBeNaN()) { + return false; + } + if (a->canBeInfiniteOrNaN() != b->canBeInfiniteOrNaN()) { + return false; + } + if (!a->canBeInfiniteOrNaN() && (a->exponent() != b->exponent())) { + return false; + } + return true; +} + +BEGIN_TEST(testJitRangeAnalysis_MathSign) { + MinimalAlloc func; + + Range* xnan = new (func.alloc) Range(); + + Range* ninf = Range::NewDoubleSingletonRange( + func.alloc, mozilla::NegativeInfinity<double>()); + Range* n1_5 = Range::NewDoubleSingletonRange(func.alloc, -1.5); + Range* n1_0 = Range::NewDoubleSingletonRange(func.alloc, -1); + Range* n0_5 = Range::NewDoubleSingletonRange(func.alloc, -0.5); + Range* n0_0 = Range::NewDoubleSingletonRange(func.alloc, -0.0); + + Range* p0_0 = Range::NewDoubleSingletonRange(func.alloc, 0.0); + Range* p0_5 = Range::NewDoubleSingletonRange(func.alloc, 0.5); + Range* p1_0 = Range::NewDoubleSingletonRange(func.alloc, 1.0); + Range* p1_5 = Range::NewDoubleSingletonRange(func.alloc, 1.5); + Range* pinf = Range::NewDoubleSingletonRange( + func.alloc, mozilla::PositiveInfinity<double>()); + + Range* xnanSign = Range::sign(func.alloc, xnan); + + Range* ninfSign = Range::sign(func.alloc, ninf); + Range* n1_5Sign = Range::sign(func.alloc, n1_5); + Range* n1_0Sign = Range::sign(func.alloc, n1_0); + Range* n0_5Sign = Range::sign(func.alloc, n0_5); + Range* n0_0Sign = Range::sign(func.alloc, n0_0); + + Range* p0_0Sign = Range::sign(func.alloc, p0_0); + Range* p0_5Sign = Range::sign(func.alloc, p0_5); + Range* p1_0Sign = Range::sign(func.alloc, p1_0); + Range* p1_5Sign = Range::sign(func.alloc, p1_5); + Range* pinfSign = Range::sign(func.alloc, pinf); + + CHECK(!xnanSign); + CHECK(EquivalentRanges(ninfSign, + Range::NewInt32SingletonRange(func.alloc, -1))); + CHECK(EquivalentRanges(n1_5Sign, + Range::NewInt32SingletonRange(func.alloc, -1))); + CHECK(EquivalentRanges(n1_0Sign, + Range::NewInt32SingletonRange(func.alloc, -1))); + + // This should ideally be just -1, but range analysis can't represent the + // specific fractional range of the constant. + CHECK(EquivalentRanges(n0_5Sign, Range::NewInt32Range(func.alloc, -1, 0))); + + CHECK(EquivalentRanges(n0_0Sign, + Range::NewDoubleSingletonRange(func.alloc, -0.0))); + + CHECK(!n0_0Sign->canHaveFractionalPart()); + CHECK(n0_0Sign->canBeNegativeZero()); + CHECK(n0_0Sign->lower() == 0); + CHECK(n0_0Sign->upper() == 0); + + CHECK( + EquivalentRanges(p0_0Sign, Range::NewInt32SingletonRange(func.alloc, 0))); + + CHECK(!p0_0Sign->canHaveFractionalPart()); + CHECK(!p0_0Sign->canBeNegativeZero()); + CHECK(p0_0Sign->lower() == 0); + CHECK(p0_0Sign->upper() == 0); + + // This should ideally be just 1, but range analysis can't represent the + // specific fractional range of the constant. + CHECK(EquivalentRanges(p0_5Sign, Range::NewInt32Range(func.alloc, 0, 1))); + + CHECK( + EquivalentRanges(p1_0Sign, Range::NewInt32SingletonRange(func.alloc, 1))); + CHECK( + EquivalentRanges(p1_5Sign, Range::NewInt32SingletonRange(func.alloc, 1))); + CHECK( + EquivalentRanges(pinfSign, Range::NewInt32SingletonRange(func.alloc, 1))); + + return true; +} +END_TEST(testJitRangeAnalysis_MathSign) + +BEGIN_TEST(testJitRangeAnalysis_MathSignBeta) { + MinimalFunc func; + + MBasicBlock* entry = func.createEntryBlock(); + MBasicBlock* thenBlock = func.createBlock(entry); + MBasicBlock* elseBlock = func.createBlock(entry); + MBasicBlock* elseThenBlock = func.createBlock(elseBlock); + MBasicBlock* elseElseBlock = func.createBlock(elseBlock); + + // if (p < 0) + MParameter* p = func.createParameter(); + entry->add(p); + MConstant* c0 = MConstant::New(func.alloc, DoubleValue(0.0)); + entry->add(c0); + MConstant* cm0 = MConstant::New(func.alloc, DoubleValue(-0.0)); + entry->add(cm0); + MCompare* cmp = + MCompare::New(func.alloc, p, c0, JSOp::Lt, MCompare::Compare_Double); + entry->add(cmp); + entry->end(MTest::New(func.alloc, cmp, thenBlock, elseBlock)); + + // { + // return Math.sign(p + -0); + // } + MAdd* thenAdd = MAdd::New(func.alloc, p, cm0, MIRType::Double); + thenBlock->add(thenAdd); + MSign* thenSign = MSign::New(func.alloc, thenAdd, MIRType::Double); + thenBlock->add(thenSign); + MReturn* thenRet = MReturn::New(func.alloc, thenSign); + thenBlock->end(thenRet); + + // else + // { + // if (p >= 0) + MCompare* elseCmp = + MCompare::New(func.alloc, p, c0, JSOp::Ge, MCompare::Compare_Double); + elseBlock->add(elseCmp); + elseBlock->end(MTest::New(func.alloc, elseCmp, elseThenBlock, elseElseBlock)); + + // { + // return Math.sign(p + -0); + // } + MAdd* elseThenAdd = MAdd::New(func.alloc, p, cm0, MIRType::Double); + elseThenBlock->add(elseThenAdd); + MSign* elseThenSign = MSign::New(func.alloc, elseThenAdd, MIRType::Double); + elseThenBlock->add(elseThenSign); + MReturn* elseThenRet = MReturn::New(func.alloc, elseThenSign); + elseThenBlock->end(elseThenRet); + + // else + // { + // return Math.sign(p + -0); + // } + // } + MAdd* elseElseAdd = MAdd::New(func.alloc, p, cm0, MIRType::Double); + elseElseBlock->add(elseElseAdd); + MSign* elseElseSign = MSign::New(func.alloc, elseElseAdd, MIRType::Double); + elseElseBlock->add(elseElseSign); + MReturn* elseElseRet = MReturn::New(func.alloc, elseElseSign); + elseElseBlock->end(elseElseRet); + + if (!func.runRangeAnalysis()) { + return false; + } + + CHECK(!p->range()); + CHECK(EquivalentRanges(c0->range(), + Range::NewDoubleSingletonRange(func.alloc, 0.0))); + CHECK(EquivalentRanges(cm0->range(), + Range::NewDoubleSingletonRange(func.alloc, -0.0))); + + // On the (p < 0) side, p is negative and not -0 (surprise!) + CHECK(EquivalentRanges( + thenAdd->range(), + new (func.alloc) + Range(Range::NoInt32LowerBound, 0, Range::IncludesFractionalParts, + Range::ExcludesNegativeZero, Range::IncludesInfinity))); + + // Consequently, its Math.sign value is not -0 either. + CHECK(EquivalentRanges(thenSign->range(), + new (func.alloc) + Range(-1, 0, Range::ExcludesFractionalParts, + Range::ExcludesNegativeZero, 0))); + + // On the (p >= 0) side, p is not negative and may be -0 (surprise!) + CHECK(EquivalentRanges( + elseThenAdd->range(), + new (func.alloc) + Range(0, Range::NoInt32UpperBound, Range::IncludesFractionalParts, + Range::IncludesNegativeZero, Range::IncludesInfinity))); + + // Consequently, its Math.sign value may be -0 too. + CHECK(EquivalentRanges(elseThenSign->range(), + new (func.alloc) + Range(0, 1, Range::ExcludesFractionalParts, + Range::IncludesNegativeZero, 0))); + + // Otherwise, p may be NaN. + CHECK(elseElseAdd->range()->isUnknown()); + CHECK(!elseElseSign->range()); + + return true; +} +END_TEST(testJitRangeAnalysis_MathSignBeta) + +BEGIN_TEST(testJitRangeAnalysis_StrictCompareBeta) { + MinimalFunc func; + + MBasicBlock* entry = func.createEntryBlock(); + MBasicBlock* thenBlock = func.createBlock(entry); + MBasicBlock* elseBlock = func.createBlock(entry); + + // if (p === 0) + MParameter* p = func.createParameter(); + entry->add(p); + MConstant* c0 = MConstant::New(func.alloc, DoubleValue(0.0)); + entry->add(c0); + MCompare* cmp = MCompare::New(func.alloc, p, c0, JSOp::StrictEq, + MCompare::Compare_Double); + entry->add(cmp); + auto* test = MTest::New(func.alloc, cmp, thenBlock, elseBlock); + entry->end(test); + + // { + // return p + -0; + // } + MConstant* cm0 = MConstant::New(func.alloc, DoubleValue(-0.0)); + thenBlock->add(cm0); + MAdd* thenAdd = MAdd::New(func.alloc, p, cm0, MIRType::Double); + thenBlock->add(thenAdd); + MReturn* thenRet = MReturn::New(func.alloc, thenAdd); + thenBlock->end(thenRet); + + // else + // { + // return 0; + // } + MReturn* elseRet = MReturn::New(func.alloc, c0); + elseBlock->end(elseRet); + + // If range analysis inserts a beta node for p, it will be able to compute + // a meaningful range for p + -0. + + auto replaceCompare = [&](auto compareType) { + auto* newCmp = + MCompare::New(func.alloc, p, c0, JSOp::StrictEq, compareType); + entry->insertBefore(cmp, newCmp); + test->replaceOperand(0, newCmp); + cmp = newCmp; + }; + + // We can't do beta node insertion with StrictEq and a non-numeric + // comparison though. + for (auto compareType : + {MCompare::Compare_Object, MCompare::Compare_String}) { + replaceCompare(compareType); + + if (!func.runRangeAnalysis()) { + return false; + } + CHECK(!thenAdd->range() || thenAdd->range()->isUnknown()); + ClearDominatorTree(func.graph); + } + + // We can do it with a numeric comparison. + replaceCompare(MCompare::Compare_Double); + if (!func.runRangeAnalysis()) { + return false; + } + CHECK(EquivalentRanges(thenAdd->range(), + Range::NewDoubleRange(func.alloc, 0.0, 0.0))); + + return true; +} +END_TEST(testJitRangeAnalysis_StrictCompareBeta) + +static void deriveShiftRightRange(int32_t lhsLower, int32_t lhsUpper, + int32_t rhsLower, int32_t rhsUpper, + int32_t* min, int32_t* max) { + // This is the reference algorithm and should be verifiable by inspection. + int64_t i, j; + *min = INT32_MAX; + *max = INT32_MIN; + for (i = lhsLower; i <= lhsUpper; i++) { + for (j = rhsLower; j <= rhsUpper; j++) { + int32_t r = int32_t(i) >> (int32_t(j) & 0x1f); + if (r > *max) *max = r; + if (r < *min) *min = r; + } + } +} + +static bool checkShiftRightRange(int32_t lhsLow, int32_t lhsHigh, + int32_t lhsInc, int32_t rhsLow, + int32_t rhsHigh, int32_t rhsInc) { + MinimalAlloc func; + int64_t lhsLower, lhsUpper, rhsLower, rhsUpper; + + for (lhsLower = lhsLow; lhsLower <= lhsHigh; lhsLower += lhsInc) { + for (lhsUpper = lhsLower; lhsUpper <= lhsHigh; lhsUpper += lhsInc) { + Range* lhsRange = Range::NewInt32Range(func.alloc, lhsLower, lhsUpper); + for (rhsLower = rhsLow; rhsLower <= rhsHigh; rhsLower += rhsInc) { + for (rhsUpper = rhsLower; rhsUpper <= rhsHigh; rhsUpper += rhsInc) { + if (!func.alloc.ensureBallast()) { + return false; + } + + Range* rhsRange = + Range::NewInt32Range(func.alloc, rhsLower, rhsUpper); + Range* result = Range::rsh(func.alloc, lhsRange, rhsRange); + int32_t min, max; + deriveShiftRightRange(lhsLower, lhsUpper, rhsLower, rhsUpper, &min, + &max); + if (!result->isInt32() || result->lower() != min || + result->upper() != max) { + return false; + } + } + } + } + } + return true; +} + +BEGIN_TEST(testJitRangeAnalysis_shiftRight) { + CHECK(checkShiftRightRange(-16, 15, 1, 0, 31, 1)); + CHECK(checkShiftRightRange(-8, 7, 1, -64, 63, 1)); + return true; +} +END_TEST(testJitRangeAnalysis_shiftRight) + +BEGIN_TEST(testJitRangeAnalysis_MathCeil) { + MinimalAlloc func; + + Range* n0_5 = Range::NewDoubleSingletonRange(func.alloc, -0.5); + Range* n0_5Ceil = Range::ceil(func.alloc, n0_5); + + CHECK(n0_5Ceil); + CHECK(n0_5Ceil->canBeNegativeZero()); + + return true; +} +END_TEST(testJitRangeAnalysis_MathCeil) diff --git a/js/src/jsapi-tests/testJitRegisterSet.cpp b/js/src/jsapi-tests/testJitRegisterSet.cpp new file mode 100644 index 0000000000..098fb5f9d5 --- /dev/null +++ b/js/src/jsapi-tests/testJitRegisterSet.cpp @@ -0,0 +1,211 @@ +/* -*- 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 "jit/RegisterSets.h" + +#include "jsapi-tests/tests.h" + +using namespace js; +using namespace js::jit; + +static bool CoPrime(size_t a, size_t b) { + if (b <= 1) { + return a == 1 || b == 1; + } + return CoPrime(b, a % b); +} + +// This macros are use to iterave over all registers in a large number of +// non-looping sequences, which does not rely on the getFirst / getLast +// functions. +#define BEGIN_INDEX_WALK(RegTotal) \ + static const size_t Total = RegTotal; \ + for (size_t walk = 1; walk < RegTotal; walk += 2) { \ + if (!CoPrime(RegTotal, walk)) continue; \ + for (size_t start = 0; start < RegTotal; start++) { \ + size_t index = start; + +#define END_INDEX_WALK \ + } \ + } + +#define BEGIN_All_WALK(RegTotal) \ + static const size_t Total = RegTotal; \ + size_t walk = 1; \ + size_t start = 0; \ + size_t index = start; + +#define FOR_ALL_REGISTERS(Register, reg) \ + do { \ + Register reg = Register::FromCode(index); + +#define END_FOR_ALL_REGISTERS \ + index = (index + walk) % Total; \ + } \ + while (index != start) + +BEGIN_TEST(testJitRegisterSet_GPR) { + BEGIN_INDEX_WALK(Registers::Total) + + LiveGeneralRegisterSet liveRegs; + AllocatableGeneralRegisterSet pool(GeneralRegisterSet::All()); + CHECK(liveRegs.empty()); + CHECK(pool.set() == GeneralRegisterSet::All()); + + FOR_ALL_REGISTERS(Register, reg) { + CHECK(!pool.has(reg) || !liveRegs.has(reg)); + if (pool.has(reg)) { + CHECK(!liveRegs.has(reg)); + pool.take(reg); + liveRegs.add(reg); + CHECK(liveRegs.has(reg)); + CHECK(!pool.has(reg)); + } + CHECK(!pool.has(reg) || !liveRegs.has(reg)); + } + END_FOR_ALL_REGISTERS; + + CHECK(pool.empty()); + + FOR_ALL_REGISTERS(Register, reg) { + CHECK(!pool.has(reg) || !liveRegs.has(reg)); + if (liveRegs.has(reg)) { + CHECK(!pool.has(reg)); + liveRegs.take(reg); + pool.add(reg); + CHECK(pool.has(reg)); + CHECK(!liveRegs.has(reg)); + } + CHECK(!pool.has(reg) || !liveRegs.has(reg)); + } + END_FOR_ALL_REGISTERS; + + CHECK(liveRegs.empty()); + CHECK(pool.set() == GeneralRegisterSet::All()); + + END_INDEX_WALK + return true; +} +END_TEST(testJitRegisterSet_GPR) + +BEGIN_TEST(testJitRegisterSet_FPU) { + BEGIN_INDEX_WALK(FloatRegisters::Total) + + LiveFloatRegisterSet liveRegs; + AllocatableFloatRegisterSet pool(FloatRegisterSet::All()); + CHECK(liveRegs.empty()); + CHECK(pool.set() == FloatRegisterSet::All()); + + FOR_ALL_REGISTERS(FloatRegister, reg) { + CHECK(!pool.has(reg) || !liveRegs.has(reg)); + if (pool.has(reg)) { + CHECK(!liveRegs.has(reg)); + pool.take(reg); + liveRegs.add(reg); + CHECK(liveRegs.has(reg)); + CHECK(!pool.has(reg)); + } + CHECK(!pool.has(reg) || !liveRegs.has(reg)); + } + END_FOR_ALL_REGISTERS; + + CHECK(pool.empty()); + + FOR_ALL_REGISTERS(FloatRegister, reg) { + CHECK(!pool.has(reg) || !liveRegs.has(reg)); + if (liveRegs.has(reg)) { + CHECK(!pool.has(reg)); + liveRegs.take(reg); + pool.add(reg); + CHECK(pool.has(reg)); + CHECK(!liveRegs.has(reg)); + } + CHECK(!pool.has(reg) || !liveRegs.has(reg)); + } + END_FOR_ALL_REGISTERS; + + CHECK(liveRegs.empty()); + CHECK(pool.set() == FloatRegisterSet::All()); + + END_INDEX_WALK + return true; +} +END_TEST(testJitRegisterSet_FPU) + +void pullAllFpus(AllocatableFloatRegisterSet& set, uint32_t& max_bits, + uint32_t bits) { + FloatRegisterSet allocSet(set.bits()); + FloatRegisterSet available_f32( + allocSet.allAllocatable<RegTypeName::Float32>()); + FloatRegisterSet available_f64( + allocSet.allAllocatable<RegTypeName::Float64>()); + FloatRegisterSet available_v128( + allocSet.allAllocatable<RegTypeName::Vector128>()); + for (FloatRegisterIterator it(available_f32); it.more(); ++it) { + FloatRegister tmp = *it; + set.take(tmp); + pullAllFpus(set, max_bits, bits + 32); + set.add(tmp); + } + for (FloatRegisterIterator it(available_f64); it.more(); ++it) { + FloatRegister tmp = *it; + set.take(tmp); + pullAllFpus(set, max_bits, bits + 64); + set.add(tmp); + } + for (FloatRegisterIterator it(available_v128); it.more(); ++it) { + FloatRegister tmp = *it; + set.take(tmp); + pullAllFpus(set, max_bits, bits + 128); + set.add(tmp); + } + if (bits >= max_bits) { + max_bits = bits; + } +} + +BEGIN_TEST(testJitRegisterSet_FPU_Aliases) { + BEGIN_All_WALK(FloatRegisters::Total); + FOR_ALL_REGISTERS(FloatRegister, reg) { + AllocatableFloatRegisterSet pool; + pool.add(reg); + + uint32_t alias_bits = 0; + for (uint32_t i = 0; i < reg.numAlignedAliased(); i++) { + FloatRegister alias = reg.alignedAliased(i); + + if (alias.isSingle()) { + if (alias_bits <= 32) { + alias_bits = 32; + } + } else if (alias.isDouble()) { + if (alias_bits <= 64) { + alias_bits = 64; + } + } else if (alias.isSimd128()) { + if (alias_bits <= 128) { + alias_bits = 128; + } + } + } + + uint32_t max_bits = 0; + pullAllFpus(pool, max_bits, 0); + + // By adding one register, we expect that we should not be able to pull + // more than any of its aligned aliases. This rule should hold for both + // x64 and ARM. + CHECK(max_bits <= alias_bits); + + // We added one register, we expect to be able to pull it back. + CHECK(max_bits > 0); + } + END_FOR_ALL_REGISTERS; + + return true; +} +END_TEST(testJitRegisterSet_FPU_Aliases) diff --git a/js/src/jsapi-tests/testLargeArrayBuffers.cpp b/js/src/jsapi-tests/testLargeArrayBuffers.cpp new file mode 100644 index 0000000000..510ce8d0b3 --- /dev/null +++ b/js/src/jsapi-tests/testLargeArrayBuffers.cpp @@ -0,0 +1,165 @@ +/* -*- 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 "js/ArrayBuffer.h" +#include "js/ArrayBufferMaybeShared.h" +#include "js/experimental/TypedData.h" +#include "js/SharedArrayBuffer.h" + +#include "jsapi-tests/tests.h" + +using namespace js; + +BEGIN_TEST(testLargeArrayBuffers) { +#ifdef JS_64BIT + constexpr size_t nbytes = size_t(6) * 1024 * 1024 * 1024; // 6 GB. + + RootedObject buffer(cx, JS::NewArrayBuffer(cx, nbytes)); + CHECK(buffer); + CHECK(JS::IsArrayBufferObject(buffer)); + + size_t length; + bool isShared; + uint8_t* data; + + // ArrayBuffer + { + CHECK_EQUAL(JS::GetArrayBufferByteLength(buffer), nbytes); + + JS::GetArrayBufferLengthAndData(buffer, &length, &isShared, &data); + CHECK_EQUAL(length, nbytes); + + length = 0; + JS::GetArrayBufferMaybeSharedLengthAndData(buffer, &length, &isShared, + &data); + CHECK_EQUAL(length, nbytes); + + length = 0; + CHECK(GetObjectAsArrayBuffer(buffer, &length, &data)); + CHECK_EQUAL(length, nbytes); + } + + // New Uint8Array + { + RootedObject tarr(cx, JS_NewUint8Array(cx, nbytes)); + CHECK(JS_IsTypedArrayObject(tarr)); + CHECK_EQUAL(JS_GetArrayBufferViewByteOffset(tarr), 0u); + CHECK_EQUAL(JS_GetArrayBufferViewByteLength(tarr), nbytes); + CHECK_EQUAL(JS_GetTypedArrayByteOffset(tarr), 0u); + CHECK_EQUAL(JS_GetTypedArrayByteLength(tarr), nbytes); + CHECK_EQUAL(JS_GetTypedArrayLength(tarr), nbytes); + + length = 0; + js::GetArrayBufferViewLengthAndData(tarr, &length, &isShared, &data); + CHECK_EQUAL(length, nbytes); + + length = 0; + js::GetUint8ArrayLengthAndData(tarr, &length, &isShared, &data); + CHECK_EQUAL(length, nbytes); + + length = 0; + JS::AutoCheckCannotGC nogc(cx); + mozilla::Span<uint8_t> span = + JS::TypedArray<Scalar::Uint8>::unwrap(tarr).getData(&isShared, nogc); + CHECK_EQUAL(span.Length(), nbytes); + + length = 0; + CHECK(JS_GetObjectAsArrayBufferView(tarr, &length, &isShared, &data)); + CHECK_EQUAL(length, nbytes); + } + + // Int16Array + { + RootedObject tarr(cx, + JS_NewInt16ArrayWithBuffer(cx, buffer, 0, nbytes / 2)); + CHECK(JS_IsTypedArrayObject(tarr)); + CHECK_EQUAL(JS_GetArrayBufferViewByteOffset(tarr), 0u); + CHECK_EQUAL(JS_GetArrayBufferViewByteLength(tarr), nbytes); + CHECK_EQUAL(JS_GetTypedArrayByteOffset(tarr), 0u); + CHECK_EQUAL(JS_GetTypedArrayByteLength(tarr), nbytes); + CHECK_EQUAL(JS_GetTypedArrayLength(tarr), nbytes / 2); + + length = 0; + js::GetArrayBufferViewLengthAndData(tarr, &length, &isShared, &data); + CHECK_EQUAL(length, nbytes); + + length = 0; + int16_t* int16Data; + js::GetInt16ArrayLengthAndData(tarr, &length, &isShared, &int16Data); + CHECK_EQUAL(length, nbytes / 2); + + length = 0; + JS::AutoCheckCannotGC nogc(cx); + mozilla::Span<short> span = + JS::TypedArray<Scalar::Int16>::unwrap(tarr).getData(&isShared, nogc); + CHECK_EQUAL(span.Length(), nbytes / 2); + + length = 0; + CHECK(JS_GetObjectAsArrayBufferView(tarr, &length, &isShared, &data)); + CHECK_EQUAL(length, nbytes); + } + + // DataView + { + RootedObject dv(cx, JS_NewDataView(cx, buffer, 0, nbytes - 10)); + CHECK(JS_IsArrayBufferViewObject(dv)); + CHECK_EQUAL(JS_GetArrayBufferViewByteOffset(dv), 0u); + CHECK_EQUAL(JS_GetArrayBufferViewByteLength(dv), nbytes - 10); + + length = 0; + js::GetArrayBufferViewLengthAndData(dv, &length, &isShared, &data); + CHECK_EQUAL(length, nbytes - 10); + + length = 0; + CHECK(JS_GetObjectAsArrayBufferView(dv, &length, &isShared, &data)); + CHECK_EQUAL(length, nbytes - 10); + } + + // Int8Array with large byteOffset. + { + RootedObject tarr(cx, + JS_NewInt8ArrayWithBuffer(cx, buffer, nbytes - 200, 32)); + CHECK(JS_IsTypedArrayObject(tarr)); + CHECK_EQUAL(JS_GetArrayBufferViewByteOffset(tarr), nbytes - 200); + CHECK_EQUAL(JS_GetArrayBufferViewByteLength(tarr), 32u); + CHECK_EQUAL(JS_GetTypedArrayByteOffset(tarr), nbytes - 200); + CHECK_EQUAL(JS_GetTypedArrayByteLength(tarr), 32u); + CHECK_EQUAL(JS_GetTypedArrayLength(tarr), 32u); + } + + // DataView with large byteOffset. + { + RootedObject dv(cx, JS_NewDataView(cx, buffer, nbytes - 200, 32)); + CHECK(JS_IsArrayBufferViewObject(dv)); + CHECK_EQUAL(JS_GetArrayBufferViewByteOffset(dv), nbytes - 200); + CHECK_EQUAL(JS_GetArrayBufferViewByteLength(dv), 32u); + } +#endif + + return true; +} +END_TEST(testLargeArrayBuffers) + +BEGIN_TEST(testLargeSharedArrayBuffers) { +#ifdef JS_64BIT + constexpr size_t nbytes = size_t(5) * 1024 * 1024 * 1024; // 5 GB. + + RootedObject buffer(cx, JS::NewSharedArrayBuffer(cx, nbytes)); + CHECK(buffer); + CHECK(JS::IsSharedArrayBufferObject(buffer)); + CHECK_EQUAL(GetSharedArrayBufferByteLength(buffer), nbytes); + + size_t length; + bool isShared; + uint8_t* data; + JS::GetSharedArrayBufferLengthAndData(buffer, &length, &isShared, &data); + CHECK_EQUAL(length, nbytes); +#endif + + return true; +} +END_TEST(testLargeSharedArrayBuffers) diff --git a/js/src/jsapi-tests/testLookup.cpp b/js/src/jsapi-tests/testLookup.cpp new file mode 100644 index 0000000000..24a1afa6fd --- /dev/null +++ b/js/src/jsapi-tests/testLookup.cpp @@ -0,0 +1,102 @@ +/* -*- 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 "js/PropertyAndElement.h" // JS_DefineProperty, JS_DefinePropertyById, JS_GetProperty +#include "jsapi-tests/tests.h" +#include "vm/JSFunction.h" // for js::IsInternalFunctionObject + +#include "vm/JSObject-inl.h" + +BEGIN_TEST(testLookup_bug522590) { + // Define a function that makes method-bearing objects. + JS::RootedValue x(cx); + EXEC("function mkobj() { return {f: function () {return 2;}} }"); + + // Calling mkobj() multiple times must create multiple functions in ES5. + EVAL("mkobj().f !== mkobj().f", &x); + CHECK(x.isTrue()); + + // Now make x.f a method. + EVAL("mkobj()", &x); + JS::RootedObject xobj(cx, x.toObjectOrNull()); + + // This lookup must not return an internal function object. + JS::RootedValue r(cx); + CHECK(JS_GetProperty(cx, xobj, "f", &r)); + CHECK(r.isObject()); + JSObject* funobj = &r.toObject(); + CHECK(funobj->is<JSFunction>()); + CHECK(!js::IsInternalFunctionObject(*funobj)); + + return true; +} +END_TEST(testLookup_bug522590) + +static const JSClass DocumentAllClass = {"DocumentAll", + JSCLASS_EMULATES_UNDEFINED}; + +bool document_resolve(JSContext* cx, JS::HandleObject obj, JS::HandleId id, + bool* resolvedp) { + // If id is "all", resolve document.all=true. + JS::RootedValue v(cx); + if (!JS_IdToValue(cx, id, &v)) { + return false; + } + + if (v.isString()) { + JSString* str = v.toString(); + JSLinearString* linearStr = JS_EnsureLinearString(cx, str); + if (!linearStr) { + return false; + } + if (JS_LinearStringEqualsLiteral(linearStr, "all")) { + JS::Rooted<JSObject*> docAll(cx, JS_NewObject(cx, &DocumentAllClass)); + if (!docAll) { + return false; + } + + JS::Rooted<JS::Value> allValue(cx, JS::ObjectValue(*docAll)); + if (!JS_DefinePropertyById(cx, obj, id, allValue, JSPROP_RESOLVING)) { + return false; + } + + *resolvedp = true; + return true; + } + } + + *resolvedp = false; + return true; +} + +static const JSClassOps document_classOps = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + document_resolve, // resolve + nullptr, // mayResolve + nullptr, // finalize + nullptr, // call + nullptr, // construct + nullptr, // trace +}; + +static const JSClass document_class = {"document", 0, &document_classOps}; + +BEGIN_TEST(testLookup_bug570195) { + JS::RootedObject obj(cx, JS_NewObject(cx, &document_class)); + CHECK(obj); + CHECK(JS_DefineProperty(cx, global, "document", obj, 0)); + JS::RootedValue v(cx); + EVAL("document.all ? true : false", &v); + CHECK(v.isFalse()); + EVAL("document.hasOwnProperty('all')", &v); + CHECK(v.isTrue()); + return true; +} +END_TEST(testLookup_bug570195) diff --git a/js/src/jsapi-tests/testLooselyEqual.cpp b/js/src/jsapi-tests/testLooselyEqual.cpp new file mode 100644 index 0000000000..8fa369f7e7 --- /dev/null +++ b/js/src/jsapi-tests/testLooselyEqual.cpp @@ -0,0 +1,186 @@ +/* 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 <limits> +#include <math.h> + +#include "js/Equality.h" // JS::LooselyEqual +#include "js/GlobalObject.h" +#include "jsapi-tests/tests.h" + +using namespace std; + +struct LooseEqualityFixture : public JSAPIRuntimeTest { + virtual ~LooseEqualityFixture() {} + + bool leq(JS::HandleValue x, JS::HandleValue y) { + bool equal; + CHECK(JS::LooselyEqual(cx, x, y, &equal) && equal); + CHECK(JS::LooselyEqual(cx, y, x, &equal) && equal); + return true; + } + + bool nleq(JS::HandleValue x, JS::HandleValue y) { + bool equal; + CHECK(JS::LooselyEqual(cx, x, y, &equal) && !equal); + CHECK(JS::LooselyEqual(cx, y, x, &equal) && !equal); + return true; + } +}; + +struct LooseEqualityData { + JS::RootedValue qNaN; + JS::RootedValue sNaN; + JS::RootedValue d42; + JS::RootedValue i42; + JS::RootedValue undef; + JS::RootedValue null; + JS::RootedValue obj; + JS::RootedValue poszero; + JS::RootedValue negzero; + + explicit LooseEqualityData(JSContext* cx) + : qNaN(cx), + sNaN(cx), + d42(cx), + i42(cx), + undef(cx), + null(cx), + obj(cx), + poszero(cx), + negzero(cx) { + qNaN = JS::CanonicalizedDoubleValue(numeric_limits<double>::quiet_NaN()); + sNaN = + JS::CanonicalizedDoubleValue(numeric_limits<double>::signaling_NaN()); + d42 = JS::DoubleValue(42.0); + i42 = JS::Int32Value(42); + undef = JS::UndefinedValue(); + null = JS::NullValue(); + obj = JS::ObjectOrNullValue(JS::CurrentGlobalOrNull(cx)); + poszero = JS::DoubleValue(0.0); + negzero = JS::DoubleValue(-0.0); +#ifdef XP_WIN +# define copysign _copysign +#endif + MOZ_RELEASE_ASSERT(copysign(1.0, poszero.toDouble()) == 1.0); + MOZ_RELEASE_ASSERT(copysign(1.0, negzero.toDouble()) == -1.0); +#ifdef XP_WIN +# undef copysign +#endif + } +}; + +// 11.9.3 1a +BEGIN_FIXTURE_TEST(LooseEqualityFixture, test_undef_leq_undef) { + LooseEqualityData d(cx); + CHECK(leq(d.undef, d.undef)); + return true; +} +END_FIXTURE_TEST(LooseEqualityFixture, test_undef_leq_undef) + +// 11.9.3 1b +BEGIN_FIXTURE_TEST(LooseEqualityFixture, test_null_leq_null) { + LooseEqualityData d(cx); + CHECK(leq(d.null, d.null)); + return true; +} +END_FIXTURE_TEST(LooseEqualityFixture, test_null_leq_null) + +// 11.9.3 1ci +BEGIN_FIXTURE_TEST(LooseEqualityFixture, test_nan_nleq_all) { + LooseEqualityData d(cx); + + CHECK(nleq(d.qNaN, d.qNaN)); + CHECK(nleq(d.qNaN, d.sNaN)); + + CHECK(nleq(d.sNaN, d.sNaN)); + CHECK(nleq(d.sNaN, d.qNaN)); + + CHECK(nleq(d.qNaN, d.d42)); + CHECK(nleq(d.qNaN, d.i42)); + CHECK(nleq(d.qNaN, d.undef)); + CHECK(nleq(d.qNaN, d.null)); + CHECK(nleq(d.qNaN, d.obj)); + + CHECK(nleq(d.sNaN, d.d42)); + CHECK(nleq(d.sNaN, d.i42)); + CHECK(nleq(d.sNaN, d.undef)); + CHECK(nleq(d.sNaN, d.null)); + CHECK(nleq(d.sNaN, d.obj)); + return true; +} +END_FIXTURE_TEST(LooseEqualityFixture, test_nan_nleq_all) + +// 11.9.3 1cii +BEGIN_FIXTURE_TEST(LooseEqualityFixture, test_all_nleq_nan) { + LooseEqualityData d(cx); + + CHECK(nleq(d.qNaN, d.qNaN)); + CHECK(nleq(d.qNaN, d.sNaN)); + + CHECK(nleq(d.sNaN, d.sNaN)); + CHECK(nleq(d.sNaN, d.qNaN)); + + CHECK(nleq(d.d42, d.qNaN)); + CHECK(nleq(d.i42, d.qNaN)); + CHECK(nleq(d.undef, d.qNaN)); + CHECK(nleq(d.null, d.qNaN)); + CHECK(nleq(d.obj, d.qNaN)); + + CHECK(nleq(d.d42, d.sNaN)); + CHECK(nleq(d.i42, d.sNaN)); + CHECK(nleq(d.undef, d.sNaN)); + CHECK(nleq(d.null, d.sNaN)); + CHECK(nleq(d.obj, d.sNaN)); + return true; +} +END_FIXTURE_TEST(LooseEqualityFixture, test_all_nleq_nan) + +// 11.9.3 1ciii +BEGIN_FIXTURE_TEST(LooseEqualityFixture, test_leq_same_nums) { + LooseEqualityData d(cx); + + CHECK(leq(d.d42, d.d42)); + CHECK(leq(d.i42, d.i42)); + CHECK(leq(d.d42, d.i42)); + CHECK(leq(d.i42, d.d42)); + return true; +} +END_FIXTURE_TEST(LooseEqualityFixture, test_leq_same_nums) + +// 11.9.3 1civ +BEGIN_FIXTURE_TEST(LooseEqualityFixture, test_pz_leq_nz) { + LooseEqualityData d(cx); + CHECK(leq(d.poszero, d.negzero)); + return true; +} +END_FIXTURE_TEST(LooseEqualityFixture, test_pz_leq_nz) + +// 11.9.3 1cv +BEGIN_FIXTURE_TEST(LooseEqualityFixture, test_nz_leq_pz) { + LooseEqualityData d(cx); + CHECK(leq(d.negzero, d.poszero)); + return true; +} +END_FIXTURE_TEST(LooseEqualityFixture, test_nz_leq_pz) + +// 1cvi onwards NOT TESTED + +// 11.9.3 2 +BEGIN_FIXTURE_TEST(LooseEqualityFixture, test_null_leq_undef) { + LooseEqualityData d(cx); + CHECK(leq(d.null, d.undef)); + return true; +} +END_FIXTURE_TEST(LooseEqualityFixture, test_null_leq_undef) + +// 11.9.3 3 +BEGIN_FIXTURE_TEST(LooseEqualityFixture, test_undef_leq_null) { + LooseEqualityData d(cx); + CHECK(leq(d.undef, d.null)); + return true; +} +END_FIXTURE_TEST(LooseEqualityFixture, test_undef_leq_null) + +// 4 onwards NOT TESTED diff --git a/js/src/jsapi-tests/testMappedArrayBuffer.cpp b/js/src/jsapi-tests/testMappedArrayBuffer.cpp new file mode 100644 index 0000000000..b07303f1bd --- /dev/null +++ b/js/src/jsapi-tests/testMappedArrayBuffer.cpp @@ -0,0 +1,195 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + */ + +#include <fcntl.h> +#include <stdio.h> + +#include "js/Array.h" // JS::NewArrayObject +#include "js/ArrayBuffer.h" // JS::{{Create,Release}MappedArrayBufferContents,DetachArrayBuffer,GetArrayBuffer{ByteLength,Data},Is{,Detached,Mapped}ArrayBufferObject,NewMappedArrayBufferWithContents,StealArrayBufferContents} +#include "js/StructuredClone.h" +#include "jsapi-tests/tests.h" +#include "vm/ArrayBufferObject.h" + +#ifdef XP_WIN +# include <io.h> +# define GET_OS_FD(a) int(_get_osfhandle(a)) +#else +# include <unistd.h> +# define GET_OS_FD(a) (a) +#endif + +const char test_data[] = + "1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; +const char test_filename[] = "temp-bug945152_MappedArrayBuffer"; + +BEGIN_TEST(testMappedArrayBuffer_bug945152) { + TempFile test_file; + FILE* test_stream = test_file.open(test_filename); + CHECK(fputs(test_data, test_stream) != EOF); + test_file.close(); + + // Offset 0. + CHECK(TestCreateObject(0, 12)); + + // Aligned offset. + CHECK(TestCreateObject(8, 12)); + + // Unaligned offset. + CHECK(CreateNewObject(11, 12) == nullptr); + + // Offset + length greater than file size. + CHECK(CreateNewObject(8, sizeof(test_data) - 7) == nullptr); + + // Release the mapped content. + CHECK(TestReleaseContents()); + + // Detach mapped array buffer. + CHECK(TestDetachObject()); + + // Clone mapped array buffer. + CHECK(TestCloneObject()); + + // Steal mapped array buffer contents. + CHECK(TestStealContents()); + + // Transfer mapped array buffer contents. + CHECK(TestTransferObject()); + + // GC so we can remove the file we created. + GC(cx); + + test_file.remove(); + + return true; +} + +JSObject* CreateNewObject(const int offset, const int length) { + int fd = open(test_filename, O_RDONLY); + void* ptr = + JS::CreateMappedArrayBufferContents(GET_OS_FD(fd), offset, length); + close(fd); + if (!ptr) { + return nullptr; + } + JSObject* obj = JS::NewMappedArrayBufferWithContents(cx, length, ptr); + if (!obj) { + JS::ReleaseMappedArrayBufferContents(ptr, length); + return nullptr; + } + return obj; +} + +bool VerifyObject(JS::HandleObject obj, uint32_t offset, uint32_t length, + const bool mapped) { + JS::AutoCheckCannotGC nogc; + + CHECK(obj); + CHECK(JS::IsArrayBufferObject(obj)); + CHECK_EQUAL(JS::GetArrayBufferByteLength(obj), length); + if (mapped) { + CHECK(JS::IsMappedArrayBufferObject(obj)); + } else { + CHECK(!JS::IsMappedArrayBufferObject(obj)); + } + bool sharedDummy; + const char* data = reinterpret_cast<const char*>( + JS::GetArrayBufferData(obj, &sharedDummy, nogc)); + CHECK(data); + CHECK(memcmp(data, test_data + offset, length) == 0); + + return true; +} + +bool TestCreateObject(uint32_t offset, uint32_t length) { + JS::RootedObject obj(cx, CreateNewObject(offset, length)); + CHECK(VerifyObject(obj, offset, length, true)); + + return true; +} + +bool TestReleaseContents() { + int fd = open(test_filename, O_RDONLY); + void* ptr = JS::CreateMappedArrayBufferContents(GET_OS_FD(fd), 0, 12); + close(fd); + if (!ptr) { + return false; + } + JS::ReleaseMappedArrayBufferContents(ptr, 12); + + return true; +} + +bool TestDetachObject() { + JS::RootedObject obj(cx, CreateNewObject(8, 12)); + CHECK(obj); + JS::DetachArrayBuffer(cx, obj); + CHECK(JS::IsDetachedArrayBufferObject(obj)); + + return true; +} + +bool TestCloneObject() { + JS::RootedObject obj1(cx, CreateNewObject(8, 12)); + CHECK(obj1); + JSAutoStructuredCloneBuffer cloned_buffer( + JS::StructuredCloneScope::SameProcess, nullptr, nullptr); + JS::RootedValue v1(cx, JS::ObjectValue(*obj1)); + CHECK(cloned_buffer.write(cx, v1, nullptr, nullptr)); + JS::RootedValue v2(cx); + CHECK(cloned_buffer.read(cx, &v2, JS::CloneDataPolicy(), nullptr, nullptr)); + JS::RootedObject obj2(cx, v2.toObjectOrNull()); + CHECK(VerifyObject(obj2, 8, 12, false)); + + return true; +} + +bool TestStealContents() { + JS::RootedObject obj(cx, CreateNewObject(8, 12)); + CHECK(obj); + void* contents = JS::StealArrayBufferContents(cx, obj); + CHECK(contents); + CHECK(memcmp(contents, test_data + 8, 12) == 0); + CHECK(JS::IsDetachedArrayBufferObject(obj)); + + return true; +} + +bool TestTransferObject() { + JS::RootedObject obj1(cx, CreateNewObject(8, 12)); + CHECK(obj1); + JS::RootedValue v1(cx, JS::ObjectValue(*obj1)); + + // Create an Array of transferable values. + JS::RootedValueVector argv(cx); + if (!argv.append(v1)) { + return false; + } + + JS::RootedObject obj( + cx, JS::NewArrayObject(cx, JS::HandleValueArray::subarray(argv, 0, 1))); + CHECK(obj); + JS::RootedValue transferable(cx, JS::ObjectValue(*obj)); + + JSAutoStructuredCloneBuffer cloned_buffer( + JS::StructuredCloneScope::SameProcess, nullptr, nullptr); + JS::CloneDataPolicy policy; + CHECK(cloned_buffer.write(cx, v1, transferable, policy, nullptr, nullptr)); + JS::RootedValue v2(cx); + CHECK(cloned_buffer.read(cx, &v2, policy, nullptr, nullptr)); + JS::RootedObject obj2(cx, v2.toObjectOrNull()); + CHECK(VerifyObject(obj2, 8, 12, true)); + CHECK(JS::IsDetachedArrayBufferObject(obj1)); + + return true; +} + +static void GC(JSContext* cx) { + JS_GC(cx); + // Trigger another to wait for background finalization to end. + JS_GC(cx); +} + +END_TEST(testMappedArrayBuffer_bug945152) + +#undef GET_OS_FD diff --git a/js/src/jsapi-tests/testMemoryAssociation.cpp b/js/src/jsapi-tests/testMemoryAssociation.cpp new file mode 100644 index 0000000000..49e910d63e --- /dev/null +++ b/js/src/jsapi-tests/testMemoryAssociation.cpp @@ -0,0 +1,44 @@ +/* -*- 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.h" +#include "jspubtd.h" + +#include "js/CompilationAndEvaluation.h" +#include "jsapi-tests/tests.h" + +static const JS::MemoryUse TestUse1 = JS::MemoryUse::XPCWrappedNative; +static const JS::MemoryUse TestUse2 = JS::MemoryUse::DOMBinding; + +BEGIN_TEST(testMemoryAssociation) { + JS::RootedObject obj(cx, JS_NewPlainObject(cx)); + CHECK(obj); + + // No association is allowed for nursery objects. + JS_GC(cx); + + // Test object/memory association. + JS::AddAssociatedMemory(obj, 100, TestUse1); + JS::RemoveAssociatedMemory(obj, 100, TestUse1); + + // Test association when object moves due to compacting GC. + void* initial = obj; + JS::AddAssociatedMemory(obj, 300, TestUse1); + JS::PrepareForFullGC(cx); + JS::NonIncrementalGC(cx, JS::GCOptions::Shrink, JS::GCReason::DEBUG_GC); + CHECK(obj != initial); + JS::RemoveAssociatedMemory(obj, 300, TestUse1); + + // Test multiple associations. + JS::AddAssociatedMemory(obj, 400, TestUse1); + JS::AddAssociatedMemory(obj, 500, TestUse2); + JS::RemoveAssociatedMemory(obj, 400, TestUse1); + JS::RemoveAssociatedMemory(obj, 500, TestUse2); + + return true; +} +END_TEST(testMemoryAssociation) diff --git a/js/src/jsapi-tests/testMutedErrors.cpp b/js/src/jsapi-tests/testMutedErrors.cpp new file mode 100644 index 0000000000..12410e0f99 --- /dev/null +++ b/js/src/jsapi-tests/testMutedErrors.cpp @@ -0,0 +1,99 @@ +/* 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 "jsfriendapi.h" +#include "js/CompilationAndEvaluation.h" +#include "js/Exception.h" +#include "js/GlobalObject.h" +#include "js/SourceText.h" +#include "jsapi-tests/tests.h" + +BEGIN_TEST(testMutedErrors) { + CHECK(testOuter("function f() {return 1}; f;")); + CHECK(testOuter( + "function outer() { return (function () {return 2}); }; outer();")); + CHECK(testOuter("eval('(function() {return 3})');")); + CHECK( + testOuter("(function (){ return eval('(function() {return 4})'); })()")); + CHECK( + testOuter("(function (){ return eval('(function() { return " + "eval(\"(function(){return 5})\") })()'); })()")); + CHECK(testOuter("new Function('return 6')")); + CHECK(testOuter("function f() { return new Function('return 7') }; f();")); + CHECK(testOuter("eval('new Function(\"return 8\")')")); + CHECK( + testOuter("(new Function('return eval(\"(function(){return 9})\")'))()")); + CHECK(testOuter("(function(){return function(){return 10}}).bind()()")); + CHECK(testOuter( + "var e = eval; (function() { return e('(function(){return 11})') })()")); + + CHECK(testError("eval(-)")); + CHECK(testError("-")); + CHECK(testError("new Function('x', '-')")); + CHECK(testError("eval('new Function(\"x\", \"-\")')")); + + /* + * NB: uncaught exceptions, when reported, have nothing on the stack so + * both the filename and mutedErrors are empty. E.g., this would fail: + * + * CHECK(testError("throw 3")); + */ + return true; +} + +bool eval(const char* asciiChars, bool mutedErrors, + JS::MutableHandleValue rval) { + size_t len = strlen(asciiChars); + mozilla::UniquePtr<char16_t[]> chars(new char16_t[len + 1]); + for (size_t i = 0; i < len; ++i) { + chars[i] = asciiChars[i]; + } + chars[len] = 0; + + JS::RealmOptions globalOptions; + JS::RootedObject global( + cx, JS_NewGlobalObject(cx, getGlobalClass(), nullptr, + JS::FireOnNewGlobalHook, globalOptions)); + CHECK(global); + JSAutoRealm ar(cx, global); + + JS::CompileOptions options(cx); + options.setMutedErrors(mutedErrors).setFileAndLine("", 0); + + JS::SourceText<char16_t> srcBuf; + CHECK(srcBuf.init(cx, chars.get(), len, JS::SourceOwnership::Borrowed)); + + return JS::Evaluate(cx, options, srcBuf, rval); +} + +bool testOuter(const char* asciiChars) { + CHECK(testInner(asciiChars, false)); + CHECK(testInner(asciiChars, true)); + return true; +} + +bool testInner(const char* asciiChars, bool mutedErrors) { + JS::RootedValue rval(cx); + CHECK(eval(asciiChars, mutedErrors, &rval)); + + JS::RootedFunction fun(cx, &rval.toObject().as<JSFunction>()); + JSScript* script = JS_GetFunctionScript(cx, fun); + CHECK(JS_ScriptHasMutedErrors(script) == mutedErrors); + + return true; +} + +bool testError(const char* asciiChars) { + JS::RootedValue rval(cx); + CHECK(!eval(asciiChars, true, &rval)); + + JS::ExceptionStack exnStack(cx); + CHECK(JS::StealPendingExceptionStack(cx, &exnStack)); + + JS::ErrorReportBuilder report(cx); + CHECK(report.init(cx, exnStack, JS::ErrorReportBuilder::WithSideEffects)); + CHECK(report.report()->isMuted == true); + return true; +} +END_TEST(testMutedErrors) diff --git a/js/src/jsapi-tests/testNewObject.cpp b/js/src/jsapi-tests/testNewObject.cpp new file mode 100644 index 0000000000..bdddede03b --- /dev/null +++ b/js/src/jsapi-tests/testNewObject.cpp @@ -0,0 +1,255 @@ +/* -*- 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 "js/Array.h" // JS::GetArrayLength, JS::IsArrayObject +#include "js/CallAndConstruct.h" // JS::Construct +#include "js/Object.h" // JS::GetClass +#include "js/PropertyAndElement.h" // JS_GetElement, JS_SetElement +#include "jsapi-tests/tests.h" +#include "vm/PlainObject.h" // js::PlainObject::class_ + +#include "vm/NativeObject-inl.h" + +using namespace js; + +static bool constructHook(JSContext* cx, unsigned argc, JS::Value* vp) { + JS::CallArgs args = CallArgsFromVp(argc, vp); + + // Check that arguments were passed properly from JS_New. + + JS::RootedObject obj(cx, JS_NewPlainObject(cx)); + if (!obj) { + JS_ReportErrorASCII(cx, "test failed, could not construct object"); + return false; + } + if (strcmp(JS::GetClass(obj)->name, "Object") != 0) { + JS_ReportErrorASCII(cx, "test failed, wrong class for 'this'"); + return false; + } + if (args.length() != 3) { + JS_ReportErrorASCII(cx, "test failed, argc == %d", args.length()); + return false; + } + if (!args[0].isInt32() || args[2].toInt32() != 2) { + JS_ReportErrorASCII(cx, "test failed, wrong value in args[2]"); + return false; + } + if (!args.isConstructing()) { + JS_ReportErrorASCII(cx, "test failed, not constructing"); + return false; + } + + // Perform a side-effect to indicate that this hook was actually called. + JS::RootedValue value(cx, args[0]); + JS::RootedObject callee(cx, &args.callee()); + if (!JS_SetElement(cx, callee, 0, value)) { + return false; + } + + args.rval().setObject(*obj); + + // trash the argv, perversely + args[0].setUndefined(); + args[1].setUndefined(); + args[2].setUndefined(); + + return true; +} + +BEGIN_TEST(testNewObject_1) { + static const size_t N = 1000; + JS::RootedValueVector argv(cx); + CHECK(argv.resize(N)); + + JS::RootedValue Array(cx); + EVAL("Array", &Array); + + bool isArray; + + // With no arguments. + JS::RootedObject obj(cx); + CHECK(JS::Construct(cx, Array, JS::HandleValueArray::empty(), &obj)); + CHECK(JS::IsArrayObject(cx, obj, &isArray)); + CHECK(isArray); + uint32_t len; + CHECK(JS::GetArrayLength(cx, obj, &len)); + CHECK_EQUAL(len, 0u); + + // With one argument. + argv[0].setInt32(4); + CHECK(JS::Construct(cx, Array, JS::HandleValueArray::subarray(argv, 0, 1), + &obj)); + CHECK(JS::IsArrayObject(cx, obj, &isArray)); + CHECK(isArray); + CHECK(JS::GetArrayLength(cx, obj, &len)); + CHECK_EQUAL(len, 4u); + + // With N arguments. + for (size_t i = 0; i < N; i++) { + argv[i].setInt32(i); + } + CHECK(JS::Construct(cx, Array, JS::HandleValueArray::subarray(argv, 0, N), + &obj)); + CHECK(JS::IsArrayObject(cx, obj, &isArray)); + CHECK(isArray); + CHECK(JS::GetArrayLength(cx, obj, &len)); + CHECK_EQUAL(len, N); + JS::RootedValue v(cx); + CHECK(JS_GetElement(cx, obj, N - 1, &v)); + CHECK(v.isInt32(N - 1)); + + // With JSClass.construct. + static const JSClassOps clsOps = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + nullptr, // finalize + nullptr, // call + constructHook, // construct + nullptr, // trace + }; + static const JSClass cls = {"testNewObject_1", 0, &clsOps}; + JS::RootedObject ctor(cx, JS_NewObject(cx, &cls)); + CHECK(ctor); + JS::RootedValue ctorVal(cx, JS::ObjectValue(*ctor)); + CHECK(JS::Construct(cx, ctorVal, JS::HandleValueArray::subarray(argv, 0, 3), + &obj)); + CHECK(JS_GetElement(cx, ctor, 0, &v)); + CHECK(v.isInt32(0)); + + return true; +} +END_TEST(testNewObject_1) + +BEGIN_TEST(testNewObject_IsMapObject) { + // Test IsMapObject and IsSetObject + + JS::RootedValue vMap(cx); + EVAL("Map", &vMap); + + bool isMap = false; + bool isSet = false; + JS::RootedObject mapObj(cx); + CHECK(JS::Construct(cx, vMap, JS::HandleValueArray::empty(), &mapObj)); + CHECK(JS::IsMapObject(cx, mapObj, &isMap)); + CHECK(isMap); + CHECK(JS::IsSetObject(cx, mapObj, &isSet)); + CHECK(!isSet); + + JS::RootedValue vSet(cx); + EVAL("Set", &vSet); + + JS::RootedObject setObj(cx); + CHECK(JS::Construct(cx, vSet, JS::HandleValueArray::empty(), &setObj)); + CHECK(JS::IsMapObject(cx, setObj, &isMap)); + CHECK(!isMap); + CHECK(JS::IsSetObject(cx, setObj, &isSet)); + CHECK(isSet); + + return true; +} +END_TEST(testNewObject_IsMapObject) + +static const JSClass Base_class = { + "Base", + JSCLASS_HAS_RESERVED_SLOTS(8), // flags +}; + +BEGIN_TEST(testNewObject_Subclassing) { + JSObject* proto = + JS_InitClass(cx, global, nullptr, nullptr, "Base", Base_constructor, 0, + nullptr, nullptr, nullptr, nullptr); + if (!proto) { + return false; + } + + CHECK_EQUAL(JS::GetClass(proto), &PlainObject::class_); + + // Calling Base without `new` should fail with a TypeError. + JS::RootedValue expectedError(cx); + EVAL("TypeError", &expectedError); + JS::RootedValue actualError(cx); + EVAL( + "try {\n" + " Base();\n" + "} catch (e) {\n" + " e.constructor;\n" + "}\n", + &actualError); + CHECK_SAME(actualError, expectedError); + + // Check prototype chains when a JS class extends a base class that's + // implemented in C++ using JS_NewObjectForConstructor. + EXEC( + "class MyClass extends Base {\n" + " ok() { return true; }\n" + "}\n" + "let myObj = new MyClass();\n"); + + JS::RootedValue result(cx); + EVAL("myObj.ok()", &result); + CHECK_SAME(result, JS::TrueValue()); + + EVAL("myObj.__proto__ === MyClass.prototype", &result); + CHECK_SAME(result, JS::TrueValue()); + EVAL("myObj.__proto__.__proto__ === Base.prototype", &result); + CHECK_SAME(result, JS::TrueValue()); + + EVAL("myObj", &result); + CHECK_EQUAL(JS::GetClass(&result.toObject()), &Base_class); + + // All reserved slots are initialized to undefined. + for (uint32_t i = 0; i < JSCLASS_RESERVED_SLOTS(&Base_class); i++) { + CHECK_SAME(JS::GetReservedSlot(&result.toObject(), i), + JS::UndefinedValue()); + } + + return true; +} + +static bool Base_constructor(JSContext* cx, unsigned argc, JS::Value* vp) { + JS::CallArgs args = CallArgsFromVp(argc, vp); + JS::RootedObject obj(cx, JS_NewObjectForConstructor(cx, &Base_class, args)); + if (!obj) { + return false; + } + args.rval().setObject(*obj); + return true; +} + +END_TEST(testNewObject_Subclassing) + +static const JSClass TestClass = {"TestObject", JSCLASS_HAS_RESERVED_SLOTS(0)}; + +BEGIN_TEST(testNewObject_elements) { + Rooted<NativeObject*> obj( + cx, NewBuiltinClassInstance(cx, &TestClass, GenericObject)); + CHECK(obj); + CHECK(!obj->isTenured()); + CHECK(obj->hasEmptyElements()); + CHECK(!obj->hasFixedElements()); + CHECK(!obj->hasDynamicElements()); + + CHECK(obj->ensureElements(cx, 1)); + CHECK(!obj->hasEmptyElements()); + CHECK(!obj->hasFixedElements()); + CHECK(obj->hasDynamicElements()); + + RootedObject array(cx, NewArrayObject(cx, 1)); + CHECK(array); + obj = &array->as<NativeObject>(); + CHECK(!obj->isTenured()); + CHECK(!obj->hasEmptyElements()); + CHECK(obj->hasFixedElements()); + CHECK(!obj->hasDynamicElements()); + + return true; +} +END_TEST(testNewObject_elements) diff --git a/js/src/jsapi-tests/testNewTargetInvokeConstructor.cpp b/js/src/jsapi-tests/testNewTargetInvokeConstructor.cpp new file mode 100644 index 0000000000..6ecc1db721 --- /dev/null +++ b/js/src/jsapi-tests/testNewTargetInvokeConstructor.cpp @@ -0,0 +1,25 @@ +/* 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 "js/CallAndConstruct.h" // JS::Construct +#include "jsapi-tests/tests.h" + +BEGIN_TEST(testNewTargetInvokeConstructor) { + JS::RootedValue func(cx); + + EVAL( + "(function(expected) { if (expected !== new.target) throw new " + "Error('whoops'); })", + &func); + + JS::RootedValueArray<1> args(cx); + args[0].set(func); + + JS::RootedObject obj(cx); + + CHECK(JS::Construct(cx, func, args, &obj)); + + return true; +} +END_TEST(testNewTargetInvokeConstructor) diff --git a/js/src/jsapi-tests/testNullRoot.cpp b/js/src/jsapi-tests/testNullRoot.cpp new file mode 100644 index 0000000000..4819116c2a --- /dev/null +++ b/js/src/jsapi-tests/testNullRoot.cpp @@ -0,0 +1,24 @@ +/* -*- 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" + +BEGIN_TEST(testNullRoot) { + obj.init(cx, nullptr); + str.init(cx, nullptr); + script.init(cx, nullptr); + + // This used to crash because obj was nullptr. + JS_GC(cx); + + return true; +} + +JS::PersistentRootedObject obj; +JS::PersistentRootedString str; +JS::PersistentRootedScript script; +END_TEST(testNullRoot) diff --git a/js/src/jsapi-tests/testNumberToString.cpp b/js/src/jsapi-tests/testNumberToString.cpp new file mode 100644 index 0000000000..d5107327c6 --- /dev/null +++ b/js/src/jsapi-tests/testNumberToString.cpp @@ -0,0 +1,133 @@ +/* -*- 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 "mozilla/FloatingPoint.h" // mozilla::{PositiveInfinity,UnspecifiedNaN} + +#include <stddef.h> // size_t +#include <string.h> // memcmp, memset + +#include "js/Conversions.h" // JS::NumberToString, JS::MaximumNumberToStringLength +#include "jsapi-tests/tests.h" // BEGIN_TEST, CHECK_EQUAL, END_TEST +#include "util/Text.h" // js_strlen + +#define REST(x) x, (js_strlen(x)), __LINE__ + +static const struct NumberToStringTest { + double number; + const char* expected; + size_t expectedLength; + size_t lineno; +} numberToStringTests[] = { + {5e-324, REST("5e-324")}, // 2**-1074 + {9.5367431640625e-7, REST("9.5367431640625e-7")}, // 2**-20 + {0.0000010984284297360395, REST("0.0000010984284297360395")}, + {0.0000019073486328125, REST("0.0000019073486328125")}, // 2**-19 + {0.000003814697265625, REST("0.000003814697265625")}, // 2**-18 + {0.0000057220458984375, REST("0.0000057220458984375")}, // 2**-18 + 2**-19 + {0.000244140625, REST("0.000244140625")}, // 2**-12 + {0.125, REST("0.125")}, + {0.25, REST("0.25")}, + {0.5, REST("0.5")}, + {1, REST("1")}, + {1.5, REST("1.5")}, + {2, REST("2")}, + {9, REST("9")}, + {10, REST("10")}, + {15, REST("15")}, + {16, REST("16")}, + {389427, REST("389427")}, + {1073741823, REST("1073741823")}, + {1073741824, REST("1073741824")}, + {1073741825, REST("1073741825")}, + {2147483647, REST("2147483647")}, + {2147483648, REST("2147483648")}, + {2147483649, REST("2147483649")}, + {4294967294, REST("4294967294")}, + {4294967295, REST("4294967295")}, + {4294967296, REST("4294967296")}, + {999999999999999900000.0, REST("999999999999999900000")}, + {999999999999999900000.0 + 65535, REST("999999999999999900000")}, + {999999999999999900000.0 + 65536, REST("1e+21")}, + {1.7976931348623157e+308, REST("1.7976931348623157e+308")}, // MAX_VALUE +}; + +static constexpr char PoisonChar = 0x37; + +struct StorageForNumberToString { + char out[JS::MaximumNumberToStringLength]; + char overflow; +} storage; + +BEGIN_TEST(testNumberToString) { + StorageForNumberToString storage; + + if (!testNormalValues(false, storage)) { + return false; + } + + if (!testNormalValues(true, storage)) { + return false; + } + + NumberToStringTest zeroTest = {0.0, REST("0")}; + if (!testOne(zeroTest, false, storage)) { + return false; + } + NumberToStringTest negativeZeroTest = {-0.0, REST("0")}; + if (!testOne(negativeZeroTest, false, storage)) { + return false; + } + + NumberToStringTest infTest = {mozilla::PositiveInfinity<double>(), + REST("Infinity")}; + if (!testOne(infTest, false, storage)) { + return false; + } + if (!testOne(infTest, true, storage)) { + return false; + } + + NumberToStringTest nanTest = {mozilla::UnspecifiedNaN<double>(), REST("NaN")}; + return testOne(nanTest, false, storage); +} + +bool testNormalValues(bool hasMinusSign, StorageForNumberToString& storage) { + for (const auto& test : numberToStringTests) { + if (!testOne(test, hasMinusSign, storage)) { + return false; + } + } + + return true; +} + +bool testOne(const NumberToStringTest& test, bool hasMinusSign, + StorageForNumberToString& storage) { + memset(&storage, PoisonChar, sizeof(storage)); + + JS::NumberToString(hasMinusSign ? -test.number : test.number, storage.out); + + CHECK_EQUAL(storage.overflow, PoisonChar); + + const char* start = storage.out; + if (hasMinusSign) { + CHECK_EQUAL(start[0], '-'); + start++; + } + + if (!checkEqual(memcmp(start, test.expected, test.expectedLength), 0, start, + test.expected, __FILE__, test.lineno)) { + return false; + } + + char actualTerminator[] = {start[test.expectedLength], '\0'}; + return checkEqual(actualTerminator[0], '\0', actualTerminator, "'\\0'", + __FILE__, test.lineno); +} +END_TEST(testNumberToString) + +#undef REST diff --git a/js/src/jsapi-tests/testOOM.cpp b/js/src/jsapi-tests/testOOM.cpp new file mode 100644 index 0000000000..4fe895e9e7 --- /dev/null +++ b/js/src/jsapi-tests/testOOM.cpp @@ -0,0 +1,117 @@ +/* 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 "mozilla/DebugOnly.h" + +#include "jsapi-tests/tests.h" + +#include "vm/HelperThreads.h" + +BEGIN_TEST(testOOM) { + JS::RootedValue v(cx, JS::Int32Value(9)); + JS::RootedString jsstr(cx, JS::ToString(cx, v)); + char16_t ch; + if (!JS_GetStringCharAt(cx, jsstr, 0, &ch)) { + return false; + } + MOZ_RELEASE_ASSERT(ch == '9'); + return true; +} + +virtual JSContext* createContext() override { + JSContext* cx = JS_NewContext(0); + if (!cx) { + return nullptr; + } + JS_SetGCParameter(cx, JSGC_MAX_BYTES, (uint32_t)-1); + return cx; +} +END_TEST(testOOM) + +#ifdef DEBUG // js::oom functions are only available in debug builds. + +const uint32_t maxAllocsPerTest = 100; + +# define START_OOM_TEST(name) \ + testName = name; \ + for (bool always : {false, true}) { \ + const char* subTest = always ? "fail always" : "fail once"; \ + printf("Test %s (%s): started: ", testName, subTest); \ + for (oomAfter = 1; oomAfter < maxAllocsPerTest; ++oomAfter) { \ + js::oom::simulator.simulateFailureAfter( \ + js::oom::FailureSimulator::Kind::OOM, oomAfter, \ + js::THREAD_TYPE_MAIN, always) + +# define END_OOM_TEST \ + if (!js::oom::HadSimulatedOOM()) { \ + printf("\nTest %s (%s): finished with %" PRIu64 " allocations\n", \ + testName, subTest, oomAfter - 1); \ + break; \ + } \ + } \ + } \ + js::oom::simulator.reset(); \ + CHECK(oomAfter != maxAllocsPerTest) + +# define MARK_STAR printf("*"); +# define MARK_PLUS printf("+"); +# define MARK_DOT printf("."); + +BEGIN_TEST(testNewContextOOM) { + uninit(); // Get rid of test harness' original JSContext. + + const char* testName; + uint64_t oomAfter; + JSContext* cx; + START_OOM_TEST("new context"); + cx = JS_NewContext(8L * 1024 * 1024); + if (cx) { + MARK_PLUS; + JS_DestroyContext(cx); + } else { + MARK_DOT; + CHECK(!JSRuntime::hasLiveRuntimes()); + } + END_OOM_TEST; + return true; +} +END_TEST(testNewContextOOM) + +BEGIN_TEST(testHelperThreadOOM) { + const char* testName; + uint64_t oomAfter; + START_OOM_TEST("helper thread state"); + + if (js::CreateHelperThreadsState()) { + if (js::EnsureHelperThreadsInitialized()) { + MARK_STAR; + } else { + MARK_PLUS; + } + } else { + MARK_DOT; + } + + // Reset the helper threads to ensure they get re-initalised in following + // iterations + js::DestroyHelperThreadsState(); + + END_OOM_TEST; + + return true; +} + +bool init() override { + JSAPIRuntimeTest::uninit(); // Discard the just-created JSContext. + js::DestroyHelperThreadsState(); // The test creates this state. + return true; +} +void uninit() override { + // Leave things initialized after this test finishes. + js::CreateHelperThreadsState(); +} + +END_TEST(testHelperThreadOOM) + +#endif diff --git a/js/src/jsapi-tests/testObjectEmulatingUndefined.cpp b/js/src/jsapi-tests/testObjectEmulatingUndefined.cpp new file mode 100644 index 0000000000..732bde7147 --- /dev/null +++ b/js/src/jsapi-tests/testObjectEmulatingUndefined.cpp @@ -0,0 +1,109 @@ +/* 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" + +static const JSClass ObjectEmulatingUndefinedClass = { + "ObjectEmulatingUndefined", JSCLASS_EMULATES_UNDEFINED}; + +static bool ObjectEmulatingUndefinedConstructor(JSContext* cx, unsigned argc, + JS::Value* vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + JSObject* obj = + JS_NewObjectForConstructor(cx, &ObjectEmulatingUndefinedClass, args); + if (!obj) { + return false; + } + args.rval().setObject(*obj); + return true; +} + +BEGIN_TEST(testObjectEmulatingUndefined_truthy) { + CHECK(JS_InitClass(cx, global, nullptr, nullptr, "ObjectEmulatingUndefined", + ObjectEmulatingUndefinedConstructor, 0, nullptr, nullptr, + nullptr, nullptr)); + + JS::RootedValue result(cx); + + EVAL("if (new ObjectEmulatingUndefined()) true; else false;", &result); + CHECK(result.isFalse()); + + EVAL("if (!new ObjectEmulatingUndefined()) true; else false;", &result); + CHECK(result.isTrue()); + + EVAL( + "var obj = new ObjectEmulatingUndefined(); \n" + "var res = []; \n" + "for (var i = 0; i < 50; i++) \n" + " res.push(Boolean(obj)); \n" + "res.every(function(v) { return v === false; });", + &result); + CHECK(result.isTrue()); + + return true; +} +END_TEST(testObjectEmulatingUndefined_truthy) + +BEGIN_TEST(testObjectEmulatingUndefined_equal) { + CHECK(JS_InitClass(cx, global, nullptr, nullptr, "ObjectEmulatingUndefined", + ObjectEmulatingUndefinedConstructor, 0, nullptr, nullptr, + nullptr, nullptr)); + + JS::RootedValue result(cx); + + EVAL("if (new ObjectEmulatingUndefined() == undefined) true; else false;", + &result); + CHECK(result.isTrue()); + + EVAL("if (new ObjectEmulatingUndefined() == null) true; else false;", + &result); + CHECK(result.isTrue()); + + EVAL("if (new ObjectEmulatingUndefined() != undefined) true; else false;", + &result); + CHECK(result.isFalse()); + + EVAL("if (new ObjectEmulatingUndefined() != null) true; else false;", + &result); + CHECK(result.isFalse()); + + EVAL( + "var obj = new ObjectEmulatingUndefined(); \n" + "var res = []; \n" + "for (var i = 0; i < 50; i++) \n" + " res.push(obj == undefined); \n" + "res.every(function(v) { return v === true; });", + &result); + CHECK(result.isTrue()); + + EVAL( + "var obj = new ObjectEmulatingUndefined(); \n" + "var res = []; \n" + "for (var i = 0; i < 50; i++) \n" + " res.push(obj == null); \n" + "res.every(function(v) { return v === true; });", + &result); + CHECK(result.isTrue()); + + EVAL( + "var obj = new ObjectEmulatingUndefined(); \n" + "var res = []; \n" + "for (var i = 0; i < 50; i++) \n" + " res.push(obj != undefined); \n" + "res.every(function(v) { return v === false; });", + &result); + CHECK(result.isTrue()); + + EVAL( + "var obj = new ObjectEmulatingUndefined(); \n" + "var res = []; \n" + "for (var i = 0; i < 50; i++) \n" + " res.push(obj != null); \n" + "res.every(function(v) { return v === false; });", + &result); + CHECK(result.isTrue()); + + return true; +} +END_TEST(testObjectEmulatingUndefined_equal) diff --git a/js/src/jsapi-tests/testObjectSwap.cpp b/js/src/jsapi-tests/testObjectSwap.cpp new file mode 100644 index 0000000000..d326df6f37 --- /dev/null +++ b/js/src/jsapi-tests/testObjectSwap.cpp @@ -0,0 +1,445 @@ +/* -*- 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/. */ + +/* + * Test JSObject::swap. + * + * This test creates objects from a description of their configuration. Objects + * are given unique property names and values. A list of configurations is + * created and the result of swapping every combination checked. + */ + +#include "mozilla/Sprintf.h" + +#include "js/AllocPolicy.h" +#include "js/Vector.h" +#include "jsapi-tests/tests.h" +#include "vm/PlainObject.h" + +#include "gc/StableCellHasher-inl.h" +#include "vm/JSObject-inl.h" + +using namespace js; + +struct NativeConfig { + uint32_t propCount; + uint32_t elementCount; + bool inDictionaryMode; +}; + +struct ProxyConfig { + bool inlineValues; +}; + +struct ObjectConfig { + const JSClass* clasp; + bool isNative; + bool nurseryAllocated; + bool hasUniqueId; + union { + NativeConfig native; + ProxyConfig proxy; + }; +}; + +using ObjectConfigVector = Vector<ObjectConfig, 0, SystemAllocPolicy>; + +static const JSClass TestProxyClasses[] = { + PROXY_CLASS_DEF("TestProxy", JSCLASS_HAS_RESERVED_SLOTS(1 /* Min */)), + PROXY_CLASS_DEF("TestProxy", JSCLASS_HAS_RESERVED_SLOTS(2)), + PROXY_CLASS_DEF("TestProxy", JSCLASS_HAS_RESERVED_SLOTS(7)), + PROXY_CLASS_DEF("TestProxy", JSCLASS_HAS_RESERVED_SLOTS(8)), + PROXY_CLASS_DEF("TestProxy", JSCLASS_HAS_RESERVED_SLOTS(14 /* Max */))}; + +static const JSClass TestDOMClasses[] = { + {"TestDOMObject", JSCLASS_IS_DOMJSCLASS | JSCLASS_HAS_RESERVED_SLOTS(0)}, + {"TestDOMObject", JSCLASS_IS_DOMJSCLASS | JSCLASS_HAS_RESERVED_SLOTS(1)}, + {"TestDOMObject", JSCLASS_IS_DOMJSCLASS | JSCLASS_HAS_RESERVED_SLOTS(2)}, + {"TestDOMObject", JSCLASS_IS_DOMJSCLASS | JSCLASS_HAS_RESERVED_SLOTS(7)}, + {"TestDOMObject", JSCLASS_IS_DOMJSCLASS | JSCLASS_HAS_RESERVED_SLOTS(8)}, + {"TestDOMObject", JSCLASS_IS_DOMJSCLASS | JSCLASS_HAS_RESERVED_SLOTS(20)}}; + +static const uint32_t TestPropertyCounts[] = {0, 1, 2, 7, 8, 20}; + +static const uint32_t TestElementCounts[] = {0, 20}; + +static bool Verbose = false; + +class TenuredProxyHandler final : public Wrapper { + public: + static const TenuredProxyHandler singleton; + constexpr TenuredProxyHandler() : Wrapper(0) {} + bool canNurseryAllocate() const override { return false; } +}; + +const TenuredProxyHandler TenuredProxyHandler::singleton; + +class NurseryProxyHandler final : public Wrapper { + public: + static const NurseryProxyHandler singleton; + constexpr NurseryProxyHandler() : Wrapper(0) {} + bool canNurseryAllocate() const override { return true; } +}; + +const NurseryProxyHandler NurseryProxyHandler::singleton; + +BEGIN_TEST(testObjectSwap) { + AutoLeaveZeal noZeal(cx); + + ObjectConfigVector objectConfigs = CreateObjectConfigs(); + + for (const ObjectConfig& config1 : objectConfigs) { + for (const ObjectConfig& config2 : objectConfigs) { + { + uint32_t id1; + RootedObject obj1(cx, CreateObject(config1, &id1)); + CHECK(obj1); + + uint32_t id2; + RootedObject obj2(cx, CreateObject(config2, &id2)); + CHECK(obj2); + + if (Verbose) { + fprintf(stderr, "Swap %p (%s) and %p (%s)\n", obj1.get(), + GetLocation(obj1), obj2.get(), GetLocation(obj2)); + } + + uint64_t uid1 = 0; + if (config1.hasUniqueId) { + uid1 = gc::GetUniqueIdInfallible(obj1); + } + uint64_t uid2 = 0; + if (config2.hasUniqueId) { + uid2 = gc::GetUniqueIdInfallible(obj2); + } + + { + AutoEnterOOMUnsafeRegion oomUnsafe; + JSObject::swap(cx, obj1, obj2, oomUnsafe); + } + + CHECK(CheckObject(obj1, config2, id2)); + CHECK(CheckObject(obj2, config1, id1)); + + CHECK(CheckUniqueIds(obj1, config1.hasUniqueId, uid1, obj2, + config2.hasUniqueId, uid2)); + } + + if (Verbose) { + fprintf(stderr, "\n"); + } + } + + // JSObject::swap can suppress GC so ensure we clean up occasionally. + JS_GC(cx); + } + + return true; +} + +ObjectConfigVector CreateObjectConfigs() { + ObjectConfigVector configs; + + ObjectConfig config; + + for (bool nurseryAllocated : {false, true}) { + config.nurseryAllocated = nurseryAllocated; + + for (bool hasUniqueId : {false, true}) { + config.hasUniqueId = hasUniqueId; + + config.isNative = true; + config.native = NativeConfig{0, false}; + + for (const JSClass& jsClass : TestDOMClasses) { + config.clasp = &jsClass; + + for (uint32_t propCount : TestPropertyCounts) { + config.native.propCount = propCount; + + for (uint32_t elementCount : TestElementCounts) { + config.native.elementCount = elementCount; + + for (bool inDictionaryMode : {false, true}) { + if (inDictionaryMode && propCount == 0) { + continue; + } + + config.native.inDictionaryMode = inDictionaryMode; + MOZ_RELEASE_ASSERT(configs.append(config)); + } + } + } + } + + config.isNative = false; + config.proxy = ProxyConfig{false}; + + for (const JSClass& jsClass : TestProxyClasses) { + config.clasp = &jsClass; + + for (bool inlineValues : {true, false}) { + config.proxy.inlineValues = inlineValues; + MOZ_RELEASE_ASSERT(configs.append(config)); + } + } + } + } + + return configs; +} + +const char* GetLocation(JSObject* obj) { + return obj->isTenured() ? "tenured heap" : "nursery"; +} + +// Counter used to give slots and property names unique values. +uint32_t nextId = 0; + +JSObject* CreateObject(const ObjectConfig& config, uint32_t* idOut) { + *idOut = nextId; + JSObject* obj = + config.isNative ? CreateNativeObject(config) : CreateProxy(config); + + if (config.hasUniqueId) { + uint64_t unused; + if (!gc::GetOrCreateUniqueId(obj, &unused)) { + return nullptr; + } + } + + return obj; +} + +JSObject* CreateNativeObject(const ObjectConfig& config) { + MOZ_ASSERT(config.isNative); + + NewObjectKind kind = config.nurseryAllocated ? GenericObject : TenuredObject; + Rooted<NativeObject*> obj(cx, + NewBuiltinClassInstance(cx, config.clasp, kind)); + if (!obj) { + return nullptr; + } + + MOZ_RELEASE_ASSERT(IsInsideNursery(obj) == config.nurseryAllocated); + + for (uint32_t i = 0; i < JSCLASS_RESERVED_SLOTS(config.clasp); i++) { + JS::SetReservedSlot(obj, i, Int32Value(nextId++)); + } + + if (config.native.inDictionaryMode) { + // Put object in dictionary mode by defining a non-last property and + // deleting it later. + MOZ_RELEASE_ASSERT(config.native.propCount != 0); + if (!JS_DefineProperty(cx, obj, "dummy", 0, JSPROP_ENUMERATE)) { + return nullptr; + } + } + + for (uint32_t i = 0; i < config.native.propCount; i++) { + RootedId name(cx, CreatePropName(nextId++)); + if (name.isVoid()) { + return nullptr; + } + + if (!JS_DefinePropertyById(cx, obj, name, nextId++, JSPROP_ENUMERATE)) { + return nullptr; + } + } + + if (config.native.elementCount) { + if (!obj->ensureElements(cx, config.native.elementCount)) { + return nullptr; + } + for (uint32_t i = 0; i < config.native.elementCount; i++) { + obj->setDenseInitializedLength(i + 1); + obj->initDenseElement(i, Int32Value(nextId++)); + } + MOZ_ASSERT(obj->hasDynamicElements()); + } + + if (config.native.inDictionaryMode) { + JS::ObjectOpResult result; + JS_DeleteProperty(cx, obj, "dummy", result); + MOZ_RELEASE_ASSERT(result.ok()); + } + + MOZ_RELEASE_ASSERT(obj->inDictionaryMode() == config.native.inDictionaryMode); + + return obj; +} + +JSObject* CreateProxy(const ObjectConfig& config) { + RootedValue priv(cx, Int32Value(nextId++)); + + RootedObject expando(cx, NewPlainObject(cx)); + RootedValue expandoId(cx, Int32Value(nextId++)); + if (!expando || !JS_SetProperty(cx, expando, "id", expandoId)) { + return nullptr; + } + + ProxyOptions options; + options.setClass(config.clasp); + options.setLazyProto(true); + + const Wrapper* handler; + if (config.nurseryAllocated) { + handler = &NurseryProxyHandler::singleton; + } else { + handler = &TenuredProxyHandler::singleton; + } + + RootedObject obj(cx, NewProxyObject(cx, handler, priv, nullptr, options)); + if (!obj) { + return nullptr; + } + + Rooted<ProxyObject*> proxy(cx, &obj->as<ProxyObject>()); + proxy->setExpando(expando); + + for (uint32_t i = 0; i < JSCLASS_RESERVED_SLOTS(config.clasp); i++) { + JS::SetReservedSlot(proxy, i, Int32Value(nextId++)); + } + + if (!config.proxy.inlineValues) { + // To create a proxy with non-inline values we must swap the proxy with an + // object with a different size. + NewObjectKind kind = + config.nurseryAllocated ? GenericObject : TenuredObject; + RootedObject dummy(cx, + NewBuiltinClassInstance(cx, &TestDOMClasses[0], kind)); + if (!dummy) { + return nullptr; + } + + AutoEnterOOMUnsafeRegion oomUnsafe; + JSObject::swap(cx, obj, dummy, oomUnsafe); + proxy = &dummy->as<ProxyObject>(); + } + + MOZ_RELEASE_ASSERT(IsInsideNursery(proxy) == config.nurseryAllocated); + MOZ_RELEASE_ASSERT(proxy->usingInlineValueArray() == + config.proxy.inlineValues); + + return proxy; +} + +bool CheckObject(HandleObject obj, const ObjectConfig& config, uint32_t id) { + CHECK(obj->is<NativeObject>() == config.isNative); + CHECK(obj->getClass() == config.clasp); + + uint32_t reservedSlots = JSCLASS_RESERVED_SLOTS(config.clasp); + + if (Verbose) { + fprintf(stderr, "Check %p is a %s object with %u reserved slots", obj.get(), + config.isNative ? "native" : "proxy", reservedSlots); + if (config.isNative) { + fprintf(stderr, + ", %u properties, %u elements and %s in dictionary mode\n", + config.native.propCount, config.native.elementCount, + config.native.inDictionaryMode ? "is" : "is not"); + } else { + fprintf(stderr, " with %s values\n", + config.proxy.inlineValues ? "inline" : "out-of-line"); + } + } + + if (!config.isNative) { + CHECK(GetProxyPrivate(obj) == Int32Value(id++)); + + Value expandoValue = GetProxyExpando(obj); + CHECK(expandoValue.isObject()); + + RootedObject expando(cx, &expandoValue.toObject()); + RootedValue expandoId(cx); + JS_GetProperty(cx, expando, "id", &expandoId); + CHECK(expandoId == Int32Value(id++)); + } + + for (uint32_t i = 0; i < reservedSlots; i++) { + CHECK(JS::GetReservedSlot(obj, i) == Int32Value(id++)); + } + + if (config.isNative) { + Rooted<NativeObject*> nobj(cx, &obj->as<NativeObject>()); + uint32_t propCount = GetPropertyCount(nobj); + + CHECK(propCount == config.native.propCount); + + for (uint32_t i = 0; i < config.native.propCount; i++) { + RootedId name(cx, CreatePropName(id++)); + CHECK(!name.isVoid()); + + RootedValue value(cx); + CHECK(JS_GetPropertyById(cx, obj, name, &value)); + CHECK(value == Int32Value(id++)); + } + + CHECK(nobj->getDenseInitializedLength() == config.native.elementCount); + for (uint32_t i = 0; i < config.native.elementCount; i++) { + Value value = nobj->getDenseElement(i); + CHECK(value == Int32Value(id++)); + } + } + + return true; +} + +bool CheckUniqueIds(HandleObject obj1, bool hasUniqueId1, uint64_t uid1, + HandleObject obj2, bool hasUniqueId2, uint64_t uid2) { + if (uid1 && uid2) { + MOZ_RELEASE_ASSERT(uid1 != uid2); + } + + // Check unique IDs are NOT swapped. + CHECK(CheckUniqueId(obj1, hasUniqueId1, uid1)); + CHECK(CheckUniqueId(obj2, hasUniqueId2, uid2)); + + // Check unique IDs are different if present. + if (gc::HasUniqueId(obj1) && gc::HasUniqueId(obj2)) { + CHECK(gc::GetUniqueIdInfallible(obj1) != gc::GetUniqueIdInfallible(obj2)); + } + + return true; +} + +bool CheckUniqueId(HandleObject obj, bool hasUniqueId, uint64_t uid) { + if (hasUniqueId) { + CHECK(gc::HasUniqueId(obj)); + CHECK(gc::GetUniqueIdInfallible(obj) == uid); + } else { + // Swap may add a unique ID to an object. + } + + if (obj->is<NativeObject>()) { + CHECK(!obj->zone()->uniqueIds().has(obj)); + } + return true; +} + +uint32_t GetPropertyCount(NativeObject* obj) { + uint32_t count = 0; + for (ShapePropertyIter<NoGC> iter(obj->shape()); !iter.done(); iter++) { + count++; + } + + return count; +} + +jsid CreatePropName(uint32_t id) { + char buffer[32]; + SprintfLiteral(buffer, "prop%u", id); + + RootedString atom(cx, JS_AtomizeString(cx, buffer)); + if (!atom) { + return jsid::Void(); + } + + return jsid::NonIntAtom(atom); +} + +END_TEST(testObjectSwap) diff --git a/js/src/jsapi-tests/testParseJSON.cpp b/js/src/jsapi-tests/testParseJSON.cpp new file mode 100644 index 0000000000..b4dd74d707 --- /dev/null +++ b/js/src/jsapi-tests/testParseJSON.cpp @@ -0,0 +1,357 @@ +/* -*- 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 <limits> +#include <string.h> + +#include "js/Array.h" // JS::IsArrayObject +#include "js/Exception.h" +#include "js/friend/ErrorMessages.h" // JSMSG_* +#include "js/JSON.h" +#include "js/MemoryFunctions.h" +#include "js/Printf.h" +#include "js/PropertyAndElement.h" // JS_GetProperty +#include "jsapi-tests/tests.h" + +using namespace js; + +class AutoInflatedString { + JSContext* const cx; + char16_t* chars_; + size_t length_; + + public: + explicit AutoInflatedString(JSContext* cx) + : cx(cx), chars_(nullptr), length_(0) {} + ~AutoInflatedString() { JS_free(cx, chars_); } + + template <size_t N> + void operator=(const char (&str)[N]) { + length_ = N - 1; + chars_ = InflateString(cx, str, length_); + if (!chars_) { + abort(); + } + } + + void operator=(const char* str) { + length_ = strlen(str); + chars_ = InflateString(cx, str, length_); + if (!chars_) { + abort(); + } + } + + const char16_t* chars() const { return chars_; } + size_t length() const { return length_; } +}; + +BEGIN_TEST(testParseJSON_success) { + // Primitives + JS::RootedValue expected(cx); + expected = JS::TrueValue(); + CHECK(TryParse(cx, "true", expected)); + + expected = JS::FalseValue(); + CHECK(TryParse(cx, "false", expected)); + + expected = JS::NullValue(); + CHECK(TryParse(cx, "null", expected)); + + expected.setInt32(0); + CHECK(TryParse(cx, "0", expected)); + + expected.setInt32(1); + CHECK(TryParse(cx, "1", expected)); + + expected.setInt32(-1); + CHECK(TryParse(cx, "-1", expected)); + + expected.setDouble(1); + CHECK(TryParse(cx, "1", expected)); + + expected.setDouble(1.75); + CHECK(TryParse(cx, "1.75", expected)); + + expected.setDouble(9e9); + CHECK(TryParse(cx, "9e9", expected)); + + expected.setDouble(std::numeric_limits<double>::infinity()); + CHECK(TryParse(cx, "9e99999", expected)); + + JS::Rooted<JSLinearString*> str(cx); + + const char16_t emptystr[] = {'\0'}; + str = js::NewStringCopyN<CanGC>(cx, emptystr, 0); + CHECK(str); + expected = JS::StringValue(str); + CHECK(TryParse(cx, "\"\"", expected)); + + const char16_t nullstr[] = {'\0'}; + str = NewString(cx, nullstr); + CHECK(str); + expected = JS::StringValue(str); + CHECK(TryParse(cx, "\"\\u0000\"", expected)); + + const char16_t backstr[] = {'\b'}; + str = NewString(cx, backstr); + CHECK(str); + expected = JS::StringValue(str); + CHECK(TryParse(cx, "\"\\b\"", expected)); + CHECK(TryParse(cx, "\"\\u0008\"", expected)); + + const char16_t newlinestr[] = { + '\n', + }; + str = NewString(cx, newlinestr); + CHECK(str); + expected = JS::StringValue(str); + CHECK(TryParse(cx, "\"\\n\"", expected)); + CHECK(TryParse(cx, "\"\\u000A\"", expected)); + + // Arrays + JS::RootedValue v(cx), v2(cx); + JS::RootedObject obj(cx); + + bool isArray; + + CHECK(Parse(cx, "[]", &v)); + CHECK(v.isObject()); + obj = &v.toObject(); + CHECK(JS::IsArrayObject(cx, obj, &isArray)); + CHECK(isArray); + CHECK(JS_GetProperty(cx, obj, "length", &v2)); + CHECK(v2.isInt32(0)); + + CHECK(Parse(cx, "[1]", &v)); + CHECK(v.isObject()); + obj = &v.toObject(); + CHECK(JS::IsArrayObject(cx, obj, &isArray)); + CHECK(isArray); + CHECK(JS_GetProperty(cx, obj, "0", &v2)); + CHECK(v2.isInt32(1)); + CHECK(JS_GetProperty(cx, obj, "length", &v2)); + CHECK(v2.isInt32(1)); + + // Objects + CHECK(Parse(cx, "{}", &v)); + CHECK(v.isObject()); + obj = &v.toObject(); + CHECK(JS::IsArrayObject(cx, obj, &isArray)); + CHECK(!isArray); + + CHECK(Parse(cx, "{ \"f\": 17 }", &v)); + CHECK(v.isObject()); + obj = &v.toObject(); + CHECK(JS::IsArrayObject(cx, obj, &isArray)); + CHECK(!isArray); + CHECK(JS_GetProperty(cx, obj, "f", &v2)); + CHECK(v2.isInt32(17)); + + return true; +} + +template <size_t N> +static JSLinearString* NewString(JSContext* cx, const char16_t (&chars)[N]) { + return js::NewStringCopyN<CanGC>(cx, chars, N); +} + +template <size_t N> +inline bool Parse(JSContext* cx, const char (&input)[N], + JS::MutableHandleValue vp) { + AutoInflatedString str(cx); + str = input; + CHECK(JS_ParseJSON(cx, str.chars(), str.length(), vp)); + return true; +} + +template <size_t N> +inline bool TryParse(JSContext* cx, const char (&input)[N], + JS::HandleValue expected) { + AutoInflatedString str(cx); + RootedValue v(cx); + str = input; + CHECK(JS_ParseJSON(cx, str.chars(), str.length(), &v)); + CHECK_SAME(v, expected); + return true; +} +END_TEST(testParseJSON_success) + +BEGIN_TEST(testParseJSON_error) { + CHECK(Error(cx, "", 1, 1)); + CHECK(Error(cx, "\n", 2, 1)); + CHECK(Error(cx, "\r", 2, 1)); + CHECK(Error(cx, "\r\n", 2, 1)); + + CHECK(Error(cx, "[", 1, 2)); + CHECK(Error(cx, "[,]", 1, 2)); + CHECK(Error(cx, "[1,]", 1, 4)); + CHECK(Error(cx, "{a:2}", 1, 2)); + CHECK(Error(cx, "{\"a\":2,}", 1, 8)); + CHECK(Error(cx, "]", 1, 1)); + CHECK(Error(cx, "\"", 1, 2)); + CHECK(Error(cx, "{]", 1, 2)); + CHECK(Error(cx, "[}", 1, 2)); + CHECK(Error(cx, "'wrongly-quoted string'", 1, 1)); + + CHECK(Error(cx, "{\"a\":2 \n b:3}", 2, 2)); + CHECK(Error(cx, "\n[", 2, 2)); + CHECK(Error(cx, "\n[,]", 2, 2)); + CHECK(Error(cx, "\n[1,]", 2, 4)); + CHECK(Error(cx, "\n{a:2}", 2, 2)); + CHECK(Error(cx, "\n{\"a\":2,}", 2, 8)); + CHECK(Error(cx, "\n]", 2, 1)); + CHECK(Error(cx, "\"bad string\n\"", 1, 12)); + CHECK(Error(cx, "\r'wrongly-quoted string'", 2, 1)); + CHECK(Error(cx, "\n\"", 2, 2)); + CHECK(Error(cx, "\n{]", 2, 2)); + CHECK(Error(cx, "\n[}", 2, 2)); + CHECK(Error(cx, "{\"a\":[2,3],\n\"b\":,5,6}", 2, 5)); + + CHECK(Error(cx, "{\"a\":2 \r b:3}", 2, 2)); + CHECK(Error(cx, "\r[", 2, 2)); + CHECK(Error(cx, "\r[,]", 2, 2)); + CHECK(Error(cx, "\r[1,]", 2, 4)); + CHECK(Error(cx, "\r{a:2}", 2, 2)); + CHECK(Error(cx, "\r{\"a\":2,}", 2, 8)); + CHECK(Error(cx, "\r]", 2, 1)); + CHECK(Error(cx, "\"bad string\r\"", 1, 12)); + CHECK(Error(cx, "\r'wrongly-quoted string'", 2, 1)); + CHECK(Error(cx, "\r\"", 2, 2)); + CHECK(Error(cx, "\r{]", 2, 2)); + CHECK(Error(cx, "\r[}", 2, 2)); + CHECK(Error(cx, "{\"a\":[2,3],\r\"b\":,5,6}", 2, 5)); + + CHECK(Error(cx, "{\"a\":2 \r\n b:3}", 2, 2)); + CHECK(Error(cx, "\r\n[", 2, 2)); + CHECK(Error(cx, "\r\n[,]", 2, 2)); + CHECK(Error(cx, "\r\n[1,]", 2, 4)); + CHECK(Error(cx, "\r\n{a:2}", 2, 2)); + CHECK(Error(cx, "\r\n{\"a\":2,}", 2, 8)); + CHECK(Error(cx, "\r\n]", 2, 1)); + CHECK(Error(cx, "\"bad string\r\n\"", 1, 12)); + CHECK(Error(cx, "\r\n'wrongly-quoted string'", 2, 1)); + CHECK(Error(cx, "\r\n\"", 2, 2)); + CHECK(Error(cx, "\r\n{]", 2, 2)); + CHECK(Error(cx, "\r\n[}", 2, 2)); + CHECK(Error(cx, "{\"a\":[2,3],\r\n\"b\":,5,6}", 2, 5)); + + CHECK(Error(cx, "\n\"bad string\n\"", 2, 12)); + CHECK(Error(cx, "\r\"bad string\r\"", 2, 12)); + CHECK(Error(cx, "\r\n\"bad string\r\n\"", 2, 12)); + + CHECK(Error(cx, "{\n\"a\":[2,3],\r\"b\":,5,6}", 3, 5)); + CHECK(Error(cx, "{\r\"a\":[2,3],\n\"b\":,5,6}", 3, 5)); + CHECK(Error(cx, "[\"\\t\\q", 1, 6)); + CHECK(Error(cx, "[\"\\t\x00", 1, 5)); + CHECK(Error(cx, "[\"\\t\x01", 1, 5)); + CHECK(Error(cx, "[\"\\t\\\x00", 1, 6)); + CHECK(Error(cx, "[\"\\t\\\x01", 1, 6)); + + // Unicode escape errors are messy. The first bad character could be + // non-hexadecimal, or it could be absent entirely. Include tests where + // there's a bad character, followed by zero to as many characters as are + // needed to form a complete Unicode escape sequence, plus one. (The extra + // characters beyond are valuable because our implementation checks for + // too-few subsequent characters first, before checking for subsequent + // non-hexadecimal characters. So \u<END>, \u0<END>, \u00<END>, and + // \u000<END> are all *detected* as invalid by the same code path, but the + // process of computing the first invalid character follows a different + // code path for each. And \uQQQQ, \u0QQQ, \u00QQ, and \u000Q are detected + // as invalid by the same code path [ignoring which precise subexpression + // triggers failure of a single condition], but the computation of the + // first invalid character follows a different code path for each.) + CHECK(Error(cx, "[\"\\t\\u", 1, 7)); + CHECK(Error(cx, "[\"\\t\\uZ", 1, 7)); + CHECK(Error(cx, "[\"\\t\\uZZ", 1, 7)); + CHECK(Error(cx, "[\"\\t\\uZZZ", 1, 7)); + CHECK(Error(cx, "[\"\\t\\uZZZZ", 1, 7)); + CHECK(Error(cx, "[\"\\t\\uZZZZZ", 1, 7)); + + CHECK(Error(cx, "[\"\\t\\u0", 1, 8)); + CHECK(Error(cx, "[\"\\t\\u0Z", 1, 8)); + CHECK(Error(cx, "[\"\\t\\u0ZZ", 1, 8)); + CHECK(Error(cx, "[\"\\t\\u0ZZZ", 1, 8)); + CHECK(Error(cx, "[\"\\t\\u0ZZZZ", 1, 8)); + + CHECK(Error(cx, "[\"\\t\\u00", 1, 9)); + CHECK(Error(cx, "[\"\\t\\u00Z", 1, 9)); + CHECK(Error(cx, "[\"\\t\\u00ZZ", 1, 9)); + CHECK(Error(cx, "[\"\\t\\u00ZZZ", 1, 9)); + + CHECK(Error(cx, "[\"\\t\\u000", 1, 10)); + CHECK(Error(cx, "[\"\\t\\u000Z", 1, 10)); + CHECK(Error(cx, "[\"\\t\\u000ZZ", 1, 10)); + + return true; +} + +template <size_t N> +inline bool Error(JSContext* cx, const char (&input)[N], uint32_t expectedLine, + uint32_t expectedColumn) { + AutoInflatedString str(cx); + RootedValue dummy(cx); + str = input; + + bool ok = JS_ParseJSON(cx, str.chars(), str.length(), &dummy); + CHECK(!ok); + + JS::ExceptionStack exnStack(cx); + CHECK(StealPendingExceptionStack(cx, &exnStack)); + + JS::ErrorReportBuilder report(cx); + CHECK(report.init(cx, exnStack, JS::ErrorReportBuilder::WithSideEffects)); + CHECK(report.report()->errorNumber == JSMSG_JSON_BAD_PARSE); + + UniqueChars lineAndColumnASCII = + JS_smprintf("line %d column %d", expectedLine, expectedColumn); + CHECK(strstr(report.toStringResult().c_str(), lineAndColumnASCII.get()) != + nullptr); + + /* We do not execute JS, so there should be no exception thrown. */ + CHECK(!JS_IsExceptionPending(cx)); + + return true; +} +END_TEST(testParseJSON_error) + +static bool Censor(JSContext* cx, unsigned argc, JS::Value* vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + MOZ_RELEASE_ASSERT(args.length() == 2); + MOZ_RELEASE_ASSERT(args[0].isString()); + args.rval().setNull(); + return true; +} + +BEGIN_TEST(testParseJSON_reviver) { + JSFunction* fun = JS_NewFunction(cx, Censor, 0, 0, "censor"); + CHECK(fun); + + JS::RootedValue filter(cx, JS::ObjectValue(*JS_GetFunctionObject(fun))); + + CHECK(TryParse(cx, "true", filter)); + CHECK(TryParse(cx, "false", filter)); + CHECK(TryParse(cx, "null", filter)); + CHECK(TryParse(cx, "1", filter)); + CHECK(TryParse(cx, "1.75", filter)); + CHECK(TryParse(cx, "[]", filter)); + CHECK(TryParse(cx, "[1]", filter)); + CHECK(TryParse(cx, "{}", filter)); + return true; +} + +template <size_t N> +inline bool TryParse(JSContext* cx, const char (&input)[N], + JS::HandleValue filter) { + AutoInflatedString str(cx); + JS::RootedValue v(cx); + str = input; + CHECK(JS_ParseJSONWithReviver(cx, str.chars(), str.length(), filter, &v)); + CHECK(v.isNull()); + return true; +} +END_TEST(testParseJSON_reviver) diff --git a/js/src/jsapi-tests/testParserAtom.cpp b/js/src/jsapi-tests/testParserAtom.cpp new file mode 100644 index 0000000000..8a1fb6ff53 --- /dev/null +++ b/js/src/jsapi-tests/testParserAtom.cpp @@ -0,0 +1,445 @@ +/* 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 "mozilla/Range.h" // mozilla::Range +#include "mozilla/Utf8.h" // mozilla::Utf8Unit + +#include <string> // std::char_traits +#include <utility> // std::initializer_list +#include <vector> // std::vector + +#include "frontend/FrontendContext.h" // AutoReportFrontendContext +#include "frontend/ParserAtom.h" // js::frontend::ParserAtomsTable, js::frontend::WellKnownParserAtoms +#include "js/TypeDecls.h" // JS::Latin1Char +#include "jsapi-tests/tests.h" + +// Test empty strings behave consistently. +BEGIN_TEST(testParserAtom_empty) { + using js::frontend::ParserAtom; + using js::frontend::ParserAtomsTable; + using js::frontend::ParserAtomVector; + using js::frontend::TaggedParserAtomIndex; + + js::AutoReportFrontendContext fc(cx); + js::LifoAlloc alloc(512); + ParserAtomsTable atomTable(alloc); + + const char ascii[] = {}; + const JS::Latin1Char latin1[] = {}; + const mozilla::Utf8Unit utf8[] = {}; + const char16_t char16[] = {}; + + // Check that the well-known empty atom matches for different entry points. + auto refIndex = TaggedParserAtomIndex::WellKnown::empty(); + CHECK(atomTable.internAscii(&fc, ascii, 0) == refIndex); + CHECK(atomTable.internLatin1(&fc, latin1, 0) == refIndex); + CHECK(atomTable.internUtf8(&fc, utf8, 0) == refIndex); + CHECK(atomTable.internChar16(&fc, char16, 0) == refIndex); + + return true; +} +END_TEST(testParserAtom_empty) + +// Test length-1 fast-path is consistent across entry points for ASCII. +BEGIN_TEST(testParserAtom_tiny1_ASCII) { + using js::frontend::ParserAtom; + using js::frontend::ParserAtomsTable; + using js::frontend::ParserAtomVector; + using js::frontend::WellKnownParserAtoms; + + js::AutoReportFrontendContext fc(cx); + js::LifoAlloc alloc(512); + ParserAtomsTable atomTable(alloc); + + char16_t a = 'a'; + const char ascii[] = {'a'}; + JS::Latin1Char latin1[] = {'a'}; + const mozilla::Utf8Unit utf8[] = {mozilla::Utf8Unit('a')}; + char16_t char16[] = {'a'}; + + auto refIndex = WellKnownParserAtoms::getSingleton().lookupTinyIndex(&a, 1); + CHECK(refIndex); + CHECK(atomTable.internAscii(&fc, ascii, 1) == refIndex); + CHECK(atomTable.internLatin1(&fc, latin1, 1) == refIndex); + CHECK(atomTable.internUtf8(&fc, utf8, 1) == refIndex); + CHECK(atomTable.internChar16(&fc, char16, 1) == refIndex); + + return true; +} +END_TEST(testParserAtom_tiny1_ASCII) + +// Test length-1 fast-path is consistent across entry points for non-ASCII. +BEGIN_TEST(testParserAtom_tiny1_nonASCII) { + using js::frontend::ParserAtom; + using js::frontend::ParserAtomsTable; + using js::frontend::ParserAtomVector; + using js::frontend::WellKnownParserAtoms; + + js::AutoReportFrontendContext fc(cx); + js::LifoAlloc alloc(512); + ParserAtomsTable atomTable(alloc); + + { + char16_t euro = 0x0080; + JS::Latin1Char latin1[] = {0x80}; + const mozilla::Utf8Unit utf8[] = { + mozilla::Utf8Unit(static_cast<unsigned char>(0xC2)), + mozilla::Utf8Unit(static_cast<unsigned char>(0x80))}; + char16_t char16[] = {0x0080}; + + auto refIndex = + WellKnownParserAtoms::getSingleton().lookupTinyIndex(&euro, 1); + CHECK(refIndex); + CHECK(atomTable.internLatin1(&fc, latin1, 1) == refIndex); + CHECK(atomTable.internUtf8(&fc, utf8, 2) == refIndex); + CHECK(atomTable.internChar16(&fc, char16, 1) == refIndex); + } + + { + char16_t frac12 = 0x00BD; + JS::Latin1Char latin1[] = {0xBD}; + const mozilla::Utf8Unit utf8[] = { + mozilla::Utf8Unit(static_cast<unsigned char>(0xC2)), + mozilla::Utf8Unit(static_cast<unsigned char>(0xBD))}; + char16_t char16[] = {0x00BD}; + + auto refIndex = + WellKnownParserAtoms::getSingleton().lookupTinyIndex(½, 1); + CHECK(refIndex); + CHECK(atomTable.internLatin1(&fc, latin1, 1) == refIndex); + CHECK(atomTable.internUtf8(&fc, utf8, 2) == refIndex); + CHECK(atomTable.internChar16(&fc, char16, 1) == refIndex); + } + + { + char16_t iquest = 0x00BF; + JS::Latin1Char latin1[] = {0xBF}; + const mozilla::Utf8Unit utf8[] = { + mozilla::Utf8Unit(static_cast<unsigned char>(0xC2)), + mozilla::Utf8Unit(static_cast<unsigned char>(0xBF))}; + char16_t char16[] = {0x00BF}; + + auto refIndex = + WellKnownParserAtoms::getSingleton().lookupTinyIndex(¿, 1); + CHECK(refIndex); + CHECK(atomTable.internLatin1(&fc, latin1, 1) == refIndex); + CHECK(atomTable.internUtf8(&fc, utf8, 2) == refIndex); + CHECK(atomTable.internChar16(&fc, char16, 1) == refIndex); + } + + { + char16_t agrave = 0x00C0; + JS::Latin1Char latin1[] = {0xC0}; + const mozilla::Utf8Unit utf8[] = { + mozilla::Utf8Unit(static_cast<unsigned char>(0xC3)), + mozilla::Utf8Unit(static_cast<unsigned char>(0x80))}; + char16_t char16[] = {0x00C0}; + + auto refIndex = + WellKnownParserAtoms::getSingleton().lookupTinyIndex(à, 1); + CHECK(refIndex); + CHECK(atomTable.internLatin1(&fc, latin1, 1) == refIndex); + CHECK(atomTable.internUtf8(&fc, utf8, 2) == refIndex); + CHECK(atomTable.internChar16(&fc, char16, 1) == refIndex); + } + + { + char16_t ae = 0x00E6; + JS::Latin1Char latin1[] = {0xE6}; + const mozilla::Utf8Unit utf8[] = { + mozilla::Utf8Unit(static_cast<unsigned char>(0xC3)), + mozilla::Utf8Unit(static_cast<unsigned char>(0xA6))}; + char16_t char16[] = {0x00E6}; + + auto refIndex = + WellKnownParserAtoms::getSingleton().lookupTinyIndex(&ae, 1); + CHECK(refIndex); + CHECK(atomTable.internLatin1(&fc, latin1, 1) == refIndex); + CHECK(atomTable.internUtf8(&fc, utf8, 2) == refIndex); + CHECK(atomTable.internChar16(&fc, char16, 1) == refIndex); + } + + { + char16_t yuml = 0x00FF; + JS::Latin1Char latin1[] = {0xFF}; + const mozilla::Utf8Unit utf8[] = { + mozilla::Utf8Unit(static_cast<unsigned char>(0xC3)), + mozilla::Utf8Unit(static_cast<unsigned char>(0xBF))}; + char16_t char16[] = {0x00FF}; + + auto refIndex = + WellKnownParserAtoms::getSingleton().lookupTinyIndex(ÿ, 1); + CHECK(refIndex); + CHECK(atomTable.internLatin1(&fc, latin1, 1) == refIndex); + CHECK(atomTable.internUtf8(&fc, utf8, 2) == refIndex); + CHECK(atomTable.internChar16(&fc, char16, 1) == refIndex); + } + + return true; +} +END_TEST(testParserAtom_tiny1_nonASCII) + +// Test for tiny1 UTF-8 with valid/invalid code units. +// +// NOTE: Passing invalid UTF-8 to internUtf8 hits assertion failure, so +// test in the opposite way. +// lookupTinyIndexUTF8 is used inside internUtf8. +BEGIN_TEST(testParserAtom_tiny1_invalidUTF8) { + using js::frontend::ParserAtom; + using js::frontend::ParserAtomsTable; + using js::frontend::WellKnownParserAtoms; + + js::AutoReportFrontendContext fc(cx); + js::LifoAlloc alloc(512); + ParserAtomsTable atomTable(alloc); + + { + const mozilla::Utf8Unit utf8[] = { + mozilla::Utf8Unit(static_cast<unsigned char>(0xC1)), + mozilla::Utf8Unit(static_cast<unsigned char>(0x80))}; + + CHECK(!WellKnownParserAtoms::getSingleton().lookupTinyIndexUTF8(utf8, 2)); + } + + { + const mozilla::Utf8Unit utf8[] = { + mozilla::Utf8Unit(static_cast<unsigned char>(0xC2)), + mozilla::Utf8Unit(static_cast<unsigned char>(0x7F))}; + + CHECK(!WellKnownParserAtoms::getSingleton().lookupTinyIndexUTF8(utf8, 2)); + } + + { + JS::Latin1Char latin1[] = {0x80}; + const mozilla::Utf8Unit utf8[] = { + mozilla::Utf8Unit(static_cast<unsigned char>(0xC2)), + mozilla::Utf8Unit(static_cast<unsigned char>(0x80))}; + + auto refIndex = + WellKnownParserAtoms::getSingleton().lookupTinyIndexUTF8(utf8, 2); + CHECK(refIndex); + CHECK(atomTable.internLatin1(&fc, latin1, 1) == refIndex); + } + + { + JS::Latin1Char latin1[] = {0xBF}; + const mozilla::Utf8Unit utf8[] = { + mozilla::Utf8Unit(static_cast<unsigned char>(0xC2)), + mozilla::Utf8Unit(static_cast<unsigned char>(0xBF))}; + + auto refIndex = + WellKnownParserAtoms::getSingleton().lookupTinyIndexUTF8(utf8, 2); + CHECK(refIndex); + CHECK(atomTable.internLatin1(&fc, latin1, 1) == refIndex); + } + + { + const mozilla::Utf8Unit utf8[] = { + mozilla::Utf8Unit(static_cast<unsigned char>(0xC2)), + mozilla::Utf8Unit(static_cast<unsigned char>(0xC0))}; + + CHECK(!WellKnownParserAtoms::getSingleton().lookupTinyIndexUTF8(utf8, 2)); + } + + { + const mozilla::Utf8Unit utf8[] = { + mozilla::Utf8Unit(static_cast<unsigned char>(0xC3)), + mozilla::Utf8Unit(static_cast<unsigned char>(0x7F))}; + + CHECK(!WellKnownParserAtoms::getSingleton().lookupTinyIndexUTF8(utf8, 2)); + } + + { + JS::Latin1Char latin1[] = {0xC0}; + const mozilla::Utf8Unit utf8[] = { + mozilla::Utf8Unit(static_cast<unsigned char>(0xC3)), + mozilla::Utf8Unit(static_cast<unsigned char>(0x80))}; + + auto refIndex = + WellKnownParserAtoms::getSingleton().lookupTinyIndexUTF8(utf8, 2); + CHECK(refIndex); + CHECK(atomTable.internLatin1(&fc, latin1, 1) == refIndex); + } + + { + JS::Latin1Char latin1[] = {0xFF}; + const mozilla::Utf8Unit utf8[] = { + mozilla::Utf8Unit(static_cast<unsigned char>(0xC3)), + mozilla::Utf8Unit(static_cast<unsigned char>(0xBF))}; + + auto refIndex = + WellKnownParserAtoms::getSingleton().lookupTinyIndexUTF8(utf8, 2); + CHECK(refIndex); + CHECK(atomTable.internLatin1(&fc, latin1, 1) == refIndex); + } + + { + const mozilla::Utf8Unit utf8[] = { + mozilla::Utf8Unit(static_cast<unsigned char>(0xC3)), + mozilla::Utf8Unit(static_cast<unsigned char>(0xC0))}; + + CHECK(!WellKnownParserAtoms::getSingleton().lookupTinyIndexUTF8(utf8, 2)); + } + + { + const mozilla::Utf8Unit utf8[] = { + mozilla::Utf8Unit(static_cast<unsigned char>(0xC4)), + mozilla::Utf8Unit(static_cast<unsigned char>(0x7F))}; + + CHECK(!WellKnownParserAtoms::getSingleton().lookupTinyIndexUTF8(utf8, 2)); + } + + { + const mozilla::Utf8Unit utf8[] = { + mozilla::Utf8Unit(static_cast<unsigned char>(0xC4)), + mozilla::Utf8Unit(static_cast<unsigned char>(0x80))}; + + CHECK(!WellKnownParserAtoms::getSingleton().lookupTinyIndexUTF8(utf8, 2)); + } + + { + const mozilla::Utf8Unit utf8[] = { + mozilla::Utf8Unit(static_cast<unsigned char>(0xC4)), + mozilla::Utf8Unit(static_cast<unsigned char>(0xBF))}; + + CHECK(!WellKnownParserAtoms::getSingleton().lookupTinyIndexUTF8(utf8, 2)); + } + + { + const mozilla::Utf8Unit utf8[] = { + mozilla::Utf8Unit(static_cast<unsigned char>(0xC4)), + mozilla::Utf8Unit(static_cast<unsigned char>(0xC0))}; + + CHECK(!WellKnownParserAtoms::getSingleton().lookupTinyIndexUTF8(utf8, 2)); + } + + return true; +} +END_TEST(testParserAtom_tiny1_invalidUTF8) + +// Test length-2 fast-path is consistent across entry points. +BEGIN_TEST(testParserAtom_tiny2) { + using js::frontend::ParserAtom; + using js::frontend::ParserAtomsTable; + using js::frontend::ParserAtomVector; + using js::frontend::WellKnownParserAtoms; + + js::AutoReportFrontendContext fc(cx); + js::LifoAlloc alloc(512); + ParserAtomsTable atomTable(alloc); + + const char ascii[] = {'a', '0'}; + JS::Latin1Char latin1[] = {'a', '0'}; + const mozilla::Utf8Unit utf8[] = {mozilla::Utf8Unit('a'), + mozilla::Utf8Unit('0')}; + char16_t char16[] = {'a', '0'}; + + auto refIndex = + WellKnownParserAtoms::getSingleton().lookupTinyIndex(ascii, 2); + CHECK(refIndex); + CHECK(atomTable.internAscii(&fc, ascii, 2) == refIndex); + CHECK(atomTable.internLatin1(&fc, latin1, 2) == refIndex); + CHECK(atomTable.internUtf8(&fc, utf8, 2) == refIndex); + CHECK(atomTable.internChar16(&fc, char16, 2) == refIndex); + + // Note: If Latin1-Extended characters become supported, then UTF-8 behaviour + // should be tested. + char16_t ae0[] = {0x00E6, '0'}; + CHECK(!WellKnownParserAtoms::getSingleton().lookupTinyIndex(ae0, 2)); + + return true; +} +END_TEST(testParserAtom_tiny2) + +// Test length-3 fast-path is consistent across entry points. +BEGIN_TEST(testParserAtom_int) { + using js::frontend::ParserAtom; + using js::frontend::ParserAtomsTable; + using js::frontend::ParserAtomVector; + using js::frontend::WellKnownParserAtoms; + + js::AutoReportFrontendContext fc(cx); + js::LifoAlloc alloc(512); + ParserAtomsTable atomTable(alloc); + + { + const char ascii[] = {'1', '0', '0'}; + JS::Latin1Char latin1[] = {'1', '0', '0'}; + const mozilla::Utf8Unit utf8[] = { + mozilla::Utf8Unit('1'), mozilla::Utf8Unit('0'), mozilla::Utf8Unit('0')}; + char16_t char16[] = {'1', '0', '0'}; + + auto refIndex = + WellKnownParserAtoms::getSingleton().lookupTinyIndex(ascii, 3); + CHECK(refIndex); + CHECK(atomTable.internAscii(&fc, ascii, 3) == refIndex); + CHECK(atomTable.internLatin1(&fc, latin1, 3) == refIndex); + CHECK(atomTable.internUtf8(&fc, utf8, 3) == refIndex); + CHECK(atomTable.internChar16(&fc, char16, 3) == refIndex); + } + + { + const char ascii[] = {'2', '5', '5'}; + JS::Latin1Char latin1[] = {'2', '5', '5'}; + const mozilla::Utf8Unit utf8[] = { + mozilla::Utf8Unit('2'), mozilla::Utf8Unit('5'), mozilla::Utf8Unit('5')}; + char16_t char16[] = {'2', '5', '5'}; + + auto refIndex = + WellKnownParserAtoms::getSingleton().lookupTinyIndex(ascii, 3); + CHECK(refIndex); + CHECK(atomTable.internAscii(&fc, ascii, 3) == refIndex); + CHECK(atomTable.internLatin1(&fc, latin1, 3) == refIndex); + CHECK(atomTable.internUtf8(&fc, utf8, 3) == refIndex); + CHECK(atomTable.internChar16(&fc, char16, 3) == refIndex); + } + + { + const char ascii[] = {'0', '9', '9'}; + + CHECK(!WellKnownParserAtoms::getSingleton().lookupTinyIndex(ascii, 3)); + } + + { + const char ascii[] = {'0', 'F', 'F'}; + + CHECK(!WellKnownParserAtoms::getSingleton().lookupTinyIndex(ascii, 3)); + } + + { + const char ascii[] = {'1', '0', 'A'}; + + CHECK(!WellKnownParserAtoms::getSingleton().lookupTinyIndex(ascii, 3)); + } + + { + const char ascii[] = {'1', '0', 'a'}; + + CHECK(!WellKnownParserAtoms::getSingleton().lookupTinyIndex(ascii, 3)); + } + + { + const char ascii[] = {'2', '5', '6'}; + + CHECK(!WellKnownParserAtoms::getSingleton().lookupTinyIndex(ascii, 3)); + } + + { + const char ascii[] = {'3', '0', '0'}; + + CHECK(!WellKnownParserAtoms::getSingleton().lookupTinyIndex(ascii, 3)); + } + + return true; +} +END_TEST(testParserAtom_int) + +// "€" U+0080 +// "½" U+00BD +// "¿" U+00BF +// "À" U+00C0 +// "æ" U+00E6 +// "ÿ" U+00FF +// "π" U+03C0 +// "🍕" U+1F355 diff --git a/js/src/jsapi-tests/testPersistentRooted.cpp b/js/src/jsapi-tests/testPersistentRooted.cpp new file mode 100644 index 0000000000..4b3ab3050d --- /dev/null +++ b/js/src/jsapi-tests/testPersistentRooted.cpp @@ -0,0 +1,212 @@ +/* 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 "js/Class.h" +#include "jsapi-tests/tests.h" + +using namespace JS; + +struct BarkWhenTracedClass { + static int finalizeCount; + static int traceCount; + + static const JSClass class_; + static void finalize(JS::GCContext* gcx, JSObject* obj) { finalizeCount++; } + static void trace(JSTracer* trc, JSObject* obj) { traceCount++; } + static void reset() { + finalizeCount = 0; + traceCount = 0; + } +}; + +int BarkWhenTracedClass::finalizeCount; +int BarkWhenTracedClass::traceCount; + +static const JSClassOps BarkWhenTracedClassClassOps = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + BarkWhenTracedClass::finalize, // finalize + nullptr, // call + nullptr, // construct + BarkWhenTracedClass::trace, // trace +}; + +const JSClass BarkWhenTracedClass::class_ = {"BarkWhenTracedClass", + JSCLASS_FOREGROUND_FINALIZE, + &BarkWhenTracedClassClassOps}; + +struct Kennel { + PersistentRootedObject obj; + Kennel() {} + explicit Kennel(JSContext* cx) : obj(cx) {} + Kennel(JSContext* cx, const HandleObject& woof) : obj(cx, woof) {} + void init(JSContext* cx, const HandleObject& woof) { obj.init(cx, woof); } + void clear() { obj = nullptr; } +}; + +// A function for allocating a Kennel and a barker. Only allocating +// PersistentRooteds on the heap, and in this function, helps ensure that the +// conservative GC doesn't find stray references to the barker. Ugh. +MOZ_NEVER_INLINE static Kennel* Allocate(JSContext* cx) { + RootedObject barker(cx, JS_NewObject(cx, &BarkWhenTracedClass::class_)); + if (!barker) { + return nullptr; + } + + return new Kennel(cx, barker); +} + +// Do a GC, expecting |n| barkers to be finalized. +static bool GCFinalizesNBarkers(JSContext* cx, int n) { + int preGCTrace = BarkWhenTracedClass::traceCount; + int preGCFinalize = BarkWhenTracedClass::finalizeCount; + + JS_GC(cx); + + return (BarkWhenTracedClass::finalizeCount == preGCFinalize + n && + BarkWhenTracedClass::traceCount > preGCTrace); +} + +// PersistentRooted instances protect their contents from being recycled. +BEGIN_TEST(test_PersistentRooted) { + BarkWhenTracedClass::reset(); + + mozilla::UniquePtr<Kennel> kennel(Allocate(cx)); + CHECK(kennel.get()); + + // GC should be able to find our barker. + CHECK(GCFinalizesNBarkers(cx, 0)); + + kennel = nullptr; + + // Now GC should not be able to find the barker. + JS_GC(cx); + CHECK(BarkWhenTracedClass::finalizeCount == 1); + + return true; +} +END_TEST(test_PersistentRooted) + +// GC should not be upset by null PersistentRooteds. +BEGIN_TEST(test_PersistentRootedNull) { + BarkWhenTracedClass::reset(); + + Kennel kennel(cx); + CHECK(!kennel.obj); + + JS_GC(cx); + CHECK(BarkWhenTracedClass::finalizeCount == 0); + + return true; +} +END_TEST(test_PersistentRootedNull) + +// Copy construction works. +BEGIN_TEST(test_PersistentRootedCopy) { + BarkWhenTracedClass::reset(); + + mozilla::UniquePtr<Kennel> kennel(Allocate(cx)); + CHECK(kennel.get()); + + CHECK(GCFinalizesNBarkers(cx, 0)); + + // Copy construction! AMAZING! + mozilla::UniquePtr<Kennel> newKennel(new Kennel(*kennel)); + + CHECK(GCFinalizesNBarkers(cx, 0)); + + kennel = nullptr; + + CHECK(GCFinalizesNBarkers(cx, 0)); + + newKennel = nullptr; + + // Now that kennel and nowKennel are both deallocated, GC should not be + // able to find the barker. + JS_GC(cx); + CHECK(BarkWhenTracedClass::finalizeCount == 1); + + return true; +} +END_TEST(test_PersistentRootedCopy) + +// Assignment works. +BEGIN_TEST(test_PersistentRootedAssign) { + BarkWhenTracedClass::reset(); + + mozilla::UniquePtr<Kennel> kennel(Allocate(cx)); + CHECK(kennel.get()); + + CHECK(GCFinalizesNBarkers(cx, 0)); + + // Allocate a new, empty kennel. + mozilla::UniquePtr<Kennel> kennel2(new Kennel(cx)); + + // Assignment! ASTONISHING! + *kennel2 = *kennel; + + // With both kennels referring to the same barker, it is held alive. + CHECK(GCFinalizesNBarkers(cx, 0)); + + kennel2 = nullptr; + + // The destination of the assignment alone holds the barker alive. + CHECK(GCFinalizesNBarkers(cx, 0)); + + // Allocate a second barker. + kennel2 = mozilla::UniquePtr<Kennel>(Allocate(cx)); + CHECK(kennel2.get()); + + *kennel = *kennel2; + + // Nothing refers to the first kennel any more. + CHECK(GCFinalizesNBarkers(cx, 1)); + + kennel = nullptr; + kennel2 = nullptr; + + // Now that kennel and kennel2 are both deallocated, GC should not be + // able to find the barker. + JS_GC(cx); + CHECK(BarkWhenTracedClass::finalizeCount == 2); + + return true; +} +END_TEST(test_PersistentRootedAssign) + +static PersistentRootedObject gGlobalRoot; + +// PersistentRooted instances can initialized in a separate step to allow for +// global PersistentRooteds. +BEGIN_TEST(test_GlobalPersistentRooted) { + BarkWhenTracedClass::reset(); + + CHECK(!gGlobalRoot.initialized()); + + { + RootedObject barker(cx, JS_NewObject(cx, &BarkWhenTracedClass::class_)); + CHECK(barker); + + gGlobalRoot.init(cx, barker); + } + + CHECK(gGlobalRoot.initialized()); + + // GC should be able to find our barker. + CHECK(GCFinalizesNBarkers(cx, 0)); + + gGlobalRoot.reset(); + CHECK(!gGlobalRoot.initialized()); + + // Now GC should not be able to find the barker. + JS_GC(cx); + CHECK(BarkWhenTracedClass::finalizeCount == 1); + + return true; +} +END_TEST(test_GlobalPersistentRooted) diff --git a/js/src/jsapi-tests/testPreserveJitCode.cpp b/js/src/jsapi-tests/testPreserveJitCode.cpp new file mode 100644 index 0000000000..e27e63f2d1 --- /dev/null +++ b/js/src/jsapi-tests/testPreserveJitCode.cpp @@ -0,0 +1,100 @@ +/* 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 "mozilla/Utf8.h" // mozilla::Utf8Unit + +#include "jit/Ion.h" // js::jit::IsIonEnabled +#include "js/CallAndConstruct.h" // JS::CallFunction +#include "js/CompilationAndEvaluation.h" // JS::CompileFunction +#include "js/GlobalObject.h" // JS_NewGlobalObject +#include "js/SourceText.h" // JS::Source{Ownership,Text} +#include "jsapi-tests/tests.h" +#include "util/Text.h" + +#include "vm/JSObject-inl.h" +#include "vm/JSScript-inl.h" + +using namespace JS; + +static void ScriptCallback(JSRuntime* rt, void* data, js::BaseScript* script, + const JS::AutoRequireNoGC& nogc) { + unsigned& count = *static_cast<unsigned*>(data); + if (script->asJSScript()->hasIonScript()) { + ++count; + } +} + +BEGIN_TEST(test_PreserveJitCode) { + CHECK(testPreserveJitCode(false, 0)); + CHECK(testPreserveJitCode(true, 1)); + return true; +} + +unsigned countIonScripts(JSObject* global) { + unsigned count = 0; + js::IterateScripts(cx, global->nonCCWRealm(), &count, ScriptCallback); + return count; +} + +bool testPreserveJitCode(bool preserveJitCode, unsigned remainingIonScripts) { + cx->runtime()->setOffthreadIonCompilationEnabled(false); + + RootedObject global(cx, createTestGlobal(preserveJitCode)); + CHECK(global); + JSAutoRealm ar(cx, global); + + // The Ion JIT may be unavailable due to --disable-jit or lack of support + // for this platform. + if (!js::jit::IsIonEnabled(cx)) { + knownFail = true; + } + + CHECK_EQUAL(countIonScripts(global), 0u); + + static constexpr char source[] = + "var i = 0;\n" + "var sum = 0;\n" + "while (i < 10) {\n" + " sum += i;\n" + " ++i;\n" + "}\n" + "return sum;\n"; + constexpr unsigned length = js_strlen(source); + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + CHECK(srcBuf.init(cx, source, length, JS::SourceOwnership::Borrowed)); + + JS::CompileOptions options(cx); + options.setFileAndLine(__FILE__, 1); + + JS::RootedFunction fun(cx); + JS::RootedObjectVector emptyScopeChain(cx); + fun = JS::CompileFunction(cx, emptyScopeChain, options, "f", 0, nullptr, + srcBuf); + CHECK(fun); + + RootedValue value(cx); + for (unsigned i = 0; i < 1500; ++i) { + CHECK(JS_CallFunction(cx, global, fun, JS::HandleValueArray::empty(), + &value)); + } + CHECK_EQUAL(value.toInt32(), 45); + CHECK_EQUAL(countIonScripts(global), 1u); + + NonIncrementalGC(cx, JS::GCOptions::Normal, GCReason::API); + CHECK_EQUAL(countIonScripts(global), remainingIonScripts); + + NonIncrementalGC(cx, JS::GCOptions::Shrink, GCReason::API); + CHECK_EQUAL(countIonScripts(global), 0u); + + return true; +} + +JSObject* createTestGlobal(bool preserveJitCode) { + JS::RealmOptions options; + options.creationOptions().setPreserveJitCode(preserveJitCode); + return JS_NewGlobalObject(cx, getGlobalClass(), nullptr, + JS::FireOnNewGlobalHook, options); +} +END_TEST(test_PreserveJitCode) diff --git a/js/src/jsapi-tests/testPrintError.cpp b/js/src/jsapi-tests/testPrintError.cpp new file mode 100644 index 0000000000..31d2c2a95f --- /dev/null +++ b/js/src/jsapi-tests/testPrintError.cpp @@ -0,0 +1,125 @@ +/* -*- 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 <cstdio> // fclose, fflush, open_memstream + +#include "js/ErrorReport.h" // JS::PrintError +#include "js/Warnings.h" // JS::SetWarningReporter, JS::WarnUTF8 + +#include "jsapi-tests/tests.h" + +class AutoStreamBuffer { + char* buffer; + size_t size; + FILE* fp; + + public: + AutoStreamBuffer() { fp = open_memstream(&buffer, &size); } + + ~AutoStreamBuffer() { + fclose(fp); + free(buffer); + } + + FILE* stream() { return fp; } + + bool contains(const char* str) { + if (fflush(fp) != 0) { + fprintf(stderr, "Error flushing stream\n"); + return false; + } + if (strcmp(buffer, str) != 0) { + fprintf(stderr, "Expected |%s|, got |%s|\n", str, buffer); + return false; + } + return true; + } +}; + +BEGIN_TEST(testPrintError_Works) { + AutoStreamBuffer buf; + + CHECK(!execDontReport("throw null;", "testPrintError_Works.js", 3)); + + JS::ExceptionStack exnStack(cx); + CHECK(JS::StealPendingExceptionStack(cx, &exnStack)); + + JS::ErrorReportBuilder builder(cx); + CHECK(builder.init(cx, exnStack, JS::ErrorReportBuilder::NoSideEffects)); + JS::PrintError(buf.stream(), builder, false); + + CHECK(buf.contains("testPrintError_Works.js:3:1 uncaught exception: null\n")); + + return true; +} +END_TEST(testPrintError_Works) + +BEGIN_TEST(testPrintError_SkipWarning) { + JS::SetWarningReporter(cx, warningReporter); + CHECK(JS::WarnUTF8(cx, "warning message")); + CHECK(warningSuccess); + return true; +} + +static bool warningSuccess; + +static void warningReporter(JSContext* cx, JSErrorReport* report) { + AutoStreamBuffer buf; + JS::PrintError(buf.stream(), report, false); + warningSuccess = buf.contains(""); +} +END_TEST(testPrintError_SkipWarning) + +bool cls_testPrintError_SkipWarning::warningSuccess = false; + +BEGIN_TEST(testPrintError_PrintWarning) { + JS::SetWarningReporter(cx, warningReporter); + CHECK(JS::WarnUTF8(cx, "warning message")); + CHECK(warningSuccess); + return true; +} + +static bool warningSuccess; + +static void warningReporter(JSContext* cx, JSErrorReport* report) { + AutoStreamBuffer buf; + JS::PrintError(buf.stream(), report, true); + warningSuccess = buf.contains("warning: warning message\n"); +} +END_TEST(testPrintError_PrintWarning) + +bool cls_testPrintError_PrintWarning::warningSuccess = false; + +#define BURRITO "\xF0\x9F\x8C\xAF" + +BEGIN_TEST(testPrintError_UTF16CodeUnits) { + AutoStreamBuffer buf; + + static const char utf8code[] = + "function f() {\n var x = `\n" BURRITO "`; " BURRITO "; } f();"; + + CHECK(!execDontReport(utf8code, "testPrintError_UTF16CodeUnits.js", 1)); + + JS::ExceptionStack exnStack(cx); + CHECK(JS::StealPendingExceptionStack(cx, &exnStack)); + + JS::ErrorReportBuilder builder(cx); + CHECK(builder.init(cx, exnStack, JS::ErrorReportBuilder::NoSideEffects)); + JS::PrintError(buf.stream(), builder, false); + + CHECK( + buf.contains("testPrintError_UTF16CodeUnits.js:3:6 SyntaxError: illegal " + "character U+1F32F:\n" + "testPrintError_UTF16CodeUnits.js:3:6 " BURRITO "`; " BURRITO + "; } f();\n" + "testPrintError_UTF16CodeUnits.js:3:6 .....^\n")); + + return true; +} +END_TEST(testPrintError_UTF16CodeUnits) + +#undef BURRITO diff --git a/js/src/jsapi-tests/testPrintf.cpp b/js/src/jsapi-tests/testPrintf.cpp new file mode 100644 index 0000000000..332d0d85cd --- /dev/null +++ b/js/src/jsapi-tests/testPrintf.cpp @@ -0,0 +1,69 @@ +/* -*- 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 "mozilla/IntegerPrintfMacros.h" + +#include <cfloat> +#include <stdarg.h> + +#include "js/Printf.h" + +#include "jsapi-tests/tests.h" + +static bool MOZ_FORMAT_PRINTF(2, 3) + print_one(const char* expect, const char* fmt, ...) { + va_list ap; + + va_start(ap, fmt); + JS::UniqueChars output = JS_vsmprintf(fmt, ap); + va_end(ap); + + return output && !strcmp(output.get(), expect); +} + +static const char* zero() { + // gcc 9 is altogether too clever about detecting that this will always + // return nullptr. Do not replace 0x10 with 0x1; it will no longer work. + return uintptr_t(&zero) == 0x10 ? "never happens" : nullptr; +} + +BEGIN_TEST(testPrintf) { + CHECK(print_one("23", "%d", 23)); + CHECK(print_one("-1", "%d", -1)); + CHECK(print_one("23", "%u", 23u)); + CHECK(print_one("0x17", "0x%x", 23u)); + CHECK(print_one("0xFF", "0x%X", 255u)); + CHECK(print_one("027", "0%o", 23u)); + CHECK(print_one("-1", "%hd", (short)-1)); + // This could be expanded if need be, it's just convenient to do + // it this way. + if (sizeof(short) == 2) { + CHECK(print_one("8000", "%hx", (unsigned short)0x8000)); + } + CHECK(print_one("0xf0f0", "0x%lx", 0xf0f0ul)); + CHECK(print_one("0xF0F0", "0x%llX", 0xf0f0ull)); + CHECK(print_one("27270", "%zu", (size_t)27270)); + CHECK(print_one("27270", "%zu", (size_t)27270)); + CHECK(print_one("hello", "he%so", "ll")); + CHECK(print_one("(null)", "%s", ::zero())); + CHECK(print_one("0", "%p", (char*)0)); + CHECK(print_one("h", "%c", 'h')); + CHECK(print_one("1.500000", "%f", 1.5f)); + CHECK(print_one("1.5", "%g", 1.5)); + + // Regression test for bug#1350097. The bug was an assertion + // failure caused by printing a very long floating point value. + print_one("ignore", "%lf", DBL_MAX); + + CHECK(print_one("2727", "%" PRIu32, (uint32_t)2727)); + CHECK(print_one("aa7", "%" PRIx32, (uint32_t)2727)); + CHECK(print_one("2727", "%" PRIu64, (uint64_t)2727)); + CHECK(print_one("aa7", "%" PRIx64, (uint64_t)2727)); + + return true; +} +END_TEST(testPrintf) diff --git a/js/src/jsapi-tests/testPrivateGCThingValue.cpp b/js/src/jsapi-tests/testPrivateGCThingValue.cpp new file mode 100644 index 0000000000..a4560b68d2 --- /dev/null +++ b/js/src/jsapi-tests/testPrivateGCThingValue.cpp @@ -0,0 +1,64 @@ +/* -*- 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 "mozilla/Utf8.h" // mozilla::Utf8Unit + +#include "jsapi.h" + +#include "js/CompilationAndEvaluation.h" // JS::Compile +#include "js/HeapAPI.h" +#include "js/SourceText.h" // JS::Source{Ownership,Text} +#include "jsapi-tests/tests.h" +#include "util/Text.h" + +class TestTracer final : public JS::CallbackTracer { + void onChild(JS::GCCellPtr thing, const char* name) override { + if (thing.asCell() == expectedCell && thing.kind() == expectedKind) { + found = true; + } + } + + public: + js::gc::Cell* expectedCell; + JS::TraceKind expectedKind; + bool found; + + explicit TestTracer(JSContext* cx) + : JS::CallbackTracer(cx), + expectedCell(nullptr), + expectedKind(static_cast<JS::TraceKind>(0)), + found(false) {} +}; + +static const JSClass TestClass = {"TestClass", JSCLASS_HAS_RESERVED_SLOTS(1)}; + +BEGIN_TEST(testPrivateGCThingValue) { + JS::RootedObject obj(cx, JS_NewObject(cx, &TestClass)); + CHECK(obj); + + // Make a JSScript to stick into a PrivateGCThingValue. + static const char code[] = "'objet petit a'"; + + JS::CompileOptions options(cx); + options.setFileAndLine(__FILE__, __LINE__); + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + CHECK(srcBuf.init(cx, code, js_strlen(code), JS::SourceOwnership::Borrowed)); + + JS::RootedScript script(cx, JS::Compile(cx, options, srcBuf)); + CHECK(script); + JS_SetReservedSlot(obj, 0, PrivateGCThingValue(script)); + + TestTracer trc(cx); + trc.expectedCell = script; + trc.expectedKind = JS::TraceKind::Script; + JS::TraceChildren(&trc, JS::GCCellPtr(obj, JS::TraceKind::Object)); + CHECK(trc.found); + + return true; +} +END_TEST(testPrivateGCThingValue) diff --git a/js/src/jsapi-tests/testProfileStrings.cpp b/js/src/jsapi-tests/testProfileStrings.cpp new file mode 100644 index 0000000000..030b31ce8f --- /dev/null +++ b/js/src/jsapi-tests/testProfileStrings.cpp @@ -0,0 +1,225 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + * + * Tests the stack-based instrumentation profiler on a JSRuntime + */ +/* 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 "mozilla/Atomics.h" + +#include "js/CallAndConstruct.h" +#include "js/ContextOptions.h" +#include "js/GlobalObject.h" +#include "js/PropertySpec.h" +#include "jsapi-tests/tests.h" +#include "vm/JSContext.h" + +static ProfilingStack profilingStack; +static uint32_t peakStackPointer = 0; + +static void reset(JSContext* cx) { + profilingStack.stackPointer = 0; + cx->runtime()->geckoProfiler().stringsReset(); + cx->runtime()->geckoProfiler().enableSlowAssertions(true); + js::EnableContextProfilingStack(cx, true); +} + +static const JSClass ptestClass = {"Prof", 0}; + +static bool test_fn(JSContext* cx, unsigned argc, JS::Value* vp) { + peakStackPointer = profilingStack.stackPointer; + return true; +} + +static bool test_fn2(JSContext* cx, unsigned argc, JS::Value* vp) { + JS::RootedValue r(cx); + JS::RootedObject global(cx, JS::CurrentGlobalOrNull(cx)); + return JS_CallFunctionName(cx, global, "d", JS::HandleValueArray::empty(), + &r); +} + +static bool enable(JSContext* cx, unsigned argc, JS::Value* vp) { + js::EnableContextProfilingStack(cx, true); + return true; +} + +static bool disable(JSContext* cx, unsigned argc, JS::Value* vp) { + js::EnableContextProfilingStack(cx, false); + return true; +} + +static bool Prof(JSContext* cx, unsigned argc, JS::Value* vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + JSObject* obj = JS_NewObjectForConstructor(cx, &ptestClass, args); + if (!obj) { + return false; + } + args.rval().setObject(*obj); + return true; +} + +static const JSFunctionSpec ptestFunctions[] = { + JS_FN("test_fn", test_fn, 0, 0), JS_FN("test_fn2", test_fn2, 0, 0), + JS_FN("enable", enable, 0, 0), JS_FN("disable", disable, 0, 0), JS_FS_END}; + +static JSObject* initialize(JSContext* cx) { + js::SetContextProfilingStack(cx, &profilingStack); + JS::RootedObject global(cx, JS::CurrentGlobalOrNull(cx)); + return JS_InitClass(cx, global, nullptr, nullptr, "Prof", Prof, 0, nullptr, + ptestFunctions, nullptr, nullptr); +} + +BEGIN_TEST(testProfileStrings_isCalledWithInterpreter) { + CHECK(initialize(cx)); + + EXEC("function g() { var p = new Prof(); p.test_fn(); }"); + EXEC("function f() { g(); }"); + EXEC("function e() { f(); }"); + EXEC("function d() { e(); }"); + EXEC("function c() { d(); }"); + EXEC("function b() { c(); }"); + EXEC("function a() { b(); }"); + EXEC("function check() { var p = new Prof(); p.test_fn(); a(); }"); + EXEC("function check2() { var p = new Prof(); p.test_fn2(); }"); + + reset(cx); + { + JS::RootedValue rval(cx); + /* Make sure the stack resets and we have an entry for each stack */ + CHECK(JS_CallFunctionName(cx, global, "check", + JS::HandleValueArray::empty(), &rval)); + CHECK(profilingStack.stackPointer == 0); + CHECK(peakStackPointer >= 8); + CHECK(cx->runtime()->geckoProfiler().stringsCount() == 8); + /* Make sure the stack resets and we added no new entries */ + peakStackPointer = 0; + CHECK(JS_CallFunctionName(cx, global, "check", + JS::HandleValueArray::empty(), &rval)); + CHECK(profilingStack.stackPointer == 0); + CHECK(peakStackPointer >= 8); + CHECK(cx->runtime()->geckoProfiler().stringsCount() == 8); + } + reset(cx); + { + JS::RootedValue rval(cx); + CHECK(JS_CallFunctionName(cx, global, "check2", + JS::HandleValueArray::empty(), &rval)); + CHECK(cx->runtime()->geckoProfiler().stringsCount() == 5); + CHECK(peakStackPointer >= 6); + CHECK(profilingStack.stackPointer == 0); + } + return true; +} +END_TEST(testProfileStrings_isCalledWithInterpreter) + +BEGIN_TEST(testProfileStrings_isCalledWithJIT) { + CHECK(initialize(cx)); + + EXEC("function g() { var p = new Prof(); p.test_fn(); }"); + EXEC("function f() { g(); }"); + EXEC("function e() { f(); }"); + EXEC("function d() { e(); }"); + EXEC("function c() { d(); }"); + EXEC("function b() { c(); }"); + EXEC("function a() { b(); }"); + EXEC("function check() { var p = new Prof(); p.test_fn(); a(); }"); + EXEC("function check2() { var p = new Prof(); p.test_fn2(); }"); + + reset(cx); + { + JS::RootedValue rval(cx); + /* Make sure the stack resets and we have an entry for each stack */ + CHECK(JS_CallFunctionName(cx, global, "check", + JS::HandleValueArray::empty(), &rval)); + CHECK(profilingStack.stackPointer == 0); + CHECK(peakStackPointer >= 8); + + /* Make sure the stack resets and we added no new entries */ + uint32_t cnt = cx->runtime()->geckoProfiler().stringsCount(); + peakStackPointer = 0; + CHECK(JS_CallFunctionName(cx, global, "check", + JS::HandleValueArray::empty(), &rval)); + CHECK(profilingStack.stackPointer == 0); + CHECK(cx->runtime()->geckoProfiler().stringsCount() == cnt); + CHECK(peakStackPointer >= 8); + } + + return true; +} +END_TEST(testProfileStrings_isCalledWithJIT) + +BEGIN_TEST(testProfileStrings_isCalledWhenError) { + CHECK(initialize(cx)); + + EXEC("function check2() { throw 'a'; }"); + + reset(cx); + { + JS::RootedValue rval(cx); + /* Make sure the stack resets and we have an entry for each stack */ + bool ok = JS_CallFunctionName(cx, global, "check2", + JS::HandleValueArray::empty(), &rval); + CHECK(!ok); + CHECK(profilingStack.stackPointer == 0); + CHECK(cx->runtime()->geckoProfiler().stringsCount() == 1); + + JS_ClearPendingException(cx); + } + + return true; +} +END_TEST(testProfileStrings_isCalledWhenError) + +BEGIN_TEST(testProfileStrings_worksWhenEnabledOnTheFly) { + CHECK(initialize(cx)); + + EXEC("function b(p) { p.test_fn(); }"); + EXEC("function a() { var p = new Prof(); p.enable(); b(p); }"); + reset(cx); + js::EnableContextProfilingStack(cx, false); + { + /* enable it in the middle of JS and make sure things check out */ + JS::RootedValue rval(cx); + JS_CallFunctionName(cx, global, "a", JS::HandleValueArray::empty(), &rval); + CHECK(profilingStack.stackPointer == 0); + CHECK(peakStackPointer >= 1); + CHECK(cx->runtime()->geckoProfiler().stringsCount() == 1); + } + + EXEC("function d(p) { p.disable(); }"); + EXEC("function c() { var p = new Prof(); d(p); }"); + reset(cx); + { + /* now disable in the middle of js */ + JS::RootedValue rval(cx); + JS_CallFunctionName(cx, global, "c", JS::HandleValueArray::empty(), &rval); + CHECK(profilingStack.stackPointer == 0); + } + + EXEC("function e() { var p = new Prof(); d(p); p.enable(); b(p); }"); + reset(cx); + { + /* now disable in the middle of js, but re-enable before final exit */ + JS::RootedValue rval(cx); + JS_CallFunctionName(cx, global, "e", JS::HandleValueArray::empty(), &rval); + CHECK(profilingStack.stackPointer == 0); + CHECK(peakStackPointer >= 3); + } + + EXEC("function h() { }"); + EXEC("function g(p) { p.disable(); for (var i = 0; i < 100; i++) i++; }"); + EXEC("function f() { g(new Prof()); }"); + reset(cx); + cx->runtime()->geckoProfiler().enableSlowAssertions(false); + { + JS::RootedValue rval(cx); + /* disable, and make sure that if we try to re-enter the JIT the pop + * will still happen */ + JS_CallFunctionName(cx, global, "f", JS::HandleValueArray::empty(), &rval); + CHECK(profilingStack.stackPointer == 0); + } + return true; +} +END_TEST(testProfileStrings_worksWhenEnabledOnTheFly) diff --git a/js/src/jsapi-tests/testPromise.cpp b/js/src/jsapi-tests/testPromise.cpp new file mode 100644 index 0000000000..2f9cf03048 --- /dev/null +++ b/js/src/jsapi-tests/testPromise.cpp @@ -0,0 +1,170 @@ +/* -*- 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.h" + +#include "jsapi-tests/tests.h" + +using namespace JS; + +static bool executor_called = false; + +static bool PromiseExecutor(JSContext* cx, unsigned argc, Value* vp) { +#ifdef DEBUG + CallArgs args = CallArgsFromVp(argc, vp); +#endif // DEBUG + MOZ_ASSERT(args.length() == 2); + MOZ_ASSERT(args[0].toObject().is<JSFunction>()); + MOZ_ASSERT(args[1].toObject().is<JSFunction>()); + + executor_called = true; + return true; +} + +static JSObject* CreatePromise(JSContext* cx) { + RootedFunction executor( + cx, JS_NewFunction(cx, PromiseExecutor, 2, 0, "executor")); + if (!executor) { + return nullptr; + } + return JS::NewPromiseObject(cx, executor); +} + +BEGIN_TEST(testPromise_NewPromise) { + RootedObject promise(cx, CreatePromise(cx)); + CHECK(promise); + CHECK(executor_called); + + return true; +} +END_TEST(testPromise_NewPromise) + +BEGIN_TEST(testPromise_GetPromiseState) { + RootedObject promise(cx, CreatePromise(cx)); + if (!promise) { + return false; + } + + CHECK(JS::GetPromiseState(promise) == JS::PromiseState::Pending); + + return true; +} +END_TEST(testPromise_GetPromiseState) + +BEGIN_TEST(testPromise_ResolvePromise) { + RootedObject promise(cx, CreatePromise(cx)); + if (!promise) { + return false; + } + + RootedValue result(cx); + result.setInt32(42); + JS::ResolvePromise(cx, promise, result); + + CHECK(JS::GetPromiseState(promise) == JS::PromiseState::Fulfilled); + + return true; +} +END_TEST(testPromise_ResolvePromise) + +BEGIN_TEST(testPromise_RejectPromise) { + RootedObject promise(cx, CreatePromise(cx)); + if (!promise) { + return false; + } + + RootedValue result(cx); + result.setInt32(42); + JS::RejectPromise(cx, promise, result); + + CHECK(JS::GetPromiseState(promise) == JS::PromiseState::Rejected); + + return true; +} +END_TEST(testPromise_RejectPromise) + +static bool thenHandler_called = false; + +static bool PromiseThenHandler(JSContext* cx, unsigned argc, Value* vp) { +#ifdef DEBUG + CallArgs args = CallArgsFromVp(argc, vp); +#endif // DEBUG + MOZ_ASSERT(args.length() == 1); + + thenHandler_called = true; + return true; +} + +static bool catchHandler_called = false; + +static bool PromiseCatchHandler(JSContext* cx, unsigned argc, Value* vp) { +#ifdef DEBUG + CallArgs args = CallArgsFromVp(argc, vp); +#endif // DEBUG + MOZ_ASSERT(args.length() == 1); + + catchHandler_called = true; + return true; +} + +BEGIN_TEST(testPromise_PromiseThen) { + RootedObject promise(cx, CreatePromise(cx)); + if (!promise) { + return false; + } + + RootedFunction thenHandler( + cx, JS_NewFunction(cx, PromiseThenHandler, 1, 0, "thenHandler")); + if (!thenHandler) { + return false; + } + RootedFunction catchHandler( + cx, JS_NewFunction(cx, PromiseCatchHandler, 1, 0, "catchHandler")); + if (!catchHandler) { + return false; + } + JS::AddPromiseReactions(cx, promise, thenHandler, catchHandler); + + RootedValue result(cx); + result.setInt32(42); + JS::ResolvePromise(cx, promise, result); + js::RunJobs(cx); + + CHECK(thenHandler_called); + + return true; +} +END_TEST(testPromise_PromiseThen) + +BEGIN_TEST(testPromise_PromiseCatch) { + RootedObject promise(cx, CreatePromise(cx)); + if (!promise) { + return false; + } + + RootedFunction thenHandler( + cx, JS_NewFunction(cx, PromiseThenHandler, 1, 0, "thenHandler")); + if (!thenHandler) { + return false; + } + RootedFunction catchHandler( + cx, JS_NewFunction(cx, PromiseCatchHandler, 1, 0, "catchHandler")); + if (!catchHandler) { + return false; + } + JS::AddPromiseReactions(cx, promise, thenHandler, catchHandler); + + RootedValue result(cx); + result.setInt32(42); + JS::RejectPromise(cx, promise, result); + js::RunJobs(cx); + + CHECK(catchHandler_called); + + return true; +} +END_TEST(testPromise_PromiseCatch) diff --git a/js/src/jsapi-tests/testPropCache.cpp b/js/src/jsapi-tests/testPropCache.cpp new file mode 100644 index 0000000000..90300e6090 --- /dev/null +++ b/js/src/jsapi-tests/testPropCache.cpp @@ -0,0 +1,47 @@ +/* -*- 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 "js/PropertyAndElement.h" // JS_DefineObject +#include "jsapi-tests/tests.h" + +static int g_counter; + +static bool CounterAdd(JSContext* cx, JS::HandleObject obj, JS::HandleId id, + JS::HandleValue v) { + g_counter++; + return true; +} + +static const JSClassOps CounterClassOps = { + CounterAdd, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + nullptr, // finalize + nullptr, // call + nullptr, // construct + nullptr, // trace +}; + +static const JSClass CounterClass = {"Counter", /* name */ + 0, /* flags */ + &CounterClassOps}; + +BEGIN_TEST(testPropCache_bug505798) { + g_counter = 0; + EXEC("var x = {};"); + CHECK(JS_DefineObject(cx, global, "y", &CounterClass, JSPROP_ENUMERATE)); + EXEC( + "var arr = [x, y];\n" + "for (var i = 0; i < arr.length; i++)\n" + " arr[i].p = 1;\n"); + CHECK_EQUAL(g_counter, 1); + return true; +} +END_TEST(testPropCache_bug505798) diff --git a/js/src/jsapi-tests/testPropertyKey.cpp b/js/src/jsapi-tests/testPropertyKey.cpp new file mode 100644 index 0000000000..aed210a658 --- /dev/null +++ b/js/src/jsapi-tests/testPropertyKey.cpp @@ -0,0 +1,69 @@ +/* -*- 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 "js/Id.h" // JS::PropertyKey, JS::GetWellKnownSymbolKey, JS::ToGetterId, JS::ToSetterId +#include "js/String.h" // JSString, JS_AtomizeString, JS_StringEqualsAscii +#include "js/Symbol.h" // JS::Symbol, JS::SymbolCode +#include "jsapi-tests/tests.h" + +BEGIN_TEST(testPropertyKeyGetterAndSetter) { + JS::Rooted<JSString*> str(cx, JS_AtomizeString(cx, "prop")); + CHECK(str); + + JS::Rooted<JS::PropertyKey> strId(cx, JS::PropertyKey::NonIntAtom(str)); + MOZ_ASSERT(strId.isString()); + + JS::Rooted<JS::PropertyKey> symId( + cx, JS::GetWellKnownSymbolKey(cx, JS::SymbolCode::iterator)); + MOZ_ASSERT(symId.isSymbol()); + + JS::Rooted<JS::PropertyKey> numId(cx, JS::PropertyKey::Int(42)); + MOZ_ASSERT(numId.isInt()); + + bool match; + + JS::Rooted<JS::PropertyKey> strGetterId(cx); + CHECK(JS::ToGetterId(cx, strId, &strGetterId)); + CHECK(strGetterId.isString()); + CHECK(JS_StringEqualsAscii(cx, strGetterId.toString(), "get prop", &match)); + CHECK(match); + + JS::Rooted<JS::PropertyKey> strSetterId(cx); + CHECK(JS::ToSetterId(cx, strId, &strSetterId)); + CHECK(strSetterId.isString()); + CHECK(JS_StringEqualsAscii(cx, strSetterId.toString(), "set prop", &match)); + CHECK(match); + + JS::Rooted<JS::PropertyKey> symGetterId(cx); + CHECK(JS::ToGetterId(cx, symId, &symGetterId)); + CHECK(symGetterId.isString()); + CHECK(JS_StringEqualsAscii(cx, symGetterId.toString(), + "get [Symbol.iterator]", &match)); + CHECK(match); + + JS::Rooted<JS::PropertyKey> symSetterId(cx); + CHECK(JS::ToSetterId(cx, symId, &symSetterId)); + CHECK(symSetterId.isString()); + CHECK(JS_StringEqualsAscii(cx, symSetterId.toString(), + "set [Symbol.iterator]", &match)); + CHECK(match); + + JS::Rooted<JS::PropertyKey> numGetterId(cx); + CHECK(JS::ToGetterId(cx, numId, &numGetterId)); + CHECK(numGetterId.isString()); + CHECK(JS_StringEqualsAscii(cx, numGetterId.toString(), "get 42", &match)); + CHECK(match); + + JS::Rooted<JS::PropertyKey> numSetterId(cx); + CHECK(JS::ToSetterId(cx, numId, &numSetterId)); + CHECK(numSetterId.isString()); + CHECK(JS_StringEqualsAscii(cx, numSetterId.toString(), "set 42", &match)); + CHECK(match); + + return true; +} +END_TEST(testPropertyKeyGetterAndSetter) diff --git a/js/src/jsapi-tests/testRecordTupleToSource.cpp b/js/src/jsapi-tests/testRecordTupleToSource.cpp new file mode 100644 index 0000000000..5fecf9100a --- /dev/null +++ b/js/src/jsapi-tests/testRecordTupleToSource.cpp @@ -0,0 +1,45 @@ +/* -*- 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/. */ + +#ifdef ENABLE_RECORD_TUPLE + +# include <string> + +# include "jsfriendapi.h" + +# include "jsapi-tests/tests.h" + +static const struct TestTuple { + const char* expr_string; + const char* expected; +} tests[] = { + {"#[]", "#[]"}, + {"#[1, 2, 3]", "#[1, 2, 3]"}, + {"#{}", "#{}"}, + {"#{\"a\": 1, \"b\": \"c\", \"c\": #[]}", + "#{\"a\": 1, \"b\": \"c\", \"c\": #[]}"}, + {"Object(#[])", "#[]"}, + {"Object(#[1, 2, 3])", "#[1, 2, 3]"}, + {"Object(#{})", "#{}"}, + {"Object(#{\"a\": 1, \"b\": \"c\", \"c\": #[]})", + "#{\"a\": 1, \"b\": \"c\", \"c\": #[]}"}, +}; + +BEGIN_TEST(testRecordTupleToSource) { + JS::Rooted<JS::Value> result(cx); + for (const auto& test : tests) { + EVAL(test.expr_string, &result); + JSLinearString* prettyPrinted = + JS_ASSERT_STRING_IS_LINEAR(JS_ValueToSource(cx, result)); + CHECK(JS_LinearStringEqualsAscii(prettyPrinted, test.expected)); + } + + return true; +} +END_TEST(testRecordTupleToSource) + +#endif diff --git a/js/src/jsapi-tests/testRegExp.cpp b/js/src/jsapi-tests/testRegExp.cpp new file mode 100644 index 0000000000..9a2b6f665f --- /dev/null +++ b/js/src/jsapi-tests/testRegExp.cpp @@ -0,0 +1,65 @@ +/* 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 "js/RegExp.h" +#include "js/RegExpFlags.h" +#include "jsapi-tests/tests.h" + +BEGIN_TEST(testObjectIsRegExp) { + JS::RootedValue val(cx); + + bool isRegExp; + + EVAL("new Object", &val); + JS::RootedObject obj(cx, val.toObjectOrNull()); + CHECK(JS::ObjectIsRegExp(cx, obj, &isRegExp)); + CHECK(!isRegExp); + + EVAL("/foopy/", &val); + obj = val.toObjectOrNull(); + CHECK(JS::ObjectIsRegExp(cx, obj, &isRegExp)); + CHECK(isRegExp); + + return true; +} +END_TEST(testObjectIsRegExp) + +BEGIN_TEST(testGetRegExpFlags) { + JS::RootedValue val(cx); + JS::RootedObject obj(cx); + + EVAL("/foopy/", &val); + obj = val.toObjectOrNull(); + CHECK_EQUAL(JS::GetRegExpFlags(cx, obj), + JS::RegExpFlags(JS::RegExpFlag::NoFlags)); + + EVAL("/foopy/g", &val); + obj = val.toObjectOrNull(); + CHECK_EQUAL(JS::GetRegExpFlags(cx, obj), + JS::RegExpFlags(JS::RegExpFlag::Global)); + + EVAL("/foopy/gi", &val); + obj = val.toObjectOrNull(); + CHECK_EQUAL( + JS::GetRegExpFlags(cx, obj), + JS::RegExpFlags(JS::RegExpFlag::Global | JS::RegExpFlag::IgnoreCase)); + + return true; +} +END_TEST(testGetRegExpFlags) + +BEGIN_TEST(testGetRegExpSource) { + JS::RootedValue val(cx); + JS::RootedObject obj(cx); + + EVAL("/foopy/", &val); + obj = val.toObjectOrNull(); + JSString* source = JS::GetRegExpSource(cx, obj); + CHECK(source); + CHECK(JS_LinearStringEqualsLiteral(JS_ASSERT_STRING_IS_LINEAR(source), + "foopy")); + + return true; +} +END_TEST(testGetRegExpSource) diff --git a/js/src/jsapi-tests/testResolveRecursion.cpp b/js/src/jsapi-tests/testResolveRecursion.cpp new file mode 100644 index 0000000000..44b5cd35a3 --- /dev/null +++ b/js/src/jsapi-tests/testResolveRecursion.cpp @@ -0,0 +1,184 @@ +/* -*- 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 "js/Object.h" // JS::GetReservedSlot, JS::SetReservedSlot +#include "js/PropertyAndElement.h" // JS_DefineProperty, JS_DefinePropertyById +#include "jsapi-tests/tests.h" + +/* + * Test that resolve hook recursion for the same object and property is + * prevented. + */ +BEGIN_TEST(testResolveRecursion) { + static const JSClassOps my_resolve_classOps = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + my_resolve, // resolve + nullptr, // mayResolve + nullptr, // finalize + nullptr, // call + nullptr, // construct + nullptr, // trace + }; + + static const JSClass my_resolve_class = { + "MyResolve", JSCLASS_HAS_RESERVED_SLOTS(SlotCount), &my_resolve_classOps}; + + obj1.init(cx, JS_NewObject(cx, &my_resolve_class)); + CHECK(obj1); + obj2.init(cx, JS_NewObject(cx, &my_resolve_class)); + CHECK(obj2); + JS::SetReservedSlot(obj1, TestSlot, JS::PrivateValue(this)); + JS::SetReservedSlot(obj2, TestSlot, JS::PrivateValue(this)); + + JS::RootedValue obj1Val(cx, JS::ObjectValue(*obj1)); + JS::RootedValue obj2Val(cx, JS::ObjectValue(*obj2)); + CHECK(JS_DefineProperty(cx, global, "obj1", obj1Val, 0)); + CHECK(JS_DefineProperty(cx, global, "obj2", obj2Val, 0)); + + resolveEntryCount = 0; + resolveExitCount = 0; + + /* Start the essence of the test via invoking the first resolve hook. */ + JS::RootedValue v(cx); + EVAL("obj1.x", &v); + CHECK(v.isFalse()); + CHECK_EQUAL(resolveEntryCount, 4); + CHECK_EQUAL(resolveExitCount, 4); + + obj1 = nullptr; + obj2 = nullptr; + return true; +} + +enum Slots { TestSlot, SlotCount }; + +JS::PersistentRootedObject obj1; +JS::PersistentRootedObject obj2; +int resolveEntryCount; +int resolveExitCount; + +struct AutoIncrCounters { + explicit AutoIncrCounters(cls_testResolveRecursion* t) : t(t) { + t->resolveEntryCount++; + } + + ~AutoIncrCounters() { t->resolveExitCount++; } + + cls_testResolveRecursion* t; +}; + +bool doResolve(JS::HandleObject obj, JS::HandleId id, bool* resolvedp) { + CHECK_EQUAL(resolveExitCount, 0); + AutoIncrCounters incr(this); + CHECK(obj == obj1 || obj == obj2); + + CHECK(id.isString()); + + JSLinearString* str = JS_EnsureLinearString(cx, id.toString()); + CHECK(str); + JS::RootedValue v(cx); + if (JS_LinearStringEqualsLiteral(str, "x")) { + if (obj == obj1) { + /* First resolve hook invocation. */ + CHECK_EQUAL(resolveEntryCount, 1); + EVAL("obj2.y = true", &v); + CHECK(v.isTrue()); + CHECK(JS_DefinePropertyById(cx, obj, id, JS::FalseHandleValue, + JSPROP_RESOLVING)); + *resolvedp = true; + return true; + } + if (obj == obj2) { + CHECK_EQUAL(resolveEntryCount, 4); + *resolvedp = false; + return true; + } + } else if (JS_LinearStringEqualsLiteral(str, "y")) { + if (obj == obj2) { + CHECK_EQUAL(resolveEntryCount, 2); + CHECK(JS_DefinePropertyById(cx, obj, id, JS::NullHandleValue, + JSPROP_RESOLVING)); + EVAL("obj1.x", &v); + CHECK(v.isUndefined()); + EVAL("obj1.y", &v); + CHECK(v.isInt32(0)); + *resolvedp = true; + return true; + } + if (obj == obj1) { + CHECK_EQUAL(resolveEntryCount, 3); + EVAL("obj1.x", &v); + CHECK(v.isUndefined()); + EVAL("obj1.y", &v); + CHECK(v.isUndefined()); + EVAL("obj2.y", &v); + CHECK(v.isNull()); + EVAL("obj2.x", &v); + CHECK(v.isUndefined()); + EVAL("obj1.y = 0", &v); + CHECK(v.isInt32(0)); + *resolvedp = true; + return true; + } + } + CHECK(false); + return false; +} + +static bool my_resolve(JSContext* cx, JS::HandleObject obj, JS::HandleId id, + bool* resolvedp) { + void* p = JS::GetReservedSlot(obj, TestSlot).toPrivate(); + return static_cast<cls_testResolveRecursion*>(p)->doResolve(obj, id, + resolvedp); +} +END_TEST(testResolveRecursion) + +/* + * Test that JS_InitStandardClasses does not cause resolve hooks to be called. + * + * (XPConnect apparently does have global classes, such as the one created by + * nsMessageManagerScriptExecutor::InitChildGlobalInternal(), that have resolve + * hooks which can call back into JS, and on which JS_InitStandardClasses is + * called. Calling back into JS in the middle of resolving `undefined` is bad.) + */ +BEGIN_TEST(testResolveRecursion_InitStandardClasses) { + CHECK(JS::InitRealmStandardClasses(cx)); + return true; +} + +const JSClass* getGlobalClass() override { + static const JSClassOps myGlobalClassOps = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + my_resolve, // resolve + nullptr, // mayResolve + nullptr, // finalize + nullptr, // call + nullptr, // construct + JS_GlobalObjectTraceHook, // trace + }; + + static const JSClass myGlobalClass = { + "testResolveRecursion_InitStandardClasses_myGlobalClass", + JSCLASS_GLOBAL_FLAGS, &myGlobalClassOps}; + + return &myGlobalClass; +} + +static bool my_resolve(JSContext* cx, JS::HandleObject obj, JS::HandleId id, + bool* resolvedp) { + MOZ_ASSERT_UNREACHABLE( + "resolve hook should not be called from InitStandardClasses"); + JS_ReportErrorASCII(cx, "FAIL"); + return false; +} +END_TEST(testResolveRecursion_InitStandardClasses) diff --git a/js/src/jsapi-tests/testResult.cpp b/js/src/jsapi-tests/testResult.cpp new file mode 100644 index 0000000000..5d8d0f88ea --- /dev/null +++ b/js/src/jsapi-tests/testResult.cpp @@ -0,0 +1,98 @@ +/* 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.h" // JS_NewPlainObject +#include "js/Result.h" // Error, Ok, Result +#include "js/String.h" // JS_NewStringCopyZ +#include "jsapi-tests/tests.h" + +JS::Result<> SimpleSuccess(JSContext*) { return JS::Ok(); } + +JS::Result<> SimpleFailure(JSContext*) { return JS::Result<>{JS::Error()}; } + +JS::Result<int> PODSuccess(JSContext*) { return 42; } + +JS::Result<int> PODFailure(JSContext*) { return JS::Result<int>{JS::Error()}; } + +JS::Result<JSObject*> ObjectSuccess(JSContext* cx) { + JS::RootedObject obj{cx, JS_NewPlainObject(cx)}; + MOZ_RELEASE_ASSERT(obj); + return obj.get(); +} + +JS::Result<JSObject*> ObjectFailure(JSContext*) { + return JS::Result<JSObject*>{JS::Error()}; +} + +JS::Result<JSString*> StringSuccess(JSContext* cx) { + JS::RootedString str{cx, JS_NewStringCopyZ(cx, "foo")}; + MOZ_RELEASE_ASSERT(str); + return str.get(); +} + +JS::Result<JSString*> StringFailure(JSContext*) { + return JS::Result<JSString*>{JS::Error()}; +} + +BEGIN_TEST(testResult_SimpleSuccess) { + JS::Result<> result = SimpleSuccess(cx); + CHECK(result.isOk()); + return true; +} +END_TEST(testResult_SimpleSuccess) + +BEGIN_TEST(testResult_SimpleFailure) { + JS::Result<> result = SimpleFailure(cx); + CHECK(result.isErr()); + return true; +} +END_TEST(testResult_SimpleFailure) + +BEGIN_TEST(testResult_PODSuccess) { + JS::Result<int> result = PODSuccess(cx); + CHECK(result.isOk()); + CHECK_EQUAL(result.unwrap(), 42); + CHECK_EQUAL(result.inspect(), 42); + return true; +} +END_TEST(testResult_PODSuccess) + +BEGIN_TEST(testResult_PODFailure) { + JS::Result<int> result = PODFailure(cx); + CHECK(result.isErr()); + return true; +} +END_TEST(testResult_PODFailure) + +BEGIN_TEST(testResult_ObjectSuccess) { + JS::Result<JSObject*> result = ObjectSuccess(cx); + CHECK(result.isOk()); + CHECK(result.inspect() != nullptr); + CHECK(result.unwrap() != nullptr); + return true; +} +END_TEST(testResult_ObjectSuccess) + +BEGIN_TEST(testResult_ObjectFailure) { + JS::Result<JSObject*> result = ObjectFailure(cx); + CHECK(result.isErr()); + return true; +} +END_TEST(testResult_ObjectFailure) + +BEGIN_TEST(testResult_StringSuccess) { + JS::Result<JSString*> result = StringSuccess(cx); + CHECK(result.isOk()); + CHECK(result.inspect() != nullptr); + CHECK(result.unwrap() != nullptr); + return true; +} +END_TEST(testResult_StringSuccess) + +BEGIN_TEST(testResult_StringFailure) { + JS::Result<JSString*> result = StringFailure(cx); + CHECK(result.isErr()); + return true; +} +END_TEST(testResult_StringFailure) diff --git a/js/src/jsapi-tests/testSABAccounting.cpp b/js/src/jsapi-tests/testSABAccounting.cpp new file mode 100644 index 0000000000..38607bc0aa --- /dev/null +++ b/js/src/jsapi-tests/testSABAccounting.cpp @@ -0,0 +1,31 @@ +#include "builtin/TestingFunctions.h" +#include "js/SharedArrayBuffer.h" +#include "jsapi-tests/tests.h" + +BEGIN_TEST(testSABAccounting) { + // Purge what we can + JS::PrepareForFullGC(cx); + NonIncrementalGC(cx, JS::GCOptions::Shrink, JS::GCReason::API); + + // Self-hosting and chrome code should not use SABs, or the point of this + // predicate is completely lost. + CHECK(!JS::ContainsSharedArrayBuffer(cx)); + + JS::RootedObject obj(cx), obj2(cx); + CHECK(obj = JS::NewSharedArrayBuffer(cx, 4096)); + CHECK(JS::ContainsSharedArrayBuffer(cx)); + CHECK(obj2 = JS::NewSharedArrayBuffer(cx, 4096)); + CHECK(JS::ContainsSharedArrayBuffer(cx)); + + // Discard those objects again. + obj = nullptr; + obj2 = nullptr; + JS::PrepareForFullGC(cx); + NonIncrementalGC(cx, JS::GCOptions::Shrink, JS::GCReason::API); + + // Should be back to base state. + CHECK(!JS::ContainsSharedArrayBuffer(cx)); + + return true; +} +END_TEST(testSABAccounting) diff --git a/js/src/jsapi-tests/testSameValue.cpp b/js/src/jsapi-tests/testSameValue.cpp new file mode 100644 index 0000000000..c2e12207f2 --- /dev/null +++ b/js/src/jsapi-tests/testSameValue.cpp @@ -0,0 +1,26 @@ +/* -*- 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 "js/Equality.h" // JS::SameValue +#include "jsapi-tests/tests.h" + +BEGIN_TEST(testSameValue) { + /* + * NB: passing a double that fits in an integer jsval is API misuse. As a + * matter of defense in depth, however, JS::SameValue should return the + * correct result comparing a positive-zero double to a negative-zero + * double, and this is believed to be the only way to make such a + * comparison possible. + */ + JS::RootedValue v1(cx, JS::DoubleValue(0.0)); + JS::RootedValue v2(cx, JS::DoubleValue(-0.0)); + bool same; + CHECK(JS::SameValue(cx, v1, v2, &same)); + CHECK(!same); + return true; +} +END_TEST(testSameValue) diff --git a/js/src/jsapi-tests/testSavedStacks.cpp b/js/src/jsapi-tests/testSavedStacks.cpp new file mode 100644 index 0000000000..b53fbd79ad --- /dev/null +++ b/js/src/jsapi-tests/testSavedStacks.cpp @@ -0,0 +1,398 @@ +/* -*- 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 "mozilla/Utf8.h" // mozilla::Utf8Unit + +#include "builtin/TestingFunctions.h" +#include "js/ColumnNumber.h" // JS::LimitedColumnNumberOneOrigin, JS::TaggedColumnNumberOneOrigin +#include "js/CompilationAndEvaluation.h" // JS::Evaluate +#include "js/Exception.h" +#include "js/SavedFrameAPI.h" +#include "js/SourceText.h" // JS::Source{Ownership,Text} +#include "js/Stack.h" +#include "jsapi-tests/tests.h" +#include "util/Text.h" +#include "vm/ArrayObject.h" +#include "vm/Realm.h" +#include "vm/SavedStacks.h" + +BEGIN_TEST(testSavedStacks_withNoStack) { + JS::Realm* realm = cx->realm(); + realm->setAllocationMetadataBuilder(&js::SavedStacks::metadataBuilder); + JS::RootedObject obj(cx, js::NewDenseEmptyArray(cx)); + realm->setAllocationMetadataBuilder(nullptr); + return true; +} +END_TEST(testSavedStacks_withNoStack) + +BEGIN_TEST(testSavedStacks_ApiDefaultValues) { + JS::Rooted<js::SavedFrame*> savedFrame(cx, nullptr); + + JSPrincipals* principals = cx->realm()->principals(); + + // Source + JS::RootedString str(cx); + JS::SavedFrameResult result = + JS::GetSavedFrameSource(cx, principals, savedFrame, &str); + CHECK(result == JS::SavedFrameResult::AccessDenied); + CHECK(str.get() == cx->runtime()->emptyString); + + // Line + uint32_t line = 123; + result = JS::GetSavedFrameLine(cx, principals, savedFrame, &line); + CHECK(result == JS::SavedFrameResult::AccessDenied); + CHECK(line == 0); + + // Column + JS::TaggedColumnNumberOneOrigin column(JS::LimitedColumnNumberOneOrigin(123)); + result = JS::GetSavedFrameColumn(cx, principals, savedFrame, &column); + CHECK(result == JS::SavedFrameResult::AccessDenied); + CHECK(column == JS::TaggedColumnNumberOneOrigin()); + + // Function display name + result = + JS::GetSavedFrameFunctionDisplayName(cx, principals, savedFrame, &str); + CHECK(result == JS::SavedFrameResult::AccessDenied); + CHECK(str.get() == nullptr); + + // Parent + JS::RootedObject parent(cx); + result = JS::GetSavedFrameParent(cx, principals, savedFrame, &parent); + CHECK(result == JS::SavedFrameResult::AccessDenied); + CHECK(parent.get() == nullptr); + + // Stack string + CHECK(JS::BuildStackString(cx, principals, savedFrame, &str)); + CHECK(str.get() == cx->runtime()->emptyString); + + return true; +} +END_TEST(testSavedStacks_ApiDefaultValues) + +BEGIN_TEST(testSavedStacks_RangeBasedForLoops) { + CHECK(js::DefineTestingFunctions(cx, global, false, false)); + + JS::RootedValue val(cx); + CHECK( + evaluate("(function one() { \n" // 1 + " return (function two() { \n" // 2 + " return (function three() { \n" // 3 + " return saveStack(); \n" // 4 + " }()); \n" // 5 + " }()); \n" // 6 + "}()); \n", // 7 + "filename.js", 1, &val)); + + CHECK(val.isObject()); + JS::RootedObject obj(cx, &val.toObject()); + + CHECK(obj->is<js::SavedFrame>()); + JS::Rooted<js::SavedFrame*> savedFrame(cx, &obj->as<js::SavedFrame>()); + + JS::Rooted<js::SavedFrame*> rf(cx, savedFrame); + for (JS::Handle<js::SavedFrame*> frame : + js::SavedFrame::RootedRange(cx, rf)) { + JS_GC(cx); + CHECK(frame == rf); + rf = rf->getParent(); + } + CHECK(rf == nullptr); + + // Stack string + static const char SpiderMonkeyStack[] = + "three@filename.js:4:14\n" + "two@filename.js:5:6\n" + "one@filename.js:6:4\n" + "@filename.js:7:2\n"; + static const char V8Stack[] = + " at three (filename.js:4:14)\n" + " at two (filename.js:5:6)\n" + " at one (filename.js:6:4)\n" + " at filename.js:7:2"; + struct { + js::StackFormat format; + const char* expected; + } expectations[] = {{js::StackFormat::Default, SpiderMonkeyStack}, + {js::StackFormat::SpiderMonkey, SpiderMonkeyStack}, + {js::StackFormat::V8, V8Stack}}; + auto CheckStacks = [&]() { + for (auto& expectation : expectations) { + JS::RootedString str(cx); + JSPrincipals* principals = cx->realm()->principals(); + CHECK(JS::BuildStackString(cx, principals, savedFrame, &str, 0, + expectation.format)); + JSLinearString* lin = str->ensureLinear(cx); + CHECK(lin); + CHECK(js::StringEqualsAscii(lin, expectation.expected)); + } + return true; + }; + + CHECK(CheckStacks()); + + js::SetStackFormat(cx, js::StackFormat::V8); + expectations[0].expected = V8Stack; + + CHECK(CheckStacks()); + + return true; +} +END_TEST(testSavedStacks_RangeBasedForLoops) + +BEGIN_TEST(testSavedStacks_ErrorStackSpiderMonkey) { + JS::RootedValue val(cx); + CHECK( + evaluate("(function one() { \n" // 1 + " return (function two() { \n" // 2 + " return (function three() { \n" // 3 + " return new Error('foo'); \n" // 4 + " }()); \n" // 5 + " }()); \n" // 6 + "}()).stack \n", // 7 + "filename.js", 1, &val)); + + CHECK(val.isString()); + JS::RootedString stack(cx, val.toString()); + + // Stack string + static const char SpiderMonkeyStack[] = + "three@filename.js:4:14\n" + "two@filename.js:5:6\n" + "one@filename.js:6:4\n" + "@filename.js:7:2\n"; + JSLinearString* lin = stack->ensureLinear(cx); + CHECK(lin); + CHECK(js::StringEqualsLiteral(lin, SpiderMonkeyStack)); + + return true; +} +END_TEST(testSavedStacks_ErrorStackSpiderMonkey) + +BEGIN_TEST(testSavedStacks_ErrorStackV8) { + js::SetStackFormat(cx, js::StackFormat::V8); + + JS::RootedValue val(cx); + CHECK( + evaluate("(function one() { \n" // 1 + " return (function two() { \n" // 2 + " return (function three() { \n" // 3 + " return new Error('foo'); \n" // 4 + " }()); \n" // 5 + " }()); \n" // 6 + "}()).stack \n", // 7 + "filename.js", 1, &val)); + + CHECK(val.isString()); + JS::RootedString stack(cx, val.toString()); + + // Stack string + static const char V8Stack[] = + "Error: foo\n" + " at three (filename.js:4:14)\n" + " at two (filename.js:5:6)\n" + " at one (filename.js:6:4)\n" + " at filename.js:7:2"; + JSLinearString* lin = stack->ensureLinear(cx); + CHECK(lin); + CHECK(js::StringEqualsLiteral(lin, V8Stack)); + + return true; +} +END_TEST(testSavedStacks_ErrorStackV8) + +BEGIN_TEST(testSavedStacks_selfHostedFrames) { + CHECK(js::DefineTestingFunctions(cx, global, false, false)); + + JS::RootedValue val(cx); + // 0 1 2 3 + // 0123456789012345678901234567890123456789 + CHECK( + evaluate("(function one() { \n" // 1 + " try { \n" // 2 + " [1].map(function two() { \n" // 3 + " throw saveStack(); \n" // 4 + " }); \n" // 5 + " } catch (stack) { \n" // 6 + " return stack; \n" // 7 + " } \n" // 8 + "}()) \n", // 9 + "filename.js", 1, &val)); + + CHECK(val.isObject()); + JS::RootedObject obj(cx, &val.toObject()); + + CHECK(obj->is<js::SavedFrame>()); + JS::Rooted<js::SavedFrame*> savedFrame(cx, &obj->as<js::SavedFrame>()); + + JS::Rooted<js::SavedFrame*> selfHostedFrame(cx, savedFrame->getParent()); + CHECK(selfHostedFrame->isSelfHosted(cx)); + + JSPrincipals* principals = cx->realm()->principals(); + + // Source + JS::RootedString str(cx); + JS::SavedFrameResult result = JS::GetSavedFrameSource( + cx, principals, selfHostedFrame, &str, JS::SavedFrameSelfHosted::Exclude); + CHECK(result == JS::SavedFrameResult::Ok); + JSLinearString* lin = str->ensureLinear(cx); + CHECK(lin); + CHECK(js::StringEqualsLiteral(lin, "filename.js")); + + // Source, including self-hosted frames + result = JS::GetSavedFrameSource(cx, principals, selfHostedFrame, &str, + JS::SavedFrameSelfHosted::Include); + CHECK(result == JS::SavedFrameResult::Ok); + lin = str->ensureLinear(cx); + CHECK(lin); + CHECK(js::StringEqualsLiteral(lin, "self-hosted")); + + // Line + uint32_t line = 123; + result = JS::GetSavedFrameLine(cx, principals, selfHostedFrame, &line, + JS::SavedFrameSelfHosted::Exclude); + CHECK(result == JS::SavedFrameResult::Ok); + CHECK_EQUAL(line, 3U); + + // Column + JS::TaggedColumnNumberOneOrigin column(JS::LimitedColumnNumberOneOrigin(123)); + result = JS::GetSavedFrameColumn(cx, principals, selfHostedFrame, &column, + JS::SavedFrameSelfHosted::Exclude); + CHECK(result == JS::SavedFrameResult::Ok); + CHECK_EQUAL(column.oneOriginValue(), 9U); + + // Function display name + result = JS::GetSavedFrameFunctionDisplayName( + cx, principals, selfHostedFrame, &str, JS::SavedFrameSelfHosted::Exclude); + CHECK(result == JS::SavedFrameResult::Ok); + lin = str->ensureLinear(cx); + CHECK(lin); + CHECK(js::StringEqualsLiteral(lin, "one")); + + // Parent + JS::RootedObject parent(cx); + result = JS::GetSavedFrameParent(cx, principals, savedFrame, &parent, + JS::SavedFrameSelfHosted::Exclude); + CHECK(result == JS::SavedFrameResult::Ok); + // JS::GetSavedFrameParent does this super funky and potentially unexpected + // thing where it doesn't return the next subsumed parent but any next + // parent. This so that callers can still get the "asyncParent" property + // which is only on the first frame of the async parent stack and that frame + // might not be subsumed by the caller. It is expected that callers will + // still interact with the frame through the JSAPI accessors, so this should + // be safe and should not leak privileged info to unprivileged + // callers. However, because of that, we don't test that the parent we get + // here is the selfHostedFrame's parent (because, as just explained, it + // isn't) and instead check that asking for the source property gives us the + // expected value. + result = JS::GetSavedFrameSource(cx, principals, parent, &str, + JS::SavedFrameSelfHosted::Exclude); + CHECK(result == JS::SavedFrameResult::Ok); + lin = str->ensureLinear(cx); + CHECK(lin); + CHECK(js::StringEqualsLiteral(lin, "filename.js")); + + return true; +} +END_TEST(testSavedStacks_selfHostedFrames) + +BEGIN_TEST(test_GetPendingExceptionStack) { + CHECK(js::DefineTestingFunctions(cx, global, false, false)); + + JSPrincipals* principals = cx->realm()->principals(); + + static const char sourceText[] = + // 1 2 3 + // 123456789012345678901234567890123456789 + "(function one() { \n" // 1 + " (function two() { \n" // 2 + " (function three() { \n" // 3 + " throw 5; \n" // 4 + " }()); \n" // 5 + " }()); \n" // 6 + "}()) \n"; // 7 + + JS::CompileOptions opts(cx); + opts.setFileAndLine("filename.js", 1U); + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + CHECK(srcBuf.init(cx, sourceText, js_strlen(sourceText), + JS::SourceOwnership::Borrowed)); + + JS::RootedValue val(cx); + bool ok = JS::Evaluate(cx, opts, srcBuf, &val); + + CHECK(!ok); + CHECK(JS_IsExceptionPending(cx)); + CHECK(val.isUndefined()); + + JS::ExceptionStack exnStack(cx); + CHECK(JS::GetPendingExceptionStack(cx, &exnStack)); + CHECK(exnStack.stack()); + CHECK(exnStack.stack()->is<js::SavedFrame>()); + JS::Rooted<js::SavedFrame*> savedFrameStack( + cx, &exnStack.stack()->as<js::SavedFrame>()); + + CHECK(exnStack.exception().isInt32()); + CHECK(exnStack.exception().toInt32() == 5); + + struct { + uint32_t line; + uint32_t column; + const char* source; + const char* functionDisplayName; + } expected[] = {{4, 7, "filename.js", "three"}, + {5, 6, "filename.js", "two"}, + {6, 4, "filename.js", "one"}, + {7, 2, "filename.js", nullptr}}; + + size_t i = 0; + for (JS::Handle<js::SavedFrame*> frame : + js::SavedFrame::RootedRange(cx, savedFrameStack)) { + CHECK(i < 4); + + // Line + uint32_t line = 123; + JS::SavedFrameResult result = JS::GetSavedFrameLine( + cx, principals, frame, &line, JS::SavedFrameSelfHosted::Exclude); + CHECK(result == JS::SavedFrameResult::Ok); + CHECK_EQUAL(line, expected[i].line); + + // Column + JS::TaggedColumnNumberOneOrigin column( + JS::LimitedColumnNumberOneOrigin(123)); + result = JS::GetSavedFrameColumn(cx, principals, frame, &column, + JS::SavedFrameSelfHosted::Exclude); + CHECK(result == JS::SavedFrameResult::Ok); + CHECK_EQUAL(column.oneOriginValue(), expected[i].column); + + // Source + JS::RootedString str(cx); + result = JS::GetSavedFrameSource(cx, principals, frame, &str, + JS::SavedFrameSelfHosted::Exclude); + CHECK(result == JS::SavedFrameResult::Ok); + JSLinearString* linear = str->ensureLinear(cx); + CHECK(linear); + CHECK(js::StringEqualsAscii(linear, expected[i].source)); + + // Function display name + result = JS::GetSavedFrameFunctionDisplayName( + cx, principals, frame, &str, JS::SavedFrameSelfHosted::Exclude); + CHECK(result == JS::SavedFrameResult::Ok); + if (auto expectedName = expected[i].functionDisplayName) { + CHECK(str); + linear = str->ensureLinear(cx); + CHECK(linear); + CHECK(js::StringEqualsAscii(linear, expectedName)); + } else { + CHECK(!str); + } + + i++; + } + + return true; +} +END_TEST(test_GetPendingExceptionStack) diff --git a/js/src/jsapi-tests/testScriptInfo.cpp b/js/src/jsapi-tests/testScriptInfo.cpp new file mode 100644 index 0000000000..780d6f632d --- /dev/null +++ b/js/src/jsapi-tests/testScriptInfo.cpp @@ -0,0 +1,56 @@ +/* -*- 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 "mozilla/Utf8.h" // mozilla::Utf8Unit + +#include "jsapi.h" + +#include "js/CompilationAndEvaluation.h" // JS::Compile +#include "js/SourceText.h" // JS::Source{Ownership,Text} +#include "jsapi-tests/tests.h" +#include "util/Text.h" + +static const char code[] = + "xx = 1; \n\ + \n\ +try { \n\ + debugger; \n\ + \n\ + xx += 1; \n\ +} \n\ +catch (e) \n\ +{ \n\ + xx += 1; \n\ +}\n\ +//@ sourceMappingURL=http://example.com/path/to/source-map.json"; + +BEGIN_TEST(testScriptInfo) { + unsigned startLine = 1000; + + JS::CompileOptions options(cx); + options.setFileAndLine(__FILE__, startLine); + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + CHECK(srcBuf.init(cx, code, js_strlen(code), JS::SourceOwnership::Borrowed)); + + JS::RootedScript script(cx, JS::Compile(cx, options, srcBuf)); + CHECK(script); + + CHECK_EQUAL(JS_GetScriptBaseLineNumber(cx, script), startLine); + CHECK(strcmp(JS_GetScriptFilename(script), __FILE__) == 0); + + return true; +} +static bool CharsMatch(const char16_t* p, const char* q) { + while (*q) { + if (*p++ != *q++) { + return false; + } + } + return true; +} +END_TEST(testScriptInfo) diff --git a/js/src/jsapi-tests/testScriptObject.cpp b/js/src/jsapi-tests/testScriptObject.cpp new file mode 100644 index 0000000000..10215e9c87 --- /dev/null +++ b/js/src/jsapi-tests/testScriptObject.cpp @@ -0,0 +1,214 @@ +/* -*- 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 "mozilla/Utf8.h" // mozilla::Utf8Unit + +#include "js/CompilationAndEvaluation.h" // JS::Compile{,Utf8{File,Path}} +#include "js/PropertyAndElement.h" // JS_SetProperty +#include "js/SourceText.h" // JS::Source{Ownership,Text} +#include "jsapi-tests/tests.h" + +struct ScriptObjectFixture : public JSAPIRuntimeTest { + static const int code_size; + static const char code[]; + static char16_t uc_code[]; + + ScriptObjectFixture() { + for (int i = 0; i < code_size; i++) { + uc_code[i] = code[i]; + } + } + + bool tryScript(JS::HandleScript script) { + CHECK(script); + + JS_GC(cx); + + /* After a garbage collection, the script should still work. */ + JS::RootedValue result(cx); + CHECK(JS_ExecuteScript(cx, script, &result)); + + return true; + } +}; + +const char ScriptObjectFixture::code[] = + "(function(a, b){return a+' '+b;}('hello', 'world'))"; +const int ScriptObjectFixture::code_size = + sizeof(ScriptObjectFixture::code) - 1; +char16_t ScriptObjectFixture::uc_code[ScriptObjectFixture::code_size]; + +BEGIN_FIXTURE_TEST(ScriptObjectFixture, bug438633_CompileScript) { + JS::CompileOptions options(cx); + options.setFileAndLine(__FILE__, __LINE__); + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + CHECK(srcBuf.init(cx, code, code_size, JS::SourceOwnership::Borrowed)); + + JS::RootedScript script(cx, JS::Compile(cx, options, srcBuf)); + CHECK(script); + + return tryScript(script); +} +END_FIXTURE_TEST(ScriptObjectFixture, bug438633_CompileScript) + +BEGIN_FIXTURE_TEST(ScriptObjectFixture, bug438633_CompileScript_empty) { + JS::CompileOptions options(cx); + options.setFileAndLine(__FILE__, __LINE__); + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + CHECK(srcBuf.init(cx, "", 0, JS::SourceOwnership::Borrowed)); + + JS::RootedScript script(cx, JS::Compile(cx, options, srcBuf)); + CHECK(script); + + return tryScript(script); +} +END_FIXTURE_TEST(ScriptObjectFixture, bug438633_CompileScript_empty) + +BEGIN_FIXTURE_TEST(ScriptObjectFixture, bug438633_CompileScriptForPrincipals) { + JS::CompileOptions options(cx); + options.setFileAndLine(__FILE__, __LINE__); + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + CHECK(srcBuf.init(cx, code, code_size, JS::SourceOwnership::Borrowed)); + + JS::RootedScript script(cx, JS::Compile(cx, options, srcBuf)); + + return tryScript(script); +} +END_FIXTURE_TEST(ScriptObjectFixture, bug438633_CompileScriptForPrincipals) + +BEGIN_FIXTURE_TEST(ScriptObjectFixture, bug438633_JS_CompileUCScript) { + JS::CompileOptions options(cx); + options.setFileAndLine(__FILE__, __LINE__); + + JS::SourceText<char16_t> srcBuf; + CHECK(srcBuf.init(cx, uc_code, code_size, JS::SourceOwnership::Borrowed)); + + JS::RootedScript script(cx, JS::Compile(cx, options, srcBuf)); + CHECK(script); + + return tryScript(script); +} +END_FIXTURE_TEST(ScriptObjectFixture, bug438633_JS_CompileUCScript) + +BEGIN_FIXTURE_TEST(ScriptObjectFixture, bug438633_JS_CompileUCScript_empty) { + JS::CompileOptions options(cx); + options.setFileAndLine(__FILE__, __LINE__); + + JS::SourceText<char16_t> srcBuf; + CHECK(srcBuf.init(cx, uc_code, 0, JS::SourceOwnership::Borrowed)); + + JS::RootedScript script(cx, JS::Compile(cx, options, srcBuf)); + CHECK(script); + + return tryScript(script); +} +END_FIXTURE_TEST(ScriptObjectFixture, bug438633_JS_CompileUCScript_empty) + +BEGIN_FIXTURE_TEST(ScriptObjectFixture, + bug438633_JS_CompileUCScriptForPrincipals) { + JS::CompileOptions options(cx); + options.setFileAndLine(__FILE__, __LINE__); + + JS::SourceText<char16_t> srcBuf; + CHECK(srcBuf.init(cx, uc_code, code_size, JS::SourceOwnership::Borrowed)); + + JS::RootedScript script(cx, JS::Compile(cx, options, srcBuf)); + CHECK(script); + + return tryScript(script); +} +END_FIXTURE_TEST(ScriptObjectFixture, bug438633_JS_CompileUCScriptForPrincipals) + +BEGIN_FIXTURE_TEST(ScriptObjectFixture, bug438633_JS_CompileFile) { + TempFile tempScript; + static const char script_filename[] = "temp-bug438633_JS_CompileFile"; + FILE* script_stream = tempScript.open(script_filename); + CHECK(fputs(code, script_stream) != EOF); + tempScript.close(); + + JS::CompileOptions options(cx); + options.setFileAndLine(script_filename, 1); + + JS::RootedScript script(cx, + JS::CompileUtf8Path(cx, options, script_filename)); + CHECK(script); + + tempScript.remove(); + return tryScript(script); +} +END_FIXTURE_TEST(ScriptObjectFixture, bug438633_JS_CompileFile) + +BEGIN_FIXTURE_TEST(ScriptObjectFixture, bug438633_JS_CompileFile_empty) { + TempFile tempScript; + static const char script_filename[] = "temp-bug438633_JS_CompileFile_empty"; + tempScript.open(script_filename); + tempScript.close(); + + JS::CompileOptions options(cx); + options.setFileAndLine(script_filename, 1); + + JS::RootedScript script(cx, + JS::CompileUtf8Path(cx, options, script_filename)); + CHECK(script); + + tempScript.remove(); + return tryScript(script); +} +END_FIXTURE_TEST(ScriptObjectFixture, bug438633_JS_CompileFile_empty) + +BEGIN_FIXTURE_TEST(ScriptObjectFixture, bug438633_JS_CompileFileHandle) { + TempFile tempScript; + FILE* script_stream = tempScript.open("temp-bug438633_JS_CompileFileHandle"); + CHECK(fputs(code, script_stream) != EOF); + CHECK(fseek(script_stream, 0, SEEK_SET) != EOF); + + JS::CompileOptions options(cx); + options.setFileAndLine("temporary file", 1); + + JS::RootedScript script(cx, JS::CompileUtf8File(cx, options, script_stream)); + CHECK(script); + + return tryScript(script); +} +END_FIXTURE_TEST(ScriptObjectFixture, bug438633_JS_CompileFileHandle) + +BEGIN_FIXTURE_TEST(ScriptObjectFixture, bug438633_JS_CompileFileHandle_empty) { + TempFile tempScript; + FILE* script_stream = + tempScript.open("temp-bug438633_JS_CompileFileHandle_empty"); + + JS::CompileOptions options(cx); + options.setFileAndLine("empty temporary file", 1); + + JS::RootedScript script(cx, JS::CompileUtf8File(cx, options, script_stream)); + CHECK(script); + + return tryScript(script); +} +END_FIXTURE_TEST(ScriptObjectFixture, bug438633_JS_CompileFileHandle_empty) + +BEGIN_FIXTURE_TEST(ScriptObjectFixture, + bug438633_JS_CompileFileHandleForPrincipals) { + TempFile tempScript; + FILE* script_stream = + tempScript.open("temp-bug438633_JS_CompileFileHandleForPrincipals"); + CHECK(fputs(code, script_stream) != EOF); + CHECK(fseek(script_stream, 0, SEEK_SET) != EOF); + + JS::CompileOptions options(cx); + options.setFileAndLine("temporary file", 1); + + JS::RootedScript script(cx, JS::CompileUtf8File(cx, options, script_stream)); + CHECK(script); + + return tryScript(script); +} +END_FIXTURE_TEST(ScriptObjectFixture, + bug438633_JS_CompileFileHandleForPrincipals) diff --git a/js/src/jsapi-tests/testScriptSourceCompression.cpp b/js/src/jsapi-tests/testScriptSourceCompression.cpp new file mode 100644 index 0000000000..479a60b827 --- /dev/null +++ b/js/src/jsapi-tests/testScriptSourceCompression.cpp @@ -0,0 +1,498 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=8 sts=4 et sw=4 tw=99: + */ +/* 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 "mozilla/Assertions.h" // MOZ_RELEASE_ASSERT +#include "mozilla/RefPtr.h" // RefPtr +#include "mozilla/Utf8.h" // mozilla::Utf8Unit + +#include <algorithm> // std::all_of, std::equal, std::move, std::transform +#include <iterator> // std::size +#include <memory> // std::uninitialized_fill_n +#include <stddef.h> // size_t +#include <stdint.h> // uint32_t + +#include "jsapi.h" // JS_EnsureLinearString, JS_GC, JS_Get{Latin1,TwoByte}LinearStringChars, JS_GetStringLength, JS_ValueToFunction +#include "jstypes.h" // JS_PUBLIC_API + +#include "gc/GC.h" // js::gc::FinishGC +#include "js/CompilationAndEvaluation.h" // JS::Evaluate +#include "js/CompileOptions.h" // JS::CompileOptions, JS::InstantiateOptions +#include "js/Conversions.h" // JS::ToString +#include "js/experimental/JSStencil.h" // JS::Stencil, JS::InstantiateGlobalStencil +#include "js/MemoryFunctions.h" // JS_malloc +#include "js/RootingAPI.h" // JS::MutableHandle, JS::Rooted +#include "js/SourceText.h" // JS::SourceOwnership, JS::SourceText +#include "js/String.h" // JS::GetLatin1LinearStringChars, JS::GetTwoByteLinearStringChars, JS::StringHasLatin1Chars +#include "js/UniquePtr.h" // js::UniquePtr +#include "js/Utility.h" // JS::FreePolicy +#include "js/Value.h" // JS::NullValue, JS::ObjectValue, JS::Value +#include "jsapi-tests/tests.h" +#include "util/Text.h" // js_strlen +#include "vm/Compression.h" // js::Compressor::CHUNK_SIZE +#include "vm/HelperThreads.h" // js::RunPendingSourceCompressions +#include "vm/JSFunction.h" // JSFunction::getOrCreateScript +#include "vm/JSScript.h" // JSScript, js::ScriptSource::MinimumCompressibleLength, js::SynchronouslyCompressSource +#include "vm/Monitor.h" // js::Monitor, js::AutoLockMonitor + +using mozilla::Utf8Unit; + +struct JS_PUBLIC_API JSContext; +class JS_PUBLIC_API JSString; + +template <typename Unit> +using Source = js::UniquePtr<Unit[], JS::FreePolicy>; + +constexpr size_t ChunkSize = js::Compressor::CHUNK_SIZE; +constexpr size_t MinimumCompressibleLength = + js::ScriptSource::MinimumCompressibleLength; + +// Don't use ' ' to spread stuff across lines. +constexpr char FillerWhitespace = '\n'; + +template <typename Unit> +static Source<Unit> MakeSourceAllWhitespace(JSContext* cx, size_t len) { + static_assert(ChunkSize % sizeof(Unit) == 0, + "chunk size presumed to be a multiple of char size"); + + Source<Unit> source( + reinterpret_cast<Unit*>(JS_malloc(cx, len * sizeof(Unit)))); + if (source) { + std::uninitialized_fill_n(source.get(), len, FillerWhitespace); + } + return source; +} + +template <typename Unit> +static JSFunction* EvaluateChars(JSContext* cx, Source<Unit> chars, size_t len, + char functionName, const char* func) { + JS::CompileOptions options(cx); + options.setFileAndLine(func, 1); + + // Evaluate the provided source text, containing a function named + // |functionName|. + JS::SourceText<Unit> sourceText; + if (!sourceText.init(cx, std::move(chars), len)) { + return nullptr; + } + + { + JS::Rooted<JS::Value> dummy(cx); + if (!JS::Evaluate(cx, options, sourceText, &dummy)) { + return nullptr; + } + } + + // Evaluate the name of that function. + JS::Rooted<JS::Value> rval(cx); + const char16_t name[] = {char16_t(functionName)}; + JS::SourceText<char16_t> srcbuf; + if (!srcbuf.init(cx, name, std::size(name), JS::SourceOwnership::Borrowed)) { + return nullptr; + } + if (!JS::Evaluate(cx, options, srcbuf, &rval)) { + return nullptr; + } + + // Return the function. + MOZ_RELEASE_ASSERT(rval.isObject()); + return JS_ValueToFunction(cx, rval); +} + +static void CompressSourceSync(JS::Handle<JSFunction*> fun, JSContext* cx) { + JS::Rooted<JSScript*> script(cx, JSFunction::getOrCreateScript(cx, fun)); + MOZ_RELEASE_ASSERT(script); + MOZ_RELEASE_ASSERT(script->scriptSource()->hasSourceText()); + + MOZ_RELEASE_ASSERT(js::SynchronouslyCompressSource(cx, script)); + + MOZ_RELEASE_ASSERT(script->scriptSource()->hasCompressedSource()); +} + +static constexpr char FunctionStart[] = "function @() {"; +constexpr size_t FunctionStartLength = js_strlen(FunctionStart); +constexpr size_t FunctionNameOffset = 9; + +static_assert(FunctionStart[FunctionNameOffset] == '@', + "offset must correctly point at the function name location"); + +static constexpr char FunctionEnd[] = "return 42; }"; +constexpr size_t FunctionEndLength = js_strlen(FunctionEnd); + +template <typename Unit> +static void WriteFunctionOfSizeAtOffset(Source<Unit>& source, + size_t usableSourceLen, + char functionName, + size_t functionLength, size_t offset) { + MOZ_RELEASE_ASSERT(functionLength >= MinimumCompressibleLength, + "function must be a certain size to be compressed"); + MOZ_RELEASE_ASSERT(offset <= usableSourceLen, + "offset must not exceed usable source"); + MOZ_RELEASE_ASSERT(functionLength <= usableSourceLen, + "function must fit in usable source"); + MOZ_RELEASE_ASSERT(offset <= usableSourceLen - functionLength, + "function must not extend past usable source"); + + // Assigning |char| to |char16_t| is permitted, but we deliberately require a + // cast to assign |char| to |Utf8Unit|. |std::copy_n| would handle the first + // case, but the required transformation for UTF-8 demands |std::transform|. + auto TransformToUnit = [](char c) { return Unit(c); }; + + // Fill in the function start. + std::transform(FunctionStart, FunctionStart + FunctionStartLength, + &source[offset], TransformToUnit); + source[offset + FunctionNameOffset] = Unit(functionName); + + // Fill in the function end. + std::transform(FunctionEnd, FunctionEnd + FunctionEndLength, + &source[offset + functionLength - FunctionEndLength], + TransformToUnit); +} + +static JSString* DecompressSource(JSContext* cx, JS::Handle<JSFunction*> fun) { + JS::Rooted<JS::Value> fval(cx, JS::ObjectValue(*JS_GetFunctionObject(fun))); + return JS::ToString(cx, fval); +} + +static bool IsExpectedFunctionString(JS::Handle<JSString*> str, + char functionName, JSContext* cx) { + JSLinearString* lstr = JS_EnsureLinearString(cx, str); + MOZ_RELEASE_ASSERT(lstr); + + size_t len = JS_GetStringLength(str); + if (len < FunctionStartLength || len < FunctionEndLength) { + return false; + } + + JS::AutoAssertNoGC nogc(cx); + + auto CheckContents = [functionName, len](const auto* chars) { + // Check the function in parts: + // + // * "function " + // * "A" + // * "() {" + // * "\n...\n" + // * "return 42; }" + return std::equal(chars, chars + FunctionNameOffset, FunctionStart) && + chars[FunctionNameOffset] == functionName && + std::equal(chars + FunctionNameOffset + 1, + chars + FunctionStartLength, + FunctionStart + FunctionNameOffset + 1) && + std::all_of(chars + FunctionStartLength, + chars + len - FunctionEndLength, + [](auto c) { return c == FillerWhitespace; }) && + std::equal(chars + len - FunctionEndLength, chars + len, + FunctionEnd); + }; + + bool hasExpectedContents; + if (JS::StringHasLatin1Chars(str)) { + const JS::Latin1Char* chars = JS::GetLatin1LinearStringChars(nogc, lstr); + hasExpectedContents = CheckContents(chars); + } else { + const char16_t* chars = JS::GetTwoByteLinearStringChars(nogc, lstr); + hasExpectedContents = CheckContents(chars); + } + + return hasExpectedContents; +} + +BEGIN_TEST(testScriptSourceCompression_inOneChunk) { + CHECK(run<char16_t>()); + CHECK(run<Utf8Unit>()); + return true; +} + +template <typename Unit> +bool run() { + constexpr size_t len = MinimumCompressibleLength + 55; + auto source = MakeSourceAllWhitespace<Unit>(cx, len); + CHECK(source); + + // Write out a 'b' or 'c' function that is long enough to be compressed, + // that starts after source start and ends before source end. + constexpr char FunctionName = 'a' + sizeof(Unit); + WriteFunctionOfSizeAtOffset(source, len, FunctionName, + MinimumCompressibleLength, + len - MinimumCompressibleLength); + + JS::Rooted<JSFunction*> fun(cx); + fun = EvaluateChars(cx, std::move(source), len, FunctionName, __FUNCTION__); + CHECK(fun); + + CompressSourceSync(fun, cx); + + JS::Rooted<JSString*> str(cx, DecompressSource(cx, fun)); + CHECK(str); + CHECK(IsExpectedFunctionString(str, FunctionName, cx)); + + return true; +} +END_TEST(testScriptSourceCompression_inOneChunk) + +BEGIN_TEST(testScriptSourceCompression_endsAtBoundaryInOneChunk) { + CHECK(run<char16_t>()); + CHECK(run<Utf8Unit>()); + return true; +} + +template <typename Unit> +bool run() { + constexpr size_t len = ChunkSize / sizeof(Unit); + auto source = MakeSourceAllWhitespace<Unit>(cx, len); + CHECK(source); + + // Write out a 'd' or 'e' function that is long enough to be compressed, + // that (for no particular reason) starts after source start and ends + // before usable source end. + constexpr char FunctionName = 'c' + sizeof(Unit); + WriteFunctionOfSizeAtOffset(source, len, FunctionName, + MinimumCompressibleLength, + len - MinimumCompressibleLength); + + JS::Rooted<JSFunction*> fun(cx); + fun = EvaluateChars(cx, std::move(source), len, FunctionName, __FUNCTION__); + CHECK(fun); + + CompressSourceSync(fun, cx); + + JS::Rooted<JSString*> str(cx, DecompressSource(cx, fun)); + CHECK(str); + CHECK(IsExpectedFunctionString(str, FunctionName, cx)); + + return true; +} +END_TEST(testScriptSourceCompression_endsAtBoundaryInOneChunk) + +BEGIN_TEST(testScriptSourceCompression_isExactChunk) { + CHECK(run<char16_t>()); + CHECK(run<Utf8Unit>()); + return true; +} + +template <typename Unit> +bool run() { + constexpr size_t len = ChunkSize / sizeof(Unit); + auto source = MakeSourceAllWhitespace<Unit>(cx, len); + CHECK(source); + + // Write out a 'f' or 'g' function that occupies the entire source (and + // entire chunk, too). + constexpr char FunctionName = 'e' + sizeof(Unit); + WriteFunctionOfSizeAtOffset(source, len, FunctionName, len, 0); + + JS::Rooted<JSFunction*> fun(cx); + fun = EvaluateChars(cx, std::move(source), len, FunctionName, __FUNCTION__); + CHECK(fun); + + CompressSourceSync(fun, cx); + + JS::Rooted<JSString*> str(cx, DecompressSource(cx, fun)); + CHECK(str); + CHECK(IsExpectedFunctionString(str, FunctionName, cx)); + + return true; +} +END_TEST(testScriptSourceCompression_isExactChunk) + +BEGIN_TEST(testScriptSourceCompression_crossesChunkBoundary) { + CHECK(run<char16_t>()); + CHECK(run<Utf8Unit>()); + return true; +} + +template <typename Unit> +bool run() { + constexpr size_t len = ChunkSize / sizeof(Unit) + 293; + auto source = MakeSourceAllWhitespace<Unit>(cx, len); + CHECK(source); + + // This function crosses a chunk boundary but does not end at one. + constexpr size_t FunctionSize = 177 + ChunkSize / sizeof(Unit); + + // Write out a 'h' or 'i' function. + constexpr char FunctionName = 'g' + sizeof(Unit); + WriteFunctionOfSizeAtOffset(source, len, FunctionName, FunctionSize, 37); + + JS::Rooted<JSFunction*> fun(cx); + fun = EvaluateChars(cx, std::move(source), len, FunctionName, __FUNCTION__); + CHECK(fun); + + CompressSourceSync(fun, cx); + + JS::Rooted<JSString*> str(cx, DecompressSource(cx, fun)); + CHECK(str); + CHECK(IsExpectedFunctionString(str, FunctionName, cx)); + + return true; +} +END_TEST(testScriptSourceCompression_crossesChunkBoundary) + +BEGIN_TEST(testScriptSourceCompression_crossesChunkBoundary_endsAtBoundary) { + CHECK(run<char16_t>()); + CHECK(run<Utf8Unit>()); + return true; +} + +template <typename Unit> +bool run() { + // Exactly two chunks. + constexpr size_t len = (2 * ChunkSize) / sizeof(Unit); + auto source = MakeSourceAllWhitespace<Unit>(cx, len); + CHECK(source); + + // This function crosses a chunk boundary, and it ends exactly at the end + // of both the second chunk and the full source. + constexpr size_t FunctionSize = 1 + ChunkSize / sizeof(Unit); + + // Write out a 'j' or 'k' function. + constexpr char FunctionName = 'i' + sizeof(Unit); + WriteFunctionOfSizeAtOffset(source, len, FunctionName, FunctionSize, + len - FunctionSize); + + JS::Rooted<JSFunction*> fun(cx); + fun = EvaluateChars(cx, std::move(source), len, FunctionName, __FUNCTION__); + CHECK(fun); + + CompressSourceSync(fun, cx); + + JS::Rooted<JSString*> str(cx, DecompressSource(cx, fun)); + CHECK(str); + CHECK(IsExpectedFunctionString(str, FunctionName, cx)); + + return true; +} +END_TEST(testScriptSourceCompression_crossesChunkBoundary_endsAtBoundary) + +BEGIN_TEST(testScriptSourceCompression_containsWholeChunk) { + CHECK(run<char16_t>()); + CHECK(run<Utf8Unit>()); + return true; +} + +template <typename Unit> +bool run() { + constexpr size_t len = (2 * ChunkSize) / sizeof(Unit) + 17; + auto source = MakeSourceAllWhitespace<Unit>(cx, len); + CHECK(source); + + // This function crosses two chunk boundaries and begins/ends in the middle + // of chunk boundaries. + constexpr size_t FunctionSize = 2 + ChunkSize / sizeof(Unit); + + // Write out a 'l' or 'm' function. + constexpr char FunctionName = 'k' + sizeof(Unit); + WriteFunctionOfSizeAtOffset(source, len, FunctionName, FunctionSize, + ChunkSize / sizeof(Unit) - 1); + + JS::Rooted<JSFunction*> fun(cx); + fun = EvaluateChars(cx, std::move(source), len, FunctionName, __FUNCTION__); + CHECK(fun); + + CompressSourceSync(fun, cx); + + JS::Rooted<JSString*> str(cx, DecompressSource(cx, fun)); + CHECK(str); + CHECK(IsExpectedFunctionString(str, FunctionName, cx)); + + return true; +} +END_TEST(testScriptSourceCompression_containsWholeChunk) + +BEGIN_TEST(testScriptSourceCompression_containsWholeChunk_endsAtBoundary) { + CHECK(run<char16_t>()); + CHECK(run<Utf8Unit>()); + return true; +} + +template <typename Unit> +bool run() { + // Exactly three chunks. + constexpr size_t len = (3 * ChunkSize) / sizeof(Unit); + auto source = MakeSourceAllWhitespace<Unit>(cx, len); + CHECK(source); + + // This function crosses two chunk boundaries and ends at a chunk boundary. + constexpr size_t FunctionSize = 1 + (2 * ChunkSize) / sizeof(Unit); + + // Write out a 'n' or 'o' function. + constexpr char FunctionName = 'm' + sizeof(Unit); + WriteFunctionOfSizeAtOffset(source, len, FunctionName, FunctionSize, + ChunkSize / sizeof(Unit) - 1); + + JS::Rooted<JSFunction*> fun(cx); + fun = EvaluateChars(cx, std::move(source), len, FunctionName, __FUNCTION__); + CHECK(fun); + + CompressSourceSync(fun, cx); + + JS::Rooted<JSString*> str(cx, DecompressSource(cx, fun)); + CHECK(str); + CHECK(IsExpectedFunctionString(str, FunctionName, cx)); + + return true; +} +END_TEST(testScriptSourceCompression_containsWholeChunk_endsAtBoundary) + +BEGIN_TEST(testScriptSourceCompression_spansMultipleMiddleChunks) { + CHECK(run<char16_t>()); + CHECK(run<Utf8Unit>()); + return true; +} + +template <typename Unit> +bool run() { + // Four chunks. + constexpr size_t len = (4 * ChunkSize) / sizeof(Unit); + auto source = MakeSourceAllWhitespace<Unit>(cx, len); + CHECK(source); + + // This function spans the two middle chunks and further extends one + // character to each side. + constexpr size_t FunctionSize = 2 + (2 * ChunkSize) / sizeof(Unit); + + // Write out a 'p' or 'q' function. + constexpr char FunctionName = 'o' + sizeof(Unit); + WriteFunctionOfSizeAtOffset(source, len, FunctionName, FunctionSize, + ChunkSize / sizeof(Unit) - 1); + + JS::Rooted<JSFunction*> fun(cx); + fun = EvaluateChars(cx, std::move(source), len, FunctionName, __FUNCTION__); + CHECK(fun); + + CompressSourceSync(fun, cx); + + JS::Rooted<JSString*> str(cx, DecompressSource(cx, fun)); + CHECK(str); + CHECK(IsExpectedFunctionString(str, FunctionName, cx)); + + return true; +} +END_TEST(testScriptSourceCompression_spansMultipleMiddleChunks) + +BEGIN_TEST(testScriptSourceCompression_automatic) { + constexpr size_t len = MinimumCompressibleLength + 55; + auto chars = MakeSourceAllWhitespace<char16_t>(cx, len); + CHECK(chars); + + JS::SourceText<char16_t> source; + CHECK(source.init(cx, std::move(chars), len)); + + JS::CompileOptions options(cx); + JS::Rooted<JSScript*> script(cx, JS::Compile(cx, options, source)); + CHECK(script); + + // Check that source compression was triggered by the compile. If the + // off-thread source compression system is globally disabled, the source will + // remain uncompressed. + js::RunPendingSourceCompressions(cx->runtime()); + bool expected = js::IsOffThreadSourceCompressionEnabled(); + CHECK(script->scriptSource()->hasCompressedSource() == expected); + + return true; +} +END_TEST(testScriptSourceCompression_automatic) diff --git a/js/src/jsapi-tests/testSetProperty.cpp b/js/src/jsapi-tests/testSetProperty.cpp new file mode 100644 index 0000000000..dfc176778a --- /dev/null +++ b/js/src/jsapi-tests/testSetProperty.cpp @@ -0,0 +1,54 @@ +/* -*- 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 "js/Object.h" // JS::GetClass +#include "js/PropertyAndElement.h" // JS_DefineProperty +#include "jsapi-tests/tests.h" + +BEGIN_TEST(testSetProperty_InheritedGlobalSetter) { + // This is a JSAPI test because jsapi-test globals can be set up to not have + // a resolve hook and therefore can use the property cache in some cases + // where the shell can't. + MOZ_RELEASE_ASSERT(!JS::GetClass(global)->getResolve()); + + CHECK(JS::InitRealmStandardClasses(cx)); + + CHECK(JS_DefineProperty(cx, global, "HOTLOOP", 8, 0)); + EXEC( + "var n = 0;\n" + "var global = this;\n" + "function f() { n++; }\n" + "Object.defineProperty(Object.prototype, 'x', {set: f});\n" + "for (var i = 0; i < HOTLOOP; i++)\n" + " global.x = i;\n"); + EXEC( + "if (n != HOTLOOP)\n" + " throw 'FAIL';\n"); + return true; +} + +const JSClass* getGlobalClass(void) override { + static const JSClassOps noResolveGlobalClassOps = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + nullptr, // finalize + nullptr, // call + nullptr, // construct + JS_GlobalObjectTraceHook, // trace + }; + + static const JSClass noResolveGlobalClass = { + "testSetProperty_InheritedGlobalSetter_noResolveGlobalClass", + JSCLASS_GLOBAL_FLAGS, &noResolveGlobalClassOps}; + + return &noResolveGlobalClass; +} +END_TEST(testSetProperty_InheritedGlobalSetter) diff --git a/js/src/jsapi-tests/testSetPropertyIgnoringNamedGetter.cpp b/js/src/jsapi-tests/testSetPropertyIgnoringNamedGetter.cpp new file mode 100644 index 0000000000..cc4ef9cdc8 --- /dev/null +++ b/js/src/jsapi-tests/testSetPropertyIgnoringNamedGetter.cpp @@ -0,0 +1,73 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + */ + +#include "jsfriendapi.h" + +#include "js/PropertyAndElement.h" // JS_DefineProperty +#include "js/Proxy.h" + +#include "jsapi-tests/tests.h" + +using namespace js; +using namespace JS; + +class CustomProxyHandler : public Wrapper { + public: + CustomProxyHandler() : Wrapper(0) {} + + bool getOwnPropertyDescriptor( + JSContext* cx, HandleObject proxy, HandleId id, + MutableHandle<mozilla::Maybe<PropertyDescriptor>> desc) const override { + if (id.isString() && + JS_LinearStringEqualsLiteral(id.toLinearString(), "phantom")) { + desc.set(mozilla::Some(PropertyDescriptor::Data( + Int32Value(42), + {PropertyAttribute::Configurable, PropertyAttribute::Enumerable, + PropertyAttribute::Writable}))); + return true; + } + + return Wrapper::getOwnPropertyDescriptor(cx, proxy, id, desc); + } + + bool set(JSContext* cx, HandleObject proxy, HandleId id, HandleValue v, + HandleValue receiver, ObjectOpResult& result) const override { + Rooted<mozilla::Maybe<PropertyDescriptor>> desc(cx); + if (!Wrapper::getOwnPropertyDescriptor(cx, proxy, id, &desc)) { + return false; + } + return SetPropertyIgnoringNamedGetter(cx, proxy, id, v, receiver, desc, + result); + } +}; + +const CustomProxyHandler customProxyHandler; + +BEGIN_TEST(testSetPropertyIgnoringNamedGetter_direct) { + RootedValue protov(cx); + EVAL("Object.prototype", &protov); + + RootedValue targetv(cx); + EVAL("({})", &targetv); + + RootedObject proxyObj(cx, NewProxyObject(cx, &customProxyHandler, targetv, + &protov.toObject(), ProxyOptions())); + CHECK(proxyObj); + + CHECK(JS_DefineProperty(cx, global, "target", targetv, 0)); + CHECK(JS_DefineProperty(cx, global, "proxy", proxyObj, 0)); + + RootedValue v(cx); + EVAL("Object.getOwnPropertyDescriptor(proxy, 'phantom').value", &v); + CHECK_SAME(v, Int32Value(42)); + + EXEC("proxy.phantom = 123"); + EVAL("Object.getOwnPropertyDescriptor(proxy, 'phantom').value", &v); + CHECK_SAME(v, Int32Value(42)); + EVAL("target.phantom", &v); + CHECK_SAME(v, Int32Value(123)); + + return true; +} +END_TEST(testSetPropertyIgnoringNamedGetter_direct) diff --git a/js/src/jsapi-tests/testSharedImmutableStringsCache.cpp b/js/src/jsapi-tests/testSharedImmutableStringsCache.cpp new file mode 100644 index 0000000000..461bd6b21d --- /dev/null +++ b/js/src/jsapi-tests/testSharedImmutableStringsCache.cpp @@ -0,0 +1,72 @@ +/* -*- 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 "mozilla/IntegerRange.h" + +#include "js/Vector.h" +#include "jsapi-tests/tests.h" +#include "threading/Thread.h" +#include "util/Text.h" +#include "vm/SharedImmutableStringsCache.h" + +const int NUM_THREADS = 256; +const int NUM_ITERATIONS = 256; + +const int NUM_STRINGS = 4; +const char16_t* const STRINGS[NUM_STRINGS] = {u"uno", u"dos", u"tres", + u"quattro"}; + +struct CacheAndIndex { + js::SharedImmutableStringsCache* cache; + int index; + + CacheAndIndex(js::SharedImmutableStringsCache* cache, int index) + : cache(cache), index(index) {} +}; + +static void getString(CacheAndIndex* cacheAndIndex) { + for (int i = 0; i < NUM_ITERATIONS; i++) { + auto str = STRINGS[cacheAndIndex->index % NUM_STRINGS]; + + auto dupe = js::DuplicateString(str); + MOZ_RELEASE_ASSERT(dupe); + + auto deduped = + cacheAndIndex->cache->getOrCreate(std::move(dupe), js_strlen(str)); + MOZ_RELEASE_ASSERT(deduped); + MOZ_RELEASE_ASSERT( + js::EqualChars(str, deduped.chars(), js_strlen(str) + 1)); + + { + auto cloned = deduped.clone(); + // We should be de-duplicating and giving back the same string. + MOZ_RELEASE_ASSERT(deduped.chars() == cloned.chars()); + } + } + + js_delete(cacheAndIndex); +} + +BEGIN_TEST(testSharedImmutableStringsCache) { + auto& cache = js::SharedImmutableStringsCache::getSingleton(); + + js::Vector<js::Thread> threads(cx); + CHECK(threads.reserve(NUM_THREADS)); + + for (auto i : mozilla::IntegerRange(NUM_THREADS)) { + auto cacheAndIndex = js_new<CacheAndIndex>(&cache, i); + CHECK(cacheAndIndex); + threads.infallibleEmplaceBack(); + CHECK(threads.back().init(getString, cacheAndIndex)); + } + + for (auto& thread : threads) { + thread.join(); + } + + return true; +} +END_TEST(testSharedImmutableStringsCache) diff --git a/js/src/jsapi-tests/testSinglyLinkedList.cpp b/js/src/jsapi-tests/testSinglyLinkedList.cpp new file mode 100644 index 0000000000..e7fba612aa --- /dev/null +++ b/js/src/jsapi-tests/testSinglyLinkedList.cpp @@ -0,0 +1,162 @@ +/* -*- 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 <array> + +#include "ds/SinglyLinkedList.h" +#include "jsapi-tests/tests.h" + +using namespace js; + +struct IntElement { + int value; + IntElement* next = nullptr; + + explicit IntElement(int v) : value(v) {} +}; +using TestList = SinglyLinkedList<IntElement>; + +BEGIN_TEST(testSinglyLinkedList) { + // Test empty lists. + + TestList list; + CHECK(list.isEmpty()); + CHECK(!list.first()); + CHECK(!list.last()); + CHECK(CountList(list) == 0); + + // Test list pushBack and first/last accessors. + + list.pushBack(MakeElement(1)); + CHECK(!list.isEmpty()); + CHECK(list.first()->value == 1); + CHECK(list.last()->value == 1); + CHECK(CheckList<1>(list)); + + list.pushBack(MakeElement(2)); + list.pushBack(MakeElement(3)); + CHECK(!list.isEmpty()); + CHECK(list.first()->value == 1); + CHECK(list.last()->value == 3); + CHECK((CheckList<1, 2, 3>(list))); + + // Test popFront. + + IntElement* e = list.popFront(); + CHECK(e->value == 1); + js_delete(e); + CHECK(list.first()->value == 2); + CHECK((CheckList<2, 3>(list))); + + e = list.popFront(); + CHECK(e->value == 2); + js_delete(e); + CHECK(list.first()->value == 3); + + // Test pushFront. + + list.pushFront(MakeElement(2)); + CHECK(list.first()->value == 2); + CHECK((CheckList<2, 3>(list))); + + list.pushFront(MakeElement(1)); + CHECK(list.first()->value == 1); + CHECK((CheckList<1, 2, 3>(list))); + + // Test moveFrontToBack. + + list.moveFrontToBack(); + CHECK(list.first()->value == 2); + CHECK(list.last()->value == 1); + CHECK((CheckList<2, 3, 1>(list))); + list.moveFrontToBack(); + list.moveFrontToBack(); + CHECK((CheckList<1, 2, 3>(list))); + + // Test move constructor and assignment. + + TestList list2(std::move(list)); + CHECK(list.isEmpty()); + CHECK((CheckList<1, 2, 3>(list2))); + + list = std::move(list2); + CHECK(list2.isEmpty()); + CHECK((CheckList<1, 2, 3>(list))); + + // Test release. + + IntElement* head = list.release(); + CHECK(list.isEmpty()); + CHECK(head->value == 1); + CHECK(head->next->value == 2); + CHECK(head->next->next->value == 3); + CHECK(!head->next->next->next); + + // Test construct from linked list. + + list = TestList(head, head->next->next); + CHECK((CheckList<1, 2, 3>(list))); + + // Test append. + + CHECK(list2.isEmpty()); + list.append(std::move(list2)); + CHECK((CheckList<1, 2, 3>(list))); + CHECK(list2.isEmpty()); + + list2.pushBack(MakeElement(4)); + list2.pushBack(MakeElement(5)); + list2.pushBack(MakeElement(6)); + list.append(std::move(list2)); + CHECK((CheckList<1, 2, 3, 4, 5, 6>(list))); + CHECK(list2.isEmpty()); + + // Cleanup. + + while (!list.isEmpty()) { + js_delete(list.popFront()); + } + CHECK(list.isEmpty()); + CHECK(!list.first()); + CHECK(!list.last()); + CHECK(CountList(list) == 0); + + return true; +} + +IntElement* MakeElement(int value) { + IntElement* element = js_new<IntElement>(value); + MOZ_RELEASE_ASSERT(element); + return element; +} + +size_t CountList(const TestList& list) { + size_t i = 0; + for (auto iter = list.iter(); !iter.done(); iter.next()) { + i++; + } + return i; +} + +template <int... Values> +bool CheckList(const TestList& list) { + int expected[] = {Values...}; + constexpr size_t N = std::size(expected); + + size_t i = 0; + for (auto iter = list.iter(); !iter.done(); iter.next()) { + CHECK(i < N); + CHECK(iter->value == expected[i]); + i++; + } + + CHECK(i == N); + + return true; +} + +END_TEST(testSinglyLinkedList) diff --git a/js/src/jsapi-tests/testSliceBudget.cpp b/js/src/jsapi-tests/testSliceBudget.cpp new file mode 100644 index 0000000000..f73e1c8beb --- /dev/null +++ b/js/src/jsapi-tests/testSliceBudget.cpp @@ -0,0 +1,120 @@ +/* -*- 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 "js/SliceBudget.h" +#include "jsapi-tests/tests.h" + +using namespace js; + +BEGIN_TEST(testSliceBudgetUnlimited) { + SliceBudget budget = SliceBudget::unlimited(); + CHECK(budget.isUnlimited()); + CHECK(!budget.isTimeBudget()); + CHECK(!budget.isWorkBudget()); + + CHECK(!budget.isOverBudget()); + + budget.step(1000000); + CHECK(!budget.isOverBudget()); + + return true; +} +END_TEST(testSliceBudgetUnlimited) + +BEGIN_TEST(testSliceBudgetWork) { + SliceBudget budget = SliceBudget(WorkBudget(10000)); + CHECK(!budget.isUnlimited()); + CHECK(budget.isWorkBudget()); + CHECK(!budget.isTimeBudget()); + + CHECK(budget.workBudget() == 10000); + + CHECK(!budget.isOverBudget()); + + budget.step(5000); + CHECK(!budget.isOverBudget()); + + budget.step(5000); + CHECK(budget.isOverBudget()); + + return true; +} +END_TEST(testSliceBudgetWork) + +BEGIN_TEST(testSliceBudgetTime) { + SliceBudget budget = SliceBudget(TimeBudget(10000)); + CHECK(!budget.isUnlimited()); + CHECK(!budget.isWorkBudget()); + CHECK(budget.isTimeBudget()); + + CHECK(budget.timeBudget() == 10000); + + CHECK(!budget.isOverBudget()); + + budget.step(5000); + budget.step(5000); + CHECK(!budget.isOverBudget()); + + // This doesn't test the deadline is correct as that would require waiting. + + return true; +} +END_TEST(testSliceBudgetTime) + +BEGIN_TEST(testSliceBudgetTimeZero) { + SliceBudget budget = SliceBudget(TimeBudget(0)); + budget.step(1000); + CHECK(budget.isOverBudget()); + + return true; +} +END_TEST(testSliceBudgetTimeZero) + +BEGIN_TEST(testSliceBudgetInterruptibleTime) { + SliceBudget::InterruptRequestFlag wantInterrupt(false); + + // Interruptible 100 second budget. This test will finish in well under that + // time. + static constexpr int64_t LONG_TIME = 100000; + SliceBudget budget = SliceBudget(TimeBudget(LONG_TIME), &wantInterrupt); + CHECK(!budget.isUnlimited()); + CHECK(!budget.isWorkBudget()); + CHECK(budget.isTimeBudget()); + + CHECK(budget.timeBudget() == LONG_TIME); + + CHECK(!budget.isOverBudget()); + + // We do a little work, very small amount of time passes. + budget.step(500); + + // Not enough work to check interrupt, and no interrupt anyway. + CHECK(!budget.isOverBudget()); + + // External signal: interrupt requested. + wantInterrupt = true; + + // Interrupt requested, but not enough work has been done to check for it. + CHECK(!budget.isOverBudget()); + + // Do enough work for an expensive check. + budget.step(1000); + + // Interrupt requested! + CHECK(budget.isOverBudget()); + + // The external flag is not reset, but the budget will internally remember + // that an interrupt was requested. + CHECK(wantInterrupt); + wantInterrupt = false; + CHECK(budget.isOverBudget()); + + // This doesn't test the deadline is correct as that would require waiting. + + return true; +} +END_TEST(testSliceBudgetInterruptibleTime) diff --git a/js/src/jsapi-tests/testSlowScript.cpp b/js/src/jsapi-tests/testSlowScript.cpp new file mode 100644 index 0000000000..154120eed1 --- /dev/null +++ b/js/src/jsapi-tests/testSlowScript.cpp @@ -0,0 +1,72 @@ +/* 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 "js/PropertyAndElement.h" // JS_DefineFunction +#include "jsapi-tests/tests.h" + +static bool InterruptCallback(JSContext* cx) { return false; } + +static unsigned sRemain; + +static bool RequestInterruptCallback(JSContext* cx, unsigned argc, + JS::Value* vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + if (!sRemain--) { + JS_RequestInterruptCallback(cx); + } + args.rval().setUndefined(); + return true; +} + +BEGIN_TEST(testSlowScript) { + JS_AddInterruptCallback(cx, InterruptCallback); + JS_DefineFunction(cx, global, "requestInterruptCallback", + RequestInterruptCallback, 0, 0); + + CHECK( + test("while (true)" + " for (i in [0,0,0,0])" + " requestInterruptCallback();")); + + CHECK( + test("while (true)" + " for (i in [0,0,0,0])" + " for (j in [0,0,0,0])" + " requestInterruptCallback();")); + + CHECK( + test("while (true)" + " for (i in [0,0,0,0])" + " for (j in [0,0,0,0])" + " for (k in [0,0,0,0])" + " requestInterruptCallback();")); + + CHECK( + test("function* f() { while (true) yield requestInterruptCallback() }" + "for (i of f()) ;")); + + CHECK( + test("function* f() { while (true) yield 1 }" + "for (i of f())" + " requestInterruptCallback();")); + + return true; +} + +bool test(const char* bytes) { + JS::RootedValue v(cx); + + sRemain = 0; + CHECK(!evaluate(bytes, __FILE__, __LINE__, &v)); + CHECK(!JS_IsExceptionPending(cx)); + JS_ClearPendingException(cx); + + sRemain = 1000; + CHECK(!evaluate(bytes, __FILE__, __LINE__, &v)); + CHECK(!JS_IsExceptionPending(cx)); + JS_ClearPendingException(cx); + + return true; +} +END_TEST(testSlowScript) diff --git a/js/src/jsapi-tests/testSourcePolicy.cpp b/js/src/jsapi-tests/testSourcePolicy.cpp new file mode 100644 index 0000000000..b6f9d4a8d7 --- /dev/null +++ b/js/src/jsapi-tests/testSourcePolicy.cpp @@ -0,0 +1,57 @@ +/* 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 "mozilla/Utf8.h" // mozilla::Utf8Unit + +#include "js/CompilationAndEvaluation.h" // JS::CompileFunction, JS::Evaluate +#include "js/GlobalObject.h" // JS_NewGlobalObject +#include "js/MemoryFunctions.h" +#include "js/SourceText.h" // JS::Source{Ownership,Text} +#include "jsapi-tests/tests.h" +#include "vm/JSScript.h" + +BEGIN_TEST(testBug795104) { + JS::RealmOptions options; + options.behaviors().setDiscardSource(true); + + JS::RootedObject g(cx, JS_NewGlobalObject(cx, getGlobalClass(), nullptr, + JS::FireOnNewGlobalHook, options)); + CHECK(g); + + JSAutoRealm ar(cx, g); + + const size_t strLen = 60002; + char* s = static_cast<char*>(JS_malloc(cx, strLen)); + CHECK(s); + + s[0] = '"'; + memset(s + 1, 'x', strLen - 2); + s[strLen - 1] = '"'; + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + CHECK(srcBuf.init(cx, s, strLen, JS::SourceOwnership::Borrowed)); + + JS::CompileOptions opts(cx); + + // We don't want an rval for our JS::Evaluate call + opts.setNoScriptRval(true); + + JS::RootedValue unused(cx); + CHECK(JS::Evaluate(cx, opts, srcBuf, &unused)); + + JS::RootedFunction fun(cx); + JS::RootedObjectVector emptyScopeChain(cx); + + // But when compiling a function we don't want to use no-rval + // mode, since it's not supported for functions. + opts.setNoScriptRval(false); + + fun = JS::CompileFunction(cx, emptyScopeChain, opts, "f", 0, nullptr, srcBuf); + CHECK(fun); + + JS_free(cx, s); + + return true; +} +END_TEST(testBug795104) diff --git a/js/src/jsapi-tests/testSparseBitmap.cpp b/js/src/jsapi-tests/testSparseBitmap.cpp new file mode 100644 index 0000000000..bd5e7fee95 --- /dev/null +++ b/js/src/jsapi-tests/testSparseBitmap.cpp @@ -0,0 +1,115 @@ +/* -*- 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 "mozilla/PodOperations.h" + +#include "ds/Bitmap.h" + +#include "jsapi-tests/tests.h" + +using namespace js; + +BEGIN_TEST(testSparseBitmapBasics) { + SparseBitmap bitmap; + + // Test bits in first block are initially zero. + for (size_t i = 0; i < 100; i++) { + CHECK(!bitmap.getBit(i)); + } + + // Test bits in different blocks are initially zero. + for (size_t i = 0; i < 100; i++) { + CHECK(!bitmap.getBit(i * 1000)); + } + + // Set some bits in the first block and check they are set. + for (size_t i = 0; i < 100; i += 2) { + bitmap.setBit(i); + } + for (size_t i = 0; i < 100; i++) { + CHECK(bitmap.getBit(i) == ((i % 2) == 0)); + } + + // Set some bits in different blocks and check they are set. + for (size_t i = 0; i < 100; i += 2) { + bitmap.setBit(i * 1000); + } + for (size_t i = 0; i < 100; i++) { + CHECK(bitmap.getBit(i * 1000) == ((i % 2) == 0)); + } + + // Create another bitmap with different bits set. + SparseBitmap other; + for (size_t i = 1; i < 100; i += 2) { + other.setBit(i * 1000); + } + for (size_t i = 0; i < 100; i++) { + CHECK(other.getBit(i * 1000) == ((i % 2) != 0)); + } + + // OR some bits into this bitmap and check the result. + bitmap.bitwiseOrWith(other); + for (size_t i = 0; i < 100; i++) { + CHECK(bitmap.getBit(i * 1000)); + } + + // AND some bits into this bitmap and check the result. + DenseBitmap dense; + size_t wordCount = (100 * 1000) / JS_BITS_PER_WORD + 1; + CHECK(dense.ensureSpace(wordCount)); + other.bitwiseOrInto(dense); + bitmap.bitwiseAndWith(dense); + for (size_t i = 0; i < 100; i++) { + CHECK(bitmap.getBit(i * 1000) == ((i % 2) != 0)); + } + + return true; +} +END_TEST(testSparseBitmapBasics) + +BEGIN_TEST(testSparseBitmapExternalOR) { + // Testing ORing data into an external array. + + const size_t wordCount = 10; + + // Create a bitmap with one bit set per word so we can tell them apart. + SparseBitmap bitmap; + for (size_t i = 0; i < wordCount; i++) { + bitmap.setBit(i * JS_BITS_PER_WORD + i); + } + + // Copy a single word. + uintptr_t target[wordCount]; + mozilla::PodArrayZero(target); + bitmap.bitwiseOrRangeInto(0, 1, target); + CHECK(target[0] == 1u << 0); + CHECK(target[1] == 0); + + // Copy a word at an offset. + mozilla::PodArrayZero(target); + bitmap.bitwiseOrRangeInto(1, 1, target); + CHECK(target[0] == 1u << 1); + CHECK(target[1] == 0); + + // Check data is ORed with original target contents. + mozilla::PodArrayZero(target); + bitmap.bitwiseOrRangeInto(0, 1, target); + bitmap.bitwiseOrRangeInto(1, 1, target); + CHECK(target[0] == ((1u << 0) | (1u << 1))); + + // Copy multiple words at an offset. + mozilla::PodArrayZero(target); + bitmap.bitwiseOrRangeInto(2, wordCount - 2, target); + for (size_t i = 0; i < wordCount - 2; i++) { + CHECK(target[i] == (1u << (i + 2))); + } + CHECK(target[wordCount - 1] == 0); + + return true; +} + +END_TEST(testSparseBitmapExternalOR) diff --git a/js/src/jsapi-tests/testStencil.cpp b/js/src/jsapi-tests/testStencil.cpp new file mode 100644 index 0000000000..ab89222ebd --- /dev/null +++ b/js/src/jsapi-tests/testStencil.cpp @@ -0,0 +1,339 @@ +/* -*- 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 <string.h> + +#include "jsapi.h" + +#include "frontend/CompilationStencil.h" +#include "js/CompilationAndEvaluation.h" +#include "js/experimental/CompileScript.h" +#include "js/experimental/JSStencil.h" +#include "js/Modules.h" +#include "js/PropertyAndElement.h" // JS_GetProperty, JS_HasOwnProperty, JS_SetProperty +#include "js/Transcoding.h" +#include "jsapi-tests/tests.h" +#include "vm/HelperThreads.h" // js::RunPendingSourceCompressions +#include "vm/Monitor.h" // js::Monitor, js::AutoLockMonitor + +BEGIN_TEST(testStencil_Basic) { + const char* chars = + "function f() { return 42; }" + "f();"; + auto result = basic_test<char, mozilla::Utf8Unit>(chars); + CHECK(result); + + const char16_t* chars16 = + u"function f() { return 42; }" + u"f();"; + auto result16 = basic_test<char16_t, char16_t>(chars16); + CHECK(result16); + + return true; +} + +template <typename CharT, typename SourceT> +bool basic_test(const CharT* chars) { + size_t length = std::char_traits<CharT>::length(chars); + + JS::SourceText<SourceT> srcBuf; + CHECK(srcBuf.init(cx, chars, length, JS::SourceOwnership::Borrowed)); + + JS::CompileOptions options(cx); + RefPtr<JS::Stencil> stencil = + JS::CompileGlobalScriptToStencil(cx, options, srcBuf); + CHECK(stencil); + + JS::InstantiateOptions instantiateOptions(options); + JS::RootedScript script( + cx, JS::InstantiateGlobalStencil(cx, instantiateOptions, stencil)); + CHECK(script); + + JS::RootedValue rval(cx); + CHECK(JS_ExecuteScript(cx, script, &rval)); + CHECK(rval.isNumber() && rval.toNumber() == 42); + + return true; +} +END_TEST(testStencil_Basic) + +BEGIN_TEST(testStencil_Module) { + const char* chars = + "export function f() { return 42; }" + "globalThis.x = f();"; + auto result = basic_test<char, mozilla::Utf8Unit>(chars); + CHECK(result); + + const char16_t* chars16 = + u"export function f() { return 42; }" + u"globalThis.x = f();"; + auto result16 = basic_test<char16_t, char16_t>(chars16); + CHECK(result16); + + return true; +} + +template <typename CharT, typename SourceT> +bool basic_test(const CharT* chars) { + size_t length = std::char_traits<CharT>::length(chars); + + JS::SourceText<SourceT> srcBuf; + CHECK(srcBuf.init(cx, chars, length, JS::SourceOwnership::Borrowed)); + + JS::CompileOptions options(cx); + RefPtr<JS::Stencil> stencil = + JS::CompileModuleScriptToStencil(cx, options, srcBuf); + CHECK(stencil); + + JS::InstantiateOptions instantiateOptions(options); + JS::RootedObject moduleObject( + cx, JS::InstantiateModuleStencil(cx, instantiateOptions, stencil)); + CHECK(moduleObject); + + // Link and evaluate the module graph. The link step used to be call + // "instantiate" but is unrelated to the concept in Stencil with same name. + JS::RootedValue rval(cx); + CHECK(JS::ModuleLink(cx, moduleObject)); + CHECK(JS::ModuleEvaluate(cx, moduleObject, &rval)); + CHECK(!rval.isUndefined()); + + js::RunJobs(cx); + CHECK(JS_GetProperty(cx, global, "x", &rval)); + CHECK(rval.isNumber() && rval.toNumber() == 42); + + return true; +} +END_TEST(testStencil_Module) + +BEGIN_TEST(testStencil_NonSyntactic) { + const char* chars = + "function f() { return x; }" + "f();"; + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + CHECK(srcBuf.init(cx, chars, strlen(chars), JS::SourceOwnership::Borrowed)); + + JS::CompileOptions options(cx); + options.setNonSyntacticScope(true); + + RefPtr<JS::Stencil> stencil = + JS::CompileGlobalScriptToStencil(cx, options, srcBuf); + CHECK(stencil); + + JS::InstantiateOptions instantiateOptions(options); + JS::RootedScript script( + cx, JS::InstantiateGlobalStencil(cx, instantiateOptions, stencil)); + CHECK(script); + + JS::RootedObject obj(cx, JS_NewPlainObject(cx)); + JS::RootedValue val(cx, JS::Int32Value(42)); + CHECK(obj); + CHECK(JS_SetProperty(cx, obj, "x", val)); + + JS::RootedObjectVector chain(cx); + CHECK(chain.append(obj)); + + JS::RootedValue rval(cx); + CHECK(JS_ExecuteScript(cx, chain, script, &rval)); + CHECK(rval.isNumber() && rval.toNumber() == 42); + + return true; +} +END_TEST(testStencil_NonSyntactic) + +BEGIN_TEST(testStencil_MultiGlobal) { + const char* chars = + "/**************************************/" + "/**************************************/" + "/**************************************/" + "/**************************************/" + "/**************************************/" + "/**************************************/" + "function f() { return 42; }" + "f();"; + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + CHECK(srcBuf.init(cx, chars, strlen(chars), JS::SourceOwnership::Borrowed)); + + JS::CompileOptions options(cx); + RefPtr<JS::Stencil> stencil = + JS::CompileGlobalScriptToStencil(cx, options, srcBuf); + CHECK(stencil); + + CHECK(RunInNewGlobal(cx, stencil)); + CHECK(RunInNewGlobal(cx, stencil)); + CHECK(RunInNewGlobal(cx, stencil)); + + // Start any pending SourceCompressionTasks now to confirm nothing fell apart + // when using a JS::Stencil multiple times. + CHECK(strlen(chars) > js::ScriptSource::MinimumCompressibleLength); + js::RunPendingSourceCompressions(cx->runtime()); + + return true; +} +bool RunInNewGlobal(JSContext* cx, RefPtr<JS::Stencil> stencil) { + JS::RootedObject otherGlobal(cx, createGlobal()); + CHECK(otherGlobal); + + JSAutoRealm ar(cx, otherGlobal); + + JS::InstantiateOptions instantiateOptions; + JS::RootedScript script( + cx, JS::InstantiateGlobalStencil(cx, instantiateOptions, stencil)); + CHECK(script); + + JS::RootedValue rval(cx); + CHECK(JS_ExecuteScript(cx, script, &rval)); + CHECK(rval.isNumber() && rval.toNumber() == 42); + + return true; +} +END_TEST(testStencil_MultiGlobal) + +BEGIN_TEST(testStencil_Transcode) { + JS::SetProcessBuildIdOp(TestGetBuildId); + + JS::TranscodeBuffer buffer; + + { + const char* chars = + "function f() { return 42; }" + "f();"; + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + CHECK(srcBuf.init(cx, chars, strlen(chars), JS::SourceOwnership::Borrowed)); + + JS::CompileOptions options(cx); + RefPtr<JS::Stencil> stencil = + JS::CompileGlobalScriptToStencil(cx, options, srcBuf); + CHECK(stencil); + + // Encode Stencil to XDR + JS::TranscodeResult res = JS::EncodeStencil(cx, stencil, buffer); + CHECK(res == JS::TranscodeResult::Ok); + CHECK(!buffer.empty()); + + // Instantiate and Run + JS::InstantiateOptions instantiateOptions(options); + JS::RootedScript script( + cx, JS::InstantiateGlobalStencil(cx, instantiateOptions, stencil)); + JS::RootedValue rval(cx); + CHECK(script); + CHECK(JS_ExecuteScript(cx, script, &rval)); + CHECK(rval.isNumber() && rval.toNumber() == 42); + } + + // Create a new global + CHECK(createGlobal()); + JSAutoRealm ar(cx, global); + + // Confirm it doesn't have the old code + bool found = false; + CHECK(JS_HasOwnProperty(cx, global, "f", &found)); + CHECK(!found); + + { + // Decode the stencil into new range + RefPtr<JS::Stencil> stencil; + + { + JS::DecodeOptions decodeOptions; + JS::TranscodeRange range(buffer.begin(), buffer.length()); + JS::TranscodeResult res = + JS::DecodeStencil(cx, decodeOptions, range, getter_AddRefs(stencil)); + CHECK(res == JS::TranscodeResult::Ok); + } + + { + JS::FrontendContext* fc = JS::NewFrontendContext(); + JS::DecodeOptions decodeOptions; + JS::TranscodeRange range(buffer.begin(), buffer.length()); + JS::TranscodeResult res = + JS::DecodeStencil(fc, decodeOptions, range, getter_AddRefs(stencil)); + CHECK(res == JS::TranscodeResult::Ok); + JS::DestroyFrontendContext(fc); + } + + // Delete the buffer to verify that the decoded stencil has no dependency + // to the buffer. + memset(buffer.begin(), 0, buffer.length()); + buffer.clear(); + + // Instantiate and Run + JS::InstantiateOptions instantiateOptions; + JS::RootedScript script( + cx, JS::InstantiateGlobalStencil(cx, instantiateOptions, stencil)); + stencil = nullptr; + JS::RootedValue rval(cx); + CHECK(script); + CHECK(JS_ExecuteScript(cx, script, &rval)); + CHECK(rval.isNumber() && rval.toNumber() == 42); + } + + return true; +} +static bool TestGetBuildId(JS::BuildIdCharVector* buildId) { + const char buildid[] = "testXDR"; + return buildId->append(buildid, sizeof(buildid)); +} +END_TEST(testStencil_Transcode) + +BEGIN_TEST(testStencil_TranscodeBorrowing) { + JS::SetProcessBuildIdOp(TestGetBuildId); + + JS::TranscodeBuffer buffer; + + { + const char* chars = + "function f() { return 42; }" + "f();"; + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + CHECK(srcBuf.init(cx, chars, strlen(chars), JS::SourceOwnership::Borrowed)); + + JS::CompileOptions options(cx); + RefPtr<JS::Stencil> stencil = + JS::CompileGlobalScriptToStencil(cx, options, srcBuf); + CHECK(stencil); + + // Encode Stencil to XDR + JS::TranscodeResult res = JS::EncodeStencil(cx, stencil, buffer); + CHECK(res == JS::TranscodeResult::Ok); + CHECK(!buffer.empty()); + } + + JS::RootedScript script(cx); + { + JS::TranscodeRange range(buffer.begin(), buffer.length()); + JS::DecodeOptions decodeOptions; + decodeOptions.borrowBuffer = true; + RefPtr<JS::Stencil> stencil; + JS::TranscodeResult res = + JS::DecodeStencil(cx, decodeOptions, range, getter_AddRefs(stencil)); + CHECK(res == JS::TranscodeResult::Ok); + + JS::InstantiateOptions instantiateOptions; + script = JS::InstantiateGlobalStencil(cx, instantiateOptions, stencil); + CHECK(script); + } + + // Delete the buffer to verify that the instantiated script has no dependency + // to the buffer. + memset(buffer.begin(), 0, buffer.length()); + buffer.clear(); + + JS::RootedValue rval(cx); + CHECK(JS_ExecuteScript(cx, script, &rval)); + CHECK(rval.isNumber() && rval.toNumber() == 42); + + return true; +} +static bool TestGetBuildId(JS::BuildIdCharVector* buildId) { + const char buildid[] = "testXDR"; + return buildId->append(buildid, sizeof(buildid)); +} +END_TEST(testStencil_TranscodeBorrowing) diff --git a/js/src/jsapi-tests/testStringBuffer.cpp b/js/src/jsapi-tests/testStringBuffer.cpp new file mode 100644 index 0000000000..b4fee6203d --- /dev/null +++ b/js/src/jsapi-tests/testStringBuffer.cpp @@ -0,0 +1,27 @@ +/* -*- 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 "util/StringBuffer.h" +#include "vm/JSAtomUtils.h" // AtomizeString + +BEGIN_TEST(testStringBuffer_finishString) { + JSString* str = JS_NewStringCopyZ(cx, "foopy"); + CHECK(str); + + JS::Rooted<JSAtom*> atom(cx, js::AtomizeString(cx, str)); + CHECK(atom); + + js::StringBuffer buffer(cx); + CHECK(buffer.append("foopy")); + + JS::Rooted<JSAtom*> finishedAtom(cx, buffer.finishAtom()); + CHECK(finishedAtom); + CHECK_EQUAL(atom, finishedAtom); + return true; +} +END_TEST(testStringBuffer_finishString) diff --git a/js/src/jsapi-tests/testStringIsArrayIndex.cpp b/js/src/jsapi-tests/testStringIsArrayIndex.cpp new file mode 100644 index 0000000000..7eb90fcbac --- /dev/null +++ b/js/src/jsapi-tests/testStringIsArrayIndex.cpp @@ -0,0 +1,81 @@ +/* -*- 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 <string> + +#include "jsfriendapi.h" + +#include "jsapi-tests/tests.h" + +static const struct TestTuple { + /* The string being tested. */ + const char16_t* string; + /* The number of characters from the string to use. */ + size_t length; + /* Whether this string is an index. */ + bool isindex; + /* If it's an index, what index it is. Ignored if not an index. */ + uint32_t index; + + constexpr TestTuple(const char16_t* string, bool isindex, uint32_t index) + : TestTuple(string, std::char_traits<char16_t>::length(string), isindex, + index) {} + + constexpr TestTuple(const char16_t* string, size_t length, bool isindex, + uint32_t index) + : string(string), length(length), isindex(isindex), index(index) {} +} tests[] = { + {u"0", true, 0}, + {u"1", true, 1}, + {u"2", true, 2}, + {u"9", true, 9}, + {u"10", true, 10}, + {u"15", true, 15}, + {u"16", true, 16}, + {u"17", true, 17}, + {u"99", true, 99}, + {u"100", true, 100}, + {u"255", true, 255}, + {u"256", true, 256}, + {u"257", true, 257}, + {u"999", true, 999}, + {u"1000", true, 1000}, + {u"4095", true, 4095}, + {u"4096", true, 4096}, + {u"9999", true, 9999}, + {u"1073741823", true, 1073741823}, + {u"1073741824", true, 1073741824}, + {u"1073741825", true, 1073741825}, + {u"2147483647", true, 2147483647}, + {u"2147483648", true, 2147483648u}, + {u"2147483649", true, 2147483649u}, + {u"4294967294", true, 4294967294u}, + {u"4294967295", false, + 0}, // Not an array index because need to be able to represent length + {u"-1", false, 0}, + {u"abc", false, 0}, + {u" 0", false, 0}, + {u"0 ", false, 0}, + // Tests to make sure the passed-in length is taken into account + {u"0 ", 1, true, 0}, + {u"123abc", 3, true, 123}, + {u"123abc", 2, true, 12}, +}; + +BEGIN_TEST(testStringIsArrayIndex) { + for (const auto& test : tests) { + uint32_t index; + bool isindex = js::StringIsArrayIndex(test.string, test.length, &index); + CHECK_EQUAL(isindex, test.isindex); + if (isindex) { + CHECK_EQUAL(index, test.index); + } + } + + return true; +} +END_TEST(testStringIsArrayIndex) diff --git a/js/src/jsapi-tests/testStructuredClone.cpp b/js/src/jsapi-tests/testStructuredClone.cpp new file mode 100644 index 0000000000..3a16175207 --- /dev/null +++ b/js/src/jsapi-tests/testStructuredClone.cpp @@ -0,0 +1,365 @@ +/* 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 "builtin/TestingFunctions.h" +#include "js/ArrayBuffer.h" // JS::{IsArrayBufferObject,GetArrayBufferLengthAndData,NewExternalArrayBuffer} +#include "js/GlobalObject.h" // JS_NewGlobalObject +#include "js/PropertyAndElement.h" // JS_GetProperty, JS_SetProperty +#include "js/StructuredClone.h" + +#include "jsapi-tests/tests.h" + +using namespace js; + +#ifdef DEBUG +// Skip test, since it will abort with an assert in buf->Init(7). +#else +BEGIN_TEST(testStructuredClone_invalidLength) { + auto buf = js::MakeUnique<JSStructuredCloneData>( + JS::StructuredCloneScope::DifferentProcess); + CHECK(buf); + CHECK(buf->Init(7)); + RootedValue clone(cx); + JS::CloneDataPolicy policy; + CHECK(!JS_ReadStructuredClone(cx, *buf, JS_STRUCTURED_CLONE_VERSION, + JS::StructuredCloneScope::DifferentProcess, + &clone, policy, nullptr, nullptr)); + return true; +} +END_TEST(testStructuredClone_invalidLength) +#endif + +BEGIN_TEST(testStructuredClone_object) { + JS::RootedObject g1(cx, createGlobal()); + JS::RootedObject g2(cx, createGlobal()); + CHECK(g1); + CHECK(g2); + + JS::RootedValue v1(cx); + + { + JSAutoRealm ar(cx, g1); + JS::RootedValue prop(cx, JS::Int32Value(1337)); + + JS::RootedObject obj(cx, JS_NewPlainObject(cx)); + v1 = JS::ObjectOrNullValue(obj); + CHECK(v1.isObject()); + CHECK(JS_SetProperty(cx, obj, "prop", prop)); + } + + { + JSAutoRealm ar(cx, g2); + JS::RootedValue v2(cx); + + CHECK(JS_StructuredClone(cx, v1, &v2, nullptr, nullptr)); + CHECK(v2.isObject()); + JS::RootedObject obj(cx, &v2.toObject()); + + JS::RootedValue prop(cx); + CHECK(JS_GetProperty(cx, obj, "prop", &prop)); + CHECK(prop.isInt32()); + CHECK(&v1.toObject() != obj); + CHECK_EQUAL(prop.toInt32(), 1337); + } + + return true; +} +END_TEST(testStructuredClone_object) + +BEGIN_TEST(testStructuredClone_string) { + JS::RootedObject g1(cx, createGlobal()); + JS::RootedObject g2(cx, createGlobal()); + CHECK(g1); + CHECK(g2); + + JS::RootedValue v1(cx); + + { + JSAutoRealm ar(cx, g1); + JS::RootedValue prop(cx, JS::Int32Value(1337)); + + v1 = JS::StringValue(JS_NewStringCopyZ(cx, "Hello World!")); + CHECK(v1.isString()); + CHECK(v1.toString()); + } + + { + JSAutoRealm ar(cx, g2); + JS::RootedValue v2(cx); + + CHECK(JS_StructuredClone(cx, v1, &v2, nullptr, nullptr)); + CHECK(v2.isString()); + CHECK(v2.toString()); + + JS::RootedValue expected( + cx, JS::StringValue(JS_NewStringCopyZ(cx, "Hello World!"))); + CHECK_SAME(v2, expected); + } + + return true; +} +END_TEST(testStructuredClone_string) + +BEGIN_TEST(testStructuredClone_externalArrayBuffer) { + ExternalData data("One two three four"); + auto dataPointer = data.pointer(); + JS::RootedObject g1(cx, createGlobal()); + JS::RootedObject g2(cx, createGlobal()); + CHECK(g1); + CHECK(g2); + + JS::RootedValue v1(cx); + + { + JSAutoRealm ar(cx, g1); + + JS::RootedObject obj( + cx, JS::NewExternalArrayBuffer(cx, data.len(), std::move(dataPointer))); + CHECK(!data.wasFreed()); + + v1 = JS::ObjectOrNullValue(obj); + CHECK(v1.isObject()); + } + + { + JSAutoRealm ar(cx, g2); + JS::RootedValue v2(cx); + + CHECK(JS_StructuredClone(cx, v1, &v2, nullptr, nullptr)); + CHECK(v2.isObject()); + + JS::RootedObject obj(cx, &v2.toObject()); + CHECK(&v1.toObject() != obj); + + size_t len; + bool isShared; + uint8_t* clonedData; + JS::GetArrayBufferLengthAndData(obj, &len, &isShared, &clonedData); + + // The contents of the two array buffers should be equal, but not the + // same pointer. + CHECK_EQUAL(len, data.len()); + CHECK(clonedData != data.contents()); + CHECK(strcmp(reinterpret_cast<char*>(clonedData), data.asString()) == 0); + CHECK(!data.wasFreed()); + } + + // GC the array buffer before data goes out of scope + v1.setNull(); + JS_GC(cx); + JS_GC(cx); // Trigger another to wait for background finalization to end + + CHECK(data.wasFreed()); + + return true; +} +END_TEST(testStructuredClone_externalArrayBuffer) + +BEGIN_TEST(testStructuredClone_externalArrayBufferDifferentThreadOrProcess) { + CHECK(testStructuredCloneCopy(JS::StructuredCloneScope::SameProcess)); + CHECK(testStructuredCloneCopy(JS::StructuredCloneScope::DifferentProcess)); + return true; +} + +bool testStructuredCloneCopy(JS::StructuredCloneScope scope) { + ExternalData data("One two three four"); + auto dataPointer = data.pointer(); + JS::RootedObject buffer( + cx, JS::NewExternalArrayBuffer(cx, data.len(), std::move(dataPointer))); + CHECK(buffer); + CHECK(!data.wasFreed()); + + JS::RootedValue v1(cx, JS::ObjectValue(*buffer)); + JS::RootedValue v2(cx); + CHECK(clone(scope, v1, &v2)); + JS::RootedObject bufferOut(cx, v2.toObjectOrNull()); + CHECK(bufferOut); + CHECK(JS::IsArrayBufferObject(bufferOut)); + + size_t len; + bool isShared; + uint8_t* clonedData; + JS::GetArrayBufferLengthAndData(bufferOut, &len, &isShared, &clonedData); + + // Cloning should copy the data, so the contents of the two array buffers + // should be equal, but not the same pointer. + CHECK_EQUAL(len, data.len()); + CHECK(clonedData != data.contents()); + CHECK(strcmp(reinterpret_cast<char*>(clonedData), data.asString()) == 0); + CHECK(!data.wasFreed()); + + buffer = nullptr; + bufferOut = nullptr; + v1.setNull(); + v2.setNull(); + JS_GC(cx); + JS_GC(cx); + CHECK(data.wasFreed()); + + return true; +} + +bool clone(JS::StructuredCloneScope scope, JS::HandleValue v1, + JS::MutableHandleValue v2) { + JSAutoStructuredCloneBuffer clonedBuffer(scope, nullptr, nullptr); + CHECK(clonedBuffer.write(cx, v1)); + CHECK(clonedBuffer.read(cx, v2)); + return true; +} +END_TEST(testStructuredClone_externalArrayBufferDifferentThreadOrProcess) + +struct StructuredCloneTestPrincipals final : public JSPrincipals { + uint32_t rank; + + explicit StructuredCloneTestPrincipals(uint32_t rank, int32_t rc = 1) + : rank(rank) { + this->refcount = rc; + } + + bool write(JSContext* cx, JSStructuredCloneWriter* writer) override { + return JS_WriteUint32Pair(writer, rank, 0); + } + + bool isSystemOrAddonPrincipal() override { return true; } + + static bool read(JSContext* cx, JSStructuredCloneReader* reader, + JSPrincipals** outPrincipals) { + uint32_t rank; + uint32_t unused; + if (!JS_ReadUint32Pair(reader, &rank, &unused)) { + return false; + } + + *outPrincipals = new StructuredCloneTestPrincipals(rank); + return !!*outPrincipals; + } + + static void destroy(JSPrincipals* p) { + auto p1 = static_cast<StructuredCloneTestPrincipals*>(p); + delete p1; + } + + static uint32_t getRank(JSPrincipals* p) { + if (!p) { + return 0; + } + return static_cast<StructuredCloneTestPrincipals*>(p)->rank; + } + + static bool subsumes(JSPrincipals* a, JSPrincipals* b) { + return getRank(a) > getRank(b); + } + + static JSSecurityCallbacks securityCallbacks; + + static StructuredCloneTestPrincipals testPrincipals; +}; + +JSSecurityCallbacks StructuredCloneTestPrincipals::securityCallbacks = { + nullptr, // contentSecurityPolicyAllows + subsumes}; + +BEGIN_TEST(testStructuredClone_SavedFrame) { + JS_SetSecurityCallbacks(cx, + &StructuredCloneTestPrincipals::securityCallbacks); + JS_InitDestroyPrincipalsCallback(cx, StructuredCloneTestPrincipals::destroy); + JS_InitReadPrincipalsCallback(cx, StructuredCloneTestPrincipals::read); + + auto testPrincipals = new StructuredCloneTestPrincipals(42, 0); + CHECK(testPrincipals); + + auto DONE = (JSPrincipals*)0xDEADBEEF; + + struct { + const char* name; + JSPrincipals* principals; + } principalsToTest[] = { + {"IsSystem", &js::ReconstructedSavedFramePrincipals::IsSystem}, + {"IsNotSystem", &js::ReconstructedSavedFramePrincipals::IsNotSystem}, + {"testPrincipals", testPrincipals}, + {"nullptr principals", nullptr}, + {"DONE", DONE}}; + + const char* FILENAME = "filename.js"; + + for (auto* pp = principalsToTest; pp->principals != DONE; pp++) { + fprintf(stderr, "Testing with principals '%s'\n", pp->name); + + JS::RealmOptions options; + JS::RootedObject g(cx, + JS_NewGlobalObject(cx, getGlobalClass(), pp->principals, + JS::FireOnNewGlobalHook, options)); + CHECK(g); + JSAutoRealm ar(cx, g); + + CHECK(js::DefineTestingFunctions(cx, g, false, false)); + + JS::RootedValue srcVal(cx); + CHECK( + evaluate("(function one() { \n" // 1 + " return (function two() { \n" // 2 + " return (function three() { \n" // 3 + " return saveStack(); \n" // 4 + " }()); \n" // 5 + " }()); \n" // 6 + "}()); \n", // 7 + FILENAME, 1, &srcVal)); + + CHECK(srcVal.isObject()); + JS::RootedObject srcObj(cx, &srcVal.toObject()); + + CHECK(srcObj->is<js::SavedFrame>()); + JS::Rooted<js::SavedFrame*> srcFrame(cx, &srcObj->as<js::SavedFrame>()); + + CHECK(srcFrame->getPrincipals() == pp->principals); + + JS::RootedValue destVal(cx); + CHECK(JS_StructuredClone(cx, srcVal, &destVal, nullptr, nullptr)); + + CHECK(destVal.isObject()); + JS::RootedObject destObj(cx, &destVal.toObject()); + + CHECK(destObj->is<js::SavedFrame>()); + JS::Handle<js::SavedFrame*> destFrame = destObj.as<js::SavedFrame>(); + + size_t framesCopied = 0; + for (JS::Handle<js::SavedFrame*> f : + js::SavedFrame::RootedRange(cx, destFrame)) { + framesCopied++; + + CHECK(f != srcFrame); + + if (pp->principals == testPrincipals) { + // We shouldn't get a pointer to the same + // StructuredCloneTestPrincipals instance since we should have + // serialized and then deserialized it into a new instance. + CHECK(f->getPrincipals() != pp->principals); + + // But it should certainly have the same rank. + CHECK(StructuredCloneTestPrincipals::getRank(f->getPrincipals()) == + StructuredCloneTestPrincipals::getRank(pp->principals)); + } else { + // For our singleton principals, we should always get the same + // pointer back. + CHECK(js::ReconstructedSavedFramePrincipals::is(pp->principals) || + pp->principals == nullptr); + CHECK(f->getPrincipals() == pp->principals); + } + + CHECK(EqualStrings(f->getSource(), srcFrame->getSource())); + CHECK(f->getLine() == srcFrame->getLine()); + CHECK(f->getColumn() == srcFrame->getColumn()); + CHECK(EqualStrings(f->getFunctionDisplayName(), + srcFrame->getFunctionDisplayName())); + + srcFrame = srcFrame->getParent(); + } + + // Four function frames + one global frame. + CHECK(framesCopied == 4); + } + + return true; +} +END_TEST(testStructuredClone_SavedFrame) diff --git a/js/src/jsapi-tests/testSymbol.cpp b/js/src/jsapi-tests/testSymbol.cpp new file mode 100644 index 0000000000..effc416c2d --- /dev/null +++ b/js/src/jsapi-tests/testSymbol.cpp @@ -0,0 +1,79 @@ +/* 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 "js/Symbol.h" +#include "jsapi-tests/tests.h" + +BEGIN_TEST(testSymbol_New) { + using namespace JS; + + RootedString desc(cx, nullptr); + RootedSymbol sym1(cx); + CHECK(sym1 = NewSymbol(cx, desc)); + CHECK_NULL(GetSymbolDescription(sym1)); + RootedValue v(cx, SymbolValue(sym1)); + CHECK_EQUAL(JS_TypeOfValue(cx, v), JSTYPE_SYMBOL); + + RootedSymbol sym2(cx); + CHECK(sym2 = NewSymbol(cx, desc)); + CHECK(sym1 != sym2); + + CHECK(desc = JS_NewStringCopyZ(cx, "ponies")); + CHECK(sym2 = NewSymbol(cx, desc)); + CHECK_SAME(StringValue(GetSymbolDescription(sym2)), StringValue(desc)); + + return true; +} +END_TEST(testSymbol_New) + +BEGIN_TEST(testSymbol_GetSymbolFor) { + using namespace JS; + + RootedString desc(cx, JS_NewStringCopyZ(cx, "ponies")); + CHECK(desc); + RootedSymbol sym1(cx); + CHECK(sym1 = GetSymbolFor(cx, desc)); + CHECK_SAME(StringValue(GetSymbolDescription(sym1)), StringValue(desc)); + + // Calling JS::GetSymbolFor again with the same arguments produces the + // same Symbol. + RootedSymbol sym2(cx); + CHECK(sym2 = GetSymbolFor(cx, desc)); + CHECK_EQUAL(sym1, sym2); + + // Passing a new but equal string also produces the same Symbol. + CHECK(desc = JS_NewStringCopyZ(cx, "ponies")); + CHECK(sym2 = GetSymbolFor(cx, desc)); + CHECK_EQUAL(sym1, sym2); + + // But SymbolNew always produces a new distinct Symbol. + CHECK(sym2 = NewSymbol(cx, desc)); + CHECK(sym2 != sym1); + + return true; +} +END_TEST(testSymbol_GetSymbolFor) + +BEGIN_TEST(testSymbol_GetWellKnownSymbol) { + using namespace JS; + + Rooted<Symbol*> sym1(cx); + CHECK(sym1 = GetWellKnownSymbol(cx, SymbolCode::iterator)); + RootedValue v(cx); + EVAL("Symbol.iterator", &v); + CHECK_SAME(v, SymbolValue(sym1)); + + // The description of a well-known symbol is as specified. + RootedString desc(cx); + CHECK(desc = JS_NewStringCopyZ(cx, "Symbol.iterator")); + CHECK_SAME(StringValue(GetSymbolDescription(sym1)), StringValue(desc)); + + // GetSymbolFor never returns a well-known symbol. + Rooted<Symbol*> sym2(cx); + CHECK(sym2 = GetSymbolFor(cx, desc)); + CHECK(sym2 != sym1); + + return true; +} +END_TEST(testSymbol_GetWellKnownSymbol) diff --git a/js/src/jsapi-tests/testThreadingConditionVariable.cpp b/js/src/jsapi-tests/testThreadingConditionVariable.cpp new file mode 100644 index 0000000000..1bff194372 --- /dev/null +++ b/js/src/jsapi-tests/testThreadingConditionVariable.cpp @@ -0,0 +1,220 @@ +/* -*- 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 "threading/ConditionVariable.h" +#include "threading/Thread.h" +#include "vm/MutexIDs.h" + +struct TestState { + js::Mutex mutex MOZ_UNANNOTATED; + js::ConditionVariable condition; + bool flag; + js::Thread testThread; + + explicit TestState(bool createThread = true) + : mutex(js::mutexid::TestMutex), flag(false) { + if (createThread) { + MOZ_RELEASE_ASSERT(testThread.init(setFlag, this)); + } + } + + static void setFlag(TestState* state) { + js::UniqueLock<js::Mutex> lock(state->mutex); + state->flag = true; + state->condition.notify_one(); + } + + void join() { testThread.join(); } +}; + +BEGIN_TEST(testThreadingConditionVariable) { + auto state = mozilla::MakeUnique<TestState>(); + { + js::UniqueLock<js::Mutex> lock(state->mutex); + while (!state->flag) { + state->condition.wait(lock); + } + } + state->join(); + + CHECK(state->flag); + + return true; +} +END_TEST(testThreadingConditionVariable) + +BEGIN_TEST(testThreadingConditionVariablePredicate) { + auto state = mozilla::MakeUnique<TestState>(); + { + js::UniqueLock<js::Mutex> lock(state->mutex); + state->condition.wait(lock, [&state]() { return state->flag; }); + } + state->join(); + + CHECK(state->flag); + + return true; +} +END_TEST(testThreadingConditionVariablePredicate) + +BEGIN_TEST(testThreadingConditionVariableUntilOkay) { + auto state = mozilla::MakeUnique<TestState>(); + { + js::UniqueLock<js::Mutex> lock(state->mutex); + while (!state->flag) { + auto to = + mozilla::TimeStamp::Now() + mozilla::TimeDuration::FromSeconds(600); + js::CVStatus res = state->condition.wait_until(lock, to); + CHECK(res == js::CVStatus::NoTimeout); + } + } + state->join(); + + CHECK(state->flag); + + return true; +} +END_TEST(testThreadingConditionVariableUntilOkay) + +BEGIN_TEST(testThreadingConditionVariableUntilTimeout) { + auto state = mozilla::MakeUnique<TestState>(false); + { + js::UniqueLock<js::Mutex> lock(state->mutex); + while (!state->flag) { + auto to = mozilla::TimeStamp::Now() + + mozilla::TimeDuration::FromMilliseconds(10); + js::CVStatus res = state->condition.wait_until(lock, to); + if (res == js::CVStatus::Timeout) { + break; + } + } + } + CHECK(!state->flag); + + // Timeout in the past should return with timeout immediately. + { + js::UniqueLock<js::Mutex> lock(state->mutex); + auto to = + mozilla::TimeStamp::Now() - mozilla::TimeDuration::FromMilliseconds(10); + js::CVStatus res = state->condition.wait_until(lock, to); + CHECK(res == js::CVStatus::Timeout); + } + + return true; +} +END_TEST(testThreadingConditionVariableUntilTimeout) + +BEGIN_TEST(testThreadingConditionVariableUntilOkayPredicate) { + auto state = mozilla::MakeUnique<TestState>(); + { + js::UniqueLock<js::Mutex> lock(state->mutex); + auto to = + mozilla::TimeStamp::Now() + mozilla::TimeDuration::FromSeconds(600); + bool res = state->condition.wait_until(lock, to, + [&state]() { return state->flag; }); + CHECK(res); + } + state->join(); + + CHECK(state->flag); + + return true; +} +END_TEST(testThreadingConditionVariableUntilOkayPredicate) + +BEGIN_TEST(testThreadingConditionVariableUntilTimeoutPredicate) { + auto state = mozilla::MakeUnique<TestState>(false); + { + js::UniqueLock<js::Mutex> lock(state->mutex); + auto to = + mozilla::TimeStamp::Now() + mozilla::TimeDuration::FromMilliseconds(10); + bool res = state->condition.wait_until(lock, to, + [&state]() { return state->flag; }); + CHECK(!res); + } + CHECK(!state->flag); + + return true; +} +END_TEST(testThreadingConditionVariableUntilTimeoutPredicate) + +BEGIN_TEST(testThreadingConditionVariableForOkay) { + auto state = mozilla::MakeUnique<TestState>(); + { + js::UniqueLock<js::Mutex> lock(state->mutex); + while (!state->flag) { + auto duration = mozilla::TimeDuration::FromSeconds(600); + js::CVStatus res = state->condition.wait_for(lock, duration); + CHECK(res == js::CVStatus::NoTimeout); + } + } + state->join(); + + CHECK(state->flag); + + return true; +} +END_TEST(testThreadingConditionVariableForOkay) + +BEGIN_TEST(testThreadingConditionVariableForTimeout) { + auto state = mozilla::MakeUnique<TestState>(false); + { + js::UniqueLock<js::Mutex> lock(state->mutex); + while (!state->flag) { + auto duration = mozilla::TimeDuration::FromMilliseconds(10); + js::CVStatus res = state->condition.wait_for(lock, duration); + if (res == js::CVStatus::Timeout) { + break; + } + } + } + CHECK(!state->flag); + + // Timeout in the past should return with timeout immediately. + { + js::UniqueLock<js::Mutex> lock(state->mutex); + auto duration = mozilla::TimeDuration::FromMilliseconds(-10); + js::CVStatus res = state->condition.wait_for(lock, duration); + CHECK(res == js::CVStatus::Timeout); + } + + return true; +} +END_TEST(testThreadingConditionVariableForTimeout) + +BEGIN_TEST(testThreadingConditionVariableForOkayPredicate) { + auto state = mozilla::MakeUnique<TestState>(); + { + js::UniqueLock<js::Mutex> lock(state->mutex); + auto duration = mozilla::TimeDuration::FromSeconds(600); + bool res = state->condition.wait_for(lock, duration, + [&state]() { return state->flag; }); + CHECK(res); + } + state->join(); + + CHECK(state->flag); + + return true; +} +END_TEST(testThreadingConditionVariableForOkayPredicate) + +BEGIN_TEST(testThreadingConditionVariableForTimeoutPredicate) { + auto state = mozilla::MakeUnique<TestState>(false); + { + js::UniqueLock<js::Mutex> lock(state->mutex); + auto duration = mozilla::TimeDuration::FromMilliseconds(10); + bool res = state->condition.wait_for(lock, duration, + [&state]() { return state->flag; }); + CHECK(!res); + } + CHECK(!state->flag); + + return true; +} +END_TEST(testThreadingConditionVariableForTimeoutPredicate) diff --git a/js/src/jsapi-tests/testThreadingExclusiveData.cpp b/js/src/jsapi-tests/testThreadingExclusiveData.cpp new file mode 100644 index 0000000000..0a72a639ac --- /dev/null +++ b/js/src/jsapi-tests/testThreadingExclusiveData.cpp @@ -0,0 +1,84 @@ +/* -*- 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 "mozilla/IntegerRange.h" +#include "js/Vector.h" +#include "jsapi-tests/tests.h" +#include "threading/ExclusiveData.h" +#include "threading/Thread.h" + +// One thread for each bit in our counter. +const static uint8_t NumThreads = 64; +const static bool ShowDiagnostics = false; + +struct CounterAndBit { + uint8_t bit; + const js::ExclusiveData<uint64_t>& counter; + + CounterAndBit(uint8_t bit, const js::ExclusiveData<uint64_t>& counter) + : bit(bit), counter(counter) { + MOZ_ASSERT(bit < NumThreads); + } +}; + +void printDiagnosticMessage(uint8_t bit, uint64_t seen) { + if (!ShowDiagnostics) { + return; + } + + fprintf(stderr, "Thread %d saw ", bit); + for (auto i : mozilla::IntegerRange(NumThreads)) { + if (seen & (uint64_t(1) << i)) { + fprintf(stderr, "1"); + } else { + fprintf(stderr, "0"); + } + } + fprintf(stderr, "\n"); +} + +void setBitAndCheck(CounterAndBit* counterAndBit) { + while (true) { + { + // Set our bit. Repeatedly setting it is idempotent. + auto guard = counterAndBit->counter.lock(); + printDiagnosticMessage(counterAndBit->bit, guard); + guard |= (uint64_t(1) << counterAndBit->bit); + } + + { + // Check to see if we have observed all the other threads setting + // their bit as well. + auto guard = counterAndBit->counter.lock(); + printDiagnosticMessage(counterAndBit->bit, guard); + if (guard == UINT64_MAX) { + js_delete(counterAndBit); + return; + } + } + } +} + +BEGIN_TEST(testExclusiveData) { + js::ExclusiveData<uint64_t> counter(js::mutexid::TestMutex, 0); + + js::Vector<js::Thread> threads(cx); + CHECK(threads.reserve(NumThreads)); + + for (auto i : mozilla::IntegerRange(NumThreads)) { + auto counterAndBit = js_new<CounterAndBit>(i, counter); + CHECK(counterAndBit); + CHECK(threads.emplaceBack()); + CHECK(threads.back().init(setBitAndCheck, counterAndBit)); + } + + for (auto& thread : threads) { + thread.join(); + } + + return true; +} +END_TEST(testExclusiveData) diff --git a/js/src/jsapi-tests/testThreadingMutex.cpp b/js/src/jsapi-tests/testThreadingMutex.cpp new file mode 100644 index 0000000000..4e9bd691f7 --- /dev/null +++ b/js/src/jsapi-tests/testThreadingMutex.cpp @@ -0,0 +1,49 @@ +/* -*- 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 "threading/LockGuard.h" +#include "vm/MutexIDs.h" + +#ifdef DEBUG +# define DEBUG_CHECK(x) CHECK(x) +#else +# define DEBUG_CHECK(x) +#endif + +BEGIN_TEST(testThreadingMutex) { + js::Mutex mutex MOZ_UNANNOTATED(js::mutexid::TestMutex); + DEBUG_CHECK(!mutex.isOwnedByCurrentThread()); + mutex.lock(); + DEBUG_CHECK(mutex.isOwnedByCurrentThread()); + mutex.unlock(); + DEBUG_CHECK(!mutex.isOwnedByCurrentThread()); + return true; +} +END_TEST(testThreadingMutex) + +BEGIN_TEST(testThreadingLockGuard) { + js::Mutex mutex MOZ_UNANNOTATED(js::mutexid::TestMutex); + DEBUG_CHECK(!mutex.isOwnedByCurrentThread()); + js::LockGuard guard(mutex); + DEBUG_CHECK(mutex.isOwnedByCurrentThread()); + return true; +} +END_TEST(testThreadingLockGuard) + +BEGIN_TEST(testThreadingUnlockGuard) { + js::Mutex mutex MOZ_UNANNOTATED(js::mutexid::TestMutex); + DEBUG_CHECK(!mutex.isOwnedByCurrentThread()); + js::LockGuard guard(mutex); + DEBUG_CHECK(mutex.isOwnedByCurrentThread()); + js::UnlockGuard unguard(guard); + DEBUG_CHECK(!mutex.isOwnedByCurrentThread()); + return true; +} +END_TEST(testThreadingUnlockGuard) + +#undef DEBUG_CHECK diff --git a/js/src/jsapi-tests/testThreadingThread.cpp b/js/src/jsapi-tests/testThreadingThread.cpp new file mode 100644 index 0000000000..68a7a388b0 --- /dev/null +++ b/js/src/jsapi-tests/testThreadingThread.cpp @@ -0,0 +1,107 @@ +/* -*- 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 "mozilla/Atomics.h" +#include "mozilla/IntegerRange.h" +#include "mozilla/Vector.h" + +#include <utility> + +#include "js/AllocPolicy.h" +#include "jsapi-tests/tests.h" +#include "threading/Thread.h" + +BEGIN_TEST(testThreadingThreadJoin) { + bool flag = false; + js::Thread thread; + CHECK(thread.init([](bool* flagp) { *flagp = true; }, &flag)); + CHECK(thread.joinable()); + thread.join(); + CHECK(flag); + CHECK(!thread.joinable()); + return true; +} +END_TEST(testThreadingThreadJoin) + +BEGIN_TEST(testThreadingThreadDetach) { + // We are going to detach this thread. Unlike join, we can't have it pointing + // at the stack because it might do the write after we have returned and + // pushed a new frame. + bool* flag = js_new<bool>(false); + js::Thread thread; + CHECK(thread.init( + [](bool* flag) { + *flag = true; + js_delete(flag); + }, + std::move(flag))); + CHECK(thread.joinable()); + thread.detach(); + CHECK(!thread.joinable()); + + return true; +} +END_TEST(testThreadingThreadDetach) + +BEGIN_TEST(testThreadingThreadSetName) { + js::Thread thread; + CHECK(thread.init([]() { js::ThisThread::SetName("JSAPI Test Thread"); })); + thread.detach(); + return true; +} +END_TEST(testThreadingThreadSetName) + +BEGIN_TEST(testThreadingThreadId) { + CHECK(js::ThreadId() == js::ThreadId()); + js::ThreadId fromOther; + js::Thread thread; + CHECK(thread.init( + [](js::ThreadId* idp) { *idp = js::ThreadId::ThisThreadId(); }, + &fromOther)); + js::ThreadId fromMain = thread.get_id(); + thread.join(); + CHECK(fromOther == fromMain); + return true; +} +END_TEST(testThreadingThreadId) + +BEGIN_TEST(testThreadingThreadVectorMoveConstruct) { + const static size_t N = 10; + mozilla::Atomic<int> count(0); + mozilla::Vector<js::Thread, 0, js::SystemAllocPolicy> v; + for (auto i : mozilla::IntegerRange(N)) { + CHECK(v.emplaceBack()); + CHECK(v.back().init([](mozilla::Atomic<int>* countp) { (*countp)++; }, + &count)); + CHECK(v.length() == i + 1); + } + for (auto& th : v) { + th.join(); + } + CHECK(count == 10); + return true; +} +END_TEST(testThreadingThreadVectorMoveConstruct) + +// This test is checking that args are using "decay" copy, per spec. If we do +// not use decay copy properly, the rvalue reference |bool&& b| in the +// constructor will automatically become an lvalue reference |bool& b| in the +// trampoline, causing us to read through the reference when passing |bool bb| +// from the trampoline. If the parent runs before the child, the bool may have +// already become false, causing the trampoline to read the changed value, thus +// causing the child's assertion to fail. +BEGIN_TEST(testThreadingThreadArgCopy) { + for (size_t i = 0; i < 10000; ++i) { + bool b = true; + js::Thread thread; + CHECK(thread.init([](bool bb) { MOZ_RELEASE_ASSERT(bb); }, b)); + b = false; + thread.join(); + } + return true; +} +END_TEST(testThreadingThreadArgCopy) diff --git a/js/src/jsapi-tests/testToSignedOrUnsignedInteger.cpp b/js/src/jsapi-tests/testToSignedOrUnsignedInteger.cpp new file mode 100644 index 0000000000..6fc82a9445 --- /dev/null +++ b/js/src/jsapi-tests/testToSignedOrUnsignedInteger.cpp @@ -0,0 +1,68 @@ +/* -*- 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 <math.h> + +#include "js/Conversions.h" + +#include "jsapi-tests/tests.h" + +using JS::ToSignedInteger; +using JS::ToUnsignedInteger; + +BEGIN_TEST(testToUint8TwiceUint8Range) { + double d = -256; + uint8_t expected = 0; + do { + CHECK(ToUnsignedInteger<uint8_t>(d) == expected); + + d++; + expected++; + } while (d <= 256); + return true; +} +END_TEST(testToUint8TwiceUint8Range) + +BEGIN_TEST(testToInt8) { + double d = -128; + int8_t expected = -128; + do { + CHECK(ToSignedInteger<int8_t>(d) == expected); + + d++; + expected++; + } while (expected < 127); + return true; +} +END_TEST(testToInt8) + +BEGIN_TEST(testToUint32Large) { + CHECK(ToUnsignedInteger<uint32_t>(pow(2.0, 83)) == 0); + CHECK(ToUnsignedInteger<uint32_t>(pow(2.0, 83) + pow(2.0, 31)) == (1U << 31)); + CHECK(ToUnsignedInteger<uint32_t>(pow(2.0, 83) + 2 * pow(2.0, 31)) == 0); + CHECK(ToUnsignedInteger<uint32_t>(pow(2.0, 83) + 3 * pow(2.0, 31)) == + (1U << 31)); + CHECK(ToUnsignedInteger<uint32_t>(pow(2.0, 84)) == 0); + CHECK(ToUnsignedInteger<uint32_t>(pow(2.0, 84) + pow(2.0, 31)) == 0); + CHECK(ToUnsignedInteger<uint32_t>(pow(2.0, 84) + pow(2.0, 32)) == 0); + return true; +} +END_TEST(testToUint32Large) + +BEGIN_TEST(testToUint64Large) { + CHECK(ToUnsignedInteger<uint64_t>(pow(2.0, 115)) == 0); + CHECK(ToUnsignedInteger<uint64_t>(pow(2.0, 115) + pow(2.0, 63)) == + (1ULL << 63)); + CHECK(ToUnsignedInteger<uint64_t>(pow(2.0, 115) + 2 * pow(2.0, 63)) == 0); + CHECK(ToUnsignedInteger<uint64_t>(pow(2.0, 115) + 3 * pow(2.0, 63)) == + (1ULL << 63)); + CHECK(ToUnsignedInteger<uint64_t>(pow(2.0, 116)) == 0); + CHECK(ToUnsignedInteger<uint64_t>(pow(2.0, 116) + pow(2.0, 63)) == 0); + CHECK(ToUnsignedInteger<uint64_t>(pow(2.0, 116) + pow(2.0, 64)) == 0); + return true; +} +END_TEST(testToUint64Large) diff --git a/js/src/jsapi-tests/testTypedArrays.cpp b/js/src/jsapi-tests/testTypedArrays.cpp new file mode 100644 index 0000000000..0f5e316d4b --- /dev/null +++ b/js/src/jsapi-tests/testTypedArrays.cpp @@ -0,0 +1,341 @@ +/* -*- 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 "js/ArrayBuffer.h" // JS::{NewArrayBuffer,IsArrayBufferObject,GetArrayBuffer{ByteLength,Data}} +#include "js/experimental/TypedData.h" // JS_GetArrayBufferViewBuffer, JS_GetTypedArray{Length,ByteOffset,ByteLength}, JS_Get{{Ui,I}nt{8,16,32},Float{32,64},Uint8Clamped}ArrayData, JS_IsTypedArrayObject, JS_New{{Ui,I}nt{8,16,32},Float{32,64},Uint8Clamped}Array{,FromArray,WithBuffer} +#include "js/PropertyAndElement.h" // JS_GetElement, JS_SetElement +#include "js/SharedArrayBuffer.h" // JS::{NewSharedArrayBuffer,GetSharedArrayBufferData} +#include "jsapi-tests/tests.h" +#include "vm/Realm.h" + +using namespace js; + +BEGIN_TEST(testTypedArrays) { + bool ok = true; + + ok = ok && + TestPlainTypedArray<JS_NewInt8Array, int8_t, JS_GetInt8ArrayData>(cx) && + TestPlainTypedArray<JS_NewUint8Array, uint8_t, JS_GetUint8ArrayData>( + cx) && + TestPlainTypedArray<JS_NewUint8ClampedArray, uint8_t, + JS_GetUint8ClampedArrayData>(cx) && + TestPlainTypedArray<JS_NewInt16Array, int16_t, JS_GetInt16ArrayData>( + cx) && + TestPlainTypedArray<JS_NewUint16Array, uint16_t, JS_GetUint16ArrayData>( + cx) && + TestPlainTypedArray<JS_NewInt32Array, int32_t, JS_GetInt32ArrayData>( + cx) && + TestPlainTypedArray<JS_NewUint32Array, uint32_t, JS_GetUint32ArrayData>( + cx) && + TestPlainTypedArray<JS_NewFloat32Array, float, JS_GetFloat32ArrayData>( + cx) && + TestPlainTypedArray<JS_NewFloat64Array, double, JS_GetFloat64ArrayData>( + cx); + + size_t nbytes = sizeof(double) * 8; + RootedObject buffer(cx, JS::NewArrayBuffer(cx, nbytes)); + CHECK(JS::IsArrayBufferObject(buffer)); + + RootedObject proto(cx); + JS_GetPrototype(cx, buffer, &proto); + CHECK(!JS::IsArrayBufferObject(proto)); + + { + JS::AutoCheckCannotGC nogc; + bool isShared; + CHECK_EQUAL(JS::GetArrayBufferByteLength(buffer), nbytes); + memset(JS::GetArrayBufferData(buffer, &isShared, nogc), 1, nbytes); + CHECK(!isShared); // Because ArrayBuffer + } + + ok = + ok && + TestArrayFromBuffer<JS_NewInt8ArrayWithBuffer, JS_NewInt8ArrayFromArray, + int8_t, false, JS_GetInt8ArrayData>(cx) && + TestArrayFromBuffer<JS_NewUint8ArrayWithBuffer, JS_NewUint8ArrayFromArray, + uint8_t, false, JS_GetUint8ArrayData>(cx) && + TestArrayFromBuffer<JS_NewUint8ClampedArrayWithBuffer, + JS_NewUint8ClampedArrayFromArray, uint8_t, false, + JS_GetUint8ClampedArrayData>(cx) && + TestArrayFromBuffer<JS_NewInt16ArrayWithBuffer, JS_NewInt16ArrayFromArray, + int16_t, false, JS_GetInt16ArrayData>(cx) && + TestArrayFromBuffer<JS_NewUint16ArrayWithBuffer, + JS_NewUint16ArrayFromArray, uint16_t, false, + JS_GetUint16ArrayData>(cx) && + TestArrayFromBuffer<JS_NewInt32ArrayWithBuffer, JS_NewInt32ArrayFromArray, + int32_t, false, JS_GetInt32ArrayData>(cx) && + TestArrayFromBuffer<JS_NewUint32ArrayWithBuffer, + JS_NewUint32ArrayFromArray, uint32_t, false, + JS_GetUint32ArrayData>(cx) && + TestArrayFromBuffer<JS_NewFloat32ArrayWithBuffer, + JS_NewFloat32ArrayFromArray, float, false, + JS_GetFloat32ArrayData>(cx) && + TestArrayFromBuffer<JS_NewFloat64ArrayWithBuffer, + JS_NewFloat64ArrayFromArray, double, false, + JS_GetFloat64ArrayData>(cx); + + ok = + ok && + TestArrayFromBuffer<JS_NewInt8ArrayWithBuffer, JS_NewInt8ArrayFromArray, + int8_t, true, JS_GetInt8ArrayData>(cx) && + TestArrayFromBuffer<JS_NewUint8ArrayWithBuffer, JS_NewUint8ArrayFromArray, + uint8_t, true, JS_GetUint8ArrayData>(cx) && + TestArrayFromBuffer<JS_NewUint8ClampedArrayWithBuffer, + JS_NewUint8ClampedArrayFromArray, uint8_t, true, + JS_GetUint8ClampedArrayData>(cx) && + TestArrayFromBuffer<JS_NewInt16ArrayWithBuffer, JS_NewInt16ArrayFromArray, + int16_t, true, JS_GetInt16ArrayData>(cx) && + TestArrayFromBuffer<JS_NewUint16ArrayWithBuffer, + JS_NewUint16ArrayFromArray, uint16_t, true, + JS_GetUint16ArrayData>(cx) && + TestArrayFromBuffer<JS_NewInt32ArrayWithBuffer, JS_NewInt32ArrayFromArray, + int32_t, true, JS_GetInt32ArrayData>(cx) && + TestArrayFromBuffer<JS_NewUint32ArrayWithBuffer, + JS_NewUint32ArrayFromArray, uint32_t, true, + JS_GetUint32ArrayData>(cx) && + TestArrayFromBuffer<JS_NewFloat32ArrayWithBuffer, + JS_NewFloat32ArrayFromArray, float, true, + JS_GetFloat32ArrayData>(cx) && + TestArrayFromBuffer<JS_NewFloat64ArrayWithBuffer, + JS_NewFloat64ArrayFromArray, double, true, + JS_GetFloat64ArrayData>(cx); + + return ok; +} + +// Test pinning a view's length. +bool TestViewLengthPinning(Handle<JSObject*> view) { + // Pin the length of an inline view. (Fails if shared memory.) + bool isShared = view.as<NativeObject>()->isSharedMemory(); + CHECK(JS::PinArrayBufferOrViewLength(view, true) == !isShared); + + // Fail to pin an already-pinned length. + CHECK(!JS::PinArrayBufferOrViewLength(view, true)); + + // Extract an ArrayBuffer. This may cause it to be created, in which case it + // will inherit the pinned status from the view. + bool bufferIsShared; + Rooted<JSObject*> buffer( + cx, JS_GetArrayBufferViewBuffer(cx, view, &bufferIsShared)); + CHECK(isShared == bufferIsShared); + + // Cannot pin the buffer, since it is already pinned. + CHECK(!JS::PinArrayBufferOrViewLength(buffer, true)); + + // Should fail to be detached, since its length is pinned. + CHECK(!JS::DetachArrayBuffer(cx, buffer)); + CHECK(cx->isExceptionPending()); + cx->clearPendingException(); + + // Unpin (fails if shared memory). + CHECK(JS::PinArrayBufferOrViewLength(view, false) == !isShared); + + // Fail to unpin when already unpinned. + CHECK(!JS::PinArrayBufferOrViewLength(view, false)); + + return true; +} + +// Test pinning the length of an ArrayBuffer or SharedArrayBuffer. +bool TestBufferLengthPinning(Handle<JSObject*> buffer) { + // Pin the length of an inline view. (Fails if shared memory.) + bool isShared = !buffer->is<ArrayBufferObject>(); + CHECK(JS::PinArrayBufferOrViewLength(buffer, true) == !isShared); + + // Fail to pin an already-pinned length. + CHECK(!JS::PinArrayBufferOrViewLength(buffer, true)); + + // Should fail to be detached, since its length is pinned. + CHECK(!JS::DetachArrayBuffer(cx, buffer)); + CHECK(cx->isExceptionPending()); + cx->clearPendingException(); + + // Unpin (fails if shared memory). + CHECK(JS::PinArrayBufferOrViewLength(buffer, false) == !isShared); + + // Fail to unpin when already unpinned. + CHECK(!JS::PinArrayBufferOrViewLength(buffer, false)); + + return true; +} + +// Shared memory can only be mapped by a TypedArray by creating the +// TypedArray with a SharedArrayBuffer explicitly, so no tests here. + +template <JSObject* Create(JSContext*, size_t), typename Element, + Element* GetData(JSObject*, bool* isShared, + const JS::AutoRequireNoGC&)> +bool TestPlainTypedArray(JSContext* cx) { + { + RootedObject notArray(cx, Create(cx, SIZE_MAX)); + CHECK(!notArray); + JS_ClearPendingException(cx); + } + + RootedObject array(cx, Create(cx, 7)); + CHECK(JS_IsTypedArrayObject(array)); + RootedObject proto(cx); + JS_GetPrototype(cx, array, &proto); + CHECK(!JS_IsTypedArrayObject(proto)); + + CHECK_EQUAL(JS_GetTypedArrayLength(array), 7u); + CHECK_EQUAL(JS_GetTypedArrayByteOffset(array), 0u); + CHECK_EQUAL(JS_GetTypedArrayByteLength(array), sizeof(Element) * 7); + + TestViewLengthPinning(array); + + { + JS::AutoCheckCannotGC nogc; + Element* data; + bool isShared; + CHECK(data = GetData(array, &isShared, nogc)); + CHECK(!isShared); // Because ArrayBuffer + *data = 13; + } + RootedValue v(cx); + CHECK(JS_GetElement(cx, array, 0, &v)); + CHECK_SAME(v, Int32Value(13)); + + return true; +} + +template < + JSObject* CreateWithBuffer(JSContext*, JS::HandleObject, size_t, int64_t), + JSObject* CreateFromArray(JSContext*, JS::HandleObject), typename Element, + bool Shared, Element* GetData(JSObject*, bool*, const JS::AutoRequireNoGC&)> +bool TestArrayFromBuffer(JSContext* cx) { + if (Shared && + !cx->realm()->creationOptions().getSharedMemoryAndAtomicsEnabled()) { + return true; + } + + size_t elts = 8; + size_t nbytes = elts * sizeof(Element); + RootedObject buffer(cx, Shared ? JS::NewSharedArrayBuffer(cx, nbytes) + : JS::NewArrayBuffer(cx, nbytes)); + + TestBufferLengthPinning(buffer); + + { + JS::AutoCheckCannotGC nogc; + bool isShared; + void* data = Shared ? JS::GetSharedArrayBufferData(buffer, &isShared, nogc) + : JS::GetArrayBufferData(buffer, &isShared, nogc); + CHECK_EQUAL(Shared, isShared); + memset(data, 1, nbytes); + } + + { + RootedObject notArray(cx, CreateWithBuffer(cx, buffer, UINT32_MAX, -1)); + CHECK(!notArray); + JS_ClearPendingException(cx); + } + + RootedObject array(cx, CreateWithBuffer(cx, buffer, 0, -1)); + CHECK_EQUAL(JS_GetTypedArrayLength(array), elts); + CHECK_EQUAL(JS_GetTypedArrayByteOffset(array), 0u); + CHECK_EQUAL(JS_GetTypedArrayByteLength(array), nbytes); + { + bool isShared; + CHECK_EQUAL(JS_GetArrayBufferViewBuffer(cx, array, &isShared), + (JSObject*)buffer); + CHECK_EQUAL(Shared, isShared); + } + + TestViewLengthPinning(array); + + { + JS::AutoCheckCannotGC nogc; + Element* data; + bool isShared; + + CHECK(data = GetData(array, &isShared, nogc)); + CHECK_EQUAL(Shared, isShared); + + CHECK_EQUAL( + (void*)data, + Shared ? (void*)JS::GetSharedArrayBufferData(buffer, &isShared, nogc) + : (void*)JS::GetArrayBufferData(buffer, &isShared, nogc)); + CHECK_EQUAL(Shared, isShared); + + CHECK_EQUAL(*reinterpret_cast<uint8_t*>(data), 1u); + } + + RootedObject shortArray(cx, CreateWithBuffer(cx, buffer, 0, elts / 2)); + CHECK_EQUAL(JS_GetTypedArrayLength(shortArray), elts / 2); + CHECK_EQUAL(JS_GetTypedArrayByteOffset(shortArray), 0u); + CHECK_EQUAL(JS_GetTypedArrayByteLength(shortArray), nbytes / 2); + + RootedObject ofsArray(cx, CreateWithBuffer(cx, buffer, nbytes / 2, -1)); + CHECK_EQUAL(JS_GetTypedArrayLength(ofsArray), elts / 2); + CHECK_EQUAL(JS_GetTypedArrayByteOffset(ofsArray), nbytes / 2); + CHECK_EQUAL(JS_GetTypedArrayByteLength(ofsArray), nbytes / 2); + + // Make sure all 3 views reflect the same buffer at the expected locations + JS::RootedValue v(cx, JS::Int32Value(39)); + CHECK(JS_SetElement(cx, array, 0, v)); + JS::RootedValue v2(cx); + CHECK(JS_GetElement(cx, array, 0, &v2)); + CHECK_SAME(v, v2); + CHECK(JS_GetElement(cx, shortArray, 0, &v2)); + CHECK_SAME(v, v2); + { + JS::AutoCheckCannotGC nogc; + Element* data; + bool isShared; + CHECK(data = GetData(array, &isShared, nogc)); + CHECK_EQUAL(Shared, isShared); + CHECK_EQUAL(long(v.toInt32()), long(reinterpret_cast<Element*>(data)[0])); + } + + v.setInt32(40); + CHECK(JS_SetElement(cx, array, elts / 2, v)); + CHECK(JS_GetElement(cx, array, elts / 2, &v2)); + CHECK_SAME(v, v2); + CHECK(JS_GetElement(cx, ofsArray, 0, &v2)); + CHECK_SAME(v, v2); + { + JS::AutoCheckCannotGC nogc; + Element* data; + bool isShared; + CHECK(data = GetData(array, &isShared, nogc)); + CHECK_EQUAL(Shared, isShared); + CHECK_EQUAL(long(v.toInt32()), + long(reinterpret_cast<Element*>(data)[elts / 2])); + } + + v.setInt32(41); + CHECK(JS_SetElement(cx, array, elts - 1, v)); + CHECK(JS_GetElement(cx, array, elts - 1, &v2)); + CHECK_SAME(v, v2); + CHECK(JS_GetElement(cx, ofsArray, elts / 2 - 1, &v2)); + CHECK_SAME(v, v2); + { + JS::AutoCheckCannotGC nogc; + Element* data; + bool isShared; + CHECK(data = GetData(array, &isShared, nogc)); + CHECK_EQUAL(Shared, isShared); + CHECK_EQUAL(long(v.toInt32()), + long(reinterpret_cast<Element*>(data)[elts - 1])); + } + + JS::RootedObject copy(cx, CreateFromArray(cx, array)); + CHECK(JS_GetElement(cx, array, 0, &v)); + CHECK(JS_GetElement(cx, copy, 0, &v2)); + CHECK_SAME(v, v2); + + /* The copy should not see changes in the original */ + v2.setInt32(42); + CHECK(JS_SetElement(cx, array, 0, v2)); + CHECK(JS_GetElement(cx, copy, 0, &v2)); + CHECK_SAME(v2, v); /* v is still the original value from 'array' */ + + return true; +} + +END_TEST(testTypedArrays) diff --git a/js/src/jsapi-tests/testUTF8.cpp b/js/src/jsapi-tests/testUTF8.cpp new file mode 100644 index 0000000000..b32a6dd4b9 --- /dev/null +++ b/js/src/jsapi-tests/testUTF8.cpp @@ -0,0 +1,231 @@ +/* -*- 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 "mozilla/Range.h" // mozilla::Range +#include "mozilla/Span.h" // mozilla::Span +#include "mozilla/Utf8.h" // mozilla::ConvertUtf8toUtf16 + +#include "js/CharacterEncoding.h" +#include "jsapi-tests/tests.h" + +BEGIN_TEST(testUTF8_badUTF8) { + static const char badUTF8[] = "...\xC0..."; + JSString* str = JS_NewStringCopyZ(cx, badUTF8); + CHECK(str); + char16_t ch; + if (!JS_GetStringCharAt(cx, str, 3, &ch)) { + return false; + } + CHECK(ch == 0x00C0); + return true; +} +END_TEST(testUTF8_badUTF8) + +BEGIN_TEST(testUTF8_bigUTF8) { + static const char bigUTF8[] = "...\xFB\xBF\xBF\xBF\xBF..."; + JSString* str = JS_NewStringCopyZ(cx, bigUTF8); + CHECK(str); + char16_t ch; + if (!JS_GetStringCharAt(cx, str, 3, &ch)) { + return false; + } + CHECK(ch == 0x00FB); + return true; +} +END_TEST(testUTF8_bigUTF8) + +BEGIN_TEST(testUTF8_badSurrogate) { + static const char16_t badSurrogate[] = {'A', 'B', 'C', 0xDEEE, 'D', 'E', 0}; + mozilla::Range<const char16_t> tbchars(badSurrogate, js_strlen(badSurrogate)); + JS::Latin1CharsZ latin1 = JS::LossyTwoByteCharsToNewLatin1CharsZ(cx, tbchars); + CHECK(latin1); + CHECK(latin1[3] == 0x00EE); + return true; +} +END_TEST(testUTF8_badSurrogate) + +BEGIN_TEST(testUTF8_LossyConversion) { + // Maximal subparts of an ill-formed subsequence should be replaced with + // single REPLACEMENT CHARACTER. + + // Input ends with partial sequence. + // clang-format off + const char* inputs1[] = { + "\xC2", + "\xDF", + "\xE0", + "\xE0\xA0", + "\xF0", + "\xF0\x90", + "\xF0\x90\x80", + }; + // clang-format on + + char16_t outputBuf[8]; + mozilla::Span output(outputBuf, 8); + + for (const char* input : inputs1) { + size_t len; + JS::TwoByteCharsZ utf16 = JS::LossyUTF8CharsToNewTwoByteCharsZ( + cx, JS::UTF8Chars(input, js_strlen(input)), &len, + js::StringBufferArena); + CHECK(utf16); + CHECK(len == 1); + CHECK(utf16[0] == 0xFFFD); + + // Make sure the behavior matches to encoding_rs. + len = mozilla::ConvertUtf8toUtf16(mozilla::Span(input, js_strlen(input)), + output); + CHECK(len == 1); + CHECK(outputBuf[0] == 0xFFFD); + } + + // Partial sequence followed by ASCII range. + // clang-format off + const char* inputs2[] = { + "\xC2 ", + "\xDF ", + "\xE0 ", + "\xE0\xA0 ", + "\xF0 ", + "\xF0\x90 ", + "\xF0\x90\x80 ", + }; + // clang-format on + + for (const char* input : inputs2) { + size_t len; + JS::TwoByteCharsZ utf16 = JS::LossyUTF8CharsToNewTwoByteCharsZ( + cx, JS::UTF8Chars(input, js_strlen(input)), &len, + js::StringBufferArena); + CHECK(utf16); + CHECK(len == 2); + CHECK(utf16[0] == 0xFFFD); + CHECK(utf16[1] == 0x20); + + len = mozilla::ConvertUtf8toUtf16(mozilla::Span(input, js_strlen(input)), + output); + CHECK(len == 2); + CHECK(outputBuf[0] == 0xFFFD); + CHECK(outputBuf[1] == 0x20); + } + + // Partial sequence followed by other first code unit. + // clang-format off + const char* inputs3[] = { + "\xC2\xC2\x80", + "\xDF\xC2\x80", + "\xE0\xC2\x80", + "\xE0\xA0\xC2\x80", + "\xF0\xC2\x80", + "\xF0\x90\xC2\x80", + "\xF0\x90\x80\xC2\x80", + }; + // clang-format on + + for (const char* input : inputs3) { + size_t len; + JS::TwoByteCharsZ utf16 = JS::LossyUTF8CharsToNewTwoByteCharsZ( + cx, JS::UTF8Chars(input, js_strlen(input)), &len, + js::StringBufferArena); + CHECK(utf16); + CHECK(len == 2); + CHECK(utf16[0] == 0xFFFD); + CHECK(utf16[1] == 0x80); + + len = mozilla::ConvertUtf8toUtf16(mozilla::Span(input, js_strlen(input)), + output); + CHECK(len == 2); + CHECK(outputBuf[0] == 0xFFFD); + CHECK(outputBuf[1] == 0x80); + } + + // Invalid second byte. + // clang-format off + const char* inputs4[] = { + "\xE0\x9F\x80\x80", + "\xED\xA0\x80\x80", + "\xF0\x80\x80\x80", + "\xF4\x90\x80\x80", + }; + // clang-format on + + for (const char* input : inputs4) { + size_t len; + JS::TwoByteCharsZ utf16 = JS::LossyUTF8CharsToNewTwoByteCharsZ( + cx, JS::UTF8Chars(input, js_strlen(input)), &len, + js::StringBufferArena); + CHECK(utf16); + CHECK(len == 4); + CHECK(utf16[0] == 0xFFFD); + CHECK(utf16[1] == 0xFFFD); + CHECK(utf16[2] == 0xFFFD); + CHECK(utf16[3] == 0xFFFD); + + len = mozilla::ConvertUtf8toUtf16(mozilla::Span(input, js_strlen(input)), + output); + CHECK(len == 4); + CHECK(outputBuf[0] == 0xFFFD); + CHECK(outputBuf[1] == 0xFFFD); + CHECK(outputBuf[2] == 0xFFFD); + CHECK(outputBuf[3] == 0xFFFD); + } + + // Invalid second byte, with not sufficient number of units. + // clang-format off + const char* inputs5[] = { + "\xE0\x9F\x80", + "\xED\xA0\x80", + "\xF0\x80\x80", + "\xF4\x90\x80", + }; + const char* inputs6[] = { + "\xE0\x9F", + "\xED\xA0", + "\xF0\x80", + "\xF4\x90", + }; + // clang-format on + + for (const char* input : inputs5) { + size_t len; + JS::TwoByteCharsZ utf16 = JS::LossyUTF8CharsToNewTwoByteCharsZ( + cx, JS::UTF8Chars(input, js_strlen(input)), &len, + js::StringBufferArena); + CHECK(utf16); + CHECK(len == 3); + CHECK(utf16[0] == 0xFFFD); + CHECK(utf16[1] == 0xFFFD); + CHECK(utf16[2] == 0xFFFD); + + len = mozilla::ConvertUtf8toUtf16(mozilla::Span(input, js_strlen(input)), + output); + CHECK(len == 3); + CHECK(outputBuf[0] == 0xFFFD); + CHECK(outputBuf[1] == 0xFFFD); + CHECK(outputBuf[2] == 0xFFFD); + } + + for (const char* input : inputs6) { + size_t len; + JS::TwoByteCharsZ utf16 = JS::LossyUTF8CharsToNewTwoByteCharsZ( + cx, JS::UTF8Chars(input, js_strlen(input)), &len, + js::StringBufferArena); + CHECK(utf16); + CHECK(len == 2); + CHECK(utf16[0] == 0xFFFD); + CHECK(utf16[1] == 0xFFFD); + + len = mozilla::ConvertUtf8toUtf16(mozilla::Span(input, js_strlen(input)), + output); + CHECK(len == 2); + CHECK(outputBuf[0] == 0xFFFD); + CHECK(outputBuf[1] == 0xFFFD); + } + return true; +} +END_TEST(testUTF8_LossyConversion) diff --git a/js/src/jsapi-tests/testUbiNode.cpp b/js/src/jsapi-tests/testUbiNode.cpp new file mode 100644 index 0000000000..bbe11d928b --- /dev/null +++ b/js/src/jsapi-tests/testUbiNode.cpp @@ -0,0 +1,971 @@ +/* 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 "mozilla/Utf8.h" // mozilla::Utf8Unit + +#include "builtin/TestingFunctions.h" +#include "js/CompilationAndEvaluation.h" // JS::Compile +#include "js/GlobalObject.h" // JS_NewGlobalObject +#include "js/SourceText.h" // JS::Source{Ownership,Text} +#include "js/UbiNode.h" +#include "js/UbiNodeDominatorTree.h" +#include "js/UbiNodePostOrder.h" +#include "js/UbiNodeShortestPaths.h" +#include "jsapi-tests/tests.h" +#include "util/Text.h" +#include "vm/Compartment.h" +#include "vm/Realm.h" +#include "vm/SavedFrame.h" + +#include "vm/JSObject-inl.h" + +using JS::RootedObject; +using JS::RootedScript; +using JS::RootedString; +using namespace js; + +// A helper JS::ubi::Node concrete implementation that can be used to make mock +// graphs for testing traversals with. +struct FakeNode { + char name; + JS::ubi::EdgeVector edges; + + explicit FakeNode(char name) : name(name), edges() {} + + bool addEdgeTo(FakeNode& referent, const char16_t* edgeName = nullptr) { + JS::ubi::Node node(&referent); + + if (edgeName) { + auto ownedName = js::DuplicateString(edgeName); + MOZ_RELEASE_ASSERT(ownedName); + return edges.emplaceBack(ownedName.release(), node); + } + + return edges.emplaceBack(nullptr, node); + } +}; + +namespace JS { +namespace ubi { + +template <> +class Concrete<FakeNode> : public Base { + protected: + explicit Concrete(FakeNode* ptr) : Base(ptr) {} + FakeNode& get() const { return *static_cast<FakeNode*>(ptr); } + + public: + static void construct(void* storage, FakeNode* ptr) { + new (storage) Concrete(ptr); + } + + UniquePtr<EdgeRange> edges(JSContext* cx, bool wantNames) const override { + return UniquePtr<EdgeRange>(js_new<PreComputedEdgeRange>(get().edges)); + } + + Node::Size size(mozilla::MallocSizeOf) const override { return 1; } + + static const char16_t concreteTypeName[]; + const char16_t* typeName() const override { return concreteTypeName; } +}; + +const char16_t Concrete<FakeNode>::concreteTypeName[] = u"FakeNode"; + +} // namespace ubi +} // namespace JS + +// ubi::Node::zone works +BEGIN_TEST(test_ubiNodeZone) { + RootedObject global1(cx, JS::CurrentGlobalOrNull(cx)); + CHECK(global1); + CHECK(JS::ubi::Node(global1).zone() == cx->zone()); + + JS::RealmOptions globalOptions; + RootedObject global2( + cx, JS_NewGlobalObject(cx, getGlobalClass(), nullptr, + JS::FireOnNewGlobalHook, globalOptions)); + CHECK(global2); + CHECK(global1->zone() != global2->zone()); + CHECK(JS::ubi::Node(global2).zone() == global2->zone()); + CHECK(JS::ubi::Node(global2).zone() != global1->zone()); + + JS::CompileOptions options(cx); + + // Create a string and a script in the original zone... + RootedString string1( + cx, JS_NewStringCopyZ(cx, "Simpson's Individual Stringettes!")); + CHECK(string1); + + JS::SourceText<mozilla::Utf8Unit> emptySrcBuf; + CHECK(emptySrcBuf.init(cx, "", 0, JS::SourceOwnership::Borrowed)); + + RootedScript script1(cx, JS::Compile(cx, options, emptySrcBuf)); + CHECK(script1); + + { + // ... and then enter global2's zone and create a string and script + // there, too. + JSAutoRealm ar(cx, global2); + + RootedString string2(cx, + JS_NewStringCopyZ(cx, "A million household uses!")); + CHECK(string2); + RootedScript script2(cx, JS::Compile(cx, options, emptySrcBuf)); + CHECK(script2); + + CHECK(JS::ubi::Node(string1).zone() == global1->zone()); + CHECK(JS::ubi::Node(script1).zone() == global1->zone()); + + CHECK(JS::ubi::Node(string2).zone() == global2->zone()); + CHECK(JS::ubi::Node(script2).zone() == global2->zone()); + } + + return true; +} +END_TEST(test_ubiNodeZone) + +// ubi::Node::compartment works +BEGIN_TEST(test_ubiNodeCompartment) { + RootedObject global1(cx, JS::CurrentGlobalOrNull(cx)); + CHECK(global1); + CHECK(JS::ubi::Node(global1).compartment() == cx->compartment()); + CHECK(JS::ubi::Node(global1).realm() == cx->realm()); + + JS::RealmOptions globalOptions; + RootedObject global2( + cx, JS_NewGlobalObject(cx, getGlobalClass(), nullptr, + JS::FireOnNewGlobalHook, globalOptions)); + CHECK(global2); + CHECK(global1->compartment() != global2->compartment()); + CHECK(JS::ubi::Node(global2).compartment() == global2->compartment()); + CHECK(JS::ubi::Node(global2).compartment() != global1->compartment()); + CHECK(JS::ubi::Node(global2).realm() == global2->nonCCWRealm()); + CHECK(JS::ubi::Node(global2).realm() != global1->nonCCWRealm()); + + JS::CompileOptions options(cx); + + JS::SourceText<mozilla::Utf8Unit> emptySrcBuf; + CHECK(emptySrcBuf.init(cx, "", 0, JS::SourceOwnership::Borrowed)); + + // Create a script in the original realm... + RootedScript script1(cx, JS::Compile(cx, options, emptySrcBuf)); + CHECK(script1); + + { + // ... and then enter global2's realm and create a script + // there, too. + JSAutoRealm ar(cx, global2); + + RootedScript script2(cx, JS::Compile(cx, options, emptySrcBuf)); + CHECK(script2); + + CHECK(JS::ubi::Node(script1).compartment() == global1->compartment()); + CHECK(JS::ubi::Node(script2).compartment() == global2->compartment()); + CHECK(JS::ubi::Node(script1).realm() == global1->nonCCWRealm()); + CHECK(JS::ubi::Node(script2).realm() == global2->nonCCWRealm()); + + // Now create a wrapper for global1 in global2's compartment. + RootedObject wrappedGlobal1(cx, global1); + CHECK(cx->compartment()->wrap(cx, &wrappedGlobal1)); + + // Cross-compartment wrappers have a compartment() but not a realm(). + CHECK(JS::ubi::Node(wrappedGlobal1).zone() == cx->zone()); + CHECK(JS::ubi::Node(wrappedGlobal1).compartment() == cx->compartment()); + CHECK(JS::ubi::Node(wrappedGlobal1).realm() == nullptr); + } + + return true; +} +END_TEST(test_ubiNodeCompartment) + +template <typename F, typename G> +static bool checkString(const char* expected, F fillBufferFunction, + G stringGetterFunction) { + auto expectedLength = strlen(expected); + char16_t buf[1024]; + if (fillBufferFunction(mozilla::RangedPtr<char16_t>(buf, 1024), 1024) != + expectedLength || + !EqualChars(buf, expected, expectedLength)) { + return false; + } + + auto string = stringGetterFunction(); + // Expecting a |JSAtom*| from a live |JS::ubi::StackFrame|. + if (!string.template is<JSAtom*>() || + !StringEqualsAscii(string.template as<JSAtom*>(), expected)) { + return false; + } + + return true; +} + +BEGIN_TEST(test_ubiStackFrame) { + CHECK(js::DefineTestingFunctions(cx, global, false, false)); + + JS::RootedValue val(cx); + CHECK( + evaluate("(function one() { \n" // 1 + " return (function two() { \n" // 2 + " return (function three() { \n" // 3 + " return saveStack(); \n" // 4 + " }()); \n" // 5 + " }()); \n" // 6 + "}()); \n", // 7 + "filename.js", 1, &val)); + + CHECK(val.isObject()); + JS::RootedObject obj(cx, &val.toObject()); + + CHECK(obj->is<SavedFrame>()); + JS::Rooted<SavedFrame*> savedFrame(cx, &obj->as<SavedFrame>()); + + JS::ubi::StackFrame ubiFrame(savedFrame); + + // All frames should be from the "filename.js" source. + while (ubiFrame) { + CHECK(checkString( + "filename.js", + [&](mozilla::RangedPtr<char16_t> ptr, size_t length) { + return ubiFrame.source(ptr, length); + }, + [&] { return ubiFrame.source(); })); + ubiFrame = ubiFrame.parent(); + } + + ubiFrame = savedFrame; + + auto bufferFunctionDisplayName = [&](mozilla::RangedPtr<char16_t> ptr, + size_t length) { + return ubiFrame.functionDisplayName(ptr, length); + }; + auto getFunctionDisplayName = [&] { return ubiFrame.functionDisplayName(); }; + + CHECK( + checkString("three", bufferFunctionDisplayName, getFunctionDisplayName)); + CHECK(ubiFrame.line() == 4); + + ubiFrame = ubiFrame.parent(); + CHECK(checkString("two", bufferFunctionDisplayName, getFunctionDisplayName)); + CHECK(ubiFrame.line() == 5); + + ubiFrame = ubiFrame.parent(); + CHECK(checkString("one", bufferFunctionDisplayName, getFunctionDisplayName)); + CHECK(ubiFrame.line() == 6); + + ubiFrame = ubiFrame.parent(); + CHECK(ubiFrame.functionDisplayName().is<JSAtom*>()); + CHECK(ubiFrame.functionDisplayName().as<JSAtom*>() == nullptr); + CHECK(ubiFrame.line() == 7); + + ubiFrame = ubiFrame.parent(); + CHECK(!ubiFrame); + + return true; +} +END_TEST(test_ubiStackFrame) + +BEGIN_TEST(test_ubiCoarseType) { + // Test that our explicit coarseType() overrides work as expected. + + JSObject* obj = nullptr; + CHECK(JS::ubi::Node(obj).coarseType() == JS::ubi::CoarseType::Object); + + JSScript* script = nullptr; + CHECK(JS::ubi::Node(script).coarseType() == JS::ubi::CoarseType::Script); + + js::BaseScript* baseScript = nullptr; + CHECK(JS::ubi::Node(baseScript).coarseType() == JS::ubi::CoarseType::Script); + + js::jit::JitCode* jitCode = nullptr; + CHECK(JS::ubi::Node(jitCode).coarseType() == JS::ubi::CoarseType::Script); + + JSString* str = nullptr; + CHECK(JS::ubi::Node(str).coarseType() == JS::ubi::CoarseType::String); + + // Test that the default when coarseType() is not overridden is Other. + + JS::Symbol* sym = nullptr; + CHECK(JS::ubi::Node(sym).coarseType() == JS::ubi::CoarseType::Other); + + return true; +} +END_TEST(test_ubiCoarseType) + +struct ExpectedEdge { + char from; + char to; + + ExpectedEdge(FakeNode& fromNode, FakeNode& toNode) + : from(fromNode.name), to(toNode.name) {} +}; + +namespace mozilla { + +template <> +struct DefaultHasher<ExpectedEdge> { + using Lookup = ExpectedEdge; + + static HashNumber hash(const Lookup& l) { + return mozilla::AddToHash(l.from, l.to); + } + + static bool match(const ExpectedEdge& k, const Lookup& l) { + return k.from == l.from && k.to == l.to; + } +}; + +} // namespace mozilla + +BEGIN_TEST(test_ubiPostOrder) { + // Construct the following graph: + // + // .-----. + // | | + // .-------| r |---------------. + // | | | | + // | '-----' | + // | | + // .--V--. .--V--. + // | | | | + // .------| a |------. .----| e |----. + // | | | | | | | | + // | '--^--' | | '-----' | + // | | | | | + // .--V--. | .--V--. .--V--. .--V--. + // | | | | | | | | | + // | b | '------| c |-----> f |---------> g | + // | | | | | | | | + // '-----' '-----' '-----' '-----' + // | | + // | .-----. | + // | | | | + // '------> d <------' + // | | + // '-----' + // + + FakeNode r('r'); + FakeNode a('a'); + FakeNode b('b'); + FakeNode c('c'); + FakeNode d('d'); + FakeNode e('e'); + FakeNode f('f'); + FakeNode g('g'); + + js::HashSet<ExpectedEdge> expectedEdges(cx); + + auto declareEdge = [&](FakeNode& from, FakeNode& to) { + return from.addEdgeTo(to) && expectedEdges.putNew(ExpectedEdge(from, to)); + }; + + CHECK(declareEdge(r, a)); + CHECK(declareEdge(r, e)); + CHECK(declareEdge(a, b)); + CHECK(declareEdge(a, c)); + CHECK(declareEdge(b, d)); + CHECK(declareEdge(c, a)); + CHECK(declareEdge(c, d)); + CHECK(declareEdge(c, f)); + CHECK(declareEdge(e, f)); + CHECK(declareEdge(e, g)); + CHECK(declareEdge(f, g)); + + js::Vector<char, 8, js::SystemAllocPolicy> visited; + { + // Do a PostOrder traversal, starting from r. Accumulate the names of + // the nodes we visit in `visited`. Remove edges we traverse from + // `expectedEdges` as we find them to ensure that we only find each edge + // once. + + JS::AutoCheckCannotGC nogc(cx); + JS::ubi::PostOrder traversal(cx, nogc); + CHECK(traversal.addStart(&r)); + + auto onNode = [&](const JS::ubi::Node& node) { + return visited.append(node.as<FakeNode>()->name); + }; + + auto onEdge = [&](const JS::ubi::Node& origin, const JS::ubi::Edge& edge) { + ExpectedEdge e(*origin.as<FakeNode>(), *edge.referent.as<FakeNode>()); + if (!expectedEdges.has(e)) { + fprintf(stderr, "Error: Unexpected edge from %c to %c!\n", + origin.as<FakeNode>()->name, + edge.referent.as<FakeNode>()->name); + return false; + } + + expectedEdges.remove(e); + return true; + }; + + CHECK(traversal.traverse(onNode, onEdge)); + } + + fprintf(stderr, "visited.length() = %lu\n", (unsigned long)visited.length()); + for (size_t i = 0; i < visited.length(); i++) { + fprintf(stderr, "visited[%lu] = '%c'\n", (unsigned long)i, visited[i]); + } + + CHECK(visited.length() == 8); + CHECK(visited[0] == 'g'); + CHECK(visited[1] == 'f'); + CHECK(visited[2] == 'e'); + CHECK(visited[3] == 'd'); + CHECK(visited[4] == 'c'); + CHECK(visited[5] == 'b'); + CHECK(visited[6] == 'a'); + CHECK(visited[7] == 'r'); + + // We found all the edges we expected. + CHECK(expectedEdges.count() == 0); + + return true; +} +END_TEST(test_ubiPostOrder) + +BEGIN_TEST(test_JS_ubi_DominatorTree) { + // Construct the following graph: + // + // .-----. + // | <--------------------------------. + // .--------+--------------| r |--------------. | + // | | | | | | + // | | '-----' | | + // | .--V--. .--V--. | + // | | | | | | + // | | b | | c |--------. | + // | | | | | | | + // | '-----' '-----' | | + // .--V--. | | .--V--. | + // | | | | | | | + // | a <-----+ | .----| g | | + // | | | .----' | | | | + // '-----' | | | '-----' | + // | | | | | | + // .--V--. | .-----. .--V--. | | | + // | | | | | | | | | | + // | d <-----+----> e <----. | f | | | | + // | | | | | | | | | | + // '-----' '-----' | '-----' | | | + // | .-----. | | | | .--V--. | + // | | | | | | .-' | | | + // '-----> l | | | | | | j | | + // | | '--. | | | | | | + // '-----' | | | | '-----' | + // | .--V--. | | .--V--. | | + // | | | | | | | | | + // '-------> h |-' '---> i <------' | + // | | .---------> | | + // '-----' | '-----' | + // | .-----. | | + // | | | | | + // '----------> k <---------' | + // | | | + // '-----' | + // | | + // '----------------------------' + // + // This graph has the following dominator tree: + // + // r + // |-- a + // |-- b + // |-- c + // | |-- f + // | `-- g + // | `-- j + // |-- d + // | `-- l + // |-- e + // |-- i + // |-- k + // `-- h + // + // This graph and dominator tree are taken from figures 1 and 2 of "A Fast + // Algorithm for Finding Dominators in a Flowgraph" by Lengauer et al: + // http://www.cs.princeton.edu/courses/archive/spr03/cs423/download/dominators.pdf. + + FakeNode r('r'); + FakeNode a('a'); + FakeNode b('b'); + FakeNode c('c'); + FakeNode d('d'); + FakeNode e('e'); + FakeNode f('f'); + FakeNode g('g'); + FakeNode h('h'); + FakeNode i('i'); + FakeNode j('j'); + FakeNode k('k'); + FakeNode l('l'); + + CHECK(r.addEdgeTo(a)); + CHECK(r.addEdgeTo(b)); + CHECK(r.addEdgeTo(c)); + CHECK(a.addEdgeTo(d)); + CHECK(b.addEdgeTo(a)); + CHECK(b.addEdgeTo(d)); + CHECK(b.addEdgeTo(e)); + CHECK(c.addEdgeTo(f)); + CHECK(c.addEdgeTo(g)); + CHECK(d.addEdgeTo(l)); + CHECK(e.addEdgeTo(h)); + CHECK(f.addEdgeTo(i)); + CHECK(g.addEdgeTo(i)); + CHECK(g.addEdgeTo(j)); + CHECK(h.addEdgeTo(e)); + CHECK(h.addEdgeTo(k)); + CHECK(i.addEdgeTo(k)); + CHECK(j.addEdgeTo(i)); + CHECK(k.addEdgeTo(r)); + CHECK(k.addEdgeTo(i)); + CHECK(l.addEdgeTo(h)); + + mozilla::Maybe<JS::ubi::DominatorTree> maybeTree; + { + JS::AutoCheckCannotGC noGC(cx); + maybeTree = JS::ubi::DominatorTree::Create(cx, noGC, &r); + } + + CHECK(maybeTree.isSome()); + auto& tree = *maybeTree; + + // We return the null JS::ubi::Node for nodes that were not reachable in the + // graph when computing the dominator tree. + FakeNode m('m'); + CHECK(tree.getImmediateDominator(&m) == JS::ubi::Node()); + CHECK(tree.getDominatedSet(&m).isNothing()); + + struct { + FakeNode& dominated; + FakeNode& dominator; + } domination[] = {{r, r}, {a, r}, {b, r}, {c, r}, {d, r}, {e, r}, {f, c}, + {g, c}, {h, r}, {i, r}, {j, g}, {k, r}, {l, d}}; + + for (auto& relation : domination) { + // Test immediate dominator. + fprintf( + stderr, "%c's immediate dominator is %c\n", relation.dominated.name, + tree.getImmediateDominator(&relation.dominator).as<FakeNode>()->name); + CHECK(tree.getImmediateDominator(&relation.dominated) == + JS::ubi::Node(&relation.dominator)); + + // Test the dominated set. Build up the expected dominated set based on + // the set of nodes immediately dominated by this one in `domination`, + // then iterate over the actual dominated set and check against the + // expected set. + + auto& node = relation.dominated; + fprintf(stderr, "Checking %c's dominated set:\n", node.name); + + js::HashSet<char> expectedDominatedSet(cx); + for (auto& rel : domination) { + if (&rel.dominator == &node) { + fprintf(stderr, " Expecting %c\n", rel.dominated.name); + CHECK(expectedDominatedSet.putNew(rel.dominated.name)); + } + } + + auto maybeActualDominatedSet = tree.getDominatedSet(&node); + CHECK(maybeActualDominatedSet.isSome()); + auto& actualDominatedSet = *maybeActualDominatedSet; + + for (const auto& dominated : actualDominatedSet) { + fprintf(stderr, " Found %c\n", dominated.as<FakeNode>()->name); + CHECK(expectedDominatedSet.has(dominated.as<FakeNode>()->name)); + expectedDominatedSet.remove(dominated.as<FakeNode>()->name); + } + + // Ensure we found them all and aren't still expecting nodes we never + // got. + CHECK(expectedDominatedSet.count() == 0); + + fprintf(stderr, "Done checking %c's dominated set.\n\n", node.name); + } + + struct { + FakeNode& node; + JS::ubi::Node::Size retainedSize; + } sizes[] = { + {r, 13}, {a, 1}, {b, 1}, {c, 4}, {d, 2}, {e, 1}, {f, 1}, + {g, 2}, {h, 1}, {i, 1}, {j, 1}, {k, 1}, {l, 1}, + }; + + for (auto& expected : sizes) { + JS::ubi::Node::Size actual = 0; + CHECK(tree.getRetainedSize(&expected.node, nullptr, actual)); + CHECK(actual == expected.retainedSize); + } + + return true; +} +END_TEST(test_JS_ubi_DominatorTree) + +BEGIN_TEST(test_JS_ubi_Node_scriptFilename) { + JS::RootedValue val(cx); + CHECK( + evaluate("(function one() { \n" // 1 + " return (function two() { \n" // 2 + " return (function three() { \n" // 3 + " return function four() {}; \n" // 4 + " }()); \n" // 5 + " }()); \n" // 6 + "}()); \n", // 7 + "my-cool-filename.js", 1, &val)); + + CHECK(val.isObject()); + JS::RootedObject obj(cx, &val.toObject()); + + CHECK(obj->is<JSFunction>()); + JS::RootedFunction func(cx, &obj->as<JSFunction>()); + + JS::RootedScript script(cx, JSFunction::getOrCreateScript(cx, func)); + CHECK(script); + CHECK(script->filename()); + + JS::ubi::Node node(script); + const char* filename = node.scriptFilename(); + CHECK(filename); + CHECK(strcmp(filename, script->filename()) == 0); + CHECK(strcmp(filename, "my-cool-filename.js") == 0); + + return true; +} +END_TEST(test_JS_ubi_Node_scriptFilename) + +#define LAMBDA_CHECK(cond) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, "%s:%d:CHECK failed: " #cond "\n", __FILE__, __LINE__); \ + return false; \ + } \ + } while (false) + +static void dumpPath(JS::ubi::Path& path) { + for (size_t i = 0; i < path.length(); i++) { + fprintf(stderr, "path[%llu]->predecessor() = '%c'\n", (long long unsigned)i, + path[i]->predecessor().as<FakeNode>()->name); + } +} + +BEGIN_TEST(test_JS_ubi_ShortestPaths_no_path) { + // Create the following graph: + // + // .---. .---. .---. + // | a | <--> | c | | b | + // '---' '---' '---' + FakeNode a('a'); + FakeNode b('b'); + FakeNode c('c'); + CHECK(a.addEdgeTo(c)); + CHECK(c.addEdgeTo(a)); + + mozilla::Maybe<JS::ubi::ShortestPaths> maybeShortestPaths; + { + JS::AutoCheckCannotGC noGC(cx); + + JS::ubi::NodeSet targets; + CHECK(targets.put(&b)); + + maybeShortestPaths = + JS::ubi::ShortestPaths::Create(cx, noGC, 10, &a, std::move(targets)); + } + + CHECK(maybeShortestPaths); + auto& paths = *maybeShortestPaths; + + size_t numPathsFound = 0; + bool ok = paths.forEachPath(&b, [&](JS::ubi::Path& path) { + numPathsFound++; + dumpPath(path); + return true; + }); + CHECK(ok); + CHECK(numPathsFound == 0); + + return true; +} +END_TEST(test_JS_ubi_ShortestPaths_no_path) + +BEGIN_TEST(test_JS_ubi_ShortestPaths_one_path) { + // Create the following graph: + // + // .---. .---. .---. + // | a | <--> | c | --> | b | + // '---' '---' '---' + FakeNode a('a'); + FakeNode b('b'); + FakeNode c('c'); + CHECK(a.addEdgeTo(c)); + CHECK(c.addEdgeTo(a)); + CHECK(c.addEdgeTo(b)); + + mozilla::Maybe<JS::ubi::ShortestPaths> maybeShortestPaths; + { + JS::AutoCheckCannotGC noGC(cx); + + JS::ubi::NodeSet targets; + CHECK(targets.put(&b)); + + maybeShortestPaths = + JS::ubi::ShortestPaths::Create(cx, noGC, 10, &a, std::move(targets)); + } + + CHECK(maybeShortestPaths); + auto& paths = *maybeShortestPaths; + + size_t numPathsFound = 0; + bool ok = paths.forEachPath(&b, [&](JS::ubi::Path& path) { + numPathsFound++; + + dumpPath(path); + LAMBDA_CHECK(path.length() == 2); + LAMBDA_CHECK(path[0]->predecessor() == JS::ubi::Node(&a)); + LAMBDA_CHECK(path[1]->predecessor() == JS::ubi::Node(&c)); + + return true; + }); + + CHECK(ok); + CHECK(numPathsFound == 1); + + return true; +} +END_TEST(test_JS_ubi_ShortestPaths_one_path) + +BEGIN_TEST(test_JS_ubi_ShortestPaths_multiple_paths) { + // Create the following graph: + // + // .---. + // .-----| a |-----. + // | '---' | + // V | V + // .---. | .---. + // | b | | | d | + // '---' | '---' + // | | | + // V | V + // .---. | .---. + // | c | | | e | + // '---' V '---' + // | .---. | + // '---->| f |<----' + // '---' + FakeNode a('a'); + FakeNode b('b'); + FakeNode c('c'); + FakeNode d('d'); + FakeNode e('e'); + FakeNode f('f'); + CHECK(a.addEdgeTo(b)); + CHECK(a.addEdgeTo(f)); + CHECK(a.addEdgeTo(d)); + CHECK(b.addEdgeTo(c)); + CHECK(c.addEdgeTo(f)); + CHECK(d.addEdgeTo(e)); + CHECK(e.addEdgeTo(f)); + + mozilla::Maybe<JS::ubi::ShortestPaths> maybeShortestPaths; + { + JS::AutoCheckCannotGC noGC(cx); + + JS::ubi::NodeSet targets; + CHECK(targets.put(&f)); + + maybeShortestPaths = + JS::ubi::ShortestPaths::Create(cx, noGC, 10, &a, std::move(targets)); + } + + CHECK(maybeShortestPaths); + auto& paths = *maybeShortestPaths; + + size_t numPathsFound = 0; + bool ok = paths.forEachPath(&f, [&](JS::ubi::Path& path) { + numPathsFound++; + dumpPath(path); + + switch (path.back()->predecessor().as<FakeNode>()->name) { + case 'a': { + LAMBDA_CHECK(path.length() == 1); + break; + } + + case 'c': { + LAMBDA_CHECK(path.length() == 3); + LAMBDA_CHECK(path[0]->predecessor() == JS::ubi::Node(&a)); + LAMBDA_CHECK(path[1]->predecessor() == JS::ubi::Node(&b)); + LAMBDA_CHECK(path[2]->predecessor() == JS::ubi::Node(&c)); + break; + } + + case 'e': { + LAMBDA_CHECK(path.length() == 3); + LAMBDA_CHECK(path[0]->predecessor() == JS::ubi::Node(&a)); + LAMBDA_CHECK(path[1]->predecessor() == JS::ubi::Node(&d)); + LAMBDA_CHECK(path[2]->predecessor() == JS::ubi::Node(&e)); + break; + } + + default: { + // Unexpected path! + LAMBDA_CHECK(false); + } + } + + return true; + }); + + CHECK(ok); + fprintf(stderr, "numPathsFound = %llu\n", (long long unsigned)numPathsFound); + CHECK(numPathsFound == 3); + + return true; +} +END_TEST(test_JS_ubi_ShortestPaths_multiple_paths) + +BEGIN_TEST(test_JS_ubi_ShortestPaths_more_paths_than_max) { + // Create the following graph: + // + // .---. + // .-----| a |-----. + // | '---' | + // V | V + // .---. | .---. + // | b | | | d | + // '---' | '---' + // | | | + // V | V + // .---. | .---. + // | c | | | e | + // '---' V '---' + // | .---. | + // '---->| f |<----' + // '---' + FakeNode a('a'); + FakeNode b('b'); + FakeNode c('c'); + FakeNode d('d'); + FakeNode e('e'); + FakeNode f('f'); + CHECK(a.addEdgeTo(b)); + CHECK(a.addEdgeTo(f)); + CHECK(a.addEdgeTo(d)); + CHECK(b.addEdgeTo(c)); + CHECK(c.addEdgeTo(f)); + CHECK(d.addEdgeTo(e)); + CHECK(e.addEdgeTo(f)); + + mozilla::Maybe<JS::ubi::ShortestPaths> maybeShortestPaths; + { + JS::AutoCheckCannotGC noGC(cx); + + JS::ubi::NodeSet targets; + CHECK(targets.put(&f)); + + maybeShortestPaths = + JS::ubi::ShortestPaths::Create(cx, noGC, 1, &a, std::move(targets)); + } + + CHECK(maybeShortestPaths); + auto& paths = *maybeShortestPaths; + + size_t numPathsFound = 0; + bool ok = paths.forEachPath(&f, [&](JS::ubi::Path& path) { + numPathsFound++; + dumpPath(path); + return true; + }); + + CHECK(ok); + fprintf(stderr, "numPathsFound = %llu\n", (long long unsigned)numPathsFound); + CHECK(numPathsFound == 1); + + return true; +} +END_TEST(test_JS_ubi_ShortestPaths_more_paths_than_max) + +BEGIN_TEST(test_JS_ubi_ShortestPaths_multiple_edges_to_target) { + // Create the following graph: + // + // .---. + // .-----| a |-----. + // | '---' | + // | | | + // |x |y |z + // | | | + // | V | + // | .---. | + // '---->| b |<----' + // '---' + FakeNode a('a'); + FakeNode b('b'); + CHECK(a.addEdgeTo(b, u"x")); + CHECK(a.addEdgeTo(b, u"y")); + CHECK(a.addEdgeTo(b, u"z")); + + mozilla::Maybe<JS::ubi::ShortestPaths> maybeShortestPaths; + { + JS::AutoCheckCannotGC noGC(cx); + + JS::ubi::NodeSet targets; + CHECK(targets.put(&b)); + + maybeShortestPaths = + JS::ubi::ShortestPaths::Create(cx, noGC, 10, &a, std::move(targets)); + } + + CHECK(maybeShortestPaths); + auto& paths = *maybeShortestPaths; + + size_t numPathsFound = 0; + bool foundX = false; + bool foundY = false; + bool foundZ = false; + + bool ok = paths.forEachPath(&b, [&](JS::ubi::Path& path) { + numPathsFound++; + dumpPath(path); + + LAMBDA_CHECK(path.length() == 1); + LAMBDA_CHECK(path.back()->name()); + LAMBDA_CHECK(js_strlen(path.back()->name().get()) == 1); + + auto c = uint8_t(path.back()->name().get()[0]); + fprintf(stderr, "Edge name = '%c'\n", c); + + switch (c) { + case 'x': { + foundX = true; + break; + } + case 'y': { + foundY = true; + break; + } + case 'z': { + foundZ = true; + break; + } + default: { + // Unexpected edge! + LAMBDA_CHECK(false); + } + } + + return true; + }); + + CHECK(ok); + fprintf(stderr, "numPathsFound = %llu\n", (long long unsigned)numPathsFound); + CHECK(numPathsFound == 3); + CHECK(foundX); + CHECK(foundY); + CHECK(foundZ); + + return true; +} +END_TEST(test_JS_ubi_ShortestPaths_multiple_edges_to_target) + +#undef LAMBDA_CHECK diff --git a/js/src/jsapi-tests/testUncaughtSymbol.cpp b/js/src/jsapi-tests/testUncaughtSymbol.cpp new file mode 100644 index 0000000000..ce026a48c6 --- /dev/null +++ b/js/src/jsapi-tests/testUncaughtSymbol.cpp @@ -0,0 +1,57 @@ +/* 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 "js/Exception.h" +#include "jsapi-tests/tests.h" + +using JS::CreateError; +using JS::ObjectValue; +using JS::Rooted; +using JS::Value; + +enum SymbolExceptionType { + NONE, + SYMBOL_ITERATOR, + SYMBOL_FOO, + SYMBOL_EMPTY, +}; + +BEGIN_TEST(testUncaughtSymbol) { + CHECK(!execDontReport("throw Symbol.iterator;", __FILE__, __LINE__)); + CHECK(GetSymbolExceptionType(cx) == SYMBOL_ITERATOR); + + CHECK(!execDontReport("throw Symbol('foo');", __FILE__, __LINE__)); + CHECK(GetSymbolExceptionType(cx) == SYMBOL_FOO); + + CHECK(!execDontReport("throw Symbol();", __FILE__, __LINE__)); + CHECK(GetSymbolExceptionType(cx) == SYMBOL_EMPTY); + + return true; +} + +static SymbolExceptionType GetSymbolExceptionType(JSContext* cx) { + JS::ExceptionStack exnStack(cx); + MOZ_RELEASE_ASSERT(JS::StealPendingExceptionStack(cx, &exnStack)); + MOZ_RELEASE_ASSERT(exnStack.exception().isSymbol()); + + JS::ErrorReportBuilder report(cx); + MOZ_RELEASE_ASSERT( + report.init(cx, exnStack, JS::ErrorReportBuilder::WithSideEffects)); + + if (strcmp(report.toStringResult().c_str(), + "uncaught exception: Symbol(Symbol.iterator)") == 0) { + return SYMBOL_ITERATOR; + } + if (strcmp(report.toStringResult().c_str(), + "uncaught exception: Symbol(foo)") == 0) { + return SYMBOL_FOO; + } + if (strcmp(report.toStringResult().c_str(), "uncaught exception: Symbol()") == + 0) { + return SYMBOL_EMPTY; + } + MOZ_CRASH("Unexpected symbol"); +} + +END_TEST(testUncaughtSymbol) diff --git a/js/src/jsapi-tests/testValueABI.cpp b/js/src/jsapi-tests/testValueABI.cpp new file mode 100644 index 0000000000..298bd7dd99 --- /dev/null +++ b/js/src/jsapi-tests/testValueABI.cpp @@ -0,0 +1,53 @@ +/* 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 "js/GlobalObject.h" +#include "jsapi-tests/tests.h" + +/* + * Bug 689101 - jsval is technically a non-POD type because it has a private + * data member. On gcc, this doesn't seem to matter. On MSVC, this prevents + * returning a jsval from a function between C and C++ because it will use a + * retparam in C++ and a direct return value in C. + * + * Bug 712289 - jsval alignment was different on 32-bit platforms between C and + * C++ because the default alignments of js::Value and jsval_layout differ. + */ + +extern "C" { + +extern bool C_ValueToObject(JSContext* cx, jsval v, JSObject** obj); + +extern jsval C_GetEmptyStringValue(JSContext* cx); + +extern size_t C_jsvalAlignmentTest(); +} + +BEGIN_TEST(testValueABI_retparam) { + JS::RootedObject obj(cx, JS::CurrentGlobalOrNull(cx)); + RootedValue v(cx, ObjectValue(*obj)); + obj = nullptr; + CHECK(C_ValueToObject(cx, v, obj.address())); + bool equal; + RootedValue v2(cx, ObjectValue(*obj)); + CHECK(JS_StrictlyEqual(cx, v, v2, &equal)); + CHECK(equal); + + v = C_GetEmptyStringValue(cx); + CHECK(v.isString()); + + return true; +} +END_TEST(testValueABI_retparam) + +BEGIN_TEST(testValueABI_alignment) { + typedef struct { + char c; + jsval v; + } AlignTest; + CHECK(C_jsvalAlignmentTest() == sizeof(AlignTest)); + + return true; +} +END_TEST(testValueABI_alignment) diff --git a/js/src/jsapi-tests/testWasmLEB128.cpp b/js/src/jsapi-tests/testWasmLEB128.cpp new file mode 100644 index 0000000000..58ded9ab26 --- /dev/null +++ b/js/src/jsapi-tests/testWasmLEB128.cpp @@ -0,0 +1,173 @@ +/* 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 <stdlib.h> + +#include "jsapi-tests/tests.h" + +#include "wasm/WasmValidate.h" + +static bool WriteValidBytes(js::wasm::Encoder& encoder, bool* passed) { + *passed = false; + if (!encoder.empty()) { + return true; + } + + // These remain the same under LEB128 unsigned encoding + if (!encoder.writeVarU32(0x0) || !encoder.writeVarU32(0x1) || + !encoder.writeVarU32(0x42)) { + return false; + } + + // 0x01 0x80 + if (!encoder.writeVarU32(0x80)) { + return false; + } + + // 0x03 0x80 + if (!encoder.writeVarU32(0x180)) { + return false; + } + + if (encoder.empty()) { + return true; + } + if (encoder.currentOffset() != 7) { + return true; + } + *passed = true; + return true; +} + +BEGIN_TEST(testWasmLEB128_encoding) { + using namespace js; + using namespace wasm; + + Bytes bytes; + Encoder encoder(bytes); + + bool passed; + if (!WriteValidBytes(encoder, &passed)) { + return false; + } + CHECK(passed); + + size_t i = 0; + CHECK(bytes[i++] == 0x0); + CHECK(bytes[i++] == 0x1); + CHECK(bytes[i++] == 0x42); + + CHECK(bytes[i++] == 0x80); + CHECK(bytes[i++] == 0x01); + + CHECK(bytes[i++] == 0x80); + CHECK(bytes[i++] == 0x03); + + if (i + 1 < bytes.length()) { + CHECK(bytes[i++] == 0x00); + } + return true; +} +END_TEST(testWasmLEB128_encoding) + +BEGIN_TEST(testWasmLEB128_valid_decoding) { + using namespace js; + using namespace wasm; + + Bytes bytes; + if (!bytes.append(0x0) || !bytes.append(0x1) || !bytes.append(0x42)) { + return false; + } + + if (!bytes.append(0x80) || !bytes.append(0x01)) { + return false; + } + + if (!bytes.append(0x80) || !bytes.append(0x03)) { + return false; + } + + { + // Fallible decoding + Decoder decoder(bytes); + uint32_t value; + + CHECK(decoder.readVarU32(&value) && value == 0x0); + CHECK(decoder.readVarU32(&value) && value == 0x1); + CHECK(decoder.readVarU32(&value) && value == 0x42); + CHECK(decoder.readVarU32(&value) && value == 0x80); + CHECK(decoder.readVarU32(&value) && value == 0x180); + + CHECK(decoder.done()); + } + + { + // Infallible decoding + Decoder decoder(bytes); + uint32_t value; + + value = decoder.uncheckedReadVarU32(); + CHECK(value == 0x0); + value = decoder.uncheckedReadVarU32(); + CHECK(value == 0x1); + value = decoder.uncheckedReadVarU32(); + CHECK(value == 0x42); + value = decoder.uncheckedReadVarU32(); + CHECK(value == 0x80); + value = decoder.uncheckedReadVarU32(); + CHECK(value == 0x180); + + CHECK(decoder.done()); + } + return true; +} +END_TEST(testWasmLEB128_valid_decoding) + +BEGIN_TEST(testWasmLEB128_invalid_decoding) { + using namespace js; + using namespace wasm; + + Bytes bytes; + // Fill bits as per 28 encoded bits + if (!bytes.append(0x80) || !bytes.append(0x80) || !bytes.append(0x80) || + !bytes.append(0x80)) { + return false; + } + + // Test last valid values + if (!bytes.append(0x00)) { + return false; + } + + for (uint8_t i = 0; i < 0x0F; i++) { + bytes[4] = i; + + { + Decoder decoder(bytes); + uint32_t value; + CHECK(decoder.readVarU32(&value)); + CHECK(value == uint32_t(i << 28)); + CHECK(decoder.done()); + } + + { + Decoder decoder(bytes); + uint32_t value = decoder.uncheckedReadVarU32(); + CHECK(value == uint32_t(i << 28)); + CHECK(decoder.done()); + } + } + + // Test all invalid values of the same size + for (uint8_t i = 0x10; i < 0xF0; i++) { + bytes[4] = i; + + Decoder decoder(bytes); + uint32_t value; + CHECK(!decoder.readVarU32(&value)); + } + + return true; +} +END_TEST(testWasmLEB128_invalid_decoding) diff --git a/js/src/jsapi-tests/testWasmReturnCalls.cpp b/js/src/jsapi-tests/testWasmReturnCalls.cpp new file mode 100644 index 0000000000..ef06bf4710 --- /dev/null +++ b/js/src/jsapi-tests/testWasmReturnCalls.cpp @@ -0,0 +1,97 @@ +/* -*- 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 "jit/MacroAssembler.h" + +#include "jsapi-tests/tests.h" +#include "jsapi-tests/testsJit.h" + +#include "jit/MacroAssembler-inl.h" + +using namespace js; +using namespace js::jit; + +#if defined(ENABLE_WASM_TAIL_CALLS) && !defined(JS_CODEGEN_NONE) + +// Check if wasmMarkSlowCall produces the byte sequence that can +// wasmCheckSlowCallsite detect. +BEGIN_TEST(testWasmCheckSlowCallMarkerHit) { + js::LifoAlloc lifo(4096); + TempAllocator alloc(&lifo); + JitContext jc(cx); + StackMacroAssembler masm(cx, alloc); + AutoCreatedBy acb(masm, __func__); + + PrepareJit(masm); + + Label check, fail, end; + masm.call(&check); + masm.wasmMarkSlowCall(); + masm.jump(&end); + + masm.bind(&check); +# ifdef JS_USE_LINK_REGISTER +# if !defined(JS_CODEGEN_LOONG64) && !defined(JS_CODEGEN_MIPS64) && \ + !defined(JS_CODEGEN_RISCV64) + static constexpr Register ra = lr; +# endif +# else + static constexpr Register ra = ABINonArgReg2; + masm.loadPtr(Address(StackPointer, 0), ra); +# endif + + masm.wasmCheckSlowCallsite(ra, &fail, ABINonArgReg1, ABINonArgReg2); + masm.abiret(); + + masm.bind(&fail); + masm.printf("Failed\n"); + masm.breakpoint(); + + masm.bind(&end); + return ExecuteJit(cx, masm); +} +END_TEST(testWasmCheckSlowCallMarkerHit) + +// Check if wasmCheckSlowCallsite does not detect non-marked slow calls. +BEGIN_TEST(testWasmCheckSlowCallMarkerMiss) { + js::LifoAlloc lifo(4096); + TempAllocator alloc(&lifo); + JitContext jc(cx); + StackMacroAssembler masm(cx, alloc); + AutoCreatedBy acb(masm, __func__); + + PrepareJit(masm); + + Label check, fast, end; + masm.call(&check); + masm.nop(); + masm.jump(&end); + + masm.bind(&check); +# ifdef JS_USE_LINK_REGISTER +# if !defined(JS_CODEGEN_LOONG64) && !defined(JS_CODEGEN_MIPS64) && \ + !defined(JS_CODEGEN_RISCV64) + static constexpr Register ra = lr; +# endif +# else + static constexpr Register ra = ABINonArgReg2; + masm.loadPtr(Address(StackPointer, 0), ra); +# endif + + masm.wasmCheckSlowCallsite(ra, &fast, ABINonArgReg1, ABINonArgReg2); + masm.printf("Failed\n"); + masm.breakpoint(); + + masm.bind(&fast); + masm.abiret(); + + masm.bind(&end); + return ExecuteJit(cx, masm); +} +END_TEST(testWasmCheckSlowCallMarkerMiss) + +#endif // ENABLE_WASM_TAIL_CALLS diff --git a/js/src/jsapi-tests/testWeakMap.cpp b/js/src/jsapi-tests/testWeakMap.cpp new file mode 100644 index 0000000000..0872571d3d --- /dev/null +++ b/js/src/jsapi-tests/testWeakMap.cpp @@ -0,0 +1,324 @@ +/* -*- 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 "gc/Zone.h" +#include "js/Array.h" // JS::GetArrayLength +#include "js/Exception.h" // JS_IsExceptionPending +#include "js/GlobalObject.h" // JS_NewGlobalObject +#include "js/PropertyAndElement.h" // JS_DefineProperty +#include "js/WeakMap.h" +#include "jsapi-tests/tests.h" +#include "vm/Realm.h" + +using namespace js; + +static bool checkSize(JSContext* cx, JS::HandleObject map, uint32_t expected) { + JS::RootedObject keys(cx); + if (!JS_NondeterministicGetWeakMapKeys(cx, map, &keys)) { + return false; + } + + uint32_t length; + if (!JS::GetArrayLength(cx, keys, &length)) { + return false; + } + + return length == expected; +} + +JSObject* keyDelegate = nullptr; + +BEGIN_TEST(testWeakMap_basicOperations) { + JS::RootedObject map(cx, JS::NewWeakMapObject(cx)); + CHECK(IsWeakMapObject(map)); + + JS::RootedValue key(cx, JS::ObjectOrNullValue(newKey())); + CHECK(!key.isNull()); + CHECK(!JS::IsWeakMapObject(&key.toObject())); + + JS::RootedValue r(cx); + CHECK(GetWeakMapEntry(cx, map, key, &r)); + CHECK(r.isUndefined()); + + CHECK(checkSize(cx, map, 0)); + + JS::RootedValue val(cx, JS::Int32Value(1)); + CHECK(SetWeakMapEntry(cx, map, key, val)); + + CHECK(GetWeakMapEntry(cx, map, key, &r)); + CHECK(r == val); + CHECK(checkSize(cx, map, 1)); + + JS_GC(cx); + + CHECK(GetWeakMapEntry(cx, map, key, &r)); + CHECK(r == val); + CHECK(checkSize(cx, map, 1)); + + key.setUndefined(); + JS_GC(cx); + + CHECK(checkSize(cx, map, 0)); + + return true; +} + +JSObject* newKey() { return JS_NewPlainObject(cx); } +END_TEST(testWeakMap_basicOperations) + +BEGIN_TEST(testWeakMap_setWeakMapEntry_invalid_key) { + JS::RootedObject map(cx, JS::NewWeakMapObject(cx)); + CHECK(IsWeakMapObject(map)); + CHECK(checkSize(cx, map, 0)); + + JS::RootedString test(cx, JS_NewStringCopyZ(cx, "test")); + // sym is a Symbol in global Symbol registry and hence can't be used as a key. + JS::RootedSymbol sym(cx, JS::GetSymbolFor(cx, test)); + JS::RootedValue key(cx, JS::SymbolValue(sym)); + CHECK(!key.isUndefined()); + + JS::RootedValue val(cx, JS::Int32Value(1)); + + CHECK(!JS_IsExceptionPending(cx)); + CHECK(SetWeakMapEntry(cx, map, key, val) == false); + + CHECK(JS_IsExceptionPending(cx)); + JS::Rooted<JS::Value> exn(cx); + CHECK(JS_GetPendingException(cx, &exn)); + JS::Rooted<JSObject*> obj(cx, &exn.toObject()); + JSErrorReport* err = JS_ErrorFromException(cx, obj); + CHECK(err->exnType == JSEXN_TYPEERR); + + JS_ClearPendingException(cx); + + JS::RootedValue r(cx); + CHECK(GetWeakMapEntry(cx, map, key, &r)); + CHECK(r == JS::UndefinedValue()); + CHECK(checkSize(cx, map, 0)); + + return true; +} +END_TEST(testWeakMap_setWeakMapEntry_invalid_key) + +#ifdef NIGHTLY_BUILD +BEGIN_TEST(testWeakMap_basicOperations_symbols_as_keys) { + JS::RootedObject map(cx, JS::NewWeakMapObject(cx)); + CHECK(IsWeakMapObject(map)); + + JS::RootedString test(cx, JS_NewStringCopyZ(cx, "test")); + JS::RootedSymbol sym(cx, JS::NewSymbol(cx, test)); + CHECK(sym); + JS::RootedValue key(cx, JS::SymbolValue(sym)); + + JS::RootedValue r(cx); + CHECK(GetWeakMapEntry(cx, map, key, &r)); + CHECK(r.isUndefined()); + + CHECK(checkSize(cx, map, 0)); + + JS::RootedValue val(cx, JS::Int32Value(1)); + CHECK(SetWeakMapEntry(cx, map, key, val)); + + CHECK(GetWeakMapEntry(cx, map, key, &r)); + CHECK(r == val); + CHECK(checkSize(cx, map, 1)); + + JS_GC(cx); + + CHECK(GetWeakMapEntry(cx, map, key, &r)); + CHECK(r == val); + CHECK(checkSize(cx, map, 1)); + + sym = nullptr; + key.setUndefined(); + JS_GC(cx); + + CHECK(checkSize(cx, map, 0)); + + return true; +} +END_TEST(testWeakMap_basicOperations_symbols_as_keys) +#endif + +BEGIN_TEST(testWeakMap_keyDelegates) { + AutoLeaveZeal nozeal(cx); + + AutoGCParameter param(cx, JSGC_INCREMENTAL_GC_ENABLED, true); + JS_GC(cx); + JS::RootedObject map(cx, JS::NewWeakMapObject(cx)); + CHECK(map); + + JS::RootedObject delegate(cx, newDelegate()); + JS::RootedObject key(cx, delegate); + if (!JS_WrapObject(cx, &key)) { + return false; + } + CHECK(key); + CHECK(delegate); + + keyDelegate = delegate; + + JS::RootedObject delegateRoot(cx); + { + JSAutoRealm ar(cx, delegate); + delegateRoot = JS_NewPlainObject(cx); + CHECK(delegateRoot); + JS::RootedValue delegateValue(cx, JS::ObjectValue(*delegate)); + CHECK(JS_DefineProperty(cx, delegateRoot, "delegate", delegateValue, 0)); + } + delegate = nullptr; + + /* + * Perform an incremental GC, introducing an unmarked CCW to force the map + * zone to finish marking before the delegate zone. + */ + CHECK(newCCW(map, delegateRoot)); + performIncrementalGC(); +#ifdef DEBUG + CHECK(map->zone()->lastSweepGroupIndex() < + delegateRoot->zone()->lastSweepGroupIndex()); +#endif + + /* Add our entry to the weakmap. */ + JS::RootedValue keyVal(cx, JS::ObjectValue(*key)); + JS::RootedValue val(cx, JS::Int32Value(1)); + CHECK(SetWeakMapEntry(cx, map, keyVal, val)); + CHECK(checkSize(cx, map, 1)); + + /* + * Check the delegate keeps the entry alive even if the key is not reachable. + */ + key = nullptr; + keyVal.setUndefined(); + CHECK(newCCW(map, delegateRoot)); + performIncrementalGC(); + CHECK(checkSize(cx, map, 1)); + + /* + * Check that the zones finished marking at the same time, which is + * necessary because of the presence of the delegate and the CCW. + */ +#ifdef DEBUG + CHECK(map->zone()->lastSweepGroupIndex() == + delegateRoot->zone()->lastSweepGroupIndex()); +#endif + + /* Check that when the delegate becomes unreachable the entry is removed. */ + delegateRoot = nullptr; + keyDelegate = nullptr; + JS_GC(cx); + CHECK(checkSize(cx, map, 0)); + + return true; +} + +static size_t DelegateObjectMoved(JSObject* obj, JSObject* old) { + if (!keyDelegate) { + return 0; // Object got moved before we set keyDelegate to point to it. + } + + MOZ_RELEASE_ASSERT(keyDelegate == old); + keyDelegate = obj; + return 0; +} + +JSObject* newKey() { + static const JSClass keyClass = { + "keyWithDelegate", JSCLASS_HAS_RESERVED_SLOTS(1), + JS_NULL_CLASS_OPS, JS_NULL_CLASS_SPEC, + JS_NULL_CLASS_EXT, JS_NULL_OBJECT_OPS}; + + JS::RootedObject key(cx, JS_NewObject(cx, &keyClass)); + if (!key) { + return nullptr; + } + + return key; +} + +JSObject* newCCW(JS::HandleObject sourceZone, JS::HandleObject destZone) { + /* + * Now ensure that this zone will be swept first by adding a cross + * compartment wrapper to a new object in the same zone as the + * delegate object. + */ + JS::RootedObject object(cx); + { + JSAutoRealm ar(cx, destZone); + object = JS_NewPlainObject(cx); + if (!object) { + return nullptr; + } + } + { + JSAutoRealm ar(cx, sourceZone); + if (!JS_WrapObject(cx, &object)) { + return nullptr; + } + } + + // In order to test the SCC algorithm, we need the wrapper/wrappee to be + // tenured. + cx->runtime()->gc.evictNursery(); + + return object; +} + +JSObject* newDelegate() { + static const JSClassOps delegateClassOps = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + nullptr, // finalize + nullptr, // call + nullptr, // construct + JS_GlobalObjectTraceHook, // trace + }; + + static const js::ClassExtension delegateClassExtension = { + DelegateObjectMoved, // objectMovedOp + }; + + static const JSClass delegateClass = { + "delegate", + JSCLASS_GLOBAL_FLAGS | JSCLASS_HAS_RESERVED_SLOTS(1), + &delegateClassOps, + JS_NULL_CLASS_SPEC, + &delegateClassExtension, + JS_NULL_OBJECT_OPS}; + + /* Create the global object. */ + JS::RealmOptions options; + JS::RootedObject global(cx, + JS_NewGlobalObject(cx, &delegateClass, nullptr, + JS::FireOnNewGlobalHook, options)); + if (!global) { + return nullptr; + } + + JS_SetReservedSlot(global, 0, JS::Int32Value(42)); + return global; +} + +void performIncrementalGC() { + JSRuntime* rt = cx->runtime(); + js::SliceBudget budget(js::WorkBudget(1000)); + rt->gc.startDebugGC(JS::GCOptions::Normal, budget); + + // Wait until we've started marking before finishing the GC + // non-incrementally. + while (rt->gc.state() == gc::State::Prepare) { + rt->gc.debugGCSlice(budget); + } + if (JS::IsIncrementalGCInProgress(cx)) { + rt->gc.finishGC(JS::GCReason::DEBUG_GC); + } +} +END_TEST(testWeakMap_keyDelegates) diff --git a/js/src/jsapi-tests/testWindowNonConfigurable.cpp b/js/src/jsapi-tests/testWindowNonConfigurable.cpp new file mode 100644 index 0000000000..a0aa78f90a --- /dev/null +++ b/js/src/jsapi-tests/testWindowNonConfigurable.cpp @@ -0,0 +1,69 @@ +/* -*- 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 "js/PropertyAndElement.h" // JS_DefineProperty +#include "js/Proxy.h" +#include "jsapi-tests/tests.h" + +class WindowProxyHandler : public js::ForwardingProxyHandler { + public: + constexpr WindowProxyHandler() : js::ForwardingProxyHandler(&family) {} + + static const char family; + + virtual bool defineProperty(JSContext* cx, JS::HandleObject proxy, + JS::HandleId id, + JS::Handle<JS::PropertyDescriptor> desc, + JS::ObjectOpResult& result) const override { + if (desc.hasConfigurable() && !desc.configurable()) { + result.failCantDefineWindowNonConfigurable(); + return true; + } + return ForwardingProxyHandler::defineProperty(cx, proxy, id, desc, result); + } +}; +const char WindowProxyHandler::family = 0; + +static const JSClass windowProxy_class = + PROXY_CLASS_DEF("TestWindowProxy", JSCLASS_HAS_RESERVED_SLOTS(1)); +static const WindowProxyHandler windowProxy_handler; + +BEGIN_TEST(testWindowNonConfigurable) { + JS::RootedObject wrapped(cx, JS_NewObject(cx, nullptr)); + CHECK(wrapped); + JS::RootedValue wrappedVal(cx, JS::ObjectValue(*wrapped)); + js::ProxyOptions options; + options.setClass(&windowProxy_class); + JS::RootedObject obj(cx, NewProxyObject(cx, &windowProxy_handler, wrappedVal, + nullptr, options)); + CHECK(obj); + CHECK(JS_DefineProperty(cx, global, "windowProxy", obj, 0)); + JS::RootedValue v(cx); + EVAL( + "Object.defineProperty(windowProxy, 'bar', {value: 1, configurable: " + "false})", + &v); + CHECK(v.isNull()); // This is the important bit! + EVAL( + "Object.defineProperty(windowProxy, 'bar', {value: 1, configurable: " + "true})", + &v); + CHECK(&v.toObject() == obj); + EVAL( + "Reflect.defineProperty(windowProxy, 'foo', {value: 1, configurable: " + "false})", + &v); + CHECK(v.isFalse()); + EVAL( + "Reflect.defineProperty(windowProxy, 'foo', {value: 1, configurable: " + "true})", + &v); + CHECK(v.isTrue()); + + return true; +} +END_TEST(testWindowNonConfigurable) diff --git a/js/src/jsapi-tests/tests.cpp b/js/src/jsapi-tests/tests.cpp new file mode 100644 index 0000000000..1dc36e7bc3 --- /dev/null +++ b/js/src/jsapi-tests/tests.cpp @@ -0,0 +1,298 @@ +/* -*- 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/Utf8.h" // mozilla::Utf8Unit + +#include <stdio.h> + +#include "js/ArrayBuffer.h" +#include "js/CompilationAndEvaluation.h" // JS::Evaluate +#include "js/GlobalObject.h" // 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} + +JSAPIRuntimeTest* JSAPIRuntimeTest::list; +JSAPIFrontendTest* JSAPIFrontendTest::list; + +bool JSAPIRuntimeTest::init(JSContext* maybeReusableContext) { + if (maybeReusableContext && reuseGlobal) { + cx = maybeReusableContext; + global.init(cx, JS::CurrentGlobalOrNull(cx)); + return init(); + } + + 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 init(); +} + +JSContext* JSAPIRuntimeTest::maybeForgetContext() { + if (!reuseGlobal) { + return nullptr; + } + + JSContext* reusableCx = cx; + global.reset(); + cx = nullptr; + return reusableCx; +} + +/* static */ +void JSAPIRuntimeTest::MaybeFreeContext(JSContext* maybeCx) { + if (maybeCx) { + JS::LeaveRealm(maybeCx, nullptr); + JS_DestroyContext(maybeCx); + } +} + +void JSAPIRuntimeTest::uninit() { + global.reset(); + MaybeFreeContext(cx); + cx = nullptr; + msgs.clear(); +} + +bool JSAPIRuntimeTest::exec(const char* utf8, const char* filename, + int lineno) { + JS::CompileOptions opts(cx); + opts.setFileAndLine(filename, lineno); + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + JS::RootedValue v(cx); + return (srcBuf.init(cx, utf8, strlen(utf8), JS::SourceOwnership::Borrowed) && + JS::Evaluate(cx, opts, srcBuf, &v)) || + fail(JSAPITestString(utf8), filename, lineno); +} + +bool JSAPIRuntimeTest::execDontReport(const char* utf8, const char* filename, + int lineno) { + JS::CompileOptions opts(cx); + opts.setFileAndLine(filename, lineno); + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + JS::RootedValue v(cx); + return srcBuf.init(cx, utf8, strlen(utf8), JS::SourceOwnership::Borrowed) && + JS::Evaluate(cx, opts, srcBuf, &v); +} + +bool JSAPIRuntimeTest::evaluate(const char* utf8, const char* filename, + int lineno, JS::MutableHandleValue vp) { + JS::CompileOptions opts(cx); + opts.setFileAndLine(filename, lineno); + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + return (srcBuf.init(cx, utf8, strlen(utf8), JS::SourceOwnership::Borrowed) && + JS::Evaluate(cx, opts, srcBuf, vp)) || + fail(JSAPITestString(utf8), filename, lineno); +} + +bool JSAPIRuntimeTest::definePrint() { + return JS_DefineFunction(cx, global, "print", (JSNative)print, 0, 0); +} + +JSObject* JSAPIRuntimeTest::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; +} + +struct CommandOptions { + bool list = false; + bool frontendOnly = false; + bool help = false; + const char* filter = nullptr; +}; + +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.frontendOnly = true; + continue; + } + + if (!options.filter) { + options.filter = argv[i]; + continue; + } + + printf("error: Unrecognized option: %s\n", argv[i]); + options.help = true; + } +} + +template <typename TestT> +void PrintTests(TestT* list) { + for (TestT* test = list; test; test = test->next) { + printf("%s\n", test->name()); + } +} + +template <typename TestT, typename InitF, typename RunF, typename BeforeUninitF> +void RunTests(int& total, int& failures, CommandOptions& options, TestT* list, + InitF init, RunF run, BeforeUninitF beforeUninit) { + for (TestT* test = list; test; test = test->next) { + 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 (!init(test)) { + printf("TEST-UNEXPECTED-FAIL | %s | Failed to initialize.\n", name); + failures++; + test->uninit(); + continue; + } + + if (run(test)) { + printf("TEST-PASS | %s | ok\n", name); + } else { + JSAPITestString messages = test->messages(); + printf("%s | %s | %.*s\n", + (test->knownFail ? "TEST-KNOWN-FAIL" : "TEST-UNEXPECTED-FAIL"), + name, (int)messages.length(), messages.begin()); + if (!test->knownFail) { + failures++; + } + } + + beforeUninit(test); + + test->uninit(); + } +} + +int main(int argc, char* argv[]) { + int total = 0; + int failures = 0; + CommandOptions options; + parseArgs(argc, argv, options); + + if (options.help) { + 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"); + return 0; + } + + // Override prefs for jsapi-tests. + JS::Prefs::setAtStartup_weakrefs(true); + JS::Prefs::setAtStartup_experimental_weakrefs_expose_cleanupSome(true); +#ifdef NIGHTLY_BUILD + JS::Prefs::setAtStartup_experimental_symbols_as_weakmap_keys(true); +#endif + + if (!options.frontendOnly) { + if (!JS_Init()) { + printf("TEST-UNEXPECTED-FAIL | jsapi-tests | JS_Init() failed.\n"); + return 1; + } + } else { + if (!JS_FrontendOnlyInit()) { + printf("TEST-UNEXPECTED-FAIL | jsapi-tests | JS_Init() failed.\n"); + return 1; + } + } + + if (options.list) { + PrintTests(JSAPIRuntimeTest::list); + PrintTests(JSAPIFrontendTest::list); + return 0; + } + + // 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 + // global. + JSContext* maybeReusedContext = nullptr; + + if (!options.frontendOnly) { + RunTests( + total, failures, options, JSAPIRuntimeTest::list, + [&maybeReusedContext](JSAPIRuntimeTest* test) { + return test->init(maybeReusedContext); + }, + [](JSAPIRuntimeTest* test) { return test->run(test->global); }, + [&maybeReusedContext](JSAPIRuntimeTest* test) { + // Return a non-nullptr pointer if the context & global can safely be + // reused for the next test. + maybeReusedContext = test->maybeForgetContext(); + }); + } + RunTests( + total, failures, options, JSAPIFrontendTest::list, + [](JSAPIFrontendTest* test) { return test->init(); }, + [](JSAPIFrontendTest* test) { return test->run(); }, + [](JSAPIFrontendTest* test) {}); + + if (!options.frontendOnly) { + JSAPIRuntimeTest::MaybeFreeContext(maybeReusedContext); + + MOZ_RELEASE_ASSERT(!JSRuntime::hasLiveRuntimes()); + JS_ShutDown(); + } else { + 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; +} diff --git a/js/src/jsapi-tests/tests.h b/js/src/jsapi-tests/tests.h new file mode 100644 index 0000000000..75522478d7 --- /dev/null +++ b/js/src/jsapi-tests/tests.h @@ -0,0 +1,633 @@ +/* -*- 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/. */ + +#ifndef jsapi_tests_tests_h +#define jsapi_tests_tests_h + +#include "mozilla/Sprintf.h" + +#include <errno.h> +#include <iterator> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <type_traits> + +#include "jsapi.h" + +#include "gc/GC.h" +#include "js/AllocPolicy.h" +#include "js/ArrayBuffer.h" +#include "js/CharacterEncoding.h" +#include "js/Conversions.h" +#include "js/Equality.h" // JS::SameValue +#include "js/GlobalObject.h" // JS::DefaultGlobalClassOps +#include "js/RegExpFlags.h" // JS::RegExpFlags +#include "js/Vector.h" +#include "js/Warnings.h" // JS::SetWarningReporter +#include "vm/JSContext.h" + +/* Note: Aborts on OOM. */ +class JSAPITestString { + js::Vector<char, 0, js::SystemAllocPolicy> chars; + + public: + JSAPITestString() {} + explicit JSAPITestString(const char* s) { *this += s; } + JSAPITestString(const JSAPITestString& s) { *this += s; } + + const char* begin() const { return chars.begin(); } + const char* end() const { return chars.end(); } + size_t length() const { return chars.length(); } + void clear() { chars.clearAndFree(); } + + JSAPITestString& operator+=(const char* s) { + if (!chars.append(s, strlen(s))) { + abort(); + } + return *this; + } + + JSAPITestString& operator+=(const JSAPITestString& s) { + if (!chars.append(s.begin(), s.length())) { + abort(); + } + return *this; + } +}; + +inline JSAPITestString operator+(const JSAPITestString& a, const char* b) { + JSAPITestString result = a; + result += b; + return result; +} + +inline JSAPITestString operator+(const JSAPITestString& a, + const JSAPITestString& b) { + JSAPITestString result = a; + result += b; + return result; +} + +class JSAPIRuntimeTest; + +class JSAPITest { + public: + bool knownFail; + JSAPITestString msgs; + + JSAPITest() : knownFail(false) {} + + virtual ~JSAPITest() {} + + virtual const char* name() = 0; + + virtual void maybeAppendException(JSAPITestString& message) {} + + bool fail(const JSAPITestString& msg = JSAPITestString(), + const char* filename = "-", int lineno = 0) { + char location[256]; + SprintfLiteral(location, "%s:%d:", filename, lineno); + + JSAPITestString message(location); + message += msg; + + maybeAppendException(message); + + fprintf(stderr, "%.*s\n", int(message.length()), message.begin()); + + if (msgs.length() != 0) { + msgs += " | "; + } + msgs += message; + return false; + } + + JSAPITestString messages() const { return msgs; } +}; + +class JSAPIRuntimeTest : public JSAPITest { + public: + static JSAPIRuntimeTest* list; + JSAPIRuntimeTest* next; + + JSContext* cx; + JS::PersistentRootedObject global; + + // Whether this test is willing to skip its init() and reuse a global (and + // JSContext etc.) from a previous test that also has reuseGlobal=true. It + // also means this test is willing to skip its uninit() if it is followed by + // another reuseGlobal test. + bool reuseGlobal; + + JSAPIRuntimeTest() : JSAPITest(), cx(nullptr), reuseGlobal(false) { + next = list; + list = this; + } + + virtual ~JSAPIRuntimeTest() { + MOZ_RELEASE_ASSERT(!cx); + MOZ_RELEASE_ASSERT(!global); + } + + // Initialize this test, possibly with the cx from a previously run test. + bool init(JSContext* maybeReusedContext); + + // If this test is ok with its cx and global being reused, release this + // test's cx to be reused by another test. + JSContext* maybeForgetContext(); + + static void MaybeFreeContext(JSContext* maybeCx); + + // The real initialization happens in init(JSContext*), above, but this + // method may be overridden to perform additional initialization after the + // JSContext and global have been created. + virtual bool init() { return true; } + virtual void uninit(); + + virtual bool run(JS::HandleObject global) = 0; + +#define EXEC(s) \ + do { \ + if (!exec(s, __FILE__, __LINE__)) return false; \ + } while (false) + + bool exec(const char* utf8, const char* filename, int lineno); + + // Like exec(), but doesn't call fail() if JS::Evaluate returns false. + bool execDontReport(const char* utf8, const char* filename, int lineno); + +#define EVAL(s, vp) \ + do { \ + if (!evaluate(s, __FILE__, __LINE__, vp)) return false; \ + } while (false) + + bool evaluate(const char* utf8, const char* filename, int lineno, + JS::MutableHandleValue vp); + + JSAPITestString jsvalToSource(JS::HandleValue v) { + JS::Rooted<JSString*> str(cx, JS_ValueToSource(cx, v)); + if (str) { + if (JS::UniqueChars bytes = JS_EncodeStringToUTF8(cx, str)) { + return JSAPITestString(bytes.get()); + } + } + JS_ClearPendingException(cx); + return JSAPITestString("<<error converting value to string>>"); + } + + JSAPITestString toSource(char c) { + char buf[2] = {c, '\0'}; + return JSAPITestString(buf); + } + + JSAPITestString toSource(long v) { + char buf[40]; + SprintfLiteral(buf, "%ld", v); + return JSAPITestString(buf); + } + + JSAPITestString toSource(unsigned long v) { + char buf[40]; + SprintfLiteral(buf, "%lu", v); + return JSAPITestString(buf); + } + + JSAPITestString toSource(long long v) { + char buf[40]; + SprintfLiteral(buf, "%lld", v); + return JSAPITestString(buf); + } + + JSAPITestString toSource(unsigned long long v) { + char buf[40]; + SprintfLiteral(buf, "%llu", v); + return JSAPITestString(buf); + } + + JSAPITestString toSource(double d) { + char buf[40]; + SprintfLiteral(buf, "%17lg", d); + return JSAPITestString(buf); + } + + JSAPITestString toSource(unsigned int v) { + return toSource((unsigned long)v); + } + + JSAPITestString toSource(int v) { return toSource((long)v); } + + JSAPITestString toSource(bool v) { + return JSAPITestString(v ? "true" : "false"); + } + + JSAPITestString toSource(JS::RegExpFlags flags) { + JSAPITestString 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; + } + + JSAPITestString toSource(JSAtom* v) { + JS::RootedValue val(cx, JS::StringValue((JSString*)v)); + return jsvalToSource(val); + } + + // Note that in some still-supported GCC versions (we think anything before + // GCC 4.6), this template does not work when the second argument is + // nullptr. It infers type U = long int. Use CHECK_NULL instead. + template <typename T, typename U> + bool checkEqual(const T& actual, const U& expected, const char* actualExpr, + const char* expectedExpr, const char* filename, int lineno) { + static_assert(std::is_signed_v<T> == std::is_signed_v<U>, + "using CHECK_EQUAL with different-signed inputs triggers " + "compiler warnings"); + static_assert( + std::is_unsigned_v<T> == std::is_unsigned_v<U>, + "using CHECK_EQUAL with different-signed inputs triggers compiler " + "warnings"); + return (actual == expected) || + fail(JSAPITestString("CHECK_EQUAL failed: expected (") + + expectedExpr + ") = " + toSource(expected) + ", got (" + + actualExpr + ") = " + toSource(actual), + filename, lineno); + } + +#define CHECK_EQUAL(actual, expected) \ + do { \ + if (!checkEqual(actual, expected, #actual, #expected, __FILE__, __LINE__)) \ + return false; \ + } while (false) + + template <typename T> + bool checkNull(const T* actual, const char* actualExpr, const char* filename, + int lineno) { + return (actual == nullptr) || + fail(JSAPITestString("CHECK_NULL failed: expected nullptr, got (") + + actualExpr + ") = " + toSource(actual), + filename, lineno); + } + +#define CHECK_NULL(actual) \ + do { \ + if (!checkNull(actual, #actual, __FILE__, __LINE__)) return false; \ + } while (false) + + bool checkSame(const JS::Value& actualArg, const JS::Value& expectedArg, + const char* actualExpr, const char* expectedExpr, + const char* filename, int lineno) { + bool same; + JS::RootedValue actual(cx, actualArg), expected(cx, expectedArg); + return (JS::SameValue(cx, actual, expected, &same) && same) || + fail(JSAPITestString( + "CHECK_SAME failed: expected JS::SameValue(cx, ") + + actualExpr + ", " + expectedExpr + + "), got !JS::SameValue(cx, " + jsvalToSource(actual) + + ", " + jsvalToSource(expected) + ")", + filename, lineno); + } + +#define CHECK_SAME(actual, expected) \ + do { \ + if (!checkSame(actual, expected, #actual, #expected, __FILE__, __LINE__)) \ + return false; \ + } while (false) + +#define CHECK(expr) \ + do { \ + if (!(expr)) \ + return fail(JSAPITestString("CHECK failed: " #expr), __FILE__, \ + __LINE__); \ + } while (false) + + void maybeAppendException(JSAPITestString& message) override { + if (JS_IsExceptionPending(cx)) { + message += " -- "; + + js::gc::AutoSuppressGC gcoff(cx); + JS::RootedValue v(cx); + JS_GetPendingException(cx, &v); + JS_ClearPendingException(cx); + JS::Rooted<JSString*> s(cx, JS::ToString(cx, v)); + if (s) { + if (JS::UniqueChars bytes = JS_EncodeStringToLatin1(cx, s)) { + message += bytes.get(); + } + } + } + } + + static const JSClass* basicGlobalClass() { + static const JSClass c = {"global", JSCLASS_GLOBAL_FLAGS, + &JS::DefaultGlobalClassOps}; + return &c; + } + + protected: + static bool print(JSContext* cx, unsigned argc, JS::Value* vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + JS::Rooted<JSString*> 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 definePrint(); + + virtual JSContext* createContext() { + JSContext* cx = JS_NewContext(8L * 1024 * 1024); + if (!cx) { + return nullptr; + } + JS::SetWarningReporter(cx, &reportWarning); + return cx; + } + + static void reportWarning(JSContext* cx, JSErrorReport* report) { + MOZ_RELEASE_ASSERT(report->isWarning()); + + fprintf(stderr, "%s:%u:%s\n", + report->filename ? report->filename.c_str() : "<no filename>", + (unsigned int)report->lineno, report->message().c_str()); + } + + virtual const JSClass* getGlobalClass() { return basicGlobalClass(); } + + virtual JSObject* createGlobal(JSPrincipals* principals = nullptr); +}; + +class JSAPIFrontendTest : public JSAPITest { + public: + static JSAPIFrontendTest* list; + JSAPIFrontendTest* next; + + JSAPIFrontendTest() : JSAPITest() { + next = list; + list = this; + } + + virtual ~JSAPIFrontendTest() {} + + virtual bool init() { return true; } + virtual void uninit() {} + + virtual bool run() = 0; +}; + +#define BEGIN_TEST_WITH_ATTRIBUTES_AND_EXTRA(testname, attrs, extra) \ + class cls_##testname : public JSAPIRuntimeTest { \ + public: \ + virtual const char* name() override { return #testname; } \ + extra virtual bool run(JS::HandleObject global) override attrs + +#define BEGIN_TEST_WITH_ATTRIBUTES(testname, attrs) \ + BEGIN_TEST_WITH_ATTRIBUTES_AND_EXTRA(testname, attrs, ) + +#define BEGIN_TEST(testname) BEGIN_TEST_WITH_ATTRIBUTES(testname, ) + +#define BEGIN_FRONTEND_TEST_WITH_ATTRIBUTES_AND_EXTRA(testname, attrs, extra) \ + class cls_##testname : public JSAPIFrontendTest { \ + public: \ + virtual const char* name() override { return #testname; } \ + extra virtual bool run() override attrs + +#define BEGIN_FRONTEND_TEST_WITH_ATTRIBUTES(testname, attrs) \ + BEGIN_FRONTEND_TEST_WITH_ATTRIBUTES_AND_EXTRA(testname, attrs, ) + +#define BEGIN_FRONTEND_TEST(testname) \ + BEGIN_FRONTEND_TEST_WITH_ATTRIBUTES(testname, ) + +#define BEGIN_REUSABLE_TEST(testname) \ + BEGIN_TEST_WITH_ATTRIBUTES_AND_EXTRA( \ + testname, , cls_##testname() \ + : JSAPIRuntimeTest() { reuseGlobal = true; }) + +#define END_TEST(testname) \ + } \ + ; \ + static cls_##testname cls_##testname##_instance; + +/* + * A "fixture" is a subclass of JSAPIRuntimeTest that holds common definitions + * for a set of tests. Each test that wants to use the fixture should use + * BEGIN_FIXTURE_TEST and END_FIXTURE_TEST, just as one would use BEGIN_TEST and + * END_TEST, but include the fixture class as the first argument. The fixture + * class's declarations are then in scope for the test bodies. + */ + +#define BEGIN_FIXTURE_TEST(fixture, testname) \ + class cls_##testname : public fixture { \ + public: \ + virtual const char* name() override { return #testname; } \ + virtual bool run(JS::HandleObject global) override + +#define END_FIXTURE_TEST(fixture, testname) \ + } \ + ; \ + static cls_##testname cls_##testname##_instance; + +/* + * A class for creating and managing one temporary file. + * + * We could use the ISO C temporary file functions here, but those try to + * create files in the root directory on Windows, which fails for users + * without Administrator privileges. + */ +class TempFile { + const char* name; + FILE* stream; + + public: + TempFile() : name(), stream() {} + ~TempFile() { + if (stream) { + close(); + } + if (name) { + remove(); + } + } + + /* + * Return a stream for a temporary file named |fileName|. Infallible. + * Use only once per TempFile instance. If the file is not explicitly + * closed and deleted via the member functions below, this object's + * destructor will clean them up. + */ + FILE* open(const char* fileName) { + stream = fopen(fileName, "wb+"); + if (!stream) { + fprintf(stderr, "error opening temporary file '%s': %s\n", fileName, + strerror(errno)); + exit(1); + } + name = fileName; + return stream; + } + + /* Close the temporary file's stream. */ + void close() { + if (fclose(stream) == EOF) { + fprintf(stderr, "error closing temporary file '%s': %s\n", name, + strerror(errno)); + exit(1); + } + stream = nullptr; + } + + /* Delete the temporary file. */ + void remove() { + if (::remove(name) != 0) { + fprintf(stderr, "error deleting temporary file '%s': %s\n", name, + strerror(errno)); + exit(1); + } + name = nullptr; + } +}; + +// Just a wrapper around JSPrincipals that allows static construction. +class TestJSPrincipals : public JSPrincipals { + public: + explicit TestJSPrincipals(int rc = 0) : JSPrincipals() { refcount = rc; } + + bool write(JSContext* cx, JSStructuredCloneWriter* writer) override { + MOZ_ASSERT(false, "not implemented"); + return false; + } + + bool isSystemOrAddonPrincipal() override { return true; } +}; + +// A class that simulates externally memory-managed data, for testing with +// array buffers. +class ExternalData { + char* contents_; + size_t len_; + bool uniquePointerCreated_ = false; + + public: + explicit ExternalData(const char* str) + : contents_(strdup(str)), len_(strlen(str) + 1) {} + + size_t len() const { return len_; } + void* contents() const { return contents_; } + char* asString() const { return contents_; } + bool wasFreed() const { return !contents_; } + + void free() { + MOZ_ASSERT(!wasFreed()); + ::free(contents_); + contents_ = nullptr; + } + + mozilla::UniquePtr<void, JS::BufferContentsDeleter> pointer() { + MOZ_ASSERT(!uniquePointerCreated_, + "Not allowed to create multiple unique pointers to contents"); + uniquePointerCreated_ = true; + return {contents_, {ExternalData::freeCallback, this}}; + } + + static void freeCallback(void* contents, void* userData) { + auto self = static_cast<ExternalData*>(userData); + MOZ_ASSERT(self->contents() == contents); + self->free(); + } +}; + +class AutoGCParameter { + JSContext* cx_; + JSGCParamKey key_; + uint32_t value_; + + public: + explicit AutoGCParameter(JSContext* cx, JSGCParamKey key, uint32_t value) + : cx_(cx), key_(key), value_() { + value_ = JS_GetGCParameter(cx, key); + JS_SetGCParameter(cx, key, value); + } + ~AutoGCParameter() { JS_SetGCParameter(cx_, key_, value_); } +}; + +#ifdef JS_GC_ZEAL +/* + * Temporarily disable the GC zeal setting. This is only useful in tests that + * need very explicit GC behavior and should not be used elsewhere. + */ +class AutoLeaveZeal { + JSContext* cx_; + uint32_t zealBits_; + uint32_t frequency_; + + public: + explicit 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() { + 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 +class AutoLeaveZeal { + public: + explicit AutoLeaveZeal(JSContext* cx) {} +}; +#endif + +#endif /* jsapi_tests_tests_h */ diff --git a/js/src/jsapi-tests/testsJit.cpp b/js/src/jsapi-tests/testsJit.cpp new file mode 100644 index 0000000000..6e270d0659 --- /dev/null +++ b/js/src/jsapi-tests/testsJit.cpp @@ -0,0 +1,79 @@ +/* -*- 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/testsJit.h" + +#include "jit/JitCommon.h" +#include "jit/Linker.h" + +#include "jit/MacroAssembler-inl.h" + +// On entry to the JIT code, save every register. +void PrepareJit(js::jit::MacroAssembler& masm) { + using namespace js::jit; +#if defined(JS_CODEGEN_ARM64) + masm.Mov(PseudoStackPointer64, sp); + masm.SetStackPointer64(PseudoStackPointer64); +#endif + AllocatableRegisterSet regs(RegisterSet::All()); + LiveRegisterSet save(regs.asLiveSet()); +#if defined(JS_CODEGEN_ARM) + save.add(js::jit::d15); +#endif +#if defined(JS_CODEGEN_MIPS32) || defined(JS_CODEGEN_MIPS64) || \ + defined(JS_CODEGEN_LOONG64) || defined(JS_CODEGEN_RISCV64) + save.add(js::jit::ra); +#elif defined(JS_USE_LINK_REGISTER) + save.add(js::jit::lr); +#endif + masm.PushRegsInMask(save); +} + +// Generate the exit path of the JIT code, which restores every register. Then, +// make it executable and run it. +bool ExecuteJit(JSContext* cx, js::jit::MacroAssembler& masm) { + using namespace js::jit; + AllocatableRegisterSet regs(RegisterSet::All()); + LiveRegisterSet save(regs.asLiveSet()); +#if defined(JS_CODEGEN_ARM) + save.add(js::jit::d15); +#endif +#if defined(JS_CODEGEN_MIPS32) || defined(JS_CODEGEN_MIPS64) || \ + defined(JS_CODEGEN_LOONG64) || defined(JS_CODEGEN_RISCV64) + save.add(js::jit::ra); +#elif defined(JS_USE_LINK_REGISTER) + save.add(js::jit::lr); +#endif + masm.PopRegsInMask(save); +#if defined(JS_CODEGEN_ARM64) + // Return using the value popped into x30. + masm.abiret(); + + // Reset stack pointer. + masm.SetStackPointer64(PseudoStackPointer64); +#else + // Exit the JIT-ed code using the ABI return style. + masm.abiret(); +#endif + + if (masm.oom()) { + return false; + } + + JitCode* code = nullptr; + { + Linker linker(masm); + code = linker.newCode(cx, CodeKind::Other); + if (!code) { + return false; + } + } + + JS::AutoSuppressGCAnalysis suppress; + EnterTest test = code->as<EnterTest>(); + CALL_GENERATED_0(test); + return true; +} diff --git a/js/src/jsapi-tests/testsJit.h b/js/src/jsapi-tests/testsJit.h new file mode 100644 index 0000000000..0984e71c96 --- /dev/null +++ b/js/src/jsapi-tests/testsJit.h @@ -0,0 +1,21 @@ +/* -*- 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/. */ + +#ifndef jsapi_tests_testsJit_h +#define jsapi_tests_testsJit_h + +#include "jit/MacroAssembler.h" + +typedef void (*EnterTest)(); + +// On entry to the JIT code, save every register. +void PrepareJit(js::jit::MacroAssembler& masm); + +// Generate the exit path of the JIT code, which restores every register. Then, +// make it executable and run it. +bool ExecuteJit(JSContext* cx, js::jit::MacroAssembler& masm); + +#endif /* !jsapi_tests_testsJit_h */ diff --git a/js/src/jsapi-tests/valueABI.c b/js/src/jsapi-tests/valueABI.c new file mode 100644 index 0000000000..4dc10e1d48 --- /dev/null +++ b/js/src/jsapi-tests/valueABI.c @@ -0,0 +1,25 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sw=2 et tw=80 ft=c: + * 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.h" + +/* See testValueABI.cpp */ + +bool C_ValueToObject(JSContext* cx, jsval v, JSObject** obj) { + return JS_ValueToObject(cx, v, obj); +} + +jsval C_GetEmptyStringValue(JSContext* cx) { + return JS_GetEmptyStringValue(cx); +} + +size_t C_jsvalAlignmentTest() { + typedef struct { + char c; + jsval v; + } AlignTest; + return sizeof(AlignTest); +} |