Bug 1370653 - Update PromiseTestUtils for use in mochitests. r=Mossop
authorPaolo Amadini <paolo.mozmail@amadzone.org>
Tue, 06 Jun 2017 21:33:08 +0100
changeset 410773 eb977b3751ca6084523f35adb88ca49dfab84fe5
parent 410772 985626bc4d9c44f1bb12f808276a256397596d0b
child 410774 3666d91a4263663c5e56f3daa7b9d20f9cc9922f
push id7391
push usermtabara@mozilla.com
push dateMon, 12 Jun 2017 13:08:53 +0000
treeherdermozilla-beta@2191d7f87e2e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMossop
bugs1370653
milestone55.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 1370653 - Update PromiseTestUtils for use in mochitests. r=Mossop This adds a new coarse-grained whitelisting function, whose usage should be kept to a minimum but is necessary because many mochitests have cleanup issues on shutdown. The module now handles cases that only happen in mochitests, where rejections can occur in contexts that are unloaded and more than one test file can be executed sequentially in the same process. MozReview-Commit-ID: 8xejMxoSBzf
toolkit/modules/tests/modules/PromiseTestUtils.jsm
--- a/toolkit/modules/tests/modules/PromiseTestUtils.jsm
+++ b/toolkit/modules/tests/modules/PromiseTestUtils.jsm
@@ -48,16 +48,23 @@ this.PromiseTestUtils = {
    * When an uncaught rejection is detected, it is ignored if one of the
    * functions in this array returns true when called with the rejection details
    * as its only argument. When a function matches an expected rejection, it is
    * then removed from the array.
    */
   _rejectionIgnoreFns: [],
 
   /**
+   * If any of the functions in this array returns true when called with the
+   * rejection details as its only argument, the rejection is ignored. This
+   * happens after the "_rejectionIgnoreFns" array is processed.
+   */
+  _globalRejectionIgnoreFns: [],
+
+  /**
    * Called only by the test infrastructure, registers the rejection observers.
    *
    * This should be called only once, and a matching "uninit" call must be made
    * or the tests will crash on shutdown.
    */
   init() {
     if (this._initialized) {
       Cu.reportError("This object was already initialized.");
@@ -145,23 +152,31 @@ this.PromiseTestUtils = {
       let reason = PromiseDebugging.getState(promise).reason;
       if (reason === this._ensureDOMPromiseRejectionsProcessedReason) {
         // Ignore the special promise for ensureDOMPromiseRejectionsProcessed.
         return;
       }
       message = reason.message || ("" + reason);
     } catch (ex) {}
 
+    // We should convert the rejection stack to a string immediately. This is
+    // because the object might not be available when we report the rejection
+    // later, if the error occurred in a context that has been unloaded.
+    let stack = "(Unable to convert rejection stack to string.)";
+    try {
+      stack = "" + PromiseDebugging.getRejectionStack(promise);
+    } catch (ex) {}
+
     // It's important that we don't store any reference to the provided Promise
     // object or its value after this function returns in order to avoid leaks.
     this._rejections.push({
       id: PromiseDebugging.getPromiseID(promise),
       message,
       date: new Date(),
-      stack: PromiseDebugging.getRejectionStack(promise),
+      stack,
     });
   },
 
   // UncaughtRejectionObserver
   onConsumed(promise) {
     // We don't expect that many unhandled rejections will appear at the same
     // time, so the algorithm doesn't need to be optimized for that case.
     let id = PromiseDebugging.getPromiseID(promise);
@@ -190,16 +205,29 @@ this.PromiseTestUtils = {
    */
   expectUncaughtRejection(regExpOrCheckFn) {
     let checkFn = !("test" in regExpOrCheckFn) ? regExpOrCheckFn :
                   rejection => regExpOrCheckFn.test(rejection.message);
     this._rejectionIgnoreFns.push(checkFn);
   },
 
   /**
+   * Whitelists an entire class of Promise rejections. Usage of this function
+   * should be kept to a minimum because it has a broad scope and doesn't
+   * prevent new unhandled rejections of this class from being added.
+   *
+   * @param regExp
+   *        This should match the error message of the rejection.
+   */
+  whitelistRejectionsGlobally(regExp) {
+    this._globalRejectionIgnoreFns.push(
+      rejection => regExp.test(rejection.message));
+  },
+
+  /**
    * Fails the test if there are any uncaught rejections at this time that have
    * not been whitelisted using expectUncaughtRejection.
    *
    * Depending on the configuration of the test suite, this function might only
    * report the details of the first uncaught rejection that was generated.
    *
    * This is called by the test suite at the end of each test function.
    */
@@ -214,16 +242,21 @@ this.PromiseTestUtils = {
       // If one of the ignore functions matches, ignore the rejection, then
       // remove the function so that each function only matches one rejection.
       let index = this._rejectionIgnoreFns.findIndex(f => f(rejection));
       if (index != -1) {
         this._rejectionIgnoreFns.splice(index, 1);
         continue;
       }
 
+      // Check the global whitelisting functions.
+      if (this._globalRejectionIgnoreFns.some(fn => fn(rejection))) {
+        continue;
+      }
+
       // Report the error. This operation can throw an exception, depending on
       // the configuration of the test suite that handles the assertion.
       Assert.ok(false,
                 `A promise chain failed to handle a rejection:` +
                 ` ${rejection.message} - rejection date: ${rejection.date}` +
                 ` - stack: ${rejection.stack}`);
     }
   },
@@ -235,10 +268,12 @@ this.PromiseTestUtils = {
    * This is called by the test suite at the end of each test file.
    */
   assertNoMoreExpectedRejections() {
     // Only log this condition is there is a failure.
     if (this._rejectionIgnoreFns.length > 0) {
       Assert.equal(this._rejectionIgnoreFns.length, 0,
              "Unable to find a rejection expected by expectUncaughtRejection.");
     }
+    // Reset the list of expected rejections in case the test suite continues.
+    this._rejectionIgnoreFns = [];
   },
 };