Bug 1242505 - Part 2 - Update PromiseTestUtils for use in mochitests. r=Mossop draft
authorPaolo Amadini <paolo.mozmail@amadzone.org>
Thu, 25 May 2017 15:00:29 +0100
changeset 585615 de5fa91a5e9769c7ca42fc2c00ea54f169a3e12b
parent 585614 83a77cbfe5629abe99ae575b57592361ea9627b8
child 585616 59e5b84cb431f3ca28287d30a3da8fbea1363ec5
push id61146
push userpaolo.mozmail@amadzone.org
push dateSat, 27 May 2017 08:22:25 +0000
reviewersMossop
bugs1242505
milestone55.0a1
Bug 1242505 - Part 2 - 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 = [];
   },
 };