Skip to content

Commit ef5d229

Browse files
authored
Test that DOMExceptions from WebGPU always have stacks (#3105)
1 parent 3dbe4ce commit ef5d229

File tree

10 files changed

+82
-33
lines changed

10 files changed

+82
-33
lines changed

src/common/framework/fixture.ts

+25-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { TestCaseRecorder } from '../internal/logging/test_case_recorder.js';
22
import { JSONWithUndefined } from '../internal/params_utils.js';
3-
import { assert, unreachable } from '../util/util.js';
3+
import { assert, ExceptionCheckOptions, unreachable } from '../util/util.js';
44

55
export class SkipTestCase extends Error {}
66
export class UnexpectedPassError extends Error {}
@@ -237,16 +237,26 @@ export class Fixture<S extends SubcaseBatchState = SubcaseBatchState> {
237237
}
238238

239239
/** Expect that the provided promise rejects, with the provided exception name. */
240-
shouldReject(expectedName: string, p: Promise<unknown>, msg?: string): void {
240+
shouldReject(
241+
expectedName: string,
242+
p: Promise<unknown>,
243+
{ allowMissingStack = false, message }: ExceptionCheckOptions = {}
244+
): void {
241245
this.eventualAsyncExpectation(async niceStack => {
242-
const m = msg ? ': ' + msg : '';
246+
const m = message ? ': ' + message : '';
243247
try {
244248
await p;
245249
niceStack.message = 'DID NOT REJECT' + m;
246250
this.rec.expectationFailed(niceStack);
247251
} catch (ex) {
248-
niceStack.message = 'rejected as expected' + m;
249252
this.expectErrorValue(expectedName, ex, niceStack);
253+
if (!allowMissingStack) {
254+
if (!(ex instanceof Error && typeof ex.stack === 'string')) {
255+
const exMessage = ex instanceof Error ? ex.message : '?';
256+
niceStack.message = `rejected as expected, but missing stack (${exMessage})${m}`;
257+
this.rec.expectationFailed(niceStack);
258+
}
259+
}
250260
}
251261
});
252262
}
@@ -257,8 +267,12 @@ export class Fixture<S extends SubcaseBatchState = SubcaseBatchState> {
257267
*
258268
* MAINTENANCE_TODO: Change to `string | false` so the exception name is always checked.
259269
*/
260-
shouldThrow(expectedError: string | boolean, fn: () => void, msg?: string): void {
261-
const m = msg ? ': ' + msg : '';
270+
shouldThrow(
271+
expectedError: string | boolean,
272+
fn: () => void,
273+
{ allowMissingStack = false, message }: ExceptionCheckOptions = {}
274+
) {
275+
const m = message ? ': ' + message : '';
262276
try {
263277
fn();
264278
if (expectedError === false) {
@@ -271,6 +285,11 @@ export class Fixture<S extends SubcaseBatchState = SubcaseBatchState> {
271285
this.rec.expectationFailed(new Error('threw unexpectedly' + m));
272286
} else {
273287
this.expectErrorValue(expectedError, ex, new Error(m));
288+
if (!allowMissingStack) {
289+
if (!(ex instanceof Error && typeof ex.stack === 'string')) {
290+
this.rec.expectationFailed(new Error('threw as expected, but missing stack' + m));
291+
}
292+
}
274293
}
275294
}
276295
}

src/common/util/util.ts

+17-3
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,29 @@ export function assertOK<T>(value: Error | T): T {
4747
return value;
4848
}
4949

50+
/** Options for assertReject, shouldReject, and friends. */
51+
export type ExceptionCheckOptions = { allowMissingStack?: boolean; message?: string };
52+
5053
/**
5154
* Resolves if the provided promise rejects; rejects if it does not.
5255
*/
53-
export async function assertReject(p: Promise<unknown>, msg?: string): Promise<void> {
56+
export async function assertReject(
57+
expectedName: string,
58+
p: Promise<unknown>,
59+
{ allowMissingStack = false, message }: ExceptionCheckOptions = {}
60+
): Promise<void> {
5461
try {
5562
await p;
56-
unreachable(msg);
63+
unreachable(message);
5764
} catch (ex) {
58-
// Assertion OK
65+
// Asserted as expected
66+
if (!allowMissingStack) {
67+
const m = message ? ` (${message})` : '';
68+
assert(
69+
ex instanceof Error && typeof ex.stack === 'string',
70+
'threw as expected, but missing stack' + m
71+
);
72+
}
5973
}
6074
}
6175

src/unittests/loaders_and_trees.spec.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -703,7 +703,10 @@ async function testIterateCollapsed(
703703
subqueriesToExpand: expectations,
704704
});
705705
if (expectedResult === 'throws') {
706-
t.shouldReject('Error', treePromise, 'loadTree should have thrown Error');
706+
t.shouldReject('Error', treePromise, {
707+
// Some errors here use StacklessError to print nicer command line outputs.
708+
allowMissingStack: true,
709+
});
707710
return;
708711
}
709712
const tree = await treePromise;

src/unittests/test_group.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ g.test('invalid_test_name').fn(t => {
206206
() => {
207207
g.test(name).fn(() => {});
208208
},
209-
name
209+
{ message: name }
210210
);
211211
}
212212
});

src/webgpu/api/operation/adapter/requestDevice.spec.ts

+2
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ g.test('stale')
118118
// Cause a type error by requesting with an unknown feature.
119119
if (awaitInitialError) {
120120
await assertReject(
121+
'TypeError',
121122
adapter.requestDevice({ requiredFeatures: ['unknown-feature' as GPUFeatureName] })
122123
);
123124
} else {
@@ -131,6 +132,7 @@ g.test('stale')
131132
// Cause an operation error by requesting with an alignment limit that is not a power of 2.
132133
if (awaitInitialError) {
133134
await assertReject(
135+
'OperationError',
134136
adapter.requestDevice({ requiredLimits: { minUniformBufferOffsetAlignment: 255 } })
135137
);
136138
} else {

src/webgpu/api/validation/buffer/mapping.spec.ts

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class F extends ValidationTest {
4545
assert(expectation.rejectName === null, 'mapAsync unexpectedly passed');
4646
} catch (ex) {
4747
assert(ex instanceof Error, 'mapAsync rejected with non-error');
48+
assert(typeof ex.stack === 'string', 'mapAsync rejected without a stack');
4849
assert(expectation.rejectName === ex.name, `mapAsync rejected unexpectedly with: ${ex}`);
4950
assert(
5051
expectation.earlyRejection === rejectedEarly,

src/webgpu/api/validation/capability_checks/features/texture_formats.spec.ts

+18-10
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,7 @@ g.test('color_target_state')
274274
)
275275
.params(u =>
276276
u
277+
.combine('isAsync', [false, true])
277278
.combine('format', kOptionalTextureFormats)
278279
.filter(t => !!kTextureFormatInfo[t.format].colorRender)
279280
.combine('enable_required_feature', [true, false])
@@ -287,10 +288,12 @@ g.test('color_target_state')
287288
}
288289
})
289290
.fn(t => {
290-
const { format, enable_required_feature } = t.params;
291+
const { isAsync, format, enable_required_feature } = t.params;
291292

292-
t.shouldThrow(enable_required_feature ? false : 'TypeError', () => {
293-
t.device.createRenderPipeline({
293+
t.doCreateRenderPipelineTest(
294+
isAsync,
295+
enable_required_feature,
296+
{
294297
layout: 'auto',
295298
vertex: {
296299
module: t.device.createShaderModule({
@@ -313,8 +316,9 @@ g.test('color_target_state')
313316
entryPoint: 'main',
314317
targets: [{ format }],
315318
},
316-
});
317-
});
319+
},
320+
'TypeError'
321+
);
318322
});
319323

320324
g.test('depth_stencil_state')
@@ -326,6 +330,7 @@ g.test('depth_stencil_state')
326330
)
327331
.params(u =>
328332
u
333+
.combine('isAsync', [false, true])
329334
.combine('format', kOptionalTextureFormats)
330335
.filter(t => !!(kTextureFormatInfo[t.format].depth || kTextureFormatInfo[t.format].stencil))
331336
.combine('enable_required_feature', [true, false])
@@ -339,10 +344,12 @@ g.test('depth_stencil_state')
339344
}
340345
})
341346
.fn(t => {
342-
const { format, enable_required_feature } = t.params;
347+
const { isAsync, format, enable_required_feature } = t.params;
343348

344-
t.shouldThrow(enable_required_feature ? false : 'TypeError', () => {
345-
t.device.createRenderPipeline({
349+
t.doCreateRenderPipelineTest(
350+
isAsync,
351+
enable_required_feature,
352+
{
346353
layout: 'auto',
347354
vertex: {
348355
module: t.device.createShaderModule({
@@ -370,8 +377,9 @@ g.test('depth_stencil_state')
370377
entryPoint: 'main',
371378
targets: [{ format: 'rgba8unorm' }],
372379
},
373-
});
374-
});
380+
},
381+
'TypeError'
382+
);
375383
});
376384

377385
g.test('render_bundle_encoder_descriptor_color_format')

src/webgpu/api/validation/capability_checks/limits/limit_utils.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,9 @@ export class LimitTestsImpl extends GPUTestBase {
330330
requiredFeatures?: GPUFeatureName[]
331331
) {
332332
if (shouldReject) {
333-
this.shouldReject('OperationError', adapter.requestDevice({ requiredLimits }));
333+
this.shouldReject('OperationError', adapter.requestDevice({ requiredLimits }), {
334+
allowMissingStack: true,
335+
});
334336
return undefined;
335337
} else {
336338
return await adapter.requestDevice({ requiredLimits, requiredFeatures });
@@ -562,12 +564,12 @@ export class LimitTestsImpl extends GPUTestBase {
562564
expectedName: string,
563565
p: Promise<unknown>,
564566
shouldReject: boolean,
565-
msg?: string
567+
message?: string
566568
): Promise<void> {
567569
if (shouldReject) {
568-
this.shouldReject(expectedName, p, msg);
570+
this.shouldReject(expectedName, p, { message });
569571
} else {
570-
this.shouldResolve(p, msg);
572+
this.shouldResolve(p, message);
571573
}
572574

573575
// We need to explicitly wait for the promise because the device may be

src/webgpu/examples.spec.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ g.test('basic').fn(t => {
4747
throw new TypeError();
4848
},
4949
// Log message.
50-
'function should throw Error'
50+
{ message: 'function should throw Error' }
5151
);
5252
});
5353

@@ -59,17 +59,17 @@ g.test('basic,async').fn(t => {
5959
// Promise expected to reject.
6060
Promise.reject(new TypeError()),
6161
// Log message.
62-
'Promise.reject should reject'
62+
{ message: 'Promise.reject should reject' }
6363
);
6464

65-
// Promise can also be an IIFE.
65+
// Promise can also be an IIFE (immediately-invoked function expression).
6666
t.shouldReject(
6767
'TypeError',
6868
// eslint-disable-next-line @typescript-eslint/require-await
6969
(async () => {
7070
throw new TypeError();
7171
})(),
72-
'Promise.reject should reject'
72+
{ message: 'Promise.reject should reject' }
7373
);
7474
});
7575

src/webgpu/util/device_pool.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -378,10 +378,10 @@ class DeviceHolder implements DeviceProvider {
378378
await this.device.queue.onSubmittedWorkDone();
379379
}
380380

381-
await assertReject(
382-
this.device.popErrorScope(),
383-
'There was an extra error scope on the stack after a test'
384-
);
381+
await assertReject('OperationError', this.device.popErrorScope(), {
382+
allowMissingStack: true,
383+
message: 'There was an extra error scope on the stack after a test',
384+
});
385385

386386
if (gpuOutOfMemoryError !== null) {
387387
assert(gpuOutOfMemoryError instanceof GPUOutOfMemoryError);

0 commit comments

Comments
 (0)