Compare commits

...
55 Commits
Author SHA1 Message Date
Dan Helfman 9417359da3 Fix for regression with missing verbose log entries (#177). 2019-05-16 10:50:19 -07:00
Dan Helfman 1cf0e1bd84 Support for various Borg directory environment variables (#153). 2019-05-16 10:34:52 -07:00
Dan Helfman 223f803e87 Fix formatting. 2019-05-14 13:09:36 -07:00
Dan Helfman 6cb901d083 Bump version for release. 2019-05-14 13:07:49 -07:00
Dan HelfmanandGitea 096be14230 Run tests for all installed versions of Python (#166). 2019-05-14 20:06:08 +00:00
Dan Helfman bb8b1e58e8 PR feedback: Consistency. 2019-05-14 12:19:56 -07:00
Dan Helfman 06261d8c86 Merge branch 'master' into tox-skip-missing-interpreters 2019-05-14 12:18:30 -07:00
Dan HelfmanandGitea 869cccf884 Upgrade pip to a particular version during local test and CI. 2019-05-14 19:17:30 +00:00
Dan Helfman 0defaf9cb5 Run tests for all installed versions of Python (#166). 2019-05-14 12:09:07 -07:00
Dan Helfman 60b1f9921d Don't use pip wrapper script in CI. 2019-05-14 12:01:40 -07:00
Dan Helfman f61bc91b0f Merge branch 'master' into upgrade-pip 2019-05-14 10:31:03 -07:00
Dan Helfman ed2c6053de Upgrade pip to a particular version during local test and CI. 2019-05-14 10:28:04 -07:00
Dan Helfman 2cffa8deaa Add missing ticket number to NEWS item. 2019-05-14 10:07:46 -07:00
Dan Helfman f0581271f6 Automatically sort Python imports in code. 2019-05-14 10:02:41 -07:00
Dan HelfmanandGitea 99522234ea Automatically sort Python imports in code. 2019-05-14 17:02:37 +00:00
Dan Helfman 67f2862fb1 Change paths to reflect new pip install --user documentation. 2019-05-14 10:00:50 -07:00
Luke Murphy 1c0dc3f904 Run isort over the source 2019-05-14 18:59:19 +02:00
Luke Murphy b94dbff216 Add isort configuration
Closes https://projects.torsion.org/witten/borgmatic/issues/169.
2019-05-14 18:59:19 +02:00
Dan Helfman 7388c723cd Mention tox.ini refactoring in NEWS. 2019-05-14 09:45:39 -07:00
Dan HelfmanandGitea 128be3c17d Factor out build/test configuration from tox.ini file. 2019-05-14 16:45:24 +00:00
Luke Murphy 4c30c94258 Add workaround for editable failure
See https://projects.torsion.org/witten/borgmatic/pulls/165#issuecomment-1416.
2019-05-14 13:17:15 +02:00
Luke Murphy 20b8b45aeb Remove all configuration from Tox file
This puts tool configuration in their familiar and standard
locations and simplifies the Tox configuration to just laying
out the environments and factors.

This also allows users who do not want to deal with overhead of Tox (for
whatever reasons ...) to run pytest/black/etc. and have the same
behaviour.
2019-05-14 13:17:15 +02:00
Dan Helfman 2dd899f287 Linkify build status badge. 2019-05-13 22:33:28 -07:00
Dan Helfman a13cc0ab17 More tests for colored logging. 2019-05-13 21:10:26 -07:00
Dan Helfman 620f9e64d6 A few more tests for new colored logging. 2019-05-13 20:49:20 -07:00
Dan Helfman 25c320b281 Pin pip version: cherrypick of https://projects.torsion.org/witten/borgmatic/pulls/172 2019-05-13 20:01:25 -07:00
Dan Helfman f19eec56ac Add tox pin to NEWS. 2019-05-13 17:07:20 -07:00
Dan HelfmanandGitea 7cbcff2e9b Pin tox version. 2019-05-14 00:06:07 +00:00
Dan Helfman 9f6407ada6 Mention continuous integration badge in NEWS. 2019-05-13 14:54:24 -07:00
Dan HelfmanandGitea e933ecf046 Add drone CI note and badge. 2019-05-13 21:51:00 +00:00
Luke Murphy 4010a2ed77 Add note about Drone CI to contributing docs 2019-05-13 23:43:11 +02:00
Luke Murphy 2f36096e1a Add Drone build badge 2019-05-13 23:42:11 +02:00
Luke Murphy 82ec45e375 Pin tox version
Towards more reproducible results with Tox.
2019-05-13 23:12:18 +02:00
Dan Helfman 37362150fe Color records that are logged via logger.handle() as well. 2019-05-13 13:50:32 -07:00
Dan Helfman a7ba97803f Add colored output to NEWS. 2019-05-13 19:52:54 +00:00
Dan HelfmanandGitea 31dc903877 Integrate colorama for colored output. 2019-05-13 19:50:36 +00:00
Dan Helfman 8943867433 Bump to dev version. 2019-05-13 19:07:27 +00:00
Dan Helfman d9cb110563 Document installing borgmatic with pip install --user instead of a system Python install. 2019-05-13 19:06:42 +00:00
Dan HelfmanandGitea 32113cee67 Document installing borgmatic with pip install --user instead of a system Python install. 2019-05-13 19:04:24 +00:00
Luke Murphy a621ce199a Add tests for borgmatic.logger.to_bool 2019-05-13 13:40:23 +02:00
Luke Murphy 1f524d6c87 Add borgmatic custom logger 2019-05-13 13:40:18 +02:00
Luke Murphy 0320d449ec Add documentation about colorama 2019-05-13 13:40:17 +02:00
Luke Murphy 30f007687a Add colorama to testing dependencies 2019-05-13 13:40:17 +02:00
Luke Murphy adf7856162 Add new colorama dependency 2019-05-13 13:40:17 +02:00
Luke Murphy f9dce8b2d3 Recommend user installs when upgrading 2019-05-13 13:18:59 +02:00
Luke Murphy 15cb6270ef Recommend a tox user install for developing 2019-05-13 13:18:45 +02:00
Luke Murphy ed14fdbac9 Recommend root user package install
This can do bad things to a system Python install. So, we try to
mitigate this by recommending a root user user site installation.
2019-05-13 13:18:37 +02:00
Dan Helfman 8650a15db1 Document validate-borgmatic-config and add a few tests. 2019-05-11 14:05:16 -07:00
Dan HelfmanandGitea 6a10022543 Add validate-borgmatic-config command. 2019-05-11 20:15:06 +00:00
Robin Schneider 52e4f48eb9 Add validate-borgmatic-config command
Useful when generating the borgmatic configuration file with
configuration management and before moving the generated file in place
checking if it is actually valid.
2019-05-10 00:10:28 +02:00
Dan Helfman f5e1e8bec9 In continuous integration build matrix, use newer Alpine 3.9 instead of 3.8. 2019-05-07 16:19:03 -07:00
Dan Helfman a291477c19 Fix for hooks executing when using --dry-run (#160). 2019-05-07 16:06:31 -07:00
Dan Helfman 1c88dda76a Fix for invalid JSON output when using multiple borgmatic configuration files (#155). 2019-04-02 22:30:14 -07:00
Dan Helfman 0b59c22c23 Fix for seemingly random filename ordering when running through a directory of configuration files (#157). 2019-03-30 16:41:21 -07:00
Dan Helfman 576377e2b2 Clarify differences between Docker images. 2019-03-16 15:04:48 -07:00
62 changed files with 820 additions and 306 deletions
+3 -2
View File
@@ -3,7 +3,8 @@ pipeline:
image: python:${PYTHON_VERSION}-alpine${ALPINE_VERSION}
pull: true
commands:
- pip install tox
- python -m pip install --upgrade pip==19.1.1
- pip install tox==3.10.0
- tox
- apk add --no-cache borgbackup
- tox -e end-to-end
@@ -11,7 +12,7 @@ pipeline:
matrix:
ALPINE_VERSION:
- 3.7
- 3.8
- 3.9
PYTHON_VERSION:
- 3.5
- 3.6
+1 -1
View File
@@ -7,6 +7,6 @@ Johannes Feichtner: Support for user hooks
Michele Lazzeri: Custom archive names
Nick Whyte: Support prefix filtering for archive consistency checks
newtonne: Read encryption password from external file
Robin `ypid` Schneider: Support additional options of Borg
Robin `ypid` Schneider: Support additional options of Borg and add validate-borgmatic-config command
Scott Squires: Custom archive names
Thomas LÉVEIL: Support for a keep_minutely prune option. Support for the --json option
+29
View File
@@ -1,3 +1,32 @@
1.3.5
* #153: Support for various Borg directory environment variables (BORG_CONFIG_DIR, BORG_CACHE_DIR,
etc.) via options in borgmatic's storage configuration.
* #177: Fix for regression with missing verbose log entries.
1.3.4
* Part of #125: Color borgmatic (but not Borg) output when using an interactive terminal.
* #166: Run tests for all installed versions of Python.
* #168: Update README with continuous integration badge.
* #169: Automatically sort Python imports in code.
* Document installing borgmatic with pip install --user instead of a system Python install.
* Get more reproducible builds by pinning the versions of pip and tox used to run tests.
* Factor out build/test configuration from tox.ini file.
1.3.3
* Add validate-borgmatic-config command, useful for validating borgmatic config generated by
configuration management or even edited by hand.
1.3.2
* #160: Fix for hooks executing when using --dry-run. Now hooks are skipped during a dry run.
1.3.1
* #155: Fix for invalid JSON output when using multiple borgmatic configuration files.
* #157: Fix for seemingly random filename ordering when running through a directory of
configuration files.
* Fix for empty JSON output when using --create --json.
* Now capturing Borg output only when --json flag is used. Previously, borgmatic delayed Borg
output even without the --json flag.
1.3.0
* #148: Configuration file includes and merging via "!include" tag to support reuse of common
options across configuration files.
+3 -1
View File
@@ -2,9 +2,11 @@
title: borgmatic
permalink: borgmatic/index.html
---
<a href="https://build.torsion.org/witten/borgmatic" alt="build status">![Build Status](https://build.torsion.org/api/badges/witten/borgmatic/status.svg)</a>
## Overview
<img src="https://projects.torsion.org/witten/borgmatic/raw/branch/master/static/borgmatic.png" width="150px" style="float: right; padding-left: 1em;">
<img src="https://projects.torsion.org/witten/borgmatic/raw/branch/master/static/borgmatic.png" alt="borgmatic logo" width="150px" style="float: right; padding-left: 1em;">
borgmatic is a simple Python wrapper script for the
[Borg](https://www.borgbackup.org/) backup software that initiates a backup,
+2 -2
View File
@@ -3,13 +3,13 @@ import os
import subprocess
from borgmatic.borg import extract
from borgmatic.logger import get_logger
DEFAULT_CHECKS = ('repository', 'archives')
DEFAULT_PREFIX = '{hostname}-'
logger = logging.getLogger(__name__)
logger = get_logger(__name__)
def _parse_checks(consistency_config):
+5 -5
View File
@@ -2,11 +2,12 @@ import glob
import itertools
import logging
import os
import subprocess
import tempfile
from borgmatic.borg.execute import execute_command
from borgmatic.logger import get_logger
logger = logging.getLogger(__name__)
logger = get_logger(__name__)
def _expand_directory(directory):
@@ -107,7 +108,7 @@ def create_archive(
):
'''
Given vebosity/dry-run flags, a local or remote repository path, a location config dict, and a
storage config dict, create a Borg archive.
storage config dict, create a Borg archive and return Borg's JSON output (if any).
'''
sources = _expand_directories(location_config['source_directories'])
@@ -157,5 +158,4 @@ def create_archive(
+ (('--json',) if json else ())
)
logger.debug(' '.join(full_command))
subprocess.check_call(full_command)
return execute_command(full_command, capture_output=json)
+15 -11
View File
@@ -1,15 +1,19 @@
import os
OPTION_TO_ENVIRONMENT_VARIABLE = {
'borg_base_directory': 'BORG_BASE_DIR',
'borg_config_directory': 'BORG_CONFIG_DIR',
'borg_cache_directory': 'BORG_CACHE_DIR',
'borg_security_directory': 'BORG_SECURITY_DIR',
'borg_keys_directory': 'BORG_KEYS_DIR',
'encryption_passcommand': 'BORG_PASSCOMMAND',
'encryption_passphrase': 'BORG_PASSPHRASE',
'ssh_command': 'BORG_RSH',
}
def initialize(storage_config):
passcommand = storage_config.get('encryption_passcommand')
if passcommand:
os.environ['BORG_PASSCOMMAND'] = passcommand
passphrase = storage_config.get('encryption_passphrase')
if passphrase:
os.environ['BORG_PASSPHRASE'] = passphrase
ssh_command = storage_config.get('ssh_command')
if ssh_command:
os.environ['BORG_RSH'] = ssh_command
for option_name, environment_variable_name in OPTION_TO_ENVIRONMENT_VARIABLE.items():
value = storage_config.get(option_name)
if value:
os.environ[environment_variable_name] = value
+19
View File
@@ -0,0 +1,19 @@
import subprocess
from borgmatic.logger import get_logger
logger = get_logger(__name__)
def execute_command(full_command, capture_output=False):
'''
Execute the given command (a sequence of command/argument strings). If capture output is True,
then return the command's output as a string.
'''
logger.debug(' '.join(full_command))
if capture_output:
output = subprocess.check_output(full_command)
return output.decode() if output is not None else None
else:
subprocess.check_call(full_command)
+3 -2
View File
@@ -1,9 +1,10 @@
import logging
import sys
import subprocess
import sys
from borgmatic.logger import get_logger
logger = logging.getLogger(__name__)
logger = get_logger(__name__)
def extract_last_archive_dry_run(repository, lock_wait=None, local_path='borg', remote_path=None):
+6 -8
View File
@@ -1,16 +1,17 @@
import logging
import subprocess
from borgmatic.borg.execute import execute_command
from borgmatic.logger import get_logger
logger = logging.getLogger(__name__)
logger = get_logger(__name__)
def display_archives_info(
repository, storage_config, local_path='borg', remote_path=None, json=False
):
'''
Given a local or remote repository path, and a storage config dict,
display summary information for Borg archives in the repository.
Given a local or remote repository path, and a storage config dict, display summary information
for Borg archives in the repository or return JSON summary information.
'''
lock_wait = storage_config.get('lock_wait', None)
@@ -23,7 +24,4 @@ def display_archives_info(
+ (('--json',) if json else ())
)
logger.debug(' '.join(full_command))
output = subprocess.check_output(full_command)
return output.decode() if output is not None else None
return execute_command(full_command, capture_output=json)
+2 -1
View File
@@ -1,8 +1,9 @@
import logging
import subprocess
from borgmatic.logger import get_logger
logger = logging.getLogger(__name__)
logger = get_logger(__name__)
def initialize_repository(
+7 -7
View File
@@ -1,16 +1,18 @@
import logging
import subprocess
from borgmatic.borg.execute import execute_command
from borgmatic.logger import get_logger
logger = logging.getLogger(__name__)
logger = get_logger(__name__)
def list_archives(
repository, storage_config, archive=None, local_path='borg', remote_path=None, json=False
):
'''
Given a local or remote repository path and a storage config dict, list Borg archives in the
repository. Or, if an archive name is given, list the files in that archive.
Given a local or remote repository path and a storage config dict, display the output of listing
Borg archives in the repository or return JSON output. Or, if an archive name is given, listing
the files in that archive.
'''
lock_wait = storage_config.get('lock_wait', None)
@@ -22,7 +24,5 @@ def list_archives(
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+ (('--json',) if json else ())
)
logger.debug(' '.join(full_command))
output = subprocess.check_output(full_command)
return output.decode() if output is not None else None
return execute_command(full_command, capture_output=json)
+2 -1
View File
@@ -1,8 +1,9 @@
import logging
import subprocess
from borgmatic.logger import get_logger
logger = logging.getLogger(__name__)
logger = get_logger(__name__)
def _make_prune_flags(retention_config):
+66 -69
View File
@@ -1,31 +1,29 @@
from argparse import ArgumentParser
import collections
import json
import logging
import os
from subprocess import CalledProcessError
import sys
from argparse import ArgumentParser
from subprocess import CalledProcessError
import colorama
import pkg_resources
from borgmatic.borg import (
check as borg_check,
create as borg_create,
environment as borg_environment,
prune as borg_prune,
extract as borg_extract,
list as borg_list,
info as borg_info,
init as borg_init,
)
from borgmatic.borg import check as borg_check
from borgmatic.borg import create as borg_create
from borgmatic.borg import environment as borg_environment
from borgmatic.borg import extract as borg_extract
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 import hook
from borgmatic.config import checks, collect, convert, validate
from borgmatic.logger import get_logger, should_do_markup
from borgmatic.signals import configure_signals
from borgmatic.verbosity import verbosity_to_log_level
logger = logging.getLogger(__name__)
logger = get_logger(__name__)
LEGACY_CONFIG_PATH = '/etc/borgmatic/config'
@@ -169,6 +167,9 @@ def parse_arguments(*arguments):
action='store_true',
help='Go through the motions, but do not actually write to any repositories',
)
common_group.add_argument(
'-nc', '--no-color', dest='no_color', action='store_true', help='Disable colored output'
)
common_group.add_argument(
'-v',
'--verbosity',
@@ -258,6 +259,8 @@ def run_configuration(config_filename, config, args): # pragma: no cover
'''
Given a config filename and the corresponding parsed config dict, execute its defined pruning,
backups, consistency checks, and/or other actions.
Yield JSON output strings from executing any actions that produce JSON.
'''
(location, storage, retention, consistency, hooks) = (
config.get(section_name, {})
@@ -270,56 +273,42 @@ def run_configuration(config_filename, config, args): # pragma: no cover
borg_environment.initialize(storage)
if args.create:
hook.execute_hook(hooks.get('before_backup'), config_filename, 'pre-backup')
hook.execute_hook(
hooks.get('before_backup'), config_filename, 'pre-backup', args.dry_run
)
_run_commands(
args=args,
consistency=consistency,
local_path=local_path,
location=location,
remote_path=remote_path,
retention=retention,
storage=storage,
)
for repository_path in location['repositories']:
yield from run_actions(
args=args,
location=location,
storage=storage,
retention=retention,
consistency=consistency,
local_path=local_path,
remote_path=remote_path,
repository_path=repository_path,
)
if args.create:
hook.execute_hook(hooks.get('after_backup'), config_filename, 'post-backup')
hook.execute_hook(
hooks.get('after_backup'), config_filename, 'post-backup', args.dry_run
)
except (OSError, CalledProcessError):
hook.execute_hook(hooks.get('on_error'), config_filename, 'on-error')
hook.execute_hook(hooks.get('on_error'), config_filename, 'on-error', args.dry_run)
raise
def _run_commands(*, args, consistency, local_path, location, remote_path, retention, storage):
json_results = []
for unexpanded_repository in location['repositories']:
_run_commands_on_repository(
args=args,
consistency=consistency,
json_results=json_results,
local_path=local_path,
location=location,
remote_path=remote_path,
retention=retention,
storage=storage,
unexpanded_repository=unexpanded_repository,
)
if args.json:
sys.stdout.write(json.dumps(json_results))
def _run_commands_on_repository(
*,
args,
consistency,
json_results,
local_path,
location,
remote_path,
retention,
storage,
unexpanded_repository
def run_actions(
*, args, location, storage, retention, consistency, local_path, remote_path, repository_path
): # pragma: no cover
repository = os.path.expanduser(unexpanded_repository)
'''
Given parsed command-line arguments as an argparse.ArgumentParser instance, several different
configuration dicts, local and remote paths to Borg, and a repository name, run all actions
from the command-line arguments on the given repository.
Yield JSON output strings from executing any actions that produce JSON.
'''
repository = os.path.expanduser(repository_path)
dry_run_label = ' (dry run; not making any changes)' if args.dry_run else ''
if args.init:
logger.info('{}: Initializing repository'.format(repository))
@@ -344,7 +333,7 @@ def _run_commands_on_repository(
)
if args.create:
logger.info('{}: Creating archive{}'.format(repository, dry_run_label))
borg_create.create_archive(
json_output = borg_create.create_archive(
args.dry_run,
repository,
location,
@@ -353,7 +342,10 @@ def _run_commands_on_repository(
remote_path=remote_path,
progress=args.progress,
stats=args.stats,
json=args.json,
)
if json_output:
yield json.loads(json_output)
if args.check and checks.repository_enabled_for_checks(repository, consistency):
logger.info('{}: Running consistency checks'.format(repository))
borg_check.check_archives(
@@ -376,7 +368,7 @@ def _run_commands_on_repository(
if args.list:
if args.repository is None or repository == args.repository:
logger.info('{}: Listing archives'.format(repository))
output = borg_list.list_archives(
json_output = borg_list.list_archives(
repository,
storage,
args.archive,
@@ -384,19 +376,15 @@ def _run_commands_on_repository(
remote_path=remote_path,
json=args.json,
)
if args.json:
json_results.append(json.loads(output))
else:
sys.stdout.write(output)
if json_output:
yield json.loads(json_output)
if args.info:
logger.info('{}: Displaying summary info for archives'.format(repository))
output = borg_info.display_archives_info(
json_output = borg_info.display_archives_info(
repository, storage, local_path=local_path, remote_path=remote_path, json=args.json
)
if args.json:
json_results.append(json.loads(output))
else:
sys.stdout.write(output)
if json_output:
yield json.loads(json_output)
def collect_configuration_run_summary_logs(config_filenames, args):
@@ -404,6 +392,9 @@ def collect_configuration_run_summary_logs(config_filenames, args):
Given a sequence of configuration filenames and parsed command-line arguments as an
argparse.ArgumentParser instance, run each configuration file and yield a series of
logging.LogRecord instances containing summary information about each run.
As a side effect of running through these configuration files, output their JSON results, if
any, to stdout.
'''
# Dict mapping from config filename to corresponding parsed config dict.
configs = collections.OrderedDict()
@@ -433,9 +424,10 @@ def collect_configuration_run_summary_logs(config_filenames, args):
return
# Execute the actions corresponding to each configuration file.
json_results = []
for config_filename, config in configs.items():
try:
run_configuration(config_filename, config, args)
json_results.extend(list(run_configuration(config_filename, config, args)))
yield logging.makeLogRecord(
dict(
levelno=logging.INFO,
@@ -451,6 +443,9 @@ def collect_configuration_run_summary_logs(config_filenames, args):
)
yield logging.makeLogRecord(dict(levelno=logging.CRITICAL, msg=error))
if json_results:
sys.stdout.write(json.dumps(json_results))
if not config_filenames:
yield logging.makeLogRecord(
dict(
@@ -478,6 +473,8 @@ def main(): # pragma: no cover
logger.critical(error)
exit_with_help_link()
colorama.init(autoreset=True, strip=not should_do_markup(args.no_color))
logging.basicConfig(level=verbosity_to_log_level(args.verbosity), format='%(message)s')
if args.version:
+1 -2
View File
@@ -1,13 +1,12 @@
from argparse import ArgumentParser
import os
import sys
import textwrap
from argparse import ArgumentParser
from ruamel import yaml
from borgmatic.config import convert, generate, legacy, validate
DEFAULT_SOURCE_CONFIG_FILENAME = '/etc/borgmatic/config'
DEFAULT_SOURCE_EXCLUDES_FILENAME = '/etc/borgmatic/excludes'
DEFAULT_DESTINATION_CONFIG_FILENAME = '/etc/borgmatic/config.yaml'
+1 -2
View File
@@ -1,9 +1,8 @@
from argparse import ArgumentParser
import sys
from argparse import ArgumentParser
from borgmatic.config import generate, validate
DEFAULT_DESTINATION_CONFIG_FILENAME = '/etc/borgmatic/config.yaml'
+17 -7
View File
@@ -1,24 +1,34 @@
import logging
import subprocess
from borgmatic.logger import get_logger
logger = logging.getLogger(__name__)
logger = get_logger(__name__)
def execute_hook(commands, config_filename, description):
def execute_hook(commands, config_filename, description, dry_run):
'''
Given a list of hook commands to execute, a config filename, a hook description, and whether
this is a dry run, run the given commands. Or, don't run them if this is a dry run.
'''
if not commands:
logger.debug('{}: No commands to run for {} hook'.format(config_filename, description))
return
dry_run_label = ' (dry run; not actually running hooks)' if dry_run else ''
if len(commands) == 1:
logger.info('{}: Running command for {} hook'.format(config_filename, description))
logger.info(
'{}: Running command for {} hook{}'.format(config_filename, description, dry_run_label)
)
else:
logger.info(
'{}: Running {} commands for {} hook'.format(
config_filename, len(commands), description
'{}: Running {} commands for {} hook{}'.format(
config_filename, len(commands), description, dry_run_label
)
)
for command in commands:
logger.debug('{}: Hook command: {}'.format(config_filename, command))
subprocess.check_call(command, shell=True)
if not dry_run:
subprocess.check_call(command, shell=True)
+57
View File
@@ -0,0 +1,57 @@
import logging
import sys
from argparse import ArgumentParser
from borgmatic.config import collect, validate
from borgmatic.logger import get_logger
logger = get_logger(__name__)
def parse_arguments(*arguments):
'''
Given command-line arguments with which this script was invoked, parse the arguments and return
them as an ArgumentParser instance.
'''
config_paths = collect.get_default_config_paths()
parser = ArgumentParser(description='Validate borgmatic configuration file(s).')
parser.add_argument(
'-c',
'--config',
nargs='+',
dest='config_paths',
default=config_paths,
help='Configuration filenames or directories, defaults to: {}'.format(
' '.join(config_paths)
),
)
return parser.parse_args(arguments)
def main(): # pragma: no cover
args = parse_arguments(*sys.argv[1:])
logging.basicConfig(level=logging.INFO, format='%(message)s')
config_filenames = tuple(collect.collect_config_filenames(args.config_paths))
if len(config_filenames) == 0:
logger.critical('No files to validate found')
sys.exit(1)
found_issues = False
for config_filename in config_filenames:
try:
validate.parse_configuration(config_filename, validate.schema_filename())
except (ValueError, OSError, validate.Validation_error) as error:
logging.critical('{}: Error parsing configuration file'.format(config_filename))
logging.critical(error)
found_issues = True
if found_issues:
sys.exit(1)
else:
logger.info(
'All given configuration files are valid: {}'.format(', '.join(config_filenames))
)
+1 -1
View File
@@ -41,7 +41,7 @@ def collect_config_filenames(config_paths):
yield path
continue
for filename in os.listdir(path):
for filename in sorted(os.listdir(path)):
full_filename = os.path.join(path, filename)
if full_filename.endswith('.yaml') and not os.path.isdir(full_filename):
yield full_filename
+4 -4
View File
@@ -64,9 +64,9 @@ def convert_legacy_parsed_config(source_config, source_excludes, schema):
return destination_config
class LegacyConfigurationNotUpgraded(FileNotFoundError):
class Legacy_configuration_not_upgraded(FileNotFoundError):
def __init__(self):
super(LegacyConfigurationNotUpgraded, self).__init__(
super(Legacy_configuration_not_upgraded, self).__init__(
'''borgmatic changed its configuration file format in version 1.1.0 from INI-style
to YAML. This better supports validation, and has a more natural way to express
lists of values. To upgrade your existing configuration, run:
@@ -83,7 +83,7 @@ instead of the old one.'''
def guard_configuration_upgraded(source_config_filename, destination_config_filenames):
'''
If legacy source configuration exists but no destination upgraded configs do, raise
LegacyConfigurationNotUpgraded.
Legacy_configuration_not_upgraded.
The idea is that we want to alert the user about upgrading their config if they haven't already.
'''
@@ -92,4 +92,4 @@ def guard_configuration_upgraded(source_config_filename, destination_config_file
)
if os.path.exists(source_config_filename) and not destination_config_exists:
raise LegacyConfigurationNotUpgraded()
raise Legacy_configuration_not_upgraded()
-1
View File
@@ -2,7 +2,6 @@ import os
from ruamel import yaml
INDENT = 4
-1
View File
@@ -1,7 +1,6 @@
from collections import OrderedDict, namedtuple
from configparser import RawConfigParser
Section_format = namedtuple('Section_format', ('name', 'options'))
Config_option = namedtuple('Config_option', ('name', 'value_type', 'required'))
+2 -2
View File
@@ -1,10 +1,10 @@
import os
import logging
import ruamel.yaml
from borgmatic.logger import get_logger
logger = logging.getLogger(__name__)
logger = get_logger(__name__)
def load_configuration(filename):
+26
View File
@@ -168,6 +168,32 @@ map:
Command to use instead of "ssh". This can be used to specify ssh options.
Defaults to not set.
example: ssh -i /path/to/private/key
borg_base_directory:
type: scalar
desc: |
Base path used for various Borg directories. Defaults to $HOME, ~$USER, or ~.
See https://borgbackup.readthedocs.io/en/stable/usage/general.html#environment-variables for details.
example: /path/to/base
borg_config_directory:
type: scalar
desc: |
Path for Borg configuration files. Defaults to $borg_base_directory/.config/borg
example: /path/to/base/config
borg_cache_directory:
type: scalar
desc: |
Path for Borg cache files. Defaults to $borg_base_directory/.cache/borg
example: /path/to/base/cache
borg_security_directory:
type: scalar
desc: |
Path for Borg security and encryption nonce files. Defaults to $borg_base_directory/.config/borg/security
example: /path/to/base/config/security
borg_keys_directory:
type: scalar
desc: |
Path for Borg encryption key files. Defaults to $borg_base_directory/.config/borg/keys
example: /path/to/base/config/keys
umask:
type: scalar
desc: Umask to be used for borg create. Defaults to 0077.
+2 -2
View File
@@ -6,9 +6,9 @@ import pykwalify.errors
import ruamel.yaml
from borgmatic.config import load
from borgmatic.logger import get_logger
logger = logging.getLogger(__name__)
logger = get_logger(__name__)
def schema_filename():
+99
View File
@@ -0,0 +1,99 @@
import logging
import os
import sys
import colorama
def to_bool(arg):
'''
Return a boolean value based on `arg`.
'''
if arg is None or isinstance(arg, bool):
return arg
if isinstance(arg, str):
arg = arg.lower()
if arg in ('yes', 'on', '1', 'true', 1):
return True
return False
def should_do_markup(no_color):
'''
Determine if we should enable colorama marking up.
'''
if no_color:
return False
py_colors = os.environ.get('PY_COLORS', None)
if py_colors is not None:
return to_bool(py_colors)
return sys.stdout.isatty() and os.environ.get('TERM') != 'dumb'
LOG_LEVEL_TO_COLOR = {
logging.CRITICAL: colorama.Fore.RED,
logging.ERROR: colorama.Fore.RED,
logging.WARN: colorama.Fore.YELLOW,
logging.INFO: colorama.Fore.GREEN,
logging.DEBUG: colorama.Fore.CYAN,
}
class Borgmatic_logger(logging.Logger):
def critical(self, msg, *args, **kwargs):
color = LOG_LEVEL_TO_COLOR.get(logging.CRITICAL)
return super(Borgmatic_logger, self).critical(color_text(color, msg), *args, **kwargs)
def error(self, msg, *args, **kwargs):
color = LOG_LEVEL_TO_COLOR.get(logging.ERROR)
return super(Borgmatic_logger, self).error(color_text(color, msg), *args, **kwargs)
def warn(self, msg, *args, **kwargs):
color = LOG_LEVEL_TO_COLOR.get(logging.WARN)
return super(Borgmatic_logger, self).warn(color_text(color, msg), *args, **kwargs)
def info(self, msg, *args, **kwargs):
color = LOG_LEVEL_TO_COLOR.get(logging.INFO)
return super(Borgmatic_logger, self).info(color_text(color, msg), *args, **kwargs)
def debug(self, msg, *args, **kwargs):
color = LOG_LEVEL_TO_COLOR.get(logging.DEBUG)
return super(Borgmatic_logger, self).debug(color_text(color, msg), *args, **kwargs)
def handle(self, record):
color = LOG_LEVEL_TO_COLOR.get(record.levelno)
colored_record = logging.makeLogRecord(
dict(levelno=record.levelno, msg=color_text(color, record.msg))
)
return super(Borgmatic_logger, self).handle(colored_record)
def get_logger(name=None):
'''
Build a logger with the given name.
'''
logging.setLoggerClass(Borgmatic_logger)
logger = logging.getLogger(name)
return logger
def color_text(color, message):
'''
Give colored text.
'''
if not color:
return message
return '{}{}{}'.format(color, message, colorama.Style.RESET_ALL)
-1
View File
@@ -1,6 +1,5 @@
import logging
VERBOSITY_WARNING = 0
VERBOSITY_SOME = 1
VERBOSITY_LOTS = 2
+7 -1
View File
@@ -41,7 +41,7 @@ you're in the `borgmatic/` working copy, install tox, which is used for
setting up testing environments:
```bash
sudo pip3 install tox
pip3 install --user tox
```
Finally, to actually run tests, run:
@@ -91,6 +91,12 @@ formatter and [Flake8](http://flake8.pycqa.org/en/latest/) code checker, so
certain code style requirements will be enforced when running automated tests.
See the Black and Flake8 documentation for more information.
## Continuous integration
Each pull request triggers a continuous integration build which runs the test
suite. You can view these builds on
[build.torsion.org](https://build.torsion.org/witten/borgmatic), and they're
also linked from the commits list on each pull request.
## Related documentation
+34 -6
View File
@@ -7,23 +7,31 @@ To get up and running, first [install
Borg](https://borgbackup.readthedocs.io/en/latest/installation.html), at
least version 1.1.
Then, run the following command to download and install borgmatic:
Borgmatic consumes configurations in `/etc/borgmatic/` and `/etc/borgmatic.d/`
by default. Therefore, we show how to install borgmatic for the root user which
will have access permissions for these locations by default.
Run the following commands to download and install borgmatic:
```bash
sudo pip3 install --upgrade borgmatic
sudo pip3 install --user --upgrade borgmatic
```
This is a [recommended user site
installation](https://packaging.python.org/tutorials/installing-packages/#installing-to-the-user-site).
You will need to ensure that `/root/.local/bin` is available on your `$PATH` so
that the borgmatic executable is available.
Note that your pip binary may have a different name than "pip3". Make sure
you're using Python 3, as borgmatic does not support Python 2.
### Other ways to install
Along with the above process, you have several other options for installing
borgmatic:
* [Docker image](https://hub.docker.com/r/monachus/borgmatic/)
* [Another Docker image](https://hub.docker.com/r/b3vis/borgmatic/)
* [Docker base image](https://hub.docker.com/r/monachus/borgmatic/)
* [Docker image with support for scheduled backups](https://hub.docker.com/r/b3vis/borgmatic/)
* [Debian](https://tracker.debian.org/pkg/borgmatic)
* [Ubuntu](https://launchpad.net/ubuntu/+source/borgmatic)
* [Fedora](https://bodhi.fedoraproject.org/updates/?search=borgmatic)
@@ -76,7 +84,21 @@ FAQ](http://borgbackup.readthedocs.io/en/stable/faq.html#how-can-i-specify-the-e
for more info.
###
### Validation
If you'd like to validate that your borgmatic configuration is valid, the
following command is available for that:
```bash
sudo validate-borgmatic-config
```
This command's exit status (`$?` in Bash) is zero when configuration is valid
and non-zero otherwise.
Validating configuration can be useful if you generate your configuration
files via configuration management, or you just want to double check that your
hand edits are valid.
## Initialization
@@ -165,6 +187,12 @@ sudo systemctl start borgmatic.timer
Feel free to modify the timer file based on how frequently you'd like
borgmatic to run.
## Colored Output
Borgmatic uses [colorama](https://pypi.org/project/colorama/) to produce
colored terminal output by default. It is disabled when a non-interactive
terminal is detected (like a cron job). Otherwise, it can be disabled by
passing `--no-color` or by setting the environment variable `PY_COLORS=False`.
## Troubleshooting
+4 -4
View File
@@ -7,7 +7,7 @@ In general, all you should need to do to upgrade borgmatic is run the
following:
```bash
sudo pip3 install --upgrade borgmatic
sudo pip3 install --user --upgrade borgmatic
```
See below about special cases.
@@ -25,14 +25,14 @@ already running borgmatic with Python 3, then you can upgrade borgmatic
in-place:
```bash
sudo pip3 install --upgrade borgmatic
sudo pip3 install --user --upgrade borgmatic
```
But if you were running borgmatic with Python 2, uninstall and reinstall instead:
```bash
sudo pip uninstall borgmatic
sudo pip3 install borgmatic
sudo pip3 install --user borgmatic
```
The pip binary names for different versions of Python can differ, so the above
@@ -67,7 +67,7 @@ from atticmatic to borgmatic by running the following commands:
```bash
sudo pip3 uninstall atticmatic
sudo pip3 install borgmatic
sudo pip3 install --user borgmatic
```
That's it! borgmatic will continue using your /etc/borgmatic configuration
+3
View File
@@ -0,0 +1,3 @@
[tool.black]
line-length = 100
skip-string-normalization = true
+1 -1
View File
@@ -1,3 +1,3 @@
# You can drop this file into /etc/cron.d/ to run borgmatic nightly.
0 3 * * * PATH=$PATH:/usr/bin /usr/bin/borgmatic
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 /usr/local/bin/borgmatic
0 3 * * * root PATH=$PATH:/usr/local/bin /root/.local/bin/borgmatic
+1 -1
View File
@@ -3,4 +3,4 @@ Description=borgmatic backup
[Service]
Type=oneshot
ExecStart=/usr/local/bin/borgmatic
ExecStart=/root/.local/bin/borgmatic
-7
View File
@@ -1,7 +0,0 @@
#!/usr/bin/env sh
if which black; then
black --skip-string-normalization --line-length 100 --check .
else
echo "Skipping black due to not being installed."
fi
Executable
+6
View File
@@ -0,0 +1,6 @@
#!/usr/bin/env sh
# Temporary work around for https://github.com/pypa/pip/issues/6434
python -m pip install --upgrade pip==19.1.1
python -m pip install --no-use-pep517 $*
+16
View File
@@ -1,2 +1,18 @@
[metadata]
description-file=README.md
[tool:pytest]
testpaths = tests
addopts = --cov-report term-missing:skip-covered --cov=borgmatic --ignore=tests/end-to-end
[flake8]
ignore = E501,W503
exclude = *.*/*
[tool:isort]
force_single_line = False
include_trailing_comma = True
known_first_party = borgmatic
line_length = 100
multi_line_output = 3
skip = .tox
+9 -4
View File
@@ -1,7 +1,6 @@
from setuptools import setup, find_packages
from setuptools import find_packages, setup
VERSION = '1.3.0'
VERSION = '1.3.5'
setup(
@@ -26,9 +25,15 @@ setup(
'borgmatic = borgmatic.commands.borgmatic:main',
'upgrade-borgmatic-config = borgmatic.commands.convert_config:main',
'generate-borgmatic-config = borgmatic.commands.generate_config:main',
'validate-borgmatic-config = borgmatic.commands.validate_config:main',
]
},
obsoletes=['atticmatic'],
install_requires=('pykwalify>=1.6.0,<14.06', 'ruamel.yaml>0.15.0,<0.16.0', 'setuptools'),
install_requires=(
'pykwalify>=1.6.0,<14.06',
'ruamel.yaml>0.15.0,<0.16.0',
'setuptools',
'colorama>=0.4.1,<0.5',
),
include_package_data=True,
)
+2
View File
@@ -3,10 +3,12 @@ atomicwrites==1.2.1
attrs==18.2.0
black==18.9b0; python_version >= '3.6'
Click==7.0
colorama==0.4.1
coverage==4.5.1
docopt==0.6.2
flake8==3.5.0
flexmock==0.10.2
isort==4.3.19
mccabe==0.6.1
more-itertools==4.3.0
pluggy==0.7.1
+36
View File
@@ -0,0 +1,36 @@
import os
import subprocess
import tempfile
def test_validate_config_command_with_valid_configuration_succeeds():
with tempfile.TemporaryDirectory() as temporary_directory:
config_path = os.path.join(temporary_directory, 'test.yaml')
subprocess.check_call(
'generate-borgmatic-config --destination {}'.format(config_path).split(' ')
)
exit_code = subprocess.call(
'validate-borgmatic-config --config {}'.format(config_path).split(' ')
)
assert exit_code == 0
def test_validate_config_command_with_invalid_configuration_fails():
with tempfile.TemporaryDirectory() as temporary_directory:
config_path = os.path.join(temporary_directory, 'test.yaml')
subprocess.check_call(
'generate-borgmatic-config --destination {}'.format(config_path).split(' ')
)
config = open(config_path).read().replace('keep_daily: 7', 'keep_daily: "7"')
config_file = open(config_path, 'w')
config_file.write(config)
config_file.close()
exit_code = subprocess.call(
'validate-borgmatic-config --config {}'.format(config_path).split(' ')
)
assert exit_code == 1
+1 -1
View File
@@ -1,7 +1,7 @@
import subprocess
from flexmock import flexmock
import pytest
from flexmock import flexmock
from borgmatic.commands import borgmatic as module
@@ -1,7 +1,7 @@
import os
from flexmock import flexmock
import pytest
from flexmock import flexmock
from borgmatic.commands import convert_config as module
@@ -0,0 +1,20 @@
from flexmock import flexmock
from borgmatic.commands import validate_config 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)
parser = module.parse_arguments()
assert parser.config_paths == config_paths
def test_parse_arguments_with_multiple_config_paths_parses_as_list():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
parser = module.parse_arguments('--config', 'myconfig', 'otherconfig')
assert parser.config_paths == ['myconfig', 'otherconfig']
+2 -2
View File
@@ -1,9 +1,9 @@
from io import StringIO
import os
import sys
from io import StringIO
from flexmock import flexmock
import pytest
from flexmock import flexmock
from borgmatic.config import generate as module
+2 -3
View File
@@ -1,7 +1,6 @@
from io import StringIO
from collections import OrderedDict
import string
from collections import OrderedDict
from io import StringIO
from borgmatic.config import legacy as module
+1 -1
View File
@@ -2,8 +2,8 @@ import io
import string
import sys
from flexmock import flexmock
import pytest
from flexmock import flexmock
from borgmatic.config import validate as module
+3 -2
View File
@@ -1,11 +1,12 @@
from subprocess import STDOUT
import logging
import sys
from subprocess import STDOUT
from flexmock import flexmock
import pytest
from flexmock import flexmock
from borgmatic.borg import check as module
from ..test_verbosity import insert_logging_mock
+88 -38
View File
@@ -3,6 +3,7 @@ import logging
from flexmock import flexmock
from borgmatic.borg import create as module
from ..test_verbosity import insert_logging_mock
@@ -69,11 +70,6 @@ def test_write_pattern_file_with_empty_exclude_patterns_does_not_raise():
module._write_pattern_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 test_make_pattern_flags_includes_pattern_filename_when_given():
pattern_flags = module._make_pattern_flags(
location_config={'patterns': ['R /', '- /var']}, pattern_filename='/tmp/patterns'
@@ -169,7 +165,9 @@ def test_create_archive_calls_borg_with_parameters():
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(())
insert_subprocess_mock(CREATE_COMMAND)
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND, capture_output=False
)
module.create_archive(
dry_run=False,
@@ -192,7 +190,9 @@ def test_create_archive_with_patterns_calls_borg_with_patterns():
).and_return(None)
flexmock(module).should_receive('_make_pattern_flags').and_return(pattern_flags)
flexmock(module).should_receive('_make_exclude_flags').and_return(())
insert_subprocess_mock(CREATE_COMMAND + pattern_flags)
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + pattern_flags, capture_output=False
)
module.create_archive(
dry_run=False,
@@ -215,7 +215,9 @@ def test_create_archive_with_exclude_patterns_calls_borg_with_excludes():
)
flexmock(module).should_receive('_make_pattern_flags').and_return(())
flexmock(module).should_receive('_make_exclude_flags').and_return(exclude_flags)
insert_subprocess_mock(CREATE_COMMAND + exclude_flags)
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + exclude_flags, capture_output=False
)
module.create_archive(
dry_run=False,
@@ -236,7 +238,9 @@ def test_create_archive_with_log_info_calls_borg_with_info_parameter():
flexmock(module).should_receive('_make_pattern_flags').and_return(())
flexmock(module).should_receive('_make_pattern_flags').and_return(())
flexmock(module).should_receive('_make_exclude_flags').and_return(())
insert_subprocess_mock(CREATE_COMMAND + ('--list', '--filter', 'AME-', '--info', '--stats'))
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + ('--list', '--filter', 'AME-', '--info', '--stats'), capture_output=False
)
insert_logging_mock(logging.INFO)
module.create_archive(
@@ -257,8 +261,9 @@ def test_create_archive_with_log_debug_calls_borg_with_debug_parameter():
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(())
insert_subprocess_mock(
CREATE_COMMAND + ('--list', '--filter', 'AME-', '--stats', '--debug', '--show-rc')
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + ('--list', '--filter', 'AME-', '--stats', '--debug', '--show-rc'),
capture_output=False,
)
insert_logging_mock(logging.DEBUG)
@@ -281,7 +286,9 @@ def test_create_archive_with_dry_run_calls_borg_with_dry_run_parameter():
flexmock(module).should_receive('_make_pattern_flags').and_return(())
flexmock(module).should_receive('_make_pattern_flags').and_return(())
flexmock(module).should_receive('_make_exclude_flags').and_return(())
insert_subprocess_mock(CREATE_COMMAND + ('--dry-run',))
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + ('--dry-run',), capture_output=False
)
module.create_archive(
dry_run=True,
@@ -304,7 +311,9 @@ def test_create_archive_with_dry_run_and_log_info_calls_borg_without_stats_param
flexmock(module).should_receive('_make_pattern_flags').and_return(())
flexmock(module).should_receive('_make_pattern_flags').and_return(())
flexmock(module).should_receive('_make_exclude_flags').and_return(())
insert_subprocess_mock(CREATE_COMMAND + ('--list', '--filter', 'AME-', '--info', '--dry-run'))
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + ('--list', '--filter', 'AME-', '--info', '--dry-run'), capture_output=False
)
insert_logging_mock(logging.INFO)
module.create_archive(
@@ -328,8 +337,9 @@ def test_create_archive_with_dry_run_and_log_debug_calls_borg_without_stats_para
flexmock(module).should_receive('_make_pattern_flags').and_return(())
flexmock(module).should_receive('_make_pattern_flags').and_return(())
flexmock(module).should_receive('_make_exclude_flags').and_return(())
insert_subprocess_mock(
CREATE_COMMAND + ('--list', '--filter', 'AME-', '--debug', '--show-rc', '--dry-run')
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + ('--list', '--filter', 'AME-', '--debug', '--show-rc', '--dry-run'),
capture_output=False,
)
insert_logging_mock(logging.DEBUG)
@@ -351,7 +361,9 @@ def test_create_archive_with_checkpoint_interval_calls_borg_with_checkpoint_inte
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(())
insert_subprocess_mock(CREATE_COMMAND + ('--checkpoint-interval', '600'))
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + ('--checkpoint-interval', '600'), capture_output=False
)
module.create_archive(
dry_run=False,
@@ -371,7 +383,9 @@ def test_create_archive_with_chunker_params_calls_borg_with_chunker_params_param
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(())
insert_subprocess_mock(CREATE_COMMAND + ('--chunker-params', '1,2,3,4'))
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + ('--chunker-params', '1,2,3,4'), capture_output=False
)
module.create_archive(
dry_run=False,
@@ -391,7 +405,9 @@ def test_create_archive_with_compression_calls_borg_with_compression_parameters(
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(())
insert_subprocess_mock(CREATE_COMMAND + ('--compression', 'rle'))
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + ('--compression', 'rle'), capture_output=False
)
module.create_archive(
dry_run=False,
@@ -411,7 +427,9 @@ def test_create_archive_with_remote_rate_limit_calls_borg_with_remote_ratelimit_
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(())
insert_subprocess_mock(CREATE_COMMAND + ('--remote-ratelimit', '100'))
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + ('--remote-ratelimit', '100'), capture_output=False
)
module.create_archive(
dry_run=False,
@@ -431,7 +449,9 @@ def test_create_archive_with_one_file_system_calls_borg_with_one_file_system_par
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(())
insert_subprocess_mock(CREATE_COMMAND + ('--one-file-system',))
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + ('--one-file-system',), capture_output=False
)
module.create_archive(
dry_run=False,
@@ -452,7 +472,9 @@ def test_create_archive_with_numeric_owner_calls_borg_with_numeric_owner_paramet
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(())
insert_subprocess_mock(CREATE_COMMAND + ('--numeric-owner',))
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + ('--numeric-owner',), capture_output=False
)
module.create_archive(
dry_run=False,
@@ -473,7 +495,9 @@ def test_create_archive_with_read_special_calls_borg_with_read_special_parameter
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(())
insert_subprocess_mock(CREATE_COMMAND + ('--read-special',))
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + ('--read-special',), capture_output=False
)
module.create_archive(
dry_run=False,
@@ -494,7 +518,9 @@ def test_create_archive_with_bsd_flags_true_calls_borg_without_nobsdflags_parame
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(())
insert_subprocess_mock(CREATE_COMMAND)
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND, capture_output=False
)
module.create_archive(
dry_run=False,
@@ -515,7 +541,9 @@ def test_create_archive_with_bsd_flags_false_calls_borg_with_nobsdflags_paramete
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(())
insert_subprocess_mock(CREATE_COMMAND + ('--nobsdflags',))
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + ('--nobsdflags',), capture_output=False
)
module.create_archive(
dry_run=False,
@@ -536,7 +564,9 @@ def test_create_archive_with_files_cache_calls_borg_with_files_cache_parameters(
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(())
insert_subprocess_mock(CREATE_COMMAND + ('--files-cache', 'ctime,size'))
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + ('--files-cache', 'ctime,size'), capture_output=False
)
module.create_archive(
dry_run=False,
@@ -557,7 +587,9 @@ def test_create_archive_with_local_path_calls_borg_via_local_path():
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(())
insert_subprocess_mock(('borg1',) + CREATE_COMMAND[1:])
flexmock(module).should_receive('execute_command').with_args(
('borg1',) + CREATE_COMMAND[1:], capture_output=False
)
module.create_archive(
dry_run=False,
@@ -578,7 +610,9 @@ def test_create_archive_with_remote_path_calls_borg_with_remote_path_parameters(
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(())
insert_subprocess_mock(CREATE_COMMAND + ('--remote-path', 'borg1'))
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + ('--remote-path', 'borg1'), capture_output=False
)
module.create_archive(
dry_run=False,
@@ -599,7 +633,9 @@ def test_create_archive_with_umask_calls_borg_with_umask_parameters():
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(())
insert_subprocess_mock(CREATE_COMMAND + ('--umask', '740'))
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + ('--umask', '740'), capture_output=False
)
module.create_archive(
dry_run=False,
@@ -619,7 +655,9 @@ def test_create_archive_with_lock_wait_calls_borg_with_lock_wait_parameters():
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(())
insert_subprocess_mock(CREATE_COMMAND + ('--lock-wait', '5'))
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + ('--lock-wait', '5'), capture_output=False
)
module.create_archive(
dry_run=False,
@@ -639,9 +677,11 @@ def test_create_archive_with_json_calls_borg_with_json_parameter():
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(())
insert_subprocess_mock(CREATE_COMMAND + ('--json',))
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + ('--json',), capture_output=True
).and_return('[]')
module.create_archive(
json_output = module.create_archive(
dry_run=False,
repository='repo',
location_config={
@@ -653,6 +693,8 @@ def test_create_archive_with_json_calls_borg_with_json_parameter():
json=True,
)
assert json_output == '[]'
def test_create_archive_with_source_directories_glob_expands():
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'food'))
@@ -660,8 +702,9 @@ def test_create_archive_with_source_directories_glob_expands():
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(())
insert_subprocess_mock(
('borg', 'create', 'repo::{}'.format(DEFAULT_ARCHIVE_NAME), 'foo', 'food')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', 'repo::{}'.format(DEFAULT_ARCHIVE_NAME), 'foo', 'food'),
capture_output=False,
)
flexmock(module.glob).should_receive('glob').with_args('foo*').and_return(['foo', 'food'])
@@ -683,7 +726,9 @@ def test_create_archive_with_non_matching_source_directories_glob_passes_through
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(())
insert_subprocess_mock(('borg', 'create', 'repo::{}'.format(DEFAULT_ARCHIVE_NAME), 'foo*'))
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', 'repo::{}'.format(DEFAULT_ARCHIVE_NAME), 'foo*'), capture_output=False
)
flexmock(module.glob).should_receive('glob').with_args('foo*').and_return([])
module.create_archive(
@@ -704,8 +749,9 @@ def test_create_archive_with_glob_calls_borg_with_expanded_directories():
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(())
insert_subprocess_mock(
('borg', 'create', 'repo::{}'.format(DEFAULT_ARCHIVE_NAME), 'foo', 'food')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', 'repo::{}'.format(DEFAULT_ARCHIVE_NAME), 'foo', 'food'),
capture_output=False,
)
module.create_archive(
@@ -726,7 +772,9 @@ def test_create_archive_with_archive_name_format_calls_borg_with_archive_name():
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(())
insert_subprocess_mock(('borg', 'create', 'repo::ARCHIVE_NAME', 'foo', 'bar'))
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', 'repo::ARCHIVE_NAME', 'foo', 'bar'), capture_output=False
)
module.create_archive(
dry_run=False,
@@ -746,7 +794,9 @@ def test_create_archive_with_archive_name_format_accepts_borg_placeholders():
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(())
insert_subprocess_mock(('borg', 'create', 'repo::Documents_{hostname}-{now}', 'foo', 'bar'))
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', 'repo::Documents_{hostname}-{now}', 'foo', 'bar'), capture_output=False
)
module.create_archive(
dry_run=False,
+2 -3
View File
@@ -42,8 +42,7 @@ def test_initialize_without_configuration_should_not_set_environment():
try:
os.environ = {}
module.initialize({})
assert os.environ.get('BORG_PASSCOMMAND') is None
assert os.environ.get('BORG_PASSPHRASE') is None
assert os.environ.get('BORG_RSH') is None
assert sum(1 for key in os.environ.keys() if key.startswith('BORG_')) == 0
finally:
os.environ = orig_environ
+26
View File
@@ -0,0 +1,26 @@
from flexmock import flexmock
from borgmatic.borg import execute as module
def test_execute_command_calls_full_command():
full_command = ['foo', 'bar']
subprocess = flexmock(module.subprocess)
subprocess.should_receive('check_call').with_args(full_command).once()
output = module.execute_command(full_command)
assert output is None
def test_execute_command_captures_output():
full_command = ['foo', 'bar']
expected_output = '[]'
subprocess = flexmock(module.subprocess)
subprocess.should_receive('check_output').with_args(full_command).and_return(
flexmock(decode=lambda: expected_output)
).once()
output = module.execute_command(full_command, capture_output=True)
assert output == expected_output
+1
View File
@@ -4,6 +4,7 @@ import sys
from flexmock import flexmock
from borgmatic.borg import extract as module
from ..test_verbosity import insert_logging_mock
+23 -14
View File
@@ -3,56 +3,65 @@ import logging
from flexmock import flexmock
from borgmatic.borg import info as module
from ..test_verbosity import insert_logging_mock
def insert_subprocess_mock(check_call_command, **kwargs):
subprocess = flexmock(module.subprocess)
subprocess.should_receive('check_output').with_args(check_call_command, **kwargs).once()
INFO_COMMAND = ('borg', 'info', 'repo')
def test_display_archives_info_calls_borg_with_parameters():
insert_subprocess_mock(INFO_COMMAND)
flexmock(module).should_receive('execute_command').with_args(INFO_COMMAND, capture_output=False)
module.display_archives_info(repository='repo', storage_config={})
def test_display_archives_info_with_log_info_calls_borg_with_info_parameter():
insert_subprocess_mock(INFO_COMMAND + ('--info',))
flexmock(module).should_receive('execute_command').with_args(
INFO_COMMAND + ('--info',), capture_output=False
)
insert_logging_mock(logging.INFO)
module.display_archives_info(repository='repo', storage_config={})
def test_display_archives_info_with_log_debug_calls_borg_with_debug_parameter():
insert_subprocess_mock(INFO_COMMAND + ('--debug', '--show-rc'))
flexmock(module).should_receive('execute_command').with_args(
INFO_COMMAND + ('--debug', '--show-rc'), capture_output=False
)
insert_logging_mock(logging.DEBUG)
module.display_archives_info(repository='repo', storage_config={})
def test_display_archives_info_with_json_calls_borg_with_json_parameter():
insert_subprocess_mock(INFO_COMMAND + ('--json',))
flexmock(module).should_receive('execute_command').with_args(
INFO_COMMAND + ('--json',), capture_output=True
).and_return('[]')
module.display_archives_info(repository='repo', storage_config={}, json=True)
json_output = module.display_archives_info(repository='repo', storage_config={}, json=True)
assert json_output == '[]'
def test_display_archives_info_with_local_path_calls_borg_via_local_path():
insert_subprocess_mock(('borg1',) + INFO_COMMAND[1:])
flexmock(module).should_receive('execute_command').with_args(
('borg1',) + INFO_COMMAND[1:], capture_output=False
)
module.display_archives_info(repository='repo', storage_config={}, local_path='borg1')
def test_display_archives_info_with_remote_path_calls_borg_with_remote_path_parameters():
insert_subprocess_mock(INFO_COMMAND + ('--remote-path', 'borg1'))
flexmock(module).should_receive('execute_command').with_args(
INFO_COMMAND + ('--remote-path', 'borg1'), capture_output=False
)
module.display_archives_info(repository='repo', storage_config={}, remote_path='borg1')
def test_display_archives_info_with_lock_wait_calls_borg_with_lock_wait_parameters():
storage_config = {'lock_wait': 5}
insert_subprocess_mock(INFO_COMMAND + ('--lock-wait', '5'))
flexmock(module).should_receive('execute_command').with_args(
INFO_COMMAND + ('--lock-wait', '5'), capture_output=False
)
module.display_archives_info(repository='repo', storage_config=storage_config)
+1 -1
View File
@@ -3,8 +3,8 @@ import logging
from flexmock import flexmock
from borgmatic.borg import init as module
from ..test_verbosity import insert_logging_mock
from ..test_verbosity import insert_logging_mock
INFO_REPOSITORY_EXISTS_RESPONSE_CODE = 0
INFO_REPOSITORY_NOT_FOUND_RESPONSE_CODE = 2
+26 -15
View File
@@ -3,32 +3,31 @@ import logging
from flexmock import flexmock
from borgmatic.borg import list as module
from ..test_verbosity import insert_logging_mock
def insert_subprocess_mock(check_call_command, **kwargs):
subprocess = flexmock(module.subprocess)
subprocess.should_receive('check_output').with_args(check_call_command, **kwargs).once()
LIST_COMMAND = ('borg', 'list', 'repo')
def test_list_archives_calls_borg_with_parameters():
insert_subprocess_mock(LIST_COMMAND)
flexmock(module).should_receive('execute_command').with_args(LIST_COMMAND, capture_output=False)
module.list_archives(repository='repo', storage_config={})
def test_list_archives_with_log_info_calls_borg_with_info_parameter():
insert_subprocess_mock(LIST_COMMAND + ('--info',))
flexmock(module).should_receive('execute_command').with_args(
LIST_COMMAND + ('--info',), capture_output=False
)
insert_logging_mock(logging.INFO)
module.list_archives(repository='repo', storage_config={})
def test_list_archives_with_log_debug_calls_borg_with_debug_parameter():
insert_subprocess_mock(LIST_COMMAND + ('--debug', '--show-rc'))
flexmock(module).should_receive('execute_command').with_args(
LIST_COMMAND + ('--debug', '--show-rc'), capture_output=False
)
insert_logging_mock(logging.DEBUG)
module.list_archives(repository='repo', storage_config={})
@@ -36,31 +35,43 @@ def test_list_archives_with_log_debug_calls_borg_with_debug_parameter():
def test_list_archives_with_lock_wait_calls_borg_with_lock_wait_parameters():
storage_config = {'lock_wait': 5}
insert_subprocess_mock(LIST_COMMAND + ('--lock-wait', '5'))
flexmock(module).should_receive('execute_command').with_args(
LIST_COMMAND + ('--lock-wait', '5'), capture_output=False
)
module.list_archives(repository='repo', storage_config=storage_config)
def test_list_archives_with_archive_calls_borg_with_archive_parameter():
storage_config = {}
insert_subprocess_mock(('borg', 'list', 'repo::archive'))
flexmock(module).should_receive('execute_command').with_args(
('borg', 'list', 'repo::archive'), capture_output=False
)
module.list_archives(repository='repo', storage_config=storage_config, archive='archive')
def test_list_archives_with_local_path_calls_borg_via_local_path():
insert_subprocess_mock(('borg1',) + LIST_COMMAND[1:])
flexmock(module).should_receive('execute_command').with_args(
('borg1',) + LIST_COMMAND[1:], capture_output=False
)
module.list_archives(repository='repo', storage_config={}, local_path='borg1')
def test_list_archives_with_remote_path_calls_borg_with_remote_path_parameters():
insert_subprocess_mock(LIST_COMMAND + ('--remote-path', 'borg1'))
flexmock(module).should_receive('execute_command').with_args(
LIST_COMMAND + ('--remote-path', 'borg1'), capture_output=False
)
module.list_archives(repository='repo', storage_config={}, remote_path='borg1')
def test_list_archives_with_json_calls_borg_with_json_parameter():
insert_subprocess_mock(LIST_COMMAND + ('--json',))
flexmock(module).should_receive('execute_command').with_args(
LIST_COMMAND + ('--json',), capture_output=True
).and_return('[]')
module.list_archives(repository='repo', storage_config={}, json=True)
json_output = module.list_archives(repository='repo', storage_config={}, json=True)
assert json_output == '[]'
+1
View File
@@ -4,6 +4,7 @@ from collections import OrderedDict
from flexmock import flexmock
from borgmatic.borg import prune as module
from ..test_verbosity import insert_logging_mock
+17 -48
View File
@@ -1,55 +1,11 @@
import json
import sys
from flexmock import flexmock
from borgmatic.commands import borgmatic as module
def test_run_commands_handles_multiple_json_outputs_in_array():
(
flexmock(module)
.should_receive('_run_commands_on_repository')
.times(3)
.replace_with(
lambda args, consistency, json_results, local_path, location, remote_path, retention, storage, unexpanded_repository: json_results.append(
{"whatever": unexpanded_repository}
)
)
)
(
flexmock(sys.stdout)
.should_call("write")
.with_args(
json.dumps(
json.loads(
'''
[
{"whatever": "fake_repo1"},
{"whatever": "fake_repo2"},
{"whatever": "fake_repo3"}
]
'''
)
)
)
)
module._run_commands(
args=flexmock(json=True),
consistency=None,
local_path=None,
location={'repositories': ['fake_repo1', 'fake_repo2', 'fake_repo3']},
remote_path=None,
retention=None,
storage=None,
)
def test_collect_configuration_run_summary_logs_info_for_success():
flexmock(module.validate).should_receive('parse_configuration').and_return({'test.yaml': {}})
flexmock(module).should_receive('run_configuration')
flexmock(module).should_receive('run_configuration').and_return([])
args = flexmock(extract=False, list=False)
logs = tuple(module.collect_configuration_run_summary_logs(('test.yaml',), args=args))
@@ -60,7 +16,7 @@ def test_collect_configuration_run_summary_logs_info_for_success():
def test_collect_configuration_run_summary_logs_info_for_success_with_extract():
flexmock(module.validate).should_receive('parse_configuration').and_return({'test.yaml': {}})
flexmock(module.validate).should_receive('guard_configuration_contains_repository')
flexmock(module).should_receive('run_configuration')
flexmock(module).should_receive('run_configuration').and_return([])
args = flexmock(extract=True, list=False, repository='repo')
logs = tuple(module.collect_configuration_run_summary_logs(('test.yaml',), args=args))
@@ -94,7 +50,7 @@ def test_collect_configuration_run_summary_logs_critical_for_list_with_archive_a
def test_collect_configuration_run_summary_logs_info_for_success_with_list():
flexmock(module.validate).should_receive('parse_configuration').and_return({'test.yaml': {}})
flexmock(module).should_receive('run_configuration')
flexmock(module).should_receive('run_configuration').and_return([])
args = flexmock(extract=False, list=True, repository='repo', archive=None)
logs = tuple(module.collect_configuration_run_summary_logs(('test.yaml',), args=args))
@@ -124,9 +80,22 @@ def test_collect_configuration_run_summary_logs_critical_for_run_error():
def test_collect_configuration_run_summary_logs_critical_for_missing_configs():
flexmock(module.validate).should_receive('parse_configuration').and_return({'test.yaml': {}})
flexmock(module).should_receive('run_configuration')
flexmock(module).should_receive('run_configuration').and_return([])
args = flexmock(config_paths=(), extract=False, list=False)
logs = tuple(module.collect_configuration_run_summary_logs(config_filenames=(), args=args))
assert any(log for log in logs if log.levelno == module.logging.CRITICAL)
def test_collect_configuration_run_summary_logs_outputs_merged_json_results():
flexmock(module.validate).should_receive('parse_configuration').and_return(
{'test.yaml': {}, 'test2.yaml': {}}
)
flexmock(module).should_receive('run_configuration').and_return(['foo', 'bar']).and_return(
['baz']
)
flexmock(module.sys.stdout).should_receive('write').with_args('["foo", "bar", "baz"]').once()
args = flexmock(extract=False, list=False)
tuple(module.collect_configuration_run_summary_logs(('test.yaml', 'test2.yaml'), args=args))
@@ -7,7 +7,7 @@ def test_execute_hook_invokes_each_command():
subprocess = flexmock(module.subprocess)
subprocess.should_receive('check_call').with_args(':', shell=True).once()
module.execute_hook([':'], 'config.yaml', 'pre-backup')
module.execute_hook([':'], 'config.yaml', 'pre-backup', dry_run=False)
def test_execute_hook_with_multiple_commands_invokes_each_command():
@@ -15,8 +15,15 @@ def test_execute_hook_with_multiple_commands_invokes_each_command():
subprocess.should_receive('check_call').with_args(':', shell=True).once()
subprocess.should_receive('check_call').with_args('true', shell=True).once()
module.execute_hook([':', 'true'], 'config.yaml', 'pre-backup')
module.execute_hook([':', 'true'], 'config.yaml', 'pre-backup', dry_run=False)
def test_execute_hook_with_dry_run_skips_commands():
subprocess = flexmock(module.subprocess)
subprocess.should_receive('check_call').never()
module.execute_hook([':', 'true'], 'config.yaml', 'pre-backup', dry_run=True)
def test_execute_hook_with_empty_commands_does_not_raise():
module.execute_hook([], 'config.yaml', 'post-backup')
module.execute_hook([], 'config.yaml', 'post-backup', dry_run=False)
+10 -2
View File
@@ -1,3 +1,5 @@
import sys
from flexmock import flexmock
from borgmatic.config import collect as module
@@ -37,7 +39,10 @@ def test_collect_config_filenames_collects_files_from_given_directories_and_igno
mock_path.should_receive('isdir').with_args('/etc/borgmatic.d/foo.yaml').and_return(False)
mock_path.should_receive('isdir').with_args('/etc/borgmatic.d/bar').and_return(True)
mock_path.should_receive('isdir').with_args('/etc/borgmatic.d/baz.yaml').and_return(False)
flexmock(module.os).should_receive('listdir').and_return(['foo.yaml', 'bar', 'baz.yaml'])
flexmock(module.os).should_receive('listdir')
flexmock(sys.modules['builtins']).should_receive('sorted').and_return(
['foo.yaml', 'bar', 'baz.yaml']
)
config_filenames = tuple(module.collect_config_filenames(config_paths))
@@ -56,7 +61,10 @@ def test_collect_config_filenames_collects_files_from_given_directories_and_igno
mock_path.should_receive('isdir').with_args('/etc/borgmatic.d/foo.yaml').and_return(False)
mock_path.should_receive('isdir').with_args('/etc/borgmatic.d/bar.yaml~').and_return(False)
mock_path.should_receive('isdir').with_args('/etc/borgmatic.d/baz.txt').and_return(False)
flexmock(module.os).should_receive('listdir').and_return(['foo.yaml', 'bar.yaml~', 'baz.txt'])
flexmock(module.os).should_receive('listdir')
flexmock(sys.modules['builtins']).should_receive('sorted').and_return(
['foo.yaml', 'bar.yaml~', 'baz.txt']
)
config_filenames = tuple(module.collect_config_filenames(config_paths))
+3 -4
View File
@@ -1,12 +1,11 @@
from collections import defaultdict, OrderedDict, namedtuple
import os
from collections import OrderedDict, defaultdict, namedtuple
from flexmock import flexmock
import pytest
from flexmock import flexmock
from borgmatic.config import convert as module
Parsed_config = namedtuple('Parsed_config', ('location', 'storage', 'retention', 'consistency'))
@@ -91,7 +90,7 @@ def test_guard_configuration_upgraded_raises_when_only_source_config_present():
flexmock(os.path).should_receive('exists').with_args('config.yaml').and_return(False)
flexmock(os.path).should_receive('exists').with_args('other.yaml').and_return(False)
with pytest.raises(module.LegacyConfigurationNotUpgraded):
with pytest.raises(module.Legacy_configuration_not_upgraded):
module.guard_configuration_upgraded('config', ('config.yaml', 'other.yaml'))
+1 -1
View File
@@ -1,7 +1,7 @@
from collections import OrderedDict
from flexmock import flexmock
import pytest
from flexmock import flexmock
from borgmatic.config import legacy as module
+74
View File
@@ -0,0 +1,74 @@
import pytest
from flexmock import flexmock
from borgmatic import logger as module
@pytest.mark.parametrize('bool_val', (True, 'yes', 'on', '1', 'true', 'True', 1))
def test_to_bool_parses_true_values(bool_val):
assert module.to_bool(bool_val)
@pytest.mark.parametrize('bool_val', (False, 'no', 'off', '0', 'false', 'False', 0))
def test_to_bool_parses_false_values(bool_val):
assert not module.to_bool(bool_val)
def test_to_bool_passes_none_through():
assert module.to_bool(None) is None
def test_should_do_markup_respects_no_color_value():
assert module.should_do_markup(no_color=True) is False
def test_should_do_markup_respects_PY_COLORS_environment_variable():
flexmock(module.os.environ).should_receive('get').and_return('True')
flexmock(module).should_receive('to_bool').and_return(True)
assert module.should_do_markup(no_color=False) is True
def test_should_do_markup_prefers_no_color_value_to_PY_COLORS():
flexmock(module.os.environ).should_receive('get').and_return('True')
flexmock(module).should_receive('to_bool').and_return(True)
assert module.should_do_markup(no_color=True) is False
def test_should_do_markup_respects_stdout_tty_value():
flexmock(module.os.environ).should_receive('get').and_return(None)
assert module.should_do_markup(no_color=False) is False
def test_should_do_markup_prefers_PY_COLORS_to_stdout_tty_value():
flexmock(module.os.environ).should_receive('get').and_return('True')
flexmock(module).should_receive('to_bool').and_return(True)
assert module.should_do_markup(no_color=False) is True
@pytest.mark.parametrize('method_name', ('critical', 'error', 'warn', 'info', 'debug'))
def test_borgmatic_logger_log_method_does_not_raise(method_name):
flexmock(module).should_receive('color_text')
flexmock(module.logging.Logger).should_receive(method_name)
getattr(module.Borgmatic_logger('test'), method_name)(msg='hi')
def test_borgmatic_logger_handle_does_not_raise():
flexmock(module).should_receive('color_text')
flexmock(module.logging.Logger).should_receive('handle')
module.Borgmatic_logger('test').handle(
module.logging.makeLogRecord(dict(levelno=module.logging.CRITICAL, msg='hi'))
)
def test_color_text_does_not_raise():
module.color_text(module.colorama.Fore.RED, 'hi')
def test_color_text_without_color_does_not_raise():
module.color_text(None, 'hi')
+14 -9
View File
@@ -1,27 +1,32 @@
[tox]
envlist = py3
envlist = py35,py36,py37
skip_missing_interpreters = True
skipsdist = True
minversion = 3.10.0
[testenv]
usedevelop = True
deps = -rtest_requirements.txt
whitelist_externals = sh
install_command =
sh scripts/pip {opts} {packages}
commands =
py.test --cov-report term-missing:skip-covered --cov=borgmatic --ignore=tests/end-to-end \
tests []
sh scripts/black
pytest
py36,py37: black --check .
isort --recursive --check-only --settings-path setup.cfg .
flake8 .
[testenv:black]
basepython = python3.7
commands =
black --skip-string-normalization --line-length 100 .
black .
[testenv:end-to-end]
deps = -rtest_requirements.txt
commands =
py.test tests/end-to-end []
pytest tests/end-to-end
[flake8]
ignore = E501,W503
exclude = *.*/*
[testenv:isort]
deps = {[testenv]deps}
commands =
isort {posargs:--recursive} --settings-path setup.cfg .