Skip to content

Commit 801dd3e

Browse files
committed
Add recovery tests for BatteryStatusTracker
Signed-off-by: Sahas Subramanian <[email protected]>
1 parent 22fefbd commit 801dd3e

File tree

1 file changed

+340
-3
lines changed

1 file changed

+340
-3
lines changed

tests/actor/test_battery_status.py

Lines changed: 340 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
import math
77
from dataclasses import dataclass
88
from datetime import datetime, timedelta, timezone
9-
from typing import Generic, Iterable, List, Optional, TypeVar
9+
from typing import AsyncIterator, Generic, Iterable, List, Optional, TypeVar
1010

11+
import pytest
1112
import time_machine
1213
from frequenz.api.microgrid.battery_pb2 import ComponentState as BatteryState
1314
from frequenz.api.microgrid.battery_pb2 import Error as BatteryError
@@ -17,7 +18,7 @@
1718
from frequenz.api.microgrid.inverter_pb2 import ComponentState as InverterState
1819
from frequenz.api.microgrid.inverter_pb2 import Error as InverterError
1920
from frequenz.api.microgrid.inverter_pb2 import ErrorCode as InverterErrorCode
20-
from frequenz.channels import Broadcast
21+
from frequenz.channels import Broadcast, Receiver
2122
from pytest_mock import MockerFixture
2223

