summaryrefslogtreecommitdiffstats
path: root/js/src/jsapi-tests/testObjectSwap.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'js/src/jsapi-tests/testObjectSwap.cpp')
-rw-r--r--js/src/jsapi-tests/testObjectSwap.cpp445
1 files changed, 445 insertions, 0 deletions
diff --git a/js/src/jsapi-tests/testObjectSwap.cpp b/js/src/jsapi-tests/testObjectSwap.cpp
new file mode 100644
index 0000000000..d326df6f37
--- /dev/null
+++ b/js/src/jsapi-tests/testObjectSwap.cpp
@@ -0,0 +1,445 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: set ts=8 sts=2 et sw=2 tw=80:
+ */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Test JSObject::swap.
+ *
+ * This test creates objects from a description of their configuration. Objects
+ * are given unique property names and values. A list of configurations is
+ * created and the result of swapping every combination checked.
+ */
+
+#include "mozilla/Sprintf.h"
+
+#include "js/AllocPolicy.h"
+#include "js/Vector.h"
+#include "jsapi-tests/tests.h"
+#include "vm/PlainObject.h"
+
+#include "gc/StableCellHasher-inl.h"
+#include "vm/JSObject-inl.h"
+
+using namespace js;
+
+struct NativeConfig {
+ uint32_t propCount;
+ uint32_t elementCount;
+ bool inDictionaryMode;
+};
+
+struct ProxyConfig {
+ bool inlineValues;
+};
+
+struct ObjectConfig {
+ const JSClass* clasp;
+ bool isNative;
+ bool nurseryAllocated;
+ bool hasUniqueId;
+ union {
+ NativeConfig native;
+ ProxyConfig proxy;
+ };
+};
+
+using ObjectConfigVector = Vector<ObjectConfig, 0, SystemAllocPolicy>;
+
+static const JSClass TestProxyClasses[] = {
+ PROXY_CLASS_DEF("TestProxy", JSCLASS_HAS_RESERVED_SLOTS(1 /* Min */)),
+ PROXY_CLASS_DEF("TestProxy", JSCLASS_HAS_RESERVED_SLOTS(2)),
+ PROXY_CLASS_DEF("TestProxy", JSCLASS_HAS_RESERVED_SLOTS(7)),
+ PROXY_CLASS_DEF("TestProxy", JSCLASS_HAS_RESERVED_SLOTS(8)),
+ PROXY_CLASS_DEF("TestProxy", JSCLASS_HAS_RESERVED_SLOTS(14 /* Max */))};
+
+static const JSClass TestDOMClasses[] = {
+ {"TestDOMObject", JSCLASS_IS_DOMJSCLASS | JSCLASS_HAS_RESERVED_SLOTS(0)},
+ {"TestDOMObject", JSCLASS_IS_DOMJSCLASS | JSCLASS_HAS_RESERVED_SLOTS(1)},
+ {"TestDOMObject", JSCLASS_IS_DOMJSCLASS | JSCLASS_HAS_RESERVED_SLOTS(2)},
+ {"TestDOMObject", JSCLASS_IS_DOMJSCLASS | JSCLASS_HAS_RESERVED_SLOTS(7)},
+ {"TestDOMObject", JSCLASS_IS_DOMJSCLASS | JSCLASS_HAS_RESERVED_SLOTS(8)},
+ {"TestDOMObject", JSCLASS_IS_DOMJSCLASS | JSCLASS_HAS_RESERVED_SLOTS(20)}};
+
+static const uint32_t TestPropertyCounts[] = {0, 1, 2, 7, 8, 20};
+
+static const uint32_t TestElementCounts[] = {0, 20};
+
+static bool Verbose = false;
+
+class TenuredProxyHandler final : public Wrapper {
+ public:
+ static const TenuredProxyHandler singleton;
+ constexpr TenuredProxyHandler() : Wrapper(0) {}
+ bool canNurseryAllocate() const override { return false; }
+};
+
+const TenuredProxyHandler TenuredProxyHandler::singleton;
+
+class NurseryProxyHandler final : public Wrapper {
+ public:
+ static const NurseryProxyHandler singleton;
+ constexpr NurseryProxyHandler() : Wrapper(0) {}
+ bool canNurseryAllocate() const override { return true; }
+};
+
+const NurseryProxyHandler NurseryProxyHandler::singleton;
+
+BEGIN_TEST(testObjectSwap) {
+ AutoLeaveZeal noZeal(cx);
+
+ ObjectConfigVector objectConfigs = CreateObjectConfigs();
+
+ for (const ObjectConfig& config1 : objectConfigs) {
+ for (const ObjectConfig& config2 : objectConfigs) {
+ {
+ uint32_t id1;
+ RootedObject obj1(cx, CreateObject(config1, &id1));
+ CHECK(obj1);
+
+ uint32_t id2;
+ RootedObject obj2(cx, CreateObject(config2, &id2));
+ CHECK(obj2);
+
+ if (Verbose) {
+ fprintf(stderr, "Swap %p (%s) and %p (%s)\n", obj1.get(),
+ GetLocation(obj1), obj2.get(), GetLocation(obj2));
+ }
+
+ uint64_t uid1 = 0;
+ if (config1.hasUniqueId) {
+ uid1 = gc::GetUniqueIdInfallible(obj1);
+ }
+ uint64_t uid2 = 0;
+ if (config2.hasUniqueId) {
+ uid2 = gc::GetUniqueIdInfallible(obj2);
+ }
+
+ {
+ AutoEnterOOMUnsafeRegion oomUnsafe;
+ JSObject::swap(cx, obj1, obj2, oomUnsafe);
+ }
+
+ CHECK(CheckObject(obj1, config2, id2));
+ CHECK(CheckObject(obj2, config1, id1));
+
+ CHECK(CheckUniqueIds(obj1, config1.hasUniqueId, uid1, obj2,
+ config2.hasUniqueId, uid2));
+ }
+
+ if (Verbose) {
+ fprintf(stderr, "\n");
+ }
+ }
+
+ // JSObject::swap can suppress GC so ensure we clean up occasionally.
+ JS_GC(cx);
+ }
+
+ return true;
+}
+
+ObjectConfigVector CreateObjectConfigs() {
+ ObjectConfigVector configs;
+
+ ObjectConfig config;
+
+ for (bool nurseryAllocated : {false, true}) {
+ config.nurseryAllocated = nurseryAllocated;
+
+ for (bool hasUniqueId : {false, true}) {
+ config.hasUniqueId = hasUniqueId;
+
+ config.isNative = true;
+ config.native = NativeConfig{0, false};
+
+ for (const JSClass& jsClass : TestDOMClasses) {
+ config.clasp = &jsClass;
+
+ for (uint32_t propCount : TestPropertyCounts) {
+ config.native.propCount = propCount;
+
+ for (uint32_t elementCount : TestElementCounts) {
+ config.native.elementCount = elementCount;
+
+ for (bool inDictionaryMode : {false, true}) {
+ if (inDictionaryMode && propCount == 0) {
+ continue;
+ }
+
+ config.native.inDictionaryMode = inDictionaryMode;
+ MOZ_RELEASE_ASSERT(configs.append(config));
+ }
+ }
+ }
+ }
+
+ config.isNative = false;
+ config.proxy = ProxyConfig{false};
+
+ for (const JSClass& jsClass : TestProxyClasses) {
+ config.clasp = &jsClass;
+
+ for (bool inlineValues : {true, false}) {
+ config.proxy.inlineValues = inlineValues;
+ MOZ_RELEASE_ASSERT(configs.append(config));
+ }
+ }
+ }
+ }
+
+ return configs;
+}
+
+const char* GetLocation(JSObject* obj) {
+ return obj->isTenured() ? "tenured heap" : "nursery";
+}
+
+// Counter used to give slots and property names unique values.
+uint32_t nextId = 0;
+
+JSObject* CreateObject(const ObjectConfig& config, uint32_t* idOut) {
+ *idOut = nextId;
+ JSObject* obj =
+ config.isNative ? CreateNativeObject(config) : CreateProxy(config);
+
+ if (config.hasUniqueId) {
+ uint64_t unused;
+ if (!gc::GetOrCreateUniqueId(obj, &unused)) {
+ return nullptr;
+ }
+ }
+
+ return obj;
+}
+
+JSObject* CreateNativeObject(const ObjectConfig& config) {
+ MOZ_ASSERT(config.isNative);
+
+ NewObjectKind kind = config.nurseryAllocated ? GenericObject : TenuredObject;
+ Rooted<NativeObject*> obj(cx,
+ NewBuiltinClassInstance(cx, config.clasp, kind));
+ if (!obj) {
+ return nullptr;
+ }
+
+ MOZ_RELEASE_ASSERT(IsInsideNursery(obj) == config.nurseryAllocated);
+
+ for (uint32_t i = 0; i < JSCLASS_RESERVED_SLOTS(config.clasp); i++) {
+ JS::SetReservedSlot(obj, i, Int32Value(nextId++));
+ }
+
+ if (config.native.inDictionaryMode) {
+ // Put object in dictionary mode by defining a non-last property and
+ // deleting it later.
+ MOZ_RELEASE_ASSERT(config.native.propCount != 0);
+ if (!JS_DefineProperty(cx, obj, "dummy", 0, JSPROP_ENUMERATE)) {
+ return nullptr;
+ }
+ }
+
+ for (uint32_t i = 0; i < config.native.propCount; i++) {
+ RootedId name(cx, CreatePropName(nextId++));
+ if (name.isVoid()) {
+ return nullptr;
+ }
+
+ if (!JS_DefinePropertyById(cx, obj, name, nextId++, JSPROP_ENUMERATE)) {
+ return nullptr;
+ }
+ }
+
+ if (config.native.elementCount) {
+ if (!obj->ensureElements(cx, config.native.elementCount)) {
+ return nullptr;
+ }
+ for (uint32_t i = 0; i < config.native.elementCount; i++) {
+ obj->setDenseInitializedLength(i + 1);
+ obj->initDenseElement(i, Int32Value(nextId++));
+ }
+ MOZ_ASSERT(obj->hasDynamicElements());
+ }
+
+ if (config.native.inDictionaryMode) {
+ JS::ObjectOpResult result;
+ JS_DeleteProperty(cx, obj, "dummy", result);
+ MOZ_RELEASE_ASSERT(result.ok());
+ }
+
+ MOZ_RELEASE_ASSERT(obj->inDictionaryMode() == config.native.inDictionaryMode);
+
+ return obj;
+}
+
+JSObject* CreateProxy(const ObjectConfig& config) {
+ RootedValue priv(cx, Int32Value(nextId++));
+
+ RootedObject expando(cx, NewPlainObject(cx));
+ RootedValue expandoId(cx, Int32Value(nextId++));
+ if (!expando || !JS_SetProperty(cx, expando, "id", expandoId)) {
+ return nullptr;
+ }
+
+ ProxyOptions options;
+ options.setClass(config.clasp);
+ options.setLazyProto(true);
+
+ const Wrapper* handler;
+ if (config.nurseryAllocated) {
+ handler = &NurseryProxyHandler::singleton;
+ } else {
+ handler = &TenuredProxyHandler::singleton;
+ }
+
+ RootedObject obj(cx, NewProxyObject(cx, handler, priv, nullptr, options));
+ if (!obj) {
+ return nullptr;
+ }
+
+ Rooted<ProxyObject*> proxy(cx, &obj->as<ProxyObject>());
+ proxy->setExpando(expando);
+
+ for (uint32_t i = 0; i < JSCLASS_RESERVED_SLOTS(config.clasp); i++) {
+ JS::SetReservedSlot(proxy, i, Int32Value(nextId++));
+ }
+
+ if (!config.proxy.inlineValues) {
+ // To create a proxy with non-inline values we must swap the proxy with an
+ // object with a different size.
+ NewObjectKind kind =
+ config.nurseryAllocated ? GenericObject : TenuredObject;
+ RootedObject dummy(cx,
+ NewBuiltinClassInstance(cx, &TestDOMClasses[0], kind));
+ if (!dummy) {
+ return nullptr;
+ }
+
+ AutoEnterOOMUnsafeRegion oomUnsafe;
+ JSObject::swap(cx, obj, dummy, oomUnsafe);
+ proxy = &dummy->as<ProxyObject>();
+ }
+
+ MOZ_RELEASE_ASSERT(IsInsideNursery(proxy) == config.nurseryAllocated);
+ MOZ_RELEASE_ASSERT(proxy->usingInlineValueArray() ==
+ config.proxy.inlineValues);
+
+ return proxy;
+}
+
+bool CheckObject(HandleObject obj, const ObjectConfig& config, uint32_t id) {
+ CHECK(obj->is<NativeObject>() == config.isNative);
+ CHECK(obj->getClass() == config.clasp);
+
+ uint32_t reservedSlots = JSCLASS_RESERVED_SLOTS(config.clasp);
+
+ if (Verbose) {
+ fprintf(stderr, "Check %p is a %s object with %u reserved slots", obj.get(),
+ config.isNative ? "native" : "proxy", reservedSlots);
+ if (config.isNative) {
+ fprintf(stderr,
+ ", %u properties, %u elements and %s in dictionary mode\n",
+ config.native.propCount, config.native.elementCount,
+ config.native.inDictionaryMode ? "is" : "is not");
+ } else {
+ fprintf(stderr, " with %s values\n",
+ config.proxy.inlineValues ? "inline" : "out-of-line");
+ }
+ }
+
+ if (!config.isNative) {
+ CHECK(GetProxyPrivate(obj) == Int32Value(id++));
+
+ Value expandoValue = GetProxyExpando(obj);
+ CHECK(expandoValue.isObject());
+
+ RootedObject expando(cx, &expandoValue.toObject());
+ RootedValue expandoId(cx);
+ JS_GetProperty(cx, expando, "id", &expandoId);
+ CHECK(expandoId == Int32Value(id++));
+ }
+
+ for (uint32_t i = 0; i < reservedSlots; i++) {
+ CHECK(JS::GetReservedSlot(obj, i) == Int32Value(id++));
+ }
+
+ if (config.isNative) {
+ Rooted<NativeObject*> nobj(cx, &obj->as<NativeObject>());
+ uint32_t propCount = GetPropertyCount(nobj);
+
+ CHECK(propCount == config.native.propCount);
+
+ for (uint32_t i = 0; i < config.native.propCount; i++) {
+ RootedId name(cx, CreatePropName(id++));
+ CHECK(!name.isVoid());
+
+ RootedValue value(cx);
+ CHECK(JS_GetPropertyById(cx, obj, name, &value));
+ CHECK(value == Int32Value(id++));
+ }
+
+ CHECK(nobj->getDenseInitializedLength() == config.native.elementCount);
+ for (uint32_t i = 0; i < config.native.elementCount; i++) {
+ Value value = nobj->getDenseElement(i);
+ CHECK(value == Int32Value(id++));
+ }
+ }
+
+ return true;
+}
+
+bool CheckUniqueIds(HandleObject obj1, bool hasUniqueId1, uint64_t uid1,
+ HandleObject obj2, bool hasUniqueId2, uint64_t uid2) {
+ if (uid1 && uid2) {
+ MOZ_RELEASE_ASSERT(uid1 != uid2);
+ }
+
+ // Check unique IDs are NOT swapped.
+ CHECK(CheckUniqueId(obj1, hasUniqueId1, uid1));
+ CHECK(CheckUniqueId(obj2, hasUniqueId2, uid2));
+
+ // Check unique IDs are different if present.
+ if (gc::HasUniqueId(obj1) && gc::HasUniqueId(obj2)) {
+ CHECK(gc::GetUniqueIdInfallible(obj1) != gc::GetUniqueIdInfallible(obj2));
+ }
+
+ return true;
+}
+
+bool CheckUniqueId(HandleObject obj, bool hasUniqueId, uint64_t uid) {
+ if (hasUniqueId) {
+ CHECK(gc::HasUniqueId(obj));
+ CHECK(gc::GetUniqueIdInfallible(obj) == uid);
+ } else {
+ // Swap may add a unique ID to an object.
+ }
+
+ if (obj->is<NativeObject>()) {
+ CHECK(!obj->zone()->uniqueIds().has(obj));
+ }
+ return true;
+}
+
+uint32_t GetPropertyCount(NativeObject* obj) {
+ uint32_t count = 0;
+ for (ShapePropertyIter<NoGC> iter(obj->shape()); !iter.done(); iter++) {
+ count++;
+ }
+
+ return count;
+}
+
+jsid CreatePropName(uint32_t id) {
+ char buffer[32];
+ SprintfLiteral(buffer, "prop%u", id);
+
+ RootedString atom(cx, JS_AtomizeString(cx, buffer));
+ if (!atom) {
+ return jsid::Void();
+ }
+
+ return jsid::NonIntAtom(atom);
+}
+
+END_TEST(testObjectSwap)