Skip to content

Commit

Permalink
Added support for canceling tasks, fixes net-a-porter-mobile#42, fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
csknns committed Jul 5, 2017
1 parent b052fab commit f916f8a
Show file tree
Hide file tree
Showing 4 changed files with 248 additions and 92 deletions.
42 changes: 40 additions & 2 deletions Example/Tests/CallbackMethodTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ final class CallbackMethodTests: XCTestCase {

// Tell URLSession to mock this URL
let body = "Test response 1".data(using: .utf8)!
URLSession.mockNext(request: request, body: body)
URLSession.mockNext(request: request, body: body, delay: 0.25)

// Create a session
let session = URLSession(configuration: .default)
Expand All @@ -32,6 +32,44 @@ final class CallbackMethodTests: XCTestCase {
}
task.resume()

self.waitForExpectations(timeout: 0.1) { _ in }
// Record the start time
let start = NSDate()

self.waitForExpectations(timeout: 0.5) { _ in
// Check the delay
let interval = -start.timeIntervalSinceNow

XCTAssert(interval >= 0.25, "Should have taken more than 0.25 second to perform (it took \(interval)")
}
}

func testSession_WithSingleMock_CancelShouldReturnError_Callback() {
let expectation = self.expectation(description: "Callback called back")

// Make the request we are going to mock
let url = URL(string: "https://www.example.com")!
let request = URLRequest(url: url)

// Tell URLSession to mock this URL
let body = "Test response 1".data(using: .utf8)!
URLSession.mockNext(request: request, body: body, delay: 2.25)

// Create a session
let session = URLSession(configuration: .default)

// Perform the task
let task = session.dataTask(with: request) { data, response, error in
let cancelledError : NSError = NSError(domain: NSURLErrorDomain, code: NSURLErrorCancelled, userInfo: nil)

XCTAssertEqual((error as NSError?), cancelledError)

expectation.fulfill()
}
task.resume()

task.cancel()

self.waitForExpectations(timeout: 2.5) { _ in }
}

}
30 changes: 30 additions & 0 deletions Example/Tests/NSURLSessionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,36 @@ class NSURLSessionTests: XCTestCase {
XCTAssertNotNil(delegate.errorKeyedByTaskIdentifier[task2.taskIdentifier])
}
}

func testSession_WithSingleMock_CancelShouldReturnError() {
let expectation = self.expectation(description: "Complete called for 1")

// Tell NSURLSession to mock this URL, each time with different data
let url = URL(string: "https://www.example.com/1")!
let body = "Test response".data(using: String.Encoding.utf8)!
let request = URLRequest(url: url)
_ = URLSession.mockNext(request: request, body: body)

// Create a session
let conf = URLSessionConfiguration.default
let delegate = SessionTestDelegate(expectations: [ expectation ])
let session = URLSession(configuration: conf, delegate: delegate, delegateQueue: OperationQueue())

// Perform both tasks
let task = session.dataTask(with: request)
task.resume()

task.cancel()

// Validate that the mock data was returned
self.waitForExpectations(timeout: 1) { timeoutError in
XCTAssertNil(timeoutError)

let cancelledError : NSError = NSError(domain: NSURLErrorDomain, code: NSURLErrorCancelled, userInfo: nil)

XCTAssertEqual(delegate.errorKeyedByTaskIdentifier[task.taskIdentifier]! as NSError, cancelledError, "When event is cancelled delegate should return an error in domain NSURLErrorDomain with code NSURLErrorCancelled")
}
}

func testSession_WithClearMocks_ShouldClearExistingMocks() {
let path = "www.example.com/test.json"
Expand Down
99 changes: 10 additions & 89 deletions Pod/Classes/NSURLSession/MockEntry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,105 +41,26 @@ class MockEntry: SessionMock, Equatable {

// Use the extractions from the match to create the data
case .matches(let extractions):
// Do we respond to the completion handler, or the session delegate?
let handler = { (completionHandler: ((Data?, URLResponse?, Error?) -> Void)?) -> (MockSessionDataTask) -> Void in
if let completionHandler = completionHandler {
return self.respondToCompletionHandler(request: request, extractions: extractions, completionHandler: completionHandler)
} else {
return self.respondToDelegate(request: request, extractions: extractions, session: session)
}
}(completionHandler)

let task = MockSessionDataTask() { task in
task._state = .running

handler(task)
}

task._originalRequest = request

return task
}
}

