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 + """ +}