Skip to content

Commit 30b45a2

Browse files
committed
URLSession: Update all task types with the byte count of sent data
- urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:) was only sent to tasks of type URLSesisonUploadTask, but testing showed it should be sent to all task types. - Update tests that now see an extra delegate callback of this method. - test_simpleUploadWithDelegateProvidingInputStream was flaky due to the expectation sometimes being fulfilled multiple times as it was fulfilled in the urlSession(_:dataTask:didReceive:) delegate. - Migrate this test to the SessionDelegate and add extra tests to check the response and delegate callbacks for each HTTP method. - For HTTPUploadDelegate, add the expectation fulfilment to a delegate callback for urlSession(_ :task:didCompleteWithError:) so that test_simpleUploadWithDelegate() still works.
1 parent e1ceed8 commit 30b45a2

File tree

2 files changed

+121
-68
lines changed

2 files changed

+121
-68
lines changed

Sources/FoundationNetworking/URLSession/NativeProtocol.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,7 @@ internal class _NativeProtocol: URLProtocol, _EasyHandleDelegate {
160160
}
161161

162162
fileprivate func notifyDelegate(aboutUploadedData count: Int64) {
163-
guard let task = self.task as? URLSessionUploadTask,
164-
let session = self.task?.session as? URLSession,
163+
guard let task = self.task, let session = task.session as? URLSession,
165164
case .taskDelegate(let delegate) = session.behaviour(for: task) else { return }
166165
task.countOfBytesSent += count
167166
session.delegateQueue.addOperation {

Tests/Foundation/Tests/TestURLSession.swift

Lines changed: 120 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -121,17 +121,16 @@ class TestURLSession: LoopbackServerTest {
121121
// Only POST sets a default Content-Type if it is nil
122122
let postedContentType = contentType ?? ((method == "POST") ? "application/x-www-form-urlencoded" : nil)
123123

124+
let callBacks: [String]
124125
switch method {
125126
case "HEAD":
126127
XCTAssertNil(delegate.error)
127128
XCTAssertNotNil(delegate.response)
128129
XCTAssertEqual(httpResponse?.statusCode, 200)
129-
XCTAssertEqual(delegate.callbacks.count, 2)
130-
let callbacks = ["urlSession(_:dataTask:didReceive:completionHandler:)",
131-
"urlSession(_:task:didCompleteWithError:)"
132-
]
133-
XCTAssertEqual(delegate.callbacks, callbacks)
134130
XCTAssertNil(delegate.receivedData)
131+
callBacks = ["urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)",
132+
"urlSession(_:dataTask:didReceive:completionHandler:)",
133+
"urlSession(_:task:didCompleteWithError:)"]
135134

136135
case "GET":
137136
// GET requests must not have a body, which causes an error
@@ -144,21 +143,14 @@ class TestURLSession: LoopbackServerTest {
144143
XCTAssertEqual(errorURL, url)
145144

146145
XCTAssertNil(delegate.response)
147-
XCTAssertEqual(delegate.callbacks.count, 1)
148-
XCTAssertEqual(delegate.callbacks, ["urlSession(_:task:didCompleteWithError:)"])
149146
XCTAssertNil(delegate.receivedData)
147+
callBacks = ["urlSession(_:task:didCompleteWithError:)"]
150148

151149
default:
152150
XCTAssertNil(delegate.error)
153151
XCTAssertNotNil(delegate.response)
154152
XCTAssertEqual(httpResponse?.statusCode, 200)
155153

156-
XCTAssertEqual(delegate.callbacks.count, 3)
157-
let callBacks = ["urlSession(_:dataTask:didReceive:completionHandler:)",
158-
"urlSession(_:dataTask:didReceive:)",
159-
"urlSession(_:task:didCompleteWithError:)"
160-
]
161-
XCTAssertEqual(delegate.callbacks, callBacks)
162154
XCTAssertNotNil(delegate.receivedData)
163155
XCTAssertEqual(delegate.receivedData?.count, contentLength)
164156
if let receivedData = delegate.receivedData, let jsonBody = try? JSONSerialization.jsonObject(with: receivedData, options: []) as? [String: String] {
@@ -170,8 +162,14 @@ class TestURLSession: LoopbackServerTest {
170162
}
171163
} else {
172164
XCTFail("No JSON body for \(method)")
173-
}
165+
}
166+
callBacks = ["urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)",
167+
"urlSession(_:dataTask:didReceive:completionHandler:)",
168+
"urlSession(_:dataTask:didReceive:)",
169+
"urlSession(_:task:didCompleteWithError:)"]
174170
}
171+
XCTAssertEqual(delegate.callbacks.count, callBacks.count)
172+
XCTAssertEqual(delegate.callbacks, callBacks)
175173
}
176174
}
177175
}
@@ -990,10 +988,12 @@ class TestURLSession: LoopbackServerTest {
990988

991989
delegate.uploadCompletedExpectation = expectation(description: "PUT \(urlString): Upload data")
992990

993-
let fileData = Data(count: 16*1024)
991+
let fileData = Data(count: 16 * 1024)
994992
let task = session.uploadTask(with: request, from: fileData)
995993
task.resume()
996994
waitForExpectations(timeout: 20)
995+
XCTAssertEqual(delegate.totalBytesSent, Int64(fileData.count))
996+
997997
}
998998

