diff options
Diffstat (limited to 'dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/error_scope.spec.ts')
-rw-r--r-- | dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/error_scope.spec.ts | 283 |
1 files changed, 283 insertions, 0 deletions
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/error_scope.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/error_scope.spec.ts new file mode 100644 index 0000000000..7a023f770e --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/error_scope.spec.ts @@ -0,0 +1,283 @@ +export const description = ` +Error scope validation tests. + +Note these must create their own device, not use GPUTest (that one already has error scopes on it). + +TODO: (POSTV1) Test error scopes of different threads and make sure they go to the right place. +TODO: (POSTV1) Test that unhandled errors go the right device, and nowhere if the device was dropped. +`; + +import { Fixture } from '../../../common/framework/fixture.js'; +import { makeTestGroup } from '../../../common/framework/test_group.js'; +import { getGPU } from '../../../common/util/navigator_gpu.js'; +import { assert, raceWithRejectOnTimeout } from '../../../common/util/util.js'; +import { kErrorScopeFilters, kGeneratableErrorScopeFilters } from '../../capability_info.js'; +import { kMaxUnsignedLongLongValue } from '../../constants.js'; + +class ErrorScopeTests extends Fixture { + _device: GPUDevice | undefined = undefined; + + get device(): GPUDevice { + assert(this._device !== undefined); + return this._device; + } + + async init(): Promise<void> { + await super.init(); + const gpu = getGPU(); + const adapter = await gpu.requestAdapter(); + assert(adapter !== null); + const device = await adapter.requestDevice(); + assert(device !== null); + this._device = device; + } + + // Generates an error of the given filter type. For now, the errors are generated by calling a + // known code-path to cause the error. This can be updated in the future should there be a more + // direct way to inject errors. + generateError(filter: GPUErrorFilter): void { + switch (filter) { + case 'out-of-memory': + // Generating an out-of-memory error by allocating a massive buffer. + this.device.createBuffer({ + size: kMaxUnsignedLongLongValue, // Unrealistically massive buffer size + usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST, + }); + break; + case 'validation': + // Generating a validation error by passing in an invalid usage when creating a buffer. + this.device.createBuffer({ + size: 1024, + usage: 0xffff, // Invalid GPUBufferUsage + }); + break; + } + // MAINTENANCE_TODO: This is a workaround for Chromium not flushing. Remove when not needed. + this.device.queue.submit([]); + } + + // Checks whether the error is of the type expected given the filter. + isInstanceOfError(filter: GPUErrorFilter, error: GPUError | null): boolean { + switch (filter) { + case 'out-of-memory': + return error instanceof GPUOutOfMemoryError; + case 'validation': + return error instanceof GPUValidationError; + case 'internal': + return error instanceof GPUInternalError; + } + } + + // Expect an uncapturederror event to occur. Note: this MUST be awaited, because + // otherwise it could erroneously pass by capturing an error from later in the test. + async expectUncapturedError(fn: Function): Promise<GPUUncapturedErrorEvent> { + return this.immediateAsyncExpectation(() => { + // MAINTENANCE_TODO: Make arbitrary timeout value a test runner variable + const TIMEOUT_IN_MS = 1000; + + const promise: Promise<GPUUncapturedErrorEvent> = new Promise(resolve => { + const eventListener = ((event: GPUUncapturedErrorEvent) => { + this.debug(`Got uncaptured error event with ${event.error}`); + resolve(event); + }) as EventListener; + + this.device.addEventListener('uncapturederror', eventListener, { once: true }); + }); + + fn(); + + return raceWithRejectOnTimeout( + promise, + TIMEOUT_IN_MS, + 'Timeout occurred waiting for uncaptured error' + ); + }); + } +} + +export const g = makeTestGroup(ErrorScopeTests); + +g.test('simple') + .desc( + ` +Tests that error scopes catches their expected errors, firing an uncaptured error event otherwise. + +- Same error and error filter (popErrorScope should return the error) +- Different error from filter (uncaptured error should result) + ` + ) + .params(u => + u.combine('errorType', kGeneratableErrorScopeFilters).combine('errorFilter', kErrorScopeFilters) + ) + .fn(async t => { + const { errorType, errorFilter } = t.params; + t.device.pushErrorScope(errorFilter); + + if (errorType !== errorFilter) { + // Different error case + const uncapturedErrorEvent = await t.expectUncapturedError(() => { + t.generateError(errorType); + }); + t.expect(t.isInstanceOfError(errorType, uncapturedErrorEvent.error)); + + const error = await t.device.popErrorScope(); + t.expect(error === null); + } else { + // Same error as filter + t.generateError(errorType); + const error = await t.device.popErrorScope(); + t.expect(t.isInstanceOfError(errorType, error)); + } + }); + +g.test('empty') + .desc( + ` +Tests that popping an empty error scope stack should reject. + ` + ) + .fn(async t => { + const promise = t.device.popErrorScope(); + t.shouldReject('OperationError', promise); + }); + +g.test('parent_scope') + .desc( + ` +Tests that an error bubbles to the correct parent scope. + +- Different error types as the parent scope +- Different depths of non-capturing filters for the generated error + ` + ) + .params(u => + u + .combine('errorFilter', kGeneratableErrorScopeFilters) + .combine('stackDepth', [1, 10, 100, 1000]) + ) + .fn(async t => { + const { errorFilter, stackDepth } = t.params; + t.device.pushErrorScope(errorFilter); + + // Push a bunch of error filters onto the stack (none that match errorFilter) + const unmatchedFilters = kErrorScopeFilters.filter(filter => { + return filter !== errorFilter; + }); + for (let i = 0; i < stackDepth; i++) { + t.device.pushErrorScope(unmatchedFilters[i % unmatchedFilters.length]); + } + + // Cause the error and then pop all the unrelated filters. + t.generateError(errorFilter); + const promises = []; + for (let i = 0; i < stackDepth; i++) { + promises.push(t.device.popErrorScope()); + } + const errors = await Promise.all(promises); + t.expect(errors.every(e => e === null)); + + // Finally the actual error should have been caught by the parent scope. + const error = await t.device.popErrorScope(); + t.expect(t.isInstanceOfError(errorFilter, error)); + }); + +g.test('current_scope') + .desc( + ` +Tests that an error does not bubbles to parent scopes when local scope matches. + +- Different error types as the current scope +- Different depths of non-capturing filters for the generated error + ` + ) + .params(u => + u + .combine('errorFilter', kGeneratableErrorScopeFilters) + .combine('stackDepth', [1, 10, 100, 1000, 100000]) + ) + .fn(async t => { + const { errorFilter, stackDepth } = t.params; + + // Push a bunch of error filters onto the stack + for (let i = 0; i < stackDepth; i++) { + t.device.pushErrorScope(kErrorScopeFilters[i % kErrorScopeFilters.length]); + } + + // Current scope should catch the error immediately. + t.device.pushErrorScope(errorFilter); + t.generateError(errorFilter); + const error = await t.device.popErrorScope(); + t.expect(t.isInstanceOfError(errorFilter, error)); + + // Remaining scopes shouldn't catch anything. + const promises = []; + for (let i = 0; i < stackDepth; i++) { + promises.push(t.device.popErrorScope()); + } + const errors = await Promise.all(promises); + t.expect(errors.every(e => e === null)); + }); + +g.test('balanced_siblings') + .desc( + ` +Tests that sibling error scopes need to be balanced. + +- Different error types as the current scope +- Different number of sibling errors + ` + ) + .params(u => + u.combine('errorFilter', kErrorScopeFilters).combine('numErrors', [1, 10, 100, 1000]) + ) + .fn(async t => { + const { errorFilter, numErrors } = t.params; + + const promises = []; + for (let i = 0; i < numErrors; i++) { + t.device.pushErrorScope(errorFilter); + promises.push(t.device.popErrorScope()); + } + + { + // Trying to pop an additional non-exisiting scope should reject. + const promise = t.device.popErrorScope(); + t.shouldReject('OperationError', promise); + } + + const errors = await Promise.all(promises); + t.expect(errors.every(e => e === null)); + }); + +g.test('balanced_nesting') + .desc( + ` +Tests that nested error scopes need to be balanced. + +- Different error types as the current scope +- Different number of nested errors + ` + ) + .params(u => + u.combine('errorFilter', kErrorScopeFilters).combine('numErrors', [1, 10, 100, 1000]) + ) + .fn(async t => { + const { errorFilter, numErrors } = t.params; + + for (let i = 0; i < numErrors; i++) { + t.device.pushErrorScope(errorFilter); + } + + const promises = []; + for (let i = 0; i < numErrors; i++) { + promises.push(t.device.popErrorScope()); + } + const errors = await Promise.all(promises); + t.expect(errors.every(e => e === null)); + + { + // Trying to pop an additional non-exisiting scope should reject. + const promise = t.device.popErrorScope(); + t.shouldReject('OperationError', promise); + } + }); |