mirror of
https://github.com/ansible/awx.git
synced 2026-05-20 15:27:47 -02:30
add more tests for weird timezone/DST boundaries in schedules
see: https://github.com/ansible/awx/pull/1024
This commit is contained in:
@@ -619,7 +619,19 @@ class SchedulePreview(GenericAPIView):
|
|||||||
def post(self, request):
|
def post(self, request):
|
||||||
serializer = self.get_serializer(data=request.data)
|
serializer = self.get_serializer(data=request.data)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
schedule = list(Schedule.rrulestr(serializer.validated_data['rrule']).xafter(now(), count=10))
|
next_stamp = now()
|
||||||
|
schedule = []
|
||||||
|
gen = Schedule.rrulestr(serializer.validated_data['rrule']).xafter(next_stamp, count=20)
|
||||||
|
|
||||||
|
# loop across the entire generator and grab the first 10 events
|
||||||
|
for event in gen:
|
||||||
|
if len(schedule) >= 10:
|
||||||
|
break
|
||||||
|
if not dateutil.tz.datetime_exists(event):
|
||||||
|
# skip imaginary dates, like 2:30 on DST boundaries
|
||||||
|
continue
|
||||||
|
schedule.append(event)
|
||||||
|
|
||||||
return Response({
|
return Response({
|
||||||
'local': schedule,
|
'local': schedule,
|
||||||
'utc': [s.astimezone(pytz.utc) for s in schedule]
|
'utc': [s.astimezone(pytz.utc) for s in schedule]
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import re
|
|||||||
import logging
|
import logging
|
||||||
import datetime
|
import datetime
|
||||||
import dateutil.rrule
|
import dateutil.rrule
|
||||||
from dateutil.tz import gettz
|
from dateutil.tz import gettz, datetime_exists
|
||||||
|
|
||||||
# Django
|
# Django
|
||||||
from django.db import models
|
from django.db import models
|
||||||
@@ -185,7 +185,11 @@ class Schedule(CommonModel, LaunchTimeConfig):
|
|||||||
def update_computed_fields(self):
|
def update_computed_fields(self):
|
||||||
future_rs = Schedule.rrulestr(self.rrule, forceset=True)
|
future_rs = Schedule.rrulestr(self.rrule, forceset=True)
|
||||||
next_run_actual = future_rs.after(now())
|
next_run_actual = future_rs.after(now())
|
||||||
|
|
||||||
if next_run_actual is not None:
|
if next_run_actual is not None:
|
||||||
|
if not datetime_exists(next_run_actual):
|
||||||
|
# skip imaginary dates, like 2:30 on DST boundaries
|
||||||
|
next_run_actual = future_rs.after(next_run_actual)
|
||||||
next_run_actual = next_run_actual.astimezone(pytz.utc)
|
next_run_actual = next_run_actual.astimezone(pytz.utc)
|
||||||
|
|
||||||
self.next_run = next_run_actual
|
self.next_run = next_run_actual
|
||||||
|
|||||||
@@ -158,3 +158,118 @@ def test_interval_by_local_day(post, admin_user):
|
|||||||
'2030-04-07 01:00:00+00:00',
|
'2030-04-07 01:00:00+00:00',
|
||||||
'2030-05-05 01:00:00+00:00',
|
'2030-05-05 01:00:00+00:00',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_weekday_timezone_boundary(post, admin_user):
|
||||||
|
url = reverse('api:schedule_rrule')
|
||||||
|
rrule = 'DTSTART;TZID=America/New_York:20300101T210000 RRULE:FREQ=WEEKLY;BYDAY=TU;INTERVAL=1;COUNT=3'
|
||||||
|
r = post(url, {'rrule': rrule}, admin_user, expect=200)
|
||||||
|
|
||||||
|
assert map(str, r.data['local']) == [
|
||||||
|
'2030-01-01 21:00:00-05:00',
|
||||||
|
'2030-01-08 21:00:00-05:00',
|
||||||
|
'2030-01-15 21:00:00-05:00',
|
||||||
|
]
|
||||||
|
|
||||||
|
assert map(str, r.data['utc']) == [
|
||||||
|
'2030-01-02 02:00:00+00:00',
|
||||||
|
'2030-01-09 02:00:00+00:00',
|
||||||
|
'2030-01-16 02:00:00+00:00',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_first_monthly_weekday_timezone_boundary(post, admin_user):
|
||||||
|
url = reverse('api:schedule_rrule')
|
||||||
|
rrule = 'DTSTART;TZID=America/New_York:20300101T210000 RRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=1;INTERVAL=1;COUNT=3'
|
||||||
|
r = post(url, {'rrule': rrule}, admin_user, expect=200)
|
||||||
|
|
||||||
|
assert map(str, r.data['local']) == [
|
||||||
|
'2030-01-06 21:00:00-05:00',
|
||||||
|
'2030-02-03 21:00:00-05:00',
|
||||||
|
'2030-03-03 21:00:00-05:00',
|
||||||
|
]
|
||||||
|
|
||||||
|
assert map(str, r.data['utc']) == [
|
||||||
|
'2030-01-07 02:00:00+00:00',
|
||||||
|
'2030-02-04 02:00:00+00:00',
|
||||||
|
'2030-03-04 02:00:00+00:00',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_annual_timezone_boundary(post, admin_user):
|
||||||
|
url = reverse('api:schedule_rrule')
|
||||||
|
rrule = 'DTSTART;TZID=America/New_York:20301231T230000 RRULE:FREQ=YEARLY;INTERVAL=1;COUNT=3'
|
||||||
|
r = post(url, {'rrule': rrule}, admin_user, expect=200)
|
||||||
|
|
||||||
|
assert map(str, r.data['local']) == [
|
||||||
|
'2030-12-31 23:00:00-05:00',
|
||||||
|
'2031-12-31 23:00:00-05:00',
|
||||||
|
'2032-12-31 23:00:00-05:00',
|
||||||
|
]
|
||||||
|
|
||||||
|
assert map(str, r.data['utc']) == [
|
||||||
|
'2031-01-01 04:00:00+00:00',
|
||||||
|
'2032-01-01 04:00:00+00:00',
|
||||||
|
'2033-01-01 04:00:00+00:00',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_dst_phantom_hour(post, admin_user):
|
||||||
|
# The DST period in the United States begins at 02:00 (2 am) local time, so
|
||||||
|
# the hour from 2:00:00 to 2:59:59 does not exist in the night of the
|
||||||
|
# switch.
|
||||||
|
|
||||||
|
# Three Sundays, starting 2:30AM America/New_York, starting Mar 3, 2030,
|
||||||
|
# should _not_ include Mar 10, 2030 @ 2:30AM (because it doesn't exist)
|
||||||
|
url = reverse('api:schedule_rrule')
|
||||||
|
rrule = 'DTSTART;TZID=America/New_York:20300303T023000 RRULE:FREQ=WEEKLY;BYDAY=SU;INTERVAL=1;COUNT=3'
|
||||||
|
r = post(url, {'rrule': rrule}, admin_user, expect=200)
|
||||||
|
|
||||||
|
assert map(str, r.data['local']) == [
|
||||||
|
'2030-03-03 02:30:00-05:00',
|
||||||
|
'2030-03-17 02:30:00-04:00', # Skip 3/10 because 3/10 @ 2:30AM isn't a real date
|
||||||
|
]
|
||||||
|
|
||||||
|
assert map(str, r.data['utc']) == [
|
||||||
|
'2030-03-03 07:30:00+00:00',
|
||||||
|
'2030-03-17 06:30:00+00:00', # Skip 3/10 because 3/10 @ 2:30AM isn't a real date
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_months_with_31_days(post, admin_user):
|
||||||
|
url = reverse('api:schedule_rrule')
|
||||||
|
rrule = 'DTSTART;TZID=America/New_York:20300101T000000 RRULE:FREQ=MONTHLY;INTERVAL=1;BYMONTHDAY=31;COUNT=7'
|
||||||
|
r = post(url, {'rrule': rrule}, admin_user, expect=200)
|
||||||
|
|
||||||
|
# 30 days have September, April, June, and November...
|
||||||
|
assert map(str, r.data['local']) == [
|
||||||
|
'2030-01-31 00:00:00-05:00',
|
||||||
|
'2030-03-31 00:00:00-04:00',
|
||||||
|
'2030-05-31 00:00:00-04:00',
|
||||||
|
'2030-07-31 00:00:00-04:00',
|
||||||
|
'2030-08-31 00:00:00-04:00',
|
||||||
|
'2030-10-31 00:00:00-04:00',
|
||||||
|
'2030-12-31 00:00:00-05:00',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_dst_rollback_duplicates(post, admin_user):
|
||||||
|
# From Nov 2 -> Nov 3, 2030, daylight savings ends and we "roll back" an hour.
|
||||||
|
# Make sure we don't "double count" duplicate times in the "rolled back"
|
||||||
|
# hour.
|
||||||
|
|
||||||
|
url = reverse('api:schedule_rrule')
|
||||||
|
rrule = 'DTSTART;TZID=America/New_York:20301102T233000 RRULE:FREQ=HOURLY;INTERVAL=1;COUNT=5'
|
||||||
|
r = post(url, {'rrule': rrule}, admin_user, expect=200)
|
||||||
|
|
||||||
|
assert map(str, r.data['local']) == [
|
||||||
|
'2030-11-02 23:30:00-04:00',
|
||||||
|
'2030-11-03 00:30:00-04:00',
|
||||||
|
'2030-11-03 01:30:00-04:00',
|
||||||
|
'2030-11-03 02:30:00-05:00',
|
||||||
|
'2030-11-03 03:30:00-05:00',
|
||||||
|
]
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
from awx.main.models import JobTemplate, Schedule
|
from datetime import datetime
|
||||||
|
|
||||||
|
import mock
|
||||||
import pytest
|
import pytest
|
||||||
|
import pytz
|
||||||
|
|
||||||
|
from awx.main.models import JobTemplate, Schedule
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -166,3 +171,24 @@ def test_utc_until_in_the_past(job_template):
|
|||||||
s.save()
|
s.save()
|
||||||
|
|
||||||
assert s.next_run is s.dtstart is s.dtend is None
|
assert s.next_run is s.dtstart is s.dtend is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@mock.patch('awx.main.models.schedules.now', lambda: datetime(2030, 03, 05, tzinfo=pytz.utc))
|
||||||
|
def test_dst_phantom_hour(job_template):
|
||||||
|
# The DST period in the United States begins at 02:00 (2 am) local time, so
|
||||||
|
# the hour from 2:00:00 to 2:59:59 does not exist in the night of the
|
||||||
|
# switch.
|
||||||
|
|
||||||
|
# Three Sundays, starting 2:30AM America/New_York, starting Mar 3, 2030,
|
||||||
|
# (which doesn't exist)
|
||||||
|
rrule = 'DTSTART;TZID=America/New_York:20300303T023000 RRULE:FREQ=WEEKLY;BYDAY=SU;INTERVAL=1;COUNT=3'
|
||||||
|
s = Schedule(
|
||||||
|
name='Some Schedule',
|
||||||
|
rrule=rrule,
|
||||||
|
unified_job_template=job_template
|
||||||
|
)
|
||||||
|
s.save()
|
||||||
|
|
||||||
|
# 3/10/30 @ 2:30AM is skipped because it _doesn't exist_ <cue twilight zone music>
|
||||||
|
assert str(s.next_run) == '2030-03-17 06:30:00+00:00'
|
||||||
|
|||||||
Reference in New Issue
Block a user