Skip to content

Commit 099a664

Browse files
committed
zarr store testing
1 parent 28b3337 commit 099a664

File tree

1 file changed

+279
-0
lines changed

1 file changed

+279
-0
lines changed

src/zarr-store.test.js

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
jest.mock('zarr-js', () => {
2+
const handlers = new Map()
3+
const openMock = jest.fn((url, callback) => {
4+
const handler = handlers.get(url)
5+
if (!handler) {
6+
callback(new Error(`Missing handler for URL: ${url}`))
7+
return
8+
}
9+
callback(null, handler)
10+
})
11+
12+
const factory = jest.fn(() => ({ open: openMock }))
13+
14+
factory.__set = (url, handler) => handlers.set(url, handler)
15+
factory.__clear = () => {
16+
handlers.clear()
17+
openMock.mockClear()
18+
factory.mockClear()
19+
}
20+
factory.__openMock = openMock
21+
22+
return factory
23+
})
24+
25+
import zarr from 'zarr-js'
26+
import ZarrStore from './zarr-store'
27+
28+
const mockJsonResponse = (value) =>
29+
Promise.resolve({
30+
json: () => Promise.resolve(value),
31+
})
32+
33+
const createV2Metadata = () => ({
34+
metadata: {
35+
'.zattrs': {
36+
multiscales: [
37+
{
38+
datasets: [
39+
{ path: '0', pixels_per_tile: 256, crs: 'EPSG:3857' },
40+
{ path: '1', pixels_per_tile: 256, crs: 'EPSG:3857' },
41+
],
42+
},
43+
],
44+
},
45+
'0/temp/.zattrs': {
46+
_ARRAY_DIMENSIONS: ['time', 'y', 'x'],
47+
},
48+
'0/temp/.zarray': {
49+
shape: [2, 256, 256],
50+
chunks: [1, 256, 256],
51+
fill_value: -9999,
52+
dtype: '<f4',
53+
},
54+
},
55+
})
56+
57+
const createV3Metadata = () => ({
58+
attributes: {
59+
multiscales: [
60+
{
61+
datasets: [
62+
{ path: '0', pixels_per_tile: 256, crs: 'EPSG:3857' },
63+
{ path: '1', pixels_per_tile: 256, crs: 'EPSG:3857' },
64+
],
65+
},
66+
],
67+
},
68+
})
69+
70+
const createV3ArrayMetadata = () => ({
71+
attributes: {
72+
_ARRAY_DIMENSIONS: ['time', 'y', 'x'],
73+
},
74+
shape: [2, 256, 256],
75+
chunk_grid: {
76+
configuration: {
77+
chunk_shape: [1, 256, 256],
78+
},
79+
},
80+
codecs: [{ name: 'gzip' }],
81+
fill_value: 0,
82+
data_type: '<f4',
83+
})
84+
85+
const registerChunk = (url, values) => {
86+
const handler = jest.fn((chunkIndices, callback) => {
87+
callback(null, { data: new Float32Array(values) })
88+
})
89+
zarr.__set(url, handler)
90+
return handler
91+
}
92+
93+
describe('ZarrStore', () => {
94+
const originalGlobalFetch = global.fetch
95+
const hasWindow = typeof window !== 'undefined'
96+
const originalWindowFetch = hasWindow ? window.fetch : undefined
97+
98+
let fetchMock
99+
let createdWindow
100+
101+
beforeEach(() => {
102+
createdWindow = false
103+
fetchMock = jest.fn()
104+
105+
global.fetch = fetchMock
106+
107+
if (!hasWindow) {
108+
global.window = {}
109+
createdWindow = true
110+
}
111+
window.fetch = fetchMock
112+
113+
zarr.__clear()
114+
ZarrStore._cache.clear()
115+
})
116+
117+
afterEach(() => {
118+
if (originalGlobalFetch !== undefined) {
119+
global.fetch = originalGlobalFetch
120+
} else {
121+
delete global.fetch
122+
}
123+
124+
if (createdWindow) {
125+
delete global.window
126+
} else if (hasWindow) {
127+
if (originalWindowFetch !== undefined) {
128+
window.fetch = originalWindowFetch
129+
} else {
130+
delete window.fetch
131+
}
132+
}
133+
})
134+
135+
it('loads v2 metadata and coordinates while reusing cached metadata', async () => {
136+
const source = 'https://example.com/v2'
137+
const coordinateValues = [2000, 2001]
138+
139+
const zmetadata = createV2Metadata()
140+
registerChunk(`${source}/0/time`, coordinateValues)
141+
142+
fetchMock.mockResolvedValueOnce(mockJsonResponse(zmetadata))
143+
144+
const store = new ZarrStore({
145+
source,
146+
version: 'v2',
147+
variable: 'temp',
148+
coordinateKeys: ['time'],
149+
})
150+
151+
await expect(store.initialized()).resolves.toBe(true)
152+
153+
expect(fetchMock).toHaveBeenCalledTimes(1)
154+
expect(fetchMock).toHaveBeenCalledWith(`${source}/.zmetadata`)
155+
156+
expect(store.describe()).toEqual({
157+
metadata: { metadata: zmetadata.metadata },
158+
dimensions: ['time', 'y', 'x'],
159+
shape: [2, 256, 256],
160+
chunks: [1, 256, 256],
161+
fill_value: -9999,
162+
dtype: '<f4',
163+
coordinates: { time: coordinateValues },
164+
levels: [0, 1],
165+
maxZoom: 1,
166+
tileSize: 256,
167+
crs: 'EPSG:3857',
168+
})
169+
170+
fetchMock.mockClear()
171+
172+
const secondStore = new ZarrStore({
173+
source,
174+
version: 'v2',
175+
variable: 'temp',
176+
coordinateKeys: ['time'],
177+
})
178+
179+
await expect(secondStore.initialized()).resolves.toBe(true)
180+
expect(fetchMock).not.toHaveBeenCalled()
181+
})
182+
183+
it('loads v3 metadata and array metadata using cached responses', async () => {
184+
const source = 'https://example.com/v3'
185+
const metadata = createV3Metadata()
186+
const arrayMetadata = createV3ArrayMetadata()
187+
188+
fetchMock
189+
.mockResolvedValueOnce(mockJsonResponse(metadata))
190+
.mockResolvedValueOnce(mockJsonResponse(arrayMetadata))
191+
192+
const store = new ZarrStore({
193+
source,
194+
version: 'v3',
195+
variable: 'temp',
196+
})
197+
198+
await expect(store.initialized()).resolves.toBe(true)
199+
200+
expect(fetchMock).toHaveBeenNthCalledWith(1, `${source}/zarr.json`)
201+
expect(fetchMock).toHaveBeenNthCalledWith(2, `${source}/0/temp/zarr.json`)
202+
203+
expect(store.describe()).toEqual({
204+
metadata,
205+
dimensions: ['time', 'y', 'x'],
206+
shape: [2, 256, 256],
207+
chunks: [1, 256, 256],
208+
fill_value: 0,
209+
dtype: '<f4',
210+
coordinates: {},
211+
levels: [0, 1],
212+
maxZoom: 1,
213+
tileSize: 256,
214+
crs: 'EPSG:3857',
215+
})
216+
217+
fetchMock.mockClear()
218+
219+
const secondStore = new ZarrStore({
220+
source,
221+
version: 'v3',
222+
variable: 'temp',
223+
})
224+
225+
await expect(secondStore.initialized()).resolves.toBe(true)
226+
expect(fetchMock).not.toHaveBeenCalled()
227+
})
228+
229+
it('requires source and variable options', () => {
230+
expect(() => new ZarrStore({ variable: 'temp' })).toThrow(
231+
'source is a required parameter'
232+
)
233+
expect(() => new ZarrStore({ source: 'https://example.com' })).toThrow(
234+
'variable is a required parameter'
235+
)
236+
})
237+
238+
it('reuses cached getter functions when requesting the same chunk key', async () => {
239+
const source = 'https://example.com/v2'
240+
const zmetadata = createV2Metadata()
241+
const chunkValues = [1, 2, 3, 4]
242+
243+
fetchMock.mockResolvedValueOnce(mockJsonResponse(zmetadata))
244+
const chunkHandler = registerChunk(`${source}/0/temp`, chunkValues)
245+
246+
const store = new ZarrStore({
247+
source,
248+
version: 'v2',
249+
variable: 'temp',
250+
})
251+
252+
await expect(store.initialized()).resolves.toBe(true)
253+
254+
const firstChunk = await store.getChunk('0/temp', [0, 0])
255+
expect(firstChunk.data).toEqual(new Float32Array(chunkValues))
256+
257+
const secondChunk = await store.getChunk('0/temp', [0, 1])
258+
expect(secondChunk.data).toEqual(new Float32Array(chunkValues))
259+
260+
expect(zarr).toHaveBeenCalledTimes(1)
261+
expect(zarr.__openMock).toHaveBeenCalledTimes(1)
262+
expect(zarr.__openMock).toHaveBeenCalledWith(
263+
`${source}/0/temp`,
264+
expect.any(Function),
265+
expect.anything()
266+
)
267+
expect(chunkHandler).toHaveBeenCalledTimes(2)
268+
expect(chunkHandler).toHaveBeenNthCalledWith(
269+
1,
270+
[0, 0],
271+
expect.any(Function)
272+
)
273+
expect(chunkHandler).toHaveBeenNthCalledWith(
274+
2,
275+
[0, 1],
276+
expect.any(Function)
277+
)
278+
})
279+
})

0 commit comments

Comments
 (0)