2
0
Эх сурвалжийг харах

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).

Dan Helfman 2 жил өмнө
parent
commit
ae036aebd7

+ 2 - 0
NEWS

@@ -1,4 +1,6 @@
 1.7.3.dev0
 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
  * #587: Warn when ignoring a configured "read_special" value of false, as true is needed when
    database hooks are enabled.
    database hooks are enabled.
 
 

+ 100 - 14
borgmatic/borg/create.py

@@ -3,6 +3,7 @@ import itertools
 import logging
 import logging
 import os
 import os
 import pathlib
 import pathlib
+import stat
 import tempfile
 import tempfile
 
 
 from borgmatic.borg import environment, feature, flags, state
 from borgmatic.borg import environment, feature, flags, state
@@ -104,16 +105,21 @@ def deduplicate_directories(directory_devices, additional_directory_devices):
     return tuple(sorted(deduplicated))
     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
     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.
     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.
     Return None if no patterns are provided.
     '''
     '''
     if not patterns:
     if not patterns:
         return None
         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(
     pattern_file.write(
         '\n'.join(tuple(patterns) + tuple(f'R {source}' for source in (sources or [])))
         '\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}'
 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.
     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(
 def create_archive(
     dry_run,
     dry_run,
     repository,
     repository,
@@ -239,11 +297,13 @@ def create_archive(
     If a sequence of stream processes is given (instances of subprocess.Popen), then execute the
     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.
     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(
     sources = deduplicate_directories(
         map_directories_to_devices(
         map_directories_to_devices(
             expand_directories(
             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(
         additional_directory_devices=map_directories_to_devices(
@@ -265,6 +325,7 @@ def create_archive(
     upload_rate_limit = storage_config.get('upload_rate_limit', None)
     upload_rate_limit = storage_config.get('upload_rate_limit', None)
     umask = storage_config.get('umask', None)
     umask = storage_config.get('umask', None)
     lock_wait = storage_config.get('lock_wait', 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')
     files_cache = location_config.get('files_cache')
     archive_name_format = storage_config.get('archive_name_format', DEFAULT_ARCHIVE_NAME_FORMAT)
     archive_name_format = storage_config.get('archive_name_format', DEFAULT_ARCHIVE_NAME_FORMAT)
     extra_borg_options = storage_config.get('extra_borg_options', {}).get('create', '')
     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.'
             f'{repository}: Ignoring configured "read_special" value of false, as true is needed for database hooks.'
         )
         )
 
 
-    full_command = (
+    create_command = (
         tuple(local_path.split(' '))
         tuple(local_path.split(' '))
         + ('create',)
         + ('create',)
         + make_pattern_flags(location_config, pattern_file.name if pattern_file else None)
         + make_pattern_flags(location_config, pattern_file.name if pattern_file else None)
@@ -318,19 +379,14 @@ def create_archive(
         + atime_flags
         + atime_flags
         + (('--noctime',) if location_config.get('ctime') is False else ())
         + (('--noctime',) if location_config.get('ctime') is False else ())
         + (('--nobirthtime',) if location_config.get('birthtime') 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
         + noflags_flags
         + (('--files-cache', files_cache) if files_cache else ())
         + (('--files-cache', files_cache) if files_cache else ())
         + (('--remote-path', remote_path) if remote_path else ())
         + (('--remote-path', remote_path) if remote_path else ())
         + (('--umask', str(umask)) if umask else ())
         + (('--umask', str(umask)) if umask else ())
         + (('--lock-wait', str(lock_wait)) if lock_wait else ())
         + (('--lock-wait', str(lock_wait)) if lock_wait else ())
         + (('--list', '--filter', 'AME-') if list_files and not json and not progress 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 ())
         + (('--dry-run',) if dry_run else ())
-        + (('--progress',) if progress else ())
-        + (('--json',) if json else ())
         + (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
         + (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
         + flags.make_repository_archive_flags(repository, archive_name_format, local_borg_version)
         + flags.make_repository_archive_flags(repository, archive_name_format, local_borg_version)
         + (sources if not pattern_file else ())
         + (sources if not pattern_file else ())
@@ -349,9 +405,39 @@ def create_archive(
 
 
     borg_environment = environment.make_environment(storage_config)
     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:
     if stream_processes:
         return execute_command_with_processes(
         return execute_command_with_processes(
-            full_command,
+            create_command,
             stream_processes,
             stream_processes,
             output_log_level,
             output_log_level,
             output_file,
             output_file,
@@ -361,7 +447,7 @@ def create_archive(
         )
         )
 
 
     return execute_command(
     return execute_command(
-        full_command,
+        create_command,
         output_log_level,
         output_log_level,
         output_file,
         output_file,
         borg_local_path=local_path,
         borg_local_path=local_path,

+ 2 - 2
borgmatic/config/override.py

@@ -70,8 +70,8 @@ def parse_overrides(raw_overrides):
 
 
 def apply_overrides(config, 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)
     overrides = parse_overrides(raw_overrides)
 
 

+ 1 - 1
borgmatic/execute.py

@@ -197,7 +197,7 @@ def execute_command(
 
 
     if output_log_level is None:
     if output_log_level is None:
         output = subprocess.check_output(
         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
         return output.decode() if output is not None else None
 
 

+ 21 - 11
tests/end-to-end/test_database.py

@@ -9,20 +9,24 @@ import pytest
 
 
 
 
 def write_configuration(
 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
     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
     for testing. This includes injecting the given repository path, borgmatic source directory for
     storing database dumps, dump format (for PostgreSQL), and encryption passphrase.
     storing database dumps, dump format (for PostgreSQL), and encryption passphrase.
     '''
     '''
-    config = '''
+    config = f'''
 location:
 location:
     source_directories:
     source_directories:
-        - {}
+        - {source_directory}
     repositories:
     repositories:
-        - {}
-    borgmatic_source_directory: {}
+        - {repository_path}
+    borgmatic_source_directory: {borgmatic_source_directory}
 
 
 storage:
 storage:
     encryption_passphrase: "test"
     encryption_passphrase: "test"
@@ -33,7 +37,7 @@ hooks:
           hostname: postgresql
           hostname: postgresql
           username: postgres
           username: postgres
           password: test
           password: test
-          format: {}
+          format: {postgresql_dump_format}
         - name: all
         - name: all
           hostname: postgresql
           hostname: postgresql
           username: postgres
           username: postgres
@@ -57,9 +61,7 @@ hooks:
           hostname: mongodb
           hostname: mongodb
           username: root
           username: root
           password: test
           password: test
-'''.format(
-        config_path, repository_path, borgmatic_source_directory, postgresql_dump_format
-    )
+'''
 
 
     with open(config_path, 'w') as config_file:
     with open(config_path, 'w') as config_file:
         config_file.write(config)
         config_file.write(config)
@@ -71,11 +73,16 @@ def test_database_dump_and_restore():
     repository_path = os.path.join(temporary_directory, 'test.borg')
     repository_path = os.path.join(temporary_directory, 'test.borg')
     borgmatic_source_directory = os.path.join(temporary_directory, '.borgmatic')
     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()
     original_working_directory = os.getcwd()
 
 
     try:
     try:
         config_path = os.path.join(temporary_directory, 'test.yaml')
         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(
         subprocess.check_call(
             ['borgmatic', '-v', '2', '--config', config_path, 'init', '--encryption', 'repokey']
             ['borgmatic', '-v', '2', '--config', config_path, 'init', '--encryption', 'repokey']
@@ -114,6 +121,7 @@ def test_database_dump_and_restore_with_directory_format():
     try:
     try:
         config_path = os.path.join(temporary_directory, 'test.yaml')
         config_path = os.path.join(temporary_directory, 'test.yaml')
         write_configuration(
         write_configuration(
+            temporary_directory,
             config_path,
             config_path,
             repository_path,
             repository_path,
             borgmatic_source_directory,
             borgmatic_source_directory,
@@ -146,7 +154,9 @@ def test_database_dump_with_error_causes_borgmatic_to_exit():
 
 
     try:
     try:
         config_path = os.path.join(temporary_directory, 'test.yaml')
         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(
         subprocess.check_call(
             ['borgmatic', '-v', '2', '--config', config_path, 'init', '--encryption', 'repokey']
             ['borgmatic', '-v', '2', '--config', config_path, 'init', '--encryption', 'repokey']

+ 270 - 70
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([])
     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(
 @pytest.mark.parametrize(
     'filename_lists,opened_filenames',
     'filename_lists,opened_filenames',
     (
     (
@@ -267,25 +276,25 @@ def test_make_exclude_flags_is_empty_when_config_has_no_excludes():
     assert exclude_flags == ()
     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('exists').and_return(True)
     flexmock(module.os.path).should_receive('expanduser')
     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('exists').and_return(False)
     flexmock(module.os.path).should_receive('expanduser')
     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('exists').and_return(True)
     flexmock(module.os.path).should_receive('expanduser')
     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
         module.state.DEFAULT_BORGMATIC_SOURCE_DIRECTORY
     ]
     ]
 
 
@@ -300,12 +309,93 @@ def test_pattern_root_directories_parses_roots_and_ignores_others():
     ) == ['/root', '/baz']
     ) == ['/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}'
 DEFAULT_ARCHIVE_NAME = '{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}'
 REPO_ARCHIVE_WITH_PATHS = (f'repo::{DEFAULT_ARCHIVE_NAME}', 'foo', 'bar')
 REPO_ARCHIVE_WITH_PATHS = (f'repo::{DEFAULT_ARCHIVE_NAME}', 'foo', 'bar')
 
 
 
 
 def test_create_archive_calls_borg_with_parameters():
 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('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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():
 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('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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():
 def test_create_archive_with_patterns_calls_borg_with_patterns_including_converted_source_directories():
     pattern_flags = ('--patterns-from', 'patterns')
     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('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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():
 def test_create_archive_with_exclude_patterns_calls_borg_with_excludes():
     exclude_flags = ('--exclude-from', '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('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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():
 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('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
     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_log_level=logging.INFO,
         output_file=None,
         output_file=None,
         borg_local_path='borg',
         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():
 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('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
     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_log_level=None,
         output_file=None,
         output_file=None,
         borg_local_path='borg',
         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():
 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('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
     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_log_level=logging.INFO,
         output_file=None,
         output_file=None,
         borg_local_path='borg',
         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():
 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('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
     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_log_level=None,
         output_file=None,
         output_file=None,
         borg_local_path='borg',
         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():
 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('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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():
 def test_create_archive_with_stats_and_dry_run_calls_borg_without_stats_parameter():
     # --dry-run and --stats are mutually exclusive, see:
     # --dry-run and --stats are mutually exclusive, see:
     # https://borgbackup.readthedocs.io/en/stable/usage/create.html#description
     # 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('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
     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_log_level=logging.INFO,
         output_file=None,
         output_file=None,
         borg_local_path='borg',
         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():
 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('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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():
 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('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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():
 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('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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(
 def test_create_archive_with_upload_rate_limit_calls_borg_with_upload_ratelimit_parameters(
     feature_available, option_flag
     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('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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():
 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('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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():
 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('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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(
 def test_create_archive_with_numeric_ids_calls_borg_with_numeric_ids_parameter(
     feature_available, option_flag
     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('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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():
 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('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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}',)
         (f'repo::{DEFAULT_ARCHIVE_NAME}',)
     )
     )
     flexmock(module.environment).should_receive('make_environment')
     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,
+        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(
     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 + ('--exclude-from', '/excludes'),
         output_log_level=logging.INFO,
         output_log_level=logging.INFO,
         output_file=None,
         output_file=None,
         borg_local_path='borg',
         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_name, option_value
 ):
 ):
     option_flag = '--no' + option_name.replace('', '') if option_value is False else None
     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('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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(
 def test_create_archive_with_atime_option_calls_borg_with_corresponding_parameter(
     option_value, feature_available, option_flag
     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('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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(
 def test_create_archive_with_flags_option_calls_borg_with_corresponding_parameter(
     option_value, feature_available, option_flag
     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('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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():
 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('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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():
 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('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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():
 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('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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():
 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('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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():
 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('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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():
 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('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
     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_log_level=logging.WARNING,
         output_file=None,
         output_file=None,
         borg_local_path='borg',
         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():
 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('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
     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_log_level=logging.INFO,
         output_file=None,
         output_file=None,
         borg_local_path='borg',
         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():
 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('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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():
 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('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
     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_log_level=logging.INFO,
         output_file=None,
         output_file=None,
         borg_local_path='borg',
         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():
 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('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
     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_log_level=logging.INFO,
         output_file=module.DO_NOT_CAPTURE,
         output_file=module.DO_NOT_CAPTURE,
         borg_local_path='borg',
         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():
 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('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
     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_log_level=logging.INFO,
         output_file=module.DO_NOT_CAPTURE,
         output_file=module.DO_NOT_CAPTURE,
         borg_local_path='borg',
         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():
 def test_create_archive_with_progress_and_stream_processes_calls_borg_with_progress_parameter():
     processes = flexmock()
     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('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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}',)
         (f'repo::{DEFAULT_ARCHIVE_NAME}',)
     )
     )
     flexmock(module.environment).should_receive('make_environment')
     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(
     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,
         processes=processes,
         output_log_level=logging.INFO,
         output_log_level=logging.INFO,
         output_file=module.DO_NOT_CAPTURE,
         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()
     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('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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).should_receive('write_pattern_file').and_return(None)
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module).should_receive('ensure_files_readable')
     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_pattern_flags').and_return(())
     flexmock(module).should_receive('make_exclude_flags').and_return(())
     flexmock(module).should_receive('make_exclude_flags').and_return(())
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
         (f'repo::{DEFAULT_ARCHIVE_NAME}',)
         (f'repo::{DEFAULT_ARCHIVE_NAME}',)
     )
     )
     flexmock(module.environment).should_receive('make_environment')
     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(
+        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(
     flexmock(module).should_receive('execute_command_with_processes').with_args(
-        ('borg', 'create', '--one-file-system', '--read-special') + REPO_ARCHIVE_WITH_PATHS,
+        create_command,
         processes=processes,
         processes=processes,
         output_log_level=logging.INFO,
         output_log_level=logging.INFO,
         output_file=None,
         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():
 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('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
     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_log_level=None,
         output_file=None,
         output_file=None,
         borg_local_path='borg',
         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():
 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('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
     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_log_level=None,
         output_file=None,
         output_file=None,
         borg_local_path='borg',
         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():
 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('deduplicate_directories').and_return(('foo', 'food'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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():
 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('deduplicate_directories').and_return(('foo*',))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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():
 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('deduplicate_directories').and_return(('foo', 'food'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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():
 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('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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():
 def test_create_archive_with_archive_name_format_accepts_borg_placeholders():
     repository_archive_pattern = 'repo::Documents_{hostname}-{now}'
     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('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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():
 def test_create_archive_with_repository_accepts_borg_placeholders():
     repository_archive_pattern = '{fqdn}::Documents_{hostname}-{now}'
     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('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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():
 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('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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()
     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('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').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}',)
         (f'repo::{DEFAULT_ARCHIVE_NAME}',)
     )
     )
     flexmock(module.environment).should_receive('make_environment')
     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(
+        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(
     flexmock(module).should_receive('execute_command_with_processes').with_args(
-        ('borg', 'create', '--one-file-system', '--read-special') + REPO_ARCHIVE_WITH_PATHS,
+        create_command,
         processes=processes,
         processes=processes,
         output_log_level=logging.INFO,
         output_log_level=logging.INFO,
         output_file=None,
         output_file=None,

+ 8 - 4
tests/unit/test_execute.py

@@ -218,7 +218,7 @@ def test_execute_command_captures_output():
     expected_output = '[]'
     expected_output = '[]'
     flexmock(module.os, environ={'a': 'b'})
     flexmock(module.os, environ={'a': 'b'})
     flexmock(module.subprocess).should_receive('check_output').with_args(
     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()
     ).and_return(flexmock(decode=lambda: expected_output)).once()
 
 
     output = module.execute_command(full_command, output_log_level=None)
     output = module.execute_command(full_command, output_log_level=None)
@@ -231,7 +231,7 @@ def test_execute_command_captures_output_with_shell():
     expected_output = '[]'
     expected_output = '[]'
     flexmock(module.os, environ={'a': 'b'})
     flexmock(module.os, environ={'a': 'b'})
     flexmock(module.subprocess).should_receive('check_output').with_args(
     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()
     ).and_return(flexmock(decode=lambda: expected_output)).once()
 
 
     output = module.execute_command(full_command, output_log_level=None, shell=True)
     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 = '[]'
     expected_output = '[]'
     flexmock(module.os, environ={'a': 'b'})
     flexmock(module.os, environ={'a': 'b'})
     flexmock(module.subprocess).should_receive('check_output').with_args(
     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()
     ).and_return(flexmock(decode=lambda: expected_output)).once()
 
 
     output = module.execute_command(
     output = module.execute_command(
@@ -259,7 +263,7 @@ def test_execute_command_captures_output_with_working_directory():
     expected_output = '[]'
     expected_output = '[]'
     flexmock(module.os, environ={'a': 'b'})
     flexmock(module.os, environ={'a': 'b'})
     flexmock(module.subprocess).should_receive('check_output').with_args(
     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()
     ).and_return(flexmock(decode=lambda: expected_output)).once()
 
 
     output = module.execute_command(
     output = module.execute_command(