import Foundation import XCTest @testable import tart #if canImport(DiskImageKit) import DiskImageKit #endif final class VMStorageOCITests: XCTestCase { func testPopulateStandalonePushedImageCachesDiskAndManifest() throws { try withTemporaryTartHome { let source = try standaloneSource(diskData: Data("disk".utf8)) let manifest = try flatManifest() let name = try digestName(for: manifest) let storage = try VMStorageOCI() try storage.populate(name, from: source, manifest: manifest) let cached = try storage.open(name) XCTAssertTrue(cached.isStandalone) XCTAssertEqual(try Data(contentsOf: cached.diskURL), Data("disk".utf8)) XCTAssertEqual(try OCIManifest(fromJSON: Data(contentsOf: cached.manifestURL)), manifest) } } func testStackedCloneRequiresManifestForLegacyStandaloneCachedImage() throws { try withTemporaryTartHome { let manifest = try flatManifest() let name = try digestName(for: manifest) let storage = try VMStorageOCI() let record = try storage.create(name) try config().save(toURL: record.configURL) XCTAssertTrue(FileManager.default.createFile(atPath: record.nvramURL.path, contents: Data())) XCTAssertTrue(FileManager.default.createFile(atPath: record.diskURL.path, contents: Data())) XCTAssertTrue(try storage.hasUsableCachedImageForClone(name)) XCTAssertFalse(try storage.hasUsableCachedImageForClone(name, requireManifest: true)) } } func testCloneCacheCheckRejectsMissingOrWrongSizedStackedContent() throws { try withTemporaryTartHome { let baseData = Data("base".utf8) let overlayData = Data("overlay".utf8) let baseDigest = Digest.hash(baseData) let overlayDigest = Digest.hash(overlayData) let manifest = try stackedManifest( baseContentDigest: baseDigest, overlayContentDigest: overlayDigest, baseUncompressedSize: UInt64(baseData.count), overlayUncompressedSize: UInt64(overlayData.count) ) let name = try digestName(for: manifest) let storage = try VMStorageOCI() let record = try storage.create(name) try config().save(toURL: record.configURL) XCTAssertTrue(FileManager.default.createFile(atPath: record.nvramURL.path, contents: Data())) try manifest.toJSON().write(to: record.manifestURL) XCTAssertFalse(try storage.hasUsableCachedImageForClone(name)) let contentStore = try ContentStore() try installContent(baseData, contentDigest: baseDigest, into: contentStore) try installContent(overlayData, contentDigest: overlayDigest, into: contentStore) XCTAssertTrue(try storage.hasUsableCachedImageForClone(name)) try Data("bad".utf8).write(to: try contentStore.contentURL(for: overlayDigest)) XCTAssertFalse(try storage.hasUsableCachedImageForClone(name)) } } func testListIncludesStackedCachedImage() throws { try withTemporaryTartHome { let manifest = try stackedManifest() let name = try digestName(for: manifest) let storage = try VMStorageOCI() let record = try storage.create(name) try config().save(toURL: record.configURL) XCTAssertTrue(FileManager.default.createFile(atPath: record.nvramURL.path, contents: Data())) try manifest.toJSON().write(to: record.manifestURL) XCTAssertTrue(try storage.list().contains { $0.0 == name.description }) XCTAssertEqual(try record.diskSizeBytes(), 4096) XCTAssertNoThrow(try record.allocatedSizeBytes()) } } func testStackedCacheHitRequiresExpectedContentSizes() throws { try withTemporaryTartHome { let baseData = Data(repeating: 0x41, count: 10) let overlayData = Data(repeating: 0x42, count: 20) let baseDigest = Digest.hash(baseData) let overlayDigest = Digest.hash(overlayData) let manifest = try stackedManifest( baseContentDigest: baseDigest, overlayContentDigest: overlayDigest, baseUncompressedSize: 10, overlayUncompressedSize: 20 ) let name = try digestName(for: manifest) let storage = try VMStorageOCI() let record = try storage.create(name) try config().save(toURL: record.configURL) XCTAssertTrue(FileManager.default.createFile(atPath: record.nvramURL.path, contents: Data())) try manifest.toJSON().write(to: record.manifestURL) XCTAssertFalse(try storage.hasCompleteCachedImage(name, manifest: manifest)) XCTAssertEqual(try storage.requiredDiskStorageBytes(for: manifest), 30) let contentStore = try ContentStore() try installContent(baseData, contentDigest: baseDigest, into: contentStore) XCTAssertFalse(try storage.hasCompleteCachedImage(name, manifest: manifest)) XCTAssertEqual(try storage.requiredDiskStorageBytes(for: manifest), 20) try installContent(overlayData, contentDigest: overlayDigest, into: contentStore) XCTAssertTrue(try storage.hasCompleteCachedImage(name, manifest: manifest)) XCTAssertEqual(try storage.requiredDiskStorageBytes(for: manifest), 0) let overlayURL = try contentStore.contentURL(for: overlayDigest) try Data("corrupt".utf8).write(to: overlayURL) XCTAssertFalse(try storage.hasCompleteCachedImage(name, manifest: manifest)) XCTAssertEqual(try storage.requiredDiskStorageBytes(for: manifest), 20) } } func testStackedPullReusesPreviouslyPulledStandaloneDisk() throws { try withTemporaryTartHome { let diskData = Data([0]) let contentDigest = Digest.hash(diskData) let flatManifest = try flatManifest() let flatName = try digestName(for: flatManifest) let storage = try VMStorageOCI() let flatRecord = try storage.create(flatName) try config().save(toURL: flatRecord.configURL) XCTAssertTrue(FileManager.default.createFile(atPath: flatRecord.nvramURL.path, contents: Data())) try diskData.write(to: flatRecord.diskURL) try flatManifest.toJSON().write(to: flatRecord.manifestURL) let stackedManifest = try stackedManifest(baseContentDigest: contentDigest) XCTAssertNil(try ContentStore().existingContentURL(for: contentDigest)) try storage.reuseStandaloneDiskForStackedBaseIfPossible(stackedManifest) let reusedURL = try XCTUnwrap(try ContentStore().existingContentURL(for: contentDigest)) XCTAssertEqual(try Data(contentsOf: reusedURL), diskData) } } func testStackedPullDoesNotRehashInstalledBaseBeforeReuse() throws { try withTemporaryTartHome { let contentDigest = Digest.hash(Data("base".utf8)) let manifest = try stackedManifest(baseContentDigest: contentDigest) let contentURL = try ContentStore().contentURL(for: contentDigest) // Hashing this path would throw. Once an entry is published, this // fast path must trust its presence and let normal pull validation // repair unusable content later. try FileManager.default.createDirectory(at: contentURL, withIntermediateDirectories: false) XCTAssertNoThrow(try VMStorageOCI().reuseStandaloneDiskForStackedBaseIfPossible(manifest)) } } func testNewTagDoesNotValidateCachedStackBeforeLock() throws { try withTemporaryTartHome { let baseDigest = Digest.hash(Data("base".utf8)) let overlayDigest = Digest.hash(Data("overlay".utf8)) let manifest = try stackedManifest( baseContentDigest: baseDigest, overlayContentDigest: overlayDigest ) let digestName = try digestName(for: manifest) let tagName = RemoteName( host: digestName.host, namespace: digestName.namespace, reference: Reference(tag: "latest") ) let storage = try VMStorageOCI() let record = try storage.create(digestName) try config().save(toURL: record.configURL) XCTAssertTrue(FileManager.default.createFile(atPath: record.nvramURL.path, contents: Data())) try manifest.toJSON().write(to: record.manifestURL) // Hashing this directory as a disk file throws. A new tag must skip // validation until after it has taken the host lock. let contentURL = try ContentStore().contentURL(for: baseDigest) try FileManager.default.createDirectory(at: contentURL, withIntermediateDirectories: false) XCTAssertFalse(try storage.hasCompleteLinkedImage(tagName, digestName: digestName, manifest: manifest)) } } func testStandaloneLayerCacheIgnoresStackedCachedImages() async throws { try await withTemporaryTartHome { var targetManifest = try flatManifest() var stackedCandidateManifest = try stackedManifest() let sharedDiskSize = 2 * 1024 * 1024 * 1024 targetManifest.layers[1].size = sharedDiskSize stackedCandidateManifest.layers[1] = targetManifest.layers[1] let candidateName = try digestName(for: stackedCandidateManifest) let storage = try VMStorageOCI() let candidate = try storage.create(candidateName) try config().save(toURL: candidate.configURL) XCTAssertTrue(FileManager.default.createFile(atPath: candidate.nvramURL.path, contents: Data())) try stackedCandidateManifest.toJSON().write(to: candidate.manifestURL) let targetName = RemoteName( host: "example.com", namespace: "org/target", reference: Reference(digest: try targetManifest.digest()) ) let registry = try Registry(host: targetName.host, namespace: targetName.namespace) let layerCache = try await storage.chooseLocalLayerCache(targetName, targetManifest, registry) XCTAssertNil(layerCache) } } func testGCPrunesOnlyUnreferencedContent() throws { try withTemporaryTartHome { let contentStore = try ContentStore() let referenced = try installContent(Data("referenced".utf8), into: contentStore) let unreferenced = try installContent(Data("unreferenced".utf8), into: contentStore) let stacked = try VMStorageLocal().create("stacked") try config().save(toURL: stacked.configURL) XCTAssertTrue(FileManager.default.createFile(atPath: stacked.nvramURL.path, contents: Data())) XCTAssertTrue(FileManager.default.createFile(atPath: stacked.overlayURL.path, contents: Data())) try pinnedBaseManifest(contentDigest: referenced.digest).toJSON().write(to: stacked.manifestURL) try VMStorageOCI().gc() XCTAssertTrue(FileManager.default.fileExists(atPath: referenced.url.path)) XCTAssertFalse(FileManager.default.fileExists(atPath: unreferenced.url.path)) } } func testGCPrunesContentWithoutOCIStorageDirectory() throws { try withTemporaryTartHome { let contentStore = try ContentStore() let unreferenced = try installContent(Data("unreferenced".utf8), into: contentStore) let storage = try VMStorageOCI() XCTAssertFalse(FileManager.default.fileExists(atPath: storage.baseURL.path)) try storage.gc() XCTAssertFalse(FileManager.default.fileExists(atPath: unreferenced.url.path)) } } func testGCDoesNotPruneContentReferencedByInProgressManifest() throws { try withTemporaryTartHome { let contentStore = try ContentStore() let referenced = try installContent(Data("in-progress".utf8), into: contentStore) let temporaryVMDir = try VMDirectory.temporary() // Pull and clone publish the manifest before config, NVRAM, or a // writable overlay necessarily exist. try pinnedBaseManifest(contentDigest: referenced.digest).toJSON().write(to: temporaryVMDir.manifestURL) try VMStorageOCI().gc() XCTAssertTrue(FileManager.default.fileExists(atPath: referenced.url.path)) } } func testStalePrunableDoesNotDeleteNewlyReferencedContent() throws { try withTemporaryTartHome { let contentStore = try ContentStore() let content = try installContent(Data("new-reference".utf8), into: contentStore) let storage = try VMStorageOCI() let candidate = try XCTUnwrap(storage.prunables().first { $0.url.resolvingSymlinksInPath() == content.url.resolvingSymlinksInPath() }) // Simulate a clone or pull publishing its manifest after prune built // the candidate list but before deletion starts. let temporaryVMDir = try VMDirectory.temporary() try contentStore.withPruneLock { try pinnedBaseManifest(contentDigest: content.digest).toJSON().write(to: temporaryVMDir.manifestURL) } try candidate.delete() XCTAssertTrue(FileManager.default.fileExists(atPath: content.url.path)) } } func testVMDirectoryDeletionOfStackedOCIRecordWaitsForPruneLock() throws { try withTemporaryTartHome { let storage = try VMStorageOCI() let record = try createRecord(for: stackedManifest(), in: storage) try assertDeletionWaitsForPruneLock(record: record) { try record.delete() } } } func testStorageDeletionOfStackedOCIRecordWaitsForPruneLock() throws { try withTemporaryTartHome { let storage = try VMStorageOCI() let manifest = try stackedManifest() let name = try digestName(for: manifest) let record = try createRecord(for: manifest, in: storage) try assertDeletionWaitsForPruneLock(record: record) { try storage.delete(name) } } } func testStorageDeletionOfIncompleteManifestRecordWaitsForPruneLock() throws { try withTemporaryTartHome { let storage = try VMStorageOCI() let name = RemoteName( host: "example.com", namespace: "org/image", reference: Reference(digest: "sha256:incomplete") ) let record = try storage.create(name) try Data("{}".utf8).write(to: record.manifestURL) try assertDeletionWaitsForPruneLock(record: record) { try storage.delete(name) } } } func testVMDirectoryDeletionOfUnpublishedRecordWaitsForPruneLock() throws { try withTemporaryTartHome { let storage = try VMStorageOCI() let record = try storage.create(RemoteName( host: "example.com", namespace: "org/image", reference: Reference(digest: "sha256:unpublished") )) try assertDeletionWaitsForPruneLock(record: record) { try record.removeFromDisk() } } } func testTagReplacementWaitsForPruneLock() throws { try withTemporaryTartHome { let storage = try VMStorageOCI() let firstManifest = try stackedManifest(baseContentDigest: "sha256:first") let secondManifest = try stackedManifest(baseContentDigest: "sha256:second") let firstName = try digestName(for: firstManifest) let secondName = try digestName(for: secondManifest) _ = try createRecord(for: firstManifest, in: storage) _ = try createRecord(for: secondManifest, in: storage) let tagName = RemoteName( host: secondName.host, namespace: secondName.namespace, reference: Reference(tag: "latest") ) let contentStore = try ContentStore() let lockHeld = DispatchSemaphore(value: 0) let releaseLock = DispatchSemaphore(value: 0) let replacementStarted = DispatchSemaphore(value: 0) let replacementFinished = DispatchSemaphore(value: 0) DispatchQueue.global().async { try? contentStore.withPruneLock { lockHeld.signal() releaseLock.wait() } } XCTAssertEqual(lockHeld.wait(timeout: .now() + 1), .success) DispatchQueue.global().async { replacementStarted.signal() try? storage.link(from: tagName, to: secondName) replacementFinished.signal() } XCTAssertEqual(replacementStarted.wait(timeout: .now() + 1), .success) XCTAssertEqual(replacementFinished.wait(timeout: .now() + 0.1), .timedOut) releaseLock.signal() XCTAssertEqual(replacementFinished.wait(timeout: .now() + 1), .success) XCTAssertTrue(storage.linked(from: tagName, to: secondName)) XCTAssertFalse(storage.linked(from: tagName, to: firstName)) } } func testGCDeletionOfStackedOCIRecordWaitsForPruneLock() throws { try withTemporaryTartHome { let storage = try VMStorageOCI() let record = try createRecord(for: stackedManifest(), in: storage) try assertDeletionWaitsForPruneLock(record: record) { try storage.gc() } } } func testTemporaryManifestGCWaitsForPruneLock() throws { try withTemporaryTartHome { let temporaryVMDir = try VMDirectory.temporary() try stackedManifest().toJSON().write(to: temporaryVMDir.manifestURL) try assertDeletionWaitsForPruneLock(record: temporaryVMDir) { try Config().gc() } } } func testLockedTemporaryDirectorySurvivesGarbageCollection() throws { try withTemporaryTartHome { let temporaryVMDir = try VMDirectory.temporary() let lock = try FileLock(lockURL: temporaryVMDir.baseURL) try lock.lock() try Config().gc() XCTAssertTrue(FileManager.default.fileExists(atPath: temporaryVMDir.baseURL.path)) try lock.unlock() try Config().gc() XCTAssertFalse(FileManager.default.fileExists(atPath: temporaryVMDir.baseURL.path)) } } func testMovingStackedOCIRecordWaitsForPruneLock() throws { try withTemporaryTartHome { let storage = try VMStorageOCI() let source = try temporaryVMDirectory() let manifest = try stackedManifest() let name = try digestName(for: manifest) try config().save(toURL: source.configURL) XCTAssertTrue(FileManager.default.createFile(atPath: source.nvramURL.path, contents: Data())) try manifest.toJSON().write(to: source.manifestURL) let contentStore = try ContentStore() let lockHeld = DispatchSemaphore(value: 0) let releaseLock = DispatchSemaphore(value: 0) let moveStarted = DispatchSemaphore(value: 0) let moveFinished = DispatchSemaphore(value: 0) DispatchQueue.global().async { try? contentStore.withPruneLock { lockHeld.signal() releaseLock.wait() } } XCTAssertEqual(lockHeld.wait(timeout: .now() + 1), .success) DispatchQueue.global().async { moveStarted.signal() try? storage.move(name, from: source) moveFinished.signal() } XCTAssertEqual(moveStarted.wait(timeout: .now() + 1), .success) XCTAssertEqual(moveFinished.wait(timeout: .now() + 0.1), .timedOut) XCTAssertTrue(FileManager.default.fileExists(atPath: source.baseURL.path)) releaseLock.signal() XCTAssertEqual(moveFinished.wait(timeout: .now() + 1), .success) XCTAssertFalse(FileManager.default.fileExists(atPath: source.baseURL.path)) XCTAssertTrue(try storage.open(name).isStackedCachedImage) } } func testPruningLastStackedOCIRecordReclaimsItsContent() throws { try withTemporaryTartHome { let contentStore = try ContentStore() let baseContent = try installContent(Data("record-only-base".utf8), into: contentStore) let overlayContent = try installContent(Data("record-only-overlay".utf8), into: contentStore) let manifest = try stackedManifest( baseContentDigest: baseContent.digest, overlayContentDigest: overlayContent.digest, baseUncompressedSize: UInt64(try baseContent.url.sizeBytes()), overlayUncompressedSize: UInt64(try overlayContent.url.sizeBytes()) ) let name = try digestName(for: manifest) let storage = try VMStorageOCI() let record = try storage.create(name) try config().save(toURL: record.configURL) XCTAssertTrue(FileManager.default.createFile(atPath: record.nvramURL.path, contents: Data())) try manifest.toJSON().write(to: record.manifestURL) let candidate = try XCTUnwrap(storage.prunables().first { $0.url.lastPathComponent == record.url.lastPathComponent }) XCTAssertGreaterThanOrEqual( try candidate.allocatedSizeBytes(), try baseContent.url.allocatedSizeBytes() + overlayContent.url.allocatedSizeBytes() ) // The record itself fits in this budget, so pruning only succeeds if it // accounts for the immutable content released with the final reference. try Prune.pruneSpaceBudget( prunableStorages: [storage], spaceBudgetBytes: UInt64(try record.allocatedSizeBytes()) ) XCTAssertFalse(FileManager.default.fileExists(atPath: record.url.path)) XCTAssertFalse(FileManager.default.fileExists(atPath: baseContent.url.path)) XCTAssertFalse(FileManager.default.fileExists(atPath: overlayContent.url.path)) } } func testMalformedStackedOCIRecordDoesNotBlockPruning() throws { try withTemporaryTartHome { let contentStore = try ContentStore() let baseContent = try installContent(Data("valid-base".utf8), into: contentStore) let overlayContent = try installContent(Data("valid-overlay".utf8), into: contentStore) let storage = try VMStorageOCI() let validRecord = try createRecord(for: stackedManifest( baseContentDigest: baseContent.digest, overlayContentDigest: overlayContent.digest, baseUncompressedSize: UInt64(try baseContent.url.sizeBytes()), overlayUncompressedSize: UInt64(try overlayContent.url.sizeBytes()) ), in: storage) let malformedRecord = try storage.create(RemoteName( host: "example.com", namespace: "org/image", reference: Reference(digest: "sha256:malformed") )) try config().save(toURL: malformedRecord.configURL) XCTAssertTrue(FileManager.default.createFile(atPath: malformedRecord.nvramURL.path, contents: Data())) try Data("{".utf8).write(to: malformedRecord.manifestURL) XCTAssertNoThrow(try storage.prunables()) try Prune.pruneSpaceBudget(prunableStorages: [storage], spaceBudgetBytes: 0) XCTAssertFalse(FileManager.default.fileExists(atPath: validRecord.url.path)) XCTAssertFalse(FileManager.default.fileExists(atPath: malformedRecord.url.path)) XCTAssertFalse(FileManager.default.fileExists(atPath: baseContent.url.path)) XCTAssertFalse(FileManager.default.fileExists(atPath: overlayContent.url.path)) } } func testPruningOneStackedOCIRecordPreservesSharedContent() throws { try withTemporaryTartHome { let contentStore = try ContentStore() let baseContent = try installContent(Data("shared-base".utf8), into: contentStore) let firstOverlay = try installContent(Data("first-overlay".utf8), into: contentStore) let secondOverlay = try installContent(Data("second-overlay".utf8), into: contentStore) let storage = try VMStorageOCI() let firstManifest = try stackedManifest( baseContentDigest: baseContent.digest, overlayContentDigest: firstOverlay.digest, baseUncompressedSize: UInt64(try baseContent.url.sizeBytes()), overlayUncompressedSize: UInt64(try firstOverlay.url.sizeBytes()) ) let secondManifest = try stackedManifest( baseContentDigest: baseContent.digest, overlayContentDigest: secondOverlay.digest, baseUncompressedSize: UInt64(try baseContent.url.sizeBytes()), overlayUncompressedSize: UInt64(try secondOverlay.url.sizeBytes()) ) let firstRecord = try createRecord(for: firstManifest, in: storage) let secondRecord = try createRecord(for: secondManifest, in: storage) let firstCandidate = try XCTUnwrap(storage.prunables().first { $0.url.lastPathComponent == firstRecord.url.lastPathComponent }) try firstCandidate.delete() XCTAssertTrue(FileManager.default.fileExists(atPath: baseContent.url.path)) XCTAssertFalse(FileManager.default.fileExists(atPath: firstOverlay.url.path)) XCTAssertTrue(FileManager.default.fileExists(atPath: secondOverlay.url.path)) let secondCandidate = try XCTUnwrap(storage.prunables().first { $0.url.lastPathComponent == secondRecord.url.lastPathComponent }) try secondCandidate.delete() XCTAssertFalse(FileManager.default.fileExists(atPath: baseContent.url.path)) XCTAssertFalse(FileManager.default.fileExists(atPath: secondOverlay.url.path)) } } func testSpaceBudgetRecomputesSharedContentAfterOwnerDeletion() throws { try withTemporaryTartHome { let contentStore = try ContentStore() let baseContent = try installContent(Data("shared-base".utf8), into: contentStore) let firstOverlay = try installContent(Data("first-overlay".utf8), into: contentStore) let secondOverlay = try installContent(Data("second-overlay".utf8), into: contentStore) let storage = try VMStorageOCI() let firstManifest = try stackedManifest( baseContentDigest: baseContent.digest, overlayContentDigest: firstOverlay.digest, baseUncompressedSize: UInt64(try baseContent.url.sizeBytes()), overlayUncompressedSize: UInt64(try firstOverlay.url.sizeBytes()) ) let secondManifest = try stackedManifest( baseContentDigest: baseContent.digest, overlayContentDigest: secondOverlay.digest, baseUncompressedSize: UInt64(try baseContent.url.sizeBytes()), overlayUncompressedSize: UInt64(try secondOverlay.url.sizeBytes()) ) let olderRecord = try createRecord(for: firstManifest, in: storage) let newerRecord = try createRecord(for: secondManifest, in: storage) try olderRecord.url.updateAccessDate(Date(timeIntervalSince1970: 1)) try newerRecord.url.updateAccessDate(Date(timeIntervalSince1970: 2)) let olderCandidate = try XCTUnwrap(storage.prunables().first { $0.url.lastPathComponent == olderRecord.url.lastPathComponent }) let budget = UInt64(try olderCandidate.allocatedSizeBytes()) // On the first pass the newer record owns the shared base and is // selected for deletion, while the older record fits this budget. // Recomputing must then charge the surviving record for the base and // prune it too. try Prune.pruneSpaceBudget( prunableStorages: [storage], spaceBudgetBytes: budget ) XCTAssertFalse(FileManager.default.fileExists(atPath: olderRecord.url.path)) XCTAssertFalse(FileManager.default.fileExists(atPath: newerRecord.url.path)) XCTAssertFalse(FileManager.default.fileExists(atPath: baseContent.url.path)) XCTAssertFalse(FileManager.default.fileExists(atPath: firstOverlay.url.path)) XCTAssertFalse(FileManager.default.fileExists(atPath: secondOverlay.url.path)) } } func testSpaceBudgetRecomputesBeforeDeletingAnotherCandidate() throws { try withTemporaryTartHome { let contentStore = try ContentStore() let sharedBase = try installContent(Data(repeating: 0x41, count: 128 * 1024), into: contentStore) let newestOverlay = try installContent(Data("newest-overlay".utf8), into: contentStore) let middleOverlay = try installContent(Data("middle-overlay".utf8), into: contentStore) let retainedBase = try installContent(Data(repeating: 0x42, count: 32 * 1024), into: contentStore) let retainedOverlay = try installContent(Data("retained-overlay".utf8), into: contentStore) let storage = try VMStorageOCI() let newest = try createRecord(for: stackedManifest( baseContentDigest: sharedBase.digest, overlayContentDigest: newestOverlay.digest, baseUncompressedSize: UInt64(try sharedBase.url.sizeBytes()), overlayUncompressedSize: UInt64(try newestOverlay.url.sizeBytes()) ), in: storage) let middle = try createRecord(for: stackedManifest( baseContentDigest: sharedBase.digest, overlayContentDigest: middleOverlay.digest, baseUncompressedSize: UInt64(try sharedBase.url.sizeBytes()), overlayUncompressedSize: UInt64(try middleOverlay.url.sizeBytes()) ), in: storage) let retained = try createRecord(for: stackedManifest( baseContentDigest: retainedBase.digest, overlayContentDigest: retainedOverlay.digest, baseUncompressedSize: UInt64(try retainedBase.url.sizeBytes()), overlayUncompressedSize: UInt64(try retainedOverlay.url.sizeBytes()) ), in: storage) try newest.url.updateAccessDate(Date(timeIntervalSince1970: 3)) try middle.url.updateAccessDate(Date(timeIntervalSince1970: 2)) try retained.url.updateAccessDate(Date(timeIntervalSince1970: 1)) let retainedCandidate = try XCTUnwrap(storage.prunables().first { $0.url.lastPathComponent == retained.url.lastPathComponent }) // Initially the newest record owns the shared base and is too large. // The middle record appears small enough to retain, making the oldest // unrelated record look like a second deletion candidate. After the // first deletion, ownership moves to the middle record; recomputing // before selecting again must delete it and preserve the unrelated one. try Prune.pruneSpaceBudget( prunableStorages: [storage], spaceBudgetBytes: UInt64(try retainedCandidate.allocatedSizeBytes()) ) XCTAssertFalse(FileManager.default.fileExists(atPath: newest.url.path)) XCTAssertFalse(FileManager.default.fileExists(atPath: middle.url.path)) XCTAssertTrue(FileManager.default.fileExists(atPath: retained.url.path)) XCTAssertFalse(FileManager.default.fileExists(atPath: sharedBase.url.path)) XCTAssertTrue(FileManager.default.fileExists(atPath: retainedBase.url.path)) XCTAssertTrue(FileManager.default.fileExists(atPath: retainedOverlay.url.path)) } } func testAutomaticReclaimRecomputesSharedContentAfterOwnerDeletion() throws { try withTemporaryTartHome { let contentStore = try ContentStore() let sharedBase = try installContent(Data(repeating: 0x41, count: 128 * 1024), into: contentStore) let initiatorOverlay = try installContent(Data("initiator-overlay".utf8), into: contentStore) let ownerOverlay = try installContent(Data("owner-overlay".utf8), into: contentStore) let unrelatedBase = try installContent(Data("unrelated-base".utf8), into: contentStore) let unrelatedOverlay = try installContent(Data("unrelated-overlay".utf8), into: contentStore) let storage = try VMStorageOCI() let initiatorManifest = try stackedManifest( baseContentDigest: sharedBase.digest, overlayContentDigest: initiatorOverlay.digest, baseUncompressedSize: UInt64(try sharedBase.url.sizeBytes()), overlayUncompressedSize: UInt64(try initiatorOverlay.url.sizeBytes()) ) let ownerManifest = try stackedManifest( baseContentDigest: sharedBase.digest, overlayContentDigest: ownerOverlay.digest, baseUncompressedSize: UInt64(try sharedBase.url.sizeBytes()), overlayUncompressedSize: UInt64(try ownerOverlay.url.sizeBytes()) ) let unrelatedManifest = try stackedManifest( baseContentDigest: unrelatedBase.digest, overlayContentDigest: unrelatedOverlay.digest, baseUncompressedSize: UInt64(try unrelatedBase.url.sizeBytes()), overlayUncompressedSize: UInt64(try unrelatedOverlay.url.sizeBytes()) ) let initiator = try createRecord(for: initiatorManifest, in: storage) let owner = try createRecord(for: ownerManifest, in: storage) let unrelated = try createRecord(for: unrelatedManifest, in: storage) try initiator.url.updateAccessDate(Date(timeIntervalSince1970: 1)) try owner.url.updateAccessDate(Date(timeIntervalSince1970: 2)) try unrelated.url.updateAccessDate(Date(timeIntervalSince1970: 3)) let sharedBaseSize = UInt64(try sharedBase.url.allocatedSizeBytes()) // The owner is the first deletable record and is initially charged for // the shared base. Deleting it cannot reclaim that base because the // protected initiator still references it, so reclaim must continue. try Prune.reclaimIfPossible(sharedBaseSize, initiator) XCTAssertTrue(FileManager.default.fileExists(atPath: initiator.url.path)) XCTAssertFalse(FileManager.default.fileExists(atPath: owner.url.path)) XCTAssertFalse(FileManager.default.fileExists(atPath: unrelated.url.path)) XCTAssertTrue(FileManager.default.fileExists(atPath: sharedBase.url.path)) } } #if canImport(DiskImageKit) @available(macOS 27.0, *) func testPopulateStackedPushedImageCachesImmutableTopOverlay() throws { if #unavailable(macOS 27.0) { throw XCTSkip("DiskImageKit tests require macOS 27 or newer") } try withTemporaryTartHome { let source = try diskImageSource() let stacked = try temporaryVMDirectory() try source.cloneAsStackedBase(to: stacked, generateMAC: false) var manifest = try OCIManifest(fromJSON: Data(contentsOf: stacked.manifestURL)) let contentDigest = try Digest.hash(stacked.overlayURL) var overlay = OCIManifestLayer( mediaType: asifOverlayMediaType, size: 1, digest: "sha256:overlay-transport", uncompressedSize: 1, uncompressedContentDigest: "sha256:overlay-chunk" ) overlay.annotations?[diskFileContentDigestAnnotation] = contentDigest overlay.annotations?[diskFileChunkCountAnnotation] = "1" manifest.layers.insert(overlay, at: manifest.layers.count - 1) let name = try digestName(for: manifest) let storage = try VMStorageOCI() try storage.populate(name, from: stacked, manifest: manifest) let cached = try storage.open(name) XCTAssertTrue(cached.isStackedCachedImage) XCTAssertFalse(FileManager.default.fileExists(atPath: cached.overlayURL.path)) XCTAssertEqual(try OCIManifest(fromJSON: Data(contentsOf: cached.manifestURL)), manifest) let cachedContent = try XCTUnwrap(try ContentStore().existingContentURL(for: contentDigest)) XCTAssertEqual(try Digest.hash(cachedContent), contentDigest) } } #endif private func standaloneSource(diskData: Data) throws -> VMDirectory { let vmDir = try temporaryVMDirectory() try config().save(toURL: vmDir.configURL) XCTAssertTrue(FileManager.default.createFile(atPath: vmDir.nvramURL.path, contents: Data())) try diskData.write(to: vmDir.diskURL) return vmDir } private func createRecord(for manifest: OCIManifest, in storage: VMStorageOCI) throws -> VMDirectory { let record = try storage.create(try digestName(for: manifest)) try config().save(toURL: record.configURL) XCTAssertTrue(FileManager.default.createFile(atPath: record.nvramURL.path, contents: Data())) try manifest.toJSON().write(to: record.manifestURL) return record } private func assertDeletionWaitsForPruneLock( record: VMDirectory, deletion: @escaping () throws -> Void ) throws { let contentStore = try ContentStore() let lockHeld = DispatchSemaphore(value: 0) let releaseLock = DispatchSemaphore(value: 0) let deletionStarted = DispatchSemaphore(value: 0) let deletionFinished = DispatchSemaphore(value: 0) DispatchQueue.global().async { try? contentStore.withPruneLock { lockHeld.signal() releaseLock.wait() } } XCTAssertEqual(lockHeld.wait(timeout: .now() + 1), .success) DispatchQueue.global().async { deletionStarted.signal() try? deletion() deletionFinished.signal() } XCTAssertEqual(deletionStarted.wait(timeout: .now() + 1), .success) XCTAssertEqual(deletionFinished.wait(timeout: .now() + 0.1), .timedOut) XCTAssertTrue(FileManager.default.fileExists(atPath: record.baseURL.path)) releaseLock.signal() XCTAssertEqual(deletionFinished.wait(timeout: .now() + 1), .success) XCTAssertFalse(FileManager.default.fileExists(atPath: record.baseURL.path)) } #if canImport(DiskImageKit) @available(macOS 27.0, *) private func diskImageSource() throws -> VMDirectory { let vmDir = try temporaryVMDirectory() try config().save(toURL: vmDir.configURL) XCTAssertTrue(FileManager.default.createFile(atPath: vmDir.nvramURL.path, contents: Data())) _ = try DiskImage(creating: .raw(url: vmDir.diskURL, blockCount: 8)) try flatManifest().toJSON().write(to: vmDir.manifestURL) return vmDir } #endif private func config() -> VMConfig { VMConfig( platform: Linux(), cpuCountMin: 2, memorySizeMin: 512 * 1024 * 1024, diskFormat: .raw ) } private func flatManifest() throws -> OCIManifest { let disk = OCIManifestLayer( mediaType: diskV2MediaType, size: 1, digest: "sha256:disk-transport", uncompressedSize: 1, uncompressedContentDigest: "sha256:disk-chunk" ) return OCIManifest( config: OCIManifestConfig(size: 1, digest: "sha256:oci-config"), layers: [ OCIManifestLayer(mediaType: configMediaType, size: 1, digest: "sha256:config"), disk, OCIManifestLayer(mediaType: nvramMediaType, size: 1, digest: "sha256:nvram"), ] ) } private func stackedManifest( baseContentDigest: String = "sha256:base", overlayContentDigest: String = "sha256:overlay", baseUncompressedSize: UInt64 = 1, overlayUncompressedSize: UInt64 = 1 ) throws -> OCIManifest { var manifest = try flatManifest() manifest.annotations?[diskBlockSizeAnnotation] = "512" manifest.annotations?[uncompressedDiskSizeAnnotation] = "4096" manifest.layers[1].annotations?[diskFileContentDigestAnnotation] = baseContentDigest manifest.layers[1].annotations?[uncompressedSizeAnnotation] = String(baseUncompressedSize) var overlay = OCIManifestLayer( mediaType: asifOverlayMediaType, size: 1, digest: "sha256:overlay-transport", uncompressedSize: overlayUncompressedSize, uncompressedContentDigest: "sha256:overlay-chunk" ) overlay.annotations?[diskFileContentDigestAnnotation] = overlayContentDigest overlay.annotations?[diskFileChunkCountAnnotation] = "1" manifest.layers.insert(overlay, at: manifest.layers.count - 1) return manifest } private func installContent(_ data: Data, contentDigest: String, into contentStore: ContentStore) throws { let temporaryURL = try contentStore.temporaryContentURL(for: contentDigest) try data.write(to: temporaryURL) _ = try contentStore.install(temporaryURL, contentDigest: contentDigest) } private func pinnedBaseManifest(contentDigest: String) -> OCIManifest { var disk = OCIManifestLayer( mediaType: diskV2MediaType, size: 1, digest: "sha256:disk-transport", uncompressedSize: 1, uncompressedContentDigest: "sha256:disk-chunk" ) disk.annotations?[diskFileContentDigestAnnotation] = contentDigest return OCIManifest( config: OCIManifestConfig(size: 1, digest: "sha256:oci-config"), layers: [ OCIManifestLayer(mediaType: configMediaType, size: 1, digest: "sha256:config"), disk, OCIManifestLayer(mediaType: nvramMediaType, size: 1, digest: "sha256:nvram"), ] ) } private func installContent(_ data: Data, into contentStore: ContentStore) throws -> (digest: String, url: URL) { let digest = Digest.hash(data) let temporaryURL = try contentStore.temporaryContentURL(for: digest) try data.write(to: temporaryURL) return (digest, try contentStore.install(temporaryURL, contentDigest: digest)) } private func digestName(for manifest: OCIManifest) throws -> RemoteName { RemoteName( host: "example.com", namespace: "org/image", reference: Reference(digest: try manifest.digest()) ) } private func withTemporaryTartHome(_ body: () throws -> Void) throws { let home = try temporaryDirectory() let previousHome = ProcessInfo.processInfo.environment["TART_HOME"] setenv("TART_HOME", home.path, 1) defer { if let previousHome { setenv("TART_HOME", previousHome, 1) } else { unsetenv("TART_HOME") } } try body() } private func withTemporaryTartHome(_ body: () async throws -> Void) async throws { let home = try temporaryDirectory() let previousHome = ProcessInfo.processInfo.environment["TART_HOME"] setenv("TART_HOME", home.path, 1) defer { if let previousHome { setenv("TART_HOME", previousHome, 1) } else { unsetenv("TART_HOME") } } try await body() } private func temporaryVMDirectory() throws -> VMDirectory { VMDirectory(baseURL: try temporaryDirectory()) } private func temporaryDirectory() throws -> URL { let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) try FileManager.default.createDirectory(at: url, withIntermediateDirectories: false) addTeardownBlock { try? FileManager.default.removeItem(at: url) } return url } }