瀏覽代碼

Fix environment variable plumbing so options in one configuration file aren't used for others (#555).

Dan Helfman 3 年之前
父節點
當前提交
457ed80744

+ 2 - 0
NEWS

@@ -3,6 +3,8 @@
    the first few lines.
  * #554: Fix all monitoring hooks to warn if the server returns an HTTP 4xx error. This can happen
    with Healthchecks, for instance, when using an invalid ping URL.
+ * #555: Fix environment variable plumbing so options like "encryption_passphrase" and
+   "encryption_passcommand" in one configuration file aren't used for other configuration files.
 
 1.6.4
  * #546, #382: Keep your repository passphrases and database passwords outside of borgmatic's

+ 5 - 1
borgmatic/borg/borg.py

@@ -1,5 +1,6 @@
 import logging
 
+from borgmatic.borg import environment
 from borgmatic.borg.flags import make_flags
 from borgmatic.execute import execute_command
 
@@ -51,5 +52,8 @@ def run_arbitrary_borg(
     )
 
     return execute_command(
-        full_command, output_log_level=logging.WARNING, borg_local_path=local_path,
+        full_command,
+        output_log_level=logging.WARNING,
+        borg_local_path=local_path,
+        extra_environment=environment.make_environment(storage_config),
     )

+ 10 - 4
borgmatic/borg/check.py

@@ -5,7 +5,7 @@ import logging
 import os
 import pathlib
 
