From ebcb8b85cd6eb84cfaa8012cb65285011e90a2a4 Mon Sep 17 00:00:00 2001
From: Morris Richman <81453549+Mcrich23@users.noreply.github.com>
Date: Thu, 23 Oct 2025 11:09:42 -0700
Subject: [PATCH 01/13] removed tests that were doing nothing
---
.../ApplicationConfigurationTests.swift | 235 ------------------
.../EnvFileLoadingTests.swift | 2 -
.../ErrorHandlingTests.swift | 122 ---------
.../PortMappingTests.swift | 149 -----------
.../VolumeConfigurationTests.swift | 141 -----------
5 files changed, 649 deletions(-)
delete mode 100644 Tests/Container-ComposeTests/ApplicationConfigurationTests.swift
delete mode 100644 Tests/Container-ComposeTests/ErrorHandlingTests.swift
delete mode 100644 Tests/Container-ComposeTests/PortMappingTests.swift
delete mode 100644 Tests/Container-ComposeTests/VolumeConfigurationTests.swift
diff --git a/Tests/Container-ComposeTests/ApplicationConfigurationTests.swift b/Tests/Container-ComposeTests/ApplicationConfigurationTests.swift
deleted file mode 100644
index 3b4a1ab..0000000
--- a/Tests/Container-ComposeTests/ApplicationConfigurationTests.swift
+++ /dev/null
@@ -1,235 +0,0 @@
-//===----------------------------------------------------------------------===//
-// 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
-@testable import ContainerComposeCore
-
-@Suite("Application Configuration Tests")
-struct ApplicationConfigurationTests {
-
- @Test("Command name is container-compose")
- func commandName() {
- let expectedName = "container-compose"
- #expect(expectedName == "container-compose")
- }
-
- @Test("Version string format")
- func versionStringFormat() {
- let version = "v0.5.1"
- let commandName = "container-compose"
- let versionString = "\(commandName) version \(version)"
-
- #expect(versionString == "container-compose version v0.5.1")
- }
-
- @Test("Version string contains command name")
- func versionStringContainsCommandName() {
- let versionString = "container-compose version v0.5.1"
-
- #expect(versionString.contains("container-compose"))
- }
-
- @Test("Version string contains version number")
- func versionStringContainsVersionNumber() {
- let versionString = "container-compose version v0.5.1"
-
- #expect(versionString.contains("v0.5.1"))
- }
-
- @Test("Supported subcommands")
- func supportedSubcommands() {
- let subcommands = ["up", "down", "version"]
-
- #expect(subcommands.contains("up"))
- #expect(subcommands.contains("down"))
- #expect(subcommands.contains("version"))
- #expect(subcommands.count == 3)
- }
-
- @Test("Abstract description")
- func abstractDescription() {
- let abstract = "A tool to use manage Docker Compose files with Apple Container"
-
- #expect(abstract.contains("Docker Compose"))
- #expect(abstract.contains("Apple Container"))
- }
-
- @Test("Default compose filenames")
- func defaultComposeFilenames() {
- let filenames = [
- "compose.yml",
- "compose.yaml",
- "docker-compose.yml",
- "docker-compose.yaml"
- ]
-
- #expect(filenames.count == 4)
- #expect(filenames.contains("compose.yml"))
- #expect(filenames.contains("docker-compose.yml"))
- }
-
- @Test("Default env file name")
- func defaultEnvFileName() {
- let envFile = ".env"
-
- #expect(envFile == ".env")
- }
-}
-
-@Suite("Command Line Flag Tests")
-struct CommandLineFlagTests {
-
- @Test("ComposeUp flags - detach flag short form")
- func composeUpDetachFlagShortForm() {
- let shortFlag = "-d"
- #expect(shortFlag == "-d")
- }
-
- @Test("ComposeUp flags - detach flag long form")
- func composeUpDetachFlagLongForm() {
- let longFlag = "--detach"
- #expect(longFlag == "--detach")
- }
-
- @Test("ComposeUp flags - file flag short form")
- func composeUpFileFlagShortForm() {
- let shortFlag = "-f"
- #expect(shortFlag == "-f")
- }
-
- @Test("ComposeUp flags - file flag long form")
- func composeUpFileFlagLongForm() {
- let longFlag = "--file"
- #expect(longFlag == "--file")
- }
-
- @Test("ComposeUp flags - build flag short form")
- func composeUpBuildFlagShortForm() {
- let shortFlag = "-b"
- #expect(shortFlag == "-b")
- }
-
- @Test("ComposeUp flags - build flag long form")
- func composeUpBuildFlagLongForm() {
- let longFlag = "--build"
- #expect(longFlag == "--build")
- }
-
- @Test("ComposeUp flags - no-cache flag")
- func composeUpNoCacheFlag() {
- let flag = "--no-cache"
- #expect(flag == "--no-cache")
- }
-
- @Test("ComposeDown flags - file flag")
- func composeDownFileFlag() {
- let shortFlag = "-f"
- let longFlag = "--file"
-
- #expect(shortFlag == "-f")
- #expect(longFlag == "--file")
- }
-}
-
-@Suite("File Path Resolution Tests")
-struct FilePathResolutionTests {
-
- @Test("Compose path from cwd and filename")
- func composePathResolution() {
- let cwd = "/home/user/project"
- let filename = "compose.yml"
- let composePath = "\(cwd)/\(filename)"
-
- #expect(composePath == "/home/user/project/compose.yml")
- }
-
- @Test("Env file path from cwd")
- func envFilePathResolution() {
- let cwd = "/home/user/project"
- let envFile = ".env"
- let envFilePath = "\(cwd)/\(envFile)"
-
- #expect(envFilePath == "/home/user/project/.env")
- }
-
- @Test("Current directory path")
- func currentDirectoryPath() {
- let currentPath = FileManager.default.currentDirectoryPath
-
- #expect(currentPath.isEmpty == false)
- }
-
- @Test("Project name from directory")
- func projectNameFromDirectory() {
- let path = "/home/user/my-project"
- let url = URL(fileURLWithPath: path)
- let projectName = url.lastPathComponent
-
- #expect(projectName == "my-project")
- }
-
- @Test("Project name extraction")
- func projectNameExtraction() {
- let paths = [
- "/home/user/web-app",
- "/var/projects/api-service",
- "/tmp/test-container"
- ]
-
- let names = paths.map { URL(fileURLWithPath: $0).lastPathComponent }
-
- #expect(names[0] == "web-app")
- #expect(names[1] == "api-service")
- #expect(names[2] == "test-container")
- }
-}
-
-@Suite("Container Naming Tests")
-struct ContainerNamingTests {
-
- @Test("Container name with project prefix")
- func containerNameWithProjectPrefix() {
- let projectName = "my-project"
- let serviceName = "web"
- let containerName = "\(projectName)-\(serviceName)"
-
- #expect(containerName == "my-project-web")
- }
-
- @Test("Multiple container names")
- func multipleContainerNames() {
- let projectName = "app"
- let services = ["web", "db", "redis"]
- let containerNames = services.map { "\(projectName)-\($0)" }
-
- #expect(containerNames.count == 3)
- #expect(containerNames[0] == "app-web")
- #expect(containerNames[1] == "app-db")
- #expect(containerNames[2] == "app-redis")
- }
-
- @Test("Container name sanitization")
- func containerNameSanitization() {
- // Container names should be valid
- let projectName = "my-project"
- let serviceName = "web-service"
- let containerName = "\(projectName)-\(serviceName)"
-
- #expect(containerName.contains(" ") == false)
- #expect(containerName.contains("-") == true)
- }
-}
diff --git a/Tests/Container-ComposeTests/EnvFileLoadingTests.swift b/Tests/Container-ComposeTests/EnvFileLoadingTests.swift
index a206d9b..4252485 100644
--- a/Tests/Container-ComposeTests/EnvFileLoadingTests.swift
+++ b/Tests/Container-ComposeTests/EnvFileLoadingTests.swift
@@ -180,5 +180,3 @@ struct EnvFileLoadingTests {
#expect(envVars.count == 4)
}
}
-
-// Test helper function that mimics the actual implementation
diff --git a/Tests/Container-ComposeTests/ErrorHandlingTests.swift b/Tests/Container-ComposeTests/ErrorHandlingTests.swift
deleted file mode 100644
index cb4c611..0000000
--- a/Tests/Container-ComposeTests/ErrorHandlingTests.swift
+++ /dev/null
@@ -1,122 +0,0 @@
-//===----------------------------------------------------------------------===//
-// 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
-@testable import ContainerComposeCore
-
-@Suite("Error Handling Tests")
-struct ErrorHandlingTests {
-
- @Test("YamlError.composeFileNotFound contains path")
- func yamlErrorComposeFileNotFoundMessage() {
- let error = YamlError.composeFileNotFound("/path/to/directory")
- let description = error.errorDescription
-
- #expect(description != nil)
- #expect(description?.contains("/path/to/directory") == true)
- }
-
- @Test("ComposeError.imageNotFound contains service name")
- func composeErrorImageNotFoundMessage() {
- let error = ComposeError.imageNotFound("my-service")
- let description = error.errorDescription
-
- #expect(description != nil)
- #expect(description?.contains("my-service") == true)
- }
-
- @Test("ComposeError.invalidProjectName has appropriate message")
- func composeErrorInvalidProjectNameMessage() {
- let error = ComposeError.invalidProjectName
- let description = error.errorDescription
-
- #expect(description != nil)
- #expect(description?.contains("project name") == true)
- }
-
- @Test("TerminalError.commandFailed contains command info")
- func terminalErrorCommandFailedMessage() {
- let error = TerminalError.commandFailed("container run nginx")
- let description = error.errorDescription
-
- #expect(description != nil)
- #expect(description?.contains("Command failed") == true)
- }
-
- @Test("CommandOutput enum cases")
- func commandOutputEnumCases() {
- let stdout = CommandOutput.stdout("test output")
- let stderr = CommandOutput.stderr("error output")
- let exitCode = CommandOutput.exitCode(0)
-
- switch stdout {
- case .stdout(let output):
- #expect(output == "test output")
- default:
- Issue.record("Expected stdout case")
- }
-
- switch stderr {
- case .stderr(let output):
- #expect(output == "error output")
- default:
- Issue.record("Expected stderr case")
- }
-
- switch exitCode {
- case .exitCode(let code):
- #expect(code == 0)
- default:
- Issue.record("Expected exitCode case")
- }
- }
-}
-
-// Test helper enums that mirror the actual implementation
-enum YamlError: Error, LocalizedError {
- case composeFileNotFound(String)
-
- var errorDescription: String? {
- switch self {
- case .composeFileNotFound(let path):
- return "compose.yml not found at \(path)"
- }
- }
-}
-
-enum ComposeError: Error, LocalizedError {
- case imageNotFound(String)
- case invalidProjectName
-
- var errorDescription: String? {
- switch self {
- case .imageNotFound(let name):
- return "Service \(name) must define either 'image' or 'build'."
- case .invalidProjectName:
- return "Could not find project name."
- }
- }
-}
-
-enum TerminalError: Error, LocalizedError {
- case commandFailed(String)
-
- var errorDescription: String? {
- "Command failed: \(self)"
- }
-}
-
diff --git a/Tests/Container-ComposeTests/PortMappingTests.swift b/Tests/Container-ComposeTests/PortMappingTests.swift
deleted file mode 100644
index 4baf2d9..0000000
--- a/Tests/Container-ComposeTests/PortMappingTests.swift
+++ /dev/null
@@ -1,149 +0,0 @@
-//===----------------------------------------------------------------------===//
-// 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
-@testable import ContainerComposeCore
-
-@Suite("Port Mapping Tests")
-struct PortMappingTests {
-
- @Test("Parse simple port mapping")
- func parseSimplePortMapping() {
- let portString = "8080:80"
- let components = portString.split(separator: ":").map(String.init)
-
- #expect(components.count == 2)
- #expect(components[0] == "8080")
- #expect(components[1] == "80")
- }
-
- @Test("Parse port mapping with protocol")
- func parsePortMappingWithProtocol() {
- let portString = "8080:80/tcp"
- let parts = portString.split(separator: "/")
- let portParts = parts[0].split(separator: ":").map(String.init)
-
- #expect(portParts.count == 2)
- #expect(portParts[0] == "8080")
- #expect(portParts[1] == "80")
- #expect(parts.count == 2)
- #expect(String(parts[1]) == "tcp")
- }
-
- @Test("Parse port mapping with IP binding")
- func parsePortMappingWithIPBinding() {
- let portString = "127.0.0.1:8080:80"
- let components = portString.split(separator: ":").map(String.init)
-
- #expect(components.count == 3)
- #expect(components[0] == "127.0.0.1")
- #expect(components[1] == "8080")
- #expect(components[2] == "80")
- }
-
- @Test("Parse single port (container only)")
- func parseSinglePort() {
- let portString = "80"
- let components = portString.split(separator: ":").map(String.init)
-
- #expect(components.count == 1)
- #expect(components[0] == "80")
- }
-
- @Test("Parse port range")
- func parsePortRange() {
- let portString = "8000-8010:8000-8010"
- let components = portString.split(separator: ":").map(String.init)
-
- #expect(components.count == 2)
- #expect(components[0] == "8000-8010")
- #expect(components[1] == "8000-8010")
- }
-
- @Test("Parse UDP port mapping")
- func parseUDPPortMapping() {
- let portString = "53:53/udp"
- let parts = portString.split(separator: "/")
- let portParts = parts[0].split(separator: ":").map(String.init)
-
- #expect(portParts.count == 2)
- #expect(String(parts[1]) == "udp")
- }
-
- @Test("Parse IPv6 address binding")
- func parseIPv6AddressBinding() {
- let portString = "[::1]:8080:80"
-
- // IPv6 addresses are enclosed in brackets
- #expect(portString.contains("[::1]"))
- }
-
- @Test("Multiple port mappings in array")
- func multiplePortMappings() {
- let ports = ["80:80", "443:443", "8080:8080"]
-
- #expect(ports.count == 3)
- for port in ports {
- let components = port.split(separator: ":").map(String.init)
- #expect(components.count == 2)
- }
- }
-
- @Test("Port mapping with string format in YAML")
- func portMappingStringFormat() {
- let port1 = "8080:80"
- let port2 = "3000"
-
- #expect(port1.contains(":") == true)
- #expect(port2.contains(":") == false)
- }
-
- @Test("Extract host port from mapping")
- func extractHostPort() {
- let portString = "8080:80"
- let components = portString.split(separator: ":").map(String.init)
- let hostPort = components.first
-
- #expect(hostPort == "8080")
- }
-
- @Test("Extract container port from mapping")
- func extractContainerPort() {
- let portString = "8080:80"
- let components = portString.split(separator: ":").map(String.init)
- let containerPort = components.last
-
- #expect(containerPort == "80")
- }
-
- @Test("Validate numeric port values")
- func validateNumericPortValues() {
- let validPort = "8080"
- let invalidPort = "not-a-port"
-
- #expect(Int(validPort) != nil)
- #expect(Int(invalidPort) == nil)
- }
-
- @Test("Parse quoted port string")
- func parseQuotedPortString() {
- // In YAML, ports can be quoted to ensure string interpretation
- let portString = "8080:80"
-
- #expect(portString == "8080:80")
- }
-}
diff --git a/Tests/Container-ComposeTests/VolumeConfigurationTests.swift b/Tests/Container-ComposeTests/VolumeConfigurationTests.swift
deleted file mode 100644
index a4e82b9..0000000
--- a/Tests/Container-ComposeTests/VolumeConfigurationTests.swift
+++ /dev/null
@@ -1,141 +0,0 @@
-//===----------------------------------------------------------------------===//
-// 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
-@testable import ContainerComposeCore
-
-@Suite("Volume Configuration Tests")
-struct VolumeConfigurationTests {
-
- @Test("Parse named volume mount")
- func parseNamedVolumeMount() {
- let volumeString = "db-data:/var/lib/postgresql/data"
- let components = volumeString.split(separator: ":").map(String.init)
-
- #expect(components.count == 2)
- #expect(components[0] == "db-data")
- #expect(components[1] == "/var/lib/postgresql/data")
- }
-
- @Test("Parse bind mount with absolute path")
- func parseBindMountAbsolutePath() {
- let volumeString = "/host/path:/container/path"
- let components = volumeString.split(separator: ":").map(String.init)
-
- #expect(components.count == 2)
- #expect(components[0] == "/host/path")
- #expect(components[1] == "/container/path")
- }
-
- @Test("Parse bind mount with relative path")
- func parseBindMountRelativePath() {
- let volumeString = "./data:/app/data"
- let components = volumeString.split(separator: ":").map(String.init)
-
- #expect(components.count == 2)
- #expect(components[0] == "./data")
- #expect(components[1] == "/app/data")
- }
-
- @Test("Parse volume with read-only flag")
- func parseVolumeWithReadOnlyFlag() {
- let volumeString = "db-data:/var/lib/postgresql/data:ro"
- let components = volumeString.split(separator: ":").map(String.init)
-
- #expect(components.count == 3)
- #expect(components[0] == "db-data")
- #expect(components[1] == "/var/lib/postgresql/data")
- #expect(components[2] == "ro")
- }
-
- @Test("Identify bind mount by forward slash")
- func identifyBindMountBySlash() {
- let namedVolume = "my-volume"
- let bindMount = "/absolute/path"
- let relativeMount = "./relative/path"
-
- #expect(namedVolume.contains("/") == false)
- #expect(bindMount.contains("/") == true)
- #expect(relativeMount.contains("/") == true)
- }
-
- @Test("Identify bind mount by dot prefix")
- func identifyBindMountByDot() {
- let volumes = ["./data", "../config", "named-volume"]
-
- #expect(volumes[0].starts(with: ".") == true)
- #expect(volumes[1].starts(with: ".") == true)
- #expect(volumes[2].starts(with: ".") == false)
- }
-
- @Test("Parse volume mount with multiple colons")
- func parseVolumeMountWithMultipleColons() {
- let volumeString = "/host/path:/container/path:ro"
- let components = volumeString.split(separator: ":").map(String.init)
-
- #expect(components.count == 3)
- #expect(components[0] == "/host/path")
- #expect(components[1] == "/container/path")
- #expect(components[2] == "ro")
- }
-
- @Test("Handle invalid volume format")
- func handleInvalidVolumeFormat() {
- let invalidVolume = "invalid-format"
- let components = invalidVolume.split(separator: ":").map(String.init)
-
- // Should have only one component (no colon)
- #expect(components.count == 1)
- }
-
- @Test("Parse tmpfs mount (if supported)")
- func parseTmpfsMount() {
- let volumeString = "tmpfs:/app/tmp"
- let components = volumeString.split(separator: ":").map(String.init)
-
- #expect(components.count == 2)
- #expect(components[0] == "tmpfs")
- #expect(components[1] == "/app/tmp")
- }
-
- @Test("Resolve relative path to absolute")
- func resolveRelativePathToAbsolute() {
- let relativePath = "./data"
- let cwd = "/home/user/project"
- let fullPath = cwd + "/" + relativePath
-
- #expect(fullPath == "/home/user/project/./data")
- }
-
- @Test("Handle tilde expansion in path")
- func handleTildeInPath() {
- let pathWithTilde = "~/data"
- let pathWithAbsolute = "/absolute/path"
-
- #expect(pathWithTilde.starts(with: "~") == true)
- #expect(pathWithAbsolute.starts(with: "/") == true)
- }
-
- @Test("Empty volume definitions should be handled")
- func handleEmptyVolumeDefinitions() {
- // When volumes section exists but is empty
- let volumes: [String: Volume] = [:]
-
- #expect(volumes.isEmpty == true)
- }
-
-}
From 3f413632a09cbc5360ffcd9f61de1cb88a0b0ce7 Mon Sep 17 00:00:00 2001
From: Morris Richman <81453549+Mcrich23@users.noreply.github.com>
Date: Thu, 23 Oct 2025 12:08:12 -0700
Subject: [PATCH 02/13] Update DockerComposeParsingTests.swift
---
.../DockerComposeParsingTests.swift | 294 +++++++++++++++++-
1 file changed, 293 insertions(+), 1 deletion(-)
diff --git a/Tests/Container-ComposeTests/DockerComposeParsingTests.swift b/Tests/Container-ComposeTests/DockerComposeParsingTests.swift
index 7e49c9a..74a98e5 100644
--- a/Tests/Container-ComposeTests/DockerComposeParsingTests.swift
+++ b/Tests/Container-ComposeTests/DockerComposeParsingTests.swift
@@ -21,7 +21,7 @@ import Foundation
@Suite("DockerCompose YAML Parsing Tests")
struct DockerComposeParsingTests {
-
+ // MARK: File Snippets
@Test("Parse basic docker-compose.yml with single service")
func parseBasicCompose() throws {
let yaml = """
@@ -392,4 +392,296 @@ struct DockerComposeParsingTests {
try decoder.decode(DockerCompose.self, from: yaml)
}
}
+
+ // MARK: Full Files
+ @Test("Parse WordPress with MySQL compose file")
+ func parseWordPressCompose() throws {
+ let yaml = """
+ 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
+
+ volumes:
+ wordpress_data:
+ db_data:
+ """
+
+ let decoder = YAMLDecoder()
+ let compose = try decoder.decode(DockerCompose.self, from: yaml)
+
+ #expect(compose.services.count == 2)
+ #expect(compose.services["wordpress"] != nil)
+ #expect(compose.services["db"] != nil)
+ #expect(compose.volumes?.count == 2)
+ #expect(compose.services["wordpress"]??.depends_on?.contains("db") == true)
+ }
+
+ @Test("Parse three-tier web application")
+ func parseThreeTierApp() throws {
+ let yaml = """
+ 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
+
+ volumes:
+ db-data:
+
+ networks:
+ frontend:
+ backend:
+ """
+
+ let decoder = YAMLDecoder()
+ let compose = try decoder.decode(DockerCompose.self, from: yaml)
+
+ #expect(compose.name == "webapp")
+ #expect(compose.services.count == 4)
+ #expect(compose.networks?.count == 2)
+ #expect(compose.volumes?.count == 1)
+ }
+
+ @Test("Parse microservices architecture")
+ func parseMicroservicesCompose() throws {
+ let yaml = """
+ version: '3.8'
+
+ services:
+ api-gateway:
+ image: traefik:v2.10
+ ports:
+ - "80:80"
+ - "8080: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
+ """
+
+ let decoder = YAMLDecoder()
+ let compose = try decoder.decode(DockerCompose.self, from: yaml)
+
+ #expect(compose.services.count == 5)
+ #expect(compose.services["api-gateway"]??.depends_on?.count == 3)
+ }
+
+ @Test("Parse development environment with build")
+ func parseDevelopmentEnvironment() throws {
+ let yaml = """
+ 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
+ """
+
+ let decoder = YAMLDecoder()
+ let compose = try decoder.decode(DockerCompose.self, from: yaml)
+
+ #expect(compose.services["app"]??.build != nil)
+ #expect(compose.services["app"]??.build?.context == ".")
+ #expect(compose.services["app"]??.volumes?.count == 2)
+ }
+
+ @Test("Parse compose with secrets and configs")
+ func parseComposeWithSecretsAndConfigs() throws {
+ let yaml = """
+ version: '3.8'
+
+ services:
+ app:
+ image: myapp:latest
+ configs:
+ - source: app_config
+ target: /etc/app/config.yml
+ secrets:
+ - db_password
+
+ configs:
+ app_config:
+ external: true
+
+ secrets:
+ db_password:
+ external: true
+ """
+
+ let decoder = YAMLDecoder()
+ let compose = try decoder.decode(DockerCompose.self, from: yaml)
+
+ #expect(compose.configs != nil)
+ #expect(compose.secrets != nil)
+ }
+
+ @Test("Parse compose with healthchecks and restart policies")
+ func parseComposeWithHealthchecksAndRestart() throws {
+ let yaml = """
+ 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
+ """
+
+ let decoder = YAMLDecoder()
+ let compose = try decoder.decode(DockerCompose.self, from: yaml)
+
+ #expect(compose.services["web"]??.restart == "unless-stopped")
+ #expect(compose.services["web"]??.healthcheck != nil)
+ #expect(compose.services["db"]??.restart == "always")
+ }
+
+ @Test("Parse compose with complex dependency chain")
+ func parseComplexDependencyChain() throws {
+ let yaml = """
+ 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
+ """
+
+ let decoder = YAMLDecoder()
+ let compose = try decoder.decode(DockerCompose.self, from: yaml)
+
+ #expect(compose.services.count == 4)
+
+ // Test dependency resolution
+ let services: [(String, Service)] = compose.services.compactMap({ serviceName, service in
+ guard let service else { return nil }
+ return (serviceName, service)
+ })
+ let sorted = try Service.topoSortConfiguredServices(services)
+
+ // db and cache should come before api
+ let dbIndex = sorted.firstIndex(where: { $0.serviceName == "db" })!
+ let cacheIndex = sorted.firstIndex(where: { $0.serviceName == "cache" })!
+ let apiIndex = sorted.firstIndex(where: { $0.serviceName == "api" })!
+ let frontendIndex = sorted.firstIndex(where: { $0.serviceName == "frontend" })!
+
+ #expect(dbIndex < apiIndex)
+ #expect(cacheIndex < apiIndex)
+ #expect(apiIndex < frontendIndex)
+ }
}
From 8f0b1602136526ba9808d417828f11b7e10403ad Mon Sep 17 00:00:00 2001
From: Morris Richman <81453549+Mcrich23@users.noreply.github.com>
Date: Thu, 23 Oct 2025 14:05:56 -0700
Subject: [PATCH 03/13] WIP: moved integration tests to testing compose up
---
.../DockerComposeParsingTests.swift | 198 ++++++++++--------
.../IntegrationTests.swift | 44 ++--
2 files changed, 130 insertions(+), 112 deletions(-)
diff --git a/Tests/Container-ComposeTests/DockerComposeParsingTests.swift b/Tests/Container-ComposeTests/DockerComposeParsingTests.swift
index 74a98e5..9e827fa 100644
--- a/Tests/Container-ComposeTests/DockerComposeParsingTests.swift
+++ b/Tests/Container-ComposeTests/DockerComposeParsingTests.swift
@@ -396,7 +396,107 @@ struct DockerComposeParsingTests {
// MARK: Full Files
@Test("Parse WordPress with MySQL compose file")
func parseWordPressCompose() throws {
- let yaml = """
+ let yaml = Self.dockerComposeYaml1
+
+ let decoder = YAMLDecoder()
+ let compose = try decoder.decode(DockerCompose.self, from: yaml)
+
+ #expect(compose.services.count == 2)
+ #expect(compose.services["wordpress"] != nil)
+ #expect(compose.services["db"] != nil)
+ #expect(compose.volumes?.count == 2)
+ #expect(compose.services["wordpress"]??.depends_on?.contains("db") == true)
+ }
+
+ @Test("Parse three-tier web application")
+ func parseThreeTierApp() throws {
+ let yaml = Self.dockerComposeYaml2
+
+ let decoder = YAMLDecoder()
+ let compose = try decoder.decode(DockerCompose.self, from: yaml)
+
+ #expect(compose.name == "webapp")
+ #expect(compose.services.count == 4)
+ #expect(compose.networks?.count == 2)
+ #expect(compose.volumes?.count == 1)
+ }
+
+ @Test("Parse microservices architecture")
+ func parseMicroservicesCompose() throws {
+ let yaml = Self.dockerComposeYaml3
+
+ let decoder = YAMLDecoder()
+ let compose = try decoder.decode(DockerCompose.self, from: yaml)
+
+ #expect(compose.services.count == 5)
+ #expect(compose.services["api-gateway"]??.depends_on?.count == 3)
+ }
+
+ @Test("Parse development environment with build")
+ func parseDevelopmentEnvironment() throws {
+ let yaml = Self.dockerComposeYaml4
+
+ let decoder = YAMLDecoder()
+ let compose = try decoder.decode(DockerCompose.self, from: yaml)
+
+ #expect(compose.services["app"]??.build != nil)
+ #expect(compose.services["app"]??.build?.context == ".")
+ #expect(compose.services["app"]??.volumes?.count == 2)
+ }
+
+ @Test("Parse compose with secrets and configs")
+ func parseComposeWithSecretsAndConfigs() throws {
+ let yaml = Self.dockerComposeYaml5
+
+ let decoder = YAMLDecoder()
+ let compose = try decoder.decode(DockerCompose.self, from: yaml)
+
+ #expect(compose.configs != nil)
+ #expect(compose.secrets != nil)
+ }
+
+ @Test("Parse compose with healthchecks and restart policies")
+ func parseComposeWithHealthchecksAndRestart() throws {
+ let yaml = Self.dockerComposeYaml6
+
+ let decoder = YAMLDecoder()
+ let compose = try decoder.decode(DockerCompose.self, from: yaml)
+
+ #expect(compose.services["web"]??.restart == "unless-stopped")
+ #expect(compose.services["web"]??.healthcheck != nil)
+ #expect(compose.services["db"]??.restart == "always")
+ }
+
+ @Test("Parse compose with complex dependency chain")
+ func parseComplexDependencyChain() throws {
+ let yaml = Self.dockerComposeYaml6
+
+ let decoder = YAMLDecoder()
+ let compose = try decoder.decode(DockerCompose.self, from: yaml)
+
+ #expect(compose.services.count == 4)
+
+ // Test dependency resolution
+ let services: [(String, Service)] = compose.services.compactMap({ serviceName, service in
+ guard let service else { return nil }
+ return (serviceName, service)
+ })
+ let sorted = try Service.topoSortConfiguredServices(services)
+
+ // db and cache should come before api
+ let dbIndex = sorted.firstIndex(where: { $0.serviceName == "db" })!
+ let cacheIndex = sorted.firstIndex(where: { $0.serviceName == "cache" })!
+ let apiIndex = sorted.firstIndex(where: { $0.serviceName == "api" })!
+ let frontendIndex = sorted.firstIndex(where: { $0.serviceName == "frontend" })!
+
+ #expect(dbIndex < apiIndex)
+ #expect(cacheIndex < apiIndex)
+ #expect(apiIndex < frontendIndex)
+ }
+}
+
+extension DockerComposeParsingTests {
+ static let dockerComposeYaml1 = """
version: '3.8'
services:
@@ -428,20 +528,8 @@ struct DockerComposeParsingTests {
wordpress_data:
db_data:
"""
-
- let decoder = YAMLDecoder()
- let compose = try decoder.decode(DockerCompose.self, from: yaml)
-
- #expect(compose.services.count == 2)
- #expect(compose.services["wordpress"] != nil)
- #expect(compose.services["db"] != nil)
- #expect(compose.volumes?.count == 2)
- #expect(compose.services["wordpress"]??.depends_on?.contains("db") == true)
- }
- @Test("Parse three-tier web application")
- func parseThreeTierApp() throws {
- let yaml = """
+ static let dockerComposeYaml2 = """
version: '3.8'
name: webapp
@@ -491,19 +579,8 @@ struct DockerComposeParsingTests {
frontend:
backend:
"""
-
- let decoder = YAMLDecoder()
- let compose = try decoder.decode(DockerCompose.self, from: yaml)
-
- #expect(compose.name == "webapp")
- #expect(compose.services.count == 4)
- #expect(compose.networks?.count == 2)
- #expect(compose.volumes?.count == 1)
- }
- @Test("Parse microservices architecture")
- func parseMicroservicesCompose() throws {
- let yaml = """
+ static let dockerComposeYaml3 = """
version: '3.8'
services:
@@ -538,17 +615,8 @@ struct DockerComposeParsingTests {
environment:
POSTGRES_PASSWORD: postgres
"""
-
- let decoder = YAMLDecoder()
- let compose = try decoder.decode(DockerCompose.self, from: yaml)
-
- #expect(compose.services.count == 5)
- #expect(compose.services["api-gateway"]??.depends_on?.count == 3)
- }
- @Test("Parse development environment with build")
- func parseDevelopmentEnvironment() throws {
- let yaml = """
+ static let dockerComposeYaml4 = """
version: '3.8'
services:
@@ -565,18 +633,8 @@ struct DockerComposeParsingTests {
- "3000:3000"
command: npm run dev
"""
-
- let decoder = YAMLDecoder()
- let compose = try decoder.decode(DockerCompose.self, from: yaml)
-
- #expect(compose.services["app"]??.build != nil)
- #expect(compose.services["app"]??.build?.context == ".")
- #expect(compose.services["app"]??.volumes?.count == 2)
- }
- @Test("Parse compose with secrets and configs")
- func parseComposeWithSecretsAndConfigs() throws {
- let yaml = """
+ static let dockerComposeYaml5 = """
version: '3.8'
services:
@@ -596,17 +654,8 @@ struct DockerComposeParsingTests {
db_password:
external: true
"""
-
- let decoder = YAMLDecoder()
- let compose = try decoder.decode(DockerCompose.self, from: yaml)
-
- #expect(compose.configs != nil)
- #expect(compose.secrets != nil)
- }
- @Test("Parse compose with healthchecks and restart policies")
- func parseComposeWithHealthchecksAndRestart() throws {
- let yaml = """
+ static let dockerComposeYaml6 = """
version: '3.8'
services:
@@ -629,18 +678,8 @@ struct DockerComposeParsingTests {
timeout: 5s
retries: 5
"""
-
- let decoder = YAMLDecoder()
- let compose = try decoder.decode(DockerCompose.self, from: yaml)
-
- #expect(compose.services["web"]??.restart == "unless-stopped")
- #expect(compose.services["web"]??.healthcheck != nil)
- #expect(compose.services["db"]??.restart == "always")
- }
- @Test("Parse compose with complex dependency chain")
- func parseComplexDependencyChain() throws {
- let yaml = """
+ static let dockerComposeYaml7 = """
version: '3.8'
services:
@@ -661,27 +700,4 @@ struct DockerComposeParsingTests {
db:
image: postgres:14
"""
-
- let decoder = YAMLDecoder()
- let compose = try decoder.decode(DockerCompose.self, from: yaml)
-
- #expect(compose.services.count == 4)
-
- // Test dependency resolution
- let services: [(String, Service)] = compose.services.compactMap({ serviceName, service in
- guard let service else { return nil }
- return (serviceName, service)
- })
- let sorted = try Service.topoSortConfiguredServices(services)
-
- // db and cache should come before api
- let dbIndex = sorted.firstIndex(where: { $0.serviceName == "db" })!
- let cacheIndex = sorted.firstIndex(where: { $0.serviceName == "cache" })!
- let apiIndex = sorted.firstIndex(where: { $0.serviceName == "api" })!
- let frontendIndex = sorted.firstIndex(where: { $0.serviceName == "frontend" })!
-
- #expect(dbIndex < apiIndex)
- #expect(cacheIndex < apiIndex)
- #expect(apiIndex < frontendIndex)
- }
}
diff --git a/Tests/Container-ComposeTests/IntegrationTests.swift b/Tests/Container-ComposeTests/IntegrationTests.swift
index dbd024b..11a46b5 100644
--- a/Tests/Container-ComposeTests/IntegrationTests.swift
+++ b/Tests/Container-ComposeTests/IntegrationTests.swift
@@ -16,10 +16,12 @@
import Testing
import Foundation
+import ContainerCommands
+import ContainerClient
@testable import Yams
@testable import ContainerComposeCore
-@Suite("Integration Tests - Real-World Compose Files")
+@Suite("Compose Up Tests - Real-World Compose Files", .containerDependent)
struct IntegrationTests {
@Test("Parse WordPress with MySQL compose file")
@@ -267,7 +269,7 @@ struct IntegrationTests {
}
@Test("Parse compose with complex dependency chain")
- func parseComplexDependencyChain() throws {
+ func parseComplexDependencyChain() async throws {
let yaml = """
version: '3.8'
@@ -289,28 +291,28 @@ struct IntegrationTests {
db:
image: postgres:14
"""
+ let tempLocation = URL.temporaryDirectory.appending(path: "Container-Compose_Tests/docker-compose.yaml")
+ try? FileManager.default.createDirectory(at: tempLocation.deletingLastPathComponent(), withIntermediateDirectories: true)
+ try yaml.write(to: tempLocation, atomically: false, encoding: .utf8)
- let decoder = YAMLDecoder()
- let compose = try decoder.decode(DockerCompose.self, from: yaml)
-
- #expect(compose.services.count == 4)
-
- // Test dependency resolution
- let services: [(String, Service)] = compose.services.compactMap({ serviceName, service in
- guard let service else { return nil }
- return (serviceName, service)
- })
- let sorted = try Service.topoSortConfiguredServices(services)
+ var composeUp = try ComposeUp.parse(["-d", "--cwd", tempLocation.deletingLastPathComponent().path(percentEncoded: false)])
+ try await composeUp.run()
- // db and cache should come before api
- let dbIndex = sorted.firstIndex(where: { $0.serviceName == "db" })!
- let cacheIndex = sorted.firstIndex(where: { $0.serviceName == "cache" })!
- let apiIndex = sorted.firstIndex(where: { $0.serviceName == "api" })!
- let frontendIndex = sorted.firstIndex(where: { $0.serviceName == "frontend" })!
+ let containers = try await ClientContainer.list()
+ print(containers)
+ }
+}
+
+struct ContainerDependentTrait: TestScoping, TestTrait, SuiteTrait {
+ func provideScope(for test: Test, testCase: Test.Case?, performing function: () async throws -> Void) async throws {
+ // Start Server
+ try await Application.SystemStart.parse([]).run()
- #expect(dbIndex < apiIndex)
- #expect(cacheIndex < apiIndex)
- #expect(apiIndex < frontendIndex)
+ // Run Test
+ try await function()
}
}
+extension Trait where Self == ContainerDependentTrait {
+ static var containerDependent: ContainerDependentTrait { .init() }
+}
From bc442b14381425dc5c0da248b41f7e41b0f7cffe Mon Sep 17 00:00:00 2001
From: Morris Richman <81453549+Mcrich23@users.noreply.github.com>
Date: Thu, 23 Oct 2025 14:59:49 -0700
Subject: [PATCH 04/13] WIP: Create first compose up test
---
.../DockerComposeParsingTests.swift | 4 +-
.../IntegrationTests.swift | 278 ++++--------------
2 files changed, 67 insertions(+), 215 deletions(-)
diff --git a/Tests/Container-ComposeTests/DockerComposeParsingTests.swift b/Tests/Container-ComposeTests/DockerComposeParsingTests.swift
index 9e827fa..1820b7f 100644
--- a/Tests/Container-ComposeTests/DockerComposeParsingTests.swift
+++ b/Tests/Container-ComposeTests/DockerComposeParsingTests.swift
@@ -587,8 +587,8 @@ extension DockerComposeParsingTests {
api-gateway:
image: traefik:v2.10
ports:
- - "80:80"
- - "8080:8080"
+ - "81:80"
+ - "8081:8080"
depends_on:
- auth-service
- user-service
diff --git a/Tests/Container-ComposeTests/IntegrationTests.swift b/Tests/Container-ComposeTests/IntegrationTests.swift
index 11a46b5..3678816 100644
--- a/Tests/Container-ComposeTests/IntegrationTests.swift
+++ b/Tests/Container-ComposeTests/IntegrationTests.swift
@@ -25,102 +25,68 @@ import ContainerClient
struct IntegrationTests {
@Test("Parse WordPress with MySQL compose file")
- func parseWordPressCompose() throws {
- let yaml = """
- version: '3.8'
+ func parseWordPressCompose() async throws {
+ let yaml = DockerComposeParsingTests.dockerComposeYaml1
- 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
-
- volumes:
- wordpress_data:
- db_data:
- """
+ let tempLocation = URL.temporaryDirectory.appending(path: "Container-Compose_Tests_\(UUID().uuidString)/docker-compose.yaml")
+ try? FileManager.default.createDirectory(at: tempLocation.deletingLastPathComponent(), withIntermediateDirectories: true)
+ try yaml.write(to: tempLocation, atomically: false, encoding: .utf8)
+ let folderName = tempLocation.deletingLastPathComponent().lastPathComponent
- let decoder = YAMLDecoder()
- let compose = try decoder.decode(DockerCompose.self, from: yaml)
+ var composeUp = try ComposeUp.parse(["-d", "--cwd", tempLocation.deletingLastPathComponent().path(percentEncoded: false)])
+ try await composeUp.run()
- #expect(compose.services.count == 2)
- #expect(compose.services["wordpress"] != nil)
- #expect(compose.services["db"] != nil)
- #expect(compose.volumes?.count == 2)
- #expect(compose.services["wordpress"]??.depends_on?.contains("db") == true)
+ // Get these containers
+ let containers = try await ClientContainer.list()
+ .filter({
+ $0.configuration.id.contains(tempLocation.deletingLastPathComponent().lastPathComponent)
+ })
+
+ // Assert correct wordpress container information
+ guard let wordpressContainer = containers.first(where: { $0.configuration.id == "\(folderName)-wordpress" }),
+ let dbContainer = containers.first(where: { $0.configuration.id == "\(folderName)-db" })
+ else {
+ throw Errors.containerNotFound
+ }
+
+ // Check Ports
+ #expect(wordpressContainer.configuration.publishedPorts.map({ "\($0.hostAddress):\($0.hostPort):\($0.containerPort)" }) == ["0.0.0.0:8080:80"])
+
+ // Check Image
+ #expect(wordpressContainer.configuration.image.reference == "docker.io/library/wordpress:latest")
+
+ // Check Environment
+ let wpEnvArray = wordpressContainer.configuration.initProcess.environment.map({ (String($0.split(separator: "=")[0]), String($0.split(separator: "=")[1])) })
+ let wpEnv = Dictionary(uniqueKeysWithValues: wpEnvArray)
+ #expect(wpEnv["WORDPRESS_DB_HOST"] == String(dbContainer.networks.first!.address.split(separator: "/")[0]))
+ #expect(wpEnv["WORDPRESS_DB_USER"] == "wordpress")
+ #expect(wpEnv["WORDPRESS_DB_PASSWORD"] == "wordpress")
+ #expect(wpEnv["WORDPRESS_DB_NAME"] == "wordpress")
+
+ // Check Volume
+ #expect(wordpressContainer.configuration.mounts.map(\.destination) == ["/var/www/"])
+
+ // Assert correct db container information
+
+ // Check Image
+ #expect(dbContainer.configuration.image.reference == "docker.io/library/mysql:8.0")
+
+ // Check Environment
+ let dbEnvArray = dbContainer.configuration.initProcess.environment.map({ (String($0.split(separator: "=")[0]), String($0.split(separator: "=")[1])) })
+ let dbEnv = Dictionary(uniqueKeysWithValues: dbEnvArray)
+ #expect(dbEnv["MYSQL_ROOT_PASSWORD"] == "rootpassword")
+ #expect(dbEnv["MYSQL_DATABASE"] == "wordpress")
+ #expect(dbEnv["MYSQL_USER"] == "wordpress")
+ #expect(dbEnv["MYSQL_PASSWORD"] == "wordpress")
+
+ // Check Volume
+ #expect(dbContainer.configuration.mounts.map(\.destination) == ["/var/lib/"])
+ print("")
}
@Test("Parse three-tier web application")
func parseThreeTierApp() throws {
- let yaml = """
- 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
-
- volumes:
- db-data:
-
- networks:
- frontend:
- backend:
- """
+ let yaml = DockerComposeParsingTests.dockerComposeYaml2
let decoder = YAMLDecoder()
let compose = try decoder.decode(DockerCompose.self, from: yaml)
@@ -133,41 +99,7 @@ struct IntegrationTests {
@Test("Parse microservices architecture")
func parseMicroservicesCompose() throws {
- let yaml = """
- version: '3.8'
-
- services:
- api-gateway:
- image: traefik:v2.10
- ports:
- - "80:80"
- - "8080: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
- """
+ let yaml = DockerComposeParsingTests.dockerComposeYaml3
let decoder = YAMLDecoder()
let compose = try decoder.decode(DockerCompose.self, from: yaml)
@@ -178,23 +110,7 @@ struct IntegrationTests {
@Test("Parse development environment with build")
func parseDevelopmentEnvironment() throws {
- let yaml = """
- 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
- """
+ let yaml = DockerComposeParsingTests.dockerComposeYaml4
let decoder = YAMLDecoder()
let compose = try decoder.decode(DockerCompose.self, from: yaml)
@@ -206,26 +122,7 @@ struct IntegrationTests {
@Test("Parse compose with secrets and configs")
func parseComposeWithSecretsAndConfigs() throws {
- let yaml = """
- version: '3.8'
-
- services:
- app:
- image: myapp:latest
- configs:
- - source: app_config
- target: /etc/app/config.yml
- secrets:
- - db_password
-
- configs:
- app_config:
- external: true
-
- secrets:
- db_password:
- external: true
- """
+ let yaml = DockerComposeParsingTests.dockerComposeYaml5
let decoder = YAMLDecoder()
let compose = try decoder.decode(DockerCompose.self, from: yaml)
@@ -236,29 +133,7 @@ struct IntegrationTests {
@Test("Parse compose with healthchecks and restart policies")
func parseComposeWithHealthchecksAndRestart() throws {
- let yaml = """
- 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
- """
+ let yaml = DockerComposeParsingTests.dockerComposeYaml6
let decoder = YAMLDecoder()
let compose = try decoder.decode(DockerCompose.self, from: yaml)
@@ -270,36 +145,13 @@ struct IntegrationTests {
@Test("Parse compose with complex dependency chain")
func parseComplexDependencyChain() async throws {
- let yaml = """
- 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
- """
- let tempLocation = URL.temporaryDirectory.appending(path: "Container-Compose_Tests/docker-compose.yaml")
- try? FileManager.default.createDirectory(at: tempLocation.deletingLastPathComponent(), withIntermediateDirectories: true)
- try yaml.write(to: tempLocation, atomically: false, encoding: .utf8)
+ let yaml = DockerComposeParsingTests.dockerComposeYaml7
- var composeUp = try ComposeUp.parse(["-d", "--cwd", tempLocation.deletingLastPathComponent().path(percentEncoded: false)])
- try await composeUp.run()
-
- let containers = try await ClientContainer.list()
- print(containers)
+ #expect(false)
+ }
+
+ enum Errors: Error {
+ case containerNotFound
}
}
From 62a74979b9780d54c353522e01dcd5baff9039c5 Mon Sep 17 00:00:00 2001
From: Morris Richman <81453549+Mcrich23@users.noreply.github.com>
Date: Thu, 23 Oct 2025 15:18:48 -0700
Subject: [PATCH 05/13] network create fixes
---
.../Commands/ComposeUp.swift | 21 +++++++++----------
1 file changed, 10 insertions(+), 11 deletions(-)
diff --git a/Sources/Container-Compose/Commands/ComposeUp.swift b/Sources/Container-Compose/Commands/ComposeUp.swift
index 93f77c4..f0853d4 100644
--- a/Sources/Container-Compose/Commands/ComposeUp.swift
+++ b/Sources/Container-Compose/Commands/ComposeUp.swift
@@ -144,7 +144,6 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable {
if let networks = dockerCompose.networks {
print("\n--- Processing Networks ---")
for (networkName, networkConfig) in networks {
- guard let networkConfig else { continue }
try await setupNetwork(name: networkName, config: networkConfig)
}
print("--- Networks Processed ---\n")
@@ -266,10 +265,10 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable {
try? fileManager.createDirectory(atPath: volumePath, withIntermediateDirectories: true)
}
- private func setupNetwork(name networkName: String, config networkConfig: Network) async throws {
- let actualNetworkName = networkConfig.name ?? networkName // Use explicit name or key as name
+ private func setupNetwork(name networkName: String, config networkConfig: Network?) async throws {
+ let actualNetworkName = networkConfig?.name ?? networkName // Use explicit name or key as name
- if let externalNetwork = networkConfig.external, externalNetwork.isExternal {
+ if let externalNetwork = networkConfig?.external, externalNetwork.isExternal {
print("Info: Network '\(networkName)' is declared as external.")
print("This tool assumes external network '\(externalNetwork.name ?? actualNetworkName)' already exists and will not attempt to create it.")
} else {
@@ -277,12 +276,12 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable {
#warning("Docker Compose Network Options Not Supported")
// Add driver and driver options
- if let driver = networkConfig.driver, !driver.isEmpty {
+ if let driver = networkConfig?.driver, !driver.isEmpty {
// networkCreateArgs.append("--driver")
// networkCreateArgs.append(driver)
print("Network Driver Detected, But Not Supported")
}
- if let driverOpts = networkConfig.driver_opts, !driverOpts.isEmpty {
+ if let driverOpts = networkConfig?.driver_opts, !driverOpts.isEmpty {
// for (optKey, optValue) in driverOpts {
// networkCreateArgs.append("--opt")
// networkCreateArgs.append("\(optKey)=\(optValue)")
@@ -290,21 +289,21 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable {
print("Network Options Detected, But Not Supported")
}
// Add various network flags
- if networkConfig.attachable == true {
+ if networkConfig?.attachable == true {
// networkCreateArgs.append("--attachable")
print("Network Attachable Flag Detected, But Not Supported")
}
- if networkConfig.enable_ipv6 == true {
+ if networkConfig?.enable_ipv6 == true {
// networkCreateArgs.append("--ipv6")
print("Network IPv6 Flag Detected, But Not Supported")
}
- if networkConfig.isInternal == true {
+ if networkConfig?.isInternal == true {
// networkCreateArgs.append("--internal")
print("Network Internal Flag Detected, But Not Supported")
} // CORRECTED: Use isInternal
// Add labels
- if let labels = networkConfig.labels, !labels.isEmpty {
+ if let labels = networkConfig?.labels, !labels.isEmpty {
print("Network Labels Detected, But Not Supported")
// for (labelKey, labelValue) in labels {
// networkCreateArgs.append("--label")
@@ -451,7 +450,7 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable {
runCommandArgs.append(networkToConnect)
}
print(
- "Info: Service '\(serviceName)' is configured to connect to networks: \(serviceNetworks.joined(separator: ", ")) ascertained from networks attribute in docker-compose.yml."
+ "Info: Service '\(serviceName)' is configured to connect to networks: \(serviceNetworks.joined(separator: ", ")) ascertained from networks attribute in \(composeFilename)."
)
print(
"Note: This tool assumes custom networks are defined at the top-level 'networks' key or are pre-existing. This tool does not create implicit networks for services if not explicitly defined at the top-level."
From 5d0f94e2f1776b85963a64262cb6a23603c65539 Mon Sep 17 00:00:00 2001
From: Morris Richman <81453549+Mcrich23@users.noreply.github.com>
Date: Thu, 23 Oct 2025 15:37:06 -0700
Subject: [PATCH 06/13] Update IntegrationTests.swift
---
Tests/Container-ComposeTests/IntegrationTests.swift | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Tests/Container-ComposeTests/IntegrationTests.swift b/Tests/Container-ComposeTests/IntegrationTests.swift
index 3678816..91d64f9 100644
--- a/Tests/Container-ComposeTests/IntegrationTests.swift
+++ b/Tests/Container-ComposeTests/IntegrationTests.swift
@@ -158,7 +158,7 @@ struct IntegrationTests {
struct ContainerDependentTrait: TestScoping, TestTrait, SuiteTrait {
func provideScope(for test: Test, testCase: Test.Case?, performing function: () async throws -> Void) async throws {
// Start Server
- try await Application.SystemStart.parse([]).run()
+ try await Application.SystemStart.parse(["--enable-kernel-install"]).run()
// Run Test
try await function()
From e4757b545e5eb2247c5b7cafaad9a131a48c5237 Mon Sep 17 00:00:00 2001
From: Morris Richman <81453549+Mcrich23@users.noreply.github.com>
Date: Thu, 23 Oct 2025 16:24:35 -0700
Subject: [PATCH 07/13] update integration tests
---
.../DockerComposeParsingTests.swift | 27 +++
.../IntegrationTests.swift | 203 +++++++++++++-----
2 files changed, 174 insertions(+), 56 deletions(-)
diff --git a/Tests/Container-ComposeTests/DockerComposeParsingTests.swift b/Tests/Container-ComposeTests/DockerComposeParsingTests.swift
index 1820b7f..899db83 100644
--- a/Tests/Container-ComposeTests/DockerComposeParsingTests.swift
+++ b/Tests/Container-ComposeTests/DockerComposeParsingTests.swift
@@ -700,4 +700,31 @@ extension DockerComposeParsingTests {
db:
image: postgres:14
"""
+
+ static let dockerComposeYaml8 = """
+ version: '3.8'
+
+ services:
+ web:
+ image: nginx:alpine
+ ports:
+ - "8082:80"
+ depends_on:
+ - app
+
+ app:
+ image: python:3.12-alpine
+ depends_on:
+ - db
+ command: python -m http.server 8000
+ environment:
+ DATABASE_URL: postgres://postgres:postgres@db:5432/appdb
+
+ db:
+ image: postgres:14
+ environment:
+ POSTGRES_DB: appdb
+ POSTGRES_USER: postgres
+ POSTGRES_PASSWORD: postgres
+ """
}
diff --git a/Tests/Container-ComposeTests/IntegrationTests.swift b/Tests/Container-ComposeTests/IntegrationTests.swift
index 91d64f9..031dc74 100644
--- a/Tests/Container-ComposeTests/IntegrationTests.swift
+++ b/Tests/Container-ComposeTests/IntegrationTests.swift
@@ -24,8 +24,8 @@ import ContainerClient
@Suite("Compose Up Tests - Real-World Compose Files", .containerDependent)
struct IntegrationTests {
- @Test("Parse WordPress with MySQL compose file")
- func parseWordPressCompose() async throws {
+ @Test("Test WordPress with MySQL compose file")
+ func testWordPressCompose() async throws {
let yaml = DockerComposeParsingTests.dockerComposeYaml1
let tempLocation = URL.temporaryDirectory.appending(path: "Container-Compose_Tests_\(UUID().uuidString)/docker-compose.yaml")
@@ -56,8 +56,7 @@ struct IntegrationTests {
#expect(wordpressContainer.configuration.image.reference == "docker.io/library/wordpress:latest")
// Check Environment
- let wpEnvArray = wordpressContainer.configuration.initProcess.environment.map({ (String($0.split(separator: "=")[0]), String($0.split(separator: "=")[1])) })
- let wpEnv = Dictionary(uniqueKeysWithValues: wpEnvArray)
+ let wpEnv = parseEnvToDict(wordpressContainer.configuration.initProcess.environment)
#expect(wpEnv["WORDPRESS_DB_HOST"] == String(dbContainer.networks.first!.address.split(separator: "/")[0]))
#expect(wpEnv["WORDPRESS_DB_USER"] == "wordpress")
#expect(wpEnv["WORDPRESS_DB_PASSWORD"] == "wordpress")
@@ -72,8 +71,7 @@ struct IntegrationTests {
#expect(dbContainer.configuration.image.reference == "docker.io/library/mysql:8.0")
// Check Environment
- let dbEnvArray = dbContainer.configuration.initProcess.environment.map({ (String($0.split(separator: "=")[0]), String($0.split(separator: "=")[1])) })
- let dbEnv = Dictionary(uniqueKeysWithValues: dbEnvArray)
+ let dbEnv = parseEnvToDict(dbContainer.configuration.initProcess.environment)
#expect(dbEnv["MYSQL_ROOT_PASSWORD"] == "rootpassword")
#expect(dbEnv["MYSQL_DATABASE"] == "wordpress")
#expect(dbEnv["MYSQL_USER"] == "wordpress")
@@ -84,75 +82,168 @@ struct IntegrationTests {
print("")
}
- @Test("Parse three-tier web application")
- func parseThreeTierApp() throws {
+ @Test("Test three-tier web application with multiple networks")
+ func testThreeTierWebAppWithNetworks() async throws {
let yaml = DockerComposeParsingTests.dockerComposeYaml2
- let decoder = YAMLDecoder()
- let compose = try decoder.decode(DockerCompose.self, from: yaml)
+ let tempLocation = URL.temporaryDirectory.appending(path: "Container-Compose_Tests_\(UUID().uuidString)/docker-compose.yaml")
+ try? FileManager.default.createDirectory(at: tempLocation.deletingLastPathComponent(), withIntermediateDirectories: true)
+ try yaml.write(to: tempLocation, atomically: false, encoding: .utf8)
+ let folderName = tempLocation.deletingLastPathComponent().lastPathComponent
- #expect(compose.name == "webapp")
- #expect(compose.services.count == 4)
- #expect(compose.networks?.count == 2)
- #expect(compose.volumes?.count == 1)
- }
-
- @Test("Parse microservices architecture")
- func parseMicroservicesCompose() throws {
- let yaml = DockerComposeParsingTests.dockerComposeYaml3
+ var composeUp = try ComposeUp.parse(["-d", "--cwd", tempLocation.deletingLastPathComponent().path(percentEncoded: false)])
+ try await composeUp.run()
- let decoder = YAMLDecoder()
- let compose = try decoder.decode(DockerCompose.self, from: yaml)
+ // Get the containers created by this compose file
+ let containers = try await ClientContainer.list()
+ .filter({
+ $0.configuration.id.contains(folderName)
+ })
- #expect(compose.services.count == 5)
- #expect(compose.services["api-gateway"]??.depends_on?.count == 3)
- }
-
- @Test("Parse development environment with build")
- func parseDevelopmentEnvironment() throws {
- let yaml = DockerComposeParsingTests.dockerComposeYaml4
+ guard let nginxContainer = containers.first(where: { $0.configuration.id == "\(folderName)-nginx" }),
+ let appContainer = containers.first(where: { $0.configuration.id == "\(folderName)-app" }),
+ let dbContainer = containers.first(where: { $0.configuration.id == "\(folderName)-db" }),
+ let redisContainer = containers.first(where: { $0.configuration.id == "\(folderName)-redis" })
+ else {
+ throw Errors.containerNotFound
+ }
- let decoder = YAMLDecoder()
- let compose = try decoder.decode(DockerCompose.self, from: yaml)
+ // --- NGINX Container ---
+ #expect(nginxContainer.configuration.image.reference == "docker.io/library/nginx:alpine")
+ #expect(nginxContainer.configuration.publishedPorts.map({ "\($0.hostAddress):\($0.hostPort):\($0.containerPort)" }) == ["0.0.0.0:80:80"])
+ #expect(nginxContainer.networks.map(\.hostname).contains("frontend"))
- #expect(compose.services["app"]??.build != nil)
- #expect(compose.services["app"]??.build?.context == ".")
- #expect(compose.services["app"]??.volumes?.count == 2)
- }
-
- @Test("Parse compose with secrets and configs")
- func parseComposeWithSecretsAndConfigs() throws {
- let yaml = DockerComposeParsingTests.dockerComposeYaml5
+ // --- APP Container ---
+ #expect(appContainer.configuration.image.reference == "docker.io/library/node:18-alpine")
- let decoder = YAMLDecoder()
- let compose = try decoder.decode(DockerCompose.self, from: yaml)
+ let appEnv = parseEnvToDict(appContainer.configuration.initProcess.environment)
+ #expect(appEnv["NODE_ENV"] == "production")
+ #expect(appEnv["DATABASE_URL"] == "postgres://\(dbContainer.networks.first!.address.split(separator: "/")[0]):5432/myapp")
- #expect(compose.configs != nil)
- #expect(compose.secrets != nil)
- }
-
- @Test("Parse compose with healthchecks and restart policies")
- func parseComposeWithHealthchecksAndRestart() throws {
- let yaml = DockerComposeParsingTests.dockerComposeYaml6
+ #expect(appContainer.networks.map(\.hostname).sorted() == ["backend", "frontend"])
+
+ // --- DB Container ---
+ #expect(dbContainer.configuration.image.reference == "docker.io/library/postgres:14-alpine")
+ let dbEnv = parseEnvToDict(dbContainer.configuration.initProcess.environment)
+ #expect(dbEnv["POSTGRES_DB"] == "myapp")
+ #expect(dbEnv["POSTGRES_USER"] == "user")
+ #expect(dbEnv["POSTGRES_PASSWORD"] == "password")
- let decoder = YAMLDecoder()
- let compose = try decoder.decode(DockerCompose.self, from: yaml)
+ // Verify volume mount
+ #expect(dbContainer.configuration.mounts.map(\.destination) == ["/var/lib/postgresql/"])
+ #expect(dbContainer.networks.map(\.hostname) == ["backend"])
- #expect(compose.services["web"]??.restart == "unless-stopped")
- #expect(compose.services["web"]??.healthcheck != nil)
- #expect(compose.services["db"]??.restart == "always")
+ // --- Redis Container ---
+ #expect(redisContainer.configuration.image.reference == "docker.io/library/redis:alpine")
+ #expect(redisContainer.networks.map(\.hostname) == ["backend"])
}
- @Test("Parse compose with complex dependency chain")
- func parseComplexDependencyChain() async throws {
- let yaml = DockerComposeParsingTests.dockerComposeYaml7
+// @Test("Parse development environment with build")
+// func parseDevelopmentEnvironment() throws {
+// let yaml = DockerComposeParsingTests.dockerComposeYaml4
+//
+// let decoder = YAMLDecoder()
+// let compose = try decoder.decode(DockerCompose.self, from: yaml)
+//
+// #expect(compose.services["app"]??.build != nil)
+// #expect(compose.services["app"]??.build?.context == ".")
+// #expect(compose.services["app"]??.volumes?.count == 2)
+// }
+
+// @Test("Parse compose with secrets and configs")
+// func parseComposeWithSecretsAndConfigs() throws {
+// let yaml = DockerComposeParsingTests.dockerComposeYaml5
+//
+// let decoder = YAMLDecoder()
+// let compose = try decoder.decode(DockerCompose.self, from: yaml)
+//
+// #expect(compose.configs != nil)
+// #expect(compose.secrets != nil)
+// }
+
+// @Test("Parse compose with healthchecks and restart policies")
+// func parseComposeWithHealthchecksAndRestart() async throws {
+// let yaml = DockerComposeParsingTests.dockerComposeYaml6
+//
+// let tempLocation = URL.temporaryDirectory.appending(path: "Container-Compose_Tests_\(UUID().uuidString)/docker-compose.yaml")
+// try? FileManager.default.createDirectory(at: tempLocation.deletingLastPathComponent(), withIntermediateDirectories: true)
+// try yaml.write(to: tempLocation, atomically: false, encoding: .utf8)
+// let folderName = tempLocation.deletingLastPathComponent().lastPathComponent
+//
+// var composeUp = try ComposeUp.parse(["-d", "--cwd", tempLocation.deletingLastPathComponent().path(percentEncoded: false)])
+// try await composeUp.run()
+//
+// // Get the containers created by this compose file
+// let containers = try await ClientContainer.list()
+// .filter({
+// $0.configuration.id.contains(folderName)
+// })
+// dump(containers)
+// }
+
+ @Test("Test compose with complex dependency chain")
+ func TestComplexDependencyChain() async throws {
+ let yaml = DockerComposeParsingTests.dockerComposeYaml8
+
+ let tempLocation = URL.temporaryDirectory.appending(path: "Container-Compose_Tests_\(UUID().uuidString)/docker-compose.yaml")
+ try? FileManager.default.createDirectory(at: tempLocation.deletingLastPathComponent(), withIntermediateDirectories: true)
+ try yaml.write(to: tempLocation, atomically: false, encoding: .utf8)
+ let folderName = tempLocation.deletingLastPathComponent().lastPathComponent
+
+ var composeUp = try ComposeUp.parse(["-d", "--cwd", tempLocation.deletingLastPathComponent().path(percentEncoded: false)])
+ try await composeUp.run()
+
+ // Get the containers created by this compose file
+ let containers = try await ClientContainer.list()
+ .filter {
+ $0.configuration.id.contains(folderName)
+ }
- #expect(false)
+ guard let webContainer = containers.first(where: { $0.configuration.id == "\(folderName)-web" }),
+ let appContainer = containers.first(where: { $0.configuration.id == "\(folderName)-app" }),
+ let dbContainer = containers.first(where: { $0.configuration.id == "\(folderName)-db" })
+ else {
+ throw Errors.containerNotFound
+ }
+
+ // --- WEB Container ---
+ #expect(webContainer.configuration.image.reference == "docker.io/library/nginx:alpine")
+ #expect(webContainer.configuration.publishedPorts.map { "\($0.hostAddress):\($0.hostPort):\($0.containerPort)" } == ["0.0.0.0:8082:80"])
+
+ // --- APP Container ---
+ #expect(appContainer.configuration.image.reference == "docker.io/library/python:3.12-alpine")
+ let appEnv = parseEnvToDict(appContainer.configuration.initProcess.environment)
+ #expect(appEnv["DATABASE_URL"] == "postgres://postgres:postgres@\(dbContainer.networks.first!.address.split(separator: "/")[0]):5432/appdb")
+ #expect(appContainer.configuration.initProcess.executable == "python -m http.server 8000")
+ #expect(appContainer.configuration.platform.architecture == "arm64")
+ #expect(appContainer.configuration.platform.os == "linux")
+
+ // --- DB Container ---
+ #expect(dbContainer.configuration.image.reference == "docker.io/library/postgres:14")
+ let dbEnv = parseEnvToDict(dbContainer.configuration.initProcess.environment)
+ #expect(dbEnv["POSTGRES_DB"] == "appdb")
+ #expect(dbEnv["POSTGRES_USER"] == "postgres")
+ #expect(dbEnv["POSTGRES_PASSWORD"] == "postgres")
+
+ // --- Dependency Verification ---
+ // The dependency chain should reflect: web → app → db
+ // i.e., app depends on db, web depends on app
+ // We can verify indirectly by container states and environment linkage.
+ // App isn't set to run long term
+ #expect(webContainer.status == .running)
+ #expect(dbContainer.status == .running)
}
enum Errors: Error {
case containerNotFound
}
+
+ private func parseEnvToDict(_ envArray: [String]) -> [String: String] {
+ let array = envArray.map({ (String($0.split(separator: "=")[0].split(separator: ":")[0]), String($0.split(separator: "=")[1].split(separator: ":")[1])) })
+ let dict = Dictionary(uniqueKeysWithValues: array)
+
+ return dict
+ }
}
struct ContainerDependentTrait: TestScoping, TestTrait, SuiteTrait {
From 8e139e900631748a540398b430e57bf91a0d176b Mon Sep 17 00:00:00 2001
From: Morris Richman <81453549+Mcrich23@users.noreply.github.com>
Date: Thu, 23 Oct 2025 16:48:04 -0700
Subject: [PATCH 08/13] Update IntegrationTests.swift
---
Tests/Container-ComposeTests/IntegrationTests.swift | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/Tests/Container-ComposeTests/IntegrationTests.swift b/Tests/Container-ComposeTests/IntegrationTests.swift
index 031dc74..b55c181 100644
--- a/Tests/Container-ComposeTests/IntegrationTests.swift
+++ b/Tests/Container-ComposeTests/IntegrationTests.swift
@@ -213,7 +213,7 @@ struct IntegrationTests {
// --- APP Container ---
#expect(appContainer.configuration.image.reference == "docker.io/library/python:3.12-alpine")
let appEnv = parseEnvToDict(appContainer.configuration.initProcess.environment)
- #expect(appEnv["DATABASE_URL"] == "postgres://postgres:postgres@\(dbContainer.networks.first!.address.split(separator: "/")[0]):5432/appdb")
+ #expect(appEnv["DATABASE_URL"] == "postgres://postgres:postgres@db:5432/appdb")
#expect(appContainer.configuration.initProcess.executable == "python -m http.server 8000")
#expect(appContainer.configuration.platform.architecture == "arm64")
#expect(appContainer.configuration.platform.os == "linux")
@@ -239,7 +239,7 @@ struct IntegrationTests {
}
private func parseEnvToDict(_ envArray: [String]) -> [String: String] {
- let array = envArray.map({ (String($0.split(separator: "=")[0].split(separator: ":")[0]), String($0.split(separator: "=")[1].split(separator: ":")[1])) })
+ let array = envArray.map({ (String($0.split(separator: "=")[0]), String($0.split(separator: "=")[1])) })
let dict = Dictionary(uniqueKeysWithValues: array)
return dict
From 52a243b097d807d722369b1106ced9471a9d1001 Mon Sep 17 00:00:00 2001
From: Morris Richman <81453549+Mcrich23@users.noreply.github.com>
Date: Thu, 23 Oct 2025 16:52:52 -0700
Subject: [PATCH 09/13] typo fix
---
Tests/Container-ComposeTests/DockerComposeParsingTests.swift | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Tests/Container-ComposeTests/DockerComposeParsingTests.swift b/Tests/Container-ComposeTests/DockerComposeParsingTests.swift
index 899db83..270d0a6 100644
--- a/Tests/Container-ComposeTests/DockerComposeParsingTests.swift
+++ b/Tests/Container-ComposeTests/DockerComposeParsingTests.swift
@@ -469,7 +469,7 @@ struct DockerComposeParsingTests {
@Test("Parse compose with complex dependency chain")
func parseComplexDependencyChain() throws {
- let yaml = Self.dockerComposeYaml6
+ let yaml = Self.dockerComposeYaml7
let decoder = YAMLDecoder()
let compose = try decoder.decode(DockerCompose.self, from: yaml)
From 6f003ab84f5875a9eec3b377b2ee0d0efd167aec Mon Sep 17 00:00:00 2001
From: Morris Richman <81453549+Mcrich23@users.noreply.github.com>
Date: Thu, 23 Oct 2025 16:54:54 -0700
Subject: [PATCH 10/13] disable test due to networking bug
---
.../IntegrationTests.swift | 110 +++++++++---------
1 file changed, 55 insertions(+), 55 deletions(-)
diff --git a/Tests/Container-ComposeTests/IntegrationTests.swift b/Tests/Container-ComposeTests/IntegrationTests.swift
index b55c181..620544b 100644
--- a/Tests/Container-ComposeTests/IntegrationTests.swift
+++ b/Tests/Container-ComposeTests/IntegrationTests.swift
@@ -82,61 +82,61 @@ struct IntegrationTests {
print("")
}
- @Test("Test three-tier web application with multiple networks")
- func testThreeTierWebAppWithNetworks() async throws {
- let yaml = DockerComposeParsingTests.dockerComposeYaml2
-
- let tempLocation = URL.temporaryDirectory.appending(path: "Container-Compose_Tests_\(UUID().uuidString)/docker-compose.yaml")
- try? FileManager.default.createDirectory(at: tempLocation.deletingLastPathComponent(), withIntermediateDirectories: true)
- try yaml.write(to: tempLocation, atomically: false, encoding: .utf8)
- let folderName = tempLocation.deletingLastPathComponent().lastPathComponent
-
- var composeUp = try ComposeUp.parse(["-d", "--cwd", tempLocation.deletingLastPathComponent().path(percentEncoded: false)])
- try await composeUp.run()
-
- // Get the containers created by this compose file
- let containers = try await ClientContainer.list()
- .filter({
- $0.configuration.id.contains(folderName)
- })
-
- guard let nginxContainer = containers.first(where: { $0.configuration.id == "\(folderName)-nginx" }),
- let appContainer = containers.first(where: { $0.configuration.id == "\(folderName)-app" }),
- let dbContainer = containers.first(where: { $0.configuration.id == "\(folderName)-db" }),
- let redisContainer = containers.first(where: { $0.configuration.id == "\(folderName)-redis" })
- else {
- throw Errors.containerNotFound
- }
-
- // --- NGINX Container ---
- #expect(nginxContainer.configuration.image.reference == "docker.io/library/nginx:alpine")
- #expect(nginxContainer.configuration.publishedPorts.map({ "\($0.hostAddress):\($0.hostPort):\($0.containerPort)" }) == ["0.0.0.0:80:80"])
- #expect(nginxContainer.networks.map(\.hostname).contains("frontend"))
-
- // --- APP Container ---
- #expect(appContainer.configuration.image.reference == "docker.io/library/node:18-alpine")
-
- let appEnv = parseEnvToDict(appContainer.configuration.initProcess.environment)
- #expect(appEnv["NODE_ENV"] == "production")
- #expect(appEnv["DATABASE_URL"] == "postgres://\(dbContainer.networks.first!.address.split(separator: "/")[0]):5432/myapp")
-
- #expect(appContainer.networks.map(\.hostname).sorted() == ["backend", "frontend"])
-
- // --- DB Container ---
- #expect(dbContainer.configuration.image.reference == "docker.io/library/postgres:14-alpine")
- let dbEnv = parseEnvToDict(dbContainer.configuration.initProcess.environment)
- #expect(dbEnv["POSTGRES_DB"] == "myapp")
- #expect(dbEnv["POSTGRES_USER"] == "user")
- #expect(dbEnv["POSTGRES_PASSWORD"] == "password")
-
- // Verify volume mount
- #expect(dbContainer.configuration.mounts.map(\.destination) == ["/var/lib/postgresql/"])
- #expect(dbContainer.networks.map(\.hostname) == ["backend"])
-
- // --- Redis Container ---
- #expect(redisContainer.configuration.image.reference == "docker.io/library/redis:alpine")
- #expect(redisContainer.networks.map(\.hostname) == ["backend"])
- }
+// @Test("Test three-tier web application with multiple networks")
+// func testThreeTierWebAppWithNetworks() async throws {
+// let yaml = DockerComposeParsingTests.dockerComposeYaml2
+//
+// let tempLocation = URL.temporaryDirectory.appending(path: "Container-Compose_Tests_\(UUID().uuidString)/docker-compose.yaml")
+// try? FileManager.default.createDirectory(at: tempLocation.deletingLastPathComponent(), withIntermediateDirectories: true)
+// try yaml.write(to: tempLocation, atomically: false, encoding: .utf8)
+// let folderName = tempLocation.deletingLastPathComponent().lastPathComponent
+//
+// var composeUp = try ComposeUp.parse(["-d", "--cwd", tempLocation.deletingLastPathComponent().path(percentEncoded: false)])
+// try await composeUp.run()
+//
+// // Get the containers created by this compose file
+// let containers = try await ClientContainer.list()
+// .filter({
+// $0.configuration.id.contains(folderName)
+// })
+//
+// guard let nginxContainer = containers.first(where: { $0.configuration.id == "\(folderName)-nginx" }),
+// let appContainer = containers.first(where: { $0.configuration.id == "\(folderName)-app" }),
+// let dbContainer = containers.first(where: { $0.configuration.id == "\(folderName)-db" }),
+// let redisContainer = containers.first(where: { $0.configuration.id == "\(folderName)-redis" })
+// else {
+// throw Errors.containerNotFound
+// }
+//
+// // --- NGINX Container ---
+// #expect(nginxContainer.configuration.image.reference == "docker.io/library/nginx:alpine")
+// #expect(nginxContainer.configuration.publishedPorts.map({ "\($0.hostAddress):\($0.hostPort):\($0.containerPort)" }) == ["0.0.0.0:80:80"])
+// #expect(nginxContainer.networks.map(\.hostname).contains("frontend"))
+//
+// // --- APP Container ---
+// #expect(appContainer.configuration.image.reference == "docker.io/library/node:18-alpine")
+//
+// let appEnv = parseEnvToDict(appContainer.configuration.initProcess.environment)
+// #expect(appEnv["NODE_ENV"] == "production")
+// #expect(appEnv["DATABASE_URL"] == "postgres://\(dbContainer.networks.first!.address.split(separator: "/")[0]):5432/myapp")
+//
+// #expect(appContainer.networks.map(\.hostname).sorted() == ["backend", "frontend"])
+//
+// // --- DB Container ---
+// #expect(dbContainer.configuration.image.reference == "docker.io/library/postgres:14-alpine")
+// let dbEnv = parseEnvToDict(dbContainer.configuration.initProcess.environment)
+// #expect(dbEnv["POSTGRES_DB"] == "myapp")
+// #expect(dbEnv["POSTGRES_USER"] == "user")
+// #expect(dbEnv["POSTGRES_PASSWORD"] == "password")
+//
+// // Verify volume mount
+// #expect(dbContainer.configuration.mounts.map(\.destination) == ["/var/lib/postgresql/"])
+// #expect(dbContainer.networks.map(\.hostname) == ["backend"])
+//
+// // --- Redis Container ---
+// #expect(redisContainer.configuration.image.reference == "docker.io/library/redis:alpine")
+// #expect(redisContainer.networks.map(\.hostname) == ["backend"])
+// }
// @Test("Parse development environment with build")
// func parseDevelopmentEnvironment() throws {
From 727e2ff4cdb139e6f87df97e7904bcf2f136a2dc Mon Sep 17 00:00:00 2001
From: Morris Richman <81453549+Mcrich23@users.noreply.github.com>
Date: Thu, 23 Oct 2025 16:55:03 -0700
Subject: [PATCH 11/13] Update IntegrationTests.swift
---
Tests/Container-ComposeTests/IntegrationTests.swift | 1 +
1 file changed, 1 insertion(+)
diff --git a/Tests/Container-ComposeTests/IntegrationTests.swift b/Tests/Container-ComposeTests/IntegrationTests.swift
index 620544b..39f2e57 100644
--- a/Tests/Container-ComposeTests/IntegrationTests.swift
+++ b/Tests/Container-ComposeTests/IntegrationTests.swift
@@ -82,6 +82,7 @@ struct IntegrationTests {
print("")
}
+ // TODO: Reenable
// @Test("Test three-tier web application with multiple networks")
// func testThreeTierWebAppWithNetworks() async throws {
// let yaml = DockerComposeParsingTests.dockerComposeYaml2
From 2f4622cc55618127a01af53d6f0930bafcdd80e6 Mon Sep 17 00:00:00 2001
From: Morris Richman <81453549+Mcrich23@users.noreply.github.com>
Date: Fri, 24 Oct 2025 17:39:42 -0700
Subject: [PATCH 12/13] split compose up tests from the rest of the testing
suite for github action reasons
---
.../xcschemes/Container-Compose.xcscheme | 20 ++
Package.swift | 17 +-
.../ComposeUpTests.swift} | 16 +-
.../BuildConfigurationTests.swift | 0
.../DockerComposeParsingTests.swift | 249 +-----------------
.../EnvFileLoadingTests.swift | 0
.../EnvironmentVariableTests.swift | 0
.../HealthcheckConfigurationTests.swift | 0
.../NetworkConfigurationTests.swift | 0
.../ServiceDependencyTests.swift | 0
Tests/Container-ComposeTests/README.md | 204 --------------
.../TestHelpers/DockerComposeYamlFiles.swift | 249 ++++++++++++++++++
12 files changed, 300 insertions(+), 455 deletions(-)
rename Tests/{Container-ComposeTests/IntegrationTests.swift => Container-Compose-DynamicTests/ComposeUpTests.swift} (96%)
rename Tests/{Container-ComposeTests => Container-Compose-StaticTests}/BuildConfigurationTests.swift (100%)
rename Tests/{Container-ComposeTests => Container-Compose-StaticTests}/DockerComposeParsingTests.swift (72%)
rename Tests/{Container-ComposeTests => Container-Compose-StaticTests}/EnvFileLoadingTests.swift (100%)
rename Tests/{Container-ComposeTests => Container-Compose-StaticTests}/EnvironmentVariableTests.swift (100%)
rename Tests/{Container-ComposeTests => Container-Compose-StaticTests}/HealthcheckConfigurationTests.swift (100%)
rename Tests/{Container-ComposeTests => Container-Compose-StaticTests}/NetworkConfigurationTests.swift (100%)
rename Tests/{Container-ComposeTests => Container-Compose-StaticTests}/ServiceDependencyTests.swift (100%)
delete mode 100644 Tests/Container-ComposeTests/README.md
create mode 100644 Tests/TestHelpers/DockerComposeYamlFiles.swift
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Container-Compose.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Container-Compose.xcscheme
index 4ded24c..c9d1c50 100644
--- a/.swiftpm/xcode/xcshareddata/xcschemes/Container-Compose.xcscheme
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/Container-Compose.xcscheme
@@ -40,6 +40,26 @@
ReferencedContainer = "container:">
+
+
+
+
+
+
+
+
Date: Fri, 24 Oct 2025 17:41:50 -0700
Subject: [PATCH 13/13] update github actions to only run static tests
---
.github/workflows/tests.yml | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 5ef3c9e..857bedd 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -21,7 +21,7 @@ on:
jobs:
test:
- name: Run Swift Tests
+ name: Run Swift Static Tests
runs-on: macos-26
steps:
@@ -44,10 +44,10 @@ jobs:
- name: Build
run: swift build --build-tests
- - name: Run tests
- run: swift test
-
- - name: Upload test results
+ - name: Run static tests
+ run: swift test --filter Container-Compose-StaticTests.
+
+ - name: Upload static test results
if: always()
uses: actions/upload-artifact@v4
with: