Browse Source

Split out Borg integration code into multiple files, as it was getting kind of hairy all in one.

Dan Helfman 8 years ago
parent
commit
247d36a309

+ 0 - 242
borgmatic/borg.py

@@ -1,242 +0,0 @@
-from datetime import datetime
-import glob
-import itertools
-import os
-import platform
-import sys
-import re
-import subprocess
-import tempfile
-
-from borgmatic.verbosity import VERBOSITY_SOME, VERBOSITY_LOTS
-
-
-# Integration with Borg for actually handling backups.
-
-
-COMMAND = 'borg'
-
-
-def initialize(storage_config, command=COMMAND):
-    passphrase = storage_config.get('encryption_passphrase')
-
-    if passphrase:
-        os.environ['{}_PASSPHRASE'.format(command.upper())] = passphrase
-
-
-def _write_exclude_file(exclude_patterns=None):
-    '''
-    Given a sequence of exclude patterns, write them to a named temporary file and return it. Return
-    None if no patterns are provided.
-    '''
-    if not exclude_patterns:
-        return None
-
-    exclude_file = tempfile.NamedTemporaryFile('w')
-    exclude_file.write('\n'.join(exclude_patterns))
-    exclude_file.flush()
-
-    return exclude_file
-
-
-def create_archive(
-    verbosity, repository, location_config, storage_config, command=COMMAND,
-):
-    '''
-    Given a vebosity flag, a storage config dict, a list of source directories, a local or remote
-    repository path, a list of exclude patterns, and a command to run, create a Borg archive.
-    '''
-    sources = tuple(
-        itertools.chain.from_iterable(
-            glob.glob(directory) or [directory]
-            for directory in location_config['source_directories']
-        )
-    )
-
-    exclude_file = _write_exclude_file(location_config.get('exclude_patterns'))
-    exclude_flags = ('--exclude-from', exclude_file.name) if exclude_file else ()
-    compression = storage_config.get('compression', None)
-    compression_flags = ('--compression', compression) if compression else ()
-    umask = storage_config.get('umask', None)
-    umask_flags = ('--umask', str(umask)) if umask else ()
-    one_file_system_flags = ('--one-file-system',) if location_config.get('one_file_system') else ()
-    remote_path = location_config.get('remote_path')
-    remote_path_flags = ('--remote-path', remote_path) if remote_path else ()
-    verbosity_flags = {
-        VERBOSITY_SOME: ('--info', '--stats',),
-        VERBOSITY_LOTS: ('--debug', '--list', '--stats'),
-    }.get(verbosity, ())
-
-    full_command = (
-        command, 'create',
-        '{repository}::{hostname}-{timestamp}'.format(
-            repository=repository,
-            hostname=platform.node(),
-            timestamp=datetime.now().isoformat(),
-        ),
-    ) + sources + exclude_flags + compression_flags + one_file_system_flags + \
-        remote_path_flags + umask_flags + verbosity_flags
-
-    subprocess.check_call(full_command)
-
-
-def _make_prune_flags(retention_config):
-    '''
-    Given a retention config dict mapping from option name to value, tranform it into an iterable of
-    command-line name-value flag pairs.
-
-    For example, given a retention config of:
-
-        {'keep_weekly': 4, 'keep_monthly': 6}
-
-    This will be returned as an iterable of:
-
-        (
-            ('--keep-weekly', '4'),
-            ('--keep-monthly', '6'),
-        )
-    '''
-    return (
-        ('--' + option_name.replace('_', '-'), str(retention_config[option_name]))
-        for option_name, value in retention_config.items()
-    )
-
-
-def prune_archives(verbosity, repository, retention_config, command=COMMAND, remote_path=None):
-    '''
-    Given a verbosity flag, a local or remote repository path, a retention config dict, and a
-    command to run, prune Borg archives according the the retention policy specified in that
-    configuration.
-    '''
-    remote_path_flags = ('--remote-path', remote_path) if remote_path else ()
-    verbosity_flags = {
-        VERBOSITY_SOME: ('--info', '--stats',),
-        VERBOSITY_LOTS: ('--debug', '--stats'),
-    }.get(verbosity, ())
-
-    full_command = (
-        command, 'prune',
-        repository,
-    ) + tuple(
-        element
-        for pair in _make_prune_flags(retention_config)
-        for element in pair
-    ) + remote_path_flags + verbosity_flags
-
-    subprocess.check_call(full_command)
-
-
-DEFAULT_CHECKS = ('repository', 'archives')
-
-
-def _parse_checks(consistency_config):
-    '''
-    Given a consistency config with a "checks" list, transform it to a tuple of named checks to run.
-
-    For example, given a retention config of:
-
-        {'checks': ['repository', 'archives']}
-
-    This will be returned as:
-
-        ('repository', 'archives')
-
-    If no "checks" option is present, return the DEFAULT_CHECKS. If the checks value is the string
-    "disabled", return an empty tuple, meaning that no checks should be run.
-    '''
-    checks = consistency_config.get('checks', [])
-    if checks == ['disabled']:
-        return ()
-
-    return tuple(check for check in checks if check.lower() not in ('disabled', '')) or DEFAULT_CHECKS
-
-
-def _make_check_flags(checks, check_last=None):
-    '''
-    Given a parsed sequence of checks, transform it into tuple of command-line flags.
-
-    For example, given parsed checks of:
-
-        ('repository',)
-
-    This will be returned as:
-
-        ('--repository-only',)
-
-    Additionally, if a check_last value is given, a "--last" flag will be added.
-    '''
-    last_flag = ('--last', str(check_last)) if check_last else ()
-    if checks == DEFAULT_CHECKS:
-        return last_flag
-
-    return tuple(
-        '--{}-only'.format(check) for check in checks
-        if check in DEFAULT_CHECKS
-    ) + last_flag
-
-
-def check_archives(verbosity, repository, consistency_config, command=COMMAND, remote_path=None):
-    '''
-    Given a verbosity flag, a local or remote repository path, a consistency config dict, and a
-    command to run, check the contained Borg archives for consistency.
-
-    If there are no consistency checks to run, skip running them.
-    '''
-    checks = _parse_checks(consistency_config)
-    check_last = consistency_config.get('check_last', None)
-
-    if set(checks).intersection(set(DEFAULT_CHECKS)):
-        remote_path_flags = ('--remote-path', remote_path) if remote_path else ()
-        verbosity_flags = {
-            VERBOSITY_SOME: ('--info',),
-            VERBOSITY_LOTS: ('--debug',),
-        }.get(verbosity, ())
-
-        full_command = (
-            command, 'check',
-            repository,
-        ) + _make_check_flags(checks, check_last) + remote_path_flags + verbosity_flags
-
-        # The check command spews to stdout/stderr even without the verbose flag. Suppress it.
-        stdout = None if verbosity_flags else open(os.devnull, 'w')
-
-        subprocess.check_call(full_command, stdout=stdout, stderr=subprocess.STDOUT)
-
-    if 'extract' in checks:
-        extract_last_archive_dry_run(verbosity, repository, command, remote_path)
-
-
-def extract_last_archive_dry_run(verbosity, repository, command=COMMAND, remote_path=None):
-    '''
-    Perform an extraction dry-run of just the most recent archive. If there are no archives, skip
-    the dry-run.
-    '''
-    remote_path_flags = ('--remote-path', remote_path) if remote_path else ()
-    verbosity_flags = {
-        VERBOSITY_SOME: ('--info',),
-        VERBOSITY_LOTS: ('--debug',),
-    }.get(verbosity, ())
-
-    full_list_command = (
-        command, 'list',
-        '--short',
-        repository,
-    ) + remote_path_flags + verbosity_flags
-
-    list_output = subprocess.check_output(full_list_command).decode(sys.stdout.encoding)
-
-    last_archive_name = list_output.strip().split('\n')[-1]
-    if not last_archive_name:
-        return
-
-    list_flag = ('--list',) if verbosity == VERBOSITY_LOTS else ()
-    full_extract_command = (
-        command, 'extract',
-        '--dry-run',
-        '{repository}::{last_archive_name}'.format(
-            repository=repository,
-            last_archive_name=last_archive_name,
-        ),
-    ) + remote_path_flags + verbosity_flags + list_flag
-
-    subprocess.check_call(full_extract_command)

