mirror of
https://github.com/apple/container.git
synced 2026-08-24 10:05:43 -05:00
Migrate registry tests to new test support types. (#1845)
- Part of #1833.
This commit is contained in:
@@ -1,70 +0,0 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2026 Apple Inc. and the container project authors.
|
||||
//
|
||||
// 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 Foundation
|
||||
import Testing
|
||||
|
||||
class TestCLIProgressAuto: CLITest {
|
||||
@Test func testAutoProgressFallsBackToPlainWhenPiped() throws {
|
||||
let (_, _, error, status) = try run(arguments: [
|
||||
"image", "pull",
|
||||
"--progress", "auto",
|
||||
alpine,
|
||||
])
|
||||
#expect(status == 0, "image pull should succeed, stderr: \(error)")
|
||||
let lines = error.components(separatedBy: .newlines)
|
||||
.filter { !$0.contains("Warning! Running debug build") && !$0.isEmpty }
|
||||
#expect(!lines.isEmpty, "expected plain progress output on stderr when piped")
|
||||
#expect(!error.contains("\u{1B}["), "expected no ANSI escapes in piped output")
|
||||
}
|
||||
|
||||
@Test func testExplicitPlainProgress() throws {
|
||||
let (_, _, error, status) = try run(arguments: [
|
||||
"image", "pull",
|
||||
"--progress", "plain",
|
||||
alpine,
|
||||
])
|
||||
#expect(status == 0, "image pull --progress plain should succeed, stderr: \(error)")
|
||||
let lines = error.components(separatedBy: .newlines)
|
||||
.filter { !$0.contains("Warning! Running debug build") && !$0.isEmpty }
|
||||
#expect(!lines.isEmpty, "expected plain progress output on stderr")
|
||||
#expect(!error.contains("\u{1B}["), "expected no ANSI escapes with --progress plain")
|
||||
}
|
||||
|
||||
@Test func testExplicitAnsiProgress() throws {
|
||||
let (_, _, error, status) = try run(arguments: [
|
||||
"image", "pull",
|
||||
"--progress", "ansi",
|
||||
alpine,
|
||||
])
|
||||
#expect(status == 0, "image pull --progress ansi should succeed, stderr: \(error)")
|
||||
let lines = error.components(separatedBy: .newlines)
|
||||
.filter { !$0.contains("Warning! Running debug build") && !$0.isEmpty }
|
||||
#expect(!lines.isEmpty, "expected ansi progress output on stderr")
|
||||
}
|
||||
|
||||
@Test func testNoneProgressSuppressesOutput() throws {
|
||||
let (_, _, error, status) = try run(arguments: [
|
||||
"image", "pull",
|
||||
"--progress", "none",
|
||||
alpine,
|
||||
])
|
||||
#expect(status == 0, "image pull --progress none should succeed, stderr: \(error)")
|
||||
let lines = error.components(separatedBy: .newlines)
|
||||
.filter { !$0.contains("Warning! Running debug build") && !$0.isEmpty }
|
||||
#expect(lines.isEmpty, "expected no progress output on stderr with --progress none")
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2026 Apple Inc. and the container project authors.
|
||||
//
|
||||
// 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 Foundation
|
||||
import Testing
|
||||
|
||||
@Suite(.serialSuites)
|
||||
class TestCLIRegistry: CLITest {
|
||||
@Test func testListDefaultFormat() throws {
|
||||
let (_, output, error, status) = try run(arguments: ["registry", "list"])
|
||||
#expect(status == 0, "registry list should succeed, stderr: \(error)")
|
||||
|
||||
let requiredHeaders = ["HOSTNAME", "USERNAME", "MODIFIED", "CREATED"]
|
||||
#expect(
|
||||
requiredHeaders.allSatisfy { output.contains($0) },
|
||||
"output should contain all required headers"
|
||||
)
|
||||
}
|
||||
|
||||
@Test func testListJSONFormat() throws {
|
||||
let (data, _, error, status) = try run(arguments: ["registry", "list", "--format", "json"])
|
||||
#expect(status == 0, "registry list --format json should succeed, stderr: \(error)")
|
||||
|
||||
let json = try JSONSerialization.jsonObject(with: data, options: [])
|
||||
#expect(json is [Any], "JSON output should be an array")
|
||||
}
|
||||
|
||||
@Test func testListQuietMode() throws {
|
||||
let (_, output, error, status) = try run(arguments: ["registry", "list", "-q"])
|
||||
#expect(status == 0, "registry list -q should succeed, stderr: \(error)")
|
||||
|
||||
#expect(!output.contains("HOSTNAME"), "quiet mode should not contain headers")
|
||||
#expect(!output.contains("USERNAME"), "quiet mode should not contain headers")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2026 Apple Inc. and the container project authors.
|
||||
//
|
||||
// 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 Foundation
|
||||
import Testing
|
||||
|
||||
@Suite(.serialized)
|
||||
struct TestCLIBuilderEnvOnlySerial {
|
||||
@Test func testBuildEnvironmentOnlyImageFromScratch() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
try await f.withBuilder { f in
|
||||
let dir = try f.createTempDir()
|
||||
let dockerfile =
|
||||
"""
|
||||
FROM scratch
|
||||
ARG BUILD_DATE
|
||||
ARG VERSION=1.0.0
|
||||
ENV TERM=xterm \\
|
||||
BUILD_DATE=${BUILD_DATE} \\
|
||||
APP_VERSION=${VERSION} \\
|
||||
PATH=/usr/local/bin:/usr/bin:/bin
|
||||
LABEL maintainer="test@example.com" version="${VERSION}"
|
||||
"""
|
||||
try f.createContext(dir: dir, dockerfile: dockerfile)
|
||||
let imageName = "test-env-only:\(UUID().uuidString)"
|
||||
try f.build(tag: imageName, contextDir: dir, buildArgs: ["BUILD_DATE=2025-01-01", "VERSION=2.0.0"])
|
||||
try f.assertImageBuilt(imageName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testBuildEnvironmentOnlyImageFromAlpine() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
try await f.withBuilder { f in
|
||||
let dir = try f.createTempDir()
|
||||
let dockerfile =
|
||||
"""
|
||||
FROM ghcr.io/linuxcontainers/alpine:3.20
|
||||
ENV APP_NAME=myapp APP_VERSION=1.0.0 APP_ENV=production
|
||||
LABEL maintainer="test@example.com" version="1.0.0"
|
||||
"""
|
||||
try f.createContext(dir: dir, dockerfile: dockerfile)
|
||||
let imageName = "test-alpine-env:\(UUID().uuidString)"
|
||||
try f.build(tag: imageName, contextDir: dir)
|
||||
try f.assertImageBuilt(imageName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testMultiStageBuildWithEnvOnlyBase() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
try await f.withBuilder { f in
|
||||
let baseDir = try f.createTempDir()
|
||||
let baseDockerfile =
|
||||
"""
|
||||
FROM scratch
|
||||
ARG JOBS=6
|
||||
ARG ARCH=amd64
|
||||
ENV MAKEOPTS="-j${JOBS}" ARCH="${ARCH}" PATH=/usr/local/bin:/usr/bin
|
||||
"""
|
||||
try f.createContext(dir: baseDir, dockerfile: baseDockerfile)
|
||||
let baseImageName = "test-env-base:\(UUID().uuidString)"
|
||||
try f.build(tag: baseImageName, contextDir: baseDir, buildArgs: ["JOBS=8", "ARCH=arm64"])
|
||||
try f.assertImageBuilt(baseImageName)
|
||||
|
||||
let downstreamDir = try f.createTempDir()
|
||||
let downstreamDockerfile =
|
||||
"""
|
||||
FROM \(baseImageName)
|
||||
LABEL test="env-inherited"
|
||||
"""
|
||||
try f.createContext(dir: downstreamDir, dockerfile: downstreamDockerfile)
|
||||
let downstreamImageName = "test-env-child:\(UUID().uuidString)"
|
||||
try f.build(tag: downstreamImageName, contextDir: downstreamDir)
|
||||
try f.assertImageBuilt(downstreamImageName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testComplexArgAndEnvCombinations() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
try await f.withBuilder { f in
|
||||
let dir = try f.createTempDir()
|
||||
let dockerfile =
|
||||
"""
|
||||
FROM scratch
|
||||
ARG JOBS=6
|
||||
ARG MAXLOAD=7.00
|
||||
ARG ARCH=amd64
|
||||
ARG PROFILE_PATH=23.0/split-usr/no-multilib
|
||||
ARG CHOST=x86_64-pc-linux-gnu
|
||||
ARG CFLAGS=-O2 -pipe
|
||||
ENV JOBS="${JOBS}" MAXLOAD="${MAXLOAD}" \\
|
||||
GENTOO_PROFILE="default/linux/${ARCH}/${PROFILE_PATH}" \\
|
||||
CHOST="${CHOST}" MAKEOPTS="-j${JOBS}" \\
|
||||
CFLAGS="${CFLAGS}" CXXFLAGS="${CFLAGS}"
|
||||
LABEL maintainer="test@example.com"
|
||||
"""
|
||||
try f.createContext(dir: dir, dockerfile: dockerfile)
|
||||
let imageName = "test-complex-env:\(UUID().uuidString)"
|
||||
try f.build(tag: imageName, contextDir: dir, buildArgs: ["JOBS=12", "ARCH=arm64"])
|
||||
try f.assertImageBuilt(imageName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testLabelOnlyDockerfile() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
try await f.withBuilder { f in
|
||||
let dir = try f.createTempDir()
|
||||
let dockerfile =
|
||||
"""
|
||||
FROM scratch
|
||||
LABEL maintainer="test@example.com" version="1.0.0" \\
|
||||
description="Test image with only labels" \\
|
||||
org.opencontainers.image.title="Test Image"
|
||||
"""
|
||||
try f.createContext(dir: dir, dockerfile: dockerfile)
|
||||
let imageName = "test-label-only:\(UUID().uuidString)"
|
||||
try f.build(tag: imageName, contextDir: dir)
|
||||
try f.assertImageBuilt(imageName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2026 Apple Inc. and the container project authors.
|
||||
//
|
||||
// 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 Darwin
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
/// Tests for `container builder start`, `stop`, and `delete` lifecycle commands.
|
||||
///
|
||||
/// These tests manage the builder manually — they do not use ``withBuilder``
|
||||
/// because they are specifically testing the lifecycle commands themselves.
|
||||
/// They acquire the shared builder lock via ``withBuilderLock`` to serialise
|
||||
/// correctly with tests that use ``withBuilder(_:)``.
|
||||
@Suite(.serialized)
|
||||
struct TestCLIBuilderLifecycleSerial {
|
||||
@Test func testBuilderStartStopCommand() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
try await f.withBuilderLock {
|
||||
f.addCleanup { try? f.builderDelete(force: true) }
|
||||
|
||||
try f.builderStart()
|
||||
try await f.waitForBuilderRunning()
|
||||
let status1 = try f.getContainerStatus("buildkit")
|
||||
#expect(status1 == "running", "buildkit container should be running")
|
||||
|
||||
try f.builderStop()
|
||||
let status2 = try f.getContainerStatus("buildkit")
|
||||
#expect(status2 == "stopped", "buildkit container should be stopped")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testBuilderEnvironmentColors() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
try await f.withBuilderLock {
|
||||
let originalColors = ProcessInfo.processInfo.environment["BUILDKIT_COLORS"]
|
||||
let originalNoColor = ProcessInfo.processInfo.environment["NO_COLOR"]
|
||||
f.addCleanup {
|
||||
if let c = originalColors { setenv("BUILDKIT_COLORS", c, 1) } else { unsetenv("BUILDKIT_COLORS") }
|
||||
if let n = originalNoColor { setenv("NO_COLOR", n, 1) } else { unsetenv("NO_COLOR") }
|
||||
_ = try? f.builderDelete(force: true)
|
||||
}
|
||||
|
||||
_ = try? f.builderDelete(force: true)
|
||||
setenv("BUILDKIT_COLORS", "run=green:warning=yellow:error=red:cancel=cyan", 1)
|
||||
setenv("NO_COLOR", "true", 1)
|
||||
|
||||
try f.run(["builder", "start"]).check()
|
||||
try await f.waitForBuilderRunning()
|
||||
|
||||
let container = try f.inspectContainer("buildkit")
|
||||
let env = container.configuration.initProcess.environment
|
||||
#expect(
|
||||
env.contains("BUILDKIT_COLORS=run=green:warning=yellow:error=red:cancel=cyan"),
|
||||
"BUILDKIT_COLORS should be forwarded to the buildkit container")
|
||||
#expect(
|
||||
env.contains("NO_COLOR=true"),
|
||||
"NO_COLOR should be forwarded to the buildkit container")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2026 Apple Inc. and the container project authors.
|
||||
//
|
||||
// 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 Foundation
|
||||
import Testing
|
||||
|
||||
@Suite(.serialized)
|
||||
struct TestCLIBuilderLocalOutputSerial {
|
||||
@Test func testBuildLocalOutputHappyPath() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
try await f.withBuilder { f in
|
||||
// Comprehensive multi-stage build with context and build args.
|
||||
let dir = try f.createTempDir()
|
||||
let dockerfile =
|
||||
"""
|
||||
ARG MESSAGE=default
|
||||
FROM scratch AS builder
|
||||
ADD build.txt /build.txt
|
||||
ADD testfile.txt /hello.txt
|
||||
FROM scratch
|
||||
COPY --from=builder /build.txt /final.txt
|
||||
COPY --from=builder /hello.txt /app/hello.txt
|
||||
ADD message.txt /message.txt
|
||||
"""
|
||||
let context: [ContainerFixture.FileSystemEntry] = [
|
||||
.file("build.txt", content: .data("Building stage\n".data(using: .utf8)!)),
|
||||
.file("testfile.txt", content: .data("Hello from local build\n".data(using: .utf8)!)),
|
||||
.file("message.txt", content: .data("Hello from build args\n".data(using: .utf8)!)),
|
||||
]
|
||||
try f.createContext(dir: dir, dockerfile: dockerfile, context: context)
|
||||
let outputDir = dir.appending("comprehensive-local-output")
|
||||
let imageName = "local-comprehensive-test:\(UUID().uuidString)"
|
||||
let response = try f.buildWithPathsAndLocalOutput(
|
||||
tag: imageName, contextDir: dir, outputDir: outputDir,
|
||||
buildArgs: ["MESSAGE=Hello from build args"])
|
||||
#expect(response.contains(outputDir.string), "output should reference the export path")
|
||||
#expect(FileManager.default.fileExists(atPath: outputDir.string))
|
||||
let contents = try FileManager.default.contentsOfDirectory(atPath: outputDir.string)
|
||||
#expect(!contents.isEmpty, "output directory should contain files")
|
||||
|
||||
// Basic local output.
|
||||
let basicDir = try f.createTempDir()
|
||||
try f.createContext(
|
||||
dir: basicDir,
|
||||
dockerfile: "FROM scratch\nADD testfile.txt /hello.txt",
|
||||
context: [.file("testfile.txt", content: .data("Hello from basic build\n".data(using: .utf8)!))])
|
||||
let basicOutputDir = basicDir.appending("basic-local-output")
|
||||
let basicResponse = try f.buildWithPathsAndLocalOutput(
|
||||
tag: "local-basic-test:\(UUID().uuidString)", contextDir: basicDir, outputDir: basicOutputDir)
|
||||
#expect(basicResponse.contains(basicOutputDir.string))
|
||||
#expect(FileManager.default.fileExists(atPath: basicOutputDir.string))
|
||||
|
||||
// Build with context (COPY instruction).
|
||||
let ctxDir = try f.createTempDir()
|
||||
try f.createContext(
|
||||
dir: ctxDir,
|
||||
dockerfile: "FROM scratch\nCOPY testfile.txt /app/testfile.txt",
|
||||
context: [.file("testfile.txt", content: .data("Test content\n".data(using: .utf8)!))])
|
||||
let ctxOutputDir = ctxDir.appending("context-local-output")
|
||||
let ctxResponse = try f.buildWithPathsAndLocalOutput(
|
||||
tag: "local-context-test:\(UUID().uuidString)", contextDir: ctxDir, outputDir: ctxOutputDir)
|
||||
#expect(ctxResponse.contains(ctxOutputDir.string))
|
||||
#expect(FileManager.default.fileExists(atPath: ctxOutputDir.string))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testBuildLocalOutputEdgeCases() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
try await f.withBuilder { f in
|
||||
// Different paths for Dockerfile context and build context.
|
||||
let dockerfileDir = try f.createTempDir()
|
||||
try f.createContext(
|
||||
dir: dockerfileDir,
|
||||
dockerfile: "FROM scratch\nCOPY . /app",
|
||||
context: [.file("dockerfile-context.txt", content: .data("Dockerfile context\n".data(using: .utf8)!))])
|
||||
|
||||
let buildContextDir = try f.createTempDir()
|
||||
try f.createContext(
|
||||
dir: buildContextDir, dockerfile: "",
|
||||
context: [.file("build-context.txt", content: .data("Build context\n".data(using: .utf8)!))])
|
||||
|
||||
let outputDir = dockerfileDir.appending("diffpaths-local-output")
|
||||
let response = try f.buildWithPathsAndLocalOutput(
|
||||
tag: "local-diffpaths-test:\(UUID().uuidString)",
|
||||
contextDir: buildContextDir,
|
||||
dockerfilePath: dockerfileDir.appending("Dockerfile"),
|
||||
outputDir: outputDir)
|
||||
#expect(response.contains(outputDir.string))
|
||||
#expect(FileManager.default.fileExists(atPath: outputDir.string))
|
||||
|
||||
// Build into an existing output directory (should merge/overwrite).
|
||||
let existingDir = try f.createTempDir()
|
||||
try f.createContext(
|
||||
dir: existingDir,
|
||||
dockerfile: "FROM scratch\nADD newfile.txt /newfile.txt",
|
||||
context: [.file("newfile.txt", content: .data("New content\n".data(using: .utf8)!))])
|
||||
let existingOutputDir = existingDir.appending("existing-output")
|
||||
try FileManager.default.createDirectory(
|
||||
atPath: existingOutputDir.string, withIntermediateDirectories: true, attributes: nil)
|
||||
try "Existing content\n".data(using: .utf8)!
|
||||
.write(to: URL(filePath: existingOutputDir.appending("existing.txt").string), options: .atomic)
|
||||
let existingResponse = try f.buildWithPathsAndLocalOutput(
|
||||
tag: "local-existing-test:\(UUID().uuidString)",
|
||||
contextDir: existingDir, outputDir: existingOutputDir)
|
||||
#expect(existingResponse.contains(existingOutputDir.string))
|
||||
let contents = try FileManager.default.contentsOfDirectory(atPath: existingOutputDir.string)
|
||||
#expect(!contents.isEmpty)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testBuildLocalOutputFailure() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
try await f.withBuilder { f in
|
||||
let dir = try f.createTempDir()
|
||||
try f.createContext(
|
||||
dir: dir,
|
||||
dockerfile: "FROM scratch\nADD test.txt /test.txt",
|
||||
context: [.file("test.txt", content: .data("test\n".data(using: .utf8)!))])
|
||||
|
||||
// An uncreateable path should cause the build to fail.
|
||||
let result = try f.run([
|
||||
"build",
|
||||
"-f", dir.appending("Dockerfile").string,
|
||||
"-t", "local-invalid-test:\(UUID().uuidString)",
|
||||
"--output", "type=local,dest=/nonexistent/invalid/path",
|
||||
dir.appending("context").string,
|
||||
])
|
||||
#expect(result.status != 0, "build with invalid output path should fail")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,125 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2026 Apple Inc. and the container project authors.
|
||||
//
|
||||
// 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 Foundation
|
||||
import Testing
|
||||
|
||||
@Suite(.serialized)
|
||||
struct TestCLIBuilderTarExportSerial {
|
||||
@Test func testBuildExportTar() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
try await f.withBuilder { f in
|
||||
let dir = try f.createTempDir()
|
||||
try f.createContext(
|
||||
dir: dir,
|
||||
dockerfile: "FROM scratch\nADD emptyFile /",
|
||||
context: [.file("emptyFile", content: .zeroFilled(size: 1))])
|
||||
|
||||
let exportPath = dir.appending("export.tar")
|
||||
let result = try f.run([
|
||||
"build",
|
||||
"-f", dir.appending("Dockerfile").string,
|
||||
"-o", "type=tar,dest=\(exportPath.string)",
|
||||
dir.appending("context").string,
|
||||
])
|
||||
#expect(result.status == 0, "build with tar export should succeed")
|
||||
#expect(FileManager.default.fileExists(atPath: exportPath.string), "tar file should exist")
|
||||
#expect(result.output.contains(exportPath.string), "output should reference export path")
|
||||
let attrs = try FileManager.default.attributesOfItem(atPath: exportPath.string)
|
||||
#expect((attrs[.size] as? Int ?? 0) > 0, "exported tar should not be empty")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testBuildExportTarToDirectory() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
try await f.withBuilder { f in
|
||||
let dir = try f.createTempDir()
|
||||
try f.createContext(
|
||||
dir: dir,
|
||||
dockerfile: "FROM ghcr.io/linuxcontainers/alpine:3.20\nRUN echo \"test\" > /test.txt")
|
||||
|
||||
let exportDir = dir.appending("exports")
|
||||
try FileManager.default.createDirectory(
|
||||
atPath: exportDir.string, withIntermediateDirectories: true, attributes: nil)
|
||||
|
||||
let result = try f.run([
|
||||
"build",
|
||||
"-f", dir.appending("Dockerfile").string,
|
||||
"-o", "type=tar,dest=\(exportDir.string)",
|
||||
dir.appending("context").string,
|
||||
])
|
||||
#expect(result.status == 0, "build with tar export to directory should succeed")
|
||||
let expectedTar = exportDir.appending("out.tar")
|
||||
#expect(
|
||||
FileManager.default.fileExists(atPath: expectedTar.string),
|
||||
"tar file should exist at out.tar")
|
||||
#expect(result.output.contains(expectedTar.string), "output should reference out.tar")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testBuildExportTarMultipleRuns() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
try await f.withBuilder { f in
|
||||
let dir = try f.createTempDir()
|
||||
try f.createContext(
|
||||
dir: dir,
|
||||
dockerfile: "FROM scratch\nADD testFile /",
|
||||
context: [.file("testFile", content: .data("test data".data(using: .utf8)!))])
|
||||
|
||||
let exportDir = dir.appending("exports")
|
||||
try FileManager.default.createDirectory(
|
||||
atPath: exportDir.string, withIntermediateDirectories: true, attributes: nil)
|
||||
|
||||
let buildArgs = [
|
||||
"build",
|
||||
"-f", dir.appending("Dockerfile").string,
|
||||
"-o", "type=tar,dest=\(exportDir.string)",
|
||||
dir.appending("context").string,
|
||||
]
|
||||
|
||||
let r1 = try f.run(buildArgs)
|
||||
#expect(r1.status == 0, "first build should succeed")
|
||||
#expect(FileManager.default.fileExists(atPath: exportDir.appending("out.tar").string))
|
||||
|
||||
let r2 = try f.run(buildArgs)
|
||||
#expect(r2.status == 0, "second build should succeed")
|
||||
#expect(
|
||||
FileManager.default.fileExists(atPath: exportDir.appending("out.tar.1").string),
|
||||
"second tar should exist at out.tar.1")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testBuildExportTarInvalidDest() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
try await f.withBuilder { f in
|
||||
let dir = try f.createTempDir()
|
||||
try f.createContext(dir: dir, dockerfile: "FROM scratch")
|
||||
|
||||
let result = try f.run([
|
||||
"build",
|
||||
"-f", dir.appending("Dockerfile").string,
|
||||
"-o", "type=tar", // missing dest
|
||||
dir.appending("context").string,
|
||||
])
|
||||
#expect(result.status != 0, "build without dest should fail")
|
||||
#expect(result.error.contains("dest field is required"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2026 Apple Inc. and the container project authors.
|
||||
//
|
||||
// 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
|
||||
|
||||
@Suite
|
||||
struct TestCLIProgressAuto {
|
||||
private let alpine = ContainerFixture.warmupImages[0]
|
||||
|
||||
@Test func testAutoProgressFallsBackToPlainWhenPiped() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let result = try f.run(["image", "pull", "--progress", "auto", alpine])
|
||||
#expect(result.status == 0, "image pull should succeed, stderr: \(result.error)")
|
||||
let lines = result.error.components(separatedBy: .newlines)
|
||||
.filter { !$0.contains("Warning! Running debug build") && !$0.isEmpty }
|
||||
#expect(!lines.isEmpty, "expected plain progress output on stderr when piped")
|
||||
#expect(!result.error.contains("\u{1B}["), "expected no ANSI escapes in piped output")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testExplicitPlainProgress() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let result = try f.run(["image", "pull", "--progress", "plain", alpine])
|
||||
#expect(
|
||||
result.status == 0,
|
||||
"image pull --progress plain should succeed, stderr: \(result.error)")
|
||||
let lines = result.error.components(separatedBy: .newlines)
|
||||
.filter { !$0.contains("Warning! Running debug build") && !$0.isEmpty }
|
||||
#expect(!lines.isEmpty, "expected plain progress output on stderr")
|
||||
#expect(!result.error.contains("\u{1B}["), "expected no ANSI escapes with --progress plain")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testExplicitAnsiProgress() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let result = try f.run(["image", "pull", "--progress", "ansi", alpine])
|
||||
// Verify the command succeeds; ANSI output is suppressed in non-TTY contexts
|
||||
// so we don't assert on stderr content here.
|
||||
#expect(
|
||||
result.status == 0,
|
||||
"image pull --progress ansi should succeed, stderr: \(result.error)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testNoneProgressSuppressesOutput() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let result = try f.run(["image", "pull", "--progress", "none", alpine])
|
||||
#expect(
|
||||
result.status == 0,
|
||||
"image pull --progress none should succeed, stderr: \(result.error)")
|
||||
let lines = result.error.components(separatedBy: .newlines)
|
||||
.filter { !$0.contains("Warning! Running debug build") && !$0.isEmpty }
|
||||
#expect(lines.isEmpty, "expected no progress output on stderr with --progress none")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2026 Apple Inc. and the container project authors.
|
||||
//
|
||||
// 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 Foundation
|
||||
import Testing
|
||||
|
||||
@Suite
|
||||
struct TestCLIRegistry {
|
||||
@Test func testListDefaultFormat() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let result = try f.run(["registry", "list"])
|
||||
#expect(result.status == 0, "registry list should succeed, stderr: \(result.error)")
|
||||
|
||||
let requiredHeaders = ["HOSTNAME", "USERNAME", "MODIFIED", "CREATED"]
|
||||
#expect(
|
||||
requiredHeaders.allSatisfy { result.output.contains($0) },
|
||||
"output should contain all required headers"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testListJSONFormat() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let result = try f.run(["registry", "list", "--format", "json"])
|
||||
#expect(
|
||||
result.status == 0,
|
||||
"registry list --format json should succeed, stderr: \(result.error)")
|
||||
|
||||
let json = try JSONSerialization.jsonObject(with: result.outputData, options: [])
|
||||
#expect(json is [Any], "JSON output should be an array")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testListQuietMode() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let result = try f.run(["registry", "list", "-q"])
|
||||
#expect(result.status == 0, "registry list -q should succeed, stderr: \(result.error)")
|
||||
#expect(!result.output.contains("HOSTNAME"), "quiet mode should not contain headers")
|
||||
#expect(!result.output.contains("USERNAME"), "quiet mode should not contain headers")
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
-11
@@ -1,5 +1,5 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-2026 Apple Inc. and the container project authors.
|
||||
// Copyright © 2026 Apple Inc. and the container project authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -16,19 +16,22 @@
|
||||
|
||||
import Testing
|
||||
|
||||
@Suite
|
||||
struct TestCLIPluginErrors {
|
||||
@Test
|
||||
func testHelpfulMessageWhenPluginsUnavailable() throws {
|
||||
@Test func testHelpfulMessageWhenPluginsUnavailable() async throws {
|
||||
// Intentionally invoke an unknown plugin command. In CI this should run
|
||||
// without the APIServer started, so DefaultCommand will fail to create
|
||||
// a PluginLoader and emit the improved guidance.
|
||||
let cli = try CLITest()
|
||||
let (_, _, stderr, status) = try cli.run(arguments: ["nosuchplugin"]) // non-existent plugin name
|
||||
|
||||
#expect(status != 0)
|
||||
#expect(stderr.contains("container system start"))
|
||||
#expect(stderr.contains("Plugins are unavailable") || stderr.contains("Plugin 'container-"))
|
||||
// Should include at least one computed plugin search path hint
|
||||
#expect(stderr.contains("container-plugins") || stderr.contains("container/plugins"))
|
||||
try await ContainerFixture.with { f in
|
||||
let result = try f.run(["nosuchplugin"])
|
||||
#expect(result.status != 0)
|
||||
#expect(result.error.contains("container system start"))
|
||||
#expect(
|
||||
result.error.contains("Plugins are unavailable")
|
||||
|| result.error.contains("Plugin 'container-"))
|
||||
#expect(
|
||||
result.error.contains("container-plugins")
|
||||
|| result.error.contains("container/plugins"))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user