Migrate registry tests to new test support types. (#1845)

- Part of #1833.
This commit is contained in:
J Logan
2026-06-30 10:40:26 -07:00
committed by GitHub
parent d29e6edd5e
commit 586fa07d2a
10 changed files with 1727 additions and 129 deletions
@@ -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")
}
}
}
@@ -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"))
}
}
}