aboutsummaryrefslogtreecommitdiffhomepage
path: root/Example/Database/Tests/Integration/FPersist.m
diff options
context:
space:
mode:
Diffstat (limited to 'Example/Database/Tests/Integration/FPersist.m')
-rw-r--r--Example/Database/Tests/Integration/FPersist.m489
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