-from borgmatic.borg import extract, info, state
+from borgmatic.borg import environment, extract, info, state
 from borgmatic.execute import DO_NOT_CAPTURE, execute_command
 
 DEFAULT_CHECKS = (
@@ -304,16 +304,22 @@ def check_archives(
             + (repository,)
         )
 
+        borg_environment = environment.make_environment(storage_config)
+
         # The Borg repair option triggers an interactive prompt, which won't work when output is
         # captured. And progress messes with the terminal directly.
         if repair or progress:
-            execute_command(full_command, output_file=DO_NOT_CAPTURE)
+            execute_command(
+                full_command, output_file=DO_NOT_CAPTURE, extra_environment=borg_environment
+            )
         else:
-            execute_command(full_command)
+            execute_command(full_command, extra_environment=borg_environment)
 
         for check in checks:
             write_check_time(make_check_time_path(location_config, borg_repository_id, check))
 
     if 'extract' in checks:
-        extract.extract_last_archive_dry_run(repository, lock_wait, local_path, remote_path)
+        extract.extract_last_archive_dry_run(
+            storage_config, repository, lock_wait, local_path, remote_path
+        )
         write_check_time(make_check_time_path(location_config, borg_repository_id, 'extract'))

+ 7 - 1
borgmatic/borg/compact.py

@@ -1,5 +1,6 @@
 import logging
 
+from borgmatic.borg import environment
 from borgmatic.execute import execute_command
 
 logger = logging.getLogger(__name__)
@@ -38,4 +39,9 @@ def compact_segments(
     )
 
     if not dry_run:
-        execute_command(full_command, output_log_level=logging.INFO, borg_local_path=local_path)
+        execute_command(
+            full_command,
+            output_log_level=logging.INFO,
+            borg_local_path=local_path,
+            extra_environment=environment.make_environment(storage_config),
+        )

+ 5 - 1
borgmatic/borg/create.py

@@ -5,7 +5,7 @@ import os
 import pathlib
 import tempfile
 
-from borgmatic.borg import feature, state
+from borgmatic.borg import environment, feature, state
 from borgmatic.execute import DO_NOT_CAPTURE, execute_command, execute_command_with_processes
 
 logger = logging.getLogger(__name__)
@@ -317,6 +317,8 @@ def create_archive(
     # the terminal directly.
     output_file = DO_NOT_CAPTURE if progress else None
 
+    borg_environment = environment.make_environment(storage_config)
+
     if stream_processes:
         return execute_command_with_processes(
             full_command,
@@ -325,6 +327,7 @@ def create_archive(
             output_file,
             borg_local_path=local_path,
             working_directory=working_directory,
+            extra_environment=borg_environment,
         )
 
     return execute_command(
@@ -333,4 +336,5 @@ def create_archive(
         output_file,
         borg_local_path=local_path,
         working_directory=working_directory,
+        extra_environment=borg_environment,
     )

+ 12 - 11
borgmatic/borg/environment.py

@@ -1,5 +1,3 @@
-import os
-
 OPTION_TO_ENVIRONMENT_VARIABLE = {
     'borg_base_directory': 'BORG_BASE_DIR',
     'borg_config_directory': 'BORG_CONFIG_DIR',
@@ -18,21 +16,24 @@ DEFAULT_BOOL_OPTION_TO_ENVIRONMENT_VARIABLE = {
 }
 
 
-def initialize(storage_config):
-    for option_name, environment_variable_name in OPTION_TO_ENVIRONMENT_VARIABLE.items():
+def make_environment(storage_config):
+    '''
+    Given a borgmatic storage configuration dict, return its options converted to a Borg environment
+    variable dict.
+    '''
+    environment = {}
 
-        # Options from borgmatic configuration take precedence over already set BORG_* environment
-        # variables.
-        value = storage_config.get(option_name) or os.environ.get(environment_variable_name)
+    for option_name, environment_variable_name in OPTION_TO_ENVIRONMENT_VARIABLE.items():
+        value = storage_config.get(option_name)
 
         if value:
-            os.environ[environment_variable_name] = value
-        else:
-            os.environ.pop(environment_variable_name, None)
+            environment[environment_variable_name] = value
 
     for (
         option_name,
         environment_variable_name,
     ) in DEFAULT_BOOL_OPTION_TO_ENVIRONMENT_VARIABLE.items():
         value = storage_config.get(option_name, False)
-        os.environ[environment_variable_name] = 'yes' if value else 'no'
+        environment[environment_variable_name] = 'yes' if value else 'no'
+
+    return environment

+ 2 - 0
borgmatic/borg/export_tar.py

@@ -1,6 +1,7 @@
 import logging
 import os
 
+from borgmatic.borg import environment
 from borgmatic.execute import DO_NOT_CAPTURE, execute_command
 
 logger = logging.getLogger(__name__)
@@ -61,4 +62,5 @@ def export_tar_archive(
         output_file=DO_NOT_CAPTURE if destination_path == '-' else None,
         output_log_level=output_log_level,
         borg_local_path=local_path,
+        extra_environment=environment.make_environment(storage_config),
     )

+ 23 - 6
borgmatic/borg/extract.py

@@ -2,13 +2,15 @@ import logging
 import os
 import subprocess
 
-from borgmatic.borg import feature
+from borgmatic.borg import environment, feature
 from borgmatic.execute import DO_NOT_CAPTURE, execute_command
 
 logger = logging.getLogger(__name__)
 
 
-def extract_last_archive_dry_run(repository, lock_wait=None, local_path='borg', remote_path=None):
+def extract_last_archive_dry_run(
+    storage_config, repository, lock_wait=None, local_path='borg', remote_path=None
+):
     '''
     Perform an extraction dry-run of the most recent archive. If there are no archives, skip the
     dry-run.
@@ -29,8 +31,13 @@ def extract_last_archive_dry_run(repository, lock_wait=None, local_path='borg',
         + (repository,)
     )
 
+    borg_environment = environment.make_environment(storage_config)
+
     list_output = execute_command(
-        full_list_command, output_log_level=None, borg_local_path=local_path
+        full_list_command,
+        output_log_level=None,
+        borg_local_path=local_path,
+        extra_environment=borg_environment,
     )
 
     try:
@@ -52,7 +59,9 @@ def extract_last_archive_dry_run(repository, lock_wait=None, local_path='borg',
         )
     )
 
-    execute_command(full_extract_command, working_directory=None)
+    execute_command(
+        full_extract_command, working_directory=None, extra_environment=borg_environment
+    )
 
 
 def extract_archive(
@@ -106,11 +115,16 @@ def extract_archive(
         + (tuple(paths) if paths else ())
     )
 
+    borg_environment = environment.make_environment(storage_config)
+
     # The progress output isn't compatible with captured and logged output, as progress messes with
     # the terminal directly.
     if progress:
         return execute_command(
-            full_command, output_file=DO_NOT_CAPTURE, working_directory=destination_path
+            full_command,
+            output_file=DO_NOT_CAPTURE,
+            working_directory=destination_path,
+            extra_environment=borg_environment,
         )
         return None
 
@@ -120,8 +134,11 @@ def extract_archive(
             output_file=subprocess.PIPE,
             working_directory=destination_path,
             run_to_completion=False,
+            extra_environment=borg_environment,
         )
 
     # Don't give Borg local path, so as to error on warnings, as Borg only gives a warning if the
     # restore paths don't exist in the archive!
-    execute_command(full_command, working_directory=destination_path)
+    execute_command(
+        full_command, working_directory=destination_path, extra_environment=borg_environment
+    )

+ 2 - 0
borgmatic/borg/info.py

@@ -1,5 +1,6 @@
 import logging
 
+from borgmatic.borg import environment
 from borgmatic.borg.flags import make_flags, make_flags_from_arguments
 from borgmatic.execute import execute_command
 
@@ -42,4 +43,5 @@ def display_archives_info(
         full_command,
         output_log_level=None if info_arguments.json else logging.WARNING,
         borg_local_path=local_path,
+        extra_environment=environment.make_environment(storage_config),
     )

+ 7 - 2
borgmatic/borg/init.py

@@ -2,7 +2,7 @@ import argparse
 import logging
 import subprocess
 
-from borgmatic.borg import info
+from borgmatic.borg import environment, info
 from borgmatic.execute import DO_NOT_CAPTURE, execute_command
 
 logger = logging.getLogger(__name__)
@@ -54,4 +54,9 @@ def initialize_repository(
     )
 
     # Do not capture output here, so as to support interactive prompts.
-    execute_command(init_command, output_file=DO_NOT_CAPTURE, borg_local_path=local_path)
+    execute_command(
+        init_command,
+        output_file=DO_NOT_CAPTURE,
+        borg_local_path=local_path,
+        extra_environment=environment.make_environment(storage_config),
+    )

+ 11 - 1
borgmatic/borg/list.py

@@ -2,6 +2,7 @@ import copy
 import logging
 import re
 
+from borgmatic.borg import environment
 from borgmatic.borg.flags import make_flags, make_flags_from_arguments
 from borgmatic.execute import execute_command
 
@@ -31,7 +32,12 @@ def resolve_archive_name(repository, archive, storage_config, local_path='borg',
         + ('--short', repository)
     )
 
-    output = execute_command(full_command, output_log_level=None, borg_local_path=local_path)
+    output = execute_command(
+        full_command,
+        output_log_level=None,
+        borg_local_path=local_path,
+        extra_environment=environment.make_environment(storage_config),
+    )
     try:
         latest_archive = output.strip().splitlines()[-1]
     except IndexError:
@@ -111,6 +117,8 @@ def list_archives(repository, storage_config, list_arguments, local_path='borg',
     archive. Or, if list_arguments.find_paths are given, list the files by searching across multiple
     archives.
     '''
+    borg_environment = environment.make_environment(storage_config)
+
     # If there are any paths to find (and there's not a single archive already selected), start by
     # getting a list of archives to search.
     if list_arguments.find_paths and not list_arguments.archive:
@@ -127,6 +135,7 @@ def list_archives(repository, storage_config, list_arguments, local_path='borg',
                 ),
                 output_log_level=None,
                 borg_local_path=local_path,
+                extra_environment=borg_environment,
             )
             .strip('\n')
             .split('\n')
@@ -154,6 +163,7 @@ def list_archives(repository, storage_config, list_arguments, local_path='borg',
             main_command,
             output_log_level=None if list_arguments.json else logging.WARNING,
             borg_local_path=local_path,
+            extra_environment=borg_environment,
         )
 
         if list_arguments.json:

+ 10 - 2
borgmatic/borg/mount.py

@@ -1,5 +1,6 @@
 import logging
 
+from borgmatic.borg import environment
 from borgmatic.execute import DO_NOT_CAPTURE, execute_command
 
 logger = logging.getLogger(__name__)
@@ -38,9 +39,16 @@ def mount_archive(
         + (tuple(paths) if paths else ())
     )
 
+    borg_environment = environment.make_environment(storage_config)
+
     # Don't capture the output when foreground mode is used so that ctrl-C can work properly.
     if foreground:
-        execute_command(full_command, output_file=DO_NOT_CAPTURE, borg_local_path=local_path)
+        execute_command(
+            full_command,
+            output_file=DO_NOT_CAPTURE,
+            borg_local_path=local_path,
+            extra_environment=borg_environment,
+        )
         return
 
-    execute_command(full_command, borg_local_path=local_path)
+    execute_command(full_command, borg_local_path=local_path, extra_environment=borg_environment)

+ 7 - 1
borgmatic/borg/prune.py

@@ -1,5 +1,6 @@
 import logging
 
+from borgmatic.borg import environment
 from borgmatic.execute import execute_command
 
 logger = logging.getLogger(__name__)
@@ -72,4 +73,9 @@ def prune_archives(
     else:
         output_log_level = logging.INFO
 
-    execute_command(full_command, output_log_level=output_log_level, borg_local_path=local_path)
+    execute_command(
+        full_command,
+        output_log_level=output_log_level,
+        borg_local_path=local_path,
+        extra_environment=environment.make_environment(storage_config),
+    )

+ 9 - 3
borgmatic/borg/version.py

@@ -1,13 +1,14 @@
 import logging
 
+from borgmatic.borg import environment
 from borgmatic.execute import execute_command
 
 logger = logging.getLogger(__name__)
 
 
-def local_borg_version(local_path='borg'):
+def local_borg_version(storage_config, local_path='borg'):
     '''
-    Given a local Borg binary path, return a version string for it.
+    Given a storage configuration dict and a local Borg binary path, return a version string for it.
 
     Raise OSError or CalledProcessError if there is a problem running Borg.
     Raise ValueError if the version cannot be parsed.
@@ -17,7 +18,12 @@ def local_borg_version(local_path='borg'):
         + (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
         + (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
     )
-    output = execute_command(full_command, output_log_level=None, borg_local_path=local_path)
+    output = execute_command(
+        full_command,
+        output_log_level=None,
+        borg_local_path=local_path,
+        extra_environment=environment.make_environment(storage_config),
+    )
 
     try:
         return output.split(' ')[1].strip()

+ 2 - 4
borgmatic/commands/borgmatic.py

@@ -16,7 +16,6 @@ from borgmatic.borg import borg as borg_borg
 from borgmatic.borg import check as borg_check
 from borgmatic.borg import compact as borg_compact
 from borgmatic.borg import create as borg_create
-from borgmatic.borg import environment as borg_environment
 from borgmatic.borg import export_tar as borg_export_tar
 from borgmatic.borg import extract as borg_extract
 from borgmatic.borg import feature as borg_feature
@@ -60,14 +59,13 @@ def run_configuration(config_filename, config, arguments):
     remote_path = location.get('remote_path')
     retries = storage.get('retries', 0)
     retry_wait = storage.get('retry_wait', 0)
-    borg_environment.initialize(storage)
     encountered_error = None
     error_repository = ''
     using_primary_action = {'prune', 'compact', 'create', 'check'}.intersection(arguments)
     monitoring_log_level = verbosity_to_log_level(global_arguments.monitoring_verbosity)
 
     try:
-        local_borg_version = borg_version.local_borg_version(local_path)
+        local_borg_version = borg_version.local_borg_version(storage, local_path)
     except (OSError, CalledProcessError, ValueError) as error:
         yield from log_error_records(
             '{}: Error getting local Borg version'.format(config_filename), error
@@ -835,7 +833,7 @@ def collect_configuration_run_summary_logs(configs, arguments):
         logger.info('Unmounting mount point {}'.format(arguments['umount'].mount_point))
         try:
             borg_umount.unmount_archive(
-                mount_point=arguments['umount'].mount_point, local_path=get_local_path(configs)
+                mount_point=arguments['umount'].mount_point, local_path=get_local_path(configs),
             )
         except (CalledProcessError, OSError) as error:
             yield from log_error_records('Error unmounting mount point', error)

+ 1 - 1
tests/integration/config/test_generate.py

@@ -113,7 +113,7 @@ def test_write_configuration_with_already_existing_file_raises():
 def test_write_configuration_with_already_existing_file_and_overwrite_does_not_raise():
     flexmock(os.path).should_receive('exists').and_return(True)
 
-    module.write_configuration('config.yaml', 'config: yaml', overwrite=True)
+    module.write_configuration('/tmp/config.yaml', 'config: yaml', overwrite=True)
 
 
 def test_write_configuration_with_already_existing_directory_does_not_raise():

+ 43 - 6
tests/unit/borg/test_borg.py

@@ -8,8 +8,12 @@ from ..test_verbosity import insert_logging_mock
 
 
 def test_run_arbitrary_borg_calls_borg_with_parameters():
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
-        ('borg', 'break-lock', 'repo'), output_log_level=logging.WARNING, borg_local_path='borg'
+        ('borg', 'break-lock', 'repo'),
+        output_log_level=logging.WARNING,
+        borg_local_path='borg',
+        extra_environment=None,
     )
 
     module.run_arbitrary_borg(
@@ -18,10 +22,12 @@ def test_run_arbitrary_borg_calls_borg_with_parameters():
 
 
 def test_run_arbitrary_borg_with_log_info_calls_borg_with_info_parameter():
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'break-lock', 'repo', '--info'),
         output_log_level=logging.WARNING,
         borg_local_path='borg',
+        extra_environment=None,
     )
     insert_logging_mock(logging.INFO)
 
@@ -31,10 +37,12 @@ def test_run_arbitrary_borg_with_log_info_calls_borg_with_info_parameter():
 
 
 def test_run_arbitrary_borg_with_log_debug_calls_borg_with_debug_parameter():
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'break-lock', 'repo', '--debug', '--show-rc'),
         output_log_level=logging.WARNING,
         borg_local_path='borg',
+        extra_environment=None,
     )
     insert_logging_mock(logging.DEBUG)
 
@@ -45,10 +53,12 @@ def test_run_arbitrary_borg_with_log_debug_calls_borg_with_debug_parameter():
 
 def test_run_arbitrary_borg_with_lock_wait_calls_borg_with_lock_wait_parameters():
     storage_config = {'lock_wait': 5}
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'break-lock', 'repo', '--lock-wait', '5'),
         output_log_level=logging.WARNING,
         borg_local_path='borg',
+        extra_environment=None,
     )
 
     module.run_arbitrary_borg(
@@ -58,10 +68,12 @@ def test_run_arbitrary_borg_with_lock_wait_calls_borg_with_lock_wait_parameters(
 
 def test_run_arbitrary_borg_with_archive_calls_borg_with_archive_parameter():
     storage_config = {}
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'break-lock', 'repo::archive'),
         output_log_level=logging.WARNING,
         borg_local_path='borg',
+        extra_environment=None,
     )
 
     module.run_arbitrary_borg(
@@ -70,8 +82,12 @@ def test_run_arbitrary_borg_with_archive_calls_borg_with_archive_parameter():
 
 
 def test_run_arbitrary_borg_with_local_path_calls_borg_via_local_path():
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
-        ('borg1', 'break-lock', 'repo'), output_log_level=logging.WARNING, borg_local_path='borg1'
+        ('borg1', 'break-lock', 'repo'),
+        output_log_level=logging.WARNING,
+        borg_local_path='borg1',
+        extra_environment=None,
     )
 
     module.run_arbitrary_borg(
@@ -80,10 +96,12 @@ def test_run_arbitrary_borg_with_local_path_calls_borg_via_local_path():
 
 
 def test_run_arbitrary_borg_with_remote_path_calls_borg_with_remote_path_parameters():
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'break-lock', 'repo', '--remote-path', 'borg1'),
         output_log_level=logging.WARNING,
         borg_local_path='borg',
+        extra_environment=None,
     )
 
     module.run_arbitrary_borg(
@@ -92,10 +110,12 @@ def test_run_arbitrary_borg_with_remote_path_calls_borg_with_remote_path_paramet
 
 
 def test_run_arbitrary_borg_passes_borg_specific_parameters_to_borg():
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'list', 'repo', '--progress'),
         output_log_level=logging.WARNING,
         borg_local_path='borg',
+        extra_environment=None,
     )
 
     module.run_arbitrary_borg(
@@ -104,8 +124,12 @@ def test_run_arbitrary_borg_passes_borg_specific_parameters_to_borg():
 
 
 def test_run_arbitrary_borg_omits_dash_dash_in_parameters_passed_to_borg():
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
-        ('borg', 'break-lock', 'repo'), output_log_level=logging.WARNING, borg_local_path='borg',
+        ('borg', 'break-lock', 'repo'),
+        output_log_level=logging.WARNING,
+        borg_local_path='borg',
+        extra_environment=None,
     )
 
     module.run_arbitrary_borg(
@@ -114,8 +138,9 @@ def test_run_arbitrary_borg_omits_dash_dash_in_parameters_passed_to_borg():
 
 
 def test_run_arbitrary_borg_without_borg_specific_parameters_does_not_raise():
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
-        ('borg',), output_log_level=logging.WARNING, borg_local_path='borg',
+        ('borg',), output_log_level=logging.WARNING, borg_local_path='borg', extra_environment=None,
     )
 
     module.run_arbitrary_borg(
@@ -124,8 +149,12 @@ def test_run_arbitrary_borg_without_borg_specific_parameters_does_not_raise():
 
 
 def test_run_arbitrary_borg_passes_key_sub_command_to_borg_before_repository():
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
-        ('borg', 'key', 'export', 'repo'), output_log_level=logging.WARNING, borg_local_path='borg',
+        ('borg', 'key', 'export', 'repo'),
+        output_log_level=logging.WARNING,
+        borg_local_path='borg',
+        extra_environment=None,
     )
 
     module.run_arbitrary_borg(
@@ -134,10 +163,12 @@ def test_run_arbitrary_borg_passes_key_sub_command_to_borg_before_repository():
 
 
 def test_run_arbitrary_borg_passes_debug_sub_command_to_borg_before_repository():
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'debug', 'dump-manifest', 'repo', 'path'),
         output_log_level=logging.WARNING,
         borg_local_path='borg',
+        extra_environment=None,
     )
 
     module.run_arbitrary_borg(
@@ -146,8 +177,12 @@ def test_run_arbitrary_borg_passes_debug_sub_command_to_borg_before_repository()
 
 
 def test_run_arbitrary_borg_with_debug_info_command_does_not_pass_borg_repository():
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
-        ('borg', 'debug', 'info'), output_log_level=logging.WARNING, borg_local_path='borg',
+        ('borg', 'debug', 'info'),
+        output_log_level=logging.WARNING,
+        borg_local_path='borg',
+        extra_environment=None,
     )
 
     module.run_arbitrary_borg(
@@ -156,10 +191,12 @@ def test_run_arbitrary_borg_with_debug_info_command_does_not_pass_borg_repositor
 
 
 def test_run_arbitrary_borg_with_debug_convert_profile_command_does_not_pass_borg_repository():
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'debug', 'convert-profile', 'in', 'out'),
         output_log_level=logging.WARNING,
         borg_local_path='borg',
+        extra_environment=None,
     )
 
     module.run_arbitrary_borg(

+ 12 - 3
tests/unit/borg/test_check.py

@@ -9,7 +9,10 @@ from ..test_verbosity import insert_logging_mock
 
 
 def insert_execute_command_mock(command):
-    flexmock(module).should_receive('execute_command').with_args(command).once()
+    flexmock(module.environment).should_receive('make_environment')
+    flexmock(module).should_receive('execute_command').with_args(
+        command, extra_environment=None
+    ).once()
 
 
 def insert_execute_command_never():
@@ -310,8 +313,11 @@ def test_check_archives_with_progress_calls_borg_with_progress_parameter():
     )
     flexmock(module).should_receive('make_check_flags').and_return(())
     flexmock(module).should_receive('execute_command').never()
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
-        ('borg', 'check', '--progress', 'repo'), output_file=module.DO_NOT_CAPTURE
+        ('borg', 'check', '--progress', 'repo'),
+        output_file=module.DO_NOT_CAPTURE,
+        extra_environment=None,
     ).once()
     flexmock(module).should_receive('make_check_time_path')
     flexmock(module).should_receive('write_check_time')
@@ -335,8 +341,11 @@ def test_check_archives_with_repair_calls_borg_with_repair_parameter():
     )
     flexmock(module).should_receive('make_check_flags').and_return(())
     flexmock(module).should_receive('execute_command').never()
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
-        ('borg', 'check', '--repair', 'repo'), output_file=module.DO_NOT_CAPTURE
+        ('borg', 'check', '--repair', 'repo'),
+        output_file=module.DO_NOT_CAPTURE,
+        extra_environment=None,
     ).once()
     flexmock(module).should_receive('make_check_time_path')
     flexmock(module).should_receive('write_check_time')

+ 5 - 1
tests/unit/borg/test_compact.py

@@ -8,8 +8,12 @@ from ..test_verbosity import insert_logging_mock
 
 
 def insert_execute_command_mock(compact_command, output_log_level):
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
-        compact_command, output_log_level=output_log_level, borg_local_path=compact_command[0]
+        compact_command,
+        output_log_level=output_log_level,
+        borg_local_path=compact_command[0],
+        extra_environment=None,
     ).once()
 
 

+ 120 - 0
tests/unit/borg/test_create.py

@@ -292,12 +292,50 @@ def test_create_archive_calls_borg_with_parameters():
     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(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create') + ARCHIVE_WITH_PATHS,
         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'],
+            'exclude_patterns': None,
+        },
+        storage_config={},
+        local_borg_version='1.2.3',
+    )
+
+
+def test_create_archive_calls_borg_with_environment():
+    flexmock(module).should_receive('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.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)
+    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(())
+    environment = {'BORG_THINGY': 'YUP'}
+    flexmock(module.environment).should_receive('make_environment').and_return(environment)
+    flexmock(module).should_receive('execute_command').with_args(
+        ('borg', 'create') + ARCHIVE_WITH_PATHS,
+        output_log_level=logging.INFO,
+        output_file=None,
+        borg_local_path='borg',
+        working_directory=None,
+        extra_environment=environment,
     )
 
     module.create_archive(
@@ -328,12 +366,14 @@ def test_create_archive_with_patterns_calls_borg_with_patterns():
     flexmock(module).should_receive('ensure_files_readable')
     flexmock(module).should_receive('make_pattern_flags').and_return(pattern_flags)
     flexmock(module).should_receive('make_exclude_flags').and_return(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create') + pattern_flags + ARCHIVE_WITH_PATHS,
         output_log_level=logging.INFO,
         output_file=None,
         borg_local_path='borg',
         working_directory=None,
+        extra_environment=None,
     )
 
     module.create_archive(
@@ -364,12 +404,14 @@ def test_create_archive_with_exclude_patterns_calls_borg_with_excludes():
     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(exclude_flags)
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create') + exclude_flags + ARCHIVE_WITH_PATHS,
         output_log_level=logging.INFO,
         output_file=None,
         borg_local_path='borg',
         working_directory=None,
+        extra_environment=None,
     )
 
     module.create_archive(
@@ -397,12 +439,14 @@ def test_create_archive_with_log_info_calls_borg_with_info_parameter():
     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(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create', '--info') + ARCHIVE_WITH_PATHS,
         output_log_level=logging.INFO,
         output_file=None,
         borg_local_path='borg',
         working_directory=None,
+        extra_environment=None,
     )
     insert_logging_mock(logging.INFO)
 
@@ -431,12 +475,14 @@ def test_create_archive_with_log_info_and_json_suppresses_most_borg_output():
     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(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create', '--json') + ARCHIVE_WITH_PATHS,
         output_log_level=None,
         output_file=None,
         borg_local_path='borg',
         working_directory=None,
+        extra_environment=None,
     )
     insert_logging_mock(logging.INFO)
 
@@ -466,12 +512,14 @@ def test_create_archive_with_log_debug_calls_borg_with_debug_parameter():
     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(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create', '--debug', '--show-rc') + ARCHIVE_WITH_PATHS,
         output_log_level=logging.INFO,
         output_file=None,
         borg_local_path='borg',
         working_directory=None,
+        extra_environment=None,
     )
     insert_logging_mock(logging.DEBUG)
 
@@ -500,12 +548,14 @@ def test_create_archive_with_log_debug_and_json_suppresses_most_borg_output():
     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(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create', '--json') + ARCHIVE_WITH_PATHS,
         output_log_level=None,
         output_file=None,
         borg_local_path='borg',
         working_directory=None,
+        extra_environment=None,
     )
     insert_logging_mock(logging.DEBUG)
 
@@ -535,12 +585,14 @@ def test_create_archive_with_dry_run_calls_borg_with_dry_run_parameter():
     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(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create', '--dry-run') + ARCHIVE_WITH_PATHS,
         output_log_level=logging.INFO,
         output_file=None,
         borg_local_path='borg',
         working_directory=None,
+        extra_environment=None,
     )
 
     module.create_archive(
@@ -570,12 +622,14 @@ def test_create_archive_with_stats_and_dry_run_calls_borg_without_stats_paramete
     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(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create', '--info', '--dry-run') + ARCHIVE_WITH_PATHS,
         output_log_level=logging.INFO,
         output_file=None,
         borg_local_path='borg',
         working_directory=None,
+        extra_environment=None,
     )
     insert_logging_mock(logging.INFO)
 
@@ -605,12 +659,14 @@ def test_create_archive_with_checkpoint_interval_calls_borg_with_checkpoint_inte
     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(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create', '--checkpoint-interval', '600') + ARCHIVE_WITH_PATHS,
         output_log_level=logging.INFO,
         output_file=None,
         borg_local_path='borg',
         working_directory=None,
+        extra_environment=None,
     )
 
     module.create_archive(
@@ -638,12 +694,14 @@ def test_create_archive_with_chunker_params_calls_borg_with_chunker_params_param
     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(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create', '--chunker-params', '1,2,3,4') + ARCHIVE_WITH_PATHS,
         output_log_level=logging.INFO,
         output_file=None,
         borg_local_path='borg',
         working_directory=None,
+        extra_environment=None,
     )
 
     module.create_archive(
@@ -671,12 +729,14 @@ def test_create_archive_with_compression_calls_borg_with_compression_parameters(
     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(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create', '--compression', 'rle') + ARCHIVE_WITH_PATHS,
         output_log_level=logging.INFO,
         output_file=None,
         borg_local_path='borg',
         working_directory=None,
+        extra_environment=None,
     )
 
     module.create_archive(
@@ -709,12 +769,14 @@ def test_create_archive_with_remote_rate_limit_calls_borg_with_upload_ratelimit_
     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(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create', option_flag, '100') + ARCHIVE_WITH_PATHS,
         output_log_level=logging.INFO,
         output_file=None,
         borg_local_path='borg',
         working_directory=None,
+        extra_environment=None,
     )
 
     module.create_archive(
@@ -744,12 +806,14 @@ def test_create_archive_with_working_directory_calls_borg_with_working_directory
     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(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create') + ARCHIVE_WITH_PATHS,
         output_log_level=logging.INFO,
         output_file=None,
         borg_local_path='borg',
         working_directory='/working/dir',
+        extra_environment=None,
     )
 
     module.create_archive(
@@ -778,12 +842,14 @@ def test_create_archive_with_one_file_system_calls_borg_with_one_file_system_par
     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(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create', '--one-file-system') + ARCHIVE_WITH_PATHS,
         output_log_level=logging.INFO,
         output_file=None,
         borg_local_path='borg',
         working_directory=None,
+        extra_environment=None,
     )
 
     module.create_archive(
@@ -817,12 +883,14 @@ def test_create_archive_with_numeric_owner_calls_borg_with_numeric_ids_parameter
     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(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create', option_flag) + ARCHIVE_WITH_PATHS,
         output_log_level=logging.INFO,
         output_file=None,
         borg_local_path='borg',
         working_directory=None,
+        extra_environment=None,
     )
 
     module.create_archive(
@@ -851,12 +919,14 @@ def test_create_archive_with_read_special_calls_borg_with_read_special_parameter
     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(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create', '--read-special') + ARCHIVE_WITH_PATHS,
         output_log_level=logging.INFO,
         output_file=None,
         borg_local_path='borg',
         working_directory=None,
+        extra_environment=None,
     )
 
     module.create_archive(
@@ -892,12 +962,14 @@ def test_create_archive_with_basic_option_calls_borg_with_corresponding_paramete
     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(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create') + ((option_flag,) if option_flag else ()) + ARCHIVE_WITH_PATHS,
         output_log_level=logging.INFO,
         output_file=None,
         borg_local_path='borg',
         working_directory=None,
+        extra_environment=None,
     )
 
     module.create_archive(
@@ -937,12 +1009,14 @@ def test_create_archive_with_atime_option_calls_borg_with_corresponding_paramete
     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(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create') + ((option_flag,) if option_flag else ()) + ARCHIVE_WITH_PATHS,
         output_log_level=logging.INFO,
         output_file=None,
         borg_local_path='borg',
         working_directory=None,
+        extra_environment=None,
     )
 
     module.create_archive(
@@ -982,12 +1056,14 @@ def test_create_archive_with_bsd_flags_option_calls_borg_with_corresponding_para
     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(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create') + ((option_flag,) if option_flag else ()) + ARCHIVE_WITH_PATHS,
         output_log_level=logging.INFO,
         output_file=None,
         borg_local_path='borg',
         working_directory=None,
+        extra_environment=None,
     )
 
     module.create_archive(
@@ -1016,12 +1092,14 @@ def test_create_archive_with_files_cache_calls_borg_with_files_cache_parameters(
     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(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create', '--files-cache', 'ctime,size') + ARCHIVE_WITH_PATHS,
         output_log_level=logging.INFO,
         output_file=None,
         borg_local_path='borg',
         working_directory=None,
+        extra_environment=None,
     )
 
     module.create_archive(
@@ -1050,12 +1128,14 @@ def test_create_archive_with_local_path_calls_borg_via_local_path():
     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(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg1', 'create') + ARCHIVE_WITH_PATHS,
         output_log_level=logging.INFO,
         output_file=None,
         borg_local_path='borg1',
         working_directory=None,
+        extra_environment=None,
     )
 
     module.create_archive(
@@ -1084,12 +1164,14 @@ def test_create_archive_with_remote_path_calls_borg_with_remote_path_parameters(
     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(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create', '--remote-path', 'borg1') + ARCHIVE_WITH_PATHS,
         output_log_level=logging.INFO,
         output_file=None,
         borg_local_path='borg',
         working_directory=None,
+        extra_environment=None,
     )
 
     module.create_archive(
@@ -1118,12 +1200,14 @@ def test_create_archive_with_umask_calls_borg_with_umask_parameters():
     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(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create', '--umask', '740') + ARCHIVE_WITH_PATHS,
         output_log_level=logging.INFO,
         output_file=None,
         borg_local_path='borg',
         working_directory=None,
+        extra_environment=None,
     )
 
     module.create_archive(
@@ -1151,12 +1235,14 @@ def test_create_archive_with_lock_wait_calls_borg_with_lock_wait_parameters():
     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(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create', '--lock-wait', '5') + ARCHIVE_WITH_PATHS,
         output_log_level=logging.INFO,
         output_file=None,
         borg_local_path='borg',
         working_directory=None,
+        extra_environment=None,
     )
 
     module.create_archive(
@@ -1184,12 +1270,14 @@ def test_create_archive_with_stats_calls_borg_with_stats_parameter_and_warning_o
     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(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create', '--stats') + ARCHIVE_WITH_PATHS,
         output_log_level=logging.WARNING,
         output_file=None,
         borg_local_path='borg',
         working_directory=None,
+        extra_environment=None,
     )
 
     module.create_archive(
@@ -1218,12 +1306,14 @@ def test_create_archive_with_stats_and_log_info_calls_borg_with_stats_parameter_
     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(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create', '--info', '--stats') + ARCHIVE_WITH_PATHS,
         output_log_level=logging.INFO,
         output_file=None,
         borg_local_path='borg',
         working_directory=None,
+        extra_environment=None,
     )
     insert_logging_mock(logging.INFO)
 
@@ -1253,12 +1343,14 @@ def test_create_archive_with_files_calls_borg_with_list_parameter_and_warning_ou
     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(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create', '--list', '--filter', 'AME-') + ARCHIVE_WITH_PATHS,
         output_log_level=logging.WARNING,
         output_file=None,
         borg_local_path='borg',
         working_directory=None,
+        extra_environment=None,
     )
 
     module.create_archive(
@@ -1287,12 +1379,14 @@ def test_create_archive_with_files_and_log_info_calls_borg_with_list_parameter_a
     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(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create', '--list', '--filter', 'AME-', '--info') + ARCHIVE_WITH_PATHS,
         output_log_level=logging.INFO,
         output_file=None,
         borg_local_path='borg',
         working_directory=None,
+        extra_environment=None,
     )
     insert_logging_mock(logging.INFO)
 
@@ -1322,12 +1416,14 @@ def test_create_archive_with_progress_and_log_info_calls_borg_with_progress_para
     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(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create', '--info', '--progress') + ARCHIVE_WITH_PATHS,
         output_log_level=logging.INFO,
         output_file=module.DO_NOT_CAPTURE,
         borg_local_path='borg',
         working_directory=None,
+        extra_environment=None,
     )
     insert_logging_mock(logging.INFO)
 
@@ -1357,12 +1453,14 @@ def test_create_archive_with_progress_calls_borg_with_progress_parameter():
     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(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create', '--progress') + ARCHIVE_WITH_PATHS,
         output_log_level=logging.INFO,
         output_file=module.DO_NOT_CAPTURE,
         borg_local_path='borg',
         working_directory=None,
+        extra_environment=None,
     )
 
     module.create_archive(
@@ -1392,6 +1490,7 @@ def test_create_archive_with_progress_and_stream_processes_calls_borg_with_progr
     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(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command_with_processes').with_args(
         ('borg', 'create', '--one-file-system', '--read-special', '--progress')
         + ARCHIVE_WITH_PATHS,
@@ -1400,6 +1499,7 @@ def test_create_archive_with_progress_and_stream_processes_calls_borg_with_progr
         output_file=module.DO_NOT_CAPTURE,
         borg_local_path='borg',
         working_directory=None,
+        extra_environment=None,
     )
 
     module.create_archive(
@@ -1429,12 +1529,14 @@ def test_create_archive_with_json_calls_borg_with_json_parameter():
     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(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create', '--json') + ARCHIVE_WITH_PATHS,
         output_log_level=None,
         output_file=None,
         borg_local_path='borg',
         working_directory=None,
+        extra_environment=None,
     ).and_return('[]')
 
     json_output = module.create_archive(
@@ -1465,12 +1567,14 @@ def test_create_archive_with_stats_and_json_calls_borg_without_stats_parameter()
     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(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create', '--json') + ARCHIVE_WITH_PATHS,
         output_log_level=None,
         output_file=None,
         borg_local_path='borg',
         working_directory=None,
+        extra_environment=None,
     ).and_return('[]')
 
     json_output = module.create_archive(
@@ -1502,12 +1606,14 @@ def test_create_archive_with_source_directories_glob_expands():
     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(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create', 'repo::{}'.format(DEFAULT_ARCHIVE_NAME), 'foo', 'food'),
         output_log_level=logging.INFO,
         output_file=None,
         borg_local_path='borg',
         working_directory=None,
+        extra_environment=None,
     )
     flexmock(module.glob).should_receive('glob').with_args('foo*').and_return(['foo', 'food'])
 
@@ -1536,12 +1642,14 @@ def test_create_archive_with_non_matching_source_directories_glob_passes_through
     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(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create', 'repo::{}'.format(DEFAULT_ARCHIVE_NAME), 'foo*'),
         output_log_level=logging.INFO,
         output_file=None,
         borg_local_path='borg',
         working_directory=None,
+        extra_environment=None,
     )
     flexmock(module.glob).should_receive('glob').with_args('foo*').and_return([])
 
@@ -1570,12 +1678,14 @@ def test_create_archive_with_glob_calls_borg_with_expanded_directories():
     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(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create', 'repo::{}'.format(DEFAULT_ARCHIVE_NAME), 'foo', 'food'),
         output_log_level=logging.INFO,
         output_file=None,
         borg_local_path='borg',
         working_directory=None,
+        extra_environment=None,
     )
 
     module.create_archive(
@@ -1603,12 +1713,14 @@ def test_create_archive_with_archive_name_format_calls_borg_with_archive_name():
     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(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create', 'repo::ARCHIVE_NAME', 'foo', 'bar'),
         output_log_level=logging.INFO,
         output_file=None,
         borg_local_path='borg',
         working_directory=None,
+        extra_environment=None,
     )
 
     module.create_archive(
@@ -1636,12 +1748,14 @@ def test_create_archive_with_archive_name_format_accepts_borg_placeholders():
     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(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create', 'repo::Documents_{hostname}-{now}', 'foo', 'bar'),
         output_log_level=logging.INFO,
         output_file=None,
         borg_local_path='borg',
         working_directory=None,
+        extra_environment=None,
     )
 
     module.create_archive(
@@ -1669,12 +1783,14 @@ def test_create_archive_with_repository_accepts_borg_placeholders():
     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(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create', '{fqdn}::Documents_{hostname}-{now}', 'foo', 'bar'),
         output_log_level=logging.INFO,
         output_file=None,
         borg_local_path='borg',
         working_directory=None,
+        extra_environment=None,
     )
 
     module.create_archive(
@@ -1702,12 +1818,14 @@ def test_create_archive_with_extra_borg_options_calls_borg_with_extra_options():
     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(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create', '--extra', '--options') + ARCHIVE_WITH_PATHS,
         output_log_level=logging.INFO,
         output_file=None,
         borg_local_path='borg',
         working_directory=None,
+        extra_environment=None,
     )
 
     module.create_archive(
@@ -1736,6 +1854,7 @@ def test_create_archive_with_stream_processes_calls_borg_with_processes():
     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(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command_with_processes').with_args(
         ('borg', 'create', '--one-file-system', '--read-special') + ARCHIVE_WITH_PATHS,
         processes=processes,
@@ -1743,6 +1862,7 @@ def test_create_archive_with_stream_processes_calls_borg_with_processes():
         output_file=None,
         borg_local_path='borg',
         working_directory=None,
+        extra_environment=None,
     )
 
     module.create_archive(

+ 18 - 68
tests/unit/borg/test_environment.py

@@ -1,84 +1,34 @@
-import os
-
 from borgmatic.borg import environment as module
 
 
-def test_initialize_with_passcommand_should_set_environment():
-    orig_environ = os.environ
-
-    try:
-        os.environ = {}
-        module.initialize({'encryption_passcommand': 'command'})
-        assert os.environ.get('BORG_PASSCOMMAND') == 'command'
-    finally:
-        os.environ = orig_environ
-
-
-def test_initialize_with_passphrase_should_set_environment():
-    orig_environ = os.environ
-
-    try:
-        os.environ = {}
-        module.initialize({'encryption_passphrase': 'pass'})
-        assert os.environ.get('BORG_PASSPHRASE') == 'pass'
-    finally:
-        os.environ = orig_environ
-
-
-def test_initialize_with_ssh_command_should_set_environment():
-    orig_environ = os.environ
-
-    try:
-        os.environ = {}
-        module.initialize({'ssh_command': 'ssh -C'})
-        assert os.environ.get('BORG_RSH') == 'ssh -C'
-    finally:
-        os.environ = orig_environ
+def test_make_environment_with_passcommand_should_set_environment():
+    environment = module.make_environment({'encryption_passcommand': 'command'})
 
+    assert environment.get('BORG_PASSCOMMAND') == 'command'
 
-def test_initialize_without_configuration_should_only_set_default_environment():
-    orig_environ = os.environ
 
-    try:
-        os.environ = {}
-        module.initialize({})
+def test_make_environment_with_passphrase_should_set_environment():
+    environment = module.make_environment({'encryption_passphrase': 'pass'})
 
-        assert {key: value for key, value in os.environ.items() if key.startswith('BORG_')} == {
-            'BORG_RELOCATED_REPO_ACCESS_IS_OK': 'no',
-            'BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK': 'no',
-        }
-    finally:
-        os.environ = orig_environ
+    assert environment.get('BORG_PASSPHRASE') == 'pass'
 
 
-def test_initialize_with_relocated_repo_access_should_override_default():
-    orig_environ = os.environ
+def test_make_environment_with_ssh_command_should_set_environment():
+    environment = module.make_environment({'ssh_command': 'ssh -C'})
 
-    try:
-        os.environ = {}
-        module.initialize({'relocated_repo_access_is_ok': True})
-        assert os.environ.get('BORG_RELOCATED_REPO_ACCESS_IS_OK') == 'yes'
-    finally:
-        os.environ = orig_environ
+    assert environment.get('BORG_RSH') == 'ssh -C'
 
 
-def test_initialize_prefers_configuration_option_over_borg_environment_variable():
-    orig_environ = os.environ
+def test_make_environment_without_configuration_should_only_set_default_environment():
+    environment = module.make_environment({})
 
-    try:
-        os.environ = {'BORG_SSH': 'mosh'}
-        module.initialize({'ssh_command': 'ssh -C'})
-        assert os.environ.get('BORG_RSH') == 'ssh -C'
-    finally:
-        os.environ = orig_environ
+    assert environment == {
+        'BORG_RELOCATED_REPO_ACCESS_IS_OK': 'no',
+        'BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK': 'no',
+    }
 
 
-def test_initialize_passes_through_existing_borg_environment_variable():
-    orig_environ = os.environ
+def test_make_environment_with_relocated_repo_access_should_override_default():
+    environment = module.make_environment({'relocated_repo_access_is_ok': True})
 
-    try:
-        os.environ = {'BORG_PASSPHRASE': 'pass'}
-        module.initialize({'ssh_command': 'ssh -C'})
-        assert os.environ.get('BORG_PASSPHRASE') == 'pass'
-    finally:
-        os.environ = orig_environ
+    assert environment.get('BORG_RELOCATED_REPO_ACCESS_IS_OK') == 'yes'

+ 2 - 0
tests/unit/borg/test_export_tar.py

@@ -10,11 +10,13 @@ from ..test_verbosity import insert_logging_mock
 def insert_execute_command_mock(
     command, output_log_level=logging.INFO, borg_local_path='borg', capture=True
 ):
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         command,
         output_file=None if capture else module.DO_NOT_CAPTURE,
         output_log_level=output_log_level,
         borg_local_path=borg_local_path,
+        extra_environment=None,
     ).once()
 
 

+ 21 - 10
tests/unit/borg/test_extract.py

@@ -9,14 +9,16 @@ from ..test_verbosity import insert_logging_mock
 
 
 def insert_execute_command_mock(command, working_directory=None):
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
-        command, working_directory=working_directory
+        command, working_directory=working_directory, extra_environment=None,
     ).once()
 
 
 def insert_execute_command_output_mock(command, result):
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
-        command, output_log_level=None, borg_local_path=command[0]
+        command, output_log_level=None, borg_local_path=command[0], extra_environment=None,
     ).and_return(result).once()
 
 
@@ -27,14 +29,14 @@ def test_extract_last_archive_dry_run_calls_borg_with_last_archive():
     insert_execute_command_mock(('borg', 'extract', '--dry-run', 'repo::archive2'))
     flexmock(module.feature).should_receive('available').and_return(True)
 
-    module.extract_last_archive_dry_run(repository='repo', lock_wait=None)
+    module.extract_last_archive_dry_run(storage_config={}, repository='repo', lock_wait=None)
 
 
 def test_extract_last_archive_dry_run_without_any_archives_should_not_raise():
     insert_execute_command_output_mock(('borg', 'list', '--short', 'repo'), result='\n')
     flexmock(module.feature).should_receive('available').and_return(True)
 
-    module.extract_last_archive_dry_run(repository='repo', lock_wait=None)
+    module.extract_last_archive_dry_run(storage_config={}, repository='repo', lock_wait=None)
 
 
 def test_extract_last_archive_dry_run_with_log_info_calls_borg_with_info_parameter():
@@ -45,7 +47,7 @@ def test_extract_last_archive_dry_run_with_log_info_calls_borg_with_info_paramet
     insert_logging_mock(logging.INFO)
     flexmock(module.feature).should_receive('available').and_return(True)
 
-    module.extract_last_archive_dry_run(repository='repo', lock_wait=None)
+    module.extract_last_archive_dry_run(storage_config={}, repository='repo', lock_wait=None)
 
 
 def test_extract_last_archive_dry_run_with_log_debug_calls_borg_with_debug_parameter():
@@ -58,7 +60,7 @@ def test_extract_last_archive_dry_run_with_log_debug_calls_borg_with_debug_param
     insert_logging_mock(logging.DEBUG)
     flexmock(module.feature).should_receive('available').and_return(True)
 
-    module.extract_last_archive_dry_run(repository='repo', lock_wait=None)
+    module.extract_last_archive_dry_run(storage_config={}, repository='repo', lock_wait=None)
 
 
 def test_extract_last_archive_dry_run_calls_borg_via_local_path():
@@ -68,7 +70,9 @@ def test_extract_last_archive_dry_run_calls_borg_via_local_path():
     insert_execute_command_mock(('borg1', 'extract', '--dry-run', 'repo::archive2'))
     flexmock(module.feature).should_receive('available').and_return(True)
 
-    module.extract_last_archive_dry_run(repository='repo', lock_wait=None, local_path='borg1')
+    module.extract_last_archive_dry_run(
+        storage_config={}, repository='repo', lock_wait=None, local_path='borg1'
+    )
 
 
 def test_extract_last_archive_dry_run_calls_borg_with_remote_path_parameters():
@@ -80,7 +84,9 @@ def test_extract_last_archive_dry_run_calls_borg_with_remote_path_parameters():
     )
     flexmock(module.feature).should_receive('available').and_return(True)
 
-    module.extract_last_archive_dry_run(repository='repo', lock_wait=None, remote_path='borg1')
+    module.extract_last_archive_dry_run(
+        storage_config={}, repository='repo', lock_wait=None, remote_path='borg1'
+    )
 
 
 def test_extract_last_archive_dry_run_calls_borg_with_lock_wait_parameters():
@@ -92,7 +98,7 @@ def test_extract_last_archive_dry_run_calls_borg_with_lock_wait_parameters():
     )
     flexmock(module.feature).should_receive('available').and_return(True)
 
-    module.extract_last_archive_dry_run(repository='repo', lock_wait=5)
+    module.extract_last_archive_dry_run(storage_config={}, repository='repo', lock_wait=5)
 
 
 def test_extract_archive_calls_borg_with_path_parameters():
@@ -267,10 +273,12 @@ def test_extract_archive_calls_borg_with_strip_components():
 
 def test_extract_archive_calls_borg_with_progress_parameter():
     flexmock(module.os.path).should_receive('abspath').and_return('repo')
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'extract', '--progress', 'repo::archive'),
         output_file=module.DO_NOT_CAPTURE,
         working_directory=None,
+        extra_environment=None,
     ).once()
     flexmock(module.feature).should_receive('available').and_return(True)
 
@@ -306,11 +314,13 @@ def test_extract_archive_with_progress_and_extract_to_stdout_raises():
 def test_extract_archive_calls_borg_with_stdout_parameter_and_returns_process():
     flexmock(module.os.path).should_receive('abspath').and_return('repo')
     process = flexmock()
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'extract', '--stdout', 'repo::archive'),
         output_file=module.subprocess.PIPE,
         working_directory=None,
         run_to_completion=False,
+        extra_environment=None,
     ).and_return(process).once()
     flexmock(module.feature).should_receive('available').and_return(True)
 
@@ -331,8 +341,9 @@ def test_extract_archive_calls_borg_with_stdout_parameter_and_returns_process():
 
 def test_extract_archive_skips_abspath_for_remote_repository():
     flexmock(module.os.path).should_receive('abspath').never()
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
-        ('borg', 'extract', 'server:repo::archive'), working_directory=None
+        ('borg', 'extract', 'server:repo::archive'), working_directory=None, extra_environment=None,
     ).once()
     flexmock(module.feature).should_receive('available').and_return(True)
 

+ 43 - 7
tests/unit/borg/test_info.py

@@ -9,8 +9,12 @@ from ..test_verbosity import insert_logging_mock
 
 
 def test_display_archives_info_calls_borg_with_parameters():
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
-        ('borg', 'info', 'repo'), output_log_level=logging.WARNING, borg_local_path='borg'
+        ('borg', 'info', 'repo'),
+        output_log_level=logging.WARNING,
+        borg_local_path='borg',
+        extra_environment=None,
     )
 
     module.display_archives_info(
@@ -19,8 +23,12 @@ def test_display_archives_info_calls_borg_with_parameters():
 
 
 def test_display_archives_info_with_log_info_calls_borg_with_info_parameter():
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
-        ('borg', 'info', '--info', 'repo'), output_log_level=logging.WARNING, borg_local_path='borg'
+        ('borg', 'info', '--info', 'repo'),
+        output_log_level=logging.WARNING,
+        borg_local_path='borg',
+        extra_environment=None,
     )
     insert_logging_mock(logging.INFO)
     module.display_archives_info(
@@ -29,8 +37,12 @@ def test_display_archives_info_with_log_info_calls_borg_with_info_parameter():
 
 
 def test_display_archives_info_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', 'info', '--json', 'repo'), output_log_level=None, borg_local_path='borg'
+        ('borg', 'info', '--json', 'repo'),
+        output_log_level=None,
+        borg_local_path='borg',
+        extra_environment=None,
     ).and_return('[]')
 
     insert_logging_mock(logging.INFO)
@@ -42,10 +54,12 @@ def test_display_archives_info_with_log_info_and_json_suppresses_most_borg_outpu
 
 
 def test_display_archives_info_with_log_debug_calls_borg_with_debug_parameter():
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'info', '--debug', '--show-rc', 'repo'),
         output_log_level=logging.WARNING,
         borg_local_path='borg',
+        extra_environment=None,
     )
     insert_logging_mock(logging.DEBUG)
 
@@ -55,8 +69,12 @@ def test_display_archives_info_with_log_debug_calls_borg_with_debug_parameter():
 
 
 def test_display_archives_info_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', 'info', '--json', 'repo'), output_log_level=None, borg_local_path='borg'
+        ('borg', 'info', '--json', 'repo'),
+        output_log_level=None,
+        borg_local_path='borg',
+        extra_environment=None,
     ).and_return('[]')
 
     insert_logging_mock(logging.DEBUG)
@@ -68,8 +86,12 @@ def test_display_archives_info_with_log_debug_and_json_suppresses_most_borg_outp
 
 
 def test_display_archives_info_with_json_calls_borg_with_json_parameter():
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
-        ('borg', 'info', '--json', 'repo'), output_log_level=None, borg_local_path='borg'
+        ('borg', 'info', '--json', 'repo'),
+        output_log_level=None,
+        borg_local_path='borg',
+        extra_environment=None,
     ).and_return('[]')
 
     json_output = module.display_archives_info(
@@ -80,8 +102,12 @@ def test_display_archives_info_with_json_calls_borg_with_json_parameter():
 
 
 def test_display_archives_info_with_archive_calls_borg_with_archive_parameter():
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
-        ('borg', 'info', 'repo::archive'), output_log_level=logging.WARNING, borg_local_path='borg'
+        ('borg', 'info', 'repo::archive'),
+        output_log_level=logging.WARNING,
+        borg_local_path='borg',
+        extra_environment=None,
     )
 
     module.display_archives_info(
@@ -90,8 +116,12 @@ def test_display_archives_info_with_archive_calls_borg_with_archive_parameter():
 
 
 def test_display_archives_info_with_local_path_calls_borg_via_local_path():
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
-        ('borg1', 'info', 'repo'), output_log_level=logging.WARNING, borg_local_path='borg1'
+        ('borg1', 'info', 'repo'),
+        output_log_level=logging.WARNING,
+        borg_local_path='borg1',
+        extra_environment=None,
     )
 
     module.display_archives_info(
@@ -103,10 +133,12 @@ def test_display_archives_info_with_local_path_calls_borg_via_local_path():
 
 
 def test_display_archives_info_with_remote_path_calls_borg_with_remote_path_parameters():
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'info', '--remote-path', 'borg1', 'repo'),
         output_log_level=logging.WARNING,
         borg_local_path='borg',
+        extra_environment=None,
     )
 
     module.display_archives_info(
@@ -119,10 +151,12 @@ def test_display_archives_info_with_remote_path_calls_borg_with_remote_path_para
 
 def test_display_archives_info_with_lock_wait_calls_borg_with_lock_wait_parameters():
     storage_config = {'lock_wait': 5}
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'info', '--lock-wait', '5', 'repo'),
         output_log_level=logging.WARNING,
         borg_local_path='borg',
+        extra_environment=None,
     )
 
     module.display_archives_info(
@@ -134,10 +168,12 @@ def test_display_archives_info_with_lock_wait_calls_borg_with_lock_wait_paramete
 
 @pytest.mark.parametrize('argument_name', ('prefix', 'glob_archives', 'sort_by', 'first', 'last'))
 def test_display_archives_info_passes_through_arguments_to_borg(argument_name):
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'info', '--' + argument_name.replace('_', '-'), 'value', 'repo'),
         output_log_level=logging.WARNING,
         borg_local_path='borg',
+        extra_environment=None,
     )
 
     module.display_archives_info(

+ 6 - 1
tests/unit/borg/test_init.py

@@ -23,8 +23,12 @@ def insert_info_command_not_found_mock():
 
 
 def insert_init_command_mock(init_command, **kwargs):
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
-        init_command, output_file=module.DO_NOT_CAPTURE, borg_local_path=init_command[0]
+        init_command,
+        output_file=module.DO_NOT_CAPTURE,
+        borg_local_path=init_command[0],
+        extra_environment=None,
     ).once()
 
 
@@ -37,6 +41,7 @@ def test_initialize_repository_calls_borg_with_parameters():
 
 def test_initialize_repository_raises_for_borg_init_error():
     insert_info_command_not_found_mock()
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').and_raise(
         module.subprocess.CalledProcessError(2, 'borg init')
     )

+ 48 - 7
tests/unit/borg/test_list.py

@@ -24,8 +24,12 @@ def test_resolve_archive_name_passes_through_non_latest_archive_name():
 
 def test_resolve_archive_name_calls_borg_with_parameters():
     expected_archive = 'archive-name'
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
-        ('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS, output_log_level=None, borg_local_path='borg'
+        ('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS,
+        output_log_level=None,
+        borg_local_path='borg',
+        extra_environment=None,
     ).and_return(expected_archive + '\n')
 
     assert module.resolve_archive_name('repo', 'latest', storage_config={}) == expected_archive
@@ -33,10 +37,12 @@ def test_resolve_archive_name_calls_borg_with_parameters():
 
 def test_resolve_archive_name_with_log_info_calls_borg_with_info_parameter():
     expected_archive = 'archive-name'
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'list', '--info') + BORG_LIST_LATEST_ARGUMENTS,
         output_log_level=None,
         borg_local_path='borg',
+        extra_environment=None,
     ).and_return(expected_archive + '\n')
     insert_logging_mock(logging.INFO)
 
@@ -45,10 +51,12 @@ def test_resolve_archive_name_with_log_info_calls_borg_with_info_parameter():
 
 def test_resolve_archive_name_with_log_debug_calls_borg_with_debug_parameter():
     expected_archive = 'archive-name'
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'list', '--debug', '--show-rc') + BORG_LIST_LATEST_ARGUMENTS,
         output_log_level=None,
         borg_local_path='borg',
+        extra_environment=None,
     ).and_return(expected_archive + '\n')
     insert_logging_mock(logging.DEBUG)
 
@@ -57,10 +65,12 @@ def test_resolve_archive_name_with_log_debug_calls_borg_with_debug_parameter():
 
 def test_resolve_archive_name_with_local_path_calls_borg_via_local_path():
     expected_archive = 'archive-name'
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg1', 'list') + BORG_LIST_LATEST_ARGUMENTS,
         output_log_level=None,
         borg_local_path='borg1',
+        extra_environment=None,
     ).and_return(expected_archive + '\n')
 
     assert (
@@ -71,10 +81,12 @@ def test_resolve_archive_name_with_local_path_calls_borg_via_local_path():
 
 def test_resolve_archive_name_with_remote_path_calls_borg_with_remote_path_parameters():
     expected_archive = 'archive-name'
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'list', '--remote-path', 'borg1') + BORG_LIST_LATEST_ARGUMENTS,
         output_log_level=None,
         borg_local_path='borg',
+        extra_environment=None,
     ).and_return(expected_archive + '\n')
 
     assert (
@@ -84,8 +96,12 @@ def test_resolve_archive_name_with_remote_path_calls_borg_with_remote_path_param
 
 
 def test_resolve_archive_name_without_archives_raises():
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
-        ('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS, output_log_level=None, borg_local_path='borg'
+        ('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS,
+        output_log_level=None,
+        borg_local_path='borg',
+        extra_environment=None,
     ).and_return('')
 
     with pytest.raises(ValueError):
@@ -95,10 +111,12 @@ def test_resolve_archive_name_without_archives_raises():
 def test_resolve_archive_name_with_lock_wait_calls_borg_with_lock_wait_parameters():
     expected_archive = 'archive-name'
 
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'list', '--lock-wait', 'okay') + BORG_LIST_LATEST_ARGUMENTS,
         output_log_level=None,
         borg_local_path='borg',
+        extra_environment=None,
     ).and_return(expected_archive + '\n')
 
     assert (
@@ -296,8 +314,12 @@ def test_list_archives_calls_borg_with_parameters():
         remote_path=None,
     ).and_return(('borg', 'list', 'repo'))
     flexmock(module).should_receive('make_find_paths').and_return(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
-        ('borg', 'list', 'repo'), output_log_level=logging.WARNING, borg_local_path='borg'
+        ('borg', 'list', 'repo'),
+        output_log_level=logging.WARNING,
+        borg_local_path='borg',
+        extra_environment=None,
     ).once()
 
     module.list_archives(
@@ -316,8 +338,12 @@ def test_list_archives_with_json_suppresses_most_borg_output():
         remote_path=None,
     ).and_return(('borg', 'list', 'repo'))
     flexmock(module).should_receive('make_find_paths').and_return(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
-        ('borg', 'list', 'repo'), output_log_level=None, borg_local_path='borg'
+        ('borg', 'list', 'repo'),
+        output_log_level=None,
+        borg_local_path='borg',
+        extra_environment=None,
     ).once()
 
     module.list_archives(
@@ -336,8 +362,12 @@ def test_list_archives_calls_borg_with_local_path():
         remote_path=None,
     ).and_return(('borg2', 'list', 'repo'))
     flexmock(module).should_receive('make_find_paths').and_return(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
-        ('borg2', 'list', 'repo'), output_log_level=logging.WARNING, borg_local_path='borg2'
+        ('borg2', 'list', 'repo'),
+        output_log_level=logging.WARNING,
+        borg_local_path='borg2',
+        extra_environment=None,
     ).once()
 
     module.list_archives(
@@ -355,20 +385,27 @@ def test_list_archives_calls_borg_multiple_times_with_find_paths():
         ('borg', 'list', 'repo')
     ).and_return(('borg', 'list', 'repo::archive1')).and_return(('borg', 'list', 'repo::archive2'))
     flexmock(module).should_receive('make_find_paths').and_return(glob_paths)
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
-        ('borg', 'list', 'repo'), output_log_level=None, borg_local_path='borg'
+        ('borg', 'list', 'repo'),
+        output_log_level=None,
+        borg_local_path='borg',
+        extra_environment=None,
     ).and_return(
         'archive1   Sun, 2022-05-29 15:27:04 [abc]\narchive2   Mon, 2022-05-30 19:47:15 [xyz]'
     ).once()
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'list', 'repo::archive1') + glob_paths,
         output_log_level=logging.WARNING,
         borg_local_path='borg',
+        extra_environment=None,
     ).once()
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'list', 'repo::archive2') + glob_paths,
         output_log_level=logging.WARNING,
         borg_local_path='borg',
+        extra_environment=None,
     ).once()
 
     module.list_archives(
@@ -387,8 +424,12 @@ def test_list_archives_calls_borg_with_archive():
         remote_path=None,
     ).and_return(('borg', 'list', 'repo::archive'))
     flexmock(module).should_receive('make_find_paths').and_return(())
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
-        ('borg', 'list', 'repo::archive'), output_log_level=logging.WARNING, borg_local_path='borg'
+        ('borg', 'list', 'repo::archive'),
+        output_log_level=logging.WARNING,
+        borg_local_path='borg',
+        extra_environment=None,
     ).once()
 
     module.list_archives(

+ 4 - 1
tests/unit/borg/test_mount.py

@@ -8,8 +8,9 @@ from ..test_verbosity import insert_logging_mock
 
 
 def insert_execute_command_mock(command):
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
-        command, borg_local_path='borg'
+        command, borg_local_path='borg', extra_environment=None,
     ).once()
 
 
@@ -117,10 +118,12 @@ def test_mount_archive_with_log_debug_calls_borg_with_debug_parameters():
 
 
 def test_mount_archive_calls_borg_with_foreground_parameter():
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'mount', '--foreground', 'repo::archive', '/mnt'),
         output_file=module.DO_NOT_CAPTURE,
         borg_local_path='borg',
+        extra_environment=None,
     ).once()
 
     module.mount_archive(

+ 5 - 1
tests/unit/borg/test_prune.py

@@ -9,8 +9,12 @@ from ..test_verbosity import insert_logging_mock
 
 
 def insert_execute_command_mock(prune_command, output_log_level):
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
-        prune_command, output_log_level=output_log_level, borg_local_path=prune_command[0]
+        prune_command,
+        output_log_level=output_log_level,
+        borg_local_path=prune_command[0],
+        extra_environment=None,
     ).once()
 
 

+ 12 - 6
tests/unit/borg/test_version.py

@@ -11,39 +11,45 @@ VERSION = '1.2.3'
 
 
 def insert_execute_command_mock(command, borg_local_path='borg', version_output=f'borg {VERSION}'):
+    flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
-        command, output_log_level=None, borg_local_path=borg_local_path
+        command, output_log_level=None, borg_local_path=borg_local_path, extra_environment=None,
     ).once().and_return(version_output)
 
 
 def test_local_borg_version_calls_borg_with_required_parameters():
     insert_execute_command_mock(('borg', '--version'))
+    flexmock(module.environment).should_receive('make_environment')
 
-    assert module.local_borg_version() == VERSION
+    assert module.local_borg_version({}) == VERSION
 
 
 def test_local_borg_version_with_log_info_calls_borg_with_info_parameter():
     insert_execute_command_mock(('borg', '--version', '--info'))
     insert_logging_mock(logging.INFO)
+    flexmock(module.environment).should_receive('make_environment')
 
-    assert module.local_borg_version() == VERSION
+    assert module.local_borg_version({}) == VERSION
 
 
 def test_local_borg_version_with_log_debug_calls_borg_with_debug_parameters():
     insert_execute_command_mock(('borg', '--version', '--debug', '--show-rc'))
     insert_logging_mock(logging.DEBUG)
+    flexmock(module.environment).should_receive('make_environment')
 
-    assert module.local_borg_version() == VERSION
+    assert module.local_borg_version({}) == VERSION
 
 
 def test_local_borg_version_with_local_borg_path_calls_borg_with_it():
     insert_execute_command_mock(('borg1', '--version'), borg_local_path='borg1')
+    flexmock(module.environment).should_receive('make_environment')
 
-    assert module.local_borg_version('borg1') == VERSION
+    assert module.local_borg_version({}, 'borg1') == VERSION
 
 
 def test_local_borg_version_with_invalid_version_raises():
     insert_execute_command_mock(('borg', '--version'), version_output='wtf')
+    flexmock(module.environment).should_receive('make_environment')
 
     with pytest.raises(ValueError):
-        module.local_borg_version()
+        module.local_borg_version({})

+ 0 - 17
tests/unit/commands/test_borgmatic.py

@@ -9,7 +9,6 @@ from borgmatic.commands import borgmatic as module
 
 
 def test_run_configuration_runs_actions_for_each_repository():
-    flexmock(module.borg_environment).should_receive('initialize')
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     expected_results = [flexmock(), flexmock()]
     flexmock(module).should_receive('run_actions').and_return(expected_results[:1]).and_return(
@@ -24,7 +23,6 @@ def test_run_configuration_runs_actions_for_each_repository():
 
 
 def test_run_configuration_with_invalid_borg_version_errors():
-    flexmock(module.borg_environment).should_receive('initialize')
     flexmock(module.borg_version).should_receive('local_borg_version').and_raise(ValueError)
     flexmock(module.command).should_receive('execute_hook').never()
     flexmock(module.dispatch).should_receive('call_hooks').never()
@@ -36,7 +34,6 @@ def test_run_configuration_with_invalid_borg_version_errors():
 
 
 def test_run_configuration_logs_monitor_start_error():
-    flexmock(module.borg_environment).should_receive('initialize')
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     flexmock(module.dispatch).should_receive('call_hooks').and_raise(OSError).and_return(
         None
@@ -53,7 +50,6 @@ def test_run_configuration_logs_monitor_start_error():
 
 
 def test_run_configuration_bails_for_monitor_start_soft_failure():
-    flexmock(module.borg_environment).should_receive('initialize')
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     error = subprocess.CalledProcessError(borgmatic.hooks.command.SOFT_FAIL_EXIT_CODE, 'try again')
     flexmock(module.dispatch).should_receive('call_hooks').and_raise(error)
@@ -68,7 +64,6 @@ def test_run_configuration_bails_for_monitor_start_soft_failure():
 
 
 def test_run_configuration_logs_actions_error():
-    flexmock(module.borg_environment).should_receive('initialize')
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     flexmock(module.command).should_receive('execute_hook')
     flexmock(module.dispatch).should_receive('call_hooks')
@@ -84,7 +79,6 @@ def test_run_configuration_logs_actions_error():
 
 
 def test_run_configuration_bails_for_actions_soft_failure():
-    flexmock(module.borg_environment).should_receive('initialize')
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     flexmock(module.dispatch).should_receive('call_hooks')
     error = subprocess.CalledProcessError(borgmatic.hooks.command.SOFT_FAIL_EXIT_CODE, 'try again')
@@ -100,7 +94,6 @@ def test_run_configuration_bails_for_actions_soft_failure():
 
 
 def test_run_configuration_logs_monitor_finish_error():
-    flexmock(module.borg_environment).should_receive('initialize')
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     flexmock(module.dispatch).should_receive('call_hooks').and_return(None).and_return(
         None
@@ -117,7 +110,6 @@ def test_run_configuration_logs_monitor_finish_error():
 
 
 def test_run_configuration_bails_for_monitor_finish_soft_failure():
-    flexmock(module.borg_environment).should_receive('initialize')
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     error = subprocess.CalledProcessError(borgmatic.hooks.command.SOFT_FAIL_EXIT_CODE, 'try again')
     flexmock(module.dispatch).should_receive('call_hooks').and_return(None).and_return(
@@ -135,7 +127,6 @@ def test_run_configuration_bails_for_monitor_finish_soft_failure():
 
 
 def test_run_configuration_logs_on_error_hook_error():
-    flexmock(module.borg_environment).should_receive('initialize')
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     flexmock(module.command).should_receive('execute_hook').and_raise(OSError)
     expected_results = [flexmock(), flexmock()]
@@ -152,7 +143,6 @@ def test_run_configuration_logs_on_error_hook_error():
 
 
 def test_run_configuration_bails_for_on_error_hook_soft_failure():
-    flexmock(module.borg_environment).should_receive('initialize')
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     error = subprocess.CalledProcessError(borgmatic.hooks.command.SOFT_FAIL_EXIT_CODE, 'try again')
     flexmock(module.command).should_receive('execute_hook').and_raise(error)
@@ -169,7 +159,6 @@ def test_run_configuration_bails_for_on_error_hook_soft_failure():
 
 def test_run_configuration_retries_soft_error():
     # Run action first fails, second passes
-    flexmock(module.borg_environment).should_receive('initialize')
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     flexmock(module.command).should_receive('execute_hook')
     flexmock(module).should_receive('run_actions').and_raise(OSError).and_return([])
@@ -182,7 +171,6 @@ def test_run_configuration_retries_soft_error():
 
 def test_run_configuration_retries_hard_error():
     # Run action fails twice
-    flexmock(module.borg_environment).should_receive('initialize')
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     flexmock(module.command).should_receive('execute_hook')
     flexmock(module).should_receive('run_actions').and_raise(OSError).times(2)
@@ -203,7 +191,6 @@ def test_run_configuration_retries_hard_error():
 
 
 def test_run_repos_ordered():
-    flexmock(module.borg_environment).should_receive('initialize')
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     flexmock(module.command).should_receive('execute_hook')
     flexmock(module).should_receive('run_actions').and_raise(OSError).times(2)
@@ -221,7 +208,6 @@ def test_run_repos_ordered():
 
 
 def test_run_configuration_retries_round_robbin():
-    flexmock(module.borg_environment).should_receive('initialize')
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     flexmock(module.command).should_receive('execute_hook')
     flexmock(module).should_receive('run_actions').and_raise(OSError).times(4)
@@ -252,7 +238,6 @@ def test_run_configuration_retries_round_robbin():
 
 
 def test_run_configuration_retries_one_passes():
-    flexmock(module.borg_environment).should_receive('initialize')
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     flexmock(module.command).should_receive('execute_hook')
     flexmock(module).should_receive('run_actions').and_raise(OSError).and_raise(OSError).and_return(
@@ -281,7 +266,6 @@ def test_run_configuration_retries_one_passes():
 
 
 def test_run_configuration_retry_wait():
-    flexmock(module.borg_environment).should_receive('initialize')
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     flexmock(module.command).should_receive('execute_hook')
     flexmock(module).should_receive('run_actions').and_raise(OSError).times(4)
@@ -320,7 +304,6 @@ def test_run_configuration_retry_wait():
 
 
 def test_run_configuration_retries_timeout_multiple_repos():
-    flexmock(module.borg_environment).should_receive('initialize')
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     flexmock(module.command).should_receive('execute_hook')
     flexmock(module).should_receive('run_actions').and_raise(OSError).and_raise(OSError).and_return(