TeraRuntimeResourceLifecycleTests.swift (7033B)
1 @testable import TeraApp 2 import XCTest 3 4 final class TeraRuntimeResourceLifecycleTests: XCTestCase { 5 func testStartupTimeoutClosesLateBackendWithoutResurrection() async throws { 6 let configuration = TeraRuntimeClientTests().makeConfiguration(generation: "30") 7 let backend = ResourceTestBackend(publicKeyHex: configuration.publicKeyHex) 8 let pause = ResourceTestPause() 9 let deadlines = TeraRuntimeDeadlinePolicy(startupNanoseconds: 1_000_000) 10 let client = TeraRuntimeClient( 11 factory: { _ in await pause.wait(); return await backend.start() }, deadlines: deadlines 12 ) 13 let start = Task { try await client.start(configuration: configuration) } 14 await pause.entered.wait() 15 do { 16 _ = try await start.value 17 XCTFail("Startup deadline must end the wait") 18 } catch let TeraRuntimeClientError.startup(failure) { 19 XCTAssertEqual(failure.code, "ios.runtime.deadline_exceeded") 20 } 21 await pause.resume.open() 22 await assertClosed(backend) 23 guard case .failed = await client.lifecycle() else { return XCTFail("Late success cannot revive the runtime") } 24 _ = try await client.stop() 25 } 26 27 func testLastCancelledStartupWaiterClosesItsLateBackend() async throws { 28 let configuration = TeraRuntimeClientTests().makeConfiguration(generation: "31") 29 let backend = ResourceTestBackend(publicKeyHex: configuration.publicKeyHex) 30 let pause = ResourceTestPause() 31 let client = TeraRuntimeClient(factory: { _ in await pause.wait(); return await backend.start() }) 32 let start = Task { try await client.start(configuration: configuration) } 33 await pause.entered.wait() 34 start.cancel() 35 do { 36 _ = try await start.value 37 XCTFail("A cancelled sole waiter must not adopt the runtime") 38 } catch let TeraRuntimeClientError.startup(failure) { 39 XCTAssertEqual(failure.code, "ios.runtime.cancelled") 40 } 41 let lifecycle = await client.lifecycle() 42 XCTAssertEqual(lifecycle, .stopped) 43 await pause.resume.open() 44 await assertClosed(backend) 45 _ = try await client.stop() 46 let count = await backend.shutdownCount 47 XCTAssertEqual(count, 1) 48 } 49 50 func testLateSupersededStartupCannotCloseNewRuntime() async throws { 51 let firstConfig = TeraRuntimeClientTests().makeConfiguration(generation: "32") 52 let nextConfig = TeraRuntimeClientTests().makeConfiguration(generation: "33") 53 let old = ResourceTestBackend(publicKeyHex: firstConfig.publicKeyHex) 54 let current = ResourceTestBackend(publicKeyHex: nextConfig.publicKeyHex) 55 let pause = ResourceTestPause() 56 let client = TeraRuntimeClient(factory: { configuration in 57 if configuration == firstConfig { 58 await pause.wait(); return await old.start() 59 } 60 return await current.start() 61 }) 62 let first = Task { try await client.start(configuration: firstConfig) } 63 await pause.entered.wait() 64 let replacement = Task { try await client.start(configuration: nextConfig) } 65 do { 66 _ = try await first.value 67 XCTFail("Old startup must be superseded") 68 } catch { 69 XCTAssertEqual(error as? TeraRuntimeClientError, .superseded) 70 } 71 await pause.resume.open() 72 await assertClosed(old) 73 _ = try await replacement.value 74 let snapshot = try await client.snapshot() 75 let activeCloses = await current.shutdownCount 76 XCTAssertEqual(snapshot.identity.publicKeyHex, nextConfig.publicKeyHex) 77 XCTAssertEqual(activeCloses, 0) 78 _ = try await client.stop() 79 } 80 81 func testLateSubscriptionAfterCancellationDetachesOnce() async throws { 82 try await assertLateSubscription(termination: .cancel) 83 } 84 85 func testLateSubscriptionAfterTimeoutDetachesOnce() async throws { 86 try await assertLateSubscription(termination: .timeout) 87 } 88 89 func testLateSubscriptionAfterStopDetachesOnce() async throws { 90 try await assertLateSubscription(termination: .stop) 91 } 92 93 func testAdoptedSubscriptionFinishesAndDetachesOnceOnRepeatedStop() async throws { 94 let configuration = TeraRuntimeClientTests().makeConfiguration(generation: "34") 95 let backend = ResourceTestBackend(publicKeyHex: configuration.publicKeyHex) 96 let client = TeraRuntimeClient(factory: { _ in await backend.start() }) 97 _ = try await client.start(configuration: configuration) 98 let stream = try await client.changes() 99 await backend.emit(1) 100 var iterator = stream.makeAsyncIterator() 101 let first = await iterator.next() 102 XCTAssertEqual(first?.revision.rawValue, 1) 103 _ = try await client.stop() 104 _ = try await client.stop() 105 await backend.emit(2) 106 let final = await iterator.next() 107 let cancels = await backend.token.cancelCount 108 let closes = await backend.shutdownCount 109 XCTAssertNil(final) 110 XCTAssertEqual(cancels, 1) 111 XCTAssertEqual(closes, 1) 112 } 113 114 private enum Termination { case cancel, timeout, stop } 115 116 private func assertLateSubscription(termination: Termination) async throws { 117 let configuration = TeraRuntimeClientTests().makeConfiguration(generation: "35") 118 let backend = ResourceTestBackend(publicKeyHex: configuration.publicKeyHex) 119 let pause = ResourceTestPause() 120 await backend.pauseSubscription(pause) 121 let deadlines = TeraRuntimeDeadlinePolicy( 122 subscriptionNanoseconds: termination == .timeout ? 1_000_000 : 10_000_000_000 123 ) 124 let client = TeraRuntimeClient(factory: { _ in await backend.start() }, deadlines: deadlines) 125 _ = try await client.start(configuration: configuration) 126 let creation = Task { try await client.changes() } 127 await pause.entered.wait() 128 var stop: Task<TeraRuntimeShutdownReceipt, Error>? 129 switch termination { 130 case .cancel: creation.cancel() 131 case .stop: stop = Task { try await client.stop() } 132 case .timeout: break 133 } 134 do { 135 _ = try await creation.value 136 XCTFail("Abandoned creation must not return an observer") 137 } catch { 138 if termination == .stop { 139 XCTAssertEqual(error as? TeraRuntimeClientError, .superseded) 140 } else { 141 guard case let TeraRuntimeClientError.subscription(failure) = error else { throw error } 142 XCTAssertEqual(failure.code, termination == .timeout ? "ios.runtime.deadline_exceeded" : "ios.runtime.cancelled") 143 } 144 } 145 await pause.resume.open() 146 let detached = expectation(description: "Late token detached") 147 let wait = Task { await backend.token.cancelled.wait(); detached.fulfill() } 148 await fulfillment(of: [detached], timeout: 2) 149 await backend.token.cancelled.open() 150 await wait.value 151 _ = try await stop?.value 152 _ = try await client.stop() 153 let count = await backend.token.cancelCount 154 XCTAssertEqual(count, 1) 155 } 156 157 private func assertClosed(_ backend: ResourceTestBackend) async { 158 let closed = expectation(description: "Late backend closed") 159 let wait = Task { await backend.closed.wait(); closed.fulfill() } 160 await fulfillment(of: [closed], timeout: 2) 161 await backend.closed.open() 162 await wait.value 163 let count = await backend.shutdownCount 164 XCTAssertEqual(count, 1) 165 } 166 }