field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

TeraRuntimeClientTests.swift (13535B)


      1 @testable import TeraApp
      2 import XCTest
      3 
      4 final class TeraRuntimeClientTests: XCTestCase {
      5     func testConcurrentStartSharesOneBackend() async throws {
      6         let harness = RuntimeHarness()
      7         let client = TeraRuntimeClient(factory: harness.start)
      8         let configuration = makeConfiguration(generation: "01")
      9 
     10         let snapshots = try await withThrowingTaskGroup(of: TeraRuntimeSnapshot.self) { group in
     11             for _ in 0 ..< 64 {
     12                 group.addTask {
     13                     try await client.start(configuration: configuration)
     14                 }
     15             }
     16             return try await group.reduce(into: []) { $0.append($1) }
     17         }
     18 
     19         XCTAssertEqual(snapshots.count, 64)
     20         let startCount = await harness.startCount()
     21         let lifecycle = await client.lifecycle()
     22         XCTAssertEqual(startCount, 1)
     23         XCTAssertEqual(lifecycle, .running(generation: TeraSessionGeneration(rawValue: 1)))
     24         _ = try await client.stop()
     25     }
     26 
     27     func testOverlappingStopsAwaitOneTypedShutdownFailure() async throws {
     28         let failure = TeraRuntimeFailure.local(
     29           operation: "test.shutdown",
     30           code: "test.shutdown_failed",
     31           safeMessage: "Shutdown did not complete."
     32         )
     33         let harness = RuntimeHarness(shutdownFailure: failure)
     34         let client = TeraRuntimeClient(factory: harness.start)
     35         _ = try await client.start(configuration: makeConfiguration(generation: "03"))
     36 
     37         let results = await withTaskGroup(of: Result<TeraRuntimeShutdownReceipt, Error>.self) {
     38             group in
     39             for _ in 0 ..< 32 {
     40                 group.addTask {
     41                     do {
     42                         return try await .success(client.stop())
     43                     } catch {
     44                         return .failure(error)
     45                     }
     46                 }
     47             }
     48             return await group.reduce(into: []) { $0.append($1) }
     49         }
     50 
     51         let shutdownCount = await harness.shutdownCount()
     52         XCTAssertEqual(shutdownCount, 1)
     53         XCTAssertEqual(results.count, 32)
     54         for result in results {
     55             guard case let .failure(error) = result else {
     56                 return XCTFail("Expected every waiter to receive the shutdown failure")
     57             }
     58             XCTAssertEqual(error as? TeraRuntimeClientError, .shutdown(failure))
     59         }
     60     }
     61 
     62     func testNewConfigurationSupersedesAndClosesSlowStartup() async throws {
     63         let harness = RuntimeHarness(startDelayNanoseconds: 30_000_000)
     64         let client = TeraRuntimeClient(factory: harness.start)
     65         let firstConfiguration = makeConfiguration(generation: "04")
     66         let secondConfiguration = makeConfiguration(generation: "05")
     67 
     68         let first = Task {
     69             try await client.start(configuration: firstConfiguration)
     70         }
     71         try await Task.sleep(nanoseconds: 2_000_000)
     72         let second = Task {
     73             try await client.start(configuration: secondConfiguration)
     74         }
     75 
     76         do {
     77             _ = try await first.value
     78             XCTFail("The older startup should be superseded")
     79         } catch {
     80             XCTAssertEqual(error as? TeraRuntimeClientError, .superseded)
     81         }
     82         let snapshot = try await second.value
     83         XCTAssertEqual(snapshot.identity.publicKeyHex, secondConfiguration.publicKeyHex)
     84         let startCount = await harness.startCount()
     85         let supersededShutdownCount = await harness.shutdownCount()
     86         XCTAssertEqual(startCount, 2)
     87         XCTAssertEqual(supersededShutdownCount, 1)
     88         _ = try await client.stop()
     89         let finalShutdownCount = await harness.shutdownCount()
     90         XCTAssertEqual(finalShutdownCount, 2)
     91     }
     92 
     93     func testBufferCapacityAndStoppedSubscriptionFailClosed() async throws {
     94         let harness = RuntimeHarness()
     95         let client = TeraRuntimeClient(factory: harness.start)
     96         do {
     97             _ = try await client.changes()
     98             XCTFail("A stopped runtime cannot subscribe")
     99         } catch {
    100             XCTAssertEqual(error as? TeraRuntimeClientError, .notRunning)
    101         }
    102 
    103         _ = try await client.start(configuration: makeConfiguration(generation: "06"))
    104         for invalidCapacity in [0, 65] {
    105             do {
    106                 _ = try await client.changes(bufferCapacity: invalidCapacity)
    107                 XCTFail("Invalid capacities must fail closed")
    108             } catch {
    109                 XCTAssertEqual(error as? TeraRuntimeClientError, .invalidBufferCapacity)
    110             }
    111         }
    112         _ = try await client.stop()
    113     }
    114 
    115     func testStartupDeadlineReturnsPromptlyAndClosesLateBackend() async throws {
    116         let harness = RuntimeHarness(startDelayNanoseconds: 60_000_000)
    117         let client = TeraRuntimeClient(
    118           factory: harness.start,
    119           deadlines: makeDeadlines(startup: 5_000_000)
    120         )
    121 
    122         do {
    123             _ = try await client.start(configuration: makeConfiguration(generation: "07"))
    124             XCTFail("A startup that exceeds its deadline must fail")
    125         } catch let TeraRuntimeClientError.startup(failure) {
    126             XCTAssertEqual(failure.code, "ios.runtime.deadline_exceeded")
    127             XCTAssertTrue(failure.operationID?.hasSuffix("-startup") == true)
    128         }
    129 
    130         try await Task.sleep(nanoseconds: 80_000_000)
    131         let shutdownCount = await harness.shutdownCount()
    132         XCTAssertEqual(shutdownCount, 1)
    133     }
    134 
    135     func testStopDrainsCancellationIgnoringStartup() async throws {
    136         let harness = RuntimeHarness(startDelayNanoseconds: 100_000_000)
    137         let client = TeraRuntimeClient(factory: harness.start)
    138         let configuration = makeConfiguration(generation: "08")
    139         let startup = Task {
    140             try await client.start(configuration: configuration)
    141         }
    142         try await Task.sleep(nanoseconds: 2_000_000)
    143 
    144         let receipt = try await client.stop()
    145 
    146         XCTAssertEqual(receipt, .alreadyStopped)
    147         let completedCloses = await harness.shutdownCount()
    148         XCTAssertEqual(completedCloses, 1)
    149         do {
    150             _ = try await startup.value
    151             XCTFail("The cancelled startup must not claim backend ownership")
    152         } catch {
    153             XCTAssertEqual(error as? TeraRuntimeClientError, .superseded)
    154         }
    155         let shutdownCount = await harness.shutdownCount()
    156         XCTAssertEqual(shutdownCount, 1)
    157     }
    158 
    159     func testCancellingOneStartupWaiterDoesNotCancelSharedStartup() async throws {
    160         let harness = RuntimeHarness(startDelayNanoseconds: 40_000_000)
    161         let client = TeraRuntimeClient(factory: harness.start)
    162         let configuration = makeConfiguration(generation: "0c")
    163         let cancelledWaiter = Task {
    164             try await client.start(configuration: configuration)
    165         }
    166         let survivingWaiter = Task {
    167             try await client.start(configuration: configuration)
    168         }
    169         try await Task.sleep(nanoseconds: 2_000_000)
    170         cancelledWaiter.cancel()
    171 
    172         do {
    173             _ = try await cancelledWaiter.value
    174             XCTFail("The cancelled waiter must return promptly")
    175         } catch let TeraRuntimeClientError.startup(failure) {
    176             XCTAssertEqual(failure.code, "ios.runtime.cancelled")
    177         }
    178         let snapshot = try await survivingWaiter.value
    179         XCTAssertEqual(snapshot.identity.publicKeyHex, configuration.publicKeyHex)
    180         let startCount = await harness.startCount()
    181         XCTAssertEqual(startCount, 1)
    182         _ = try await client.stop()
    183     }
    184 
    185     func testOperationDeadlineAndLateValueCannotEscape() async throws {
    186         let harness = RuntimeHarness(snapshotDelayNanoseconds: 60_000_000)
    187         let client = TeraRuntimeClient(
    188           factory: harness.start,
    189           deadlines: makeDeadlines(operation: 5_000_000)
    190         )
    191         _ = try await client.start(configuration: makeConfiguration(generation: "09"))
    192 
    193         do {
    194             _ = try await client.snapshot()
    195             XCTFail("A runtime operation that exceeds its deadline must fail")
    196         } catch let TeraRuntimeClientError.status(failure) {
    197             XCTAssertEqual(failure.code, "ios.runtime.deadline_exceeded")
    198             XCTAssertTrue(failure.operationID?.hasSuffix("-operation") == true)
    199         }
    200 
    201         _ = try await client.stop()
    202     }
    203 
    204     func testSuspendCancelsTrackedOperationWithoutClosingRuntime() async throws {
    205         let harness = RuntimeHarness(snapshotDelayNanoseconds: 60_000_000)
    206         let client = TeraRuntimeClient(factory: harness.start)
    207         let configuration = makeConfiguration(generation: "0d")
    208         _ = try await client.start(configuration: configuration)
    209         let pendingSnapshot = Task {
    210             try await client.snapshot()
    211         }
    212         try await Task.sleep(nanoseconds: 2_000_000)
    213 
    214         await client.suspend()
    215 
    216         do {
    217             _ = try await pendingSnapshot.value
    218             XCTFail("Suspension must cancel tracked presentation work")
    219         } catch let TeraRuntimeClientError.status(failure) {
    220             XCTAssertEqual(failure.code, "ios.runtime.cancelled")
    221         }
    222         let lifecycle = await client.lifecycle()
    223         XCTAssertEqual(lifecycle, .running(generation: TeraSessionGeneration(rawValue: 1)))
    224         try await Task.sleep(nanoseconds: 80_000_000)
    225         let recoveredSnapshot = try await client.snapshot()
    226         XCTAssertEqual(recoveredSnapshot.identity.publicKeyHex, configuration.publicKeyHex)
    227         _ = try await client.stop()
    228     }
    229 
    230     func testShutdownDeadlineQuarantinesBackendUntilLateSuccess() async throws {
    231         let harness = RuntimeHarness(shutdownDelayNanoseconds: 60_000_000)
    232         let client = TeraRuntimeClient(
    233           factory: harness.start,
    234           deadlines: makeDeadlines(shutdown: 5_000_000)
    235         )
    236         _ = try await client.start(configuration: makeConfiguration(generation: "0a"))
    237 
    238         do {
    239             _ = try await client.stop()
    240             XCTFail("A shutdown that exceeds its deadline must fail")
    241         } catch let TeraRuntimeClientError.shutdown(failure) {
    242             XCTAssertEqual(failure.code, "ios.runtime.deadline_exceeded")
    243         }
    244         if case .failed = await client.lifecycle() {
    245             // Expected while the detached backend remains quarantined.
    246         } else {
    247             XCTFail("The timed-out backend must remain quarantined")
    248         }
    249 
    250         try await Task.sleep(nanoseconds: 80_000_000)
    251         let finalLifecycle = await client.lifecycle()
    252         let finalReceipt = try await client.stop()
    253         XCTAssertEqual(finalLifecycle, .stopped)
    254         XCTAssertEqual(finalReceipt, .alreadyStopped)
    255     }
    256 
    257     @MainActor
    258     func testObservationFailureIsVisibleAndRecovers() async throws {
    259         let harness = RuntimeHarness(subscriptionFailures: 1)
    260         let client = TeraRuntimeClient(factory: harness.start)
    261         let snapshot = try await client.start(configuration: makeConfiguration(generation: "0b"))
    262         let store = TeraTodayStore(
    263           runtimeClient: client,
    264           contexts: [.defaultContext(snapshot: snapshot)],
    265           observationDelay: { _ in try await Task.sleep(nanoseconds: 20_000_000) }
    266         )
    267 
    268         await store.start()
    269         try await waitUntil {
    270             if case .retrying = store.observationState {
    271                 return true
    272             }
    273             return false
    274         }
    275         try await waitUntil { store.observationState == .active }
    276 
    277         let subscriptionAttempts = await harness.subscriptionAttemptCount()
    278         XCTAssertGreaterThanOrEqual(subscriptionAttempts, 2)
    279         store.stop()
    280         XCTAssertEqual(store.observationState, .stopped)
    281         _ = try await client.stop()
    282     }
    283 
    284     private func makeDeadlines(
    285       startup: UInt64 = 1_000_000_000,
    286       operation: UInt64 = 1_000_000_000,
    287       subscription: UInt64 = 1_000_000_000,
    288       shutdown: UInt64 = 1_000_000_000
    289     ) -> TeraRuntimeDeadlinePolicy {
    290         TeraRuntimeDeadlinePolicy(
    291           startupNanoseconds: startup,
    292           operationNanoseconds: operation,
    293           subscriptionNanoseconds: subscription,
    294           shutdownNanoseconds: shutdown
    295         )
    296     }
    297 
    298     @MainActor
    299     private func waitUntil(
    300         _ predicate: @escaping @MainActor () -> Bool
    301     ) async throws {
    302         for _ in 0 ..< 200 {
    303             if predicate() {
    304                 return
    305             }
    306             try await Task.sleep(nanoseconds: 1_000_000)
    307         }
    308         XCTAssertTrue(predicate())
    309     }
    310 
    311     func makeConfiguration(generation: String) -> TeraRuntimeLaunchConfiguration {
    312         TeraRuntimeLaunchConfiguration(
    313           applicationSupportDirectory: "/tmp/radroots-runtime-tests-\(generation)",
    314           publicKeyHex: String(repeating: generation, count: 32),
    315           sourceGenerationHex: String(repeating: generation, count: 32),
    316           sourceGenerationCreatedAtUnixMilliseconds: 1,
    317           protectedData: .available,
    318           networkProfile: .simulator,
    319           writableRelays: ["ws://127.0.0.1:7447"],
    320           blossom: TeraBlossomEndpointConfiguration(
    321             hostKind: .simulator,
    322             endpointAuthority: .loopbackDevelopment,
    323             primaryOrigin: "http://127.0.0.1:3000",
    324             fallbackOrigins: []
    325           ),
    326           app: TeraRuntimeAppMetadata(
    327             bundleIdentifier: "org.radroots.tests",
    328             version: "0.1.0-alpha",
    329             buildNumber: "1",
    330             buildSHA: nil
    331           ),
    332           signerGeneration: generation,
    333           signer: TestRuntimeSigner(),
    334           adoptBootstrapSettings: false
    335         )
    336     }
    337 }