소스 검색

Refactor Request Event Management (#2796)

* Update Request event management and add tests.

* DRY up state switch.

* Add additional test for actions from multiple queues.
Jon Shier 6 년 전
부모
커밋
5bedc2af71

+ 0 - 4
Alamofire.xcodeproj/project.pbxproj

@@ -372,7 +372,6 @@
 		319917A4209CDAC400103A19 /* RequestTaskMap.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestTaskMap.swift; sourceTree = "<group>"; };
 		319917A9209CDCB000103A19 /* HTTPHeaders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPHeaders.swift; sourceTree = "<group>"; };
 		319917B8209CE53A00103A19 /* OperationQueue+Alamofire.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OperationQueue+Alamofire.swift"; sourceTree = "<group>"; };
-		31B2CA9421AA24F5005B371A /* Package@swift-4.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Package@swift-4.swift"; sourceTree = "<group>"; };
 		31B2CA9521AA25CD005B371A /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = "<group>"; };
 		31D83FCD20D5C29300D93E47 /* URLConvertible+URLRequestConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLConvertible+URLRequestConvertible.swift"; sourceTree = "<group>"; };
 		31DADDFA224811ED0051390F /* AlamofireExtended.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlamofireExtended.swift; sourceTree = "<group>"; };
@@ -431,7 +430,6 @@
 		4CE292321EF4A393008DA555 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = "<group>"; };
 		4CE292331EF4A393008DA555 /* CONTRIBUTING.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CONTRIBUTING.md; sourceTree = "<group>"; };
 		4CE292391EF4B12B008DA555 /* Alamofire.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = Alamofire.podspec; sourceTree = "<group>"; };
-		4CE2923A1EF4B12B008DA555 /* Package@swift-3.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Package@swift-3.swift"; sourceTree = "<group>"; };
 		4CF3B4281F5FC7900075BE59 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
 		4CF626EF1BA7CB3E0011A099 /* Alamofire.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Alamofire.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		4CF626F81BA7CB3E0011A099 /* Alamofire tvOS Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Alamofire tvOS Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -762,8 +760,6 @@
 				4CE292391EF4B12B008DA555 /* Alamofire.podspec */,
 				4CF3B4281F5FC7900075BE59 /* LICENSE */,
 				31B2CA9521AA25CD005B371A /* Package.swift */,
-				31B2CA9421AA24F5005B371A /* Package@swift-4.swift */,
-				4CE2923A1EF4B12B008DA555 /* Package@swift-3.swift */,
 			);
 			name = Deployment;
 			sourceTree = "<group>";

+ 0 - 28
Package@swift-3.swift

@@ -1,28 +0,0 @@
-// swift-tools-version:3.0
-//
-//  Package@swift-3.swift
-//
-//  Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
-//
-//  Permission is hereby granted, free of charge, to any person obtaining a copy
-//  of this software and associated documentation files (the "Software"), to deal
-//  in the Software without restriction, including without limitation the rights
-//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-//  copies of the Software, and to permit persons to whom the Software is
-//  furnished to do so, subject to the following conditions:
-//
-//  The above copyright notice and this permission notice shall be included in
-//  all copies or substantial portions of the Software.
-//
-//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-//  THE SOFTWARE.
-//
-
-import PackageDescription
-
-let package = Package(name: "Alamofire", dependencies : [], exclude: ["Tests"])

+ 0 - 41
Package@swift-4.2.swift

@@ -1,41 +0,0 @@
-// swift-tools-version:4.2
-//
-//  Package.swift
-//
-//  Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
-//
-//  Permission is hereby granted, free of charge, to any person obtaining a copy
-//  of this software and associated documentation files (the "Software"), to deal
-//  in the Software without restriction, including without limitation the rights
-//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-//  copies of the Software, and to permit persons to whom the Software is
-//  furnished to do so, subject to the following conditions:
-//
-//  The above copyright notice and this permission notice shall be included in
-//  all copies or substantial portions of the Software.
-//
-//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-//  THE SOFTWARE.
-//
-
-import PackageDescription
-
-let package = Package(
-    name: "Alamofire",
-    products: [
-        .library(
-            name: "Alamofire",
-            targets: ["Alamofire"])
-    ],
-    targets: [
-        .target(
-            name: "Alamofire",
-            path: "Source")
-    ],
-    swiftLanguageVersions: [.v3, .v4]
-)

