309 lines
10 KiB
Python
309 lines
10 KiB
Python
import pytest
|
|
|
|
from unittest import mock
|
|
|
|
from zfs_backup.snapshot import Snapshot
|
|
from zfs_backup.dataset import Dataset
|
|
from zfs_backup.zfs_path import ZFSPath
|
|
|
|
|
|
class TestZFSPath:
|
|
@pytest.mark.parametrize(
|
|
"elements",
|
|
[
|
|
["pool"],
|
|
["pool", "dataset1", "dataset2"],
|
|
],
|
|
)
|
|
def test__sanity_check_valid(self, elements):
|
|
ZFSPath._sanity_check(elements)
|
|
|
|
@pytest.mark.parametrize(
|
|
"elements",
|
|
[
|
|
[""],
|
|
["", "dataset1"],
|
|
["pool", "dataset1", ""],
|
|
["pool", "", "dataset2"],
|
|
],
|
|
)
|
|
def test__sanity_check_invalid(self, elements):
|
|
with pytest.raises(ValueError):
|
|
ZFSPath._sanity_check(elements)
|
|
|
|
@pytest.mark.parametrize(
|
|
"elements, snapshot_name, result",
|
|
[
|
|
[["pool"], None, ""],
|
|
[["pool", "dataset"], None, "dataset"],
|
|
[["pool", "dataset"], "snapshot", "dataset@snapshot"],
|
|
[
|
|
["pool", "dataset1", "dataset2"],
|
|
"snapshot",
|
|
"dataset1/dataset2@snapshot",
|
|
],
|
|
],
|
|
ids=[
|
|
"only pool",
|
|
"pool with one dataset",
|
|
"pool with dataset and snapshot",
|
|
"pool with two nested datasets and snapshot",
|
|
],
|
|
)
|
|
def test_name_without_pool(self, elements, snapshot_name, result):
|
|
instance = ZFSPath(elements, snapshot_name)
|
|
assert instance.name_without_pool == result
|
|
|
|
@pytest.mark.parametrize(
|
|
"elements, snapshot_name, result",
|
|
[
|
|
[["pool"], None, "pool"],
|
|
[["pool", "dataset"], None, "pool/dataset"],
|
|
[["pool", "dataset"], "snapshot", "pool/dataset@snapshot"],
|
|
[
|
|
["pool", "dataset1", "dataset2"],
|
|
"snapshot",
|
|
"pool/dataset1/dataset2@snapshot",
|
|
],
|
|
],
|
|
ids=[
|
|
"only pool",
|
|
"pool with one dataset",
|
|
"pool with dataset and snapshot",
|
|
"pool with two nested datasets and snapshot",
|
|
],
|
|
)
|
|
def test___repr__(self, elements, snapshot_name, result):
|
|
assert str(ZFSPath(elements, snapshot_name)) == result
|
|
|
|
@pytest.mark.parametrize(
|
|
"instance1, instance2, result",
|
|
[
|
|
[ZFSPath(["pool"]), ZFSPath(["pool"]), True],
|
|
[ZFSPath(["pool"]), ZFSPath(["tank"]), False],
|
|
[ZFSPath(["pool", "dataset"]), ZFSPath(["pool", "dataset"]), True],
|
|
[ZFSPath(["pool", "dataset"]), ZFSPath(["pool", "bla"]), False],
|
|
[
|
|
ZFSPath(["pool", "dataset"], "snapshot"),
|
|
ZFSPath(["pool", "dataset"], "snapshot"),
|
|
True,
|
|
],
|
|
[
|
|
ZFSPath(["pool", "dataset"], "snapshot"),
|
|
ZFSPath(["pool", "dataset"], "bla"),
|
|
False,
|
|
],
|
|
],
|
|
ids=[
|
|
"only pool equal",
|
|
"only pool not equal",
|
|
"dataset equal",
|
|
"dataset not equal",
|
|
"snapshot equal",
|
|
"snapshot not equal",
|
|
],
|
|
)
|
|
def test___eq__(self, instance1, instance2, result):
|
|
assert (instance1 == instance2) == result
|
|
|
|
@pytest.mark.parametrize(
|
|
"string, elements, snapshot_name",
|
|
[
|
|
["pool/dataset", ["pool", "dataset"], None],
|
|
["pool/dataset@snapshot", ["pool", "dataset"], "snapshot"],
|
|
[
|
|
"pool/dataset1/dataset2@snapshot",
|
|
["pool", "dataset1", "dataset2"],
|
|
"snapshot",
|
|
],
|
|
],
|
|
ids=[
|
|
"pool/dataset",
|
|
"pool/dataset@snapshot",
|
|
"pool/dataset1/dataset2@snapshot",
|
|
],
|
|
)
|
|
def test_from_string(self, string, elements, snapshot_name):
|
|
instance = ZFSPath.from_string(string)
|
|
assert instance._elements == elements
|
|
assert instance._snapshot_name == instance.snapshot_name == snapshot_name
|
|
|
|
@pytest.mark.parametrize(
|
|
"instance1, instance2",
|
|
[
|
|
[ZFSPath(["pool", "dataset"]), ZFSPath(["pool", "dataset"])],
|
|
[ZFSPath(["pool", "dataset"], "snapshot"), ZFSPath(["pool", "dataset"])],
|
|
],
|
|
ids=[
|
|
"pool with dataset",
|
|
"pool with dataset and snapshot",
|
|
],
|
|
)
|
|
def test_dataset(self, instance1, instance2):
|
|
assert instance1.dataset == instance2
|
|
|
|
@pytest.mark.parametrize(
|
|
"instance1, instance2",
|
|
[
|
|
[ZFSPath(["pool"]), ZFSPath(["tank"])],
|
|
[ZFSPath(["pool", "dataset"]), ZFSPath(["tank", "dataset"])],
|
|
[
|
|
ZFSPath(["pool", "dataset"], "snapshot"),
|
|
ZFSPath(["tank", "dataset"], "snapshot"),
|
|
],
|
|
],
|
|
ids=[
|
|
"pool",
|
|
"pool with dataset",
|
|
"pool with dataset and snapshot",
|
|
],
|
|
)
|
|
def test_replace_pool(self, instance1, instance2):
|
|
assert instance1.replace_pool(instance2.pool_name) == instance2
|
|
|
|
|
|
class TestZFS:
|
|
@pytest.mark.parametrize("string", ["tank", "tank/ROOT", "tank/ROOT/default"])
|
|
def test_get_snapshots(self, string):
|
|
path = ZFSPath.from_string(string)
|
|
ds = Dataset(path)
|
|
for snapshot in ds.snapshots:
|
|
assert snapshot.dataset == ds
|
|
|
|
|
|
class TestDataset:
|
|
@pytest.mark.parametrize("zfspath", ["tank", "tank/ROOT", "tank/ROOT/default"])
|
|
def test__clean_old_snapshots_for_interval(self, zfspath):
|
|
"""Check that only snapshots that are direct children of the dataset are found during cleanup."""
|
|
|
|
def validate_snapshot_selection(snapshot: Snapshot):
|
|
print(snapshot) # show output with "pytest -s"
|
|
str(snapshot).startswith(f"{zfspath}@")
|
|
|
|
dataset = Dataset(ZFSPath.from_string(zfspath))
|
|
with mock.patch.object(
|
|
Snapshot, "destroy", lambda snapshot: validate_snapshot_selection(snapshot)
|
|
):
|
|
dataset._clean_old_snapshots_for_interval("daily", 999)
|
|
|
|
def test__clean_old_snapshots_for_interval_2(self):
|
|
def validate_snapshot_selection(snapshot: Snapshot):
|
|
nonlocal counter
|
|
print(snapshot)
|
|
str(snapshot).startswith("tank/ROOT@")
|
|
counter = counter + 1
|
|
|
|
snapshots = gen_snapshots_for_tank_ROOT()
|
|
MAX_COUNT = 30
|
|
with mock.patch.object(Dataset, "snapshots", snapshots):
|
|
with mock.patch.object(
|
|
Snapshot,
|
|
"destroy",
|
|
lambda snapshot: validate_snapshot_selection(snapshot),
|
|
):
|
|
ds = Dataset.from_string("tank/ROOT")
|
|
|
|
counter = 0
|
|
ds._clean_old_snapshots_for_interval("monthly", MAX_COUNT)
|
|
assert counter <= MAX_COUNT # not enough snapshots to reach MAX_COUNT
|
|
|
|
counter = 0
|
|
ds._clean_old_snapshots_for_interval("daily", MAX_COUNT)
|
|
assert counter <= MAX_COUNT
|
|
|
|
@pytest.mark.parametrize("max_count", [10, 40])
|
|
def test__get_oldest_snapshots_exceeding_max_count(self, max_count):
|
|
snapshots = [
|
|
snap for snap in gen_snapshots_for_tank_ROOT() if "daily" in str(snap)
|
|
]
|
|
ORIGINAL_COUNT = 31
|
|
assert len(snapshots) == ORIGINAL_COUNT
|
|
expected_result_len = ORIGINAL_COUNT - max_count
|
|
if expected_result_len < 0:
|
|
expected_result_len = 0
|
|
result = Dataset._get_oldest_snapshots_exceeding_max_count(snapshots, max_count)
|
|
assert len(result) == expected_result_len
|
|
|
|
|
|
def gen_snapshots_for_tank_ROOT() -> list[Snapshot]:
|
|
return [
|
|
Snapshot.from_string(line) for line in SNAPSHOTS.split("\n") if line.strip()
|
|
]
|
|
|
|
|
|
SNAPSHOTS = """
|
|
tank/ROOT@znap_2021-11-30-2300_monthly
|
|
tank/ROOT@znap_2021-12-31-2300_monthly
|
|
tank/ROOT@znap_2022-01-31-2300_monthly
|
|
tank/ROOT@znap_2022-02-28-2300_monthly
|
|
tank/ROOT@znap_2022-03-13-2300_weekly
|
|
tank/ROOT@znap_2022-03-20-2300_weekly
|
|
tank/ROOT@znap_2022-03-27-2200_weekly
|
|
tank/ROOT@znap_2022-03-31-2200_monthly
|
|
tank/ROOT@znap_2022-04-03-2200_weekly
|
|
tank/ROOT@znap_2022-04-03-2200_daily
|
|
tank/ROOT@znap_2022-04-04-2200_daily
|
|
tank/ROOT@znap_2022-04-05-2200_daily
|
|
tank/ROOT@znap_2022-04-06-2200_daily
|
|
tank/ROOT@znap_2022-04-07-2200_daily
|
|
tank/ROOT@znap_2022-04-08-2200_daily
|
|
tank/ROOT@znap_2022-04-09-2200_daily
|
|
tank/ROOT@znap_2022-04-10-2200_weekly
|
|
tank/ROOT@znap_2022-04-10-2200_daily
|
|
tank/ROOT@znap_2022-04-11-2200_daily
|
|
tank/ROOT@znap_2022-04-12-2200_daily
|
|
tank/ROOT@znap_2022-04-13-2200_daily
|
|
tank/ROOT@znap_2022-04-14-2200_daily
|
|
tank/ROOT@znap_2022-04-15-2200_daily
|
|
tank/ROOT@znap_2022-04-16-2200_daily
|
|
tank/ROOT@znap_2022-04-17-2200_daily
|
|
tank/ROOT@znap_2022-04-17-2200_weekly
|
|
tank/ROOT@znap_2022-04-18-2200_daily
|
|
tank/ROOT@znap_2022-04-19-2200_daily
|
|
tank/ROOT@znap_2022-04-20-2200_daily
|
|
tank/ROOT@znap_2022-04-21-2200_daily
|
|
tank/ROOT@znap_2022-04-22-2200_daily
|
|
tank/ROOT@znap_2022-04-23-2200_daily
|
|
tank/ROOT@znap_2022-04-24-2200_daily
|
|
tank/ROOT@znap_2022-04-24-2200_weekly
|
|
tank/ROOT@znap_2022-04-25-2200_daily
|
|
tank/ROOT@znap_2022-04-26-2200_daily
|
|
tank/ROOT@znap_2022-04-27-2200_daily
|
|
tank/ROOT@znap_2022-04-28-2200_daily
|
|
tank/ROOT@znap_2022-04-29-2200_daily
|
|
tank/ROOT@znap_2022-04-30-2200_daily
|
|
tank/ROOT@znap_2022-04-30-2200_monthly
|
|
tank/ROOT@znap_2022-05-01-2200_daily
|
|
tank/ROOT@znap_2022-05-01-2200_weekly
|
|
tank/ROOT@znap_2022-05-02-2200_daily
|
|
tank/ROOT@znap_2022-05-03-0900_hourly
|
|
tank/ROOT@znap_2022-05-03-1000_hourly
|
|
tank/ROOT@znap_2022-05-03-1100_hourly
|
|
tank/ROOT@znap_2022-05-03-1200_hourly
|
|
tank/ROOT@znap_2022-05-03-1300_hourly
|
|
tank/ROOT@znap_2022-05-03-1400_hourly
|
|
tank/ROOT@znap_2022-05-03-1500_hourly
|
|
tank/ROOT@znap_2022-05-03-1600_hourly
|
|
tank/ROOT@znap_2022-05-03-1700_hourly
|
|
tank/ROOT@znap_2022-05-03-1800_hourly
|
|
tank/ROOT@znap_2022-05-03-1900_hourly
|
|
tank/ROOT@znap_2022-05-03-2000_hourly
|
|
tank/ROOT@znap_2022-05-03-2100_hourly
|
|
tank/ROOT@znap_2022-05-03-2200_hourly
|
|
tank/ROOT@znap_2022-05-03-2200_daily
|
|
tank/ROOT@znap_2022-05-03-2300_hourly
|
|
tank/ROOT@znap_2022-05-04-0000_hourly
|
|
tank/ROOT@znap_2022-05-04-0100_hourly
|
|
tank/ROOT@znap_2022-05-04-0200_hourly
|
|
tank/ROOT@znap_2022-05-04-0300_hourly
|
|
tank/ROOT@znap_2022-05-04-0400_hourly
|
|
tank/ROOT@znap_2022-05-04-0500_hourly
|
|
tank/ROOT@znap_2022-05-04-0600_hourly
|
|
tank/ROOT@znap_2022-05-04-0700_hourly
|
|
tank/ROOT@znap_2022-05-04-0715_frequent
|
|
tank/ROOT@znap_2022-05-04-0730_frequent
|
|
tank/ROOT@znap_2022-05-04-0745_frequent
|
|
tank/ROOT@znap_2022-05-04-0800_hourly
|
|
tank/ROOT@znap_2022-05-04-0800_frequent
|
|
"""
|