diff --git a/src/common/framework/fixture.ts b/src/common/framework/fixture.ts index 381d60ba047a..795532406bd2 100644 --- a/src/common/framework/fixture.ts +++ b/src/common/framework/fixture.ts @@ -1,6 +1,6 @@ import { TestCaseRecorder } from '../internal/logging/test_case_recorder.js'; import { JSONWithUndefined } from '../internal/params_utils.js'; -import { assert, unreachable } from '../util/util.js'; +import { assert, ExceptionCheckOptions, unreachable } from '../util/util.js'; export class SkipTestCase extends Error {} export class UnexpectedPassError extends Error {} @@ -237,16 +237,26 @@ export class Fixture { } /** Expect that the provided promise rejects, with the provided exception name. */ - shouldReject(expectedName: string, p: Promise, msg?: string): void { + shouldReject( + expectedName: string, + p: Promise, + { allowMissingStack = false, message }: ExceptionCheckOptions = {} + ): void { this.eventualAsyncExpectation(async niceStack => { - const m = msg ? ': ' + msg : ''; + const m = message ? ': ' + message : ''; try { await p; niceStack.message = 'DID NOT REJECT' + m; this.rec.expectationFailed(niceStack); } catch (ex) { - niceStack.message = 'rejected as expected' + m; this.expectErrorValue(expectedName, ex, niceStack); + if (!allowMissingStack) { + if (!(ex instanceof Error && typeof ex.stack === 'string')) { + const exMessage = ex instanceof Error ? ex.message : '?'; + niceStack.message = `rejected as expected, but missing stack (${exMessage})${m}`; + this.rec.expectationFailed(niceStack); + } + } } }); } @@ -257,8 +267,12 @@ export class Fixture { * * MAINTENANCE_TODO: Change to `string | false` so the exception name is always checked. */ - shouldThrow(expectedError: string | boolean, fn: () => void, msg?: string): void { - const m = msg ? ': ' + msg : ''; + shouldThrow( + expectedError: string | boolean, + fn: () => void, + { allowMissingStack = false, message }: ExceptionCheckOptions = {} + ) { + const m = message ? ': ' + message : ''; try { fn(); if (expectedError === false) { @@ -271,6 +285,11 @@ export class Fixture { this.rec.expectationFailed(new Error('threw unexpectedly' + m)); } else { this.expectErrorValue(expectedError, ex, new Error(m)); + if (!allowMissingStack) { + if (!(ex instanceof Error && typeof ex.stack === 'string')) { + this.rec.expectationFailed(new Error('threw as expected, but missing stack' + m)); + } + } } } } diff --git a/src/common/util/util.ts b/src/common/util/util.ts index be109fc9d422..1da380036382 100644 --- a/src/common/util/util.ts +++ b/src/common/util/util.ts @@ -47,15 +47,29 @@ export function assertOK(value: Error | T): T { return value; } +/** Options for assertReject, shouldReject, and friends. */ +export type ExceptionCheckOptions = { allowMissingStack?: boolean; message?: string }; + /** * Resolves if the provided promise rejects; rejects if it does not. */ -export async function assertReject(p: Promise, msg?: string): Promise { +export async function assertReject( + expectedName: string, + p: Promise, + { allowMissingStack = false, message }: ExceptionCheckOptions = {} +): Promise { try { await p; - unreachable(msg); + unreachable(message); } catch (ex) { - // Assertion OK + // Asserted as expected + if (!allowMissingStack) { + const m = message ? ` (${message})` : ''; + assert( + ex instanceof Error && typeof ex.stack === 'string', + 'threw as expected, but missing stack' + m + ); + } } } diff --git a/src/unittests/loaders_and_trees.spec.ts b/src/unittests/loaders_and_trees.spec.ts index c7ff1fa43a84..b396b0525940 100644 --- a/src/unittests/loaders_and_trees.spec.ts +++ b/src/unittests/loaders_and_trees.spec.ts @@ -703,7 +703,10 @@ async function testIterateCollapsed( subqueriesToExpand: expectations, }); if (expectedResult === 'throws') { - t.shouldReject('Error', treePromise, 'loadTree should have thrown Error'); + t.shouldReject('Error', treePromise, { + // Some errors here use StacklessError to print nicer command line outputs. + allowMissingStack: true, + }); return; } const tree = await treePromise; diff --git a/src/unittests/test_group.spec.ts b/src/unittests/test_group.spec.ts index 8e1129411c74..526f577c9edf 100644 --- a/src/unittests/test_group.spec.ts +++ b/src/unittests/test_group.spec.ts @@ -206,7 +206,7 @@ g.test('invalid_test_name').fn(t => { () => { g.test(name).fn(() => {}); }, - name + { message: name } ); } }); diff --git a/src/webgpu/api/operation/adapter/requestDevice.spec.ts b/src/webgpu/api/operation/adapter/requestDevice.spec.ts index 7d930a5e19df..314da6356eb7 100644 --- a/src/webgpu/api/operation/adapter/requestDevice.spec.ts +++ b/src/webgpu/api/operation/adapter/requestDevice.spec.ts @@ -118,6 +118,7 @@ g.test('stale') // Cause a type error by requesting with an unknown feature. if (awaitInitialError) { await assertReject( + 'TypeError', adapter.requestDevice({ requiredFeatures: ['unknown-feature' as GPUFeatureName] }) ); } else { @@ -131,6 +132,7 @@ g.test('stale') // Cause an operation error by requesting with an alignment limit that is not a power of 2. if (awaitInitialError) { await assertReject( + 'OperationError', adapter.requestDevice({ requiredLimits: { minUniformBufferOffsetAlignment: 255 } }) ); } else { diff --git a/src/webgpu/api/validation/buffer/mapping.spec.ts b/src/webgpu/api/validation/buffer/mapping.spec.ts index c6f3c782af6d..58d7f2767aee 100644 --- a/src/webgpu/api/validation/buffer/mapping.spec.ts +++ b/src/webgpu/api/validation/buffer/mapping.spec.ts @@ -45,6 +45,7 @@ class F extends ValidationTest { assert(expectation.rejectName === null, 'mapAsync unexpectedly passed'); } catch (ex) { assert(ex instanceof Error, 'mapAsync rejected with non-error'); + assert(typeof ex.stack === 'string', 'mapAsync rejected without a stack'); assert(expectation.rejectName === ex.name, `mapAsync rejected unexpectedly with: ${ex}`); assert( expectation.earlyRejection === rejectedEarly, diff --git a/src/webgpu/api/validation/capability_checks/features/texture_formats.spec.ts b/src/webgpu/api/validation/capability_checks/features/texture_formats.spec.ts index 8654bc6feba6..eb7005dd29d0 100644 --- a/src/webgpu/api/validation/capability_checks/features/texture_formats.spec.ts +++ b/src/webgpu/api/validation/capability_checks/features/texture_formats.spec.ts @@ -274,6 +274,7 @@ g.test('color_target_state') ) .params(u => u + .combine('isAsync', [false, true]) .combine('format', kOptionalTextureFormats) .filter(t => !!kTextureFormatInfo[t.format].colorRender) .combine('enable_required_feature', [true, false]) @@ -287,10 +288,12 @@ g.test('color_target_state') } }) .fn(t => { - const { format, enable_required_feature } = t.params; + const { isAsync, format, enable_required_feature } = t.params; - t.shouldThrow(enable_required_feature ? false : 'TypeError', () => { - t.device.createRenderPipeline({ + t.doCreateRenderPipelineTest( + isAsync, + enable_required_feature, + { layout: 'auto', vertex: { module: t.device.createShaderModule({ @@ -313,8 +316,9 @@ g.test('color_target_state') entryPoint: 'main', targets: [{ format }], }, - }); - }); + }, + 'TypeError' + ); }); g.test('depth_stencil_state') @@ -326,6 +330,7 @@ g.test('depth_stencil_state') ) .params(u => u + .combine('isAsync', [false, true]) .combine('format', kOptionalTextureFormats) .filter(t => !!(kTextureFormatInfo[t.format].depth || kTextureFormatInfo[t.format].stencil)) .combine('enable_required_feature', [true, false]) @@ -339,10 +344,12 @@ g.test('depth_stencil_state') } }) .fn(t => { - const { format, enable_required_feature } = t.params; + const { isAsync, format, enable_required_feature } = t.params; - t.shouldThrow(enable_required_feature ? false : 'TypeError', () => { - t.device.createRenderPipeline({ + t.doCreateRenderPipelineTest( + isAsync, + enable_required_feature, + { layout: 'auto', vertex: { module: t.device.createShaderModule({ @@ -370,8 +377,9 @@ g.test('depth_stencil_state') entryPoint: 'main', targets: [{ format: 'rgba8unorm' }], }, - }); - }); + }, + 'TypeError' + ); }); g.test('render_bundle_encoder_descriptor_color_format') diff --git a/src/webgpu/api/validation/capability_checks/limits/limit_utils.ts b/src/webgpu/api/validation/capability_checks/limits/limit_utils.ts index f6b0f96aa805..47a5a468d7d1 100644 --- a/src/webgpu/api/validation/capability_checks/limits/limit_utils.ts +++ b/src/webgpu/api/validation/capability_checks/limits/limit_utils.ts @@ -330,7 +330,9 @@ export class LimitTestsImpl extends GPUTestBase { requiredFeatures?: GPUFeatureName[] ) { if (shouldReject) { - this.shouldReject('OperationError', adapter.requestDevice({ requiredLimits })); + this.shouldReject('OperationError', adapter.requestDevice({ requiredLimits }), { + allowMissingStack: true, + }); return undefined; } else { return await adapter.requestDevice({ requiredLimits, requiredFeatures }); @@ -562,12 +564,12 @@ export class LimitTestsImpl extends GPUTestBase { expectedName: string, p: Promise, shouldReject: boolean, - msg?: string + message?: string ): Promise { if (shouldReject) { - this.shouldReject(expectedName, p, msg); + this.shouldReject(expectedName, p, { message }); } else { - this.shouldResolve(p, msg); + this.shouldResolve(p, message); } // We need to explicitly wait for the promise because the device may be diff --git a/src/webgpu/examples.spec.ts b/src/webgpu/examples.spec.ts index 35969543741f..4864393eca91 100644 --- a/src/webgpu/examples.spec.ts +++ b/src/webgpu/examples.spec.ts @@ -47,7 +47,7 @@ g.test('basic').fn(t => { throw new TypeError(); }, // Log message. - 'function should throw Error' + { message: 'function should throw Error' } ); }); @@ -59,17 +59,17 @@ g.test('basic,async').fn(t => { // Promise expected to reject. Promise.reject(new TypeError()), // Log message. - 'Promise.reject should reject' + { message: 'Promise.reject should reject' } ); - // Promise can also be an IIFE. + // Promise can also be an IIFE (immediately-invoked function expression). t.shouldReject( 'TypeError', // eslint-disable-next-line @typescript-eslint/require-await (async () => { throw new TypeError(); })(), - 'Promise.reject should reject' + { message: 'Promise.reject should reject' } ); }); diff --git a/src/webgpu/util/device_pool.ts b/src/webgpu/util/device_pool.ts index e8584df19613..843d6dc83e1a 100644 --- a/src/webgpu/util/device_pool.ts +++ b/src/webgpu/util/device_pool.ts @@ -378,10 +378,10 @@ class DeviceHolder implements DeviceProvider { await this.device.queue.onSubmittedWorkDone(); } - await assertReject( - this.device.popErrorScope(), - 'There was an extra error scope on the stack after a test' - ); + await assertReject('OperationError', this.device.popErrorScope(), { + allowMissingStack: true, + message: 'There was an extra error scope on the stack after a test', + }); if (gpuOutOfMemoryError !== null) { assert(gpuOutOfMemoryError instanceof GPUOutOfMemoryError);