Skip to content

Commit bae9089

Browse files
Merge pull request #304 from AndreWohnsland/dev
Add tests for machine controller
2 parents 4f3061a + f1a6d0b commit bae9089

File tree

2 files changed

+213
-13
lines changed

2 files changed

+213
-13
lines changed

src/machine/controller.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -128,19 +128,7 @@ def _start_preparation(self, w: MainScreen | None, prep_data: list[_PreparationD
128128
self._print_time = 0.0
129129
current_time = 0.0
130130
# need to cut data into chunks
131-
chunk = cfg.MAKER_SIMULTANEOUSLY_PUMPS
132-
# also take the recipe order in consideration
133-
# first separate the preparation data into a list of lists,
134-
# where each list contains the data for one recipe order
135-
unique_orders = list({x.recipe_order for x in prep_data})
136-
# sort to ensure lowest order is first
137-
unique_orders.sort()
138-
chunked_preparation: list[list[_PreparationData]] = []
139-
for number in unique_orders:
140-
# get all the same order number
141-
order_chunk = [x for x in prep_data if x.recipe_order == number]
142-
# split the chunk again, if the size exceeds the chunk size
143-
chunked_preparation.extend([order_chunk[i : i + chunk] for i in range(0, len(order_chunk), chunk)])
131+
chunked_preparation = self._chunk_preparation_data(prep_data)
144132
chunk_max = [max(x.flow_time for x in y) for y in chunked_preparation]
145133
max_time = round(sum(chunk_max), 2)
146134
cocktail_start_time = time.perf_counter()
@@ -175,6 +163,23 @@ def _start_preparation(self, w: MainScreen | None, prep_data: list[_PreparationD
175163
self._stop_pumps(pins, progress)
176164
return current_time, max_time
177165

166+
def _chunk_preparation_data(self, prep_data: list[_PreparationData]) -> list[list[_PreparationData]]:
167+
"""Chunk the preparation data into smaller sections respecting the MAKER_SIMULTANEOUSLY_PUMPS."""
168+
chunk = cfg.MAKER_SIMULTANEOUSLY_PUMPS
169+
# also take the recipe order in consideration
170+
# first separate the preparation data into a list of lists,
171+
# where each list contains the data for one recipe order
172+
unique_orders = list({x.recipe_order for x in prep_data})
173+
# sort to ensure lowest order is first
174+
unique_orders.sort()
175+
chunked_preparation: list[list[_PreparationData]] = []
176+
for number in unique_orders:
177+
# get all the same order number
178+
order_chunk = [x for x in prep_data if x.recipe_order == number]
179+
# split the chunk again, if the size exceeds the chunk size
180+
chunked_preparation.extend([order_chunk[i : i + chunk] for i in range(0, len(order_chunk), chunk)])
181+
return chunked_preparation
182+
178183
def _process_preparation_section(
179184
self,
180185
current_time: float,

tests/test_machine_controller.py

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
from unittest.mock import ANY, MagicMock, patch
2+
3+
import pytest
4+
5+
from src.config.config_manager import CONFIG
6+
from src.config.config_types import PumpConfig
7+
from src.machine.controller import MachineController, _build_preparation_data, _PreparationData
8+
from src.models import Ingredient
9+
10+
11+
class TestController:
12+
def test_build_preparation_data(self):
13+
original_pump_config = CONFIG.PUMP_CONFIG.copy()
14+
original_maker_number_bottles = CONFIG.MAKER_NUMBER_BOTTLES
15+
16+
try:
17+
CONFIG.PUMP_CONFIG = [ # type: ignore
18+
PumpConfig(pin=1, volume_flow=10.0, tube_volume=0),
19+
PumpConfig(pin=2, volume_flow=20.0, tube_volume=0),
20+
]
21+
CONFIG.MAKER_NUMBER_BOTTLES = 2
22+
23+
# Create actual Ingredient objects
24+
ingredients = [
25+
Ingredient(
26+
id=1,
27+
name="Test Ing 1",
28+
alcohol=40,
29+
bottle_volume=750,
30+
fill_level=500,
31+
hand=False,
32+
pump_speed=100,
33+
amount=100,
34+
bottle=1,
35+
recipe_order=1,
36+
),
37+
Ingredient(
38+
id=2,
39+
name="Test Ing 2",
40+
alcohol=0,
41+
bottle_volume=750,
42+
fill_level=500,
43+
hand=False,
44+
pump_speed=50,
45+
amount=200,
46+
bottle=2,
47+
recipe_order=2,
48+
),
49+
Ingredient(
50+
id=3,
51+
name="Hand Ing",
52+
alcohol=0,
53+
bottle_volume=750,
54+
fill_level=500,
55+
hand=True,
56+
pump_speed=100,
57+
amount=50,
58+
bottle=None,
59+
recipe_order=1,
60+
),
61+
]
62+
63+
prep_data = _build_preparation_data(ingredients)
64+
65+
# Verify results
66+
assert len(prep_data) == 2
67+
assert prep_data[0].pin == 1
68+
assert prep_data[0].volume_flow == pytest.approx(10.0)
69+
assert prep_data[0].flow_time == pytest.approx(10.0) # 100ml / 10ml/s
70+
assert prep_data[0].recipe_order == 1
71+
72+
assert prep_data[1].pin == 2
73+
assert prep_data[1].volume_flow == pytest.approx(10.0) # 20.0 * 0.5 (pump_speed 50%)
74+
assert prep_data[1].flow_time == pytest.approx(20.0) # 200ml / 10ml/s
75+
assert prep_data[1].recipe_order == 2
76+
77+
finally:
78+
# Restore original configuration
79+
CONFIG.PUMP_CONFIG = original_pump_config # type: ignore
80+
CONFIG.MAKER_NUMBER_BOTTLES = original_maker_number_bottles
81+
82+
def test_chunk_preparation_data(self):
83+
# Set original value to restore later
84+
original_simultaneous_pumps = CONFIG.MAKER_SIMULTANEOUSLY_PUMPS
85+
86+
try:
87+
# Set test configuration
88+
CONFIG.MAKER_SIMULTANEOUSLY_PUMPS = 2
89+
90+
# Create test data
91+
prep_data = [
92+
_PreparationData(pin=1, volume_flow=10, flow_time=5, recipe_order=1),
93+
_PreparationData(pin=2, volume_flow=10, flow_time=5, recipe_order=1),
94+
_PreparationData(pin=3, volume_flow=10, flow_time=5, recipe_order=1),
95+
_PreparationData(pin=4, volume_flow=10, flow_time=5, recipe_order=2),
96+
]
97+
98+
mc = MachineController()
99+
chunks = mc._chunk_preparation_data(prep_data)
100+
101+
# Should split first three into two chunks (2+1), and one chunk for order=2
102+
assert len(chunks) == 3
103+
assert [len(chunk) for chunk in chunks] == [2, 1, 1]
104+
assert chunks[0][0].recipe_order == 1
105+
assert chunks[1][0].recipe_order == 1
106+
assert chunks[2][0].recipe_order == 2
107+
108+
finally:
109+
# Restore original configuration
110+
CONFIG.MAKER_SIMULTANEOUSLY_PUMPS = original_simultaneous_pumps
111+
112+
def test_process_preparation_section(self):
113+
# Create test section data
114+
section = [
115+
_PreparationData(pin=1, volume_flow=10, flow_time=5),
116+
_PreparationData(pin=2, volume_flow=20, flow_time=3),
117+
]
118+
119+
mc = MachineController()
120+
# Mock only the internal _stop_pumps method
121+
mc._stop_pumps = MagicMock()
122+
123+
# First call: section_time < all flow_times
124+
mc._process_preparation_section(0, 10, section, section_time=2)
125+
assert section[0].consumption == 20 # 10 ml/s * 2s
126+
assert section[1].consumption == 40 # 20 ml/s * 2s
127+
assert not section[0].closed
128+
assert not section[1].closed
129+
mc._stop_pumps.assert_not_called()
130+
131+
# Second call: section_time > flow_time for pin=2
132+
mc._process_preparation_section(0, 10, section, section_time=4)
133+
assert section[0].consumption == 40 # 10 ml/s * 4s
134+
assert section[1].closed
135+
mc._stop_pumps.assert_called_once_with([2], ANY)
136+
137+
# Third call: section_time > flow_time for pin=1
138+
mc._process_preparation_section(0, 10, section, section_time=6)
139+
assert section[0].closed
140+
assert mc._stop_pumps.call_count == 2
141+
mc._stop_pumps.assert_any_call([1], ANY)
142+
143+
@patch("time.perf_counter")
144+
def test_start_preparation(self, mock_time):
145+
# Mock time to simulate passage of time during preparation
146+
# First call is for cocktail_start_time, then section_start_time, then repeatedly in the while loop
147+
mock_time.side_effect = [
148+
0.0, # cocktail_start_time
149+
0.0, # First section_start_time
150+
1.0, # Time checks during first preparation
151+
1.0, # Second section_start_time
152+
2.0, # Time checks during second preparation
153+
]
154+
155+
# Create test data with two ingredients with different recipe orders
156+
prep_data = [
157+
_PreparationData(pin=1, volume_flow=10, flow_time=1.0, recipe_order=1),
158+
_PreparationData(pin=2, volume_flow=10, flow_time=1.0, recipe_order=2),
159+
]
160+
161+
mc = MachineController()
162+
mc._start_pumps = MagicMock()
163+
mc._stop_pumps = MagicMock()
164+
mc._process_preparation_section = MagicMock()
165+
mc._consumption_print = MagicMock()
166+
167+
# Call the method under test (w is None as requested)
168+
current_time, max_time = mc._start_preparation(None, prep_data, True)
169+
170+
# Verify the method worked correctly
171+
assert max_time == pytest.approx(2.0) # 1.0s + 1.0s for the two ingredients
172+
assert current_time == pytest.approx(2.0) # Last time value from our mock
173+
174+
# Verify _start_pumps was called for both chunks with correct pins
175+
assert mc._start_pumps.call_count == 2
176+
mc._start_pumps.assert_any_call([1], ANY) # First ingredient
177+
mc._start_pumps.assert_any_call([2], ANY) # Second ingredient
178+
179+
# Verify _stop_pumps was called for both chunks with correct pins
180+
assert mc._stop_pumps.call_count == 2
181+
mc._stop_pumps.assert_any_call([1], ANY) # First ingredient
182+
mc._stop_pumps.assert_any_call([2], ANY) # Second ingredient
183+
184+
# Verify _process_preparation_section was called multiple times for each chunk
185+
assert mc._process_preparation_section.call_count >= 2
186+
187+
# Verify calls were in the right sequence (first processing ingredient 1, then ingredient 2)
188+
first_call = mc._process_preparation_section.call_args_list[0]
189+
assert first_call[0][2] == [prep_data[0]] # First call with first ingredient
190+
191+
# Find first call with second ingredient
192+
second_ingredient_calls = [
193+
call for call in mc._process_preparation_section.call_args_list if call[0][2] == [prep_data[1]]
194+
]
195+
assert len(second_ingredient_calls) > 0 # Should have at least one call with second ingredient

0 commit comments

Comments
 (0)