summaryrefslogtreecommitdiffstats
path: root/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/error_scope.spec.ts
blob: 7a023f770e687f9f551f86e2bae637ed6ce13b01 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
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);
    }
  });