Update tests

This commit is contained in:
2026-02-04 19:31:29 -08:00
parent 5dc9235992
commit ee1df4cb21
5 changed files with 1386 additions and 1126 deletions

View File

@@ -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()