Skip to content

Commit

Permalink
Test that DOMExceptions from WebGPU always have stacks
Browse files Browse the repository at this point in the history
Issue: 3103
  • Loading branch information
kainino0x committed Oct 27, 2023
1 parent 6e21caa commit 32d4eba
Show file tree
Hide file tree
Showing 21 changed files with 363 additions and 194 deletions.
31 changes: 25 additions & 6 deletions src/common/framework/fixture.ts
Original file line number Diff line number Diff line change
@@ -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 {}
Expand Down Expand Up @@ -237,16 +237,26 @@ export class Fixture<S extends SubcaseBatchState = SubcaseBatchState> {
}

/** Expect that the provided promise rejects, with the provided exception name. */
shouldReject(expectedName: string, p: Promise<unknown>, msg?: string): void {
shouldReject(
expectedName: string,
p: Promise<unknown>,
{ checkForStackProperty = 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;
niceStack.message = 'rejected as expected, but wrong name' + m;
this.expectErrorValue(expectedName, ex, niceStack);
if (checkForStackProperty) {
if (!(ex instanceof Error && typeof ex.stack === 'string')) {
niceStack.message = 'rejected as expected, but missing stack' + m;
this.rec.expectationFailed(niceStack);
}
}
}
});
}
Expand All @@ -257,8 +267,12 @@ export class Fixture<S extends SubcaseBatchState = SubcaseBatchState> {
*
* 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,
{ checkForStackProperty = false, message }: ExceptionCheckOptions = {}
) {
const m = message ? ': ' + message : '';
try {
fn();
if (expectedError === false) {
Expand All @@ -271,6 +285,11 @@ export class Fixture<S extends SubcaseBatchState = SubcaseBatchState> {
this.rec.expectationFailed(new Error('threw unexpectedly' + m));
} else {
this.expectErrorValue(expectedError, ex, new Error(m));
if (checkForStackProperty) {
if (!(ex instanceof Error && typeof ex.stack === 'string')) {
this.rec.expectationFailed(new Error('missing stack' + m));
}
}
}
}
}
Expand Down
17 changes: 14 additions & 3 deletions src/common/util/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,26 @@ export function assertOK<T>(value: Error | T): T {
return value;
}

/** Options for assertReject, shouldReject, and friends. */
export type ExceptionCheckOptions = { checkForStackProperty?: boolean; message?: string };

/**
* Resolves if the provided promise rejects; rejects if it does not.
*/
export async function assertReject(p: Promise<unknown>, msg?: string): Promise<void> {
export async function assertReject(
expectedName: string,
p: Promise<unknown>,
{ checkForStackProperty = false, message }: ExceptionCheckOptions = {}
): Promise<void> {
try {
await p;
unreachable(msg);
unreachable(message);
} catch (ex) {
// Assertion OK
// Asserted as expected
if (checkForStackProperty) {
const m = message ? ` (${message})` : '';
assert(ex instanceof Error && typeof ex.stack === 'string', 'missing stack property' + m);
}
}
}

Expand Down
14 changes: 9 additions & 5 deletions src/stress/memory/oom.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,10 @@ mapAsync on it should reject and produce a validation error. `
if (unmapBeforeResolve) {
// Should reject with abort error because buffer will be unmapped
// before validation check finishes.
t.shouldReject('AbortError', promise!);
t.shouldReject('AbortError', promise!, { checkForStackProperty: true });
} else {
// Should also reject in addition to the validation error.
t.shouldReject('OperationError', promise!);
t.shouldReject('OperationError', promise!, { checkForStackProperty: true });

// Wait for validation error before unmap to ensure validation check
// ends before unmap.
Expand All @@ -151,9 +151,13 @@ mapAsync on it should reject and produce a validation error. `

// Should throw an OperationError because the buffer is not mapped.
// Note: not a RangeError because the state of the buffer is checked first.
t.shouldThrow('OperationError', () => {
errorBuffer.getMappedRange();
});
t.shouldThrow(
'OperationError',
() => {
errorBuffer.getMappedRange();
},
{ checkForStackProperty: true }
);

// Should't be a validation error even if the buffer failed to be mapped.
errorBuffer.unmap();
Expand Down
2 changes: 1 addition & 1 deletion src/unittests/loaders_and_trees.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -703,7 +703,7 @@ async function testIterateCollapsed(
subqueriesToExpand: expectations,
});
if (expectedResult === 'throws') {
t.shouldReject('Error', treePromise, 'loadTree should have thrown Error');
t.shouldReject('Error', treePromise);
return;
}
const tree = await treePromise;
Expand Down
8 changes: 5 additions & 3 deletions src/unittests/test_group.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ g.test('invalid_test_name').fn(t => {
() => {
g.test(name).fn(() => {});
},
name
{ message: name }
);
}
});
Expand Down Expand Up @@ -353,7 +353,8 @@ g.test('shouldReject').fn(async t0 => {
'TypeError',
(async () => {
throw new TypeError();
})()
})(),
{ checkForStackProperty: true }
);