+ 0 - 41
Package@swift-4.swift

@@ -1,41 +0,0 @@
-// swift-tools-version:4.0
-//
-//  Package@swift-4.swift
-//
-//  Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
-//
-//  Permission is hereby granted, free of charge, to any person obtaining a copy
-//  of this software and associated documentation files (the "Software"), to deal
-//  in the Software without restriction, including without limitation the rights
-//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-//  copies of the Software, and to permit persons to whom the Software is
-//  furnished to do so, subject to the following conditions:
-//
-//  The above copyright notice and this permission notice shall be included in
-//  all copies or substantial portions of the Software.
-//
-//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-//  THE SOFTWARE.
-//
-
-import PackageDescription
-
-let package = Package(
-    name: "Alamofire",
-    products: [
-        .library(
-            name: "Alamofire",
-            targets: ["Alamofire"])
-    ],
-    targets: [
-        .target(
-            name: "Alamofire",
-            path: "Source")
-    ],
-    swiftLanguageVersions: [3, 4]
-)

+ 45 - 0
Source/EventMonitor.swift

@@ -130,12 +130,21 @@ public protocol EventMonitor {
     /// Event called when a `Request` receives a `resume` call.
     func requestDidResume(_ request: Request)
 
+    /// Event called when a `Request`'s associated `URLSessionTask` is resumed.
+    func request(_ request: Request, didResumeTask task: URLSessionTask)
+
     /// Event called when a `Request` receives a `suspend` call.
     func requestDidSuspend(_ request: Request)
 
+    /// Event called when a `Request`'s associated `URLSessionTask` is suspended.
+    func request(_ request: Request, didSuspendTask task: URLSessionTask)
+
     /// Event called when a `Request` receives a `cancel` call.
     func requestDidCancel(_ request: Request)
 
+    /// Event called when a `Request`'s associated `URLSessionTask` is cancelled.
+    func request(_ request: Request, didCancelTask task: URLSessionTask)
+
     // MARK: DataRequest Events
 
     /// Event called when a `DataRequest` calls a `Validation`.
@@ -242,8 +251,11 @@ extension EventMonitor {
     public func requestIsRetrying(_ request: Request) { }
     public func requestDidFinish(_ request: Request) { }
     public func requestDidResume(_ request: Request) { }
+    public func request(_ request: Request, didResumeTask task: URLSessionTask) { }
     public func requestDidSuspend(_ request: Request) { }
+    public func request(_ request: Request, didSuspendTask task: URLSessionTask) { }
     public func requestDidCancel(_ request: Request) { }
+    public func request(_ request: Request, didCancelTask task: URLSessionTask) { }
     public func request(_ request: DataRequest,
                         didValidateRequest urlRequest: URLRequest?,
                         response: HTTPURLResponse,
@@ -424,14 +436,26 @@ public final class CompositeEventMonitor: EventMonitor {
         performEvent { $0.requestDidResume(request) }
     }
 
+    public func request(_ request: Request, didResumeTask task: URLSessionTask) {
+        performEvent { $0.request(request, didResumeTask: task) }
+    }
+
     public func requestDidSuspend(_ request: Request) {
         performEvent { $0.requestDidSuspend(request) }
     }
 
+    public func request(_ request: Request, didSuspendTask task: URLSessionTask) {
+        performEvent { $0.request(request, didSuspendTask: task) }
+    }
+
     public func requestDidCancel(_ request: Request) {
         performEvent { $0.requestDidCancel(request) }
     }
 
+    public func request(_ request: Request, didCancelTask task: URLSessionTask) {
+        performEvent { $0.request(request, didCancelTask: task) }
+    }
+
     public func request(_ request: DataRequest,
                         didValidateRequest urlRequest: URLRequest?,
                         response: HTTPURLResponse,
@@ -571,12 +595,21 @@ open class ClosureEventMonitor: EventMonitor {
     /// Closure called on the `requestDidResume(_:)` event.
     open var requestDidResume: ((Request) -> Void)?
 
+    /// Closure called on the `request(_:didResumeTask:)` event.
+    open var requestDidResumeTask: ((Request, URLSessionTask) -> Void)?
+
     /// Closure called on the `requestDidSuspend(_:)` event.
     open var requestDidSuspend: ((Request) -> Void)?
 
+    /// Closure called on the `request(_:didSuspendTask:)` event.
+    open var requestDidSuspendTask: ((Request, URLSessionTask) -> Void)?
+
     /// Closure called on the `requestDidCancel(_:)` event.
     open var requestDidCancel: ((Request) -> Void)?
 
+    /// Closure called on the `request(_:didCancelTask:)` event.
+    open var requestDidCancelTask: ((Request, URLSessionTask) -> Void)?
+
     /// Closure called on the `request(_:didValidateRequest:response:data:withResult:)` event.
     open var requestDidValidateRequestResponseDataWithResult: ((DataRequest, URLRequest?, HTTPURLResponse, Data?, Request.ValidationResult) -> Void)?
 
@@ -722,14 +755,26 @@ open class ClosureEventMonitor: EventMonitor {
         requestDidResume?(request)
     }
 
+    public func request(_ request: Request, didResumeTask task: URLSessionTask) {
+        requestDidResumeTask?(request, task)
+    }
+
     open func requestDidSuspend(_ request: Request) {
         requestDidSuspend?(request)
     }
 
+    public func request(_ request: Request, didSuspendTask task: URLSessionTask) {
+        requestDidSuspendTask?(request, task)
+    }
+
     open func requestDidCancel(_ request: Request) {
         requestDidCancel?(request)
     }
 
+    public func request(_ request: Request, didCancelTask task: URLSessionTask) {
+        requestDidCancelTask?(request, task)
+    }
+
     open func request(_ request: DataRequest,
                       didValidateRequest urlRequest: URLRequest?,
                       response: HTTPURLResponse,

+ 16 - 0
Source/Protector.swift

@@ -128,3 +128,19 @@ extension Protector where T == Data? {
         }
     }
 }
+
+extension Protector where T == Request.MutableState {
+    /// Attempts to transition to the passed `State`.
+    ///
+    /// - Parameter state: The `State` to attempt transition to.
+    /// - Returns:         Whether the transtion occured.
+    func attemptToTransitionTo(_ state: Request.State) -> Bool {
+        return lock.around {
+            guard value.state.canTransitionTo(state) else { return false }
+
+            value.state = state
+
+            return true
+        }
+    }
+}

+ 22 - 13
Source/Request.swift

@@ -66,12 +66,12 @@ open class Request {
     /// The `Request`'s interceptor.
     public let interceptor: RequestInterceptor?
     /// The `Request`'s delegate.
-    public weak var delegate: RequestDelegate?
+    public private(set) weak var delegate: RequestDelegate?
 
     // MARK: - Updated State
 
     /// Type encapsulating all mutable state that may need to be accessed from anything other than the `underlyingQueue`.
-    private struct MutableState {
+    struct MutableState {
         /// State of the `Request`.
         var state: State = .initialized
         /// `ProgressHandler` and `DispatchQueue` provided for upload progress callbacks.
@@ -303,11 +303,21 @@ open class Request {
         eventMonitor?.requestDidResume(self)
     }
 
+    /// Called when a `URLSessionTask` is resumed on behalf of the instance.
+    func didResumeTask(_ task: URLSessionTask) {
+        eventMonitor?.request(self, didResumeTask: task)
+    }
+
     /// Called when suspension is completed.
     func didSuspend() {
         eventMonitor?.requestDidSuspend(self)
     }
 
+    /// Callend when a `URLSessionTask` is suspended on behalf of the instance.
+    func didSuspendTask(_ task: URLSessionTask) {
+        eventMonitor?.request(self, didSuspendTask: task)
+    }
+
     /// Called when cancellation is completed, sets `error` to `AFError.explicitlyCancelled`.
     func didCancel() {
         error = AFError.explicitlyCancelled
@@ -315,6 +325,11 @@ open class Request {
         eventMonitor?.requestDidCancel(self)
     }
 
+    /// Called when a `URLSessionTask` is cancelled on behalf of the instance.
+    func didCancelTask(_ task: URLSessionTask) {
+        eventMonitor?.request(self, didCancelTask: task)
+    }
+
     /// Called when a `URLSessionTaskMetrics` value is gathered on behalf of the `Request`.
     func didGatherMetrics(_ metrics: URLSessionTaskMetrics) {
         protectedMutableState.write { $0.metrics.append(metrics) }
@@ -465,9 +480,7 @@ open class Request {
     /// - Returns: The `Request`.
     @discardableResult
     open func cancel() -> Self {
-        guard state.canTransitionTo(.cancelled) else { return self }
-
-        state = .cancelled
+        guard protectedMutableState.attemptToTransitionTo(.cancelled) else { return self }
 
         delegate?.cancelRequest(self)
 
@@ -479,9 +492,7 @@ open class Request {
     /// - Returns: The `Request`.
     @discardableResult
     open func suspend() -> Self {
-        guard state.canTransitionTo(.suspended) else { return self }
-
-        state = .suspended
+        guard protectedMutableState.attemptToTransitionTo(.suspended) else { return self }
 
         delegate?.suspendRequest(self)
 
@@ -494,9 +505,7 @@ open class Request {
     /// - Returns: The `Request`.
     @discardableResult
     open func resume() -> Self {
-        guard state.canTransitionTo(.resumed) else { return self }
-
-        state = .resumed
+        guard protectedMutableState.attemptToTransitionTo(.resumed) else { return self }
 
         delegate?.resumeRequest(self)
 
@@ -877,12 +886,12 @@ open class DownloadRequest: Request {
 
     // MARK: Updated State
 
-    private struct MutableState {
+    private struct DownloadRequestMutableState {
         var resumeData: Data?
         var fileURL: URL?
     }
 
-    private let protectedMutableState: Protector<MutableState> = Protector(MutableState())
+    private let protectedMutableState: Protector<DownloadRequestMutableState> = Protector(DownloadRequestMutableState())
 
     public var resumeData: Data? { return protectedMutableState.directValue.resumeData }
     public var fileURL: URL? { return protectedMutableState.directValue.fileURL }

+ 34 - 17
Source/Session.swift

@@ -455,7 +455,7 @@ open class Session {
         requestTaskMap[request] = task
         request.didCreateTask(task)
 
-        resumeOrSuspendTask(task, ifNecessaryForRequest: request)
+        updateStatesForTask(task, request: request)
     }
 
     func didReceiveResumeData(_ data: Data, for request: DownloadRequest) {
@@ -465,18 +465,25 @@ open class Session {
         requestTaskMap[request] = task
         request.didCreateTask(task)
 
-        resumeOrSuspendTask(task, ifNecessaryForRequest: request)
+        updateStatesForTask(task, request: request)
     }
 
-    func resumeOrSuspendTask(_ task: URLSessionTask, ifNecessaryForRequest request: Request) {
-        if startRequestsImmediately || request.isResumed {
+    func updateStatesForTask(_ task: URLSessionTask, request: Request) {
+        switch (startRequestsImmediately, request.state) {
+        case (true, .initialized):
+            request.resume()
+        case (false, .initialized):
+            // Do nothing.
+            break
+        case (_, .resumed):
             task.resume()
-            request.didResume()
-        }
-
-        if request.isSuspended {
+            request.didResumeTask(task)
+        case (_, .suspended):
             task.suspend()
-            request.didSuspend()
+            request.didSuspendTask(task)
+        case (_, .cancelled):
+            task.cancel()
+            request.didCancelTask(task)
         }
     }
 
@@ -547,21 +554,23 @@ extension Session: RequestDelegate {
 
     public func cancelRequest(_ request: Request) {
         rootQueue.async {
+            request.didCancel()
+
             guard let task = self.requestTaskMap[request] else {
-                request.didCancel()
                 request.finish()
                 return
             }
 
             task.cancel()
-            request.didCancel()
+            request.didCancelTask(task)
         }
     }
 
     public func cancelDownloadRequest(_ request: DownloadRequest, byProducingResumeData: @escaping (Data?) -> Void) {
         rootQueue.async {
+            request.didCancel()
+
             guard let downloadTask = self.requestTaskMap[request] as? URLSessionDownloadTask else {
-                request.didCancel()
                 request.finish()
                 return
             }
@@ -569,7 +578,7 @@ extension Session: RequestDelegate {
             downloadTask.cancel { (data) in
                 self.rootQueue.async {
                     byProducingResumeData(data)
-                    request.didCancel()
+                    request.didCancelTask(downloadTask)
                 }
             }
         }
@@ -577,19 +586,27 @@ extension Session: RequestDelegate {
 
     public func suspendRequest(_ request: Request) {
         rootQueue.async {
-            guard !request.isCancelled, let task = self.requestTaskMap[request] else { return }
+            guard !request.isCancelled else { return }
 
-            task.suspend()
             request.didSuspend()
+
+            guard let task = self.requestTaskMap[request] else { return }
+
+            task.suspend()
+            request.didSuspendTask(task)
         }
     }
 
     public func resumeRequest(_ request: Request) {
         rootQueue.async {
-            guard !request.isCancelled, let task = self.requestTaskMap[request] else { return }
+            guard !request.isCancelled else { return }
 
-            task.resume()
             request.didResume()
+
+            guard let task = self.requestTaskMap[request] else { return }
+
+            task.resume()
+            request.didResumeTask(task)
         }
     }
 }

+ 12 - 0
Tests/NSLoggingEventMonitor.swift

@@ -140,14 +140,26 @@ public final class NSLoggingEventMonitor: EventMonitor {
         NSLog("Request: \(request) didResume")
     }
 
+    public func request(_ request: Request, didResumeTask task: URLSessionTask) {
+        NSLog("Request: \(request) didResumeTask: \(task)")
+    }
+
     public func requestDidSuspend(_ request: Request) {
         NSLog("Request: \(request) didSuspend")
     }
 
+    public func request(_ request: Request, didSuspendTask task: URLSessionTask) {
+        NSLog("Request: \(request) didSuspendTask: \(task)")
+    }
+
     public func requestDidCancel(_ request: Request) {
         NSLog("Request: \(request) didCancel")
     }
 
+    public func request(_ request: Request, didCancelTask task: URLSessionTask) {
+        NSLog("Request: \(request) didCancelTask: \(task)")
+    }
+
     public func request(_ request: DataRequest, didParseResponse response: DataResponse<Data?>) {
         NSLog("Request: \(request), didParseResponse: \(response)")
     }

+ 262 - 0
Tests/RequestTests.swift

@@ -262,6 +262,268 @@ class RequestResponseTestCase: BaseTestCase {
         // Then
         XCTAssertEqual(receivedResponse?.result.value?.form, ["property": "one"])
     }
+
+    // MARK: - Lifetime Events
+
+    func testThatAutomaticallyResumedRequestReceivesAppropriateLifetimeEvents() {
+        // Given
+        let eventMonitor = ClosureEventMonitor()
+        let session = Session(eventMonitors: [eventMonitor])
+
+        let expect = expectation(description: "request should receive appropriate lifetime events")
+        expect.expectedFulfillmentCount = 3
+
+        eventMonitor.requestDidResumeTask = { (_, _) in expect.fulfill() }
+        eventMonitor.requestDidResume = { _ in expect.fulfill() }
+        eventMonitor.requestDidFinish = { _ in expect.fulfill() }
+        // Fulfill other events that would exceed the expected count. Inverted expectations require the full timeout.
+        eventMonitor.requestDidSuspend = { _ in expect.fulfill() }
+        eventMonitor.requestDidSuspendTask = { (_, _) in expect.fulfill() }
+        eventMonitor.requestDidCancel = { _ in expect.fulfill() }
+        eventMonitor.requestDidCancelTask = { (_, _) in expect.fulfill() }
+
+        // When
+        let request = session.request(URLRequest.makeHTTPBinRequest())
+
+        waitForExpectations(timeout: timeout, handler: nil)
+
+        // Then
+        XCTAssertEqual(request.state, .resumed)
+    }
+
+    func testThatAutomaticallyAndManuallyResumedRequestReceivesAppropriateLifetimeEvents() {
+        // Given
+        let eventMonitor = ClosureEventMonitor()
+        let session = Session(eventMonitors: [eventMonitor])
+
+        let expect = expectation(description: "request should receive appropriate lifetime events")
+        expect.expectedFulfillmentCount = 3
+
+        eventMonitor.requestDidResumeTask = { (_, _) in expect.fulfill() }
+        eventMonitor.requestDidResume = { _ in expect.fulfill() }
+        eventMonitor.requestDidFinish = { _ in expect.fulfill() }
+        // Fulfill other events that would exceed the expected count. Inverted expectations require the full timeout.
+        eventMonitor.requestDidSuspend = { _ in expect.fulfill() }
+        eventMonitor.requestDidSuspendTask = { (_, _) in expect.fulfill() }
+        eventMonitor.requestDidCancel = { _ in expect.fulfill() }
+        eventMonitor.requestDidCancelTask = { (_, _) in expect.fulfill() }
+
+        // When
+        let request = session.request(URLRequest.makeHTTPBinRequest())
+        for _ in 0..<100 {
+            request.resume()
+        }
+
+        waitForExpectations(timeout: timeout, handler: nil)
+
+        // Then
+        XCTAssertEqual(request.state, .resumed)
+    }
+
+    func testThatManuallyResumedRequestReceivesAppropriateLifetimeEvents() {
+        // Given
+        let eventMonitor = ClosureEventMonitor()
+        let session = Session(startRequestsImmediately: false, eventMonitors: [eventMonitor])
+
+        let expect = expectation(description: "request should receive appropriate lifetime events")
+        expect.expectedFulfillmentCount = 3
+
+        eventMonitor.requestDidResumeTask = { (_, _) in expect.fulfill() }
+        eventMonitor.requestDidResume = { _ in expect.fulfill() }
+        eventMonitor.requestDidFinish = { _ in expect.fulfill() }
+        // Fulfill other events that would exceed the expected count. Inverted expectations require the full timeout.
+        eventMonitor.requestDidSuspend = { _ in expect.fulfill() }
+        eventMonitor.requestDidSuspendTask = { (_, _) in expect.fulfill() }
+        eventMonitor.requestDidCancel = { _ in expect.fulfill() }
+        eventMonitor.requestDidCancelTask = { (_, _) in expect.fulfill() }
+
+        // When
+        let request = session.request(URLRequest.makeHTTPBinRequest())
+        for _ in 0..<100 {
+            request.resume()
+        }
+
+        waitForExpectations(timeout: timeout, handler: nil)
+
+        // Then
+        XCTAssertEqual(request.state, .resumed)
+    }
+
+    func testThatRequestManuallyResumedManyTimesOnlyReceivesAppropriateLifetimeEvents() {
+        // Given
+        let eventMonitor = ClosureEventMonitor()
+        let session = Session(startRequestsImmediately: false, eventMonitors: [eventMonitor])
+
+        let expect = expectation(description: "request should receive appropriate lifetime events")
+        expect.expectedFulfillmentCount = 3
+
+        eventMonitor.requestDidResumeTask = { (_, _) in expect.fulfill() }
+        eventMonitor.requestDidResume = { _ in expect.fulfill() }
+        eventMonitor.requestDidFinish = { _ in expect.fulfill() }
+        // Fulfill other events that would exceed the expected count. Inverted expectations require the full timeout.
+        eventMonitor.requestDidSuspend = { _ in expect.fulfill() }
+        eventMonitor.requestDidSuspendTask = { (_, _) in expect.fulfill() }
+        eventMonitor.requestDidCancel = { _ in expect.fulfill() }
+        eventMonitor.requestDidCancelTask = { (_, _) in expect.fulfill() }
+
+        // When
+        let request = session.request(URLRequest.makeHTTPBinRequest())
+        for _ in 0..<100 {
+            request.resume()
+        }
+
+        waitForExpectations(timeout: timeout, handler: nil)
+
+        // Then
+        XCTAssertEqual(request.state, .resumed)
+    }
+
+    func testThatRequestManuallySuspendedManyTimesAfterAutomaticResumeOnlyReceivesAppropriateLifetimeEvents() {
+        // Given
+        let eventMonitor = ClosureEventMonitor()
+        let session = Session(startRequestsImmediately: false, eventMonitors: [eventMonitor])
+
+        let expect = expectation(description: "request should receive appropriate lifetime events")
+        expect.expectedFulfillmentCount = 2
+
+        eventMonitor.requestDidSuspendTask = { (_, _) in expect.fulfill() }
+        eventMonitor.requestDidSuspend = { _ in expect.fulfill() }
+        // Fulfill other events that would exceed the expected count. Inverted expectations require the full timeout.
+        eventMonitor.requestDidCancel = { _ in expect.fulfill() }
+        eventMonitor.requestDidCancelTask = { (_, _) in expect.fulfill() }
+
+        // When
+        let request = session.request(URLRequest.makeHTTPBinRequest())
+        for _ in 0..<100 {
+            request.suspend()
+        }
+
+        waitForExpectations(timeout: timeout, handler: nil)
+
+        // Then
+        XCTAssertEqual(request.state, .suspended)
+    }
+
+    func testThatRequestManuallySuspendedManyTimesOnlyReceivesAppropriateLifetimeEvents() {
+        // Given
+        let eventMonitor = ClosureEventMonitor()
+        let session = Session(startRequestsImmediately: false, eventMonitors: [eventMonitor])
+
+        let expect = expectation(description: "request should receive appropriate lifetime events")
+        expect.expectedFulfillmentCount = 2
+
+        eventMonitor.requestDidSuspendTask = { (_, _) in expect.fulfill() }
+        eventMonitor.requestDidSuspend = { _ in expect.fulfill() }
+        // Fulfill other events that would exceed the expected count. Inverted expectations require the full timeout.
+        eventMonitor.requestDidResume = { _ in expect.fulfill() }
+        eventMonitor.requestDidResumeTask = { (_, _) in expect.fulfill() }
+        eventMonitor.requestDidCancel = { _ in expect.fulfill() }
+        eventMonitor.requestDidCancelTask = { (_, _) in expect.fulfill() }
+
+        // When
+        let request = session.request(URLRequest.makeHTTPBinRequest())
+        for _ in 0..<100 {
+            request.suspend()
+        }
+
+        waitForExpectations(timeout: timeout, handler: nil)
+
+        // Then
+        XCTAssertEqual(request.state, .suspended)
+    }
+
+    func testThatRequestManuallyCancelledManyTimesAfterAutomaticResumeOnlyReceivesAppropriateLifetimeEvents() {
+        // Given
+        let eventMonitor = ClosureEventMonitor()
+        let session = Session(eventMonitors: [eventMonitor])
+
+        let expect = expectation(description: "request should receive appropriate lifetime events")
+        expect.expectedFulfillmentCount = 2
+
+        eventMonitor.requestDidCancelTask = { (_, _) in expect.fulfill() }
+        eventMonitor.requestDidCancel = { _ in expect.fulfill() }
+        // Fulfill other events that would exceed the expected count. Inverted expectations require the full timeout.
+        eventMonitor.requestDidSuspend = { _ in expect.fulfill() }
+        eventMonitor.requestDidSuspendTask = { (_, _) in expect.fulfill() }
+
+        // When
+        let request = session.request(URLRequest.makeHTTPBinRequest())
+        // Cancellation stops task creation, so don't cancel the request until the task has been created.
+        eventMonitor.requestDidCreateTask = { (_, _) in
+            for _ in 0..<100 {
+                request.cancel()
+            }
+        }
+
+        waitForExpectations(timeout: timeout, handler: nil)
+
+        // Then
+        XCTAssertEqual(request.state, .cancelled)
+    }
+
+    func testThatRequestManuallyCancelledManyTimesOnlyReceivesAppropriateLifetimeEvents() {
+        // Given
+        let eventMonitor = ClosureEventMonitor()
+        let session = Session(startRequestsImmediately: false, eventMonitors: [eventMonitor])
+
+        let expect = expectation(description: "request should receive appropriate lifetime events")
+        expect.expectedFulfillmentCount = 2
+
+        eventMonitor.requestDidCancelTask = { (_, _) in expect.fulfill() }
+        eventMonitor.requestDidCancel = { _ in expect.fulfill() }
+        // Fulfill other events that would exceed the expected count. Inverted expectations require the full timeout.
+        eventMonitor.requestDidResume = { _ in expect.fulfill() }
+        eventMonitor.requestDidResumeTask = { (_, _) in expect.fulfill() }
+        eventMonitor.requestDidSuspend = { _ in expect.fulfill() }
+        eventMonitor.requestDidSuspendTask = { (_, _) in expect.fulfill() }
+
+        // When
+        let request = session.request(URLRequest.makeHTTPBinRequest())
+        // Cancellation stops task creation, so don't cancel the request until the task has been created.
+        eventMonitor.requestDidCreateTask = { (_, _) in
+            for _ in 0..<100 {
+                request.cancel()
+            }
+        }
+
+        waitForExpectations(timeout: timeout, handler: nil)
+
+        // Then
+        XCTAssertEqual(request.state, .cancelled)
+    }
+    
+    func testThatRequestManuallyCancelledManyTimesOnManyQueuesOnlyReceivesAppropriateLifetimeEvents() {
+        // Given
+        let eventMonitor = ClosureEventMonitor()
+        let session = Session(eventMonitors: [eventMonitor])
+        
+        let expect = expectation(description: "request should receive appropriate lifetime events")
+        expect.expectedFulfillmentCount = 5
+        
+        eventMonitor.requestDidCancelTask = { (_, _) in expect.fulfill() }
+        eventMonitor.requestDidCancel = { _ in expect.fulfill() }
+        eventMonitor.requestDidResume = { _ in expect.fulfill() }
+        eventMonitor.requestDidResumeTask = { (_, _) in expect.fulfill() }
+        // Fulfill other events that would exceed the expected count. Inverted expectations require the full timeout.
+        eventMonitor.requestDidSuspend = { _ in expect.fulfill() }
+        eventMonitor.requestDidSuspendTask = { (_, _) in expect.fulfill() }
+        
+        // When
+        let request = session.request(URLRequest.makeHTTPBinRequest())
+        // Cancellation stops task creation, so don't cancel the request until the task has been created.
+        eventMonitor.requestDidCreateTask = { (_, _) in
+            DispatchQueue.concurrentPerform(iterations: 100) { i in
+                request.cancel()
+                
+                if i == 99 { expect.fulfill() }
+            }
+        }
+        
+        waitForExpectations(timeout: timeout, handler: nil)
+        
+        // Then
+        XCTAssertEqual(request.state, .cancelled)
+    }
 }
 
 // MARK: -

+ 22 - 0
Tests/SessionTests.swift

@@ -1472,6 +1472,28 @@ class SessionTestCase: BaseTestCase {
         XCTAssertTrue(session.requestTaskMap.isEmpty)
         XCTAssertEqual(completionCallCount, 1)
     }
+
+    // MARK: Tests - Request State
+
+    func testThatSessionSetsRequestStateWhenStartRequestsImmediatelyIsTrue() {
+        // Given
+        let session = Session()
+
+        let expectation = self.expectation(description: "request should complete")
+        var response: DataResponse<Any>?
+
+        // When
+        let request = session.request("https://httpbin.org/get").responseJSON { resp in
+            response = resp
+            expectation.fulfill()
+        }
+
+        waitForExpectations(timeout: timeout, handler: nil)
+
+        // Then
+        XCTAssertEqual(request.state, .resumed)
+        XCTAssertEqual(response?.result.isSuccess, true)
+    }
 }
 
 // MARK: -