Skip to content

Commit 7f40ba5

Browse files
committed
TST: Add interactive timer tests
This adds more robust interactive timer tests to assert against some of the discrepencies that were found in testing. - Run loop shouldn't depend on callback time - Slow callbacks shouldn't cause a timer to drift over time, it should continually fire at the requested cadence - When start() is called again it should invalidate the previous timer associated with that Timer object
1 parent e0c484d commit 7f40ba5

File tree

1 file changed

+29
-18
lines changed

1 file changed

+29
-18
lines changed

lib/matplotlib/tests/test_backends_interactive.py

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -624,14 +624,12 @@ def _impl_test_interactive_timers():
624624
# NOTE: We run the timer tests in parallel to avoid longer sequential
625625
# delays which adds to the testing time. Add new tests to one of
626626
# the current event loop iterations if possible.
627+
import time
627628
from unittest.mock import Mock
628629
import matplotlib.pyplot as plt
629630

630631
fig = plt.figure()
631-
event_loop_time = 1 # in seconds
632-
expected_200ms_calls = int(event_loop_time / 0.2)
633-
634-
# Start at 2s interval (would only get one firing), then update to 200ms
632+
# Start at 2s interval (wouldn't get any firings), then update to 100ms
635633
timer_repeating = fig.canvas.new_timer(2000)
636634
mock_repeating = Mock()
637635
timer_repeating.add_callback(mock_repeating)
@@ -642,42 +640,55 @@ def _impl_test_interactive_timers():
642640

643641
timer_repeating.start()
644642
# Test updating the interval updates a running timer
645-
timer_repeating.interval = 200
643+
timer_repeating.interval = 100
646644
# Start as a repeating timer then change to singleshot via the attribute
647645
timer_single_shot.start()
648646
timer_single_shot.single_shot = True
649647

650-
fig.canvas.start_event_loop(event_loop_time)
651-
assert 1 < mock_repeating.call_count <= expected_200ms_calls + 1, \
652-
f"Interval update: Expected between 2 and {expected_200ms_calls + 1} calls, " \
653-
f"got {mock_repeating.call_count}"
648+
fig.canvas.start_event_loop(0.5)
649+
assert 2 <= mock_repeating.call_count <= 5, \
650+
f"Interval update: Expected 2-5 calls, got {mock_repeating.call_count}"
654651
assert mock_single_shot.call_count == 1, \
655652
f"Singleshot: Expected 1 call, got {mock_single_shot.call_count}"
656653

657-
# 200ms timer triggers and the callback takes 100ms to run
658-
# Test that we don't drift and that we get called on every 200ms
659-
# interval and not every 300ms
660-
mock_repeating.side_effect = lambda: time.sleep(0.1)
654+
# 250ms timer triggers and the callback takes 150ms to run
655+
# Test that we don't drift and that we get called on every 250ms
656+
# firing and not every 400ms
657+
timer_repeating.interval = 250
658+
mock_repeating.side_effect = lambda: time.sleep(0.15)
659+
# calling start() again on a repeating timer should remove the old
660+
# one, so we don't want double the number of calls here either because
661+
# two timers are potentially running.
662+
timer_repeating.start()
661663
mock_repeating.call_count = 0
662664
# Make sure we can start the timer after stopping a singleshot timer
663665
timer_single_shot.stop()
664666
timer_single_shot.start()
665667

668+
event_loop_time = 2 # in seconds
669+
expected_calls = int(event_loop_time / (timer_repeating.interval / 1000))
670+
671+
t_start = time.perf_counter()
666672
fig.canvas.start_event_loop(event_loop_time)
667-
# Not exact timers, so add a little slop. We really want to make sure we are
668-
# getting more than 3 (every 300ms).
669-
assert mock_repeating.call_count >= expected_200ms_calls - 1, \
670-
f"Slow callback: Expected at least {expected_200ms_calls - 1} calls, " \
673+
t_loop = time.perf_counter() - t_start
674+
# Should be around 2s, but allow for some slop on CI. We want to make sure
675+
# we aren't getting 2 + (callback time) 0.5s/iteration, which would be 4+ s.
676+
assert 1.8 < t_loop < 3, \
677+
f"Event loop: Expected to run for around 2s, but ran for {t_loop:.2f}s"
678+
# Not exact timers, so add some slop. (Quite a bit for CI resources)
679+
assert abs(mock_repeating.call_count - expected_calls) <= 2, \
680+
f"Slow callback: Expected {expected_calls} calls, " \
671681
f"got {mock_repeating.call_count}"
672682
assert mock_single_shot.call_count == 2, \
673683
f"Singleshot: Expected 2 calls, got {mock_single_shot.call_count}"
674-
plt.close("all")
675684

676685

677686
@pytest.mark.parametrize("env", _get_testable_interactive_backends())
678687
def test_interactive_timers(env):
679688
if env["MPLBACKEND"] == "wx":
680689
pytest.skip("wx backend is deprecated; tests failed on appveyor")
690+
if env["MPLBACKEND"].startswith("gtk3") and is_ci_environment():
691+
pytest.xfail("GTK3 backend timer is slow on CI resources")
681692
_run_helper(_impl_test_interactive_timers,
682693
timeout=_test_timeout, extra_env=env)
683694

0 commit comments

Comments
 (0)