const g = makeTestGroupForUnitTesting(UnitTest);
Expand All @@ -363,7 +364,8 @@ g.test('shouldReject').fn(async t0 => {
'Error',
(async () => {
throw new TypeError();
})()
})(),
{ checkForStackProperty: true }
);
});

Expand Down
31 changes: 22 additions & 9 deletions src/webgpu/api/operation/adapter/requestDevice.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,25 +118,31 @@ g.test('stale')
// Cause a type error by requesting with an unknown feature.
if (awaitInitialError) {
await assertReject(
adapter.requestDevice({ requiredFeatures: ['unknown-feature' as GPUFeatureName] })
'TypeError',
adapter.requestDevice({ requiredFeatures: ['unknown-feature' as GPUFeatureName] }),
{ checkForStackProperty: true }
);
} else {
t.shouldReject(
'TypeError',
adapter.requestDevice({ requiredFeatures: ['unknown-feature' as GPUFeatureName] })
adapter.requestDevice({ requiredFeatures: ['unknown-feature' as GPUFeatureName] }),
{ checkForStackProperty: true }
);
}
break;
case 'OperationError':
// Cause an operation error by requesting with an alignment limit that is not a power of 2.
if (awaitInitialError) {
await assertReject(
adapter.requestDevice({ requiredLimits: { minUniformBufferOffsetAlignment: 255 } })
'OperationError',
adapter.requestDevice({ requiredLimits: { minUniformBufferOffsetAlignment: 255 } }),
{ checkForStackProperty: true }
);
} else {
t.shouldReject(
'OperationError',
adapter.requestDevice({ requiredLimits: { minUniformBufferOffsetAlignment: 255 } })
adapter.requestDevice({ requiredLimits: { minUniformBufferOffsetAlignment: 255 } }),
{ checkForStackProperty: true }
);
}
break;
Expand Down Expand Up @@ -184,7 +190,8 @@ g.test('features,unknown')

t.shouldReject(
'TypeError',
adapter.requestDevice({ requiredFeatures: ['unknown-feature' as GPUFeatureName] })
adapter.requestDevice({ requiredFeatures: ['unknown-feature' as GPUFeatureName] }),
{ checkForStackProperty: true }
);
});

Expand All @@ -208,7 +215,7 @@ g.test('features,known')
const device = await promise;
t.expect(device.features.has(feature), 'Device should include the required feature');
} else {
t.shouldReject('TypeError', promise);
t.shouldReject('TypeError', promise, { checkForStackProperty: true });
}
});

Expand All @@ -225,7 +232,9 @@ g.test('limits,unknown')

const requiredLimits: Record<string, number> = { unknownLimitName: 9000 };

t.shouldReject('OperationError', adapter.requestDevice({ requiredLimits }));
t.shouldReject('OperationError', adapter.requestDevice({ requiredLimits }), {
checkForStackProperty: true,
});
});