+ 0 - 0
borgmatic/borg/__init__.py


+ 85 - 0
borgmatic/borg/check.py

@@ -0,0 +1,85 @@
+import os
+import subprocess
+
+from borgmatic.borg import extract
+from borgmatic.verbosity import VERBOSITY_SOME, VERBOSITY_LOTS
+
+
+DEFAULT_CHECKS = ('repository', 'archives')
+
+
+def _parse_checks(consistency_config):
+    '''
+    Given a consistency config with a "checks" list, transform it to a tuple of named checks to run.
+
+    For example, given a retention config of:
+
+        {'checks': ['repository', 'archives']}
+
+    This will be returned as:
+
+        ('repository', 'archives')
+
+    If no "checks" option is present, return the DEFAULT_CHECKS. If the checks value is the string
+    "disabled", return an empty tuple, meaning that no checks should be run.
+    '''
+    checks = consistency_config.get('checks', [])
+    if checks == ['disabled']:
+        return ()
+
+    return tuple(check for check in checks if check.lower() not in ('disabled', '')) or DEFAULT_CHECKS
+
+
+def _make_check_flags(checks, check_last=None):
+    '''
+    Given a parsed sequence of checks, transform it into tuple of command-line flags.
+
+    For example, given parsed checks of:
+
+        ('repository',)
+
+    This will be returned as:
+
+        ('--repository-only',)
+
+    Additionally, if a check_last value is given, a "--last" flag will be added.
+    '''
+    last_flag = ('--last', str(check_last)) if check_last else ()
+    if checks == DEFAULT_CHECKS:
+        return last_flag
+
+    return tuple(
+        '--{}-only'.format(check) for check in checks
+        if check in DEFAULT_CHECKS
+    ) + last_flag
+
+
+def check_archives(verbosity, repository, consistency_config, remote_path=None):
+    '''
+    Given a verbosity flag, a local or remote repository path, a consistency config dict, and a
+    command to run, check the contained Borg archives for consistency.
+
+    If there are no consistency checks to run, skip running them.
+    '''
+    checks = _parse_checks(consistency_config)
+    check_last = consistency_config.get('check_last', None)
+
+    if set(checks).intersection(set(DEFAULT_CHECKS)):
+        remote_path_flags = ('--remote-path', remote_path) if remote_path else ()
+        verbosity_flags = {
+            VERBOSITY_SOME: ('--info',),
+            VERBOSITY_LOTS: ('--debug',),
+        }.get(verbosity, ())
+
+        full_command = (
+            'borg', 'check',
+            repository,
+        ) + _make_check_flags(checks, check_last) + remote_path_flags + verbosity_flags
+
+        # The check command spews to stdout/stderr even without the verbose flag. Suppress it.
+        stdout = None if verbosity_flags else open(os.devnull, 'w')
+
+        subprocess.check_call(full_command, stdout=stdout, stderr=subprocess.STDOUT)
+
+    if 'extract' in checks:
+        extract.extract_last_archive_dry_run(verbosity, repository, remote_path)

+ 72 - 0
borgmatic/borg/create.py

@@ -0,0 +1,72 @@
+from datetime import datetime
+import glob
+import itertools
+import os
+import platform
+import subprocess
+import tempfile
+
+from borgmatic.verbosity import VERBOSITY_SOME, VERBOSITY_LOTS
+
+
+def initialize(storage_config):
+    passphrase = storage_config.get('encryption_passphrase')
+
+    if passphrase:
+        os.environ['BORG_PASSPHRASE'] = passphrase
+
+
+def _write_exclude_file(exclude_patterns=None):
+    '''
+    Given a sequence of exclude patterns, write them to a named temporary file and return it. Return
+    None if no patterns are provided.
+    '''
+    if not exclude_patterns:
+        return None
+
+    exclude_file = tempfile.NamedTemporaryFile('w')
+    exclude_file.write('\n'.join(exclude_patterns))
+    exclude_file.flush()
+
+    return exclude_file
+
+
+def create_archive(
+    verbosity, repository, location_config, storage_config,
+):
+    '''
+    Given a vebosity flag, a storage config dict, a list of source directories, a local or remote
+    repository path, a list of exclude patterns, create a Borg archive.
+    '''
+    sources = tuple(
+        itertools.chain.from_iterable(
+            glob.glob(directory) or [directory]
+            for directory in location_config['source_directories']
+        )
+    )
+
+    exclude_file = _write_exclude_file(location_config.get('exclude_patterns'))
+    exclude_flags = ('--exclude-from', exclude_file.name) if exclude_file else ()
+    compression = storage_config.get('compression', None)
+    compression_flags = ('--compression', compression) if compression else ()
+    umask = storage_config.get('umask', None)
+    umask_flags = ('--umask', str(umask)) if umask else ()
+    one_file_system_flags = ('--one-file-system',) if location_config.get('one_file_system') else ()
+    remote_path = location_config.get('remote_path')
+    remote_path_flags = ('--remote-path', remote_path) if remote_path else ()
+    verbosity_flags = {
+        VERBOSITY_SOME: ('--info', '--stats',),
+        VERBOSITY_LOTS: ('--debug', '--list', '--stats'),
+    }.get(verbosity, ())
+
+    full_command = (
+        'borg', 'create',
+        '{repository}::{hostname}-{timestamp}'.format(
+            repository=repository,
+            hostname=platform.node(),
+            timestamp=datetime.now().isoformat(),
+        ),
+    ) + sources + exclude_flags + compression_flags + one_file_system_flags + \
+        remote_path_flags + umask_flags + verbosity_flags
+
+    subprocess.check_call(full_command)

+ 40 - 0
borgmatic/borg/extract.py

@@ -0,0 +1,40 @@
+import sys
+import subprocess
+
+from borgmatic.verbosity import VERBOSITY_SOME, VERBOSITY_LOTS
+
+
+def extract_last_archive_dry_run(verbosity, repository, remote_path=None):
+    '''
+    Perform an extraction dry-run of just the most recent archive. If there are no archives, skip
+    the dry-run.
+    '''
+    remote_path_flags = ('--remote-path', remote_path) if remote_path else ()
+    verbosity_flags = {
+        VERBOSITY_SOME: ('--info',),
+        VERBOSITY_LOTS: ('--debug',),
+    }.get(verbosity, ())
+
+    full_list_command = (
+        'borg', 'list',
+        '--short',
+        repository,
+    ) + remote_path_flags + verbosity_flags
+
+    list_output = subprocess.check_output(full_list_command).decode(sys.stdout.encoding)
+
+    last_archive_name = list_output.strip().split('\n')[-1]
+    if not last_archive_name:
+        return
+
+    list_flag = ('--list',) if verbosity == VERBOSITY_LOTS else ()
+    full_extract_command = (
+        'borg', 'extract',
+        '--dry-run',
+        '{repository}::{last_archive_name}'.format(
+            repository=repository,
+            last_archive_name=last_archive_name,
+        ),
+    ) + remote_path_flags + verbosity_flags + list_flag
+
+    subprocess.check_call(full_extract_command)

+ 48 - 0
borgmatic/borg/prune.py

@@ -0,0 +1,48 @@
+import subprocess
+
+from borgmatic.verbosity import VERBOSITY_SOME, VERBOSITY_LOTS
+
+
+def _make_prune_flags(retention_config):
+    '''
+    Given a retention config dict mapping from option name to value, tranform it into an iterable of
+    command-line name-value flag pairs.
+
+    For example, given a retention config of:
+
+        {'keep_weekly': 4, 'keep_monthly': 6}
+
+    This will be returned as an iterable of:
+
+        (
+            ('--keep-weekly', '4'),
+            ('--keep-monthly', '6'),
+        )
+    '''
+    return (
+        ('--' + option_name.replace('_', '-'), str(retention_config[option_name]))
+        for option_name, value in retention_config.items()
+    )
+
+
+def prune_archives(verbosity, repository, retention_config, remote_path=None):
+    '''
+    Given a verbosity flag, a local or remote repository path, a retention config dict, prune Borg
+    archives according the the retention policy specified in that configuration.
+    '''
+    remote_path_flags = ('--remote-path', remote_path) if remote_path else ()
+    verbosity_flags = {
+        VERBOSITY_SOME: ('--info', '--stats',),
+        VERBOSITY_LOTS: ('--debug', '--stats'),
+    }.get(verbosity, ())
+
+    full_command = (
+        'borg', 'prune',
+        repository,
+    ) + tuple(
+        element
+        for pair in _make_prune_flags(retention_config)
+        for element in pair
+    ) + remote_path_flags + verbosity_flags
+
+    subprocess.check_call(full_command)

+ 0 - 0
borgmatic/tests/unit/borg/__init__.py


+ 185 - 0
borgmatic/tests/unit/borg/test_check.py

@@ -0,0 +1,185 @@
+from subprocess import STDOUT
+import sys
+
+from flexmock import flexmock
+import pytest
+
+from borgmatic.borg import check as module
+from borgmatic.verbosity import VERBOSITY_SOME, VERBOSITY_LOTS
+
+
+def insert_subprocess_mock(check_call_command, **kwargs):
+    subprocess = flexmock(module.subprocess)
+    subprocess.should_receive('check_call').with_args(check_call_command, **kwargs).once()
+
+
+def insert_subprocess_never():
+    subprocess = flexmock(module.subprocess)
+    subprocess.should_receive('check_call').never()
+
+
+def test_parse_checks_returns_them_as_tuple():
+    checks = module._parse_checks({'checks': ['foo', 'disabled', 'bar']})
+
+    assert checks == ('foo', 'bar')
+
+
+def test_parse_checks_with_missing_value_returns_defaults():
+    checks = module._parse_checks({})
+
+    assert checks == module.DEFAULT_CHECKS
+
+
+def test_parse_checks_with_blank_value_returns_defaults():
+    checks = module._parse_checks({'checks': []})
+
+    assert checks == module.DEFAULT_CHECKS
+
+
+def test_parse_checks_with_disabled_returns_no_checks():
+    checks = module._parse_checks({'checks': ['disabled']})
+
+    assert checks == ()
+
+
+def test_make_check_flags_with_checks_returns_flags():
+    flags = module._make_check_flags(('repository',))
+
+    assert flags == ('--repository-only',)
+
+
+def test_make_check_flags_with_extract_check_does_not_make_extract_flag():
+    flags = module._make_check_flags(('extract',))
+
+    assert flags == ()
+
+
+def test_make_check_flags_with_default_checks_returns_no_flags():
+    flags = module._make_check_flags(module.DEFAULT_CHECKS)
+
+    assert flags == ()
+
+
+def test_make_check_flags_with_checks_and_last_returns_flags_including_last():
+    flags = module._make_check_flags(('repository',), check_last=3)
+
+    assert flags == ('--repository-only', '--last', '3')
+
+
+def test_make_check_flags_with_default_checks_and_last_returns_last_flag():
+    flags = module._make_check_flags(module.DEFAULT_CHECKS, check_last=3)
+
+    assert flags == ('--last', '3')
+
+
+@pytest.mark.parametrize(
+    'checks',
+    (
+        ('repository',),
+        ('archives',),
+        ('repository', 'archives'),
+        ('repository', 'archives', 'other'),
+    ),
+)
+def test_check_archives_should_call_borg_with_parameters(checks):
+    check_last = flexmock()
+    consistency_config = flexmock().should_receive('get').and_return(check_last).mock
+    flexmock(module).should_receive('_parse_checks').and_return(checks)
+    flexmock(module).should_receive('_make_check_flags').with_args(checks, check_last).and_return(())
+    stdout = flexmock()
+    insert_subprocess_mock(
+        ('borg', 'check', 'repo'),
+        stdout=stdout, stderr=STDOUT,
+    )
+    flexmock(sys.modules['builtins']).should_receive('open').and_return(stdout)
+    flexmock(module.os).should_receive('devnull')
+
+    module.check_archives(
+        verbosity=None,
+        repository='repo',
+        consistency_config=consistency_config,
+    )
+
+
+def test_check_archives_with_extract_check_should_call_extract_only():
+    checks = ('extract',)
+    check_last = flexmock()
+    consistency_config = flexmock().should_receive('get').and_return(check_last).mock
+    flexmock(module).should_receive('_parse_checks').and_return(checks)
+    flexmock(module).should_receive('_make_check_flags').never()
+    flexmock(module.extract).should_receive('extract_last_archive_dry_run').once()
+    insert_subprocess_never()
+
+    module.check_archives(
+        verbosity=None,
+        repository='repo',
+        consistency_config=consistency_config,
+    )
+
+
+def test_check_archives_with_verbosity_some_should_call_borg_with_info_parameter():
+    checks = ('repository',)
+    consistency_config = flexmock().should_receive('get').and_return(None).mock
+    flexmock(module).should_receive('_parse_checks').and_return(checks)
+    flexmock(module).should_receive('_make_check_flags').and_return(())
+    insert_subprocess_mock(
+        ('borg', 'check', 'repo', '--info'),
+        stdout=None, stderr=STDOUT,
+    )
+
+    module.check_archives(
+        verbosity=VERBOSITY_SOME,
+        repository='repo',
+        consistency_config=consistency_config,
+    )
+
+
+def test_check_archives_with_verbosity_lots_should_call_borg_with_debug_parameter():
+    checks = ('repository',)
+    consistency_config = flexmock().should_receive('get').and_return(None).mock
+    flexmock(module).should_receive('_parse_checks').and_return(checks)
+    flexmock(module).should_receive('_make_check_flags').and_return(())
+    insert_subprocess_mock(
+        ('borg', 'check', 'repo', '--debug'),
+        stdout=None, stderr=STDOUT,
+    )
+
+    module.check_archives(
+        verbosity=VERBOSITY_LOTS,
+        repository='repo',
+        consistency_config=consistency_config,
+    )
+
+
+def test_check_archives_without_any_checks_should_bail():
+    consistency_config = flexmock().should_receive('get').and_return(None).mock
+    flexmock(module).should_receive('_parse_checks').and_return(())
+    insert_subprocess_never()
+
+    module.check_archives(
+        verbosity=None,
+        repository='repo',
+        consistency_config=consistency_config,
+    )
+
+
+def test_check_archives_with_remote_path_should_call_borg_with_remote_path_parameters():
+    checks = ('repository',)
+    check_last = flexmock()
+    consistency_config = flexmock().should_receive('get').and_return(check_last).mock
+    flexmock(module).should_receive('_parse_checks').and_return(checks)
+    flexmock(module).should_receive('_make_check_flags').with_args(checks, check_last).and_return(())
+    stdout = flexmock()
+    insert_subprocess_mock(
+        ('borg', 'check', 'repo', '--remote-path', 'borg1'),
+        stdout=stdout, stderr=STDOUT,
+    )
+    flexmock(sys.modules['builtins']).should_receive('open').and_return(stdout)
+    flexmock(module.os).should_receive('devnull')
+
+    module.check_archives(
+        verbosity=None,
+        repository='repo',
+        consistency_config=consistency_config,
+        remote_path='borg1',
+    )

