Bug 810490 - Constant stack space promise. r=mossop
authorPaolo Amadini <paolo.mozmail@amadzone.org>
Thu, 30 May 2013 14:23:42 +0200
changeset 133452 87bbe2a5b08af46e1ca3a7d17f3217efdef3b282
parent 133451 d7c6d6061ab59a60446ea3c9d1e092dfdb04811e
child 133453 d71234d65e90c487182729cfce61c98666be0f51
push idunknown
push userunknown
push dateunknown
reviewersmossop
bugs810490
milestone24.0a1
Bug 810490 - Constant stack space promise. r=mossop
addon-sdk/moz.build
addon-sdk/test/moz.build
addon-sdk/test/unit/head.js
addon-sdk/test/unit/test_promise.js
addon-sdk/test/unit/xpcshell.ini
toolkit/modules/Makefile.in
toolkit/modules/Promise.jsm
toolkit/modules/tests/xpcshell/test_Promise.js
toolkit/modules/tests/xpcshell/xpcshell.ini
--- a/addon-sdk/moz.build
+++ b/addon-sdk/moz.build
@@ -1,8 +1,6 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
-TEST_DIRS += ['test']
-
deleted file mode 100644
--- a/addon-sdk/test/moz.build
+++ /dev/null
@@ -1,9 +0,0 @@
-# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
-# vim: set filetype=python:
-# 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/.
-
-MODULE = 'test_addon_sdk'
-
-XPCSHELL_TESTS_MANIFESTS += ['unit/xpcshell.ini']
deleted file mode 100644
--- a/addon-sdk/test/unit/head.js
+++ /dev/null
@@ -1,74 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-
-Components.utils.import("resource://gre/modules/commonjs/sdk/core/promise.js");
-
-let run_promise_tests = function run_promise_tests(tests, cb) {
-  let timer = Components.classes["@mozilla.org/timer;1"]
-     .createInstance(Components.interfaces.nsITimer);
-  let loop = function loop(index) {
-    if (index >= tests.length) {
-      if (cb) {
-        cb.call();
-      }
-      return;
-    }
-    do_print("Launching test " + (index + 1) + "/" + tests.length);
-    let test = tests[index];
-    // Execute from an empty stack
-    let next = function next() {
-      do_print("Test " + (index + 1) + "/" + tests.length + " complete");
-      do_execute_soon(function() {
-        loop(index + 1);
-      });
-    };
-    let result = test();
-    result.then(next, next);
-  };
-  return loop(0);
-};
-
-let make_promise_test = function(test) {
-  return function runtest() {
-    do_print("Test starting: " + test);
-    try {
-      let result = test();
-      if (result && "promise" in result) {
-        result = result.promise;
-      }
-      if (!result || !("then" in result)) {
-        let exn;
-        try {
-          do_throw("Test " + test + " did not return a promise: " + result);
-        } catch (x) {
-          exn = x;
-        }
-        return Promise.reject(exn);
-      }
-      // The test returns a promise
-      result = result.then(
-        // Test complete
-        function onResolve() {
-          do_print("Test complete: " + test);
-        },
-        // The test failed with an unexpected error
-        function onReject(err) {
-          let detail;
-          if (err && typeof err == "object" && "stack" in err) {
-            detail = err.stack;
-          } else {
-            detail = "(no stack)";
-          }
-          do_throw("Test " + test + " rejected with the following reason: "
-              + err + detail);
-      });
-      return result;
-    } catch (x) {
-      // The test failed because of an error outside of a promise
-      do_throw("Error in body of test " + test + ": " + x + " at " + x.stack);
-      return Promise.reject();
-    }
-  };
-};
-
deleted file mode 100644
--- a/addon-sdk/test/unit/xpcshell.ini
+++ /dev/null
@@ -1,5 +0,0 @@
-[DEFAULT]
-head = head.js
-tail = 
-
-[test_promise.js]
--- a/toolkit/modules/Makefile.in
+++ b/toolkit/modules/Makefile.in
@@ -21,16 +21,17 @@ EXTRA_JS_MODULES := \
   FileUtils.jsm \
   Geometry.jsm \
   InlineSpellChecker.jsm \
   NewTabUtils.jsm \
   PageMenu.jsm \
   PopupNotifications.jsm \
   Preferences.jsm \
   PrivateBrowsingUtils.jsm \
