summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorGilbert Gilb's <gilbsgilbs@users.noreply.github.com>2017-12-10 19:43:22 +0100
committerAlex Grönholm <alex.gronholm@nextday.fi>2017-12-10 20:43:22 +0200
commit60c757c9d95c50d66ec2e1abc03b459b45702e58 (patch)
tree32f6f50e97e1c1e650c1e099a5f77d9526b327f2 /tests
parentc6c5031276c3b864e127c637d2bdd48138a0b426 (diff)
downloadapscheduler-60c757c9d95c50d66ec2e1abc03b459b45702e58.tar.gz
Implement random jitter option for CronTrigger and IntervalTrigger (#258)
Diffstat (limited to 'tests')
-rw-r--r--tests/test_triggers.py185
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)