// // MigrationTests.swift // KeymanEngineTests // // Created by Joshua Horton on 2/20/20. // Copyright © 2020 SIL International. All rights reserved. // import Foundation @testable import KeymanEngine import XCTest class MigrationTests: XCTestCase { override func tearDown() { TestUtils.standardTearDown() } // // Uncomment this and set up whatever resources are needed for your test bundle in order to build one! // func testForMigrationBundleConstruction() { // do { // if let sil_euro_latin = try ResourceFileManager.shared.prepareKMPInstall(from: TestUtils.Keyboards.silEuroLatinKMP) as? KeyboardKeymanPackage { // try ResourceFileManager.shared.install(resourceWithID: TestUtils.Keyboards.sil_euro_latin.fullID, from: sil_euro_latin) // } else { // XCTFail() // } // // if let sencoten_kbd_kmp = try ResourceFileManager.shared.prepareKMPInstall(from: TestUtils.Keyboards.fvSencotenKMP) as? KeyboardKeymanPackage { // try ResourceFileManager.shared.install(resourceWithID: TestUtils.Keyboards.fv_sencoten.fullID, from: sencoten_kbd_kmp) // } else { // XCTFail() // } // // if let mtntKMP = try ResourceFileManager.shared.prepareKMPInstall(from: TestUtils.LexicalModels.mtntKMP) as? LexicalModelKeymanPackage { // try ResourceFileManager.shared.install(resourceWithID: TestUtils.LexicalModels.mtnt.fullID, // from: mtntKMP) // } else { // XCTFail() // } // // if let sencoten_lm_kmp = try ResourceFileManager.shared.prepareKMPInstall(from: TestUtils.LexicalModels.sencotenKMP) as? LexicalModelKeymanPackage { // try ResourceFileManager.shared.install(resourceWithID: TestUtils.LexicalModels.sencoten.fullID, // from: sencoten_lm_kmp) // } else { // XCTFail() // } // // } catch { // XCTFail("File system commands failed.") // } // // // Set the desired version entry for your format, if different from the actual local version. // // Only use if you're sure the specified version has a matching file format. // Storage.active.userDefaults.lastEngineVersion = Version.packageBasedFileReorg // Storage.active.userDefaults.migrationLevel = 20 // migratedForKMP // // // Note: you may need to manually pause at a certain (marked) point within this method // // for everything to successfully write out! // if let saveState = try? TestUtils.EngineStateBundler.createBundle(withName: "Early 14.0 with defaults") { // self.add(saveState) // } else { // XCTFail() // } // } func testSimpleEarly14Migration() { // A case where the user has both SENCOTEN installed for both keyboard & lexical model. // Mirrors `testNoDefaultsEarly14Migration`, but where sil_euro_latin // was not removed by the user; in contrast, the base resources // should be auto-updated. TestUtils.Migrations.applyBundleToFileSystem(TestUtils.Migrations.simple_14) Migrations.migrate(storage: Storage.active) Migrations.updateResources(storage: Storage.active) let userDefaults = Storage.active.userDefaults let userKeyboards = userDefaults.userKeyboards ?? [] let userLexicalModels = userDefaults.userLexicalModels ?? [] XCTAssertEqual(userKeyboards.count, 2) // Because there's a lexical model update, it installs the whole package. // Not exactly ideal, but correct for pre-existing behavior, which // installs the whole default package instead of a single language-code pairing. XCTAssertEqual(userLexicalModels.count, 4) //[s]il_[e]uro_[l]atin let kbdSEL = userKeyboards.first(where: { $0.fullID == TestUtils.Keyboards.sil_euro_latin.fullID }) XCTAssertNotNil(kbdSEL) // Because there's a keyboard update (1.9.3 vs 1.9.1, at the time of writing). // we expect a more recent version than the testing version. XCTAssertGreaterThan(Version(kbdSEL!.version)!, Version(TestUtils.Keyboards.sil_euro_latin.version)!) XCTAssertTrue(userLexicalModels.contains(where: { $0.fullID == TestUtils.LexicalModels.mtnt.fullID })) XCTAssertTrue(userKeyboards.contains(where: { $0.fullID == TestUtils.Keyboards.fv_sencoten.fullID })) XCTAssertTrue(userLexicalModels.contains(where: { $0.fullID == TestUtils.LexicalModels.sencoten.fullID })) } func testNoDefaultsEarly14Migration() { // A case where the user only has SENCOTEN installed for both keyboard & lexical model. // sil_euro_latin was explicitly removed by the user. TestUtils.Migrations.applyBundleToFileSystem(TestUtils.Migrations.noDefault_14) Migrations.migrate(storage: Storage.active) Migrations.updateResources(storage: Storage.active) let userDefaults = Storage.active.userDefaults let userKeyboards = userDefaults.userKeyboards ?? [] let userLexicalModels = userDefaults.userLexicalModels ?? [] XCTAssertEqual(userKeyboards.count, 1) XCTAssertEqual(userLexicalModels.count, 1) XCTAssertFalse(userKeyboards.contains(where: { $0.fullID == TestUtils.Keyboards.sil_euro_latin.fullID })) XCTAssertFalse(userLexicalModels.contains(where: { $0.fullID == TestUtils.LexicalModels.mtnt.fullID })) XCTAssertTrue(userKeyboards.contains(where: { $0.fullID == TestUtils.Keyboards.fv_sencoten.fullID })) XCTAssertTrue(userLexicalModels.contains(where: { $0.fullID == TestUtils.LexicalModels.sencoten.fullID })) } func testComplexVersion13Migration() { TestUtils.Migrations.applyBundleToFileSystem(TestUtils.Migrations.cloud_to_kmp_13) Migrations.migrate(storage: Storage.active) Migrations.updateResources(storage: Storage.active) let userDefaults = Storage.active.userDefaults let userKeyboards = userDefaults.userKeyboards // The fun thing to test here - sil_euro_latin is first wrapped with an auto-generated // kmp.json, THEN updated with the actual KMP. let sil_euro_latin_kbds = userKeyboards!.filter { return $0.id == "sil_euro_latin" } sil_euro_latin_kbds.forEach { XCTAssertEqual($0.packageID, "sil_euro_latin") XCTAssertEqual($0.version, Defaults.keyboard.version) } let sil_euro_latin_package = ResourceFileManager.shared.installedPackages.first(where: { $0.id == "sil_euro_latin" }) as! KeyboardKeymanPackage XCTAssertFalse(sil_euro_latin_package.metadata.isAutogeneratedWrapper) // As it's installing a true package, there will be files existing here that won't exist // for the others. let validationFilenames = ["kmp.inf", "readme.htm", "welcome.htm", "sil_euro_latin.kmx", "DejaVuSans.ttf", "currency.png", "usage.htm"] validationFilenames.forEach { let file = sil_euro_latin_package.sourceFolder.appendingPathComponent($0) XCTAssertTrue(FileManager.default.fileExists(atPath: file.path), "Expected file \(file.path) missing") } } func testVersion13ResourceMigration() { TestUtils.Migrations.applyBundleToFileSystem(TestUtils.Migrations.simple_13) Migrations.updateResources(storage: Storage.active) let userDefaults = Storage.active.userDefaults // SIL EuroLatin should be updated to the currently-bundled version. The lexical model version should be unchanged. let defaultKbd = userDefaults.userKeyboards![0] XCTAssertEqual(defaultKbd.id, Defaults.keyboard.id) XCTAssertEqual(defaultKbd.version, Defaults.keyboard.version) let keyboardURL = Storage.active.resourceURL(for: Defaults.keyboard)! XCTAssert(FileManager.default.fileExists(atPath: keyboardURL.path)) } func testVersion13CloudToKMPMigration() throws { TestUtils.Migrations.applyBundleToFileSystem(TestUtils.Migrations.cloud_to_kmp_13) let initialUserKeyboards = Storage.active.userDefaults.userKeyboards! let initialUserModels = Storage.active.userDefaults.userLexicalModels! // A baseline load-check; confirms we have the correct resource count at the start. XCTAssertEqual(initialUserKeyboards.count, 7) XCTAssertEqual(initialUserModels.count, 6) try Migrations.migrateCloudResourcesToKMPFormat() // Now that migration has completed, it's time to check the results. let userKeyboards = Storage.active.userDefaults.userKeyboards! let userModels = Storage.active.userDefaults.userLexicalModels! // Pass 1: do we have a matching set of resources after the migration? XCTAssertEqual(userKeyboards.count, 7) XCTAssertEqual(userModels.count, 6) userKeyboards.forEach{ kbd in XCTAssertTrue(initialUserKeyboards.contains(where: { $0.fullID == kbd.fullID })) XCTAssertEqual(kbd.packageID, kbd.id) } userModels.forEach{ lm in XCTAssertTrue(initialUserModels.contains(where: { $0.fullID == lm.fullID })) XCTAssertEqual(lm.packageID, lm.id) } // Pass 2: do we get the expected package metadata and file structure? let installedPackages = ResourceFileManager.shared.installedPackages installedPackages.forEach { package in // A bit lazy, but at least we'll know which package(s) the error occurs for. let generalMessage = "Unexpected value for package \(package.id)" XCTAssertEqual(package.resources.count, 1, generalMessage) let metadataFile = package.sourceFolder.appendingPathComponent("kmp.json") XCTAssertTrue(FileManager.default.fileExists(atPath: metadataFile.path), generalMessage) // "sil_sahu" has a version-matched locally-cached KMP, so it should be migrated based on // that local KMP. if package.id == "sil_sahu" { XCTAssertFalse(package.metadata.isAutogeneratedWrapper, generalMessage) XCTAssertNotEqual(package.version, Version(KMPInfo.AUTOGEN_VERSION.description), generalMessage) XCTAssertEqual(package.version, Version("1.0")) // The version of the stored sil_sahu package. // As it's installing a true package, there will be files existing here that won't exist // for the others. let validationFilenames = ["kmp.inf", "readme.htm", "welcome.htm", "sil_sahu.kmx"] validationFilenames.forEach { let file = package.sourceFolder.appendingPathComponent($0) XCTAssertTrue(FileManager.default.fileExists(atPath: file.path), "Expected file \(file.path) missing for package \(package.id)") } // The others get autogenerated version wrappers. // Even sil_cameroon_qwerty, which has a locally-cached KMP... but the version's out of date. } else { XCTAssertTrue(package.metadata.isAutogeneratedWrapper, generalMessage) XCTAssertEqual(package.version, Version(KMPInfo.AUTOGEN_VERSION.description), generalMessage) } // .installableResourceSets[0] -> the set of LanguageResources represented by the // one expected script-file-resource in each 'package'. switch package.id { case "sil_euro_latin": XCTAssertEqual(package.installableResourceSets[0].count, 2, generalMessage) case "nrc.en.mtnt": XCTAssertEqual(package.installableResourceSets[0].count, 3, generalMessage) case "nrc.str.sencoten": XCTAssertEqual(package.installableResourceSets[0].count, 2, generalMessage) default: XCTAssertEqual(package.installableResourceSets[0].count, 1, generalMessage) } } } func testVersion12ResourceMigration() { TestUtils.Migrations.applyBundleToFileSystem(TestUtils.Migrations.simple_12) Migrations.updateResources(storage: Storage.active) let userDefaults = Storage.active.userDefaults // The two keyboards should remain in the same location, while the lexical model should be upgraded. // v 0.1.2 -> 0.1.4. // Minor surprise (as of 13.0) is that 0.1.2's file actually remains! let defaultModel = userDefaults.userLexicalModels![0] XCTAssertEqual(defaultModel.id, Defaults.lexicalModel.id) XCTAssertEqual(defaultModel.version, Defaults.lexicalModel.version) let modelURL = Storage.active.lexicalModelURL(for: TestUtils.LexicalModels.mtnt) // The bundled, 0.1.4 version. XCTAssert(FileManager.default.fileExists(atPath: modelURL.path)) } func testVersion12AdhocMigration() throws { TestUtils.Migrations.applyBundleToFileSystem(TestUtils.Migrations.adhoc_12) Storage.active.userDefaults.lastEngineVersion = Version.firstTracked Migrations.migrate(storage: Storage.active) // The files in the .documents directory should be erased after this method is run. let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] let documentsContents = try FileManager.default.contentsOfDirectory(atPath: documentsDirectory.path) XCTAssertEqual(documentsContents.count, 1, "Documents directory was not purged of installation by-products during ad-hoc resource migration!") // The .kmp.zip is converted back into its original .kmp file, which remains in the Documents directory // so that it may be used for sharing. let kmpURL = URL(fileURLWithPath: documentsContents[0]) XCTAssertEqual(kmpURL.lastPathComponent, "khmer10.kmp") } func testVersion10ResourceMigration() { TestUtils.Migrations.applyBundleToFileSystem(TestUtils.Migrations.simple_10) Storage.active.userDefaults.lastEngineVersion = nil Migrations.updateResources(storage: Storage.active) let userDefaults = Storage.active.userDefaults // The old keyboard should be gone entirely, replaced by the current version's default resources. // There should only be one default resource per type (at least, as of 13.0.) XCTAssertEqual(userDefaults.userKeyboards!.count, 1, "Unexpected keyboard count after migration!") // 3, b/c all language-code variants are counted. XCTAssertEqual(userDefaults.userLexicalModels!.count, 3, "Unexpected lexical model count after migration!") let defaultKeyboard = userDefaults.userKeyboards![0] XCTAssertNotEqual(TestUtils.Migrations.european2.id, defaultKeyboard.id) // Double-ensure that the ID is new. XCTAssertEqual(defaultKeyboard.id, Defaults.keyboard.id) XCTAssertEqual(defaultKeyboard.version, Defaults.keyboard.version) let defaultModel = userDefaults.userLexicalModels![0] XCTAssertEqual(defaultModel.id, Defaults.lexicalModel.id) XCTAssertEqual(defaultModel.version, Defaults.lexicalModel.version) let keyboardURL = Storage.active.keyboardURL(for: Defaults.keyboard) let modelURL = Storage.active.lexicalModelURL(for: Defaults.lexicalModel) XCTAssert(FileManager.default.fileExists(atPath: keyboardURL.path)) XCTAssert(FileManager.default.fileExists(atPath: modelURL.path)) } func testNoDefaultVersion10ResourceMigration() { TestUtils.Migrations.applyBundleToFileSystem(TestUtils.Migrations.noDefault_10) Migrations.updateResources(storage: Storage.active) Storage.active.userDefaults.lastEngineVersion = nil let userDefaults = Storage.active.userDefaults // No new resources should be installed - only what was originally present should be there. XCTAssertEqual(userDefaults.userKeyboards!.count, 1, "Unexpected keyboard count after migration!") // There was no prior lexical model installed, so the installation still proceeds. The user just won't see it. XCTAssertNil(userDefaults.userLexicalModels, "Unexpected lexical model(s) installed after migration!") let keyboard = userDefaults.userKeyboards![0] XCTAssertEqual(keyboard.id, TestUtils.Keyboards.khmer_angkor.id, "Unexpected keyboard was installed after migration!") XCTAssertEqual(keyboard.version, TestUtils.Keyboards.khmer_angkor.version) // If successful, confirms that the current default resources are not installed - as expected. } }