diff options
Diffstat (limited to 'Example/Database/Tests/Integration/FPersist.m')
-rw-r--r-- | Example/Database/Tests/Integration/FPersist.m | 489 |
1 files changed, 489 insertions, 0 deletions
diff --git a/Example/Database/Tests/Integration/FPersist.m b/Example/Database/Tests/Integration/FPersist.m new file mode 100644 index 0000000..2326e08 --- /dev/null +++ b/Example/Database/Tests/Integration/FPersist.m @@ -0,0 +1,489 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <XCTest/XCTest.h> +#import <Foundation/Foundation.h> +#import "FPersist.h" +#import "FIRDatabaseReference.h" +#import "FIRDatabaseReference_Private.h" +#import "FRepo_Private.h" +#import "FTestHelpers.h" +#import "FDevice.h" +#import "FIRDatabaseQuery_Private.h" + +@implementation FPersist + +- (void) setUp { + [super setUp]; + + NSFileManager *fileManager = [NSFileManager defaultManager]; + + NSString *baseDir = [FPersist getFirebaseDir]; + // HACK: We want to clean up old persistence files from previous test runs, but on OSX, baseDir is going to be something + // like /Users/michael/Documents/firebase, and we probably shouldn't blindly delete it, since somebody might have actual + // documents there. We should probably change the directory where we store persistence on OSX to .firebase or something + // to avoid colliding with real files, but for now, we'll leave it and just manually delete each of the /0, /1, /2, etc. + // directories that may exist from previous test runs. As of now (2014/09/07), these directories only go up to ~50, but + // if we add a ton more tests, we may need to increase the 100. But I'm guessing we'll rewrite persistence and move the + // persistence folder before then though. + for(int i = 0; i < 100; i++) { + // TODO: This hack is uneffective because the format now follows different rules. Persistence really needs a purge + // option + NSString *dir = [NSString stringWithFormat:@"%@/%d", baseDir, i]; + if ([fileManager fileExistsAtPath:dir]) { + NSError *error; + [[NSFileManager defaultManager] removeItemAtPath:dir error:&error]; + if (error) { + XCTFail(@"Failed to clear persisted data at %@: %@", dir, error); + } + } + } +} + +- (void) testSetIsResentAfterRestart { + FIRDatabaseReference *readerRef = [FTestHelpers getRandomNode]; + NSString *url = [readerRef description]; + FDevice* device = [[FDevice alloc] initOfflineWithUrl:url]; + + // Monitor the data at this location. + __block FIRDataSnapshot *readSnapshot = nil; + [readerRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + readSnapshot = snapshot; + }]; + + // Do some sets while offline and then "kill" the app, so it doesn't get sent to Firebase. + [device do:^(FIRDatabaseReference *ref) { + [ref setValue:@{ @"a": @42, @"b": @3.1415, @"c": @"hello", @"d": @{ @"dd": @"dd-val", @".priority": @"d-pri"} }]; + [[ref child:@"a"] setValue:@"a-val"]; + [[ref child:@"c"] setPriority:@"c-pri"]; + [ref updateChildValues:@{ @"b": @"b-val"}]; + }]; + + // restart and wait for "idle" (so all pending puts should have been sent). + [device restartOnline]; + [device waitForIdleUsingWaiter:self]; + + // Pending sets should have gone through. + id expected = @{ + @"a": @"a-val", + @"b": @"b-val", + @"c": @{ @".value": @"hello", @".priority": @"c-pri" }, + @"d": @{ @"dd": @"dd-val", @".priority": @"d-pri" } + }; + [self waitForExportValueOf:readerRef toBe:expected]; + + // Set the value to something else (12). + [readerRef setValue:@12]; + + // "restart" the app again and make sure it doesn't set it to 42 again. + [device restartOnline]; + [device waitForIdleUsingWaiter:self]; + + // Make sure data is still 12. + [self waitForRoundTrip:readerRef]; + XCTAssertEqual(readSnapshot.value, @12, @"Read data should still be 12."); + [device dispose]; +} + +- (void) testSetIsReappliedAfterRestart { + FDevice* device = [[FDevice alloc] initOffline]; + + // Do some sets while offline and then "kill" the app, so it doesn't get sent to Firebase. + [device do:^(FIRDatabaseReference *ref) { + [ref setValue:@{ @"a": @42, @"b": @3.1415, @"c": @"hello" }]; + [[ref child:@"a"] setValue:@"a-val"]; + [[ref child:@"c"] setPriority:@"c-pri"]; + [ref updateChildValues:@{ @"b": @"b-val"}]; + }]; + + // restart the app offline and observe the data. + [device restartOffline]; + + // Pending sets should be visible + id expected = @{ + @"a": @"a-val", + @"b": @"b-val", + @"c": @{ @".value": @"hello", @".priority": @"c-pri" } + }; + [device do:^(FIRDatabaseReference *ref) { + [self waitForExportValueOf:ref toBe:expected]; + }]; + [device dispose]; +} + +- (void) testServerDataCachedOffline1 { + FIRDatabaseReference *writerRef = [FTestHelpers getRandomNode]; + FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writerRef description] ]; + __block BOOL done = NO; + id data = @{@"a": @1, @"b": @2}; + [writerRef setValue:data withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + done = YES; + }]; + WAIT_FOR(done); + + // Wait for the data to get it cached. + [device do:^(FIRDatabaseReference *ref) { + [self waitForValueOf:ref toBe:data]; + }]; + + // Should still be there after restart, offline. + [device restartOffline]; + [device do:^(FIRDatabaseReference *ref) { + [self waitForValueOf:ref toBe:data]; + }]; + + // Children should be there too. + [device restartOffline]; + [device do:^(FIRDatabaseReference *ref) { + [self waitForValueOf:[ref child:@"a"] toBe:@1]; + }]; + [device dispose]; +} + +- (void) testServerDataCompleteness1 { + FIRDatabaseReference *writerRef = [FTestHelpers getRandomNode]; + FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writerRef description] ]; + id data = @{@"child": @{@"a": @1, @"b": @2 }, @"other": @"blah"}; + [self waitForCompletionOf:writerRef setValue:data]; + + // Wait for each child to get it cached (but not the parent). + [device do:^(FIRDatabaseReference *ref) { + [self waitForValueOf:[ref child:@"child/a"] toBe:@1]; + [self waitForValueOf:[ref child:@"child/b"] toBe:@2]; + [self waitForValueOf:[ref child:@"other"] toBe:@"blah"]; + }]; + + // Restart, offline, should get child_added events, but not value. + [device restartOffline]; + __block BOOL gotA, gotB; + [device do:^(FIRDatabaseReference *ref) { + FIRDatabaseReference *childRef = [ref child:@"child"]; + [childRef observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) { + if ([snapshot.key isEqualToString:@"a"]) { + XCTAssertEqualObjects(snapshot.value, @1, @"Got a"); + gotA = YES; + } else if ([snapshot.key isEqualToString:@"b"]) { + XCTAssertEqualObjects(snapshot.value, @2, @"Got a"); + gotB = YES; + } else { + XCTFail(@"Unexpected child event."); + } + }]; + + // Listen for value events (which we should *not* get). + [childRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + XCTFail(@"Got a value event with incomplete data!"); + }]; + + // Wait for another location just to make sure we wait long enough that we /would/ get a value event if it + // was coming. + [self waitForValueOf:[ref child:@"other"] toBe:@"blah"]; + }]; + + XCTAssertTrue(gotA && gotB, @"Got a and b."); + [device dispose]; +} + +- (void) testServerDataCompleteness2 { + FIRDatabaseReference *writerRef = [FTestHelpers getRandomNode]; + FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writerRef description] ]; + id data = @{@"a": @1, @"b": @2}; + [self waitForCompletionOf:writerRef setValue:data]; + + // Wait for the children individually. + [device do:^(FIRDatabaseReference *ref) { + [self waitForValueOf:[ref child:@"a"] toBe:@1]; + [self waitForValueOf:[ref child:@"b"] toBe:@2]; + }]; + + // Should still be there after restart, offline. + [device restartOffline]; + [device do:^(FIRDatabaseReference *ref) { + [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + // No-op. Just triggering a listen at this location. + }]; + [self waitForValueOf:[ref child:@"a"] toBe:@1]; + [self waitForValueOf:[ref child:@"b"] toBe:@2]; + }]; + [device dispose]; +} + +- (void)testServerDataLimit { + FIRDatabaseReference *writerRef = [FTestHelpers getRandomNode]; + FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writerRef description] ]; + [self waitForCompletionOf:writerRef setValue:@{@"a": @1, @"b": @2, @"c": @3}]; + + // Cache limit(2) of the data. + [device do:^(FIRDatabaseReference *ref) { + FIRDatabaseQuery *limitRef = [ref queryLimitedToLast:2]; + [self waitForValueOf:limitRef toBe:@{@"b": @2, @"c": @3 }]; + }]; + + // We should be able to get limit(2) data offline, but not the whole node. + [device restartOffline]; + [device do:^(FIRDatabaseReference *ref) { + [ref observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + XCTFail(@"Got value event for whole node!"); + }]; + + FIRDatabaseQuery *limitRef = [ref queryLimitedToLast:2]; + [self waitForValueOf:limitRef toBe:@{@"b": @2, @"c": @3 }]; + }]; + [device dispose]; +} + +- (void)testRemoveWhileOfflineAndRestart { + FIRDatabaseReference *writerRef = [FTestHelpers getRandomNode]; + FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writerRef description] ]; + + [[writerRef child:@"test"] setValue:@"test"]; + [device do:^(FIRDatabaseReference *ref) { + // Cache this location. + __block id val = nil; + [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + val = snapshot.value; + }]; + [self waitUntil:^BOOL { + return [val isEqual:@{@"test": @"test"}]; + }]; + }]; + [device restartOffline]; + + __block BOOL done = NO; + [writerRef removeValueWithCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + done = YES; + }]; + WAIT_FOR(done); + + [device goOnline]; + [device waitForIdleUsingWaiter:self]; + [device do:^(FIRDatabaseReference *ref) { + [self waitForValueOf:ref toBe:[NSNull null]]; + }]; + [device dispose]; +} + + +- (void)testDeltaSyncAfterRestart { + FIRDatabaseReference *writerRef = [FTestHelpers getRandomNode]; + FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writerRef description] ]; + + [writerRef setValue:@"test"]; + + [device do:^(FIRDatabaseReference *ref) { + // Cache this location. + __block id val = nil; + [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + val = snapshot.value; + }]; + [self waitUntil:^BOOL { + return [val isEqual:@"test"]; + }]; + XCTAssertEqual(ref.repo.dataUpdateCount, 1L, @"Should have gotten one update."); + }]; + [device restartOnline]; + + [device waitForIdleUsingWaiter:self]; + [device do:^(FIRDatabaseReference *ref) { + [self waitForValueOf:ref toBe:@"test"]; + XCTAssertEqual(ref.repo.dataUpdateCount, 0L, @"Should have gotten no updates."); + }]; + [device dispose]; +} + +- (void)testDeltaSyncWorksWithUnfilteredQuery { + FIRDatabaseReference *writerRef = [FTestHelpers getRandomNode]; + FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writerRef description] ]; + + // List must be large enough to trigger delta sync. + NSMutableDictionary *longList = [[NSMutableDictionary alloc] init]; + for(NSInteger i = 0; i < 50; i++) { + NSString *key = [[writerRef childByAutoId] key]; + longList[key] = @{ @"order": @1, @"text": @"This is an awesome message!" }; + } + + [writerRef setValue:longList]; + + [device do:^(FIRDatabaseReference *ref) { + // Cache this location. + [self waitForValueOf:[ref queryOrderedByChild:@"order"] toBe:longList]; + XCTAssertEqual(ref.repo.dataUpdateCount, 1L, @"Should have gotten one update."); + }]; + [device restartOffline]; + + // Add a new child while the device is offline. + FIRDatabaseReference *newChildRef = [writerRef childByAutoId]; + NSDictionary *newChild = @{ @"order": @50, @"text": @"This is a new appended child!" }; + + [self waitForCompletionOf:newChildRef setValue:newChild]; + longList[[newChildRef key]] = newChild; + + [device goOnline]; + [device do:^(FIRDatabaseReference *ref) { + // Wait for updated value with new child. + [self waitForValueOf:[ref queryOrderedByChild:@"order"] toBe:longList]; + XCTAssertEqual(ref.repo.rangeMergeUpdateCount, 1L, @"Should have gotten a range merge update."); + }]; + [device dispose]; +} + +- (void) testPutsAreRestoredInOrder { + FDevice *device = [[FDevice alloc] initOffline]; + + // Store puts which should have a putId with 10 which is lexiographical small than 9 + [device do:^(FIRDatabaseReference *ref) { + for (int i = 0; i < 11; i++) { + [ref setValue:[NSNumber numberWithInt:i]]; + } + }]; + + // restart the app offline and observe the data. + [device restartOffline]; + + // Make sure that the write with putId 10 wins, not 9 + id expected = @10; + [device do:^(FIRDatabaseReference *ref) { + [self waitForExportValueOf:ref toBe:expected]; + }]; + [device dispose]; +} + +- (void) testStoreSetsPerf1 { + if (!runPerfTests) return; + // Disable persistence in FDevice for comparison without persistence + FDevice *device = [[FDevice alloc] initOnline]; + + __block BOOL done = NO; + [device do:^(FIRDatabaseReference *ref) { + NSDate *start = [NSDate date]; + [self writeChildren:ref count:1000 size:100 waitForComplete:NO]; + + [self waitForQueue:ref]; + + NSLog(@"Elapsed: %f", [[NSDate date] timeIntervalSinceDate:start]); + done = YES; + }]; + + WAIT_FOR(done); + [device dispose]; +} + +- (void) testStoreListenPerf1 { + if (!runPerfTests) return; + // Disable persistence in FDevice for comparison without persistence + + // Write 1000 x 100-byte children, to read back. + unsigned int count = 1000; + FIRDatabaseReference *writer = [FTestHelpers getRandomNode]; + [self writeChildren:writer count:count size:100]; + + FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writer description]]; + + __block BOOL done = NO; + [device do:^(FIRDatabaseReference *ref) { + NSDate *start = [NSDate date]; + [ref observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + // Wait to make sure we're done persisting everything. + [self waitForQueue:ref]; + XCTAssertEqual(snapshot.childrenCount, count, @"Got correct data."); + NSLog(@"Elapsed: %f", [[NSDate date] timeIntervalSinceDate:start]); + done = YES; + }]; + }]; + + WAIT_FOR(done); + [device dispose]; +} + +- (void) testRestoreListenPerf1 { + if (!runPerfTests) return; + + // NOTE: Since this is testing restoration of data from cache after restarting, it only works with persistence on. + + // Write 1000 * 100-byte children, to read back. + unsigned int count = 1000; + FIRDatabaseReference *writer = [FTestHelpers getRandomNode]; + [self writeChildren:writer count:count size:100]; + + FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writer description]]; + + // Get the data cached. + __block BOOL done = NO; + [device do:^(FIRDatabaseReference *ref) { + [ref observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + XCTAssertEqual(snapshot.childrenCount, count, @"Got correct data."); + done = YES; + }]; + }]; + WAIT_FOR(done); + + // Restart offline and see how long it takes to restore the data from cache. + [device restartOffline]; + done = NO; + [device do:^(FIRDatabaseReference *ref) { + NSDate *start = [NSDate date]; + [ref observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + // Wait to make sure we're done persisting everything. + XCTAssertEqual(snapshot.childrenCount, count, @"Got correct data."); + [self waitForQueue:ref]; + NSLog(@"Elapsed: %f", [[NSDate date] timeIntervalSinceDate:start]); + done = YES; + }]; + }]; + + WAIT_FOR(done); + [device dispose]; +} + +- (void)writeChildren:(FIRDatabaseReference *)writer count:(unsigned int)count size:(unsigned int)size { + [self writeChildren:writer count:count size:size waitForComplete:YES]; +} + +- (void)writeChildren:(FIRDatabaseReference *)writer count:(unsigned int)count size:(unsigned int)size waitForComplete:(BOOL)waitForComplete { + __block BOOL done = NO; + + NSString *data = [self randomStringOfLength:size]; + for(int i = 0; i < count; i++) { + [[writer childByAutoId] setValue:data withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + if (i == (count - 1)) { + done = YES; + } + }]; + } + if (waitForComplete) { + WAIT_FOR(done); + } +} + +NSString *letters = @"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; +- (NSString*) randomStringOfLength:(unsigned int)len { + NSMutableString *randomString = [NSMutableString stringWithCapacity: len]; + + for (int i=0; i<len; i++) { + [randomString appendFormat: @"%C", [letters characterAtIndex: arc4random() % [letters length]]]; + } + return randomString; +} + ++ (NSString *) getFirebaseDir { + NSArray *dirPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + NSString *documentsDir = [dirPaths objectAtIndex:0]; + NSString *firebaseDir = [documentsDir stringByAppendingPathComponent:@"firebase"]; + + return firebaseDir; +} + +@end |