From 36ac40c62e9696eefee6af6fe563f8dd79de935c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 21:30:12 +0000 Subject: [PATCH 1/4] Add idempotence-s3fs-nio module for S3 bucket test resource generation Co-authored-by: carlspring <1436265+carlspring@users.noreply.github.com> --- .../modules/idempotence-s3fs-nio.md | 55 ++++++ idempotence-s3fs-nio/pom.xml | 144 +++++++++++++++ .../config/S3fsNioIdempotenceProperties.java | 46 +++++ .../extension/S3TestResourceExtension.java | 148 ++++++++++++++++ .../idempotence/io/S3ResourceCopier.java | 167 ++++++++++++++++++ .../io/S3fsNioPathTransformer.java | 80 +++++++++ .../idempotence/util/S3FileSystemUtils.java | 66 +++++++ ...g.idempotence.config.IdempotenceProperties | 1 + ...ing.testing.idempotence.io.PathTransformer | 1 + .../io/S3fsNioPathTransformerTest.java | 82 +++++++++ .../util/S3fsNioTestMethodUtilsTest.java | 55 ++++++ .../src/test/resources/foo.txt | 1 + .../src/test/resources/logback.xml | 46 +++++ .../src/test/resources/nested/dir/foo.txt | 1 + pom.xml | 1 + 15 files changed, 894 insertions(+) create mode 100644 docs/content/developers-guide/modules/idempotence-s3fs-nio.md create mode 100644 idempotence-s3fs-nio/pom.xml create mode 100644 idempotence-s3fs-nio/src/main/java/org/carlspring/testing/idempotence/config/S3fsNioIdempotenceProperties.java create mode 100644 idempotence-s3fs-nio/src/main/java/org/carlspring/testing/idempotence/extension/S3TestResourceExtension.java create mode 100644 idempotence-s3fs-nio/src/main/java/org/carlspring/testing/idempotence/io/S3ResourceCopier.java create mode 100644 idempotence-s3fs-nio/src/main/java/org/carlspring/testing/idempotence/io/S3fsNioPathTransformer.java create mode 100644 idempotence-s3fs-nio/src/main/java/org/carlspring/testing/idempotence/util/S3FileSystemUtils.java create mode 100644 idempotence-s3fs-nio/src/main/resources/META-INF/services/org.carlspring.testing.idempotence.config.IdempotenceProperties create mode 100644 idempotence-s3fs-nio/src/main/resources/META-INF/services/org.carlspring.testing.idempotence.io.PathTransformer create mode 100644 idempotence-s3fs-nio/src/test/java/org/carlspring/testing/idempotence/io/S3fsNioPathTransformerTest.java create mode 100644 idempotence-s3fs-nio/src/test/java/org/carlspring/testing/idempotence/util/S3fsNioTestMethodUtilsTest.java create mode 100644 idempotence-s3fs-nio/src/test/resources/foo.txt create mode 100644 idempotence-s3fs-nio/src/test/resources/logback.xml create mode 100644 idempotence-s3fs-nio/src/test/resources/nested/dir/foo.txt diff --git a/docs/content/developers-guide/modules/idempotence-s3fs-nio.md b/docs/content/developers-guide/modules/idempotence-s3fs-nio.md new file mode 100644 index 0000000..3d0924c --- /dev/null +++ b/docs/content/developers-guide/modules/idempotence-s3fs-nio.md @@ -0,0 +1,55 @@ +# `idempotence-s3fs-nio` + +This Maven module adds support for generating test files in S3 buckets using the +[carlspring/s3fs-nio](https://github.com/carlspring/s3fs-nio) library. + +## Directory Structure (S3) + +Test resources are copied to an S3 URI of the form: + +``` +s3:///[bucket-name]/[prefix]/[TestClass]-[testMethod]/[resource-path] +``` + +For example, with the default configuration: + +``` +s3:///idempotence-test-resources/MyTest-testSingleFile/foo.txt +``` + +## Configuration + +Set the `org.carlspring.testing.idempotence.basedir` system property to an S3 URI: + +| Example | Description | +|---------|-------------| +| `s3:///my-bucket` | Default AWS S3 endpoint, bucket `my-bucket` | +| `s3:///my-bucket/prefix` | Default AWS S3 endpoint, bucket `my-bucket`, key prefix `prefix` | +| `s3://localhost:9090/my-bucket` | Custom endpoint (e.g. MinIO), bucket `my-bucket` | + +If no system property is set, the default base directory is `s3:///idempotence-test-resources`. + +## Usage + +Use the `S3TestResourceExtension` JUnit Jupiter extension instead of (or in addition to) the +standard `TestResourceExtension`: + +```java +@ExtendWith(S3TestResourceExtension.class) +class MyTest { + + @Test + @TestResources(@TestResource(source = "classpath:/foo.txt")) + void testSingleFile() { + // The file is available in S3 at: + // s3:///idempotence-test-resources/MyTest-testSingleFile/foo.txt + } + +} +``` + +## S3 Credentials + +S3 credentials and region are resolved by the AWS SDK's default credential provider chain. +For local development with MinIO, configure the endpoint via the system property and supply +credentials via the standard AWS environment variables or `~/.aws/credentials`. diff --git a/idempotence-s3fs-nio/pom.xml b/idempotence-s3fs-nio/pom.xml new file mode 100644 index 0000000..a7eb846 --- /dev/null +++ b/idempotence-s3fs-nio/pom.xml @@ -0,0 +1,144 @@ + + + 4.0.0 + + + org.carlspring.testing.idempotence + idempotence-parent + 1.0.0-SNAPSHOT + ../idempotence-parent + + + org.carlspring.testing.idempotence + idempotence-s3fs-nio + 1.0.0-SNAPSHOT + jar + + Idempotence S3FS NIO + S3 extension for the Idempotence project using s3fs-nio + https://carlspring.github.io/idempotence/ + + + 3.0.0 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + org.apache.maven.plugins + maven-source-plugin + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + + + + org.carlspring.testing.idempotence + idempotence-core + ${project.version} + + + + org.carlspring.cloud.aws + s3fs-nio + ${version.s3fs-nio} + + + + org.junit.jupiter + junit-jupiter-api + + + org.junit.jupiter + junit-jupiter-engine + + + + org.slf4j + slf4j-api + + + + ch.qos.logback + logback-core + + + ch.qos.logback + logback-classic + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + The Apache License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + carlspring + Martin Todorov + carlspring@gmail.com + https://github.com/carlspring + + + + + https://github.com/carlspring/idempotence + scm:git:git://github.com/carlspring/idempotence.git + scm:git:ssh://git@github.com:carlspring/idempotence.git + ${project.version} + + + + GitHub Issues + https://github.com/carlspring/idempotence/issues + + + + GitHub Actions + https://github.com/carlspring/idempotence/actions + + + + + central + Maven Central Release Repository + https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ + + + central + Maven Central Snapshot Repository + https://s01.oss.sonatype.org/content/repositories/snapshots/ + + + + diff --git a/idempotence-s3fs-nio/src/main/java/org/carlspring/testing/idempotence/config/S3fsNioIdempotenceProperties.java b/idempotence-s3fs-nio/src/main/java/org/carlspring/testing/idempotence/config/S3fsNioIdempotenceProperties.java new file mode 100644 index 0000000..da98e98 --- /dev/null +++ b/idempotence-s3fs-nio/src/main/java/org/carlspring/testing/idempotence/config/S3fsNioIdempotenceProperties.java @@ -0,0 +1,46 @@ +package org.carlspring.testing.idempotence.config; + +import org.springframework.stereotype.Service; + +/** + * S3-specific implementation of {@link AbstractIdempotenceProperties} that uses + * an S3 URI as the base directory for test resources. + *

