Dr0ptp4kt has submitted this change and it was merged.

Change subject: merge release/4.1.1
......................................................................


merge release/4.1.1

Here's a breakdown of the conflicts:

- Wikipedia.xcodeproj/project.pbxproj

Pretty simple, just some new files

- Wikipedia/AppDelegate+DataMigrationProgressDelegate.h
- Wikipedia/AppDelegate+DataMigrationProgressDelegate.m
- Wikipedia/AppDelegate.m

Some issues w/ refactoring the crash reporting refactor and data
migration progress consolidation. I added a property to verify the alert
view is the data migration alert, just to be doubly sure.

- Wikipedia/Networking/Fetchers/MWKImageInfoFetcher.m
- Wikipedia/View Controllers/Image Gallery/WMFImageGalleryViewController.m

Had to refactor some stuff to incorporate the "no canonical filename"
fallback that Corey added.

- Wikipedia/Wikipedia-Info.plist

Version & bundle identifier conflict.

- WikipediaUnitTests/OldDataSchemaMigratorTests.m

New tests added

Change-Id: I4e0c130c3aa9e66b7e2af312294b3e21cfc26b7e
Ticket: T96452
---
M MediaWikiKit/MediaWikiKit/MWKSectionList.m
A MediaWikiKit/MediaWikiKit/MWKSectionList_Private.h
M Wikipedia.xcodeproj/project.pbxproj
M Wikipedia/AppDelegate.m
M Wikipedia/Data/DataMigrator.h
M Wikipedia/Data/DataMigrator.m
M Wikipedia/Data/OldDataSchemaMigrator.h
M Wikipedia/Data/OldDataSchemaMigrator.m
M Wikipedia/View Controllers/DataMigration/DataMigrationProgressViewController.h
M Wikipedia/View Controllers/DataMigration/DataMigrationProgressViewController.m
M Wikipedia/View Controllers/Image Gallery/WMFImageInfoController.m
M Wikipedia/View Controllers/SearchResults/SearchResultsController.m
M Wikipedia/Wikipedia-Info.plist
M Wikipedia/en.lproj/Localizable.strings
M Wikipedia/qqq.lproj/Localizable.strings
A WikipediaUnitTests/MWKSectionListTests.m
M WikipediaUnitTests/OldDataSchemaMigratorTests.m
17 files changed, 376 insertions(+), 150 deletions(-)

Approvals:
  Dr0ptp4kt: Looks good to me, approved
  Mhurd: Looks good to me, but someone else must approve



diff --git a/MediaWikiKit/MediaWikiKit/MWKSectionList.m 
b/MediaWikiKit/MediaWikiKit/MWKSectionList.m
index e606895..57a184b 100644
--- a/MediaWikiKit/MediaWikiKit/MWKSectionList.m
+++ b/MediaWikiKit/MediaWikiKit/MWKSectionList.m
@@ -6,6 +6,7 @@
 //  Copyright (c) 2014 Wikimedia Foundation. All rights reserved.
 //
 
+#import "MWKSectionList_Private.h"
 #import "MediaWikiKit.h"
 
 @implementation MWKSectionList {
@@ -27,35 +28,49 @@
         _article      = article;
         mutationState = 0;
         if (_sections == nil) {
-            _sections = [@[] mutableCopy];
-            NSFileManager* fm = [NSFileManager defaultManager];
-            NSString* path    = [[self.article.dataStore 
pathForTitle:self.article.title] stringByAppendingPathComponent:@"sections"];
-            NSArray* files    = [fm contentsOfDirectoryAtPath:path error:nil];
-            files = [files sortedArrayUsingComparator:^NSComparisonResult 
(NSString* obj1, NSString* obj2) {
-                int sectionId1 = [obj1 intValue];
-                int sectionId2 = [obj2 intValue];
-                if (sectionId1 < sectionId2) {
-                    return NSOrderedAscending;
-                } else if (sectionId1 == sectionId2) {
-                    return NSOrderedSame;
-                } else {
-                    return NSOrderedDescending;
-                }
-            }];
-            NSRegularExpression* redigits = [NSRegularExpression 
regularExpressionWithPattern:@"^\\d+$" options:0 error:nil];
-            for (NSString* subpath in files) {
-                NSString* filename = [subpath lastPathComponent];
-                NSArray* matches   = [redigits matchesInString:filename 
options:0 range:NSMakeRange(0, [filename length])];
-                if (matches && [matches count]) {
-                    int sectionId = [filename intValue];
-                    _sections[sectionId] = [self.article.dataStore 
sectionWithId:sectionId article:self.article];
-                }
-            }
+            _sections = [NSMutableArray array];
+            [self importSectionsFromDisk];
         }
     }
     return self;
 }
 
