feat(ios/engine): package contents auto-update matching installed resources

This commit is contained in:
jahorton 2020-06-16 12:50:15 +07:00
parent 38771d4a2a
commit 2361fa1871
8 changed files with 104 additions and 5 deletions

View file

@ -134,6 +134,16 @@ public extension UserDefaults {
}
}
func userResources<Resource: LanguageResource>(ofType: Resource.Type) -> [Resource]? {
if ofType == InstallableKeyboard.self {
return (userKeyboards as? [Resource])
} else if ofType == InstallableLexicalModel.self {
return (userLexicalModels as? [Resource])
} else {
return nil
}
}
var userResources: [AnyLanguageResource]? {
get {
let keyboards = userKeyboards ?? []

View file

@ -25,6 +25,12 @@ extension LanguageResourceFullID where Self: Equatable {
}
}
extension LanguageResourceFullID {
var description: String {
return "{\(type): {id = \(id), languageID=\(languageID)}}"
}
}
// Alas, 'associatedtype' stuff isn't exactly generic, and it's impossible to wildcard.
// So, this supports Swift's "type erasure" pattern, acting as a "wildcarded"
// LanguageResource that doesn't care about the specific associatedtype(s) that

View file

@ -222,19 +222,52 @@ public class ResourceFileManager {
resourceWithID fullID: ResourceType.FullID,
from package: PackageType) throws {
guard let resource = package.findResource(withID: fullID) else {
try install(resourcesWithIDs: [fullID], from: package)
}
public func install<Resource: LanguageResource,
Package: TypedKeymanPackage<Resource>> (
resourcesWithIDs fullIDs: [Resource.FullID], from package: Package) throws {
if fullIDs.contains(where: { package.findResource(withID: $0) == nil }) {
let missingResource = fullIDs.first(where: { package.findResource(withID: $0) == nil })!
log.error("Resource with full ID \(missingResource.description) not in package")
throw KMPError.resourceNotInPackage
}
do {
try copyWithOverwrite(from: package.sourceFolder,
to: Storage.active.resourceDir(for: resource)!)
to: Storage.active.packageDir(for: package)!)
} catch {
log.error("Could not create installation directory and/or copy resources: \(error)")
throw KMPError.fileSystem
}
addResource(resource)
let updatables = findPotentialUpdates(in: package).map { return $0.typedFullID }
let fullList = fullIDs + updatables
fullList.forEach { addResource(package.findResource(withID: $0)!) }
}
internal func findPotentialUpdates<Resource: LanguageResource,
Package: TypedKeymanPackage<Resource>> (
in package: Package,
ignoring resourcesToIgnore: [Resource] = []) -> [Resource] {
let installedResources = Storage.active.userDefaults.userResources(ofType: Resource.self) ?? []
var updatableResources: [Resource] = []
installedResources.forEach { resource in
// If there's no package ID, default to the resource's ID.
// If the package ID matches the resource's package ID and we're not ignoring the resource,
// check to ensure that the package does contain the resource.
if (resource.packageID ?? resource.id) == package.id,
!resourcesToIgnore.contains(where: { $0.typedFullID == resource.typedFullID }) {
if let updatable = package.findResource(withID: resource.typedFullID) {
updatableResources.append(updatable)
}
}
}
return updatableResources
}
internal func addResource<Resource: LanguageResource>(_ resource: Resource) {

View file

@ -277,7 +277,9 @@ extension Storage {
// Perform an auto-install of the lexical model's KMP if not already installed.
let defaultKMPFile = defaultLexicalModelDir.appendingPathComponent("\(Defaults.lexicalModel.id).model.kmp")
let package = try ResourceFileManager.shared.prepareKMPInstall(from: defaultKMPFile) as! LexicalModelKeymanPackage
try ResourceFileManager.shared.install(resourceWithID: Defaults.lexicalModel.fullID, from: package)
// Install all languages for the model, not just the default-listed one.
try ResourceFileManager.shared.install(resourcesWithIDs: package.installables[0].map { $0.fullID }, from: package)
} catch {
log.error("Failed to install the default lexical model from the bundled KMP: \(error)")
}

View file

@ -118,4 +118,16 @@ class FileManagementTests: XCTestCase {
XCTAssertEqual(models.count, 1, "Unexpected number of models were installed")
XCTAssertEqual(models[0].id, "nrc.en.mtnt", "Installed lexical model ID mismatch")
}
func testInstallUpdateCheck() throws {
// Has a resource in need of updates (sil_euro_latin)
TestUtils.Migrations.applyBundleToFileSystem(TestUtils.Migrations.cloud_to_kmp_13)
let package = try ResourceFileManager.shared.prepareKMPInstall(from: TestUtils.Keyboards.silEuroLatinKMP) as! KeyboardKeymanPackage
let updatables = ResourceFileManager.shared.findPotentialUpdates(in: package)
XCTAssertEqual(updatables.count, 2)
XCTAssertTrue(updatables.contains(where: { $0.languageID == "en" }))
XCTAssertTrue(updatables.contains(where: { $0.languageID == "fr" }))
}
}

View file

@ -20,7 +20,30 @@ class MigrationTests: XCTestCase {
Migrations.migrate(storage: Storage.active)
Migrations.updateResources(storage: Storage.active)
// TODO: test things.
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, TestUtils.Keyboards.sil_euro_latin.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() {

View file

@ -12,6 +12,8 @@ import Foundation
extension TestUtils {
enum Keyboards {
static let khmerAngkorKMP = TestUtils.keyboardsBundle.url(forResource: "khmer_angkor", withExtension: "kmp")!
static let silEuroLatinKMP = TestUtils.keyboardsBundle.url(forResource: "sil_euro_latin", withExtension: "kmp")!
static let khmer_angkor = InstallableKeyboard(id: "khmer_angkor",
name: "Khmer Angkor",
languageID: "km",
@ -21,6 +23,7 @@ extension TestUtils {
font: nil,
oskFont: nil,
isCustom: false)
static let khmer10 = InstallableKeyboard(id: "khmer10",
name: "Khmer (NiDA)",
languageID: "km",
@ -30,5 +33,15 @@ extension TestUtils {
font: nil,
oskFont: nil,
isCustom: false)
static let sil_euro_latin = InstallableKeyboard(id: "sil_euro_latin",
name: "EuroLatin (SIL)",
languageID: "en",
languageName: "English",
version: "1.9.1",
isRTL: false,
font: Font(family: "LatinWeb", source: ["DejaVuSans.ttf"], size: nil),
oskFont: nil,
isCustom: false)
}
}