Update tests
This commit is contained in:
@@ -1,332 +1,449 @@
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
import tempfile
|
||||
from unittest import TestCase, mock
|
||||
from unittest import mock
|
||||
from datetime import datetime
|
||||
|
||||
from curateipsum import backup as bk, fs
|
||||
from curateipsum import backup as bk
|
||||
|
||||
|
||||
class TestBackupCleanup(TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.backup_dir = tempfile.TemporaryDirectory(prefix="backup_")
|
||||
class TestBackupCleanup:
|
||||
"""Tests for backup cleanup and retention policies."""
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.backup_dir.cleanup()
|
||||
|
||||
def _add_backup(self, backup_name: str) -> fs.PseudoDirEntry:
|
||||
backup = fs.PseudoDirEntry(os.path.join(self.backup_dir.name, backup_name))
|
||||
os.mkdir(backup.path)
|
||||
bk.set_backup_marker(backup)
|
||||
|
||||
fd, path = tempfile.mkstemp(prefix="backup_file_", dir=backup.path)
|
||||
with open(fd, "w") as f:
|
||||
f.write(''.join(random.choices(string.printable, k=128)))
|
||||
return backup
|
||||
|
||||
@staticmethod
|
||||
def _check_backup_not_empty(backup: fs.PseudoDirEntry) -> bool:
|
||||
return bool(os.listdir(backup.path))
|
||||
|
||||
def _check_backups(self, expected_backups):
|
||||
backups_list = os.listdir(self.backup_dir.name)
|
||||
self.assertEqual(sorted(b.name for b in expected_backups),
|
||||
sorted(backups_list))
|
||||
for b in expected_backups:
|
||||
self.assertTrue(self._check_backup_not_empty(b))
|
||||
|
||||
def _run_cleanup(self, **kwargs):
|
||||
""" Run cleanup_old_backups with null parameters. """
|
||||
cleanup_kwargs = {
|
||||
"backups_dir": self.backup_dir.name,
|
||||
"dry_run": False,
|
||||
"keep_all": None,
|
||||
"keep_daily": None,
|
||||
"keep_weekly": None,
|
||||
"keep_monthly": None,
|
||||
"keep_yearly": None,
|
||||
}
|
||||
cleanup_kwargs.update(**kwargs)
|
||||
bk.cleanup_old_backups(**cleanup_kwargs)
|
||||
|
||||
def test_no_backups(self):
|
||||
""" Test behaviour with no available backups """
|
||||
bk.cleanup_old_backups(self.backup_dir.name)
|
||||
self.assertFalse(os.listdir(self.backup_dir.name))
|
||||
def test_no_backups(self, backup_dir, run_cleanup):
|
||||
"""Test behaviour with no available backups"""
|
||||
backup_dir.mkdir()
|
||||
bk.cleanup_old_backups(str(backup_dir))
|
||||
assert not os.listdir(str(backup_dir))
|
||||
|
||||
@mock.patch(f"{bk.__name__}.datetime", wraps=datetime)
|
||||
def test_only_one_backup(self, mock_datetime):
|
||||
""" Test the only backup will not be removed in any case """
|
||||
def test_only_one_backup(self, mock_datetime, add_backup, run_cleanup,
|
||||
check_backups):
|
||||
"""Test the only backup will not be removed in any case"""
|
||||
mock_datetime.now.return_value = datetime(2021, 10, 20)
|
||||
# very old backup
|
||||
only_backup = self._add_backup("20010101_0000")
|
||||
self._run_cleanup(keep_all=1)
|
||||
self._check_backups([only_backup])
|
||||
only_backup = add_backup("20010101_0000")
|
||||
run_cleanup(keep_all=1)
|
||||
check_backups([only_backup])
|
||||
|
||||
@mock.patch(f"{bk.__name__}.datetime", wraps=datetime)
|
||||
def test_at_least_one_should_be_left(self, mock_datetime):
|
||||
""" Test at least one backup should be left """
|
||||
def test_at_least_one_should_be_left(self, mock_datetime, add_backup,
|
||||
run_cleanup, check_backups):
|
||||
"""Test at least one backup should be left"""
|
||||
mock_datetime.now.return_value = datetime(2021, 10, 20)
|
||||
backups = [
|
||||
self._add_backup("20211103_0300"), # this one is the latest and should be kept
|
||||
self._add_backup("20201216_0100"), # the rest should be removed
|
||||
self._add_backup("20200716_0100"),
|
||||
self._add_backup("20181116_0100"),
|
||||
add_backup("20211103_0300"), # latest, should be kept
|
||||
add_backup("20201216_0100"), # rest should be removed
|
||||
add_backup("20200716_0100"),
|
||||
add_backup("20181116_0100"),
|
||||
]
|
||||
expected_backups = [backups[0]]
|
||||
self._run_cleanup()
|
||||
self._check_backups(expected_backups)
|
||||
run_cleanup()
|
||||
check_backups(expected_backups)
|
||||
|
||||
@mock.patch(f"{bk.__name__}.datetime", wraps=datetime)
|
||||
def test_keep_all_threshold_only(self, mock_datetime):
|
||||
""" Test threshold for keeping all backups """
|
||||
def test_keep_all_threshold_only(self, mock_datetime, add_backup,
|
||||
run_cleanup, check_backups):
|
||||
"""Test threshold for keeping all backups"""
|
||||
mock_datetime.now.return_value = datetime(2021, 10, 20)
|
||||
backups = [
|
||||
self._add_backup("20211019_0300"), # keep
|
||||
self._add_backup("20211017_0100"), # keep
|
||||
self._add_backup("20211016_2300"), # remove, older than 3 days
|
||||
add_backup("20211019_0300"), # keep
|
||||
add_backup("20211017_0100"), # keep
|
||||
add_backup("20211016_2300"), # remove, older than 3 days
|
||||
]
|
||||
expected_backups = backups[:2]
|
||||
self._run_cleanup(keep_all=3)
|
||||
self._check_backups(expected_backups)
|
||||
run_cleanup(keep_all=3)
|
||||
check_backups(expected_backups)
|
||||
|
||||
@mock.patch(f"{bk.__name__}.datetime", wraps=datetime)
|
||||
def test_keep_daily_threshold_only(self, mock_datetime):
|
||||
""" Test threshold for keeping daily backups """
|
||||
def test_keep_daily_threshold_only(self, mock_datetime, add_backup,
|
||||
run_cleanup, check_backups):
|
||||
"""Test threshold for keeping daily backups"""
|
||||
mock_datetime.now.return_value = datetime(2021, 10, 20)
|
||||
backups = [
|
||||
self._add_backup("20211019_0300"), # keep, first daily backup at 2021-10-19
|
||||
self._add_backup("20211017_2100"), # remove, not the first daily backup
|
||||
self._add_backup("20211017_0100"), # remove, not the first daily backup
|
||||
self._add_backup("20211017_0030"), # keep, first daily backup at 2021-10-17
|
||||
self._add_backup("20211016_2300"), # remove, older than 3 days
|
||||
self._add_backup("20211016_0100"), # remove, older than 3 days
|
||||
add_backup("20211019_0300"), # keep, first daily at 2021-10-19
|
||||
add_backup("20211017_2100"), # remove, not first daily
|
||||
add_backup("20211017_0100"), # remove, not first daily
|
||||
add_backup("20211017_0030"), # keep, first daily at 2021-10-17
|
||||
add_backup("20211016_2300"), # remove, older than 3 days
|
||||
add_backup("20211016_0100"), # remove, older than 3 days
|
||||
]
|
||||
expected_backups = [backups[0], backups[3]]
|
||||
self._run_cleanup(keep_daily=3)
|
||||
self._check_backups(expected_backups)
|
||||
run_cleanup(keep_daily=3)
|
||||
check_backups(expected_backups)
|
||||
|
||||
@mock.patch(f"{bk.__name__}.datetime", wraps=datetime)
|
||||
def test_keep_all_and_daily_thresholds(self, mock_datetime):
|
||||
""" Test threshold for keeping all and daily backups """
|
||||
def test_keep_all_and_daily_thresholds(self, mock_datetime, add_backup,
|
||||
run_cleanup, check_backups):
|
||||
"""Test threshold for keeping all and daily backups"""
|
||||
mock_datetime.now.return_value = datetime(2021, 10, 20)
|
||||
backups = [
|
||||
self._add_backup("20211019_0300"), # keep, newer than 3 days
|
||||
self._add_backup("20211017_0200"), # keep, newer than 3 days
|
||||
self._add_backup("20211017_0100"), # keep, newer than 3 days
|
||||
self._add_backup("20211016_2300"), # remove, not the first daily backup
|
||||
self._add_backup("20211016_2200"), # keep, the first daily backup at 2021-10-16
|
||||
self._add_backup("20211015_2200"), # remove, not the first daily backup
|
||||
self._add_backup("20211015_1500"), # remove, not the first daily backup
|
||||
self._add_backup("20211015_0200"), # keep, the first daily backup at 2021-10-15
|
||||
self._add_backup("20211014_2200"), # remove, older than 5 days
|
||||
self._add_backup("20211014_2000"), # remove, older than 5 days
|
||||
self._add_backup("20211014_1232"), # remove, older than 5 days
|
||||
add_backup("20211019_0300"), # keep, newer than 3 days
|
||||
add_backup("20211017_0200"), # keep, newer than 3 days
|
||||
add_backup("20211017_0100"), # keep, newer than 3 days
|
||||
add_backup("20211016_2300"), # remove, not first daily
|
||||
add_backup("20211016_2200"), # keep, first daily at 2021-10-16
|
||||
add_backup("20211015_2200"), # remove, not first daily
|
||||
add_backup("20211015_1500"), # remove, not first daily
|
||||
add_backup("20211015_0200"), # keep, first daily at 2021-10-15
|
||||
add_backup("20211014_2200"), # remove, older than 5 days
|
||||
add_backup("20211014_2000"), # remove, older than 5 days
|
||||
add_backup("20211014_1232"), # remove, older than 5 days
|
||||
]
|
||||
expected_backups = backups[0:3] + [backups[4]] + [backups[7]]
|
||||
self._run_cleanup(keep_all=3, keep_daily=5)
|
||||
self._check_backups(expected_backups)
|
||||
run_cleanup(keep_all=3, keep_daily=5)
|
||||
check_backups(expected_backups)
|
||||
|
||||
@mock.patch(f"{bk.__name__}.datetime", wraps=datetime)
|
||||
def test_keep_weekly_threshold_only(self, mock_datetime):
|
||||
""" Test threshold for keeping weekly backups """
|
||||
def test_keep_weekly_threshold_only(self, mock_datetime, add_backup,
|
||||
run_cleanup, check_backups):
|
||||
"""Test threshold for keeping weekly backups"""
|
||||
mock_datetime.now.return_value = datetime(2021, 11, 11)
|
||||
backups = [
|
||||
self._add_backup("20211111_0300"), # remove, not the first weekly backup (Thursday)
|
||||
self._add_backup("20211110_0300"), # remove, not the first weekly backup (Wednesday)
|
||||
self._add_backup("20211108_0100"), # keep, first weekly backup at 2021-11-08 (Monday)
|
||||
self._add_backup("20211107_2300"), # remove, not the first weekly backup (Sunday)
|
||||
self._add_backup("20211107_0100"), # keep, first weekly backup at 2021-11-07 (Sunday)
|
||||
self._add_backup("20211031_0100"), # remove, not the first weekly backup (Sunday)
|
||||
self._add_backup("20211025_0100"), # keep, first weekly backup at 2021-10-25 (Monday)
|
||||
self._add_backup("20211024_0100"), # remove, not the first weekly backup (Sunday)
|
||||
self._add_backup("20211023_0100"), # remove, not the first weekly backup (Saturday)
|
||||
self._add_backup("20211022_0100"), # keep, first weekly backup at 2021-10-22 (Friday)
|
||||
self._add_backup("20211008_0100"), # remove, not the first weekly backup (Friday)
|
||||
self._add_backup("20211007_0100"), # remove, not the first weekly backup (Thursday)
|
||||
self._add_backup("20211004_0100"), # keep, first weekly backup at 2021-10-04 (Monday)
|
||||
self._add_backup("20211003_0100"), # remove, older than 5 weeks
|
||||
self._add_backup("20211002_0100"), # remove, older than 5 weeks
|
||||
add_backup("20211111_0300"), # remove, not first weekly (Thu)
|
||||
add_backup("20211110_0300"), # remove, not first weekly (Wed)
|
||||
add_backup("20211108_0100"), # keep, first weekly 2021-11-08 (Mon)
|
||||
add_backup("20211107_2300"), # remove, not first weekly (Sun)
|
||||
add_backup("20211107_0100"), # keep, first weekly 2021-11-07 (Sun)
|
||||
add_backup("20211031_0100"), # remove, not first weekly (Sun)
|
||||
add_backup("20211025_0100"), # keep, first weekly 2021-10-25 (Mon)
|
||||
add_backup("20211024_0100"), # remove, not first weekly (Sun)
|
||||
add_backup("20211023_0100"), # remove, not first weekly (Sat)
|
||||
add_backup("20211022_0100"), # keep, first weekly 2021-10-22 (Fri)
|
||||
add_backup("20211008_0100"), # remove, not first weekly (Fri)
|
||||
add_backup("20211007_0100"), # remove, not first weekly (Thu)
|
||||
add_backup("20211004_0100"), # keep, first weekly 2021-10-04 (Mon)
|
||||
add_backup("20211003_0100"), # remove, older than 5 weeks
|
||||
add_backup("20211002_0100"), # remove, older than 5 weeks
|
||||
|
||||
]
|
||||
expected_backups = [backups[2], backups[4], backups[6],
|
||||
backups[9], backups[12]]
|
||||
self._run_cleanup(keep_weekly=5)
|
||||
self._check_backups(expected_backups)
|
||||
run_cleanup(keep_weekly=5)
|
||||
check_backups(expected_backups)
|
||||
|
||||
@mock.patch(f"{bk.__name__}.datetime", wraps=datetime)
|
||||
def test_keep_weekly_threshold_inclusive(self, mock_datetime):
|
||||
""" Test threshold for keeping weekly backups """
|
||||
def test_keep_weekly_threshold_inclusive(self, mock_datetime, add_backup,
|
||||
run_cleanup, check_backups):
|
||||
"""Test threshold for keeping weekly backups"""
|
||||
mock_datetime.now.return_value = datetime(2021, 11, 11)
|
||||
backups = [
|
||||
self._add_backup("20211111_0300"), # remove, not the first weekly backup (Thursday)
|
||||
self._add_backup("20211110_0300"), # keep, first weekly backup (Wednesday)
|
||||
self._add_backup("20211107_0100"), # remove, not the first weekly backup (Sunday)
|
||||
self._add_backup("20211102_0100"), # keep, first weekly backup (Tuesday)
|
||||
add_backup("20211111_0300"), # remove, not first weekly (Thu)
|
||||
add_backup("20211110_0300"), # keep, first weekly (Wed)
|
||||
add_backup("20211107_0100"), # remove, not first weekly (Sun)
|
||||
add_backup("20211102_0100"), # keep, first weekly (Tue)
|
||||
]
|
||||
expected_backups = [backups[1], backups[3]]
|
||||
self._run_cleanup(keep_weekly=5)
|
||||
self._check_backups(expected_backups)
|
||||
run_cleanup(keep_weekly=5)
|
||||
check_backups(expected_backups)
|
||||
|
||||
@mock.patch(f"{bk.__name__}.datetime", wraps=datetime)
|
||||
def test_keep_monthly_threshold_only(self, mock_datetime):
|
||||
""" Test threshold for keeping monthly backups """
|
||||
def test_keep_monthly_threshold_only(self, mock_datetime, add_backup,
|
||||
run_cleanup, check_backups):
|
||||
"""Test threshold for keeping monthly backups"""
|
||||
mock_datetime.now.return_value = datetime(2021, 11, 11)
|
||||
backups = [
|
||||
self._add_backup("20211103_0300"), # keep, first monthly backup at 2021-11
|
||||
self._add_backup("20211019_0300"), # remove, not the first monthly backup
|
||||
self._add_backup("20211017_2100"), # remove, not the first monthly backup
|
||||
self._add_backup("20211017_0100"), # keep, first monthly backup at 2021-10
|
||||
self._add_backup("20210916_2300"), # remove, not the first monthly backup
|
||||
self._add_backup("20210916_0100"), # keep, first monthly backup at 2021-09
|
||||
self._add_backup("20210816_0100"), # remove, not the first monthly backup
|
||||
self._add_backup("20210810_0000"), # keep, first monthly backup at 2021-08
|
||||
self._add_backup("20210716_0100"), # remove, older than 3 months
|
||||
self._add_backup("20210715_0100"), # remove, older than 3 months
|
||||
add_backup("20211103_0300"), # keep, first monthly at 2021-11
|
||||
add_backup("20211019_0300"), # remove, not first monthly
|
||||
add_backup("20211017_2100"), # remove, not first monthly
|
||||
add_backup("20211017_0100"), # keep, first monthly at 2021-10
|
||||
add_backup("20210916_2300"), # remove, not first monthly
|
||||
add_backup("20210916_0100"), # keep, first monthly at 2021-09
|
||||
add_backup("20210816_0100"), # remove, not first monthly
|
||||
add_backup("20210810_0000"), # keep, first monthly at 2021-08
|
||||
add_backup("20210716_0100"), # remove, older than 3 months
|
||||
add_backup("20210715_0100"), # remove, older than 3 months
|
||||
]
|
||||
expected_backups = [backups[0], backups[3], backups[5], backups[7]]
|
||||
self._run_cleanup(keep_monthly=3)
|
||||
self._check_backups(expected_backups)
|
||||
run_cleanup(keep_monthly=3)
|
||||
check_backups(expected_backups)
|
||||
|
||||
@mock.patch(f"{bk.__name__}.datetime", wraps=datetime)
|
||||
def test_keep_yearly_threshold_only(self, mock_datetime):
|
||||
""" Test threshold for keeping yearly backups """
|
||||
def test_keep_yearly_threshold_only(self, mock_datetime, add_backup,
|
||||
run_cleanup, check_backups):
|
||||
"""Test threshold for keeping yearly backups"""
|
||||
mock_datetime.now.return_value = datetime(2021, 11, 11)
|
||||
backups = [
|
||||
self._add_backup("20211103_0300"), # remove, not the first yearly backup in 2021
|
||||
self._add_backup("20210810_0000"), # remove, not the first yearly backup in 2021
|
||||
self._add_backup("20210716_0100"), # keep, first yearly backup in 2021
|
||||
self._add_backup("20201216_0100"), # remove, not the first yearly backup in 2020
|
||||
self._add_backup("20200716_0100"), # keep, first yearly backup in 2020
|
||||
self._add_backup("20191216_0100"), # remove, not the first yearly backup in 2019
|
||||
self._add_backup("20190316_0100"), # keep, first yearly backup in 2019
|
||||
self._add_backup("20181216_0100"), # remove, not the first yearly backup in 2018
|
||||
self._add_backup("20181116_0100"), # keep, first yearly backup in 2018
|
||||
self._add_backup("20171116_0100"), # remove, older than 3 years
|
||||
self._add_backup("20171115_0100"), # remove, older than 3 years
|
||||
add_backup("20211103_0300"), # remove, not first yearly in 2021
|
||||
add_backup("20210810_0000"), # remove, not first yearly in 2021
|
||||
add_backup("20210716_0100"), # keep, first yearly in 2021
|
||||
add_backup("20201216_0100"), # remove, not first yearly in 2020
|
||||
add_backup("20200716_0100"), # keep, first yearly in 2020
|
||||
add_backup("20191216_0100"), # remove, not first yearly in 2019
|
||||
add_backup("20190316_0100"), # keep, first yearly in 2019
|
||||
add_backup("20181216_0100"), # remove, not first yearly in 2018
|
||||
add_backup("20181116_0100"), # keep, first yearly in 2018
|
||||
add_backup("20171116_0100"), # remove, older than 3 years
|
||||
add_backup("20171115_0100"), # remove, older than 3 years
|
||||
]
|
||||
expected_backups = [backups[2], backups[4], backups[6], backups[8]]
|
||||
self._run_cleanup(keep_yearly=3)
|
||||
self._check_backups(expected_backups)
|
||||
run_cleanup(keep_yearly=3)
|
||||
check_backups(expected_backups)
|
||||
|
||||
@mock.patch(f"{bk.__name__}.datetime", wraps=datetime)
|
||||
def test_dry_run(self, mock_datetime):
|
||||
""" Test dry run does not remove anything """
|
||||
def test_dry_run(self, mock_datetime, add_backup, run_cleanup,
|
||||
check_backups):
|
||||
"""Test dry run does not remove anything"""
|
||||
mock_datetime.now.return_value = datetime(2021, 11, 11)
|
||||
backups = [
|
||||
self._add_backup("20211103_0300"),
|
||||
self._add_backup("20210810_0000"),
|
||||
self._add_backup("20210716_0100"),
|
||||
self._add_backup("20200716_0100"),
|
||||
self._add_backup("20181116_0100"),
|
||||
add_backup("20211103_0300"),
|
||||
add_backup("20210810_0000"),
|
||||
add_backup("20210716_0100"),
|
||||
add_backup("20200716_0100"),
|
||||
add_backup("20181116_0100"),
|
||||
]
|
||||
self._run_cleanup(keep_all=2, dry_run=True)
|
||||
self._check_backups(backups)
|
||||
run_cleanup(keep_all=2, dry_run=True)
|
||||
check_backups(backups)
|
||||
|
||||
|
||||
class TestBackupLock(TestCase):
|
||||
class TestBackupLock:
|
||||
"""Test suite for backup lock file functionality."""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.backup_dir = tempfile.TemporaryDirectory(prefix="backup_lock_")
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.backup_dir.cleanup()
|
||||
|
||||
def test_lock_creation(self):
|
||||
def test_lock_creation(self, backup_dir):
|
||||
"""Test that lock file is created with current PID"""
|
||||
result = bk.set_backups_lock(self.backup_dir.name)
|
||||
self.assertTrue(result)
|
||||
backup_dir.mkdir()
|
||||
result = bk.set_backups_lock(str(backup_dir))
|
||||
assert result
|
||||
|
||||
lock_path = os.path.join(self.backup_dir.name, bk.LOCK_FILE)
|
||||
self.assertTrue(os.path.exists(lock_path))
|
||||
lock_path = os.path.join(str(backup_dir), bk.LOCK_FILE)
|
||||
assert os.path.exists(lock_path)
|
||||
|
||||
with open(lock_path, "r") as f:
|
||||
pid = int(f.read().strip())
|
||||
self.assertEqual(pid, os.getpid())
|
||||
assert pid == os.getpid()
|
||||
|
||||
def test_lock_prevents_concurrent_backup(self):
|
||||
def test_lock_prevents_concurrent_backup(self, backup_dir):
|
||||
"""Test that second lock acquisition is blocked"""
|
||||
backup_dir.mkdir()
|
||||
# First lock should succeed
|
||||
result1 = bk.set_backups_lock(self.backup_dir.name)
|
||||
self.assertTrue(result1)
|
||||
result1 = bk.set_backups_lock(str(backup_dir))
|
||||
assert result1
|
||||
|
||||
# Second lock should fail (same process trying to lock again)
|
||||
# The second lock should fail (same process trying to lock again)
|
||||
# Write a different PID to simulate another process
|
||||
lock_path = os.path.join(self.backup_dir.name, bk.LOCK_FILE)
|
||||
lock_path = os.path.join(str(backup_dir), bk.LOCK_FILE)
|
||||
with open(lock_path, "w") as f:
|
||||
f.write(str(os.getpid()))
|
||||
|
||||
result2 = bk.set_backups_lock(self.backup_dir.name, force=False)
|
||||
self.assertFalse(result2)
|
||||
result2 = bk.set_backups_lock(str(backup_dir), force=False)
|
||||
assert not result2
|
||||
|
||||
def test_stale_lock_is_removed(self):
|
||||
def test_stale_lock_is_removed(self, backup_dir):
|
||||
"""Test that lock from non-existent process is cleaned up"""
|
||||
lock_path = os.path.join(self.backup_dir.name, bk.LOCK_FILE)
|
||||
backup_dir.mkdir()
|
||||
lock_path = os.path.join(str(backup_dir), bk.LOCK_FILE)
|
||||
|
||||
# Create lock with non-existent PID
|
||||
with open(lock_path, "w") as f:
|
||||
f.write("999999")
|
||||
|
||||
# Lock should succeed by removing stale lock
|
||||
result = bk.set_backups_lock(self.backup_dir.name)
|
||||
self.assertTrue(result)
|
||||
result = bk.set_backups_lock(str(backup_dir))
|
||||
assert result
|
||||
|
||||
# Verify new lock has current PID
|
||||
with open(lock_path, "r") as f:
|
||||
pid = int(f.read().strip())
|
||||
self.assertEqual(pid, os.getpid())
|
||||
assert pid == os.getpid()
|
||||
|
||||
def test_corrupted_lock_is_handled(self):
|
||||
def test_corrupted_lock_is_handled(self, backup_dir):
|
||||
"""Test that corrupted lock file is handled gracefully"""
|
||||
lock_path = os.path.join(self.backup_dir.name, bk.LOCK_FILE)
|
||||
backup_dir.mkdir()
|
||||
lock_path = os.path.join(str(backup_dir), bk.LOCK_FILE)
|
||||
|
||||
# Create corrupted lock file (non-numeric content)
|
||||
with open(lock_path, "w") as f:
|
||||
f.write("not a number")
|
||||
|
||||
# Lock should succeed by removing corrupted lock
|
||||
result = bk.set_backups_lock(self.backup_dir.name)
|
||||
self.assertTrue(result)
|
||||
result = bk.set_backups_lock(str(backup_dir))
|
||||
assert result
|
||||
|
||||
# Verify new lock has current PID
|
||||
with open(lock_path, "r") as f:
|
||||
pid = int(f.read().strip())
|
||||
self.assertEqual(pid, os.getpid())
|
||||
assert pid == os.getpid()
|
||||
|
||||
def test_empty_lock_is_handled(self):
|
||||
def test_empty_lock_is_handled(self, backup_dir):
|
||||
"""Test that empty lock file is handled gracefully"""
|
||||
lock_path = os.path.join(self.backup_dir.name, bk.LOCK_FILE)
|
||||
backup_dir.mkdir()
|
||||
lock_path = os.path.join(str(backup_dir), bk.LOCK_FILE)
|
||||
|
||||
# Create empty lock file
|
||||
# Create the empty lock file
|
||||
open(lock_path, "w").close()
|
||||
|
||||
# Lock should succeed by removing empty lock
|
||||
result = bk.set_backups_lock(self.backup_dir.name)
|
||||
self.assertTrue(result)
|
||||
result = bk.set_backups_lock(str(backup_dir))
|
||||
assert result
|
||||
|
||||
# Verify new lock has current PID
|
||||
with open(lock_path, "r") as f:
|
||||
pid = int(f.read().strip())
|
||||
self.assertEqual(pid, os.getpid())
|
||||
assert pid == os.getpid()
|
||||
|
||||
def test_lock_release(self):
|
||||
def test_lock_release(self, backup_dir):
|
||||
"""Test that lock file is properly released"""
|
||||
bk.set_backups_lock(self.backup_dir.name)
|
||||
lock_path = os.path.join(self.backup_dir.name, bk.LOCK_FILE)
|
||||
self.assertTrue(os.path.exists(lock_path))
|
||||
backup_dir.mkdir()
|
||||
bk.set_backups_lock(str(backup_dir))
|
||||
lock_path = os.path.join(str(backup_dir), bk.LOCK_FILE)
|
||||
assert os.path.exists(lock_path)
|
||||
|
||||
bk.release_backups_lock(self.backup_dir.name)
|
||||
self.assertFalse(os.path.exists(lock_path))
|
||||
bk.release_backups_lock(str(backup_dir))
|
||||
assert not os.path.exists(lock_path)
|
||||
|
||||
def test_release_nonexistent_lock(self):
|
||||
def test_release_nonexistent_lock(self, backup_dir):
|
||||
"""Test that releasing non-existent lock doesn't raise error"""
|
||||
backup_dir.mkdir()
|
||||
# Should not raise any exception
|
||||
bk.release_backups_lock(self.backup_dir.name)
|
||||
bk.release_backups_lock(str(backup_dir))
|
||||
|
||||
lock_path = os.path.join(self.backup_dir.name, bk.LOCK_FILE)
|
||||
self.assertFalse(os.path.exists(lock_path))
|
||||
lock_path = os.path.join(str(backup_dir), bk.LOCK_FILE)
|
||||
assert not os.path.exists(lock_path)
|
||||
|
||||
|
||||
# TODO add tests for iterating over backups (marker, dirname)
|
||||
class TestBackupIteration:
|
||||
"""Tests for internal backup iteration and validation functions."""
|
||||
|
||||
def test_is_backup_valid_backup(self, add_backup):
|
||||
"""Test _is_backup recognizes valid backup directory"""
|
||||
backup = add_backup("20210101_120000")
|
||||
assert bk._is_backup(backup)
|
||||
|
||||
def test_is_backup_missing_marker(self, backup_dir, tmp_path):
|
||||
"""Test _is_backup rejects directory without marker"""
|
||||
backup_dir.mkdir()
|
||||
backup_path = backup_dir / "20210101_120000"
|
||||
backup_path.mkdir()
|
||||
# Create content but no marker
|
||||
(backup_path / "file.txt").write_text("content")
|
||||
|
||||
entry = os.scandir(str(backup_dir))
|
||||
backup = next(entry)
|
||||
entry.close()
|
||||
assert not bk._is_backup(backup)
|
||||
|
||||
def test_is_backup_only_marker_no_content(self, backup_dir):
|
||||
"""Test _is_backup rejects directory with only marker file"""
|
||||
backup_dir.mkdir()
|
||||
backup_path = backup_dir / "20210101_120000"
|
||||
backup_path.mkdir()
|
||||
# Create only marker, no content
|
||||
marker_name = f"{bk.BACKUP_MARKER}_20210101_120000"
|
||||
(backup_path / marker_name).touch()
|
||||
|
||||
entry = os.scandir(str(backup_dir))
|
||||
backup = next(entry)
|
||||
entry.close()
|
||||
assert not bk._is_backup(backup)
|
||||
|
||||
def test_is_backup_invalid_name_format(self, backup_dir):
|
||||
"""Test _is_backup rejects invalid directory name"""
|
||||
backup_dir.mkdir()
|
||||
backup_path = backup_dir / "not-a-backup"
|
||||
backup_path.mkdir()
|
||||
# Create marker and content
|
||||
marker_name = f"{bk.BACKUP_MARKER}_not-a-backup"
|
||||
(backup_path / marker_name).touch()
|
||||
(backup_path / "file.txt").write_text("content")
|
||||
|
||||
entry = os.scandir(str(backup_dir))
|
||||
backup = next(entry)
|
||||
entry.close()
|
||||
assert not bk._is_backup(backup)
|
||||
|
||||
def test_iterate_backups_empty_directory(self, backup_dir):
|
||||
"""Test _iterate_backups on empty directory"""
|
||||
backup_dir.mkdir()
|
||||
backups = list(bk._iterate_backups(str(backup_dir)))
|
||||
assert backups == []
|
||||
|
||||
def test_iterate_backups_mixed_contents(self, backup_dir, add_backup):
|
||||
"""Test _iterate_backups filters non-backup entries"""
|
||||
# Create valid backups
|
||||
backup1 = add_backup("20210101_120000")
|
||||
backup2 = add_backup("20210102_120000")
|
||||
|
||||
# Create invalid entries
|
||||
(backup_dir / "random_file.txt").write_text("not a backup")
|
||||
(backup_dir / "invalid_dir").mkdir()
|
||||
(backup_dir / bk.LOCK_FILE).touch()
|
||||
|
||||
backups = sorted(bk._iterate_backups(str(backup_dir)),
|
||||
key=lambda e: e.name)
|
||||
assert len(backups) == 2
|
||||
assert backups[0].name == backup1.name
|
||||
assert backups[1].name == backup2.name
|
||||
|
||||
def test_iterate_backups_incomplete_backup(self, backup_dir):
|
||||
"""Test _iterate_backups skips backup without marker"""
|
||||
backup_dir.mkdir()
|
||||
# Create complete backup
|
||||
complete = backup_dir / "20210101_120000"
|
||||
complete.mkdir()
|
||||
(complete / "file.txt").write_text("content")
|
||||
marker_name = f"{bk.BACKUP_MARKER}_20210101_120000"
|
||||
(complete / marker_name).touch()
|
||||
|
||||
# Create incomplete backup (no marker)
|
||||
incomplete = backup_dir / "20210102_120000"
|
||||
incomplete.mkdir()
|
||||
(incomplete / "file.txt").write_text("content")
|
||||
|
||||
backups = list(bk._iterate_backups(str(backup_dir)))
|
||||
assert len(backups) == 1
|
||||
assert backups[0].name == "20210101_120000"
|
||||
|
||||
def test_get_latest_backup_returns_most_recent(self, backup_dir,
|
||||
add_backup):
|
||||
"""Test _get_latest_backup returns most recent backup"""
|
||||
add_backup("20210101_120000")
|
||||
add_backup("20210102_120000")
|
||||
latest = add_backup("20210103_120000")
|
||||
|
||||
result = bk._get_latest_backup(str(backup_dir))
|
||||
assert result is not None
|
||||
assert result.name == latest.name
|
||||
|
||||
def test_get_latest_backup_empty_directory(self, backup_dir):
|
||||
"""Test _get_latest_backup returns None for empty directory"""
|
||||
backup_dir.mkdir()
|
||||
result = bk._get_latest_backup(str(backup_dir))
|
||||
assert result is None
|
||||
|
||||
def test_get_latest_backup_no_valid_backups(self, backup_dir):
|
||||
"""Test _get_latest_backup returns None with no valid backups"""
|
||||
backup_dir.mkdir()
|
||||
# Create incomplete backup
|
||||
incomplete = backup_dir / "20210101_120000"
|
||||
incomplete.mkdir()
|
||||
(incomplete / "file.txt").write_text("content")
|
||||
# no marker
|
||||
|
||||
result = bk._get_latest_backup(str(backup_dir))
|
||||
assert result is None
|
||||
|
||||
def test_set_backup_marker_creates_marker(self, backup_dir):
|
||||
"""Test set_backup_marker creates marker file"""
|
||||
backup_dir.mkdir()
|
||||
backup_path = backup_dir / "20210101_120000"
|
||||
backup_path.mkdir()
|
||||
|
||||
backup_entry = bk.fs.PseudoDirEntry(str(backup_path))
|
||||
bk.set_backup_marker(backup_entry)
|
||||
|
||||
marker_name = f"{bk.BACKUP_MARKER}_20210101_120000"
|
||||
marker_path = backup_path / marker_name
|
||||
assert marker_path.exists()
|
||||
|
||||
def test_set_backup_marker_idempotent(self, backup_dir):
|
||||
"""Test set_backup_marker is idempotent"""
|
||||
backup_dir.mkdir()
|
||||
backup_path = backup_dir / "20210101_120000"
|
||||
backup_path.mkdir()
|
||||
|
||||
backup_entry = bk.fs.PseudoDirEntry(str(backup_path))
|
||||
bk.set_backup_marker(backup_entry)
|
||||
# Call again - should not fail
|
||||
bk.set_backup_marker(backup_entry)
|
||||
|
||||
marker_name = f"{bk.BACKUP_MARKER}_20210101_120000"
|
||||
marker_path = backup_path / marker_name
|
||||
assert marker_path.exists()
|
||||
|
||||
Reference in New Issue
Block a user