• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

visgl / luma.gl / 23412192171

22 Mar 2026 08:49PM UTC coverage: 73.59% (-0.6%) from 74.227%
23412192171

Pull #2439

github

web-flow
Merge 99091cdc8 into 7c172e633
Pull Request #2439: feat(engine): add async texture buffer read

4597 of 7074 branches covered (64.98%)

Branch coverage included in aggregate %.

111 of 213 new or added lines in 20 files covered. (52.11%)

40 existing lines in 8 files now uncovered.

10525 of 13475 relevant lines covered (78.11%)

263.46 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

73.63
/modules/test-utils/src/create-test-device.ts
1
// luma.gl
2
// SPDX-License-Identifier: MIT
3
// Copyright (c) vis.gl contributors
4

5
import type {Device, CanvasContextProps} from '@luma.gl/core';
6
import {luma, log} from '@luma.gl/core';
7
import {webgl2Adapter, WebGLDevice} from '@luma.gl/webgl';
8
import {webgpuAdapter, WebGPUDevice} from '@luma.gl/webgpu';
9
import {nullAdapter} from './null-device/null-adapter';
10
import {NullDevice} from './null-device/null-device';
11

12
const DEFAULT_CANVAS_CONTEXT_PROPS: CanvasContextProps = {width: 1, height: 1};
58✔
13
const TEST_DEVICE_CACHE_KEY = '__lumaTestDeviceCache';
58✔
14

15
type TestDeviceCache = {
16
  /** A null device intended for testing - @note Only available after getTestDevices() has completed */
17
  nullDevicePromise: Promise<NullDevice> | null;
18
  /** This WebGL Device can be used directly but will not have WebGL debugging initialized */
19
  webglDevicePromise: Promise<WebGLDevice> | null;
20
  /** A shared offscreen WebGL device for presentation-context tests */
21
  presentationWebglDevicePromise: Promise<WebGLDevice | null> | null;
22
  /** A WebGL 2 Device intended for testing - @note Only available after getTestDevices() has completed */
23
  webgpuDevicePromise: Promise<WebGPUDevice | null> | null;
24
};
25

26
declare global {
27
  interface Window {
28
    [TEST_DEVICE_CACHE_KEY]?: TestDeviceCache;
29
  }
30
}
31

32
const testDeviceCache = getOrCreateTestDeviceCache();
58✔
33

34
/** Includes WebGPU device if available */
35
export async function getTestDevices(
36
  types: Readonly<('webgl' | 'webgpu' | 'null' | 'unknown')[]> = ['webgl', 'webgpu']
68✔
37
): Promise<Device[]> {
38
  const promises = types.map(type => getTestDevice(type));
148✔
39
  const devices = await Promise.all(promises);
68✔
40
  return devices.filter(device => device !== null);
148✔
41
}
42

43
export async function getTestDevice(
44
  type: 'webgl' | 'webgpu' | 'null' | 'unknown'
45
): Promise<Device | null> {
46
  switch (type) {
148!
47
    case 'webgl':
48
      return getOrCreateWebGLTestDevicePromise();
67✔
49
    case 'webgpu':
50
      return getWebGPUTestDevice();
67✔
51
    case 'null':
52
      return getOrCreateNullTestDevicePromise();
14✔
53
    case 'unknown':
54
      return null;
×
55
  }
56
}
57

58
/** returns WebGPU device promise, if available */
59
export async function getWebGPUTestDevice(): Promise<WebGPUDevice | null> {
60
  const webgpuDevice = await getOrCreateWebGPUTestDevicePromise();
107✔
61
  if (webgpuDevice?.isLost) {
107!
NEW
62
    if (testDeviceCache.webgpuDevicePromise) {
×
NEW
63
      testDeviceCache.webgpuDevicePromise = null;
×
64
    }
NEW
65
    return getOrCreateWebGPUTestDevicePromise();
×
66
  }
67
  return webgpuDevice;
107✔
68
}
69

70
/** returns WebGL device promise, if available */
71
export async function getWebGLTestDevice(): Promise<WebGLDevice> {
72
  return getOrCreateWebGLTestDevicePromise();
158✔
73
}
74

75
/** returns an offscreen WebGL device promise for presentation-context tests, if available */
76
export async function getPresentationWebGLTestDevice(): Promise<WebGLDevice | null> {
77
  return getOrCreatePresentationWebGLTestDevicePromise();
5✔
78
}
79

80
/** returns null device promise, if available */
81
export async function getNullTestDevice(): Promise<NullDevice> {
82
  return getOrCreateNullTestDevicePromise();
11✔
83
}
84

85
function getOrCreateWebGPUTestDevicePromise(): Promise<WebGPUDevice | null> {
86
  testDeviceCache.webgpuDevicePromise ||= makeWebGPUTestDevice();
107✔
87
  return testDeviceCache.webgpuDevicePromise;
107✔
88
}
89

90
function getOrCreateWebGLTestDevicePromise(): Promise<WebGLDevice> {
91
  testDeviceCache.webglDevicePromise ||= makeWebGLTestDevice();
225✔
92
  return testDeviceCache.webglDevicePromise;
225✔
93
}
94

95
function getOrCreatePresentationWebGLTestDevicePromise(): Promise<WebGLDevice | null> {
96
  testDeviceCache.presentationWebglDevicePromise ||= makePresentationWebGLTestDevice();
5✔
97
  return testDeviceCache.presentationWebglDevicePromise;
5✔
98
}
99

100
function getOrCreateNullTestDevicePromise(): Promise<NullDevice> {
101
  testDeviceCache.nullDevicePromise ||= makeNullTestDevice();
25✔
102
  return testDeviceCache.nullDevicePromise;
25✔
103
}
104

