/*
* Copyright 2017 Google Inc.
*
* 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.
*/
package com.google.firebase.database.snapshot;
import static com.google.firebase.database.TestHelpers.fromSingleQuotedString;
import static com.google.firebase.database.TestHelpers.path;
import static com.google.firebase.database.snapshot.NodeUtilities.NodeFromJSON;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import com.google.firebase.database.MapBuilder;
import com.google.firebase.database.core.Path;
import com.google.firebase.database.utilities.Utilities;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.junit.Test;
public class CompoundHashTest {
private static final CompoundHash.SplitStrategy NEVER_SPLIT_STRATEGY =
new CompoundHash.SplitStrategy() {
@Override
public boolean shouldSplit(CompoundHash.CompoundHashBuilder state) {
return false;
}
};
private static CompoundHash.SplitStrategy splitAtPaths(String... paths) {
final List<Path> pathList = new ArrayList<>();
for (String path : paths) {
pathList.add(path(path));
}
return new CompoundHash.SplitStrategy() {
@Override
public boolean shouldSplit(CompoundHash.CompoundHashBuilder state) {
return pathList.contains(state.currentPath());
}
};
}
private static void assertWithinPercent(int expected, int actual, double percent) {
double percentDecimal = percent / 100.0;
double lowerBound = expected * (1 - percentDecimal);
double upperBound = expected * (1 + percentDecimal);
assertTrue(
String.format("Not within range: (%02f, %02f): %d", lowerBound, upperBound, actual),
actual > lowerBound);
assertTrue(
String.format("Not within range: (%02f, %02f): %d", lowerBound, upperBound, actual),
actual < upperBound);
}
@Test
public void emptyNodeYieldsEmptyHash() {
CompoundHash hash = CompoundHash.fromNode(EmptyNode.Empty());
assertEquals(Collections.<Path>emptyList(), hash.getPosts());
assertEquals(Arrays.asList(""), hash.getHashes());
}
@Test
public void compoundHashIsAlwaysFollowedByEmptyHash() {
Node node = NodeFromJSON(fromSingleQuotedString("{'foo': 'bar'}"));
CompoundHash hash = CompoundHash.fromNode(node, NEVER_SPLIT_STRATEGY);
String expectedHash = Utilities.sha1HexDigest("(\"foo\":(string:\"bar\"))");
assertEquals(Arrays.asList(path("foo")), hash.getPosts());
assertEquals(Arrays.asList(expectedHash, ""), hash.getHashes());
}
@Test
public void compoundHashCanSplitAtPriority() {
Node node =
NodeFromJSON(
fromSingleQuotedString(
"{'foo': {'!beforePriority': 'before', '.priority': 'prio', 'afterPriority': "
+ "'after'}, 'qux': 'qux'}"));
CompoundHash hash = CompoundHash.fromNode(node, splitAtPaths("foo/.priority"));
String firstHash =
Utilities.sha1HexDigest(
"(\"foo\":(\"!beforePriority\":(string:\"before\"),\".priority\":"
+ "(string:\"prio\")))");
String secondHash =
Utilities.sha1HexDigest(
"(\"foo\":(\"afterPriority\":(string:\"after\")),\"qux\":(string:\"qux\"))");
assertEquals(Arrays.asList(path("foo/.priority"), path("qux")), hash.getPosts());
assertEquals(Arrays.asList(firstHash, secondHash, ""), hash.getHashes());
}
@Test
public void hashesPriorityLeafNodes() {
Node node =
NodeFromJSON(fromSingleQuotedString("{'foo': {'.value': 'bar', '.priority': 'baz'}}"));
CompoundHash hash = CompoundHash.fromNode(node, NEVER_SPLIT_STRATEGY);
String expectedHash =
Utilities.sha1HexDigest("(\"foo\":(priority:string:\"baz\":string:\"bar\"))");
assertEquals(Arrays.asList(path("foo")), hash.getPosts());
assertEquals(Arrays.asList(expectedHash, ""), hash.getHashes());
}
@Test
public void hashingFollowsFirebaseKeySemantics() {
Node node = NodeFromJSON(fromSingleQuotedString("{'1': 'one', '2': 'two', '10': 'ten'}"));
// 10 is after 2 in Firebase key semantics, but would be before 2 in string semantics
CompoundHash hash = CompoundHash.fromNode(node, splitAtPaths("2"));
String firstHash =
Utilities.sha1HexDigest("(\"1\":(string:\"one\"),\"2\":" + "(string:\"two\"))");
String secondHash = Utilities.sha1HexDigest("(\"10\":(string:\"ten\"))");
assertEquals(Arrays.asList(path("2"), path("10")), hash.getPosts());
assertEquals(Arrays.asList(firstHash, secondHash, ""), hash.getHashes());
}
@Test
public void hashingOnChildBoundariesWorks() {
Node node =
NodeFromJSON(
fromSingleQuotedString(
"{'bar': {'deep': 'value'}, 'foo': {'other-deep': " + "'value'}}"));
CompoundHash hash = CompoundHash.fromNode(node, splitAtPaths("bar/deep"));
String firstHash = Utilities.sha1HexDigest("(\"bar\":(\"deep\":(string:\"value\")))");
String secondHash =
Utilities.sha1HexDigest("(\"foo\":(\"other-deep\":(string:\"value\"))" + ")");
assertEquals(Arrays.asList(path("bar/deep"), path("foo/other-deep")), hash.getPosts());
assertEquals(Arrays.asList(firstHash, secondHash, ""), hash.getHashes());
}
@Test
public void commasAreSetForNestedChildren() {
Node node =
NodeFromJSON(
fromSingleQuotedString(
"{'bar': {'deep': 'value'}, 'foo': {'other-deep': " + "'value'}}"));
CompoundHash hash = CompoundHash.fromNode(node, NEVER_SPLIT_STRATEGY);
String hashValue =
Utilities.sha1HexDigest(
"(\"bar\":(\"deep\":(string:\"value\")),\"foo\":(\"other-deep\":"
+ "(string:\"value\")))");
assertEquals(Arrays.asList(path("foo/other-deep")), hash.getPosts());
assertEquals(Arrays.asList(hashValue, ""), hash.getHashes());
}
@Test
public void quotedStringsAndKeys() {
Map<String, Object> data = new MapBuilder().put("\"\\\"\\", "\"\\\"\\").put("\"", "\\").build();
Node node = NodeFromJSON(data);
CompoundHash hash = CompoundHash.fromNode(node, NEVER_SPLIT_STRATEGY);
String hashValue =
Utilities.sha1HexDigest(
"(\"\\\"\":(string:\"\\\\\"),\"\\\"\\\\\\\"\\\\\":(string:\"\\\"\\\\\\\"\\\\\"))");
assertEquals(Arrays.asList(path("\"\\\"\\")), hash.getPosts());
assertEquals(Arrays.asList(hashValue, ""), hash.getHashes());
}
@Test
public void defaultSplitHasSensibleAmountOfHashes() {
Node node10k = EmptyNode.Empty();
Node node100k = EmptyNode.Empty();
Node node1M = EmptyNode.Empty();
for (int i = 0; i < 500; i++) {
// roughly 15-20 bytes serialized per node, 100k total
node10k =
node10k.updateImmediateChild(ChildKey.fromString("key-" + i), NodeFromJSON("value"));
}
for (int i = 0; i < 5000; i++) {
// roughly 15-20 bytes serialized per node, 100k total
node100k =
node100k.updateImmediateChild(ChildKey.fromString("key-" + i), NodeFromJSON("value"));
}
for (int i = 0; i < 50000; i++) {
// roughly 15-20 bytes serialized per node, 1M total
node1M = node1M.updateImmediateChild(ChildKey.fromString("key-" + i), NodeFromJSON("value"));
}
CompoundHash hash10K = CompoundHash.fromNode(node10k);
CompoundHash hash100K = CompoundHash.fromNode(node100k);
CompoundHash hash1M = CompoundHash.fromNode(node1M);
assertWithinPercent(15, hash10K.getHashes().size(), /*percent=*/ 10);
assertWithinPercent(50, hash100K.getHashes().size(), /*percent=*/ 10);
assertWithinPercent(150, hash1M.getHashes().size(), /*percent=*/ 10);
}
@Test
public void defaultSplitHandlesLargeLeafNodeAtRoot() {
StringBuilder largeString = new StringBuilder();
for (int i = 0; i < 50 * 1024; i++) {
largeString.append("x");
}
Node leafNode = NodeFromJSON(largeString.toString(), EmptyNode.Empty());
CompoundHash hash = CompoundHash.fromNode(leafNode);
assertEquals(2, hash.getHashes().size());
}
}