Merge pull request #2692 from keymanapp/feat/ios/migration-unit-tests

feat(ios): KeymanEngine version migration unit tests
This commit is contained in:
Joshua Horton 2020-02-26 20:46:37 +07:00 • committed by GitHub
commit 4190d41761
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 622 additions and 1 deletions

View file

@ -173,8 +173,14 @@
CE8EDEB323F53F96009E1FF6 /* VersionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8EDEB223F53F96009E1FF6 /* VersionTests.swift */; };
CE976D4F23FA380700FFDF3A /* Reachability.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A0FC9FC22D66D9E00D33F86 /* Reachability.framework */; };
CE976D5023FA380700FFDF3A /* Reachability.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9A0FC9FC22D66D9E00D33F86 /* Reachability.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
CE9B440C23FE49F000499CAB /* Migrations.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9B440B23FE49F000499CAB /* Migrations.swift */; };
CE9B440F23FE4E7500499CAB /* 12.0 Ad-hoc Migration.bundle in Resources */ = {isa = PBXBuildFile; fileRef = CE9B440D23FE4E7400499CAB /* 12.0 Ad-hoc Migration.bundle */; };
CE9B441023FE4E7500499CAB /* Simple 12.0 Migration.bundle in Resources */ = {isa = PBXBuildFile; fileRef = CE9B440E23FE4E7500499CAB /* Simple 12.0 Migration.bundle */; };
CE9CD88023FCC1CA002BF2F8 /* TestUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9CD87F23FCC1CA002BF2F8 /* TestUtils.swift */; };
CE9CD88323FCC4E3002BF2F8 /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9CD88223FCC4E3002BF2F8 /* UserDefaults.swift */; };
CEDFEF8F23FE43B700BECF39 /* MigrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDFEF8E23FE43B700BECF39 /* MigrationTests.swift */; };
CEF888D223FE6ADF00667693 /* No-defaults 10.0 Migration.bundle in Resources */ = {isa = PBXBuildFile; fileRef = CEF888D023FE6ADE00667693 /* No-defaults 10.0 Migration.bundle */; };
CEF888D323FE6ADF00667693 /* Simple 10.0 Migration.bundle in Resources */ = {isa = PBXBuildFile; fileRef = CEF888D123FE6ADF00667693 /* Simple 10.0 Migration.bundle */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -428,10 +434,16 @@
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>"; };
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>"; };
CE9B440E23FE4E7500499CAB /* Simple 12.0 Migration.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = "Simple 12.0 Migration.bundle"; sourceTree = "<group>"; };
CE9CD87F23FCC1CA002BF2F8 /* TestUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestUtils.swift; sourceTree = "<group>"; };
CE9CD88223FCC4E3002BF2F8 /* UserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaults.swift; sourceTree = "<group>"; };
CECB38931F2199BC0098882F /* Reachability.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Reachability.h; path = KeymanEngine/lib/Reachability/Reachability.h; sourceTree = SOURCE_ROOT; };
CECB38941F2199BC0098882F /* Reachability.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Reachability.m; path = KeymanEngine/lib/Reachability/Reachability.m; sourceTree = SOURCE_ROOT; };
CEDFEF8E23FE43B700BECF39 /* MigrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationTests.swift; sourceTree = "<group>"; };
CEF888D023FE6ADE00667693 /* No-defaults 10.0 Migration.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = "No-defaults 10.0 Migration.bundle"; sourceTree = "<group>"; };
CEF888D123FE6ADF00667693 /* Simple 10.0 Migration.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = "Simple 10.0 Migration.bundle"; sourceTree = "<group>"; };
F243887E14BBD43000A3E055 /* KeymanEngineDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = KeymanEngineDemo.app; sourceTree = BUILT_PRODUCTS_DIR; };
F243889014BBD43100A3E055 /* KeymanEngine-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "KeymanEngine-Prefix.pch"; sourceTree = "<group>"; };
F27FCAEE157FD59100FBBA20 /* Keyman-iphoneos.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Keyman-iphoneos.xcconfig"; sourceTree = "<group>"; };
@ -658,6 +670,7 @@
9A079DD3223194B100581263 /* Info.plist */,
CE8EDEB223F53F96009E1FF6 /* VersionTests.swift */,
CE4459CD23FBBA8D003151FD /* AppDelegate.swift */,
CEDFEF8E23FE43B700BECF39 /* MigrationTests.swift */,
);
path = KeymanEngineTests;
sourceTree = "<group>";
@ -765,6 +778,10 @@
CE37C38923FCC9AF007031C6 /* resources */ = {
isa = PBXGroup;
children = (
CEF888D023FE6ADE00667693 /* No-defaults 10.0 Migration.bundle */,
CEF888D123FE6ADF00667693 /* Simple 10.0 Migration.bundle */,
CE9B440D23FE4E7400499CAB /* 12.0 Ad-hoc Migration.bundle */,
CE9B440E23FE4E7500499CAB /* Simple 12.0 Migration.bundle */,
CE37C38A23FCC9EE007031C6 /* Keyboards.bundle */,
CE37C38E23FCD52F007031C6 /* Lexical Models.bundle */,
);
@ -798,6 +815,7 @@
CE9CD88223FCC4E3002BF2F8 /* UserDefaults.swift */,
CE37C38C23FCD41E007031C6 /* Keyboards.swift */,
CE37C39023FCD617007031C6 /* LexicalModels.swift */,
CE9B440B23FE49F000499CAB /* Migrations.swift */,
);
path = TestUtils;
sourceTree = "<group>";
@ -1144,8 +1162,12 @@
files = (
9AD4F53F22A8A286007992D3 /* LanguageSettingsViewController.xib in Resources */,
CE37C38B23FCC9EE007031C6 /* Keyboards.bundle in Resources */,
CE9B441023FE4E7500499CAB /* Simple 12.0 Migration.bundle in Resources */,
9ADC45A022E1895D004C78C6 /* LanguageLMDetailViewController.xib in Resources */,
CEF888D223FE6ADF00667693 /* No-defaults 10.0 Migration.bundle in Resources */,
CE9B440F23FE4E7500499CAB /* 12.0 Ad-hoc Migration.bundle in Resources */,
9A0FCA0922D7C58B00D33F86 /* Keyman.bundle in Resources */,
CEF888D323FE6ADF00667693 /* Simple 10.0 Migration.bundle in Resources */,
CE37C38F23FCD52F007031C6 /* Lexical Models.bundle in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -1241,10 +1263,12 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
CE9B440C23FE49F000499CAB /* Migrations.swift in Sources */,
CE37C39123FCD617007031C6 /* LexicalModels.swift in Sources */,
CE37C38D23FCD41E007031C6 /* Keyboards.swift in Sources */,
CE9CD88323FCC4E3002BF2F8 /* UserDefaults.swift in Sources */,
CE8EDEB323F53F96009E1FF6 /* VersionTests.swift in Sources */,
CEDFEF8F23FE43B700BECF39 /* MigrationTests.swift in Sources */,
CE9CD88023FCC1CA002BF2F8 /* TestUtils.swift in Sources */,
CE8EDEB123F53D1A009E1FF6 /* FileManagementTests.swift in Sources */,
);

