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 }