55
66import logging
77from collections .abc import AsyncIterator
8- from contextlib import AsyncExitStack
98from typing import Any
109from unittest import mock
1110
1413from frequenz .api .common import components_pb2 , metrics_pb2
1514from frequenz .api .microgrid import grid_pb2 , inverter_pb2 , microgrid_pb2
1615from frequenz .client .base import retry
16+ from google .protobuf .empty_pb2 import Empty
1717
1818from frequenz .client .microgrid import (
1919 ApiClientError ,
3030 InverterType ,
3131 MeterData ,
3232 MicrogridApiClient ,
33+ MicrogridId ,
3334)
3435
3536
@@ -46,14 +47,23 @@ def __init__(self, *, retry_strategy: retry.Strategy | None = None) -> None:
4647 mock_stub .SetPowerReactive = mock .AsyncMock ("SetPowerReactive" )
4748 mock_stub .AddInclusionBounds = mock .AsyncMock ("AddInclusionBounds" )
4849 mock_stub .StreamComponentData = mock .Mock ("StreamComponentData" )
50+ mock_stub .GetMicrogridMetadata = mock .AsyncMock ("GetMicrogridMetadata" )
4951 super ().__init__ ("grpc://mock_host:1234" , retry_strategy = retry_strategy )
5052 self .mock_stub = mock_stub
5153 self ._stub = mock_stub # pylint: disable=protected-access
5254
5355
54- async def test_components () -> None :
56+ @pytest .fixture
57+ async def client () -> AsyncIterator [_TestClient ]:
58+ """Return a test client."""
59+ async with _TestClient (
60+ retry_strategy = retry .LinearBackoff (interval = 0.0 , jitter = 0.0 , limit = 6 )
61+ ) as client_instance :
62+ yield client_instance
63+
64+
65+ async def test_components (client : _TestClient ) -> None :
5566 """Test the components() method."""
56- client = _TestClient ()
5767 server_response = microgrid_pb2 .ComponentList ()
5868 client .mock_stub .ListComponents .return_value = server_response
5969 assert set (await client .components ()) == set ()
@@ -212,9 +222,8 @@ async def test_components() -> None:
212222 }
213223
214224
215- async def test_components_grpc_error () -> None :
225+ async def test_components_grpc_error (client : _TestClient ) -> None :
216226 """Test the components() method when the gRPC call fails."""
217- client = _TestClient ()
218227 client .mock_stub .ListComponents .side_effect = grpc .aio .AioRpcError (
219228 mock .MagicMock (name = "mock_status" ),
220229 mock .MagicMock (name = "mock_initial_metadata" ),
@@ -231,9 +240,8 @@ async def test_components_grpc_error() -> None:
231240 await client .components ()
232241
233242
234- async def test_connections () -> None :
243+ async def test_connections (client : _TestClient ) -> None :
235244 """Test the connections() method."""
236- client = _TestClient ()
237245
238246 def assert_filter (* , starts : set [int ], ends : set [int ]) -> None :
239247 client .mock_stub .ListConnections .assert_called_once ()
@@ -370,9 +378,8 @@ def assert_filter(*, starts: set[int], ends: set[int]) -> None:
370378 assert_filter (starts = {1 , 2 , 4 }, ends = {4 , 5 , 6 })
371379
372380
373- async def test_connections_grpc_error () -> None :
381+ async def test_connections_grpc_error (client : _TestClient ) -> None :
374382 """Test the components() method when the gRPC call fails."""
375- client = _TestClient ()
376383 client .mock_stub .ListConnections .side_effect = grpc .aio .AioRpcError (
377384 mock .MagicMock (name = "mock_status" ),
378385 mock .MagicMock (name = "mock_initial_metadata" ),
@@ -389,6 +396,71 @@ async def test_connections_grpc_error() -> None:
389396 await client .connections ()
390397
391398
399+ async def test_metadata_success (client : _TestClient ) -> None :
400+ """Test the metadata() method with a successful gRPC call."""
401+ mock_metadata_response = microgrid_pb2 .MicrogridMetadata (
402+ microgrid_id = 123 ,
403+ location = microgrid_pb2 .Location (latitude = 40.7128 , longitude = - 74.0060 ),
404+ )
405+ client .mock_stub .GetMicrogridMetadata .return_value = mock_metadata_response
406+
407+ metadata = await client .metadata ()
408+
409+ assert metadata .microgrid_id == MicrogridId (123 )
410+ assert metadata .location is not None
411+ assert metadata .location .latitude == pytest .approx (40.7128 )
412+ assert metadata .location .longitude == pytest .approx (- 74.0060 )
413+ client .mock_stub .GetMicrogridMetadata .assert_called_once_with (Empty (), timeout = 60 )
414+
415+
416+ async def test_metadata_no_location (client : _TestClient ) -> None :
417+ """Test the metadata() method when location is not set in the response."""
418+ mock_metadata_response = microgrid_pb2 .MicrogridMetadata (microgrid_id = 456 )
419+ client .mock_stub .GetMicrogridMetadata .return_value = mock_metadata_response
420+
421+ metadata = await client .metadata ()
422+
423+ assert metadata .microgrid_id == MicrogridId (456 )
424+ assert metadata .location is None
425+ client .mock_stub .GetMicrogridMetadata .assert_called_once_with (Empty (), timeout = 60 )
426+
427+
428+ async def test_metadata_empty_response (client : _TestClient ) -> None :
429+ """Test the metadata() method when the server returns an empty response."""
430+ client .mock_stub .GetMicrogridMetadata .return_value = None
431+
432+ metadata = await client .metadata ()
433+
434+ assert metadata .microgrid_id is None
435+ assert metadata .location is None
436+ client .mock_stub .GetMicrogridMetadata .assert_called_once_with (Empty (), timeout = 60 )
437+
438+
439+ async def test_metadata_grpc_error (
440+ client : _TestClient , caplog : pytest .LogCaptureFixture
441+ ) -> None :
442+ """Test the metadata() method when the gRPC call fails."""
443+ caplog .set_level (logging .WARNING )
444+ client .mock_stub .GetMicrogridMetadata .side_effect = grpc .aio .AioRpcError (
445+ mock .MagicMock (name = "mock_status" ),
446+ mock .MagicMock (name = "mock_initial_metadata" ),
447+ mock .MagicMock (name = "mock_trailing_metadata" ),
448+ "fake grpc details for metadata" ,
449+ "fake grpc debug_error_string for metadata" ,
450+ )
451+
452+ metadata = await client .metadata ()
453+
454+ assert metadata .microgrid_id is None
455+ assert metadata .location is None
456+ client .mock_stub .GetMicrogridMetadata .assert_called_once_with (Empty (), timeout = 60 )
457+ assert len (caplog .records ) == 1
458+ assert caplog .records [0 ].levelname == "ERROR"
459+ assert "The microgrid metadata is not available." in caplog .records [0 ].message
460+ assert caplog .records [0 ].exc_text is not None
461+ assert "fake grpc details for metadata" in caplog .records [0 ].exc_text
462+
463+
392464@pytest .fixture
393465def meter83 () -> microgrid_pb2 .Component :
394466 """Return a test meter component."""
@@ -433,9 +505,8 @@ def component_list(
433505
434506
435507@pytest .mark .parametrize ("method" , ["meter_data" , "battery_data" , "inverter_data" ])
436- async def test_data_component_not_found (method : str ) -> None :
508+ async def test_data_component_not_found (method : str , client : _TestClient ) -> None :
437509 """Test the meter_data() method."""
438- client = _TestClient ()
439510 client .mock_stub .ListComponents .return_value = microgrid_pb2 .ComponentList ()
440511
441512 # It should raise a ValueError for a missing component_id
@@ -456,9 +527,9 @@ async def test_data_bad_category(
456527 method : str ,
457528 component_id : ComponentId ,
458529 component_list : list [microgrid_pb2 .Component ],
530+ client : _TestClient ,
459531) -> None :
460532 """Test the meter_data() method."""
461- client = _TestClient ()
462533 client .mock_stub .ListComponents .return_value = microgrid_pb2 .ComponentList (
463534 components = component_list
464535 )
@@ -484,9 +555,9 @@ async def test_component_data(
484555 component_id : ComponentId ,
485556 component_class : type [ComponentData ],
486557 component_list : list [microgrid_pb2 .Component ],
558+ client : _TestClient ,
487559) -> None :
488560 """Test the meter_data() method."""
489- client = _TestClient ()
490561 client .mock_stub .ListComponents .return_value = microgrid_pb2 .ComponentList (
491562 components = component_list
492563 )
@@ -498,13 +569,9 @@ async def stream_data(
498569
499570 client .mock_stub .StreamComponentData .side_effect = stream_data
500571 receiver = await getattr (client , method )(component_id )
501- async with AsyncExitStack () as stack :
502- stack .push_async_callback (
503- client ._broadcasters [component_id ].stop # pylint: disable=protected-access
504- )
505- latest = await receiver .receive ()
506- assert isinstance (latest , component_class )
507- assert latest .component_id == component_id
572+ latest = await receiver .receive ()
573+ assert isinstance (latest , component_class )
574+ assert latest .component_id == component_id
508575
509576
510577@pytest .mark .parametrize (
@@ -516,18 +583,17 @@ async def stream_data(
516583 ("ev_charger_data" , ComponentId (101 ), EVChargerData ),
517584 ],
518585)
586+ # pylint: disable-next=too-many-arguments,too-many-positional-arguments
519587async def test_component_data_grpc_error (
520588 method : str ,
521589 component_id : ComponentId ,
522590 component_class : type [ComponentData ],
523591 component_list : list [microgrid_pb2 .Component ],
524592 caplog : pytest .LogCaptureFixture ,
593+ client : _TestClient ,
525594) -> None :
526595 """Test the components() method when the gRPC call fails."""
527596 caplog .set_level (logging .WARNING )
528- client = _TestClient (
529- retry_strategy = retry .LinearBackoff (interval = 0.0 , jitter = 0.0 , limit = 6 )
530- )
531597 client .mock_stub .ListComponents .return_value = microgrid_pb2 .ComponentList (
532598 components = component_list
533599 )
@@ -551,21 +617,17 @@ async def stream_data(
551617
552618 client .mock_stub .StreamComponentData .side_effect = stream_data
553619 receiver = await getattr (client , method )(component_id )
554- async with AsyncExitStack () as stack :
555- stack .push_async_callback (
556- client ._broadcasters [component_id ].stop # pylint: disable=protected-access
557- )
558- latest = await receiver .receive ()
559- assert isinstance (latest , component_class )
560- assert latest .component_id == component_id
620+ latest = await receiver .receive ()
621+ assert isinstance (latest , component_class )
622+ assert latest .component_id == component_id
561623
562- latest = await receiver .receive ()
563- assert isinstance (latest , component_class )
564- assert latest .component_id == component_id
624+ latest = await receiver .receive ()
625+ assert isinstance (latest , component_class )
626+ assert latest .component_id == component_id
565627
566- latest = await receiver .receive ()
567- assert isinstance (latest , component_class )
568- assert latest .component_id == component_id
628+ latest = await receiver .receive ()
629+ assert isinstance (latest , component_class )
630+ assert latest .component_id == component_id
569631
570632 # This is not super portable, it will change if the GrpcStreamBroadcaster changes,
571633 # but without this there isn't much to check by this test.
@@ -584,9 +646,10 @@ async def stream_data(
584646
585647
586648@pytest .mark .parametrize ("power_w" , [0 , 0.0 , 12 , - 75 , 0.1 , - 0.0001 , 134.0 ])
587- async def test_set_power_ok (power_w : float , meter83 : microgrid_pb2 .Component ) -> None :
649+ async def test_set_power_ok (
650+ power_w : float , meter83 : microgrid_pb2 .Component , client : _TestClient
651+ ) -> None :
588652 """Test if charge is able to charge component."""
589- client = _TestClient ()
590653 client .mock_stub .ListComponents .return_value = microgrid_pb2 .ComponentList (
591654 components = [meter83 ]
592655 )
@@ -600,9 +663,8 @@ async def test_set_power_ok(power_w: float, meter83: microgrid_pb2.Component) ->
600663 )
601664
602665
603- async def test_set_power_grpc_error () -> None :
666+ async def test_set_power_grpc_error (client : _TestClient ) -> None :
604667 """Test set_power() raises ApiClientError when the gRPC call fails."""
605- client = _TestClient ()
606668 client .mock_stub .SetPowerActive .side_effect = grpc .aio .AioRpcError (
607669 mock .MagicMock (name = "mock_status" ),
608670 mock .MagicMock (name = "mock_initial_metadata" ),
@@ -624,10 +686,9 @@ async def test_set_power_grpc_error() -> None:
624686 [0 , 0.0 , 12 , - 75 , 0.1 , - 0.0001 , 134.0 ],
625687)
626688async def test_set_reactive_power_ok (
627- reactive_power_var : float , meter83 : microgrid_pb2 .Component
689+ reactive_power_var : float , meter83 : microgrid_pb2 .Component , client : _TestClient
628690) -> None :
629691 """Test if charge is able to charge component."""
630- client = _TestClient ()
631692 client .mock_stub .ListComponents .return_value = microgrid_pb2 .ComponentList (
632693 components = [meter83 ]
633694 )
@@ -643,9 +704,8 @@ async def test_set_reactive_power_ok(
643704 )
644705
645706
646- async def test_set_reactive_power_grpc_error () -> None :
707+ async def test_set_reactive_power_grpc_error (client : _TestClient ) -> None :
647708 """Test set_power() raises ApiClientError when the gRPC call fails."""
648- client = _TestClient ()
649709 client .mock_stub .SetPowerReactive .side_effect = grpc .aio .AioRpcError (
650710 mock .MagicMock (name = "mock_status" ),
651711 mock .MagicMock (name = "mock_initial_metadata" ),
@@ -675,10 +735,9 @@ async def test_set_reactive_power_grpc_error() -> None:
675735 ids = str ,
676736)
677737async def test_set_bounds_ok (
678- bounds : metrics_pb2 .Bounds , inverter99 : microgrid_pb2 .Component
738+ bounds : metrics_pb2 .Bounds , inverter99 : microgrid_pb2 .Component , client : _TestClient
679739) -> None :
680740 """Test if charge is able to charge component."""
681- client = _TestClient ()
682741 client .mock_stub .ListComponents .return_value = microgrid_pb2 .ComponentList (
683742 components = [inverter99 ]
684743 )
@@ -704,10 +763,9 @@ async def test_set_bounds_ok(
704763 ids = str ,
705764)
706765async def test_set_bounds_fail (
707- bounds : metrics_pb2 .Bounds , inverter99 : microgrid_pb2 .Component
766+ bounds : metrics_pb2 .Bounds , inverter99 : microgrid_pb2 .Component , client : _TestClient
708767) -> None :
709768 """Test if charge is able to charge component."""
710- client = _TestClient ()
711769 client .mock_stub .ListComponents .return_value = microgrid_pb2 .ComponentList (
712770 components = [inverter99 ]
713771 )
@@ -717,9 +775,8 @@ async def test_set_bounds_fail(
717775 client .mock_stub .AddInclusionBounds .assert_not_called ()
718776
719777
720- async def test_set_bounds_grpc_error () -> None :
721- """Test the components() method when the gRPC call fails."""
722- client = _TestClient ()
778+ async def test_set_bounds_grpc_error (client : _TestClient ) -> None :
779+ """Test set_bounds() raises ApiClientError when the gRPC call fails."""
723780 client .mock_stub .AddInclusionBounds .side_effect = grpc .aio .AioRpcError (
724781 mock .MagicMock (name = "mock_status" ),
725782 mock .MagicMock (name = "mock_initial_metadata" ),
0 commit comments