Skip to content

Commit

Permalink
Refactor the migration error handling logic to include the underlying…
Browse files Browse the repository at this point in the history
  • Loading branch information
salimbraksa authored May 11, 2023
1 parent aa92c34 commit 591a5ed
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 73 deletions.
13 changes: 13 additions & 0 deletions WordPress/Classes/Utility/CoreDataHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,19 @@ extension ContextManager {
}
}

extension ContextManager.ContextManagerError: LocalizedError, CustomDebugStringConvertible {
var errorDescription: String? {
switch self {
case .missingCoordinatorOrStore: return "Missing coordinator or store"
case .missingDatabase: return "Missing database"
}
}

var debugDescription: String {
return localizedDescription
}
}

extension CoreDataStack {
/// Perform a query using the `mainContext` and return the result.
func performQuery<T>(_ block: @escaping (NSManagedObjectContext) -> T) -> T {
Expand Down
64 changes: 64 additions & 0 deletions WordPress/Jetpack/Classes/Utility/DataMigrationError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import Foundation

enum DataMigrationError {
case databaseImportError(underlyingError: Error)
case databaseExportError(underlyingError: Error)
case backupLocationNil
case sharedUserDefaultsNil
case dataNotReadyToImport
}

extension DataMigrationError: LocalizedError, CustomNSError {

var errorDescription: String? {
switch self {
case .backupLocationNil: return "Database shared directory not found"
case .sharedUserDefaultsNil: return "Shared user defaults not found"
case .dataNotReadyToImport: return "The data wasn't ready to import"
case .databaseImportError(let error): return "Import Failed: \(error.localizedDescription)"
case .databaseExportError(let error): return "Export Failed: \(error.localizedDescription)"
}
}

static var errorDomain: String {
return String(describing: DataMigrationError.self)
}

var errorCode: Int {
switch self {
case .dataNotReadyToImport: return 100
case .backupLocationNil: return 200
case .sharedUserDefaultsNil: return 201
case .databaseImportError(let error): return 1000 + (error as NSError).code
case .databaseExportError(let error): return 2000 + (error as NSError).code
}
}

var errorUserInfo: [String: Any] {
switch self {
case .databaseExportError(let error), .databaseImportError(let error):
let nsError = error as NSError
return ["underlying-error-domain": nsError.domain,
"underlying-error-code": nsError.code,
"underlying-error-message": nsError.localizedDescription,
"underlying-error-user-info": nsError.userInfo]
default:
return [:]
}
}
}

extension DataMigrationError: CustomDebugStringConvertible {
var debugDescription: String {
return "[\(Self.errorDomain)] \(localizedDescription)"
}
}

extension DataMigrationError: Equatable {

static func ==(left: DataMigrationError, right: DataMigrationError) -> Bool {
let leftNSError = left as NSError
let rightNSError = right as NSError
return leftNSError == rightNSError
}
}
109 changes: 38 additions & 71 deletions WordPress/Jetpack/Classes/Utility/DataMigrator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,34 +16,6 @@ protocol ContentDataMigrating {
func deleteExportedData()
}

enum DataMigrationError: LocalizedError, CustomNSError {
case databaseImportError
case databaseExportError
case sharedUserDefaultsNil
case dataNotReadyToImport

var errorDescription: String? {
switch self {
case .databaseImportError: return "The database couldn't be copied from shared directory"
case .databaseExportError: return "The database couldn't be copied to shared directory"
case .sharedUserDefaultsNil: return "Shared user defaults not found"
case .dataNotReadyToImport: return "The data wasn't ready to import"
}
}

static var errorDomain: String {
return String(describing: DataMigrationError.self)
}

var errorUserInfo: [String: Any] {
var userInfo = [String: Any]()
if let errorDescription {
userInfo[NSDebugDescriptionErrorKey] = errorDescription
}
return userInfo
}
}

final class DataMigrator {
private let coreDataStack: CoreDataStack
private let backupLocation: URL?
Expand Down Expand Up @@ -72,15 +44,12 @@ final class DataMigrator {
extension DataMigrator: ContentDataMigrating {

func exportData(completion: ((Result<Void, DataMigrationError>) -> Void)? = nil) {
guard let backupLocation, copyDatabase(to: backupLocation) else {
let error = DataMigrationError.databaseExportError
self.crashLogger.logError(error)
completion?(.failure(error))
return
}
guard populateSharedDefaults() else {
let error = DataMigrationError.sharedUserDefaultsNil
self.crashLogger.logError(error)
do {
try copyDatabase(to: backupLocation)
try populateSharedDefaults()
} catch {
let error = DataMigrationError.databaseExportError(underlyingError: error)
log(error: error)
completion?(.failure(error))
return
}
Expand All @@ -97,20 +66,17 @@ extension DataMigrator: ContentDataMigrating {
return
}

guard let backupLocation, restoreDatabase(from: backupLocation) else {
let error = DataMigrationError.databaseImportError
self.crashLogger.logError(error)
completion?(.failure(error))
return
}
do {
try restoreDatabase(from: backupLocation)

/// Upon successful database restoration, the backup files in the App Group will be deleted.
/// This means that the exported data is no longer complete when the user attempts another migration.
isDataReadyToMigrate = false
/// Upon successful database restoration, the backup files in the App Group will be deleted.
/// This means that the exported data is no longer complete when the user attempts another migration.
isDataReadyToMigrate = false

guard populateFromSharedDefaults() else {
let error = DataMigrationError.sharedUserDefaultsNil
self.crashLogger.logError(error)
try populateFromSharedDefaults()
} catch {
let error = DataMigrationError.databaseImportError(underlyingError: error)
log(error: error)
completion?(.failure(error))
return
}
Expand Down Expand Up @@ -164,52 +130,53 @@ private extension DataMigrator {
}
}

func copyDatabase(to destination: URL) -> Bool {
do {
try coreDataStack.createStoreCopy(to: destination)
} catch {
DDLogError("Error copying database: \(error)")
return false
func copyDatabase(to destination: URL?) throws {
guard let destination else {
throw DataMigrationError.backupLocationNil
}
return true
try coreDataStack.createStoreCopy(to: destination)
}

func restoreDatabase(from source: URL) -> Bool {
do {
try coreDataStack.restoreStoreCopy(from: source)
} catch {
DDLogError("Error restoring database: \(error)")
return false
func restoreDatabase(from source: URL?) throws {
guard let source else {
throw DataMigrationError.backupLocationNil
}
return true
try coreDataStack.restoreStoreCopy(from: source)
}

func populateSharedDefaults() -> Bool {
func populateSharedDefaults() throws {
guard let sharedDefaults = sharedDefaults else {
return false
throw DataMigrationError.sharedUserDefaultsNil
}

let data = localDefaults.dictionaryRepresentation()
var temporaryDictionary: [String: Any] = [:]
for (key, value) in data {
temporaryDictionary[key] = value
}
sharedDefaults.set(temporaryDictionary, forKey: DefaultsWrapper.dictKey)
return true
}

func populateFromSharedDefaults() -> Bool {
func populateFromSharedDefaults() throws {
guard let sharedDefaults = sharedDefaults,
let temporaryDictionary = sharedDefaults.dictionary(forKey: DefaultsWrapper.dictKey) else {
return false
throw DataMigrationError.sharedUserDefaultsNil
}

for (key, value) in temporaryDictionary {
localDefaults.set(value, forKey: key)
}
AppAppearance.overrideAppearance()
sharedDefaults.removeObject(forKey: DefaultsWrapper.dictKey)
return true
}

private func log(error: DataMigrationError, userInfo: [String: Any] = [:]) {
let userInfo = userInfo.merging(self.userInfo(for: error)) { $1 }
DDLogError(error)
crashLogger.logError(error, userInfo: userInfo, level: .error)
}

private func userInfo(for error: DataMigrationError) -> [String: Any] {
let defaultUserInfo = ["backup-location": backupLocation?.absoluteString as Any]
return defaultUserInfo.merging(error.errorUserInfo) { $1 }
}
}

Expand Down
6 changes: 6 additions & 0 deletions WordPress/WordPress.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -3646,6 +3646,8 @@
F4D9AF4F288AD2E300803D40 /* SuggestionViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4D9AF4E288AD2E300803D40 /* SuggestionViewModelTests.swift */; };
F4D9AF51288AE23500803D40 /* SuggestionTableViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4D9AF50288AE23500803D40 /* SuggestionTableViewTests.swift */; };
F4D9AF53288AE2BA00803D40 /* SuggestionsTableViewDelegateMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4D9AF52288AE2BA00803D40 /* SuggestionsTableViewDelegateMock.swift */; };
F4DD58322A095210009A772D /* DataMigrationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4DD58312A095210009A772D /* DataMigrationError.swift */; };
F4DD58332A095210009A772D /* DataMigrationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4DD58312A095210009A772D /* DataMigrationError.swift */; };
F4DDE2C229C92F0D00C02A76 /* CrashLogging+Singleton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4DDE2C129C92F0D00C02A76 /* CrashLogging+Singleton.swift */; };
F4DDE2C329C92F0D00C02A76 /* CrashLogging+Singleton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4DDE2C129C92F0D00C02A76 /* CrashLogging+Singleton.swift */; };
F4E79301296EEE320025E8E0 /* MigrationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4E79300296EEE320025E8E0 /* MigrationState.swift */; };
Expand Down Expand Up @@ -8997,6 +8999,7 @@
F4D9AF4E288AD2E300803D40 /* SuggestionViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionViewModelTests.swift; sourceTree = "<group>"; };
F4D9AF50288AE23500803D40 /* SuggestionTableViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionTableViewTests.swift; sourceTree = "<group>"; };
F4D9AF52288AE2BA00803D40 /* SuggestionsTableViewDelegateMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionsTableViewDelegateMock.swift; sourceTree = "<group>"; };
F4DD58312A095210009A772D /* DataMigrationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataMigrationError.swift; sourceTree = "<group>"; };
F4DDE2C129C92F0D00C02A76 /* CrashLogging+Singleton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CrashLogging+Singleton.swift"; sourceTree = "<group>"; };
F4E79300296EEE320025E8E0 /* MigrationState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationState.swift; sourceTree = "<group>"; };
F4EDAA4B29A516E900622D3D /* ReaderPostService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderPostService.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -13216,6 +13219,7 @@
children = (
8332DD2329259AE300802F7D /* DataMigrator.swift */,
FED65D78293511E4008071BF /* SharedDataIssueSolver.swift */,
F4DD58312A095210009A772D /* DataMigrationError.swift */,
);
path = Utility;
sourceTree = "<group>";
Expand Down Expand Up @@ -22336,6 +22340,7 @@
FA4F65A72594337300EAA9F5 /* JetpackRestoreOptionsViewController.swift in Sources */,
7E14635720B3BEAB00B95F41 /* WPStyleGuide+Loader.swift in Sources */,
087EBFA81F02313E001F7ACE /* MediaThumbnailService.swift in Sources */,
F4DD58322A095210009A772D /* DataMigrationError.swift in Sources */,
08F8CD2A1EBD22EF0049D0C0 /* MediaExporter.swift in Sources */,
FAC086D725EDFB1E00B94F2A /* ReaderRelatedPostsCell.swift in Sources */,
324780E1247F2E2A00987525 /* NoResultsViewController+FollowedSites.swift in Sources */,
Expand Down Expand Up @@ -24669,6 +24674,7 @@
FABB24092602FC2C00C8785C /* DiffAbstractValue+Attributes.swift in Sources */,
FABB240A2602FC2C00C8785C /* Environment.swift in Sources */,
FABB240B2602FC2C00C8785C /* AbstractPostListViewController.swift in Sources */,
F4DD58332A095210009A772D /* DataMigrationError.swift in Sources */,
FABB240C2602FC2C00C8785C /* ManagedAccountSettings.swift in Sources */,
FA88EAD6260AE69C001D232B /* TemplatePreviewViewController.swift in Sources */,
F41BDD7B29114E2400B7F2B0 /* MigrationStep.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ final class ContentMigrationCoordinatorTests: CoreDataTestCase {
}

func test_startAndDo_givenExportError_shouldInvokeClosureWithError() {
mockDataMigrator.exportErrorToReturn = .databaseExportError
let error: DataMigrationError = .databaseExportError(underlyingError: ContextManager.ContextManagerError.missingCoordinatorOrStore)
mockDataMigrator.exportErrorToReturn = error

let expect = expectation(description: "Content migration should fail")
coordinator.startAndDo { result in
Expand Down
2 changes: 1 addition & 1 deletion WordPress/WordPressTest/DataMigratorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class DataMigratorTests: XCTestCase {
let migratorError = getExportDataMigratorError(migrator)

// Then
XCTAssertEqual(migratorError, .sharedUserDefaultsNil)
XCTAssertEqual(migratorError, DataMigrationError.databaseExportError(underlyingError: DataMigrationError.sharedUserDefaultsNil))
}

func test_importData_givenDataIsNotExported_shouldFail() {
Expand Down

0 comments on commit 591a5ed

Please sign in to comment.