diff --git a/NEWS b/NEWS index e4578faf..d9943f76 100644 --- a/NEWS +++ b/NEWS @@ -3,6 +3,8 @@ contain multiple paths. * #1168: Fix for the "list", "info", and "delete" options in "extra_borg_options" being ignored when "--archive" is omitted with Borg 1.x. + * #1169: Fix for a regression in the ZFS, LVM, and Btrfs hooks in which partial excludes of + snapshot paths were ignored. * Add a "rename" option to "extra_borg_options" to support passing arbitrary flags to "borg rename". diff --git a/borgmatic/hooks/data_source/btrfs.py b/borgmatic/hooks/data_source/btrfs.py index 95b0cc73..0fd758ae 100644 --- a/borgmatic/hooks/data_source/btrfs.py +++ b/borgmatic/hooks/data_source/btrfs.py @@ -329,9 +329,15 @@ def dump_data_sources( snapshot_subvolume(btrfs_command, subvolume.path, snapshot_path) + last_contained_pattern_index = borgmatic.hooks.data_source.config.get_last_pattern_index( + patterns, subvolume.contained_patterns + ) + for pattern in subvolume.contained_patterns: snapshot_pattern = make_borg_snapshot_pattern(subvolume.path, pattern) - borgmatic.hooks.data_source.config.replace_pattern(patterns, pattern, snapshot_pattern) + borgmatic.hooks.data_source.config.replace_pattern( + patterns, pattern, snapshot_pattern, last_contained_pattern_index + ) borgmatic.hooks.data_source.config.inject_pattern( patterns, make_snapshot_exclude_pattern(subvolume.path) diff --git a/borgmatic/hooks/data_source/config.py b/borgmatic/hooks/data_source/config.py index 35bce138..bd261d9b 100644 --- a/borgmatic/hooks/data_source/config.py +++ b/borgmatic/hooks/data_source/config.py @@ -1,3 +1,4 @@ +import contextlib import json import logging import shutil @@ -131,7 +132,23 @@ def inject_pattern(patterns, data_source_pattern): patterns.insert(0, data_source_pattern) -def replace_pattern(patterns, pattern_to_replace, data_source_pattern): +def get_last_pattern_index(patterns, patterns_subset): + ''' + Given a sequence of all patterns and a subset of those patterns, find each subset pattern in the + all patterns sequence and return the highest (last) index. + ''' + last_pattern_index = 0 + + for pattern in patterns_subset: + with contextlib.suppress(ValueError): + last_pattern_index = max(patterns.index(pattern), last_pattern_index) + + return last_pattern_index + + +def replace_pattern( + patterns, pattern_to_replace, data_source_pattern, last_contained_pattern_index +): ''' Given a list of borgmatic.borg.pattern.Pattern instances representing the configured patterns, replace the given pattern with the given data source pattern. The idea is that borgmatic is @@ -139,8 +156,32 @@ def replace_pattern(patterns, pattern_to_replace, data_source_pattern): that the hook's data gets included in the backup. As part of this replacement, if the data source pattern is a root pattern, also insert an - "include" version of the given root pattern right after the replaced pattern, in an attempt to - preempt any of the user's configured exclude patterns that may follow. + "include" version of the given root pattern right after the given last contained pattern index + in an attempt to preempt any of the user's configured global exclude patterns that may follow. + But we don't want to preempt any intentional partial excludes of the data source pattern itself, + which is why the include goes after the last contained pattern index. + + For instance, let's say that the patterns are effectively: + + R /foo + R /bar + - /bar/.cache + R /baz + - ** + + ... and "R /bar" is the pattern to replace, data source pattern is "R /bar/snapshot", and the + last contained pattern index is 2 (corresponding to "- /bar/.cache"). The resulting patterns + after calling this function would be: + + R /foo + R /bar/snapshot + - /bar/snapshot/.cache + + /bar/snapshot + R /baz + - ** + + Note that the positioning of "+ /bar/snapshot" means that it overrides the "- **" global exclude + but not the "- /bar/snapshot/.cache" contained pattern exclude. If the pattern to replace can't be found in the given patterns, then just inject the data source pattern at the start of the list. @@ -156,7 +197,7 @@ def replace_pattern(patterns, pattern_to_replace, data_source_pattern): if data_source_pattern.type == borgmatic.borg.pattern.Pattern_type.ROOT: patterns.insert( - index + 1, + last_contained_pattern_index + 1, borgmatic.borg.pattern.Pattern( path=data_source_pattern.path, type=borgmatic.borg.pattern.Pattern_type.INCLUDE, diff --git a/borgmatic/hooks/data_source/lvm.py b/borgmatic/hooks/data_source/lvm.py index 3cdc2cdc..07e9d090 100644 --- a/borgmatic/hooks/data_source/lvm.py +++ b/borgmatic/hooks/data_source/lvm.py @@ -269,6 +269,10 @@ def dump_data_sources( snapshot_mount_path, ) + last_contained_pattern_index = borgmatic.hooks.data_source.config.get_last_pattern_index( + patterns, logical_volume.contained_patterns + ) + for pattern in logical_volume.contained_patterns: snapshot_pattern = make_borg_snapshot_pattern( pattern, @@ -276,7 +280,9 @@ def dump_data_sources( normalized_runtime_directory, ) - borgmatic.hooks.data_source.config.replace_pattern(patterns, pattern, snapshot_pattern) + borgmatic.hooks.data_source.config.replace_pattern( + patterns, pattern, snapshot_pattern, last_contained_pattern_index + ) return [] diff --git a/borgmatic/hooks/data_source/zfs.py b/borgmatic/hooks/data_source/zfs.py index 77b3ac9d..eeec02a0 100644 --- a/borgmatic/hooks/data_source/zfs.py +++ b/borgmatic/hooks/data_source/zfs.py @@ -300,6 +300,10 @@ def dump_data_sources( snapshot_mount_path, ) + last_contained_pattern_index = borgmatic.hooks.data_source.config.get_last_pattern_index( + patterns, dataset.contained_patterns + ) + for pattern in dataset.contained_patterns: snapshot_pattern = make_borg_snapshot_pattern( pattern, @@ -307,7 +311,9 @@ def dump_data_sources( normalized_runtime_directory, ) - borgmatic.hooks.data_source.config.replace_pattern(patterns, pattern, snapshot_pattern) + borgmatic.hooks.data_source.config.replace_pattern( + patterns, pattern, snapshot_pattern, last_contained_pattern_index + ) return [] diff --git a/tests/integration/hooks/data_source/test_btrfs.py b/tests/integration/hooks/data_source/test_btrfs.py index f225622b..1c6bc4e5 100644 --- a/tests/integration/hooks/data_source/test_btrfs.py +++ b/tests/integration/hooks/data_source/test_btrfs.py @@ -5,11 +5,22 @@ from borgmatic.hooks.data_source import btrfs as module def test_dump_data_sources_snapshots_each_subvolume_and_updates_patterns(): - patterns = [Pattern('/foo'), Pattern('/mnt/subvol1'), Pattern('/mnt/subvol2')] + patterns = [ + Pattern('/foo'), + Pattern('/mnt/subvol1'), + Pattern('/mnt/subvol1/.cache', Pattern_type.EXCLUDE), + Pattern('/mnt/subvol2'), + ] config = {'btrfs': {}} flexmock(module).should_receive('get_subvolumes').and_return( ( - module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)), + module.Subvolume( + '/mnt/subvol1', + contained_patterns=( + Pattern('/mnt/subvol1'), + Pattern('/mnt/subvol1/.cache', Pattern_type.EXCLUDE), + ), + ), module.Subvolume('/mnt/subvol2', contained_patterns=(Pattern('/mnt/subvol2'),)), ), ) @@ -50,6 +61,7 @@ def test_dump_data_sources_snapshots_each_subvolume_and_updates_patterns(): ), Pattern('/foo'), Pattern('/mnt/subvol1/.borgmatic-snapshot-1234/./mnt/subvol1'), + Pattern('/mnt/subvol1/.borgmatic-snapshot-1234/./mnt/subvol1/.cache', Pattern_type.EXCLUDE), Pattern('/mnt/subvol1/.borgmatic-snapshot-1234/./mnt/subvol1', Pattern_type.INCLUDE), Pattern('/mnt/subvol2/.borgmatic-snapshot-1234/./mnt/subvol2'), Pattern('/mnt/subvol2/.borgmatic-snapshot-1234/./mnt/subvol2', Pattern_type.INCLUDE), diff --git a/tests/integration/hooks/data_source/test_lvm.py b/tests/integration/hooks/data_source/test_lvm.py index fb525dae..37231d88 100644 --- a/tests/integration/hooks/data_source/test_lvm.py +++ b/tests/integration/hooks/data_source/test_lvm.py @@ -6,13 +6,20 @@ from borgmatic.hooks.data_source import lvm as module def test_dump_data_sources_snapshots_and_mounts_and_updates_patterns(): config = {'lvm': {}} - patterns = [Pattern('/mnt/lvolume1/subdir'), Pattern('/mnt/lvolume2')] + patterns = [ + Pattern('/mnt/lvolume1/subdir'), + Pattern('/mnt/lvolume1/subdir/.cache', Pattern_type.EXCLUDE), + Pattern('/mnt/lvolume2'), + ] logical_volumes = ( module.Logical_volume( name='lvolume1', device_path='/dev/lvolume1', mount_point='/mnt/lvolume1', - contained_patterns=(Pattern('/mnt/lvolume1/subdir'),), + contained_patterns=( + Pattern('/mnt/lvolume1/subdir'), + Pattern('/mnt/lvolume1/subdir/.cache', Pattern_type.EXCLUDE), + ), ), module.Logical_volume( name='lvolume2', @@ -75,6 +82,9 @@ def test_dump_data_sources_snapshots_and_mounts_and_updates_patterns(): assert patterns == [ Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume1/subdir'), + Pattern( + '/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume1/subdir/.cache', Pattern_type.EXCLUDE + ), Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume1/subdir', Pattern_type.INCLUDE), Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2'), Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2', Pattern_type.INCLUDE), diff --git a/tests/unit/hooks/data_source/test_btrfs.py b/tests/unit/hooks/data_source/test_btrfs.py index cc426403..25a1b1e4 100644 --- a/tests/unit/hooks/data_source/test_btrfs.py +++ b/tests/unit/hooks/data_source/test_btrfs.py @@ -450,6 +450,9 @@ def test_dump_data_sources_snapshots_each_subvolume_and_replaces_patterns(): '/mnt/subvol2', object, ).and_return(Pattern('/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2')) + flexmock(module.borgmatic.hooks.data_source.config).should_receive( + 'get_last_pattern_index' + ).and_return(0) flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args( object, Pattern('/mnt/subvol1'), @@ -457,6 +460,7 @@ def test_dump_data_sources_snapshots_each_subvolume_and_replaces_patterns(): '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1', source=module.borgmatic.borg.pattern.Pattern_source.HOOK, ), + 0, ).once() flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args( object, @@ -465,6 +469,7 @@ def test_dump_data_sources_snapshots_each_subvolume_and_replaces_patterns(): '/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2', source=module.borgmatic.borg.pattern.Pattern_source.HOOK, ), + 0, ).once() flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args( object, @@ -527,6 +532,9 @@ def test_dump_data_sources_uses_custom_btrfs_command_in_commands(): '/mnt/subvol1', object, ).and_return(Pattern('/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1')) + flexmock(module.borgmatic.hooks.data_source.config).should_receive( + 'get_last_pattern_index' + ).and_return(0) flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args( object, Pattern('/mnt/subvol1'), @@ -534,6 +542,7 @@ def test_dump_data_sources_uses_custom_btrfs_command_in_commands(): '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1', source=module.borgmatic.borg.pattern.Pattern_source.HOOK, ), + 0, ).once() flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args( object, @@ -594,6 +603,9 @@ def test_dump_data_sources_with_findmnt_command_warns(): '/mnt/subvol1', object, ).and_return(Pattern('/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1')) + flexmock(module.borgmatic.hooks.data_source.config).should_receive( + 'get_last_pattern_index' + ).and_return(0) flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args( object, Pattern('/mnt/subvol1'), @@ -601,6 +613,7 @@ def test_dump_data_sources_with_findmnt_command_warns(): '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1', source=module.borgmatic.borg.pattern.Pattern_source.HOOK, ), + 0, ).once() flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args( object, @@ -641,6 +654,9 @@ def test_dump_data_sources_with_dry_run_skips_snapshot_and_patterns_update(): ) flexmock(module).should_receive('snapshot_subvolume').never() flexmock(module).should_receive('make_snapshot_exclude_pattern').never() + flexmock(module.borgmatic.hooks.data_source.config).should_receive( + 'get_last_pattern_index' + ).never() flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').never() flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').never() @@ -666,6 +682,9 @@ def test_dump_data_sources_without_matching_subvolumes_skips_snapshot_and_patter flexmock(module).should_receive('make_snapshot_path').never() flexmock(module).should_receive('snapshot_subvolume').never() flexmock(module).should_receive('make_snapshot_exclude_pattern').never() + flexmock(module.borgmatic.hooks.data_source.config).should_receive( + 'get_last_pattern_index' + ).never() flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').never() flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').never() diff --git a/tests/unit/hooks/data_source/test_config.py b/tests/unit/hooks/data_source/test_config.py index 00103a87..a183449c 100644 --- a/tests/unit/hooks/data_source/test_config.py +++ b/tests/unit/hooks/data_source/test_config.py @@ -203,6 +203,52 @@ def test_inject_pattern_with_root_pattern_prepends_it_along_with_corresponding_i ] +def test_get_last_pattern_index_with_ordered_subset_patterns_finds_last_one(): + patterns = [ + module.borgmatic.borg.pattern.Pattern('/foo'), + module.borgmatic.borg.pattern.Pattern('/bar'), + module.borgmatic.borg.pattern.Pattern('/baz'), + module.borgmatic.borg.pattern.Pattern('/quux'), + ] + patterns_subset = [ + module.borgmatic.borg.pattern.Pattern('/bar'), + module.borgmatic.borg.pattern.Pattern('/baz'), + ] + + assert module.get_last_pattern_index(patterns, patterns_subset) == 2 + + +def test_get_last_pattern_index_with_unordered_subset_patterns_finds_last_one(): + patterns = [ + module.borgmatic.borg.pattern.Pattern('/foo'), + module.borgmatic.borg.pattern.Pattern('/bar'), + module.borgmatic.borg.pattern.Pattern('/baz'), + module.borgmatic.borg.pattern.Pattern('/quux'), + ] + patterns_subset = [ + module.borgmatic.borg.pattern.Pattern('/baz'), + module.borgmatic.borg.pattern.Pattern('/bar'), + ] + + assert module.get_last_pattern_index(patterns, patterns_subset) == 2 + + +def test_get_last_pattern_index_with_unknown_subset_patterns_skips_it(): + patterns = [ + module.borgmatic.borg.pattern.Pattern('/foo'), + module.borgmatic.borg.pattern.Pattern('/bar'), + module.borgmatic.borg.pattern.Pattern('/baz'), + module.borgmatic.borg.pattern.Pattern('/quux'), + ] + patterns_subset = [ + module.borgmatic.borg.pattern.Pattern('/baz'), + module.borgmatic.borg.pattern.Pattern('/unknown'), + module.borgmatic.borg.pattern.Pattern('/bar'), + ] + + assert module.get_last_pattern_index(patterns, patterns_subset) == 2 + + def test_replace_pattern_swaps_out_pattern_in_place(): patterns = [ module.borgmatic.borg.pattern.Pattern('/etc'), @@ -217,6 +263,7 @@ def test_replace_pattern_swaps_out_pattern_in_place(): '/foo/bar', type=module.borgmatic.borg.pattern.Pattern_type.EXCLUDE, ), + 0, ) assert patterns == [ @@ -243,6 +290,7 @@ def test_replace_pattern_with_unknown_pattern_falls_back_to_injecting(): patterns, module.borgmatic.borg.pattern.Pattern('/unknown'), module.borgmatic.borg.pattern.Pattern('/foo/bar'), + 0, ) @@ -251,20 +299,23 @@ def test_replace_pattern_with_root_pattern_swaps_it_in_along_with_corresponding_ module.borgmatic.borg.pattern.Pattern('/etc'), module.borgmatic.borg.pattern.Pattern('/var'), module.borgmatic.borg.pattern.Pattern('/lib'), + module.borgmatic.borg.pattern.Pattern('/run'), ] module.replace_pattern( patterns, module.borgmatic.borg.pattern.Pattern('/var'), module.borgmatic.borg.pattern.Pattern('/foo/bar'), + 2, ) assert patterns == [ module.borgmatic.borg.pattern.Pattern('/etc'), module.borgmatic.borg.pattern.Pattern('/foo/bar'), + module.borgmatic.borg.pattern.Pattern('/lib'), module.borgmatic.borg.pattern.Pattern( '/foo/bar', type=module.borgmatic.borg.pattern.Pattern_type.INCLUDE, ), - module.borgmatic.borg.pattern.Pattern('/lib'), + module.borgmatic.borg.pattern.Pattern('/run'), ] diff --git a/tests/unit/hooks/data_source/test_lvm.py b/tests/unit/hooks/data_source/test_lvm.py index 3c2615bc..8091dca4 100644 --- a/tests/unit/hooks/data_source/test_lvm.py +++ b/tests/unit/hooks/data_source/test_lvm.py @@ -360,6 +360,9 @@ def test_dump_data_sources_snapshots_and_mounts_and_replaces_patterns(): logical_volumes[1], '/run/borgmatic', ).and_return(Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2')) + flexmock(module.borgmatic.hooks.data_source.config).should_receive( + 'get_last_pattern_index' + ).and_return(0) flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args( object, Pattern('/mnt/lvolume1/subdir'), @@ -367,6 +370,7 @@ def test_dump_data_sources_snapshots_and_mounts_and_replaces_patterns(): '/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume1/subdir', source=module.borgmatic.borg.pattern.Pattern_source.HOOK, ), + 0, ).once() flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args( object, @@ -375,6 +379,7 @@ def test_dump_data_sources_snapshots_and_mounts_and_replaces_patterns(): '/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2', source=module.borgmatic.borg.pattern.Pattern_source.HOOK, ), + 0, ).once() assert ( @@ -396,6 +401,9 @@ def test_dump_data_sources_with_no_logical_volumes_skips_snapshots(): flexmock(module).should_receive('get_logical_volumes').and_return(()) flexmock(module).should_receive('snapshot_logical_volume').never() flexmock(module).should_receive('mount_snapshot').never() + flexmock(module.borgmatic.hooks.data_source.config).should_receive( + 'get_last_pattern_index' + ).and_return(0) flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').never() assert ( @@ -477,6 +485,9 @@ def test_dump_data_sources_uses_snapshot_size_for_snapshot(): logical_volumes[1], '/run/borgmatic', ).and_return(Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2')) + flexmock(module.borgmatic.hooks.data_source.config).should_receive( + 'get_last_pattern_index' + ).and_return(0) flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args( object, Pattern('/mnt/lvolume1/subdir'), @@ -484,6 +495,7 @@ def test_dump_data_sources_uses_snapshot_size_for_snapshot(): '/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume1/subdir', source=module.borgmatic.borg.pattern.Pattern_source.HOOK, ), + 0, ).once() flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args( object, @@ -492,6 +504,7 @@ def test_dump_data_sources_uses_snapshot_size_for_snapshot(): '/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2', source=module.borgmatic.borg.pattern.Pattern_source.HOOK, ), + 0, ).once() assert ( @@ -580,6 +593,9 @@ def test_dump_data_sources_uses_custom_commands(): logical_volumes[1], '/run/borgmatic', ).and_return(Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2')) + flexmock(module.borgmatic.hooks.data_source.config).should_receive( + 'get_last_pattern_index' + ).and_return(0) flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args( object, Pattern('/mnt/lvolume1/subdir'), @@ -587,6 +603,7 @@ def test_dump_data_sources_uses_custom_commands(): '/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume1/subdir', source=module.borgmatic.borg.pattern.Pattern_source.HOOK, ), + 0, ).once() flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args( object, @@ -595,6 +612,7 @@ def test_dump_data_sources_uses_custom_commands(): '/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2', source=module.borgmatic.borg.pattern.Pattern_source.HOOK, ), + 0, ).once() assert ( @@ -633,6 +651,9 @@ def test_dump_data_sources_with_dry_run_skips_snapshots_and_does_not_touch_patte flexmock(module).should_receive('snapshot_logical_volume').never() flexmock(module).should_receive('get_snapshots').never() flexmock(module).should_receive('mount_snapshot').never() + flexmock(module.borgmatic.hooks.data_source.config).should_receive( + 'get_last_pattern_index' + ).never() flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').never() assert ( @@ -714,6 +735,9 @@ def test_dump_data_sources_ignores_mismatch_between_given_patterns_and_contained logical_volumes[1], '/run/borgmatic', ).and_return(Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2')) + flexmock(module.borgmatic.hooks.data_source.config).should_receive( + 'get_last_pattern_index' + ).and_return(0) flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args( object, Pattern('/mnt/lvolume1/subdir'), @@ -721,6 +745,7 @@ def test_dump_data_sources_ignores_mismatch_between_given_patterns_and_contained '/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume1/subdir', source=module.borgmatic.borg.pattern.Pattern_source.HOOK, ), + 0, ).once() flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args( object, @@ -729,6 +754,7 @@ def test_dump_data_sources_ignores_mismatch_between_given_patterns_and_contained '/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2', source=module.borgmatic.borg.pattern.Pattern_source.HOOK, ), + 0, ).once() assert ( @@ -785,6 +811,9 @@ def test_dump_data_sources_with_missing_snapshot_errors(): snapshot_name='lvolume2_borgmatic-1234', ).never() flexmock(module).should_receive('mount_snapshot').never() + flexmock(module.borgmatic.hooks.data_source.config).should_receive( + 'get_last_pattern_index' + ).never() flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').never() with pytest.raises(ValueError): diff --git a/tests/unit/hooks/data_source/test_zfs.py b/tests/unit/hooks/data_source/test_zfs.py index cdb39d94..fbefca7e 100644 --- a/tests/unit/hooks/data_source/test_zfs.py +++ b/tests/unit/hooks/data_source/test_zfs.py @@ -328,6 +328,9 @@ def test_dump_data_sources_snapshots_and_mounts_and_replaces_patterns(): '/run/borgmatic', ).and_return(Pattern('/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset/subdir')) patterns = [Pattern('/mnt/dataset/subdir')] + flexmock(module.borgmatic.hooks.data_source.config).should_receive( + 'get_last_pattern_index' + ).and_return(0) flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args( object, Pattern('/mnt/dataset/subdir'), @@ -335,6 +338,7 @@ def test_dump_data_sources_snapshots_and_mounts_and_replaces_patterns(): '/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset/subdir', source=module.borgmatic.borg.pattern.Pattern_source.HOOK, ), + 0, ).once() assert ( @@ -355,6 +359,9 @@ def test_dump_data_sources_with_no_datasets_skips_snapshots(): flexmock(module.os).should_receive('getpid').and_return(1234) flexmock(module).should_receive('snapshot_dataset').never() flexmock(module).should_receive('mount_snapshot').never() + flexmock(module.borgmatic.hooks.data_source.config).should_receive( + 'get_last_pattern_index' + ).never() flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').never() patterns = [Pattern('/mnt/dataset')] @@ -400,6 +407,9 @@ def test_dump_data_sources_uses_custom_commands(): dataset, '/run/borgmatic', ).and_return(Pattern('/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset/subdir')) + flexmock(module.borgmatic.hooks.data_source.config).should_receive( + 'get_last_pattern_index' + ).and_return(0) flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args( object, Pattern('/mnt/dataset/subdir'), @@ -407,6 +417,7 @@ def test_dump_data_sources_uses_custom_commands(): '/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset/subdir', source=module.borgmatic.borg.pattern.Pattern_source.HOOK, ), + 0, ).once() patterns = [Pattern('/mnt/dataset/subdir')] hook_config = { @@ -437,6 +448,9 @@ def test_dump_data_sources_with_dry_run_skips_commands_and_does_not_touch_patter flexmock(module.os).should_receive('getpid').and_return(1234) flexmock(module).should_receive('snapshot_dataset').never() flexmock(module).should_receive('mount_snapshot').never() + flexmock(module.borgmatic.hooks.data_source.config).should_receive( + 'get_last_pattern_index' + ).and_return(0) flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').never() patterns = [Pattern('/mnt/dataset')] @@ -480,6 +494,9 @@ def test_dump_data_sources_ignores_mismatch_between_given_patterns_and_contained dataset, '/run/borgmatic', ).and_return(Pattern('/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset/subdir')) + flexmock(module.borgmatic.hooks.data_source.config).should_receive( + 'get_last_pattern_index' + ).and_return(0) flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args( object, Pattern('/mnt/dataset/subdir'), @@ -487,6 +504,7 @@ def test_dump_data_sources_ignores_mismatch_between_given_patterns_and_contained '/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset/subdir', source=module.borgmatic.borg.pattern.Pattern_source.HOOK, ), + 0, ).once() patterns = [Pattern('/hmm')]