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: