From abf2b3a8c7ef8f7be03943a22090b8992f539f54 Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Sun, 21 Jan 2024 11:34:40 -0800 Subject: [PATCH] Elevate specific Borg warnings to errors or squash errors to warnings (#798). --- NEWS | 1 + borgmatic/borg/borg.py | 3 +- borgmatic/borg/break_lock.py | 7 +- borgmatic/borg/check.py | 14 +- borgmatic/borg/compact.py | 3 +- borgmatic/borg/create.py | 12 +- borgmatic/borg/environment.py | 4 + borgmatic/borg/export_key.py | 3 +- borgmatic/borg/export_tar.py | 3 +- borgmatic/borg/extract.py | 17 ++- borgmatic/borg/info.py | 5 +- borgmatic/borg/list.py | 6 +- borgmatic/borg/mount.py | 10 +- borgmatic/borg/prune.py | 3 +- borgmatic/borg/rcreate.py | 3 +- borgmatic/borg/rinfo.py | 5 +- borgmatic/borg/rlist.py | 10 +- borgmatic/borg/transfer.py | 1 + borgmatic/borg/umount.py | 6 +- borgmatic/borg/version.py | 1 + borgmatic/commands/borgmatic.py | 1 + borgmatic/config/schema.yaml | 31 ++++ borgmatic/execute.py | 107 +++++++++++--- docs/how-to/customize-warnings-and-errors.md | 83 +++++++++++ docs/how-to/develop-on-borgmatic.md | 2 +- docs/how-to/make-per-application-backups.md | 4 +- docs/how-to/upgrade.md | 2 +- tests/integration/test_execute.py | 148 +++++++++++++++---- tests/unit/borg/test_borg.py | 36 +++++ tests/unit/borg/test_break_lock.py | 31 +++- tests/unit/borg/test_check.py | 43 +++++- tests/unit/borg/test_compact.py | 19 ++- tests/unit/borg/test_create.py | 105 +++++++++++++ tests/unit/borg/test_environment.py | 3 +- tests/unit/borg/test_export_key.py | 35 ++++- tests/unit/borg/test_export_tar.py | 45 +++++- tests/unit/borg/test_extract.py | 77 +++++++++- tests/unit/borg/test_list.py | 59 +++++++- tests/unit/borg/test_mount.py | 48 +++++- tests/unit/borg/test_prune.py | 25 +++- tests/unit/borg/test_rcreate.py | 27 +++- tests/unit/borg/test_rinfo.py | 55 +++++++ tests/unit/borg/test_rlist.py | 31 ++++ tests/unit/borg/test_transfer.py | 45 ++++++ tests/unit/borg/test_umount.py | 25 +++- tests/unit/borg/test_version.py | 13 +- tests/unit/test_execute.py | 71 +++++---- 47 files changed, 1156 insertions(+), 132 deletions(-) create mode 100644 docs/how-to/customize-warnings-and-errors.md diff --git a/NEWS b/NEWS index bb5dc78c..e3cf6feb 100644 --- a/NEWS +++ b/NEWS @@ -1,6 +1,7 @@ 1.8.7.dev0 * #736: Store included configuration files within each backup archive in support of the "config bootstrap" action. Previously, only top-level configuration files were stored. + * #798: Elevate specific Borg warnings to errors or squash errors to warnings. * #810: SECURITY: Prevent shell injection attacks within the PostgreSQL hook, the MongoDB hook, the SQLite hook, the "borgmatic borg" action, and command hook variable/constant interpolation. * #814: Fix a traceback when providing an invalid "--override" value for a list option. diff --git a/borgmatic/borg/borg.py b/borgmatic/borg/borg.py index 2af0188d..7fa47545 100644 --- a/borgmatic/borg/borg.py +++ b/borgmatic/borg/borg.py @@ -59,7 +59,6 @@ def run_arbitrary_borg( return execute_command( tuple(shlex.quote(part) for part in full_command), output_file=DO_NOT_CAPTURE, - borg_local_path=local_path, shell=True, extra_environment=dict( (environment.make_environment(config) or {}), @@ -68,4 +67,6 @@ def run_arbitrary_borg( 'ARCHIVE': archive if archive else '', }, ), + borg_local_path=local_path, + borg_exit_codes=config.get('borg_exit_codes'), ) diff --git a/borgmatic/borg/break_lock.py b/borgmatic/borg/break_lock.py index c0ee5dbc..9a779eab 100644 --- a/borgmatic/borg/break_lock.py +++ b/borgmatic/borg/break_lock.py @@ -34,4 +34,9 @@ def break_lock( ) borg_environment = environment.make_environment(config) - execute_command(full_command, borg_local_path=local_path, extra_environment=borg_environment) + execute_command( + full_command, + extra_environment=borg_environment, + borg_local_path=local_path, + borg_exit_codes=config.get('borg_exit_codes'), + ) diff --git a/borgmatic/borg/check.py b/borgmatic/borg/check.py index 220a0fc2..034ed611 100644 --- a/borgmatic/borg/check.py +++ b/borgmatic/borg/check.py @@ -434,15 +434,25 @@ def check_archives( ) borg_environment = environment.make_environment(config) + borg_exit_codes = config.get('borg_exit_codes') # The Borg repair option triggers an interactive prompt, which won't work when output is # captured. And progress messes with the terminal directly. if check_arguments.repair or check_arguments.progress: execute_command( - full_command, output_file=DO_NOT_CAPTURE, extra_environment=borg_environment + full_command, + output_file=DO_NOT_CAPTURE, + extra_environment=borg_environment, + borg_local_path=local_path, + borg_exit_codes=borg_exit_codes, ) else: - execute_command(full_command, extra_environment=borg_environment) + execute_command( + full_command, + extra_environment=borg_environment, + borg_local_path=local_path, + borg_exit_codes=borg_exit_codes, + ) for check in checks: write_check_time( diff --git a/borgmatic/borg/compact.py b/borgmatic/borg/compact.py index 20bbe129..dfaea3e6 100644 --- a/borgmatic/borg/compact.py +++ b/borgmatic/borg/compact.py @@ -48,6 +48,7 @@ def compact_segments( execute_command( full_command, output_log_level=logging.INFO, - borg_local_path=local_path, extra_environment=environment.make_environment(config), + borg_local_path=local_path, + borg_exit_codes=config.get('borg_exit_codes'), ) diff --git a/borgmatic/borg/create.py b/borgmatic/borg/create.py index 9d2a64ce..8a899e92 100644 --- a/borgmatic/borg/create.py +++ b/borgmatic/borg/create.py @@ -272,7 +272,7 @@ def any_parent_directories(path, candidate_parents): def collect_special_file_paths( - create_command, local_path, working_directory, borg_environment, skip_directories + create_command, config, local_path, working_directory, borg_environment, skip_directories ): ''' Given a Borg create command as a tuple, a local Borg path, a working directory, a dict of @@ -290,6 +290,7 @@ def collect_special_file_paths( working_directory=working_directory, extra_environment=borg_environment, borg_local_path=local_path, + borg_exit_codes=config.get('borg_exit_codes'), ) paths = tuple( @@ -469,6 +470,7 @@ def create_archive( logger.debug(f'{repository_path}: Collecting special file paths') special_file_paths = collect_special_file_paths( create_flags + create_positional_arguments, + config, local_path, working_directory, borg_environment, @@ -494,6 +496,7 @@ def create_archive( + (('--progress',) if progress else ()) + (('--json',) if json else ()) ) + borg_exit_codes = config.get('borg_exit_codes') if stream_processes: return execute_command_with_processes( @@ -501,9 +504,10 @@ def create_archive( stream_processes, output_log_level, output_file, - borg_local_path=local_path, working_directory=working_directory, extra_environment=borg_environment, + borg_local_path=local_path, + borg_exit_codes=borg_exit_codes, ) elif output_log_level is None: return execute_command_and_capture_output( @@ -511,13 +515,15 @@ def create_archive( working_directory=working_directory, extra_environment=borg_environment, borg_local_path=local_path, + borg_exit_codes=borg_exit_codes, ) else: execute_command( create_flags + create_positional_arguments, output_log_level, output_file, - borg_local_path=local_path, working_directory=working_directory, extra_environment=borg_environment, + borg_local_path=local_path, + borg_exit_codes=borg_exit_codes, ) diff --git a/borgmatic/borg/environment.py b/borgmatic/borg/environment.py index 6c7b6e7d..30be936c 100644 --- a/borgmatic/borg/environment.py +++ b/borgmatic/borg/environment.py @@ -50,4 +50,8 @@ def make_environment(config): if value is not None: environment[environment_variable_name] = 'YES' if value else 'NO' + # On Borg 1.4.0a1+, take advantage of more specific exit codes. No effect on + # older versions of Borg. + environment['BORG_EXIT_CODES'] = 'modern' + return environment diff --git a/borgmatic/borg/export_key.py b/borgmatic/borg/export_key.py index b249e0bb..96edadac 100644 --- a/borgmatic/borg/export_key.py +++ b/borgmatic/borg/export_key.py @@ -65,6 +65,7 @@ def export_key( full_command, output_file=output_file, output_log_level=logging.ANSWER, - borg_local_path=local_path, extra_environment=environment.make_environment(config), + borg_local_path=local_path, + borg_exit_codes=config.get('borg_exit_codes'), ) diff --git a/borgmatic/borg/export_tar.py b/borgmatic/borg/export_tar.py index 47e3c20d..2805e9c1 100644 --- a/borgmatic/borg/export_tar.py +++ b/borgmatic/borg/export_tar.py @@ -69,6 +69,7 @@ def export_tar_archive( full_command, output_file=DO_NOT_CAPTURE if destination_path == '-' else None, output_log_level=output_log_level, - borg_local_path=local_path, extra_environment=environment.make_environment(config), + borg_local_path=local_path, + borg_exit_codes=config.get('borg_exit_codes'), ) diff --git a/borgmatic/borg/extract.py b/borgmatic/borg/extract.py index dec203fc..966e2971 100644 --- a/borgmatic/borg/extract.py +++ b/borgmatic/borg/extract.py @@ -57,7 +57,11 @@ def extract_last_archive_dry_run( ) execute_command( - full_extract_command, working_directory=None, extra_environment=borg_environment + full_extract_command, + working_directory=None, + extra_environment=borg_environment, + borg_local_path=local_path, + borg_exit_codes=config.get('borg_exit_codes'), ) @@ -127,6 +131,7 @@ def extract_archive( ) borg_environment = environment.make_environment(config) + borg_exit_codes = config.get('borg_exit_codes') # The progress output isn't compatible with captured and logged output, as progress messes with # the terminal directly. @@ -136,6 +141,8 @@ def extract_archive( output_file=DO_NOT_CAPTURE, working_directory=destination_path, extra_environment=borg_environment, + borg_local_path=local_path, + borg_exit_codes=borg_exit_codes, ) return None @@ -146,10 +153,16 @@ def extract_archive( working_directory=destination_path, run_to_completion=False, extra_environment=borg_environment, + borg_local_path=local_path, + borg_exit_codes=borg_exit_codes, ) # Don't give Borg local path so as to error on warnings, as "borg extract" only gives a warning # if the restore paths don't exist in the archive. execute_command( - full_command, working_directory=destination_path, extra_environment=borg_environment + full_command, + working_directory=destination_path, + extra_environment=borg_environment, + borg_local_path=local_path, + borg_exit_codes=borg_exit_codes, ) diff --git a/borgmatic/borg/info.py b/borgmatic/borg/info.py index cf31a737..59964c8f 100644 --- a/borgmatic/borg/info.py +++ b/borgmatic/borg/info.py @@ -95,11 +95,13 @@ def display_archives_info( local_path, remote_path, ) + borg_exit_codes = config.get('borg_exit_codes') json_info = execute_command_and_capture_output( json_command, extra_environment=environment.make_environment(config), borg_local_path=local_path, + borg_exit_codes=borg_exit_codes, ) if info_arguments.json: @@ -110,6 +112,7 @@ def display_archives_info( execute_command( main_command, output_log_level=logging.ANSWER, - borg_local_path=local_path, extra_environment=environment.make_environment(config), + borg_local_path=local_path, + borg_exit_codes=borg_exit_codes, ) diff --git a/borgmatic/borg/list.py b/borgmatic/borg/list.py index fcbb1571..7c29df38 100644 --- a/borgmatic/borg/list.py +++ b/borgmatic/borg/list.py @@ -124,6 +124,7 @@ def capture_archive_listing( ), extra_environment=borg_environment, borg_local_path=local_path, + borg_exit_codes=config.get('borg_exit_codes'), ) .strip('\n') .split('\n') @@ -189,6 +190,7 @@ def list_archive( ) borg_environment = environment.make_environment(config) + borg_exit_codes = config.get('borg_exit_codes') # If there are any paths to find (and there's not a single archive already selected), start by # getting a list of archives to search. @@ -219,6 +221,7 @@ def list_archive( ), extra_environment=borg_environment, borg_local_path=local_path, + borg_exit_codes=borg_exit_codes, ) .strip('\n') .split('\n') @@ -251,6 +254,7 @@ def list_archive( execute_command( main_command, output_log_level=logging.ANSWER, - borg_local_path=local_path, extra_environment=borg_environment, + borg_local_path=local_path, + borg_exit_codes=borg_exit_codes, ) diff --git a/borgmatic/borg/mount.py b/borgmatic/borg/mount.py index 9d034688..d8e71c86 100644 --- a/borgmatic/borg/mount.py +++ b/borgmatic/borg/mount.py @@ -65,9 +65,15 @@ def mount_archive( execute_command( full_command, output_file=DO_NOT_CAPTURE, - borg_local_path=local_path, extra_environment=borg_environment, + borg_local_path=local_path, + borg_exit_codes=config.get('borg_exit_codes'), ) return - execute_command(full_command, borg_local_path=local_path, extra_environment=borg_environment) + execute_command( + full_command, + extra_environment=borg_environment, + borg_local_path=local_path, + borg_exit_codes=config.get('borg_exit_codes'), + ) diff --git a/borgmatic/borg/prune.py b/borgmatic/borg/prune.py index 79a43da3..94883bad 100644 --- a/borgmatic/borg/prune.py +++ b/borgmatic/borg/prune.py @@ -94,6 +94,7 @@ def prune_archives( execute_command( full_command, output_log_level=output_log_level, - borg_local_path=local_path, extra_environment=environment.make_environment(config), + borg_local_path=local_path, + borg_exit_codes=config.get('borg_exit_codes'), ) diff --git a/borgmatic/borg/rcreate.py b/borgmatic/borg/rcreate.py index 8fc70d95..935b183b 100644 --- a/borgmatic/borg/rcreate.py +++ b/borgmatic/borg/rcreate.py @@ -81,6 +81,7 @@ def create_repository( execute_command( rcreate_command, output_file=DO_NOT_CAPTURE, - borg_local_path=local_path, extra_environment=environment.make_environment(config), + borg_local_path=local_path, + borg_exit_codes=config.get('borg_exit_codes'), ) diff --git a/borgmatic/borg/rinfo.py b/borgmatic/borg/rinfo.py index ab4197e6..4f211ef2 100644 --- a/borgmatic/borg/rinfo.py +++ b/borgmatic/borg/rinfo.py @@ -49,17 +49,20 @@ def display_repository_info( ) extra_environment = environment.make_environment(config) + borg_exit_codes = config.get('borg_exit_codes') if rinfo_arguments.json: return execute_command_and_capture_output( full_command, extra_environment=extra_environment, borg_local_path=local_path, + borg_exit_codes=borg_exit_codes, ) else: execute_command( full_command, output_log_level=logging.ANSWER, - borg_local_path=local_path, extra_environment=extra_environment, + borg_local_path=local_path, + borg_exit_codes=borg_exit_codes, ) diff --git a/borgmatic/borg/rlist.py b/borgmatic/borg/rlist.py index 54b45b34..0ab10c57 100644 --- a/borgmatic/borg/rlist.py +++ b/borgmatic/borg/rlist.py @@ -45,6 +45,7 @@ def resolve_archive_name( full_command, extra_environment=environment.make_environment(config), borg_local_path=local_path, + borg_exit_codes=config.get('borg_exit_codes'), ) try: latest_archive = output.strip().splitlines()[-1] @@ -147,9 +148,13 @@ def list_repository( local_path, remote_path, ) + borg_exit_codes = config.get('borg_exit_codes') json_listing = execute_command_and_capture_output( - json_command, extra_environment=borg_environment, borg_local_path=local_path + json_command, + extra_environment=borg_environment, + borg_local_path=local_path, + borg_exit_codes=borg_exit_codes, ) if rlist_arguments.json: @@ -160,6 +165,7 @@ def list_repository( execute_command( main_command, output_log_level=logging.ANSWER, - borg_local_path=local_path, extra_environment=borg_environment, + borg_local_path=local_path, + borg_exit_codes=borg_exit_codes, ) diff --git a/borgmatic/borg/transfer.py b/borgmatic/borg/transfer.py index f91349fc..eb2b6c66 100644 --- a/borgmatic/borg/transfer.py +++ b/borgmatic/borg/transfer.py @@ -56,5 +56,6 @@ def transfer_archives( output_log_level=logging.ANSWER, output_file=DO_NOT_CAPTURE if transfer_arguments.progress else None, borg_local_path=local_path, + borg_exit_codes=config.get('borg_exit_codes'), extra_environment=environment.make_environment(config), ) diff --git a/borgmatic/borg/umount.py b/borgmatic/borg/umount.py index 985f9b30..5bfae7b3 100644 --- a/borgmatic/borg/umount.py +++ b/borgmatic/borg/umount.py @@ -5,7 +5,7 @@ from borgmatic.execute import execute_command logger = logging.getLogger(__name__) -def unmount_archive(mount_point, local_path='borg'): +def unmount_archive(config, mount_point, local_path='borg'): ''' Given a mounted filesystem mount point, and an optional local Borg paths, umount the filesystem from the mount point. @@ -17,4 +17,6 @@ def unmount_archive(mount_point, local_path='borg'): + (mount_point,) ) - execute_command(full_command) + execute_command( + full_command, borg_local_path=local_path, borg_exit_codes=config.get('borg_exit_codes') + ) diff --git a/borgmatic/borg/version.py b/borgmatic/borg/version.py index 9ded62a7..87facf21 100644 --- a/borgmatic/borg/version.py +++ b/borgmatic/borg/version.py @@ -22,6 +22,7 @@ def local_borg_version(config, local_path='borg'): full_command, extra_environment=environment.make_environment(config), borg_local_path=local_path, + borg_exit_codes=config.get('borg_exit_codes'), ) try: diff --git a/borgmatic/commands/borgmatic.py b/borgmatic/commands/borgmatic.py index 52f00da3..d4d68d5f 100644 --- a/borgmatic/commands/borgmatic.py +++ b/borgmatic/commands/borgmatic.py @@ -801,6 +801,7 @@ def collect_configuration_run_summary_logs(configs, config_paths, arguments): logger.info(f"Unmounting mount point {arguments['umount'].mount_point}") try: borg_umount.unmount_archive( + config, mount_point=arguments['umount'].mount_point, local_path=get_local_path(configs), ) diff --git a/borgmatic/config/schema.yaml b/borgmatic/config/schema.yaml index e66ba624..7d81c498 100644 --- a/borgmatic/config/schema.yaml +++ b/borgmatic/config/schema.yaml @@ -341,6 +341,37 @@ properties: Path for Borg encryption key files. Defaults to $borg_base_directory/.config/borg/keys example: /path/to/base/config/keys + borg_exit_codes: + type: array + items: + type: object + required: ['code', 'treat_as'] + additionalProperties: false + properties: + code: + type: integer + not: {enum: [0]} + description: | + The exit code for an existing Borg warning or error. + example: 100 + treat_as: + type: string + enum: ['error', 'warning'] + description: | + Whether to consider the exit code as an error or as a + warning in borgmatic. + example: error + description: | + A list of Borg exit codes that should be elevated to errors or + squashed to warnings as indicated. By default, Borg error exit codes + (2 to 99) are treated as errors while warning exit codes (1 and + 100+) are treated as warnings. Exit codes other than 1 and 2 are + only present in Borg 1.4.0+. + example: + - code: 13 + treat_as: warning + - code: 100 + treat_as: error umask: type: integer description: | diff --git a/borgmatic/execute.py b/borgmatic/execute.py index 2158e1e0..fdc9eb76 100644 --- a/borgmatic/execute.py +++ b/borgmatic/execute.py @@ -1,4 +1,5 @@ import collections +import enum import logging import os import select @@ -8,22 +9,61 @@ logger = logging.getLogger(__name__) ERROR_OUTPUT_MAX_LINE_COUNT = 25 -BORG_ERROR_EXIT_CODE = 2 +BORG_ERROR_EXIT_CODE_START = 2 +BORG_ERROR_EXIT_CODE_END = 99 -def exit_code_indicates_error(command, exit_code, borg_local_path=None): +class Exit_status(enum.Enum): + STILL_RUNNING = 1 + SUCCESS = 2 + WARNING = 3 + ERROR = 4 + + +def interpret_exit_code(command, exit_code, borg_local_path=None, borg_exit_codes=None): ''' - Return True if the given exit code from running a command corresponds to an error. If a Borg - local path is given and matches the process' command, then treat exit code 1 as a warning - instead of an error. + Return an Exit_status value (e.g. SUCCESS, ERROR, or WARNING) based on interpreting the given + exit code. If a Borg local path is given and matches the process' command, then interpret the + exit code based on Borg's documented exit code semantics. And if Borg exit codes are given as a + sequence of exit code configuration dicts, then take those configured preferences into account. ''' if exit_code is None: - return False + return Exit_status.STILL_RUNNING + if exit_code == 0: + return Exit_status.SUCCESS if borg_local_path and command[0] == borg_local_path: - return bool(exit_code < 0 or exit_code >= BORG_ERROR_EXIT_CODE) + # First try looking for the exit code in the borg_exit_codes configuration. + for entry in borg_exit_codes or (): + if entry.get('code') == exit_code: + treat_as = entry.get('treat_as') - return bool(exit_code != 0) + if treat_as == 'error': + logger.error( + f'Treating exit code {exit_code} as an error, as per configuration' + ) + return Exit_status.ERROR + elif treat_as == 'warning': + logger.warning( + f'Treating exit code {exit_code} as a warning, as per configuration' + ) + return Exit_status.WARNING + + # If the exit code doesn't have explicit configuration, then fall back to the default Borg + # behavior. + return ( + Exit_status.ERROR + if ( + exit_code < 0 + or ( + exit_code >= BORG_ERROR_EXIT_CODE_START + and exit_code <= BORG_ERROR_EXIT_CODE_END + ) + ) + else Exit_status.WARNING + ) + + return Exit_status.ERROR def command_for_process(process): @@ -60,7 +100,7 @@ def append_last_lines(last_lines, captured_output, line, output_log_level): logger.log(output_log_level, line) -def log_outputs(processes, exclude_stdouts, output_log_level, borg_local_path): +def log_outputs(processes, exclude_stdouts, output_log_level, borg_local_path, borg_exit_codes): ''' Given a sequence of subprocess.Popen() instances for multiple processes, log the output for each process with the requested log level. Additionally, raise a CalledProcessError if a process @@ -68,7 +108,8 @@ def log_outputs(processes, exclude_stdouts, output_log_level, borg_local_path): path). If output log level is None, then instead of logging, capture output for each process and return - it as a dict from the process to its output. + it as a dict from the process to its output. Use the given Borg local path and exit code + configuration to decide what's an error and what's a warning. For simplicity, it's assumed that the output buffer for each process is its stdout. But if any stdouts are given to exclude, then for any matching processes, log from their stderr instead. @@ -132,11 +173,13 @@ def log_outputs(processes, exclude_stdouts, output_log_level, borg_local_path): if exit_code is None: still_running = True + command = process.args.split(' ') if isinstance(process.args, str) else process.args + continue command = process.args.split(' ') if isinstance(process.args, str) else process.args + exit_status = interpret_exit_code(command, exit_code, borg_local_path, borg_exit_codes) - # If any process errors, then raise accordingly. - if exit_code_indicates_error(command, exit_code, borg_local_path): + if exit_status in (Exit_status.ERROR, Exit_status.WARNING): # If an error occurs, include its output in the raised exception so that we don't # inadvertently hide error output. output_buffer = output_buffer_for_process(process, exclude_stdouts) @@ -162,9 +205,13 @@ def log_outputs(processes, exclude_stdouts, output_log_level, borg_local_path): other_process.stdout.read(0) other_process.kill() - raise subprocess.CalledProcessError( - exit_code, command_for_process(process), '\n'.join(last_lines) - ) + if exit_status == Exit_status.ERROR: + raise subprocess.CalledProcessError( + exit_code, command_for_process(process), '\n'.join(last_lines) + ) + + still_running = False + break if captured_outputs: return { @@ -199,6 +246,7 @@ def execute_command( extra_environment=None, working_directory=None, borg_local_path=None, + borg_exit_codes=None, run_to_completion=True, ): ''' @@ -209,8 +257,9 @@ def execute_command( augment the current environment, and pass the result into the command. If a working directory is given, use that as the present working directory when running the command. If a Borg local path is given, and the command matches it (regardless of arguments), treat exit code 1 as a warning - instead of an error. If run to completion is False, then return the process for the command - without executing it to completion. + instead of an error. But if Borg exit codes are given as a sequence of exit code configuration + dicts, then use that configuration to decide what's an error and what's a warning. If run to + completion is False, then return the process for the command without executing it to completion. Raise subprocesses.CalledProcessError if an error occurs while running the command. ''' @@ -232,7 +281,11 @@ def execute_command( return process log_outputs( - (process,), (input_file, output_file), output_log_level, borg_local_path=borg_local_path + (process,), + (input_file, output_file), + output_log_level, + borg_local_path, + borg_exit_codes, ) @@ -243,6 +296,7 @@ def execute_command_and_capture_output( extra_environment=None, working_directory=None, borg_local_path=None, + borg_exit_codes=None, ): ''' Execute the given command (a sequence of command/argument strings), capturing and returning its @@ -251,7 +305,9 @@ def execute_command_and_capture_output( given, then use it to augment the current environment, and pass the result into the command. If a working directory is given, use that as the present working directory when running the command. If a Borg local path is given, and the command matches it (regardless of arguments), - treat exit code 1 as a warning instead of an error. + treat exit code 1 as a warning instead of an error. But if Borg exit codes are given as a + sequence of exit code configuration dicts, then use that configuration to decide what's an error + and what's a warning. Raise subprocesses.CalledProcessError if an error occurs while running the command. ''' @@ -268,7 +324,10 @@ def execute_command_and_capture_output( cwd=working_directory, ) except subprocess.CalledProcessError as error: - if exit_code_indicates_error(command, error.returncode, borg_local_path): + if ( + interpret_exit_code(command, error.returncode, borg_local_path, borg_exit_codes) + == Exit_status.ERROR + ): raise output = error.output @@ -285,6 +344,7 @@ def execute_command_with_processes( extra_environment=None, working_directory=None, borg_local_path=None, + borg_exit_codes=None, ): ''' Execute the given command (a sequence of command/argument strings) and log its output at the @@ -299,7 +359,9 @@ def execute_command_with_processes( use it to augment the current environment, and pass the result into the command. If a working directory is given, use that as the present working directory when running the command. If a Borg local path is given, then for any matching command or process (regardless of arguments), - treat exit code 1 as a warning instead of an error. + treat exit code 1 as a warning instead of an error. But if Borg exit codes are given as a + sequence of exit code configuration dicts, then use that configuration to decide what's an error + and what's a warning. Raise subprocesses.CalledProcessError if an error occurs while running the command or in the upstream process. @@ -334,7 +396,8 @@ def execute_command_with_processes( tuple(processes) + (command_process,), (input_file, output_file), output_log_level, - borg_local_path=borg_local_path, + borg_local_path, + borg_exit_codes, ) if output_log_level is None: diff --git a/docs/how-to/customize-warnings-and-errors.md b/docs/how-to/customize-warnings-and-errors.md new file mode 100644 index 00000000..9f97e1a5 --- /dev/null +++ b/docs/how-to/customize-warnings-and-errors.md @@ -0,0 +1,83 @@ +--- +title: How to customize warnings and errors +eleventyNavigation: + key: 💥 Customize warnings/errors + parent: How-to guides + order: 12 +--- +## When things go wrong + +After Borg runs, it indicates whether it succeeded via its exit code, a +numeric ID indicating success, warning, or error. borgmatic consumes this exit +code to decide how to respond. Normally, a Borg error results in a borgmatic +error, while a Borg warning or success doesn't. + +But if that default behavior isn't sufficient for your needs, you can +customize how borgmatic interprets [Borg's exit +codes](https://borgbackup.readthedocs.io/en/stable/usage/general.html#return-codes). +For instance, to elevate Borg warnings to errors, thereby causing borgmatic to +error on them, use the following borgmatic configuration: + +```yaml +borg_exit_codes: + - exit_code: 1 + treat_as: error +``` + +Be aware though that Borg exits with a warning code for a variety of benign +situations such as files changing while they're being read, so this example +may not meet your needs. Keep reading though for more granular exit code +configuration. + +Here's an example that squashes Borg errors to warnings: + +```yaml +borg_exit_codes: + - exit_code: 2 + treat_as: warning +``` + +Be careful with this example though, because it prevents borgmatic from +erroring when Borg errors, which may not be desirable. + +New in Borg version 1.4 Borg +support for [more granular exit +codes](https://borgbackup.readthedocs.io/en/1.4-maint/usage/general.html#return-codes) +means that you can configure borgmatic to respond to specific Borg conditions. +See the full list of [Borg 1.4 error and warning exit +codes](https://borgbackup.readthedocs.io/en/1.4.0b1/internals/frontends.html#message-ids). +The `rc:` numeric value there tells you the exit code for each. + +For instance, this borgmatic configuration elevates all Borg backup file +permission warnings (exit code `105`)—and only those warnings—to errors: + +```yaml +borg_exit_codes: + - exit_code: 105 + treat_as: error +``` + +The following configuration does that *and* elevates backup file not found +warnings (exit code `107`) to errors as well: + +```yaml +borg_exit_codes: + - exit_code: 105 + treat_as: error + - exit_code: 107 + treat_as: error +``` + +If you don't know the exit code for a particular Borg error or warning you're +experiencing, you can usually find it in your borgmatic output when +`--verbosity 2` is enabled. For instance, here's a snippet of that output when +a backup file is not found: + +``` +/noexist: stat: [Errno 2] No such file or directory: '/noexist' +... +terminating with warning status, rc 107 +``` + +The exit status to use in that case would be `107` if you want to configure +borgmatic to treat it as an error. diff --git a/docs/how-to/develop-on-borgmatic.md b/docs/how-to/develop-on-borgmatic.md index f731c241..1e368687 100644 --- a/docs/how-to/develop-on-borgmatic.md +++ b/docs/how-to/develop-on-borgmatic.md @@ -3,7 +3,7 @@ title: How to develop on borgmatic eleventyNavigation: key: 🏗️ Develop on borgmatic parent: How-to guides - order: 13 + order: 14 --- ## Source code diff --git a/docs/how-to/make-per-application-backups.md b/docs/how-to/make-per-application-backups.md index 091c7dfd..4078abb3 100644 --- a/docs/how-to/make-per-application-backups.md +++ b/docs/how-to/make-per-application-backups.md @@ -139,8 +139,8 @@ Some borgmatic command-line actions also have a `--match-archives` flag that overrides both the auto-matching behavior and the `match_archives` configuration option. -Prior to 1.7.11 The way to -limit the archives used for the `prune` action was a `prefix` option in the +Prior to version 1.7.11 The way +to limit the archives used for the `prune` action was a `prefix` option in the `retention` section for matching against the start of archive names. And the option for limiting the archives used for the `check` action was a separate `prefix` in the `consistency` section. Both of these options are deprecated in diff --git a/docs/how-to/upgrade.md b/docs/how-to/upgrade.md index 9708c005..a3bd3b80 100644 --- a/docs/how-to/upgrade.md +++ b/docs/how-to/upgrade.md @@ -3,7 +3,7 @@ title: How to upgrade borgmatic and Borg eleventyNavigation: key: 📦 Upgrade borgmatic/Borg parent: How-to guides - order: 12 + order: 13 --- ## Upgrading borgmatic diff --git a/tests/integration/test_execute.py b/tests/integration/test_execute.py index 9c62941b..21b442d5 100644 --- a/tests/integration/test_execute.py +++ b/tests/integration/test_execute.py @@ -11,7 +11,7 @@ from borgmatic import execute as module def test_log_outputs_logs_each_line_separately(): flexmock(module.logger).should_receive('log').with_args(logging.INFO, 'hi').once() flexmock(module.logger).should_receive('log').with_args(logging.INFO, 'there').once() - flexmock(module).should_receive('exit_code_indicates_error').and_return(False) + flexmock(module).should_receive('interpret_exit_code').and_return(module.Exit_status.SUCCESS) hi_process = subprocess.Popen(['echo', 'hi'], stdout=subprocess.PIPE) flexmock(module).should_receive('output_buffer_for_process').with_args( @@ -28,13 +28,14 @@ def test_log_outputs_logs_each_line_separately(): exclude_stdouts=(), output_log_level=logging.INFO, borg_local_path='borg', + borg_exit_codes=None, ) def test_log_outputs_skips_logs_for_process_with_none_stdout(): flexmock(module.logger).should_receive('log').with_args(logging.INFO, 'hi').never() flexmock(module.logger).should_receive('log').with_args(logging.INFO, 'there').once() - flexmock(module).should_receive('exit_code_indicates_error').and_return(False) + flexmock(module).should_receive('interpret_exit_code').and_return(module.Exit_status.SUCCESS) hi_process = subprocess.Popen(['echo', 'hi'], stdout=None) flexmock(module).should_receive('output_buffer_for_process').with_args( @@ -51,12 +52,13 @@ def test_log_outputs_skips_logs_for_process_with_none_stdout(): exclude_stdouts=(), output_log_level=logging.INFO, borg_local_path='borg', + borg_exit_codes=None, ) def test_log_outputs_returns_output_without_logging_for_output_log_level_none(): flexmock(module.logger).should_receive('log').never() - flexmock(module).should_receive('exit_code_indicates_error').and_return(False) + flexmock(module).should_receive('interpret_exit_code').and_return(module.Exit_status.SUCCESS) hi_process = subprocess.Popen(['echo', 'hi'], stdout=subprocess.PIPE) flexmock(module).should_receive('output_buffer_for_process').with_args( @@ -73,6 +75,7 @@ def test_log_outputs_returns_output_without_logging_for_output_log_level_none(): exclude_stdouts=(), output_log_level=None, borg_local_path='borg', + borg_exit_codes=None, ) assert captured_outputs == {hi_process: 'hi', there_process: 'there'} @@ -80,7 +83,7 @@ def test_log_outputs_returns_output_without_logging_for_output_log_level_none(): def test_log_outputs_includes_error_output_in_exception(): flexmock(module.logger).should_receive('log') - flexmock(module).should_receive('exit_code_indicates_error').and_return(True) + flexmock(module).should_receive('interpret_exit_code').and_return(module.Exit_status.ERROR) flexmock(module).should_receive('command_for_process').and_return('grep') process = subprocess.Popen(['grep'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) @@ -88,7 +91,11 @@ def test_log_outputs_includes_error_output_in_exception(): with pytest.raises(subprocess.CalledProcessError) as error: module.log_outputs( - (process,), exclude_stdouts=(), output_log_level=logging.INFO, borg_local_path='borg' + (process,), + exclude_stdouts=(), + output_log_level=logging.INFO, + borg_local_path='borg', + borg_exit_codes=None, ) assert error.value.output @@ -100,7 +107,7 @@ def test_log_outputs_logs_multiline_error_output(): of a process' traceback. ''' flexmock(module.logger).should_receive('log') - flexmock(module).should_receive('exit_code_indicates_error').and_return(True) + flexmock(module).should_receive('interpret_exit_code').and_return(module.Exit_status.ERROR) flexmock(module).should_receive('command_for_process').and_return('grep') process = subprocess.Popen( @@ -111,13 +118,17 @@ def test_log_outputs_logs_multiline_error_output(): with pytest.raises(subprocess.CalledProcessError): module.log_outputs( - (process,), exclude_stdouts=(), output_log_level=logging.INFO, borg_local_path='borg' + (process,), + exclude_stdouts=(), + output_log_level=logging.INFO, + borg_local_path='borg', + borg_exit_codes=None, ) def test_log_outputs_skips_error_output_in_exception_for_process_with_none_stdout(): flexmock(module.logger).should_receive('log') - flexmock(module).should_receive('exit_code_indicates_error').and_return(True) + flexmock(module).should_receive('interpret_exit_code').and_return(module.Exit_status.ERROR) flexmock(module).should_receive('command_for_process').and_return('grep') process = subprocess.Popen(['grep'], stdout=None) @@ -125,30 +136,43 @@ def test_log_outputs_skips_error_output_in_exception_for_process_with_none_stdou with pytest.raises(subprocess.CalledProcessError) as error: module.log_outputs( - (process,), exclude_stdouts=(), output_log_level=logging.INFO, borg_local_path='borg' + (process,), + exclude_stdouts=(), + output_log_level=logging.INFO, + borg_local_path='borg', + borg_exit_codes=None, ) assert error.value.returncode == 2 assert not error.value.output -def test_log_outputs_kills_other_processes_when_one_errors(): +def test_log_outputs_kills_other_processes_and_raises_when_one_errors(): flexmock(module.logger).should_receive('log') flexmock(module).should_receive('command_for_process').and_return('grep') process = subprocess.Popen(['grep'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - flexmock(module).should_receive('exit_code_indicates_error').with_args( - ['grep'], None, 'borg' - ).and_return(False) - flexmock(module).should_receive('exit_code_indicates_error').with_args( - ['grep'], 2, 'borg' - ).and_return(True) + flexmock(module).should_receive('interpret_exit_code').with_args( + ['grep'], + None, + 'borg', + None, + ).and_return(module.Exit_status.SUCCESS) + flexmock(module).should_receive('interpret_exit_code').with_args( + ['grep'], + 2, + 'borg', + None, + ).and_return(module.Exit_status.ERROR) other_process = subprocess.Popen( ['sleep', '2'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT ) - flexmock(module).should_receive('exit_code_indicates_error').with_args( - ['sleep', '2'], None, 'borg' - ).and_return(False) + flexmock(module).should_receive('interpret_exit_code').with_args( + ['sleep', '2'], + None, + 'borg', + None, + ).and_return(module.Exit_status.SUCCESS) flexmock(module).should_receive('output_buffer_for_process').with_args(process, ()).and_return( process.stdout ) @@ -163,12 +187,56 @@ def test_log_outputs_kills_other_processes_when_one_errors(): exclude_stdouts=(), output_log_level=logging.INFO, borg_local_path='borg', + borg_exit_codes=None, ) assert error.value.returncode == 2 assert error.value.output +def test_log_outputs_kills_other_processes_and_returns_when_one_exits_with_warning(): + flexmock(module.logger).should_receive('log') + flexmock(module).should_receive('command_for_process').and_return('grep') + + process = subprocess.Popen(['grep'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + flexmock(module).should_receive('interpret_exit_code').with_args( + ['grep'], + None, + 'borg', + None, + ).and_return(module.Exit_status.SUCCESS) + flexmock(module).should_receive('interpret_exit_code').with_args( + ['grep'], + 2, + 'borg', + None, + ).and_return(module.Exit_status.WARNING) + other_process = subprocess.Popen( + ['sleep', '2'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT + ) + flexmock(module).should_receive('interpret_exit_code').with_args( + ['sleep', '2'], + None, + 'borg', + None, + ).and_return(module.Exit_status.SUCCESS) + flexmock(module).should_receive('output_buffer_for_process').with_args(process, ()).and_return( + process.stdout + ) + flexmock(module).should_receive('output_buffer_for_process').with_args( + other_process, () + ).and_return(other_process.stdout) + flexmock(other_process).should_receive('kill').once() + + module.log_outputs( + (process, other_process), + exclude_stdouts=(), + output_log_level=logging.INFO, + borg_local_path='borg', + borg_exit_codes=None, + ) + + def test_log_outputs_vents_other_processes_when_one_exits(): ''' Execute a command to generate a longish random string and pipe it into another command that @@ -204,6 +272,7 @@ def test_log_outputs_vents_other_processes_when_one_exits(): exclude_stdouts=(process.stdout,), output_log_level=logging.INFO, borg_local_path='borg', + borg_exit_codes=None, ) @@ -235,6 +304,7 @@ def test_log_outputs_does_not_error_when_one_process_exits(): exclude_stdouts=(process.stdout,), output_log_level=logging.INFO, borg_local_path='borg', + borg_exit_codes=None, ) @@ -243,17 +313,27 @@ def test_log_outputs_truncates_long_error_output(): flexmock(module).should_receive('command_for_process').and_return('grep') process = subprocess.Popen(['grep'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - flexmock(module).should_receive('exit_code_indicates_error').with_args( - ['grep'], None, 'borg' - ).and_return(False) - flexmock(module).should_receive('exit_code_indicates_error').with_args( - ['grep'], 2, 'borg' - ).and_return(True) + flexmock(module).should_receive('interpret_exit_code').with_args( + ['grep'], + None, + 'borg', + None, + ).and_return(module.Exit_status.SUCCESS) + flexmock(module).should_receive('interpret_exit_code').with_args( + ['grep'], + 2, + 'borg', + None, + ).and_return(module.Exit_status.ERROR) flexmock(module).should_receive('output_buffer_for_process').and_return(process.stdout) with pytest.raises(subprocess.CalledProcessError) as error: flexmock(module, ERROR_OUTPUT_MAX_LINE_COUNT=0).log_outputs( - (process,), exclude_stdouts=(), output_log_level=logging.INFO, borg_local_path='borg' + (process,), + exclude_stdouts=(), + output_log_level=logging.INFO, + borg_local_path='borg', + borg_exit_codes=None, ) assert error.value.returncode == 2 @@ -262,24 +342,32 @@ def test_log_outputs_truncates_long_error_output(): def test_log_outputs_with_no_output_logs_nothing(): flexmock(module.logger).should_receive('log').never() - flexmock(module).should_receive('exit_code_indicates_error').and_return(False) + flexmock(module).should_receive('interpret_exit_code').and_return(module.Exit_status.SUCCESS) process = subprocess.Popen(['true'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) flexmock(module).should_receive('output_buffer_for_process').and_return(process.stdout) module.log_outputs( - (process,), exclude_stdouts=(), output_log_level=logging.INFO, borg_local_path='borg' + (process,), + exclude_stdouts=(), + output_log_level=logging.INFO, + borg_local_path='borg', + borg_exit_codes=None, ) def test_log_outputs_with_unfinished_process_re_polls(): flexmock(module.logger).should_receive('log').never() - flexmock(module).should_receive('exit_code_indicates_error').and_return(False) + flexmock(module).should_receive('interpret_exit_code').and_return(module.Exit_status.SUCCESS) process = subprocess.Popen(['true'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) flexmock(process).should_receive('poll').and_return(None).and_return(0).times(3) flexmock(module).should_receive('output_buffer_for_process').and_return(process.stdout) module.log_outputs( - (process,), exclude_stdouts=(), output_log_level=logging.INFO, borg_local_path='borg' + (process,), + exclude_stdouts=(), + output_log_level=logging.INFO, + borg_local_path='borg', + borg_exit_codes=None, ) diff --git a/tests/unit/borg/test_borg.py b/tests/unit/borg/test_borg.py index a132b7a5..fee4c469 100644 --- a/tests/unit/borg/test_borg.py +++ b/tests/unit/borg/test_borg.py @@ -16,6 +16,7 @@ def test_run_arbitrary_borg_calls_borg_with_flags(): ('borg', 'break-lock', '::'), output_file=module.borgmatic.execute.DO_NOT_CAPTURE, borg_local_path='borg', + borg_exit_codes=None, shell=True, extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''}, ) @@ -37,6 +38,7 @@ def test_run_arbitrary_borg_with_log_info_calls_borg_with_info_flag(): ('borg', 'break-lock', '--info', '::'), output_file=module.borgmatic.execute.DO_NOT_CAPTURE, borg_local_path='borg', + borg_exit_codes=None, shell=True, extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''}, ) @@ -59,6 +61,7 @@ def test_run_arbitrary_borg_with_log_debug_calls_borg_with_debug_flag(): ('borg', 'break-lock', '--debug', '--show-rc', '::'), output_file=module.borgmatic.execute.DO_NOT_CAPTURE, borg_local_path='borg', + borg_exit_codes=None, shell=True, extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''}, ) @@ -84,6 +87,7 @@ def test_run_arbitrary_borg_with_lock_wait_calls_borg_with_lock_wait_flags(): ('borg', 'break-lock', '--lock-wait', '5', '::'), output_file=module.borgmatic.execute.DO_NOT_CAPTURE, borg_local_path='borg', + borg_exit_codes=None, shell=True, extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''}, ) @@ -105,6 +109,7 @@ def test_run_arbitrary_borg_with_archive_calls_borg_with_archive_flag(): ('borg', 'break-lock', "'::$ARCHIVE'"), output_file=module.borgmatic.execute.DO_NOT_CAPTURE, borg_local_path='borg', + borg_exit_codes=None, shell=True, extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': 'archive'}, ) @@ -127,6 +132,7 @@ def test_run_arbitrary_borg_with_local_path_calls_borg_via_local_path(): ('borg1', 'break-lock', '::'), output_file=module.borgmatic.execute.DO_NOT_CAPTURE, borg_local_path='borg1', + borg_exit_codes=None, shell=True, extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''}, ) @@ -140,6 +146,29 @@ def test_run_arbitrary_borg_with_local_path_calls_borg_via_local_path(): ) +def test_run_arbitrary_borg_with_exit_codes_calls_borg_using_them(): + 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.environment).should_receive('make_environment') + borg_exit_codes = flexmock() + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'break-lock', '::'), + output_file=module.borgmatic.execute.DO_NOT_CAPTURE, + borg_local_path='borg', + borg_exit_codes=borg_exit_codes, + shell=True, + extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''}, + ) + + module.run_arbitrary_borg( + repository_path='repo', + config={'borg_exit_codes': borg_exit_codes}, + local_borg_version='1.2.3', + options=['break-lock', '::'], + ) + + def test_run_arbitrary_borg_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 @@ -151,6 +180,7 @@ def test_run_arbitrary_borg_with_remote_path_calls_borg_with_remote_path_flags() ('borg', 'break-lock', '--remote-path', 'borg1', '::'), output_file=module.borgmatic.execute.DO_NOT_CAPTURE, borg_local_path='borg', + borg_exit_codes=None, shell=True, extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''}, ) @@ -175,6 +205,7 @@ def test_run_arbitrary_borg_with_remote_path_injection_attack_gets_escaped(): ('borg', 'break-lock', '--remote-path', "'borg1; naughty-command'", '::'), output_file=module.borgmatic.execute.DO_NOT_CAPTURE, borg_local_path='borg', + borg_exit_codes=None, shell=True, extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''}, ) @@ -197,6 +228,7 @@ def test_run_arbitrary_borg_passes_borg_specific_flags_to_borg(): ('borg', 'list', '--progress', '::'), output_file=module.borgmatic.execute.DO_NOT_CAPTURE, borg_local_path='borg', + borg_exit_codes=None, shell=True, extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''}, ) @@ -218,6 +250,7 @@ def test_run_arbitrary_borg_omits_dash_dash_in_flags_passed_to_borg(): ('borg', 'break-lock', '::'), output_file=module.borgmatic.execute.DO_NOT_CAPTURE, borg_local_path='borg', + borg_exit_codes=None, shell=True, extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''}, ) @@ -239,6 +272,7 @@ def test_run_arbitrary_borg_without_borg_specific_flags_does_not_raise(): ('borg',), output_file=module.borgmatic.execute.DO_NOT_CAPTURE, borg_local_path='borg', + borg_exit_codes=None, shell=True, extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''}, ) @@ -260,6 +294,7 @@ def test_run_arbitrary_borg_passes_key_sub_command_to_borg_before_injected_flags ('borg', 'key', 'export', '--info', '::'), output_file=module.borgmatic.execute.DO_NOT_CAPTURE, borg_local_path='borg', + borg_exit_codes=None, shell=True, extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''}, ) @@ -282,6 +317,7 @@ def test_run_arbitrary_borg_passes_debug_sub_command_to_borg_before_injected_fla ('borg', 'debug', 'dump-manifest', '--info', '::', 'path'), output_file=module.borgmatic.execute.DO_NOT_CAPTURE, borg_local_path='borg', + borg_exit_codes=None, shell=True, extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''}, ) diff --git a/tests/unit/borg/test_break_lock.py b/tests/unit/borg/test_break_lock.py index ff26cab7..47da993f 100644 --- a/tests/unit/borg/test_break_lock.py +++ b/tests/unit/borg/test_break_lock.py @@ -7,11 +7,12 @@ from borgmatic.borg import break_lock as module from ..test_verbosity import insert_logging_mock -def insert_execute_command_mock(command): +def insert_execute_command_mock(command, borg_exit_codes=None): flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( command, - borg_local_path='borg', + borg_local_path=command[0], + borg_exit_codes=borg_exit_codes, extra_environment=None, ).once() @@ -28,6 +29,32 @@ def test_break_lock_calls_borg_with_required_flags(): ) +def test_break_lock_calls_borg_with_local_path(): + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + insert_execute_command_mock(('borg1', 'break-lock', 'repo')) + + module.break_lock( + repository_path='repo', + config={}, + local_borg_version='1.2.3', + global_arguments=flexmock(log_json=False), + local_path='borg1', + ) + + +def test_break_lock_calls_borg_using_exit_codes(): + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + insert_execute_command_mock(('borg1', 'break-lock', 'repo')) + + module.break_lock( + repository_path='repo', + config={}, + local_borg_version='1.2.3', + global_arguments=flexmock(log_json=False), + local_path='borg1', + ) + + def test_break_lock_calls_borg_with_remote_path_flags(): flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_execute_command_mock(('borg', 'break-lock', '--remote-path', 'borg1', 'repo')) diff --git a/tests/unit/borg/test_check.py b/tests/unit/borg/test_check.py index e222d680..323c3389 100644 --- a/tests/unit/borg/test_check.py +++ b/tests/unit/borg/test_check.py @@ -8,10 +8,13 @@ from borgmatic.borg import check as module from ..test_verbosity import insert_logging_mock -def insert_execute_command_mock(command): +def insert_execute_command_mock(command, borg_exit_codes=None): flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - command, extra_environment=None + command, + extra_environment=None, + borg_local_path=command[0], + borg_exit_codes=borg_exit_codes, ).once() @@ -689,6 +692,8 @@ def test_check_archives_with_progress_passes_through_to_borg(): ('borg', 'check', '--progress', 'repo'), output_file=module.DO_NOT_CAPTURE, extra_environment=None, + borg_local_path='borg', + borg_exit_codes=None, ).once() flexmock(module).should_receive('make_check_time_path') flexmock(module).should_receive('write_check_time') @@ -723,6 +728,8 @@ def test_check_archives_with_repair_passes_through_to_borg(): ('borg', 'check', '--repair', 'repo'), output_file=module.DO_NOT_CAPTURE, extra_environment=None, + borg_local_path='borg', + borg_exit_codes=None, ).once() flexmock(module).should_receive('make_check_time_path') flexmock(module).should_receive('write_check_time') @@ -963,6 +970,36 @@ def test_check_archives_with_local_path_calls_borg_via_local_path(): ) +def test_check_archives_with_exit_codes_calls_borg_using_them(): + checks = ('repository',) + check_last = flexmock() + borg_exit_codes = flexmock() + config = {'check_last': check_last, 'borg_exit_codes': borg_exit_codes} + flexmock(module.rinfo).should_receive('display_repository_info').and_return( + '{"repository": {"id": "repo"}}' + ) + flexmock(module).should_receive('upgrade_check_times') + flexmock(module).should_receive('parse_checks') + flexmock(module).should_receive('make_archive_filter_flags').and_return(()) + flexmock(module).should_receive('make_archives_check_id').and_return(None) + flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks) + flexmock(module).should_receive('make_check_flags').with_args(checks, ()).and_return(()) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + insert_execute_command_mock(('borg', 'check', 'repo'), borg_exit_codes=borg_exit_codes) + flexmock(module).should_receive('make_check_time_path') + flexmock(module).should_receive('write_check_time') + + module.check_archives( + repository_path='repo', + config=config, + local_borg_version='1.2.3', + check_arguments=flexmock( + progress=None, repair=None, only_checks=None, force=None, match_archives=None + ), + global_arguments=flexmock(log_json=False), + ) + + def test_check_archives_with_remote_path_passes_through_to_borg(): checks = ('repository',) check_last = flexmock() @@ -1128,6 +1165,8 @@ def test_check_archives_with_match_archives_passes_through_to_borg(): flexmock(module).should_receive('execute_command').with_args( ('borg', 'check', '--match-archives', 'foo-*', 'repo'), extra_environment=None, + borg_local_path='borg', + borg_exit_codes=None, ).once() flexmock(module).should_receive('make_check_time_path') flexmock(module).should_receive('write_check_time') diff --git a/tests/unit/borg/test_compact.py b/tests/unit/borg/test_compact.py index c8e3e7f4..75efda24 100644 --- a/tests/unit/borg/test_compact.py +++ b/tests/unit/borg/test_compact.py @@ -7,12 +7,13 @@ from borgmatic.borg import compact as module from ..test_verbosity import insert_logging_mock -def insert_execute_command_mock(compact_command, output_log_level): +def insert_execute_command_mock(compact_command, output_log_level, borg_exit_codes=None): flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( compact_command, output_log_level=output_log_level, borg_local_path=compact_command[0], + borg_exit_codes=borg_exit_codes, extra_environment=None, ).once() @@ -87,6 +88,22 @@ def test_compact_segments_with_local_path_calls_borg_via_local_path(): ) +def test_compact_segments_with_exit_codes_calls_borg_using_them(): + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + borg_exit_codes = flexmock() + insert_execute_command_mock( + COMPACT_COMMAND + ('repo',), logging.INFO, borg_exit_codes=borg_exit_codes + ) + + module.compact_segments( + dry_run=False, + repository_path='repo', + config={'borg_exit_codes': borg_exit_codes}, + local_borg_version='1.2.3', + global_arguments=flexmock(log_json=False), + ) + + def test_compact_segments_with_remote_path_calls_borg_with_remote_path_parameters(): flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_execute_command_mock(COMPACT_COMMAND + ('--remote-path', 'borg1', 'repo'), logging.INFO) diff --git a/tests/unit/borg/test_create.py b/tests/unit/borg/test_create.py index 32ef90d3..bf91bc39 100644 --- a/tests/unit/borg/test_create.py +++ b/tests/unit/borg/test_create.py @@ -402,6 +402,7 @@ def test_collect_special_file_paths_parses_special_files_from_borg_dry_run_file_ assert module.collect_special_file_paths( ('borg', 'create'), + config={}, local_path=None, working_directory=None, borg_environment=None, @@ -420,6 +421,7 @@ def test_collect_special_file_paths_excludes_requested_directories(): assert module.collect_special_file_paths( ('borg', 'create'), + config={}, local_path=None, working_directory=None, borg_environment=None, @@ -438,6 +440,7 @@ def test_collect_special_file_paths_excludes_non_special_files(): assert module.collect_special_file_paths( ('borg', 'create'), + config={}, local_path=None, working_directory=None, borg_environment=None, @@ -452,12 +455,14 @@ def test_collect_special_file_paths_omits_exclude_no_dump_flag_from_command(): working_directory=None, extra_environment=None, borg_local_path='borg', + borg_exit_codes=None, ).and_return('Processing files ...\n- /foo\n+ /bar\n- /baz').once() flexmock(module).should_receive('special_file').and_return(True) flexmock(module).should_receive('any_parent_directories').and_return(False) module.collect_special_file_paths( ('borg', 'create', '--exclude-nodump'), + config={}, local_path='borg', working_directory=None, borg_environment=None, @@ -494,6 +499,7 @@ def test_create_archive_calls_borg_with_parameters(): output_log_level=logging.INFO, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -538,6 +544,7 @@ def test_create_archive_calls_borg_with_environment(): output_log_level=logging.INFO, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=environment, ) @@ -584,6 +591,7 @@ def test_create_archive_with_patterns_calls_borg_with_patterns_including_convert output_log_level=logging.INFO, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -634,6 +642,7 @@ def test_create_archive_with_sources_and_config_paths_calls_borg_with_sources_an output_log_level=logging.INFO, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=environment, ) @@ -681,6 +690,7 @@ def test_create_archive_with_sources_and_config_paths_with_store_config_files_fa output_log_level=logging.INFO, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=environment, ) @@ -727,6 +737,7 @@ def test_create_archive_with_exclude_patterns_calls_borg_with_excludes(): output_log_level=logging.INFO, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -770,6 +781,7 @@ def test_create_archive_with_log_info_calls_borg_with_info_parameter(): output_log_level=logging.INFO, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -814,6 +826,7 @@ def test_create_archive_with_log_info_and_json_suppresses_most_borg_output(): working_directory=None, extra_environment=None, borg_local_path='borg', + borg_exit_codes=None, ) insert_logging_mock(logging.INFO) @@ -857,6 +870,7 @@ def test_create_archive_with_log_debug_calls_borg_with_debug_parameter(): output_log_level=logging.INFO, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -901,6 +915,7 @@ def test_create_archive_with_log_debug_and_json_suppresses_most_borg_output(): working_directory=None, extra_environment=None, borg_local_path='borg', + borg_exit_codes=None, ) insert_logging_mock(logging.DEBUG) @@ -944,6 +959,7 @@ def test_create_archive_with_dry_run_calls_borg_with_dry_run_parameter(): output_log_level=logging.INFO, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -989,6 +1005,7 @@ def test_create_archive_with_stats_and_dry_run_calls_borg_without_stats_paramete output_log_level=logging.INFO, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -1034,6 +1051,7 @@ def test_create_archive_with_checkpoint_interval_calls_borg_with_checkpoint_inte output_log_level=logging.INFO, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -1078,6 +1096,7 @@ def test_create_archive_with_checkpoint_volume_calls_borg_with_checkpoint_volume output_log_level=logging.INFO, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -1122,6 +1141,7 @@ def test_create_archive_with_chunker_params_calls_borg_with_chunker_params_param output_log_level=logging.INFO, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -1166,6 +1186,7 @@ def test_create_archive_with_compression_calls_borg_with_compression_parameters( output_log_level=logging.INFO, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -1216,6 +1237,7 @@ def test_create_archive_with_upload_rate_limit_calls_borg_with_upload_ratelimit_ output_log_level=logging.INFO, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -1262,6 +1284,7 @@ def test_create_archive_with_working_directory_calls_borg_with_working_directory output_log_level=logging.INFO, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory='/working/dir', extra_environment=None, ) @@ -1306,6 +1329,7 @@ def test_create_archive_with_one_file_system_calls_borg_with_one_file_system_par output_log_level=logging.INFO, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -1356,6 +1380,7 @@ def test_create_archive_with_numeric_ids_calls_borg_with_numeric_ids_parameter( output_log_level=logging.INFO, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -1402,6 +1427,7 @@ def test_create_archive_with_read_special_calls_borg_with_read_special_parameter output_log_level=logging.INFO, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -1410,6 +1436,7 @@ def test_create_archive_with_read_special_calls_borg_with_read_special_parameter output_log_level=logging.INFO, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -1466,6 +1493,7 @@ def test_create_archive_with_basic_option_calls_borg_with_corresponding_paramete output_log_level=logging.INFO, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -1521,6 +1549,7 @@ def test_create_archive_with_atime_option_calls_borg_with_corresponding_paramete output_log_level=logging.INFO, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -1576,6 +1605,7 @@ def test_create_archive_with_flags_option_calls_borg_with_corresponding_paramete output_log_level=logging.INFO, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -1620,6 +1650,7 @@ def test_create_archive_with_files_cache_calls_borg_with_files_cache_parameters( output_log_level=logging.INFO, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -1664,6 +1695,7 @@ def test_create_archive_with_local_path_calls_borg_via_local_path(): output_log_level=logging.INFO, output_file=None, borg_local_path='borg1', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -1683,6 +1715,52 @@ def test_create_archive_with_local_path_calls_borg_via_local_path(): ) +def test_create_archive_with_exit_codes_calls_borg_using_them(): + 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({}) + 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).should_receive('make_list_filter_flags').and_return('FOO') + 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') + borg_exit_codes = flexmock() + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'create') + REPO_ARCHIVE_WITH_PATHS, + output_log_level=logging.INFO, + output_file=None, + borg_local_path='borg', + borg_exit_codes=borg_exit_codes, + working_directory=None, + extra_environment=None, + ) + + module.create_archive( + dry_run=False, + repository_path='repo', + config={ + 'source_directories': ['foo', 'bar'], + 'repositories': ['repo'], + 'exclude_patterns': None, + 'borg_exit_codes': borg_exit_codes, + }, + config_paths=['/tmp/test.yaml'], + local_borg_version='1.2.3', + global_arguments=flexmock(log_json=False), + ) + + 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 @@ -1708,6 +1786,7 @@ def test_create_archive_with_remote_path_calls_borg_with_remote_path_parameters( output_log_level=logging.INFO, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -1752,6 +1831,7 @@ def test_create_archive_with_umask_calls_borg_with_umask_parameters(): output_log_level=logging.INFO, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -1796,6 +1876,7 @@ def test_create_archive_with_log_json_calls_borg_with_log_json_parameters(): output_log_level=logging.INFO, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -1839,6 +1920,7 @@ def test_create_archive_with_lock_wait_calls_borg_with_lock_wait_parameters(): output_log_level=logging.INFO, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -1883,6 +1965,7 @@ def test_create_archive_with_stats_calls_borg_with_stats_parameter_and_answer_ou output_log_level=module.borgmatic.logger.ANSWER, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -1927,6 +2010,7 @@ def test_create_archive_with_files_calls_borg_with_list_parameter_and_answer_out output_log_level=module.borgmatic.logger.ANSWER, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -1971,6 +2055,7 @@ def test_create_archive_with_progress_and_log_info_calls_borg_with_progress_para output_log_level=logging.INFO, output_file=module.DO_NOT_CAPTURE, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -2016,6 +2101,7 @@ def test_create_archive_with_progress_calls_borg_with_progress_parameter(): output_log_level=logging.INFO, output_file=module.DO_NOT_CAPTURE, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -2070,6 +2156,7 @@ def test_create_archive_with_progress_and_stream_processes_calls_borg_with_progr output_log_level=logging.INFO, output_file=module.DO_NOT_CAPTURE, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -2079,6 +2166,7 @@ def test_create_archive_with_progress_and_stream_processes_calls_borg_with_progr output_log_level=logging.INFO, output_file=module.DO_NOT_CAPTURE, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -2134,6 +2222,7 @@ def test_create_archive_with_stream_processes_ignores_read_special_false_and_log output_log_level=logging.INFO, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -2143,6 +2232,7 @@ def test_create_archive_with_stream_processes_ignores_read_special_false_and_log output_log_level=logging.INFO, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -2203,6 +2293,7 @@ def test_create_archive_with_stream_processes_adds_special_files_to_excludes(): output_log_level=logging.INFO, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -2212,6 +2303,7 @@ def test_create_archive_with_stream_processes_adds_special_files_to_excludes(): output_log_level=logging.INFO, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -2267,6 +2359,7 @@ def test_create_archive_with_stream_processes_and_read_special_does_not_add_spec output_log_level=logging.INFO, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -2276,6 +2369,7 @@ def test_create_archive_with_stream_processes_and_read_special_does_not_add_spec output_log_level=logging.INFO, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -2321,6 +2415,7 @@ def test_create_archive_with_json_calls_borg_with_json_parameter(): working_directory=None, extra_environment=None, borg_local_path='borg', + borg_exit_codes=None, ).and_return('[]') json_output = module.create_archive( @@ -2365,6 +2460,7 @@ def test_create_archive_with_stats_and_json_calls_borg_without_stats_parameter() working_directory=None, extra_environment=None, borg_local_path='borg', + borg_exit_codes=None, ).and_return('[]') json_output = module.create_archive( @@ -2410,6 +2506,7 @@ def test_create_archive_with_source_directories_glob_expands(): output_log_level=logging.INFO, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -2454,6 +2551,7 @@ def test_create_archive_with_non_matching_source_directories_glob_passes_through output_log_level=logging.INFO, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -2498,6 +2596,7 @@ def test_create_archive_with_glob_calls_borg_with_expanded_directories(): output_log_level=logging.INFO, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -2541,6 +2640,7 @@ def test_create_archive_with_archive_name_format_calls_borg_with_archive_name(): output_log_level=logging.INFO, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -2586,6 +2686,7 @@ def test_create_archive_with_archive_name_format_accepts_borg_placeholders(): output_log_level=logging.INFO, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -2631,6 +2732,7 @@ def test_create_archive_with_repository_accepts_borg_placeholders(): output_log_level=logging.INFO, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -2675,6 +2777,7 @@ def test_create_archive_with_extra_borg_options_calls_borg_with_extra_options(): output_log_level=logging.INFO, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -2728,6 +2831,7 @@ def test_create_archive_with_stream_processes_calls_borg_with_processes_and_read output_log_level=logging.INFO, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) @@ -2737,6 +2841,7 @@ def test_create_archive_with_stream_processes_calls_borg_with_processes_and_read output_log_level=logging.INFO, output_file=None, borg_local_path='borg', + borg_exit_codes=None, working_directory=None, extra_environment=None, ) diff --git a/tests/unit/borg/test_environment.py b/tests/unit/borg/test_environment.py index cf426873..224bd7b3 100644 --- a/tests/unit/borg/test_environment.py +++ b/tests/unit/borg/test_environment.py @@ -22,7 +22,8 @@ def test_make_environment_with_ssh_command_should_set_environment(): def test_make_environment_without_configuration_should_not_set_environment(): environment = module.make_environment({}) - assert environment == {} + # borgmatic always sets this Borg environment variable. + assert environment == {'BORG_EXIT_CODES': 'modern'} def test_make_environment_with_relocated_repo_access_true_should_set_environment_yes(): diff --git a/tests/unit/borg/test_export_key.py b/tests/unit/borg/test_export_key.py index e505a57b..157b1b3d 100644 --- a/tests/unit/borg/test_export_key.py +++ b/tests/unit/borg/test_export_key.py @@ -9,7 +9,7 @@ from borgmatic.borg import export_key as module from ..test_verbosity import insert_logging_mock -def insert_execute_command_mock(command, output_file=module.DO_NOT_CAPTURE): +def insert_execute_command_mock(command, output_file=module.DO_NOT_CAPTURE, borg_exit_codes=None): borgmatic.logger.add_custom_log_levels() flexmock(module.environment).should_receive('make_environment') @@ -17,7 +17,8 @@ def insert_execute_command_mock(command, output_file=module.DO_NOT_CAPTURE): command, output_file=output_file, output_log_level=module.logging.ANSWER, - borg_local_path='borg', + borg_local_path=command[0], + borg_exit_codes=borg_exit_codes, extra_environment=None, ).once() @@ -36,6 +37,36 @@ def test_export_key_calls_borg_with_required_flags(): ) +def test_export_key_calls_borg_with_local_path(): + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + flexmock(module.os.path).should_receive('exists').never() + insert_execute_command_mock(('borg1', 'key', 'export', 'repo')) + + module.export_key( + repository_path='repo', + config={}, + local_borg_version='1.2.3', + export_arguments=flexmock(paper=False, qr_html=False, path=None), + global_arguments=flexmock(dry_run=False, log_json=False), + local_path='borg1', + ) + + +def test_export_key_calls_borg_using_exit_codes(): + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + flexmock(module.os.path).should_receive('exists').never() + borg_exit_codes = flexmock() + insert_execute_command_mock(('borg', 'key', 'export', 'repo'), borg_exit_codes=borg_exit_codes) + + module.export_key( + repository_path='repo', + config={'borg_exit_codes': borg_exit_codes}, + local_borg_version='1.2.3', + export_arguments=flexmock(paper=False, qr_html=False, path=None), + global_arguments=flexmock(dry_run=False, log_json=False), + ) + + def test_export_key_calls_borg_with_remote_path_flags(): flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) flexmock(module.os.path).should_receive('exists').never() diff --git a/tests/unit/borg/test_export_tar.py b/tests/unit/borg/test_export_tar.py index 32b0967f..43353eae 100644 --- a/tests/unit/borg/test_export_tar.py +++ b/tests/unit/borg/test_export_tar.py @@ -8,7 +8,11 @@ from ..test_verbosity import insert_logging_mock def insert_execute_command_mock( - command, output_log_level=logging.INFO, borg_local_path='borg', capture=True + command, + output_log_level=logging.INFO, + borg_local_path='borg', + borg_exit_codes=None, + capture=True, ): flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( @@ -16,11 +20,12 @@ def insert_execute_command_mock( output_file=None if capture else module.DO_NOT_CAPTURE, output_log_level=output_log_level, borg_local_path=borg_local_path, + borg_exit_codes=borg_exit_codes, extra_environment=None, ).once() -def test_export_tar_archive_calls_borg_with_path_parameters(): +def test_export_tar_archive_calls_borg_with_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_repository_archive_flags').and_return( @@ -42,7 +47,7 @@ def test_export_tar_archive_calls_borg_with_path_parameters(): ) -def test_export_tar_archive_calls_borg_with_local_path_parameters(): +def test_export_tar_archive_calls_borg_with_local_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_repository_archive_flags').and_return( @@ -65,7 +70,31 @@ def test_export_tar_archive_calls_borg_with_local_path_parameters(): ) -def test_export_tar_archive_calls_borg_with_remote_path_parameters(): +def test_export_tar_archive_calls_borg_using_exit_codes(): + 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',) + ) + borg_exit_codes = flexmock() + insert_execute_command_mock( + ('borg', 'export-tar', 'repo::archive', 'test.tar'), + borg_exit_codes=borg_exit_codes, + ) + + module.export_tar_archive( + dry_run=False, + repository_path='repo', + archive='archive', + paths=None, + destination_path='test.tar', + config={'borg_exit_codes': borg_exit_codes}, + local_borg_version='1.2.3', + global_arguments=flexmock(log_json=False), + ) + + +def test_export_tar_archive_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_repository_archive_flags').and_return( @@ -88,7 +117,7 @@ def test_export_tar_archive_calls_borg_with_remote_path_parameters(): ) -def test_export_tar_archive_calls_borg_with_umask_parameters(): +def test_export_tar_archive_calls_borg_with_umask_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_repository_archive_flags').and_return( @@ -130,7 +159,7 @@ def test_export_tar_archive_calls_borg_with_log_json_parameter(): ) -def test_export_tar_archive_calls_borg_with_lock_wait_parameters(): +def test_export_tar_archive_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_repository_archive_flags').and_return( @@ -173,7 +202,7 @@ 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(): +def test_export_tar_archive_with_log_debug_calls_borg_with_debug_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_repository_archive_flags').and_return( @@ -216,7 +245,7 @@ def test_export_tar_archive_calls_borg_with_dry_run_parameter(): ) -def test_export_tar_archive_calls_borg_with_tar_filter_parameters(): +def test_export_tar_archive_calls_borg_with_tar_filter_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_repository_archive_flags').and_return( diff --git a/tests/unit/borg/test_extract.py b/tests/unit/borg/test_extract.py index a65aac73..19146321 100644 --- a/tests/unit/borg/test_extract.py +++ b/tests/unit/borg/test_extract.py @@ -8,12 +8,14 @@ from borgmatic.borg import extract as module from ..test_verbosity import insert_logging_mock -def insert_execute_command_mock(command, working_directory=None): +def insert_execute_command_mock(command, working_directory=None, borg_exit_codes=None): flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( command, working_directory=working_directory, extra_environment=None, + borg_local_path=command[0], + borg_exit_codes=borg_exit_codes, ).once() @@ -99,6 +101,25 @@ def test_extract_last_archive_dry_run_calls_borg_via_local_path(): ) +def test_extract_last_archive_dry_run_calls_borg_using_exit_codes(): + flexmock(module.rlist).should_receive('resolve_archive_name').and_return('archive') + borg_exit_codes = flexmock() + insert_execute_command_mock( + ('borg', 'extract', '--dry-run', 'repo::archive'), borg_exit_codes=borg_exit_codes + ) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) + + module.extract_last_archive_dry_run( + config={'borg_exit_codes': borg_exit_codes}, + local_borg_version='1.2.3', + global_arguments=flexmock(log_json=False), + repository_path='repo', + lock_wait=None, + ) + + def test_extract_last_archive_dry_run_calls_borg_with_remote_path_flags(): flexmock(module.rlist).should_receive('resolve_archive_name').and_return('archive') insert_execute_command_mock( @@ -174,6 +195,54 @@ def test_extract_archive_calls_borg_with_path_flags(): ) +def test_extract_archive_calls_borg_with_local_path(): + flexmock(module.os.path).should_receive('abspath').and_return('repo') + insert_execute_command_mock(('borg1', 'extract', 'repo::archive')) + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) + flexmock(module.borgmatic.config.validate).should_receive( + 'normalize_repository_path' + ).and_return('repo') + + module.extract_archive( + dry_run=False, + repository='repo', + archive='archive', + paths=None, + config={}, + local_borg_version='1.2.3', + global_arguments=flexmock(log_json=False), + local_path='borg1', + ) + + +def test_extract_archive_calls_borg_with_exit_codes(): + flexmock(module.os.path).should_receive('abspath').and_return('repo') + borg_exit_codes = flexmock() + insert_execute_command_mock( + ('borg', 'extract', 'repo::archive'), borg_exit_codes=borg_exit_codes + ) + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) + flexmock(module.borgmatic.config.validate).should_receive( + 'normalize_repository_path' + ).and_return('repo') + + module.extract_archive( + dry_run=False, + repository='repo', + archive='archive', + paths=None, + config={'borg_exit_codes': borg_exit_codes}, + local_borg_version='1.2.3', + global_arguments=flexmock(log_json=False), + ) + + def test_extract_archive_calls_borg_with_remote_path_flags(): flexmock(module.os.path).should_receive('abspath').and_return('repo') insert_execute_command_mock(('borg', 'extract', '--remote-path', 'borg1', 'repo::archive')) @@ -470,6 +539,8 @@ def test_extract_archive_calls_borg_with_progress_parameter(): output_file=module.DO_NOT_CAPTURE, working_directory=None, extra_environment=None, + borg_local_path='borg', + borg_exit_codes=None, ).once() flexmock(module.feature).should_receive('available').and_return(True) flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( @@ -518,6 +589,8 @@ def test_extract_archive_calls_borg_with_stdout_parameter_and_returns_process(): working_directory=None, run_to_completion=False, extra_environment=None, + borg_local_path='borg', + borg_exit_codes=None, ).and_return(process).once() flexmock(module.feature).should_receive('available').and_return(True) flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( @@ -549,6 +622,8 @@ def test_extract_archive_skips_abspath_for_remote_repository(): ('borg', 'extract', 'server:repo::archive'), working_directory=None, extra_environment=None, + borg_local_path='borg', + borg_exit_codes=None, ).once() flexmock(module.feature).should_receive('available').and_return(True) flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( diff --git a/tests/unit/borg/test_list.py b/tests/unit/borg/test_list.py index 4d1d5346..b753efd9 100644 --- a/tests/unit/borg/test_list.py +++ b/tests/unit/borg/test_list.py @@ -293,7 +293,7 @@ def test_capture_archive_listing_does_not_raise(): module.capture_archive_listing( repository_path='repo', archive='archive', - config=flexmock(), + config={}, local_borg_version=flexmock(), global_arguments=flexmock(log_json=False), ) @@ -332,6 +332,7 @@ def test_list_archive_calls_borg_with_flags(): ('borg', 'list', 'repo::archive'), output_log_level=module.borgmatic.logger.ANSWER, borg_local_path='borg', + borg_exit_codes=None, extra_environment=None, ).once() @@ -395,6 +396,7 @@ def test_list_archive_calls_borg_with_local_path(): ('borg2', 'list', 'repo::archive'), output_log_level=module.borgmatic.logger.ANSWER, borg_local_path='borg2', + borg_exit_codes=None, extra_environment=None, ).once() @@ -408,6 +410,53 @@ def test_list_archive_calls_borg_with_local_path(): ) +def test_list_archive_calls_borg_using_exit_codes(): + 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=False, + find_paths=None, + prefix=None, + match_archives=None, + sort_by=None, + first=None, + last=None, + ) + global_arguments = flexmock(log_json=False) + + flexmock(module.feature).should_receive('available').and_return(False) + borg_exit_codes = flexmock() + flexmock(module).should_receive('make_list_command').with_args( + repository_path='repo', + config={'borg_exit_codes': borg_exit_codes}, + local_borg_version='1.2.3', + list_arguments=list_arguments, + global_arguments=global_arguments, + local_path='borg', + remote_path=None, + ).and_return(('borg', 'list', 'repo::archive')) + flexmock(module).should_receive('make_find_paths').and_return(()) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'list', 'repo::archive'), + output_log_level=module.borgmatic.logger.ANSWER, + borg_local_path='borg', + borg_exit_codes=borg_exit_codes, + extra_environment=None, + ).once() + + module.list_archive( + repository_path='repo', + config={'borg_exit_codes': borg_exit_codes}, + local_borg_version='1.2.3', + list_arguments=list_arguments, + global_arguments=global_arguments, + ) + + 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 @@ -430,6 +479,7 @@ def test_list_archive_calls_borg_multiple_times_with_find_paths(): ('borg', 'list', 'repo'), extra_environment=None, borg_local_path='borg', + borg_exit_codes=None, ).and_return('archive1\narchive2').once() flexmock(module).should_receive('make_list_command').and_return( ('borg', 'list', 'repo::archive1') @@ -440,12 +490,14 @@ def test_list_archive_calls_borg_multiple_times_with_find_paths(): ('borg', 'list', 'repo::archive1') + glob_paths, output_log_level=module.borgmatic.logger.ANSWER, borg_local_path='borg', + borg_exit_codes=None, extra_environment=None, ).once() flexmock(module).should_receive('execute_command').with_args( ('borg', 'list', 'repo::archive2') + glob_paths, output_log_level=module.borgmatic.logger.ANSWER, borg_local_path='borg', + borg_exit_codes=None, extra_environment=None, ).once() @@ -491,6 +543,7 @@ def test_list_archive_calls_borg_with_archive(): ('borg', 'list', 'repo::archive'), output_log_level=module.borgmatic.logger.ANSWER, borg_local_path='borg', + borg_exit_codes=None, extra_environment=None, ).once() @@ -611,6 +664,7 @@ def test_list_archive_with_archive_ignores_archive_filter_flag( ('borg', 'list', 'repo::archive'), output_log_level=module.borgmatic.logger.ANSWER, borg_local_path='borg', + borg_exit_codes=None, extra_environment=None, ).once() @@ -669,6 +723,7 @@ def test_list_archive_with_find_paths_allows_archive_filter_flag_but_only_passes ('borg', 'rlist', '--repo', 'repo'), extra_environment=None, borg_local_path='borg', + borg_exit_codes=None, ).and_return('archive1\narchive2').once() flexmock(module).should_receive('make_list_command').with_args( @@ -715,12 +770,14 @@ def test_list_archive_with_find_paths_allows_archive_filter_flag_but_only_passes ('borg', 'list', '--repo', 'repo', 'archive1') + glob_paths, output_log_level=module.borgmatic.logger.ANSWER, borg_local_path='borg', + borg_exit_codes=None, extra_environment=None, ).once() flexmock(module).should_receive('execute_command').with_args( ('borg', 'list', '--repo', 'repo', 'archive2') + glob_paths, output_log_level=module.borgmatic.logger.ANSWER, borg_local_path='borg', + borg_exit_codes=None, extra_environment=None, ).once() diff --git a/tests/unit/borg/test_mount.py b/tests/unit/borg/test_mount.py index 9ee37f91..afc3b4bd 100644 --- a/tests/unit/borg/test_mount.py +++ b/tests/unit/borg/test_mount.py @@ -7,11 +7,12 @@ from borgmatic.borg import mount as module from ..test_verbosity import insert_logging_mock -def insert_execute_command_mock(command): +def insert_execute_command_mock(command, borg_exit_codes=None): flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( command, - borg_local_path='borg', + borg_local_path=command[0], + borg_exit_codes=borg_exit_codes, extra_environment=None, ).once() @@ -93,6 +94,47 @@ def test_mount_archive_calls_borg_with_path_flags(): ) +def test_mount_archive_calls_borg_with_local_path(): + flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) + insert_execute_command_mock(('borg1', 'mount', 'repo::archive', '/mnt')) + + mount_arguments = flexmock(mount_point='/mnt', options=None, paths=None, foreground=False) + module.mount_archive( + repository_path='repo', + archive='archive', + mount_arguments=mount_arguments, + config={}, + local_borg_version='1.2.3', + global_arguments=flexmock(log_json=False), + local_path='borg1', + ) + + +def test_mount_archive_calls_borg_using_exit_codes(): + flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) + borg_exit_codes = flexmock() + insert_execute_command_mock( + ('borg', 'mount', 'repo::archive', '/mnt'), + borg_exit_codes=borg_exit_codes, + ) + + mount_arguments = flexmock(mount_point='/mnt', options=None, paths=None, foreground=False) + module.mount_archive( + repository_path='repo', + archive='archive', + mount_arguments=mount_arguments, + config={'borg_exit_codes': borg_exit_codes}, + local_borg_version='1.2.3', + global_arguments=flexmock(log_json=False), + ) + + def test_mount_archive_calls_borg_with_remote_path_flags(): flexmock(module.feature).should_receive('available').and_return(False) flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( @@ -216,6 +258,7 @@ def test_mount_archive_calls_borg_with_foreground_parameter(): ('borg', 'mount', '--foreground', 'repo::archive', '/mnt'), output_file=module.DO_NOT_CAPTURE, borg_local_path='borg', + borg_exit_codes=None, extra_environment=None, ).once() @@ -288,6 +331,7 @@ def test_mount_archive_with_date_based_matching_calls_borg_with_date_based_flags '/mnt', ), borg_local_path='borg', + borg_exit_codes=None, extra_environment=None, ) diff --git a/tests/unit/borg/test_prune.py b/tests/unit/borg/test_prune.py index 34a1563a..79a0d983 100644 --- a/tests/unit/borg/test_prune.py +++ b/tests/unit/borg/test_prune.py @@ -7,12 +7,13 @@ from borgmatic.borg import prune as module from ..test_verbosity import insert_logging_mock -def insert_execute_command_mock(prune_command, output_log_level): +def insert_execute_command_mock(prune_command, output_log_level, borg_exit_codes=None): flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( prune_command, output_log_level=output_log_level, borg_local_path=prune_command[0], + borg_exit_codes=borg_exit_codes, extra_environment=None, ).once() @@ -227,6 +228,27 @@ def test_prune_archives_with_local_path_calls_borg_via_local_path(): ) +def test_prune_archives_with_exit_codes_calls_borg_using_them(): + 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',)) + borg_exit_codes = flexmock() + insert_execute_command_mock( + ('borg',) + PRUNE_COMMAND[1:] + ('repo',), logging.INFO, borg_exit_codes + ) + + prune_arguments = flexmock(stats=False, list_archives=False) + module.prune_archives( + dry_run=False, + repository_path='repo', + config={'borg_exit_codes': borg_exit_codes}, + local_borg_version='1.2.3', + global_arguments=flexmock(log_json=False), + prune_arguments=prune_arguments, + ) + + def test_prune_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 @@ -403,6 +425,7 @@ def test_prune_archives_with_date_based_matching_calls_borg_with_date_based_flag ), output_log_level=logging.INFO, borg_local_path='borg', + borg_exit_codes=None, extra_environment=None, ) diff --git a/tests/unit/borg/test_rcreate.py b/tests/unit/borg/test_rcreate.py index be11a829..8927f291 100644 --- a/tests/unit/borg/test_rcreate.py +++ b/tests/unit/borg/test_rcreate.py @@ -22,12 +22,13 @@ def insert_rinfo_command_not_found_mock(): ) -def insert_rcreate_command_mock(rcreate_command, **kwargs): +def insert_rcreate_command_mock(rcreate_command, borg_exit_codes=None, **kwargs): flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( rcreate_command, output_file=module.DO_NOT_CAPTURE, borg_local_path=rcreate_command[0], + borg_exit_codes=borg_exit_codes, extra_environment=None, ).once() @@ -353,6 +354,30 @@ def test_create_repository_with_local_path_calls_borg_via_local_path(): ) +def test_create_repository_with_exit_codes_calls_borg_using_them(): + borg_exit_codes = flexmock() + insert_rinfo_command_not_found_mock() + insert_rcreate_command_mock( + ('borg',) + RCREATE_COMMAND[1:] + ('--repo', 'repo'), borg_exit_codes=borg_exit_codes + ) + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return( + ( + '--repo', + 'repo', + ) + ) + + module.create_repository( + dry_run=False, + repository_path='repo', + config={'borg_exit_codes': borg_exit_codes}, + local_borg_version='2.3.4', + global_arguments=flexmock(log_json=False), + encryption_mode='repokey', + ) + + def test_create_repository_with_remote_path_calls_borg_with_remote_path_flag(): insert_rinfo_command_not_found_mock() insert_rcreate_command_mock(RCREATE_COMMAND + ('--remote-path', 'borg1', '--repo', 'repo')) diff --git a/tests/unit/borg/test_rinfo.py b/tests/unit/borg/test_rinfo.py index 41bfd2eb..0f38535e 100644 --- a/tests/unit/borg/test_rinfo.py +++ b/tests/unit/borg/test_rinfo.py @@ -21,6 +21,7 @@ def test_display_repository_info_calls_borg_with_flags(): flexmock(module).should_receive('execute_command_and_capture_output').with_args( ('borg', 'rinfo', '--json', '--repo', 'repo'), borg_local_path='borg', + borg_exit_codes=None, extra_environment=None, ).and_return('[]') flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags') @@ -28,6 +29,7 @@ def test_display_repository_info_calls_borg_with_flags(): ('borg', 'rinfo', '--repo', 'repo'), output_log_level=module.borgmatic.logger.ANSWER, borg_local_path='borg', + borg_exit_codes=None, extra_environment=None, ) @@ -49,6 +51,7 @@ def test_display_repository_info_without_borg_features_calls_borg_with_info_sub_ flexmock(module).should_receive('execute_command_and_capture_output').with_args( ('borg', 'rinfo', '--json', 'repo'), borg_local_path='borg', + borg_exit_codes=None, extra_environment=None, ).and_return('[]') flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags') @@ -56,6 +59,7 @@ def test_display_repository_info_without_borg_features_calls_borg_with_info_sub_ ('borg', 'info', 'repo'), output_log_level=module.borgmatic.logger.ANSWER, borg_local_path='borg', + borg_exit_codes=None, extra_environment=None, ) @@ -82,6 +86,7 @@ def test_display_repository_info_with_log_info_calls_borg_with_info_flag(): flexmock(module).should_receive('execute_command_and_capture_output').with_args( ('borg', 'rinfo', '--info', '--json', '--repo', 'repo'), borg_local_path='borg', + borg_exit_codes=None, extra_environment=None, ).and_return('[]') flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags') @@ -89,6 +94,7 @@ def test_display_repository_info_with_log_info_calls_borg_with_info_flag(): ('borg', 'rinfo', '--info', '--repo', 'repo'), output_log_level=module.borgmatic.logger.ANSWER, borg_local_path='borg', + borg_exit_codes=None, extra_environment=None, ) insert_logging_mock(logging.INFO) @@ -116,6 +122,7 @@ def test_display_repository_info_with_log_info_and_json_suppresses_most_borg_out ('borg', 'rinfo', '--json', '--repo', 'repo'), extra_environment=None, borg_local_path='borg', + borg_exit_codes=None, ).and_return('[]') flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags').never() @@ -145,6 +152,7 @@ def test_display_repository_info_with_log_debug_calls_borg_with_debug_flag(): flexmock(module).should_receive('execute_command_and_capture_output').with_args( ('borg', 'rinfo', '--debug', '--show-rc', '--json', '--repo', 'repo'), borg_local_path='borg', + borg_exit_codes=None, extra_environment=None, ).and_return('[]') flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags') @@ -152,6 +160,7 @@ def test_display_repository_info_with_log_debug_calls_borg_with_debug_flag(): ('borg', 'rinfo', '--debug', '--show-rc', '--repo', 'repo'), output_log_level=module.borgmatic.logger.ANSWER, borg_local_path='borg', + borg_exit_codes=None, extra_environment=None, ) insert_logging_mock(logging.DEBUG) @@ -180,6 +189,7 @@ def test_display_repository_info_with_log_debug_and_json_suppresses_most_borg_ou ('borg', 'rinfo', '--json', '--repo', 'repo'), extra_environment=None, borg_local_path='borg', + borg_exit_codes=None, ).and_return('[]') flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags').never() @@ -210,6 +220,7 @@ def test_display_repository_info_with_json_calls_borg_with_json_flag(): ('borg', 'rinfo', '--json', '--repo', 'repo'), extra_environment=None, borg_local_path='borg', + borg_exit_codes=None, ).and_return('[]') flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags').never() @@ -239,12 +250,14 @@ def test_display_repository_info_with_local_path_calls_borg_via_local_path(): ('borg1', 'rinfo', '--json', '--repo', 'repo'), extra_environment=None, borg_local_path='borg', + borg_exit_codes=None, ).and_return('[]') flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags') flexmock(module).should_receive('execute_command').with_args( ('borg1', 'rinfo', '--repo', 'repo'), output_log_level=module.borgmatic.logger.ANSWER, borg_local_path='borg1', + borg_exit_codes=None, extra_environment=None, ) @@ -258,6 +271,42 @@ def test_display_repository_info_with_local_path_calls_borg_via_local_path(): ) +def test_display_repository_info_with_exit_codes_calls_borg_using_them(): + 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') + borg_exit_codes = flexmock() + flexmock(module).should_receive('execute_command_and_capture_output').with_args( + ('borg', 'rinfo', '--json', '--repo', 'repo'), + extra_environment=None, + borg_local_path='borg', + borg_exit_codes=borg_exit_codes, + ).and_return('[]') + flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'rinfo', '--repo', 'repo'), + output_log_level=module.borgmatic.logger.ANSWER, + borg_local_path='borg', + borg_exit_codes=borg_exit_codes, + extra_environment=None, + ) + + module.display_repository_info( + repository_path='repo', + config={'borg_exit_codes': borg_exit_codes}, + local_borg_version='2.3.4', + rinfo_arguments=flexmock(json=False), + global_arguments=flexmock(log_json=False), + ) + + def test_display_repository_info_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 @@ -273,12 +322,14 @@ def test_display_repository_info_with_remote_path_calls_borg_with_remote_path_fl ('borg', 'rinfo', '--remote-path', 'borg1', '--json', '--repo', 'repo'), extra_environment=None, borg_local_path='borg', + borg_exit_codes=None, ).and_return('[]') flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags') flexmock(module).should_receive('execute_command').with_args( ('borg', 'rinfo', '--remote-path', 'borg1', '--repo', 'repo'), output_log_level=module.borgmatic.logger.ANSWER, borg_local_path='borg', + borg_exit_codes=None, extra_environment=None, ) @@ -307,12 +358,14 @@ def test_display_repository_info_with_log_json_calls_borg_with_log_json_flags(): ('borg', 'rinfo', '--log-json', '--json', '--repo', 'repo'), extra_environment=None, borg_local_path='borg', + borg_exit_codes=None, ).and_return('[]') flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags') flexmock(module).should_receive('execute_command').with_args( ('borg', 'rinfo', '--log-json', '--repo', 'repo'), output_log_level=module.borgmatic.logger.ANSWER, borg_local_path='borg', + borg_exit_codes=None, extra_environment=None, ) @@ -341,12 +394,14 @@ def test_display_repository_info_with_lock_wait_calls_borg_with_lock_wait_flags( ('borg', 'rinfo', '--lock-wait', '5', '--json', '--repo', 'repo'), extra_environment=None, borg_local_path='borg', + borg_exit_codes=None, ).and_return('[]') flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags') flexmock(module).should_receive('execute_command').with_args( ('borg', 'rinfo', '--lock-wait', '5', '--repo', 'repo'), output_log_level=module.borgmatic.logger.ANSWER, borg_local_path='borg', + borg_exit_codes=None, extra_environment=None, ) diff --git a/tests/unit/borg/test_rlist.py b/tests/unit/borg/test_rlist.py index 0cd7736f..e2c49f32 100644 --- a/tests/unit/borg/test_rlist.py +++ b/tests/unit/borg/test_rlist.py @@ -38,6 +38,7 @@ def test_resolve_archive_name_calls_borg_with_flags(): ('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS, extra_environment=None, borg_local_path='borg', + borg_exit_codes=None, ).and_return(expected_archive + '\n') assert ( @@ -59,6 +60,7 @@ def test_resolve_archive_name_with_log_info_calls_borg_without_info_flag(): ('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS, extra_environment=None, borg_local_path='borg', + borg_exit_codes=None, ).and_return(expected_archive + '\n') insert_logging_mock(logging.INFO) @@ -81,6 +83,7 @@ def test_resolve_archive_name_with_log_debug_calls_borg_without_debug_flag(): ('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS, extra_environment=None, borg_local_path='borg', + borg_exit_codes=None, ).and_return(expected_archive + '\n') insert_logging_mock(logging.DEBUG) @@ -103,6 +106,7 @@ def test_resolve_archive_name_with_local_path_calls_borg_via_local_path(): ('borg1', 'list') + BORG_LIST_LATEST_ARGUMENTS, extra_environment=None, borg_local_path='borg1', + borg_exit_codes=None, ).and_return(expected_archive + '\n') assert ( @@ -118,6 +122,29 @@ def test_resolve_archive_name_with_local_path_calls_borg_via_local_path(): ) +def test_resolve_archive_name_with_exit_codes_calls_borg_using_them(): + expected_archive = 'archive-name' + flexmock(module.environment).should_receive('make_environment') + borg_exit_codes = flexmock() + flexmock(module).should_receive('execute_command_and_capture_output').with_args( + ('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS, + extra_environment=None, + borg_local_path='borg', + borg_exit_codes=borg_exit_codes, + ).and_return(expected_archive + '\n') + + assert ( + module.resolve_archive_name( + 'repo', + 'latest', + config={'borg_exit_codes': borg_exit_codes}, + local_borg_version='1.2.3', + global_arguments=flexmock(log_json=False), + ) + == expected_archive + ) + + def test_resolve_archive_name_with_remote_path_calls_borg_with_remote_path_flags(): expected_archive = 'archive-name' flexmock(module.environment).should_receive('make_environment') @@ -125,6 +152,7 @@ def test_resolve_archive_name_with_remote_path_calls_borg_with_remote_path_flags ('borg', 'list', '--remote-path', 'borg1') + BORG_LIST_LATEST_ARGUMENTS, extra_environment=None, borg_local_path='borg', + borg_exit_codes=None, ).and_return(expected_archive + '\n') assert ( @@ -146,6 +174,7 @@ def test_resolve_archive_name_without_archives_raises(): ('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS, extra_environment=None, borg_local_path='borg', + borg_exit_codes=None, ).and_return('') with pytest.raises(ValueError): @@ -166,6 +195,7 @@ def test_resolve_archive_name_with_log_json_calls_borg_with_log_json_flags(): ('borg', 'list', '--log-json') + BORG_LIST_LATEST_ARGUMENTS, extra_environment=None, borg_local_path='borg', + borg_exit_codes=None, ).and_return(expected_archive + '\n') assert ( @@ -188,6 +218,7 @@ def test_resolve_archive_name_with_lock_wait_calls_borg_with_lock_wait_flags(): ('borg', 'list', '--lock-wait', 'okay') + BORG_LIST_LATEST_ARGUMENTS, extra_environment=None, borg_local_path='borg', + borg_exit_codes=None, ).and_return(expected_archive + '\n') assert ( diff --git a/tests/unit/borg/test_transfer.py b/tests/unit/borg/test_transfer.py index 3e1aa804..4ee28c43 100644 --- a/tests/unit/borg/test_transfer.py +++ b/tests/unit/borg/test_transfer.py @@ -21,6 +21,7 @@ def test_transfer_archives_calls_borg_with_flags(): output_log_level=module.borgmatic.logger.ANSWER, output_file=None, borg_local_path='borg', + borg_exit_codes=None, extra_environment=None, ) @@ -52,6 +53,7 @@ def test_transfer_archives_with_dry_run_calls_borg_with_dry_run_flag(): output_log_level=module.borgmatic.logger.ANSWER, output_file=None, borg_local_path='borg', + borg_exit_codes=None, extra_environment=None, ) @@ -80,6 +82,7 @@ def test_transfer_archives_with_log_info_calls_borg_with_info_flag(): output_log_level=module.borgmatic.logger.ANSWER, output_file=None, borg_local_path='borg', + borg_exit_codes=None, extra_environment=None, ) insert_logging_mock(logging.INFO) @@ -108,6 +111,7 @@ def test_transfer_archives_with_log_debug_calls_borg_with_debug_flag(): output_log_level=module.borgmatic.logger.ANSWER, output_file=None, borg_local_path='borg', + borg_exit_codes=None, extra_environment=None, ) insert_logging_mock(logging.DEBUG) @@ -139,6 +143,7 @@ def test_transfer_archives_with_archive_calls_borg_with_match_archives_flag(): output_log_level=module.borgmatic.logger.ANSWER, output_file=None, borg_local_path='borg', + borg_exit_codes=None, extra_environment=None, ) @@ -169,6 +174,7 @@ def test_transfer_archives_with_match_archives_calls_borg_with_match_archives_fl output_log_level=module.borgmatic.logger.ANSWER, output_file=None, borg_local_path='borg', + borg_exit_codes=None, extra_environment=None, ) @@ -199,6 +205,7 @@ def test_transfer_archives_with_archive_name_format_calls_borg_with_match_archiv output_log_level=module.borgmatic.logger.ANSWER, output_file=None, borg_local_path='borg', + borg_exit_codes=None, extra_environment=None, ) @@ -227,6 +234,7 @@ def test_transfer_archives_with_local_path_calls_borg_via_local_path(): output_log_level=module.borgmatic.logger.ANSWER, output_file=None, borg_local_path='borg2', + borg_exit_codes=None, extra_environment=None, ) @@ -243,6 +251,36 @@ def test_transfer_archives_with_local_path_calls_borg_via_local_path(): ) +def test_transfer_archives_with_exit_codes_calls_borg_using_them(): + 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_match_archives_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') + borg_exit_codes = flexmock() + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'transfer', '--repo', 'repo'), + output_log_level=module.borgmatic.logger.ANSWER, + output_file=None, + borg_local_path='borg', + borg_exit_codes=borg_exit_codes, + extra_environment=None, + ) + + module.transfer_archives( + dry_run=False, + repository_path='repo', + config={'borg_exit_codes': borg_exit_codes}, + local_borg_version='2.3.4', + transfer_arguments=flexmock( + archive=None, progress=None, match_archives=None, source_repository=None + ), + global_arguments=flexmock(log_json=False), + ) + + 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 @@ -259,6 +297,7 @@ def test_transfer_archives_with_remote_path_calls_borg_with_remote_path_flags(): output_log_level=module.borgmatic.logger.ANSWER, output_file=None, borg_local_path='borg', + borg_exit_codes=None, extra_environment=None, ) @@ -291,6 +330,7 @@ def test_transfer_archives_with_log_json_calls_borg_with_log_json_flags(): output_log_level=module.borgmatic.logger.ANSWER, output_file=None, borg_local_path='borg', + borg_exit_codes=None, extra_environment=None, ) @@ -323,6 +363,7 @@ def test_transfer_archives_with_lock_wait_calls_borg_with_lock_wait_flags(): output_log_level=module.borgmatic.logger.ANSWER, output_file=None, borg_local_path='borg', + borg_exit_codes=None, extra_environment=None, ) @@ -351,6 +392,7 @@ def test_transfer_archives_with_progress_calls_borg_with_progress_flag(): output_log_level=module.borgmatic.logger.ANSWER, output_file=module.DO_NOT_CAPTURE, borg_local_path='borg', + borg_exit_codes=None, extra_environment=None, ) @@ -383,6 +425,7 @@ def test_transfer_archives_passes_through_arguments_to_borg(argument_name): output_log_level=module.borgmatic.logger.ANSWER, output_file=None, borg_local_path='borg', + borg_exit_codes=None, extra_environment=None, ) @@ -417,6 +460,7 @@ def test_transfer_archives_with_source_repository_calls_borg_with_other_repo_fla output_log_level=module.borgmatic.logger.ANSWER, output_file=None, borg_local_path='borg', + borg_exit_codes=None, extra_environment=None, ) @@ -459,6 +503,7 @@ def test_transfer_archives_with_date_based_matching_calls_borg_with_date_based_f output_log_level=module.borgmatic.logger.ANSWER, output_file=None, borg_local_path='borg', + borg_exit_codes=None, extra_environment=None, ) diff --git a/tests/unit/borg/test_umount.py b/tests/unit/borg/test_umount.py index faa05592..2fba0b6b 100644 --- a/tests/unit/borg/test_umount.py +++ b/tests/unit/borg/test_umount.py @@ -7,25 +7,40 @@ from borgmatic.borg import umount as module from ..test_verbosity import insert_logging_mock -def insert_execute_command_mock(command): - flexmock(module).should_receive('execute_command').with_args(command).once() +def insert_execute_command_mock(command, borg_local_path='borg', borg_exit_codes=None): + flexmock(module).should_receive('execute_command').with_args( + command, borg_local_path=borg_local_path, borg_exit_codes=borg_exit_codes + ).once() def test_unmount_archive_calls_borg_with_required_parameters(): insert_execute_command_mock(('borg', 'umount', '/mnt')) - module.unmount_archive(mount_point='/mnt') + module.unmount_archive(config={}, mount_point='/mnt') def test_unmount_archive_with_log_info_calls_borg_with_info_parameter(): insert_execute_command_mock(('borg', 'umount', '--info', '/mnt')) insert_logging_mock(logging.INFO) - module.unmount_archive(mount_point='/mnt') + module.unmount_archive(config={}, mount_point='/mnt') def test_unmount_archive_with_log_debug_calls_borg_with_debug_parameters(): insert_execute_command_mock(('borg', 'umount', '--debug', '--show-rc', '/mnt')) insert_logging_mock(logging.DEBUG) - module.unmount_archive(mount_point='/mnt') + module.unmount_archive(config={}, mount_point='/mnt') + + +def test_unmount_archive_calls_borg_with_local_path(): + insert_execute_command_mock(('borg1', 'umount', '/mnt'), borg_local_path='borg1') + + module.unmount_archive(config={}, mount_point='/mnt', local_path='borg1') + + +def test_unmount_archive_calls_borg_with_exit_codes(): + borg_exit_codes = flexmock() + insert_execute_command_mock(('borg', 'umount', '/mnt'), borg_exit_codes=borg_exit_codes) + + module.unmount_archive(config={'borg_exit_codes': borg_exit_codes}, mount_point='/mnt') diff --git a/tests/unit/borg/test_version.py b/tests/unit/borg/test_version.py index a00235a5..cd575e38 100644 --- a/tests/unit/borg/test_version.py +++ b/tests/unit/borg/test_version.py @@ -11,13 +11,14 @@ VERSION = '1.2.3' def insert_execute_command_and_capture_output_mock( - command, borg_local_path='borg', version_output=f'borg {VERSION}' + command, borg_local_path='borg', borg_exit_codes=None, version_output=f'borg {VERSION}' ): flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command_and_capture_output').with_args( command, extra_environment=None, borg_local_path=borg_local_path, + borg_exit_codes=borg_exit_codes, ).once().and_return(version_output) @@ -51,6 +52,16 @@ def test_local_borg_version_with_local_borg_path_calls_borg_with_it(): assert module.local_borg_version({}, 'borg1') == VERSION +def test_local_borg_version_with_borg_exit_codes_calls_using_with_them(): + borg_exit_codes = flexmock() + insert_execute_command_and_capture_output_mock( + ('borg', '--version'), borg_exit_codes=borg_exit_codes + ) + flexmock(module.environment).should_receive('make_environment') + + assert module.local_borg_version({'borg_exit_codes': borg_exit_codes}) == VERSION + + def test_local_borg_version_with_invalid_version_raises(): insert_execute_command_and_capture_output_mock(('borg', '--version'), version_output='wtf') flexmock(module.environment).should_receive('make_environment') diff --git a/tests/unit/test_execute.py b/tests/unit/test_execute.py index 1971cce0..c1499b89 100644 --- a/tests/unit/test_execute.py +++ b/tests/unit/test_execute.py @@ -7,32 +7,49 @@ from borgmatic import execute as module @pytest.mark.parametrize( - 'command,exit_code,borg_local_path,expected_result', + 'command,exit_code,borg_local_path,borg_exit_codes,expected_result', ( - (['grep'], 2, None, True), - (['grep'], 2, 'borg', True), - (['borg'], 2, 'borg', True), - (['borg1'], 2, 'borg1', True), - (['grep'], 1, None, True), - (['grep'], 1, 'borg', True), - (['borg'], 1, 'borg', False), - (['borg1'], 1, 'borg1', False), - (['grep'], 0, None, False), - (['grep'], 0, 'borg', False), - (['borg'], 0, 'borg', False), - (['borg1'], 0, 'borg1', False), + (['grep'], 2, None, None, module.Exit_status.ERROR), + (['grep'], 2, 'borg', None, module.Exit_status.ERROR), + (['borg'], 2, 'borg', None, module.Exit_status.ERROR), + (['borg1'], 2, 'borg1', None, module.Exit_status.ERROR), + (['grep'], 1, None, None, module.Exit_status.ERROR), + (['grep'], 1, 'borg', None, module.Exit_status.ERROR), + (['borg'], 1, 'borg', None, module.Exit_status.WARNING), + (['borg1'], 1, 'borg1', None, module.Exit_status.WARNING), + (['grep'], 100, None, None, module.Exit_status.ERROR), + (['grep'], 100, 'borg', None, module.Exit_status.ERROR), + (['borg'], 100, 'borg', None, module.Exit_status.WARNING), + (['borg1'], 100, 'borg1', None, module.Exit_status.WARNING), + (['grep'], 0, None, None, module.Exit_status.SUCCESS), + (['grep'], 0, 'borg', None, module.Exit_status.SUCCESS), + (['borg'], 0, 'borg', None, module.Exit_status.SUCCESS), + (['borg1'], 0, 'borg1', None, module.Exit_status.SUCCESS), # -9 exit code occurs when child process get SIGKILLed. - (['grep'], -9, None, True), - (['grep'], -9, 'borg', True), - (['borg'], -9, 'borg', True), - (['borg1'], -9, 'borg1', True), - (['borg'], None, None, False), + (['grep'], -9, None, None, module.Exit_status.ERROR), + (['grep'], -9, 'borg', None, module.Exit_status.ERROR), + (['borg'], -9, 'borg', None, module.Exit_status.ERROR), + (['borg1'], -9, 'borg1', None, module.Exit_status.ERROR), + (['borg'], None, None, None, module.Exit_status.STILL_RUNNING), + (['borg'], 1, 'borg', [], module.Exit_status.WARNING), + (['borg'], 1, 'borg', [{}], module.Exit_status.WARNING), + (['borg'], 1, 'borg', [{'code': 1}], module.Exit_status.WARNING), + (['grep'], 1, 'borg', [{'code': 100, 'treat_as': 'error'}], module.Exit_status.ERROR), + (['borg'], 1, 'borg', [{'code': 100, 'treat_as': 'error'}], module.Exit_status.WARNING), + (['borg'], 1, 'borg', [{'code': 1, 'treat_as': 'error'}], module.Exit_status.ERROR), + (['borg'], 2, 'borg', [{'code': 99, 'treat_as': 'warning'}], module.Exit_status.ERROR), + (['borg'], 2, 'borg', [{'code': 2, 'treat_as': 'warning'}], module.Exit_status.WARNING), + (['borg'], 100, 'borg', [{'code': 1, 'treat_as': 'error'}], module.Exit_status.WARNING), + (['borg'], 100, 'borg', [{'code': 100, 'treat_as': 'error'}], module.Exit_status.ERROR), ), ) -def test_exit_code_indicates_error_respects_exit_code_and_borg_local_path( - command, exit_code, borg_local_path, expected_result +def test_interpret_exit_code_respects_exit_code_and_borg_local_path( + command, exit_code, borg_local_path, borg_exit_codes, expected_result ): - assert module.exit_code_indicates_error(command, exit_code, borg_local_path) is expected_result + assert ( + module.interpret_exit_code(command, exit_code, borg_local_path, borg_exit_codes) + is expected_result + ) def test_command_for_process_converts_sequence_command_to_string(): @@ -178,7 +195,7 @@ def test_execute_command_calls_full_command_without_capturing_output(): flexmock(module.subprocess).should_receive('Popen').with_args( full_command, stdin=None, stdout=None, stderr=None, shell=False, env=None, cwd=None ).and_return(flexmock(wait=lambda: 0)).once() - flexmock(module).should_receive('exit_code_indicates_error').and_return(False) + flexmock(module).should_receive('interpret_exit_code').and_return(module.Exit_status.SUCCESS) flexmock(module).should_receive('log_outputs') output = module.execute_command(full_command, output_file=module.DO_NOT_CAPTURE) @@ -323,7 +340,9 @@ def test_execute_command_and_capture_output_returns_output_when_process_error_is flexmock(module.subprocess).should_receive('check_output').with_args( full_command, stderr=None, shell=False, env=None, cwd=None ).and_raise(subprocess.CalledProcessError(1, full_command, err_output)).once() - flexmock(module).should_receive('exit_code_indicates_error').and_return(False).once() + flexmock(module).should_receive('interpret_exit_code').and_return( + module.Exit_status.SUCCESS + ).once() output = module.execute_command_and_capture_output(full_command) @@ -338,7 +357,9 @@ def test_execute_command_and_capture_output_raises_when_command_errors(): flexmock(module.subprocess).should_receive('check_output').with_args( full_command, stderr=None, shell=False, env=None, cwd=None ).and_raise(subprocess.CalledProcessError(2, full_command, expected_output)).once() - flexmock(module).should_receive('exit_code_indicates_error').and_return(True).once() + flexmock(module).should_receive('interpret_exit_code').and_return( + module.Exit_status.ERROR + ).once() with pytest.raises(subprocess.CalledProcessError): module.execute_command_and_capture_output(full_command) @@ -467,7 +488,7 @@ def test_execute_command_with_processes_calls_full_command_without_capturing_out flexmock(module.subprocess).should_receive('Popen').with_args( full_command, stdin=None, stdout=None, stderr=None, shell=False, env=None, cwd=None ).and_return(flexmock(wait=lambda: 0)).once() - flexmock(module).should_receive('exit_code_indicates_error').and_return(False) + flexmock(module).should_receive('interpret_exit_code').and_return(module.Exit_status.SUCCESS) flexmock(module).should_receive('log_outputs') output = module.execute_command_with_processes(