Bug 989960 - Unhandled rejections in DOM Promises should cause xpcshell tests to fail. r=Yoric

--HG--
extra : commitid : yWjnDGhk9j
extra : rebase_source : 24228660defd6971a621d52eee0b79be823c6926
extra : amend_source : ef8de24265fa9026b0f764a63453b816442f7232
extra : source : 3958782fe187026cbc3755ad6eae6920e67fa7d2
This commit is contained in:
Paolo Amadini 2016-02-03 12:58:03 +00:00
parent 00fe34957a
commit 8b8676204b
28 changed files with 458 additions and 60 deletions

View File

@ -3,6 +3,8 @@
// Test getDisplayString.
Cu.import("resource://testing-common/PromiseTestUtils.jsm", this);
var gDebuggee;
var gClient;
var gThreadClient;
@ -125,6 +127,7 @@ function test_display_string()
output: "Promise (fulfilled: 5)"
},
{
// This rejection is left uncaught, see expectUncaughtRejection below.
input: "Promise.reject(new Error())",
output: "Promise (rejected: Error)"
},
@ -134,6 +137,8 @@ function test_display_string()
}
];
PromiseTestUtils.expectUncaughtRejection(/Error/);
gThreadClient.addOneTimeListener("paused", function(aEvent, aPacket) {
const args = aPacket.frame.arguments;

View File

@ -8,6 +8,8 @@
"use strict";
Cu.import("resource://testing-common/PromiseTestUtils.jsm", this);
const { PromisesFront } = require("devtools/server/actors/promises");
var events = require("sdk/event/core");
@ -52,6 +54,7 @@ function* testPromisesSettled(client, form, makeResolvePromise,
let foundResolvedPromise = yield onPromiseSettled;
ok(foundResolvedPromise, "Found our resolved promise");
PromiseTestUtils.expectUncaughtRejection(r => r.message == resolution);
onPromiseSettled = oncePromiseSettled(front, resolution, false, true);
let rejectedPromise = makeRejectPromise(resolution);
let foundRejectedPromise = yield onPromiseSettled;

View File

@ -62,6 +62,11 @@ var listener = {
}
}
// Ignored until they are fixed in bug 1242968.
if (string.includes("JavaScript Warning")) {
return;
}
do_throw("head_acorn.js got console message: " + string + "\n");
}
};

View File

@ -35,6 +35,11 @@ var listener = {
}
}
// Ignored until they are fixed in bug 1242968.
if (string.includes("JavaScript Warning")) {
return;
}
do_throw("head_pretty-fast.js got console message: " + string + "\n");
}
};

View File