+ * The default base directory is {@code s3:///idempotence-test-resources}, which points + * to a bucket named {@code idempotence-test-resources} on the default AWS S3 endpoint. + * Override the {@code org.carlspring.testing.idempotence.basedir} system property to + * specify a different S3 URI (e.g. {@code s3://localhost:9090/my-bucket/prefix} for + * a local S3-compatible server such as MinIO). + *

+ * + * @author carlspring + */ +@Service +public class S3fsNioIdempotenceProperties + extends AbstractIdempotenceProperties +{ + + private String basedir = System.getProperty("org.carlspring.testing.idempotence.basedir") != null ? + System.getProperty("org.carlspring.testing.idempotence.basedir") : + "s3:///idempotence-test-resources"; + + /** + * Creates a new instance of {@link S3fsNioIdempotenceProperties}. + */ + public S3fsNioIdempotenceProperties() + { + } + + @Override + public String getBasedir() + { + return basedir; + } + + @Override + public void setBasedir(String basedir) + { + this.basedir = basedir; + } + +} diff --git a/idempotence-s3fs-nio/src/main/java/org/carlspring/testing/idempotence/extension/S3TestResourceExtension.java b/idempotence-s3fs-nio/src/main/java/org/carlspring/testing/idempotence/extension/S3TestResourceExtension.java new file mode 100644 index 0000000..dcf0b24 --- /dev/null +++ b/idempotence-s3fs-nio/src/main/java/org/carlspring/testing/idempotence/extension/S3TestResourceExtension.java @@ -0,0 +1,148 @@ +package org.carlspring.testing.idempotence.extension; + +import org.carlspring.testing.idempotence.annotation.TestResources; +import org.carlspring.testing.idempotence.config.IdempotencePropertiesService; +import org.carlspring.testing.idempotence.io.S3ResourceCopier; +import org.carlspring.testing.idempotence.util.S3FileSystemUtils; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.net.URI; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.FileVisitResult; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Optional; + +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * JUnit Jupiter extension that copies test resources to an S3 bucket before each test method + * is executed. It also cleans up the S3 "directory" before copying to ensure a clean, + * isolated environment. + *

+ * Uses s3fs-nio to interact with S3 via + * the Java NIO2 API. + *

+ *

+ * Configure the target S3 location by setting the + * {@code org.carlspring.testing.idempotence.basedir} system property to an S3 URI + * (e.g. {@code s3:///my-bucket/test-resources} or + * {@code s3://localhost:9090/my-bucket/test-resources} for a local MinIO instance). + *

+ * + * @author carlspring + */ +public class S3TestResourceExtension + implements BeforeEachCallback, BeforeAllCallback +{ + + private static final Logger logger = LoggerFactory.getLogger(S3TestResourceExtension.class); + + private final S3ResourceCopier s3ResourceCopier = + new S3ResourceCopier(IdempotencePropertiesService.getInstance() + .getIdempotenceProperties() + .getBasedir()); + + + /** + * Creates a new instance of {@link S3TestResourceExtension}. + */ + public S3TestResourceExtension() + { + } + + @Override + public void beforeAll(ExtensionContext context) + throws Exception + { + } + + @Override + public void beforeEach(ExtensionContext context) + throws Exception + { + cleanUp(context); + copyResources(context.getRequiredTestMethod().getAnnotation(TestResources.class), context); + } + + /** + * Removes any existing objects in the S3 "directory" for the current test method, + * ensuring a clean isolated environment for the current execution. + */ + private void cleanUp(ExtensionContext context) + throws IOException + { + String basedir = IdempotencePropertiesService.getInstance() + .getIdempotenceProperties() + .getBasedir(); + String testResourceDir = getTestResourceDirectory(context); + + URI baseUri = URI.create(basedir); + FileSystem s3FileSystem = S3FileSystemUtils.getOrCreateS3FileSystem(baseUri); + + Path s3TestDir = s3FileSystem.getPath(baseUri.getPath() + "/" + testResourceDir); + + if (!Files.exists(s3TestDir)) + { + return; + } + + Files.walkFileTree(s3TestDir, new SimpleFileVisitor<>() + { + @Override + public FileVisitResult visitFile(Path file, + BasicFileAttributes attrs) + throws IOException + { + logger.debug("Removing '{}'...", file); + + Files.deleteIfExists(file); + + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, + IOException exc) + throws IOException + { + if (!dir.equals(s3TestDir)) + { + logger.debug("Removing '{}'...", dir); + + Files.deleteIfExists(dir); + } + + return FileVisitResult.CONTINUE; + } + }); + } + + private void copyResources(TestResources testResources, + ExtensionContext context) + throws IOException + { + if (testResources != null) + { + String testResourceDir = getTestResourceDirectory(context); + s3ResourceCopier.copyResources(testResources.value(), testResourceDir); + } + } + + private String getTestResourceDirectory(ExtensionContext context) + { + String className = context.getRequiredTestClass().getSimpleName(); + Optional testMethod = context.getTestMethod(); + + return testMethod.map(method -> String.format("%s-%s", className, method.getName())) + .orElse(className); + } + +} diff --git a/idempotence-s3fs-nio/src/main/java/org/carlspring/testing/idempotence/io/S3ResourceCopier.java b/idempotence-s3fs-nio/src/main/java/org/carlspring/testing/idempotence/io/S3ResourceCopier.java new file mode 100644 index 0000000..6cec55c --- /dev/null +++ b/idempotence-s3fs-nio/src/main/java/org/carlspring/testing/idempotence/io/S3ResourceCopier.java @@ -0,0 +1,167 @@ +package org.carlspring.testing.idempotence.io; + +import org.carlspring.testing.idempotence.annotation.TestResource; +import org.carlspring.testing.idempotence.config.PathTransformerService; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import org.carlspring.testing.idempotence.util.S3FileSystemUtils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; + +/** + * Copies test resources from the classpath to an S3 bucket using the + * s3fs-nio library. + *

+ * This copier resolves the S3 {@link FileSystem} from the configured base URI and + * writes each resource using the NIO2 {@link Files#copy(java.io.InputStream, Path, java.nio.file.CopyOption...)} API. + *

+ * + * @author carlspring + */ +public class S3ResourceCopier +{ + + private static final Logger logger = LoggerFactory.getLogger(S3ResourceCopier.class); + + private final PathTransformer pathTransformer = PathTransformerService.getInstance().getPathTransformer(); + + private final String resourceBaseUri; + + + /** + * Creates a new instance of {@link S3ResourceCopier}. + * + * @param resourceBaseUri the S3 base URI to which test resources will be copied + * (e.g. {@code s3:///my-bucket/test-resources}) + */ + public S3ResourceCopier(String resourceBaseUri) + { + this.resourceBaseUri = resourceBaseUri; + } + + /** + * Copies the given test resources to the specified directory within the S3 base URI. + * + * @param testResources the array of {@link TestResource} annotations describing the resources to copy + * @param testResourceDir the relative directory within the base URI where resources should be placed + * @throws IOException if an I/O error occurs during copying + */ + public void copyResources(TestResource[] testResources, + String testResourceDir) + throws IOException + { + URI baseUri = URI.create(resourceBaseUri); + FileSystem s3FileSystem = S3FileSystemUtils.getOrCreateS3FileSystem(baseUri); + + PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); + + for (TestResource testResource : testResources) + { + logger.debug("testResource: {}", testResource.source()); + + String sourcePath = testResource.source(); + + Resource[] resources = resolver.getResources(sourcePath); + + for (Resource resource : resources) + { + try + { + String relativePath = resolveRelativePath(testResource, resource); + + Path s3DestDir = buildS3DestinationDir(s3FileSystem, baseUri, testResourceDir, testResource); + Path s3DestFile = s3DestDir.resolve(relativePath); + + if (logger.isDebugEnabled()) + { + logger.debug("relativePath: {}", relativePath); + logger.debug("s3DestDir: {}", s3DestDir); + logger.debug("s3DestFile: {}", s3DestFile); + } + + createParentDirectories(s3DestFile); + + logger.debug("Copying {} to {}...", resource.getDescription(), s3DestFile); + + Files.copy(resource.getInputStream(), + s3DestFile, + StandardCopyOption.REPLACE_EXISTING); + } + catch (IOException e) + { + logger.error(e.getMessage(), e); + } + } + } + } + + private String resolveRelativePath(TestResource testResource, + Resource resource) + throws IOException + { + if (resource instanceof ClassPathResource classPathResource) + { + if (testResource.destinationDir().isEmpty()) + { + return pathTransformer.relativize(classPathResource.getFile().toPath()).toString(); + } + else + { + return pathTransformer.relativize( + classPathResource.getFile().toPath().getFileName()).toString(); + } + } + else + { + if (testResource.destinationDir().isEmpty()) + { + return pathTransformer.relativize(resource.getFile().toPath()).toString(); + } + else + { + return pathTransformer.relativize( + resource.getFile().toPath().getFileName()).toString(); + } + } + } + + private Path buildS3DestinationDir(FileSystem s3FileSystem, + URI baseUri, + String testResourceDir, + TestResource testResource) + { + String basePath = baseUri.getPath(); + + String destDirPath = !testResource.destinationDir().isEmpty() ? + basePath + "/" + testResourceDir + "/" + testResource.destinationDir() : + basePath + "/" + testResourceDir; + + return s3FileSystem.getPath(destDirPath); + } + + private void createParentDirectories(Path path) + { + Path parent = path.getParent(); + if (parent != null) + { + try + { + Files.createDirectories(parent); + } + catch (IOException e) + { + logger.debug("Could not create parent directories for {}: {}", path, e.getMessage()); + } + } + } + +} diff --git a/idempotence-s3fs-nio/src/main/java/org/carlspring/testing/idempotence/io/S3fsNioPathTransformer.java b/idempotence-s3fs-nio/src/main/java/org/carlspring/testing/idempotence/io/S3fsNioPathTransformer.java new file mode 100644 index 0000000..f27beee --- /dev/null +++ b/idempotence-s3fs-nio/src/main/java/org/carlspring/testing/idempotence/io/S3fsNioPathTransformer.java @@ -0,0 +1,80 @@ +package org.carlspring.testing.idempotence.io; + +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * S3-specific implementation of {@link PathTransformer}. + *

+ * Relativizes classpath resource paths so that only the meaningful portion + * (relative to the classpath root) is used when constructing destination paths in S3. + * Handles both Maven ({@code target/test-classes}) and Gradle + * ({@code build/resources/test}, {@code build/classes/java/test}) build output directories. + *

+ * + * @author carlspring + */ +public class S3fsNioPathTransformer + implements PathTransformer +{ + + private static final String MAVEN_TEST_CLASSES = "target/test-classes"; + + private static final String GRADLE_RESOURCES_TEST = "build/resources/test"; + + private static final String GRADLE_CLASSES_TEST = "build/classes/java/test"; + + + /** + * Creates a new instance of {@link S3fsNioPathTransformer}. + */ + public S3fsNioPathTransformer() + { + } + + /** + * At present, this is a no-op method. + * + * @param basePath the base path (unused) + * @param path the resource path to transform + * @return the unchanged resource path + */ + @Override + public Path transform(Path basePath, Path path) + { + return path; + } + + /** + * Relativizes the path based on known Maven and Gradle build output directory patterns. + * + * @param path the path to relativize + * @return the relativized path, with any known build output directory prefix removed + */ + @Override + public Path relativize(Path path) + { + String pathString = path.toString(); + + if (pathString.contains(MAVEN_TEST_CLASSES)) + { + return Paths.get(pathString.substring(pathString.indexOf(MAVEN_TEST_CLASSES) + + MAVEN_TEST_CLASSES.length() + 1)); + } + else if (pathString.contains(GRADLE_RESOURCES_TEST)) + { + return Paths.get(pathString.substring(pathString.indexOf(GRADLE_RESOURCES_TEST) + + GRADLE_RESOURCES_TEST.length() + 1)); + } + else if (pathString.contains(GRADLE_CLASSES_TEST)) + { + return Paths.get(pathString.substring(pathString.indexOf(GRADLE_CLASSES_TEST) + + GRADLE_CLASSES_TEST.length() + 1)); + } + else + { + return path; + } + } + +} diff --git a/idempotence-s3fs-nio/src/main/java/org/carlspring/testing/idempotence/util/S3FileSystemUtils.java b/idempotence-s3fs-nio/src/main/java/org/carlspring/testing/idempotence/util/S3FileSystemUtils.java new file mode 100644 index 0000000..0cc653f --- /dev/null +++ b/idempotence-s3fs-nio/src/main/java/org/carlspring/testing/idempotence/util/S3FileSystemUtils.java @@ -0,0 +1,66 @@ +package org.carlspring.testing.idempotence.util; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.util.Collections; + +/** + * Utility class for working with S3 {@link FileSystem} instances via the s3fs-nio provider. + * + * @author carlspring + */ +public final class S3FileSystemUtils +{ + + private S3FileSystemUtils() + { + } + + /** + * Returns an existing S3 {@link FileSystem} for the root URI derived from the given base URI, + * or creates and registers a new one if none exists yet. + *

+ * The root URI is constructed from the scheme, optional host, and optional port of the provided + * URI (e.g. {@code s3:///} or {@code s3://localhost:9090/}). + *

+ * + * @param baseUri the S3 base URI (e.g. {@code s3:///my-bucket/prefix}) + * @return the S3 {@link FileSystem} + * @throws IOException if an I/O error occurs while creating a new {@link FileSystem} + */ + public static FileSystem getOrCreateS3FileSystem(URI baseUri) + throws IOException + { + URI s3RootUri = buildRootUri(baseUri); + try + { + return FileSystems.getFileSystem(s3RootUri); + } + catch (java.nio.file.FileSystemNotFoundException e) + { + return FileSystems.newFileSystem(s3RootUri, Collections.emptyMap()); + } + } + + private static URI buildRootUri(URI baseUri) + { + StringBuilder sb = new StringBuilder(baseUri.getScheme()).append("://"); + + if (baseUri.getHost() != null) + { + sb.append(baseUri.getHost()); + } + + if (baseUri.getPort() != -1) + { + sb.append(":").append(baseUri.getPort()); + } + + sb.append("/"); + + return URI.create(sb.toString()); + } + +} diff --git a/idempotence-s3fs-nio/src/main/resources/META-INF/services/org.carlspring.testing.idempotence.config.IdempotenceProperties b/idempotence-s3fs-nio/src/main/resources/META-INF/services/org.carlspring.testing.idempotence.config.IdempotenceProperties new file mode 100644 index 0000000..3801705 --- /dev/null +++ b/idempotence-s3fs-nio/src/main/resources/META-INF/services/org.carlspring.testing.idempotence.config.IdempotenceProperties @@ -0,0 +1 @@ +org.carlspring.testing.idempotence.config.S3fsNioIdempotenceProperties diff --git a/idempotence-s3fs-nio/src/main/resources/META-INF/services/org.carlspring.testing.idempotence.io.PathTransformer b/idempotence-s3fs-nio/src/main/resources/META-INF/services/org.carlspring.testing.idempotence.io.PathTransformer new file mode 100644 index 0000000..cea123f --- /dev/null +++ b/idempotence-s3fs-nio/src/main/resources/META-INF/services/org.carlspring.testing.idempotence.io.PathTransformer @@ -0,0 +1 @@ +org.carlspring.testing.idempotence.io.S3fsNioPathTransformer diff --git a/idempotence-s3fs-nio/src/test/java/org/carlspring/testing/idempotence/io/S3fsNioPathTransformerTest.java b/idempotence-s3fs-nio/src/test/java/org/carlspring/testing/idempotence/io/S3fsNioPathTransformerTest.java new file mode 100644 index 0000000..8039849 --- /dev/null +++ b/idempotence-s3fs-nio/src/test/java/org/carlspring/testing/idempotence/io/S3fsNioPathTransformerTest.java @@ -0,0 +1,82 @@ +package org.carlspring.testing.idempotence.io; + +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author carlspring + */ +class S3fsNioPathTransformerTest +{ + + @Test + void testTransformIsNoOp() + { + Path basePath = Paths.get("target/test-classes"); + Path resourcePath = Paths.get("foo.txt"); + + S3fsNioPathTransformer transformer = new S3fsNioPathTransformer(); + Path transformedPath = transformer.transform(basePath, resourcePath); + + assertEquals("foo.txt", transformedPath.toString()); + } + + @Test + void testRelativizeMavenPath() + { + Path path = Paths.get("/home/runner/work/project/target/test-classes/foo.txt"); + + S3fsNioPathTransformer transformer = new S3fsNioPathTransformer(); + Path relativized = transformer.relativize(path); + + assertEquals("foo.txt", relativized.toString()); + } + + @Test + void testRelativizeMavenNestedPath() + { + Path path = Paths.get("/home/runner/work/project/target/test-classes/nested/dir/foo.txt"); + + S3fsNioPathTransformer transformer = new S3fsNioPathTransformer(); + Path relativized = transformer.relativize(path); + + assertEquals("nested/dir/foo.txt", relativized.toString()); + } + + @Test + void testRelativizeGradleResourcesPath() + { + Path path = Paths.get("/home/runner/work/project/build/resources/test/foo.txt"); + + S3fsNioPathTransformer transformer = new S3fsNioPathTransformer(); + Path relativized = transformer.relativize(path); + + assertEquals("foo.txt", relativized.toString()); + } + + @Test + void testRelativizeGradleClassesPath() + { + Path path = Paths.get("/home/runner/work/project/build/classes/java/test/nested/dir/foo.txt"); + + S3fsNioPathTransformer transformer = new S3fsNioPathTransformer(); + Path relativized = transformer.relativize(path); + + assertEquals("nested/dir/foo.txt", relativized.toString()); + } + + @Test + void testRelativizeUnknownPath() + { + Path path = Paths.get("foo.txt"); + + S3fsNioPathTransformer transformer = new S3fsNioPathTransformer(); + Path relativized = transformer.relativize(path); + + assertEquals("foo.txt", relativized.toString()); + } + +} diff --git a/idempotence-s3fs-nio/src/test/java/org/carlspring/testing/idempotence/util/S3fsNioTestMethodUtilsTest.java b/idempotence-s3fs-nio/src/test/java/org/carlspring/testing/idempotence/util/S3fsNioTestMethodUtilsTest.java new file mode 100644 index 0000000..5eaf569 --- /dev/null +++ b/idempotence-s3fs-nio/src/test/java/org/carlspring/testing/idempotence/util/S3fsNioTestMethodUtilsTest.java @@ -0,0 +1,55 @@ +package org.carlspring.testing.idempotence.util; + +import org.carlspring.testing.idempotence.config.IdempotencePropertiesService; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author carlspring + */ +// This is running in sequential mode because we're overriding the default behavior of the +// IdempotenceProperties for the purpose of testing. +@Execution(ExecutionMode.SAME_THREAD) +class S3fsNioTestMethodUtilsTest +{ + + + @Test + void testMethodsDefault() + { + TestInvocationDetails details = TestMethodService.getTestInvocationDetails(Thread.currentThread().getStackTrace()); + + assertEquals("S3fsNioTestMethodUtilsTest", details.getClassName()); + assertEquals("testMethodsDefault", details.getMethodName()); + assertEquals("s3:///idempotence-test-resources/S3fsNioTestMethodUtilsTest-testMethodsDefault", + details.getPathToMethodTestResources()); + } + + @Test + void testMethodsWithFQDNPath() + { + IdempotencePropertiesService.getInstance() + .getIdempotenceProperties() + .setUseFullyQualifiedClassNamePrefixes(true); + + try + { + TestInvocationDetails details = TestMethodService.getTestInvocationDetails(Thread.currentThread().getStackTrace()); + + assertEquals("org/carlspring/testing/idempotence/util/S3fsNioTestMethodUtilsTest", details.getClassName()); + assertEquals("testMethodsWithFQDNPath", details.getMethodName()); + assertEquals("s3:///idempotence-test-resources/org/carlspring/testing/idempotence/util/S3fsNioTestMethodUtilsTest-testMethodsWithFQDNPath", + details.getPathToMethodTestResources()); + } + finally + { + IdempotencePropertiesService.getInstance() + .getIdempotenceProperties() + .setUseFullyQualifiedClassNamePrefixes(false); + } + } + +} diff --git a/idempotence-s3fs-nio/src/test/resources/foo.txt b/idempotence-s3fs-nio/src/test/resources/foo.txt new file mode 100644 index 0000000..7c6ded1 --- /dev/null +++ b/idempotence-s3fs-nio/src/test/resources/foo.txt @@ -0,0 +1 @@ +foo.txt diff --git a/idempotence-s3fs-nio/src/test/resources/logback.xml b/idempotence-s3fs-nio/src/test/resources/logback.xml new file mode 100644 index 0000000..7403512 --- /dev/null +++ b/idempotence-s3fs-nio/src/test/resources/logback.xml @@ -0,0 +1,46 @@ + + + + + + + true + + + + + %d{HH:mm:ss.SSS dd-MM-yyyy} | %-5.5p | %-20.20t | %-50.50logger{50} | %m%n + + + + + target/idempotence.log + true + + %d{HH:mm:ss.SSS dd-MM-yyyy} | %-5.5p | %-20.20t | %-50.50logger{50} | %m%n + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/idempotence-s3fs-nio/src/test/resources/nested/dir/foo.txt b/idempotence-s3fs-nio/src/test/resources/nested/dir/foo.txt new file mode 100644 index 0000000..2e850fa --- /dev/null +++ b/idempotence-s3fs-nio/src/test/resources/nested/dir/foo.txt @@ -0,0 +1 @@ +nested/dir/foo.txt diff --git a/pom.xml b/pom.xml index 391446c..c08648a 100644 --- a/pom.xml +++ b/pom.xml @@ -23,6 +23,7 @@ idempotence-maven idempotence-gradle idempotence-gradle-integration-tests + idempotence-s3fs-nio From 46fc0ad3369a8f32ff3f48f01f7d369c68bbbf69 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 22:25:31 +0000 Subject: [PATCH 2/4] Rename system property to org.carlspring.testing.idempotence.s3fs.basedir Co-authored-by: carlspring <1436265+carlspring@users.noreply.github.com> --- .../developers-guide/modules/idempotence-s3fs-nio.md | 2 +- .../idempotence/config/S3fsNioIdempotenceProperties.java | 6 +++--- .../idempotence/extension/S3TestResourceExtension.java | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/content/developers-guide/modules/idempotence-s3fs-nio.md b/docs/content/developers-guide/modules/idempotence-s3fs-nio.md index 3d0924c..4e869ae 100644 --- a/docs/content/developers-guide/modules/idempotence-s3fs-nio.md +++ b/docs/content/developers-guide/modules/idempotence-s3fs-nio.md @@ -19,7 +19,7 @@ s3:///idempotence-test-resources/MyTest-testSingleFile/foo.txt ## Configuration -Set the `org.carlspring.testing.idempotence.basedir` system property to an S3 URI: +Set the `org.carlspring.testing.idempotence.s3fs.basedir` system property to an S3 URI: | Example | Description | |---------|-------------| diff --git a/idempotence-s3fs-nio/src/main/java/org/carlspring/testing/idempotence/config/S3fsNioIdempotenceProperties.java b/idempotence-s3fs-nio/src/main/java/org/carlspring/testing/idempotence/config/S3fsNioIdempotenceProperties.java index da98e98..6c70d83 100644 --- a/idempotence-s3fs-nio/src/main/java/org/carlspring/testing/idempotence/config/S3fsNioIdempotenceProperties.java +++ b/idempotence-s3fs-nio/src/main/java/org/carlspring/testing/idempotence/config/S3fsNioIdempotenceProperties.java @@ -8,7 +8,7 @@ *

* The default base directory is {@code s3:///idempotence-test-resources}, which points * to a bucket named {@code idempotence-test-resources} on the default AWS S3 endpoint. - * Override the {@code org.carlspring.testing.idempotence.basedir} system property to + * Override the {@code org.carlspring.testing.idempotence.s3fs.basedir} system property to * specify a different S3 URI (e.g. {@code s3://localhost:9090/my-bucket/prefix} for * a local S3-compatible server such as MinIO). *

@@ -20,8 +20,8 @@ public class S3fsNioIdempotenceProperties extends AbstractIdempotenceProperties { - private String basedir = System.getProperty("org.carlspring.testing.idempotence.basedir") != null ? - System.getProperty("org.carlspring.testing.idempotence.basedir") : + private String basedir = System.getProperty("org.carlspring.testing.idempotence.s3fs.basedir") != null ? + System.getProperty("org.carlspring.testing.idempotence.s3fs.basedir") : "s3:///idempotence-test-resources"; /** diff --git a/idempotence-s3fs-nio/src/main/java/org/carlspring/testing/idempotence/extension/S3TestResourceExtension.java b/idempotence-s3fs-nio/src/main/java/org/carlspring/testing/idempotence/extension/S3TestResourceExtension.java index dcf0b24..ef26595 100644 --- a/idempotence-s3fs-nio/src/main/java/org/carlspring/testing/idempotence/extension/S3TestResourceExtension.java +++ b/idempotence-s3fs-nio/src/main/java/org/carlspring/testing/idempotence/extension/S3TestResourceExtension.java @@ -32,7 +32,7 @@ *

*

* Configure the target S3 location by setting the - * {@code org.carlspring.testing.idempotence.basedir} system property to an S3 URI + * {@code org.carlspring.testing.idempotence.s3fs.basedir} system property to an S3 URI * (e.g. {@code s3:///my-bucket/test-resources} or * {@code s3://localhost:9090/my-bucket/test-resources} for a local MinIO instance). *

From 4de4179d2137c8b6a87b6fc2318a4d8a05330afc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 23:22:47 +0000 Subject: [PATCH 3/4] Add S3BasicFunctionalityTest with MinIO Testcontainer and lazy S3ResourceCopier Co-authored-by: carlspring <1436265+carlspring@users.noreply.github.com> --- idempotence-s3fs-nio/pom.xml | 20 +++ .../extension/S3TestResourceExtension.java | 10 +- .../idempotence/S3BasicFunctionalityTest.java | 164 ++++++++++++++++++ 3 files changed, 188 insertions(+), 6 deletions(-) create mode 100644 idempotence-s3fs-nio/src/test/java/org/carlspring/testing/idempotence/S3BasicFunctionalityTest.java diff --git a/idempotence-s3fs-nio/pom.xml b/idempotence-s3fs-nio/pom.xml index a7eb846..bb327ec 100644 --- a/idempotence-s3fs-nio/pom.xml +++ b/idempotence-s3fs-nio/pom.xml @@ -22,6 +22,7 @@ 3.0.0 + 1.21.3 @@ -92,6 +93,25 @@ spring-boot-starter-test test + + + org.testcontainers + testcontainers + ${version.testcontainers} + test + + + org.testcontainers + junit-jupiter + ${version.testcontainers} + test + + + org.testcontainers + minio + ${version.testcontainers} + test + diff --git a/idempotence-s3fs-nio/src/main/java/org/carlspring/testing/idempotence/extension/S3TestResourceExtension.java b/idempotence-s3fs-nio/src/main/java/org/carlspring/testing/idempotence/extension/S3TestResourceExtension.java index ef26595..312d1ad 100644 --- a/idempotence-s3fs-nio/src/main/java/org/carlspring/testing/idempotence/extension/S3TestResourceExtension.java +++ b/idempotence-s3fs-nio/src/main/java/org/carlspring/testing/idempotence/extension/S3TestResourceExtension.java @@ -45,11 +45,6 @@ public class S3TestResourceExtension private static final Logger logger = LoggerFactory.getLogger(S3TestResourceExtension.class); - private final S3ResourceCopier s3ResourceCopier = - new S3ResourceCopier(IdempotencePropertiesService.getInstance() - .getIdempotenceProperties() - .getBasedir()); - /** * Creates a new instance of {@link S3TestResourceExtension}. @@ -131,8 +126,11 @@ private void copyResources(TestResources testResources, { if (testResources != null) { + String basedir = IdempotencePropertiesService.getInstance() + .getIdempotenceProperties() + .getBasedir(); String testResourceDir = getTestResourceDirectory(context); - s3ResourceCopier.copyResources(testResources.value(), testResourceDir); + new S3ResourceCopier(basedir).copyResources(testResources.value(), testResourceDir); } } diff --git a/idempotence-s3fs-nio/src/test/java/org/carlspring/testing/idempotence/S3BasicFunctionalityTest.java b/idempotence-s3fs-nio/src/test/java/org/carlspring/testing/idempotence/S3BasicFunctionalityTest.java new file mode 100644 index 0000000..707af8b --- /dev/null +++ b/idempotence-s3fs-nio/src/test/java/org/carlspring/testing/idempotence/S3BasicFunctionalityTest.java @@ -0,0 +1,164 @@ +package org.carlspring.testing.idempotence; + +import org.carlspring.testing.idempotence.annotation.TestResource; +import org.carlspring.testing.idempotence.annotation.TestResources; +import org.carlspring.testing.idempotence.config.IdempotencePropertiesService; +import org.carlspring.testing.idempotence.config.S3fsNioIdempotenceProperties; +import org.carlspring.testing.idempotence.extension.S3TestResourceExtension; +import org.carlspring.testing.idempotence.util.S3FileSystemUtils; + +import java.net.URI; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.testcontainers.containers.MinIOContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.http.apache.ApacheHttpClient; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.S3Configuration; + +/** + * Functional test verifying that {@link S3TestResourceExtension} correctly copies + * resources declared via {@link TestResources} into an S3 bucket and that those + * objects can subsequently be found via the NIO2 {@link FileSystem} API. + * + *

A MinIO instance is started automatically via Testcontainers before the tests run. + * The test is skipped when Docker is not available.

+ * + * @author carlspring + */ +@Testcontainers(disabledWithoutDocker = true) +@ExtendWith(S3TestResourceExtension.class) +class S3BasicFunctionalityTest +{ + + static final String BUCKET = "idempotence-test-resources"; + + @Container + static final MinIOContainer minio = new MinIOContainer("minio/minio:RELEASE.2025-09-07T16-13-09Z"); + + @BeforeAll + static void setUp() + throws Exception + { + URI s3Endpoint = URI.create(minio.getS3URL()); + String host = s3Endpoint.getHost(); + int port = s3Endpoint.getPort(); + + // Configure s3fs-nio system properties for MinIO + System.setProperty("s3fs.access.key", minio.getUserName()); + System.setProperty("s3fs.secret.key", minio.getPassword()); + System.setProperty("s3fs.region", "us-east-1"); + System.setProperty("s3fs.path.style.access", "true"); + System.setProperty("s3fs.protocol", "http"); + + // Update the idempotence basedir to point at the MinIO instance + String basedir = "s3://" + host + ":" + port + "/" + BUCKET; + S3fsNioIdempotenceProperties props = new S3fsNioIdempotenceProperties(); + props.setBasedir(basedir); + IdempotencePropertiesService.getInstance().setIdempotenceProperties(props); + + // Create the test bucket via the AWS SDK + try (S3Client s3Client = S3Client.builder() + .endpointOverride(URI.create(minio.getS3URL())) + .credentialsProvider(StaticCredentialsProvider.create( + AwsBasicCredentials.create(minio.getUserName(), + minio.getPassword()))) + .region(Region.US_EAST_1) + .serviceConfiguration(S3Configuration.builder() + .pathStyleAccessEnabled(true) + .build()) + .httpClient(ApacheHttpClient.builder().build()) + .build()) + { + s3Client.createBucket(b -> b.bucket(BUCKET)); + } + } + + @AfterAll + static void tearDown() + { + // Close the S3 filesystem to release resources + String basedir = IdempotencePropertiesService.getInstance() + .getIdempotenceProperties() + .getBasedir(); + try + { + FileSystem s3fs = S3FileSystemUtils.getOrCreateS3FileSystem(URI.create(basedir)); + s3fs.close(); + } + catch (Exception ignored) + { + } + + // Restore the default idempotence properties + IdempotencePropertiesService.getInstance().setIdempotenceProperties(new S3fsNioIdempotenceProperties()); + + // Remove s3fs system properties set for MinIO + System.clearProperty("s3fs.access.key"); + System.clearProperty("s3fs.secret.key"); + System.clearProperty("s3fs.region"); + System.clearProperty("s3fs.path.style.access"); + System.clearProperty("s3fs.protocol"); + } + + @Test + @TestResources(@TestResource(source = "classpath:/foo.txt")) + void testSingleFile() + throws Exception + { + String basedir = IdempotencePropertiesService.getInstance() + .getIdempotenceProperties() + .getBasedir(); + URI baseUri = URI.create(basedir); + FileSystem s3fs = S3FileSystemUtils.getOrCreateS3FileSystem(baseUri); + Path s3File = s3fs.getPath(baseUri.getPath() + "/S3BasicFunctionalityTest-testSingleFile/foo.txt"); + + Assertions.assertTrue(Files.exists(s3File), "Test resource file should exist in S3!"); + } + + @Test + @TestResources(@TestResource(source = "classpath:/**/foo.txt")) + void testWithPatterns() + throws Exception + { + String basedir = IdempotencePropertiesService.getInstance() + .getIdempotenceProperties() + .getBasedir(); + URI baseUri = URI.create(basedir); + FileSystem s3fs = S3FileSystemUtils.getOrCreateS3FileSystem(baseUri); + Path s3File = s3fs.getPath(baseUri.getPath() + "/S3BasicFunctionalityTest-testWithPatterns/nested/dir/foo.txt"); + + Assertions.assertTrue(Files.exists(s3File), "Test resource file should exist in S3!"); + } + + @Test + @TestResources({ @TestResource(source = "classpath:/foo.txt"), + @TestResource(source = "classpath:/**/foo.txt") }) + void testMultipleWithPatterns() + throws Exception + { + String basedir = IdempotencePropertiesService.getInstance() + .getIdempotenceProperties() + .getBasedir(); + URI baseUri = URI.create(basedir); + FileSystem s3fs = S3FileSystemUtils.getOrCreateS3FileSystem(baseUri); + + Path s3File1 = s3fs.getPath(baseUri.getPath() + "/S3BasicFunctionalityTest-testMultipleWithPatterns/foo.txt"); + Assertions.assertTrue(Files.exists(s3File1), "Test resource file should exist in S3!"); + + Path s3File2 = s3fs.getPath(baseUri.getPath() + "/S3BasicFunctionalityTest-testMultipleWithPatterns/nested/dir/foo.txt"); + Assertions.assertTrue(Files.exists(s3File2), "Test resource file should exist in S3!"); + } + +} From 90422b0fdb454bcfa14b0992584bf8575f6b2950 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 02:05:57 +0000 Subject: [PATCH 4/4] Rename S3BasicFunctionalityTest to MinIO variant and add S3BasicFunctionalityAWSTest Co-authored-by: carlspring <1436265+carlspring@users.noreply.github.com> --- .../S3BasicFunctionalityAWSTest.java | 191 ++++++++++++++++++ ...ava => S3BasicFunctionalityMinIOTest.java} | 12 +- 2 files changed, 197 insertions(+), 6 deletions(-) create mode 100644 idempotence-s3fs-nio/src/test/java/org/carlspring/testing/idempotence/S3BasicFunctionalityAWSTest.java rename idempotence-s3fs-nio/src/test/java/org/carlspring/testing/idempotence/{S3BasicFunctionalityTest.java => S3BasicFunctionalityMinIOTest.java} (95%) diff --git a/idempotence-s3fs-nio/src/test/java/org/carlspring/testing/idempotence/S3BasicFunctionalityAWSTest.java b/idempotence-s3fs-nio/src/test/java/org/carlspring/testing/idempotence/S3BasicFunctionalityAWSTest.java new file mode 100644 index 0000000..d8074c4 --- /dev/null +++ b/idempotence-s3fs-nio/src/test/java/org/carlspring/testing/idempotence/S3BasicFunctionalityAWSTest.java @@ -0,0 +1,191 @@ +package org.carlspring.testing.idempotence; + +import org.carlspring.testing.idempotence.annotation.TestResource; +import org.carlspring.testing.idempotence.annotation.TestResources; +import org.carlspring.testing.idempotence.config.IdempotencePropertiesService; +import org.carlspring.testing.idempotence.config.S3fsNioIdempotenceProperties; +import org.carlspring.testing.idempotence.extension.S3TestResourceExtension; +import org.carlspring.testing.idempotence.util.S3FileSystemUtils; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.FileSystem; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +import org.junit.jupiter.api.extension.ExtendWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Functional test verifying that {@link S3TestResourceExtension} correctly copies + * resources declared via {@link TestResources} into a real AWS S3 bucket and that those + * objects can subsequently be found via the NIO2 {@link FileSystem} API. + * + *

This test is only enabled when the {@code AWS_ACCESS_KEY_ID} environment variable + * is set, indicating that explicit AWS credentials are available. The target S3 location + * is configured via the {@code org.carlspring.testing.idempotence.s3fs.basedir} system + * property (defaults to {@code s3:///idempotence-test-resources}).

+ * + *

Prerequisites:

+ *
    + *
  • AWS credentials must be available (e.g. via {@code AWS_ACCESS_KEY_ID} / + * {@code AWS_SECRET_ACCESS_KEY} environment variables or {@code ~/.aws/credentials}).
  • + *
  • The target S3 bucket must already exist and the credentials must have + * read/write/delete permissions on it.
  • + *
  • The AWS region can be configured via the {@code s3fs.region} system property or + * the {@code AWS_DEFAULT_REGION} environment variable.
  • + *
+ * + * @author carlspring + */ +@EnabledIfEnvironmentVariable(named = "AWS_ACCESS_KEY_ID", matches = "\\S+") +@ExtendWith(S3TestResourceExtension.class) +class S3BasicFunctionalityAWSTest +{ + + private static final Logger logger = LoggerFactory.getLogger(S3BasicFunctionalityAWSTest.class); + + @BeforeAll + static void setUp() + { + // Use the default S3fsNioIdempotenceProperties which reads the basedir from the + // org.carlspring.testing.idempotence.s3fs.basedir system property, or defaults to + // s3:///idempotence-test-resources. AWS credentials and region are resolved via the + // SDK's default provider chain (env vars, ~/.aws/credentials, EC2 instance profile). + S3fsNioIdempotenceProperties props = new S3fsNioIdempotenceProperties(); + IdempotencePropertiesService.getInstance().setIdempotenceProperties(props); + + logger.info("Running AWS S3 functional tests against: {}", props.getBasedir()); + } + + @AfterAll + static void tearDown() + { + String basedir = IdempotencePropertiesService.getInstance() + .getIdempotenceProperties() + .getBasedir(); + URI baseUri = URI.create(basedir); + + // Clean up all test resource directories created by this test class + try + { + FileSystem s3fs = S3FileSystemUtils.getOrCreateS3FileSystem(baseUri); + cleanDirectory(s3fs, baseUri, "S3BasicFunctionalityAWSTest-testSingleFile"); + cleanDirectory(s3fs, baseUri, "S3BasicFunctionalityAWSTest-testWithPatterns"); + cleanDirectory(s3fs, baseUri, "S3BasicFunctionalityAWSTest-testMultipleWithPatterns"); + s3fs.close(); + } + catch (Exception e) + { + logger.warn("Failed to clean up S3 test resources: {}", e.getMessage()); + } + + // Restore the default idempotence properties + IdempotencePropertiesService.getInstance().setIdempotenceProperties(new S3fsNioIdempotenceProperties()); + } + + @Test + @TestResources(@TestResource(source = "classpath:/foo.txt")) + void testSingleFile() + throws Exception + { + String basedir = IdempotencePropertiesService.getInstance() + .getIdempotenceProperties() + .getBasedir(); + URI baseUri = URI.create(basedir); + FileSystem s3fs = S3FileSystemUtils.getOrCreateS3FileSystem(baseUri); + Path s3File = s3fs.getPath(baseUri.getPath() + "/S3BasicFunctionalityAWSTest-testSingleFile/foo.txt"); + + Assertions.assertTrue(Files.exists(s3File), "Test resource file should exist in S3!"); + } + + @Test + @TestResources(@TestResource(source = "classpath:/**/foo.txt")) + void testWithPatterns() + throws Exception + { + String basedir = IdempotencePropertiesService.getInstance() + .getIdempotenceProperties() + .getBasedir(); + URI baseUri = URI.create(basedir); + FileSystem s3fs = S3FileSystemUtils.getOrCreateS3FileSystem(baseUri); + Path s3File = s3fs.getPath(baseUri.getPath() + "/S3BasicFunctionalityAWSTest-testWithPatterns/nested/dir/foo.txt"); + + Assertions.assertTrue(Files.exists(s3File), "Test resource file should exist in S3!"); + } + + @Test + @TestResources({ @TestResource(source = "classpath:/foo.txt"), + @TestResource(source = "classpath:/**/foo.txt") }) + void testMultipleWithPatterns() + throws Exception + { + String basedir = IdempotencePropertiesService.getInstance() + .getIdempotenceProperties() + .getBasedir(); + URI baseUri = URI.create(basedir); + FileSystem s3fs = S3FileSystemUtils.getOrCreateS3FileSystem(baseUri); + + Path s3File1 = s3fs.getPath(baseUri.getPath() + "/S3BasicFunctionalityAWSTest-testMultipleWithPatterns/foo.txt"); + Assertions.assertTrue(Files.exists(s3File1), "Test resource file should exist in S3!"); + + Path s3File2 = s3fs.getPath(baseUri.getPath() + "/S3BasicFunctionalityAWSTest-testMultipleWithPatterns/nested/dir/foo.txt"); + Assertions.assertTrue(Files.exists(s3File2), "Test resource file should exist in S3!"); + } + + private static void cleanDirectory(FileSystem s3fs, + URI baseUri, + String dirName) + { + Path s3Dir = s3fs.getPath(baseUri.getPath() + "/" + dirName); + + if (!Files.exists(s3Dir)) + { + return; + } + + try + { + Files.walkFileTree(s3Dir, new SimpleFileVisitor<>() + { + @Override + public FileVisitResult visitFile(Path file, + BasicFileAttributes attrs) + throws IOException + { + Files.deleteIfExists(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, + IOException exc) + throws IOException + { + if (exc != null) + { + logger.warn("Error visiting directory {}: {}", dir, exc.getMessage()); + return FileVisitResult.CONTINUE; + } + + Files.deleteIfExists(dir); + return FileVisitResult.CONTINUE; + } + }); + } + catch (IOException e) + { + logger.warn("Failed to clean S3 directory {}: {}", dirName, e.getMessage()); + } + } + +} diff --git a/idempotence-s3fs-nio/src/test/java/org/carlspring/testing/idempotence/S3BasicFunctionalityTest.java b/idempotence-s3fs-nio/src/test/java/org/carlspring/testing/idempotence/S3BasicFunctionalityMinIOTest.java similarity index 95% rename from idempotence-s3fs-nio/src/test/java/org/carlspring/testing/idempotence/S3BasicFunctionalityTest.java rename to idempotence-s3fs-nio/src/test/java/org/carlspring/testing/idempotence/S3BasicFunctionalityMinIOTest.java index 707af8b..f536998 100644 --- a/idempotence-s3fs-nio/src/test/java/org/carlspring/testing/idempotence/S3BasicFunctionalityTest.java +++ b/idempotence-s3fs-nio/src/test/java/org/carlspring/testing/idempotence/S3BasicFunctionalityMinIOTest.java @@ -29,7 +29,7 @@ /** * Functional test verifying that {@link S3TestResourceExtension} correctly copies - * resources declared via {@link TestResources} into an S3 bucket and that those + * resources declared via {@link TestResources} into a MinIO bucket and that those * objects can subsequently be found via the NIO2 {@link FileSystem} API. * *

A MinIO instance is started automatically via Testcontainers before the tests run. @@ -39,7 +39,7 @@ */ @Testcontainers(disabledWithoutDocker = true) @ExtendWith(S3TestResourceExtension.class) -class S3BasicFunctionalityTest +class S3BasicFunctionalityMinIOTest { static final String BUCKET = "idempotence-test-resources"; @@ -122,7 +122,7 @@ void testSingleFile() .getBasedir(); URI baseUri = URI.create(basedir); FileSystem s3fs = S3FileSystemUtils.getOrCreateS3FileSystem(baseUri); - Path s3File = s3fs.getPath(baseUri.getPath() + "/S3BasicFunctionalityTest-testSingleFile/foo.txt"); + Path s3File = s3fs.getPath(baseUri.getPath() + "/S3BasicFunctionalityMinIOTest-testSingleFile/foo.txt"); Assertions.assertTrue(Files.exists(s3File), "Test resource file should exist in S3!"); } @@ -137,7 +137,7 @@ void testWithPatterns() .getBasedir(); URI baseUri = URI.create(basedir); FileSystem s3fs = S3FileSystemUtils.getOrCreateS3FileSystem(baseUri); - Path s3File = s3fs.getPath(baseUri.getPath() + "/S3BasicFunctionalityTest-testWithPatterns/nested/dir/foo.txt"); + Path s3File = s3fs.getPath(baseUri.getPath() + "/S3BasicFunctionalityMinIOTest-testWithPatterns/nested/dir/foo.txt"); Assertions.assertTrue(Files.exists(s3File), "Test resource file should exist in S3!"); } @@ -154,10 +154,10 @@ void testMultipleWithPatterns() URI baseUri = URI.create(basedir); FileSystem s3fs = S3FileSystemUtils.getOrCreateS3FileSystem(baseUri); - Path s3File1 = s3fs.getPath(baseUri.getPath() + "/S3BasicFunctionalityTest-testMultipleWithPatterns/foo.txt"); + Path s3File1 = s3fs.getPath(baseUri.getPath() + "/S3BasicFunctionalityMinIOTest-testMultipleWithPatterns/foo.txt"); Assertions.assertTrue(Files.exists(s3File1), "Test resource file should exist in S3!"); - Path s3File2 = s3fs.getPath(baseUri.getPath() + "/S3BasicFunctionalityTest-testMultipleWithPatterns/nested/dir/foo.txt"); + Path s3File2 = s3fs.getPath(baseUri.getPath() + "/S3BasicFunctionalityMinIOTest-testMultipleWithPatterns/nested/dir/foo.txt"); Assertions.assertTrue(Files.exists(s3File2), "Test resource file should exist in S3!"); }