Sunil Mohan Adapa 3255a7e658
backups: schedule: tests: Fix failures due to long test run
Closes: #2023.

When importing the test module, datetime.now() is executed and value is kept. If
a test suite runs for a long time, the time deltas are being calculated much
later when the test case runs. This creates an difference in expected different
between the two values.

Fix this by completely removing all uses of time relative to current date time.
Only use absolute date time values. This should not reduce the effectiveness of
the test cases.

Tests performed:

- Rerun unit tests for backups module.

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
2021-01-30 14:29:05 -05:00

480 lines
14 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-or-later
"""
Test scheduling of backups.
"""
import json
from datetime import datetime, timedelta
from unittest.mock import MagicMock, call, patch
import pytest
import plinth.modules.backups.repository # noqa, pylint: disable=unused-import
from plinth.app import App
from ..components import BackupRestore
from ..schedule import Schedule
setup_helper = MagicMock()
class AppTest(App):
"""Sample App for testing."""
app_id = 'test-app'
def _get_backup_component(name):
"""Return a BackupRestore component."""
return BackupRestore(name)
def _get_test_app(name):
"""Return an App."""
app = AppTest()
app.app_id = name
app._all_apps[name] = app
app.add(BackupRestore(name + '-component'))
return app
def test_init_default_values():
"""Test initialization of schedule with default values."""
schedule = Schedule('test-uuid')
assert schedule.repository_uuid == 'test-uuid'
assert not schedule.enabled
assert schedule.daily_to_keep == 5
assert schedule.weekly_to_keep == 3
assert schedule.monthly_to_keep == 3
assert schedule.run_at_hour == 2
assert schedule.unselected_apps == []
def test_init():
"""Test initialization with explicit values."""
schedule = Schedule('test-uuid', enabled=True, daily_to_keep=1,
weekly_to_keep=2, monthly_to_keep=5, run_at_hour=0,
unselected_apps=['test-app1', 'test-app2'])
assert schedule.repository_uuid == 'test-uuid'
assert schedule.enabled
assert schedule.daily_to_keep == 1
assert schedule.weekly_to_keep == 2
assert schedule.monthly_to_keep == 5
assert schedule.run_at_hour == 0
assert schedule.unselected_apps == ['test-app1', 'test-app2']
def test_get_storage_format():
"""Test that storage format is properly returned."""
schedule = Schedule('test-uuid', enabled=True, daily_to_keep=1,
weekly_to_keep=2, monthly_to_keep=5, run_at_hour=23,
unselected_apps=['test-app1', 'test-app2'])
assert schedule.get_storage_format() == {
'enabled': True,
'daily_to_keep': 1,
'weekly_to_keep': 2,
'monthly_to_keep': 5,
'run_at_hour': 23,
'unselected_apps': ['test-app1', 'test-app2'],
}
def _get_archives_from_test_data(data):
"""Return a list of archives from test data."""
archives = []
for index, item in enumerate(data):
archive_time = item['time']
if isinstance(archive_time, str):
archive_time = datetime.strptime(archive_time,
'%Y-%m-%d %H:%M:%S+0000')
archive = {
'comment':
json.dumps({
'type': 'scheduled',
'periods': item['periods']
}),
'start':
archive_time.strftime('%Y-%m-%dT%H:%M:%S.%f'),
'name':
f'archive-{index}'
}
archives.append(archive)
return archives
# - First item is the arguments to send construct Schedule()
# - Second item is the list of previous backups in the system.
# - Third item is the return value of datetime.datetime.now().
# - Fourth item is the list of periods for which backups must be triggered.
# - Fifth item is the list of expected archives to be deleted after backup.
cases = [
# Schedule is disabled
[
[False, 10, 10, 10, 0],
[],
datetime(2021, 1, 1),
[],
[],
],
# No past backups
[
[True, 10, 10, 10, 0],
[],
datetime(2021, 1, 1),
['daily', 'weekly', 'monthly'],
[],
],
# Daily backup taken recently
[
[True, 10, 10, 10, 0],
[{
'periods': ['daily'],
'time': datetime(2021, 1, 1) - timedelta(seconds=600)
}],
datetime(2021, 1, 1),
[],
[],
],
# Weekly backup taken recently
[
[True, 10, 10, 10, 0],
[{
'periods': ['weekly'],
'time': datetime(2021, 1, 1) - timedelta(seconds=600)
}],
datetime(2021, 1, 1),
[],
[],
],
# Monthly backup taken recently
[
[True, 10, 10, 10, 0],
[{
'periods': ['monthly'],
'time': datetime(2021, 1, 1) - timedelta(seconds=600)
}],
datetime(2021, 1, 1),
[],
[],
],
# Backup taken not so recently
[
[True, 10, 10, 10, 0],
[{
'periods': ['daily'],
'time': datetime(2021, 1, 1) - timedelta(seconds=3 * 3600)
}],
datetime(2021, 1, 1),
['daily', 'weekly', 'monthly'],
[],
],
# Too long since a daily backup, not scheduled time
[
[True, 10, 10, 10, 2],
[{
'periods': ['daily'],
'time': datetime(2021, 1, 1) - timedelta(days=1, seconds=3601)
}],
datetime(2021, 1, 1),
['daily', 'weekly', 'monthly'],
[],
],
# No too long since a daily backup, not scheduled time
[
[True, 10, 10, 10, 2],
[{
'periods': ['daily'],
'time': datetime(2021, 1, 1) - timedelta(days=1, seconds=3600)
}],
datetime(2021, 1, 1),
['weekly', 'monthly'],
[],
],
# Too long since a weekly backup, not scheduled time
[
[True, 10, 10, 10, 2],
[{
'periods': ['weekly'],
'time': datetime(2021, 1, 1) - timedelta(days=7, seconds=3601)
}],
datetime(2021, 1, 1),
['daily', 'weekly', 'monthly'],
[],
],
# No too long since a daily backup, not scheduled time
[
[True, 10, 10, 10, 2],
[{
'periods': ['weekly'],
'time': datetime(2021, 1, 1) - timedelta(days=7, seconds=3600)
}],
datetime(2021, 1, 1),
['daily', 'monthly'],
[],
],
# Too long since a monthly backup, not scheduled time, year rounding
[
[True, 10, 10, 10, 2],
[{
'periods': ['monthly'],
'time': datetime(2020, 12, 1)
}],
datetime(2021, 1, 1, 1, 0, 1),
['daily', 'weekly', 'monthly'],
[],
],
# No too long since a monthly backup, not scheduled time, year rounding
[
[True, 10, 10, 10, 2],
[{
'periods': ['monthly'],
'time': datetime(2020, 12, 1)
}],
datetime(2021, 1, 1, 1),
['daily', 'weekly'],
[],
],
# Too long since a monthly backup, not scheduled time, no year rounding
[
[True, 10, 10, 10, 2],
[{
'periods': ['monthly'],
'time': datetime(2020, 11, 1)
}],
datetime(2020, 12, 1, 1, 0, 1),
['daily', 'weekly', 'monthly'],
[],
],
# No too long since a monthly backup, not scheduled time, no year rounding
[
[True, 10, 10, 10, 2],
[{
'periods': ['monthly'],
'time': datetime(2020, 11, 1)
}],
datetime(2020, 12, 1, 1),
['daily', 'weekly'],
[],
],
# Time for daily backup
[
[True, 10, 10, 10, 0],
[{
'periods': ['daily', 'weekly', 'monthly'],
'time': datetime(2021, 1, 2) - timedelta(seconds=3 * 3600)
}],
datetime(2021, 1, 2),
['daily'],
[],
],
# Time for daily backup, different scheduled time
[
[True, 10, 10, 10, 11],
[{
'periods': ['daily', 'weekly', 'monthly'],
'time': datetime(2021, 1, 2, 11) - timedelta(seconds=3 * 3600)
}],
datetime(2021, 1, 2, 11),
['daily'],
[],
],
# Time for daily/weekly backup, 2021-01-03 is a Sunday
[
[True, 10, 10, 10, 0],
[{
'periods': ['daily', 'weekly', 'monthly'],
'time': datetime(2021, 1, 3) - timedelta(seconds=3 * 3600)
}],
datetime(2021, 1, 3),
['daily', 'weekly'],
[],
],
# Time for daily/monthly backup
[
[True, 10, 10, 10, 0],
[{
'periods': ['daily', 'weekly', 'monthly'],
'time': datetime(2021, 1, 1) - timedelta(seconds=3 * 3600)
}],
datetime(2021, 1, 1),
['daily', 'monthly'],
[],
],
# Daily backups disabled by setting the no. of backups to keep to 0
[
[True, 0, 10, 10, 0],
[],
datetime(2021, 1, 1),
['weekly', 'monthly'],
[],
],
# Weekly backups disabled by setting the no. of backups to keep to 0
[
[True, 10, 0, 10, 0],
[],
datetime(2021, 1, 1),
['daily', 'monthly'],
[],
],
# Monthly backups disabled by setting the no. of backups to keep to 0
[
[True, 10, 10, 0, 0],
[],
datetime(2021, 1, 1),
['daily', 'weekly'],
[],
],
# Not scheduled, not too long since last, no backup necessary
[
[True, 10, 10, 10, 0],
[{
'periods': ['daily', 'weekly', 'monthly'],
'time': datetime(2021, 1, 2, 1) - timedelta(seconds=3 * 3600)
}],
datetime(2021, 1, 2, 1),
[],
[],
],
# Cleanup daily backups
[
[True, 2, 10, 10, 0],
[{
'periods': ['daily'],
'time': datetime(2021, 1, 3, 1) - timedelta(seconds=3 * 3600)
}, {
'periods': ['daily'],
'time': datetime(2021, 1, 2, 1) - timedelta(seconds=3 * 3600)
}, {
'periods': ['daily'],
'time': datetime(2021, 1, 1, 1) - timedelta(seconds=3 * 3600)
}, {
'periods': ['daily'],
'time': datetime(2021, 1, 1, 0) - timedelta(seconds=3 * 3600)
}],
datetime(2021, 1, 4, 1),
['daily', 'weekly', 'monthly'],
['archive-2', 'archive-3'],
],
# Cleanup weekly backups
[
[True, 10, 2, 10, 0],
[{
'periods': ['weekly'],
'time': datetime(2021, 1, 3, 1) - timedelta(seconds=3 * 3600)
}, {
'periods': ['weekly'],
'time': datetime(2021, 1, 2, 1) - timedelta(seconds=3 * 3600)
}, {
'periods': ['weekly'],
'time': datetime(2021, 1, 1, 1) - timedelta(seconds=3 * 3600)
}, {
'periods': ['weekly'],
'time': datetime(2021, 1, 1, 0) - timedelta(seconds=3 * 3600)
}],
datetime(2021, 1, 4, 1),
['daily', 'monthly'],
['archive-2', 'archive-3'],
],
# Cleanup monthly backups
[
[True, 10, 10, 2, 0],
[{
'periods': ['monthly'],
'time': datetime(2021, 1, 3, 1) - timedelta(seconds=3 * 3600)
}, {
'periods': ['monthly'],
'time': datetime(2021, 1, 2, 1) - timedelta(seconds=3 * 3600)
}, {
'periods': ['monthly'],
'time': datetime(2021, 1, 1, 1) - timedelta(seconds=3 * 3600)
}, {
'periods': ['monthly'],
'time': datetime(2021, 1, 1, 0) - timedelta(seconds=3 * 3600)
}],
datetime(2021, 1, 4, 1),
['daily', 'weekly'],
['archive-2', 'archive-3'],
],
# Cleanup daily backups, but keep due to them being weekly/monthly too
[
[True, 2, 1, 10, 0],
[{
'periods': ['daily'],
'time': datetime(2021, 1, 6, 1) - timedelta(seconds=3 * 3600)
}, {
'periods': ['daily'],
'time': datetime(2021, 1, 5, 1) - timedelta(seconds=3 * 3600)
}, {
'periods': ['daily', 'weekly'],
'time': datetime(2021, 1, 4, 1) - timedelta(seconds=3 * 3600)
}, {
'periods': ['daily', 'weekly'],
'time': datetime(2021, 1, 3, 1) - timedelta(seconds=3 * 3600)
}, {
'periods': ['daily', 'monthly'],
'time': datetime(2021, 1, 2, 0) - timedelta(seconds=3 * 3600)
}, {
'periods': ['daily'],
'time': datetime(2021, 1, 1, 0) - timedelta(seconds=3 * 3600)
}],
datetime(2021, 1, 7, 1),
['daily'],
['archive-3', 'archive-5'],
],
]
@pytest.mark.parametrize(
'schedule_params,archives_data,test_now,run_periods,cleanups', cases)
@patch('plinth.modules.backups.repository.get_instance')
def test_run_schedule(get_instance, schedule_params, archives_data, test_now,
run_periods, cleanups):
"""Test that backups are run at expected time."""
setup_helper.get_state.return_value = 'up-to-date'
repository = MagicMock()
repository.list_archives.side_effect = \
lambda: _get_archives_from_test_data(archives_data)
get_instance.return_value = repository
with patch('plinth.modules.backups.schedule.datetime') as mock_datetime, \
patch('plinth.app.App.list') as app_list:
app_list.return_value = [
_get_test_app('test-app1'),
_get_test_app('test-app2'),
_get_test_app('test-app3')
]
mock_datetime.now.return_value = test_now
mock_datetime.strptime = datetime.strptime
mock_datetime.min = datetime.min
mock_datetime.side_effect = lambda *args, **kwargs: datetime(
*args, **kwargs)
schedule = Schedule('test_uuid', schedule_params[0],
schedule_params[1], schedule_params[2],
schedule_params[3], schedule_params[4],
['test-app2'])
schedule.run_schedule()
if not run_periods:
repository.create_archive.assert_not_called()
else:
run_periods.sort()
name = 'scheduled: {periods}: {datetime}'.format(
periods=', '.join(run_periods),
datetime=mock_datetime.now().strftime('%Y-%m-%d:%H:%M'))
app_ids = ['test-app1', 'test-app3']
archive_comment = json.dumps({
'type': 'scheduled',
'periods': run_periods
}).replace('{', '{{').replace('}', '}}')
repository.create_archive.assert_has_calls(
[call(name, app_ids, archive_comment=archive_comment)])
if not cleanups:
repository.delete_archive.assert_not_called()
else:
calls = [call(name) for name in cleanups]
repository.delete_archive.assert_has_calls(calls)