mirror of
https://github.com/nodejs/node.git
synced 2025-05-15 22:04:26 +00:00

PR-URL: https://github.com/nodejs/node/pull/52293 Reviewed-By: Moshe Atlow <moshe@atlow.co.il> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com> Reviewed-By: Richard Lau <rlau@redhat.com>
617 lines
16 KiB
JavaScript
617 lines
16 KiB
JavaScript
// Copyright 2023 the V8 project authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
let {session, contextGroup, Protocol} = InspectorTest.start('Checks when promise rejections are predicted to be caught.');
|
|
|
|
// catch-prediction.js
|
|
|
|
function rejectAfterDelayInPromiseConstructor() {
|
|
return new Promise((pass, reject) => setTimeout(() => reject(new Error('fail')), 0));
|
|
}
|
|
|
|
function promiseReject() {
|
|
return Promise.reject(new Error('fail'));
|
|
}
|
|
|
|
function throwInPromiseConstructor() {
|
|
return new Promise(() => {throw new Error('fail');});
|
|
}
|
|
|
|
function rejectInPromiseConstructor() {
|
|
return new Promise((pass, reject) => {
|
|
reject(new Error('fail'));
|
|
console.log('after reject');
|
|
});
|
|
}
|
|
|
|
function rejectAfterDelayInTryInPromiseConstructor() {
|
|
return new Promise((pass, reject) => setTimeout(() => {
|
|
try {
|
|
reject(new Error('fail'));
|
|
} catch(e) {
|
|
console.log('fake'); // Won't be hit
|
|
}
|
|
}, 0));
|
|
}
|
|
|
|
function rejectBindAfterDelay() {
|
|
// Unbreakable location because there will be nothing on the callstack
|
|
return new Promise((pass, reject) => setTimeout(reject.bind(null, new Error('fail')), 0));
|
|
}
|
|
|
|
async function throwFromAsync() {
|
|
throw new Error('fail');
|
|
}
|
|
|
|
async function throwFromAsyncAfterDelay() {
|
|
await delay();
|
|
throw new Error('fail');
|
|
}
|
|
|
|
function customRejectingThenable() {
|
|
// JS supports awaiting on user defined promise-like objects
|
|
return {
|
|
then(onFulfilled, onRejected) {
|
|
onRejected(new Error('fail'));
|
|
}
|
|
}
|
|
}
|
|
|
|
function customRejectingThenableAfterDelay() {
|
|
// JS supports awaiting on user defined promise-like objects
|
|
return {
|
|
then(onFulfilled, onRejected) {
|
|
setTimeout(() => onRejected(new Error('fail')), 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
function exceptionInThen() {
|
|
return Promise.resolve().then(() => {throw new Error('fail');});
|
|
}
|
|
|
|
function exceptionInCatch() {
|
|
return Promise.reject(new Error('will catch')).catch(() => {
|
|
console.log('throwing again');
|
|
throw new Error('fail');
|
|
});
|
|
}
|
|
|
|
async function exceptionInAsyncGenerator() {
|
|
async function* gen() {
|
|
yield 1;
|
|
throw new Error('fail');
|
|
}
|
|
for await(const x of gen()) {}
|
|
}
|
|
|
|
async function rejectInGenerator() {
|
|
function* gen() {
|
|
yield promiseReject();
|
|
}
|
|
for await(const x of gen()) {}
|
|
}
|
|
|
|
async function rejectAfterDelayInGenerator() {
|
|
function* gen() {
|
|
yield rejectAfterDelayInPromiseConstructor();
|
|
}
|
|
for await(const x of gen()) {}
|
|
}
|
|
|
|
async function rejectInAsyncGenerator() {
|
|
async function* gen() {
|
|
yield promiseReject();
|
|
}
|
|
for await(const x of gen()) {}
|
|
}
|
|
|
|
async function rejectAfterDelayInAsyncGenerator() {
|
|
async function* gen() {
|
|
yield rejectAfterDelayInPromiseConstructor();
|
|
}
|
|
for await(const x of gen()) {}
|
|
}
|
|
|
|
async function asyncGeneratorThrowMethod() {
|
|
async function* gen() {
|
|
yield 1;
|
|
}
|
|
const g = gen();
|
|
await g.next();
|
|
await g.throw(new Error('fail'));
|
|
}
|
|
|
|
async function asyncCatchingGeneratorThrowMethod() {
|
|
async function* gen() {
|
|
try {
|
|
yield 1;
|
|
} catch (e) {
|
|
console.log('caught by generator');
|
|
}
|
|
}
|
|
const g = gen();
|
|
await g.next();
|
|
await g.throw(new Error('fail'));
|
|
}
|
|
|
|
async function throwInAsyncGenerator() {
|
|
async function* gen() {
|
|
yield 1;
|
|
throw new Error('fail');
|
|
}
|
|
for await(const x of gen()) {}
|
|
}
|
|
|
|
function promiseRejectInTry() {
|
|
// A synchronous try can't catch a promise rejection.
|
|
try {
|
|
return Promise.reject(new Error('fail'));
|
|
} catch (e) {
|
|
console.log('fake'); // Will not catch
|
|
}
|
|
}
|
|
|
|
function rejectAfterFulfillInPromiseConstructor() {
|
|
return new Promise((pass, reject) => {
|
|
pass('ok');
|
|
console.log('after pass');
|
|
reject(new Error('fail'));
|
|
console.log('after reject');
|
|
});
|
|
}
|
|
|
|
function rejectAfterDelayAfterFulfillInPromiseConstructor() {
|
|
return new Promise((pass, reject) => {
|
|
pass('ok');
|
|
setTimeout(() => reject(new Error('fail')), 0);
|
|
});
|
|
}
|
|
|
|
function throwAfterFulfillInPromiseConstructor() {
|
|
return new Promise((pass, reject) => {
|
|
pass('ok');
|
|
console.log('after pass');
|
|
throw new Error('fail');
|
|
});
|
|
}
|
|
|
|
async function dontHandleAsync(fn) {
|
|
// Unhandled exception that is trivial to predict,
|
|
// for comparison.
|
|
await fn();
|
|
}
|
|
|
|
async function awaitAndCreateInTry(fn) {
|
|
try {
|
|
// The easy case to get right, for comparison.
|
|
await fn();
|
|
} catch(e) {
|
|
console.log('caught');
|
|
}
|
|
}
|
|
|
|
function catchMethod(fn) {
|
|
// Catch handler may not be attached at the time promise is rejected
|
|
fn().catch((e) => console.log('caught'));
|
|
}
|
|
|
|
function finallyMethod(fn) {
|
|
// Is a finally handler considered caught?
|
|
fn().finally(() => console.log('finally handler'));
|
|
}
|
|
|
|
function catchMethodInTry(fn) {
|
|
try {
|
|
fn().catch((e) => console.log('caught'));
|
|
} catch(e) {
|
|
console.log('fake'); // Won't be hit
|
|
}
|
|
}
|
|
|
|
async function awaitInTry(fn) {
|
|
// No handler when promise is created, but there is when it is awaited
|
|
const promise = fn();
|
|
try {
|
|
await promise;
|
|
} catch(e) {
|
|
console.log('caught');
|
|
}
|
|
}
|
|
|
|
async function awaitAndCreateInTryFinally(fn) {
|
|
try {
|
|
await fn();
|
|
} finally {
|
|
// Is finally handler treated as a catch?
|
|
console.log('finally handler');
|
|
}
|
|
}
|
|
|
|
async function createInTry(fn) {
|
|
let promise = null;
|
|
// In this test case we don't handle the exception,
|
|
// but first we try to convince JS we will.
|
|
try {
|
|
promise = fn();
|
|
} catch(e) {
|
|
console.log('fake'); // Won't be hit
|
|
}
|
|
|
|
if(promise) {
|
|
await promise;
|
|
}
|
|
}
|
|
|
|
function fireAndForgetInTry(fn) {
|
|
try {
|
|
// Unhandled exception, but will we mispredict as caught?
|
|
fn();
|
|
} catch(e) {
|
|
console.log('fake'); // Won't be hit
|
|
}
|
|
}
|
|
|
|
function finallyAndCatchMethod(fn) {
|
|
// Will we account for the catch method after the finally?
|
|
fn().finally(() => console.log('finally handler'))
|
|
.catch(() => console.log('caught'));
|
|
}
|
|
|
|
function thenMethod(fn) {
|
|
fn().then(() => console.log('not hit'));
|
|
}
|
|
|
|
function thenMethodWithTwoArgs(fn) {
|
|
fn().then(() => console.log('not hit'), () => console.log('caught'));
|
|
}
|
|
|
|
async function caughtPromiseRace(fn) {
|
|
try {
|
|
await Promise.race([fn()]);
|
|
} catch(e) {
|
|
console.log('caught');
|
|
}
|
|
}
|
|
|
|
async function caughtInTryPromiseRace(fn) {
|
|
const promise = fn();
|
|
try {
|
|
await Promise.race([promise]);
|
|
} catch(e) {
|
|
console.log('caught');
|
|
}
|
|
}
|
|
|
|
async function uncaughtPromiseRace(fn) {
|
|
await Promise.race([fn()]);
|
|
}
|
|
|
|
async function ignoredPromiseRace(fn) {
|
|
// Race will resolve to first promise, preventing exception from second
|
|
// from propagating. Is this caught or uncaught?
|
|
await Promise.race([Promise.resolve(), fn()]);
|
|
}
|
|
|
|
async function ignoredPromiseRaceInTry(fn) {
|
|
try {
|
|
// Race will resolve to first promise, preventing exception from second
|
|
// from propagating. Is this caught or uncaught?
|
|
await Promise.race([Promise.resolve(), fn()]);
|
|
} catch(e) {
|
|
console.log('caught');
|
|
}
|
|
}
|
|
|
|
async function caughtPromiseAll(fn) {
|
|
try {
|
|
await Promise.all([fn()]);
|
|
} catch(e) {
|
|
console.log('caught');
|
|
}
|
|
}
|
|
|
|
async function uncaughtPromiseAll(fn) {
|
|
await Promise.all([fn()]);
|
|
}
|
|
|
|
async function caughtPromiseAny(fn) {
|
|
try {
|
|
await Promise.any([fn()]);
|
|
} catch(e) {
|
|
console.log('caught');
|
|
}
|
|
}
|
|
|
|
async function uncaughtPromiseAny(fn) {
|
|
await Promise.any([fn()]);
|
|
}
|
|
|
|
async function ignoredPromiseAny(fn) {
|
|
// Any will resolve to first promise, preventing exception from second
|
|
// from propagating. Is this caught or uncaught?
|
|
await Promise.any([Promise.resolve(), fn()]);
|
|
}
|
|
|
|
async function ignoredPromiseAnyInTry(fn) {
|
|
try {
|
|
// Any will resolve to first promise, preventing exception from second
|
|
// from propagating. Is this caught or uncaught?
|
|
await Promise.any([Promise.resolve(), fn()]);
|
|
} catch(e) {
|
|
console.log('caught');
|
|
}
|
|
}
|
|
|
|
async function ignoredPromiseAnyWithIndirection(fn) {
|
|
// Similar to other ignored cases but we have to walk the promise tree to
|
|
// find the already fulfilled promise.
|
|
async function indirectThrow() {
|
|
await fn();
|
|
}
|
|
async function indirectAny() {
|
|
await Promise.any([Promise.resolve(), indirectThrow()]);
|
|
}
|
|
await indirectAny();
|
|
}
|
|
|
|
async function ignoredPromiseAnyWithIndirectionInTry(fn) {
|
|
async function indirectThrow() {
|
|
await fn();
|
|
}
|
|
|
|
try {
|
|
// Similar to other ignored cases but we have to walk the promise tree to
|
|
// find the already fulfilled promise.
|
|
await Promise.any([Promise.resolve(), indirectThrow()]);
|
|
} catch(e) {
|
|
console.log('caught');
|
|
}
|
|
}
|
|
|
|
async function awaitAfterDelayInTry(fn) {
|
|
try {
|
|
// Predicts correctly if thrown immediately but if not no handler will
|
|
// be attached when it throws.
|
|
const promise = fn();
|
|
console.log('sleeping');
|
|
await delay();
|
|
await delay();
|
|
console.log('awaiting');
|
|
await promise;
|
|
} catch(e) {
|
|
console.log('caught');
|
|
}
|
|
}
|
|
|
|
async function catchInAsyncGenerator(fn) {
|
|
async function* gen() {
|
|
try {
|
|
yield fn();
|
|
} catch (e) {
|
|
console.log('caught');
|
|
}
|
|
}
|
|
for await(const x of gen()) {}
|
|
}
|
|
|
|
async function catchInGenerator(fn) {
|
|
function* gen() {
|
|
try {
|
|
yield fn();
|
|
} catch (e) {
|
|
console.log('fake'); // Will not catch
|
|
}
|
|
}
|
|
for await(const x of gen()) {}
|
|
}
|
|
|
|
async function catchAndAlsoDont(fn) {
|
|
const uncaughtPromise = fn();
|
|
const caughtPromise = uncaughtPromise.catch(() => console.log('caught'));
|
|
await uncaughtPromise;
|
|
console.log('never reached');
|
|
}
|
|
|
|
function delay() {
|
|
return new Promise(resolve => setTimeout(resolve, 0));
|
|
}
|
|
|
|
async function testWrapper(throwFunc, handleFunc) {
|
|
// testWrapper runs testFunc and completes successfully when testFunc
|
|
// completes, but does so in a setTimeout so that it doesn't catch any
|
|
// exceptions thrown by testFunc.
|
|
async function runWithResolution(resolve) {
|
|
try {
|
|
const promise = handleFunc(throwFunc);
|
|
console.log('awaiting promise');
|
|
await promise;
|
|
console.log('finished without error');
|
|
} finally {
|
|
// Leave exceptions uncaught, but the testWrapper continues normally
|
|
// anyway. Needs to be in a setTimeout so that uncaught exceptions
|
|
// are handled before the test is complete.
|
|
setTimeout(resolve, 0);
|
|
}
|
|
}
|
|
await new Promise(resolve => setTimeout(runWithResolution.bind(null, resolve), 0));
|
|
}
|
|
|
|
// -------------------------------------
|
|
|
|
// Order of functions should match above so that line numbers match
|
|
const basicThrowFunctions = [rejectAfterDelayInPromiseConstructor, promiseReject];
|
|
const advancedThrowFunctions = [
|
|
throwInPromiseConstructor,
|
|
rejectInPromiseConstructor,
|
|
rejectAfterDelayInTryInPromiseConstructor,
|
|
rejectBindAfterDelay,
|
|
throwFromAsync,
|
|
throwFromAsyncAfterDelay,
|
|
customRejectingThenable,
|
|
customRejectingThenableAfterDelay,
|
|
exceptionInThen,
|
|
exceptionInCatch,
|
|
exceptionInAsyncGenerator,
|
|
rejectInGenerator,
|
|
rejectAfterDelayInGenerator,
|
|
rejectInAsyncGenerator,
|
|
rejectAfterDelayInAsyncGenerator,
|
|
asyncGeneratorThrowMethod,
|
|
asyncCatchingGeneratorThrowMethod,
|
|
throwInAsyncGenerator,
|
|
promiseRejectInTry,
|
|
rejectAfterFulfillInPromiseConstructor,
|
|
rejectAfterDelayAfterFulfillInPromiseConstructor,
|
|
throwAfterFulfillInPromiseConstructor,
|
|
];
|
|
const basicCatchFunctions = [dontHandleAsync, awaitAndCreateInTry];
|
|
const advancedCatchFunctions = [
|
|
catchMethod,
|
|
finallyMethod,
|
|
catchMethodInTry,
|
|
awaitInTry,
|
|
awaitAndCreateInTryFinally,
|
|
createInTry,
|
|
fireAndForgetInTry,
|
|
finallyAndCatchMethod,
|
|
thenMethod,
|
|
thenMethodWithTwoArgs,
|
|
caughtPromiseRace,
|
|
caughtInTryPromiseRace,
|
|
uncaughtPromiseRace,
|
|
ignoredPromiseRace,
|
|
ignoredPromiseRaceInTry,
|
|
caughtPromiseAll,
|
|
uncaughtPromiseAll,
|
|
caughtPromiseAny,
|
|
uncaughtPromiseAny,
|
|
ignoredPromiseAny,
|
|
ignoredPromiseAnyInTry,
|
|
ignoredPromiseAnyWithIndirection,
|
|
ignoredPromiseAnyWithIndirectionInTry,
|
|
awaitAfterDelayInTry,
|
|
catchInAsyncGenerator,
|
|
catchInGenerator,
|
|
catchAndAlsoDont,
|
|
];
|
|
const helpers = [delay, testWrapper];
|
|
|
|
const file = [...basicThrowFunctions, ...advancedThrowFunctions, ...basicCatchFunctions, ...advancedCatchFunctions, ...helpers].join('\n\n');
|
|
const startLine = 9; // Should match first line of first function
|
|
contextGroup.addScript(file, startLine, 0, 'catch-prediction.js');
|
|
session.setupScriptMap();
|
|
|
|
let pauseCount = 0
|
|
let uncaught = false;
|
|
let predictedUncaught = null;
|
|
let exceptionDetails = null;
|
|
let exceptionStackTrace = null;
|
|
let exceptionId = null;
|
|
|
|
function lazyLogException() {
|
|
if (exceptionDetails || exceptionStackTrace) {
|
|
InspectorTest.log(`Uncaught exception: ${exceptionDetails}`);
|
|
session.logAsyncStackTrace(exceptionStackTrace);
|
|
}
|
|
exceptionDetails = null;
|
|
exceptionStackTrace = null;
|
|
}
|
|
|
|
function uncaughtString(uncaught) {
|
|
return ((uncaught && 'uncaught') ?? '(undefined)') || 'caught';
|
|
}
|
|
|
|
Protocol.Debugger.onPaused(message => {
|
|
lazyLogException();
|
|
predictedUncaught = message.params.data?.uncaught;
|
|
InspectorTest.log(`Paused on ${uncaughtString(predictedUncaught)} ${message.params.reason}`);
|
|
session.logCallFrames(message.params.callFrames);
|
|
session.logAsyncStackTrace(message.params.asyncStackTrace);
|
|
InspectorTest.log('');
|
|
pauseCount++;
|
|
Protocol.Debugger.resume();
|
|
});
|
|
|
|
Protocol.Console.onMessageAdded(event => {
|
|
lazyLogException();
|
|
InspectorTest.log('console: ' + event.params.message.text);
|
|
});
|
|
Protocol.Debugger.enable();
|
|
Protocol.Debugger.setAsyncCallStackDepth({maxDepth: 6});
|
|
Protocol.Console.enable();
|
|
Protocol.Runtime.onExceptionRevoked(event => {
|
|
if (!uncaught) {
|
|
InspectorTest.log('ERROR: Revoked exception when none thrown');
|
|
} else if (!exceptionId) {
|
|
InspectorTest.log('ERROR: No exception id');
|
|
} else if (exceptionId !== event.params.exceptionId) {
|
|
InspectorTest.log('ERROR: exception id does not match');
|
|
}
|
|
exceptionId = null;
|
|
uncaught = false;
|
|
let message = 'Previously printed uncaught exception revoked';
|
|
if (exceptionDetails || exceptionStackTrace) {
|
|
message = 'Uncaught message immediately revoked';
|
|
exceptionDetails = null;
|
|
exceptionStackTrace = null;
|
|
}
|
|
InspectorTest.log(message + ' for reason: ' + event.params.reason)
|
|
});
|
|
Protocol.Runtime.onExceptionThrown(event => {
|
|
lazyLogException();
|
|
if (uncaught) {
|
|
InspectorTest.log('ERROR: Uncaught exception thrown twice');
|
|
}
|
|
uncaught = true;
|
|
// Save this info and lazily log it so we can not log it when it is
|
|
// immediately revoked.
|
|
exceptionDetails = event.params.exceptionDetails.text;
|
|
exceptionStackTrace = event.params.exceptionDetails.stackTrace;
|
|
exceptionId = event.params.exceptionDetails.exceptionId;
|
|
});
|
|
Protocol.Runtime.enable();
|
|
|
|
const testFunctions = [];
|
|
|
|
function addTestFunctionsFor(throwFunc, catchFunc) {
|
|
async function testCase(next) {
|
|
InspectorTest.log(`> Throwing from ${throwFunc.name}, handling with ${catchFunc.name}`);
|
|
await Protocol.Debugger.setPauseOnExceptions({state: 'all'});
|
|
uncaught = false;
|
|
pauseCount = 0;
|
|
predictedUncaught = null;
|
|
exceptionId = null;
|
|
await Protocol.Runtime.evaluate({expression: `testWrapper(${throwFunc.name}, ${catchFunc.name})//# sourceURL=test_framework.js`, awaitPromise: true});
|
|
lazyLogException();
|
|
InspectorTest.log('');
|
|
if (pauseCount !== 1) {
|
|
InspectorTest.log(`WARNING: Test paused ${pauseCount} times`);
|
|
}
|
|
if (pauseCount > 0) {
|
|
if (uncaught === predictedUncaught) {
|
|
InspectorTest.log('Correctly predicted as ' + uncaughtString(uncaught));
|
|
} else {
|
|
InspectorTest.log(`PREDICTION MISMATCH: Exception was ${uncaughtString(uncaught)} but was predicted to be ${uncaughtString(predictedUncaught)}`);
|
|
}
|
|
}
|
|
InspectorTest.log('------------------------------------------------------');
|
|
}
|
|
testFunctions.push(testCase);
|
|
}
|
|
|
|
for (const throwFunc of [...basicThrowFunctions, ...advancedThrowFunctions]) {
|
|
for (const catchFunc of basicCatchFunctions) {
|
|
addTestFunctionsFor(throwFunc, catchFunc);
|
|
}
|
|
}
|
|
|
|
for (const catchFunc of advancedCatchFunctions) {
|
|
for (const throwFunc of basicThrowFunctions) {
|
|
addTestFunctionsFor(throwFunc, catchFunc);
|
|
}
|
|
}
|
|
|
|
InspectorTest.runAsyncTestSuite(testFunctions);
|