+  Promise.jsm \
   PropertyListUtils.jsm \
   RemoteWebProgress.jsm \
   Sqlite.jsm \
   Task.jsm \
   TelemetryTimestamps.jsm \
   Timer.jsm \
   $(NULL)
 
new file mode 100755
--- /dev/null
+++ b/toolkit/modules/Promise.jsm
@@ -0,0 +1,515 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
+/* 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+  "Promise"
+];
+
+/**
+ * This module implements the "promise" construct, according to the
+ * "Promises/A+" proposal as known in April 2013, documented here:
+ *
+ * <http://promises-aplus.github.com/promises-spec/>
+ *
+ * A promise is an object representing a value that may not be available yet.
+ * Internally, a promise can be in one of three states:
+ *
+ * - Pending, when the final value is not available yet.  This is the only state
+ *   that may transition to one of the other two states.
+ *
+ * - Resolved, when and if the final value becomes available.  A resolution
+ *   value becomes permanently associated with the promise.  This may be any
+ *   value, including "undefined".
+ *
+ * - Rejected, if an error prevented the final value from being determined.  A
+ *   rejection reason becomes permanently associated with the promise.  This may
+ *   be any value, including "undefined", though it is generally an Error
+ *   object, like in exception handling.
+ *
+ * A reference to an existing promise may be received by different means, for
+ * example as the return value of a call into an asynchronous API.  In this
+ * case, the state of the promise can be observed but not directly controlled.
+ *
+ * To observe the state of a promise, its "then" method must be used.  This
+ * method registers callback functions that are called as soon as the promise is
+ * either resolved or rejected.  The method returns a new promise, that in turn
+ * is resolved or rejected depending on the state of the original promise and on
+ * the behavior of the callbacks.  For example, unhandled exceptions in the
+ * callbacks cause the new promise to be rejected, even if the original promise
+ * is resolved.  See the documentation of the "then" method for details.
+ *
+ * Promises may also be created using the "Promise.defer" function, the main
+ * entry point of this module.  The function, along with the new promise,
+ * returns separate methods to change its state to be resolved or rejected.
+ * See the documentation of the "Deferred" prototype for details.
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Cu.import("resource://gre/modules/Promise.jsm");
+ *
+ * // This function creates and returns a new promise.
+ * function promiseValueAfterTimeout(aValue, aTimeout)
+ * {
+ *   let deferred = Promise.defer();
+ *
+ *   try {
+ *     // An asynchronous operation will trigger the resolution of the promise.
+ *     // In this example, we don't have a callback that triggers a rejection.
+ *     do_timeout(aTimeout, function () {
+ *       deferred.resolve(aValue);
+ *     });
+ *   } catch (ex) {
+ *     // Generally, functions returning promises propagate exceptions through
+ *     // the returned promise, though they may also choose to fail early.
+ *     deferred.reject(ex);
+ *   }
+ *
+ *   // We don't return the deferred to the caller, but only the contained
+ *   // promise, so that the caller cannot accidentally change its state.
+ *   return deferred.promise;
+ * }
+ *
+ * // This code uses the promise returned be the function above.
+ * let promise = promiseValueAfterTimeout("Value", 1000);
+ *
+ * let newPromise = promise.then(function onResolve(aValue) {
+ *   do_print("Resolved with this value: " + aValue);
+ * }, function onReject(aReason) {
+ *   do_print("Rejected with this reason: " + aReason);
+ * });
+ *
+ * // Unexpected errors should always be reported at the end of a promise chain.
+ * newPromise.then(null, Components.utils.reportError);
+ *
+ * -----------------------------------------------------------------------------
+ */
+
+////////////////////////////////////////////////////////////////////////////////
+//// Globals
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+const STATUS_PENDING = 0;
+const STATUS_RESOLVED = 1;
+const STATUS_REJECTED = 2;
+
+// These "private names" allow some properties of the Promise object to be
+// accessed only by this module, while still being visible on the object
+// manually when using a debugger.  They don't strictly guarantee that the
+// properties are inaccessible by other code, but provide enough protection to
+// avoid using them by mistake.
+const salt = Math.floor(Math.random() * 100);
+const Name = (n) => "{private:" + n + ":" + salt + "}";
+const N_STATUS = Name("status");
+const N_VALUE = Name("value");
+const N_HANDLERS = Name("handlers");
+
+////////////////////////////////////////////////////////////////////////////////
+//// Promise
+
+/**
+ * This object provides the public module functions.
+ */
+this.Promise = Object.freeze({
+  /**
+   * Creates a new pending promise and provides methods to resolve or reject it.
+   *
+   * @return A new object, containing the new promise in the "promise" property,
+   *         and the methods to change its state in the "resolve" and "reject"
+   *         properties.  See the Deferred documentation for details.
+   */
+  defer: function ()
+  {
+    return new Deferred();
+  },
+
+  /**
+   * Creates a new promise resolved with the specified value, or propagates the
+   * state of an existing promise.
+   *
+   * @param aValue
+   *        If this value is not a promise, including "undefined", it becomes
+   *        the resolution value of the returned promise.  If this value is a
+   *        promise, then the returned promise will eventually assume the same
+   *        state as the provided promise.
+   *
+   * @return A promise that can be pending, resolved, or rejected.
+   */
+  resolve: function (aValue)
+  {
+    let promise = new PromiseImpl();
+    PromiseWalker.completePromise(promise, STATUS_RESOLVED, aValue);
+    return promise;
+  },
+
+  /**
+   * Creates a new promise rejected with the specified reason.
+   *
+   * @param aReason
+   *        The rejection reason for the returned promise.  Although the reason
+   *        can be "undefined", it is generally an Error object, like in
+   *        exception handling.
+   *
+   * @return A rejected promise.
+   *
+   * @note The aReason argument should not be a promise.  Using a rejected
+   *       promise for the value of aReason would make the rejection reason
+   *       equal to the rejected promise itself, and not its rejection reason.
+   */
+  reject: function (aReason)
+  {
+    let promise = new PromiseImpl();
+    PromiseWalker.completePromise(promise, STATUS_REJECTED, aReason);
+    return promise;
+  },
+});
+
+////////////////////////////////////////////////////////////////////////////////
+//// PromiseWalker
+
+/**
+ * This singleton object invokes the handlers registered on resolved and
+ * rejected promises, ensuring that processing is not recursive and is done in
+ * the same order as registration occurred on each promise.
+ *
+ * There is no guarantee on the order of execution of handlers registered on
+ * different promises.
+ */
+this.PromiseWalker = {
+  /**
+   * Singleton array of all the unprocessed handlers currently registered on
+   * resolved or rejected promises.  Handlers are removed from the array as soon
+   * as they are processed.
+   */
+  handlers: [],
+
+  /**
+   * Called when a promise needs to change state to be resolved or rejected.
+   *
+   * @param aPromise
+   *        Promise that needs to change state.  If this is already resolved or
+   *        rejected, this method has no effect.
+   * @param aStatus
+   *        New desired status, either STATUS_RESOLVED or STATUS_REJECTED.
+   * @param aValue
+   *        Associated resolution value or rejection reason.
+   */
+  completePromise: function (aPromise, aStatus, aValue)
+  {
+    // Do nothing if the promise is already resolved or rejected.
+    if (aPromise[N_STATUS] != STATUS_PENDING) {
+      return;
+    }
+
+    // Resolving with another promise will cause this promise to eventually
+    // assume the state of the provided promise.
+    if (aStatus == STATUS_RESOLVED && aValue &&
+        typeof(aValue.then) == "function") {
+      aValue.then(this.completePromise.bind(this, aPromise, STATUS_RESOLVED),
+                  this.completePromise.bind(this, aPromise, STATUS_REJECTED));
+      return;
+    }
+
+    // Change the promise status and schedule our handlers for processing.
+    aPromise[N_STATUS] = aStatus;
+    aPromise[N_VALUE] = aValue;
+    if (aPromise[N_HANDLERS].length > 0) {
+      this.schedulePromise(aPromise);
+    }
+  },
+
+  /**
+   * Schedules the resolution or rejection handlers registered on the provided
+   * promise for processing.
+   *
+   * @param aPromise
+   *        Resolved or rejected promise whose handlers should be processed.  It
+   *        is expected that this promise has at least one handler to process.
+   */
+  schedulePromise: function (aPromise)
+  {
+    // Migrate the handlers from the provided promise to the global list.
+    for (let handler of aPromise[N_HANDLERS]) {
+      this.handlers.push(handler);
+    }
+    aPromise[N_HANDLERS].length = 0;
+
+    // Schedule the walker loop on the next tick of the event loop.
+    if (!this.walkerLoopScheduled) {
+      this.walkerLoopScheduled = true;
+      Services.tm.currentThread.dispatch(this.walkerLoop,
+                                         Ci.nsIThread.DISPATCH_NORMAL);
+    }
+  },
+
+  /**
+   * Indicates whether the walker loop is currently scheduled for execution on
+   * the next tick of the event loop.
+   */
+  walkerLoopScheduled: false,
+
+  /**
+   * Processes all the known handlers during this tick of the event loop.  This
+   * eager processing is done to avoid unnecessarily exiting and re-entering the
+   * JavaScript context for each handler on a resolved or rejected promise.
+   *
+   * This function is called with "this" bound to the PromiseWalker object.
+   */
+  walkerLoop: function ()
+  {
+    // Allow rescheduling the walker loop immediately.  This makes this walker
+    // resilient to the case where one handler does not return, but starts a
+    // nested event loop.  In that case, the newly scheduled walker will take
+    // over.  In the common case, the newly scheduled walker will be invoked
+    // after this one has returned, with no actual handler to process.  This
+    // small overhead is required to make nested event loops work correctly, but
+    // occurs at most once per resolution chain, thus having only a minor
+    // impact on overall performance.
+    this.walkerLoopScheduled = false;
+
+    // Process all the known handlers eagerly.
+    while (this.handlers.length > 0) {
+      this.handlers.shift().process();
+    }
+  },
+};
+
+// Bind the function to the singleton once.
+PromiseWalker.walkerLoop = PromiseWalker.walkerLoop.bind(PromiseWalker);
+
+////////////////////////////////////////////////////////////////////////////////
+//// Deferred
+
+/**
+ * Returned by "Promise.defer" to provide a new promise along with methods to
+ * change its state.
+ */
+function Deferred()
+{
+  this.promise = new PromiseImpl();
+  this.resolve = this.resolve.bind(this);
+  this.reject = this.reject.bind(this);
+
+  Object.freeze(this);
+}
+
+Deferred.prototype = {
+  /**
+   * A newly created promise, initially in the pending state.
+   */
+  promise: null,
+
+  /**
+   * Resolves the associated promise with the specified value, or propagates the
+   * state of an existing promise.  If the associated promise has already been
+   * resolved or rejected, this method does nothing.
+   *
+   * This function is bound to its associated promise when "Promise.defer" is
+   * called, and can be called with any value of "this".
+   *
+   * @param aValue
+   *        If this value is not a promise, including "undefined", it becomes
+   *        the resolution value of the associated promise.  If this value is a
+   *        promise, then the associated promise will eventually assume the same
+   *        state as the provided promise.
+   *
+   * @note Calling this method with a pending promise as the aValue argument,
+   *       and then calling it again with another value before the promise is
+   *       resolved or rejected, has unspecified behavior and should be avoided.
+   */
+  resolve: function (aValue) {
+    PromiseWalker.completePromise(this.promise, STATUS_RESOLVED, aValue);
+  },
+
+  /**
+   * Rejects the associated promise with the specified reason.  If the promise
+   * has already been resolved or rejected, this method does nothing.
+   *
+   * This function is bound to its associated promise when "Promise.defer" is
+   * called, and can be called with any value of "this".
+   *
+   * @param aReason
+   *        The rejection reason for the associated promise.  Although the
+   *        reason can be "undefined", it is generally an Error object, like in
+   *        exception handling.
+   *
+   * @note The aReason argument should not generally be a promise.  In fact,
+   *       using a rejected promise for the value of aReason would make the
+   *       rejection reason equal to the rejected promise itself, not to the
+   *       rejection reason of the rejected promise.
+   */
+  reject: function (aReason) {
+    PromiseWalker.completePromise(this.promise, STATUS_REJECTED, aReason);
+  },
+};
+
+////////////////////////////////////////////////////////////////////////////////
+//// PromiseImpl
+
+/**
+ * The promise object implementation.  This includes the public "then" method,
+ * as well as private state properties.
+ */
+function PromiseImpl()
+{
+  /*
+   * Internal status of the promise.  This can be equal to STATUS_PENDING,
+   * STATUS_RESOLVED, or STATUS_REJECTED.
+   */
+  Object.defineProperty(this, N_STATUS, { value: STATUS_PENDING,
+                                          writable: true });
+
+  /*
+   * When the N_STATUS property is STATUS_RESOLVED, this contains the final
+   * resolution value, that cannot be a promise, because resolving with a
+   * promise will cause its state to be eventually propagated instead.  When the
+   * N_STATUS property is STATUS_REJECTED, this contains the final rejection
+   * reason, that could be a promise, even if this is uncommon.
+   */
+  Object.defineProperty(this, N_VALUE, { writable: true });
+
+  /*
+   * Array of Handler objects registered by the "then" method, and not processed
+   * yet.  Handlers are removed when the promise is resolved or rejected.
+   */
+  Object.defineProperty(this, N_HANDLERS, { value: [] });
+
+  Object.seal(this);
+}
+
+PromiseImpl.prototype = {
+  /**
+   * Calls one of the provided functions as soon as this promise is either
+   * resolved or rejected.  A new promise is returned, whose state evolves
+   * depending on this promise and the provided callback functions.
+   *
+   * The appropriate callback is always invoked after this method returns, even
+   * if this promise is already resolved or rejected.  You can also call the
+   * "then" method multiple times on the same promise, and the callbacks will be
+   * invoked in the same order as they were registered.
+   *
+   * @param aOnResolve
+   *        If the promise is resolved, this function is invoked with the
+   *        resolution value of the promise as its only argument, and the
+   *        outcome of the function determines the state of the new promise
+   *        returned by the "then" method.  In case this parameter is not a
+   *        function (usually "null"), the new promise returned by the "then"
+   *        method is resolved with the same value as the original promise.
+   *
+   * @param aOnReject
+   *        If the promise is rejected, this function is invoked with the
+   *        rejection reason of the promise as its only argument, and the
+   *        outcome of the function determines the state of the new promise
+   *        returned by the "then" method.  In case this parameter is not a
+   *        function (usually left "undefined"), the new promise returned by the
+   *        "then" method is rejected with the same reason as the original
+   *        promise.
+   *
+   * @return A new promise that is initially pending, then assumes a state that
+   *         depends on the outcome of the invoked callback function:
+   *          - If the callback returns a value that is not a promise, including
+   *            "undefined", the new promise is resolved with this resolution
+   *            value, even if the original promise was rejected.
+   *          - If the callback throws an exception, the new promise is rejected
+   *            with the exception as the rejection reason, even if the original
+   *            promise was resolved.
+   *          - If the callback returns a promise, the new promise will
+   *            eventually assume the same state as the returned promise.
+   *
+   * @note If the aOnResolve callback throws an exception, the aOnReject
+   *       callback is not invoked.  You can register a rejection callback on
+   *       the returned promise instead, to process any exception occurred in
+   *       either of the callbacks registered on this promise.
+   */
+  then: function (aOnResolve, aOnReject)
+  {
+    let handler = new Handler(this, aOnResolve, aOnReject);
+    this[N_HANDLERS].push(handler);
+
+    // Ensure the handler is scheduled for processing if this promise is already
+    // resolved or rejected.
+    if (this[N_STATUS] != STATUS_PENDING) {
+      PromiseWalker.schedulePromise(this);
+    }
+
+    return handler.nextPromise;
+  },
+};
+
+////////////////////////////////////////////////////////////////////////////////
+//// Handler
+
+/**
+ * Handler registered on a promise by the "then" function.
+ */
+function Handler(aThisPromise, aOnResolve, aOnReject)
+{
+  this.thisPromise = aThisPromise;
+  this.onResolve = aOnResolve;
+  this.onReject = aOnReject;
+  this.nextPromise = new PromiseImpl();
+}
+
+Handler.prototype = {
+  /**
+   * Promise on which the "then" method was called.
+   */
+  thisPromise: null,
+
+  /**
+   * Unmodified resolution handler provided to the "then" method.
+   */
+  onResolve: null,
+
+  /**
+   * Unmodified rejection handler provided to the "then" method.
+   */
+  onReject: null,
+
+  /**
+   * New promise that will be returned by the "then" method.
+   */
+  nextPromise: null,
+
+  /**
+   * Called after thisPromise is resolved or rejected, invokes the appropriate
+   * callback and propagates the result to nextPromise.
+   */
+  process: function()
+  {
+    // The state of this promise is propagated unless a handler is defined.
+    let nextStatus = this.thisPromise[N_STATUS];
+    let nextValue = this.thisPromise[N_VALUE];
+
+    try {
+      // If a handler is defined for either resolution or rejection, invoke it
+      // to determine the state of the next promise, that will be resolved with
+      // the returned value, that can also be another promise.
+      if (nextStatus == STATUS_RESOLVED) {
+        if (typeof(this.onResolve) == "function") {
+          nextValue = this.onResolve(nextValue);
+        }
+      } else if (typeof(this.onReject) == "function") {
+        nextValue = this.onReject(nextValue);
+        nextStatus = STATUS_RESOLVED;
+      }
+    } catch (ex) {
+      // If an exception occurred in the handler, reject the next promise.
+      nextStatus = STATUS_REJECTED;
+      nextValue = ex;
+    }
+
+    // Propagate the newly determined state to the next promise.
+    PromiseWalker.completePromise(this.nextPromise, nextStatus, nextValue);
+  },
+};
rename from addon-sdk/test/unit/test_promise.js
rename to toolkit/modules/tests/xpcshell/test_Promise.js
--- a/addon-sdk/test/unit/test_promise.js
+++ b/toolkit/modules/tests/xpcshell/test_Promise.js
@@ -1,12 +1,88 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
+Components.utils.import("resource://gre/modules/Promise.jsm");
+
+////////////////////////////////////////////////////////////////////////////////
+//// Test runner
+
+let run_promise_tests = function run_promise_tests(tests, cb) {
+  let timer = Components.classes["@mozilla.org/timer;1"]
+     .createInstance(Components.interfaces.nsITimer);
+  let loop = function loop(index) {
+    if (index >= tests.length) {
+      if (cb) {
+        cb.call();
+      }
+      return;
+    }
+    do_print("Launching test " + (index + 1) + "/" + tests.length);
+    let test = tests[index];
+    // Execute from an empty stack
+    let next = function next() {
+      do_print("Test " + (index + 1) + "/" + tests.length + " complete");
+      do_execute_soon(function() {
+        loop(index + 1);
+      });
+    };
+    let result = test();
+    result.then(next, next);
+  };
+  return loop(0);
+};
+
+let make_promise_test = function(test) {
+  return function runtest() {
+    do_print("Test starting: " + test);
+    try {
+      let result = test();
+      if (result && "promise" in result) {
+        result = result.promise;
+      }
+      if (!result || !("then" in result)) {
+        let exn;
+        try {
+          do_throw("Test " + test + " did not return a promise: " + result);
+        } catch (x) {
+          exn = x;
+        }
+        return Promise.reject(exn);
+      }
+      // The test returns a promise
+      result = result.then(
+        // Test complete
+        function onResolve() {
+          do_print("Test complete: " + test);
+        },
+        // The test failed with an unexpected error
+        function onReject(err) {
+          let detail;
+          if (err && typeof err == "object" && "stack" in err) {
+            detail = err.stack;
+          } else {
+            detail = "(no stack)";
+          }
+          do_throw("Test " + test + " rejected with the following reason: "
+              + err + detail);
+      });
+      return result;
+    } catch (x) {
+      // The test failed because of an error outside of a promise
+      do_throw("Error in body of test " + test + ": " + x + " at " + x.stack);
+      return Promise.reject();
+    }
+  };
+};
+
+////////////////////////////////////////////////////////////////////////////////
+//// Tests
+
 let tests = [];
 
 // Utility function to observe an failures in a promise
 // This function is useful if the promise itself is
 // not returned.
 let observe_failures = function observe_failures(promise) {
   promise.then(null, function onReject(reason) {
     test.do_throw("Observed failure in test " + test + ": " + reason);
@@ -54,16 +130,79 @@ tests.push(make_promise_test(
     // Install remaining observers
     for(;i < SIZE; ++i) {
       install_observer(i);
     }
 
     return result;
   }));
 
+// Test that observers registered on a pending promise are notified in order.
+tests.push(
+  make_promise_test(function then_returns_before_callbacks(test) {
+    let deferred = Promise.defer();
+    let promise = deferred.promise;
+
+    let order = 0;
+
+    promise.then(
+      function onResolve() {
+        do_check_eq(order, 0);
+        order++;
+      }
+    );
+
+    promise.then(
+      function onResolve() {
+        do_check_eq(order, 1);
+        order++;
+      }
+    );
+
+    let newPromise = promise.then(
+      function onResolve() {
+        do_check_eq(order, 2);
+      }
+    );
+
+    deferred.resolve();
+
+    // This test finishes after the last handler succeeds.
+    return newPromise;
+  }));
+
+// Test that observers registered on a resolved promise are notified in order.
+tests.push(
+  make_promise_test(function then_returns_before_callbacks(test) {
+    let promise = Promise.resolve();
+
+    let order = 0;
+
+    promise.then(
+      function onResolve() {
+        do_check_eq(order, 0);
+        order++;
+      }
+    );
+
+    promise.then(
+      function onResolve() {
+        do_check_eq(order, 1);
+        order++;
+      }
+    );
+
+    // This test finishes after the last handler succeeds.
+    return promise.then(
+      function onResolve() {
+        do_check_eq(order, 2);
+      }
+    );
+  }));
+
 // Test that all observers are notified at most once, even if source
 // is resolved/rejected several times
 tests.push(make_promise_test(
   function notification_once(test) {
     // The size of the test
     const SIZE = 10;
     const RESULT = "this is an arbitrary value";
 
@@ -384,13 +523,70 @@ tests.push(
     let promise = Promise.resolve(RESULT).then(
       function onResolve(result) {
         do_check_eq(result, RESULT, "Promise.resolve propagated the correct result");
       }
     );
     return promise;
   }));
 
+// Test that the code after "then" is always executed before the callbacks
+tests.push(
+  make_promise_test(function then_returns_before_callbacks(test) {
+    let promise = Promise.resolve();
+
+    let thenExecuted = false;
+
+    promise = promise.then(
+      function onResolve() {
+        thenExecuted = true;
+      }
+    );
+
+    do_check_false(thenExecuted);
+
+    return promise;
+  }));
+
+// Test that chaining promises does not generate long stack traces
+tests.push(
+  make_promise_test(function chaining_short_stack(test) {
+    let source = Promise.defer();
+    let promise = source.promise;
+
+    const NUM_ITERATIONS = 100;
+
+    for (let i = 0; i < NUM_ITERATIONS; i++) {
+      promise = promise.then(
+        function onResolve(result) {
+          return result + ".";
+        }
+      );
+    }
+
+    promise = promise.then(
+      function onResolve(result) {
+        // Check that the execution went as expected.
+        let expectedString = new Array(1 + NUM_ITERATIONS).join(".");
+        do_check_true(result == expectedString);
+
+        // Check that we didn't generate one or more stack frames per iteration.
+        let stackFrameCount = 0;
+        let stackFrame = Components.stack;
+        while (stackFrame) {
+          stackFrameCount++;
+          stackFrame = stackFrame.caller;
+        }
+
+        do_check_true(stackFrameCount < NUM_ITERATIONS);
+      }
+    );
+
+    source.resolve("");
+
+    return promise;
+  }));
+
 function run_test()
 {
   do_test_pending();
   run_promise_tests(tests, do_test_finished);
 }
--- a/toolkit/modules/tests/xpcshell/xpcshell.ini
+++ b/toolkit/modules/tests/xpcshell/xpcshell.ini
@@ -1,14 +1,15 @@
 [DEFAULT]
 head =
 tail =
 
 [test_dict.js]
 [test_FileUtils.js]
 [test_newtab-migrate-v1.js]
 [test_Preferences.js]
+[test_Promise.js]
 [test_propertyListsUtils.js]
 [test_readCertPrefs.js]
 [test_sqlite.js]
 [test_task.js]
 [test_TelemetryTimestamps.js]
 [test_timer.js]