Integration test: cache warmup image tarfiles. (#2074)

- When pulling warmup images for concurrent tests, save
  the images to a cache directory under the application root.
- Serial tests that aren't testing pull can save time by restoring
  a cached warmup image.
This commit is contained in:
J Logan
2026-08-05 11:17:20 -07:00
committed by GitHub
parent bc50fcb593
commit 60612eef51
5 changed files with 52 additions and 8 deletions
@@ -15,6 +15,7 @@
//===----------------------------------------------------------------------===//
import Foundation
import SystemPackage
import Testing
// MARK: - Image inspect types
@@ -69,6 +70,27 @@ extension ContainerFixture {
try run(args).check()
}
/// Saves `image` to ``WarmupImage/cacheTarPath``, overwriting any existing archive.
///
/// Called once per image by the `ImageWarmup` suite. No `--platform`/`--os`/`--arch`
/// is passed, so `image save` captures every platform the image supports.
public func cacheWarmupImage(_ image: WarmupImage) throws {
try FileManager.default.createDirectory(
atPath: WarmupImage.cacheDirectory.string, withIntermediateDirectories: true)
try run(["image", "save", "--output", image.cacheTarPath.string, image.rawValue])
.check("failed to cache \(image.rawValue)")
}
/// Reloads `image` from its cached tar archive rather than pulling over the network.
///
/// Use this in serial tests to restore a warmup image after a destructive operation
/// (`image rm --all`, `image prune`) removes it from the store. Requires the
/// `ImageWarmup` suite to have already run and populated the cache.
public func restoreWarmupImage(_ image: WarmupImage) throws {
try run(["image", "load", "--input", image.cacheTarPath.string])
.check("failed to restore \(image.rawValue) from cache")
}
/// Returns the full inspect output for an image, including variant information.
public func doInspectImages(_ name: String) throws -> [ImageInspectOutput] {
let result = try run(["image", "inspect", name]).check()
@@ -14,10 +14,29 @@
// limitations under the License.
//===----------------------------------------------------------------------===//
import ContainerPersistence
import SystemPackage
/// Images preloaded by the ``ImageWarmup`` suite before concurrent tests run.
/// Add new commonly-used images here; the warmup pass pulls them in parallel.
public enum WarmupImage: String, CaseIterable, Sendable {
case alpine320 = "ghcr.io/linuxcontainers/alpine:3.20"
case alpine318 = "ghcr.io/linuxcontainers/alpine:3.18"
case busybox136 = "ghcr.io/containerd/busybox:1.36"
/// Directory under app-root holding OCI tar archives of each warmup image.
///
/// Living under app-root (rather than a scratch dir tied to a single
/// fixture) means the cache survives across the warmup/concurrent/serial
/// `swift test` invocations, which run as separate processes, and rides
/// along whatever process clears app-root between full test runs no
/// dedicated cleanup needed.
public static var cacheDirectory: FilePath {
PathUtils.BaseConfigPath.appRoot.basePath().appending("test-image-cache")
}
/// Path to this image's cached OCI tar archive.
public var cacheTarPath: FilePath {
Self.cacheDirectory.appending("\(self).tar")
}
}
@@ -59,8 +59,8 @@ struct TestCLIImagePruneSerial {
try? f.doRemoveImages()
f.addCleanup { try? f.doRemoveImages() }
try f.doPull(alpine)
try f.doPull(busybox)
try f.restoreWarmupImage(.alpine320)
try f.restoreWarmupImage(.busybox136)
#expect(try f.isImagePresent(alpine), "expected \(alpine) to be pulled")
#expect(try f.isImagePresent(busybox), "expected \(busybox) to be pulled")
@@ -84,8 +84,8 @@ struct TestCLIImagePruneSerial {
try? f.doRemoveImages()
}
try f.doPull(alpine)
try f.doPull(busybox)
try f.restoreWarmupImage(.alpine320)
try f.restoreWarmupImage(.busybox136)
#expect(try f.isImagePresent(alpine), "expected \(alpine) to be pulled")
#expect(try f.isImagePresent(busybox), "expected \(busybox) to be pulled")
@@ -38,7 +38,7 @@ struct TestCLISystemDFSerial {
@Test func imageDiskUsageIsPopulatedAfterPull() async throws {
try await ContainerFixture.with { f in
try withCleanImageStore(f) {
try f.doPull(self.alpine)
try f.restoreWarmupImage(.alpine320)
let stats = try systemDiskUsage(f)
#expect(stats.images.total >= 1)
#expect(stats.images.active == 0)
@@ -52,7 +52,7 @@ struct TestCLISystemDFSerial {
@Test func tagsDoNotDoubleCountImageStorage() async throws {
try await ContainerFixture.with { f in
try withCleanImageStore(f) {
try f.doPull(self.alpine)
try f.restoreWarmupImage(.alpine320)
let before = try systemDiskUsage(f)
try f.doImageTag(self.alpine, newName: "local/system-df-alpine:tag-one")
try f.doImageTag(self.alpine, newName: "local/system-df-alpine:tag-two")
@@ -69,7 +69,7 @@ struct TestCLISystemDFSerial {
try await ContainerFixture.with { f in
try withCleanImageStore(f) {
let baseline = try systemDiskUsage(f)
try f.doPull(self.alpine)
try f.restoreWarmupImage(.alpine320)
try f.doImageTag(self.alpine, newName: "local/system-df-alpine:delete-probe")
let beforeDelete = try systemDiskUsage(f)
@@ -20,13 +20,16 @@ import Testing
/// Pulls each image in ``WarmupImage`` in parallel before concurrent
/// integration tests run. The Makefile's warmup pass runs this suite first
/// so that ``ContainerFixture/copyWarmupImage(_:)`` can tag from a
/// pre-populated store rather than pulling on demand.
/// pre-populated store rather than pulling on demand, and so that
/// ``ContainerFixture/restoreWarmupImage(_:)`` has a cached tar archive to
/// reload from after a serial test wipes the image store.
@Suite
struct ImageWarmup {
@Test(arguments: WarmupImage.allCases)
func pull(image: WarmupImage) async throws {
try await ContainerFixture.with { f in
try f.run(["image", "pull", image.rawValue]).check("failed to pull \(image.rawValue)")
try f.cacheWarmupImage(image)
}
}
}