From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 21:33:14 +0200 Subject: Adding upstream version 115.7.0esr. Signed-off-by: Daniel Baumann --- js/src/jsapi-tests/Makefile.in | 9 + js/src/jsapi-tests/README | 173 ++++ js/src/jsapi-tests/hidePointer.cpp | 25 + js/src/jsapi-tests/jsapi-tests-gdb.py.in | 8 + js/src/jsapi-tests/moz.build | 200 +++++ js/src/jsapi-tests/selfTest.cpp | 19 + js/src/jsapi-tests/testAddPropertyPropcache.cpp | 67 ++ js/src/jsapi-tests/testArgumentsObject.cpp | 91 ++ js/src/jsapi-tests/testArrayBuffer.cpp | 449 ++++++++++ js/src/jsapi-tests/testArrayBufferOrViewAPI.cpp | 111 +++ js/src/jsapi-tests/testArrayBufferView.cpp | 177 ++++ .../testArrayBufferWithUserOwnedContents.cpp | 54 ++ js/src/jsapi-tests/testAssemblerBuffer.cpp | 588 +++++++++++++ js/src/jsapi-tests/testAtomicOperations.cpp | 304 +++++++ .../testAtomizeUtf8NonAsciiLatin1CodePoint.cpp | 212 +++++ .../jsapi-tests/testAtomizeWithoutActiveZone.cpp | 52 ++ js/src/jsapi-tests/testAvlTree.cpp | 383 ++++++++ js/src/jsapi-tests/testBigInt.cpp | 768 ++++++++++++++++ js/src/jsapi-tests/testBoundFunction.cpp | 32 + js/src/jsapi-tests/testBug604087.cpp | 91 ++ js/src/jsapi-tests/testCallArgs.cpp | 85 ++ .../testCallNonGenericMethodOnProxy.cpp | 87 ++ js/src/jsapi-tests/testChromeBuffer.cpp | 257 ++++++ js/src/jsapi-tests/testCompileNonSyntactic.cpp | 114 +++ js/src/jsapi-tests/testCompileScript.cpp | 242 +++++ js/src/jsapi-tests/testCompileUtf8.cpp | 300 +++++++ js/src/jsapi-tests/testDateToLocaleString.cpp | 63 ++ js/src/jsapi-tests/testDebugger.cpp | 62 ++ js/src/jsapi-tests/testDecimalNumber.cpp | 289 ++++++ js/src/jsapi-tests/testDeduplication.cpp | 113 +++ js/src/jsapi-tests/testDeepFreeze.cpp | 58 ++ .../testDefineGetterSetterNonEnumerable.cpp | 51 ++ js/src/jsapi-tests/testDefineProperty.cpp | 26 + .../jsapi-tests/testDeflateStringToUTF8Buffer.cpp | 162 ++++ .../testDifferentNewTargetInvokeConstructor.cpp | 38 + js/src/jsapi-tests/testEmptyWindowIsOmitted.cpp | 155 ++++ js/src/jsapi-tests/testErrorCopying.cpp | 33 + js/src/jsapi-tests/testErrorInterceptor.cpp | 145 +++ js/src/jsapi-tests/testErrorInterceptorGC.cpp | 32 + js/src/jsapi-tests/testErrorLineOfContext.cpp | 74 ++ js/src/jsapi-tests/testException.cpp | 92 ++ js/src/jsapi-tests/testExecuteInJSMEnvironment.cpp | 107 +++ js/src/jsapi-tests/testExternalStrings.cpp | 52 ++ js/src/jsapi-tests/testFindSCCs.cpp | 241 +++++ js/src/jsapi-tests/testForOfIterator.cpp | 50 ++ .../jsapi-tests/testForceLexicalInitialization.cpp | 39 + js/src/jsapi-tests/testForwardSetProperty.cpp | 98 +++ .../testFreshGlobalEvalRedefinition.cpp | 63 ++ js/src/jsapi-tests/testFrontendCompileStencil.cpp | 68 ++ js/src/jsapi-tests/testFrontendJSON.cpp | 200 +++++ js/src/jsapi-tests/testFunctionBinding.cpp | 83 ++ js/src/jsapi-tests/testFunctionNonSyntactic.cpp | 95 ++ js/src/jsapi-tests/testFunctionProperties.cpp | 26 + js/src/jsapi-tests/testGCAllocator.cpp | 354 ++++++++ js/src/jsapi-tests/testGCCellPtr.cpp | 58 ++ js/src/jsapi-tests/testGCChunkPool.cpp | 74 ++ js/src/jsapi-tests/testGCExactRooting.cpp | 915 +++++++++++++++++++ js/src/jsapi-tests/testGCFinalizeCallback.cpp | 202 +++++ js/src/jsapi-tests/testGCGrayMarking.cpp | 803 +++++++++++++++++ js/src/jsapi-tests/testGCHeapBarriers.cpp | 828 ++++++++++++++++++ js/src/jsapi-tests/testGCHooks.cpp | 279 ++++++ js/src/jsapi-tests/testGCMarking.cpp | 448 ++++++++++ js/src/jsapi-tests/testGCOutOfMemory.cpp | 79 ++ js/src/jsapi-tests/testGCStoreBufferRemoval.cpp | 110 +++ js/src/jsapi-tests/testGCUniqueId.cpp | 125 +++ js/src/jsapi-tests/testGCWeakCache.cpp | 698 +++++++++++++++ js/src/jsapi-tests/testGetPropertyDescriptor.cpp | 54 ++ js/src/jsapi-tests/testHashTable.cpp | 564 ++++++++++++ js/src/jsapi-tests/testIndexToString.cpp | 120 +++ js/src/jsapi-tests/testInformalValueTypeName.cpp | 43 + js/src/jsapi-tests/testIntString.cpp | 49 ++ js/src/jsapi-tests/testIntern.cpp | 51 ++ js/src/jsapi-tests/testIntlAvailableLocales.cpp | 68 ++ js/src/jsapi-tests/testIsInsideNursery.cpp | 31 + js/src/jsapi-tests/testIteratorObject.cpp | 34 + js/src/jsapi-tests/testJSEvaluateScript.cpp | 38 + js/src/jsapi-tests/testJSON.cpp | 112 +++ js/src/jsapi-tests/testJitABIcalls.cpp | 718 +++++++++++++++ js/src/jsapi-tests/testJitDCEinGVN.cpp | 147 ++++ js/src/jsapi-tests/testJitFoldsTo.cpp | 262 ++++++ js/src/jsapi-tests/testJitGVN.cpp | 299 +++++++ js/src/jsapi-tests/testJitMacroAssembler.cpp | 816 +++++++++++++++++ js/src/jsapi-tests/testJitMinimalFunc.h | 123 +++ .../testJitMoveEmitterCycles-mips32.cpp | 480 ++++++++++ js/src/jsapi-tests/testJitMoveEmitterCycles.cpp | 616 +++++++++++++ js/src/jsapi-tests/testJitRValueAlloc.cpp | 256 ++++++ js/src/jsapi-tests/testJitRangeAnalysis.cpp | 368 ++++++++ js/src/jsapi-tests/testJitRegisterSet.cpp | 211 +++++ js/src/jsapi-tests/testLargeArrayBuffers.cpp | 166 ++++ js/src/jsapi-tests/testLookup.cpp | 102 +++ js/src/jsapi-tests/testLooselyEqual.cpp | 186 ++++ js/src/jsapi-tests/testMappedArrayBuffer.cpp | 195 +++++ js/src/jsapi-tests/testMemoryAssociation.cpp | 44 + js/src/jsapi-tests/testMutedErrors.cpp | 99 +++ js/src/jsapi-tests/testNewObject.cpp | 255 ++++++ .../jsapi-tests/testNewTargetInvokeConstructor.cpp | 25 + js/src/jsapi-tests/testNullRoot.cpp | 24 + js/src/jsapi-tests/testNumberToString.cpp | 133 +++ js/src/jsapi-tests/testOOM.cpp | 114 +++ .../jsapi-tests/testObjectEmulatingUndefined.cpp | 109 +++ js/src/jsapi-tests/testObjectSwap.cpp | 445 ++++++++++ js/src/jsapi-tests/testParseJSON.cpp | 357 ++++++++ js/src/jsapi-tests/testParserAtom.cpp | 445 ++++++++++ js/src/jsapi-tests/testPersistentRooted.cpp | 212 +++++ js/src/jsapi-tests/testPreserveJitCode.cpp | 100 +++ js/src/jsapi-tests/testPrintError.cpp | 125 +++ js/src/jsapi-tests/testPrintf.cpp | 69 ++ js/src/jsapi-tests/testPrivateGCThingValue.cpp | 64 ++ js/src/jsapi-tests/testProfileStrings.cpp | 225 +++++ js/src/jsapi-tests/testPromise.cpp | 170 ++++ js/src/jsapi-tests/testPropCache.cpp | 47 + js/src/jsapi-tests/testPropertyKey.cpp | 69 ++ js/src/jsapi-tests/testRecordTupleToSource.cpp | 45 + js/src/jsapi-tests/testRegExp.cpp | 65 ++ js/src/jsapi-tests/testResolveRecursion.cpp | 184 ++++ js/src/jsapi-tests/testResult.cpp | 98 +++ js/src/jsapi-tests/testSABAccounting.cpp | 31 + js/src/jsapi-tests/testSameValue.cpp | 26 + js/src/jsapi-tests/testSavedStacks.cpp | 396 +++++++++ js/src/jsapi-tests/testScriptInfo.cpp | 56 ++ js/src/jsapi-tests/testScriptObject.cpp | 214 +++++ js/src/jsapi-tests/testScriptSourceCompression.cpp | 551 ++++++++++++ js/src/jsapi-tests/testSetProperty.cpp | 54 ++ .../testSetPropertyIgnoringNamedGetter.cpp | 73 ++ .../testSharedImmutableStringsCache.cpp | 72 ++ js/src/jsapi-tests/testSliceBudget.cpp | 117 +++ js/src/jsapi-tests/testSlowScript.cpp | 72 ++ js/src/jsapi-tests/testSourcePolicy.cpp | 57 ++ js/src/jsapi-tests/testSparseBitmap.cpp | 115 +++ js/src/jsapi-tests/testStencil.cpp | 881 +++++++++++++++++++ js/src/jsapi-tests/testStringBuffer.cpp | 27 + js/src/jsapi-tests/testStringIsArrayIndex.cpp | 81 ++ js/src/jsapi-tests/testStructuredClone.cpp | 365 ++++++++ js/src/jsapi-tests/testSymbol.cpp | 79 ++ .../jsapi-tests/testThreadingConditionVariable.cpp | 220 +++++ js/src/jsapi-tests/testThreadingExclusiveData.cpp | 84 ++ js/src/jsapi-tests/testThreadingMutex.cpp | 33 + js/src/jsapi-tests/testThreadingThread.cpp | 107 +++ .../jsapi-tests/testToSignedOrUnsignedInteger.cpp | 68 ++ js/src/jsapi-tests/testTypedArrays.cpp | 278 ++++++ js/src/jsapi-tests/testUTF8.cpp | 47 + js/src/jsapi-tests/testUbiNode.cpp | 971 +++++++++++++++++++++ js/src/jsapi-tests/testUncaughtSymbol.cpp | 57 ++ js/src/jsapi-tests/testValueABI.cpp | 53 ++ js/src/jsapi-tests/testWasmLEB128.cpp | 173 ++++ js/src/jsapi-tests/testWeakMap.cpp | 255 ++++++ js/src/jsapi-tests/testWindowNonConfigurable.cpp | 69 ++ js/src/jsapi-tests/tests.cpp | 292 +++++++ js/src/jsapi-tests/tests.h | 621 +++++++++++++ js/src/jsapi-tests/testsJit.cpp | 80 ++ js/src/jsapi-tests/testsJit.h | 21 + js/src/jsapi-tests/valueABI.c | 25 + 152 files changed, 28739 insertions(+) create mode 100644 js/src/jsapi-tests/Makefile.in create mode 100644 js/src/jsapi-tests/README create mode 100644 js/src/jsapi-tests/hidePointer.cpp create mode 100644 js/src/jsapi-tests/jsapi-tests-gdb.py.in create mode 100644 js/src/jsapi-tests/moz.build create mode 100644 js/src/jsapi-tests/selfTest.cpp create mode 100644 js/src/jsapi-tests/testAddPropertyPropcache.cpp create mode 100644 js/src/jsapi-tests/testArgumentsObject.cpp create mode 100644 js/src/jsapi-tests/testArrayBuffer.cpp create mode 100644 js/src/jsapi-tests/testArrayBufferOrViewAPI.cpp create mode 100644 js/src/jsapi-tests/testArrayBufferView.cpp create mode 100644 js/src/jsapi-tests/testArrayBufferWithUserOwnedContents.cpp create mode 100644 js/src/jsapi-tests/testAssemblerBuffer.cpp create mode 100644 js/src/jsapi-tests/testAtomicOperations.cpp create mode 100644 js/src/jsapi-tests/testAtomizeUtf8NonAsciiLatin1CodePoint.cpp create mode 100644 js/src/jsapi-tests/testAtomizeWithoutActiveZone.cpp create mode 100644 js/src/jsapi-tests/testAvlTree.cpp create mode 100644 js/src/jsapi-tests/testBigInt.cpp create mode 100644 js/src/jsapi-tests/testBoundFunction.cpp create mode 100644 js/src/jsapi-tests/testBug604087.cpp create mode 100644 js/src/jsapi-tests/testCallArgs.cpp create mode 100644 js/src/jsapi-tests/testCallNonGenericMethodOnProxy.cpp create mode 100644 js/src/jsapi-tests/testChromeBuffer.cpp create mode 100644 js/src/jsapi-tests/testCompileNonSyntactic.cpp create mode 100644 js/src/jsapi-tests/testCompileScript.cpp create mode 100644 js/src/jsapi-tests/testCompileUtf8.cpp create mode 100644 js/src/jsapi-tests/testDateToLocaleString.cpp create mode 100644 js/src/jsapi-tests/testDebugger.cpp create mode 100644 js/src/jsapi-tests/testDecimalNumber.cpp create mode 100644 js/src/jsapi-tests/testDeduplication.cpp create mode 100644 js/src/jsapi-tests/testDeepFreeze.cpp create mode 100644 js/src/jsapi-tests/testDefineGetterSetterNonEnumerable.cpp create mode 100644 js/src/jsapi-tests/testDefineProperty.cpp create mode 100644 js/src/jsapi-tests/testDeflateStringToUTF8Buffer.cpp create mode 100644 js/src/jsapi-tests/testDifferentNewTargetInvokeConstructor.cpp create mode 100644 js/src/jsapi-tests/testEmptyWindowIsOmitted.cpp create mode 100644 js/src/jsapi-tests/testErrorCopying.cpp create mode 100644 js/src/jsapi-tests/testErrorInterceptor.cpp create mode 100644 js/src/jsapi-tests/testErrorInterceptorGC.cpp create mode 100644 js/src/jsapi-tests/testErrorLineOfContext.cpp create mode 100644 js/src/jsapi-tests/testException.cpp create mode 100644 js/src/jsapi-tests/testExecuteInJSMEnvironment.cpp create mode 100644 js/src/jsapi-tests/testExternalStrings.cpp create mode 100644 js/src/jsapi-tests/testFindSCCs.cpp create mode 100644 js/src/jsapi-tests/testForOfIterator.cpp create mode 100644 js/src/jsapi-tests/testForceLexicalInitialization.cpp create mode 100644 js/src/jsapi-tests/testForwardSetProperty.cpp create mode 100644 js/src/jsapi-tests/testFreshGlobalEvalRedefinition.cpp create mode 100644 js/src/jsapi-tests/testFrontendCompileStencil.cpp create mode 100644 js/src/jsapi-tests/testFrontendJSON.cpp create mode 100644 js/src/jsapi-tests/testFunctionBinding.cpp create mode 100644 js/src/jsapi-tests/testFunctionNonSyntactic.cpp create mode 100644 js/src/jsapi-tests/testFunctionProperties.cpp create mode 100644 js/src/jsapi-tests/testGCAllocator.cpp create mode 100644 js/src/jsapi-tests/testGCCellPtr.cpp create mode 100644 js/src/jsapi-tests/testGCChunkPool.cpp create mode 100644 js/src/jsapi-tests/testGCExactRooting.cpp create mode 100644 js/src/jsapi-tests/testGCFinalizeCallback.cpp create mode 100644 js/src/jsapi-tests/testGCGrayMarking.cpp create mode 100644 js/src/jsapi-tests/testGCHeapBarriers.cpp create mode 100644 js/src/jsapi-tests/testGCHooks.cpp create mode 100644 js/src/jsapi-tests/testGCMarking.cpp create mode 100644 js/src/jsapi-tests/testGCOutOfMemory.cpp create mode 100644 js/src/jsapi-tests/testGCStoreBufferRemoval.cpp create mode 100644 js/src/jsapi-tests/testGCUniqueId.cpp create mode 100644 js/src/jsapi-tests/testGCWeakCache.cpp create mode 100644 js/src/jsapi-tests/testGetPropertyDescriptor.cpp create mode 100644 js/src/jsapi-tests/testHashTable.cpp create mode 100644 js/src/jsapi-tests/testIndexToString.cpp create mode 100644 js/src/jsapi-tests/testInformalValueTypeName.cpp create mode 100644 js/src/jsapi-tests/testIntString.cpp create mode 100644 js/src/jsapi-tests/testIntern.cpp create mode 100644 js/src/jsapi-tests/testIntlAvailableLocales.cpp create mode 100644 js/src/jsapi-tests/testIsInsideNursery.cpp create mode 100644 js/src/jsapi-tests/testIteratorObject.cpp create mode 100644 js/src/jsapi-tests/testJSEvaluateScript.cpp create mode 100644 js/src/jsapi-tests/testJSON.cpp create mode 100644 js/src/jsapi-tests/testJitABIcalls.cpp create mode 100644 js/src/jsapi-tests/testJitDCEinGVN.cpp create mode 100644 js/src/jsapi-tests/testJitFoldsTo.cpp create mode 100644 js/src/jsapi-tests/testJitGVN.cpp create mode 100644 js/src/jsapi-tests/testJitMacroAssembler.cpp create mode 100644 js/src/jsapi-tests/testJitMinimalFunc.h create mode 100644 js/src/jsapi-tests/testJitMoveEmitterCycles-mips32.cpp create mode 100644 js/src/jsapi-tests/testJitMoveEmitterCycles.cpp create mode 100644 js/src/jsapi-tests/testJitRValueAlloc.cpp create mode 100644 js/src/jsapi-tests/testJitRangeAnalysis.cpp create mode 100644 js/src/jsapi-tests/testJitRegisterSet.cpp create mode 100644 js/src/jsapi-tests/testLargeArrayBuffers.cpp create mode 100644 js/src/jsapi-tests/testLookup.cpp create mode 100644 js/src/jsapi-tests/testLooselyEqual.cpp create mode 100644 js/src/jsapi-tests/testMappedArrayBuffer.cpp create mode 100644 js/src/jsapi-tests/testMemoryAssociation.cpp create mode 100644 js/src/jsapi-tests/testMutedErrors.cpp create mode 100644 js/src/jsapi-tests/testNewObject.cpp create mode 100644 js/src/jsapi-tests/testNewTargetInvokeConstructor.cpp create mode 100644 js/src/jsapi-tests/testNullRoot.cpp create mode 100644 js/src/jsapi-tests/testNumberToString.cpp create mode 100644 js/src/jsapi-tests/testOOM.cpp create mode 100644 js/src/jsapi-tests/testObjectEmulatingUndefined.cpp create mode 100644 js/src/jsapi-tests/testObjectSwap.cpp create mode 100644 js/src/jsapi-tests/testParseJSON.cpp create mode 100644 js/src/jsapi-tests/testParserAtom.cpp create mode 100644 js/src/jsapi-tests/testPersistentRooted.cpp create mode 100644 js/src/jsapi-tests/testPreserveJitCode.cpp create mode 100644 js/src/jsapi-tests/testPrintError.cpp create mode 100644 js/src/jsapi-tests/testPrintf.cpp create mode 100644 js/src/jsapi-tests/testPrivateGCThingValue.cpp create mode 100644 js/src/jsapi-tests/testProfileStrings.cpp create mode 100644 js/src/jsapi-tests/testPromise.cpp create mode 100644 js/src/jsapi-tests/testPropCache.cpp create mode 100644 js/src/jsapi-tests/testPropertyKey.cpp create mode 100644 js/src/jsapi-tests/testRecordTupleToSource.cpp create mode 100644 js/src/jsapi-tests/testRegExp.cpp create mode 100644 js/src/jsapi-tests/testResolveRecursion.cpp create mode 100644 js/src/jsapi-tests/testResult.cpp create mode 100644 js/src/jsapi-tests/testSABAccounting.cpp create mode 100644 js/src/jsapi-tests/testSameValue.cpp create mode 100644 js/src/jsapi-tests/testSavedStacks.cpp create mode 100644 js/src/jsapi-tests/testScriptInfo.cpp create mode 100644 js/src/jsapi-tests/testScriptObject.cpp create mode 100644 js/src/jsapi-tests/testScriptSourceCompression.cpp create mode 100644 js/src/jsapi-tests/testSetProperty.cpp create mode 100644 js/src/jsapi-tests/testSetPropertyIgnoringNamedGetter.cpp create mode 100644 js/src/jsapi-tests/testSharedImmutableStringsCache.cpp create mode 100644 js/src/jsapi-tests/testSliceBudget.cpp create mode 100644 js/src/jsapi-tests/testSlowScript.cpp create mode 100644 js/src/jsapi-tests/testSourcePolicy.cpp create mode 100644 js/src/jsapi-tests/testSparseBitmap.cpp create mode 100644 js/src/jsapi-tests/testStencil.cpp create mode 100644 js/src/jsapi-tests/testStringBuffer.cpp create mode 100644 js/src/jsapi-tests/testStringIsArrayIndex.cpp create mode 100644 js/src/jsapi-tests/testStructuredClone.cpp create mode 100644 js/src/jsapi-tests/testSymbol.cpp create mode 100644 js/src/jsapi-tests/testThreadingConditionVariable.cpp create mode 100644 js/src/jsapi-tests/testThreadingExclusiveData.cpp create mode 100644 js/src/jsapi-tests/testThreadingMutex.cpp create mode 100644 js/src/jsapi-tests/testThreadingThread.cpp create mode 100644 js/src/jsapi-tests/testToSignedOrUnsignedInteger.cpp create mode 100644 js/src/jsapi-tests/testTypedArrays.cpp create mode 100644 js/src/jsapi-tests/testUTF8.cpp create mode 100644 js/src/jsapi-tests/testUbiNode.cpp create mode 100644 js/src/jsapi-tests/testUncaughtSymbol.cpp create mode 100644 js/src/jsapi-tests/testValueABI.cpp create mode 100644 js/src/jsapi-tests/testWasmLEB128.cpp create mode 100644 js/src/jsapi-tests/testWeakMap.cpp create mode 100644 js/src/jsapi-tests/testWindowNonConfigurable.cpp create mode 100644 js/src/jsapi-tests/tests.cpp create mode 100644 js/src/jsapi-tests/tests.h create mode 100644 js/src/jsapi-tests/testsJit.cpp create mode 100644 js/src/jsapi-tests/testsJit.h create mode 100644 js/src/jsapi-tests/valueABI.c (limited to 'js/src/jsapi-tests') 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 _. (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 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..41ee139698 --- /dev/null +++ b/js/src/jsapi-tests/moz.build @@ -0,0 +1,200 @@ +# -*- 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", + "testChromeBuffer.cpp", + "testCompileNonSyntactic.cpp", + "testCompileScript.cpp", + "testCompileUtf8.cpp", + "testDateToLocaleString.cpp", + "testDebugger.cpp", + "testDeduplication.cpp", + "testDeepFreeze.cpp", + "testDefineGetterSetterNonEnumerable.cpp", + "testDefineProperty.cpp", + "testDeflateStringToUTF8Buffer.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", + "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", + "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", + "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", + "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 CONFIG["JS_HAS_INTL_API"]: + UNIFIED_SOURCES += [ + "testDecimalNumber.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 +bool ExhaustiveTest(const char funcode[]) { + RootedValue v(cx); + EVAL(funcode, &v); + + EVAL(CALL_CODES[ArgCount], &v); + Rooted argsobj(cx, + &v.toObjectOrNull()->as()); + + JS::RootedValueArray 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 +static void ClearElements(JS::RootedValueArray& 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..fd74993893 --- /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(data) = MAGIC_VALUE_2; + } + CHECK(JS_GetElement(cx, view, 0, &v)); + CHECK(v.isInt32(MAGIC_VALUE_2)); + + // Steal the contents + void* 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, 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(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"); + + // The buffer takes ownership of the data. + JS::RootedObject buffer( + cx, JS::NewExternalArrayBuffer(cx, data.len(), data.contents(), + &ExternalData::freeCallback, &data)); + 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(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"); + + // When not passing a free function, the buffer doesn't own the data. + JS::RootedObject buffer( + cx, JS::NewExternalArrayBuffer(cx, data.len(), data.contents(), nullptr)); + 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(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); + JS::RootedObject buffer( + cx, JS::NewExternalArrayBuffer(cx, data.len(), data.contents(), + &ExternalData::freeCallback, &data)); + 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(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"); + JS::RootedObject externalBuffer( + cx, JS::NewExternalArrayBuffer(cx, data.len(), data.contents(), + &ExternalData::freeCallback, &data)); + 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(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::NewExternalArrayBuffer( + cx, data1.len(), data1.contents(), nullptr)); + + CHECK(buffer1); + + ExternalData data2("Six"); + JS::RootedObject buffer2(cx, JS::NewExternalArrayBuffer( + cx, data2.len(), data2.contents(), nullptr)); + + 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(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::NewExternalArrayBuffer(cx, data1.len(), data1.contents(), nullptr); + } + CHECK(buffer1); + CHECK(JS_WrapObject(cx, &buffer1)); + + ExternalData data2("Six"); + JS::RootedObject buffer2(cx, JS::NewExternalArrayBuffer( + cx, data2.len(), data2.contents(), nullptr)); + + 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(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::NewExternalArrayBuffer(cx, data.len(), data.contents(), nullptr)); + + 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 + +#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 +static JSObject* CreateObj(JSContext* cx, size_t len) { + return ViewType::create(cx, len).asObject(); +} + +template <> +JSObject* CreateObj(JSContext* cx, size_t len) { + JS::Rooted 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>(cx, otherGlobal))); + CHECK((TestType>(cx, otherGlobal))); + CHECK((TestType>(cx, otherGlobal))); + CHECK((TestType>(cx, otherGlobal))); + CHECK((TestType>(cx, otherGlobal))); + CHECK((TestType>(cx, otherGlobal))); + CHECK((TestType>(cx, otherGlobal))); + CHECK((TestType>(cx, otherGlobal))); + CHECK((TestType>(cx, otherGlobal))); + CHECK((TestType(cx, otherGlobal))); + CHECK((TestType(cx, otherGlobal))); + + return true; +} + +template +bool TestType(JSContext* cx, Handle otherGlobal) { + JS::Rooted obj(cx, CreateObj(cx, 8)); + CHECK(obj); + + // Any of these should be creatable as an ArrayBufferOrView. + JS::Rooted 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) { + // Check that we can't make an API object of a different type. + JS::Rooted 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 u16array(cx, CreateObj(cx, 10)); + CHECK(u16array); + auto deny = APIType::fromObject(u16array); + CHECK(!deny); + deny = APIType::unwrap(u16array); + CHECK(!deny); + } + + CHECK_EQUAL(abov.asObject(), obj); + + JS::Rooted wrapped(cx); + { + AutoRealm ar(cx, otherGlobal); + wrapped = CreateObj(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 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..60cae01e1b --- /dev/null +++ b/js/src/jsapi-tests/testArrayBufferView.cpp @@ -0,0 +1,177 @@ +/* -*- 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 +static JSObject* Create(JSContext* cx, size_t len) { + return ViewType::create(cx, len).asObject(); +} + +template <> +JSObject* Create(JSContext* cx, size_t len) { + JS::Rooted buffer(cx, JS::NewArrayBuffer(cx, len)); + if (!buffer) { + return nullptr; + } + return JS_NewDataView(cx, buffer, 0, len); +} + +template +struct InternalType { + using Type = uint8_t; +}; + +#define INT_TYPE(ExternalType, NativeType, Name) \ + template <> \ + struct InternalType> { \ + using Type = NativeType; \ + }; +JS_FOR_EACH_TYPED_ARRAY(INT_TYPE) +#undef INT_TYPE + +BEGIN_TEST(testArrayBufferView_type) { + CHECK((TestViewType, 7, 7>(cx))); + CHECK((TestViewType, 33, 33>(cx))); + CHECK((TestViewType, 7, 7>(cx))); + CHECK((TestViewType, 3, 6>(cx))); + CHECK((TestViewType, 17, 34>(cx))); + CHECK((TestViewType, 15, 60>(cx))); + CHECK((TestViewType, 8, 32>(cx))); + CHECK((TestViewType, 7, 28>(cx))); + CHECK((TestViewType, 9, 72>(cx))); + CHECK((TestViewType(cx))); + + JS::Rooted hasTypedObject(cx); + EVAL("typeof TypedObject !== 'undefined'", &hasTypedObject); + if (hasTypedObject.isTrue()) { + JS::Rooted tval(cx); + EVAL( + "var T = new TypedObject.StructType({ x: TypedObject.uint32 });\n" + "new T(new ArrayBuffer(4));", + &tval); + + JS::Rooted tobj(cx, &tval.toObject()); + CHECK(!JS_IsArrayBufferViewObject(tobj)); + } + + return true; +} + +template +struct ScalarTypeOf { + static constexpr js::Scalar::Type value = js::Scalar::MaxTypedArrayViewType; +}; + +template +struct ScalarTypeOf> { + static constexpr js::Scalar::Type value = EType; +}; +template +bool TestViewType(JSContext* cx) { + JS::Rooted obj(cx, Create(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::value); + + if (JS_IsTypedArrayObject(unwrapped)) { + CHECK(unwrapped->as().type() == + TypeIDOfType::Type>::id); + } + + bool shared2; + size_t len; + uint8_t* data2 = + reinterpret_cast(view.getLengthAndData(&len, &shared2, nogc)); + CHECK(obj == view.asObject()); + CHECK(data1 == data2); + CHECK(shared1 == shared2); + CHECK(len == ExpectedLength); + + JS::Heap hv(view); + + bool shared3; + size_t len3; + uint8_t* data3 = + reinterpret_cast(hv.getLengthAndData(&len3, &shared3, nogc)); + CHECK(obj == hv.asObject()); + CHECK(data1 == data3); + CHECK(shared1 == shared3); + CHECK(len3 == ExpectedLength); + } + + JS::RealmOptions options; + JS::RootedObject otherGlobal( + cx, JS_NewGlobalObject(cx, basicGlobalClass(), nullptr, + JS::DontFireOnNewGlobalHook, options)); + CHECK(otherGlobal); + + JS::Rooted buffer(cx); + { + AutoRealm ar(cx, otherGlobal); + buffer = JS::NewArrayBuffer(cx, 8); + CHECK(buffer); + CHECK(buffer->as().byteLength() == 8); + } + CHECK(buffer->compartment() == otherGlobal->compartment()); + CHECK(JS_WrapObject(cx, &buffer)); + CHECK(buffer->compartment() == global->compartment()); + + JS::Rooted dataview(cx, JS_NewDataView(cx, buffer, 4, 4)); + CHECK(dataview); + CHECK(dataview->is()); + + JS::Rooted val(cx); + + val = ObjectValue(*dataview); + CHECK(JS_SetProperty(cx, global, "view", val)); + + EVAL("view.buffer", &val); + CHECK(val.toObject().is()); + + CHECK(dataview->compartment() == global->compartment()); + JS::Rooted otherView(cx, js::UncheckedUnwrap(dataview)); + CHECK(otherView->compartment() == otherGlobal->compartment()); + JS::Rooted 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 // 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 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( + 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..7979e6822a --- /dev/null +++ b/js/src/jsapi-tests/testAssemblerBuffer.cpp @@ -0,0 +1,588 @@ +/* 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 + +#include "jit/shared/IonAssemblerBufferWithConstantPools.h" +#include "jsapi-tests/tests.h" +#include "vm/JSAtom.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(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(loadAddr); + uint32_t index = *load & 0xffff; + MOZ_ASSERT(*load == (0xc1cc0000 | index), + "Expected constant pool load(index)"); + ptrdiff_t offset = reinterpret_cast(constPoolAddr) - + reinterpret_cast(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(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..06000f7676 --- /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/WasmJS.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 p = \ + SharedMem::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 p2 = \ + SharedMem::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 p = \ + SharedMem::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 p2 = \ + SharedMem::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 p = \ + SharedMem::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..4b16d303a5 --- /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 // UINT8_MAX +#include // 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/JSAtom.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 atom16(cx); + Rooted 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(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(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 atom16, + MutableHandle 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(first), + static_cast(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(first), static_cast(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 atom16, + MutableHandle atom8) { + CHECK(first <= UINT8_MAX); + CHECK(second <= UINT8_MAX); + CHECK(v <= UINT16_MAX); + + const char16_t utf16[] = {static_cast(v)}; + atom16.set(js::AtomizeChars(cx, utf16, 1)); + CHECK(atom16); + CHECK(atom16->length() == 1); + CHECK(atom16->latin1OrTwoByteChar(0) == v); + + const char utf8[] = {static_cast(first), static_cast(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 atom16, + MutableHandle atom8) { + CHECK(first <= UINT8_MAX); + CHECK(second <= UINT8_MAX); + + const char bytes[] = {static_cast(first), static_cast(second)}; + const char* iter = &bytes[1]; + Maybe 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 atom16, + MutableHandle atom8) { + CHECK(first <= UINT8_MAX); + CHECK(second <= UINT8_MAX); + + const char bytes[] = {static_cast(first), static_cast(second)}; + const char* iter = &bytes[1]; + Maybe 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(first), static_cast(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..19222ec574 --- /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 +#include + +#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 AvlTreeTestIF : public AvlTreeImpl { + // Shorthands for names in the implementation (parent) class. + using Impl = AvlTreeImpl; + using ImplTag = typename AvlTreeImpl::Tag; + using ImplNode = typename AvlTreeImpl::Node; + using ImplResult = typename AvlTreeImpl::Result; + using ImplNodeAndResult = typename AvlTreeImpl::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->right); + } + 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->right); + 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->tag != ImplTag::Free || node->right != 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->right); + 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& tree, + const std::set& 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 +bool SetContains(std::set 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 tree(&alloc); + std::set 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::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::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 emptyTree(&alloc); + CHECK(emptyTree.testSize() == 0); + // Full tree iteration gets us nothing. + AvlTreeTestIF::Iter iter1(&emptyTree); + CHECK(!iter1.hasMore()); + // Starting iteration with any number gets us nothing. + AvlTreeTestIF::Iter iter2(&emptyTree, CmpInt(42)); + CHECK(!iter2.hasMore()); + } + + // Finally with a one-element tree. + { + AvlTreeTestIF unitTree(&alloc); + bool was_inserted = unitTree.testInsert(CmpInt(1337)); + CHECK(was_inserted); + CHECK(unitTree.testSize() == 1); + // Try full tree iteration. + AvlTreeTestIF::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::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 + +#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 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 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 input{ + mozilla::MakeStringSpan(u"18446744073709551616")}; + JS::BigInt* bi = JS::StringToBigInt(cx, input); + CHECK(bi); + JS::Rooted val(cx, JS::BigIntValue(bi)); + JS::Rooted 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 +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 +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 +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 +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 val(cx, JS::BigIntValue(bi)); + JS::Rooted 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 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 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 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 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 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 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 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 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 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 +inline bool Convert(int64_t input, uint8_t radix, const char (&expected)[N]) { + JS::Rooted bi(cx, JS::NumberToBigInt(cx, input)); + CHECK(bi); + JS::Rooted 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 bi( + cx, + JS::SimpleStringToBigInt(cx, mozilla::MakeStringSpan(allPossible), 36)); + CHECK(bi); + JS::Rooted 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 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 foofun(cx, &foo.toObject()); + JS::Rooted 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().setReservedSlot(0, js::Int32Value(2)); + + JS::RootedObject c3wrapper(cx, wrap(cx, outerObj, compartment3)); + CHECK(c3wrapper); + c3wrapper->as().setReservedSlot(0, js::Int32Value(3)); + + JS::RootedObject c4wrapper(cx, wrap(cx, outerObj, compartment4)); + CHECK(c4wrapper); + c4wrapper->as().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 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 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/testChromeBuffer.cpp b/js/src/jsapi-tests/testChromeBuffer.cpp new file mode 100644 index 0000000000..251d6a18ad --- /dev/null +++ b/js/src/jsapi-tests/testChromeBuffer.cpp @@ -0,0 +1,257 @@ +/* -*- 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); + { + JSAutoRealm ar(cx, trusted_glob); + const char* paramName = "x"; + static const char bytes[] = "return x ? 1 + trusted(x-1) : 0"; + + JS::SourceText 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 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); + } + + /* + * 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 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 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 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 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..466ac8bfa2 --- /dev/null +++ b/js/src/jsapi-tests/testCompileNonSyntactic.cpp @@ -0,0 +1,114 @@ +/* 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 + +#include "js/CompilationAndEvaluation.h" // JS::Compile +#include "js/CompileOptions.h" // JS::CompileOptions, JS::InstantiateOptions +#include "js/experimental/JSStencil.h" // JS::Stencil, JS::CompileToStencilOffThread, JS::FinishCompileToStencilOffThread, 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; + +struct OffThreadTask { + OffThreadTask() : monitor(js::mutexid::ShellOffThreadState), token(nullptr) {} + + OffThreadToken* waitUntilDone(JSContext* cx) { + AutoLockMonitor alm(monitor); + while (!token) { + alm.wait(); + } + OffThreadToken* result = token; + token = nullptr; + return result; + } + + void markDone(JS::OffThreadToken* tokenArg) { + AutoLockMonitor alm(monitor); + token = tokenArg; + alm.notify(); + } + + static void OffThreadCallback(OffThreadToken* token, void* context) { + auto self = static_cast(context); + self->markDone(token); + } + + js::Monitor monitor MOZ_UNANNOTATED; + OffThreadToken* token; +}; + +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 buf16; + CHECK(buf16.init(cx, src_16.data(), src_16.length(), + JS::SourceOwnership::Borrowed)); + + JS::SourceText 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 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); + } + + options.forceAsync = true; + OffThreadTask task; + OffThreadToken* token; + + JS::SourceText srcBuf; + RefPtr stencil; + CHECK(srcBuf.init(cx, src_16.data(), src_16.length(), + JS::SourceOwnership::Borrowed)); + + CHECK(CompileToStencilOffThread(cx, options, srcBuf, task.OffThreadCallback, + &task)); + CHECK(token = task.waitUntilDone(cx)); + CHECK(stencil = FinishOffThreadStencil(cx, token)); + InstantiateOptions instantiateOptions(options); + CHECK(script = InstantiateGlobalStencil(cx, instantiateOptions, stencil)); + 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..886af8dbe7 --- /dev/null +++ b/js/src/jsapi-tests/testCompileScript.cpp @@ -0,0 +1,242 @@ +/* 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 +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 buf16; + CHECK(buf16.init(cx, src_16.data(), src_16.length(), + JS::SourceOwnership::Borrowed)); + + JS::CompilationStorage compileStorage; + RefPtr 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 buf8; + CHECK( + buf8.init(cx, src.data(), src.length(), JS::SourceOwnership::Borrowed)); + + JS::CompilationStorage compileStorage; + RefPtr 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 srcBuf; + CHECK(srcBuf.init(cx, badSrc.data(), badSrc.length(), + JS::SourceOwnership::Borrowed)); + + JS::CompilationStorage compileStorage; + RefPtr 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 == 9); + } + + return true; +} + +bool testNonsyntacticCompile() { + const char* chars = + "function f() { return x; }" + "f();"; + + JS::SourceText 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 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 buf16; + CHECK(buf16.init(cx, src_16.data(), src_16.length(), + JS::SourceOwnership::Borrowed)); + + JS::CompilationStorage compileStorage; + RefPtr 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 buf8; + CHECK( + buf8.init(cx, src.data(), src.length(), JS::SourceOwnership::Borrowed)); + + JS::CompilationStorage compileStorage; + RefPtr 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 srcBuf; + CHECK(srcBuf.init(cx, badSrc.data(), badSrc.length(), + JS::SourceOwnership::Borrowed)); + + JS::CompilationStorage compileStorage; + RefPtr 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 == 9); + } + + 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 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 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, compileStorage, *stencil, storage)); + CHECK(compileStorage.getInput().atomCache.size() == 1); // 'field' + 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 + +#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 +bool testBadUtf8(const char (&chars)[N], unsigned errorNumber, + TestMessage testMessage, const char* badBytes) { + JS::Rooted script(cx); + { + JS::CompileOptions options(cx); + + JS::SourceText 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 +bool testContext(const char (&chars)[N], + const char16_t (&expectedContext)[ContextLenWithNull]) { + JS::Rooted script(cx); + { + JS::CompileOptions options(cx); + + JS::SourceText 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 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/testDecimalNumber.cpp b/js/src/jsapi-tests/testDecimalNumber.cpp new file mode 100644 index 0000000000..c63051523d --- /dev/null +++ b/js/src/jsapi-tests/testDecimalNumber.cpp @@ -0,0 +1,289 @@ +/* -*- 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/Span.h" + +#include + +#include "builtin/intl/DecimalNumber.h" +#include "jsapi-tests/tests.h" + +using namespace js::intl; + +static mozilla::Maybe DecimalFrom( + std::string_view sv) { + auto span = mozilla::Span(reinterpret_cast(sv.data()), + sv.length()); + return DecimalNumber::from(span); +} + +BEGIN_TEST(test_DecimalNumberParse) { + struct TestData { + std::string_view string; + bool success; + bool zero; + bool negative; + bool exponentTooLarge; + int32_t exponent; + size_t significandStart; + size_t significandEnd; + } tests[] = { + // "Infinity" and "NaN" aren't accepted. + {"Infinity", false}, + {"+Infinity", false}, + {"-Infinity", false}, + {"NaN", false}, + {"+NaN", false}, + {"-NaN", false}, + + // Non-decimal strings aren't accepted. + {"0xbad", false}, + {"0Xbad", false}, + {"0o0", false}, + {"0O0", false}, + {"0b0", false}, + {"0B0", false}, + + // Doesn't start with sign, digit, or decimal point. + {"e0", false}, + {"Hello", false}, + + // Incomplete number strings. + + // 1. Missing digit after sign. + {"+", false}, + {"+ ", false}, + {"+e0", false}, + {"-", false}, + {"- ", false}, + {"-e0", false}, + + // 2. Missing digit when starting with decimal point. + {".", false}, + {". ", false}, + {".e0", false}, + {"+.", false}, + {"+. ", false}, + {"+.e0", false}, + {"-.", false}, + {"-. ", false}, + {"-.e0", false}, + + // 3. Missing digit after exponent. + {"1e", false}, + {"1e ", false}, + {"1e+", false}, + {"1e+ ", false}, + {"1e-", false}, + {"1e- ", false}, + + // Empty and whitespace-only strings parse as zero. + {"", true, true, false, false, 0, 0, 0}, + {" ", true, true, false, false, 0, 0, 0}, + {" \t ", true, true, false, false, 0, 0, 0}, + + // Positive zero. + {"0", true, true, false, false, 0, 0, 0}, + {"0000", true, true, false, false, 0, 0, 0}, + {"0.0", true, true, false, false, 0, 0, 0}, + {"0.", true, true, false, false, 0, 0, 0}, + {".0", true, true, false, false, 0, 0, 0}, + {"0e0", true, true, false, false, 0, 0, 0}, + {"0e+1", true, true, false, false, 0, 0, 0}, + {"0e-1", true, true, false, false, 0, 0, 0}, + + {"+0", true, true, false, false, 0, 0, 0}, + {"+0000", true, true, false, false, 0, 0, 0}, + {"+0.0", true, true, false, false, 0, 0, 0}, + {"+0.", true, true, false, false, 0, 0, 0}, + {"+.0", true, true, false, false, 0, 0, 0}, + {"+0e0", true, true, false, false, 0, 0, 0}, + {"+0e+1", true, true, false, false, 0, 0, 0}, + {"+0e-1", true, true, false, false, 0, 0, 0}, + + // Negative zero. + {"-0", true, true, true, false, 0, 0, 0}, + {"-0000", true, true, true, false, 0, 0, 0}, + {"-0.0", true, true, true, false, 0, 0, 0}, + {"-0.", true, true, true, false, 0, 0, 0}, + {"-.0", true, true, true, false, 0, 0, 0}, + {"-0e0", true, true, true, false, 0, 0, 0}, + {"-0e+1", true, true, true, false, 0, 0, 0}, + {"-0e-1", true, true, true, false, 0, 0, 0}, + + // Integer numbers. + {"1", true, false, false, false, 1, 0, 1}, + {"10", true, false, false, false, 2, 0, 2}, + {"123", true, false, false, false, 3, 0, 3}, + {"123456789012345678901234567890", true, false, false, false, 30, 0, 30}, + {"1e100", true, false, false, false, 101, 0, 1}, + {"123e39", true, false, false, false, 42, 0, 3}, + + {"+1", true, false, false, false, 1, 1, 2}, + {"+10", true, false, false, false, 2, 1, 3}, + {"+123", true, false, false, false, 3, 1, 4}, + {"+123456789012345678901234567890", true, false, false, false, 30, 1, 31}, + {"+1e100", true, false, false, false, 101, 1, 2}, + {"+123e39", true, false, false, false, 42, 1, 4}, + + {"-1", true, false, true, false, 1, 1, 2}, + {"-10", true, false, true, false, 2, 1, 3}, + {"-123", true, false, true, false, 3, 1, 4}, + {"-123456789012345678901234567890", true, false, true, false, 30, 1, 31}, + {"-1e100", true, false, true, false, 101, 1, 2}, + {"-123e39", true, false, true, false, 42, 1, 4}, + + // Fractional numbers. + {"0.1", true, false, false, false, 0, 2, 3}, + {"0.01", true, false, false, false, -1, 3, 4}, + {"0.001", true, false, false, false, -2, 4, 5}, + {"1.001", true, false, false, false, 1, 0, 5}, + + {".1", true, false, false, false, 0, 1, 2}, + {".01", true, false, false, false, -1, 2, 3}, + {".001", true, false, false, false, -2, 3, 4}, + + {"+.1", true, false, false, false, 0, 2, 3}, + {"+.01", true, false, false, false, -1, 3, 4}, + {"+.001", true, false, false, false, -2, 4, 5}, + + {"-.1", true, false, true, false, 0, 2, 3}, + {"-.01", true, false, true, false, -1, 3, 4}, + {"-.001", true, false, true, false, -2, 4, 5}, + + // Fractional number with exponent part. + {".1e0", true, false, false, false, 0, 1, 2}, + {".01e0", true, false, false, false, -1, 2, 3}, + {".001e0", true, false, false, false, -2, 3, 4}, + + {".1e1", true, false, false, false, 1, 1, 2}, + {".01e1", true, false, false, false, 0, 2, 3}, + {".001e1", true, false, false, false, -1, 3, 4}, + + {".1e-1", true, false, false, false, -1, 1, 2}, + {".01e-1", true, false, false, false, -2, 2, 3}, + {".001e-1", true, false, false, false, -3, 3, 4}, + + {"1.1e0", true, false, false, false, 1, 0, 3}, + {"1.01e0", true, false, false, false, 1, 0, 4}, + {"1.001e0", true, false, false, false, 1, 0, 5}, + + {"1.1e1", true, false, false, false, 2, 0, 3}, + {"1.01e1", true, false, false, false, 2, 0, 4}, + {"1.001e1", true, false, false, false, 2, 0, 5}, + + {"1.1e-1", true, false, false, false, 0, 0, 3}, + {"1.01e-1", true, false, false, false, 0, 0, 4}, + {"1.001e-1", true, false, false, false, 0, 0, 5}, + + // Exponent too large. + // + // The exponent is limited to: |abs(exp) < 2147483648|. + {".1e2147483647", true, false, false, false, 2147483647, 1, 2}, + {".1e-2147483647", true, false, false, false, -2147483647, 1, 2}, + {"1e2147483646", true, false, false, false, 2147483647, 0, 1}, + {".01e-2147483646", true, false, false, false, -2147483647, 2, 3}, + {"1e2147483647", true, false, false, true, 0, 0, 1}, + {".1e2147483648", true, false, false, true, 0, 1, 2}, + {".1e-2147483648", true, false, false, true, 0, 1, 2}, + {".01e-2147483647", true, false, false, true, 0, 2, 3}, + }; + + for (const auto& test : tests) { + if (auto decimal = DecimalFrom(test.string)) { + CHECK(test.success); + CHECK_EQUAL(decimal->isZero(), test.zero); + CHECK_EQUAL(decimal->isNegative(), test.negative); + CHECK_EQUAL(decimal->exponentTooLarge(), test.exponentTooLarge); + CHECK_EQUAL(decimal->exponent(), test.exponent); + CHECK_EQUAL(decimal->significandStart(), test.significandStart); + CHECK_EQUAL(decimal->significandEnd(), test.significandEnd); + } else { + CHECK(!test.success); + } + } + + return true; +} +END_TEST(test_DecimalNumberParse) + +static int32_t NormalizeResult(int32_t r) { return r < 0 ? -1 : r > 0 ? 1 : 0; } + +BEGIN_TEST(test_DecimalNumberCompareTo) { + struct TestData { + std::string_view left; + std::string_view right; + int32_t result; + } tests[] = { + // Compare zeros. + {"0", "0", 0}, + {"-0", "-0", 0}, + {"-0", "+0", -1}, + {"+0", "-0", 1}, + + // Compare with different signs. + {"-1", "+1", -1}, + {"+1", "-1", 1}, + + // Compare zero against non-zero. + {"0", "+1", -1}, + {"0", "-1", 1}, + {"+1", "0", 1}, + {"-1", "0", -1}, + + // Compare with different exponent. + {"1", "0.1", 1}, + {"1e2", "1000", -1}, + {"-1", "-0.1", -1}, + {"-1e2", "-1000", 1}, + + // Compare with same exponent. + {"1", "2", -1}, + {"1", "1", 0}, + {"2", "1", 1}, + + // ToString(1e10) is "10000000000". + {"1e10", "10000000000", 0}, + {"1e10", "10000000001", -1}, + {"-1e10", "-10000000001", 1}, + + // ToString(1e30) is "1e+30". + {"1e30", "1000000000000000000000000000000", 0}, + {"1e30", "1000000000000000000000000000001", -1}, + {"-1e30", "-1000000000000000000000000000001", 1}, + + // ToString(1e-5) is "0.00001". + {"1e-5", "0.00001", 0}, + {"1e-5", "0.00002", -1}, + {"-1e-5", "-0.00002", 1}, + + // ToString(1e-10) is "1e-10". + {"1e-10", "0.0000000001", 0}, + {"1e-10", "0.0000000002", -1}, + {"-1e-10", "-0.0000000002", 1}, + + // Additional tests with exponent forms. + {"1.23e5", "123000", 0}, + {".123e-3", "0.000123", 0}, + {".123e+5", "12300", 0}, + {".00123e5", "123", 0}, + }; + + for (const auto& test : tests) { + auto left = DecimalFrom(test.left); + CHECK(left); + + auto right = DecimalFrom(test.right); + CHECK(right); + + int32_t result = left->compareTo(*right); + CHECK_EQUAL(NormalizeResult(result), test.result); + } + + return true; +} +END_TEST(test_DecimalNumberCompareTo) diff --git a/js/src/jsapi-tests/testDeduplication.cpp b/js/src/jsapi-tests/testDeduplication.cpp new file mode 100644 index 0000000000..6e6ded6b71 --- /dev/null +++ b/js/src/jsapi-tests/testDeduplication.cpp @@ -0,0 +1,113 @@ +/* -*- 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 + +#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); + + { + // 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); + + dep = JS_NewDependentString(cx, str, 10, 100); + CHECK(dep); + + depdep = JS_NewDependentString(cx, dep, 10, 80); + CHECK(depdep); + + // Repeat. This one will not be prevented from deduplication. + str2 = JS_NewStringCopyZ(cx, text); + CHECK(str2); + + dep2 = JS_NewDependentString(cx, str2, 10, 100); + CHECK(dep2); + + depdep2 = JS_NewDependentString(cx, dep2, 10, 80); + CHECK(depdep2); + } + + // Initializing an AutoStableStringChars with `depdep` should prevent the + // owner of its chars (`str`) from deduplication. + 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 `str` and `original`. + CHECK(SameChars(cx, depdep2, str2, 20)); + CHECK(SameChars(cx, depdep2, original, 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> 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/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 + +#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 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 source; + MOZ_RELEASE_ASSERT( + source.init(cx, chars, len, JS::SourceOwnership::Borrowed)); + + JS::CompileOptions options(cx); + return JS::Compile(cx, options, source); +} + +template +bool testOmittedWindow(const CharT (&chars)[N], unsigned expectedErrorNumber, + const char* badCodeUnits = nullptr) { + JS::Rooted 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..be451c27ba --- /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, 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 + +#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 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 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 srcBuf; + CHECK(srcBuf.init(cx, chars, len, JS::SourceOwnership::Borrowed)); + + JS::CompileOptions options(cx); + return JS::Evaluate(cx, options, srcBuf, rval); +} + +template +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..6e0d26aa26 --- /dev/null +++ b/js/src/jsapi-tests/testException.cpp @@ -0,0 +1,92 @@ +/* -*- 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/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> 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> 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> cause( + cx, mozilla::Some(JS::Int32Value(-1))); + JS::RootedValue err(cx); + CHECK(JS::CreateError(cx, JSEXN_ERR, nullptr, empty, 1, 1, nullptr, empty, + cause, &err)); + CHECK(err.isObject()); + JS::Rooted> maybeCause( + cx, JS::GetExceptionCause(&err.toObject())); + CHECK(maybeCause.isSome()); + CHECK_SAME(*cause, *maybeCause); + + CHECK(JS::CreateError(cx, JSEXN_ERR, nullptr, empty, 1, 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 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()); + 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 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..59c1029fb2 --- /dev/null +++ b/js/src/jsapi-tests/testExternalStrings.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 "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 int finalized1 = 0; +static int finalized2 = 0; + +struct ExternalStringCallbacks : public JSExternalStringCallbacks { + int* finalizedCount = nullptr; + + explicit ExternalStringCallbacks(int* finalizedCount) + : finalizedCount(finalizedCount) {} + + void finalize(char16_t* chars) const override { + MOZ_ASSERT(chars == arr); + (*finalizedCount)++; + } + + size_t sizeOfBuffer(const char16_t* chars, + mozilla::MallocSizeOf mallocSizeOf) const override { + MOZ_CRASH("Unexpected call"); + } +}; + +static const ExternalStringCallbacks callbacks1(&finalized1); +static const ExternalStringCallbacks callbacks2(&finalized2); + +BEGIN_TEST(testExternalStrings) { + const unsigned N = 1000; + + for (unsigned i = 0; i < N; ++i) { + CHECK(JS_NewExternalString(cx, arr, arrlen, &callbacks1)); + CHECK(JS_NewExternalString(cx, arr, arrlen, &callbacks2)); + } + + JS_GC(cx); + + // a generous fudge factor to account for strings rooted by conservative gc + const unsigned epsilon = 10; + + CHECK((N - finalized1) < epsilon); + CHECK((N - finalized2) < epsilon); + + return true; +} +END_TEST(testExternalStrings) 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 +#include + +#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 { + unsigned index; +}; + +using TestComponentFinder = ComponentFinder; + +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 g(cx, cx->global()); + JS::Rooted env( + cx, js::GlobalLexicalEnvironmentObject::create(cx, g)); + + JS::RootedValue uninitialized(cx, JS::MagicValue(JS_UNINITIALIZED_LEXICAL)); + JS::Rooted 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 g( + cx, + JS_NewGlobalObject(cx, &cls, nullptr, JS::FireOnNewGlobalHook, options)); + if (!g) { + return false; + } + + JSAutoRealm ar(cx, g); + JS::Rooted 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 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..a148ff133a --- /dev/null +++ b/js/src/jsapi-tests/testFrontendCompileStencil.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 "mozilla/RefPtr.h" +#include "mozilla/Utf8.h" + +#include + +#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::CompileOptions options((JS::CompileOptions::ForFrontendContext())); + + { + const char source[] = "var a = 10;"; + + JS::SourceText srcBuf; + CHECK( + srcBuf.init(fc, source, strlen(source), JS::SourceOwnership::Borrowed)); + JS::CompilationStorage compileStorage; + RefPtr stencil = + JS::CompileGlobalScriptToStencil(fc, options, srcBuf, compileStorage); + CHECK(stencil); + CHECK(compileStorage.hasInput()); + } + + { + const char16_t source[] = u"var a = 10;"; + + JS::SourceText srcBuf; + CHECK(srcBuf.init(fc, source, std::char_traits::length(source), + JS::SourceOwnership::Borrowed)); + JS::CompilationStorage compileStorage; + RefPtr 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/testFrontendJSON.cpp b/js/src/jsapi-tests/testFrontendJSON.cpp new file mode 100644 index 0000000000..d4d109a1b5 --- /dev/null +++ b/js/src/jsapi-tests/testFrontendJSON.cpp @@ -0,0 +1,200 @@ +/* -*- 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 + +#include "js/JSON.h" +#include "jsapi-tests/tests.h" + +using namespace JS; + +BEGIN_FRONTEND_TEST(testIsValidJSONLatin1) { + const char* source; + + source = "true"; + CHECK(IsValidJSON(reinterpret_cast(source), + strlen(source))); + + source = "false"; + CHECK(IsValidJSON(reinterpret_cast(source), + strlen(source))); + + source = "null"; + CHECK(IsValidJSON(reinterpret_cast(source), + strlen(source))); + + source = "0"; + CHECK(IsValidJSON(reinterpret_cast(source), + strlen(source))); + + source = "1"; + CHECK(IsValidJSON(reinterpret_cast(source), + strlen(source))); + + source = "-1"; + CHECK(IsValidJSON(reinterpret_cast(source), + strlen(source))); + + source = "1.75"; + CHECK(IsValidJSON(reinterpret_cast(source), + strlen(source))); + + source = "9000000000"; + CHECK(IsValidJSON(reinterpret_cast(source), + strlen(source))); + + source = "\"foo\""; + CHECK(IsValidJSON(reinterpret_cast(source), + strlen(source))); + + source = "[]"; + CHECK(IsValidJSON(reinterpret_cast(source), + strlen(source))); + + source = "[1, true]"; + CHECK(IsValidJSON(reinterpret_cast(source), + strlen(source))); + + source = "{}"; + CHECK(IsValidJSON(reinterpret_cast(source), + strlen(source))); + + source = "{\"key\": 10}"; + CHECK(IsValidJSON(reinterpret_cast(source), + strlen(source))); + + source = "{\"key\": 10, \"prop\": 20}"; + CHECK(IsValidJSON(reinterpret_cast(source), + strlen(source))); + + source = "1 "; + CHECK(IsValidJSON(reinterpret_cast(source), + strlen(source))); + + // Invalid cases. + + source = ""; + CHECK(!IsValidJSON(reinterpret_cast(source), + strlen(source))); + + source = "1 1"; + CHECK(!IsValidJSON(reinterpret_cast(source), + strlen(source))); + + source = ".1"; + CHECK(!IsValidJSON(reinterpret_cast(source), + strlen(source))); + + source = "undefined"; + CHECK(!IsValidJSON(reinterpret_cast(source), + strlen(source))); + + source = "TRUE"; + CHECK(!IsValidJSON(reinterpret_cast(source), + strlen(source))); + + source = "'foo'"; + CHECK(!IsValidJSON(reinterpret_cast(source), + strlen(source))); + + source = "["; + CHECK(!IsValidJSON(reinterpret_cast(source), + strlen(source))); + + source = "{"; + CHECK(!IsValidJSON(reinterpret_cast(source), + strlen(source))); + + source = "/a/"; + CHECK(!IsValidJSON(reinterpret_cast(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::length(source))); + + source = u"false"; + CHECK(IsValidJSON(source, std::char_traits::length(source))); + + source = u"null"; + CHECK(IsValidJSON(source, std::char_traits::length(source))); + + source = u"0"; + CHECK(IsValidJSON(source, std::char_traits::length(source))); + + source = u"1"; + CHECK(IsValidJSON(source, std::char_traits::length(source))); + + source = u"-1"; + CHECK(IsValidJSON(source, std::char_traits::length(source))); + + source = u"1.75"; + CHECK(IsValidJSON(source, std::char_traits::length(source))); + + source = u"9000000000"; + CHECK(IsValidJSON(source, std::char_traits::length(source))); + + source = u"\"foo\""; + CHECK(IsValidJSON(source, std::char_traits::length(source))); + + source = u"[]"; + CHECK(IsValidJSON(source, std::char_traits::length(source))); + + source = u"[1, true]"; + CHECK(IsValidJSON(source, std::char_traits::length(source))); + + source = u"{}"; + CHECK(IsValidJSON(source, std::char_traits::length(source))); + + source = u"{\"key\": 10}"; + CHECK(IsValidJSON(source, std::char_traits::length(source))); + + source = u"{\"key\": 10, \"prop\": 20}"; + CHECK(IsValidJSON(source, std::char_traits::length(source))); + + source = u"1 "; + CHECK(IsValidJSON(source, std::char_traits::length(source))); + + // Invalid cases. + + source = u""; + CHECK(!IsValidJSON(source, std::char_traits::length(source))); + + source = u"1 1"; + CHECK(!IsValidJSON(source, std::char_traits::length(source))); + + source = u".1"; + CHECK(!IsValidJSON(source, std::char_traits::length(source))); + + source = u"undefined"; + CHECK(!IsValidJSON(source, std::char_traits::length(source))); + + source = u"TRUE"; + CHECK(!IsValidJSON(source, std::char_traits::length(source))); + + source = u"'foo'"; + CHECK(!IsValidJSON(source, std::char_traits::length(source))); + + source = u"["; + CHECK(!IsValidJSON(source, std::char_traits::length(source))); + + source = u"{"; + CHECK(!IsValidJSON(source, std::char_traits::length(source))); + + source = u"/a/"; + CHECK(!IsValidJSON(source, std::char_traits::length(source))); + + return true; +} + +END_TEST(testIsValidJSONTwoBytes) 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 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 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 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 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 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 + +#include "gc/Memory.h" +#include "jsapi-tests/tests.h" + +#if defined(XP_WIN) +# include "util/WindowsWrapper.h" +# include +#elif defined(__wasi__) +// Nothing. +#else +# include +# include +# include +# include +# include +# include +# include +#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 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 + +#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..d53e293f23 --- /dev/null +++ b/js/src/jsapi-tests/testGCExactRooting.cpp @@ -0,0 +1,915 @@ +/* -*- 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 obj; + HeapPtr 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 obj; + HeapPtr 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 +struct MutableWrappedPtrOperations { + HeapPtr& obj() { return static_cast(this)->get().obj; } + HeapPtr& str() { return static_cast(this)->get().str; } + int constructor() { + return static_cast(this)->get().whichConstructor; + } +}; + +template +struct MutableWrappedPtrOperations { + HeapPtr& obj() { return static_cast(this)->get().obj; } + HeapPtr& str() { return static_cast(this)->get().str; } + int constructor() { + return static_cast(this)->get().whichConstructor; + } +}; +} // namespace js + +BEGIN_TEST(testGCRootedStaticStructInternalStackStorageAugmented) { + // Test Rooted constructors for a copyable type. + JS::Rooted r1(cx); + JS::Rooted r2(cx, 3.4); + JS::Rooted r3(cx, MyContainer(cx)); + JS::Rooted r4(cx, cx); + JS::Rooted r5(cx, cx, cx, cx); + + JS::Rooted rv(cx); + + CHECK_EQUAL(r1.constructor(), 1); // direct SafelyInitialized + 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 nc1(cx); + JS::Rooted nc2(cx, 3.4); + // Compile error: cannot copy + // JS::Rooted nc3(cx, MyNonCopyableContainer(cx)); + JS::Rooted nc4(cx, cx); + JS::Rooted 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 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 heap(cx, container); + + // Copyable types in place. + JS::PersistentRooted cp1(cx); + JS::PersistentRooted cp2(cx, 7.8); + JS::PersistentRooted cp3(cx, cx); + JS::PersistentRooted cp4(cx, cx, cx, cx); + + CHECK_EQUAL(cp1.constructor(), 1); // direct SafelyInitialized + 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 ncp1(cx); + JS::PersistentRooted ncp2(cx, 7.8); + + // We're not just using a 1-arg constructor, right? + JS::PersistentRooted ncp3(cx, cx); + JS::PersistentRooted ncp4(cx, cx, cx, cx); + + CHECK_EQUAL(ncp1.constructor(), 1); // direct SafelyInitialized + 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 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 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 sContainer; +BEGIN_TEST(testGCPersistentRootedTraceableCannotOutliveRuntime) { + JS::Rooted 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; + +BEGIN_TEST(testGCRootedHashMap) { + JS::Rooted 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) { + 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 map(cx, 15); + + JS_GC(cx); + CHECK(map.putNew(12, 13)); + + return true; +} +END_TEST(testSafelyUnrootedGCHashMap) + +static bool FillMyHashMap(JSContext* cx, MutableHandle 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 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 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; + +BEGIN_TEST(testGCRootedVector) { + JS::Rooted 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().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 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>& rooted) { + // Ensure range enumeration works through the reference. + for (auto shape : rooted) { + CHECK(shape); + } + return true; +} + +bool receiveHandleToShapeVector(JS::Handle> handle) { + // Ensure range enumeration works through the handle. + for (auto shape : handle) { + CHECK(shape); + } + return true; +} + +bool receiveMutableHandleToShapeVector( + JS::MutableHandle> 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; + JS::Rooted 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().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 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; + +static bool FillVector(JSContext* cx, MutableHandle 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().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 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 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 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; + +BEGIN_TEST(testGCVectorEmplaceBack) { + JS::Rooted vector(cx, FooVector(cx)); + + CHECK(vector.emplaceBack(1, 2)); + + return true; +} +END_TEST(testGCVectorEmplaceBack) + +BEGIN_TEST(testRootedMaybeValue) { + JS::Rooted> maybeNothing(cx); + CHECK(maybeNothing.isNothing()); + CHECK(!maybeNothing.isSome()); + + JS::Rooted> maybe(cx, Some(UndefinedValue())); + CHECK(CheckConstOperations>&>(maybe)); + CHECK(CheckConstOperations>>(maybe)); + CHECK(CheckConstOperations>>(&maybe)); + + maybe = Some(JS::TrueValue()); + CHECK(CheckMutableOperations>&>(maybe)); + + maybe = Some(JS::TrueValue()); + CHECK(CheckMutableOperations>>(&maybe)); + + CHECK(JS::NothingHandleValue.isNothing()); + + return true; +} + +template +bool CheckConstOperations(T someUndefinedValue) { + CHECK(someUndefinedValue.isSome()); + CHECK(someUndefinedValue.value().isUndefined()); + CHECK(someUndefinedValue->isUndefined()); + CHECK((*someUndefinedValue).isUndefined()); + return true; +} + +template +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 because Heap would get + // traced via the store buffer. Heap 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 : public IgnoreGCPolicy {}; +} // 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 unrootedValerr(val); + Rooted> valerr(cx, val); + + Result unrootedOkval(val); + Rooted> okval(cx, val); + + Result simple{mozilla::Ok()}; + + Result unrootedValobj1(val); + Rooted> valobj1(cx, val); + Result unrootedValobj2(obj); + Rooted> valobj2(cx, obj); + + // Test nested traceable structures. + Result, JSObject*> maybeobj( + mozilla::Some(mozilla::Ok())); + Rooted, 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, JSObject*>> + + // But this should be fine when no tracing is required. + Result, double> dummy(3.4); + + // One thing I didn't realize initially about Result<>: unwrap() takes + // ownership of a value. In the case of Result, that means the + // contained Maybe is reset to Nothing. + Result, int> confusing(mozilla::Some(7)); + CHECK(confusing.unwrap().isSome()); + CHECK(!confusing.unwrap().isSome()); + + Result, JSObject*> maybevalobj( + mozilla::Some(val.get())); + Rooted, JSObject*>> rooted_maybevalobj( + cx, mozilla::Some(val.get())); + + // Custom types that haven't had GCPolicy explicitly specialized. + SimpleTraceable s1{obj, val}; + Result custom(s1); + SimpleTraceable s2{obj, val}; + Rooted> 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 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 : UnusedZeroEnum {}; +} // namespace mozilla::detail + +namespace JS { +template <> +struct GCPolicy + : public IgnoreGCPolicy {}; +} // 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 { + 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 : public IgnoreGCPolicy {}; +} // 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 : HasFreeLSB { +}; +} // namespace mozilla::detail + +BEGIN_TEST_WITH_ATTRIBUTES(testRootedResultCtors, JS_EXPECT_HAZARDS) { + JSObject* unrootedObj = JS_NewPlainObject(cx); + CHECK(unrootedObj); + Rooted obj(cx, unrootedObj); + + using mozilla::detail::PackingStrategy; + + static_assert(Result::Strategy == + PackingStrategy::Variant); + Rooted> vv(cx, DontCopyMe_Variant{obj}); + static_assert(Result::Strategy == + PackingStrategy::Variant); + Rooted> ve(cx, + DontCopyMe_Variant{obj}); + + static_assert(Result::Strategy == + PackingStrategy::NullIsOk); + Rooted> nv( + cx, DontCopyMe_NullIsOk{JS::ObjectValue(*obj)}); + + static_assert(Result::Strategy == + PackingStrategy::LowBitTagIsError); + Rooted> 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(packed); + } + static uintptr_t Store(JSObject* obj) { + return reinterpret_cast(obj); + } + + DontCopyMe_PackedVariant() : obj(0) {} + explicit DontCopyMe_PackedVariant(JSObject* objArg) + : obj(reinterpret_cast(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); +static_assert(std::is_default_constructible_v); +static_assert(mozilla::detail::IsPackableVariant::value); + +BEGIN_TEST_WITH_ATTRIBUTES(testResultPackedVariant, JS_EXPECT_HAZARDS) { + JSObject* unrootedObj = JS_NewPlainObject(cx); + CHECK(unrootedObj); + Rooted obj(cx, unrootedObj); + + using mozilla::detail::PackingStrategy; + + static_assert(Result::Strategy == + PackingStrategy::PackedVariant); + Rooted> pv( + cx, DontCopyMe_PackedVariant{obj}); + static_assert(Result::Strategy == + PackingStrategy::PackedVariant); + Rooted> 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..6e9f6d83e4 --- /dev/null +++ b/js/src/jsapi-tests/testGCGrayMarking.cpp @@ -0,0 +1,803 @@ +/* -*- 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 + +#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 { + static const JS::RootKind kind = JS::RootKind::Traceable; +}; + +template <> +struct GCPolicy + : public NonGCPointerPolicy {}; + +} // 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 valueValue(cx, ObjectValue(*value)); + CHECK(SetWeakMapEntry(cx, weakMap, key, 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 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 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 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 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* weakMapOut, + JSObject** keyOut, JSObject** valueOut) { + RootedObject key(cx, AllocWeakmapKeyObject()); + CHECK(key); + + RootedObject value(cx, AllocPlainObject()); + CHECK(value); + + auto weakMap = cx->make_unique(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& 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(); + 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 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 grayRoot1; + JS::Heap 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(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 +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(); + 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 +struct TestStruct { + W wrapper; + + void trace(JSTracer* trc) { + TraceNullableEdge(trc, &wrapper, "TestStruct::wrapper"); + } + + TestStruct() {} + explicit TestStruct(T init) : wrapper(init) {} +}; + +template +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(CreateNurseryGCThing(cx)); +} + +template <> +JS::Uint8Array CreateNurseryGCThing(JSContext* cx) { + JS::Rooted 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 +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(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 +void* CreateHiddenTenuredGCThing(JSContext* cx) { + return CreateTenuredGCThing(cx); +} + +template <> +void* CreateHiddenTenuredGCThing(JSContext* cx) { + return CreateTenuredGCThing(cx).asObjectUnbarriered(); +} + +template <> +void* CreateHiddenTenuredGCThing(JSContext* cx) { + return CreateTenuredGCThing(cx).asObjectUnbarriered(); +} + +static uintptr_t UnbarrieredCastToInt(gc::Cell* cell) { + return reinterpret_cast(cell); +} +static uintptr_t UnbarrieredCastToInt(const JS::ArrayBufferOrView& view) { + return UnbarrieredCastToInt(view.asObjectUnbarriered()); +} + +template +T RecoverHiddenGCThing(void* ptr) { + return reinterpret_cast(ptr); +} + +template <> +JS::ArrayBuffer RecoverHiddenGCThing(void* ptr) { + return JS::ArrayBuffer::fromObject(RecoverHiddenGCThing(ptr)); +} + +template <> +JS::Uint8Array RecoverHiddenGCThing(void* ptr) { + return JS::Uint8Array::fromObject(RecoverHiddenGCThing(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(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()); + CHECK(TestHeapPostBarriersForType()); + CHECK(TestHeapPostBarriersForType()); + // 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 +bool TestHeapPostBarriersForType() { + CHECK((TestHeapPostBarriersForWrapper())); + CHECK((TestHeapPostBarriersForMovableWrapper())); + CHECK((TestHeapPostBarriersForMovableWrapper())); + CHECK((TestHeapPostBarriersForMovableWrapper())); + return true; +} + +template