TeraRecoverySchedulingTests.swift (5284B)
1 import Foundation 2 @testable import TeraApp 3 import XCTest 4 5 @MainActor 6 final class TeraRecoverySchedulingTests: XCTestCase { 7 func testLifecycleBudgetContinuesUntilExhaustedAndNextResumeTraversesRemainder() async throws { 8 let backend = try TeraScopeBackend() 9 let client = try await TeraScopeFixtures.client(backend) 10 let media = RecoverySchedulingMedia(pending: 1000) 11 let store = TeraNativeRepairStore(client: client, media: media) 12 for expected in [744, 488, 232, 0] { 13 await store.reconcile() 14 XCTAssertEqual(store.progress?.remaining, expected) 15 XCTAssertLessThanOrEqual(store.issues.count, 64) 16 } 17 let calls = await media.calls 18 XCTAssertEqual(calls, 16) 19 store.stop() 20 _ = try await client.stop() 21 } 22 23 func testStopRetainsSingleWorkerAndCoalescesResumeWithoutFreshNotification() async throws { 24 let backend = try TeraScopeBackend() 25 let client = try await TeraScopeFixtures.client(backend) 26 let media = RecoverySchedulingMedia(pending: 1000) 27 let pause = ResourceTestPause() 28 await media.pauseNext(pause) 29 let store = TeraNativeRepairStore(client: client, media: media) 30 let running = Task { await store.reconcile() } 31 await pause.entered.wait() 32 for _ in 0 ..< 100 { 33 store.retry() 34 } 35 store.stop() 36 for _ in 0 ..< 100 { 37 store.retry() 38 } 39 let during = await media.calls 40 XCTAssertEqual(during, 1) 41 XCTAssertTrue(store.isRunning) 42 await pause.resume.open() 43 _ = await running.value 44 await TeraScopeFixtures.eventually { !store.isRunning } 45 let final = await media.calls 46 let maximum = await media.maximumActive 47 XCTAssertEqual(final, 5, "One cancelled worker and one four-batch resumed request") 48 XCTAssertEqual(maximum, 1) 49 XCTAssertEqual(store.progress?.remaining, 744) 50 store.stop() 51 _ = try await client.stop() 52 } 53 54 func testForegroundResumeRetriesProtectedDataWithoutTransferOrDraftEvent() async throws { 55 let backend = try TeraScopeBackend() 56 let client = try await TeraScopeFixtures.client(backend) 57 let media = RecoverySchedulingMedia(pending: 65, locked: true) 58 let stores = TeraProductStores(runtimeClient: client, addMedia: media) 59 stores.configure(snapshot: TeraScopeFixtures.snapshot()) 60 await stores.resume() 61 XCTAssertEqual(stores.add.recovery.transfers.progress?.pause, .protectedData) 62 XCTAssertEqual(stores.add.state, .ready) 63 await media.unlock() 64 await stores.resume() 65 XCTAssertNil(stores.add.recovery.transfers.progress?.pause) 66 XCTAssertEqual(stores.add.recovery.transfers.progress?.remaining, 0) 67 let calls = await media.calls 68 XCTAssertEqual(calls, 3) 69 stores.suspend() 70 await stores.resume() 71 XCTAssertEqual(stores.add.state, .ready) 72 stores.stop() 73 _ = try await client.stop() 74 } 75 76 func testAlreadyCancelledCallerCannotCancelCurrentRecovery() async throws { 77 let backend = try TeraScopeBackend() 78 let client = try await TeraScopeFixtures.client(backend) 79 let media = RecoverySchedulingMedia(pending: 65) 80 let held = ResourceTestPause() 81 await media.pauseNext(held) 82 let store = TeraNativeRepairStore(client: client, media: media) 83 let current = Task { await store.reconcile() } 84 await held.entered.wait() 85 let stalePause = ResourceTestPause() 86 var staleReturned = false 87 let stale = Task { 88 await stalePause.wait() 89 await store.reconcile() 90 staleReturned = true 91 } 92 await stalePause.entered.wait() 93 stale.cancel() 94 await stalePause.resume.open() 95 await TeraScopeFixtures.eventually { staleReturned } 96 XCTAssertTrue(store.isRunning) 97 await held.resume.open() 98 _ = await current.value 99 await stale.value 100 XCTAssertEqual(store.progress?.remaining, 0) 101 XCTAssertFalse(store.isRunning) 102 store.stop() 103 _ = try await client.stop() 104 } 105 } 106 107 private actor RecoverySchedulingMedia: TeraAddMediaHandling { 108 private var pending: Int 109 private var locked: Bool 110 private var pause: ResourceTestPause? 111 private var active = 0 112 private(set) var calls = 0 113 private(set) var maximumActive = 0 114 115 init(pending: Int, locked: Bool = false) { 116 self.pending = pending; self.locked = locked 117 } 118 119 func pauseNext(_ value: ResourceTestPause) { 120 pause = value 121 } 122 123 func unlock() { 124 locked = false 125 } 126 127 func support() -> TeraAddMediaSupport { 128 .unavailable 129 } 130 131 func recoverNativeUploads(client _: TeraRuntimeClient) async throws -> TeraNativeRecoveryProgress { 132 calls += 1 133 active += 1 134 maximumActive = max(maximumActive, active) 135 defer { active -= 1 } 136 if let pause { 137 self.pause = nil; await pause.wait() 138 } 139 try Task.checkCancellation() 140 if locked { 141 return .init(visited: 0, remaining: pending, needsAttention: false, pause: .protectedData) 142 } 143 let visited = min(64, pending) 144 pending -= visited 145 return .init(visited: visited, remaining: pending, needsAttention: false) 146 } 147 148 func importImages(limit _: Int) throws -> [TeraPreparedMedia] { 149 throw TeraComposerAcknowledgment.unconfirmed 150 } 151 152 func captureImage() throws -> TeraPreparedMedia { 153 throw TeraComposerAcknowledgment.unconfirmed 154 } 155 156 func open(_: [TeraPreparedMedia]) throws -> TeraOpenedMedia { 157 throw TeraComposerAcknowledgment.unconfirmed 158 } 159 }