diff --git a/Makefile_v1 b/Makefile_v1 index af193e3e8..1648687e2 100644 --- a/Makefile_v1 +++ b/Makefile_v1 @@ -11,30 +11,51 @@ GOOGLEAPIS_REPO = $(REPO_DIR)/googleapis TESTS_REPO = $(REPO_DIR)/conformance-tests TEST_PROTO_DIR = $(TESTS_REPO)/firestore/v1 TEST_PROTO_SRC = $(TEST_PROTO_DIR)/proto/google/cloud/conformance/firestore/v1/tests.proto +TESTDATA_DIR = `pwd`/tests/unit/v1/testdata/ TMPDIR = /tmp/python-fs-proto -TMPDIR_FS = $(TMPDIR)/google/cloud/firestore_v1/proto +TMPDIR_FS = $(TMPDIR)/google/cloud/firestore_v1/types TEST_PROTO_COPY = $(TMPDIR_FS)/tests.proto +TEST_GEN_OUT = tests/unit/v1/conformance_tests.py +OUTDIR = /tmp/python-fs-gen -.PHONY: sync-protos gen-protos +.PHONY: sync-protos gen-protos docker-pull -gen-protos: sync-protos tweak-protos - # TODO(jba): Put the generated proto somewhere more suitable. - $(PROTOC) --python_out=. \ - -I $(TMPDIR) \ - -I $(PROTOBUF_REPO)/src \ - -I $(GOOGLEAPIS_REPO) \ - $(TEST_PROTO_COPY) +gen-protos: sync-protos tweak-protos docker-pull gen-protos-raw + +gen-protos-raw: + mkdir -p $(OUTDIR) + docker run \ + --mount type=bind,source=$(TMPDIR),destination="/in",readonly \ + --mount type=bind,source=$(OUTDIR),destination="/out" \ + --rm \ + --user `id -u`:`id -g` \ + gcr.io/gapic-images/gapic-generator-python + cp $(OUTDIR)/google/cloud/firestore_v1/types/tests.py \ + $(TEST_GEN_OUT) + sed -i -e \ + "s@package='google.cloud.firestore_v1'@package='tests.unit.v1'@" \ + $(TEST_GEN_OUT) tweak-protos: mkdir -p $(TMPDIR_FS) cp $(GOOGLEAPIS_REPO)/google/firestore/v1/*.proto $(TMPDIR_FS) - sed -i -e 's@google/firestore/v1@google/cloud/firestore_v1/proto@' $(TMPDIR_FS)/*.proto + sed -i -e 's@google/firestore/v1@google/cloud/firestore_v1/types@' $(TMPDIR_FS)/*.proto + sed -i -e 's@package google\.firestore\.v1@package google.cloud.firestore_v1@' $(TMPDIR_FS)/*.proto cp $(TEST_PROTO_SRC) $(TEST_PROTO_COPY) - sed -i -e 's@package google.cloud.conformance.firestore.v1@package google.cloud.firestore_v1.proto@' $(TEST_PROTO_COPY) - sed -i -e 's@google/firestore/v1@google/cloud/firestore_v1/proto@' $(TEST_PROTO_COPY) + sed -i -e 's@package google\.cloud\.conformance\.firestore\.v1@package google.cloud.firestore_v1@' $(TEST_PROTO_COPY) + sed -i -e 's@google/firestore/v1@google/cloud/firestore_v1/types@' $(TEST_PROTO_COPY) + sed -i -e 's@google\.firestore\.v1@google.cloud.firestore_v1@' $(TEST_PROTO_COPY) + sed -i -e 's@Cursor@Cursor_@' $(TEST_PROTO_COPY) sync-protos: cd $(PROTOBUF_REPO); git pull cd $(GOOGLEAPIS_REPO); git pull cd $(TESTS_REPO); git pull + +docker-pull: + docker pull gcr.io/gapic-images/gapic-generator-python:latest + +copy-testdata: + rm $(TESTDATA_DIR)/*.json + cp $(TEST_PROTO_DIR)/*.json $(TESTDATA_DIR)/ diff --git a/Makefile_v1beta1 b/Makefile_v1beta1 deleted file mode 100644 index 69cf87f41..000000000 --- a/Makefile_v1beta1 +++ /dev/null @@ -1,37 +0,0 @@ -# This makefile builds the protos needed for cross-language Firestore tests. - -# Assume protoc is on the path. The proto compiler must be one that -# supports proto3 syntax. -PROTOC = protoc - -# Dependent repos. -REPO_DIR = $(HOME)/git-repos -PROTOBUF_REPO = $(REPO_DIR)/protobuf -GOOGLEAPIS_REPO = $(REPO_DIR)/googleapis -TESTS_REPO = $(REPO_DIR)/gcp/google-cloud-common - -TMPDIR = /tmp/python-fs-proto -TMPDIR_FS = $(TMPDIR)/google/cloud/firestore_v1beta1/proto - -.PHONY: sync-protos gen-protos - -gen-protos: sync-protos tweak-protos - # TODO(jba): Put the generated proto somewhere more suitable. - $(PROTOC) --python_out=google/cloud/firestore_v1beta1/proto \ - -I $(TMPDIR) \ - -I $(PROTOBUF_REPO)/src \ - -I $(GOOGLEAPIS_REPO) \ - $(TMPDIR)/test_v1beta1.proto - -tweak-protos: - mkdir -p $(TMPDIR_FS) - cp $(GOOGLEAPIS_REPO)/google/firestore/v1beta1/*.proto $(TMPDIR_FS) - sed -i -e 's@google/firestore/v1beta1@google/cloud/firestore_v1beta1/proto@' $(TMPDIR_FS)/*.proto - cp $(TESTS_REPO)/testing/firestore/proto/test_v1beta1.proto $(TMPDIR) - sed -i -e 's@package tests@package tests.v1beta1@' $(TMPDIR)/test_v1beta1.proto - sed -i -e 's@google/firestore/v1beta1@google/cloud/firestore_v1beta1/proto@' $(TMPDIR)/test_v1beta1.proto - -sync-protos: - cd $(PROTOBUF_REPO); git pull - cd $(GOOGLEAPIS_REPO); git pull - #cd $(TESTS_REPO); git pull diff --git a/tests/unit/v1/conformance_tests.py b/tests/unit/v1/conformance_tests.py new file mode 100644 index 000000000..0718f8e5f --- /dev/null +++ b/tests/unit/v1/conformance_tests.py @@ -0,0 +1,531 @@ +# -*- coding: utf-8 -*- + +# Copyright 2020 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. +# + +import proto # type: ignore + + +from google.cloud.firestore_v1.types import common +from google.cloud.firestore_v1.types import document +from google.cloud.firestore_v1.types import firestore +from google.cloud.firestore_v1.types import query as gcf_query +from google.protobuf import timestamp_pb2 as timestamp # type: ignore + + +__protobuf__ = proto.module( + package="tests.unit.v1", + manifest={ + "TestFile", + "Test", + "GetTest", + "CreateTest", + "SetTest", + "UpdateTest", + "UpdatePathsTest", + "DeleteTest", + "SetOption", + "QueryTest", + "Clause", + "Select", + "Where", + "OrderBy", + "Cursor_", + "DocSnapshot", + "FieldPath", + "ListenTest", + "Snapshot", + "DocChange", + }, +) + + +class TestFile(proto.Message): + r"""A collection of tests. + + Attributes: + tests (Sequence[~.gcf_tests.Test]): + + """ + + tests = proto.RepeatedField(proto.MESSAGE, number=1, message="Test",) + + +class Test(proto.Message): + r"""A Test describes a single client method call and its expected + result. + + Attributes: + description (str): + short description of the test + comment (str): + a comment describing the behavior being + tested + get (~.gcf_tests.GetTest): + + create (~.gcf_tests.CreateTest): + + set_ (~.gcf_tests.SetTest): + + update (~.gcf_tests.UpdateTest): + + update_paths (~.gcf_tests.UpdatePathsTest): + + delete (~.gcf_tests.DeleteTest): + + query (~.gcf_tests.QueryTest): + + listen (~.gcf_tests.ListenTest): + + """ + + description = proto.Field(proto.STRING, number=1) + + comment = proto.Field(proto.STRING, number=10) + + get = proto.Field(proto.MESSAGE, number=2, oneof="test", message="GetTest",) + + create = proto.Field(proto.MESSAGE, number=3, oneof="test", message="CreateTest",) + + set_ = proto.Field(proto.MESSAGE, number=4, oneof="test", message="SetTest",) + + update = proto.Field(proto.MESSAGE, number=5, oneof="test", message="UpdateTest",) + + update_paths = proto.Field( + proto.MESSAGE, number=6, oneof="test", message="UpdatePathsTest", + ) + + delete = proto.Field(proto.MESSAGE, number=7, oneof="test", message="DeleteTest",) + + query = proto.Field(proto.MESSAGE, number=8, oneof="test", message="QueryTest",) + + listen = proto.Field(proto.MESSAGE, number=9, oneof="test", message="ListenTest",) + + +class GetTest(proto.Message): + r"""Call to the DocumentRef.Get method. + + Attributes: + doc_ref_path (str): + The path of the doc, e.g. + "projects/projectID/databases/(default)/documents/C/d". + request (~.firestore.GetDocumentRequest): + The request that the call should send to the + Firestore service. + """ + + doc_ref_path = proto.Field(proto.STRING, number=1) + + request = proto.Field( + proto.MESSAGE, number=2, message=firestore.GetDocumentRequest, + ) + + +class CreateTest(proto.Message): + r"""Call to DocumentRef.Create. + + Attributes: + doc_ref_path (str): + The path of the doc, e.g. + "projects/projectID/databases/(default)/documents/C/d". + json_data (str): + The data passed to Create, as JSON. The + strings "Delete" and "ServerTimestamp" denote + the two special sentinel values. Values that + could be interpreted as integers (i.e. digit + strings) should be treated as integers. + request (~.firestore.CommitRequest): + The request that the call should generate. + is_error (bool): + If true, the call should result in an error + without generating a request. If this is true, + request should not be set. + """ + + doc_ref_path = proto.Field(proto.STRING, number=1) + + json_data = proto.Field(proto.STRING, number=2) + + request = proto.Field(proto.MESSAGE, number=3, message=firestore.CommitRequest,) + + is_error = proto.Field(proto.BOOL, number=4) + + +class SetTest(proto.Message): + r"""A call to DocumentRef.Set. + + Attributes: + doc_ref_path (str): + path of doc + option (~.gcf_tests.SetOption): + option to the Set call, if any + json_data (str): + data (see CreateTest.json_data) + request (~.firestore.CommitRequest): + expected request + is_error (bool): + call signals an error + """ + + doc_ref_path = proto.Field(proto.STRING, number=1) + + option = proto.Field(proto.MESSAGE, number=2, message="SetOption",) + + json_data = proto.Field(proto.STRING, number=3) + + request = proto.Field(proto.MESSAGE, number=4, message=firestore.CommitRequest,) + + is_error = proto.Field(proto.BOOL, number=5) + + +class UpdateTest(proto.Message): + r"""A call to the form of DocumentRef.Update that represents the + data as a map or dictionary. + + Attributes: + doc_ref_path (str): + path of doc + precondition (~.common.Precondition): + precondition in call, if any + json_data (str): + data (see CreateTest.json_data) + request (~.firestore.CommitRequest): + expected request + is_error (bool): + call signals an error + """ + + doc_ref_path = proto.Field(proto.STRING, number=1) + + precondition = proto.Field(proto.MESSAGE, number=2, message=common.Precondition,) + + json_data = proto.Field(proto.STRING, number=3) + + request = proto.Field(proto.MESSAGE, number=4, message=firestore.CommitRequest,) + + is_error = proto.Field(proto.BOOL, number=5) + + +class UpdatePathsTest(proto.Message): + r"""A call to the form of DocumentRef.Update that represents the + data as a list of field paths and their values. + + Attributes: + doc_ref_path (str): + path of doc + precondition (~.common.Precondition): + precondition in call, if any + field_paths (Sequence[~.gcf_tests.FieldPath]): + parallel sequences: field_paths[i] corresponds to + json_values[i] + json_values (Sequence[str]): + the argument values, as JSON + request (~.firestore.CommitRequest): + expected rquest + is_error (bool): + call signals an error + """ + + doc_ref_path = proto.Field(proto.STRING, number=1) + + precondition = proto.Field(proto.MESSAGE, number=2, message=common.Precondition,) + + field_paths = proto.RepeatedField(proto.MESSAGE, number=3, message="FieldPath",) + + json_values = proto.RepeatedField(proto.STRING, number=4) + + request = proto.Field(proto.MESSAGE, number=5, message=firestore.CommitRequest,) + + is_error = proto.Field(proto.BOOL, number=6) + + +class DeleteTest(proto.Message): + r"""A call to DocmentRef.Delete + + Attributes: + doc_ref_path (str): + path of doc + precondition (~.common.Precondition): + + request (~.firestore.CommitRequest): + expected rquest + is_error (bool): + call signals an error + """ + + doc_ref_path = proto.Field(proto.STRING, number=1) + + precondition = proto.Field(proto.MESSAGE, number=2, message=common.Precondition,) + + request = proto.Field(proto.MESSAGE, number=3, message=firestore.CommitRequest,) + + is_error = proto.Field(proto.BOOL, number=4) + + +class SetOption(proto.Message): + r"""An option to the DocumentRef.Set call. + + Attributes: + all_ (bool): + if true, merge all fields ("fields" is + ignored). + fields (Sequence[~.gcf_tests.FieldPath]): + field paths for a Merge option + """ + + all_ = proto.Field(proto.BOOL, number=1) + + fields = proto.RepeatedField(proto.MESSAGE, number=2, message="FieldPath",) + + +class QueryTest(proto.Message): + r""" + + Attributes: + coll_path (str): + path of collection, e.g. + "projects/projectID/databases/(default)/documents/C". + clauses (Sequence[~.gcf_tests.Clause]): + + query (~.gcf_query.StructuredQuery): + + is_error (bool): + + """ + + coll_path = proto.Field(proto.STRING, number=1) + + clauses = proto.RepeatedField(proto.MESSAGE, number=2, message="Clause",) + + query = proto.Field(proto.MESSAGE, number=3, message=gcf_query.StructuredQuery,) + + is_error = proto.Field(proto.BOOL, number=4) + + +class Clause(proto.Message): + r""" + + Attributes: + select (~.gcf_tests.Select): + + where (~.gcf_tests.Where): + + order_by (~.gcf_tests.OrderBy): + + offset (int): + + limit (int): + + start_at (~.gcf_tests.Cursor_): + + start_after (~.gcf_tests.Cursor_): + + end_at (~.gcf_tests.Cursor_): + + end_before (~.gcf_tests.Cursor_): + + """ + + select = proto.Field(proto.MESSAGE, number=1, oneof="clause", message="Select",) + + where = proto.Field(proto.MESSAGE, number=2, oneof="clause", message="Where",) + + order_by = proto.Field(proto.MESSAGE, number=3, oneof="clause", message="OrderBy",) + + offset = proto.Field(proto.INT32, number=4, oneof="clause") + + limit = proto.Field(proto.INT32, number=5, oneof="clause") + + start_at = proto.Field(proto.MESSAGE, number=6, oneof="clause", message="Cursor_",) + + start_after = proto.Field( + proto.MESSAGE, number=7, oneof="clause", message="Cursor_", + ) + + end_at = proto.Field(proto.MESSAGE, number=8, oneof="clause", message="Cursor_",) + + end_before = proto.Field( + proto.MESSAGE, number=9, oneof="clause", message="Cursor_", + ) + + +class Select(proto.Message): + r""" + + Attributes: + fields (Sequence[~.gcf_tests.FieldPath]): + + """ + + fields = proto.RepeatedField(proto.MESSAGE, number=1, message="FieldPath",) + + +class Where(proto.Message): + r""" + + Attributes: + path (~.gcf_tests.FieldPath): + + op (str): + + json_value (str): + + """ + + path = proto.Field(proto.MESSAGE, number=1, message="FieldPath",) + + op = proto.Field(proto.STRING, number=2) + + json_value = proto.Field(proto.STRING, number=3) + + +class OrderBy(proto.Message): + r""" + + Attributes: + path (~.gcf_tests.FieldPath): + + direction (str): + "asc" or "desc". + """ + + path = proto.Field(proto.MESSAGE, number=1, message="FieldPath",) + + direction = proto.Field(proto.STRING, number=2) + + +class Cursor_(proto.Message): + r""" + + Attributes: + doc_snapshot (~.gcf_tests.DocSnapshot): + one of: + json_values (Sequence[str]): + + """ + + doc_snapshot = proto.Field(proto.MESSAGE, number=1, message="DocSnapshot",) + + json_values = proto.RepeatedField(proto.STRING, number=2) + + +class DocSnapshot(proto.Message): + r""" + + Attributes: + path (str): + + json_data (str): + + """ + + path = proto.Field(proto.STRING, number=1) + + json_data = proto.Field(proto.STRING, number=2) + + +class FieldPath(proto.Message): + r""" + + Attributes: + field (Sequence[str]): + + """ + + field = proto.RepeatedField(proto.STRING, number=1) + + +class ListenTest(proto.Message): + r"""A test of the Listen streaming RPC (a.k.a. FireStore watch). If the + sequence of responses is provided to the implementation, it should + produce the sequence of snapshots. If is_error is true, an error + should occur after the snapshots. + + The tests assume that the query is + Collection("projects/projectID/databases/(default)/documents/C").OrderBy("a", + Ascending) + + The watch target ID used in these tests is 1. Test interpreters + should either change their client's ID for testing, or change the ID + in the tests before running them. + + Attributes: + responses (Sequence[~.firestore.ListenResponse]): + + snapshots (Sequence[~.gcf_tests.Snapshot]): + + is_error (bool): + + """ + + responses = proto.RepeatedField( + proto.MESSAGE, number=1, message=firestore.ListenResponse, + ) + + snapshots = proto.RepeatedField(proto.MESSAGE, number=2, message="Snapshot",) + + is_error = proto.Field(proto.BOOL, number=3) + + +class Snapshot(proto.Message): + r""" + + Attributes: + docs (Sequence[~.document.Document]): + + changes (Sequence[~.gcf_tests.DocChange]): + + read_time (~.timestamp.Timestamp): + + """ + + docs = proto.RepeatedField(proto.MESSAGE, number=1, message=document.Document,) + + changes = proto.RepeatedField(proto.MESSAGE, number=2, message="DocChange",) + + read_time = proto.Field(proto.MESSAGE, number=3, message=timestamp.Timestamp,) + + +class DocChange(proto.Message): + r""" + + Attributes: + kind (~.gcf_tests.DocChange.Kind): + + doc (~.document.Document): + + old_index (int): + + new_index (int): + + """ + + class Kind(proto.Enum): + r"""""" + KIND_UNSPECIFIED = 0 + ADDED = 1 + REMOVED = 2 + MODIFIED = 3 + + kind = proto.Field(proto.ENUM, number=1, enum=Kind,) + + doc = proto.Field(proto.MESSAGE, number=2, message=document.Document,) + + old_index = proto.Field(proto.INT32, number=3) + + new_index = proto.Field(proto.INT32, number=4) + + +__all__ = tuple(sorted(__protobuf__.manifest)) diff --git a/tests/unit/v1/_test_cross_language.py b/tests/unit/v1/test_cross_language.py similarity index 87% rename from tests/unit/v1/_test_cross_language.py rename to tests/unit/v1/test_cross_language.py index 10fece5eb..49bc11506 100644 --- a/tests/unit/v1/_test_cross_language.py +++ b/tests/unit/v1/test_cross_language.py @@ -12,8 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -# TODO(microgen): currently cross language tests don't run as part of test pass -# This should be updated (and its makefile) to generate like other proto classes import functools import glob import json @@ -22,19 +20,21 @@ import mock import pytest -from google.protobuf import json_format from google.cloud.firestore_v1.types import document from google.cloud.firestore_v1.types import firestore -from google.cloud.firestore_v1.proto import tests_pb2 from google.cloud.firestore_v1.types import write +from tests.unit.v1 import conformance_tests + def _load_test_json(filename): - with open(filename, "r") as tp_file: - tp_json = json.load(tp_file) - test_file = tests_pb2.TestFile() - json_format.ParseDict(tp_json, test_file) shortname = os.path.split(filename)[-1] + + with open(filename, "r") as tp_file: + tp_json = tp_file.read() + + test_file = conformance_tests.TestFile.from_json(tp_json) + for test_proto in test_file.tests: test_proto.description = test_proto.description + " (%s)" % shortname yield test_proto @@ -48,51 +48,31 @@ def _load_test_json(filename): ALL_TESTPROTOS.extend(_load_test_json(filename)) _CREATE_TESTPROTOS = [ - test_proto - for test_proto in ALL_TESTPROTOS - if test_proto.WhichOneof("test") == "create" + test_proto for test_proto in ALL_TESTPROTOS if "create" in test_proto ] -_GET_TESTPROTOS = [ - test_proto - for test_proto in ALL_TESTPROTOS - if test_proto.WhichOneof("test") == "get" -] +_GET_TESTPROTOS = [test_proto for test_proto in ALL_TESTPROTOS if "get" in test_proto] -_SET_TESTPROTOS = [ - test_proto - for test_proto in ALL_TESTPROTOS - if test_proto.WhichOneof("test") == "set" -] +_SET_TESTPROTOS = [test_proto for test_proto in ALL_TESTPROTOS if "set_" in test_proto] _UPDATE_TESTPROTOS = [ - test_proto - for test_proto in ALL_TESTPROTOS - if test_proto.WhichOneof("test") == "update" + test_proto for test_proto in ALL_TESTPROTOS if "update" in test_proto ] _UPDATE_PATHS_TESTPROTOS = [ - test_proto - for test_proto in ALL_TESTPROTOS - if test_proto.WhichOneof("test") == "update_paths" + test_proto for test_proto in ALL_TESTPROTOS if "update_paths" in test_proto ] _DELETE_TESTPROTOS = [ - test_proto - for test_proto in ALL_TESTPROTOS - if test_proto.WhichOneof("test") == "delete" + test_proto for test_proto in ALL_TESTPROTOS if "delete" in test_proto ] _LISTEN_TESTPROTOS = [ - test_proto - for test_proto in ALL_TESTPROTOS - if test_proto.WhichOneof("test") == "listen" + test_proto for test_proto in ALL_TESTPROTOS if "listen" in test_proto ] _QUERY_TESTPROTOS = [ - test_proto - for test_proto in ALL_TESTPROTOS - if test_proto.WhichOneof("test") == "query" + test_proto for test_proto in ALL_TESTPROTOS if "query" in test_proto ] @@ -125,11 +105,19 @@ def _run_testcase(testcase, call, firestore_api, client): call() else: call() + + wrapped_writes = [ + write.Write.wrap(write_pb) for write_pb in testcase.request.writes + ] + + expected_request = { + "database": client._database_string, + "writes": wrapped_writes, + "transaction": None, + } + firestore_api.commit.assert_called_once_with( - client._database_string, - list(testcase.request.writes), - transaction=None, - metadata=client._rpc_metadata, + request=expected_request, metadata=client._rpc_metadata, ) @@ -153,18 +141,24 @@ def test_get_testprotos(test_proto): doc.get() # No '.textprotos' for errors, field_paths. + expected_request = { + "name": doc._document_path, + "mask": None, + "transaction": None, + } + firestore_api.get_document.assert_called_once_with( - doc._document_path, mask=None, transaction=None, metadata=client._rpc_metadata, + request=expected_request, metadata=client._rpc_metadata, ) @pytest.mark.parametrize("test_proto", _SET_TESTPROTOS) def test_set_testprotos(test_proto): - testcase = test_proto.set + testcase = test_proto.set_ firestore_api = _mock_firestore_api() client, doc = _make_client_document(firestore_api, testcase) data = convert_data(json.loads(testcase.json_data)) - if testcase.HasField("option"): + if "option" in testcase: merge = convert_set_option(testcase.option) else: merge = False @@ -178,7 +172,7 @@ def test_update_testprotos(test_proto): firestore_api = _mock_firestore_api() client, doc = _make_client_document(firestore_api, testcase) data = convert_data(json.loads(testcase.json_data)) - if testcase.HasField("precondition"): + if "precondition" in testcase: option = convert_precondition(testcase.precondition) else: option = None @@ -197,7 +191,7 @@ def test_delete_testprotos(test_proto): testcase = test_proto.delete firestore_api = _mock_firestore_api() client, doc = _make_client_document(firestore_api, testcase) - if testcase.HasField("precondition"): + if "precondition" in testcase: option = convert_precondition(testcase.precondition) else: option = None @@ -245,9 +239,12 @@ def callback(keys, applied_changes, read_time): db_str = "projects/projectID/databases/(default)" watch._firestore._database_string_internal = db_str + wrapped_responses = [ + firestore.ListenResponse.wrap(proto) for proto in testcase.responses + ] if testcase.is_error: try: - for proto in testcase.responses: + for proto in wrapped_responses: watch.on_snapshot(proto) except RuntimeError: # listen-target-add-wrong-id.textpro @@ -255,7 +252,7 @@ def callback(keys, applied_changes, read_time): pass else: - for proto in testcase.responses: + for proto in wrapped_responses: watch.on_snapshot(proto) assert len(snapshots) == len(testcase.snapshots) @@ -328,7 +325,7 @@ def convert_set_option(option): _helpers.FieldPath(*field.field).to_api_repr() for field in option.fields ] - assert option.all + assert option.all_ return True @@ -454,40 +451,39 @@ def parse_query(testcase): query = collection for clause in testcase.clauses: - kind = clause.WhichOneof("clause") - if kind == "select": + if "select" in clause: field_paths = [ ".".join(field_path.field) for field_path in clause.select.fields ] query = query.select(field_paths) - elif kind == "where": + elif "where" in clause: path = ".".join(clause.where.path.field) value = convert_data(json.loads(clause.where.json_value)) query = query.where(path, clause.where.op, value) - elif kind == "order_by": + elif "order_by" in clause: path = ".".join(clause.order_by.path.field) direction = clause.order_by.direction direction = _directions.get(direction, direction) query = query.order_by(path, direction=direction) - elif kind == "offset": + elif "offset" in clause: query = query.offset(clause.offset) - elif kind == "limit": + elif "limit" in clause: query = query.limit(clause.limit) - elif kind == "start_at": + elif "start_at" in clause: cursor = parse_cursor(clause.start_at, client) query = query.start_at(cursor) - elif kind == "start_after": + elif "start_after" in clause: cursor = parse_cursor(clause.start_after, client) query = query.start_after(cursor) - elif kind == "end_at": + elif "end_at" in clause: cursor = parse_cursor(clause.end_at, client) query = query.end_at(cursor) - elif kind == "end_before": + elif "end_before" in clause: cursor = parse_cursor(clause.end_before, client) query = query.end_before(cursor) else: # pragma: NO COVER - raise ValueError("Unknown query clause: {}".format(kind)) + raise ValueError("Unknown query clause: {}".format(clause)) return query @@ -501,7 +497,7 @@ def parse_cursor(cursor, client): from google.cloud.firestore_v1 import DocumentReference from google.cloud.firestore_v1 import DocumentSnapshot - if cursor.HasField("doc_snapshot"): + if "doc_snapshot" in cursor: path = parse_path(cursor.doc_snapshot.path) doc_ref = DocumentReference(*path, client=client)