From b652a966625eacb5241dfba95fd28ade438e25e9 Mon Sep 17 00:00:00 2001 From: Kajorn Pathomkeerati Date: Tue, 10 Nov 2020 14:45:52 +0100 Subject: [PATCH 01/10] Implement network reachability state --- ...SCNetworkReachabilityFlags+Extension.swift | 25 ++++++++++ .../Reachability/NetworkReachability.swift | 43 +++++++++++++++++ .../NetworkReachabilityStatusTests.swift | 47 +++++++++++++++++++ 3 files changed, 115 insertions(+) create mode 100644 Sources/Jetworking/Extensions/SCNetworkReachabilityFlags+Extension.swift create mode 100644 Sources/Jetworking/Utility/Reachability/NetworkReachability.swift create mode 100644 Tests/JetworkingTests/UtilityTests/NetworkReachabilityStatusTests.swift diff --git a/Sources/Jetworking/Extensions/SCNetworkReachabilityFlags+Extension.swift b/Sources/Jetworking/Extensions/SCNetworkReachabilityFlags+Extension.swift new file mode 100644 index 0000000..e7bac70 --- /dev/null +++ b/Sources/Jetworking/Extensions/SCNetworkReachabilityFlags+Extension.swift @@ -0,0 +1,25 @@ +import Foundation +import SystemConfiguration + +extension SCNetworkReachabilityFlags { + var isReachableViaCellular: Bool { + #if os(iOS) + return contains(.isWWAN) + #else + return false + #endif + } + + var isReachableViaNetworkInterface: Bool { + contains(.reachable) && + (!contains(.connectionRequired) || canEstablishConnectionAutomatically) + } + + private var canEstablishConnection: Bool { + !intersection([.connectionOnTraffic, .connectionOnDemand]).isEmpty + } + + private var canEstablishConnectionAutomatically: Bool { + canEstablishConnection && !contains(.interventionRequired) + } +} diff --git a/Sources/Jetworking/Utility/Reachability/NetworkReachability.swift b/Sources/Jetworking/Utility/Reachability/NetworkReachability.swift new file mode 100644 index 0000000..739aa0b --- /dev/null +++ b/Sources/Jetworking/Utility/Reachability/NetworkReachability.swift @@ -0,0 +1,43 @@ +import Foundation +import SystemConfiguration + +/// Typealias for the state of network reachability. +public typealias NetworkStatus = NetworkReachability.State + +/// A container for all enumerations related to network state. +public enum NetworkReachability { + /// Defines the various connection types detected by reachability flags. + public enum ConnectionInterface: Equatable { + /// LAN or WiFi. + case localWiFi + + /// Cellular connection. + case cellular + } + + /// Defines the various states of network reachability. + public enum State: Equatable { + /// It could not be determined whether the network is reachable. + case notDetermined + + /// The network is not reachable. + case unreachable + + /// The network is reachable over an interface `ConnectionInterface`. + case reachable(ConnectionInterface) + + init(_ flags: SCNetworkReachabilityFlags) { + guard flags.isReachableViaNetworkInterface else { + self = .unreachable + return + } + + var networkStatus: Self = .reachable(.localWiFi) + if flags.isReachableViaCellular { + networkStatus = .reachable(.cellular) + } + + self = networkStatus + } + } +} diff --git a/Tests/JetworkingTests/UtilityTests/NetworkReachabilityStatusTests.swift b/Tests/JetworkingTests/UtilityTests/NetworkReachabilityStatusTests.swift new file mode 100644 index 0000000..196b602 --- /dev/null +++ b/Tests/JetworkingTests/UtilityTests/NetworkReachabilityStatusTests.swift @@ -0,0 +1,47 @@ +import SystemConfiguration +import XCTest +@testable import Jetworking + +final class NetworkReachabilityStateTests: XCTestCase { + func testNetworkStatusForNonReachableConnectionThatMustBeEstablishOnDemand() { + let reachabilityFlags: SCNetworkReachabilityFlags = [.connectionOnDemand] + XCTAssertEqual(NetworkStatus(reachabilityFlags), .unreachable) + } + + func testNetworkStatusForReachableConnectionThatRequiresToBeEstablish() { + let reachabilityFlags: SCNetworkReachabilityFlags = [.reachable, .connectionRequired] + XCTAssertEqual(NetworkStatus(reachabilityFlags), .unreachable) + } + + func testNetworkStatusForReachableConnectionThatRequiresToBeEstablishedWithUserIntervention() { + let reachabilityFlags: SCNetworkReachabilityFlags = [.reachable, .connectionRequired, .interventionRequired] + XCTAssertEqual(NetworkStatus(reachabilityFlags), .unreachable) + } + + func testNetworkStatusForReachableConnection() { + let reachabilityFlags: SCNetworkReachabilityFlags = [.reachable] + XCTAssertEqual(NetworkStatus(reachabilityFlags), .reachable(.localWiFi)) + } + + func testNetworkStatusForReachableConnectionThatRequiresToBeEstablishedOnDemand() { + let reachabilityFlags: SCNetworkReachabilityFlags = [.reachable, .connectionRequired, .connectionOnDemand] + XCTAssertEqual(NetworkStatus(reachabilityFlags), .reachable(.localWiFi)) + } + + func testNetworkStatusForReachableConnectionThatRequiresToBeEstablishedOnTraffic() { + let reachabilityFlags: SCNetworkReachabilityFlags = [.reachable, .connectionRequired, .connectionOnTraffic] + XCTAssertEqual(NetworkStatus(reachabilityFlags), .reachable(.localWiFi)) + } + + #if os(iOS) + func testNetworkStatusForReachableConnectionViaCellular() { + let reachabilityFlags: SCNetworkReachabilityFlags = [.reachable, .isWWAN] + XCTAssertEqual(NetworkStatus(reachabilityFlags), .reachable(.cellular)) + } + + func testNetworkStatusForReachableConnectionViaCellularThatRequiresToBeEstablished() { + let reachabilityFlags: SCNetworkReachabilityFlags = [.reachable, .isWWAN, .connectionRequired] + XCTAssertEqual(NetworkStatus(reachabilityFlags), .unreachable) + } + #endif +} From 47c7e09268301f8cab121621e4e23bfd9b62fecc Mon Sep 17 00:00:00 2001 From: Kajorn Pathomkeerati Date: Tue, 10 Nov 2020 16:23:18 +0100 Subject: [PATCH 02/10] Implement network reachability manager --- .../NetworkReachabilityManager.swift | 172 ++++++++++++++++++ .../Reachability/NetworkStatusListener.swift | 64 +++++++ .../NetworkReachabilityManagerTests.swift | 104 +++++++++++ 3 files changed, 340 insertions(+) create mode 100644 Sources/Jetworking/Utility/Reachability/NetworkReachabilityManager.swift create mode 100644 Sources/Jetworking/Utility/Reachability/NetworkStatusListener.swift create mode 100644 Tests/JetworkingTests/UtilityTests/NetworkReachabilityManagerTests.swift diff --git a/Sources/Jetworking/Utility/Reachability/NetworkReachabilityManager.swift b/Sources/Jetworking/Utility/Reachability/NetworkReachabilityManager.swift new file mode 100644 index 0000000..fdcccd1 --- /dev/null +++ b/Sources/Jetworking/Utility/Reachability/NetworkReachabilityManager.swift @@ -0,0 +1,172 @@ +import Foundation +import SystemConfiguration + +/// A closure executed when the network reachability status changes. The closure takes a single argument: the +/// network reachability status. +public typealias NetworkStatusCallback = (NetworkStatus) -> Void + +/// A implementation listens for reachability changes of hosts and addresses +/// for available network interfaces. +/// +/// Network reachability cannot tell your application +/// if you can connect to a particular host, +/// only that an interface is available that might allow a connection, +/// and whether that interface is the WWAN. +open class NetworkReachabilityManager { + // MARK: - Properties + /// Determines whether the network is currently reachable. + open var isReachable: Bool { + status == .reachable(.cellular) || status == .reachable(.localWiFi) + } + + /// Returns the current network reachability status. + open var status: NetworkStatus { + flags.map(NetworkStatus.init) ?? .notDetermined + } + + /// `DispatchQueue` on which reachability will update. + public let reachabilityQueue = DispatchQueue(label: "com.jamitlabs.jetworking.network-reachability") + + /// Flags of the current reachability type, if any. + private var flags: SCNetworkReachabilityFlags? { + var flags = SCNetworkReachabilityFlags() + return (SCNetworkReachabilityGetFlags(reachability, &flags)) ? flags : nil + } + + /// `SCNetworkReachability` instance providing notifications. + private let reachability: SCNetworkReachability + + /// A runtime instance for status listener + private var statusListener: NetworkStatusListener = .init() + + // MARK: - Initialization + /// Creates an instance with the specified host. + /// + /// - Note: The `host` value must *not* contain a scheme (`http`, etc.), just the hostname. + /// + /// - Parameters: + /// - host: Host used to evaluate network reachability. + public convenience init(host: String) throws { + guard let reachability = SCNetworkReachabilityCreateWithName(nil, host) else { + throw NSError( + domain: kCFErrorDomainSystemConfiguration as String, + code: Int(SCError()), + userInfo: [NSLocalizedDescriptionKey: SCErrorString(SCError())] + ) + } + + self.init(reachability: reachability) + } + + /// Creates an instance that monitors the zero address (0.0.0.0). + /// + /// The reachability treats the address as a special token that causes it + /// to actually monitor the general routing status of the device, + /// both IPv4 and IPv6. + public convenience init() throws { + var zeroAddress = sockaddr() + zeroAddress.sa_len = UInt8(MemoryLayout.size) + zeroAddress.sa_family = sa_family_t(AF_INET) + + guard let reachability = SCNetworkReachabilityCreateWithAddress(nil, &zeroAddress) else { + throw NSError( + domain: kCFErrorDomainSystemConfiguration as String, + code: Int(SCError()), + userInfo: [NSLocalizedDescriptionKey: SCErrorString(SCError())] + ) + } + + self.init(reachability: reachability) + } + + private init(reachability: SCNetworkReachability) { + self.reachability = reachability + } + + deinit { + stopListening() + } + + // MARK: - Listening + /// Starts listening for changes in network reachability status. + /// + /// - Note: Stops and removes any existing listener. + /// + /// - Parameters: + /// - queue: A queue on which to call the callback closure. Use the main queue by default. + /// - callback: A closure called when reachability changes. + open func startListening( + on queue: DispatchQueue = .main, + withCallbackOnStatusUpdate callback: @escaping NetworkStatusCallback + ) throws { + stopListening() + + statusListener.update { state in + state.callbackQueue = queue + state.callback = callback + } + + var context = SCNetworkReachabilityContext( + version: 0, + info: Unmanaged.passUnretained(self).toOpaque(), + retain: nil, + release: nil, + copyDescription: nil + ) + let callback: SCNetworkReachabilityCallBack = { _, flags, info in + guard let info = info else { return } + + let instance = Unmanaged.fromOpaque(info).takeUnretainedValue() + instance.notifyListener(flags) + } + + if !SCNetworkReachabilitySetDispatchQueue(reachability, reachabilityQueue) { + stopListening() + throw NSError( + domain: kCFErrorDomainSystemConfiguration as String, + code: Int(SCError()), + userInfo: [NSLocalizedDescriptionKey: SCErrorString(SCError())] + ) + } + + if !SCNetworkReachabilitySetCallback(reachability, callback, &context) { + stopListening() + throw NSError( + domain: kCFErrorDomainSystemConfiguration as String, + code: Int(SCError()), + userInfo: [NSLocalizedDescriptionKey: SCErrorString(SCError())] + ) + } + + // Initial status check + flags.flatMap { currentFlags in + reachabilityQueue.async { self.notifyListener(currentFlags) } + } + } + + /// Stops listening for changes in network reachability status. + open func stopListening() { + SCNetworkReachabilitySetCallback(reachability, nil, nil) + SCNetworkReachabilitySetDispatchQueue(reachability, nil) + statusListener.reset() + } + + // MARK: - Internal - Listener Notification + /// Calls the callback closure in the callback queue if the computed status has been changed. + /// + /// - Note: Should only be called from the `reachabilityQueue`. + /// + /// - Parameter flags: `SCNetworkReachabilityFlags` to use to calculate the status. + func notifyListener(_ flags: SCNetworkReachabilityFlags) { + let newStatus = NetworkStatus(flags) + + statusListener.update { listener in + guard listener.previousStatus != newStatus else { return } + + listener.previousStatus = newStatus + + let callback = listener.callback + listener.callbackQueue?.async { callback?(newStatus) } + } + } +} diff --git a/Sources/Jetworking/Utility/Reachability/NetworkStatusListener.swift b/Sources/Jetworking/Utility/Reachability/NetworkStatusListener.swift new file mode 100644 index 0000000..b2eb649 --- /dev/null +++ b/Sources/Jetworking/Utility/Reachability/NetworkStatusListener.swift @@ -0,0 +1,64 @@ +import Foundation + +/// Mutable storage for network status and callback. +struct NetworkStatusListener { + /// A closure executed when the network reachability status changes. + var callback: NetworkStatusCallback? + + /// A working queue on which listeners will be called. + var callbackQueue: DispatchQueue? + + /// Network status in previous time. + var previousStatus: NetworkStatus? + + private let lock: UnfairLock = .init() + + init( + callback: NetworkStatusCallback? = nil, + callbackQueue: DispatchQueue? = nil, + previousStatus: NetworkStatus? = nil + ) { + self.callback = callback + self.callbackQueue = callbackQueue + self.previousStatus = previousStatus + } + + mutating func update(callback: @escaping (inout Self) -> Void) { + lock.lock() + defer { lock.unlock() } + + callback(&self) + } + + mutating func reset() { + lock.lock() + defer { lock.unlock() } + + callback = nil + callbackQueue = nil + previousStatus = nil + } +} + +/// An implementation of unfair lock in Swift +fileprivate final class UnfairLock { + private let unfairLock: os_unfair_lock_t + + init() { + unfairLock = .allocate(capacity: 1) + unfairLock.initialize(to: os_unfair_lock()) + } + + deinit { + unfairLock.deinitialize(count: 1) + unfairLock.deallocate() + } + + func lock() { + os_unfair_lock_lock(unfairLock) + } + + func unlock() { + os_unfair_lock_unlock(unfairLock) + } +} diff --git a/Tests/JetworkingTests/UtilityTests/NetworkReachabilityManagerTests.swift b/Tests/JetworkingTests/UtilityTests/NetworkReachabilityManagerTests.swift new file mode 100644 index 0000000..27a94f6 --- /dev/null +++ b/Tests/JetworkingTests/UtilityTests/NetworkReachabilityManagerTests.swift @@ -0,0 +1,104 @@ +import XCTest +@testable import Jetworking + +final class NetworkReachabilityManagerTests: XCTestCase { + var timeout: TimeInterval = 10 + + func testManagerInitializedWithHost() { + let manager: NetworkReachabilityManager? = try? .init(host: "localhost") + + XCTAssertNotNil(manager) + } + + func testManagerInitializedWithAddress() { + let manager: NetworkReachabilityManager? = try? .init() + + XCTAssertNotNil(manager) + } + + func testHostManagerStartWithReachableStatus() { + let manager: NetworkReachabilityManager? = try? .init(host: "localhost") + + XCTAssertEqual(manager?.isReachable, true) + XCTAssertEqual(manager?.status, .reachable(.localWiFi)) + } + + func testAddressManagerStartWithReachableStatus() { + let manager: NetworkReachabilityManager? = try? .init() + + XCTAssertEqual(manager?.isReachable, true) + XCTAssertEqual(manager?.status, .reachable(.localWiFi)) + } + + func testHostManagerRestart() { + let manager: NetworkReachabilityManager? = try? .init(host: "localhost") + let firstCallbackExpectation = expectation(description: "First callback should be called") + let secondCallbackExpectation = expectation(description: "Second callback should be called") + + try? manager?.startListening { _ in + firstCallbackExpectation.fulfill() + } + wait(for: [firstCallbackExpectation], timeout: timeout) + + manager?.stopListening() + + try? manager?.startListening { _ in + secondCallbackExpectation.fulfill() + } + wait(for: [secondCallbackExpectation], timeout: timeout) + + XCTAssertEqual(manager?.status, .reachable(.localWiFi)) + } + + func testAddressManagerRestart() { + let manager: NetworkReachabilityManager? = try? .init() + let firstCallbackExpectation = expectation(description: "First callback should be called") + let secondCallbackExpectation = expectation(description: "Second callback should be called") + + try? manager?.startListening { _ in + firstCallbackExpectation.fulfill() + } + wait(for: [firstCallbackExpectation], timeout: timeout) + + manager?.stopListening() + + try? manager?.startListening { _ in + secondCallbackExpectation.fulfill() + } + wait(for: [secondCallbackExpectation], timeout: timeout) + + XCTAssertEqual(manager?.status, .reachable(.localWiFi)) + } + + func testHostManagerDeinitialized() { + let expect = expectation(description: "Reachability queue should get cleared") + var manager: NetworkReachabilityManager? = try? .init(host: "localhost") + weak var weakManager = manager + + try? manager?.startListening(withCallbackOnStatusUpdate: { _ in }) + manager?.stopListening() + manager?.reachabilityQueue.async { expect.fulfill() } + manager = nil + + waitForExpectations(timeout: timeout) + + XCTAssertNil(manager) + XCTAssertNil(weakManager) + } + + func testAddressManagerDeinitialized() { + let expect = expectation(description: "Reachability queue should get clear") + var manager: NetworkReachabilityManager? = try? .init() + weak var weakManager = manager + + try? manager?.startListening(withCallbackOnStatusUpdate: { _ in }) + manager?.stopListening() + manager?.reachabilityQueue.async { expect.fulfill() } + manager = nil + + waitForExpectations(timeout: timeout) + + XCTAssertNil(manager) + XCTAssertNil(weakManager) + } +} From 07ccd5a1714526aa5420ab1155fd6fa81a92e462 Mon Sep 17 00:00:00 2001 From: Kajorn Pathomkeerati Date: Wed, 11 Nov 2020 17:33:08 +0100 Subject: [PATCH 03/10] Implement client task executor --- Sources/Jetworking/Client/Client.swift | 8 ++++ .../Client/Executor/ClientTaskExecutor.swift | 45 +++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 Sources/Jetworking/Client/Executor/ClientTaskExecutor.swift diff --git a/Sources/Jetworking/Client/Client.swift b/Sources/Jetworking/Client/Client.swift index 8dbe182..cfd4ead 100644 --- a/Sources/Jetworking/Client/Client.swift +++ b/Sources/Jetworking/Client/Client.swift @@ -9,6 +9,14 @@ enum APIError: Error { public final class Client { public typealias RequestCompletion = (HTTPURLResponse?, Result) -> Void + + internal enum Task { + case dataTask(request: URLRequest, completion: ((Data?, URLResponse?, Error?) -> Void)) + case downloadTask(request: URLRequest) + case uploadDataTask(request: URLRequest, data: Data) + case uploadFileTask(request: URLRequest, fileURL: URL) + } + // MARK: - Properties private let configuration: Configuration diff --git a/Sources/Jetworking/Client/Executor/ClientTaskExecutor.swift b/Sources/Jetworking/Client/Executor/ClientTaskExecutor.swift new file mode 100644 index 0000000..1364e7a --- /dev/null +++ b/Sources/Jetworking/Client/Executor/ClientTaskExecutor.swift @@ -0,0 +1,45 @@ +import Foundation + +enum ClientTaskError: Error { + case unexpectedTaskExecution + case connectionUnavailable +} + +final class ClientTaskExecutor: NSObject { + internal static let `default` = ClientTaskExecutor() + + private var reachabilityManager: NetworkReachabilityManager? = try? .init() + + override init() { + super.init() + + do { + try reachabilityManager?.startListening { _ in } + } catch { + NSLog("[WARNING] Faild to start network monitor. Error: \(error)") + } + } + + func perform(_ task: Client.Task, on executor: T) throws -> CancellableRequest? { + guard reachabilityManager == nil || reachabilityManager?.isReachable == true else { + throw ClientTaskError.connectionUnavailable + } + + switch (executor, task) { + case (is RequestExecutor, let .dataTask(request, completionHandler)) : + return (executor as! RequestExecutor).send(request: request, completionHandler) + + case (is DownloadExecutor, let .downloadTask(request)): + return (executor as! DownloadExecutor).download(request: request) + + case (is UploadExecutor, let .uploadDataTask(request, data)): + return (executor as! UploadExecutor).upload(request: request, from: data) + + case (is UploadExecutor, let .uploadFileTask(request, fileURL)): + return (executor as! UploadExecutor).upload(request: request, fromFile: fileURL) + + default: + throw ClientTaskError.unexpectedTaskExecution + } + } +} From 0f7f2c5ad8963a2455cacb90b56a1df8a941fa6d Mon Sep 17 00:00:00 2001 From: Kajorn Pathomkeerati Date: Wed, 11 Nov 2020 17:33:26 +0100 Subject: [PATCH 04/10] Apply client task executor --- Sources/Jetworking/Client/Client.swift | 29 +++++++++++++++++++------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/Sources/Jetworking/Client/Client.swift b/Sources/Jetworking/Client/Client.swift index cfd4ead..4c6e6b1 100644 --- a/Sources/Jetworking/Client/Client.swift +++ b/Sources/Jetworking/Client/Client.swift @@ -22,6 +22,8 @@ public final class Client { private lazy var session: URLSession = .init(configuration: .default) + private let taskExecutor: ClientTaskExecutor = .default + private lazy var requestExecutor: RequestExecutor = { switch configuration.requestExecutorType { case .sync: @@ -101,7 +103,7 @@ public final class Client { public func get(endpoint: Endpoint, _ completion: @escaping RequestCompletion) -> CancellableRequest? { do { let request: URLRequest = try createRequest(forHttpMethod: .GET, and: endpoint) - return requestExecutor.send(request: request) { [weak self] data, urlResponse, error in + let task: Task = .dataTask(request: request) { [weak self] data, urlResponse, error in self?.handleResponse( data: data, urlResponse: urlResponse, @@ -110,6 +112,7 @@ public final class Client { completion: completion ) } + return try taskExecutor.perform(task, on: requestExecutor) } catch { enqueue(completion(nil, .failure(error))) } @@ -122,7 +125,7 @@ public final class Client { do { let bodyData: Data = try configuration.encoder.encode(body) let request: URLRequest = try createRequest(forHttpMethod: .POST, and: endpoint, and: bodyData) - return requestExecutor.send(request: request) { [weak self] data, urlResponse, error in + let task: Task = .dataTask(request: request) { [weak self] data, urlResponse, error in self?.handleResponse( data: data, urlResponse: urlResponse, @@ -131,6 +134,7 @@ public final class Client { completion: completion ) } + return try taskExecutor.perform(task, on: requestExecutor) } catch { enqueue(completion(nil, .failure(error))) } @@ -143,7 +147,7 @@ public final class Client { do { let bodyData: Data = try configuration.encoder.encode(body) let request: URLRequest = try createRequest(forHttpMethod: .PUT, and: endpoint, and: bodyData) - return requestExecutor.send(request: request) { [weak self] data, urlResponse, error in + let task: Task = .dataTask(request: request) { [weak self] data, urlResponse, error in self?.handleResponse( data: data, urlResponse: urlResponse, @@ -152,6 +156,7 @@ public final class Client { completion: completion ) } + return try taskExecutor.perform(task, on: requestExecutor) } catch { enqueue(completion(nil, .failure(error))) } @@ -168,7 +173,7 @@ public final class Client { do { let bodyData: Data = try configuration.encoder.encode(body) let request: URLRequest = try createRequest(forHttpMethod: .PATCH, and: endpoint, and: bodyData) - return requestExecutor.send(request: request) { [weak self] data, urlResponse, error in + let task: Task = .dataTask(request: request) { [weak self] data, urlResponse, error in self?.handleResponse( data: data, urlResponse: urlResponse, @@ -177,6 +182,7 @@ public final class Client { completion: completion ) } + return try taskExecutor.perform(task, on: requestExecutor) } catch { enqueue(completion(nil, .failure(error))) } @@ -188,7 +194,7 @@ public final class Client { public func delete(endpoint: Endpoint, parameter: [String: Any] = [:], _ completion: @escaping RequestCompletion) -> CancellableRequest? { do { let request: URLRequest = try createRequest(forHttpMethod: .DELETE, and: endpoint) - return requestExecutor.send(request: request) { [weak self] data, urlResponse, error in + let task: Task = .dataTask(request: request) { [weak self] data, urlResponse, error in self?.handleResponse( data: data, urlResponse: urlResponse, @@ -197,6 +203,7 @@ public final class Client { completion: completion ) } + return try taskExecutor.perform(task, on: requestExecutor) } catch { enqueue(completion(nil, .failure(error))) } @@ -214,7 +221,7 @@ public final class Client { guard checkForValidDownloadURL(url) else { return nil } let request: URLRequest = .init(url: url) - let task = downloadExecutor.download(request: request) + let task = try? taskExecutor.perform(.downloadTask(request: request), on: downloadExecutor) task.flatMap { executingDownloads[$0.identifier] = DownloadHandler( progressHandler: progressHandler, @@ -232,7 +239,10 @@ public final class Client { _ completion: @escaping UploadHandler.CompletionHandler ) -> CancellableRequest? { let request: URLRequest = .init(url: url, httpMethod: .POST) - let task = uploadExecutor.upload(request: request, fromFile: fileURL) + let task = try? taskExecutor.perform( + .uploadFileTask(request: request, fileURL: fileURL), + on: uploadExecutor + ) task.flatMap { executingUploads[$0.identifier] = UploadHandler( progressHandler: progressHandler, @@ -267,7 +277,10 @@ public final class Client { // TODO: Extract into constants request.setValue("\(multipartType.rawValue); boundary=\(boundary)", forHTTPHeaderField: "Content-Type") - let task = uploadExecutor.upload(request: request, from: multipartData) + let task = try? taskExecutor.perform( + .uploadDataTask(request: request, data: multipartData), + on: uploadExecutor + ) task.flatMap { executingUploads[$0.identifier] = UploadHandler( progressHandler: progressHandler, From 20ff52abf0702b5152ab06a3a4ee7457776d4404 Mon Sep 17 00:00:00 2001 From: Kajorn Pathomkeerati Date: Thu, 12 Nov 2020 09:17:21 +0100 Subject: [PATCH 05/10] Rename network status --- .../Reachability/NetworkReachability.swift | 2 +- .../NetworkReachabilityManager.swift | 38 ++++++++++--------- ...=> NetworkReachabilityStateListener.swift} | 12 +++--- .../NetworkReachabilityManagerTests.swift | 12 +++--- .../NetworkReachabilityStatusTests.swift | 16 ++++---- 5 files changed, 41 insertions(+), 39 deletions(-) rename Sources/Jetworking/Utility/Reachability/{NetworkStatusListener.swift => NetworkReachabilityStateListener.swift} (78%) diff --git a/Sources/Jetworking/Utility/Reachability/NetworkReachability.swift b/Sources/Jetworking/Utility/Reachability/NetworkReachability.swift index 739aa0b..6f24893 100644 --- a/Sources/Jetworking/Utility/Reachability/NetworkReachability.swift +++ b/Sources/Jetworking/Utility/Reachability/NetworkReachability.swift @@ -2,7 +2,7 @@ import Foundation import SystemConfiguration /// Typealias for the state of network reachability. -public typealias NetworkStatus = NetworkReachability.State +public typealias NetworkReachabilityState = NetworkReachability.State /// A container for all enumerations related to network state. public enum NetworkReachability { diff --git a/Sources/Jetworking/Utility/Reachability/NetworkReachabilityManager.swift b/Sources/Jetworking/Utility/Reachability/NetworkReachabilityManager.swift index fdcccd1..df98200 100644 --- a/Sources/Jetworking/Utility/Reachability/NetworkReachabilityManager.swift +++ b/Sources/Jetworking/Utility/Reachability/NetworkReachabilityManager.swift @@ -3,7 +3,7 @@ import SystemConfiguration /// A closure executed when the network reachability status changes. The closure takes a single argument: the /// network reachability status. -public typealias NetworkStatusCallback = (NetworkStatus) -> Void +public typealias NetworkReachabilityStateCallback = (NetworkReachabilityState) -> Void /// A implementation listens for reachability changes of hosts and addresses /// for available network interfaces. @@ -13,15 +13,17 @@ public typealias NetworkStatusCallback = (NetworkStatus) -> Void /// only that an interface is available that might allow a connection, /// and whether that interface is the WWAN. open class NetworkReachabilityManager { + public static let `default`: NetworkReachabilityManager? = try? NetworkReachabilityManager() + // MARK: - Properties /// Determines whether the network is currently reachable. open var isReachable: Bool { - status == .reachable(.cellular) || status == .reachable(.localWiFi) + state == .reachable(.cellular) || state == .reachable(.localWiFi) } - /// Returns the current network reachability status. - open var status: NetworkStatus { - flags.map(NetworkStatus.init) ?? .notDetermined + /// Returns the current network reachability state. + open var state: NetworkReachabilityState { + flags.map(NetworkReachabilityState.init) ?? .notDetermined } /// `DispatchQueue` on which reachability will update. @@ -36,8 +38,8 @@ open class NetworkReachabilityManager { /// `SCNetworkReachability` instance providing notifications. private let reachability: SCNetworkReachability - /// A runtime instance for status listener - private var statusListener: NetworkStatusListener = .init() + /// A runtime instance for state listener + private var stateListener: NetworkReachabilityStateListener = .init() // MARK: - Initialization /// Creates an instance with the specified host. @@ -79,7 +81,7 @@ open class NetworkReachabilityManager { self.init(reachability: reachability) } - private init(reachability: SCNetworkReachability) { + init(reachability: SCNetworkReachability) { self.reachability = reachability } @@ -88,7 +90,7 @@ open class NetworkReachabilityManager { } // MARK: - Listening - /// Starts listening for changes in network reachability status. + /// Starts listening for changes in network reachability state. /// /// - Note: Stops and removes any existing listener. /// @@ -97,11 +99,11 @@ open class NetworkReachabilityManager { /// - callback: A closure called when reachability changes. open func startListening( on queue: DispatchQueue = .main, - withCallbackOnStatusUpdate callback: @escaping NetworkStatusCallback + withCallbackOnStateChange callback: @escaping NetworkReachabilityStateCallback ) throws { stopListening() - statusListener.update { state in + stateListener.update { state in state.callbackQueue = queue state.callback = callback } @@ -138,29 +140,29 @@ open class NetworkReachabilityManager { ) } - // Initial status check + // Initial state check flags.flatMap { currentFlags in reachabilityQueue.async { self.notifyListener(currentFlags) } } } - /// Stops listening for changes in network reachability status. + /// Stops listening for changes in network reachability state. open func stopListening() { SCNetworkReachabilitySetCallback(reachability, nil, nil) SCNetworkReachabilitySetDispatchQueue(reachability, nil) - statusListener.reset() + stateListener.reset() } // MARK: - Internal - Listener Notification - /// Calls the callback closure in the callback queue if the computed status has been changed. + /// Calls the callback closure in the callback queue if the computed state has been changed. /// /// - Note: Should only be called from the `reachabilityQueue`. /// - /// - Parameter flags: `SCNetworkReachabilityFlags` to use to calculate the status. + /// - Parameter flags: `SCNetworkReachabilityFlags` to use to calculate the state. func notifyListener(_ flags: SCNetworkReachabilityFlags) { - let newStatus = NetworkStatus(flags) + let newStatus = NetworkReachabilityState(flags) - statusListener.update { listener in + stateListener.update { listener in guard listener.previousStatus != newStatus else { return } listener.previousStatus = newStatus diff --git a/Sources/Jetworking/Utility/Reachability/NetworkStatusListener.swift b/Sources/Jetworking/Utility/Reachability/NetworkReachabilityStateListener.swift similarity index 78% rename from Sources/Jetworking/Utility/Reachability/NetworkStatusListener.swift rename to Sources/Jetworking/Utility/Reachability/NetworkReachabilityStateListener.swift index b2eb649..927ac81 100644 --- a/Sources/Jetworking/Utility/Reachability/NetworkStatusListener.swift +++ b/Sources/Jetworking/Utility/Reachability/NetworkReachabilityStateListener.swift @@ -1,22 +1,22 @@ import Foundation /// Mutable storage for network status and callback. -struct NetworkStatusListener { - /// A closure executed when the network reachability status changes. - var callback: NetworkStatusCallback? +struct NetworkReachabilityStateListener { + /// A closure executed when the network reachability state changes. + var callback: NetworkReachabilityStateCallback? /// A working queue on which listeners will be called. var callbackQueue: DispatchQueue? /// Network status in previous time. - var previousStatus: NetworkStatus? + var previousStatus: NetworkReachabilityState? private let lock: UnfairLock = .init() init( - callback: NetworkStatusCallback? = nil, + callback: NetworkReachabilityStateCallback? = nil, callbackQueue: DispatchQueue? = nil, - previousStatus: NetworkStatus? = nil + previousStatus: NetworkReachabilityState? = nil ) { self.callback = callback self.callbackQueue = callbackQueue diff --git a/Tests/JetworkingTests/UtilityTests/NetworkReachabilityManagerTests.swift b/Tests/JetworkingTests/UtilityTests/NetworkReachabilityManagerTests.swift index 27a94f6..ab76e84 100644 --- a/Tests/JetworkingTests/UtilityTests/NetworkReachabilityManagerTests.swift +++ b/Tests/JetworkingTests/UtilityTests/NetworkReachabilityManagerTests.swift @@ -20,14 +20,14 @@ final class NetworkReachabilityManagerTests: XCTestCase { let manager: NetworkReachabilityManager? = try? .init(host: "localhost") XCTAssertEqual(manager?.isReachable, true) - XCTAssertEqual(manager?.status, .reachable(.localWiFi)) + XCTAssertEqual(manager?.state, .reachable(.localWiFi)) } func testAddressManagerStartWithReachableStatus() { let manager: NetworkReachabilityManager? = try? .init() XCTAssertEqual(manager?.isReachable, true) - XCTAssertEqual(manager?.status, .reachable(.localWiFi)) + XCTAssertEqual(manager?.state, .reachable(.localWiFi)) } func testHostManagerRestart() { @@ -47,7 +47,7 @@ final class NetworkReachabilityManagerTests: XCTestCase { } wait(for: [secondCallbackExpectation], timeout: timeout) - XCTAssertEqual(manager?.status, .reachable(.localWiFi)) + XCTAssertEqual(manager?.state, .reachable(.localWiFi)) } func testAddressManagerRestart() { @@ -67,7 +67,7 @@ final class NetworkReachabilityManagerTests: XCTestCase { } wait(for: [secondCallbackExpectation], timeout: timeout) - XCTAssertEqual(manager?.status, .reachable(.localWiFi)) + XCTAssertEqual(manager?.state, .reachable(.localWiFi)) } func testHostManagerDeinitialized() { @@ -75,7 +75,7 @@ final class NetworkReachabilityManagerTests: XCTestCase { var manager: NetworkReachabilityManager? = try? .init(host: "localhost") weak var weakManager = manager - try? manager?.startListening(withCallbackOnStatusUpdate: { _ in }) + try? manager?.startListening(withCallbackOnStateChange: { _ in }) manager?.stopListening() manager?.reachabilityQueue.async { expect.fulfill() } manager = nil @@ -91,7 +91,7 @@ final class NetworkReachabilityManagerTests: XCTestCase { var manager: NetworkReachabilityManager? = try? .init() weak var weakManager = manager - try? manager?.startListening(withCallbackOnStatusUpdate: { _ in }) + try? manager?.startListening(withCallbackOnStateChange: { _ in }) manager?.stopListening() manager?.reachabilityQueue.async { expect.fulfill() } manager = nil diff --git a/Tests/JetworkingTests/UtilityTests/NetworkReachabilityStatusTests.swift b/Tests/JetworkingTests/UtilityTests/NetworkReachabilityStatusTests.swift index 196b602..6656425 100644 --- a/Tests/JetworkingTests/UtilityTests/NetworkReachabilityStatusTests.swift +++ b/Tests/JetworkingTests/UtilityTests/NetworkReachabilityStatusTests.swift @@ -5,43 +5,43 @@ import XCTest final class NetworkReachabilityStateTests: XCTestCase { func testNetworkStatusForNonReachableConnectionThatMustBeEstablishOnDemand() { let reachabilityFlags: SCNetworkReachabilityFlags = [.connectionOnDemand] - XCTAssertEqual(NetworkStatus(reachabilityFlags), .unreachable) + XCTAssertEqual(NetworkReachabilityState(reachabilityFlags), .unreachable) } func testNetworkStatusForReachableConnectionThatRequiresToBeEstablish() { let reachabilityFlags: SCNetworkReachabilityFlags = [.reachable, .connectionRequired] - XCTAssertEqual(NetworkStatus(reachabilityFlags), .unreachable) + XCTAssertEqual(NetworkReachabilityState(reachabilityFlags), .unreachable) } func testNetworkStatusForReachableConnectionThatRequiresToBeEstablishedWithUserIntervention() { let reachabilityFlags: SCNetworkReachabilityFlags = [.reachable, .connectionRequired, .interventionRequired] - XCTAssertEqual(NetworkStatus(reachabilityFlags), .unreachable) + XCTAssertEqual(NetworkReachabilityState(reachabilityFlags), .unreachable) } func testNetworkStatusForReachableConnection() { let reachabilityFlags: SCNetworkReachabilityFlags = [.reachable] - XCTAssertEqual(NetworkStatus(reachabilityFlags), .reachable(.localWiFi)) + XCTAssertEqual(NetworkReachabilityState(reachabilityFlags), .reachable(.localWiFi)) } func testNetworkStatusForReachableConnectionThatRequiresToBeEstablishedOnDemand() { let reachabilityFlags: SCNetworkReachabilityFlags = [.reachable, .connectionRequired, .connectionOnDemand] - XCTAssertEqual(NetworkStatus(reachabilityFlags), .reachable(.localWiFi)) + XCTAssertEqual(NetworkReachabilityState(reachabilityFlags), .reachable(.localWiFi)) } func testNetworkStatusForReachableConnectionThatRequiresToBeEstablishedOnTraffic() { let reachabilityFlags: SCNetworkReachabilityFlags = [.reachable, .connectionRequired, .connectionOnTraffic] - XCTAssertEqual(NetworkStatus(reachabilityFlags), .reachable(.localWiFi)) + XCTAssertEqual(NetworkReachabilityState(reachabilityFlags), .reachable(.localWiFi)) } #if os(iOS) func testNetworkStatusForReachableConnectionViaCellular() { let reachabilityFlags: SCNetworkReachabilityFlags = [.reachable, .isWWAN] - XCTAssertEqual(NetworkStatus(reachabilityFlags), .reachable(.cellular)) + XCTAssertEqual(NetworkReachabilityState(reachabilityFlags), .reachable(.cellular)) } func testNetworkStatusForReachableConnectionViaCellularThatRequiresToBeEstablished() { let reachabilityFlags: SCNetworkReachabilityFlags = [.reachable, .isWWAN, .connectionRequired] - XCTAssertEqual(NetworkStatus(reachabilityFlags), .unreachable) + XCTAssertEqual(NetworkReachabilityState(reachabilityFlags), .unreachable) } #endif } From 6f1d4dd24d90ddfc244579d14295bcdf008dca94 Mon Sep 17 00:00:00 2001 From: Kajorn Pathomkeerati Date: Fri, 13 Nov 2020 12:23:20 +0100 Subject: [PATCH 06/10] Add abstract APIs for reachability monitor --- .../Client/Executor/ClientTaskExecutor.swift | 7 +++-- .../NetworkReachabilityManager.swift | 6 +--- .../NetworkReachabilityMonitor.swift | 28 +++++++++++++++++++ 3 files changed, 33 insertions(+), 8 deletions(-) create mode 100644 Sources/Jetworking/Utility/Reachability/NetworkReachabilityMonitor.swift diff --git a/Sources/Jetworking/Client/Executor/ClientTaskExecutor.swift b/Sources/Jetworking/Client/Executor/ClientTaskExecutor.swift index 1364e7a..5f4b5df 100644 --- a/Sources/Jetworking/Client/Executor/ClientTaskExecutor.swift +++ b/Sources/Jetworking/Client/Executor/ClientTaskExecutor.swift @@ -8,13 +8,14 @@ enum ClientTaskError: Error { final class ClientTaskExecutor: NSObject { internal static let `default` = ClientTaskExecutor() - private var reachabilityManager: NetworkReachabilityManager? = try? .init() + private var reachabilityManager: NetworkReachabilityMonitor? - override init() { + init(reachabilityMonitor: NetworkReachabilityMonitor? = NetworkReachabilityManager.default) { + self.reachabilityManager = reachabilityMonitor super.init() do { - try reachabilityManager?.startListening { _ in } + try self.reachabilityManager?.startListening(on: .main) { _ in } } catch { NSLog("[WARNING] Faild to start network monitor. Error: \(error)") } diff --git a/Sources/Jetworking/Utility/Reachability/NetworkReachabilityManager.swift b/Sources/Jetworking/Utility/Reachability/NetworkReachabilityManager.swift index df98200..90c69f7 100644 --- a/Sources/Jetworking/Utility/Reachability/NetworkReachabilityManager.swift +++ b/Sources/Jetworking/Utility/Reachability/NetworkReachabilityManager.swift @@ -1,10 +1,6 @@ import Foundation import SystemConfiguration -/// A closure executed when the network reachability status changes. The closure takes a single argument: the -/// network reachability status. -public typealias NetworkReachabilityStateCallback = (NetworkReachabilityState) -> Void - /// A implementation listens for reachability changes of hosts and addresses /// for available network interfaces. /// @@ -12,7 +8,7 @@ public typealias NetworkReachabilityStateCallback = (NetworkReachabilityState) - /// if you can connect to a particular host, /// only that an interface is available that might allow a connection, /// and whether that interface is the WWAN. -open class NetworkReachabilityManager { +open class NetworkReachabilityManager: NetworkReachabilityMonitor { public static let `default`: NetworkReachabilityManager? = try? NetworkReachabilityManager() // MARK: - Properties diff --git a/Sources/Jetworking/Utility/Reachability/NetworkReachabilityMonitor.swift b/Sources/Jetworking/Utility/Reachability/NetworkReachabilityMonitor.swift new file mode 100644 index 0000000..91bcc41 --- /dev/null +++ b/Sources/Jetworking/Utility/Reachability/NetworkReachabilityMonitor.swift @@ -0,0 +1,28 @@ +import Foundation + +/// A closure executed when the network reachability status changes. The closure takes a single argument: the +/// network reachability status. +public typealias NetworkReachabilityStateCallback = (NetworkReachabilityState) -> Void + +public protocol NetworkReachabilityMonitor { + /// Determines whether the network is currently reachable. + var isReachable: Bool { get } + + /// Returns the current network reachability state. + var state: NetworkReachabilityState { get } + + /// Starts listening for changes in network reachability state. + /// + /// - Note: Stops and removes any existing listener. + /// + /// - Parameters: + /// - queue: A queue on which to call the callback closure. Use the main queue by default. + /// - callback: A closure called when reachability changes. + func startListening( + on queue: DispatchQueue, + withCallbackOnStateChange callback: @escaping NetworkReachabilityStateCallback + ) throws + + /// Stops listening for changes in network reachability state. + func stopListening() +} From b040ee1ac5cfb6e54810356bad56af4a05c9e685 Mon Sep 17 00:00:00 2001 From: Kajorn Pathomkeerati Date: Fri, 13 Nov 2020 15:55:11 +0100 Subject: [PATCH 07/10] Add tests for client task executor --- .../ClientTaskExecutorTests.swift | 158 ++++++++++++++++++ .../Mocks/Executors/MockAsyncDelegation.swift | 5 + .../MockDownloadExecutorDelegation.swift | 16 ++ .../MockUploadExecutorDelegation.swift | 16 ++ .../MockNetworkReachability.swift | 25 +++ 5 files changed, 220 insertions(+) create mode 100644 Tests/JetworkingTests/ExecutorTests/ClientTaskExecutorTests.swift create mode 100644 Tests/JetworkingTests/Mocks/Executors/MockAsyncDelegation.swift create mode 100644 Tests/JetworkingTests/Mocks/Executors/MockDownloadExecutorDelegation.swift create mode 100644 Tests/JetworkingTests/Mocks/Executors/MockUploadExecutorDelegation.swift create mode 100644 Tests/JetworkingTests/Mocks/NetworkReachability/MockNetworkReachability.swift diff --git a/Tests/JetworkingTests/ExecutorTests/ClientTaskExecutorTests.swift b/Tests/JetworkingTests/ExecutorTests/ClientTaskExecutorTests.swift new file mode 100644 index 0000000..1b37b32 --- /dev/null +++ b/Tests/JetworkingTests/ExecutorTests/ClientTaskExecutorTests.swift @@ -0,0 +1,158 @@ +import XCTest +@testable import Jetworking + +final class ClientTaskExecutorTests: XCTestCase { + // MARK: - Network state tests + func testClientTaskExecutorWithoutNetworkReachabilityCheck() { + let taskExecutor = ClientTaskExecutor(reachabilityMonitor: nil) + let requestExecutor = AsyncRequestExecutor(session: URLSession(configuration: .default)) + XCTAssertNoThrow(try taskExecutor.perform(makeSampleClientDataTask(), on: requestExecutor)) + } + + func testClientTaskExecutorInUnreachableNetwork() { + let taskExecutor = makeSampleClientTaskExecutor(state: .unreachable) + let requestExecutor = AsyncRequestExecutor(session: URLSession(configuration: .default)) + XCTAssertThrowsError(try taskExecutor.perform(makeSampleClientDataTask(), on: requestExecutor)) + } + + func testClientTaskExecutorInNotDeterminedNetwork() { + let taskExecutor = makeSampleClientTaskExecutor(state: .notDetermined) + let requestExecutor = AsyncRequestExecutor(session: URLSession(configuration: .default)) + XCTAssertThrowsError(try taskExecutor.perform(makeSampleClientDataTask(), on: requestExecutor)) + } + + func testClientTaskExecutorInReachableNetwork() { + let taskExecutor = makeSampleClientTaskExecutor(state: .reachable(.localWiFi)) + let requestExecutor = AsyncRequestExecutor(session: URLSession(configuration: .default)) + XCTAssertNoThrow(try taskExecutor.perform(makeSampleClientDataTask(), on: requestExecutor)) + } + + func testClientTaskExecutorInUnreachableNetworkThenReachableNetwork() { + let reachabilityMonitor = makeSampleReachabilityMonitor(state: .unreachable) + let taskExecutor = ClientTaskExecutor(reachabilityMonitor: reachabilityMonitor) + + let requestExecutor = AsyncRequestExecutor(session: URLSession(configuration: .default)) + XCTAssertThrowsError(try taskExecutor.perform(makeSampleClientDataTask(), on: requestExecutor)) + + (reachabilityMonitor as? MockNetworkReachabilityMonitor)?.reachabilityState = .reachable(.localWiFi) + + XCTAssertNoThrow(try taskExecutor.perform(makeSampleClientDataTask(), on: requestExecutor)) + } + + func testClientTaskExecutorInReachableNetworkThenUnreachableNetwork() { + let reachabilityMonitor = makeSampleReachabilityMonitor(state: .reachable(.localWiFi)) + let taskExecutor = ClientTaskExecutor(reachabilityMonitor: reachabilityMonitor) + + let requestExecutor = AsyncRequestExecutor(session: URLSession(configuration: .default)) + XCTAssertNoThrow(try taskExecutor.perform(makeSampleClientDataTask(), on: requestExecutor)) + + (reachabilityMonitor as? MockNetworkReachabilityMonitor)?.reachabilityState = .unreachable + + XCTAssertThrowsError(try taskExecutor.perform(makeSampleClientDataTask(), on: requestExecutor)) + } + + // MARK: - Task operation tests + func testClientTaskExecutorWithDataTask() { + let taskExecutor: ClientTaskExecutor = makeSampleClientTaskExecutor() + let task: Client.Task = makeSampleClientDataTask() + + let requestExecutor = AsyncRequestExecutor(session: URLSession(configuration: .default)) + XCTAssertNoThrow(try taskExecutor.perform(task, on: requestExecutor)) + + let downloadExecutor = DefaultDownloadExecutor( + sessionConfiguration: URLSession.shared.configuration, + downloadExecutorDelegate: MockDownloadExecutorDelegation() + ) + XCTAssertThrowsError(try taskExecutor.perform(task, on: downloadExecutor)) + + let uploadExecutor = DefaultUploadExecutor.init( + sessionConfiguration: URLSession.shared.configuration, + uploadExecutorDelegate: MockUploadExecutorDelegation() + ) + XCTAssertThrowsError(try taskExecutor.perform(task, on: uploadExecutor)) + } + + func testClientTaskExecutorWithDownloadTask() { + let taskExecutor: ClientTaskExecutor = makeSampleClientTaskExecutor() + let task: Client.Task = .downloadTask(request: makeSampleRequest()) + + let requestExecutor = AsyncRequestExecutor(session: URLSession(configuration: .default)) + XCTAssertThrowsError(try taskExecutor.perform(task, on: requestExecutor)) + + let downloadExecutor = DefaultDownloadExecutor( + sessionConfiguration: URLSession.shared.configuration, + downloadExecutorDelegate: MockDownloadExecutorDelegation() + ) + XCTAssertNoThrow(try taskExecutor.perform(task, on: downloadExecutor)) + + let uploadExecutor = DefaultUploadExecutor.init( + sessionConfiguration: URLSession.shared.configuration, + uploadExecutorDelegate: MockUploadExecutorDelegation() + ) + XCTAssertThrowsError(try taskExecutor.perform(task, on: uploadExecutor)) + } + + func testClientTaskExecutorWithUploadDataTask() { + let task: Client.Task = .uploadDataTask(request: makeSampleRequest(), data: Data()) + let taskExecutor: ClientTaskExecutor = makeSampleClientTaskExecutor() + + let requestExecutor = AsyncRequestExecutor(session: URLSession(configuration: .default)) + XCTAssertThrowsError(try taskExecutor.perform(task, on: requestExecutor)) + + let downloadExecutor = DefaultDownloadExecutor( + sessionConfiguration: URLSession.shared.configuration, + downloadExecutorDelegate: MockDownloadExecutorDelegation() + ) + XCTAssertThrowsError(try taskExecutor.perform(task, on: downloadExecutor)) + + let uploadExecutor = DefaultUploadExecutor.init( + sessionConfiguration: URLSession.shared.configuration, + uploadExecutorDelegate: MockUploadExecutorDelegation() + ) + XCTAssertNoThrow(try taskExecutor.perform(task, on: uploadExecutor)) + } + + func testClientTaskExecutorWithUploadFileTask() { + let filePath = Bundle.module.path(forResource: "avatar", ofType: ".png")! + var components: URLComponents = .init() + components.scheme = "file" + components.path = filePath + + let task: Client.Task = .uploadFileTask(request: makeSampleRequest(), fileURL: components.url!) + let taskExecutor: ClientTaskExecutor = makeSampleClientTaskExecutor() + + let requestExecutor = AsyncRequestExecutor(session: URLSession(configuration: .default)) + XCTAssertThrowsError(try taskExecutor.perform(task, on: requestExecutor)) + + let downloadExecutor = DefaultDownloadExecutor( + sessionConfiguration: URLSession.shared.configuration, + downloadExecutorDelegate: MockDownloadExecutorDelegation() + ) + XCTAssertThrowsError(try taskExecutor.perform(task, on: downloadExecutor)) + + let uploadExecutor = DefaultUploadExecutor.init( + sessionConfiguration: URLSession.shared.configuration, + uploadExecutorDelegate: MockUploadExecutorDelegation() + ) + XCTAssertNoThrow(try taskExecutor.perform(task, on: uploadExecutor)) + } +} + +extension ClientTaskExecutorTests { + func makeSampleRequest() -> URLRequest { + .init(url: URL(string: "http://localhost")!) + } + + func makeSampleReachabilityMonitor(state: NetworkReachabilityState) -> NetworkReachabilityMonitor { + MockNetworkReachabilityMonitor(state: state) + } + + func makeSampleClientTaskExecutor(state: NetworkReachabilityState = .reachable(.localWiFi)) -> ClientTaskExecutor { + let reachabilityMonitor = makeSampleReachabilityMonitor(state: state) + return ClientTaskExecutor(reachabilityMonitor: reachabilityMonitor) + } + + func makeSampleClientDataTask() -> Client.Task { + .dataTask(request: makeSampleRequest(), completion: { _,_,_ in }) + } +} diff --git a/Tests/JetworkingTests/Mocks/Executors/MockAsyncDelegation.swift b/Tests/JetworkingTests/Mocks/Executors/MockAsyncDelegation.swift new file mode 100644 index 0000000..906a829 --- /dev/null +++ b/Tests/JetworkingTests/Mocks/Executors/MockAsyncDelegation.swift @@ -0,0 +1,5 @@ +import Foundation + +class MockAsyncDelegation { + var callback: (Bool) -> Void = { _ in } +} diff --git a/Tests/JetworkingTests/Mocks/Executors/MockDownloadExecutorDelegation.swift b/Tests/JetworkingTests/Mocks/Executors/MockDownloadExecutorDelegation.swift new file mode 100644 index 0000000..ec9b52e --- /dev/null +++ b/Tests/JetworkingTests/Mocks/Executors/MockDownloadExecutorDelegation.swift @@ -0,0 +1,16 @@ +import Foundation +import Jetworking + +final class MockDownloadExecutorDelegation: MockAsyncDelegation, DownloadExecutorDelegate { + func downloadExecutor(_ downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { + // Implement this if needed + } + + func downloadExecutor(_ downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { + callback(true) + } + + func downloadExecutor(_ downloadTask: URLSessionDownloadTask, didCompleteWithError error: Error?) { + callback(error == nil) + } +} diff --git a/Tests/JetworkingTests/Mocks/Executors/MockUploadExecutorDelegation.swift b/Tests/JetworkingTests/Mocks/Executors/MockUploadExecutorDelegation.swift new file mode 100644 index 0000000..f5c0bad --- /dev/null +++ b/Tests/JetworkingTests/Mocks/Executors/MockUploadExecutorDelegation.swift @@ -0,0 +1,16 @@ +import Foundation +import Jetworking + +final class MockUploadExecutorDelegation: MockAsyncDelegation, UploadExecutorDelegate { + func uploadExecutor(_ uploadTask: URLSessionUploadTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { + // Implement this if needed + } + + func uploadExecutor(didFinishWith uploadTask: URLSessionUploadTask) { + callback(true) + } + + func uploadExecutor(_ uploadTask: URLSessionUploadTask, didCompleteWithError error: Error?) { + callback(error == nil) + } +} diff --git a/Tests/JetworkingTests/Mocks/NetworkReachability/MockNetworkReachability.swift b/Tests/JetworkingTests/Mocks/NetworkReachability/MockNetworkReachability.swift new file mode 100644 index 0000000..c87a1da --- /dev/null +++ b/Tests/JetworkingTests/Mocks/NetworkReachability/MockNetworkReachability.swift @@ -0,0 +1,25 @@ +import Foundation +import Jetworking + +final class MockNetworkReachabilityMonitor: NetworkReachabilityMonitor { + var isReachable: Bool { if case .reachable = state { return true } else { return false } } + + var state: NetworkReachabilityState { reachabilityState } + + var reachabilityState: NetworkReachabilityState + + init(state: NetworkReachabilityState = .notDetermined) { + reachabilityState = state + } + + func startListening( + on queue: DispatchQueue, + withCallbackOnStateChange callback: @escaping NetworkReachabilityStateCallback + ) throws { + // Implement this if needed + } + + func stopListening() { + // Implement this if needed + } +} From e1a7049ce95b676454e7e4f6566573af20f0ddfe Mon Sep 17 00:00:00 2001 From: Kajorn Pathomkeerati Date: Fri, 13 Nov 2020 16:25:39 +0100 Subject: [PATCH 08/10] Add a link to system library --- Package.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index b5463bd..9e7b805 100644 --- a/Package.swift +++ b/Package.swift @@ -19,7 +19,10 @@ let package = Package( // Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets can depend on other targets in this package, and on products in packages which this package depends on. .target( - name: "Jetworking" + name: "Jetworking", + linkerSettings: [ + .linkedFramework("SystemConfiguration") + ] ), .testTarget( name: "JetworkingTests", From 65be8216bc4e1a93b8a2d05a19368ab56d7b0691 Mon Sep 17 00:00:00 2001 From: Kajorn Pathomkeerati Date: Mon, 16 Nov 2020 11:28:16 +0100 Subject: [PATCH 09/10] Add connection error --- .../Jetworking/Client/Executor/ClientTaskExecutor.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Sources/Jetworking/Client/Executor/ClientTaskExecutor.swift b/Sources/Jetworking/Client/Executor/ClientTaskExecutor.swift index 5f4b5df..05f8b3c 100644 --- a/Sources/Jetworking/Client/Executor/ClientTaskExecutor.swift +++ b/Sources/Jetworking/Client/Executor/ClientTaskExecutor.swift @@ -2,7 +2,12 @@ import Foundation enum ClientTaskError: Error { case unexpectedTaskExecution - case connectionUnavailable + + static let connectionUnavailable = NSError( + domain: URLError.errorDomain, + code: URLError.Code.notConnectedToInternet.rawValue, + userInfo: nil + ) } final class ClientTaskExecutor: NSObject { From c154ae1f55665ba3677f318d54965f0a8529acde Mon Sep 17 00:00:00 2001 From: Kajorn Pathomkeerati Date: Mon, 16 Nov 2020 12:17:59 +0100 Subject: [PATCH 10/10] Rename a connection interface Apple has eliminated usage of the term "localWiFi" for reachability. For more details: https://developer.apple.com/library/archive/samplecode/Reachability/Listings/ReadMe_md.html --- .../Utility/Reachability/NetworkReachability.swift | 6 +++--- .../Utility/Reachability/NetworkReachabilityManager.swift | 2 +- .../ExecutorTests/ClientTaskExecutorTests.swift | 8 ++++---- .../UtilityTests/NetworkReachabilityManagerTests.swift | 8 ++++---- .../UtilityTests/NetworkReachabilityStatusTests.swift | 6 +++--- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Sources/Jetworking/Utility/Reachability/NetworkReachability.swift b/Sources/Jetworking/Utility/Reachability/NetworkReachability.swift index 6f24893..d18e28d 100644 --- a/Sources/Jetworking/Utility/Reachability/NetworkReachability.swift +++ b/Sources/Jetworking/Utility/Reachability/NetworkReachability.swift @@ -8,8 +8,8 @@ public typealias NetworkReachabilityState = NetworkReachability.State public enum NetworkReachability { /// Defines the various connection types detected by reachability flags. public enum ConnectionInterface: Equatable { - /// LAN or WiFi. - case localWiFi + /// Wired ethernet or WiFi. + case wiredOrWirelessLAN /// Cellular connection. case cellular @@ -32,7 +32,7 @@ public enum NetworkReachability { return } - var networkStatus: Self = .reachable(.localWiFi) + var networkStatus: Self = .reachable(.wiredOrWirelessLAN) if flags.isReachableViaCellular { networkStatus = .reachable(.cellular) } diff --git a/Sources/Jetworking/Utility/Reachability/NetworkReachabilityManager.swift b/Sources/Jetworking/Utility/Reachability/NetworkReachabilityManager.swift index 90c69f7..f7c8d62 100644 --- a/Sources/Jetworking/Utility/Reachability/NetworkReachabilityManager.swift +++ b/Sources/Jetworking/Utility/Reachability/NetworkReachabilityManager.swift @@ -14,7 +14,7 @@ open class NetworkReachabilityManager: NetworkReachabilityMonitor { // MARK: - Properties /// Determines whether the network is currently reachable. open var isReachable: Bool { - state == .reachable(.cellular) || state == .reachable(.localWiFi) + state == .reachable(.cellular) || state == .reachable(.wiredOrWirelessLAN) } /// Returns the current network reachability state. diff --git a/Tests/JetworkingTests/ExecutorTests/ClientTaskExecutorTests.swift b/Tests/JetworkingTests/ExecutorTests/ClientTaskExecutorTests.swift index 1b37b32..d3ee4c1 100644 --- a/Tests/JetworkingTests/ExecutorTests/ClientTaskExecutorTests.swift +++ b/Tests/JetworkingTests/ExecutorTests/ClientTaskExecutorTests.swift @@ -22,7 +22,7 @@ final class ClientTaskExecutorTests: XCTestCase { } func testClientTaskExecutorInReachableNetwork() { - let taskExecutor = makeSampleClientTaskExecutor(state: .reachable(.localWiFi)) + let taskExecutor = makeSampleClientTaskExecutor(state: .reachable(.wiredOrWirelessLAN)) let requestExecutor = AsyncRequestExecutor(session: URLSession(configuration: .default)) XCTAssertNoThrow(try taskExecutor.perform(makeSampleClientDataTask(), on: requestExecutor)) } @@ -34,13 +34,13 @@ final class ClientTaskExecutorTests: XCTestCase { let requestExecutor = AsyncRequestExecutor(session: URLSession(configuration: .default)) XCTAssertThrowsError(try taskExecutor.perform(makeSampleClientDataTask(), on: requestExecutor)) - (reachabilityMonitor as? MockNetworkReachabilityMonitor)?.reachabilityState = .reachable(.localWiFi) + (reachabilityMonitor as? MockNetworkReachabilityMonitor)?.reachabilityState = .reachable(.wiredOrWirelessLAN) XCTAssertNoThrow(try taskExecutor.perform(makeSampleClientDataTask(), on: requestExecutor)) } func testClientTaskExecutorInReachableNetworkThenUnreachableNetwork() { - let reachabilityMonitor = makeSampleReachabilityMonitor(state: .reachable(.localWiFi)) + let reachabilityMonitor = makeSampleReachabilityMonitor(state: .reachable(.wiredOrWirelessLAN)) let taskExecutor = ClientTaskExecutor(reachabilityMonitor: reachabilityMonitor) let requestExecutor = AsyncRequestExecutor(session: URLSession(configuration: .default)) @@ -147,7 +147,7 @@ extension ClientTaskExecutorTests { MockNetworkReachabilityMonitor(state: state) } - func makeSampleClientTaskExecutor(state: NetworkReachabilityState = .reachable(.localWiFi)) -> ClientTaskExecutor { + func makeSampleClientTaskExecutor(state: NetworkReachabilityState = .reachable(.wiredOrWirelessLAN)) -> ClientTaskExecutor { let reachabilityMonitor = makeSampleReachabilityMonitor(state: state) return ClientTaskExecutor(reachabilityMonitor: reachabilityMonitor) } diff --git a/Tests/JetworkingTests/UtilityTests/NetworkReachabilityManagerTests.swift b/Tests/JetworkingTests/UtilityTests/NetworkReachabilityManagerTests.swift index ab76e84..8649144 100644 --- a/Tests/JetworkingTests/UtilityTests/NetworkReachabilityManagerTests.swift +++ b/Tests/JetworkingTests/UtilityTests/NetworkReachabilityManagerTests.swift @@ -20,14 +20,14 @@ final class NetworkReachabilityManagerTests: XCTestCase { let manager: NetworkReachabilityManager? = try? .init(host: "localhost") XCTAssertEqual(manager?.isReachable, true) - XCTAssertEqual(manager?.state, .reachable(.localWiFi)) + XCTAssertEqual(manager?.state, .reachable(.wiredOrWirelessLAN)) } func testAddressManagerStartWithReachableStatus() { let manager: NetworkReachabilityManager? = try? .init() XCTAssertEqual(manager?.isReachable, true) - XCTAssertEqual(manager?.state, .reachable(.localWiFi)) + XCTAssertEqual(manager?.state, .reachable(.wiredOrWirelessLAN)) } func testHostManagerRestart() { @@ -47,7 +47,7 @@ final class NetworkReachabilityManagerTests: XCTestCase { } wait(for: [secondCallbackExpectation], timeout: timeout) - XCTAssertEqual(manager?.state, .reachable(.localWiFi)) + XCTAssertEqual(manager?.state, .reachable(.wiredOrWirelessLAN)) } func testAddressManagerRestart() { @@ -67,7 +67,7 @@ final class NetworkReachabilityManagerTests: XCTestCase { } wait(for: [secondCallbackExpectation], timeout: timeout) - XCTAssertEqual(manager?.state, .reachable(.localWiFi)) + XCTAssertEqual(manager?.state, .reachable(.wiredOrWirelessLAN)) } func testHostManagerDeinitialized() { diff --git a/Tests/JetworkingTests/UtilityTests/NetworkReachabilityStatusTests.swift b/Tests/JetworkingTests/UtilityTests/NetworkReachabilityStatusTests.swift index 6656425..a5c4f0c 100644 --- a/Tests/JetworkingTests/UtilityTests/NetworkReachabilityStatusTests.swift +++ b/Tests/JetworkingTests/UtilityTests/NetworkReachabilityStatusTests.swift @@ -20,17 +20,17 @@ final class NetworkReachabilityStateTests: XCTestCase { func testNetworkStatusForReachableConnection() { let reachabilityFlags: SCNetworkReachabilityFlags = [.reachable] - XCTAssertEqual(NetworkReachabilityState(reachabilityFlags), .reachable(.localWiFi)) + XCTAssertEqual(NetworkReachabilityState(reachabilityFlags), .reachable(.wiredOrWirelessLAN)) } func testNetworkStatusForReachableConnectionThatRequiresToBeEstablishedOnDemand() { let reachabilityFlags: SCNetworkReachabilityFlags = [.reachable, .connectionRequired, .connectionOnDemand] - XCTAssertEqual(NetworkReachabilityState(reachabilityFlags), .reachable(.localWiFi)) + XCTAssertEqual(NetworkReachabilityState(reachabilityFlags), .reachable(.wiredOrWirelessLAN)) } func testNetworkStatusForReachableConnectionThatRequiresToBeEstablishedOnTraffic() { let reachabilityFlags: SCNetworkReachabilityFlags = [.reachable, .connectionRequired, .connectionOnTraffic] - XCTAssertEqual(NetworkReachabilityState(reachabilityFlags), .reachable(.localWiFi)) + XCTAssertEqual(NetworkReachabilityState(reachabilityFlags), .reachable(.wiredOrWirelessLAN)) } #if os(iOS)