TeraMediaCleanupTests.swift (11321B)
1 import Darwin 2 import Foundation 3 import RadrootsKit 4 @testable import TeraApp 5 import XCTest 6 7 final class TeraMediaCleanupTests: XCTestCase { 8 func testInvalidRootCannotCreateAnAdmissionMarker() { 9 for path in ["", "relative", "/tmp/../unknown", "/tmp/./unknown", "/"] { 10 XCTAssertThrowsError(try TeraMediaProcessUse.admit(applicationSupportDirectory: path)) 11 } 12 } 13 14 func testProtectedDataLossDuringInventoryRetainsEveryFile() async throws { 15 let fixture = try MediaCleanupFixture() 16 defer { fixture.remove() } 17 let old = try fixture.stage(String(repeating: "a", count: 64)) 18 let availability = MediaCleanupAvailability() 19 let result = await TeraMediaCleanup.run(roots: fixture.roots, now: fixture.now, protectedData: { await availability.firstOnly() }) 20 XCTAssertEqual(result, .retained) 21 XCTAssertTrue(fixture.exists(old)) 22 } 23 24 func testFailedRuntimeCreationLeavesItsProcessProtectionInPlace() async throws { 25 let fixture = try MediaCleanupFixture() 26 defer { fixture.remove() } 27 let old = try fixture.stage(String(repeating: "a", count: 64)) 28 let configuration = TeraRuntimeLaunchConfiguration( 29 applicationSupportDirectory: fixture.roots.dataRoot.path, publicKeyHex: "invalid", 30 sourceGenerationHex: String(repeating: "04", count: 32), sourceGenerationCreatedAtUnixMilliseconds: 1_800_000_000_000, 31 protectedData: .available, networkProfile: .publicNetwork, writableRelays: [], blossom: nil, 32 app: TeraRuntimeAppMetadata(bundleIdentifier: "test.cleanup", version: "1", buildNumber: "1", buildSHA: nil), 33 signerGeneration: "cleanup", signer: ComposerForbiddenSigner(), adoptBootstrapSettings: false 34 ) 35 do { 36 _ = try await TeraGeneratedRuntimeBackend.start(configuration: configuration) 37 XCTFail("Invalid identity must fail runtime creation") 38 } catch {} 39 let result = await TeraMediaCleanup.run(roots: fixture.roots, now: fixture.now) 40 XCTAssertEqual(result, .retained) 41 XCTAssertTrue(fixture.exists(old)) 42 } 43 44 func testLifecycleStartupFinishesCleanupBeforeRecordingNativeSessionUse() async throws { 45 let fixture = try MediaCleanupFixture() 46 defer { fixture.remove() } 47 let old = try fixture.stage(String(repeating: "a", count: 64)) 48 try FileManager.default.setAttributes([.modificationDate: Date(timeIntervalSince1970: 1)], ofItemAtPath: old.path) 49 let buffer = TeraDiagnosticsBuffer() 50 let coordinator = TeraLifecycleCoordinator( 51 telemetry: buffer, buffer: buffer, fileAccess: RadrootsAppleFileAccess(roots: fixture.roots), 52 transfer: BackgroundTransferHarness(), transferIdentifier: "test.tera.cleanup.transfer" 53 ) 54 async let first = coordinator.attachBackgroundEvents() 55 async let second = coordinator.attachBackgroundEvents() 56 let results = await (first, second) 57 XCTAssertTrue(results.0) 58 XCTAssertTrue(results.1) 59 XCTAssertFalse(fixture.exists(old)) 60 XCTAssertTrue(fixture.exists(fixture.roots.dataRoot.appendingPathComponent("\(TeraMediaProcessUse.directory)/\(getpid())"))) 61 await TeraBackgroundEventRouter.shared.detachAndCompletePending() 62 } 63 64 func testColdPassCollectsOldOrphansAndScratchButPreservesFreshOpaqueAndLeaseFiles() async throws { 65 let fixture = try MediaCleanupFixture() 66 defer { fixture.remove() } 67 let old = try fixture.stage(String(repeating: "a", count: 64)) 68 let fresh = try fixture.stage(String(repeating: "b", count: 64), old: false) 69 let opaque = try fixture.stage("unknown") 70 let scratch = try fixture.stage(".radroots_pending_12345678-1234-1234-1234-123456789abc") 71 let lease = fixture.roots.dataRoot.appendingPathComponent("staged_blob_leases/lease") 72 try FileManager.default.createDirectory(at: lease.deletingLastPathComponent(), withIntermediateDirectories: true) 73 try Data([1]).write(to: lease) 74 let result = await TeraMediaCleanup.run(roots: fixture.roots, now: fixture.now) 75 XCTAssertEqual(result, .completed(removed: 2, exhaustedBudget: false)) 76 XCTAssertFalse(fixture.exists(old)) 77 XCTAssertFalse(fixture.exists(scratch)) 78 XCTAssertTrue(fixture.exists(fresh)) 79 XCTAssertTrue(fixture.exists(opaque)) 80 XCTAssertTrue(fixture.exists(lease)) 81 } 82 83 func testProcessMarkerOutlivesDroppedAdmissionAndRetainsDetachedWork() async throws { 84 let fixture = try MediaCleanupFixture() 85 defer { fixture.remove() } 86 let old = try fixture.stage(String(repeating: "a", count: 64)) 87 do { 88 let use = try TeraMediaProcessUse.admit(root: fixture.roots.dataRoot) 89 XCTAssertNil(try RadrootsAppleFileMaintenance(root: fixture.roots.dataRoot).reserveMaintenance()) 90 withExtendedLifetime(use) {} 91 } 92 // The descriptor has gone, as in a failed factory or cancelled caller; 93 // the live process marker still prevents destructive absence inference. 94 let result = await TeraMediaCleanup.run(roots: fixture.roots, now: fixture.now) 95 XCTAssertEqual(result, .retained) 96 XCTAssertTrue(fixture.exists(old)) 97 } 98 99 func testExclusivePassRejectsNewAdmissionBeforeAnyProcessMarkerIsWritten() throws { 100 let fixture = try MediaCleanupFixture() 101 defer { fixture.remove() } 102 let reservation = try XCTUnwrap(RadrootsAppleFileMaintenance(root: fixture.roots.dataRoot).reserveMaintenance()) 103 defer { withExtendedLifetime(reservation) {} } 104 XCTAssertThrowsError(try TeraMediaProcessUse.admit(root: fixture.roots.dataRoot)) 105 XCTAssertFalse(fixture.exists(fixture.roots.dataRoot.appendingPathComponent(TeraMediaProcessUse.directory))) 106 } 107 108 func testOnlyDefinitelyAbsentWellFormedProcessMarkersCanBeRetired() throws { 109 let fixture = try MediaCleanupFixture() 110 defer { fixture.remove() } 111 let access = RadrootsAppleFileAccess(roots: fixture.roots) 112 let file = RadrootsFileReference(scope: .data, relativePath: "\(TeraMediaProcessUse.directory)/123") 113 try access.write(.inline(Data("tera.media.use.v1 123\n".utf8)), to: file) 114 let reservation = try XCTUnwrap(RadrootsAppleFileMaintenance(root: fixture.roots.dataRoot).reserveMaintenance()) 115 defer { withExtendedLifetime(reservation) {} } 116 var remaining = 64 117 XCTAssertFalse(try TeraMediaProcessUse.hasNoLiveUsers(roots: fixture.roots, scan: reservation.openDirectory(relativePath: TeraMediaProcessUse.directory), remaining: &remaining, isAbsent: { _ in false })) 118 remaining = 64 119 XCTAssertTrue(try TeraMediaProcessUse.hasNoLiveUsers(roots: fixture.roots, scan: reservation.openDirectory(relativePath: TeraMediaProcessUse.directory), remaining: &remaining, isAbsent: { $0 == 123 })) 120 try access.write(.inline(Data("unknown version".utf8)), to: file) 121 remaining = 64 122 XCTAssertFalse(try TeraMediaProcessUse.hasNoLiveUsers(roots: fixture.roots, scan: reservation.openDirectory(relativePath: TeraMediaProcessUse.directory), remaining: &remaining, isAbsent: { _ in true })) 123 } 124 125 func testActiveUploadAndUnverifiedOrCancelledNativeReceiptKeepTheirSharedSource() async throws { 126 for state in [RadrootsBackgroundTransferState.running, .awaitingVerification, .cancelled, .completed] { 127 let fixture = try MediaCleanupFixture() 128 let upload = try BackgroundUploadFixture() 129 defer { fixture.remove(); upload.remove() } 130 let source = try fixture.stage(upload.media.sha256) 131 let orphan = try fixture.stage(String(repeating: "e", count: 64)) 132 let request = try upload.request(job: upload.job(revision: 1, operation: String(repeating: "2", count: 32))) 133 let snapshot = try RadrootsBackgroundTransferSnapshot(request: request, state: state) 134 try await RadrootsAppleBackgroundTransferStore(roots: fixture.roots).saveSnapshot(snapshot) 135 let result = await TeraMediaCleanup.run(roots: fixture.roots, now: fixture.now) 136 XCTAssertEqual(result, .completed(removed: 1, exhaustedBudget: false)) 137 XCTAssertTrue(fixture.exists(source)) 138 XCTAssertFalse(fixture.exists(orphan)) 139 } 140 } 141 142 func testUnknownTransferEvidenceAndBackupRootPreventCollection() async throws { 143 for transfer in [false, true] { 144 let fixture = try MediaCleanupFixture() 145 defer { fixture.remove() } 146 let old = try fixture.stage(String(repeating: "a", count: 64)) 147 if transfer { 148 try RadrootsAppleFileAccess(roots: fixture.roots).write(.inline(Data("unknown".utf8)), to: RadrootsFileReference(scope: .data, relativePath: "background_transfers/transfers.json")) 149 } else { 150 try FileManager.default.createDirectory(at: fixture.roots.dataRoot.appendingPathComponent("backups"), withIntermediateDirectories: true) 151 } 152 let result = await TeraMediaCleanup.run(roots: fixture.roots, now: fixture.now) 153 XCTAssertEqual(result, .retained) 154 XCTAssertTrue(fixture.exists(old)) 155 } 156 } 157 158 func testCancelledPassRetainsFilesAndFreshPassRebuildsProof() async throws { 159 let fixture = try MediaCleanupFixture() 160 defer { fixture.remove() } 161 let old = try fixture.stage(String(repeating: "a", count: 64)) 162 let task = Task { 163 withUnsafeCurrentTask { $0?.cancel() } 164 return await TeraMediaCleanup.run(roots: fixture.roots, now: fixture.now) 165 } 166 let cancelled = await task.value 167 XCTAssertEqual(cancelled, .retained) 168 XCTAssertTrue(fixture.exists(old)) 169 let fresh = await TeraMediaCleanup.run(roots: fixture.roots, now: fixture.now) 170 XCTAssertEqual(fresh, .completed(removed: 1, exhaustedBudget: false)) 171 } 172 173 func testRemovalQuotaStopsBoundedlyAndNextPassReconcilesTheRemainder() async throws { 174 let fixture = try MediaCleanupFixture() 175 defer { fixture.remove() } 176 for value in 1 ... 70 { 177 _ = try fixture.stage(String(format: "%064x", value)) 178 } 179 let first = await TeraMediaCleanup.run(roots: fixture.roots, now: fixture.now) 180 XCTAssertEqual(first, .completed(removed: 64, exhaustedBudget: true)) 181 let second = await TeraMediaCleanup.run(roots: fixture.roots, now: fixture.now) 182 XCTAssertEqual(second, .completed(removed: 6, exhaustedBudget: false)) 183 } 184 } 185 186 struct MediaCleanupFixture: Sendable { 187 let root: URL 188 let roots: RadrootsAppleFileRoots 189 let now = Date(timeIntervalSince1970: 1_800_000_000) 190 191 init() throws { 192 root = FileManager.default.temporaryDirectory.appendingPathComponent("tera-media-cleanup-\(UUID().uuidString)", isDirectory: true) 193 roots = try TeraDurableMediaRoots.selectingStaging(in: RadrootsAppleFileRoots( 194 appIdentifier: "test.tera.cleanup", dataRoot: root.appendingPathComponent("data", isDirectory: true), 195 cacheRoot: root.appendingPathComponent("cache", isDirectory: true), temporaryRoot: root.appendingPathComponent("temporary", isDirectory: true) 196 )) 197 try FileManager.default.createDirectory(at: roots.stagedBlobsRoot, withIntermediateDirectories: true) 198 } 199 200 func stage(_ name: String, old: Bool = true) throws -> URL { 201 let url = roots.stagedBlobsRoot.appendingPathComponent(name) 202 try Data([1, 2, 3]).write(to: url) 203 try FileManager.default.setAttributes([.modificationDate: old ? now.addingTimeInterval(-8 * 86400) : now], ofItemAtPath: url.path) 204 return url 205 } 206 207 func exists(_ url: URL) -> Bool { 208 FileManager.default.fileExists(atPath: url.path) 209 } 210 211 func remove() { 212 try? FileManager.default.removeItem(at: root) 213 } 214 } 215 216 private actor MediaCleanupAvailability { 217 private var calls = 0 218 func firstOnly() -> Bool { 219 calls += 1 220 return calls == 1 221 } 222 }