View file

@ -16,6 +16,7 @@ extension Storage {
if paths.isEmpty {
return nil
}
log.info("\(paths[0])")
return Storage(baseURL: paths[0], userDefaults: UserDefaults.standard)
}()

View file

@ -0,0 +1,111 @@
//
// 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()
}
func testVersion12ResourceMigration() {
TestUtils.Migrations.applyBundleToFileSystem(TestUtils.Migrations.simple_12)
let userDefaults = Storage.active.userDefaults
userDefaults.lastEngineVersion = Version("12.0")!
let versionResources = TestUtils.Migrations.getVersionHistory(for: Version("12.0")!)
TestUtils.UserDefaults.addKeyboard(versionResources[0] as! InstallableKeyboard)
TestUtils.UserDefaults.addKeyboard(TestUtils.Keyboards.khmer_angkor)
TestUtils.UserDefaults.addLexicalModel(versionResources[1] as! InstallableLexicalModel)
Migrations.updateResources(storage: Storage.active)
// 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)
let userDefaults = Storage.active.userDefaults
userDefaults.lastEngineVersion = Version("12.0")!
TestUtils.UserDefaults.addKeyboard(TestUtils.Keyboards.khmer10)
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)
let userDefaults = Storage.active.userDefaults
TestUtils.UserDefaults.addKeyboard(TestUtils.Migrations.european2)
Migrations.updateResources(storage: Storage.active)
// 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)
let userDefaults = Storage.active.userDefaults
TestUtils.UserDefaults.addKeyboard(TestUtils.Keyboards.khmer_angkor)
Migrations.updateResources(storage: Storage.active)
// 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.
}
}

View file

@ -7,9 +7,28 @@
//
import Foundation
@testable import KeymanEngine
extension TestUtils {
enum Keyboards {
static let khmerAngkorKMP = TestUtils.keyboardsBundle.url(forResource: "khmer_angkor", withExtension: "kmp")!
static let khmer_angkor = InstallableKeyboard(id: "khmer_angkor",
name: "Khmer Angkor",
languageID: "kh",
languageName: "Khmer (Central)",
version: "1.0.6",
isRTL: false,
font: nil,
oskFont: nil,
isCustom: false)
static let khmer10 = InstallableKeyboard(id: "khmer10",
name: "Khmer (NiDA)",
languageID: "kh",
languageName: "Khmer (Central)",
version: "1.3",
isRTL: false,
font: nil,
oskFont: nil,
isCustom: false)
}
}

View file

@ -7,9 +7,11 @@
//
import Foundation
@testable import KeymanEngine
extension TestUtils {
enum LexicalModels {
static let mtntKMP = TestUtils.lexicalModelsBundle.url(forResource: "nrc.en.mtnt.model", withExtension: "kmp")!
static let mtnt = Defaults.lexicalModel
}
}

View file

