diff options
author | Paul Beusterien <paulbeusterien@google.com> | 2017-05-15 12:27:07 -0700 |
---|---|---|
committer | Paul Beusterien <paulbeusterien@google.com> | 2017-05-15 12:27:07 -0700 |
commit | 98ba64449a632518bd2b86fe8d927f4a960d3ddc (patch) | |
tree | 131d9c4272fa6179fcda6c5a33fcb3b1bd57ad2e /Example/Database/Tests/Unit/FLevelDBStorageEngineTests.m | |
parent | 32461366c9e204a527ca05e6e9b9404a2454ac51 (diff) |
Initial
Diffstat (limited to 'Example/Database/Tests/Unit/FLevelDBStorageEngineTests.m')
-rw-r--r-- | Example/Database/Tests/Unit/FLevelDBStorageEngineTests.m | 583 |
1 files changed, 583 insertions, 0 deletions
diff --git a/Example/Database/Tests/Unit/FLevelDBStorageEngineTests.m b/Example/Database/Tests/Unit/FLevelDBStorageEngineTests.m new file mode 100644 index 0000000..658a894 --- /dev/null +++ b/Example/Database/Tests/Unit/FLevelDBStorageEngineTests.m @@ -0,0 +1,583 @@ +/* + * 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 "FLevelDBStorageEngine.h" +#import "FSnapshotUtilities.h" +#import "FQueryParams.h" +#import "FPathIndex.h" +#import "FTrackedQuery.h" +#import "FWriteRecord.h" +#import "FTestHelpers.h" +#import "FEmptyNode.h" + +@interface FLevelDBStorageEngineTests : XCTestCase + +@end + +@implementation FLevelDBStorageEngineTests + +- (FLevelDBStorageEngine *)cleanStorageEngine { + NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:@"test-db"]; + FLevelDBStorageEngine *db = [[FLevelDBStorageEngine alloc] initWithPath:path]; + [db purgeEverything]; + return db; +} + +#define SAMPLE_NODE ([FSnapshotUtilities nodeFrom:@{ @"foo": @{ @"bar": @YES, @"baz": @"string" }, @"qux": @2, @"quu": @1.2 }]) + +#define ONE_MEG_NODE ([FTestHelpers leafNodeOfSize:1024*1024]) +#define FIVE_MEG_NODE ([FTestHelpers leafNodeOfSize:5*1024*1024]) +#define TEN_MEG_NODE ([FTestHelpers leafNodeOfSize:10*1024*1024]) +#define TEN_MEG_MINUS_ONE_NODE ([FTestHelpers leafNodeOfSize:10*1024*1024 - 1]) + +#define SAMPLE_PARAMS \ + ([[[[[FQueryParams defaultInstance] orderBy:[[FPathIndex alloc] initWithPath:PATH(@"child")]] \ + startAt:[FSnapshotUtilities nodeFrom:@"startVal"] childKey:@"startKey"] \ + endAt:[FSnapshotUtilities nodeFrom:@"endVal"] childKey:@"endKey"] \ + limitToLast:5]) + +#define SAMPLE_QUERY \ + ([[FQuerySpec alloc] initWithPath:[FPath pathWithString:@"foo"] params:SAMPLE_PARAMS]) + +#define DEFAULT_FOO_QUERY \ + ([[FQuerySpec alloc] initWithPath:[FPath pathWithString:@"foo"] params:[FQueryParams defaultInstance]]) + +#define SAMPLE_TRACKED_QUERY \ + ([[FTrackedQuery alloc] initWithId:1 \ + query:SAMPLE_QUERY \ + isPinned:NO \ + lastUse:100 \ + Active:NO \ + isComplete:NO]) +#define OVERWRITE_RECORD(__path, __node, __writeId) \ + ([[FWriteRecord alloc] initWithPath:[FPath pathWithString:__path] overwrite:__node writeId:__writeId visible:YES]) + +#define MERGE_RECORD(__path, __merge, __writeId) \ + ([[FWriteRecord alloc] initWithPath:[FPath pathWithString:__path] merge:__merge writeId:__writeId]) + +- (void)testUserWriteIsPersisted { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine saveUserOverwrite:SAMPLE_NODE atPath:[FPath pathWithString:@"foo/bar"] writeId:1]; + XCTAssertEqualObjects(engine.userWrites, @[OVERWRITE_RECORD(@"foo/bar", SAMPLE_NODE, 1)]); +} + +- (void)testUserMergeIsPersisted { + FCompoundWrite *merge = [FCompoundWrite compoundWriteWithValueDictionary:@{@"foo": @{@"bar": @1, @"baz": @"string"}, @"quu": @YES}]; + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine saveUserMerge:merge atPath:PATH(@"foo/bar") writeId:1]; + XCTAssertEqualObjects(engine.userWrites, @[MERGE_RECORD(@"foo/bar", merge, 1)]); +} + +- (void)testDeepUserMergeIsPersisted { + FCompoundWrite *merge = [FCompoundWrite compoundWriteWithValueDictionary:@{@"foo/bar": @1, @"foo/baz": @"string", @"quu/qux": @YES, @"shallow": @2}]; + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine saveUserMerge:merge atPath:PATH(@"foo/bar") writeId:1]; + XCTAssertEqualObjects(engine.userWrites, @[MERGE_RECORD(@"foo/bar", merge, 1)]); +} + +- (void)testSameWriteIdOverwritesOldWrite { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine saveUserOverwrite:NODE(@"first") atPath:PATH(@"foo/bar") writeId:1]; + [engine saveUserOverwrite:NODE(@"second") atPath:PATH(@"other/path") writeId:1]; + XCTAssertEqualObjects(engine.userWrites, @[OVERWRITE_RECORD(@"other/path", NODE(@"second"), 1)]); +} + +- (void)testHugeWriteWorks { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine saveUserOverwrite:TEN_MEG_NODE atPath:PATH(@"foo/bar") writeId:1]; + FCompoundWrite *merge = [[FCompoundWrite emptyWrite] addWrite:TEN_MEG_NODE atKey:@"update"]; + [engine saveUserMerge:merge atPath:PATH(@"foo/bar") writeId:2]; + NSArray *expected = @[OVERWRITE_RECORD(@"foo/bar", TEN_MEG_NODE, 1), MERGE_RECORD(@"foo/bar", merge, 2)]; + XCTAssertEqualObjects(engine.userWrites, expected); +} + +- (void)testHugeWritesCanBeDeleted { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine saveUserOverwrite:TEN_MEG_NODE atPath:PATH(@"foo/bar") writeId:1]; + [engine removeUserWrite:1]; + XCTAssertTrue(engine.userWrites.count == 0); +} + +- (void)testHugeWritesCanBeInterleavedWithSmallWrites { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + + [engine saveUserOverwrite:NODE(@"node-1") atPath:PATH(@"foo/1") writeId:1]; + [engine saveUserOverwrite:TEN_MEG_NODE atPath:PATH(@"foo/2") writeId:2]; + [engine saveUserOverwrite:NODE(@"node-3") atPath:PATH(@"foo/3") writeId:3]; + [engine saveUserOverwrite:FIVE_MEG_NODE atPath:PATH(@"foo/4") writeId:4]; + + NSArray *expected = @[OVERWRITE_RECORD(@"foo/1", NODE(@"node-1"), 1), + OVERWRITE_RECORD(@"foo/2", TEN_MEG_NODE, 2), + OVERWRITE_RECORD(@"foo/3", NODE(@"node-3"), 3), + OVERWRITE_RECORD(@"foo/4", FIVE_MEG_NODE, 4)]; + XCTAssertEqualObjects(engine.userWrites, expected); +} + +// This is ported from the Android client and doesn't really make sense since we don't have multi part writes, but +// It's always good to have tests, so what the heck... +- (void)testSameWriteIdOverwritesOldMultiPartWrite { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + + [engine saveUserOverwrite:TEN_MEG_NODE atPath:PATH(@"foo/bar") writeId:1]; + [engine saveUserOverwrite:NODE(@"second") atPath:PATH(@"other/path") writeId:1]; + + XCTAssertEqualObjects(engine.userWrites, @[OVERWRITE_RECORD(@"other/path", NODE(@"second"), 1)]); +} + +- (void)testWritesAreReturnedInOrder { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + NSUInteger count = 20; + for (NSUInteger i = count - 1; i > 0; i--) { + NSString *path = [NSString stringWithFormat:@"foo/%lu", (unsigned long)i]; + [engine saveUserOverwrite:NODE(@(i)) atPath:PATH(path) writeId:i]; + } + NSString *path = [NSString stringWithFormat:@"foo/%lu", (unsigned long)count]; + [engine saveUserOverwrite:NODE(@(count)) atPath:PATH(path) writeId:count]; + NSArray *userWrites = engine.userWrites; + XCTAssertEqual(userWrites.count, count); + for (NSUInteger i = 1; i <= count; i++) { + NSString *path = [NSString stringWithFormat:@"foo/%lu", (unsigned long)i]; + XCTAssertEqualObjects(userWrites[i-1], OVERWRITE_RECORD(path, NODE(@(i)), i)); + } +} + +- (void)testRemoveAllUserWrites { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + + [engine saveUserOverwrite:NODE(@"node-1") atPath:PATH(@"foo/1") writeId:1]; + [engine saveUserOverwrite:TEN_MEG_NODE atPath:PATH(@"foo/2") writeId:2]; + FCompoundWrite *merge = [[FCompoundWrite emptyWrite] addWrite:TEN_MEG_NODE atKey:@"update"]; + [engine saveUserMerge:merge atPath:PATH(@"foo/bar") writeId:3]; + [engine removeAllUserWrites]; + XCTAssertEqualObjects(engine.userWrites, @[]); +} + + +- (void)testCacheSavedIsReturned { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine updateServerCache:SAMPLE_NODE atPath:PATH(@"foo") merge:NO]; + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], SAMPLE_NODE); +} + +- (void)testCacheSavedIsReturnedAtRoot { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine updateServerCache:SAMPLE_NODE atPath:PATH(@"") merge:NO]; + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"")], SAMPLE_NODE); +} + +- (void)testLaterCacheWritesOverwriteOlderWrites { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine updateServerCache:SAMPLE_NODE atPath:PATH(@"foo") merge:NO]; + [engine updateServerCache:NODE(@"later-bar") atPath:PATH(@"foo/bar") merge:NO]; + // this does not affect the node + [engine updateServerCache:NODE(@"unaffected") atPath:PATH(@"unaffected") merge:NO]; + [engine updateServerCache:NODE(@"later-qux") atPath:PATH(@"foo/later-qux") merge:NO]; + [engine updateServerCache:NODE(@"latest-bar") atPath:PATH(@"foo/bar") merge:NO]; + + id<FNode> expected = [[SAMPLE_NODE updateImmediateChild:@"bar" withNewChild:NODE(@"latest-bar")] + updateImmediateChild:@"later-qux" withNewChild:NODE(@"later-qux")]; + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], expected); +} + +- (void)testLaterCacheWritesOverwriteOlderDeeperWrites { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine updateServerCache:SAMPLE_NODE atPath:PATH(@"foo") merge:NO]; + [engine updateServerCache:NODE(@"later-bar") atPath:PATH(@"foo/bar") merge:NO]; + // this does not affect the node + [engine updateServerCache:NODE(@"unaffected") atPath:PATH(@"unaffected") merge:NO]; + [engine updateServerCache:NODE(@"later-qux") atPath:PATH(@"foo/later-qux") merge:NO]; + [engine updateServerCache:NODE(@"latest-bar") atPath:PATH(@"foo/bar") merge:NO]; + [engine updateServerCache:NODE(@"latest-foo") atPath:PATH(@"foo") merge:NO]; + + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], NODE(@"latest-foo")); +} + +- (void)testLaterCacheWritesDontAffectEarlierWritesAtUnaffectedPath { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine updateServerCache:SAMPLE_NODE atPath:PATH(@"foo") merge:NO]; + // this does not affect the node + [engine updateServerCache:NODE(@"unaffected") atPath:PATH(@"unaffected") merge:NO]; + [engine updateServerCache:NODE(@"latest-foo") atPath:PATH(@"foo") merge:NO]; + + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"unaffected")], NODE(@"unaffected")); +} + +- (void)testMergeOnEmptyCacheGivesResults { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + NSDictionary *mergeData = @{@"foo": @"foo-value", @"bar": @"bar-value"}; + FCompoundWrite *merge = [FCompoundWrite compoundWriteWithValueDictionary:mergeData]; + [engine updateServerCacheWithMerge:merge atPath:PATH(@"foo")]; + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], NODE(mergeData)); +} + +- (void)testMergePartlyOverwritingPreviousWrite { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + id<FNode> existingNode = NODE((@{@"foo": @"foo-value", @"bar": @"bar-value"})); + [engine updateServerCache:existingNode atPath:PATH(@"foo") merge:NO]; + + FCompoundWrite *merge = [FCompoundWrite compoundWriteWithValueDictionary:@{@"foo": @"new-foo-value", @"baz": @"baz-value"}]; + [engine updateServerCacheWithMerge:merge atPath:PATH(@"foo")]; + + id<FNode> expected = NODE((@{@"foo": @"new-foo-value", @"bar": @"bar-value", @"baz": @"baz-value"})); + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], expected); +} + +- (void)testDeepMergePartlyOverwritingPreviousWrite { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + id<FNode> existingNode = NODE((@{@"foo": @{ @"bar": @"bar-value", @"baz": @"baz-value"}, @"qux": @"qux-value"})); + [engine updateServerCache:existingNode atPath:PATH(@"foo") merge:NO]; + + FCompoundWrite *merge = [FCompoundWrite compoundWriteWithValueDictionary:@{@"foo/bar": @"new-bar-value", @"quu": @"quu-value"}]; + [engine updateServerCacheWithMerge:merge atPath:PATH(@"foo")]; + + id<FNode> expected = NODE((@{@"foo": @{ @"bar": @"new-bar-value", @"baz": @"baz-value"}, @"qux": @"qux-value", @"quu": @"quu-value"})); + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], expected); +} + +- (void)testMergePartlyOverwritingPreviousMerge { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + FCompoundWrite *merge1 = [FCompoundWrite compoundWriteWithValueDictionary:@{@"foo": @"foo-value", @"bar": @"bar-value"}]; + [engine updateServerCacheWithMerge:merge1 atPath:PATH(@"foo")]; + + FCompoundWrite *merge2 = [FCompoundWrite compoundWriteWithValueDictionary:@{@"foo": @"new-foo-value", @"baz": @"baz-value"}]; + [engine updateServerCacheWithMerge:merge2 atPath:PATH(@"foo")]; + + id<FNode> expected = NODE((@{@"foo": @"new-foo-value", @"bar": @"bar-value", @"baz": @"baz-value"})); + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], expected); +} + +- (void)testOverwriteRemovesPreviousMerge { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + id<FNode> initial = NODE((@{@"foo": @"foo-value", @"bar": @"bar-value"})); + [engine updateServerCache:initial atPath:PATH(@"foo") merge:NO]; + + FCompoundWrite *merge2 = [FCompoundWrite compoundWriteWithValueDictionary:@{@"foo": @"new-foo-value", @"baz": @"baz-value"}]; + [engine updateServerCacheWithMerge:merge2 atPath:PATH(@"foo")]; + + id<FNode> replacingNode = NODE((@{@"qux": @"qux-value", @"quu": @"quu-value"})); + [engine updateServerCache:replacingNode atPath:PATH(@"foo") merge:NO]; + + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], replacingNode); +} + +- (void)testEmptyOverwriteDeletesNodeFromHigherWrite { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + + id<FNode> initial = NODE((@{@"foo": @"foo-value", @"bar": @"bar-value"})); + [engine updateServerCache:initial atPath:PATH(@"foo") merge:NO]; + + // delete bar + [engine updateServerCache:NODE(nil) atPath:PATH(@"foo/bar") merge:NO]; + + id<FNode> expected = NODE((@{@"foo": @"foo-value"})); + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], expected); +} + +- (void)testDeeperReadFromHigherSet { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + + id<FNode> initial = NODE((@{@"foo": @"foo-value", @"bar": @"bar-value"})); + [engine updateServerCache:initial atPath:PATH(@"foo") merge:NO]; + + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo/bar")], NODE(@"bar-value")); +} + +- (void)testDeeperLeafNodeSetRemovesHigherLeafNodes { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine updateServerCache:NODE(@"level-0") atPath:PATH(@"") merge:NO]; + [engine updateServerCache:NODE(@"level-1") atPath:PATH(@"lvl1") merge:NO]; + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"")], NODE((@{@"lvl1": @"level-1"}))); + + [engine updateServerCache:NODE(@"level-2") atPath:PATH(@"lvl1/lvl2") merge:NO]; + + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"lvl1")], NODE((@{@"lvl2": @"level-2"}))); + + [engine updateServerCache:NODE(@"level-4") atPath:PATH(@"lvl1/lvl2/lvl3/lvl4") merge:NO]; + + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"lvl1")], NODE((@{@"lvl2": @{@"lvl3": @{@"lvl4": @"level-4"}}}))); +} + + +// This test causes a split on Android so it doesn't really make sense here, but why not test anyways... +- (void)testHugeNodeWithSplit { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + + id<FNode> outer = [FEmptyNode emptyNode]; + // This structure ensures splits at various depths + for (NSUInteger i = 0; i < 100; i++) { // Outer + id<FNode> inner = [FEmptyNode emptyNode]; + for (NSUInteger j = 0; j < i; j++) { // Inner + id<FNode> innerMost = [FEmptyNode emptyNode]; + for (NSUInteger k = 0; k < j; k++) { + NSString *key = [NSString stringWithFormat:@"key-%lu", (unsigned long)k]; + id<FNode> node = NODE(([NSString stringWithFormat:@"leaf-%lu", (unsigned long)k])); + innerMost = [innerMost updateImmediateChild:key withNewChild:node]; + } + NSString *innerKey = [NSString stringWithFormat:@"key-%lu", (unsigned long)j]; + inner = [inner updateImmediateChild:innerKey withNewChild:innerMost]; + } + NSString *outerKey = [NSString stringWithFormat:@"key-%lu", (unsigned long)i]; + outer = [outer updateImmediateChild:outerKey withNewChild:inner]; + } + [engine updateServerCache:outer atPath:PATH(@"foo") merge:NO]; + + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], outer); +} + +- (void)testManyLargeLeafNodes { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + id<FNode> outer = [FEmptyNode emptyNode]; + for (NSUInteger i = 0; i < 30; i++) { + NSString *outerKey = [NSString stringWithFormat:@"key-%lu", (unsigned long)i]; + outer = [outer updateImmediateChild:outerKey withNewChild:ONE_MEG_NODE]; + } + + [engine updateServerCache:outer atPath:PATH(@"foo") merge:NO]; + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], outer); +} + +- (void)testPriorityWorks { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + + [engine updateServerCache:NODE(@"bar-value") atPath:PATH(@"foo/bar") merge:NO]; + [engine updateServerCache:NODE(@"prio-value") atPath:PATH(@"foo/.priority") merge:NO]; + + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], NODE((@{ @".priority": @"prio-value", @"bar": @"bar-value"}))); +} + +- (void)testSimilarSiblingsAreNotLoaded { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + + [engine updateServerCache:NODE(@"value") atPath:PATH(@"foo/123") merge:NO]; + [engine updateServerCache:NODE(@"sibling-value") atPath:PATH(@"foo/1230") merge:NO]; + + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo/123")], NODE(@"value")); +} + +// TODO: this test fails, but it is a rare edge case around priorities which would require a bunch of code +// Fix whenever we have too much time on our hands +- (void)priorityIsCleared { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + + [engine updateServerCache:NODE((@{@"bar": @"bar-value"})) atPath:PATH(@"foo") merge:NO]; + [engine updateServerCache:NODE(@"prio-value") atPath:PATH(@"foo/.priority") merge:NO]; + [engine updateServerCache:NODE(nil) atPath:PATH(@"foo/bar") merge:NO]; + [engine updateServerCache:NODE(@"baz-value") atPath:PATH(@"foo/baz") merge:NO]; + + // Priority should have been cleaned out + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], NODE(@{@"baz": @"baz-value"})); +} + +- (void)testHugeLeafNode { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine updateServerCache:TEN_MEG_NODE atPath:PATH(@"foo") merge:NO]; + + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], TEN_MEG_NODE); +} + +- (void)testHugeLeafNodeSiblings { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine updateServerCache:TEN_MEG_NODE atPath:PATH(@"foo/one") merge:NO]; + [engine updateServerCache:TEN_MEG_MINUS_ONE_NODE atPath:PATH(@"foo/two") merge:NO]; + + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo/one")], TEN_MEG_NODE); + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo/two")], TEN_MEG_MINUS_ONE_NODE); +} + +- (void)testHugeLeafNodeThenTinyLeafNode { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine updateServerCache:TEN_MEG_NODE atPath:PATH(@"foo") merge:NO]; + [engine updateServerCache:NODE(@"tiny") atPath:PATH(@"foo") merge:NO]; + + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], NODE(@"tiny")); +} + +- (void)testHugeLeafNodeThenSmallerLeafNode { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine updateServerCache:TEN_MEG_NODE atPath:PATH(@"foo") merge:NO]; + [engine updateServerCache:FIVE_MEG_NODE atPath:PATH(@"foo") merge:NO]; + + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], FIVE_MEG_NODE); +} + +- (void)testHugeLeafNodeThenDeeperSet { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine updateServerCache:TEN_MEG_NODE atPath:PATH(@"foo") merge:NO]; + [engine updateServerCache:NODE(@"deep-value") atPath:PATH(@"foo/deep") merge:NO]; + + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], NODE((@{@"deep": @"deep-value"}))); +} + +// Well this is awkward, but NSJSONSerialization fails to deserialize JSON with tiny/huge doubles +// It is kind of bad we raise "invalid" data, but at least we don't crash *trollface* +- (void)testExtremeDoublesAsServerCache { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine updateServerCache:NODE((@{@"works": @"value", @"fails": @(2.225073858507201e-308)})) atPath:PATH(@"foo") merge:NO]; + + // Will drop the tiny double + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], NODE(@{@"works": @"value"})); + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo/fails")], [FEmptyNode emptyNode]); +} + +- (void)testExtremeDoublesAsTrackedQuery { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + id<FNode> tinyDouble = NODE(@(2.225073858507201e-308)); + + FQueryParams *params = [[[FQueryParams defaultInstance] startAt:tinyDouble] endAt:tinyDouble]; + FTrackedQuery *doesNotWork = [[FTrackedQuery alloc] initWithId:0 + query:[[FQuerySpec alloc] initWithPath:PATH(@"foo") params:params] + lastUse:0 + isActive:NO]; + FTrackedQuery *doesWork = [[FTrackedQuery alloc] initWithId:1 + query:[FQuerySpec defaultQueryAtPath:PATH(@"bar")] + lastUse:0 + isActive:NO]; + [engine saveTrackedQuery:doesNotWork]; + [engine saveTrackedQuery:doesWork]; + // One will be dropped, the other should still be there + XCTAssertEqualObjects([engine loadTrackedQueries], @[doesWork]); +} + +- (void)testExtremeDoublesAsUserWrites { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + id<FNode> tinyDouble = NODE(@(2.225073858507201e-308)); + + [engine saveUserOverwrite:tinyDouble atPath:PATH(@"foo") writeId:1]; + [engine saveUserMerge:[[FCompoundWrite emptyWrite] addWrite:tinyDouble atPath:PATH(@"bar")] atPath:PATH(@"foo") writeId:2]; + [engine saveUserOverwrite:NODE(@"should-work") atPath:PATH(@"other") writeId:3]; + + // The other two should be dropped and only the valid should remain + XCTAssertEqualObjects([engine userWrites], @[[[FWriteRecord alloc] initWithPath:PATH(@"other") + overwrite:NODE(@"should-work") + writeId:3 + visible:YES]]); +} + +- (void)testLongValuesDontLosePrecision { + id longValue = @1542405709418655810; + id floatValue = @2.47; + id<FNode> expectedData = NODE((@{@"long": longValue, @"float": floatValue})); + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine updateServerCache:expectedData atPath:PATH(@"foo") merge:NO]; + id<FNode> actualData = [engine serverCacheAtPath:PATH(@"foo")]; + NSDictionary* value = [actualData val]; + XCTAssertEqualObjects([value[@"long"] stringValue], [longValue stringValue]); + XCTAssertEqualObjects([value[@"float"] stringValue], [floatValue stringValue]); +} + +// NSJSONSerialization has a bug in which it rounds doubles wrongly so hashes end up not matching on the server for +// some doubles (including 2.47). Make sure LevelDB has the correct hash for that +- (void)testDoublesAreRoundedProperly { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine updateServerCache:NODE(@(2.47)) atPath:PATH(@"foo") merge:NO]; + + // Expected hash for 2.47 parsed correctly + NSString *hashFor247 = @"EsibHXKcBp2/b/bn/a0C5WffcUU="; + XCTAssertEqualObjects([[engine serverCacheAtPath:PATH(@"foo")] dataHash], hashFor247); +} + +// TODO[offline]: Somehow test estimated server size? +// TODO[offline]: Test pruning! + +- (void)testSaveAndLoadTrackedQueries { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + + NSArray *queries = @[[[FTrackedQuery alloc] initWithId:1 query:SAMPLE_QUERY lastUse:100 isActive:NO isComplete:NO], + [[FTrackedQuery alloc] initWithId:2 query:[FQuerySpec defaultQueryAtPath:PATH(@"a")] lastUse:200 isActive:NO isComplete:NO], + [[FTrackedQuery alloc] initWithId:3 query:[FQuerySpec defaultQueryAtPath:PATH(@"b")] lastUse:300 isActive:YES isComplete:NO], + [[FTrackedQuery alloc] initWithId:4 query:[FQuerySpec defaultQueryAtPath:PATH(@"c")] lastUse:400 isActive:NO isComplete:YES], + [[FTrackedQuery alloc] initWithId:5 query:[FQuerySpec defaultQueryAtPath:PATH(@"foo")] lastUse:500 isActive:NO isComplete:NO]]; + + [queries enumerateObjectsUsingBlock:^(FTrackedQuery *query, NSUInteger idx, BOOL *stop) { + [engine saveTrackedQuery:query]; + }]; + + XCTAssertEqualObjects([engine loadTrackedQueries], queries); +} + +- (void)testOverwriteTrackedQueryById { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + + FTrackedQuery *first = [[FTrackedQuery alloc] initWithId:1 query:SAMPLE_QUERY lastUse:100 isActive:NO isComplete:NO]; + FTrackedQuery *second = [[FTrackedQuery alloc] initWithId:1 query:DEFAULT_FOO_QUERY lastUse:200 isActive:YES isComplete:YES]; + [engine saveTrackedQuery:first]; + [engine saveTrackedQuery:second]; + + XCTAssertEqualObjects([engine loadTrackedQueries], @[second]); +} + +- (void)testDeleteTrackedQuery { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + FTrackedQuery *query1 = [[FTrackedQuery alloc] initWithId:1 query:[FQuerySpec defaultQueryAtPath:PATH(@"a")] lastUse:100 isActive:NO isComplete:NO]; + FTrackedQuery *query2 = [[FTrackedQuery alloc] initWithId:2 query:[FQuerySpec defaultQueryAtPath:PATH(@"b")] lastUse:200 isActive:YES isComplete:NO]; + FTrackedQuery *query3 = [[FTrackedQuery alloc] initWithId:3 query:[FQuerySpec defaultQueryAtPath:PATH(@"c")] lastUse:300 isActive:NO isComplete:YES]; + [engine saveTrackedQuery:query1]; + [engine saveTrackedQuery:query2]; + [engine saveTrackedQuery:query3]; + + [engine removeTrackedQuery:2]; + XCTAssertEqualObjects([engine loadTrackedQueries], (@[query1, query3])); +} + +- (void)testSaveAndLoadTrackedQueryKeys { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + NSSet *keys = [NSSet setWithArray:@[@"foo", @"☁", @"10", @"٩(͡๏̯͡๏)۶"]]; + [engine setTrackedQueryKeys:keys forQueryId:1]; + [engine setTrackedQueryKeys:[NSSet setWithArray:@[@"not", @"included"]] forQueryId:2]; + + XCTAssertEqualObjects([engine trackedQueryKeysForQuery:1], keys); +} + +- (void)testSaveOverwritesTrackedQueryKeys { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine setTrackedQueryKeys:[NSSet setWithArray:@[@"a", @"b", @"c"]] forQueryId:1]; + [engine setTrackedQueryKeys:[NSSet setWithArray:@[@"c", @"d", @"e"]] forQueryId:1]; + + XCTAssertEqualObjects([engine trackedQueryKeysForQuery:1], ([NSSet setWithArray:@[@"c", @"d", @"e"]])); +} + +- (void)testUpdateTrackedQueryKeys { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine setTrackedQueryKeys:[NSSet setWithArray:@[@"a", @"b", @"c"]] forQueryId:1]; + [engine updateTrackedQueryKeysWithAddedKeys:[NSSet setWithArray:@[@"c", @"d", @"e"]] + removedKeys:[NSSet setWithArray:@[@"a", @"b"]] + forQueryId:1]; + XCTAssertEqualObjects([engine trackedQueryKeysForQuery:1], ([NSSet setWithArray:@[@"c", @"d", @"e"]])); +} + +- (void)testRemoveTrackedQueryRemovesTrackedQueryKeys { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + FTrackedQuery *query1 = [[FTrackedQuery alloc] initWithId:1 query:[FQuerySpec defaultQueryAtPath:PATH(@"a")] lastUse:100 isActive:NO isComplete:NO]; + FTrackedQuery *query2 = [[FTrackedQuery alloc] initWithId:2 query:[FQuerySpec defaultQueryAtPath:PATH(@"b")] lastUse:200 isActive:NO isComplete:NO]; + [engine saveTrackedQuery:query1]; + [engine saveTrackedQuery:query2]; + [engine setTrackedQueryKeys:[NSSet setWithArray:@[@"a", @"b"]] forQueryId:1]; + [engine setTrackedQueryKeys:[NSSet setWithArray:@[@"b", @"c"]] forQueryId:2]; + + XCTAssertEqualObjects([engine loadTrackedQueries], (@[query1, query2])); + XCTAssertEqualObjects([engine trackedQueryKeysForQuery:1], ([NSSet setWithArray:@[@"a", @"b"]])); + XCTAssertEqualObjects([engine trackedQueryKeysForQuery:2], ([NSSet setWithArray:@[@"b", @"c"]])); + + [engine removeTrackedQuery:1]; + + XCTAssertEqualObjects([engine loadTrackedQueries], (@[query2])); + XCTAssertEqualObjects([engine trackedQueryKeysForQuery:1], [NSSet set]); + XCTAssertEqualObjects([engine trackedQueryKeysForQuery:2], ([NSSet setWithArray:@[@"b", @"c"]])); +} + +@end |