/* 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");
Components.utils.import("resource://gre/modules/Services.jsm");
Components.utils.import("resource://gre/modules/Task.jsm");
Components.utils.import("resource://testing-common/PromiseTestUtils.jsm");

// Prevent test failures due to the unhandled rejections in this test file.
PromiseTestUtils.disableUncaughtRejectionObserverForSelfTest();

// Test runner

var run_promise_tests = function run_promise_tests(tests, cb) {
  let loop = function loop(index) {
    if (index >= tests.length) {
      if (cb) {
        cb.call();
      }
      return;
    }
    info("Launching test " + (index + 1) + "/" + tests.length);
    let test = tests[index];
    // Execute from an empty stack
    let next = function next() {
      info("Test " + (index + 1) + "/" + tests.length + " complete");
      executeSoon(function() {
        loop(index + 1);
      });
    };
    let result = test();
    result.then(next, next);
  };
  return loop(0);
};

var make_promise_test = function(test) {
  return function runtest() {
    info("Test starting: " + test.name);
    try {
      let result = test();
      if (result && "promise" in result) {
        result = result.promise;
      }
      if (!result || !("then" in result)) {
        let exn;
        try {
          do_throw("Test " + test.name + " 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() {
          info("Test complete: " + test.name);
        },
        // 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.name + " 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.name + ": " + x + " at " + x.stack);
      return Promise.reject();
    }
  };
};

// Tests

var tests = [];

// Utility function to observe an failures in a promise
// This function is useful if the promise itself is
// not returned.
var observe_failures = function observe_failures(promise) {
  promise.catch(function onReject(reason) {
    do_throw("Observed failure in test: " + reason);
  });
};

// Test that all observers are notified
tests.push(make_promise_test(
  function notification(test) {
    // The size of the test
    const SIZE = 10;
    const RESULT = "this is an arbitrary value";

    // Number of observers that yet need to be notified
    let expected = SIZE;

    // |true| once an observer has been notified
    let notified = [];

    // The promise observed
    let source = Promise.defer();
    let result = Promise.defer();

    let install_observer = function install_observer(i) {
      observe_failures(source.promise.then(
        function onSuccess(value) {
          Assert.ok(!notified[i], "Ensuring that observer is notified at most once");
          notified[i] = true;

          Assert.equal(value, RESULT, "Ensuring that the observed value is correct");
          if (--expected == 0) {
            result.resolve();
          }
        }));
    };

    // Install a number of observers before resolving
    let i;
    for (i = 0; i < SIZE / 2; ++i) {
      install_observer(i);
    }

    source.resolve(RESULT);

    // Install remaining observers
    for (;i < SIZE; ++i) {
      install_observer(i);
    }

    return result;
  }));

// Test that observers get the correct "this" value in strict mode.
tests.push(
  make_promise_test(function handlers_this_value(test) {
    return Promise.resolve().then(
      function onResolve() {
        // Since this file is in strict mode, the correct value is "undefined".
        Assert.equal(this, undefined);
        throw "reject";
      }
    ).then(
      null,
      function onReject() {
        // Since this file is in strict mode, the correct value is "undefined".
        Assert.equal(this, undefined);
      }
    );
  }));

// 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() {
        Assert.equal(order, 0);
        order++;
      }
    );

    promise.then(
      function onResolve() {
        Assert.equal(order, 1);
        order++;
      }
    );

    let newPromise = promise.then(
      function onResolve() {
        Assert.equal(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() {
        Assert.equal(order, 0);
        order++;
      }
    );

    promise.then(
      function onResolve() {
        Assert.equal(order, 1);
        order++;
      }
    );

    // This test finishes after the last handler succeeds.
    return promise.then(
      function onResolve() {
        Assert.equal(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";

    // Number of observers that yet need to be notified
    let expected = SIZE;

    // |true| once an observer has been notified
    let notified = [];

    // The promise observed
    let observed = Promise.defer();
    let result = Promise.defer();

    let install_observer = function install_observer(i) {
      observe_failures(observed.promise.then(
        function onSuccess(value) {
          Assert.ok(!notified[i], "Ensuring that observer is notified at most once");
          notified[i] = true;

          Assert.equal(value, RESULT, "Ensuring that the observed value is correct");
          if (--expected == 0) {
            result.resolve();
          }
        }));
    };

    // Install a number of observers before resolving
    let i;
    for (i = 0; i < SIZE / 2; ++i) {
      install_observer(i);
    }

    observed.resolve(RESULT);

    // Install remaining observers
    for (;i < SIZE; ++i) {
      install_observer(i);
    }

    // Resolve some more
    for (i = 0; i < 10; ++i) {
      observed.resolve(RESULT);
      observed.reject();
    }

    return result;
  }));

// Test that throwing an exception from a onResolve listener
// does not prevent other observers from receiving the notification
// of success.
tests.push(
  make_promise_test(function exceptions_do_not_stop_notifications(test) {
    let source = Promise.defer();

    let exception_thrown = false;
    let exception_content = new Error("Boom!");

    let observer_1 = source.promise.then(
      function onResolve() {
        exception_thrown = true;
        throw exception_content;
      });

    source.promise.then(
      function onResolve() {
        Assert.ok(exception_thrown, "Second observer called after first observer has thrown");
      }
    );

    let result = observer_1.then(
      function onResolve() {
        do_throw("observer_1 should not have resolved");
      },
      function onReject(reason) {
        Assert.ok(reason == exception_content, "Obtained correct rejection");
      }
    );

    source.resolve();
    return result;
  }
));

// Test that, once a promise is resolved, further resolve/reject
// are ignored.
tests.push(
  make_promise_test(function subsequent_resolves_are_ignored(test) {
    let deferred = Promise.defer();
    deferred.resolve(1);
    deferred.resolve(2);
    deferred.reject(3);

    let result = deferred.promise.then(
      function onResolve(value) {
        Assert.equal(value, 1, "Resolution chose the first value");
      },
      function onReject(reason) {
        do_throw("Obtained a rejection while the promise was already resolved");
      }
    );

    return result;
  }));

// Test that, once a promise is rejected, further resolve/reject
// are ignored.
tests.push(
  make_promise_test(function subsequent_rejects_are_ignored(test) {
    let deferred = Promise.defer();
    deferred.reject(1);
    deferred.reject(2);
    deferred.resolve(3);

    let result = deferred.promise.then(
      function onResolve() {
        do_throw("Obtained a resolution while the promise was already rejected");
      },
      function onReject(reason) {
        Assert.equal(reason, 1, "Rejection chose the first value");
      }
    );

    return result;
  }));

// Test that returning normally from a rejection recovers from the error
// and that listeners are informed of a success.
tests.push(
  make_promise_test(function recovery(test) {
    let boom = new Error("Boom!");
    let deferred = Promise.defer();
    const RESULT = "An arbitrary value";

    let promise = deferred.promise.then(
      function onResolve() {
        do_throw("A rejected promise should not resolve");
      },
      function onReject(reason) {
        Assert.ok(reason == boom, "Promise was rejected with the correct error");
        return RESULT;
      }
    );

    promise = promise.then(
      function onResolve(value) {
        Assert.equal(value, RESULT, "Promise was recovered with the correct value");
      }
    );

    deferred.reject(boom);
    return promise;
  }));

// Test that returning a resolved promise from a onReject causes a resolution
// (recovering from the error) and that returning a rejected promise
// from a onResolve listener causes a rejection (raising an error).
tests.push(
  make_promise_test(function recovery_with_promise(test) {
    let boom = new Error("Arbitrary error");
    let deferred = Promise.defer();
    const RESULT = "An arbitrary value";
    const boom2 = new Error("Another arbitrary error");

    // return a resolved promise from a onReject listener
    let promise = deferred.promise.then(
      function onResolve() {
        do_throw("A rejected promise should not resolve");
      },
      function onReject(reason) {
        Assert.ok(reason == boom, "Promise was rejected with the correct error");
        return Promise.resolve(RESULT);
      }
    );

    // return a rejected promise from a onResolve listener
    promise = promise.then(
      function onResolve(value) {
        Assert.equal(value, RESULT, "Promise was recovered with the correct value");
        return Promise.reject(boom2);
      }
    );

    promise = promise.catch(
      function onReject(reason) {
        Assert.equal(reason, boom2, "Rejection was propagated with the correct " +
                "reason, through a promise");
      }
    );

    deferred.reject(boom);
    return promise;
  }));

// Test that we can resolve with promises of promises
tests.push(
  make_promise_test(function test_propagation(test) {
    const RESULT = "Yet another arbitrary value";
    let d1 = Promise.defer();
    let d2 = Promise.defer();
    let d3 = Promise.defer();

    d3.resolve(d2.promise);
    d2.resolve(d1.promise);
    d1.resolve(RESULT);

    return d3.promise.then(
      function onSuccess(value) {
        Assert.equal(value, RESULT, "Resolution with a promise eventually yielded "
                + " the correct result");
      }
    );
  }));

// Test sequences of |then| and |catch|
tests.push(
  make_promise_test(function test_chaining(test) {
    let error_1 = new Error("Error 1");
    let error_2 = new Error("Error 2");
    let result_1 = "First result";
    let result_2 = "Second result";
    let result_3 = "Third result";

    let source = Promise.defer();

    let promise = source.promise.then().then();

    source.resolve(result_1);

    // Check that result_1 is correctly propagated
    promise = promise.then(
      function onSuccess(result) {
        Assert.equal(result, result_1, "Result was propagated correctly through " +
                " several applications of |then|");
        return result_2;
      }
    );

    // Check that returning from the promise produces a resolution
    promise = promise.catch(
      function onReject() {
        do_throw("Incorrect rejection");
      }
    );

    // ... and that the check did not alter the value
    promise = promise.then(
      function onResolve(value) {
        Assert.equal(value, result_2, "Result was propagated correctly once again");
      }
    );

    // Now the same kind of tests for rejections
    promise = promise.then(
      function onResolve() {
        throw error_1;
      }
    );

    promise = promise.then(
      function onResolve() {
        do_throw("Incorrect resolution: the exception should have caused a rejection");
      }
    );

    promise = promise.catch(
      function onReject(reason) {
        Assert.ok(reason == error_1, "Reason was propagated correctly");
        throw error_2;
      }
    );

    promise = promise.catch(
      function onReject(reason) {
        Assert.ok(reason == error_2, "Throwing an error altered the reason " +
            "as expected");
        return result_3;
      }
    );

    promise = promise.then(
      function onResolve(result) {
        Assert.equal(result, result_3, "Error was correctly recovered");
      }
    );

    return promise;
  }));

// Test that resolving with a rejected promise actually rejects
tests.push(
  make_promise_test(function resolve_to_rejected(test) {
    let source = Promise.defer();
    let error = new Error("Boom");

    let promise = source.promise.then(
      function onResolve() {
        do_throw("Incorrect call to onResolve listener");
      },
      function onReject(reason) {
        Assert.equal(reason, error, "Rejection lead to the expected reason");
      }
    );

    source.resolve(Promise.reject(error));

    return promise;
  }));

// Test that Promise.resolve resolves as expected
tests.push(
  make_promise_test(function test_resolve(test) {
    const RESULT = "arbitrary value";
    let p1 = Promise.resolve(RESULT);
    let p2 = Promise.resolve(p1);
    Assert.equal(p1, p2, "Promise.resolve used on a promise just returns the promise");

    return p1.then(
      function onResolve(result) {
        Assert.equal(result, RESULT, "Promise.resolve propagated the correct result");
      }
    );
  }));

// Test that Promise.resolve throws when its argument is an async function.
tests.push(
  make_promise_test(function test_promise_resolve_throws_with_async_function(test) {
    Assert.throws(() => Promise.resolve(Task.async(function* () {})), // eslint-disable-line mozilla/no-task
                  /Cannot resolve a promise with an async function/);
    return Promise.resolve();
  }));

// 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;
      }
    );

    Assert.ok(!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(".");
        Assert.ok(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;
        }

        Assert.ok(stackFrameCount < NUM_ITERATIONS);
      }
    );

    source.resolve("");

    return promise;
  }));

// Test that the values of the promise return by Promise.all() are kept in the
// given order even if the given promises are resolved in arbitrary order
tests.push(
  make_promise_test(function all_resolve(test) {
    let d1 = Promise.defer();
    let d2 = Promise.defer();
    let d3 = Promise.defer();

    d3.resolve(4);
    d2.resolve(2);
    executeSoon(() => d1.resolve(1));

    let promises = [d1.promise, d2.promise, 3, d3.promise];

    return Promise.all(promises).then(
      function onResolve([val1, val2, val3, val4]) {
        Assert.equal(val1, 1);
        Assert.equal(val2, 2);
        Assert.equal(val3, 3);
        Assert.equal(val4, 4);
      }
    );
  }));

// Test that rejecting one of the promises passed to Promise.all()
// rejects the promise return by Promise.all()
tests.push(
  make_promise_test(function all_reject(test) {
    let error = new Error("Boom");

    let d1 = Promise.defer();
    let d2 = Promise.defer();
    let d3 = Promise.defer();

    d3.resolve(3);
    d2.resolve(2);
    executeSoon(() => d1.reject(error));

    let promises = [d1.promise, d2.promise, d3.promise];

    return Promise.all(promises).then(
      function onResolve() {
        do_throw("Incorrect call to onResolve listener");
      },
      function onReject(reason) {
        Assert.equal(reason, error, "Rejection lead to the expected reason");
      }
    );
  }));

// Test that passing only values (not promises) to Promise.all()
// forwards them all as resolution values.
tests.push(
  make_promise_test(function all_resolve_no_promises(test) {
    try {
      Promise.all(null);
      Assert.ok(false, "all() should only accept iterables");
    } catch (e) {
      Assert.ok(true, "all() fails when first the arg is not an iterable");
    }

    let p1 = Promise.all([]).then(
      function onResolve(val) {
        Assert.ok(Array.isArray(val) && val.length == 0);
      }
    );

    let p2 = Promise.all([1, 2, 3]).then(
      function onResolve([val1, val2, val3]) {
        Assert.equal(val1, 1);
        Assert.equal(val2, 2);
        Assert.equal(val3, 3);
      }
    );

    return Promise.all([p1, p2]);
  }));

// Test that Promise.all() handles non-array iterables
tests.push(
  make_promise_test(function all_iterable(test) {
    function* iterable() {
      yield 1;
      yield 2;
      yield 3;
    }

    return Promise.all(iterable()).then(
      function onResolve([val1, val2, val3]) {
        Assert.equal(val1, 1);
        Assert.equal(val2, 2);
        Assert.equal(val3, 3);
      },
      function onReject() {
        do_throw("all() unexpectedly rejected");
      }
    );
  }));

// Test that throwing from the iterable passed to Promise.all() rejects the
// promise returned by Promise.all()
tests.push(
  make_promise_test(function all_iterable_throws(test) {
    function* iterable() {
      throw 1;
    }

    return Promise.all(iterable()).then(
      function onResolve() {
        do_throw("all() unexpectedly resolved");
      },
      function onReject(reason) {
        Assert.equal(reason, 1, "all() rejects when the iterator throws");
      }
    );
  }));

// Test that Promise.race() resolves with the first available resolution value
tests.push(
  make_promise_test(function race_resolve(test) {
    let p1 = Promise.resolve(1);
    let p2 = Promise.resolve().then(() => 2);

    return Promise.race([p1, p2]).then(
      function onResolve(value) {
        Assert.equal(value, 1);
      }
    );
  }));

// Test that passing only values (not promises) to Promise.race() works
tests.push(
  make_promise_test(function race_resolve_no_promises(test) {
    try {
      Promise.race(null);
      Assert.ok(false, "race() should only accept iterables");
    } catch (e) {
      Assert.ok(true, "race() fails when first the arg is not an iterable");
    }

    return Promise.race([1, 2, 3]).then(
      function onResolve(value) {
        Assert.equal(value, 1);
      }
    );
  }));

// Test that Promise.race() never resolves when passed an empty iterable
tests.push(
  make_promise_test(function race_resolve_never(test) {
    return new Promise(resolve => {
      Promise.race([]).then(
        function onResolve() {
          do_throw("race() unexpectedly resolved");
        },
        function onReject() {
          do_throw("race() unexpectedly rejected");
        }
      );

      // Approximate "never" so we don't have to solve the halting problem.
      do_timeout(200, resolve);
    });
  }));

// Test that Promise.race() handles non-array iterables.
tests.push(
  make_promise_test(function race_iterable(test) {
    function* iterable() {
      yield 1;
      yield 2;
      yield 3;
    }

    return Promise.race(iterable()).then(
      function onResolve(value) {
        Assert.equal(value, 1);
      },
      function onReject() {
        do_throw("race() unexpectedly rejected");
      }
    );
  }));

// Test that throwing from the iterable passed to Promise.race() rejects the
// promise returned by Promise.race()
tests.push(
  make_promise_test(function race_iterable_throws(test) {
    function* iterable() {
      throw 1;
    }

    return Promise.race(iterable()).then(
      function onResolve() {
        do_throw("race() unexpectedly resolved");
      },
      function onReject(reason) {
        Assert.equal(reason, 1, "race() rejects when the iterator throws");
      }
    );
  }));

// Test that rejecting one of the promises passed to Promise.race() rejects the
// promise returned by Promise.race()
tests.push(
  make_promise_test(function race_reject(test) {
    let p1 = Promise.reject(1);
    let p2 = Promise.resolve(2);
    let p3 = Promise.resolve(3);

    return Promise.race([p1, p2, p3]).then(
      function onResolve() {
        do_throw("race() unexpectedly resolved");
      },
      function onReject(reason) {
        Assert.equal(reason, 1, "race() rejects when given a rejected promise");
      }
    );
  }));

// Test behavior of the Promise constructor.
tests.push(
  make_promise_test(function test_constructor(test) {
    try {
      new Promise(null);
      Assert.ok(false, "Constructor should fail when not passed a function");
    } catch (e) {
      Assert.ok(true, "Constructor fails when not passed a function");
    }

    let executorRan = false;
    let promise = new Promise(
      function executor(resolve, reject) {
        executorRan = true;
        Assert.equal(this, undefined);
        Assert.equal(typeof resolve, "function",
                     "resolve function should be passed to the executor");
        Assert.equal(typeof reject, "function",
                     "reject function should be passed to the executor");
      }
    );
    do_check_instanceof(promise, Promise);
    Assert.ok(executorRan, "Executor should execute synchronously");

    // resolve a promise from the executor
    let resolvePromise = new Promise(
      function executor(resolve) {
        resolve(1);
      }
    ).then(
      function onResolve(value) {
        Assert.equal(value, 1, "Executor resolved with correct value");
      },
      function onReject() {
        do_throw("Executor unexpectedly rejected");
      }
    );

    // reject a promise from the executor
    let rejectPromise = new Promise(
      function executor(_, reject) {
        reject(1);
      }
    ).then(
      function onResolve() {
        do_throw("Executor unexpectedly resolved");
      },
      function onReject(reason) {
        Assert.equal(reason, 1, "Executor rejected with correct value");
      }
    );

    // throw from the executor, causing a rejection
    let throwPromise = new Promise(
      function executor() {
        throw 1;
      }
    ).then(
      function onResolve() {
        do_throw("Throwing inside an executor should not resolve the promise");
      },
      function onReject(reason) {
        Assert.equal(reason, 1, "Executor rejected with correct value");
      }
    );

    return Promise.all([resolvePromise, rejectPromise, throwPromise]);
  }));

// Test deadlock in Promise.jsm with nested event loops
// The scenario being tested is:
// promise_1.then({
//   do some work that will asynchronously signal done
//   start an event loop waiting for the done signal
// }
// where the async work uses resolution of a second promise to
// trigger the "done" signal. While this would likely work in a
// naive implementation, our constant-stack implementation needs
// a special case to avoid deadlock. Note that this test is
// sensitive to the implementation-dependent order in which then()
// clauses for two different promises are executed, so it is
// possible for other implementations to pass this test and still
// have similar deadlocks.
tests.push(
  make_promise_test(function promise_nested_eventloop_deadlock(test) {
    // Set up a (long enough to be noticeable) timeout to
    // exit the nested event loop and throw if the test run is hung
    let shouldExitNestedEventLoop = false;

    function event_loop() {
      Services.tm.spinEventLoopUntil(() => shouldExitNestedEventLoop);
    }

    // I wish there was a way to cancel xpcshell do_timeout()s
    do_timeout(2000, () => {
      if (!shouldExitNestedEventLoop) {
        shouldExitNestedEventLoop = true;
        do_throw("Test timed out");
      }
    });

    let promise1 = Promise.resolve(1);
    let promise2 = Promise.resolve(2);

    info("Setting wait for first promise");
    promise1.then(value => {
      info("Starting event loop");
      event_loop();
    }, null);

    info("Setting wait for second promise");
    return promise2.catch(error => { return 3; })
    .then(
      count => {
        shouldExitNestedEventLoop = true;
      });
  }));

function wait_for_uncaught(aMustAppear, aTimeout = undefined) {
  let remaining = new Set();
  for (let k of aMustAppear) {
    remaining.add(k);
  }
  let deferred = Promise.defer();
  let print = info;
  let observer = function({message, stack}) {
    let data = message + stack;
    print("Observing " + message + ", looking for " + aMustAppear.join(", "));
    for (let expected of remaining) {
      if (data.indexOf(expected) != -1) {
        print("I found " + expected);
        remaining.delete(expected);
      }
      if (remaining.size == 0 && observer) {
        Promise.Debugging.removeUncaughtErrorObserver(observer);
        observer = null;
        deferred.resolve();
      }
    }
  };
  Promise.Debugging.addUncaughtErrorObserver(observer);
  if (aTimeout) {
    do_timeout(aTimeout, function timeout() {
      if (observer) {
        Promise.Debugging.removeUncaughtErrorObserver(observer);
        observer = null;
      }
      deferred.reject(new Error("Timeout"));
    });
  }
  return deferred.promise;
}

// Test that uncaught errors are reported as uncaught
(function() {
  let make_string_rejection = function make_string_rejection() {
    let salt = (Math.random() * ( Math.pow(2, 24) - 1 ));
    let string = "This is an uncaught rejection " + salt;
    // Our error is not Error-like nor an nsIException, so the stack will
    // include the closure doing the actual rejection.
    return {mustFind: ["test_rejection_closure", string], error: string};
  };
  let make_num_rejection = function make_num_rejection() {
    let salt = (Math.random() * ( Math.pow(2, 24) - 1 ));
    // Our error is not Error-like nor an nsIException, so the stack will
    // include the closure doing the actual rejection.
    return {mustFind: ["test_rejection_closure", salt], error: salt};
  };
  let make_undefined_rejection = function make_undefined_rejection() {
    // Our error is not Error-like nor an nsIException, so the stack will
    // include the closure doing the actual rejection.
    return {mustFind: ["test_rejection_closure"], error: undefined};
  };
  let make_error_rejection = function make_error_rejection() {
    let salt = (Math.random() * ( Math.pow(2, 24) - 1 ));
    let error = new Error("This is an uncaught error " + salt);
    return {
      mustFind: [error.message, error.fileName, error.lineNumber, error.stack],
      error
    };
  };
  let make_exception_rejection = function make_exception_rejection() {
    let salt = (Math.random() * ( Math.pow(2, 24) - 1 ));
    let exn = new Components.Exception("This is an uncaught exception " + salt,
                                       Components.results.NS_ERROR_NOT_AVAILABLE);
    return {
      mustFind: [exn.message, exn.filename, exn.lineNumber, exn.location.toString()],
      error: exn
    };
  };
  for (let make_rejection of [make_string_rejection,
    make_num_rejection,
    make_undefined_rejection,
    make_error_rejection,
    make_exception_rejection]) {
      let {mustFind, error} = make_rejection();
      let name = make_rejection.name;
      tests.push(make_promise_test(function test_uncaught_is_reported() {
        info("Testing with rejection " + name);
        let promise = wait_for_uncaught(mustFind);
        (function test_rejection_closure() {
          // For the moment, we cannot be absolutely certain that a value is
          // garbage-collected, even if it is not referenced anymore, due to
          // the conservative stack-scanning algorithm.
          //
          // To be _almost_ certain that a value will be garbage-collected, we
          // 1. isolate that value in an anonymous closure;
          // 2. allocate 100 values instead of 1 (gc-ing a single value from
          //    these is sufficient for the test);
          // 3. place everything in a loop, as the JIT typically reuses memory;
          // 4. call all the GC methods we can.
          //
          // Unfortunately, we might still have intermittent failures,
          // materialized as timeouts.
          //
          for (let i = 0; i < 100; ++i) {
            Promise.reject(error);
          }
        })();
        info("Posted all rejections");
        Components.utils.forceGC();
        Components.utils.forceCC();
        Components.utils.forceShrinkingGC();
        return promise;
      }));
  }
})();


// Test that caught errors are not reported as uncaught
tests.push(
make_promise_test(function test_caught_is_not_reported() {
  let salt = (Math.random() * ( Math.pow(2, 24) - 1 ));
  let promise = wait_for_uncaught([salt], 500);
  (function() {
    let uncaught = Promise.reject("This error, on the other hand, is caught " + salt);
    uncaught.catch(function() { /* ignore rejection */ });
    uncaught = null;
  })();
  // Isolate this in a function to increase likelihood that the gc will
  // realise that |uncaught| has remained uncaught.
  Components.utils.forceGC();

  return promise.then(function onSuccess() {
    throw new Error("This error was caught and should not have been reported");
  }, function onError() {
    info("The caught error was not reported, all is fine");
  }
  );
}));

// Bug 1033406 - Make sure Promise works even after freezing.
tests.push(
  make_promise_test(function test_freezing_promise(test) {
    var p = new Promise(function executor(resolve) {
      executeSoon(resolve);
    });
    Object.freeze(p);
    return p;
  })
);

function run_test() {
  do_test_pending();
  run_promise_tests(tests, do_test_finished);
}
