Compare commits

..
13 Commits
24 changed files with 327 additions and 155 deletions
+6 -6
View File
@@ -1,30 +1,30 @@
---
kind: pipeline
name: python-3-5-alpine-3-9
name: python-3-5-alpine-3-10
steps:
- name: build
image: python:3.5-alpine3.9
image: python:3.5-alpine3.10
pull: always
commands:
- scripts/run-tests
---
kind: pipeline
name: python-3-6-alpine-3-9
name: python-3-6-alpine-3-10
steps:
- name: build
image: python:3.6-alpine3.9
image: python:3.6-alpine3.10
pull: always
commands:
- scripts/run-tests
---
kind: pipeline
name: python-3-7-alpine-3-9
name: python-3-7-alpine-3-10
steps:
- name: build
image: python:3.7-alpine3.9
image: python:3.7-alpine3.10
pull: always
commands:
- scripts/run-tests
+1
View File
@@ -1 +1,2 @@
include borgmatic/config/schema.yaml
graft sample/systemd
+14
View File
@@ -1,3 +1,17 @@
1.3.20
* #205: More robust sample systemd service: boot delay, network dependency, lowered CPU/IO
priority, etc.
* #221: Fix "borgmatic create --progress" output so that it updates on the console in real-time.
1.3.19
* #219: Fix visibility of "borgmatic prune --stats" output.
1.3.18
* #220: Fix regression of argument parsing for default actions.
1.3.17
* #217: Fix error with "borgmatic check --only" command-line flag with "extract" consistency check.
1.3.16
* #210: Support for Borg check --verify-data flag via borgmatic "data" consistency check.
* #210: Override configured consistency checks via "borgmatic check --only" command-line flag.
+7 -1
View File
@@ -4,7 +4,7 @@ import logging
import os
import tempfile
from borgmatic.execute import execute_command
from borgmatic.execute import execute_command, execute_command_without_capture
logger = logging.getLogger(__name__)
@@ -163,6 +163,12 @@ def create_archive(
+ sources
)
# The progress output isn't compatible with captured and logged output, as progress messes with
# the terminal directly.
if progress:
execute_command_without_capture(full_command)
return
if json:
output_log_level = None
elif stats:
+7 -1
View File
@@ -1,6 +1,6 @@
import logging
from borgmatic.execute import execute_command
from borgmatic.execute import execute_command, execute_command_without_capture
logger = logging.getLogger(__name__)
@@ -83,4 +83,10 @@ def extract_archive(
+ (tuple(restore_paths) if restore_paths else ())
)
# The progress output isn't compatible with captured and logged output, as progress messes with
# the terminal directly.
if progress:
execute_command_without_capture(full_command)
return
execute_command(full_command)
+2 -6
View File
@@ -1,7 +1,7 @@
import logging
import subprocess
from borgmatic.execute import BORG_ERROR_EXIT_CODE, execute_command
from borgmatic.execute import execute_command, execute_command_without_capture
logger = logging.getLogger(__name__)
@@ -45,8 +45,4 @@ def initialize_repository(
)
# Don't use execute_command() here because it doesn't support interactive prompts.
try:
subprocess.check_call(init_command)
except subprocess.CalledProcessError as error:
if error.returncode >= BORG_ERROR_EXIT_CODE:
raise
execute_command_without_capture(init_command)
+1 -1
View File
@@ -64,4 +64,4 @@ def prune_archives(
+ (repository,)
)
execute_command(full_command)
execute_command(full_command, output_log_level=logging.WARNING if stats else logging.INFO)
+60 -21
View File
@@ -14,14 +14,14 @@ SUBPARSER_ALIASES = {
}
def parse_subparser_arguments(unparsed_arguments, top_level_parser, subparsers):
def parse_subparser_arguments(unparsed_arguments, 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.
Given a sequence of arguments, and a subparsers object as returned by
argparse.ArgumentParser().add_subparsers(), give each requested action's subparser a shot at
parsing all arguments. This allows common arguments like "--repository" to be shared across
multiple subparsers.
Return the result as a dict mapping from subparser name (or "global") to a parsed namespace of
arguments.
Return the result as a dict mapping from subparser name to a parsed namespace of arguments.
'''
arguments = collections.OrderedDict()
remaining_arguments = list(unparsed_arguments)
@@ -31,35 +31,73 @@ def parse_subparser_arguments(unparsed_arguments, top_level_parser, subparsers):
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:
if subparser_name not in remaining_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)
# If a parsed value happens to be the same as the name of a subparser, remove it from the
# remaining arguments. This prevents, for instance, "check --only extract" from triggering
# the "extract" subparser.
parsed, unused_remaining = subparser.parse_known_args(unparsed_arguments)
for value in vars(parsed).values():
if isinstance(value, str):
if value in subparsers.choices:
remaining_arguments.remove(value)
elif isinstance(value, list):
for item in value:
if item in subparsers.choices:
remaining_arguments.remove(item)
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)
parsed, unused_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_global_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(), parse and return any global
arguments as a parsed argparse.Namespace instance.
'''
# Ask each subparser, one by one, to greedily consume arguments. Any arguments that remain
# are global arguments.
remaining_arguments = list(unparsed_arguments)
present_subparser_names = set()
for subparser_name, subparser in subparsers.choices.items():
if subparser_name not in remaining_arguments:
continue
present_subparser_names.add(subparser_name)
unused_parsed, remaining_arguments = subparser.parse_known_args(remaining_arguments)
# If no actions are explicitly requested, assume defaults: prune, create, and check.
if (
not present_subparser_names
and '--help' not in unparsed_arguments
and '-h' not in unparsed_arguments
):
for subparser_name in ('prune', 'create', 'check'):
subparser = subparsers.choices[subparser_name]
unused_parsed, remaining_arguments = subparser.parse_known_args(remaining_arguments)
# Remove the subparser names themselves.
for subparser_name in present_subparser_names:
if subparser_name in remaining_arguments:
remaining_arguments.remove(subparser_name)
return top_level_parser.parse_args(remaining_arguments)
def parse_arguments(*unparsed_arguments):
'''
Given command-line arguments with which this script was invoked, parse the arguments and return
@@ -218,7 +256,7 @@ def parse_arguments(*unparsed_arguments):
choices=('repository', 'archives', 'data', 'extract'),
dest='only',
action='append',
help='Run a particular consistency check instead of configured checks (can specify multiple times)',
help='Run a particular consistency check (repository, archives, data, or extract) instead of configured checks; can specify flag multiple times',
)
check_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
@@ -339,7 +377,8 @@ def parse_arguments(*unparsed_arguments):
)
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)
arguments = parse_subparser_arguments(unparsed_arguments, subparsers)
arguments['global'] = parse_global_arguments(unparsed_arguments, top_level_parser, subparsers)
if arguments['global'].excludes_filename:
raise ValueError(
+15
View File
@@ -61,3 +61,18 @@ def execute_command(full_command, output_log_level=logging.INFO, shell=False):
return output.decode() if output is not None else None
else:
execute_and_log_output(full_command, output_log_level, shell=shell)
def execute_command_without_capture(full_command):
'''
Execute the given command (a sequence of command/argument strings), but don't capture or log its
output in any way. This is necessary for commands that monkey with the terminal (e.g. progress
display) or provide interactive prompts.
'''
logger.debug(' '.join(full_command))
try:
subprocess.check_call(full_command)
except subprocess.CalledProcessError as error:
if error.returncode >= BORG_ERROR_EXIT_CODE:
raise
+3 -3
View File
@@ -1,4 +1,4 @@
FROM python:3.7.3-alpine3.10 as borgmatic
FROM python:3.7.4-alpine3.10 as borgmatic
COPY . /app
RUN pip install --no-cache /app && generate-borgmatic-config && chmod +r /etc/borgmatic/config.yaml
@@ -7,7 +7,7 @@ RUN borgmatic --help > /command-line.txt \
echo -e "\n--------------------------------------------------------------------------------\n" >> /command-line.txt \
&& borgmatic "$action" --help >> /command-line.txt; done
FROM node:12.4.0-alpine as html
FROM node:12.10.0-alpine as html
WORKDIR /source
@@ -23,7 +23,7 @@ COPY . /source
RUN npx eleventy --input=/source/docs --output=/output/docs \
&& mv /output/docs/index.html /output/index.html
FROM nginx:1.16.0-alpine
FROM nginx:1.16.1-alpine
COPY --from=html /output /usr/share/nginx/html
COPY --from=borgmatic /etc/borgmatic/config.yaml /usr/share/nginx/html/docs/reference/config.yaml
@@ -68,6 +68,16 @@ consistency:
- path/of/repository_to_check.borg
```
Finally, you can override your configuration file's consistency checks, and
run particular checks via the command-line. For instance:
```bash
borgmatic check --only data --only extract
```
This is useful for running slow consistency checks on an infrequent basis,
separate from your regular checks.
## Troubleshooting
+10
View File
@@ -20,6 +20,16 @@ Or, for even more progress and debug spew:
borgmatic --verbosity 2
```
## Backup summary
If you're less concerned with progress during a backup, and you just want to
see the summary of archive statistics at the end, you can use the stats
option:
```bash
borgmatic --stats
```
## Existing backups
Borgmatic provides convenient flags for Borg's
-3
View File
@@ -1,3 +0,0 @@
# You can drop this file into /etc/cron.d/ to run borgmatic nightly.
0 3 * * * PATH=$PATH:/usr/bin /root/.local/bin/borgmatic
+1 -1
View File
@@ -1,3 +1,3 @@
# You can drop this file into /etc/cron.d/ to run borgmatic nightly.
0 3 * * * root PATH=$PATH:/usr/local/bin /root/.local/bin/borgmatic
0 3 * * * root PATH=$PATH:/usr/bin:/usr/local/bin /root/.local/bin/borgmatic --syslog-verbosity 1
+16 -1
View File
@@ -1,7 +1,22 @@
[Unit]
Description=borgmatic backup
Wants=network-online.target
After=network-online.target
ConditionACPower=true
[Service]
Type=oneshot
ExecStart=/root/.local/bin/borgmatic
# Lower CPU and I/O priority.
Nice=19
CPUSchedulingPolicy=batch
IOSchedulingClass=best-effort
IOSchedulingPriority=7
IOWeight=100
Restart=no
LogRateLimitIntervalSec=0
# Delay start to prevent backups running during boot.
ExecStartPre=sleep 1m
ExecStart=systemd-inhibit --who="borgmatic" --why="Prevent interrupting scheduled backup" /root/.local/bin/borgmatic --syslog-verbosity 1
+1 -1
View File
@@ -1,6 +1,6 @@
from setuptools import find_packages, setup
VERSION = '1.3.16'
VERSION = '1.3.20'
setup(
+7 -7
View File
@@ -4,21 +4,21 @@ attrs==19.1.0
black==19.3b0; python_version >= '3.6'
click==7.0
colorama==0.4.1
coverage==4.5.3
coverage==4.5.4
docopt==0.6.2
flake8==3.7.7
flake8==3.7.8
flexmock==0.10.4
isort==4.3.20
isort==4.3.21
mccabe==0.6.1
more-itertools==7.0.0
pluggy==0.12.0
more-itertools==7.2.0
pluggy==0.13.0
py==1.8.0
pycodestyle==2.5.0
pyflakes==2.1.1
pykwalify==1.7.0
pytest==4.6.3
pytest==5.1.2
pytest-cov==2.7.1
python-dateutil==2.8.0
PyYAML==5.1.1
PyYAML==5.1.2
ruamel.yaml>0.15.0,<0.17.0
toml==0.10.0
+30 -6
View File
@@ -78,6 +78,18 @@ def test_parse_arguments_with_no_actions_defaults_to_all_actions_enabled():
assert 'check' in arguments
def test_parse_arguments_with_no_actions_passes_argument_to_relevant_actions():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
arguments = module.parse_arguments('--stats')
assert 'prune' in arguments
assert arguments['prune'].stats
assert 'create' in arguments
assert arguments['create'].stats
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'])
@@ -322,12 +334,6 @@ def test_parse_arguments_with_stats_flag_but_no_create_or_prune_flag_raises_valu
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'])
@@ -346,3 +352,21 @@ def test_parse_arguments_disallows_json_with_both_list_and_info():
with pytest.raises(ValueError):
module.parse_arguments('list', 'info', '--json')
def test_parse_arguments_check_only_extract_does_not_raise_extract_subparser_error():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
module.parse_arguments('check', '--only', 'extract')
def test_parse_arguments_extract_archive_check_does_not_raise_check_subparser_error():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
module.parse_arguments('extract', '--archive', 'check')
def test_parse_arguments_extract_with_check_only_extract_does_not_raise():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
module.parse_arguments('extract', '--archive', 'name', 'check', '--only', 'extract')
+23
View File
@@ -758,6 +758,29 @@ def test_create_archive_with_stats_calls_borg_with_stats_parameter():
)
def test_create_archive_with_progress_calls_borg_with_progress_parameter():
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
flexmock(module).should_receive('_expand_home_directories').and_return(())
flexmock(module).should_receive('_write_pattern_file').and_return(None)
flexmock(module).should_receive('_make_pattern_flags').and_return(())
flexmock(module).should_receive('_make_exclude_flags').and_return(())
flexmock(module).should_receive('execute_command_without_capture').with_args(
('borg', 'create', '--progress') + ARCHIVE_WITH_PATHS
)
module.create_archive(
dry_run=False,
repository='repo',
location_config={
'source_directories': ['foo', 'bar'],
'repositories': ['repo'],
'exclude_patterns': None,
},
storage_config={},
progress=True,
)
def test_create_archive_with_json_calls_borg_with_json_parameter():
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
flexmock(module).should_receive('_expand_home_directories').and_return(())
+3 -1
View File
@@ -195,7 +195,9 @@ def test_extract_archive_calls_borg_with_dry_run_parameter():
def test_extract_archive_calls_borg_with_progress_parameter():
insert_execute_command_mock(('borg', 'extract', '--progress', 'repo::archive'))
flexmock(module).should_receive('execute_command_without_capture').with_args(
('borg', 'extract', '--progress', 'repo::archive')
).once()
module.extract_archive(
dry_run=False,
+4 -13
View File
@@ -23,8 +23,8 @@ def insert_info_command_not_found_mock():
def insert_init_command_mock(init_command, **kwargs):
flexmock(module.subprocess).should_receive('check_call').with_args(
init_command, **kwargs
flexmock(module).should_receive('execute_command_without_capture').with_args(
init_command
).once()
@@ -35,18 +35,9 @@ def test_initialize_repository_calls_borg_with_parameters():
module.initialize_repository(repository='repo', encryption_mode='repokey')
def test_initialize_repository_does_not_raise_for_borg_init_warning():
insert_info_command_not_found_mock()
flexmock(module.subprocess).should_receive('check_call').and_raise(
module.subprocess.CalledProcessError(1, 'borg init')
)
module.initialize_repository(repository='repo', encryption_mode='repokey')
def test_initialize_repository_raises_for_borg_init_error():
insert_info_command_not_found_mock()
flexmock(module.subprocess).should_receive('check_call').and_raise(
flexmock(module).should_receive('execute_command_without_capture').and_raise(
module.subprocess.CalledProcessError(2, 'borg init')
)
@@ -56,7 +47,7 @@ def test_initialize_repository_raises_for_borg_init_error():
def test_initialize_repository_skips_initialization_when_repository_already_exists():
insert_info_command_found_mock()
flexmock(module.subprocess).should_receive('check_call').never()
flexmock(module).should_receive('execute_command_without_capture').never()
module.initialize_repository(repository='repo', encryption_mode='repokey')
+28 -10
View File
@@ -8,8 +8,10 @@ from borgmatic.borg import prune as module
from ..test_verbosity import insert_logging_mock
def insert_execute_command_mock(prune_command, **kwargs):
flexmock(module).should_receive('execute_command').with_args(prune_command).once()
def insert_execute_command_mock(prune_command, output_log_level):
flexmock(module).should_receive('execute_command').with_args(
prune_command, output_log_level=output_log_level
).once()
BASE_PRUNE_FLAGS = (('--keep-daily', '1'), ('--keep-weekly', '2'), ('--keep-monthly', '3'))
@@ -61,7 +63,7 @@ def test_prune_archives_calls_borg_with_parameters():
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
BASE_PRUNE_FLAGS
)
insert_execute_command_mock(PRUNE_COMMAND + ('repo',))
insert_execute_command_mock(PRUNE_COMMAND + ('repo',), logging.INFO)
module.prune_archives(
dry_run=False, repository='repo', storage_config={}, retention_config=retention_config
@@ -73,7 +75,7 @@ def test_prune_archives_with_log_info_calls_borg_with_info_parameter():
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
BASE_PRUNE_FLAGS
)
insert_execute_command_mock(PRUNE_COMMAND + ('--stats', '--info', 'repo'))
insert_execute_command_mock(PRUNE_COMMAND + ('--stats', '--info', 'repo'), logging.INFO)
insert_logging_mock(logging.INFO)
module.prune_archives(
@@ -87,7 +89,7 @@ def test_prune_archives_with_log_debug_calls_borg_with_debug_parameter():
BASE_PRUNE_FLAGS
)
insert_execute_command_mock(
PRUNE_COMMAND + ('--stats', '--debug', '--list', '--show-rc', 'repo')
PRUNE_COMMAND + ('--stats', '--debug', '--list', '--show-rc', 'repo'), logging.INFO
)
insert_logging_mock(logging.DEBUG)
@@ -101,7 +103,7 @@ def test_prune_archives_with_dry_run_calls_borg_with_dry_run_parameter():
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
BASE_PRUNE_FLAGS
)
insert_execute_command_mock(PRUNE_COMMAND + ('--dry-run', 'repo'))
insert_execute_command_mock(PRUNE_COMMAND + ('--dry-run', 'repo'), logging.INFO)
module.prune_archives(
repository='repo', storage_config={}, dry_run=True, retention_config=retention_config
@@ -113,7 +115,7 @@ def test_prune_archives_with_local_path_calls_borg_via_local_path():
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
BASE_PRUNE_FLAGS
)
insert_execute_command_mock(('borg1',) + PRUNE_COMMAND[1:] + ('repo',))
insert_execute_command_mock(('borg1',) + PRUNE_COMMAND[1:] + ('repo',), logging.INFO)
module.prune_archives(
dry_run=False,
@@ -129,7 +131,7 @@ def test_prune_archives_with_remote_path_calls_borg_with_remote_path_parameters(
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
BASE_PRUNE_FLAGS
)
insert_execute_command_mock(PRUNE_COMMAND + ('--remote-path', 'borg1', 'repo'))
insert_execute_command_mock(PRUNE_COMMAND + ('--remote-path', 'borg1', 'repo'), logging.INFO)
module.prune_archives(
dry_run=False,
@@ -140,13 +142,29 @@ def test_prune_archives_with_remote_path_calls_borg_with_remote_path_parameters(
)
def test_prune_archives_with_stats_calls_borg_with_stats_parameter():
retention_config = flexmock()
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
BASE_PRUNE_FLAGS
)
insert_execute_command_mock(PRUNE_COMMAND + ('--stats', 'repo'), logging.WARNING)
module.prune_archives(
dry_run=False,
repository='repo',
storage_config={},
retention_config=retention_config,
stats=True,
)
def test_prune_archives_with_umask_calls_borg_with_umask_parameters():
storage_config = {'umask': '077'}
retention_config = flexmock()
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
BASE_PRUNE_FLAGS
)
insert_execute_command_mock(PRUNE_COMMAND + ('--umask', '077', 'repo'))
insert_execute_command_mock(PRUNE_COMMAND + ('--umask', '077', 'repo'), logging.INFO)
module.prune_archives(
dry_run=False,
@@ -162,7 +180,7 @@ def test_prune_archives_with_lock_wait_calls_borg_with_lock_wait_parameters():
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
BASE_PRUNE_FLAGS
)
insert_execute_command_mock(PRUNE_COMMAND + ('--lock-wait', '5', 'repo'))
insert_execute_command_mock(PRUNE_COMMAND + ('--lock-wait', '5', 'repo'), logging.INFO)
module.prune_archives(
dry_run=False,
+52 -73
View File
@@ -4,9 +4,7 @@ from borgmatic.commands import arguments as module
def test_parse_subparser_arguments_consumes_subparser_arguments_before_subparser_name():
global_namespace = flexmock()
action_namespace = flexmock(foo=True)
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
subparsers = flexmock(
choices={
'action': flexmock(parse_known_args=lambda arguments: (action_namespace, [])),
@@ -14,17 +12,13 @@ def test_parse_subparser_arguments_consumes_subparser_arguments_before_subparser
}
)
arguments = module.parse_subparser_arguments(
('--foo', 'true', 'action'), top_level_parser, subparsers
)
arguments = module.parse_subparser_arguments(('--foo', 'true', 'action'), subparsers)
assert arguments == {'action': action_namespace, 'global': global_namespace}
assert arguments == {'action': action_namespace}
def test_parse_subparser_arguments_consumes_subparser_arguments_after_subparser_name():
global_namespace = flexmock()
action_namespace = flexmock(foo=True)
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
subparsers = flexmock(
choices={
'action': flexmock(parse_known_args=lambda arguments: (action_namespace, [])),
@@ -32,57 +26,13 @@ def test_parse_subparser_arguments_consumes_subparser_arguments_after_subparser_
}
)
arguments = module.parse_subparser_arguments(
('action', '--foo', 'true'), top_level_parser, subparsers
)
arguments = module.parse_subparser_arguments(('action', '--foo', 'true'), subparsers)
assert arguments == {'action': action_namespace, 'global': global_namespace}
def test_parse_subparser_arguments_consumes_global_arguments_before_subparser_name():
global_namespace = flexmock(verbosity='lots')
action_namespace = flexmock()
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
subparsers = flexmock(
choices={
'action': flexmock(
parse_known_args=lambda arguments: (action_namespace, ['--verbosity', 'lots'])
),
'other': flexmock(),
}
)
arguments = module.parse_subparser_arguments(
('--verbosity', 'lots', 'action'), top_level_parser, subparsers
)
assert arguments == {'action': action_namespace, 'global': global_namespace}
def test_parse_subparser_arguments_consumes_global_arguments_after_subparser_name():
global_namespace = flexmock(verbosity='lots')
action_namespace = flexmock()
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
subparsers = flexmock(
choices={
'action': flexmock(
parse_known_args=lambda arguments: (action_namespace, ['--verbosity', 'lots'])
),
'other': flexmock(),
}
)
arguments = module.parse_subparser_arguments(
('action', '--verbosity', 'lots'), top_level_parser, subparsers
)
assert arguments == {'action': action_namespace, 'global': global_namespace}
assert arguments == {'action': action_namespace}
def test_parse_subparser_arguments_consumes_subparser_arguments_with_alias():
global_namespace = flexmock()
action_namespace = flexmock(foo=True)
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
action_subparser = flexmock(parse_known_args=lambda arguments: (action_namespace, []))
subparsers = flexmock(
choices={
@@ -94,18 +44,14 @@ def test_parse_subparser_arguments_consumes_subparser_arguments_with_alias():
)
flexmock(module).SUBPARSER_ALIASES = {'action': ['-a'], 'other': ['-o']}
arguments = module.parse_subparser_arguments(
('-a', '--foo', 'true'), top_level_parser, subparsers
)
arguments = module.parse_subparser_arguments(('-a', '--foo', 'true'), subparsers)
assert arguments == {'action': action_namespace, 'global': global_namespace}
assert arguments == {'action': action_namespace}
def test_parse_subparser_arguments_consumes_multiple_subparser_arguments():
global_namespace = flexmock()
action_namespace = flexmock(foo=True)
other_namespace = flexmock(bar=3)
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
subparsers = flexmock(
choices={
'action': flexmock(
@@ -116,22 +62,16 @@ def test_parse_subparser_arguments_consumes_multiple_subparser_arguments():
)
arguments = module.parse_subparser_arguments(
('action', '--foo', 'true', 'other', '--bar', '3'), top_level_parser, subparsers
('action', '--foo', 'true', 'other', '--bar', '3'), subparsers
)
assert arguments == {
'action': action_namespace,
'other': other_namespace,
'global': global_namespace,
}
assert arguments == {'action': action_namespace, 'other': other_namespace}
def test_parse_subparser_arguments_applies_default_subparsers():
global_namespace = flexmock()
prune_namespace = flexmock()
create_namespace = flexmock(progress=True)
check_namespace = flexmock()
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
subparsers = flexmock(
choices={
'prune': flexmock(parse_known_args=lambda arguments: (prune_namespace, ['--progress'])),
@@ -141,17 +81,16 @@ def test_parse_subparser_arguments_applies_default_subparsers():
}
)
arguments = module.parse_subparser_arguments(('--progress'), top_level_parser, subparsers)
arguments = module.parse_subparser_arguments(('--progress'), subparsers)
assert arguments == {
'prune': prune_namespace,
'create': create_namespace,
'check': check_namespace,
'global': global_namespace,
}
def test_parse_subparser_arguments_with_help_does_not_apply_default_subparsers():
def test_parse_global_arguments_with_help_does_not_apply_default_subparsers():
global_namespace = flexmock(verbosity='lots')
action_namespace = flexmock()
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
@@ -164,8 +103,48 @@ def test_parse_subparser_arguments_with_help_does_not_apply_default_subparsers()
}
)
arguments = module.parse_subparser_arguments(
arguments = module.parse_global_arguments(
('--verbosity', 'lots', '--help'), top_level_parser, subparsers
)
assert arguments == {'global': global_namespace}
assert arguments == global_namespace
def test_parse_global_arguments_consumes_global_arguments_before_subparser_name():
global_namespace = flexmock(verbosity='lots')
action_namespace = flexmock()
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
subparsers = flexmock(
choices={
'action': flexmock(
parse_known_args=lambda arguments: (action_namespace, ['--verbosity', 'lots'])
),
'other': flexmock(),
}
)
arguments = module.parse_global_arguments(
('--verbosity', 'lots', 'action'), top_level_parser, subparsers
)
assert arguments == global_namespace
def test_parse_global_arguments_consumes_global_arguments_after_subparser_name():
global_namespace = flexmock(verbosity='lots')
action_namespace = flexmock()
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
subparsers = flexmock(
choices={
'action': flexmock(
parse_known_args=lambda arguments: (action_namespace, ['--verbosity', 'lots'])
),
'other': flexmock(),
}
)
arguments = module.parse_global_arguments(
('action', '--verbosity', 'lots'), top_level_parser, subparsers
)
assert arguments == global_namespace
+26
View File
@@ -1,5 +1,6 @@
import logging
import pytest
from flexmock import flexmock
from borgmatic import execute as module
@@ -49,3 +50,28 @@ def test_execute_command_captures_output_with_shell():
output = module.execute_command(full_command, output_log_level=None, shell=True)
assert output == expected_output
def test_execute_command_without_capture_does_not_raise_on_success():
flexmock(module.subprocess).should_receive('check_call').and_raise(
module.subprocess.CalledProcessError(0, 'borg init')
)
module.execute_command_without_capture(('borg', 'init'))
def test_execute_command_without_capture_does_not_raise_on_warning():
flexmock(module.subprocess).should_receive('check_call').and_raise(
module.subprocess.CalledProcessError(1, 'borg init')
)
module.execute_command_without_capture(('borg', 'init'))
def test_execute_command_without_capture_raises_on_error():
flexmock(module.subprocess).should_receive('check_call').and_raise(
module.subprocess.CalledProcessError(2, 'borg init')
)
with pytest.raises(module.subprocess.CalledProcessError):
module.execute_command_without_capture(('borg', 'init'))