Files
firebase-ios-sdk/Example/Database/Tests/Unit/FLevelDBStorageEngineTests.m
T
Sebastian Schmidt c67a8bdc58 Fixing parsing for double numbers (#107)
NSJSONSerialization parses long values as doubles and returns NSNumber that are backed by doubles to the users. For NSNumbers that can be stored as longs, we can overwrite this behavior by re-running the initialization step.
2017-06-27 10:04:47 -07:00

604 lines
30 KiB
Objective-C

/*
* 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);
}
- (void)testIntegersAreReturnedsAsIntegers {
id intValue = @247;
id longValue = @1542405709418655810;
id doubleValue = @0xFFFFFFFFFFFFFFFFUL; // This number can't be represented as a signed long.
id<FNode> expectedData = NODE((@{@"int": @247, @"long": longValue, @"double": doubleValue}));
FLevelDBStorageEngine *engine = [self cleanStorageEngine];
[engine updateServerCache:expectedData atPath:PATH(@"foo") merge:NO];
id<FNode> actualData = [engine serverCacheAtPath:PATH(@"foo")];
NSNumber* actualInt = [actualData val][@"int"];
NSNumber* actualLong = [actualData val][@"long"];
NSNumber* actualDouble = [actualData val][@"double"];
XCTAssertEqualObjects([actualInt stringValue], [intValue stringValue]);
XCTAssertEqual(CFNumberGetType((CFNumberRef)actualInt), kCFNumberSInt64Type);
XCTAssertEqualObjects([actualLong stringValue ], [longValue stringValue]);
XCTAssertEqual(CFNumberGetType((CFNumberRef)actualLong), kCFNumberSInt64Type);
XCTAssertEqual(CFNumberGetType((CFNumberRef)actualDouble), kCFNumberFloat64Type);
}
// 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