summaryrefslogtreecommitdiffstats
path: root/js/src/jsapi-tests/testWeakMap.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'js/src/jsapi-tests/testWeakMap.cpp')
-rw-r--r--js/src/jsapi-tests/testWeakMap.cpp324
1 files changed, 324 insertions, 0 deletions
diff --git a/js/src/jsapi-tests/testWeakMap.cpp b/js/src/jsapi-tests/testWeakMap.cpp
new file mode 100644
index 0000000000..0872571d3d
--- /dev/null
+++ b/js/src/jsapi-tests/testWeakMap.cpp
@@ -0,0 +1,324 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: set ts=8 sts=2 et sw=2 tw=80:
+ */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gc/Zone.h"
+#include "js/Array.h" // JS::GetArrayLength
+#include "js/Exception.h" // JS_IsExceptionPending
+#include "js/GlobalObject.h" // JS_NewGlobalObject
+#include "js/PropertyAndElement.h" // JS_DefineProperty
+#include "js/WeakMap.h"
+#include "jsapi-tests/tests.h"
+#include "vm/Realm.h"
+
+using namespace js;
+
+static bool checkSize(JSContext* cx, JS::HandleObject map, uint32_t expected) {
+ JS::RootedObject keys(cx);
+ if (!JS_NondeterministicGetWeakMapKeys(cx, map, &keys)) {
+ return false;
+ }
+
+ uint32_t length;
+ if (!JS::GetArrayLength(cx, keys, &length)) {
+ return false;
+ }
+
+ return length == expected;
+}
+
+JSObject* keyDelegate = nullptr;
+
+BEGIN_TEST(testWeakMap_basicOperations) {
+ JS::RootedObject map(cx, JS::NewWeakMapObject(cx));
+ CHECK(IsWeakMapObject(map));
+
+ JS::RootedValue key(cx, JS::ObjectOrNullValue(newKey()));
+ CHECK(!key.isNull());
+ CHECK(!JS::IsWeakMapObject(&key.toObject()));
+
+ JS::RootedValue r(cx);
+ CHECK(GetWeakMapEntry(cx, map, key, &r));
+ CHECK(r.isUndefined());
+
+ CHECK(checkSize(cx, map, 0));
+
+ JS::RootedValue val(cx, JS::Int32Value(1));
+ CHECK(SetWeakMapEntry(cx, map, key, val));
+
+ CHECK(GetWeakMapEntry(cx, map, key, &r));
+ CHECK(r == val);
+ CHECK(checkSize(cx, map, 1));
+
+ JS_GC(cx);
+
+ CHECK(GetWeakMapEntry(cx, map, key, &r));
+ CHECK(r == val);
+ CHECK(checkSize(cx, map, 1));
+
+ key.setUndefined();
+ JS_GC(cx);
+
+ CHECK(checkSize(cx, map, 0));
+
+ return true;
+}
+
+JSObject* newKey() { return JS_NewPlainObject(cx); }
+END_TEST(testWeakMap_basicOperations)
+
+BEGIN_TEST(testWeakMap_setWeakMapEntry_invalid_key) {
+ JS::RootedObject map(cx, JS::NewWeakMapObject(cx));
+ CHECK(IsWeakMapObject(map));
+ CHECK(checkSize(cx, map, 0));
+
+ JS::RootedString test(cx, JS_NewStringCopyZ(cx, "test"));
+ // sym is a Symbol in global Symbol registry and hence can't be used as a key.
+ JS::RootedSymbol sym(cx, JS::GetSymbolFor(cx, test));
+ JS::RootedValue key(cx, JS::SymbolValue(sym));
+ CHECK(!key.isUndefined());
+
+ JS::RootedValue val(cx, JS::Int32Value(1));
+
+ CHECK(!JS_IsExceptionPending(cx));
+ CHECK(SetWeakMapEntry(cx, map, key, val) == false);
+
+ CHECK(JS_IsExceptionPending(cx));
+ JS::Rooted<JS::Value> exn(cx);
+ CHECK(JS_GetPendingException(cx, &exn));
+ JS::Rooted<JSObject*> obj(cx, &exn.toObject());
+ JSErrorReport* err = JS_ErrorFromException(cx, obj);
+ CHECK(err->exnType == JSEXN_TYPEERR);
+
+ JS_ClearPendingException(cx);
+
+ JS::RootedValue r(cx);
+ CHECK(GetWeakMapEntry(cx, map, key, &r));
+ CHECK(r == JS::UndefinedValue());
+ CHECK(checkSize(cx, map, 0));
+
+ return true;
+}
+END_TEST(testWeakMap_setWeakMapEntry_invalid_key)
+
+#ifdef NIGHTLY_BUILD
+BEGIN_TEST(testWeakMap_basicOperations_symbols_as_keys) {
+ JS::RootedObject map(cx, JS::NewWeakMapObject(cx));
+ CHECK(IsWeakMapObject(map));
+
+ JS::RootedString test(cx, JS_NewStringCopyZ(cx, "test"));
+ JS::RootedSymbol sym(cx, JS::NewSymbol(cx, test));
+ CHECK(sym);
+ JS::RootedValue key(cx, JS::SymbolValue(sym));
+
+ JS::RootedValue r(cx);
+ CHECK(GetWeakMapEntry(cx, map, key, &r));
+ CHECK(r.isUndefined());
+
+ CHECK(checkSize(cx, map, 0));
+
+ JS::RootedValue val(cx, JS::Int32Value(1));
+ CHECK(SetWeakMapEntry(cx, map, key, val));
+
+ CHECK(GetWeakMapEntry(cx, map, key, &r));
+ CHECK(r == val);
+ CHECK(checkSize(cx, map, 1));
+
+ JS_GC(cx);
+
+ CHECK(GetWeakMapEntry(cx, map, key, &r));
+ CHECK(r == val);
+ CHECK(checkSize(cx, map, 1));
+
+ sym = nullptr;
+ key.setUndefined();
+ JS_GC(cx);
+
+ CHECK(checkSize(cx, map, 0));
+
+ return true;
+}
+END_TEST(testWeakMap_basicOperations_symbols_as_keys)
+#endif
+
+BEGIN_TEST(testWeakMap_keyDelegates) {
+ AutoLeaveZeal nozeal(cx);
+
+ AutoGCParameter param(cx, JSGC_INCREMENTAL_GC_ENABLED, true);
+ JS_GC(cx);
+ JS::RootedObject map(cx, JS::NewWeakMapObject(cx));
+ CHECK(map);
+
+ JS::RootedObject delegate(cx, newDelegate());
+ JS::RootedObject key(cx, delegate);
+ if (!JS_WrapObject(cx, &key)) {
+ return false;
+ }
+ CHECK(key);
+ CHECK(delegate);
+
+ keyDelegate = delegate;
+
+ JS::RootedObject delegateRoot(cx);
+ {
+ JSAutoRealm ar(cx, delegate);
+ delegateRoot = JS_NewPlainObject(cx);
+ CHECK(delegateRoot);
+ JS::RootedValue delegateValue(cx, JS::ObjectValue(*delegate));
+ CHECK(JS_DefineProperty(cx, delegateRoot, "delegate", delegateValue, 0));
+ }
+ delegate = nullptr;
+
+ /*
+ * Perform an incremental GC, introducing an unmarked CCW to force the map
+ * zone to finish marking before the delegate zone.
+ */
+ CHECK(newCCW(map, delegateRoot));
+ performIncrementalGC();
+#ifdef DEBUG
+ CHECK(map->zone()->lastSweepGroupIndex() <
+ delegateRoot->zone()->lastSweepGroupIndex());
+#endif
+
+ /* Add our entry to the weakmap. */
+ JS::RootedValue keyVal(cx, JS::ObjectValue(*key));
+ JS::RootedValue val(cx, JS::Int32Value(1));
+ CHECK(SetWeakMapEntry(cx, map, keyVal, val));
+ CHECK(checkSize(cx, map, 1));
+
+ /*
+ * Check the delegate keeps the entry alive even if the key is not reachable.
+ */
+ key = nullptr;
+ keyVal.setUndefined();
+ CHECK(newCCW(map, delegateRoot));
+ performIncrementalGC();
+ CHECK(checkSize(cx, map, 1));
+
+ /*
+ * Check that the zones finished marking at the same time, which is
+ * necessary because of the presence of the delegate and the CCW.
+ */
+#ifdef DEBUG
+ CHECK(map->zone()->lastSweepGroupIndex() ==
+ delegateRoot->zone()->lastSweepGroupIndex());
+#endif
+
+ /* Check that when the delegate becomes unreachable the entry is removed. */
+ delegateRoot = nullptr;
+ keyDelegate = nullptr;
+ JS_GC(cx);
+ CHECK(checkSize(cx, map, 0));
+
+ return true;
+}
+
+static size_t DelegateObjectMoved(JSObject* obj, JSObject* old) {
+ if (!keyDelegate) {
+ return 0; // Object got moved before we set keyDelegate to point to it.
+ }
+
+ MOZ_RELEASE_ASSERT(keyDelegate == old);
+ keyDelegate = obj;
+ return 0;
+}
+
+JSObject* newKey() {
+ static const JSClass keyClass = {
+ "keyWithDelegate", JSCLASS_HAS_RESERVED_SLOTS(1),
+ JS_NULL_CLASS_OPS, JS_NULL_CLASS_SPEC,
+ JS_NULL_CLASS_EXT, JS_NULL_OBJECT_OPS};
+
+ JS::RootedObject key(cx, JS_NewObject(cx, &keyClass));
+ if (!key) {
+ return nullptr;
+ }
+
+ return key;
+}
+
+JSObject* newCCW(JS::HandleObject sourceZone, JS::HandleObject destZone) {
+ /*
+ * Now ensure that this zone will be swept first by adding a cross
+ * compartment wrapper to a new object in the same zone as the
+ * delegate object.
+ */
+ JS::RootedObject object(cx);
+ {
+ JSAutoRealm ar(cx, destZone);
+ object = JS_NewPlainObject(cx);
+ if (!object) {
+ return nullptr;
+ }
+ }
+ {
+ JSAutoRealm ar(cx, sourceZone);
+ if (!JS_WrapObject(cx, &object)) {
+ return nullptr;
+ }
+ }
+
+ // In order to test the SCC algorithm, we need the wrapper/wrappee to be
+ // tenured.
+ cx->runtime()->gc.evictNursery();
+
+ return object;
+}
+
+JSObject* newDelegate() {
+ static const JSClassOps delegateClassOps = {
+ nullptr, // addProperty
+ nullptr, // delProperty
+ nullptr, // enumerate
+ nullptr, // newEnumerate
+ nullptr, // resolve
+ nullptr, // mayResolve
+ nullptr, // finalize
+ nullptr, // call
+ nullptr, // construct
+ JS_GlobalObjectTraceHook, // trace
+ };
+
+ static const js::ClassExtension delegateClassExtension = {
+ DelegateObjectMoved, // objectMovedOp
+ };
+
+ static const JSClass delegateClass = {
+ "delegate",
+ JSCLASS_GLOBAL_FLAGS | JSCLASS_HAS_RESERVED_SLOTS(1),
+ &delegateClassOps,
+ JS_NULL_CLASS_SPEC,
+ &delegateClassExtension,
+ JS_NULL_OBJECT_OPS};
+
+ /* Create the global object. */
+ JS::RealmOptions options;
+ JS::RootedObject global(cx,
+ JS_NewGlobalObject(cx, &delegateClass, nullptr,
+ JS::FireOnNewGlobalHook, options));
+ if (!global) {
+ return nullptr;
+ }
+
+ JS_SetReservedSlot(global, 0, JS::Int32Value(42));
+ return global;
+}
+
+void performIncrementalGC() {
+ JSRuntime* rt = cx->runtime();
+ js::SliceBudget budget(js::WorkBudget(1000));
+ rt->gc.startDebugGC(JS::GCOptions::Normal, budget);
+
+ // Wait until we've started marking before finishing the GC
+ // non-incrementally.
+ while (rt->gc.state() == gc::State::Prepare) {
+ rt->gc.debugGCSlice(budget);
+ }
+ if (JS::IsIncrementalGCInProgress(cx)) {
+ rt->gc.finishGC(JS::GCReason::DEBUG_GC);
+ }
+}
+END_TEST(testWeakMap_keyDelegates)