@ -0,0 +1,82 @@
//
// Migrations.swift
// KeymanEngineTests
//
// Created by Joshua Horton on 2/20/20.
// Copyright © 2020 SIL International. All rights reserved.
//
import Foundation
@testable import KeymanEngine
import XCTest
extension TestUtils {
enum Migrations {
// Important locations within Migration-test .bundles
static let documentsFolder = "Documents"
static let baseFolder = "Library/keyman"
// Bundle references
static let simple_12 = TestUtils.findSubBundle(forResource: "Simple 12.0 Migration", ofType: ".bundle")
static let adhoc_12 = TestUtils.findSubBundle(forResource: "12.0 Ad-hoc Migration", ofType: ".bundle")
static let simple_10 = TestUtils.findSubBundle(forResource: "Simple 10.0 Migration", ofType: ".bundle")
static let noDefault_10 = TestUtils.findSubBundle(forResource: "No-defaults 10.0 Migration", ofType: ".bundle")
// The default keyboard in version 10.0.
static let european2_font = Font(family: "LatinWeb", source: ["DejaVuSans.ttf"], size: nil)
static let european2 = InstallableKeyboard(id: "european2",
name: "EuroLatin2 Keyboard",
languageID: "en",
languageName: "English",
version: "1.6",
isRTL: false,
font: european2_font,
oskFont: nil,
isCustom: false)
static func applyBundleToFileSystem(_ bundle: Bundle) {
let fileManager = FileManager.default
let bundlePath = URL(fileURLWithPath: bundle.resourcePath!)
let documentsSource = bundlePath.appendingPathComponent(documentsFolder)
let documentsDestination = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
let baseSource = bundlePath.appendingPathComponent(baseFolder)
let baseDestination = fileManager.urls(for: .libraryDirectory,
in: .userDomainMask)[0]
.appendingPathComponent("keyman")
do {
// Avoid .replaceItemAt - it'll remove the files from the source Bundle!
if fileManager.fileExists(atPath: documentsSource.path) {
if fileManager.fileExists(atPath: documentsDestination.path) {
try fileManager.removeItem(at: documentsDestination)
}
try fileManager.copyItem(at: documentsSource, to: documentsDestination)
}
if fileManager.fileExists(atPath: baseDestination.path) {
try fileManager.removeItem(at: baseDestination)
}
try fileManager.copyItem(at: baseSource, to: baseDestination)
} catch {
XCTFail("Could not properly set the state for a migration test: \(error)")
}
}
static func getVersionHistory(for version: Version) -> [LanguageResource] {
let resourceHistory = KeymanEngine.Migrations.resourceHistory
let match: [[LanguageResource]] = resourceHistory.compactMap() { entry in
if entry.version == version {
return entry.resources
} else {
return nil
}
}
return match[0]
}
}
}

View file

@ -18,7 +18,7 @@ import Foundation
enum TestUtils {
static let mainBundle = Bundle(for: FileManagementTests.self) // Can't use TestUtils, as it's an enum
private static func findSubBundle(forResource resource: String, ofType type: String) -> Bundle {
static func findSubBundle(forResource resource: String, ofType type: String) -> Bundle {
return Bundle(path: mainBundle.path(forResource: resource, ofType: type)!)!
}

View file

@ -45,5 +45,35 @@ extension TestUtils {
userDefaults.synchronize()
userDefaults.removePersistentDomain(forName: domain)
}
static func addKeyboard(_ keyboard: InstallableKeyboard) {
// Lifted from Manager.addKeyboard
let userDefaults = Storage.active.userDefaults
var userKeyboards = userDefaults.userKeyboards ?? []
// Update keyboard if it exists
if let index = userKeyboards.firstIndex(where: { $0.fullID == keyboard.fullID }) {
userKeyboards[index] = keyboard
} else {
userKeyboards.append(keyboard)
}
userDefaults.userKeyboards = userKeyboards
}
static func addLexicalModel(_ lexicalModel: InstallableLexicalModel) {
// Lifted from Manager.addKeyboard
let userDefaults = Storage.active.userDefaults
var userModels = userDefaults.userLexicalModels ?? []
// Update keyboard if it exists
if let index = userModels.firstIndex(where: { $0.fullID == lexicalModel.fullID }) {
userModels[index] = lexicalModel
} else {
userModels.append(lexicalModel)
}
userDefaults.userLexicalModels = userModels
}
}
}

View file

@ -0,0 +1 @@
{"system":{"keymanDeveloperVersion":"12.0.1500.0","fileVersion":"12.0"},"options":{"followKeyboardVersion":true},"info":{"author":{"description":"Eddie Antonio Santos","url":"mailto:easantos@ualberta.ca"},"copyright":{"description":"© 2019 National Research Council Canada"},"name":{"description":"English language model mined from MTNT"},"version":{"description":"0.1.2"}},"files":[{"name":"nrc.en.mtnt.model.js","description":"Lexical model nrc.en.mtnt.model.js","copyLocation":"0","fileType":".model.js"}],"lexicalModels":[{"name":"English dictionary (MTNT)","id":"nrc.en.mtnt","version":"0.1.2","languages":[{"name":"English","id":"en"},{"name":"English (US)","id":"en-us"},{"name":"English (Canada)","id":"en-ca"}]}]}