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

RadrootsReceiptBarrierTests.swift (8581B)


      1 import Darwin
      2 import Foundation
      3 @testable import RadrootsKit
      4 import RadrootsKitTesting
      5 import Testing
      6 
      7 @Test func receiptBarrierRetriesProgressConflictAndStoreFailureBeforeAcknowledgement() async throws {
      8     let roots = try appleTransferRoots()
      9     defer { try? FileManager.default.removeItem(at: roots.dataRoot.deletingLastPathComponent()) }
     10     let request = try appleUploadRequest(identifier: "receipt.barrier", responsePolicy: .boundedJSON())
     11     let generation = UUID()
     12     let snapshot = try RadrootsBackgroundTransferSnapshot(request: request, state: .running, executionID: generation)
     13     let store = ReceiptBarrierStore(snapshot: snapshot)
     14     let resolver = RadrootsAppleBackgroundTransferFileResolver(roots: roots)
     15     let coordinator = RadrootsTransferCoordinator(sessionIdentifier: "receipt.tests", store: store,
     16                                                   fileResolver: resolver)
     17     let callbacks = RadrootsTransferCallbackQueue()
     18     let acknowledgement = RadrootsCompletionProbe()
     19     let body = Data(#"{"url":"https://example.org/blob"}"#.utf8)
     20     callbacks.enqueue(receipt: request.identifier) {
     21         await coordinator.complete(identifier: request.identifier,
     22                                    completion: RadrootsTransferCompletion(
     23                                        platformError: nil,
     24                                        stagedDownloadResult: nil,
     25                                        httpResult: RadrootsBackgroundHTTPResult(statusCode: 200,
     26                                                                                 mediaType: "application/json",
     27                                                                                 body: body,
     28                                                                                 bodyExceeded: false),
     29                                        bytesTransferred: 10,
     30                                        totalBytesExpected: 10
     31                                    ), executionID: generation)
     32     }
     33     try await store.waitUntilEntered()
     34     #expect(await store.entered)
     35     #expect(callbacks.pendingIdentifiers == [request.identifier])
     36     for _ in 0 ..< 10 {
     37         await coordinator.handleBackgroundEvents(identifier: "receipt.tests") { acknowledgement.markCompleted() }
     38     }
     39     callbacks.enqueue { await coordinator.finishBackgroundEvents(identifier: "receipt.tests") }
     40     #expect(!acknowledgement.completed)
     41     await store.release()
     42     for _ in 0 ..< 500 {
     43         if acknowledgement.completionCount == 10 {
     44             break
     45         }
     46         try await Task.sleep(for: .milliseconds(10))
     47     }
     48     #expect(acknowledgement.completionCount == 10)
     49     #expect(await store.terminalAttempts == 4)
     50     #expect(callbacks.pendingIdentifiers.isEmpty)
     51     let receipt = try #require(try await store.loadSnapshots().first)
     52     #expect(receipt.state == .awaitingVerification && receipt.executionID == generation)
     53     #expect(receipt.response?.body == body && receipt.progress.bytesTransferred == 10)
     54 }
     55 
     56 @Test func receiptBarrierSurvivesActualStoreLockAndRecoversAfterPersistence() async throws {
     57     let roots = try appleTransferRoots()
     58     defer { try? FileManager.default.removeItem(at: roots.dataRoot.deletingLastPathComponent()) }
     59     let store = RadrootsAppleBackgroundTransferStore(roots: roots)
     60     let request = try appleUploadRequest(identifier: "receipt.lock")
     61     try await store.saveSnapshot(RadrootsBackgroundTransferSnapshot(request: request, state: .running))
     62     let lockURL = try roots.resolvedURL(for: RadrootsFileReference(scope: .data,
     63                                                                    relativePath: "background_transfers/transfers.lock"))
     64     let descriptor = try #require(try RadrootsAtomicFile.acquireExclusiveLock(at: lockURL))
     65     let coordinator = RadrootsTransferCoordinator(sessionIdentifier: "locked.receipt", store: store,
     66                                                   fileResolver: RadrootsAppleBackgroundTransferFileResolver(
     67                                                       roots: roots
     68                                                   ))
     69     let pending = Task { await coordinator.complete(
     70         identifier: request.identifier,
     71         completion: RadrootsTransferCompletion(platformError: nil, stagedDownloadResult: nil,
     72                                                httpResult: successfulHTTPResult(),
     73                                                bytesTransferred: 10,
     74                                                totalBytesExpected: 10)
     75     ) }
     76     for _ in 0 ..< 100 {
     77         if await coordinator.hasPendingReceipts {
     78             break
     79         }
     80         try await Task.sleep(for: .milliseconds(10))
     81     }
     82     let acknowledgement = RadrootsCompletionProbe()
     83     await coordinator.handleBackgroundEvents(identifier: "locked.receipt") { acknowledgement.markCompleted() }
     84     await coordinator.finishBackgroundEvents(identifier: "locked.receipt")
     85     #expect(!acknowledgement.completed)
     86     pending.cancel()
     87     Darwin.close(descriptor)
     88     await pending.value
     89     #expect(acknowledgement.completed)
     90     let restarted = RadrootsAppleBackgroundTransferStore(roots: roots)
     91     #expect(try await restarted.loadSnapshots().first?.state == .awaitingVerification)
     92 }
     93 
     94 private actor ReceiptBarrierStore: RadrootsBackgroundTransferStore {
     95     private let storage: RadrootsInMemoryBackgroundTransferStore
     96     private var gate: CheckedContinuation<Void, Never>?
     97     private(set) var entered = false
     98     private(set) var terminalAttempts = 0
     99 
    100     func waitUntilEntered() async throws {
    101         for _ in 0 ..< 100 {
    102             if entered {
    103                 return
    104             }
    105             try await Task.sleep(for: .milliseconds(10))
    106         }
    107         throw RadrootsBackgroundTransferError.transferFailure
    108     }
    109 
    110     init(snapshot: RadrootsBackgroundTransferSnapshot) {
    111         storage = .init(snapshots: [snapshot])
    112     }
    113 
    114     func release() {
    115         gate?.resume(); gate = nil
    116     }
    117 
    118     func compareExchangeSnapshot(expected: RadrootsBackgroundTransferSnapshot?,
    119                                  desired: RadrootsBackgroundTransferSnapshot) async throws -> Bool
    120     {
    121         if desired.state == .awaitingVerification {
    122             terminalAttempts += 1
    123             if terminalAttempts == 1 {
    124                 entered = true
    125                 await withCheckedContinuation { gate = $0 }
    126                 let current = try #require(expected)
    127                 let progress = try RadrootsBackgroundTransferSnapshot(request: current.request, state: .running,
    128                                                                       progress: RadrootsBackgroundTransferProgress(
    129                                                                           bytesTransferred: 5,
    130                                                                           totalBytesExpected: 10
    131                                                                       ),
    132                                                                       executionID: current.executionID)
    133                 try await storage.saveSnapshot(progress)
    134             } else if terminalAttempts <= 3 {
    135                 throw RadrootsBackgroundTransferError.persistenceFailure
    136             }
    137         }
    138         let exchanged = try await storage.compareExchangeSnapshot(expected: expected, desired: desired)
    139         if desired.state == .awaitingVerification, terminalAttempts == 4, exchanged {
    140             // The write took effect, but its caller lost the successful return.
    141             throw RadrootsBackgroundTransferError.persistenceFailure
    142         }
    143         return exchanged
    144     }
    145 
    146     func withAdmission<Result: Sendable>(for identifier: RadrootsBackgroundTransferIdentifier,
    147                                          operation: @escaping @Sendable () async throws -> Result)
    148         async throws -> Result
    149     {
    150         try await storage.withAdmission(for: identifier, operation: operation)
    151     }
    152 
    153     func admissionIsActive(for identifier: RadrootsBackgroundTransferIdentifier) async throws -> Bool {
    154         try await storage.admissionIsActive(for: identifier)
    155     }
    156 
    157     func loadSnapshots() async throws -> [RadrootsBackgroundTransferSnapshot] {
    158         try await storage.loadSnapshots()
    159     }
    160 
    161     func saveSnapshot(_ snapshot: RadrootsBackgroundTransferSnapshot) async throws {
    162         try await storage.saveSnapshot(snapshot)
    163     }
    164 
    165     func removeSnapshot(for identifier: RadrootsBackgroundTransferIdentifier) async throws {
    166         try await storage.removeSnapshot(for: identifier)
    167     }
    168 
    169     func removeAllSnapshots() async throws {
    170         try await storage.removeAllSnapshots()
    171     }
    172 }