From cc19d2c3c8bd2e0a17d7c401029b17d9a5941f18 Mon Sep 17 00:00:00 2001 From: lukasmittag Date: Wed, 25 Sep 2024 14:31:01 +0200 Subject: [PATCH] Finish fixing tests --- kuksa-client/kuksa_client/grpc/__init__.py | 60 +- kuksa-client/kuksa_client/grpc/aio.py | 17 +- kuksa-client/tests/conftest.py | 67 +- kuksa-client/tests/test_grpc.py | 1784 ++++++++++++++------ 4 files changed, 1390 insertions(+), 538 deletions(-) diff --git a/kuksa-client/kuksa_client/grpc/__init__.py b/kuksa-client/kuksa_client/grpc/__init__.py index 4e51cfb..d6cb5c0 100644 --- a/kuksa-client/kuksa_client/grpc/__init__.py +++ b/kuksa-client/kuksa_client/grpc/__init__.py @@ -700,8 +700,16 @@ def from_tuple(cls, path: str, dp: types_v2.Datapoint): field_descriptor, value = data[0] field_name = field_descriptor.name value = getattr(dp.value, field_name) + if dp.timestamp.seconds == 0 and dp.timestamp.nanos == 0: + timestamp = None + else: + timestamp = dp.timestamp.ToDatetime( + tzinfo=datetime.timezone.utc, + ) return cls( - entry=DataEntry(path=path, value=Datapoint(value)), + entry=DataEntry( + path=path, value=Datapoint(value=value, timestamp=timestamp) + ), fields=[Field(value=types_v1.FIELD_VALUE)], ) @@ -918,7 +926,6 @@ class VSSClient(BaseVSSClient): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.channel = None - self.channel2 = None self.exit_stack = contextlib.ExitStack() def __enter__(self): @@ -952,20 +959,16 @@ def connect(self, target_host=None): logger.info(f"Using TLS server name {self.tls_server_name}") options = [("grpc.ssl_target_name_override", self.tls_server_name)] channel = grpc.secure_channel(target_host, creds, options) - channel2 = grpc.secure_channel(target_host, creds, options) else: logger.debug("Not providing explicit TLS server name") channel = grpc.secure_channel(target_host, creds) - channel2 = grpc.secure_channel(target_host, creds) else: logger.info("Establishing insecure channel") channel = grpc.insecure_channel(target_host) - channel2 = grpc.insecure_channel(target_host) self.channel = self.exit_stack.enter_context(channel) - self.channel2 = self.exit_stack.enter_context(channel2) self.client_stub_v1 = val_grpc_v1.VALStub(self.channel) - self.client_stub_v2 = val_grpc_v2.VALStub(self.channel2) + self.client_stub_v2 = val_grpc_v2.VALStub(self.channel) self.connected = True if self.ensure_startup_connection: logger.debug("Connected to server: %s", self.get_server_info()) @@ -975,7 +978,6 @@ def disconnect(self): self.client_stub_v1 = None self.client_stub_v2 = None self.channel = None - self.channel2 = None self.connected = False @check_connected @@ -1144,6 +1146,7 @@ def subscribe_current_values( SubscribeEntry(path, View.CURRENT_VALUE, (Field.VALUE,)) for path in paths ), + v1=False, **rpc_kwargs, ): yield {update.entry.path: update.entry.value for update in updates} @@ -1249,6 +1252,15 @@ def set( self._process_set_response(resp) else: logger.info("Using v2") + if len(updates) == 0: + raise VSSClientError( + error={ + "code": grpc.StatusCode.INVALID_ARGUMENT.value[0], + "reason": grpc.StatusCode.INVALID_ARGUMENT.value[1], + "message": "No datapoints requested", + }, + errors=[], + ) for update in updates: req = self._prepare_publish_value_request( update, paths_with_required_type @@ -1260,7 +1272,7 @@ def set( @check_connected def subscribe( - self, entries: Iterable[SubscribeEntry], **rpc_kwargs + self, entries: Iterable[SubscribeEntry], v1: bool = True, **rpc_kwargs ) -> Iterator[List[EntryUpdate]]: """ Parameters: @@ -1271,14 +1283,28 @@ def subscribe( rpc_kwargs["metadata"] = self.generate_metadata_header( rpc_kwargs.get("metadata") ) - req = self._prepare_subscribe_request(entries) - resp_stream = self.client_stub_v1.Subscribe(req, **rpc_kwargs) - try: - for resp in resp_stream: - logger.debug("%s: %s", type(resp).__name__, resp) - yield [EntryUpdate.from_message(update) for update in resp.updates] - except RpcError as exc: - raise VSSClientError.from_grpc_error(exc) from exc + if v1: + req = self._prepare_subscribe_request(entries) + resp_stream = self.client_stub_v1.Subscribe(req, **rpc_kwargs) + try: + for resp in resp_stream: + logger.debug("%s: %s", type(resp).__name__, resp) + yield [EntryUpdate.from_message(update) for update in resp.updates] + except RpcError as exc: + raise VSSClientError.from_grpc_error(exc) from exc + else: + logger.info("Using v2") + req = self._prepare_subscribev2_request(entries) + resp_stream = self.client_stub_v2.Subscribe(req, **rpc_kwargs) + try: + for resp in resp_stream: + logger.debug("%s: %s", type(resp).__name__, resp) + yield [ + EntryUpdate.from_tuple(path, dp) + for path, dp in resp.entries.items() + ] + except RpcError as exc: + raise VSSClientError.from_grpc_error(exc) from exc @check_connected def authorize(self, token: str, **rpc_kwargs) -> str: diff --git a/kuksa-client/kuksa_client/grpc/aio.py b/kuksa-client/kuksa_client/grpc/aio.py index 2ba3532..b26f10f 100644 --- a/kuksa-client/kuksa_client/grpc/aio.py +++ b/kuksa-client/kuksa_client/grpc/aio.py @@ -56,7 +56,6 @@ class VSSClient(BaseVSSClient): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.channel = None - self.channel2 = None self.exit_stack = contextlib.AsyncExitStack() async def __aenter__(self): @@ -76,20 +75,16 @@ async def connect(self, target_host=None): logger.info(f"Using TLS server name {self.tls_server_name}") options = [("grpc.ssl_target_name_override", self.tls_server_name)] channel = grpc.aio.secure_channel(target_host, creds, options) - channel2 = grpc.aio.secure_channel(target_host, creds, options) else: logger.debug("Not providing explicit TLS server name") channel = grpc.aio.secure_channel(target_host, creds) - channel2 = grpc.aio.secure_channel(target_host, creds) else: logger.info("Establishing insecure channel") channel = grpc.aio.insecure_channel(target_host) - channel2 = grpc.aio.insecure_channel(target_host) self.channel = await self.exit_stack.enter_async_context(channel) - self.channel2 = await self.exit_stack.enter_async_context(channel2) self.client_stub_v1 = val_grpc_v1.VALStub(self.channel) - self.client_stub_v2 = val_grpc_v2.VALStub(self.channel2) + self.client_stub_v2 = val_grpc_v2.VALStub(self.channel) self.connected = True if self.ensure_startup_connection: logger.debug("Connected to server: %s", await self.get_server_info()) @@ -99,7 +94,6 @@ async def disconnect(self): self.client_stub_v1 = None self.client_stub_v2 = None self.channel = None - self.channel2 = None self.connected = False def check_connected_async(func): @@ -417,6 +411,15 @@ async def set( self._process_set_response(resp) else: logger.info("Using v2") + if len(updates) == 0: + raise VSSClientError( + error={ + "code": grpc.StatusCode.INVALID_ARGUMENT.value[0], + "reason": grpc.StatusCode.INVALID_ARGUMENT.value[1], + "message": "No datapoints requested", + }, + errors=[], + ) for update in updates: req = self._prepare_publish_value_request( update, paths_with_required_type diff --git a/kuksa-client/tests/conftest.py b/kuksa-client/tests/conftest.py index 16fa22a..9fe35a7 100644 --- a/kuksa-client/tests/conftest.py +++ b/kuksa-client/tests/conftest.py @@ -22,7 +22,8 @@ import pytest import pytest_asyncio -from kuksa.val.v1 import val_pb2_grpc +from kuksa.val.v1 import val_pb2_grpc as val_v1 +from kuksa.val.v2 import val_pb2_grpc as val_v2 import tests @@ -32,22 +33,32 @@ def resources_path_fixture(): return pathlib.Path(tests.__path__[0]) / 'resources' -@pytest.fixture(name='val_servicer', scope='function') -def val_servicer_fixture(mocker): - servicer = val_pb2_grpc.VALServicer() - mocker.patch.object(servicer, 'Get', spec=True) - mocker.patch.object(servicer, 'Set', spec=True) - mocker.patch.object(servicer, 'Subscribe', spec=True) - mocker.patch.object(servicer, 'GetServerInfo', spec=True) +@pytest.fixture(name="val_servicer_v1", scope="function") +def val_servicer_v1_fixture(mocker): + servicer_v1 = val_v1.VALServicer() + mocker.patch.object(servicer_v1, "Get", spec=True) + mocker.patch.object(servicer_v1, "Set", spec=True) + mocker.patch.object(servicer_v1, "Subscribe", spec=True) + mocker.patch.object(servicer_v1, "GetServerInfo", spec=True) - return servicer + return servicer_v1 -@pytest_asyncio.fixture(name='val_server', scope='function') -async def val_server_fixture(unused_tcp_port, val_servicer): +@pytest.fixture(name="val_servicer_v2", scope="function") +def val_servicer_v2_fixture(mocker): + servicer_v2 = val_v2.VALServicer() + mocker.patch.object(servicer_v2, "PublishValue", spec=True) + mocker.patch.object(servicer_v2, "Subscribe", spec=True) + + return servicer_v2 + + +@pytest_asyncio.fixture(name="val_server", scope="function") +async def val_server_fixture(unused_tcp_port, val_servicer_v1, val_servicer_v2): server = grpc.aio.server() - val_pb2_grpc.add_VALServicer_to_server(val_servicer, server) - server.add_insecure_port(f'127.0.0.1:{unused_tcp_port}') + val_v1.add_VALServicer_to_server(val_servicer_v1, server) + val_v2.add_VALServicer_to_server(val_servicer_v2, server) + server.add_insecure_port(f"127.0.0.1:{unused_tcp_port}") await server.start() try: yield server @@ -55,18 +66,26 @@ async def val_server_fixture(unused_tcp_port, val_servicer): await server.stop(grace=2.0) -@pytest_asyncio.fixture(name='secure_val_server', scope='function') -async def secure_val_server_fixture(unused_tcp_port, resources_path, val_servicer): +@pytest_asyncio.fixture(name="secure_val_server", scope="function") +async def secure_val_server_fixture( + unused_tcp_port, resources_path, val_servicer_v1, val_servicer_v2 +): server = grpc.aio.server() - val_pb2_grpc.add_VALServicer_to_server(val_servicer, server) - server.add_secure_port(f'localhost:{unused_tcp_port}', grpc.ssl_server_credentials( - private_key_certificate_chain_pairs=[( - (resources_path / 'test-server.key').read_bytes(), - (resources_path / 'test-server.pem').read_bytes(), - )], - root_certificates=(resources_path / 'test-ca.pem').read_bytes(), - require_client_auth=False, - )) + val_v1.add_VALServicer_to_server(val_servicer_v1, server) + val_v2.add_VALServicer_to_server(val_servicer_v2, server) + server.add_secure_port( + f"localhost:{unused_tcp_port}", + grpc.ssl_server_credentials( + private_key_certificate_chain_pairs=[ + ( + (resources_path / "test-server.key").read_bytes(), + (resources_path / "test-server.pem").read_bytes(), + ) + ], + root_certificates=(resources_path / "test-ca.pem").read_bytes(), + require_client_auth=False, + ), + ) await server.start() try: yield server diff --git a/kuksa-client/tests/test_grpc.py b/kuksa-client/tests/test_grpc.py index 687801f..036c13f 100644 --- a/kuksa-client/tests/test_grpc.py +++ b/kuksa-client/tests/test_grpc.py @@ -26,8 +26,17 @@ import grpc.aio import pytest -from kuksa.val.v1 import val_pb2 -from kuksa.val.v1 import types_pb2 +from typing import Dict + +from kuksa.val.v1 import types_pb2 as types_v1 + +from kuksa.val.v1 import val_pb2 as val_v1 + +# from kuksa.val.v1 import val_pb2_grpc as val_grpc_v1 +from kuksa.val.v2 import types_pb2 as types_v2 +from kuksa.val.v2 import val_pb2 as val_v2 + +# from kuksa.val.v2 import val_pb2_grpc as val_grpc_v2 import kuksa_client.grpc from kuksa_client.grpc import Datapoint @@ -71,10 +80,10 @@ def test_from_grpc_error(self): assert client_error.errors == expected_client_error.errors def test_to_dict(self): - error = types_pb2.Error( - code=404, reason='not_found', message="Does.Not.Exist not found") - errors = (types_pb2.DataEntryError( - path='Does.Not.Exist', error=error),) + error = types_v1.Error( + code=404, reason="not_found", message="Does.Not.Exist not found" + ) + errors = (types_v1.DataEntryError(path="Does.Not.Exist", error=error),) error = json_format.MessageToDict( error, preserving_proto_field_name=True) errors = [json_format.MessageToDict( @@ -90,12 +99,14 @@ def test_to_dict(self): class TestMetadata: def test_to_message_empty(self): - assert Metadata().to_message() == types_pb2.Metadata() + assert Metadata().to_message() == types_v1.Metadata() def test_to_message_value_restriction_without_value_type(self): with pytest.raises(ValueError) as exc_info: - assert Metadata(value_restriction=ValueRestriction() - ).to_message() == types_pb2.Metadata() + assert ( + Metadata(value_restriction=ValueRestriction()).to_message() + == types_v1.Metadata() + ) assert exc_info.value.args == ( "Cannot set value_restriction from data type UNSPECIFIED",) @@ -130,13 +141,16 @@ def test_to_from_message_signed_value_restriction(self, value_type, min_value, m allowed_values = None if (min_value, max_value, allowed_values) == (None, None, None): - expected_message = types_pb2.Metadata() + expected_message = types_v1.Metadata() output_metadata = Metadata() else: - expected_message = types_pb2.Metadata(value_restriction=types_pb2.ValueRestriction( - signed=types_pb2.ValueRestrictionInt( - min=min_value, max=max_value, allowed_values=allowed_values), - )) + expected_message = types_v1.Metadata( + value_restriction=types_v1.ValueRestriction( + signed=types_v1.ValueRestrictionInt( + min=min_value, max=max_value, allowed_values=allowed_values + ), + ) + ) output_metadata = Metadata(value_restriction=ValueRestriction( min=min_value, max=max_value, allowed_values=allowed_values, )) @@ -173,13 +187,16 @@ def test_to_from_message_unsigned_value_restriction(self, value_type, min_value, allowed_values = None if (min_value, max_value, allowed_values) == (None, None, None): - expected_message = types_pb2.Metadata() + expected_message = types_v1.Metadata() output_metadata = Metadata() else: - expected_message = types_pb2.Metadata(value_restriction=types_pb2.ValueRestriction( - unsigned=types_pb2.ValueRestrictionUint( - min=min_value, max=max_value, allowed_values=allowed_values), - )) + expected_message = types_v1.Metadata( + value_restriction=types_v1.ValueRestriction( + unsigned=types_v1.ValueRestrictionUint( + min=min_value, max=max_value, allowed_values=allowed_values + ), + ) + ) output_metadata = Metadata(value_restriction=ValueRestriction( min=min_value, max=max_value, allowed_values=allowed_values, )) @@ -214,13 +231,16 @@ def test_to_from_message_float_value_restriction(self, value_type, min_value, ma allowed_values = None if (min_value, max_value, allowed_values) == (None, None, None): - expected_message = types_pb2.Metadata() + expected_message = types_v1.Metadata() output_metadata = Metadata() else: - expected_message = types_pb2.Metadata(value_restriction=types_pb2.ValueRestriction( - floating_point=types_pb2.ValueRestrictionFloat( - min=min_value, max=max_value, allowed_values=allowed_values), - )) + expected_message = types_v1.Metadata( + value_restriction=types_v1.ValueRestriction( + floating_point=types_v1.ValueRestrictionFloat( + min=min_value, max=max_value, allowed_values=allowed_values + ), + ) + ) output_metadata = Metadata(value_restriction=ValueRestriction( min=min_value, max=max_value, allowed_values=allowed_values, )) @@ -241,13 +261,16 @@ def test_to_from_message_string_value_restriction(self, value_type, allowed_valu allowed_values = None if allowed_values is None: - expected_message = types_pb2.Metadata() + expected_message = types_v1.Metadata() output_metadata = Metadata() else: - expected_message = types_pb2.Metadata(value_restriction=types_pb2.ValueRestriction( - string=types_pb2.ValueRestrictionString( - allowed_values=allowed_values), - )) + expected_message = types_v1.Metadata( + value_restriction=types_v1.ValueRestriction( + string=types_v1.ValueRestrictionString( + allowed_values=allowed_values + ), + ) + ) output_metadata = Metadata(value_restriction=ValueRestriction( allowed_values=allowed_values, )) @@ -260,7 +283,7 @@ def test_metadata_from_message_value_restriction_no_type(self): This intends to cover the case when the proto message has a value restriction, but no contents (type not specified) """ - input_message = types_pb2.Metadata(value_restriction=types_pb2.ValueRestriction()) + input_message = types_v1.Metadata(value_restriction=types_v1.ValueRestriction()) expected_metadata = Metadata() assert Metadata.from_message(input_message) == expected_metadata @@ -336,25 +359,78 @@ def test_to_dict(self, init_kwargs, metadata_dict): class TestDatapoint: - @pytest.mark.parametrize('value_type, init_args, message', [ - (DataType.BOOLEAN, (None,), types_pb2.Datapoint()), - (DataType.BOOLEAN, ('False',), types_pb2.Datapoint(bool=False)), - (DataType.BOOLEAN, ('false',), types_pb2.Datapoint(bool=False)), - (DataType.BOOLEAN, ('F',), types_pb2.Datapoint(bool=False)), - (DataType.BOOLEAN, ('f',), types_pb2.Datapoint(bool=False)), - (DataType.BOOLEAN, (True, datetime.datetime(2022, 11, 16, tzinfo=datetime.timezone.utc)), types_pb2.Datapoint( - bool=True, timestamp=timestamp_pb2.Timestamp(seconds=1668556800), - )), - (DataType.INT8_ARRAY, ('[-128, 127]',), types_pb2.Datapoint( - int32_array=types_pb2.Int32Array(values=[-128, 127]))), - ]) - def test_to_message(self, value_type, init_args, message): - assert Datapoint(*init_args).to_message(value_type) == message + + @pytest.mark.parametrize( + "value_type, init_args, message_v1, message_v2", + [ + (DataType.BOOLEAN, (None,), types_v1.Datapoint(), types_v2.Datapoint()), + ( + DataType.BOOLEAN, + ("False",), + types_v1.Datapoint(bool=False), + types_v2.Datapoint(value=types_v2.Value(bool=False)), + ), + ( + DataType.BOOLEAN, + ("false",), + types_v1.Datapoint(bool=False), + types_v2.Datapoint(value=types_v2.Value(bool=False)), + ), + ( + DataType.BOOLEAN, + ("F",), + types_v1.Datapoint(bool=False), + types_v2.Datapoint(value=types_v2.Value(bool=False)), + ), + ( + DataType.BOOLEAN, + ("f",), + types_v1.Datapoint(bool=False), + types_v2.Datapoint(value=types_v2.Value(bool=False)), + ), + ( + DataType.BOOLEAN, + (True, datetime.datetime(2022, 11, 16, tzinfo=datetime.timezone.utc)), + types_v1.Datapoint( + bool=True, + timestamp=timestamp_pb2.Timestamp(seconds=1668556800), + ), + types_v2.Datapoint( + value=types_v2.Value(bool=True), + timestamp=timestamp_pb2.Timestamp(seconds=1668556800), + ), + ), + ( + DataType.INT8_ARRAY, + ("[-128, 127]",), + types_v1.Datapoint(int32_array=types_v1.Int32Array(values=[-128, 127])), + types_v2.Datapoint( + value=types_v2.Value( + int32_array=types_v2.Int32Array(values=[-128, 127]) + ), + ), + ), + ], + ) + def test_to_message(self, value_type, init_args, message_v1, message_v2): + assert Datapoint(*init_args).v1_to_message(value_type) == message_v1 + assert Datapoint(*init_args).v2_to_message(value_type) == message_v2 @pytest.mark.parametrize('value_type', [DataType.UNSPECIFIED, DataType.TIMESTAMP, DataType.TIMESTAMP_ARRAY]) - def test_to_message_unsupported_value_type(self, value_type): + def test_v1_to_message_unsupported_value_type(self, value_type): + with pytest.raises(ValueError) as exc_info: + Datapoint(42).v1_to_message(value_type) + assert exc_info.value.args[0].startswith( + "Cannot determine which field to set with data type" + ) + + @pytest.mark.parametrize( + "value_type", + [DataType.UNSPECIFIED, DataType.TIMESTAMP, DataType.TIMESTAMP_ARRAY], + ) + def test_v2_to_message_unsupported_value_type(self, value_type): with pytest.raises(ValueError) as exc_info: - Datapoint(42).to_message(value_type) + Datapoint(42).v2_to_message(value_type) assert exc_info.value.args[0].startswith( 'Cannot determine which field to set with data type') @@ -400,15 +476,19 @@ def test_to_dict(self, entry, fields, update_dict): @pytest.mark.asyncio class TestVSSClient: - @pytest.mark.usefixtures('secure_val_server') - async def test_secure_connection(self, unused_tcp_port, resources_path, val_servicer): - val_servicer.GetServerInfo.return_value = val_pb2.GetServerInfoResponse( - name='test_server', version='1.2.3') + + @pytest.mark.usefixtures("secure_val_server") + async def test_secure_connection( + self, unused_tcp_port, resources_path, val_servicer_v1 + ): + val_servicer_v1.GetServerInfo.return_value = val_v1.GetServerInfoResponse( + name="test_server", version="1.2.3" + ) async with VSSClient('localhost', unused_tcp_port, root_certificates=resources_path / 'test-ca.pem', ensure_startup_connection=True ): - assert val_servicer.GetServerInfo.call_count == 1 + assert val_servicer_v1.GetServerInfo.call_count == 1 async def test_get_current_values(self, mocker, unused_tcp_port): client = VSSClient('127.0.0.1', unused_tcp_port) @@ -665,64 +745,85 @@ async def subscribe_response_stream(**kwargs): 'Vehicle.Chassis.Height': Metadata(entry_type=EntryType.ATTRIBUTE), } - @pytest.mark.usefixtures('val_server') - async def test_get_some_entries(self, unused_tcp_port, val_servicer): - val_servicer.Get.return_value = val_pb2.GetResponse(entries=[ - types_pb2.DataEntry( - path='Vehicle.Speed', - value=types_pb2.Datapoint( - timestamp=timestamp_pb2.Timestamp( - seconds=1667837915, nanos=247307674), - float=42.0, + @pytest.mark.usefixtures("val_server") + async def test_get_some_entries(self, unused_tcp_port, val_servicer_v1): + val_servicer_v1.Get.return_value = val_v1.GetResponse( + entries=[ + types_v1.DataEntry( + path="Vehicle.Speed", + value=types_v1.Datapoint( + timestamp=timestamp_pb2.Timestamp( + seconds=1667837915, nanos=247307674 + ), + float=42.0, + ), ), - ), - types_pb2.DataEntry(path='Vehicle.ADAS.ABS.IsActive', - actuator_target=types_pb2.Datapoint(bool=True)), - types_pb2.DataEntry( - path='Vehicle.Chassis.Height', - metadata=types_pb2.Metadata( - data_type=types_pb2.DATA_TYPE_UINT16, - entry_type=types_pb2.ENTRY_TYPE_ATTRIBUTE, - description="Overall vehicle height, in mm.", - comment="No comment.", - deprecation="V2.1 moved to Vehicle.Height", - unit="mm", + types_v1.DataEntry( + path="Vehicle.ADAS.ABS.IsActive", + actuator_target=types_v1.Datapoint(bool=True), ), - ), - types_pb2.DataEntry( - path='Vehicle.Chassis.Height', metadata=types_pb2.Metadata(data_type=types_pb2.DATA_TYPE_UINT16), - ), - types_pb2.DataEntry( - path='Vehicle.Chassis.Height', metadata=types_pb2.Metadata(entry_type=types_pb2.ENTRY_TYPE_ATTRIBUTE), - ), - types_pb2.DataEntry( - path='Vehicle.Chassis.Height', - metadata=types_pb2.Metadata( - description="Overall vehicle height, in mm."), - ), - types_pb2.DataEntry( - path='Vehicle.Chassis.Height', metadata=types_pb2.Metadata(comment="No comment."), - ), - types_pb2.DataEntry( - path='Vehicle.Chassis.Height', metadata=types_pb2.Metadata(deprecation="V2.1 moved to Vehicle.Height"), - ), - types_pb2.DataEntry(path='Vehicle.Chassis.Height', - metadata=types_pb2.Metadata(unit="mm")), - types_pb2.DataEntry( - path='Vehicle.CurrentLocation.Heading', - metadata=types_pb2.Metadata(value_restriction=types_pb2.ValueRestriction( - floating_point=types_pb2.ValueRestrictionFloat( - min=0, max=360), - )), - ), - types_pb2.DataEntry( - path='Dummy.With.Allowed.Values', - metadata=types_pb2.Metadata(value_restriction=types_pb2.ValueRestriction( - signed=types_pb2.ValueRestrictionInt( - allowed_values=[12, 42, 666]), - )), - ), - ]) + types_v1.DataEntry( + path="Vehicle.Chassis.Height", + metadata=types_v1.Metadata( + data_type=types_v1.DATA_TYPE_UINT16, + entry_type=types_v1.ENTRY_TYPE_ATTRIBUTE, + description="Overall vehicle height, in mm.", + comment="No comment.", + deprecation="V2.1 moved to Vehicle.Height", + unit="mm", + ), + ), + types_v1.DataEntry( + path="Vehicle.Chassis.Height", + metadata=types_v1.Metadata(data_type=types_v1.DATA_TYPE_UINT16), + ), + types_v1.DataEntry( + path="Vehicle.Chassis.Height", + metadata=types_v1.Metadata( + entry_type=types_v1.ENTRY_TYPE_ATTRIBUTE + ), + ), + types_v1.DataEntry( + path="Vehicle.Chassis.Height", + metadata=types_v1.Metadata( + description="Overall vehicle height, in mm." + ), + ), + types_v1.DataEntry( + path="Vehicle.Chassis.Height", + metadata=types_v1.Metadata(comment="No comment."), + ), + types_v1.DataEntry( + path="Vehicle.Chassis.Height", + metadata=types_v1.Metadata( + deprecation="V2.1 moved to Vehicle.Height" + ), + ), + types_v1.DataEntry( + path="Vehicle.Chassis.Height", metadata=types_v1.Metadata(unit="mm") + ), + types_v1.DataEntry( + path="Vehicle.CurrentLocation.Heading", + metadata=types_v1.Metadata( + value_restriction=types_v1.ValueRestriction( + floating_point=types_v1.ValueRestrictionFloat( + min=0, max=360 + ), + ) + ), + ), + types_v1.DataEntry( + path="Dummy.With.Allowed.Values", + metadata=types_v1.Metadata( + value_restriction=types_v1.ValueRestriction( + signed=types_v1.ValueRestrictionInt( + allowed_values=[12, 42, 666] + ), + ) + ), + ), + ] + ) async with VSSClient('127.0.0.1', unused_tcp_port, ensure_startup_connection=False) as client: entries = await client.get(entries=(entry for entry in ( # generator is intentional as get accepts Iterable @@ -784,66 +885,74 @@ async def test_get_some_entries(self, unused_tcp_port, val_servicer): allowed_values=[12, 42, 666]), )), ] - assert val_servicer.Get.call_args[0][0].entries == val_pb2.GetRequest(entries=( - val_pb2.EntryRequest( - path='Vehicle.Speed', view=types_pb2.VIEW_CURRENT_VALUE, fields=(types_pb2.FIELD_VALUE,), - ), - val_pb2.EntryRequest( - path='Vehicle.ADAS.ABS.IsActive', - view=types_pb2.VIEW_TARGET_VALUE, - fields=(types_pb2.FIELD_ACTUATOR_TARGET,), - ), - val_pb2.EntryRequest( - path='Vehicle.Chassis.Height', - view=types_pb2.VIEW_METADATA, - fields=(types_pb2.FIELD_METADATA,), - ), - val_pb2.EntryRequest( - path='Vehicle.Chassis.Height', - view=types_pb2.VIEW_METADATA, - fields=(types_pb2.FIELD_METADATA_DATA_TYPE,), - ), - val_pb2.EntryRequest( - path='Vehicle.Chassis.Height', - view=types_pb2.VIEW_METADATA, - fields=(types_pb2.FIELD_METADATA_DESCRIPTION,), - ), - val_pb2.EntryRequest( - path='Vehicle.Chassis.Height', - view=types_pb2.VIEW_METADATA, - fields=(types_pb2.FIELD_METADATA_ENTRY_TYPE,), - ), - val_pb2.EntryRequest( - path='Vehicle.Chassis.Height', - view=types_pb2.VIEW_METADATA, - fields=(types_pb2.FIELD_METADATA_COMMENT,), - ), - val_pb2.EntryRequest( - path='Vehicle.Chassis.Height', - view=types_pb2.VIEW_METADATA, - fields=(types_pb2.FIELD_METADATA_DEPRECATION,), - ), - val_pb2.EntryRequest( - path='Vehicle.Chassis.Height', - view=types_pb2.VIEW_METADATA, - fields=(types_pb2.FIELD_METADATA_UNIT,), - ), - val_pb2.EntryRequest( - path='Vehicle.CurrentLocation.Heading', - view=types_pb2.VIEW_METADATA, - fields=(types_pb2.FIELD_METADATA_VALUE_RESTRICTION,), - ), - val_pb2.EntryRequest( - path='Dummy.With.Allowed.Values', - view=types_pb2.VIEW_METADATA, - fields=(types_pb2.FIELD_METADATA_VALUE_RESTRICTION,), - ), - )).entries + assert ( + val_servicer_v1.Get.call_args[0][0].entries + == val_v1.GetRequest( + entries=( + val_v1.EntryRequest( + path="Vehicle.Speed", + view=types_v1.VIEW_CURRENT_VALUE, + fields=(types_v1.FIELD_VALUE,), + ), + val_v1.EntryRequest( + path="Vehicle.ADAS.ABS.IsActive", + view=types_v1.VIEW_TARGET_VALUE, + fields=(types_v1.FIELD_ACTUATOR_TARGET,), + ), + val_v1.EntryRequest( + path="Vehicle.Chassis.Height", + view=types_v1.VIEW_METADATA, + fields=(types_v1.FIELD_METADATA,), + ), + val_v1.EntryRequest( + path="Vehicle.Chassis.Height", + view=types_v1.VIEW_METADATA, + fields=(types_v1.FIELD_METADATA_DATA_TYPE,), + ), + val_v1.EntryRequest( + path="Vehicle.Chassis.Height", + view=types_v1.VIEW_METADATA, + fields=(types_v1.FIELD_METADATA_DESCRIPTION,), + ), + val_v1.EntryRequest( + path="Vehicle.Chassis.Height", + view=types_v1.VIEW_METADATA, + fields=(types_v1.FIELD_METADATA_ENTRY_TYPE,), + ), + val_v1.EntryRequest( + path="Vehicle.Chassis.Height", + view=types_v1.VIEW_METADATA, + fields=(types_v1.FIELD_METADATA_COMMENT,), + ), + val_v1.EntryRequest( + path="Vehicle.Chassis.Height", + view=types_v1.VIEW_METADATA, + fields=(types_v1.FIELD_METADATA_DEPRECATION,), + ), + val_v1.EntryRequest( + path="Vehicle.Chassis.Height", + view=types_v1.VIEW_METADATA, + fields=(types_v1.FIELD_METADATA_UNIT,), + ), + val_v1.EntryRequest( + path="Vehicle.CurrentLocation.Heading", + view=types_v1.VIEW_METADATA, + fields=(types_v1.FIELD_METADATA_VALUE_RESTRICTION,), + ), + val_v1.EntryRequest( + path="Dummy.With.Allowed.Values", + view=types_v1.VIEW_METADATA, + fields=(types_v1.FIELD_METADATA_VALUE_RESTRICTION,), + ), + ) + ).entries + ) - @pytest.mark.usefixtures('val_server') - async def test_get_no_entries_requested(self, unused_tcp_port, val_servicer): - val_servicer.Get.side_effect = generate_error( - grpc.StatusCode.INVALID_ARGUMENT, 'No datapoints requested') + @pytest.mark.usefixtures("val_server") + async def test_get_no_entries_requested(self, unused_tcp_port, val_servicer_v1): + val_servicer_v1.Get.side_effect = generate_error( + grpc.StatusCode.INVALID_ARGUMENT, "No datapoints requested" + ) async with VSSClient('127.0.0.1', unused_tcp_port, ensure_startup_connection=False) as client: with pytest.raises(kuksa_client.grpc.VSSClientError) as exc_info: await client.get(entries=[]) @@ -853,14 +962,16 @@ async def test_get_no_entries_requested(self, unused_tcp_port, val_servicer): 'reason': grpc.StatusCode.INVALID_ARGUMENT.value[1], 'message': 'No datapoints requested', }, errors=[]).args - assert val_servicer.Get.call_args[0][0] == val_pb2.GetRequest() - - @pytest.mark.usefixtures('val_server') - async def test_get_unset_entries(self, unused_tcp_port, val_servicer): - val_servicer.Get.return_value = val_pb2.GetResponse(entries=[ - types_pb2.DataEntry(path='Vehicle.Speed'), - types_pb2.DataEntry(path='Vehicle.ADAS.ABS.IsActive'), - ]) + assert val_servicer_v1.Get.call_args[0][0] == val_v1.GetRequest() + + @pytest.mark.usefixtures("val_server") + async def test_get_unset_entries(self, unused_tcp_port, val_servicer_v1): + val_servicer_v1.Get.return_value = val_v1.GetResponse( + entries=[ + types_v1.DataEntry(path="Vehicle.Speed"), + types_v1.DataEntry(path="Vehicle.ADAS.ABS.IsActive"), + ] + ) async with VSSClient('127.0.0.1', unused_tcp_port, ensure_startup_connection=False) as client: entries = await client.get(entries=( EntryRequest('Vehicle.Speed', @@ -871,14 +982,15 @@ async def test_get_unset_entries(self, unused_tcp_port, val_servicer): assert entries == [DataEntry('Vehicle.Speed'), DataEntry( 'Vehicle.ADAS.ABS.IsActive')] - @pytest.mark.usefixtures('val_server') - async def test_get_nonexistent_entries(self, unused_tcp_port, val_servicer): - error = types_pb2.Error( - code=404, reason='not_found', message="Does.Not.Exist not found") - errors = (types_pb2.DataEntryError( - path='Does.Not.Exist', error=error),) - val_servicer.Get.return_value = val_pb2.GetResponse( - error=error, errors=errors) + @pytest.mark.usefixtures("val_server") + async def test_get_nonexistent_entries(self, unused_tcp_port, val_servicer_v1): + error = types_v1.Error( + code=404, reason="not_found", message="Does.Not.Exist not found" + ) + errors = (types_v1.DataEntryError(path="Does.Not.Exist", error=error),) + val_servicer_v1.Get.return_value = val_v1.GetResponse( + error=error, errors=errors + ) async with VSSClient('127.0.0.1', unused_tcp_port, ensure_startup_connection=False) as client: with pytest.raises(VSSClientError): await client.get(entries=( @@ -886,134 +998,328 @@ async def test_get_nonexistent_entries(self, unused_tcp_port, val_servicer): View.CURRENT_VALUE, (Field.VALUE,)), )) - @pytest.mark.usefixtures('val_server') - async def test_set_some_updates(self, unused_tcp_port, val_servicer): - val_servicer.Get.return_value = val_pb2.GetResponse(entries=( - types_pb2.DataEntry( - path='Vehicle.Speed', metadata=types_pb2.Metadata(data_type=types_pb2.DATA_TYPE_FLOAT), - ), - types_pb2.DataEntry( - path='Vehicle.ADAS.ABS.IsActive', - metadata=types_pb2.Metadata( - data_type=types_pb2.DATA_TYPE_BOOLEAN), - ), - types_pb2.DataEntry( - path='Vehicle.Cabin.Door.Row1.Left.Shade.Position', - metadata=types_pb2.Metadata( - data_type=types_pb2.DATA_TYPE_UINT8), - ), - )) - val_servicer.Set.return_value = val_pb2.SetResponse() - async with VSSClient('127.0.0.1', unused_tcp_port, ensure_startup_connection=False) as client: - await client.set(updates=[ - EntryUpdate(DataEntry('Vehicle.Speed', - value=Datapoint(value=42.0)), (Field.VALUE,)), - EntryUpdate(DataEntry( - 'Vehicle.ADAS.ABS.IsActive', actuator_target=Datapoint(value=False), - ), (Field.ACTUATOR_TARGET,)), - EntryUpdate(DataEntry('Vehicle.ADAS.CruiseControl.Error', metadata=Metadata( - data_type=DataType.BOOLEAN, - entry_type=EntryType.SENSOR, - description="Indicates if cruise control system incurred and error condition.", - comment="No comment", - deprecation="Never to be deprecated", - unit=None, - value_restriction=None, - )), (Field.METADATA,)), - EntryUpdate(DataEntry('Vehicle.ADAS.CruiseControl.Error', metadata=Metadata( - data_type=DataType.BOOLEAN, - )), (Field.METADATA_DATA_TYPE,)), - EntryUpdate(DataEntry('Vehicle.ADAS.CruiseControl.Error', metadata=Metadata( - description="Indicates if cruise control system incurred and error condition.", - )), (Field.METADATA_DESCRIPTION,)), - EntryUpdate(DataEntry('Vehicle.ADAS.CruiseControl.Error', metadata=Metadata( - entry_type=EntryType.SENSOR, - )), (Field.METADATA_ENTRY_TYPE,)), - EntryUpdate(DataEntry('Vehicle.ADAS.CruiseControl.Error', metadata=Metadata( - comment="No comment", - )), (Field.METADATA_COMMENT,)), - EntryUpdate(DataEntry('Vehicle.ADAS.CruiseControl.Error', metadata=Metadata( - deprecation="Never to be deprecated", - )), (Field.METADATA_DEPRECATION,)), - EntryUpdate(DataEntry('Vehicle.Cabin.Door.Row1.Left.Shade.Position', metadata=Metadata( - unit='percent', - )), (Field.METADATA_UNIT,)), - EntryUpdate(DataEntry('Vehicle.Cabin.Door.Row1.Left.Shade.Position', metadata=Metadata( - value_restriction=ValueRestriction(min=0, max=100), - )), (Field.METADATA_VALUE_RESTRICTION,)), - ]) - assert val_servicer.Get.call_count == 1 - assert val_servicer.Get.call_args[0][0].entries == val_pb2.GetRequest(entries=( - val_pb2.EntryRequest(path='Vehicle.Speed', view=View.METADATA, fields=( - Field.METADATA_DATA_TYPE,)), - val_pb2.EntryRequest( - path='Vehicle.ADAS.ABS.IsActive', view=View.METADATA, fields=(Field.METADATA_DATA_TYPE,), + @pytest.mark.usefixtures("val_server") + async def test_set_some_updates_v1(self, unused_tcp_port, val_servicer_v1): + val_servicer_v1.Get.return_value = val_v1.GetResponse( + entries=( + types_v1.DataEntry( + path="Vehicle.Speed", + metadata=types_v1.Metadata(data_type=types_v1.DATA_TYPE_FLOAT), + ), + types_v1.DataEntry( + path="Vehicle.ADAS.ABS.IsActive", + metadata=types_v1.Metadata(data_type=types_v1.DATA_TYPE_BOOLEAN), ), - val_pb2.EntryRequest( - path='Vehicle.Cabin.Door.Row1.Left.Shade.Position', - view=View.METADATA, - fields=(Field.METADATA_DATA_TYPE,), + types_v1.DataEntry( + path="Vehicle.Cabin.Door.Row1.Left.Shade.Position", + metadata=types_v1.Metadata(data_type=types_v1.DATA_TYPE_UINT8), ), - )).entries - assert val_servicer.Set.call_args[0][0].updates == val_pb2.SetRequest(updates=( - val_pb2.EntryUpdate(entry=types_pb2.DataEntry( - path='Vehicle.Speed', value=types_pb2.Datapoint(float=42.0), - ), fields=(types_pb2.FIELD_VALUE,)), - val_pb2.EntryUpdate(entry=types_pb2.DataEntry( - path='Vehicle.ADAS.ABS.IsActive', actuator_target=types_pb2.Datapoint(bool=False), - ), fields=(types_pb2.FIELD_ACTUATOR_TARGET,)), - val_pb2.EntryUpdate(entry=types_pb2.DataEntry( - path='Vehicle.ADAS.CruiseControl.Error', - metadata=types_pb2.Metadata( - data_type=types_pb2.DATA_TYPE_BOOLEAN, - entry_type=types_pb2.ENTRY_TYPE_SENSOR, - description="Indicates if cruise control system incurred and error condition.", - comment="No comment", - deprecation="Never to be deprecated", + ) + ) + val_servicer_v1.Set.return_value = val_v1.SetResponse() + async with VSSClient( + "127.0.0.1", unused_tcp_port, ensure_startup_connection=False + ) as client: + await client.set( + updates=[ + EntryUpdate( + DataEntry("Vehicle.Speed", value=Datapoint(value=42.0)), + (Field.VALUE,), + ), + EntryUpdate( + DataEntry( + "Vehicle.ADAS.ABS.IsActive", + actuator_target=Datapoint(value=False), + ), + (Field.ACTUATOR_TARGET,), + ), + EntryUpdate( + DataEntry( + "Vehicle.ADAS.CruiseControl.Error", + metadata=Metadata( + data_type=DataType.BOOLEAN, + entry_type=EntryType.SENSOR, + description="Indicates if cruise control system incurred and error condition.", + comment="No comment", + deprecation="Never to be deprecated", + unit=None, + value_restriction=None, + ), + ), + (Field.METADATA,), + ), + EntryUpdate( + DataEntry( + "Vehicle.ADAS.CruiseControl.Error", + metadata=Metadata( + data_type=DataType.BOOLEAN, + ), + ), + (Field.METADATA_DATA_TYPE,), + ), + EntryUpdate( + DataEntry( + "Vehicle.ADAS.CruiseControl.Error", + metadata=Metadata( + description="Indicates if cruise control system incurred and error condition.", + ), + ), + (Field.METADATA_DESCRIPTION,), + ), + EntryUpdate( + DataEntry( + "Vehicle.ADAS.CruiseControl.Error", + metadata=Metadata( + entry_type=EntryType.SENSOR, + ), + ), + (Field.METADATA_ENTRY_TYPE,), + ), + EntryUpdate( + DataEntry( + "Vehicle.ADAS.CruiseControl.Error", + metadata=Metadata( + comment="No comment", + ), + ), + (Field.METADATA_COMMENT,), + ), + EntryUpdate( + DataEntry( + "Vehicle.ADAS.CruiseControl.Error", + metadata=Metadata( + deprecation="Never to be deprecated", + ), + ), + (Field.METADATA_DEPRECATION,), + ), + EntryUpdate( + DataEntry( + "Vehicle.Cabin.Door.Row1.Left.Shade.Position", + metadata=Metadata( + unit="percent", + ), + ), + (Field.METADATA_UNIT,), ), - ), fields=(types_pb2.FIELD_METADATA,)), - val_pb2.EntryUpdate(entry=types_pb2.DataEntry( - path='Vehicle.ADAS.CruiseControl.Error', - metadata=types_pb2.Metadata( - data_type=types_pb2.DATA_TYPE_BOOLEAN), - ), fields=(types_pb2.FIELD_METADATA_DATA_TYPE,)), - val_pb2.EntryUpdate(entry=types_pb2.DataEntry( - path='Vehicle.ADAS.CruiseControl.Error', - metadata=types_pb2.Metadata( - description="Indicates if cruise control system incurred and error condition." + EntryUpdate( + DataEntry( + "Vehicle.Cabin.Door.Row1.Left.Shade.Position", + metadata=Metadata( + value_restriction=ValueRestriction(min=0, max=100), + ), + ), + (Field.METADATA_VALUE_RESTRICTION,), ), - ), fields=(types_pb2.FIELD_METADATA_DESCRIPTION,)), - val_pb2.EntryUpdate(entry=types_pb2.DataEntry( - path='Vehicle.ADAS.CruiseControl.Error', - metadata=types_pb2.Metadata( - entry_type=types_pb2.ENTRY_TYPE_SENSOR), - ), fields=(types_pb2.FIELD_METADATA_ENTRY_TYPE,)), - val_pb2.EntryUpdate(entry=types_pb2.DataEntry( - path='Vehicle.ADAS.CruiseControl.Error', - metadata=types_pb2.Metadata(comment="No comment"), - ), fields=(types_pb2.FIELD_METADATA_COMMENT,)), - val_pb2.EntryUpdate(entry=types_pb2.DataEntry( - path='Vehicle.ADAS.CruiseControl.Error', - metadata=types_pb2.Metadata( - deprecation="Never to be deprecated"), - ), fields=(types_pb2.FIELD_METADATA_DEPRECATION,)), - val_pb2.EntryUpdate(entry=types_pb2.DataEntry( - path='Vehicle.Cabin.Door.Row1.Left.Shade.Position', - metadata=types_pb2.Metadata(unit="percent"), - ), fields=(types_pb2.FIELD_METADATA_UNIT,)), - val_pb2.EntryUpdate(entry=types_pb2.DataEntry( - path='Vehicle.Cabin.Door.Row1.Left.Shade.Position', - metadata=types_pb2.Metadata(value_restriction=types_pb2.ValueRestriction( - unsigned=types_pb2.ValueRestrictionUint( - min=0, max=100), - )), - ), fields=(types_pb2.FIELD_METADATA_VALUE_RESTRICTION,)), - )).updates - - @pytest.mark.usefixtures('val_server') - async def test_set_no_updates_provided(self, unused_tcp_port, val_servicer): - val_servicer.Set.side_effect = generate_error( - grpc.StatusCode.INVALID_ARGUMENT, 'No datapoints requested') + ] + ) + assert val_servicer_v1.Get.call_count == 1 + assert ( + val_servicer_v1.Get.call_args[0][0].entries + == val_v1.GetRequest( + entries=( + val_v1.EntryRequest( + path="Vehicle.Speed", + view=View.METADATA, + fields=(Field.METADATA_DATA_TYPE,), + ), + val_v1.EntryRequest( + path="Vehicle.ADAS.ABS.IsActive", + view=View.METADATA, + fields=(Field.METADATA_DATA_TYPE,), + ), + val_v1.EntryRequest( + path="Vehicle.Cabin.Door.Row1.Left.Shade.Position", + view=View.METADATA, + fields=(Field.METADATA_DATA_TYPE,), + ), + ) + ).entries + ) + assert ( + val_servicer_v1.Set.call_args[0][0].updates + == val_v1.SetRequest( + updates=( + val_v1.EntryUpdate( + entry=types_v1.DataEntry( + path="Vehicle.Speed", + value=types_v1.Datapoint(float=42.0), + ), + fields=(types_v1.FIELD_VALUE,), + ), + val_v1.EntryUpdate( + entry=types_v1.DataEntry( + path="Vehicle.ADAS.ABS.IsActive", + actuator_target=types_v1.Datapoint(bool=False), + ), + fields=(types_v1.FIELD_ACTUATOR_TARGET,), + ), + val_v1.EntryUpdate( + entry=types_v1.DataEntry( + path="Vehicle.ADAS.CruiseControl.Error", + metadata=types_v1.Metadata( + data_type=types_v1.DATA_TYPE_BOOLEAN, + entry_type=types_v1.ENTRY_TYPE_SENSOR, + description="Indicates if cruise control system incurred and error condition.", + comment="No comment", + deprecation="Never to be deprecated", + ), + ), + fields=(types_v1.FIELD_METADATA,), + ), + val_v1.EntryUpdate( + entry=types_v1.DataEntry( + path="Vehicle.ADAS.CruiseControl.Error", + metadata=types_v1.Metadata( + data_type=types_v1.DATA_TYPE_BOOLEAN + ), + ), + fields=(types_v1.FIELD_METADATA_DATA_TYPE,), + ), + val_v1.EntryUpdate( + entry=types_v1.DataEntry( + path="Vehicle.ADAS.CruiseControl.Error", + metadata=types_v1.Metadata( + description="Indicates if cruise control system incurred and error condition." + ), + ), + fields=(types_v1.FIELD_METADATA_DESCRIPTION,), + ), + val_v1.EntryUpdate( + entry=types_v1.DataEntry( + path="Vehicle.ADAS.CruiseControl.Error", + metadata=types_v1.Metadata( + entry_type=types_v1.ENTRY_TYPE_SENSOR + ), + ), + fields=(types_v1.FIELD_METADATA_ENTRY_TYPE,), + ), + val_v1.EntryUpdate( + entry=types_v1.DataEntry( + path="Vehicle.ADAS.CruiseControl.Error", + metadata=types_v1.Metadata(comment="No comment"), + ), + fields=(types_v1.FIELD_METADATA_COMMENT,), + ), + val_v1.EntryUpdate( + entry=types_v1.DataEntry( + path="Vehicle.ADAS.CruiseControl.Error", + metadata=types_v1.Metadata( + deprecation="Never to be deprecated" + ), + ), + fields=(types_v1.FIELD_METADATA_DEPRECATION,), + ), + val_v1.EntryUpdate( + entry=types_v1.DataEntry( + path="Vehicle.Cabin.Door.Row1.Left.Shade.Position", + metadata=types_v1.Metadata(unit="percent"), + ), + fields=(types_v1.FIELD_METADATA_UNIT,), + ), + val_v1.EntryUpdate( + entry=types_v1.DataEntry( + path="Vehicle.Cabin.Door.Row1.Left.Shade.Position", + metadata=types_v1.Metadata( + value_restriction=types_v1.ValueRestriction( + unsigned=types_v1.ValueRestrictionUint( + min=0, max=100 + ), + ) + ), + ), + fields=(types_v1.FIELD_METADATA_VALUE_RESTRICTION,), + ), + ) + ).updates + ) + + @pytest.mark.usefixtures("val_server") + async def test_set_some_updates_v2( + self, unused_tcp_port, val_servicer_v2, val_servicer_v1 + ): + val_servicer_v1.Get.return_value = val_v1.GetResponse( + entries=( + types_v1.DataEntry( + path="Vehicle.Speed", + metadata=types_v1.Metadata(data_type=types_v1.DATA_TYPE_FLOAT), + ), + types_v1.DataEntry( + path="Vehicle.ADAS.ABS.IsActive", + metadata=types_v1.Metadata(data_type=types_v1.DATA_TYPE_BOOLEAN), + ), + ) + ) + val_servicer_v2.PublishValue.return_value = val_v2.PublishValueResponse() + _updates = [ + EntryUpdate( + DataEntry("Vehicle.Speed", value=Datapoint(value=42.0)), + (Field.VALUE,), + ), + EntryUpdate( + DataEntry( + "Vehicle.ADAS.ABS.IsActive", + value=Datapoint(value=False), + ), + (Field.VALUE,), + ), + ] + + async with VSSClient( + "127.0.0.1", unused_tcp_port, ensure_startup_connection=False + ) as client: + await client.set( + updates=_updates, + v1=False, + ) + assert val_servicer_v1.Get.call_count == 1 + assert ( + val_servicer_v1.Get.call_args[0][0].entries + == val_v1.GetRequest( + entries=( + val_v1.EntryRequest( + path="Vehicle.Speed", + view=View.METADATA, + fields=(Field.METADATA_DATA_TYPE,), + ), + val_v1.EntryRequest( + path="Vehicle.ADAS.ABS.IsActive", + view=View.METADATA, + fields=(Field.METADATA_DATA_TYPE,), + ), + ) + ).entries + ) + + expected_requests = [ + val_v2.PublishValueRequest( + signal_id=types_v2.SignalID(path="Vehicle.Speed"), + data_point=types_v2.Datapoint(value=types_v2.Value(float=42.0)), + ), + val_v2.PublishValueRequest( + signal_id=types_v2.SignalID(path="Vehicle.ADAS.ABS.IsActive"), + data_point=types_v2.Datapoint(value=types_v2.Value(bool=False)), + ), + ] + + assert val_servicer_v2.PublishValue.call_count == len(_updates) + + actual_requests = [ + call[0][0] for call in val_servicer_v2.PublishValue.call_args_list + ] + + for actual_request, expected_request in zip( + actual_requests, expected_requests + ): + assert actual_request == expected_request + + @pytest.mark.usefixtures("val_server") + async def test_set_no_updates_provided( + self, unused_tcp_port, val_servicer_v1, val_servicer_v2 + ): + val_servicer_v1.Set.side_effect = generate_error( + grpc.StatusCode.INVALID_ARGUMENT, "No datapoints requested" + ) async with VSSClient('127.0.0.1', unused_tcp_port, ensure_startup_connection=False) as client: with pytest.raises(kuksa_client.grpc.VSSClientError) as exc_info: await client.set(updates=[]) @@ -1023,44 +1329,133 @@ async def test_set_no_updates_provided(self, unused_tcp_port, val_servicer): 'reason': grpc.StatusCode.INVALID_ARGUMENT.value[1], 'message': 'No datapoints requested', }, errors=[]).args - assert val_servicer.Get.call_count == 0 - assert val_servicer.Set.call_args[0][0].updates == val_pb2.SetRequest( - ).updates - - @pytest.mark.usefixtures('val_server') - async def test_set_nonexistent_entries(self, unused_tcp_port, val_servicer): - error = types_pb2.Error( - code=404, reason='not_found', message="Does.Not.Exist not found") - errors = (types_pb2.DataEntryError( - path='Does.Not.Exist', error=error),) - val_servicer.Get.return_value = val_pb2.GetResponse( - error=error, errors=errors) - val_servicer.Set.return_value = val_pb2.SetResponse( - error=error, errors=errors) - async with VSSClient('127.0.0.1', unused_tcp_port, ensure_startup_connection=False) as client: + assert val_servicer_v1.Get.call_count == 0 + assert ( + val_servicer_v1.Set.call_args[0][0].updates + == val_v1.SetRequest().updates + ) + async with VSSClient( + "127.0.0.1", unused_tcp_port, ensure_startup_connection=False + ) as client: + with pytest.raises(kuksa_client.grpc.VSSClientError) as exc_info: + await client.set(updates=[], v1=False) + + assert ( + exc_info.value.args + == kuksa_client.grpc.VSSClientError( + error={ + "code": grpc.StatusCode.INVALID_ARGUMENT.value[0], + "reason": grpc.StatusCode.INVALID_ARGUMENT.value[1], + "message": "No datapoints requested", + }, + errors=[], + ).args + ) + assert val_servicer_v1.Get.call_count == 0 + assert val_servicer_v2.PublishValue.call_count == 0 + + @pytest.mark.usefixtures("val_server") + async def test_set_nonexistent_entries_v1(self, unused_tcp_port, val_servicer_v1): + error = types_v1.Error( + code=404, reason="not_found", message="Does.Not.Exist not found" + ) + errors = (types_v1.DataEntryError(path="Does.Not.Exist", error=error),) + val_servicer_v1.Get.return_value = val_v1.GetResponse( + error=error, errors=errors + ) + val_servicer_v1.Set.return_value = val_v1.SetResponse( + error=error, errors=errors + ) + async with VSSClient( + "127.0.0.1", unused_tcp_port, ensure_startup_connection=False + ) as client: + with pytest.raises(VSSClientError): + await client.set( + updates=( + EntryUpdate( + DataEntry("Does.Not.Exist", value=Datapoint(value=42.0)), + (Field.VALUE,), + ), + ), + ) + + assert val_servicer_v1.Get.call_count == 1 + assert val_servicer_v1.Set.call_count == 0 + with pytest.raises(VSSClientError): + await client.set( + updates=( + EntryUpdate( + DataEntry( + "Does.Not.Exist", + value=Datapoint(value=42.0), + metadata=Metadata(data_type=DataType.FLOAT), + ), + (Field.VALUE,), + ), + ), + ) + + assert ( + val_servicer_v1.Get.call_count == 1 + ) # Get should'nt have been called again + assert val_servicer_v1.Set.call_count == 1 + + @pytest.mark.usefixtures("val_server") + async def test_set_nonexistent_entries_v2( + self, unused_tcp_port, val_servicer_v2, val_servicer_v1 + ): + error = types_v1.Error( + code=404, reason="not_found", message="Does.Not.Exist not found" + ) + errors = (types_v1.DataEntryError(path="Does.Not.Exist", error=error),) + val_servicer_v1.Get.return_value = val_v1.GetResponse( + error=error, errors=errors + ) + val_servicer_v2.PublishValue.side_effect = generate_error( + grpc.StatusCode.NOT_FOUND, + "Does.Not.Exist not found", + ) + async with VSSClient( + "127.0.0.1", unused_tcp_port, ensure_startup_connection=False + ) as client: with pytest.raises(VSSClientError): - await client.set(updates=( - EntryUpdate(DataEntry('Does.Not.Exist', value=Datapoint(value=42.0)), (Field.VALUE,)),), + await client.set( + updates=( + EntryUpdate( + DataEntry("Does.Not.Exist", value=Datapoint(value=42.0)), + (Field.VALUE,), + ), + ), + v1=False, ) - assert val_servicer.Get.call_count == 1 - assert val_servicer.Set.call_count == 0 + assert val_servicer_v1.Get.call_count == 1 + assert val_servicer_v2.PublishValue.call_count == 0 with pytest.raises(VSSClientError): - await client.set(updates=( - EntryUpdate(DataEntry( - 'Does.Not.Exist', - value=Datapoint(value=42.0), - metadata=Metadata(data_type=DataType.FLOAT), - ), (Field.VALUE,)),), + await client.set( + updates=( + EntryUpdate( + DataEntry( + "Does.Not.Exist", + value=Datapoint(value=42.0), + metadata=Metadata(data_type=DataType.FLOAT), + ), + (Field.VALUE,), + ), + ), + v1=False, ) - assert val_servicer.Get.call_count == 1 # Get should'nt have been called again - assert val_servicer.Set.call_count == 1 + assert ( + val_servicer_v1.Get.call_count == 1 + ) # Get should'nt have been called again + assert val_servicer_v2.PublishValue.call_count == 1 - @pytest.mark.usefixtures('val_server') - async def test_authorize_successful(self, unused_tcp_port, val_servicer): - val_servicer.GetServerInfo.return_value = val_pb2.GetServerInfoResponse( - name='test_server', version='1.2.3') + @pytest.mark.usefixtures("val_server") + async def test_authorize_successful(self, unused_tcp_port, val_servicer_v1): + val_servicer_v1.GetServerInfo.return_value = val_v1.GetServerInfoResponse( + name="test_server", version="1.2.3" + ) async with VSSClient('127.0.0.1', unused_tcp_port, ensure_startup_connection=False) as client: # token from kuksa.val directory under jwt/provide-vehicle-speed.token token = ('eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJsb2NhbCBkZXYiLCJpc3MiOiJjcmVhdGVUb2' @@ -1091,123 +1486,354 @@ async def test_authorize_successful(self, unused_tcp_port, val_servicer): assert client.authorization_header == bearer assert success == "Authenticated" - @pytest.mark.usefixtures('val_server') - async def test_authorize_unsuccessful(self, unused_tcp_port, val_servicer): - val_servicer.GetServerInfo.side_effect = generate_error( - grpc.StatusCode.UNAUTHENTICATED, 'Invalid auth token: DecodeError(\"InvalidToken\")') + @pytest.mark.usefixtures("val_server") + async def test_authorize_unsuccessful(self, unused_tcp_port, val_servicer_v1): + val_servicer_v1.GetServerInfo.side_effect = generate_error( + grpc.StatusCode.UNAUTHENTICATED, + 'Invalid auth token: DecodeError("InvalidToken")', + ) async with VSSClient('127.0.0.1', unused_tcp_port, ensure_startup_connection=False) as client: with pytest.raises(VSSClientError): await client.authorize(token='') assert client.authorization_header is None - @pytest.mark.usefixtures('val_server') - async def test_subscribe_some_entries(self, mocker, unused_tcp_port, val_servicer): + @pytest.mark.usefixtures("val_server") + async def test_subscribe_some_entries_v1( + self, mocker, unused_tcp_port, val_servicer_v1 + ): async with VSSClient('127.0.0.1', unused_tcp_port, ensure_startup_connection=False) as client: responses = ( # 1st response is subscription ack - val_pb2.SubscribeResponse(updates=[ - val_pb2.EntryUpdate(entry=types_pb2.DataEntry( - path='Vehicle.Speed', - value=types_pb2.Datapoint( - timestamp=timestamp_pb2.Timestamp( - seconds=1667837915, nanos=247307674), - float=42.0, - ), - ), fields=(Field.VALUE,)), - val_pb2.EntryUpdate(entry=types_pb2.DataEntry( - path='Vehicle.ADAS.ABS.IsActive', - actuator_target=types_pb2.Datapoint(bool=True), - ), fields=(Field.ACTUATOR_TARGET,)), - val_pb2.EntryUpdate(entry=types_pb2.DataEntry( - path='Vehicle.Chassis.Height', - metadata=types_pb2.Metadata( - data_type=types_pb2.DATA_TYPE_UINT16, - ), - ), fields=(Field.METADATA_DATA_TYPE,)), - ]), + val_v1.SubscribeResponse( + updates=[ + val_v1.EntryUpdate( + entry=types_v1.DataEntry( + path="Vehicle.Speed", + value=types_v1.Datapoint( + timestamp=timestamp_pb2.Timestamp( + seconds=1667837915, nanos=247307674 + ), + float=42.0, + ), + ), + fields=(Field.VALUE,), + ), + val_v1.EntryUpdate( + entry=types_v1.DataEntry( + path="Vehicle.ADAS.ABS.IsActive", + actuator_target=types_v1.Datapoint(bool=True), + ), + fields=(Field.ACTUATOR_TARGET,), + ), + val_v1.EntryUpdate( + entry=types_v1.DataEntry( + path="Vehicle.Chassis.Height", + metadata=types_v1.Metadata( + data_type=types_v1.DATA_TYPE_UINT16, + ), + ), + fields=(Field.METADATA_DATA_TYPE,), + ), + ] + ), # Remaining responses are actual events. - val_pb2.SubscribeResponse(updates=[ - val_pb2.EntryUpdate(entry=types_pb2.DataEntry( - path='Vehicle.Speed', - value=types_pb2.Datapoint( - timestamp=timestamp_pb2.Timestamp( - seconds=1667837912, nanos=247307674), - float=43.0, - ), - ), fields=(Field.VALUE,)), - ]), - val_pb2.SubscribeResponse(updates=[ - val_pb2.EntryUpdate(entry=types_pb2.DataEntry( - path='Vehicle.ADAS.ABS.IsActive', - actuator_target=types_pb2.Datapoint(bool=False), - ), fields=(Field.ACTUATOR_TARGET,)), - ]), - val_pb2.SubscribeResponse(updates=[ - val_pb2.EntryUpdate(entry=types_pb2.DataEntry( - path='Vehicle.Chassis.Height', - metadata=types_pb2.Metadata( - data_type=types_pb2.DATA_TYPE_UINT8, - ), - ), fields=(Field.METADATA_DATA_TYPE,)), - ]), + val_v1.SubscribeResponse( + updates=[ + val_v1.EntryUpdate( + entry=types_v1.DataEntry( + path="Vehicle.Speed", + value=types_v1.Datapoint( + timestamp=timestamp_pb2.Timestamp( + seconds=1667837912, nanos=247307674 + ), + float=43.0, + ), + ), + fields=(Field.VALUE,), + ), + ] + ), + val_v1.SubscribeResponse( + updates=[ + val_v1.EntryUpdate( + entry=types_v1.DataEntry( + path="Vehicle.ADAS.ABS.IsActive", + actuator_target=types_v1.Datapoint(bool=False), + ), + fields=(Field.ACTUATOR_TARGET,), + ), + ] + ), + val_v1.SubscribeResponse( + updates=[ + val_v1.EntryUpdate( + entry=types_v1.DataEntry( + path="Vehicle.Chassis.Height", + metadata=types_v1.Metadata( + data_type=types_v1.DATA_TYPE_UINT8, + ), + ), + fields=(Field.METADATA_DATA_TYPE,), + ), + ] + ), + ) + val_servicer_v1.Subscribe.return_value = ( + response for response in responses ) - val_servicer.Subscribe.return_value = ( - response for response in responses) actual_responses = [] - async for updates in client.subscribe(entries=(entry for entry in ( # generator is intentional (Iterable) - EntryRequest('Vehicle.Speed', - View.CURRENT_VALUE, (Field.VALUE,)), - EntryRequest('Vehicle.ADAS.ABS.IsActive', - View.TARGET_VALUE, (Field.ACTUATOR_TARGET,)), - EntryRequest('Vehicle.Chassis.Height', - View.METADATA, (Field.METADATA_DATA_TYPE,)), - ))): + async for updates in client.subscribe( + entries=( + entry + for entry in ( # generator is intentional (Iterable) + EntryRequest( + "Vehicle.Speed", View.CURRENT_VALUE, (Field.VALUE,) + ), + EntryRequest( + "Vehicle.ADAS.ABS.IsActive", + View.TARGET_VALUE, + (Field.ACTUATOR_TARGET,), + ), + EntryRequest( + "Vehicle.Chassis.Height", + View.METADATA, + (Field.METADATA_DATA_TYPE,), + ), + ) + ) + ): actual_responses.append(updates) assert actual_responses == [ [ - EntryUpdate(entry=DataEntry(path='Vehicle.Speed', value=Datapoint( - value=42.0, - timestamp=datetime.datetime( - 2022, 11, 7, 16, 18, 35, 247307, tzinfo=datetime.timezone.utc), - )), fields=[Field.VALUE]), EntryUpdate( entry=DataEntry( - path='Vehicle.ADAS.ABS.IsActive', actuator_target=Datapoint(value=True)), + path="Vehicle.Speed", + value=Datapoint( + value=42.0, + timestamp=datetime.datetime( + 2022, + 11, + 7, + 16, + 18, + 35, + 247307, + tzinfo=datetime.timezone.utc, + ), + ), + ), + fields=[Field.VALUE], + ), + EntryUpdate( + entry=DataEntry( + path="Vehicle.ADAS.ABS.IsActive", + actuator_target=Datapoint(value=True), + ), + fields=[Field.ACTUATOR_TARGET], + ), + EntryUpdate( + entry=DataEntry( + path="Vehicle.Chassis.Height", + metadata=Metadata( + data_type=DataType.UINT16, + ), + ), + fields=[Field.METADATA_DATA_TYPE], + ), + ], + [ + EntryUpdate( + entry=DataEntry( + path="Vehicle.Speed", + value=Datapoint( + value=43.0, + timestamp=datetime.datetime( + 2022, + 11, + 7, + 16, + 18, + 32, + 247307, + tzinfo=datetime.timezone.utc, + ), + ), + ), + fields=[Field.VALUE], + ) + ], + [ + EntryUpdate( + entry=DataEntry( + path="Vehicle.ADAS.ABS.IsActive", + actuator_target=Datapoint( + value=False, + ), + ), fields=[Field.ACTUATOR_TARGET], + ) + ], + [ + EntryUpdate( + entry=DataEntry( + path="Vehicle.Chassis.Height", + metadata=Metadata( + data_type=DataType.UINT8, + ), + ), + fields=[Field.METADATA_DATA_TYPE], + ) + ], + ] + + @pytest.mark.usefixtures("val_server") + async def test_subscribe_some_entries_v2( + self, mocker, unused_tcp_port, val_servicer_v2 + ): + _entries: Dict[str, types_v2.Datapoint] = { + "Vehicle.Speed": types_v2.Datapoint( + timestamp=timestamp_pb2.Timestamp(seconds=1667837915, nanos=247307674), + value=types_v2.Value(float=42.0), + ), + "Vehicle.ADAS.ABS.IsActive": types_v2.Datapoint( + value=types_v2.Value(bool=True) + ), + } + _entries_2: Dict[str, types_v2.Datapoint] = { + "Vehicle.Speed": types_v2.Datapoint( + timestamp=timestamp_pb2.Timestamp(seconds=1667837912, nanos=247307674), + value=types_v2.Value(float=43.0), + ), + "Vehicle.ADAS.ABS.IsActive": types_v2.Datapoint( + value=types_v2.Value(bool=False) + ), + } + async with VSSClient( + "127.0.0.1", unused_tcp_port, ensure_startup_connection=False + ) as client: + responses = ( + val_v2.SubscribeResponse(entries=_entries), + val_v2.SubscribeResponse(entries=_entries_2), + ) + val_servicer_v2.Subscribe.return_value = ( + response for response in responses + ) + + actual_responses = [] + async for updates in client.subscribe( + entries=( + entry + for entry in ( # generator is intentional (Iterable) + EntryRequest( + "Vehicle.Speed", View.CURRENT_VALUE, (Field.VALUE,) + ), + EntryRequest( + "Vehicle.ADAS.ABS.IsActive", + View.CURRENT_VALUE, + (Field.VALUE,), + ), + ) + ), + v1=False, + ): + actual_responses.append(updates) + + assert actual_responses == [ + [ + EntryUpdate( + entry=DataEntry( + path="Vehicle.Speed", + value=Datapoint( + value=42.0, + timestamp=datetime.datetime( + 2022, + 11, + 7, + 16, + 18, + 35, + 247307, + tzinfo=datetime.timezone.utc, + ), + ), + ), + fields=[Field.VALUE], + ), + EntryUpdate( + entry=DataEntry( + path="Vehicle.ADAS.ABS.IsActive", + value=Datapoint(value=True), + ), + fields=[Field.VALUE], + ), + ], + [ + EntryUpdate( + entry=DataEntry( + path="Vehicle.Speed", + value=Datapoint( + value=43.0, + timestamp=datetime.datetime( + 2022, + 11, + 7, + 16, + 18, + 32, + 247307, + tzinfo=datetime.timezone.utc, + ), + ), + ), + fields=[Field.VALUE], + ), + EntryUpdate( + entry=DataEntry( + path="Vehicle.ADAS.ABS.IsActive", + value=Datapoint(value=False), + ), + fields=[Field.VALUE], ), - EntryUpdate(entry=DataEntry(path='Vehicle.Chassis.Height', metadata=Metadata( - data_type=DataType.UINT16, - )), fields=[Field.METADATA_DATA_TYPE]) ], - [EntryUpdate(entry=DataEntry(path='Vehicle.Speed', value=Datapoint( - value=43.0, - timestamp=datetime.datetime( - 2022, 11, 7, 16, 18, 32, 247307, tzinfo=datetime.timezone.utc), - )), fields=[Field.VALUE])], - [EntryUpdate(entry=DataEntry(path='Vehicle.ADAS.ABS.IsActive', actuator_target=Datapoint( - value=False, - )), fields=[Field.ACTUATOR_TARGET])], - [EntryUpdate(entry=DataEntry(path='Vehicle.Chassis.Height', metadata=Metadata( - data_type=DataType.UINT8, - )), fields=[Field.METADATA_DATA_TYPE])], ] - @pytest.mark.usefixtures('val_server') - async def test_subscribe_no_entries_requested(self, mocker, unused_tcp_port, val_servicer): - val_servicer.Subscribe.side_effect = generate_error( - grpc.StatusCode.INVALID_ARGUMENT, 'Subscription request must contain at least one entry.', + @pytest.mark.usefixtures("val_server") + async def test_subscribe_no_entries_requested( + self, mocker, unused_tcp_port, val_servicer_v1, val_servicer_v2 + ): + val_servicer_v1.Subscribe.side_effect = generate_error( + grpc.StatusCode.INVALID_ARGUMENT, + "Subscription request must contain at least one entry.", + ) + val_servicer_v2.Subscribe.side_effect = generate_error( + grpc.StatusCode.INVALID_ARGUMENT, + "Subscription request must contain at least one entry.", ) async with VSSClient('127.0.0.1', unused_tcp_port, ensure_startup_connection=False) as client: with pytest.raises(VSSClientError): async for _ in client.subscribe(entries=()): pass - @pytest.mark.usefixtures('val_server') - async def test_subscribe_nonexistent_entries(self, mocker, unused_tcp_port, val_servicer): - val_servicer.Subscribe.side_effect = generate_error( - grpc.StatusCode.INVALID_ARGUMENT, 'NotFound') + async with VSSClient( + "127.0.0.1", unused_tcp_port, ensure_startup_connection=False + ) as client: + with pytest.raises(VSSClientError): + async for _ in client.subscribe(entries=(), v1=False): + pass + + @pytest.mark.usefixtures("val_server") + async def test_subscribe_nonexistent_entries( + self, mocker, unused_tcp_port, val_servicer_v1, val_servicer_v2 + ): + val_servicer_v1.Subscribe.side_effect = generate_error( + grpc.StatusCode.INVALID_ARGUMENT, "NotFound" + ) + val_servicer_v2.Subscribe.side_effect = generate_error( + grpc.StatusCode.INVALID_ARGUMENT, "NotFound" + ) + async with VSSClient('127.0.0.1', unused_tcp_port, ensure_startup_connection=False) as client: with pytest.raises(VSSClientError): async for _ in client.subscribe(entries=(entry for entry in ( # generator is intentional (Iterable) @@ -1216,19 +1842,38 @@ async def test_subscribe_nonexistent_entries(self, mocker, unused_tcp_port, val_ ))): pass - @pytest.mark.usefixtures('val_server') - async def test_get_server_info(self, unused_tcp_port, val_servicer): - val_servicer.GetServerInfo.return_value = val_pb2.GetServerInfoResponse( - name='test_server', version='1.2.3') + async with VSSClient( + "127.0.0.1", unused_tcp_port, ensure_startup_connection=False + ) as client: + with pytest.raises(VSSClientError): + async for _ in client.subscribe( + entries=( + entry + for entry in ( # generator is intentional (Iterable) + EntryRequest( + "Does.Not.Exist", View.CURRENT_VALUE, (Field.VALUE,) + ), + ) + ), + v1=False, + ): + pass + + @pytest.mark.usefixtures("val_server") + async def test_get_server_info(self, unused_tcp_port, val_servicer_v1): + val_servicer_v1.GetServerInfo.return_value = val_v1.GetServerInfoResponse( + name="test_server", version="1.2.3" + ) async with VSSClient('127.0.0.1', unused_tcp_port, ensure_startup_connection=False) as client: server_info = await client.get_server_info() assert server_info == ServerInfo( name='test_server', version='1.2.3') - @pytest.mark.usefixtures('val_server') - async def test_get_server_info_unavailable(self, unused_tcp_port, val_servicer): - val_servicer.GetServerInfo.side_effect = generate_error( - grpc.StatusCode.UNAVAILABLE, 'Unavailable') + @pytest.mark.usefixtures("val_server") + async def test_get_server_info_unavailable(self, unused_tcp_port, val_servicer_v1): + val_servicer_v1.GetServerInfo.side_effect = generate_error( + grpc.StatusCode.UNAVAILABLE, "Unavailable" + ) async with VSSClient('127.0.0.1', unused_tcp_port, ensure_startup_connection=False) as client: with pytest.raises(VSSClientError): await client.get_server_info() @@ -1236,80 +1881,237 @@ async def test_get_server_info_unavailable(self, unused_tcp_port, val_servicer): @pytest.mark.asyncio class TestSubscriberManager: - @pytest.mark.usefixtures('val_server') - async def test_add_subscriber(self, mocker, unused_tcp_port, val_servicer): + + @pytest.mark.usefixtures("val_server") + async def test_add_subscriber_v1(self, mocker, unused_tcp_port, val_servicer_v1): async with VSSClient('127.0.0.1', unused_tcp_port, ensure_startup_connection=False) as client: subscriber_manager = SubscriberManager(client) responses = ( # 1st response is subscription ack - val_pb2.SubscribeResponse(updates=[ - val_pb2.EntryUpdate(entry=types_pb2.DataEntry( - path='Vehicle.Speed', - value=types_pb2.Datapoint( - timestamp=timestamp_pb2.Timestamp( - seconds=1667837915, nanos=247307674), - float=42.0, - ), - ), fields=(Field.VALUE,)), - ]), + val_v1.SubscribeResponse( + updates=[ + val_v1.EntryUpdate( + entry=types_v1.DataEntry( + path="Vehicle.Speed", + value=types_v1.Datapoint( + timestamp=timestamp_pb2.Timestamp( + seconds=1667837915, nanos=247307674 + ), + float=42.0, + ), + ), + fields=(Field.VALUE,), + ), + ] + ), # Remaining responses are actual events that should invoke callback. - val_pb2.SubscribeResponse(updates=[ - val_pb2.EntryUpdate(entry=types_pb2.DataEntry( - path='Vehicle.Speed', - value=types_pb2.Datapoint( - timestamp=timestamp_pb2.Timestamp( - seconds=1667837912, nanos=247307674), - float=43.0, - ), - ), fields=(Field.VALUE,)), - ]), + val_v1.SubscribeResponse( + updates=[ + val_v1.EntryUpdate( + entry=types_v1.DataEntry( + path="Vehicle.Speed", + value=types_v1.Datapoint( + timestamp=timestamp_pb2.Timestamp( + seconds=1667837912, nanos=247307674 + ), + float=43.0, + ), + ), + fields=(Field.VALUE,), + ), + ] + ), ) callback = mocker.Mock() - val_servicer.Subscribe.return_value = ( - response for response in responses) + val_servicer_v1.Subscribe.return_value = ( + response for response in responses + ) - subscribe_response_stream = client.subscribe(entries=( - EntryRequest('Vehicle.Speed', - View.CURRENT_VALUE, (Field.VALUE,)), - )) - sub_uid = await subscriber_manager.add_subscriber(subscribe_response_stream, callback=callback) + subscribe_response_stream = client.subscribe( + entries=( + EntryRequest("Vehicle.Speed", View.CURRENT_VALUE, (Field.VALUE,)), + ) + ) + sub_uid = await subscriber_manager.add_subscriber( + subscribe_response_stream, callback=callback + ) assert isinstance(sub_uid, uuid.UUID) while callback.call_count < 1: await asyncio.sleep(0.01) - actual_updates = [list(call_args[0][0]) - for call_args in callback.call_args_list] + actual_updates = [ + list(call_args[0][0]) for call_args in callback.call_args_list + ] + assert actual_updates == [ - [EntryUpdate(entry=DataEntry(path='Vehicle.Speed', value=Datapoint( - value=43.0, - timestamp=datetime.datetime( - 2022, 11, 7, 16, 18, 32, 247307, tzinfo=datetime.timezone.utc), - )), fields=[Field.VALUE])], + [ + EntryUpdate( + entry=DataEntry( + path="Vehicle.Speed", + value=Datapoint( + value=43.0, + timestamp=datetime.datetime( + 2022, + 11, + 7, + 16, + 18, + 32, + 247307, + tzinfo=datetime.timezone.utc, + ), + ), + ), + fields=[Field.VALUE], + ) + ], ] - @pytest.mark.usefixtures('val_server') - async def test_remove_subscriber(self, mocker, unused_tcp_port, val_servicer): - async with VSSClient('127.0.0.1', unused_tcp_port, ensure_startup_connection=False) as client: + @pytest.mark.usefixtures("val_server") + async def test_remove_subscriber_v1(self, mocker, unused_tcp_port, val_servicer_v1): + async with VSSClient( + "127.0.0.1", unused_tcp_port, ensure_startup_connection=False + ) as client: subscriber_manager = SubscriberManager(client) responses = ( - val_pb2.SubscribeResponse(updates=[ - val_pb2.EntryUpdate(entry=types_pb2.DataEntry( - path='Vehicle.Speed', - value=types_pb2.Datapoint( - timestamp=timestamp_pb2.Timestamp( - seconds=1667837915, nanos=247307674), - float=42.0, - ), - ), fields=(Field.VALUE,)), - ]), + val_v1.SubscribeResponse( + updates=[ + val_v1.EntryUpdate( + entry=types_v1.DataEntry( + path="Vehicle.Speed", + value=types_v1.Datapoint( + timestamp=timestamp_pb2.Timestamp( + seconds=1667837915, nanos=247307674 + ), + float=42.0, + ), + ), + fields=(Field.VALUE,), + ), + ] + ), + ) + val_servicer_v1.Subscribe.return_value = ( + response for response in responses + ) + subscribe_response_stream = client.subscribe( + entries=( + EntryRequest("Vehicle.Speed", View.CURRENT_VALUE, (Field.VALUE,)), + ) + ) + sub_uid = await subscriber_manager.add_subscriber( + subscribe_response_stream, callback=mocker.Mock() + ) + subscriber = subscriber_manager.subscribers.get(sub_uid) + + await subscriber_manager.remove_subscriber(sub_uid) + + assert subscriber_manager.subscribers.get(sub_uid) is None + assert subscriber.done() + + with pytest.raises(ValueError) as exc_info: + await subscriber_manager.remove_subscriber(sub_uid) + assert ( + exc_info.value.args[0] == f"Could not find subscription {str(sub_uid)}" + ) + + @pytest.mark.usefixtures("val_server") + async def test_add_subscriber_v2(self, mocker, unused_tcp_port, val_servicer_v2): + _entries: Dict[str, types_v2.Datapoint] = { + "Vehicle.Speed": types_v2.Datapoint( + timestamp=timestamp_pb2.Timestamp(seconds=1667837915, nanos=247307674), + value=types_v2.Value(float=42.0), + ), + } + _entries_2: Dict[str, types_v2.Datapoint] = { + "Vehicle.Speed": types_v2.Datapoint( + timestamp=timestamp_pb2.Timestamp(seconds=1667837912, nanos=247307674), + value=types_v2.Value(float=43.0), + ), + } + async with VSSClient( + "127.0.0.1", unused_tcp_port, ensure_startup_connection=False + ) as client: + subscriber_manager = SubscriberManager(client) + responses = ( + # 1st response is subscription ack + val_v2.SubscribeResponse(entries=_entries), + # Remaining responses are actual events that should invoke callback. + val_v2.SubscribeResponse(entries=_entries_2), + ) + callback = mocker.Mock() + val_servicer_v2.Subscribe.return_value = ( + response for response in responses + ) + + subscribe_response_stream = client.subscribe( + entries=( + EntryRequest("Vehicle.Speed", View.CURRENT_VALUE, (Field.VALUE,)), + ), + v1=False, + ) + sub_uid = await subscriber_manager.add_subscriber( + subscribe_response_stream, callback=callback + ) + + assert isinstance(sub_uid, uuid.UUID) + while callback.call_count < 1: + await asyncio.sleep(0.01) + actual_updates = [ + list(call_args[0][0]) for call_args in callback.call_args_list + ] + + assert actual_updates == [ + [ + EntryUpdate( + entry=DataEntry( + path="Vehicle.Speed", + value=Datapoint( + value=43.0, + timestamp=datetime.datetime( + 2022, + 11, + 7, + 16, + 18, + 32, + 247307, + tzinfo=datetime.timezone.utc, + ), + ), + ), + fields=[Field.VALUE], + ) + ], + ] + + @pytest.mark.usefixtures("val_server") + async def test_remove_subscriber_v2(self, mocker, unused_tcp_port, val_servicer_v2): + async with VSSClient( + "127.0.0.1", unused_tcp_port, ensure_startup_connection=False + ) as client: + subscriber_manager = SubscriberManager(client) + _entries: Dict[str, types_v2.Datapoint] = { + "Vehicle.Speed": types_v2.Datapoint( + timestamp=timestamp_pb2.Timestamp( + seconds=1667837915, nanos=247307674 + ), + value=types_v2.Value(float=42.0), + ), + } + responses = (val_v2.SubscribeResponse(entries=_entries),) + val_servicer_v2.Subscribe.return_value = ( + response for response in responses + ) + subscribe_response_stream = client.subscribe( + entries=( + EntryRequest("Vehicle.Speed", View.CURRENT_VALUE, (Field.VALUE,)), + ), + v1=False, + ) + sub_uid = await subscriber_manager.add_subscriber( + subscribe_response_stream, callback=mocker.Mock() ) - val_servicer.Subscribe.return_value = ( - response for response in responses) - subscribe_response_stream = client.subscribe(entries=( - EntryRequest('Vehicle.Speed', - View.CURRENT_VALUE, (Field.VALUE,)), - )) - sub_uid = await subscriber_manager.add_subscriber(subscribe_response_stream, callback=mocker.Mock()) subscriber = subscriber_manager.subscribers.get(sub_uid) await subscriber_manager.remove_subscriber(sub_uid) @@ -1319,4 +2121,6 @@ async def test_remove_subscriber(self, mocker, unused_tcp_port, val_servicer): with pytest.raises(ValueError) as exc_info: await subscriber_manager.remove_subscriber(sub_uid) - assert exc_info.value.args[0] == f"Could not find subscription {str(sub_uid)}" + assert ( + exc_info.value.args[0] == f"Could not find subscription {str(sub_uid)}" + )