Skip to content

Commit 5be7edb

Browse files
committed
fixup component test_base
1 parent 983f3de commit 5be7edb

File tree

1 file changed

+269
-62
lines changed

1 file changed

+269
-62
lines changed

tests/component/test_base.py

Lines changed: 269 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,105 +1,312 @@
11
# License: MIT
22
# Copyright © 2025 Frequenz Energy-as-a-Service GmbH
33

4-
"""Tests for active_at functionality across components."""
4+
"""Tests for the Component base class and its functionality."""
55

66
from datetime import datetime, timezone
7-
from typing import Literal
8-
from unittest.mock import Mock
7+
from unittest.mock import Mock, patch
98

109
import pytest
1110

1211
from frequenz.client.microgrid import ComponentId, Lifetime, MicrogridId
1312
from frequenz.client.microgrid.component._base import Component
1413
from frequenz.client.microgrid.component._category import ComponentCategory
1514
from frequenz.client.microgrid.component._status import ComponentStatus
15+
from frequenz.client.microgrid.metrics._bounds import Bounds
16+
from frequenz.client.microgrid.metrics._metric import Metric
1617

1718

1819
# Test component subclass
19-
class TestComponent(Component):
20+
class _FakeComponent(Component):
2021
"""A simple component implementation for testing."""
2122

22-
category: Literal[ComponentCategory.UNSPECIFIED] = ComponentCategory.UNSPECIFIED
23+
24+
def test_instantiation() -> None:
25+
"""Test that Component base class cannot be instantiated directly."""
26+
with pytest.raises(TypeError, match="Cannot instantiate Component directly"):
27+
_ = Component(
28+
id=ComponentId(1),
29+
microgrid_id=MicrogridId(1),
30+
category=ComponentCategory.UNSPECIFIED,
31+
)
2332

2433

2534
@pytest.mark.parametrize(
26-
"status,lifetime_active,expected",
35+
"name,expected_str",
2736
[
28-
pytest.param(
29-
ComponentStatus.ACTIVE,
30-
True,
31-
True,
32-
id="active status and active lifetime",
33-
),
34-
pytest.param(
35-
ComponentStatus.ACTIVE,
36-
False,
37-
False,
38-
id="active status but inactive lifetime",
39-
),
40-
pytest.param(
41-
ComponentStatus.INACTIVE,
42-
True,
43-
False,
44-
id="inactive status but active lifetime",
45-
),
46-
pytest.param(
47-
ComponentStatus.INACTIVE,
48-
False,
49-
False,
50-
id="inactive status and inactive lifetime",
51-
),
52-
pytest.param(
53-
ComponentStatus.UNSPECIFIED,
54-
True,
55-
True,
56-
id="unspecified status but active lifetime",
57-
),
58-
pytest.param(
59-
ComponentStatus.UNSPECIFIED,
60-
False,
61-
False,
62-
id="unspecified status and inactive lifetime",
63-
),
37+
(None, "CID1<_FakeComponent>"),
38+
("test-component", "CID1<_FakeComponent>:test-component"),
6439
],
6540
)
66-
def test_component_active_at(
67-
status: ComponentStatus,
68-
lifetime_active: bool,
69-
expected: bool,
70-
caplog: pytest.LogCaptureFixture,
71-
) -> None:
72-
"""Test active_at behavior with different status and lifetime combinations.
41+
def test_str(name: str | None, expected_str: str) -> None:
42+
"""Test string representation of a component."""
43+
component = _FakeComponent(
44+
id=ComponentId(1),
45+
microgrid_id=MicrogridId(2),
46+
category=ComponentCategory.UNSPECIFIED,
47+
name=name,
48+
)
49+
assert str(component) == expected_str
50+
51+
52+
def test_metadata() -> None:
53+
"""Test component metadata fields."""
54+
component = _FakeComponent(
55+
id=ComponentId(1),
56+
microgrid_id=MicrogridId(2),
57+
category=ComponentCategory.UNSPECIFIED,
58+
name="test-component",
59+
manufacturer="Test Manufacturer",
60+
model_name="Test Model",
61+
)
62+
63+
assert component.name == "test-component"
64+
assert component.manufacturer == "Test Manufacturer"
65+
assert component.model_name == "Test Model"
66+
67+
68+
def test_rated_bounds() -> None:
69+
"""Test component rated bounds handling."""
70+
bounds = Bounds(lower=-100.0, upper=100.0)
71+
rated_bounds: dict[Metric | int, Bounds] = {Metric.AC_ACTIVE_POWER: bounds}
72+
73+
component = _FakeComponent(
74+
id=ComponentId(1),
75+
microgrid_id=MicrogridId(2),
76+
category=ComponentCategory.UNSPECIFIED,
77+
rated_bounds=rated_bounds,
78+
)
79+
80+
assert component.rated_bounds == rated_bounds
81+
assert component.rated_bounds[Metric.AC_ACTIVE_POWER] == bounds
7382

74-
Args:
75-
status: The component status to test with
76-
lifetime_active: Whether the lifetime should report as active
77-
expected: The expected result
78-
caplog: Fixture to capture log messages
7983

80-
Raises:
81-
AssertionError: If any of the assertions fail.
82-
"""
84+
def test_category_specific_metadata() -> None:
85+
"""Test component category-specific metadata handling."""
86+
metadata = {"key1": "value1", "key2": 42}
87+
88+
component = _FakeComponent(
89+
id=ComponentId(1),
90+
microgrid_id=MicrogridId(2),
91+
category=ComponentCategory.UNSPECIFIED,
92+
category_specific_metadata=metadata,
93+
)
94+
95+
assert component.category_specific_metadata == metadata
96+
assert component.category_specific_metadata["key1"] == "value1"
97+
assert component.category_specific_metadata["key2"] == 42
98+
99+
100+
def test_default_values() -> None:
101+
"""Test component default values."""
102+
component = _FakeComponent(
103+
id=ComponentId(1),
104+
microgrid_id=MicrogridId(2),
105+
category=ComponentCategory.UNSPECIFIED,
106+
)
107+
108+
assert component.status == ComponentStatus.ACTIVE
109+
assert component.name is None
110+
assert component.manufacturer is None
111+
assert component.model_name is None
112+
assert component.operational_lifetime == Lifetime()
113+
assert component.rated_bounds == {}
114+
assert component.category_specific_metadata == {}
115+
116+
117+
@pytest.mark.parametrize("status", list(ComponentStatus))
118+
@pytest.mark.parametrize("lifetime_active", [True, False])
119+
def test_active_at(
120+
status: ComponentStatus, lifetime_active: bool, caplog: pytest.LogCaptureFixture
121+
) -> None:
122+
"""Test active_at behavior with different status and lifetime combinations."""
83123
caplog.set_level("WARNING")
84124

