From b3c3f9fd66e59e46293991c79d63fc02a90b502e Mon Sep 17 00:00:00 2001 From: Lily Vulcano Date: Mon, 19 Aug 2019 13:29:47 -0700 Subject: [PATCH] Parity: Networking: URLUploadTask and URLDownloadTask MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implement downloadTask(withResumeData…). We do not currently support resume data, so act as Darwin does when invalid data is fed to these methods. - Implement uploadTask(withStreamedRequest:). - Some cleanup, some testing. --- Foundation/URLSession/NativeProtocol.swift | 62 +--- Foundation/URLSession/URLSession.swift | 30 +- Foundation/URLSession/URLSessionTask.swift | 47 ++- .../URLSession/ftp/FTPURLProtocol.swift | 6 +- .../URLSession/http/HTTPURLProtocol.swift | 18 +- TestFoundation/TestURLSession.swift | 272 ++++++++++++------ 6 files changed, 280 insertions(+), 155 deletions(-) diff --git a/Foundation/URLSession/NativeProtocol.swift b/Foundation/URLSession/NativeProtocol.swift index f1310145f7..faa71e4ba3 100644 --- a/Foundation/URLSession/NativeProtocol.swift +++ b/Foundation/URLSession/NativeProtocol.swift @@ -333,12 +333,9 @@ internal class _NativeProtocol: URLProtocol, _EasyHandleDelegate { } } - func createTransferState(url: URL, workQueue: DispatchQueue) -> _TransferState { + func createTransferState(url: URL, body: _Body, workQueue: DispatchQueue) -> _TransferState { let drain = createTransferBodyDataDrain() - guard let t = task else { - fatalError("Cannot create transfer state") - } - switch t.body { + switch body { case .none: return _TransferState(url: url, bodyDataDrain: drain) case .data(let data): @@ -358,22 +355,19 @@ internal class _NativeProtocol: URLProtocol, _EasyHandleDelegate { /// Start a new transfer func startNewTransfer(with request: URLRequest) { - guard let t = task else { - fatalError() - } - t.currentRequest = request + let task = self.task! + task.currentRequest = request guard let url = request.url else { fatalError("No URL in request.") } - self.internalState = .transferReady(createTransferState(url: url, workQueue: t.workQueue)) - if let authRequest = task?.authRequest { - configureEasyHandle(for: authRequest) - } else { - configureEasyHandle(for: request) - } - if (t.suspendCount) < 1 { - resume() + task.getBody { (body) in + self.internalState = .transferReady(self.createTransferState(url: url, body: body, workQueue: task.workQueue)) + let request = task.authRequest ?? request + self.configureEasyHandle(for: request, body: body) + if (task.suspendCount) < 1 { + self.resume() + } } } @@ -427,7 +421,7 @@ internal class _NativeProtocol: URLProtocol, _EasyHandleDelegate { } } - func configureEasyHandle(for: URLRequest) { + func configureEasyHandle(for request: URLRequest, body: _Body) { NSRequiresConcreteImplementation() } } @@ -624,37 +618,7 @@ extension _NativeProtocol._ResponseHeaderLines { } internal extension _NativeProtocol { - enum _Body { - case none - case data(DispatchData) - /// Body data is read from the given file URL - case file(URL) - case stream(InputStream) - } -} - -fileprivate extension _NativeProtocol._Body { - enum _Error : Error { - case fileForBodyDataNotFound - } - - /// - Returns: The body length, or `nil` for no body (e.g. `GET` request). - func getBodyLength() throws -> UInt64? { - switch self { - case .none: - return 0 - case .data(let d): - return UInt64(d.count) - /// Body data is read from the given file URL - case .file(let fileURL): - guard let s = try FileManager.default.attributesOfItem(atPath: fileURL.path)[.size] as? NSNumber else { - throw _Error.fileForBodyDataNotFound - } - return s.uint64Value - case .stream: - return nil - } - } + typealias _Body = URLSessionTask._Body } extension _NativeProtocol { diff --git a/Foundation/URLSession/URLSession.swift b/Foundation/URLSession/URLSession.swift index 0a782dd477..cc84ad1eed 100644 --- a/Foundation/URLSession/URLSession.swift +++ b/Foundation/URLSession/URLSession.swift @@ -433,7 +433,10 @@ open class URLSession : NSObject { } /* Creates an upload task with the given request. The previously set body stream of the request (if any) is ignored and the URLSession:task:needNewBodyStream: delegate will be called when the body payload is required. */ - open func uploadTask(withStreamedRequest request: URLRequest) -> URLSessionUploadTask { NSUnimplemented() } + open func uploadTask(withStreamedRequest request: URLRequest) -> URLSessionUploadTask { + let r = URLSession._Request(request) + return uploadTask(with: r, body: nil, behaviour: .callDelegate) + } /* Creates a download task with the given request. */ open func downloadTask(with request: URLRequest) -> URLSessionDownloadTask { @@ -447,7 +450,9 @@ open class URLSession : NSObject { } /* Creates a download task with the resume data. If the download cannot be successfully resumed, URLSession:task:didCompleteWithError: will be called. */ - open func downloadTask(withResumeData resumeData: Data) -> URLSessionDownloadTask { NSUnimplemented() } + open func downloadTask(withResumeData resumeData: Data) -> URLSessionDownloadTask { + return invalidDownloadTask(behavior: .callDelegate) + } /* Creates a bidirectional stream task to a given host and port. */ @@ -511,7 +516,7 @@ fileprivate extension URLSession { /// Create an upload task. /// /// All public methods funnel into this one. - func uploadTask(with request: _Request, body: URLSessionTask._Body, behaviour: _TaskRegistry._Behaviour) -> URLSessionUploadTask { + func uploadTask(with request: _Request, body: URLSessionTask._Body?, behaviour: _TaskRegistry._Behaviour) -> URLSessionUploadTask { guard !self.invalidated else { fatalError("Session invalidated") } let r = createConfiguredRequest(from: request) let i = createNextTaskIdentifier() @@ -533,6 +538,21 @@ fileprivate extension URLSession { } return task } + + /// Create a download task that is marked invalid. + func invalidDownloadTask(behavior: _TaskRegistry._Behaviour) -> URLSessionDownloadTask { + /* We do not support resume data in swift-corelibs-foundation, so whatever we are passed, we should just behave as Darwin does in the presence of invalid data. */ + + guard !self.invalidated else { fatalError("Session invalidated") } + let task = URLSessionDownloadTask() + task.createdFromInvalidResumeData = true + task.taskIdentifier = createNextTaskIdentifier() + task.session = self + workQueue.async { + self.taskRegistry.add(task, behaviour: behavior) + } + return task + } } @@ -588,7 +608,9 @@ extension URLSession { return downloadTask(with: _Request(url), behavior: .downloadCompletionHandler(completionHandler)) } - open func downloadTask(withResumeData resumeData: Data, completionHandler: @escaping (URL?, URLResponse?, Error?) -> Void) -> URLSessionDownloadTask { NSUnimplemented() } + open func downloadTask(withResumeData resumeData: Data, completionHandler: @escaping (URL?, URLResponse?, Error?) -> Void) -> URLSessionDownloadTask { + return invalidDownloadTask(behavior: .downloadCompletionHandler(completionHandler)) + } } internal extension URLSession { diff --git a/Foundation/URLSession/URLSessionTask.swift b/Foundation/URLSession/URLSessionTask.swift index 6b22203642..2f4629ef0f 100644 --- a/Foundation/URLSession/URLSessionTask.swift +++ b/Foundation/URLSession/URLSessionTask.swift @@ -54,7 +54,7 @@ open class URLSessionTask : NSObject, NSCopying { default: let toBeSent: Int64? - if let bodyLength = try? self.body.getBodyLength() { + if let bodyLength = try? self.knownBody?.getBodyLength() { toBeSent = Int64(clamping: bodyLength) } else if self.countOfBytesExpectedToSend > 0 { toBeSent = Int64(clamping: self.countOfBytesExpectedToSend) @@ -95,9 +95,10 @@ open class URLSessionTask : NSObject, NSCopying { /// How many times the task has been suspended, 0 indicating a running task. internal var suspendCount = 1 - internal var session: URLSessionProtocol! //change to nil when task completes - internal let body: _Body + internal var actualSession: URLSession? { return session as? URLSession } + internal var session: URLSessionProtocol! //change to nil when task completes + fileprivate enum ProtocolState { case toBeCreated case awaitingCacheReply(Bag<(URLProtocol?) -> Void>) @@ -193,6 +194,27 @@ open class URLSessionTask : NSObject, NSCopying { } } + + internal let knownBody: _Body? + func getBody(completion: @escaping (_Body) -> Void) { + if let body = knownBody { + completion(body) + return + } + + if let session = actualSession, let delegate = session.delegate as? URLSessionTaskDelegate { + delegate.urlSession(session, task: self) { (stream) in + if let stream = stream { + completion(.stream(stream)) + } else { + completion(.none) + } + } + } else { + completion(.none) + } + } + private let syncQ = DispatchQueue(label: "org.swift.URLSessionTask.SyncQ") private var hasTriggeredResume: Bool = false internal var isSuspendedAfterResume: Bool { @@ -212,7 +234,7 @@ open class URLSessionTask : NSObject, NSCopying { session = _MissingURLSession() taskIdentifier = 0 originalRequest = nil - body = .none + knownBody = URLSessionTask._Body.none workQueue = DispatchQueue(label: "URLSessionTask.notused.0") super.init() } @@ -226,13 +248,13 @@ open class URLSessionTask : NSObject, NSCopying { self.init(session: session, request: request, taskIdentifier: taskIdentifier, body: .none) } } - internal init(session: URLSession, request: URLRequest, taskIdentifier: Int, body: _Body) { + internal init(session: URLSession, request: URLRequest, taskIdentifier: Int, body: _Body?) { self.session = session /* make sure we're actually having a serial queue as it's used for synchronization */ self.workQueue = DispatchQueue.init(label: "org.swift.URLSessionTask.WorkQueue", target: session.workQueue) self.taskIdentifier = taskIdentifier self.originalRequest = request - self.body = body + self.knownBody = body super.init() self.currentRequest = request self.progress.cancellationHandler = { [weak self] in @@ -252,7 +274,7 @@ open class URLSessionTask : NSObject, NSCopying { } /// An identifier for this task, assigned by and unique to the owning session - open private(set) var taskIdentifier: Int + open internal(set) var taskIdentifier: Int /// May be nil if this is a stream task @@ -589,6 +611,17 @@ open class URLSessionUploadTask : URLSessionDataTask { */ open class URLSessionDownloadTask : URLSessionTask { + var createdFromInvalidResumeData = false + + // If a task is created from invalid resume data, prevent attempting creation of the protocol object. + override func _getProtocol(_ callback: @escaping (URLProtocol?) -> Void) { + if createdFromInvalidResumeData { + callback(nil) + } else { + super._getProtocol(callback) + } + } + internal var fileLength = -1.0 /* Cancel the download (and calls the superclass -cancel). If diff --git a/Foundation/URLSession/ftp/FTPURLProtocol.swift b/Foundation/URLSession/ftp/FTPURLProtocol.swift index ef3be7ce75..16874e8d5f 100644 --- a/Foundation/URLSession/ftp/FTPURLProtocol.swift +++ b/Foundation/URLSession/ftp/FTPURLProtocol.swift @@ -51,7 +51,7 @@ internal class _FTPURLProtocol: _NativeProtocol { } } - override func configureEasyHandle(for request: URLRequest) { + override func configureEasyHandle(for request: URLRequest, body: _Body) { easyHandle.set(verboseModeOn: enableLibcurlDebugOutput) easyHandle.set(debugOutputOn: enableLibcurlDebugOutput, task: task!) easyHandle.set(skipAllSignalHandling: true) @@ -59,8 +59,8 @@ internal class _FTPURLProtocol: _NativeProtocol { easyHandle.set(url: url) easyHandle.set(preferredReceiveBufferSize: Int.max) do { - switch (task?.body, try task?.body.getBodyLength()) { - case (.some(URLSessionTask._Body.none), _): + switch (body, try body.getBodyLength()) { + case (.none, _): set(requestBodyLength: .noBody) case (_, .some(let length)): set(requestBodyLength: .length(length)) diff --git a/Foundation/URLSession/http/HTTPURLProtocol.swift b/Foundation/URLSession/http/HTTPURLProtocol.swift index 26b5993b2e..377ee065fc 100644 --- a/Foundation/URLSession/http/HTTPURLProtocol.swift +++ b/Foundation/URLSession/http/HTTPURLProtocol.swift @@ -260,7 +260,7 @@ internal class _HTTPURLProtocol: _NativeProtocol { /// Set options on the easy handle to match the given request. /// /// This performs a series of `curl_easy_setopt()` calls. - override func configureEasyHandle(for request: URLRequest) { + override func configureEasyHandle(for request: URLRequest, body: _Body) { // At this point we will call the equivalent of curl_easy_setopt() // to configure everything on the handle. Since we might be re-using // a handle, we must be sure to set everything and not rely on default @@ -294,8 +294,8 @@ internal class _HTTPURLProtocol: _NativeProtocol { easyHandle.setAllowedProtocolsToHTTPAndHTTPS() easyHandle.set(preferredReceiveBufferSize: Int.max) do { - switch (task?.body, try task?.body.getBodyLength()) { - case (nil, _): + switch (body, try body.getBodyLength()) { + case (.none, _): set(requestBodyLength: .noBody) case (_, let length?): set(requestBodyLength: .length(length)) @@ -509,11 +509,15 @@ fileprivate extension _HTTPURLProtocol { /// Any header values that should be removed from the ones set by libcurl /// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_HTTPHEADER.html var curlHeadersToRemove: [String] { - if task?.body == nil { - return [] - } else { - return ["Expect"] + if let task = task { + if task.knownBody == nil { + return [] + } else if case .some(.none) = task.knownBody { + return [] + } } + + return ["Expect"] } } diff --git a/TestFoundation/TestURLSession.swift b/TestFoundation/TestURLSession.swift index 4950d10e19..9e9c6ab243 100644 --- a/TestFoundation/TestURLSession.swift +++ b/TestFoundation/TestURLSession.swift @@ -9,59 +9,6 @@ class TestURLSession : LoopbackServerTest { - static var allTests: [(String, (TestURLSession) -> () throws -> Void)] { - return [ - ("test_dataTaskWithURL", test_dataTaskWithURL), - ("test_dataTaskWithURLRequest", test_dataTaskWithURLRequest), - ("test_dataTaskWithURLCompletionHandler", test_dataTaskWithURLCompletionHandler), - ("test_dataTaskWithURLRequestCompletionHandler", test_dataTaskWithURLRequestCompletionHandler), - // ("test_dataTaskWithHttpInputStream", test_dataTaskWithHttpInputStream), - Flaky test - ("test_gzippedDataTask", test_gzippedDataTask), - ("test_downloadTaskWithURL", test_downloadTaskWithURL), - ("test_downloadTaskWithURLRequest", test_downloadTaskWithURLRequest), - ("test_downloadTaskWithRequestAndHandler", test_downloadTaskWithRequestAndHandler), - ("test_downloadTaskWithURLAndHandler", test_downloadTaskWithURLAndHandler), - ("test_gzippedDownloadTask", test_gzippedDownloadTask), - ("test_finishTaskAndInvalidate", test_finishTasksAndInvalidate), - ("test_taskError", test_taskError), - ("test_taskCopy", test_taskCopy), - ("test_cancelTask", test_cancelTask), - ("test_taskTimeout", test_taskTimeout), - ("test_verifyRequestHeaders", test_verifyRequestHeaders), - ("test_verifyHttpAdditionalHeaders", test_verifyHttpAdditionalHeaders), - ("test_timeoutInterval", test_timeoutInterval), - ("test_httpRedirectionWithCompleteRelativePath", test_httpRedirectionWithCompleteRelativePath), - ("test_httpRedirectionWithInCompleteRelativePath", test_httpRedirectionWithInCompleteRelativePath), - ("test_httpRedirectionWithDefaultPort", test_httpRedirectionWithDefaultPort), - ("test_httpRedirectionTimeout", test_httpRedirectionTimeout), - ("test_http0_9SimpleResponses", test_http0_9SimpleResponses), - ("test_outOfRangeButCorrectlyFormattedHTTPCode", test_outOfRangeButCorrectlyFormattedHTTPCode), - ("test_missingContentLengthButStillABody", test_missingContentLengthButStillABody), - ("test_illegalHTTPServerResponses", test_illegalHTTPServerResponses), - ("test_dataTaskWithSharedDelegate", test_dataTaskWithSharedDelegate), - // ("test_simpleUploadWithDelegate", test_simpleUploadWithDelegate), - Server needs modification - ("test_concurrentRequests", test_concurrentRequests), - ("test_disableCookiesStorage", test_disableCookiesStorage), - ("test_cookiesStorage", test_cookiesStorage), - ("test_cookieStorageForEphmeralConfiguration", test_cookieStorageForEphmeralConfiguration), - ("test_setCookies", test_setCookies), - ("test_dontSetCookies", test_dontSetCookies), - ("test_initURLSessionConfiguration", test_initURLSessionConfiguration), - ("test_basicAuthRequest", test_basicAuthRequest), - ("test_redirectionWithSetCookies", test_redirectionWithSetCookies), - ("test_postWithEmptyBody", test_postWithEmptyBody), - ("test_basicAuthWithUnauthorizedHeader", test_basicAuthWithUnauthorizedHeader), - ("test_checkErrorTypeAfterInvalidateAndCancel", test_checkErrorTypeAfterInvalidateAndCancel), - ("test_taskCountAfterInvalidateAndCancel", test_taskCountAfterInvalidateAndCancel), - ("test_sessionDelegateAfterInvalidateAndCancel", test_sessionDelegateAfterInvalidateAndCancel), - ("test_getAllTasks", test_getAllTasks), - ("test_getTasksWithCompletion", test_getTasksWithCompletion), - /* ⚠️ */ ("test_noDoubleCallbackWhenCancellingAndProtocolFailsFast", - /* ⚠️ */ testExpectedToFail(test_noDoubleCallbackWhenCancellingAndProtocolFailsFast, "This test crashes nondeterministically: https://bugs.swift.org/browse/SR-11310")), - ("test_cancelledTasksCannotBeResumed", test_cancelledTasksCannotBeResumed), - ] - } - func test_dataTaskWithURL() { let urlString = "http://127.0.0.1:\(TestURLSession.serverPort)/Nepal" let url = URL(string: urlString)! @@ -200,7 +147,7 @@ class TestURLSession : LoopbackServerTest { func test_downloadTaskWithURL() { let urlString = "http://127.0.0.1:\(TestURLSession.serverPort)/country.txt" let url = URL(string: urlString)! - let d = DownloadTask(with: expectation(description: "Download GET \(urlString): with a delegate")) + let d = DownloadTask(testCase: self, description: "Download GET \(urlString): with a delegate") d.run(with: url) waitForExpectations(timeout: 12) } @@ -208,7 +155,7 @@ class TestURLSession : LoopbackServerTest { func test_downloadTaskWithURLRequest() { let urlString = "http://127.0.0.1:\(TestURLSession.serverPort)/country.txt" let urlRequest = URLRequest(url: URL(string: urlString)!) - let d = DownloadTask(with: expectation(description: "Download GET \(urlString): with a delegate")) + let d = DownloadTask(testCase: self, description: "Download GET \(urlString): with a delegate") d.run(with: urlRequest) waitForExpectations(timeout: 12) } @@ -256,7 +203,7 @@ class TestURLSession : LoopbackServerTest { func test_gzippedDownloadTask() { let urlString = "http://127.0.0.1:\(TestURLSession.serverPort)/gzipped-response" let url = URL(string: urlString)! - let d = DownloadTask(with: expectation(description: "GET \(urlString): gzipped response")) + let d = DownloadTask(testCase: self, description: "GET \(urlString): gzipped response") d.run(with: url) waitForExpectations(timeout: 12) if d.totalBytesWritten != "Hello World!".utf8.count { @@ -1027,6 +974,104 @@ class TestURLSession : LoopbackServerTest { waitForExpectations(timeout: 1) } + func test_invalidResumeDataForDownloadTask() { + let done = expectation(description: "Invalid resume data for download task (with completion block)") + URLSession.shared.downloadTask(withResumeData: Data()) { (url, response, error) in + XCTAssertNil(url) + XCTAssertNil(response) + XCTAssert(error is URLError) + XCTAssertEqual((error as? URLError)?.errorCode, URLError.unsupportedURL.rawValue) + + done.fulfill() + }.resume() + waitForExpectations(timeout: 20) + + let d = DownloadTask(testCase: self, description: "Invalid resume data for download task") + d.run { (session) -> DownloadTask.Configuration in + return DownloadTask.Configuration(task: session.downloadTask(withResumeData: Data()), + errorExpectation: + { (error) in + XCTAssert(error is URLError) + XCTAssertEqual((error as? URLError)?.errorCode, URLError.unsupportedURL.rawValue) + }) + } + waitForExpectations(timeout: 20) + } + + func test_simpleUploadWithDelegateProvidingInputStream() throws { + let delegate = HTTPUploadDelegate() + let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil) + let urlString = "http://127.0.0.1:\(TestURLSession.serverPort)/upload" + var request = URLRequest(url: URL(string: urlString)!) + request.httpMethod = "PUT" + + delegate.uploadCompletedExpectation = expectation(description: "PUT \(urlString): Upload data") + + + let fileData = Data(count: 16*1024) + let stream = InputStream(data: fileData) + stream.open() + delegate.streamToProvideOnRequest = stream + + let task = session.uploadTask(withStreamedRequest: request) + task.resume() + waitForExpectations(timeout: 20) + } + + static var allTests: [(String, (TestURLSession) -> () throws -> Void)] { + return [ + ("test_dataTaskWithURL", test_dataTaskWithURL), + ("test_dataTaskWithURLRequest", test_dataTaskWithURLRequest), + ("test_dataTaskWithURLCompletionHandler", test_dataTaskWithURLCompletionHandler), + ("test_dataTaskWithURLRequestCompletionHandler", test_dataTaskWithURLRequestCompletionHandler), + // ("test_dataTaskWithHttpInputStream", test_dataTaskWithHttpInputStream), - Flaky test + ("test_gzippedDataTask", test_gzippedDataTask), + ("test_downloadTaskWithURL", test_downloadTaskWithURL), + ("test_downloadTaskWithURLRequest", test_downloadTaskWithURLRequest), + ("test_downloadTaskWithRequestAndHandler", test_downloadTaskWithRequestAndHandler), + ("test_downloadTaskWithURLAndHandler", test_downloadTaskWithURLAndHandler), + ("test_gzippedDownloadTask", test_gzippedDownloadTask), + ("test_finishTaskAndInvalidate", test_finishTasksAndInvalidate), + ("test_taskError", test_taskError), + ("test_taskCopy", test_taskCopy), + ("test_cancelTask", test_cancelTask), + ("test_taskTimeout", test_taskTimeout), + ("test_verifyRequestHeaders", test_verifyRequestHeaders), + ("test_verifyHttpAdditionalHeaders", test_verifyHttpAdditionalHeaders), + ("test_timeoutInterval", test_timeoutInterval), + ("test_httpRedirectionWithCompleteRelativePath", test_httpRedirectionWithCompleteRelativePath), + ("test_httpRedirectionWithInCompleteRelativePath", test_httpRedirectionWithInCompleteRelativePath), + ("test_httpRedirectionWithDefaultPort", test_httpRedirectionWithDefaultPort), + ("test_httpRedirectionTimeout", test_httpRedirectionTimeout), + ("test_http0_9SimpleResponses", test_http0_9SimpleResponses), + ("test_outOfRangeButCorrectlyFormattedHTTPCode", test_outOfRangeButCorrectlyFormattedHTTPCode), + ("test_missingContentLengthButStillABody", test_missingContentLengthButStillABody), + ("test_illegalHTTPServerResponses", test_illegalHTTPServerResponses), + ("test_dataTaskWithSharedDelegate", test_dataTaskWithSharedDelegate), + // ("test_simpleUploadWithDelegate", test_simpleUploadWithDelegate), - Server needs modification + ("test_concurrentRequests", test_concurrentRequests), + ("test_disableCookiesStorage", test_disableCookiesStorage), + ("test_cookiesStorage", test_cookiesStorage), + ("test_cookieStorageForEphmeralConfiguration", test_cookieStorageForEphmeralConfiguration), + ("test_setCookies", test_setCookies), + ("test_dontSetCookies", test_dontSetCookies), + ("test_initURLSessionConfiguration", test_initURLSessionConfiguration), + ("test_basicAuthRequest", test_basicAuthRequest), + ("test_redirectionWithSetCookies", test_redirectionWithSetCookies), + ("test_postWithEmptyBody", test_postWithEmptyBody), + ("test_basicAuthWithUnauthorizedHeader", test_basicAuthWithUnauthorizedHeader), + ("test_checkErrorTypeAfterInvalidateAndCancel", test_checkErrorTypeAfterInvalidateAndCancel), + ("test_taskCountAfterInvalidateAndCancel", test_taskCountAfterInvalidateAndCancel), + ("test_sessionDelegateAfterInvalidateAndCancel", test_sessionDelegateAfterInvalidateAndCancel), + ("test_getAllTasks", test_getAllTasks), + ("test_getTasksWithCompletion", test_getTasksWithCompletion), + ("test_invalidResumeDataForDownloadTask", test_invalidResumeDataForDownloadTask), + ("test_simpleUploadWithDelegateProvidingInputStream", test_simpleUploadWithDelegateProvidingInputStream), + ("test_noDoubleCallbackWhenCancellingAndProtocolFailsFast", test_noDoubleCallbackWhenCancellingAndProtocolFailsFast), + ("test_cancelledTasksCannotBeResumed", test_cancelledTasksCannotBeResumed), + ] + } + } class SharedDelegate: NSObject { @@ -1189,15 +1234,28 @@ extension DataTask : URLSessionTaskDelegate { class DownloadTask : NSObject { var totalBytesWritten: Int64 = 0 - let dwdExpectation: XCTestExpectation! + var didDownloadExpectation: XCTestExpectation? + let didCompleteExpectation: XCTestExpectation var session: URLSession! = nil var task: URLSessionDownloadTask! = nil + var errorExpectation: ((Error) -> Void)? + weak var testCase: XCTestCase? + var expectationsDescription: String - init(with expectation: XCTestExpectation) { - dwdExpectation = expectation + init(testCase: XCTestCase, description: String) { + self.expectationsDescription = description + self.testCase = testCase + self.didCompleteExpectation = testCase.expectation(description: "Did complete \(description)") + } + + private func makeDownloadExpectation() { + guard didDownloadExpectation == nil else { return } + self.didDownloadExpectation = testCase!.expectation(description: "Did finish download: \(description)") + self.testCase = nil // No need for it any more here. } func run(with url: URL) { + makeDownloadExpectation() let config = URLSessionConfiguration.default config.timeoutIntervalForRequest = 8 session = URLSession(configuration: config, delegate: self, delegateQueue: nil) @@ -1206,12 +1264,32 @@ class DownloadTask : NSObject { } func run(with urlRequest: URLRequest) { + makeDownloadExpectation() let config = URLSessionConfiguration.default config.timeoutIntervalForRequest = 8 session = URLSession(configuration: config, delegate: self, delegateQueue: nil) task = session.downloadTask(with: urlRequest) task.resume() } + + struct Configuration { + var task: URLSessionDownloadTask + var errorExpectation: ((Error) -> Void)? + } + + func run(configuration: (URLSession) -> Configuration) { + let config = URLSessionConfiguration.default + config.timeoutIntervalForRequest = 8 + session = URLSession(configuration: config, delegate: self, delegateQueue: nil) + let taskConfiguration = configuration(session) + + task = taskConfiguration.task + errorExpectation = taskConfiguration.errorExpectation + if errorExpectation == nil { + makeDownloadExpectation() + } + task.resume() + } } extension DownloadTask : URLSessionDownloadDelegate { @@ -1222,24 +1300,61 @@ extension DownloadTask : URLSessionDownloadDelegate { } public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { + defer { didDownloadExpectation?.fulfill() } + + guard self.errorExpectation == nil else { + XCTFail("Expected an error, but got …didFinishDownloadingTo… from download task \(downloadTask) (at \(location))") + return + } + do { let attr = try FileManager.default.attributesOfItem(atPath: location.path) XCTAssertEqual((attr[.size]! as? NSNumber)!.int64Value, totalBytesWritten, "Size of downloaded file not equal to total bytes downloaded") } catch { XCTFail("Unable to calculate size of the downloaded file") } - dwdExpectation.fulfill() } } extension DownloadTask : URLSessionTaskDelegate { public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { - guard let e = error as? URLError else { return } - XCTAssertEqual(e.code, .timedOut, "Unexpected error code") - dwdExpectation.fulfill() + defer { didCompleteExpectation.fulfill() } + + if let errorExpectation = self.errorExpectation { + if let error = error { + errorExpectation(error) + } else { + XCTFail("Expected an error, but got a completion without error from download task \(task)") + } + } else { + guard let e = error as? URLError else { return } + XCTAssertEqual(e.code, .timedOut, "Unexpected error code") + } } } +class FailFastProtocol: URLProtocol { + enum Error: Swift.Error { + case fastError + } + + override class func canInit(with request: URLRequest) -> Bool { + return request.url?.scheme == "failfast" + } + + override class func canInit(with task: URLSessionTask) -> Bool { + guard let request = task.currentRequest else { return false } + return canInit(with: request) + } + + override func startLoading() { + client?.urlProtocol(self, didFailWithError: Error.fastError) + } + + override func stopLoading() { + // Intentionally blank + } +} class HTTPRedirectionDataTask : NSObject { let dataTaskExpectation: XCTestExpectation! var session: URLSession! = nil @@ -1305,6 +1420,7 @@ extension HTTPRedirectionDataTask : URLSessionTaskDelegate { class HTTPUploadDelegate: NSObject { var uploadCompletedExpectation: XCTestExpectation! + var streamToProvideOnRequest: InputStream? var totalBytesSent: Int64 = 0 } @@ -1312,6 +1428,14 @@ extension HTTPUploadDelegate: URLSessionTaskDelegate { func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { self.totalBytesSent = totalBytesSent } + + func urlSession(_ session: URLSession, task: URLSessionTask, needNewBodyStream completionHandler: @escaping (InputStream?) -> Void) { + if streamToProvideOnRequest == nil { + XCTFail("This shouldn't have been invoked -- no stream was set.") + } + + completionHandler(self.streamToProvideOnRequest) + } } extension HTTPUploadDelegate: URLSessionDataDelegate { @@ -1321,25 +1445,3 @@ extension HTTPUploadDelegate: URLSessionDataDelegate { } } -class FailFastProtocol: URLProtocol { - enum Error: Swift.Error { - case fastError - } - - override class func canInit(with request: URLRequest) -> Bool { - return request.url?.scheme == "failfast" - } - - override class func canInit(with task: URLSessionTask) -> Bool { - guard let request = task.currentRequest else { return false } - return canInit(with: request) - } - - override func startLoading() { - client?.urlProtocol(self, didFailWithError: Error.fastError) - } - - override func stopLoading() { - // Intentionally blank - } -}