From f6b2f8fb411369f76642207a79f2378a316ff020 Mon Sep 17 00:00:00 2001 From: Brian Chen Date: Mon, 17 Jun 2019 12:31:51 -0700 Subject: [PATCH 1/4] copy java files in ktx --- firebase-firestore/ktx/ktx.gradle | 3 +- .../firebase/firestore/testutil/TestUtil.java | 618 ++++++++++++++++++ 2 files changed, 619 insertions(+), 2 deletions(-) create mode 100644 firebase-firestore/ktx/src/test/java/com/google/firebase/firestore/testutil/TestUtil.java diff --git a/firebase-firestore/ktx/ktx.gradle b/firebase-firestore/ktx/ktx.gradle index cad7aece2d9..bfe98524542 100644 --- a/firebase-firestore/ktx/ktx.gradle +++ b/firebase-firestore/ktx/ktx.gradle @@ -33,8 +33,7 @@ android { main.java.srcDirs += 'src/main/kotlin' test.java { srcDir 'src/test/kotlin' - srcDir '../src/testUtil/java' - srcDir '../src/roboUtil/java' + srcDir 'src/test/java' } } testOptions.unitTests.includeAndroidResources = true diff --git a/firebase-firestore/ktx/src/test/java/com/google/firebase/firestore/testutil/TestUtil.java b/firebase-firestore/ktx/src/test/java/com/google/firebase/firestore/testutil/TestUtil.java new file mode 100644 index 00000000000..6d08655f5f2 --- /dev/null +++ b/firebase-firestore/ktx/src/test/java/com/google/firebase/firestore/testutil/TestUtil.java @@ -0,0 +1,618 @@ +// Copyright 2018 Google LLC +// +// 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.firestore.testutil; + +import static com.google.common.truth.Truth.assertThat; +import static java.util.Arrays.asList; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.fail; + +import androidx.annotation.NonNull; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Charsets; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.google.firebase.Timestamp; +import com.google.firebase.database.collection.ImmutableSortedMap; +import com.google.firebase.database.collection.ImmutableSortedSet; +import com.google.firebase.firestore.Blob; +import com.google.firebase.firestore.DocumentReference; +import com.google.firebase.firestore.TestAccessHelper; +import com.google.firebase.firestore.UserDataConverter; +import com.google.firebase.firestore.core.Filter; +import com.google.firebase.firestore.core.Filter.Operator; +import com.google.firebase.firestore.core.OrderBy; +import com.google.firebase.firestore.core.OrderBy.Direction; +import com.google.firebase.firestore.core.Query; +import com.google.firebase.firestore.core.UserData.ParsedUpdateData; +import com.google.firebase.firestore.local.LocalViewChanges; +import com.google.firebase.firestore.local.QueryData; +import com.google.firebase.firestore.local.QueryPurpose; +import com.google.firebase.firestore.model.DatabaseId; +import com.google.firebase.firestore.model.Document; +import com.google.firebase.firestore.model.DocumentKey; +import com.google.firebase.firestore.model.DocumentSet; +import com.google.firebase.firestore.model.FieldPath; +import com.google.firebase.firestore.model.MaybeDocument; +import com.google.firebase.firestore.model.NoDocument; +import com.google.firebase.firestore.model.ResourcePath; +import com.google.firebase.firestore.model.SnapshotVersion; +import com.google.firebase.firestore.model.UnknownDocument; +import com.google.firebase.firestore.model.mutation.DeleteMutation; +import com.google.firebase.firestore.model.mutation.FieldMask; +import com.google.firebase.firestore.model.mutation.FieldTransform; +import com.google.firebase.firestore.model.mutation.MutationResult; +import com.google.firebase.firestore.model.mutation.PatchMutation; +import com.google.firebase.firestore.model.mutation.Precondition; +import com.google.firebase.firestore.model.mutation.SetMutation; +import com.google.firebase.firestore.model.mutation.TransformMutation; +import com.google.firebase.firestore.model.value.FieldValue; +import com.google.firebase.firestore.model.value.ObjectValue; +import com.google.firebase.firestore.remote.RemoteEvent; +import com.google.firebase.firestore.remote.TargetChange; +import com.google.firebase.firestore.remote.WatchChange.DocumentChange; +import com.google.firebase.firestore.remote.WatchChangeAggregator; +import com.google.protobuf.ByteString; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import javax.annotation.Nullable; + +/** A set of utilities for tests */ +public class TestUtil { + + /** A string sentinel that can be used with patchMutation() to mark a field for deletion. */ + public static final String DELETE_SENTINEL = ""; + + public static final long ARBITRARY_SEQUENCE_NUMBER = 2; + + @SuppressWarnings("unchecked") + public static Map map(Object... entries) { + Map res = new HashMap<>(); + for (int i = 0; i < entries.length; i += 2) { + res.put((String) entries[i], (T) entries[i + 1]); + } + return res; + } + + public static Blob blob(int... bytes) { + return Blob.fromByteString(byteString(bytes)); + } + + public static ByteString byteString(int... bytes) { + byte[] primitive = new byte[bytes.length]; + for (int i = 0; i < bytes.length; i++) { + primitive[i] = (byte) bytes[i]; + } + return ByteString.copyFrom(primitive); + } + + public static FieldMask fieldMask(String... fields) { + FieldPath[] mask = new FieldPath[fields.length]; + for (int i = 0; i < fields.length; i++) { + mask[i] = field(fields[i]); + } + return FieldMask.fromSet(new HashSet<>(Arrays.asList(mask))); + } + + public static final Map EMPTY_MAP = new HashMap<>(); + + public static FieldValue wrap(Object value) { + DatabaseId databaseId = DatabaseId.forProject("project"); + UserDataConverter dataConverter = new UserDataConverter(databaseId); + // HACK: We use parseQueryValue() since it accepts scalars as well as arrays / objects, and + // our tests currently use wrap() pretty generically so we don't know the intent. + return dataConverter.parseQueryValue(value); + } + + public static ObjectValue wrapObject(Map value) { + // Cast is safe here because value passed in is a map + return (ObjectValue) wrap(value); + } + + public static ObjectValue wrapObject(Object... entries) { + return wrapObject(map(entries)); + } + + public static DocumentKey key(String key) { + return DocumentKey.fromPathString(key); + } + + public static ResourcePath path(String key) { + return ResourcePath.fromString(key); + } + + public static Query query(String path) { + return Query.atPath(path(path)); + } + + public static FieldPath field(String path) { + return FieldPath.fromSegments(Arrays.asList(path.split("\\."))); + } + + public static DocumentReference ref(String key) { + return TestAccessHelper.createDocumentReference(key(key)); + } + + public static DatabaseId dbId(String project, String database) { + return DatabaseId.forDatabase(project, database); + } + + public static DatabaseId dbId(String project) { + return DatabaseId.forProject(project); + } + + public static SnapshotVersion version(long versionMicros) { + long seconds = versionMicros / 1000000; + int nanos = (int) (versionMicros % 1000000L) * 1000; + return new SnapshotVersion(new Timestamp(seconds, nanos)); + } + + public static Document doc(String key, long version, Map data) { + return new Document( + key(key), version(version), wrapObject(data), Document.DocumentState.SYNCED); + } + + public static Document doc(DocumentKey key, long version, Map data) { + return new Document(key, version(version), wrapObject(data), Document.DocumentState.SYNCED); + } + + public static Document doc( + String key, long version, ObjectValue data, Document.DocumentState documentState) { + return new Document(key(key), version(version), data, documentState); + } + + public static Document doc( + String key, long version, Map data, Document.DocumentState documentState) { + return new Document(key(key), version(version), wrapObject(data), documentState); + } + + public static NoDocument deletedDoc(String key, long version) { + return deletedDoc(key, version, /*hasCommittedMutations=*/ false); + } + + public static NoDocument deletedDoc(String key, long version, boolean hasCommittedMutations) { + return new NoDocument(key(key), version(version), hasCommittedMutations); + } + + public static UnknownDocument unknownDoc(String key, long version) { + return new UnknownDocument(key(key), version(version)); + } + + public static DocumentSet docSet(Comparator comparator, Document... documents) { + DocumentSet set = DocumentSet.emptySet(comparator); + for (Document document : documents) { + set = set.add(document); + } + return set; + } + + public static ImmutableSortedSet keySet(DocumentKey... keys) { + ImmutableSortedSet keySet = DocumentKey.emptyKeySet(); + for (DocumentKey key : keys) { + keySet = keySet.insert(key); + } + return keySet; + } + + public static Filter filter(String key, String operator, Object value) { + return Filter.create(field(key), operatorFromString(operator), wrap(value)); + } + + public static Operator operatorFromString(String s) { + if (s.equals("<")) { + return Operator.LESS_THAN; + } else if (s.equals("<=")) { + return Operator.LESS_THAN_OR_EQUAL; + } else if (s.equals("==")) { + return Operator.EQUAL; + } else if (s.equals(">")) { + return Operator.GREATER_THAN; + } else if (s.equals(">=")) { + return Operator.GREATER_THAN_OR_EQUAL; + } else if (s.equals("array-contains")) { + return Operator.ARRAY_CONTAINS; + } else { + throw new IllegalStateException("Unknown operator: " + s); + } + } + + public static OrderBy orderBy(String key) { + return orderBy(key, "asc"); + } + + public static OrderBy orderBy(String key, String dir) { + Direction direction; + if (dir.equals("asc")) { + direction = Direction.ASCENDING; + } else if (dir.equals("desc")) { + direction = Direction.DESCENDING; + } else { + throw new IllegalArgumentException("Unknown direction: " + dir); + } + return OrderBy.getInstance(direction, field(key)); + } + + public static void testEquality(List> equalityGroups) { + for (int i = 0; i < equalityGroups.size(); i++) { + List group = equalityGroups.get(i); + for (Object value : group) { + for (List otherGroup : equalityGroups) { + for (Object otherValue : otherGroup) { + if (otherGroup == group) { + assertEquals(value, otherValue); + } else { + assertNotEquals(value, otherValue); + } + } + } + } + } + } + + public static QueryData queryData(int targetId, QueryPurpose queryPurpose, String path) { + return new QueryData(query(path), targetId, ARBITRARY_SEQUENCE_NUMBER, queryPurpose); + } + + public static ImmutableSortedMap docUpdates(MaybeDocument... docs) { + ImmutableSortedMap res = + ImmutableSortedMap.Builder.emptyMap(DocumentKey.comparator()); + for (MaybeDocument doc : docs) { + res = res.insert(doc.getKey(), doc); + } + return res; + } + + public static ImmutableSortedMap docUpdates(Document... docs) { + ImmutableSortedMap res = + ImmutableSortedMap.Builder.emptyMap(DocumentKey.comparator()); + for (Document doc : docs) { + res = res.insert(doc.getKey(), doc); + } + return res; + } + + public static TargetChange targetChange( + ByteString resumeToken, + boolean current, + @Nullable Collection addedDocuments, + @Nullable Collection modifiedDocuments, + @Nullable Collection removedDocuments) { + ImmutableSortedSet addedDocumentKeys = DocumentKey.emptyKeySet(); + ImmutableSortedSet modifiedDocumentKeys = DocumentKey.emptyKeySet(); + ImmutableSortedSet removedDocumentKeys = DocumentKey.emptyKeySet(); + + if (addedDocuments != null) { + for (Document document : addedDocuments) { + addedDocumentKeys = addedDocumentKeys.insert(document.getKey()); + } + } + + if (modifiedDocuments != null) { + for (Document document : modifiedDocuments) { + modifiedDocumentKeys = modifiedDocumentKeys.insert(document.getKey()); + } + } + + if (removedDocuments != null) { + for (MaybeDocument document : removedDocuments) { + removedDocumentKeys = removedDocumentKeys.insert(document.getKey()); + } + } + + return new TargetChange( + resumeToken, current, addedDocumentKeys, modifiedDocumentKeys, removedDocumentKeys); + } + + public static TargetChange ackTarget(Document... docs) { + return targetChange(ByteString.EMPTY, true, Arrays.asList(docs), null, null); + } + + public static Map activeQueries(Iterable targets) { + Query query = query("foo"); + Map listenMap = new HashMap<>(); + for (Integer targetId : targets) { + QueryData queryData = + new QueryData(query, targetId, ARBITRARY_SEQUENCE_NUMBER, QueryPurpose.LISTEN); + listenMap.put(targetId, queryData); + } + return listenMap; + } + + public static Map activeQueries(Integer... targets) { + return activeQueries(asList(targets)); + } + + public static Map activeLimboQueries( + String docKey, Iterable targets) { + Query query = query(docKey); + Map listenMap = new HashMap<>(); + for (Integer targetId : targets) { + QueryData queryData = + new QueryData(query, targetId, ARBITRARY_SEQUENCE_NUMBER, QueryPurpose.LIMBO_RESOLUTION); + listenMap.put(targetId, queryData); + } + return listenMap; + } + + public static Map activeLimboQueries(String docKey, Integer... targets) { + return activeLimboQueries(docKey, asList(targets)); + } + + public static RemoteEvent addedRemoteEvent( + MaybeDocument doc, List updatedInTargets, List removedFromTargets) { + DocumentChange change = + new DocumentChange(updatedInTargets, removedFromTargets, doc.getKey(), doc); + WatchChangeAggregator aggregator = + new WatchChangeAggregator( + new WatchChangeAggregator.TargetMetadataProvider() { + @Override + public ImmutableSortedSet getRemoteKeysForTarget(int targetId) { + return DocumentKey.emptyKeySet(); + } + + @Override + public QueryData getQueryDataForTarget(int targetId) { + return queryData(targetId, QueryPurpose.LISTEN, doc.getKey().toString()); + } + }); + aggregator.handleDocumentChange(change); + return aggregator.createRemoteEvent(doc.getVersion()); + } + + public static RemoteEvent updateRemoteEvent( + MaybeDocument doc, List updatedInTargets, List removedFromTargets) { + return updateRemoteEvent(doc, updatedInTargets, removedFromTargets, Collections.emptyList()); + } + + public static RemoteEvent updateRemoteEvent( + MaybeDocument doc, + List updatedInTargets, + List removedFromTargets, + List limboTargets) { + DocumentChange change = + new DocumentChange(updatedInTargets, removedFromTargets, doc.getKey(), doc); + WatchChangeAggregator aggregator = + new WatchChangeAggregator( + new WatchChangeAggregator.TargetMetadataProvider() { + @Override + public ImmutableSortedSet getRemoteKeysForTarget(int targetId) { + return DocumentKey.emptyKeySet().insert(doc.getKey()); + } + + @Override + public QueryData getQueryDataForTarget(int targetId) { + boolean isLimbo = + !(updatedInTargets.contains(targetId) || removedFromTargets.contains(targetId)); + QueryPurpose purpose = + isLimbo ? QueryPurpose.LIMBO_RESOLUTION : QueryPurpose.LISTEN; + return queryData(targetId, purpose, doc.getKey().toString()); + } + }); + aggregator.handleDocumentChange(change); + return aggregator.createRemoteEvent(doc.getVersion()); + } + + public static SetMutation setMutation(String path, Map values) { + return new SetMutation(key(path), wrapObject(values), Precondition.NONE); + } + + public static PatchMutation patchMutation(String path, Map values) { + return patchMutation(path, values, null); + } + + public static PatchMutation patchMutation( + String path, Map values, @Nullable List updateMask) { + ObjectValue objectValue = ObjectValue.emptyObject(); + ArrayList objectMask = new ArrayList<>(); + for (Entry entry : values.entrySet()) { + FieldPath fieldPath = field(entry.getKey()); + objectMask.add(fieldPath); + if (!entry.getValue().equals(DELETE_SENTINEL)) { + FieldValue parsedValue = wrap(entry.getValue()); + objectValue = objectValue.set(fieldPath, parsedValue); + } + } + + boolean merge = updateMask != null; + + // We sort the fieldMaskPaths to make the order deterministic in tests. (Otherwise, when we + // flatten a Set to a proto repeated field, we'll end up comparing in iterator order and + // possibly consider {foo,bar} != {bar,foo}.) + SortedSet fieldMaskPaths = new TreeSet<>(merge ? updateMask : objectMask); + + return new PatchMutation( + key(path), + objectValue, + FieldMask.fromSet(fieldMaskPaths), + merge ? Precondition.NONE : Precondition.exists(true)); + } + + public static DeleteMutation deleteMutation(String path) { + return new DeleteMutation(key(path), Precondition.NONE); + } + + /** + * Creates a TransformMutation by parsing any FieldValue sentinels in the provided data. The data + * is expected to use dotted-notation for nested fields (i.e. { "foo.bar": FieldValue.foo() } and + * must not contain any non-sentinel data. + */ + public static TransformMutation transformMutation(String path, Map data) { + UserDataConverter dataConverter = new UserDataConverter(DatabaseId.forProject("project")); + ParsedUpdateData result = dataConverter.parseUpdateData(data); + + // The order of the transforms doesn't matter, but we sort them so tests can assume a particular + // order. + ArrayList fieldTransforms = new ArrayList<>(result.getFieldTransforms()); + Collections.sort( + fieldTransforms, (ft1, ft2) -> ft1.getFieldPath().compareTo(ft2.getFieldPath())); + + return new TransformMutation(key(path), fieldTransforms); + } + + public static MutationResult mutationResult(long version) { + return new MutationResult(version(version), null); + } + + public static LocalViewChanges viewChanges( + int targetId, List addedKeys, List removedKeys) { + ImmutableSortedSet added = DocumentKey.emptyKeySet(); + for (String keyPath : addedKeys) { + added = added.insert(key(keyPath)); + } + ImmutableSortedSet removed = DocumentKey.emptyKeySet(); + for (String keyPath : removedKeys) { + removed = removed.insert(key(keyPath)); + } + return new LocalViewChanges(targetId, added, removed); + } + + /** Creates a resume token to match the given snapshot version. */ + @Nullable + public static ByteString resumeToken(long snapshotVersion) { + if (snapshotVersion == 0) { + return null; + } + + String snapshotString = "snapshot-" + snapshotVersion; + return ByteString.copyFrom(snapshotString, Charsets.UTF_8); + } + + @NonNull + private static ByteString resumeToken(SnapshotVersion snapshotVersion) { + if (snapshotVersion.equals(SnapshotVersion.NONE)) { + return ByteString.EMPTY; + } else { + return ByteString.copyFromUtf8(snapshotVersion.toString()); + } + } + + public static ByteString streamToken(String contents) { + return ByteString.copyFrom(contents, Charsets.UTF_8); + } + + private static Map fromJsonString(String json) { + try { + ObjectMapper mapper = new ObjectMapper(); + return mapper.readValue(json, new TypeReference>() {}); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static Map fromSingleQuotedString(String json) { + return fromJsonString(json.replace("'", "\"")); + } + + /** Converts the values of an ImmutableSortedMap into a list, preserving key order. */ + public static List values(ImmutableSortedMap map) { + List result = new ArrayList<>(); + for (Map.Entry entry : map) { + result.add(entry.getValue()); + } + return result; + } + + /** + * Asserts that the actual set is equal to the expected one. + * + * @param expected A list of the expected contents of the set, in order. + * @param actual The set to compare against. + * @param The type of the values of in common between the expected list and actual set. + */ + // PORTING NOTE: JUnit and XCTest use reversed conventions on expected and actual values :-(. + public static void assertSetEquals(List expected, ImmutableSortedSet actual) { + List actualList = Lists.newArrayList(actual); + assertEquals(expected, actualList); + } + + /** + * Asserts that the actual set is equal to the expected one. + * + * @param expected A list of the expected contents of the set, in order. + * @param actual The set to compare against. + * @param The type of the values of in common between the expected list and actual set. + */ + // PORTING NOTE: JUnit and XCTest use reversed conventions on expected and actual values :-(. + public static void assertSetEquals(List expected, Set actual) { + Set expectedSet = Sets.newHashSet(expected); + assertEquals(expectedSet, actual); + } + + /** Asserts that the given runnable block fails with an internal error. */ + public static void assertFails(Runnable block) { + try { + block.run(); + } catch (AssertionError e) { + assertThat(e).hasMessageThat().startsWith("INTERNAL ASSERTION FAILED:"); + // Otherwise success + return; + } + fail("Should have failed"); + } + + public static void assertDoesNotThrow(Runnable block) { + try { + block.run(); + } catch (Exception e) { + fail("Should not have thrown " + e); + } + } + + // TODO: We could probably do some de-duplication between assertFails / expectError. + /** Expects runnable to throw an exception with a specific error message. */ + public static void expectError(Runnable runnable, String exceptionMessage) { + expectError(runnable, exceptionMessage, /*context=*/ null); + } + + /** + * Expects runnable to throw an exception with a specific error message. An optional context (e.g. + * "for bad_data") can be provided which will be displayed in any resulting failure message. + */ + public static void expectError(Runnable runnable, String exceptionMessage, String context) { + boolean exceptionThrown = false; + try { + runnable.run(); + } catch (Throwable throwable) { + exceptionThrown = true; + String contextMessage = "Expected exception message was incorrect"; + if (context != null) { + contextMessage += " (" + context + ")"; + } + assertEquals(contextMessage, exceptionMessage, throwable.getMessage()); + } + if (!exceptionThrown) { + context = (context == null) ? "" : context; + fail( + "Expected exception with message '" + + exceptionMessage + + "' but no exception was thrown" + + context); + } + } +} From 99912613248152c66a1bc4354e6e978ae06574da Mon Sep 17 00:00:00 2001 From: Brian Chen Date: Mon, 17 Jun 2019 15:02:18 -0700 Subject: [PATCH 2/4] run linter --- .../firebase/firestore/testutil/TestUtil.java | 122 +++++++++--------- 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/firebase-firestore/ktx/src/test/java/com/google/firebase/firestore/testutil/TestUtil.java b/firebase-firestore/ktx/src/test/java/com/google/firebase/firestore/testutil/TestUtil.java index 6d08655f5f2..2960fb2699e 100644 --- a/firebase-firestore/ktx/src/test/java/com/google/firebase/firestore/testutil/TestUtil.java +++ b/firebase-firestore/ktx/src/test/java/com/google/firebase/firestore/testutil/TestUtil.java @@ -175,7 +175,7 @@ public static SnapshotVersion version(long versionMicros) { public static Document doc(String key, long version, Map data) { return new Document( - key(key), version(version), wrapObject(data), Document.DocumentState.SYNCED); + key(key), version(version), wrapObject(data), Document.DocumentState.SYNCED); } public static Document doc(DocumentKey key, long version, Map data) { @@ -183,12 +183,12 @@ public static Document doc(DocumentKey key, long version, Map da } public static Document doc( - String key, long version, ObjectValue data, Document.DocumentState documentState) { + String key, long version, ObjectValue data, Document.DocumentState documentState) { return new Document(key(key), version(version), data, documentState); } public static Document doc( - String key, long version, Map data, Document.DocumentState documentState) { + String key, long version, Map data, Document.DocumentState documentState) { return new Document(key(key), version(version), wrapObject(data), documentState); } @@ -281,7 +281,7 @@ public static QueryData queryData(int targetId, QueryPurpose queryPurpose, Strin public static ImmutableSortedMap docUpdates(MaybeDocument... docs) { ImmutableSortedMap res = - ImmutableSortedMap.Builder.emptyMap(DocumentKey.comparator()); + ImmutableSortedMap.Builder.emptyMap(DocumentKey.comparator()); for (MaybeDocument doc : docs) { res = res.insert(doc.getKey(), doc); } @@ -290,7 +290,7 @@ public static ImmutableSortedMap docUpdates(MaybeDoc public static ImmutableSortedMap docUpdates(Document... docs) { ImmutableSortedMap res = - ImmutableSortedMap.Builder.emptyMap(DocumentKey.comparator()); + ImmutableSortedMap.Builder.emptyMap(DocumentKey.comparator()); for (Document doc : docs) { res = res.insert(doc.getKey(), doc); } @@ -298,11 +298,11 @@ public static ImmutableSortedMap docUpdates(Document... d } public static TargetChange targetChange( - ByteString resumeToken, - boolean current, - @Nullable Collection addedDocuments, - @Nullable Collection modifiedDocuments, - @Nullable Collection removedDocuments) { + ByteString resumeToken, + boolean current, + @Nullable Collection addedDocuments, + @Nullable Collection modifiedDocuments, + @Nullable Collection removedDocuments) { ImmutableSortedSet addedDocumentKeys = DocumentKey.emptyKeySet(); ImmutableSortedSet modifiedDocumentKeys = DocumentKey.emptyKeySet(); ImmutableSortedSet removedDocumentKeys = DocumentKey.emptyKeySet(); @@ -326,7 +326,7 @@ public static TargetChange targetChange( } return new TargetChange( - resumeToken, current, addedDocumentKeys, modifiedDocumentKeys, removedDocumentKeys); + resumeToken, current, addedDocumentKeys, modifiedDocumentKeys, removedDocumentKeys); } public static TargetChange ackTarget(Document... docs) { @@ -338,7 +338,7 @@ public static Map activeQueries(Iterable targets) { Map listenMap = new HashMap<>(); for (Integer targetId : targets) { QueryData queryData = - new QueryData(query, targetId, ARBITRARY_SEQUENCE_NUMBER, QueryPurpose.LISTEN); + new QueryData(query, targetId, ARBITRARY_SEQUENCE_NUMBER, QueryPurpose.LISTEN); listenMap.put(targetId, queryData); } return listenMap; @@ -349,12 +349,12 @@ public static Map activeQueries(Integer... targets) { } public static Map activeLimboQueries( - String docKey, Iterable targets) { + String docKey, Iterable targets) { Query query = query(docKey); Map listenMap = new HashMap<>(); for (Integer targetId : targets) { QueryData queryData = - new QueryData(query, targetId, ARBITRARY_SEQUENCE_NUMBER, QueryPurpose.LIMBO_RESOLUTION); + new QueryData(query, targetId, ARBITRARY_SEQUENCE_NUMBER, QueryPurpose.LIMBO_RESOLUTION); listenMap.put(targetId, queryData); } return listenMap; @@ -365,55 +365,55 @@ public static Map activeLimboQueries(String docKey, Integer. } public static RemoteEvent addedRemoteEvent( - MaybeDocument doc, List updatedInTargets, List removedFromTargets) { + MaybeDocument doc, List updatedInTargets, List removedFromTargets) { DocumentChange change = - new DocumentChange(updatedInTargets, removedFromTargets, doc.getKey(), doc); + new DocumentChange(updatedInTargets, removedFromTargets, doc.getKey(), doc); WatchChangeAggregator aggregator = - new WatchChangeAggregator( - new WatchChangeAggregator.TargetMetadataProvider() { - @Override - public ImmutableSortedSet getRemoteKeysForTarget(int targetId) { - return DocumentKey.emptyKeySet(); - } - - @Override - public QueryData getQueryDataForTarget(int targetId) { - return queryData(targetId, QueryPurpose.LISTEN, doc.getKey().toString()); - } - }); + new WatchChangeAggregator( + new WatchChangeAggregator.TargetMetadataProvider() { + @Override + public ImmutableSortedSet getRemoteKeysForTarget(int targetId) { + return DocumentKey.emptyKeySet(); + } + + @Override + public QueryData getQueryDataForTarget(int targetId) { + return queryData(targetId, QueryPurpose.LISTEN, doc.getKey().toString()); + } + }); aggregator.handleDocumentChange(change); return aggregator.createRemoteEvent(doc.getVersion()); } public static RemoteEvent updateRemoteEvent( - MaybeDocument doc, List updatedInTargets, List removedFromTargets) { + MaybeDocument doc, List updatedInTargets, List removedFromTargets) { return updateRemoteEvent(doc, updatedInTargets, removedFromTargets, Collections.emptyList()); } public static RemoteEvent updateRemoteEvent( - MaybeDocument doc, - List updatedInTargets, - List removedFromTargets, - List limboTargets) { + MaybeDocument doc, + List updatedInTargets, + List removedFromTargets, + List limboTargets) { DocumentChange change = - new DocumentChange(updatedInTargets, removedFromTargets, doc.getKey(), doc); + new DocumentChange(updatedInTargets, removedFromTargets, doc.getKey(), doc); WatchChangeAggregator aggregator = - new WatchChangeAggregator( - new WatchChangeAggregator.TargetMetadataProvider() { - @Override - public ImmutableSortedSet getRemoteKeysForTarget(int targetId) { - return DocumentKey.emptyKeySet().insert(doc.getKey()); - } - - @Override - public QueryData getQueryDataForTarget(int targetId) { - boolean isLimbo = - !(updatedInTargets.contains(targetId) || removedFromTargets.contains(targetId)); - QueryPurpose purpose = - isLimbo ? QueryPurpose.LIMBO_RESOLUTION : QueryPurpose.LISTEN; - return queryData(targetId, purpose, doc.getKey().toString()); - } - }); + new WatchChangeAggregator( + new WatchChangeAggregator.TargetMetadataProvider() { + @Override + public ImmutableSortedSet getRemoteKeysForTarget(int targetId) { + return DocumentKey.emptyKeySet().insert(doc.getKey()); + } + + @Override + public QueryData getQueryDataForTarget(int targetId) { + boolean isLimbo = + !(updatedInTargets.contains(targetId) || removedFromTargets.contains(targetId)); + QueryPurpose purpose = + isLimbo ? QueryPurpose.LIMBO_RESOLUTION : QueryPurpose.LISTEN; + return queryData(targetId, purpose, doc.getKey().toString()); + } + }); aggregator.handleDocumentChange(change); return aggregator.createRemoteEvent(doc.getVersion()); } @@ -427,7 +427,7 @@ public static PatchMutation patchMutation(String path, Map value } public static PatchMutation patchMutation( - String path, Map values, @Nullable List updateMask) { + String path, Map values, @Nullable List updateMask) { ObjectValue objectValue = ObjectValue.emptyObject(); ArrayList objectMask = new ArrayList<>(); for (Entry entry : values.entrySet()) { @@ -447,10 +447,10 @@ public static PatchMutation patchMutation( SortedSet fieldMaskPaths = new TreeSet<>(merge ? updateMask : objectMask); return new PatchMutation( - key(path), - objectValue, - FieldMask.fromSet(fieldMaskPaths), - merge ? Precondition.NONE : Precondition.exists(true)); + key(path), + objectValue, + FieldMask.fromSet(fieldMaskPaths), + merge ? Precondition.NONE : Precondition.exists(true)); } public static DeleteMutation deleteMutation(String path) { @@ -470,7 +470,7 @@ public static TransformMutation transformMutation(String path, Map fieldTransforms = new ArrayList<>(result.getFieldTransforms()); Collections.sort( - fieldTransforms, (ft1, ft2) -> ft1.getFieldPath().compareTo(ft2.getFieldPath())); + fieldTransforms, (ft1, ft2) -> ft1.getFieldPath().compareTo(ft2.getFieldPath())); return new TransformMutation(key(path), fieldTransforms); } @@ -480,7 +480,7 @@ public static MutationResult mutationResult(long version) { } public static LocalViewChanges viewChanges( - int targetId, List addedKeys, List removedKeys) { + int targetId, List addedKeys, List removedKeys) { ImmutableSortedSet added = DocumentKey.emptyKeySet(); for (String keyPath : addedKeys) { added = added.insert(key(keyPath)); @@ -609,10 +609,10 @@ public static void expectError(Runnable runnable, String exceptionMessage, Strin if (!exceptionThrown) { context = (context == null) ? "" : context; fail( - "Expected exception with message '" - + exceptionMessage - + "' but no exception was thrown" - + context); + "Expected exception with message '" + + exceptionMessage + + "' but no exception was thrown" + + context); } } } From ffe8fcac789f346eec865160d8b968708214ae5e Mon Sep 17 00:00:00 2001 From: Brian Chen Date: Mon, 17 Jun 2019 16:18:29 -0700 Subject: [PATCH 3/4] moved all required files, passing ktx build --- firebase-firestore/ktx/ktx.gradle | 8 +- .../firebase/firestore/TestAccessHelper.java | 31 +++ .../google/firebase/firestore/TestUtil.java | 182 ++++++++++++++++++ .../firebase/firestore/testutil/TestUtil.java | 19 +- 4 files changed, 229 insertions(+), 11 deletions(-) create mode 100644 firebase-firestore/ktx/src/test/java/com/google/firebase/firestore/TestAccessHelper.java create mode 100644 firebase-firestore/ktx/src/test/java/com/google/firebase/firestore/TestUtil.java diff --git a/firebase-firestore/ktx/ktx.gradle b/firebase-firestore/ktx/ktx.gradle index 2e4f17d484a..ffbfb8f652f 100644 --- a/firebase-firestore/ktx/ktx.gradle +++ b/firebase-firestore/ktx/ktx.gradle @@ -23,7 +23,7 @@ firebaseLibrary { } android { - compileSdkVersion project.targetSdkVersion + compileSdkVersion 28 defaultConfig { minSdkVersion project.minSdkVersion multiDexEnabled true @@ -38,16 +38,18 @@ android { } } testOptions.unitTests.includeAndroidResources = true + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" - implementation project(':firebase-common') implementation project(':firebase-common:ktx') implementation project(':firebase-firestore') implementation 'androidx.annotation:annotation:1.1.0' - testImplementation project(':firebase-database-collection') testImplementation 'org.mockito:mockito-core:2.25.0' testImplementation 'com.fasterxml.jackson.core:jackson-databind:2.9.8' diff --git a/firebase-firestore/ktx/src/test/java/com/google/firebase/firestore/TestAccessHelper.java b/firebase-firestore/ktx/src/test/java/com/google/firebase/firestore/TestAccessHelper.java new file mode 100644 index 00000000000..bab88979493 --- /dev/null +++ b/firebase-firestore/ktx/src/test/java/com/google/firebase/firestore/TestAccessHelper.java @@ -0,0 +1,31 @@ +// Copyright 2018 Google LLC +// +// 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.firestore; + +import com.google.firebase.firestore.model.DocumentKey; + +public final class TestAccessHelper { + + /** Makes the DocumentReference constructor accessible. */ + public static DocumentReference createDocumentReference(DocumentKey documentKey) { + // We can use null here because the tests only use this as a wrapper for documentKeys. + return new DocumentReference(documentKey, null); + } + + /** Makes the getKey() method accessible. */ + public static DocumentKey referenceKey(DocumentReference documentReference) { + return documentReference.getKey(); + } +} diff --git a/firebase-firestore/ktx/src/test/java/com/google/firebase/firestore/TestUtil.java b/firebase-firestore/ktx/src/test/java/com/google/firebase/firestore/TestUtil.java new file mode 100644 index 00000000000..ffce83cbaa4 --- /dev/null +++ b/firebase-firestore/ktx/src/test/java/com/google/firebase/firestore/TestUtil.java @@ -0,0 +1,182 @@ +// Copyright 2019 Google LLC +// +// 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.firestore; + +import com.google.android.gms.tasks.Task; +import com.google.firebase.database.collection.ImmutableSortedSet; +import com.google.firebase.firestore.core.DocumentViewChange; +import com.google.firebase.firestore.core.DocumentViewChange.Type; +import com.google.firebase.firestore.core.ViewSnapshot; +import com.google.firebase.firestore.local.QueryData; +import com.google.firebase.firestore.model.Document; +import com.google.firebase.firestore.model.DocumentKey; +import com.google.firebase.firestore.model.DocumentSet; +import com.google.firebase.firestore.model.ResourcePath; +import com.google.firebase.firestore.model.value.ObjectValue; +import com.google.firebase.firestore.remote.WatchChangeAggregator; + +import org.junit.Assert; +import org.robolectric.Robolectric; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import androidx.annotation.Nullable; + +import static com.google.firebase.firestore.testutil.TestUtil.doc; +import static com.google.firebase.firestore.testutil.TestUtil.docSet; +import static com.google.firebase.firestore.testutil.TestUtil.key; +import static org.mockito.Mockito.mock; + +public class TestUtil { + + private static final FirebaseFirestore FIRESTORE = mock(FirebaseFirestore.class); + + public static FirebaseFirestore firestore() { + return FIRESTORE; + } + + public static CollectionReference collectionReference(String path) { + return new CollectionReference(ResourcePath.fromString(path), FIRESTORE); + } + + public static DocumentReference documentReference(String path) { + return new DocumentReference(key(path), FIRESTORE); + } + + public static DocumentSnapshot documentSnapshot( + String path, Map data, boolean isFromCache) { + if (data == null) { + return DocumentSnapshot.fromNoDocument( + FIRESTORE, key(path), isFromCache, /*hasPendingWrites=*/ false); + } else { + return DocumentSnapshot.fromDocument( + FIRESTORE, doc(path, 1L, data), isFromCache, /*hasPendingWrites=*/ false); + } + } + + public static Query query(String path) { + return new Query(com.google.firebase.firestore.testutil.TestUtil.query(path), FIRESTORE); + } + + /** + * A convenience method for creating a particular query snapshot for tests. + * + * @param path To be used in constructing the query. + * @param oldDocs Provides the prior set of documents in the QuerySnapshot. Each entry maps to a + * document, with the key being the document id, and the value being the document contents. + * @param docsToAdd Specifies data to be added into the query snapshot as of now. Each entry maps + * to a document, with the key being the document id, and the value being the document + * contents. + * @param isFromCache Whether the query snapshot is cache result. + * @return A query snapshot that consists of both sets of documents. + */ + public static QuerySnapshot querySnapshot( + String path, + Map oldDocs, + Map docsToAdd, + boolean hasPendingWrites, + boolean isFromCache) { + DocumentSet oldDocuments = docSet(Document.keyComparator()); + ImmutableSortedSet mutatedKeys = DocumentKey.emptyKeySet(); + for (Map.Entry pair : oldDocs.entrySet()) { + String docKey = path + "/" + pair.getKey(); + oldDocuments = + oldDocuments.add( + doc( + docKey, + 1L, + pair.getValue(), + hasPendingWrites + ? Document.DocumentState.SYNCED + : Document.DocumentState.LOCAL_MUTATIONS)); + + if (hasPendingWrites) { + mutatedKeys = mutatedKeys.insert(key(docKey)); + } + } + DocumentSet newDocuments = docSet(Document.keyComparator()); + List documentChanges = new ArrayList<>(); + for (Map.Entry pair : docsToAdd.entrySet()) { + String docKey = path + "/" + pair.getKey(); + Document docToAdd = + doc( + docKey, + 1L, + pair.getValue(), + hasPendingWrites + ? Document.DocumentState.SYNCED + : Document.DocumentState.LOCAL_MUTATIONS); + newDocuments = newDocuments.add(docToAdd); + documentChanges.add(DocumentViewChange.create(Type.ADDED, docToAdd)); + + if (hasPendingWrites) { + mutatedKeys = mutatedKeys.insert(key(docKey)); + } + } + ViewSnapshot viewSnapshot = + new ViewSnapshot( + com.google.firebase.firestore.testutil.TestUtil.query(path), + newDocuments, + oldDocuments, + documentChanges, + isFromCache, + mutatedKeys, + true, + /* excludesMetadataChanges= */ false); + return new QuerySnapshot(query(path), viewSnapshot, FIRESTORE); + } + + /** + * An implementation of TargetMetadataProvider that provides controlled access to the + * `TargetMetadataProvider` callbacks. Any target accessed via these callbacks must be registered + * beforehand via `setSyncedKeys()`. + */ + public static class TestTargetMetadataProvider + implements WatchChangeAggregator.TargetMetadataProvider { + final Map> syncedKeys = new HashMap<>(); + final Map queryData = new HashMap<>(); + + @Override + public ImmutableSortedSet getRemoteKeysForTarget(int targetId) { + return syncedKeys.get(targetId) != null + ? syncedKeys.get(targetId) + : DocumentKey.emptyKeySet(); + } + + @Nullable + @Override + public QueryData getQueryDataForTarget(int targetId) { + return queryData.get(targetId); + } + + /** Sets or replaces the local state for the provided query data. */ + public void setSyncedKeys(QueryData queryData, ImmutableSortedSet keys) { + this.queryData.put(queryData.getTargetId(), queryData); + this.syncedKeys.put(queryData.getTargetId(), keys); + } + } + + public static T waitFor(Task task) { + if (!task.isComplete()) { + Robolectric.flushBackgroundThreadScheduler(); + } + Assert.assertTrue( + "Expected task to be completed after background thread flush", task.isComplete()); + return task.getResult(); + } +} diff --git a/firebase-firestore/ktx/src/test/java/com/google/firebase/firestore/testutil/TestUtil.java b/firebase-firestore/ktx/src/test/java/com/google/firebase/firestore/testutil/TestUtil.java index 2960fb2699e..0010cb55e79 100644 --- a/firebase-firestore/ktx/src/test/java/com/google/firebase/firestore/testutil/TestUtil.java +++ b/firebase-firestore/ktx/src/test/java/com/google/firebase/firestore/testutil/TestUtil.java @@ -1,4 +1,4 @@ -// Copyright 2018 Google LLC +// Copyright 2019 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,13 +14,6 @@ package com.google.firebase.firestore.testutil; -import static com.google.common.truth.Truth.assertThat; -import static java.util.Arrays.asList; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.fail; - -import androidx.annotation.NonNull; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Charsets; @@ -67,6 +60,7 @@ import com.google.firebase.firestore.remote.WatchChange.DocumentChange; import com.google.firebase.firestore.remote.WatchChangeAggregator; import com.google.protobuf.ByteString; + import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; @@ -81,8 +75,17 @@ import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; + import javax.annotation.Nullable; +import androidx.annotation.NonNull; + +import static java.util.Arrays.asList; +import static org.junit.Assert.fail; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static com.google.common.truth.Truth.assertThat; + /** A set of utilities for tests */ public class TestUtil { From 992adac44765689136bd41380d8593692eaee75b Mon Sep 17 00:00:00 2001 From: Brian Chen Date: Mon, 17 Jun 2019 16:19:02 -0700 Subject: [PATCH 4/4] linting changes --- .../google/firebase/firestore/TestUtil.java | 19 ++++++++----------- .../firebase/firestore/testutil/TestUtil.java | 17 +++++++---------- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/firebase-firestore/ktx/src/test/java/com/google/firebase/firestore/TestUtil.java b/firebase-firestore/ktx/src/test/java/com/google/firebase/firestore/TestUtil.java index ffce83cbaa4..d2d032ea3ae 100644 --- a/firebase-firestore/ktx/src/test/java/com/google/firebase/firestore/TestUtil.java +++ b/firebase-firestore/ktx/src/test/java/com/google/firebase/firestore/TestUtil.java @@ -14,6 +14,12 @@ package com.google.firebase.firestore; +import static com.google.firebase.firestore.testutil.TestUtil.doc; +import static com.google.firebase.firestore.testutil.TestUtil.docSet; +import static com.google.firebase.firestore.testutil.TestUtil.key; +import static org.mockito.Mockito.mock; + +import androidx.annotation.Nullable; import com.google.android.gms.tasks.Task; import com.google.firebase.database.collection.ImmutableSortedSet; import com.google.firebase.firestore.core.DocumentViewChange; @@ -26,21 +32,12 @@ import com.google.firebase.firestore.model.ResourcePath; import com.google.firebase.firestore.model.value.ObjectValue; import com.google.firebase.firestore.remote.WatchChangeAggregator; - -import org.junit.Assert; -import org.robolectric.Robolectric; - import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; - -import androidx.annotation.Nullable; - -import static com.google.firebase.firestore.testutil.TestUtil.doc; -import static com.google.firebase.firestore.testutil.TestUtil.docSet; -import static com.google.firebase.firestore.testutil.TestUtil.key; -import static org.mockito.Mockito.mock; +import org.junit.Assert; +import org.robolectric.Robolectric; public class TestUtil { diff --git a/firebase-firestore/ktx/src/test/java/com/google/firebase/firestore/testutil/TestUtil.java b/firebase-firestore/ktx/src/test/java/com/google/firebase/firestore/testutil/TestUtil.java index 0010cb55e79..c2ce41b0d8a 100644 --- a/firebase-firestore/ktx/src/test/java/com/google/firebase/firestore/testutil/TestUtil.java +++ b/firebase-firestore/ktx/src/test/java/com/google/firebase/firestore/testutil/TestUtil.java @@ -14,6 +14,13 @@ package com.google.firebase.firestore.testutil; +import static com.google.common.truth.Truth.assertThat; +import static java.util.Arrays.asList; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.fail; + +import androidx.annotation.NonNull; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Charsets; @@ -60,7 +67,6 @@ import com.google.firebase.firestore.remote.WatchChange.DocumentChange; import com.google.firebase.firestore.remote.WatchChangeAggregator; import com.google.protobuf.ByteString; - import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; @@ -75,17 +81,8 @@ import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; - import javax.annotation.Nullable; -import androidx.annotation.NonNull; - -import static java.util.Arrays.asList; -import static org.junit.Assert.fail; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; -import static com.google.common.truth.Truth.assertThat; - /** A set of utilities for tests */ public class TestUtil {