Skip to content

Commit 31b1f8e

Browse files
Tests for training data request batching and time operations/data preperation
1 parent d6358cd commit 31b1f8e

File tree

2 files changed

+425
-0
lines changed

2 files changed

+425
-0
lines changed
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
from unittest.mock import Mock
2+
3+
from service_ml_forecast.services.openremote_service import OpenRemoteService
4+
5+
# Constants for test values
6+
EXPECTED_CALLS_3_MONTHS = 3
7+
EXPECTED_CALLS_2_MONTHS = 2
8+
EXPECTED_CALLS_4_MONTHS = 4
9+
10+
# Timestamp constants for chunking boundary tests
11+
JAN_1_2024 = 1704067200000 # 2024-01-01 00:00:00 UTC
12+
FEB_1_2024 = 1706745600000 # 2024-02-01 00:00:00 UTC
13+
MAR_1_2024 = 1709251200000 # 2024-03-01 00:00:00 UTC
14+
APR_1_2024 = 1711929600000 # 2024-04-01 00:00:00 UTC
15+
16+
17+
def test_get_historical_datapoints_single_month_no_chunking(mock_openremote_service: OpenRemoteService) -> None:
18+
"""Test that single month requests don't trigger chunking.
19+
20+
Verifies that:
21+
- Requests spanning 1 month or less use a single API call
22+
- No chunking logic is applied for short time periods
23+
- The client method is called exactly once with the original timestamps
24+
"""
25+
# Mock the client method
26+
mock_client = Mock()
27+
mock_openremote_service.client = mock_client
28+
29+
# Mock return value for single month
30+
mock_datapoints = [{"timestamp": JAN_1_2024, "value": 100}]
31+
mock_client.assets.get_historical_datapoints.return_value = mock_datapoints
32+
33+
# Test single month (Jan 1 to Feb 1)
34+
from_timestamp = JAN_1_2024 # 2024-01-01 00:00:00 UTC
35+
to_timestamp = FEB_1_2024 # 2024-02-01 00:00:00 UTC
36+
37+
result = mock_openremote_service._get_historical_datapoints(
38+
"test_asset", "test_attribute", from_timestamp, to_timestamp
39+
)
40+
41+
# Should call client method once (no chunking)
42+
mock_client.assets.get_historical_datapoints.assert_called_once_with(
43+
"test_asset", "test_attribute", from_timestamp, to_timestamp
44+
)
45+
assert result == mock_datapoints
46+
47+
48+
def test_get_historical_datapoints_multi_month_chunking(mock_openremote_service: OpenRemoteService) -> None:
49+
"""Test that multi-month requests trigger chunking with correct boundaries.
50+
51+
Verifies that:
52+
- Requests spanning more than 1 month are split into monthly chunks
53+
- Each month gets its own API call to avoid datapoint limits
54+
- All chunk results are properly combined into a single result
55+
- Chunk boundaries are correct with no gaps or overlaps
56+
- First chunk starts at original from_timestamp, last chunk ends at original to_timestamp
57+
"""
58+
# Mock the client method
59+
mock_client = Mock()
60+
mock_openremote_service.client = mock_client
61+
62+
# Mock return values for chunks
63+
chunk1_datapoints = [{"timestamp": JAN_1_2024, "value": 100}] # Jan
64+
chunk2_datapoints = [{"timestamp": FEB_1_2024, "value": 200}] # Feb
65+
chunk3_datapoints = [{"timestamp": MAR_1_2024, "value": 300}] # Mar
66+
67+
mock_client.assets.get_historical_datapoints.side_effect = [
68+
chunk1_datapoints,
69+
chunk2_datapoints,
70+
chunk3_datapoints,
71+
]
72+
73+
# Test 3 months (Jan 1 to Apr 1)
74+
from_timestamp = JAN_1_2024 # 2024-01-01 00:00:00 UTC
75+
to_timestamp = APR_1_2024 # 2024-04-01 00:00:00 UTC
76+
77+
result = mock_openremote_service._get_historical_datapoints(
78+
"test_asset", "test_attribute", from_timestamp, to_timestamp
79+
)
80+
81+
# Should call client method 3 times (one for each month)
82+
assert mock_client.assets.get_historical_datapoints.call_count == EXPECTED_CALLS_3_MONTHS
83+
84+
# Verify the calls were made with correct timestamps
85+
calls = mock_client.assets.get_historical_datapoints.call_args_list
86+
assert len(calls) == EXPECTED_CALLS_3_MONTHS
87+
88+
# Verify chunk boundaries are correct
89+
call1_args = calls[0][0] # (asset_id, attribute_name, from_ts, to_ts)
90+
call2_args = calls[1][0]
91+
call3_args = calls[2][0]
92+
93+
# First chunk: Jan 1 to Feb 1
94+
assert call1_args[2] == from_timestamp # 2024-01-01 00:00:00 UTC
95+
assert call1_args[3] == FEB_1_2024 # 2024-02-01 00:00:00 UTC
96+
97+
# Second chunk: Feb 1 to Mar 1
98+
assert call2_args[2] == FEB_1_2024 # 2024-02-01 00:00:00 UTC
99+
assert call2_args[3] == MAR_1_2024 # 2024-03-01 00:00:00 UTC
100+
101+
# Third chunk: Mar 1 to Apr 1
102+
assert call3_args[2] == MAR_1_2024 # 2024-03-01 00:00:00 UTC
103+
assert call3_args[3] == to_timestamp # 2024-04-01 00:00:00 UTC
104+
105+
# Verify no gaps or overlaps between chunks
106+
assert call1_args[3] == call2_args[2] # Feb 1 boundary
107+
assert call2_args[3] == call3_args[2] # Mar 1 boundary
108+
109+
# Check that all datapoints are combined
110+
expected_datapoints = chunk1_datapoints + chunk2_datapoints + chunk3_datapoints
111+
assert result == expected_datapoints
112+
113+
114+
def test_get_historical_datapoints_chunking_partial_months(mock_openremote_service: OpenRemoteService) -> None:
115+
"""Test chunking with partial months and boundary verification.
116+
117+
Verifies that:
118+
- Partial months at the start (Jan 15-31) create a separate API call
119+
- Full months in the middle (Feb, Mar) each get their own API call
120+
- Partial months at the end (Apr 1-30) create a separate API call
121+
- 15 Jan to 30 Apr results in exactly 4 API calls (Jan 15-31, Feb 1-29, Mar 1-31, Apr 1-30)
122+
- 1 Jan to 1 Apr results in exactly 3 API calls (Jan 1-31, Feb 1-29, Mar 1-31)
123+
- All datapoints from all chunks are properly combined
124+
- Chunk boundaries respect the original from_timestamp and to_timestamp limits
125+
"""
126+
# Mock the client method
127+
mock_client = Mock()
128+
mock_openremote_service.client = mock_client
129+
130+
# Mock return values for chunks
131+
jan_datapoints = [{"timestamp": 1705276800000, "value": 100}] # Jan 15-31
132+
feb_datapoints = [{"timestamp": FEB_1_2024, "value": 200}] # Feb 1-29
133+
mar_datapoints = [{"timestamp": MAR_1_2024, "value": 300}] # Mar 1-31
134+
apr_datapoints = [{"timestamp": APR_1_2024, "value": 400}] # Apr 1-30
135+
136+
mock_client.assets.get_historical_datapoints.side_effect = [
137+
jan_datapoints,
138+
feb_datapoints,
139+
mar_datapoints,
140+
apr_datapoints,
141+
]
142+
143+
# Test 15 Jan to 30 Apr (should be 4 API calls)
144+
from_timestamp = 1705276800000 # 2024-01-15 00:00:00 UTC
145+
to_timestamp = 1714435200000 # 2024-04-30 00:00:00 UTC
146+
147+
result = mock_openremote_service._get_historical_datapoints(
148+
"test_asset", "test_attribute", from_timestamp, to_timestamp
149+
)
150+
151+
# Should call client method 4 times (Jan 15-31, Feb 1-29, Mar 1-31, Apr 1-30)
152+
assert mock_client.assets.get_historical_datapoints.call_count == EXPECTED_CALLS_4_MONTHS
153+
154+
# Verify the calls were made with correct timestamps
155+
calls = mock_client.assets.get_historical_datapoints.call_args_list
156+
assert len(calls) == EXPECTED_CALLS_4_MONTHS
157+
158+
# Verify boundaries for partial month scenario
159+
call1_args = calls[0][0] # First partial chunk
160+
call4_args = calls[3][0] # Last partial chunk
161+
162+
# First chunk starts at original from_timestamp (Jan 15)
163+
assert call1_args[2] == from_timestamp # 2024-01-15 00:00:00 UTC
164+
165+
# Last chunk ends at original to_timestamp (Apr 30), not extended to May 1
166+
assert call4_args[3] == to_timestamp # 2024-04-30 00:00:00 UTC
167+
168+
# Check that all datapoints are combined
169+
expected_datapoints = jan_datapoints + feb_datapoints + mar_datapoints + apr_datapoints
170+
assert result == expected_datapoints
171+
172+
# Reset mock for second test
173+
mock_client.reset_mock()
174+
mock_client.assets.get_historical_datapoints.side_effect = [
175+
jan_datapoints,
176+
feb_datapoints,
177+
mar_datapoints,
178+
]
179+
180+
# Test 1 Jan to 1 Apr (should be 3 API calls)
181+
from_timestamp = JAN_1_2024 # 2024-01-01 00:00:00 UTC
182+
to_timestamp = APR_1_2024 # 2024-04-01 00:00:00 UTC
183+
184+
result = mock_openremote_service._get_historical_datapoints(
185+
"test_asset", "test_attribute", from_timestamp, to_timestamp
186+
)
187+
188+
# Should call client method 3 times (Jan 1-31, Feb 1-29, Mar 1-31)
189+
assert mock_client.assets.get_historical_datapoints.call_count == EXPECTED_CALLS_3_MONTHS
190+
191+
# Verify boundaries for the second test case
192+
calls = mock_client.assets.get_historical_datapoints.call_args_list
193+
first_call_args = calls[0][0]
194+
last_call_args = calls[2][0]
195+
196+
# First chunk starts at from_timestamp and last chunk ends at to_timestamp
197+
assert first_call_args[2] == from_timestamp # 2024-01-01 00:00:00 UTC
198+
assert last_call_args[3] == to_timestamp # 2024-04-01 00:00:00 UTC
199+
200+
# Check that all datapoints are combined
201+
expected_datapoints = jan_datapoints + feb_datapoints + mar_datapoints
202+
assert result == expected_datapoints
203+
204+
205+
def test_get_historical_datapoints_chunking_failure_handling(mock_openremote_service: OpenRemoteService) -> None:
206+
"""Test that chunking fails gracefully when any chunk fails.
207+
208+
Verifies that:
209+
- If any chunk request fails (returns None), the entire operation fails
210+
- The method returns None when any chunk fails
211+
- Failed requests are logged appropriately
212+
- The operation stops at the first failure and doesn't continue processing
213+
"""
214+
# Mock the client method
215+
mock_client = Mock()
216+
mock_openremote_service.client = mock_client
217+
218+
# Mock first chunk succeeds, second chunk fails
219+
chunk1_datapoints = [{"timestamp": JAN_1_2024, "value": 100}]
220+
mock_client.assets.get_historical_datapoints.side_effect = [
221+
chunk1_datapoints,
222+
None, # Second chunk fails
223+
]
224+
225+
# Test 2 months (Jan 1 to Mar 1)
226+
from_timestamp = JAN_1_2024 # 2024-01-01 00:00:00 UTC
227+
to_timestamp = MAR_1_2024 # 2024-03-01 00:00:00 UTC
228+
229+
result = mock_openremote_service._get_historical_datapoints(
230+
"test_asset", "test_attribute", from_timestamp, to_timestamp
231+
)
232+
233+
# Should return None when any chunk fails
234+
assert result is None
235+
236+
# Should call client method 2 times (first succeeds, second fails)
237+
assert mock_client.assets.get_historical_datapoints.call_count == EXPECTED_CALLS_2_MONTHS
238+
239+
240+
def test_get_historical_datapoints_chunking_edge_case_same_timestamp(
241+
mock_openremote_service: OpenRemoteService,
242+
) -> None:
243+
"""Test chunking edge case when from_timestamp equals to_timestamp.
244+
245+
Verifies that:
246+
- When start and end timestamps are identical, no chunking is applied
247+
- The request is treated as a single month request (months_diff <= 1)
248+
- A single API call is made with the identical timestamps
249+
- The result is returned directly without modification
250+
"""
251+
# Mock the client method
252+
mock_client = Mock()
253+
mock_openremote_service.client = mock_client
254+
255+
# Mock return value
256+
mock_datapoints = [{"timestamp": JAN_1_2024, "value": 100}]
257+
mock_client.assets.get_historical_datapoints.return_value = mock_datapoints
258+
259+
# Test same timestamp
260+
timestamp = JAN_1_2024 # 2024-01-01 00:00:00 UTC
261+
262+
result = mock_openremote_service._get_historical_datapoints("test_asset", "test_attribute", timestamp, timestamp)
263+
264+
# Should call client method once (goes through single month path)
265+
mock_client.assets.get_historical_datapoints.assert_called_once_with(
266+
"test_asset", "test_attribute", timestamp, timestamp
267+
)
268+
assert result == mock_datapoints

0 commit comments

Comments
 (0)