Skip to content

Commit b327ba6

Browse files
committed
Create test_basebuilder.py
1 parent 1da3ec6 commit b327ba6

File tree

1 file changed

+190
-0
lines changed

1 file changed

+190
-0
lines changed
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
"""test_basebuilder.py: Unit tests for the ComponentBuilder abstract base class."""
2+
3+
import unittest
4+
from unittest.mock import MagicMock, patch
5+
6+
# Import ComponentBuilder from its actual location
7+
from pownet.builder.basebuilder import ComponentBuilder
8+
9+
# PATCH_BASE should be the module where ComponentBuilder is defined,
10+
# as this is the context where its internal imports (like gp and SystemInput) are resolved.
11+
PATCH_BASE = "pownet.builder.basebuilder"
12+
13+
# We need ABC for creating test subclasses
14+
from abc import ABC, abstractmethod # ABC is implicitly used by ComponentBuilder
15+
16+
17+
# Minimal concrete implementation for testing ComponentBuilder
18+
class MinimalConcreteBuilder(ComponentBuilder):
19+
"""A minimal concrete subclass for testing ComponentBuilder."""
20+
21+
def add_variables(self, step_k: int) -> None:
22+
"""Mock implementation."""
23+
pass
24+
25+
def get_fixed_objective_terms(self) -> MagicMock: # Actual type is gp.LinExpr
26+
"""Mock implementation."""
27+
return MagicMock(name="MockLinExpr_fixed")
28+
29+
def get_variable_objective_terms(
30+
self, step_k: int, **kwargs
31+
) -> MagicMock: # Actual type is gp.LinExpr
32+
"""Mock implementation."""
33+
return MagicMock(name="MockLinExpr_variable")
34+
35+
def add_constraints(self, step_k: int, init_conds: dict, **kwargs) -> None:
36+
"""Mock implementation."""
37+
pass
38+
39+
def update_variables(self, step_k: int) -> None:
40+
"""Mock implementation."""
41+
pass
42+
43+
def update_constraints(self, step_k: int, init_conds: dict, **kwargs) -> None:
44+
"""Mock implementation."""
45+
pass
46+
47+
def get_variables(
48+
self,
49+
) -> dict[str, MagicMock]: # Actual type is dict[str, gp.tupledict]
50+
"""Mock implementation."""
51+
return {"mock_var": MagicMock(name="MockTupleDict")}
52+
53+
54+
# An incomplete subclass for testing abstract method enforcement
55+
class IncompleteBuilder(ComponentBuilder):
56+
"""An incomplete subclass that misses some abstract methods."""
57+
58+
def add_variables(self, step_k: int) -> None:
59+
pass
60+
61+
def get_fixed_objective_terms(self) -> MagicMock:
62+
return MagicMock(name="MockLinExpr_fixed_incomplete")
63+
64+
# Missing: get_variable_objective_terms, add_constraints, etc.
65+
def get_variables(self) -> dict[str, MagicMock]:
66+
return {"mock_var_incomplete": MagicMock(name="MockTupleDict_incomplete")}
67+
68+
# To make it instantiable for other tests, we would need to implement all other abstract methods.
69+
# For this test, we want it to remain abstract.
70+
@abstractmethod # Explicitly mark remaining methods as abstract if not implemented
71+
def get_variable_objective_terms(self, step_k: int, **kwargs) -> MagicMock:
72+
pass
73+
74+
@abstractmethod
75+
def add_constraints(self, step_k: int, init_conds: dict, **kwargs) -> None:
76+
pass
77+
78+
@abstractmethod
79+
def update_variables(self, step_k: int) -> None:
80+
pass
81+
82+
@abstractmethod
83+
def update_constraints(self, step_k: int, init_conds: dict, **kwargs) -> None:
84+
pass
85+
86+
87+
@patch(f"{PATCH_BASE}.SystemInput", autospec=True)
88+
@patch(f"{PATCH_BASE}.gp") # Patch the 'gp' alias used in basebuilder.py
89+
class TestComponentBuilder(unittest.TestCase):
90+
91+
def _configure_mock_gp_alias(self, mock_gp_alias):
92+
"""Helper to configure the mocked 'gp' alias and its attributes."""
93+
mock_gp_alias.Model = MagicMock(name="MockGPModelClass")
94+
mock_gp_alias.LinExpr = MagicMock(name="MockGPLinExprClass")
95+
mock_gp_alias.tupledict = MagicMock(name="MockGPTupleDictClass")
96+
return mock_gp_alias.Model.return_value # Return a mock model instance
97+
98+
def test_component_builder_is_abc_and_cannot_be_instantiated(
99+
self, mock_gp_alias: MagicMock, mock_system_input_class: MagicMock
100+
):
101+
"""Test that ComponentBuilder cannot be instantiated directly."""
102+
mock_model_instance = self._configure_mock_gp_alias(mock_gp_alias)
103+
# Ensure all required arguments for SystemInput are provided
104+
mock_inputs_instance = mock_system_input_class(
105+
input_folder="dummy",
106+
model_name="test_model", # Assuming model_name is a required arg for SystemInput
107+
year=2023, # Assuming year is a required arg
108+
sim_horizon=24, # Assuming sim_horizon is a required arg for constructor
109+
)
110+
# ComponentBuilder uses inputs.sim_horizon, so ensure it's set on the mock if not by constructor
111+
mock_inputs_instance.sim_horizon = 10 # This is what ComponentBuilder will use
112+
113+
with self.assertRaisesRegex(
114+
TypeError,
115+
# Updated regex to match the beginning of the actual error message
116+
r"Can't instantiate abstract class ComponentBuilder without an implementation for abstract method",
117+
):
118+
ComponentBuilder(mock_model_instance, mock_inputs_instance)
119+
120+
def test_concrete_subclass_instantiation_and_init_attributes(
121+
self, mock_gp_alias: MagicMock, mock_system_input_class: MagicMock
122+
):
123+
"""Test instantiation of a concrete subclass and __init__ attributes."""
124+
mock_model_instance = self._configure_mock_gp_alias(mock_gp_alias)
125+
126+
mock_inputs_instance = mock_system_input_class(
127+
input_folder="dummy_concrete_path",
128+
model_name="test_model_concrete",
129+
year=2023,
130+
sim_horizon=24, # Initial value for SystemInput constructor
131+
)
132+
# ComponentBuilder's __init__ uses inputs.sim_horizon.
133+
# We are testing that ComponentBuilder correctly picks up this value.
134+
# So, the value set here is what we expect ComponentBuilder to use.
135+
mock_inputs_instance.sim_horizon = 5
136+
137+
builder = MinimalConcreteBuilder(
138+
model=mock_model_instance, inputs=mock_inputs_instance
139+
)
140+
141+
self.assertIsInstance(builder, MinimalConcreteBuilder)
142+
self.assertIsInstance(builder, ComponentBuilder)
143+
self.assertEqual(builder.model, mock_model_instance)
144+
self.assertEqual(builder.inputs, mock_inputs_instance)
145+
# Test against the value that ComponentBuilder's __init__ should have used
146+
self.assertEqual(builder.sim_horizon, 5)
147+
self.assertEqual(list(builder.timesteps), list(range(1, 5 + 1)))
148+
149+
def test_incomplete_subclass_cannot_be_instantiated(
150+
self, mock_gp_alias: MagicMock, mock_system_input_class: MagicMock
151+
):
152+
"""Test that a subclass missing abstract methods cannot be instantiated."""
153+
mock_model_instance = self._configure_mock_gp_alias(mock_gp_alias)
154+
mock_inputs_instance = mock_system_input_class(
155+
input_folder="dummy_incomplete_path",
156+
model_name="test_model_incomplete",
157+
year=2023,
158+
sim_horizon=24,
159+
)
160+
mock_inputs_instance.sim_horizon = 3
161+
162+
with self.assertRaisesRegex(
163+
TypeError,
164+
# Updated regex to match the beginning of the actual error message
165+
r"Can't instantiate abstract class IncompleteBuilder without an implementation for abstract method",
166+
):
167+
IncompleteBuilder(mock_model_instance, mock_inputs_instance)
168+
169+
def test_abstract_methods_exist(
170+
self, mock_gp_alias: MagicMock, mock_system_input_class: MagicMock
171+
):
172+
"""Check that all declared abstract methods are indeed marked as abstract."""
173+
expected_abstract_methods = frozenset(
174+
{ # Use frozenset for direct comparison
175+
"add_variables",
176+
"get_fixed_objective_terms",
177+
"get_variable_objective_terms",
178+
"add_constraints",
179+
"update_variables",
180+
"update_constraints",
181+
"get_variables",
182+
}
183+
)
184+
self.assertEqual(
185+
ComponentBuilder.__abstractmethods__, expected_abstract_methods
186+
)
187+
188+
189+
if __name__ == "__main__":
190+
unittest.main(argv=["first-arg-is-ignored"], exit=False)

0 commit comments

Comments
 (0)