mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-28 03:07:43 +00:00
Merge pull request #3180 from keymanapp/feat/ios/httpdownloader-unit-tests
feat(ios/engine): HTTPDownloader unit testing
This commit is contained in:
commit
e8cbcd0adb
8 changed files with 420 additions and 3 deletions
|
|
@ -126,6 +126,9 @@
|
|||
C0D3F3601F9F3AD80055C7CF /* InstallableKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D3F35F1F9F3AD80055C7CF /* InstallableKeyboard.swift */; };
|
||||
C0E30C8C1FC40D0400C80416 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0E30C8B1FC40D0400C80416 /* Storage.swift */; };
|
||||
C0EF3E7B1F95B65300CE9BD4 /* KeymanWebDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0EF3E7A1F95B65300CE9BD4 /* KeymanWebDelegate.swift */; };
|
||||
CE07E3D2247E3FB100DFA9D2 /* HTTPDownloaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE07E3D1247E3FB100DFA9D2 /* HTTPDownloaderTests.swift */; };
|
||||
CE07E3D6247E41DB00DFA9D2 /* URLSessionMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE07E3D5247E41DB00DFA9D2 /* URLSessionMock.swift */; };
|
||||
CE07E3D8247E43CB00DFA9D2 /* URLSessionDownloadTaskMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE07E3D7247E43CB00DFA9D2 /* URLSessionDownloadTaskMock.swift */; };
|
||||
CE17ABDE23069E76005FBB14 /* LanguageResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE17ABDD23069E76005FBB14 /* LanguageResource.swift */; };
|
||||
CE1E1EC12303C8CC001C7BE0 /* ResourceDownloadStatusToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1E1EC02303C8CC001C7BE0 /* ResourceDownloadStatusToolbar.swift */; };
|
||||
CE1F67A32304EB3800FF6972 /* ResourceDownloadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1F67A22304EB3800FF6972 /* ResourceDownloadManager.swift */; };
|
||||
|
|
@ -174,6 +177,7 @@
|
|||
CE808A4D236697D500713E6B /* Reachability.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A0FC9FC22D66D9E00D33F86 /* Reachability.framework */; };
|
||||
CE808A4F236697D800713E6B /* XCGLogger.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1687ACCC1FD8DE5300926D69 /* XCGLogger.framework */; };
|
||||
CE808A532366980A00713E6B /* Zip.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 165C8B8B1FD8220600D4A78D /* Zip.framework */; };
|
||||
CE88042F247F4B97009BCA1A /* ExpectationDownloaderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE88042E247F4B97009BCA1A /* ExpectationDownloaderDelegate.swift */; };
|
||||
CE8EDEB123F53D1A009E1FF6 /* FileManagementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A079DD1223194B100581263 /* FileManagementTests.swift */; };
|
||||
CE8EDEB323F53F96009E1FF6 /* VersionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8EDEB223F53F96009E1FF6 /* VersionTests.swift */; };
|
||||
CE976D4F23FA380700FFDF3A /* Reachability.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A0FC9FC22D66D9E00D33F86 /* Reachability.framework */; };
|
||||
|
|
@ -420,6 +424,9 @@
|
|||
C0ED71B21F6BB0B1002A2FD6 /* HTTPDownloadRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPDownloadRequest.swift; sourceTree = "<group>"; };
|
||||
C0ED71B41F6BBFAF002A2FD6 /* HTTPDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPDownloader.swift; sourceTree = "<group>"; };
|
||||
C0EF3E7A1F95B65300CE9BD4 /* KeymanWebDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeymanWebDelegate.swift; sourceTree = "<group>"; };
|
||||
CE07E3D1247E3FB100DFA9D2 /* HTTPDownloaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPDownloaderTests.swift; sourceTree = "<group>"; };
|
||||
CE07E3D5247E41DB00DFA9D2 /* URLSessionMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionMock.swift; sourceTree = "<group>"; };
|
||||
CE07E3D7247E43CB00DFA9D2 /* URLSessionDownloadTaskMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionDownloadTaskMock.swift; sourceTree = "<group>"; };
|
||||
CE17ABDD23069E76005FBB14 /* LanguageResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageResource.swift; sourceTree = "<group>"; };
|
||||
CE1E1EC02303C8CC001C7BE0 /* ResourceDownloadStatusToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourceDownloadStatusToolbar.swift; sourceTree = "<group>"; };
|
||||
CE1F67A22304EB3800FF6972 /* ResourceDownloadManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourceDownloadManager.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -443,6 +450,7 @@
|
|||
CE7A26D723CEEC630005955C /* Colors+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Colors+Extension.swift"; sourceTree = "<group>"; };
|
||||
CE7A26DA23CEEF640005955C /* Colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Colors.swift; sourceTree = "<group>"; };
|
||||
CE7ADD6523DE89FC00BC9A00 /* Alerts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alerts.swift; sourceTree = "<group>"; };
|
||||
CE88042E247F4B97009BCA1A /* ExpectationDownloaderDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpectationDownloaderDelegate.swift; sourceTree = "<group>"; };
|
||||
CE8EDEB223F53F96009E1FF6 /* VersionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionTests.swift; sourceTree = "<group>"; };
|
||||
CE9B440B23FE49F000499CAB /* Migrations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Migrations.swift; sourceTree = "<group>"; };
|
||||
CE9B440D23FE4E7400499CAB /* 12.0 Ad-hoc Migration.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = "12.0 Ad-hoc Migration.bundle"; sourceTree = "<group>"; };
|
||||
|
|
@ -684,6 +692,7 @@
|
|||
CE9CD88123FCC4C7002BF2F8 /* TestUtils */,
|
||||
CE37C38923FCC9AF007031C6 /* resources */,
|
||||
9A079DD1223194B100581263 /* FileManagementTests.swift */,
|
||||
CE07E3D1247E3FB100DFA9D2 /* HTTPDownloaderTests.swift */,
|
||||
9A079DD3223194B100581263 /* Info.plist */,
|
||||
CE8EDEB223F53F96009E1FF6 /* VersionTests.swift */,
|
||||
CE4459CD23FBBA8D003151FD /* AppDelegate.swift */,
|
||||
|
|
@ -784,6 +793,16 @@
|
|||
path = KeyboardRepository;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CE07E3D3247E418800DFA9D2 /* Downloading */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE07E3D5247E41DB00DFA9D2 /* URLSessionMock.swift */,
|
||||
CE07E3D7247E43CB00DFA9D2 /* URLSessionDownloadTaskMock.swift */,
|
||||
CE88042E247F4B97009BCA1A /* ExpectationDownloaderDelegate.swift */,
|
||||
);
|
||||
path = Downloading;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CE25CCBB1F1DA72A005AA2BC /* HTTPRequest */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -829,6 +848,7 @@
|
|||
CE9CD88123FCC4C7002BF2F8 /* TestUtils */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE07E3D3247E418800DFA9D2 /* Downloading */,
|
||||
CE9CD87F23FCC1CA002BF2F8 /* TestUtils.swift */,
|
||||
CE9CD88223FCC4E3002BF2F8 /* UserDefaults.swift */,
|
||||
CE37C38C23FCD41E007031C6 /* Keyboards.swift */,
|
||||
|
|
@ -1043,6 +1063,7 @@
|
|||
C06D37281F81F4E100F61AE0 /* Headers */,
|
||||
C06D37291F81F4E100F61AE0 /* Resources */,
|
||||
CEC0C66F2410B005003E1BCD /* ShellScript */,
|
||||
CE880430247F6EB6009BCA1A /* ShellScript */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
|
|
@ -1268,6 +1289,24 @@
|
|||
shellPath = /bin/sh;
|
||||
shellScript = "#if which swiftlint >/dev/null; then\n# swiftlint --config ../../.swiftlint.yml\n#else\n# echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\n#fi\n";
|
||||
};
|
||||
CE880430247F6EB6009BCA1A /* ShellScript */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n. \"$KEYMAN_ROOT/resources/build/xcode-utils.sh\"\n\nphaseSetBundleVersions\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
CEC0C66F2410B005003E1BCD /* ShellScript */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
|
|
@ -1285,6 +1324,7 @@
|
|||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n. \"$KEYMAN_ROOT/resources/build/xcode-utils.sh\"\n\nif [ ${UPLOAD_SENTRY:-false} = true ]; then\n # Calls resource script to perform the dSYM upload\n phaseSentryDsymUpload \"keyman-ios\"\nfi\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
|
|
@ -1302,11 +1342,15 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
CE07E3D2247E3FB100DFA9D2 /* HTTPDownloaderTests.swift in Sources */,
|
||||
CE9B440C23FE49F000499CAB /* Migrations.swift in Sources */,
|
||||
CE88042F247F4B97009BCA1A /* ExpectationDownloaderDelegate.swift in Sources */,
|
||||
CE07E3D8247E43CB00DFA9D2 /* URLSessionDownloadTaskMock.swift in Sources */,
|
||||
CE37C39123FCD617007031C6 /* LexicalModels.swift in Sources */,
|
||||
CE37C38D23FCD41E007031C6 /* Keyboards.swift in Sources */,
|
||||
CE9CD88323FCC4E3002BF2F8 /* UserDefaults.swift in Sources */,
|
||||
CEF888D523FE786C00667693 /* KeyboardScaleTests.swift in Sources */,
|
||||
CE07E3D6247E41DB00DFA9D2 /* URLSessionMock.swift in Sources */,
|
||||
CE8EDEB323F53F96009E1FF6 /* VersionTests.swift in Sources */,
|
||||
CEDFEF8F23FE43B700BECF39 /* MigrationTests.swift in Sources */,
|
||||
CE9CD88023FCC1CA002BF2F8 /* TestUtils.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
enum DownloadType {
|
||||
enum DownloadType: String {
|
||||
case downloadFile
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,10 +24,10 @@ class HTTPDownloader: NSObject {
|
|||
var downloadSession: URLSession!
|
||||
public var userInfo: [String: Any] = [:]
|
||||
|
||||
init(_ handler: HTTPDownloadDelegate?) {
|
||||
init(_ handler: HTTPDownloadDelegate?, session: URLSession = .shared) {
|
||||
super.init()
|
||||
self.handler = handler
|
||||
downloadSession = URLSession.shared
|
||||
downloadSession = session
|
||||
}
|
||||
|
||||
func addRequest(_ request: HTTPDownloadRequest) {
|
||||
|
|
|
|||
143
ios/engine/KMEI/KeymanEngineTests/HTTPDownloaderTests.swift
Normal file
143
ios/engine/KMEI/KeymanEngineTests/HTTPDownloaderTests.swift
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
//
|
||||
// HTTPDownloaderTests.swift
|
||||
// KeymanEngineTests
|
||||
//
|
||||
// Created by Joshua Horton on 5/27/20.
|
||||
// Copyright © 2020 SIL International. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import KeymanEngine
|
||||
|
||||
class HTTPDownloaderTests: XCTestCase {
|
||||
var downloader: HTTPDownloader?
|
||||
var mockedURLSession: TestUtils.Downloading.URLSessionMock?
|
||||
|
||||
override func setUp() {
|
||||
mockedURLSession = TestUtils.Downloading.URLSessionMock()
|
||||
downloader = HTTPDownloader(nil, session: mockedURLSession!)
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
let queueWasCleared = mockedURLSession!.queueIsEmpty
|
||||
mockedURLSession = nil
|
||||
downloader = nil
|
||||
|
||||
if !queueWasCleared {
|
||||
throw NSError(domain: "Keyman",
|
||||
code: 4,
|
||||
userInfo: [NSLocalizedDescriptionKey: "A test did not fully utilize its queued mock results!"])
|
||||
}
|
||||
}
|
||||
|
||||
func testSingleRequestSuccess() throws {
|
||||
// Simple, easy case: our local testing copy of the Khmer Angkor KMP file.
|
||||
let mockedResult = TestUtils.Downloading.DownloadMockResult(location: TestUtils.Keyboards.khmerAngkorKMP, error: nil)
|
||||
mockedURLSession?.queueMockResult(mockedResult)
|
||||
|
||||
let testDelegate = TestUtils.Downloading.ExpectationDownloaderDelegate()
|
||||
downloader!.handler = testDelegate
|
||||
|
||||
let request = HTTPDownloadRequest(url: TestUtils.Keyboards.khmerAngkorKMP)
|
||||
downloader!.addRequest(request)
|
||||
testDelegate.expect(method: .RequestStarted, request: request)
|
||||
testDelegate.expect(method: .RequestFinished, request: request)
|
||||
testDelegate.expect(method: .QueueFinished, queue: downloader!)
|
||||
|
||||
XCTAssertEqual(downloader!.requestsCount, 1)
|
||||
downloader!.run()
|
||||
testDelegate.sequentialWait(testCase: self, timeout: 10)
|
||||
}
|
||||
|
||||
func testSingleRequestFailure() throws {
|
||||
// A twist on the previous version - the 'download' fails.
|
||||
let mockedResult = TestUtils.Downloading.DownloadMockResult(location: nil, error: NSError(domain: "KeymanTests", code: 1, userInfo: nil))
|
||||
mockedURLSession?.queueMockResult(mockedResult)
|
||||
|
||||
let testDelegate = TestUtils.Downloading.ExpectationDownloaderDelegate()
|
||||
downloader!.handler = testDelegate
|
||||
|
||||
let request = HTTPDownloadRequest(url: TestUtils.Keyboards.khmerAngkorKMP)
|
||||
downloader!.addRequest(request)
|
||||
testDelegate.expect(method: .RequestStarted, request: request)
|
||||
testDelegate.expect(method: .RequestFailed, request: request)
|
||||
testDelegate.expect(method: .QueueFinished, queue: downloader!)
|
||||
|
||||
XCTAssertEqual(downloader!.requestsCount, 1)
|
||||
downloader!.run()
|
||||
testDelegate.sequentialWait(testCase: self, timeout: 10)
|
||||
}
|
||||
|
||||
func testSequentialRequests() {
|
||||
let mockedResult1 = TestUtils.Downloading.DownloadMockResult(location: TestUtils.Keyboards.khmerAngkorKMP, error: nil)
|
||||
mockedURLSession?.queueMockResult(mockedResult1)
|
||||
|
||||
let mockedResult2 = TestUtils.Downloading.DownloadMockResult(location:
|
||||
TestUtils.LexicalModels.mtntKMP, error: nil)
|
||||
mockedURLSession?.queueMockResult(mockedResult2)
|
||||
|
||||
let testDelegate = TestUtils.Downloading.ExpectationDownloaderDelegate()
|
||||
downloader!.handler = testDelegate
|
||||
|
||||
let request1 = HTTPDownloadRequest(url: TestUtils.Keyboards.khmerAngkorKMP)
|
||||
downloader!.addRequest(request1)
|
||||
testDelegate.expect(method: .RequestStarted, request: request1)
|
||||
testDelegate.expect(method: .RequestFinished, request: request1)
|
||||
|
||||
let request2 = HTTPDownloadRequest(url: TestUtils.LexicalModels.mtntKMP)
|
||||
downloader!.addRequest(request2)
|
||||
testDelegate.expect(method: .RequestStarted, request: request2)
|
||||
testDelegate.expect(method: .RequestFinished, request: request2)
|
||||
testDelegate.expect(method: .QueueFinished, queue: downloader!)
|
||||
|
||||
XCTAssertEqual(downloader!.requestsCount, 2)
|
||||
downloader!.run()
|
||||
// This is currently safe, as all downloads are sequential. If we ever
|
||||
// enable parallel downloads, this will obviously need to change
|
||||
testDelegate.sequentialWait(testCase: self, timeout: 10)
|
||||
}
|
||||
|
||||
func testRequestCancellation() {
|
||||
let mockedResult1 = TestUtils.Downloading.DownloadMockResult(location: TestUtils.Keyboards.khmerAngkorKMP, error: nil)
|
||||
mockedURLSession?.queueMockResult(mockedResult1)
|
||||
|
||||
// No second mocked result - its call should be cancelled.
|
||||
|
||||
let testDelegate = TestUtils.Downloading.ExpectationDownloaderDelegate()
|
||||
downloader!.handler = testDelegate
|
||||
|
||||
let request1 = HTTPDownloadRequest(url: TestUtils.Keyboards.khmerAngkorKMP)
|
||||
downloader!.addRequest(request1)
|
||||
testDelegate.expect(method: .RequestStarted, request: request1)
|
||||
|
||||
let request2 = HTTPDownloadRequest(url: TestUtils.LexicalModels.mtntKMP)
|
||||
downloader!.addRequest(request2)
|
||||
testDelegate.expectAbsence(method: .RequestStarted, request: request2)
|
||||
testDelegate.expectAbsence(method: .RequestFinished, request: request2)
|
||||
testDelegate.expect(method: .QueueCancelled, queue: downloader!)
|
||||
|
||||
// Placed here for chronological ordering. We expect the request to finish because of
|
||||
// our mocking structure, but want to delay its completion long enough for the
|
||||
// `cancelAllOperations` call below to complete.
|
||||
testDelegate.expect(method: .RequestFinished, request: request1, completion: { response in
|
||||
// Force a delay so that our `cancelAllOperations` call has a chance to execute;
|
||||
// our mocking structure is otherwise synchronous!
|
||||
sleep(1) // in seconds.
|
||||
response.expectation.fulfill()
|
||||
return false
|
||||
})
|
||||
|
||||
// Ideally, we could add this... but because of how things are currently mocked,
|
||||
// it actually does trigger - we have a .RequestFinished processing after .QueueCancelled,
|
||||
// which then notes the empty queue.
|
||||
//
|
||||
// testDelegate.expectAbsence(method: .QueueFinished, queue: downloader!)
|
||||
|
||||
XCTAssertEqual(downloader!.requestsCount, 2)
|
||||
downloader!.run()
|
||||
downloader!.cancelAllOperations()
|
||||
// This is currently safe, as all downloads are sequential. If we ever
|
||||
// enable parallel downloads, this will obviously need to change
|
||||
testDelegate.sequentialWait(testCase: self, timeout: 5)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
//
|
||||
// BaseTestDownloaderDelegate.swift
|
||||
// KeymanEngineTests
|
||||
//
|
||||
// Created by Joshua Horton on 5/28/20.
|
||||
// Copyright © 2020 SIL International. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
@testable import KeymanEngine
|
||||
|
||||
extension TestUtils.Downloading {
|
||||
class ExpectationDownloaderDelegate: HTTPDownloadDelegate {
|
||||
enum Method: String {
|
||||
case RequestStarted
|
||||
case RequestFinished
|
||||
case RequestFailed
|
||||
case QueueFinished
|
||||
case QueueCancelled
|
||||
}
|
||||
|
||||
typealias ExpectationMetBlock = (ExpectationQueueEntry) -> Bool
|
||||
|
||||
struct ExpectationQueueEntry {
|
||||
let method: Method
|
||||
let request: HTTPDownloadRequest?
|
||||
let queue: HTTPDownloader?
|
||||
let completion: ExpectationMetBlock?
|
||||
let expectation: XCTestExpectation
|
||||
|
||||
init(method: Method, request: HTTPDownloadRequest, completion: ExpectationMetBlock? = nil) {
|
||||
self.init(method: method, request: request, queue: nil, completion: completion)
|
||||
}
|
||||
|
||||
init(method: Method, queue: HTTPDownloader, completion: ExpectationMetBlock? = nil) {
|
||||
self.init(method: method, request: nil, queue: queue, completion: completion)
|
||||
}
|
||||
|
||||
private init(method: Method, request: HTTPDownloadRequest?, queue: HTTPDownloader?, completion: ExpectationMetBlock? = nil) {
|
||||
self.method = method
|
||||
self.request = request
|
||||
self.queue = queue
|
||||
self.completion = completion
|
||||
|
||||
// Now to dynamically build the description test for the Expectation, then the Expectation itself
|
||||
var description: String
|
||||
switch method {
|
||||
case .RequestStarted, .RequestFinished, .RequestFailed:
|
||||
description = "\(method.rawValue) for \(request!.typeCode.rawValue) from \(request!.url)"
|
||||
case .QueueFinished, .QueueCancelled:
|
||||
description = "\(method.rawValue)"
|
||||
}
|
||||
self.expectation = XCTestExpectation(description: description)
|
||||
}
|
||||
}
|
||||
|
||||
private var expectationQueue: [ExpectationQueueEntry] = []
|
||||
private var expectedAbsences: [ExpectationQueueEntry] = []
|
||||
|
||||
private func doExpectationMatch(method: Method, request: HTTPDownloadRequest? = nil, queue: HTTPDownloader? = nil) {
|
||||
|
||||
if expectationQueue.count == 0 {
|
||||
return
|
||||
} else {
|
||||
let head = expectationQueue[0]
|
||||
if head.method == method && head.request == request && head.queue == queue {
|
||||
expectationQueue.removeFirst()
|
||||
|
||||
// Now to look to fulfill the expectation.
|
||||
if head.completion != nil {
|
||||
if head.completion!(head) {
|
||||
head.expectation.fulfill()
|
||||
}
|
||||
} else {
|
||||
head.expectation.fulfill()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func downloadRequestStarted(_ request: HTTPDownloadRequest) {
|
||||
doExpectationMatch(method: .RequestStarted, request: request)
|
||||
}
|
||||
|
||||
func downloadRequestFinished(_ request: HTTPDownloadRequest) {
|
||||
doExpectationMatch(method: .RequestFinished, request: request)
|
||||
}
|
||||
|
||||
func downloadRequestFailed(_ request: HTTPDownloadRequest) {
|
||||
doExpectationMatch(method: .RequestFailed, request: request)
|
||||
}
|
||||
|
||||
func downloadQueueFinished(_ queue: HTTPDownloader) {
|
||||
doExpectationMatch(method: .QueueFinished, queue: queue)
|
||||
}
|
||||
|
||||
func downloadQueueCancelled(_ queue: HTTPDownloader) {
|
||||
doExpectationMatch(method: .QueueCancelled, queue: queue)
|
||||
}
|
||||
|
||||
func expect(method: Method, request: HTTPDownloadRequest, completion: ExpectationMetBlock? = nil) {
|
||||
expectationQueue.append(ExpectationQueueEntry(method: method, request: request, completion: completion))
|
||||
}
|
||||
|
||||
func expect(method: Method, queue: HTTPDownloader, completion: ExpectationMetBlock? = nil) {
|
||||
expectationQueue.append(ExpectationQueueEntry(method: method, queue: queue, completion: completion))
|
||||
}
|
||||
|
||||
func expectAbsence(method: Method, request: HTTPDownloadRequest, completion: ExpectationMetBlock? = nil) {
|
||||
let entry = ExpectationQueueEntry(method: method, request: request, completion: completion)
|
||||
entry.expectation.isInverted = true
|
||||
expectedAbsences.append(entry)
|
||||
}
|
||||
|
||||
func expectAbsence(method: Method, queue: HTTPDownloader, completion: ExpectationMetBlock? = nil) {
|
||||
let entry = ExpectationQueueEntry(method: method, queue: queue, completion: completion)
|
||||
entry.expectation.isInverted = true
|
||||
expectedAbsences.append(entry)
|
||||
}
|
||||
|
||||
func sequentialWait(testCase: XCTestCase, timeout: TimeInterval = 10) {
|
||||
// while expectationQueue.count > 0 {
|
||||
// testCase.wait(for: [expectationQueue[0].expectation], timeout: timeout)
|
||||
// }
|
||||
var expectationList: [XCTestExpectation] = expectationQueue.map { entry in
|
||||
return entry.expectation
|
||||
}
|
||||
|
||||
let absenceList: [XCTestExpectation] = expectedAbsences.map { entry in
|
||||
return entry.expectation
|
||||
}
|
||||
|
||||
expectationList.append(contentsOf: absenceList)
|
||||
testCase.wait(for: expectationList, timeout: timeout, enforceOrder: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
//
|
||||
// URLSessionDownloadTaskMock.swift
|
||||
// KeymanEngineTests
|
||||
//
|
||||
// Created by Joshua Horton on 5/27/20.
|
||||
// Copyright © 2020 SIL International. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension TestUtils.Downloading {
|
||||
/**
|
||||
* Many thanks to https://www.swiftbysundell.com/articles/mocking-in-swift/ for the approach used here.
|
||||
*/
|
||||
class URLSessionDownloadTaskMock: URLSessionDownloadTask {
|
||||
private let closure: () -> Void
|
||||
|
||||
init(closure: @escaping () -> Void) {
|
||||
self.closure = closure
|
||||
}
|
||||
|
||||
/*
|
||||
* For mocked DownloadTasks, just use the precomputed completion closure
|
||||
* provided to the constructor.
|
||||
*/
|
||||
override func resume() {
|
||||
closure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
//
|
||||
// URLSessionMock.swift
|
||||
// KeymanEngineTests
|
||||
//
|
||||
// Created by Joshua Horton on 5/27/20.
|
||||
// Copyright © 2020 SIL International. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension TestUtils.Downloading {
|
||||
class DownloadMockResult {
|
||||
let location: URL?
|
||||
let error: Error?
|
||||
|
||||
init(location: URL?, error: Error?) {
|
||||
self.location = location
|
||||
self.error = error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Many thanks to https://www.swiftbysundell.com/articles/mocking-in-swift/ for the approach used here.
|
||||
*/
|
||||
class URLSessionMock: URLSession {
|
||||
private var mockedResultQueue: [DownloadMockResult]
|
||||
|
||||
override init() {
|
||||
mockedResultQueue = []
|
||||
}
|
||||
|
||||
func queueMockResult(_ result: DownloadMockResult) {
|
||||
mockedResultQueue.append(result)
|
||||
}
|
||||
|
||||
var queueIsEmpty: Bool {
|
||||
get {
|
||||
return mockedResultQueue.isEmpty
|
||||
}
|
||||
}
|
||||
|
||||
override func downloadTask(with url: URL, completionHandler: @escaping (URL?, URLResponse?, Error?) -> Void) -> URLSessionDownloadTask {
|
||||
|
||||
if(mockedResultQueue.count <= 0) {
|
||||
fatalError("No mocked results have been provided for this downloadTask call!")
|
||||
} else {
|
||||
return URLSessionDownloadTaskMock {
|
||||
let response = self.mockedResultQueue.removeFirst()
|
||||
|
||||
// A bit of white-box testing - we know that HTTPDownloader doesn't actually examine
|
||||
// the URLResponse headers, so we don't provide any.
|
||||
completionHandler(response.location, nil, response.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,11 @@ import Foundation
|
|||
* It's a common Swift pattern, used because enums can't be instantiated.
|
||||
*/
|
||||
enum TestUtils {
|
||||
// Sets up an internally-nested "namespace".
|
||||
enum Downloading {
|
||||
|
||||
}
|
||||
|
||||
static let mainBundle = Bundle(for: FileManagementTests.self) // Can't use TestUtils, as it's an enum
|
||||
|
||||
static func findSubBundle(forResource resource: String, ofType type: String) -> Bundle {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue