Files
2025-08-07 12:17:59 -07:00

316 lines
12 KiB
Swift

//===----------------------------------------------------------------------===//
// 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
}
}