From 1f5135343a9907c556b56b64c1061c5256b3a7f0 Mon Sep 17 00:00:00 2001 From: Konstantin Date: Thu, 27 Aug 2026 15:16:33 +0200 Subject: [PATCH 1/2] Test infrastructure: detect missing data types, batch zarr-python The dtype test providers mirrored our own DataType enum by hand, which made them tautological: they could only assert that what we implemented is implemented, so a data type we never added was invisible and no test could fail for it. That is how float16 and string went unnoticed despite a broad interop matrix. Drive coverage from outside our code instead: - ZarrTest.dataTypeProviderV3/V2 now derive from DataType.values(), so a new data type is picked up by every test using them without anyone remembering to extend a list. - New DataTypeConformanceTest checks an external list (generated from zarr-python's registry, committed as spec-data-types-v3.json) against our enum. Each of the 11 data types we lack is enumerated in KNOWN_UNSUPPORTED with a note on what it needs. The assertion is bidirectional, so implementing a data type shows up as a deletion there and a stale entry fails too. Metadata-only: 46 tests in 0.6s, no I/O and no Python. - ZarrV3Test.dataTypeAndEndianProvider derives from the enum as well. The hand-written version had already drifted, omitting INT64 and UINT64, so 8-byte byte-order handling was untested. Also cut the interop tier's cost. It ran one `uv run` per test case, paying interpreter startup plus `import zarr, numpy` roughly 136 times; the 16 KB test arrays were never the expense. zarr-python now runs as one long-lived worker speaking JSON lines over stdin/stdout. A worker rather than an up-front batch keeps per-test semantics: each test still makes its own call and asserts its own result, so failures stay attributed to the test that caused them. Fixture logic moved to zarr_fixtures.py, which both the worker and the existing CLI scripts call, so the two cannot drift. Full interop run: 129.3s -> 19.2s for the same 174 passing tests. Tag the interop tests `interop` and exclude them from `mvn test` by default, so pull requests get the fast offline tiers and the cross-implementation matrix moves to a nightly job. Interop coverage is not optional for a format library: a zarr-java write followed by a zarr-java read passes even when reader and writer share the same misreading of the spec, and only a second implementation catches that. --- README.md | 50 +++- pom.xml | 20 ++ .../zarrjava/DataTypeConformanceTest.java | 242 ++++++++++++++++++ .../dev/zarr/zarrjava/ZarrPythonTests.java | 59 +++-- .../dev/zarr/zarrjava/ZarrPythonWorker.java | 183 +++++++++++++ src/test/java/dev/zarr/zarrjava/ZarrTest.java | 52 ++-- .../java/dev/zarr/zarrjava/ZarrV3Test.java | 31 ++- .../generate_spec_data_types.py | 107 ++++++++ src/test/python-scripts/zarr_fixtures.py | 182 +++++++++++++ src/test/python-scripts/zarr_python_group.py | 33 +-- src/test/python-scripts/zarr_python_read.py | 38 +-- .../python-scripts/zarr_python_read_v2.py | 39 +-- src/test/python-scripts/zarr_python_worker.py | 92 +++++++ src/test/python-scripts/zarr_python_write.py | 36 +-- .../python-scripts/zarr_python_write_v2.py | 35 +-- src/test/python-scripts/zstd_decompress.py | 17 +- src/test/resources/spec-data-types-v3.json | 163 ++++++++++++ 17 files changed, 1158 insertions(+), 221 deletions(-) create mode 100644 src/test/java/dev/zarr/zarrjava/DataTypeConformanceTest.java create mode 100644 src/test/java/dev/zarr/zarrjava/ZarrPythonWorker.java create mode 100644 src/test/python-scripts/generate_spec_data_types.py create mode 100644 src/test/python-scripts/zarr_fixtures.py create mode 100644 src/test/python-scripts/zarr_python_worker.py create mode 100644 src/test/resources/spec-data-types-v3.json diff --git a/README.md b/README.md index faf0cf2f..abf7d656 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,36 @@ data ### Run Tests Locally -To be able to run the tests locally, make sure to have `python3.11` and `uv` installed. +The test suite is split into two tiers. + +**Fast tier (default).** Offline, no Python required, runs in seconds. This is what every pull +request should run: + +``` +mvn test +``` + +It covers the metadata-only data type conformance checks, per-data-type round-trips, and the +committed golden fixtures. + +**Interop tier.** Cross-checks zarr-java against zarr-python, and needs `python3.11` and `uv` +installed. Tagged `interop` and excluded from the default run; enable it by clearing the excluded +groups: + +``` +mvn test -DexcludedTestGroups= # everything, both tiers +mvn test -DexcludedTestGroups= -Dtest=ZarrPythonTests # just the interop tier +``` + +These tests matter more than their tag suggests, so the intent is for a nightly job to run them +rather than for anyone to skip them indefinitely. A test that writes with zarr-java and reads back +with zarr-java passes even when the reader and writer share the same misunderstanding of the spec — +the resulting store is then wrong for every other tool, and only a second implementation can catch +it. + +zarr-python runs as one long-lived worker process for the whole suite (see `ZarrPythonWorker` and +`src/test/python-scripts/zarr_python_worker.py`) instead of one `uv run` per test case. Interpreter +startup used to dominate the interop tier; batching it took the full interop run from ~129s to ~19s. Furthermore, you will need the `l4_sample` test data: @@ -69,6 +98,25 @@ Furthermore, you will need the `l4_sample` test data: && unzip l4_sample.zip ` +### Data Type Coverage + +`DataTypeConformanceTest` checks zarr-java's data types against an external list +(`src/test/resources/spec-data-types-v3.json`, generated from zarr-python's registry). Data types we +do not implement yet are enumerated in that test's `KNOWN_UNSUPPORTED` set. + +The list is deliberately *not* derived from our own `DataType` enum. A provider built from the enum +can only assert that what we implemented is implemented, so a data type we never added stays +invisible and no test can fail for it — which is how `float16` and `string` went unnoticed. + +Implementing a data type therefore means deleting its entry from `KNOWN_UNSUPPORTED`; the assertion +is bidirectional, so leaving a stale entry there fails the build too. + +To refresh the list after a zarr-python upgrade: + +``` +uv run src/test/python-scripts/generate_spec_data_types.py +``` + ### Code Style & Formatting This project uses IntelliJ IDEA default Java formatting diff --git a/pom.xml b/pom.xml index e3d04d97..7c243924 100644 --- a/pom.xml +++ b/pom.xml @@ -44,6 +44,11 @@ 8 UTF-8 + + interop 2.20.0 2.34.6 5.9.1 @@ -156,6 +161,21 @@ 3.2.5 false + + ${excludedTestGroups} 1.44 diff --git a/src/test/java/dev/zarr/zarrjava/DataTypeConformanceTest.java b/src/test/java/dev/zarr/zarrjava/DataTypeConformanceTest.java new file mode 100644 index 00000000..c65ef6d5 --- /dev/null +++ b/src/test/java/dev/zarr/zarrjava/DataTypeConformanceTest.java @@ -0,0 +1,242 @@ +package dev.zarr.zarrjava; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import dev.zarr.zarrjava.v3.ArrayMetadata; +import dev.zarr.zarrjava.v3.DataType; +import dev.zarr.zarrjava.v3.Node; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.stream.Stream; + +/** + * Metadata-only conformance tests for Zarr v3 data types. + * + *

These tests exist because the rest of the suite cannot detect a data type we never + * implemented. Providers such as {@link ZarrTest#dataTypeProviderV3()} enumerate our own + * {@link DataType} enum, so they can only ever assert that what we implemented is implemented -- a + * missing data type is invisible by construction, and no test can fail for an enum constant that + * does not exist. That blind spot is how {@code float16} and {@code string} went unnoticed. + * + *

The fix is to drive coverage from an external list: {@code + * /spec-data-types-v3.json}, generated from zarr-python's data type registry by {@code + * src/test/python-scripts/generate_spec_data_types.py} and committed so these tests stay offline + * and fast. Every entry must either parse or be named in {@link #KNOWN_UNSUPPORTED}. + * + *

The central assertion is deliberately bidirectional. An entry that fails to parse and is not + * listed is a coverage gap; an entry that parses but is still listed means the list went stale. + * Implementing a data type therefore shows up as a one-line deletion from {@link + * #KNOWN_UNSUPPORTED}, and forgetting to delete it is itself a test failure. + * + *

No I/O, no compression, no Python: these run in milliseconds and belong in every pull-request + * build. + */ +public class DataTypeConformanceTest { + + private static final String RESOURCE = "/spec-data-types-v3.json"; + + /** + * Data types that a reference implementation supports and zarr-java does not. + * + *

This list is the honest inventory of our gaps. It is expected to shrink, never grow: each + * entry removed is a data type gained. Keeping the gaps enumerated here rather than merely + * absent is the whole point -- an unimplemented data type is now a visible, deliberate decision + * instead of a silent hole. + * + *

Grouped by the work each needs: + * + *

    + *
  • Fixed itemsize -- {@code float16}, {@code complex64}, {@code complex128}, + * {@code fixed_length_utf32}, {@code raw_bytes}, {@code null_terminated_bytes}. Additive: + * these need an enum entry, an in-memory representation, and fill-value rules. The + * existing {@code bytes} codec already serializes them; no new codec is required. + * {@code ucar.ma2} has no half-float and no complex type, so the open question is how to + * represent them in memory, not how to encode them. + *
  • Variable itemsize -- {@code string}, {@code variable_length_bytes}. These + * require a dedicated array-to-bytes codec ({@code vlen-utf8} / {@code vlen-bytes}) + * because there is no itemsize to compute offsets from, which means breaking the + * fixed-size assumption in {@code core.DataType#getByteCount()}. + *
  • Compound / parameterized -- {@code structured}, {@code numpy.datetime64}, + * {@code numpy.timedelta64}. Note that zarr-python itself warns that {@code structured} + * and {@code variable_length_bytes} have no stable Zarr v3 specification yet. + *
+ */ + static final Set KNOWN_UNSUPPORTED = Collections.unmodifiableSet( + new LinkedHashSet<>(Arrays.asList( + "float16", + "complex64", + "complex128", + "fixed_length_utf32", + "string", + "raw_bytes", + "null_terminated_bytes", + "variable_length_bytes", + "structured", + "numpy.datetime64", + "numpy.timedelta64" + ))); + + private static JsonNode loadSpecDocument() { + try (InputStream in = DataTypeConformanceTest.class.getResourceAsStream(RESOURCE)) { + Assertions.assertNotNull(in, "Missing test resource " + RESOURCE + + ". Regenerate it with 'uv run src/test/python-scripts/generate_spec_data_types.py'."); + return new ObjectMapper().readTree(in); + } catch (IOException e) { + throw new RuntimeException("Could not read " + RESOURCE, e); + } + } + + static Stream specDataTypes() { + JsonNode dataTypes = loadSpecDocument().get("data_types"); + Stream.Builder builder = Stream.builder(); + for (JsonNode entry : dataTypes) { + builder.add(Arguments.of( + entry.get("name").asText(), + entry.get("data_type"), + entry.get("fill_value") + )); + } + return builder.build(); + } + + /** + * Builds the smallest {@code zarr.json} that exercises a data type, so a parse failure can only + * be about the data type or its fill value. + */ + private static String minimalArrayMetadata(JsonNode dataType, JsonNode fillValue) { + ObjectMapper objectMapper = new ObjectMapper(); + ObjectNode root = objectMapper.createObjectNode(); + root.put("zarr_format", 3); + root.put("node_type", "array"); + ArrayNode shape = root.putArray("shape"); + shape.add(4); + root.set("data_type", dataType); + ObjectNode chunkGrid = root.putObject("chunk_grid"); + chunkGrid.put("name", "regular"); + chunkGrid.putObject("configuration").putArray("chunk_shape").add(2); + root.putObject("chunk_key_encoding").put("name", "default"); + root.set("fill_value", fillValue); + root.putArray("codecs").addObject().put("name", "bytes"); + return root.toString(); + } + + /** + * Every data type a reference implementation writes must either be understood by zarr-java or be + * an acknowledged gap. + * + *

Deserializes the {@code data_type} member alone rather than a whole metadata document, so + * the result reflects data type support specifically and cannot be perturbed by unrelated + * validation elsewhere in {@link ArrayMetadata} (codec/data type compatibility checks, for + * instance). + */ + @ParameterizedTest(name = "{0}") + @MethodSource("specDataTypes") + public void specDataTypeIsSupportedOrKnownUnsupported(String name, JsonNode dataTypeJson, + JsonNode fillValueJson) { + boolean parsed; + String failure = null; + try { + DataType parsedDataType = + Node.makeObjectMapper().treeToValue(dataTypeJson, DataType.class); + parsed = parsedDataType != null; + } catch (Exception e) { + parsed = false; + failure = e.getClass().getSimpleName() + ": " + e.getMessage(); + } + + boolean listedAsUnsupported = KNOWN_UNSUPPORTED.contains(name); + + if (parsed && listedAsUnsupported) { + Assertions.fail("Data type '" + name + "' now parses, but is still listed in " + + "KNOWN_UNSUPPORTED. Remove it from that list -- implementing a data type is " + + "meant to show up as a deletion there."); + } + if (!parsed && !listedAsUnsupported) { + Assertions.fail("Data type '" + name + "' is supported by the reference implementation " + + "but zarr-java cannot parse it: " + failure + + "\nEither implement it in dev.zarr.zarrjava.v3.DataType or add it to " + + "KNOWN_UNSUPPORTED with a note on what it needs."); + } + } + + /** + * For the data types we do support, the fill value a reference implementation writes must parse + * as part of a complete metadata document. + * + *

Skipped for acknowledged gaps -- those already fail the test above, and reporting the same + * gap twice would only obscure it. + */ + @ParameterizedTest(name = "{0}") + @MethodSource("specDataTypes") + public void specFillValueParsesForSupportedDataType(String name, JsonNode dataTypeJson, + JsonNode fillValueJson) throws Exception { + Assumptions.assumeFalse(KNOWN_UNSUPPORTED.contains(name), + "data type not supported yet: " + name); + + String json = minimalArrayMetadata(dataTypeJson, fillValueJson); + ArrayMetadata metadata = Node.makeObjectMapper().readValue(json, ArrayMetadata.class); + + Assertions.assertNotNull(metadata.dataType, + "Parsed metadata for '" + name + "' has no data type"); + Assertions.assertNotNull(metadata.parsedFillValue, + "Fill value " + fillValueJson + " for '" + name + "' parsed to null"); + } + + /** + * Guards the conformance list itself: a resource that failed to load, or that lost entries in a + * regeneration, would otherwise make the tests above pass vacuously. + */ + @Test + public void specDataTypeListIsPresentAndPlausible() { + JsonNode document = loadSpecDocument(); + JsonNode dataTypes = document.get("data_types"); + Assertions.assertNotNull(dataTypes, "Conformance list has no 'data_types' member"); + Assertions.assertTrue(dataTypes.size() >= 22, + "Expected at least the 22 data types zarr-python 3.1.6 supports, found " + + dataTypes.size() + ". Was " + RESOURCE + " regenerated correctly?"); + + Set names = new HashSet<>(); + for (JsonNode entry : dataTypes) { + names.add(entry.get("name").asText()); + } + Assertions.assertTrue(names.containsAll(KNOWN_UNSUPPORTED), + "KNOWN_UNSUPPORTED names missing from " + RESOURCE + ": " + + KNOWN_UNSUPPORTED.stream().filter(n -> !names.contains(n)).toArray()); + } + + /** + * Documents the gap as a single number, so a reviewer sees at a glance how much of a reference + * implementation's data type surface we cover. Update the expected count when the list shrinks. + */ + @Test + public void supportedDataTypeCountIsAsExpected() { + JsonNode dataTypes = loadSpecDocument().get("data_types"); + int total = dataTypes.size(); + int unsupported = 0; + for (JsonNode entry : dataTypes) { + if (KNOWN_UNSUPPORTED.contains(entry.get("name").asText())) { + unsupported++; + } + } + Assertions.assertEquals(KNOWN_UNSUPPORTED.size(), unsupported, + "Every KNOWN_UNSUPPORTED entry should appear in the conformance list"); + Assertions.assertEquals(DataType.values().length, total - unsupported, + "Number of data types the conformance list says we support should match the " + + "DataType enum. If these diverge, either the enum gained a data type that " + + "is not in the conformance list, or KNOWN_UNSUPPORTED is stale."); + } +} diff --git a/src/test/java/dev/zarr/zarrjava/ZarrPythonTests.java b/src/test/java/dev/zarr/zarrjava/ZarrPythonTests.java index 5c22b5fc..57189b6c 100644 --- a/src/test/java/dev/zarr/zarrjava/ZarrPythonTests.java +++ b/src/test/java/dev/zarr/zarrjava/ZarrPythonTests.java @@ -12,6 +12,7 @@ import dev.zarr.zarrjava.v3.codec.CodecBuilder; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @@ -22,16 +23,31 @@ import java.io.IOException; import java.io.InputStreamReader; import java.nio.ByteBuffer; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Arrays; import java.util.stream.Stream; +/** + * Cross-implementation tests: everything here checks zarr-java against zarr-python. + * + *

These are the only tests that need Python, and they are tagged {@code interop} so the + * pull-request build can skip them. That split matters because an interop failure is usually a real + * format bug worth investigating slowly, while the fast offline tiers -- {@link + * DataTypeConformanceTest}, the per-data-type round-trips in {@link ZarrV3Test} and {@link + * ZarrV2Test}, and the committed golden fixtures -- give quick feedback on every change. See the + * "Running the tests" section of the README. + * + *

An external reference implementation is not optional for a format library. A test that writes + * with zarr-java and reads it back with zarr-java passes even when reader and writer share the same + * misunderstanding of the spec, and the resulting store is wrong for every other tool. Only a second + * implementation can catch that. + * + *

zarr-python runs as a single long-lived process ({@link ZarrPythonWorker}) rather than one + * launch per test case; see that class for why. + */ +@Tag("interop") public class ZarrPythonTests extends ZarrTest { - final static Path PYTHON_TEST_PATH = Paths.get("src/test/python-scripts/"); - public static int runCommand(String... command) throws IOException, InterruptedException { ProcessBuilder pb = new ProcessBuilder(); pb.command().addAll(Arrays.asList(command)); @@ -114,17 +130,23 @@ static Stream compressorAndDataTypeProviderV2() { return Stream.concat(datatypeTests, bloscTests); } - public void run_python_script(String scriptName, String... args) throws IOException, InterruptedException { - int exitCode = runCommand(Stream.concat(Stream.of("uv", "run", PYTHON_TEST_PATH.resolve(scriptName) - .toString()), Arrays.stream(args)).toArray(String[]::new)); - assert exitCode == 0; + /** + * Runs one zarr-python operation in the shared worker process. + * + *

Replaces the previous one-{@code uv run}-per-call approach. The operation names map to the + * functions in {@code src/test/python-scripts/zarr_fixtures.py}, which the standalone CLI + * scripts also call -- so driving an operation from here and running the matching script by hand + * execute the same code. + */ + public void runPython(String op, String... args) throws IOException, InterruptedException { + ZarrPythonWorker.get().call(op, args); } @ParameterizedTest @MethodSource("compressorAndDataTypeProviderV3") public void testReadV3(String codec, String codecParam, DataType dataType) throws IOException, ZarrException, InterruptedException { StoreHandle storeHandle = new FilesystemStore(TESTOUTPUT).resolve("testReadV3", codec, codecParam, dataType.name()); - run_python_script("zarr_python_write.py", codec, codecParam, dataType.name().toLowerCase(), storeHandle.toPath().toString()); + runPython("write_v3", codec, codecParam, dataType.name().toLowerCase(), storeHandle.toPath().toString()); Array array = Array.open(storeHandle); ucar.ma2.Array result = array.read(); @@ -199,14 +221,14 @@ public void testWriteV3(String codec, String codecParam, DataType dataType) thro assertIsTestdata(result, dataType); //read in zarr_python - run_python_script("zarr_python_read.py", codec, codecParam, dataType.name().toLowerCase(), storeHandle.toPath().toString()); + runPython("read_v3", codec, codecParam, dataType.name().toLowerCase(), storeHandle.toPath().toString()); } @ParameterizedTest @MethodSource("compressorAndDataTypeProviderV2") public void testReadV2(String compressor, String compressorParam, dev.zarr.zarrjava.v2.DataType dt) throws IOException, ZarrException, InterruptedException { StoreHandle storeHandle = new FilesystemStore(TESTOUTPUT).resolve("testReadV2", compressor, compressorParam, dt.name()); - run_python_script("zarr_python_write_v2.py", compressor, compressorParam, dt.getValue(), storeHandle.toPath().toString()); + runPython("write_v2", compressor, compressorParam, dt.getValue(), storeHandle.toPath().toString()); dev.zarr.zarrjava.v2.Array array = dev.zarr.zarrjava.v2.Array.open(storeHandle); ucar.ma2.Array result = array.read(); @@ -265,7 +287,7 @@ public void testWriteV2(String compressor, String compressorParam, dev.zarr.zarr assertIsTestdata(result, dt); //read in zarr_python - run_python_script("zarr_python_read_v2.py", compressor, compressorParam, dt.getValue(), storeHandle.toPath().toString()); + runPython("read_v2", compressor, compressorParam, dt.getValue(), storeHandle.toPath().toString()); } @CsvSource({"0,true", "0,false", "5, true", "10, false"}) @@ -292,14 +314,7 @@ public void testZstdLibrary(int level, boolean checksumFlag) throws IOException, } //decompress in python - int exitCode = ZarrPythonTests.runCommand( - "uv", - "run", - PYTHON_TEST_PATH.resolve("zstd_decompress.py").toString(), - compressedDataPath, - Integer.toString(number) - ); - assert exitCode == 0; + runPython("zstd_decompress", compressedDataPath, Integer.toString(number)); } @Test @@ -316,7 +331,7 @@ public void testGroupReadWriteV2() throws Exception { array.write(testdata(dataType)); - run_python_script("zarr_python_group.py", storeHandle.toPath().toString(), storeHandle2.toPath().toString(), "" + 2); + runPython("group", storeHandle.toPath().toString(), storeHandle2.toPath().toString(), "" + 2); Group group2 = Group.open(storeHandle2); Assertions.assertEquals("value", group2.metadata().attributes().get("attr")); @@ -343,7 +358,7 @@ public void testGroupReadWriteV3() throws Exception { array.write(testdata(dataType)); - run_python_script("zarr_python_group.py", storeHandle.toPath().toString(), storeHandle2.toPath().toString(), "" + 3); + runPython("group", storeHandle.toPath().toString(), storeHandle2.toPath().toString(), "" + 3); dev.zarr.zarrjava.v3.Group group2 = dev.zarr.zarrjava.v3.Group.open(storeHandle2); Assertions.assertEquals("value", group2.metadata().attributes().get("attr")); diff --git a/src/test/java/dev/zarr/zarrjava/ZarrPythonWorker.java b/src/test/java/dev/zarr/zarrjava/ZarrPythonWorker.java new file mode 100644 index 00000000..e6dd5488 --- /dev/null +++ b/src/test/java/dev/zarr/zarrjava/ZarrPythonWorker.java @@ -0,0 +1,183 @@ +package dev.zarr.zarrjava; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * A single long-lived zarr-python process, shared by every interop test in the JVM. + * + *

The interop suite used to run one {@code uv run