Files
container/Tests/CLITests/TestCLINoParallelCases.swift
T
Noah Thornton e3c49803a0 Move to TOML configuration for defaults (#1425)
- Discussion topic #1336.
- This change migrates away from using `UserDefaults`,
  instead providing a TOML configuration mechanism for
  user configurable settings. All existing system property
  settings keys are supported in the new configuration
  file. However, users will have to migrate any settings
  they have configured in the `UserDefaults` into TOML
  for these settings to take effect.
- Breaking changes:
  * `container system property get` is removed in favor of
    users directly utilizing `container system property list --format toml | jq<>`.
  * `container system property set` is removed since the TOML
    configuration is effectively immutable during the lifetime of the
    `container` daemon. Uses can edit the TOML they have in their home
    directory, however no changes will take effect until the daemon is
    restarted via `container system stop && container system start`
* `container system property list --format table` is removed as
    generating tabular format is non-trivial and the new TOML format is
    intended to be human readable
2026-05-04 12:04:24 -07:00

305 lines
12 KiB
Swift

//===----------------------------------------------------------------------===//
// Copyright © 2025-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 ContainerAPIClient
import ContainerizationExtras
import Foundation
import Testing
/// Tests that need total control over environment to avoid conflicts.
@Suite(.serialSuites, .serialized)
class TestCLINoParallelCases: CLITest {
func getTestName() -> String {
Test.current!.name.trimmingCharacters(in: ["(", ")"]).lowercased()
}
func getLowercasedTestName() -> String {
getTestName().lowercased()
}
@Test func testImageSingleConcurrentDownload() throws {
// removing this image during parallel tests breaks stuff!
_ = try? run(arguments: ["image", "rm", alpine])
defer { _ = try? run(arguments: ["image", "rm", "--all"]) }
do {
try doPull(imageName: alpine, args: ["--max-concurrent-downloads", "1"])
let imagePresent = try isImagePresent(targetImage: alpine)
#expect(imagePresent, "Expected image to be pulled with maxConcurrentDownloads=1")
} catch {
Issue.record("failed to pull image with maxConcurrentDownloads flag: \(error)")
return
}
}
@Test func testImageManyConcurrentDownloads() throws {
// removing this image during parallel tests breaks stuff!
_ = try? run(arguments: ["image", "rm", alpine])
defer { _ = try? run(arguments: ["image", "rm", "--all"]) }
do {
try doPull(imageName: alpine, args: ["--max-concurrent-downloads", "64"])
let imagePresent = try isImagePresent(targetImage: alpine)
#expect(imagePresent, "Expected image to be pulled with maxConcurrentDownloads=64")
} catch {
Issue.record("failed to pull image with maxConcurrentDownloads flag: \(error)")
return
}
}
@Test func testImagePruneNoImages() throws {
// Prune with no images should succeed
_ = try? run(arguments: ["image", "rm", "--all"])
let (_, output, error, status) = try run(arguments: ["image", "prune"])
if status != 0 {
throw CLIError.executionFailed("image prune failed: \(error)")
}
#expect(output.contains("Zero KB"), "should show no space reclaimed")
}
@Test func testImagePruneUnusedImages() throws {
// 1. Pull the images
_ = try? run(arguments: ["image", "rm", "--all"])
defer { _ = try? run(arguments: ["image", "rm", "--all"]) }
try doPull(imageName: alpine)
try doPull(imageName: busybox)
// 2. Verify the images are present
let alpinePresent = try isImagePresent(targetImage: alpine)
#expect(alpinePresent, "expected to see image \(alpine) pulled")
let busyBoxPresent = try isImagePresent(targetImage: busybox)
#expect(busyBoxPresent, "expected to see image \(busybox) pulled")
// 3. Prune with the -a flag should remove all unused images
let (_, output, error, status) = try run(arguments: ["image", "prune", "-a"])
if status != 0 {
throw CLIError.executionFailed("image prune failed: \(error)")
}
#expect(output.contains(alpine), "should prune alpine image")
#expect(output.contains(busybox), "should prune busybox image")
// 4. Verify the images are gone
let alpineRemoved = try !isImagePresent(targetImage: alpine)
#expect(alpineRemoved, "expected image \(alpine) to be removed")
let busyboxRemoved = try !isImagePresent(targetImage: busybox)
#expect(busyboxRemoved, "expected image \(busybox) to be removed")
}
@Test func testImagePruneDanglingImages() throws {
let name = getTestName()
let containerName = "\(name)_container"
// 1. Pull the images
_ = try? run(arguments: ["image", "rm", "--all"])
defer { _ = try? run(arguments: ["image", "rm", "--all"]) }
_ = try? run(arguments: ["rm", "--all", "--force"])
defer { _ = try? run(arguments: ["rm", "--all", "--force"]) }
try doPull(imageName: alpine)
try doPull(imageName: busybox)
// 2. Verify the images are present
let alpinePresent = try isImagePresent(targetImage: alpine)
#expect(alpinePresent, "expected to see image \(alpine) pulled")
let busyBoxPresent = try isImagePresent(targetImage: busybox)
#expect(busyBoxPresent, "expected to see image \(busybox) pulled")
// 3. Create a running container based on alpine
try doLongRun(
name: containerName,
image: alpine
)
try waitForContainerRunning(containerName)
// 4. Prune should only remove the dangling image
let (_, output, error, status) = try run(arguments: ["image", "prune", "-a"])
if status != 0 {
throw CLIError.executionFailed("image prune failed: \(error)")
}
#expect(output.contains(busybox), "should prune busybox image")
// 5. Verify the busybox image is gone
let busyboxRemoved = try !isImagePresent(targetImage: busybox)
#expect(busyboxRemoved, "expected image \(busybox) to be removed")
// 6. Verify the alpine image still exists
let alpineStillPresent = try isImagePresent(targetImage: alpine)
#expect(alpineStillPresent, "expected image \(alpine) to remain")
}
@available(macOS 26, *)
@Test func testNetworkPruneNoNetworks() throws {
// Ensure the testnetworkcreateanduse network is deleted
// Clean up is necessary for testing prune with no networks
doNetworkDeleteIfExists(name: "testnetworkcreateanduse")
// Prune with no networks should succeed
let (_, _, _, statusBefore) = try run(arguments: ["network", "list", "--quiet"])
#expect(statusBefore == 0)
let (_, output, error, status) = try run(arguments: ["network", "prune"])
if status != 0 {
throw CLIError.executionFailed("network prune failed: \(error)")
}
#expect(output.isEmpty, "should show no networks pruned")
}
@available(macOS 26, *)
@Test func testNetworkPruneUnusedNetworks() throws {
let name = getTestName()
let network1 = "\(name)_1"
let network2 = "\(name)_2"
// Clean up any existing resources from previous runs
doNetworkDeleteIfExists(name: network1)
doNetworkDeleteIfExists(name: network2)
defer {
doNetworkDeleteIfExists(name: network1)
doNetworkDeleteIfExists(name: network2)
}
try doNetworkCreate(name: network1)
try doNetworkCreate(name: network2)
// Verify networks are created
let (_, listBefore, _, statusBefore) = try run(arguments: ["network", "list", "--quiet"])
#expect(statusBefore == 0)
#expect(listBefore.contains(network1))
#expect(listBefore.contains(network2))
// Prune should remove both
let (_, output, error, status) = try run(arguments: ["network", "prune"])
if status != 0 {
throw CLIError.executionFailed("network prune failed: \(error)")
}
#expect(output.contains(network1), "should prune network1")
#expect(output.contains(network2), "should prune network2")
// Verify networks are gone
let (_, listAfter, _, statusAfter) = try run(arguments: ["network", "list", "--quiet"])
#expect(statusAfter == 0)
#expect(!listAfter.contains(network1), "network1 should be pruned")
#expect(!listAfter.contains(network2), "network2 should be pruned")
}
@available(macOS 26, *)
@Test func testNetworkPruneSkipsNetworksInUse() throws {
let name = getTestName()
let containerName = "\(name)_c1"
let networkInUse = "\(name)_inuse"
let networkUnused = "\(name)_unused"
// Clean up any existing resources from previous runs
try? doStop(name: containerName)
try? doRemove(name: containerName)
doNetworkDeleteIfExists(name: networkInUse)
doNetworkDeleteIfExists(name: networkUnused)
defer {
try? doStop(name: containerName)
try? doRemove(name: containerName)
doNetworkDeleteIfExists(name: networkInUse)
doNetworkDeleteIfExists(name: networkUnused)
}
try doNetworkCreate(name: networkInUse)
try doNetworkCreate(name: networkUnused)
// Verify networks are created
let (_, listBefore, _, statusBefore) = try run(arguments: ["network", "list", "--quiet"])
#expect(statusBefore == 0)
#expect(listBefore.contains(networkInUse))
#expect(listBefore.contains(networkUnused))
// Creation of container with network connection
let port = UInt16.random(in: 50000..<60000)
try doLongRun(
name: containerName,
image: "docker.io/library/python:alpine",
args: ["--network", networkInUse],
containerArgs: ["python3", "-m", "http.server", "--bind", "0.0.0.0", "\(port)"]
)
try waitForContainerRunning(containerName)
let container = try inspectContainer(containerName)
#expect(container.networks.count > 0)
// Prune should only remove the unused network
let (_, _, error, status) = try run(arguments: ["network", "prune"])
if status != 0 {
throw CLIError.executionFailed("network prune failed: \(error)")
}
// Verify in-use network still exists
let (_, listAfter, _, statusAfter) = try run(arguments: ["network", "list", "--quiet"])
#expect(statusAfter == 0)
#expect(listAfter.contains(networkInUse), "network in use should NOT be pruned")
#expect(!listAfter.contains(networkUnused), "unused network should be pruned")
}
@available(macOS 26, *)
@Test func testNetworkPruneSkipsNetworkAttachedToStoppedContainer() async throws {
let name = getTestName()
let containerName = "\(name)_c1"
let networkName = "\(name)"
// Clean up any existing resources from previous runs
try? doStop(name: containerName)
try? doRemove(name: containerName)
doNetworkDeleteIfExists(name: networkName)
defer {
try? doStop(name: containerName)
try? doRemove(name: containerName)
doNetworkDeleteIfExists(name: networkName)
}
try doNetworkCreate(name: networkName)
// Creation of container with network connection
let port = UInt16.random(in: 50000..<60000)
try doLongRun(
name: containerName,
image: "docker.io/library/python:alpine",
args: ["--network", networkName],
containerArgs: ["python3", "-m", "http.server", "--bind", "0.0.0.0", "\(port)"]
)
try await Task.sleep(for: .seconds(1))
// Prune should NOT remove the network (container exists, even if stopped)
let (_, _, error, status) = try run(arguments: ["network", "prune"])
if status != 0 {
throw CLIError.executionFailed("network prune failed: \(error)")
}
let (_, listAfter, _, statusAfter) = try run(arguments: ["network", "list", "--quiet"])
#expect(statusAfter == 0)
#expect(listAfter.contains(networkName), "network attached to stopped container should NOT be pruned")
try? doStop(name: containerName)
try? doRemove(name: containerName)
let (_, _, error2, status2) = try run(arguments: ["network", "prune"])
if status2 != 0 {
throw CLIError.executionFailed("network prune failed: \(error2)")
}
// Verify network is gone
let (_, listFinal, _, statusFinal) = try run(arguments: ["network", "list", "--quiet"])
#expect(statusFinal == 0)
#expect(!listFinal.contains(networkName), "network should be pruned after container is deleted")
}
}