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
3@testable import RastrilloApple
4
5final class FakePathSource: NativePathSource, @unchecked Sendable {
6 private let lock = NSLock()
7 private var update: (@Sendable (NativePath) -> Void)?
8 private(set) var cancelled = false
9 func start(_ update: @escaping @Sendable (NativePath) -> Void) { lock.withLock { self.update = update } }
10 func cancel() { lock.withLock { cancelled = true } }
11 func send(_ status: NativePath.Status) { lock.withLock { update }?(NativePath(status: status)) }
12}
13
14@MainActor private final class Counter {
15 var count = 0
16 var task: Task<Void, Never>?
17 init(_ connectivity: NativeConnectivity) {
18 let changes = connectivity.changes
19 task = Task { @MainActor [weak self] in for await _ in changes { self?.count += 1 } }
20 }
21}
22
23@MainActor private func settle(until condition: () -> Bool, seconds: Double = 2) async {
24 let deadline = Date().addingTimeInterval(seconds)
25 while !condition() && Date() < deadline { try? await Task.sleep(nanoseconds: 5_000_000) }
26}
27
28@MainActor final class ConnectivityTests: XCTestCase {
29 let origin = URL(string: "https://docs.example")!
30 let other = URL(string: "https://sheets.example")!
31
32 func testOfflineFromThePathOrFromAFailedRequest() async {
33 let source = FakePathSource()
34 let connectivity = NativeConnectivity(source: source)
35 XCTAssertFalse(connectivity.isOffline(origin), "An unknown path must not block requests")
36 source.send(.unsatisfied)
37 await settle { connectivity.path?.status == .unsatisfied }
38 XCTAssertTrue(connectivity.isOffline(origin))
39 XCTAssertTrue(connectivity.isOffline(other))
40 source.send(.satisfied)
41 await settle { connectivity.isPathSatisfied }
42 XCTAssertFalse(connectivity.isOffline(origin))
43
44 connectivity.record(.offline(.cannotConnectToHost), origin: origin)
45 XCTAssertTrue(connectivity.isOffline(origin), "The path is up but that server did not answer")
46 XCTAssertFalse(connectivity.isOffline(other), "One unreachable server says nothing about another")
47 connectivity.record(.responded(status: 503), origin: origin)
48 XCTAssertFalse(connectivity.isOffline(origin), "Any response proves the server is reachable")
49
50 connectivity.record(.offline(.timedOut), origin: origin)
51 source.send(.requiresConnection)
52 await settle { connectivity.path?.status == .requiresConnection }
53 source.send(.satisfied)
54 await settle { connectivity.isPathSatisfied }
55 XCTAssertNil(connectivity.serverReachable[origin], "A path change must forget what the old path learned")
56 }
57
58 func testChangesFireOnPathReachabilityAndForeground() async {
59 let source = FakePathSource()
60 let connectivity = NativeConnectivity(source: source)
61 let counter = Counter(connectivity)
62 defer { counter.task?.cancel() }
63 source.send(.satisfied)
64 await settle { counter.count == 1 }
65 XCTAssertEqual(counter.count, 1, "A path change must fire")
66 connectivity.record(.responded(status: 200), origin: origin)
67 await settle { counter.count == 2 }
68 XCTAssertEqual(counter.count, 2, "A server becoming reachable must wake waiting senders")
69 connectivity.record(.responded(status: 200), origin: origin)
70 connectivity.record(.offline(.timedOut), origin: origin)
71 try? await Task.sleep(nanoseconds: 50_000_000)
72 XCTAssertEqual(counter.count, 2, "Repeated responses and failures are not changes")
73 connectivity.foregrounded()
74 await settle { counter.count == 3 }
75 XCTAssertEqual(counter.count, 3)
76 }
77
78 func testWaitEndsEarlyWhenConnectivityChanges() async {
79 let source = FakePathSource()
80 let connectivity = NativeConnectivity(source: source)
81 source.send(.satisfied)
82 await settle { connectivity.path != nil }
83 let start = Date()
84 let waiting = Task { await connectivity.wait(upTo: 30) }
85 try? await Task.sleep(nanoseconds: 50_000_000)
86 connectivity.foregrounded()
87 let changed = await waiting.value
88 XCTAssertLessThan(Date().timeIntervalSince(start), 2, "A change must cut the backoff short")
89 XCTAssertTrue(changed, "The caller must learn that connectivity changed, to reset its backoff")
90
91 let timed = Date()
92 let elapsed = await connectivity.wait(upTo: 0.05)
93 XCTAssertLessThan(Date().timeIntervalSince(timed), 1, "The delay must elapse on its own")
94 XCTAssertFalse(elapsed, "An elapsed delay is not a change")
95 }
96
97 /// The battery rule: while the path is down, no timer runs at all.
98 func testNoTimedAttemptsWhileThePathIsDown() async {
99 let source = FakePathSource()
100 let connectivity = NativeConnectivity(source: source)
101 source.send(.unsatisfied)
102 await settle { connectivity.path != nil }
103 var returned = false
104 let waiting = Task { @MainActor in await connectivity.wait(upTo: 0.01); returned = true }
105 try? await Task.sleep(nanoseconds: 300_000_000)
106 XCTAssertFalse(returned, "A backoff delay elapsed while the path was down")
107 source.send(.satisfied)
108 await settle { returned }
109 XCTAssertTrue(returned, "A restored path must end the wait at once")
110 waiting.cancel()
111 }
112
113 func testNativeHTTPReportsThroughTheObserver() async throws {
114 let fd = socket(AF_INET, SOCK_STREAM, 0)
115 var address = sockaddr_in()
116 address.sin_family = sa_family_t(AF_INET)
117 address.sin_addr.s_addr = inet_addr("127.0.0.1")
118 var length = socklen_t(MemoryLayout<sockaddr_in>.size)
119 _ = withUnsafeMutablePointer(to: &address) {
120 $0.withMemoryRebound(to: sockaddr.self, capacity: 1) { Darwin.bind(fd, $0, length) == 0 ? getsockname(fd, $0, &length) : -1 }
121 }
122 close(fd)
123 let closed = URL(string: "http://127.0.0.1:\(UInt16(bigEndian: address.sin_port))")!
124 let connectivity = NativeConnectivity(source: FakePathSource())
125 let client = NativeHTTP(origin: closed, observer: connectivity.observer(for: closed))
126 _ = try? await client.responseDetails("/")
127 await settle { connectivity.serverReachable[closed] != nil }
128 XCTAssertEqual(connectivity.serverReachable[closed], false)
129 XCTAssertTrue(connectivity.isOffline(closed))
130 }
131
132 func testNetworkPathSourceDeliversAPath() async {
133 let connectivity = NativeConnectivity(source: NativeNetworkPathSource())
134 await settle(until: { connectivity.path != nil }, seconds: 5)
135 XCTAssertNotNil(connectivity.path, "NWPathMonitor never reported a path")
136 }
137}
138