Compare commits

..
18 Commits
Author SHA1 Message Date
Dan Helfman ca49109ce7 Bump version for release. 2024-01-03 10:08:05 -08:00
Dan Helfman 6a7f71f92f Clarify prune action help concerning running compact afterwards (#808). 2024-01-03 10:03:35 -08:00
Dan Helfman 5f3dc1cfb0 Stream SQLite databases directly to Borg instead of dumping to an intermediate file (#807). 2023-12-31 11:07:59 -08:00
Dan Helfman f2023aed22 Fix typo. 2023-12-30 15:48:55 -08:00
Dan Helfman a03c2744e5 Update docs/how-to/provide-your-passwords.md (#805).
Reviewed-on: https://projects.torsion.org/borgmatic-collective/borgmatic/pulls/805
Reviewed-by: Dan Helfman <witten@torsion.org>
2023-12-30 23:48:32 +00:00
axel simon 4176532317 Update docs/how-to/provide-your-passwords.md
Provide an explanation of encryption_passcommand.
Also, adjust headers for consistency.
2023-12-30 23:45:56 +00:00
Dan Helfman 9d6025e902 Validate the configured action names in the "skip_actions" option (#804). 2023-12-28 20:07:57 -08:00
Dan Helfman cf739bc997 The "check --force" flag now runs checks even if "check" is in "skip_actions" (#802). 2023-12-28 10:22:48 -08:00
Dan Helfman 84823dfb91 Clarify constants/placeholders interaction and improve examples (#763). 2023-12-24 11:18:17 -08:00
Dan Helfman 20cf0f7089 Add an "--ssh-command" flag to the "config bootstrap" action (#767). 2023-12-24 10:33:55 -08:00
Dan Helfman 67af0f5734 Document limitation with constant interpolation at the start of a value (#741). 2023-12-22 21:39:44 -08:00
Dan Helfman e80e0a253c Add configured repository labels to the JSON output for all actions (#800). 2023-12-20 09:17:41 -08:00
Dan Helfman 72587a3b72 Merge branch 'main' of ssh://projects.torsion.org:3022/borgmatic-collective/borgmatic 2023-12-04 11:17:59 -08:00
Dan Helfman 8b49a59aff Fix a traceback when the "repositories" option contains both strings and key/value pairs (#794). 2023-12-04 11:17:13 -08:00
Dan Helfman e120dff9ff Add debug message that logs borg version for every config (#714).
Reviewed-on: https://projects.torsion.org/borgmatic-collective/borgmatic/pulls/792
2023-11-25 03:59:40 +00:00
Tobias Hodapp 257678b66f Fixed borg -> Borg 2023-11-24 18:47:37 +01:00
Tobias Hodapp 422c5e32f4 Added debug message that logs borg version for every config 2023-11-23 11:46:10 +01:00
Dan Helfman c34ad7dde7 Update documentation about configuration includes and constants (#745). 2023-11-19 21:22:10 -08:00
31 changed files with 602 additions and 96 deletions
+13
View File
@@ -1,3 +1,16 @@
1.8.6
* #767: Add an "--ssh-command" flag to the "config bootstrap" action for setting a custom SSH
command, as no configuration is available (including the "ssh_command" option) until
bootstrapping completes.
* #794: Fix a traceback when the "repositories" option contains both strings and key/value pairs.
* #800: Add configured repository labels to the JSON output for all actions.
* #802: The "check --force" flag now runs checks even if "check" is in "skip_actions".
* #804: Validate the configured action names in the "skip_actions" option.
* #807: Stream SQLite databases directly to Borg instead of dumping to an intermediate file.
* When logging commands that borgmatic executes, log the environment variables that
borgmatic sets for those commands. (But don't log their values, since they often contain
passwords.)
1.8.5
* #701: Add a "skip_actions" option to skip running particular actions, handy for append-only or
checkless configurations. See the documentation for more information:
+6 -4
View File
@@ -31,18 +31,19 @@ def get_config_paths(bootstrap_arguments, global_arguments, local_borg_version):
borgmatic_manifest_path = os.path.expanduser(
os.path.join(borgmatic_source_directory, 'bootstrap', 'manifest.json')
)
config = {'ssh_command': bootstrap_arguments.ssh_command}
extract_process = borgmatic.borg.extract.extract_archive(
global_arguments.dry_run,
bootstrap_arguments.repository,
borgmatic.borg.rlist.resolve_archive_name(
bootstrap_arguments.repository,
bootstrap_arguments.archive,
{},
config,
local_borg_version,
global_arguments,
),
[borgmatic_manifest_path],
{},
config,
local_borg_version,
global_arguments,
extract_to_stdout=True,
@@ -79,6 +80,7 @@ def run_bootstrap(bootstrap_arguments, global_arguments, local_borg_version):
manifest_config_paths = get_config_paths(
bootstrap_arguments, global_arguments, local_borg_version
)
config = {'ssh_command': bootstrap_arguments.ssh_command}
logger.info(f"Bootstrapping config paths: {', '.join(manifest_config_paths)}")
@@ -88,12 +90,12 @@ def run_bootstrap(bootstrap_arguments, global_arguments, local_borg_version):
borgmatic.borg.rlist.resolve_archive_name(
bootstrap_arguments.repository,
bootstrap_arguments.archive,
{},
config,
local_borg_version,
global_arguments,
),
[config_path.lstrip(os.path.sep) for config_path in manifest_config_paths],
{},
config,
local_borg_version,
global_arguments,
extract_to_stdout=False,
+3 -2
View File
@@ -3,6 +3,7 @@ import json
import logging
import os
import borgmatic.actions.json
import borgmatic.borg.create
import borgmatic.borg.state
import borgmatic.config.validate
@@ -107,8 +108,8 @@ def run_create(
list_files=create_arguments.list_files,
stream_processes=stream_processes,
)
if json_output: # pragma: nocover
yield json.loads(json_output)
if json_output:
yield borgmatic.actions.json.parse_json(json_output, repository.get('label'))
borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
'remove_data_source_dumps',
+4 -4
View File
@@ -1,7 +1,7 @@
import json
import logging
import borgmatic.actions.arguments
import borgmatic.actions.json
import borgmatic.borg.info
import borgmatic.borg.rlist
import borgmatic.config.validate
@@ -26,7 +26,7 @@ def run_info(
if info_arguments.repository is None or borgmatic.config.validate.repositories_match(
repository, info_arguments.repository
):
if not info_arguments.json: # pragma: nocover
if not info_arguments.json:
logger.answer(
f'{repository.get("label", repository["path"])}: Displaying archive summary information'
)
@@ -48,5 +48,5 @@ def run_info(
local_path,
remote_path,
)
if json_output: # pragma: nocover
yield json.loads(json_output)
if json_output:
yield borgmatic.actions.json.parse_json(json_output, repository.get('label'))
+16
View File
@@ -0,0 +1,16 @@
import json
def parse_json(borg_json_output, label):
'''
Given a Borg JSON output string, parse it as JSON into a dict. Inject the given borgmatic
repository label into it and return the dict.
'''
json_data = json.loads(borg_json_output)
if 'repository' not in json_data:
return json_data
json_data['repository']['label'] = label or ''
return json_data
+6 -6
View File
@@ -1,7 +1,7 @@
import json
import logging
import borgmatic.actions.arguments
import borgmatic.actions.json
import borgmatic.borg.list
import borgmatic.config.validate
@@ -25,10 +25,10 @@ def run_list(
if list_arguments.repository is None or borgmatic.config.validate.repositories_match(
repository, list_arguments.repository
):
if not list_arguments.json: # pragma: nocover
if list_arguments.find_paths:
if not list_arguments.json:
if list_arguments.find_paths: # pragma: no cover
logger.answer(f'{repository.get("label", repository["path"])}: Searching archives')
elif not list_arguments.archive:
elif not list_arguments.archive: # pragma: no cover
logger.answer(f'{repository.get("label", repository["path"])}: Listing archives')
archive_name = borgmatic.borg.rlist.resolve_archive_name(
@@ -49,5 +49,5 @@ def run_list(
local_path,
remote_path,
)
if json_output: # pragma: nocover
yield json.loads(json_output)
if json_output:
yield borgmatic.actions.json.parse_json(json_output, repository.get('label'))
+4 -4
View File
@@ -1,6 +1,6 @@
import json
import logging
import borgmatic.actions.json
import borgmatic.borg.rinfo
import borgmatic.config.validate
@@ -24,7 +24,7 @@ def run_rinfo(
if rinfo_arguments.repository is None or borgmatic.config.validate.repositories_match(
repository, rinfo_arguments.repository
):
if not rinfo_arguments.json: # pragma: nocover
if not rinfo_arguments.json:
logger.answer(
f'{repository.get("label", repository["path"])}: Displaying repository summary information'
)
@@ -38,5 +38,5 @@ def run_rinfo(
local_path=local_path,
remote_path=remote_path,
)
if json_output: # pragma: nocover
yield json.loads(json_output)
if json_output:
yield borgmatic.actions.json.parse_json(json_output, repository.get('label'))
+4 -4
View File
@@ -1,6 +1,6 @@
import json
import logging
import borgmatic.actions.json
import borgmatic.borg.rlist
import borgmatic.config.validate
@@ -24,7 +24,7 @@ def run_rlist(
if rlist_arguments.repository is None or borgmatic.config.validate.repositories_match(
repository, rlist_arguments.repository
):
if not rlist_arguments.json: # pragma: nocover
if not rlist_arguments.json:
logger.answer(f'{repository.get("label", repository["path"])}: Listing repository')
json_output = borgmatic.borg.rlist.list_repository(
@@ -36,5 +36,5 @@ def run_rlist(
local_path=local_path,
remote_path=remote_path,
)
if json_output: # pragma: nocover
yield json.loads(json_output)
if json_output:
yield borgmatic.actions.json.parse_json(json_output, repository.get('label'))
+7 -2
View File
@@ -467,8 +467,8 @@ def make_parsers():
prune_parser = action_parsers.add_parser(
'prune',
aliases=ACTION_ALIASES['prune'],
help='Prune archives according to the retention policy (with Borg 1.2+, run compact afterwards to actually free space)',
description='Prune archives according to the retention policy (with Borg 1.2+, run compact afterwards to actually free space)',
help='Prune archives according to the retention policy (with Borg 1.2+, you must run compact afterwards to actually free space)',
description='Prune archives according to the retention policy (with Borg 1.2+, you must run compact afterwards to actually free space)',
add_help=False,
)
prune_group = prune_parser.add_argument_group('prune arguments')
@@ -731,6 +731,11 @@ def make_parsers():
action='store_true',
help='Display progress for each file as it is extracted',
)
config_bootstrap_group.add_argument(
'--ssh-command',
metavar='COMMAND',
help='Command to use instead of "ssh"',
)
config_bootstrap_group.add_argument(
'-h', '--help', action='help', help='Show this help message and exit'
)
+17 -2
View File
@@ -44,6 +44,20 @@ from borgmatic.verbosity import verbosity_to_log_level
logger = logging.getLogger(__name__)
def get_skip_actions(config, arguments):
'''
Given a configuration dict and command-line arguments as an argparse.Namespace, return a list of
the configured action names to skip. Omit "check" from this list though if "check --force" is
part of the command-like arguments.
'''
skip_actions = config.get('skip_actions', [])
if 'check' in arguments and arguments['check'].force:
return [action for action in skip_actions if action != 'check']
return skip_actions
def run_configuration(config_filename, config, arguments):
'''
Given a config filename, the corresponding parsed config dict, and command-line arguments as a
@@ -66,7 +80,7 @@ def run_configuration(config_filename, config, arguments):
using_primary_action = {'create', 'prune', 'compact', 'check'}.intersection(arguments)
monitoring_log_level = verbosity_to_log_level(global_arguments.monitoring_verbosity)
monitoring_hooks_are_activated = using_primary_action and monitoring_log_level != DISABLED
skip_actions = config.get('skip_actions')
skip_actions = get_skip_actions(config, arguments)
if skip_actions:
logger.debug(
@@ -75,6 +89,7 @@ def run_configuration(config_filename, config, arguments):
try:
local_borg_version = borg_version.local_borg_version(config, local_path)
logger.debug(f'{config_filename}: Borg {local_borg_version}')
except (OSError, CalledProcessError, ValueError) as error:
yield from log_error_records(f'{config_filename}: Error getting local Borg version', error)
return
@@ -276,7 +291,7 @@ def run_actions(
'repositories': ','.join([repo['path'] for repo in config['repositories']]),
'log_file': global_arguments.log_file if global_arguments.log_file else '',
}
skip_actions = set(config.get('skip_actions', {}))
skip_actions = set(get_skip_actions(config, arguments))
command.execute_hook(
config.get('before_actions'),
+5 -2
View File
@@ -192,7 +192,7 @@ def normalize(config_filename, config):
# Upgrade remote repositories to ssh:// syntax, required in Borg 2.
repositories = config.get('repositories')
if repositories:
if isinstance(repositories[0], str):
if any(isinstance(repository, str) for repository in repositories):
logs.append(
logging.makeLogRecord(
dict(
@@ -202,7 +202,10 @@ def normalize(config_filename, config):
)
)
)
config['repositories'] = [{'path': repository} for repository in repositories]
config['repositories'] = [
{'path': repository} if isinstance(repository, str) else repository
for repository in repositories
]
repositories = config['repositories']
config['repositories'] = []
+28 -7
View File
@@ -6,14 +6,15 @@ properties:
constants:
type: object
description: |
Constants to use in the configuration file. All occurrences of the
constant name within curly braces will be replaced with the value.
For example, if you have a constant named "hostname" with the value
"myhostname", then the string "{hostname}" will be replaced with
"myhostname" in the configuration file.
Constants to use in the configuration file. Within option values,
all occurrences of the constant name in curly braces will be
replaced with the constant value. For example, if you have a
constant named "app_name" with the value "myapp", then the string
"{app_name}" will be replaced with "myapp" in the configuration
file.
example:
hostname: myhostname
prefix: myprefix
app_name: myapp
user: myuser
source_directories:
type: array
items:
@@ -534,6 +535,26 @@ properties:
type: array
items:
type: string
enum:
- rcreate
- transfer
- prune
- compact
- create
- check
- extract
- config
- export-tar
- mount
- umount
- restore
- rlist
- list
- rinfo
- info
- break-lock
- key
- borg
description: |
List of one or more actions to skip running for this configuration
file, even if specified on the command-line (explicitly or
+7 -7
View File
@@ -172,19 +172,19 @@ def log_outputs(processes, exclude_stdouts, output_log_level, borg_local_path):
}
def log_command(full_command, input_file=None, output_file=None):
def log_command(full_command, input_file=None, output_file=None, environment=None):
'''
Log the given command (a sequence of command/argument strings), along with its input/output file
paths.
paths and extra environment variables (with omitted values in case they contain passwords).
'''
logger.debug(
' '.join(full_command)
' '.join(tuple(f'{key}=***' for key in (environment or {}).keys()) + tuple(full_command))
+ (f" < {getattr(input_file, 'name', '')}" if input_file else '')
+ (f" > {getattr(output_file, 'name', '')}" if output_file else '')
)
# An sentinel passed as an output file to execute_command() to indicate that the command's output
# A sentinel passed as an output file to execute_command() to indicate that the command's output
# should be allowed to flow through to stdout without being captured for logging. Useful for
# commands with interactive prompts or those that mess directly with the console.
DO_NOT_CAPTURE = object()
@@ -214,7 +214,7 @@ def execute_command(
Raise subprocesses.CalledProcessError if an error occurs while running the command.
'''
log_command(full_command, input_file, output_file)
log_command(full_command, input_file, output_file, extra_environment)
environment = {**os.environ, **extra_environment} if extra_environment else None
do_not_capture = bool(output_file is DO_NOT_CAPTURE)
command = ' '.join(full_command) if shell else full_command
@@ -255,7 +255,7 @@ def execute_command_and_capture_output(
Raise subprocesses.CalledProcessError if an error occurs while running the command.
'''
log_command(full_command)
log_command(full_command, environment=extra_environment)
environment = {**os.environ, **extra_environment} if extra_environment else None
command = ' '.join(full_command) if shell else full_command
@@ -304,7 +304,7 @@ def execute_command_with_processes(
Raise subprocesses.CalledProcessError if an error occurs while running the command or in the
upstream process.
'''
log_command(full_command, input_file, output_file)
log_command(full_command, input_file, output_file, extra_environment)
environment = {**os.environ, **extra_environment} if extra_environment else None
do_not_capture = bool(output_file is DO_NOT_CAPTURE)
command = ' '.join(full_command) if shell else full_command
+7 -4
View File
@@ -18,10 +18,12 @@ def make_dump_path(config): # pragma: no cover
def dump_data_sources(databases, config, log_prefix, dry_run):
'''
Dump the given SQLite3 databases to a file. The databases are supplied as a sequence of
Dump the given SQLite3 databases to a named pipe. The databases are supplied as a sequence of
configuration dicts, as per the configuration schema. Use the given configuration dict to
construct the destination path and the given log prefix in any log entries. If this is a dry
run, then don't actually dump anything.
construct the destination path and the given log prefix in any log entries.
Return a sequence of subprocess.Popen instances for the dump processes ready to spew to a named
pipe. But if this is a dry run, then don't actually dump anything and return an empty sequence.
'''
dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
processes = []
@@ -40,6 +42,7 @@ def dump_data_sources(databases, config, log_prefix, dry_run):
dump_path = make_dump_path(config)
dump_filename = dump.make_data_source_dump_filename(dump_path, database['name'])
if os.path.exists(dump_filename):
logger.warning(
f'{log_prefix}: Skipping duplicate dump of SQLite database at {database_path} to {dump_filename}'
@@ -59,7 +62,7 @@ def dump_data_sources(databases, config, log_prefix, dry_run):
if dry_run:
continue
dump.create_parent_directory_for_dump(dump_filename)
dump.create_named_pipe_for_dump(dump_filename)
processes.append(execute_command(command, shell=True, run_to_completion=False))
return processes
@@ -153,6 +153,10 @@ though—or the most frequently configured check will apply.
If you want to temporarily ignore your configured frequencies, you can invoke
`borgmatic check --force` to run checks unconditionally.
<span class="minilink minilink-addedin">New in version 1.8.6</span> `borgmatic
check --force` runs `check` even if it's specified in the `skip_actions`
option.
### Running only checks
+5 -2
View File
@@ -167,8 +167,8 @@ configuration file used to create this archive was located at
`/etc/borgmatic/config.yaml` when the archive was created.
Note that to run the `config bootstrap` action, you don't need to have a
borgmatic configuration file. You only need to specify the repository to use via
the `--repository` flag; borgmatic will figure out the rest.
borgmatic configuration file. You only need to specify the repository to use
via the `--repository` flag; borgmatic will figure out the rest.
If a destination directory is not specified, the configuration files will be
extracted to their original locations, silently *overwriting* any configuration
@@ -183,6 +183,9 @@ If you want to extract the configuration file from a specific archive, use the
borgmatic config bootstrap --repository repo.borg --archive host-2023-01-02T04:06:07.080910 --destination /tmp
```
See the output of `config bootstrap --help` for additional flags you may need
for bootstrapping.
<span class="minilink minilink-addedin">New in version 1.8.1</span> Set the
`store_config_files` option to `false` to disable the automatic backup of
borgmatic configuration files, for instance if they contain sensitive
+35 -12
View File
@@ -301,7 +301,7 @@ options via an include and then overrides one of them locally:
<<: !include /etc/borgmatic/common.yaml
constants:
hostname: myhostname
base_directory: /opt
repositories:
- path: repo.borg
@@ -311,13 +311,13 @@ This is what `common.yaml` might look like:
```yaml
constants:
prefix: myprefix
hostname: otherhost
app_name: myapp
base_directory: /var/lib
```
Once this include gets merged in, the resulting configuration would have a
`prefix` value of `myprefix` and an overridden `hostname` value of
`myhostname`.
Once this include gets merged in, the resulting configuration would have an
`app_name` value of `myapp` and an overridden `base_directory` value of
`/opt`.
When there's an option collision between the local file and the merged
include, the local file's option takes precedence.
@@ -540,8 +540,7 @@ tool is borgmatic's support for defining custom constants. This is similar to
the [variable interpolation
feature](https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/#variable-interpolation)
for command hooks, but the constants feature lets you substitute your own
custom values into anywhere in the entire configuration file. (Constants don't
work across includes or separate configuration files though.)
custom values into any option values in the entire configuration file.
Here's an example usage:
@@ -564,10 +563,15 @@ forget to specify the section (like `location:` or `storage:`) that any option
is in.
In this example, when borgmatic runs, all instances of `{user}` get replaced
with `foo` and all instances of `{archive_prefix}` get replaced with `bar-`.
(And in this particular example, `{now}` doesn't get replaced with anything,
but gets passed directly to Borg.) After substitution, the logical result
looks something like this:
with `foo` and all instances of `{archive_prefix}` get replaced with `bar`.
And `{now}` doesn't get replaced with anything, but gets passed directly to
Borg, which has its own
[placeholders](https://borgbackup.readthedocs.io/en/stable/usage/help.html#borg-help-placeholders)
using the same syntax as borgmatic constants. So borgmatic options like
`archive_name_format` that get passed directly to Borg can use either Borg
placeholders or borgmatic constants or both!
After substitution, the logical result looks something like this:
```yaml
source_directories:
@@ -579,5 +583,24 @@ source_directories:
archive_name_format: 'bar-{now}'
```
Note that if you'd like to interpolate a constant into the beginning of a
value, you'll need to quote it. For instance, this won't work:
```yaml
source_directories:
- {my_home_directory}/.config # This will error!
```
Instead, do this:
```yaml
source_directories:
- "{my_home_directory}/.config"
```
<span class="minilink minilink-addedin">New in version 1.8.5</span> Constants
work across includes, meaning you can define a constant and then include a
separate configuration file that uses that constant.
An alternate to constants is passing in your values via [environment
variables](https://torsion.org/borgmatic/docs/how-to/provide-your-passwords/).
+16 -4
View File
@@ -5,7 +5,7 @@ eleventyNavigation:
parent: How-to guides
order: 2
---
## Environment variable interpolation
## Providing passwords and secrets to borgmatic
If you want to use a Borg repository passphrase or database passwords with
borgmatic, you can set them directly in your borgmatic configuration file,
@@ -19,6 +19,18 @@ encryption_passphrase: yourpassphrase
But if you'd rather store them outside of borgmatic, whether for convenience
or security reasons, read on.
### Delegating to another application
borgmatic supports calling another application such as a password manager to
obtain the Borg passphrase to a repository.
For example, to ask the *Pass* password manager to provide the passphrase:
```yaml
encryption_passcommand: pass path/to/borg-repokey
```
### Environment variable interpolation
<span class="minilink minilink-addedin">New in version 1.6.4</span> borgmatic
supports interpolating arbitrary environment variables directly into option
values in your configuration file. That means you can instruct borgmatic to
@@ -58,7 +70,7 @@ This uses the `YOUR_DATABASE_PASSWORD` environment variable as your database
password.
### Interpolation defaults
#### Interpolation defaults
If you'd like to set a default for your environment variables, you can do so
with the following syntax:
@@ -72,7 +84,7 @@ environment variable is not set. Without a default, if the environment
variable doesn't exist, borgmatic will error.
### Disabling interpolation
#### Disabling interpolation
To disable this environment variable interpolation feature entirely, you can
pass the `--no-environment-interpolation` flag on the command-line.
@@ -85,7 +97,7 @@ can escape it with a backslash. For instance, if your password is literally
encryption_passphrase: \${A}@!
```
### Related features
## Related features
Another way to override particular options within a borgmatic configuration
file is to use a [configuration
+1 -1
View File
@@ -1,6 +1,6 @@
from setuptools import find_packages, setup
VERSION = '1.8.5'
VERSION = '1.8.6'
setup(
+26
View File
@@ -1,3 +1,9 @@
import pkgutil
import borgmatic.actions
import borgmatic.config.load
import borgmatic.config.validate
MAXIMUM_LINE_LENGTH = 80
@@ -6,3 +12,23 @@ def test_schema_line_length_stays_under_limit():
for line in schema_file.readlines():
assert len(line.rstrip('\n')) <= MAXIMUM_LINE_LENGTH
ACTIONS_MODULE_NAMES_TO_OMIT = {'arguments', 'export_key', 'json'}
ACTIONS_MODULE_NAMES_TO_ADD = {'key', 'umount'}
def test_schema_skip_actions_correspond_to_supported_actions():
'''
Ensure that the allowed actions in the schema's "skip_actions" option don't drift from
borgmatic's actual supported actions.
'''
schema = borgmatic.config.load.load_configuration(borgmatic.config.validate.schema_filename())
schema_skip_actions = set(schema['properties']['skip_actions']['items']['enum'])
supported_actions = {
module.name.replace('_', '-')
for module in pkgutil.iter_modules(borgmatic.actions.__path__)
if module.name not in ACTIONS_MODULE_NAMES_TO_OMIT
}.union(ACTIONS_MODULE_NAMES_TO_ADD)
assert schema_skip_actions == supported_actions
+82 -2
View File
@@ -9,6 +9,7 @@ def test_get_config_paths_returns_list_of_config_paths():
borgmatic_source_directory=None,
repository='repo',
archive='archive',
ssh_command=None,
)
global_arguments = flexmock(
dry_run=False,
@@ -30,11 +31,46 @@ def test_get_config_paths_returns_list_of_config_paths():
]
def test_get_config_paths_translates_ssh_command_argument_to_config():
bootstrap_arguments = flexmock(
borgmatic_source_directory=None,
repository='repo',
archive='archive',
ssh_command='ssh -i key',
)
global_arguments = flexmock(
dry_run=False,
)
local_borg_version = flexmock()
extract_process = flexmock(
stdout=flexmock(
read=lambda: '{"config_paths": ["/borgmatic/config.yaml"]}',
),
)
flexmock(module.borgmatic.borg.extract).should_receive('extract_archive').with_args(
False,
'repo',
'archive',
object,
{'ssh_command': 'ssh -i key'},
object,
object,
extract_to_stdout=True,
).and_return(extract_process)
flexmock(module.borgmatic.borg.rlist).should_receive('resolve_archive_name').with_args(
'repo', 'archive', {'ssh_command': 'ssh -i key'}, object, object
).and_return('archive')
assert module.get_config_paths(bootstrap_arguments, global_arguments, local_borg_version) == [
'/borgmatic/config.yaml'
]
def test_get_config_paths_with_missing_manifest_raises_value_error():
bootstrap_arguments = flexmock(
borgmatic_source_directory=None,
repository='repo',
archive='archive',
ssh_command=None,
)
global_arguments = flexmock(
dry_run=False,
@@ -57,6 +93,7 @@ def test_get_config_paths_with_broken_json_raises_value_error():
borgmatic_source_directory=None,
repository='repo',
archive='archive',
ssh_command=None,
)
global_arguments = flexmock(
dry_run=False,
@@ -81,6 +118,7 @@ def test_get_config_paths_with_json_missing_key_raises_value_error():
borgmatic_source_directory=None,
repository='repo',
archive='archive',
ssh_command=None,
)
global_arguments = flexmock(
dry_run=False,
@@ -101,6 +139,7 @@ def test_get_config_paths_with_json_missing_key_raises_value_error():
def test_run_bootstrap_does_not_raise():
flexmock(module).should_receive('get_config_paths').and_return(['/borgmatic/config.yaml'])
bootstrap_arguments = flexmock(
repository='repo',
archive='archive',
@@ -108,6 +147,7 @@ def test_run_bootstrap_does_not_raise():
strip_components=1,
progress=False,
borgmatic_source_directory='/borgmatic',
ssh_command=None,
)
global_arguments = flexmock(
dry_run=False,
@@ -115,14 +155,54 @@ def test_run_bootstrap_does_not_raise():
local_borg_version = flexmock()
extract_process = flexmock(
stdout=flexmock(
read=lambda: '{"config_paths": ["/borgmatic/config.yaml"]}',
read=lambda: '{"config_paths": ["borgmatic/config.yaml"]}',
),
)
flexmock(module.borgmatic.borg.extract).should_receive('extract_archive').and_return(
extract_process
).twice()
).once()
flexmock(module.borgmatic.borg.rlist).should_receive('resolve_archive_name').and_return(
'archive'
)
module.run_bootstrap(bootstrap_arguments, global_arguments, local_borg_version)
def test_run_bootstrap_translates_ssh_command_argument_to_config():
flexmock(module).should_receive('get_config_paths').and_return(['/borgmatic/config.yaml'])
bootstrap_arguments = flexmock(
repository='repo',
archive='archive',
destination='dest',
strip_components=1,
progress=False,
borgmatic_source_directory='/borgmatic',
ssh_command='ssh -i key',
)
global_arguments = flexmock(
dry_run=False,
)
local_borg_version = flexmock()
extract_process = flexmock(
stdout=flexmock(
read=lambda: '{"config_paths": ["borgmatic/config.yaml"]}',
),
)
flexmock(module.borgmatic.borg.extract).should_receive('extract_archive').with_args(
False,
'repo',
'archive',
object,
{'ssh_command': 'ssh -i key'},
object,
object,
extract_to_stdout=False,
destination_path='dest',
strip_components=1,
progress=False,
).and_return(extract_process).once()
flexmock(module.borgmatic.borg.rlist).should_receive('resolve_archive_name').with_args(
'repo', 'archive', {'ssh_command': 'ssh -i key'}, object, object
).and_return('archive')
module.run_bootstrap(bootstrap_arguments, global_arguments, local_borg_version)
+45 -4
View File
@@ -19,7 +19,7 @@ def test_run_create_executes_and_calls_hooks_for_configured_repository():
repository=None,
progress=flexmock(),
stats=flexmock(),
json=flexmock(),
json=False,
list_files=flexmock(),
)
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False, used_config_paths=[])
@@ -54,7 +54,7 @@ def test_run_create_with_store_config_files_false_does_not_create_borgmatic_mani
repository=None,
progress=flexmock(),
stats=flexmock(),
json=flexmock(),
json=False,
list_files=flexmock(),
)
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False, used_config_paths=[])
@@ -91,7 +91,7 @@ def test_run_create_runs_with_selected_repository():
repository=flexmock(),
progress=flexmock(),
stats=flexmock(),
json=flexmock(),
json=False,
list_files=flexmock(),
)
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False, used_config_paths=[])
@@ -123,7 +123,7 @@ def test_run_create_bails_if_repository_does_not_match():
repository=flexmock(),
progress=flexmock(),
stats=flexmock(),
json=flexmock(),
json=False,
list_files=flexmock(),
)
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False, used_config_paths=[])
@@ -144,6 +144,47 @@ def test_run_create_bails_if_repository_does_not_match():
)
def test_run_create_produces_json():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive(
'repositories_match'
).once().and_return(True)
flexmock(module.borgmatic.borg.create).should_receive('create_archive').once().and_return(
flexmock()
)
parsed_json = flexmock()
flexmock(module.borgmatic.actions.json).should_receive('parse_json').and_return(parsed_json)
flexmock(module).should_receive('create_borgmatic_manifest').once()
flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2)
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').and_return({})
flexmock(module.borgmatic.hooks.dispatch).should_receive(
'call_hooks_even_if_unconfigured'
).and_return({})
create_arguments = flexmock(
repository=flexmock(),
progress=flexmock(),
stats=flexmock(),
json=True,
list_files=flexmock(),
)
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False, used_config_paths=[])
assert list(
module.run_create(
config_filename='test.yaml',
repository={'path': 'repo'},
config={},
hook_context={},
local_borg_version=None,
create_arguments=create_arguments,
global_arguments=global_arguments,
dry_run_label='',
local_path=None,
remote_path=None,
)
) == [parsed_json]
def test_create_borgmatic_manifest_creates_manifest_file():
flexmock(module.os.path).should_receive('join').with_args(
module.borgmatic.borg.state.DEFAULT_BORGMATIC_SOURCE_DIRECTORY, 'bootstrap', 'manifest.json'
+30 -1
View File
@@ -13,7 +13,7 @@ def test_run_info_does_not_raise():
flexmock()
)
flexmock(module.borgmatic.borg.info).should_receive('display_archives_info')
info_arguments = flexmock(repository=flexmock(), archive=flexmock(), json=flexmock())
info_arguments = flexmock(repository=flexmock(), archive=flexmock(), json=False)
list(
module.run_info(
@@ -26,3 +26,32 @@ def test_run_info_does_not_raise():
remote_path=None,
)
)
def test_run_info_produces_json():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.rlist).should_receive('resolve_archive_name').and_return(
flexmock()
)
flexmock(module.borgmatic.actions.arguments).should_receive('update_arguments').and_return(
flexmock()
)
flexmock(module.borgmatic.borg.info).should_receive('display_archives_info').and_return(
flexmock()
)
parsed_json = flexmock()
flexmock(module.borgmatic.actions.json).should_receive('parse_json').and_return(parsed_json)
info_arguments = flexmock(repository=flexmock(), archive=flexmock(), json=True)
assert list(
module.run_info(
repository={'path': 'repo'},
config={},
local_borg_version=None,
info_arguments=info_arguments,
global_arguments=flexmock(log_json=False),
local_path=None,
remote_path=None,
)
) == [parsed_json]
+25
View File
@@ -0,0 +1,25 @@
from flexmock import flexmock
from borgmatic.actions import json as module
def test_parse_json_loads_json_from_string():
flexmock(module.json).should_receive('loads').and_return({'repository': {'id': 'foo'}})
assert module.parse_json('{"repository": {"id": "foo"}}', label=None) == {
'repository': {'id': 'foo', 'label': ''}
}
def test_parse_json_injects_label_into_parsed_data():
flexmock(module.json).should_receive('loads').and_return({'repository': {'id': 'foo'}})
assert module.parse_json('{"repository": {"id": "foo"}}', label='bar') == {
'repository': {'id': 'foo', 'label': 'bar'}
}
def test_parse_json_injects_nothing_when_repository_missing():
flexmock(module.json).should_receive('loads').and_return({'stuff': {'id': 'foo'}})
assert module.parse_json('{"stuff": {"id": "foo"}}', label='bar') == {'stuff': {'id': 'foo'}}
+30 -1
View File
@@ -13,7 +13,9 @@ def test_run_list_does_not_raise():
flexmock()
)
flexmock(module.borgmatic.borg.list).should_receive('list_archive')
list_arguments = flexmock(repository=flexmock(), archive=flexmock(), json=flexmock())
list_arguments = flexmock(
repository=flexmock(), archive=flexmock(), json=False, find_paths=None
)
list(
module.run_list(
@@ -26,3 +28,30 @@ def test_run_list_does_not_raise():
remote_path=None,
)
)
def test_run_list_produces_json():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.rlist).should_receive('resolve_archive_name').and_return(
flexmock()
)
flexmock(module.borgmatic.actions.arguments).should_receive('update_arguments').and_return(
flexmock()
)
flexmock(module.borgmatic.borg.list).should_receive('list_archive').and_return(flexmock())
parsed_json = flexmock()
flexmock(module.borgmatic.actions.json).should_receive('parse_json').and_return(parsed_json)
list_arguments = flexmock(repository=flexmock(), archive=flexmock(), json=True)
assert list(
module.run_list(
repository={'path': 'repo'},
config={},
local_borg_version=None,
list_arguments=list_arguments,
global_arguments=flexmock(log_json=False),
local_path=None,
remote_path=None,
)
) == [parsed_json]
+24 -1
View File
@@ -7,7 +7,7 @@ def test_run_rinfo_does_not_raise():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.rinfo).should_receive('display_repository_info')
rinfo_arguments = flexmock(repository=flexmock(), json=flexmock())
rinfo_arguments = flexmock(repository=flexmock(), json=False)
list(
module.run_rinfo(
@@ -20,3 +20,26 @@ def test_run_rinfo_does_not_raise():
remote_path=None,
)
)
def test_run_rinfo_parses_json():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.rinfo).should_receive('display_repository_info').and_return(
flexmock()
)
parsed_json = flexmock()
flexmock(module.borgmatic.actions.json).should_receive('parse_json').and_return(parsed_json)
rinfo_arguments = flexmock(repository=flexmock(), json=True)
list(
module.run_rinfo(
repository={'path': 'repo'},
config={},
local_borg_version=None,
rinfo_arguments=rinfo_arguments,
global_arguments=flexmock(log_json=False),
local_path=None,
remote_path=None,
)
) == [parsed_json]
+22 -1
View File
@@ -7,7 +7,7 @@ def test_run_rlist_does_not_raise():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.rlist).should_receive('list_repository')
rlist_arguments = flexmock(repository=flexmock(), json=flexmock())
rlist_arguments = flexmock(repository=flexmock(), json=False)
list(
module.run_rlist(
@@ -20,3 +20,24 @@ def test_run_rlist_does_not_raise():
remote_path=None,
)
)
def test_run_rlist_produces_json():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.rlist).should_receive('list_repository').and_return(flexmock())
parsed_json = flexmock()
flexmock(module.borgmatic.actions.json).should_receive('parse_json').and_return(parsed_json)
rlist_arguments = flexmock(repository=flexmock(), json=True)
assert list(
module.run_rlist(
repository={'path': 'repo'},
config={},
local_borg_version=None,
rlist_arguments=rlist_arguments,
global_arguments=flexmock(),
local_path=None,
remote_path=None,
)
) == [parsed_json]
+64
View File
@@ -2,14 +2,34 @@ import logging
import subprocess
import time
import pytest
from flexmock import flexmock
import borgmatic.hooks.command
from borgmatic.commands import borgmatic as module
@pytest.mark.parametrize(
'config,arguments,expected_actions',
(
({}, {}, []),
({'skip_actions': []}, {}, []),
({'skip_actions': ['prune', 'check']}, {}, ['prune', 'check']),
(
{'skip_actions': ['prune', 'check']},
{'check': flexmock(force=False)},
['prune', 'check'],
),
({'skip_actions': ['prune', 'check']}, {'check': flexmock(force=True)}, ['prune']),
),
)
def test_get_skip_actions_uses_config_and_arguments(config, arguments, expected_actions):
assert module.get_skip_actions(config, arguments) == expected_actions
def test_run_configuration_runs_actions_for_each_repository():
flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
expected_results = [flexmock(), flexmock()]
flexmock(module).should_receive('run_actions').and_return(expected_results[:1]).and_return(
@@ -25,6 +45,7 @@ def test_run_configuration_runs_actions_for_each_repository():
def test_run_configuration_with_skip_actions_does_not_raise():
flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
flexmock(module).should_receive('get_skip_actions').and_return(['compact'])
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module).should_receive('run_actions').and_return(flexmock()).and_return(flexmock())
config = {'repositories': [{'path': 'foo'}, {'path': 'bar'}], 'skip_actions': ['compact']}
@@ -35,6 +56,7 @@ def test_run_configuration_with_skip_actions_does_not_raise():
def test_run_configuration_with_invalid_borg_version_errors():
flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.borg_version).should_receive('local_borg_version').and_raise(ValueError)
flexmock(module.command).should_receive('execute_hook').never()
flexmock(module.dispatch).should_receive('call_hooks').never()
@@ -47,6 +69,7 @@ def test_run_configuration_with_invalid_borg_version_errors():
def test_run_configuration_logs_monitor_start_error():
flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.dispatch).should_receive('call_hooks').and_raise(OSError).and_return(
None
@@ -64,6 +87,7 @@ def test_run_configuration_logs_monitor_start_error():
def test_run_configuration_bails_for_monitor_start_soft_failure():
flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
error = subprocess.CalledProcessError(borgmatic.hooks.command.SOFT_FAIL_EXIT_CODE, 'try again')
flexmock(module.dispatch).should_receive('call_hooks').and_raise(error)
@@ -79,6 +103,7 @@ def test_run_configuration_bails_for_monitor_start_soft_failure():
def test_run_configuration_logs_actions_error():
flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.command).should_receive('execute_hook')
flexmock(module.dispatch).should_receive('call_hooks')
@@ -95,6 +120,7 @@ def test_run_configuration_logs_actions_error():
def test_run_configuration_bails_for_actions_soft_failure():
flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.dispatch).should_receive('call_hooks')
error = subprocess.CalledProcessError(borgmatic.hooks.command.SOFT_FAIL_EXIT_CODE, 'try again')
@@ -111,6 +137,7 @@ def test_run_configuration_bails_for_actions_soft_failure():
def test_run_configuration_logs_monitor_log_error():
flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.dispatch).should_receive('call_hooks').and_return(None).and_return(
None
@@ -128,6 +155,7 @@ def test_run_configuration_logs_monitor_log_error():
def test_run_configuration_bails_for_monitor_log_soft_failure():
flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
error = subprocess.CalledProcessError(borgmatic.hooks.command.SOFT_FAIL_EXIT_CODE, 'try again')
flexmock(module.dispatch).should_receive('call_hooks').and_return(None).and_return(
@@ -146,6 +174,7 @@ def test_run_configuration_bails_for_monitor_log_soft_failure():
def test_run_configuration_logs_monitor_finish_error():
flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.dispatch).should_receive('call_hooks').and_return(None).and_return(
None
@@ -163,6 +192,7 @@ def test_run_configuration_logs_monitor_finish_error():
def test_run_configuration_bails_for_monitor_finish_soft_failure():
flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
error = subprocess.CalledProcessError(borgmatic.hooks.command.SOFT_FAIL_EXIT_CODE, 'try again')
flexmock(module.dispatch).should_receive('call_hooks').and_return(None).and_return(
@@ -181,6 +211,7 @@ def test_run_configuration_bails_for_monitor_finish_soft_failure():
def test_run_configuration_does_not_call_monitoring_hooks_if_monitoring_hooks_are_disabled():
flexmock(module).should_receive('verbosity_to_log_level').and_return(module.DISABLED)
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.dispatch).should_receive('call_hooks').never()
@@ -194,6 +225,7 @@ def test_run_configuration_does_not_call_monitoring_hooks_if_monitoring_hooks_ar
def test_run_configuration_logs_on_error_hook_error():
flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.command).should_receive('execute_hook').and_raise(OSError)
expected_results = [flexmock(), flexmock()]
@@ -211,6 +243,7 @@ def test_run_configuration_logs_on_error_hook_error():
def test_run_configuration_bails_for_on_error_hook_soft_failure():
flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
error = subprocess.CalledProcessError(borgmatic.hooks.command.SOFT_FAIL_EXIT_CODE, 'try again')
flexmock(module.command).should_receive('execute_hook').and_raise(error)
@@ -228,6 +261,7 @@ def test_run_configuration_bails_for_on_error_hook_soft_failure():
def test_run_configuration_retries_soft_error():
# Run action first fails, second passes
flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.command).should_receive('execute_hook')
flexmock(module).should_receive('run_actions').and_raise(OSError).and_return([])
@@ -241,6 +275,7 @@ def test_run_configuration_retries_soft_error():
def test_run_configuration_retries_hard_error():
# Run action fails twice
flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.command).should_receive('execute_hook')
flexmock(module).should_receive('run_actions').and_raise(OSError).times(2)
@@ -263,6 +298,7 @@ def test_run_configuration_retries_hard_error():
def test_run_configuration_repos_ordered():
flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.command).should_receive('execute_hook')
flexmock(module).should_receive('run_actions').and_raise(OSError).times(2)
@@ -281,6 +317,7 @@ def test_run_configuration_repos_ordered():
def test_run_configuration_retries_round_robin():
flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.command).should_receive('execute_hook')
flexmock(module).should_receive('run_actions').and_raise(OSError).times(4)
@@ -315,6 +352,7 @@ def test_run_configuration_retries_round_robin():
def test_run_configuration_retries_one_passes():
flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.command).should_receive('execute_hook')
flexmock(module).should_receive('run_actions').and_raise(OSError).and_raise(OSError).and_return(
@@ -347,6 +385,7 @@ def test_run_configuration_retries_one_passes():
def test_run_configuration_retry_wait():
flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.command).should_receive('execute_hook')
flexmock(module).should_receive('run_actions').and_raise(OSError).times(4)
@@ -390,6 +429,7 @@ def test_run_configuration_retry_wait():
def test_run_configuration_retries_timeout_multiple_repos():
flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.command).should_receive('execute_hook')
flexmock(module).should_receive('run_actions').and_raise(OSError).and_raise(OSError).and_return(
@@ -429,6 +469,7 @@ def test_run_configuration_retries_timeout_multiple_repos():
def test_run_actions_runs_rcreate():
flexmock(module).should_receive('add_custom_log_levels')
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.command).should_receive('execute_hook')
flexmock(borgmatic.actions.rcreate).should_receive('run_rcreate').once()
@@ -447,6 +488,7 @@ def test_run_actions_runs_rcreate():
def test_run_actions_adds_log_file_to_hook_context():
flexmock(module).should_receive('add_custom_log_levels')
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.command).should_receive('execute_hook')
expected = flexmock()
flexmock(borgmatic.actions.create).should_receive('run_create').with_args(
@@ -478,6 +520,7 @@ def test_run_actions_adds_log_file_to_hook_context():
def test_run_actions_runs_transfer():
flexmock(module).should_receive('add_custom_log_levels')
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.command).should_receive('execute_hook')
flexmock(borgmatic.actions.transfer).should_receive('run_transfer').once()
@@ -496,6 +539,7 @@ def test_run_actions_runs_transfer():
def test_run_actions_runs_create():
flexmock(module).should_receive('add_custom_log_levels')
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.command).should_receive('execute_hook')
expected = flexmock()
flexmock(borgmatic.actions.create).should_receive('run_create').and_yield(expected).once()
@@ -516,6 +560,7 @@ def test_run_actions_runs_create():
def test_run_actions_with_skip_actions_skips_create():
flexmock(module).should_receive('add_custom_log_levels')
flexmock(module).should_receive('get_skip_actions').and_return(['create'])
flexmock(module.command).should_receive('execute_hook')
flexmock(borgmatic.actions.create).should_receive('run_create').never()
@@ -534,6 +579,7 @@ def test_run_actions_with_skip_actions_skips_create():
def test_run_actions_runs_prune():
flexmock(module).should_receive('add_custom_log_levels')
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.command).should_receive('execute_hook')
flexmock(borgmatic.actions.prune).should_receive('run_prune').once()
@@ -552,6 +598,7 @@ def test_run_actions_runs_prune():
def test_run_actions_with_skip_actions_skips_prune():
flexmock(module).should_receive('add_custom_log_levels')
flexmock(module).should_receive('get_skip_actions').and_return(['prune'])
flexmock(module.command).should_receive('execute_hook')
flexmock(borgmatic.actions.prune).should_receive('run_prune').never()
@@ -570,6 +617,7 @@ def test_run_actions_with_skip_actions_skips_prune():
def test_run_actions_runs_compact():
flexmock(module).should_receive('add_custom_log_levels')
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.command).should_receive('execute_hook')
flexmock(borgmatic.actions.compact).should_receive('run_compact').once()
@@ -588,6 +636,7 @@ def test_run_actions_runs_compact():
def test_run_actions_with_skip_actions_skips_compact():
flexmock(module).should_receive('add_custom_log_levels')
flexmock(module).should_receive('get_skip_actions').and_return(['compact'])
flexmock(module.command).should_receive('execute_hook')
flexmock(borgmatic.actions.compact).should_receive('run_compact').never()
@@ -606,6 +655,7 @@ def test_run_actions_with_skip_actions_skips_compact():
def test_run_actions_runs_check_when_repository_enabled_for_checks():
flexmock(module).should_receive('add_custom_log_levels')
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.command).should_receive('execute_hook')
flexmock(module.checks).should_receive('repository_enabled_for_checks').and_return(True)
flexmock(borgmatic.actions.check).should_receive('run_check').once()
@@ -625,6 +675,7 @@ def test_run_actions_runs_check_when_repository_enabled_for_checks():
def test_run_actions_skips_check_when_repository_not_enabled_for_checks():
flexmock(module).should_receive('add_custom_log_levels')
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.command).should_receive('execute_hook')
flexmock(module.checks).should_receive('repository_enabled_for_checks').and_return(False)
flexmock(borgmatic.actions.check).should_receive('run_check').never()
@@ -644,6 +695,7 @@ def test_run_actions_skips_check_when_repository_not_enabled_for_checks():
def test_run_actions_with_skip_actions_skips_check():
flexmock(module).should_receive('add_custom_log_levels')
flexmock(module).should_receive('get_skip_actions').and_return(['check'])
flexmock(module.command).should_receive('execute_hook')
flexmock(module.checks).should_receive('repository_enabled_for_checks').and_return(True)
flexmock(borgmatic.actions.check).should_receive('run_check').never()
@@ -663,6 +715,7 @@ def test_run_actions_with_skip_actions_skips_check():
def test_run_actions_runs_extract():
flexmock(module).should_receive('add_custom_log_levels')
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.command).should_receive('execute_hook')
flexmock(borgmatic.actions.extract).should_receive('run_extract').once()
@@ -681,6 +734,7 @@ def test_run_actions_runs_extract():
def test_run_actions_runs_export_tar():
flexmock(module).should_receive('add_custom_log_levels')
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.command).should_receive('execute_hook')
flexmock(borgmatic.actions.export_tar).should_receive('run_export_tar').once()
@@ -699,6 +753,7 @@ def test_run_actions_runs_export_tar():
def test_run_actions_runs_mount():
flexmock(module).should_receive('add_custom_log_levels')
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.command).should_receive('execute_hook')
flexmock(borgmatic.actions.mount).should_receive('run_mount').once()
@@ -717,6 +772,7 @@ def test_run_actions_runs_mount():
def test_run_actions_runs_restore():
flexmock(module).should_receive('add_custom_log_levels')
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.command).should_receive('execute_hook')
flexmock(borgmatic.actions.restore).should_receive('run_restore').once()
@@ -735,6 +791,7 @@ def test_run_actions_runs_restore():
def test_run_actions_runs_rlist():
flexmock(module).should_receive('add_custom_log_levels')
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.command).should_receive('execute_hook')
expected = flexmock()
flexmock(borgmatic.actions.rlist).should_receive('run_rlist').and_yield(expected).once()
@@ -755,6 +812,7 @@ def test_run_actions_runs_rlist():
def test_run_actions_runs_list():
flexmock(module).should_receive('add_custom_log_levels')
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.command).should_receive('execute_hook')
expected = flexmock()
flexmock(borgmatic.actions.list).should_receive('run_list').and_yield(expected).once()
@@ -775,6 +833,7 @@ def test_run_actions_runs_list():
def test_run_actions_runs_rinfo():
flexmock(module).should_receive('add_custom_log_levels')
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.command).should_receive('execute_hook')
expected = flexmock()
flexmock(borgmatic.actions.rinfo).should_receive('run_rinfo').and_yield(expected).once()
@@ -795,6 +854,7 @@ def test_run_actions_runs_rinfo():
def test_run_actions_runs_info():
flexmock(module).should_receive('add_custom_log_levels')
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.command).should_receive('execute_hook')
expected = flexmock()
flexmock(borgmatic.actions.info).should_receive('run_info').and_yield(expected).once()
@@ -815,6 +875,7 @@ def test_run_actions_runs_info():
def test_run_actions_runs_break_lock():
flexmock(module).should_receive('add_custom_log_levels')
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.command).should_receive('execute_hook')
flexmock(borgmatic.actions.break_lock).should_receive('run_break_lock').once()
@@ -833,6 +894,7 @@ def test_run_actions_runs_break_lock():
def test_run_actions_runs_export_key():
flexmock(module).should_receive('add_custom_log_levels')
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.command).should_receive('execute_hook')
flexmock(borgmatic.actions.export_key).should_receive('run_export_key').once()
@@ -851,6 +913,7 @@ def test_run_actions_runs_export_key():
def test_run_actions_runs_borg():
flexmock(module).should_receive('add_custom_log_levels')
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.command).should_receive('execute_hook')
flexmock(borgmatic.actions.borg).should_receive('run_borg').once()
@@ -869,6 +932,7 @@ def test_run_actions_runs_borg():
def test_run_actions_runs_multiple_actions_in_argument_order():
flexmock(module).should_receive('add_custom_log_levels')
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.command).should_receive('execute_hook')
flexmock(borgmatic.actions.borg).should_receive('run_borg').once().ordered()
flexmock(borgmatic.actions.restore).should_receive('run_restore').once().ordered()
+5 -12
View File
@@ -216,6 +216,11 @@ def test_normalize_sections_with_only_scalar_raises():
{'repositories': [{'path': '/repo'}]},
True,
),
(
{'repositories': [{'path': 'first'}, 'file:///repo']},
{'repositories': [{'path': 'first'}, {'path': '/repo'}]},
True,
),
(
{'repositories': [{'path': 'foo@bar:/repo', 'label': 'foo'}]},
{'repositories': [{'path': 'ssh://foo@bar/repo', 'label': 'foo'}]},
@@ -251,15 +256,3 @@ def test_normalize_applies_hard_coded_normalization_to_config(
assert logs
else:
assert logs == []
def test_normalize_raises_error_if_repository_data_is_not_consistent():
flexmock(module).should_receive('normalize_sections').and_return([])
with pytest.raises(TypeError):
module.normalize(
'test.yaml',
{
'repositories': [{'path': 'foo@bar:/repo', 'label': 'foo'}, 'file:///repo'],
},
)
+7 -7
View File
@@ -13,7 +13,7 @@ def test_dump_data_sources_logs_and_skips_if_dump_already_exists():
'/path/to/dump/database'
)
flexmock(module.os.path).should_receive('exists').and_return(True)
flexmock(module.dump).should_receive('create_parent_directory_for_dump').never()
flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
flexmock(module).should_receive('execute_command').never()
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == []
@@ -31,7 +31,7 @@ def test_dump_data_sources_dumps_each_database():
'/path/to/dump/database'
)
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.dump).should_receive('create_parent_directory_for_dump')
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').and_return(processes[0]).and_return(
processes[1]
)
@@ -39,7 +39,7 @@ def test_dump_data_sources_dumps_each_database():
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == processes
def test_dumping_database_with_non_existent_path_warns_and_dumps_database():
def test_dump_data_sources_with_non_existent_path_warns_and_dumps_database():
databases = [
{'path': '/path/to/database1', 'name': 'database1'},
]
@@ -51,13 +51,13 @@ def test_dumping_database_with_non_existent_path_warns_and_dumps_database():
'/path/to/dump/database'
)
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.dump).should_receive('create_parent_directory_for_dump')
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').and_return(processes[0])
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == processes
def test_dumping_database_with_name_all_warns_and_dumps_all_databases():
def test_dump_data_sources_with_name_all_warns_and_dumps_all_databases():
databases = [
{'path': '/path/to/database1', 'name': 'all'},
]
@@ -71,7 +71,7 @@ def test_dumping_database_with_name_all_warns_and_dumps_all_databases():
'/path/to/dump/database'
)
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.dump).should_receive('create_parent_directory_for_dump')
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').and_return(processes[0])
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == processes
@@ -85,7 +85,7 @@ def test_dump_data_sources_does_not_dump_if_dry_run():
'/path/to/dump/database'
)
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.dump).should_receive('create_parent_directory_for_dump').never()
flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
flexmock(module).should_receive('execute_command').never()
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=True) == []
+54
View File
@@ -100,8 +100,39 @@ def test_append_last_lines_with_output_log_level_none_appends_captured_output():
assert captured_output == ['captured', 'line']
@pytest.mark.parametrize(
'full_command,input_file,output_file,environment,expected_result',
(
(('foo', 'bar'), None, None, None, 'foo bar'),
(('foo', 'bar'), flexmock(name='input'), None, None, 'foo bar < input'),
(('foo', 'bar'), None, flexmock(name='output'), None, 'foo bar > output'),
(
('foo', 'bar'),
flexmock(name='input'),
flexmock(name='output'),
None,
'foo bar < input > output',
),
(
('foo', 'bar'),
None,
None,
{'DBPASS': 'secret', 'OTHER': 'thing'},
'DBPASS=*** OTHER=*** foo bar',
),
),
)
def test_log_command_logs_command_constructed_from_arguments(
full_command, input_file, output_file, environment, expected_result
):
flexmock(module.logger).should_receive('debug').with_args(expected_result).once()
module.log_command(full_command, input_file, output_file, environment)
def test_execute_command_calls_full_command():
full_command = ['foo', 'bar']
flexmock(module).should_receive('log_command')
flexmock(module.os, environ={'a': 'b'})
flexmock(module.subprocess).should_receive('Popen').with_args(
full_command,
@@ -122,6 +153,7 @@ def test_execute_command_calls_full_command():
def test_execute_command_calls_full_command_with_output_file():
full_command = ['foo', 'bar']
output_file = flexmock(name='test')
flexmock(module).should_receive('log_command')
flexmock(module.os, environ={'a': 'b'})
flexmock(module.subprocess).should_receive('Popen').with_args(
full_command,
@@ -141,6 +173,7 @@ def test_execute_command_calls_full_command_with_output_file():
def test_execute_command_calls_full_command_without_capturing_output():
full_command = ['foo', 'bar']
flexmock(module).should_receive('log_command')
flexmock(module.os, environ={'a': 'b'})
flexmock(module.subprocess).should_receive('Popen').with_args(
full_command, stdin=None, stdout=None, stderr=None, shell=False, env=None, cwd=None
@@ -156,6 +189,7 @@ def test_execute_command_calls_full_command_without_capturing_output():
def test_execute_command_calls_full_command_with_input_file():
full_command = ['foo', 'bar']
input_file = flexmock(name='test')
flexmock(module).should_receive('log_command')
flexmock(module.os, environ={'a': 'b'})
flexmock(module.subprocess).should_receive('Popen').with_args(
full_command,
@@ -175,6 +209,7 @@ def test_execute_command_calls_full_command_with_input_file():
def test_execute_command_calls_full_command_with_shell():
full_command = ['foo', 'bar']
flexmock(module).should_receive('log_command')
flexmock(module.os, environ={'a': 'b'})
flexmock(module.subprocess).should_receive('Popen').with_args(
' '.join(full_command),
@@ -194,6 +229,7 @@ def test_execute_command_calls_full_command_with_shell():
def test_execute_command_calls_full_command_with_extra_environment():
full_command = ['foo', 'bar']
flexmock(module).should_receive('log_command')
flexmock(module.os, environ={'a': 'b'})
flexmock(module.subprocess).should_receive('Popen').with_args(
full_command,
@@ -213,6 +249,7 @@ def test_execute_command_calls_full_command_with_extra_environment():
def test_execute_command_calls_full_command_with_working_directory():
full_command = ['foo', 'bar']
flexmock(module).should_receive('log_command')
flexmock(module.os, environ={'a': 'b'})
flexmock(module.subprocess).should_receive('Popen').with_args(
full_command,
@@ -233,6 +270,7 @@ def test_execute_command_calls_full_command_with_working_directory():
def test_execute_command_without_run_to_completion_returns_process():
full_command = ['foo', 'bar']
process = flexmock()
flexmock(module).should_receive('log_command')
flexmock(module.os, environ={'a': 'b'})
flexmock(module.subprocess).should_receive('Popen').with_args(
full_command,
@@ -251,6 +289,7 @@ def test_execute_command_without_run_to_completion_returns_process():
def test_execute_command_and_capture_output_returns_stdout():
full_command = ['foo', 'bar']
expected_output = '[]'
flexmock(module).should_receive('log_command')
flexmock(module.os, environ={'a': 'b'})
flexmock(module.subprocess).should_receive('check_output').with_args(
full_command, stderr=None, shell=False, env=None, cwd=None
@@ -264,6 +303,7 @@ def test_execute_command_and_capture_output_returns_stdout():
def test_execute_command_and_capture_output_with_capture_stderr_returns_stderr():
full_command = ['foo', 'bar']
expected_output = '[]'
flexmock(module).should_receive('log_command')
flexmock(module.os, environ={'a': 'b'})
flexmock(module.subprocess).should_receive('check_output').with_args(
full_command, stderr=module.subprocess.STDOUT, shell=False, env=None, cwd=None
@@ -278,6 +318,7 @@ def test_execute_command_and_capture_output_returns_output_when_process_error_is
full_command = ['foo', 'bar']
expected_output = '[]'
err_output = b'[]'
flexmock(module).should_receive('log_command')
flexmock(module.os, environ={'a': 'b'})
flexmock(module.subprocess).should_receive('check_output').with_args(
full_command, stderr=None, shell=False, env=None, cwd=None
@@ -292,6 +333,7 @@ def test_execute_command_and_capture_output_returns_output_when_process_error_is
def test_execute_command_and_capture_output_raises_when_command_errors():
full_command = ['foo', 'bar']
expected_output = '[]'
flexmock(module).should_receive('log_command')
flexmock(module.os, environ={'a': 'b'})
flexmock(module.subprocess).should_receive('check_output').with_args(
full_command, stderr=None, shell=False, env=None, cwd=None
@@ -305,6 +347,7 @@ def test_execute_command_and_capture_output_raises_when_command_errors():
def test_execute_command_and_capture_output_returns_output_with_shell():
full_command = ['foo', 'bar']
expected_output = '[]'
flexmock(module).should_receive('log_command')
flexmock(module.os, environ={'a': 'b'})
flexmock(module.subprocess).should_receive('check_output').with_args(
'foo bar', stderr=None, shell=True, env=None, cwd=None
@@ -318,6 +361,7 @@ def test_execute_command_and_capture_output_returns_output_with_shell():
def test_execute_command_and_capture_output_returns_output_with_extra_environment():
full_command = ['foo', 'bar']
expected_output = '[]'
flexmock(module).should_receive('log_command')
flexmock(module.os, environ={'a': 'b'})
flexmock(module.subprocess).should_receive('check_output').with_args(
full_command,
@@ -337,6 +381,7 @@ def test_execute_command_and_capture_output_returns_output_with_extra_environmen
def test_execute_command_and_capture_output_returns_output_with_working_directory():
full_command = ['foo', 'bar']
expected_output = '[]'
flexmock(module).should_receive('log_command')
flexmock(module.os, environ={'a': 'b'})
flexmock(module.subprocess).should_receive('check_output').with_args(
full_command, stderr=None, shell=False, env=None, cwd='/working'
@@ -352,6 +397,7 @@ def test_execute_command_and_capture_output_returns_output_with_working_director
def test_execute_command_with_processes_calls_full_command():
full_command = ['foo', 'bar']
processes = (flexmock(),)
flexmock(module).should_receive('log_command')
flexmock(module.os, environ={'a': 'b'})
flexmock(module.subprocess).should_receive('Popen').with_args(
full_command,
@@ -372,6 +418,7 @@ def test_execute_command_with_processes_calls_full_command():
def test_execute_command_with_processes_returns_output_with_output_log_level_none():
full_command = ['foo', 'bar']
processes = (flexmock(),)
flexmock(module).should_receive('log_command')
flexmock(module.os, environ={'a': 'b'})
process = flexmock(stdout=None)
flexmock(module.subprocess).should_receive('Popen').with_args(
@@ -394,6 +441,7 @@ def test_execute_command_with_processes_calls_full_command_with_output_file():
full_command = ['foo', 'bar']
processes = (flexmock(),)
output_file = flexmock(name='test')
flexmock(module).should_receive('log_command')
flexmock(module.os, environ={'a': 'b'})
flexmock(module.subprocess).should_receive('Popen').with_args(
full_command,
@@ -414,6 +462,7 @@ def test_execute_command_with_processes_calls_full_command_with_output_file():
def test_execute_command_with_processes_calls_full_command_without_capturing_output():
full_command = ['foo', 'bar']
processes = (flexmock(),)
flexmock(module).should_receive('log_command')
flexmock(module.os, environ={'a': 'b'})
flexmock(module.subprocess).should_receive('Popen').with_args(
full_command, stdin=None, stdout=None, stderr=None, shell=False, env=None, cwd=None
@@ -432,6 +481,7 @@ def test_execute_command_with_processes_calls_full_command_with_input_file():
full_command = ['foo', 'bar']
processes = (flexmock(),)
input_file = flexmock(name='test')
flexmock(module).should_receive('log_command')
flexmock(module.os, environ={'a': 'b'})
flexmock(module.subprocess).should_receive('Popen').with_args(
full_command,
@@ -452,6 +502,7 @@ def test_execute_command_with_processes_calls_full_command_with_input_file():
def test_execute_command_with_processes_calls_full_command_with_shell():
full_command = ['foo', 'bar']
processes = (flexmock(),)
flexmock(module).should_receive('log_command')
flexmock(module.os, environ={'a': 'b'})
flexmock(module.subprocess).should_receive('Popen').with_args(
' '.join(full_command),
@@ -472,6 +523,7 @@ def test_execute_command_with_processes_calls_full_command_with_shell():
def test_execute_command_with_processes_calls_full_command_with_extra_environment():
full_command = ['foo', 'bar']
processes = (flexmock(),)
flexmock(module).should_receive('log_command')
flexmock(module.os, environ={'a': 'b'})
flexmock(module.subprocess).should_receive('Popen').with_args(
full_command,
@@ -494,6 +546,7 @@ def test_execute_command_with_processes_calls_full_command_with_extra_environmen
def test_execute_command_with_processes_calls_full_command_with_working_directory():
full_command = ['foo', 'bar']
processes = (flexmock(),)
flexmock(module).should_receive('log_command')
flexmock(module.os, environ={'a': 'b'})
flexmock(module.subprocess).should_receive('Popen').with_args(
full_command,
@@ -515,6 +568,7 @@ def test_execute_command_with_processes_calls_full_command_with_working_director
def test_execute_command_with_processes_kills_processes_on_error():
full_command = ['foo', 'bar']
flexmock(module).should_receive('log_command')
process = flexmock(stdout=flexmock(read=lambda count: None))
process.should_receive('poll')
process.should_receive('kill').once()