Compare commits

...
12 Commits
26 changed files with 253 additions and 198 deletions
+31
View File
@@ -0,0 +1,31 @@
#### What I'm trying to do and why
#### Steps to reproduce (if a bug)
Include (sanitized) borgmatic configuration files if applicable.
#### Actual behavior (if a bug)
Include (sanitized) `--verbosity 2` output if applicable.
#### Expected behavior (if a bug)
#### Other notes / implementation ideas
#### Environment
**borgmatic version:** [version here]
Use `sudo borgmatic --version` or `sudo pip show borgmatic | grep ^Version`
**borgmatic installation method:** [e.g., Debian package, Docker container, etc.]
**Borg version:** [version here]
Use `sudo borg --version`
**Python version:** [version here]
Use `python --version`
**operating system and version:** [OS here]
+8
View File
@@ -1,3 +1,11 @@
1.3.8
* #191: Disable console color via "color" option in borgmatic configuration output section.
1.3.7
* #196: Fix for unclear error message for invalid YAML merge include.
* #197: Don't color syslog output.
* Change default syslog verbosity to show errors only.
1.3.6 1.3.6
* #53: Log to syslog in addition to existing console logging. Add --syslog-verbosity flag to * #53: Log to syslog in addition to existing console logging. Add --syslog-verbosity flag to
customize the log level. See the documentation for more information: customize the log level. See the documentation for more information:
+1 -2
View File
@@ -2,13 +2,12 @@ import logging
from borgmatic.borg import extract from borgmatic.borg import extract
from borgmatic.execute import execute_command from borgmatic.execute import execute_command
from borgmatic.logger import get_logger
DEFAULT_CHECKS = ('repository', 'archives') DEFAULT_CHECKS = ('repository', 'archives')
DEFAULT_PREFIX = '{hostname}-' DEFAULT_PREFIX = '{hostname}-'
logger = get_logger(__name__) logger = logging.getLogger(__name__)
def _parse_checks(consistency_config): def _parse_checks(consistency_config):
+1 -2
View File
@@ -5,9 +5,8 @@ import os
import tempfile import tempfile
from borgmatic.execute import execute_command from borgmatic.execute import execute_command
from borgmatic.logger import get_logger
logger = get_logger(__name__) logger = logging.getLogger(__name__)
def _expand_directory(directory): def _expand_directory(directory):
+1 -2
View File
@@ -1,9 +1,8 @@
import logging import logging
from borgmatic.execute import execute_command from borgmatic.execute import execute_command
from borgmatic.logger import get_logger
logger = get_logger(__name__) logger = logging.getLogger(__name__)
def extract_last_archive_dry_run(repository, lock_wait=None, local_path='borg', remote_path=None): def extract_last_archive_dry_run(repository, lock_wait=None, local_path='borg', remote_path=None):
+1 -2
View File
@@ -1,9 +1,8 @@
import logging import logging
from borgmatic.execute import execute_command from borgmatic.execute import execute_command
from borgmatic.logger import get_logger
logger = get_logger(__name__) logger = logging.getLogger(__name__)
def display_archives_info( def display_archives_info(
+1 -2
View File
@@ -2,9 +2,8 @@ import logging
import subprocess import subprocess
from borgmatic.execute import execute_command from borgmatic.execute import execute_command
from borgmatic.logger import get_logger
logger = get_logger(__name__) logger = logging.getLogger(__name__)
INFO_REPOSITORY_NOT_FOUND_EXIT_CODE = 2 INFO_REPOSITORY_NOT_FOUND_EXIT_CODE = 2
+1 -2
View File
@@ -1,9 +1,8 @@
import logging import logging
from borgmatic.execute import execute_command from borgmatic.execute import execute_command
from borgmatic.logger import get_logger
logger = get_logger(__name__) logger = logging.getLogger(__name__)
def list_archives( def list_archives(
+1 -2
View File
@@ -1,9 +1,8 @@
import logging import logging
from borgmatic.execute import execute_command from borgmatic.execute import execute_command
from borgmatic.logger import get_logger
logger = get_logger(__name__) logger = logging.getLogger(__name__)
def _make_prune_flags(retention_config): def _make_prune_flags(retention_config):
+47 -29
View File
@@ -19,11 +19,11 @@ from borgmatic.borg import init as borg_init
from borgmatic.borg import list as borg_list from borgmatic.borg import list as borg_list
from borgmatic.borg import prune as borg_prune from borgmatic.borg import prune as borg_prune
from borgmatic.config import checks, collect, convert, validate from borgmatic.config import checks, collect, convert, validate
from borgmatic.logger import configure_logging, get_logger, should_do_markup from borgmatic.logger import configure_logging, should_do_markup
from borgmatic.signals import configure_signals from borgmatic.signals import configure_signals
from borgmatic.verbosity import verbosity_to_log_level from borgmatic.verbosity import verbosity_to_log_level
logger = get_logger(__name__) logger = logging.getLogger(__name__)
LEGACY_CONFIG_PATH = '/etc/borgmatic/config' LEGACY_CONFIG_PATH = '/etc/borgmatic/config'
@@ -182,7 +182,7 @@ def parse_arguments(*arguments):
'--syslog-verbosity', '--syslog-verbosity',
type=int, type=int,
choices=range(0, 3), choices=range(0, 3),
default=1, default=0,
help='Display verbose progress to syslog (from none to lots: 0, 1, or 2)', help='Display verbose progress to syslog (from none to lots: 0, 1, or 2)',
) )
common_group.add_argument( common_group.add_argument(
@@ -404,37 +404,50 @@ def run_actions(
yield json.loads(json_output) yield json.loads(json_output)
def collect_configuration_run_summary_logs(config_filenames, args): def load_configurations(config_filenames):
''' '''
Given a sequence of configuration filenames and parsed command-line arguments as an Given a sequence of configuration filenames, load and validate each configuration file. Return
argparse.ArgumentParser instance, run each configuration file and yield a series of the results as a tuple of: dict of configuration filename to corresponding parsed configuration,
logging.LogRecord instances containing summary information about each run. and sequence of logging.LogRecord instances containing any parse errors.
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. # Dict mapping from config filename to corresponding parsed config dict.
configs = collections.OrderedDict() configs = collections.OrderedDict()
logs = []
# Parse and load each configuration file. # Parse and load each configuration file.
for config_filename in config_filenames: for config_filename in config_filenames:
try: try:
logger.info('{}: Parsing configuration file'.format(config_filename))
configs[config_filename] = validate.parse_configuration( configs[config_filename] = validate.parse_configuration(
config_filename, validate.schema_filename() config_filename, validate.schema_filename()
) )
except (ValueError, OSError, validate.Validation_error) as error: except (ValueError, OSError, validate.Validation_error) as error:
yield logging.makeLogRecord( logs.extend(
dict( [
levelno=logging.CRITICAL, logging.makeLogRecord(
levelname='CRITICAL', dict(
msg='{}: Error parsing configuration file'.format(config_filename), levelno=logging.CRITICAL,
) levelname='CRITICAL',
) msg='{}: Error parsing configuration file'.format(config_filename),
yield logging.makeLogRecord( )
dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg=error) ),
logging.makeLogRecord(
dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg=error)
),
]
) )
return (configs, logs)
def collect_configuration_run_summary_logs(configs, args):
'''
Given a dict of configuration filename to corresponding parsed configuration, 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.
'''
# Run cross-file validation checks. # Run cross-file validation checks.
if args.extract or (args.list and args.archive): if args.extract or (args.list and args.archive):
try: try:
@@ -472,7 +485,7 @@ def collect_configuration_run_summary_logs(config_filenames, args):
if json_results: if json_results:
sys.stdout.write(json.dumps(json_results)) sys.stdout.write(json.dumps(json_results))
if not config_filenames: if not configs:
yield logging.makeLogRecord( yield logging.makeLogRecord(
dict( dict(
levelno=logging.CRITICAL, levelno=logging.CRITICAL,
@@ -507,25 +520,30 @@ def main(): # pragma: no cover
logger.critical('Error parsing arguments: {}'.format(' '.join(sys.argv))) logger.critical('Error parsing arguments: {}'.format(' '.join(sys.argv)))
exit_with_help_link() exit_with_help_link()
colorama.init(autoreset=True, strip=not should_do_markup(args.no_color))
configure_logging(
verbosity_to_log_level(args.verbosity), verbosity_to_log_level(args.syslog_verbosity)
)
if args.version: if args.version:
print(pkg_resources.require('borgmatic')[0].version) print(pkg_resources.require('borgmatic')[0].version)
sys.exit(0) sys.exit(0)
config_filenames = tuple(collect.collect_config_filenames(args.config_paths)) config_filenames = tuple(collect.collect_config_filenames(args.config_paths))
configs, parse_logs = load_configurations(config_filenames)
colorama.init(autoreset=True, strip=not should_do_markup(args.no_color, configs))
configure_logging(
verbosity_to_log_level(args.verbosity), verbosity_to_log_level(args.syslog_verbosity)
)
logger.debug('Ensuring legacy configuration is upgraded') logger.debug('Ensuring legacy configuration is upgraded')
convert.guard_configuration_upgraded(LEGACY_CONFIG_PATH, config_filenames) convert.guard_configuration_upgraded(LEGACY_CONFIG_PATH, config_filenames)
summary_logs = tuple(collect_configuration_run_summary_logs(config_filenames, args)) summary_logs = list(collect_configuration_run_summary_logs(configs, args))
logger.info('') logger.info('')
logger.info('summary:') logger.info('summary:')
[logger.handle(log) for log in summary_logs if log.levelno >= logger.getEffectiveLevel()] [
logger.handle(log)
for log in parse_logs + summary_logs
if log.levelno >= logger.getEffectiveLevel()
]
if any(log.levelno == logging.CRITICAL for log in summary_logs): if any(log.levelno == logging.CRITICAL for log in summary_logs):
exit_with_help_link() exit_with_help_link()
+1 -2
View File
@@ -3,9 +3,8 @@ import sys
from argparse import ArgumentParser from argparse import ArgumentParser
from borgmatic.config import collect, validate from borgmatic.config import collect, validate
from borgmatic.logger import get_logger
logger = get_logger(__name__) logger = logging.getLogger(__name__)
def parse_arguments(*arguments): def parse_arguments(*arguments):
+3 -6
View File
@@ -1,10 +1,9 @@
import logging
import os import os
import ruamel.yaml import ruamel.yaml
from borgmatic.logger import get_logger logger = logging.getLogger(__name__)
logger = get_logger(__name__)
def load_configuration(filename): def load_configuration(filename):
@@ -54,9 +53,7 @@ class Include_constructor(ruamel.yaml.SafeConstructor):
for index, (key_node, value_node) in enumerate(node.value): for index, (key_node, value_node) in enumerate(node.value):
if key_node.tag == u'tag:yaml.org,2002:merge' and value_node.tag == '!include': if key_node.tag == u'tag:yaml.org,2002:merge' and value_node.tag == '!include':
included_value = representer.represent_mapping( included_value = representer.represent_data(self.construct_object(value_node))
tag='tag:yaml.org,2002:map', mapping=self.construct_object(value_node)
)
node.value[index] = (key_node, included_value) node.value[index] = (key_node, included_value)
super(Include_constructor, self).flatten_mapping(node) super(Include_constructor, self).flatten_mapping(node)
+10
View File
@@ -301,6 +301,16 @@ map:
this prefix. Borg placeholders can be used. See the output of this prefix. Borg placeholders can be used. See the output of
"borg help placeholders" for details. Defaults to "{hostname}-". "borg help placeholders" for details. Defaults to "{hostname}-".
example: sourcehostname example: sourcehostname
output:
desc: |
Options for customizing borgmatic's own output and logging.
map:
color:
type: bool
desc: |
Apply color to console output. Can be overridden with --no-color command-line
flag. Defaults to true.
example: false
hooks: hooks:
desc: | desc: |
Shell commands or scripts to execute before and after a backup or if an error has occurred. Shell commands or scripts to execute before and after a backup or if an error has occurred.
-10
View File
@@ -6,9 +6,6 @@ import pykwalify.errors
import ruamel.yaml import ruamel.yaml
from borgmatic.config import load from borgmatic.config import load
from borgmatic.logger import get_logger
logger = get_logger(__name__)
def schema_filename(): def schema_filename():
@@ -66,13 +63,6 @@ def apply_logical_validation(config_filename, parsed_configuration):
), ),
) )
consistency_prefix = parsed_configuration.get('consistency', {}).get('prefix')
if archive_name_format and not consistency_prefix:
logger.warning(
'Since version 1.1.16, if you provide `archive_name_format`, you should also'
' specify `consistency.prefix`.'
)
def parse_configuration(config_filename, schema_filename): def parse_configuration(config_filename, schema_filename):
''' '''
+1 -3
View File
@@ -1,9 +1,7 @@
import logging import logging
import subprocess import subprocess
from borgmatic.logger import get_logger logger = logging.getLogger(__name__)
logger = get_logger(__name__)
def execute_and_log_output(full_command, output_log_level, shell): def execute_and_log_output(full_command, output_log_level, shell):
+1 -2
View File
@@ -2,9 +2,8 @@ import logging
import os import os
from borgmatic import execute from borgmatic import execute
from borgmatic.logger import get_logger
logger = get_logger(__name__) logger = logging.getLogger(__name__)
def execute_hook(commands, umask, config_filename, description, dry_run): def execute_hook(commands, umask, config_filename, description, dry_run):
+10 -48
View File
@@ -21,13 +21,17 @@ def to_bool(arg):
return False return False
def should_do_markup(no_color): def should_do_markup(no_color, configs):
''' '''
Determine if we should enable colorama marking up. Given the value of the command-line no-color argument, and a dict of configuration filename to
corresponding parsed configuration, determine if we should enable colorama marking up.
''' '''
if no_color: if no_color:
return False return False
if any(config.get('output', {}).get('color') is False for config in configs.values()):
return False
py_colors = os.environ.get('PY_COLORS', None) py_colors = os.environ.get('PY_COLORS', None)
if py_colors is not None: if py_colors is not None:
@@ -45,52 +49,10 @@ LOG_LEVEL_TO_COLOR = {
} }
class Borgmatic_logger(logging.Logger): class Console_color_formatter(logging.Formatter):
def critical(self, msg, *args, **kwargs): def format(self, record):
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) color = LOG_LEVEL_TO_COLOR.get(record.levelno)
colored_record = logging.makeLogRecord( return color_text(color, record.msg)
dict(
levelno=record.levelno,
levelname=record.levelname,
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): def color_text(color, message):
@@ -111,7 +73,7 @@ def configure_logging(console_log_level, syslog_log_level=None):
syslog_log_level = console_log_level syslog_log_level = console_log_level
console_handler = logging.StreamHandler() console_handler = logging.StreamHandler()
console_handler.setFormatter(logging.Formatter('%(message)s')) console_handler.setFormatter(Console_color_formatter())
console_handler.setLevel(console_log_level) console_handler.setLevel(console_log_level)
syslog_path = None syslog_path = None
+3 -3
View File
@@ -42,11 +42,11 @@ using systemd, try running `journalctl -xe`. Otherwise, try viewing
You can customize the log level used for syslog logging with the You can customize the log level used for syslog logging with the
`--syslog-verbosity` flag, and this is independent from the console logging `--syslog-verbosity` flag, and this is independent from the console logging
`--verbosity` flag described above. For instance, to disable syslog logging `--verbosity` flag described above. For instance, to get additional
except for errors: information about the progress of the backup as it proceeds:
```bash ```bash
borgmatic --syslog-verbosity 0 borgmatic --syslog-verbosity 1
``` ```
Or to increase syslog logging to include debug spew: Or to increase syslog logging to include debug spew:
@@ -105,6 +105,10 @@ include, the local file's option takes precedent. And note that this is a
shallow merge rather than a deep merge, so the merging does not descend into shallow merge rather than a deep merge, so the merging does not descend into
nested values. nested values.
Note that this `<<` include merging syntax is only for merging in mappings
(keys/values). If you'd like to include other types like scalars or lists
directly, please see the section above about standard includes.
## Related documentation ## Related documentation
+6 -5
View File
@@ -199,12 +199,13 @@ sudo systemctl start borgmatic.timer
Feel free to modify the timer file based on how frequently you'd like Feel free to modify the timer file based on how frequently you'd like
borgmatic to run. borgmatic to run.
## Colored Output ## Colored output
Borgmatic uses [colorama](https://pypi.org/project/colorama/) to produce Borgmatic produces colored terminal output by default. It is disabled when a
colored terminal output by default. It is disabled when a non-interactive non-interactive terminal is detected (like a cron job). Otherwise, you can
terminal is detected (like a cron job). Otherwise, it can be disabled by disable it by passing the `--no-color` flag, setting the environment variable
passing `--no-color` or by setting the environment variable `PY_COLORS=False`. `PY_COLORS=False`, or setting the `color` option to `false` in the `output`
section of configuration.
## Troubleshooting ## Troubleshooting
+1 -1
View File
@@ -1,6 +1,6 @@
from setuptools import find_packages, setup from setuptools import find_packages, setup
VERSION = '1.3.6' VERSION = '1.3.8'
setup( setup(
+3 -3
View File
@@ -15,7 +15,7 @@ def test_parse_arguments_with_no_arguments_uses_defaults():
assert parser.config_paths == config_paths assert parser.config_paths == config_paths
assert parser.excludes_filename is None assert parser.excludes_filename is None
assert parser.verbosity == 0 assert parser.verbosity == 0
assert parser.syslog_verbosity == 1 assert parser.syslog_verbosity == 0
assert parser.json is False assert parser.json is False
@@ -26,7 +26,7 @@ def test_parse_arguments_with_multiple_config_paths_parses_as_list():
assert parser.config_paths == ['myconfig', 'otherconfig'] assert parser.config_paths == ['myconfig', 'otherconfig']
assert parser.verbosity == 0 assert parser.verbosity == 0
assert parser.syslog_verbosity == 1 assert parser.syslog_verbosity == 0
def test_parse_arguments_with_verbosity_overrides_default(): def test_parse_arguments_with_verbosity_overrides_default():
@@ -38,7 +38,7 @@ def test_parse_arguments_with_verbosity_overrides_default():
assert parser.config_paths == config_paths assert parser.config_paths == config_paths
assert parser.excludes_filename is None assert parser.excludes_filename is None
assert parser.verbosity == 1 assert parser.verbosity == 1
assert parser.syslog_verbosity == 1 assert parser.syslog_verbosity == 0
def test_parse_arguments_with_syslog_verbosity_overrides_default(): def test_parse_arguments_with_syslog_verbosity_overrides_default():
+22
View File
@@ -1,5 +1,7 @@
import sys import sys
import pytest
import ruamel.yaml
from flexmock import flexmock from flexmock import flexmock
from borgmatic.config import load as module from borgmatic.config import load as module
@@ -38,3 +40,23 @@ def test_load_configuration_merges_include():
) )
assert module.load_configuration('config.yaml') == {'foo': 'override', 'baz': 'quux'} assert module.load_configuration('config.yaml') == {'foo': 'override', 'baz': 'quux'}
def test_load_configuration_does_not_merge_include_list():
builtins = flexmock(sys.modules['builtins'])
builtins.should_receive('open').with_args('include.yaml').and_return(
'''
- one
- two
'''
)
builtins.should_receive('open').with_args('config.yaml').and_return(
'''
foo: bar
repositories:
<<: !include include.yaml
'''
)
with pytest.raises(ruamel.yaml.error.YAMLError):
assert module.load_configuration('config.yaml')
+42 -35
View File
@@ -3,99 +3,106 @@ from flexmock import flexmock
from borgmatic.commands import borgmatic as module from borgmatic.commands import borgmatic as module
def test_load_configurations_collects_parsed_configurations():
configuration = flexmock()
other_configuration = flexmock()
flexmock(module.validate).should_receive('parse_configuration').and_return(
configuration
).and_return(other_configuration)
configs, logs = tuple(module.load_configurations(('test.yaml', 'other.yaml')))
assert configs == {'test.yaml': configuration, 'other.yaml': other_configuration}
assert logs == []
def test_load_configurations_logs_critical_for_parse_error():
flexmock(module.validate).should_receive('parse_configuration').and_raise(ValueError)
configs, logs = tuple(module.load_configurations(('test.yaml',)))
assert configs == {}
assert any(log for log in logs if log.levelno == module.logging.CRITICAL)
def test_collect_configuration_run_summary_logs_info_for_success(): 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').and_return([]) flexmock(module).should_receive('run_configuration').and_return([])
args = flexmock(extract=False, list=False) args = flexmock(extract=False, list=False)
logs = tuple(module.collect_configuration_run_summary_logs(('test.yaml',), args=args)) logs = tuple(module.collect_configuration_run_summary_logs({'test.yaml': {}}, args=args))
assert all(log for log in logs if log.levelno == module.logging.INFO) assert all(log for log in logs if log.levelno == module.logging.INFO)
def test_collect_configuration_run_summary_logs_info_for_success_with_extract(): 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.validate).should_receive('guard_configuration_contains_repository')
flexmock(module).should_receive('run_configuration').and_return([]) flexmock(module).should_receive('run_configuration').and_return([])
args = flexmock(extract=True, list=False, repository='repo') args = flexmock(extract=True, list=False, repository='repo')
logs = tuple(module.collect_configuration_run_summary_logs(('test.yaml',), args=args)) logs = tuple(module.collect_configuration_run_summary_logs({'test.yaml': {}}, args=args))
assert all(log for log in logs if log.levelno == module.logging.INFO) assert all(log for log in logs if log.levelno == module.logging.INFO)
def test_collect_configuration_run_summary_logs_critical_for_extract_with_repository_error(): def test_collect_configuration_run_summary_logs_critical_for_extract_with_repository_error():
flexmock(module.validate).should_receive('parse_configuration').and_return({'test.yaml': {}})
flexmock(module.validate).should_receive('guard_configuration_contains_repository').and_raise( flexmock(module.validate).should_receive('guard_configuration_contains_repository').and_raise(
ValueError ValueError
) )
args = flexmock(extract=True, list=False, repository='repo') args = flexmock(extract=True, list=False, repository='repo')
logs = tuple(module.collect_configuration_run_summary_logs(('test.yaml',), args=args)) logs = tuple(module.collect_configuration_run_summary_logs({'test.yaml': {}}, args=args))
assert any(log for log in logs if log.levelno == module.logging.CRITICAL) assert any(log for log in logs if log.levelno == module.logging.CRITICAL)
def test_collect_configuration_run_summary_logs_critical_for_list_with_archive_and_repository_error(): def test_collect_configuration_run_summary_logs_critical_for_list_with_archive_and_repository_error():
flexmock(module.validate).should_receive('parse_configuration').and_return({'test.yaml': {}})
flexmock(module.validate).should_receive('guard_configuration_contains_repository').and_raise( flexmock(module.validate).should_receive('guard_configuration_contains_repository').and_raise(
ValueError ValueError
) )
args = flexmock(extract=False, list=True, repository='repo', archive='test') args = flexmock(extract=False, list=True, repository='repo', archive='test')
logs = tuple(module.collect_configuration_run_summary_logs(('test.yaml',), args=args)) logs = tuple(module.collect_configuration_run_summary_logs({'test.yaml': {}}, args=args))
assert any(log for log in logs if log.levelno == module.logging.CRITICAL) assert any(log for log in logs if log.levelno == module.logging.CRITICAL)
def test_collect_configuration_run_summary_logs_info_for_success_with_list(): 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').and_return([]) flexmock(module).should_receive('run_configuration').and_return([])
args = flexmock(extract=False, list=True, repository='repo', archive=None) args = flexmock(extract=False, list=True, repository='repo', archive=None)
logs = tuple(module.collect_configuration_run_summary_logs(('test.yaml',), args=args)) logs = tuple(module.collect_configuration_run_summary_logs({'test.yaml': {}}, args=args))
assert all(log for log in logs if log.levelno == module.logging.INFO) assert all(log for log in logs if log.levelno == module.logging.INFO)
def test_collect_configuration_run_summary_logs_critical_for_parse_error():
flexmock(module.validate).should_receive('parse_configuration').and_raise(ValueError)
args = flexmock(extract=False, list=False)
logs = tuple(module.collect_configuration_run_summary_logs(('test.yaml',), args=args))
assert any(log for log in logs if log.levelno == module.logging.CRITICAL)
def test_collect_configuration_run_summary_logs_critical_for_run_error(): def test_collect_configuration_run_summary_logs_critical_for_run_error():
flexmock(module.validate).should_receive('parse_configuration').and_return({'test.yaml': {}})
flexmock(module.validate).should_receive('guard_configuration_contains_repository') flexmock(module.validate).should_receive('guard_configuration_contains_repository')
flexmock(module).should_receive('run_configuration').and_raise(ValueError) flexmock(module).should_receive('run_configuration').and_raise(ValueError)
args = flexmock(extract=False, list=False) args = flexmock(extract=False, list=False)
logs = tuple(module.collect_configuration_run_summary_logs(('test.yaml',), args=args)) logs = tuple(module.collect_configuration_run_summary_logs({'test.yaml': {}}, args=args))
assert any(log for log in logs if log.levelno == module.logging.CRITICAL)
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').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) assert any(log for log in logs if log.levelno == module.logging.CRITICAL)
def test_collect_configuration_run_summary_logs_outputs_merged_json_results(): 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( flexmock(module).should_receive('run_configuration').and_return(['foo', 'bar']).and_return(
['baz'] ['baz']
) )
flexmock(module.sys.stdout).should_receive('write').with_args('["foo", "bar", "baz"]').once() flexmock(module.sys.stdout).should_receive('write').with_args('["foo", "bar", "baz"]').once()
args = flexmock(extract=False, list=False) args = flexmock(extract=False, list=False)
tuple(module.collect_configuration_run_summary_logs(('test.yaml', 'test2.yaml'), args=args)) tuple(
module.collect_configuration_run_summary_logs(
{'test.yaml': {}, 'test2.yaml': {}}, args=args
)
)
def test_collect_configuration_run_summary_logs_critical_for_missing_configs():
flexmock(module).should_receive('run_configuration').and_return([])
args = flexmock(extract=False, list=False, config_paths=[])
logs = tuple(module.collect_configuration_run_summary_logs({}, args=args))
assert any(log for log in logs if log.levelno == module.logging.CRITICAL)
+1 -19
View File
@@ -1,5 +1,4 @@
import pytest import pytest
from flexmock import flexmock
from borgmatic.config import validate as module from borgmatic.config import validate as module
@@ -37,20 +36,6 @@ def test_apply_logical_validation_raises_if_archive_name_format_present_without_
) )
def test_apply_logical_validation_warns_if_archive_name_format_present_without_consistency_prefix():
logger = flexmock(module.logger)
logger.should_receive('warning').once()
module.apply_logical_validation(
'config.yaml',
{
'storage': {'archive_name_format': '{hostname}-{now}'},
'retention': {'prefix': '{hostname}-'},
'consistency': {},
},
)
def test_apply_locical_validation_raises_if_unknown_repository_in_check_repositories(): def test_apply_locical_validation_raises_if_unknown_repository_in_check_repositories():
with pytest.raises(module.Validation_error): with pytest.raises(module.Validation_error):
module.apply_logical_validation( module.apply_logical_validation(
@@ -74,10 +59,7 @@ def test_apply_locical_validation_does_not_raise_if_known_repository_in_check_re
) )
def test_apply_logical_validation_does_not_raise_or_warn_if_archive_name_format_and_prefix_present(): def test_apply_logical_validation_does_not_raise_if_archive_name_format_and_prefix_present():
logger = flexmock(module.logger)
logger.should_receive('warning').never()
module.apply_logical_validation( module.apply_logical_validation(
'config.yaml', 'config.yaml',
{ {
+52 -18
View File
@@ -21,51 +21,81 @@ def test_to_bool_passes_none_through():
def test_should_do_markup_respects_no_color_value(): def test_should_do_markup_respects_no_color_value():
assert module.should_do_markup(no_color=True) is False assert module.should_do_markup(no_color=True, configs={}) is False
def test_should_do_markup_respects_config_value():
assert (
module.should_do_markup(no_color=False, configs={'foo.yaml': {'output': {'color': False}}})
is False
)
def test_should_do_markup_prefers_any_false_config_value():
assert (
module.should_do_markup(
no_color=False,
configs={
'foo.yaml': {'output': {'color': True}},
'bar.yaml': {'output': {'color': False}},
},
)
is False
)
def test_should_do_markup_respects_PY_COLORS_environment_variable(): def test_should_do_markup_respects_PY_COLORS_environment_variable():
flexmock(module.os.environ).should_receive('get').and_return('True') flexmock(module.os.environ).should_receive('get').and_return('True')
flexmock(module).should_receive('to_bool').and_return(True) flexmock(module).should_receive('to_bool').and_return(True)
assert module.should_do_markup(no_color=False) is True assert module.should_do_markup(no_color=False, configs={}) is True
def test_should_do_markup_prefers_no_color_value_to_config_value():
assert (
module.should_do_markup(no_color=True, configs={'foo.yaml': {'output': {'color': True}}})
is False
)
def test_should_do_markup_prefers_config_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=False, configs={'foo.yaml': {'output': {'color': False}}})
is False
)
def test_should_do_markup_prefers_no_color_value_to_PY_COLORS(): def test_should_do_markup_prefers_no_color_value_to_PY_COLORS():
flexmock(module.os.environ).should_receive('get').and_return('True') flexmock(module.os.environ).should_receive('get').and_return('True')
flexmock(module).should_receive('to_bool').and_return(True) flexmock(module).should_receive('to_bool').and_return(True)
assert module.should_do_markup(no_color=True) is False assert module.should_do_markup(no_color=True, configs={}) is False
def test_should_do_markup_respects_stdout_tty_value(): def test_should_do_markup_respects_stdout_tty_value():
flexmock(module.os.environ).should_receive('get').and_return(None) flexmock(module.os.environ).should_receive('get').and_return(None)
assert module.should_do_markup(no_color=False) is False assert module.should_do_markup(no_color=False, configs={}) is False
def test_should_do_markup_prefers_PY_COLORS_to_stdout_tty_value(): def test_should_do_markup_prefers_PY_COLORS_to_stdout_tty_value():
flexmock(module.os.environ).should_receive('get').and_return('True') flexmock(module.os.environ).should_receive('get').and_return('True')
flexmock(module).should_receive('to_bool').and_return(True) flexmock(module).should_receive('to_bool').and_return(True)
assert module.should_do_markup(no_color=False) is True assert module.should_do_markup(no_color=False, configs={}) is True
@pytest.mark.parametrize('method_name', ('critical', 'error', 'warn', 'info', 'debug')) def test_console_color_formatter_format_includes_log_message():
def test_borgmatic_logger_log_method_does_not_raise(method_name): plain_message = 'uh oh'
flexmock(module).should_receive('color_text') record = flexmock(levelno=logging.CRITICAL, msg=plain_message)
flexmock(module.logging.Logger).should_receive(method_name)
getattr(module.Borgmatic_logger('test'), method_name)(msg='hi') colored_message = module.Console_color_formatter().format(record)
assert colored_message != plain_message
def test_borgmatic_logger_handle_does_not_raise(): assert plain_message in colored_message
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(): def test_color_text_does_not_raise():
@@ -77,6 +107,7 @@ def test_color_text_without_color_does_not_raise():
def test_configure_logging_probes_for_log_socket_on_linux(): def test_configure_logging_probes_for_log_socket_on_linux():
flexmock(module).should_receive('Console_color_formatter')
flexmock(module.logging).should_receive('basicConfig').with_args( flexmock(module.logging).should_receive('basicConfig').with_args(
level=logging.INFO, handlers=tuple level=logging.INFO, handlers=tuple
) )
@@ -91,6 +122,7 @@ def test_configure_logging_probes_for_log_socket_on_linux():
def test_configure_logging_probes_for_log_socket_on_macos(): def test_configure_logging_probes_for_log_socket_on_macos():
flexmock(module).should_receive('Console_color_formatter')
flexmock(module.logging).should_receive('basicConfig').with_args( flexmock(module.logging).should_receive('basicConfig').with_args(
level=logging.INFO, handlers=tuple level=logging.INFO, handlers=tuple
) )
@@ -105,6 +137,7 @@ def test_configure_logging_probes_for_log_socket_on_macos():
def test_configure_logging_sets_global_logger_to_most_verbose_log_level(): def test_configure_logging_sets_global_logger_to_most_verbose_log_level():
flexmock(module).should_receive('Console_color_formatter')
flexmock(module.logging).should_receive('basicConfig').with_args( flexmock(module.logging).should_receive('basicConfig').with_args(
level=logging.DEBUG, handlers=tuple level=logging.DEBUG, handlers=tuple
).once() ).once()
@@ -114,6 +147,7 @@ def test_configure_logging_sets_global_logger_to_most_verbose_log_level():
def test_configure_logging_skips_syslog_if_not_found(): def test_configure_logging_skips_syslog_if_not_found():
flexmock(module).should_receive('Console_color_formatter')
flexmock(module.logging).should_receive('basicConfig').with_args( flexmock(module.logging).should_receive('basicConfig').with_args(
level=logging.INFO, handlers=tuple level=logging.INFO, handlers=tuple
) )