diff --git a/borgmatic/commands/arguments.py b/borgmatic/commands/arguments.py new file mode 100644 index 000000000..699ac6d73 --- /dev/null +++ b/borgmatic/commands/arguments.py @@ -0,0 +1,292 @@ +import collections +from argparse import ArgumentParser + +from borgmatic.config import collect + +SUBPARSER_ALIASES = { + 'init': ['--init', '-I'], + 'prune': ['--prune', '-p'], + 'create': ['--create', '-C'], + 'check': ['--check', '-k'], + 'extract': ['--extract', '-x'], + 'list': ['--list', '-l'], + 'info': ['--info', '-i'], +} + + +def parse_subparser_arguments(unparsed_arguments, top_level_parser, subparsers): + ''' + Given a sequence of arguments, a top-level parser (containing subparsers), and a subparsers + object as returned by argparse.ArgumentParser().add_subparsers(), ask each subparser to parse + its own arguments and the top-level parser to parse any remaining arguments. + + Return the result as a dict mapping from subparser name (or "global") to a parsed namespace of + arguments. + ''' + arguments = collections.OrderedDict() + remaining_arguments = list(unparsed_arguments) + alias_to_subparser_name = { + alias: subparser_name + for subparser_name, aliases in SUBPARSER_ALIASES.items() + for alias in aliases + } + + # Give each requested action's subparser a shot at parsing all arguments. + for subparser_name, subparser in subparsers.choices.items(): + if subparser_name not in unparsed_arguments: + continue + + remaining_arguments.remove(subparser_name) + canonical_name = alias_to_subparser_name.get(subparser_name, subparser_name) + + parsed, remaining = subparser.parse_known_args(unparsed_arguments) + arguments[canonical_name] = parsed + + # If no actions are explicitly requested, assume defaults: prune, create, and check. + if not arguments and '--help' not in unparsed_arguments and '-h' not in unparsed_arguments: + for subparser_name in ('prune', 'create', 'check'): + subparser = subparsers.choices[subparser_name] + parsed, remaining = subparser.parse_known_args(unparsed_arguments) + arguments[subparser_name] = parsed + + # Then ask each subparser, one by one, to greedily consume arguments. Any arguments that remain + # are global arguments. + for subparser_name in arguments.keys(): + subparser = subparsers.choices[subparser_name] + parsed, remaining_arguments = subparser.parse_known_args(remaining_arguments) + + arguments['global'] = top_level_parser.parse_args(remaining_arguments) + + return arguments + + +def parse_arguments(*unparsed_arguments): + ''' + Given command-line arguments with which this script was invoked, parse the arguments and return + them as a dict mapping from subparser name (or "global") to an argparse.Namespace instance. + ''' + config_paths = collect.get_default_config_paths() + + global_parser = ArgumentParser(add_help=False) + global_group = global_parser.add_argument_group('global arguments') + + global_group.add_argument( + '-c', + '--config', + nargs='*', + dest='config_paths', + default=config_paths, + help='Configuration filenames or directories, defaults to: {}'.format( + ' '.join(config_paths) + ), + ) + global_group.add_argument( + '--excludes', + dest='excludes_filename', + help='Deprecated in favor of exclude_patterns within configuration', + ) + global_group.add_argument( + '-n', + '--dry-run', + dest='dry_run', + action='store_true', + help='Go through the motions, but do not actually write to any repositories', + ) + global_group.add_argument( + '-nc', '--no-color', dest='no_color', action='store_true', help='Disable colored output' + ) + global_group.add_argument( + '-v', + '--verbosity', + type=int, + choices=range(0, 3), + default=0, + help='Display verbose progress to the console (from none to lots: 0, 1, or 2)', + ) + global_group.add_argument( + '--syslog-verbosity', + type=int, + choices=range(0, 3), + default=0, + help='Display verbose progress to syslog (from none to lots: 0, 1, or 2)', + ) + global_group.add_argument( + '--version', + dest='version', + default=False, + action='store_true', + help='Display installed version number of borgmatic and exit', + ) + + top_level_parser = ArgumentParser( + description=''' + A simple wrapper script for the Borg backup software that creates and prunes backups. + If none of the action options are given, then borgmatic defaults to: prune, create, and + check archives. + ''', + parents=[global_parser], + ) + + subparsers = top_level_parser.add_subparsers(title='actions', metavar='') + init_parser = subparsers.add_parser( + 'init', + aliases=SUBPARSER_ALIASES['init'], + help='Initialize an empty Borg repository', + description='Initialize an empty Borg repository', + add_help=False, + ) + init_group = init_parser.add_argument_group('init arguments') + init_group.add_argument( + '-e', + '--encryption', + dest='encryption_mode', + help='Borg repository encryption mode', + required=True, + ) + init_group.add_argument( + '--append-only', + dest='append_only', + action='store_true', + help='Create an append-only repository', + ) + init_group.add_argument( + '--storage-quota', + dest='storage_quota', + help='Create a repository with a fixed storage quota', + ) + init_group.add_argument('-h', '--help', action='help', help='Show this help message and exit') + + prune_parser = subparsers.add_parser( + 'prune', + aliases=SUBPARSER_ALIASES['prune'], + help='Prune archives according to the retention policy', + description='Prune archives according to the retention policy', + add_help=False, + ) + prune_group = prune_parser.add_argument_group('prune arguments') + prune_group.add_argument( + '--stats', + dest='stats', + default=False, + action='store_true', + help='Display statistics of archive', + ) + prune_group.add_argument('-h', '--help', action='help', help='Show this help message and exit') + + create_parser = subparsers.add_parser( + 'create', + aliases=SUBPARSER_ALIASES['create'], + help='Create archives (actually perform backups)', + description='Create archives (actually perform backups)', + add_help=False, + ) + create_group = create_parser.add_argument_group('create arguments') + create_group.add_argument( + '--progress', + dest='progress', + default=False, + action='store_true', + help='Display progress for each file as it is processed', + ) + create_group.add_argument( + '--stats', + dest='stats', + default=False, + action='store_true', + help='Display statistics of archive', + ) + create_group.add_argument( + '--json', dest='json', default=False, action='store_true', help='Output results as JSON' + ) + create_group.add_argument('-h', '--help', action='help', help='Show this help message and exit') + + check_parser = subparsers.add_parser( + 'check', + aliases=SUBPARSER_ALIASES['check'], + help='Check archives for consistency', + description='Check archives for consistency', + add_help=False, + ) + check_group = check_parser.add_argument_group('check arguments') + check_group.add_argument('-h', '--help', action='help', help='Show this help message and exit') + + extract_parser = subparsers.add_parser( + 'extract', + aliases=SUBPARSER_ALIASES['extract'], + help='Extract a named archive to the current directory', + description='Extract a named archive to the current directory', + add_help=False, + ) + extract_group = extract_parser.add_argument_group('extract arguments') + extract_group.add_argument( + '--repository', + help='Path of repository to use, defaults to the configured repository if there is only one', + ) + extract_group.add_argument('--archive', help='Name of archive to operate on', required=True) + extract_group.add_argument( + '--restore-path', + nargs='+', + dest='restore_paths', + help='Paths to restore from archive, defaults to the entire archive', + ) + extract_group.add_argument( + '--progress', + dest='progress', + default=False, + action='store_true', + help='Display progress for each file as it is processed', + ) + extract_group.add_argument( + '-h', '--help', action='help', help='Show this help message and exit' + ) + + list_parser = subparsers.add_parser( + 'list', + aliases=SUBPARSER_ALIASES['list'], + help='List archives', + description='List archives', + add_help=False, + ) + list_group = list_parser.add_argument_group('list arguments') + list_group.add_argument( + '--repository', + help='Path of repository to use, defaults to the configured repository if there is only one', + ) + list_group.add_argument('--archive', help='Name of archive to operate on') + list_group.add_argument( + '--json', dest='json', default=False, action='store_true', help='Output results as JSON' + ) + list_group.add_argument('-h', '--help', action='help', help='Show this help message and exit') + + info_parser = subparsers.add_parser( + 'info', + aliases=SUBPARSER_ALIASES['info'], + help='Display summary information on archives', + description='Display summary information on archives', + add_help=False, + ) + info_group = info_parser.add_argument_group('info arguments') + info_group.add_argument( + '--json', dest='json', default=False, action='store_true', help='Output results as JSON' + ) + info_group.add_argument('-h', '--help', action='help', help='Show this help message and exit') + + arguments = parse_subparser_arguments(unparsed_arguments, top_level_parser, subparsers) + + if arguments['global'].excludes_filename: + raise ValueError( + 'The --excludes option has been replaced with exclude_patterns in configuration' + ) + + if 'init' in arguments and arguments['global'].dry_run: + raise ValueError('The init action cannot be used with the --dry-run option') + + if ( + 'list' in arguments + and 'info' in arguments + and arguments['list'].json + and arguments['info'].json + ): + raise ValueError('With the --json option, list and info actions cannot be used together') + + return arguments diff --git a/borgmatic/commands/borgmatic.py b/borgmatic/commands/borgmatic.py index 42a2972ba..9b87ed197 100644 --- a/borgmatic/commands/borgmatic.py +++ b/borgmatic/commands/borgmatic.py @@ -3,7 +3,6 @@ import json import logging import os import sys -from argparse import ArgumentParser from subprocess import CalledProcessError import colorama @@ -18,6 +17,7 @@ from borgmatic.borg import info as borg_info from borgmatic.borg import init as borg_init from borgmatic.borg import list as borg_list from borgmatic.borg import prune as borg_prune +from borgmatic.commands.arguments import parse_arguments from borgmatic.config import checks, collect, convert, validate from borgmatic.logger import configure_logging, should_do_markup from borgmatic.signals import configure_signals @@ -26,293 +26,6 @@ from borgmatic.verbosity import verbosity_to_log_level logger = logging.getLogger(__name__) LEGACY_CONFIG_PATH = '/etc/borgmatic/config' -SUBPARSER_ALIASES = { - 'init': ['--init', '-I'], - 'prune': ['--prune', '-p'], - 'create': ['--create', '-C'], - 'check': ['--check', '-k'], - 'extract': ['--extract', '-x'], - 'list': ['--list', '-l'], - 'info': ['--info', '-i'], -} - - -def parse_subparser_arguments(unparsed_arguments, top_level_parser, subparsers): - ''' - Given a sequence of arguments, a top-level parser (containing subparsers), and a subparsers - object as returned by argparse.ArgumentParser().add_subparsers(), ask each subparser to parse - its own arguments and the top-level parser to parse any remaining arguments. - - Return the result as a dict mapping from subparser name (or "global") to a parsed namespace of - arguments. - ''' - arguments = collections.OrderedDict() - remaining_arguments = list(unparsed_arguments) - alias_to_subparser_name = { - alias: subparser_name - for subparser_name, aliases in SUBPARSER_ALIASES.items() - for alias in aliases - } - - # Give each requested action's subparser a shot at parsing all arguments. - for subparser_name, subparser in subparsers.choices.items(): - if subparser_name not in unparsed_arguments: - continue - - remaining_arguments.remove(subparser_name) - canonical_name = alias_to_subparser_name.get(subparser_name, subparser_name) - - parsed, remaining = subparser.parse_known_args(unparsed_arguments) - arguments[canonical_name] = parsed - - # If no actions are explicitly requested, assume defaults: prune, create, and check. - if not arguments and '--help' not in unparsed_arguments and '-h' not in unparsed_arguments: - for subparser_name in ('prune', 'create', 'check'): - subparser = subparsers.choices[subparser_name] - parsed, remaining = subparser.parse_known_args(unparsed_arguments) - arguments[subparser_name] = parsed - - # Then ask each subparser, one by one, to greedily consume arguments. Any arguments that remain - # are global arguments. - for subparser_name in arguments.keys(): - subparser = subparsers.choices[subparser_name] - parsed, remaining_arguments = subparser.parse_known_args(remaining_arguments) - - arguments['global'] = top_level_parser.parse_args(remaining_arguments) - - return arguments - - -def parse_arguments(*unparsed_arguments): - ''' - Given command-line arguments with which this script was invoked, parse the arguments and return - them as a dict mapping from subparser name (or "global") to an argparse.Namespace instance. - ''' - config_paths = collect.get_default_config_paths() - - global_parser = ArgumentParser(add_help=False) - global_group = global_parser.add_argument_group('global arguments') - - global_group.add_argument( - '-c', - '--config', - nargs='*', - dest='config_paths', - default=config_paths, - help='Configuration filenames or directories, defaults to: {}'.format( - ' '.join(config_paths) - ), - ) - global_group.add_argument( - '--excludes', - dest='excludes_filename', - help='Deprecated in favor of exclude_patterns within configuration', - ) - global_group.add_argument( - '-n', - '--dry-run', - dest='dry_run', - action='store_true', - help='Go through the motions, but do not actually write to any repositories', - ) - global_group.add_argument( - '-nc', '--no-color', dest='no_color', action='store_true', help='Disable colored output' - ) - global_group.add_argument( - '-v', - '--verbosity', - type=int, - choices=range(0, 3), - default=0, - help='Display verbose progress to the console (from none to lots: 0, 1, or 2)', - ) - global_group.add_argument( - '--syslog-verbosity', - type=int, - choices=range(0, 3), - default=0, - help='Display verbose progress to syslog (from none to lots: 0, 1, or 2)', - ) - global_group.add_argument( - '--version', - dest='version', - default=False, - action='store_true', - help='Display installed version number of borgmatic and exit', - ) - - top_level_parser = ArgumentParser( - description=''' - A simple wrapper script for the Borg backup software that creates and prunes backups. - If none of the action options are given, then borgmatic defaults to: prune, create, and - check archives. - ''', - parents=[global_parser], - ) - - subparsers = top_level_parser.add_subparsers(title='actions', metavar='') - init_parser = subparsers.add_parser( - 'init', - aliases=SUBPARSER_ALIASES['init'], - help='Initialize an empty Borg repository', - description='Initialize an empty Borg repository', - add_help=False, - ) - init_group = init_parser.add_argument_group('init arguments') - init_group.add_argument( - '-e', - '--encryption', - dest='encryption_mode', - help='Borg repository encryption mode', - required=True, - ) - init_group.add_argument( - '--append-only', - dest='append_only', - action='store_true', - help='Create an append-only repository', - ) - init_group.add_argument( - '--storage-quota', - dest='storage_quota', - help='Create a repository with a fixed storage quota', - ) - init_group.add_argument('-h', '--help', action='help', help='Show this help message and exit') - - prune_parser = subparsers.add_parser( - 'prune', - aliases=SUBPARSER_ALIASES['prune'], - help='Prune archives according to the retention policy', - description='Prune archives according to the retention policy', - add_help=False, - ) - prune_group = prune_parser.add_argument_group('prune arguments') - prune_group.add_argument( - '--stats', - dest='stats', - default=False, - action='store_true', - help='Display statistics of archive', - ) - prune_group.add_argument('-h', '--help', action='help', help='Show this help message and exit') - - create_parser = subparsers.add_parser( - 'create', - aliases=SUBPARSER_ALIASES['create'], - help='Create archives (actually perform backups)', - description='Create archives (actually perform backups)', - add_help=False, - ) - create_group = create_parser.add_argument_group('create arguments') - create_group.add_argument( - '--progress', - dest='progress', - default=False, - action='store_true', - help='Display progress for each file as it is processed', - ) - create_group.add_argument( - '--stats', - dest='stats', - default=False, - action='store_true', - help='Display statistics of archive', - ) - create_group.add_argument( - '--json', dest='json', default=False, action='store_true', help='Output results as JSON' - ) - create_group.add_argument('-h', '--help', action='help', help='Show this help message and exit') - - check_parser = subparsers.add_parser( - 'check', - aliases=SUBPARSER_ALIASES['check'], - help='Check archives for consistency', - description='Check archives for consistency', - add_help=False, - ) - check_group = check_parser.add_argument_group('check arguments') - check_group.add_argument('-h', '--help', action='help', help='Show this help message and exit') - - extract_parser = subparsers.add_parser( - 'extract', - aliases=SUBPARSER_ALIASES['extract'], - help='Extract a named archive to the current directory', - description='Extract a named archive to the current directory', - add_help=False, - ) - extract_group = extract_parser.add_argument_group('extract arguments') - extract_group.add_argument( - '--repository', - help='Path of repository to use, defaults to the configured repository if there is only one', - ) - extract_group.add_argument('--archive', help='Name of archive to operate on', required=True) - extract_group.add_argument( - '--restore-path', - nargs='+', - dest='restore_paths', - help='Paths to restore from archive, defaults to the entire archive', - ) - extract_group.add_argument( - '--progress', - dest='progress', - default=False, - action='store_true', - help='Display progress for each file as it is processed', - ) - extract_group.add_argument( - '-h', '--help', action='help', help='Show this help message and exit' - ) - - list_parser = subparsers.add_parser( - 'list', - aliases=SUBPARSER_ALIASES['list'], - help='List archives', - description='List archives', - add_help=False, - ) - list_group = list_parser.add_argument_group('list arguments') - list_group.add_argument( - '--repository', - help='Path of repository to use, defaults to the configured repository if there is only one', - ) - list_group.add_argument('--archive', help='Name of archive to operate on') - list_group.add_argument( - '--json', dest='json', default=False, action='store_true', help='Output results as JSON' - ) - list_group.add_argument('-h', '--help', action='help', help='Show this help message and exit') - - info_parser = subparsers.add_parser( - 'info', - aliases=SUBPARSER_ALIASES['info'], - help='Display summary information on archives', - description='Display summary information on archives', - add_help=False, - ) - info_group = info_parser.add_argument_group('info arguments') - info_group.add_argument( - '--json', dest='json', default=False, action='store_true', help='Output results as JSON' - ) - info_group.add_argument('-h', '--help', action='help', help='Show this help message and exit') - - arguments = parse_subparser_arguments(unparsed_arguments, top_level_parser, subparsers) - - if arguments['global'].excludes_filename: - raise ValueError( - 'The --excludes option has been replaced with exclude_patterns in configuration' - ) - - if 'init' in arguments and arguments['global'].dry_run: - raise ValueError('The init action cannot be used with the --dry-run option') - - if ( - 'list' in arguments - and 'info' in arguments - and arguments['list'].json - and arguments['info'].json - ): - raise ValueError('With the --json option, list and info actions cannot be used together') - - return arguments def run_configuration(config_filename, config, arguments): # pragma: no cover diff --git a/tests/integration/commands/test_arguments.py b/tests/integration/commands/test_arguments.py new file mode 100644 index 000000000..e5c28ce6e --- /dev/null +++ b/tests/integration/commands/test_arguments.py @@ -0,0 +1,348 @@ +import pytest +from flexmock import flexmock + +from borgmatic.commands import arguments as module + + +def test_parse_arguments_with_no_arguments_uses_defaults(): + config_paths = ['default'] + flexmock(module.collect).should_receive('get_default_config_paths').and_return(config_paths) + + arguments = module.parse_arguments() + + global_arguments = arguments['global'] + assert global_arguments.config_paths == config_paths + assert global_arguments.excludes_filename is None + assert global_arguments.verbosity == 0 + assert global_arguments.syslog_verbosity == 0 + + +def test_parse_arguments_with_multiple_config_paths_parses_as_list(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + arguments = module.parse_arguments('--config', 'myconfig', 'otherconfig') + + global_arguments = arguments['global'] + assert global_arguments.config_paths == ['myconfig', 'otherconfig'] + assert global_arguments.verbosity == 0 + assert global_arguments.syslog_verbosity == 0 + + +def test_parse_arguments_with_verbosity_overrides_default(): + config_paths = ['default'] + flexmock(module.collect).should_receive('get_default_config_paths').and_return(config_paths) + + arguments = module.parse_arguments('--verbosity', '1') + + global_arguments = arguments['global'] + assert global_arguments.config_paths == config_paths + assert global_arguments.excludes_filename is None + assert global_arguments.verbosity == 1 + assert global_arguments.syslog_verbosity == 0 + + +def test_parse_arguments_with_syslog_verbosity_overrides_default(): + config_paths = ['default'] + flexmock(module.collect).should_receive('get_default_config_paths').and_return(config_paths) + + arguments = module.parse_arguments('--syslog-verbosity', '2') + + global_arguments = arguments['global'] + assert global_arguments.config_paths == config_paths + assert global_arguments.excludes_filename is None + assert global_arguments.verbosity == 0 + assert global_arguments.syslog_verbosity == 2 + + +def test_parse_arguments_with_list_json_overrides_default(): + arguments = module.parse_arguments('list', '--json') + + assert 'list' in arguments + assert arguments['list'].json is True + + +def test_parse_arguments_with_dashed_list_json_overrides_default(): + arguments = module.parse_arguments('--list', '--json') + + assert 'list' in arguments + assert arguments['list'].json is True + + +def test_parse_arguments_with_no_actions_defaults_to_all_actions_enabled(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + arguments = module.parse_arguments() + + assert 'prune' in arguments + assert 'create' in arguments + assert 'check' in arguments + + +def test_parse_arguments_with_help_and_no_actions_shows_global_help(capsys): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + with pytest.raises(SystemExit) as exit: + module.parse_arguments('--help') + + assert exit.value.code == 0 + captured = capsys.readouterr() + assert 'global arguments:' in captured.out + assert 'actions:' in captured.out + + +def test_parse_arguments_with_help_and_action_shows_action_help(capsys): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + with pytest.raises(SystemExit) as exit: + module.parse_arguments('create', '--help') + + assert exit.value.code == 0 + captured = capsys.readouterr() + assert 'global arguments:' not in captured.out + assert 'actions:' not in captured.out + assert 'create arguments:' in captured.out + + +def test_parse_arguments_with_prune_action_leaves_other_actions_disabled(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + arguments = module.parse_arguments('prune') + + assert 'prune' in arguments + assert 'create' not in arguments + assert 'check' not in arguments + + +def test_parse_arguments_with_dashed_prune_action_leaves_other_actions_disabled(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + arguments = module.parse_arguments('--prune') + + assert 'prune' in arguments + assert 'create' not in arguments + assert 'check' not in arguments + + +def test_parse_arguments_with_multiple_actions_leaves_other_action_disabled(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + arguments = module.parse_arguments('create', 'check') + + assert 'prune' not in arguments + assert 'create' in arguments + assert 'check' in arguments + + +def test_parse_arguments_with_multiple_dashed_actions_leaves_other_action_disabled(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + arguments = module.parse_arguments('--create', '--check') + + assert 'prune' not in arguments + assert 'create' in arguments + assert 'check' in arguments + + +def test_parse_arguments_with_invalid_arguments_exits(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + with pytest.raises(SystemExit): + module.parse_arguments('--posix-me-harder') + + +def test_parse_arguments_disallows_deprecated_excludes_option(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + with pytest.raises(ValueError): + module.parse_arguments('--config', 'myconfig', '--excludes', 'myexcludes') + + +def test_parse_arguments_disallows_encryption_mode_without_init(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + with pytest.raises(SystemExit): + module.parse_arguments('--config', 'myconfig', '--encryption', 'repokey') + + +def test_parse_arguments_allows_encryption_mode_with_init(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + module.parse_arguments('--config', 'myconfig', 'init', '--encryption', 'repokey') + + +def test_parse_arguments_allows_encryption_mode_with_dashed_init(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + module.parse_arguments('--config', 'myconfig', '--init', '--encryption', 'repokey') + + +def test_parse_arguments_requires_encryption_mode_with_init(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + with pytest.raises(SystemExit): + module.parse_arguments('--config', 'myconfig', 'init') + + +def test_parse_arguments_disallows_append_only_without_init(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + with pytest.raises(SystemExit): + module.parse_arguments('--config', 'myconfig', '--append-only') + + +def test_parse_arguments_disallows_storage_quota_without_init(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + with pytest.raises(SystemExit): + module.parse_arguments('--config', 'myconfig', '--storage-quota', '5G') + + +def test_parse_arguments_allows_init_and_prune(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + module.parse_arguments('--config', 'myconfig', 'init', '--encryption', 'repokey', 'prune') + + +def test_parse_arguments_allows_init_and_create(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + module.parse_arguments('--config', 'myconfig', 'init', '--encryption', 'repokey', 'create') + + +def test_parse_arguments_disallows_init_and_dry_run(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + with pytest.raises(ValueError): + module.parse_arguments( + '--config', 'myconfig', 'init', '--encryption', 'repokey', '--dry-run' + ) + + +def test_parse_arguments_disallows_repository_without_extract_or_list(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + with pytest.raises(SystemExit): + module.parse_arguments('--config', 'myconfig', '--repository', 'test.borg') + + +def test_parse_arguments_allows_repository_with_extract(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + module.parse_arguments( + '--config', 'myconfig', 'extract', '--repository', 'test.borg', '--archive', 'test' + ) + + +def test_parse_arguments_allows_repository_with_list(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + module.parse_arguments('--config', 'myconfig', 'list', '--repository', 'test.borg') + + +def test_parse_arguments_disallows_archive_without_extract_or_list(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + with pytest.raises(SystemExit): + module.parse_arguments('--config', 'myconfig', '--archive', 'test') + + +def test_parse_arguments_disallows_restore_paths_without_extract(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + with pytest.raises(SystemExit): + module.parse_arguments('--config', 'myconfig', '--restore-path', 'test') + + +def test_parse_arguments_allows_archive_with_extract(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + module.parse_arguments('--config', 'myconfig', 'extract', '--archive', 'test') + + +def test_parse_arguments_allows_archive_with_dashed_extract(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + module.parse_arguments('--config', 'myconfig', '--extract', '--archive', 'test') + + +def test_parse_arguments_allows_archive_with_list(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + module.parse_arguments('--config', 'myconfig', 'list', '--archive', 'test') + + +def test_parse_arguments_requires_archive_with_extract(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + with pytest.raises(SystemExit): + module.parse_arguments('--config', 'myconfig', 'extract') + + +def test_parse_arguments_allows_progress_before_create(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + module.parse_arguments('--progress', 'create', 'list') + + +def test_parse_arguments_allows_progress_after_create(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + module.parse_arguments('create', '--progress', 'list') + + +def test_parse_arguments_allows_progress_and_extract(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + module.parse_arguments('--progress', 'extract', '--archive', 'test', 'list') + + +def test_parse_arguments_disallows_progress_without_create(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + with pytest.raises(SystemExit): + module.parse_arguments('--progress', 'list') + + +def test_parse_arguments_with_stats_and_create_flags_does_not_raise(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + module.parse_arguments('--stats', 'create', 'list') + + +def test_parse_arguments_with_stats_and_prune_flags_does_not_raise(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + module.parse_arguments('--stats', 'prune', 'list') + + +def test_parse_arguments_with_stats_flag_but_no_create_or_prune_flag_raises_value_error(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + with pytest.raises(SystemExit): + module.parse_arguments('--stats', 'list') + + +def test_parse_arguments_with_just_stats_flag_does_not_raise(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + module.parse_arguments('--stats') + + +def test_parse_arguments_allows_json_with_list_or_info(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + module.parse_arguments('list', '--json') + module.parse_arguments('info', '--json') + + +def test_parse_arguments_allows_json_with_dashed_info(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + module.parse_arguments('--info', '--json') + + +def test_parse_arguments_disallows_json_with_both_list_and_info(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + with pytest.raises(ValueError): + module.parse_arguments('list', 'info', '--json') diff --git a/tests/integration/commands/test_borgmatic.py b/tests/integration/commands/test_borgmatic.py index 53ea2d363..901e5d555 100644 --- a/tests/integration/commands/test_borgmatic.py +++ b/tests/integration/commands/test_borgmatic.py @@ -1,355 +1,10 @@ import subprocess -import pytest from flexmock import flexmock from borgmatic.commands import borgmatic as module -def test_parse_arguments_with_no_arguments_uses_defaults(): - config_paths = ['default'] - flexmock(module.collect).should_receive('get_default_config_paths').and_return(config_paths) - - arguments = module.parse_arguments() - - global_arguments = arguments['global'] - assert global_arguments.config_paths == config_paths - assert global_arguments.excludes_filename is None - assert global_arguments.verbosity == 0 - assert global_arguments.syslog_verbosity == 0 - - -def test_parse_arguments_with_multiple_config_paths_parses_as_list(): - flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - - arguments = module.parse_arguments('--config', 'myconfig', 'otherconfig') - - global_arguments = arguments['global'] - assert global_arguments.config_paths == ['myconfig', 'otherconfig'] - assert global_arguments.verbosity == 0 - assert global_arguments.syslog_verbosity == 0 - - -def test_parse_arguments_with_verbosity_overrides_default(): - config_paths = ['default'] - flexmock(module.collect).should_receive('get_default_config_paths').and_return(config_paths) - - arguments = module.parse_arguments('--verbosity', '1') - - global_arguments = arguments['global'] - assert global_arguments.config_paths == config_paths - assert global_arguments.excludes_filename is None - assert global_arguments.verbosity == 1 - assert global_arguments.syslog_verbosity == 0 - - -def test_parse_arguments_with_syslog_verbosity_overrides_default(): - config_paths = ['default'] - flexmock(module.collect).should_receive('get_default_config_paths').and_return(config_paths) - - arguments = module.parse_arguments('--syslog-verbosity', '2') - - global_arguments = arguments['global'] - assert global_arguments.config_paths == config_paths - assert global_arguments.excludes_filename is None - assert global_arguments.verbosity == 0 - assert global_arguments.syslog_verbosity == 2 - - -def test_parse_arguments_with_list_json_overrides_default(): - arguments = module.parse_arguments('list', '--json') - - assert 'list' in arguments - assert arguments['list'].json is True - - -def test_parse_arguments_with_dashed_list_json_overrides_default(): - arguments = module.parse_arguments('--list', '--json') - - assert 'list' in arguments - assert arguments['list'].json is True - - -def test_parse_arguments_with_no_actions_defaults_to_all_actions_enabled(): - flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - - arguments = module.parse_arguments() - - assert 'prune' in arguments - assert 'create' in arguments - assert 'check' in arguments - - -def test_parse_arguments_with_help_and_no_actions_shows_global_help(capsys): - flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - - with pytest.raises(SystemExit) as exit: - module.parse_arguments('--help') - - assert exit.value.code == 0 - captured = capsys.readouterr() - assert 'global arguments:' in captured.out - assert 'actions:' in captured.out - - -def test_parse_arguments_with_help_and_action_shows_action_help(capsys): - flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - - with pytest.raises(SystemExit) as exit: - module.parse_arguments('create', '--help') - - assert exit.value.code == 0 - captured = capsys.readouterr() - assert 'global arguments:' not in captured.out - assert 'actions:' not in captured.out - assert 'create arguments:' in captured.out - - -def test_parse_arguments_with_prune_action_leaves_other_actions_disabled(): - flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - - arguments = module.parse_arguments('prune') - - assert 'prune' in arguments - assert 'create' not in arguments - assert 'check' not in arguments - - -def test_parse_arguments_with_dashed_prune_action_leaves_other_actions_disabled(): - flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - - arguments = module.parse_arguments('--prune') - - assert 'prune' in arguments - assert 'create' not in arguments - assert 'check' not in arguments - - -def test_parse_arguments_with_multiple_actions_leaves_other_action_disabled(): - flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - - arguments = module.parse_arguments('create', 'check') - - assert 'prune' not in arguments - assert 'create' in arguments - assert 'check' in arguments - - -def test_parse_arguments_with_multiple_dashed_actions_leaves_other_action_disabled(): - flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - - arguments = module.parse_arguments('--create', '--check') - - assert 'prune' not in arguments - assert 'create' in arguments - assert 'check' in arguments - - -def test_parse_arguments_with_invalid_arguments_exits(): - flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - - with pytest.raises(SystemExit): - module.parse_arguments('--posix-me-harder') - - -def test_parse_arguments_disallows_deprecated_excludes_option(): - flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - - with pytest.raises(ValueError): - module.parse_arguments('--config', 'myconfig', '--excludes', 'myexcludes') - - -def test_parse_arguments_disallows_encryption_mode_without_init(): - flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - - with pytest.raises(SystemExit): - module.parse_arguments('--config', 'myconfig', '--encryption', 'repokey') - - -def test_parse_arguments_allows_encryption_mode_with_init(): - flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - - module.parse_arguments('--config', 'myconfig', 'init', '--encryption', 'repokey') - - -def test_parse_arguments_allows_encryption_mode_with_dashed_init(): - flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - - module.parse_arguments('--config', 'myconfig', '--init', '--encryption', 'repokey') - - -def test_parse_arguments_requires_encryption_mode_with_init(): - flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - - with pytest.raises(SystemExit): - module.parse_arguments('--config', 'myconfig', 'init') - - -def test_parse_arguments_disallows_append_only_without_init(): - flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - - with pytest.raises(SystemExit): - module.parse_arguments('--config', 'myconfig', '--append-only') - - -def test_parse_arguments_disallows_storage_quota_without_init(): - flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - - with pytest.raises(SystemExit): - module.parse_arguments('--config', 'myconfig', '--storage-quota', '5G') - - -def test_parse_arguments_allows_init_and_prune(): - flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - - module.parse_arguments('--config', 'myconfig', 'init', '--encryption', 'repokey', 'prune') - - -def test_parse_arguments_allows_init_and_create(): - flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - - module.parse_arguments('--config', 'myconfig', 'init', '--encryption', 'repokey', 'create') - - -def test_parse_arguments_disallows_init_and_dry_run(): - flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - - with pytest.raises(ValueError): - module.parse_arguments( - '--config', 'myconfig', 'init', '--encryption', 'repokey', '--dry-run' - ) - - -def test_parse_arguments_disallows_repository_without_extract_or_list(): - flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - - with pytest.raises(SystemExit): - module.parse_arguments('--config', 'myconfig', '--repository', 'test.borg') - - -def test_parse_arguments_allows_repository_with_extract(): - flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - - module.parse_arguments( - '--config', 'myconfig', 'extract', '--repository', 'test.borg', '--archive', 'test' - ) - - -def test_parse_arguments_allows_repository_with_list(): - flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - - module.parse_arguments('--config', 'myconfig', 'list', '--repository', 'test.borg') - - -def test_parse_arguments_disallows_archive_without_extract_or_list(): - flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - - with pytest.raises(SystemExit): - module.parse_arguments('--config', 'myconfig', '--archive', 'test') - - -def test_parse_arguments_disallows_restore_paths_without_extract(): - flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - - with pytest.raises(SystemExit): - module.parse_arguments('--config', 'myconfig', '--restore-path', 'test') - - -def test_parse_arguments_allows_archive_with_extract(): - flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - - module.parse_arguments('--config', 'myconfig', 'extract', '--archive', 'test') - - -def test_parse_arguments_allows_archive_with_dashed_extract(): - flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - - module.parse_arguments('--config', 'myconfig', '--extract', '--archive', 'test') - - -def test_parse_arguments_allows_archive_with_list(): - flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - - module.parse_arguments('--config', 'myconfig', 'list', '--archive', 'test') - - -def test_parse_arguments_requires_archive_with_extract(): - flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - - with pytest.raises(SystemExit): - module.parse_arguments('--config', 'myconfig', 'extract') - - -def test_parse_arguments_allows_progress_before_create(): - flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - - module.parse_arguments('--progress', 'create', 'list') - - -def test_parse_arguments_allows_progress_after_create(): - flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - - module.parse_arguments('create', '--progress', 'list') - - -def test_parse_arguments_allows_progress_and_extract(): - flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - - module.parse_arguments('--progress', 'extract', '--archive', 'test', 'list') - - -def test_parse_arguments_disallows_progress_without_create(): - flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - - with pytest.raises(SystemExit): - module.parse_arguments('--progress', 'list') - - -def test_parse_arguments_with_stats_and_create_flags_does_not_raise(): - flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - - module.parse_arguments('--stats', 'create', 'list') - - -def test_parse_arguments_with_stats_and_prune_flags_does_not_raise(): - flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - - module.parse_arguments('--stats', 'prune', 'list') - - -def test_parse_arguments_with_stats_flag_but_no_create_or_prune_flag_raises_value_error(): - flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - - with pytest.raises(SystemExit): - module.parse_arguments('--stats', 'list') - - -def test_parse_arguments_with_just_stats_flag_does_not_raise(): - flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - - module.parse_arguments('--stats') - - -def test_parse_arguments_allows_json_with_list_or_info(): - flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - - module.parse_arguments('list', '--json') - module.parse_arguments('info', '--json') - - -def test_parse_arguments_allows_json_with_dashed_info(): - flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - - module.parse_arguments('--info', '--json') - - -def test_parse_arguments_disallows_json_with_both_list_and_info(): - flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - - with pytest.raises(ValueError): - module.parse_arguments('list', 'info', '--json') - - def test_borgmatic_version_matches_news_version(): flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])