Bug 1140558 - Part 2 - Make the testing deepEqual implementation shared properly in ObjectUtils.jsm. r=yoric, a=lmandel
authorGeorg Fritzsche <georg.fritzsche@googlemail.com>
Thu, 02 Apr 2015 21:33:46 +0200
changeset 267213 aab52686db29026236fb63a53b3defc891617fba
parent 267212 79582fe6d87e5202c533c97aeed9fbf297415a10
child 267214 e289c20fd8ee1b8cdee481c504f51ca529646a83
push id830
push userraliiev@mozilla.com
push dateFri, 19 Jun 2015 19:24:37 +0000
treeherdermozilla-release@932614382a68 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersyoric, lmandel
bugs1140558
milestone39.0a2
Bug 1140558 - Part 2 - Make the testing deepEqual implementation shared properly in ObjectUtils.jsm. r=yoric, a=lmandel
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,96 @@
+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;
+
+  try {
+    Assert.ok(!deepEqual(b, c));
+  } catch (e) {
+    Assert.ok(true, "Didn't recurse infinitely.");
+  }
+});
--- 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]