+ 264 - 0
borgmatic/tests/unit/borg/test_create.py

@@ -0,0 +1,264 @@
+import os
+
+from flexmock import flexmock
+
+from borgmatic.borg import create as module
+from borgmatic.verbosity import VERBOSITY_SOME, VERBOSITY_LOTS
+
+
+def test_initialize_with_passphrase_should_set_environment():
+    orig_environ = os.environ
+
+    try:
+        os.environ = {}
+        module.initialize({'encryption_passphrase': 'pass'})
+        assert os.environ.get('BORG_PASSPHRASE') == 'pass'
+    finally:
+        os.environ = orig_environ
+
+
+def test_initialize_without_passphrase_should_not_set_environment():
+    orig_environ = os.environ
+
+    try:
+        os.environ = {}
+        module.initialize({})
+        assert os.environ.get('BORG_PASSPHRASE') == None
+    finally:
+        os.environ = orig_environ
+
+
+def test_write_exclude_file_does_not_raise():
+    temporary_file = flexmock(
+        name='filename',
+        write=lambda mode: None,
+        flush=lambda: None,
+    )
+    flexmock(module.tempfile).should_receive('NamedTemporaryFile').and_return(temporary_file)
+
+    module._write_exclude_file(['exclude'])
+
+
+def test_write_exclude_file_with_empty_exclude_patterns_does_not_raise():
+    module._write_exclude_file([])
+
+
+def insert_subprocess_mock(check_call_command, **kwargs):
+    subprocess = flexmock(module.subprocess)
+    subprocess.should_receive('check_call').with_args(check_call_command, **kwargs).once()
+
+
+def insert_platform_mock():
+    flexmock(module.platform).should_receive('node').and_return('host')
+
+
+def insert_datetime_mock():
+    flexmock(module).datetime = flexmock().should_receive('now').and_return(
+        flexmock().should_receive('isoformat').and_return('now').mock
+    ).mock
+
+
+CREATE_COMMAND = ('borg', 'create', 'repo::host-now', 'foo', 'bar')
+
+
+def test_create_archive_should_call_borg_with_parameters():
+    flexmock(module).should_receive('_write_exclude_file')
+    insert_subprocess_mock(CREATE_COMMAND)
+    insert_platform_mock()
+    insert_datetime_mock()
+
+    module.create_archive(
+        verbosity=None,
+        repository='repo',
+        location_config={
+            'source_directories': ['foo', 'bar'],
+            'repositories': ['repo'],
+            'exclude_patterns': None,
+        },
+        storage_config={},
+    )
+
+
+def test_create_archive_with_exclude_patterns_should_call_borg_with_excludes():
+    flexmock(module).should_receive('_write_exclude_file').and_return(flexmock(name='excludes'))
+    insert_subprocess_mock(CREATE_COMMAND + ('--exclude-from', 'excludes'))
+    insert_platform_mock()
+    insert_datetime_mock()
+
+    module.create_archive(
+        verbosity=None,
+        repository='repo',
+        location_config={
+            'source_directories': ['foo', 'bar'],
+            'repositories': ['repo'],
+            'exclude_patterns': ['exclude'],
+        },
+        storage_config={},
+    )
+
+
+def test_create_archive_with_verbosity_some_should_call_borg_with_info_parameter():
+    flexmock(module).should_receive('_write_exclude_file')
+    insert_subprocess_mock(CREATE_COMMAND + ('--info', '--stats',))
+    insert_platform_mock()
+    insert_datetime_mock()
+
+    module.create_archive(
+        verbosity=VERBOSITY_SOME,
+        repository='repo',
+        location_config={
+            'source_directories': ['foo', 'bar'],
+            'repositories': ['repo'],
+            'exclude_patterns': None,
+        },
+        storage_config={},
+    )
+
+
+def test_create_archive_with_verbosity_lots_should_call_borg_with_debug_parameter():
+    flexmock(module).should_receive('_write_exclude_file')
+    insert_subprocess_mock(CREATE_COMMAND + ('--debug', '--list', '--stats'))
+    insert_platform_mock()
+    insert_datetime_mock()
+
+    module.create_archive(
+        verbosity=VERBOSITY_LOTS,
+        repository='repo',
+        location_config={
+            'source_directories': ['foo', 'bar'],
+            'repositories': ['repo'],
+            'exclude_patterns': None,
+        },
+        storage_config={},
+    )
+
+
+def test_create_archive_with_compression_should_call_borg_with_compression_parameters():
+    flexmock(module).should_receive('_write_exclude_file')
+    insert_subprocess_mock(CREATE_COMMAND + ('--compression', 'rle'))
+    insert_platform_mock()
+    insert_datetime_mock()
+
+    module.create_archive(
+        verbosity=None,
+        repository='repo',
+        location_config={
+            'source_directories': ['foo', 'bar'],
+            'repositories': ['repo'],
+            'exclude_patterns': None,
+        },
+        storage_config={'compression': 'rle'},
+    )
+
+
+def test_create_archive_with_one_file_system_should_call_borg_with_one_file_system_parameters():
+    flexmock(module).should_receive('_write_exclude_file')
+    insert_subprocess_mock(CREATE_COMMAND + ('--one-file-system',))
+    insert_platform_mock()
+    insert_datetime_mock()
+
+    module.create_archive(
+        verbosity=None,
+        repository='repo',
+        location_config={
+            'source_directories': ['foo', 'bar'],
+            'repositories': ['repo'],
+            'one_file_system': True,
+            'exclude_patterns': None,
+        },
+        storage_config={},
+    )
+
+
+def test_create_archive_with_remote_path_should_call_borg_with_remote_path_parameters():
+    flexmock(module).should_receive('_write_exclude_file')
+    insert_subprocess_mock(CREATE_COMMAND + ('--remote-path', 'borg1'))
+    insert_platform_mock()
+    insert_datetime_mock()
+
+    module.create_archive(
+        verbosity=None,
+        repository='repo',
+        location_config={
+            'source_directories': ['foo', 'bar'],
+            'repositories': ['repo'],
+            'remote_path': 'borg1',
+            'exclude_patterns': None,
+        },
+        storage_config={},
+    )
+
+
+def test_create_archive_with_umask_should_call_borg_with_umask_parameters():
+    flexmock(module).should_receive('_write_exclude_file')
+    insert_subprocess_mock(CREATE_COMMAND + ('--umask', '740'))
+    insert_platform_mock()
+    insert_datetime_mock()
+
+    module.create_archive(
+        verbosity=None,
+        repository='repo',
+        location_config={
+            'source_directories': ['foo', 'bar'],
+            'repositories': ['repo'],
+            'exclude_patterns': None,
+        },
+        storage_config={'umask': 740},
+    )
+
+
+def test_create_archive_with_source_directories_glob_expands():
+    flexmock(module).should_receive('_write_exclude_file')
+    insert_subprocess_mock(('borg', 'create', 'repo::host-now', 'foo', 'food'))
+    insert_platform_mock()
+    insert_datetime_mock()
+    flexmock(module.glob).should_receive('glob').with_args('foo*').and_return(['foo', 'food'])
+
+    module.create_archive(
+        verbosity=None,
+        repository='repo',
+        location_config={
+            'source_directories': ['foo*'],
+            'repositories': ['repo'],
+            'exclude_patterns': None,
+        },
+        storage_config={},
+    )
+
+
+def test_create_archive_with_non_matching_source_directories_glob_passes_through():
+    flexmock(module).should_receive('_write_exclude_file')
+    insert_subprocess_mock(('borg', 'create', 'repo::host-now', 'foo*'))
+    insert_platform_mock()
+    insert_datetime_mock()
+    flexmock(module.glob).should_receive('glob').with_args('foo*').and_return([])
+
+    module.create_archive(
+        verbosity=None,
+        repository='repo',
+        location_config={
+            'source_directories': ['foo*'],
+            'repositories': ['repo'],
+            'exclude_patterns': None,
+        },
+        storage_config={},
+    )
+
+
+def test_create_archive_with_glob_should_call_borg_with_expanded_directories():
+    flexmock(module).should_receive('_write_exclude_file')
+    insert_subprocess_mock(('borg', 'create', 'repo::host-now', 'foo', 'food'))
+    insert_platform_mock()
+    insert_datetime_mock()
+    flexmock(module.glob).should_receive('glob').with_args('foo*').and_return(['foo', 'food'])
+
+    module.create_archive(
+        verbosity=None,
+        repository='repo',
+        location_config={
+            'source_directories': ['foo*'],
+            'repositories': ['repo'],
+            'exclude_patterns': None,
+        },
+        storage_config={},
+    )

