1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
@testable import Grain
import XCTest
final class NetworkRetryTests: GrainTestCase {
// MARK: - Classification
func testDroppedConnectionsAreTransient() {
for code in [URLError.timedOut, .networkConnectionLost, .notConnectedToInternet, .cannotConnectToHost, .dnsLookupFailed] {
XCTAssertTrue(NetworkRetry.isTransient(URLError(code)), "\(code) should be retried")
}
}
func testServerAndRateLimitStatusesAreTransient() {
for status in [408, 425, 429, 500, 502, 503, 504] {
XCTAssertTrue(
NetworkRetry.isTransient(XRPCError.httpError(statusCode: status, body: nil)),
"HTTP \(status) should be retried"
)
}
}
/// The important half: retrying these would either fail identically or, if
/// the call weren't idempotent, duplicate work.
func testClientErrorsAndCancellationAreNotTransient() {
for status in [400, 401, 403, 404, 409, 413] {
XCTAssertFalse(
NetworkRetry.isTransient(XRPCError.httpError(statusCode: status, body: nil)),
"HTTP \(status) should not be retried"
)
}
XCTAssertFalse(NetworkRetry.isTransient(CancellationError()))
XCTAssertFalse(NetworkRetry.isTransient(XRPCError.unauthorized))
XCTAssertFalse(NetworkRetry.isTransient(URLError(.badURL)))
}
// MARK: - Behaviour
func testRetriesUntilTheOperationSucceeds() async throws {
var attempts = 0
let result = try await NetworkRetry.run(attempts: 4) { () -> String in
attempts += 1
if attempts < 3 {
throw URLError(.networkConnectionLost)
}
return "posted"
}
XCTAssertEqual(result, "posted")
XCTAssertEqual(attempts, 3)
}
func testGivesUpAfterTheAttemptBudgetAndRethrows() async {
var attempts = 0
do {
_ = try await NetworkRetry.run(attempts: 3) { () -> String in
attempts += 1
throw URLError(.timedOut)
}
XCTFail("expected the final failure to propagate")
} catch {
XCTAssertEqual((error as? URLError)?.code, .timedOut)
}
XCTAssertEqual(attempts, 3)
}
func testDoesNotRetryNonTransientFailures() async {
var attempts = 0
do {
_ = try await NetworkRetry.run(attempts: 4) { () -> String in
attempts += 1
throw XRPCError.httpError(statusCode: 400, body: nil)
}
XCTFail("expected the failure to propagate")
} catch {
// expected
}
XCTAssertEqual(attempts, 1, "a 400 must not be repeated")
}
func testReportsEachRetryToTheCaller() async throws {
var retries: [Int] = []
var attempts = 0
_ = try await NetworkRetry.run(attempts: 4, onRetry: { attempt, _ in retries.append(attempt) }) { () -> Int in
attempts += 1
if attempts < 3 {
throw URLError(.timedOut)
}
return attempts
}
XCTAssertEqual(retries, [1, 2])
}
}