//===----------------------------------------------------------------------===// // Copyright © 2025 Apple Inc. and the container 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 ContainerizationOCI import Foundation import Testing @testable import ContainerBuildIR struct BasicTests { @Test func digestCreation() throws { // Test creating digest from bytes let bytes = Data(repeating: 0xAB, count: 32) let digest = try Digest(algorithm: .sha256, bytes: bytes) #expect(digest.algorithm == .sha256) #expect(digest.bytes == bytes) // Test parsing digest string let digestString = "sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789" let parsed = try Digest(parsing: digestString) #expect(parsed.stringValue == digestString) // Test invalid length #expect(throws: Error.self) { try Digest(algorithm: .sha256, bytes: Data(count: 16)) } } @Test func imageReference() throws { // Test parsing various formats let refs = [ ("ubuntu", "ubuntu:latest"), ("ubuntu:20.04", "ubuntu:20.04"), ("ghcr.io/owner/repo:tag", "ghcr.io/owner/repo:tag"), ("localhost:5000/test", "localhost:5000/test:latest"), ("image@sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", "image@sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"), ] for (input, expected) in refs { guard let ref = ImageReference(parsing: input) else { Issue.record("Failed to parse: \(input)") continue } #expect(ref.stringValue == expected, "Failed for input: \(input)") } // Test creation let ref = try ImageReference( registry: "docker.io", repository: "library/nginx", tag: "alpine" ) #expect(ref.stringValue == "docker.io/library/nginx:alpine") } @Test func simpleGraph() throws { // Create a simple graph guard let imageRef = ImageReference(parsing: "alpine:latest") else { Issue.record("Failed to parse image reference") return } let graph = try GraphBuilder.singleStage( from: imageRef ) { builder in try builder .run("apk add --no-cache curl") .workdir("/app") .copyFromContext(paths: ["main.go"], to: "/app/") .run("go build -o app main.go") .entrypoint(.exec(["/app/app"])) } #expect(graph.stages.count == 1) #expect(graph.stages[0].nodes.count == 5) // Validate the graph let validator = StandardValidator() let result = validator.validate(graph) #expect(result.isValid == true, "Graph validation failed: \(result.errors)") } @Test func multiStageGraph() throws { guard let golangRef = ImageReference(parsing: "golang:1.21"), let alpineRef = ImageReference(parsing: "alpine:latest") else { Issue.record("Failed to parse image references") return } let graph = try GraphBuilder.multiStage { builder in // Build stage try builder .stage(name: "builder", from: golangRef) .workdir("/src") .copyFromContext(paths: ["go.mod", "go.sum", "*.go"], to: "./") .run("go build -o /app") // Runtime stage try builder .stage(from: alpineRef) .run("apk add --no-cache ca-certificates") .copyFromStage(.named("builder"), paths: ["/app"], to: "/usr/local/bin/app") .user(.uid(1000)) .entrypoint(Command.exec(["/usr/local/bin/app"])) } #expect(graph.stages.count == 2) #expect(graph.stages[0].name == "builder") #expect(graph.stages[1].name == nil) // Check stage dependencies let deps = graph.stages[1].stageDependencies() #expect(deps.contains(.named("builder")) == true) } @Test func operationTypes() throws { // Test ExecOperation let execOp = ExecOperation( command: .shell("echo 'Hello, World!'"), environment: Environment([ (key: "FOO", value: .literal("bar")) ]), workingDirectory: "/tmp" ) #expect(execOp.command.displayString == "echo 'Hello, World!'") #expect(execOp.environment.effectiveEnvironment["FOO"] == "bar") // Test FilesystemOperation let fsOp = FilesystemOperation( action: .copy, source: .context(ContextSource(paths: ["file.txt"])), destination: "/app/file.txt", fileMetadata: FileMetadata( ownership: Ownership(user: .numeric(id: 1000), group: .numeric(id: 1000)), permissions: .mode(0o644) ) ) #expect(fsOp.action == .copy) #expect(fsOp.destination == "/app/file.txt") // Test MetadataOperation let metaOp = MetadataOperation( action: .setLabel(key: "version", value: "1.0.0") ) if case .setLabel(let key, let value) = metaOp.action { #expect(key == "version") #expect(value == "1.0.0") } else { Issue.record("Wrong metadata action type") } } @Test func serialization() throws { // Create a graph using the builder guard let imageRef = ImageReference(parsing: "ubuntu:22.04") else { Issue.record("Failed to parse image reference") return } let originalGraph = try GraphBuilder.singleStage( from: imageRef, platform: .linuxAMD64 ) { builder in try builder .run("apt-get update && apt-get install -y curl") .workdir("/app") .copyFromContext(paths: ["package.json", "src/"], to: "/app/") .run("npm install") .env("NODE_ENV", "production") .expose([PortSpec(port: 3000)]) .cmd(Command.exec(["node", "src/index.js"])) } // Serialize to JSON let coder = JSONIRCoder(prettyPrint: true) let data = try coder.encode(originalGraph) // Deserialize let decodedGraph = try coder.decode(data) // Compare #expect(originalGraph.stages.count == decodedGraph.stages.count) #expect(originalGraph.buildArgs == decodedGraph.buildArgs) #expect(originalGraph.targetPlatforms == decodedGraph.targetPlatforms) } @Test func validation() throws { // Create a graph with issues guard let ubuntuRef = ImageReference(parsing: "ubuntu") else { Issue.record("Failed to parse image reference") return } let builder = GraphBuilder() try builder .stage(from: ubuntuRef) .run("apt-get update") // Warning: update without install .copyFromStage(.named("nonexistent"), paths: ["/file"], to: "/") // Error: stage doesn't exist // GraphBuilder should throw validation errors during build #expect(throws: ValidationError.self) { try builder.build() } } @Test func semanticAnalysis() throws { // Create a Python build graph inline let graph = try BuildGraph( stages: [ // Dependencies stage BuildStage( name: "dependencies", base: ImageOperation( source: .registry(ImageReference(parsing: "python:3.11-slim")!), platform: .linuxAMD64 ), nodes: [ BuildNode( operation: MetadataOperation( action: .setWorkdir(path: "/app") ) ), BuildNode( operation: FilesystemOperation( action: .copy, source: .context(ContextSource(paths: ["requirements.txt"])), destination: "/app/" ) ), BuildNode( operation: ExecOperation( command: .shell("pip install --user --no-cache-dir -r requirements.txt") ) ), ] ), // Application stage BuildStage( name: "app", base: ImageOperation( source: .registry(ImageReference(parsing: "python:3.11-slim")!), platform: .linuxAMD64 ), nodes: [ BuildNode( operation: FilesystemOperation( action: .copy, source: .stage(.named("dependencies"), paths: ["/root/.local"]), destination: "/root/.local" ) ), BuildNode( operation: MetadataOperation( action: .setUser(user: .uidGid(uid: 1000, gid: 1000)) ) ), BuildNode( operation: MetadataOperation( action: .setCmd(command: .exec(["python", "main.py"])) ) ), ] ), ], targetPlatforms: [.linuxAMD64, .linuxARM64] ) let analyzer = SemanticAnalyzer() let context = AnalysisContext(reporter: nil, sourceMap: nil) let analyzedGraph = try analyzer.analyze(graph, context: context) // SemanticAnalyzer should return the graph unchanged #expect(analyzedGraph.stages.count == graph.stages.count) #expect(analyzedGraph.buildArgs.count == graph.buildArgs.count) } @Test func graphTraversal() throws { let stage = BuildStage( name: "test", base: ImageOperation(source: .scratch), nodes: [ BuildNode(id: UUID(), operation: MetadataOperation(action: .setWorkdir(path: "/app")), dependencies: []), BuildNode(id: UUID(), operation: ExecOperation(command: .shell("echo test")), dependencies: []), BuildNode(id: UUID(), operation: MetadataOperation(action: .setUser(user: .uid(1000))), dependencies: []), ] ) // Test topological sort let sorted = try GraphTraversal.topologicalSort(stage) #expect(sorted.count == stage.nodes.count) // Test finding roots let roots = GraphTraversal.findRoots(in: stage) #expect(roots.count == 3) // All nodes are roots in this case // Test finding leaves let leaves = GraphTraversal.findLeaves(in: stage) #expect(leaves.count == 3) // All nodes are leaves in this case } }