TeraRuntimeClientFixtures.swift (7728B)
1 import Foundation 2 @testable import TeraApp 3 4 struct TestRuntimeSigner: TeraRuntimeSigner { 5 func availability() async -> TeraRuntimeSignerAvailability { 6 .ready 7 } 8 9 func sign(_: TeraRuntimeSigningRequest) async -> TeraRuntimeSigningOutcome { 10 .failed 11 } 12 } 13 14 actor RuntimeHarness { 15 private var configurations: [TeraRuntimeLaunchConfiguration] = [] 16 17 func launchConfigurations() -> [TeraRuntimeLaunchConfiguration] { 18 configurations 19 } 20 21 private let startDelayNanoseconds: UInt64 22 private let snapshotDelayNanoseconds: UInt64 23 private let shutdownDelayNanoseconds: UInt64 24 private let shutdownFailure: TeraRuntimeFailure? 25 private var subscriptionFailures: Int 26 private var starts = 0 27 private var shutdowns = 0 28 private var cancels = 0 29 private var subscriptionAttempts = 0 30 private var receivers: [UUID: @Sendable (TeraRuntimeChange) async -> Void] = [:] 31 32 init( 33 startDelayNanoseconds: UInt64 = 0, 34 snapshotDelayNanoseconds: UInt64 = 0, 35 shutdownDelayNanoseconds: UInt64 = 10_000_000, 36 subscriptionFailures: Int = 0, 37 shutdownFailure: TeraRuntimeFailure? = nil 38 ) { 39 self.startDelayNanoseconds = startDelayNanoseconds 40 self.snapshotDelayNanoseconds = snapshotDelayNanoseconds 41 self.shutdownDelayNanoseconds = shutdownDelayNanoseconds 42 self.subscriptionFailures = subscriptionFailures 43 self.shutdownFailure = shutdownFailure 44 } 45 46 func start( 47 configuration: TeraRuntimeLaunchConfiguration 48 ) async throws -> TeraRuntimeBackendStart { 49 configurations.append(configuration) 50 starts += 1 51 if startDelayNanoseconds > 0 { 52 await Task.detached { [startDelayNanoseconds] in 53 try? await Task.sleep(nanoseconds: startDelayNanoseconds) 54 }.value 55 } 56 let backend = TestRuntimeBackend( 57 harness: self, 58 publicKeyHex: configuration.publicKeyHex, 59 snapshotDelayNanoseconds: snapshotDelayNanoseconds, 60 shutdownDelayNanoseconds: shutdownDelayNanoseconds, 61 shutdownFailure: shutdownFailure 62 ) 63 return await TeraRuntimeBackendStart( 64 backend: backend, 65 snapshot: backend.snapshotValue() 66 ) 67 } 68 69 func addReceiver( 70 _ receive: @escaping @Sendable (TeraRuntimeChange) async -> Void 71 ) throws -> UUID { 72 subscriptionAttempts += 1 73 if subscriptionFailures > 0 { 74 subscriptionFailures -= 1 75 throw TeraRuntimeFailure.local( 76 operation: "test.subscribe", 77 code: "test.subscribe_retryable", 78 safeMessage: "The test subscription is temporarily unavailable." 79 ) 80 } 81 let id = UUID() 82 receivers[id] = receive 83 return id 84 } 85 86 func removeReceiver(id: UUID) { 87 if receivers.removeValue(forKey: id) != nil { 88 cancels += 1 89 } 90 } 91 92 func emit(_ change: TeraRuntimeChange) async { 93 for receive in receivers.values { 94 await receive(change) 95 } 96 } 97 98 func emitRevision(_ revision: UInt64) async { 99 guard let configuration = configurations.last else { 100 preconditionFailure("Start the fixture runtime before emitting changes") 101 } 102 await emit(TeraRuntimeChange( 103 schemaVersion: 3, 104 scope: TeraRuntimeChangeScope(publicKey: configuration.publicKeyHex, 105 sourceGeneration: configuration.sourceGenerationHex, context: nil), 106 epoch: String(repeating: "1", count: 32), 107 revision: TeraProjectionRevision(rawValue: revision), delivery: .change, kind: .today, entityID: "card-\(revision)" 108 )) 109 } 110 111 func recordShutdown() { 112 shutdowns += 1 113 } 114 115 func startCount() -> Int { 116 starts 117 } 118 119 func shutdownCount() -> Int { 120 shutdowns 121 } 122 123 func cancelCount() -> Int { 124 cancels 125 } 126 127 func subscriptionAttemptCount() -> Int { 128 subscriptionAttempts 129 } 130 } 131 132 private actor TestRuntimeBackend: TeraRuntimeBackend { 133 private let harness: RuntimeHarness 134 private let snapshotValueStored: TeraRuntimeSnapshot 135 private let snapshotDelayNanoseconds: UInt64 136 private let shutdownDelayNanoseconds: UInt64 137 private let shutdownFailure: TeraRuntimeFailure? 138 private var closed = false 139 140 init( 141 harness: RuntimeHarness, 142 publicKeyHex: String, 143 snapshotDelayNanoseconds: UInt64, 144 shutdownDelayNanoseconds: UInt64, 145 shutdownFailure: TeraRuntimeFailure? 146 ) { 147 self.harness = harness 148 self.snapshotDelayNanoseconds = snapshotDelayNanoseconds 149 self.shutdownDelayNanoseconds = shutdownDelayNanoseconds 150 self.shutdownFailure = shutdownFailure 151 snapshotValueStored = TeraRuntimeSnapshot( 152 identity: TeraRuntimeIdentity( 153 publicKeyHex: publicKeyHex, 154 hostSignerConfigured: true 155 ), 156 relay: nil, 157 blossomConfiguration: nil, 158 blossomEvidence: nil, 159 crateName: "tera_ffi", 160 crateVersion: "0.1.0-alpha", 161 isClosed: false 162 ) 163 } 164 165 func snapshotValue() -> TeraRuntimeSnapshot { 166 snapshotValueStored 167 } 168 169 func snapshot() async throws -> TeraRuntimeSnapshot { 170 if snapshotDelayNanoseconds > 0 { 171 await Task.detached { [snapshotDelayNanoseconds] in 172 try? await Task.sleep(nanoseconds: snapshotDelayNanoseconds) 173 }.value 174 } 175 if closed { 176 throw TeraRuntimeFailure.local( 177 operation: "test.snapshot", 178 code: "test.closed", 179 safeMessage: "The test runtime is closed." 180 ) 181 } 182 return snapshotValueStored 183 } 184 185 func todayPage(request _: TeraTodayPageRequest) throws -> TeraTodayPage { 186 throw TeraRuntimeFailure.local( 187 operation: "test.today.page", 188 code: "test.unsupported", 189 safeMessage: "Today is not configured for this lifecycle test." 190 ) 191 } 192 193 func refreshToday( 194 context _: TeraLocalNetwork, 195 nowUnixSeconds _: UInt64, 196 update _: TeraTodayProjectionUpdate, 197 backfillCursor _: String? 198 ) throws -> TeraTodaySyncReceipt { 199 throw TeraRuntimeFailure.local( 200 operation: "test.today.refresh", 201 code: "test.unsupported", 202 safeMessage: "Today is not configured for this lifecycle test." 203 ) 204 } 205 206 func subscribe( 207 bufferCapacity _: Int, 208 receive: @escaping @Sendable (TeraRuntimeChange) async -> Void 209 ) async throws -> any TeraRuntimeSubscriptionToken { 210 let id = try await harness.addReceiver(receive) 211 return TestSubscriptionToken(harness: harness, id: id) 212 } 213 214 func shutdown() async throws -> TeraRuntimeShutdownReceipt { 215 await harness.recordShutdown() 216 await Task.detached { [shutdownDelayNanoseconds] in 217 try? await Task.sleep(nanoseconds: shutdownDelayNanoseconds) 218 }.value 219 if let shutdownFailure { 220 throw shutdownFailure 221 } 222 let alreadyClosed = closed 223 closed = true 224 return TeraRuntimeShutdownReceipt(state: "closed", alreadyClosed: alreadyClosed) 225 } 226 } 227 228 private actor TestSubscriptionToken: TeraRuntimeSubscriptionToken { 229 private let harness: RuntimeHarness 230 private let id: UUID 231 private var cancelled = false 232 233 init(harness: RuntimeHarness, id: UUID) { 234 self.harness = harness 235 self.id = id 236 } 237 238 func cancel() async { 239 guard !cancelled else { return } 240 cancelled = true 241 await harness.removeReceiver(id: id) 242 } 243 }