From 06389ac412406ea73c82e0864c23b7f005e66846 Mon Sep 17 00:00:00 2001 From: rtoohil Date: Mon, 1 Dec 2025 12:08:04 -0500 Subject: [PATCH 1/9] Handle compose down with container_name case --- .../Commands/ComposeDown.swift | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/Sources/Container-Compose/Commands/ComposeDown.swift b/Sources/Container-Compose/Commands/ComposeDown.swift index 9108900..03fdc63 100644 --- a/Sources/Container-Compose/Commands/ComposeDown.swift +++ b/Sources/Container-Compose/Commands/ComposeDown.swift @@ -103,25 +103,37 @@ public struct ComposeDown: AsyncParsableCommand { }) } - try await stopOldStuff(services.map({ $0.serviceName }), remove: false) + try await stopOldStuff(services, remove: false) } - private func stopOldStuff(_ services: [String], remove: Bool) async throws { + private func stopOldStuff(_ services: [(serviceName: String, service: Service)], remove: Bool) async throws { guard let projectName else { return } - let containers = services.map { "\(projectName)-\($0)" } - for container in containers { - print("Stopping container: \(container)") - guard let container = try? await ClientContainer.get(id: container) else { continue } + for (serviceName, service) in services { + // Respect explicit container_name, otherwise use default pattern + let containerName: String + if let explicitContainerName = service.container_name { + containerName = explicitContainerName + } else { + containerName = "\(projectName)-\(serviceName)" + } + + print("Stopping container: \(containerName)") + guard let container = try? await ClientContainer.get(id: containerName) else { + print("Warning: Container '\(containerName)' not found, skipping.") + continue + } do { try await container.stop() + print("Successfully stopped container: \(containerName)") } catch { print("Error Stopping Container: \(error)") } if remove { do { try await container.delete() + print("Successfully removed container: \(containerName)") } catch { print("Error Removing Container: \(error)") } From b2bc1bc20d3ce62b4a38f60364c0ce7405e37c44 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Fri, 30 Jan 2026 17:11:09 +0100 Subject: [PATCH 2/9] Providing tests for `container-compose down` This provies the missing tests for PR #39 --- .../ComposeDownTests.swift | 59 +++++++++++++++++++ .../TestHelpers/DockerComposeYamlFiles.swift | 10 ++++ 2 files changed, 69 insertions(+) create mode 100644 Tests/Container-Compose-DynamicTests/ComposeDownTests.swift diff --git a/Tests/Container-Compose-DynamicTests/ComposeDownTests.swift b/Tests/Container-Compose-DynamicTests/ComposeDownTests.swift new file mode 100644 index 0000000..2dddcad --- /dev/null +++ b/Tests/Container-Compose-DynamicTests/ComposeDownTests.swift @@ -0,0 +1,59 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2025 Morris Richman and the Container-Compose project authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import Testing +import Foundation +import ContainerCommands +import ContainerAPIClient +import TestHelpers +@testable import ContainerComposeCore + +@Suite("Compose Down Tests") +struct ComposeDownTests { + + @Test("What goes up must come down - container_name") + func testUpAndDownContainerName() async throws { + let yaml = DockerComposeYamlFiles.dockerComposeYaml1 + + let tempLocation = URL.temporaryDirectory.appending(path: "Container-Compose_Tests_\(UUID().uuidString)/docker-compose.yaml") + let tempBase = tempLocation.deletingLastPathComponent() + try? FileManager.default.createDirectory(at: tempLocation.deletingLastPathComponent(), withIntermediateDirectories: true) + try yaml.write(to: tempLocation, atomically: false, encoding: .utf8) + let folderName = tempBase.lastPathComponent + + var composeUp = try ComposeUp.parse(["-d", "--cwd", tempBase.path(percentEncoded: false)]) + try await composeUp.run() + + var containers = try await ClientContainer.list() + .filter({ + $0.configuration.id.contains(folderName) + }) + + #expect(containers.count == 1, "Expected 1 containers to be running, found \(containers.count)") + //#expect(containers[0].configuration.names.contains("/custom_nginx"), "Expected container to have name /custom_nginx, found \(containers[0].configuration.names)") + + var composeDown = try ComposeDown.parse(["--cwd", tempBase.path(percentEncoded: false)]) + try await composeDown.run() + + containers = try await ClientContainer.list() + .filter({ + $0.configuration.id.contains(folderName) + }) + + #expect(containers.count == 0, "Expected no containers to be running, found \(containers.count)") + } + +} \ No newline at end of file diff --git a/Tests/TestHelpers/DockerComposeYamlFiles.swift b/Tests/TestHelpers/DockerComposeYamlFiles.swift index 43e9b12..bba086a 100644 --- a/Tests/TestHelpers/DockerComposeYamlFiles.swift +++ b/Tests/TestHelpers/DockerComposeYamlFiles.swift @@ -246,4 +246,14 @@ public struct DockerComposeYamlFiles { POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres """ + + public static let dockerComposeYaml9 = """ + version: '3.8' + services: + web: + image: nginx:alpine + container_name: custom_nginx + ports: + - "8082:80" + """ } From 59919993354c7bca7aed8077d8670545fc8d659c Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Fri, 30 Jan 2026 17:48:25 +0100 Subject: [PATCH 3/9] Correctly asserting up/down --- .../ComposeDownTests.swift | 20 +- .../TestHelpers/DockerComposeYamlFiles.swift | 422 +++++++++--------- 2 files changed, 224 insertions(+), 218 deletions(-) diff --git a/Tests/Container-Compose-DynamicTests/ComposeDownTests.swift b/Tests/Container-Compose-DynamicTests/ComposeDownTests.swift index 2dddcad..0ad9dbe 100644 --- a/Tests/Container-Compose-DynamicTests/ComposeDownTests.swift +++ b/Tests/Container-Compose-DynamicTests/ComposeDownTests.swift @@ -26,34 +26,38 @@ struct ComposeDownTests { @Test("What goes up must come down - container_name") func testUpAndDownContainerName() async throws { - let yaml = DockerComposeYamlFiles.dockerComposeYaml1 + // Create a new temporary UUID to use as a container name, otherwise we might conflict with + // existing containers on the system + let containerName = UUID().uuidString + + let yaml = DockerComposeYamlFiles.dockerComposeYaml9(containerName: containerName) let tempLocation = URL.temporaryDirectory.appending(path: "Container-Compose_Tests_\(UUID().uuidString)/docker-compose.yaml") let tempBase = tempLocation.deletingLastPathComponent() try? FileManager.default.createDirectory(at: tempLocation.deletingLastPathComponent(), withIntermediateDirectories: true) try yaml.write(to: tempLocation, atomically: false, encoding: .utf8) - let folderName = tempBase.lastPathComponent var composeUp = try ComposeUp.parse(["-d", "--cwd", tempBase.path(percentEncoded: false)]) try await composeUp.run() - + var containers = try await ClientContainer.list() .filter({ - $0.configuration.id.contains(folderName) + $0.configuration.id.contains(containerName) }) - #expect(containers.count == 1, "Expected 1 containers to be running, found \(containers.count)") - //#expect(containers[0].configuration.names.contains("/custom_nginx"), "Expected container to have name /custom_nginx, found \(containers[0].configuration.names)") + #expect(containers.count == 1, "Expected 1 container with the name \(containerName), found \(containers.count)") + #expect(containers[0].status == .running, "Expected container \(containerName) to be running, found status: \(containers[0].status.rawValue)") var composeDown = try ComposeDown.parse(["--cwd", tempBase.path(percentEncoded: false)]) try await composeDown.run() containers = try await ClientContainer.list() .filter({ - $0.configuration.id.contains(folderName) + $0.configuration.id.contains(containerName) }) - #expect(containers.count == 0, "Expected no containers to be running, found \(containers.count)") + #expect(containers.count == 1, "Expected 1 container with the name \(containerName), found \(containers.count)") + #expect(containers[0].status == .stopped, "Expected container \(containerName) to be stopped, found status: \(containers[0].status.rawValue)") } } \ No newline at end of file diff --git a/Tests/TestHelpers/DockerComposeYamlFiles.swift b/Tests/TestHelpers/DockerComposeYamlFiles.swift index bba086a..bade420 100644 --- a/Tests/TestHelpers/DockerComposeYamlFiles.swift +++ b/Tests/TestHelpers/DockerComposeYamlFiles.swift @@ -14,213 +14,215 @@ // limitations under the License. //===----------------------------------------------------------------------===// +import Foundation + public struct DockerComposeYamlFiles { - public static let dockerComposeYaml1 = """ - version: '3.8' - - services: - wordpress: - image: wordpress:latest - ports: - - "8080:80" - environment: - WORDPRESS_DB_HOST: db - WORDPRESS_DB_USER: wordpress - WORDPRESS_DB_PASSWORD: wordpress - WORDPRESS_DB_NAME: wordpress - depends_on: - - db - volumes: - - wordpress_data:/var/www/html - - db: - image: mysql:8.0 - environment: - MYSQL_DATABASE: wordpress - MYSQL_USER: wordpress - MYSQL_PASSWORD: wordpress - MYSQL_ROOT_PASSWORD: rootpassword - volumes: - - db_data:/var/lib/mysql - + public static let dockerComposeYaml1 = """ + version: '3.8' + + services: + wordpress: + image: wordpress:latest + ports: + - "8080:80" + environment: + WORDPRESS_DB_HOST: db + WORDPRESS_DB_USER: wordpress + WORDPRESS_DB_PASSWORD: wordpress + WORDPRESS_DB_NAME: wordpress + depends_on: + - db volumes: - wordpress_data: - db_data: - """ - - public static let dockerComposeYaml2 = """ - version: '3.8' - name: webapp - - services: - nginx: - image: nginx:alpine - ports: - - "80:80" - depends_on: - - app - networks: - - frontend - - app: - image: node:18-alpine - working_dir: /app - environment: - NODE_ENV: production - DATABASE_URL: postgres://db:5432/myapp - depends_on: - - db - - redis - networks: - - frontend - - backend - - db: - image: postgres:14-alpine - environment: - POSTGRES_DB: myapp - POSTGRES_USER: user - POSTGRES_PASSWORD: password - volumes: - - db-data:/var/lib/postgresql/data - networks: - - backend - - redis: - image: redis:alpine - networks: - - backend - + - wordpress_data:/var/www/html + + db: + image: mysql:8.0 + environment: + MYSQL_DATABASE: wordpress + MYSQL_USER: wordpress + MYSQL_PASSWORD: wordpress + MYSQL_ROOT_PASSWORD: rootpassword volumes: - db-data: - + - db_data:/var/lib/mysql + + volumes: + wordpress_data: + db_data: + """ + + public static let dockerComposeYaml2 = """ + version: '3.8' + name: webapp + + services: + nginx: + image: nginx:alpine + ports: + - "80:80" + depends_on: + - app networks: - frontend: - backend: - """ - - public static let dockerComposeYaml3 = """ - version: '3.8' - - services: - api-gateway: - image: traefik:v2.10 - ports: - - "81:80" - - "8081:8080" - depends_on: - - auth-service - - user-service - - order-service - - auth-service: - image: auth:latest - environment: - JWT_SECRET: secret123 - DATABASE_URL: postgres://db:5432/auth - - user-service: - image: user:latest - environment: - DATABASE_URL: postgres://db:5432/users - - order-service: - image: order:latest - environment: - DATABASE_URL: postgres://db:5432/orders - - db: - image: postgres:14 - environment: - POSTGRES_PASSWORD: postgres - """ - - public static let dockerComposeYaml4 = """ - version: '3.8' - - services: - app: - build: - context: . - dockerfile: Dockerfile.dev - volumes: - - ./app:/app - - /app/node_modules - environment: - NODE_ENV: development - ports: - - "3000:3000" - command: npm run dev - """ - - public static let dockerComposeYaml5 = """ - version: '3.8' - - services: - app: - image: myapp:latest - configs: - - source: app_config - target: /etc/app/config.yml - secrets: - - db_password - + - frontend + + app: + image: node:18-alpine + working_dir: /app + environment: + NODE_ENV: production + DATABASE_URL: postgres://db:5432/myapp + depends_on: + - db + - redis + networks: + - frontend + - backend + + db: + image: postgres:14-alpine + environment: + POSTGRES_DB: myapp + POSTGRES_USER: user + POSTGRES_PASSWORD: password + volumes: + - db-data:/var/lib/postgresql/data + networks: + - backend + + redis: + image: redis:alpine + networks: + - backend + + volumes: + db-data: + + networks: + frontend: + backend: + """ + + public static let dockerComposeYaml3 = """ + version: '3.8' + + services: + api-gateway: + image: traefik:v2.10 + ports: + - "81:80" + - "8081:8080" + depends_on: + - auth-service + - user-service + - order-service + + auth-service: + image: auth:latest + environment: + JWT_SECRET: secret123 + DATABASE_URL: postgres://db:5432/auth + + user-service: + image: user:latest + environment: + DATABASE_URL: postgres://db:5432/users + + order-service: + image: order:latest + environment: + DATABASE_URL: postgres://db:5432/orders + + db: + image: postgres:14 + environment: + POSTGRES_PASSWORD: postgres + """ + + public static let dockerComposeYaml4 = """ + version: '3.8' + + services: + app: + build: + context: . + dockerfile: Dockerfile.dev + volumes: + - ./app:/app + - /app/node_modules + environment: + NODE_ENV: development + ports: + - "3000:3000" + command: npm run dev + """ + + public static let dockerComposeYaml5 = """ + version: '3.8' + + services: + app: + image: myapp:latest configs: - app_config: - external: true - + - source: app_config + target: /etc/app/config.yml secrets: - db_password: - external: true - """ - - public static let dockerComposeYaml6 = """ - version: '3.8' - - services: - web: - image: nginx:latest - restart: unless-stopped - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - - db: - image: postgres:14 - restart: always - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 10s - timeout: 5s - retries: 5 - """ - - public static let dockerComposeYaml7 = """ - version: '3.8' - - services: - frontend: - image: frontend:latest - depends_on: - - api - - api: - image: api:latest - depends_on: - - cache - - db - - cache: - image: redis:alpine - - db: - image: postgres:14 - """ - - public static let dockerComposeYaml8 = """ + - db_password + + configs: + app_config: + external: true + + secrets: + db_password: + external: true + """ + + public static let dockerComposeYaml6 = """ + version: '3.8' + + services: + web: + image: nginx:latest + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + db: + image: postgres:14 + restart: always + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 10s + timeout: 5s + retries: 5 + """ + + public static let dockerComposeYaml7 = """ + version: '3.8' + + services: + frontend: + image: frontend:latest + depends_on: + - api + + api: + image: api:latest + depends_on: + - cache + - db + + cache: + image: redis:alpine + + db: + image: postgres:14 + """ + + public static let dockerComposeYaml8 = """ version: '3.8' services: @@ -247,13 +249,13 @@ public struct DockerComposeYamlFiles { POSTGRES_PASSWORD: postgres """ - public static let dockerComposeYaml9 = """ - version: '3.8' - services: - web: - image: nginx:alpine - container_name: custom_nginx - ports: - - "8082:80" - """ + public static func dockerComposeYaml9(containerName: String) -> String { + return """ + version: '3.8' + services: + web: + image: nginx:alpine + container_name: \(containerName) + """ + } } From 6dbc82005929a5e713ea5e2b45400354fc909dff Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Fri, 30 Jan 2026 18:09:54 +0100 Subject: [PATCH 4/9] Added copyYamlToTemporaryLocation as test helper --- .../ComposeDownTests.swift | 93 +++- .../TestHelpers/DockerComposeYamlFiles.swift | 456 ++++++++++-------- 2 files changed, 313 insertions(+), 236 deletions(-) diff --git a/Tests/Container-Compose-DynamicTests/ComposeDownTests.swift b/Tests/Container-Compose-DynamicTests/ComposeDownTests.swift index 0ad9dbe..c8a8880 100644 --- a/Tests/Container-Compose-DynamicTests/ComposeDownTests.swift +++ b/Tests/Container-Compose-DynamicTests/ComposeDownTests.swift @@ -14,50 +14,99 @@ // limitations under the License. //===----------------------------------------------------------------------===// -import Testing -import Foundation -import ContainerCommands import ContainerAPIClient +import ContainerCommands +import Foundation import TestHelpers +import Testing + @testable import ContainerComposeCore @Suite("Compose Down Tests") struct ComposeDownTests { + @Test("What goes up must come down - two containers") + func testUpAndDownComplex() async throws { + let yaml = DockerComposeYamlFiles.dockerComposeYaml1 + let project = try DockerComposeYamlFiles.copyYamlToTemporaryLocation(yaml: yaml) + + var composeUp = try ComposeUp.parse([ + "-d", "--cwd", project.base.path(percentEncoded: false), + ]) + try await composeUp.run() + + var containers = try await ClientContainer.list() + .filter({ + $0.configuration.id.contains(project.name) + }) + + #expect( + containers.count == 2, + "Expected 2 containers for \(project.name), found \(containers.count)") + #expect( + containers[0].status == .running, + "Expected wordpress container to be running, found status: \(containers[0].status.rawValue)" + ) + + var composeDown = try ComposeDown.parse(["--cwd", project.base.path(percentEncoded: false)]) + try await composeDown.run() + + containers = try await ClientContainer.list() + .filter({ + $0.configuration.id.contains(project.name) + }) + + #expect( + containers.count == 2, + "Expected 2 containers for \(project.name), found \(containers.count)") + #expect( + containers[0].status == .stopped, + "Expected wordpress container to be stopped, found status: \(containers[0].status.rawValue)" + ) + } + @Test("What goes up must come down - container_name") - func testUpAndDownContainerName() async throws { + func testUpAndDownContainerName() async throws { // Create a new temporary UUID to use as a container name, otherwise we might conflict with // existing containers on the system - let containerName = UUID().uuidString + let containerName = UUID().uuidString let yaml = DockerComposeYamlFiles.dockerComposeYaml9(containerName: containerName) - - let tempLocation = URL.temporaryDirectory.appending(path: "Container-Compose_Tests_\(UUID().uuidString)/docker-compose.yaml") - let tempBase = tempLocation.deletingLastPathComponent() - try? FileManager.default.createDirectory(at: tempLocation.deletingLastPathComponent(), withIntermediateDirectories: true) - try yaml.write(to: tempLocation, atomically: false, encoding: .utf8) - - var composeUp = try ComposeUp.parse(["-d", "--cwd", tempBase.path(percentEncoded: false)]) + let project = try DockerComposeYamlFiles.copyYamlToTemporaryLocation(yaml: yaml) + + var composeUp = try ComposeUp.parse([ + "-d", "--cwd", project.base.path(percentEncoded: false), + ]) try await composeUp.run() - + var containers = try await ClientContainer.list() .filter({ $0.configuration.id.contains(containerName) }) - - #expect(containers.count == 1, "Expected 1 container with the name \(containerName), found \(containers.count)") - #expect(containers[0].status == .running, "Expected container \(containerName) to be running, found status: \(containers[0].status.rawValue)") - - var composeDown = try ComposeDown.parse(["--cwd", tempBase.path(percentEncoded: false)]) + + #expect( + containers.count == 1, + "Expected 1 container with the name \(containerName), found \(containers.count)") + #expect( + containers[0].status == .running, + "Expected container \(containerName) to be running, found status: \(containers[0].status.rawValue)" + ) + + var composeDown = try ComposeDown.parse(["--cwd", project.base.path(percentEncoded: false)]) try await composeDown.run() containers = try await ClientContainer.list() .filter({ $0.configuration.id.contains(containerName) }) - - #expect(containers.count == 1, "Expected 1 container with the name \(containerName), found \(containers.count)") - #expect(containers[0].status == .stopped, "Expected container \(containerName) to be stopped, found status: \(containers[0].status.rawValue)") + + #expect( + containers.count == 1, + "Expected 1 container with the name \(containerName), found \(containers.count)") + #expect( + containers[0].status == .stopped, + "Expected container \(containerName) to be stopped, found status: \(containers[0].status.rawValue)" + ) } -} \ No newline at end of file +} diff --git a/Tests/TestHelpers/DockerComposeYamlFiles.swift b/Tests/TestHelpers/DockerComposeYamlFiles.swift index bade420..f1539b7 100644 --- a/Tests/TestHelpers/DockerComposeYamlFiles.swift +++ b/Tests/TestHelpers/DockerComposeYamlFiles.swift @@ -17,245 +17,273 @@ import Foundation public struct DockerComposeYamlFiles { - public static let dockerComposeYaml1 = """ - version: '3.8' + public static let dockerComposeYaml1 = """ + version: '3.8' + + services: + wordpress: + image: wordpress:latest + ports: + - "8080:80" + environment: + WORDPRESS_DB_HOST: db + WORDPRESS_DB_USER: wordpress + WORDPRESS_DB_PASSWORD: wordpress + WORDPRESS_DB_NAME: wordpress + depends_on: + - db + volumes: + - wordpress_data:/var/www/html + + db: + image: mysql:8.0 + environment: + MYSQL_DATABASE: wordpress + MYSQL_USER: wordpress + MYSQL_PASSWORD: wordpress + MYSQL_ROOT_PASSWORD: rootpassword + volumes: + - db_data:/var/lib/mysql - services: - wordpress: - image: wordpress:latest - ports: - - "8080:80" - environment: - WORDPRESS_DB_HOST: db - WORDPRESS_DB_USER: wordpress - WORDPRESS_DB_PASSWORD: wordpress - WORDPRESS_DB_NAME: wordpress - depends_on: - - db - volumes: - - wordpress_data:/var/www/html - - db: - image: mysql:8.0 - environment: - MYSQL_DATABASE: wordpress - MYSQL_USER: wordpress - MYSQL_PASSWORD: wordpress - MYSQL_ROOT_PASSWORD: rootpassword volumes: - - db_data:/var/lib/mysql + wordpress_data: + db_data: + """ - volumes: - wordpress_data: - db_data: - """ + public static let dockerComposeYaml2 = """ + version: '3.8' + name: webapp - public static let dockerComposeYaml2 = """ - version: '3.8' - name: webapp + services: + nginx: + image: nginx:alpine + ports: + - "80:80" + depends_on: + - app + networks: + - frontend + + app: + image: node:18-alpine + working_dir: /app + environment: + NODE_ENV: production + DATABASE_URL: postgres://db:5432/myapp + depends_on: + - db + - redis + networks: + - frontend + - backend + + db: + image: postgres:14-alpine + environment: + POSTGRES_DB: myapp + POSTGRES_USER: user + POSTGRES_PASSWORD: password + volumes: + - db-data:/var/lib/postgresql/data + networks: + - backend + + redis: + image: redis:alpine + networks: + - backend - services: - nginx: - image: nginx:alpine - ports: - - "80:80" - depends_on: - - app - networks: - - frontend - - app: - image: node:18-alpine - working_dir: /app - environment: - NODE_ENV: production - DATABASE_URL: postgres://db:5432/myapp - depends_on: - - db - - redis - networks: - - frontend - - backend - - db: - image: postgres:14-alpine - environment: - POSTGRES_DB: myapp - POSTGRES_USER: user - POSTGRES_PASSWORD: password volumes: - - db-data:/var/lib/postgresql/data - networks: - - backend - - redis: - image: redis:alpine + db-data: + networks: - - backend + frontend: + backend: + """ - volumes: - db-data: + public static let dockerComposeYaml3 = """ + version: '3.8' - networks: - frontend: - backend: - """ + services: + api-gateway: + image: traefik:v2.10 + ports: + - "81:80" + - "8081:8080" + depends_on: + - auth-service + - user-service + - order-service + + auth-service: + image: auth:latest + environment: + JWT_SECRET: secret123 + DATABASE_URL: postgres://db:5432/auth + + user-service: + image: user:latest + environment: + DATABASE_URL: postgres://db:5432/users + + order-service: + image: order:latest + environment: + DATABASE_URL: postgres://db:5432/orders + + db: + image: postgres:14 + environment: + POSTGRES_PASSWORD: postgres + """ - public static let dockerComposeYaml3 = """ - version: '3.8' + public static let dockerComposeYaml4 = """ + version: '3.8' - services: - api-gateway: - image: traefik:v2.10 - ports: - - "81:80" - - "8081:8080" - depends_on: - - auth-service - - user-service - - order-service - - auth-service: - image: auth:latest - environment: - JWT_SECRET: secret123 - DATABASE_URL: postgres://db:5432/auth - - user-service: - image: user:latest - environment: - DATABASE_URL: postgres://db:5432/users - - order-service: - image: order:latest - environment: - DATABASE_URL: postgres://db:5432/orders - - db: - image: postgres:14 - environment: - POSTGRES_PASSWORD: postgres - """ + services: + app: + build: + context: . + dockerfile: Dockerfile.dev + volumes: + - ./app:/app + - /app/node_modules + environment: + NODE_ENV: development + ports: + - "3000:3000" + command: npm run dev + """ - public static let dockerComposeYaml4 = """ - version: '3.8' + public static let dockerComposeYaml5 = """ + version: '3.8' - services: - app: - build: - context: . - dockerfile: Dockerfile.dev - volumes: - - ./app:/app - - /app/node_modules - environment: - NODE_ENV: development - ports: - - "3000:3000" - command: npm run dev - """ + services: + app: + image: myapp:latest + configs: + - source: app_config + target: /etc/app/config.yml + secrets: + - db_password - public static let dockerComposeYaml5 = """ - version: '3.8' - - services: - app: - image: myapp:latest configs: - - source: app_config - target: /etc/app/config.yml + app_config: + external: true + secrets: - - db_password + db_password: + external: true + """ + + public static let dockerComposeYaml6 = """ + version: '3.8' + + services: + web: + image: nginx:latest + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + db: + image: postgres:14 + restart: always + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 10s + timeout: 5s + retries: 5 + """ + + public static let dockerComposeYaml7 = """ + version: '3.8' + + services: + frontend: + image: frontend:latest + depends_on: + - api + + api: + image: api:latest + depends_on: + - cache + - db + + cache: + image: redis:alpine + + db: + image: postgres:14 + """ - configs: - app_config: - external: true + public static let dockerComposeYaml8 = """ + version: '3.8' - secrets: - db_password: - external: true - """ + services: + web: + image: nginx:alpine + ports: + - "8082:80" + depends_on: + - app - public static let dockerComposeYaml6 = """ - version: '3.8' + app: + image: python:3.12-alpine + depends_on: + - db + command: python -m http.server 8000 + environment: + DATABASE_URL: postgres://postgres:postgres@db:5432/appdb - services: - web: - image: nginx:latest - restart: unless-stopped - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - - db: - image: postgres:14 - restart: always - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 10s - timeout: 5s - retries: 5 - """ + db: + image: postgres:14 + environment: + POSTGRES_DB: appdb + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + """ - public static let dockerComposeYaml7 = """ - version: '3.8' + public static func dockerComposeYaml9(containerName: String) -> String { + return """ + version: '3.8' + services: + web: + image: nginx:alpine + container_name: \(containerName) + """ + } - services: - frontend: - image: frontend:latest - depends_on: - - api - - api: - image: api:latest - depends_on: - - cache - - db - - cache: - image: redis:alpine - - db: - image: postgres:14 - """ + /// Represents a temporary Docker Compose project copied to a temporary location for testing. + public struct TemporaryProject { + /// The URL of the temporary docker-compose.yaml file. + public let url: URL - public static let dockerComposeYaml8 = """ - version: '3.8' + /// The base directory containing the temporary docker-compose.yaml file. + public let base: URL - services: - web: - image: nginx:alpine - ports: - - "8082:80" - depends_on: - - app + /// The project name derived from the temporary directory name. + public let name: String + } - app: - image: python:3.12-alpine - depends_on: - - db - command: python -m http.server 8000 - environment: - DATABASE_URL: postgres://postgres:postgres@db:5432/appdb + /// Copies the provided Docker Compose YAML content to a temporary location and returns a + /// TemporaryProject. + /// - Parameter yaml: The Docker Compose YAML content to copy. + /// - Returns: A TemporaryProject containing the URL and project name. + public static func copyYamlToTemporaryLocation(yaml: String) throws -> TemporaryProject { + let tempLocation = URL.temporaryDirectory.appending( + path: "Container-Compose_Tests_\(UUID().uuidString)/docker-compose.yaml") + let tempBase = tempLocation.deletingLastPathComponent() + try? FileManager.default.createDirectory(at: tempBase, withIntermediateDirectories: true) + try yaml.write(to: tempLocation, atomically: false, encoding: .utf8) + let projectName = tempBase.lastPathComponent - db: - image: postgres:14 - environment: - POSTGRES_DB: appdb - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - """ + return TemporaryProject(url: tempLocation, base: tempBase, name: projectName) + } - public static func dockerComposeYaml9(containerName: String) -> String { - return """ - version: '3.8' - services: - web: - image: nginx:alpine - container_name: \(containerName) - """ - } } From 861de1a976afcfd0de405bff8812cdb32703db27 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Sat, 31 Jan 2026 23:54:16 +0100 Subject: [PATCH 5/9] Addressing code review --- .../ComposeDownTests.swift | 46 +++++++++++++++++-- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/Tests/Container-Compose-DynamicTests/ComposeDownTests.swift b/Tests/Container-Compose-DynamicTests/ComposeDownTests.swift index c8a8880..5ebade1 100644 --- a/Tests/Container-Compose-DynamicTests/ComposeDownTests.swift +++ b/Tests/Container-Compose-DynamicTests/ComposeDownTests.swift @@ -22,7 +22,7 @@ import Testing @testable import ContainerComposeCore -@Suite("Compose Down Tests") +@Suite("Compose Down Tests", .containerDependent, .serialized) struct ComposeDownTests { @Test("What goes up must come down - two containers") @@ -43,9 +43,25 @@ struct ComposeDownTests { #expect( containers.count == 2, "Expected 2 containers for \(project.name), found \(containers.count)") + + guard + let wordpressContainer = containers.first(where: { + $0.configuration.id == "\(project.name)-wordpress" + }), + let dbContainer = containers.first(where: { + $0.configuration.id == "\(project.name)-db" + }) + else { + throw Errors.containerNotFound + } + #expect( - containers[0].status == .running, - "Expected wordpress container to be running, found status: \(containers[0].status.rawValue)" + wordpressContainer.status == .running, + "Expected wordpress container to be running, found status: \(wordpressContainer.status.rawValue)" + ) + #expect( + dbContainer.status == .running, + "Expected db container to be running, found status: \(dbContainer.status.rawValue)" ) var composeDown = try ComposeDown.parse(["--cwd", project.base.path(percentEncoded: false)]) @@ -59,9 +75,25 @@ struct ComposeDownTests { #expect( containers.count == 2, "Expected 2 containers for \(project.name), found \(containers.count)") + + guard + let wordpressContainer = containers.first(where: { + $0.configuration.id == "\(project.name)-wordpress" + }), + let dbContainer = containers.first(where: { + $0.configuration.id == "\(project.name)-db" + }) + else { + throw Errors.containerNotFound + } + #expect( - containers[0].status == .stopped, - "Expected wordpress container to be stopped, found status: \(containers[0].status.rawValue)" + wordpressContainer.status == .stopped, + "Expected wordpress container to be stopped, found status: \(wordpressContainer.status.rawValue)" + ) + #expect( + dbContainer.status == .stopped, + "Expected db container to be stopped, found status: \(dbContainer.status.rawValue)" ) } @@ -109,4 +141,8 @@ struct ComposeDownTests { ) } + enum Errors: Error { + case containerNotFound + } + } From fca6889d7ffef460f69a9a860796dbbf77b4127e Mon Sep 17 00:00:00 2001 From: Morris Richman <81453549+Mcrich23@users.noreply.github.com> Date: Tue, 3 Feb 2026 20:34:09 -0800 Subject: [PATCH 6/9] test expect cleanup for containers running and stopping --- .../ComposeDownTests.swift | 44 ++----------------- 1 file changed, 4 insertions(+), 40 deletions(-) diff --git a/Tests/Container-Compose-DynamicTests/ComposeDownTests.swift b/Tests/Container-Compose-DynamicTests/ComposeDownTests.swift index 5ebade1..939a599 100644 --- a/Tests/Container-Compose-DynamicTests/ComposeDownTests.swift +++ b/Tests/Container-Compose-DynamicTests/ComposeDownTests.swift @@ -44,25 +44,7 @@ struct ComposeDownTests { containers.count == 2, "Expected 2 containers for \(project.name), found \(containers.count)") - guard - let wordpressContainer = containers.first(where: { - $0.configuration.id == "\(project.name)-wordpress" - }), - let dbContainer = containers.first(where: { - $0.configuration.id == "\(project.name)-db" - }) - else { - throw Errors.containerNotFound - } - - #expect( - wordpressContainer.status == .running, - "Expected wordpress container to be running, found status: \(wordpressContainer.status.rawValue)" - ) - #expect( - dbContainer.status == .running, - "Expected db container to be running, found status: \(dbContainer.status.rawValue)" - ) + #expect(containers.filter({ $0.status == .stopped}).count == 2, "Expected 2 running containers for \(project.name), found \(containers.count)") var composeDown = try ComposeDown.parse(["--cwd", project.base.path(percentEncoded: false)]) try await composeDown.run() @@ -76,25 +58,7 @@ struct ComposeDownTests { containers.count == 2, "Expected 2 containers for \(project.name), found \(containers.count)") - guard - let wordpressContainer = containers.first(where: { - $0.configuration.id == "\(project.name)-wordpress" - }), - let dbContainer = containers.first(where: { - $0.configuration.id == "\(project.name)-db" - }) - else { - throw Errors.containerNotFound - } - - #expect( - wordpressContainer.status == .stopped, - "Expected wordpress container to be stopped, found status: \(wordpressContainer.status.rawValue)" - ) - #expect( - dbContainer.status == .stopped, - "Expected db container to be stopped, found status: \(dbContainer.status.rawValue)" - ) + #expect(containers.filter({ $0.status == .stopped}).count == 2, "Expected 2 stopped containers for \(project.name), found \(containers.count)") } @Test("What goes up must come down - container_name") @@ -120,7 +84,7 @@ struct ComposeDownTests { containers.count == 1, "Expected 1 container with the name \(containerName), found \(containers.count)") #expect( - containers[0].status == .running, + containers.filter({ $0.status == .running}).count == 1, "Expected container \(containerName) to be running, found status: \(containers[0].status.rawValue)" ) @@ -136,7 +100,7 @@ struct ComposeDownTests { containers.count == 1, "Expected 1 container with the name \(containerName), found \(containers.count)") #expect( - containers[0].status == .stopped, + containers.filter({ $0.status == .stopped }).count == 1, "Expected container \(containerName) to be stopped, found status: \(containers[0].status.rawValue)" ) } From 5f54b08733c78bfb6bc9c3cc4f6ec98555a5df69 Mon Sep 17 00:00:00 2001 From: Morris Richman <81453549+Mcrich23@users.noreply.github.com> Date: Tue, 3 Feb 2026 20:40:43 -0800 Subject: [PATCH 7/9] Update ComposeDownTests.swift --- Tests/Container-Compose-DynamicTests/ComposeDownTests.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/Container-Compose-DynamicTests/ComposeDownTests.swift b/Tests/Container-Compose-DynamicTests/ComposeDownTests.swift index 939a599..f11a737 100644 --- a/Tests/Container-Compose-DynamicTests/ComposeDownTests.swift +++ b/Tests/Container-Compose-DynamicTests/ComposeDownTests.swift @@ -44,7 +44,7 @@ struct ComposeDownTests { containers.count == 2, "Expected 2 containers for \(project.name), found \(containers.count)") - #expect(containers.filter({ $0.status == .stopped}).count == 2, "Expected 2 running containers for \(project.name), found \(containers.count)") + #expect(containers.filter({ $0.status == .stopped }).count == 2, "Expected 2 running containers for \(project.name), found \(containers.count)") var composeDown = try ComposeDown.parse(["--cwd", project.base.path(percentEncoded: false)]) try await composeDown.run() @@ -85,7 +85,7 @@ struct ComposeDownTests { "Expected 1 container with the name \(containerName), found \(containers.count)") #expect( containers.filter({ $0.status == .running}).count == 1, - "Expected container \(containerName) to be running, found status: \(containers[0].status.rawValue)" + "Expected container \(containerName) to be running, found status: \(containers.map(\.status))" ) var composeDown = try ComposeDown.parse(["--cwd", project.base.path(percentEncoded: false)]) @@ -101,7 +101,7 @@ struct ComposeDownTests { "Expected 1 container with the name \(containerName), found \(containers.count)") #expect( containers.filter({ $0.status == .stopped }).count == 1, - "Expected container \(containerName) to be stopped, found status: \(containers[0].status.rawValue)" + "Expected container \(containerName) to be stopped, found status: \(containers.map(\.status))" ) } From d4be60c592bfb9373bd09a025c89e0b7f5b548e8 Mon Sep 17 00:00:00 2001 From: Morris Richman <81453549+Mcrich23@users.noreply.github.com> Date: Wed, 4 Feb 2026 09:27:23 -0800 Subject: [PATCH 8/9] Update ComposeDownTests.swift --- Tests/Container-Compose-DynamicTests/ComposeDownTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Container-Compose-DynamicTests/ComposeDownTests.swift b/Tests/Container-Compose-DynamicTests/ComposeDownTests.swift index f11a737..b7b199c 100644 --- a/Tests/Container-Compose-DynamicTests/ComposeDownTests.swift +++ b/Tests/Container-Compose-DynamicTests/ComposeDownTests.swift @@ -44,7 +44,7 @@ struct ComposeDownTests { containers.count == 2, "Expected 2 containers for \(project.name), found \(containers.count)") - #expect(containers.filter({ $0.status == .stopped }).count == 2, "Expected 2 running containers for \(project.name), found \(containers.count)") + #expect(containers.filter({ $0.status == .running }).count == 2, "Expected 2 running containers for \(project.name), found \(containers.count)") var composeDown = try ComposeDown.parse(["--cwd", project.base.path(percentEncoded: false)]) try await composeDown.run() From 0cb25fa19beae41450c5770f221cfb277efebe06 Mon Sep 17 00:00:00 2001 From: Morris Richman <81453549+Mcrich23@users.noreply.github.com> Date: Wed, 4 Feb 2026 09:30:31 -0800 Subject: [PATCH 9/9] Update ComposeDownTests.swift --- Tests/Container-Compose-DynamicTests/ComposeDownTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/Container-Compose-DynamicTests/ComposeDownTests.swift b/Tests/Container-Compose-DynamicTests/ComposeDownTests.swift index b7b199c..d983bfc 100644 --- a/Tests/Container-Compose-DynamicTests/ComposeDownTests.swift +++ b/Tests/Container-Compose-DynamicTests/ComposeDownTests.swift @@ -44,7 +44,7 @@ struct ComposeDownTests { containers.count == 2, "Expected 2 containers for \(project.name), found \(containers.count)") - #expect(containers.filter({ $0.status == .running }).count == 2, "Expected 2 running containers for \(project.name), found \(containers.count)") + #expect(containers.filter({ $0.status == .running }).count == 2, "Expected 2 running containers for \(project.name), found \(containers.filter({ $0.status == .running }).count)") var composeDown = try ComposeDown.parse(["--cwd", project.base.path(percentEncoded: false)]) try await composeDown.run() @@ -58,7 +58,7 @@ struct ComposeDownTests { containers.count == 2, "Expected 2 containers for \(project.name), found \(containers.count)") - #expect(containers.filter({ $0.status == .stopped}).count == 2, "Expected 2 stopped containers for \(project.name), found \(containers.count)") + #expect(containers.filter({ $0.status == .stopped}).count == 2, "Expected 2 stopped containers for \(project.name), found \(containers.filter({ $0.status == .stopped }).count)") } @Test("What goes up must come down - container_name")