+- (void)importSectionsFromDisk {
+    NSFileManager* fm = [NSFileManager defaultManager];
+    NSString* path    = [[self.article.dataStore 
pathForTitle:self.article.title] stringByAppendingPathComponent:@"sections"];
+
+    NSArray* files = [fm contentsOfDirectoryAtPath:path error:nil];
+    files = [files sortedArrayUsingComparator:^NSComparisonResult (NSString* 
obj1, NSString* obj2) {
+        int sectionId1 = [obj1 intValue];
+        int sectionId2 = [obj2 intValue];
+        if (sectionId1 < sectionId2) {
+            return NSOrderedAscending;
+        } else if (sectionId1 == sectionId2) {
+            return NSOrderedSame;
+        } else {
+            return NSOrderedDescending;
+        }
+    }];
+
+    NSRegularExpression* redigits = [NSRegularExpression 
regularExpressionWithPattern:@"^\\d+$" options:0 error:nil];
+    @try {
+        for (NSString* subpath in files) {
+            NSString* filename = [subpath lastPathComponent];
+            NSArray* matches   = [redigits matchesInString:filename options:0 
range:NSMakeRange(0, [filename length])];
+            if (matches && [matches count]) {
+                [self readAndInsertSection:[filename intValue]];
+            }
+        }
+    }@catch (NSException* e) {
+        NSLog(@"Failed to import sections at path %@, leaving list empty.", 
path);
+        [_sections removeAllObjects];
+    }
+}
+
+- (void)readAndInsertSection:(int)sectionId {
+    _sections[sectionId] = [self.article.dataStore sectionWithId:sectionId 
article:self.article];
+}
+
 - (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState*)state
                                   objects:(__unsafe_unretained id [])stackbuf
                                     count:(NSUInteger)len {
diff --git a/MediaWikiKit/MediaWikiKit/MWKSectionList_Private.h 
b/MediaWikiKit/MediaWikiKit/MWKSectionList_Private.h
new file mode 100644
index 0000000..94795ce
--- /dev/null
+++ b/MediaWikiKit/MediaWikiKit/MWKSectionList_Private.h
@@ -0,0 +1,19 @@
+//
+//  MWKSectionList_Private.h
+//  Wikipedia
+//
+//  Created by Brian Gerstle on 4/16/15.
+//  Copyright (c) 2015 Wikimedia Foundation. All rights reserved.
+//
+
+#import "MWKSectionList.h"
+
+@interface MWKSectionList ()
+
+/// Import list of sections from disk using the receiver's `article` and 
`dataStore`.
+- (void)importSectionsFromDisk;
+
+/// Read `sectionId` from the receiver's `article.dataStore` and insert it 
into the receiver's section list..
+- (void)readAndInsertSection:(int)sectionId;
+
+@end
diff --git a/Wikipedia.xcodeproj/project.pbxproj 
b/Wikipedia.xcodeproj/project.pbxproj
index e5004fa..7e95694 100644
--- a/Wikipedia.xcodeproj/project.pbxproj
+++ b/Wikipedia.xcodeproj/project.pbxproj
@@ -225,7 +225,6 @@
                BC86B9361A92966B00B4C039 /* 
AFHTTPRequestOperationManager+UniqueRequests.m in Sources */ = {isa = 
PBXBuildFile; fileRef = BC86B9351A92966B00B4C039 /* 
AFHTTPRequestOperationManager+UniqueRequests.m */; };
                BC86B93D1A929CC500B4C039 /* 
UICollectionViewFlowLayout+NSCopying.m in Sources */ = {isa = PBXBuildFile; 
fileRef = BC86B93C1A929CC500B4C039 /* UICollectionViewFlowLayout+NSCopying.m 
*/; };
                BC86B9401A929D7900B4C039 /* 
UICollectionViewFlowLayout+WMFItemSizeThatFits.m in Sources */ = {isa = 
PBXBuildFile; fileRef = BC86B93F1A929D7900B4C039 /* 
UICollectionViewFlowLayout+WMFItemSizeThatFits.m */; };
-               BC90C4B91AC201BB009F36D2 /* 
AppDelegate+DataMigrationProgressDelegate.m in Sources */ = {isa = 
PBXBuildFile; fileRef = BC90C4B81AC201BB009F36D2 /* 
AppDelegate+DataMigrationProgressDelegate.m */; };
                BC90C4BE1AC219FE009F36D2 /* UIWindow+WMFMainScreenWindow.m in 
Sources */ = {isa = PBXBuildFile; fileRef = BC90C4BD1AC219FE009F36D2 /* 
UIWindow+WMFMainScreenWindow.m */; };
                BC955BC71A82BEFD000EF9E4 /* MWKImageInfoFetcher.m in Sources */ 
= {isa = PBXBuildFile; fileRef = BC955BC61A82BEFD000EF9E4 /* 
MWKImageInfoFetcher.m */; };
                BC955BCF1A82C2FA000EF9E4 /* 
AFHTTPRequestOperationManager+WMFConfig.m in Sources */ = {isa = PBXBuildFile; 
fileRef = BC955BCE1A82C2FA000EF9E4 /* AFHTTPRequestOperationManager+WMFConfig.m 
*/; };
@@ -286,6 +285,7 @@
                BCC185D81A9E5628005378F8 /* UILabel+WMFStyling.m in Sources */ 
= {isa = PBXBuildFile; fileRef = BCC185D71A9E5628005378F8 /* 
UILabel+WMFStyling.m */; };
                BCC185E01A9EC836005378F8 /* UIButton+FrameUtils.m in Sources */ 
= {isa = PBXBuildFile; fileRef = BCC185DF1A9EC836005378F8 /* 
UIButton+FrameUtils.m */; };
                BCC185E81A9FA498005378F8 /* 
UICollectionViewFlowLayout+AttributeUtils.m in Sources */ = {isa = 
PBXBuildFile; fileRef = BCC185E71A9FA498005378F8 /* 
UICollectionViewFlowLayout+AttributeUtils.m */; };
+               BCCED2D01AE03BE20094EB7E /* MWKSectionListTests.m in Sources */ 
= {isa = PBXBuildFile; fileRef = BCCED2CF1AE03BE20094EB7E /* 
MWKSectionListTests.m */; };
                BCDB75C41AB0E8300005593F /* WMFSubstringUtilsTests.m in Sources 
*/ = {isa = PBXBuildFile; fileRef = BCDB75C31AB0E8300005593F /* 
WMFSubstringUtilsTests.m */; };
                BCE912BA1ACC5E6900B74B42 /* NSIndexSet+BKReduce.m in Sources */ 
= {isa = PBXBuildFile; fileRef = BCE912B91ACC5E6900B74B42 /* 
NSIndexSet+BKReduce.m */; };
                BCE912BD1ACC629B00B74B42 /* NSIndexSet+BKReduceTests.m in 
Sources */ = {isa = PBXBuildFile; fileRef = BCE912BC1ACC629B00B74B42 /* 
NSIndexSet+BKReduceTests.m */; };
@@ -752,8 +752,6 @@
                BC86B93C1A929CC500B4C039 /* 
UICollectionViewFlowLayout+NSCopying.m */ = {isa = PBXFileReference; 
fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = 
"UICollectionViewFlowLayout+NSCopying.m"; sourceTree = "<group>"; };
                BC86B93E1A929D7900B4C039 /* 
UICollectionViewFlowLayout+WMFItemSizeThatFits.h */ = {isa = PBXFileReference; 
fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = 
"UICollectionViewFlowLayout+WMFItemSizeThatFits.h"; sourceTree = "<group>"; };
                BC86B93F1A929D7900B4C039 /* 
UICollectionViewFlowLayout+WMFItemSizeThatFits.m */ = {isa = PBXFileReference; 
fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = 
"UICollectionViewFlowLayout+WMFItemSizeThatFits.m"; sourceTree = "<group>"; };
-               BC90C4B71AC201BB009F36D2 /* 
AppDelegate+DataMigrationProgressDelegate.h */ = {isa = PBXFileReference; 
fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = 
"AppDelegate+DataMigrationProgressDelegate.h"; sourceTree = "<group>"; };
-               BC90C4B81AC201BB009F36D2 /* 
AppDelegate+DataMigrationProgressDelegate.m */ = {isa = PBXFileReference; 
fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = 
"AppDelegate+DataMigrationProgressDelegate.m"; sourceTree = "<group>"; };
                BC90C4BC1AC219FE009F36D2 /* UIWindow+WMFMainScreenWindow.h */ = 
{isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; 
path = "UIWindow+WMFMainScreenWindow.h"; sourceTree = "<group>"; };
                BC90C4BD1AC219FE009F36D2 /* UIWindow+WMFMainScreenWindow.m */ = 
{isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = 
sourcecode.c.objc; path = "UIWindow+WMFMainScreenWindow.m"; sourceTree = 
"<group>"; };
                BC955BC51A82BEFD000EF9E4 /* MWKImageInfoFetcher.h */ = {isa = 
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = 
MWKImageInfoFetcher.h; sourceTree = "<group>"; };
@@ -884,6 +882,8 @@
                BCC185E71A9FA498005378F8 /* 
UICollectionViewFlowLayout+AttributeUtils.m */ = {isa = PBXFileReference; 
fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = 
"UICollectionViewFlowLayout+AttributeUtils.m"; sourceTree = "<group>"; };
                BCDB75BC1AB0D3DE0005593F /* WMFImageInfoController_Private.h */ 
= {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = 
sourcecode.c.h; name = WMFImageInfoController_Private.h; path = "Image 
Gallery/WMFImageInfoController_Private.h"; sourceTree = "<group>"; };
                BCDB75BD1AB0DFC40005593F /* WMFRangeUtils.h */ = {isa = 
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = 
WMFRangeUtils.h; sourceTree = "<group>"; };
+               BCCED2CF1AE03BE20094EB7E /* MWKSectionListTests.m */ = {isa = 
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path 
= MWKSectionListTests.m; sourceTree = "<group>"; };
+               BCCED2D21AE041BD0094EB7E /* MWKSectionList_Private.h */ = {isa 
= PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path 
= MWKSectionList_Private.h; sourceTree = "<group>"; };
                BCDB75C31AB0E8300005593F /* WMFSubstringUtilsTests.m */ = {isa 
= PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; 
path = WMFSubstringUtilsTests.m; sourceTree = "<group>"; };
                BCE912B81ACC5E6900B74B42 /* NSIndexSet+BKReduce.h */ = {isa = 
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = 
"NSIndexSet+BKReduce.h"; sourceTree = "<group>"; };
                BCE912B91ACC5E6900B74B42 /* NSIndexSet+BKReduce.m */ = {isa = 
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path 
= "NSIndexSet+BKReduce.m"; sourceTree = "<group>"; };
@@ -2041,6 +2041,7 @@
                                BC31B2511AB1D9DC008138CA /* 
WMFImageInfoControllerTests.m */,
                                BCE912BC1ACC629B00B74B42 /* 
NSIndexSet+BKReduceTests.m */,
                                0EBC56951AD5B22800E82CDD /* 
BITHockeyManagerWMFExtensionsTests.m */,
+                               BCCED2CF1AE03BE20094EB7E /* 
MWKSectionListTests.m */,
                        );
                        path = WikipediaUnitTests;
                        sourceTree = "<group>";
@@ -2182,6 +2183,7 @@
                                BCB6699E1A83F6C300C7B1FE /* MWKImageList.h */,
                                BCB6699F1A83F6C300C7B1FE /* MWKImageList.m */,
                                BCB669A01A83F6C300C7B1FE /* MWKSectionList.h */,
+                               BCCED2D21AE041BD0094EB7E /* 
MWKSectionList_Private.h */,
                                BCB669A11A83F6C300C7B1FE /* MWKSectionList.m */,
                        );
                        name = "Data i/o";
