onevcat 10 лет назад
Родитель
Сommit
9965081fda

+ 202 - 87
Sources/ImagePrefetcher.swift

@@ -32,121 +32,236 @@
 #endif
 
 
-/// Progress update block of prefetcher.
-public typealias PrefetchProgressBlock = ((completedURLs: Int, allURLs: Int) -> ())
+/// Progress update block of prefetcher. 
+///
+/// - `skippedResources`: An array of resources that are already cached before the prefetching starting.
+/// - `failedResources`: An array of resources that fail to be downloaded. It could because of being cancelled while downloading, encountered an error when downloading or the download not being started at all.
+/// - `completedResources`: An array of resources that are downloaded and cached successfully.
+public typealias PrefetcherProgressBlock = ((skippedResources: [Resource], failedResources: [Resource], completedResources: [Resource]) -> ())
 
 /// Completion block of prefetcher.
-public typealias PrefetchCompletionBlock = ((cancelled: Bool, completedURLs: Int, skippedURLs: Int) -> ())
+///
+/// - `skippedResources`: An array of resources that are already cached before the prefetching starting.
+/// - `failedResources`: An array of resources that fail to be downloaded. It could because of being cancelled while downloading, encountered an error when downloading or the download not being started at all.
+/// - `completedResources`: An array of resources that are downloaded and cached successfully.
+public typealias PrefetcherCompletionHandler = ((skippedResources: [Resource], failedResources: [Resource], completedResources: [Resource]) -> ())
 
