Bug 748309 - Test structured clone handling of cyclic graphs. r=jorendorff
authorSteve Fink <sfink@mozilla.com>
Fri, 21 Sep 2012 13:10:28 -0700
changeset 107767 2c9976725a5779075efb21a087ccc644eac0684c
parent 107766 f0e182ab06a9a0aa65825544ac44135e9e67e42a
child 107768 259206b2c7b25d08ac11125c814de0ee43ea121a
push id1708
push userakeybl@mozilla.com
push dateMon, 19 Nov 2012 21:10:21 +0000
treeherdermozilla-esr52@2704e441363f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjorendorff
bugs748309
milestone18.0a1
Bug 748309 - Test structured clone handling of cyclic graphs. r=jorendorff
js/src/tests/js1_8_5/extensions/clone-complex-object.js
--- a/js/src/tests/js1_8_5/extensions/clone-complex-object.js
+++ b/js/src/tests/js1_8_5/extensions/clone-complex-object.js
@@ -1,77 +1,168 @@
 // |reftest| skip-if(!xulRuntime.shell)
 // -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 // Any copyright is dedicated to the Public Domain.
 // http://creativecommons.org/licenses/publicdomain/
 
+// Set of properties on a cloned object that are legitimately non-enumerable,
+// grouped by object type.
+var non_enumerable = { 'Array': [ 'length' ],
+                       'String': [ 'length' ] };
+
+// Set of properties on a cloned object that are legitimately non-configurable,
+// grouped by object type. The property name '0' stands in for any indexed
+// property.
+var non_configurable = { 'String': [ 0 ],
+                         '(typed array)': [ 0 ] };
+
+// Set of properties on a cloned object that are legitimately non-writable,
+// grouped by object type. The property name '0' stands in for any indexed
+// property.
+var non_writable = { 'String': [ 0 ] };
+
+function classOf(obj) {
+    var classString = Object.prototype.toString.call(obj);
+    var [ all, classname ] = classString.match(/\[object (\w+)/);
+    return classname;
+}
+
+function isIndex(p) {
+    var u = p >>> 0;
+    return ("" + u == p && u != 0xffffffff);
+}
+
+function notIndex(p) {
+    return !isIndex(p);
+}
+
+function tableContains(table, cls, prop) {
+    if (isIndex(prop))
+        prop = 0;
+    if (cls.match(/\wArray$/))
+        cls = "(typed array)";
+    var exceptionalProps = table[cls] || [];
+    return exceptionalProps.indexOf(prop) != -1;
+}
+
+function shouldBeConfigurable(cls, prop) {
+    return !tableContains(non_configurable, cls, prop);
+}
+
+function shouldBeWritable(cls, prop) {
+    return !tableContains(non_writable, cls, prop);
+}
+
+function ownProperties(obj) {
+    return Object.getOwnPropertyNames(obj).
+        map(function (p) { return [p, Object.getOwnPropertyDescriptor(obj, p)]; });
+}
+
+function isCloneable(pair) {
+    return typeof pair[0] === 'string' && pair[1].enumerable;
+}
+
+function compareProperties(a, b, stack, path) {
+    var ca = classOf(a);
+
+    // 'b', the original object, may have non-enumerable or XMLName properties;
+    // ignore them. 'a', the clone, should not have any non-enumerable
+    // properties (except .length, if it's an Array or String) or XMLName
+    // properties.
+    var pb = ownProperties(b).filter(isCloneable);
+    var pa = ownProperties(a);
+    for (var i = 0; i < pa.length; i++) {
+        var propname = pa[i][0];
+        assertEq(typeof propname, "string", "clone should not have E4X properties " + path);
+        if (!pa[i][1].enumerable) {
+            if (tableContains(non_enumerable, ca, propname)) {
+                // remove it so that the comparisons below will work
+                pa.splice(i, 1);
+                i--;
+            } else {
+                throw new Error("non-enumerable clone property " + uneval(pa[i][0]) + " " + path);
+            }
+        }
+    }
+
+    // Check that, apart from properties whose names are array indexes,
+    // the enumerable properties appear in the same order.
+    var aNames = pa.map(function (pair) { return pair[1]; }).filter(notIndex);
+    var bNames = pa.map(function (pair) { return pair[1]; }).filter(notIndex);
+    assertEq(aNames.join(","), bNames.join(","), path);
+
+    // Check that the lists are the same when including array indexes.
+    function byName(a, b) { a = a[0]; b = b[0]; return a < b ? -1 : a === b ? 0 : 1; }
+    pa.sort(byName);
+    pb.sort(byName);
+    assertEq(pa.length, pb.length, "should see the same number of properties " + path);
+    for (var i = 0; i < pa.length; i++) {
+        var aName = pa[i][0];
+        var bName = pb[i][0];
+        assertEq(aName, bName, path);
+
+        var path2 = isIndex(aName) ? path + "[" + aName + "]" : path + "." + aName;
+        var da = pa[i][1];
+        var db = pb[i][1];
+        assertEq(da.configurable, shouldBeConfigurable(ca, aName), path2);
+        assertEq(da.writable, shouldBeWritable(ca, aName), path2);
+        assertEq("value" in da, true, path2);
+        var va = da.value;
+        var vb = b[pb[i][0]];
+        stack.push([va, vb, path2]);
+    }
+}
+
 function isClone(a, b) {
-    var stack = [[a, b]];
+    var stack = [[a, b, 'obj']];
     var memory = new WeakMap();
     var rmemory = new WeakMap();
 
     while (stack.length > 0) {
         var pair = stack.pop();
-        var x = pair[0], y = pair[1];
+        var x = pair[0], y = pair[1], path = pair[2];
         if (typeof x !== "object" || x === null) {
             // x is primitive.
-            if (x !== y)
-                return false;
+            assertEq(x, y, "equal primitives");
         } else if (x instanceof Date) {
-            if (x.getTime() !== y.getTime())
-                return false;
+            assertEq(x.getTime(), y.getTime(), "equal times for cloned Dates");
         } else if (memory.has(x)) {
             // x is an object we have seen before in a.
-            if (y !== memory.get(x))
-                return false;
-            assertEq(rmemory.get(y), x);
+            assertEq(y, memory.get(x), "repeated object the same");
+            assertEq(rmemory.get(y), x, "repeated object's clone already seen");
         } else {
             // x is an object we have not seen before.
-	          // Check that we have not seen y before either.
-            if (rmemory.has(y))
-                return false;
-
-            // x and y must be of the same [[Class]].
-            var xcls = Object.prototype.toString.call(x);
-            var ycls = Object.prototype.toString.call(y);
-            if (xcls !== ycls)
-                return false;
+	    // Check that we have not seen y before either.
+            assertEq(rmemory.has(y), false);
 
-            // This function is only designed to check Objects and Arrays.
-            assertEq(xcls === "[object Object]" || xcls === "[object Array]",
-                     true);
+            var xcls = classOf(x);
+            var ycls = classOf(y);
+            assertEq(xcls, ycls, "same [[Class]]");
 
-            // Compare objects.
-            var xk = Object.keys(x), yk = Object.keys(y);
-            if (xk.length !== yk.length)
-                return false;
-            for (var i = 0; i < xk.length; i++) {
-                // We must see the same property names in the same order.
-                if (xk[i] !== yk[i])
-                    return false;
+            // clone objects should have the default prototype of the class
+            assertEq(Object.getPrototypeOf(x), this[xcls].prototype);
 
-                // Put the property values on the stack to compare later.
-                stack.push([x[xk[i]], y[yk[i]]]);
-            }
+            compareProperties(x, y, stack, path);
 
             // Record that we have seen this pair of objects.
             memory.set(x, y);
             rmemory.set(y, x);
         }
     }
     return true;
 }
 
-function check(a) {
-    assertEq(isClone(a, deserialize(serialize(a))), true);
+function check(val) {
+    var clone = deserialize(serialize(val));
+    assertEq(isClone(val, clone), true);
+    return clone;
 }
 
-// Various recursive objects, i.e. those which the structured cloning
-// algorithm wants us to reject due to "memory".
-//
+// Various recursive objects
+
 // Recursive array.
 var a = [];
 a[0] = a;
 check(a);
 
 // Recursive Object.
 var b = {};
 b.next = b;
@@ -98,9 +189,125 @@ for (var i = 0; i < 10000; i++) {
     b[0] = {};
     b[1] = [];
     b = b[1];
 }
 b[0] = {owner: a};
 b[1] = [];
 check(a);
 
+// Date objects should not be identical even if representing the same date
+var ar = [ new Date(1000), new Date(1000) ];
+var clone = check(ar);
+assertEq(clone[0] === clone[1], false);
+
+// Identity preservation for various types of objects
+
+function checkSimpleIdentity(v)
+{
+    a = check([ v, v ]);
+    assertEq(a[0] === a[1], true);
+    return a;
+}
+
+var v = new Boolean(true);
+checkSimpleIdentity(v);
+
+v = new Number(17);
+checkSimpleIdentity(v);
+
+v = new String("yo");
+checkSimpleIdentity(v);
+
+v = "fish";
+checkSimpleIdentity(v);
+
+v = new Int8Array([ 10, 20 ]);
+checkSimpleIdentity(v);
+
+v = new ArrayBuffer(7);
+checkSimpleIdentity(v);
+
+v = new Date(1000);
+b = [ v, v, { 'date': v } ];
+clone = check(b);
+assertEq(clone[0] === clone[1], true);
+assertEq(clone[0], clone[2]['date']);
+assertEq(clone[0] === v, false);
+
+// Reduced and modified from postMessage_structured_clone test
+let foo = { };
+let baz = { };
+let obj = { 'foo': foo,
+            'bar': { 'foo': foo },
+            'expando': { 'expando': baz },
+            'baz': baz };
+check(obj);
+
+for (var obj of new getTestContent)
+    check(obj);
+
+// Stolen wholesale from postMessage_structured_clone_helper.js
+function getTestContent()
+{
+  yield "hello";
+  yield 2+3;
+  yield 12;
+  yield null;
+  yield "complex" + "string";
+  yield new Object();
+  yield new Date(1306113544);
+  yield [1, 2, 3, 4, 5];
+  let obj = new Object();
+  obj.foo = 3;
+  obj.bar = "hi";
+  obj.baz = new Date(1306113544);
+  obj.boo = obj;
+  yield obj;
+
+  let recursiveobj = new Object();
+  recursiveobj.a = recursiveobj;
+  recursiveobj.foo = new Object();
+  recursiveobj.foo.bar = "bar";
+  recursiveobj.foo.backref = recursiveobj;
+  recursiveobj.foo.baz = 84;
+  recursiveobj.foo.backref2 = recursiveobj;
+  recursiveobj.bar = new Object();
+  recursiveobj.bar.foo = "foo";
+  recursiveobj.bar.backref = recursiveobj;
+  recursiveobj.bar.baz = new Date(1306113544);
+  recursiveobj.bar.backref2 = recursiveobj;
+  recursiveobj.expando = recursiveobj;
+  yield recursiveobj;
+
+  let obj = new Object();
+  obj.expando1 = 1;
+  obj.foo = new Object();
+  obj.foo.bar = 2;
+  obj.bar = new Object();
+  obj.bar.foo = obj.foo;
+  obj.expando = new Object();
+  obj.expando.expando = new Object();
+  obj.expando.expando.obj = obj;
+  obj.expando2 = 4;
+  obj.baz = obj.expando.expando;
+  obj.blah = obj.bar;
+  obj.foo.baz = obj.blah;
+  obj.foo.blah = obj.blah;
+  yield obj;
+
+  let diamond = new Object();
+  let obj = new Object();
+  obj.foo = "foo";
+  obj.bar = 92;
+  obj.backref = diamond;
+  diamond.ref1 = obj;
+  diamond.ref2 = obj;
+  yield diamond;
+
+  let doubleref = new Object();
+  let obj = new Object();
+  doubleref.ref1 = obj;
+  doubleref.ref2 = obj;
+  yield doubleref;
+}
+
 reportCompare(0, 0, 'ok');