From 360156e3b1555322c5b69ba76110398e65da54a5 Mon Sep 17 00:00:00 2001 From: Vandal <86826719+VandalByte@users.noreply.github.com> Date: Sun, 6 Apr 2025 21:34:04 +0530 Subject: [PATCH] refactor codebase --- borgmatic/actions/check.py | 6 +- borgmatic/actions/create.py | 265 +---------------- borgmatic/actions/pattern.py | 261 +++++++++++++++++ borgmatic/actions/recreate.py | 2 +- borgmatic/borg/create.py | 102 +------ borgmatic/borg/flags.py | 39 +++ borgmatic/borg/pattern.py | 56 ++++ borgmatic/borg/recreate.py | 6 +- borgmatic/config/schema.yaml | 1 + tests/unit/actions/test_check.py | 20 +- tests/unit/actions/test_create.py | 440 +---------------------------- tests/unit/actions/test_pattern.py | 426 ++++++++++++++++++++++++++++ tests/unit/borg/test_create.py | 262 ++++------------- tests/unit/borg/test_flags.py | 97 +++++++ tests/unit/borg/test_pattern.py | 52 ++++ tests/unit/borg/test_recreate.py | 140 ++++----- 16 files changed, 1103 insertions(+), 1072 deletions(-) create mode 100644 borgmatic/actions/pattern.py create mode 100644 tests/unit/actions/test_pattern.py create mode 100644 tests/unit/borg/test_pattern.py diff --git a/borgmatic/actions/check.py b/borgmatic/actions/check.py index 13023ff9..a55f84aa 100644 --- a/borgmatic/actions/check.py +++ b/borgmatic/actions/check.py @@ -8,7 +8,7 @@ import pathlib import random import shutil -import borgmatic.actions.create +import borgmatic.actions.pattern import borgmatic.borg.check import borgmatic.borg.create import borgmatic.borg.environment @@ -373,8 +373,8 @@ def collect_spot_check_source_paths( dry_run=True, repository_path=repository['path'], config=dict(config, list_details=True), - patterns=borgmatic.actions.create.process_patterns( - borgmatic.actions.create.collect_patterns(config), + patterns=borgmatic.actions.pattern.process_patterns( + borgmatic.actions.pattern.collect_patterns(config), working_directory, ), local_borg_version=local_borg_version, diff --git a/borgmatic/actions/create.py b/borgmatic/actions/create.py index a92af261..0c7c4634 100644 --- a/borgmatic/actions/create.py +++ b/borgmatic/actions/create.py @@ -1,272 +1,15 @@ -import glob -import itertools import logging -import os -import pathlib import borgmatic.actions.json import borgmatic.borg.create -import borgmatic.borg.pattern import borgmatic.config.paths import borgmatic.config.validate -import borgmatic.hooks.command import borgmatic.hooks.dispatch +from borgmatic.actions import pattern logger = logging.getLogger(__name__) -def parse_pattern(pattern_line, default_style=borgmatic.borg.pattern.Pattern_style.NONE): - ''' - Given a Borg pattern as a string, parse it into a borgmatic.borg.pattern.Pattern instance and - return it. - ''' - try: - (pattern_type, remainder) = pattern_line.split(' ', maxsplit=1) - except ValueError: - raise ValueError(f'Invalid pattern: {pattern_line}') - - try: - (parsed_pattern_style, path) = remainder.split(':', maxsplit=1) - pattern_style = borgmatic.borg.pattern.Pattern_style(parsed_pattern_style) - except ValueError: - pattern_style = default_style - path = remainder - - return borgmatic.borg.pattern.Pattern( - path, - borgmatic.borg.pattern.Pattern_type(pattern_type), - borgmatic.borg.pattern.Pattern_style(pattern_style), - source=borgmatic.borg.pattern.Pattern_source.CONFIG, - ) - - -def collect_patterns(config): - ''' - Given a configuration dict, produce a single sequence of patterns comprised of the configured - source directories, patterns, excludes, pattern files, and exclude files. - - The idea is that Borg has all these different ways of specifying includes, excludes, source - directories, etc., but we'd like to collapse them all down to one common format (patterns) for - ease of manipulation within borgmatic. - ''' - try: - return ( - tuple( - borgmatic.borg.pattern.Pattern( - source_directory, source=borgmatic.borg.pattern.Pattern_source.CONFIG - ) - for source_directory in config.get('source_directories', ()) - ) - + tuple( - parse_pattern(pattern_line.strip()) - for pattern_line in config.get('patterns', ()) - if not pattern_line.lstrip().startswith('#') - if pattern_line.strip() - ) - + tuple( - parse_pattern( - f'{borgmatic.borg.pattern.Pattern_type.NO_RECURSE.value} {exclude_line.strip()}', - borgmatic.borg.pattern.Pattern_style.FNMATCH, - ) - for exclude_line in config.get('exclude_patterns', ()) - ) - + tuple( - parse_pattern(pattern_line.strip()) - for filename in config.get('patterns_from', ()) - for pattern_line in open(filename).readlines() - if not pattern_line.lstrip().startswith('#') - if pattern_line.strip() - ) - + tuple( - parse_pattern( - f'{borgmatic.borg.pattern.Pattern_type.NO_RECURSE.value} {exclude_line.strip()}', - borgmatic.borg.pattern.Pattern_style.FNMATCH, - ) - for filename in config.get('exclude_from', ()) - for exclude_line in open(filename).readlines() - if not exclude_line.lstrip().startswith('#') - if exclude_line.strip() - ) - ) - except (FileNotFoundError, OSError) as error: - logger.debug(error) - - raise ValueError(f'Cannot read patterns_from/exclude_from file: {error.filename}') - - -def expand_directory(directory, working_directory): - ''' - Given a directory path, expand any tilde (representing a user's home directory) and any globs - therein. Return a list of one or more resulting paths. - - Take into account the given working directory so that relative paths are supported. - ''' - expanded_directory = os.path.expanduser(directory) - - # This would be a lot easier to do with glob(..., root_dir=working_directory), but root_dir is - # only available in Python 3.10+. - normalized_directory = os.path.join(working_directory or '', expanded_directory) - glob_paths = glob.glob(normalized_directory) - - if not glob_paths: - return [expanded_directory] - - working_directory_prefix = os.path.join(working_directory or '', '') - - return [ - ( - glob_path - # If these are equal, that means we didn't add any working directory prefix above. - if normalized_directory == expanded_directory - # Remove the working directory prefix that we added above in order to make glob() work. - # We can't use os.path.relpath() here because it collapses any use of Borg's slashdot - # hack. - else glob_path.removeprefix(working_directory_prefix) - ) - for glob_path in glob_paths - ] - - -def expand_patterns(patterns, working_directory=None, skip_paths=None): - ''' - Given a sequence of borgmatic.borg.pattern.Pattern instances and an optional working directory, - expand tildes and globs in each root pattern and expand just tildes in each non-root pattern. - The idea is that non-root patterns may be regular expressions or other pattern styles containing - "*" that borgmatic should not expand as a shell glob. - - Return all the resulting patterns as a tuple. - - If a set of paths are given to skip, then don't expand any patterns matching them. - ''' - if patterns is None: - return () - - return tuple( - itertools.chain.from_iterable( - ( - ( - borgmatic.borg.pattern.Pattern( - expanded_path, - pattern.type, - pattern.style, - pattern.device, - pattern.source, - ) - for expanded_path in expand_directory(pattern.path, working_directory) - ) - if pattern.type == borgmatic.borg.pattern.Pattern_type.ROOT - and pattern.path not in (skip_paths or ()) - else ( - borgmatic.borg.pattern.Pattern( - os.path.expanduser(pattern.path), - pattern.type, - pattern.style, - pattern.device, - pattern.source, - ), - ) - ) - for pattern in patterns - ) - ) - - -def device_map_patterns(patterns, working_directory=None): - ''' - Given a sequence of borgmatic.borg.pattern.Pattern instances and an optional working directory, - determine the identifier for the device on which the pattern's path resides—or None if the path - doesn't exist or is from a non-root pattern. Return an updated sequence of patterns with the - device field populated. But if the device field is already set, don't bother setting it again. - - This is handy for determining whether two different pattern paths are on the same filesystem - (have the same device identifier). - ''' - return tuple( - borgmatic.borg.pattern.Pattern( - pattern.path, - pattern.type, - pattern.style, - device=pattern.device - or ( - os.stat(full_path).st_dev - if pattern.type == borgmatic.borg.pattern.Pattern_type.ROOT - and os.path.exists(full_path) - else None - ), - source=pattern.source, - ) - for pattern in patterns - for full_path in (os.path.join(working_directory or '', pattern.path),) - ) - - -def deduplicate_patterns(patterns): - ''' - Given a sequence of borgmatic.borg.pattern.Pattern instances, return them with all duplicate - root child patterns removed. For instance, if two root patterns are given with paths "/foo" and - "/foo/bar", return just the one with "/foo". Non-root patterns are passed through without - modification. - - The one exception to deduplication is two paths are on different filesystems (devices). In that - case, they won't get deduplicated, in case they both need to be passed to Borg (e.g. the - one_file_system option is true). - - The idea is that if Borg is given a root parent pattern, then it doesn't also need to be given - child patterns, because it will naturally spider the contents of the parent pattern's path. And - there are cases where Borg coming across the same file twice will result in duplicate reads and - even hangs, e.g. when a database hook is using a named pipe for streaming database dumps to - Borg. - ''' - deduplicated = {} # Use just the keys as an ordered set. - - for pattern in patterns: - if pattern.type != borgmatic.borg.pattern.Pattern_type.ROOT: - deduplicated[pattern] = True - continue - - parents = pathlib.PurePath(pattern.path).parents - - # If another directory in the given list is a parent of current directory (even n levels up) - # and both are on the same filesystem, then the current directory is a duplicate. - for other_pattern in patterns: - if other_pattern.type != borgmatic.borg.pattern.Pattern_type.ROOT: - continue - - if any( - pathlib.PurePath(other_pattern.path) == parent - and pattern.device is not None - and other_pattern.device == pattern.device - for parent in parents - ): - break - else: - deduplicated[pattern] = True - - return tuple(deduplicated.keys()) - - -def process_patterns(patterns, working_directory, skip_expand_paths=None): - ''' - Given a sequence of Borg patterns and a configured working directory, expand and deduplicate any - "root" patterns, returning the resulting root and non-root patterns as a list. - - If any paths are given to skip, don't expand them. - ''' - skip_paths = set(skip_expand_paths or ()) - - return list( - deduplicate_patterns( - device_map_patterns( - expand_patterns( - patterns, - working_directory=working_directory, - skip_paths=skip_paths, - ) - ) - ) - ) - - def run_create( config_filename, repository, @@ -310,7 +53,7 @@ def run_create( borgmatic_runtime_directory, global_arguments.dry_run, ) - patterns = process_patterns(collect_patterns(config), working_directory) + patterns = pattern.process_patterns(pattern.collect_patterns(config), working_directory) active_dumps = borgmatic.hooks.dispatch.call_hooks( 'dump_data_sources', config, @@ -324,7 +67,9 @@ def run_create( # Process the patterns again in case any data source hooks updated them. Without this step, # we could end up with duplicate paths that cause Borg to hang when it tries to read from # the same named pipe twice. - patterns = process_patterns(patterns, working_directory, skip_expand_paths=config_paths) + patterns = pattern.process_patterns( + patterns, working_directory, skip_expand_paths=config_paths + ) stream_processes = [process for processes in active_dumps.values() for process in processes] json_output = borgmatic.borg.create.create_archive( diff --git a/borgmatic/actions/pattern.py b/borgmatic/actions/pattern.py new file mode 100644 index 00000000..b7781424 --- /dev/null +++ b/borgmatic/actions/pattern.py @@ -0,0 +1,261 @@ +import glob +import itertools +import logging +import os +import pathlib + +import borgmatic.borg.pattern + +logger = logging.getLogger(__name__) + + +def parse_pattern(pattern_line, default_style=borgmatic.borg.pattern.Pattern_style.NONE): + ''' + Given a Borg pattern as a string, parse it into a borgmatic.borg.pattern.Pattern instance and + return it. + ''' + try: + (pattern_type, remainder) = pattern_line.split(' ', maxsplit=1) + except ValueError: + raise ValueError(f'Invalid pattern: {pattern_line}') + + try: + (parsed_pattern_style, path) = remainder.split(':', maxsplit=1) + pattern_style = borgmatic.borg.pattern.Pattern_style(parsed_pattern_style) + except ValueError: + pattern_style = default_style + path = remainder + + return borgmatic.borg.pattern.Pattern( + path, + borgmatic.borg.pattern.Pattern_type(pattern_type), + borgmatic.borg.pattern.Pattern_style(pattern_style), + source=borgmatic.borg.pattern.Pattern_source.CONFIG, + ) + + +def collect_patterns(config): + ''' + Given a configuration dict, produce a single sequence of patterns comprised of the configured + source directories, patterns, excludes, pattern files, and exclude files. + + The idea is that Borg has all these different ways of specifying includes, excludes, source + directories, etc., but we'd like to collapse them all down to one common format (patterns) for + ease of manipulation within borgmatic. + ''' + try: + return ( + tuple( + borgmatic.borg.pattern.Pattern( + source_directory, source=borgmatic.borg.pattern.Pattern_source.CONFIG + ) + for source_directory in config.get('source_directories', ()) + ) + + tuple( + parse_pattern(pattern_line.strip()) + for pattern_line in config.get('patterns', ()) + if not pattern_line.lstrip().startswith('#') + if pattern_line.strip() + ) + + tuple( + parse_pattern( + f'{borgmatic.borg.pattern.Pattern_type.NO_RECURSE.value} {exclude_line.strip()}', + borgmatic.borg.pattern.Pattern_style.FNMATCH, + ) + for exclude_line in config.get('exclude_patterns', ()) + ) + + tuple( + parse_pattern(pattern_line.strip()) + for filename in config.get('patterns_from', ()) + for pattern_line in open(filename).readlines() + if not pattern_line.lstrip().startswith('#') + if pattern_line.strip() + ) + + tuple( + parse_pattern( + f'{borgmatic.borg.pattern.Pattern_type.NO_RECURSE.value} {exclude_line.strip()}', + borgmatic.borg.pattern.Pattern_style.FNMATCH, + ) + for filename in config.get('exclude_from', ()) + for exclude_line in open(filename).readlines() + if not exclude_line.lstrip().startswith('#') + if exclude_line.strip() + ) + ) + except (FileNotFoundError, OSError) as error: + logger.debug(error) + + raise ValueError(f'Cannot read patterns_from/exclude_from file: {error.filename}') + + +def expand_directory(directory, working_directory): + ''' + Given a directory path, expand any tilde (representing a user's home directory) and any globs + therein. Return a list of one or more resulting paths. + + Take into account the given working directory so that relative paths are supported. + ''' + expanded_directory = os.path.expanduser(directory) + + # This would be a lot easier to do with glob(..., root_dir=working_directory), but root_dir is + # only available in Python 3.10+. + normalized_directory = os.path.join(working_directory or '', expanded_directory) + glob_paths = glob.glob(normalized_directory) + + if not glob_paths: + return [expanded_directory] + + working_directory_prefix = os.path.join(working_directory or '', '') + + return [ + ( + glob_path + # If these are equal, that means we didn't add any working directory prefix above. + if normalized_directory == expanded_directory + # Remove the working directory prefix that we added above in order to make glob() work. + # We can't use os.path.relpath() here because it collapses any use of Borg's slashdot + # hack. + else glob_path.removeprefix(working_directory_prefix) + ) + for glob_path in glob_paths + ] + + +def expand_patterns(patterns, working_directory=None, skip_paths=None): + ''' + Given a sequence of borgmatic.borg.pattern.Pattern instances and an optional working directory, + expand tildes and globs in each root pattern and expand just tildes in each non-root pattern. + The idea is that non-root patterns may be regular expressions or other pattern styles containing + "*" that borgmatic should not expand as a shell glob. + + Return all the resulting patterns as a tuple. + + If a set of paths are given to skip, then don't expand any patterns matching them. + ''' + if patterns is None: + return () + + return tuple( + itertools.chain.from_iterable( + ( + ( + borgmatic.borg.pattern.Pattern( + expanded_path, + pattern.type, + pattern.style, + pattern.device, + pattern.source, + ) + for expanded_path in expand_directory(pattern.path, working_directory) + ) + if pattern.type == borgmatic.borg.pattern.Pattern_type.ROOT + and pattern.path not in (skip_paths or ()) + else ( + borgmatic.borg.pattern.Pattern( + os.path.expanduser(pattern.path), + pattern.type, + pattern.style, + pattern.device, + pattern.source, + ), + ) + ) + for pattern in patterns + ) + ) + + +def device_map_patterns(patterns, working_directory=None): + ''' + Given a sequence of borgmatic.borg.pattern.Pattern instances and an optional working directory, + determine the identifier for the device on which the pattern's path resides—or None if the path + doesn't exist or is from a non-root pattern. Return an updated sequence of patterns with the + device field populated. But if the device field is already set, don't bother setting it again. + + This is handy for determining whether two different pattern paths are on the same filesystem + (have the same device identifier). + ''' + return tuple( + borgmatic.borg.pattern.Pattern( + pattern.path, + pattern.type, + pattern.style, + device=pattern.device + or ( + os.stat(full_path).st_dev + if pattern.type == borgmatic.borg.pattern.Pattern_type.ROOT + and os.path.exists(full_path) + else None + ), + source=pattern.source, + ) + for pattern in patterns + for full_path in (os.path.join(working_directory or '', pattern.path),) + ) + + +def deduplicate_patterns(patterns): + ''' + Given a sequence of borgmatic.borg.pattern.Pattern instances, return them with all duplicate + root child patterns removed. For instance, if two root patterns are given with paths "/foo" and + "/foo/bar", return just the one with "/foo". Non-root patterns are passed through without + modification. + + The one exception to deduplication is two paths are on different filesystems (devices). In that + case, they won't get deduplicated, in case they both need to be passed to Borg (e.g. the + one_file_system option is true). + + The idea is that if Borg is given a root parent pattern, then it doesn't also need to be given + child patterns, because it will naturally spider the contents of the parent pattern's path. And + there are cases where Borg coming across the same file twice will result in duplicate reads and + even hangs, e.g. when a database hook is using a named pipe for streaming database dumps to + Borg. + ''' + deduplicated = {} # Use just the keys as an ordered set. + + for pattern in patterns: + if pattern.type != borgmatic.borg.pattern.Pattern_type.ROOT: + deduplicated[pattern] = True + continue + + parents = pathlib.PurePath(pattern.path).parents + + # If another directory in the given list is a parent of current directory (even n levels up) + # and both are on the same filesystem, then the current directory is a duplicate. + for other_pattern in patterns: + if other_pattern.type != borgmatic.borg.pattern.Pattern_type.ROOT: + continue + + if any( + pathlib.PurePath(other_pattern.path) == parent + and pattern.device is not None + and other_pattern.device == pattern.device + for parent in parents + ): + break + else: + deduplicated[pattern] = True + + return tuple(deduplicated.keys()) + + +def process_patterns(patterns, working_directory, skip_expand_paths=None): + ''' + Given a sequence of Borg patterns and a configured working directory, expand and deduplicate any + "root" patterns, returning the resulting root and non-root patterns as a list. + + If any paths are given to skip, don't expand them. + ''' + skip_paths = set(skip_expand_paths or ()) + + return list( + deduplicate_patterns( + device_map_patterns( + expand_patterns( + patterns, + working_directory=working_directory, + skip_paths=skip_paths, + ) + ) + ) + ) diff --git a/borgmatic/actions/recreate.py b/borgmatic/actions/recreate.py index 3cef7762..94c50642 100644 --- a/borgmatic/actions/recreate.py +++ b/borgmatic/actions/recreate.py @@ -2,7 +2,7 @@ import logging import borgmatic.borg.recreate import borgmatic.config.validate -from borgmatic.actions.create import collect_patterns, process_patterns +from borgmatic.actions.pattern import collect_patterns, process_patterns logger = logging.getLogger(__name__) diff --git a/borgmatic/borg/create.py b/borgmatic/borg/create.py index de5115dc..efaf795c 100644 --- a/borgmatic/borg/create.py +++ b/borgmatic/borg/create.py @@ -1,9 +1,7 @@ -import itertools import logging import os import pathlib import stat -import tempfile import textwrap import borgmatic.borg.pattern @@ -20,76 +18,6 @@ from borgmatic.execute import ( logger = logging.getLogger(__name__) -def write_patterns_file(patterns, borgmatic_runtime_directory, patterns_file=None): - ''' - Given a sequence of patterns as borgmatic.borg.pattern.Pattern instances, write them to a named - temporary file in the given borgmatic runtime directory and return the file object so it can - continue to exist on disk as long as the caller needs it. - - If an optional open pattern file is given, append to it instead of making a new temporary file. - Return None if no patterns are provided. - ''' - if not patterns: - return None - - if patterns_file is None: - patterns_file = tempfile.NamedTemporaryFile('w', dir=borgmatic_runtime_directory) - operation_name = 'Writing' - else: - patterns_file.write('\n') - operation_name = 'Appending' - - patterns_output = '\n'.join( - f'{pattern.type.value} {pattern.style.value}{":" if pattern.style.value else ""}{pattern.path}' - for pattern in patterns - ) - logger.debug(f'{operation_name} patterns to {patterns_file.name}:\n{patterns_output}') - - patterns_file.write(patterns_output) - patterns_file.flush() - - return patterns_file - - -def make_exclude_flags(config): - ''' - Given a configuration dict with various exclude options, return the corresponding Borg flags as - a tuple. - ''' - caches_flag = ('--exclude-caches',) if config.get('exclude_caches') else () - if_present_flags = tuple( - itertools.chain.from_iterable( - ('--exclude-if-present', if_present) - for if_present in config.get('exclude_if_present', ()) - ) - ) - keep_exclude_tags_flags = ('--keep-exclude-tags',) if config.get('keep_exclude_tags') else () - exclude_nodump_flags = ('--exclude-nodump',) if config.get('exclude_nodump') else () - - return caches_flag + if_present_flags + keep_exclude_tags_flags + exclude_nodump_flags - - -def make_list_filter_flags(local_borg_version, dry_run): - ''' - Given the local Borg version and whether this is a dry run, return the corresponding flags for - passing to "--list --filter". The general idea is that excludes are shown for a dry run or when - the verbosity is debug. - ''' - base_flags = 'AME' - show_excludes = logger.isEnabledFor(logging.DEBUG) - - if feature.available(feature.Feature.EXCLUDED_FILES_MINUS, local_borg_version): - if show_excludes or dry_run: - return f'{base_flags}+-' - else: - return base_flags - - if show_excludes: - return f'{base_flags}x-' - else: - return f'{base_flags}-' - - def special_file(path, working_directory=None): ''' Return whether the given path is a special file (character device, block device, or named pipe @@ -182,24 +110,6 @@ def collect_special_file_paths( ) -def check_all_root_patterns_exist(patterns): - ''' - Given a sequence of borgmatic.borg.pattern.Pattern instances, check that all root pattern - paths exist. If any don't, raise an exception. - ''' - missing_paths = [ - pattern.path - for pattern in patterns - if pattern.type == borgmatic.borg.pattern.Pattern_type.ROOT - if not os.path.exists(pattern.path) - ] - - if missing_paths: - raise ValueError( - f"Source directories or root pattern paths do not exist: {', '.join(missing_paths)}" - ) - - MAX_SPECIAL_FILE_PATHS_LENGTH = 1000 @@ -224,9 +134,11 @@ def make_base_create_command( arguments, open pattern file handle). ''' if config.get('source_directories_must_exist', False): - check_all_root_patterns_exist(patterns) + borgmatic.borg.pattern.check_all_root_patterns_exist(patterns) - patterns_file = write_patterns_file(patterns, borgmatic_runtime_directory) + patterns_file = borgmatic.borg.pattern.write_patterns_file( + patterns, borgmatic_runtime_directory + ) checkpoint_interval = config.get('checkpoint_interval', None) checkpoint_volume = config.get('checkpoint_volume', None) chunker_params = config.get('chunker_params', None) @@ -235,7 +147,7 @@ def make_base_create_command( upload_buffer_size = config.get('upload_buffer_size', None) umask = config.get('umask', None) lock_wait = config.get('lock_wait', None) - list_filter_flags = make_list_filter_flags(local_borg_version, dry_run) + list_filter_flags = flags.make_list_filter_flags(local_borg_version, dry_run) files_cache = config.get('files_cache') archive_name_format = config.get( 'archive_name_format', flags.get_default_archive_name_format(local_borg_version) @@ -270,7 +182,7 @@ def make_base_create_command( tuple(local_path.split(' ')) + ('create',) + (('--patterns-from', patterns_file.name) if patterns_file else ()) - + make_exclude_flags(config) + + flags.make_exclude_flags(config) + (('--checkpoint-interval', str(checkpoint_interval)) if checkpoint_interval else ()) + (('--checkpoint-volume', str(checkpoint_volume)) if checkpoint_volume else ()) + (('--chunker-params', chunker_params) if chunker_params else ()) @@ -329,7 +241,7 @@ def make_base_create_command( logger.warning( f'Excluding special files to prevent Borg from hanging: {truncated_special_file_paths}' ) - patterns_file = write_patterns_file( + patterns_file = borgmatic.borg.pattern.write_patterns_file( tuple( borgmatic.borg.pattern.Pattern( special_file_path, diff --git a/borgmatic/borg/flags.py b/borgmatic/borg/flags.py index a88400e1..c9064901 100644 --- a/borgmatic/borg/flags.py +++ b/borgmatic/borg/flags.py @@ -197,3 +197,42 @@ def omit_flag_and_value(arguments, flag): if flag not in (previous_argument, argument) if not argument.startswith(f'{flag}=') ) + + +def make_exclude_flags(config): + ''' + Given a configuration dict with various exclude options, return the corresponding Borg flags as + a tuple. + ''' + caches_flag = ('--exclude-caches',) if config.get('exclude_caches') else () + if_present_flags = tuple( + itertools.chain.from_iterable( + ('--exclude-if-present', if_present) + for if_present in config.get('exclude_if_present', ()) + ) + ) + keep_exclude_tags_flags = ('--keep-exclude-tags',) if config.get('keep_exclude_tags') else () + exclude_nodump_flags = ('--exclude-nodump',) if config.get('exclude_nodump') else () + + return caches_flag + if_present_flags + keep_exclude_tags_flags + exclude_nodump_flags + + +def make_list_filter_flags(local_borg_version, dry_run): + ''' + Given the local Borg version and whether this is a dry run, return the corresponding flags for + passing to "--list --filter". The general idea is that excludes are shown for a dry run or when + the verbosity is debug. + ''' + base_flags = 'AME' + show_excludes = logger.isEnabledFor(logging.DEBUG) + + if feature.available(feature.Feature.EXCLUDED_FILES_MINUS, local_borg_version): + if show_excludes or dry_run: + return f'{base_flags}+-' + else: + return base_flags + + if show_excludes: + return f'{base_flags}x-' + else: + return f'{base_flags}-' diff --git a/borgmatic/borg/pattern.py b/borgmatic/borg/pattern.py index dfce7c3b..f84498a1 100644 --- a/borgmatic/borg/pattern.py +++ b/borgmatic/borg/pattern.py @@ -1,5 +1,12 @@ import collections import enum +import logging +import os +import tempfile + +import borgmatic.borg.pattern + +logger = logging.getLogger(__name__) # See https://borgbackup.readthedocs.io/en/stable/usage/help.html#borg-help-patterns @@ -48,3 +55,52 @@ Pattern = collections.namedtuple( Pattern_source.HOOK, ), ) + + +def write_patterns_file(patterns, borgmatic_runtime_directory, patterns_file=None): + ''' + Given a sequence of patterns as borgmatic.borg.pattern.Pattern instances, write them to a named + temporary file in the given borgmatic runtime directory and return the file object so it can + continue to exist on disk as long as the caller needs it. + + If an optional open pattern file is given, append to it instead of making a new temporary file. + Return None if no patterns are provided. + ''' + if not patterns: + return None + + if patterns_file is None: + patterns_file = tempfile.NamedTemporaryFile('w', dir=borgmatic_runtime_directory) + operation_name = 'Writing' + else: + patterns_file.write('\n') + operation_name = 'Appending' + + patterns_output = '\n'.join( + f'{pattern.type.value} {pattern.style.value}{":" if pattern.style.value else ""}{pattern.path}' + for pattern in patterns + ) + logger.debug(f'{operation_name} patterns to {patterns_file.name}:\n{patterns_output}') + + patterns_file.write(patterns_output) + patterns_file.flush() + + return patterns_file + + +def check_all_root_patterns_exist(patterns): + ''' + Given a sequence of borgmatic.borg.pattern.Pattern instances, check that all root pattern + paths exist. If any don't, raise an exception. + ''' + missing_paths = [ + pattern.path + for pattern in patterns + if pattern.type == borgmatic.borg.pattern.Pattern_type.ROOT + if not os.path.exists(pattern.path) + ] + + if missing_paths: + raise ValueError( + f"Source directories or root pattern paths do not exist: {', '.join(missing_paths)}" + ) diff --git a/borgmatic/borg/recreate.py b/borgmatic/borg/recreate.py index b687256e..bd38bab5 100644 --- a/borgmatic/borg/recreate.py +++ b/borgmatic/borg/recreate.py @@ -6,7 +6,7 @@ import borgmatic.borg.feature import borgmatic.config.paths import borgmatic.execute from borgmatic.borg import flags -from borgmatic.borg.create import make_exclude_flags, make_list_filter_flags, write_patterns_file +from borgmatic.borg.pattern import write_patterns_file logger = logging.getLogger(__name__) @@ -29,7 +29,7 @@ def recreate_archive( arguments. ''' lock_wait = config.get('lock_wait', None) - exclude_flags = make_exclude_flags(config) + exclude_flags = flags.make_exclude_flags(config) compression = config.get('compression', None) chunker_params = config.get('chunker_params', None) # Available recompress MODES: "if-different", "always", "never" (default) @@ -52,7 +52,7 @@ def recreate_archive( ( '--list', '--filter', - make_list_filter_flags(local_borg_version, global_arguments.dry_run), + flags.make_list_filter_flags(local_borg_version, global_arguments.dry_run), ) if config.get('list_details') else () diff --git a/borgmatic/config/schema.yaml b/borgmatic/config/schema.yaml index aa56a8be..195b1a56 100644 --- a/borgmatic/config/schema.yaml +++ b/borgmatic/config/schema.yaml @@ -847,6 +847,7 @@ properties: - compact - create - recreate + - pattern - check - delete - extract diff --git a/tests/unit/actions/test_check.py b/tests/unit/actions/test_check.py index e7eee5c4..3140252a 100644 --- a/tests/unit/actions/test_check.py +++ b/tests/unit/actions/test_check.py @@ -561,10 +561,10 @@ def test_collect_spot_check_source_paths_parses_borg_output(): flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return( flexmock() ) - flexmock(module.borgmatic.actions.create).should_receive('collect_patterns').and_return( + flexmock(module.borgmatic.actions.pattern).should_receive('collect_patterns').and_return( flexmock() ) - flexmock(module.borgmatic.actions.create).should_receive('process_patterns').and_return( + flexmock(module.borgmatic.actions.pattern).should_receive('process_patterns').and_return( [Pattern('foo'), Pattern('bar')] ) flexmock(module.borgmatic.borg.create).should_receive('make_base_create_command').with_args( @@ -608,10 +608,10 @@ def test_collect_spot_check_source_paths_passes_through_stream_processes_false() flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return( flexmock() ) - flexmock(module.borgmatic.actions.create).should_receive('collect_patterns').and_return( + flexmock(module.borgmatic.actions.pattern).should_receive('collect_patterns').and_return( flexmock() ) - flexmock(module.borgmatic.actions.create).should_receive('process_patterns').and_return( + flexmock(module.borgmatic.actions.pattern).should_receive('process_patterns').and_return( [Pattern('foo'), Pattern('bar')] ) flexmock(module.borgmatic.borg.create).should_receive('make_base_create_command').with_args( @@ -655,10 +655,10 @@ def test_collect_spot_check_source_paths_without_working_directory_parses_borg_o flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return( flexmock() ) - flexmock(module.borgmatic.actions.create).should_receive('collect_patterns').and_return( + flexmock(module.borgmatic.actions.pattern).should_receive('collect_patterns').and_return( flexmock() ) - flexmock(module.borgmatic.actions.create).should_receive('process_patterns').and_return( + flexmock(module.borgmatic.actions.pattern).should_receive('process_patterns').and_return( [Pattern('foo'), Pattern('bar')] ) flexmock(module.borgmatic.borg.create).should_receive('make_base_create_command').with_args( @@ -702,10 +702,10 @@ def test_collect_spot_check_source_paths_skips_directories(): flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return( flexmock() ) - flexmock(module.borgmatic.actions.create).should_receive('collect_patterns').and_return( + flexmock(module.borgmatic.actions.pattern).should_receive('collect_patterns').and_return( flexmock() ) - flexmock(module.borgmatic.actions.create).should_receive('process_patterns').and_return( + flexmock(module.borgmatic.actions.pattern).should_receive('process_patterns').and_return( [Pattern('foo'), Pattern('bar')] ) flexmock(module.borgmatic.borg.create).should_receive('make_base_create_command').with_args( @@ -847,10 +847,10 @@ def test_collect_spot_check_source_paths_uses_working_directory(): flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return( flexmock() ) - flexmock(module.borgmatic.actions.create).should_receive('collect_patterns').and_return( + flexmock(module.borgmatic.actions.pattern).should_receive('collect_patterns').and_return( flexmock() ) - flexmock(module.borgmatic.actions.create).should_receive('process_patterns').and_return( + flexmock(module.borgmatic.actions.pattern).should_receive('process_patterns').and_return( [Pattern('foo'), Pattern('bar')] ) flexmock(module.borgmatic.borg.create).should_receive('make_base_create_command').with_args( diff --git a/tests/unit/actions/test_create.py b/tests/unit/actions/test_create.py index 6761f8f4..722b6365 100644 --- a/tests/unit/actions/test_create.py +++ b/tests/unit/actions/test_create.py @@ -1,429 +1,9 @@ -import io -import sys +import os import pytest from flexmock import flexmock from borgmatic.actions import create as module -from borgmatic.borg.pattern import Pattern, Pattern_source, Pattern_style, Pattern_type - - -@pytest.mark.parametrize( - 'pattern_line,expected_pattern', - ( - ('R /foo', Pattern('/foo', source=Pattern_source.CONFIG)), - ('P sh', Pattern('sh', Pattern_type.PATTERN_STYLE, source=Pattern_source.CONFIG)), - ('+ /foo*', Pattern('/foo*', Pattern_type.INCLUDE, source=Pattern_source.CONFIG)), - ( - '+ sh:/foo*', - Pattern( - '/foo*', Pattern_type.INCLUDE, Pattern_style.SHELL, source=Pattern_source.CONFIG - ), - ), - ), -) -def test_parse_pattern_transforms_pattern_line_to_instance(pattern_line, expected_pattern): - module.parse_pattern(pattern_line) == expected_pattern - - -def test_parse_pattern_with_invalid_pattern_line_errors(): - with pytest.raises(ValueError): - module.parse_pattern('/foo') - - -def test_collect_patterns_converts_source_directories(): - assert module.collect_patterns({'source_directories': ['/foo', '/bar']}) == ( - Pattern('/foo', source=Pattern_source.CONFIG), - Pattern('/bar', source=Pattern_source.CONFIG), - ) - - -def test_collect_patterns_parses_config_patterns(): - flexmock(module).should_receive('parse_pattern').with_args('R /foo').and_return(Pattern('/foo')) - flexmock(module).should_receive('parse_pattern').with_args('# comment').never() - flexmock(module).should_receive('parse_pattern').with_args('').never() - flexmock(module).should_receive('parse_pattern').with_args(' ').never() - flexmock(module).should_receive('parse_pattern').with_args('R /bar').and_return(Pattern('/bar')) - - assert module.collect_patterns({'patterns': ['R /foo', '# comment', '', ' ', 'R /bar']}) == ( - Pattern('/foo'), - Pattern('/bar'), - ) - - -def test_collect_patterns_converts_exclude_patterns(): - assert module.collect_patterns({'exclude_patterns': ['/foo', '/bar', 'sh:**/baz']}) == ( - Pattern( - '/foo', Pattern_type.NO_RECURSE, Pattern_style.FNMATCH, source=Pattern_source.CONFIG - ), - Pattern( - '/bar', Pattern_type.NO_RECURSE, Pattern_style.FNMATCH, source=Pattern_source.CONFIG - ), - Pattern( - '**/baz', Pattern_type.NO_RECURSE, Pattern_style.SHELL, source=Pattern_source.CONFIG - ), - ) - - -def test_collect_patterns_reads_config_patterns_from_file(): - builtins = flexmock(sys.modules['builtins']) - builtins.should_receive('open').with_args('file1.txt').and_return(io.StringIO('R /foo')) - builtins.should_receive('open').with_args('file2.txt').and_return( - io.StringIO('R /bar\n# comment\n\n \nR /baz') - ) - flexmock(module).should_receive('parse_pattern').with_args('R /foo').and_return(Pattern('/foo')) - flexmock(module).should_receive('parse_pattern').with_args('# comment').never() - flexmock(module).should_receive('parse_pattern').with_args('').never() - flexmock(module).should_receive('parse_pattern').with_args(' ').never() - flexmock(module).should_receive('parse_pattern').with_args('R /bar').and_return(Pattern('/bar')) - flexmock(module).should_receive('parse_pattern').with_args('R /baz').and_return(Pattern('/baz')) - - assert module.collect_patterns({'patterns_from': ['file1.txt', 'file2.txt']}) == ( - Pattern('/foo'), - Pattern('/bar'), - Pattern('/baz'), - ) - - -def test_collect_patterns_errors_on_missing_config_patterns_from_file(): - builtins = flexmock(sys.modules['builtins']) - builtins.should_receive('open').with_args('file1.txt').and_raise(FileNotFoundError) - flexmock(module).should_receive('parse_pattern').never() - - with pytest.raises(ValueError): - module.collect_patterns({'patterns_from': ['file1.txt', 'file2.txt']}) - - -def test_collect_patterns_reads_config_exclude_from_file(): - builtins = flexmock(sys.modules['builtins']) - builtins.should_receive('open').with_args('file1.txt').and_return(io.StringIO('/foo')) - builtins.should_receive('open').with_args('file2.txt').and_return( - io.StringIO('/bar\n# comment\n\n \n/baz') - ) - flexmock(module).should_receive('parse_pattern').with_args( - '! /foo', default_style=Pattern_style.FNMATCH - ).and_return(Pattern('/foo', Pattern_type.NO_RECURSE, Pattern_style.FNMATCH)) - flexmock(module).should_receive('parse_pattern').with_args( - '! /bar', default_style=Pattern_style.FNMATCH - ).and_return(Pattern('/bar', Pattern_type.NO_RECURSE, Pattern_style.FNMATCH)) - flexmock(module).should_receive('parse_pattern').with_args('# comment').never() - flexmock(module).should_receive('parse_pattern').with_args('').never() - flexmock(module).should_receive('parse_pattern').with_args(' ').never() - flexmock(module).should_receive('parse_pattern').with_args( - '! /baz', default_style=Pattern_style.FNMATCH - ).and_return(Pattern('/baz', Pattern_type.NO_RECURSE, Pattern_style.FNMATCH)) - - assert module.collect_patterns({'exclude_from': ['file1.txt', 'file2.txt']}) == ( - Pattern('/foo', Pattern_type.NO_RECURSE, Pattern_style.FNMATCH), - Pattern('/bar', Pattern_type.NO_RECURSE, Pattern_style.FNMATCH), - Pattern('/baz', Pattern_type.NO_RECURSE, Pattern_style.FNMATCH), - ) - - -def test_collect_patterns_errors_on_missing_config_exclude_from_file(): - builtins = flexmock(sys.modules['builtins']) - builtins.should_receive('open').with_args('file1.txt').and_raise(OSError) - flexmock(module).should_receive('parse_pattern').never() - - with pytest.raises(ValueError): - module.collect_patterns({'exclude_from': ['file1.txt', 'file2.txt']}) - - -def test_expand_directory_with_basic_path_passes_it_through(): - flexmock(module.os.path).should_receive('expanduser').and_return('foo') - flexmock(module.glob).should_receive('glob').and_return([]) - - paths = module.expand_directory('foo', None) - - assert paths == ['foo'] - - -def test_expand_directory_with_glob_expands(): - flexmock(module.os.path).should_receive('expanduser').and_return('foo*') - flexmock(module.glob).should_receive('glob').and_return(['foo', 'food']) - - paths = module.expand_directory('foo*', None) - - assert paths == ['foo', 'food'] - - -def test_expand_directory_strips_off_working_directory(): - flexmock(module.os.path).should_receive('expanduser').and_return('foo') - flexmock(module.glob).should_receive('glob').with_args('/working/dir/foo').and_return([]).once() - - paths = module.expand_directory('foo', working_directory='/working/dir') - - assert paths == ['foo'] - - -def test_expand_directory_globs_working_directory_and_strips_it_off(): - flexmock(module.os.path).should_receive('expanduser').and_return('foo*') - flexmock(module.glob).should_receive('glob').with_args('/working/dir/foo*').and_return( - ['/working/dir/foo', '/working/dir/food'] - ).once() - - paths = module.expand_directory('foo*', working_directory='/working/dir') - - assert paths == ['foo', 'food'] - - -def test_expand_directory_with_slashdot_hack_globs_working_directory_and_strips_it_off(): - flexmock(module.os.path).should_receive('expanduser').and_return('./foo*') - flexmock(module.glob).should_receive('glob').with_args('/working/dir/./foo*').and_return( - ['/working/dir/./foo', '/working/dir/./food'] - ).once() - - paths = module.expand_directory('./foo*', working_directory='/working/dir') - - assert paths == ['./foo', './food'] - - -def test_expand_directory_with_working_directory_matching_start_of_directory_does_not_strip_it_off(): - flexmock(module.os.path).should_receive('expanduser').and_return('/working/dir/foo') - flexmock(module.glob).should_receive('glob').with_args('/working/dir/foo').and_return( - ['/working/dir/foo'] - ).once() - - paths = module.expand_directory('/working/dir/foo', working_directory='/working/dir') - - assert paths == ['/working/dir/foo'] - - -def test_expand_patterns_flattens_expanded_directories(): - flexmock(module).should_receive('expand_directory').with_args('~/foo', None).and_return( - ['/root/foo'] - ) - flexmock(module).should_receive('expand_directory').with_args('bar*', None).and_return( - ['bar', 'barf'] - ) - - paths = module.expand_patterns((Pattern('~/foo'), Pattern('bar*'))) - - assert paths == (Pattern('/root/foo'), Pattern('bar'), Pattern('barf')) - - -def test_expand_patterns_with_working_directory_passes_it_through(): - flexmock(module).should_receive('expand_directory').with_args('foo', '/working/dir').and_return( - ['/working/dir/foo'] - ) - - patterns = module.expand_patterns((Pattern('foo'),), working_directory='/working/dir') - - assert patterns == (Pattern('/working/dir/foo'),) - - -def test_expand_patterns_does_not_expand_skip_paths(): - flexmock(module).should_receive('expand_directory').with_args('/foo', None).and_return(['/foo']) - flexmock(module).should_receive('expand_directory').with_args('/bar*', None).never() - - patterns = module.expand_patterns((Pattern('/foo'), Pattern('/bar*')), skip_paths=('/bar*',)) - - assert patterns == (Pattern('/foo'), Pattern('/bar*')) - - -def test_expand_patterns_considers_none_as_no_patterns(): - assert module.expand_patterns(None) == () - - -def test_expand_patterns_expands_tildes_and_globs_in_root_patterns(): - flexmock(module.os.path).should_receive('expanduser').never() - flexmock(module).should_receive('expand_directory').and_return( - ['/root/foo/one', '/root/foo/two'] - ) - - paths = module.expand_patterns((Pattern('~/foo/*'),)) - - assert paths == (Pattern('/root/foo/one'), Pattern('/root/foo/two')) - - -def test_expand_patterns_expands_only_tildes_in_non_root_patterns(): - flexmock(module).should_receive('expand_directory').never() - flexmock(module.os.path).should_receive('expanduser').and_return('/root/bar/*') - - paths = module.expand_patterns((Pattern('~/bar/*', Pattern_type.INCLUDE),)) - - assert paths == (Pattern('/root/bar/*', Pattern_type.INCLUDE),) - - -def test_device_map_patterns_gives_device_id_per_path(): - flexmock(module.os.path).should_receive('exists').and_return(True) - flexmock(module.os).should_receive('stat').with_args('/foo').and_return(flexmock(st_dev=55)) - flexmock(module.os).should_receive('stat').with_args('/bar').and_return(flexmock(st_dev=66)) - - device_map = module.device_map_patterns((Pattern('/foo'), Pattern('/bar'))) - - assert device_map == ( - Pattern('/foo', device=55), - Pattern('/bar', device=66), - ) - - -def test_device_map_patterns_only_considers_root_patterns(): - flexmock(module.os.path).should_receive('exists').and_return(True) - flexmock(module.os).should_receive('stat').with_args('/foo').and_return(flexmock(st_dev=55)) - flexmock(module.os).should_receive('stat').with_args('/bar*').never() - - device_map = module.device_map_patterns( - (Pattern('/foo'), Pattern('/bar*', Pattern_type.INCLUDE)) - ) - - assert device_map == ( - Pattern('/foo', device=55), - Pattern('/bar*', Pattern_type.INCLUDE), - ) - - -def test_device_map_patterns_with_missing_path_does_not_error(): - flexmock(module.os.path).should_receive('exists').and_return(True).and_return(False) - flexmock(module.os).should_receive('stat').with_args('/foo').and_return(flexmock(st_dev=55)) - flexmock(module.os).should_receive('stat').with_args('/bar').never() - - device_map = module.device_map_patterns((Pattern('/foo'), Pattern('/bar'))) - - assert device_map == ( - Pattern('/foo', device=55), - Pattern('/bar'), - ) - - -def test_device_map_patterns_uses_working_directory_to_construct_path(): - flexmock(module.os.path).should_receive('exists').and_return(True) - flexmock(module.os).should_receive('stat').with_args('/foo').and_return(flexmock(st_dev=55)) - flexmock(module.os).should_receive('stat').with_args('/working/dir/bar').and_return( - flexmock(st_dev=66) - ) - - device_map = module.device_map_patterns( - (Pattern('/foo'), Pattern('bar')), working_directory='/working/dir' - ) - - assert device_map == ( - Pattern('/foo', device=55), - Pattern('bar', device=66), - ) - - -def test_device_map_patterns_with_existing_device_id_does_not_overwrite_it(): - flexmock(module.os.path).should_receive('exists').and_return(True) - flexmock(module.os).should_receive('stat').with_args('/foo').and_return(flexmock(st_dev=55)) - flexmock(module.os).should_receive('stat').with_args('/bar').and_return(flexmock(st_dev=100)) - - device_map = module.device_map_patterns((Pattern('/foo'), Pattern('/bar', device=66))) - - assert device_map == ( - Pattern('/foo', device=55), - Pattern('/bar', device=66), - ) - - -@pytest.mark.parametrize( - 'patterns,expected_patterns', - ( - ((Pattern('/', device=1), Pattern('/root', device=1)), (Pattern('/', device=1),)), - ((Pattern('/', device=1), Pattern('/root/', device=1)), (Pattern('/', device=1),)), - ( - (Pattern('/', device=1), Pattern('/root', device=2)), - (Pattern('/', device=1), Pattern('/root', device=2)), - ), - ((Pattern('/root', device=1), Pattern('/', device=1)), (Pattern('/', device=1),)), - ( - (Pattern('/root', device=1), Pattern('/root/foo', device=1)), - (Pattern('/root', device=1),), - ), - ( - (Pattern('/root/', device=1), Pattern('/root/foo', device=1)), - (Pattern('/root/', device=1),), - ), - ( - (Pattern('/root', device=1), Pattern('/root/foo/', device=1)), - (Pattern('/root', device=1),), - ), - ( - (Pattern('/root', device=1), Pattern('/root/foo', device=2)), - (Pattern('/root', device=1), Pattern('/root/foo', device=2)), - ), - ( - (Pattern('/root/foo', device=1), Pattern('/root', device=1)), - (Pattern('/root', device=1),), - ), - ( - (Pattern('/root', device=None), Pattern('/root/foo', device=None)), - (Pattern('/root'), Pattern('/root/foo')), - ), - ( - ( - Pattern('/root', device=1), - Pattern('/etc', device=1), - Pattern('/root/foo/bar', device=1), - ), - (Pattern('/root', device=1), Pattern('/etc', device=1)), - ), - ( - ( - Pattern('/root', device=1), - Pattern('/root/foo', device=1), - Pattern('/root/foo/bar', device=1), - ), - (Pattern('/root', device=1),), - ), - ((Pattern('/dup', device=1), Pattern('/dup', device=1)), (Pattern('/dup', device=1),)), - ( - (Pattern('/foo', device=1), Pattern('/bar', device=1)), - (Pattern('/foo', device=1), Pattern('/bar', device=1)), - ), - ( - (Pattern('/foo', device=1), Pattern('/bar', device=2)), - (Pattern('/foo', device=1), Pattern('/bar', device=2)), - ), - ((Pattern('/root/foo', device=1),), (Pattern('/root/foo', device=1),)), - ( - (Pattern('/', device=1), Pattern('/root', Pattern_type.INCLUDE, device=1)), - (Pattern('/', device=1), Pattern('/root', Pattern_type.INCLUDE, device=1)), - ), - ( - (Pattern('/root', Pattern_type.INCLUDE, device=1), Pattern('/', device=1)), - (Pattern('/root', Pattern_type.INCLUDE, device=1), Pattern('/', device=1)), - ), - ), -) -def test_deduplicate_patterns_omits_child_paths_on_the_same_filesystem(patterns, expected_patterns): - assert module.deduplicate_patterns(patterns) == expected_patterns - - -def test_process_patterns_includes_patterns(): - flexmock(module).should_receive('deduplicate_patterns').and_return( - (Pattern('foo'), Pattern('bar')) - ) - flexmock(module).should_receive('device_map_patterns').and_return({}) - flexmock(module).should_receive('expand_patterns').with_args( - (Pattern('foo'), Pattern('bar')), - working_directory='/working', - skip_paths=set(), - ).and_return(()).once() - - assert module.process_patterns( - (Pattern('foo'), Pattern('bar')), - working_directory='/working', - ) == [Pattern('foo'), Pattern('bar')] - - -def test_process_patterns_skips_expand_for_requested_paths(): - skip_paths = {flexmock()} - flexmock(module).should_receive('deduplicate_patterns').and_return( - (Pattern('foo'), Pattern('bar')) - ) - flexmock(module).should_receive('device_map_patterns').and_return({}) - flexmock(module).should_receive('expand_patterns').with_args( - (Pattern('foo'), Pattern('bar')), - working_directory='/working', - skip_paths=skip_paths, - ).and_return(()).once() - - assert module.process_patterns( - (Pattern('foo'), Pattern('bar')), - working_directory='/working', - skip_expand_paths=skip_paths, - ) == [Pattern('foo'), Pattern('bar')] def test_run_create_executes_and_calls_hooks_for_configured_repository(): @@ -437,9 +17,9 @@ def test_run_create_executes_and_calls_hooks_for_configured_repository(): flexmock(module.borgmatic.hooks.dispatch).should_receive( 'call_hooks_even_if_unconfigured' ).and_return({}) - flexmock(module).should_receive('collect_patterns').and_return(()) - flexmock(module).should_receive('process_patterns').and_return([]) - flexmock(module.os.path).should_receive('join').and_return('/run/borgmatic/bootstrap') + flexmock(module.borgmatic.actions.pattern).should_receive('collect_patterns').and_return(()) + flexmock(module.borgmatic.actions.pattern).should_receive('process_patterns').and_return([]) + flexmock(os.path).should_receive('join').and_return('/run/borgmatic/bootstrap') create_arguments = flexmock( repository=None, progress=flexmock(), @@ -478,9 +58,9 @@ def test_run_create_runs_with_selected_repository(): flexmock(module.borgmatic.hooks.dispatch).should_receive( 'call_hooks_even_if_unconfigured' ).and_return({}) - flexmock(module).should_receive('collect_patterns').and_return(()) - flexmock(module).should_receive('process_patterns').and_return([]) - flexmock(module.os.path).should_receive('join').and_return('/run/borgmatic/bootstrap') + flexmock(module.borgmatic.actions.pattern).should_receive('collect_patterns').and_return(()) + flexmock(module.borgmatic.actions.pattern).should_receive('process_patterns').and_return([]) + flexmock(os.path).should_receive('join').and_return('/run/borgmatic/bootstrap') create_arguments = flexmock( repository=flexmock(), progress=flexmock(), @@ -621,9 +201,9 @@ def test_run_create_produces_json(): flexmock(module.borgmatic.hooks.dispatch).should_receive( 'call_hooks_even_if_unconfigured' ).and_return({}) - flexmock(module).should_receive('collect_patterns').and_return(()) - flexmock(module).should_receive('process_patterns').and_return([]) - flexmock(module.os.path).should_receive('join').and_return('/run/borgmatic/bootstrap') + flexmock(module.borgmatic.actions.pattern).should_receive('collect_patterns').and_return(()) + flexmock(module.borgmatic.actions.pattern).should_receive('process_patterns').and_return([]) + flexmock(os.path).should_receive('join').and_return('/run/borgmatic/bootstrap') create_arguments = flexmock( repository=flexmock(), progress=flexmock(), diff --git a/tests/unit/actions/test_pattern.py b/tests/unit/actions/test_pattern.py new file mode 100644 index 00000000..0afcd94d --- /dev/null +++ b/tests/unit/actions/test_pattern.py @@ -0,0 +1,426 @@ +import io +import sys + +import pytest +from flexmock import flexmock + +from borgmatic.actions import pattern as module +from borgmatic.borg.pattern import Pattern, Pattern_source, Pattern_style, Pattern_type + + +@pytest.mark.parametrize( + 'pattern_line,expected_pattern', + ( + ('R /foo', Pattern('/foo', source=Pattern_source.CONFIG)), + ('P sh', Pattern('sh', Pattern_type.PATTERN_STYLE, source=Pattern_source.CONFIG)), + ('+ /foo*', Pattern('/foo*', Pattern_type.INCLUDE, source=Pattern_source.CONFIG)), + ( + '+ sh:/foo*', + Pattern( + '/foo*', Pattern_type.INCLUDE, Pattern_style.SHELL, source=Pattern_source.CONFIG + ), + ), + ), +) +def test_parse_pattern_transforms_pattern_line_to_instance(pattern_line, expected_pattern): + module.parse_pattern(pattern_line) == expected_pattern + + +def test_parse_pattern_with_invalid_pattern_line_errors(): + with pytest.raises(ValueError): + module.parse_pattern('/foo') + + +def test_collect_patterns_converts_source_directories(): + assert module.collect_patterns({'source_directories': ['/foo', '/bar']}) == ( + Pattern('/foo', source=Pattern_source.CONFIG), + Pattern('/bar', source=Pattern_source.CONFIG), + ) + + +def test_collect_patterns_parses_config_patterns(): + flexmock(module).should_receive('parse_pattern').with_args('R /foo').and_return(Pattern('/foo')) + flexmock(module).should_receive('parse_pattern').with_args('# comment').never() + flexmock(module).should_receive('parse_pattern').with_args('').never() + flexmock(module).should_receive('parse_pattern').with_args(' ').never() + flexmock(module).should_receive('parse_pattern').with_args('R /bar').and_return(Pattern('/bar')) + + assert module.collect_patterns({'patterns': ['R /foo', '# comment', '', ' ', 'R /bar']}) == ( + Pattern('/foo'), + Pattern('/bar'), + ) + + +def test_collect_patterns_converts_exclude_patterns(): + assert module.collect_patterns({'exclude_patterns': ['/foo', '/bar', 'sh:**/baz']}) == ( + Pattern( + '/foo', Pattern_type.NO_RECURSE, Pattern_style.FNMATCH, source=Pattern_source.CONFIG + ), + Pattern( + '/bar', Pattern_type.NO_RECURSE, Pattern_style.FNMATCH, source=Pattern_source.CONFIG + ), + Pattern( + '**/baz', Pattern_type.NO_RECURSE, Pattern_style.SHELL, source=Pattern_source.CONFIG + ), + ) + + +def test_collect_patterns_reads_config_patterns_from_file(): + builtins = flexmock(sys.modules['builtins']) + builtins.should_receive('open').with_args('file1.txt').and_return(io.StringIO('R /foo')) + builtins.should_receive('open').with_args('file2.txt').and_return( + io.StringIO('R /bar\n# comment\n\n \nR /baz') + ) + flexmock(module).should_receive('parse_pattern').with_args('R /foo').and_return(Pattern('/foo')) + flexmock(module).should_receive('parse_pattern').with_args('# comment').never() + flexmock(module).should_receive('parse_pattern').with_args('').never() + flexmock(module).should_receive('parse_pattern').with_args(' ').never() + flexmock(module).should_receive('parse_pattern').with_args('R /bar').and_return(Pattern('/bar')) + flexmock(module).should_receive('parse_pattern').with_args('R /baz').and_return(Pattern('/baz')) + + assert module.collect_patterns({'patterns_from': ['file1.txt', 'file2.txt']}) == ( + Pattern('/foo'), + Pattern('/bar'), + Pattern('/baz'), + ) + + +def test_collect_patterns_errors_on_missing_config_patterns_from_file(): + builtins = flexmock(sys.modules['builtins']) + builtins.should_receive('open').with_args('file1.txt').and_raise(FileNotFoundError) + flexmock(module).should_receive('parse_pattern').never() + + with pytest.raises(ValueError): + module.collect_patterns({'patterns_from': ['file1.txt', 'file2.txt']}) + + +def test_collect_patterns_reads_config_exclude_from_file(): + builtins = flexmock(sys.modules['builtins']) + builtins.should_receive('open').with_args('file1.txt').and_return(io.StringIO('/foo')) + builtins.should_receive('open').with_args('file2.txt').and_return( + io.StringIO('/bar\n# comment\n\n \n/baz') + ) + flexmock(module).should_receive('parse_pattern').with_args( + '! /foo', default_style=Pattern_style.FNMATCH + ).and_return(Pattern('/foo', Pattern_type.NO_RECURSE, Pattern_style.FNMATCH)) + flexmock(module).should_receive('parse_pattern').with_args( + '! /bar', default_style=Pattern_style.FNMATCH + ).and_return(Pattern('/bar', Pattern_type.NO_RECURSE, Pattern_style.FNMATCH)) + flexmock(module).should_receive('parse_pattern').with_args('# comment').never() + flexmock(module).should_receive('parse_pattern').with_args('').never() + flexmock(module).should_receive('parse_pattern').with_args(' ').never() + flexmock(module).should_receive('parse_pattern').with_args( + '! /baz', default_style=Pattern_style.FNMATCH + ).and_return(Pattern('/baz', Pattern_type.NO_RECURSE, Pattern_style.FNMATCH)) + + assert module.collect_patterns({'exclude_from': ['file1.txt', 'file2.txt']}) == ( + Pattern('/foo', Pattern_type.NO_RECURSE, Pattern_style.FNMATCH), + Pattern('/bar', Pattern_type.NO_RECURSE, Pattern_style.FNMATCH), + Pattern('/baz', Pattern_type.NO_RECURSE, Pattern_style.FNMATCH), + ) + + +def test_collect_patterns_errors_on_missing_config_exclude_from_file(): + builtins = flexmock(sys.modules['builtins']) + builtins.should_receive('open').with_args('file1.txt').and_raise(OSError) + flexmock(module).should_receive('parse_pattern').never() + + with pytest.raises(ValueError): + module.collect_patterns({'exclude_from': ['file1.txt', 'file2.txt']}) + + +def test_expand_directory_with_basic_path_passes_it_through(): + flexmock(module.os.path).should_receive('expanduser').and_return('foo') + flexmock(module.glob).should_receive('glob').and_return([]) + + paths = module.expand_directory('foo', None) + + assert paths == ['foo'] + + +def test_expand_directory_with_glob_expands(): + flexmock(module.os.path).should_receive('expanduser').and_return('foo*') + flexmock(module.glob).should_receive('glob').and_return(['foo', 'food']) + + paths = module.expand_directory('foo*', None) + + assert paths == ['foo', 'food'] + + +def test_expand_directory_strips_off_working_directory(): + flexmock(module.os.path).should_receive('expanduser').and_return('foo') + flexmock(module.glob).should_receive('glob').with_args('/working/dir/foo').and_return([]).once() + + paths = module.expand_directory('foo', working_directory='/working/dir') + + assert paths == ['foo'] + + +def test_expand_directory_globs_working_directory_and_strips_it_off(): + flexmock(module.os.path).should_receive('expanduser').and_return('foo*') + flexmock(module.glob).should_receive('glob').with_args('/working/dir/foo*').and_return( + ['/working/dir/foo', '/working/dir/food'] + ).once() + + paths = module.expand_directory('foo*', working_directory='/working/dir') + + assert paths == ['foo', 'food'] + + +def test_expand_directory_with_slashdot_hack_globs_working_directory_and_strips_it_off(): + flexmock(module.os.path).should_receive('expanduser').and_return('./foo*') + flexmock(module.glob).should_receive('glob').with_args('/working/dir/./foo*').and_return( + ['/working/dir/./foo', '/working/dir/./food'] + ).once() + + paths = module.expand_directory('./foo*', working_directory='/working/dir') + + assert paths == ['./foo', './food'] + + +def test_expand_directory_with_working_directory_matching_start_of_directory_does_not_strip_it_off(): + flexmock(module.os.path).should_receive('expanduser').and_return('/working/dir/foo') + flexmock(module.glob).should_receive('glob').with_args('/working/dir/foo').and_return( + ['/working/dir/foo'] + ).once() + + paths = module.expand_directory('/working/dir/foo', working_directory='/working/dir') + + assert paths == ['/working/dir/foo'] + + +def test_expand_patterns_flattens_expanded_directories(): + flexmock(module).should_receive('expand_directory').with_args('~/foo', None).and_return( + ['/root/foo'] + ) + flexmock(module).should_receive('expand_directory').with_args('bar*', None).and_return( + ['bar', 'barf'] + ) + + paths = module.expand_patterns((Pattern('~/foo'), Pattern('bar*'))) + + assert paths == (Pattern('/root/foo'), Pattern('bar'), Pattern('barf')) + + +def test_expand_patterns_with_working_directory_passes_it_through(): + flexmock(module).should_receive('expand_directory').with_args('foo', '/working/dir').and_return( + ['/working/dir/foo'] + ) + + patterns = module.expand_patterns((Pattern('foo'),), working_directory='/working/dir') + + assert patterns == (Pattern('/working/dir/foo'),) + + +def test_expand_patterns_does_not_expand_skip_paths(): + flexmock(module).should_receive('expand_directory').with_args('/foo', None).and_return(['/foo']) + flexmock(module).should_receive('expand_directory').with_args('/bar*', None).never() + + patterns = module.expand_patterns((Pattern('/foo'), Pattern('/bar*')), skip_paths=('/bar*',)) + + assert patterns == (Pattern('/foo'), Pattern('/bar*')) + + +def test_expand_patterns_considers_none_as_no_patterns(): + assert module.expand_patterns(None) == () + + +def test_expand_patterns_expands_tildes_and_globs_in_root_patterns(): + flexmock(module.os.path).should_receive('expanduser').never() + flexmock(module).should_receive('expand_directory').and_return( + ['/root/foo/one', '/root/foo/two'] + ) + + paths = module.expand_patterns((Pattern('~/foo/*'),)) + + assert paths == (Pattern('/root/foo/one'), Pattern('/root/foo/two')) + + +def test_expand_patterns_expands_only_tildes_in_non_root_patterns(): + flexmock(module).should_receive('expand_directory').never() + flexmock(module.os.path).should_receive('expanduser').and_return('/root/bar/*') + + paths = module.expand_patterns((Pattern('~/bar/*', Pattern_type.INCLUDE),)) + + assert paths == (Pattern('/root/bar/*', Pattern_type.INCLUDE),) + + +def test_device_map_patterns_gives_device_id_per_path(): + flexmock(module.os.path).should_receive('exists').and_return(True) + flexmock(module.os).should_receive('stat').with_args('/foo').and_return(flexmock(st_dev=55)) + flexmock(module.os).should_receive('stat').with_args('/bar').and_return(flexmock(st_dev=66)) + + device_map = module.device_map_patterns((Pattern('/foo'), Pattern('/bar'))) + + assert device_map == ( + Pattern('/foo', device=55), + Pattern('/bar', device=66), + ) + + +def test_device_map_patterns_only_considers_root_patterns(): + flexmock(module.os.path).should_receive('exists').and_return(True) + flexmock(module.os).should_receive('stat').with_args('/foo').and_return(flexmock(st_dev=55)) + flexmock(module.os).should_receive('stat').with_args('/bar*').never() + + device_map = module.device_map_patterns( + (Pattern('/foo'), Pattern('/bar*', Pattern_type.INCLUDE)) + ) + + assert device_map == ( + Pattern('/foo', device=55), + Pattern('/bar*', Pattern_type.INCLUDE), + ) + + +def test_device_map_patterns_with_missing_path_does_not_error(): + flexmock(module.os.path).should_receive('exists').and_return(True).and_return(False) + flexmock(module.os).should_receive('stat').with_args('/foo').and_return(flexmock(st_dev=55)) + flexmock(module.os).should_receive('stat').with_args('/bar').never() + + device_map = module.device_map_patterns((Pattern('/foo'), Pattern('/bar'))) + + assert device_map == ( + Pattern('/foo', device=55), + Pattern('/bar'), + ) + + +def test_device_map_patterns_uses_working_directory_to_construct_path(): + flexmock(module.os.path).should_receive('exists').and_return(True) + flexmock(module.os).should_receive('stat').with_args('/foo').and_return(flexmock(st_dev=55)) + flexmock(module.os).should_receive('stat').with_args('/working/dir/bar').and_return( + flexmock(st_dev=66) + ) + + device_map = module.device_map_patterns( + (Pattern('/foo'), Pattern('bar')), working_directory='/working/dir' + ) + + assert device_map == ( + Pattern('/foo', device=55), + Pattern('bar', device=66), + ) + + +def test_device_map_patterns_with_existing_device_id_does_not_overwrite_it(): + flexmock(module.os.path).should_receive('exists').and_return(True) + flexmock(module.os).should_receive('stat').with_args('/foo').and_return(flexmock(st_dev=55)) + flexmock(module.os).should_receive('stat').with_args('/bar').and_return(flexmock(st_dev=100)) + + device_map = module.device_map_patterns((Pattern('/foo'), Pattern('/bar', device=66))) + + assert device_map == ( + Pattern('/foo', device=55), + Pattern('/bar', device=66), + ) + + +@pytest.mark.parametrize( + 'patterns,expected_patterns', + ( + ((Pattern('/', device=1), Pattern('/root', device=1)), (Pattern('/', device=1),)), + ((Pattern('/', device=1), Pattern('/root/', device=1)), (Pattern('/', device=1),)), + ( + (Pattern('/', device=1), Pattern('/root', device=2)), + (Pattern('/', device=1), Pattern('/root', device=2)), + ), + ((Pattern('/root', device=1), Pattern('/', device=1)), (Pattern('/', device=1),)), + ( + (Pattern('/root', device=1), Pattern('/root/foo', device=1)), + (Pattern('/root', device=1),), + ), + ( + (Pattern('/root/', device=1), Pattern('/root/foo', device=1)), + (Pattern('/root/', device=1),), + ), + ( + (Pattern('/root', device=1), Pattern('/root/foo/', device=1)), + (Pattern('/root', device=1),), + ), + ( + (Pattern('/root', device=1), Pattern('/root/foo', device=2)), + (Pattern('/root', device=1), Pattern('/root/foo', device=2)), + ), + ( + (Pattern('/root/foo', device=1), Pattern('/root', device=1)), + (Pattern('/root', device=1),), + ), + ( + (Pattern('/root', device=None), Pattern('/root/foo', device=None)), + (Pattern('/root'), Pattern('/root/foo')), + ), + ( + ( + Pattern('/root', device=1), + Pattern('/etc', device=1), + Pattern('/root/foo/bar', device=1), + ), + (Pattern('/root', device=1), Pattern('/etc', device=1)), + ), + ( + ( + Pattern('/root', device=1), + Pattern('/root/foo', device=1), + Pattern('/root/foo/bar', device=1), + ), + (Pattern('/root', device=1),), + ), + ((Pattern('/dup', device=1), Pattern('/dup', device=1)), (Pattern('/dup', device=1),)), + ( + (Pattern('/foo', device=1), Pattern('/bar', device=1)), + (Pattern('/foo', device=1), Pattern('/bar', device=1)), + ), + ( + (Pattern('/foo', device=1), Pattern('/bar', device=2)), + (Pattern('/foo', device=1), Pattern('/bar', device=2)), + ), + ((Pattern('/root/foo', device=1),), (Pattern('/root/foo', device=1),)), + ( + (Pattern('/', device=1), Pattern('/root', Pattern_type.INCLUDE, device=1)), + (Pattern('/', device=1), Pattern('/root', Pattern_type.INCLUDE, device=1)), + ), + ( + (Pattern('/root', Pattern_type.INCLUDE, device=1), Pattern('/', device=1)), + (Pattern('/root', Pattern_type.INCLUDE, device=1), Pattern('/', device=1)), + ), + ), +) +def test_deduplicate_patterns_omits_child_paths_on_the_same_filesystem(patterns, expected_patterns): + assert module.deduplicate_patterns(patterns) == expected_patterns + + +def test_process_patterns_includes_patterns(): + flexmock(module).should_receive('deduplicate_patterns').and_return( + (Pattern('foo'), Pattern('bar')) + ) + flexmock(module).should_receive('device_map_patterns').and_return({}) + flexmock(module).should_receive('expand_patterns').with_args( + (Pattern('foo'), Pattern('bar')), + working_directory='/working', + skip_paths=set(), + ).and_return(()).once() + + assert module.process_patterns( + (Pattern('foo'), Pattern('bar')), + working_directory='/working', + ) == [Pattern('foo'), Pattern('bar')] + + +def test_process_patterns_skips_expand_for_requested_paths(): + skip_paths = {flexmock()} + flexmock(module).should_receive('deduplicate_patterns').and_return( + (Pattern('foo'), Pattern('bar')) + ) + flexmock(module).should_receive('device_map_patterns').and_return({}) + flexmock(module).should_receive('expand_patterns').with_args( + (Pattern('foo'), Pattern('bar')), + working_directory='/working', + skip_paths=skip_paths, + ).and_return(()).once() + + assert module.process_patterns( + (Pattern('foo'), Pattern('bar')), + working_directory='/working', + skip_expand_paths=skip_paths, + ) == [Pattern('foo'), Pattern('bar')] diff --git a/tests/unit/borg/test_create.py b/tests/unit/borg/test_create.py index 80b80ee1..54326641 100644 --- a/tests/unit/borg/test_create.py +++ b/tests/unit/borg/test_create.py @@ -9,131 +9,6 @@ from borgmatic.borg.pattern import Pattern, Pattern_source, Pattern_style, Patte from ..test_verbosity import insert_logging_mock -def test_write_patterns_file_writes_pattern_lines(): - temporary_file = flexmock(name='filename', flush=lambda: None) - temporary_file.should_receive('write').with_args('R /foo\n+ sh:/foo/bar') - flexmock(module.tempfile).should_receive('NamedTemporaryFile').and_return(temporary_file) - - module.write_patterns_file( - [Pattern('/foo'), Pattern('/foo/bar', Pattern_type.INCLUDE, Pattern_style.SHELL)], - borgmatic_runtime_directory='/run/user/0', - ) - - -def test_write_patterns_file_with_empty_exclude_patterns_does_not_raise(): - module.write_patterns_file([], borgmatic_runtime_directory='/run/user/0') - - -def test_write_patterns_file_appends_to_existing(): - patterns_file = flexmock(name='filename', flush=lambda: None) - patterns_file.should_receive('write').with_args('\n') - patterns_file.should_receive('write').with_args('R /foo\n+ /foo/bar') - flexmock(module.tempfile).should_receive('NamedTemporaryFile').never() - - module.write_patterns_file( - [Pattern('/foo'), Pattern('/foo/bar', Pattern_type.INCLUDE)], - borgmatic_runtime_directory='/run/user/0', - patterns_file=patterns_file, - ) - - -def test_make_exclude_flags_includes_exclude_caches_when_true_in_config(): - exclude_flags = module.make_exclude_flags(config={'exclude_caches': True}) - - assert exclude_flags == ('--exclude-caches',) - - -def test_make_exclude_flags_does_not_include_exclude_caches_when_false_in_config(): - exclude_flags = module.make_exclude_flags(config={'exclude_caches': False}) - - assert exclude_flags == () - - -def test_make_exclude_flags_includes_exclude_if_present_when_in_config(): - exclude_flags = module.make_exclude_flags( - config={'exclude_if_present': ['exclude_me', 'also_me']} - ) - - assert exclude_flags == ( - '--exclude-if-present', - 'exclude_me', - '--exclude-if-present', - 'also_me', - ) - - -def test_make_exclude_flags_includes_keep_exclude_tags_when_true_in_config(): - exclude_flags = module.make_exclude_flags(config={'keep_exclude_tags': True}) - - assert exclude_flags == ('--keep-exclude-tags',) - - -def test_make_exclude_flags_does_not_include_keep_exclude_tags_when_false_in_config(): - exclude_flags = module.make_exclude_flags(config={'keep_exclude_tags': False}) - - assert exclude_flags == () - - -def test_make_exclude_flags_includes_exclude_nodump_when_true_in_config(): - exclude_flags = module.make_exclude_flags(config={'exclude_nodump': True}) - - assert exclude_flags == ('--exclude-nodump',) - - -def test_make_exclude_flags_does_not_include_exclude_nodump_when_false_in_config(): - exclude_flags = module.make_exclude_flags(config={'exclude_nodump': False}) - - assert exclude_flags == () - - -def test_make_exclude_flags_is_empty_when_config_has_no_excludes(): - exclude_flags = module.make_exclude_flags(config={}) - - assert exclude_flags == () - - -def test_make_list_filter_flags_with_debug_and_feature_available_includes_plus_and_minus(): - flexmock(module.logger).should_receive('isEnabledFor').and_return(True) - flexmock(module.feature).should_receive('available').and_return(True) - - assert module.make_list_filter_flags(local_borg_version=flexmock(), dry_run=False) == 'AME+-' - - -def test_make_list_filter_flags_with_info_and_feature_available_omits_plus_and_minus(): - flexmock(module.logger).should_receive('isEnabledFor').and_return(False) - flexmock(module.feature).should_receive('available').and_return(True) - - assert module.make_list_filter_flags(local_borg_version=flexmock(), dry_run=False) == 'AME' - - -def test_make_list_filter_flags_with_debug_and_feature_available_and_dry_run_includes_plus_and_minus(): - flexmock(module.logger).should_receive('isEnabledFor').and_return(True) - flexmock(module.feature).should_receive('available').and_return(True) - - assert module.make_list_filter_flags(local_borg_version=flexmock(), dry_run=True) == 'AME+-' - - -def test_make_list_filter_flags_with_info_and_feature_available_and_dry_run_includes_plus_and_minus(): - flexmock(module.logger).should_receive('isEnabledFor').and_return(False) - flexmock(module.feature).should_receive('available').and_return(True) - - assert module.make_list_filter_flags(local_borg_version=flexmock(), dry_run=True) == 'AME+-' - - -def test_make_list_filter_flags_with_debug_and_feature_not_available_includes_x(): - flexmock(module.logger).should_receive('isEnabledFor').and_return(True) - flexmock(module.feature).should_receive('available').and_return(False) - - assert module.make_list_filter_flags(local_borg_version=flexmock(), dry_run=False) == 'AMEx-' - - -def test_make_list_filter_flags_with_info_and_feature_not_available_omits_x(): - flexmock(module.logger).should_receive('isEnabledFor').and_return(False) - flexmock(module.feature).should_receive('available').and_return(False) - - assert module.make_list_filter_flags(local_borg_version=flexmock(), dry_run=False) == 'AME-' - - @pytest.mark.parametrize( 'character_device,block_device,fifo,expected_result', ( @@ -326,10 +201,10 @@ REPO_ARCHIVE = (f'repo::{DEFAULT_ARCHIVE_NAME}',) def test_make_base_create_produces_borg_command(): flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None) - flexmock(module).should_receive('write_patterns_file').and_return(None) - flexmock(module).should_receive('make_list_filter_flags').and_return('FOO') + flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('FOO') flexmock(module.feature).should_receive('available').and_return(True) - flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(()) flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( (f'repo::{DEFAULT_ARCHIVE_NAME}',) ) @@ -355,16 +230,16 @@ def test_make_base_create_produces_borg_command(): def test_make_base_create_command_includes_patterns_file_in_borg_command(): flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None) mock_pattern_file = flexmock(name='/tmp/patterns') - flexmock(module).should_receive('write_patterns_file').and_return(mock_pattern_file).and_return( - None - ) - flexmock(module).should_receive('make_list_filter_flags').and_return('FOO') + flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return( + mock_pattern_file + ).and_return(None) + flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('FOO') flexmock(module.flags).should_receive('get_default_archive_name_format').and_return( '{hostname}' ) flexmock(module.feature).should_receive('available').and_return(True) pattern_flags = ('--patterns-from', mock_pattern_file.name) - flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(()) flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( (f'repo::{DEFAULT_ARCHIVE_NAME}',) ) @@ -390,13 +265,13 @@ def test_make_base_create_command_includes_patterns_file_in_borg_command(): def test_make_base_create_command_with_store_config_false_omits_config_files(): flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None) - flexmock(module).should_receive('write_patterns_file').and_return(None) - flexmock(module).should_receive('make_list_filter_flags').and_return('FOO') + flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('FOO') flexmock(module.flags).should_receive('get_default_archive_name_format').and_return( '{hostname}' ) flexmock(module.feature).should_receive('available').and_return(True) - flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(()) flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( (f'repo::{DEFAULT_ARCHIVE_NAME}',) ) @@ -455,13 +330,13 @@ def test_make_base_create_command_includes_configuration_option_as_command_flag( option_name, option_value, feature_available, option_flags ): flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None) - flexmock(module).should_receive('write_patterns_file').and_return(None) - flexmock(module).should_receive('make_list_filter_flags').and_return('FOO') + flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('FOO') flexmock(module.flags).should_receive('get_default_archive_name_format').and_return( '{hostname}' ) flexmock(module.feature).should_receive('available').and_return(feature_available) - flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(()) flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( (f'repo::{DEFAULT_ARCHIVE_NAME}',) ) @@ -487,13 +362,13 @@ def test_make_base_create_command_includes_configuration_option_as_command_flag( def test_make_base_create_command_includes_dry_run_in_borg_command(): flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None) - flexmock(module).should_receive('write_patterns_file').and_return(None) - flexmock(module).should_receive('make_list_filter_flags').and_return('FOO') + flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('FOO') flexmock(module.flags).should_receive('get_default_archive_name_format').and_return( '{hostname}' ) flexmock(module.feature).should_receive('available').and_return(True) - flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(()) flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( (f'repo::{DEFAULT_ARCHIVE_NAME}',) ) @@ -519,13 +394,13 @@ def test_make_base_create_command_includes_dry_run_in_borg_command(): def test_make_base_create_command_includes_local_path_in_borg_command(): flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None) - flexmock(module).should_receive('write_patterns_file').and_return(None) - flexmock(module).should_receive('make_list_filter_flags').and_return('FOO') + flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('FOO') flexmock(module.flags).should_receive('get_default_archive_name_format').and_return( '{hostname}' ) flexmock(module.feature).should_receive('available').and_return(True) - flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(()) flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( (f'repo::{DEFAULT_ARCHIVE_NAME}',) ) @@ -551,13 +426,13 @@ def test_make_base_create_command_includes_local_path_in_borg_command(): def test_make_base_create_command_includes_remote_path_in_borg_command(): flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None) - flexmock(module).should_receive('write_patterns_file').and_return(None) - flexmock(module).should_receive('make_list_filter_flags').and_return('FOO') + flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('FOO') flexmock(module.flags).should_receive('get_default_archive_name_format').and_return( '{hostname}' ) flexmock(module.feature).should_receive('available').and_return(True) - flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(()) flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( (f'repo::{DEFAULT_ARCHIVE_NAME}',) ) @@ -583,13 +458,13 @@ def test_make_base_create_command_includes_remote_path_in_borg_command(): def test_make_base_create_command_includes_log_json_in_borg_command(): flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None) - flexmock(module).should_receive('write_patterns_file').and_return(None) - flexmock(module).should_receive('make_list_filter_flags').and_return('FOO') + flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('FOO') flexmock(module.flags).should_receive('get_default_archive_name_format').and_return( '{hostname}' ) flexmock(module.feature).should_receive('available').and_return(True) - flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(()) flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( (f'repo::{DEFAULT_ARCHIVE_NAME}',) ) @@ -614,13 +489,13 @@ def test_make_base_create_command_includes_log_json_in_borg_command(): def test_make_base_create_command_includes_list_flags_in_borg_command(): flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None) - flexmock(module).should_receive('write_patterns_file').and_return(None) - flexmock(module).should_receive('make_list_filter_flags').and_return('FOO') + flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('FOO') flexmock(module.flags).should_receive('get_default_archive_name_format').and_return( '{hostname}' ) flexmock(module.feature).should_receive('available').and_return(True) - flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(()) flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( (f'repo::{DEFAULT_ARCHIVE_NAME}',) ) @@ -648,22 +523,22 @@ def test_make_base_create_command_with_stream_processes_ignores_read_special_fal patterns = [Pattern('foo'), Pattern('bar')] patterns_file = flexmock(name='patterns') flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None) - flexmock(module).should_receive('write_patterns_file').with_args( + flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').with_args( patterns, '/run/borgmatic' ).and_return(patterns_file) - flexmock(module).should_receive('make_list_filter_flags').and_return('FOO') + flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('FOO') flexmock(module.flags).should_receive('get_default_archive_name_format').and_return( '{hostname}' ) flexmock(module.feature).should_receive('available').and_return(True) - flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(()) flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( (f'repo::{DEFAULT_ARCHIVE_NAME}',) ) flexmock(module.logger).should_receive('warning').twice() flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('collect_special_file_paths').and_return(('/dev/null',)).once() - flexmock(module).should_receive('write_patterns_file').with_args( + flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').with_args( ( Pattern( '/dev/null', @@ -675,7 +550,7 @@ def test_make_base_create_command_with_stream_processes_ignores_read_special_fal '/run/borgmatic', patterns_file=patterns_file, ).and_return(patterns_file).once() - flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(()) (create_flags, create_positional_arguments, pattern_file) = module.make_base_create_command( dry_run=False, @@ -699,22 +574,22 @@ def test_make_base_create_command_with_stream_processes_ignores_read_special_fal def test_make_base_create_command_without_patterns_and_with_stream_processes_ignores_read_special_false_and_excludes_special_files(): flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None) - flexmock(module).should_receive('write_patterns_file').with_args( + flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').with_args( [], '/run/borgmatic' ).and_return(None) - flexmock(module).should_receive('make_list_filter_flags').and_return('FOO') + flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('FOO') flexmock(module.flags).should_receive('get_default_archive_name_format').and_return( '{hostname}' ) flexmock(module.feature).should_receive('available').and_return(True) - flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(()) flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( (f'repo::{DEFAULT_ARCHIVE_NAME}',) ) flexmock(module.logger).should_receive('warning').twice() flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('collect_special_file_paths').and_return(('/dev/null',)).once() - flexmock(module).should_receive('write_patterns_file').with_args( + flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').with_args( ( Pattern( '/dev/null', @@ -726,7 +601,7 @@ def test_make_base_create_command_without_patterns_and_with_stream_processes_ign '/run/borgmatic', patterns_file=None, ).and_return(flexmock(name='patterns')).once() - flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(()) (create_flags, create_positional_arguments, pattern_file) = module.make_base_create_command( dry_run=False, @@ -750,13 +625,13 @@ def test_make_base_create_command_without_patterns_and_with_stream_processes_ign def test_make_base_create_command_with_stream_processes_and_read_special_true_skips_special_files_excludes(): flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None) - flexmock(module).should_receive('write_patterns_file').and_return(None) - flexmock(module).should_receive('make_list_filter_flags').and_return('FOO') + flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('FOO') flexmock(module.flags).should_receive('get_default_archive_name_format').and_return( '{hostname}' ) flexmock(module.feature).should_receive('available').and_return(True) - flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(()) flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( (f'repo::{DEFAULT_ARCHIVE_NAME}',) ) @@ -785,13 +660,13 @@ def test_make_base_create_command_with_stream_processes_and_read_special_true_sk def test_make_base_create_command_includes_archive_name_format_in_borg_command(): flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None) - flexmock(module).should_receive('write_patterns_file').and_return(None) - flexmock(module).should_receive('make_list_filter_flags').and_return('FOO') + flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('FOO') flexmock(module.flags).should_receive('get_default_archive_name_format').and_return( '{hostname}' ) flexmock(module.feature).should_receive('available').and_return(True) - flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(()) flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( ('repo::ARCHIVE_NAME',) ) @@ -817,13 +692,13 @@ def test_make_base_create_command_includes_archive_name_format_in_borg_command() def test_make_base_create_command_includes_default_archive_name_format_in_borg_command(): flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None) - flexmock(module).should_receive('write_patterns_file').and_return(None) - flexmock(module).should_receive('make_list_filter_flags').and_return('FOO') + flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('FOO') flexmock(module.flags).should_receive('get_default_archive_name_format').and_return( '{hostname}' ) flexmock(module.feature).should_receive('available').and_return(True) - flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(()) flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( ('repo::{hostname}',) ) @@ -848,13 +723,13 @@ def test_make_base_create_command_includes_default_archive_name_format_in_borg_c def test_make_base_create_command_includes_archive_name_format_with_placeholders_in_borg_command(): repository_archive_pattern = 'repo::Documents_{hostname}-{now}' # noqa: FS003 - flexmock(module).should_receive('write_patterns_file').and_return(None) - flexmock(module).should_receive('make_list_filter_flags').and_return('FOO') + flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('FOO') flexmock(module.flags).should_receive('get_default_archive_name_format').and_return( '{hostname}' ) flexmock(module.feature).should_receive('available').and_return(True) - flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(()) flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( (repository_archive_pattern,) ) @@ -880,13 +755,13 @@ def test_make_base_create_command_includes_archive_name_format_with_placeholders def test_make_base_create_command_includes_repository_and_archive_name_format_with_placeholders_in_borg_command(): repository_archive_pattern = '{fqdn}::Documents_{hostname}-{now}' # noqa: FS003 - flexmock(module).should_receive('write_patterns_file').and_return(None) - flexmock(module).should_receive('make_list_filter_flags').and_return('FOO') + flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('FOO') flexmock(module.flags).should_receive('get_default_archive_name_format').and_return( '{hostname}' ) flexmock(module.feature).should_receive('available').and_return(True) - flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(()) flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( (repository_archive_pattern,) ) @@ -912,13 +787,13 @@ def test_make_base_create_command_includes_repository_and_archive_name_format_wi def test_make_base_create_command_includes_extra_borg_options_in_borg_command(): flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None) - flexmock(module).should_receive('write_patterns_file').and_return(None) - flexmock(module).should_receive('make_list_filter_flags').and_return('FOO') + flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('FOO') flexmock(module.flags).should_receive('get_default_archive_name_format').and_return( '{hostname}' ) flexmock(module.feature).should_receive('available').and_return(True) - flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(()) flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( (f'repo::{DEFAULT_ARCHIVE_NAME}',) ) @@ -944,7 +819,9 @@ def test_make_base_create_command_includes_extra_borg_options_in_borg_command(): def test_make_base_create_command_with_non_existent_directory_and_source_directories_must_exist_raises(): flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None) - flexmock(module).should_receive('check_all_root_patterns_exist').and_raise(ValueError) + flexmock(module.borgmatic.borg.pattern).should_receive( + 'check_all_root_patterns_exist' + ).and_raise(ValueError) with pytest.raises(ValueError): module.make_base_create_command( @@ -1570,22 +1447,3 @@ def test_create_archive_calls_borg_with_working_directory(): global_arguments=flexmock(log_json=False), borgmatic_runtime_directory='/borgmatic/run', ) - - -def test_check_all_root_patterns_exist_with_existent_pattern_path_does_not_raise(): - flexmock(module.os.path).should_receive('exists').and_return(True) - - module.check_all_root_patterns_exist([Pattern('foo')]) - - -def test_check_all_root_patterns_exist_with_non_root_pattern_skips_existence_check(): - flexmock(module.os.path).should_receive('exists').never() - - module.check_all_root_patterns_exist([Pattern('foo', Pattern_type.INCLUDE)]) - - -def test_check_all_root_patterns_exist_with_non_existent_pattern_path_raises(): - flexmock(module.os.path).should_receive('exists').and_return(False) - - with pytest.raises(ValueError): - module.check_all_root_patterns_exist([Pattern('foo')]) diff --git a/tests/unit/borg/test_flags.py b/tests/unit/borg/test_flags.py index 63dbab35..f7bf2f6a 100644 --- a/tests/unit/borg/test_flags.py +++ b/tests/unit/borg/test_flags.py @@ -327,3 +327,100 @@ def test_omit_flag_and_value_without_flag_present_passes_through_arguments(): 'create', '--other', ) + + +def test_make_exclude_flags_includes_exclude_caches_when_true_in_config(): + exclude_flags = module.make_exclude_flags(config={'exclude_caches': True}) + + assert exclude_flags == ('--exclude-caches',) + + +def test_make_exclude_flags_does_not_include_exclude_caches_when_false_in_config(): + exclude_flags = module.make_exclude_flags(config={'exclude_caches': False}) + + assert exclude_flags == () + + +def test_make_exclude_flags_includes_exclude_if_present_when_in_config(): + exclude_flags = module.make_exclude_flags( + config={'exclude_if_present': ['exclude_me', 'also_me']} + ) + + assert exclude_flags == ( + '--exclude-if-present', + 'exclude_me', + '--exclude-if-present', + 'also_me', + ) + + +def test_make_exclude_flags_includes_keep_exclude_tags_when_true_in_config(): + exclude_flags = module.make_exclude_flags(config={'keep_exclude_tags': True}) + + assert exclude_flags == ('--keep-exclude-tags',) + + +def test_make_exclude_flags_does_not_include_keep_exclude_tags_when_false_in_config(): + exclude_flags = module.make_exclude_flags(config={'keep_exclude_tags': False}) + + assert exclude_flags == () + + +def test_make_exclude_flags_includes_exclude_nodump_when_true_in_config(): + exclude_flags = module.make_exclude_flags(config={'exclude_nodump': True}) + + assert exclude_flags == ('--exclude-nodump',) + + +def test_make_exclude_flags_does_not_include_exclude_nodump_when_false_in_config(): + exclude_flags = module.make_exclude_flags(config={'exclude_nodump': False}) + + assert exclude_flags == () + + +def test_make_exclude_flags_is_empty_when_config_has_no_excludes(): + exclude_flags = module.make_exclude_flags(config={}) + + assert exclude_flags == () + + +def test_make_list_filter_flags_with_debug_and_feature_available_includes_plus_and_minus(): + flexmock(module.logger).should_receive('isEnabledFor').and_return(True) + flexmock(module.feature).should_receive('available').and_return(True) + + assert module.make_list_filter_flags(local_borg_version=flexmock(), dry_run=False) == 'AME+-' + + +def test_make_list_filter_flags_with_info_and_feature_available_omits_plus_and_minus(): + flexmock(module.logger).should_receive('isEnabledFor').and_return(False) + flexmock(module.feature).should_receive('available').and_return(True) + + assert module.make_list_filter_flags(local_borg_version=flexmock(), dry_run=False) == 'AME' + + +def test_make_list_filter_flags_with_debug_and_feature_available_and_dry_run_includes_plus_and_minus(): + flexmock(module.logger).should_receive('isEnabledFor').and_return(True) + flexmock(module.feature).should_receive('available').and_return(True) + + assert module.make_list_filter_flags(local_borg_version=flexmock(), dry_run=True) == 'AME+-' + + +def test_make_list_filter_flags_with_info_and_feature_available_and_dry_run_includes_plus_and_minus(): + flexmock(module.logger).should_receive('isEnabledFor').and_return(False) + flexmock(module.feature).should_receive('available').and_return(True) + + assert module.make_list_filter_flags(local_borg_version=flexmock(), dry_run=True) == 'AME+-' + + +def test_make_list_filter_flags_with_debug_and_feature_not_available_includes_x(): + flexmock(module.logger).should_receive('isEnabledFor').and_return(True) + flexmock(module.feature).should_receive('available').and_return(False) + + assert module.make_list_filter_flags(local_borg_version=flexmock(), dry_run=False) == 'AMEx-' + + +def test_make_list_filter_flags_with_info_and_feature_not_available_omits_x(): + flexmock(module.logger).should_receive('isEnabledFor').and_return(False) + flexmock(module.feature).should_receive('available').and_return(False) + + assert module.make_list_filter_flags(local_borg_version=flexmock(), dry_run=False) == 'AME-' diff --git a/tests/unit/borg/test_pattern.py b/tests/unit/borg/test_pattern.py new file mode 100644 index 00000000..7ed5158c --- /dev/null +++ b/tests/unit/borg/test_pattern.py @@ -0,0 +1,52 @@ +import pytest +from flexmock import flexmock + +from borgmatic.borg import pattern as module +from borgmatic.borg.pattern import Pattern, Pattern_style, Pattern_type + + +def test_write_patterns_file_writes_pattern_lines(): + temporary_file = flexmock(name='filename', flush=lambda: None) + temporary_file.should_receive('write').with_args('R /foo\n+ sh:/foo/bar') + flexmock(module.tempfile).should_receive('NamedTemporaryFile').and_return(temporary_file) + + module.write_patterns_file( + [Pattern('/foo'), Pattern('/foo/bar', Pattern_type.INCLUDE, Pattern_style.SHELL)], + borgmatic_runtime_directory='/run/user/0', + ) + + +def test_write_patterns_file_with_empty_exclude_patterns_does_not_raise(): + module.write_patterns_file([], borgmatic_runtime_directory='/run/user/0') + + +def test_write_patterns_file_appends_to_existing(): + patterns_file = flexmock(name='filename', flush=lambda: None) + patterns_file.should_receive('write').with_args('\n') + patterns_file.should_receive('write').with_args('R /foo\n+ /foo/bar') + flexmock(module.tempfile).should_receive('NamedTemporaryFile').never() + + module.write_patterns_file( + [Pattern('/foo'), Pattern('/foo/bar', Pattern_type.INCLUDE)], + borgmatic_runtime_directory='/run/user/0', + patterns_file=patterns_file, + ) + + +def test_check_all_root_patterns_exist_with_existent_pattern_path_does_not_raise(): + flexmock(module.os.path).should_receive('exists').and_return(True) + + module.check_all_root_patterns_exist([Pattern('foo')]) + + +def test_check_all_root_patterns_exist_with_non_root_pattern_skips_existence_check(): + flexmock(module.os.path).should_receive('exists').never() + + module.check_all_root_patterns_exist([Pattern('foo', Pattern_type.INCLUDE)]) + + +def test_check_all_root_patterns_exist_with_non_existent_pattern_path_raises(): + flexmock(module.os.path).should_receive('exists').and_return(False) + + with pytest.raises(ValueError): + module.check_all_root_patterns_exist([Pattern('foo')]) diff --git a/tests/unit/borg/test_recreate.py b/tests/unit/borg/test_recreate.py index d0d527bf..5abfc5cb 100644 --- a/tests/unit/borg/test_recreate.py +++ b/tests/unit/borg/test_recreate.py @@ -21,9 +21,9 @@ def insert_execute_command_mock(command, working_directory=None, borg_exit_codes def test_recreate_archive_dry_run_skips_execution(): - flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(()) - flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None) - flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('') + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('') flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(()) flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True) flexmock(module.borgmatic.borg.flags).should_receive( @@ -59,9 +59,9 @@ def test_recreate_archive_dry_run_skips_execution(): def test_recreate_calls_borg_with_required_flags(): - flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(()) - flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None) - flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('') + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('') flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(()) flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True) flexmock(module.borgmatic.borg.flags).should_receive( @@ -94,9 +94,9 @@ def test_recreate_calls_borg_with_required_flags(): def test_recreate_with_remote_path(): - flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(()) - flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None) - flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('') + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('') flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(()) flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True) flexmock(module.borgmatic.borg.flags).should_receive( @@ -129,9 +129,9 @@ def test_recreate_with_remote_path(): def test_recreate_with_lock_wait(): - flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(()) - flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None) - flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('') + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('') flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(()) flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True) flexmock(module.borgmatic.borg.flags).should_receive( @@ -163,9 +163,9 @@ def test_recreate_with_lock_wait(): def test_recreate_with_log_info(): - flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(()) - flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None) - flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('') + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('') flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(()) flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True) flexmock(module.borgmatic.borg.flags).should_receive( @@ -199,9 +199,9 @@ def test_recreate_with_log_info(): def test_recreate_with_log_debug(): - flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(()) - flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None) - flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('') + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('') flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(()) flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True) flexmock(module.borgmatic.borg.flags).should_receive( @@ -234,9 +234,9 @@ def test_recreate_with_log_debug(): def test_recreate_with_log_json(): - flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(()) - flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None) - flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('') + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('') flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(()) flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True) flexmock(module.borgmatic.borg.flags).should_receive( @@ -268,8 +268,8 @@ def test_recreate_with_log_json(): def test_recreate_with_list_config_calls_borg_with_list_flag(): - flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(()) - flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None) flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(()) flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True) flexmock(module.borgmatic.borg.flags).should_receive( @@ -280,7 +280,9 @@ def test_recreate_with_list_config_calls_borg_with_list_flag(): 'repo', ) ) - flexmock(module).should_receive('make_list_filter_flags').and_return('AME+-') + flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return( + 'AME+-' + ) insert_execute_command_mock( ('borg', 'recreate', '--list', '--filter', 'AME+-', '--repo', 'repo') ) @@ -304,8 +306,8 @@ def test_recreate_with_list_config_calls_borg_with_list_flag(): def test_recreate_with_patterns_from_flag(): - flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(()) - flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('') + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('') flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(()) flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True) flexmock(module.borgmatic.borg.flags).should_receive( @@ -341,8 +343,8 @@ def test_recreate_with_patterns_from_flag(): def test_recreate_with_exclude_flags(): - flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None) - flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('') + flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('') flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(()) flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True) flexmock(module.borgmatic.borg.flags).should_receive( @@ -353,7 +355,9 @@ def test_recreate_with_exclude_flags(): 'repo', ) ) - flexmock(module).should_receive('make_exclude_flags').and_return(('--exclude', 'pattern')) + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return( + ('--exclude', 'pattern') + ) insert_execute_command_mock(('borg', 'recreate', '--exclude', 'pattern', '--repo', 'repo')) module.recreate_archive( @@ -375,9 +379,9 @@ def test_recreate_with_exclude_flags(): def test_recreate_with_target_flag(): - flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(()) - flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None) - flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('') + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('') flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(()) flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True) flexmock(module.borgmatic.borg.flags).should_receive( @@ -409,9 +413,9 @@ def test_recreate_with_target_flag(): def test_recreate_with_comment_flag(): - flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(()) - flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None) - flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('') + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('') flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(()) flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True) flexmock(module.borgmatic.borg.flags).should_receive( @@ -445,9 +449,9 @@ def test_recreate_with_comment_flag(): def test_recreate_with_timestamp_flag(): - flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(()) - flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None) - flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('') + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('') flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(()) flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True) flexmock(module.borgmatic.borg.flags).should_receive( @@ -481,9 +485,9 @@ def test_recreate_with_timestamp_flag(): def test_recreate_with_compression_flag(): - flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(()) - flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None) - flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('') + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('') flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(()) flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True) flexmock(module.borgmatic.borg.flags).should_receive( @@ -515,9 +519,9 @@ def test_recreate_with_compression_flag(): def test_recreate_with_chunker_params_flag(): - flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(()) - flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None) - flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('') + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('') flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(()) flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True) flexmock(module.borgmatic.borg.flags).should_receive( @@ -551,9 +555,9 @@ def test_recreate_with_chunker_params_flag(): def test_recreate_with_recompress_flag(): - flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(()) - flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None) - flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('') + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('') flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(()) flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True) flexmock(module.borgmatic.borg.flags).should_receive( @@ -585,9 +589,9 @@ def test_recreate_with_recompress_flag(): def test_recreate_with_match_archives_star(): - flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(()) - flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None) - flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('') + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('') flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(()) flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True) flexmock(module.borgmatic.borg.flags).should_receive( @@ -619,9 +623,9 @@ def test_recreate_with_match_archives_star(): def test_recreate_with_match_archives_regex(): - flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(()) - flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None) - flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('') + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('') flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(()) flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True) flexmock(module.borgmatic.borg.flags).should_receive( @@ -653,9 +657,9 @@ def test_recreate_with_match_archives_regex(): def test_recreate_with_match_archives_shell(): - flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(()) - flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None) - flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('') + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('') flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(()) flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True) flexmock(module.borgmatic.borg.flags).should_receive( @@ -687,9 +691,9 @@ def test_recreate_with_match_archives_shell(): def test_recreate_with_match_archives_and_feature_available_calls_borg_with_match_archives(): - flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(()) - flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None) - flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('') + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('') flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').with_args( 'foo-*', None, '1.2.3' ).and_return(('--match-archives', 'foo-*')) @@ -719,9 +723,9 @@ def test_recreate_with_match_archives_and_feature_available_calls_borg_with_matc def test_recreate_with_archives_flag_and_feature_available_calls_borg_with_match_archives(): - flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(()) - flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None) - flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('') + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('') flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').with_args( 'archive', None, '1.2.3' ).and_return(('--match-archives', 'archive')) @@ -753,9 +757,9 @@ def test_recreate_with_archives_flag_and_feature_available_calls_borg_with_match def test_recreate_with_match_archives_and_feature_not_available_calls_borg_without_match_archives(): - flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(()) - flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None) - flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('') + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('') flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').never() flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(False) flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return( @@ -783,9 +787,9 @@ def test_recreate_with_match_archives_and_feature_not_available_calls_borg_witho def test_recreate_with_archives_flags_and_feature_not_available_calls_borg_with_combined_repo_and_archive(): - flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(()) - flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None) - flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('') + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('') flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').never() flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(False) flexmock(module.borgmatic.borg.flags).should_receive(