Skip to content

Commit 5e4a684

Browse files
authored
Improve stop() behavior and test coverage. (#18)
1 parent 026c4cc commit 5e4a684

File tree

2 files changed

+158
-30
lines changed

2 files changed

+158
-30
lines changed

Source/LDSwiftEventSource.swift

+45-25
Original file line numberDiff line numberDiff line change
@@ -95,17 +95,24 @@ class EventSourceDelegate: NSObject, URLSessionDataDelegate {
9595

9696
private let delegateQueue: DispatchQueue = DispatchQueue(label: "ESDelegateQueue")
9797

98-
private var readyState: ReadyState = .raw
98+
private var readyState: ReadyState = .raw {
99+
didSet {
100+
#if !os(Linux)
101+
os_log("State: %@ -> %@", log: logger, type: .debug, oldValue.rawValue, readyState.rawValue)
102+
#endif
103+
}
104+
}
99105

100106
private var lastEventId: String?
101107
private var reconnectTime: TimeInterval
102108
private var connectedTime: Date?
103109

104110
private var reconnectionAttempts: Int = 0
105-
private var errorHandlerAction: ConnectionErrorAction?
111+
private var errorHandlerAction: ConnectionErrorAction = .proceed
106112
private let utf8LineParser: UTF8LineParser = UTF8LineParser()
107113
// swiftlint:disable:next implicitly_unwrapped_optional
108114
private var eventParser: EventParser!
115+
private var urlSession: URLSession?
109116
private var sessionTask: URLSessionDataTask?
110117

111118
init(config: EventSource.Config) {
@@ -123,33 +130,30 @@ class EventSourceDelegate: NSObject, URLSessionDataDelegate {
123130
#endif
124131
return
125132
}
133+
self.urlSession = self.createSession()
126134
self.connect()
127135
}
128136
}
129137

130138
func stop() {
139+
let previousState = readyState
140+
readyState = .shutdown
131141
sessionTask?.cancel()
132-
if readyState == .open {
142+
if previousState == .open {
133143
config.handler.onClosed()
134144
}
135-
readyState = .shutdown
136145
}
137146

138147
func getLastEventId() -> String? { lastEventId }
139148

140-
private func connect() {
141-
#if !os(Linux)
142-
os_log("Starting EventSource client", log: logger, type: .info)
143-
#endif
144-
let connectionHandler: ConnectionHandler = (
145-
setReconnectionTime: { reconnectionTime in self.reconnectTime = reconnectionTime },
146-
setLastEventId: { eventId in self.lastEventId = eventId }
147-
)
148-
self.eventParser = EventParser(handler: self.config.handler, connectionHandler: connectionHandler)
149+
func createSession() -> URLSession {
149150
let sessionConfig = URLSessionConfiguration.default
150151
sessionConfig.httpAdditionalHeaders = ["Accept": "text/event-stream", "Cache-Control": "no-cache"]
151152
sessionConfig.timeoutIntervalForRequest = self.config.idleTimeout
152-
let session = URLSession(configuration: sessionConfig, delegate: self, delegateQueue: nil)
153+
return URLSession(configuration: sessionConfig, delegate: self, delegateQueue: nil)
154+
}
155+
156+
func createRequest() -> URLRequest {
153157
var urlRequest = URLRequest(url: self.config.url,
154158
cachePolicy: URLRequest.CachePolicy.reloadIgnoringLocalAndRemoteCacheData,
155159
timeoutInterval: self.config.idleTimeout)
@@ -159,12 +163,24 @@ class EventSourceDelegate: NSObject, URLSessionDataDelegate {
159163
urlRequest.allHTTPHeaderFields = self.config.headerTransform(
160164
urlRequest.allHTTPHeaderFields?.merging(self.config.headers) { $1 } ?? self.config.headers
161165
)
162-
let task = session.dataTask(with: urlRequest)
163-
task.resume()
166+
return urlRequest
167+
}
168+
169+
private func connect() {
170+
#if !os(Linux)
171+
os_log("Starting EventSource client", log: logger, type: .info)
172+
#endif
173+
let connectionHandler: ConnectionHandler = (
174+
setReconnectionTime: { reconnectionTime in self.reconnectTime = reconnectionTime },
175+
setLastEventId: { eventId in self.lastEventId = eventId }
176+
)
177+
self.eventParser = EventParser(handler: self.config.handler, connectionHandler: connectionHandler)
178+
let task = urlSession?.dataTask(with: createRequest())
179+
task?.resume()
164180
sessionTask = task
165181
}
166182

167-
private func dispatchError(error: Error) -> ConnectionErrorAction {
183+
func dispatchError(error: Error) -> ConnectionErrorAction {
168184
let action: ConnectionErrorAction = config.connectionErrorHandler(error)
169185
if action != .shutdown {
170186
config.handler.onError(error: error)
@@ -173,6 +189,9 @@ class EventSourceDelegate: NSObject, URLSessionDataDelegate {
173189
}
174190

175191
private func afterComplete() {
192+
guard readyState != .shutdown
193+
else { return }
194+
176195
var nextState: ReadyState = .closed
177196
let currentState: ReadyState = readyState
178197
if errorHandlerAction == .shutdown {
@@ -182,9 +201,6 @@ class EventSourceDelegate: NSObject, URLSessionDataDelegate {
182201
nextState = .shutdown
183202
}
184203
readyState = nextState
185-
#if !os(Linux)
186-
os_log("State: %@ -> %@", log: logger, type: .debug, currentState.rawValue, nextState.rawValue)
187-
#endif
188204

189205
if currentState == .open {
190206
config.handler.onClosed()
@@ -214,6 +230,8 @@ class EventSourceDelegate: NSObject, URLSessionDataDelegate {
214230
}
215231
}
216232

233+
// MARK: URLSession Delegates
234+
217235
// Tells the delegate that the task finished transferring data.
218236
public func urlSession(_ session: URLSession,
219237
task: URLSessionTask,
@@ -223,7 +241,7 @@ class EventSourceDelegate: NSObject, URLSessionDataDelegate {
223241
eventParser.parse(line: "")
224242

225243
if let error = error {
226-
if readyState != .shutdown {
244+
if readyState != .shutdown && errorHandlerAction != .shutdown {
227245
#if !os(Linux)
228246
os_log("Connection error: %@", log: logger, type: .info, error.localizedDescription)
229247
#endif
@@ -249,6 +267,12 @@ class EventSourceDelegate: NSObject, URLSessionDataDelegate {
249267
os_log("initial reply received", log: logger, type: .debug)
250268
#endif
251269

270+
guard readyState != .shutdown
271+
else {
272+
completionHandler(.cancel)
273+
return
274+
}
275+
252276
// swiftlint:disable:next force_cast
253277
let httpResponse = response as! HTTPURLResponse
254278
if (200..<300).contains(httpResponse.statusCode) {
@@ -261,10 +285,6 @@ class EventSourceDelegate: NSObject, URLSessionDataDelegate {
261285
os_log("Unsuccessful response: %d", log: logger, type: .info, httpResponse.statusCode)
262286
#endif
263287
errorHandlerAction = dispatchError(error: UnsuccessfulResponseError(responseCode: httpResponse.statusCode))
264-
265-
if errorHandlerAction == .shutdown {
266-
readyState = .shutdown
267-
}
268288
completionHandler(.cancel)
269289
}
270290
}

Tests/LDSwiftEventSourceTests.swift

+113-5
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@ import XCTest
33

44
final class LDSwiftEventSourceTests: XCTestCase {
55
func testConfigDefaults() {
6-
let handler = MockHandler()
76
let url = URL(string: "abc")!
8-
let config = EventSource.Config(handler: handler, url: url)
7+
let config = EventSource.Config(handler: MockHandler(), url: url)
98
XCTAssertEqual(config.url, url)
109
XCTAssertEqual(config.method, "GET")
1110
XCTAssertEqual(config.body, nil)
@@ -15,12 +14,13 @@ final class LDSwiftEventSourceTests: XCTestCase {
1514
XCTAssertEqual(config.maxReconnectTime, 30.0)
1615
XCTAssertEqual(config.backoffResetThreshold, 60.0)
1716
XCTAssertEqual(config.idleTimeout, 300.0)
17+
XCTAssertEqual(config.headerTransform(["abc": "123"]), ["abc": "123"])
18+
XCTAssertEqual(config.connectionErrorHandler(DummyError()), .proceed)
1819
}
1920

2021
func testConfigModification() {
21-
let handler = MockHandler()
2222
let url = URL(string: "abc")!
23-
var config = EventSource.Config(handler: handler, url: url)
23+
var config = EventSource.Config(handler: MockHandler(), url: url)
2424

2525
let testBody = "test data".data(using: .utf8)
2626
let testHeaders = ["Authorization": "basic abc"]
@@ -29,11 +29,12 @@ final class LDSwiftEventSourceTests: XCTestCase {
2929
config.body = testBody
3030
config.lastEventId = "eventId"
3131
config.headers = testHeaders
32-
config.headerTransform = { _ in [:] }
3332
config.reconnectTime = 2.0
3433
config.maxReconnectTime = 60.0
3534
config.backoffResetThreshold = 120.0
3635
config.idleTimeout = 180.0
36+
config.headerTransform = { _ in [:] }
37+
config.connectionErrorHandler = { _ in .shutdown }
3738

3839
XCTAssertEqual(config.url, url)
3940
XCTAssertEqual(config.method, "REPORT")
@@ -45,22 +46,129 @@ final class LDSwiftEventSourceTests: XCTestCase {
4546
XCTAssertEqual(config.maxReconnectTime, 60.0)
4647
XCTAssertEqual(config.backoffResetThreshold, 120.0)
4748
XCTAssertEqual(config.idleTimeout, 180.0)
49+
XCTAssertEqual(config.connectionErrorHandler(DummyError()), .shutdown)
50+
}
51+
52+
func testLastEventIdFromConfig() {
53+
var config = EventSource.Config(handler: MockHandler(), url: URL(string: "abc")!)
54+
var es = EventSource(config: config)
55+
XCTAssertEqual(es.getLastEventId(), nil)
56+
config.lastEventId = "def"
57+
es = EventSource(config: config)
58+
XCTAssertEqual(es.getLastEventId(), "def")
59+
}
60+
61+
func testCreatedSession() {
62+
let config = EventSource.Config(handler: MockHandler(), url: URL(string: "abc")!)
63+
let session = EventSourceDelegate(config: config).createSession()
64+
XCTAssertEqual(session.configuration.timeoutIntervalForRequest, config.idleTimeout)
65+
XCTAssertEqual(session.configuration.httpAdditionalHeaders?["Accept"] as? String, "text/event-stream")
66+
XCTAssertEqual(session.configuration.httpAdditionalHeaders?["Cache-Control"] as? String, "no-cache")
67+
}
68+
69+
func testCreateRequest() {
70+
// 192.0.2.1 is assigned as TEST-NET-1 reserved usage.
71+
var config = EventSource.Config(handler: MockHandler(), url: URL(string: "http://192.0.2.1")!)
72+
// Testing default configs
73+
var request = EventSourceDelegate(config: config).createRequest()
74+
XCTAssertEqual(request.url, config.url)
75+
XCTAssertEqual(request.httpMethod, config.method)
76+
XCTAssertEqual(request.httpBody, config.body)
77+
XCTAssertEqual(request.timeoutInterval, config.idleTimeout)
78+
XCTAssertEqual(request.allHTTPHeaderFields, config.headers)
79+
// Testing customized configs
80+
let testBody = "test data".data(using: .utf8)
81+
let testHeaders = ["removing": "a", "updating": "b"]
82+
let overrideHeaders = ["updating": "c", "last-event-id": "eventId2"]
83+
config.method = "REPORT"
84+
config.body = testBody
85+
config.lastEventId = "eventId"
86+
config.headers = testHeaders
87+
config.idleTimeout = 180.0
88+
config.headerTransform = { provided in
89+
XCTAssertEqual(provided, ["removing": "a", "updating": "b", "Last-Event-ID": "eventId"])
90+
return overrideHeaders
91+
}
92+
request = EventSourceDelegate(config: config).createRequest()
93+
XCTAssertEqual(request.url, config.url)
94+
XCTAssertEqual(request.httpMethod, config.method)
95+
XCTAssertEqual(request.httpBody, config.body)
96+
XCTAssertEqual(request.timeoutInterval, config.idleTimeout)
97+
XCTAssertEqual(request.allHTTPHeaderFields, overrideHeaders)
98+
}
99+
100+
func testDispatchError() {
101+
let handler = MockHandler()
102+
var connectionErrorHandlerCallCount = 0
103+
var connectionErrorAction: ConnectionErrorAction = .proceed
104+
var config = EventSource.Config(handler: handler, url: URL(string: "abc")!)
105+
config.connectionErrorHandler = { error in
106+
connectionErrorHandlerCallCount += 1
107+
return connectionErrorAction
108+
}
109+
let es = EventSourceDelegate(config: config)
110+
XCTAssertEqual(es.dispatchError(error: DummyError()), .proceed)
111+
XCTAssertEqual(connectionErrorHandlerCallCount, 1)
112+
guard case .error(let err) = handler.takeEvent(), err is DummyError
113+
else {
114+
XCTFail("handler should receive error if EventSource is not shutting down")
115+
return
116+
}
117+
XCTAssertTrue(handler.receivedEvents.isEmpty)
118+
connectionErrorAction = .shutdown
119+
XCTAssertEqual(es.dispatchError(error: DummyError()), .shutdown)
120+
XCTAssertEqual(connectionErrorHandlerCallCount, 2)
121+
XCTAssertTrue(handler.receivedEvents.isEmpty)
122+
}
123+
}
124+
125+
private enum ReceivedEvent: Equatable {
126+
case opened, closed, message(String, MessageEvent), comment(String), error(Error)
127+
128+
static func == (lhs: ReceivedEvent, rhs: ReceivedEvent) -> Bool {
129+
switch (lhs, rhs) {
130+
case (.opened, .opened):
131+
return true
132+
case (.closed, .closed):
133+
return true
134+
case let (.message(typeLhs, eventLhs), .message(typeRhs, eventRhs)):
135+
return typeLhs == typeRhs && eventLhs == eventRhs
136+
case let (.comment(lhs), .comment(rhs)):
137+
return lhs == rhs
138+
case (.error, .error):
139+
return true
140+
default:
141+
return false
142+
}
48143
}
49144
}
50145

51146
private class MockHandler: EventHandler {
147+
var receivedEvents: [ReceivedEvent] = []
148+
52149
func onOpened() {
150+
receivedEvents.append(.opened)
53151
}
54152

55153
func onClosed() {
154+
receivedEvents.append(.closed)
56155
}
57156

58157
func onMessage(eventType: String, messageEvent: MessageEvent) {
158+
receivedEvents.append(.message(eventType, messageEvent))
59159
}
60160

61161
func onComment(comment: String) {
162+
receivedEvents.append(.comment(comment))
62163
}
63164

64165
func onError(error: Error) {
166+
receivedEvents.append(.error(error))
167+
}
168+
169+
func takeEvent() -> ReceivedEvent {
170+
receivedEvents.remove(at: 0)
65171
}
66172
}
173+
174+
private class DummyError: Error { }

0 commit comments

Comments
 (0)