// MARK: - Completion handler responder
private func respondToCompletionHandler(request: URLRequest, extractions: [String], completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> (MockSessionDataTask) -> Void {
return { task in
let response = self.response(request.url!, extractions)

switch response {
case .success(let statusCode, let headers, let body):
let urlResponse = HTTPURLResponse(url: request.url!, statusCode: statusCode, httpVersion: "HTTP/1.1", headerFields: headers)!
completionHandler(body, urlResponse, nil)
case .failure(let error):
let urlResponse = HTTPURLResponse(url: request.url!, statusCode: 500, httpVersion: "HTTP/1.1", headerFields: [:])!
completionHandler(nil, urlResponse, error)
}
}
}

// MARK: - Session delegate responder
private func respondToDelegate(request: URLRequest, extractions: [String], session: URLSession) -> (MockSessionDataTask) -> Void {
return { task in
let response = self.response(request.url!, extractions)

switch response {
case let .success(statusCode, headers, body):
self.respondToDelegateWith(request: request, session: session, task: task, statusCode: statusCode, headers: headers, body: body)

case let .failure(error):
self.respondToDelegateWith(request: request, session: session, task: task, error: error)
}
}
}

private func respondToDelegateWith(request: URLRequest, session: URLSession, task: MockSessionDataTask, statusCode: Int, headers: [String:String], body: Data?) {
let timeDelta = 0.02
var time = self.delay
let response = self.response(request.url!, extractions)

if let delegate = session.delegate as? URLSessionDataDelegate {

DispatchQueue.main.asyncAfter(deadline: .now() + time) {
let response = HTTPURLResponse(url: request.url!, statusCode: statusCode, httpVersion: "HTTP/1.1", headerFields: headers)!
task.response = response

delegate.urlSession?(session, dataTask: task, didReceive: response) { _ in }
}

time += timeDelta
switch(response) {
case let .success(statusCode, headers, body):
task.scheduleMockedResponsesWith(request: request, session: session, delay: self.delay, statusCode: statusCode, headers: headers, body: body, completionHandler: completionHandler)

if let body = body {

DispatchQueue.main.asyncAfter(deadline: .now() + time) {
delegate.urlSession?(session, dataTask: task, didReceive: body)
case let .failure(error):
task.scheduleMockedResponsesWith(request: request, session: session, delay: self.delay, error: error, completionHandler: completionHandler)
}

time += timeDelta
}
}

if let delegate = session.delegate as? URLSessionTaskDelegate {

DispatchQueue.main.asyncAfter(deadline: .now() + time) {
delegate.urlSession?(session, task: task, didCompleteWithError: nil)
task._state = .completed
}
}

}

private func respondToDelegateWith(request: URLRequest, session: URLSession, task: MockSessionDataTask, error: NSError) {
if let delegate = session.delegate as? URLSessionTaskDelegate {

DispatchQueue.main.asyncAfter(deadline: .now() + self.delay) {
delegate.urlSession?(session, task: task, didCompleteWithError: error)
task._state = .completed
}


return task
}
}

}


func ==(lhs: MockEntry, rhs: MockEntry) -> Bool {
return lhs === rhs
}
169 changes: 168 additions & 1 deletion Pod/Classes/NSURLSession/MockSessionDataTask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ And, curiously, added the properties that `NSURLSessionDataTask` says it has but
*/
class MockSessionDataTask: URLSessionDataTask {

var mockedResponseItems : [DispatchWorkItem]?;
weak var session: URLSession?
private var mutex: pthread_mutex_t = pthread_mutex_t()
private var completionHandler: ((Data? , URLResponse?, Error?) -> Void)?

let onResume: (_ task: MockSessionDataTask)->()

init(onResume: @escaping (_ task: MockSessionDataTask)->()) {
Expand Down Expand Up @@ -65,7 +70,28 @@ class MockSessionDataTask: URLSessionDataTask {
}

override func cancel() {
self._state = .canceling
pthread_mutex_lock(&mutex)
//Cancel the task only if we haven't got a response yet and the task is running
if (self._response == nil
&& (self._state == .running || self._state == .suspended)) {
self._state = .canceling

for mockedResponseItem in mockedResponseItems! {
mockedResponseItem.cancel()
}

if let completionHandler = completionHandler {
let urlResponse = HTTPURLResponse(url: (self.originalRequest?.url)!, statusCode: NSURLErrorCancelled, httpVersion: "HTTP/1.1", headerFields: [:])!
completionHandler(nil, urlResponse, NSError(domain: NSURLErrorDomain, code: NSURLErrorCancelled, userInfo: nil))
}
else {
if let delegate = session?.delegate as? URLSessionDataDelegate {
delegate.urlSession?(self.session!, task: self, didCompleteWithError: NSError(domain: NSURLErrorDomain, code: NSURLErrorCancelled, userInfo: nil))
}
}

}
pthread_mutex_unlock(&mutex)
}

override var countOfBytesExpectedToSend: Int64 {
Expand All @@ -75,4 +101,145 @@ class MockSessionDataTask: URLSessionDataTask {
override var countOfBytesExpectedToReceive: Int64 {
return NSURLSessionTransferSizeUnknown
}

func scheduleMockedResponsesWith(request: URLRequest, session: URLSession, delay: Double, statusCode: Int, headers: [String:String], body: Data?, completionHandler : ((Data?, URLResponse?, Error?) -> Void)?) {
var items : [DispatchWorkItem] = [];

if let completionHandler = completionHandler {
items.append(DispatchWorkItem { [weak self] in
guard let task = self else { return }

pthread_mutex_lock(&task.mutex)
if (task._state == .running
|| task._state == .suspended) {
let response = HTTPURLResponse(url: request.url!, statusCode: statusCode, httpVersion: "HTTP/1.1", headerFields: headers)!
task.response = response
task._state = .completed
completionHandler(body, response, nil);
}
pthread_mutex_unlock(&task.mutex)
})
}
else {
items.append(DispatchWorkItem { [weak self] in
guard let task = self else { return }
guard let delegate : URLSessionDataDelegate = task.session?.delegate as? URLSessionDataDelegate else { return }

pthread_mutex_lock(&task.mutex)
if (task._state == .running
|| task._state == .suspended) {
let response = HTTPURLResponse(url: request.url!, statusCode: statusCode, httpVersion: "HTTP/1.1", headerFields: headers)!
task.response = response
delegate.urlSession?(session, dataTask: task, didReceive: response) { _ in }
}
pthread_mutex_unlock(&task.mutex)
})

if let body = body {
items.append(DispatchWorkItem { [weak self] in
guard let task = self else { return }
guard let delegate : URLSessionDataDelegate = task.session?.delegate as? URLSessionDataDelegate else { return }

pthread_mutex_lock(&task.mutex)
if (task._state == .running
|| task._state == .suspended) {
delegate.urlSession?(session, dataTask: task, didReceive: body)
}
pthread_mutex_unlock(&task.mutex)
})
}

items.append(DispatchWorkItem { [weak self] in
guard let task = self else { return }
guard let delegate : URLSessionTaskDelegate = task.session?.delegate as? URLSessionTaskDelegate else { return }

pthread_mutex_lock(&task.mutex)
if (task._state == .running
|| task._state == .suspended) {
task._state = .completed
delegate.urlSession?(session, task: task, didCompleteWithError: nil)
}
pthread_mutex_unlock(&task.mutex)
})
}

self.completionHandler = completionHandler;
self._originalRequest = request
self.session = session
self._state = .running

schedule(mockedResponses: items, after: delay)
}

func scheduleMockedResponsesWith(request: URLRequest, session: URLSession, delay: Double, error: NSError, completionHandler : ((Data?, URLResponse?, Error?) -> Void)?) {
var items : [DispatchWorkItem] = [];

if let completionHandler = completionHandler {
items.append(DispatchWorkItem { [weak self] in
guard let task = self else { return }

pthread_mutex_lock(&task.mutex)
if (task._state == .running
|| task._state == .suspended) {
task._state = .completed
let urlResponse = HTTPURLResponse(url: request.url!, statusCode: 500, httpVersion: "HTTP/1.1", headerFields: [:])!
completionHandler(nil, urlResponse, error)

}
pthread_mutex_unlock(&task.mutex)
})
}
else {
items.append(DispatchWorkItem { [weak self] in
guard let task = self else { return }
guard let delegate : URLSessionTaskDelegate = task.session?.delegate as? URLSessionTaskDelegate else { return }

pthread_mutex_lock(&task.mutex)
if (task._state == .running
|| task._state == .suspended) {
task._state = .completed
delegate.urlSession?(session, task: task, didCompleteWithError: error)
}
pthread_mutex_unlock(&task.mutex)
})
}

self.completionHandler = completionHandler;
self._originalRequest = request
self.session = session
self._state = .running

schedule(mockedResponses: items, after: delay)
}

private func schedule(mockedResponses: [DispatchWorkItem], after: Double) {
let timeDelta = 0.02
var time = after

pthread_mutex_lock(&mutex)

//cancel previous responses if any
if let mockedResponseItems = self.mockedResponseItems {
for mockedResponseItem in mockedResponseItems {
mockedResponseItem.cancel()
}
}

self.mockedResponseItems = mockedResponses

for mockedResponseItem in mockedResponses {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + time,
execute: mockedResponseItem)
time += timeDelta
}

pthread_mutex_unlock(&mutex)
}

deinit {
self.cancel()
pthread_mutex_destroy(&self.mutex)
}

}

0 comments on commit f916f8a

Please sign in to comment.