Bläddra i källkod

Fix logs that interfere with JSON output by making warnings go to stderr instead of stdout (#602).

Dan Helfman 2 år sedan
förälder
incheckning
c657764367

+ 3 - 0
NEWS

@@ -1,3 +1,6 @@
+1.7.6.dev0
+ * #602: Fix logs that interfere with JSON output by making warnings go to stderr instead of stdout.
+
 1.7.5
  * #311: Override PostgreSQL dump/restore commands via configuration options.
  * #604: Fix traceback when a configuration section is present but lacking any options.

+ 3 - 1
borgmatic/borg/borg.py

@@ -1,5 +1,6 @@
 import logging
 
+import borgmatic.logger
 from borgmatic.borg import environment, flags
 from borgmatic.execute import execute_command
 
@@ -25,6 +26,7 @@ def run_arbitrary_borg(
     sequence of arbitrary command-line Borg options, and an optional archive name, run an arbitrary
     Borg command on the given repository/archive.
     '''
+    borgmatic.logger.add_custom_log_levels()
     lock_wait = storage_config.get('lock_wait', None)
 
     try:
@@ -60,7 +62,7 @@ def run_arbitrary_borg(
 
     return execute_command(
         full_command,
-        output_log_level=logging.WARNING,
+        output_log_level=logging.ANSWER,
         borg_local_path=local_path,
         extra_environment=environment.make_environment(storage_config),
     )

+ 4 - 2
borgmatic/borg/create.py

@@ -6,6 +6,7 @@ import pathlib
 import stat
 import tempfile
 
+import borgmatic.logger
 from borgmatic.borg import environment, feature, flags, state
 from borgmatic.execute import (
     DO_NOT_CAPTURE,
@@ -305,6 +306,7 @@ def create_archive(
     If a sequence of stream processes is given (instances of subprocess.Popen), then execute the
     create command while also triggering the given processes to produce output.
     '''
+    borgmatic.logger.add_custom_log_levels()
     borgmatic_source_directories = expand_directories(
         collect_borgmatic_source_directories(location_config.get('borgmatic_source_directory'))
     )
@@ -406,8 +408,8 @@ def create_archive(
 
     if json:
         output_log_level = None
-    elif (stats or list_files) and logger.getEffectiveLevel() == logging.WARNING:
-        output_log_level = logging.WARNING
+    elif list_files or (stats and not dry_run):
+        output_log_level = logging.ANSWER
     else:
         output_log_level = logging.INFO
 

+ 4 - 2
borgmatic/borg/export_tar.py

@@ -1,6 +1,7 @@
 import logging
 import os
 
+import borgmatic.logger
 from borgmatic.borg import environment, flags
 from borgmatic.execute import DO_NOT_CAPTURE, execute_command
 
@@ -30,6 +31,7 @@ def export_tar_archive(
 
     If the destination path is "-", then stream the output to stdout instead of to a file.
     '''
+    borgmatic.logger.add_custom_log_levels()
     umask = storage_config.get('umask', None)
     lock_wait = storage_config.get('lock_wait', None)
 
@@ -53,8 +55,8 @@ def export_tar_archive(
         + (tuple(paths) if paths else ())
     )
 
-    if list_files and logger.getEffectiveLevel() == logging.WARNING:
-        output_log_level = logging.WARNING
+    if list_files:
+        output_log_level = logging.ANSWER
     else:
         output_log_level = logging.INFO
 

+ 3 - 1
borgmatic/borg/info.py

@@ -1,5 +1,6 @@
 import logging
 
+import borgmatic.logger
 from borgmatic.borg import environment, feature, flags
 from borgmatic.execute import execute_command, execute_command_and_capture_output
 
@@ -19,6 +20,7 @@ def display_archives_info(
     arguments to the info action, display summary information for Borg archives in the repository or
     return JSON summary information.
     '''
+    borgmatic.logger.add_custom_log_levels()
     lock_wait = storage_config.get('lock_wait', None)
 
     full_command = (
@@ -62,7 +64,7 @@ def display_archives_info(
     else:
         execute_command(
             full_command,
-            output_log_level=logging.WARNING,
+            output_log_level=logging.ANSWER,
             borg_local_path=local_path,
             extra_environment=environment.make_environment(storage_config),
         )

+ 5 - 2
borgmatic/borg/list.py

@@ -3,6 +3,7 @@ import copy
 import logging
 import re
 
+import borgmatic.logger
 from borgmatic.borg import environment, feature, flags, rlist
 from borgmatic.execute import execute_command, execute_command_and_capture_output
 
@@ -99,6 +100,8 @@ def list_archive(
     list the files by searching across multiple archives. If neither find_paths nor archive name
     are given, instead list the archives in the given repository.
     '''
+    borgmatic.logger.add_custom_log_levels()
+
     if not list_arguments.archive and not list_arguments.find_paths:
         if feature.available(feature.Feature.RLIST, local_borg_version):
             logger.warning(
@@ -170,7 +173,7 @@ def list_archive(
 
     # For each archive listed by Borg, run list on the contents of that archive.
     for archive in archive_lines:
-        logger.warning(f'{repository}: Listing archive {archive}')
+        logger.answer(f'{repository}: Listing archive {archive}')
 
         archive_arguments = copy.copy(list_arguments)
         archive_arguments.archive = archive
@@ -191,7 +194,7 @@ def list_archive(
 
         execute_command(
             main_command,
-            output_log_level=logging.WARNING,
+            output_log_level=logging.ANSWER,
             borg_local_path=local_path,
             extra_environment=borg_environment,
         )

+ 4 - 2
borgmatic/borg/prune.py

@@ -1,5 +1,6 @@
 import logging
 
+import borgmatic.logger
 from borgmatic.borg import environment, feature, flags
 from borgmatic.execute import execute_command
 
@@ -52,6 +53,7 @@ def prune_archives(
     retention config dict, prune Borg archives according to the retention policy specified in that
     configuration.
     '''
+    borgmatic.logger.add_custom_log_levels()
     umask = storage_config.get('umask', None)
     lock_wait = storage_config.get('lock_wait', None)
     extra_borg_options = storage_config.get('extra_borg_options', {}).get('prune', '')
@@ -75,8 +77,8 @@ def prune_archives(
         + flags.make_repository_flags(repository, local_borg_version)
     )
 
-    if (stats or list_archives) and logger.getEffectiveLevel() == logging.WARNING:
-        output_log_level = logging.WARNING
+    if stats or list_archives:
+        output_log_level = logging.ANSWER
     else:
         output_log_level = logging.INFO
 

+ 3 - 1
borgmatic/borg/rinfo.py

@@ -1,5 +1,6 @@
 import logging
 
+import borgmatic.logger
 from borgmatic.borg import environment, feature, flags
 from borgmatic.execute import execute_command, execute_command_and_capture_output
 
@@ -19,6 +20,7 @@ def display_repository_info(
     arguments to the rinfo action, display summary information for the Borg repository or return
     JSON summary information.
     '''
+    borgmatic.logger.add_custom_log_levels()
     lock_wait = storage_config.get('lock_wait', None)
 
     full_command = (
@@ -53,7 +55,7 @@ def display_repository_info(
     else:
         execute_command(
             full_command,
-            output_log_level=logging.WARNING,
+            output_log_level=logging.ANSWER,
             borg_local_path=local_path,
             extra_environment=extra_environment,
         )

+ 3 - 1
borgmatic/borg/rlist.py

@@ -1,5 +1,6 @@
 import logging
 
+import borgmatic.logger
 from borgmatic.borg import environment, feature, flags
 from borgmatic.execute import execute_command, execute_command_and_capture_output
 
@@ -108,6 +109,7 @@ def list_repository(
     arguments to the list action, and local and remote Borg paths, display the output of listing
     Borg archives in the given repository (or return JSON output).
     '''
+    borgmatic.logger.add_custom_log_levels()
     borg_environment = environment.make_environment(storage_config)
 
     main_command = make_rlist_command(
@@ -119,7 +121,7 @@ def list_repository(
     else:
         execute_command(
             main_command,
-            output_log_level=logging.WARNING,
+            output_log_level=logging.ANSWER,
             borg_local_path=local_path,
             extra_environment=borg_environment,
         )

+ 4 - 1
borgmatic/borg/transfer.py

@@ -1,5 +1,6 @@
 import logging
 
+import borgmatic.logger
 from borgmatic.borg import environment, flags
 from borgmatic.execute import execute_command
 
@@ -19,6 +20,8 @@ def transfer_archives(
     Given a dry-run flag, a local or remote repository path, a storage config dict, the local Borg
     version, and the arguments to the transfer action, transfer archives to the given repository.
     '''
+    borgmatic.logger.add_custom_log_levels()
+
     full_command = (
         (local_path, 'transfer')
         + (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
@@ -41,7 +44,7 @@ def transfer_archives(
 
     return execute_command(
         full_command,
-        output_log_level=logging.WARNING,
+        output_log_level=logging.ANSWER,
         borg_local_path=local_path,
         extra_environment=environment.make_environment(storage_config),
     )

+ 9 - 8
borgmatic/commands/borgmatic.py

@@ -33,7 +33,7 @@ from borgmatic.borg import version as borg_version
 from borgmatic.commands.arguments import parse_arguments
 from borgmatic.config import checks, collect, convert, validate
 from borgmatic.hooks import command, dispatch, dump, monitor
-from borgmatic.logger import configure_logging, should_do_markup
+from borgmatic.logger import add_custom_log_levels, configure_logging, should_do_markup
 from borgmatic.signals import configure_signals
 from borgmatic.verbosity import verbosity_to_log_level
 
@@ -244,6 +244,7 @@ def run_actions(
     action or a hook. Raise ValueError if the arguments or configuration passed to action are
     invalid.
     '''
+    add_custom_log_levels()
     repository = os.path.expanduser(repository_path)
     global_arguments = arguments['global']
     dry_run_label = ' (dry run; not making any changes)' if global_arguments.dry_run else ''
@@ -651,7 +652,7 @@ def run_actions(
         ):
             rlist_arguments = copy.copy(arguments['rlist'])
             if not rlist_arguments.json:  # pragma: nocover
-                logger.warning('{}: Listing repository'.format(repository))
+                logger.answer('{}: Listing repository'.format(repository))
             json_output = borg_rlist.list_repository(
                 repository,
                 storage,
@@ -669,9 +670,9 @@ def run_actions(
             list_arguments = copy.copy(arguments['list'])
             if not list_arguments.json:  # pragma: nocover
                 if list_arguments.find_paths:
-                    logger.warning('{}: Searching archives'.format(repository))
+                    logger.answer('{}: Searching archives'.format(repository))
                 elif not list_arguments.archive:
-                    logger.warning('{}: Listing archives'.format(repository))
+                    logger.answer('{}: Listing archives'.format(repository))
             list_arguments.archive = borg_rlist.resolve_archive_name(
                 repository,
                 list_arguments.archive,
@@ -696,7 +697,7 @@ def run_actions(
         ):
             rinfo_arguments = copy.copy(arguments['rinfo'])
             if not rinfo_arguments.json:  # pragma: nocover
-                logger.warning('{}: Displaying repository summary information'.format(repository))
+                logger.answer('{}: Displaying repository summary information'.format(repository))
             json_output = borg_rinfo.display_repository_info(
                 repository,
                 storage,
@@ -713,7 +714,7 @@ def run_actions(
         ):
             info_arguments = copy.copy(arguments['info'])
             if not info_arguments.json:  # pragma: nocover
-                logger.warning('{}: Displaying archive summary information'.format(repository))
+                logger.answer('{}: Displaying archive summary information'.format(repository))
             info_arguments.archive = borg_rlist.resolve_archive_name(
                 repository,
                 info_arguments.archive,
@@ -736,7 +737,7 @@ def run_actions(
         if arguments['break-lock'].repository is None or validate.repositories_match(
             repository, arguments['break-lock'].repository
         ):
-            logger.warning(f'{repository}: Breaking repository and cache locks')
+            logger.info(f'{repository}: Breaking repository and cache locks')
             borg_break_lock.break_lock(
                 repository,
                 storage,
@@ -748,7 +749,7 @@ def run_actions(
         if arguments['borg'].repository is None or validate.repositories_match(
             repository, arguments['borg'].repository
         ):
-            logger.warning('{}: Running arbitrary Borg command'.format(repository))
+            logger.info('{}: Running arbitrary Borg command'.format(repository))
             archive_name = borg_rlist.resolve_archive_name(
                 repository,
                 arguments['borg'].archive,

+ 54 - 11
borgmatic/logger.py

@@ -85,18 +85,19 @@ class Multi_stream_handler(logging.Handler):
             handler.setLevel(level)
 
 
-LOG_LEVEL_TO_COLOR = {
-    logging.CRITICAL: colorama.Fore.RED,
-    logging.ERROR: colorama.Fore.RED,
-    logging.WARN: colorama.Fore.YELLOW,
-    logging.INFO: colorama.Fore.GREEN,
-    logging.DEBUG: colorama.Fore.CYAN,
-}
-
-
 class Console_color_formatter(logging.Formatter):
     def format(self, record):
-        color = LOG_LEVEL_TO_COLOR.get(record.levelno)
+        add_custom_log_levels()
+
+        color = {
+            logging.CRITICAL: colorama.Fore.RED,
+            logging.ERROR: colorama.Fore.RED,
+            logging.WARN: colorama.Fore.YELLOW,
+            logging.ANSWER: colorama.Fore.MAGENTA,
+            logging.INFO: colorama.Fore.GREEN,
+            logging.DEBUG: colorama.Fore.CYAN,
+        }.get(record.levelno)
+
         return color_text(color, record.msg)
 
 
@@ -110,6 +111,45 @@ def color_text(color, message):
     return '{}{}{}'.format(color, message, colorama.Style.RESET_ALL)
 
 
+def add_logging_level(level_name, level_number):
+    '''
+    Globally add a custom logging level based on the given (all uppercase) level name and number.
+    Do this idempotently.
+
+    Inspired by https://stackoverflow.com/questions/2183233/how-to-add-a-custom-loglevel-to-pythons-logging-facility/35804945#35804945
+    '''
+    method_name = level_name.lower()
+
+    if not hasattr(logging, level_name):
+        logging.addLevelName(level_number, level_name)
+        setattr(logging, level_name, level_number)
+
+    if not hasattr(logging, method_name):
+
+        def log_for_level(self, message, *args, **kwargs):  # pragma: no cover
+            if self.isEnabledFor(level_number):
+                self._log(level_number, message, args, **kwargs)
+
+        setattr(logging.getLoggerClass(), method_name, log_for_level)
+
+    if not hasattr(logging.getLoggerClass(), method_name):
+
+        def log_to_root(message, *args, **kwargs):  # pragma: no cover
+            logging.log(level_number, message, *args, **kwargs)
+
+        setattr(logging, method_name, log_to_root)
+
+
+ANSWER = logging.WARN - 5
+
+
+def add_custom_log_levels():  # pragma: no cover
+    '''
+    Add a custom log level between WARN and INFO for user-requested answers.
+    '''
+    add_logging_level('ANSWER', ANSWER)
+
+
 def configure_logging(
     console_log_level,
     syslog_log_level=None,
@@ -130,6 +170,8 @@ def configure_logging(
     if monitoring_log_level is None:
         monitoring_log_level = console_log_level
 
+    add_custom_log_levels()
+
     # Log certain log levels to console stderr and others to stdout. This supports use cases like
     # grepping (non-error) output.
     console_error_handler = logging.StreamHandler(sys.stderr)
@@ -138,7 +180,8 @@ def configure_logging(
         {
             logging.CRITICAL: console_error_handler,
             logging.ERROR: console_error_handler,
-            logging.WARN: console_standard_handler,
+            logging.WARN: console_error_handler,
+            logging.ANSWER: console_standard_handler,
             logging.INFO: console_standard_handler,
             logging.DEBUG: console_standard_handler,
         }

+ 6 - 2
borgmatic/verbosity.py

@@ -1,7 +1,9 @@
 import logging
 
+import borgmatic.logger
+
 VERBOSITY_ERROR = -1
-VERBOSITY_WARNING = 0
+VERBOSITY_ANSWER = 0
 VERBOSITY_SOME = 1
 VERBOSITY_LOTS = 2
 
@@ -10,9 +12,11 @@ def verbosity_to_log_level(verbosity):
     '''
     Given a borgmatic verbosity value, return the corresponding Python log level.
     '''
+    borgmatic.logger.add_custom_log_levels()
+
     return {
         VERBOSITY_ERROR: logging.ERROR,
-        VERBOSITY_WARNING: logging.WARNING,
+        VERBOSITY_ANSWER: logging.ANSWER,
         VERBOSITY_SOME: logging.INFO,
         VERBOSITY_LOTS: logging.DEBUG,
     }.get(verbosity, logging.WARNING)

+ 1 - 1
setup.py

@@ -1,6 +1,6 @@
 from setuptools import find_packages, setup
 
-VERSION = '1.7.5'
+VERSION = '1.7.6.dev0'
 
 
 setup(

+ 45 - 14
tests/unit/borg/test_borg.py

@@ -8,12 +8,14 @@ from ..test_verbosity import insert_logging_mock
 
 
 def test_run_arbitrary_borg_calls_borg_with_parameters():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'break-lock', 'repo'),
-        output_log_level=logging.WARNING,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg',
         extra_environment=None,
     )
@@ -24,12 +26,14 @@ def test_run_arbitrary_borg_calls_borg_with_parameters():
 
 
 def test_run_arbitrary_borg_with_log_info_calls_borg_with_info_parameter():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_flags').and_return(())
     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,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg',
         extra_environment=None,
     )
@@ -41,12 +45,14 @@ 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.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_flags').and_return(())
     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,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg',
         extra_environment=None,
     )
@@ -58,6 +64,8 @@ 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():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     storage_config = {'lock_wait': 5}
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_flags').and_return(()).and_return(
@@ -66,7 +74,7 @@ def test_run_arbitrary_borg_with_lock_wait_calls_borg_with_lock_wait_parameters(
     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,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg',
         extra_environment=None,
     )
@@ -80,6 +88,8 @@ 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():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
         ('repo::archive',)
     )
@@ -87,7 +97,7 @@ def test_run_arbitrary_borg_with_archive_calls_borg_with_archive_parameter():
     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,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg',
         extra_environment=None,
     )
@@ -102,12 +112,14 @@ 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.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg1', 'break-lock', 'repo'),
-        output_log_level=logging.WARNING,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg1',
         extra_environment=None,
     )
@@ -122,6 +134,8 @@ 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.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_flags').and_return(
         ('--remote-path', 'borg1')
@@ -129,7 +143,7 @@ def test_run_arbitrary_borg_with_remote_path_calls_borg_with_remote_path_paramet
     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,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg',
         extra_environment=None,
     )
@@ -144,12 +158,14 @@ 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.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'list', 'repo', '--progress'),
-        output_log_level=logging.WARNING,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg',
         extra_environment=None,
     )
@@ -163,12 +179,14 @@ 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.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'break-lock', 'repo'),
-        output_log_level=logging.WARNING,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg',
         extra_environment=None,
     )
@@ -182,11 +200,16 @@ 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.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_repository_flags').never()
     flexmock(module.flags).should_receive('make_flags').and_return(())
     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', extra_environment=None,
+        ('borg',),
+        output_log_level=module.borgmatic.logger.ANSWER,
+        borg_local_path='borg',
+        extra_environment=None,
     )
 
     module.run_arbitrary_borg(
@@ -195,12 +218,14 @@ 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.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'key', 'export', 'repo'),
-        output_log_level=logging.WARNING,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg',
         extra_environment=None,
     )
@@ -211,12 +236,14 @@ 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.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_flags').and_return(())
     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,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg',
         extra_environment=None,
     )
@@ -230,12 +257,14 @@ 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.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_repository_flags').never()
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'debug', 'info'),
-        output_log_level=logging.WARNING,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg',
         extra_environment=None,
     )
@@ -246,12 +275,14 @@ 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.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_repository_flags').never()
     flexmock(module.flags).should_receive('make_flags').and_return(())
     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,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg',
         extra_environment=None,
     )

+ 92 - 86
tests/unit/borg/test_create.py

@@ -413,6 +413,8 @@ REPO_ARCHIVE_WITH_PATHS = (f'repo::{DEFAULT_ARCHIVE_NAME}', 'foo', 'bar')
 
 
 def test_create_archive_calls_borg_with_parameters():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
@@ -452,6 +454,8 @@ def test_create_archive_calls_borg_with_parameters():
 
 
 def test_create_archive_calls_borg_with_environment():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
@@ -492,6 +496,8 @@ def test_create_archive_calls_borg_with_environment():
 
 
 def test_create_archive_with_patterns_calls_borg_with_patterns_including_converted_source_directories():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     pattern_flags = ('--patterns-from', 'patterns')
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
@@ -534,6 +540,8 @@ def test_create_archive_with_patterns_calls_borg_with_patterns_including_convert
 
 
 def test_create_archive_with_exclude_patterns_calls_borg_with_excludes():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     exclude_flags = ('--exclude-from', 'excludes')
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
@@ -576,6 +584,8 @@ def test_create_archive_with_exclude_patterns_calls_borg_with_excludes():
 
 
 def test_create_archive_with_log_info_calls_borg_with_info_parameter():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
@@ -616,6 +626,8 @@ def test_create_archive_with_log_info_calls_borg_with_info_parameter():
 
 
 def test_create_archive_with_log_info_and_json_suppresses_most_borg_output():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
@@ -654,6 +666,8 @@ def test_create_archive_with_log_info_and_json_suppresses_most_borg_output():
 
 
 def test_create_archive_with_log_debug_calls_borg_with_debug_parameter():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
@@ -694,6 +708,8 @@ def test_create_archive_with_log_debug_calls_borg_with_debug_parameter():
 
 
 def test_create_archive_with_log_debug_and_json_suppresses_most_borg_output():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
@@ -732,6 +748,8 @@ def test_create_archive_with_log_debug_and_json_suppresses_most_borg_output():
 
 
 def test_create_archive_with_dry_run_calls_borg_with_dry_run_parameter():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
@@ -773,6 +791,8 @@ def test_create_archive_with_dry_run_calls_borg_with_dry_run_parameter():
 def test_create_archive_with_stats_and_dry_run_calls_borg_without_stats_parameter():
     # --dry-run and --stats are mutually exclusive, see:
     # https://borgbackup.readthedocs.io/en/stable/usage/create.html#description
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
@@ -814,6 +834,8 @@ def test_create_archive_with_stats_and_dry_run_calls_borg_without_stats_paramete
 
 
 def test_create_archive_with_checkpoint_interval_calls_borg_with_checkpoint_interval_parameters():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
@@ -853,6 +875,8 @@ def test_create_archive_with_checkpoint_interval_calls_borg_with_checkpoint_inte
 
 
 def test_create_archive_with_chunker_params_calls_borg_with_chunker_params_parameters():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
@@ -892,6 +916,8 @@ def test_create_archive_with_chunker_params_calls_borg_with_chunker_params_param
 
 
 def test_create_archive_with_compression_calls_borg_with_compression_parameters():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
@@ -936,6 +962,8 @@ def test_create_archive_with_compression_calls_borg_with_compression_parameters(
 def test_create_archive_with_upload_rate_limit_calls_borg_with_upload_ratelimit_parameters(
     feature_available, option_flag
 ):
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
@@ -975,6 +1003,8 @@ def test_create_archive_with_upload_rate_limit_calls_borg_with_upload_ratelimit_
 
 
 def test_create_archive_with_working_directory_calls_borg_with_working_directory():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
@@ -1017,6 +1047,8 @@ def test_create_archive_with_working_directory_calls_borg_with_working_directory
 
 
 def test_create_archive_with_one_file_system_calls_borg_with_one_file_system_parameter():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
@@ -1062,6 +1094,8 @@ def test_create_archive_with_one_file_system_calls_borg_with_one_file_system_par
 def test_create_archive_with_numeric_ids_calls_borg_with_numeric_ids_parameter(
     feature_available, option_flag
 ):
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
@@ -1102,6 +1136,8 @@ def test_create_archive_with_numeric_ids_calls_borg_with_numeric_ids_parameter(
 
 
 def test_create_archive_with_read_special_calls_borg_with_read_special_parameter():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
@@ -1158,6 +1194,8 @@ def test_create_archive_with_read_special_calls_borg_with_read_special_parameter
 def test_create_archive_with_basic_option_calls_borg_with_corresponding_parameter(
     option_name, option_value
 ):
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     option_flag = '--no' + option_name.replace('', '') if option_value is False else None
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
@@ -1210,6 +1248,8 @@ def test_create_archive_with_basic_option_calls_borg_with_corresponding_paramete
 def test_create_archive_with_atime_option_calls_borg_with_corresponding_parameter(
     option_value, feature_available, option_flag
 ):
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
@@ -1261,6 +1301,8 @@ def test_create_archive_with_atime_option_calls_borg_with_corresponding_paramete
 def test_create_archive_with_flags_option_calls_borg_with_corresponding_parameter(
     option_value, feature_available, option_flag
 ):
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
@@ -1301,6 +1343,8 @@ def test_create_archive_with_flags_option_calls_borg_with_corresponding_paramete
 
 
 def test_create_archive_with_files_cache_calls_borg_with_files_cache_parameters():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
@@ -1341,6 +1385,8 @@ def test_create_archive_with_files_cache_calls_borg_with_files_cache_parameters(
 
 
 def test_create_archive_with_local_path_calls_borg_via_local_path():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
@@ -1381,6 +1427,8 @@ def test_create_archive_with_local_path_calls_borg_via_local_path():
 
 
 def test_create_archive_with_remote_path_calls_borg_with_remote_path_parameters():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
@@ -1421,6 +1469,8 @@ def test_create_archive_with_remote_path_calls_borg_with_remote_path_parameters(
 
 
 def test_create_archive_with_umask_calls_borg_with_umask_parameters():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
@@ -1460,6 +1510,8 @@ def test_create_archive_with_umask_calls_borg_with_umask_parameters():
 
 
 def test_create_archive_with_lock_wait_calls_borg_with_lock_wait_parameters():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
@@ -1498,7 +1550,9 @@ def test_create_archive_with_lock_wait_calls_borg_with_lock_wait_parameters():
     )
 
 
-def test_create_archive_with_stats_calls_borg_with_stats_parameter_and_warning_output_log_level():
+def test_create_archive_with_stats_calls_borg_with_stats_parameter_and_answer_output_log_level():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
@@ -1517,7 +1571,7 @@ def test_create_archive_with_stats_calls_borg_with_stats_parameter_and_warning_o
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create') + REPO_ARCHIVE_WITH_PATHS + ('--stats',),
-        output_log_level=logging.WARNING,
+        output_log_level=module.borgmatic.logger.ANSWER,
         output_file=None,
         borg_local_path='borg',
         working_directory=None,
@@ -1538,48 +1592,9 @@ def test_create_archive_with_stats_calls_borg_with_stats_parameter_and_warning_o
     )
 
 
-def test_create_archive_with_stats_and_log_info_calls_borg_with_stats_parameter_and_info_output_log_level():
-    flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
-    flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
-    flexmock(module).should_receive('map_directories_to_devices').and_return({})
-    flexmock(module).should_receive('expand_directories').and_return(())
-    flexmock(module).should_receive('pattern_root_directories').and_return([])
-    flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
-    flexmock(module).should_receive('expand_home_directories').and_return(())
-    flexmock(module).should_receive('write_pattern_file').and_return(None)
-    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(())
-    flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
-        (f'repo::{DEFAULT_ARCHIVE_NAME}',)
-    )
-    flexmock(module.environment).should_receive('make_environment')
-    flexmock(module).should_receive('execute_command').with_args(
-        ('borg', 'create') + REPO_ARCHIVE_WITH_PATHS + ('--info', '--stats'),
-        output_log_level=logging.INFO,
-        output_file=None,
-        borg_local_path='borg',
-        working_directory=None,
-        extra_environment=None,
-    )
-    insert_logging_mock(logging.INFO)
-
-    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',
-        stats=True,
-    )
-
-
-def test_create_archive_with_files_calls_borg_with_list_parameter_and_warning_output_log_level():
+def test_create_archive_with_files_calls_borg_with_list_parameter_and_answer_output_log_level():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
@@ -1598,7 +1613,7 @@ def test_create_archive_with_files_calls_borg_with_list_parameter_and_warning_ou
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create', '--list', '--filter', 'AMEx-') + REPO_ARCHIVE_WITH_PATHS,
-        output_log_level=logging.WARNING,
+        output_log_level=module.borgmatic.logger.ANSWER,
         output_file=None,
         borg_local_path='borg',
         working_directory=None,
@@ -1619,48 +1634,9 @@ def test_create_archive_with_files_calls_borg_with_list_parameter_and_warning_ou
     )
 
 
-def test_create_archive_with_files_and_log_info_calls_borg_with_list_parameter_and_info_output_log_level():
-    flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
-    flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
-    flexmock(module).should_receive('map_directories_to_devices').and_return({})
-    flexmock(module).should_receive('expand_directories').and_return(())
-    flexmock(module).should_receive('pattern_root_directories').and_return([])
-    flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
-    flexmock(module).should_receive('expand_home_directories').and_return(())
-    flexmock(module).should_receive('write_pattern_file').and_return(None)
-    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(())
-    flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
-        (f'repo::{DEFAULT_ARCHIVE_NAME}',)
-    )
-    flexmock(module.environment).should_receive('make_environment')
-    flexmock(module).should_receive('execute_command').with_args(
-        ('borg', 'create', '--list', '--filter', 'AMEx-') + REPO_ARCHIVE_WITH_PATHS + ('--info',),
-        output_log_level=logging.INFO,
-        output_file=None,
-        borg_local_path='borg',
-        working_directory=None,
-        extra_environment=None,
-    )
-    insert_logging_mock(logging.INFO)
-
-    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',
-        list_files=True,
-    )
-
-
 def test_create_archive_with_progress_and_log_info_calls_borg_with_progress_parameter_and_no_list():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
@@ -1702,6 +1678,8 @@ def test_create_archive_with_progress_and_log_info_calls_borg_with_progress_para
 
 
 def test_create_archive_with_progress_calls_borg_with_progress_parameter():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
@@ -1742,6 +1720,8 @@ def test_create_archive_with_progress_calls_borg_with_progress_parameter():
 
 
 def test_create_archive_with_progress_and_stream_processes_calls_borg_with_progress_parameter():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     processes = flexmock()
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
@@ -1800,6 +1780,8 @@ def test_create_archive_with_progress_and_stream_processes_calls_borg_with_progr
 
 
 def test_create_archive_with_stream_processes_ignores_read_special_false_and_logs_warnings():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     processes = flexmock()
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
@@ -1860,6 +1842,8 @@ def test_create_archive_with_stream_processes_ignores_read_special_false_and_log
 
 
 def test_create_archive_with_stream_processes_adds_special_files_to_excludes():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     processes = flexmock()
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
@@ -1924,6 +1908,8 @@ def test_create_archive_with_stream_processes_adds_special_files_to_excludes():
 
 
 def test_create_archive_with_stream_processes_and_read_special_does_not_add_special_files_to_excludes():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     processes = flexmock()
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
@@ -1985,6 +1971,8 @@ def test_create_archive_with_stream_processes_and_read_special_does_not_add_spec
 
 
 def test_create_archive_with_json_calls_borg_with_json_parameter():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
@@ -2024,6 +2012,8 @@ def test_create_archive_with_json_calls_borg_with_json_parameter():
 
 
 def test_create_archive_with_stats_and_json_calls_borg_without_stats_parameter():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
@@ -2064,6 +2054,8 @@ def test_create_archive_with_stats_and_json_calls_borg_without_stats_parameter()
 
 
 def test_create_archive_with_source_directories_glob_expands():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'food'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
@@ -2104,6 +2096,8 @@ def test_create_archive_with_source_directories_glob_expands():
 
 
 def test_create_archive_with_non_matching_source_directories_glob_passes_through():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo*',))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
@@ -2144,6 +2138,8 @@ def test_create_archive_with_non_matching_source_directories_glob_passes_through
 
 
 def test_create_archive_with_glob_calls_borg_with_expanded_directories():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'food'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
@@ -2183,6 +2179,8 @@ def test_create_archive_with_glob_calls_borg_with_expanded_directories():
 
 
 def test_create_archive_with_archive_name_format_calls_borg_with_archive_name():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
@@ -2222,6 +2220,8 @@ def test_create_archive_with_archive_name_format_calls_borg_with_archive_name():
 
 
 def test_create_archive_with_archive_name_format_accepts_borg_placeholders():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     repository_archive_pattern = 'repo::Documents_{hostname}-{now}'
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
@@ -2262,6 +2262,8 @@ def test_create_archive_with_archive_name_format_accepts_borg_placeholders():
 
 
 def test_create_archive_with_repository_accepts_borg_placeholders():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     repository_archive_pattern = '{fqdn}::Documents_{hostname}-{now}'
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
@@ -2302,6 +2304,8 @@ def test_create_archive_with_repository_accepts_borg_placeholders():
 
 
 def test_create_archive_with_extra_borg_options_calls_borg_with_extra_options():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
@@ -2341,6 +2345,8 @@ def test_create_archive_with_extra_borg_options_calls_borg_with_extra_options():
 
 
 def test_create_archive_with_stream_processes_calls_borg_with_processes_and_read_special():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     processes = flexmock()
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))

+ 27 - 1
tests/unit/borg/test_export_tar.py

@@ -21,6 +21,8 @@ def insert_execute_command_mock(
 
 
 def test_export_tar_archive_calls_borg_with_path_parameters():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
         ('repo::archive',)
     )
@@ -41,6 +43,8 @@ def test_export_tar_archive_calls_borg_with_path_parameters():
 
 
 def test_export_tar_archive_calls_borg_with_local_path_parameters():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
         ('repo::archive',)
     )
@@ -62,6 +66,8 @@ def test_export_tar_archive_calls_borg_with_local_path_parameters():
 
 
 def test_export_tar_archive_calls_borg_with_remote_path_parameters():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
         ('repo::archive',)
     )
@@ -83,6 +89,8 @@ def test_export_tar_archive_calls_borg_with_remote_path_parameters():
 
 
 def test_export_tar_archive_calls_borg_with_umask_parameters():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
         ('repo::archive',)
     )
@@ -103,6 +111,8 @@ def test_export_tar_archive_calls_borg_with_umask_parameters():
 
 
 def test_export_tar_archive_calls_borg_with_lock_wait_parameters():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
         ('repo::archive',)
     )
@@ -123,6 +133,8 @@ def test_export_tar_archive_calls_borg_with_lock_wait_parameters():
 
 
 def test_export_tar_archive_with_log_info_calls_borg_with_info_parameter():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
         ('repo::archive',)
     )
@@ -142,6 +154,8 @@ def test_export_tar_archive_with_log_info_calls_borg_with_info_parameter():
 
 
 def test_export_tar_archive_with_log_debug_calls_borg_with_debug_parameters():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
         ('repo::archive',)
     )
@@ -163,6 +177,8 @@ def test_export_tar_archive_with_log_debug_calls_borg_with_debug_parameters():
 
 
 def test_export_tar_archive_calls_borg_with_dry_run_parameter():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
         ('repo::archive',)
     )
@@ -181,6 +197,8 @@ def test_export_tar_archive_calls_borg_with_dry_run_parameter():
 
 
 def test_export_tar_archive_calls_borg_with_tar_filter_parameters():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
         ('repo::archive',)
     )
@@ -202,13 +220,15 @@ def test_export_tar_archive_calls_borg_with_tar_filter_parameters():
 
 
 def test_export_tar_archive_calls_borg_with_list_parameter():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
         ('repo::archive',)
     )
     flexmock(module.os.path).should_receive('abspath').and_return('repo')
     insert_execute_command_mock(
         ('borg', 'export-tar', '--list', 'repo::archive', 'test.tar'),
-        output_log_level=logging.WARNING,
+        output_log_level=logging.ANSWER,
     )
 
     module.export_tar_archive(
@@ -224,6 +244,8 @@ def test_export_tar_archive_calls_borg_with_list_parameter():
 
 
 def test_export_tar_archive_calls_borg_with_strip_components_parameter():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
         ('repo::archive',)
     )
@@ -245,6 +267,8 @@ def test_export_tar_archive_calls_borg_with_strip_components_parameter():
 
 
 def test_export_tar_archive_skips_abspath_for_remote_repository_parameter():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
         ('server:repo::archive',)
     )
@@ -263,6 +287,8 @@ def test_export_tar_archive_skips_abspath_for_remote_repository_parameter():
 
 
 def test_export_tar_archive_calls_borg_with_stdout_destination_path():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
         ('repo::archive',)
     )

+ 33 - 9
tests/unit/borg/test_info.py

@@ -9,13 +9,15 @@ from ..test_verbosity import insert_logging_mock
 
 
 def test_display_archives_info_calls_borg_with_parameters():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'info', '--repo', 'repo'),
-        output_log_level=logging.WARNING,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg',
         extra_environment=None,
     )
@@ -29,13 +31,15 @@ def test_display_archives_info_calls_borg_with_parameters():
 
 
 def test_display_archives_info_with_log_info_calls_borg_with_info_parameter():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'info', '--info', '--repo', 'repo'),
-        output_log_level=logging.WARNING,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg',
         extra_environment=None,
     )
@@ -49,6 +53,8 @@ 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.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
@@ -69,13 +75,15 @@ 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.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'info', '--debug', '--show-rc', '--repo', 'repo'),
-        output_log_level=logging.WARNING,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg',
         extra_environment=None,
     )
@@ -90,6 +98,8 @@ 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.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
@@ -110,6 +120,8 @@ 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.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
@@ -129,6 +141,8 @@ def test_display_archives_info_with_json_calls_borg_with_json_parameter():
 
 
 def test_display_archives_info_with_archive_calls_borg_with_match_archives_parameter():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').with_args(
         'match-archives', 'archive'
@@ -138,7 +152,7 @@ def test_display_archives_info_with_archive_calls_borg_with_match_archives_param
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'info', '--repo', 'repo', '--match-archives', 'archive'),
-        output_log_level=logging.WARNING,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg',
         extra_environment=None,
     )
@@ -152,13 +166,15 @@ def test_display_archives_info_with_archive_calls_borg_with_match_archives_param
 
 
 def test_display_archives_info_with_local_path_calls_borg_via_local_path():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg1', 'info', '--repo', 'repo'),
-        output_log_level=logging.WARNING,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg1',
         extra_environment=None,
     )
@@ -173,6 +189,8 @@ 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.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').with_args(
         'remote-path', 'borg1'
@@ -182,7 +200,7 @@ def test_display_archives_info_with_remote_path_calls_borg_with_remote_path_para
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'info', '--remote-path', 'borg1', '--repo', 'repo'),
-        output_log_level=logging.WARNING,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg',
         extra_environment=None,
     )
@@ -197,6 +215,8 @@ 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():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').with_args('lock-wait', 5).and_return(
         ('--lock-wait', '5')
@@ -207,7 +227,7 @@ def test_display_archives_info_with_lock_wait_calls_borg_with_lock_wait_paramete
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'info', '--lock-wait', '5', '--repo', 'repo'),
-        output_log_level=logging.WARNING,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg',
         extra_environment=None,
     )
@@ -221,6 +241,8 @@ def test_display_archives_info_with_lock_wait_calls_borg_with_lock_wait_paramete
 
 
 def test_display_archives_info_with_prefix_calls_borg_with_match_archives_parameters():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').with_args(
         'match-archives', 'sh:foo*'
@@ -230,7 +252,7 @@ def test_display_archives_info_with_prefix_calls_borg_with_match_archives_parame
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'info', '--match-archives', 'sh:foo*', '--repo', 'repo'),
-        output_log_level=logging.WARNING,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg',
         extra_environment=None,
     )
@@ -245,6 +267,8 @@ def test_display_archives_info_with_prefix_calls_borg_with_match_archives_parame
 
 @pytest.mark.parametrize('argument_name', ('match_archives', 'sort_by', 'first', 'last'))
 def test_display_archives_info_passes_through_arguments_to_borg(argument_name):
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flag_name = f"--{argument_name.replace('_', ' ')}"
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(
@@ -254,7 +278,7 @@ 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', flag_name, 'value', '--repo', 'repo'),
-        output_log_level=logging.WARNING,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg',
         extra_environment=None,
     )

+ 35 - 8
tests/unit/borg/test_list.py

@@ -254,6 +254,9 @@ def test_make_find_paths_adds_globs_to_path_fragments():
 
 
 def test_list_archive_calls_borg_with_parameters():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
+    flexmock(module.logger).answer = lambda message: None
     list_arguments = argparse.Namespace(
         archive='archive',
         paths=None,
@@ -279,7 +282,7 @@ def test_list_archive_calls_borg_with_parameters():
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'list', 'repo::archive'),
-        output_log_level=logging.WARNING,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg',
         extra_environment=None,
     ).once()
@@ -293,6 +296,9 @@ def test_list_archive_calls_borg_with_parameters():
 
 
 def test_list_archive_with_archive_and_json_errors():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
+    flexmock(module.logger).answer = lambda message: None
     list_arguments = argparse.Namespace(archive='archive', paths=None, json=True, find_paths=None)
 
     flexmock(module.feature).should_receive('available').and_return(False)
@@ -307,6 +313,9 @@ def test_list_archive_with_archive_and_json_errors():
 
 
 def test_list_archive_calls_borg_with_local_path():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
+    flexmock(module.logger).answer = lambda message: None
     list_arguments = argparse.Namespace(
         archive='archive',
         paths=None,
@@ -332,7 +341,7 @@ def test_list_archive_calls_borg_with_local_path():
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg2', 'list', 'repo::archive'),
-        output_log_level=logging.WARNING,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg2',
         extra_environment=None,
     ).once()
@@ -347,6 +356,9 @@ def test_list_archive_calls_borg_with_local_path():
 
 
 def test_list_archive_calls_borg_multiple_times_with_find_paths():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
+    flexmock(module.logger).answer = lambda message: None
     glob_paths = ('**/*foo.txt*/**',)
     list_arguments = argparse.Namespace(
         archive=None,
@@ -371,13 +383,13 @@ def test_list_archive_calls_borg_multiple_times_with_find_paths():
     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,
+        output_log_level=module.borgmatic.logger.ANSWER,
         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,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg',
         extra_environment=None,
     ).once()
@@ -391,6 +403,9 @@ def test_list_archive_calls_borg_multiple_times_with_find_paths():
 
 
 def test_list_archive_calls_borg_with_archive():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
+    flexmock(module.logger).answer = lambda message: None
     list_arguments = argparse.Namespace(
         archive='archive',
         paths=None,
@@ -416,7 +431,7 @@ def test_list_archive_calls_borg_with_archive():
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'list', 'repo::archive'),
-        output_log_level=logging.WARNING,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg',
         extra_environment=None,
     ).once()
@@ -430,6 +445,9 @@ def test_list_archive_calls_borg_with_archive():
 
 
 def test_list_archive_without_archive_delegates_to_list_repository():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
+    flexmock(module.logger).answer = lambda message: None
     list_arguments = argparse.Namespace(
         archive=None,
         short=None,
@@ -457,6 +475,9 @@ def test_list_archive_without_archive_delegates_to_list_repository():
 
 
 def test_list_archive_with_borg_features_without_archive_delegates_to_list_repository():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
+    flexmock(module.logger).answer = lambda message: None
     list_arguments = argparse.Namespace(
         archive=None,
         short=None,
@@ -487,6 +508,9 @@ def test_list_archive_with_borg_features_without_archive_delegates_to_list_repos
     'archive_filter_flag', ('prefix', 'match_archives', 'sort_by', 'first', 'last',),
 )
 def test_list_archive_with_archive_ignores_archive_filter_flag(archive_filter_flag,):
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
+    flexmock(module.logger).answer = lambda message: None
     default_filter_flags = {
         'prefix': None,
         'match_archives': None,
@@ -513,7 +537,7 @@ def test_list_archive_with_archive_ignores_archive_filter_flag(archive_filter_fl
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'list', 'repo::archive'),
-        output_log_level=logging.WARNING,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg',
         extra_environment=None,
     ).once()
@@ -534,6 +558,9 @@ def test_list_archive_with_archive_ignores_archive_filter_flag(archive_filter_fl
 def test_list_archive_with_find_paths_allows_archive_filter_flag_but_only_passes_it_to_rlist(
     archive_filter_flag,
 ):
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
+    flexmock(module.logger).answer = lambda message: None
     default_filter_flags = {
         'prefix': None,
         'match_archives': None,
@@ -600,13 +627,13 @@ def test_list_archive_with_find_paths_allows_archive_filter_flag_but_only_passes
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'list', '--repo', 'repo', 'archive1') + glob_paths,
-        output_log_level=logging.WARNING,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg',
         extra_environment=None,
     ).once()
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'list', '--repo', 'repo', 'archive2') + glob_paths,
-        output_log_level=logging.WARNING,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg',
         extra_environment=None,
     ).once()

+ 26 - 36
tests/unit/borg/test_prune.py

@@ -78,6 +78,8 @@ PRUNE_COMMAND = ('borg', 'prune', '--keep-daily', '1', '--keep-weekly', '2', '--
 
 
 def test_prune_archives_calls_borg_with_parameters():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('make_prune_flags').and_return(BASE_PRUNE_FLAGS)
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     insert_execute_command_mock(PRUNE_COMMAND + ('repo',), logging.INFO)
@@ -92,6 +94,8 @@ def test_prune_archives_calls_borg_with_parameters():
 
 
 def test_prune_archives_with_log_info_calls_borg_with_info_parameter():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('make_prune_flags').and_return(BASE_PRUNE_FLAGS)
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     insert_execute_command_mock(PRUNE_COMMAND + ('--info', 'repo'), logging.INFO)
@@ -107,6 +111,8 @@ def test_prune_archives_with_log_info_calls_borg_with_info_parameter():
 
 
 def test_prune_archives_with_log_debug_calls_borg_with_debug_parameter():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('make_prune_flags').and_return(BASE_PRUNE_FLAGS)
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     insert_execute_command_mock(PRUNE_COMMAND + ('--debug', '--show-rc', 'repo'), logging.INFO)
@@ -122,6 +128,8 @@ def test_prune_archives_with_log_debug_calls_borg_with_debug_parameter():
 
 
 def test_prune_archives_with_dry_run_calls_borg_with_dry_run_parameter():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('make_prune_flags').and_return(BASE_PRUNE_FLAGS)
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     insert_execute_command_mock(PRUNE_COMMAND + ('--dry-run', 'repo'), logging.INFO)
@@ -136,6 +144,8 @@ def test_prune_archives_with_dry_run_calls_borg_with_dry_run_parameter():
 
 
 def test_prune_archives_with_local_path_calls_borg_via_local_path():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('make_prune_flags').and_return(BASE_PRUNE_FLAGS)
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     insert_execute_command_mock(('borg1',) + PRUNE_COMMAND[1:] + ('repo',), logging.INFO)
@@ -151,6 +161,8 @@ def test_prune_archives_with_local_path_calls_borg_via_local_path():
 
 
 def test_prune_archives_with_remote_path_calls_borg_with_remote_path_parameters():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('make_prune_flags').and_return(BASE_PRUNE_FLAGS)
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     insert_execute_command_mock(PRUNE_COMMAND + ('--remote-path', 'borg1', 'repo'), logging.INFO)
@@ -165,10 +177,12 @@ def test_prune_archives_with_remote_path_calls_borg_with_remote_path_parameters(
     )
 
 
-def test_prune_archives_with_stats_calls_borg_with_stats_parameter_and_warning_output_log_level():
+def test_prune_archives_with_stats_calls_borg_with_stats_parameter_and_answer_output_log_level():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('make_prune_flags').and_return(BASE_PRUNE_FLAGS)
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
-    insert_execute_command_mock(PRUNE_COMMAND + ('--stats', 'repo'), logging.WARNING)
+    insert_execute_command_mock(PRUNE_COMMAND + ('--stats', 'repo'), module.borgmatic.logger.ANSWER)
 
     module.prune_archives(
         dry_run=False,
@@ -180,42 +194,12 @@ def test_prune_archives_with_stats_calls_borg_with_stats_parameter_and_warning_o
     )
 
 
-def test_prune_archives_with_stats_and_log_info_calls_borg_with_stats_parameter_and_info_output_log_level():
+def test_prune_archives_with_files_calls_borg_with_list_parameter_and_answer_output_log_level():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('make_prune_flags').and_return(BASE_PRUNE_FLAGS)
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
-    insert_logging_mock(logging.INFO)
-    insert_execute_command_mock(PRUNE_COMMAND + ('--stats', '--info', 'repo'), logging.INFO)
-
-    module.prune_archives(
-        dry_run=False,
-        repository='repo',
-        storage_config={},
-        retention_config=flexmock(),
-        local_borg_version='1.2.3',
-        stats=True,
-    )
-
-
-def test_prune_archives_with_files_calls_borg_with_list_parameter_and_warning_output_log_level():
-    flexmock(module).should_receive('make_prune_flags').and_return(BASE_PRUNE_FLAGS)
-    flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
-    insert_execute_command_mock(PRUNE_COMMAND + ('--list', 'repo'), logging.WARNING)
-
-    module.prune_archives(
-        dry_run=False,
-        repository='repo',
-        storage_config={},
-        retention_config=flexmock(),
-        local_borg_version='1.2.3',
-        list_archives=True,
-    )
-
-
-def test_prune_archives_with_files_and_log_info_calls_borg_with_list_parameter_and_info_output_log_level():
-    flexmock(module).should_receive('make_prune_flags').and_return(BASE_PRUNE_FLAGS)
-    flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
-    insert_logging_mock(logging.INFO)
-    insert_execute_command_mock(PRUNE_COMMAND + ('--info', '--list', 'repo'), logging.INFO)
+    insert_execute_command_mock(PRUNE_COMMAND + ('--list', 'repo'), module.borgmatic.logger.ANSWER)
 
     module.prune_archives(
         dry_run=False,
@@ -228,6 +212,8 @@ def test_prune_archives_with_files_and_log_info_calls_borg_with_list_parameter_a
 
 
 def test_prune_archives_with_umask_calls_borg_with_umask_parameters():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     storage_config = {'umask': '077'}
     flexmock(module).should_receive('make_prune_flags').and_return(BASE_PRUNE_FLAGS)
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
@@ -243,6 +229,8 @@ def test_prune_archives_with_umask_calls_borg_with_umask_parameters():
 
 
 def test_prune_archives_with_lock_wait_calls_borg_with_lock_wait_parameters():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     storage_config = {'lock_wait': 5}
     flexmock(module).should_receive('make_prune_flags').and_return(BASE_PRUNE_FLAGS)
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
@@ -258,6 +246,8 @@ def test_prune_archives_with_lock_wait_calls_borg_with_lock_wait_parameters():
 
 
 def test_prune_archives_with_extra_borg_options_calls_borg_with_extra_options():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('make_prune_flags').and_return(BASE_PRUNE_FLAGS)
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     insert_execute_command_mock(PRUNE_COMMAND + ('--extra', '--options', 'repo'), logging.INFO)

+ 27 - 7
tests/unit/borg/test_rinfo.py

@@ -8,12 +8,14 @@ from ..test_verbosity import insert_logging_mock
 
 
 def test_display_repository_info_calls_borg_with_parameters():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'rinfo', '--repo', 'repo'),
-        output_log_level=logging.WARNING,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg',
         extra_environment=None,
     )
@@ -27,12 +29,14 @@ def test_display_repository_info_calls_borg_with_parameters():
 
 
 def test_display_repository_info_without_borg_features_calls_borg_with_info_sub_command():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.feature).should_receive('available').and_return(False)
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'info', 'repo'),
-        output_log_level=logging.WARNING,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg',
         extra_environment=None,
     )
@@ -46,12 +50,14 @@ def test_display_repository_info_without_borg_features_calls_borg_with_info_sub_
 
 
 def test_display_repository_info_with_log_info_calls_borg_with_info_parameter():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'rinfo', '--info', '--repo', 'repo'),
-        output_log_level=logging.WARNING,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg',
         extra_environment=None,
     )
@@ -65,6 +71,8 @@ def test_display_repository_info_with_log_info_calls_borg_with_info_parameter():
 
 
 def test_display_repository_info_with_log_info_and_json_suppresses_most_borg_output():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',))
     flexmock(module.environment).should_receive('make_environment')
@@ -84,12 +92,14 @@ def test_display_repository_info_with_log_info_and_json_suppresses_most_borg_out
 
 
 def test_display_repository_info_with_log_debug_calls_borg_with_debug_parameter():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'rinfo', '--debug', '--show-rc', '--repo', 'repo'),
-        output_log_level=logging.WARNING,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg',
         extra_environment=None,
     )
@@ -104,6 +114,8 @@ def test_display_repository_info_with_log_debug_calls_borg_with_debug_parameter(
 
 
 def test_display_repository_info_with_log_debug_and_json_suppresses_most_borg_output():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',))
     flexmock(module.environment).should_receive('make_environment')
@@ -123,6 +135,8 @@ def test_display_repository_info_with_log_debug_and_json_suppresses_most_borg_ou
 
 
 def test_display_repository_info_with_json_calls_borg_with_json_parameter():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',))
     flexmock(module.environment).should_receive('make_environment')
@@ -141,12 +155,14 @@ def test_display_repository_info_with_json_calls_borg_with_json_parameter():
 
 
 def test_display_repository_info_with_local_path_calls_borg_via_local_path():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg1', 'rinfo', '--repo', 'repo'),
-        output_log_level=logging.WARNING,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg1',
         extra_environment=None,
     )
@@ -161,12 +177,14 @@ def test_display_repository_info_with_local_path_calls_borg_via_local_path():
 
 
 def test_display_repository_info_with_remote_path_calls_borg_with_remote_path_parameters():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'rinfo', '--remote-path', 'borg1', '--repo', 'repo'),
-        output_log_level=logging.WARNING,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg',
         extra_environment=None,
     )
@@ -181,13 +199,15 @@ def test_display_repository_info_with_remote_path_calls_borg_with_remote_path_pa
 
 
 def test_display_repository_info_with_lock_wait_calls_borg_with_lock_wait_parameters():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     storage_config = {'lock_wait': 5}
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'rinfo', '--lock-wait', '5', '--repo', 'repo'),
-        output_log_level=logging.WARNING,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg',
         extra_environment=None,
     )

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

@@ -325,6 +325,8 @@ def test_make_rlist_command_includes_additional_flags(argument_name):
 
 
 def test_list_repository_calls_borg_with_parameters():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     rlist_arguments = argparse.Namespace(json=False)
 
     flexmock(module.feature).should_receive('available').and_return(False)
@@ -339,7 +341,7 @@ def test_list_repository_calls_borg_with_parameters():
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'rlist', 'repo'),
-        output_log_level=logging.WARNING,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg',
         extra_environment=None,
     ).once()
@@ -353,6 +355,8 @@ def test_list_repository_calls_borg_with_parameters():
 
 
 def test_list_repository_with_json_returns_borg_output():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     rlist_arguments = argparse.Namespace(json=True)
     json_output = flexmock()
 

+ 32 - 11
tests/unit/borg/test_transfer.py

@@ -9,13 +9,15 @@ from ..test_verbosity import insert_logging_mock
 
 
 def test_transfer_archives_calls_borg_with_flags():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'transfer', '--repo', 'repo'),
-        output_log_level=logging.WARNING,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg',
         extra_environment=None,
     )
@@ -30,6 +32,8 @@ def test_transfer_archives_calls_borg_with_flags():
 
 
 def test_transfer_archives_with_dry_run_calls_borg_with_dry_run_flag():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').with_args('dry-run', True).and_return(
         ('--dry-run',)
@@ -39,7 +43,7 @@ def test_transfer_archives_with_dry_run_calls_borg_with_dry_run_flag():
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'transfer', '--repo', 'repo', '--dry-run'),
-        output_log_level=logging.WARNING,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg',
         extra_environment=None,
     )
@@ -54,13 +58,15 @@ def test_transfer_archives_with_dry_run_calls_borg_with_dry_run_flag():
 
 
 def test_transfer_archives_with_log_info_calls_borg_with_info_flag():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'transfer', '--info', '--repo', 'repo'),
-        output_log_level=logging.WARNING,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg',
         extra_environment=None,
     )
@@ -75,13 +81,15 @@ def test_transfer_archives_with_log_info_calls_borg_with_info_flag():
 
 
 def test_transfer_archives_with_log_debug_calls_borg_with_debug_flag():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'transfer', '--debug', '--show-rc', '--repo', 'repo'),
-        output_log_level=logging.WARNING,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg',
         extra_environment=None,
     )
@@ -97,6 +105,8 @@ def test_transfer_archives_with_log_debug_calls_borg_with_debug_flag():
 
 
 def test_transfer_archives_with_archive_calls_borg_with_match_archives_flag():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').with_args(
         'match-archives', 'archive'
@@ -106,7 +116,7 @@ def test_transfer_archives_with_archive_calls_borg_with_match_archives_flag():
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'transfer', '--match-archives', 'archive', '--repo', 'repo'),
-        output_log_level=logging.WARNING,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg',
         extra_environment=None,
     )