2324
from frequenz.sdk.actor.power_distributing._battery_status import (
@@ -115,6 +116,26 @@ class Message(Generic[T]):
115116
INVERTER_ID = 8
116117

117118

119+
class _Timeout:
120+
"""Sentinel for timeout."""
121+
122+
123+
async def recv_timeout(recv: Receiver[T], timeout: float = 0.1) -> T | type[_Timeout]:
124+
"""Receive message from receiver with timeout.
125+
126+
Args:
127+
recv: Receiver to receive message from.
128+
timeout: Timeout in seconds.
129+
130+
Returns:
131+
Received message or _Timeout if timeout is reached.
132+
"""
133+
try:
134+
return await asyncio.wait_for(recv.receive(), timeout=timeout)
135+
except asyncio.TimeoutError:
136+
return _Timeout
137+
138+
118139
# pylint: disable=protected-access, unused-argument
119140
class TestBatteryStatus:
120141
"""Tests BatteryStatusTracker."""
@@ -446,7 +467,8 @@ async def test_sync_blocking_interrupted_with_with_max_data(
446467
assert tracker._get_new_status_if_changed() is None
447468
time.shift(timeout)
448469

449-
await tracker.stop()
470+
await tracker.stop()
471+
await mock_microgrid.cleanup()
450472

451473
@time_machine.travel("2022-01-01 00:00 UTC", tick=False)
452474
async def test_sync_blocking_interrupted_with_invalid_message(
@@ -508,6 +530,7 @@ async def test_sync_blocking_interrupted_with_invalid_message(
508530
assert tracker._get_new_status_if_changed() is Status.WORKING
509531

510532
await tracker.stop()
533+
await mock_microgrid.cleanup()
511534

512535
@time_machine.travel("2022-01-01 00:00 UTC", tick=False)
513536
async def test_timers(self, mocker: MockerFixture) -> None:
@@ -644,3 +667,317 @@ async def test_async_battery_status(self, mocker: MockerFixture) -> None:
644667

645668
await tracker.stop()
646669
await mock_microgrid.cleanup()
670+
671+
672+
class TestBatteryStatusRecovery:
673+
"""Test battery status recovery.
674+
675+
The following cases are tested:
676+
677+
- battery/inverter data missing
678+
- battery/inverter bad state
679+
- battery/inverter warning/critical error
680+
- battery capacity missing
681+
- received stale battery/inverter data
682+
"""
683+
684+
@pytest.fixture
685+
async def setup_tracker(
686+
self, mocker: MockerFixture
687+
) -> AsyncIterator[tuple[MockMicrogrid, Receiver[Status]]]:
688+
"""Setup a BatteryStatusTracker instance to run tests with."""
689+
mock_microgrid = MockMicrogrid(grid_side_meter=True)
690+
mock_microgrid.add_batteries(1)
691+
await mock_microgrid.start(mocker)
692+
693+
status_channel = Broadcast[Status]("battery_status")
694+
set_power_result_channel = Broadcast[SetPowerResult]("set_power_result")
695+
696+
status_receiver = status_channel.new_receiver()
697+
698+
_tracker = BatteryStatusTracker(
699+
BATTERY_ID,
700+
max_data_age_sec=0.1,
701+
max_blocking_duration_sec=1,
702+
status_sender=status_channel.new_sender(),
703+
set_power_result_receiver=set_power_result_channel.new_receiver(),
704+
)
705+
706+
await asyncio.sleep(0.05)
707+
708+
yield (mock_microgrid, status_receiver)
709+
710+
await _tracker.stop()
711+
await mock_microgrid.cleanup()
712+
713+
async def _send_healthy_battery(
714+
self, mock_microgrid: MockMicrogrid, timestamp: datetime | None = None
715+
) -> None:
716+
await mock_microgrid.mock_client.send(
717+
battery_data(
718+
timestamp=timestamp,
719+
component_id=BATTERY_ID,
720+
component_state=BatteryState.COMPONENT_STATE_IDLE,
721+
relay_state=BatteryRelayState.RELAY_STATE_CLOSED,
722+
)
723+
)
724+
725+
async def _send_battery_missing_capacity(
726+
self, mock_microgrid: MockMicrogrid
727+
) -> None:
728+
await mock_microgrid.mock_client.send(
729+
battery_data(
730+
component_id=BATTERY_ID,
731+
component_state=BatteryState.COMPONENT_STATE_IDLE,
732+
relay_state=BatteryRelayState.RELAY_STATE_CLOSED,
733+
capacity=math.nan,
734+
)
735+
)
736+
737+
async def _send_healthy_inverter(
738+
self, mock_microgrid: MockMicrogrid, timestamp: datetime | None = None
739+
) -> None:
740+
await mock_microgrid.mock_client.send(
741+
inverter_data(
742+
timestamp=timestamp,
743+
component_id=INVERTER_ID,
744+
component_state=InverterState.COMPONENT_STATE_IDLE,
745+
)
746+
)
747+
748+
async def _send_bad_state_battery(self, mock_microgrid: MockMicrogrid) -> None:
749+
await mock_microgrid.mock_client.send(
750+
battery_data(
751+
component_id=BATTERY_ID,
752+
component_state=BatteryState.COMPONENT_STATE_ERROR,
753+
relay_state=BatteryRelayState.RELAY_STATE_CLOSED,
754+
)
755+
)
756+
757+
async def _send_bad_state_inverter(self, mock_microgrid: MockMicrogrid) -> None:
758+
await mock_microgrid.mock_client.send(
759+
inverter_data(
760+
component_id=INVERTER_ID,
761+
component_state=InverterState.COMPONENT_STATE_ERROR,
762+
)
763+
)
764+
765+
async def _send_critical_error_battery(self, mock_microgrid: MockMicrogrid) -> None:
766+
battery_critical_error = BatteryError(
767+
code=BatteryErrorCode.ERROR_CODE_BLOCK_ERROR,
768+
level=ErrorLevel.ERROR_LEVEL_CRITICAL,
769+
msg="",
770+
)
771+
await mock_microgrid.mock_client.send(
772+
battery_data(
773+
component_id=BATTERY_ID,
774+
component_state=BatteryState.COMPONENT_STATE_IDLE,
775+
relay_state=BatteryRelayState.RELAY_STATE_CLOSED,
776+
errors=[battery_critical_error],
777+
)
778+
)
779+
780+
async def _send_warning_error_battery(self, mock_microgrid: MockMicrogrid) -> None:
781+
battery_warning_error = BatteryError(
782+
code=BatteryErrorCode.ERROR_CODE_HIGH_HUMIDITY,
783+
level=ErrorLevel.ERROR_LEVEL_WARN,
784+
msg="",
785+
)
786+
await mock_microgrid.mock_client.send(
787+
battery_data(
788+
component_id=BATTERY_ID,
789+
component_state=BatteryState.COMPONENT_STATE_IDLE,
790+
relay_state=BatteryRelayState.RELAY_STATE_CLOSED,
791+
errors=[battery_warning_error],
792+
)
793+
)
794+
795+
async def _send_critical_error_inverter(
796+
self, mock_microgrid: MockMicrogrid
797+
) -> None:
798+
inverter_critical_error = InverterError(
799+
code=InverterErrorCode.ERROR_CODE_UNSPECIFIED,
800+
level=ErrorLevel.ERROR_LEVEL_CRITICAL,
801+
msg="",
802+
)
803+
await mock_microgrid.mock_client.send(
804+
inverter_data(
805+
component_id=INVERTER_ID,
806+
component_state=InverterState.COMPONENT_STATE_IDLE,
807+
errors=[inverter_critical_error],
808+
)
809+
)
810+
811+
async def _send_warning_error_inverter(self, mock_microgrid: MockMicrogrid) -> None:
812+
inverter_warning_error = InverterError(
813+
code=InverterErrorCode.ERROR_CODE_UNSPECIFIED,
814+
level=ErrorLevel.ERROR_LEVEL_WARN,
815+
msg="",
816+
)
817+
await mock_microgrid.mock_client.send(
818+
inverter_data(
819+
component_id=INVERTER_ID,
820+
component_state=InverterState.COMPONENT_STATE_IDLE,
821+
errors=[inverter_warning_error],
822+
)
823+
)
824+
825+
async def test_missing_data(
826+
self,
827+
setup_tracker: tuple[MockMicrogrid, Receiver[Status]],
828+
) -> None:
829+
"""Test recovery after missing data."""
830+
mock_microgrid, status_receiver = setup_tracker
831+
832+
await self._send_healthy_battery(mock_microgrid)
833+
await self._send_healthy_inverter(mock_microgrid)
834+
assert await status_receiver.receive() is Status.WORKING
835+
836+
# --- missing battery data ---
837+
await self._send_healthy_inverter(mock_microgrid)
838+
assert await status_receiver.receive() is Status.NOT_WORKING
839+
840+
await self._send_healthy_battery(mock_microgrid)
841+
await self._send_healthy_inverter(mock_microgrid)
842+
assert await status_receiver.receive() is Status.WORKING
843+
844+
# --- missing inverter data ---
845+
await self._send_healthy_battery(mock_microgrid)
846+
assert await status_receiver.receive() is Status.NOT_WORKING
847+
848+
await self._send_healthy_battery(mock_microgrid)
849+
await self._send_healthy_inverter(mock_microgrid)
850+
assert await status_receiver.receive() is Status.WORKING
851+
852+
async def test_bad_state(
853+
self,
854+
setup_tracker: tuple[MockMicrogrid, Receiver[Status]],
855+
) -> None:
856+
"""Test recovery after bad component state."""
857+
mock_microgrid, status_receiver = setup_tracker
858+
859+
await self._send_healthy_battery(mock_microgrid)
860+
await self._send_healthy_inverter(mock_microgrid)
861+
assert await status_receiver.receive() is Status.WORKING
862+
863+
# --- bad battery state ---
864+
await self._send_healthy_inverter(mock_microgrid)
865+
await self._send_bad_state_battery(mock_microgrid)
866+
assert await status_receiver.receive() is Status.NOT_WORKING
867+
868+
await self._send_healthy_battery(mock_microgrid)
869+
await self._send_healthy_inverter(mock_microgrid)
870+
assert await status_receiver.receive() is Status.WORKING
871+
872+
# --- bad inverter state ---
873+
await self._send_bad_state_inverter(mock_microgrid)
874+
await self._send_healthy_battery(mock_microgrid)
875+
assert await status_receiver.receive() is Status.NOT_WORKING
876+
877+
await self._send_healthy_battery(mock_microgrid)
878+
await self._send_healthy_inverter(mock_microgrid)
879+
assert await status_receiver.receive() is Status.WORKING
880+
881+
async def test_critical_error(
882+
self,
883+
setup_tracker: tuple[MockMicrogrid, Receiver[Status]],
884+
) -> None:
885+
"""Test recovery after critical error."""
886+
887+
mock_microgrid, status_receiver = setup_tracker
888+
889+
await self._send_healthy_inverter(mock_microgrid)
890+
await self._send_healthy_battery(mock_microgrid)
891+
assert await status_receiver.receive() is Status.WORKING
892+
893+
# --- battery warning error (keeps working) ---
894+
await self._send_healthy_inverter(mock_microgrid)
895+
await self._send_warning_error_battery(mock_microgrid)
896+
assert await recv_timeout(status_receiver, timeout=0.1) is _Timeout
897+
898+
await self._send_healthy_battery(mock_microgrid)
899+
await self._send_healthy_inverter(mock_microgrid)
900+
901+
# --- battery critical error ---
902+
await self._send_healthy_inverter(mock_microgrid)
903+
await self._send_critical_error_battery(mock_microgrid)
904+
assert await status_receiver.receive() is Status.NOT_WORKING
905+
906+
await self._send_healthy_battery(mock_microgrid)
907+
await self._send_healthy_inverter(mock_microgrid)
908+
assert await status_receiver.receive() is Status.WORKING
909+
910+
# --- inverter warning error (keeps working) ---
911+
await self._send_healthy_battery(mock_microgrid)
912+
await self._send_warning_error_inverter(mock_microgrid)
913+
assert await recv_timeout(status_receiver, timeout=0.1) is _Timeout
914+
915+
await self._send_healthy_battery(mock_microgrid)
916+
await self._send_healthy_inverter(mock_microgrid)
917+
918+
# --- inverter critical error ---
919+
await self._send_healthy_battery(mock_microgrid)
920+
await self._send_critical_error_inverter(mock_microgrid)
921+
assert await status_receiver.receive() is Status.NOT_WORKING
922+
923+
await self._send_healthy_battery(mock_microgrid)
924+
await self._send_healthy_inverter(mock_microgrid)
925+
assert await status_receiver.receive() is Status.WORKING
926+
927+
async def test_missing_capacity(
928+
self,
929+
setup_tracker: tuple[MockMicrogrid, Receiver[Status]],
930+
) -> None:
931+
"""Test recovery after missing capacity."""
932+
mock_microgrid, status_receiver = setup_tracker
933+
934+
await self._send_healthy_battery(mock_microgrid)
935+
await self._send_healthy_inverter(mock_microgrid)
936+
assert await status_receiver.receive() is Status.WORKING
937+
938+
await self._send_healthy_inverter(mock_microgrid)
939+
await self._send_battery_missing_capacity(mock_microgrid)
940+
assert await status_receiver.receive() is Status.NOT_WORKING
941+
942+
await self._send_healthy_battery(mock_microgrid)
943+
await self._send_healthy_inverter(mock_microgrid)
944+
assert await status_receiver.receive() is Status.WORKING
945+
946+
async def test_stale_data(
947+
self,
948+
setup_tracker: tuple[MockMicrogrid, Receiver[Status]],
949+
) -> None:
950+
"""Test recovery after stale data."""
951+
mock_microgrid, status_receiver = setup_tracker
952+
953+
timestamp = datetime.now(timezone.utc)
954+
await self._send_healthy_battery(mock_microgrid, timestamp)
955+
await self._send_healthy_inverter(mock_microgrid)
956+
assert await status_receiver.receive() is Status.WORKING
957+
958+
# --- stale battery data ---
959+
await self._send_healthy_inverter(mock_microgrid)
960+
await self._send_healthy_battery(mock_microgrid, timestamp)
961+
assert await recv_timeout(status_receiver) is _Timeout
962+
963+
await self._send_healthy_inverter(mock_microgrid)
964+
await self._send_healthy_battery(mock_microgrid, timestamp)
965+
assert await recv_timeout(status_receiver) is Status.NOT_WORKING
966+
967+
timestamp = datetime.now(timezone.utc)
968+
await self._send_healthy_battery(mock_microgrid, timestamp)
969+
await self._send_healthy_inverter(mock_microgrid, timestamp)
970+
assert await status_receiver.receive() is Status.WORKING
971+
972+
# --- stale inverter data ---
973+
await self._send_healthy_battery(mock_microgrid)
974+
await self._send_healthy_inverter(mock_microgrid, timestamp)
975+
assert await recv_timeout(status_receiver) is _Timeout
976+
977+
await self._send_healthy_battery(mock_microgrid)
978+
await self._send_healthy_inverter(mock_microgrid, timestamp)
979+
assert await recv_timeout(status_receiver) is Status.NOT_WORKING
980+
981+
await self._send_healthy_battery(mock_microgrid)
982+
await self._send_healthy_inverter(mock_microgrid)
983+
assert await status_receiver.receive() is Status.WORKING

0 commit comments

Comments
 (0)