+ 100 - 0
borgmatic/tests/unit/borg/test_extract.py

@@ -0,0 +1,100 @@
+import sys
+
+from flexmock import flexmock
+
+from borgmatic.borg import extract as module
+from borgmatic.verbosity import VERBOSITY_SOME, VERBOSITY_LOTS
+
+
+def insert_subprocess_mock(check_call_command, **kwargs):
+    subprocess = flexmock(module.subprocess)
+    subprocess.should_receive('check_call').with_args(check_call_command, **kwargs).once()
+
+
+def insert_subprocess_never():
+    subprocess = flexmock(module.subprocess)
+    subprocess.should_receive('check_call').never()
+
+
+def insert_subprocess_check_output_mock(check_output_command, result, **kwargs):
+    subprocess = flexmock(module.subprocess)
+    subprocess.should_receive('check_output').with_args(check_output_command, **kwargs).and_return(result).once()
+
+
+def test_extract_last_archive_dry_run_should_call_borg_with_last_archive():
+    flexmock(sys.stdout).encoding = 'utf-8'
+    insert_subprocess_check_output_mock(
+        ('borg', 'list', '--short', 'repo'),
+        result='archive1\narchive2\n'.encode('utf-8'),
+    )
+    insert_subprocess_mock(
+        ('borg', 'extract', '--dry-run', 'repo::archive2'),
+    )
+
+    module.extract_last_archive_dry_run(
+        verbosity=None,
+        repository='repo',
+    )
+
+
+def test_extract_last_archive_dry_run_without_any_archives_should_bail():
+    flexmock(sys.stdout).encoding = 'utf-8'
+    insert_subprocess_check_output_mock(
+        ('borg', 'list', '--short', 'repo'),
+        result='\n'.encode('utf-8'),
+    )
+    insert_subprocess_never()
+
+    module.extract_last_archive_dry_run(
+        verbosity=None,
+        repository='repo',
+    )
+
+
+def test_extract_last_archive_dry_run_with_verbosity_some_should_call_borg_with_info_parameter():
+    flexmock(sys.stdout).encoding = 'utf-8'
+    insert_subprocess_check_output_mock(
+        ('borg', 'list', '--short', 'repo', '--info'),
+        result='archive1\narchive2\n'.encode('utf-8'),
+    )
+    insert_subprocess_mock(
+        ('borg', 'extract', '--dry-run', 'repo::archive2', '--info'),
+    )
+
+    module.extract_last_archive_dry_run(
+        verbosity=VERBOSITY_SOME,
+        repository='repo',
+    )
+
+
+def test_extract_last_archive_dry_run_with_verbosity_lots_should_call_borg_with_debug_parameter():
+    flexmock(sys.stdout).encoding = 'utf-8'
+    insert_subprocess_check_output_mock(
+        ('borg', 'list', '--short', 'repo', '--debug'),
+        result='archive1\narchive2\n'.encode('utf-8'),
+    )
+    insert_subprocess_mock(
+        ('borg', 'extract', '--dry-run', 'repo::archive2', '--debug', '--list'),
+    )
+
+    module.extract_last_archive_dry_run(
+        verbosity=VERBOSITY_LOTS,
+        repository='repo',
+    )
+
+
+def test_extract_last_archive_dry_run_should_call_borg_with_remote_path_parameters():
+    flexmock(sys.stdout).encoding = 'utf-8'
+    insert_subprocess_check_output_mock(
+        ('borg', 'list', '--short', 'repo', '--remote-path', 'borg1'),
+        result='archive1\narchive2\n'.encode('utf-8'),
+    )
+    insert_subprocess_mock(
+        ('borg', 'extract', '--dry-run', 'repo::archive2', '--remote-path', 'borg1'),
+    )
+
+    module.extract_last_archive_dry_run(
+        verbosity=None,
+        repository='repo',
+        remote_path='borg1',
+    )

+ 93 - 0
borgmatic/tests/unit/borg/test_prune.py

@@ -0,0 +1,93 @@
+from collections import OrderedDict
+
+from flexmock import flexmock
+
+from borgmatic.borg import prune as module
+from borgmatic.verbosity import VERBOSITY_SOME, VERBOSITY_LOTS
+
+
+def insert_subprocess_mock(check_call_command, **kwargs):
+    subprocess = flexmock(module.subprocess)
+    subprocess.should_receive('check_call').with_args(check_call_command, **kwargs).once()
+
+
+BASE_PRUNE_FLAGS = (
+    ('--keep-daily', '1'),
+    ('--keep-weekly', '2'),
+    ('--keep-monthly', '3'),
+)
+
+
+def test_make_prune_flags_should_return_flags_from_config():
+    retention_config = OrderedDict(
+        (
+            ('keep_daily', 1),
+            ('keep_weekly', 2),
+            ('keep_monthly', 3),
+        )
+    )
+
+    result = module._make_prune_flags(retention_config)
+
+    assert tuple(result) == BASE_PRUNE_FLAGS
+
+
+PRUNE_COMMAND = (
+    'borg', 'prune', 'repo', '--keep-daily', '1', '--keep-weekly', '2', '--keep-monthly', '3',
+)
+
+
+def test_prune_archives_should_call_borg_with_parameters():
+    retention_config = flexmock()
+    flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
+        BASE_PRUNE_FLAGS,
+    )
+    insert_subprocess_mock(PRUNE_COMMAND)
+
+    module.prune_archives(
+        verbosity=None,
+        repository='repo',
+        retention_config=retention_config,
+    )
+
+
+def test_prune_archives_with_verbosity_some_should_call_borg_with_info_parameter():
+    retention_config = flexmock()
+    flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
+        BASE_PRUNE_FLAGS,
+    )
+    insert_subprocess_mock(PRUNE_COMMAND + ('--info', '--stats',))
+
+    module.prune_archives(
+        repository='repo',
+        verbosity=VERBOSITY_SOME,
+        retention_config=retention_config,
+    )
+
+
+def test_prune_archives_with_verbosity_lots_should_call_borg_with_debug_parameter():
+    retention_config = flexmock()
+    flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
+        BASE_PRUNE_FLAGS,
+    )
+    insert_subprocess_mock(PRUNE_COMMAND + ('--debug', '--stats',))
+
+    module.prune_archives(
+        repository='repo',
+        verbosity=VERBOSITY_LOTS,
+        retention_config=retention_config,
+    )
+
+def test_prune_archives_with_remote_path_should_call_borg_with_remote_path_parameters():
+    retention_config = flexmock()
+    flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
+        BASE_PRUNE_FLAGS,
+    )
+    insert_subprocess_mock(PRUNE_COMMAND + ('--remote-path', 'borg1'))
+
+    module.prune_archives(
+        verbosity=None,
+        repository='repo',
+        retention_config=retention_config,
+        remote_path='borg1',
+    )

