@@ -3,36 +3,154 @@ import XCTest
33// We need to know whether Apple changes the NSURLSessionTask implementation.
44class SentryNSURLSessionSwizzleTargetDiscoveryTests : XCTestCase {
55
6- func test_URLSessionTask_ByIosVersion( ) {
6+ func test_URLSessionTask_ByIosVersion( ) {
77 let classes = SentryNSURLSessionSwizzleTargetDiscovery . urlSessionTaskClassesToTrack ( )
8-
8+
99 XCTAssertEqual ( classes. count, 1 )
1010 XCTAssertTrue ( classes. first === URLSessionTask . self)
1111 }
12-
13- func test_URLSessionCompletionHandler_DataTaskWithRequest( ) {
14- let selector = #selector( URLSession . dataTask ( with: completionHandler: ) as ( URLSession ) -> ( URLRequest , @escaping ( Data ? , URLResponse ? , Error ? ) -> Void ) -> URLSessionDataTask )
15- assertCompletionHandlerDiscovery ( selector: selector, selectorName: " dataTaskWithRequest:completionHandler: " )
12+
13+ // MARK: - NSURLSession class hierarchy validation tests
14+ //
15+ // Based on testing, NSURLSession implements dataTaskWithRequest:completionHandler:
16+ // and dataTaskWithURL:completionHandler: directly on the base class.
17+ //
18+ // The swizzling code relies on this by swizzling [NSURLSession class] directly
19+ // rather than doing runtime discovery. These tests verify that assumption
20+ // still holds — if Apple ever moves these methods to a subclass, these tests
21+ // will fail and we'll know to update the swizzling approach.
22+
23+ func test_URLSession_isNotClassCluster_dataTaskWithRequest( ) {
24+ let selector = #selector( URLSession . dataTask ( with: completionHandler: )
25+ as ( URLSession ) -> ( URLRequest , @escaping @Sendable ( Data ? , URLResponse ? , Error ? ) -> Void ) -> URLSessionDataTask )
26+ assertNSURLSessionImplementsDirectly ( selector: selector, selectorName: " dataTaskWithRequest:completionHandler: " )
1627 }
17-
18- func test_URLSessionCompletionHandler_DataTaskWithURL( ) {
19- let selector = #selector( URLSession . dataTask ( with: completionHandler: ) as ( URLSession ) -> ( URL , @escaping ( Data ? , URLResponse ? , Error ? ) -> Void ) -> URLSessionDataTask )
20- assertCompletionHandlerDiscovery ( selector: selector, selectorName: " dataTaskWithURL:completionHandler: " )
28+
29+ func test_URLSession_isNotClassCluster_dataTaskWithURL( ) {
30+ let selector = #selector( URLSession . dataTask ( with: completionHandler: )
31+ as ( URLSession ) -> ( URL , @escaping @Sendable ( Data ? , URLResponse ? , Error ? ) -> Void ) -> URLSessionDataTask )
32+ assertNSURLSessionImplementsDirectly ( selector: selector, selectorName: " dataTaskWithURL:completionHandler: " )
33+ }
34+
35+ // MARK: - dataTaskWithURL: / dataTaskWithRequest: independence
36+ //
37+ // We swizzle both dataTaskWithRequest:completionHandler: and
38+ // dataTaskWithURL:completionHandler: because they are independent
39+ // implementations — dataTaskWithURL: does NOT dispatch to
40+ // dataTaskWithRequest: via objc_msgSend.
41+ //
42+ // If this test ever fails, Apple has changed the internal dispatch so
43+ // one calls through to the other. In that case, remove the redundant
44+ // swizzle and add a deduplication guard to avoid double-capture.
45+
46+ func test_dataTaskWithURL_doesNotCallThrough_dataTaskWithRequest( ) {
47+ assertNoCallThrough (
48+ from: NSSelectorFromString ( " dataTaskWithURL:completionHandler: " ) ,
49+ to: NSSelectorFromString ( " dataTaskWithRequest:completionHandler: " ) ,
50+ call: { session in
51+ let url = URL ( string: " https://example.com " ) !
52+ let task = session. dataTask ( with: url) { _, _, _ in }
53+ task. cancel ( )
54+ }
55+ )
2156 }
22-
23- // MARK: - Helper Methods
24-
25- private func assertCompletionHandlerDiscovery( selector: Selector , selectorName: String ) {
26- let classes = SentryNSURLSessionSwizzleTargetDiscovery . urlSessionCompletionHandlerClasses ( toSwizzle: selector)
27-
28- // At the time of writing, internal session subclasses don't override these data task factory methods
29- // If we ever see more than 1 class that isn't URLSession itself - flag it to be checked out.
30- XCTAssertEqual ( classes. count, 1 , " Should have exactly URLSession to swizzle for \( selectorName) " )
31- XCTAssertTrue ( classes. first === URLSession . self, " Should be URLSession class for \( selectorName) " )
32-
33- // Double-check the method that we need actually exists.
34- let hasMethod = class_getInstanceMethod ( classes. first!, selector) != nil
35- XCTAssertTrue ( hasMethod, " URLSession should have \( selectorName) " )
57+
58+ func test_dataTaskWithRequest_doesNotCallThrough_dataTaskWithURL( ) {
59+ assertNoCallThrough (
60+ from: NSSelectorFromString ( " dataTaskWithRequest:completionHandler: " ) ,
61+ to: NSSelectorFromString ( " dataTaskWithURL:completionHandler: " ) ,
62+ call: { session in
63+ let request = URLRequest ( url: URL ( string: " https://example.com " ) !)
64+ let task = session. dataTask ( with: request) { _, _, _ in }
65+ task. cancel ( )
66+ }
67+ )
68+ }
69+
70+ /// Temporarily replaces the IMP of `targetSelector` with one that increments
71+ /// a counter, then invokes `call` (which should trigger `sourceSelector`).
72+ /// Asserts the counter stays at 0 — meaning `sourceSelector` does not
73+ /// internally dispatch to `targetSelector` via objc_msgSend.
74+ private func assertNoCallThrough(
75+ from sourceSelector: Selector ,
76+ to targetSelector: Selector ,
77+ call: ( URLSession ) -> Void
78+ ) {
79+ guard let method = class_getInstanceMethod ( URLSession . self, targetSelector) else {
80+ XCTFail ( " URLSession should implement \( targetSelector) " )
81+ return
82+ }
83+
84+ let originalIMP = method_getImplementation ( method)
85+ defer { method_setImplementation ( method, originalIMP) }
86+
87+ var hitCount = 0
88+
89+ let replacementBlock : @convention ( block) ( NSObject , AnyObject , Any ? ) -> AnyObject = { obj, arg, handler in
90+ hitCount += 1
91+ typealias Fn = @convention ( c) ( NSObject , Selector , AnyObject , Any ? ) -> AnyObject
92+ let original = unsafeBitCast ( originalIMP, to: Fn . self)
93+ return original ( obj, targetSelector, arg, handler)
94+ }
95+
96+ method_setImplementation ( method, imp_implementationWithBlock ( replacementBlock) )
97+
98+ let session = URLSession ( configuration: . ephemeral)
99+ defer { session. invalidateAndCancel ( ) }
100+
101+ call ( session)
102+
103+ XCTAssertEqual (
104+ hitCount, 0 ,
105+ " \( sourceSelector) called through to \( targetSelector) . "
106+ + " These methods are no longer independent — remove the redundant swizzle "
107+ + " in SentrySwizzleWrapperHelper and add a deduplication guard. "
108+ )
36109 }
37110
111+ // MARK: - Helper
112+
113+ /// Walks the class hierarchy for sessions created with default and ephemeral
114+ /// configurations and asserts that no subclass overrides `selector`.
115+ private func assertNSURLSessionImplementsDirectly( selector: Selector , selectorName: String ) {
116+ let baseClass : AnyClass = URLSession . self
117+
118+ // The base class must implement the method.
119+ XCTAssertNotNil (
120+ class_getInstanceMethod ( baseClass, selector) ,
121+ " URLSession should implement \( selectorName) "
122+ )
123+
124+ // Check sessions created with each relevant configuration.
125+ let configs : [ URLSessionConfiguration ] = [
126+ . default,
127+ . ephemeral
128+ ]
129+
130+ for config in configs {
131+ let session = URLSession ( configuration: config)
132+ let sessionClass : AnyClass = type ( of: session)
133+
134+ defer { session. invalidateAndCancel ( ) }
135+
136+ if sessionClass === baseClass {
137+ continue
138+ }
139+
140+ // If Apple returns a subclass, it must NOT provide its own
141+ // implementation — it should inherit from URLSession.
142+ let subMethod = class_getInstanceMethod ( sessionClass, selector)
143+ let baseMethod = class_getInstanceMethod ( baseClass, selector)
144+
145+ if let subMethod, let baseMethod {
146+ let subIMP = method_getImplementation ( subMethod)
147+ let baseIMP = method_getImplementation ( baseMethod)
148+ XCTAssertEqual (
149+ subIMP, baseIMP,
150+ " \( NSStringFromClass ( sessionClass) ) overrides \( selectorName) with an unexpected IMP — "
151+ + " Verify swizzling in SentrySwizzleWrapperHelper is correct for dataTasks. "
152+ )
153+ }
154+ }
155+ }
38156}
0 commit comments