actualKeys = readOnlyZipStore.resolve("subgroup").list()
+ .map(node -> String.join("/", node))
+ .collect(Collectors.toSet());
+
+ Assertions.assertEquals(expectedSubgroupKeys, actualKeys);
+
+ assertIsTestGroupV3(Group.open(readOnlyZipStore.resolve()), true);
+ }
+}
diff --git a/src/test/java/dev/zarr/zarrjava/store/S3StoreTest.java b/src/test/java/dev/zarr/zarrjava/store/S3StoreTest.java
new file mode 100644
index 0000000..520d3a0
--- /dev/null
+++ b/src/test/java/dev/zarr/zarrjava/store/S3StoreTest.java
@@ -0,0 +1,97 @@
+package dev.zarr.zarrjava.store;
+
+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 software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
+import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
+import software.amazon.awssdk.core.sync.RequestBody;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.S3Configuration;
+import software.amazon.awssdk.services.s3.model.PutObjectRequest;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.nio.ByteBuffer;
+
+/**
+ * Tests for S3Store
+ *
+ * Requires a local S3 mock server running at http://localhost:9090
+ * with a bucket named "zarr-test-bucket"
+ *
+ * Execute the following command to start a local S3 mock server:
+ *
+ * docker run -p 9090:9090 -p 9191:9191 -e "initialBuckets=zarr-test-bucket" adobe/s3mock:3.11.0
+ *
+ */
+@Tag("s3")
+public class S3StoreTest extends WritableStoreTest {
+
+ String s3Endpoint = "http://localhost:9090";
+ String bucketName = "zarr-test-bucket";
+ S3Client s3Client;
+ String testDataKey = "testData";
+
+ @BeforeAll
+ void setUpS3Client() {
+ s3Client = S3Client.builder()
+ .endpointOverride(URI.create(s3Endpoint))
+ .region(Region.US_EAST_1) // required, but ignored
+ .serviceConfiguration(
+ S3Configuration.builder()
+ .pathStyleAccessEnabled(true) // required
+ .build()
+ )
+ .credentialsProvider(StaticCredentialsProvider.create(
+ AwsBasicCredentials.create("accessKey", "secretKey")
+ ))
+ .build();
+ // Clean up the bucket
+ try {
+ s3Client.listObjectsV2Paginator(builder -> builder.bucket(bucketName).build())
+ .contents()
+ .forEach(s3Object -> {
+ s3Client.deleteObject(builder -> builder.bucket(bucketName).key(s3Object.key()).build());
+ });
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Test
+ void testReadWriteS3Store() {
+ S3Store s3Store = new S3Store(s3Client, bucketName, "");
+
+ StoreHandle storeHandle = s3Store.resolve("testfile");
+ byte[] testData = new byte[100];
+ for (int i = 0; i < testData.length; i++) {
+ testData[i] = (byte) i;
+ }
+ storeHandle.set(ByteBuffer.wrap(testData));
+ ByteBuffer retrievedData = storeHandle.read();
+ byte[] retrievedBytes = new byte[retrievedData.remaining()];
+ retrievedData.get(retrievedBytes);
+ Assertions.assertArrayEquals(testData, retrievedBytes);
+ }
+
+
+ @Override
+ Store writableStore() {
+ return new S3Store(s3Client, bucketName, "");
+ }
+
+ @Override
+ StoreHandle storeHandleWithData() {
+ try (InputStream byteStream = new ByteArrayInputStream(testData())) {
+ s3Client.putObject(PutObjectRequest.builder().bucket(bucketName).key("/" + testDataKey).build(), RequestBody.fromContentProvider(() -> byteStream, "application/octet-stream"));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return new S3Store(s3Client, bucketName, "").resolve(testDataKey);
+ }
+}
diff --git a/src/test/java/dev/zarr/zarrjava/store/StoreTest.java b/src/test/java/dev/zarr/zarrjava/store/StoreTest.java
new file mode 100644
index 0000000..ece5318
--- /dev/null
+++ b/src/test/java/dev/zarr/zarrjava/store/StoreTest.java
@@ -0,0 +1,129 @@
+package dev.zarr.zarrjava.store;
+
+import dev.zarr.zarrjava.ZarrException;
+import dev.zarr.zarrjava.ZarrTest;
+import dev.zarr.zarrjava.core.Array;
+import dev.zarr.zarrjava.core.Attributes;
+import dev.zarr.zarrjava.core.Group;
+import dev.zarr.zarrjava.core.Node;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import ucar.ma2.DataType;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+public abstract class StoreTest extends ZarrTest {
+
+ abstract StoreHandle storeHandleWithData();
+
+ @Test
+ public void testInputStream() throws IOException {
+ StoreHandle storeHandle = storeHandleWithData();
+ InputStream is = storeHandle.getInputStream(10, 20);
+ byte[] buffer = new byte[10];
+ int bytesRead = is.read(buffer);
+ Assertions.assertEquals(10, bytesRead);
+ byte[] expectedBuffer = new byte[10];
+ storeHandle.read(10, 20).get(expectedBuffer);
+ Assertions.assertArrayEquals(expectedBuffer, buffer);
+ }
+
+
+ @Test
+ public void testStoreGetSize() {
+ StoreHandle storeHandle = storeHandleWithData();
+ long size = storeHandle.getSize();
+ long actual_size = storeHandle.read().remaining();
+ Assertions.assertEquals(actual_size, size);
+ }
+
+
+ byte[] testData() {
+ byte[] testData = new byte[1024 * 1024];
+ for (int i = 0; i < testData.length; i++) {
+ testData[i] = (byte) (i % 256);
+ }
+ return testData;
+ }
+ int[] testDataInt() {
+ int[] testData = new int[1024 * 1024];
+ for (int i = 0; i < testData.length; i++) {
+ testData[i] = i;
+ }
+ return testData;
+ }
+
+
+ Group writeTestGroupV3(StoreHandle storeHandle, boolean useParallel) throws ZarrException, IOException {
+
+ dev.zarr.zarrjava.v3.Group group = dev.zarr.zarrjava.v3.Group.create(storeHandle);
+ dev.zarr.zarrjava.v3.Array array = group.createArray("array", b -> b
+ .withShape(1024, 1024)
+ .withDataType(dev.zarr.zarrjava.v3.DataType.UINT8)
+ .withChunkShape(512, 512)
+ );
+ array.write(ucar.ma2.Array.factory(DataType.BYTE, new int[]{1024, 1024}, testData()), useParallel);
+ dev.zarr.zarrjava.v3.Group subgroup = group.createGroup("subgroup");
+ dev.zarr.zarrjava.v3.Array subgrouparray = subgroup.createArray("array", b -> b
+ .withShape(1024, 1024)
+ .withDataType(dev.zarr.zarrjava.v3.DataType.UINT8)
+ .withChunkShape(512, 512)
+ );
+ subgrouparray.write(ucar.ma2.Array.factory(DataType.BYTE, new int[]{1024, 1024}, testData()), useParallel);
+
+ group.setAttributes(new Attributes(b -> b.set("some", "value")));
+ return group;
+ }
+
+ void assertIsTestGroupV3(Group group, boolean useParallel) throws ZarrException, IOException {
+ Stream nodes = group.list();
+ List nodeList = nodes.collect(Collectors.toList());
+ Assertions.assertEquals(3, nodeList.size());
+ Array array = (Array) group.get("array");
+ Assertions.assertNotNull(array);
+ ucar.ma2.Array result = array.read(useParallel);
+ Assertions.assertArrayEquals(testData(), (byte[]) result.get1DJavaArray(DataType.BYTE));
+ Group subgroup = (Group) group.get("subgroup");
+ Array subgrouparray = (Array) subgroup.get("array");
+ result = subgrouparray.read(useParallel);
+ Assertions.assertArrayEquals(testData(), (byte[]) result.get1DJavaArray(ucar.ma2.DataType.BYTE));
+ Attributes attrs = group.metadata().attributes();
+ Assertions.assertNotNull(attrs);
+ Assertions.assertEquals("value", attrs.getString("some"));
+ }
+
+
+ dev.zarr.zarrjava.v2.Group writeTestGroupV2(StoreHandle storeHandle, boolean useParallel) throws ZarrException, IOException {
+ dev.zarr.zarrjava.v2.Group group = dev.zarr.zarrjava.v2.Group.create(storeHandle);
+ dev.zarr.zarrjava.v2.Array array = group.createArray("array", b -> b
+ .withShape(1024, 1024)
+ .withDataType(dev.zarr.zarrjava.v2.DataType.UINT8)
+ .withChunks(512, 512)
+ );
+ array.write(ucar.ma2.Array.factory(DataType.BYTE, new int[]{1024, 1024}, testData()), useParallel);
+ group.createGroup("subgroup");
+ group.setAttributes(new Attributes().set("some", "value"));
+ return group;
+ }
+
+ void assertIsTestGroupV2(Group group, boolean useParallel) throws ZarrException, IOException {
+ Stream nodes = group.list();
+ Assertions.assertEquals(2, nodes.count());
+ Array array = (Array) group.get("array");
+ Assertions.assertNotNull(array);
+ ucar.ma2.Array result = array.read(useParallel);
+ Assertions.assertArrayEquals(testData(), (byte[]) result.get1DJavaArray(DataType.BYTE));
+ Attributes attrs = group.metadata().attributes();
+ Assertions.assertNotNull(attrs);
+ Assertions.assertEquals("value", attrs.getString("some"));
+ }
+
+ @Test
+ abstract void testList() throws ZarrException, IOException;
+}
diff --git a/src/test/java/dev/zarr/zarrjava/store/WritableStoreTest.java b/src/test/java/dev/zarr/zarrjava/store/WritableStoreTest.java
new file mode 100644
index 0000000..e1c9b45
--- /dev/null
+++ b/src/test/java/dev/zarr/zarrjava/store/WritableStoreTest.java
@@ -0,0 +1,112 @@
+package dev.zarr.zarrjava.store;
+
+import dev.zarr.zarrjava.ZarrException;
+import dev.zarr.zarrjava.core.Array;
+import dev.zarr.zarrjava.core.Attributes;
+import dev.zarr.zarrjava.core.Group;
+import dev.zarr.zarrjava.core.Node;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+public abstract class WritableStoreTest extends StoreTest {
+ abstract Store writableStore();
+
+ @Test
+ public void testList() throws IOException, ZarrException {
+ StoreHandle storeHandle = writableStore().resolve("testList");
+ boolean useParallel = true;
+ writeTestGroupV3(storeHandle, useParallel);
+ java.util.Set expectedSubgroupKeys = new java.util.HashSet<>(Arrays.asList(
+ "array/c/1/1",
+ "array/c/0/0",
+ "array/c/0/1",
+ "zarr.json",
+ "array",
+ "array/c/1/0",
+ "array/c/1",
+ "array/c/0",
+ "array/zarr.json",
+ "array/c"
+ ));
+
+ java.util.Set actualKeys = storeHandle.resolve("subgroup").list()
+ .map(node -> String.join("/", node))
+ .collect(Collectors.toSet());
+ Assertions.assertEquals(expectedSubgroupKeys, actualKeys);
+
+ List allKeys = storeHandle.list()
+ .map(node -> String.join("/", node))
+ .collect(Collectors.toList());
+ Assertions.assertEquals(21, allKeys.size(), "Total number of keys in store should be 21 but was: " + allKeys);
+ }
+
+ @Test
+ public void testWriteRead() throws IOException, ZarrException {
+ StoreHandle storeHandle = writableStore().resolve("testWriteRead");
+ boolean useParallel = true;
+ Group group = writeTestGroupV3(storeHandle, useParallel);
+ assertIsTestGroupV3(group, useParallel);
+ }
+
+ @ParameterizedTest
+ @CsvSource({"false", "true",})
+ public void testWriteReadV3(boolean useParallel) throws ZarrException, IOException {
+ int[] testData = testDataInt();
+ Store store = writableStore();
+ StoreHandle storeHandle = store.resolve("testWriteReadV3").resolve(store.getClass().getSimpleName()).resolve("" + useParallel);
+
+ dev.zarr.zarrjava.v3.Group group = dev.zarr.zarrjava.v3.Group.create(storeHandle);
+ Array array = group.createArray("array", b -> b
+ .withShape(1024, 1024)
+ .withDataType(dev.zarr.zarrjava.v3.DataType.UINT32)
+ .withChunkShape(64, 64)
+ );
+ array.write(ucar.ma2.Array.factory(ucar.ma2.DataType.UINT, new int[]{1024, 1024}, testData), useParallel);
+ group.createGroup("subgroup");
+ group.setAttributes(new Attributes(b -> b.set("some", "value")));
+ Stream nodes = group.list();
+ Assertions.assertEquals(2, nodes.count());
+
+ ucar.ma2.Array result = array.read(useParallel);
+ Assertions.assertArrayEquals(testData, (int[]) result.get1DJavaArray(ucar.ma2.DataType.UINT));
+ Attributes attrs = group.metadata().attributes;
+ Assertions.assertNotNull(attrs);
+ Assertions.assertEquals("value", attrs.getString("some"));
+ }
+
+ @ParameterizedTest
+ @CsvSource({"false", "true",})
+ public void testWriteReadV2(boolean useParallel) throws ZarrException, IOException {
+ int[] testData = testDataInt();
+ Store store = writableStore();
+ StoreHandle storeHandle = store.resolve("testMemoryStoreV2").resolve(store.getClass().getSimpleName()).resolve("" + useParallel);
+ dev.zarr.zarrjava.v2.Group group = dev.zarr.zarrjava.v2.Group.create(storeHandle);
+ dev.zarr.zarrjava.v2.Array array = group.createArray("array", b -> b
+ .withShape(1024, 1024)
+ .withDataType(dev.zarr.zarrjava.v2.DataType.UINT32)
+ .withChunks(512, 512)
+ );
+ array.write(ucar.ma2.Array.factory(ucar.ma2.DataType.UINT, new int[]{1024, 1024}, testData), useParallel);
+ group.createGroup("subgroup");
+ Stream nodes = group.list();
+ group.setAttributes(new Attributes().set("description", "test group"));
+ Assertions.assertEquals(2, nodes.count());
+
+ ucar.ma2.Array result = array.read(useParallel);
+ Assertions.assertArrayEquals(testData, (int[]) result.get1DJavaArray(ucar.ma2.DataType.UINT));
+ Attributes attrs = group.metadata().attributes;
+ Assertions.assertNotNull(attrs);
+ Assertions.assertEquals("test group", attrs.getString("description"));
+ }
+
+}