Implement URLSession.DataTaskPublisher (#127)

This commit is contained in:
Sergej Jaskiewicz
2019-12-13 16:44:03 +03:00
committed by GitHub
parent cf41c25cf7
commit c6536cf8d3
8 changed files with 1003 additions and 21 deletions
-13
View File
@@ -54,19 +54,6 @@ extension Timer {
}
}
extension URLSession {
public func dataTaskPublisher(for url: Foundation.URL) -> Foundation.URLSession.DataTaskPublisher
public func dataTaskPublisher(for request: Foundation.URLRequest) -> Foundation.URLSession.DataTaskPublisher
public struct DataTaskPublisher : Combine.Publisher {
public typealias Output = (data: Foundation.Data, response: Foundation.URLResponse)
public typealias Failure = Foundation.URLError
public let request: Foundation.URLRequest
public let session: Foundation.URLSession
public init(request: Foundation.URLRequest, session: Foundation.URLSession)
public func receive<S>(subscriber: S) where S : Combine.Subscriber, S.Failure == Foundation.URLSession.DataTaskPublisher.Failure, S.Input == Foundation.URLSession.DataTaskPublisher.Output
}
}
extension OperationQueue : Combine.Scheduler {
public struct SchedulerTimeType : Swift.Strideable, Swift.Codable, Swift.Hashable {
public var date: Foundation.Date
@@ -0,0 +1,17 @@
//
// Violations.swift
//
//
// Created by Sergej Jaskiewicz on 13.12.2019.
//
import OpenCombine
extension Subscribers.Demand {
internal func assertNonZero(file: StaticString = #file,
line: UInt = #line) {
if self == .none {
fatalError("API Violation: demand must not be zero", file: file, line: line)
}
}
}
@@ -0,0 +1,233 @@
//
// URLSession.swift
//
//
// Created by Sergej Jaskiewicz on 13.12.2019.
//
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
import OpenCombine
extension URLSession {
/// A namespace for disambiguation when both OpenCombine and Foundation are imported.
///
/// Foundation extends `URLSession` with new methods and nested types.
/// If you import both OpenCombine and Foundation, you will not be able
/// to write `URLSession.DataTaskPublisher`,
/// because Swift is unable to understand which `DataTaskPublisher`
/// you're referring to the one declared in Foundation or in OpenCombine.
///
/// So you have to write `URLSession.OCombine.DataTaskPublisher`.
///
/// This bug is tracked [here](https://bugs.swift.org/browse/SR-11183).
///
/// You can omit this whenever Combine is not available (e. g. on Linux).
public struct OCombine {
public let session: URLSession
public init(_ session: URLSession) {
self.session = session
}
public struct DataTaskPublisher: Publisher {
public typealias Output = (data: Data, response: URLResponse)
public typealias Failure = URLError
public let request: URLRequest
public let session: URLSession
public init(request: URLRequest, session: URLSession) {
self.request = request
self.session = session
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Failure == Failure, Downstream.Input == Output
{
let subscription = Inner(parent: self, downstream: subscriber)
subscriber.receive(subscription: subscription)
}
}
/// Returns a publisher that wraps a URL session data task for a given URL.
///
/// The publisher publishes data when the task completes, or terminates if
/// the task fails with an error.
///
/// - Parameter url: The URL for which to create a data task.
/// - Returns: A publisher that wraps a data task for the URL.
public func dataTaskPublisher(for url: URL) -> DataTaskPublisher {
return dataTaskPublisher(for: URLRequest(url: url))
}
/// Returns a publisher that wraps a URL session data task for a given
/// URL request.
///
/// The publisher publishes data when the task completes, or terminates if
/// the task fails with an error.
///
/// - Parameter request: The URL request for which to create a data task.
/// - Returns: A publisher that wraps a data task for the URL request.
public func dataTaskPublisher(for request: URLRequest) -> DataTaskPublisher {
return .init(request: request, session: session)
}
}
#if !canImport(Combine)
public typealias DataTaskPublisher = OCombine.DataTaskPublisher
#endif
}
extension URLSession {
/// A namespace for disambiguation when both OpenCombine and Foundation are imported.
///
/// Foundation extends `URLSession` with new methods and nested types.
/// If you import both OpenCombine and Foundation, you will not be able
/// to write `URLSession.shared.dataTaskPublisher(for: url)`,
/// because Swift is unable to understand which `dataTaskPublisher` method
/// you're referring to the one declared in Foundation or in OpenCombine.
///
/// So you have to write `URLSession.shared.ocombine.dataTaskPublisher(for: url)`.
///
/// This bug is tracked [here](https://bugs.swift.org/browse/SR-11183).
///
/// You can omit this whenever Combine is not available (e. g. on Linux).
public var ocombine: OCombine { return .init(self) }
#if !canImport(Combine)
/// Returns a publisher that wraps a URL session data task for a given URL.
///
/// The publisher publishes data when the task completes, or terminates if the task
/// fails with an error.
///
/// - Parameter url: The URL for which to create a data task.
/// - Returns: A publisher that wraps a data task for the URL.
public func dataTaskPublisher(for url: URL) -> DataTaskPublisher {
return ocombine.dataTaskPublisher(for: url)
}
/// Returns a publisher that wraps a URL session data task for a given URL request.
///
/// The publisher publishes data when the task completes, or terminates if the task
/// fails with an error.
///
/// - Parameter request: The URL request for which to create a data task.
/// - Returns: A publisher that wraps a data task for the URL request.
public func dataTaskPublisher(for request: URLRequest) -> DataTaskPublisher {
return ocombine.dataTaskPublisher(for: request)
}
#endif
}
extension URLSession.OCombine.DataTaskPublisher {
private class Inner<Downstream: Subscriber>
: Subscription,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Downstream.Input == (data: Data, response: URLResponse),
Downstream.Failure == URLError
{
private let lock = UnfairLock.allocate()
private var parent: URLSession.OCombine.DataTaskPublisher?
private var downstream: Downstream?
private var demand = Subscribers.Demand.none
private var task: URLSessionDataTask?
fileprivate init(parent: URLSession.OCombine.DataTaskPublisher,
downstream: Downstream) {
self.parent = parent
self.downstream = downstream
}
deinit {
lock.deallocate()
}
func request(_ demand: Subscribers.Demand) {
demand.assertNonZero()
lock.lock()
guard let parent = self.parent else {
lock.unlock()
return
}
if self.task == nil {
task = parent.session.dataTask(with: parent.request,
completionHandler: handleResponse)
}
self.demand += demand
let task = self.task
lock.unlock()
task?.resume()
}
private func handleResponse(data: Data?, response: URLResponse?, error: Error?) {
lock.lock()
guard demand > 0, parent != nil, let downstream = self.downstream else {
lock.unlock()
return
}
lockedTerminate()
lock.unlock()
switch (data, response, error) {
case let (data, response?, nil):
_ = downstream.receive((data ?? Data(), response))
downstream.receive(completion: .finished)
case let (_, _, error as URLError):
downstream.receive(completion: .failure(error))
default:
downstream.receive(completion: .failure(URLError(.unknown)))
}
}
func cancel() {
lock.lock()
guard parent != nil else {
lock.unlock()
return
}
let task = self.task
lockedTerminate()
lock.unlock()
task?.cancel()
}
private func lockedTerminate() {
parent = nil
downstream = nil
demand = .none
task = nil
}
var description: String { return "DataTaskPublisher" }
var customMirror: Mirror {
lock.lock()
defer { lock.unlock() }
let children: [Mirror.Child] = [
("task", task as Any),
("downstream", downstream as Any),
("parent", parent as Any),
("demand", demand)
]
return Mirror(self, children: children)
}
var playgroundDescription: Any { return description }
}
}
@@ -0,0 +1,724 @@
//
// URLSessionTests.swift
//
//
// Created by Sergej Jaskiewicz on 13.12.2019.
//
// swiftlint:disable multiline_arguments
import Foundation
import XCTest
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
// We can't test it on non-Darwin platforms because swift-corelibs-foundation
// doesn't allow us to override some URLSession methods that we need.
//
// As soon as https://github.com/apple/swift-corelibs-foundation/pull/2587 makes it
// into a release, we can enable these tests on non-Darwin platforms.
//
// The publisher itself though should work alright on those platforms.
#if canImport(Darwin)
#if OPENCOMBINE_COMPATIBILITY_TEST
import Combine
#else
import OpenCombine
import OpenCombineFoundation
#endif
@available(macOS 10.15, iOS 13.0, *)
final class URLSessionTests: XCTestCase {
private typealias TrackingSubscriber =
TrackingSubscriberBase<(data: Data, response: URLResponse), URLError>
private let testURL = URL(string: "https://github.com")!
private let testRequest = URLRequest(url: URL(string: "https://github.com")!,
cachePolicy: .reloadIgnoringCacheData,
timeoutInterval: 42)
private let testData = Data("test data".utf8)
private let testResponse = URLResponse(url: URL(string: "https://example.com")!,
mimeType: "text/markdown",
expectedContentLength: 300,
textEncodingName: "utf-8")
private let testError = URLError(.cannotParseResponse, userInfo: ["a" : 1])
private let unknownError = URLError(.unknown)
func testDataTaskPublisherFromURL() {
let publisher = makePublisher(TestURLSession(testDataTask: .init()), testURL)
let expectedRequest = URLRequest(url: testURL)
XCTAssertEqual(publisher.request, expectedRequest)
}
func testDataTaskPublisherFromRequest() {
let publisher = makePublisher(TestURLSession(testDataTask: .init()), testRequest)
XCTAssertEqual(publisher.request, testRequest)
}
func testReceiveNothing() {
testReceiveResult(nil, nil, nil,
expected: [.subscription("DataTaskPublisher"),
.completion(.failure(unknownError))])
}
func testReceiveOnlyData() {
testReceiveResult(testData, nil, nil,
expected: [.subscription("DataTaskPublisher"),
.completion(.failure(unknownError))])
}
func testReceiveDataAndResponse() {
testReceiveResult(testData, testResponse, nil,
expected: [.subscription("DataTaskPublisher"),
.value((testData, testResponse)),
.completion(.finished)])
}
func testReceiveDataAndURLError() {
testReceiveResult(testData, nil, testError,
expected: [.subscription("DataTaskPublisher"),
.completion(.failure(testError))])
}
func testReceiveDataAndUnrelatedError() {
testReceiveResult(testData, nil, TestingError.oops,
expected: [.subscription("DataTaskPublisher"),
.completion(.failure(unknownError))])
}
func testReceiveOnlyResponse() {
testReceiveResult(nil, testResponse, nil,
expected: [.subscription("DataTaskPublisher"),
.value((Data(), testResponse)),
.completion(.finished)])
}
func testReceiveResponseAndURLError() {
testReceiveResult(nil, testResponse, testError,
expected: [.subscription("DataTaskPublisher"),
.completion(.failure(testError))])
}
func testReceiveResponseAndUnrelatedError() {
testReceiveResult(nil, testResponse, TestingError.oops,
expected: [.subscription("DataTaskPublisher"),
.completion(.failure(unknownError))])
}
func testReceiveOnlyURLError() {
testReceiveResult(nil, nil, testError,
expected: [.subscription("DataTaskPublisher"),
.completion(.failure(testError))])
}
func testReceiveOnlyUnrelatedError() {
testReceiveResult(nil, nil, TestingError.oops,
expected: [.subscription("DataTaskPublisher"),
.completion(.failure(unknownError))])
}
func testRequesting() throws {
let dataTask = TestURLSessionDataTask()
let session = TestURLSession(testDataTask: dataTask)
let publisher = makePublisher(session, testRequest)
var downstreamSubscription: Subscription?
let tracking = TrackingSubscriber(
receiveSubscription: { downstreamSubscription = $0 }
)
publisher.subscribe(tracking)
tracking.assertHistoryEqual([.subscription("DataTaskPublisher")],
valueComparator: ==)
XCTAssertEqual(dataTask.history, [])
XCTAssertEqual(session.history, [])
session.completeDataTasks(testData, testResponse, nil)
try XCTUnwrap(downstreamSubscription).request(.max(2))
try XCTUnwrap(downstreamSubscription).request(.max(1))
tracking.assertHistoryEqual([.subscription("DataTaskPublisher")],
valueComparator: ==)
XCTAssertEqual(dataTask.history, [.resume, .resume])
XCTAssertEqual(session.history, [.dataTaskWithRequestAndCompletion(testRequest)])
try XCTUnwrap(downstreamSubscription).cancel()
tracking.assertHistoryEqual([.subscription("DataTaskPublisher")],
valueComparator: ==)
XCTAssertEqual(dataTask.history, [.resume, .resume, .cancel])
XCTAssertEqual(session.history, [.dataTaskWithRequestAndCompletion(testRequest)])
session.completeDataTasks(testData, testResponse, nil)
tracking.assertHistoryEqual([.subscription("DataTaskPublisher")],
valueComparator: ==)
XCTAssertEqual(dataTask.history, [.resume, .resume, .cancel])
XCTAssertEqual(session.history, [.dataTaskWithRequestAndCompletion(testRequest)])
}
func testCancelAlreadyCancelled() throws {
let dataTask = TestURLSessionDataTask()
let session = TestURLSession(testDataTask: dataTask)
let publisher = makePublisher(session, testRequest)
var downstreamSubscription: Subscription?
let tracking = TrackingSubscriber(
receiveSubscription: { downstreamSubscription = $0 }
)
publisher.subscribe(tracking)
tracking.assertHistoryEqual([.subscription("DataTaskPublisher")],
valueComparator: ==)
XCTAssertEqual(dataTask.history, [])
XCTAssertEqual(session.history, [])
try XCTUnwrap(downstreamSubscription).request(.max(1))
try XCTUnwrap(downstreamSubscription).cancel()
try XCTUnwrap(downstreamSubscription).request(.max(1))
try XCTUnwrap(downstreamSubscription).cancel()
tracking.assertHistoryEqual([.subscription("DataTaskPublisher")],
valueComparator: ==)
XCTAssertEqual(dataTask.history, [.resume, .cancel])
XCTAssertEqual(session.history, [.dataTaskWithRequestAndCompletion(testRequest)])
}
func testCrashesOnZeroDemand() throws {
let dataTask = TestURLSessionDataTask()
let session = TestURLSession(testDataTask: dataTask)
let publisher = makePublisher(session, testURL)
var downstreamSubscription: Subscription?
let tracking = TrackingSubscriber(
receiveSubscription: { downstreamSubscription = $0 }
)
publisher.subscribe(tracking)
try assertCrashes {
try XCTUnwrap(downstreamSubscription).request(.none)
}
}
func testURLSessionSubscriptionReflection() throws {
let dataTask = TestURLSessionDataTask()
let session = TestURLSession(testDataTask: dataTask)
let publisher = makePublisher(session, testURL)
try testSubscriptionReflection(
description: "DataTaskPublisher",
customMirror: expectedChildren(
("task", "nil"),
("downstream", .contains("TrackingSubscriberBase")),
("parent", .matches(String(describing: Optional(publisher)))),
("demand", "max(0)")
),
playgroundDescription: "DataTaskPublisher",
sut: publisher
)
}
// MARK: - Generic tests
private func testReceiveResult(_ data: Data?,
_ response: URLResponse?,
_ error: Error?,
expected: [TrackingSubscriber.Event]) {
let dataTask = TestURLSessionDataTask()
let session = TestURLSession(testDataTask: dataTask)
let publisher = makePublisher(session, testRequest)
let tracking = TrackingSubscriber(receiveSubscription: { $0.request(.max(1)) })
publisher.subscribe(tracking)
tracking.assertHistoryEqual([.subscription("DataTaskPublisher")],
valueComparator: ==)
XCTAssertEqual(dataTask.history, [.resume])
XCTAssertEqual(session.history, [.dataTaskWithRequestAndCompletion(testRequest)])
session.completeDataTasks(data, response, error)
session.completeDataTasks(data, response, error)
session.completeDataTasks(data, response, error)
tracking.assertHistoryEqual(expected, valueComparator: ==)
XCTAssertEqual(dataTask.history, [.resume])
XCTAssertEqual(session.history, [.dataTaskWithRequestAndCompletion(testRequest)])
}
}
/// A simple mock URLSession that records its history and allows executing
/// callbacks synchronously
private class TestURLSession: URLSession {
enum Event: Equatable {
case delegateQueue
case delegate
case configuration
case getSessionDescription
case setSessionDescription(String?)
case finishTasksAndInvalidate
case invalidateAndCancel
case reset
case flush
case getTasksWithCompletionHandler
case getAllTasks
case dataTaskWithRequest(URLRequest)
case dataTaskWithRequestAndCompletion(URLRequest)
case dataTaskWithURL(URL)
case dataTaskWithURLAndCompletion(URL)
case uploadTaskWithRequestFromFile(URLRequest, URL)
case uploadTaskWithRequestFromFileWithCompletion(URLRequest, URL)
case uploadTaskWithRequestFromData(URLRequest, Data)
case uploadTaskWithRequestFromDataWithCompletion(URLRequest, Data?)
case uploadTaskWithStreamedRequest(URLRequest)
case downloadTaskWithRequest(URLRequest)
case downloadTaskWithRequestAndCompletion(URLRequest)
case downloadTaskWithURL(URL)
case downloadTaskWithURLAndCompletion(URL)
case downloadTaskWithResumeData(Data)
case downloadTaskWithResumeDataAndCompletion(Data)
case streamTaskWithHostNameAndPort(String, Int)
#if canImport(Darwin) && swift(>=5.1)
case streamTaskWithService(NetService)
case webSocketTaskWithURL(URL)
case webSocketTaskWithURLAndProtocols(URL, [String])
case webSocketTaskWithRequest(URLRequest)
#endif // canImport(Darwin) && swift(>=5.1)
}
private(set) var history = [Event]()
private(set) var dataTaskCompletionHandlers: [(Data?, URLResponse?, Error?) -> Void]
private let testDataTask: TestURLSessionDataTask
init(testDataTask: TestURLSessionDataTask) {
self.testDataTask = testDataTask
self.dataTaskCompletionHandlers = []
}
// MARK: Testing
func completeDataTasks(_ data: Data?, _ response: URLResponse?, _ error: Error?) {
for completionHandler in dataTaskCompletionHandlers {
completionHandler(data, response, error)
}
}
// MARK: Overrides
override class var shared: URLSession { fatalError("shared session is unavailable") }
override var delegateQueue: OperationQueue {
history.append(.delegateQueue)
return super.delegateQueue
}
override var delegate: URLSessionDelegate? {
history.append(.delegate)
return super.delegate
}
override var configuration: URLSessionConfiguration {
history.append(.configuration)
return super.configuration
}
override var sessionDescription: String? {
get {
history.append(.getSessionDescription)
return super.sessionDescription
}
set {
history.append(.setSessionDescription(newValue))
super.sessionDescription = newValue
}
}
override func finishTasksAndInvalidate() {
history.append(.finishTasksAndInvalidate)
super.finishTasksAndInvalidate()
}
override func invalidateAndCancel() {
history.append(.invalidateAndCancel)
super.invalidateAndCancel()
}
override func reset(completionHandler: @escaping () -> Void) {
history.append(.reset)
super.reset(completionHandler: completionHandler)
}
override func flush(completionHandler: @escaping () -> Void) {
history.append(.flush)
super.flush(completionHandler: completionHandler)
}
override func getTasksWithCompletionHandler(
_ completionHandler: @escaping ([URLSessionDataTask],
[URLSessionUploadTask],
[URLSessionDownloadTask]) -> Void
) {
history.append(.getTasksWithCompletionHandler)
super.getTasksWithCompletionHandler(completionHandler)
}
@available(macOS 10.11, iOS 9.0, *)
override func getAllTasks(completionHandler: @escaping ([URLSessionTask]) -> Void) {
history.append(.getAllTasks)
super.getAllTasks(completionHandler: completionHandler)
}
override func dataTask(with request: URLRequest) -> URLSessionDataTask {
history.append(.dataTaskWithRequest(request))
return testDataTask
}
override func dataTask(with url: URL) -> URLSessionDataTask {
history.append(.dataTaskWithURL(url))
return testDataTask
}
override func dataTask(
with url: URL,
completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void
) -> URLSessionDataTask {
history.append(.dataTaskWithURLAndCompletion(url))
dataTaskCompletionHandlers.append(completionHandler)
return testDataTask
}
override func dataTask(
with request: URLRequest,
completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void
) -> URLSessionDataTask {
history.append(.dataTaskWithRequestAndCompletion(request))
dataTaskCompletionHandlers.append(completionHandler)
return testDataTask
}
override func uploadTask(with request: URLRequest,
fromFile fileURL: URL) -> URLSessionUploadTask {
history.append(.uploadTaskWithRequestFromFile(request, fileURL))
return super.uploadTask(with: request, fromFile: fileURL)
}
override func uploadTask(with request: URLRequest,
from bodyData: Data) -> URLSessionUploadTask {
history.append(.uploadTaskWithRequestFromData(request, bodyData))
return super.uploadTask(with: request, from: bodyData)
}
override func uploadTask(
withStreamedRequest request: URLRequest
) -> URLSessionUploadTask {
history.append(.uploadTaskWithStreamedRequest(request))
return super.uploadTask(withStreamedRequest: request)
}
override func uploadTask(
with request: URLRequest,
fromFile fileURL: URL,
completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void
) -> URLSessionUploadTask {
history.append(.uploadTaskWithRequestFromFileWithCompletion(request, fileURL))
return super.uploadTask(with: request,
fromFile: fileURL,
completionHandler: completionHandler)
}
override func uploadTask(
with request: URLRequest,
from bodyData: Data?,
completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void
) -> URLSessionUploadTask {
history.append(.uploadTaskWithRequestFromDataWithCompletion(request, bodyData))
return super.uploadTask(with: request,
from: bodyData,
completionHandler: completionHandler)
}
override func downloadTask(with request: URLRequest) -> URLSessionDownloadTask {
history.append(.downloadTaskWithRequest(request))
return super.downloadTask(with: request)
}
override func downloadTask(with url: URL) -> URLSessionDownloadTask {
history.append(.downloadTaskWithURL(url))
return super.downloadTask(with: url)
}
override func downloadTask(
with request: URLRequest,
completionHandler: @escaping (URL?, URLResponse?, Error?) -> Void
) -> URLSessionDownloadTask {
history.append(.downloadTaskWithRequestAndCompletion(request))
return super.downloadTask(with: request, completionHandler: completionHandler)
}
override func downloadTask(
with url: URL,
completionHandler: @escaping (URL?, URLResponse?, Error?) -> Void
) -> URLSessionDownloadTask {
history.append(.downloadTaskWithURLAndCompletion(url))
return super.downloadTask(with: url, completionHandler: completionHandler)
}
override func downloadTask(
withResumeData resumeData: Data,
completionHandler: @escaping (URL?, URLResponse?, Error?) -> Void
) -> URLSessionDownloadTask {
history.append(.downloadTaskWithResumeDataAndCompletion(resumeData))
return super.downloadTask(withResumeData: resumeData,
completionHandler: completionHandler)
}
override func downloadTask(
withResumeData resumeData: Data
) -> URLSessionDownloadTask {
history.append(.downloadTaskWithResumeData(resumeData))
return super.downloadTask(withResumeData: resumeData)
}
@available(macOS 10.11, iOS 9.0, *)
override func streamTask(withHostName hostname: String,
port: Int) -> URLSessionStreamTask {
history.append(.streamTaskWithHostNameAndPort(hostname, port))
return super.streamTask(withHostName: hostname, port: port)
}
#if canImport(Darwin) && swift(>=5.1)
@available(macOS 10.11, iOS 9.0, *)
override func streamTask(with service: NetService) -> URLSessionStreamTask {
history.append(.streamTaskWithService(service))
return super.streamTask(with: service)
}
@available(macOS 10.15, iOS 13.0, *)
override func webSocketTask(with url: URL) -> URLSessionWebSocketTask {
history.append(.webSocketTaskWithURL(url))
return super.webSocketTask(with: url)
}
@available(macOS 10.15, iOS 13.0, *)
override func webSocketTask(with url: URL,
protocols: [String]) -> URLSessionWebSocketTask {
history.append(.webSocketTaskWithURLAndProtocols(url, protocols))
return super.webSocketTask(with: url, protocols: protocols)
}
@available(macOS 10.15, iOS 13.0, *)
override func webSocketTask(with request: URLRequest) -> URLSessionWebSocketTask {
history.append(.webSocketTaskWithRequest(request))
return super.webSocketTask(with: request)
}
#endif // canImport(Darwin) && swift(>=5.1)
}
private final class TestURLSessionDataTask: URLSessionDataTask {
enum Event: Equatable {
case taskIdentifier
case originalRequest
case currentRequest
case response
case progress
case getEarliestBeginDate
case setEarliestBeginDate(Date?)
case getCountOfBytesClientExpectsToSend
case setCountOfBytesClientExpectsToSend(Int64)
case getCountOfBytesClientExpectsToReceive
case setCountOfBytesClientExpectsToReceive(Int64)
case countOfBytesReceived
case countOfBytesSent
case countOfBytesExpectedToSend
case countOfBytesExpectedToReceive
case getTaskDescription
case setTaskDescription(String?)
case cancel
case state
case error
case suspend
case resume
case getPriority
case setPriority(Float)
}
private(set) var history = [Event]()
override init() {}
override var taskIdentifier: Int {
history.append(.taskIdentifier)
return super.taskIdentifier
}
override var originalRequest: URLRequest? {
history.append(.originalRequest)
return super.originalRequest
}
override var currentRequest: URLRequest? {
history.append(.currentRequest)
return super.currentRequest
}
override var response: URLResponse? {
history.append(.response)
return super.response
}
@available(macOS 10.13, iOS 11.0, *)
override var progress: Progress {
history.append(.progress)
return super.progress
}
@available(macOS 10.13, iOS 11.0, *)
override var earliestBeginDate: Date? {
get {
history.append(.getEarliestBeginDate)
#if canImport(Darwin)
return super.earliestBeginDate
#else
return nil // Deprecated in swift-corerlibs-foundation
#endif
}
set {
history.append(.setEarliestBeginDate(newValue))
#if canImport(Darwin)
super.earliestBeginDate = newValue
#endif
}
}
@available(macOS 10.13, iOS 11.0, *)
override var countOfBytesClientExpectsToSend: Int64 {
get {
history.append(.getCountOfBytesClientExpectsToSend)
return super.countOfBytesClientExpectsToSend
}
set {
history.append(.setCountOfBytesClientExpectsToSend(newValue))
super.countOfBytesClientExpectsToSend = newValue
}
}
@available(macOS 10.13, iOS 11.0, *)
override var countOfBytesClientExpectsToReceive: Int64 {
get {
history.append(.getCountOfBytesClientExpectsToReceive)
return super.countOfBytesClientExpectsToReceive
}
set {
history.append(.setCountOfBytesClientExpectsToReceive(newValue))
super.countOfBytesClientExpectsToReceive = newValue
}
}
override var countOfBytesReceived: Int64 {
history.append(.countOfBytesReceived)
return super.countOfBytesReceived
}
override var countOfBytesSent: Int64 {
history.append(.countOfBytesSent)
return super.countOfBytesSent
}
override var countOfBytesExpectedToSend: Int64 {
history.append(.countOfBytesExpectedToSend)
return super.countOfBytesExpectedToSend
}
override var countOfBytesExpectedToReceive: Int64 {
history.append(.countOfBytesExpectedToReceive)
return super.countOfBytesExpectedToReceive
}
override var taskDescription: String? {
get {
history.append(.getTaskDescription)
return super.taskDescription
}
set {
history.append(.setTaskDescription(newValue))
super.taskDescription = newValue
}
}
override func cancel() {
history.append(.cancel)
}
override var state: URLSessionTask.State {
history.append(.state)
return super.state
}
override var error: Error? {
history.append(.error)
return super.error
}
override func suspend() {
history.append(.suspend)
}
override func resume() {
history.append(.resume)
}
override var priority: Float {
get {
history.append(.getPriority)
return super.priority
}
set {
history.append(.setPriority(newValue))
super.priority = newValue
}
}
}
extension URLError: EquatableError {}
#if OPENCOMBINE_COMPATIBILITY_TEST || !canImport(Combine)
@available(macOS 10.15, iOS 13.0, *)
private func makePublisher(
_ session: URLSession,
_ url: URL
) -> URLSession.DataTaskPublisher {
return session.dataTaskPublisher(for: url)
}
@available(macOS 10.15, iOS 13.0, *)
private func makePublisher(
_ session: URLSession,
_ request: URLRequest
) -> URLSession.DataTaskPublisher {
return session.dataTaskPublisher(for: request)
}
#else
private func makePublisher(
_ session: URLSession,
_ url: URL
) -> URLSession.OCombine.DataTaskPublisher {
return session.ocombine.dataTaskPublisher(for: url)
}
private func makePublisher(
_ session: URLSession,
_ request: URLRequest
) -> URLSession.OCombine.DataTaskPublisher {
return session.ocombine.dataTaskPublisher(for: request)
}
#endif
#endif // canImport(Darwin)
@@ -134,7 +134,7 @@ internal func testSubscriptionReflection<Sut: Publisher>(
customMirror customMirrorPredicate: ((Mirror) -> Bool)?,
playgroundDescription: String,
sut: Sut
) throws where Sut.Output: Equatable {
) throws {
let tracking = TrackingSubscriberBase<Sut.Output, Sut.Failure>()
sut.subscribe(tracking)
@@ -40,6 +40,20 @@ extension TestingError: ExpressibleByStringLiteral {
}
}
protocol EquatableError: Error {
func isEqual(_ other: EquatableError) -> Bool
}
extension EquatableError where Self: Equatable {
func isEqual(_ other: EquatableError) -> Bool {
return self == (other as? Self)
}
}
extension TestingError: EquatableError {}
extension NSError: EquatableError {}
func assertThrowsError<Result>(_ expression: @autoclosure () throws -> Result,
_ expected: TestingError,
_ message: @autoclosure () -> String = "",
@@ -5,6 +5,7 @@
// Created by Sergej Jaskiewicz on 11.06.2019.
//
import Foundation
import XCTest
#if OPENCOMBINE_COMPATIBILITY_TEST
@@ -224,8 +225,8 @@ extension TrackingSubscriberBase.Event {
switch (lhs, rhs) {
case (.finished, .finished):
return true
case let (.failure(lhs), .failure(rhs)):
return (lhs as? TestingError) == (rhs as? TestingError)
case let (.failure(lhs as EquatableError), .failure(rhs as EquatableError)):
return lhs.isEqual(rhs)
default:
return false
}
@@ -281,8 +282,9 @@ final class TrackingSubjectBase<Output: Equatable, Failure: Error>
switch (lhs, rhs) {
case (.finished, .finished):
return true
case let (.failure(lhs), .failure(rhs)):
return (lhs as? TestingError) == (rhs as? TestingError)
case let (.failure(lhs as EquatableError),
.failure(rhs as EquatableError)):
return lhs.isEqual(rhs)
default:
return false
}
@@ -210,10 +210,15 @@ final class MapErrorTests: XCTestCase {
}
}
private struct OtherError: Error {
let original: Error
private struct OtherError: EquatableError {
let original: EquatableError
init(_ original: Error) {
init(_ original: EquatableError) {
self.original = original
}
func isEqual(_ other: EquatableError) -> Bool {
guard let other = other as? OtherError else { return false }
return original.isEqual(other.original)
}
}