85125
mock_lifetime = Mock(spec=Lifetime)
86126
mock_lifetime.active_at.return_value = lifetime_active
87127

88-
component = TestComponent(
128+
component = _FakeComponent(
89129
id=ComponentId(1),
90-
category=ComponentCategory.UNSPECIFIED,
91130
microgrid_id=MicrogridId(1),
131+
category=ComponentCategory.UNSPECIFIED,
92132
status=status,
93133
operational_lifetime=mock_lifetime,
94134
)
95135

96136
test_time = datetime.now(timezone.utc)
137+
expected = status != ComponentStatus.INACTIVE and lifetime_active
138+
97139
assert component.active_at(test_time) == expected
98140

99-
# Verify lifetime was checked
100-
if component.status == ComponentStatus.ACTIVE:
141+
if status in (ComponentStatus.ACTIVE, ComponentStatus.UNSPECIFIED):
101142
mock_lifetime.active_at.assert_called_once_with(test_time)
143+
else:
144+
mock_lifetime.active_at.assert_not_called()
102145

103-
# Verify warning for unspecified status
104146
if status is ComponentStatus.UNSPECIFIED:
105147
assert "unspecified status" in caplog.text.lower()
148+
149+
150+
def test_active() -> None:
151+
"""Test that active property uses active_at with current time."""
152+
fixed_now = datetime.now(timezone.utc)
153+
mock_lifetime = Mock(spec=Lifetime)
154+
mock_lifetime.active_at.return_value = True
155+
156+
with patch("frequenz.client.microgrid.component._base.datetime") as mock_datetime:
157+
mock_datetime.now.return_value = fixed_now
158+
component = _FakeComponent(
159+
id=ComponentId(1),
160+
microgrid_id=MicrogridId(1),
161+
category=ComponentCategory.UNSPECIFIED,
162+
status=ComponentStatus.ACTIVE,
163+
operational_lifetime=mock_lifetime,
164+
)
165+
166+
assert component.active is True
167+
168+
mock_lifetime.active_at.assert_called_once_with(fixed_now)
169+
170+
171+
COMPONENT = _FakeComponent(
172+
id=ComponentId(1),
173+
microgrid_id=MicrogridId(1),
174+
category=ComponentCategory.UNSPECIFIED,
175+
status=ComponentStatus.ACTIVE,
176+
name="test",
177+
manufacturer="Test Mfg",
178+
model_name="Model A",
179+
rated_bounds={Metric.AC_ACTIVE_POWER: Bounds(lower=-100.0, upper=100.0)},
180+
category_specific_metadata={"key": "value"},
181+
)
182+
183+
DIFFERENT_NONHASHABLE = _FakeComponent(
184+
id=COMPONENT.id,
185+
microgrid_id=COMPONENT.microgrid_id,
186+
category=COMPONENT.category,
187+
status=COMPONENT.status,
188+
name=COMPONENT.name,
189+
manufacturer=COMPONENT.manufacturer,
190+
model_name=COMPONENT.model_name,
191+
rated_bounds={Metric.AC_ACTIVE_POWER: Bounds(lower=-200.0, upper=200.0)},
192+
category_specific_metadata={"different": "metadata"},
193+
)
194+
195+
DIFFERENT_STATUS = _FakeComponent(
196+
id=COMPONENT.id,
197+
microgrid_id=COMPONENT.microgrid_id,
198+
category=COMPONENT.category,
199+
status=ComponentStatus.INACTIVE,
200+
name=COMPONENT.name,
201+
manufacturer=COMPONENT.manufacturer,
202+
model_name=COMPONENT.model_name,
203+
rated_bounds=COMPONENT.rated_bounds,
204+
category_specific_metadata=COMPONENT.category_specific_metadata,
205+
)
206+
207+
DIFFERENT_NAME = _FakeComponent(
208+
id=COMPONENT.id,
209+
microgrid_id=COMPONENT.microgrid_id,
210+
category=COMPONENT.category,
211+
status=COMPONENT.status,
212+
name="different",
213+
manufacturer=COMPONENT.manufacturer,
214+
model_name=COMPONENT.model_name,
215+
rated_bounds=COMPONENT.rated_bounds,
216+
category_specific_metadata=COMPONENT.category_specific_metadata,
217+
)
218+
219+
DIFFERENT_ID = _FakeComponent(
220+
id=ComponentId(2),
221+
microgrid_id=COMPONENT.microgrid_id,
222+
category=COMPONENT.category,
223+
status=COMPONENT.status,
224+
name=COMPONENT.name,
225+
manufacturer=COMPONENT.manufacturer,
226+
model_name=COMPONENT.model_name,
227+
rated_bounds=COMPONENT.rated_bounds,
228+
category_specific_metadata=COMPONENT.category_specific_metadata,
229+
)
230+
231+
DIFFERENT_MICROGRID_ID = _FakeComponent(
232+
id=COMPONENT.id,
233+
microgrid_id=MicrogridId(2),
234+
category=COMPONENT.category,
235+
status=COMPONENT.status,
236+
name=COMPONENT.name,
237+
manufacturer=COMPONENT.manufacturer,
238+
model_name=COMPONENT.model_name,
239+
rated_bounds=COMPONENT.rated_bounds,
240+
category_specific_metadata=COMPONENT.category_specific_metadata,
241+
)
242+
243+
DIFFERENT_BOTH_ID = _FakeComponent(
244+
id=ComponentId(2),
245+
microgrid_id=MicrogridId(2),
246+
category=COMPONENT.category,
247+
status=COMPONENT.status,
248+
name=COMPONENT.name,
249+
manufacturer=COMPONENT.manufacturer,
250+
model_name=COMPONENT.model_name,
251+
rated_bounds=COMPONENT.rated_bounds,
252+
category_specific_metadata=COMPONENT.category_specific_metadata,
253+
)
254+
255+
ALL_COMPONENTS = [
256+
COMPONENT,
257+
DIFFERENT_NONHASHABLE,
258+
DIFFERENT_STATUS,
259+
DIFFERENT_NAME,
260+
DIFFERENT_ID,
261+
DIFFERENT_MICROGRID_ID,
262+
DIFFERENT_BOTH_ID,
263+
]
264+
265+
266+
@pytest.mark.parametrize(
267+
"comp,expected",
268+
[
269+
(COMPONENT, True),
270+
(DIFFERENT_NONHASHABLE, False),
271+
(DIFFERENT_STATUS, False),
272+
(DIFFERENT_NAME, False),
273+
(DIFFERENT_ID, False),
274+
(DIFFERENT_MICROGRID_ID, False),
275+
(DIFFERENT_BOTH_ID, False),
276+
],
277+
)
278+
def test_equality(comp: Component, expected: bool) -> None:
279+
"""Test component equality."""
280+
assert (COMPONENT == comp) is expected
281+
assert (comp == COMPONENT) is expected
282+
assert (COMPONENT != comp) is not expected
283+
assert (comp != COMPONENT) is not expected
284+
285+
286+
@pytest.mark.parametrize(
287+
"comp,expected",
288+
[
289+
(COMPONENT, True),
290+
(DIFFERENT_NONHASHABLE, True),
291+
(DIFFERENT_STATUS, True),
292+
(DIFFERENT_NAME, True),
293+
(DIFFERENT_ID, False),
294+
(DIFFERENT_MICROGRID_ID, False),
295+
(DIFFERENT_BOTH_ID, False),
296+
],
297+
)
298+
def test_identity(comp: Component, expected: bool) -> None:
299+
"""Test component identity."""
300+
assert (COMPONENT.identity == comp.identity) is expected
301+
assert comp.identity == (comp.id, comp.microgrid_id)
302+
303+
304+
@pytest.mark.parametrize("comp1", ALL_COMPONENTS)
305+
@pytest.mark.parametrize("comp2", ALL_COMPONENTS)
306+
def test_hash(comp1: Component, comp2: Component) -> None:
307+
"""Test that the hash is consistent."""
308+
# We can only say the hash are the same if the components are equal, if they
309+
# are not, they could still have the same hash (and they will if they have
310+
# only different non-hashable attributes)
311+
if comp1 == comp2:
312+
assert hash(comp1) == hash(comp2)

0 commit comments

Comments
 (0)