import pytest from unittest import mock from zfs_backup import ZFSPath, Dataset, Snapshot 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 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 """