Skip to content

Commit 2690df8

Browse files
committed
fixup! Implement ListConnections
Add tests for ComponentConnection.
1 parent 0d088cc commit 2690df8

File tree

1 file changed

+246
-0
lines changed

1 file changed

+246
-0
lines changed

tests/component/test_connection.py

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
# License: MIT
2+
# Copyright © 2025 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Tests for ComponentConnection class and related functionality."""
5+
6+
from datetime import datetime, timedelta, timezone
7+
from typing import Any
8+
from unittest.mock import Mock, PropertyMock, patch
9+
10+
import pytest
11+
from frequenz.api.common.v1.microgrid import lifetime_pb2
12+
from frequenz.api.common.v1.microgrid.components import components_pb2
13+
from google.protobuf import timestamp_pb2
14+
15+
from frequenz.client.microgrid import ComponentId, Lifetime
16+
from frequenz.client.microgrid.component import ComponentConnection
17+
from frequenz.client.microgrid.component._connection_proto import (
18+
component_connection_from_proto,
19+
)
20+
21+
22+
def test_component_connection_construction() -> None:
23+
"""Test basic ComponentConnection construction and validation.
24+
25+
Raises:
26+
AssertionError: If any of the assertions fail.
27+
"""
28+
now = datetime.now(timezone.utc)
29+
lifetime = Lifetime(start=now)
30+
connection = ComponentConnection(
31+
source=ComponentId(1),
32+
destination=ComponentId(2),
33+
operational_lifetime=lifetime,
34+
)
35+
36+
assert connection.source == ComponentId(1)
37+
assert connection.destination == ComponentId(2)
38+
assert connection.operational_lifetime == lifetime
39+
40+
41+
def test_component_connection_validation() -> None:
42+
"""Test validation of source and destination components."""
43+
with pytest.raises(
44+
ValueError, match="Source and destination components must be different"
45+
):
46+
ComponentConnection(
47+
source=ComponentId(1),
48+
destination=ComponentId(1),
49+
)
50+
51+
52+
@pytest.mark.parametrize(
53+
"lifetime_active",
54+
[True, False],
55+
)
56+
def test_component_connection_active_property(lifetime_active: bool) -> None:
57+
"""Test the active property of ComponentConnection.
58+
59+
Args:
60+
lifetime_active: Whether the lifetime should be active or not.
61+
62+
Raises:
63+
AssertionError: If any of the assertions fail.
64+
"""
65+
mock_lifetime = Mock(spec=Lifetime)
66+
mock_lifetime.active_at.return_value = lifetime_active
67+
68+
connection = ComponentConnection(
69+
source=ComponentId(1),
70+
destination=ComponentId(2),
71+
operational_lifetime=mock_lifetime,
72+
)
73+
74+
test_time = datetime.now(timezone.utc)
75+
assert connection.active_at(test_time) is lifetime_active
76+
mock_lifetime.active_at.assert_called_once_with(test_time)
77+
78+
# Test that the active property uses active_at with the current time
79+
with patch("datetime.datetime") as mock_datetime:
80+
mock_datetime.now.return_value = test_time
81+
assert connection.active is lifetime_active
82+
83+
84+
def test_component_connection_string_representation() -> None:
85+
"""Test string representation of ComponentConnection.
86+
87+
Raises:
88+
AssertionError: If the string representation is incorrect.
89+
"""
90+
connection = ComponentConnection(
91+
source=ComponentId(1),
92+
destination=ComponentId(2),
93+
)
94+
assert str(connection) == "CID1->CID2"
95+
96+
97+
@pytest.mark.parametrize(
98+
"proto_data",
99+
[
100+
pytest.param(
101+
{
102+
"source_component_id": 1,
103+
"destination_component_id": 2,
104+
"has_lifetime": True,
105+
},
106+
id="complete connection",
107+
),
108+
pytest.param(
109+
{
110+
"source_component_id": 1,
111+
"destination_component_id": 2,
112+
"has_lifetime": False,
113+
},
114+
id="missing lifetime",
115+
),
116+
],
117+
)
118+
def test_component_connection_from_proto(
119+
proto_data: dict[str, Any],
120+
caplog: pytest.LogCaptureFixture,
121+
) -> None:
122+
"""Test conversion from protobuf message to ComponentConnection.
123+
124+
Args:
125+
proto_data: Test case parameters
126+
caplog: Fixture to capture log messages
127+
128+
Raises:
129+
AssertionError: If any of the assertions fail.
130+
"""
131+
caplog.set_level("DEBUG") # Ensure we capture DEBUG level messages
132+
133+
proto = components_pb2.ComponentConnection(
134+
source_component_id=proto_data["source_component_id"],
135+
destination_component_id=proto_data["destination_component_id"],
136+
)
137+
138+
if proto_data["has_lifetime"]:
139+
now = datetime.now(timezone.utc)
140+
start_time = timestamp_pb2.Timestamp()
141+
start_time.FromDatetime(now)
142+
lifetime = lifetime_pb2.Lifetime()
143+
lifetime.start_timestamp.CopyFrom(start_time)
144+
proto.operational_lifetime.CopyFrom(lifetime)
145+
146+
connection = component_connection_from_proto(proto)
147+
148+
assert connection.source == ComponentId(proto_data["source_component_id"])
149+
assert connection.destination == ComponentId(proto_data["destination_component_id"])
150+
151+
if not proto_data["has_lifetime"]:
152+
assert "missing operational lifetime" in caplog.text.lower()
153+
154+
155+
def test_component_connection_from_proto_same_ids() -> None:
156+
"""Test proto conversion with same source and destination IDs."""
157+
proto = components_pb2.ComponentConnection(
158+
source_component_id=1,
159+
destination_component_id=1,
160+
)
161+
162+
with pytest.raises(
163+
ValueError, match="Source and destination components must be different"
164+
):
165+
component_connection_from_proto(proto)
166+
167+
168+
@patch("frequenz.client.microgrid.component._connection_proto.lifetime_from_proto")
169+
def test_component_connection_from_proto_invalid_lifetime(
170+
mock_lifetime_from_proto: Mock,
171+
caplog: pytest.LogCaptureFixture,
172+
) -> None:
173+
"""Test proto conversion with invalid lifetime data.
174+
175+
Args:
176+
mock_lifetime_from_proto: Mock for lifetime conversion
177+
caplog: Fixture to capture log messages
178+
179+
Raises:
180+
AssertionError: If any of the assertions fail.
181+
"""
182+
mock_lifetime_from_proto.side_effect = ValueError("Invalid lifetime")
183+
184+
proto = components_pb2.ComponentConnection(
185+
source_component_id=1,
186+
destination_component_id=2,
187+
)
188+
now = datetime.now(timezone.utc)
189+
start_time = timestamp_pb2.Timestamp()
190+
start_time.FromDatetime(now)
191+
lifetime = lifetime_pb2.Lifetime()
192+
lifetime.start_timestamp.CopyFrom(start_time)
193+
proto.operational_lifetime.CopyFrom(lifetime)
194+
195+
connection = component_connection_from_proto(proto)
196+
197+
assert connection.source == ComponentId(1)
198+
assert connection.destination == ComponentId(2)
199+
assert "invalid operational lifetime" in caplog.text.lower()
200+
mock_lifetime_from_proto.assert_called_once_with(proto.operational_lifetime)
201+
202+
203+
@pytest.mark.parametrize(
204+
"case",
205+
[
206+
pytest.param(
207+
{"test_time": "past", "expected": True},
208+
id="past timestamp",
209+
),
210+
pytest.param(
211+
{"test_time": "now", "expected": True},
212+
id="current timestamp",
213+
),
214+
pytest.param(
215+
{"test_time": "future", "expected": True},
216+
id="future timestamp",
217+
),
218+
],
219+
)
220+
def test_component_connection_active_at(case: dict[str, Any]) -> None:
221+
"""Test checking activity at specific timestamps.
222+
223+
Args:
224+
case: Test case parameters
225+
226+
Raises:
227+
AssertionError: If any of the assertions fail.
228+
"""
229+
mock_lifetime = Mock(spec=Lifetime)
230+
mock_lifetime.active_at = Mock(return_value=case["expected"])
231+
232+
connection = ComponentConnection(
233+
source=ComponentId(1),
234+
destination=ComponentId(2),
235+
operational_lifetime=mock_lifetime,
236+
)
237+
238+
now = datetime.now(timezone.utc)
239+
test_time = {
240+
"past": now - timedelta(hours=1),
241+
"now": now,
242+
"future": now + timedelta(hours=1),
243+
}[case["test_time"]]
244+
245+
assert connection.active_at(test_time) == case["expected"]
246+
mock_lifetime.active_at.assert_called_once_with(test_time)

0 commit comments

Comments
 (0)