// // AssociatingPackageInstallerTests.swift // KeymanEngineTests // // Created by Joshua Horton on 8/6/20. // Copyright © 2020 SIL International. All rights reserved. // import XCTest @testable import KeymanEngine class AssociatingPackageInstallerTests: XCTestCase { var mockedURLSession: TestUtils.Downloading.URLSessionMock! var downloadManager: ResourceDownloadManager! let mockedSearchCallback: LanguagePickAssociator.AssociationSearcher = { lgCodes, callback in var searchResult: [String: (KeymanPackage.Key, URL)?] = [:] // We aren't downloading these models, so a placeholder URL is fine. let placeholderURL = URL.init(string: "http://place.holder.com")! lgCodes.forEach { lgCode in switch lgCode { case "en": searchResult["en"] = (TestUtils.Packages.Keys.nrc_en_mtnt, placeholderURL) case "str": searchResult["str"] = (TestUtils.Packages.Keys.nrc_str_sencoten, placeholderURL) default: searchResult[lgCode] = nil } } callback(searchResult) } override func setUp() { // Ensure Manager's standard init() occurs before our tests. // Otherwise, if our first Manager reference is DURING a test, we'll see unwanted behavior from it. _ = Manager.shared // Resets resource directories for a clean slate. // We'll be testing against actually-installed packages. TestUtils.standardTearDown() mockedURLSession = TestUtils.Downloading.URLSessionMock() downloadManager = ResourceDownloadManager(session: mockedURLSession!, autoExecute: true) } override func tearDownWithError() throws { TestUtils.standardTearDown() let queueWasCleared = mockedURLSession!.queueIsEmpty mockedURLSession = nil if !queueWasCleared { throw NSError(domain: "Keyman", code: 4, userInfo: [NSLocalizedDescriptionKey: "A test did not fully utilize its queued mock results!"]) } } func testKeyboardNoAssociations() throws { guard let package = try ResourceFileManager.shared.prepareKMPInstall(from: TestUtils.Keyboards.silEuroLatinKMP) as? KeyboardKeymanPackage else { XCTFail() return } let startExpectation = XCTestExpectation() let completeExpectation = XCTestExpectation() let installer = AssociatingPackageInstaller(for: package, defaultLanguageCode: "en", downloadManager: downloadManager) { status in if status == .starting { startExpectation.fulfill() } else if status == .complete { completeExpectation.fulfill() } else if status == .cancelled { XCTFail() } } installer.pickLanguages(withCodes: Set(["en"])) wait(for: [startExpectation, completeExpectation], timeout: 5) guard let userKeyboards = Storage.active.userDefaults.userKeyboards else { XCTFail() return } XCTAssertTrue(userKeyboards.contains { $0.fullID == TestUtils.Keyboards.sil_euro_latin.fullID }) XCTAssertNil(Storage.active.userDefaults.userLexicalModels) XCTAssertEqual(ResourceFileManager.shared.installState(forPackage: TestUtils.Packages.Keys.sil_euro_latin), .installed) XCTAssertEqual(ResourceFileManager.shared.installState(forPackage: TestUtils.Packages.Keys.nrc_en_mtnt), .none) } func testLexicalModelNoAssociations() throws { guard let package = try ResourceFileManager.shared.prepareKMPInstall(from: TestUtils.LexicalModels.mtntKMP) as? LexicalModelKeymanPackage else { XCTFail() return } let startExpectation = XCTestExpectation() let completeExpectation = XCTestExpectation() let installer = AssociatingPackageInstaller(for: package, defaultLanguageCode: "en", downloadManager: downloadManager) { status in if status == .starting { startExpectation.fulfill() } else if status == .complete { completeExpectation.fulfill() } else if status == .cancelled { XCTFail() } } installer.pickLanguages(withCodes: Set(["en"])) wait(for: [startExpectation, completeExpectation], timeout: 5) guard let userLexicalModels = Storage.active.userDefaults.userLexicalModels else { XCTFail() return } XCTAssertTrue(userLexicalModels.contains { $0.fullID == TestUtils.LexicalModels.mtnt.fullID }) XCTAssertNil(Storage.active.userDefaults.userKeyboards) XCTAssertEqual(ResourceFileManager.shared.installState(forPackage: TestUtils.Packages.Keys.nrc_en_mtnt), .installed) XCTAssertEqual(ResourceFileManager.shared.installState(forPackage: TestUtils.Packages.Keys.sil_euro_latin), .none) } func testKeyboardWithAssociatedLexicalModels() throws { let mockedQuery = TestUtils.Downloading.MockResult(location: TestUtils.Queries.model_case_en, error: nil) let mockedDownload = TestUtils.Downloading.MockResult(location: TestUtils.LexicalModels.mtntKMP, error: nil) mockedURLSession.queueMockResult(.data(mockedQuery)) mockedURLSession.queueMockResult(.download(mockedDownload)) guard let package = try ResourceFileManager.shared.prepareKMPInstall(from: TestUtils.Keyboards.silEuroLatinKMP) as? KeyboardKeymanPackage else { XCTFail() return } let startExpectation = XCTestExpectation() let completeExpectation = XCTestExpectation() let installer = AssociatingPackageInstaller(for: package, defaultLanguageCode: "en", downloadManager: downloadManager, withAssociators: [ .lexicalModels ]) { status in if status == .starting { startExpectation.fulfill() } else if status == .complete { completeExpectation.fulfill() } else if status == .cancelled { XCTFail() } } installer.pickLanguages(withCodes: Set(["en"])) wait(for: [startExpectation, completeExpectation], timeout: 5) guard let userKeyboards = Storage.active.userDefaults.userKeyboards, let userLexicalModels = Storage.active.userDefaults.userLexicalModels else { XCTFail() return } XCTAssertTrue(userKeyboards.contains { $0.fullID == TestUtils.Keyboards.sil_euro_latin.fullID }) XCTAssertTrue(userLexicalModels.contains { $0.fullID == TestUtils.LexicalModels.mtnt.fullID} ) XCTAssertEqual(ResourceFileManager.shared.installState(forPackage: TestUtils.Packages.Keys.sil_euro_latin), .installed) XCTAssertEqual(ResourceFileManager.shared.installState(forPackage: TestUtils.Packages.Keys.nrc_en_mtnt), .installed) } func testCancellationNoAssociations() throws { guard let package = try ResourceFileManager.shared.prepareKMPInstall(from: TestUtils.Keyboards.silEuroLatinKMP) as? KeyboardKeymanPackage else { XCTFail() return } let cancelExpectation = XCTestExpectation() let installer = AssociatingPackageInstaller(for: package, defaultLanguageCode: "en", downloadManager: downloadManager) { status in if status == .cancelled { // When the user 'cancels' language selection, this installer should also // signal cancellation. cancelExpectation.fulfill() } else { XCTFail() } } // A bit of white-box testing - sets up the closure framework installer.initializeSynchronizationGroups() // And the install closure, which we'll send a 'cancel' signal to. let installClosure = installer.coreInstallationClosure() installClosure(nil) wait(for: [cancelExpectation], timeout: 5) XCTAssertNil(Storage.active.userDefaults.userKeyboards) XCTAssertNil(Storage.active.userDefaults.userLexicalModels) } func testCancellationWithAssociations() throws { guard let package = try ResourceFileManager.shared.prepareKMPInstall(from: TestUtils.Keyboards.silEuroLatinKMP) as? KeyboardKeymanPackage else { XCTFail() return } // While the query should occur, the installation phase should never be reached. // So, we don't mock the download. let mockedQuery = TestUtils.Downloading.MockResult(location: TestUtils.Queries.model_case_en, error: nil) mockedURLSession.queueMockResult(.data(mockedQuery)) let cancelExpectation = XCTestExpectation() let installer = AssociatingPackageInstaller(for: package, defaultLanguageCode: "en", downloadManager: downloadManager, withAssociators: [.lexicalModels]) { status in if status == .cancelled { // When the user 'cancels' language selection, this installer should also // signal cancellation. cancelExpectation.fulfill() } else { XCTFail() } } // Heavier white-box testing this time. installer.initializeSynchronizationGroups() installer.constructAssociationPickers() installer.associationQueriers?.forEach { $0.pickerInitialized() $0.selectLanguages(Set(["en"])) // calls the expected query // And, since we're modeling cancellation... $0.pickerDismissed() } // Finally, the install closure, which we'll send a 'cancel' signal to. let installClosure = installer.coreInstallationClosure() installClosure(nil) wait(for: [cancelExpectation], timeout: 5) XCTAssertNil(Storage.active.userDefaults.userKeyboards) XCTAssertNil(Storage.active.userDefaults.userLexicalModels) } func testDeinitConstructiveCancellation() throws { guard let package = try ResourceFileManager.shared.prepareKMPInstall(from: TestUtils.Keyboards.silEuroLatinKMP) as? KeyboardKeymanPackage else { XCTFail() return } // While the query should occur, the installation phase should never be reached. // So, we don't mock the download. let mockedQuery = TestUtils.Downloading.MockResult(location: TestUtils.Queries.model_case_en, error: nil) let queryExpectation = XCTestExpectation() mockedURLSession.queueMockResult(.data(mockedQuery), expectation: queryExpectation) let cancelExpectation = XCTestExpectation() do { let installer = AssociatingPackageInstaller(for: package, defaultLanguageCode: "en", downloadManager: downloadManager, withAssociators: [.lexicalModels]) { status in // When the user 'cancels' language selection, this installer should also // signal cancellation. if status == .cancelled { // First, wait to ensure that the standard query did launch. // Otherwise, we might accidentally skip the mocked query! self.wait(for: [queryExpectation], timeout: 4) cancelExpectation.fulfill() } else { XCTFail() } } // Heavier white-box testing this time. installer.initializeSynchronizationGroups() installer.constructAssociationPickers() installer.associationQueriers?.forEach { $0.pickerInitialized() $0.selectLanguages(Set(["en"])) // calls the expected query // De-init will trigger the 'dismiss' signal. } } wait(for: [cancelExpectation], timeout: 5) XCTAssertNil(Storage.active.userDefaults.userKeyboards) XCTAssertNil(Storage.active.userDefaults.userLexicalModels) } func testDeinitUnstarted() throws { guard let package = try ResourceFileManager.shared.prepareKMPInstall(from: TestUtils.Keyboards.silEuroLatinKMP) as? KeyboardKeymanPackage else { XCTFail() return } let cancelExpectation = XCTestExpectation() do { let _ = AssociatingPackageInstaller(for: package, defaultLanguageCode: "en", downloadManager: downloadManager, withAssociators: [.lexicalModels]) { status in // Since language-picking never began, no "cancelled" signal should occur. if status == .cancelled { cancelExpectation.fulfill() } else { XCTFail() } } } wait(for: [cancelExpectation], timeout: 1) XCTAssertNil(Storage.active.userDefaults.userKeyboards) XCTAssertNil(Storage.active.userDefaults.userLexicalModels) } func testPackageLangCodePartialMatch() throws { guard let strPackage = try ResourceFileManager.shared.prepareKMPInstall(from: TestUtils.Keyboards.fvSencotenKMP) as? KeyboardKeymanPackage else { XCTFail() return } guard let eurolatinPackage = try ResourceFileManager.shared.prepareKMPInstall(from: TestUtils.Keyboards.silEuroLatinKMP) as? KeyboardKeymanPackage else { XCTFail() return } let strInstaller = AssociatingPackageInstaller(for: strPackage, defaultLanguageCode: "str", // correct code: str-latn downloadManager: downloadManager) { _ in } // Package does not contain "str", but does contain "str-latn" XCTAssertEqual(strInstaller.defaultLgCode, "str-latn") let eurolatinInstaller = AssociatingPackageInstaller(for: eurolatinPackage, defaultLanguageCode: "en-fake-bcp", // correct code: "en" downloadManager: downloadManager) { _ in } // Package does not contain "en-fake-bcp", but does contain "en". XCTAssertEqual(eurolatinInstaller.defaultLgCode, "en") // Note: the current naive approach isn't exactly BCP-47 subtag aware - it relies on one tag // containing the entire other tag as a substring. } }