105
async function makeWebGPUTestDevice(): Promise<WebGPUDevice | null> {
106
  const webgpuDeviceResolvers = withResolvers<WebGPUDevice | null>();
25✔
107
  try {
25✔
108
    const webgpuDevice = (await luma.createDevice({
25✔
109
      id: 'webgpu-test-device',
110
      type: 'webgpu',
111
      adapters: [webgpuAdapter],
112
      createCanvasContext: DEFAULT_CANVAS_CONTEXT_PROPS,
113
      debug: true
114
    })) as unknown as WebGPUDevice;
115
    webgpuDevice.lost.finally(() => {
25✔
116
      if (testDeviceCache.webgpuDevicePromise === webgpuDeviceResolvers.promise) {
6!
NEW
117
        testDeviceCache.webgpuDevicePromise = null;
×
118
      }
119
    });
120
    webgpuDeviceResolvers.resolve(webgpuDevice);
25✔
121
  } catch (error) {
122
    log.error(String(error))();
×
123
    // @ts-ignore TODO
124
    webgpuDeviceResolvers.resolve(null);
×
125
  }
126
  return webgpuDeviceResolvers.promise;
25✔
127
}
128

129
/** returns WebGL device promise, if available */
130
async function makeWebGLTestDevice(): Promise<WebGLDevice> {
131
  const webglDeviceResolvers = withResolvers<WebGLDevice>();
47✔
132
  try {
47✔
133
    const webglDevice = (await luma.createDevice({
47✔
134
      id: 'webgl-test-device',
135
      type: 'webgl',
136
      adapters: [webgl2Adapter],
137
      createCanvasContext: DEFAULT_CANVAS_CONTEXT_PROPS,
138
      debug: true
139
    })) as unknown as WebGLDevice;
140
    webglDevice.lost.finally(() => {
47✔
141
      if (testDeviceCache.webglDevicePromise === webglDeviceResolvers.promise) {
6!
NEW
142
        testDeviceCache.webglDevicePromise = null;
×
143
      }
144
    });
145
    webglDeviceResolvers.resolve(webglDevice);
47✔
146
  } catch (error) {
147
    log.error(String(error))();
×
148
    // @ts-ignore TODO
149
    webglDeviceResolvers.resolve(null);
×
150
  }
151
  return webglDeviceResolvers.promise;
47✔
152
}
153

154
async function makePresentationWebGLTestDevice(): Promise<WebGLDevice | null> {
155
  if (typeof OffscreenCanvas === 'undefined') {
1!
156
    return null;
×
157
  }
158

159
  const presentationWebGLDeviceResolvers = withResolvers<WebGLDevice | null>();
1✔
160
  try {
1✔
161
    const webglDevice = (await luma.createDevice({
1✔
162
      id: 'webgl-presentation-context-test-device',
163
      type: 'webgl',
164
      adapters: [webgl2Adapter],
165
      createCanvasContext: {canvas: new OffscreenCanvas(4, 4)},
166
      debug: true
167
    })) as unknown as WebGLDevice;
168
    webglDevice.lost.finally(() => {
1✔
169
      if (
1!
170
        testDeviceCache.presentationWebglDevicePromise === presentationWebGLDeviceResolvers.promise
171
      ) {
NEW
172
        testDeviceCache.presentationWebglDevicePromise = null;
×
173
      }
174
    });
175
    presentationWebGLDeviceResolvers.resolve(webglDevice);
1✔
176
  } catch (error) {
177
    log.error(String(error))();
×
178
    presentationWebGLDeviceResolvers.resolve(null);
×
179
  }
180
  return presentationWebGLDeviceResolvers.promise;
1✔
181
}
182

183
/** returns null device promise, if available */
184
async function makeNullTestDevice(): Promise<NullDevice> {
185
  const nullDeviceResolvers = withResolvers<NullDevice>();
8✔
186
  try {
8✔
187
    const nullDevice = (await luma.createDevice({
8✔
188
      id: 'null-test-device',
189
      type: 'null',
190
      adapters: [nullAdapter],
191
      createCanvasContext: DEFAULT_CANVAS_CONTEXT_PROPS,
192
      debug: true
193
    })) as unknown as NullDevice;
194
    nullDeviceResolvers.resolve(nullDevice);
8✔
195
  } catch (error) {
196
    log.error(String(error))();
×
197
    // @ts-ignore TODO
NEW
198
    testDeviceCache.nullDevicePromise = Promise.resolve(null);
×
199
  }
200
  return nullDeviceResolvers.promise;
8✔
201
}
202

203
// HELPERS
204

205
function getOrCreateTestDeviceCache(): TestDeviceCache {
206
  const rootObject = globalThis as typeof globalThis & {
58✔
207
    [TEST_DEVICE_CACHE_KEY]?: TestDeviceCache;
208
  };
209

210
  rootObject[TEST_DEVICE_CACHE_KEY] ||= {
58✔
211
    nullDevicePromise: null,
212
    webglDevicePromise: null,
213
    presentationWebglDevicePromise: null,
214
    webgpuDevicePromise: null
215
  };
216

217
  return rootObject[TEST_DEVICE_CACHE_KEY];
58✔
218
}
219

220
// TODO - replace with Promise.withResolvers once we upgrade TS baseline
221
function withResolvers<T>(): {
222
  promise: Promise<T>;
223
  resolve: (t: T) => void;
224
  reject: (error: Error) => void;
225
} {
226
  let resolve;
227
  let reject;
228
  const promise = new Promise<T>((_resolve, _reject) => {
81✔
229
    resolve = _resolve;
81✔
230
    reject = _reject;
81✔
231
  });
232
  // @ts-ignore Assigned in callback.
233
  return {promise, resolve, reject};
81✔
234
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc