| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510 |
- /*
- * 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 "FPersist.h"
- #import <Foundation/Foundation.h>
- #import <XCTest/XCTest.h>
- #import "FDevice.h"
- #import "FIRDatabaseQuery_Private.h"
- #import "FIRDatabaseReference.h"
- #import "FIRDatabaseReference_Private.h"
- #import "FRepo_Private.h"
- #import "FTestHelpers.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
|