Bug 1140558 - Part 2 - Make the testing deepEqual implementation shared properly in ObjectUtils.jsm. r=yoric
☠☠ backed out by 83e259349f52 ☠ ☠
authorGeorg Fritzsche <georg.fritzsche@googlemail.com>
Fri, 27 Mar 2015 21:01:19 +0100
changeset 265048 9623fa2b2e16de005b1508ba257e78b53c55270b
parent 265047 d7a94bbfa3ef6a267d711d4e806fd3c6bbde0a77
child 265049 a54f84a3d26537d9ed5ed0472a4abd480b2c8b79
push id4718
push userraliiev@mozilla.com
push dateMon, 11 May 2015 18:39:53 +0000
treeherdermozilla-beta@c20c4ef55f08 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersyoric
bugs1140558
milestone39.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1140558 - Part 2 - Make the testing deepEqual implementation shared properly in ObjectUtils.jsm. r=yoric
testing/modules/Assert.jsm
testing/modules/tests/xpcshell/test_assert.js
toolkit/modules/ObjectUtils.jsm
toolkit/modules/moz.build
toolkit/modules/tests/xpcshell/test_ObjectUtils.js
toolkit/modules/tests/xpcshell/xpcshell.ini
--- a/testing/modules/Assert.jsm
+++ b/testing/modules/Assert.jsm
@@ -12,16 +12,17 @@
 
 "use strict";
 
 this.EXPORTED_SYMBOLS = [
   "Assert"
 ];
 
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/ObjectUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                   "resource://gre/modules/Promise.jsm");
 /**
  * 1. The assert module provides functions that throw AssertionError's when
  * particular conditions are not met.
  *
  * To use the module you'll need to instantiate it first, which allows consumers
@@ -253,105 +254,19 @@ proto.notEqual = function notEqual(actua
  * @param actual
  *        (mixed) Test subject to be evaluated as equivalent to `expected`, including nested properties
  * @param expected
  *        (mixed) Test reference to evaluate against `actual`
  * @param message (optional)
  *        (string) Short explanation of the expected result
  */
 proto.deepEqual = function deepEqual(actual, expected, message) {
-  this.report(!_deepEqual(actual, expected), actual, expected, message, "deepEqual");
+  this.report(!ObjectUtils.deepEqual(actual, expected), actual, expected, message, "deepEqual");
 };
 
