Makes version update testable.
This commit is contained in:
parent
28a9483de8
commit
dd1310d40a
|
@ -9,15 +9,15 @@ jobs:
|
||||||
DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer
|
DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
- name: Clean
|
- name: Clean McBopomofo
|
||||||
run: xcodebuild -scheme McBopomofo -configuration Release clean
|
run: xcodebuild -scheme McBopomofo -configuration Release clean
|
||||||
- name: Clean
|
- name: Clean McBopomofoInstaller
|
||||||
run: xcodebuild -scheme McBopomofoInstaller -configuration Release clean
|
run: xcodebuild -scheme McBopomofoInstaller -configuration Release clean
|
||||||
- name: Build
|
- name: Build McBopomofo
|
||||||
run: xcodebuild -scheme McBopomofo -configuration Release build
|
run: xcodebuild -scheme McBopomofo -configuration Release build
|
||||||
- name: Build
|
- name: Build McBopomofoInstaller
|
||||||
run: xcodebuild -scheme McBopomofoInstaller -configuration Release build
|
run: xcodebuild -scheme McBopomofoInstaller -configuration Release build
|
||||||
- name: Test
|
- name: Test McBopomofo App Bundle
|
||||||
run: xcodebuild -scheme McBopomofo -configuration Debug test
|
run: xcodebuild -scheme McBopomofo -configuration Debug test
|
||||||
- name: Test CandidateUI
|
- name: Test CandidateUI
|
||||||
run: swift test
|
run: swift test
|
||||||
|
|
|
@ -56,6 +56,7 @@
|
||||||
D47F7DD0278C0897002F9DD7 /* NonModalAlertWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DCF278C0897002F9DD7 /* NonModalAlertWindowController.swift */; };
|
D47F7DD0278C0897002F9DD7 /* NonModalAlertWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DCF278C0897002F9DD7 /* NonModalAlertWindowController.swift */; };
|
||||||
D47F7DD3278C1263002F9DD7 /* UserOverrideModel.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DD2278C1263002F9DD7 /* UserOverrideModel.cpp */; };
|
D47F7DD3278C1263002F9DD7 /* UserOverrideModel.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DD2278C1263002F9DD7 /* UserOverrideModel.cpp */; };
|
||||||
D485D3B92796A8A000657FF3 /* PreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D485D3B82796A8A000657FF3 /* PreferencesTests.swift */; };
|
D485D3B92796A8A000657FF3 /* PreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D485D3B82796A8A000657FF3 /* PreferencesTests.swift */; };
|
||||||
|
D485D3C02796CE3200657FF3 /* VersionUpdateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D485D3BF2796CE3200657FF3 /* VersionUpdateTests.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
@ -201,6 +202,7 @@
|
||||||
D47F7DD2278C1263002F9DD7 /* UserOverrideModel.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = UserOverrideModel.cpp; sourceTree = "<group>"; };
|
D47F7DD2278C1263002F9DD7 /* UserOverrideModel.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = UserOverrideModel.cpp; sourceTree = "<group>"; };
|
||||||
D485D3B62796A8A000657FF3 /* McBopomofoTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = McBopomofoTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
D485D3B62796A8A000657FF3 /* McBopomofoTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = McBopomofoTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
D485D3B82796A8A000657FF3 /* PreferencesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesTests.swift; sourceTree = "<group>"; };
|
D485D3B82796A8A000657FF3 /* PreferencesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesTests.swift; sourceTree = "<group>"; };
|
||||||
|
D485D3BF2796CE3200657FF3 /* VersionUpdateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionUpdateTests.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
@ -454,6 +456,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D485D3B82796A8A000657FF3 /* PreferencesTests.swift */,
|
D485D3B82796A8A000657FF3 /* PreferencesTests.swift */,
|
||||||
|
D485D3BF2796CE3200657FF3 /* VersionUpdateTests.swift */,
|
||||||
);
|
);
|
||||||
path = McBopomofoTests;
|
path = McBopomofoTests;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -688,6 +691,7 @@
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
D485D3B92796A8A000657FF3 /* PreferencesTests.swift in Sources */,
|
D485D3B92796A8A000657FF3 /* PreferencesTests.swift in Sources */,
|
||||||
|
D485D3C02796CE3200657FF3 /* VersionUpdateTests.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
import XCTest
|
||||||
|
@testable import McBopomofo
|
||||||
|
|
||||||
|
class VersionUpdateApiTests: XCTestCase {
|
||||||
|
func testFetchVersionUpdateInfo() {
|
||||||
|
let exp = self.expectation(description: "wait for 3 seconds")
|
||||||
|
_ = VersionUpdateApi.check(forced: true) { result in
|
||||||
|
exp.fulfill()
|
||||||
|
switch result {
|
||||||
|
case .success(_):
|
||||||
|
break
|
||||||
|
case .failure(let error):
|
||||||
|
XCTFail(error.localizedDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.wait(for: [exp], timeout: 3.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,119 @@ private let kUpdateInfoSiteKey = "UpdateInfoSite"
|
||||||
private let kNextCheckInterval: TimeInterval = 86400.0
|
private let kNextCheckInterval: TimeInterval = 86400.0
|
||||||
private let kTimeoutInterval: TimeInterval = 60.0
|
private let kTimeoutInterval: TimeInterval = 60.0
|
||||||
|
|
||||||
|
struct VersionUpdateReport {
|
||||||
|
var siteUrl: URL?
|
||||||
|
var currentShortVersion: String = ""
|
||||||
|
var currentVersion: String = ""
|
||||||
|
var remoteShortVersion: String = ""
|
||||||
|
var remoteVersion: String = ""
|
||||||
|
var versionDescription: String = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
enum VersionUpdateApiResult
|
||||||
|
{
|
||||||
|
case shouldUpdate(report: VersionUpdateReport)
|
||||||
|
case noNeedToUpdate
|
||||||
|
case ignored
|
||||||
|
}
|
||||||
|
|
||||||
|
enum VersionUpdateApiError: Error, LocalizedError {
|
||||||
|
case connectionError(message:String)
|
||||||
|
|
||||||
|
var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
|
case .connectionError(let message):
|
||||||
|
return String(format: NSLocalizedString("There may be no internet connection or the server failed to respond.\n\nError message: %@", comment: ""), message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct VersionUpdateApi {
|
||||||
|
static func check(forced: Bool, callback: @escaping (Result<VersionUpdateApiResult, Error>)->()) -> URLSessionTask? {
|
||||||
|
guard let infoDict = Bundle.main.infoDictionary,
|
||||||
|
let updateInfoURLString = infoDict[kUpdateInfoEndpointKey] as? String,
|
||||||
|
let updateInfoURL = URL(string: updateInfoURLString) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let request = URLRequest(url: updateInfoURL, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: kTimeoutInterval)
|
||||||
|
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||||
|
if let error = error {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
forced ?
|
||||||
|
callback(.failure(VersionUpdateApiError.connectionError(message: error.localizedDescription))) :
|
||||||
|
callback(.success(.ignored))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
guard let plist = try PropertyListSerialization.propertyList(from: data ?? Data(), options: [], format: nil) as? [AnyHashable: Any],
|
||||||
|
let remoteVersion = plist[kCFBundleVersionKey] as? String,
|
||||||
|
let infoDict = Bundle.main.infoDictionary
|
||||||
|
else {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
forced ? callback(.success(.noNeedToUpdate)): callback(.success(.ignored))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Validate info (e.g. bundle identifier)
|
||||||
|
// TODO: Use HTML to display change log, need a new key like UpdateInfoChangeLogURL for this
|
||||||
|
|
||||||
|
let currentVersion = infoDict[kCFBundleVersionKey as String] as? String ?? ""
|
||||||
|
let result = currentVersion.compare(remoteVersion, options: .numeric, range: nil, locale: nil)
|
||||||
|
|
||||||
|
if result != .orderedAscending {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
forced ? callback(.success(.noNeedToUpdate)): callback(.success(.ignored))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let siteInfoURLString = plist[kUpdateInfoSiteKey] as? String,
|
||||||
|
let siteInfoURL = URL(string: siteInfoURLString)
|
||||||
|
else {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
forced ? callback(.success(.noNeedToUpdate)): callback(.success(.ignored))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var report = VersionUpdateReport(siteUrl:siteInfoURL)
|
||||||
|
var versionDescription = ""
|
||||||
|
let versionDescriptions = plist["Description"] as? [AnyHashable: Any]
|
||||||
|
if let versionDescriptions = versionDescriptions {
|
||||||
|
var locale = "en"
|
||||||
|
let supportedLocales = ["en", "zh-Hant", "zh-Hans"]
|
||||||
|
let preferredTags = Bundle.preferredLocalizations(from: supportedLocales)
|
||||||
|
if let first = preferredTags.first {
|
||||||
|
locale = first
|
||||||
|
}
|
||||||
|
versionDescription = versionDescriptions[locale] as? String ?? versionDescriptions["en"] as? String ?? ""
|
||||||
|
if !versionDescription.isEmpty {
|
||||||
|
versionDescription = "\n\n" + versionDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
report.currentShortVersion = infoDict["CFBundleShortVersionString"] as? String ?? ""
|
||||||
|
report.currentVersion = currentVersion
|
||||||
|
report.remoteShortVersion = plist["CFBundleShortVersionString"] as? String ?? ""
|
||||||
|
report.remoteVersion = remoteVersion
|
||||||
|
report.versionDescription = versionDescription
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
callback(.success(.shouldUpdate(report: report)))
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
forced ? callback(.success(.noNeedToUpdate)): callback(.success(.ignored))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
task.resume()
|
||||||
|
return task
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@objc (AppDelegate)
|
@objc (AppDelegate)
|
||||||
class AppDelegate: NSObject, NSApplicationDelegate, NonModalAlertWindowControllerDelegate {
|
class AppDelegate: NSObject, NSApplicationDelegate, NonModalAlertWindowControllerDelegate {
|
||||||
|
|
||||||
|
@ -99,112 +212,38 @@ class AppDelegate: NSObject, NSApplicationDelegate, NonModalAlertWindowControlle
|
||||||
let nextUpdateDate = Date(timeInterval: kNextCheckInterval, since: Date())
|
let nextUpdateDate = Date(timeInterval: kNextCheckInterval, since: Date())
|
||||||
UserDefaults.standard.set(nextUpdateDate, forKey: kNextUpdateCheckDateKey)
|
UserDefaults.standard.set(nextUpdateDate, forKey: kNextUpdateCheckDateKey)
|
||||||
|
|
||||||
guard let infoDict = Bundle.main.infoDictionary,
|
checkTask = VersionUpdateApi.check(forced: forced) { result in
|
||||||
let updateInfoURLString = infoDict[kUpdateInfoEndpointKey] as? String,
|
|
||||||
let updateInfoURL = URL(string: updateInfoURLString) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let request = URLRequest(url: updateInfoURL, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: kTimeoutInterval)
|
|
||||||
|
|
||||||
func showNoUpdateAvailableAlert() {
|
|
||||||
NonModalAlertWindowController.shared.show(title: NSLocalizedString("Check for Update Completed", comment: ""), content: NSLocalizedString("You are already using the latest version of McBopomofo.", comment: ""), confirmButtonTitle: NSLocalizedString("OK", comment: ""), cancelButtonTitle: nil, cancelAsDefault: false, delegate: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
|
||||||
defer {
|
defer {
|
||||||
self.checkTask = nil
|
self.checkTask = nil
|
||||||
}
|
}
|
||||||
|
switch result {
|
||||||
if let error = error {
|
case .success(let apiResult):
|
||||||
if forced {
|
switch apiResult {
|
||||||
let title = NSLocalizedString("Update Check Failed", comment: "")
|
case .shouldUpdate(let report):
|
||||||
let content = String(format: NSLocalizedString("There may be no internet connection or the server failed to respond.\n\nError message: %@", comment: ""), error.localizedDescription)
|
self.updateNextStepURL = report.siteUrl
|
||||||
let buttonTitle = NSLocalizedString("Dismiss", comment: "")
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
NonModalAlertWindowController.shared.show(title: title, content: content, confirmButtonTitle: buttonTitle, cancelButtonTitle: nil, cancelAsDefault: false, delegate: nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
|
||||||
guard let plist = try PropertyListSerialization.propertyList(from: data ?? Data(), options: [], format: nil) as? [AnyHashable: Any],
|
|
||||||
let remoteVersion = plist[kCFBundleVersionKey] as? String,
|
|
||||||
let infoDict = Bundle.main.infoDictionary
|
|
||||||
else {
|
|
||||||
if forced {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
showNoUpdateAvailableAlert()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Validate info (e.g. bundle identifier)
|
|
||||||
// TODO: Use HTML to display change log, need a new key like UpdateInfoChangeLogURL for this
|
|
||||||
|
|
||||||
let currentVersion = infoDict[kCFBundleVersionKey as String] as? String ?? ""
|
|
||||||
let result = currentVersion.compare(remoteVersion, options: .numeric, range: nil, locale: nil)
|
|
||||||
|
|
||||||
if result != .orderedAscending {
|
|
||||||
if forced {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
showNoUpdateAvailableAlert()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let siteInfoURLString = plist[kUpdateInfoSiteKey] as? String,
|
|
||||||
let siteInfoURL = URL(string: siteInfoURLString) else {
|
|
||||||
if forced {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
showNoUpdateAvailableAlert()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.updateNextStepURL = siteInfoURL
|
|
||||||
|
|
||||||
var versionDescription = ""
|
|
||||||
let versionDescriptions = plist["Description"] as? [AnyHashable: Any]
|
|
||||||
if let versionDescriptions = versionDescriptions {
|
|
||||||
var locale = "en"
|
|
||||||
let supportedLocales = ["en", "zh-Hant", "zh-Hans"]
|
|
||||||
let preferredTags = Bundle.preferredLocalizations(from: supportedLocales)
|
|
||||||
if let first = preferredTags.first {
|
|
||||||
locale = first
|
|
||||||
}
|
|
||||||
versionDescription = versionDescriptions[locale] as? String ?? versionDescriptions["en"] as? String ?? ""
|
|
||||||
if !versionDescription.isEmpty {
|
|
||||||
versionDescription = "\n\n" + versionDescription
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let content = String(format: NSLocalizedString("You're currently using McBopomofo %@ (%@), a new version %@ (%@) is now available. Do you want to visit McBopomofo's website to download the version?%@", comment: ""),
|
let content = String(format: NSLocalizedString("You're currently using McBopomofo %@ (%@), a new version %@ (%@) is now available. Do you want to visit McBopomofo's website to download the version?%@", comment: ""),
|
||||||
infoDict["CFBundleShortVersionString"] as? String ?? "",
|
report.currentShortVersion,
|
||||||
currentVersion,
|
report.currentVersion,
|
||||||
plist["CFBundleShortVersionString"] as? String ?? "",
|
report.remoteShortVersion,
|
||||||
remoteVersion,
|
report.remoteVersion,
|
||||||
versionDescription)
|
report.versionDescription)
|
||||||
DispatchQueue.main.async {
|
|
||||||
NonModalAlertWindowController.shared.show(title: NSLocalizedString("New Version Available", comment: ""), content: content, confirmButtonTitle: NSLocalizedString("Visit Website", comment: ""), cancelButtonTitle: NSLocalizedString("Not Now", comment: ""), cancelAsDefault: false, delegate: self)
|
NonModalAlertWindowController.shared.show(title: NSLocalizedString("New Version Available", comment: ""), content: content, confirmButtonTitle: NSLocalizedString("Visit Website", comment: ""), cancelButtonTitle: NSLocalizedString("Not Now", comment: ""), cancelAsDefault: false, delegate: self)
|
||||||
|
case .noNeedToUpdate, .ignored:
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
case .failure(let error):
|
||||||
} catch {
|
switch error {
|
||||||
if forced {
|
case VersionUpdateApiError.connectionError(let message):
|
||||||
DispatchQueue.main.async {
|
let title = NSLocalizedString("Update Check Failed", comment: "")
|
||||||
showNoUpdateAvailableAlert()
|
let content = String(format: NSLocalizedString("There may be no internet connection or the server failed to respond.\n\nError message: %@", comment: ""), message)
|
||||||
|
let buttonTitle = NSLocalizedString("Dismiss", comment: "")
|
||||||
|
NonModalAlertWindowController.shared.show(title: title, content: content, confirmButtonTitle: buttonTitle, cancelButtonTitle: nil, cancelAsDefault: false, delegate: nil)
|
||||||
|
default:
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
checkTask = task
|
|
||||||
task.resume()
|
|
||||||
}
|
|
||||||
|
|
||||||
func nonModalAlertWindowControllerDidConfirm(_ controller: NonModalAlertWindowController) {
|
func nonModalAlertWindowControllerDidConfirm(_ controller: NonModalAlertWindowController) {
|
||||||
if let updateNextStepURL = updateNextStepURL {
|
if let updateNextStepURL = updateNextStepURL {
|
||||||
|
|
Loading…
Reference in New Issue