@@ -2365,8 +2367,6 @@
                                D4BC22B3181E9E6300CAC673 /* empty.png */,
                                D4991447181D51DE00E6073C /* AppDelegate.h */,
                                D4991448181D51DE00E6073C /* AppDelegate.m */,
-                               BC90C4B71AC201BB009F36D2 /* 
AppDelegate+DataMigrationProgressDelegate.h */,
-                               BC90C4B81AC201BB009F36D2 /* 
AppDelegate+DataMigrationProgressDelegate.m */,
                                D499144A181D51DE00E6073C /* 
Main_iPhone.storyboard */,
                                D46CD8C018A1AC4F0042959E /* InfoPlist.strings 
*/,
                                D46CD8C218A1AC4F0042959E /* Localizable.strings 
*/,
@@ -2857,6 +2857,7 @@
                                BC0FED771AAA026C002488D7 /* 
WMFImageURLParsingTests.m in Sources */,
                                BCEC778F1AC9AEC800D9DDA5 /* 
MWKImage+AssociationTestUtils.m in Sources */,
                                04F1226A1ACB822D002FC3B5 /* 
NSString+FormattedAttributedStringTests.m in Sources */,
+                               BCCED2D01AE03BE20094EB7E /* 
MWKSectionListTests.m in Sources */,
                                BC0FED6B1AAA0268002488D7 /* 
MWKDataStoreStorageTests.m in Sources */,
                                0484B9071ABB50FA00874073 /* WMFArticleParsing.m 
in Sources */,
                                BC0FED751AAA026C002488D7 /* 
NSArray+BKIndexTests.m in Sources */,
@@ -3002,7 +3003,6 @@
                                BC86B93D1A929CC500B4C039 /* 
UICollectionViewFlowLayout+NSCopying.m in Sources */,
                                04D686C91AB28FE40009B44A /* 
UIImage+WMFFocalImageDrawing.m in Sources */,
                                0429300A18604898002A13FC /* 
SavedPagesResultCell.m in Sources */,
-                               BC90C4B91AC201BB009F36D2 /* 
AppDelegate+DataMigrationProgressDelegate.m in Sources */,
                                0487048419F8262600B7D307 /* CaptchaResetter.m 
in Sources */,
                                D407E6411A51DBDA00CCC8B1 /* SchemaConverter.m 
in Sources */,
                                BCB669A71A83F6C400C7B1FE /* MWKSiteDataObject.m 
in Sources */,
diff --git a/Wikipedia/AppDelegate.m b/Wikipedia/AppDelegate.m
index 9de2b4e..32c5e32 100644
--- a/Wikipedia/AppDelegate.m
+++ b/Wikipedia/AppDelegate.m
@@ -6,12 +6,15 @@
 #import "WikipediaAppUtils.h"
 #import "SessionSingleton.h"
 #import "BITHockeyManager+WMFExtensions.h"
-#import "AppDelegate+DataMigrationProgressDelegate.h"
 #import "UIWindow+WMFMainScreenWindow.h"
+#import "DataMigrationProgressViewController.h"
+#import "UIWindow+WMFMainScreenWindow.h"
+#import "WikipediaAppUtils.h"
 
 
 @interface AppDelegate ()
-
+<DataMigrationProgressDelegate>
+@property (nonatomic, weak) UIAlertView* dataMigrationAlert;
 @end
 
 @implementation AppDelegate
@@ -132,4 +135,39 @@
     // Called when the application is about to terminate. Save data if 
appropriate. See also applicationDidEnterBackground:.
 }
 
