From 1de078981a5a758861913bf89c54ac14012c5b46 Mon Sep 17 00:00:00 2001 From: Srivathsav01 Date: Wed, 30 Sep 2026 00:43:38 +0530 Subject: [PATCH 1/6] Keep !override values when parsing compose files for image names --- .../containers/ParsedDockerComposeFile.java | 45 +++++++++++++- .../containers/DockerComposeFilesTest.java | 12 ++++ ...ParsedDockerComposeFileValidationTest.java | 61 +++++++++++++++++++ ...ker-compose-imagename-overriding-tag-a.yml | 7 +++ ...ker-compose-imagename-overriding-tag-b.yml | 7 +++ 5 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 core/src/test/resources/docker-compose-imagename-overriding-tag-a.yml create mode 100644 core/src/test/resources/docker-compose-imagename-overriding-tag-b.yml diff --git a/core/src/main/java/org/testcontainers/containers/ParsedDockerComposeFile.java b/core/src/main/java/org/testcontainers/containers/ParsedDockerComposeFile.java index 2d3104af6b1..fd66ca3f0e1 100644 --- a/core/src/main/java/org/testcontainers/containers/ParsedDockerComposeFile.java +++ b/core/src/main/java/org/testcontainers/containers/ParsedDockerComposeFile.java @@ -1,6 +1,7 @@ package org.testcontainers.containers; import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -12,6 +13,8 @@ import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.constructor.SafeConstructor; import org.yaml.snakeyaml.nodes.Node; +import org.yaml.snakeyaml.nodes.NodeId; +import org.yaml.snakeyaml.nodes.ScalarNode; import org.yaml.snakeyaml.nodes.Tag; import org.yaml.snakeyaml.representer.Representer; import org.yaml.snakeyaml.resolver.Resolver; @@ -32,6 +35,19 @@ @EqualsAndHashCode class ParsedDockerComposeFile { + /** + * Top-level elements of the Compose file format that are not services, e.g. in a file without a 'services' element + */ + private static final Set TOP_LEVEL_ELEMENTS = ImmutableSet.of( + "version", + "name", + "include", + "networks", + "volumes", + "configs", + "secrets" + ); + private final Map composeFileContent; private final String composeFileName; @@ -46,17 +62,22 @@ class ParsedDockerComposeFile { LoaderOptions options = new LoaderOptions(); options.setMaxAliasesForCollections(1_000); DumperOptions dumperOptions = new DumperOptions(); + Resolver resolver = new Resolver(); SafeConstructor constructor = new SafeConstructor(options) { @Override protected Object constructObject(Node node) { - if (node.getTag().equals(new Tag("!reset")) || node.getTag().equals(new Tag("!override"))) { + if (node.getTag().equals(new Tag("!reset"))) { return null; } + if (node.getTag().equals(new Tag("!override"))) { + // !override replaces the value from previous compose files, so keep the value as if it was untagged + node.setTag(resolveUntaggedTag(node, resolver)); + } return super.constructObject(node); } }; - Yaml yaml = new Yaml(constructor, new Representer(dumperOptions), dumperOptions, options, new Resolver()); + Yaml yaml = new Yaml(constructor, new Representer(dumperOptions), dumperOptions, options, resolver); try (FileInputStream fileInputStream = FileUtils.openInputStream(composeFile)) { composeFileContent = yaml.load(fileInputStream); } catch (Exception e) { @@ -76,8 +97,20 @@ protected Object constructObject(Node node) { parseAndValidate(); } + private static Tag resolveUntaggedTag(Node node, Resolver resolver) { + switch (node.getNodeId()) { + case mapping: + return Tag.MAP; + case sequence: + return Tag.SEQ; + default: + return resolver.resolve(NodeId.scalar, ((ScalarNode) node).getValue(), true); + } + } + private void parseAndValidate() { final Map servicesMap; + final boolean legacyFormat; if (composeFileContent.containsKey("version") && "2.0".equals(composeFileContent.get("version"))) { log.warn( "Testcontainers may not be able to clean up networks spawned using Docker Compose v2.0 files. " + @@ -104,12 +137,18 @@ private void parseAndValidate() { @SuppressWarnings("unchecked") Map temp = (Map) servicesElement; servicesMap = temp; + legacyFormat = false; } else { + // Compose file format v1: top-level elements are services servicesMap = composeFileContent; + legacyFormat = true; } for (Map.Entry entry : servicesMap.entrySet()) { String serviceName = entry.getKey(); + if (legacyFormat && (TOP_LEVEL_ELEMENTS.contains(serviceName) || serviceName.startsWith("x-"))) { + continue; + } Object serviceDefinition = entry.getValue(); if (!(serviceDefinition instanceof Map)) { log.debug( @@ -117,7 +156,7 @@ private void parseAndValidate() { composeFileName, serviceName ); - break; + continue; } @SuppressWarnings("unchecked") diff --git a/core/src/test/java/org/testcontainers/containers/DockerComposeFilesTest.java b/core/src/test/java/org/testcontainers/containers/DockerComposeFilesTest.java index 4704817c0e0..6d081b16000 100644 --- a/core/src/test/java/org/testcontainers/containers/DockerComposeFilesTest.java +++ b/core/src/test/java/org/testcontainers/containers/DockerComposeFilesTest.java @@ -29,4 +29,16 @@ void shouldGetDependencyImagesWhenOverriding() { assertThat(dockerComposeFiles.getDependencyImages()) .containsExactlyInAnyOrder("alpine:3.17", "redis:b", "mysql:b", "aservice:latest"); } + + @Test + void shouldGetDependencyImagesWhenOverridingWithOverrideTag() { + DockerComposeFiles dockerComposeFiles = new DockerComposeFiles( + Lists.newArrayList( + new File("src/test/resources/docker-compose-imagename-overriding-tag-a.yml"), + new File("src/test/resources/docker-compose-imagename-overriding-tag-b.yml") + ) + ); + assertThat(dockerComposeFiles.getDependencyImages()) + .containsExactlyInAnyOrder("postgres:16", "redis:7", "mysql:8.4"); + } } diff --git a/core/src/test/java/org/testcontainers/containers/ParsedDockerComposeFileValidationTest.java b/core/src/test/java/org/testcontainers/containers/ParsedDockerComposeFileValidationTest.java index 36126df6a38..5e76cdba58a 100644 --- a/core/src/test/java/org/testcontainers/containers/ParsedDockerComposeFileValidationTest.java +++ b/core/src/test/java/org/testcontainers/containers/ParsedDockerComposeFileValidationTest.java @@ -150,6 +150,67 @@ void shouldObtainImageFromDockerfileBuildWithContext() { ); // redis, mysql from compose file, alpine:3.17 from Dockerfile build } + @Test + void shouldObtainImageNamesFromOverrideTag() throws Exception { + File file = temporaryFolder.resolve("override-docker-compose.yml").toFile(); + try (PrintWriter writer = new PrintWriter(file)) { + writer.println("services:"); + writer.println(" db: !override"); + writer.println(" image: postgres:16"); + writer.println(" redis:"); + writer.println(" image: !override redis:7"); + } + ParsedDockerComposeFile parsedFile = new ParsedDockerComposeFile(file); + assertThat(parsedFile.getServiceNameToImageNames()) + .as("values tagged with !override are kept") + .containsOnly(entry("db", Sets.newHashSet("postgres:16")), entry("redis", Sets.newHashSet("redis:7"))); + } + + @Test + void shouldIgnoreImageNamesRemovedWithResetTag() throws Exception { + File file = temporaryFolder.resolve("reset-docker-compose.yml").toFile(); + try (PrintWriter writer = new PrintWriter(file)) { + writer.println("services:"); + writer.println(" db:"); + writer.println(" image: !reset null"); + writer.println(" redis:"); + writer.println(" image: redis:7"); + } + ParsedDockerComposeFile parsedFile = new ParsedDockerComposeFile(file); + assertThat(parsedFile.getServiceNameToImageNames()) + .as("values tagged with !reset are removed") + .containsOnly(entry("redis", Sets.newHashSet("redis:7"))); + } + + @Test + void shouldContinueAfterServiceWithUnknownStructure() { + ParsedDockerComposeFile parsedFile = new ParsedDockerComposeFile( + ImmutableMap.of( + "services", + ImmutableMap.of("unknown", "not a map", "redis", ImmutableMap.of("image", "redis:7")) + ) + ); + assertThat(parsedFile.getServiceNameToImageNames()) + .as("services after one with an unknown structure are still parsed") + .containsOnly(entry("redis", Sets.newHashSet("redis:7"))); + } + + @Test + void shouldIgnoreTopLevelElementsWithoutServicesElement() throws Exception { + File file = temporaryFolder.resolve("no-services-docker-compose.yml").toFile(); + try (PrintWriter writer = new PrintWriter(file)) { + writer.println("version: \"3.8\""); + writer.println("x-common:"); + writer.println(" image: busybox:1.36"); + writer.println("networks:"); + writer.println(" backend: {}"); + } + ParsedDockerComposeFile parsedFile = new ParsedDockerComposeFile(file); + assertThat(parsedFile.getServiceNameToImageNames()) + .as("extensions and top-level elements are not services") + .isEmpty(); + } + @Test void shouldSupportALotOfAliases() throws Exception { File file = temporaryFolder.resolve("tmp-docker-compose.yml").toFile(); diff --git a/core/src/test/resources/docker-compose-imagename-overriding-tag-a.yml b/core/src/test/resources/docker-compose-imagename-overriding-tag-a.yml new file mode 100644 index 00000000000..9d6457e8a6c --- /dev/null +++ b/core/src/test/resources/docker-compose-imagename-overriding-tag-a.yml @@ -0,0 +1,7 @@ +services: + db: + image: postgres:15 + redis: + image: redis:6 + mysql: + image: mysql:8.0 diff --git a/core/src/test/resources/docker-compose-imagename-overriding-tag-b.yml b/core/src/test/resources/docker-compose-imagename-overriding-tag-b.yml new file mode 100644 index 00000000000..315fc0dd971 --- /dev/null +++ b/core/src/test/resources/docker-compose-imagename-overriding-tag-b.yml @@ -0,0 +1,7 @@ +services: + db: !override + image: postgres:16 + redis: + image: !override redis:7 + mysql: + image: mysql:8.4 From 9759638a13e2cce8cdfbf05c8bd9510a8d843414 Mon Sep 17 00:00:00 2001 From: Srivathsav01 Date: Wed, 30 Sep 2026 10:16:15 +0530 Subject: [PATCH 2/6] Keep quoted values tagged with '!override' as strings --- .../containers/ParsedDockerComposeFile.java | 3 ++- .../ParsedDockerComposeFileValidationTest.java | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/testcontainers/containers/ParsedDockerComposeFile.java b/core/src/main/java/org/testcontainers/containers/ParsedDockerComposeFile.java index fd66ca3f0e1..18b89b37040 100644 --- a/core/src/main/java/org/testcontainers/containers/ParsedDockerComposeFile.java +++ b/core/src/main/java/org/testcontainers/containers/ParsedDockerComposeFile.java @@ -104,7 +104,8 @@ private static Tag resolveUntaggedTag(Node node, Resolver resolver) { case sequence: return Tag.SEQ; default: - return resolver.resolve(NodeId.scalar, ((ScalarNode) node).getValue(), true); + ScalarNode scalarNode = (ScalarNode) node; + return resolver.resolve(NodeId.scalar, scalarNode.getValue(), scalarNode.isPlain()); } } diff --git a/core/src/test/java/org/testcontainers/containers/ParsedDockerComposeFileValidationTest.java b/core/src/test/java/org/testcontainers/containers/ParsedDockerComposeFileValidationTest.java index 5e76cdba58a..9207c805f15 100644 --- a/core/src/test/java/org/testcontainers/containers/ParsedDockerComposeFileValidationTest.java +++ b/core/src/test/java/org/testcontainers/containers/ParsedDockerComposeFileValidationTest.java @@ -166,6 +166,20 @@ void shouldObtainImageNamesFromOverrideTag() throws Exception { .containsOnly(entry("db", Sets.newHashSet("postgres:16")), entry("redis", Sets.newHashSet("redis:7"))); } + @Test + void shouldKeepQuotedValuesTaggedWithOverrideAsStrings() throws Exception { + File file = temporaryFolder.resolve("override-quoted-docker-compose.yml").toFile(); + try (PrintWriter writer = new PrintWriter(file)) { + writer.println("services:"); + writer.println(" db:"); + writer.println(" image: !override \"1.0\""); + } + ParsedDockerComposeFile parsedFile = new ParsedDockerComposeFile(file); + assertThat(parsedFile.getServiceNameToImageNames()) + .as("quoted values tagged with !override are not implicitly typed") + .containsOnly(entry("db", Sets.newHashSet("1.0"))); + } + @Test void shouldIgnoreImageNamesRemovedWithResetTag() throws Exception { File file = temporaryFolder.resolve("reset-docker-compose.yml").toFile(); From 522bbeef63a4307b762da686b5ffdcb57a27ada2 Mon Sep 17 00:00:00 2001 From: Kevin Wittek Date: Sat, 17 Oct 2026 19:13:15 +0200 Subject: [PATCH 3/6] Only skip extension fields in compose files without 'services' Legacy v1 files may have services named like top-level elements of newer formats (e.g. 'name' or 'networks'). Non-map elements are already skipped. --- .../containers/ParsedDockerComposeFile.java | 17 ++-------------- ...ParsedDockerComposeFileValidationTest.java | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/core/src/main/java/org/testcontainers/containers/ParsedDockerComposeFile.java b/core/src/main/java/org/testcontainers/containers/ParsedDockerComposeFile.java index 18b89b37040..e9ebb5dacec 100644 --- a/core/src/main/java/org/testcontainers/containers/ParsedDockerComposeFile.java +++ b/core/src/main/java/org/testcontainers/containers/ParsedDockerComposeFile.java @@ -1,7 +1,6 @@ package org.testcontainers.containers; import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -35,19 +34,6 @@ @EqualsAndHashCode class ParsedDockerComposeFile { - /** - * Top-level elements of the Compose file format that are not services, e.g. in a file without a 'services' element - */ - private static final Set TOP_LEVEL_ELEMENTS = ImmutableSet.of( - "version", - "name", - "include", - "networks", - "volumes", - "configs", - "secrets" - ); - private final Map composeFileContent; private final String composeFileName; @@ -147,7 +133,8 @@ private void parseAndValidate() { for (Map.Entry entry : servicesMap.entrySet()) { String serviceName = entry.getKey(); - if (legacyFormat && (TOP_LEVEL_ELEMENTS.contains(serviceName) || serviceName.startsWith("x-"))) { + // extension fields are not services + if (legacyFormat && serviceName.startsWith("x-")) { continue; } Object serviceDefinition = entry.getValue(); diff --git a/core/src/test/java/org/testcontainers/containers/ParsedDockerComposeFileValidationTest.java b/core/src/test/java/org/testcontainers/containers/ParsedDockerComposeFileValidationTest.java index 9207c805f15..4351de522fb 100644 --- a/core/src/test/java/org/testcontainers/containers/ParsedDockerComposeFileValidationTest.java +++ b/core/src/test/java/org/testcontainers/containers/ParsedDockerComposeFileValidationTest.java @@ -225,6 +225,26 @@ void shouldIgnoreTopLevelElementsWithoutServicesElement() throws Exception { .isEmpty(); } + @Test + void shouldObtainImageNamesV1WithServicesNamedLikeTopLevelElements() throws Exception { + File file = temporaryFolder.resolve("v1-top-level-names-docker-compose.yml").toFile(); + try (PrintWriter writer = new PrintWriter(file)) { + writer.println("name:"); + writer.println(" image: busybox:1.36"); + writer.println("networks:"); + writer.println(" image: redis:7"); + writer.println("x-common:"); + writer.println(" image: alpine:3.20"); + } + ParsedDockerComposeFile parsedFile = new ParsedDockerComposeFile(file); + assertThat(parsedFile.getServiceNameToImageNames()) + .as("v1 services may be named like top-level elements of newer formats") + .containsOnly( + entry("name", Sets.newHashSet("busybox:1.36")), + entry("networks", Sets.newHashSet("redis:7")) + ); + } + @Test void shouldSupportALotOfAliases() throws Exception { File file = temporaryFolder.resolve("tmp-docker-compose.yml").toFile(); From 5485e8c498a323c95562ab8092ad57b01ec9964c Mon Sep 17 00:00:00 2001 From: Kevin Wittek Date: Sat, 17 Oct 2026 19:21:08 +0200 Subject: [PATCH 4/6] Don't skip x-* entries in compose files without 'services' Legacy v1 files may have services named like extension fields, and files without 'services' can't reliably be told apart from newer formats. --- .../containers/ParsedDockerComposeFile.java | 8 ------- ...ParsedDockerComposeFileValidationTest.java | 24 +++++++++++++++---- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/core/src/main/java/org/testcontainers/containers/ParsedDockerComposeFile.java b/core/src/main/java/org/testcontainers/containers/ParsedDockerComposeFile.java index e9ebb5dacec..74ecab217eb 100644 --- a/core/src/main/java/org/testcontainers/containers/ParsedDockerComposeFile.java +++ b/core/src/main/java/org/testcontainers/containers/ParsedDockerComposeFile.java @@ -97,7 +97,6 @@ private static Tag resolveUntaggedTag(Node node, Resolver resolver) { private void parseAndValidate() { final Map servicesMap; - final boolean legacyFormat; if (composeFileContent.containsKey("version") && "2.0".equals(composeFileContent.get("version"))) { log.warn( "Testcontainers may not be able to clean up networks spawned using Docker Compose v2.0 files. " + @@ -124,19 +123,12 @@ private void parseAndValidate() { @SuppressWarnings("unchecked") Map temp = (Map) servicesElement; servicesMap = temp; - legacyFormat = false; } else { - // Compose file format v1: top-level elements are services servicesMap = composeFileContent; - legacyFormat = true; } for (Map.Entry entry : servicesMap.entrySet()) { String serviceName = entry.getKey(); - // extension fields are not services - if (legacyFormat && serviceName.startsWith("x-")) { - continue; - } Object serviceDefinition = entry.getValue(); if (!(serviceDefinition instanceof Map)) { log.debug( diff --git a/core/src/test/java/org/testcontainers/containers/ParsedDockerComposeFileValidationTest.java b/core/src/test/java/org/testcontainers/containers/ParsedDockerComposeFileValidationTest.java index 4351de522fb..0b77c1d6877 100644 --- a/core/src/test/java/org/testcontainers/containers/ParsedDockerComposeFileValidationTest.java +++ b/core/src/test/java/org/testcontainers/containers/ParsedDockerComposeFileValidationTest.java @@ -214,14 +214,12 @@ void shouldIgnoreTopLevelElementsWithoutServicesElement() throws Exception { File file = temporaryFolder.resolve("no-services-docker-compose.yml").toFile(); try (PrintWriter writer = new PrintWriter(file)) { writer.println("version: \"3.8\""); - writer.println("x-common:"); - writer.println(" image: busybox:1.36"); writer.println("networks:"); writer.println(" backend: {}"); } ParsedDockerComposeFile parsedFile = new ParsedDockerComposeFile(file); assertThat(parsedFile.getServiceNameToImageNames()) - .as("extensions and top-level elements are not services") + .as("top-level elements without images are not services") .isEmpty(); } @@ -233,8 +231,6 @@ void shouldObtainImageNamesV1WithServicesNamedLikeTopLevelElements() throws Exce writer.println(" image: busybox:1.36"); writer.println("networks:"); writer.println(" image: redis:7"); - writer.println("x-common:"); - writer.println(" image: alpine:3.20"); } ParsedDockerComposeFile parsedFile = new ParsedDockerComposeFile(file); assertThat(parsedFile.getServiceNameToImageNames()) @@ -245,6 +241,24 @@ void shouldObtainImageNamesV1WithServicesNamedLikeTopLevelElements() throws Exce ); } + @Test + void shouldObtainImageNamesV1WithServiceNamedLikeExtensionField() throws Exception { + File file = temporaryFolder.resolve("v1-extension-like-name-docker-compose.yml").toFile(); + try (PrintWriter writer = new PrintWriter(file)) { + writer.println("x-api:"); + writer.println(" image: private.example/api:1.0"); + writer.println("redis:"); + writer.println(" image: redis:7"); + } + ParsedDockerComposeFile parsedFile = new ParsedDockerComposeFile(file); + assertThat(parsedFile.getServiceNameToImageNames()) + .as("v1 services may be named like extension fields") + .containsOnly( + entry("x-api", Sets.newHashSet("private.example/api:1.0")), + entry("redis", Sets.newHashSet("redis:7")) + ); + } + @Test void shouldSupportALotOfAliases() throws Exception { File file = temporaryFolder.resolve("tmp-docker-compose.yml").toFile(); From 8bed4995731ccef0b6c0a1ff7d3777a8342463df Mon Sep 17 00:00:00 2001 From: Kevin Wittek Date: Sat, 17 Oct 2026 19:49:10 +0200 Subject: [PATCH 5/6] Use compose files from test resources in parser tests --- ...ParsedDockerComposeFileValidationTest.java | 66 +++++-------------- ...ompose-imagename-overriding-tag-quoted.yml | 3 + ...-compose-imagename-parsing-no-services.yml | 3 + ...agename-parsing-v1-extension-like-name.yml | 4 ++ ...e-imagename-parsing-v1-top-level-names.yml | 4 ++ .../docker-compose-imagename-reset-tag.yml | 5 ++ 6 files changed, 36 insertions(+), 49 deletions(-) create mode 100644 core/src/test/resources/docker-compose-imagename-overriding-tag-quoted.yml create mode 100644 core/src/test/resources/docker-compose-imagename-parsing-no-services.yml create mode 100644 core/src/test/resources/docker-compose-imagename-parsing-v1-extension-like-name.yml create mode 100644 core/src/test/resources/docker-compose-imagename-parsing-v1-top-level-names.yml create mode 100644 core/src/test/resources/docker-compose-imagename-reset-tag.yml diff --git a/core/src/test/java/org/testcontainers/containers/ParsedDockerComposeFileValidationTest.java b/core/src/test/java/org/testcontainers/containers/ParsedDockerComposeFileValidationTest.java index 0b77c1d6877..466b0e4b7d5 100644 --- a/core/src/test/java/org/testcontainers/containers/ParsedDockerComposeFileValidationTest.java +++ b/core/src/test/java/org/testcontainers/containers/ParsedDockerComposeFileValidationTest.java @@ -151,29 +151,21 @@ void shouldObtainImageFromDockerfileBuildWithContext() { } @Test - void shouldObtainImageNamesFromOverrideTag() throws Exception { - File file = temporaryFolder.resolve("override-docker-compose.yml").toFile(); - try (PrintWriter writer = new PrintWriter(file)) { - writer.println("services:"); - writer.println(" db: !override"); - writer.println(" image: postgres:16"); - writer.println(" redis:"); - writer.println(" image: !override redis:7"); - } + void shouldObtainImageNamesFromOverrideTag() { + File file = new File("src/test/resources/docker-compose-imagename-overriding-tag-b.yml"); ParsedDockerComposeFile parsedFile = new ParsedDockerComposeFile(file); assertThat(parsedFile.getServiceNameToImageNames()) .as("values tagged with !override are kept") - .containsOnly(entry("db", Sets.newHashSet("postgres:16")), entry("redis", Sets.newHashSet("redis:7"))); + .containsOnly( + entry("db", Sets.newHashSet("postgres:16")), + entry("redis", Sets.newHashSet("redis:7")), + entry("mysql", Sets.newHashSet("mysql:8.4")) + ); } @Test - void shouldKeepQuotedValuesTaggedWithOverrideAsStrings() throws Exception { - File file = temporaryFolder.resolve("override-quoted-docker-compose.yml").toFile(); - try (PrintWriter writer = new PrintWriter(file)) { - writer.println("services:"); - writer.println(" db:"); - writer.println(" image: !override \"1.0\""); - } + void shouldKeepQuotedValuesTaggedWithOverrideAsStrings() { + File file = new File("src/test/resources/docker-compose-imagename-overriding-tag-quoted.yml"); ParsedDockerComposeFile parsedFile = new ParsedDockerComposeFile(file); assertThat(parsedFile.getServiceNameToImageNames()) .as("quoted values tagged with !override are not implicitly typed") @@ -181,15 +173,8 @@ void shouldKeepQuotedValuesTaggedWithOverrideAsStrings() throws Exception { } @Test - void shouldIgnoreImageNamesRemovedWithResetTag() throws Exception { - File file = temporaryFolder.resolve("reset-docker-compose.yml").toFile(); - try (PrintWriter writer = new PrintWriter(file)) { - writer.println("services:"); - writer.println(" db:"); - writer.println(" image: !reset null"); - writer.println(" redis:"); - writer.println(" image: redis:7"); - } + void shouldIgnoreImageNamesRemovedWithResetTag() { + File file = new File("src/test/resources/docker-compose-imagename-reset-tag.yml"); ParsedDockerComposeFile parsedFile = new ParsedDockerComposeFile(file); assertThat(parsedFile.getServiceNameToImageNames()) .as("values tagged with !reset are removed") @@ -210,13 +195,8 @@ void shouldContinueAfterServiceWithUnknownStructure() { } @Test - void shouldIgnoreTopLevelElementsWithoutServicesElement() throws Exception { - File file = temporaryFolder.resolve("no-services-docker-compose.yml").toFile(); - try (PrintWriter writer = new PrintWriter(file)) { - writer.println("version: \"3.8\""); - writer.println("networks:"); - writer.println(" backend: {}"); - } + void shouldIgnoreTopLevelElementsWithoutServicesElement() { + File file = new File("src/test/resources/docker-compose-imagename-parsing-no-services.yml"); ParsedDockerComposeFile parsedFile = new ParsedDockerComposeFile(file); assertThat(parsedFile.getServiceNameToImageNames()) .as("top-level elements without images are not services") @@ -224,14 +204,8 @@ void shouldIgnoreTopLevelElementsWithoutServicesElement() throws Exception { } @Test - void shouldObtainImageNamesV1WithServicesNamedLikeTopLevelElements() throws Exception { - File file = temporaryFolder.resolve("v1-top-level-names-docker-compose.yml").toFile(); - try (PrintWriter writer = new PrintWriter(file)) { - writer.println("name:"); - writer.println(" image: busybox:1.36"); - writer.println("networks:"); - writer.println(" image: redis:7"); - } + void shouldObtainImageNamesV1WithServicesNamedLikeTopLevelElements() { + File file = new File("src/test/resources/docker-compose-imagename-parsing-v1-top-level-names.yml"); ParsedDockerComposeFile parsedFile = new ParsedDockerComposeFile(file); assertThat(parsedFile.getServiceNameToImageNames()) .as("v1 services may be named like top-level elements of newer formats") @@ -242,14 +216,8 @@ void shouldObtainImageNamesV1WithServicesNamedLikeTopLevelElements() throws Exce } @Test - void shouldObtainImageNamesV1WithServiceNamedLikeExtensionField() throws Exception { - File file = temporaryFolder.resolve("v1-extension-like-name-docker-compose.yml").toFile(); - try (PrintWriter writer = new PrintWriter(file)) { - writer.println("x-api:"); - writer.println(" image: private.example/api:1.0"); - writer.println("redis:"); - writer.println(" image: redis:7"); - } + void shouldObtainImageNamesV1WithServiceNamedLikeExtensionField() { + File file = new File("src/test/resources/docker-compose-imagename-parsing-v1-extension-like-name.yml"); ParsedDockerComposeFile parsedFile = new ParsedDockerComposeFile(file); assertThat(parsedFile.getServiceNameToImageNames()) .as("v1 services may be named like extension fields") diff --git a/core/src/test/resources/docker-compose-imagename-overriding-tag-quoted.yml b/core/src/test/resources/docker-compose-imagename-overriding-tag-quoted.yml new file mode 100644 index 00000000000..312a954e07a --- /dev/null +++ b/core/src/test/resources/docker-compose-imagename-overriding-tag-quoted.yml @@ -0,0 +1,3 @@ +services: + db: + image: !override "1.0" diff --git a/core/src/test/resources/docker-compose-imagename-parsing-no-services.yml b/core/src/test/resources/docker-compose-imagename-parsing-no-services.yml new file mode 100644 index 00000000000..5826c6b4c3b --- /dev/null +++ b/core/src/test/resources/docker-compose-imagename-parsing-no-services.yml @@ -0,0 +1,3 @@ +version: "3.8" +networks: + backend: {} diff --git a/core/src/test/resources/docker-compose-imagename-parsing-v1-extension-like-name.yml b/core/src/test/resources/docker-compose-imagename-parsing-v1-extension-like-name.yml new file mode 100644 index 00000000000..adfe476a497 --- /dev/null +++ b/core/src/test/resources/docker-compose-imagename-parsing-v1-extension-like-name.yml @@ -0,0 +1,4 @@ +x-api: + image: private.example/api:1.0 +redis: + image: redis:7 diff --git a/core/src/test/resources/docker-compose-imagename-parsing-v1-top-level-names.yml b/core/src/test/resources/docker-compose-imagename-parsing-v1-top-level-names.yml new file mode 100644 index 00000000000..e7f7851d201 --- /dev/null +++ b/core/src/test/resources/docker-compose-imagename-parsing-v1-top-level-names.yml @@ -0,0 +1,4 @@ +name: + image: busybox:1.36 +networks: + image: redis:7 diff --git a/core/src/test/resources/docker-compose-imagename-reset-tag.yml b/core/src/test/resources/docker-compose-imagename-reset-tag.yml new file mode 100644 index 00000000000..ac4c495014a --- /dev/null +++ b/core/src/test/resources/docker-compose-imagename-reset-tag.yml @@ -0,0 +1,5 @@ +services: + db: + image: !reset null + redis: + image: redis:7 From b7b71edb635e77b801ec72abf1361c30bc432681 Mon Sep 17 00:00:00 2001 From: Kevin Wittek Date: Sat, 17 Oct 2026 19:55:10 +0200 Subject: [PATCH 6/6] Remove leftover compose parser tests and merge v1 service name tests --- ...ParsedDockerComposeFileValidationTest.java | 26 +++---------------- ...-compose-imagename-parsing-no-services.yml | 3 --- ...agename-parsing-v1-extension-like-name.yml | 4 --- ...e-imagename-parsing-v1-top-level-names.yml | 2 ++ 4 files changed, 5 insertions(+), 30 deletions(-) delete mode 100644 core/src/test/resources/docker-compose-imagename-parsing-no-services.yml delete mode 100644 core/src/test/resources/docker-compose-imagename-parsing-v1-extension-like-name.yml diff --git a/core/src/test/java/org/testcontainers/containers/ParsedDockerComposeFileValidationTest.java b/core/src/test/java/org/testcontainers/containers/ParsedDockerComposeFileValidationTest.java index 466b0e4b7d5..630bc463663 100644 --- a/core/src/test/java/org/testcontainers/containers/ParsedDockerComposeFileValidationTest.java +++ b/core/src/test/java/org/testcontainers/containers/ParsedDockerComposeFileValidationTest.java @@ -194,36 +194,16 @@ void shouldContinueAfterServiceWithUnknownStructure() { .containsOnly(entry("redis", Sets.newHashSet("redis:7"))); } - @Test - void shouldIgnoreTopLevelElementsWithoutServicesElement() { - File file = new File("src/test/resources/docker-compose-imagename-parsing-no-services.yml"); - ParsedDockerComposeFile parsedFile = new ParsedDockerComposeFile(file); - assertThat(parsedFile.getServiceNameToImageNames()) - .as("top-level elements without images are not services") - .isEmpty(); - } - @Test void shouldObtainImageNamesV1WithServicesNamedLikeTopLevelElements() { File file = new File("src/test/resources/docker-compose-imagename-parsing-v1-top-level-names.yml"); ParsedDockerComposeFile parsedFile = new ParsedDockerComposeFile(file); assertThat(parsedFile.getServiceNameToImageNames()) - .as("v1 services may be named like top-level elements of newer formats") + .as("v1 services may be named like top-level elements or extension fields of newer formats") .containsOnly( entry("name", Sets.newHashSet("busybox:1.36")), - entry("networks", Sets.newHashSet("redis:7")) - ); - } - - @Test - void shouldObtainImageNamesV1WithServiceNamedLikeExtensionField() { - File file = new File("src/test/resources/docker-compose-imagename-parsing-v1-extension-like-name.yml"); - ParsedDockerComposeFile parsedFile = new ParsedDockerComposeFile(file); - assertThat(parsedFile.getServiceNameToImageNames()) - .as("v1 services may be named like extension fields") - .containsOnly( - entry("x-api", Sets.newHashSet("private.example/api:1.0")), - entry("redis", Sets.newHashSet("redis:7")) + entry("networks", Sets.newHashSet("redis:7")), + entry("x-api", Sets.newHashSet("private.example/api:1.0")) ); } diff --git a/core/src/test/resources/docker-compose-imagename-parsing-no-services.yml b/core/src/test/resources/docker-compose-imagename-parsing-no-services.yml deleted file mode 100644 index 5826c6b4c3b..00000000000 --- a/core/src/test/resources/docker-compose-imagename-parsing-no-services.yml +++ /dev/null @@ -1,3 +0,0 @@ -version: "3.8" -networks: - backend: {} diff --git a/core/src/test/resources/docker-compose-imagename-parsing-v1-extension-like-name.yml b/core/src/test/resources/docker-compose-imagename-parsing-v1-extension-like-name.yml deleted file mode 100644 index adfe476a497..00000000000 --- a/core/src/test/resources/docker-compose-imagename-parsing-v1-extension-like-name.yml +++ /dev/null @@ -1,4 +0,0 @@ -x-api: - image: private.example/api:1.0 -redis: - image: redis:7 diff --git a/core/src/test/resources/docker-compose-imagename-parsing-v1-top-level-names.yml b/core/src/test/resources/docker-compose-imagename-parsing-v1-top-level-names.yml index e7f7851d201..0a09d6bc762 100644 --- a/core/src/test/resources/docker-compose-imagename-parsing-v1-top-level-names.yml +++ b/core/src/test/resources/docker-compose-imagename-parsing-v1-top-level-names.yml @@ -2,3 +2,5 @@ name: image: busybox:1.36 networks: image: redis:7 +x-api: + image: private.example/api:1.0