chore: import upstream snapshot with attribution
container project - merge build / Invoke build (push) Successful in 1s
container project - merge build / Invoke build (push) Successful in 1s
This commit is contained in:
@@ -0,0 +1,546 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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 TestCLICopyCommand {
|
||||
|
||||
// MARK: - Basic host/container copy
|
||||
|
||||
@Test func testCopyHostToContainer() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
try await f.withContainer(image: image) { name in
|
||||
let src = f.testDir.appending("testfile.txt")
|
||||
let content = "hello from host"
|
||||
try content.write(toFile: src.string, atomically: true, encoding: .utf8)
|
||||
try f.run(["copy", src.string, "\(name):/tmp/"]).check()
|
||||
let cat = try f.doExec(name, cmd: ["cat", "/tmp/testfile.txt"])
|
||||
#expect(cat.trimmingCharacters(in: .whitespacesAndNewlines) == content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testCopyContainerToHost() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
try await f.withContainer(image: image) { name in
|
||||
let content = "hello from container"
|
||||
try f.doExec(name, cmd: ["sh", "-c", "echo -n '\(content)' > /tmp/containerfile.txt"])
|
||||
let dest = f.testDir.appending("containerfile.txt")
|
||||
try f.run(["copy", "\(name):/tmp/containerfile.txt", dest.string]).check()
|
||||
let hostContent = try String(contentsOfFile: dest.string, encoding: .utf8)
|
||||
#expect(hostContent == content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testCopyUsingCpAlias() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
try await f.withContainer(image: image) { name in
|
||||
let src = f.testDir.appending("aliasfile.txt")
|
||||
let content = "testing cp alias"
|
||||
try content.write(toFile: src.string, atomically: true, encoding: .utf8)
|
||||
try f.run(["cp", src.string, "\(name):/tmp/"]).check()
|
||||
let cat = try f.doExec(name, cmd: ["cat", "/tmp/aliasfile.txt"])
|
||||
#expect(cat.trimmingCharacters(in: .whitespacesAndNewlines) == content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testCopyLocalToLocalFails() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let result = try f.run(["copy", "/tmp/source.txt", "/tmp/dest.txt"])
|
||||
#expect(result.status != 0, "expected local-to-local copy to fail")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testCopyContainerToContainerFails() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
let name = "\(f.testID)-c"
|
||||
try f.doCreate(name: name, image: image)
|
||||
f.addCleanup { try f.doRemoveIfExists(name, ignoreFailure: true) }
|
||||
let result = try f.run(["copy", "\(name):/tmp/file.txt", "\(name):/tmp/file2.txt"])
|
||||
#expect(result.status != 0, "expected container-to-container copy to fail")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testCopyToNonRunningContainerFails() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
let name = "\(f.testID)-c"
|
||||
try f.doCreate(name: name, image: image)
|
||||
f.addCleanup { try f.doRemoveIfExists(name, ignoreFailure: true) }
|
||||
let src = f.testDir.appending("norun.txt")
|
||||
try "test".write(toFile: src.string, atomically: true, encoding: .utf8)
|
||||
let result = try f.run(["copy", src.string, "\(name):/tmp/"])
|
||||
#expect(result.status != 0, "expected copy to non-running container to fail")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testCopyDirectoryHostToContainer() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
try await f.withContainer(image: image) { name in
|
||||
let srcDir = f.testDir.appending("hostdir")
|
||||
try FileManager.default.createDirectory(atPath: srcDir.string, withIntermediateDirectories: true, attributes: nil)
|
||||
try "file1 content".write(toFile: srcDir.appending("file1.txt").string, atomically: true, encoding: .utf8)
|
||||
try "file2 content".write(toFile: srcDir.appending("file2.txt").string, atomically: true, encoding: .utf8)
|
||||
try f.run(["copy", srcDir.string, "\(name):/tmp/"]).check()
|
||||
let cat1 = try f.doExec(name, cmd: ["cat", "/tmp/hostdir/file1.txt"])
|
||||
#expect(cat1.trimmingCharacters(in: .whitespacesAndNewlines) == "file1 content")
|
||||
let cat2 = try f.doExec(name, cmd: ["cat", "/tmp/hostdir/file2.txt"])
|
||||
#expect(cat2.trimmingCharacters(in: .whitespacesAndNewlines) == "file2 content")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testCopyDirectoryContainerToHost() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
try await f.withContainer(image: image) { name in
|
||||
try f.doExec(name, cmd: ["sh", "-c", "mkdir -p /tmp/guestdir && echo -n 'aaa' > /tmp/guestdir/a.txt && echo -n 'bbb' > /tmp/guestdir/b.txt"])
|
||||
let dest = f.testDir.appending("guestdir")
|
||||
try f.run(["copy", "\(name):/tmp/guestdir", dest.string]).check()
|
||||
let a = try String(contentsOfFile: dest.appending("a.txt").string, encoding: .utf8)
|
||||
#expect(a == "aaa")
|
||||
let b = try String(contentsOfFile: dest.appending("b.txt").string, encoding: .utf8)
|
||||
#expect(b == "bbb")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testCopyNestedDirectoryHostToContainer() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
try await f.withContainer(image: image) { name in
|
||||
let srcDir = f.testDir.appending("nested")
|
||||
let subDir = srcDir.appending("sub")
|
||||
try FileManager.default.createDirectory(atPath: subDir.string, withIntermediateDirectories: true, attributes: nil)
|
||||
try "root file".write(toFile: srcDir.appending("root.txt").string, atomically: true, encoding: .utf8)
|
||||
try "nested file".write(toFile: subDir.appending("deep.txt").string, atomically: true, encoding: .utf8)
|
||||
try f.run(["copy", srcDir.string, "\(name):/tmp/"]).check()
|
||||
let catRoot = try f.doExec(name, cmd: ["cat", "/tmp/nested/root.txt"])
|
||||
#expect(catRoot.trimmingCharacters(in: .whitespacesAndNewlines) == "root file")
|
||||
let catDeep = try f.doExec(name, cmd: ["cat", "/tmp/nested/sub/deep.txt"])
|
||||
#expect(catDeep.trimmingCharacters(in: .whitespacesAndNewlines) == "nested file")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testCopyNestedDirectoryContainerToHost() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
try await f.withContainer(image: image) { name in
|
||||
try f.doExec(name, cmd: ["sh", "-c", "mkdir -p /tmp/nested/sub && echo -n 'root file' > /tmp/nested/root.txt && echo -n 'nested file' > /tmp/nested/sub/deep.txt"])
|
||||
let dest = f.testDir.appending("nested")
|
||||
try f.run(["copy", "\(name):/tmp/nested", dest.string]).check()
|
||||
let root = try String(contentsOfFile: dest.appending("root.txt").string, encoding: .utf8)
|
||||
#expect(root == "root file")
|
||||
let deep = try String(contentsOfFile: dest.appending("sub").appending("deep.txt").string, encoding: .utf8)
|
||||
#expect(deep == "nested file")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CopyOut S1: no trailing slash
|
||||
|
||||
@Test func testCopyOutFileToExistingFile() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
try await f.withContainer(image: image) { name in
|
||||
let content = "container content"
|
||||
try f.doExec(name, cmd: ["sh", "-c", "echo -n '\(content)' > /tmp/source.txt"])
|
||||
let dest = f.testDir.appending("existing.txt")
|
||||
try "old content".write(toFile: dest.string, atomically: true, encoding: .utf8)
|
||||
try f.run(["copy", "\(name):/tmp/source.txt", dest.string]).check()
|
||||
let result = try String(contentsOfFile: dest.string, encoding: .utf8)
|
||||
#expect(result == content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testCopyOutDirectoryToExistingFileFails() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
try await f.withContainer(image: image) { name in
|
||||
try f.doExec(name, cmd: ["sh", "-c", "mkdir -p /tmp/srcdir && echo -n 'x' > /tmp/srcdir/file.txt"])
|
||||
let dest = f.testDir.appending("existing.txt")
|
||||
try "x".write(toFile: dest.string, atomically: true, encoding: .utf8)
|
||||
let result = try f.run(["copy", "\(name):/tmp/srcdir", dest.string])
|
||||
#expect(result.status != 0, "expected directory-to-existing-file to fail")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testCopyOutFileToExistingDirectory() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
try await f.withContainer(image: image) { name in
|
||||
let content = "container content"
|
||||
try f.doExec(name, cmd: ["sh", "-c", "echo -n '\(content)' > /tmp/source.txt"])
|
||||
let destDir = f.testDir.appending("dstdir")
|
||||
try FileManager.default.createDirectory(atPath: destDir.string, withIntermediateDirectories: true, attributes: nil)
|
||||
try f.run(["copy", "\(name):/tmp/source.txt", destDir.string]).check()
|
||||
let result = try String(contentsOfFile: destDir.appending("source.txt").string, encoding: .utf8)
|
||||
#expect(result == content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testCopyOutDirectoryToExistingDirectory() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
try await f.withContainer(image: image) { name in
|
||||
try f.doExec(name, cmd: ["sh", "-c", "mkdir -p /tmp/srcdir && echo -n 'hello' > /tmp/srcdir/file.txt"])
|
||||
let destDir = f.testDir.appending("dstdir")
|
||||
try FileManager.default.createDirectory(atPath: destDir.string, withIntermediateDirectories: true, attributes: nil)
|
||||
try f.run(["copy", "\(name):/tmp/srcdir", destDir.string]).check()
|
||||
let result = try String(contentsOfFile: destDir.appending("srcdir").appending("file.txt").string, encoding: .utf8)
|
||||
#expect(result == "hello")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CopyOut S2: trailing slash on dst
|
||||
|
||||
@Test func testCopyOutFileToNonExistingTrailingSlashFails() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
try await f.withContainer(image: image) { name in
|
||||
try f.doExec(name, cmd: ["sh", "-c", "echo -n 'x' > /tmp/source.txt"])
|
||||
let dest = f.testDir.appending("nonexistent").string + "/"
|
||||
let result = try f.run(["copy", "\(name):/tmp/source.txt", dest])
|
||||
#expect(result.status != 0, "expected file-to-nonexisting/ to fail")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testCopyOutDirectoryToNonExistingTrailingSlash() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
try await f.withContainer(image: image) { name in
|
||||
try f.doExec(name, cmd: ["sh", "-c", "mkdir -p /tmp/srcdir && echo -n 'hello' > /tmp/srcdir/file.txt"])
|
||||
let destDir = f.testDir.appending("newdir")
|
||||
try f.run(["copy", "\(name):/tmp/srcdir", destDir.string + "/"]).check()
|
||||
var isDir: ObjCBool = false
|
||||
#expect(FileManager.default.fileExists(atPath: destDir.string, isDirectory: &isDir) && isDir.boolValue)
|
||||
let result = try String(contentsOfFile: destDir.appending("file.txt").string, encoding: .utf8)
|
||||
#expect(result == "hello")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testCopyOutFileToExistingDirectoryTrailingSlash() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
try await f.withContainer(image: image) { name in
|
||||
let content = "container content"
|
||||
try f.doExec(name, cmd: ["sh", "-c", "echo -n '\(content)' > /tmp/source.txt"])
|
||||
let destDir = f.testDir.appending("dstdir")
|
||||
try FileManager.default.createDirectory(atPath: destDir.string, withIntermediateDirectories: true, attributes: nil)
|
||||
try f.run(["copy", "\(name):/tmp/source.txt", destDir.string + "/"]).check()
|
||||
let result = try String(contentsOfFile: destDir.appending("source.txt").string, encoding: .utf8)
|
||||
#expect(result == content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testCopyOutDirectoryToExistingDirectoryTrailingSlash() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
try await f.withContainer(image: image) { name in
|
||||
try f.doExec(name, cmd: ["sh", "-c", "mkdir -p /tmp/srcdir && echo -n 'hello' > /tmp/srcdir/file.txt"])
|
||||
let destDir = f.testDir.appending("dstdir")
|
||||
try FileManager.default.createDirectory(atPath: destDir.string, withIntermediateDirectories: true, attributes: nil)
|
||||
try f.run(["copy", "\(name):/tmp/srcdir", destDir.string + "/"]).check()
|
||||
let result = try String(contentsOfFile: destDir.appending("srcdir").appending("file.txt").string, encoding: .utf8)
|
||||
#expect(result == "hello")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CopyOut S3: trailing slash on src
|
||||
|
||||
@Test func testCopyOutDirectoryContentsToNonExisting() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
try await f.withContainer(image: image) { name in
|
||||
try f.doExec(name, cmd: ["sh", "-c", "mkdir -p /tmp/srcdir/sub && echo -n 'hello' > /tmp/srcdir/file.txt"])
|
||||
let destDir = f.testDir.appending("newdir")
|
||||
try f.run(["copy", "\(name):/tmp/srcdir/", destDir.string]).check()
|
||||
let result = try String(contentsOfFile: destDir.appending("file.txt").string, encoding: .utf8)
|
||||
#expect(result == "hello")
|
||||
var isDir: ObjCBool = false
|
||||
#expect(FileManager.default.fileExists(atPath: destDir.appending("sub").string, isDirectory: &isDir) && isDir.boolValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testCopyOutDirectoryContentsToExistingFileFails() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
try await f.withContainer(image: image) { name in
|
||||
try f.doExec(name, cmd: ["sh", "-c", "mkdir -p /tmp/srcdir && echo -n 'x' > /tmp/srcdir/file.txt"])
|
||||
let dest = f.testDir.appending("existing.txt")
|
||||
try "x".write(toFile: dest.string, atomically: true, encoding: .utf8)
|
||||
let result = try f.run(["copy", "\(name):/tmp/srcdir/", dest.string])
|
||||
#expect(result.status != 0, "expected directory/-to-existing-file to fail")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testCopyOutDirectoryContentsToExistingDirectory() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
try await f.withContainer(image: image) { name in
|
||||
try f.doExec(name, cmd: ["sh", "-c", "mkdir -p /tmp/srcdir && echo -n 'hello' > /tmp/srcdir/file.txt"])
|
||||
let destDir = f.testDir.appending("dstdir")
|
||||
try FileManager.default.createDirectory(atPath: destDir.string, withIntermediateDirectories: true, attributes: nil)
|
||||
try f.run(["copy", "\(name):/tmp/srcdir/", destDir.string]).check()
|
||||
let result = try String(contentsOfFile: destDir.appending("srcdir").appending("file.txt").string, encoding: .utf8)
|
||||
#expect(result == "hello")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CopyOut S4: trailing slash on both src and dst
|
||||
|
||||
@Test func testCopyOutDirectoryContentsToNonExistingTrailingSlash() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
try await f.withContainer(image: image) { name in
|
||||
try f.doExec(name, cmd: ["sh", "-c", "mkdir -p /tmp/srcdir && echo -n 'hello' > /tmp/srcdir/file.txt"])
|
||||
let destDir = f.testDir.appending("newdir")
|
||||
try f.run(["copy", "\(name):/tmp/srcdir/", destDir.string + "/"]).check()
|
||||
let result = try String(contentsOfFile: destDir.appending("file.txt").string, encoding: .utf8)
|
||||
#expect(result == "hello")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testCopyOutDirectoryContentsToExistingDirectoryTrailingSlash() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
try await f.withContainer(image: image) { name in
|
||||
try f.doExec(name, cmd: ["sh", "-c", "mkdir -p /tmp/srcdir && echo -n 'hello' > /tmp/srcdir/file.txt"])
|
||||
let destDir = f.testDir.appending("dstdir")
|
||||
try FileManager.default.createDirectory(atPath: destDir.string, withIntermediateDirectories: true, attributes: nil)
|
||||
try f.run(["copy", "\(name):/tmp/srcdir/", destDir.string + "/"]).check()
|
||||
let result = try String(contentsOfFile: destDir.appending("srcdir").appending("file.txt").string, encoding: .utf8)
|
||||
#expect(result == "hello")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CopyIn S1: no trailing slash
|
||||
|
||||
@Test func testCopyInFileToExistingFile() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
try await f.withContainer(image: image) { name in
|
||||
let content = "new content"
|
||||
let src = f.testDir.appending("source.txt")
|
||||
try content.write(toFile: src.string, atomically: true, encoding: .utf8)
|
||||
try f.doExec(name, cmd: ["sh", "-c", "echo -n 'old content' > /tmp/existing.txt"])
|
||||
try f.run(["copy", src.string, "\(name):/tmp/existing.txt"]).check()
|
||||
let result = try f.doExec(name, cmd: ["cat", "/tmp/existing.txt"])
|
||||
#expect(result.trimmingCharacters(in: .whitespacesAndNewlines) == content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testCopyInDirectoryToExistingFileFails() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
try await f.withContainer(image: image) { name in
|
||||
let srcDir = f.testDir.appending("srcdir")
|
||||
try FileManager.default.createDirectory(atPath: srcDir.string, withIntermediateDirectories: true, attributes: nil)
|
||||
try "x".write(toFile: srcDir.appending("file.txt").string, atomically: true, encoding: .utf8)
|
||||
try f.doExec(name, cmd: ["sh", "-c", "echo -n 'x' > /tmp/existing.txt"])
|
||||
let result = try f.run(["copy", srcDir.string, "\(name):/tmp/existing.txt"])
|
||||
#expect(result.status != 0, "expected directory-to-existing-file to fail")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testCopyInFileToExistingDirectory() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
try await f.withContainer(image: image) { name in
|
||||
let content = "host content"
|
||||
let src = f.testDir.appending("source.txt")
|
||||
try content.write(toFile: src.string, atomically: true, encoding: .utf8)
|
||||
try f.doExec(name, cmd: ["mkdir", "-p", "/tmp/dstdir"])
|
||||
try f.run(["copy", src.string, "\(name):/tmp/dstdir"]).check()
|
||||
let result = try f.doExec(name, cmd: ["cat", "/tmp/dstdir/source.txt"])
|
||||
#expect(result.trimmingCharacters(in: .whitespacesAndNewlines) == content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testCopyInDirectoryToExistingDirectory() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
try await f.withContainer(image: image) { name in
|
||||
let srcDir = f.testDir.appending("srcdir")
|
||||
try FileManager.default.createDirectory(atPath: srcDir.string, withIntermediateDirectories: true, attributes: nil)
|
||||
try "hello".write(toFile: srcDir.appending("file.txt").string, atomically: true, encoding: .utf8)
|
||||
try f.doExec(name, cmd: ["mkdir", "-p", "/tmp/dstdir"])
|
||||
try f.run(["copy", srcDir.string, "\(name):/tmp/dstdir"]).check()
|
||||
let result = try f.doExec(name, cmd: ["cat", "/tmp/dstdir/srcdir/file.txt"])
|
||||
#expect(result.trimmingCharacters(in: .whitespacesAndNewlines) == "hello")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CopyIn S2: trailing slash on dst
|
||||
|
||||
@Test func testCopyInFileToNonExistingTrailingSlashFails() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
try await f.withContainer(image: image) { name in
|
||||
let src = f.testDir.appending("source.txt")
|
||||
try "x".write(toFile: src.string, atomically: true, encoding: .utf8)
|
||||
let result = try f.run(["copy", src.string, "\(name):/tmp/nonexistent/"])
|
||||
#expect(result.status != 0, "expected file-to-nonexisting/ to fail")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testCopyInDirectoryToNonExistingTrailingSlash() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
try await f.withContainer(image: image) { name in
|
||||
let srcDir = f.testDir.appending("srcdir")
|
||||
try FileManager.default.createDirectory(atPath: srcDir.string, withIntermediateDirectories: true, attributes: nil)
|
||||
try "hello".write(toFile: srcDir.appending("file.txt").string, atomically: true, encoding: .utf8)
|
||||
try f.run(["copy", srcDir.string, "\(name):/tmp/newdir/"]).check()
|
||||
let result = try f.doExec(name, cmd: ["cat", "/tmp/newdir/file.txt"])
|
||||
#expect(result.trimmingCharacters(in: .whitespacesAndNewlines) == "hello")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CopyIn S3: trailing slash on src
|
||||
|
||||
@Test func testCopyInDirectoryContentsToNonExisting() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
try await f.withContainer(image: image) { name in
|
||||
let srcDir = f.testDir.appending("srcdir")
|
||||
let subDir = srcDir.appending("sub")
|
||||
try FileManager.default.createDirectory(atPath: subDir.string, withIntermediateDirectories: true, attributes: nil)
|
||||
try "hello".write(toFile: srcDir.appending("file.txt").string, atomically: true, encoding: .utf8)
|
||||
try f.run(["copy", srcDir.string + "/", "\(name):/tmp/newdir"]).check()
|
||||
let result = try f.doExec(name, cmd: ["cat", "/tmp/newdir/file.txt"])
|
||||
#expect(result.trimmingCharacters(in: .whitespacesAndNewlines) == "hello")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testCopyInDirectoryContentsToExistingFileFails() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
try await f.withContainer(image: image) { name in
|
||||
let srcDir = f.testDir.appending("srcdir")
|
||||
try FileManager.default.createDirectory(atPath: srcDir.string, withIntermediateDirectories: true, attributes: nil)
|
||||
try "x".write(toFile: srcDir.appending("file.txt").string, atomically: true, encoding: .utf8)
|
||||
try f.doExec(name, cmd: ["sh", "-c", "echo -n 'x' > /tmp/existing.txt"])
|
||||
let result = try f.run(["copy", srcDir.string + "/", "\(name):/tmp/existing.txt"])
|
||||
#expect(result.status != 0, "expected directory/-to-existing-file to fail")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testCopyInDirectoryContentsToExistingDirectory() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
try await f.withContainer(image: image) { name in
|
||||
let srcDir = f.testDir.appending("srcdir")
|
||||
try FileManager.default.createDirectory(atPath: srcDir.string, withIntermediateDirectories: true, attributes: nil)
|
||||
try "hello".write(toFile: srcDir.appending("file.txt").string, atomically: true, encoding: .utf8)
|
||||
try f.doExec(name, cmd: ["mkdir", "-p", "/tmp/dstdir"])
|
||||
try f.run(["copy", srcDir.string + "/", "\(name):/tmp/dstdir"]).check()
|
||||
let result = try f.doExec(name, cmd: ["cat", "/tmp/dstdir/srcdir/file.txt"])
|
||||
#expect(result.trimmingCharacters(in: .whitespacesAndNewlines) == "hello")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CopyIn S4: trailing slash on both src and dst
|
||||
|
||||
@Test func testCopyInDirectoryContentsToNonExistingTrailingSlash() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
try await f.withContainer(image: image) { name in
|
||||
let srcDir = f.testDir.appending("srcdir")
|
||||
try FileManager.default.createDirectory(atPath: srcDir.string, withIntermediateDirectories: true, attributes: nil)
|
||||
try "hello".write(toFile: srcDir.appending("file.txt").string, atomically: true, encoding: .utf8)
|
||||
try f.run(["copy", srcDir.string + "/", "\(name):/tmp/newdir/"]).check()
|
||||
let result = try f.doExec(name, cmd: ["cat", "/tmp/newdir/file.txt"])
|
||||
#expect(result.trimmingCharacters(in: .whitespacesAndNewlines) == "hello")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testCopyInDirectoryContentsToExistingDirectoryTrailingSlash() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
try await f.withContainer(image: image) { name in
|
||||
let srcDir = f.testDir.appending("srcdir")
|
||||
try FileManager.default.createDirectory(atPath: srcDir.string, withIntermediateDirectories: true, attributes: nil)
|
||||
try "hello".write(toFile: srcDir.appending("file.txt").string, atomically: true, encoding: .utf8)
|
||||
try f.doExec(name, cmd: ["mkdir", "-p", "/tmp/dstdir"])
|
||||
try f.run(["copy", srcDir.string + "/", "\(name):/tmp/dstdir/"]).check()
|
||||
let result = try f.doExec(name, cmd: ["cat", "/tmp/dstdir/srcdir/file.txt"])
|
||||
#expect(result.trimmingCharacters(in: .whitespacesAndNewlines) == "hello")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Relative path resolution
|
||||
|
||||
@Test func testCopyInRelativeSourcePath() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
try await f.withContainer(image: image) { name in
|
||||
let content = "relative source"
|
||||
try content.write(toFile: f.testDir.appending("relfile.txt").string, atomically: true, encoding: .utf8)
|
||||
try f.run(["copy", "./relfile.txt", "\(name):/tmp/"], currentDirectory: f.testDir).check()
|
||||
let result = try f.doExec(name, cmd: ["cat", "/tmp/relfile.txt"])
|
||||
#expect(result.trimmingCharacters(in: .whitespacesAndNewlines) == content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testCopyOutRelativeDestinationPath() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
try await f.withContainer(image: image) { name in
|
||||
let content = "relative dest"
|
||||
try f.doExec(name, cmd: ["sh", "-c", "echo -n '\(content)' > /tmp/relfile.txt"])
|
||||
try f.run(["copy", "\(name):/tmp/relfile.txt", "./"], currentDirectory: f.testDir).check()
|
||||
let result = try String(contentsOfFile: f.testDir.appending("relfile.txt").string, encoding: .utf8)
|
||||
#expect(result == content)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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 ContainerizationExtras
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@Suite
|
||||
struct TestCLICreateCommand {
|
||||
@Test func testCreateArgsPassthrough() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
let name = "\(f.testID)-c"
|
||||
try f.doCreate(name: name, image: image, args: ["echo", "-n", "hello", "world"])
|
||||
try f.doRemove(name)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testCreateWithMACAddress() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
let name = "\(f.testID)-c"
|
||||
let expectedMAC = try MACAddress("02:42:ac:11:00:03")
|
||||
|
||||
try f.doCreate(name: name, image: image, networks: ["default,mac=\(expectedMAC)"])
|
||||
f.addCleanup { try? f.doStop(name) }
|
||||
try f.doStart(name)
|
||||
try await f.waitForContainerRunning(name)
|
||||
|
||||
let inspect = try f.inspectContainer(name)
|
||||
#expect(inspect.networks.count > 0, "expected at least one network attachment")
|
||||
let actualMAC = inspect.networks[0].macAddress?.description ?? "nil"
|
||||
#expect(
|
||||
actualMAC == expectedMAC.description,
|
||||
"expected MAC address \(expectedMAC), got \(actualMAC)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testPublishPortParserMaxPorts() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
let name = "\(f.testID)-c"
|
||||
var args: [String] = ["create", "--name", name]
|
||||
for i in 0..<64 {
|
||||
args += ["--publish", "127.0.0.1:\(8000 + i):\(9000 + i)"]
|
||||
}
|
||||
args += [image, "echo", "\"hello world\""]
|
||||
|
||||
let result = try f.run(args)
|
||||
f.addCleanup { try? f.doRemove(name) }
|
||||
#expect(result.status == 0, "expected create with 64 ports to succeed, stderr: \(result.error)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testPublishPortParserTooManyPorts() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
let name = "\(f.testID)-c"
|
||||
var args: [String] = ["create", "--name", name]
|
||||
for i in 0..<65 {
|
||||
args += ["--publish", "127.0.0.1:\(8000 + i):\(9000 + i)"]
|
||||
}
|
||||
args += [image, "echo", "\"hello world\""]
|
||||
|
||||
let result = try f.run(args)
|
||||
f.addCleanup { try? f.doRemove(name) }
|
||||
#expect(result.status != 0, "expected create with 65 ports to fail")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testCreateWithFQDNName() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
// Prefix with testID to avoid name collisions; hostname is the first FQDN component.
|
||||
let name = "\(f.testID).example.com"
|
||||
let expectedHostname = f.testID
|
||||
|
||||
try f.doCreate(name: name, image: image)
|
||||
f.addCleanup { try? f.doStop(name) }
|
||||
try f.doStart(name)
|
||||
try await f.waitForContainerRunning(name)
|
||||
|
||||
let inspect = try f.inspectContainer(name)
|
||||
let attachmentHostname = inspect.networks.first?.hostname ?? ""
|
||||
let gotHostname =
|
||||
attachmentHostname
|
||||
.split(separator: ".", maxSplits: 1, omittingEmptySubsequences: true)
|
||||
.first
|
||||
.map { String($0) } ?? attachmentHostname
|
||||
#expect(
|
||||
gotHostname == expectedHostname,
|
||||
"expected hostname '\(expectedHostname)' from FQDN '\(name)', got '\(gotHostname)'")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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 TestCLIExecCommand {
|
||||
@Test func testCreateExecCommand() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
let name = "\(f.testID)-c"
|
||||
try f.doCreate(name: name, image: image)
|
||||
f.addCleanup { try? f.doStop(name) }
|
||||
try f.doStart(name)
|
||||
try await f.waitForContainerRunning(name)
|
||||
let uname = try f.doExec(name, cmd: ["uname"])
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
#expect(uname == "Linux", "expected OS to be Linux, got \(uname)")
|
||||
try f.doStop(name)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testExecDetach() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
let name = "\(f.testID)-c"
|
||||
try f.doCreate(name: name, image: image)
|
||||
f.addCleanup { try? f.doStop(name) }
|
||||
try f.doStart(name)
|
||||
try await f.waitForContainerRunning(name)
|
||||
|
||||
let output = try f.doExec(name, cmd: ["sh", "-c", "touch /tmp/detach_test_marker"], detach: true)
|
||||
try #require(
|
||||
output.trimmingCharacters(in: .whitespacesAndNewlines) == name,
|
||||
"exec --detach should print the container name")
|
||||
|
||||
let ls = try f.doExec(name, cmd: ["ls", "/"])
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
try #require(ls.contains("tmp"), "container should still be running after detached exec")
|
||||
|
||||
// Retry until the detached process creates the marker file.
|
||||
var markerFound = false
|
||||
for _ in 0..<3 {
|
||||
let result = try f.run(["exec", name, "test", "-f", "/tmp/detach_test_marker"])
|
||||
if result.status == 0 {
|
||||
markerFound = true
|
||||
break
|
||||
}
|
||||
try await Task.sleep(for: .seconds(1))
|
||||
}
|
||||
try #require(markerFound, "marker file should be created by detached process within 3 seconds")
|
||||
|
||||
try f.doStop(name)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testExecDetachProcessRunning() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
let name = "\(f.testID)-c"
|
||||
try f.doCreate(name: name, image: image)
|
||||
f.addCleanup { try? f.doStop(name) }
|
||||
try f.doStart(name)
|
||||
try await f.waitForContainerRunning(name)
|
||||
|
||||
let output = try f.doExec(name, cmd: ["sleep", "10"], detach: true)
|
||||
try #require(
|
||||
output.trimmingCharacters(in: .whitespacesAndNewlines) == name,
|
||||
"exec --detach should print the container name")
|
||||
|
||||
let ps = try f.doExec(name, cmd: ["ps", "aux"])
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
try #require(ps.contains("sleep 10"), "detached 'sleep 10' should appear in ps output")
|
||||
|
||||
try f.doStop(name)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testExecOnExitingContainer() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
let name = "\(f.testID)-c"
|
||||
// sh exits immediately in detached mode with no stdin; container stops on its own.
|
||||
try f.doLongRun(name: name, image: image, containerArgs: ["sh"], autoRemove: false)
|
||||
f.addCleanup { try? f.doRemove(name) }
|
||||
try await Task.sleep(for: .seconds(1))
|
||||
|
||||
try f.doStart(name)
|
||||
let execResult = try f.run(["exec", name, "sleep", "infinity"])
|
||||
if execResult.status != 0 {
|
||||
#expect(
|
||||
execResult.error.contains("is not running")
|
||||
|| execResult.error.contains("failed to create process"),
|
||||
"expected 'not running' error, got: \(execResult.error)")
|
||||
}
|
||||
|
||||
try await Task.sleep(for: .seconds(1))
|
||||
// Container should still exist even if exec failed.
|
||||
_ = try f.getContainerStatus(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 ContainerizationArchive
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@Suite
|
||||
struct TestCLIExportCommand {
|
||||
@Test func testExportCommand() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
try await f.withContainer(image: image, autoRemove: false) { name in
|
||||
let mustBeInImage = "must-be-in-image"
|
||||
try f.doExec(name, cmd: ["sh", "-c", "echo \(mustBeInImage) > /foo"])
|
||||
try f.doExec(name, cmd: ["sh", "-c", "mkdir -p /parent/child"])
|
||||
let hardlinkMustRemain = "hardlink-must-remain"
|
||||
try f.doExec(name, cmd: ["sh", "-c", "echo \(hardlinkMustRemain) > /parent/child/bar"])
|
||||
try f.doExec(name, cmd: ["sh", "-c", "ln /parent/child/bar /bar"])
|
||||
let symlinkMustRemain = "symlink-must-remain"
|
||||
try f.doExec(name, cmd: ["sh", "-c", "echo \(symlinkMustRemain) > /parent/child/baz"])
|
||||
try f.doExec(name, cmd: ["sh", "-c", "ln /parent/child/baz /baz"])
|
||||
|
||||
try f.doStop(name)
|
||||
|
||||
let exportPath = f.testDir.appending("export.tar")
|
||||
try f.doExport(name, to: exportPath)
|
||||
|
||||
let exportURL = URL(filePath: exportPath.string)
|
||||
let attrs = try FileManager.default.attributesOfItem(atPath: exportPath.string)
|
||||
let fileSize = attrs[.size] as! UInt64
|
||||
#expect(fileSize > 0)
|
||||
|
||||
// TODO: verify foo bar baz are in tar file.
|
||||
let reader = try ArchiveReader(file: exportURL)
|
||||
let (foo, fooData) = try reader.extractFile(path: "/foo")
|
||||
#expect(foo.fileType == .regular)
|
||||
#expect(String(data: fooData, encoding: .utf8)?.starts(with: mustBeInImage) ?? false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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
|
||||
|
||||
/// Tests that stop, kill, and delete return errors for non-existent containers.
|
||||
@Suite
|
||||
struct TestCLINotFound {
|
||||
|
||||
@Test func testStopNonExistentContainer() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let result = try f.run(["stop", "does-not-exist"])
|
||||
#expect(result.status != 0, "stop should fail for a non-existent container")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testKillNonExistentContainer() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let result = try f.run(["kill", "does-not-exist"])
|
||||
#expect(result.status != 0, "kill should fail for a non-existent container")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testDeleteNonExistentContainer() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let result = try f.run(["delete", "does-not-exist"])
|
||||
#expect(result.status != 0, "delete should fail for a non-existent container")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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
|
||||
|
||||
/// Serial prune tests — `container prune` affects all stopped containers regardless of name.
|
||||
@Suite(.serialized)
|
||||
struct TestCLIPruneCommandSerial {
|
||||
@Test func testContainerPruneNoContainers() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
// Establish empty state — the serial global pass runs after the concurrent
|
||||
// pass, and any test that failed mid-cleanup could leave stopped containers
|
||||
// that would break the "reclaimed zero" assertion. Machine-backing containers
|
||||
// are excluded from `container delete --all` by design so this doesn't
|
||||
// interfere with the machine plugin.
|
||||
_ = try? f.run(["delete", "--all", "--force"])
|
||||
|
||||
let result = try f.run(["prune"]).check()
|
||||
#expect(result.error.contains("Reclaimed Zero KB in disk space"), "should show no containers message")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testContainerPruneStoppedContainers() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = ContainerFixture.warmupImages[0]
|
||||
if try !f.isImagePresent(image) { try f.doPull(image) }
|
||||
|
||||
// One running container that must survive the prune.
|
||||
try await f.withContainer(image: image, tag: "running", containerArgs: ["sleep", "3600"]) { npcName in
|
||||
// Two containers to stop and prune.
|
||||
try await f.withContainer(
|
||||
image: image, tag: "prune0", containerArgs: ["sleep", "3600"], autoRemove: false
|
||||
) { pc0Name in
|
||||
try await f.withContainer(
|
||||
image: image, tag: "prune1", containerArgs: ["sleep", "3600"], autoRemove: false
|
||||
) { pc1Name in
|
||||
let pc0Id = try f.getContainerId(pc0Name)
|
||||
let pc1Id = try f.getContainerId(pc1Name)
|
||||
|
||||
try f.doStop(pc0Name)
|
||||
try f.doStop(pc1Name)
|
||||
|
||||
// Poll until both containers reach stopped state.
|
||||
let deadline = Date().addingTimeInterval(30)
|
||||
while true {
|
||||
let s0 = try f.getContainerStatus(pc0Name)
|
||||
let s1 = try f.getContainerStatus(pc1Name)
|
||||
if s0 == "stopped" && s1 == "stopped" { break }
|
||||
guard Date() < deadline else {
|
||||
throw CommandError.executionFailed(
|
||||
"Timeout waiting for containers to stop: pc0=\(s0), pc1=\(s1)")
|
||||
}
|
||||
try await Task.sleep(for: .milliseconds(200))
|
||||
}
|
||||
|
||||
let result = try f.run(["prune"]).check()
|
||||
#expect(
|
||||
result.output.contains(pc0Id) && result.output.contains(pc1Id),
|
||||
"prune output should list stopped container IDs")
|
||||
#expect(
|
||||
!result.error.contains("Reclaimed Zero KB in disk space"),
|
||||
"reclaimed space should not be zero")
|
||||
|
||||
let npcStatus = try f.getContainerStatus(npcName)
|
||||
#expect(npcStatus == "running", "running container should not be pruned")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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
|
||||
|
||||
/// Concurrent removal tests — all use testID-scoped names.
|
||||
@Suite
|
||||
struct TestCLIRemove {
|
||||
@Test func testDeleteStopped() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
let name = "\(f.testID)-c"
|
||||
// create without --rm so the container persists after being stopped
|
||||
try f.doCreate(name: name, image: image)
|
||||
try f.doRemove(name)
|
||||
let result = try f.run(["inspect", name])
|
||||
#expect(result.status != 0, "container should not exist after delete")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testDeleteAlias() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
let name = "\(f.testID)-c"
|
||||
try f.doCreate(name: name, image: image)
|
||||
try f.run(["rm", name]).check("rm alias failed")
|
||||
let result = try f.run(["inspect", name])
|
||||
#expect(result.status != 0, "container should not exist after rm")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testDeleteForceRunning() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
try await f.withContainer(image: image) { name in
|
||||
try f.doRemove(name, force: true)
|
||||
let result = try f.run(["inspect", name])
|
||||
#expect(result.status != 0, "container should not exist after force delete")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testDeleteNoArgs() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let result = try f.run(["delete"])
|
||||
#expect(result.status != 0, "delete with no args should fail")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testDeleteExplicitIdsConflictWithAll() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let result = try f.run(["delete", "--all", "some-container"])
|
||||
#expect(result.status != 0, "delete --all with explicit ID should fail")
|
||||
#expect(result.error.contains("conflict"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testDeleteDuplicateIds() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
let name = "\(f.testID)-c"
|
||||
try f.doCreate(name: name, image: image)
|
||||
f.addCleanup { try f.doRemoveIfExists(name, force: true, ignoreFailure: true) }
|
||||
let result = try f.run(["delete", name, name])
|
||||
#expect(result.status == 0, "delete with duplicate IDs should succeed, stderr: \(result.error)")
|
||||
let lines = result.output.split(separator: "\n").filter { $0.contains(name) }
|
||||
#expect(lines.count == 1, "container should be deleted exactly once, got \(lines.count) lines")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testInspectMissingContainerFails() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let result = try f.run(["inspect", "definitely-missing-container"])
|
||||
#expect(result.status != 0, "inspect of missing container should fail")
|
||||
#expect(result.error.contains("container not found"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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
|
||||
|
||||
/// Serial removal tests that use `delete --all` and affect global container state.
|
||||
@Suite(.serialized)
|
||||
struct TestCLIRemoveSerial {
|
||||
@Test func testDeleteAllStopped() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = ContainerFixture.warmupImages[0]
|
||||
if try !f.isImagePresent(image) { try f.doPull(image) }
|
||||
let name1 = "\(f.testID)-c1"
|
||||
let name2 = "\(f.testID)-c2"
|
||||
try f.doCreate(name: name1, image: image)
|
||||
f.addCleanup { try f.doRemoveIfExists(name1, ignoreFailure: true) }
|
||||
try f.doCreate(name: name2, image: image)
|
||||
f.addCleanup { try f.doRemoveIfExists(name2, ignoreFailure: true) }
|
||||
|
||||
try f.run(["delete", "--all"]).check()
|
||||
|
||||
#expect(try f.run(["inspect", name1]).status != 0, "name1 should be deleted")
|
||||
#expect(try f.run(["inspect", name2]).status != 0, "name2 should be deleted")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testDeleteAllSkipsRunning() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = ContainerFixture.warmupImages[0]
|
||||
if try !f.isImagePresent(image) { try f.doPull(image) }
|
||||
let runningName = "\(f.testID)-running"
|
||||
let stoppedName = "\(f.testID)-stopped"
|
||||
|
||||
try f.doLongRun(name: runningName, image: image, autoRemove: false)
|
||||
f.addCleanup {
|
||||
try? f.doStop(runningName)
|
||||
try? f.doRemove(runningName)
|
||||
}
|
||||
try f.doCreate(name: stoppedName, image: image)
|
||||
f.addCleanup { try f.doRemoveIfExists(stoppedName, ignoreFailure: true) }
|
||||
|
||||
try f.run(["delete", "--all"]).check()
|
||||
|
||||
#expect(try f.getContainerStatus(runningName) == "running", "running container should survive delete --all")
|
||||
#expect(try f.run(["inspect", stoppedName]).status != 0, "stopped container should be deleted")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testDeleteAllForce() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = ContainerFixture.warmupImages[0]
|
||||
if try !f.isImagePresent(image) { try f.doPull(image) }
|
||||
let name = "\(f.testID)-c"
|
||||
try f.doLongRun(name: name, image: image, autoRemove: false)
|
||||
f.addCleanup { try f.doRemoveIfExists(name, force: true, ignoreFailure: true) }
|
||||
|
||||
try f.run(["delete", "--all", "--force"]).check()
|
||||
|
||||
#expect(try f.run(["inspect", name]).status != 0, "container should be deleted by --force")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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 TestCLIRmRaceCondition {
|
||||
@Test func testStopRmRace() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let name = "\(f.testID)-c"
|
||||
f.addCleanup { try f.doRemoveIfExists(name, force: true, ignoreFailure: true) }
|
||||
|
||||
try f.doCreate(name: name)
|
||||
try f.doStart(name)
|
||||
try await f.waitForContainerRunning(name)
|
||||
try f.doStop(name)
|
||||
|
||||
// Immediately attempt removal — both outcomes are valid:
|
||||
// 1. Success: race condition prevention working perfectly
|
||||
// 2. "not yet stopped" error: race detected and controlled
|
||||
var raceConditionPrevented = false
|
||||
var raceConditionDetected = false
|
||||
|
||||
do {
|
||||
try f.doRemove(name)
|
||||
raceConditionPrevented = true
|
||||
} catch CommandError.nonZeroExit(_, let message) {
|
||||
if message.contains("is not yet stopped and can not be deleted") {
|
||||
raceConditionDetected = true
|
||||
} else if message.contains("not found")
|
||||
|| message.contains("failed to delete one or more containers")
|
||||
{
|
||||
raceConditionPrevented = true
|
||||
} else {
|
||||
Issue.record("Unexpected error message: \(message)")
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
Issue.record("Unexpected error type: \(error)")
|
||||
return
|
||||
}
|
||||
|
||||
#expect(
|
||||
raceConditionPrevented || raceConditionDetected,
|
||||
"Expected either immediate success (race prevented) or controlled failure (race detected)"
|
||||
)
|
||||
|
||||
if raceConditionPrevented { return }
|
||||
|
||||
// Race detected — wait for background cleanup then retry with backoff.
|
||||
try await Task.sleep(for: .seconds(2))
|
||||
|
||||
var attempts = 0
|
||||
let maxAttempts = 5
|
||||
while attempts < maxAttempts {
|
||||
guard (try? f.getContainerStatus(name)) != nil else { break }
|
||||
do {
|
||||
try f.doRemove(name)
|
||||
break
|
||||
} catch CommandError.nonZeroExit(_, let message) {
|
||||
if message.contains("not found") { break }
|
||||
guard attempts < maxAttempts - 1 else {
|
||||
throw CommandError.executionFailed(
|
||||
"Failed to remove container after \(maxAttempts) attempts: \(message)")
|
||||
}
|
||||
let delay = 1 << attempts
|
||||
try await Task.sleep(for: .seconds(delay))
|
||||
attempts += 1
|
||||
} catch {
|
||||
guard attempts < maxAttempts - 1 else { throw error }
|
||||
let delay = 1 << attempts
|
||||
try await Task.sleep(for: .seconds(delay))
|
||||
attempts += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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 ContainerResource
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@Suite
|
||||
struct TestCLIStatsCommand {
|
||||
@Test func testStatsNoStreamJSONFormat() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
try await f.withContainer(image: image) { name in
|
||||
let result = try f.run(["stats", "--format", "json", "--no-stream", name]).check()
|
||||
let stats = try JSONDecoder().decode([ContainerStats].self, from: result.outputData)
|
||||
#expect(stats.count == 1, "expected stats for one container")
|
||||
#expect(stats[0].id == name, "container ID should match")
|
||||
let memoryUsageBytes = try #require(stats[0].memoryUsageBytes)
|
||||
let numProcesses = try #require(stats[0].numProcesses)
|
||||
#expect(memoryUsageBytes > 0, "memory usage should be non-zero")
|
||||
#expect(numProcesses >= 1, "should have at least one process")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testStatsIdleCPUPercentage() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
try await f.withContainer(image: image, containerArgs: ["sleep", "3600"]) { name in
|
||||
let result = try f.run(["stats", "--no-stream", name]).check()
|
||||
let lines = result.output.components(separatedBy: .newlines)
|
||||
#expect(lines.count >= 2, "should have at least header and one data row")
|
||||
let dataLine = try #require(lines.first { $0.contains(name) }, "should find container data row")
|
||||
let columns = dataLine.split(separator: " ").filter { !$0.isEmpty }
|
||||
#expect(columns.count >= 2, "should have at least 2 columns")
|
||||
let cpuString = String(columns[1])
|
||||
#expect(cpuString.hasSuffix("%"), "CPU column should end with %")
|
||||
let cpuValue = try #require(Double(cpuString.dropLast()), "should parse CPU percentage")
|
||||
#expect(cpuValue < 5.0, "idle container CPU should be < 5%, got \(cpuValue)%")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testStatsHighCPUPercentage() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
try await f.withContainer(image: image, containerArgs: ["sh", "-c", "while true; do :; done"]) { name in
|
||||
let result = try f.run(["stats", "--no-stream", name]).check()
|
||||
let lines = result.output.components(separatedBy: .newlines)
|
||||
#expect(lines.count >= 2, "should have at least header and one data row")
|
||||
let dataLine = try #require(lines.first { $0.contains(name) }, "should find container data row")
|
||||
let columns = dataLine.split(separator: " ").filter { !$0.isEmpty }
|
||||
#expect(columns.count >= 2, "should have at least 2 columns")
|
||||
let cpuString = String(columns[1])
|
||||
#expect(cpuString.hasSuffix("%"), "CPU column should end with %")
|
||||
let cpuValue = try #require(Double(cpuString.dropLast()), "should parse CPU percentage")
|
||||
#expect(cpuValue > 50.0, "busy container CPU should be > 50%, got \(cpuValue)%")
|
||||
#expect(cpuValue < 150.0, "single busy loop should not exceed 150%, got \(cpuValue)%")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testStatsTableFormat() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
try await f.withContainer(image: image) { name in
|
||||
let result = try f.run(["stats", "--no-stream", name]).check()
|
||||
#expect(result.output.contains("Container ID"), "output should contain table header")
|
||||
#expect(result.output.contains("Cpu %"), "output should contain CPU column")
|
||||
#expect(result.output.contains("Memory Usage"), "output should contain Memory column")
|
||||
#expect(result.output.contains(name), "output should contain container name")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testStatsAllContainers() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
// Run two containers simultaneously so both appear in the global stats snapshot.
|
||||
try await f.withContainer(image: image, tag: "c1") { name1 in
|
||||
try await f.withContainer(image: image, tag: "c2") { name2 in
|
||||
let result = try f.run(["stats", "--format", "json", "--no-stream"]).check()
|
||||
let stats = try JSONDecoder().decode([ContainerStats].self, from: result.outputData)
|
||||
try #require(stats.count >= 2, "should have stats for at least 2 containers")
|
||||
let ids = stats.map { $0.id }
|
||||
#expect(ids.contains(name1), "should include first container")
|
||||
#expect(ids.contains(name2), "should include second container")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testStatsNonExistentContainer() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let result = try f.run(["stats", "--no-stream", "nonexistent-container-xyz"])
|
||||
#expect(result.status != 0, "stats should fail for non-existent container")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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 TestCLIStop {
|
||||
@Test func testStopWithExplicitSignal() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
try await f.withContainer(image: image, autoRemove: false) { name in
|
||||
try f.doStop(name, signal: "SIGTERM")
|
||||
#expect(try f.getContainerStatus(name) == "stopped")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testStopWithoutSignal() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
try await f.withContainer(image: image, autoRemove: false) { name in
|
||||
try f.doStop(name, signal: nil)
|
||||
#expect(try f.getContainerStatus(name) == "stopped")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testStopSignalInInspect() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
try await f.withContainer(image: image, autoRemove: false) { name in
|
||||
let inspect = try f.inspectContainer(name)
|
||||
// Alpine doesn't set a STOPSIGNAL, so this should be nil.
|
||||
#expect(inspect.configuration.stopSignal == nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testStopIdempotent() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
|
||||
try await f.withContainer(image: image, autoRemove: false) { name in
|
||||
try f.doStop(name, signal: "SIGKILL")
|
||||
#expect(try f.getContainerStatus(name) == "stopped")
|
||||
// Stopping an already-stopped container should not fail.
|
||||
try f.doStop(name, signal: "SIGKILL")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user