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:
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:">
+
+
+
+
+
+
+
+
[String: String] {
+ let array = envArray.map({ (String($0.split(separator: "=")[0]), String($0.split(separator: "=")[1])) })
+ let dict = Dictionary(uniqueKeysWithValues: array)
+
+ return dict
+ }
+}
+
+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(["--enable-kernel-install"]).run()
+
+ // Run Test
+ try await function()
+ }
+}
+
+extension Trait where Self == ContainerDependentTrait {
+ static var containerDependent: ContainerDependentTrait { .init() }
+}
diff --git a/Tests/Container-ComposeTests/BuildConfigurationTests.swift b/Tests/Container-Compose-StaticTests/BuildConfigurationTests.swift
similarity index 100%
rename from Tests/Container-ComposeTests/BuildConfigurationTests.swift
rename to Tests/Container-Compose-StaticTests/BuildConfigurationTests.swift
diff --git a/Tests/Container-ComposeTests/DockerComposeParsingTests.swift b/Tests/Container-Compose-StaticTests/DockerComposeParsingTests.swift
similarity index 74%
rename from Tests/Container-ComposeTests/DockerComposeParsingTests.swift
rename to Tests/Container-Compose-StaticTests/DockerComposeParsingTests.swift
index 7e49c9a..2a237b3 100644
--- a/Tests/Container-ComposeTests/DockerComposeParsingTests.swift
+++ b/Tests/Container-Compose-StaticTests/DockerComposeParsingTests.swift
@@ -16,12 +16,13 @@
import Testing
import Foundation
+import TestHelpers
@testable import Yams
@testable import ContainerComposeCore
@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 +393,105 @@ struct DockerComposeParsingTests {
try decoder.decode(DockerCompose.self, from: yaml)
}
}
+
+ // MARK: Full Files
+ @Test("Parse WordPress with MySQL compose file")
+ func parseWordPressCompose() throws {
+ let yaml = DockerComposeYamlFiles.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 = DockerComposeYamlFiles.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 = DockerComposeYamlFiles.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 = DockerComposeYamlFiles.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 = DockerComposeYamlFiles.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 = DockerComposeYamlFiles.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 = DockerComposeYamlFiles.dockerComposeYaml7
+
+ 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/EnvFileLoadingTests.swift b/Tests/Container-Compose-StaticTests/EnvFileLoadingTests.swift
similarity index 99%
rename from Tests/Container-ComposeTests/EnvFileLoadingTests.swift
rename to Tests/Container-Compose-StaticTests/EnvFileLoadingTests.swift
index a206d9b..4252485 100644
--- a/Tests/Container-ComposeTests/EnvFileLoadingTests.swift
+++ b/Tests/Container-Compose-StaticTests/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/EnvironmentVariableTests.swift b/Tests/Container-Compose-StaticTests/EnvironmentVariableTests.swift
similarity index 100%
rename from Tests/Container-ComposeTests/EnvironmentVariableTests.swift
rename to Tests/Container-Compose-StaticTests/EnvironmentVariableTests.swift
diff --git a/Tests/Container-ComposeTests/HealthcheckConfigurationTests.swift b/Tests/Container-Compose-StaticTests/HealthcheckConfigurationTests.swift
similarity index 100%
rename from Tests/Container-ComposeTests/HealthcheckConfigurationTests.swift
rename to Tests/Container-Compose-StaticTests/HealthcheckConfigurationTests.swift
diff --git a/Tests/Container-ComposeTests/NetworkConfigurationTests.swift b/Tests/Container-Compose-StaticTests/NetworkConfigurationTests.swift
similarity index 100%
rename from Tests/Container-ComposeTests/NetworkConfigurationTests.swift
rename to Tests/Container-Compose-StaticTests/NetworkConfigurationTests.swift
diff --git a/Tests/Container-ComposeTests/ServiceDependencyTests.swift b/Tests/Container-Compose-StaticTests/ServiceDependencyTests.swift
similarity index 100%
rename from Tests/Container-ComposeTests/ServiceDependencyTests.swift
rename to Tests/Container-Compose-StaticTests/ServiceDependencyTests.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/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/README.md b/Tests/Container-ComposeTests/README.md
deleted file mode 100644
index 6aed2cf..0000000
--- a/Tests/Container-ComposeTests/README.md
+++ /dev/null
@@ -1,204 +0,0 @@
-# Container-Compose Test Suite
-
-This directory contains a comprehensive test suite for Container-Compose using Swift Testing.
-
-## Test Coverage
-
-The test suite includes **12 test files** with **150+ test cases** covering all major features of Container-Compose:
-
-### 1. DockerComposeParsingTests.swift
-Tests YAML parsing for docker-compose.yml files including:
-- Basic service definitions
-- Project name configuration
-- Multiple services
-- Volumes, networks, configs, and secrets
-- Environment variables
-- Port mappings
-- Service dependencies
-- Build contexts
-- Command configurations (string and array formats)
-- Restart policies
-- Container names and working directories
-- User permissions
-- Privileged mode and read-only filesystems
-- Interactive flags (stdin_open, tty)
-- Hostnames and platform specifications
-- Validation that services must have either image or build
-
-### 2. ServiceDependencyTests.swift
-Tests service dependency resolution and topological sorting:
-- Simple dependency chains
-- Multiple dependencies
-- Complex dependency chains
-- Services with no dependencies
-- Cyclic dependency detection
-- Diamond dependency patterns
-- Single service scenarios
-- Missing dependency handling
-
-### 3. EnvironmentVariableTests.swift
-Tests environment variable resolution:
-- Simple variable substitution
-- Default values (`${VAR:-default}`)
-- Multiple variables in a single string
-- Unresolved variables
-- Empty default values
-- Complex string interpolation
-- Case-sensitive variable names
-- Variables with underscores and numbers
-- Process environment precedence
-
-### 4. EnvFileLoadingTests.swift
-Tests .env file parsing:
-- Simple key-value pairs
-- Comment handling
-- Empty line handling
-- Values with equals signs
-- Empty values
-- Values with spaces
-- Non-existent files
-- Mixed content
-
-### 5. ErrorHandlingTests.swift
-Tests error types and messages:
-- YamlError (compose file not found)
-- ComposeError (image not found, invalid project name)
-- TerminalError (command failed)
-- CommandOutput enum cases
-
-### 6. VolumeConfigurationTests.swift
-Tests volume mounting and configuration:
-- Named volume mounts
-- Bind mounts (absolute and relative paths)
-- Read-only flags
-- Volume identification (bind vs. named)
-- Path with dots prefix
-- Multiple colons in mount specifications
-- Invalid volume formats
-- tmpfs mounts
-- Relative to absolute path resolution
-- Tilde expansion
-- Empty volume definitions
-- Volume driver options
-
-### 7. PortMappingTests.swift
-Tests port mapping configurations:
-- Simple port mappings
-- Port mappings with protocols (TCP/UDP)
-- IP binding
-- Single port (container only)
-- Port ranges
-- IPv6 address binding
-- Multiple port mappings
-- String format parsing
-- Port extraction (host and container)
-- Numeric validation
-- Quoted port strings
-
-### 8. BuildConfigurationTests.swift
-Tests Docker build configurations:
-- Build context
-- Dockerfile specification
-- Build arguments
-- Multi-stage build targets
-- Cache from specifications
-- Build labels
-- Network mode during build
-- Shared memory size
-- Services with build configurations
-- Services with both image and build
-- Path resolution (relative and absolute)
-
-### 9. HealthcheckConfigurationTests.swift
-Tests container healthcheck configurations:
-- Test commands
-- Intervals
-- Timeouts
-- Retry counts
-- Start periods
-- Complete healthcheck configurations
-- CMD-SHELL syntax
-- Disabled healthchecks
-- Services with healthchecks
-
-### 10. NetworkConfigurationTests.swift
-Tests network configurations:
-- Single and multiple networks per service
-- Network drivers
-- Driver options
-- External networks
-- Network labels
-- Multiple networks in compose files
-- Default network behavior
-- Empty network definitions
-
-### 11. ApplicationConfigurationTests.swift
-Tests CLI application structure:
-- Command name verification
-- Version string format
-- Subcommand availability
-- Abstract descriptions
-- Default compose filenames
-- Environment file names
-- Command-line flags (short and long forms)
-- File path resolution
-- Project name extraction
-- Container naming conventions
-
-### 12. IntegrationTests.swift
-Tests real-world compose file scenarios:
-- WordPress with MySQL setup
-- Three-tier web applications
-- Microservices architectures
-- Development environments with build
-- Compose files with secrets and configs
-- Healthchecks and restart policies
-- Complex dependency chains
-
-## Implementation Notes
-
-Due to Container-Compose being an executable target, the test files include their own implementations of the data structures (DockerCompose, Service, Volume, etc.) that mirror the actual implementations. This makes the tests:
-
-1. **Self-contained**: Tests don't depend on the main module being importable
-2. **Documentation**: Serve as examples of the expected structure
-3. **Portable**: Can be run independently once the build issues are resolved
-4. **Comprehensive**: Cover all major parsing and configuration scenarios
-
-## Running Tests
-
-Once the upstream dependency issue with the 'os' module is resolved (requires macOS environment), run:
-
-```bash
-swift test
-```
-
-Or to list all tests:
-
-```bash
-swift test list
-```
-
-Or to run specific test suites:
-
-```bash
-swift test --filter DockerComposeParsingTests
-swift test --filter ServiceDependencyTests
-```
-
-## Test Philosophy
-
-These tests follow the Swift Testing framework conventions and focus on:
-
-- **Feature coverage**: Every documented feature is tested
-- **Edge cases**: Boundary conditions and error cases
-- **Real-world scenarios**: Integration tests with realistic compose files
-- **Clarity**: Test names clearly describe what is being tested
-- **Isolation**: Each test is independent and can run in any order
-
-## Future Enhancements
-
-As Container-Compose evolves, tests should be added for:
-- Additional Docker Compose features as they're implemented
-- Performance tests for large compose files
-- End-to-end integration tests with actual containers (if feasible in test environment)
-- Additional error handling scenarios
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)
- }
-
-}
diff --git a/Tests/Container-ComposeTests/IntegrationTests.swift b/Tests/TestHelpers/DockerComposeYamlFiles.swift
similarity index 58%
rename from Tests/Container-ComposeTests/IntegrationTests.swift
rename to Tests/TestHelpers/DockerComposeYamlFiles.swift
index dbd024b..43e9b12 100644
--- a/Tests/Container-ComposeTests/IntegrationTests.swift
+++ b/Tests/TestHelpers/DockerComposeYamlFiles.swift
@@ -14,17 +14,8 @@
// limitations under the License.
//===----------------------------------------------------------------------===//
-import Testing
-import Foundation
-@testable import Yams
-@testable import ContainerComposeCore
-
-@Suite("Integration Tests - Real-World Compose Files")
-struct IntegrationTests {
-
- @Test("Parse WordPress with MySQL compose file")
- func parseWordPressCompose() throws {
- let yaml = """
+public struct DockerComposeYamlFiles {
+ public static let dockerComposeYaml1 = """
version: '3.8'
services:
@@ -56,20 +47,8 @@ struct IntegrationTests {
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 = """
+ public static let dockerComposeYaml2 = """
version: '3.8'
name: webapp
@@ -119,27 +98,16 @@ struct IntegrationTests {
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 = """
+ public static let dockerComposeYaml3 = """
version: '3.8'
services:
api-gateway:
image: traefik:v2.10
ports:
- - "80:80"
- - "8080:8080"
+ - "81:80"
+ - "8081:8080"
depends_on:
- auth-service
- user-service
@@ -166,17 +134,8 @@ struct IntegrationTests {
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 = """
+ public static let dockerComposeYaml4 = """
version: '3.8'
services:
@@ -193,18 +152,8 @@ struct IntegrationTests {
- "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 = """
+ public static let dockerComposeYaml5 = """
version: '3.8'
services:
@@ -224,17 +173,8 @@ struct IntegrationTests {
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 = """
+ public static let dockerComposeYaml6 = """
version: '3.8'
services:
@@ -257,18 +197,8 @@ struct IntegrationTests {
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 = """
+ public static let dockerComposeYaml7 = """
version: '3.8'
services:
@@ -289,28 +219,31 @@ struct IntegrationTests {
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)
- }
-}
+
+ public 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
+ """
+}