-function _deepEqual(actual, expected) {
-  // 7.1. All identical values are equivalent, as determined by ===.
-  if (actual === expected) {
-    return true;
-  // 7.2. If the expected value is a Date object, the actual value is
-  // equivalent if it is also a Date object that refers to the same time.
-  } else if (instanceOf(actual, "Date") && instanceOf(expected, "Date")) {
-    if (isNaN(actual.getTime()) && isNaN(expected.getTime()))
-      return true;
-    return actual.getTime() === expected.getTime();
-  // 7.3 If the expected value is a RegExp object, the actual value is
-  // equivalent if it is also a RegExp object with the same source and
-  // properties (`global`, `multiline`, `lastIndex`, `ignoreCase`).
-  } else if (instanceOf(actual, "RegExp") && instanceOf(expected, "RegExp")) {
-    return actual.source === expected.source &&
-           actual.global === expected.global &&
-           actual.multiline === expected.multiline &&
-           actual.lastIndex === expected.lastIndex &&
-           actual.ignoreCase === expected.ignoreCase;
-  // 7.4. Other pairs that do not both pass typeof value == "object",
-  // equivalence is determined by ==.
-  } else if (typeof actual != "object" && typeof expected != "object") {
-    return actual == expected;
-  // 7.5 For all other Object pairs, including Array objects, equivalence is
-  // determined by having the same number of owned properties (as verified
-  // with Object.prototype.hasOwnProperty.call), the same set of keys
-  // (although not necessarily the same order), equivalent values for every
-  // corresponding key, and an identical 'prototype' property. Note: this
-  // accounts for both named and indexed properties on Arrays.
-  } else {
-    return objEquiv(actual, expected);
-  }
-}
-
-function isUndefinedOrNull(value) {
-  return value === null || value === undefined;
-}
-
-function isArguments(object) {
-  return instanceOf(object, "Arguments");
-}
-
-function objEquiv(a, b) {
-  if (isUndefinedOrNull(a) || isUndefinedOrNull(b)) {
-    return false;
-  }
-  // An identical 'prototype' property.
-  if (a.prototype !== b.prototype) {
-    return false;
-  }
-  // Object.keys may be broken through screwy arguments passing. Converting to
-  // an array solves the problem.
-  if (isArguments(a)) {
-    if (!isArguments(b)) {
-      return false;
-    }
-    a = pSlice.call(a);
-    b = pSlice.call(b);
-    return _deepEqual(a, b);
-  }
-  let ka, kb, key, i;
-  try {
-    ka = Object.keys(a);
-    kb = Object.keys(b);
-  } catch (e) {
-    // Happens when one is a string literal and the other isn't
-    return false;
-  }
-  // Having the same number of owned properties (keys incorporates
-  // hasOwnProperty)
-  if (ka.length != kb.length)
-    return false;
-  // The same set of keys (although not necessarily the same order),
-  ka.sort();
-  kb.sort();
-  // Equivalent values for every corresponding key, and possibly expensive deep 
-  // test
-  for (i = ka.length - 1; i >= 0; i--) {
-    key = ka[i];
-    if (!_deepEqual(a[key], b[key])) {
-      return false;
-    }
-  }
-  return true;
-}
-
 /**
  * 8. The non-equivalence assertion tests for any deep inequality.
  * assert.notDeepEqual(actual, expected, message_opt);
  *
  * @param actual
  *        (mixed) Test subject to be evaluated as NOT equivalent to `expected`, including nested properties
  * @param expected
  *        (mixed) Test reference to evaluate against `actual`
--- a/testing/modules/tests/xpcshell/test_assert.js
+++ b/testing/modules/tests/xpcshell/test_assert.js
@@ -204,34 +204,16 @@ function run_test() {
 
   // use a fn to validate error object
   assert.throws(makeBlock(thrower, TypeError), function(err) {
     if ((err instanceof TypeError) && /test/.test(err)) {
       return true;
     }
   });
 
-  // Make sure deepEqual doesn't loop forever on circular refs
-
-  let b = {};
-  b.b = b;
-
-  let c = {};
-  c.b = c;
-
-  let gotError = false;
-  try {
-    assert.deepEqual(b, c);
-  } catch (e) {
-    gotError = true;
-  }
-
-  dump("All OK\n");
-  assert.ok(gotError);
-
   function testAssertionMessage(actual, expected) {
     try {
       assert.equal(actual, "");
     } catch (e) {
       assert.equal(e.toString(),
           ["AssertionError:", expected, "==", '""'].join(" "));
     }
   }
new file mode 100644
--- /dev/null
+++ b/toolkit/modules/ObjectUtils.jsm
@@ -0,0 +1,131 @@
+/* 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/. */
+
+// Portions of this file are originally from narwhal.js (http://narwhaljs.org)
+// Copyright (c) 2009 Thomas Robinson <280north.com>
+// MIT license: http://opensource.org/licenses/MIT
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+  "ObjectUtils"
+];
+
+const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
+
+this.ObjectUtils = {
+  /**
+   * This tests objects & values for deep equality.
+   *
+   * We check using the most exact approximation of equality between two objects
+   * to keep the chance of false positives to a minimum.
+   * `JSON.stringify` is not designed to be used for this purpose; objects may
+   * have ambiguous `toJSON()` implementations that would influence the test.
+   *
+   * @param a (mixed) Object or value to be compared.
+   * @param b (mixed) Object or value to be compared.
+   * @return Boolean Whether the objects are deep equal.
+   */
+  deepEqual: function(a, b) {
+    return _deepEqual(a, b);
+  },
+};
+
+// ... Start of previously MIT-licensed code.
+// This deepEqual implementation is originally from narwhal.js (http://narwhaljs.org)
+// Copyright (c) 2009 Thomas Robinson <280north.com>
+// MIT license: http://opensource.org/licenses/MIT
+
+function _deepEqual(a, b) {
+  // The numbering below refers to sections in the CommonJS spec.
+
+  // 7.1 All identical values are equivalent, as determined by ===.
+  if (a === b) {
+    return true;
+  // 7.2 If the b value is a Date object, the a value is
+  // equivalent if it is also a Date object that refers to the same time.
+  } else if (instanceOf(a, "Date") && instanceOf(b, "Date")) {
+    if (isNaN(a.getTime()) && isNaN(b.getTime()))
+      return true;
+    return a.getTime() === b.getTime();
+  // 7.3 If the b value is a RegExp object, the a value is
+  // equivalent if it is also a RegExp object with the same source and
+  // properties (`global`, `multiline`, `lastIndex`, `ignoreCase`).
+  } else if (instanceOf(a, "RegExp") && instanceOf(b, "RegExp")) {
+    return a.source === b.source &&
+           a.global === b.global &&
+           a.multiline === b.multiline &&
+           a.lastIndex === b.lastIndex &&
+           a.ignoreCase === b.ignoreCase;
+  // 7.4 Other pairs that do not both pass typeof value == "object",
+  // equivalence is determined by ==.
+  } else if (typeof a != "object" && typeof b != "object") {
+    return a == b;
+  // 7.5 For all other Object pairs, including Array objects, equivalence is
+  // determined by having the same number of owned properties (as verified
+  // with Object.prototype.hasOwnProperty.call), the same set of keys
+  // (although not necessarily the same order), equivalent values for every
+  // corresponding key, and an identical 'prototype' property. Note: this
+  // accounts for both named and indexed properties on Arrays.
+  } else {
+    return objEquiv(a, b);
+  }
+}
+
+function instanceOf(object, type) {
+  return Object.prototype.toString.call(object) == "[object " + type + "]";
+}
+
+function isUndefinedOrNull(value) {
+  return value === null || value === undefined;
+}
+
+function isArguments(object) {
+  return instanceOf(object, "Arguments");
+}
+
+function objEquiv(a, b) {
+  if (isUndefinedOrNull(a) || isUndefinedOrNull(b)) {
+    return false;
+  }
+  // An identical 'prototype' property.
+  if (a.prototype !== b.prototype) {
+    return false;
+  }
+  // Object.keys may be broken through screwy arguments passing. Converting to
+  // an array solves the problem.
+  if (isArguments(a)) {
+    if (!isArguments(b)) {
+      return false;
+    }
+    a = pSlice.call(a);
+    b = pSlice.call(b);
+    return _deepEqual(a, b);
+  }
+  let ka, kb;
+  try {
+    ka = Object.keys(a);
+    kb = Object.keys(b);
+  } catch (e) {
+    // Happens when one is a string literal and the other isn't
+    return false;
+  }
+  // Having the same number of owned properties (keys incorporates
+  // hasOwnProperty)
+  if (ka.length != kb.length)
+    return false;
+  // The same set of keys (although not necessarily the same order),
+  ka.sort();
+  kb.sort();
+  // Equivalent values for every corresponding key, and possibly expensive deep
+  // test
+  for (let key of ka) {
+    if (!_deepEqual(a[key], b[key])) {
+      return false;
+    }
+  }
+  return true;
+}
+
+// ... End of previously MIT-licensed code.
--- a/toolkit/modules/moz.build
+++ b/toolkit/modules/moz.build
@@ -25,16 +25,17 @@ EXTRA_JS_MODULES += [
     'Finder.jsm',
     'Geometry.jsm',
     'Http.jsm',
     'InlineSpellChecker.jsm',
     'InlineSpellCheckerContent.jsm',
     'LoadContextInfo.jsm',
     'Log.jsm',
     'NewTabUtils.jsm',
+    'ObjectUtils.jsm',
     'PageMenu.jsm',
     'PageMetadata.jsm',
     'PermissionsUtils.jsm',
     'PopupNotifications.jsm',
     'Preferences.jsm',
     'PrivateBrowsingUtils.jsm',
     'ProfileAge.jsm',
     'Promise-backend.js',
new file mode 100644
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/test_ObjectUtils.js
@@ -0,0 +1,92 @@
+Components.utils.import("resource://gre/modules/ObjectUtils.jsm");
+
+function run_test() {
+  run_next_test();
+}
+
+add_task(function* test_deepEqual() {
+  let deepEqual = ObjectUtils.deepEqual.bind(ObjectUtils);
+  // CommonJS 7.2
+  Assert.ok(deepEqual(new Date(2000, 3, 14), new Date(2000, 3, 14)), "deepEqual date");
+  Assert.ok(deepEqual(new Date(NaN), new Date(NaN)), "deepEqual invalid dates");
+
+  Assert.ok(!deepEqual(new Date(), new Date(2000, 3, 14)), "deepEqual date");
+
+  // 7.3
+  Assert.ok(deepEqual(/a/, /a/));
+  Assert.ok(deepEqual(/a/g, /a/g));
+  Assert.ok(deepEqual(/a/i, /a/i));
+  Assert.ok(deepEqual(/a/m, /a/m));
+  Assert.ok(deepEqual(/a/igm, /a/igm));
+  Assert.ok(!deepEqual(/ab/, /a/));
+  Assert.ok(!deepEqual(/a/g, /a/));
+  Assert.ok(!deepEqual(/a/i, /a/));
+  Assert.ok(!deepEqual(/a/m, /a/));
+  Assert.ok(!deepEqual(/a/igm, /a/im));
+
+  let re1 = /a/;
+  re1.lastIndex = 3;
+  Assert.ok(!deepEqual(re1, /a/));
+
+  // 7.4
+  Assert.ok(deepEqual(4, "4"), "deepEqual == check");
+  Assert.ok(deepEqual(true, 1), "deepEqual == check");
+  Assert.ok(!deepEqual(4, "5"), "deepEqual == check");
+
+  // 7.5
+  // having the same number of owned properties && the same set of keys
+  Assert.ok(deepEqual({a: 4}, {a: 4}));
+  Assert.ok(deepEqual({a: 4, b: "2"}, {a: 4, b: "2"}));
+  Assert.ok(deepEqual([4], ["4"]));
+  Assert.ok(!deepEqual({a: 4}, {a: 4, b: true}));
+  Assert.ok(deepEqual(["a"], {0: "a"}));
+
+  let a1 = [1, 2, 3];
+  let a2 = [1, 2, 3];
+  a1.a = "test";
+  a1.b = true;
+  a2.b = true;
+  a2.a = "test";
+  Assert.ok(!deepEqual(Object.keys(a1), Object.keys(a2)));
+  Assert.ok(deepEqual(a1, a2));
+
+  let nbRoot = {
+    toString: function() { return this.first + " " + this.last; }
+  };
+
+  function nameBuilder(first, last) {
+    this.first = first;
+    this.last = last;
+    return this;
+  }
+  nameBuilder.prototype = nbRoot;
+
+  function nameBuilder2(first, last) {
+    this.first = first;
+    this.last = last;
+    return this;
+  }
+  nameBuilder2.prototype = nbRoot;
+
+  let nb1 = new nameBuilder("Ryan", "Dahl");
+  let nb2 = new nameBuilder2("Ryan", "Dahl");
+
+  Assert.ok(deepEqual(nb1, nb2));
+
+  nameBuilder2.prototype = Object;
+  nb2 = new nameBuilder2("Ryan", "Dahl");
+  Assert.ok(!deepEqual(nb1, nb2));
+
+  // String literal + object
+  Assert.ok(!deepEqual("a", {}));
+
+  // Make sure deepEqual doesn't loop forever on circular refs
+
+  let b = {};
+  b.b = b;
+
+  let c = {};
+  c.b = c;
+
+  Assert.ok(!deepEqual(b, c));
+});
--- a/toolkit/modules/tests/xpcshell/xpcshell.ini
+++ b/toolkit/modules/tests/xpcshell/xpcshell.ini
@@ -11,16 +11,17 @@ support-files =
 [test_BinarySearch.js]
 [test_DeferredTask.js]
 [test_dict.js]
 [test_FileUtils.js]
 [test_GMPInstallManager.js]
 [test_Http.js]
 [test_Log.js]
 [test_NewTabUtils.js]
+[test_ObjectUtils.js]
 [test_PermissionsUtils.js]
 [test_Preferences.js]
 [test_Promise.js]
 [test_PromiseUtils.js]
 [test_propertyListsUtils.js]
 [test_readCertPrefs.js]
 [test_Services.js]
 [test_sqlite.js]