@ -7,6 +7,10 @@
var { utils: Cu } = Components;
Cu.import("resource://gre/modules/Timer.jsm", this);
Cu.import("resource://testing-common/PromiseTestUtils.jsm", this);
// Prevent test failures due to the unhandled rejections in this test file.
PromiseTestUtils.disableUncaughtRejectionObserverForSelfTest();
add_task(function* test_globals() {
Assert.equal(Promise.defer || undefined, undefined, "We are testing DOM Promise.");

View File

@ -458,7 +458,7 @@ this.PushService = {
// Before completing the activation check prefs. This will first check
// connection.enabled pref and then check offline state.
this._changeStateConnectionEnabledEvent(prefs.get("connection.enabled"));
});
}).catch(Cu.reportError);
} else {
// This is only used for testing. Different tests require connecting to

View File

@ -726,12 +726,15 @@ this.PushServiceHttp2 = {
.then(record => this._subscribeResource(record)
.then(recordNew => {
if (this._mainPushService) {
this._mainPushService.updateRegistrationAndNotifyApp(aSubscriptionUri,
recordNew);
this._mainPushService
.updateRegistrationAndNotifyApp(aSubscriptionUri, recordNew)
.catch(Cu.reportError);
}
}, error => {
if (this._mainPushService) {
this._mainPushService.dropRegistrationAndNotifyApp(aSubscriptionUri);
this._mainPushService
.dropRegistrationAndNotifyApp(aSubscriptionUri)
.catch(Cu.reportError);
}
})
);

View File

@ -4,6 +4,15 @@
'use strict';
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://testing-common/PromiseTestUtils.jsm");
///////////////////
//
// Whitelisting this test.
// As part of bug 1077403, the leaking uncaught rejection should be fixed.
//
// Instances of the rejection "record is undefined" may or may not appear.
PromiseTestUtils.thisTestLeaksUncaughtRejectionsAndShouldBeFixed();
const {PushDB, PushService, PushServiceHttp2} = serviceExports;

View File

@ -4,6 +4,15 @@
'use strict';
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://testing-common/PromiseTestUtils.jsm");
///////////////////
//
// Whitelisting this test.
// As part of bug 1077403, the leaking uncaught rejection should be fixed.
//
// Instances of the rejection "record is undefined" may or may not appear.
PromiseTestUtils.thisTestLeaksUncaughtRejectionsAndShouldBeFixed();
const {PushDB, PushService, PushServiceHttp2} = serviceExports;

View File

@ -10,6 +10,17 @@ const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
Cu.import("resource://testing-common/PromiseTestUtils.jsm");
///////////////////
//
// Whitelisting these tests.
// As part of bug 1077403, the shutdown crash should be fixed.
//
// These tests may crash intermittently on shutdown if the DOM Promise uncaught
// rejection observers are still registered when the watchdog operates.
PromiseTestUtils.thisTestLeaksUncaughtRejectionsAndShouldBeFixed();
var gPrefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
function setWatchdogEnabled(enabled) {

View File

@ -22,6 +22,7 @@ var _profileInitialized = false;
_register_modules_protocol_handler();
var _Promise = Components.utils.import("resource://gre/modules/Promise.jsm", {}).Promise;
var _PromiseTestUtils = Components.utils.import("resource://testing-common/PromiseTestUtils.jsm", {}).PromiseTestUtils;
// Support a common assertion library, Assert.jsm.
var AssertCls = Components.utils.import("resource://testing-common/Assert.jsm", null).Assert;
@ -213,7 +214,6 @@ function _do_main() {
function _do_quit() {
_testLogger.info("exiting test");
_Promise.Debugging.flushUncaughtErrors();
_quit = true;
}
@ -499,16 +499,8 @@ function _execute_test() {
// Call do_get_idle() to restore the factory and get the service.
_fakeIdleService.activate();
_Promise.Debugging.clearUncaughtErrorObservers();
_Promise.Debugging.addUncaughtErrorObserver(function observer({message, date, fileName, stack, lineNumber}) {
let text = " A promise chain failed to handle a rejection: " +
message + " - rejection date: " + date;
_testLogger.error(text,
{
stack: _format_stack(stack),
source_file: fileName
});
});
_PromiseTestUtils.init();
_PromiseTestUtils.Assert = Assert;
// _HEAD_FILES is dynamically defined by <runxpcshelltests.py>.
_load_files(_HEAD_FILES);
@ -539,6 +531,7 @@ function _execute_test() {
}
do_test_finished("MAIN run_test");
_do_main();
_PromiseTestUtils.assertNoUncaughtRejections();
} catch (e) {
_passed = false;
// do_check failures are already logged and set _quit to true and throw
@ -625,8 +618,14 @@ function _execute_test() {
_profileInitialized = false;
}
if (!_passed)
return;
try {
_PromiseTestUtils.ensureDOMPromiseRejectionsProcessed();
_PromiseTestUtils.assertNoUncaughtRejections();
_PromiseTestUtils.assertNoMoreExpectedRejections();
} finally {
// It's important to terminate the module to avoid crashes on shutdown.
_PromiseTestUtils.uninit();
}
}
/**
@ -1516,8 +1515,8 @@ function run_next_test()
function _run_next_test()
{
if (_gTestIndex < _gTests.length) {
// Flush uncaught errors as early and often as possible.
_Promise.Debugging.flushUncaughtErrors();
// Check for uncaught rejections as early and often as possible.
_PromiseTestUtils.assertNoUncaughtRejections();
let _properties;
[_properties, _gRunningTest,] = _gTests[_gTestIndex++];
if (typeof(_properties.skip_if) == "function" && _properties.skip_if()) {
@ -1538,10 +1537,18 @@ function run_next_test()
if (_properties._isTask) {
_gTaskRunning = true;
_Task.spawn(_gRunningTest).then(
() => { _gTaskRunning = false; run_next_test(); },
(ex) => { _gTaskRunning = false; do_report_unexpected_exception(ex); }
);
_Task.spawn(_gRunningTest).then(() => {
_gTaskRunning = false;
run_next_test();
}, ex => {
_gTaskRunning = false;
try {
do_report_unexpected_exception(ex);
} catch (ex) {
// The above throws NS_ERROR_ABORT and we don't want this to show up
// as an unhandled rejection later.
}
});
} else {
// Exceptions do not kill asynchronous tests, so they'll time out.
try {

View File

@ -35,6 +35,23 @@ TEST_FAIL_STRING = "TEST-UNEXPECTED-FAIL"
SIMPLE_PASSING_TEST = "function run_test() { do_check_true(true); }"
SIMPLE_FAILING_TEST = "function run_test() { do_check_true(false); }"
SIMPLE_UNCAUGHT_REJECTION_TEST = '''
function run_test() {
Promise.reject(new Error("Test rejection."));
do_check_true(true);
}
'''
SIMPLE_UNCAUGHT_REJECTION_JSM_TEST = '''
Components.utils.import("resource://gre/modules/Promise.jsm");
Promise.reject(new Error("Test rejection."));
function run_test() {
do_check_true(true);
}
'''
ADD_TEST_SIMPLE = '''
function run_test() { run_next_test(); }
@ -53,6 +70,26 @@ add_test(function test_failing() {
});
'''
ADD_TEST_UNCAUGHT_REJECTION = '''
function run_test() { run_next_test(); }
add_test(function test_uncaught_rejection() {
Promise.reject(new Error("Test rejection."));
run_next_test();
});
'''
ADD_TEST_UNCAUGHT_REJECTION_JSM = '''
Components.utils.import("resource://gre/modules/Promise.jsm");
function run_test() { run_next_test(); }
add_test(function test_uncaught_rejection() {
Promise.reject(new Error("Test rejection."));
run_next_test();
});
'''
CHILD_TEST_PASSING = '''
function run_test () { run_next_test(); }
@ -424,6 +461,7 @@ tail =
shuffle=shuffle,
verbose=verbose,
sequential=True,
testingModulesDir=os.path.join(objdir, '_tests', 'modules'),
utility_path=self.utility_path),
msg="""Tests should have %s, log:
========
@ -802,6 +840,30 @@ add_test({
self.assertInLog(TEST_FAIL_STRING)
self.assertNotInLog(TEST_PASS_STRING)
def testUncaughtRejection(self):
"""
Ensure a simple test with an uncaught rejection is reported.
"""
self.writeFile("test_simple_uncaught_rejection.js", SIMPLE_UNCAUGHT_REJECTION_TEST)
self.writeManifest(["test_simple_uncaught_rejection.js"])
self.assertTestResult(False)
self.assertEquals(1, self.x.testCount)
self.assertEquals(0, self.x.passCount)
self.assertEquals(1, self.x.failCount)
def testUncaughtRejectionJSM(self):
"""
Ensure a simple test with an uncaught rejection from Promise.jsm is reported.
"""
self.writeFile("test_simple_uncaught_rejection_jsm.js", SIMPLE_UNCAUGHT_REJECTION_JSM_TEST)
self.writeManifest(["test_simple_uncaught_rejection_jsm.js"])
self.assertTestResult(False)
self.assertEquals(1, self.x.testCount)
self.assertEquals(0, self.x.passCount)
self.assertEquals(1, self.x.failCount)
def testAddTestSimple(self):
"""
Ensure simple add_test() works.
@ -839,6 +901,30 @@ add_test({
self.assertEquals(0, self.x.passCount)
self.assertEquals(1, self.x.failCount)
def testAddTestUncaughtRejection(self):
"""
Ensure add_test() with an uncaught rejection is reported.
"""
self.writeFile("test_add_test_uncaught_rejection.js", ADD_TEST_UNCAUGHT_REJECTION)
self.writeManifest(["test_add_test_uncaught_rejection.js"])
self.assertTestResult(False)
self.assertEquals(1, self.x.testCount)
self.assertEquals(0, self.x.passCount)
self.assertEquals(1, self.x.failCount)
def testAddTestUncaughtRejectionJSM(self):
"""
Ensure add_test() with an uncaught rejection from Promise.jsm is reported.
"""
self.writeFile("test_add_test_uncaught_rejection_jsm.js", ADD_TEST_UNCAUGHT_REJECTION_JSM)
self.writeManifest(["test_add_test_uncaught_rejection_jsm.js"])
self.assertTestResult(False)
self.assertEquals(1, self.x.testCount)
self.assertEquals(0, self.x.passCount)
self.assertEquals(1, self.x.failCount)
def testAddTaskTestSingle(self):
"""
Ensure add_test_task() with a single passing test works.

View File

@ -2101,6 +2101,9 @@ this.DownloadCopySaver.prototype = {
// In case an error occurs while setting up the chain of objects for
// the download, ensure that we release the resources of the saver.
backgroundFileSaver.finish(Cr.NS_ERROR_FAILURE);
// Since we're not going to handle deferSaveComplete.promise below,
// we need to make sure that the rejection is handled.
deferSaveComplete.promise.catch(() => {});
throw ex;
}

View File

@ -174,7 +174,7 @@ this.DownloadImport.prototype = {
yield this.list.add(download);
if (resumeDownload) {
download.start();
download.start().catch(() => {});
} else {
yield download.refresh();
}

View File

@ -1080,7 +1080,7 @@ this.DownloadObserver = {
this._wakeTimer = null;
for (let download of this._canceledOfflineDownloads) {
download.start();
download.start().catch(() => {});
}
},

View File

@ -243,7 +243,7 @@ DownloadLegacyTransfer.prototype = {
}
// Start the download before allowing it to be controlled. Ignore errors.
aDownload.start().then(null, () => {});
aDownload.start().catch(() => {});
// Start processing all the other events received through nsITransfer.
this._deferDownload.resolve(aDownload);

View File

@ -124,8 +124,8 @@ this.DownloadStore.prototype = {
try {
if (!download.succeeded && !download.canceled && !download.error) {
// Try to restart the download if it was in progress during the
// previous session.
download.start();
// previous session. Ignore errors.
download.start().catch(() => {});
} else {
// If the download was not in progress, try to update the current
// progress from disk. This is relevant in case we retained

View File

@ -88,7 +88,7 @@ add_task(function* test_cancel_pdf_download() {
});
yield test_download_windowRef(tab, download);
download.start();
download.start().catch(() => {});
// Immediately cancel the download to test that it is erased correctly.
yield download.cancel();

View File

@ -32,7 +32,7 @@ function promiseStartDownload(aSourceUrl) {
}
return promiseNewDownload(aSourceUrl).then(download => {
download.start();
download.start().catch(() => {});
return download;
});
}
@ -64,7 +64,7 @@ function promiseStartDownload_tryToKeepPartialData() {
partFilePath: targetFilePath + ".part" },
});
download.tryToKeepPartialData = true;
download.start();
download.start().catch(() => {});
} else {
// Start a download using nsIExternalHelperAppService, that is configured
// to keep partially downloaded data by default.
@ -435,7 +435,7 @@ add_task(function* test_empty_progress_tryToKeepPartialData()
partFilePath: targetFilePath + ".part" },
});
download.tryToKeepPartialData = true;
download.start();
download.start().catch(() => {});
} else {
// Start a download using nsIExternalHelperAppService, that is configured
// to keep partially downloaded data by default.
@ -491,7 +491,7 @@ add_task(function* test_empty_noprogress()
}
};
download.start();
download.start().catch(() => {});
} else {
// When testing DownloadLegacySaver, the download is already started when it
// is created, and it may have already made all needed property change
@ -856,7 +856,7 @@ add_task(function* test_cancel_midway_restart_tryToKeepPartialData_false()
// Restart the download from the beginning.
mustInterruptResponses();
download.start();
download.start().catch(() => {});
yield promiseDownloadMidway(download);
yield promisePartFileReady(download);
@ -1143,7 +1143,7 @@ add_task(function* test_whenSucceeded_after_restart()
// we can verify getting a reference before the first download attempt.
download = yield promiseNewDownload(httpUrl("interruptible.txt"));
promiseSucceeded = download.whenSucceeded();
download.start();
download.start().catch(() => {});
} else {
// When testing DownloadLegacySaver, the download is already started when it
// is created, thus we cannot get the reference before the first attempt.
@ -1156,7 +1156,7 @@ add_task(function* test_whenSucceeded_after_restart()
// The second request is allowed to complete.
continueResponses();
download.start();
download.start().catch(() => {});
// Wait for the download to finish by waiting on the whenSucceeded promise.
yield promiseSucceeded;
@ -1343,7 +1343,7 @@ add_task(function* test_error_restart()
source: httpUrl("source.txt"),
target: targetFile,
});
download.start();
download.start().catch(() => {});
} else {
download = yield promiseStartLegacyDownload(null,
{ targetFile: targetFile });
@ -2186,7 +2186,7 @@ add_task(function* test_platform_integration()
source: httpUrl("source.txt"),
target: targetFile,
});
download.start();
download.start().catch(() => {});
}
// Wait for the whenSucceeded promise to be resolved first.

View File

@ -215,7 +215,7 @@ add_task(function* test_notifications()
let download3 = yield promiseNewDownload(httpUrl("interruptible.txt"));
let promiseAttempt1 = download1.start();
let promiseAttempt2 = download2.start();
download3.start();
download3.start().catch(() => {});
// Add downloads to list.
yield list.add(download1);
@ -250,8 +250,8 @@ add_task(function* test_no_notifications()
let list = yield promiseNewList(isPrivate);
let download1 = yield promiseNewDownload(httpUrl("interruptible.txt"));
let download2 = yield promiseNewDownload(httpUrl("interruptible.txt"));
download1.start();
download2.start();
download1.start().catch(() => {});
download2.start().catch(() => {});
// Add downloads to list.
yield list.add(download1);
@ -316,7 +316,7 @@ add_task(function* test_suspend_resume()
{
return Task.spawn(function* () {
let download = yield promiseNewDownload(httpUrl("interruptible.txt"));
download.start();
download.start().catch(() => {});
list.add(download);
return download;
});

View File

@ -348,7 +348,7 @@ add_task(function* test_history_expiration()
// Work with one finished download and one canceled download.
yield downloadOne.start();
downloadTwo.start();
downloadTwo.start().catch(() => {});
yield downloadTwo.cancel();
// We must replace the visits added while executing the downloads with visits
@ -471,7 +471,7 @@ add_task(function* test_DownloadSummary()
// Add a public download that has been canceled midway.
let canceledPublicDownload =
yield promiseNewDownload(httpUrl("interruptible.txt"));
canceledPublicDownload.start();
canceledPublicDownload.start().catch(() => {});
yield promiseDownloadMidway(canceledPublicDownload);
yield canceledPublicDownload.cancel();
yield publicList.add(canceledPublicDownload);
@ -479,7 +479,7 @@ add_task(function* test_DownloadSummary()
// Add a public download that is in progress.
let inProgressPublicDownload =
yield promiseNewDownload(httpUrl("interruptible.txt"));
inProgressPublicDownload.start();
inProgressPublicDownload.start().catch(() => {});
yield promiseDownloadMidway(inProgressPublicDownload);
yield publicList.add(inProgressPublicDownload);
@ -488,7 +488,7 @@ add_task(function* test_DownloadSummary()
source: { url: httpUrl("interruptible.txt"), isPrivate: true },
target: getTempFile(TEST_TARGET_FILE_NAME).path,
});
inProgressPrivateDownload.start();
inProgressPrivateDownload.start().catch(() => {});
yield promiseDownloadMidway(inProgressPrivateDownload);
yield privateList.add(inProgressPrivateDownload);

View File

@ -4453,6 +4453,13 @@ SearchService.prototype = {
},
_addObservers: function SRCH_SVC_addObservers() {
if (this._observersAdded) {
// There might be a race between synchronous and asynchronous
// initialization for which we try to register the observers twice.
return;
}
this._observersAdded = true;
Services.obs.addObserver(this, SEARCH_ENGINE_TOPIC, false);
Services.obs.addObserver(this, QUIT_APPLICATION_TOPIC, false);
@ -4495,6 +4502,7 @@ SearchService.prototype = {
() => shutdownState
);
},
_observersAdded: false,
_removeObservers: function SRCH_SVC_removeObservers() {
Services.obs.removeObserver(this, SEARCH_ENGINE_TOPIC);

View File

@ -47,10 +47,6 @@ add_task(function* async_init() {
add_task(function* sync_init() {
let reInitPromise = asyncReInit();
// Synchronously check the current default engine, to force a sync init.
// XXX For some reason forcing a sync init while already asynchronously
// reinitializing causes a shutdown warning related to engineMetadataService's
// finalize method having already been called. Seems harmless for the purpose
// of this test.
do_check_false(Services.search.isInitialized);
do_check_eq(Services.search.currentEngine.name, "hidden");
do_check_true(Services.search.isInitialized);

View File

@ -9,6 +9,10 @@ BROWSER_CHROME_MANIFESTS += ['tests/browser/browser.ini']
MOCHITEST_MANIFESTS += ['tests/mochitest/mochitest.ini']
MOCHITEST_CHROME_MANIFESTS += ['tests/chrome/chrome.ini']
TESTING_JS_MODULES += [
'tests/PromiseTestUtils.jsm',
]
SPHINX_TREES['toolkit_modules'] = 'docs'
EXTRA_JS_MODULES += [

View File

@ -0,0 +1,241 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* Detects and reports unhandled rejections during test runs. Test harnesses
* will fail tests in this case, unless the test whitelists itself.
*/
"use strict";
this.EXPORTED_SYMBOLS = [
"PromiseTestUtils",
];
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
Cu.import("resource://gre/modules/Services.jsm", this);
// Keep "JSMPromise" separate so "Promise" still refers to DOM Promises.
let JSMPromise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
// For now, we need test harnesses to provide a reference to Assert.jsm.
let Assert = null;
this.PromiseTestUtils = {
/**
* Array of objects containing the details of the Promise rejections that are
* currently left uncaught. This includes DOM Promise and Promise.jsm. When
* rejections in DOM Promises are consumed, they are removed from this list.
*
* The objects contain at least the following properties:
* {
* message: The error message associated with the rejection, if any.
* date: Date object indicating when the rejection was observed.
* id: For DOM Promise only, the Promise ID from PromiseDebugging. This is
* only used for tracking and should not be checked by the callers.
* stack: nsIStackFrame, SavedFrame, or string indicating the stack at the
* time the rejection was triggered. May also be null if the
* rejection was triggered while a script was on the stack.
* }
*/
_rejections: [],
/**
* 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: [],
/**
* 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.");
return;
}
PromiseDebugging.addUncaughtRejectionObserver(this);
// Promise.jsm rejections are only reported to this observer when requested,
// so we don't have to store a key to remove them when consumed.
JSMPromise.Debugging.addUncaughtErrorObserver(
rejection => this._rejections.push(rejection));
this._initialized = true;
},
_initialized: false,
/**
* Called only by the test infrastructure, unregisters the observers.
*/
uninit() {
if (!this._initialized) {
return;
}
PromiseDebugging.removeUncaughtRejectionObserver(this);
JSMPromise.Debugging.clearUncaughtErrorObservers();
this._initialized = false;
},
/**
* Called only by the test infrastructure, spins the event loop until the
* messages for pending DOM Promise rejections have been processed.
*/
ensureDOMPromiseRejectionsProcessed() {
let observed = false;
let observer = {
onLeftUncaught: promise => {
if (PromiseDebugging.getState(promise).reason ===
this._ensureDOMPromiseRejectionsProcessedReason) {
observed = true;
}
},
onConsumed() {},
};
PromiseDebugging.addUncaughtRejectionObserver(observer);
Promise.reject(this._ensureDOMPromiseRejectionsProcessedReason);
while (!observed) {
Services.tm.mainThread.processNextEvent(true);
}
PromiseDebugging.removeUncaughtRejectionObserver(observer);
},
_ensureDOMPromiseRejectionsProcessedReason: {},
/**
* Called only by the tests for PromiseDebugging.addUncaughtRejectionObserver
* and for JSMPromise.Debugging, disables the observers in this module.
*/
disableUncaughtRejectionObserverForSelfTest() {
this.uninit();
},
/**
* Called by tests that have been whitelisted, disables the observers in this
* module. For new tests where uncaught rejections are expected, you should
* use the more granular expectUncaughtRejection function instead.
*/
thisTestLeaksUncaughtRejectionsAndShouldBeFixed() {
this.uninit();
},
/**
* Sets or updates the Assert object instance to be used for error reporting.
*/
set Assert(assert) {
Assert = assert;
},
// UncaughtRejectionObserver
onLeftUncaught(promise) {
let message = "(Unable to convert rejection reason to string.)";
try {
let reason = PromiseDebugging.getState(promise).reason;
if (reason === this._ensureDOMPromiseRejectionsProcessedReason) {
// Ignore the special promise for ensureDOMPromiseRejectionsProcessed.
return;
}
message = reason.message || ("" + reason);
} 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),
});
},
// 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);
let index = this._rejections.findIndex(rejection => rejection.id == id);
// If we get a consumption notification for a rejection that was left
// uncaught before this module was initialized, we can safely ignore it.
if (index != -1) {
this._rejections.splice(index, 1);
}
},
/**
* Informs the test suite that the test code will generate a Promise rejection
* that will still be unhandled when the test file terminates.
*
* This method must be called once for each instance of Promise that is
* expected to be uncaught, even if the rejection reason is the same for each
* instance.
*
* If the expected rejection does not occur, the test will fail.
*
* @param regExpOrCheckFn
* This can either be a regular expression that should match the error
* message of the rejection, or a check function that is invoked with
* the rejection details object as its first argument.
*/
expectUncaughtRejection(regExpOrCheckFn) {
let checkFn = !("test" in regExpOrCheckFn) ? regExpOrCheckFn :
rejection => regExpOrCheckFn.test(rejection.message);
this._rejectionIgnoreFns.push(checkFn);
},
/**
* 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.
*/
assertNoUncaughtRejections() {
// Ask Promise.jsm to report all uncaught rejections to the observer now.
JSMPromise.Debugging.flushUncaughtErrors();
// If there is any uncaught rejection left at this point, the test fails.
while (this._rejections.length > 0) {
let rejection = this._rejections.shift();
// 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;
}
// 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}`);
}
},
/**
* Fails the test if any rejection indicated by expectUncaughtRejection has
* not yet been reported at this time.
*
* 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.");
}
},
};

View File

@ -1,12 +1,7 @@
"use strict";
var {ObjectUtils} = Components.utils.import("resource://gre/modules/ObjectUtils.jsm", {});
var {Promise} = Components.utils.import("resource://gre/modules/Promise.jsm", {});
add_task(function* init() {
// The code will cause uncaught rejections on purpose.
Promise.Debugging.clearUncaughtErrorObservers();
});
var {PromiseTestUtils} = Components.utils.import("resource://testing-common/PromiseTestUtils.jsm", {});
add_task(function* test_strict() {
let loose = { a: 1 };
@ -16,11 +11,13 @@ add_task(function* test_strict() {
loose.b || undefined; // Should not throw.
strict.a; // Should not throw.
PromiseTestUtils.expectUncaughtRejection(/No such property: "b"/);
Assert.throws(() => strict.b, /No such property: "b"/);
"b" in strict; // Should not throw.
strict.b = 2;
strict.b; // Should not throw.
PromiseTestUtils.expectUncaughtRejection(/No such property: "c"/);
Assert.throws(() => strict.c, /No such property: "c"/);
"c" in strict; // Should not throw.
loose.c = 3;

View File

@ -5,10 +5,10 @@
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");
// Deactivate the standard xpcshell observer, as it turns uncaught
// rejections into failures, which we don't want here.
Promise.Debugging.clearUncaughtErrorObservers();
// Prevent test failures due to the unhandled rejections in this test file.
PromiseTestUtils.disableUncaughtRejectionObserverForSelfTest();
////////////////////////////////////////////////////////////////////////////////
//// Test runner

View File

@ -5,6 +5,7 @@
Components.utils.import("resource://gre/modules/PromiseUtils.jsm");
Components.utils.import("resource://gre/modules/Timer.jsm");
Components.utils.import("resource://testing-common/PromiseTestUtils.jsm");
// Tests for PromiseUtils.jsm
function run_test() {
@ -98,8 +99,9 @@ add_task(function* test_reject_resolved_promise() {
/* Test for the case when a rejected Promise is
* passed to the reject method */
add_task(function* test_reject_resolved_promise() {
PromiseTestUtils.expectUncaughtRejection(/This one rejects/);
let def = PromiseUtils.defer();
let p = new Promise((resolve, reject) => reject(new Error("This on rejects")));
let p = new Promise((resolve, reject) => reject(new Error("This one rejects")));
def.reject(p);
yield Assert.rejects(def.promise, Promise, "Rejection with a rejected promise uses the passed promise itself as the reason of rejection");
});