@@ -121,6 +131,8 @@ def test_transfer_archives_with_archive_calls_borg_with_match_archives_flag():
 
 
 def test_transfer_archives_with_match_archives_calls_borg_with_match_archives_flag():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').with_args(
         'match-archives', 'sh:foo*'
@@ -130,7 +142,7 @@ def test_transfer_archives_with_match_archives_calls_borg_with_match_archives_fl
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'transfer', '--match-archives', 'sh:foo*', '--repo', 'repo'),
-        output_log_level=logging.WARNING,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg',
         extra_environment=None,
     )
@@ -145,13 +157,15 @@ def test_transfer_archives_with_match_archives_calls_borg_with_match_archives_fl
 
 
 def test_transfer_archives_with_local_path_calls_borg_via_local_path():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg2', 'transfer', '--repo', 'repo'),
-        output_log_level=logging.WARNING,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg2',
         extra_environment=None,
     )
@@ -167,6 +181,8 @@ def test_transfer_archives_with_local_path_calls_borg_via_local_path():
 
 
 def test_transfer_archives_with_remote_path_calls_borg_with_remote_path_flags():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').with_args(
         'remote-path', 'borg2'
@@ -176,7 +192,7 @@ def test_transfer_archives_with_remote_path_calls_borg_with_remote_path_flags():
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'transfer', '--remote-path', 'borg2', '--repo', 'repo'),
-        output_log_level=logging.WARNING,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg',
         extra_environment=None,
     )
@@ -192,6 +208,8 @@ def test_transfer_archives_with_remote_path_calls_borg_with_remote_path_flags():
 
 
 def test_transfer_archives_with_lock_wait_calls_borg_with_lock_wait_flags():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').with_args('lock-wait', 5).and_return(
         ('--lock-wait', '5')
@@ -202,7 +220,7 @@ def test_transfer_archives_with_lock_wait_calls_borg_with_lock_wait_flags():
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'transfer', '--lock-wait', '5', '--repo', 'repo'),
-        output_log_level=logging.WARNING,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg',
         extra_environment=None,
     )
@@ -218,6 +236,8 @@ def test_transfer_archives_with_lock_wait_calls_borg_with_lock_wait_flags():
 
 @pytest.mark.parametrize('argument_name', ('upgrader', 'sort_by', 'first', 'last'))
 def test_transfer_archives_passes_through_arguments_to_borg(argument_name):
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flag_name = f"--{argument_name.replace('_', ' ')}"
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(
@@ -227,7 +247,7 @@ def test_transfer_archives_passes_through_arguments_to_borg(argument_name):
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'transfer', flag_name, 'value', '--repo', 'repo'),
-        output_log_level=logging.WARNING,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg',
         extra_environment=None,
     )
@@ -244,6 +264,7 @@ def test_transfer_archives_passes_through_arguments_to_borg(argument_name):
 
 
 def test_transfer_archives_with_source_repository_calls_borg_with_other_repo_flags():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').with_args('other-repo', 'other').and_return(
         ('--other-repo', 'other')
@@ -253,7 +274,7 @@ def test_transfer_archives_with_source_repository_calls_borg_with_other_repo_fla
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'transfer', '--repo', 'repo', '--other-repo', 'other'),
-        output_log_level=logging.WARNING,
+        output_log_level=module.borgmatic.logger.ANSWER,
         borg_local_path='borg',
         extra_environment=None,
     )

+ 48 - 1
tests/unit/commands/test_borgmatic.py

@@ -9,6 +9,7 @@ from borgmatic.commands import borgmatic as module
 
 
 def test_run_configuration_runs_actions_for_each_repository():
+    flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     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(
@@ -23,6 +24,7 @@ def test_run_configuration_runs_actions_for_each_repository():
 
 
 def test_run_configuration_with_invalid_borg_version_errors():
+    flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     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()
@@ -34,6 +36,7 @@ def test_run_configuration_with_invalid_borg_version_errors():
 
 
 def test_run_configuration_logs_monitor_start_error():
+    flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     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
@@ -50,6 +53,7 @@ def test_run_configuration_logs_monitor_start_error():
 
 
 def test_run_configuration_bails_for_monitor_start_soft_failure():
+    flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     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)
@@ -64,6 +68,7 @@ def test_run_configuration_bails_for_monitor_start_soft_failure():
 
 
 def test_run_configuration_logs_actions_error():
+    flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     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')
@@ -79,6 +84,7 @@ def test_run_configuration_logs_actions_error():
 
 
 def test_run_configuration_bails_for_actions_soft_failure():
+    flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     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')
@@ -94,6 +100,7 @@ def test_run_configuration_bails_for_actions_soft_failure():
 
 
 def test_run_configuration_logs_monitor_finish_error():
+    flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     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
@@ -110,6 +117,7 @@ def test_run_configuration_logs_monitor_finish_error():
 
 
 def test_run_configuration_bails_for_monitor_finish_soft_failure():
+    flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     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(
@@ -127,6 +135,7 @@ def test_run_configuration_bails_for_monitor_finish_soft_failure():
 
 
 def test_run_configuration_logs_on_error_hook_error():
+    flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     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()]
@@ -143,6 +152,7 @@ def test_run_configuration_logs_on_error_hook_error():
 
 
 def test_run_configuration_bails_for_on_error_hook_soft_failure():
+    flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     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)
@@ -159,6 +169,7 @@ 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).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     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([])
@@ -171,6 +182,7 @@ def test_run_configuration_retries_soft_error():
 
 def test_run_configuration_retries_hard_error():
     # Run action fails twice
+    flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     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)
@@ -190,7 +202,8 @@ def test_run_configuration_retries_hard_error():
     assert results == error_logs
 
 
-def test_run_repos_ordered():
+def test_run_configuration_repos_ordered():
+    flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     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)
@@ -208,6 +221,7 @@ def test_run_repos_ordered():
 
 
 def test_run_configuration_retries_round_robbin():
+    flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     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)
@@ -238,6 +252,7 @@ def test_run_configuration_retries_round_robbin():
 
 
 def test_run_configuration_retries_one_passes():
+    flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     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(
@@ -266,6 +281,7 @@ def test_run_configuration_retries_one_passes():
 
 
 def test_run_configuration_retry_wait():
+    flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     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)
@@ -304,6 +320,7 @@ def test_run_configuration_retry_wait():
 
 
 def test_run_configuration_retries_timeout_multiple_repos():
