Each JSON record enumerates the disposition of the properties on * some corresponding intrinsic object. @@ -599,6 +614,8 @@ export const permitted = { SyntaxError: NativeError('%SyntaxErrorPrototype%'), TypeError: NativeError('%TypeErrorPrototype%'), URIError: NativeError('%URIErrorPrototype%'), + // https://github.com/endojs/endo/issues/550 + AggregateError: NativeError('%AggregateErrorPrototype%'), '%EvalErrorPrototype%': NativeErrorPrototype('EvalError'), '%RangeErrorPrototype%': NativeErrorPrototype('RangeError'), @@ -606,6 +623,8 @@ export const permitted = { '%SyntaxErrorPrototype%': NativeErrorPrototype('SyntaxError'), '%TypeErrorPrototype%': NativeErrorPrototype('TypeError'), '%URIErrorPrototype%': NativeErrorPrototype('URIError'), + // https://github.com/endojs/endo/issues/550 + '%AggregateErrorPrototype%': NativeErrorPrototype('AggregateError'), // *** Numbers and Dates @@ -1473,9 +1492,8 @@ export const permitted = { '[[Proto]]': '%FunctionPrototype%', all: fn, allSettled: fn, - // To transition from `false` to `fn` once we also have `AggregateError` - // TODO https://github.com/Agoric/SES-shim/issues/550 - any: false, // ES2021 + // https://github.com/Agoric/SES-shim/issues/550 + any: fn, prototype: '%PromisePrototype%', race: fn, reject: fn, diff --git a/packages/ses/test/error/test-aggregate-error-console-demo.js b/packages/ses/test/error/test-aggregate-error-console-demo.js new file mode 100644 index 0000000000..ba2da6ae16 --- /dev/null +++ b/packages/ses/test/error/test-aggregate-error-console-demo.js @@ -0,0 +1,24 @@ +import test from 'ava'; +import '../../index.js'; + +// This is the demo version of test-aggregate-error-console.js that +// just outputs to the actual console, rather than using the logging console +// to test. Its purpose is to eyeball rather than automated testing. +// It also serves as a demo form of test-error-cause-console.js, since +// it also shows console output for those cases. + +lockdown(); + +test('aggregate error console demo', t => { + if (typeof AggregateError === 'undefined') { + t.pass('skip test on platforms prior to AggregateError'); + return; + } + const e3 = Error('e3'); + const e2 = Error('e2', { cause: e3 }); + const u4 = URIError('u4', { cause: e2 }); + + const a1 = AggregateError([e3, u4], 'a1', { cause: e2 }); + console.log('log1', a1); + t.is(a1.cause, e2); +}); diff --git a/packages/ses/test/error/test-aggregate-error-console.js b/packages/ses/test/error/test-aggregate-error-console.js new file mode 100644 index 0000000000..ba192546be --- /dev/null +++ b/packages/ses/test/error/test-aggregate-error-console.js @@ -0,0 +1,48 @@ +import test from 'ava'; +import '../../index.js'; +import { throwsAndLogs } from './throws-and-logs.js'; + +lockdown(); + +test('aggregate error console', t => { + if (typeof AggregateError === 'undefined') { + t.pass('skip test on platforms prior to AggregateError'); + return; + } + const e3 = Error('e3'); + const e2 = Error('e2', { cause: e3 }); + const u4 = URIError('u4', { cause: e2 }); + + const a1 = AggregateError([e3, u4], 'a1', { cause: e2 }); + throwsAndLogs( + t, + () => { + console.log('log1', a1); + throw a1; + }, + /a1/, + [ + ['log', 'log1', '(AggregateError#1)'], + ['log', 'AggregateError#1:', 'a1'], + ['log', 'stack of AggregateError\n'], + ['log', 'AggregateError#1 cause:', '(Error#2)'], + ['log', 'AggregateError#1 errors:', '(Error#3)', '(URIError#4)'], + ['group', 'Nested 3 errors under AggregateError#1'], + ['log', 'Error#2:', 'e2'], + ['log', 'stack of Error\n'], + ['log', 'Error#2 cause:', '(Error#3)'], + ['group', 'Nested error under Error#2'], + ['log', 'Error#3:', 'e3'], + ['log', 'stack of Error\n'], + ['groupEnd'], + ['log', 'URIError#4:', 'u4'], + ['log', 'stack of URIError\n'], + ['log', 'URIError#4 cause:', '(Error#2)'], + ['group', 'Nested error under URIError#4'], + ['groupEnd'], + ['groupEnd'], + ['log', 'Caught', '(AggregateError#1)'], + ], + { wrapWithCausal: true }, + ); +}); diff --git a/packages/ses/test/error/test-aggregate-error.js b/packages/ses/test/error/test-aggregate-error.js new file mode 100644 index 0000000000..5c0b0b2466 --- /dev/null +++ b/packages/ses/test/error/test-aggregate-error.js @@ -0,0 +1,54 @@ +import test from 'ava'; +import '../../index.js'; + +const { getOwnPropertyDescriptor } = Object; + +lockdown(); + +test('aggregate error', t => { + if (typeof AggregateError === 'undefined') { + t.pass('skip test on platforms prior to AggregateError'); + return; + } + const e1 = Error('e1'); + const e2 = Error('e2', { cause: e1 }); + const u3 = URIError('u3', { cause: e1 }); + + const a4 = AggregateError([e2, u3], 'a4', { cause: e1 }); + t.is(a4.message, 'a4'); + t.is(a4.cause, e1); + t.deepEqual(getOwnPropertyDescriptor(a4, 'cause'), { + value: e1, + writable: true, + enumerable: false, + configurable: true, + }); + t.deepEqual(getOwnPropertyDescriptor(a4, 'errors'), { + value: [e2, u3], + writable: true, + enumerable: false, + configurable: true, + }); +}); + +test('Promise.any aggregate error', async t => { + if (typeof AggregateError === 'undefined') { + t.pass('skip test on platforms prior to AggregateError'); + return; + } + const e1 = Error('e1'); + const e2 = Error('e2', { cause: e1 }); + const u3 = URIError('u3', { cause: e1 }); + + try { + await Promise.any([Promise.reject(e2), Promise.reject(u3)]); + } catch (a4) { + t.false('cause' in a4); + t.deepEqual(getOwnPropertyDescriptor(a4, 'errors'), { + value: [e2, u3], + writable: true, + enumerable: false, + configurable: true, + }); + } +}); diff --git a/packages/ses/test/error/test-error-cause-console.js b/packages/ses/test/error/test-error-cause-console.js new file mode 100644 index 0000000000..323558d229 --- /dev/null +++ b/packages/ses/test/error/test-error-cause-console.js @@ -0,0 +1,80 @@ +import test from 'ava'; +import '../../index.js'; +import { throwsAndLogs } from './throws-and-logs.js'; + +lockdown(); + +test('error cause console control', t => { + const e1 = Error('e1'); + throwsAndLogs( + t, + () => { + console.log('log1', e1); + throw e1; + }, + /e1/, + [ + ['log', 'log1', '(Error#1)'], + ['log', 'Error#1:', 'e1'], + ['log', 'stack of Error\n'], + ['log', 'Caught', '(Error#1)'], + ], + { wrapWithCausal: true }, + ); +}); + +test('error cause console one level', t => { + const e2 = Error('e2'); + const e1 = Error('e1', { cause: e2 }); + throwsAndLogs( + t, + () => { + console.log('log1', e1); + throw e1; + }, + /e1/, + [ + ['log', 'log1', '(Error#1)'], + ['log', 'Error#1:', 'e1'], + ['log', 'stack of Error\n'], + ['log', 'Error#1 cause:', '(Error#2)'], + ['group', 'Nested error under Error#1'], + ['log', 'Error#2:', 'e2'], + ['log', 'stack of Error\n'], + ['groupEnd'], + ['log', 'Caught', '(Error#1)'], + ], + { wrapWithCausal: true }, + ); +}); + +test('error cause console nested', t => { + const e3 = Error('e3'); + const e2 = Error('e2', { cause: e3 }); + const u1 = URIError('u1', { cause: e2 }); + throwsAndLogs( + t, + () => { + console.log('log1', u1); + throw u1; + }, + /u1/, + [ + ['log', 'log1', '(URIError#1)'], + ['log', 'URIError#1:', 'u1'], + ['log', 'stack of URIError\n'], + ['log', 'URIError#1 cause:', '(Error#2)'], + ['group', 'Nested error under URIError#1'], + ['log', 'Error#2:', 'e2'], + ['log', 'stack of Error\n'], + ['log', 'Error#2 cause:', '(Error#3)'], + ['group', 'Nested error under Error#2'], + ['log', 'Error#3:', 'e3'], + ['log', 'stack of Error\n'], + ['groupEnd'], + ['groupEnd'], + ['log', 'Caught', '(URIError#1)'], + ], + { wrapWithCausal: true }, + ); +}); diff --git a/packages/ses/test/error/test-error-cause.js b/packages/ses/test/error/test-error-cause.js new file mode 100644 index 0000000000..5cd32091f5 --- /dev/null +++ b/packages/ses/test/error/test-error-cause.js @@ -0,0 +1,43 @@ +import test from 'ava'; +import '../../index.js'; + +const { getOwnPropertyDescriptor } = Object; + +lockdown(); + +test('error cause', t => { + const e1 = Error('e1'); + t.is(e1.message, 'e1'); + t.false('cause' in e1); + const e2 = Error('e2', { cause: e1 }); + t.is(e2.message, 'e2'); + t.is(e2.cause, e1); + t.deepEqual(getOwnPropertyDescriptor(e2, 'cause'), { + value: e1, + writable: true, + enumerable: false, + configurable: true, + }); + const u3 = URIError('u3', { cause: e1 }); + t.is(u3.message, 'u3'); + t.is(u3.cause, e1); + t.deepEqual(getOwnPropertyDescriptor(u3, 'cause'), { + value: e1, + writable: true, + enumerable: false, + configurable: true, + }); + if (typeof AggregateError === 'undefined') { + t.pass('skip rest of test on platforms prior to AggregateError'); + return; + } + const a4 = AggregateError([e2, u3], 'a4', { cause: e1 }); + t.is(a4.message, 'a4'); + t.is(a4.cause, e1); + t.deepEqual(getOwnPropertyDescriptor(a4, 'cause'), { + value: e1, + writable: true, + enumerable: false, + configurable: true, + }); +}); diff --git a/packages/ses/test/test-get-global-intrinsics.js b/packages/ses/test/test-get-global-intrinsics.js index b17012e4b6..19b0531612 100644 --- a/packages/ses/test/test-get-global-intrinsics.js +++ b/packages/ses/test/test-get-global-intrinsics.js @@ -60,6 +60,8 @@ test.skip('getGlobalIntrinsics', () => { 'URIError', 'WeakMap', 'WeakSet', + // https://github.com/endojs/endo/issues/550 + 'AggregateError', // *** 18.4 Other Properties of the Global Object diff --git a/packages/ses/types.d.ts b/packages/ses/types.d.ts index 79fec88bcf..c198edd1ac 100644 --- a/packages/ses/types.d.ts +++ b/packages/ses/types.d.ts @@ -119,6 +119,8 @@ export type Details = string | DetailsToken; export interface AssertMakeErrorOptions { errorName?: string; + cause?: Error; + errors?: Error[]; } type AssertTypeofBigint = ( @@ -175,6 +177,10 @@ interface ToStringable { toString(): string; } +export type GenericErrorConstructor = + | ErrorConstructor + | AggregateErrorConstructor; + export type Raise = (reason: Error) => void; // Behold: recursion. // eslint-disable-next-line no-use-before-define @@ -184,23 +190,29 @@ export interface AssertionFunctions { ( value: any, details?: Details, - errorConstructor?: ErrorConstructor, + errConstructor?: GenericErrorConstructor, + options?: AssertMakeErrorOptions, ): asserts value; typeof: AssertTypeof; equal( left: any, right: any, details?: Details, - errorConstructor?: ErrorConstructor, + errConstructor?: GenericErrorConstructor, + options?: AssertMakeErrorOptions, ): void; string(specimen: any, details?: Details): asserts specimen is string; - fail(details?: Details, errorConstructor?: ErrorConstructor): never; + fail( + details?: Details, + errConstructor?: GenericErrorConstructor, + options?: AssertMakeErrorOptions, + ): never; } export interface AssertionUtilities { error( details?: Details, - errorConstructor?: ErrorConstructor, + errConstructor?: GenericErrorConstructor, options?: AssertMakeErrorOptions, ): Error; note(error: Error, details: Details): void;