-private let defaultPrefetcherInstance = ImagePrefetcher()
-
-/// `ImagePrefetcher` represents a downloading manager for requesting many images via URLs and caching them.
-public class ImagePrefetcher: NSObject {
+/// `ImagePrefetcher` represents a downloading manager for requesting many images via URLs, then caching them.
+/// This is useful when you know a list of image resources and want to download them before showing.
+public class ImagePrefetcher {
+    
+    /// The maximum concurrent downloads to use when prefetching images. Default is 5.
+    public var maxConcurrentDownloads = 5
+    
+    private let prefetchResources: [Resource]
+    private let optionsInfo: KingfisherOptionsInfo
+    private var progressBlock: PrefetcherProgressBlock?
+    private var completionHandler: PrefetcherCompletionHandler?
+    
+    private var tasks = [NSURL: RetrieveImageDownloadTask]()
+    
+    private var skippedResources = [Resource]()
+    private var completedResources = [Resource]()
+    private var failedResources = [Resource]()
     
-    private var prefetchURLs: [NSURL]?
-    private var skippedCount = 0
     private var requestedCount = 0
-    private var finishedCount = 0
+    private var cancelled = false
     
-    private var cancelCompletionHandlerCalled = false
+    // The created manager used for prefetch. We will use the helper method in manager.
+    private let manager: KingfisherManager
     
-    /// The default manager to use for downloads.
-    public lazy var manager: KingfisherManager = KingfisherManager.sharedManager
-
-    /// The default prefetcher.
-    public class var defaultPrefetcher: ImagePrefetcher {
-        return defaultPrefetcherInstance
+    private var finished: Bool {
+        return failedResources.count + skippedResources.count + completedResources.count == prefetchResources.count
     }
-
-    /// The maximum concurrent downloads to use when prefetching images. Default is 5.
-    public var maxConcurrentDownloads = 5
     
     /**
-     Download the images from `urls` and cache them. This can be useful for background downloading
-     of assets that are required for later use in an app. This code will not try and update any UI
-     with the results of the process, but calls the handlers with the number cached etc. Failed
-     images are just skipped.
+     Init an image prefetcher with an array of URLs.
      
-     Warning: This will cancel any existing prefetch operation in progress! Use `isPrefetching() to
-     control this in your own code as you see fit.
+     The prefetcher should be initiated with a list of prefetching targets. The URLs list is immutable. 
+     After you get a valid `ImagePrefetcher` object, you could call `start()` on it to begin the prefetching process.
+     The images already cached will be skipped without downloading again.
      
-     - parameter urls:              The list of URLs to prefetch
-     - parameter progressBlock:     Block to be called when progress updates. Completed and total
-                                    counts are provided. Completed does not imply success.
-     - parameter completionHandler: Block to be called when prefetching is complete. Completed is all
-                                    those made, and skipped is the number of failed ones.
+     - parameter urls:              The URLs which should be prefetched.
+     - parameter optionsInfo:       A dictionary could control some behaviors. See `KingfisherOptionsInfo` for more.
+     - parameter progressBlock:     Called every time an resource is downloaded, skipped or cancelled.
+     - parameter completionHandler: Called when the whole prefetching process finished.
+     
+     - returns: An `ImagePrefetcher` object.
+     
+     - Note: By default, the `ImageDownloader.defaultDownloader` and `ImageCache.defaultCache` will be used as 
+     the downloader and cache target respectively. You can specify another downloader or cache by using a customized `KingfisherOptionsInfo`.
+     Both the progress and completion block will be invoked in main thread. The `CallbackDispatchQueue` in `optionsInfo` will be ignored in this method.
      */
-    public func prefetchURLs(urls: [NSURL], progressBlock: PrefetchProgressBlock?, completionHandler: PrefetchCompletionBlock?) {
-        
-        // Clear out any existing prefetch operation first
-        cancelPrefetching()
-        
-        cancelCompletionHandlerCalled = false
+    public convenience init(urls: [NSURL],
+                     optionsInfo: KingfisherOptionsInfo? = nil,
+                   progressBlock: PrefetcherProgressBlock? = nil,
+               completionHandler: PrefetcherCompletionHandler? = nil)
+    {
+        let resources = urls.map { Resource(downloadURL: $0) }
+        self.init(resources: resources, optionsInfo: optionsInfo, progressBlock: progressBlock, completionHandler: completionHandler)
+    }
+    
+    /**
+     Init an image prefetcher with an array of resources.
+     
+     The prefetcher should be initiated with a list of prefetching targets. The resources list is immutable.
+     After you get a valid `ImagePrefetcher` object, you could call `start()` on it to begin the prefetching process.
+     The images already cached will be skipped without downloading again.
+     
+     - parameter resources:         The resources which should be prefetched. See `Resource` type for more.
+     - parameter optionsInfo:       A dictionary could control some behaviors. See `KingfisherOptionsInfo` for more.
+     - parameter progressBlock:     Called every time an resource is downloaded, skipped or cancelled.
+     - parameter completionHandler: Called when the whole prefetching process finished.
+     
+     - returns: An `ImagePrefetcher` object.
+     
+     - Note: By default, the `ImageDownloader.defaultDownloader` and `ImageCache.defaultCache` will be used as
+     the downloader and cache target respectively. You can specify another downloader or cache by using a customized `KingfisherOptionsInfo`.
+     Both the progress and completion block will be invoked in main thread. The `CallbackDispatchQueue` in `optionsInfo` will be ignored in this method.
+     */
+    public init(resources: [Resource],
+              optionsInfo: KingfisherOptionsInfo? = nil,
+            progressBlock: PrefetcherProgressBlock? = nil,
+        completionHandler: PrefetcherCompletionHandler? = nil)
+    {
+        prefetchResources = resources
         
-        prefetchURLs = urls
+        // We want all callbacks from main queue, so we ignore the call back queue in options
+        let optionsInfoWithoutQueue = optionsInfo?.kf_removeAllMatchesIgnoringAssociatedValue(.CallbackDispatchQueue(nil))
+        self.optionsInfo = optionsInfoWithoutQueue ?? KingfisherEmptyOptionsInfo
         
-        guard urls.count > 0 else {
-            CompletionHandler?()
-            return
-        }
+        let cache = self.optionsInfo.targetCache ?? ImageCache.defaultCache
+        let downloader = self.optionsInfo.downloader ?? ImageDownloader.defaultDownloader
+        manager = KingfisherManager(downloader: downloader, cache: cache)
         
-        for i in (0..<urls.count) where i < maxConcurrentDownloads && requestedCount < urls.count {
-            startPrefetching(i, progressBlock: progressBlock, completionHandler: completionHandler)
-        }
+        self.progressBlock = progressBlock
+        self.completionHandler = completionHandler
     }
-   
+    
     /**
-     This cancels any existing prefetching activity that might be occuring. It does not stop any currently
-     running cache operation, but prevents any further ones being started and terminates the looping. For
-     surety, be sure that the completion block on the prefetch is called after calling this if you expect
-     an operation to be running.
+     Start to download the resources and cache them. This can be useful for background downloading
+     of assets that are required for later use in an app. This code will not try and update any UI
+     with the results of the process.
      */
-    func cancelPrefetching() {
-        prefetchURLs = .None
-        skippedCount = 0
-        requestedCount = 0
-        finishedCount = 0
+    public func start()
+    {
+        // Since we want to handle the resources cancellation in main thread only.
+        dispatch_async_safely_to_main_queue { () -> () in
+            
+            guard !self.cancelled else {
+                assertionFailure("You can not restart the same prefetcher. Try to create a new prefetcher.")
+                self.handleComplete()
+                return
+            }
+            
+            guard self.maxConcurrentDownloads > 0 else {
+                assertionFailure("There should be concurrent downloads value should be at least 1.")
+                self.handleComplete()
+                return
+            }
+            
+            guard self.prefetchResources.count > 0 else {
+                self.handleComplete()
+                return
+            }
+            
+            let initialConcurentDownloads = min(self.prefetchResources.count, self.maxConcurrentDownloads)
+            for i in 0 ..< initialConcurentDownloads {
+                self.startPrefetchingResource(self.prefetchResources[i])
+            }
+        }
     }
 
+   
     /**
-     Checks to see if this prefetcher is already prefetching any images.
-     
-     - returns: True if there are images still to be prefetched, false otherwise.
+     Stop current downloading progress, and cancel any future prefetching activity that might be occuring.
      */
-    func isPrefetching() -> Bool {
-        guard let urls = prefetchURLs else { return false }
-        return urls.count > 0
+    public func cancel() {
+        dispatch_async_safely_to_main_queue {
+            self.cancelled = true
+            self.tasks.forEach { (_, task) -> () in
+                task.cancel()
+            }
+        }
     }
     
-    internal func startPrefetching(index: Int, progressBlock: PrefetchProgressBlock?, completionHandler: PrefetchCompletionBlock?) {
-        guard let urls = prefetchURLs where index < (urls.count ?? 0) else { return }
-        
-        requestedCount++
-        
+    func downloadAndCacheResource(resource: Resource) {
+
         let task = RetrieveImageTask()
-        let resource = Resource(downloadURL: urls[index])
-        let total = urls.count
+        let downloadTask = manager.downloadAndCacheImageWithURL(
+            resource.downloadURL,
+            forKey: resource.cacheKey,
+            retrieveImageTask: task,
+            progressBlock: nil,
+            completionHandler: {
+                (image, error, _, _) -> () in
+                
+                self.tasks.removeValueForKey(resource.downloadURL)
+                
+                if let _ = error {
+                    self.failedResources.append(resource)
+                } else {
+                    self.completedResources.append(resource)
+                }
+                
+                self.reportProgress()
+                
+                if self.cancelled {
+                    if self.tasks.isEmpty {
+                        let pendingResources = self.prefetchResources[self.requestedCount..<self.prefetchResources.count]
+                        self.failedResources += Array(pendingResources)
+                        self.handleComplete()
+                    }
+                } else {
+                    self.reportCompletionOrStartNext()
+                }
+            },
+            options: optionsInfo)
         
-        manager.downloadAndCacheImageWithURL(resource.downloadURL, forKey: resource.cacheKey, retrieveImageTask: task, progressBlock: nil, completionHandler: { image, error, cacheType, imageURL in
-            self.finishedCount++
-            
-            if image == .None {
-                self.skippedCount++
+        if let downloadTask = downloadTask {
+            tasks[resource.downloadURL] = downloadTask
+        }
+    }
+    
+    func appendCachedResource(resource: Resource) {
+        skippedResources.append(resource)
+ 
+        reportProgress()
+        reportCompletionOrStartNext()
+    }
+    
+    func startPrefetchingResource(resource: Resource)
+    {
+        requestedCount += 1
+        if optionsInfo.forceRefresh {
+            downloadAndCacheResource(resource)
+        } else {
+            let alreadyInCache = manager.cache.isImageCachedForKey(resource.cacheKey).cached
+            if alreadyInCache {
+                appendCachedResource(resource)
+            } else {
+                downloadAndCacheResource(resource)
             }
-            
-            progressBlock?(completedURLs: self.finishedCount, allURLs: total)
-            
-            // Reference the prefetchURLs rather than urls in case the request has been cancelled
-            if (self.prefetchURLs?.count ?? 0) > self.requestedCount {
-                self.startPrefetching(self.requestedCount, progressBlock: progressBlock, completionHandler: completionHandler)
-            } else if self.finishedCount == self.requestedCount {
-                self.prefetchURLs?.removeAll()
-                completionHandler?(cancelled: false, completedURLs: self.finishedCount, skippedURLs: self.skippedCount)
-            } else if (self.prefetchURLs == nil || self.prefetchURLs!.count == 0) && !self.cancelCompletionHandlerCalled {
-                self.cancelCompletionHandlerCalled = true
-                completionHandler?(cancelled: true, completedURLs: self.finishedCount, skippedURLs: self.skippedCount)
+        }
+    }
+    
+    func reportProgress() {
+        progressBlock?(skippedResources: skippedResources, failedResources: failedResources, completedResources: completedResources)
+    }
+    
+    func reportCompletionOrStartNext() {
+        if finished {
+            handleComplete()
+        } else {
+            if requestedCount < prefetchResources.count {
+                startPrefetchingResource(prefetchResources[requestedCount])
             }
-            
-        }, options: nil)
+        }
+    }
+    
+    func handleComplete() {
+        completionHandler?(skippedResources: skippedResources, failedResources: failedResources, completedResources: completedResources)
+        completionHandler = nil
+        progressBlock = nil
     }
 }

+ 11 - 6
Sources/KingfisherManager.swift

@@ -91,9 +91,13 @@ public class KingfisherManager {
     
     - returns: A Kingfisher manager object with default cache, default downloader, and default prefetcher.
     */
-    public init() {
-        cache = ImageCache.defaultCache
-        downloader = ImageDownloader.defaultDownloader
+    public convenience init() {
+        self.init(downloader: ImageDownloader.defaultDownloader, cache: ImageCache.defaultCache)
+    }
+    
+    init(downloader: ImageDownloader, cache: ImageCache) {
+        self.downloader = downloader
+        self.cache = cache
     }
     
     /**
@@ -164,10 +168,10 @@ public class KingfisherManager {
                         retrieveImageTask: RetrieveImageTask,
                             progressBlock: DownloadProgressBlock?,
                         completionHandler: CompletionHandler?,
-                                  options: KingfisherOptionsInfo?)
+                                  options: KingfisherOptionsInfo?) -> RetrieveImageDownloadTask?
     {
         let downloader = options?.downloader ?? self.downloader
-        downloader.downloadImageWithURL(URL, retrieveImageTask: retrieveImageTask, options: options,
+        return downloader.downloadImageWithURL(URL, retrieveImageTask: retrieveImageTask, options: options,
             progressBlock: { receivedSize, totalSize in
                 progressBlock?(receivedSize: receivedSize, totalSize: totalSize)
             },
@@ -187,8 +191,9 @@ public class KingfisherManager {
                 if let image = image, originalData = originalData {
                     targetCache.storeImage(image, originalData: originalData, forKey: key, toDisk: !(options?.cacheMemoryOnly ?? false), completionHandler: nil)
                 }
-                
+
                 completionHandler?(image: image, error: error, cacheType: .None, imageURL: URL)
+
             })
     }
     

+ 4 - 2
Sources/KingfisherOptionsInfo.swift

@@ -67,8 +67,6 @@ infix operator <== {
     precedence 160
 }
 
-
-
 // This operator returns true if two `KingfisherOptionsInfoItem` enum is the same, without considering the associated values.
 func <== (lhs: KingfisherOptionsInfoItem, rhs: KingfisherOptionsInfoItem) -> Bool {
     switch (lhs, rhs) {
@@ -89,6 +87,10 @@ extension CollectionType where Generator.Element == KingfisherOptionsInfoItem {
     func kf_firstMatchIgnoringAssociatedValue(target: Generator.Element) -> Generator.Element? {
         return indexOf { $0 <== target }.flatMap { self[$0] }
     }
+    
+    func kf_removeAllMatchesIgnoringAssociatedValue(target: Generator.Element) -> [Generator.Element] {
+        return self.filter { !($0 <== target) }
+    }
 }
 
 extension CollectionType where Generator.Element == KingfisherOptionsInfoItem {

+ 93 - 30
Tests/KingfisherTests/ImagePrefetcherTests.swift

@@ -25,11 +25,9 @@
 //  THE SOFTWARE.
 
 import XCTest
-@testable import Kingfisher
+import Kingfisher
 
 class ImagePrefetcherTests: XCTestCase {
-
-    var prefetcher: ImagePrefetcher!
     
     override class func setUp() {
         super.setUp()
@@ -44,13 +42,12 @@ class ImagePrefetcherTests: XCTestCase {
     override func setUp() {
         super.setUp()
         // Put setup code here. This method is called before the invocation of each test method in the class.
-        prefetcher = ImagePrefetcher()
+        cleanDefaultCache()
     }
     
     override func tearDown() {
         // Put teardown code here. This method is called after the invocation of each test method in the class.
         cleanDefaultCache()
-        prefetcher = nil
         super.tearDown()
     }
 
@@ -62,20 +59,23 @@ class ImagePrefetcherTests: XCTestCase {
             stubRequest("GET", URLString).andReturn(200).withBody(testImageData)
             urls.append(NSURL(string: URLString)!)
         }
-
-        let total = urls.count
         
-        prefetcher.prefetchURLs(urls, progressBlock: { (completedURLs, allURLs) -> () in
-            XCTAssertEqual(allURLs, total, "total urls should match all those the prefetcher knows about")
-        }) { (cancelled, completedURLs, skippedURLs) -> () in
-            expectation.fulfill()
-            XCTAssertFalse(cancelled, "the prefetch should not have been cancelled")
-            XCTAssertEqual(completedURLs, total, "all requests should have been completed, regardless of success")
-            KingfisherManager.sharedManager.cache.clearMemoryCache()  // Remove from the Memory cache to ensure it is on disk!
-            let cacheStatus = KingfisherManager.sharedManager.cache.isImageCachedForKey(Resource(downloadURL: urls[0]).cacheKey)
-            XCTAssertEqual(CacheType.Disk, cacheStatus.cacheType ?? CacheType.None, "prefetched images should be cached to disk")
+        var progressCalledCount = 0
+        let prefetcher = ImagePrefetcher(urls: urls, optionsInfo: nil, progressBlock: { (skippedResources, failedResources, completedResources) -> () in
+            progressCalledCount += 1
+            }) { (skippedResources, failedResources, completedResources) -> () in
+                expectation.fulfill()
+                XCTAssertEqual(skippedResources.count, 0, "There should be no items skipped.")
+                XCTAssertEqual(failedResources.count, 0, "There should be no failed downloading.")
+                XCTAssertEqual(completedResources.count, urls.count, "All resources prefetching should be completed.")
+                XCTAssertEqual(progressCalledCount, urls.count, "Progress should be called the same time of download count.")
+                for url in urls {
+                    XCTAssertTrue(KingfisherManager.sharedManager.cache.isImageCachedForKey(url.absoluteString).cached)
+                }
         }
         
+        prefetcher.start()
+        
         waitForExpectationsWithTimeout(5, handler: nil)
     }
     
@@ -83,26 +83,42 @@ class ImagePrefetcherTests: XCTestCase {
         let expectation = expectationWithDescription("wait for prefetching images")
         
         var urls = [NSURL]()
+        var responses = [LSStubResponseDSL!]()
         for URLString in testKeys {
-            stubRequest("GET", URLString).andReturn(200).withBody(testImageData)
+            let response = stubRequest("GET", URLString).andReturn(200).withBody(testImageData).delay()
+            responses.append(response)
             urls.append(NSURL(string: URLString)!)
         }
         
-        prefetcher.maxConcurrentDownloads = 2
-        
-        prefetcher.prefetchURLs(urls, progressBlock: { (completedURLs, allURLs) -> () in
-            }) { (cancelled, completedURLs, skippedURLs) -> () in
-                XCTAssertTrue(cancelled, "the prefetch should have been cancelled")
-                // The completed and skipped URLs will depend on how far through the process the prefetch got before the cancel was called
+        let maxConcurrentCount = 2
+        let prefetcher = ImagePrefetcher(urls: urls, optionsInfo: nil, progressBlock: { (skippedResources, failedResources, completedResources) -> () in
+            
+            }) { (skippedResources, failedResources, completedResources) -> () in
                 expectation.fulfill()
+                XCTAssertEqual(skippedResources.count, 0, "There should be no items skipped.")
+                XCTAssertEqual(failedResources.count, urls.count, "The failed count should be the same with started downloads due to cancellation.")
+                XCTAssertEqual(completedResources.count, 0, "None resources prefetching should complete.")
+        }
+        
+        prefetcher.maxConcurrentDownloads = maxConcurrentCount
+        
+        prefetcher.start()
+        prefetcher.cancel()
+        
+        let delayTime = dispatch_time(DISPATCH_TIME_NOW, Int64(0.1 * Double(NSEC_PER_SEC)))
+        dispatch_after(delayTime, dispatch_get_main_queue()) {
+            for response in responses {
+                response.go()
+            }
         }
-        prefetcher.cancelPrefetching()
 
         waitForExpectationsWithTimeout(5, handler: nil)
     }
     
-    func testIsPrefetching() {
+
+    func testPrefetcherCouldSkipCachedImages() {
         let expectation = expectationWithDescription("wait for prefetching images")
+        KingfisherManager.sharedManager.cache.storeImage(UIImage(), forKey: testKeys[0])
         
         var urls = [NSURL]()
         for URLString in testKeys {
@@ -110,14 +126,61 @@ class ImagePrefetcherTests: XCTestCase {
             urls.append(NSURL(string: URLString)!)
         }
         
-        prefetcher.prefetchURLs(urls, progressBlock: { (completedURLs, allURLs) -> () in
-            XCTAssertTrue(self.prefetcher.isPrefetching(), "should be prefetching")
-            }) { (cancelled, completedURLs, skippedURLs) -> () in
-                XCTAssertFalse(cancelled, "the prefetch should not have been cancelled")
-                XCTAssertFalse(self.prefetcher.isPrefetching(), "should not be prefetching")
+        let prefetcher = ImagePrefetcher(urls: urls, optionsInfo: nil, progressBlock: { (skippedResources, failedResources, completedResources) -> () in
+
+            }) { (skippedResources, failedResources, completedResources) -> () in
                 expectation.fulfill()
+                XCTAssertEqual(skippedResources.count, 1, "There should be 1 item skipped.")
+                XCTAssertEqual(skippedResources[0].downloadURL.absoluteString, testKeys[0], "The correct image key should be skipped.")
+
+                XCTAssertEqual(failedResources.count, 0, "There should be no failed downloading.")
+                XCTAssertEqual(completedResources.count, urls.count - 1, "All resources prefetching should be completed.")
+        }
+        
+        prefetcher.start()
+        
+        waitForExpectationsWithTimeout(5, handler: nil)
+    }
+    
+    func testPrefetcherForceRefreshDownloadImages() {
+        let expectation = expectationWithDescription("wait for prefetching images")
+        
+        // Store an image in cache.
+        KingfisherManager.sharedManager.cache.storeImage(UIImage(), forKey: testKeys[0])
+        
+        var urls = [NSURL]()
+        for URLString in testKeys {
+            stubRequest("GET", URLString).andReturn(200).withBody(testImageData)
+            urls.append(NSURL(string: URLString)!)
+        }
+        
+        // Use `.ForceRefresh` to download it forcely.
+        let prefetcher = ImagePrefetcher(urls: urls, optionsInfo: [.ForceRefresh], progressBlock: { (skippedResources, failedResources, completedResources) -> () in
+            
+            }) { (skippedResources, failedResources, completedResources) -> () in
+                expectation.fulfill()
+                
+                XCTAssertEqual(skippedResources.count, 0, "There should be no item skipped.")
+                XCTAssertEqual(failedResources.count, 0, "There should be no failed downloading.")
+                XCTAssertEqual(completedResources.count, urls.count, "All resources prefetching should be completed.")
+        }
+        
+        prefetcher.start()
+        
+        waitForExpectationsWithTimeout(5, handler: nil)
+    }
+    
+    func testPrefetchWithWrongInitParameters() {
+        let expectation = expectationWithDescription("wait for prefetching images")
+        let prefetcher = ImagePrefetcher(urls: [], optionsInfo: nil, progressBlock: nil) { (skippedResources, failedResources, completedResources) -> () in
+            expectation.fulfill()
+            
+            XCTAssertEqual(skippedResources.count, 0, "There should be no item skipped.")
+            XCTAssertEqual(failedResources.count, 0, "There should be no failed downloading.")
+            XCTAssertEqual(completedResources.count, 0, "There should be no completed downloading.")
         }
         
+        prefetcher.start()
         waitForExpectationsWithTimeout(5, handler: nil)
     }
 }