999999
func test_requestWithEmptyBody() throws {
@@ -1075,17 +1075,16 @@ class TestURLSession: LoopbackServerTest {
10751075
// Only POST sets a default Content-Type if it is nil
10761076
let postedContentType = contentType ?? ((method == "POST") ? "application/x-www-form-urlencoded" : nil)
10771077

1078+
let callBacks: [String]
10781079
switch method {
10791080
case "HEAD":
10801081
XCTAssertNil(delegate.error)
10811082
XCTAssertNotNil(delegate.response)
10821083
XCTAssertEqual(httpResponse?.statusCode, 200)
1083-
XCTAssertEqual(delegate.callbacks.count, 2)
1084-
let callbacks = ["urlSession(_:dataTask:didReceive:completionHandler:)",
1085-
"urlSession(_:task:didCompleteWithError:)"
1086-
]
1087-
XCTAssertEqual(delegate.callbacks, callbacks)
10881084
XCTAssertNil(delegate.receivedData)
1085+
callBacks = ["urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)",
1086+
"urlSession(_:dataTask:didReceive:completionHandler:)",
1087+
"urlSession(_:task:didCompleteWithError:)"]
10891088

10901089
case "GET":
10911090
// GET requests must not have a body, which causes an error
@@ -1098,22 +1097,13 @@ class TestURLSession: LoopbackServerTest {
10981097
XCTAssertEqual(errorURL, url)
10991098

11001099
XCTAssertNil(delegate.response)
1101-
XCTAssertEqual(delegate.callbacks.count, 1)
1102-
XCTAssertEqual(delegate.callbacks, ["urlSession(_:task:didCompleteWithError:)"])
11031100
XCTAssertNil(delegate.receivedData)
1104-
1101+
callBacks = ["urlSession(_:task:didCompleteWithError:)"]
11051102

11061103
default:
11071104
XCTAssertNil(delegate.error)
11081105
XCTAssertNotNil(delegate.response)
11091106
XCTAssertEqual(httpResponse?.statusCode, 200)
1110-
1111-
XCTAssertEqual(delegate.callbacks.count, 3)
1112-
let callBacks = ["urlSession(_:dataTask:didReceive:completionHandler:)",
1113-
"urlSession(_:dataTask:didReceive:)",
1114-
"urlSession(_:task:didCompleteWithError:)"
1115-
]
1116-
XCTAssertEqual(delegate.callbacks, callBacks)
11171107
XCTAssertNotNil(delegate.receivedData)
11181108
XCTAssertEqual(delegate.receivedData?.count, contentLength)
11191109
if let receivedData = delegate.receivedData, let jsonBody = try? JSONSerialization.jsonObject(with: receivedData, options: []) as? [String: String] {
@@ -1129,7 +1119,13 @@ class TestURLSession: LoopbackServerTest {
11291119
} else {
11301120
XCTFail("No JSON body for \(method)")
11311121
}
1122+
callBacks = ["urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)",
1123+
"urlSession(_:dataTask:didReceive:completionHandler:)",
1124+
"urlSession(_:dataTask:didReceive:)",
1125+
"urlSession(_:task:didCompleteWithError:)"]
11321126
}
1127+
XCTAssertEqual(delegate.callbacks.count, callBacks.count)
1128+
XCTAssertEqual(delegate.callbacks, callBacks)
11331129
}
11341130
}
11351131
}
@@ -1661,40 +1657,81 @@ class TestURLSession: LoopbackServerTest {
16611657

16621658
func test_simpleUploadWithDelegateProvidingInputStream() throws {
16631659

1664-
let fileData = Data(count: 16*1024)
1660+
let fileData = Data(count: 16 * 1024)
16651661
for method in httpMethods {
1666-
let delegate = HTTPUploadDelegate()
1667-
let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil)
16681662
let urlString = "http://127.0.0.1:\(TestURLSession.serverPort)/" + method.lowercased()
1669-
var request = URLRequest(url: try XCTUnwrap(URL(string: urlString)))
1663+
let url = try XCTUnwrap(URL(string: urlString))
1664+
var request = URLRequest(url: url)
16701665
request.httpMethod = method
16711666

1672-
let stream = InputStream(data: fileData)
1667+
let delegate = SessionDelegate(with: expectation(description: "\(method) \(urlString): Upload data"))
1668+
delegate.newBodyStreamHandler = { (completionHandler: @escaping (InputStream?) -> Void) in
1669+
completionHandler(InputStream(data: fileData))
1670+
}
1671+
delegate.runUploadTask(with: request, timeoutInterval: 4)
1672+
waitForExpectations(timeout: 5)
16731673

1674-
let expect = expectation(description: "\(method) \(urlString): Upload data")
1675-
if method == "GET" || method == "HEAD" { expect.isInverted = true }
1676-
delegate.uploadCompletedExpectation = expect
1677-
delegate.streamToProvideOnRequest = stream
1678-
let task = session.uploadTask(withStreamedRequest: request)
1679-
task.resume()
1674+
let httpResponse = delegate.response as? HTTPURLResponse
1675+
let callBacks: [String]
16801676

1681-
waitForExpectations(timeout: 5)
16821677
switch method {
1683-
case "GET":
1684-
XCTAssertEqual(delegate.callbacks.count, 1, "Callback count for GET request")
1685-
XCTAssertEqual(delegate.callbacks[0], "urlSession(_:task:needNewBodyStream:)")
1686-
16871678
case "HEAD":
1688-
XCTAssertEqual(delegate.callbacks.count, 2, "Callback count for HEAD request")
1689-
XCTAssertEqual(delegate.callbacks[0], "urlSession(_:task:needNewBodyStream:)")
1690-
XCTAssertEqual(delegate.callbacks[1], "urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)")
1679+
XCTAssertNil(delegate.error)
1680+
XCTAssertNotNil(delegate.response)
1681+
XCTAssertEqual(httpResponse?.statusCode, 200)
1682+
XCTAssertNil(delegate.receivedData)
1683+
XCTAssertEqual(delegate.totalBytesSent, Int64(fileData.count))
1684+
callBacks = ["urlSession(_:task:needNewBodyStream:)",
1685+
"urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)",
1686+
"urlSession(_:dataTask:didReceive:completionHandler:)",
1687+
"urlSession(_:task:didCompleteWithError:)"]
1688+
1689+
case "GET":
1690+
// GET requests must not have a body, which causes an error
1691+
XCTAssertNotNil(delegate.error)
1692+
let error = delegate.error as? URLError
1693+
XCTAssertEqual(error?.code.rawValue, NSURLErrorDataLengthExceedsMaximum)
1694+
XCTAssertEqual(error?.localizedDescription, "resource exceeds maximum size")
1695+
let userInfo = error?.userInfo as? [String: Any]
1696+
let errorURL = userInfo?[NSURLErrorFailingURLErrorKey] as? URL
1697+
XCTAssertEqual(errorURL, url)
1698+
XCTAssertNil(delegate.response)
1699+
XCTAssertNil(delegate.receivedData)
1700+
XCTAssertEqual(delegate.totalBytesSent, 0)
1701+
callBacks = ["urlSession(_:task:needNewBodyStream:)",
1702+
"urlSession(_:task:didCompleteWithError:)"]
16911703

16921704
default:
1693-
XCTAssertEqual(delegate.callbacks.count, 3, "Callback count for \(method) request")
1694-
XCTAssertEqual(delegate.callbacks[0], "urlSession(_:task:needNewBodyStream:)")
1695-
XCTAssertEqual(delegate.callbacks[1], "urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)")
1696-
XCTAssertEqual(delegate.callbacks[2], "urlSession(_:dataTask:didReceive:)")
1705+
XCTAssertNil(delegate.error)
1706+
XCTAssertNotNil(delegate.response)
1707+
XCTAssertEqual(httpResponse?.statusCode, 200)
1708+
XCTAssertEqual(delegate.totalBytesSent, Int64(fileData.count))
1709+
XCTAssertNotNil(delegate.receivedData)
1710+
let contentLength = Int(httpResponse?.value(forHTTPHeaderField: "Content-Length") ?? "")
1711+
1712+
XCTAssertEqual(delegate.receivedData?.count, contentLength)
1713+
if let receivedData = delegate.receivedData, let jsonBody = try? JSONSerialization.jsonObject(with: receivedData, options: []) as? [String: String] {
1714+
if let postedContentType = (method == "POST") ? "application/x-www-form-urlencoded" : nil {
1715+
XCTAssertEqual(jsonBody["Content-Type"], postedContentType)
1716+
} else {
1717+
XCTAssertNil(jsonBody.index(forKey: "Content-Type"))
1718+
}
1719+
if let postedBody = jsonBody["x-base64-body"], let decodedBody = Data(base64Encoded: postedBody) {
1720+
XCTAssertEqual(decodedBody, fileData)
1721+
} else {
1722+
XCTFail("Could not decode Base64 body for \(method)")
1723+
}
1724+
} else {
1725+
XCTFail("No JSON body for \(method)")
1726+
}
1727+
callBacks = ["urlSession(_:task:needNewBodyStream:)",
1728+
"urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)",
1729+
"urlSession(_:dataTask:didReceive:completionHandler:)",
1730+
"urlSession(_:dataTask:didReceive:)",
1731+
"urlSession(_:task:didCompleteWithError:)"]
16971732
}
1733+
XCTAssertEqual(delegate.callbacks.count, callBacks.count, "Callback count for \(method)")
1734+
XCTAssertEqual(delegate.callbacks, callBacks, "Callbacks for \(method)")
16981735
}
16991736
}
17001737

@@ -1800,11 +1837,16 @@ class SessionDelegate: NSObject, URLSessionDelegate {
18001837
typealias RedirectionHandler = (HTTPURLResponse, URLRequest, @escaping (URLRequest?) -> Void) -> Void
18011838
var redirectionHandler: RedirectionHandler? = nil
18021839

1840+
typealias NewBodyStreamHandler = (@escaping (InputStream?) -> Void) -> Void
1841+
var newBodyStreamHandler: NewBodyStreamHandler? = nil
1842+
1843+
18031844
private(set) var receivedData: Data?
18041845
private(set) var error: Error?
18051846
private(set) var response: URLResponse?
18061847
private(set) var redirectionRequest: URLRequest?
18071848
private(set) var redirectionResponse: HTTPURLResponse?
1849+
private(set) var totalBytesSent: Int64 = 0
18081850
private(set) var callbacks: [String] = []
18091851
private(set) var authenticationChallenges: [URLAuthenticationChallenge] = []
18101852

@@ -1838,6 +1880,14 @@ class SessionDelegate: NSObject, URLSessionDelegate {
18381880
task.resume()
18391881
}
18401882

1883+
func runUploadTask(with request: URLRequest, timeoutInterval: Double = 3) {
1884+
let config = URLSessionConfiguration.default
1885+
config.timeoutIntervalForRequest = timeoutInterval
1886+
session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
1887+
task = session.uploadTask(withStreamedRequest: request)
1888+
task.resume()
1889+
}
1890+
18411891
func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) {
18421892
callbacks.append(#function)
18431893
self.error = error
@@ -1853,13 +1903,24 @@ extension SessionDelegate: URLSessionTaskDelegate {
18531903

18541904
public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
18551905
callbacks.append(#function)
1856-
18571906
self.error = error
18581907
expectation.fulfill()
18591908
}
18601909

1910+
public func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
1911+
if callbacks.last != #function {
1912+
callbacks.append(#function)
1913+
}
1914+
self.totalBytesSent = totalBytesSent
1915+
}
1916+
1917+
// New Body Stream
18611918
public func urlSession(_ session: URLSession, task: URLSessionTask, needNewBodyStream completionHandler: @escaping (InputStream?) -> Void) {
18621919
callbacks.append(#function)
1920+
1921+
if let handler = newBodyStreamHandler {
1922+
handler(completionHandler)
1923+
}
18631924
}
18641925

18651926
// HTTP Authentication Challenge
@@ -2255,33 +2316,26 @@ class HTTPUploadDelegate: NSObject {
22552316
private(set) var callbacks: [String] = []
22562317

22572318
var uploadCompletedExpectation: XCTestExpectation!
2258-
var streamToProvideOnRequest: InputStream?
22592319
var totalBytesSent: Int64 = 0
22602320
}
22612321

22622322
extension HTTPUploadDelegate: URLSessionTaskDelegate {
2323+
public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
2324+
callbacks.append(#function)
2325+
uploadCompletedExpectation.fulfill()
2326+
}
2327+
22632328
func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
22642329
if callbacks.last != #function {
22652330
callbacks.append(#function)
22662331
}
22672332
self.totalBytesSent = totalBytesSent
22682333
}
2269-
2270-
func urlSession(_ session: URLSession, task: URLSessionTask, needNewBodyStream completionHandler: @escaping (InputStream?) -> Void) {
2271-
callbacks.append(#function)
2272-
if streamToProvideOnRequest == nil {
2273-
XCTFail("This shouldn't have been invoked -- no stream was set.")
2274-
}
2275-
2276-
completionHandler(self.streamToProvideOnRequest)
2277-
}
22782334
}
22792335

22802336
extension HTTPUploadDelegate: URLSessionDataDelegate {
22812337
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
22822338
callbacks.append(#function)
2283-
XCTAssertEqual(self.totalBytesSent, 16*1024)
2284-
uploadCompletedExpectation.fulfill()
22852339
}
22862340
}
22872341

0 commit comments

Comments
 (0)