From ae036aebd76de470b0e1d087d6893859969e9ec8 Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Mon, 3 Oct 2022 12:58:13 -0700 Subject: [PATCH] When the "read_special" option is true or database hooks are enabled, auto-exclude special files for a "create" action to prevent Borg from hanging (#587). --- NEWS | 2 + borgmatic/borg/create.py | 114 ++++++++-- borgmatic/config/override.py | 4 +- borgmatic/execute.py | 2 +- tests/end-to-end/test_database.py | 32 ++- tests/unit/borg/test_create.py | 340 ++++++++++++++++++++++++------ tests/unit/test_execute.py | 12 +- 7 files changed, 404 insertions(+), 102 deletions(-) diff --git a/NEWS b/NEWS index d82da05..56126cb 100644 --- a/NEWS +++ b/NEWS @@ -1,4 +1,6 @@ 1.7.3.dev0 + * #587: When the "read_special" option is true or database hooks are enabled, auto-exclude special + files for a "create" action to prevent Borg from hanging. * #587: Warn when ignoring a configured "read_special" value of false, as true is needed when database hooks are enabled. diff --git a/borgmatic/borg/create.py b/borgmatic/borg/create.py index bfc4406..2f9aea7 100644 --- a/borgmatic/borg/create.py +++ b/borgmatic/borg/create.py @@ -3,6 +3,7 @@ import itertools import logging import os import pathlib +import stat import tempfile from borgmatic.borg import environment, feature, flags, state @@ -104,16 +105,21 @@ def deduplicate_directories(directory_devices, additional_directory_devices): return tuple(sorted(deduplicated)) -def write_pattern_file(patterns=None, sources=None): +def write_pattern_file(patterns=None, sources=None, pattern_file=None): ''' Given a sequence of patterns and an optional sequence of source directories, write them to a named temporary file (with the source directories as additional roots) and return the file. + If an optional open pattern file is given, overwrite it instead of making a new temporary file. Return None if no patterns are provided. ''' if not patterns: return None - pattern_file = tempfile.NamedTemporaryFile('w') + if pattern_file is None: + pattern_file = tempfile.NamedTemporaryFile('w') + else: + pattern_file.seek(0) + pattern_file.write( '\n'.join(tuple(patterns) + tuple(f'R {source}' for source in (sources or []))) ) @@ -187,7 +193,7 @@ def make_exclude_flags(location_config, exclude_filename=None): DEFAULT_ARCHIVE_NAME_FORMAT = '{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}' -def borgmatic_source_directories(borgmatic_source_directory): +def collect_borgmatic_source_directories(borgmatic_source_directory): ''' Return a list of borgmatic-specific source directories used for state like database backups. ''' @@ -218,6 +224,58 @@ def pattern_root_directories(patterns=None): ] +def special_file(path): + ''' + Return whether the given path is a special file (character device, block device, or named pipe + / FIFO). + ''' + mode = os.stat(path).st_mode + return stat.S_ISCHR(mode) or stat.S_ISBLK(mode) or stat.S_ISFIFO(mode) + + +def any_parent_directories(path, candidate_parents): + ''' + Return whether any of the given candidate parent directories are an actual parent of the given + path. This includes grandparents, etc. + ''' + for parent in candidate_parents: + if pathlib.PurePosixPath(parent) in pathlib.PurePath(path).parents: + return True + + return False + + +def collect_special_file_paths( + create_command, local_path, working_directory, borg_environment, skip_directories +): + ''' + Given a Borg create command as a tuple, a local Borg path, a working directory, and a dict of + environment variables to pass to Borg, and a sequence of parent directories to skip, collect the + paths for any special files (character devices, block devices, and named pipes / FIFOs) that + Borg would encounter during a create. These are all paths that could cause Borg to hang if its + --read-special flag is used. + ''' + paths_output = execute_command( + create_command + ('--dry-run', '--list'), + output_log_level=None, + borg_local_path=local_path, + working_directory=working_directory, + extra_environment=borg_environment, + ) + + paths = tuple( + path_line.split(' ', 1)[1] + for path_line in paths_output.split('\n') + if path_line and path_line.startswith('- ') + ) + + return tuple( + path + for path in paths + if special_file(path) and not any_parent_directories(path, skip_directories) + ) + + def create_archive( dry_run, repository, @@ -239,11 +297,13 @@ def create_archive( If a sequence of stream processes is given (instances of subprocess.Popen), then execute the create command while also triggering the given processes to produce output. ''' + borgmatic_source_directories = expand_directories( + collect_borgmatic_source_directories(location_config.get('borgmatic_source_directory')) + ) sources = deduplicate_directories( map_directories_to_devices( expand_directories( - location_config.get('source_directories', []) - + borgmatic_source_directories(location_config.get('borgmatic_source_directory')) + tuple(location_config.get('source_directories', ())) + borgmatic_source_directories ) ), additional_directory_devices=map_directories_to_devices( @@ -265,6 +325,7 @@ def create_archive( upload_rate_limit = storage_config.get('upload_rate_limit', None) umask = storage_config.get('umask', None) lock_wait = storage_config.get('lock_wait', None) + read_special = True if (location_config.get('read_special') or stream_processes) else False files_cache = location_config.get('files_cache') archive_name_format = storage_config.get('archive_name_format', DEFAULT_ARCHIVE_NAME_FORMAT) extra_borg_options = storage_config.get('extra_borg_options', {}).get('create', '') @@ -300,7 +361,7 @@ def create_archive( f'{repository}: Ignoring configured "read_special" value of false, as true is needed for database hooks.' ) - full_command = ( + create_command = ( tuple(local_path.split(' ')) + ('create',) + make_pattern_flags(location_config, pattern_file.name if pattern_file else None) @@ -318,19 +379,14 @@ def create_archive( + atime_flags + (('--noctime',) if location_config.get('ctime') is False else ()) + (('--nobirthtime',) if location_config.get('birthtime') is False else ()) - + (('--read-special',) if (location_config.get('read_special') or stream_processes) else ()) + + (('--read-special',) if read_special else ()) + noflags_flags + (('--files-cache', files_cache) if files_cache else ()) + (('--remote-path', remote_path) if remote_path else ()) + (('--umask', str(umask)) if umask else ()) + (('--lock-wait', str(lock_wait)) if lock_wait else ()) + (('--list', '--filter', 'AME-') if list_files and not json and not progress else ()) - + (('--info',) if logger.getEffectiveLevel() == logging.INFO and not json else ()) - + (('--stats',) if stats and not json and not dry_run else ()) - + (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) and not json else ()) + (('--dry-run',) if dry_run else ()) - + (('--progress',) if progress else ()) - + (('--json',) if json else ()) + (tuple(extra_borg_options.split(' ')) if extra_borg_options else ()) + flags.make_repository_archive_flags(repository, archive_name_format, local_borg_version) + (sources if not pattern_file else ()) @@ -349,9 +405,39 @@ def create_archive( borg_environment = environment.make_environment(storage_config) + # If read_special is enabled, exclude files that might cause Borg to hang. + if read_special: + special_file_paths = collect_special_file_paths( + create_command, + local_path, + working_directory, + borg_environment, + skip_directories=borgmatic_source_directories, + ) + logger.warning( + f'{repository}: Excluding special files to prevent Borg from hanging: {", ".join(special_file_paths)}' + ) + + exclude_file = write_pattern_file( + expand_home_directories( + tuple(location_config.get('exclude_patterns') or ()) + special_file_paths + ), + pattern_file=exclude_file, + ) + if exclude_file: + create_command += make_exclude_flags(location_config, exclude_file.name) + + create_command += ( + (('--info',) if logger.getEffectiveLevel() == logging.INFO and not json else ()) + + (('--stats',) if stats and not json and not dry_run else ()) + + (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) and not json else ()) + + (('--progress',) if progress else ()) + + (('--json',) if json else ()) + ) + if stream_processes: return execute_command_with_processes( - full_command, + create_command, stream_processes, output_log_level, output_file, @@ -361,7 +447,7 @@ def create_archive( ) return execute_command( - full_command, + create_command, output_log_level, output_file, borg_local_path=local_path, diff --git a/borgmatic/config/override.py b/borgmatic/config/override.py index 8596cb3..8b2a1ab 100644 --- a/borgmatic/config/override.py +++ b/borgmatic/config/override.py @@ -70,8 +70,8 @@ def parse_overrides(raw_overrides): def apply_overrides(config, raw_overrides): ''' - Given a sequence of configuration file override strings in the form of "section.option=value" - and a configuration dict, parse each override and set it the configuration dict. + Given a configuration dict and a sequence of configuration file override strings in the form of + "section.option=value", parse each override and set it the configuration dict. ''' overrides = parse_overrides(raw_overrides) diff --git a/borgmatic/execute.py b/borgmatic/execute.py index 1ecb84b..00a5fc9 100644 --- a/borgmatic/execute.py +++ b/borgmatic/execute.py @@ -197,7 +197,7 @@ def execute_command( if output_log_level is None: output = subprocess.check_output( - command, shell=shell, env=environment, cwd=working_directory + command, stderr=subprocess.STDOUT, shell=shell, env=environment, cwd=working_directory ) return output.decode() if output is not None else None diff --git a/tests/end-to-end/test_database.py b/tests/end-to-end/test_database.py index c7f6366..c6ce0a6 100644 --- a/tests/end-to-end/test_database.py +++ b/tests/end-to-end/test_database.py @@ -9,20 +9,24 @@ import pytest def write_configuration( - config_path, repository_path, borgmatic_source_directory, postgresql_dump_format='custom' + source_directory, + config_path, + repository_path, + borgmatic_source_directory, + postgresql_dump_format='custom', ): ''' Write out borgmatic configuration into a file at the config path. Set the options so as to work for testing. This includes injecting the given repository path, borgmatic source directory for storing database dumps, dump format (for PostgreSQL), and encryption passphrase. ''' - config = ''' + config = f''' location: source_directories: - - {} + - {source_directory} repositories: - - {} - borgmatic_source_directory: {} + - {repository_path} + borgmatic_source_directory: {borgmatic_source_directory} storage: encryption_passphrase: "test" @@ -33,7 +37,7 @@ hooks: hostname: postgresql username: postgres password: test - format: {} + format: {postgresql_dump_format} - name: all hostname: postgresql username: postgres @@ -57,9 +61,7 @@ hooks: hostname: mongodb username: root password: test -'''.format( - config_path, repository_path, borgmatic_source_directory, postgresql_dump_format - ) +''' with open(config_path, 'w') as config_file: config_file.write(config) @@ -71,11 +73,16 @@ def test_database_dump_and_restore(): repository_path = os.path.join(temporary_directory, 'test.borg') borgmatic_source_directory = os.path.join(temporary_directory, '.borgmatic') + # Write out a special file to ensure that it gets properly excluded and Borg doesn't hang on it. + os.mkfifo(os.path.join(temporary_directory, 'special_file')) + original_working_directory = os.getcwd() try: config_path = os.path.join(temporary_directory, 'test.yaml') - write_configuration(config_path, repository_path, borgmatic_source_directory) + write_configuration( + temporary_directory, config_path, repository_path, borgmatic_source_directory + ) subprocess.check_call( ['borgmatic', '-v', '2', '--config', config_path, 'init', '--encryption', 'repokey'] @@ -114,6 +121,7 @@ def test_database_dump_and_restore_with_directory_format(): try: config_path = os.path.join(temporary_directory, 'test.yaml') write_configuration( + temporary_directory, config_path, repository_path, borgmatic_source_directory, @@ -146,7 +154,9 @@ def test_database_dump_with_error_causes_borgmatic_to_exit(): try: config_path = os.path.join(temporary_directory, 'test.yaml') - write_configuration(config_path, repository_path, borgmatic_source_directory) + write_configuration( + temporary_directory, config_path, repository_path, borgmatic_source_directory + ) subprocess.check_call( ['borgmatic', '-v', '2', '--config', config_path, 'init', '--encryption', 'repokey'] diff --git a/tests/unit/borg/test_create.py b/tests/unit/borg/test_create.py index b7328c3..7cc2993 100644 --- a/tests/unit/borg/test_create.py +++ b/tests/unit/borg/test_create.py @@ -134,6 +134,15 @@ def test_write_pattern_file_with_empty_exclude_patterns_does_not_raise(): module.write_pattern_file([]) +def test_write_pattern_file_overwrites_existing_file(): + pattern_file = flexmock(name='filename', flush=lambda: None) + pattern_file.should_receive('seek').with_args(0).once() + pattern_file.should_receive('write').with_args('R /foo\n+ /foo/bar') + flexmock(module.tempfile).should_receive('NamedTemporaryFile').never() + + module.write_pattern_file(['R /foo', '+ /foo/bar'], pattern_file=pattern_file) + + @pytest.mark.parametrize( 'filename_lists,opened_filenames', ( @@ -267,25 +276,25 @@ def test_make_exclude_flags_is_empty_when_config_has_no_excludes(): assert exclude_flags == () -def test_borgmatic_source_directories_set_when_directory_exists(): +def test_collect_borgmatic_source_directories_set_when_directory_exists(): flexmock(module.os.path).should_receive('exists').and_return(True) flexmock(module.os.path).should_receive('expanduser') - assert module.borgmatic_source_directories('/tmp') == ['/tmp'] + assert module.collect_borgmatic_source_directories('/tmp') == ['/tmp'] -def test_borgmatic_source_directories_empty_when_directory_does_not_exist(): +def test_collect_borgmatic_source_directories_empty_when_directory_does_not_exist(): flexmock(module.os.path).should_receive('exists').and_return(False) flexmock(module.os.path).should_receive('expanduser') - assert module.borgmatic_source_directories('/tmp') == [] + assert module.collect_borgmatic_source_directories('/tmp') == [] -def test_borgmatic_source_directories_defaults_when_directory_not_given(): +def test_collect_borgmatic_source_directories_defaults_when_directory_not_given(): flexmock(module.os.path).should_receive('exists').and_return(True) flexmock(module.os.path).should_receive('expanduser') - assert module.borgmatic_source_directories(None) == [ + assert module.collect_borgmatic_source_directories(None) == [ module.state.DEFAULT_BORGMATIC_SOURCE_DIRECTORY ] @@ -300,12 +309,93 @@ def test_pattern_root_directories_parses_roots_and_ignores_others(): ) == ['/root', '/baz'] +@pytest.mark.parametrize( + 'character_device,block_device,fifo,expected_result', + ( + (False, False, False, False), + (True, False, False, True), + (False, True, False, True), + (True, True, False, True), + (False, False, True, True), + (False, True, True, True), + (True, False, True, True), + ), +) +def test_special_file_looks_at_file_type(character_device, block_device, fifo, expected_result): + flexmock(module.os).should_receive('stat').and_return(flexmock(st_mode=flexmock())) + flexmock(module.stat).should_receive('S_ISCHR').and_return(character_device) + flexmock(module.stat).should_receive('S_ISBLK').and_return(block_device) + flexmock(module.stat).should_receive('S_ISFIFO').and_return(fifo) + + assert module.special_file('/dev/special') == expected_result + + +def test_any_parent_directories_treats_parents_as_match(): + module.any_parent_directories('/foo/bar.txt', ('/foo', '/etc')) + + +def test_any_parent_directories_treats_grandparents_as_match(): + module.any_parent_directories('/foo/bar/baz.txt', ('/foo', '/etc')) + + +def test_any_parent_directories_treats_unrelated_paths_as_non_match(): + module.any_parent_directories('/foo/bar.txt', ('/usr', '/etc')) + + +def test_collect_special_file_paths_parses_special_files_from_borg_dry_run_file_list(): + flexmock(module).should_receive('execute_command').and_return( + 'Processing files ...\n- /foo\n- /bar\n- /baz' + ) + flexmock(module).should_receive('special_file').and_return(True) + flexmock(module).should_receive('any_parent_directories').and_return(False) + + assert module.collect_special_file_paths( + ('borg', 'create'), + local_path=None, + working_directory=None, + borg_environment=None, + skip_directories=flexmock(), + ) == ('/foo', '/bar', '/baz') + + +def test_collect_special_file_paths_excludes_requested_directories(): + flexmock(module).should_receive('execute_command').and_return('- /foo\n- /bar\n- /baz') + flexmock(module).should_receive('special_file').and_return(True) + flexmock(module).should_receive('any_parent_directories').and_return(False).and_return( + True + ).and_return(False) + + assert module.collect_special_file_paths( + ('borg', 'create'), + local_path=None, + working_directory=None, + borg_environment=None, + skip_directories=flexmock(), + ) == ('/foo', '/baz') + + +def test_collect_special_file_paths_excludes_non_special_files(): + flexmock(module).should_receive('execute_command').and_return('- /foo\n- /bar\n- /baz') + flexmock(module).should_receive('special_file').and_return(True).and_return(False).and_return( + True + ) + flexmock(module).should_receive('any_parent_directories').and_return(False) + + assert module.collect_special_file_paths( + ('borg', 'create'), + local_path=None, + working_directory=None, + borg_environment=None, + skip_directories=flexmock(), + ) == ('/foo', '/baz') + + DEFAULT_ARCHIVE_NAME = '{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}' REPO_ARCHIVE_WITH_PATHS = (f'repo::{DEFAULT_ARCHIVE_NAME}', 'foo', 'bar') def test_create_archive_calls_borg_with_parameters(): - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -344,7 +434,7 @@ def test_create_archive_calls_borg_with_parameters(): def test_create_archive_calls_borg_with_environment(): - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -385,7 +475,7 @@ def test_create_archive_calls_borg_with_environment(): def test_create_archive_with_patterns_calls_borg_with_patterns_including_converted_source_directories(): pattern_flags = ('--patterns-from', 'patterns') - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -427,7 +517,7 @@ def test_create_archive_with_patterns_calls_borg_with_patterns_including_convert def test_create_archive_with_exclude_patterns_calls_borg_with_excludes(): exclude_flags = ('--exclude-from', 'excludes') - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -468,7 +558,7 @@ def test_create_archive_with_exclude_patterns_calls_borg_with_excludes(): def test_create_archive_with_log_info_calls_borg_with_info_parameter(): - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -485,7 +575,7 @@ def test_create_archive_with_log_info_calls_borg_with_info_parameter(): ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create', '--info') + REPO_ARCHIVE_WITH_PATHS, + ('borg', 'create') + REPO_ARCHIVE_WITH_PATHS + ('--info',), output_log_level=logging.INFO, output_file=None, borg_local_path='borg', @@ -508,7 +598,7 @@ def test_create_archive_with_log_info_calls_borg_with_info_parameter(): def test_create_archive_with_log_info_and_json_suppresses_most_borg_output(): - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -525,7 +615,7 @@ def test_create_archive_with_log_info_and_json_suppresses_most_borg_output(): ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create', '--json') + REPO_ARCHIVE_WITH_PATHS, + ('borg', 'create') + REPO_ARCHIVE_WITH_PATHS + ('--json',), output_log_level=None, output_file=None, borg_local_path='borg', @@ -549,7 +639,7 @@ def test_create_archive_with_log_info_and_json_suppresses_most_borg_output(): def test_create_archive_with_log_debug_calls_borg_with_debug_parameter(): - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -566,7 +656,7 @@ def test_create_archive_with_log_debug_calls_borg_with_debug_parameter(): ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create', '--debug', '--show-rc') + REPO_ARCHIVE_WITH_PATHS, + ('borg', 'create') + REPO_ARCHIVE_WITH_PATHS + ('--debug', '--show-rc'), output_log_level=logging.INFO, output_file=None, borg_local_path='borg', @@ -589,7 +679,7 @@ def test_create_archive_with_log_debug_calls_borg_with_debug_parameter(): def test_create_archive_with_log_debug_and_json_suppresses_most_borg_output(): - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -606,7 +696,7 @@ def test_create_archive_with_log_debug_and_json_suppresses_most_borg_output(): ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create', '--json') + REPO_ARCHIVE_WITH_PATHS, + ('borg', 'create') + REPO_ARCHIVE_WITH_PATHS + ('--json',), output_log_level=None, output_file=None, borg_local_path='borg', @@ -630,7 +720,7 @@ def test_create_archive_with_log_debug_and_json_suppresses_most_borg_output(): def test_create_archive_with_dry_run_calls_borg_with_dry_run_parameter(): - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -671,7 +761,7 @@ def test_create_archive_with_dry_run_calls_borg_with_dry_run_parameter(): def test_create_archive_with_stats_and_dry_run_calls_borg_without_stats_parameter(): # --dry-run and --stats are mutually exclusive, see: # https://borgbackup.readthedocs.io/en/stable/usage/create.html#description - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -688,7 +778,7 @@ def test_create_archive_with_stats_and_dry_run_calls_borg_without_stats_paramete ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create', '--info', '--dry-run') + REPO_ARCHIVE_WITH_PATHS, + ('borg', 'create', '--dry-run') + REPO_ARCHIVE_WITH_PATHS + ('--info',), output_log_level=logging.INFO, output_file=None, borg_local_path='borg', @@ -712,7 +802,7 @@ def test_create_archive_with_stats_and_dry_run_calls_borg_without_stats_paramete def test_create_archive_with_checkpoint_interval_calls_borg_with_checkpoint_interval_parameters(): - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -751,7 +841,7 @@ def test_create_archive_with_checkpoint_interval_calls_borg_with_checkpoint_inte def test_create_archive_with_chunker_params_calls_borg_with_chunker_params_parameters(): - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -790,7 +880,7 @@ def test_create_archive_with_chunker_params_calls_borg_with_chunker_params_param def test_create_archive_with_compression_calls_borg_with_compression_parameters(): - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -834,7 +924,7 @@ def test_create_archive_with_compression_calls_borg_with_compression_parameters( def test_create_archive_with_upload_rate_limit_calls_borg_with_upload_ratelimit_parameters( feature_available, option_flag ): - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -873,7 +963,7 @@ def test_create_archive_with_upload_rate_limit_calls_borg_with_upload_ratelimit_ def test_create_archive_with_working_directory_calls_borg_with_working_directory(): - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -915,7 +1005,7 @@ def test_create_archive_with_working_directory_calls_borg_with_working_directory def test_create_archive_with_one_file_system_calls_borg_with_one_file_system_parameter(): - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -960,7 +1050,7 @@ def test_create_archive_with_one_file_system_calls_borg_with_one_file_system_par def test_create_archive_with_numeric_ids_calls_borg_with_numeric_ids_parameter( feature_available, option_flag ): - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -1000,7 +1090,7 @@ def test_create_archive_with_numeric_ids_calls_borg_with_numeric_ids_parameter( def test_create_archive_with_read_special_calls_borg_with_read_special_parameter(): - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -1016,8 +1106,72 @@ def test_create_archive_with_read_special_calls_borg_with_read_special_parameter (f'repo::{DEFAULT_ARCHIVE_NAME}',) ) flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('collect_special_file_paths').and_return(()) + create_command = ('borg', 'create', '--read-special') + REPO_ARCHIVE_WITH_PATHS flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create', '--read-special') + REPO_ARCHIVE_WITH_PATHS, + create_command + ('--dry-run', '--list'), + output_log_level=logging.INFO, + output_file=None, + borg_local_path='borg', + working_directory=None, + extra_environment=None, + ) + flexmock(module).should_receive('execute_command').with_args( + create_command, + output_log_level=logging.INFO, + output_file=None, + borg_local_path='borg', + working_directory=None, + extra_environment=None, + ) + + module.create_archive( + dry_run=False, + repository='repo', + location_config={ + 'source_directories': ['foo', 'bar'], + 'repositories': ['repo'], + 'read_special': True, + 'exclude_patterns': None, + }, + storage_config={}, + local_borg_version='1.2.3', + ) + + +def test_create_archive_with_read_special_adds_special_files_to_excludes(): + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) + flexmock(module).should_receive('map_directories_to_devices').and_return({}) + flexmock(module).should_receive('expand_directories').and_return(()) + flexmock(module).should_receive('pattern_root_directories').and_return([]) + flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError) + flexmock(module).should_receive('expand_home_directories').and_return(()) + flexmock(module).should_receive('write_pattern_file').and_return(None).and_return( + None + ).and_return(flexmock(name='/excludes')) + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module).should_receive('ensure_files_readable') + flexmock(module).should_receive('make_pattern_flags').and_return(()) + flexmock(module).should_receive('make_exclude_flags').and_return(()).and_return( + '--exclude-from', '/excludes' + ) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (f'repo::{DEFAULT_ARCHIVE_NAME}',) + ) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('collect_special_file_paths').and_return(()) + create_command = ('borg', 'create', '--read-special') + REPO_ARCHIVE_WITH_PATHS + flexmock(module).should_receive('execute_command').with_args( + create_command + ('--dry-run', '--list'), + output_log_level=logging.INFO, + output_file=None, + borg_local_path='borg', + working_directory=None, + extra_environment=None, + ) + flexmock(module).should_receive('execute_command').with_args( + create_command + ('--exclude-from', '/excludes'), output_log_level=logging.INFO, output_file=None, borg_local_path='borg', @@ -1047,7 +1201,7 @@ def test_create_archive_with_basic_option_calls_borg_with_corresponding_paramete option_name, option_value ): option_flag = '--no' + option_name.replace('', '') if option_value is False else None - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -1098,7 +1252,7 @@ def test_create_archive_with_basic_option_calls_borg_with_corresponding_paramete def test_create_archive_with_atime_option_calls_borg_with_corresponding_parameter( option_value, feature_available, option_flag ): - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -1149,7 +1303,7 @@ def test_create_archive_with_atime_option_calls_borg_with_corresponding_paramete def test_create_archive_with_flags_option_calls_borg_with_corresponding_parameter( option_value, feature_available, option_flag ): - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -1189,7 +1343,7 @@ def test_create_archive_with_flags_option_calls_borg_with_corresponding_paramete def test_create_archive_with_files_cache_calls_borg_with_files_cache_parameters(): - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -1229,7 +1383,7 @@ def test_create_archive_with_files_cache_calls_borg_with_files_cache_parameters( def test_create_archive_with_local_path_calls_borg_via_local_path(): - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -1269,7 +1423,7 @@ def test_create_archive_with_local_path_calls_borg_via_local_path(): def test_create_archive_with_remote_path_calls_borg_with_remote_path_parameters(): - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -1309,7 +1463,7 @@ def test_create_archive_with_remote_path_calls_borg_with_remote_path_parameters( def test_create_archive_with_umask_calls_borg_with_umask_parameters(): - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -1348,7 +1502,7 @@ def test_create_archive_with_umask_calls_borg_with_umask_parameters(): def test_create_archive_with_lock_wait_calls_borg_with_lock_wait_parameters(): - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -1387,7 +1541,7 @@ def test_create_archive_with_lock_wait_calls_borg_with_lock_wait_parameters(): def test_create_archive_with_stats_calls_borg_with_stats_parameter_and_warning_output_log_level(): - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -1404,7 +1558,7 @@ def test_create_archive_with_stats_calls_borg_with_stats_parameter_and_warning_o ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create', '--stats') + REPO_ARCHIVE_WITH_PATHS, + ('borg', 'create') + REPO_ARCHIVE_WITH_PATHS + ('--stats',), output_log_level=logging.WARNING, output_file=None, borg_local_path='borg', @@ -1427,7 +1581,7 @@ def test_create_archive_with_stats_calls_borg_with_stats_parameter_and_warning_o def test_create_archive_with_stats_and_log_info_calls_borg_with_stats_parameter_and_info_output_log_level(): - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -1444,7 +1598,7 @@ def test_create_archive_with_stats_and_log_info_calls_borg_with_stats_parameter_ ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create', '--info', '--stats') + REPO_ARCHIVE_WITH_PATHS, + ('borg', 'create') + REPO_ARCHIVE_WITH_PATHS + ('--info', '--stats'), output_log_level=logging.INFO, output_file=None, borg_local_path='borg', @@ -1468,7 +1622,7 @@ def test_create_archive_with_stats_and_log_info_calls_borg_with_stats_parameter_ def test_create_archive_with_files_calls_borg_with_list_parameter_and_warning_output_log_level(): - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -1508,7 +1662,7 @@ def test_create_archive_with_files_calls_borg_with_list_parameter_and_warning_ou def test_create_archive_with_files_and_log_info_calls_borg_with_list_parameter_and_info_output_log_level(): - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -1525,7 +1679,7 @@ def test_create_archive_with_files_and_log_info_calls_borg_with_list_parameter_a ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create', '--list', '--filter', 'AME-', '--info') + REPO_ARCHIVE_WITH_PATHS, + ('borg', 'create', '--list', '--filter', 'AME-') + REPO_ARCHIVE_WITH_PATHS + ('--info',), output_log_level=logging.INFO, output_file=None, borg_local_path='borg', @@ -1549,7 +1703,7 @@ def test_create_archive_with_files_and_log_info_calls_borg_with_list_parameter_a def test_create_archive_with_progress_and_log_info_calls_borg_with_progress_parameter_and_no_list(): - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -1566,7 +1720,7 @@ def test_create_archive_with_progress_and_log_info_calls_borg_with_progress_para ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create', '--info', '--progress') + REPO_ARCHIVE_WITH_PATHS, + ('borg', 'create') + REPO_ARCHIVE_WITH_PATHS + ('--info', '--progress',), output_log_level=logging.INFO, output_file=module.DO_NOT_CAPTURE, borg_local_path='borg', @@ -1590,7 +1744,7 @@ def test_create_archive_with_progress_and_log_info_calls_borg_with_progress_para def test_create_archive_with_progress_calls_borg_with_progress_parameter(): - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -1607,7 +1761,7 @@ def test_create_archive_with_progress_calls_borg_with_progress_parameter(): ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create', '--progress') + REPO_ARCHIVE_WITH_PATHS, + ('borg', 'create') + REPO_ARCHIVE_WITH_PATHS + ('--progress',), output_log_level=logging.INFO, output_file=module.DO_NOT_CAPTURE, borg_local_path='borg', @@ -1631,7 +1785,7 @@ def test_create_archive_with_progress_calls_borg_with_progress_parameter(): def test_create_archive_with_progress_and_stream_processes_calls_borg_with_progress_parameter(): processes = flexmock() - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -1647,9 +1801,23 @@ def test_create_archive_with_progress_and_stream_processes_calls_borg_with_progr (f'repo::{DEFAULT_ARCHIVE_NAME}',) ) flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('collect_special_file_paths').and_return(()) + create_command = ( + ('borg', 'create', '--one-file-system', '--read-special') + + REPO_ARCHIVE_WITH_PATHS + + ('--progress',) + ) flexmock(module).should_receive('execute_command_with_processes').with_args( - ('borg', 'create', '--one-file-system', '--read-special', '--progress') - + REPO_ARCHIVE_WITH_PATHS, + create_command + ('--dry-run', '--list'), + processes=processes, + output_log_level=logging.INFO, + output_file=module.DO_NOT_CAPTURE, + borg_local_path='borg', + working_directory=None, + extra_environment=None, + ) + flexmock(module).should_receive('execute_command_with_processes').with_args( + create_command, processes=processes, output_log_level=logging.INFO, output_file=module.DO_NOT_CAPTURE, @@ -1673,9 +1841,9 @@ def test_create_archive_with_progress_and_stream_processes_calls_borg_with_progr ) -def test_create_archive_with_stream_processes_ignores_read_special_false_logs_warning(): +def test_create_archive_with_stream_processes_ignores_read_special_false_and_logs_warnings(): processes = flexmock() - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -1685,15 +1853,31 @@ def test_create_archive_with_stream_processes_ignores_read_special_false_logs_wa flexmock(module).should_receive('write_pattern_file').and_return(None) flexmock(module.feature).should_receive('available').and_return(True) flexmock(module).should_receive('ensure_files_readable') - flexmock(module.logger).should_receive('warning').once() + flexmock(module.logger).should_receive('warning').twice() flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).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.environment).should_receive('make_environment') + flexmock(module).should_receive('collect_special_file_paths').and_return(()) + create_command = ( + 'borg', + 'create', + '--one-file-system', + '--read-special', + ) + REPO_ARCHIVE_WITH_PATHS flexmock(module).should_receive('execute_command_with_processes').with_args( - ('borg', 'create', '--one-file-system', '--read-special') + REPO_ARCHIVE_WITH_PATHS, + create_command + ('--dry-run', '--list'), + processes=processes, + output_log_level=logging.INFO, + output_file=None, + borg_local_path='borg', + working_directory=None, + extra_environment=None, + ) + flexmock(module).should_receive('execute_command_with_processes').with_args( + create_command, processes=processes, output_log_level=logging.INFO, output_file=None, @@ -1718,7 +1902,7 @@ def test_create_archive_with_stream_processes_ignores_read_special_false_logs_wa def test_create_archive_with_json_calls_borg_with_json_parameter(): - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -1735,7 +1919,7 @@ def test_create_archive_with_json_calls_borg_with_json_parameter(): ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create', '--json') + REPO_ARCHIVE_WITH_PATHS, + ('borg', 'create') + REPO_ARCHIVE_WITH_PATHS + ('--json',), output_log_level=None, output_file=None, borg_local_path='borg', @@ -1760,7 +1944,7 @@ def test_create_archive_with_json_calls_borg_with_json_parameter(): def test_create_archive_with_stats_and_json_calls_borg_without_stats_parameter(): - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -1777,7 +1961,7 @@ def test_create_archive_with_stats_and_json_calls_borg_without_stats_parameter() ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create', '--json') + REPO_ARCHIVE_WITH_PATHS, + ('borg', 'create') + REPO_ARCHIVE_WITH_PATHS + ('--json',), output_log_level=None, output_file=None, borg_local_path='borg', @@ -1803,7 +1987,7 @@ def test_create_archive_with_stats_and_json_calls_borg_without_stats_parameter() def test_create_archive_with_source_directories_glob_expands(): - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'food')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -1843,7 +2027,7 @@ def test_create_archive_with_source_directories_glob_expands(): def test_create_archive_with_non_matching_source_directories_glob_passes_through(): - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo*',)) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -1883,7 +2067,7 @@ def test_create_archive_with_non_matching_source_directories_glob_passes_through def test_create_archive_with_glob_calls_borg_with_expanded_directories(): - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'food')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -1922,7 +2106,7 @@ def test_create_archive_with_glob_calls_borg_with_expanded_directories(): def test_create_archive_with_archive_name_format_calls_borg_with_archive_name(): - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -1962,7 +2146,7 @@ def test_create_archive_with_archive_name_format_calls_borg_with_archive_name(): def test_create_archive_with_archive_name_format_accepts_borg_placeholders(): repository_archive_pattern = 'repo::Documents_{hostname}-{now}' - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -2002,7 +2186,7 @@ def test_create_archive_with_archive_name_format_accepts_borg_placeholders(): def test_create_archive_with_repository_accepts_borg_placeholders(): repository_archive_pattern = '{fqdn}::Documents_{hostname}-{now}' - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -2041,7 +2225,7 @@ def test_create_archive_with_repository_accepts_borg_placeholders(): def test_create_archive_with_extra_borg_options_calls_borg_with_extra_options(): - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -2079,9 +2263,9 @@ def test_create_archive_with_extra_borg_options_calls_borg_with_extra_options(): ) -def test_create_archive_with_stream_processes_calls_borg_with_processes(): +def test_create_archive_with_stream_processes_calls_borg_with_processes_and_read_special(): processes = flexmock() - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) flexmock(module).should_receive('expand_directories').and_return(()) @@ -2097,8 +2281,24 @@ def test_create_archive_with_stream_processes_calls_borg_with_processes(): (f'repo::{DEFAULT_ARCHIVE_NAME}',) ) flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('collect_special_file_paths').and_return(()) + create_command = ( + 'borg', + 'create', + '--one-file-system', + '--read-special', + ) + REPO_ARCHIVE_WITH_PATHS flexmock(module).should_receive('execute_command_with_processes').with_args( - ('borg', 'create', '--one-file-system', '--read-special') + REPO_ARCHIVE_WITH_PATHS, + create_command + ('--dry-run', 'list'), + processes=processes, + output_log_level=logging.INFO, + output_file=None, + borg_local_path='borg', + working_directory=None, + extra_environment=None, + ) + flexmock(module).should_receive('execute_command_with_processes').with_args( + create_command, processes=processes, output_log_level=logging.INFO, output_file=None, diff --git a/tests/unit/test_execute.py b/tests/unit/test_execute.py index ab4f15a..9bab8db 100644 --- a/tests/unit/test_execute.py +++ b/tests/unit/test_execute.py @@ -218,7 +218,7 @@ def test_execute_command_captures_output(): expected_output = '[]' flexmock(module.os, environ={'a': 'b'}) flexmock(module.subprocess).should_receive('check_output').with_args( - full_command, shell=False, env=None, cwd=None + full_command, stderr=module.subprocess.STDOUT, shell=False, env=None, cwd=None ).and_return(flexmock(decode=lambda: expected_output)).once() output = module.execute_command(full_command, output_log_level=None) @@ -231,7 +231,7 @@ def test_execute_command_captures_output_with_shell(): expected_output = '[]' flexmock(module.os, environ={'a': 'b'}) flexmock(module.subprocess).should_receive('check_output').with_args( - 'foo bar', shell=True, env=None, cwd=None + 'foo bar', stderr=module.subprocess.STDOUT, shell=True, env=None, cwd=None ).and_return(flexmock(decode=lambda: expected_output)).once() output = module.execute_command(full_command, output_log_level=None, shell=True) @@ -244,7 +244,11 @@ def test_execute_command_captures_output_with_extra_environment(): expected_output = '[]' flexmock(module.os, environ={'a': 'b'}) flexmock(module.subprocess).should_receive('check_output').with_args( - full_command, shell=False, env={'a': 'b', 'c': 'd'}, cwd=None + full_command, + stderr=module.subprocess.STDOUT, + shell=False, + env={'a': 'b', 'c': 'd'}, + cwd=None, ).and_return(flexmock(decode=lambda: expected_output)).once() output = module.execute_command( @@ -259,7 +263,7 @@ def test_execute_command_captures_output_with_working_directory(): expected_output = '[]' flexmock(module.os, environ={'a': 'b'}) flexmock(module.subprocess).should_receive('check_output').with_args( - full_command, shell=False, env=None, cwd='/working' + full_command, stderr=module.subprocess.STDOUT, shell=False, env=None, cwd='/working' ).and_return(flexmock(decode=lambda: expected_output)).once() output = module.execute_command(