Skip to content

Commit 5322a46

Browse files
Andrew Kitchenakitchen
Andrew Kitchen
authored andcommitted
Allow simulation of slow image loading by "pausing" the URL protocol
This ensures we are synchronously simulating the callbacks queueing up, but without relying on coincidental async timing - one less flaky test. Failure localization of runUntil() is also improved.
1 parent 0bad997 commit 5322a46

File tree

3 files changed

+47
-15
lines changed

3 files changed

+47
-15
lines changed

MapboxNavigationTests/ImageDownloaderTests.swift

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ class ImageDownloaderTests: XCTestCase {
6161
XCTAssertNil(errorReturned)
6262
}
6363

64-
func testDownloadingSameImageWhileInProgressAddsCallbacksWithoutAddingAnotherRequest() {
64+
func testDownloadingImageWhileAlreadyInProgressAddsCallbacksWithoutAddingAnotherRequest() {
6565
guard let downloader = downloader else {
6666
XCTFail()
6767
return
@@ -70,6 +70,9 @@ class ImageDownloaderTests: XCTestCase {
7070
var secondCallbackCalled = false
7171
var operation: ImageDownload
7272

73+
// URL loading is delayed in order to simulate conditions under which multiple requests for the same asset would be made
74+
ImageLoadingURLProtocolSpy.delayImageLoading()
75+
7376
downloader.downloadImage(with: imageURL) { (image, data, error) in
7477
firstCallbackCalled = true
7578
}
@@ -79,14 +82,17 @@ class ImageDownloaderTests: XCTestCase {
7982
secondCallbackCalled = true
8083
}
8184

85+
ImageLoadingURLProtocolSpy.resumeImageLoading()
86+
8287
XCTAssertTrue(operation === downloader.activeOperation(with: imageURL)!,
8388
"Expected \(String(describing: operation)) to be identical to \(String(describing: downloader.activeOperation(with: imageURL)))")
8489

8590
var spinCount = 0
86-
runUntil(condition: {
91+
92+
runUntil({
8793
spinCount += 1
8894
return operation.isFinished
89-
}, pollingInterval: 0.1, until: XCTestCase.NavigationTests.timeout)
95+
})
9096

9197
print("Succeeded after evaluating condition \(spinCount) times.")
9298

@@ -107,10 +113,10 @@ class ImageDownloaderTests: XCTestCase {
107113
}
108114
var operation = downloader.activeOperation(with: imageURL)!
109115

110-
runUntil(condition: {
116+
runUntil({
111117
spinCount += 1
112118
return operation.isFinished
113-
}, pollingInterval: 0.1, until: XCTestCase.NavigationTests.timeout)
119+
})
114120

115121
print("Succeeded after evaluating first condition \(spinCount) times.")
116122
XCTAssertTrue(callbackCalled)
@@ -123,24 +129,28 @@ class ImageDownloaderTests: XCTestCase {
123129
}
124130
operation = downloader.activeOperation(with: imageURL)!
125131

126-
runUntil(condition: {
132+
runUntil({
127133
spinCount += 1
128134
return operation.isFinished
129-
}, pollingInterval: 0.1, until: XCTestCase.NavigationTests.timeout)
135+
})
130136

131137
print("Succeeded after evaluating second condition \(spinCount) times.")
132138
XCTAssertTrue(callbackCalled)
133139
}
134140

135-
private func runUntil(condition: () -> Bool, pollingInterval: TimeInterval, until timeout: DispatchTime) {
141+
private func runUntil(_ condition: () -> Bool, testCase: String = #function) {
142+
runUntil(condition: condition, testCase: testCase, pollingInterval: NavigationTests.pollingInterval, until: NavigationTests.timeout)
143+
}
144+
145+
private func runUntil(condition: () -> Bool, testCase: String, pollingInterval: TimeInterval, until timeout: DispatchTime) {
136146
guard (timeout >= DispatchTime.now()) else {
137-
XCTFail("Timeout occurred on \(#function)")
147+
XCTFail("Timeout occurred in \(testCase)")
138148
return
139149
}
140150

141151
if condition() == false {
142152
RunLoop.current.run(until: Date(timeIntervalSinceNow: pollingInterval))
143-
runUntil(condition: condition, pollingInterval: pollingInterval, until: timeout)
153+
runUntil(condition: condition, testCase: testCase, pollingInterval: pollingInterval, until: timeout)
144154
}
145155
}
146156
}

MapboxNavigationTests/Support/ImageLoadingURLProtocolSpy.swift

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ class ImageLoadingURLProtocolSpy: URLProtocol {
99
private static var responseData: [URL: Data] = [:]
1010
private static var activeRequests: [URL: URLRequest] = [:]
1111
private static var pastRequests: [URL: URLRequest] = [:]
12+
private static let imageLoadingSemaphore = DispatchSemaphore(value: 1)
1213

13-
private var stopped: Bool = false
14+
private var loadingStopped: Bool = false
1415

1516
override class func canInit(with request: URLRequest) -> Bool {
1617
return responseData.keys.contains(request.url!)
@@ -37,7 +38,6 @@ class ImageLoadingURLProtocolSpy: URLProtocol {
3738
return
3839
}
3940

40-
// retrieve fake response (image) for request; ensure it is an image
4141
guard let data = ImageLoadingURLProtocolSpy.responseData[url], let image: UIImage = UIImage(data: data), let client = client else {
4242
XCTFail("No valid image data found for url: \(url)")
4343
return
@@ -49,25 +49,30 @@ class ImageLoadingURLProtocolSpy: URLProtocol {
4949
ImageLoadingURLProtocolSpy.activeRequests[url] = nil
5050
}
5151

52-
XCTAssertFalse(self.stopped, "URL Loading was previously stopped")
53-
52+
XCTAssertFalse(self.loadingStopped, "URL Loading was previously stopped")
53+
5454
// We only want there to be one active request per resource at any given time (with callbacks appended if requested multiple times)
5555
XCTAssertFalse(ImageLoadingURLProtocolSpy.hasActiveRequestForURL(url), "There should only be one request in flight at a time per resource")
5656
ImageLoadingURLProtocolSpy.activeRequests[url] = request
5757

5858
// send an NSHTTPURLResponse to the client
5959
let response = HTTPURLResponse.init(url: url, statusCode: 200, httpVersion: "1.1", headerFields: nil)
6060
client.urlProtocol(self, didReceive: response!, cacheStoragePolicy: .notAllowed)
61+
62+
ImageLoadingURLProtocolSpy.imageLoadingSemaphore.wait()
63+
6164
client.urlProtocol(self, didLoad: UIImagePNGRepresentation(image)!)
6265
client.urlProtocolDidFinishLoading(self)
66+
67+
ImageLoadingURLProtocolSpy.imageLoadingSemaphore.signal()
6368
}
6469

6570
let defaultQueue = DispatchQueue.global(qos: .default)
6671
defaultQueue.async(execute: urlLoadingBlock)
6772
}
6873

6974
override func stopLoading() {
70-
stopped = true
75+
loadingStopped = true
7176
}
7277

7378
/**
@@ -99,4 +104,19 @@ class ImageLoadingURLProtocolSpy: URLProtocol {
99104
class func pastRequestForURL(_ url: URL) -> URLRequest? {
100105
return pastRequests[url]
101106
}
107+
108+
/**
109+
* Pauses image loading once a request receives a response. Useful for testing re-entrant resource requests.
110+
*/
111+
class func delayImageLoading() {
112+
ImageLoadingURLProtocolSpy.imageLoadingSemaphore.wait()
113+
}
114+
115+
/**
116+
* Resumes image loading which was previously delayed due to `delayImageLoading()` having been called.
117+
*/
118+
class func resumeImageLoading() {
119+
ImageLoadingURLProtocolSpy.imageLoadingSemaphore.signal()
120+
}
121+
102122
}

MapboxNavigationTests/XCTestCase.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,7 @@ extension XCTestCase {
66
static var timeout: DispatchTime {
77
return DispatchTime.now() + DispatchTimeInterval.seconds(10)
88
}
9+
10+
static let pollingInterval: TimeInterval = 0.05
911
}
1012
}

0 commit comments

Comments
 (0)