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

RadrootsBackgroundTaskQueryTests.swift (6223B)


      1 import Foundation
      2 @testable import RadrootsKit
      3 import RadrootsKitTesting
      4 import Testing
      5 
      6 @Test func backgroundTaskQueryReturnsFirstReplyAndIgnoresDuplicates() async throws {
      7     let value: Int = try await RadrootsBackgroundTaskQuery.read { reply in
      8         reply(7)
      9         reply(9)
     10     }
     11     #expect(value == 7)
     12 }
     13 
     14 @Test func backgroundTaskQueryLostCallbackTimesOutAndIgnoresLateReply() async {
     15     let probe = BackgroundTaskQueryProbe()
     16     await #expect(throws: RadrootsBackgroundTransferError.transferFailure) {
     17         let _: Int = try await RadrootsBackgroundTaskQuery.read(timeoutNanoseconds: 10_000_000) {
     18             probe.install($0)
     19         }
     20     }
     21     probe.reply(8)
     22     probe.reply(9)
     23 }
     24 
     25 @Test func backgroundTaskQueryCancellationReleasesWaitAndIgnoresLateReply() async {
     26     let probe = BackgroundTaskQueryProbe()
     27     let task = Task {
     28         try await RadrootsBackgroundTaskQuery.read { probe.install($0) }
     29     }
     30     var registered = probe.registered.makeAsyncIterator()
     31     _ = await registered.next()
     32     task.cancel()
     33     await #expect(throws: RadrootsBackgroundTransferError.transferFailure) { try await task.value }
     34     probe.reply(8)
     35     probe.reply(9)
     36 }
     37 
     38 @Test func backgroundTaskQueryAlreadyCancelledDoesNotStartQuery() async {
     39     let probe = BackgroundTaskQueryProbe()
     40     let task = Task {
     41         withUnsafeCurrentTask { $0?.cancel() }
     42         let _: Int = try await RadrootsBackgroundTaskQuery.read { probe.install($0) }
     43     }
     44     await #expect(throws: RadrootsBackgroundTransferError.transferFailure) { try await task.value }
     45     #expect(!probe.wasInstalled)
     46 }
     47 
     48 @Test func backgroundTaskQueryRejectsInvalidBudgetsBeforeQuery() async {
     49     for budget: UInt64 in [0, 5_000_000_001, .max] {
     50         let probe = BackgroundTaskQueryProbe()
     51         await #expect(throws: RadrootsBackgroundTransferError.invalidRequest) {
     52             let _: Int = try await RadrootsBackgroundTaskQuery.read(timeoutNanoseconds: budget) { probe.install($0) }
     53         }
     54         #expect(!probe.wasInstalled)
     55     }
     56 }
     57 
     58 @Test func backgroundTaskQueryReplyCancellationRaceIsSingleResolution() async throws {
     59     for _ in 0 ..< 100 {
     60         let probe = BackgroundTaskQueryProbe()
     61         let task = Task { try await RadrootsBackgroundTaskQuery.read { probe.install($0) } }
     62         var registered = probe.registered.makeAsyncIterator()
     63         _ = await registered.next()
     64         await withTaskGroup(of: Void.self) { group in
     65             group.addTask { probe.reply(7) }
     66             group.addTask { task.cancel() }
     67             group.addTask { probe.reply(7) }
     68         }
     69         do { #expect(try await task.value == 7) } catch {
     70             #expect(error as? RadrootsBackgroundTransferError == .transferFailure)
     71         }
     72     }
     73 }
     74 
     75 @Test func backgroundTaskQueryValidatesEveryOwnedTaskBeforeInferringAbsence() throws {
     76     let request = try appleUploadRequest(identifier: "inventory.valid")
     77     let descriptor = RadrootsBackgroundURLTaskDescriptor(request: request, executionID: UUID())
     78     #expect(try RadrootsBackgroundTaskQuery.descriptors([descriptor.taskDescription]) == [descriptor])
     79     #expect(try RadrootsBackgroundTaskQuery.descriptors([request.identifier.rawValue]).first?.identifier
     80         == request.identifier)
     81     #expect(try RadrootsBackgroundTaskQuery.descriptors([]).isEmpty)
     82     #expect(try RadrootsBackgroundTaskQuery.descriptors(Array(repeating: descriptor.taskDescription, count: 4096))
     83         .count == 4096)
     84     for descriptions: [String?] in [
     85         [nil], ["radroots-transfer-v2|invalid"], [String(repeating: "a", count: 257)],
     86         [descriptor.taskDescription, nil], Array(repeating: descriptor.taskDescription, count: 4097),
     87     ] {
     88         #expect(throws: RadrootsBackgroundTransferError.transferFailure) {
     89             try RadrootsBackgroundTaskQuery.descriptors(descriptions)
     90         }
     91     }
     92 }
     93 
     94 @Test func backgroundTaskQueryUnknownInventoryPreservesReceiptsAndRefusesRetry() async throws {
     95     for state: RadrootsBackgroundTransferState in [.queued, .running, .interrupted, .expired] {
     96         let request = try appleUploadRequest(identifier: "inventory.unknown")
     97         let snapshot = try RadrootsBackgroundTransferSnapshot(request: request, state: state, executionID: UUID())
     98         let store = RadrootsInMemoryBackgroundTransferStore(snapshots: [snapshot])
     99         let probe = RadrootsAppleBackgroundTransferProbe()
    100         let original = probe.adapters()
    101         let adapters = RadrootsAppleBackgroundTransferAdapters(
    102             enqueue: original.enqueue, cancel: original.cancel,
    103             activeTransferIdentifiers: {
    104                 try await RadrootsBackgroundTaskQuery.read(timeoutNanoseconds: 1_000_000) { _ in }
    105             }, handleBackgroundEvents: original.handleBackgroundEvents
    106         )
    107         let transfer = RadrootsAppleBackgroundTransfer(store: store, adapters: adapters)
    108         await #expect(throws: RadrootsBackgroundTransferError.transferFailure) { try await transfer.snapshots() }
    109         if state == .interrupted || state == .expired {
    110             await #expect(throws: RadrootsBackgroundTransferError.transferFailure) { try await transfer.retry(request) }
    111         }
    112         #expect(try await store.loadSnapshots() == [snapshot])
    113         #expect(await probe.enqueuedRequests.isEmpty)
    114         #expect(await probe.cancelledIdentifiers.isEmpty)
    115     }
    116 }
    117 
    118 /// All mutable callback state is synchronized; invoking the callback is outside
    119 /// the lock so the query's own resolution/cancellation can run independently.
    120 private final class BackgroundTaskQueryProbe: @unchecked Sendable {
    121     let registered: AsyncStream<Void>
    122     private let registration: AsyncStream<Void>.Continuation
    123     private let lock = NSLock()
    124     private var callback: (@Sendable (Int) -> Void)?
    125 
    126     init() {
    127         (registered, registration) = AsyncStream.makeStream(bufferingPolicy: .bufferingNewest(1))
    128     }
    129 
    130     var wasInstalled: Bool {
    131         lock.withLock { callback != nil }
    132     }
    133 
    134     func install(_ callback: @escaping @Sendable (Int) -> Void) {
    135         lock.withLock { self.callback = callback }
    136         registration.yield(())
    137         registration.finish()
    138     }
    139 
    140     func reply(_ value: Int) {
    141         let pending = lock.withLock { callback }
    142         pending?(value)
    143     }
    144 }