1
0
mirror of https://github.com/linkedin/oncall.git synced 2025-11-27 23:18:38 +02:00
Files
oncall/test/test_scheduler.py
Diego Cepeda af327b4e4a Py3 (#290)
* Py3 migration

* Update to Python 3 for CircleCI

* Fix auth bugs for python 3

Also fix notifier bug to check for active users

* Update notifier exception handling

Ignore role:target lookup failures from Iris, since these don't represent
problems with the underlying system, just that people have inactive users
on-call in the future.

* Add get_id param option (#246)

* add get_id param option

* removed superfluous select and simplified logic

* Flake8 typo (#247)

* Hide confusing team settings in an advanced dropdown

* Fix test fixtures

* Add "allow duplicate" scheduler in UI

Already in backend, so enable in FE too

* Add Dockerfile to run oncall in a container

* Move deps into a virtualenv.
Run app not as super user.
Mimick prod setup by using uwsgi

* Fix issue with Dockerfile not having MANIFEST.in and wrong passwords in (#257)

config

* Update to ubuntu:18.04 and python3 packages and executables

* Open config file as utf8

The default configuration file has utf8 characters, and python3
attempts to open the file as ASCII unless an alternate encoding
is specified

* Switch to the python3 uwsgi plugin

* Update print and os.execv statements for python3

Python3 throws an exception when the first argument to os.execv is empty:
ValueError: execv() arg 2 first element cannot be empty

The module documentation suggests that the first element should be the
name of the executed program:
https://docs.python.org/3.7/library/os.html#os.execv

* Map config.docker.yaml in to the container as a volume

./ops/entrypoint.py has the start of environment variable support
to specify a configuration file, but it is incomplete until we
update ./ops/daemons/uwsgi-docker.yaml or add environment support
to oncall-notifier and oncall-scheduler.

This commit allows users to map a specific configuration file in
to their container and have it used by all oncall programs.

* Convert line endings to match the rest of the project.

* Add mysql port to docker configuration

* Assume localhost mysql for default config.yaml

* Update python-dev package and MySQL root password

* Use password when configuring mysql

The project has started using a password on the mysql instance.
Once password auth is consistently working we can consider extracting
the hardcoded password into an env file that is optionally randomly
generated on initial startup.

* Fix preview for round-robin (#269)

* #275 fix for Python3 and Gunicorn load config

* Fixed E303 flake8

* Change encoding & collation + test  unicode name

Co-authored-by: Daniel Wang <dwang159@gmail.com>
Co-authored-by: ahm3djafri <42748963+ahm3djafri@users.noreply.github.com>
Co-authored-by: TK <tkahnoski+github@gmail.com>
Co-authored-by: Tim Freund <tim@freunds.net>
Co-authored-by: Rafał Zawadzki <bluszcz@bluszcz.net>
2020-01-15 15:38:25 -08:00

276 lines
12 KiB
Python

# Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license.
# See LICENSE in the project root for license information.
import datetime
import time
import calendar
import oncall.scheduler.default
from pytz import utc, timezone
MIN = 60
HOUR = 60 * MIN
DAY = 24 * HOUR
WEEK = 7 * DAY
MOCK_SCHEDULE = {'team_id': 1, 'role_id': 2, 'roster_id': 3}
def test_find_new_user_as_least_active_user(mocker):
scheduler = oncall.scheduler.default.Scheduler()
mocker.patch('oncall.scheduler.default.Scheduler.find_new_user_in_roster').return_value = {123}
mocker.patch('oncall.scheduler.default.Scheduler.get_roster_user_ids').return_value = {135, 123}
mocker.patch('oncall.scheduler.default.Scheduler.get_busy_user_by_event_range')
mocker.patch('oncall.scheduler.default.Scheduler.find_least_active_user_id_by_team')
user_id = scheduler.find_next_user_id(MOCK_SCHEDULE, [{'start': 0, 'end': 5}], None)
assert user_id == 123
def test_calculate_future_events_7_24_shifts(mocker):
mocker.patch('oncall.scheduler.default.Scheduler.get_schedule_last_epoch').return_value = None
mock_dt = datetime.datetime(year=2017, month=2, day=7, hour=10)
mocker.patch('time.time').return_value = time.mktime(mock_dt.timetuple())
start = DAY + 10 * HOUR + 30 * MIN # Monday at 10:30 am
schedule_foo = {
'timezone': 'US/Pacific',
'auto_populate_threshold': 21,
'events': [{
'start': start, # 24hr weeklong shift starting Monday at 10:30 am
'duration': WEEK
}]
}
scheduler = oncall.scheduler.default.Scheduler()
future_events, last_epoch = scheduler.calculate_future_events(schedule_foo, None)
assert len(future_events) == 4
mondays = (6, 13, 20, 27)
for epoch, monday in zip(future_events, mondays):
assert len(epoch) == 1
ev = epoch[0]
start_dt = utc.localize(datetime.datetime.utcfromtimestamp(ev['start']))
start_dt = start_dt.astimezone(timezone('US/Pacific'))
assert start_dt.timetuple().tm_year == mock_dt.timetuple().tm_year
assert start_dt.timetuple().tm_mon == mock_dt.timetuple().tm_mon
assert start_dt.timetuple().tm_mday == monday
assert start_dt.timetuple().tm_wday == 0 # Monday
assert start_dt.timetuple().tm_hour == 10 # 10:
assert start_dt.timetuple().tm_min == 30 # 30 am
assert start_dt.timetuple().tm_sec == 00
assert ev['end'] - ev['start'] == WEEK
def test_calculate_future_events_7_12_shifts(mocker):
mocker.patch('oncall.scheduler.default.Scheduler.get_schedule_last_epoch').return_value = None
mock_dt = datetime.datetime(year=2016, month=9, day=9, hour=10)
mocker.patch('time.time').return_value = time.mktime(mock_dt.timetuple())
start = 3 * DAY + 12 * HOUR # Wednesday at noon
events = []
for i in range(7):
events.append({'start': start + DAY * i, 'duration': 12 * HOUR})
schedule_foo = {
'timezone': 'US/Eastern',
'auto_populate_threshold': 7,
'events': events
}
scheduler = oncall.scheduler.default.Scheduler()
future_events, last_epoch = scheduler.calculate_future_events(schedule_foo, None)
assert len(future_events) == 2
assert len(future_events[0]) == 7
assert len(future_events[1]) == 7
days = range(14, 22)
for ev, day in zip(future_events[1], days):
start_dt = utc.localize(datetime.datetime.utcfromtimestamp(ev['start']))
start_dt = start_dt.astimezone(timezone('US/Eastern'))
assert start_dt.timetuple().tm_year == mock_dt.timetuple().tm_year
assert start_dt.timetuple().tm_mon == mock_dt.timetuple().tm_mon
assert start_dt.timetuple().tm_mday == day
assert start_dt.timetuple().tm_hour == 12
assert start_dt.timetuple().tm_min == 00
assert start_dt.timetuple().tm_sec == 00
def test_calculate_future_events_14_12_shifts(mocker):
mocker.patch('oncall.scheduler.default.Scheduler.get_schedule_last_epoch').return_value = None
mock_dt = datetime.datetime(year=2016, month=9, day=9, hour=10)
mocker.patch('time.time').return_value = time.mktime(mock_dt.timetuple())
start = 3 * DAY + 12 * HOUR # Wednesday at noon
events = []
for i in range(14):
events.append({'start': start + DAY * i, 'duration': 12 * HOUR})
schedule_foo = {
'timezone': 'US/Central',
'auto_populate_threshold': 21,
'events': events
}
scheduler = oncall.scheduler.default.Scheduler()
future_events, last_epoch = scheduler.calculate_future_events(schedule_foo, None)
assert len(future_events) == 2
assert len(future_events[1]) == 14
days = list(range(21, 31)) + list(range(1, 6))
for ev, day in zip(future_events[1], days):
start_dt = utc.localize(datetime.datetime.utcfromtimestamp(ev['start']))
start_dt = start_dt.astimezone(timezone('US/Central'))
assert start_dt.timetuple().tm_year == mock_dt.timetuple().tm_year
assert start_dt.timetuple().tm_mday == day
assert start_dt.timetuple().tm_hour == 12
assert start_dt.timetuple().tm_min == 00
assert start_dt.timetuple().tm_sec == 00
def test_dst_ambiguous_schedule(mocker):
mocker.patch('oncall.scheduler.default.Scheduler.get_schedule_last_epoch').return_value = None
mock_dt = datetime.datetime(year=2016, month=10, day=29, hour=10)
mocker.patch('time.time').return_value = time.mktime(mock_dt.timetuple())
start = HOUR + 30 * MIN # Sunday at 1:30 am
schedule_foo = {
'timezone': 'US/Central',
'auto_populate_threshold': 14,
'events': [{
'start': start, # 24hr weeklong shift starting Sunday at 1:30 am
'duration': WEEK
}]
}
scheduler = oncall.scheduler.default.Scheduler()
future_events, last_epoch = scheduler.calculate_future_events(schedule_foo, None)
assert len(future_events) == 3
dst_events = future_events[1] + future_events[2]
assert len(dst_events) == 2
# Make sure that events are consecutive (no gaps)
assert dst_events[0]['end'] == dst_events[1]['start']
def test_dst_schedule(mocker):
mocker.patch('oncall.scheduler.default.Scheduler.get_schedule_last_epoch').return_value = None
mock_dt = datetime.datetime(year=2016, month=10, day=29, hour=10)
mocker.patch('time.time').return_value = time.mktime(mock_dt.timetuple())
start = DAY + 11 * HOUR # Monday at 11:00 am
schedule_foo = {
'timezone': 'US/Central',
'auto_populate_threshold': 14,
'events': [{
'start': start, # 24hr weeklong shift starting Monday at 11:00 am
'duration': WEEK
}]
}
scheduler = oncall.scheduler.default.Scheduler()
future_events, last_epoch = scheduler.calculate_future_events(schedule_foo, None)
assert len(future_events) == 3
dst_events = future_events[1] + future_events[2]
assert len(dst_events) == 2
# Make sure that events are consecutive (no gaps)
assert dst_events[0]['end'] == dst_events[1]['start']
for ev in dst_events:
start_dt = utc.localize(datetime.datetime.utcfromtimestamp(ev['start']))
start_dt = start_dt.astimezone(timezone('US/Central'))
assert start_dt.timetuple().tm_hour == 11
def test_existing_schedule(mocker):
mock_dt = datetime.datetime(year=2017, month=2, day=5, hour=0, tzinfo=timezone('US/Pacific'))
mocker.patch('oncall.scheduler.default.Scheduler.get_schedule_last_epoch').return_value = \
calendar.timegm(mock_dt.astimezone(utc).timetuple())
mocker.patch('time.time').return_value = time.mktime(datetime.datetime(year=2017, month=2, day=7).timetuple())
start = DAY + 10 * HOUR + 30 * MIN # Monday at 10:30 am
schedule_foo = {
'timezone': 'US/Pacific',
'auto_populate_threshold': 21,
'events': [{
'start': start, # 24hr weeklong shift starting Monday at 10:30 am
'duration': WEEK
}]
}
scheduler = oncall.scheduler.default.Scheduler()
future_events, last_epoch = scheduler.calculate_future_events(schedule_foo, None)
assert len(future_events) == 3
mondays = (13, 20, 27)
for epoch, monday in zip(future_events, mondays):
assert len(epoch) == 1
ev = epoch[0]
start_dt = utc.localize(datetime.datetime.utcfromtimestamp(ev['start']))
start_dt = start_dt.astimezone(timezone('US/Pacific'))
assert start_dt.timetuple().tm_year == mock_dt.timetuple().tm_year
assert start_dt.timetuple().tm_mon == mock_dt.timetuple().tm_mon
assert start_dt.timetuple().tm_mday == monday
assert start_dt.timetuple().tm_wday == 0 # Monday
assert start_dt.timetuple().tm_hour == 10 # 10:
assert start_dt.timetuple().tm_min == 30 # 30 am
assert start_dt.timetuple().tm_sec == 00
assert ev['end'] - ev['start'] == WEEK
def test_existing_schedule_change_epoch(mocker):
mock_dt = datetime.datetime(year=2017, month=2, day=5, hour=0, tzinfo=timezone('US/Eastern'))
mocker.patch('oncall.scheduler.default.Scheduler.get_schedule_last_epoch').return_value = \
calendar.timegm(mock_dt.astimezone(utc).timetuple())
mocker.patch('time.time').return_value = time.mktime(datetime.datetime(year=2017, month=2, day=7).timetuple())
start = DAY + 10 * HOUR + 30 * MIN # Monday at 10:30 am
schedule_foo = {
'timezone': 'US/Pacific',
'auto_populate_threshold': 21,
'events': [{
'start': start, # 24hr weeklong shift starting Monday at 10:30 am
'duration': WEEK
}]
}
scheduler = oncall.scheduler.default.Scheduler()
future_events, last_epoch = scheduler.calculate_future_events(schedule_foo, None)
assert len(future_events) == 3
mondays = (13, 20, 27)
for epoch, monday in zip(future_events, mondays):
assert len(epoch) == 1
ev = epoch[0]
start_dt = utc.localize(datetime.datetime.utcfromtimestamp(ev['start']))
start_dt = start_dt.astimezone(timezone('US/Pacific'))
assert start_dt.timetuple().tm_year == mock_dt.timetuple().tm_year
assert start_dt.timetuple().tm_mon == mock_dt.timetuple().tm_mon
assert start_dt.timetuple().tm_mday == monday
assert start_dt.timetuple().tm_wday == 0 # Monday
assert start_dt.timetuple().tm_hour == 10 # 10:
assert start_dt.timetuple().tm_min == 30 # 30 am
assert start_dt.timetuple().tm_sec == 00
assert ev['end'] - ev['start'] == WEEK
def test_find_least_active_available_user(mocker):
mock_user_ids = [123, 456, 789]
mocker.patch('oncall.scheduler.default.Scheduler.find_new_user_in_roster').return_value = set()
mocker.patch('oncall.scheduler.default.Scheduler.get_roster_user_ids').return_value = [i for i in mock_user_ids]
mock_busy_user_by_range = mocker.patch('oncall.scheduler.default.Scheduler.get_busy_user_by_event_range')
mock_active_user_by_team = mocker.patch('oncall.scheduler.default.Scheduler.find_least_active_user_id_by_team')
def mock_busy_user_by_range_side_effect(user_ids, team_id, events, cursor, table_name='event'):
assert user_ids == set(mock_user_ids)
return [123]
mock_busy_user_by_range.side_effect = mock_busy_user_by_range_side_effect
future_events = [{'start': 440, 'end': 570},
{'start': 570, 'end': 588},
{'start': 600, 'end': 700}]
scheduler = oncall.scheduler.default.Scheduler()
scheduler.find_next_user_id(MOCK_SCHEDULE, future_events, None, 'event')
mock_active_user_by_team.assert_called_with({456, 789}, 1, 440, 2, None, 'event')
def test_find_least_active_available_user_conflicts(mocker):
mock_user_ids = [123, 456, 789]
mocker.patch('oncall.scheduler.default.Scheduler.find_new_user_in_roster').return_value = None
mocker.patch('oncall.scheduler.default.Scheduler.get_roster_user_ids').return_value = [i for i in mock_user_ids]
mock_busy_user_by_range = mocker.patch('oncall.scheduler.default.Scheduler.get_busy_user_by_event_range')
mock_active_user_by_team = mocker.patch('oncall.scheduler.default.Scheduler.find_least_active_user_id_by_team')
def mock_busy_user_by_range_side_effect(user_ids, team_id, events, cursor, table_name='event'):
assert user_ids == set(mock_user_ids)
return [123, 456, 789]
mock_busy_user_by_range.side_effect = mock_busy_user_by_range_side_effect
future_events = [{'start': 440, 'end': 570}]
scheduler = oncall.scheduler.default.Scheduler()
assert scheduler.find_next_user_id(MOCK_SCHEDULE, future_events, None, table_name='event') is None
mock_active_user_by_team.assert_not_called()