From 197ed24cd8e1a48f1365609542436469c231ac08 Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Wed, 10 Apr 2024 11:21:43 -0300 Subject: [PATCH 01/10] Update dependencies to use betterproto Signed-off-by: Leandro Lucarella --- pyproject.toml | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7691277..58fefe6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,11 +2,7 @@ # Copyright © 2024 Frequenz Energy-as-a-Service GmbH [build-system] -requires = [ - "setuptools == 68.1.0", - "setuptools_scm[toml] == 7.1.0", - "frequenz-repo-config[lib] == 0.9.1", -] +requires = ["setuptools == 68.1.0", "setuptools_scm[toml] == 7.1.0"] build-backend = "setuptools.build_meta" [project] @@ -36,11 +32,10 @@ classifiers = [ ] requires-python = ">= 3.11, < 4" dependencies = [ - "frequenz-api-microgrid >= 0.15.3, < 0.16.0", + "betterproto == 2.0.0b6", "frequenz-channels >= 1.0.0-rc1, < 2.0.0", "frequenz-client-base[grpclib] >= 0.4.0, < 0.5", - "grpcio >= 1.54.2, < 2", - "protobuf >= 4.21.6, < 6", + "frequenz-microgrid-betterproto >= 0.15.3.1, < 0.16", "timezonefinder >= 6.2.0, < 7", "typing-extensions >= 4.5.0, < 5", ] From e1594173754e74ae7df6153006f2a8a91eeeddf9 Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Thu, 11 Apr 2024 14:37:54 -0300 Subject: [PATCH 02/10] Migrate the client code to use betterproto Some notable changes: * The imports can be greatly simplified as now we can import whole modules and avoid clashes without having to create one alias per symbol. * We don't need to cast awaitable functions anymore as betterproto has the correct type hints * Because of the above, a bug was uncovered and fixed, where an `await` was missing for the API call to `AddInclusionBounds`. I'm not sure if this call was not working at all or just errors wouldn't be detected. Signed-off-by: Leandro Lucarella --- src/frequenz/client/microgrid/_client.py | 139 +++++++----------- src/frequenz/client/microgrid/_component.py | 47 +++--- .../client/microgrid/_component_data.py | 48 +++--- .../client/microgrid/_component_states.py | 44 +++--- 4 files changed, 116 insertions(+), 162 deletions(-) diff --git a/src/frequenz/client/microgrid/_client.py b/src/frequenz/client/microgrid/_client.py index 47659fc..4962fc6 100644 --- a/src/frequenz/client/microgrid/_client.py +++ b/src/frequenz/client/microgrid/_client.py @@ -5,34 +5,19 @@ import asyncio import logging -from collections.abc import AsyncIterator, Awaitable, Callable, Iterable -from typing import Any, TypeVar, cast - -import grpc.aio - -# pylint: disable=no-name-in-module -from frequenz.api.common.components_pb2 import ComponentCategory as PbComponentCategory -from frequenz.api.common.metrics_pb2 import Bounds as PbBounds -from frequenz.api.microgrid.microgrid_pb2 import ComponentData as PbComponentData -from frequenz.api.microgrid.microgrid_pb2 import ComponentFilter as PbComponentFilter -from frequenz.api.microgrid.microgrid_pb2 import ComponentIdParam as PbComponentIdParam -from frequenz.api.microgrid.microgrid_pb2 import ComponentList as PbComponentList -from frequenz.api.microgrid.microgrid_pb2 import ConnectionFilter as PbConnectionFilter -from frequenz.api.microgrid.microgrid_pb2 import ConnectionList as PbConnectionList -from frequenz.api.microgrid.microgrid_pb2 import ( - MicrogridMetadata as PbMicrogridMetadata, -) -from frequenz.api.microgrid.microgrid_pb2 import SetBoundsParam as PbSetBoundsParam -from frequenz.api.microgrid.microgrid_pb2 import ( - SetPowerActiveParam as PbSetPowerActiveParam, -) -from frequenz.api.microgrid.microgrid_pb2_grpc import MicrogridStub +from collections.abc import Callable, Iterable +from typing import Any, TypeVar -# pylint: enable=no-name-in-module +import grpclib +import grpclib.client +from betterproto.lib.google import protobuf as pb_google from frequenz.channels import Receiver from frequenz.client.base import retry, streaming -from google.protobuf.empty_pb2 import Empty # pylint: disable=no-name-in-module -from google.protobuf.timestamp_pb2 import Timestamp # pylint: disable=no-name-in-module +from frequenz.microgrid.betterproto.frequenz.api import microgrid as pb_microgrid +from frequenz.microgrid.betterproto.frequenz.api.common import ( + components as pb_components, +) +from frequenz.microgrid.betterproto.frequenz.api.common import metrics as pb_metrics from ._component import ( Component, @@ -67,7 +52,7 @@ class ApiClient: def __init__( self, - grpc_channel: grpc.aio.Channel, + grpc_channel: grpclib.client.Channel, target: str, retry_strategy: retry.Strategy | None = None, ) -> None: @@ -84,7 +69,7 @@ def __init__( self.target = target """The location (as "host:port") of the microgrid API gRPC server.""" - self.api = MicrogridStub(grpc_channel) + self.api = pb_microgrid.MicrogridStub(grpc_channel) """The gRPC stub for the microgrid API.""" self._broadcasters: dict[int, streaming.GrpcStreamBroadcaster[Any, Any]] = {} @@ -101,22 +86,19 @@ async def components(self) -> Iterable[Component]: when the api call exceeded the timeout. """ try: - # grpc.aio is missing types and mypy thinks this is not awaitable, - # but it is - component_list = await cast( - Awaitable[PbComponentList], - self.api.ListComponents( - PbComponentFilter(), - timeout=int(DEFAULT_GRPC_CALL_TIMEOUT), - ), + component_list = await self.api.list_components( + pb_microgrid.ComponentFilter(), + timeout=int(DEFAULT_GRPC_CALL_TIMEOUT), ) - except grpc.aio.AioRpcError as err: + except grpclib.GRPCError as err: raise ClientError( - f"Failed to list components. Microgrid API: {self.target}. Err: {err.details()}" + f"Failed to list components. Microgrid API: {self.target}. Err: {err}" ) from err + components_only = filter( - lambda c: c.category is not PbComponentCategory.COMPONENT_CATEGORY_SENSOR, + lambda c: c.category + is not pb_components.ComponentCategory.COMPONENT_CATEGORY_SENSOR, component_list.components, ) result: Iterable[Component] = map( @@ -140,16 +122,13 @@ async def metadata(self) -> Metadata: Returns: the microgrid metadata. """ - microgrid_metadata: PbMicrogridMetadata | None = None + microgrid_metadata: pb_microgrid.MicrogridMetadata | None = None try: - microgrid_metadata = await cast( - Awaitable[PbMicrogridMetadata], - self.api.GetMicrogridMetadata( - Empty(), - timeout=int(DEFAULT_GRPC_CALL_TIMEOUT), - ), + microgrid_metadata = await self.api.get_microgrid_metadata( + pb_google.Empty(), + timeout=int(DEFAULT_GRPC_CALL_TIMEOUT), ) - except grpc.aio.AioRpcError: + except grpclib.GRPCError: _logger.exception("The microgrid metadata is not available.") if not microgrid_metadata: @@ -184,23 +163,18 @@ async def connections( ClientError: If the connection to the Microgrid API cannot be established or when the api call exceeded the timeout. """ - connection_filter = PbConnectionFilter(starts=starts, ends=ends) + connection_filter = pb_microgrid.ConnectionFilter(starts=starts, ends=ends) try: valid_components, all_connections = await asyncio.gather( self.components(), - # grpc.aio is missing types and mypy thinks this is not - # awaitable, but it is - cast( - Awaitable[PbConnectionList], - self.api.ListConnections( - connection_filter, - timeout=int(DEFAULT_GRPC_CALL_TIMEOUT), - ), + self.api.list_connections( + connection_filter, + timeout=int(DEFAULT_GRPC_CALL_TIMEOUT), ), ) - except grpc.aio.AioRpcError as err: + except grpclib.GRPCError as err: raise ClientError( - f"Failed to list connections. Microgrid API: {self.target}. Err: {err.details()}" + f"Failed to list connections. Microgrid API: {self.target}. Err: {err}" ) from err # Filter out the components filtered in `components` method. # id=0 is an exception indicating grid component. @@ -223,7 +197,7 @@ async def _new_component_data_receiver( *, component_id: int, expected_category: ComponentCategory, - transform: Callable[[PbComponentData], _ComponentDataT], + transform: Callable[[pb_microgrid.ComponentData], _ComponentDataT], maxsize: int, ) -> Receiver[_ComponentDataT]: """Return a new broadcaster receiver for a given `component_id`. @@ -250,13 +224,8 @@ async def _new_component_data_receiver( if broadcaster is None: broadcaster = streaming.GrpcStreamBroadcaster( f"raw-component-data-{component_id}", - # We need to cast here because grpc says StreamComponentData is - # a grpc.CallIterator[PbComponentData] which is not an AsyncIterator, - # but it is a grpc.aio.UnaryStreamCall[..., PbComponentData], which it - # is. - lambda: cast( - AsyncIterator[PbComponentData], - self.api.StreamComponentData(PbComponentIdParam(id=component_id)), + lambda: self.api.stream_component_data( + pb_microgrid.ComponentIdParam(id=component_id) ), transform, retry_strategy=self._retry_strategy, @@ -409,16 +378,15 @@ async def set_power(self, component_id: int, power_w: float) -> None: when the api call exceeded the timeout. """ try: - await cast( - Awaitable[Empty], - self.api.SetPowerActive( - PbSetPowerActiveParam(component_id=component_id, power=power_w), - timeout=int(DEFAULT_GRPC_CALL_TIMEOUT), + await self.api.set_power_active( + pb_microgrid.SetPowerActiveParam( + component_id=component_id, power=power_w ), + timeout=int(DEFAULT_GRPC_CALL_TIMEOUT), ) - except grpc.aio.AioRpcError as err: + except grpclib.GRPCError as err: raise ClientError( - f"Failed to set power. Microgrid API: {self.target}. Err: {err.details()}" + f"Failed to set power. Microgrid API: {self.target}. Err: {err}" ) from err async def set_bounds( @@ -427,7 +395,7 @@ async def set_bounds( lower: float, upper: float, ) -> None: - """Send `PbSetBoundsParam`s received from a channel to the Microgrid service. + """Send `SetBoundsParam`s received from a channel to the Microgrid service. Args: component_id: ID of the component to set bounds for. @@ -446,28 +414,27 @@ async def set_bounds( if lower > 0: raise ValueError(f"Lower bound {lower} must be less than or equal to 0.") - target_metric = PbSetBoundsParam.TargetMetric.TARGET_METRIC_POWER_ACTIVE + target_metric = ( + pb_microgrid.SetBoundsParamTargetMetric.TARGET_METRIC_POWER_ACTIVE + ) try: - await cast( - Awaitable[Timestamp], - self.api.AddInclusionBounds( - PbSetBoundsParam( - component_id=component_id, - target_metric=target_metric, - bounds=PbBounds(lower=lower, upper=upper), - ), - timeout=int(DEFAULT_GRPC_CALL_TIMEOUT), + await self.api.add_inclusion_bounds( + pb_microgrid.SetBoundsParam( + component_id=component_id, + target_metric=target_metric, + bounds=pb_metrics.Bounds(lower=lower, upper=upper), ), + timeout=int(DEFAULT_GRPC_CALL_TIMEOUT), ) - except grpc.aio.AioRpcError as err: + except grpclib.GRPCError as err: _logger.error( "set_bounds write failed: %s, for message: %s, api: %s. Err: %s", err, next, api_details, - err.details(), + err, ) raise ClientError( f"Failed to set inclusion bounds. Microgrid API: {self.target}. " - f"Err: {err.details()}" + f"Err: {err}" ) from err diff --git a/src/frequenz/client/microgrid/_component.py b/src/frequenz/client/microgrid/_component.py index 2775eea..96e6cc2 100644 --- a/src/frequenz/client/microgrid/_component.py +++ b/src/frequenz/client/microgrid/_component.py @@ -6,13 +6,8 @@ from dataclasses import dataclass from enum import Enum -# pylint: disable=no-name-in-module -from frequenz.api.common.components_pb2 import ComponentCategory as PbComponentCategory -from frequenz.api.microgrid.grid_pb2 import Metadata as PbGridMetadata -from frequenz.api.microgrid.inverter_pb2 import Metadata as PbInverterMetadata -from frequenz.api.microgrid.inverter_pb2 import Type as PbInverterType - -# pylint: enable=no-name-in-module +from frequenz.microgrid.betterproto.frequenz.api.common import components +from frequenz.microgrid.betterproto.frequenz.api.microgrid import grid, inverter class ComponentType(Enum): @@ -22,22 +17,22 @@ class ComponentType(Enum): class InverterType(ComponentType): """Enum representing inverter types.""" - NONE = PbInverterType.TYPE_UNSPECIFIED + NONE = inverter.Type.TYPE_UNSPECIFIED """Unspecified inverter type.""" - BATTERY = PbInverterType.TYPE_BATTERY + BATTERY = inverter.Type.TYPE_BATTERY """Battery inverter.""" - SOLAR = PbInverterType.TYPE_SOLAR + SOLAR = inverter.Type.TYPE_SOLAR """Solar inverter.""" - HYBRID = PbInverterType.TYPE_HYBRID + HYBRID = inverter.Type.TYPE_HYBRID """Hybrid inverter.""" def component_type_from_protobuf( - component_category: PbComponentCategory.ValueType, - component_metadata: PbInverterMetadata, + component_category: components.ComponentCategory, + component_metadata: inverter.Metadata, ) -> ComponentType | None: """Convert a protobuf InverterType message to Component enum. @@ -53,7 +48,7 @@ def component_type_from_protobuf( # ComponentType values in the protobuf definition are not unique across categories # as of v0.11.0, so we need to check the component category first, before doing any # component type checks. - if component_category == PbComponentCategory.COMPONENT_CATEGORY_INVERTER: + if component_category == components.ComponentCategory.COMPONENT_CATEGORY_INVERTER: if not any(int(t.value) == int(component_metadata.type) for t in InverterType): return None @@ -65,30 +60,30 @@ def component_type_from_protobuf( class ComponentCategory(Enum): """Possible types of microgrid component.""" - NONE = PbComponentCategory.COMPONENT_CATEGORY_UNSPECIFIED + NONE = components.ComponentCategory.COMPONENT_CATEGORY_UNSPECIFIED """Unspecified component category.""" - GRID = PbComponentCategory.COMPONENT_CATEGORY_GRID + GRID = components.ComponentCategory.COMPONENT_CATEGORY_GRID """Grid component.""" - METER = PbComponentCategory.COMPONENT_CATEGORY_METER + METER = components.ComponentCategory.COMPONENT_CATEGORY_METER """Meter component.""" - INVERTER = PbComponentCategory.COMPONENT_CATEGORY_INVERTER + INVERTER = components.ComponentCategory.COMPONENT_CATEGORY_INVERTER """Inverter component.""" - BATTERY = PbComponentCategory.COMPONENT_CATEGORY_BATTERY + BATTERY = components.ComponentCategory.COMPONENT_CATEGORY_BATTERY """Battery component.""" - EV_CHARGER = PbComponentCategory.COMPONENT_CATEGORY_EV_CHARGER + EV_CHARGER = components.ComponentCategory.COMPONENT_CATEGORY_EV_CHARGER """EV charger component.""" - CHP = PbComponentCategory.COMPONENT_CATEGORY_CHP + CHP = components.ComponentCategory.COMPONENT_CATEGORY_CHP """CHP component.""" def component_category_from_protobuf( - component_category: PbComponentCategory.ValueType, + component_category: components.ComponentCategory, ) -> ComponentCategory: """Convert a protobuf ComponentCategory message to ComponentCategory enum. @@ -105,7 +100,7 @@ def component_category_from_protobuf( a valid component category as it does not form part of the microgrid itself) """ - if component_category == PbComponentCategory.COMPONENT_CATEGORY_SENSOR: + if component_category == components.ComponentCategory.COMPONENT_CATEGORY_SENSOR: raise ValueError("Cannot create a component from a sensor!") if not any(t.value == component_category for t in ComponentCategory): @@ -136,8 +131,8 @@ class GridMetadata(ComponentMetadata): def component_metadata_from_protobuf( - component_category: PbComponentCategory.ValueType, - component_metadata: PbGridMetadata, + component_category: components.ComponentCategory, + component_metadata: grid.Metadata, ) -> GridMetadata | None: """Convert a protobuf GridMetadata message to GridMetadata class. @@ -150,7 +145,7 @@ def component_metadata_from_protobuf( Returns: GridMetadata instance corresponding to the protobuf message. """ - if component_category == PbComponentCategory.COMPONENT_CATEGORY_GRID: + if component_category == components.ComponentCategory.COMPONENT_CATEGORY_GRID: max_current = component_metadata.rated_fuse_current fuse = Fuse(max_current) return GridMetadata(fuse) diff --git a/src/frequenz/client/microgrid/_component_data.py b/src/frequenz/client/microgrid/_component_data.py index 71a652e..8e21715 100644 --- a/src/frequenz/client/microgrid/_component_data.py +++ b/src/frequenz/client/microgrid/_component_data.py @@ -5,20 +5,12 @@ from abc import ABC, abstractmethod from dataclasses import dataclass, field -from datetime import datetime, timezone +from datetime import datetime from typing import Self -# pylint: disable=no-name-in-module -from frequenz.api.microgrid.battery_pb2 import ComponentState as PbBatteryComponentState -from frequenz.api.microgrid.battery_pb2 import Error as PbBatteryError -from frequenz.api.microgrid.battery_pb2 import RelayState as PbBatteryRelayState -from frequenz.api.microgrid.inverter_pb2 import ( - ComponentState as PbInverterComponentState, -) -from frequenz.api.microgrid.inverter_pb2 import Error as PbInverterError -from frequenz.api.microgrid.microgrid_pb2 import ComponentData as PbComponentData - -# pylint: enable=no-name-in-module +from frequenz.microgrid.betterproto.frequenz.api import microgrid +from frequenz.microgrid.betterproto.frequenz.api.microgrid import battery, inverter + from ._component_states import EVChargerCableState, EVChargerComponentState @@ -37,10 +29,10 @@ class ComponentData(ABC): # data from a protobuf message. The whole protobuf message is stored as the `raw` # attribute. When `ComponentData` is not instantiated from a protobuf message, # i.e. using the constructor, `raw` will be set to `None`. - raw: PbComponentData | None = field(default=None, init=False) + raw: microgrid.ComponentData | None = field(default=None, init=False) """Raw component data as decoded from the wire.""" - def _set_raw(self, raw: PbComponentData) -> None: + def _set_raw(self, raw: microgrid.ComponentData) -> None: """Store raw protobuf message. It is preferred to keep the dataclasses immutable (frozen) and make the `raw` @@ -54,7 +46,7 @@ def _set_raw(self, raw: PbComponentData) -> None: @classmethod @abstractmethod - def from_proto(cls, raw: PbComponentData) -> Self: + def from_proto(cls, raw: microgrid.ComponentData) -> Self: """Create ComponentData from a protobuf message. Args: @@ -121,7 +113,7 @@ class MeterData(ComponentData): """The AC power frequency in Hertz (Hz).""" @classmethod - def from_proto(cls, raw: PbComponentData) -> Self: + def from_proto(cls, raw: microgrid.ComponentData) -> Self: """Create MeterData from a protobuf message. Args: @@ -132,7 +124,7 @@ def from_proto(cls, raw: PbComponentData) -> Self: """ meter_data = cls( component_id=raw.id, - timestamp=raw.ts.ToDatetime(tzinfo=timezone.utc), + timestamp=raw.ts, active_power=raw.meter.data.ac.power_active.value, active_power_per_phase=( raw.meter.data.ac.phase_1.power_active.value, @@ -230,17 +222,17 @@ class BatteryData(ComponentData): # pylint: disable=too-many-instance-attribute temperature: float """The (average) temperature reported by the battery, in Celsius (°C).""" - _relay_state: PbBatteryRelayState.ValueType + _relay_state: battery.RelayState """State of the battery relay.""" - _component_state: PbBatteryComponentState.ValueType + _component_state: battery.ComponentState """State of the battery.""" - _errors: list[PbBatteryError] + _errors: list[battery.Error] """List of errors in protobuf struct.""" @classmethod - def from_proto(cls, raw: PbComponentData) -> Self: + def from_proto(cls, raw: microgrid.ComponentData) -> Self: """Create BatteryData from a protobuf message. Args: @@ -252,7 +244,7 @@ def from_proto(cls, raw: PbComponentData) -> Self: raw_power = raw.battery.data.dc.power battery_data = cls( component_id=raw.id, - timestamp=raw.ts.ToDatetime(tzinfo=timezone.utc), + timestamp=raw.ts, soc=raw.battery.data.soc.avg, soc_lower_bound=raw.battery.data.soc.system_inclusion_bounds.lower, soc_upper_bound=raw.battery.data.soc.system_inclusion_bounds.upper, @@ -371,14 +363,14 @@ class InverterData(ComponentData): # pylint: disable=too-many-instance-attribut frequency: float """AC frequency, in Hertz (Hz).""" - _component_state: PbInverterComponentState.ValueType + _component_state: inverter.ComponentState """State of the inverter.""" - _errors: list[PbInverterError] + _errors: list[inverter.Error] """List of errors from the component.""" @classmethod - def from_proto(cls, raw: PbComponentData) -> Self: + def from_proto(cls, raw: microgrid.ComponentData) -> Self: """Create InverterData from a protobuf message. Args: @@ -390,7 +382,7 @@ def from_proto(cls, raw: PbComponentData) -> Self: raw_power = raw.inverter.data.ac.power_active inverter_data = cls( component_id=raw.id, - timestamp=raw.ts.ToDatetime(tzinfo=timezone.utc), + timestamp=raw.ts, active_power=raw.inverter.data.ac.power_active.value, active_power_per_phase=( raw.inverter.data.ac.phase_1.power_active.value, @@ -532,7 +524,7 @@ class EVChargerData(ComponentData): # pylint: disable=too-many-instance-attribu """The state of the ev charger.""" @classmethod - def from_proto(cls, raw: PbComponentData) -> Self: + def from_proto(cls, raw: microgrid.ComponentData) -> Self: """Create EVChargerData from a protobuf message. Args: @@ -544,7 +536,7 @@ def from_proto(cls, raw: PbComponentData) -> Self: raw_power = raw.ev_charger.data.ac.power_active ev_charger_data = cls( component_id=raw.id, - timestamp=raw.ts.ToDatetime(tzinfo=timezone.utc), + timestamp=raw.ts, active_power=raw_power.value, active_power_per_phase=( raw.ev_charger.data.ac.phase_1.power_active.value, diff --git a/src/frequenz/client/microgrid/_component_states.py b/src/frequenz/client/microgrid/_component_states.py index 855641f..d3eb223 100644 --- a/src/frequenz/client/microgrid/_component_states.py +++ b/src/frequenz/client/microgrid/_component_states.py @@ -6,36 +6,34 @@ from enum import Enum from typing import Self -# pylint: disable=no-name-in-module -from frequenz.api.microgrid.ev_charger_pb2 import CableState as PbCableState -from frequenz.api.microgrid.ev_charger_pb2 import ComponentState as PbComponentState - -# pylint: enable=no-name-in-module +from frequenz.microgrid.betterproto.frequenz.api.microgrid import ev_charger class EVChargerCableState(Enum): """Cable states of an EV Charger.""" - UNSPECIFIED = PbCableState.CABLE_STATE_UNSPECIFIED + UNSPECIFIED = ev_charger.CableState.CABLE_STATE_UNSPECIFIED """Unspecified cable state.""" - UNPLUGGED = PbCableState.CABLE_STATE_UNPLUGGED + UNPLUGGED = ev_charger.CableState.CABLE_STATE_UNPLUGGED """The cable is unplugged.""" - CHARGING_STATION_PLUGGED = PbCableState.CABLE_STATE_CHARGING_STATION_PLUGGED + CHARGING_STATION_PLUGGED = ( + ev_charger.CableState.CABLE_STATE_CHARGING_STATION_PLUGGED + ) """The cable is plugged into the charging station.""" - CHARGING_STATION_LOCKED = PbCableState.CABLE_STATE_CHARGING_STATION_LOCKED + CHARGING_STATION_LOCKED = ev_charger.CableState.CABLE_STATE_CHARGING_STATION_LOCKED """The cable is plugged into the charging station and locked.""" - EV_PLUGGED = PbCableState.CABLE_STATE_EV_PLUGGED + EV_PLUGGED = ev_charger.CableState.CABLE_STATE_EV_PLUGGED """The cable is plugged into the EV.""" - EV_LOCKED = PbCableState.CABLE_STATE_EV_LOCKED + EV_LOCKED = ev_charger.CableState.CABLE_STATE_EV_LOCKED """The cable is plugged into the EV and locked.""" @classmethod - def from_pb(cls, evc_state: PbCableState.ValueType) -> Self: + def from_pb(cls, evc_state: ev_charger.CableState) -> Self: """Convert a protobuf CableState value to EVChargerCableState enum. Args: @@ -53,35 +51,37 @@ def from_pb(cls, evc_state: PbCableState.ValueType) -> Self: class EVChargerComponentState(Enum): """Component State of an EV Charger.""" - UNSPECIFIED = PbComponentState.COMPONENT_STATE_UNSPECIFIED + UNSPECIFIED = ev_charger.ComponentState.COMPONENT_STATE_UNSPECIFIED """Unspecified component state.""" - STARTING = PbComponentState.COMPONENT_STATE_STARTING + STARTING = ev_charger.ComponentState.COMPONENT_STATE_STARTING """The component is starting.""" - NOT_READY = PbComponentState.COMPONENT_STATE_NOT_READY + NOT_READY = ev_charger.ComponentState.COMPONENT_STATE_NOT_READY """The component is not ready.""" - READY = PbComponentState.COMPONENT_STATE_READY + READY = ev_charger.ComponentState.COMPONENT_STATE_READY """The component is ready.""" - CHARGING = PbComponentState.COMPONENT_STATE_CHARGING + CHARGING = ev_charger.ComponentState.COMPONENT_STATE_CHARGING """The component is charging.""" - DISCHARGING = PbComponentState.COMPONENT_STATE_DISCHARGING + DISCHARGING = ev_charger.ComponentState.COMPONENT_STATE_DISCHARGING """The component is discharging.""" - ERROR = PbComponentState.COMPONENT_STATE_ERROR + ERROR = ev_charger.ComponentState.COMPONENT_STATE_ERROR """The component is in error state.""" - AUTHORIZATION_REJECTED = PbComponentState.COMPONENT_STATE_AUTHORIZATION_REJECTED + AUTHORIZATION_REJECTED = ( + ev_charger.ComponentState.COMPONENT_STATE_AUTHORIZATION_REJECTED + ) """The component rejected authorization.""" - INTERRUPTED = PbComponentState.COMPONENT_STATE_INTERRUPTED + INTERRUPTED = ev_charger.ComponentState.COMPONENT_STATE_INTERRUPTED """The component is interrupted.""" @classmethod - def from_pb(cls, evc_state: PbComponentState.ValueType) -> Self: + def from_pb(cls, evc_state: ev_charger.ComponentState) -> Self: """Convert a protobuf ComponentState value to EVChargerComponentState enum. Args: From 45cef003b938b08135ec677db32cdca9e45689e3 Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Thu, 11 Apr 2024 15:23:35 -0300 Subject: [PATCH 03/10] Fix call to `list_connections` Since betterproto use type hints, it expects the `ConnectionFilter` `starts` and `end` to be `list`s. We might also just take a `list` in the `connections()` methods instead to avoid the copy, maybe ensuring that the filter elements are unique (which was probably the reason to make them a `set`) is not that important. Signed-off-by: Leandro Lucarella --- src/frequenz/client/microgrid/_client.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/frequenz/client/microgrid/_client.py b/src/frequenz/client/microgrid/_client.py index 4962fc6..55bd718 100644 --- a/src/frequenz/client/microgrid/_client.py +++ b/src/frequenz/client/microgrid/_client.py @@ -5,7 +5,7 @@ import asyncio import logging -from collections.abc import Callable, Iterable +from collections.abc import Callable, Iterable, Set from typing import Any, TypeVar import grpclib @@ -145,8 +145,8 @@ async def metadata(self) -> Metadata: async def connections( self, - starts: set[int] | None = None, - ends: set[int] | None = None, + starts: Set[int] = frozenset(), + ends: Set[int] = frozenset(), ) -> Iterable[Connection]: """Fetch the connections between components in the microgrid. @@ -163,7 +163,9 @@ async def connections( ClientError: If the connection to the Microgrid API cannot be established or when the api call exceeded the timeout. """ - connection_filter = pb_microgrid.ConnectionFilter(starts=starts, ends=ends) + connection_filter = pb_microgrid.ConnectionFilter( + starts=list(starts), ends=list(ends) + ) try: valid_components, all_connections = await asyncio.gather( self.components(), From b5b52d5ba8a986a643b763137ea10f5b747522f5 Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Mon, 13 May 2024 10:22:04 +0200 Subject: [PATCH 04/10] Fix component tests to use betterproto These tests need just minor changes to the imports. Signed-off-by: Leandro Lucarella --- tests/test_component.py | 71 ++++++++++++++------------- tests/test_component_data.py | 93 +++++++++++++++++------------------- 2 files changed, 84 insertions(+), 80 deletions(-) diff --git a/tests/test_component.py b/tests/test_component.py index 1fa3e9b..3a38652 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -4,86 +4,93 @@ """Tests for the microgrid component wrapper.""" import pytest +from frequenz.microgrid.betterproto.frequenz.api.common import components -# pylint: disable=no-name-in-module -from frequenz.api.common.components_pb2 import ComponentCategory - -# pylint: enable=no-name-in-module -import frequenz.client.microgrid._component as cp +from frequenz.client.microgrid._component import ( + Component, + ComponentCategory, + component_category_from_protobuf, +) def test_component_category_from_protobuf() -> None: """Test the creating component category from protobuf.""" assert ( - cp.component_category_from_protobuf( - ComponentCategory.COMPONENT_CATEGORY_UNSPECIFIED + component_category_from_protobuf( + components.ComponentCategory.COMPONENT_CATEGORY_UNSPECIFIED ) - == cp.ComponentCategory.NONE + == ComponentCategory.NONE ) assert ( - cp.component_category_from_protobuf(ComponentCategory.COMPONENT_CATEGORY_GRID) - == cp.ComponentCategory.GRID + component_category_from_protobuf( + components.ComponentCategory.COMPONENT_CATEGORY_GRID + ) + == ComponentCategory.GRID ) assert ( - cp.component_category_from_protobuf(ComponentCategory.COMPONENT_CATEGORY_METER) - == cp.ComponentCategory.METER + component_category_from_protobuf( + components.ComponentCategory.COMPONENT_CATEGORY_METER + ) + == ComponentCategory.METER ) assert ( - cp.component_category_from_protobuf( - ComponentCategory.COMPONENT_CATEGORY_INVERTER + component_category_from_protobuf( + components.ComponentCategory.COMPONENT_CATEGORY_INVERTER ) - == cp.ComponentCategory.INVERTER + == ComponentCategory.INVERTER ) assert ( - cp.component_category_from_protobuf( - ComponentCategory.COMPONENT_CATEGORY_BATTERY + component_category_from_protobuf( + components.ComponentCategory.COMPONENT_CATEGORY_BATTERY ) - == cp.ComponentCategory.BATTERY + == ComponentCategory.BATTERY ) assert ( - cp.component_category_from_protobuf( - ComponentCategory.COMPONENT_CATEGORY_EV_CHARGER + component_category_from_protobuf( + components.ComponentCategory.COMPONENT_CATEGORY_EV_CHARGER ) - == cp.ComponentCategory.EV_CHARGER + == ComponentCategory.EV_CHARGER ) - assert cp.component_category_from_protobuf(666) == cp.ComponentCategory.NONE # type: ignore + assert component_category_from_protobuf(666) == ComponentCategory.NONE # type: ignore with pytest.raises(ValueError): - cp.component_category_from_protobuf(ComponentCategory.COMPONENT_CATEGORY_SENSOR) + component_category_from_protobuf( + components.ComponentCategory.COMPONENT_CATEGORY_SENSOR + ) # pylint: disable=invalid-name def test_Component() -> None: """Test the component category.""" - c0 = cp.Component(0, cp.ComponentCategory.GRID) + c0 = Component(0, ComponentCategory.GRID) assert c0.is_valid() - c1 = cp.Component(1, cp.ComponentCategory.GRID) + c1 = Component(1, ComponentCategory.GRID) assert c1.is_valid() - c4 = cp.Component(4, cp.ComponentCategory.METER) + c4 = Component(4, ComponentCategory.METER) assert c4.is_valid() - c5 = cp.Component(5, cp.ComponentCategory.INVERTER) + c5 = Component(5, ComponentCategory.INVERTER) assert c5.is_valid() - c6 = cp.Component(6, cp.ComponentCategory.BATTERY) + c6 = Component(6, ComponentCategory.BATTERY) assert c6.is_valid() - c7 = cp.Component(7, cp.ComponentCategory.EV_CHARGER) + c7 = Component(7, ComponentCategory.EV_CHARGER) assert c7.is_valid() - invalid_grid_id = cp.Component(-1, cp.ComponentCategory.GRID) + invalid_grid_id = Component(-1, ComponentCategory.GRID) assert not invalid_grid_id.is_valid() - invalid_type = cp.Component(666, -1) # type: ignore + invalid_type = Component(666, -1) # type: ignore assert not invalid_type.is_valid() - another_invalid_type = cp.Component(666, 666) # type: ignore + another_invalid_type = Component(666, 666) # type: ignore assert not another_invalid_type.is_valid() diff --git a/tests/test_component_data.py b/tests/test_component_data.py index ff2f3bb..e5c46ce 100644 --- a/tests/test_component_data.py +++ b/tests/test_component_data.py @@ -6,21 +6,11 @@ from datetime import datetime, timezone import pytest +from frequenz.microgrid.betterproto.frequenz.api import microgrid +from frequenz.microgrid.betterproto.frequenz.api.common import metrics +from frequenz.microgrid.betterproto.frequenz.api.common.metrics import electrical +from frequenz.microgrid.betterproto.frequenz.api.microgrid import inverter -# pylint: disable=no-name-in-module -from frequenz.api.common.metrics.electrical_pb2 import AC -from frequenz.api.common.metrics_pb2 import Bounds, Metric -from frequenz.api.microgrid.inverter_pb2 import ( - COMPONENT_STATE_DISCHARGING, - Data, - Error, - Inverter, - State, -) -from frequenz.api.microgrid.microgrid_pb2 import ComponentData as PbComponentData -from google.protobuf.timestamp_pb2 import Timestamp - -# pylint: enable=no-name-in-module from frequenz.client.microgrid import ComponentData, InverterData @@ -35,45 +25,52 @@ def test_inverter_data() -> None: """Verify the constructor for the InverterData class.""" seconds = 1234567890 - raw = PbComponentData( + raw = microgrid.ComponentData( id=5, - ts=Timestamp(seconds=seconds), - inverter=Inverter( - state=State(component_state=COMPONENT_STATE_DISCHARGING), - errors=[Error(msg="error message")], - data=Data( - dc_battery=None, - dc_solar=None, - temperature=None, - ac=AC( - frequency=Metric(value=50.1), - power_active=Metric( + ts=datetime.fromtimestamp(seconds, timezone.utc), + inverter=inverter.Inverter( + state=inverter.State( + component_state=inverter.ComponentState.COMPONENT_STATE_DISCHARGING + ), + errors=[inverter.Error(msg="error message")], + data=inverter.Data( + ac=electrical.Ac( + frequency=metrics.Metric(value=50.1), + power_active=metrics.Metric( value=100.2, - system_exclusion_bounds=Bounds(lower=-501.0, upper=501.0), - system_inclusion_bounds=Bounds(lower=-51_000.0, upper=51_000.0), + system_exclusion_bounds=metrics.Bounds( + lower=-501.0, upper=501.0 + ), + system_inclusion_bounds=metrics.Bounds( + lower=-51_000.0, upper=51_000.0 + ), ), - power_reactive=Metric( + power_reactive=metrics.Metric( value=200.3, - system_exclusion_bounds=Bounds(lower=-502.0, upper=502.0), - system_inclusion_bounds=Bounds(lower=-52_000.0, upper=52_000.0), + system_exclusion_bounds=metrics.Bounds( + lower=-502.0, upper=502.0 + ), + system_inclusion_bounds=metrics.Bounds( + lower=-52_000.0, upper=52_000.0 + ), ), - phase_1=AC.ACPhase( - current=Metric(value=12.3), - voltage=Metric(value=229.8), - power_active=Metric(value=33.1), - power_reactive=Metric(value=10.1), + phase_1=electrical.AcAcPhase( + current=metrics.Metric(value=12.3), + voltage=metrics.Metric(value=229.8), + power_active=metrics.Metric(value=33.1), + power_reactive=metrics.Metric(value=10.1), ), - phase_2=AC.ACPhase( - current=Metric(value=23.4), - voltage=Metric(value=230.0), - power_active=Metric(value=33.3), - power_reactive=Metric(value=10.2), + phase_2=electrical.AcAcPhase( + current=metrics.Metric(value=23.4), + voltage=metrics.Metric(value=230.0), + power_active=metrics.Metric(value=33.3), + power_reactive=metrics.Metric(value=10.2), ), - phase_3=AC.ACPhase( - current=Metric(value=34.5), - voltage=Metric(value=230.2), - power_active=Metric(value=33.8), - power_reactive=Metric(value=10.3), + phase_3=electrical.AcAcPhase( + current=metrics.Metric(value=34.5), + voltage=metrics.Metric(value=230.2), + power_active=metrics.Metric(value=33.8), + power_reactive=metrics.Metric(value=10.3), ), ), ), @@ -84,10 +81,10 @@ def test_inverter_data() -> None: assert inv_data.component_id == 5 assert inv_data.timestamp == datetime.fromtimestamp(seconds, timezone.utc) assert ( # pylint: disable=protected-access - inv_data._component_state == COMPONENT_STATE_DISCHARGING + inv_data._component_state == inverter.ComponentState.COMPONENT_STATE_DISCHARGING ) assert inv_data._errors == [ # pylint: disable=protected-access - Error(msg="error message") + inverter.Error(msg="error message") ] assert inv_data.frequency == pytest.approx(50.1) assert inv_data.active_power == pytest.approx(100.2) From 41afa39e244ef3ab71eb9ac29d53a0b84ccfc107 Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Fri, 10 May 2024 10:46:18 +0200 Subject: [PATCH 05/10] Remove unnecessary tests class wrapper This only adds unnecessary extra indentation. Signed-off-by: Leandro Lucarella --- tests/test_client.py | 865 ++++++++++++++++++++++--------------------- 1 file changed, 433 insertions(+), 432 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index 0ef2d2d..d5a2780 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -76,454 +76,455 @@ async def _gprc_server( assert await server.graceful_shutdown() -class TestMicrogridGrpcClient: - """Tests for the microgrid client thin wrapper.""" - - async def test_components(self) -> None: - """Test the components() method.""" - async with _gprc_server() as (servicer, microgrid): - assert set(await microgrid.components()) == set() - - servicer.add_component(0, PbComponentCategory.COMPONENT_CATEGORY_METER) - assert set(await microgrid.components()) == { - Component(0, ComponentCategory.METER) - } - - servicer.add_component(0, PbComponentCategory.COMPONENT_CATEGORY_BATTERY) - assert set(await microgrid.components()) == { - Component(0, ComponentCategory.METER), - Component(0, ComponentCategory.BATTERY), - } - - servicer.add_component(0, PbComponentCategory.COMPONENT_CATEGORY_METER) - assert set(await microgrid.components()) == { - Component(0, ComponentCategory.METER), - Component(0, ComponentCategory.BATTERY), - Component(0, ComponentCategory.METER), - } - - # sensors are not counted as components by the API client - servicer.add_component(1, PbComponentCategory.COMPONENT_CATEGORY_SENSOR) - assert set(await microgrid.components()) == { - Component(0, ComponentCategory.METER), - Component(0, ComponentCategory.BATTERY), - Component(0, ComponentCategory.METER), - } - - servicer.set_components( - [ - (9, PbComponentCategory.COMPONENT_CATEGORY_METER), - (99, PbComponentCategory.COMPONENT_CATEGORY_INVERTER), - (666, PbComponentCategory.COMPONENT_CATEGORY_SENSOR), - (999, PbComponentCategory.COMPONENT_CATEGORY_BATTERY), - ] - ) - assert set(await microgrid.components()) == { - Component(9, ComponentCategory.METER), - Component(99, ComponentCategory.INVERTER, InverterType.NONE), - Component(999, ComponentCategory.BATTERY), - } - - servicer.set_components( - [ - (99, PbComponentCategory.COMPONENT_CATEGORY_SENSOR), - ( - 100, - PbComponentCategory.COMPONENT_CATEGORY_UNSPECIFIED, - ), - (104, PbComponentCategory.COMPONENT_CATEGORY_METER), - (105, PbComponentCategory.COMPONENT_CATEGORY_INVERTER), - (106, PbComponentCategory.COMPONENT_CATEGORY_BATTERY), - ( - 107, - PbComponentCategory.COMPONENT_CATEGORY_EV_CHARGER, - ), - (999, PbComponentCategory.COMPONENT_CATEGORY_SENSOR), - ] - ) +async def test_components() -> None: + """Test the components() method.""" + async with _gprc_server() as (servicer, microgrid): + assert set(await microgrid.components()) == set() + + servicer.add_component(0, PbComponentCategory.COMPONENT_CATEGORY_METER) + assert set(await microgrid.components()) == { + Component(0, ComponentCategory.METER) + } + + servicer.add_component(0, PbComponentCategory.COMPONENT_CATEGORY_BATTERY) + assert set(await microgrid.components()) == { + Component(0, ComponentCategory.METER), + Component(0, ComponentCategory.BATTERY), + } + + servicer.add_component(0, PbComponentCategory.COMPONENT_CATEGORY_METER) + assert set(await microgrid.components()) == { + Component(0, ComponentCategory.METER), + Component(0, ComponentCategory.BATTERY), + Component(0, ComponentCategory.METER), + } + + # sensors are not counted as components by the API client + servicer.add_component(1, PbComponentCategory.COMPONENT_CATEGORY_SENSOR) + assert set(await microgrid.components()) == { + Component(0, ComponentCategory.METER), + Component(0, ComponentCategory.BATTERY), + Component(0, ComponentCategory.METER), + } + + servicer.set_components( + [ + (9, PbComponentCategory.COMPONENT_CATEGORY_METER), + (99, PbComponentCategory.COMPONENT_CATEGORY_INVERTER), + (666, PbComponentCategory.COMPONENT_CATEGORY_SENSOR), + (999, PbComponentCategory.COMPONENT_CATEGORY_BATTERY), + ] + ) + assert set(await microgrid.components()) == { + Component(9, ComponentCategory.METER), + Component(99, ComponentCategory.INVERTER, InverterType.NONE), + Component(999, ComponentCategory.BATTERY), + } + + servicer.set_components( + [ + (99, PbComponentCategory.COMPONENT_CATEGORY_SENSOR), + ( + 100, + PbComponentCategory.COMPONENT_CATEGORY_UNSPECIFIED, + ), + (104, PbComponentCategory.COMPONENT_CATEGORY_METER), + (105, PbComponentCategory.COMPONENT_CATEGORY_INVERTER), + (106, PbComponentCategory.COMPONENT_CATEGORY_BATTERY), + ( + 107, + PbComponentCategory.COMPONENT_CATEGORY_EV_CHARGER, + ), + (999, PbComponentCategory.COMPONENT_CATEGORY_SENSOR), + ] + ) - servicer.add_component( - 101, - PbComponentCategory.COMPONENT_CATEGORY_GRID, - 123.0, - ) + servicer.add_component( + 101, + PbComponentCategory.COMPONENT_CATEGORY_GRID, + 123.0, + ) - grid_max_current = 123.0 - grid_fuse = Fuse(grid_max_current) + grid_max_current = 123.0 + grid_fuse = Fuse(grid_max_current) - assert set(await microgrid.components()) == { - Component(100, ComponentCategory.NONE), - Component( - 101, - ComponentCategory.GRID, - None, - GridMetadata(fuse=grid_fuse), - ), - Component(104, ComponentCategory.METER), - Component(105, ComponentCategory.INVERTER, InverterType.NONE), - Component(106, ComponentCategory.BATTERY), - Component(107, ComponentCategory.EV_CHARGER), - } - - servicer.set_components( - [ - (9, PbComponentCategory.COMPONENT_CATEGORY_METER), - (666, PbComponentCategory.COMPONENT_CATEGORY_SENSOR), - (999, PbComponentCategory.COMPONENT_CATEGORY_BATTERY), - ] - ) - servicer.add_component( - 99, - PbComponentCategory.COMPONENT_CATEGORY_INVERTER, + assert set(await microgrid.components()) == { + Component(100, ComponentCategory.NONE), + Component( + 101, + ComponentCategory.GRID, None, - PbInverterType.INVERTER_TYPE_BATTERY, - ) + GridMetadata(fuse=grid_fuse), + ), + Component(104, ComponentCategory.METER), + Component(105, ComponentCategory.INVERTER, InverterType.NONE), + Component(106, ComponentCategory.BATTERY), + Component(107, ComponentCategory.EV_CHARGER), + } + + servicer.set_components( + [ + (9, PbComponentCategory.COMPONENT_CATEGORY_METER), + (666, PbComponentCategory.COMPONENT_CATEGORY_SENSOR), + (999, PbComponentCategory.COMPONENT_CATEGORY_BATTERY), + ] + ) + servicer.add_component( + 99, + PbComponentCategory.COMPONENT_CATEGORY_INVERTER, + None, + PbInverterType.INVERTER_TYPE_BATTERY, + ) - assert set(await microgrid.components()) == { - Component(9, ComponentCategory.METER), - Component(99, ComponentCategory.INVERTER, InverterType.BATTERY), - Component(999, ComponentCategory.BATTERY), - } + assert set(await microgrid.components()) == { + Component(9, ComponentCategory.METER), + Component(99, ComponentCategory.INVERTER, InverterType.BATTERY), + Component(999, ComponentCategory.BATTERY), + } - async def test_connections(self) -> None: - """Test the connections() method.""" - async with _gprc_server() as (servicer, microgrid): - assert set(await microgrid.connections()) == set() - servicer.add_connection(0, 0) - assert set(await microgrid.connections()) == {Connection(0, 0)} +async def test_connections() -> None: + """Test the connections() method.""" + async with _gprc_server() as (servicer, microgrid): + assert set(await microgrid.connections()) == set() - servicer.add_connection(7, 9) - servicer.add_component( - 7, - component_category=PbComponentCategory.COMPONENT_CATEGORY_BATTERY, - ) + servicer.add_connection(0, 0) + assert set(await microgrid.connections()) == {Connection(0, 0)} + + servicer.add_connection(7, 9) + servicer.add_component( + 7, + component_category=PbComponentCategory.COMPONENT_CATEGORY_BATTERY, + ) + servicer.add_component( + 9, + component_category=PbComponentCategory.COMPONENT_CATEGORY_INVERTER, + ) + assert set(await microgrid.connections()) == { + Connection(0, 0), + Connection(7, 9), + } + + servicer.add_connection(0, 0) + assert set(await microgrid.connections()) == { + Connection(0, 0), + Connection(7, 9), + Connection(0, 0), + } + + servicer.set_connections([(999, 9), (99, 19), (909, 101), (99, 91)]) + for component_id in [999, 99, 19, 909, 101, 91]: servicer.add_component( - 9, - component_category=PbComponentCategory.COMPONENT_CATEGORY_INVERTER, - ) - assert set(await microgrid.connections()) == { - Connection(0, 0), - Connection(7, 9), - } - - servicer.add_connection(0, 0) - assert set(await microgrid.connections()) == { - Connection(0, 0), - Connection(7, 9), - Connection(0, 0), - } - - servicer.set_connections([(999, 9), (99, 19), (909, 101), (99, 91)]) - for component_id in [999, 99, 19, 909, 101, 91]: - servicer.add_component( - component_id, - PbComponentCategory.COMPONENT_CATEGORY_BATTERY, - ) - - assert set(await microgrid.connections()) == { - Connection(999, 9), - Connection(99, 19), - Connection(909, 101), - Connection(99, 91), - } - - for component_id in [1, 2, 3, 4, 5, 6, 7, 8]: - servicer.add_component( - component_id, - PbComponentCategory.COMPONENT_CATEGORY_BATTERY, - ) - - servicer.set_connections( - [ - (1, 2), - (2, 3), - (2, 4), - (2, 5), - (4, 3), - (4, 5), - (4, 6), - (5, 4), - (5, 7), - (5, 8), - ] - ) - assert set(await microgrid.connections()) == { - Connection(1, 2), - Connection(2, 3), - Connection(2, 4), - Connection(2, 5), - Connection(4, 3), - Connection(4, 5), - Connection(4, 6), - Connection(5, 4), - Connection(5, 7), - Connection(5, 8), - } - - # passing empty sets is the same as passing `None`, - # filter is ignored - assert set(await microgrid.connections(starts=set(), ends=set())) == { - Connection(1, 2), - Connection(2, 3), - Connection(2, 4), - Connection(2, 5), - Connection(4, 3), - Connection(4, 5), - Connection(4, 6), - Connection(5, 4), - Connection(5, 7), - Connection(5, 8), - } - - # include filter for connection start - assert set(await microgrid.connections(starts={1})) == {Connection(1, 2)} - - assert set(await microgrid.connections(starts={2})) == { - Connection(2, 3), - Connection(2, 4), - Connection(2, 5), - } - assert set(await microgrid.connections(starts={3})) == set() - - assert set(await microgrid.connections(starts={4, 5})) == { - Connection(4, 3), - Connection(4, 5), - Connection(4, 6), - Connection(5, 4), - Connection(5, 7), - Connection(5, 8), - } - - # include filter for connection end - assert set(await microgrid.connections(ends={1})) == set() - - assert set(await microgrid.connections(ends={3})) == { - Connection(2, 3), - Connection(4, 3), - } - - assert set(await microgrid.connections(ends={2, 4, 5})) == { - Connection(1, 2), - Connection(2, 4), - Connection(2, 5), - Connection(4, 5), - Connection(5, 4), - } - - # different filters combine with AND logic - assert set( - await microgrid.connections(starts={1, 2, 4}, ends={4, 5, 6}) - ) == { - Connection(2, 4), - Connection(2, 5), - Connection(4, 5), - Connection(4, 6), - } - - assert set(await microgrid.connections(starts={3, 5}, ends={7, 8})) == { - Connection(5, 7), - Connection(5, 8), - } - - assert set(await microgrid.connections(starts={1, 5}, ends={2, 7})) == { - Connection(1, 2), - Connection(5, 7), - } - - async def test_bad_connections(self) -> None: - """Validate that the client does not apply connection filters itself.""" - - class BadServicer(mock_api.MockMicrogridServicer): - # pylint: disable=unused-argument,invalid-name - def ListConnections( - self, - request: PbConnectionFilter, - context: grpc.ServicerContext, - ) -> PbConnectionList: - """Ignores supplied `PbConnectionFilter`.""" - return PbConnectionList(connections=self._connections) - - def ListAllComponents( - self, request: Empty, context: grpc.ServicerContext - ) -> PbComponentList: - return PbComponentList(components=self._components) - - async with _gprc_server(BadServicer()) as (servicer, microgrid): - assert not list(await microgrid.connections()) - for component_id in [1, 2, 3, 4, 5, 6, 7, 8, 9]: - servicer.add_component( - component_id, - PbComponentCategory.COMPONENT_CATEGORY_BATTERY, - ) - servicer.set_connections( - [ - (1, 2), - (1, 9), - (2, 3), - (3, 4), - (4, 5), - (5, 6), - (6, 7), - (7, 6), - (7, 9), - ] + component_id, + PbComponentCategory.COMPONENT_CATEGORY_BATTERY, ) - unfiltered = { - Connection(1, 2), - Connection(1, 9), - Connection(2, 3), - Connection(3, 4), - Connection(4, 5), - Connection(5, 6), - Connection(6, 7), - Connection(7, 6), - Connection(7, 9), - } - - # because the application of filters is left to the server side, - # it doesn't matter what filters we set in the client if the - # server doesn't do its part - assert set(await microgrid.connections()) == unfiltered - assert set(await microgrid.connections(starts={1})) == unfiltered - assert set(await microgrid.connections(ends={9})) == unfiltered - assert ( - set(await microgrid.connections(starts={1, 7}, ends={3, 9})) - == unfiltered + assert set(await microgrid.connections()) == { + Connection(999, 9), + Connection(99, 19), + Connection(909, 101), + Connection(99, 91), + } + + for component_id in [1, 2, 3, 4, 5, 6, 7, 8]: + servicer.add_component( + component_id, + PbComponentCategory.COMPONENT_CATEGORY_BATTERY, ) - async def test_meter_data(self) -> None: - """Test the meter_data() method.""" - async with _gprc_server() as (servicer, microgrid): - servicer.add_component(83, PbComponentCategory.COMPONENT_CATEGORY_METER) - servicer.add_component(38, PbComponentCategory.COMPONENT_CATEGORY_BATTERY) - - with pytest.raises(ValueError): - # should raise a ValueError for missing component_id - await microgrid.meter_data(20) - - with pytest.raises(ValueError): - # should raise a ValueError for wrong component category - await microgrid.meter_data(38) - receiver = await microgrid.meter_data(83) - await asyncio.sleep(0.2) - - latest = await anext(receiver) - assert isinstance(latest, MeterData) - assert latest.component_id == 83 - - async def test_battery_data(self) -> None: - """Test the battery_data() method.""" - async with _gprc_server() as (servicer, microgrid): - servicer.add_component(83, PbComponentCategory.COMPONENT_CATEGORY_BATTERY) - servicer.add_component(38, PbComponentCategory.COMPONENT_CATEGORY_INVERTER) - - with pytest.raises(ValueError): - # should raise a ValueError for missing component_id - await microgrid.meter_data(20) - - with pytest.raises(ValueError): - # should raise a ValueError for wrong component category - await microgrid.meter_data(38) - receiver = await microgrid.battery_data(83) - await asyncio.sleep(0.2) - - latest = await anext(receiver) - assert isinstance(latest, BatteryData) - assert latest.component_id == 83 - - async def test_inverter_data(self) -> None: - """Test the inverter_data() method.""" - async with _gprc_server() as (servicer, microgrid): - servicer.add_component(83, PbComponentCategory.COMPONENT_CATEGORY_INVERTER) - servicer.add_component(38, PbComponentCategory.COMPONENT_CATEGORY_BATTERY) - - with pytest.raises(ValueError): - # should raise a ValueError for missing component_id - await microgrid.meter_data(20) - - with pytest.raises(ValueError): - # should raise a ValueError for wrong component category - await microgrid.meter_data(38) - receiver = await microgrid.inverter_data(83) - await asyncio.sleep(0.2) - - latest = await anext(receiver) - assert isinstance(latest, InverterData) - assert latest.component_id == 83 - - async def test_ev_charger_data(self) -> None: - """Test the ev_charger_data() method.""" - async with _gprc_server() as (servicer, microgrid): + servicer.set_connections( + [ + (1, 2), + (2, 3), + (2, 4), + (2, 5), + (4, 3), + (4, 5), + (4, 6), + (5, 4), + (5, 7), + (5, 8), + ] + ) + assert set(await microgrid.connections()) == { + Connection(1, 2), + Connection(2, 3), + Connection(2, 4), + Connection(2, 5), + Connection(4, 3), + Connection(4, 5), + Connection(4, 6), + Connection(5, 4), + Connection(5, 7), + Connection(5, 8), + } + + # passing empty sets is the same as passing `None`, + # filter is ignored + assert set(await microgrid.connections(starts=set(), ends=set())) == { + Connection(1, 2), + Connection(2, 3), + Connection(2, 4), + Connection(2, 5), + Connection(4, 3), + Connection(4, 5), + Connection(4, 6), + Connection(5, 4), + Connection(5, 7), + Connection(5, 8), + } + + # include filter for connection start + assert set(await microgrid.connections(starts={1})) == {Connection(1, 2)} + + assert set(await microgrid.connections(starts={2})) == { + Connection(2, 3), + Connection(2, 4), + Connection(2, 5), + } + assert set(await microgrid.connections(starts={3})) == set() + + assert set(await microgrid.connections(starts={4, 5})) == { + Connection(4, 3), + Connection(4, 5), + Connection(4, 6), + Connection(5, 4), + Connection(5, 7), + Connection(5, 8), + } + + # include filter for connection end + assert set(await microgrid.connections(ends={1})) == set() + + assert set(await microgrid.connections(ends={3})) == { + Connection(2, 3), + Connection(4, 3), + } + + assert set(await microgrid.connections(ends={2, 4, 5})) == { + Connection(1, 2), + Connection(2, 4), + Connection(2, 5), + Connection(4, 5), + Connection(5, 4), + } + + # different filters combine with AND logic + assert set(await microgrid.connections(starts={1, 2, 4}, ends={4, 5, 6})) == { + Connection(2, 4), + Connection(2, 5), + Connection(4, 5), + Connection(4, 6), + } + + assert set(await microgrid.connections(starts={3, 5}, ends={7, 8})) == { + Connection(5, 7), + Connection(5, 8), + } + + assert set(await microgrid.connections(starts={1, 5}, ends={2, 7})) == { + Connection(1, 2), + Connection(5, 7), + } + + +async def test_bad_connections() -> None: + """Validate that the client does not apply connection filters itself.""" + + class BadServicer(mock_api.MockMicrogridServicer): + # pylint: disable=unused-argument,invalid-name + def ListConnections( + self, + request: PbConnectionFilter, + context: grpc.ServicerContext, + ) -> PbConnectionList: + """Ignores supplied `PbConnectionFilter`.""" + return PbConnectionList(connections=self._connections) + + def ListAllComponents( + self, request: Empty, context: grpc.ServicerContext + ) -> PbComponentList: + return PbComponentList(components=self._components) + + async with _gprc_server(BadServicer()) as (servicer, microgrid): + assert not list(await microgrid.connections()) + for component_id in [1, 2, 3, 4, 5, 6, 7, 8, 9]: servicer.add_component( - 83, PbComponentCategory.COMPONENT_CATEGORY_EV_CHARGER + component_id, + PbComponentCategory.COMPONENT_CATEGORY_BATTERY, ) - servicer.add_component(38, PbComponentCategory.COMPONENT_CATEGORY_BATTERY) - - with pytest.raises(ValueError): - # should raise a ValueError for missing component_id - await microgrid.meter_data(20) - - with pytest.raises(ValueError): - # should raise a ValueError for wrong component category - await microgrid.meter_data(38) - receiver = await microgrid.ev_charger_data(83) - await asyncio.sleep(0.2) - - latest = await anext(receiver) - assert isinstance(latest, EVChargerData) - assert latest.component_id == 83 - - async def test_charge(self) -> None: - """Check if charge is able to charge component.""" - async with _gprc_server() as (servicer, microgrid): - servicer.add_component(83, PbComponentCategory.COMPONENT_CATEGORY_METER) - - await microgrid.set_power(component_id=83, power_w=12) - - assert servicer.latest_power is not None - assert servicer.latest_power.component_id == 83 - assert servicer.latest_power.power == 12 - - async def test_discharge(self) -> None: - """Check if discharge is able to discharge component.""" - async with _gprc_server() as (servicer, microgrid): - servicer.add_component(73, PbComponentCategory.COMPONENT_CATEGORY_METER) - - await microgrid.set_power(component_id=73, power_w=-15) - - assert servicer.latest_power is not None - assert servicer.latest_power.component_id == 73 - assert servicer.latest_power.power == -15 - - async def test_set_bounds(self) -> None: - """Check if set_bounds is able to set bounds for component.""" - async with _gprc_server() as (servicer, microgrid): - servicer.add_component(38, PbComponentCategory.COMPONENT_CATEGORY_INVERTER) - - num_calls = 4 - - target_metric = PbSetBoundsParam.TargetMetric - expected_bounds = [ - PbSetBoundsParam( - component_id=comp_id, - target_metric=target_metric.TARGET_METRIC_POWER_ACTIVE, - bounds=PbBounds(lower=-10, upper=2), - ) - for comp_id in range(num_calls) + servicer.set_connections( + [ + (1, 2), + (1, 9), + (2, 3), + (3, 4), + (4, 5), + (5, 6), + (6, 7), + (7, 6), + (7, 9), ] - for cid in range(num_calls): - await microgrid.set_bounds(cid, -10.0, 2.0) - await asyncio.sleep(0.1) + ) + + unfiltered = { + Connection(1, 2), + Connection(1, 9), + Connection(2, 3), + Connection(3, 4), + Connection(4, 5), + Connection(5, 6), + Connection(6, 7), + Connection(7, 6), + Connection(7, 9), + } + + # because the application of filters is left to the server side, + # it doesn't matter what filters we set in the client if the + # server doesn't do its part + assert set(await microgrid.connections()) == unfiltered + assert set(await microgrid.connections(starts={1})) == unfiltered + assert set(await microgrid.connections(ends={9})) == unfiltered + assert ( + set(await microgrid.connections(starts={1, 7}, ends={3, 9})) == unfiltered + ) - assert len(expected_bounds) == len(servicer.get_bounds()) - def sort_key( - bound: PbSetBoundsParam, - ) -> PbSetBoundsParam.TargetMetric.ValueType: - return bound.target_metric +async def test_meter_data() -> None: + """Test the meter_data() method.""" + async with _gprc_server() as (servicer, microgrid): + servicer.add_component(83, PbComponentCategory.COMPONENT_CATEGORY_METER) + servicer.add_component(38, PbComponentCategory.COMPONENT_CATEGORY_BATTERY) - assert sorted(servicer.get_bounds(), key=sort_key) == sorted( - expected_bounds, key=sort_key - ) + with pytest.raises(ValueError): + # should raise a ValueError for missing component_id + await microgrid.meter_data(20) + + with pytest.raises(ValueError): + # should raise a ValueError for wrong component category + await microgrid.meter_data(38) + receiver = await microgrid.meter_data(83) + await asyncio.sleep(0.2) + + latest = await anext(receiver) + assert isinstance(latest, MeterData) + assert latest.component_id == 83 + + +async def test_battery_data() -> None: + """Test the battery_data() method.""" + async with _gprc_server() as (servicer, microgrid): + servicer.add_component(83, PbComponentCategory.COMPONENT_CATEGORY_BATTERY) + servicer.add_component(38, PbComponentCategory.COMPONENT_CATEGORY_INVERTER) + + with pytest.raises(ValueError): + # should raise a ValueError for missing component_id + await microgrid.meter_data(20) + + with pytest.raises(ValueError): + # should raise a ValueError for wrong component category + await microgrid.meter_data(38) + receiver = await microgrid.battery_data(83) + await asyncio.sleep(0.2) + + latest = await anext(receiver) + assert isinstance(latest, BatteryData) + assert latest.component_id == 83 + + +async def test_inverter_data() -> None: + """Test the inverter_data() method.""" + async with _gprc_server() as (servicer, microgrid): + servicer.add_component(83, PbComponentCategory.COMPONENT_CATEGORY_INVERTER) + servicer.add_component(38, PbComponentCategory.COMPONENT_CATEGORY_BATTERY) + + with pytest.raises(ValueError): + # should raise a ValueError for missing component_id + await microgrid.meter_data(20) + + with pytest.raises(ValueError): + # should raise a ValueError for wrong component category + await microgrid.meter_data(38) + receiver = await microgrid.inverter_data(83) + await asyncio.sleep(0.2) + + latest = await anext(receiver) + assert isinstance(latest, InverterData) + assert latest.component_id == 83 + + +async def test_ev_charger_data() -> None: + """Test the ev_charger_data() method.""" + async with _gprc_server() as (servicer, microgrid): + servicer.add_component(83, PbComponentCategory.COMPONENT_CATEGORY_EV_CHARGER) + servicer.add_component(38, PbComponentCategory.COMPONENT_CATEGORY_BATTERY) + + with pytest.raises(ValueError): + # should raise a ValueError for missing component_id + await microgrid.meter_data(20) + + with pytest.raises(ValueError): + # should raise a ValueError for wrong component category + await microgrid.meter_data(38) + receiver = await microgrid.ev_charger_data(83) + await asyncio.sleep(0.2) + + latest = await anext(receiver) + assert isinstance(latest, EVChargerData) + assert latest.component_id == 83 + + +async def test_charge() -> None: + """Check if charge is able to charge component.""" + async with _gprc_server() as (servicer, microgrid): + servicer.add_component(83, PbComponentCategory.COMPONENT_CATEGORY_METER) + + await microgrid.set_power(component_id=83, power_w=12) + + assert servicer.latest_power is not None + assert servicer.latest_power.component_id == 83 + assert servicer.latest_power.power == 12 + + +async def test_discharge() -> None: + """Check if discharge is able to discharge component.""" + async with _gprc_server() as (servicer, microgrid): + servicer.add_component(73, PbComponentCategory.COMPONENT_CATEGORY_METER) + + await microgrid.set_power(component_id=73, power_w=-15) + + assert servicer.latest_power is not None + assert servicer.latest_power.component_id == 73 + assert servicer.latest_power.power == -15 + + +async def test_set_bounds() -> None: + """Check if set_bounds is able to set bounds for component.""" + async with _gprc_server() as (servicer, microgrid): + servicer.add_component(38, PbComponentCategory.COMPONENT_CATEGORY_INVERTER) + + num_calls = 4 + + target_metric = PbSetBoundsParam.TargetMetric + expected_bounds = [ + PbSetBoundsParam( + component_id=comp_id, + target_metric=target_metric.TARGET_METRIC_POWER_ACTIVE, + bounds=PbBounds(lower=-10, upper=2), + ) + for comp_id in range(num_calls) + ] + for cid in range(num_calls): + await microgrid.set_bounds(cid, -10.0, 2.0) + await asyncio.sleep(0.1) + + assert len(expected_bounds) == len(servicer.get_bounds()) + + def sort_key( + bound: PbSetBoundsParam, + ) -> PbSetBoundsParam.TargetMetric.ValueType: + return bound.target_metric + + assert sorted(servicer.get_bounds(), key=sort_key) == sorted( + expected_bounds, key=sort_key + ) From 20c19561aac41ab968bf2221be20a4ac14d1da24 Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Fri, 10 May 2024 10:50:12 +0200 Subject: [PATCH 06/10] Stop using the _grpc_server context manager In future commits we'll remove the testing grpc server in favour of just mocking the stub, so we don't need to really listen to a port and include in the unit tests all the gRPC machinery, when we only want to test the client does what it is supposed to do when getting some data from the stub. This also removes another level of extra indentation. Signed-off-by: Leandro Lucarella --- tests/test_client.py | 685 +++++++++++++++++++++---------------------- 1 file changed, 336 insertions(+), 349 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index d5a2780..b4af38b 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -78,254 +78,250 @@ async def _gprc_server( async def test_components() -> None: """Test the components() method.""" - async with _gprc_server() as (servicer, microgrid): - assert set(await microgrid.components()) == set() - - servicer.add_component(0, PbComponentCategory.COMPONENT_CATEGORY_METER) - assert set(await microgrid.components()) == { - Component(0, ComponentCategory.METER) - } - - servicer.add_component(0, PbComponentCategory.COMPONENT_CATEGORY_BATTERY) - assert set(await microgrid.components()) == { - Component(0, ComponentCategory.METER), - Component(0, ComponentCategory.BATTERY), - } - - servicer.add_component(0, PbComponentCategory.COMPONENT_CATEGORY_METER) - assert set(await microgrid.components()) == { - Component(0, ComponentCategory.METER), - Component(0, ComponentCategory.BATTERY), - Component(0, ComponentCategory.METER), - } - - # sensors are not counted as components by the API client - servicer.add_component(1, PbComponentCategory.COMPONENT_CATEGORY_SENSOR) - assert set(await microgrid.components()) == { - Component(0, ComponentCategory.METER), - Component(0, ComponentCategory.BATTERY), - Component(0, ComponentCategory.METER), - } - - servicer.set_components( - [ - (9, PbComponentCategory.COMPONENT_CATEGORY_METER), - (99, PbComponentCategory.COMPONENT_CATEGORY_INVERTER), - (666, PbComponentCategory.COMPONENT_CATEGORY_SENSOR), - (999, PbComponentCategory.COMPONENT_CATEGORY_BATTERY), - ] - ) - assert set(await microgrid.components()) == { - Component(9, ComponentCategory.METER), - Component(99, ComponentCategory.INVERTER, InverterType.NONE), - Component(999, ComponentCategory.BATTERY), - } - - servicer.set_components( - [ - (99, PbComponentCategory.COMPONENT_CATEGORY_SENSOR), - ( - 100, - PbComponentCategory.COMPONENT_CATEGORY_UNSPECIFIED, - ), - (104, PbComponentCategory.COMPONENT_CATEGORY_METER), - (105, PbComponentCategory.COMPONENT_CATEGORY_INVERTER), - (106, PbComponentCategory.COMPONENT_CATEGORY_BATTERY), - ( - 107, - PbComponentCategory.COMPONENT_CATEGORY_EV_CHARGER, - ), - (999, PbComponentCategory.COMPONENT_CATEGORY_SENSOR), - ] - ) + assert set(await microgrid.components()) == set() + + servicer.add_component(0, PbComponentCategory.COMPONENT_CATEGORY_METER) + assert set(await microgrid.components()) == {Component(0, ComponentCategory.METER)} + + servicer.add_component(0, PbComponentCategory.COMPONENT_CATEGORY_BATTERY) + assert set(await microgrid.components()) == { + Component(0, ComponentCategory.METER), + Component(0, ComponentCategory.BATTERY), + } + + servicer.add_component(0, PbComponentCategory.COMPONENT_CATEGORY_METER) + assert set(await microgrid.components()) == { + Component(0, ComponentCategory.METER), + Component(0, ComponentCategory.BATTERY), + Component(0, ComponentCategory.METER), + } + + # sensors are not counted as components by the API client + servicer.add_component(1, PbComponentCategory.COMPONENT_CATEGORY_SENSOR) + assert set(await microgrid.components()) == { + Component(0, ComponentCategory.METER), + Component(0, ComponentCategory.BATTERY), + Component(0, ComponentCategory.METER), + } + + servicer.set_components( + [ + (9, PbComponentCategory.COMPONENT_CATEGORY_METER), + (99, PbComponentCategory.COMPONENT_CATEGORY_INVERTER), + (666, PbComponentCategory.COMPONENT_CATEGORY_SENSOR), + (999, PbComponentCategory.COMPONENT_CATEGORY_BATTERY), + ] + ) + assert set(await microgrid.components()) == { + Component(9, ComponentCategory.METER), + Component(99, ComponentCategory.INVERTER, InverterType.NONE), + Component(999, ComponentCategory.BATTERY), + } + + servicer.set_components( + [ + (99, PbComponentCategory.COMPONENT_CATEGORY_SENSOR), + ( + 100, + PbComponentCategory.COMPONENT_CATEGORY_UNSPECIFIED, + ), + (104, PbComponentCategory.COMPONENT_CATEGORY_METER), + (105, PbComponentCategory.COMPONENT_CATEGORY_INVERTER), + (106, PbComponentCategory.COMPONENT_CATEGORY_BATTERY), + ( + 107, + PbComponentCategory.COMPONENT_CATEGORY_EV_CHARGER, + ), + (999, PbComponentCategory.COMPONENT_CATEGORY_SENSOR), + ] + ) - servicer.add_component( - 101, - PbComponentCategory.COMPONENT_CATEGORY_GRID, - 123.0, - ) + servicer.add_component( + 101, + PbComponentCategory.COMPONENT_CATEGORY_GRID, + 123.0, + ) - grid_max_current = 123.0 - grid_fuse = Fuse(grid_max_current) + grid_max_current = 123.0 + grid_fuse = Fuse(grid_max_current) - assert set(await microgrid.components()) == { - Component(100, ComponentCategory.NONE), - Component( - 101, - ComponentCategory.GRID, - None, - GridMetadata(fuse=grid_fuse), - ), - Component(104, ComponentCategory.METER), - Component(105, ComponentCategory.INVERTER, InverterType.NONE), - Component(106, ComponentCategory.BATTERY), - Component(107, ComponentCategory.EV_CHARGER), - } - - servicer.set_components( - [ - (9, PbComponentCategory.COMPONENT_CATEGORY_METER), - (666, PbComponentCategory.COMPONENT_CATEGORY_SENSOR), - (999, PbComponentCategory.COMPONENT_CATEGORY_BATTERY), - ] - ) - servicer.add_component( - 99, - PbComponentCategory.COMPONENT_CATEGORY_INVERTER, + assert set(await microgrid.components()) == { + Component(100, ComponentCategory.NONE), + Component( + 101, + ComponentCategory.GRID, None, - PbInverterType.INVERTER_TYPE_BATTERY, - ) + GridMetadata(fuse=grid_fuse), + ), + Component(104, ComponentCategory.METER), + Component(105, ComponentCategory.INVERTER, InverterType.NONE), + Component(106, ComponentCategory.BATTERY), + Component(107, ComponentCategory.EV_CHARGER), + } + + servicer.set_components( + [ + (9, PbComponentCategory.COMPONENT_CATEGORY_METER), + (666, PbComponentCategory.COMPONENT_CATEGORY_SENSOR), + (999, PbComponentCategory.COMPONENT_CATEGORY_BATTERY), + ] + ) + servicer.add_component( + 99, + PbComponentCategory.COMPONENT_CATEGORY_INVERTER, + None, + PbInverterType.INVERTER_TYPE_BATTERY, + ) - assert set(await microgrid.components()) == { - Component(9, ComponentCategory.METER), - Component(99, ComponentCategory.INVERTER, InverterType.BATTERY), - Component(999, ComponentCategory.BATTERY), - } + assert set(await microgrid.components()) == { + Component(9, ComponentCategory.METER), + Component(99, ComponentCategory.INVERTER, InverterType.BATTERY), + Component(999, ComponentCategory.BATTERY), + } async def test_connections() -> None: """Test the connections() method.""" - async with _gprc_server() as (servicer, microgrid): - assert set(await microgrid.connections()) == set() + assert set(await microgrid.connections()) == set() - servicer.add_connection(0, 0) - assert set(await microgrid.connections()) == {Connection(0, 0)} + servicer.add_connection(0, 0) + assert set(await microgrid.connections()) == {Connection(0, 0)} - servicer.add_connection(7, 9) + servicer.add_connection(7, 9) + servicer.add_component( + 7, + component_category=PbComponentCategory.COMPONENT_CATEGORY_BATTERY, + ) + servicer.add_component( + 9, + component_category=PbComponentCategory.COMPONENT_CATEGORY_INVERTER, + ) + assert set(await microgrid.connections()) == { + Connection(0, 0), + Connection(7, 9), + } + + servicer.add_connection(0, 0) + assert set(await microgrid.connections()) == { + Connection(0, 0), + Connection(7, 9), + Connection(0, 0), + } + + servicer.set_connections([(999, 9), (99, 19), (909, 101), (99, 91)]) + for component_id in [999, 99, 19, 909, 101, 91]: servicer.add_component( - 7, - component_category=PbComponentCategory.COMPONENT_CATEGORY_BATTERY, + component_id, + PbComponentCategory.COMPONENT_CATEGORY_BATTERY, ) + + assert set(await microgrid.connections()) == { + Connection(999, 9), + Connection(99, 19), + Connection(909, 101), + Connection(99, 91), + } + + for component_id in [1, 2, 3, 4, 5, 6, 7, 8]: servicer.add_component( - 9, - component_category=PbComponentCategory.COMPONENT_CATEGORY_INVERTER, - ) - assert set(await microgrid.connections()) == { - Connection(0, 0), - Connection(7, 9), - } - - servicer.add_connection(0, 0) - assert set(await microgrid.connections()) == { - Connection(0, 0), - Connection(7, 9), - Connection(0, 0), - } - - servicer.set_connections([(999, 9), (99, 19), (909, 101), (99, 91)]) - for component_id in [999, 99, 19, 909, 101, 91]: - servicer.add_component( - component_id, - PbComponentCategory.COMPONENT_CATEGORY_BATTERY, - ) - - assert set(await microgrid.connections()) == { - Connection(999, 9), - Connection(99, 19), - Connection(909, 101), - Connection(99, 91), - } - - for component_id in [1, 2, 3, 4, 5, 6, 7, 8]: - servicer.add_component( - component_id, - PbComponentCategory.COMPONENT_CATEGORY_BATTERY, - ) - - servicer.set_connections( - [ - (1, 2), - (2, 3), - (2, 4), - (2, 5), - (4, 3), - (4, 5), - (4, 6), - (5, 4), - (5, 7), - (5, 8), - ] + component_id, + PbComponentCategory.COMPONENT_CATEGORY_BATTERY, ) - assert set(await microgrid.connections()) == { - Connection(1, 2), - Connection(2, 3), - Connection(2, 4), - Connection(2, 5), - Connection(4, 3), - Connection(4, 5), - Connection(4, 6), - Connection(5, 4), - Connection(5, 7), - Connection(5, 8), - } - - # passing empty sets is the same as passing `None`, - # filter is ignored - assert set(await microgrid.connections(starts=set(), ends=set())) == { - Connection(1, 2), - Connection(2, 3), - Connection(2, 4), - Connection(2, 5), - Connection(4, 3), - Connection(4, 5), - Connection(4, 6), - Connection(5, 4), - Connection(5, 7), - Connection(5, 8), - } - - # include filter for connection start - assert set(await microgrid.connections(starts={1})) == {Connection(1, 2)} - - assert set(await microgrid.connections(starts={2})) == { - Connection(2, 3), - Connection(2, 4), - Connection(2, 5), - } - assert set(await microgrid.connections(starts={3})) == set() - - assert set(await microgrid.connections(starts={4, 5})) == { - Connection(4, 3), - Connection(4, 5), - Connection(4, 6), - Connection(5, 4), - Connection(5, 7), - Connection(5, 8), - } - - # include filter for connection end - assert set(await microgrid.connections(ends={1})) == set() - - assert set(await microgrid.connections(ends={3})) == { - Connection(2, 3), - Connection(4, 3), - } - - assert set(await microgrid.connections(ends={2, 4, 5})) == { - Connection(1, 2), - Connection(2, 4), - Connection(2, 5), - Connection(4, 5), - Connection(5, 4), - } - - # different filters combine with AND logic - assert set(await microgrid.connections(starts={1, 2, 4}, ends={4, 5, 6})) == { - Connection(2, 4), - Connection(2, 5), - Connection(4, 5), - Connection(4, 6), - } - - assert set(await microgrid.connections(starts={3, 5}, ends={7, 8})) == { - Connection(5, 7), - Connection(5, 8), - } - - assert set(await microgrid.connections(starts={1, 5}, ends={2, 7})) == { - Connection(1, 2), - Connection(5, 7), - } + + servicer.set_connections( + [ + (1, 2), + (2, 3), + (2, 4), + (2, 5), + (4, 3), + (4, 5), + (4, 6), + (5, 4), + (5, 7), + (5, 8), + ] + ) + assert set(await microgrid.connections()) == { + Connection(1, 2), + Connection(2, 3), + Connection(2, 4), + Connection(2, 5), + Connection(4, 3), + Connection(4, 5), + Connection(4, 6), + Connection(5, 4), + Connection(5, 7), + Connection(5, 8), + } + + # passing empty sets is the same as passing `None`, + # filter is ignored + assert set(await microgrid.connections(starts=set(), ends=set())) == { + Connection(1, 2), + Connection(2, 3), + Connection(2, 4), + Connection(2, 5), + Connection(4, 3), + Connection(4, 5), + Connection(4, 6), + Connection(5, 4), + Connection(5, 7), + Connection(5, 8), + } + + # include filter for connection start + assert set(await microgrid.connections(starts={1})) == {Connection(1, 2)} + + assert set(await microgrid.connections(starts={2})) == { + Connection(2, 3), + Connection(2, 4), + Connection(2, 5), + } + assert set(await microgrid.connections(starts={3})) == set() + + assert set(await microgrid.connections(starts={4, 5})) == { + Connection(4, 3), + Connection(4, 5), + Connection(4, 6), + Connection(5, 4), + Connection(5, 7), + Connection(5, 8), + } + + # include filter for connection end + assert set(await microgrid.connections(ends={1})) == set() + + assert set(await microgrid.connections(ends={3})) == { + Connection(2, 3), + Connection(4, 3), + } + + assert set(await microgrid.connections(ends={2, 4, 5})) == { + Connection(1, 2), + Connection(2, 4), + Connection(2, 5), + Connection(4, 5), + Connection(5, 4), + } + + # different filters combine with AND logic + assert set(await microgrid.connections(starts={1, 2, 4}, ends={4, 5, 6})) == { + Connection(2, 4), + Connection(2, 5), + Connection(4, 5), + Connection(4, 6), + } + + assert set(await microgrid.connections(starts={3, 5}, ends={7, 8})) == { + Connection(5, 7), + Connection(5, 8), + } + + assert set(await microgrid.connections(starts={1, 5}, ends={2, 7})) == { + Connection(1, 2), + Connection(5, 7), + } async def test_bad_connections() -> None: @@ -346,86 +342,82 @@ def ListAllComponents( ) -> PbComponentList: return PbComponentList(components=self._components) - async with _gprc_server(BadServicer()) as (servicer, microgrid): - assert not list(await microgrid.connections()) - for component_id in [1, 2, 3, 4, 5, 6, 7, 8, 9]: - servicer.add_component( - component_id, - PbComponentCategory.COMPONENT_CATEGORY_BATTERY, - ) - servicer.set_connections( - [ - (1, 2), - (1, 9), - (2, 3), - (3, 4), - (4, 5), - (5, 6), - (6, 7), - (7, 6), - (7, 9), - ] + assert not list(await microgrid.connections()) + for component_id in [1, 2, 3, 4, 5, 6, 7, 8, 9]: + servicer.add_component( + component_id, + PbComponentCategory.COMPONENT_CATEGORY_BATTERY, ) + servicer.set_connections( + [ + (1, 2), + (1, 9), + (2, 3), + (3, 4), + (4, 5), + (5, 6), + (6, 7), + (7, 6), + (7, 9), + ] + ) - unfiltered = { - Connection(1, 2), - Connection(1, 9), - Connection(2, 3), - Connection(3, 4), - Connection(4, 5), - Connection(5, 6), - Connection(6, 7), - Connection(7, 6), - Connection(7, 9), - } - - # because the application of filters is left to the server side, - # it doesn't matter what filters we set in the client if the - # server doesn't do its part - assert set(await microgrid.connections()) == unfiltered - assert set(await microgrid.connections(starts={1})) == unfiltered - assert set(await microgrid.connections(ends={9})) == unfiltered - assert ( - set(await microgrid.connections(starts={1, 7}, ends={3, 9})) == unfiltered - ) + unfiltered = { + Connection(1, 2), + Connection(1, 9), + Connection(2, 3), + Connection(3, 4), + Connection(4, 5), + Connection(5, 6), + Connection(6, 7), + Connection(7, 6), + Connection(7, 9), + } + + # because the application of filters is left to the server side, + # it doesn't matter what filters we set in the client if the + # server doesn't do its part + assert set(await microgrid.connections()) == unfiltered + assert set(await microgrid.connections(starts={1})) == unfiltered + assert set(await microgrid.connections(ends={9})) == unfiltered + assert set(await microgrid.connections(starts={1, 7}, ends={3, 9})) == unfiltered async def test_meter_data() -> None: """Test the meter_data() method.""" - async with _gprc_server() as (servicer, microgrid): - servicer.add_component(83, PbComponentCategory.COMPONENT_CATEGORY_METER) - servicer.add_component(38, PbComponentCategory.COMPONENT_CATEGORY_BATTERY) + servicer.add_component(83, PbComponentCategory.COMPONENT_CATEGORY_METER) + servicer.add_component(38, PbComponentCategory.COMPONENT_CATEGORY_BATTERY) - with pytest.raises(ValueError): - # should raise a ValueError for missing component_id - await microgrid.meter_data(20) + with pytest.raises(ValueError): + # should raise a ValueError for missing component_id + await microgrid.meter_data(20) - with pytest.raises(ValueError): - # should raise a ValueError for wrong component category - await microgrid.meter_data(38) - receiver = await microgrid.meter_data(83) - await asyncio.sleep(0.2) + with pytest.raises(ValueError): + # should raise a ValueError for wrong component category + await microgrid.meter_data(38) + receiver = await microgrid.meter_data(83) + await asyncio.sleep(0.2) - latest = await anext(receiver) - assert isinstance(latest, MeterData) - assert latest.component_id == 83 + +latest = await anext(receiver) +assert isinstance(latest, MeterData) +assert latest.component_id == 83 async def test_battery_data() -> None: """Test the battery_data() method.""" - async with _gprc_server() as (servicer, microgrid): - servicer.add_component(83, PbComponentCategory.COMPONENT_CATEGORY_BATTERY) - servicer.add_component(38, PbComponentCategory.COMPONENT_CATEGORY_INVERTER) + servicer.add_component(83, PbComponentCategory.COMPONENT_CATEGORY_BATTERY) + servicer.add_component(38, PbComponentCategory.COMPONENT_CATEGORY_INVERTER) - with pytest.raises(ValueError): - # should raise a ValueError for missing component_id - await microgrid.meter_data(20) + with pytest.raises(ValueError): + # should raise a ValueError for missing component_id + await microgrid.meter_data(20) - with pytest.raises(ValueError): - # should raise a ValueError for wrong component category - await microgrid.meter_data(38) - receiver = await microgrid.battery_data(83) - await asyncio.sleep(0.2) + with pytest.raises(ValueError): + # should raise a ValueError for wrong component category + await microgrid.meter_data(38) + receiver = await microgrid.battery_data(83) + await asyncio.sleep(0.2) latest = await anext(receiver) assert isinstance(latest, BatteryData) @@ -434,19 +426,18 @@ async def test_battery_data() -> None: async def test_inverter_data() -> None: """Test the inverter_data() method.""" - async with _gprc_server() as (servicer, microgrid): - servicer.add_component(83, PbComponentCategory.COMPONENT_CATEGORY_INVERTER) - servicer.add_component(38, PbComponentCategory.COMPONENT_CATEGORY_BATTERY) + servicer.add_component(83, PbComponentCategory.COMPONENT_CATEGORY_INVERTER) + servicer.add_component(38, PbComponentCategory.COMPONENT_CATEGORY_BATTERY) - with pytest.raises(ValueError): - # should raise a ValueError for missing component_id - await microgrid.meter_data(20) + with pytest.raises(ValueError): + # should raise a ValueError for missing component_id + await microgrid.meter_data(20) - with pytest.raises(ValueError): - # should raise a ValueError for wrong component category - await microgrid.meter_data(38) - receiver = await microgrid.inverter_data(83) - await asyncio.sleep(0.2) + with pytest.raises(ValueError): + # should raise a ValueError for wrong component category + await microgrid.meter_data(38) + receiver = await microgrid.inverter_data(83) + await asyncio.sleep(0.2) latest = await anext(receiver) assert isinstance(latest, InverterData) @@ -455,19 +446,18 @@ async def test_inverter_data() -> None: async def test_ev_charger_data() -> None: """Test the ev_charger_data() method.""" - async with _gprc_server() as (servicer, microgrid): - servicer.add_component(83, PbComponentCategory.COMPONENT_CATEGORY_EV_CHARGER) - servicer.add_component(38, PbComponentCategory.COMPONENT_CATEGORY_BATTERY) + servicer.add_component(83, PbComponentCategory.COMPONENT_CATEGORY_EV_CHARGER) + servicer.add_component(38, PbComponentCategory.COMPONENT_CATEGORY_BATTERY) - with pytest.raises(ValueError): - # should raise a ValueError for missing component_id - await microgrid.meter_data(20) + with pytest.raises(ValueError): + # should raise a ValueError for missing component_id + await microgrid.meter_data(20) - with pytest.raises(ValueError): - # should raise a ValueError for wrong component category - await microgrid.meter_data(38) - receiver = await microgrid.ev_charger_data(83) - await asyncio.sleep(0.2) + with pytest.raises(ValueError): + # should raise a ValueError for wrong component category + await microgrid.meter_data(38) + receiver = await microgrid.ev_charger_data(83) + await asyncio.sleep(0.2) latest = await anext(receiver) assert isinstance(latest, EVChargerData) @@ -476,47 +466,44 @@ async def test_ev_charger_data() -> None: async def test_charge() -> None: """Check if charge is able to charge component.""" - async with _gprc_server() as (servicer, microgrid): - servicer.add_component(83, PbComponentCategory.COMPONENT_CATEGORY_METER) + servicer.add_component(83, PbComponentCategory.COMPONENT_CATEGORY_METER) - await microgrid.set_power(component_id=83, power_w=12) + await microgrid.set_power(component_id=83, power_w=12) - assert servicer.latest_power is not None - assert servicer.latest_power.component_id == 83 - assert servicer.latest_power.power == 12 + assert servicer.latest_power is not None + assert servicer.latest_power.component_id == 83 + assert servicer.latest_power.power == 12 async def test_discharge() -> None: """Check if discharge is able to discharge component.""" - async with _gprc_server() as (servicer, microgrid): - servicer.add_component(73, PbComponentCategory.COMPONENT_CATEGORY_METER) + servicer.add_component(73, PbComponentCategory.COMPONENT_CATEGORY_METER) - await microgrid.set_power(component_id=73, power_w=-15) + await microgrid.set_power(component_id=73, power_w=-15) - assert servicer.latest_power is not None - assert servicer.latest_power.component_id == 73 - assert servicer.latest_power.power == -15 + assert servicer.latest_power is not None + assert servicer.latest_power.component_id == 73 + assert servicer.latest_power.power == -15 async def test_set_bounds() -> None: """Check if set_bounds is able to set bounds for component.""" - async with _gprc_server() as (servicer, microgrid): - servicer.add_component(38, PbComponentCategory.COMPONENT_CATEGORY_INVERTER) - - num_calls = 4 - - target_metric = PbSetBoundsParam.TargetMetric - expected_bounds = [ - PbSetBoundsParam( - component_id=comp_id, - target_metric=target_metric.TARGET_METRIC_POWER_ACTIVE, - bounds=PbBounds(lower=-10, upper=2), - ) - for comp_id in range(num_calls) - ] - for cid in range(num_calls): - await microgrid.set_bounds(cid, -10.0, 2.0) - await asyncio.sleep(0.1) + servicer.add_component(38, PbComponentCategory.COMPONENT_CATEGORY_INVERTER) + + num_calls = 4 + + target_metric = PbSetBoundsParam.TargetMetric + expected_bounds = [ + PbSetBoundsParam( + component_id=comp_id, + target_metric=target_metric.TARGET_METRIC_POWER_ACTIVE, + bounds=PbBounds(lower=-10, upper=2), + ) + for comp_id in range(num_calls) + ] + for cid in range(num_calls): + await microgrid.set_bounds(cid, -10.0, 2.0) + await asyncio.sleep(0.1) assert len(expected_bounds) == len(servicer.get_bounds()) From b3efe71c15e42a51610181ff7292e3c5f69b0f64 Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Mon, 13 May 2024 10:30:18 +0200 Subject: [PATCH 07/10] Fix client test to use betterproto Fixing this tests is quite complicated because we also stop using a real gRPC server (`mock_api`) for the tests, to avoid introducing too much noise and flakiness to *unit tests* (see https://github.com/frequenz-floss/frequenz-sdk-python/issues/662 for example). For now we just tried to replace `mock_api` with `unittest.mock` mocks, doing the minimal work to get the tests working and move forward with the migration to betterproto, but in the future we'll provide a higher level API client mock (see https://github.com/frequenz-floss/frequenz-client-microgrid-python/issues/39). Signed-off-by: Leandro Lucarella --- tests/test_client.py | 883 ++++++++++++++++++++++++------------------- 1 file changed, 503 insertions(+), 380 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index b4af38b..821f9d3 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -3,149 +3,171 @@ """Tests for the microgrid client thin wrapper.""" -import asyncio -import contextlib +import logging from collections.abc import AsyncIterator +from contextlib import AsyncExitStack +from typing import Any +from unittest import mock -import grpc.aio +import grpclib +import grpclib.client import pytest +from frequenz.client.base import retry +from frequenz.microgrid.betterproto.frequenz.api import microgrid +from frequenz.microgrid.betterproto.frequenz.api.common import components, metrics +from frequenz.microgrid.betterproto.frequenz.api.microgrid import grid, inverter -# pylint: disable=no-name-in-module -from frequenz.api.common.components_pb2 import ComponentCategory as PbComponentCategory -from frequenz.api.common.components_pb2 import InverterType as PbInverterType -from frequenz.api.common.metrics_pb2 import Bounds as PbBounds -from frequenz.api.microgrid.microgrid_pb2 import ComponentList as PbComponentList -from frequenz.api.microgrid.microgrid_pb2 import ConnectionFilter as PbConnectionFilter -from frequenz.api.microgrid.microgrid_pb2 import ConnectionList as PbConnectionList -from frequenz.api.microgrid.microgrid_pb2 import SetBoundsParam as PbSetBoundsParam - -# pylint: enable=no-name-in-module -from frequenz.client.base.retry import LinearBackoff -from google.protobuf.empty_pb2 import Empty # pylint: disable=no-name-in-module - -from frequenz.client.microgrid import _client as client -from frequenz.client.microgrid._component import ( +from frequenz.client.microgrid import ( + ApiClient, + BatteryData, + ClientError, Component, ComponentCategory, + ComponentData, + EVChargerData, Fuse, GridMetadata, - InverterType, -) -from frequenz.client.microgrid._component_data import ( - BatteryData, - EVChargerData, InverterData, + InverterType, MeterData, ) from frequenz.client.microgrid._connection import Connection -from . import mock_api - -# pylint: disable=missing-function-docstring -# pylint: disable=missing-class-docstring - - -# This incrementing port is a hack to avoid the inherent flakiness of the approach of -# using a real GRPC (mock) server. The server seems to stay alive for a short time after -# the test is finished, which causes the next test to fail because the port is already -# in use. -# This is a workaround until we have a better solution. -# See https://github.com/frequenz-floss/frequenz-sdk-python/issues/662 -_CURRENT_PORT: int = 57897 - - -@contextlib.asynccontextmanager -async def _gprc_server( - servicer: mock_api.MockMicrogridServicer | None = None, -) -> AsyncIterator[tuple[mock_api.MockMicrogridServicer, client.ApiClient]]: - global _CURRENT_PORT # pylint: disable=global-statement - port = _CURRENT_PORT - _CURRENT_PORT += 1 - if servicer is None: - servicer = mock_api.MockMicrogridServicer() - server = mock_api.MockGrpcServer(servicer, port=port) - microgrid = client.ApiClient( - grpc.aio.insecure_channel(f"[::]:{port}"), - f"[::]:{port}", - retry_strategy=LinearBackoff(interval=0.0, jitter=0.05), - ) - await server.start() - try: - yield servicer, microgrid - finally: - assert await server.graceful_shutdown() +# @contextlib.asynccontextmanager +# async def _gprc_server( +# servicer: mock_api.MockMicrogridServicer | None = None, +# ) -> AsyncIterator[tuple[mock_api.MockMicrogridServicer, ApiClient]]: +# global _CURRENT_PORT # pylint: disable=global-statement +# port = _CURRENT_PORT +# _CURRENT_PORT += 1 +# if servicer is None: +# servicer = mock_api.MockMicrogridServicer() +# server = mock_api.MockGrpcServer(servicer, port=port) +# client = ApiClient( +# grpc.aio.insecure_channel(f"[::]:{port}"), +# f"[::]:{port}", +# retry_strategy=LinearBackoff(interval=0.0, jitter=0.05), +# ) +# await server.start() +# try: +# yield servicer, client +# finally: +# assert await server.graceful_shutdown() + + +class _TestClient(ApiClient): + def __init__(self, *, retry_strategy: retry.Strategy | None = None) -> None: + mock_channel = mock.MagicMock(name="channel", spec=grpclib.client.Channel) + mock_stub = mock.MagicMock(name="stub", spec=microgrid.MicrogridStub) + target = "mock_host:1234" + super().__init__(mock_channel, target, retry_strategy) + self.mock_channel = mock_channel + self.mock_stub = mock_stub + self.api = mock_stub async def test_components() -> None: """Test the components() method.""" - assert set(await microgrid.components()) == set() - - servicer.add_component(0, PbComponentCategory.COMPONENT_CATEGORY_METER) - assert set(await microgrid.components()) == {Component(0, ComponentCategory.METER)} + client = _TestClient() + server_response = microgrid.ComponentList() + client.mock_stub.list_components.return_value = server_response + assert set(await client.components()) == set() + + server_response.components.append( + microgrid.Component( + id=0, category=components.ComponentCategory.COMPONENT_CATEGORY_METER + ) + ) + assert set(await client.components()) == {Component(0, ComponentCategory.METER)} - servicer.add_component(0, PbComponentCategory.COMPONENT_CATEGORY_BATTERY) - assert set(await microgrid.components()) == { + server_response.components.append( + microgrid.Component( + id=0, category=components.ComponentCategory.COMPONENT_CATEGORY_BATTERY + ) + ) + assert set(await client.components()) == { Component(0, ComponentCategory.METER), Component(0, ComponentCategory.BATTERY), } - servicer.add_component(0, PbComponentCategory.COMPONENT_CATEGORY_METER) - assert set(await microgrid.components()) == { + server_response.components.append( + microgrid.Component( + id=0, category=components.ComponentCategory.COMPONENT_CATEGORY_METER + ) + ) + assert set(await client.components()) == { Component(0, ComponentCategory.METER), Component(0, ComponentCategory.BATTERY), Component(0, ComponentCategory.METER), } # sensors are not counted as components by the API client - servicer.add_component(1, PbComponentCategory.COMPONENT_CATEGORY_SENSOR) - assert set(await microgrid.components()) == { + server_response.components.append( + microgrid.Component( + id=1, category=components.ComponentCategory.COMPONENT_CATEGORY_SENSOR + ) + ) + assert set(await client.components()) == { Component(0, ComponentCategory.METER), Component(0, ComponentCategory.BATTERY), Component(0, ComponentCategory.METER), } - servicer.set_components( - [ - (9, PbComponentCategory.COMPONENT_CATEGORY_METER), - (99, PbComponentCategory.COMPONENT_CATEGORY_INVERTER), - (666, PbComponentCategory.COMPONENT_CATEGORY_SENSOR), - (999, PbComponentCategory.COMPONENT_CATEGORY_BATTERY), - ] - ) - assert set(await microgrid.components()) == { + server_response.components[:] = [ + microgrid.Component( + id=9, category=components.ComponentCategory.COMPONENT_CATEGORY_METER + ), + microgrid.Component( + id=99, category=components.ComponentCategory.COMPONENT_CATEGORY_INVERTER + ), + microgrid.Component( + id=666, category=components.ComponentCategory.COMPONENT_CATEGORY_SENSOR + ), + microgrid.Component( + id=999, category=components.ComponentCategory.COMPONENT_CATEGORY_BATTERY + ), + ] + assert set(await client.components()) == { Component(9, ComponentCategory.METER), Component(99, ComponentCategory.INVERTER, InverterType.NONE), Component(999, ComponentCategory.BATTERY), } - servicer.set_components( - [ - (99, PbComponentCategory.COMPONENT_CATEGORY_SENSOR), - ( - 100, - PbComponentCategory.COMPONENT_CATEGORY_UNSPECIFIED, - ), - (104, PbComponentCategory.COMPONENT_CATEGORY_METER), - (105, PbComponentCategory.COMPONENT_CATEGORY_INVERTER), - (106, PbComponentCategory.COMPONENT_CATEGORY_BATTERY), - ( - 107, - PbComponentCategory.COMPONENT_CATEGORY_EV_CHARGER, - ), - (999, PbComponentCategory.COMPONENT_CATEGORY_SENSOR), - ] - ) - - servicer.add_component( - 101, - PbComponentCategory.COMPONENT_CATEGORY_GRID, - 123.0, - ) + server_response.components[:] = [ + microgrid.Component( + id=99, category=components.ComponentCategory.COMPONENT_CATEGORY_SENSOR + ), + microgrid.Component( + id=100, + category=components.ComponentCategory.COMPONENT_CATEGORY_UNSPECIFIED, + ), + microgrid.Component( + id=104, category=components.ComponentCategory.COMPONENT_CATEGORY_METER + ), + microgrid.Component( + id=105, + category=components.ComponentCategory.COMPONENT_CATEGORY_INVERTER, + ), + microgrid.Component( + id=106, category=components.ComponentCategory.COMPONENT_CATEGORY_BATTERY + ), + microgrid.Component( + id=107, + category=components.ComponentCategory.COMPONENT_CATEGORY_EV_CHARGER, + ), + microgrid.Component( + id=999, category=components.ComponentCategory.COMPONENT_CATEGORY_SENSOR + ), + microgrid.Component( + id=101, + category=components.ComponentCategory.COMPONENT_CATEGORY_GRID, + grid=grid.Metadata(rated_fuse_current=int(123.0)), + ), + ] - grid_max_current = 123.0 - grid_fuse = Fuse(grid_max_current) + grid_fuse = Fuse(123.0) - assert set(await microgrid.components()) == { + assert set(await client.components()) == { Component(100, ComponentCategory.NONE), Component( 101, @@ -159,63 +181,103 @@ async def test_components() -> None: Component(107, ComponentCategory.EV_CHARGER), } - servicer.set_components( - [ - (9, PbComponentCategory.COMPONENT_CATEGORY_METER), - (666, PbComponentCategory.COMPONENT_CATEGORY_SENSOR), - (999, PbComponentCategory.COMPONENT_CATEGORY_BATTERY), - ] - ) - servicer.add_component( - 99, - PbComponentCategory.COMPONENT_CATEGORY_INVERTER, - None, - PbInverterType.INVERTER_TYPE_BATTERY, - ) + server_response.components[:] = [ + microgrid.Component( + id=9, category=components.ComponentCategory.COMPONENT_CATEGORY_METER + ), + microgrid.Component( + id=666, category=components.ComponentCategory.COMPONENT_CATEGORY_SENSOR + ), + microgrid.Component( + id=999, category=components.ComponentCategory.COMPONENT_CATEGORY_BATTERY + ), + microgrid.Component( + id=99, + category=components.ComponentCategory.COMPONENT_CATEGORY_INVERTER, + inverter=inverter.Metadata( + type=components.InverterType.INVERTER_TYPE_BATTERY + ), + ), + ] - assert set(await microgrid.components()) == { + assert set(await client.components()) == { Component(9, ComponentCategory.METER), Component(99, ComponentCategory.INVERTER, InverterType.BATTERY), Component(999, ComponentCategory.BATTERY), } -async def test_connections() -> None: - """Test the connections() method.""" - assert set(await microgrid.connections()) == set() +async def test_components_grpc_error() -> None: + """Test the components() method when the gRPC call fails.""" + client = _TestClient() + client.mock_stub.list_components.side_effect = grpclib.GRPCError( + mock.MagicMock(name="mock status"), "fake grpc error" + ) + with pytest.raises( + ClientError, + match="Failed to list components. Microgrid API: mock_host:1234. Err: .*fake grpc error", + ): + await client.components() - servicer.add_connection(0, 0) - assert set(await microgrid.connections()) == {Connection(0, 0)} - servicer.add_connection(7, 9) - servicer.add_component( - 7, - component_category=PbComponentCategory.COMPONENT_CATEGORY_BATTERY, - ) - servicer.add_component( - 9, - component_category=PbComponentCategory.COMPONENT_CATEGORY_INVERTER, +async def test_connections() -> None: + """Test the connections() method.""" + client = _TestClient() + + def assert_filter(*, starts: set[int], ends: set[int]) -> None: + client.mock_stub.list_connections.assert_called_once() + filter_ = client.mock_stub.list_connections.call_args[0][0] + assert isinstance(filter_, microgrid.ConnectionFilter) + assert set(filter_.starts) == starts + assert set(filter_.ends) == ends + + components_response = microgrid.ComponentList() + connections_response = microgrid.ConnectionList() + client.mock_stub.list_components.return_value = components_response + client.mock_stub.list_connections.return_value = connections_response + assert set(await client.connections()) == set() + assert_filter(starts=set(), ends=set()) + + connections_response.connections.append(microgrid.Connection(start=0, end=0)) + assert set(await client.connections()) == {Connection(0, 0)} + + components_response.components.extend( + [ + microgrid.Component( + id=7, category=components.ComponentCategory.COMPONENT_CATEGORY_BATTERY + ), + microgrid.Component( + id=9, category=components.ComponentCategory.COMPONENT_CATEGORY_INVERTER + ), + ] ) - assert set(await microgrid.connections()) == { + connections_response.connections.append(microgrid.Connection(start=7, end=9)) + assert set(await client.connections()) == { Connection(0, 0), Connection(7, 9), } - servicer.add_connection(0, 0) - assert set(await microgrid.connections()) == { + connections_response.connections.append(microgrid.Connection(start=0, end=0)) + assert set(await client.connections()) == { Connection(0, 0), Connection(7, 9), Connection(0, 0), } - servicer.set_connections([(999, 9), (99, 19), (909, 101), (99, 91)]) + connections_response.connections[:] = [ + microgrid.Connection(start=999, end=9), + microgrid.Connection(start=99, end=19), + microgrid.Connection(start=909, end=101), + microgrid.Connection(start=99, end=91), + ] for component_id in [999, 99, 19, 909, 101, 91]: - servicer.add_component( - component_id, - PbComponentCategory.COMPONENT_CATEGORY_BATTERY, + components_response.components.append( + microgrid.Component( + id=component_id, + category=components.ComponentCategory.COMPONENT_CATEGORY_BATTERY, + ) ) - - assert set(await microgrid.connections()) == { + assert set(await client.connections()) == { Connection(999, 9), Connection(99, 19), Connection(909, 101), @@ -223,26 +285,25 @@ async def test_connections() -> None: } for component_id in [1, 2, 3, 4, 5, 6, 7, 8]: - servicer.add_component( - component_id, - PbComponentCategory.COMPONENT_CATEGORY_BATTERY, + components_response.components.append( + microgrid.Component( + id=component_id, + category=components.ComponentCategory.COMPONENT_CATEGORY_BATTERY, + ) ) - - servicer.set_connections( - [ - (1, 2), - (2, 3), - (2, 4), - (2, 5), - (4, 3), - (4, 5), - (4, 6), - (5, 4), - (5, 7), - (5, 8), - ] - ) - assert set(await microgrid.connections()) == { + connections_response.connections[:] = [ + microgrid.Connection(start=1, end=2), + microgrid.Connection(start=2, end=3), + microgrid.Connection(start=2, end=4), + microgrid.Connection(start=2, end=5), + microgrid.Connection(start=4, end=3), + microgrid.Connection(start=4, end=5), + microgrid.Connection(start=4, end=6), + microgrid.Connection(start=5, end=4), + microgrid.Connection(start=5, end=7), + microgrid.Connection(start=5, end=8), + ] + assert set(await client.connections()) == { Connection(1, 2), Connection(2, 3), Connection(2, 4), @@ -257,261 +318,323 @@ async def test_connections() -> None: # passing empty sets is the same as passing `None`, # filter is ignored - assert set(await microgrid.connections(starts=set(), ends=set())) == { - Connection(1, 2), - Connection(2, 3), - Connection(2, 4), - Connection(2, 5), - Connection(4, 3), - Connection(4, 5), - Connection(4, 6), - Connection(5, 4), - Connection(5, 7), - Connection(5, 8), - } + client.mock_stub.reset_mock() + await client.connections(starts=set(), ends=set()) + assert_filter(starts=set(), ends=set()) # include filter for connection start - assert set(await microgrid.connections(starts={1})) == {Connection(1, 2)} + client.mock_stub.reset_mock() + await client.connections(starts={1, 2}) + assert_filter(starts={1, 2}, ends=set()) - assert set(await microgrid.connections(starts={2})) == { - Connection(2, 3), - Connection(2, 4), - Connection(2, 5), - } - assert set(await microgrid.connections(starts={3})) == set() - - assert set(await microgrid.connections(starts={4, 5})) == { - Connection(4, 3), - Connection(4, 5), - Connection(4, 6), - Connection(5, 4), - Connection(5, 7), - Connection(5, 8), - } + client.mock_stub.reset_mock() + await client.connections(starts={2}) + assert_filter(starts={2}, ends=set()) # include filter for connection end - assert set(await microgrid.connections(ends={1})) == set() + client.mock_stub.reset_mock() + await client.connections(ends={1}) + assert_filter(starts=set(), ends={1}) - assert set(await microgrid.connections(ends={3})) == { - Connection(2, 3), - Connection(4, 3), - } - - assert set(await microgrid.connections(ends={2, 4, 5})) == { - Connection(1, 2), - Connection(2, 4), - Connection(2, 5), - Connection(4, 5), - Connection(5, 4), - } + client.mock_stub.reset_mock() + await client.connections(ends={2, 4, 5}) + assert_filter(starts=set(), ends={2, 4, 5}) # different filters combine with AND logic - assert set(await microgrid.connections(starts={1, 2, 4}, ends={4, 5, 6})) == { - Connection(2, 4), - Connection(2, 5), - Connection(4, 5), - Connection(4, 6), - } - - assert set(await microgrid.connections(starts={3, 5}, ends={7, 8})) == { - Connection(5, 7), - Connection(5, 8), - } - - assert set(await microgrid.connections(starts={1, 5}, ends={2, 7})) == { - Connection(1, 2), - Connection(5, 7), - } + client.mock_stub.reset_mock() + await client.connections(starts={1, 2, 4}, ends={4, 5, 6}) + assert_filter(starts={1, 2, 4}, ends={4, 5, 6}) -async def test_bad_connections() -> None: - """Validate that the client does not apply connection filters itself.""" - - class BadServicer(mock_api.MockMicrogridServicer): - # pylint: disable=unused-argument,invalid-name - def ListConnections( - self, - request: PbConnectionFilter, - context: grpc.ServicerContext, - ) -> PbConnectionList: - """Ignores supplied `PbConnectionFilter`.""" - return PbConnectionList(connections=self._connections) - - def ListAllComponents( - self, request: Empty, context: grpc.ServicerContext - ) -> PbComponentList: - return PbComponentList(components=self._components) - - assert not list(await microgrid.connections()) - for component_id in [1, 2, 3, 4, 5, 6, 7, 8, 9]: - servicer.add_component( - component_id, - PbComponentCategory.COMPONENT_CATEGORY_BATTERY, - ) - servicer.set_connections( - [ - (1, 2), - (1, 9), - (2, 3), - (3, 4), - (4, 5), - (5, 6), - (6, 7), - (7, 6), - (7, 9), - ] +async def test_connections_grpc_error() -> None: + """Test the components() method when the gRPC call fails.""" + client = _TestClient() + client.mock_stub.list_connections.side_effect = grpclib.GRPCError( + mock.MagicMock(name="mock status"), "fake grpc error" + ) + with pytest.raises( + ClientError, + match="Failed to list connections. Microgrid API: mock_host:1234. Err: .*fake grpc error", + ): + await client.connections() + + +@pytest.fixture +def meter83() -> microgrid.Component: + """Return a test meter component.""" + return microgrid.Component( + id=83, category=components.ComponentCategory.COMPONENT_CATEGORY_METER ) - unfiltered = { - Connection(1, 2), - Connection(1, 9), - Connection(2, 3), - Connection(3, 4), - Connection(4, 5), - Connection(5, 6), - Connection(6, 7), - Connection(7, 6), - Connection(7, 9), - } - - # because the application of filters is left to the server side, - # it doesn't matter what filters we set in the client if the - # server doesn't do its part - assert set(await microgrid.connections()) == unfiltered - assert set(await microgrid.connections(starts={1})) == unfiltered - assert set(await microgrid.connections(ends={9})) == unfiltered - assert set(await microgrid.connections(starts={1, 7}, ends={3, 9})) == unfiltered +@pytest.fixture +def battery38() -> microgrid.Component: + """Return a test battery component.""" + return microgrid.Component( + id=38, category=components.ComponentCategory.COMPONENT_CATEGORY_BATTERY + ) -async def test_meter_data() -> None: - """Test the meter_data() method.""" - servicer.add_component(83, PbComponentCategory.COMPONENT_CATEGORY_METER) - servicer.add_component(38, PbComponentCategory.COMPONENT_CATEGORY_BATTERY) - with pytest.raises(ValueError): - # should raise a ValueError for missing component_id - await microgrid.meter_data(20) +@pytest.fixture +def inverter99() -> microgrid.Component: + """Return a test inverter component.""" + return microgrid.Component( + id=99, category=components.ComponentCategory.COMPONENT_CATEGORY_INVERTER + ) - with pytest.raises(ValueError): - # should raise a ValueError for wrong component category - await microgrid.meter_data(38) - receiver = await microgrid.meter_data(83) - await asyncio.sleep(0.2) +@pytest.fixture +def ev_charger101() -> microgrid.Component: + """Return a test EV charger component.""" + return microgrid.Component( + id=101, category=components.ComponentCategory.COMPONENT_CATEGORY_EV_CHARGER + ) -latest = await anext(receiver) -assert isinstance(latest, MeterData) -assert latest.component_id == 83 +@pytest.fixture +def component_list( + meter83: microgrid.Component, + battery38: microgrid.Component, + inverter99: microgrid.Component, + ev_charger101: microgrid.Component, +) -> list[microgrid.Component]: + """Return a list of test components.""" + return [meter83, battery38, inverter99, ev_charger101] -async def test_battery_data() -> None: - """Test the battery_data() method.""" - servicer.add_component(83, PbComponentCategory.COMPONENT_CATEGORY_BATTERY) - servicer.add_component(38, PbComponentCategory.COMPONENT_CATEGORY_INVERTER) - with pytest.raises(ValueError): - # should raise a ValueError for missing component_id - await microgrid.meter_data(20) +@pytest.mark.parametrize("method", ["meter_data", "battery_data", "inverter_data"]) +async def test_data_component_not_found(method: str) -> None: + """Test the meter_data() method.""" + client = _TestClient() + client.mock_stub.list_components.return_value = microgrid.ComponentList() + + # It should raise a ValueError for a missing component_id + with pytest.raises(ValueError, match="Unable to find component with id 20"): + await getattr(client, method)(20) + + +@pytest.mark.parametrize( + "method, component_id", + [ + ("meter_data", 38), + ("battery_data", 83), + ("inverter_data", 83), + ("ev_charger_data", 99), + ], +) +async def test_data_bad_category( + method: str, component_id: int, component_list: list[microgrid.Component] +) -> None: + """Test the meter_data() method.""" + client = _TestClient() + client.mock_stub.list_components.return_value = microgrid.ComponentList( + components=component_list + ) - with pytest.raises(ValueError): - # should raise a ValueError for wrong component category - await microgrid.meter_data(38) - receiver = await microgrid.battery_data(83) - await asyncio.sleep(0.2) + # It should raise a ValueError for a wrong component category + with pytest.raises( + ValueError, match=f"Component id {component_id} is a .*, not a {method[:-5]}" + ): + await getattr(client, method)(component_id) + + +@pytest.mark.parametrize( + "method, component_id, component_class", + [ + ("meter_data", 83, MeterData), + ("battery_data", 38, BatteryData), + ("inverter_data", 99, InverterData), + ("ev_charger_data", 101, EVChargerData), + ], +) +async def test_component_data( + method: str, + component_id: int, + component_class: type[ComponentData], + component_list: list[microgrid.Component], +) -> None: + """Test the meter_data() method.""" + client = _TestClient() + client.mock_stub.list_components.return_value = microgrid.ComponentList( + components=component_list + ) - latest = await anext(receiver) - assert isinstance(latest, BatteryData) - assert latest.component_id == 83 + async def stream_data( + *args: Any, **kwargs: Any # pylint: disable=unused-argument + ) -> AsyncIterator[microgrid.ComponentData]: + yield microgrid.ComponentData(id=component_id) + client.mock_stub.stream_component_data.side_effect = stream_data + receiver = await getattr(client, method)(component_id) + async with AsyncExitStack() as stack: + stack.push_async_callback( + client._broadcasters[component_id].stop # pylint: disable=protected-access + ) + latest = await receiver.receive() + assert isinstance(latest, component_class) + assert latest.component_id == component_id + + +@pytest.mark.parametrize( + "method, component_id, component_class", + [ + ("meter_data", 83, MeterData), + ("battery_data", 38, BatteryData), + ("inverter_data", 99, InverterData), + ("ev_charger_data", 101, EVChargerData), + ], +) +async def test_component_data_grpc_error( + method: str, + component_id: int, + component_class: type[ComponentData], + component_list: list[microgrid.Component], + caplog: pytest.LogCaptureFixture, +) -> None: + """Test the components() method when the gRPC call fails.""" + caplog.set_level(logging.WARNING) + client = _TestClient( + retry_strategy=retry.LinearBackoff(interval=0.0, jitter=0.0, limit=6) + ) + client.mock_stub.list_components.return_value = microgrid.ComponentList( + components=component_list + ) -async def test_inverter_data() -> None: - """Test the inverter_data() method.""" - servicer.add_component(83, PbComponentCategory.COMPONENT_CATEGORY_INVERTER) - servicer.add_component(38, PbComponentCategory.COMPONENT_CATEGORY_BATTERY) + num_calls = 0 + + async def stream_data( + *args: Any, **kwargs: Any # pylint: disable=unused-argument + ) -> AsyncIterator[microgrid.ComponentData]: + nonlocal num_calls + num_calls += 1 + if num_calls % 2: + raise grpclib.GRPCError( + mock.MagicMock(name="mock status"), f"fake grpc error {num_calls}" + ) + yield microgrid.ComponentData(id=component_id) + + client.mock_stub.stream_component_data.side_effect = stream_data + receiver = await getattr(client, method)(component_id) + async with AsyncExitStack() as stack: + stack.push_async_callback( + client._broadcasters[component_id].stop # pylint: disable=protected-access + ) + latest = await receiver.receive() + assert isinstance(latest, component_class) + assert latest.component_id == component_id + + latest = await receiver.receive() + assert isinstance(latest, component_class) + assert latest.component_id == component_id + + latest = await receiver.receive() + assert isinstance(latest, component_class) + assert latest.component_id == component_id + + # This is not super portable, it will change if the GrpcStreamBroadcaster changes, + # but without this there isn't much to check by this test. + assert len(caplog.record_tuples) == 6 + for n, log_tuple in enumerate(caplog.record_tuples): + assert log_tuple[0] == "frequenz.client.base.streaming" + assert log_tuple[1] == logging.WARNING + assert ( + f"raw-component-data-{component_id}: connection ended, retrying" + in log_tuple[2] + ) + if n % 2: + assert "Stream exhausted" in log_tuple[2] + else: + assert f"fake grpc error {n+1}" in log_tuple[2] + + +@pytest.mark.parametrize("power_w", [0, 0.0, 12, -75, 0.1, -0.0001, 134.0]) +async def test_set_power_ok(power_w: float, meter83: microgrid.Component) -> None: + """Test if charge is able to charge component.""" + client = _TestClient() + client.mock_stub.list_components.return_value = microgrid.ComponentList( + components=[meter83] + ) - with pytest.raises(ValueError): - # should raise a ValueError for missing component_id - await microgrid.meter_data(20) + await client.set_power(component_id=83, power_w=power_w) + client.mock_stub.set_power_active.assert_called_once() + call_args = client.mock_stub.set_power_active.call_args[0] + assert call_args[0] == microgrid.SetPowerActiveParam(component_id=83, power=power_w) - with pytest.raises(ValueError): - # should raise a ValueError for wrong component category - await microgrid.meter_data(38) - receiver = await microgrid.inverter_data(83) - await asyncio.sleep(0.2) - latest = await anext(receiver) - assert isinstance(latest, InverterData) - assert latest.component_id == 83 +async def test_set_power_grpc_error() -> None: + """Test set_power() raises ClientError when the gRPC call fails.""" + client = _TestClient() + client.mock_stub.set_power_active.side_effect = grpclib.GRPCError( + mock.MagicMock(name="mock status"), "fake grpc error" + ) + with pytest.raises( + ClientError, + match="Failed to set power. Microgrid API: mock_host:1234. Err: .*fake grpc error", + ): + await client.set_power(component_id=83, power_w=100.0) + + +@pytest.mark.parametrize( + "bounds", + [ + metrics.Bounds(lower=0.0, upper=0.0), + metrics.Bounds(lower=0.0, upper=2.0), + metrics.Bounds(lower=-10.0, upper=0.0), + metrics.Bounds(lower=-10.0, upper=2.0), + ], + ids=str, +) +async def test_set_bounds_ok( + bounds: metrics.Bounds, inverter99: microgrid.Component +) -> None: + """Test if charge is able to charge component.""" + client = _TestClient() + client.mock_stub.list_components.return_value = microgrid.ComponentList( + components=[inverter99] + ) + await client.set_bounds(99, bounds.lower, bounds.upper) + client.mock_stub.add_inclusion_bounds.assert_called_once() + call_args = client.mock_stub.add_inclusion_bounds.call_args[0] + assert call_args[0] == microgrid.SetBoundsParam( + component_id=99, + target_metric=microgrid.SetBoundsParamTargetMetric.TARGET_METRIC_POWER_ACTIVE, + bounds=bounds, + ) -async def test_ev_charger_data() -> None: - """Test the ev_charger_data() method.""" - servicer.add_component(83, PbComponentCategory.COMPONENT_CATEGORY_EV_CHARGER) - servicer.add_component(38, PbComponentCategory.COMPONENT_CATEGORY_BATTERY) - with pytest.raises(ValueError): - # should raise a ValueError for missing component_id - await microgrid.meter_data(20) +@pytest.mark.parametrize( + "bounds", + [ + metrics.Bounds(lower=0.0, upper=-2.0), + metrics.Bounds(lower=10.0, upper=-2.0), + metrics.Bounds(lower=10.0, upper=0.0), + ], + ids=str, +) +async def test_set_bounds_fail( + bounds: metrics.Bounds, inverter99: microgrid.Component +) -> None: + """Test if charge is able to charge component.""" + client = _TestClient() + client.mock_stub.list_components.return_value = microgrid.ComponentList( + components=[inverter99] + ) with pytest.raises(ValueError): - # should raise a ValueError for wrong component category - await microgrid.meter_data(38) - receiver = await microgrid.ev_charger_data(83) - await asyncio.sleep(0.2) - - latest = await anext(receiver) - assert isinstance(latest, EVChargerData) - assert latest.component_id == 83 - - -async def test_charge() -> None: - """Check if charge is able to charge component.""" - servicer.add_component(83, PbComponentCategory.COMPONENT_CATEGORY_METER) - - await microgrid.set_power(component_id=83, power_w=12) - - assert servicer.latest_power is not None - assert servicer.latest_power.component_id == 83 - assert servicer.latest_power.power == 12 - - -async def test_discharge() -> None: - """Check if discharge is able to discharge component.""" - servicer.add_component(73, PbComponentCategory.COMPONENT_CATEGORY_METER) - - await microgrid.set_power(component_id=73, power_w=-15) - - assert servicer.latest_power is not None - assert servicer.latest_power.component_id == 73 - assert servicer.latest_power.power == -15 - - -async def test_set_bounds() -> None: - """Check if set_bounds is able to set bounds for component.""" - servicer.add_component(38, PbComponentCategory.COMPONENT_CATEGORY_INVERTER) - - num_calls = 4 - - target_metric = PbSetBoundsParam.TargetMetric - expected_bounds = [ - PbSetBoundsParam( - component_id=comp_id, - target_metric=target_metric.TARGET_METRIC_POWER_ACTIVE, - bounds=PbBounds(lower=-10, upper=2), - ) - for comp_id in range(num_calls) - ] - for cid in range(num_calls): - await microgrid.set_bounds(cid, -10.0, 2.0) - await asyncio.sleep(0.1) - - assert len(expected_bounds) == len(servicer.get_bounds()) + await client.set_bounds(99, bounds.lower, bounds.upper) + client.mock_stub.add_inclusion_bounds.assert_not_called() - def sort_key( - bound: PbSetBoundsParam, - ) -> PbSetBoundsParam.TargetMetric.ValueType: - return bound.target_metric - assert sorted(servicer.get_bounds(), key=sort_key) == sorted( - expected_bounds, key=sort_key +async def test_set_bounds_grpc_error() -> None: + """Test the components() method when the gRPC call fails.""" + client = _TestClient() + client.mock_stub.add_inclusion_bounds.side_effect = grpclib.GRPCError( + mock.MagicMock(name="mock status"), "fake grpc error" ) + with pytest.raises( + ClientError, + match="Failed to set inclusion bounds. Microgrid API: mock_host:1234. " + "Err: .*fake grpc error", + ): + await client.set_bounds(99, 0.0, 100.0) From 0723979aa8921539c866e6b97465f7548cdb4340 Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Mon, 13 May 2024 10:31:22 +0200 Subject: [PATCH 08/10] Remove the timeout tests The timeouts are now tested as part of gRPC failure tests in the client tests, since timeouts are handled by `grpclib` there isn't much we can specifically test about timeouts that is not just testing `grpclib` itself. Signed-off-by: Leandro Lucarella --- tests/test_timeout.py | 159 ------------------------------------------ 1 file changed, 159 deletions(-) delete mode 100644 tests/test_timeout.py diff --git a/tests/test_timeout.py b/tests/test_timeout.py deleted file mode 100644 index bd30d90..0000000 --- a/tests/test_timeout.py +++ /dev/null @@ -1,159 +0,0 @@ -# License: MIT -# Copyright © 2022 Frequenz Energy-as-a-Service GmbH - -"""Tests for the timeout handling in the client.""" - -import time -from typing import Any, Iterator -from unittest.mock import patch - -import grpc.aio -import pytest - -# pylint: disable=no-name-in-module -from frequenz.api.microgrid.microgrid_pb2 import ( - ComponentFilter, - ComponentList, - ConnectionFilter, - ConnectionList, - PowerLevelParam, - SetBoundsParam, -) -from google.protobuf.empty_pb2 import Empty -from google.protobuf.timestamp_pb2 import Timestamp - -# pylint: enable=no-name-in-module -from pytest_mock import MockerFixture - -from frequenz.client.microgrid import ApiClient, ClientError - -from .mock_api import MockGrpcServer, MockMicrogridServicer - -# How much late a response to a gRPC call should be. It is used to trigger a timeout -# error and needs to be greater than `GRPC_CALL_TIMEOUT`. -GRPC_SERVER_DELAY: float = 0.3 - - -@pytest.fixture(autouse=True) -def fake_grpc_call_timeout() -> Iterator[float]: - """Patch the default gRPC call timeout.""" - # Timeout applied to all gRPC calls under test. It is expected after that the gRPC - # calls will raise an ClientError with status code equal to DEADLINE_EXCEEDED. - grpc_call_timeout: float = 0.1 - - with patch( - "frequenz.client.microgrid._client.DEFAULT_GRPC_CALL_TIMEOUT", - grpc_call_timeout, - ): - yield grpc_call_timeout - - -async def test_components_timeout(mocker: MockerFixture) -> None: - """Test if the components() method properly raises a timeeout ClientError.""" - servicer = MockMicrogridServicer() - - def mock_list_components( - request: ComponentFilter, context: Any # pylint: disable=unused-argument - ) -> ComponentList: - time.sleep(GRPC_SERVER_DELAY) - return ComponentList(components=[]) - - mocker.patch.object(servicer, "ListComponents", mock_list_components) - server = MockGrpcServer(servicer, port=57809) - await server.start() - - target = "[::]:57809" - grpc_channel = grpc.aio.insecure_channel(target) - client = ApiClient(grpc_channel=grpc_channel, target=target) - - with pytest.raises(ClientError) as err_ctx: - _ = await client.components() - cause = err_ctx.value.__cause__ - assert isinstance(cause, grpc.aio.AioRpcError) - assert cause.code() == grpc.StatusCode.DEADLINE_EXCEEDED - assert await server.graceful_shutdown() - - -async def test_connections_timeout(mocker: MockerFixture) -> None: - """Test if the connections() method properly raises a timeout ClientError.""" - servicer = MockMicrogridServicer() - - def mock_list_connections( - request: ConnectionFilter, context: Any # pylint: disable=unused-argument - ) -> ConnectionList: - time.sleep(GRPC_SERVER_DELAY) - return ConnectionList(connections=[]) - - mocker.patch.object(servicer, "ListConnections", mock_list_connections) - server = MockGrpcServer(servicer, port=57809) - await server.start() - - target = "[::]:57809" - grpc_channel = grpc.aio.insecure_channel(target) - client = ApiClient(grpc_channel=grpc_channel, target=target) - - with pytest.raises(ClientError) as err_ctx: - _ = await client.connections() - cause = err_ctx.value.__cause__ - assert isinstance(cause, grpc.aio.AioRpcError) - assert cause.code() == grpc.StatusCode.DEADLINE_EXCEEDED - assert await server.graceful_shutdown() - - -async def test_set_power_timeout(mocker: MockerFixture) -> None: - """Test if the set_power() method properly raises a timeout ClientError.""" - servicer = MockMicrogridServicer() - - def mock_set_power( - request: PowerLevelParam, context: Any # pylint: disable=unused-argument - ) -> Empty: - time.sleep(GRPC_SERVER_DELAY) - return Empty() - - mocker.patch.object(servicer, "SetPowerActive", mock_set_power) - server = MockGrpcServer(servicer, port=57809) - await server.start() - - target = "[::]:57809" - grpc_channel = grpc.aio.insecure_channel(target) - client = ApiClient(grpc_channel=grpc_channel, target=target) - - power_values = [-100, 100] - for power_w in power_values: - with pytest.raises(ClientError) as err_ctx: - await client.set_power(component_id=1, power_w=power_w) - cause = err_ctx.value.__cause__ - assert isinstance(cause, grpc.aio.AioRpcError) - assert cause.code() == grpc.StatusCode.DEADLINE_EXCEEDED - - assert await server.graceful_shutdown() - - -async def test_set_bounds_timeout(mocker: MockerFixture) -> None: - """Test if the set_power() method properly raises a timeout AioRpcError.""" - servicer = MockMicrogridServicer() - - def mock_add_inclusion_bounds( - request: SetBoundsParam, context: Any # pylint: disable=unused-argument - ) -> Timestamp: - time.sleep(GRPC_SERVER_DELAY) - return Timestamp() - - mocker.patch.object(servicer, "AddInclusionBounds", mock_add_inclusion_bounds) - server = MockGrpcServer(servicer, port=57809) - await server.start() - - target = "[::]:57809" - grpc_channel = grpc.aio.insecure_channel(target) - client = ApiClient(grpc_channel=grpc_channel, target=target) - - bounds_values = [{"lower": 0.0, "upper": 100.0}, {"lower": -10.0, "upper": 1.0}] - - for bounds in bounds_values: - with pytest.raises(ClientError) as err_ctx: - await client.set_bounds(component_id=1, **bounds) - cause = err_ctx.value.__cause__ - assert isinstance(cause, grpc.aio.AioRpcError) - assert cause.code() == grpc.StatusCode.DEADLINE_EXCEEDED - - assert await server.graceful_shutdown() From a6d8ec7a7b6105ffed177e5ce8bc56b1459aaa40 Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Mon, 13 May 2024 10:32:14 +0200 Subject: [PATCH 09/10] Remove `mock_api` and its tests We don't use this module anymore and we don't plan to use it for now as it is very flaky. In the future we might use something similar to do integration tests. Signed-off-by: Leandro Lucarella --- tests/mock_api.py | 337 ----------------------------------------- tests/test_mock_api.py | 288 ----------------------------------- 2 files changed, 625 deletions(-) delete mode 100644 tests/mock_api.py delete mode 100644 tests/test_mock_api.py diff --git a/tests/mock_api.py b/tests/mock_api.py deleted file mode 100644 index 3d8380d..0000000 --- a/tests/mock_api.py +++ /dev/null @@ -1,337 +0,0 @@ -# License: MIT -# Copyright © 2022 Frequenz Energy-as-a-Service GmbH - -"""Mock implementation of the microgrid gRPC API. - -This is intended to support the narrow set of test cases that have to -check integration with the API. Note that this should exclude almost -all framework code, as API integration should be highly encapsulated. -""" - -import asyncio -from collections.abc import Iterable, Iterator -from concurrent import futures - -import grpc.aio - -# pylint: disable=no-name-in-module -from frequenz.api.common.components_pb2 import ( - COMPONENT_CATEGORY_BATTERY, - COMPONENT_CATEGORY_EV_CHARGER, - COMPONENT_CATEGORY_INVERTER, - COMPONENT_CATEGORY_METER, - ComponentCategory, - InverterType, -) -from frequenz.api.common.metrics.electrical_pb2 import AC -from frequenz.api.common.metrics_pb2 import Metric, MetricAggregation -from frequenz.api.microgrid.battery_pb2 import Battery -from frequenz.api.microgrid.battery_pb2 import Data as BatteryData -from frequenz.api.microgrid.ev_charger_pb2 import EvCharger -from frequenz.api.microgrid.grid_pb2 import Metadata as GridMetadata -from frequenz.api.microgrid.inverter_pb2 import Inverter -from frequenz.api.microgrid.inverter_pb2 import Metadata as InverterMetadata -from frequenz.api.microgrid.meter_pb2 import Data as MeterData -from frequenz.api.microgrid.meter_pb2 import Meter -from frequenz.api.microgrid.microgrid_pb2 import ( - Component, - ComponentData, - ComponentFilter, - ComponentIdParam, - ComponentList, - Connection, - ConnectionFilter, - ConnectionList, - MicrogridMetadata, - SetBoundsParam, - SetPowerActiveParam, - SetPowerReactiveParam, -) -from frequenz.api.microgrid.microgrid_pb2_grpc import ( - MicrogridServicer, - add_MicrogridServicer_to_server, -) -from google.protobuf.empty_pb2 import Empty -from google.protobuf.timestamp_pb2 import Timestamp -from google.protobuf.wrappers_pb2 import BoolValue - -# pylint: enable=no-name-in-module -from typing_extensions import override - - -class MockMicrogridServicer( # pylint: disable=too-many-public-methods - MicrogridServicer -): - """Servicer implementation mock for the microgrid API. - - This class implements customizable mocks of the individual API methods, - and can be bound to a gRPC server instance to create the complete API - mock. - """ - - def __init__( - self, - components: list[tuple[int, ComponentCategory.ValueType]] | None = None, - connections: list[tuple[int, int]] | None = None, - ) -> None: - """Create a MockMicrogridServicer instance.""" - self._components: list[Component] = [] - self._connections: list[Connection] = [] - self._bounds: list[SetBoundsParam] = [] - - if components is not None: - self.set_components(components) - if connections is not None: - self.set_connections(connections) - - self._latest_power: SetPowerActiveParam | None = None - - def add_component( - self, - component_id: int, - component_category: ComponentCategory.ValueType, - max_current: float | None = None, - inverter_type: InverterType.ValueType = InverterType.INVERTER_TYPE_UNSPECIFIED, - ) -> None: - """Add a component to the mock service.""" - if component_category == ComponentCategory.COMPONENT_CATEGORY_INVERTER: - self._components.append( - Component( - id=component_id, - category=component_category, - inverter=InverterMetadata(type=inverter_type), - ) - ) - elif ( - component_category == ComponentCategory.COMPONENT_CATEGORY_GRID - and max_current is not None - ): - self._components.append( - Component( - id=component_id, - category=component_category, - grid=GridMetadata(rated_fuse_current=int(max_current)), - ) - ) - else: - self._components.append( - Component(id=component_id, category=component_category) - ) - - def add_connection(self, start: int, end: int) -> None: - """Add a connection to the mock service.""" - self._connections.append(Connection(start=start, end=end)) - - def set_components( - self, components: list[tuple[int, ComponentCategory.ValueType]] - ) -> None: - """Set components to mock service, dropping existing.""" - self._components.clear() - self._components.extend( - map(lambda c: Component(id=c[0], category=c[1]), components) - ) - - def set_connections(self, connections: list[tuple[int, int]]) -> None: - """Set connections to mock service, dropping existing.""" - self._connections.clear() - self._connections.extend( - map(lambda c: Connection(start=c[0], end=c[1]), connections) - ) - - @property - def latest_power(self) -> SetPowerActiveParam | None: - """Get argumetns of the latest charge request.""" - return self._latest_power - - def get_bounds(self) -> list[SetBoundsParam]: - """Return the list of received bounds.""" - return self._bounds - - def clear_bounds(self) -> None: - """Drop all received bounds.""" - self._bounds.clear() - - @override - def ListComponents( # pylint: disable=unused-argument - self, - request: ComponentFilter, - context: grpc.ServicerContext, - ) -> ComponentList: - """List components.""" - return ComponentList(components=self._components) - - @override - def ListConnections( - self, request: ConnectionFilter, context: grpc.ServicerContext - ) -> ConnectionList: - """Return a list of all connections.""" - connections: Iterable[Connection] = self._connections - if request.starts is not None and len(request.starts) > 0: - connections = filter(lambda c: c.start in request.starts, connections) - if request.ends is not None and len(request.ends) > 0: - connections = filter(lambda c: c.end in request.ends, connections) - return ConnectionList(connections=connections) - - @override - def StreamComponentData( - self, request: ComponentIdParam, context: grpc.ServicerContext - ) -> Iterator[ComponentData]: - """Return an iterator for mock ComponentData.""" - # pylint: disable=stop-iteration-return - component = next(filter(lambda c: c.id == request.id, self._components)) - - def next_msg() -> ComponentData: - ts = Timestamp() - ts.GetCurrentTime() - if component.category == COMPONENT_CATEGORY_BATTERY: - return ComponentData( - id=request.id, - ts=ts, - battery=Battery( - data=BatteryData( - soc=MetricAggregation(avg=float(request.id % 100)), - ) - ), - ) - if component.category == COMPONENT_CATEGORY_METER: - return ComponentData( - id=request.id, - ts=ts, - meter=Meter( - data=MeterData( - ac=AC( - power_active=Metric(value=100.0), - ), - ) - ), - ) - if component.category == COMPONENT_CATEGORY_INVERTER: - return ComponentData(id=request.id, inverter=Inverter()) - if component.category == COMPONENT_CATEGORY_EV_CHARGER: - return ComponentData(id=request.id, ev_charger=EvCharger()) - return ComponentData() - - num_messages = 3 - for _ in range(num_messages): - msg = next_msg() - yield msg - - @override - def SetPowerActive( - self, request: SetPowerActiveParam, context: grpc.ServicerContext - ) -> Empty: - """Microgrid service SetPowerActive method stub.""" - self._latest_power = request - return Empty() - - @override - def SetPowerReactive( - self, request: SetPowerReactiveParam, context: grpc.ServicerContext - ) -> Empty: - """Microgrid service SetPowerReactive method stub.""" - return Empty() - - @override - def GetMicrogridMetadata( - self, request: Empty, context: grpc.ServicerContext - ) -> MicrogridMetadata: - """Microgrid service GetMicrogridMetadata method stub.""" - return MicrogridMetadata() - - @override - def CanStreamData( - self, request: ComponentIdParam, context: grpc.ServicerContext - ) -> BoolValue: - """Microgrid service CanStreamData method stub.""" - return BoolValue(value=True) - - @override - def AddExclusionBounds( - self, request: SetBoundsParam, context: grpc.ServicerContext - ) -> Timestamp: - """Microgrid service AddExclusionBounds method stub.""" - return Timestamp() - - @override - def AddInclusionBounds( - self, request: SetBoundsParam, context: grpc.ServicerContext - ) -> Timestamp: - """Microgrid service AddExclusionBounds method stub.""" - self._bounds.append(request) - return Timestamp() - - @override - def HotStandby( - self, request: ComponentIdParam, context: grpc.ServicerContext - ) -> Empty: - """Microgrid service HotStandby method stub.""" - return Empty() - - @override - def ColdStandby( - self, request: ComponentIdParam, context: grpc.ServicerContext - ) -> Empty: - """Microgrid service ColdStandby method stub.""" - return Empty() - - @override - def ErrorAck( - self, request: ComponentIdParam, context: grpc.ServicerContext - ) -> Empty: - """Microgrid service ErrorAck method stub.""" - return Empty() - - @override - def Start(self, request: ComponentIdParam, context: grpc.ServicerContext) -> Empty: - """Microgrid service Start method stub.""" - return Empty() - - @override - def Stop(self, request: ComponentIdParam, context: grpc.ServicerContext) -> Empty: - """Microgrid service Stop method stub.""" - return Empty() - - -class MockGrpcServer: - """Helper class to instantiate a gRPC server for a microgrid servicer.""" - - def __init__( - self, servicer: MicrogridServicer, host: str = "[::]", port: int = 61060 - ) -> None: - """Create a MockGrpcServicer instance.""" - self._server = grpc.aio.server(futures.ThreadPoolExecutor(max_workers=20)) - add_MicrogridServicer_to_server(servicer, self._server) - self._server.add_insecure_port(f"{host}:{port}") - - async def start(self) -> None: - """Start the server.""" - await self._server.start() - - async def _stop(self, grace: float | None) -> None: - """Stop the server.""" - await self._server.stop(grace) - - async def _wait_for_termination(self, timeout: float | None = None) -> None: - """Wait for termination.""" - await self._server.wait_for_termination(timeout) - - async def graceful_shutdown( - self, stop_timeout: float = 0.1, terminate_timeout: float = 0.2 - ) -> bool: - """Shutdown server gracefully. - - Args: - stop_timeout: Argument for self.stop method - terminate_timeout: Argument for self.wait_for_termination method. - - Returns: - True if server was stopped in given timeout. False otherwise. - """ - await self._stop(stop_timeout) - try: - await asyncio.wait_for( - self._wait_for_termination(None), timeout=terminate_timeout - ) - except TimeoutError: - return False - return True diff --git a/tests/test_mock_api.py b/tests/test_mock_api.py deleted file mode 100644 index 998a0dd..0000000 --- a/tests/test_mock_api.py +++ /dev/null @@ -1,288 +0,0 @@ -# License: MIT -# Copyright © 2022 Frequenz Energy-as-a-Service GmbH - -"""Tests for the microgrid mock api.""" - -# pylint: disable=missing-function-docstring,use-implicit-booleaness-not-comparison -# pylint: disable=invalid-name,no-name-in-module,no-member - -from unittest.mock import Mock - -import grpc.aio -from frequenz.api.common.components_pb2 import ComponentCategory -from frequenz.api.microgrid.microgrid_pb2 import ( - Component, - ComponentFilter, - Connection, - ConnectionFilter, -) -from frequenz.api.microgrid.microgrid_pb2_grpc import MicrogridStub -from google.protobuf.empty_pb2 import Empty - -from . import mock_api - - -def test_MockMicrogridServicer() -> None: - """Test the MockMicrogridServicer.""" - api = mock_api.MockMicrogridServicer() - service_context_mock = Mock(spec=grpc.ServicerContext) - assert ( - list(api.ListComponents(ComponentFilter(), service_context_mock).components) - == [] - ) - assert ( - list(api.ListConnections(ConnectionFilter(), service_context_mock).connections) - == [] - ) - - # adding new components just appends them to the list: - # duplicates are not prevented, as this may be wanted - # behaviour of the mock - api.add_component(0, ComponentCategory.COMPONENT_CATEGORY_METER) - assert list( - api.ListComponents(ComponentFilter(), service_context_mock).components - ) == [Component(id=0, category=ComponentCategory.COMPONENT_CATEGORY_METER)] - assert ( - list(api.ListConnections(ConnectionFilter(), service_context_mock).connections) - == [] - ) - - api.add_component(0, ComponentCategory.COMPONENT_CATEGORY_BATTERY) - assert list( - api.ListComponents(ComponentFilter(), service_context_mock).components - ) == [ - Component(id=0, category=ComponentCategory.COMPONENT_CATEGORY_METER), - Component(id=0, category=ComponentCategory.COMPONENT_CATEGORY_BATTERY), - ] - assert ( - list(api.ListConnections(ConnectionFilter(), service_context_mock).connections) - == [] - ) - - api.add_component(0, ComponentCategory.COMPONENT_CATEGORY_METER) - assert list( - api.ListComponents(ComponentFilter(), service_context_mock).components - ) == [ - Component(id=0, category=ComponentCategory.COMPONENT_CATEGORY_METER), - Component(id=0, category=ComponentCategory.COMPONENT_CATEGORY_BATTERY), - Component(id=0, category=ComponentCategory.COMPONENT_CATEGORY_METER), - ] - assert ( - list(api.ListConnections(ConnectionFilter(), service_context_mock).connections) - == [] - ) - - # similarly, duplicates are allowed when adding new connections - api.add_connection(0, 0) - assert list( - api.ListComponents(ComponentFilter(), service_context_mock).components - ) == [ - Component(id=0, category=ComponentCategory.COMPONENT_CATEGORY_METER), - Component(id=0, category=ComponentCategory.COMPONENT_CATEGORY_BATTERY), - Component(id=0, category=ComponentCategory.COMPONENT_CATEGORY_METER), - ] - assert list( - api.ListConnections(ConnectionFilter(), service_context_mock).connections - ) == [Connection(start=0, end=0)] - - api.add_connection(7, 9) - assert list( - api.ListComponents(ComponentFilter(), service_context_mock).components - ) == [ - Component(id=0, category=ComponentCategory.COMPONENT_CATEGORY_METER), - Component(id=0, category=ComponentCategory.COMPONENT_CATEGORY_BATTERY), - Component(id=0, category=ComponentCategory.COMPONENT_CATEGORY_METER), - ] - assert list( - api.ListConnections(ConnectionFilter(), service_context_mock).connections - ) == [ - Connection(start=0, end=0), - Connection(start=7, end=9), - ] - - api.add_connection(0, 0) - assert list( - api.ListComponents(ComponentFilter(), service_context_mock).components - ) == [ - Component(id=0, category=ComponentCategory.COMPONENT_CATEGORY_METER), - Component(id=0, category=ComponentCategory.COMPONENT_CATEGORY_BATTERY), - Component(id=0, category=ComponentCategory.COMPONENT_CATEGORY_METER), - ] - assert list( - api.ListConnections(ConnectionFilter(), service_context_mock).connections - ) == [ - Connection(start=0, end=0), - Connection(start=7, end=9), - Connection(start=0, end=0), - ] - - # `set_components` overrides all the components but leaves - # the connections alone - api.set_components( - [ - (9, ComponentCategory.COMPONENT_CATEGORY_METER), - (99, ComponentCategory.COMPONENT_CATEGORY_INVERTER), - (999, ComponentCategory.COMPONENT_CATEGORY_BATTERY), - ] - ) - assert list( - api.ListComponents(ComponentFilter(), service_context_mock).components - ) == [ - Component(id=9, category=ComponentCategory.COMPONENT_CATEGORY_METER), - Component(id=99, category=ComponentCategory.COMPONENT_CATEGORY_INVERTER), - Component(id=999, category=ComponentCategory.COMPONENT_CATEGORY_BATTERY), - ] - assert list( - api.ListConnections(ConnectionFilter(), service_context_mock).connections - ) == [ - Connection(start=0, end=0), - Connection(start=7, end=9), - Connection(start=0, end=0), - ] - - # similarly `set_connections` overrides all the existing connections - api.set_connections([(999, 9), (99, 19), (909, 101), (99, 91)]) - assert list( - api.ListComponents(ComponentFilter(), service_context_mock).components - ) == [ - Component(id=9, category=ComponentCategory.COMPONENT_CATEGORY_METER), - Component(id=99, category=ComponentCategory.COMPONENT_CATEGORY_INVERTER), - Component(id=999, category=ComponentCategory.COMPONENT_CATEGORY_BATTERY), - ] - assert list( - api.ListConnections(ConnectionFilter(), service_context_mock).connections - ) == [ - Connection(start=999, end=9), - Connection(start=99, end=19), - Connection(start=909, end=101), - Connection(start=99, end=91), - ] - - # connection requests can be filtered - assert list( - api.ListConnections( - ConnectionFilter(starts=[999]), service_context_mock - ).connections - ) == [Connection(start=999, end=9)] - assert list( - api.ListConnections( - ConnectionFilter(starts=[99]), service_context_mock - ).connections - ) == [Connection(start=99, end=19), Connection(start=99, end=91)] - assert list( - api.ListConnections( - ConnectionFilter(ends=[19]), service_context_mock - ).connections - ) == [Connection(start=99, end=19)] - assert list( - api.ListConnections( - ConnectionFilter(ends=[101]), service_context_mock - ).connections - ) == [Connection(start=909, end=101)] - assert list( - api.ListConnections( - ConnectionFilter(starts=[99, 999], ends=[9, 19]), service_context_mock - ).connections - ) == [Connection(start=999, end=9), Connection(start=99, end=19)] - - # mock API instances can be initialized with components and connections - # already in place (note there are no checks for consistency of data so - # in this case we can include connections to a non-existent component) - api_prealloc = mock_api.MockMicrogridServicer( - components=[ - (7, ComponentCategory.COMPONENT_CATEGORY_GRID), - (9, ComponentCategory.COMPONENT_CATEGORY_METER), - ], - connections=[(7, 8), (8, 9)], - ) - assert list( - api_prealloc.ListComponents(ComponentFilter(), service_context_mock).components - ) == [ - Component(id=7, category=ComponentCategory.COMPONENT_CATEGORY_GRID), - Component(id=9, category=ComponentCategory.COMPONENT_CATEGORY_METER), - ] - assert list( - api_prealloc.ListConnections( - ConnectionFilter(), service_context_mock - ).connections - ) == [ - Connection(start=7, end=8), - Connection(start=8, end=9), - ] - - -async def test_MockGrpcServer() -> None: - """Test the MockGrpcServer.""" - servicer1 = mock_api.MockMicrogridServicer( - components=[ - (1, ComponentCategory.COMPONENT_CATEGORY_GRID), - (2, ComponentCategory.COMPONENT_CATEGORY_METER), - (3, ComponentCategory.COMPONENT_CATEGORY_INVERTER), - ], - connections=[(1, 2), (2, 3)], - ) - server1 = mock_api.MockGrpcServer(servicer1, port=57809) - await server1.start() - - client = MicrogridStub(grpc.aio.insecure_channel("[::]:57809")) - - components1 = await client.ListComponents(Empty()) # type: ignore - assert list(components1.components) == [ - Component(id=1, category=ComponentCategory.COMPONENT_CATEGORY_GRID), - Component(id=2, category=ComponentCategory.COMPONENT_CATEGORY_METER), - Component(id=3, category=ComponentCategory.COMPONENT_CATEGORY_INVERTER), - ] - - connections1 = await client.ListConnections(ConnectionFilter()) # type: ignore - assert list(connections1.connections) == [ - Connection(start=1, end=2), - Connection(start=2, end=3), - ] - - await server1.graceful_shutdown() - - servicer2 = mock_api.MockMicrogridServicer( - components=[ - (6, ComponentCategory.COMPONENT_CATEGORY_GRID), - (77, ComponentCategory.COMPONENT_CATEGORY_METER), - (888, ComponentCategory.COMPONENT_CATEGORY_EV_CHARGER), - (9999, ComponentCategory.COMPONENT_CATEGORY_INVERTER), - ], - connections=[(6, 77), (6, 888), (77, 9999)], - ) - server2 = mock_api.MockGrpcServer(servicer2, port=57809) - await server2.start() - - components2 = await client.ListComponents(Empty()) # type: ignore - assert list(components2.components) == [ - Component(id=6, category=ComponentCategory.COMPONENT_CATEGORY_GRID), - Component(id=77, category=ComponentCategory.COMPONENT_CATEGORY_METER), - Component(id=888, category=ComponentCategory.COMPONENT_CATEGORY_EV_CHARGER), - Component(id=9999, category=ComponentCategory.COMPONENT_CATEGORY_INVERTER), - ] - - connections2 = await client.ListConnections(ConnectionFilter()) # type: ignore - assert list(connections2.connections) == [ - Connection(start=6, end=77), - Connection(start=6, end=888), - Connection(start=77, end=9999), - ] - - connections2b = await client.ListConnections(ConnectionFilter(starts=[6])) # type: ignore - assert list(connections2b.connections) == [ - Connection(start=6, end=77), - Connection(start=6, end=888), - ] - - connections2c = await client.ListConnections(ConnectionFilter(ends=[9999])) # type: ignore - assert list(connections2c.connections) == [Connection(start=77, end=9999)] - - connections2d = await client.ListConnections( - ConnectionFilter(starts=[6, 77], ends=[888, 9999]) - ) # type: ignore - assert list(connections2d.connections) == [ - Connection(start=6, end=888), - Connection(start=77, end=9999), - ] - - await server2.graceful_shutdown() From 78b08fafb0468bba2803597c9788775860f89fd7 Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Thu, 23 May 2024 09:59:12 +0200 Subject: [PATCH 10/10] Update release notes Signed-off-by: Leandro Lucarella --- RELEASE_NOTES.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 3346460..565dbb6 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -7,7 +7,8 @@ ## Upgrading - The client is now using [`grpclib`](https://pypi.org/project/grpclib/) to connect to the server instead of [`grpcio`](https://pypi.org/project/grpcio/). You might need to adapt the way you connect to the server in your code, using `grpcio.client.Channel`. -- The client now doesn't raise `grpc.aio.RpcError` exceptions anymore. Instead, it raises `ClientError` exceptions that have the `grpc.aio.RpcError` as their `__cause__`. You might need to adapt your error handling code to catch `ClientError` exceptions instead of `grpc.aio.RpcError` exceptions. +- The client now doesn't raise `grpc.aio.RpcError` exceptions anymore. Instead, it raises `ClientError` exceptions that have the `grpclib.GRPCError` as their `__cause__`. You might need to adapt your error handling code to catch `ClientError` exceptions instead of `grpc.aio.RpcError` exceptions. +- The client now uses protobuf/grpc bindings generated [betterproto](https://github.com/danielgtaylor/python-betterproto) instead of [grpcio](https://pypi.org/project/grpcio/). If you were using the bindings directly, you might need to do some minor adjustments to your code. ## New Features