mirror of
https://github.com/apple/container.git
synced 2026-08-27 10:56:32 +00:00
Fix BuilderStart race, parallelize container build tests. (#2002)
- Closes #2001. - Handle "container exists" error gracefully instead of failing, when trying to start the buildkit container. - Move build tests to parallel suites, while the builder lifecycle tests remain serial. Parallel builds don't use the fixture lock that deletes and restarts the builder and runs a build block in isolation.
This commit is contained in:
@@ -292,11 +292,17 @@ extension Application {
|
||||
.setDescription("Starting BuildKit container")
|
||||
])
|
||||
|
||||
try await client.create(
|
||||
configuration: config,
|
||||
options: .default,
|
||||
kernel: kernel
|
||||
)
|
||||
do {
|
||||
try await client.create(
|
||||
configuration: config,
|
||||
options: .default,
|
||||
kernel: kernel
|
||||
)
|
||||
} catch let error as ContainerizationError where error.code == .exists {
|
||||
// A concurrent `container build` invocation already created the builder
|
||||
// while we were fetching the image/kernel above. `bootstrap` below is
|
||||
// idempotent, so just proceed against the container the winner created.
|
||||
}
|
||||
|
||||
try await startBuildKit(client: client, id: Builder.builderContainerId, progressUpdate, taskManager)
|
||||
log.debug("starting BuildKit and BuildKit-shim")
|
||||
|
||||
@@ -94,42 +94,6 @@ extension ContainerFixture {
|
||||
}
|
||||
throw CommandError.executionFailed("timed out waiting for container-builder-shim on buildkit")
|
||||
}
|
||||
|
||||
/// Deletes any existing builder, starts a fresh one, runs `body`, then deletes the builder.
|
||||
///
|
||||
/// Each build test gets an isolated builder to avoid inter-test contamination.
|
||||
/// Acquires a process-wide lock so only one test holds the buildkit singleton at a time,
|
||||
/// regardless of how many suites run concurrently in the global pass.
|
||||
public func withBuilder(
|
||||
cpus: Int64 = 2,
|
||||
memoryInGBs: Int64 = 2,
|
||||
_ body: @Sendable (ContainerFixture) async throws -> Void
|
||||
) async throws {
|
||||
try await withoutActuallyEscaping(body) { escapingBody in
|
||||
try await Self.builderLock.withLock { _ in
|
||||
_ = try? self.run(["builder", "delete", "--force"])
|
||||
try self.builderStart(cpus: cpus, memoryInGBs: memoryInGBs)
|
||||
defer { _ = try? self.run(["builder", "delete", "--force"]) }
|
||||
try await self.waitForBuilderRunning()
|
||||
try await escapingBody(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Acquires the process-wide builder lock without starting a builder.
|
||||
///
|
||||
/// Use this in tests that manually manage the builder lifecycle (e.g. lifecycle
|
||||
/// tests that call ``builderStart()``/``builderStop()`` directly) so they
|
||||
/// serialise correctly with tests that use ``withBuilder(_:)``.
|
||||
public func withBuilderLock<T: Sendable>(_ body: @Sendable () async throws -> T) async throws -> T {
|
||||
try await withoutActuallyEscaping(body) { escapingBody in
|
||||
try await Self.builderLock.withLock { _ in
|
||||
try await escapingBody()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static let builderLock = AsyncLock()
|
||||
}
|
||||
|
||||
// MARK: - Build context helpers
|
||||
|
||||
@@ -71,6 +71,8 @@ public struct ContainerClient: Sendable {
|
||||
}
|
||||
|
||||
try await xpcSend(message: request)
|
||||
} catch let error as ContainerizationError {
|
||||
throw error
|
||||
} catch {
|
||||
throw ContainerizationError(
|
||||
.internalError,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,128 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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 ContainerTestSupport
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
struct TestCLIBuilderEnvOnly {
|
||||
@Test func testBuildEnvironmentOnlyImageFromScratch() async throws {
|
||||
try await ContainerFixture.with { 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
|
||||
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
|
||||
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
|
||||
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
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,139 +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 ContainerTestSupport
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,56 +21,50 @@ 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(_:)``.
|
||||
/// Serialized because they stop/delete the shared `buildkit` container, which
|
||||
/// would race with in-flight builds in the concurrent pool.
|
||||
@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) }
|
||||
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.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")
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
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")
|
||||
}
|
||||
|
||||
_ = 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,141 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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 ContainerTestSupport
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
struct TestCLIBuilderLocalOutput {
|
||||
@Test func testBuildLocalOutputHappyPath() async throws {
|
||||
try await ContainerFixture.with { 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
|
||||
// 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
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,148 +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 ContainerTestSupport
|
||||
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,117 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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 ContainerTestSupport
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
struct TestCLIBuilderTarExport {
|
||||
@Test func testBuildExportTar() async throws {
|
||||
try await ContainerFixture.with { 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
|
||||
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
|
||||
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
|
||||
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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,126 +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 ContainerTestSupport
|
||||
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,37 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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 ContainerTestSupport
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
/// Serial because this repulls the shared warmup alpine image with `--no-cache`,
|
||||
/// which would race with concurrent-pool tests relying on it already being cached.
|
||||
@Suite(.serialized)
|
||||
struct TestCLIBuilderWarmupPullSerial {
|
||||
@Test func testBuildNoCachePullLatestImage() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let dir = try f.createTempDir()
|
||||
try f.createContext(
|
||||
dir: dir,
|
||||
dockerfile: "FROM \(WarmupImage.alpine320.rawValue)\nADD emptyFile /",
|
||||
context: [.file("emptyFile", content: .zeroFilled(size: 1))])
|
||||
let image = "registry.local/no-cache-pull:\(UUID().uuidString)"
|
||||
try f.buildWithPaths(tags: [image], contextDir: dir, otherArgs: ["--pull", "--no-cache"])
|
||||
try f.assertImageBuilt(image)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user