+    flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     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(
@@ -341,6 +358,8 @@ def test_run_configuration_retries_timeout_multiple_repos():
 
 
 def test_run_actions_does_not_raise_for_rcreate_action():
+    flexmock(module).should_receive('add_custom_log_levels')
+    flexmock(module.logger).answer = lambda message: None
     flexmock(module.borg_rcreate).should_receive('create_repository')
     arguments = {
         'global': flexmock(monitoring_verbosity=1, dry_run=False),
@@ -372,6 +391,8 @@ def test_run_actions_does_not_raise_for_rcreate_action():
 
 
 def test_run_actions_does_not_raise_for_transfer_action():
+    flexmock(module).should_receive('add_custom_log_levels')
+    flexmock(module.logger).answer = lambda message: None
     flexmock(module.borg_transfer).should_receive('transfer_archives')
     arguments = {
         'global': flexmock(monitoring_verbosity=1, dry_run=False),
@@ -396,6 +417,8 @@ def test_run_actions_does_not_raise_for_transfer_action():
 
 
 def test_run_actions_calls_hooks_for_prune_action():
+    flexmock(module).should_receive('add_custom_log_levels')
+    flexmock(module.logger).answer = lambda message: None
     flexmock(module.borg_prune).should_receive('prune_archives')
     flexmock(module.command).should_receive('execute_hook').times(
         4
@@ -423,6 +446,8 @@ def test_run_actions_calls_hooks_for_prune_action():
 
 
 def test_run_actions_calls_hooks_for_compact_action():
+    flexmock(module).should_receive('add_custom_log_levels')
+    flexmock(module.logger).answer = lambda message: None
     flexmock(module.borg_feature).should_receive('available').and_return(True)
     flexmock(module.borg_compact).should_receive('compact_segments')
     flexmock(module.command).should_receive('execute_hook').times(
@@ -451,6 +476,8 @@ def test_run_actions_calls_hooks_for_compact_action():
 
 
 def test_run_actions_executes_and_calls_hooks_for_create_action():
+    flexmock(module).should_receive('add_custom_log_levels')
+    flexmock(module.logger).answer = lambda message: None
     flexmock(module.borg_create).should_receive('create_archive')
     flexmock(module.command).should_receive('execute_hook').times(
         4
@@ -482,6 +509,8 @@ def test_run_actions_executes_and_calls_hooks_for_create_action():
 
 
 def test_run_actions_calls_hooks_for_check_action():
+    flexmock(module).should_receive('add_custom_log_levels')
+    flexmock(module.logger).answer = lambda message: None
     flexmock(module.checks).should_receive('repository_enabled_for_checks').and_return(True)
     flexmock(module.borg_check).should_receive('check_archives')
     flexmock(module.command).should_receive('execute_hook').times(
@@ -512,6 +541,8 @@ def test_run_actions_calls_hooks_for_check_action():
 
 
 def test_run_actions_calls_hooks_for_extract_action():
+    flexmock(module).should_receive('add_custom_log_levels')
+    flexmock(module.logger).answer = lambda message: None
     flexmock(module.validate).should_receive('repositories_match').and_return(True)
     flexmock(module.borg_extract).should_receive('extract_archive')
     flexmock(module.command).should_receive('execute_hook').times(
@@ -547,6 +578,8 @@ def test_run_actions_calls_hooks_for_extract_action():
 
 
 def test_run_actions_does_not_raise_for_export_tar_action():
+    flexmock(module).should_receive('add_custom_log_levels')
+    flexmock(module.logger).answer = lambda message: None
     flexmock(module.validate).should_receive('repositories_match').and_return(True)
     flexmock(module.borg_export_tar).should_receive('export_tar_archive')
     arguments = {
@@ -580,6 +613,8 @@ def test_run_actions_does_not_raise_for_export_tar_action():
 
 
 def test_run_actions_does_not_raise_for_mount_action():
+    flexmock(module).should_receive('add_custom_log_levels')
+    flexmock(module.logger).answer = lambda message: None
     flexmock(module.validate).should_receive('repositories_match').and_return(True)
     flexmock(module.borg_mount).should_receive('mount_archive')
     arguments = {
@@ -612,6 +647,8 @@ def test_run_actions_does_not_raise_for_mount_action():
 
 
 def test_run_actions_does_not_raise_for_rlist_action():
+    flexmock(module).should_receive('add_custom_log_levels')
+    flexmock(module.logger).answer = lambda message: None
     flexmock(module.validate).should_receive('repositories_match').and_return(True)
     flexmock(module.borg_rlist).should_receive('list_repository')
     arguments = {
@@ -637,6 +674,8 @@ def test_run_actions_does_not_raise_for_rlist_action():
 
 
 def test_run_actions_does_not_raise_for_list_action():
+    flexmock(module).should_receive('add_custom_log_levels')
+    flexmock(module.logger).answer = lambda message: None
     flexmock(module.validate).should_receive('repositories_match').and_return(True)
     flexmock(module.borg_rlist).should_receive('resolve_archive_name').and_return(flexmock())
     flexmock(module.borg_list).should_receive('list_archive')
@@ -663,6 +702,8 @@ def test_run_actions_does_not_raise_for_list_action():
 
 
 def test_run_actions_does_not_raise_for_rinfo_action():
+    flexmock(module).should_receive('add_custom_log_levels')
+    flexmock(module.logger).answer = lambda message: None
     flexmock(module.validate).should_receive('repositories_match').and_return(True)
     flexmock(module.borg_rinfo).should_receive('display_repository_info')
     arguments = {
@@ -688,6 +729,8 @@ def test_run_actions_does_not_raise_for_rinfo_action():
 
 
 def test_run_actions_does_not_raise_for_info_action():
+    flexmock(module).should_receive('add_custom_log_levels')
+    flexmock(module.logger).answer = lambda message: None
     flexmock(module.validate).should_receive('repositories_match').and_return(True)
     flexmock(module.borg_rlist).should_receive('resolve_archive_name').and_return(flexmock())
     flexmock(module.borg_info).should_receive('display_archives_info')
@@ -714,6 +757,8 @@ def test_run_actions_does_not_raise_for_info_action():
 
 
 def test_run_actions_does_not_raise_for_break_lock_action():
+    flexmock(module).should_receive('add_custom_log_levels')
+    flexmock(module.logger).answer = lambda message: None
     flexmock(module.validate).should_receive('repositories_match').and_return(True)
     flexmock(module.borg_break_lock).should_receive('break_lock')
     arguments = {
@@ -739,6 +784,8 @@ def test_run_actions_does_not_raise_for_break_lock_action():
 
 
 def test_run_actions_does_not_raise_for_borg_action():
+    flexmock(module).should_receive('add_custom_log_levels')
+    flexmock(module.logger).answer = lambda message: None
     flexmock(module.validate).should_receive('repositories_match').and_return(True)
     flexmock(module.borg_rlist).should_receive('resolve_archive_name').and_return(flexmock())
     flexmock(module.borg_borg).should_receive('run_arbitrary_borg')

+ 48 - 0
tests/unit/test_logger.py

@@ -1,4 +1,5 @@
 import logging
+import sys
 
 import pytest
 from flexmock import flexmock
@@ -125,6 +126,8 @@ def test_multi_stream_handler_logs_to_handler_for_log_level():
 
 
 def test_console_color_formatter_format_includes_log_message():
+    flexmock(module).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.ANSWER
     plain_message = 'uh oh'
     record = flexmock(levelno=logging.CRITICAL, msg=plain_message)
 
@@ -142,7 +145,38 @@ def test_color_text_without_color_does_not_raise():
     module.color_text(None, 'hi')
 
 
+def test_add_logging_level_adds_level_name_and_sets_global_attributes_and_methods():
+    logger = flexmock()
+    flexmock(module.logging).should_receive('getLoggerClass').and_return(logger)
+    flexmock(module.logging).should_receive('addLevelName').with_args(99, 'PLAID')
+    builtins = flexmock(sys.modules['builtins'])
+    builtins.should_call('setattr')
+    builtins.should_receive('setattr').with_args(module.logging, 'PLAID', 99).once()
+    builtins.should_receive('setattr').with_args(logger, 'plaid', object).once()
+    builtins.should_receive('setattr').with_args(logging, 'plaid', object).once()
+
+    module.add_logging_level('PLAID', 99)
+
+
+def test_add_logging_level_skips_global_setting_if_already_set():
+    logger = flexmock()
+    flexmock(module.logging).should_receive('getLoggerClass').and_return(logger)
+    flexmock(module.logging).PLAID = 99
+    flexmock(logger).plaid = flexmock()
+    flexmock(logging).plaid = flexmock()
+    flexmock(module.logging).should_receive('addLevelName').never()
+    builtins = flexmock(sys.modules['builtins'])
+    builtins.should_call('setattr')
+    builtins.should_receive('setattr').with_args(module.logging, 'PLAID', 99).never()
+    builtins.should_receive('setattr').with_args(logger, 'plaid', object).never()
+    builtins.should_receive('setattr').with_args(logging, 'plaid', object).never()
+
+    module.add_logging_level('PLAID', 99)
+
+
 def test_configure_logging_probes_for_log_socket_on_linux():
+    flexmock(module).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.ANSWER
     flexmock(module).should_receive('Multi_stream_handler').and_return(
         flexmock(setFormatter=lambda formatter: None, setLevel=lambda level: None)
     )
@@ -161,6 +195,8 @@ def test_configure_logging_probes_for_log_socket_on_linux():
 
 
 def test_configure_logging_probes_for_log_socket_on_macos():
+    flexmock(module).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.ANSWER
     flexmock(module).should_receive('Multi_stream_handler').and_return(
         flexmock(setFormatter=lambda formatter: None, setLevel=lambda level: None)
     )
@@ -180,6 +216,8 @@ def test_configure_logging_probes_for_log_socket_on_macos():
 
 
 def test_configure_logging_probes_for_log_socket_on_freebsd():
+    flexmock(module).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.ANSWER
     flexmock(module).should_receive('Multi_stream_handler').and_return(
         flexmock(setFormatter=lambda formatter: None, setLevel=lambda level: None)
     )
@@ -200,6 +238,8 @@ def test_configure_logging_probes_for_log_socket_on_freebsd():
 
 
 def test_configure_logging_sets_global_logger_to_most_verbose_log_level():
+    flexmock(module).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.ANSWER
     flexmock(module).should_receive('Multi_stream_handler').and_return(
         flexmock(setFormatter=lambda formatter: None, setLevel=lambda level: None)
     )
@@ -213,6 +253,8 @@ def test_configure_logging_sets_global_logger_to_most_verbose_log_level():
 
 
 def test_configure_logging_skips_syslog_if_not_found():
+    flexmock(module).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.ANSWER
     flexmock(module).should_receive('Multi_stream_handler').and_return(
         flexmock(setFormatter=lambda formatter: None, setLevel=lambda level: None)
     )
@@ -227,6 +269,8 @@ def test_configure_logging_skips_syslog_if_not_found():
 
 
 def test_configure_logging_skips_syslog_if_interactive_console():
+    flexmock(module).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.ANSWER
     flexmock(module).should_receive('Multi_stream_handler').and_return(
         flexmock(setFormatter=lambda formatter: None, setLevel=lambda level: None)
     )
@@ -242,6 +286,8 @@ def test_configure_logging_skips_syslog_if_interactive_console():
 
 
 def test_configure_logging_to_logfile_instead_of_syslog():
+    flexmock(module).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.ANSWER
     flexmock(module).should_receive('Multi_stream_handler').and_return(
         flexmock(setFormatter=lambda formatter: None, setLevel=lambda level: None)
     )
@@ -264,6 +310,8 @@ def test_configure_logging_to_logfile_instead_of_syslog():
 
 
 def test_configure_logging_skips_logfile_if_argument_is_none():
+    flexmock(module).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.ANSWER
     flexmock(module).should_receive('Multi_stream_handler').and_return(
         flexmock(setFormatter=lambda formatter: None, setLevel=lambda level: None)
     )

+ 8 - 1
tests/unit/test_verbosity.py

@@ -15,10 +15,17 @@ def insert_logging_mock(log_level):
 
 
 def test_verbosity_to_log_level_maps_known_verbosity_to_log_level():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
+
+    assert module.verbosity_to_log_level(module.VERBOSITY_ERROR) == logging.ERROR
+    assert module.verbosity_to_log_level(module.VERBOSITY_ANSWER) == module.borgmatic.logger.ANSWER
     assert module.verbosity_to_log_level(module.VERBOSITY_SOME) == logging.INFO
     assert module.verbosity_to_log_level(module.VERBOSITY_LOTS) == logging.DEBUG
-    assert module.verbosity_to_log_level(module.VERBOSITY_ERROR) == logging.ERROR
 
 
 def test_verbosity_to_log_level_maps_unknown_verbosity_to_warning_level():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
+
     assert module.verbosity_to_log_level('my pants') == logging.WARNING