+#pragma mark - Migration
+
+- (BOOL)presentDataMigrationViewControllerIfNeeded {
+    if ([DataMigrationProgressViewController needsMigration]) {
+        UIAlertView* dialog =
+            [[UIAlertView alloc] 
initWithTitle:MWLocalizedString(@"migration-prompt-title", nil)
+                                       
message:MWLocalizedString(@"migration-prompt-message", nil)
+                                      delegate:self
+                             
cancelButtonTitle:MWLocalizedString(@"migration-skip-button-title", nil)
+                             
otherButtonTitles:MWLocalizedString(@"migration-confirm-button-title", nil), 
nil];
+        [dialog show];
+        self.dataMigrationAlert = dialog;
+        return YES;
+    } else {
+        return NO;
+    }
+}
+
+- 
(void)dataMigrationProgressComplete:(DataMigrationProgressViewController*)viewController
 {
+    [self presentRootViewController:YES withSplash:NO];
+}
+
+- (void)alertView:(UIAlertView*)alertView 
didDismissWithButtonIndex:(NSInteger)buttonIndex {
+    if (alertView == self.dataMigrationAlert) {
+        if (buttonIndex == alertView.cancelButtonIndex) {
+            [DataMigrationProgressViewController removeOldData];
+            [self presentRootViewController:YES withSplash:NO];
+        } else {
+            DataMigrationProgressViewController* migrationVC = 
[[DataMigrationProgressViewController alloc] init];
+            migrationVC.delegate = self;
+            [self transitionToRootViewController:migrationVC animated:NO];
+        }
+    }
+}
+
 @end
diff --git a/Wikipedia/Data/DataMigrator.h b/Wikipedia/Data/DataMigrator.h
index 62d37fd..e4a41fc 100644
--- a/Wikipedia/Data/DataMigrator.h
+++ b/Wikipedia/Data/DataMigrator.h
@@ -10,12 +10,13 @@
 
 @interface DataMigrator : NSObject
 
-- (id)init;
+/// @return `YES` if a SQLLite file exists at the master database path, 
otherwise `NO`.
++ (BOOL)hasData;
 
-/**
- * Is there anything that needs to be migrated?
- */
-- (BOOL)hasData;
+/// Remove the master database file.
++ (void)removeOldData;
+
+- (id)init;
 
 /**
  * Return the extracted JSON blobs from the savedPagesDB database table.
@@ -24,11 +25,5 @@
  * @return (NSArray *) of (NSDictionary *)s.
  */
 - (NSArray*)extractSavedPages;
-
-/**
- * Delete the old files.
- * @todo implement this
- */
-- (void)removeOldData;
 
 @end
diff --git a/Wikipedia/Data/DataMigrator.m b/Wikipedia/Data/DataMigrator.m
index a074312..ffdbdc4 100644
--- a/Wikipedia/Data/DataMigrator.m
+++ b/Wikipedia/Data/DataMigrator.m
@@ -17,20 +17,33 @@
 
 #pragma mark - Public methods
 
