Compare commits

..
11 Commits
22 changed files with 579 additions and 143 deletions
+1
View File
@@ -17,6 +17,7 @@ jobs:
docs:
needs: [test]
runs-on: host
if: gitea.event_name == 'push'
env:
IMAGE_NAME: projects.torsion.org/borgmatic-collective/borgmatic:docs
+16
View File
@@ -1,3 +1,19 @@
2.0.4
* #1072: Fix path rewriting for non-root patterns in the ZFS, Btrfs, and LVM hooks.
* #1073: Clarify the documentation about when an "after: error" command hook runs and how it
differs from other hooks:
https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/
* #1075: Fix an incorrect warning about Borg placeholders being unsupported in a command hook.
* #1080: If the exact same "everything" command hook is present in multiple configuration files,
only run it once.
2.0.3
* #1065: Fix a regression in monitoring hooks in which an error pinged the finish state instead of
the fail state.
* #1066: Add a "states" option to command hooks, so you can optionally skip an "after" hook if
borgmatic encounters an error.
* #1071: Fix an error in the LVM hook when removing a snapshot directory.
2.0.2
* #1035: Document potential performance issues and workarounds with the ZFS, Btrfs, and LVM hooks:
https://torsion.org/borgmatic/docs/how-to/snapshot-your-filesystems/
+39 -8
View File
@@ -165,6 +165,29 @@ def expand_patterns(patterns, working_directory=None, skip_paths=None):
)
def get_existent_path_or_parent(path):
'''
Given a path, return it if it exists. Otherwise, return the longest parent directory of the path
that exists. Return None if none of these paths exist.
This is used below for finding an existent path prefix of pattern's path, which is necessary if
the path contain globs or other special characters that we don't want to try to interpret
(because we want to leave that responsibility to Borg).
'''
if path.startswith('/e2e/'):
return None
try:
return next(
candidate_path
for candidate_path in (path,)
+ tuple(str(parent) for parent in pathlib.PurePath(path).parents)
if os.path.exists(candidate_path)
)
except StopIteration:
return None
def device_map_patterns(patterns, working_directory=None):
'''
Given a sequence of borgmatic.borg.pattern.Pattern instances and an optional working directory,
@@ -174,23 +197,31 @@ def device_map_patterns(patterns, working_directory=None):
This is handy for determining whether two different pattern paths are on the same filesystem
(have the same device identifier).
This function only considers the start of a pattern's path—from the start of the path up until
there's a path component with a glob or other non-literal character. If there are no such
characters, the whole path is considered. The rationale is that it's not feasible for borgmatic
to interpret Borg's patterns to see which actual files (and therefore devices) they map to. So
for instance, a pattern with a path of "/var/log/*/data" would end up with its device set to the
device of "/var/log"—ignoring the "/*/data" part due to that glob.
The one exception is that if a regular expression pattern path starts with "^", that will get
stripped off for purposes of determining its device.
'''
return tuple(
borgmatic.borg.pattern.Pattern(
pattern.path,
pattern.type,
pattern.style,
device=pattern.device
or (
os.stat(full_path).st_dev
if pattern.type == borgmatic.borg.pattern.Pattern_type.ROOT
and os.path.exists(full_path)
else None
),
device=pattern.device or (os.stat(existent_path).st_dev if existent_path else None),
source=pattern.source,
)
for pattern in patterns
for full_path in (os.path.join(working_directory or '', pattern.path),)
for existent_path in (
get_existent_path_or_parent(
os.path.join(working_directory or '', pattern.path.lstrip('^'))
),
)
)
+1 -1
View File
@@ -69,7 +69,7 @@ def collect_special_file_paths(
'''
# Omit "--exclude-nodump" from the Borg dry run command, because that flag causes Borg to open
# files including any named pipe we've created. And omit "--filter" because that can break the
# paths output parsing below such that path lines no longer start with th expected "- ".
# paths output parsing below such that path lines no longer start with the expected "- ".
paths_output = execute_command_and_capture_output(
flags.omit_flag_and_value(flags.omit_flag(create_command, '--exclude-nodump'), '--filter')
+ ('--dry-run', '--list'),
+60 -18
View File
@@ -279,8 +279,19 @@ def run_configuration(config_filename, config, config_paths, arguments):
encountered_error = error
error_repository = repository
# Re-raise any error, so that the Monitoring_hooks context manager wrapping this
# code can see the error and act accordingly. Do this here rather than as soon as
# the error is encountered so that an error with one repository doesn't prevent
# other repositories from running.
if encountered_error:
raise encountered_error
except (OSError, CalledProcessError, ValueError) as error:
yield from log_error_records('Error running configuration', error)
# No need to repeat logging of the error if it was already logged above.
if error_repository:
yield from log_error_records('Error running configuration')
else:
yield from log_error_records('Error running configuration', error)
encountered_error = error
@@ -290,7 +301,10 @@ def run_configuration(config_filename, config, config_paths, arguments):
try:
command.execute_hooks(
command.filter_hooks(
config.get('commands'), after='error', action_names=arguments.keys()
config.get('commands'),
after='error',
action_names=arguments.keys(),
state_names=['fail'],
),
config.get('umask'),
borgmatic.config.paths.get_working_directory(config),
@@ -879,23 +893,36 @@ def collect_configuration_run_summary_logs(configs, config_paths, arguments, log
return
try:
seen_command_hooks = []
for config_filename, config in configs.items():
command.execute_hooks(
command.filter_hooks(
config.get('commands'), before='everything', action_names=arguments.keys()
command_hooks = command.filter_hooks(
tuple(
command_hook
for command_hook in config.get('commands', ())
if command_hook not in seen_command_hooks
),
config.get('umask'),
borgmatic.config.paths.get_working_directory(config),
arguments['global'].dry_run,
configuration_filename=config_filename,
log_file=log_file_path or '',
before='everything',
action_names=arguments.keys(),
)
if command_hooks:
command.execute_hooks(
command_hooks,
config.get('umask'),
borgmatic.config.paths.get_working_directory(config),
arguments['global'].dry_run,
configuration_filename=config_filename,
log_file=log_file_path or '',
)
seen_command_hooks += list(command_hooks)
except (CalledProcessError, ValueError, OSError) as error:
yield from log_error_records('Error running before everything hook', error)
return
# Execute the actions corresponding to each configuration file.
json_results = []
encountered_error = False
for config_filename, config in configs.items():
with Log_prefix(config_filename):
@@ -906,6 +933,7 @@ def collect_configuration_run_summary_logs(configs, config_paths, arguments, log
)
if error_logs:
encountered_error = True
yield from log_error_records('An error occurred')
yield from error_logs
else:
@@ -928,23 +956,37 @@ def collect_configuration_run_summary_logs(configs, config_paths, arguments, log
local_path=get_local_path(configs),
)
except (CalledProcessError, OSError) as error:
encountered_error = True
yield from log_error_records('Error unmounting mount point', error)
if json_results:
sys.stdout.write(json.dumps(json_results))
try:
seen_command_hooks = []
for config_filename, config in configs.items():
command.execute_hooks(
command.filter_hooks(
config.get('commands'), after='everything', action_names=arguments.keys()
command_hooks = command.filter_hooks(
tuple(
command_hook
for command_hook in config.get('commands', ())
if command_hook not in seen_command_hooks
),
config.get('umask'),
borgmatic.config.paths.get_working_directory(config),
arguments['global'].dry_run,
configuration_filename=config_filename,
log_file=log_file_path or '',
after='everything',
action_names=arguments.keys(),
state_names=['fail' if encountered_error else 'finish'],
)
if command_hooks:
command.execute_hooks(
command_hooks,
config.get('umask'),
borgmatic.config.paths.get_working_directory(config),
arguments['global'].dry_run,
configuration_filename=config_filename,
log_file=log_file_path or '',
)
seen_command_hooks += list(command_hooks)
except (CalledProcessError, ValueError, OSError) as error:
yield from log_error_records('Error running after everything hook', error)
+20
View File
@@ -1225,6 +1225,26 @@ properties:
particular actions listed here. Defaults to
running for all actions.
example: [create, prune, compact, check]
states:
type: array
items:
type: string
enum:
- finish
- fail
description: |
Only trigger the hook if borgmatic encounters one
of the states (execution results) listed here,
where:
* "finish": No errors occurred.
* "fail": An error occurred.
This state is evaluated only for the scope of the
configured "action", "repository", etc., rather
than for the entire borgmatic run. Only available
for "after" hooks. Defaults to running the hook
for all states.
example:
- finish
run:
type: array
items:
+33 -16
View File
@@ -13,6 +13,19 @@ logger = logging.getLogger(__name__)
SOFT_FAIL_EXIT_CODE = 75
BORG_PLACEHOLDERS = {
'{hostname}',
'{fqdn}',
'{reverse-fqdn}',
'{now}',
'{utcnow}',
'{user}',
'{pid}',
'{borgversion}',
'{borgmajor}',
'{borgminor}',
'{borgpatch}',
}
def interpolate_context(hook_description, command, context):
@@ -23,10 +36,13 @@ def interpolate_context(hook_description, command, context):
for name, value in context.items():
command = command.replace(f'{{{name}}}', shlex.quote(str(value)))
for unsupported_variable in re.findall(r'{\w+}', command):
logger.warning(
f"Variable '{unsupported_variable}' is not supported in {hook_description} hook"
)
for unsupported_variable in re.findall(r'\{\w+\}', command):
# Warn about variables unknown to borgmatic, but don't warn if the variable name happens to
# be a Borg placeholder, as Borg should hopefully consume it.
if unsupported_variable not in BORG_PLACEHOLDERS:
logger.warning(
f'Variable "{unsupported_variable}" is not supported in the {hook_description} hook'
)
return command
@@ -47,21 +63,25 @@ def make_environment(current_environment, sys_module=sys):
return environment
def filter_hooks(command_hooks, before=None, after=None, hook_name=None, action_names=None):
def filter_hooks(command_hooks, before=None, after=None, action_names=None, state_names=None):
'''
Given a sequence of command hook dicts from configuration and one or more filters (before name,
after name, calling hook name, or a sequence of action names), filter down the command hooks to
just the ones that match the given filters.
after name, a sequence of action names, and/or a sequence of execution result state names),
filter down the command hooks to just the ones that match the given filters.
'''
return tuple(
hook_config
for hook_config in command_hooks or ()
for config_action_names in (hook_config.get('when'),)
for config_state_names in (hook_config.get('states'),)
if before is None or hook_config.get('before') == before
if after is None or hook_config.get('after') == after
if action_names is None
or config_action_names is None
or set(config_action_names or ()).intersection(set(action_names))
if state_names is None
or config_state_names is None
or set(config_state_names or ()).intersection(set(state_names))
)
@@ -143,7 +163,7 @@ class Before_after_hooks:
before_after='do_stuff',
umask=config.get('umask'),
dry_run=dry_run,
hook_name='myhook',
action_names=['create'],
):
do()
some()
@@ -160,22 +180,20 @@ class Before_after_hooks:
umask,
working_directory,
dry_run,
hook_name=None,
action_names=None,
**context,
):
'''
Given a sequence of command hook configuration dicts, the before/after name, a umask to run
commands with, a working directory to run commands with, a dry run flag, the name of the
calling hook, a sequence of action names, and any context for the executed commands, save
those data points for use below.
commands with, a working directory to run commands with, a dry run flag, a sequence of
action names, and any context for the executed commands, save those data points for use
below.
'''
self.command_hooks = command_hooks
self.before_after = before_after
self.umask = umask
self.working_directory = working_directory
self.dry_run = dry_run
self.hook_name = hook_name
self.action_names = action_names
self.context = context
@@ -188,7 +206,6 @@ class Before_after_hooks:
borgmatic.hooks.command.filter_hooks(
self.command_hooks,
before=self.before_after,
hook_name=self.hook_name,
action_names=self.action_names,
),
self.umask,
@@ -202,7 +219,7 @@ class Before_after_hooks:
# Trigger the after hook manually, since raising here will prevent it from being run
# otherwise.
self.__exit__(None, None, None)
self.__exit__(exception_type=type(error), exception=error, traceback=None)
raise ValueError(f'Error running before {self.before_after} hook: {error}')
@@ -215,8 +232,8 @@ class Before_after_hooks:
borgmatic.hooks.command.filter_hooks(
self.command_hooks,
after=self.before_after,
hook_name=self.hook_name,
action_names=self.action_names,
state_names=['fail' if exception_type else 'finish'],
),
self.umask,
self.working_directory,
+1 -1
View File
@@ -407,7 +407,7 @@ def remove_data_source_dumps(hook_config, config, borgmatic_runtime_directory, d
continue
if not dry_run:
shutil.rmtree(snapshots_directory)
shutil.rmtree(snapshots_directory, ignore_errors=True)
# Delete snapshots.
lvremove_command = hook_config.get('lvremove_command', 'lvremove')
+5 -5
View File
@@ -8,9 +8,9 @@ def get_contained_patterns(parent_directory, candidate_patterns):
'''
Given a parent directory and a set of candidate patterns potentially inside it, get the subset
of contained patterns for which the parent directory is actually the parent, a grandparent, the
very same directory, etc. The idea is if, say, /var/log and /var/lib are candidate pattern
paths, but there's a parent directory (logical volume, dataset, subvolume, etc.) at /var, then
/var is what we want to snapshot.
very same directory, etc. The idea is if, say, "/var/log" and "/var/lib" are candidate pattern
paths, but there's a parent directory (logical volume, dataset, subvolume, etc.) at "/var", then
"/var" is what we want to snapshot.
If a parent directory and a candidate pattern are on different devices, skip the pattern. That's
because any snapshot of a parent directory won't actually include "contained" directories if
@@ -18,8 +18,8 @@ def get_contained_patterns(parent_directory, candidate_patterns):
For this function to work, a candidate pattern path can't have any globs or other non-literal
characters in the initial portion of the path that matches the parent directory. For instance, a
parent directory of /var would match a candidate pattern path of /var/log/*/data, but not a
pattern path like /v*/log/*/data.
parent directory of "/var" would match a candidate pattern path of "/var/log/*/data", but not a
pattern path like "/v*/log/*/data".
The one exception is that if a regular expression pattern path starts with "^", that will get
stripped off for purposes of matching against a parent directory.
@@ -55,6 +55,25 @@ commands:
- "echo Backup: start"
```
By default, an `after` command hook runs even if an error occurs in the
corresponding `before` hook or between those two hooks. This allows you to
perform cleanup steps that correspond to `before` preparation commands—even when
something goes wrong. You may notice that this is a departure from the way that
the deprecated `after_*` hooks worked in borgmatic prior to version 2.0.0.
<span class="minilink minilink-addedin">New in version 2.0.3</span> You can
customize this behavior with the `states` option. For instance, here's an
example of an `after` hook that only triggers on success and not on error:
```yaml
commands:
- after: action
when: [create]
states: [finish]
run:
- echo "After successful create!"
```
Each command in the `commands:` list has the following options:
* `before` or `after`: Name for the point in borgmatic's execution that the commands should be run before or after, one of:
@@ -64,17 +83,17 @@ Each command in the `commands:` list has the following options:
* `everything` runs before or after all configuration files. Errors here do not trigger `error` hooks or the `fail` state in monitoring hooks. This replaces the deprecated `before_everything` and `after_everything`.
* `error` runs after an error occurs—and it's only available for `after`. This replaces the deprecated `on_error` hook.
* `when`: Only trigger the hook when borgmatic is run with particular actions (`create`, `prune`, etc.) listed here. Defaults to running for all actions.
* `states`: <span class="minilink minilink-addedin">New in version 2.0.3</span> Only trigger the hook if borgmatic encounters one of the states (execution results) listed here. This state is evaluated only for the scope of the configured `action`, `repository`, etc., rather than for the entire borgmatic run. Only available for `after` hooks. Defaults to running the hook for all states. One or more of:
* `finish`: No errors occurred.
* `fail`: An error occurred.
* `run`: List of one or more shell commands or scripts to run when this command hook is triggered.
An `after` command hook runs even if an error occurs in the corresponding
`before` hook or between those two hooks. This allows you to perform cleanup
steps that correspond to `before` preparation commands—even when something goes
wrong. This is a departure from the way that the deprecated `after_*` hooks
worked in borgmatic prior to version 2.0.0.
When command hooks run, they respect the `working_directory` option if it is
configured, meaning that the hook commands are run in that directory.
Additionally, when command hooks run, they respect the `working_directory`
option if it is configured, meaning that the hook commands are run in that
directory.
<span class="minilink minilink-addedin">New in version 2.0.4</span>If the exact
same `everything` command hook is present in multiple configuration files,
borgmatic only runs it once.
### Order of execution
@@ -97,11 +116,20 @@ borgmatic for the `create` and `prune` actions. Here's the order of execution:
* Run `after: action` hooks for `prune`.
* Run `after: repository` hooks (for the first repository).
* Run `after: configuration` hooks (from the first configuration file).
* Run `after: error` hooks (if an error occurs).
* Run `after: everything` hooks (from all configuration files).
This same order of execution extends to multiple repositories and/or
configuration files.
Based on the above, you can see the difference between, say, an `after: action`
hook with `states: [fail]` and an `after: error` hook. The `after: action hook`
runs immediately after the create action fails for a particular repository—so
before any subsequent actions for that repository or other repositories even
have a chance to run. Whereas the `after: error` hook doesn't run until all
actions for—and repositories in—a configuration file have had a chance to
execute.
### Deprecated command hooks
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "borgmatic"
version = "2.0.2"
version = "2.0.4"
authors = [
{ name="Dan Helfman", email="witten@torsion.org" },
]
+1 -1
View File
@@ -53,7 +53,7 @@ def save_snapshots(snapshot_paths):
def print_subvolume_list(arguments, snapshot_paths):
assert arguments.subvolume_path == '/mnt/subvolume'
assert arguments.subvolume_path == '/e2e/mnt/subvolume'
if not arguments.snapshots_only:
for line in BUILTIN_SUBVOLUME_LIST_LINES:
+1 -1
View File
@@ -14,7 +14,7 @@ def parse_arguments(*unparsed_arguments):
BUILTIN_FILESYSTEM_MOUNT_OUTPUT = '''{
"filesystems": [
{
"target": "/mnt/subvolume",
"target": "/e2e/mnt/subvolume",
"source": "/dev/loop0",
"fstype": "btrfs",
"options": "rw,relatime,ssd,space_cache=v2,subvolid=5,subvol=/"
+1 -1
View File
@@ -20,7 +20,7 @@ BUILTIN_BLOCK_DEVICES = {
{
'name': 'vgroup-lvolume',
'path': '/dev/mapper/vgroup-lvolume',
'mountpoint': '/mnt/lvolume',
'mountpoint': '/e2e/mnt/lvolume',
'type': 'lvm',
},
{
+4 -4
View File
@@ -23,20 +23,20 @@ def parse_arguments(*unparsed_arguments):
BUILTIN_DATASETS = (
{
'name': 'pool',
'name': 'e2e/pool',
'used': '256K',
'avail': '23.7M',
'refer': '25K',
'canmount': 'on',
'mountpoint': '/pool',
'mountpoint': '/e2e/pool',
},
{
'name': 'pool/dataset',
'name': 'e2e/pool/dataset',
'used': '256K',
'avail': '23.7M',
'refer': '25K',
'canmount': 'on',
'mountpoint': '/pool/dataset',
'mountpoint': '/e2e/pool/dataset',
},
)
@@ -16,10 +16,10 @@ def generate_configuration(config_path, repository_path):
open(config_path)
.read()
.replace('ssh://user@backupserver/./sourcehostname.borg', repository_path)
.replace('- path: /mnt/backup', '')
.replace('- path: /e2e/mnt/backup', '')
.replace('label: local', '')
.replace('- /home', f'- {config_path}')
.replace('- /etc', '- /mnt/subvolume/subdir')
.replace('- /etc', '- /e2e/mnt/subvolume/subdir')
.replace('- /var/log/syslog*', '')
+ 'encryption_passphrase: "test"\n'
+ 'btrfs:\n'
@@ -51,11 +51,11 @@ def test_btrfs_create_and_list():
f'borgmatic --config {config_path} list --archive latest'.split(' ')
).decode(sys.stdout.encoding)
assert 'mnt/subvolume/subdir/file.txt' in output
assert 'e2e/mnt/subvolume/subdir/file.txt' in output
# Assert that the snapshot has been deleted.
assert not subprocess.check_output(
'python3 /app/tests/end-to-end/commands/fake_btrfs.py subvolume list -s /mnt/subvolume'.split(
'python3 /app/tests/end-to-end/commands/fake_btrfs.py subvolume list -s /e2e/mnt/subvolume'.split(
' '
)
)
@@ -17,10 +17,10 @@ def generate_configuration(config_path, repository_path):
open(config_path)
.read()
.replace('ssh://user@backupserver/./sourcehostname.borg', repository_path)
.replace('- path: /mnt/backup', '')
.replace('- path: /e2e/mnt/backup', '')
.replace('label: local', '')
.replace('- /home', f'- {config_path}')
.replace('- /etc', '- /mnt/lvolume/subdir')
.replace('- /etc', '- /e2e/mnt/lvolume/subdir')
.replace('- /var/log/syslog*', '')
+ 'encryption_passphrase: "test"\n'
+ 'lvm:\n'
@@ -56,7 +56,7 @@ def test_lvm_create_and_list():
f'borgmatic --config {config_path} list --archive latest'.split(' ')
).decode(sys.stdout.encoding)
assert 'mnt/lvolume/subdir/file.txt' in output
assert 'e2e/mnt/lvolume/subdir/file.txt' in output
# Assert that the snapshot has been deleted.
assert not json.loads(
@@ -19,7 +19,7 @@ def generate_configuration(config_path, repository_path):
.replace('- path: /mnt/backup', '')
.replace('label: local', '')
.replace('- /home', f'- {config_path}')
.replace('- /etc', '- /pool/dataset/subdir')
.replace('- /etc', '- /e2e/pool/dataset/subdir')
.replace('- /var/log/syslog*', '')
+ 'encryption_passphrase: "test"\n'
+ 'zfs:\n'
@@ -45,14 +45,14 @@ def test_zfs_create_and_list():
)
# Run a create action to exercise ZFS snapshotting and backup.
subprocess.check_call(f'borgmatic --config {config_path} create'.split(' '))
subprocess.check_call(f'borgmatic -v 2 --config {config_path} create'.split(' '))
# List the resulting archive and assert that the snapshotted files are there.
output = subprocess.check_output(
f'borgmatic --config {config_path} list --archive latest'.split(' ')
).decode(sys.stdout.encoding)
assert 'pool/dataset/subdir/file.txt' in output
assert 'e2e/pool/dataset/subdir/file.txt' in output
# Assert that the snapshot has been deleted.
assert not subprocess.check_output(
@@ -12,3 +12,59 @@ def test_borgmatic_version_matches_news_version():
news_version = open('NEWS').readline()
assert borgmatic_version == news_version
def test_run_configuration_without_error_pings_monitoring_hooks_start_and_finish():
config = {'repositories': [{'path': 'foo'}]}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module).should_receive('run_actions').and_return([])
flexmock(module.dispatch).should_receive('call_hooks')
flexmock(module.dispatch).should_receive('call_hooks').with_args(
'ping_monitor',
config,
module.dispatch.Hook_type.MONITORING,
'test.yaml',
module.monitor.State.START,
object,
object,
).once()
flexmock(module.dispatch).should_receive('call_hooks').with_args(
'ping_monitor',
config,
module.dispatch.Hook_type.MONITORING,
'test.yaml',
module.monitor.State.FINISH,
object,
object,
).once()
list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
def test_run_configuration_with_action_error_pings_monioring_hooks_start_and_fail():
config = {'repositories': [{'path': 'foo'}]}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module).should_receive('run_actions').and_raise(OSError)
flexmock(module.dispatch).should_receive('call_hooks')
flexmock(module.dispatch).should_receive('call_hooks').with_args(
'ping_monitor',
config,
module.dispatch.Hook_type.MONITORING,
'test.yaml',
module.monitor.State.START,
object,
object,
).once()
flexmock(module.dispatch).should_receive('call_hooks').with_args(
'ping_monitor',
config,
module.dispatch.Hook_type.MONITORING,
'test.yaml',
module.monitor.State.FAIL,
object,
object,
).once()
list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
+52 -19
View File
@@ -245,36 +245,69 @@ def test_expand_patterns_expands_only_tildes_in_non_root_patterns():
assert paths == (Pattern('/root/bar/*', Pattern_type.INCLUDE),)
def test_device_map_patterns_gives_device_id_per_path():
def test_get_existent_path_or_parent_passes_through_existent_path():
flexmock(module.os.path).should_receive('exists').and_return(True)
assert module.get_existent_path_or_parent('/foo/bar/baz') == '/foo/bar/baz'
def test_get_existent_path_or_parent_with_non_existent_path_returns_none():
flexmock(module.os.path).should_receive('exists').and_return(False)
assert module.get_existent_path_or_parent('/foo/bar/baz') is None
def test_get_existent_path_or_parent_with_non_existent_path_returns_existent_parent():
flexmock(module.os.path).should_receive('exists').with_args('/foo/bar/baz*').and_return(False)
flexmock(module.os.path).should_receive('exists').with_args('/foo/bar').and_return(True)
flexmock(module.os.path).should_receive('exists').with_args('/foo').never()
flexmock(module.os.path).should_receive('exists').with_args('/').never()
assert module.get_existent_path_or_parent('/foo/bar/baz*') == '/foo/bar'
def test_get_existent_path_or_parent_with_non_existent_path_returns_existent_grandparent():
flexmock(module.os.path).should_receive('exists').with_args('/foo/bar/baz*').and_return(False)
flexmock(module.os.path).should_receive('exists').with_args('/foo/bar').and_return(False)
flexmock(module.os.path).should_receive('exists').with_args('/foo').and_return(True)
flexmock(module.os.path).should_receive('exists').with_args('/').never()
assert module.get_existent_path_or_parent('/foo/bar/baz*') == '/foo'
def test_get_existent_path_or_parent_with_end_to_end_test_prefix_returns_none():
flexmock(module.os.path).should_receive('exists').never()
assert module.get_existent_path_or_parent('/e2e/foo/bar/baz') is None
def test_device_map_patterns_gives_device_id_per_path():
flexmock(module).should_receive('get_existent_path_or_parent').replace_with(lambda path: path)
flexmock(module.os).should_receive('stat').with_args('/foo').and_return(flexmock(st_dev=55))
flexmock(module.os).should_receive('stat').with_args('/bar').and_return(flexmock(st_dev=66))
device_map = module.device_map_patterns((Pattern('/foo'), Pattern('/bar')))
assert device_map == (
Pattern('/foo', device=55),
Pattern('/bar', device=66),
)
def test_device_map_patterns_only_considers_root_patterns():
flexmock(module.os.path).should_receive('exists').and_return(True)
flexmock(module.os).should_receive('stat').with_args('/foo').and_return(flexmock(st_dev=55))
flexmock(module.os).should_receive('stat').with_args('/bar*').never()
device_map = module.device_map_patterns(
(Pattern('/foo'), Pattern('/bar*', Pattern_type.INCLUDE))
(
Pattern('/foo'),
Pattern('^/bar', type=Pattern_type.INCLUDE, style=Pattern_style.REGULAR_EXPRESSION),
)
)
assert device_map == (
Pattern('/foo', device=55),
Pattern('/bar*', Pattern_type.INCLUDE),
Pattern(
'^/bar', type=Pattern_type.INCLUDE, style=Pattern_style.REGULAR_EXPRESSION, device=66
),
)
def test_device_map_patterns_with_missing_path_does_not_error():
flexmock(module.os.path).should_receive('exists').and_return(True).and_return(False)
flexmock(module).should_receive('get_existent_path_or_parent').with_args('/foo').and_return(
'/foo'
)
flexmock(module).should_receive('get_existent_path_or_parent').with_args('/bar').and_return(
None
)
flexmock(module.os).should_receive('stat').with_args('/foo').and_return(flexmock(st_dev=55))
flexmock(module.os).should_receive('stat').with_args('/bar').never()
@@ -287,7 +320,7 @@ def test_device_map_patterns_with_missing_path_does_not_error():
def test_device_map_patterns_uses_working_directory_to_construct_path():
flexmock(module.os.path).should_receive('exists').and_return(True)
flexmock(module).should_receive('get_existent_path_or_parent').replace_with(lambda path: path)
flexmock(module.os).should_receive('stat').with_args('/foo').and_return(flexmock(st_dev=55))
flexmock(module.os).should_receive('stat').with_args('/working/dir/bar').and_return(
flexmock(st_dev=66)
@@ -304,7 +337,7 @@ def test_device_map_patterns_uses_working_directory_to_construct_path():
def test_device_map_patterns_with_existing_device_id_does_not_overwrite_it():
flexmock(module.os.path).should_receive('exists').and_return(True)
flexmock(module).should_receive('get_existent_path_or_parent').replace_with(lambda path: path)
flexmock(module.os).should_receive('stat').with_args('/foo').and_return(flexmock(st_dev=55))
flexmock(module.os).should_receive('stat').with_args('/bar').and_return(flexmock(st_dev=100))
+157 -33
View File
@@ -465,8 +465,10 @@ def test_run_configuration_logs_actions_error():
flexmock(module).should_receive('Monitoring_hooks').and_return(flexmock())
flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
expected_results = [flexmock()]
flexmock(module).should_receive('log_error_records').and_return(expected_results)
expected_results = [flexmock(), flexmock()]
flexmock(module).should_receive('log_error_records').and_return(
expected_results[:1]
).and_return(expected_results[1:])
flexmock(module).should_receive('Log_prefix').and_return(flexmock())
flexmock(module).should_receive('run_actions').and_raise(OSError)
flexmock(module.command).should_receive('filter_hooks')
@@ -532,7 +534,7 @@ def test_run_configuration_logs_on_error_hook_error():
expected_results = [flexmock(), flexmock()]
flexmock(module).should_receive('log_error_records').and_return(
expected_results[:1]
).and_return(expected_results[1:])
).and_return(expected_results[1:2]).and_return(expected_results[2:])
flexmock(module).should_receive('Log_prefix').and_return(flexmock())
flexmock(module).should_receive('run_actions').and_raise(OSError)
config = {'repositories': [{'path': 'foo'}]}
@@ -601,8 +603,10 @@ def test_run_configuration_bails_for_on_error_hook_soft_failure():
error = subprocess.CalledProcessError(borgmatic.hooks.command.SOFT_FAIL_EXIT_CODE, 'try again')
flexmock(module.command).should_receive('filter_hooks')
flexmock(module.command).should_receive('execute_hooks').and_raise(error)
expected_results = [flexmock()]
flexmock(module).should_receive('log_error_records').and_return(expected_results)
expected_results = [flexmock(), flexmock()]
flexmock(module).should_receive('log_error_records').and_return(
expected_results[:1]
).and_return(expected_results[1:])
flexmock(module).should_receive('Log_prefix').and_return(flexmock())
flexmock(module).should_receive('run_actions').and_raise(OSError)
config = {'repositories': [{'path': 'foo'}]}
@@ -654,11 +658,14 @@ def test_run_configuration_retries_hard_error():
levelno=logging.WARNING,
log_command_error_output=True,
).and_return([flexmock()])
error_logs = [flexmock()]
error_logs = [flexmock(), flexmock()]
flexmock(module).should_receive('log_error_records').with_args(
'Error running actions for repository',
OSError,
).and_return(error_logs)
).and_return(error_logs[:1]).ordered()
flexmock(module).should_receive('log_error_records').with_args(
'Error running configuration',
).and_return(error_logs[1:]).ordered()
flexmock(module.command).should_receive('filter_hooks')
flexmock(module.command).should_receive('execute_hooks')
config = {'repositories': [{'path': 'foo'}], 'retries': 1}
@@ -680,13 +687,16 @@ def test_run_configuration_retries_repositories_in_order():
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module).should_receive('Log_prefix').and_return(flexmock())
flexmock(module).should_receive('run_actions').and_raise(OSError).times(2)
expected_results = [flexmock(), flexmock()]
expected_results = [flexmock(), flexmock(), flexmock()]
flexmock(module).should_receive('log_error_records').with_args(
'Error running actions for repository', OSError
).and_return(expected_results[:1]).ordered()
flexmock(module).should_receive('log_error_records').with_args(
'Error running actions for repository', OSError
).and_return(expected_results[1:]).ordered()
).and_return(expected_results[1:2]).ordered()
flexmock(module).should_receive('log_error_records').with_args(
'Error running configuration',
).and_return(expected_results[2:]).ordered()
flexmock(module.command).should_receive('filter_hooks')
flexmock(module.command).should_receive('execute_hooks')
config = {'repositories': [{'path': 'foo'}, {'path': 'bar'}]}
@@ -728,6 +738,10 @@ def test_run_configuration_retries_round_robin():
flexmock(module).should_receive('log_error_records').with_args(
'Error running actions for repository', OSError
).and_return(bar_error_logs).ordered()
config_error_logs = [flexmock()]
flexmock(module).should_receive('log_error_records').with_args(
'Error running configuration',
).and_return(config_error_logs).ordered()
flexmock(module.command).should_receive('filter_hooks')
flexmock(module.command).should_receive('execute_hooks')
config = {
@@ -741,7 +755,7 @@ def test_run_configuration_retries_round_robin():
results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
assert results == foo_error_logs + bar_error_logs
assert results == foo_error_logs + bar_error_logs + config_error_logs
def test_run_configuration_with_one_retry():
@@ -766,10 +780,13 @@ def test_run_configuration_with_one_retry():
levelno=logging.WARNING,
log_command_error_output=True,
).and_return(flexmock()).ordered()
error_logs = [flexmock()]
error_logs = [flexmock(), flexmock()]
flexmock(module).should_receive('log_error_records').with_args(
'Error running actions for repository', OSError
).and_return(error_logs).ordered()
).and_return(error_logs[:1]).ordered()
flexmock(module).should_receive('log_error_records').with_args(
'Error running configuration',
).and_return(error_logs[1:]).ordered()
flexmock(module.command).should_receive('filter_hooks')
flexmock(module.command).should_receive('execute_hooks')
config = {
@@ -818,10 +835,13 @@ def test_run_configuration_with_retry_wait_does_backoff_after_each_retry():
).and_return([flexmock()]).ordered()
flexmock(time).should_receive('sleep').with_args(30).and_return().ordered()
error_logs = [flexmock()]
error_logs = [flexmock(), flexmock()]
flexmock(module).should_receive('log_error_records').with_args(
'Error running actions for repository', OSError
).and_return(error_logs).ordered()
).and_return(error_logs[:1]).ordered()
flexmock(module).should_receive('log_error_records').with_args(
'Error running configuration',
).and_return(error_logs[1:]).ordered()
flexmock(module.command).should_receive('filter_hooks')
flexmock(module.command).should_receive('execute_hooks')
config = {
@@ -867,10 +887,13 @@ def test_run_configuration_with_multiple_repositories_retries_with_timeout():
# Sleep before retrying bar (and failing)
flexmock(time).should_receive('sleep').with_args(10).and_return().ordered()
error_logs = [flexmock()]
error_logs = [flexmock(), flexmock()]
flexmock(module).should_receive('log_error_records').with_args(
'Error running actions for repository', OSError
).and_return(error_logs).ordered()
).and_return(error_logs[:1]).ordered()
flexmock(module).should_receive('log_error_records').with_args(
'Error running configuration',
).and_return(error_logs[1:]).ordered()
flexmock(module.command).should_receive('filter_hooks')
flexmock(module.command).should_receive('execute_hooks')
config = {
@@ -1909,7 +1932,12 @@ def test_collect_highlander_action_summary_logs_error_on_run_validate_failure():
def test_collect_configuration_run_summary_logs_info_for_success():
flexmock(module.validate).should_receive('guard_configuration_contains_repository')
flexmock(module.command).should_receive('filter_hooks')
flexmock(module.command).should_receive('filter_hooks').with_args(
object, before='everything', action_names=object
)
flexmock(module.command).should_receive('filter_hooks').with_args(
object, after='everything', action_names=object, state_names=['finish']
)
flexmock(module.command).should_receive('execute_hooks')
flexmock(module).should_receive('Log_prefix').and_return(flexmock())
flexmock(module).should_receive('run_configuration').and_return([])
@@ -1928,9 +1956,17 @@ def test_collect_configuration_run_summary_logs_info_for_success():
def test_collect_configuration_run_summary_executes_hooks_for_create():
before_everything_hook = {'before': 'everything', 'run': ['echo hi']}
after_everything_hook = {'after': 'everything', 'run': ['echo hi']}
command_hooks = (before_everything_hook, after_everything_hook)
flexmock(module.validate).should_receive('guard_configuration_contains_repository')
flexmock(module.command).should_receive('filter_hooks')
flexmock(module.command).should_receive('execute_hooks')
flexmock(module.command).should_receive('filter_hooks').with_args(
command_hooks, before='everything', action_names=object
).and_return([before_everything_hook])
flexmock(module.command).should_receive('filter_hooks').with_args(
command_hooks, after='everything', action_names=object, state_names=['finish']
).and_return([after_everything_hook])
flexmock(module.command).should_receive('execute_hooks').twice()
flexmock(module).should_receive('Log_prefix').and_return(flexmock())
flexmock(module).should_receive('run_configuration').and_return([])
arguments = {
@@ -1940,7 +1976,7 @@ def test_collect_configuration_run_summary_executes_hooks_for_create():
logs = tuple(
module.collect_configuration_run_summary_logs(
{'test.yaml': {}},
{'test.yaml': {'commands': command_hooks}},
config_paths=['/tmp/test.yaml'],
arguments=arguments,
log_file_path=None,
@@ -1950,9 +1986,51 @@ def test_collect_configuration_run_summary_executes_hooks_for_create():
assert {log.levelno for log in logs} == {logging.INFO}
def test_collect_configuration_run_summary_deduplicates_everything_hooks_across_config_files():
before_everything_hook = {'before': 'everything', 'run': ['echo hi']}
after_everything_hook = {'after': 'everything', 'run': ['echo hi']}
command_hooks = (before_everything_hook, after_everything_hook)
flexmock(module.validate).should_receive('guard_configuration_contains_repository')
flexmock(module.command).should_receive('filter_hooks').with_args(
command_hooks, before='everything', action_names=object
).and_return([before_everything_hook]).once()
flexmock(module.command).should_receive('filter_hooks').with_args(
(after_everything_hook,), before='everything', action_names=object
).and_return([]).once()
flexmock(module.command).should_receive('filter_hooks').with_args(
command_hooks, after='everything', action_names=object, state_names=['finish']
).and_return([after_everything_hook]).once()
flexmock(module.command).should_receive('filter_hooks').with_args(
(before_everything_hook,), after='everything', action_names=object, state_names=['finish']
).and_return([]).once()
flexmock(module.command).should_receive('execute_hooks').twice()
flexmock(module).should_receive('Log_prefix').and_return(flexmock())
flexmock(module).should_receive('run_configuration').and_return([])
arguments = {
'create': flexmock(),
'global': flexmock(monitoring_verbosity=1, dry_run=False),
}
logs = tuple(
module.collect_configuration_run_summary_logs(
{'test.yaml': {'commands': command_hooks}, 'other.yaml': {'commands': command_hooks}},
config_paths=['/tmp/test.yaml', '/tmp/other.yaml'],
arguments=arguments,
log_file_path=None,
)
)
assert {log.levelno for log in logs} == {logging.INFO}
def test_collect_configuration_run_summary_logs_info_for_success_with_extract():
flexmock(module.validate).should_receive('guard_configuration_contains_repository')
flexmock(module.command).should_receive('filter_hooks')
flexmock(module.command).should_receive('filter_hooks').with_args(
object, before='everything', action_names=object
)
flexmock(module.command).should_receive('filter_hooks').with_args(
object, after='everything', action_names=object, state_names=['finish']
)
flexmock(module.command).should_receive('execute_hooks')
flexmock(module).should_receive('Log_prefix').and_return(flexmock())
flexmock(module).should_receive('run_configuration').and_return([])
@@ -1995,7 +2073,12 @@ def test_collect_configuration_run_summary_logs_extract_with_repository_error():
def test_collect_configuration_run_summary_logs_info_for_success_with_mount():
flexmock(module.validate).should_receive('guard_configuration_contains_repository')
flexmock(module.command).should_receive('filter_hooks')
flexmock(module.command).should_receive('filter_hooks').with_args(
object, before='everything', action_names=object
)
flexmock(module.command).should_receive('filter_hooks').with_args(
object, after='everything', action_names=object, state_names=['finish']
)
flexmock(module.command).should_receive('execute_hooks')
flexmock(module).should_receive('Log_prefix').and_return(flexmock())
flexmock(module).should_receive('run_configuration').and_return([])
@@ -2041,7 +2124,12 @@ def test_collect_configuration_run_summary_logs_mount_with_repository_error():
def test_collect_configuration_run_summary_logs_missing_configs_error():
flexmock(module.validate).should_receive('guard_configuration_contains_repository')
flexmock(module.command).should_receive('filter_hooks')
flexmock(module.command).should_receive('filter_hooks').with_args(
object, before='everything', action_names=object
)
flexmock(module.command).should_receive('filter_hooks').with_args(
object, after='everything', action_names=object, state_names=['fail']
)
flexmock(module.command).should_receive('execute_hooks')
arguments = {'global': flexmock(config_paths=[])}
expected_logs = (flexmock(),)
@@ -2056,9 +2144,17 @@ def test_collect_configuration_run_summary_logs_missing_configs_error():
assert logs == expected_logs
def test_collect_configuration_run_summary_logs_pre_hook_error():
def test_collect_configuration_run_summary_logs_before_hook_error():
before_everything_hook = {'before': 'everything', 'run': ['echo hi']}
after_everything_hook = {'after': 'everything', 'run': ['echo hi']}
command_hooks = (before_everything_hook, after_everything_hook)
flexmock(module.validate).should_receive('guard_configuration_contains_repository')
flexmock(module.command).should_receive('filter_hooks')
flexmock(module.command).should_receive('filter_hooks').with_args(
object, before='everything', action_names=object
).and_return([before_everything_hook])
flexmock(module.command).should_receive('filter_hooks').with_args(
object, after='everything', action_names=object, state_names=['fail']
).and_return([after_everything_hook])
flexmock(module.command).should_receive('execute_hooks').and_raise(ValueError)
expected_logs = (flexmock(),)
flexmock(module).should_receive('log_error_records').and_return(expected_logs)
@@ -2069,7 +2165,7 @@ def test_collect_configuration_run_summary_logs_pre_hook_error():
logs = tuple(
module.collect_configuration_run_summary_logs(
{'test.yaml': {}},
{'test.yaml': {'commands': command_hooks}},
config_paths=['/tmp/test.yaml'],
arguments=arguments,
log_file_path=None,
@@ -2079,9 +2175,17 @@ def test_collect_configuration_run_summary_logs_pre_hook_error():
assert logs == expected_logs
def test_collect_configuration_run_summary_logs_post_hook_error():
def test_collect_configuration_run_summary_logs_after_hook_error():
before_everything_hook = {'before': 'everything', 'run': ['echo hi']}
after_everything_hook = {'after': 'everything', 'run': ['echo hi']}
command_hooks = (before_everything_hook, after_everything_hook)
flexmock(module.validate).should_receive('guard_configuration_contains_repository')
flexmock(module.command).should_receive('filter_hooks')
flexmock(module.command).should_receive('filter_hooks').with_args(
object, before='everything', action_names=object
).and_return([before_everything_hook])
flexmock(module.command).should_receive('filter_hooks').with_args(
object, after='everything', action_names=object, state_names=['finish']
).and_return([after_everything_hook])
flexmock(module.command).should_receive('execute_hooks').and_return(None).and_raise(ValueError)
flexmock(module).should_receive('Log_prefix').and_return(flexmock())
flexmock(module).should_receive('run_configuration').and_return([])
@@ -2094,7 +2198,7 @@ def test_collect_configuration_run_summary_logs_post_hook_error():
logs = tuple(
module.collect_configuration_run_summary_logs(
{'test.yaml': {}},
{'test.yaml': {'commands': command_hooks}},
config_paths=['/tmp/test.yaml'],
arguments=arguments,
log_file_path=None,
@@ -2129,7 +2233,12 @@ def test_collect_configuration_run_summary_logs_for_list_with_archive_and_reposi
def test_collect_configuration_run_summary_logs_info_for_success_with_list():
flexmock(module.validate).should_receive('guard_configuration_contains_repository')
flexmock(module.command).should_receive('filter_hooks')
flexmock(module.command).should_receive('filter_hooks').with_args(
object, before='everything', action_names=object
)
flexmock(module.command).should_receive('filter_hooks').with_args(
object, after='everything', action_names=object, state_names=['finish']
)
flexmock(module.command).should_receive('execute_hooks')
flexmock(module).should_receive('Log_prefix').and_return(flexmock())
flexmock(module).should_receive('run_configuration').and_return([])
@@ -2152,7 +2261,12 @@ def test_collect_configuration_run_summary_logs_info_for_success_with_list():
def test_collect_configuration_run_summary_logs_run_configuration_error_logs():
flexmock(module.validate).should_receive('guard_configuration_contains_repository')
flexmock(module.command).should_receive('filter_hooks')
flexmock(module.command).should_receive('filter_hooks').with_args(
object, before='everything', action_names=object
)
flexmock(module.command).should_receive('filter_hooks').with_args(
object, after='everything', action_names=object, state_names=['fail']
)
flexmock(module.command).should_receive('execute_hooks')
flexmock(module).should_receive('Log_prefix').and_return(flexmock())
flexmock(module).should_receive('run_configuration').and_return(
@@ -2175,7 +2289,12 @@ def test_collect_configuration_run_summary_logs_run_configuration_error_logs():
def test_collect_configuration_run_summary_logs_run_umount_error():
flexmock(module.validate).should_receive('guard_configuration_contains_repository')
flexmock(module.command).should_receive('filter_hooks')
flexmock(module.command).should_receive('filter_hooks').with_args(
object, before='everything', action_names=object
)
flexmock(module.command).should_receive('filter_hooks').with_args(
object, after='everything', action_names=object, state_names=['fail']
)
flexmock(module.command).should_receive('execute_hooks')
flexmock(module).should_receive('Log_prefix').and_return(flexmock())
flexmock(module).should_receive('run_configuration').and_return([])
@@ -2202,7 +2321,12 @@ def test_collect_configuration_run_summary_logs_run_umount_error():
def test_collect_configuration_run_summary_logs_outputs_merged_json_results():
flexmock(module.validate).should_receive('guard_configuration_contains_repository')
flexmock(module.command).should_receive('filter_hooks')
flexmock(module.command).should_receive('filter_hooks').with_args(
object, before='everything', action_names=object
)
flexmock(module.command).should_receive('filter_hooks').with_args(
object, after='everything', action_names=object, state_names=['finish']
)
flexmock(module.command).should_receive('execute_hooks')
flexmock(module).should_receive('Log_prefix').and_return(flexmock())
flexmock(module).should_receive('run_configuration').and_return(['foo', 'bar']).and_return(
+84 -16
View File
@@ -11,8 +11,16 @@ def test_interpolate_context_passes_through_command_without_variable():
assert module.interpolate_context('pre-backup', 'ls', {'foo': 'bar'}) == 'ls'
def test_interpolate_context_passes_through_command_with_unknown_variable():
def test_interpolate_context_warns_and_passes_through_command_with_unknown_variable():
command = 'ls {baz}' # noqa: FS003
flexmock(module.logger).should_receive('warning').once()
assert module.interpolate_context('pre-backup', command, {'foo': 'bar'}) == command
def test_interpolate_context_does_not_warn_and_passes_through_command_with_unknown_variable_matching_borg_placeholder():
command = 'ls {hostname}' # noqa: FS003
flexmock(module.logger).should_receive('warning').never()
assert module.interpolate_context('pre-backup', command, {'foo': 'bar'}) == command
@@ -167,6 +175,44 @@ def test_make_environment_with_pyinstaller_and_LD_LIBRARY_PATH_ORIG_copies_it_in
},
),
),
(
(
{
'before': 'action',
'states': ['finish'], # Not actually valid; only valid for "after".
'run': ['foo'],
},
{
'after': 'action',
'run': ['bar'],
},
{
'after': 'action',
'states': ['finish'],
'run': ['baz'],
},
{
'after': 'action',
'states': ['fail'],
'run': ['quux'],
},
),
{
'after': 'action',
'state_names': ['finish'],
},
(
{
'after': 'action',
'run': ['bar'],
},
{
'after': 'action',
'states': ['finish'],
'run': ['baz'],
},
),
),
),
)
def test_filter_hooks(hooks, filters, expected_hooks):
@@ -336,14 +382,13 @@ def test_before_after_hooks_calls_command_hooks():
flexmock(module).should_receive('filter_hooks').with_args(
commands,
before='action',
hook_name='myhook',
action_names=['create'],
).and_return(flexmock()).once()
flexmock(module).should_receive('filter_hooks').with_args(
commands,
after='action',
hook_name='myhook',
action_names=['create'],
state_names=['finish'],
).and_return(flexmock()).once()
flexmock(module).should_receive('execute_hooks').twice()
@@ -353,7 +398,6 @@ def test_before_after_hooks_calls_command_hooks():
umask=1234,
working_directory='/working',
dry_run=False,
hook_name='myhook',
action_names=['create'],
context1='stuff',
context2='such',
@@ -369,14 +413,13 @@ def test_before_after_hooks_with_before_error_runs_after_hook_and_raises():
flexmock(module).should_receive('filter_hooks').with_args(
commands,
before='action',
hook_name='myhook',
action_names=['create'],
).and_return(flexmock()).once()
flexmock(module).should_receive('filter_hooks').with_args(
commands,
after='action',
hook_name='myhook',
action_names=['create'],
state_names=['fail'],
).and_return(flexmock()).once()
flexmock(module).should_receive('execute_hooks').and_raise(OSError).and_return(None)
flexmock(module).should_receive('considered_soft_failure').and_return(False)
@@ -388,7 +431,6 @@ def test_before_after_hooks_with_before_error_runs_after_hook_and_raises():
umask=1234,
working_directory='/working',
dry_run=False,
hook_name='myhook',
action_names=['create'],
context1='stuff',
context2='such',
@@ -404,14 +446,13 @@ def test_before_after_hooks_with_before_soft_failure_raises():
flexmock(module).should_receive('filter_hooks').with_args(
commands,
before='action',
hook_name='myhook',
action_names=['create'],
).and_return(flexmock()).once()
flexmock(module).should_receive('filter_hooks').with_args(
commands,
after='action',
hook_name='myhook',
action_names=['create'],
state_names=['finish'],
).never()
flexmock(module).should_receive('execute_hooks').and_raise(OSError)
flexmock(module).should_receive('considered_soft_failure').and_return(True)
@@ -423,7 +464,6 @@ def test_before_after_hooks_with_before_soft_failure_raises():
umask=1234,
working_directory='/working',
dry_run=False,
hook_name='myhook',
action_names=['create'],
context1='stuff',
context2='such',
@@ -431,6 +471,38 @@ def test_before_after_hooks_with_before_soft_failure_raises():
pass
def test_before_after_hooks_with_wrapped_code_error_runs_after_hook_and_raises():
commands = [
{'before': 'repository', 'run': ['foo', 'bar']},
{'after': 'repository', 'run': ['baz']},
]
flexmock(module).should_receive('filter_hooks').with_args(
commands,
before='action',
action_names=['create'],
).and_return(flexmock()).once()
flexmock(module).should_receive('filter_hooks').with_args(
commands,
after='action',
action_names=['create'],
state_names=['fail'],
).and_return(flexmock()).once()
flexmock(module).should_receive('execute_hooks').twice()
with pytest.raises(ValueError):
with module.Before_after_hooks(
command_hooks=commands,
before_after='action',
umask=1234,
working_directory='/working',
dry_run=False,
action_names=['create'],
context1='stuff',
context2='such',
):
raise ValueError()
def test_before_after_hooks_with_after_error_raises():
commands = [
{'before': 'repository', 'run': ['foo', 'bar']},
@@ -439,14 +511,13 @@ def test_before_after_hooks_with_after_error_raises():
flexmock(module).should_receive('filter_hooks').with_args(
commands,
before='action',
hook_name='myhook',
action_names=['create'],
).and_return(flexmock()).once()
flexmock(module).should_receive('filter_hooks').with_args(
commands,
after='action',
hook_name='myhook',
action_names=['create'],
state_names=['finish'],
).and_return(flexmock()).once()
flexmock(module).should_receive('execute_hooks').and_return(None).and_raise(OSError)
flexmock(module).should_receive('considered_soft_failure').and_return(False)
@@ -458,7 +529,6 @@ def test_before_after_hooks_with_after_error_raises():
umask=1234,
working_directory='/working',
dry_run=False,
hook_name='myhook',
action_names=['create'],
context1='stuff',
context2='such',
@@ -474,14 +544,13 @@ def test_before_after_hooks_with_after_soft_failure_raises():
flexmock(module).should_receive('filter_hooks').with_args(
commands,
before='action',
hook_name='myhook',
action_names=['create'],
).and_return(flexmock()).once()
flexmock(module).should_receive('filter_hooks').with_args(
commands,
after='action',
hook_name='myhook',
action_names=['create'],
state_names=['finish'],
).and_return(flexmock()).once()
flexmock(module).should_receive('execute_hooks').and_return(None).and_raise(OSError)
flexmock(module).should_receive('considered_soft_failure').and_return(True)
@@ -493,7 +562,6 @@ def test_before_after_hooks_with_after_soft_failure_raises():
umask=1234,
working_directory='/working',
dry_run=False,
hook_name='myhook',
action_names=['create'],
context1='stuff',
context2='such',