diff options
author | Gilbert Gilb's <gilbsgilbs@users.noreply.github.com> | 2017-12-10 19:43:22 +0100 |
---|---|---|
committer | Alex Grönholm <alex.gronholm@nextday.fi> | 2017-12-10 20:43:22 +0200 |
commit | 60c757c9d95c50d66ec2e1abc03b459b45702e58 (patch) | |
tree | 32f6f50e97e1c1e650c1e099a5f77d9526b327f2 /tests | |
parent | c6c5031276c3b864e127c637d2bdd48138a0b426 (diff) | |
download | apscheduler-60c757c9d95c50d66ec2e1abc03b459b45702e58.tar.gz |
Implement random jitter option for CronTrigger and IntervalTrigger (#258)
Diffstat (limited to 'tests')
-rw-r--r-- | tests/test_triggers.py | 185 |
1 files changed, 169 insertions, 16 deletions
diff --git a/tests/test_triggers.py b/tests/test_triggers.py index c22136e..d854800 100644 --- a/tests/test_triggers.py +++ b/tests/test_triggers.py @@ -1,9 +1,11 @@ import pickle +import random from datetime import datetime, timedelta, date import pytest import pytz +from apscheduler.triggers.base import BaseTrigger from apscheduler.triggers.cron import CronTrigger from apscheduler.triggers.date import DateTrigger from apscheduler.triggers.interval import IntervalTrigger @@ -14,11 +16,94 @@ except ImportError: from mock import Mock +class _DummyTriggerWithJitter(BaseTrigger): + def __init__(self, dt, jitter): + self.dt = dt + self.jitter = jitter + + def get_next_fire_time(self, previous_fire_time, now): + return self._apply_jitter(self.dt, self.jitter, now) + + +class TestJitter(object): + def test_jitter_disabled(self): + dt = datetime(2017, 5, 25, 14, 49, 50) + trigger = _DummyTriggerWithJitter(dt, None) + + now = datetime(2017, 5, 25, 13, 40, 44) + assert trigger.get_next_fire_time(None, now) == dt + + def test_jitter_with_none_next_fire_time(self): + trigger = _DummyTriggerWithJitter(None, 5) + now = datetime(2017, 5, 25, 13, 40, 44) + assert trigger.get_next_fire_time(None, now) is None + + def test_jitter_positive(self, monkeypatch): + monkeypatch.setattr(random, 'uniform', lambda a, b: 30.) + + now = datetime(2017, 5, 25, 13, 40, 44) + dt = datetime(2017, 5, 25, 14, 49, 50) + expected_dt = datetime(2017, 5, 25, 14, 50, 20) + + trigger = _DummyTriggerWithJitter(dt, 60) + assert trigger.get_next_fire_time(None, now) == expected_dt + + def test_jitter_in_past_but_initial_date_in_future(self, monkeypatch): + monkeypatch.setattr(random, 'uniform', lambda a, b: -30.) + + now = datetime(2017, 5, 25, 13, 40, 44) + dt = datetime(2017, 5, 25, 13, 40, 47) + expected_dt = dt + + trigger = _DummyTriggerWithJitter(dt, 60) + assert trigger.get_next_fire_time(None, now) == expected_dt + + def test_jitter_in_future_but_initial_date_in_past(self, monkeypatch): + monkeypatch.setattr(random, 'uniform', lambda a, b: 30.) + + now = datetime(2017, 5, 25, 13, 40, 44) + dt = datetime(2017, 5, 25, 13, 40, 30) + expected_dt = datetime(2017, 5, 25, 13, 41, 0) + + trigger = _DummyTriggerWithJitter(dt, 60) + assert trigger.get_next_fire_time(None, now) == expected_dt + + def test_jitter_misfire(self, monkeypatch): + monkeypatch.setattr(random, 'uniform', lambda a, b: -30.) + + now = datetime(2017, 5, 25, 13, 40, 44) + dt = datetime(2017, 5, 25, 13, 40, 40) + expected_dt = dt + + trigger = _DummyTriggerWithJitter(dt, 60) + assert trigger.get_next_fire_time(None, now) == expected_dt + + def test_jitter_is_now(self, monkeypatch): + monkeypatch.setattr(random, 'uniform', lambda a, b: 4.) + + now = datetime(2017, 5, 25, 13, 40, 44) + dt = datetime(2017, 5, 25, 13, 40, 40) + expected_dt = now + + trigger = _DummyTriggerWithJitter(dt, 60) + assert trigger.get_next_fire_time(None, now) == expected_dt + + def test_jitter(self): + now = datetime(2017, 5, 25, 13, 36, 44) + dt = datetime(2017, 5, 25, 13, 40, 45) + min_expected_dt = datetime(2017, 5, 25, 13, 40, 40) + max_expected_dt = datetime(2017, 5, 25, 13, 40, 50) + + trigger = _DummyTriggerWithJitter(dt, 5) + for _ in range(0, 100): + assert min_expected_dt <= trigger.get_next_fire_time(None, now) <= max_expected_dt + + class TestCronTrigger(object): def test_cron_trigger_1(self, timezone): trigger = CronTrigger(year='2009/2', month='1/3', day='5-13', timezone=timezone) assert repr(trigger) == ("<CronTrigger (year='2009/2', month='1/3', day='5-13', " - "timezone='Europe/Berlin')>") + "timezone='Europe/Berlin', jitter='None')>") assert str(trigger) == "cron[year='2009/2', month='1/3', day='5-13']" start_date = timezone.localize(datetime(2008, 12, 1)) correct_next_date = timezone.localize(datetime(2009, 1, 5)) @@ -33,7 +118,7 @@ class TestCronTrigger(object): def test_cron_trigger_3(self, timezone): trigger = CronTrigger(year='2009', month='2', hour='8-10', timezone=timezone) assert repr(trigger) == ("<CronTrigger (year='2009', month='2', hour='8-10', " - "timezone='Europe/Berlin')>") + "timezone='Europe/Berlin', jitter='None')>") start_date = timezone.localize(datetime(2009, 1, 1)) correct_next_date = timezone.localize(datetime(2009, 2, 1, 8)) assert trigger.get_next_fire_time(None, start_date) == correct_next_date @@ -41,7 +126,7 @@ class TestCronTrigger(object): def test_cron_trigger_4(self, timezone): trigger = CronTrigger(year='2012', month='2', day='last', timezone=timezone) assert repr(trigger) == ("<CronTrigger (year='2012', month='2', day='last', " - "timezone='Europe/Berlin')>") + "timezone='Europe/Berlin', jitter='None')>") start_date = timezone.localize(datetime(2012, 2, 1)) correct_next_date = timezone.localize(datetime(2012, 2, 29)) assert trigger.get_next_fire_time(None, start_date) == correct_next_date @@ -55,11 +140,12 @@ class TestCronTrigger(object): def test_cron_zero_value(self, timezone): trigger = CronTrigger(year=2009, month=2, hour=0, timezone=timezone) assert repr(trigger) == ("<CronTrigger (year='2009', month='2', hour='0', " - "timezone='Europe/Berlin')>") + "timezone='Europe/Berlin', jitter='None')>") def test_cron_year_list(self, timezone): trigger = CronTrigger(year='2009,2008', timezone=timezone) - assert repr(trigger) == "<CronTrigger (year='2009,2008', timezone='Europe/Berlin')>" + assert repr(trigger) == ("<CronTrigger (year='2009,2008', timezone='Europe/Berlin', " + "jitter='None')>") assert str(trigger) == "cron[year='2009,2008']" start_date = timezone.localize(datetime(2009, 1, 1)) correct_next_date = timezone.localize(datetime(2009, 1, 1)) @@ -70,7 +156,7 @@ class TestCronTrigger(object): start_date='2009-02-03 11:00:00', timezone=timezone) assert repr(trigger) == ("<CronTrigger (year='2009', month='2', hour='8-10', " "start_date='2009-02-03 11:00:00 CET', " - "timezone='Europe/Berlin')>") + "timezone='Europe/Berlin', jitter='None')>") assert str(trigger) == "cron[year='2009', month='2', hour='8-10']" start_date = timezone.localize(datetime(2009, 1, 1)) correct_next_date = timezone.localize(datetime(2009, 2, 4, 8)) @@ -101,7 +187,7 @@ class TestCronTrigger(object): def test_cron_weekday_overlap(self, timezone): trigger = CronTrigger(year=2009, month=1, day='6-10', day_of_week='2-4', timezone=timezone) assert repr(trigger) == ("<CronTrigger (year='2009', month='1', day='6-10', " - "day_of_week='2-4', timezone='Europe/Berlin')>") + "day_of_week='2-4', timezone='Europe/Berlin', jitter='None')>") assert str(trigger) == "cron[year='2009', month='1', day='6-10', day_of_week='2-4']" start_date = timezone.localize(datetime(2009, 1, 1)) correct_next_date = timezone.localize(datetime(2009, 1, 7)) @@ -110,7 +196,7 @@ class TestCronTrigger(object): def test_cron_weekday_nomatch(self, timezone): trigger = CronTrigger(year=2009, month=1, day='6-10', day_of_week='0,6', timezone=timezone) assert repr(trigger) == ("<CronTrigger (year='2009', month='1', day='6-10', " - "day_of_week='0,6', timezone='Europe/Berlin')>") + "day_of_week='0,6', timezone='Europe/Berlin', jitter='None')>") assert str(trigger) == "cron[year='2009', month='1', day='6-10', day_of_week='0,6']" start_date = timezone.localize(datetime(2009, 1, 1)) correct_next_date = None @@ -119,7 +205,7 @@ class TestCronTrigger(object): def test_cron_weekday_positional(self, timezone): trigger = CronTrigger(year=2009, month=1, day='4th wed', timezone=timezone) assert repr(trigger) == ("<CronTrigger (year='2009', month='1', day='4th wed', " - "timezone='Europe/Berlin')>") + "timezone='Europe/Berlin', jitter='None')>") assert str(trigger) == "cron[year='2009', month='1', day='4th wed']" start_date = timezone.localize(datetime(2009, 1, 1)) correct_next_date = timezone.localize(datetime(2009, 1, 28)) @@ -128,7 +214,7 @@ class TestCronTrigger(object): def test_week_1(self, timezone): trigger = CronTrigger(year=2009, month=2, week=8, timezone=timezone) assert repr(trigger) == ("<CronTrigger (year='2009', month='2', week='8', " - "timezone='Europe/Berlin')>") + "timezone='Europe/Berlin', jitter='None')>") assert str(trigger) == "cron[year='2009', month='2', week='8']" start_date = timezone.localize(datetime(2009, 1, 1)) correct_next_date = timezone.localize(datetime(2009, 2, 16)) @@ -137,7 +223,7 @@ class TestCronTrigger(object): def test_week_2(self, timezone): trigger = CronTrigger(year=2009, week=15, day_of_week=2, timezone=timezone) assert repr(trigger) == ("<CronTrigger (year='2009', week='15', day_of_week='2', " - "timezone='Europe/Berlin')>") + "timezone='Europe/Berlin', jitter='None')>") assert str(trigger) == "cron[year='2009', week='15', day_of_week='2']" start_date = timezone.localize(datetime(2009, 1, 1)) correct_next_date = timezone.localize(datetime(2009, 4, 8)) @@ -146,7 +232,8 @@ class TestCronTrigger(object): def test_cron_extra_coverage(self, timezone): # This test has no value other than patching holes in test coverage trigger = CronTrigger(day='6,8', timezone=timezone) - assert repr(trigger) == "<CronTrigger (day='6,8', timezone='Europe/Berlin')>" + assert repr(trigger) == ("<CronTrigger (day='6,8', timezone='Europe/Berlin', " + "jitter='None')>") assert str(trigger) == "cron[day='6,8']" start_date = timezone.localize(datetime(2009, 12, 31)) correct_next_date = timezone.localize(datetime(2010, 1, 6)) @@ -162,7 +249,8 @@ class TestCronTrigger(object): """ trigger = CronTrigger(hour='5-6', timezone=timezone) - assert repr(trigger) == "<CronTrigger (hour='5-6', timezone='Europe/Berlin')>" + assert repr(trigger) == ("<CronTrigger (hour='5-6', timezone='Europe/Berlin', " + "jitter='None')>") assert str(trigger) == "cron[hour='5-6']" start_date = timezone.localize(datetime(2009, 9, 25, 7)) correct_next_date = timezone.localize(datetime(2009, 9, 26, 5)) @@ -200,7 +288,7 @@ class TestCronTrigger(object): alter_tz = pytz.FixedOffset(-600) trigger = CronTrigger(year=2009, week=15, day_of_week=2, timezone=timezone) assert repr(trigger) == ("<CronTrigger (year='2009', week='15', day_of_week='2', " - "timezone='Europe/Berlin')>") + "timezone='Europe/Berlin', jitter='None')>") assert str(trigger) == "cron[year='2009', week='15', day_of_week='2']" start_date = alter_tz.localize(datetime(2008, 12, 31, 22)) correct_next_date = timezone.localize(datetime(2009, 4, 8)) @@ -249,6 +337,43 @@ class TestCronTrigger(object): for attr in CronTrigger.__slots__: assert getattr(trigger2, attr) == getattr(trigger, attr) + def test_jitter_produces_differrent_valid_results(self, timezone): + trigger = CronTrigger(minute='*', jitter=5) + now = timezone.localize(datetime(2017, 11, 12, 6, 55, 30)) + + results = set() + for _ in range(0, 100): + next_fire_time = trigger.get_next_fire_time(None, now) + results.add(next_fire_time) + assert timedelta(seconds=25) <= (next_fire_time - now) <= timedelta(seconds=35) + assert 1 < len(results) + + def test_jitter_with_timezone(self, timezone): + est = pytz.FixedOffset(-300) + cst = pytz.FixedOffset(-360) + trigger = CronTrigger(hour=11, minute='*/5', timezone=est, jitter=5) + start_date = cst.localize(datetime(2009, 9, 26, 10, 16)) + correct_next_date = est.localize(datetime(2009, 9, 26, 11, 20)) + for _ in range(0, 100): + assert abs(trigger.get_next_fire_time(None, start_date) - + correct_next_date) <= timedelta(seconds=5) + + @pytest.mark.parametrize('trigger_args, start_date, start_date_dst, correct_next_date', [ + ({'hour': 8}, datetime(2013, 3, 9, 12), False, datetime(2013, 3, 10, 8)), + ({'hour': 8}, datetime(2013, 11, 2, 12), True, datetime(2013, 11, 3, 8)), + ({'minute': '*/30'}, datetime(2013, 3, 10, 1, 35), False, datetime(2013, 3, 10, 3)), + ({'minute': '*/30'}, datetime(2013, 11, 3, 1, 35), True, datetime(2013, 11, 3, 1)) + ], ids=['absolute_spring', 'absolute_autumn', 'interval_spring', 'interval_autumn']) + def test_jitter_dst_change(self, trigger_args, start_date, start_date_dst, correct_next_date): + timezone = pytz.timezone('US/Eastern') + trigger = CronTrigger(timezone=timezone, jitter=5, **trigger_args) + start_date = timezone.localize(start_date, is_dst=start_date_dst) + correct_next_date = timezone.localize(correct_next_date, is_dst=not start_date_dst) + + for _ in range(0, 100): + next_fire_time = trigger.get_next_fire_time(None, start_date) + assert abs(next_fire_time - correct_next_date) <= timedelta(seconds=5) + class TestDateTrigger(object): @pytest.mark.parametrize('run_date,alter_tz,previous,now,expected', [ @@ -375,7 +500,7 @@ class TestIntervalTrigger(object): def test_repr(self, trigger): assert repr(trigger) == ("<IntervalTrigger (interval=datetime.timedelta(0, 1), " "start_date='2009-08-04 00:00:02 CEST', " - "timezone='Europe/Berlin')>") + "timezone='Europe/Berlin', jitter='None')>") def test_str(self, trigger): assert str(trigger) == "interval[0:00:01]" @@ -384,9 +509,37 @@ class TestIntervalTrigger(object): """Test that the trigger is pickleable.""" trigger = IntervalTrigger(weeks=2, days=6, minutes=13, seconds=2, - start_date=date(2016, 4, 3), timezone=timezone) + start_date=date(2016, 4, 3), timezone=timezone, + jitter=12) data = pickle.dumps(trigger, 2) trigger2 = pickle.loads(data) for attr in IntervalTrigger.__slots__: assert getattr(trigger2, attr) == getattr(trigger, attr) + + def test_jitter_produces_different_valid_results(self, timezone): + trigger = IntervalTrigger(seconds=5, timezone=timezone, jitter=3) + now = datetime.now(timezone) + + results = set() + for _ in range(0, 100): + next_fire_time = trigger.get_next_fire_time(None, now) + results.add(next_fire_time) + assert timedelta(seconds=2) <= (next_fire_time - now) <= timedelta(seconds=8) + assert 1 < len(results) + + @pytest.mark.parametrize('trigger_args, start_date, start_date_dst, correct_next_date', [ + ({'hours': 1}, datetime(2013, 3, 10, 1, 35), False, datetime(2013, 3, 10, 3, 35)), + ({'hours': 1}, datetime(2013, 11, 3, 1, 35), True, datetime(2013, 11, 3, 1, 35)) + ], ids=['interval_spring', 'interval_autumn']) + def test_jitter_dst_change(self, trigger_args, start_date, start_date_dst, correct_next_date): + timezone = pytz.timezone('US/Eastern') + epsilon = timedelta(seconds=1) + start_date = timezone.localize(start_date, is_dst=start_date_dst) + trigger = IntervalTrigger(timezone=timezone, start_date=start_date, jitter=5, + **trigger_args) + correct_next_date = timezone.localize(correct_next_date, is_dst=not start_date_dst) + + for _ in range(0, 100): + next_fire_time = trigger.get_next_fire_time(None, start_date + epsilon) + assert abs(next_fire_time - correct_next_date) <= timedelta(seconds=5) |