++ (NSString*)masterDatabasePathIfExists {
+    NSString* path = [self localLibraryPath:@"Caches/Databases.db"];
+    return [[NSFileManager defaultManager] fileExistsAtPath:path] ? path : nil;
+}
+
++ (BOOL)hasData {
+    return !![self masterDatabasePathIfExists];
+}
+
++ (void)removeOldData {
+    NSString* dbPath = [[self class] masterDatabasePathIfExists];
+    if (dbPath) {
+        NSLog(@"Deleting old app's %@", dbPath);
+        [[NSFileManager defaultManager] removeItemAtPath:dbPath error:nil];
+    }
+}
+
 - (id)init {
     self = [super init];
     if (self) {
-        NSString* dbPath = [self localLibraryPath:@"Caches/Databases.db"];
-        if ([[NSFileManager defaultManager] fileExistsAtPath:dbPath]) {
+        NSString* dbPath = [[self class] masterDatabasePathIfExists];
+        if (dbPath) {
             NSLog(@"Opening sqlite database from %@", dbPath);
             masterDB = [[SQLiteHelper alloc] initWithPath:dbPath];
         }
     }
     return self;
-}
-
-- (BOOL)hasData {
-    return (masterDB != NULL);
 }
 
 - (NSArray*)extractSavedPages {
@@ -44,25 +57,16 @@
     return [NSArray arrayWithArray:arr];
 }
 
-- (void)removeOldData {
-    if ([self hasData]) {
-        NSLog(@"Deleting old app's Caches/Databases.db");
-        masterDB = nil;
-        NSString* dbPath = [self localLibraryPath:@"Caches/Databases.db"];
-        [[NSFileManager defaultManager] removeItemAtPath:dbPath error:nil];
-    }
-}
-
 #pragma mark - Private methods
 
 /**
  * Return absolute path for relative path to the installed app's documents 
folder.
  */
-- (NSString*)localDocumentPath:(NSString*)local {
++ (NSString*)localDocumentPath:(NSString*)local {
     return [[self documentRootPath] stringByAppendingPathComponent:local];
 }
 
-- (NSString*)documentRootPath {
++ (NSString*)documentRootPath {
     NSArray* documentPaths     = 
NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
     NSString* documentRootPath = [documentPaths objectAtIndex:0];
     return documentRootPath;
@@ -71,11 +75,11 @@
 /**
  * Return absolute path for relative path to the installed app's Library 
folder.
  */
-- (NSString*)localLibraryPath:(NSString*)local {
++ (NSString*)localLibraryPath:(NSString*)local {
     return [[self libraryRootPath] stringByAppendingPathComponent:local];
 }
 
-- (NSString*)libraryRootPath {
++ (NSString*)libraryRootPath {
     NSArray* libraryPaths     = 
NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
     NSString* libraryRootPath = [libraryPaths objectAtIndex:0];
     return libraryRootPath;
@@ -90,7 +94,7 @@
     NSArray* rows     = [masterDB query:@"SELECT origin, path FROM Databases 
WHERE name=?" params:@[dbname]];
     NSDictionary* row = rows[0];
     NSLog(@"row: %@", row);
-    NSString* path = [[[self localLibraryPath:@"Caches"] 
stringByAppendingPathComponent:row[@"origin"]] 
stringByAppendingPathComponent:row[@"path"]];
+    NSString* path = [[[[self class] localLibraryPath:@"Caches"] 
stringByAppendingPathComponent:row[@"origin"]] 
stringByAppendingPathComponent:row[@"path"]];
     NSLog(@"opening path %@", path);
     return [[SQLiteHelper alloc] initWithPath:path];
 }
diff --git a/Wikipedia/Data/OldDataSchemaMigrator.h 
b/Wikipedia/Data/OldDataSchemaMigrator.h
index e028df2..bf7c549 100644
--- a/Wikipedia/Data/OldDataSchemaMigrator.h
+++ b/Wikipedia/Data/OldDataSchemaMigrator.h
@@ -42,7 +42,7 @@
 @property (weak) id<OldDataSchemaDelegate> delegate;
 @property (weak) id<OldDataSchemaMigratorProgressDelegate> progressDelegate;
 
-- (BOOL)exists;
++ (BOOL)exists;
 
 /**
  *  This runs asynchronously.
@@ -50,4 +50,6 @@
  */
 - (void)migrateData;
 
++ (void)removeOldData;
+
 @end
diff --git a/Wikipedia/Data/OldDataSchemaMigrator.m 
b/Wikipedia/Data/OldDataSchemaMigrator.m
index 91cf595..cb45974 100644
--- a/Wikipedia/Data/OldDataSchemaMigrator.m
+++ b/Wikipedia/Data/OldDataSchemaMigrator.m
@@ -26,7 +26,7 @@
     self = [super init];
     if (self) {
         _savedTitles = [[NSMutableSet alloc] init];
-        if (self.exists) {
+        if ([[self class] exists]) {
             _context = [ArticleDataContextSingleton sharedInstance];
         } else {
             _context = nil;
@@ -35,19 +35,19 @@
     return self;
 }
 
-- (NSString*)sqlitePath {
++ (NSString*)sqlitePath {
     NSArray* documentPaths     = 
NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
     NSString* documentRootPath = [documentPaths objectAtIndex:0];
     NSString* filePath         = [documentRootPath 
stringByAppendingPathComponent:@"articleData6.sqlite"];
     return filePath;
 }
 
-- (BOOL)exists {
++ (BOOL)exists {
     NSString* filePath = [self sqlitePath];
     return [[NSFileManager defaultManager] fileExistsAtPath:filePath];
 }
 
-- (void)removeOldData {
++ (void)removeOldData {
     NSString* filePath   = [self sqlitePath];
     NSString* backupPath = [filePath stringByAppendingString:@".bak"];
     NSError* err         = nil;
@@ -106,7 +106,7 @@
         }
 
         [self.context saveContextAndPropagateChangesToStore:context 
completionBlock:^(NSError* error) {
-            [self removeOldData];
+            [[self class] removeOldData];
 
             if (error) {
                 [self.progressDelegate oldDataSchema:self 
didFinishWithError:error];
@@ -143,33 +143,34 @@
         // Record for later to avoid dupe imports
         [self.savedTitles addObject:key];
 
-        MWKArticle* migratedArticle = [self.delegate oldDataSchema:self 
migrateArticle:[self exportArticle:article]];
-
-        Image* thumbnail = article.thumbnailImage;
-        if (thumbnail) {
-            [self migrateThumbnailImage:thumbnail article:article 
newArticle:migratedArticle];
-        }
-        // HACK: setting thumbnailURL after migration prevents it from being 
added to the image list twice
-        migratedArticle.thumbnailURL = thumbnail.sourceUrl;
-
-        for (Section* section in [article sectionsBySectionId]) {
-            for (SectionImage* sectionImage in [section sectionImagesByIndex]) 
{
-                [self migrateImage:sectionImage newArticle:migratedArticle];
-            }
-        }
-
-        // set the lead image to the first non-thumb image
-        if ([migratedArticle.images count]) {
-            // thumbnail should always be first, if it's present (see above 
assertion)
-            NSUInteger leadImageURLIndex = (thumbnail && 
migratedArticle.images.count > 1) ? 1 : 0;
-            migratedArticle.imageURL = [migratedArticle.images 
imageURLAtIndex:leadImageURLIndex];
-        }
-
+        MWKArticle* migratedArticle;
         @try {
+            migratedArticle = [self.delegate oldDataSchema:self 
migrateArticle:[self exportArticle:article]];
+
+            Image* thumbnail = article.thumbnailImage;
+            if (thumbnail) {
+                [self migrateThumbnailImage:thumbnail article:article 
newArticle:migratedArticle];
+            }
+            // HACK: setting thumbnailURL after migration prevents it from 
being added to the image list twice
+            migratedArticle.thumbnailURL = thumbnail.sourceUrl;
+
+            for (Section* section in [article sectionsBySectionId]) {
+                for (SectionImage* sectionImage in [section 
sectionImagesByIndex]) {
+                    [self migrateImage:sectionImage 
newArticle:migratedArticle];
+                }
+            }
+
+            // set the lead image to the first non-thumb image
+            if ([migratedArticle.images count]) {
+                // thumbnail should always be first, if it's present (see 
above assertion)
+                NSUInteger leadImageURLIndex = (thumbnail && 
migratedArticle.images.count > 1) ? 1 : 0;
+                migratedArticle.imageURL = [migratedArticle.images 
imageURLAtIndex:leadImageURLIndex];
+            }
+
             [migratedArticle save];
-        } @catch (NSException* saveException) {
-            NSLog(@"Failed to save article after importing images: %@", 
saveException);
-            NSParameterAssert(!saveException);
+        }@catch (NSException* exception) {
+            NSLog(@"Failed to migrate article due to exception: %@. Removing 
data.", exception);
+            [migratedArticle remove];
         }
     }
 }
diff --git a/Wikipedia/View 
Controllers/DataMigration/DataMigrationProgressViewController.h 
b/Wikipedia/View Controllers/DataMigration/DataMigrationProgressViewController.h
index f213130..cb3a248 100644
--- a/Wikipedia/View 
Controllers/DataMigration/DataMigrationProgressViewController.h
+++ b/Wikipedia/View 
Controllers/DataMigration/DataMigrationProgressViewController.h
@@ -23,6 +23,8 @@
 @property (weak, nonatomic) IBOutlet UILabel* progressLabel;
 @property (weak, nonatomic) id<DataMigrationProgressDelegate> delegate;
 
-- (BOOL)needsMigration;
+// TODO: refactor these into class methods
++ (BOOL)needsMigration;
++ (void)removeOldData;
 
 @end
diff --git a/Wikipedia/View 
Controllers/DataMigration/DataMigrationProgressViewController.m 
b/Wikipedia/View Controllers/DataMigration/DataMigrationProgressViewController.m
index 15069f6..654217e 100644
--- a/Wikipedia/View 
Controllers/DataMigration/DataMigrationProgressViewController.m
+++ b/Wikipedia/View 
Controllers/DataMigration/DataMigrationProgressViewController.m
@@ -49,9 +49,9 @@
 - (void)viewDidAppear:(BOOL)animated {
     [super viewDidAppear:animated];
 
-    if ([self.oldDataSchema exists]) {
+    if ([OldDataSchemaMigrator exists]) {
         [self runNewMigration];
-    } else if ([self.dataMigrator hasData]) {
+    } else if ([DataMigrator hasData]) {
         [self runOldMigration];
     }
 }
@@ -77,8 +77,13 @@
     return _schemaConvertor;
 }
 
-- (BOOL)needsMigration {
-    return [self.oldDataSchema exists] || [self.dataMigrator hasData];
++ (BOOL)needsMigration {
+    return [OldDataSchemaMigrator exists] || [DataMigrator hasData];
+}
+
++ (void)removeOldData {
+    [DataMigrator removeOldData];
+    [OldDataSchemaMigrator removeOldData];
 }
 
 - (void)runNewMigration {
@@ -111,7 +116,7 @@
 
     [importer importArticles:titles];
 
-    [self.dataMigrator removeOldData];
+    [DataMigrator removeOldData];
 
     [self.progressIndicator setProgress:1.0 animated:YES completion:NULL];
 }
diff --git a/Wikipedia/View Controllers/Image Gallery/WMFImageInfoController.m 
b/Wikipedia/View Controllers/Image Gallery/WMFImageInfoController.m
index d4c38cc..b052ece 100644
--- a/Wikipedia/View Controllers/Image Gallery/WMFImageInfoController.m
+++ b/Wikipedia/View Controllers/Image Gallery/WMFImageInfoController.m
@@ -55,8 +55,30 @@
 - (NSArray*)uniqueArticleImages {
     if (!_uniqueArticleImages) {
         NSArray* uniqueArticleImages = [self.article.images 
uniqueLargestVariants];
+
+        // reverse article images if current language is RTL
         _uniqueArticleImages =
             [WikipediaAppUtils isDeviceLanguageRTL] ? [uniqueArticleImages 
wmf_reverseArray] : uniqueArticleImages;
+
+        NSMutableArray* imageFilePageTitles = [NSMutableArray 
arrayWithCapacity:_uniqueArticleImages.count];
+
+        // reduce images to only those who have valid canonical filenames
+        _uniqueArticleImages =
+            [[_uniqueArticleImages bk_reduce:[NSMutableArray 
arrayWithCapacity:_uniqueArticleImages.count]
+                                   withBlock:^id (NSMutableArray* 
uniqueArticleImages, MWKImage* image) {
+            NSAssert(image.canonicalFilename.length,
+                     @"Unable to form canonical filename from image: %@",
+                     image.sourceURL);
+            if (image.canonicalFilename.length) {
+                NSString* filePageTitle = [@"File:" 
stringByAppendingString:image.canonicalFilename];
+                [imageFilePageTitles addObject:filePageTitle];
+                [uniqueArticleImages addObject:image];
+            }
+            return uniqueArticleImages;
+        }] copy];
+
+        // strictly evaluate iamgeFilePageTitles to filter out any images 
don't have a canonicalFilename
+        _imageFilePageTitles = [imageFilePageTitles copy];
     }
     return _uniqueArticleImages;
 }
@@ -67,18 +89,6 @@
             WMFIndexImageInfo([self.dataStore 
imageInfoForArticle:self.article]) ? : [NSMutableDictionary new];
     }
     return _indexedImageInfo;
-}
-
-- (NSArray*)imageFilePageTitles {
-    if (!_imageFilePageTitles) {
-        _imageFilePageTitles = [[self uniqueArticleImages] 
bk_map:^NSString*(MWKImage* image) {
-            NSAssert(image.canonicalFilename.length,
-                     @"Unable to form canonical filename from image: %@",
-                     image.sourceURL);
-            return [@"File:" stringByAppendingString:image.canonicalFilename];
-        }];
-    }
-    return _imageFilePageTitles;
 }
 
 - (NSUInteger)indexOfImageAssociatedWithInfo:(MWKImageInfo*)info {
diff --git a/Wikipedia/View Controllers/SearchResults/SearchResultsController.m 
b/Wikipedia/View Controllers/SearchResults/SearchResultsController.m
index 61294e2..e899708 100644
--- a/Wikipedia/View Controllers/SearchResults/SearchResultsController.m
+++ b/Wikipedia/View Controllers/SearchResults/SearchResultsController.m
@@ -536,7 +536,7 @@
                 // Check if cell still onscreen! This is important!
                 NSArray* visibleRowIndexPaths = [self.searchResultsTable 
indexPathsForVisibleRows];
                 for (NSIndexPath* thisIndexPath in visibleRowIndexPaths.copy) {
-                    NSDictionary* rowData = 
self.searchResults[thisIndexPath.row];
+                    NSDictionary* rowData = [self 
searchResultForIndexPath:thisIndexPath];
                     NSString* url         = rowData[@"thumbnail"][@"source"];
                     if ([url.lastPathComponent isEqualToString:fileName]) {
                         SearchResultCell* cell = 
(SearchResultCell*)[self.searchResultsTable 
cellForRowAtIndexPath:thisIndexPath];
@@ -586,11 +586,6 @@
 
     [self.didYouMeanButton hide];
 
-    // Show "Searching..." message.
-    //[self.searchMessageLabel 
showWithText:MWLocalizedString(@"search-searching", nil)];
-
-    //[self showAlert:MWLocalizedString(@"search-searching", nil) 
type:ALERT_TYPE_MIDDLE duration:-1];
-
     (void)[[SearchResultFetcher alloc] initAndSearchForTerm:searchTerm
                                                  searchType:SEARCH_TYPE_TITLES
                                                searchReason:reason
@@ -601,52 +596,59 @@
 
 #pragma mark Search results table methods (requests actual thumb image data)
 
+- (NSDictionary*)searchResultForIndex:(NSUInteger)index {
+    if (index >= [self.searchResults count]) {
+        return nil;
+    }
+
+    return self.searchResults[index];
+}
+
+- (NSDictionary*)searchResultForIndexPath:(NSIndexPath*)indexPath {
+    return [self searchResultForIndex:indexPath.row];
+}
+
 - (NSInteger)tableView:(UITableView*)tableView 
numberOfRowsInSection:(NSInteger)section {
     return self.searchResults.count;
 }
 
 - (CGFloat)tableView:(UITableView*)tableView 
heightForRowAtIndexPath:(NSIndexPath*)indexPath {
-    if ([self.searchResults[indexPath.row][@"attributedText"] length] < 
kWMFMaxStringLength) {
+    NSDictionary* result = [self searchResultForIndexPath:indexPath];
+
+    //Optimization to prevent calculation to get cell size
+    if ([result[@"attributedText"] length] < kWMFMaxStringLength) {
         return floor(kWMFDefaultCellHeight * MENUS_SCALE_MULTIPLIER);
     }
 
-    // Update the sizing cell with any data which could change the cell height.
-    self.offScreenSizingCell.resultLabel.attributedText = 
self.searchResults[indexPath.row][@"attributedText"];
+    self.offScreenSizingCell.resultLabel.attributedText = 
result[@"attributedText"];
 
-    // Determine height for the current configuration of the sizing cell.
     return [tableView heightForSizingCell:self.offScreenSizingCell];
 }
 
 - (UITableViewCell*)tableView:(UITableView*)tableView 
cellForRowAtIndexPath:(NSIndexPath*)indexPath {
     SearchResultCell* cell = (SearchResultCell*)[tableView 
dequeueReusableCellWithIdentifier:kWMFSearchCellID];
 
-    NSString* thumbURL = 
self.searchResults[indexPath.row][@"thumbnail"][@"source"];
+    NSDictionary* result = [self searchResultForIndexPath:indexPath];
 
-    // For performance reasons, "attributedText" is build when data is 
retrieved, not here when the cell
-    // is about to be displayed.
-    cell.resultLabel.attributedText = 
self.searchResults[indexPath.row][@"attributedText"];
+    cell.resultLabel.attributedText = result[@"attributedText"];
+    cell.resultImageView.image      = self.placeholderImage;
 
-    // Set thumbnail placeholder
-    cell.resultImageView.image = self.placeholderImage;
+    NSString* thumbURL = result[@"thumbnail"][@"source"];
+    if (thumbURL) {
+        __block NSString* fileName = [thumbURL lastPathComponent];
 
-    if (!thumbURL) {
-        // Don't bother downloading if no thumbURL
-        return cell;
-    }
-
-    __block NSString* fileName = [thumbURL lastPathComponent];
-
-    // See if cache file found, show it instead of downloading if found.
-    NSString* cacheFilePath = [self.cachePath 
stringByAppendingPathComponent:fileName];
-    BOOL isDirectory        = NO;
-    BOOL fileExists         = [[NSFileManager defaultManager] 
fileExistsAtPath:cacheFilePath isDirectory:&isDirectory];
-    if (fileExists) {
-        cell.resultImageView.image = [UIImage imageWithData:[NSData 
dataWithContentsOfFile:cacheFilePath]];
-    } else {
-        // No thumb found so fetch it.
-        (void)[[ThumbnailFetcher alloc] initAndFetchThumbnailFromURL:thumbURL
-                                                         
withManager:[QueuesSingleton sharedInstance].searchResultsFetchManager
-                                                  thenNotifyDelegate:self];
+        // See if cache file found, show it instead of downloading if found.
+        NSString* cacheFilePath = [self.cachePath 
stringByAppendingPathComponent:fileName];
+        BOOL isDirectory        = NO;
+        BOOL fileExists         = [[NSFileManager defaultManager] 
fileExistsAtPath:cacheFilePath isDirectory:&isDirectory];
+        if (fileExists) {
+            cell.resultImageView.image = [UIImage imageWithData:[NSData 
dataWithContentsOfFile:cacheFilePath]];
+        } else {
+            // No thumb found so fetch it.
+            (void)[[ThumbnailFetcher alloc] 
initAndFetchThumbnailFromURL:thumbURL
+                                                             
withManager:[QueuesSingleton sharedInstance].searchResultsFetchManager
+                                                      thenNotifyDelegate:self];
+        }
     }
 
     return cell;
@@ -655,12 +657,18 @@
 - (void)tableView:(UITableView*)tableView 
didSelectRowAtIndexPath:(NSIndexPath*)indexPath {
     [self hideKeyboard];
 
-    NSString* title = self.searchResults[indexPath.row][@"title"];
+    NSDictionary* result = [self searchResultForIndexPath:indexPath];
+
+    NSString* title = result[@"title"];
 
     [self loadArticleWithTitle:title];
 }
 
 - (void)loadArticleWithTitle:(NSString*)title {
+    if ([title length] == 0) {
+        return;
+    }
+
     [self saveSearchTermToRecentList];
 
     // Set CurrentArticleTitle so web view knows what to load.
diff --git a/Wikipedia/Wikipedia-Info.plist b/Wikipedia/Wikipedia-Info.plist
index 644845e..170138d 100644
--- a/Wikipedia/Wikipedia-Info.plist
+++ b/Wikipedia/Wikipedia-Info.plist
@@ -21,7 +21,7 @@
        <key>CFBundleSignature</key>
        <string>????</string>
        <key>CFBundleVersion</key>
-       <string>4.0.7.1</string>
+       <string>4.1.0.2</string>
        <key>LSRequiresIPhoneOS</key>
        <true/>
        <key>NSLocationWhenInUseUsageDescription</key>
diff --git a/Wikipedia/en.lproj/Localizable.strings 
b/Wikipedia/en.lproj/Localizable.strings
index 213906c..334092e 100644
--- a/Wikipedia/en.lproj/Localizable.strings
+++ b/Wikipedia/en.lproj/Localizable.strings
@@ -282,11 +282,18 @@
 "nearby-location-general-error" = "Unable to determine location. Pull to 
refresh or try again later.";
 "nearby-wifi" = "Enabling Wi-Fi can help your device better determine your 
location.";
 
-"migration-update-progress-label" = "Upgrading local data";
-"migration-update-progress-count-label" = "Migrating Article $1 of $2";
+"migration-update-progress-label" = "Migrating local data";
+"migration-update-progress-count-label" = "Migrating article $1 of $2";
+"migration-prompt-title" = "Looks like we have a history!";
+"migration-prompt-message" = "We need to move your saved & recent pages over. 
This may take a minute…";
+"migration-confirm-button-title" = "Move my stuff over";
+"migration-skip-button-title" = "Delete it";
 
 
 "image-gallery-unknown-owner" = "Uploader unknown.";
+"image-gallery-fetch-image-info-error-title" = "Unable to download images";
+"image-gallery-fetch-image-info-error-message" = "UNable to download the 
images for this article. Please refresh the article and try again.";
+"image-gallery-fetch-image-info-error-ok" = "OK";
 
 "hockeyapp-alert-question" = "Would you like to send a crash report to $1 so 
Wikimedia can review your crash?";
 "hockeyapp-alert-question-with-response-field" = "Would you like to send a 
crash report to $1 so Wikimedia can review your crash? Please describe what 
happened when the crash occurred:";
diff --git a/Wikipedia/qqq.lproj/Localizable.strings 
b/Wikipedia/qqq.lproj/Localizable.strings
index ebd947d..c82ad0c 100644
--- a/Wikipedia/qqq.lproj/Localizable.strings
+++ b/Wikipedia/qqq.lproj/Localizable.strings
@@ -258,6 +258,10 @@
 "nearby-wifi" = "Alert text telling user how to improve location accuracy";
 "migration-update-progress-label" = "Label shown during automatic upgrade of 
local data to new internal format. May be on screen very briefly or for a few 
seconds.";
 "migration-update-progress-count-label" = "Shows the progress of article 
migration in text: 4 / 15, 5 / 15, etc…";
+"migration-prompt-title" = "Title of the alert shown to users before migrating 
all legacy data.";
+"migration-prompt-message" = "Mesasge explaining reason for migration, and 
what will happen if the users skips it.";
+"migration-confirm-button-title" = "Button within migration prompt that 
confirms the user wants to proceed with migration.";
+"migration-skip-button-title" = "Button with migration prompt that indicates 
user wants to skip migration and delete their data.";
 "image-gallery-unknown-owner" = "Fallback text for when an item in the image 
gallery doesn't have a specified owner.";
 "hockeyapp-alert-question" = "Alert dialog question asking user whether to 
send a crash report to HockeyApp crash reporting server. $1 will be replaced 
programmatically with the constant string 'HockeyApp'";
 "hockeyapp-alert-question-with-response-field" = "Alert dialog question asking 
user whether to send a crash report to HockeyApp crash reporting server, and 
asking the user to describe what happened when the crash occurred. $1 will be 
replaced programmatically with the constant string 'HockeyApp'";
@@ -266,3 +270,6 @@
 "hockeyapp-alert-always-send" = "Alert dialog button text for crash reporting 
to always be sent";
 "hockeyapp-alert-do-not-send" = "Alert dialog button text for crash reporting 
to not send the crash report";
 "hockeyapp-alert-privacy" = "Alert dialog button text for HockeyApp privacy 
policy. $1 will be replaced programmatically with the constant string 
'HockeyApp'";
+"image-gallery-fetch-image-info-error-title" = "Title of prompt weh image meta 
download fails in gallery";
+"image-gallery-fetch-image-info-error-message" = "Message of prompt weh image 
meta download fails in gallery";
+"image-gallery-fetch-image-info-error-ok" = "OK button of prompt weh image 
meta download fails in gallery";
diff --git a/WikipediaUnitTests/MWKSectionListTests.m 
b/WikipediaUnitTests/MWKSectionListTests.m
new file mode 100644
index 0000000..d3ccaaf
--- /dev/null
+++ b/WikipediaUnitTests/MWKSectionListTests.m
@@ -0,0 +1,80 @@
+//
+//  MWKSectionListTests.m
+//  Wikipedia
+//
+//  Created by Brian Gerstle on 4/16/15.
+//  Copyright (c) 2015 Wikimedia Foundation. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+#import <XCTest/XCTest.h>
+#import "MWKArticle.h"
+#import "MWKSectionList.h"
+#import "MWKSection.h"
+#import "MWKDataStore.h"
+#import "WMFRandomFileUtilities.h"
+
+#define MOCKITO_SHORTHAND 1
+#import <OCMockito/OCMockito.h>
+
+#define HC_SHORTHAND 1
+#import <OCHamcrest/OCHamcrest.h>
+
+// suppress warning about passing "anything()" to "sectionWithId:"
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wint-conversion"
+
+@interface MWKSectionListTests : XCTestCase
+@end
+
+@implementation MWKSectionListTests
+
+- (void)setUp {
+    [super setUp];
+}
+
+- (void)tearDown {
+    [super tearDown];
+}
+
+- (void)testCreatingSectionListWithNoData {
+    MWKArticle* mockArticle =
+        [[MWKArticle alloc] initWithTitle:nil dataStore:mock([MWKDataStore 
class])];
+    MWKSectionList* emptySectionList = [[MWKSectionList alloc] 
initWithArticle:mockArticle];
+    assertThat(@(emptySectionList.count), is(equalToInt(0)));
+    [MKTVerifyCount(mockArticle.dataStore, never()) sectionWithId:anything() 
article:anything()];
+}
+
+- (void)testSectionListInitializationExeptionHandling {
+    MWKArticle* mockArticle =
+        [[MWKArticle alloc] initWithTitle:nil dataStore:mock([MWKDataStore 
class])];
+
+    [self addEmptyFolderForSection:0 title:anything() 
mockDataStore:mockArticle.dataStore];
+
+    // mock an exception, simulating the case where required fields are missing
+    [given([mockArticle.dataStore sectionWithId:anything()
+                                        article:mockArticle])
+     willThrow:[NSException new]];
+
+    MWKSectionList* emptySectionList = [[MWKSectionList alloc] 
initWithArticle:mockArticle];
+    assertThat(@(emptySectionList.count), is(equalToInt(0)));
+}
+
+- (void)addEmptyFolderForSection:(int)sectionId
+                           title:(id)titleMatcher
+                   mockDataStore:(MWKDataStore*)mockDataStore {
+    // create an empty section directory, so that our section list will reach 
the code path
+    // where an exception will be thrown when trying to read the section data
+    NSString* randomDirectory = WMFRandomTemporaryPath();
+    NSString* randomPath      = [randomDirectory 
stringByAppendingPathComponent:@"sections/0"];
+    BOOL didCreateRandomPath  = [[NSFileManager defaultManager] 
createDirectoryAtPath:randomPath
+                                                          
withIntermediateDirectories:YES
+                                                                           
attributes:nil
+                                                                               
 error:nil];
+    NSParameterAssert(didCreateRandomPath);
+    [given([mockDataStore pathForTitle:anything()]) 
willReturn:randomDirectory];
+}
+
+@end
+
+#pragma clang diagnostic pop
diff --git a/WikipediaUnitTests/OldDataSchemaMigratorTests.m 
b/WikipediaUnitTests/OldDataSchemaMigratorTests.m
index c70ed50..f9e5cb9 100644
--- a/WikipediaUnitTests/OldDataSchemaMigratorTests.m
+++ b/WikipediaUnitTests/OldDataSchemaMigratorTests.m
@@ -73,8 +73,41 @@
     [self verifyMigrationOfArticle:oldArticle];
 }
 
+- (void)testArticleWithMissingRequiredFieldsIsGracefullySkipped {
+    Article* oldArticle = [self createOldArticleWithSections:10 
imagesPerSection:5];
+    // lastModified is a required field
+    oldArticle.lastmodified = nil;
+    [self verifySkippedMigrationOfArticle:oldArticle];
+}
+
+- (void)testArticleWithInvalidSectionIsGracefullySkipped {
+    Article* oldArticle = [self createOldArticleWithSections:10 
imagesPerSection:5];
+    Section* section = oldArticle.sectionsBySectionId.lastObject;
+    // sectionId is a required field
+    section.sectionId = nil;
+    [self verifySkippedMigrationOfArticle:oldArticle];
+}
+
+- (void)testArticleWithInvalidSectionImageIsGracefullySkipped {
+    Article* oldArticle = [self createOldArticleWithSections:10 
imagesPerSection:5];
+    Section* section = oldArticle.sectionsBySectionId.lastObject;
+    SectionImage* image = section.sectionImagesByIndex.lastObject;
+    // sourceUrl is a required field
+    image.image.sourceUrl = nil;
+    [self verifySkippedMigrationOfArticle:oldArticle];
+}
+
 #pragma mark - Test Utils
 
+- (void)verifySkippedMigrationOfArticle:(Article*)oldArticle {
+    XCTAssertNoThrow([self.migrator migrateArticle:oldArticle],
+                     @"Failed to catch an article migration exception.");
+    MWKTitle* migratedArticleTitle = [self.migrator 
migrateArticleTitle:oldArticle];
+    NSString* articleDataPath = [self.dataStore 
pathForTitle:migratedArticleTitle];
+    XCTAssertFalse([[NSFileManager defaultManager] 
fileExistsAtPath:articleDataPath],
+                   @"Expected article to not be saved due to exception during 
migration.");
+}
+
 - (void)verifyMigrationOfArticle:(Article*)oldArticle {
     [self.migrator migrateArticle:oldArticle];
 

-- 
To view, visit https://gerrit.wikimedia.org/r/205287
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I4e0c130c3aa9e66b7e2af312294b3e21cfc26b7e
Gerrit-PatchSet: 3
Gerrit-Project: apps/ios/wikipedia
Gerrit-Branch: master
Gerrit-Owner: Bgerstle <[email protected]>
Gerrit-Reviewer: Dr0ptp4kt <[email protected]>
Gerrit-Reviewer: Fjalapeno <[email protected]>
Gerrit-Reviewer: Mhurd <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to