From 045a4b41dc1a0c13d651fb2041a703f7947cefc1 Mon Sep 17 00:00:00 2001 From: snehal242005 Date: Mon, 13 Jul 2026 10:20:43 +0530 Subject: [PATCH] Fix DockerComposeContainer to support compose files from different directories --- .../ContainerisedDockerCompose.java | 22 +++--- .../org/testcontainers/utility/PathUtils.java | 42 ++++++++++ .../testcontainers/utility/PathUtilsTest.java | 79 +++++++++++++++++++ 3 files changed, 134 insertions(+), 9 deletions(-) create mode 100644 core/src/test/java/org/testcontainers/utility/PathUtilsTest.java diff --git a/core/src/main/java/org/testcontainers/containers/ContainerisedDockerCompose.java b/core/src/main/java/org/testcontainers/containers/ContainerisedDockerCompose.java index 7d6e60df29f..6d331defdfb 100644 --- a/core/src/main/java/org/testcontainers/containers/ContainerisedDockerCompose.java +++ b/core/src/main/java/org/testcontainers/containers/ContainerisedDockerCompose.java @@ -33,9 +33,11 @@ public ContainerisedDockerCompose( super(dockerImageName); addEnv(ENV_PROJECT_NAME, identifier); - // Map the docker compose file into the container - final File dockerComposeBaseFile = composeFiles.get(0); - final String pwd = dockerComposeBaseFile.getAbsoluteFile().getParentFile().getAbsolutePath(); + // Docker Compose (running inside the container) needs one shared root directory to resolve + // every -f file against, so map the files in relative to the closest common ancestor of all + // of them rather than assuming they all live alongside the first file. + final File composeFileRoot = PathUtils.findCommonParent(composeFiles); + final String pwd = composeFileRoot.getAbsolutePath(); final String containerPwd = convertToUnixFilesystemPath(pwd); final List absoluteDockerComposeFiles = composeFiles @@ -52,12 +54,14 @@ public ContainerisedDockerCompose( logger().info("Copying all files in {} into the container", pwd); withCopyFileToContainer(MountableFile.forHostPath(pwd), containerPwd); } else { - // Always copy the compose file itself - logger().info("Copying docker compose file: {}", dockerComposeBaseFile.getAbsolutePath()); - withCopyFileToContainer( - MountableFile.forHostPath(dockerComposeBaseFile.getAbsolutePath()), - convertToUnixFilesystemPath(dockerComposeBaseFile.getAbsolutePath()) - ); + // Always copy the compose files themselves, wherever they live under the common root + for (File composeFile : composeFiles) { + logger().info("Copying docker compose file: {}", composeFile.getAbsolutePath()); + withCopyFileToContainer( + MountableFile.forHostPath(composeFile.getAbsolutePath()), + convertToUnixFilesystemPath(composeFile.getAbsolutePath()) + ); + } for (String pathToCopy : fileCopyInclusions) { String hostPath = pwd + "/" + pathToCopy; logger().info("Copying inclusion file: {}", hostPath); diff --git a/core/src/main/java/org/testcontainers/utility/PathUtils.java b/core/src/main/java/org/testcontainers/utility/PathUtils.java index 7f8b5925689..1f280c0ca0b 100644 --- a/core/src/main/java/org/testcontainers/utility/PathUtils.java +++ b/core/src/main/java/org/testcontainers/utility/PathUtils.java @@ -3,12 +3,15 @@ import lombok.NonNull; import lombok.experimental.UtilityClass; +import java.io.File; import java.io.IOException; 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 java.util.List; +import java.util.stream.Collectors; /** * Filesystem operation utility methods. @@ -75,4 +78,43 @@ public static String createMinGWPath(String path) { mingwPath = mingwPath.replace(":", ""); return mingwPath; } + + /** + * Find the closest common ancestor directory of a set of files, e.g. so that a single directory tree can be + * used to resolve all of the files, even when they were supplied from different directories. + * + * @param files the files to find a common ancestor directory for + * @return the closest common ancestor directory of all the given files + */ + public static File findCommonParent(@NonNull List files) { + List parentDirs = files + .stream() + .map(file -> file.getAbsoluteFile().toPath().normalize().getParent()) + .collect(Collectors.toList()); + + Path commonParent = parentDirs.get(0); + for (Path parentDir : parentDirs) { + commonParent = commonAncestor(commonParent, parentDir); + } + return commonParent.toFile(); + } + + private static Path commonAncestor(Path a, Path b) { + if (!a.getRoot().equals(b.getRoot())) { + throw new IllegalArgumentException( + "Cannot determine a common parent directory for paths on different filesystem roots: " + a + " and " + b + ); + } + + Path commonAncestor = a.getRoot(); + int sharedNameCount = Math.min(a.getNameCount(), b.getNameCount()); + for (int i = 0; i < sharedNameCount; i++) { + Path aElement = a.getName(i); + if (!aElement.equals(b.getName(i))) { + break; + } + commonAncestor = commonAncestor.resolve(aElement); + } + return commonAncestor; + } } diff --git a/core/src/test/java/org/testcontainers/utility/PathUtilsTest.java b/core/src/test/java/org/testcontainers/utility/PathUtilsTest.java new file mode 100644 index 00000000000..3659244bd49 --- /dev/null +++ b/core/src/test/java/org/testcontainers/utility/PathUtilsTest.java @@ -0,0 +1,79 @@ +package org.testcontainers.utility; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; + +class PathUtilsTest { + + @TempDir + Path tempDir; + + @Test + void findCommonParentOfSingleFileIsItsOwnDirectory() throws IOException { + File composeFile = createFile(tempDir, "docker-compose.yml"); + + assertThat(PathUtils.findCommonParent(Collections.singletonList(composeFile))).isEqualTo(tempDir.toFile()); + } + + @Test + void findCommonParentOfFilesInTheSameDirectory() throws IOException { + File baseFile = createFile(tempDir, "docker-compose.yml"); + File overrideFile = createFile(tempDir, "docker-compose.override.yml"); + + assertThat(PathUtils.findCommonParent(Arrays.asList(baseFile, overrideFile))).isEqualTo(tempDir.toFile()); + } + + @Test + void findCommonParentOfFilesInDifferentDirectories() throws IOException { + // Mirrors the scenario from testcontainers/testcontainers-java#1863: a base compose file + // nested in a subdirectory, and an override file one level up. + Path firstDir = tempDir.resolve("first"); + Path nestedDir = firstDir.resolve("directory"); + Files.createDirectories(nestedDir); + + File baseFile = createFile(nestedDir, "docker-compose.yml"); + File overrideFile = createFile(firstDir, "docker-compose.override.yml"); + + assertThat(PathUtils.findCommonParent(Arrays.asList(baseFile, overrideFile))).isEqualTo(firstDir.toFile()); + } + + @Test + void findCommonParentIsOrderIndependent() throws IOException { + Path firstDir = tempDir.resolve("first"); + Path nestedDir = firstDir.resolve("directory"); + Files.createDirectories(nestedDir); + + File baseFile = createFile(nestedDir, "docker-compose.yml"); + File overrideFile = createFile(firstDir, "docker-compose.override.yml"); + + assertThat(PathUtils.findCommonParent(Arrays.asList(overrideFile, baseFile))).isEqualTo(firstDir.toFile()); + } + + @Test + void findCommonParentOfFilesInSiblingDirectories() throws IOException { + Path branchA = tempDir.resolve("a"); + Path branchB = tempDir.resolve("b"); + Files.createDirectories(branchA); + Files.createDirectories(branchB); + + File fileA = createFile(branchA, "docker-compose.yml"); + File fileB = createFile(branchB, "docker-compose.override.yml"); + + assertThat(PathUtils.findCommonParent(Arrays.asList(fileA, fileB))).isEqualTo(tempDir.toFile()); + } + + private static File createFile(Path directory, String name) throws IOException { + Path file = directory.resolve(name); + Files.write(file, new byte[0]); + return file.toFile(); + } +}