apple_kit

Apple-native services for Radroots iOS and macOS apps
git clone https://radroots.dev/git/apple_kit.git
Log | Files | Refs | README | LICENSE

RadrootsAdmissionCleanupTests.swift (16019B)


      1 import Darwin
      2 import Foundation
      3 @testable import RadrootsKit
      4 import Testing
      5 
      6 @Test func nativeAdmissionCleanupRetriesOnlyBoundedCreationAbsence() throws {
      7     let f = try AdmissionCleanupFixture()
      8     defer { f.remove() }
      9     var attempts = 0
     10     let path = f.path("creation")
     11     let descriptor = try #require(try RadrootsAtomicFile.acquireExclusiveLock(at: path, injectedOpenError: { attempt in
     12         attempts += 1
     13         return attempt < 2 ? ENOENT : nil
     14     }))
     15     #expect(attempts == 3)
     16     #expect(try RadrootsAtomicFile.acquireExclusiveLock(at: path) == nil)
     17     Darwin.close(descriptor)
     18     attempts = 0
     19     #expect(throws: RadrootsAppleFileError.transientFailure) {
     20         _ = try RadrootsAtomicFile.acquireExclusiveLock(at: f.path("exhausted"), injectedOpenError: { _ in
     21             attempts += 1; return ENOENT
     22         })
     23     }
     24     #expect(attempts == 4 && !FileManager.default.fileExists(atPath: f.path("exhausted").path))
     25     attempts = 0
     26     #expect(throws: RadrootsAppleFileError.permanentFailure) {
     27         _ = try RadrootsAtomicFile.acquireExclusiveLock(at: f.path("denied"), injectedOpenError: { _ in
     28             attempts += 1; return EACCES
     29         })
     30     }
     31     #expect(attempts == 1)
     32     #expect(try RadrootsAtomicFile.acquireExclusiveLock(at: f.path("missing"), create: false) == nil)
     33     #expect(!FileManager.default.fileExists(atPath: f.path("missing").path))
     34 }
     35 
     36 @Test func nativeAdmissionCleanupCreationRetryRejectsChangedParent() throws {
     37     let f = try AdmissionCleanupFixture()
     38     defer { f.remove() }
     39     var attempts = 0
     40     #expect(throws: RadrootsAppleFileError.permanentFailure) {
     41         _ = try RadrootsAtomicFile.acquireExclusiveLock(at: f.path("changed"), injectedOpenError: { _ in
     42             attempts += 1
     43             try FileManager.default.moveItem(at: f.directory, to: f.base.appendingPathComponent("moved"))
     44             try FileManager.default.createDirectory(at: f.directory, withIntermediateDirectories: true)
     45             return ENOENT
     46         })
     47     }
     48     #expect(attempts == 1)
     49     #expect(!FileManager.default.fileExists(atPath: f.path("changed").path))
     50 }
     51 
     52 @Test func nativeAdmissionCleanupConcurrentOwnersNeverSplitOneReservation() async throws {
     53     let f = try AdmissionCleanupFixture()
     54     defer { f.remove() }
     55     let request = try f.request("contended")
     56     let probe = AdmissionCleanupProbe()
     57     let collector = RadrootsAppleBackgroundTransferStore(roots: f.roots)
     58     try await withThrowingTaskGroup(of: Void.self) { group in
     59         for _ in 0 ..< 8 {
     60             group.addTask {
     61                 let store = RadrootsAppleBackgroundTransferStore(roots: f.roots)
     62                 for _ in 0 ..< 20 {
     63                     do {
     64                         _ = try await store.withAdmission(for: request.identifier) {
     65                             await probe.enter()
     66                             await Task.yield()
     67                             await probe.leave()
     68                             return RadrootsBackgroundTransferHandle(request: request)
     69                         }
     70                     } catch RadrootsBackgroundTransferError.invalidRequest {
     71                         // Another owner has this exact reservation.
     72                     } catch RadrootsBackgroundTransferError.unavailable {
     73                         // Bounded coordination contention remains transient.
     74                     }
     75                 }
     76             }
     77         }
     78         group.addTask {
     79             for _ in 0 ..< 40 {
     80                 do { _ = try await collector.collectInactiveAdmissions(limit: 7) }
     81                 catch RadrootsBackgroundTransferError.unavailable {}
     82                 await Task.yield()
     83             }
     84         }
     85         try await group.waitForAll()
     86     }
     87     #expect(await probe.maximum == 1)
     88     #expect(await probe.active == 0)
     89     #expect(await probe.completed > 0)
     90     #expect(try await !collector.admissionIsActive(for: request.identifier))
     91 }
     92 
     93 @Test func nativeAdmissionCleanupPreservesNativeReceiptAndUploadLease() async throws {
     94     let f = try AdmissionCleanupFixture()
     95     defer { f.remove() }
     96     let store = RadrootsAppleBackgroundTransferStore(roots: f.roots)
     97     let request = try f.request("unverified")
     98     let receipt = try RadrootsBackgroundTransferSnapshot(request: request, state: .awaitingVerification,
     99                                                          response: RadrootsBackgroundTransferResponse(statusCode: 200, mediaType: "application/json", body: Data("{}".utf8)),
    100                                                          executionID: UUID())
    101     try await store.saveSnapshot(receipt)
    102     let bytes = Data("OS-consumed immutable upload bytes".utf8)
    103     let lease = try RadrootsAppleFileAccess(roots: f.roots).leaseUploadBytes(bytes, identifier: UUID().uuidString.lowercased())
    104     try FileManager.default.createDirectory(at: f.directory, withIntermediateDirectories: true)
    105     try Data().write(to: f.path(request.identifier.rawValue))
    106     #expect(try await store.collectInactiveAdmissions().removedFiles == 1)
    107     #expect(try await store.loadSnapshots() == [receipt])
    108     #expect(try Data(contentsOf: lease.fileURL) == bytes)
    109 }
    110 
    111 @Test func nativeAdmissionCleanupRetainsHeldReservationAndAllowsIndependentIDs() async throws {
    112     let f = try AdmissionCleanupFixture()
    113     defer { f.remove() }
    114     let first = RadrootsAppleBackgroundTransferStore(roots: f.roots)
    115     let second = RadrootsAppleBackgroundTransferStore(roots: f.roots)
    116     let request = try f.request("held")
    117     let gate = AdmissionCleanupGate()
    118     let pending = Task {
    119         try await first.withAdmission(for: request.identifier) {
    120             await gate.hold()
    121             return RadrootsBackgroundTransferHandle(request: request)
    122         }
    123     }
    124     await gate.waitUntilEntered()
    125     let path = f.path(request.identifier.rawValue)
    126     #expect(FileManager.default.fileExists(atPath: path.path))
    127     #expect(try await second.collectInactiveAdmissions().removedFiles == 0)
    128     #expect(try await second.admissionIsActive(for: request.identifier))
    129     await #expect(throws: RadrootsBackgroundTransferError.invalidRequest) {
    130         _ = try await second.withAdmission(for: request.identifier) { RadrootsBackgroundTransferHandle(request: request) }
    131     }
    132     let other = try f.request("independent")
    133     _ = try await second.withAdmission(for: other.identifier) { RadrootsBackgroundTransferHandle(request: other) }
    134     #expect(!FileManager.default.fileExists(atPath: f.path(other.identifier.rawValue).path))
    135     #expect(FileManager.default.fileExists(atPath: path.path))
    136     await gate.release()
    137     #expect(try await pending.value.identifier == request.identifier)
    138     #expect(!FileManager.default.fileExists(atPath: path.path))
    139 }
    140 
    141 @Test func nativeAdmissionCleanupContinuesBeyondOneThousandEntriesAndRetainsUnknowns() async throws {
    142     let f = try AdmissionCleanupFixture()
    143     defer { f.remove() }
    144     try FileManager.default.createDirectory(at: f.directory, withIntermediateDirectories: true)
    145     for index in 0 ..< 1001 {
    146         try Data().write(to: f.path("orphan-\(index)"))
    147     }
    148     let nonempty = f.path("nonempty")
    149     let outside = f.base.appendingPathComponent("outside")
    150     try Data("protected".utf8).write(to: nonempty)
    151     try Data("external".utf8).write(to: outside)
    152     try FileManager.default.createSymbolicLink(at: f.path("symlink"), withDestinationURL: outside)
    153     try FileManager.default.createDirectory(at: f.path("directory"), withIntermediateDirectories: false)
    154     let unknown = f.directory.appendingPathComponent("unknown.lock")
    155     try Data().write(to: unknown)
    156     let store = RadrootsAppleBackgroundTransferStore(roots: f.roots)
    157     var removed = 0
    158     var calls = 0
    159     while true {
    160         let result = try await store.collectInactiveAdmissions(limit: 37)
    161         #expect(result.scannedEntries <= 37 && result.removedFiles <= result.scannedEntries)
    162         removed += result.removedFiles
    163         calls += 1
    164         if result.reachedEnd {
    165             break
    166         }
    167         try #require(calls < 100)
    168     }
    169     #expect(calls > 27 && removed == 1001)
    170     #expect(try Data(contentsOf: nonempty) == Data("protected".utf8))
    171     #expect(try Data(contentsOf: outside) == Data("external".utf8))
    172     #expect(FileManager.default.fileExists(atPath: unknown.path))
    173     #expect(try await store.collectInactiveAdmissions().removedFiles == 0)
    174 }
    175 
    176 @Test func nativeAdmissionCleanupRetainsInterruptedRetirementThenRecovers() async throws {
    177     let f = try AdmissionCleanupFixture()
    178     defer { f.remove() }
    179     let store = RadrootsAppleBackgroundTransferStore(roots: f.roots)
    180     let request = try f.request("interrupted")
    181     let gate = AdmissionCleanupGate()
    182     let pending = Task {
    183         try await store.withAdmission(for: request.identifier) {
    184             await gate.hold()
    185             return RadrootsBackgroundTransferHandle(request: request)
    186         }
    187     }
    188     await gate.waitUntilEntered()
    189     let coordination = try #require(try RadrootsAtomicFile.acquireExclusiveLock(at: f.coordination))
    190     await gate.release()
    191     #expect(try await pending.value.identifier == request.identifier)
    192     #expect(FileManager.default.fileExists(atPath: f.path(request.identifier.rawValue).path))
    193     Darwin.close(coordination)
    194     #expect(try await store.collectInactiveAdmissions().removedFiles == 1)
    195     #expect(try await store.collectInactiveAdmissions().removedFiles == 0)
    196 }
    197 
    198 @Test func nativeAdmissionCleanupCancellationAndOperationErrorRetireOnlyTheirReservation() async throws {
    199     let f = try AdmissionCleanupFixture()
    200     defer { f.remove() }
    201     let store = RadrootsAppleBackgroundTransferStore(roots: f.roots)
    202     let request = try f.request("cancelled")
    203     let gate = AdmissionCleanupGate()
    204     let pending = Task {
    205         try await store.withAdmission(for: request.identifier) {
    206             await gate.hold()
    207             try Task.checkCancellation()
    208             return RadrootsBackgroundTransferHandle(request: request)
    209         }
    210     }
    211     await gate.waitUntilEntered()
    212     pending.cancel()
    213     #expect(FileManager.default.fileExists(atPath: f.path(request.identifier.rawValue).path))
    214     await gate.release()
    215     await #expect(throws: CancellationError.self) { _ = try await pending.value }
    216     #expect(!FileManager.default.fileExists(atPath: f.path(request.identifier.rawValue).path))
    217     await #expect(throws: RadrootsBackgroundTransferError.transferFailure) {
    218         _ = try await store.withAdmission(for: request.identifier) { throw RadrootsBackgroundTransferError.transferFailure }
    219     }
    220     #expect(!FileManager.default.fileExists(atPath: f.path(request.identifier.rawValue).path))
    221 }
    222 
    223 @Test func nativeAdmissionCleanupBoundsAndCoordinationContentionFailBeforeWork() async throws {
    224     let f = try AdmissionCleanupFixture()
    225     defer { f.remove() }
    226     let store = RadrootsAppleBackgroundTransferStore(roots: f.roots)
    227     for limit in [0, -1, 65, Int.max] {
    228         await #expect(throws: RadrootsBackgroundTransferError.invalidRequest) {
    229             _ = try await store.collectInactiveAdmissions(limit: limit)
    230         }
    231     }
    232     #expect(!FileManager.default.fileExists(atPath: f.directory.path))
    233     let coordination = try #require(try RadrootsAtomicFile.acquireExclusiveLock(at: f.coordination))
    234     defer { Darwin.close(coordination) }
    235     await #expect(throws: RadrootsBackgroundTransferError.unavailable) {
    236         _ = try await store.collectInactiveAdmissions()
    237     }
    238 }
    239 
    240 @Test func nativeAdmissionCleanupRejectsReplacedDirectoryAndResumesFreshPass() async throws {
    241     let f = try AdmissionCleanupFixture()
    242     defer { f.remove() }
    243     let store = RadrootsAppleBackgroundTransferStore(roots: f.roots)
    244     #expect(try await !store.collectInactiveAdmissions(limit: 1).reachedEnd)
    245     try FileManager.default.moveItem(at: f.directory, to: f.base.appendingPathComponent("retired"))
    246     try FileManager.default.createDirectory(at: f.directory, withIntermediateDirectories: true)
    247     let replacement = f.path("replacement")
    248     try Data().write(to: replacement)
    249     await #expect(throws: RadrootsBackgroundTransferError.persistenceFailure) {
    250         _ = try await store.collectInactiveAdmissions()
    251     }
    252     #expect(FileManager.default.fileExists(atPath: replacement.path))
    253     #expect(try await store.collectInactiveAdmissions().removedFiles == 1)
    254 }
    255 
    256 @Test func nativeAdmissionCleanupDoesNotUnlinkReplacedLeafOrFollowRootSymlink() async throws {
    257     let f = try AdmissionCleanupFixture()
    258     defer { f.remove() }
    259     let path = f.path("replaced")
    260     let descriptor = try #require(try RadrootsAtomicFile.acquireExclusiveLock(at: path))
    261     defer { Darwin.close(descriptor) }
    262     try FileManager.default.moveItem(at: path, to: f.base.appendingPathComponent("held-inode"))
    263     try Data().write(to: path)
    264     #expect(try !RadrootsAtomicFile.removeEmptyLockedFile(at: path, descriptor: descriptor))
    265     #expect(FileManager.default.fileExists(atPath: path.path))
    266     try FileManager.default.moveItem(at: f.directory, to: f.base.appendingPathComponent("original"))
    267     try FileManager.default.createSymbolicLink(at: f.directory, withDestinationURL: f.base.appendingPathComponent("original"))
    268     let store = RadrootsAppleBackgroundTransferStore(roots: f.roots)
    269     await #expect(throws: RadrootsBackgroundTransferError.persistenceFailure) { _ = try await store.collectInactiveAdmissions() }
    270     #expect(FileManager.default.fileExists(atPath: path.path))
    271 }
    272 
    273 private actor AdmissionCleanupProbe {
    274     var active = 0
    275     var maximum = 0
    276     var completed = 0
    277     func enter() {
    278         active += 1; maximum = max(maximum, active)
    279     }
    280 
    281     func leave() {
    282         active -= 1; completed += 1
    283     }
    284 }
    285 
    286 private actor AdmissionCleanupGate {
    287     private var entered = false
    288     private var waiters: [CheckedContinuation<Void, Never>] = []
    289     private var continuation: CheckedContinuation<Void, Never>?
    290 
    291     func hold() async {
    292         entered = true
    293         for waiter in waiters {
    294             waiter.resume()
    295         }
    296         waiters.removeAll()
    297         await withCheckedContinuation { continuation = $0 }
    298     }
    299 
    300     func waitUntilEntered() async {
    301         if !entered {
    302             await withCheckedContinuation { waiters.append($0) }
    303         }
    304     }
    305 
    306     func release() {
    307         continuation?.resume(); continuation = nil
    308     }
    309 }
    310 
    311 private struct AdmissionCleanupFixture: Sendable {
    312     let base: URL
    313     let roots: RadrootsAppleFileRoots
    314     var directory: URL {
    315         roots.dataRoot.appendingPathComponent("background_transfers/admissions", isDirectory: true)
    316     }
    317 
    318     var coordination: URL {
    319         directory.appendingPathComponent(".coordination.lock")
    320     }
    321 
    322     init() throws {
    323         let raw = FileManager.default.temporaryDirectory.appendingPathComponent("admission-cleanup-\(UUID().uuidString)")
    324         try FileManager.default.createDirectory(at: raw, withIntermediateDirectories: true)
    325         let pointer = try #require(raw.path.withCString { Darwin.realpath($0, nil) })
    326         defer { Darwin.free(pointer) }
    327         base = URL(fileURLWithPath: String(cString: pointer), isDirectory: true)
    328         roots = try RadrootsAppleFileRoots(appIdentifier: "org.radroots.tests", dataRoot: base.appendingPathComponent("data"),
    329                                            cacheRoot: base.appendingPathComponent("cache"), temporaryRoot: base.appendingPathComponent("tmp"))
    330     }
    331 
    332     func path(_ identifier: String) -> URL {
    333         directory.appendingPathComponent(RadrootsAppleFileDigest.sha256(Data(identifier.utf8)) + ".lock")
    334     }
    335 
    336     func request(_ id: String) throws -> RadrootsBackgroundTransferRequest {
    337         try RadrootsBackgroundTransferRequest(identifier: RadrootsBackgroundTransferIdentifier(id),
    338                                               remoteURL: #require(URL(string: "https://example.org/upload")), method: .put,
    339                                               operation: .upload(source: .file(RadrootsFileReference(scope: .cache, relativePath: "body"))),
    340                                               responsePolicy: .boundedJSON())
    341     }
    342 
    343     func remove() {
    344         try? FileManager.default.removeItem(at: base)
    345     }
    346 }