g.test('limits,supported')
Expand Down Expand Up @@ -306,7 +315,9 @@ g.test('limit,better_than_supported')
[limit]: clamp(value, { min: 0, max: limitInfo[limit].maximumValue }),
};

t.shouldReject('OperationError', adapter.requestDevice({ requiredLimits }));
t.shouldReject('OperationError', adapter.requestDevice({ requiredLimits }), {
checkForStackProperty: true,
});
});

g.test('limit,worse_than_default')
Expand Down Expand Up @@ -369,6 +380,8 @@ g.test('limit,worse_than_default')
);
device.destroy();
} else {
t.shouldReject('OperationError', adapter.requestDevice({ requiredLimits }));
t.shouldReject('OperationError', adapter.requestDevice({ requiredLimits }), {
checkForStackProperty: true,
});
}
});
8 changes: 6 additions & 2 deletions src/webgpu/api/operation/buffers/map_ArrayBuffer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ g.test('postMessage')
});

if (transfer) {
t.shouldThrow('TypeError', () => mc.port1.postMessage(ab1, [ab1]));
t.shouldThrow('TypeError', () => mc.port1.postMessage(ab1, [ab1]), {
checkForStackProperty: true,
});
// Wait to make sure the postMessage isn't received.
await new Promise(resolve => timeout(resolve, 100));
} else {
Expand Down Expand Up @@ -85,5 +87,7 @@ g.test('postMessage')
t.expect(ab1.byteLength === 0, 'after unmap, ab1 should be detached');

// Transferring an already-detached ArrayBuffer is a DataCloneError.
t.shouldThrow('DataCloneError', () => mc.port1.postMessage(ab1, [ab1]));
t.shouldThrow('DataCloneError', () => mc.port1.postMessage(ab1, [ab1]), {
checkForStackProperty: true,
});
});
2 changes: 1 addition & 1 deletion src/webgpu/api/operation/buffers/map_oom.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ g.test('mappedAtCreation')
if (oom) {
// getMappedRange is normally valid on OOM buffers, but this one fails because the
// (default) range is too large to create the returned ArrayBuffer.
t.shouldThrow('RangeError', f);
t.shouldThrow('RangeError', f, { checkForStackProperty: true });
} else {
const buffer = f();
const mapping = buffer.getMappedRange();
Expand Down
11 changes: 8 additions & 3 deletions src/webgpu/api/validation/buffer/mapping.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -62,9 +63,13 @@ class F extends ValidationTest {
this.expect(data.byteLength === size);
}
} else {
this.shouldThrow('OperationError', () => {
buffer.getMappedRange(offset, size);
});
this.shouldThrow(
'OperationError',
() => {
buffer.getMappedRange(offset, size);
},
{ checkForStackProperty: true }
);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,13 @@ g.test('createQuerySet')
const count = 1;
const shouldException = type === 'timestamp' && !featureContainsTimestampQuery;

t.shouldThrow(shouldException ? 'TypeError' : false, () => {
t.device.createQuerySet({ type, count });
});
t.shouldThrow(
shouldException ? 'TypeError' : false,
() => {
t.device.createQuerySet({ type, count });
},
{ checkForStackProperty: true }
);
});

g.test('writeTimestamp')
Expand Down Expand Up @@ -70,7 +74,11 @@ g.test('writeTimestamp')
});
const encoder = t.createEncoder('non-pass');

t.shouldThrow(featureContainsTimestampQuery ? false : 'TypeError', () => {
encoder.encoder.writeTimestamp(querySet, 0);
});
t.shouldThrow(
featureContainsTimestampQuery ? false : 'TypeError',
() => {
encoder.encoder.writeTimestamp(querySet, 0);
},
{ checkForStackProperty: true }
);
});
Loading

0 comments on commit 32d4eba

Please sign in to comment.