+ 0 - 642
borgmatic/tests/unit/test_borg.py

@@ -1,642 +0,0 @@
-from collections import OrderedDict
-from subprocess import STDOUT
-import sys
-import os
-
-from flexmock import flexmock
-import pytest
-
-from borgmatic import borg as module
-from borgmatic.verbosity import VERBOSITY_SOME, VERBOSITY_LOTS
-
-
-def test_initialize_with_passphrase_should_set_environment():
-    orig_environ = os.environ
-
-    try:
-        os.environ = {}
-        module.initialize({'encryption_passphrase': 'pass'}, command='borg')
-        assert os.environ.get('BORG_PASSPHRASE') == 'pass'
-    finally:
-        os.environ = orig_environ
-
-
-def test_initialize_without_passphrase_should_not_set_environment():
-    orig_environ = os.environ
-
-    try:
-        os.environ = {}
-        module.initialize({}, command='borg')
-        assert os.environ.get('BORG_PASSPHRASE') == None
-    finally:
-        os.environ = orig_environ
-
-def test_write_exclude_file_does_not_raise():
-    temporary_file = flexmock(
-        name='filename',
-        write=lambda mode: None,
-        flush=lambda: None,
-    )
-    flexmock(module.tempfile).should_receive('NamedTemporaryFile').and_return(temporary_file)
-
-    module._write_exclude_file(['exclude'])
-
-
-def test_write_exclude_file_with_empty_exclude_patterns_does_not_raise():
-    module._write_exclude_file([])
-
-
-def insert_subprocess_mock(check_call_command, **kwargs):
-    subprocess = flexmock(module.subprocess)
-    subprocess.should_receive('check_call').with_args(check_call_command, **kwargs).once()
-    flexmock(module).subprocess = subprocess
-
-
-def insert_subprocess_never():
-    subprocess = flexmock(module.subprocess)
-    subprocess.should_receive('check_call').never()
-    flexmock(module).subprocess = subprocess
-
-
-def insert_subprocess_check_output_mock(check_output_command, result, **kwargs):
-    subprocess = flexmock(module.subprocess)
-    subprocess.should_receive('check_output').with_args(check_output_command, **kwargs).and_return(result).once()
-    flexmock(module).subprocess = subprocess
-
-
-def insert_platform_mock():
-    flexmock(module.platform).should_receive('node').and_return('host')
-
-
-def insert_datetime_mock():
-    flexmock(module).datetime = flexmock().should_receive('now').and_return(
-        flexmock().should_receive('isoformat').and_return('now').mock
-    ).mock
-
-
-CREATE_COMMAND = ('borg', 'create', 'repo::host-now', 'foo', 'bar')
-
-
-def test_create_archive_should_call_borg_with_parameters():
-    flexmock(module).should_receive('_write_exclude_file')
-    insert_subprocess_mock(CREATE_COMMAND)
-    insert_platform_mock()
-    insert_datetime_mock()
-
-    module.create_archive(
-        verbosity=None,
-        repository='repo',
-        location_config={
-            'source_directories': ['foo', 'bar'],
-            'repositories': ['repo'],
-            'exclude_patterns': None,
-        },
-        storage_config={},
-        command='borg',
-    )
-
-
-def test_create_archive_with_exclude_patterns_should_call_borg_with_excludes():
-    flexmock(module).should_receive('_write_exclude_file').and_return(flexmock(name='excludes'))
-    insert_subprocess_mock(CREATE_COMMAND + ('--exclude-from', 'excludes'))
-    insert_platform_mock()
-    insert_datetime_mock()
-
-    module.create_archive(
-        verbosity=None,
-        repository='repo',
-        location_config={
-            'source_directories': ['foo', 'bar'],
-            'repositories': ['repo'],
-            'exclude_patterns': ['exclude'],
-        },
-        storage_config={},
-        command='borg',
-    )
-
-
-def test_create_archive_with_verbosity_some_should_call_borg_with_info_parameter():
-    flexmock(module).should_receive('_write_exclude_file')
-    insert_subprocess_mock(CREATE_COMMAND + ('--info', '--stats',))
-    insert_platform_mock()
-    insert_datetime_mock()
-
-    module.create_archive(
-        verbosity=VERBOSITY_SOME,
-        repository='repo',
-        location_config={
-            'source_directories': ['foo', 'bar'],
-            'repositories': ['repo'],
-            'exclude_patterns': None,
-        },
-        storage_config={},
-        command='borg',
-    )
-
-
-def test_create_archive_with_verbosity_lots_should_call_borg_with_debug_parameter():
-    flexmock(module).should_receive('_write_exclude_file')
-    insert_subprocess_mock(CREATE_COMMAND + ('--debug', '--list', '--stats'))
-    insert_platform_mock()
-    insert_datetime_mock()
-
-    module.create_archive(
-        verbosity=VERBOSITY_LOTS,
-        repository='repo',
-        location_config={
-            'source_directories': ['foo', 'bar'],
-            'repositories': ['repo'],
-            'exclude_patterns': None,
-        },
-        storage_config={},
-        command='borg',
-    )
-
-
-def test_create_archive_with_compression_should_call_borg_with_compression_parameters():
-    flexmock(module).should_receive('_write_exclude_file')
-    insert_subprocess_mock(CREATE_COMMAND + ('--compression', 'rle'))
-    insert_platform_mock()
-    insert_datetime_mock()
-
-    module.create_archive(
-        verbosity=None,
-        repository='repo',
-        location_config={
-            'source_directories': ['foo', 'bar'],
-            'repositories': ['repo'],
-            'exclude_patterns': None,
-        },
-        storage_config={'compression': 'rle'},
-        command='borg',
-    )
-
-
-def test_create_archive_with_one_file_system_should_call_borg_with_one_file_system_parameters():
-    flexmock(module).should_receive('_write_exclude_file')
-    insert_subprocess_mock(CREATE_COMMAND + ('--one-file-system',))
-    insert_platform_mock()
-    insert_datetime_mock()
-
-    module.create_archive(
-        verbosity=None,
-        repository='repo',
-        location_config={
-            'source_directories': ['foo', 'bar'],
-            'repositories': ['repo'],
-            'one_file_system': True,
-            'exclude_patterns': None,
-        },
-        storage_config={},
-        command='borg',
-    )
-
-
-def test_create_archive_with_remote_path_should_call_borg_with_remote_path_parameters():
-    flexmock(module).should_receive('_write_exclude_file')
-    insert_subprocess_mock(CREATE_COMMAND + ('--remote-path', 'borg1'))
-    insert_platform_mock()
-    insert_datetime_mock()
-
-    module.create_archive(
-        verbosity=None,
-        repository='repo',
-        location_config={
-            'source_directories': ['foo', 'bar'],
-            'repositories': ['repo'],
-            'remote_path': 'borg1',
-            'exclude_patterns': None,
-        },
-        storage_config={},
-        command='borg',
-    )
-
-
-def test_create_archive_with_umask_should_call_borg_with_umask_parameters():
-    flexmock(module).should_receive('_write_exclude_file')
-    insert_subprocess_mock(CREATE_COMMAND + ('--umask', '740'))
-    insert_platform_mock()
-    insert_datetime_mock()
-
-    module.create_archive(
-        verbosity=None,
-        repository='repo',
-        location_config={
-            'source_directories': ['foo', 'bar'],
-            'repositories': ['repo'],
-            'exclude_patterns': None,
-        },
-        storage_config={'umask': 740},
-        command='borg',
-    )
-
-
-def test_create_archive_with_source_directories_glob_expands():
-    flexmock(module).should_receive('_write_exclude_file')
-    insert_subprocess_mock(('borg', 'create', 'repo::host-now', 'foo', 'food'))
-    insert_platform_mock()
-    insert_datetime_mock()
-    flexmock(module.glob).should_receive('glob').with_args('foo*').and_return(['foo', 'food'])
-
-    module.create_archive(
-        verbosity=None,
-        repository='repo',
-        location_config={
-            'source_directories': ['foo*'],
-            'repositories': ['repo'],
-            'exclude_patterns': None,
-        },
-        storage_config={},
-        command='borg',
-    )
-
-
-def test_create_archive_with_non_matching_source_directories_glob_passes_through():
-    flexmock(module).should_receive('_write_exclude_file')
-    insert_subprocess_mock(('borg', 'create', 'repo::host-now', 'foo*'))
-    insert_platform_mock()
-    insert_datetime_mock()
-    flexmock(module.glob).should_receive('glob').with_args('foo*').and_return([])
-
-    module.create_archive(
-        verbosity=None,
-        repository='repo',
-        location_config={
-            'source_directories': ['foo*'],
-            'repositories': ['repo'],
-            'exclude_patterns': None,
-        },
-        storage_config={},
-        command='borg',
-    )
-
-
-def test_create_archive_with_glob_should_call_borg_with_expanded_directories():
-    flexmock(module).should_receive('_write_exclude_file')
-    insert_subprocess_mock(('borg', 'create', 'repo::host-now', 'foo', 'food'))
-    insert_platform_mock()
-    insert_datetime_mock()
-    flexmock(module.glob).should_receive('glob').with_args('foo*').and_return(['foo', 'food'])
-
-    module.create_archive(
-        verbosity=None,
-        repository='repo',
-        location_config={
-            'source_directories': ['foo*'],
-            'repositories': ['repo'],
-            'exclude_patterns': None,
-        },
-        storage_config={},
-        command='borg',
-    )
-
-
-BASE_PRUNE_FLAGS = (
-    ('--keep-daily', '1'),
-    ('--keep-weekly', '2'),
-    ('--keep-monthly', '3'),
-)
-
-
-def test_make_prune_flags_should_return_flags_from_config():
-    retention_config = OrderedDict(
-        (
-            ('keep_daily', 1),
-            ('keep_weekly', 2),
-            ('keep_monthly', 3),
-        )
-    )
-
-    result = module._make_prune_flags(retention_config)
-
-    assert tuple(result) == BASE_PRUNE_FLAGS
-
-
-PRUNE_COMMAND = (
-    'borg', 'prune', 'repo', '--keep-daily', '1', '--keep-weekly', '2', '--keep-monthly', '3',
-)
-
-
-def test_prune_archives_should_call_borg_with_parameters():
-    retention_config = flexmock()
-    flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
-        BASE_PRUNE_FLAGS,
-    )
-    insert_subprocess_mock(PRUNE_COMMAND)
-
-    module.prune_archives(
-        verbosity=None,
-        repository='repo',
-        retention_config=retention_config,
-        command='borg',
-    )
-
-
-def test_prune_archives_with_verbosity_some_should_call_borg_with_info_parameter():
-    retention_config = flexmock()
-    flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
-        BASE_PRUNE_FLAGS,
-    )
-    insert_subprocess_mock(PRUNE_COMMAND + ('--info', '--stats',))
-
-    module.prune_archives(
-        repository='repo',
-        verbosity=VERBOSITY_SOME,
-        retention_config=retention_config,
-        command='borg',
-    )
-
-
-def test_prune_archives_with_verbosity_lots_should_call_borg_with_debug_parameter():
-    retention_config = flexmock()
-    flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
-        BASE_PRUNE_FLAGS,
-    )
-    insert_subprocess_mock(PRUNE_COMMAND + ('--debug', '--stats',))
-
-    module.prune_archives(
-        repository='repo',
-        verbosity=VERBOSITY_LOTS,
-        retention_config=retention_config,
-        command='borg',
-    )
-
-def test_prune_archive_with_remote_path_should_call_borg_with_remote_path_parameters():
-    retention_config = flexmock()
-    flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
-        BASE_PRUNE_FLAGS,
-    )
-    insert_subprocess_mock(PRUNE_COMMAND + ('--remote-path', 'borg1'))
-
-    module.prune_archives(
-        verbosity=None,
-        repository='repo',
-        retention_config=retention_config,
-        command='borg',
-        remote_path='borg1',
-    )
-
-
-def test_parse_checks_returns_them_as_tuple():
-    checks = module._parse_checks({'checks': ['foo', 'disabled', 'bar']})
-
-    assert checks == ('foo', 'bar')
-
-
-def test_parse_checks_with_missing_value_returns_defaults():
-    checks = module._parse_checks({})
-
-    assert checks == module.DEFAULT_CHECKS
-
-
-def test_parse_checks_with_blank_value_returns_defaults():
-    checks = module._parse_checks({'checks': []})
-
-    assert checks == module.DEFAULT_CHECKS
-
-
-def test_parse_checks_with_disabled_returns_no_checks():
-    checks = module._parse_checks({'checks': ['disabled']})
-
-    assert checks == ()
-
-
-def test_make_check_flags_with_checks_returns_flags():
-    flags = module._make_check_flags(('repository',))
-
-    assert flags == ('--repository-only',)
-
-
-def test_make_check_flags_with_extract_check_does_not_make_extract_flag():
-    flags = module._make_check_flags(('extract',))
-
-    assert flags == ()
-
-
-def test_make_check_flags_with_default_checks_returns_no_flags():
-    flags = module._make_check_flags(module.DEFAULT_CHECKS)
-
-    assert flags == ()
-
-
-def test_make_check_flags_with_checks_and_last_returns_flags_including_last():
-    flags = module._make_check_flags(('repository',), check_last=3)
-
-    assert flags == ('--repository-only', '--last', '3')
-
-
-def test_make_check_flags_with_default_checks_and_last_returns_last_flag():
-    flags = module._make_check_flags(module.DEFAULT_CHECKS, check_last=3)
-
-    assert flags == ('--last', '3')
-
-
-@pytest.mark.parametrize(
-    'checks',
-    (
-        ('repository',),
-        ('archives',),
-        ('repository', 'archives'),
-        ('repository', 'archives', 'other'),
-    ),
-)
-def test_check_archives_should_call_borg_with_parameters(checks):
-    check_last = flexmock()
-    consistency_config = flexmock().should_receive('get').and_return(check_last).mock
-    flexmock(module).should_receive('_parse_checks').and_return(checks)
-    flexmock(module).should_receive('_make_check_flags').with_args(checks, check_last).and_return(())
-    stdout = flexmock()
-    insert_subprocess_mock(
-        ('borg', 'check', 'repo'),
-        stdout=stdout, stderr=STDOUT,
-    )
-    insert_platform_mock()
-    insert_datetime_mock()
-    flexmock(sys.modules['builtins']).should_receive('open').and_return(stdout)
-    flexmock(module.os).should_receive('devnull')
-
-    module.check_archives(
-        verbosity=None,
-        repository='repo',
-        consistency_config=consistency_config,
-        command='borg',
-    )
-
-
-def test_check_archives_with_extract_check_should_call_extract_only():
-    checks = ('extract',)
-    check_last = flexmock()
-    consistency_config = flexmock().should_receive('get').and_return(check_last).mock
-    flexmock(module).should_receive('_parse_checks').and_return(checks)
-    flexmock(module).should_receive('_make_check_flags').never()
-    flexmock(module).should_receive('extract_last_archive_dry_run').once()
-    insert_subprocess_never()
-
-    module.check_archives(
-        verbosity=None,
-        repository='repo',
-        consistency_config=consistency_config,
-        command='borg',
-    )
-
-
-def test_check_archives_with_verbosity_some_should_call_borg_with_info_parameter():
-    checks = ('repository',)
-    consistency_config = flexmock().should_receive('get').and_return(None).mock
-    flexmock(module).should_receive('_parse_checks').and_return(checks)
-    flexmock(module).should_receive('_make_check_flags').and_return(())
-    insert_subprocess_mock(
-        ('borg', 'check', 'repo', '--info'),
-        stdout=None, stderr=STDOUT,
-    )
-    insert_platform_mock()
-    insert_datetime_mock()
-
-    module.check_archives(
-        verbosity=VERBOSITY_SOME,
-        repository='repo',
-        consistency_config=consistency_config,
-        command='borg',
-    )
-
-
-def test_check_archives_with_verbosity_lots_should_call_borg_with_debug_parameter():
-    checks = ('repository',)
-    consistency_config = flexmock().should_receive('get').and_return(None).mock
-    flexmock(module).should_receive('_parse_checks').and_return(checks)
-    flexmock(module).should_receive('_make_check_flags').and_return(())
-    insert_subprocess_mock(
-        ('borg', 'check', 'repo', '--debug'),
-        stdout=None, stderr=STDOUT,
-    )
-    insert_platform_mock()
-    insert_datetime_mock()
-
-    module.check_archives(
-        verbosity=VERBOSITY_LOTS,
-        repository='repo',
-        consistency_config=consistency_config,
-        command='borg',
-    )
-
-
-def test_check_archives_without_any_checks_should_bail():
-    consistency_config = flexmock().should_receive('get').and_return(None).mock
-    flexmock(module).should_receive('_parse_checks').and_return(())
-    insert_subprocess_never()
-
-    module.check_archives(
-        verbosity=None,
-        repository='repo',
-        consistency_config=consistency_config,
-        command='borg',
-    )
-
-
-def test_check_archives_with_remote_path_should_call_borg_with_remote_path_parameters():
-    checks = ('repository',)
-    check_last = flexmock()
-    consistency_config = flexmock().should_receive('get').and_return(check_last).mock
-    flexmock(module).should_receive('_parse_checks').and_return(checks)
-    flexmock(module).should_receive('_make_check_flags').with_args(checks, check_last).and_return(())
-    stdout = flexmock()
-    insert_subprocess_mock(
-        ('borg', 'check', 'repo', '--remote-path', 'borg1'),
-        stdout=stdout, stderr=STDOUT,
-    )
-    insert_platform_mock()
-    insert_datetime_mock()
-    flexmock(sys.modules['builtins']).should_receive('open').and_return(stdout)
-    flexmock(module.os).should_receive('devnull')
-
-    module.check_archives(
-        verbosity=None,
-        repository='repo',
-        consistency_config=consistency_config,
-        command='borg',
-        remote_path='borg1',
-    )
-
-
-def test_extract_last_archive_dry_run_should_call_borg_with_last_archive():
-    flexmock(sys.stdout).encoding = 'utf-8'
-    insert_subprocess_check_output_mock(
-        ('borg', 'list', '--short', 'repo'),
-        result='archive1\narchive2\n'.encode('utf-8'),
-    )
-    insert_subprocess_mock(
-        ('borg', 'extract', '--dry-run', 'repo::archive2'),
-    )
-
-    module.extract_last_archive_dry_run(
-        verbosity=None,
-        repository='repo',
-        command='borg',
-    )
-
-
-def test_extract_last_archive_dry_run_without_any_archives_should_bail():
-    flexmock(sys.stdout).encoding = 'utf-8'
-    insert_subprocess_check_output_mock(
-        ('borg', 'list', '--short', 'repo'),
-        result='\n'.encode('utf-8'),
-    )
-    insert_subprocess_never()
-
-    module.extract_last_archive_dry_run(
-        verbosity=None,
-        repository='repo',
-        command='borg',
-    )
-
-
-def test_extract_last_archive_dry_run_with_verbosity_some_should_call_borg_with_info_parameter():
-    flexmock(sys.stdout).encoding = 'utf-8'
-    insert_subprocess_check_output_mock(
-        ('borg', 'list', '--short', 'repo', '--info'),
-        result='archive1\narchive2\n'.encode('utf-8'),
-    )
-    insert_subprocess_mock(
-        ('borg', 'extract', '--dry-run', 'repo::archive2', '--info'),
-    )
-
-    module.extract_last_archive_dry_run(
-        verbosity=VERBOSITY_SOME,
-        repository='repo',
-        command='borg',
-    )
-
-
-def test_extract_last_archive_dry_run_with_verbosity_lots_should_call_borg_with_debug_parameter():
-    flexmock(sys.stdout).encoding = 'utf-8'
-    insert_subprocess_check_output_mock(
-        ('borg', 'list', '--short', 'repo', '--debug'),
-        result='archive1\narchive2\n'.encode('utf-8'),
-    )
-    insert_subprocess_mock(
-        ('borg', 'extract', '--dry-run', 'repo::archive2', '--debug', '--list'),
-    )
-
-    module.extract_last_archive_dry_run(
-        verbosity=VERBOSITY_LOTS,
-        repository='repo',
-        command='borg',
-    )
-
-
-def test_extract_last_archive_dry_run_should_call_borg_with_remote_path_parameters():
-    flexmock(sys.stdout).encoding = 'utf-8'
-    insert_subprocess_check_output_mock(
-        ('borg', 'list', '--short', 'repo', '--remote-path', 'borg1'),
-        result='archive1\narchive2\n'.encode('utf-8'),
-    )
-    insert_subprocess_mock(
-        ('borg', 'extract', '--dry-run', 'repo::archive2', '--remote-path', 'borg1'),
-    )
-
-    module.extract_last_archive_dry_run(
-        verbosity=None,
-        repository='repo',
-        command='borg',
-        remote_path='borg1',
-    )