diff --git a/Foundation/URLSession/NativeProtocol.swift b/Foundation/URLSession/NativeProtocol.swift index faa71e4ba3..f1310145f7 100644 --- a/Foundation/URLSession/NativeProtocol.swift +++ b/Foundation/URLSession/NativeProtocol.swift @@ -333,9 +333,12 @@ internal class _NativeProtocol: URLProtocol, _EasyHandleDelegate { } } - func createTransferState(url: URL, body: _Body, workQueue: DispatchQueue) -> _TransferState { + func createTransferState(url: URL, workQueue: DispatchQueue) -> _TransferState { let drain = createTransferBodyDataDrain() - switch body { + guard let t = task else { + fatalError("Cannot create transfer state") + } + switch t.body { case .none: return _TransferState(url: url, bodyDataDrain: drain) case .data(let data): @@ -355,19 +358,22 @@ internal class _NativeProtocol: URLProtocol, _EasyHandleDelegate { /// Start a new transfer func startNewTransfer(with request: URLRequest) { - let task = self.task! - task.currentRequest = request + guard let t = task else { + fatalError() + } + t.currentRequest = request guard let url = request.url else { fatalError("No URL in request.") } - 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() - } + 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() } } @@ -421,7 +427,7 @@ internal class _NativeProtocol: URLProtocol, _EasyHandleDelegate { } } - func configureEasyHandle(for request: URLRequest, body: _Body) { + func configureEasyHandle(for: URLRequest) { NSRequiresConcreteImplementation() } } @@ -618,7 +624,37 @@ extension _NativeProtocol._ResponseHeaderLines { } internal extension _NativeProtocol { - typealias _Body = URLSessionTask._Body + 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 + } + } } extension _NativeProtocol { diff --git a/Foundation/URLSession/URLSession.swift b/Foundation/URLSession/URLSession.swift index cc84ad1eed..0a782dd477 100644 --- a/Foundation/URLSession/URLSession.swift +++ b/Foundation/URLSession/URLSession.swift @@ -433,10 +433,7 @@ 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 { - let r = URLSession._Request(request) - return uploadTask(with: r, body: nil, behaviour: .callDelegate) - } + open func uploadTask(withStreamedRequest request: URLRequest) -> URLSessionUploadTask { NSUnimplemented() } /* Creates a download task with the given request. */ open func downloadTask(with request: URLRequest) -> URLSessionDownloadTask { @@ -450,9 +447,7 @@ 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 { - return invalidDownloadTask(behavior: .callDelegate) - } + open func downloadTask(withResumeData resumeData: Data) -> URLSessionDownloadTask { NSUnimplemented() } /* Creates a bidirectional stream task to a given host and port. */ @@ -516,7 +511,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() @@ -538,21 +533,6 @@ 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 - } } @@ -608,9 +588,7 @@ extension URLSession { return downloadTask(with: _Request(url), behavior: .downloadCompletionHandler(completionHandler)) } - open func downloadTask(withResumeData resumeData: Data, completionHandler: @escaping (URL?, URLResponse?, Error?) -> Void) -> URLSessionDownloadTask { - return invalidDownloadTask(behavior: .downloadCompletionHandler(completionHandler)) - } + open func downloadTask(withResumeData resumeData: Data, completionHandler: @escaping (URL?, URLResponse?, Error?) -> Void) -> URLSessionDownloadTask { NSUnimplemented() } } internal extension URLSession { diff --git a/Foundation/URLSession/URLSessionTask.swift b/Foundation/URLSession/URLSessionTask.swift index 2f4629ef0f..6b22203642 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.knownBody?.getBodyLength() { + if let bodyLength = try? self.body.getBodyLength() { toBeSent = Int64(clamping: bodyLength) } else if self.countOfBytesExpectedToSend > 0 { toBeSent = Int64(clamping: self.countOfBytesExpectedToSend) @@ -95,10 +95,9 @@ open class URLSessionTask : NSObject, NSCopying { /// How many times the task has been suspended, 0 indicating a running task. internal var suspendCount = 1 - - internal var actualSession: URLSession? { return session as? URLSession } internal var session: URLSessionProtocol! //change to nil when task completes - + internal let body: _Body + fileprivate enum ProtocolState { case toBeCreated case awaitingCacheReply(Bag<(URLProtocol?) -> Void>) @@ -194,27 +193,6 @@ 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 { @@ -234,7 +212,7 @@ open class URLSessionTask : NSObject, NSCopying { session = _MissingURLSession() taskIdentifier = 0 originalRequest = nil - knownBody = URLSessionTask._Body.none + body = .none workQueue = DispatchQueue(label: "URLSessionTask.notused.0") super.init() } @@ -248,13 +226,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.knownBody = body + self.body = body super.init() self.currentRequest = request self.progress.cancellationHandler = { [weak self] in @@ -274,7 +252,7 @@ open class URLSessionTask : NSObject, NSCopying { } /// An identifier for this task, assigned by and unique to the owning session - open internal(set) var taskIdentifier: Int + open private(set) var taskIdentifier: Int /// May be nil if this is a stream task @@ -611,17 +589,6 @@ 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 16874e8d5f..ef3be7ce75 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, body: _Body) { + override func configureEasyHandle(for request: URLRequest) { 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 (body, try body.getBodyLength()) { - case (.none, _): + switch (task?.body, try task?.body.getBodyLength()) { + case (.some(URLSessionTask._Body.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 377ee065fc..26b5993b2e 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, body: _Body) { + override func configureEasyHandle(for request: URLRequest) { // 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 (body, try body.getBodyLength()) { - case (.none, _): + switch (task?.body, try task?.body.getBodyLength()) { + case (nil, _): set(requestBodyLength: .noBody) case (_, let length?): set(requestBodyLength: .length(length)) @@ -509,15 +509,11 @@ 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 let task = task { - if task.knownBody == nil { - return [] - } else if case .some(.none) = task.knownBody { - return [] - } + if task?.body == nil { + return [] + } else { + return ["Expect"] } - - return ["Expect"] } } diff --git a/TestFoundation/TestURLSession.swift b/TestFoundation/TestURLSession.swift index 9e9c6ab243..22de3d2575 100644 --- a/TestFoundation/TestURLSession.swift +++ b/TestFoundation/TestURLSession.swift @@ -9,6 +9,58 @@ 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", test_noDoubleCallbackWhenCancellingAndProtocolFailsFast), + ("test_cancelledTasksCannotBeResumed", test_cancelledTasksCannotBeResumed), + ] + } + func test_dataTaskWithURL() { let urlString = "http://127.0.0.1:\(TestURLSession.serverPort)/Nepal" let url = URL(string: urlString)! @@ -147,7 +199,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(testCase: self, description: "Download GET \(urlString): with a delegate") + let d = DownloadTask(with: expectation(description: "Download GET \(urlString): with a delegate")) d.run(with: url) waitForExpectations(timeout: 12) } @@ -155,7 +207,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(testCase: self, description: "Download GET \(urlString): with a delegate") + let d = DownloadTask(with: expectation(description: "Download GET \(urlString): with a delegate")) d.run(with: urlRequest) waitForExpectations(timeout: 12) } @@ -203,7 +255,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(testCase: self, description: "GET \(urlString): gzipped response") + let d = DownloadTask(with: expectation(description: "GET \(urlString): gzipped response")) d.run(with: url) waitForExpectations(timeout: 12) if d.totalBytesWritten != "Hello World!".utf8.count { @@ -974,104 +1026,6 @@ 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 { @@ -1234,28 +1188,15 @@ extension DataTask : URLSessionTaskDelegate { class DownloadTask : NSObject { var totalBytesWritten: Int64 = 0 - var didDownloadExpectation: XCTestExpectation? - let didCompleteExpectation: XCTestExpectation + let dwdExpectation: XCTestExpectation! var session: URLSession! = nil var task: URLSessionDownloadTask! = nil - var errorExpectation: ((Error) -> Void)? - weak var testCase: XCTestCase? - var expectationsDescription: String - 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. + init(with expectation: XCTestExpectation) { + dwdExpectation = expectation } func run(with url: URL) { - makeDownloadExpectation() let config = URLSessionConfiguration.default config.timeoutIntervalForRequest = 8 session = URLSession(configuration: config, delegate: self, delegateQueue: nil) @@ -1264,32 +1205,12 @@ 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 { @@ -1300,61 +1221,24 @@ 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?) { - 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") - } + guard let e = error as? URLError else { return } + XCTAssertEqual(e.code, .timedOut, "Unexpected error code") + dwdExpectation.fulfill() } } -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 @@ -1420,7 +1304,6 @@ extension HTTPRedirectionDataTask : URLSessionTaskDelegate { class HTTPUploadDelegate: NSObject { var uploadCompletedExpectation: XCTestExpectation! - var streamToProvideOnRequest: InputStream? var totalBytesSent: Int64 = 0 } @@ -1428,14 +1311,6 @@ 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 { @@ -1445,3 +1320,25 @@ 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 + } +}