diff --git a/Sources/ContainerTestSupport/ContainerFixture+ImageHelpers.swift b/Sources/ContainerTestSupport/ContainerFixture+ImageHelpers.swift index 4dc3a199..ffdc48db 100644 --- a/Sources/ContainerTestSupport/ContainerFixture+ImageHelpers.swift +++ b/Sources/ContainerTestSupport/ContainerFixture+ImageHelpers.swift @@ -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() diff --git a/Sources/ContainerTestSupport/WarmupImage.swift b/Sources/ContainerTestSupport/WarmupImage.swift index 307f9aa4..069ed256 100644 --- a/Sources/ContainerTestSupport/WarmupImage.swift +++ b/Sources/ContainerTestSupport/WarmupImage.swift @@ -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") + } } diff --git a/Tests/IntegrationTests/Images/TestCLIImagePruneSerial.swift b/Tests/IntegrationTests/Images/TestCLIImagePruneSerial.swift index 40ea3644..5bd5632e 100644 --- a/Tests/IntegrationTests/Images/TestCLIImagePruneSerial.swift +++ b/Tests/IntegrationTests/Images/TestCLIImagePruneSerial.swift @@ -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") diff --git a/Tests/IntegrationTests/System/TestCLISystemDFSerial.swift b/Tests/IntegrationTests/System/TestCLISystemDFSerial.swift index fbf3a97e..26149e9d 100644 --- a/Tests/IntegrationTests/System/TestCLISystemDFSerial.swift +++ b/Tests/IntegrationTests/System/TestCLISystemDFSerial.swift @@ -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) diff --git a/Tests/IntegrationTests/Warmup/ImageWarmup.swift b/Tests/IntegrationTests/Warmup/ImageWarmup.swift index aa22d7bf..8399a16b 100644 --- a/Tests/IntegrationTests/Warmup/ImageWarmup.swift +++ b/Tests/IntegrationTests/Warmup/ImageWarmup.swift @@ -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) } } }