TeraRuntimeShutdownTests.swift (7593B)
1 @testable import TeraApp 2 import XCTest 3 4 final class TeraRuntimeShutdownTests: XCTestCase { 5 func testTimeoutRetainsActualOperationAndRepeatedCloseDrainsIt() async throws { 6 let configuration = TeraRuntimeClientTests().makeConfiguration(generation: "70") 7 let backend = ResourceTestBackend(publicKeyHex: configuration.publicKeyHex) 8 let pause = ResourceTestPause() 9 let client = TeraRuntimeClient( 10 factory: { _ in await backend.start() }, 11 deadlines: TeraRuntimeDeadlinePolicy(shutdownNanoseconds: 1_000_000) 12 ) 13 _ = try await client.start(configuration: configuration) 14 await backend.pauseSnapshot(pause) 15 let operation = Task { try await client.snapshot() } 16 await pause.entered.wait() 17 await assertShutdownDeadline(client) 18 let prematureCloses = await backend.shutdownCount 19 XCTAssertEqual(prematureCloses, 0) 20 await assertAdmissionClosed(client) 21 await assertShutdownDeadline(client) 22 let duplicateCloses = await backend.shutdownCount 23 XCTAssertEqual(duplicateCloses, 0) 24 await pause.resume.open() 25 _ = await operation.result 26 await awaitClosed(backend) 27 _ = try await client.stop() 28 let closes = await backend.shutdownCount 29 XCTAssertEqual(closes, 1) 30 } 31 32 func testCancellingCloseWaiterKeepsOneSharedNativeClose() async throws { 33 let configuration = TeraRuntimeClientTests().makeConfiguration(generation: "71") 34 let backend = ResourceTestBackend(publicKeyHex: configuration.publicKeyHex) 35 let pause = ResourceTestPause() 36 let client = TeraRuntimeClient(factory: { _ in await backend.start() }) 37 _ = try await client.start(configuration: configuration) 38 await backend.pauseShutdown(pause) 39 let cancelled = Task { try await client.stop() } 40 await pause.entered.wait() 41 cancelled.cancel() 42 do { 43 _ = try await cancelled.value 44 XCTFail("The caller should stop waiting") 45 } catch let TeraRuntimeClientError.shutdown(failure) { 46 XCTAssertEqual(failure.code, "ios.runtime.cancelled") 47 } 48 await assertAdmissionClosed(client) 49 let survivor = Task { try await client.stop() } 50 await pause.resume.open() 51 let receipt = try await survivor.value 52 XCTAssertEqual(receipt.state, "closed") 53 _ = try await client.stop() 54 let closes = await backend.shutdownCount 55 XCTAssertEqual(closes, 1) 56 } 57 58 func testProtectedStorageFailureRetainsBackendForExplicitRetry() async throws { 59 let configuration = TeraRuntimeClientTests().makeConfiguration(generation: "72") 60 let backend = ResourceTestBackend(publicKeyHex: configuration.publicKeyHex) 61 let failure = TeraRuntimeFailure.local( 62 operation: "test.close", code: "protected_data_unavailable", safeMessage: "Protected data unavailable" 63 ) 64 let client = TeraRuntimeClient(factory: { _ in await backend.start() }) 65 _ = try await client.start(configuration: configuration) 66 await backend.failShutdownOnce(failure) 67 do { 68 _ = try await client.stop() 69 XCTFail("Close failure must survive") 70 } catch let TeraRuntimeClientError.shutdown(observed) { XCTAssertEqual(observed, failure) } 71 await assertAdmissionClosed(client) 72 let receipt = try await client.stop() 73 XCTAssertFalse(receipt.alreadyClosed) 74 let closes = await backend.shutdownCount 75 XCTAssertEqual(closes, 2) 76 } 77 78 func testShutdownWaitsForLateStartupAndItsNativeCleanup() async throws { 79 let configuration = TeraRuntimeClientTests().makeConfiguration(generation: "73") 80 let backend = ResourceTestBackend(publicKeyHex: configuration.publicKeyHex) 81 let creation = ResourceTestPause() 82 let cleanup = ResourceTestPause() 83 await backend.pauseShutdown(cleanup) 84 let client = TeraRuntimeClient( 85 factory: { _ in await creation.wait(); return await backend.start() }, 86 deadlines: TeraRuntimeDeadlinePolicy(shutdownNanoseconds: 1_000_000) 87 ) 88 let start = Task { try await client.start(configuration: configuration) } 89 await creation.entered.wait() 90 await assertShutdownDeadline(client) 91 _ = await start.result 92 do { 93 _ = try await client.start(configuration: configuration) 94 XCTFail("A new startup cannot pass outstanding cleanup") 95 } catch let TeraRuntimeClientError.shutdown(failure) { 96 XCTAssertEqual(failure.code, "ios.runtime.deadline_exceeded") 97 } 98 await creation.resume.open() 99 await cleanup.entered.wait() 100 await assertShutdownDeadline(client) 101 await cleanup.resume.open() 102 await awaitClosed(backend) 103 _ = try await client.stop() 104 let closes = await backend.shutdownCount 105 XCTAssertEqual(closes, 1) 106 } 107 108 func testFailedLateStartupCleanupIsRetainedAndRetried() async throws { 109 let pause = ResourceTestPause() 110 let configuration = TeraRuntimeClientTests().makeConfiguration(generation: "74") 111 let backend = ResourceTestBackend(publicKeyHex: configuration.publicKeyHex) 112 await backend.failShutdownOnce(.local(operation: "test.close", code: "protected_data_unavailable", safeMessage: "Locked")) 113 let client = TeraRuntimeClient(factory: { _ in await pause.wait(); return await backend.start() }) 114 let start = Task { try await client.start(configuration: configuration) } 115 await pause.entered.wait() 116 start.cancel() 117 _ = await start.result 118 await pause.resume.open() 119 // The cleanup owner may already have observed the first failure. Either 120 // close reports it, or this explicit request resumes that failed cleanup. 121 do { 122 _ = try await client.stop() 123 } catch let TeraRuntimeClientError.shutdown(failure) { 124 XCTAssertEqual(failure.code, "protected_data_unavailable") 125 _ = try await client.stop() 126 } 127 let closes = await backend.shutdownCount 128 XCTAssertEqual(closes, 2) 129 let lifecycle = await client.lifecycle() 130 XCTAssertEqual(lifecycle, .stopped) 131 } 132 133 func testLifecycleBridgeSharesCloseAndRetainsFailedRegistration() async { 134 let bridge = TeraLifecycleBridge() 135 let attempt = ShutdownAttemptFixture() 136 await bridge.register { await attempt.close() } 137 let first = Task { await bridge.requestShutdown() } 138 await attempt.pause.entered.wait() 139 first.cancel() 140 let second = Task { await bridge.requestShutdown() } 141 await attempt.pause.resume.open() 142 await first.value 143 await second.value 144 await bridge.requestShutdown() 145 await bridge.requestShutdown() 146 let count = await attempt.calls 147 XCTAssertEqual(count, 2) 148 } 149 150 private func assertAdmissionClosed(_ client: TeraRuntimeClient) async { 151 do { 152 _ = try await client.snapshot() 153 XCTFail("Closing runtime admitted a command") 154 } catch { XCTAssertEqual(error as? TeraRuntimeClientError, .notRunning) } 155 } 156 157 private func assertShutdownDeadline(_ client: TeraRuntimeClient) async { 158 do { 159 _ = try await client.stop() 160 XCTFail("Outstanding work cannot report closed") 161 } catch let TeraRuntimeClientError.shutdown(failure) { 162 XCTAssertEqual(failure.code, "ios.runtime.deadline_exceeded") 163 } catch { XCTFail("Unexpected close failure: \(error)") } 164 } 165 166 private func awaitClosed(_ backend: ResourceTestBackend) async { 167 let completed = expectation(description: "Native close completed") 168 let wait = Task { await backend.closed.wait(); completed.fulfill() } 169 await fulfillment(of: [completed], timeout: 2) 170 await backend.closed.open() 171 await wait.value 172 } 173 } 174 175 private actor ShutdownAttemptFixture { 176 let pause = ResourceTestPause() 177 private(set) var calls = 0 178 func close() async -> Bool { 179 calls += 1 180 if calls == 1 { 181 await pause.wait(); return false 182 } 183 return true 184 } 185 }