node/deps/v8/test/inspector/debugger/break-on-exception-promise-catch-prediction.js
Michaël Zasso 1d29d81c69 deps: update V8 to 12.3.219.16
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>
2024-04-19 08:39:47 +00:00

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