Skip to content

Commit cd4b9af

Browse files
Add spec test support for target-scoped resume tokens (#969)
1 parent 3f353b8 commit cd4b9af

File tree

5 files changed

+87
-82
lines changed

5 files changed

+87
-82
lines changed

packages/firestore/src/remote/remote_syncer.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export interface RemoteSyncer {
3636
* Rejects the listen for the given targetID. This can be triggered by the
3737
* backend for any active target.
3838
*
39-
* @param targetID The targetID corresponds to one previously initiated by the
39+
* @param targetId The targetID corresponds to one previously initiated by the
4040
* user as part of QueryData passed to listen() on RemoteStore.
4141
* @param error A description of the condition that has forced the rejection.
4242
* Nearly always this will be an indication that the user is no longer

packages/firestore/test/unit/specs/existence_filter_spec.test.ts

-1
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,6 @@ describeSpec('Existence Filters:', [], () => {
204204
Query.atPath(doc2.key.path),
205205
new RpcError(Code.PERMISSION_DENIED, 'no')
206206
)
207-
.watchSnapshots(3000)
208207
.expectLimboDocs() // doc2 is no longer in limbo
209208
.expectEvents(query, {
210209
removed: [doc2]

packages/firestore/test/unit/specs/listen_spec.test.ts

+20
Original file line numberDiff line numberDiff line change
@@ -406,4 +406,24 @@ describeSpec('Listens:', [], () => {
406406
);
407407
}
408408
);
409+
410+
specTest('Persists resume token sent with target', [], () => {
411+
const query = Query.atPath(path('collection'));
412+
const docA = doc('collection/a', 2000, { key: 'a' });
413+
return spec()
414+
.withGCEnabled(false)
415+
.userListens(query)
416+
.watchAcksFull(query, 1000)
417+
.expectEvents(query, {})
418+
.watchSends({ affects: [query] }, docA)
419+
.watchSnapshots(2000, [query], 'resume-token-2000')
420+
.watchSnapshots(2000)
421+
.expectEvents(query, { added: [docA] })
422+
.userUnlistens(query)
423+
.watchRemoves(query)
424+
.userListens(query, 'resume-token-2000')
425+
.expectEvents(query, { added: [docA], fromCache: true })
426+
.watchAcksFull(query, 3000)
427+
.expectEvents(query, {});
428+
});
409429
});

packages/firestore/test/unit/specs/spec_builder.ts

+10-3
Original file line numberDiff line numberDiff line change
@@ -481,9 +481,16 @@ export class SpecBuilder {
481481
return this;
482482
}
483483

484-
watchSnapshots(version: TestSnapshotVersion): SpecBuilder {
485-
this.assertStep('Watch snapshot requires previous watch step');
486-
this.currentStep!.watchSnapshot = version;
484+
watchSnapshots(
485+
version: TestSnapshotVersion,
486+
targets?: Query[],
487+
resumeToken?: string
488+
): SpecBuilder {
489+
this.nextStep();
490+
const targetIds = targets && targets.map(query => this.getTargetId(query));
491+
this.currentStep = {
492+
watchSnapshot: { version, targetIds, resumeToken }
493+
};
487494
return this;
488495
}
489496

packages/firestore/test/unit/specs/spec_test_runner.ts

+56-77
Original file line numberDiff line numberDiff line change
@@ -454,17 +454,19 @@ abstract class TestRunner {
454454
} else if ('userDelete' in step) {
455455
return this.doDelete(step.userDelete!);
456456
} else if ('watchAck' in step) {
457-
return this.doWatchAck(step.watchAck!, step.watchSnapshot);
457+
return this.doWatchAck(step.watchAck!);
458458
} else if ('watchCurrent' in step) {
459-
return this.doWatchCurrent(step.watchCurrent!, step.watchSnapshot);
459+
return this.doWatchCurrent(step.watchCurrent!);
460460
} else if ('watchRemove' in step) {
461-
return this.doWatchRemove(step.watchRemove!, step.watchSnapshot);
461+
return this.doWatchRemove(step.watchRemove!);
462462
} else if ('watchEntity' in step) {
463-
return this.doWatchEntity(step.watchEntity!, step.watchSnapshot);
463+
return this.doWatchEntity(step.watchEntity!);
464464
} else if ('watchFilter' in step) {
465-
return this.doWatchFilter(step.watchFilter!, step.watchSnapshot);
465+
return this.doWatchFilter(step.watchFilter!);
466+
} else if ('watchSnapshot' in step) {
467+
return this.doWatchSnapshot(step.watchSnapshot!);
466468
} else if ('watchReset' in step) {
467-
return this.doWatchReset(step.watchReset!, step.watchSnapshot);
469+
return this.doWatchReset(step.watchReset!);
468470
} else if ('watchStreamClose' in step) {
469471
return this.doWatchStreamClose(step.watchStreamClose!);
470472
} else if ('writeAck' in step) {
@@ -554,46 +556,34 @@ abstract class TestRunner {
554556
});
555557
}
556558

557-
private doWatchAck(
558-
ackedTargets: SpecWatchAck,
559-
watchSnapshot?: SpecSnapshotVersion
560-
): Promise<void> {
559+
private doWatchAck(ackedTargets: SpecWatchAck): Promise<void> {
561560
const change = new WatchTargetChange(
562561
WatchTargetChangeState.Added,
563562
ackedTargets
564563
);
565-
return this.doWatchEvent(change, watchSnapshot);
564+
return this.doWatchEvent(change);
566565
}
567566

568-
private doWatchCurrent(
569-
currentTargets: SpecWatchCurrent,
570-
watchSnapshot?: SpecSnapshotVersion
571-
): Promise<void> {
567+
private doWatchCurrent(currentTargets: SpecWatchCurrent): Promise<void> {
572568
const targets = currentTargets[0];
573569
const resumeToken = currentTargets[1] as ProtoByteString;
574570
const change = new WatchTargetChange(
575571
WatchTargetChangeState.Current,
576572
targets,
577573
resumeToken
578574
);
579-
return this.doWatchEvent(change, watchSnapshot);
575+
return this.doWatchEvent(change);
580576
}
581577

582-
private doWatchReset(
583-
targetIds: SpecWatchReset,
584-
watchSnapshot?: SpecSnapshotVersion
585-
): Promise<void> {
578+
private doWatchReset(targetIds: SpecWatchReset): Promise<void> {
586579
const change = new WatchTargetChange(
587580
WatchTargetChangeState.Reset,
588581
targetIds
589582
);
590-
return this.doWatchEvent(change, watchSnapshot);
583+
return this.doWatchEvent(change);
591584
}
592585

593-
private doWatchRemove(
594-
removed: SpecWatchRemove,
595-
watchSnapshot?: SpecSnapshotVersion
596-
): Promise<void> {
586+
private doWatchRemove(removed: SpecWatchRemove): Promise<void> {
597587
const cause =
598588
removed.cause &&
599589
new FirestoreError(
@@ -619,30 +609,21 @@ abstract class TestRunner {
619609
delete this.connection.activeTargets[targetId];
620610
});
621611
}
622-
return this.doWatchEvent(change, watchSnapshot);
612+
return this.doWatchEvent(change);
623613
}
624614

625-
private doWatchEntity(
626-
watchEntity: SpecWatchEntity,
627-
watchSnapshot?: SpecSnapshotVersion
628-
): Promise<void> {
615+
private doWatchEntity(watchEntity: SpecWatchEntity): Promise<void> {
629616
if (watchEntity.docs) {
630617
assert(
631618
!watchEntity.doc,
632619
'Exactly one of `doc` or `docs` needs to be set'
633620
);
634-
let count = 0;
635621
return sequence(watchEntity.docs, (specDocument: SpecDocument) => {
636-
count++;
637-
const isLast = count === watchEntity.docs!.length;
638-
return this.doWatchEntity(
639-
{
640-
doc: specDocument,
641-
targets: watchEntity.targets,
642-
removedTargets: watchEntity.removedTargets
643-
},
644-
isLast ? watchSnapshot : undefined
645-
);
622+
return this.doWatchEntity({
623+
doc: specDocument,
624+
targets: watchEntity.targets,
625+
removedTargets: watchEntity.removedTargets
626+
});
646627
});
647628
} else if (watchEntity.doc) {
648629
const [key, version, data] = watchEntity.doc;
@@ -655,7 +636,7 @@ abstract class TestRunner {
655636
document.key,
656637
document
657638
);
658-
return this.doWatchEvent(change, watchSnapshot);
639+
return this.doWatchEvent(change);
659640
} else if (watchEntity.key) {
660641
const documentKey = key(watchEntity.key);
661642
const change = new DocumentWatchChange(
@@ -664,16 +645,13 @@ abstract class TestRunner {
664645
documentKey,
665646
null
666647
);
667-
return this.doWatchEvent(change, watchSnapshot);
648+
return this.doWatchEvent(change);
668649
} else {
669650
return fail('Either doc or docs must be set');
670651
}
671652
}
672653

673-
private doWatchFilter(
674-
watchFilter: SpecWatchFilter,
675-
watchSnapshot?: SpecSnapshotVersion
676-
): Promise<void> {
654+
private doWatchFilter(watchFilter: SpecWatchFilter): Promise<void> {
677655
const targetIds: TargetId[] = watchFilter[0];
678656
assert(
679657
targetIds.length === 1,
@@ -682,31 +660,31 @@ abstract class TestRunner {
682660
const keys = watchFilter.slice(1);
683661
const filter = new ExistenceFilter(keys.length);
684662
const change = new ExistenceFilterChange(targetIds[0], filter);
685-
return this.doWatchEvent(change, watchSnapshot);
663+
return this.doWatchEvent(change);
686664
}
687665

688-
private doWatchEvent(
689-
watchChange: WatchChange,
690-
watchSnapshot?: SpecSnapshotVersion
691-
): Promise<void> {
692-
let protoJSON = this.serializer.toTestWatchChange(watchChange);
693-
this.connection.watchStream!.callOnMessage(protoJSON);
694-
666+
private doWatchSnapshot(watchSnapshot: SpecWatchSnapshot): Promise<void> {
695667
// The client will only respond to watchSnapshots if they are on a target
696668
// change with an empty set of target IDs. So we should be sure to send a
697-
// separate event. TODO(klimt): We should make the spec tests behave more
698-
// like the backend. Alternatively, we could hook in to the system at a
699-
// level higher than the stream.
700-
const snapshot =
701-
watchSnapshot !== undefined ? version(watchSnapshot) : undefined;
702-
if (snapshot) {
703-
protoJSON = {
704-
targetChange: {
705-
readTime: this.serializer.toVersion(snapshot)
706-
}
707-
};
708-
this.connection.watchStream!.callOnMessage(protoJSON);
709-
}
669+
// separate event.
670+
const protoJSON: api.ListenResponse = {
671+
targetChange: {
672+
readTime: this.serializer.toVersion(version(watchSnapshot.version)),
673+
resumeToken: watchSnapshot.resumeToken,
674+
targetIds: watchSnapshot.targetIds
675+
}
676+
};
677+
this.connection.watchStream!.callOnMessage(protoJSON);
678+
679+
// Put a no-op in the queue so that we know when any outstanding RemoteStore
680+
// writes on the network are complete.
681+
return this.queue.enqueue(async () => {});
682+
}
683+
684+
private async doWatchEvent(watchChange: WatchChange): Promise<void> {
685+
const protoJSON = this.serializer.toTestWatchChange(watchChange);
686+
this.connection.watchStream!.callOnMessage(protoJSON);
687+
710688
// Put a no-op in the queue so that we know when any outstanding RemoteStore
711689
// writes on the network are complete.
712690
return this.queue.enqueue(async () => {});
@@ -1078,7 +1056,7 @@ export interface SpecConfig {
10781056
}
10791057

10801058
/**
1081-
* Union type for each step. The step consists of (mostly) exactly one `field`
1059+
* Union type for each step. The step consists of exactly one `field`
10821060
* set and optionally expected events in the `expect` field.
10831061
*/
10841062
export interface SpecStep {
@@ -1105,11 +1083,8 @@ export interface SpecStep {
11051083
watchEntity?: SpecWatchEntity;
11061084
/** Existence filter in the watch stream */
11071085
watchFilter?: SpecWatchFilter;
1108-
/**
1109-
* Optional snapshot version that can be additionally specified on any other
1110-
* watch event
1111-
*/
1112-
watchSnapshot?: SpecSnapshotVersion;
1086+
/** Snapshot ("NO_CHANGE") event in the watch stream. */
1087+
watchSnapshot?: SpecWatchSnapshot;
11131088
/** A step that the watch stream restarts. */
11141089
watchStreamClose?: SpecWatchStreamClose;
11151090

@@ -1183,7 +1158,11 @@ export type SpecWatchRemove = {
11831158
cause?: SpecError;
11841159
};
11851160

1186-
export type SpecSnapshotVersion = TestSnapshotVersion;
1161+
export type SpecWatchSnapshot = {
1162+
version: TestSnapshotVersion;
1163+
targetIds: TargetId[];
1164+
resumeToken?: string;
1165+
};
11871166

11881167
export type SpecWatchStreamClose = {
11891168
error: SpecError;
@@ -1192,7 +1171,7 @@ export type SpecWatchStreamClose = {
11921171

11931172
export type SpecWriteAck = {
11941173
/** The version the backend uses to ack the write. */
1195-
version: SpecSnapshotVersion;
1174+
version: TestSnapshotVersion;
11961175
/** Whether the ack is expected to generate a user callback. */
11971176
expectUserCallback: boolean;
11981177
};
@@ -1257,7 +1236,7 @@ export interface SpecQuery {
12571236
*/
12581237
export type SpecDocument = [
12591238
string,
1260-
SpecSnapshotVersion,
1239+
TestSnapshotVersion,
12611240
JsonObject<AnyJs> | null
12621241
];
12631242

0 commit comments

Comments
 (0)