2222import static com .google .cloud .firestore .UserDataConverter .NO_DELETES ;
2323import static org .mockito .Mockito .doAnswer ;
2424import static org .mockito .Mockito .doReturn ;
25+ import static org .mockito .Mockito .when ;
2526
2627import com .google .api .core .ApiFuture ;
28+ import com .google .api .core .SettableApiFuture ;
2729import com .google .api .gax .rpc .ApiStreamObserver ;
30+ import com .google .api .gax .rpc .BidiStreamingCallable ;
2831import com .google .api .gax .rpc .ServerStreamingCallable ;
2932import com .google .api .gax .rpc .UnaryCallable ;
3033import com .google .cloud .firestore .Query .Direction ;
3336import com .google .cloud .firestore .conformance .TestDefinition .CreateTest ;
3437import com .google .cloud .firestore .conformance .TestDefinition .Cursor ;
3538import com .google .cloud .firestore .conformance .TestDefinition .DeleteTest ;
39+ import com .google .cloud .firestore .conformance .TestDefinition .DocChange ;
40+ import com .google .cloud .firestore .conformance .TestDefinition .DocChange .Kind ;
3641import com .google .cloud .firestore .conformance .TestDefinition .DocSnapshot ;
3742import com .google .cloud .firestore .conformance .TestDefinition .GetTest ;
3843import com .google .cloud .firestore .conformance .TestDefinition .OrderBy ;
3944import com .google .cloud .firestore .conformance .TestDefinition .SetTest ;
45+ import com .google .cloud .firestore .conformance .TestDefinition .Snapshot ;
4046import com .google .cloud .firestore .conformance .TestDefinition .UpdatePathsTest ;
4147import com .google .cloud .firestore .conformance .TestDefinition .UpdateTest ;
4248import com .google .cloud .firestore .conformance .TestDefinition .Where ;
4349import com .google .cloud .firestore .spi .v1beta1 .FirestoreRpc ;
4450import com .google .common .base .Preconditions ;
4551import com .google .firestore .v1beta1 .BatchGetDocumentsRequest ;
4652import com .google .firestore .v1beta1 .CommitRequest ;
53+ import com .google .firestore .v1beta1 .Document ;
54+ import com .google .firestore .v1beta1 .ListenRequest ;
55+ import com .google .firestore .v1beta1 .ListenResponse ;
4756import com .google .firestore .v1beta1 .Precondition ;
4857import com .google .firestore .v1beta1 .RunQueryRequest ;
4958import com .google .firestore .v1beta1 .Value ;
5059import com .google .gson .Gson ;
5160import com .google .gson .reflect .TypeToken ;
61+ import com .google .protobuf .GeneratedMessageV3 ;
5262import com .google .protobuf .Message ;
5363import java .io .FileInputStream ;
5464import java .io .IOException ;
5565import java .lang .reflect .Type ;
5666import java .util .ArrayList ;
5767import java .util .Collections ;
68+ import java .util .Comparator ;
5869import java .util .List ;
5970import java .util .Map ;
6071import java .util .Map .Entry ;
6172import java .util .Set ;
6273import java .util .concurrent .ExecutionException ;
74+ import java .util .concurrent .Executors ;
75+ import java .util .concurrent .ScheduledExecutorService ;
76+ import javax .annotation .Nullable ;
6377import junit .framework .Protectable ;
6478import junit .framework .Test ;
6579import junit .framework .TestResult ;
7286import org .mockito .ArgumentCaptor ;
7387import org .mockito .Captor ;
7488import org .mockito .Matchers ;
89+ import org .mockito .Mock ;
7590import org .mockito .Mockito ;
7691import org .mockito .MockitoAnnotations ;
77- import org .mockito .Spy ;
92+ import org .mockito .invocation .InvocationOnMock ;
93+ import org .mockito .stubbing .Answer ;
7894import org .threeten .bp .Instant ;
7995
8096@ RunWith (AllTests .class )
@@ -90,22 +106,34 @@ private interface ConformanceTestCase extends Test, Describable {}
90106 /** If non-empty, only runs tests included in this set. */
91107 private final Set <String > includedTests = Collections .emptySet ();
92108
93- @ Spy
94- private FirestoreImpl firestoreMock =
95- new FirestoreImpl (
96- FirestoreOptions .newBuilder ().setProjectId ("projectID" ).build (),
97- Mockito .mock (FirestoreRpc .class ));
98-
99109 @ Captor private ArgumentCaptor <CommitRequest > commitCapture ;
100110
101111 @ Captor private ArgumentCaptor <BatchGetDocumentsRequest > getAllCapture ;
102112
103- @ Captor private ArgumentCaptor <ApiStreamObserver > streamObserverCapture ;
113+ @ Captor private ArgumentCaptor <ApiStreamObserver < GeneratedMessageV3 > > streamObserverCapture ;
104114
105115 @ Captor private ArgumentCaptor <RunQueryRequest > runQueryCapture ;
106116
107117 private Gson gson = new Gson ();
108118
119+ @ Mock private ApiStreamObserver <ListenRequest > noOpRequestObserver ;
120+
121+ private FirestoreImpl firestoreMock ;
122+
123+ private Query watchQuery ;
124+
125+ public ConformanceTest () {
126+ ScheduledExecutorService firestoreExecutor = Executors .newSingleThreadScheduledExecutor ();
127+ FirestoreRpc firestoreRpc = Mockito .mock (FirestoreRpc .class );
128+ when (firestoreRpc .getExecutor ()).thenReturn (firestoreExecutor );
129+
130+ firestoreMock =
131+ Mockito .spy (
132+ new FirestoreImpl (
133+ FirestoreOptions .newBuilder ().setProjectId ("projectID" ).build (), firestoreRpc ));
134+ watchQuery = collection ("projects/projectID/databases/(default)/documents/C" ).orderBy ("a" );
135+ }
136+
109137 /** Generate the test suite based on the tests defined in test_data.binprotos. */
110138 public static TestSuite suite () throws IOException {
111139 TestSuite suite = new TestSuite ();
@@ -221,6 +249,84 @@ private Object convertValue(Object data) {
221249 }
222250 }
223251
252+ /** Converts a Kind into a DocumentChange.Type */
253+ private DocumentChange .Type convertKind (Kind kind ) {
254+ DocumentChange .Type changeType = null ;
255+
256+ switch (kind ) {
257+ case ADDED :
258+ changeType = DocumentChange .Type .ADDED ;
259+ break ;
260+ case REMOVED :
261+ changeType = DocumentChange .Type .REMOVED ;
262+ break ;
263+ case MODIFIED :
264+ changeType = DocumentChange .Type .MODIFIED ;
265+ break ;
266+ default :
267+ Assert .fail ("Unable to convert document change for kind: " + kind );
268+ break ;
269+ }
270+
271+ return changeType ;
272+ }
273+
274+ /** Converts a test snapshot to its API representation. */
275+ private QuerySnapshot convertQuerySnapshot (Snapshot testSnapshot ) {
276+ Instant readTime =
277+ Instant .ofEpochSecond (
278+ testSnapshot .getReadTime ().getSeconds (), testSnapshot .getReadTime ().getNanos ());
279+
280+ final List <QueryDocumentSnapshot > documentSnapshots = new ArrayList <>();
281+
282+ for (Document document : testSnapshot .getDocsList ()) {
283+ documentSnapshots .add (
284+ QueryDocumentSnapshot .fromDocument (firestoreMock , testSnapshot .getReadTime (), document ));
285+ }
286+
287+ DocumentSet documentSet =
288+ DocumentSet .emptySet (
289+ new Comparator <QueryDocumentSnapshot >() {
290+ @ Override
291+ public int compare (QueryDocumentSnapshot o1 , QueryDocumentSnapshot o2 ) {
292+ // Comparator that preserves the insertion order as defined in the test case.
293+ for (QueryDocumentSnapshot snapshot : documentSnapshots ) {
294+ if (o1 .equals (o2 )) {
295+ return 0 ;
296+ } else if (o1 .equals (snapshot )) {
297+ return -1 ;
298+ } else if (o2 .equals (snapshot )) {
299+ return 1 ;
300+ }
301+ }
302+
303+ Assert .fail ("Unable to find position for document snapshot" );
304+ return 0 ;
305+ }
306+ });
307+
308+ for (QueryDocumentSnapshot snapshot : documentSnapshots ) {
309+ documentSet = documentSet .add (snapshot );
310+ }
311+
312+ List <DocumentChange > documentChanges = new ArrayList <>();
313+
314+ for (DocChange documentChange : testSnapshot .getChangesList ()) {
315+ QueryDocumentSnapshot documentSnapshot =
316+ QueryDocumentSnapshot .fromDocument (
317+ firestoreMock , testSnapshot .getReadTime (), documentChange .getDoc ());
318+ DocumentChange .Type changeType = convertKind (documentChange .getKind ());
319+ documentChanges .add (
320+ new DocumentChange (
321+ documentSnapshot ,
322+ changeType ,
323+ documentChange .getOldIndex (),
324+ documentChange .getNewIndex ()));
325+ }
326+
327+ return QuerySnapshot .withChanges (watchQuery , readTime , documentSet , documentChanges );
328+ }
329+
224330 /** Helper function to convert test values in a list to Firestore API types. */
225331 private List <Object > convertArray (List <Object > list ) {
226332 for (int i = 0 ; i < list .size (); ++i ) {
@@ -294,6 +400,9 @@ public void protect() throws Throwable {
294400 case QUERY :
295401 runQueryTest (testDefinition .getQuery ());
296402 break ;
403+ case LISTEN :
404+ runWatchTest (testDefinition .getListen ());
405+ break ;
297406 default :
298407 throw new UnsupportedOperationException ();
299408 }
@@ -330,6 +439,53 @@ private void runQueryTest(TestDefinition.QueryTest testCase) {
330439 }
331440 }
332441
442+ private void runWatchTest (TestDefinition .ListenTest testCase )
443+ throws ExecutionException , InterruptedException {
444+ final SettableApiFuture <Void > testCaseStarted = SettableApiFuture .create ();
445+ final SettableApiFuture <Void > testCaseFinished = SettableApiFuture .create ();
446+
447+ doAnswer (
448+ new Answer <ApiStreamObserver <ListenRequest >>() {
449+ @ Override
450+ public ApiStreamObserver <ListenRequest > answer (InvocationOnMock invocationOnMock ) {
451+ testCaseStarted .set (null );
452+ return noOpRequestObserver ;
453+ }
454+ })
455+ .when (firestoreMock )
456+ .streamRequest (streamObserverCapture .capture (), Matchers .any (BidiStreamingCallable .class ));
457+
458+ final List <Snapshot > expectedSnapshots = new ArrayList <>(testCase .getSnapshotsList ());
459+
460+ final ListenerRegistration registration =
461+ watchQuery .addSnapshotListener (
462+ new EventListener <QuerySnapshot >() {
463+ @ Override
464+ public void onEvent (
465+ @ Nullable QuerySnapshot actualSnapshot , @ Nullable FirestoreException error ) {
466+ try {
467+ Assert .assertFalse (expectedSnapshots .isEmpty ());
468+ Snapshot expectedSnapshot = expectedSnapshots .remove (0 );
469+ Assert .assertEquals (convertQuerySnapshot (expectedSnapshot ), actualSnapshot );
470+ if (expectedSnapshots .isEmpty ()) {
471+ testCaseFinished .set (null );
472+ }
473+ } catch (AssertionError e ) {
474+ testCaseFinished .setException (e );
475+ }
476+ }
477+ });
478+
479+ testCaseStarted .get ();
480+
481+ for (ListenResponse response : testCase .getResponsesList ()) {
482+ streamObserverCapture .getValue ().onNext (response );
483+ }
484+
485+ testCaseFinished .get ();
486+ registration .remove ();
487+ }
488+
333489 /** Returns a new query with 'clause' applied. */
334490 private Query applyClause (Query query , Clause clause ) {
335491 FieldPath fieldPath ;
0 commit comments