rastrillo / native Public

Clone
git clone https://amadan.net/rastrillo/native

Plain git — no account needed to clone.

Download

Download this file

1import Foundation
2import XCTest
3import RastrilloNative
4@testable import RastrilloApple
5
6@MainActor final class ReconnectTests: XCTestCase {
7 private func connectivity(_ status: NativePath.Status = .satisfied) async -> (NativeConnectivity, FakePathSource) {
8 let source = FakePathSource()
9 let connectivity = NativeConnectivity(source: source)
10 source.send(status)
11 for _ in 0..<200 where connectivity.path == nil { try? await Task.sleep(nanoseconds: 5_000_000) }
12 return (connectivity, source)
13 }
14
15 /// A Retry-After of 1 s holds even when the app is foregrounded and the
16 /// path changes during it: waking must not cross the server's deadline.
17 func testRetryAfterSurvivesConnectivityWakes() async {
18 let (connectivity, source) = await connectivity()
19 let reconnect = NativeReconnect(connectivity: connectivity, backoff: Backoff(random: { $0.lowerBound }))
20 reconnect.failed(NativeClientError.rateLimited(retryAfter: 1))
21 let start = Date()
22 var finished: Date?
23 let pausing = Task { @MainActor in await reconnect.pause(); finished = Date() }
24 try? await Task.sleep(nanoseconds: 150_000_000)
25 connectivity.foregrounded()
26 source.send(.requiresConnection)
27 try? await Task.sleep(nanoseconds: 100_000_000)
28 source.send(.satisfied)
29 try? await Task.sleep(nanoseconds: 300_000_000)
30 XCTAssertNil(finished, "A wake crossed the server's Retry-After")
31 await pausing.value
32 XCTAssertGreaterThanOrEqual(finished?.timeIntervalSince(start) ?? 0, 0.95)
33 }
34
35 /// After the deadline, the pause still waits for the path if it is down.
36 func testRetryAfterThenWaitsForThePath() async {
37 let (connectivity, source) = await connectivity()
38 let reconnect = NativeReconnect(connectivity: connectivity)
39 reconnect.failed(NativeClientError.rateLimited(retryAfter: 0.2))
40 source.send(.unsatisfied)
41 var finished = false
42 let pausing = Task { @MainActor in await reconnect.pause(); finished = true }
43 try? await Task.sleep(nanoseconds: 1_000_000_000)
44 XCTAssertFalse(finished, "It retried with the path down")
45 source.send(.satisfied)
46 await pausing.value
47 XCTAssertTrue(finished)
48 }
49
50 /// The stable-stream clock starts at `opened()`: an attempt that spent
51 /// longer than 5 s failing, and never opened, does not reset the backoff.
52 func testStableLifetimeIsTimedFromOpen() async {
53 let (connectivity, _) = await connectivity()
54 var clock: TimeInterval = 0
55 let reconnect = NativeReconnect(connectivity: connectivity, backoff: Backoff(random: { $0.lowerBound }), now: { clock })
56 let first = await reconnect.pause() // 0.25
57 clock += 10 // a 10 s refusal, never opened
58 let second = await reconnect.pause()
59 XCTAssertEqual(second, first * 2, "A slow failure counted as a stable stream")
60 reconnect.opened()
61 clock += 6 // open for 6 s, then closed
62 let third = await reconnect.pause()
63 XCTAssertEqual(third, first, "A stream open longer than 5 s must reset the backoff")
64 }
65
66 /// Without a Retry-After, a wake still cuts the backoff short.
67 func testWakeCutsAnOrdinaryBackoffShort() async {
68 let (connectivity, _) = await connectivity()
69 let reconnect = NativeReconnect(connectivity: connectivity, backoff: Backoff(random: { _ in 30 }))
70 let start = Date()
71 let pausing = Task { await reconnect.pause() }
72 try? await Task.sleep(nanoseconds: 100_000_000)
73 connectivity.foregrounded()
74 _ = await pausing.value
75 XCTAssertLessThan(Date().timeIntervalSince(start), 2)
76 }
77}
78