@@ -191,35 +191,47 @@ async def test_helper_buffer_too_big(
191191 1.0 ,
192192 datetime (2020 , 1 , 1 , 2 , 3 , 5 , 300000 , tzinfo = timezone .utc ),
193193 datetime (2020 , 1 , 1 , tzinfo = timezone .utc ),
194- datetime (2020 , 1 , 1 , 2 , 3 , 6 , tzinfo = timezone .utc ),
194+ (
195+ datetime (2020 , 1 , 1 , 2 , 3 , 6 , tzinfo = timezone .utc ),
196+ timedelta (seconds = 0.7 ),
197+ ),
195198 ),
196199 (
197200 3.0 ,
198201 datetime (2020 , 1 , 1 , 2 , 3 , 5 , 300000 , tzinfo = timezone .utc ),
199202 datetime (2020 , 1 , 1 , 0 , 0 , 5 , tzinfo = timezone .utc ),
200- datetime (2020 , 1 , 1 , 2 , 3 , 8 , tzinfo = timezone .utc ),
203+ (
204+ datetime (2020 , 1 , 1 , 2 , 3 , 8 , tzinfo = timezone .utc ),
205+ timedelta (seconds = 2.7 ),
206+ ),
201207 ),
202208 (
203209 10.0 ,
204210 datetime (2020 , 1 , 1 , 2 , 3 , 5 , 300000 , tzinfo = timezone .utc ),
205211 datetime (2020 , 1 , 1 , 0 , 0 , 5 , tzinfo = timezone .utc ),
206- datetime (2020 , 1 , 1 , 2 , 3 , 15 , tzinfo = timezone .utc ),
212+ (
213+ datetime (2020 , 1 , 1 , 2 , 3 , 15 , tzinfo = timezone .utc ),
214+ timedelta (seconds = 9.7 ),
215+ ),
207216 ),
208217 # Future align_to
209218 (
210219 10.0 ,
211220 datetime (2020 , 1 , 1 , 2 , 3 , 5 , 300000 , tzinfo = timezone .utc ),
212221 datetime (2020 , 1 , 1 , 2 , 3 , 18 , tzinfo = timezone .utc ),
213- datetime (2020 , 1 , 1 , 2 , 3 , 8 , tzinfo = timezone .utc ),
222+ (
223+ datetime (2020 , 1 , 1 , 2 , 3 , 8 , tzinfo = timezone .utc ),
224+ timedelta (seconds = 2.7 ),
225+ ),
214226 ),
215227 ),
216228)
217- def test_calculate_window_end_trivial_cases (
229+ async def test_calculate_window_end_trivial_cases (
218230 fake_time : time_machine .Coordinates ,
219231 resampling_period_s : float ,
220232 now : datetime ,
221233 align_to : datetime ,
222- result : datetime ,
234+ result : tuple [ datetime , timedelta ] ,
223235) -> None :
224236 """Test the calculation of the resampling window end for simple cases."""
225237 resampling_period = timedelta (seconds = resampling_period_s )
@@ -249,11 +261,10 @@ def test_calculate_window_end_trivial_cases(
249261 )
250262 fake_time .move_to (now )
251263 # pylint: disable=protected-access
252- assert (
253- resampler_now ._calculate_window_end () == resampler_none ._calculate_window_end ()
254- )
264+ none_result = resampler_none ._calculate_window_end ()
265+ assert resampler_now ._calculate_window_end () == none_result
255266 # pylint: disable=protected-access
256- assert resampler_none . _calculate_window_end () == now + resampling_period
267+ assert none_result [ 0 ] == now + resampling_period
257268
258269
259270async def test_resampling_window_size_is_constant (
@@ -303,6 +314,7 @@ async def test_resampling_window_size_is_constant(
303314 await resampler .resample (one_shot = True )
304315
305316 assert datetime .now (timezone .utc ).timestamp () == 2
317+ assert asyncio .get_event_loop ().time () == 2
306318 sink_mock .assert_called_once_with (
307319 Sample (
308320 timestamp + timedelta (seconds = resampling_period_s ),
@@ -343,7 +355,7 @@ async def test_resampling_window_size_is_constant(
343355 resampling_fun_mock .reset_mock ()
344356
345357
346- async def test_timer_errors_are_logged (
358+ async def test_timer_errors_are_logged ( # pylint: disable=too-many-statements
347359 fake_time : time_machine .Coordinates ,
348360 source_chan : Broadcast [Sample [Quantity ]],
349361 caplog : pytest .LogCaptureFixture ,
@@ -384,15 +396,14 @@ async def test_timer_errors_are_logged(
384396 # T = timer tick
385397
386398 # Send a few samples and run a resample tick, advancing the fake time by one period
387- # Important: this is needed because the resampling timer only starts after the first
388- # resapmle() is called, so the first resampling will never have a shift.
399+ # No log message should be produced
389400 sample0s = Sample (timestamp , value = Quantity (5.0 ))
390401 sample1s = Sample (timestamp + timedelta (seconds = 1.0 ), value = Quantity (12.0 ))
391402 await source_sender .send (sample0s )
392403 await source_sender .send (sample1s )
393404 # Here we need to advance only the wall clock because the resampler timer is not yet
394405 # started, otherwise the loop time will be advanced twice
395- fake_time . shift ( resampling_period_s )
406+ await _advance_time ( fake_time , resampling_period_s )
396407 await resampler .resample (one_shot = True )
397408
398409 assert datetime .now (timezone .utc ).timestamp () == pytest .approx (2 )
@@ -1112,6 +1123,82 @@ async def make_fake_source() -> Source:
11121123 assert isinstance (timeseries_error , TestException )
11131124
11141125
1126+ async def test_timer_is_aligned (
1127+ fake_time : time_machine .Coordinates ,
1128+ source_chan : Broadcast [Sample [Quantity ]],
1129+ caplog : pytest .LogCaptureFixture ,
1130+ ) -> None :
1131+ """Test that big differences between the expected window end and the fired timer are logged."""
1132+ timestamp = datetime .now (timezone .utc )
1133+
1134+ resampling_period_s = 2
1135+ expected_resampled_value = 42.0
1136+
1137+ resampling_fun_mock = MagicMock (
1138+ spec = ResamplingFunction , return_value = expected_resampled_value
1139+ )
1140+ config = ResamplerConfig (
1141+ resampling_period = timedelta (seconds = resampling_period_s ),
1142+ max_data_age_in_periods = 2.0 ,
1143+ resampling_function = resampling_fun_mock ,
1144+ initial_buffer_len = 4 ,
1145+ )
1146+
1147+ # Advance the time a bit so that the resampler is not aligned to the resampling
1148+ # period
1149+ await _advance_time (fake_time , resampling_period_s / 3 )
1150+
1151+ resampler = Resampler (config )
1152+
1153+ source_receiver = source_chan .new_receiver ()
1154+ source_sender = source_chan .new_sender ()
1155+
1156+ sink_mock = AsyncMock (spec = Sink , return_value = True )
1157+
1158+ resampler .add_timeseries ("test" , source_receiver , sink_mock )
1159+ source_props = resampler .get_source_properties (source_receiver )
1160+
1161+ # Test timeline
1162+ # alignment
1163+ # ,-------------.
1164+ # start = 0.667
1165+ # t(s) 0 | 1 1.5 2
1166+ # |-------+--|-----|----R-----> (no more samples)
1167+ # value 5.0 12.0
1168+ #
1169+ # R = resampling is done
1170+
1171+ # Send samples and resample
1172+ sample1s = Sample (timestamp + timedelta (seconds = 1.0 ), value = Quantity (5.0 ))
1173+ sample1_5s = Sample (timestamp + timedelta (seconds = 1.5 ), value = Quantity (12.0 ))
1174+ await source_sender .send (sample1s )
1175+ await source_sender .send (sample1_5s )
1176+ await _advance_time (fake_time , resampling_period_s * 2 / 3 )
1177+ await resampler .resample (one_shot = True )
1178+
1179+ assert datetime .now (timezone .utc ).timestamp () == pytest .approx (2 )
1180+ assert asyncio .get_running_loop ().time () == pytest .approx (2 )
1181+ sink_mock .assert_called_once_with (
1182+ Sample (
1183+ timestamp + timedelta (seconds = resampling_period_s ),
1184+ Quantity (expected_resampled_value ),
1185+ )
1186+ )
1187+ resampling_fun_mock .assert_called_once_with (
1188+ a_sequence (sample1s , sample1_5s ),
1189+ config ,
1190+ source_props ,
1191+ )
1192+ assert not [
1193+ * _filter_logs (
1194+ caplog .record_tuples ,
1195+ logger_level = logging .WARNING ,
1196+ )
1197+ ]
1198+ sink_mock .reset_mock ()
1199+ resampling_fun_mock .reset_mock ()
1200+
1201+
11151202def _get_buffer_len (resampler : Resampler , source_recvr : Source ) -> int :
11161203 # pylint: disable=protected-access
11171204 blen = resampler ._resamplers [source_recvr ]._helper ._buffer .maxlen
0 commit comments