field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

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 }