2
0
Dan Helfman 4 сар өмнө
parent
commit
6a96a78cf1
31 өөрчлөгдсөн 795 нэмэгдсэн , 592 устгасан
  1. 399 430
      borgmatic/commands/borgmatic.py
  2. 11 4
      borgmatic/config/generate.py
  3. 1 0
      borgmatic/config/normalize.py
  4. 5 3
      borgmatic/config/schema.yaml
  5. 24 8
      borgmatic/hooks/command.py
  6. 1 1
      borgmatic/hooks/data_source/bootstrap.py
  7. 1 1
      borgmatic/hooks/data_source/btrfs.py
  8. 1 1
      borgmatic/hooks/data_source/lvm.py
  9. 1 1
      borgmatic/hooks/data_source/mariadb.py
  10. 1 1
      borgmatic/hooks/data_source/mongodb.py
  11. 1 1
      borgmatic/hooks/data_source/mysql.py
  12. 1 1
      borgmatic/hooks/data_source/postgresql.py
  13. 1 1
      borgmatic/hooks/data_source/sqlite.py
  14. 1 1
      borgmatic/hooks/data_source/zfs.py
  15. 0 10
      tests/unit/actions/test_check.py
  16. 0 4
      tests/unit/actions/test_compact.py
  17. 0 7
      tests/unit/actions/test_create.py
  18. 0 2
      tests/unit/actions/test_extract.py
  19. 0 4
      tests/unit/actions/test_prune.py
  20. 134 69
      tests/unit/commands/test_borgmatic.py
  21. 0 4
      tests/unit/config/test_generate.py
  22. 7 0
      tests/unit/hooks/data_source/test_bootstrap.py
  23. 18 0
      tests/unit/hooks/data_source/test_btrfs.py
  24. 21 0
      tests/unit/hooks/data_source/test_lvm.py
  25. 18 0
      tests/unit/hooks/data_source/test_mariadb.py
  26. 21 0
      tests/unit/hooks/data_source/test_mongodb.py
  27. 18 0
      tests/unit/hooks/data_source/test_mysql.py
  28. 42 0
      tests/unit/hooks/data_source/test_postgresql.py
  29. 18 0
      tests/unit/hooks/data_source/test_sqlite.py
  30. 15 0
      tests/unit/hooks/data_source/test_zfs.py
  31. 34 38
      tests/unit/hooks/test_command.py

+ 399 - 430
borgmatic/commands/borgmatic.py

@@ -96,135 +96,39 @@ def run_configuration(config_filename, config, config_paths, arguments):
             f"Skipping {'/'.join(skip_actions)} action{'s' if len(skip_actions) > 1 else ''} due to configured skip_actions"
         )
 
-    command.execute_hooks(
-        command.filter_hooks(
-            config.get('commands'), before='configuration', action_names=arguments.keys()
-        ),
-        config.get('umask'),
-        global_arguments.dry_run,
-        configuration_filename=config_filename,
-    )
-
-    try:
-        local_borg_version = borg_version.local_borg_version(config, local_path)
-        logger.debug(f'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
-
-    try:
-        if monitoring_hooks_are_activated:
-            dispatch.call_hooks(
-                'initialize_monitor',
-                config,
-                dispatch.Hook_type.MONITORING,
-                config_filename,
-                monitoring_log_level,
-                global_arguments.dry_run,
-            )
-
-            dispatch.call_hooks(
-                'ping_monitor',
-                config,
-                dispatch.Hook_type.MONITORING,
-                config_filename,
-                monitor.State.START,
-                monitoring_log_level,
-                global_arguments.dry_run,
+    with borgmatic.hooks.command.Before_after_hooks(
+        command_hooks=config.get('commands'),
+        before_after='configuration',
+        umask=config.get('umask'),
+        dry_run=global_arguments.dry_run,
+        action_names=arguments.keys(),
+    ):
+        try:
+            local_borg_version = borg_version.local_borg_version(config, local_path)
+            logger.debug(f'Borg {local_borg_version}')
+        except (OSError, CalledProcessError, ValueError) as error:
+            yield from log_error_records(
+                f'{config_filename}: Error getting local Borg version', error
             )
-    except (OSError, CalledProcessError) as error:
-        if command.considered_soft_failure(error):
             return
 
-        encountered_error = error
-        yield from log_error_records(f'{config_filename}: Error pinging monitor', error)
-
-    if not encountered_error:
-        repo_queue = Queue()
-        for repo in config['repositories']:
-            repo_queue.put(
-                (repo, 0),
-            )
-
-        while not repo_queue.empty():
-            repository, retry_num = repo_queue.get()
-
-            with Log_prefix(repository.get('label', repository['path'])):
-                logger.debug('Running actions for repository')
-                timeout = retry_num * retry_wait
-                if timeout:
-                    logger.warning(f'Sleeping {timeout}s before next retry')
-                    time.sleep(timeout)
-                try:
-                    yield from run_actions(
-                        arguments=arguments,
-                        config_filename=config_filename,
-                        config=config,
-                        config_paths=config_paths,
-                        local_path=local_path,
-                        remote_path=remote_path,
-                        local_borg_version=local_borg_version,
-                        repository=repository,
-                    )
-                except (OSError, CalledProcessError, ValueError) as error:
-                    if retry_num < retries:
-                        repo_queue.put(
-                            (repository, retry_num + 1),
-                        )
-                        tuple(  # Consume the generator so as to trigger logging.
-                            log_error_records(
-                                'Error running actions for repository',
-                                error,
-                                levelno=logging.WARNING,
-                                log_command_error_output=True,
-                            )
-                        )
-                        logger.warning(f'Retrying... attempt {retry_num + 1}/{retries}')
-                        continue
-
-                    if command.considered_soft_failure(error):
-                        continue
-
-                    yield from log_error_records(
-                        'Error running actions for repository',
-                        error,
-                    )
-                    encountered_error = error
-                    error_repository = repository['path']
-
-    try:
-        if monitoring_hooks_are_activated:
-            # Send logs irrespective of error.
-            dispatch.call_hooks(
-                'ping_monitor',
-                config,
-                dispatch.Hook_type.MONITORING,
-                config_filename,
-                monitor.State.LOG,
-                monitoring_log_level,
-                global_arguments.dry_run,
-            )
-    except (OSError, CalledProcessError) as error:
-        if not command.considered_soft_failure(error):
-            encountered_error = error
-            yield from log_error_records('Error pinging monitor', error)
-
-    if not encountered_error:
         try:
             if monitoring_hooks_are_activated:
                 dispatch.call_hooks(
-                    'ping_monitor',
+                    'initialize_monitor',
                     config,
                     dispatch.Hook_type.MONITORING,
                     config_filename,
-                    monitor.State.FINISH,
                     monitoring_log_level,
                     global_arguments.dry_run,
                 )
+
                 dispatch.call_hooks(
-                    'destroy_monitor',
+                    'ping_monitor',
                     config,
                     dispatch.Hook_type.MONITORING,
+                    config_filename,
+                    monitor.State.START,
                     monitoring_log_level,
                     global_arguments.dry_run,
                 )
@@ -235,51 +139,138 @@ def run_configuration(config_filename, config, config_paths, arguments):
             encountered_error = error
             yield from log_error_records(f'{config_filename}: Error pinging monitor', error)
 
-    if encountered_error:
-        command.execute_hooks(
-            command.filter_hooks(
-                config.get('commands'), after='error', action_names=arguments.keys()
-            ),
-            config.get('umask'),
-            global_arguments.dry_run,
-            configuration_filename=config_filename,
-            repository=error_repository,
-            error=encountered_error,
-            output=getattr(encountered_error, 'output', ''),
-        )
+        if not encountered_error:
+            repo_queue = Queue()
+            for repo in config['repositories']:
+                repo_queue.put(
+                    (repo, 0),
+                )
 
-    command.execute_hooks(
-        command.filter_hooks(
-            config.get('commands'), after='configuration', action_names=arguments.keys()
-        ),
-        config.get('umask'),
-        global_arguments.dry_run,
-        configuration_filename=config_filename,
-    )
+            while not repo_queue.empty():
+                repository, retry_num = repo_queue.get()
+
+                with Log_prefix(repository.get('label', repository['path'])):
+                    logger.debug('Running actions for repository')
+                    timeout = retry_num * retry_wait
+                    if timeout:
+                        logger.warning(f'Sleeping {timeout}s before next retry')
+                        time.sleep(timeout)
+                    try:
+                        yield from run_actions(
+                            arguments=arguments,
+                            config_filename=config_filename,
+                            config=config,
+                            config_paths=config_paths,
+                            local_path=local_path,
+                            remote_path=remote_path,
+                            local_borg_version=local_borg_version,
+                            repository=repository,
+                        )
+                    except (OSError, CalledProcessError, ValueError) as error:
+                        if retry_num < retries:
+                            repo_queue.put(
+                                (repository, retry_num + 1),
+                            )
+                            tuple(  # Consume the generator so as to trigger logging.
+                                log_error_records(
+                                    'Error running actions for repository',
+                                    error,
+                                    levelno=logging.WARNING,
+                                    log_command_error_output=True,
+                                )
+                            )
+                            logger.warning(f'Retrying... attempt {retry_num + 1}/{retries}')
+                            continue
+
+                        if command.considered_soft_failure(error):
+                            continue
+
+                        yield from log_error_records(
+                            'Error running actions for repository',
+                            error,
+                        )
+                        encountered_error = error
+                        error_repository = repository['path']
 
-    if encountered_error and using_primary_action:
         try:
-            dispatch.call_hooks(
-                'ping_monitor',
-                config,
-                dispatch.Hook_type.MONITORING,
-                config_filename,
-                monitor.State.FAIL,
-                monitoring_log_level,
-                global_arguments.dry_run,
-            )
-            dispatch.call_hooks(
-                'destroy_monitor',
-                config,
-                dispatch.Hook_type.MONITORING,
-                monitoring_log_level,
-                global_arguments.dry_run,
-            )
+            if monitoring_hooks_are_activated:
+                # Send logs irrespective of error.
+                dispatch.call_hooks(
+                    'ping_monitor',
+                    config,
+                    dispatch.Hook_type.MONITORING,
+                    config_filename,
+                    monitor.State.LOG,
+                    monitoring_log_level,
+                    global_arguments.dry_run,
+                )
         except (OSError, CalledProcessError) as error:
-            if command.considered_soft_failure(error):
-                return
+            if not command.considered_soft_failure(error):
+                encountered_error = error
+                yield from log_error_records('Error pinging monitor', error)
 
-            yield from log_error_records(f'{config_filename}: Error running on-error hook', error)
+        if not encountered_error:
+            try:
+                if monitoring_hooks_are_activated:
+                    dispatch.call_hooks(
+                        'ping_monitor',
+                        config,
+                        dispatch.Hook_type.MONITORING,
+                        config_filename,
+                        monitor.State.FINISH,
+                        monitoring_log_level,
+                        global_arguments.dry_run,
+                    )
+                    dispatch.call_hooks(
+                        'destroy_monitor',
+                        config,
+                        dispatch.Hook_type.MONITORING,
+                        monitoring_log_level,
+                        global_arguments.dry_run,
+                    )
+            except (OSError, CalledProcessError) as error:
+                if command.considered_soft_failure(error):
+                    return
+
+                encountered_error = error
+                yield from log_error_records(f'{config_filename}: Error pinging monitor', error)
+
+        if encountered_error:
+            try:
+                command.execute_hooks(
+                    command.filter_hooks(
+                        config.get('commands'), after='error', action_names=arguments.keys()
+                    ),
+                    config.get('umask'),
+                    global_arguments.dry_run,
+                    configuration_filename=config_filename,
+                    repository=error_repository,
+                    error=encountered_error,
+                    output=getattr(encountered_error, 'output', ''),
+                )
+                dispatch.call_hooks(
+                    'ping_monitor',
+                    config,
+                    dispatch.Hook_type.MONITORING,
+                    config_filename,
+                    monitor.State.FAIL,
+                    monitoring_log_level,
+                    global_arguments.dry_run,
+                )
+                dispatch.call_hooks(
+                    'destroy_monitor',
+                    config,
+                    dispatch.Hook_type.MONITORING,
+                    monitoring_log_level,
+                    global_arguments.dry_run,
+                )
+            except (OSError, CalledProcessError) as error:
+                if command.considered_soft_failure(error):
+                    return
+
+                yield from log_error_records(
+                    f'{config_filename}: Error running after error hook', error
+                )
 
 
 def run_actions(
@@ -319,256 +310,236 @@ def run_actions(
     }
     skip_actions = set(get_skip_actions(config, arguments))
 
-    command.execute_hooks(
-        command.filter_hooks(
-            config.get('commands'), before='repository', action_names=arguments.keys()
-        ),
-        config.get('umask'),
-        global_arguments.dry_run,
-        **hook_context,
-    )
-
-    for action_name, action_arguments in arguments.items():
-        if action_name == 'global':
-            continue
-
-        command.execute_hooks(
-            command.filter_hooks(
-                config.get('commands'), before='action', action_names=arguments.keys()
-            ),
-            config.get('umask'),
-            global_arguments.dry_run,
-            **hook_context,
-        )
-
-        if action_name == 'repo-create' and action_name not in skip_actions:
-            borgmatic.actions.repo_create.run_repo_create(
-                repository,
-                config,
-                local_borg_version,
-                action_arguments,
-                global_arguments,
-                local_path,
-                remote_path,
-            )
-        elif action_name == 'transfer' and action_name not in skip_actions:
-            borgmatic.actions.transfer.run_transfer(
-                repository,
-                config,
-                local_borg_version,
-                action_arguments,
-                global_arguments,
-                local_path,
-                remote_path,
-            )
-        elif action_name == 'create' and action_name not in skip_actions:
-            yield from borgmatic.actions.create.run_create(
-                config_filename,
-                repository,
-                config,
-                config_paths,
-                local_borg_version,
-                action_arguments,
-                global_arguments,
-                dry_run_label,
-                local_path,
-                remote_path,
-            )
-        elif action_name == 'prune' and action_name not in skip_actions:
-            borgmatic.actions.prune.run_prune(
-                config_filename,
-                repository,
-                config,
-                local_borg_version,
-                action_arguments,
-                global_arguments,
-                dry_run_label,
-                local_path,
-                remote_path,
-            )
-        elif action_name == 'compact' and action_name not in skip_actions:
-            borgmatic.actions.compact.run_compact(
-                config_filename,
-                repository,
-                config,
-                local_borg_version,
-                action_arguments,
-                global_arguments,
-                dry_run_label,
-                local_path,
-                remote_path,
-            )
-        elif action_name == 'check' and action_name not in skip_actions:
-            if checks.repository_enabled_for_checks(repository, config):
-                borgmatic.actions.check.run_check(
-                    config_filename,
-                    repository,
-                    config,
-                    local_borg_version,
-                    action_arguments,
-                    global_arguments,
-                    local_path,
-                    remote_path,
-                )
-        elif action_name == 'extract' and action_name not in skip_actions:
-            borgmatic.actions.extract.run_extract(
-                config_filename,
-                repository,
-                config,
-                local_borg_version,
-                action_arguments,
-                global_arguments,
-                local_path,
-                remote_path,
-            )
-        elif action_name == 'export-tar' and action_name not in skip_actions:
-            borgmatic.actions.export_tar.run_export_tar(
-                repository,
-                config,
-                local_borg_version,
-                action_arguments,
-                global_arguments,
-                local_path,
-                remote_path,
-            )
-        elif action_name == 'mount' and action_name not in skip_actions:
-            borgmatic.actions.mount.run_mount(
-                repository,
-                config,
-                local_borg_version,
-                action_arguments,
-                global_arguments,
-                local_path,
-                remote_path,
-            )
-        elif action_name == 'restore' and action_name not in skip_actions:
-            borgmatic.actions.restore.run_restore(
-                repository,
-                config,
-                local_borg_version,
-                action_arguments,
-                global_arguments,
-                local_path,
-                remote_path,
-            )
-        elif action_name == 'repo-list' and action_name not in skip_actions:
-            yield from borgmatic.actions.repo_list.run_repo_list(
-                repository,
-                config,
-                local_borg_version,
-                action_arguments,
-                global_arguments,
-                local_path,
-                remote_path,
-            )
-        elif action_name == 'list' and action_name not in skip_actions:
-            yield from borgmatic.actions.list.run_list(
-                repository,
-                config,
-                local_borg_version,
-                action_arguments,
-                global_arguments,
-                local_path,
-                remote_path,
-            )
-        elif action_name == 'repo-info' and action_name not in skip_actions:
-            yield from borgmatic.actions.repo_info.run_repo_info(
-                repository,
-                config,
-                local_borg_version,
-                action_arguments,
-                global_arguments,
-                local_path,
-                remote_path,
-            )
-        elif action_name == 'info' and action_name not in skip_actions:
-            yield from borgmatic.actions.info.run_info(
-                repository,
-                config,
-                local_borg_version,
-                action_arguments,
-                global_arguments,
-                local_path,
-                remote_path,
-            )
-        elif action_name == 'break-lock' and action_name not in skip_actions:
-            borgmatic.actions.break_lock.run_break_lock(
-                repository,
-                config,
-                local_borg_version,
-                action_arguments,
-                global_arguments,
-                local_path,
-                remote_path,
-            )
-        elif action_name == 'export' and action_name not in skip_actions:
-            borgmatic.actions.export_key.run_export_key(
-                repository,
-                config,
-                local_borg_version,
-                action_arguments,
-                global_arguments,
-                local_path,
-                remote_path,
-            )
-        elif action_name == 'change-passphrase' and action_name not in skip_actions:
-            borgmatic.actions.change_passphrase.run_change_passphrase(
-                repository,
-                config,
-                local_borg_version,
-                action_arguments,
-                global_arguments,
-                local_path,
-                remote_path,
-            )
-        elif action_name == 'delete' and action_name not in skip_actions:
-            borgmatic.actions.delete.run_delete(
-                repository,
-                config,
-                local_borg_version,
-                action_arguments,
-                global_arguments,
-                local_path,
-                remote_path,
-            )
-        elif action_name == 'repo-delete' and action_name not in skip_actions:
-            borgmatic.actions.repo_delete.run_repo_delete(
-                repository,
-                config,
-                local_borg_version,
-                action_arguments,
-                global_arguments,
-                local_path,
-                remote_path,
-            )
-        elif action_name == 'borg' and action_name not in skip_actions:
-            borgmatic.actions.borg.run_borg(
-                repository,
-                config,
-                local_borg_version,
-                action_arguments,
-                global_arguments,
-                local_path,
-                remote_path,
-            )
-
-        command.execute_hooks(
-            command.filter_hooks(
-                config.get('commands'), after='action', action_names=arguments.keys()
-            ),
-            config.get('umask'),
-            global_arguments.dry_run,
-            **hook_context,
-        )
-
-    command.execute_hooks(
-        command.filter_hooks(
-            config.get('commands'), after='repository', action_names=arguments.keys()
-        ),
-        config.get('umask'),
-        global_arguments.dry_run,
+    with borgmatic.hooks.command.Before_after_hooks(
+        command_hooks=config.get('commands'),
+        before_after='repository',
+        umask=config.get('umask'),
+        dry_run=global_arguments.dry_run,
+        action_names=arguments.keys(),
         **hook_context,
-    )
+    ):
+        for action_name, action_arguments in arguments.items():
+            if action_name == 'global':
+                continue
+
+            with borgmatic.hooks.command.Before_after_hooks(
+                command_hooks=config.get('commands'),
+                before_after='action',
+                umask=config.get('umask'),
+                dry_run=global_arguments.dry_run,
+                action_names=arguments.keys(),
+                **hook_context,
+            ):
+                if action_name == 'repo-create' and action_name not in skip_actions:
+                    borgmatic.actions.repo_create.run_repo_create(
+                        repository,
+                        config,
+                        local_borg_version,
+                        action_arguments,
+                        global_arguments,
+                        local_path,
+                        remote_path,
+                    )
+                elif action_name == 'transfer' and action_name not in skip_actions:
+                    borgmatic.actions.transfer.run_transfer(
+                        repository,
+                        config,
+                        local_borg_version,
+                        action_arguments,
+                        global_arguments,
+                        local_path,
+                        remote_path,
+                    )
+                elif action_name == 'create' and action_name not in skip_actions:
+                    yield from borgmatic.actions.create.run_create(
+                        config_filename,
+                        repository,
+                        config,
+                        config_paths,
+                        local_borg_version,
+                        action_arguments,
+                        global_arguments,
+                        dry_run_label,
+                        local_path,
+                        remote_path,
+                    )
+                elif action_name == 'prune' and action_name not in skip_actions:
+                    borgmatic.actions.prune.run_prune(
+                        config_filename,
+                        repository,
+                        config,
+                        local_borg_version,
+                        action_arguments,
+                        global_arguments,
+                        dry_run_label,
+                        local_path,
+                        remote_path,
+                    )
+                elif action_name == 'compact' and action_name not in skip_actions:
+                    borgmatic.actions.compact.run_compact(
+                        config_filename,
+                        repository,
+                        config,
+                        local_borg_version,
+                        action_arguments,
+                        global_arguments,
+                        dry_run_label,
+                        local_path,
+                        remote_path,
+                    )
+                elif action_name == 'check' and action_name not in skip_actions:
+                    if checks.repository_enabled_for_checks(repository, config):
+                        borgmatic.actions.check.run_check(
+                            config_filename,
+                            repository,
+                            config,
+                            local_borg_version,
+                            action_arguments,
+                            global_arguments,
+                            local_path,
+                            remote_path,
+                        )
+                elif action_name == 'extract' and action_name not in skip_actions:
+                    borgmatic.actions.extract.run_extract(
+                        config_filename,
+                        repository,
+                        config,
+                        local_borg_version,
+                        action_arguments,
+                        global_arguments,
+                        local_path,
+                        remote_path,
+                    )
+                elif action_name == 'export-tar' and action_name not in skip_actions:
+                    borgmatic.actions.export_tar.run_export_tar(
+                        repository,
+                        config,
+                        local_borg_version,
+                        action_arguments,
+                        global_arguments,
+                        local_path,
+                        remote_path,
+                    )
+                elif action_name == 'mount' and action_name not in skip_actions:
+                    borgmatic.actions.mount.run_mount(
+                        repository,
+                        config,
+                        local_borg_version,
+                        action_arguments,
+                        global_arguments,
+                        local_path,
+                        remote_path,
+                    )
+                elif action_name == 'restore' and action_name not in skip_actions:
+                    borgmatic.actions.restore.run_restore(
+                        repository,
+                        config,
+                        local_borg_version,
+                        action_arguments,
+                        global_arguments,
+                        local_path,
+                        remote_path,
+                    )
+                elif action_name == 'repo-list' and action_name not in skip_actions:
+                    yield from borgmatic.actions.repo_list.run_repo_list(
+                        repository,
+                        config,
+                        local_borg_version,
+                        action_arguments,
+                        global_arguments,
+                        local_path,
+                        remote_path,
+                    )
+                elif action_name == 'list' and action_name not in skip_actions:
+                    yield from borgmatic.actions.list.run_list(
+                        repository,
+                        config,
+                        local_borg_version,
+                        action_arguments,
+                        global_arguments,
+                        local_path,
+                        remote_path,
+                    )
+                elif action_name == 'repo-info' and action_name not in skip_actions:
+                    yield from borgmatic.actions.repo_info.run_repo_info(
+                        repository,
+                        config,
+                        local_borg_version,
+                        action_arguments,
+                        global_arguments,
+                        local_path,
+                        remote_path,
+                    )
+                elif action_name == 'info' and action_name not in skip_actions:
+                    yield from borgmatic.actions.info.run_info(
+                        repository,
+                        config,
+                        local_borg_version,
+                        action_arguments,
+                        global_arguments,
+                        local_path,
+                        remote_path,
+                    )
+                elif action_name == 'break-lock' and action_name not in skip_actions:
+                    borgmatic.actions.break_lock.run_break_lock(
+                        repository,
+                        config,
+                        local_borg_version,
+                        action_arguments,
+                        global_arguments,
+                        local_path,
+                        remote_path,
+                    )
+                elif action_name == 'export' and action_name not in skip_actions:
+                    borgmatic.actions.export_key.run_export_key(
+                        repository,
+                        config,
+                        local_borg_version,
+                        action_arguments,
+                        global_arguments,
+                        local_path,
+                        remote_path,
+                    )
+                elif action_name == 'change-passphrase' and action_name not in skip_actions:
+                    borgmatic.actions.change_passphrase.run_change_passphrase(
+                        repository,
+                        config,
+                        local_borg_version,
+                        action_arguments,
+                        global_arguments,
+                        local_path,
+                        remote_path,
+                    )
+                elif action_name == 'delete' and action_name not in skip_actions:
+                    borgmatic.actions.delete.run_delete(
+                        repository,
+                        config,
+                        local_borg_version,
+                        action_arguments,
+                        global_arguments,
+                        local_path,
+                        remote_path,
+                    )
+                elif action_name == 'repo-delete' and action_name not in skip_actions:
+                    borgmatic.actions.repo_delete.run_repo_delete(
+                        repository,
+                        config,
+                        local_borg_version,
+                        action_arguments,
+                        global_arguments,
+                        local_path,
+                        remote_path,
+                    )
+                elif action_name == 'borg' and action_name not in skip_actions:
+                    borgmatic.actions.borg.run_borg(
+                        repository,
+                        config,
+                        local_borg_version,
+                        action_arguments,
+                        global_arguments,
+                        local_path,
+                        remote_path,
+                    )
 
 
 def load_configurations(config_filenames, overrides=None, resolve_env=True):
@@ -848,20 +819,19 @@ def collect_configuration_run_summary_logs(configs, config_paths, arguments):
         )
         return
 
-    if 'create' in arguments:
-        try:
-            for config_filename, config in configs.items():
-                command.execute_hooks(
-                    command.filter_hooks(
-                        config.get('commands'), before='everything', action_names=arguments.keys()
-                    ),
-                    config.get('umask'),
-                    arguments['global'].dry_run,
-                    configuration_filename=config_filename,
-                )
-        except (CalledProcessError, ValueError, OSError) as error:
-            yield from log_error_records('Error running pre-everything hook', error)
-            return
+    try:
+        for config_filename, config in configs.items():
+            command.execute_hooks(
+                command.filter_hooks(
+                    config.get('commands'), before='everything', action_names=arguments.keys()
+                ),
+                config.get('umask'),
+                arguments['global'].dry_run,
+                configuration_filename=config_filename,
+            )
+    except (CalledProcessError, ValueError, OSError) as error:
+        yield from log_error_records('Error running pre-everything hook', error)
+        return
 
     # Execute the actions corresponding to each configuration file.
     json_results = []
@@ -901,19 +871,18 @@ def collect_configuration_run_summary_logs(configs, config_paths, arguments):
     if json_results:
         sys.stdout.write(json.dumps(json_results))
 
-    if 'create' in arguments:
-        try:
-            for config_filename, config in configs.items():
-                command.execute_hooks(
-                    command.filter_hooks(
-                        config.get('commands'), after='everything', action_names=arguments.keys()
-                    ),
-                    config.get('umask'),
-                    arguments['global'].dry_run,
-                    configuration_filename=config_filename,
-                )
-        except (CalledProcessError, ValueError, OSError) as error:
-            yield from log_error_records('Error running post-everything hook', error)
+    try:
+        for config_filename, config in configs.items():
+            command.execute_hooks(
+                command.filter_hooks(
+                    config.get('commands'), after='everything', action_names=arguments.keys()
+                ),
+                config.get('umask'),
+                arguments['global'].dry_run,
+                configuration_filename=config_filename,
+            )
+    except (CalledProcessError, ValueError, OSError) as error:
+        yield from log_error_records('Error running post-everything hook', error)
 
 
 def exit_with_help_link():  # pragma: no cover

+ 11 - 4
borgmatic/config/generate.py

@@ -43,10 +43,13 @@ def get_properties(schema):
     return schema['properties']
 
 
-def schema_to_sample_configuration(schema, source_config, level=0, parent_is_sequence=False):
+def schema_to_sample_configuration(schema, source_config=None, level=0, parent_is_sequence=False):
     '''
     Given a loaded configuration schema and a source configuration, generate and return sample
     config for the schema. Include comments for each option based on the schema "description".
+
+    If a source config is given, walk it alongside the given schema so that both can be taken into
+    account when commenting out particular options in add_comments_to_configuration_object().
     '''
     schema_type = schema.get('type')
     example = schema.get('example')
@@ -72,7 +75,7 @@ def schema_to_sample_configuration(schema, source_config, level=0, parent_is_seq
                 (
                     field_name,
                     schema_to_sample_configuration(
-                        sub_schema, source_config.get(field_name, {}), level + 1
+                        sub_schema, (source_config or {}).get(field_name, {}), level + 1
                     ),
                 )
                 for field_name, sub_schema in get_properties(schema).items()
@@ -204,7 +207,9 @@ DEFAULT_KEYS = {'source_directories', 'repositories', 'keep_daily'}
 COMMENTED_OUT_SENTINEL = 'COMMENT_OUT'
 
 
-def add_comments_to_configuration_object(config, schema, source_config, indent=0, skip_first=False):
+def add_comments_to_configuration_object(
+    config, schema, source_config=None, indent=0, skip_first=False
+):
     '''
     Using descriptions from a schema as a source, add those descriptions as comments to the given
     configuration dict, putting them before each field. Indent the comment the given number of
@@ -224,7 +229,9 @@ def add_comments_to_configuration_object(config, schema, source_config, indent=0
         # If this isn't a default key, add an indicator to the comment flagging it to be commented
         # out from the sample configuration. This sentinel is consumed by downstream processing that
         # does the actual commenting out.
-        if field_name not in DEFAULT_KEYS and field_name not in source_config:
+        if field_name not in DEFAULT_KEYS and (
+            source_config is None or field_name not in source_config
+        ):
             description = (
                 '\n'.join((description, COMMENTED_OUT_SENTINEL))
                 if description

+ 1 - 0
borgmatic/config/normalize.py

@@ -134,6 +134,7 @@ def normalize_commands(config_filename, config):
             config.setdefault('commands', []).append(
                 {
                     preposition: 'everything',
+                    'when': ['create'],
                     'run': commands,
                 }
             )

+ 5 - 3
borgmatic/config/schema.yaml

@@ -1015,7 +1015,7 @@ properties:
                               List of actions for which the commands will be
                               run. Defaults to running for all actions. Ignored
                               for "dump_data_sources", which by its nature only
-                              runs for particular actions.
+                              runs for "create".
                           example: [create, prune, compact, check]
                       run:
                           type: array
@@ -1091,8 +1091,10 @@ properties:
                                   - key
                                   - borg
                           description: |
-                              List of actions for which the commands will be run.
-                              Defaults to running for all actions.
+                              List of actions for which the commands will be
+                              run. Defaults to running for all actions. Ignored
+                              for "dump_data_sources", which by its nature only
+                              runs for "create".
                           example: [create, prune, compact, check]
                       run:
                           type: array

+ 24 - 8
borgmatic/hooks/command.py

@@ -138,9 +138,9 @@ class Before_after_hooks:
        with borgmatic.hooks.command.Before_after_hooks(
            command_hooks=config.get('commands'),
            before_after='do_stuff',
-           hook_name='myhook',
            umask=config.get('umask'),
            dry_run=dry_run,
+           hook_name='myhook',
        ):
             do()
             some()
@@ -150,17 +150,27 @@ class Before_after_hooks:
     and "after" command hooks execute after the wrapped code completes.
     '''
 
-    def __init__(self, command_hooks, before_after, hook_name, umask, dry_run, **context):
+    def __init__(
+        self,
+        command_hooks,
+        before_after,
+        umask,
+        dry_run,
+        hook_name=None,
+        action_names=None,
+        **context,
+    ):
         '''
-        Given a sequence of command hook configuration dicts, the before/after name, the name of the
-        calling hook, a umask to run commands with, a dry run flag, and any context for the executed
-        commands, save those data points for use below.
+        Given a sequence of command hook configuration dicts, the before/after name, a umask 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.
         '''
         self.command_hooks = command_hooks
         self.before_after = before_after
-        self.hook_name = hook_name
         self.umask = umask
         self.dry_run = dry_run
+        self.hook_name = hook_name
+        self.action_names = action_names
         self.context = context
 
     def __enter__(self):
@@ -169,7 +179,10 @@ class Before_after_hooks:
         '''
         execute_hooks(
             borgmatic.hooks.command.filter_hooks(
-                self.command_hooks, before=self.before_after, hook_name=self.hook_name
+                self.command_hooks,
+                before=self.before_after,
+                hook_name=self.hook_name,
+                action_names=self.action_names,
             ),
             self.umask,
             self.dry_run,
@@ -182,7 +195,10 @@ class Before_after_hooks:
         '''
         execute_hooks(
             borgmatic.hooks.command.filter_hooks(
-                self.command_hooks, after=self.before_after, hook_name=self.hook_name
+                self.command_hooks,
+                after=self.before_after,
+                hook_name=self.hook_name,
+                action_names=self.action_names,
             ),
             self.umask,
             self.dry_run,

+ 1 - 1
borgmatic/hooks/data_source/bootstrap.py

@@ -41,9 +41,9 @@ def dump_data_sources(
     with borgmatic.hooks.command.Before_after_hooks(
         command_hooks=config.get('commands'),
         before_after='dump_data_sources',
-        hook_name='bootstrap',
         umask=config.get('umask'),
         dry_run=dry_run,
+        hook_name='bootstrap',
     ):
         borgmatic_manifest_path = os.path.join(
             borgmatic_runtime_directory, 'bootstrap', 'manifest.json'

+ 1 - 1
borgmatic/hooks/data_source/btrfs.py

@@ -208,9 +208,9 @@ def dump_data_sources(
     with borgmatic.hooks.command.Before_after_hooks(
         command_hooks=config.get('commands'),
         before_after='dump_data_sources',
-        hook_name='btrfs',
         umask=config.get('umask'),
         dry_run=dry_run,
+        hook_name='btrfs',
     ):
         dry_run_label = ' (dry run; not actually snapshotting anything)' if dry_run else ''
         logger.info(f'Snapshotting Btrfs subvolumes{dry_run_label}')

+ 1 - 1
borgmatic/hooks/data_source/lvm.py

@@ -201,9 +201,9 @@ def dump_data_sources(
     with borgmatic.hooks.command.Before_after_hooks(
         command_hooks=config.get('commands'),
         function_name='dump_data_sources',
-        hook_name='lvm',
         umask=config.get('umask'),
         dry_run=dry_run,
+        hook_name='lvm',
     ):
         dry_run_label = ' (dry run; not actually snapshotting anything)' if dry_run else ''
         logger.info(f'Snapshotting LVM logical volumes{dry_run_label}')

+ 1 - 1
borgmatic/hooks/data_source/mariadb.py

@@ -246,9 +246,9 @@ def dump_data_sources(
     with borgmatic.hooks.command.Before_after_hooks(
         command_hooks=config.get('commands'),
         before_after='dump_data_sources',
-        hook_name='mariadb',
         umask=config.get('umask'),
         dry_run=dry_run,
+        hook_name='mariadb',
     ):
         dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
         processes = []

+ 1 - 1
borgmatic/hooks/data_source/mongodb.py

@@ -52,9 +52,9 @@ def dump_data_sources(
     with borgmatic.hooks.command.Before_after_hooks(
         command_hooks=config.get('commands'),
         before_after='dump_data_sources',
-        hook_name='mongodb',
         umask=config.get('umask'),
         dry_run=dry_run,
+        hook_name='mongodb',
     ):
         dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
 

+ 1 - 1
borgmatic/hooks/data_source/mysql.py

@@ -173,9 +173,9 @@ def dump_data_sources(
     with borgmatic.hooks.command.Before_after_hooks(
         command_hooks=config.get('commands'),
         before_after='dump_data_sources',
-        hook_name='mysql',
         umask=config.get('umask'),
         dry_run=dry_run,
+        hook_name='mysql',
     ):
         dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
         processes = []

+ 1 - 1
borgmatic/hooks/data_source/postgresql.py

@@ -145,9 +145,9 @@ def dump_data_sources(
     with borgmatic.hooks.command.Before_after_hooks(
         command_hooks=config.get('commands'),
         before_after='dump_data_sources',
-        hook_name='postgresql',
         umask=config.get('umask'),
         dry_run=dry_run,
+        hook_name='postgresql',
     ):
         dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
         processes = []

+ 1 - 1
borgmatic/hooks/data_source/sqlite.py

@@ -51,9 +51,9 @@ def dump_data_sources(
     with borgmatic.hooks.command.Before_after_hooks(
         command_hooks=config.get('commands'),
         before_after='dump_data_sources',
-        hook_name='sqlite',
         umask=config.get('umask'),
         dry_run=dry_run,
+        hook_name='sqlite',
     ):
         dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
         processes = []

+ 1 - 1
borgmatic/hooks/data_source/zfs.py

@@ -247,9 +247,9 @@ def dump_data_sources(
     with borgmatic.hooks.command.Before_after_hooks(
         command_hooks=config.get('commands'),
         before_after='dump_data_sources',
-        hook_name='zfs',
         umask=config.get('umask'),
         dry_run=dry_run,
+        hook_name='zfs',
     ):
         dry_run_label = ' (dry run; not actually snapshotting anything)' if dry_run else ''
         logger.info(f'Snapshotting ZFS datasets{dry_run_label}')

+ 0 - 10
tests/unit/actions/test_check.py

@@ -1405,7 +1405,6 @@ def test_run_check_checks_archives_for_configured_repository():
     flexmock(module).should_receive('make_check_time_path')
     flexmock(module).should_receive('write_check_time')
     flexmock(module.borgmatic.borg.extract).should_receive('extract_last_archive_dry_run').never()
-    flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2)
     check_arguments = flexmock(
         repository=None,
         progress=flexmock(),
@@ -1419,7 +1418,6 @@ def test_run_check_checks_archives_for_configured_repository():
         config_filename='test.yaml',
         repository={'path': 'repo'},
         config={'repositories': ['repo']},
-        hook_context={},
         local_borg_version=None,
         check_arguments=check_arguments,
         global_arguments=global_arguments,
@@ -1441,7 +1439,6 @@ def test_run_check_runs_configured_extract_check():
     flexmock(module.borgmatic.borg.extract).should_receive('extract_last_archive_dry_run').once()
     flexmock(module).should_receive('make_check_time_path')
     flexmock(module).should_receive('write_check_time')
-    flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2)
     check_arguments = flexmock(
         repository=None,
         progress=flexmock(),
@@ -1455,7 +1452,6 @@ def test_run_check_runs_configured_extract_check():
         config_filename='test.yaml',
         repository={'path': 'repo'},
         config={'repositories': ['repo']},
-        hook_context={},
         local_borg_version=None,
         check_arguments=check_arguments,
         global_arguments=global_arguments,
@@ -1480,7 +1476,6 @@ def test_run_check_runs_configured_spot_check():
     flexmock(module.borgmatic.actions.check).should_receive('spot_check').once()
     flexmock(module).should_receive('make_check_time_path')
     flexmock(module).should_receive('write_check_time')
-    flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2)
     check_arguments = flexmock(
         repository=None,
         progress=flexmock(),
@@ -1494,7 +1489,6 @@ def test_run_check_runs_configured_spot_check():
         config_filename='test.yaml',
         repository={'path': 'repo'},
         config={'repositories': ['repo']},
-        hook_context={},
         local_borg_version=None,
         check_arguments=check_arguments,
         global_arguments=global_arguments,
@@ -1516,7 +1510,6 @@ def test_run_check_without_checks_runs_nothing_except_hooks():
     flexmock(module).should_receive('make_check_time_path')
     flexmock(module).should_receive('write_check_time').never()
     flexmock(module.borgmatic.borg.extract).should_receive('extract_last_archive_dry_run').never()
-    flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2)
     check_arguments = flexmock(
         repository=None,
         progress=flexmock(),
@@ -1530,7 +1523,6 @@ def test_run_check_without_checks_runs_nothing_except_hooks():
         config_filename='test.yaml',
         repository={'path': 'repo'},
         config={'repositories': ['repo']},
-        hook_context={},
         local_borg_version=None,
         check_arguments=check_arguments,
         global_arguments=global_arguments,
@@ -1569,7 +1561,6 @@ def test_run_check_checks_archives_in_selected_repository():
         config_filename='test.yaml',
         repository={'path': 'repo'},
         config={'repositories': ['repo']},
-        hook_context={},
         local_borg_version=None,
         check_arguments=check_arguments,
         global_arguments=global_arguments,
@@ -1597,7 +1588,6 @@ def test_run_check_bails_if_repository_does_not_match():
         config_filename='test.yaml',
         repository={'path': 'repo'},
         config={'repositories': ['repo']},
-        hook_context={},
         local_borg_version=None,
         check_arguments=check_arguments,
         global_arguments=global_arguments,

+ 0 - 4
tests/unit/actions/test_compact.py

@@ -8,7 +8,6 @@ def test_compact_actions_calls_hooks_for_configured_repository():
     flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
     flexmock(module.borgmatic.config.validate).should_receive('repositories_match').never()
     flexmock(module.borgmatic.borg.compact).should_receive('compact_segments').once()
-    flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2)
     compact_arguments = flexmock(
         repository=None, progress=flexmock(), cleanup_commits=flexmock(), threshold=flexmock()
     )
@@ -18,7 +17,6 @@ def test_compact_actions_calls_hooks_for_configured_repository():
         config_filename='test.yaml',
         repository={'path': 'repo'},
         config={},
-        hook_context={},
         local_borg_version=None,
         compact_arguments=compact_arguments,
         global_arguments=global_arguments,
@@ -44,7 +42,6 @@ def test_compact_runs_with_selected_repository():
         config_filename='test.yaml',
         repository={'path': 'repo'},
         config={},
-        hook_context={},
         local_borg_version=None,
         compact_arguments=compact_arguments,
         global_arguments=global_arguments,
@@ -70,7 +67,6 @@ def test_compact_bails_if_repository_does_not_match():
         config_filename='test.yaml',
         repository={'path': 'repo'},
         config={},
-        hook_context={},
         local_borg_version=None,
         compact_arguments=compact_arguments,
         global_arguments=global_arguments,

+ 0 - 7
tests/unit/actions/test_create.py

@@ -424,7 +424,6 @@ def test_run_create_executes_and_calls_hooks_for_configured_repository():
         flexmock()
     )
     flexmock(module.borgmatic.borg.create).should_receive('create_archive').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'
@@ -447,7 +446,6 @@ def test_run_create_executes_and_calls_hooks_for_configured_repository():
             repository={'path': 'repo'},
             config={},
             config_paths=['/tmp/test.yaml'],
-            hook_context={},
             local_borg_version=None,
             create_arguments=create_arguments,
             global_arguments=global_arguments,
@@ -467,7 +465,6 @@ def test_run_create_runs_with_selected_repository():
         flexmock()
     )
     flexmock(module.borgmatic.borg.create).should_receive('create_archive').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'
@@ -490,7 +487,6 @@ def test_run_create_runs_with_selected_repository():
             repository={'path': 'repo'},
             config={},
             config_paths=['/tmp/test.yaml'],
-            hook_context={},
             local_borg_version=None,
             create_arguments=create_arguments,
             global_arguments=global_arguments,
@@ -523,7 +519,6 @@ def test_run_create_bails_if_repository_does_not_match():
             repository='repo',
             config={},
             config_paths=['/tmp/test.yaml'],
-            hook_context={},
             local_borg_version=None,
             create_arguments=create_arguments,
             global_arguments=global_arguments,
@@ -547,7 +542,6 @@ def test_run_create_produces_json():
     )
     parsed_json = flexmock()
     flexmock(module.borgmatic.actions.json).should_receive('parse_json').and_return(parsed_json)
-    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'
@@ -570,7 +564,6 @@ def test_run_create_produces_json():
             repository={'path': 'repo'},
             config={},
             config_paths=['/tmp/test.yaml'],
-            hook_context={},
             local_borg_version=None,
             create_arguments=create_arguments,
             global_arguments=global_arguments,

+ 0 - 2
tests/unit/actions/test_extract.py

@@ -7,7 +7,6 @@ def test_run_extract_calls_hooks():
     flexmock(module.logger).answer = lambda message: None
     flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
     flexmock(module.borgmatic.borg.extract).should_receive('extract_archive')
-    flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2)
     extract_arguments = flexmock(
         paths=flexmock(),
         progress=flexmock(),
@@ -22,7 +21,6 @@ def test_run_extract_calls_hooks():
         config_filename='test.yaml',
         repository={'path': 'repo'},
         config={'repositories': ['repo']},
-        hook_context={},
         local_borg_version=None,
         extract_arguments=extract_arguments,
         global_arguments=global_arguments,

+ 0 - 4
tests/unit/actions/test_prune.py

@@ -7,7 +7,6 @@ def test_run_prune_calls_hooks_for_configured_repository():
     flexmock(module.logger).answer = lambda message: None
     flexmock(module.borgmatic.config.validate).should_receive('repositories_match').never()
     flexmock(module.borgmatic.borg.prune).should_receive('prune_archives').once()
-    flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2)
     prune_arguments = flexmock(repository=None, stats=flexmock(), list_archives=flexmock())
     global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
 
@@ -15,7 +14,6 @@ def test_run_prune_calls_hooks_for_configured_repository():
         config_filename='test.yaml',
         repository={'path': 'repo'},
         config={},
-        hook_context={},
         local_borg_version=None,
         prune_arguments=prune_arguments,
         global_arguments=global_arguments,
@@ -38,7 +36,6 @@ def test_run_prune_runs_with_selected_repository():
         config_filename='test.yaml',
         repository={'path': 'repo'},
         config={},
-        hook_context={},
         local_borg_version=None,
         prune_arguments=prune_arguments,
         global_arguments=global_arguments,
@@ -61,7 +58,6 @@ def test_run_prune_bails_if_repository_does_not_match():
         config_filename='test.yaml',
         repository='repo',
         config={},
-        hook_context={},
         local_borg_version=None,
         prune_arguments=prune_arguments,
         global_arguments=global_arguments,

+ 134 - 69
tests/unit/commands/test_borgmatic.py

@@ -30,6 +30,7 @@ def test_get_skip_actions_uses_config_and_arguments(config, arguments, expected_
 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.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()]
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
@@ -37,7 +38,7 @@ def test_run_configuration_runs_actions_for_each_repository():
         expected_results[1:]
     )
     config = {'repositories': [{'path': 'foo'}, {'path': 'bar'}]}
-    arguments = {'global': flexmock(monitoring_verbosity=1)}
+    arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False)}
 
     results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
 
@@ -47,11 +48,12 @@ 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.command).should_receive('Before_after_hooks').and_return(flexmock())
     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_return(flexmock()).and_return(flexmock())
     config = {'repositories': [{'path': 'foo'}, {'path': 'bar'}], 'skip_actions': ['compact']}
-    arguments = {'global': flexmock(monitoring_verbosity=1)}
+    arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False)}
 
     list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
 
@@ -59,8 +61,8 @@ 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.command).should_receive('Before_after_hooks').and_return(flexmock())
     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()
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('run_actions').never()
@@ -73,6 +75,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.command).should_receive('Before_after_hooks').and_return(flexmock())
     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
@@ -81,6 +84,8 @@ def test_run_configuration_logs_monitor_start_error():
     flexmock(module).should_receive('log_error_records').and_return(expected_results)
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('run_actions').never()
+    flexmock(module.command).should_receive('filter_hooks')
+    flexmock(module.command).should_receive('execute_hooks')
     config = {'repositories': [{'path': 'foo'}]}
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
 
@@ -92,6 +97,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.command).should_receive('Before_after_hooks').and_return(flexmock())
     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).and_return(None)
@@ -109,13 +115,15 @@ 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.command).should_receive('Before_after_hooks').and_return(flexmock())
     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')
     expected_results = [flexmock()]
     flexmock(module).should_receive('log_error_records').and_return(expected_results)
     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')
+    flexmock(module.command).should_receive('execute_hooks')
     config = {'repositories': [{'path': 'foo'}]}
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False)}
 
@@ -127,6 +135,7 @@ def test_run_configuration_logs_actions_error():
 def test_run_configuration_skips_remaining_actions_for_actions_soft_failure_but_still_runs_next_repository_actions():
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('get_skip_actions').and_return([])
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     flexmock(module.dispatch).should_receive('call_hooks').times(5)
     error = subprocess.CalledProcessError(borgmatic.hooks.command.SOFT_FAIL_EXIT_CODE, 'try again')
@@ -146,6 +155,7 @@ def test_run_configuration_skips_remaining_actions_for_actions_soft_failure_but_
 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.command).should_receive('Before_after_hooks').and_return(flexmock())
     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
@@ -154,6 +164,8 @@ def test_run_configuration_logs_monitor_log_error():
     flexmock(module).should_receive('log_error_records').and_return(expected_results)
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('run_actions').and_return([])
+    flexmock(module.command).should_receive('filter_hooks')
+    flexmock(module.command).should_receive('execute_hooks')
     config = {'repositories': [{'path': 'foo'}]}
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
 
@@ -165,6 +177,7 @@ def test_run_configuration_logs_monitor_log_error():
 def test_run_configuration_still_pings_monitor_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.command).should_receive('Before_after_hooks').and_return(flexmock())
     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(
@@ -185,6 +198,7 @@ def test_run_configuration_still_pings_monitor_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.command).should_receive('Before_after_hooks').and_return(flexmock())
     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
@@ -193,6 +207,8 @@ def test_run_configuration_logs_monitor_finish_error():
     flexmock(module).should_receive('log_error_records').and_return(expected_results)
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('run_actions').and_return([])
+    flexmock(module.command).should_receive('filter_hooks')
+    flexmock(module.command).should_receive('execute_hooks')
     config = {'repositories': [{'path': 'foo'}]}
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
 
@@ -204,6 +220,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.command).should_receive('Before_after_hooks').and_return(flexmock())
     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(
@@ -224,6 +241,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.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
 
     flexmock(module.dispatch).should_receive('call_hooks').never()
@@ -239,8 +257,10 @@ 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.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
-    flexmock(module.command).should_receive('execute_hook').and_raise(OSError)
+    flexmock(module.command).should_receive('filter_hooks')
+    flexmock(module.command).should_receive('execute_hooks').and_raise(OSError)
     expected_results = [flexmock(), flexmock()]
     flexmock(module).should_receive('log_error_records').and_return(
         expected_results[:1]
@@ -258,9 +278,11 @@ 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.command).should_receive('Before_after_hooks').and_return(flexmock())
     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)
+    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)
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
@@ -277,14 +299,18 @@ 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.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
-    flexmock(module.command).should_receive('execute_hook')
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('run_actions').and_raise(OSError).and_return([])
     flexmock(module).should_receive('log_error_records').and_return([flexmock()]).once()
+    flexmock(module.command).should_receive('filter_hooks').never()
+    flexmock(module.command).should_receive('execute_hooks').never()
     config = {'repositories': [{'path': 'foo'}], 'retries': 1}
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
+
     results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
+
     assert results == []
 
 
@@ -292,8 +318,8 @@ 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.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
-    flexmock(module.command).should_receive('execute_hook')
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('run_actions').and_raise(OSError).times(2)
     flexmock(module).should_receive('log_error_records').with_args(
@@ -307,17 +333,21 @@ def test_run_configuration_retries_hard_error():
         'Error running actions for repository',
         OSError,
     ).and_return(error_logs)
+    flexmock(module.command).should_receive('filter_hooks')
+    flexmock(module.command).should_receive('execute_hooks')
     config = {'repositories': [{'path': 'foo'}], 'retries': 1}
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
+
     results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
+
     assert results == error_logs
 
 
-def test_run_configuration_repos_ordered():
+def test_run_configuration_retries_repositories_in_order():
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('get_skip_actions').and_return([])
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
-    flexmock(module.command).should_receive('execute_hook')
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('run_actions').and_raise(OSError).times(2)
     expected_results = [flexmock(), flexmock()]
@@ -327,17 +357,21 @@ def test_run_configuration_repos_ordered():
     flexmock(module).should_receive('log_error_records').with_args(
         'Error running actions for repository', OSError
     ).and_return(expected_results[1:]).ordered()
+    flexmock(module.command).should_receive('filter_hooks')
+    flexmock(module.command).should_receive('execute_hooks')
     config = {'repositories': [{'path': 'foo'}, {'path': 'bar'}]}
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
+
     results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
+
     assert results == expected_results
 
 
 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.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
-    flexmock(module.command).should_receive('execute_hook')
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('run_actions').and_raise(OSError).times(4)
     flexmock(module).should_receive('log_error_records').with_args(
@@ -360,20 +394,24 @@ 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()
+    flexmock(module.command).should_receive('filter_hooks')
+    flexmock(module.command).should_receive('execute_hooks')
     config = {
         'repositories': [{'path': 'foo'}, {'path': 'bar'}],
         'retries': 1,
     }
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
+
     results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
+
     assert results == foo_error_logs + bar_error_logs
 
 
-def test_run_configuration_retries_one_passes():
+def test_run_configuration_with_one_retry():
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('get_skip_actions').and_return([])
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
-    flexmock(module.command).should_receive('execute_hook')
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('run_actions').and_raise(OSError).and_raise(OSError).and_return(
         []
@@ -394,20 +432,24 @@ def test_run_configuration_retries_one_passes():
     flexmock(module).should_receive('log_error_records').with_args(
         'Error running actions for repository', OSError
     ).and_return(error_logs).ordered()
+    flexmock(module.command).should_receive('filter_hooks')
+    flexmock(module.command).should_receive('execute_hooks')
     config = {
         'repositories': [{'path': 'foo'}, {'path': 'bar'}],
         'retries': 1,
     }
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
+
     results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
+
     assert results == error_logs
 
 
-def test_run_configuration_retry_wait():
+def test_run_configuration_with_retry_wait_does_backoff_after_each_retry():
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('get_skip_actions').and_return([])
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
-    flexmock(module.command).should_receive('execute_hook')
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('run_actions').and_raise(OSError).times(4)
     flexmock(module).should_receive('log_error_records').with_args(
@@ -438,21 +480,25 @@ def test_run_configuration_retry_wait():
     flexmock(module).should_receive('log_error_records').with_args(
         'Error running actions for repository', OSError
     ).and_return(error_logs).ordered()
+    flexmock(module.command).should_receive('filter_hooks')
+    flexmock(module.command).should_receive('execute_hooks')
     config = {
         'repositories': [{'path': 'foo'}],
         'retries': 3,
         'retry_wait': 10,
     }
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
+
     results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
+
     assert results == error_logs
 
 
-def test_run_configuration_retries_timeout_multiple_repos():
+def test_run_configuration_with_multiple_repositories_retries_with_timeout():
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('get_skip_actions').and_return([])
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
-    flexmock(module.command).should_receive('execute_hook')
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('run_actions').and_raise(OSError).and_raise(OSError).and_return(
         []
@@ -479,20 +525,24 @@ def test_run_configuration_retries_timeout_multiple_repos():
     flexmock(module).should_receive('log_error_records').with_args(
         'Error running actions for repository', OSError
     ).and_return(error_logs).ordered()
+    flexmock(module.command).should_receive('filter_hooks')
+    flexmock(module.command).should_receive('execute_hooks')
     config = {
         'repositories': [{'path': 'foo'}, {'path': 'bar'}],
         'retries': 1,
         'retry_wait': 10,
     }
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
+
     results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
+
     assert results == error_logs
 
 
 def test_run_actions_runs_repo_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')
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(borgmatic.actions.repo_create).should_receive('run_repo_create').once()
 
     tuple(
@@ -515,19 +565,13 @@ def test_run_actions_runs_repo_create():
 def test_run_actions_adds_label_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')
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     expected = flexmock()
     flexmock(borgmatic.actions.create).should_receive('run_create').with_args(
         config_filename=object,
         repository={'path': 'repo', 'label': 'my repo'},
         config={'repositories': []},
         config_paths=[],
-        hook_context={
-            'repository_label': 'my repo',
-            'log_file': '',
-            'repositories': '',
-            'repository': 'repo',
-        },
         local_borg_version=object,
         create_arguments=object,
         global_arguments=object,
@@ -554,19 +598,13 @@ def test_run_actions_adds_label_file_to_hook_context():
 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')
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     expected = flexmock()
     flexmock(borgmatic.actions.create).should_receive('run_create').with_args(
         config_filename=object,
         repository={'path': 'repo'},
         config={'repositories': []},
         config_paths=[],
-        hook_context={
-            'repository_label': '',
-            'log_file': 'foo',
-            'repositories': '',
-            'repository': 'repo',
-        },
         local_borg_version=object,
         create_arguments=object,
         global_arguments=object,
@@ -593,7 +631,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(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(borgmatic.actions.transfer).should_receive('run_transfer').once()
 
     tuple(
@@ -613,7 +651,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')
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     expected = flexmock()
     flexmock(borgmatic.actions.create).should_receive('run_create').and_yield(expected).once()
 
@@ -635,7 +673,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(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(borgmatic.actions.create).should_receive('run_create').never()
 
     tuple(
@@ -655,7 +693,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(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(borgmatic.actions.prune).should_receive('run_prune').once()
 
     tuple(
@@ -675,7 +713,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(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(borgmatic.actions.prune).should_receive('run_prune').never()
 
     tuple(
@@ -695,7 +733,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(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(borgmatic.actions.compact).should_receive('run_compact').once()
 
     tuple(
@@ -715,7 +753,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(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(borgmatic.actions.compact).should_receive('run_compact').never()
 
     tuple(
@@ -735,7 +773,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.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(module.checks).should_receive('repository_enabled_for_checks').and_return(True)
     flexmock(borgmatic.actions.check).should_receive('run_check').once()
 
@@ -756,7 +794,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.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(module.checks).should_receive('repository_enabled_for_checks').and_return(False)
     flexmock(borgmatic.actions.check).should_receive('run_check').never()
 
@@ -777,7 +815,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.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(module.checks).should_receive('repository_enabled_for_checks').and_return(True)
     flexmock(borgmatic.actions.check).should_receive('run_check').never()
 
@@ -798,7 +836,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(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(borgmatic.actions.extract).should_receive('run_extract').once()
 
     tuple(
@@ -818,7 +856,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(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(borgmatic.actions.export_tar).should_receive('run_export_tar').once()
 
     tuple(
@@ -838,7 +876,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(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(borgmatic.actions.mount).should_receive('run_mount').once()
 
     tuple(
@@ -858,7 +896,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(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(borgmatic.actions.restore).should_receive('run_restore').once()
 
     tuple(
@@ -878,7 +916,7 @@ def test_run_actions_runs_restore():
 def test_run_actions_runs_repo_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')
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     expected = flexmock()
     flexmock(borgmatic.actions.repo_list).should_receive('run_repo_list').and_yield(expected).once()
 
@@ -900,7 +938,7 @@ def test_run_actions_runs_repo_list():
 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')
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     expected = flexmock()
     flexmock(borgmatic.actions.list).should_receive('run_list').and_yield(expected).once()
 
@@ -922,7 +960,7 @@ def test_run_actions_runs_list():
 def test_run_actions_runs_repo_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')
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     expected = flexmock()
     flexmock(borgmatic.actions.repo_info).should_receive('run_repo_info').and_yield(expected).once()
 
@@ -944,7 +982,7 @@ def test_run_actions_runs_repo_info():
 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')
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     expected = flexmock()
     flexmock(borgmatic.actions.info).should_receive('run_info').and_yield(expected).once()
 
@@ -966,7 +1004,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(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(borgmatic.actions.break_lock).should_receive('run_break_lock').once()
 
     tuple(
@@ -986,7 +1024,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(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(borgmatic.actions.export_key).should_receive('run_export_key').once()
 
     tuple(
@@ -1006,7 +1044,7 @@ def test_run_actions_runs_export_key():
 def test_run_actions_runs_change_passphrase():
     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.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(borgmatic.actions.change_passphrase).should_receive('run_change_passphrase').once()
 
     tuple(
@@ -1029,7 +1067,7 @@ def test_run_actions_runs_change_passphrase():
 def test_run_actions_runs_delete():
     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.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(borgmatic.actions.delete).should_receive('run_delete').once()
 
     tuple(
@@ -1049,7 +1087,7 @@ def test_run_actions_runs_delete():
 def test_run_actions_runs_repo_delete():
     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.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(borgmatic.actions.repo_delete).should_receive('run_repo_delete').once()
 
     tuple(
@@ -1072,7 +1110,7 @@ def test_run_actions_runs_repo_delete():
 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(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(borgmatic.actions.borg).should_receive('run_borg').once()
 
     tuple(
@@ -1092,7 +1130,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(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(borgmatic.actions.borg).should_receive('run_borg').once().ordered()
     flexmock(borgmatic.actions.restore).should_receive('run_restore').once().ordered()
 
@@ -1398,11 +1436,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.command).should_receive('execute_hook').never()
     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).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('run_configuration').and_return([])
-    arguments = {}
+    arguments = {'global': flexmock(dry_run=False)}
 
     logs = tuple(
         module.collect_configuration_run_summary_logs(
@@ -1415,6 +1454,8 @@ def test_collect_configuration_run_summary_logs_info_for_success():
 
 def test_collect_configuration_run_summary_executes_hooks_for_create():
     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).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)}
@@ -1430,9 +1471,11 @@ def test_collect_configuration_run_summary_executes_hooks_for_create():
 
 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('execute_hooks')
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('run_configuration').and_return([])
-    arguments = {'extract': flexmock(repository='repo')}
+    arguments = {'extract': flexmock(repository='repo'), 'global': flexmock(dry_run=False)}
 
     logs = tuple(
         module.collect_configuration_run_summary_logs(
@@ -1462,9 +1505,11 @@ 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('execute_hooks')
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('run_configuration').and_return([])
-    arguments = {'mount': flexmock(repository='repo')}
+    arguments = {'mount': flexmock(repository='repo'), 'global': flexmock(dry_run=False)}
 
     logs = tuple(
         module.collect_configuration_run_summary_logs(
@@ -1481,7 +1526,7 @@ def test_collect_configuration_run_summary_logs_mount_with_repository_error():
     )
     expected_logs = (flexmock(),)
     flexmock(module).should_receive('log_error_records').and_return(expected_logs)
-    arguments = {'mount': flexmock(repository='repo')}
+    arguments = {'mount': flexmock(repository='repo'), 'global': flexmock(dry_run=False)}
 
     logs = tuple(
         module.collect_configuration_run_summary_logs(
@@ -1493,6 +1538,9 @@ 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('execute_hooks')
     arguments = {'global': flexmock(config_paths=[])}
     expected_logs = (flexmock(),)
     flexmock(module).should_receive('log_error_records').and_return(expected_logs)
@@ -1505,7 +1553,9 @@ def test_collect_configuration_run_summary_logs_missing_configs_error():
 
 
 def test_collect_configuration_run_summary_logs_pre_hook_error():
-    flexmock(module.command).should_receive('execute_hook').and_raise(ValueError)
+    flexmock(module.validate).should_receive('guard_configuration_contains_repository')
+    flexmock(module.command).should_receive('filter_hooks')
+    flexmock(module.command).should_receive('execute_hooks').and_raise(ValueError)
     expected_logs = (flexmock(),)
     flexmock(module).should_receive('log_error_records').and_return(expected_logs)
     arguments = {'create': flexmock(), 'global': flexmock(monitoring_verbosity=1, dry_run=False)}
@@ -1520,8 +1570,9 @@ def test_collect_configuration_run_summary_logs_pre_hook_error():
 
 
 def test_collect_configuration_run_summary_logs_post_hook_error():
-    flexmock(module.command).should_receive('execute_hook').and_return(None).and_raise(ValueError)
     flexmock(module.validate).should_receive('guard_configuration_contains_repository')
+    flexmock(module.command).should_receive('filter_hooks')
+    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([])
     expected_logs = (flexmock(),)
@@ -1543,7 +1594,10 @@ def test_collect_configuration_run_summary_logs_for_list_with_archive_and_reposi
     )
     expected_logs = (flexmock(),)
     flexmock(module).should_receive('log_error_records').and_return(expected_logs)
-    arguments = {'list': flexmock(repository='repo', archive='test')}
+    arguments = {
+        'list': flexmock(repository='repo', archive='test'),
+        'global': flexmock(dry_run=False),
+    }
 
     logs = tuple(
         module.collect_configuration_run_summary_logs(
@@ -1556,9 +1610,14 @@ 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('execute_hooks')
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('run_configuration').and_return([])
-    arguments = {'list': flexmock(repository='repo', archive=None)}
+    arguments = {
+        'list': flexmock(repository='repo', archive=None),
+        'global': flexmock(dry_run=False),
+    }
 
     logs = tuple(
         module.collect_configuration_run_summary_logs(
@@ -1571,12 +1630,14 @@ def test_collect_configuration_run_summary_logs_info_for_success_with_list():
 
 def test_collect_configuration_run_summary_logs_run_configuration_error():
     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).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('run_configuration').and_return(
         [logging.makeLogRecord(dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg='Error'))]
     )
     flexmock(module).should_receive('log_error_records').and_return([])
-    arguments = {}
+    arguments = {'global': flexmock(dry_run=False)}
 
     logs = tuple(
         module.collect_configuration_run_summary_logs(
@@ -1589,13 +1650,15 @@ def test_collect_configuration_run_summary_logs_run_configuration_error():
 
 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('execute_hooks')
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('run_configuration').and_return([])
     flexmock(module.borg_umount).should_receive('unmount_archive').and_raise(OSError)
     flexmock(module).should_receive('log_error_records').and_return(
         [logging.makeLogRecord(dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg='Error'))]
     )
-    arguments = {'umount': flexmock(mount_point='/mnt')}
+    arguments = {'umount': flexmock(mount_point='/mnt'), 'global': flexmock(dry_run=False)}
 
     logs = tuple(
         module.collect_configuration_run_summary_logs(
@@ -1608,6 +1671,8 @@ 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('execute_hooks')
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('run_configuration').and_return(['foo', 'bar']).and_return(
         ['baz']
@@ -1615,7 +1680,7 @@ def test_collect_configuration_run_summary_logs_outputs_merged_json_results():
     stdout = flexmock()
     stdout.should_receive('write').with_args('["foo", "bar", "baz"]').once()
     flexmock(module.sys).stdout = stdout
-    arguments = {}
+    arguments = {'global': flexmock(dry_run=False)}
 
     tuple(
         module.collect_configuration_run_summary_logs(

+ 0 - 4
tests/unit/config/test_generate.py

@@ -133,7 +133,6 @@ def test_schema_to_sample_configuration_with_unsupported_schema_raises():
 def test_merge_source_configuration_into_destination_inserts_map_fields():
     destination_config = {'foo': 'dest1', 'bar': 'dest2'}
     source_config = {'foo': 'source1', 'baz': 'source2'}
-    flexmock(module).should_receive('remove_commented_out_sentinel')
     flexmock(module).should_receive('ruamel.yaml.comments.CommentedSeq').replace_with(list)
 
     module.merge_source_configuration_into_destination(destination_config, source_config)
@@ -144,7 +143,6 @@ def test_merge_source_configuration_into_destination_inserts_map_fields():
 def test_merge_source_configuration_into_destination_inserts_nested_map_fields():
     destination_config = {'foo': {'first': 'dest1', 'second': 'dest2'}, 'bar': 'dest3'}
     source_config = {'foo': {'first': 'source1'}}
-    flexmock(module).should_receive('remove_commented_out_sentinel')
     flexmock(module).should_receive('ruamel.yaml.comments.CommentedSeq').replace_with(list)
 
     module.merge_source_configuration_into_destination(destination_config, source_config)
@@ -155,7 +153,6 @@ def test_merge_source_configuration_into_destination_inserts_nested_map_fields()
 def test_merge_source_configuration_into_destination_inserts_sequence_fields():
     destination_config = {'foo': ['dest1', 'dest2'], 'bar': ['dest3'], 'baz': ['dest4']}
     source_config = {'foo': ['source1'], 'bar': ['source2', 'source3']}
-    flexmock(module).should_receive('remove_commented_out_sentinel')
     flexmock(module).should_receive('ruamel.yaml.comments.CommentedSeq').replace_with(list)
 
     module.merge_source_configuration_into_destination(destination_config, source_config)
@@ -170,7 +167,6 @@ def test_merge_source_configuration_into_destination_inserts_sequence_fields():
 def test_merge_source_configuration_into_destination_inserts_sequence_of_maps():
     destination_config = {'foo': [{'first': 'dest1', 'second': 'dest2'}], 'bar': 'dest3'}
     source_config = {'foo': [{'first': 'source1'}, {'other': 'source2'}]}
-    flexmock(module).should_receive('remove_commented_out_sentinel')
     flexmock(module).should_receive('ruamel.yaml.comments.CommentedSeq').replace_with(list)
 
     module.merge_source_configuration_into_destination(destination_config, source_config)

+ 7 - 0
tests/unit/hooks/data_source/test_bootstrap.py

@@ -6,6 +6,9 @@ from borgmatic.hooks.data_source import bootstrap as module
 
 
 def test_dump_data_sources_creates_manifest_file():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     flexmock(module.os).should_receive('makedirs')
 
     flexmock(module.importlib.metadata).should_receive('version').and_return('1.0.0')
@@ -32,6 +35,7 @@ def test_dump_data_sources_creates_manifest_file():
 
 
 def test_dump_data_sources_with_store_config_files_false_does_not_create_manifest_file():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').never()
     flexmock(module.os).should_receive('makedirs').never()
     flexmock(module.json).should_receive('dump').never()
     hook_config = {'store_config_files': False}
@@ -47,6 +51,9 @@ def test_dump_data_sources_with_store_config_files_false_does_not_create_manifes
 
 
 def test_dump_data_sources_with_dry_run_does_not_create_manifest_file():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     flexmock(module.os).should_receive('makedirs').never()
     flexmock(module.json).should_receive('dump').never()
 

+ 18 - 0
tests/unit/hooks/data_source/test_btrfs.py

@@ -207,6 +207,9 @@ def test_make_borg_snapshot_pattern_includes_slashdot_hack_and_stripped_pattern_
 
 
 def test_dump_data_sources_snapshots_each_subvolume_and_updates_patterns():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     patterns = [Pattern('/foo'), Pattern('/mnt/subvol1')]
     config = {'btrfs': {}}
     flexmock(module).should_receive('get_subvolumes').and_return(
@@ -285,6 +288,9 @@ def test_dump_data_sources_snapshots_each_subvolume_and_updates_patterns():
 
 
 def test_dump_data_sources_uses_custom_btrfs_command_in_commands():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     patterns = [Pattern('/foo'), Pattern('/mnt/subvol1')]
     config = {'btrfs': {'btrfs_command': '/usr/local/bin/btrfs'}}
     flexmock(module).should_receive('get_subvolumes').and_return(
@@ -338,6 +344,9 @@ def test_dump_data_sources_uses_custom_btrfs_command_in_commands():
 
 
 def test_dump_data_sources_uses_custom_findmnt_command_in_commands():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     patterns = [Pattern('/foo'), Pattern('/mnt/subvol1')]
     config = {'btrfs': {'findmnt_command': '/usr/local/bin/findmnt'}}
     flexmock(module).should_receive('get_subvolumes').with_args(
@@ -393,6 +402,9 @@ def test_dump_data_sources_uses_custom_findmnt_command_in_commands():
 
 
 def test_dump_data_sources_with_dry_run_skips_snapshot_and_patterns_update():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     patterns = [Pattern('/foo'), Pattern('/mnt/subvol1')]
     config = {'btrfs': {}}
     flexmock(module).should_receive('get_subvolumes').and_return(
@@ -421,6 +433,9 @@ def test_dump_data_sources_with_dry_run_skips_snapshot_and_patterns_update():
 
 
 def test_dump_data_sources_without_matching_subvolumes_skips_snapshot_and_patterns_update():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     patterns = [Pattern('/foo'), Pattern('/mnt/subvol1')]
     config = {'btrfs': {}}
     flexmock(module).should_receive('get_subvolumes').and_return(())
@@ -445,6 +460,9 @@ def test_dump_data_sources_without_matching_subvolumes_skips_snapshot_and_patter
 
 
 def test_dump_data_sources_snapshots_adds_to_existing_exclude_patterns():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     patterns = [Pattern('/foo'), Pattern('/mnt/subvol1')]
     config = {'btrfs': {}, 'exclude_patterns': ['/bar']}
     flexmock(module).should_receive('get_subvolumes').and_return(

+ 21 - 0
tests/unit/hooks/data_source/test_lvm.py

@@ -282,6 +282,9 @@ def test_make_borg_snapshot_pattern_includes_slashdot_hack_and_stripped_pattern_
 
 
 def test_dump_data_sources_snapshots_and_mounts_and_updates_patterns():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     config = {'lvm': {}}
     patterns = [Pattern('/mnt/lvolume1/subdir'), Pattern('/mnt/lvolume2')]
     logical_volumes = (
@@ -351,6 +354,9 @@ def test_dump_data_sources_snapshots_and_mounts_and_updates_patterns():
 
 
 def test_dump_data_sources_with_no_logical_volumes_skips_snapshots():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     config = {'lvm': {}}
     patterns = [Pattern('/mnt/lvolume1/subdir'), Pattern('/mnt/lvolume2')]
     flexmock(module).should_receive('get_logical_volumes').and_return(())
@@ -373,6 +379,9 @@ def test_dump_data_sources_with_no_logical_volumes_skips_snapshots():
 
 
 def test_dump_data_sources_uses_snapshot_size_for_snapshot():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     config = {'lvm': {'snapshot_size': '1000PB'}}
     patterns = [Pattern('/mnt/lvolume1/subdir'), Pattern('/mnt/lvolume2')]
     logical_volumes = (
@@ -448,6 +457,9 @@ def test_dump_data_sources_uses_snapshot_size_for_snapshot():
 
 
 def test_dump_data_sources_uses_custom_commands():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     config = {
         'lvm': {
             'lsblk_command': '/usr/local/bin/lsblk',
@@ -534,6 +546,9 @@ def test_dump_data_sources_uses_custom_commands():
 
 
 def test_dump_data_sources_with_dry_run_skips_snapshots_and_does_not_touch_patterns():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     config = {'lvm': {}}
     patterns = [Pattern('/mnt/lvolume1/subdir'), Pattern('/mnt/lvolume2')]
     flexmock(module).should_receive('get_logical_volumes').and_return(
@@ -585,6 +600,9 @@ def test_dump_data_sources_with_dry_run_skips_snapshots_and_does_not_touch_patte
 
 
 def test_dump_data_sources_ignores_mismatch_between_given_patterns_and_contained_patterns():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     config = {'lvm': {}}
     patterns = [Pattern('/hmm')]
     logical_volumes = (
@@ -655,6 +673,9 @@ def test_dump_data_sources_ignores_mismatch_between_given_patterns_and_contained
 
 
 def test_dump_data_sources_with_missing_snapshot_errors():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     config = {'lvm': {}}
     patterns = [Pattern('/mnt/lvolume1/subdir'), Pattern('/mnt/lvolume2')]
     flexmock(module).should_receive('get_logical_volumes').and_return(

+ 18 - 0
tests/unit/hooks/data_source/test_mariadb.py

@@ -237,6 +237,9 @@ def test_use_streaming_false_for_no_databases():
 
 
 def test_dump_data_sources_dumps_each_database():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'foo'}, {'name': 'bar'}]
     processes = [flexmock(), flexmock()]
     flexmock(module).should_receive('make_dump_path').and_return('')
@@ -278,6 +281,9 @@ def test_dump_data_sources_dumps_each_database():
 
 
 def test_dump_data_sources_dumps_with_password():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     database = {'name': 'foo', 'username': 'root', 'password': 'trustsome1'}
     process = flexmock()
     flexmock(module).should_receive('make_dump_path').and_return('')
@@ -312,6 +318,9 @@ def test_dump_data_sources_dumps_with_password():
 
 
 def test_dump_data_sources_dumps_all_databases_at_once():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'all'}]
     process = flexmock()
     flexmock(module).should_receive('make_dump_path').and_return('')
@@ -343,6 +352,9 @@ def test_dump_data_sources_dumps_all_databases_at_once():
 
 
 def test_dump_data_sources_dumps_all_databases_separately_when_format_configured():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'all', 'format': 'sql'}]
     processes = [flexmock(), flexmock()]
     flexmock(module).should_receive('make_dump_path').and_return('')
@@ -850,6 +862,9 @@ def test_execute_dump_command_with_dry_run_skips_mariadb_dump():
 
 
 def test_dump_data_sources_errors_for_missing_all_databases():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'all'}]
     flexmock(module).should_receive('make_dump_path').and_return('')
     flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
@@ -873,6 +888,9 @@ def test_dump_data_sources_errors_for_missing_all_databases():
 
 
 def test_dump_data_sources_does_not_error_for_missing_all_databases_with_dry_run():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'all'}]
     flexmock(module).should_receive('make_dump_path').and_return('')
     flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})

+ 21 - 0
tests/unit/hooks/data_source/test_mongodb.py

@@ -24,6 +24,9 @@ def test_use_streaming_false_for_no_databases():
 
 
 def test_dump_data_sources_runs_mongodump_for_each_database():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'foo'}, {'name': 'bar'}]
     processes = [flexmock(), flexmock()]
     flexmock(module).should_receive('make_dump_path').and_return('')
@@ -53,6 +56,9 @@ def test_dump_data_sources_runs_mongodump_for_each_database():
 
 
 def test_dump_data_sources_with_dry_run_skips_mongodump():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'foo'}, {'name': 'bar'}]
     flexmock(module).should_receive('make_dump_path').and_return('')
     flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
@@ -75,6 +81,9 @@ def test_dump_data_sources_with_dry_run_skips_mongodump():
 
 
 def test_dump_data_sources_runs_mongodump_with_hostname_and_port():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'foo', 'hostname': 'database.example.org', 'port': 5433}]
     process = flexmock()
     flexmock(module).should_receive('make_dump_path').and_return('')
@@ -111,6 +120,9 @@ def test_dump_data_sources_runs_mongodump_with_hostname_and_port():
 
 
 def test_dump_data_sources_runs_mongodump_with_username_and_password():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [
         {
             'name': 'foo',
@@ -162,6 +174,9 @@ def test_dump_data_sources_runs_mongodump_with_username_and_password():
 
 
 def test_dump_data_sources_runs_mongodump_with_directory_format():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'foo', 'format': 'directory'}]
     flexmock(module).should_receive('make_dump_path').and_return('')
     flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
@@ -189,6 +204,9 @@ def test_dump_data_sources_runs_mongodump_with_directory_format():
 
 
 def test_dump_data_sources_runs_mongodump_with_options():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'foo', 'options': '--stuff=such'}]
     process = flexmock()
     flexmock(module).should_receive('make_dump_path').and_return('')
@@ -222,6 +240,9 @@ def test_dump_data_sources_runs_mongodump_with_options():
 
 
 def test_dump_data_sources_runs_mongodumpall_for_all_databases():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'all'}]
     process = flexmock()
     flexmock(module).should_receive('make_dump_path').and_return('')

+ 18 - 0
tests/unit/hooks/data_source/test_mysql.py

@@ -134,6 +134,9 @@ def test_use_streaming_false_for_no_databases():
 
 
 def test_dump_data_sources_dumps_each_database():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'foo'}, {'name': 'bar'}]
     processes = [flexmock(), flexmock()]
     flexmock(module).should_receive('make_dump_path').and_return('')
@@ -172,6 +175,9 @@ def test_dump_data_sources_dumps_each_database():
 
 
 def test_dump_data_sources_dumps_with_password():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     database = {'name': 'foo', 'username': 'root', 'password': 'trustsome1'}
     process = flexmock()
     flexmock(module).should_receive('make_dump_path').and_return('')
@@ -206,6 +212,9 @@ def test_dump_data_sources_dumps_with_password():
 
 
 def test_dump_data_sources_dumps_all_databases_at_once():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'all'}]
     process = flexmock()
     flexmock(module).should_receive('make_dump_path').and_return('')
@@ -237,6 +246,9 @@ def test_dump_data_sources_dumps_all_databases_at_once():
 
 
 def test_dump_data_sources_dumps_all_databases_separately_when_format_configured():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'all', 'format': 'sql'}]
     processes = [flexmock(), flexmock()]
     flexmock(module).should_receive('make_dump_path').and_return('')
@@ -762,6 +774,9 @@ def test_execute_dump_command_with_dry_run_skips_mysqldump():
 
 
 def test_dump_data_sources_errors_for_missing_all_databases():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'all'}]
     flexmock(module).should_receive('make_dump_path').and_return('')
     flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
@@ -785,6 +800,9 @@ def test_dump_data_sources_errors_for_missing_all_databases():
 
 
 def test_dump_data_sources_does_not_error_for_missing_all_databases_with_dry_run():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'all'}]
     flexmock(module).should_receive('make_dump_path').and_return('')
     flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})

+ 42 - 0
tests/unit/hooks/data_source/test_postgresql.py

@@ -236,6 +236,9 @@ def test_use_streaming_false_for_no_databases():
 
 
 def test_dump_data_sources_runs_pg_dump_for_each_database():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'foo'}, {'name': 'bar'}]
     processes = [flexmock(), flexmock()]
     flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
@@ -284,6 +287,9 @@ def test_dump_data_sources_runs_pg_dump_for_each_database():
 
 
 def test_dump_data_sources_raises_when_no_database_names_to_dump():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'foo'}, {'name': 'bar'}]
     flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
     flexmock(module).should_receive('make_dump_path').and_return('')
@@ -301,6 +307,9 @@ def test_dump_data_sources_raises_when_no_database_names_to_dump():
 
 
 def test_dump_data_sources_does_not_raise_when_no_database_names_to_dump():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'foo'}, {'name': 'bar'}]
     flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
     flexmock(module).should_receive('make_dump_path').and_return('')
@@ -317,6 +326,9 @@ def test_dump_data_sources_does_not_raise_when_no_database_names_to_dump():
 
 
 def test_dump_data_sources_with_duplicate_dump_skips_pg_dump():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'foo'}, {'name': 'bar'}]
     flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
     flexmock(module).should_receive('make_dump_path').and_return('')
@@ -344,6 +356,9 @@ def test_dump_data_sources_with_duplicate_dump_skips_pg_dump():
 
 
 def test_dump_data_sources_with_dry_run_skips_pg_dump():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'foo'}, {'name': 'bar'}]
     flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
     flexmock(module).should_receive('make_dump_path').and_return('')
@@ -374,6 +389,9 @@ def test_dump_data_sources_with_dry_run_skips_pg_dump():
 
 
 def test_dump_data_sources_runs_pg_dump_with_hostname_and_port():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'foo', 'hostname': 'database.example.org', 'port': 5433}]
     process = flexmock()
     flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
@@ -420,6 +438,9 @@ def test_dump_data_sources_runs_pg_dump_with_hostname_and_port():
 
 
 def test_dump_data_sources_runs_pg_dump_with_username_and_password():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'foo', 'username': 'postgres', 'password': 'trustsome1'}]
     process = flexmock()
     flexmock(module).should_receive('make_environment').and_return(
@@ -466,6 +487,9 @@ def test_dump_data_sources_runs_pg_dump_with_username_and_password():
 
 
 def test_dump_data_sources_with_username_injection_attack_gets_escaped():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'foo', 'username': 'postgres; naughty-command', 'password': 'trustsome1'}]
     process = flexmock()
     flexmock(module).should_receive('make_environment').and_return(
@@ -512,6 +536,9 @@ def test_dump_data_sources_with_username_injection_attack_gets_escaped():
 
 
 def test_dump_data_sources_runs_pg_dump_with_directory_format():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'foo', 'format': 'directory'}]
     flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
     flexmock(module).should_receive('make_dump_path').and_return('')
@@ -556,6 +583,9 @@ def test_dump_data_sources_runs_pg_dump_with_directory_format():
 
 
 def test_dump_data_sources_runs_pg_dump_with_string_compression():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'foo', 'compression': 'winrar'}]
     processes = [flexmock()]
     flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
@@ -603,6 +633,9 @@ def test_dump_data_sources_runs_pg_dump_with_string_compression():
 
 
 def test_dump_data_sources_runs_pg_dump_with_integer_compression():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'foo', 'compression': 0}]
     processes = [flexmock()]
     flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
@@ -650,6 +683,9 @@ def test_dump_data_sources_runs_pg_dump_with_integer_compression():
 
 
 def test_dump_data_sources_runs_pg_dump_with_options():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'foo', 'options': '--stuff=such'}]
     process = flexmock()
     flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
@@ -693,6 +729,9 @@ def test_dump_data_sources_runs_pg_dump_with_options():
 
 
 def test_dump_data_sources_runs_pg_dumpall_for_all_databases():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'all'}]
     process = flexmock()
     flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
@@ -725,6 +764,9 @@ def test_dump_data_sources_runs_pg_dumpall_for_all_databases():
 
 
 def test_dump_data_sources_runs_non_default_pg_dump():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'foo', 'pg_dump_command': 'special_pg_dump --compress *'}]
     process = flexmock()
     flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})

+ 18 - 0
tests/unit/hooks/data_source/test_sqlite.py

@@ -17,6 +17,9 @@ def test_use_streaming_false_for_no_databases():
 
 
 def test_dump_data_sources_logs_and_skips_if_dump_already_exists():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'path': '/path/to/database', 'name': 'database'}]
 
     flexmock(module).should_receive('make_dump_path').and_return('/run/borgmatic')
@@ -41,6 +44,9 @@ def test_dump_data_sources_logs_and_skips_if_dump_already_exists():
 
 
 def test_dump_data_sources_dumps_each_database():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [
         {'path': '/path/to/database1', 'name': 'database1'},
         {'path': '/path/to/database2', 'name': 'database2'},
@@ -71,6 +77,9 @@ def test_dump_data_sources_dumps_each_database():
 
 
 def test_dump_data_sources_with_path_injection_attack_gets_escaped():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [
         {'path': '/path/to/database1; naughty-command', 'name': 'database1'},
     ]
@@ -108,6 +117,9 @@ def test_dump_data_sources_with_path_injection_attack_gets_escaped():
 
 
 def test_dump_data_sources_with_non_existent_path_warns_and_dumps_database():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [
         {'path': '/path/to/database1', 'name': 'database1'},
     ]
@@ -136,6 +148,9 @@ def test_dump_data_sources_with_non_existent_path_warns_and_dumps_database():
 
 
 def test_dump_data_sources_with_name_all_warns_and_dumps_all_databases():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [
         {'path': '/path/to/database1', 'name': 'all'},
     ]
@@ -166,6 +181,9 @@ def test_dump_data_sources_with_name_all_warns_and_dumps_all_databases():
 
 
 def test_dump_data_sources_does_not_dump_if_dry_run():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'path': '/path/to/database', 'name': 'database'}]
 
     flexmock(module).should_receive('make_dump_path').and_return('/run/borgmatic')

+ 15 - 0
tests/unit/hooks/data_source/test_zfs.py

@@ -296,6 +296,9 @@ def test_make_borg_snapshot_pattern_includes_slashdot_hack_and_stripped_pattern_
 
 
 def test_dump_data_sources_snapshots_and_mounts_and_updates_patterns():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     dataset = flexmock(
         name='dataset',
         mount_point='/mnt/dataset',
@@ -338,6 +341,9 @@ def test_dump_data_sources_snapshots_and_mounts_and_updates_patterns():
 
 
 def test_dump_data_sources_with_no_datasets_skips_snapshots():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     flexmock(module).should_receive('get_datasets_to_backup').and_return(())
     flexmock(module.os).should_receive('getpid').and_return(1234)
     flexmock(module).should_receive('snapshot_dataset').never()
@@ -360,6 +366,9 @@ def test_dump_data_sources_with_no_datasets_skips_snapshots():
 
 
 def test_dump_data_sources_uses_custom_commands():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     dataset = flexmock(
         name='dataset',
         mount_point='/mnt/dataset',
@@ -409,6 +418,9 @@ def test_dump_data_sources_uses_custom_commands():
 
 
 def test_dump_data_sources_with_dry_run_skips_commands_and_does_not_touch_patterns():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     flexmock(module).should_receive('get_datasets_to_backup').and_return(
         (flexmock(name='dataset', mount_point='/mnt/dataset'),)
     )
@@ -433,6 +445,9 @@ def test_dump_data_sources_with_dry_run_skips_commands_and_does_not_touch_patter
 
 
 def test_dump_data_sources_ignores_mismatch_between_given_patterns_and_contained_patterns():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     dataset = flexmock(
         name='dataset',
         mount_point='/mnt/dataset',

+ 34 - 38
tests/unit/hooks/test_command.py

@@ -48,43 +48,35 @@ def test_make_environment_with_pyinstaller_and_LD_LIBRARY_PATH_ORIG_copies_it_in
     ) == {'LD_LIBRARY_PATH_ORIG': '/lib/lib/lib', 'LD_LIBRARY_PATH': '/lib/lib/lib'}
 
 
-def test_execute_hook_invokes_each_command():
-    flexmock(module).should_receive('interpolate_context').replace_with(
-        lambda hook_description, command, context: command
-    )
-    flexmock(module).should_receive('make_environment').and_return({})
-    flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
-        [':'],
-        output_log_level=logging.WARNING,
-        shell=True,
-        environment={},
-    ).once()
-
-    module.execute_hook([':'], None, 'config.yaml', 'pre-backup', dry_run=False)
+LOGGING_ANSWER = flexmock()
 
 
-def test_execute_hook_with_multiple_commands_invokes_each_command():
+def test_execute_hooks_invokes_each_hook_and_command():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = LOGGING_ANSWER
     flexmock(module).should_receive('interpolate_context').replace_with(
         lambda hook_description, command, context: command
     )
     flexmock(module).should_receive('make_environment').and_return({})
-    flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
-        [':'],
-        output_log_level=logging.WARNING,
-        shell=True,
-        environment={},
-    ).once()
-    flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
-        ['true'],
-        output_log_level=logging.WARNING,
-        shell=True,
-        environment={},
-    ).once()
 
-    module.execute_hook([':', 'true'], None, 'config.yaml', 'pre-backup', dry_run=False)
+    for command in ('foo', 'bar', 'baz'):
+        flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
+            [command],
+            output_log_level=LOGGING_ANSWER,
+            shell=True,
+            environment={},
+        ).once()
+
+    module.execute_hooks(
+        [{'before': 'create', 'run': ['foo']}, {'before': 'create', 'run': ['bar', 'baz']}],
+        umask=None,
+        dry_run=False,
+    )
 
 
-def test_execute_hook_with_umask_sets_that_umask():
+def test_execute_hooks_with_umask_sets_that_umask():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = LOGGING_ANSWER
     flexmock(module).should_receive('interpolate_context').replace_with(
         lambda hook_description, command, context: command
     )
@@ -92,42 +84,46 @@ def test_execute_hook_with_umask_sets_that_umask():
     flexmock(module.os).should_receive('umask').with_args(0o22).once()
     flexmock(module).should_receive('make_environment').and_return({})
     flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
-        [':'],
-        output_log_level=logging.WARNING,
+        ['foo'],
+        output_log_level=logging.ANSWER,
         shell=True,
         environment={},
     )
 
-    module.execute_hook([':'], 77, 'config.yaml', 'pre-backup', dry_run=False)
+    module.execute_hooks([{'before': 'create', 'run': ['foo']}], umask=77, dry_run=False)
 
 
-def test_execute_hook_with_dry_run_skips_commands():
+def test_execute_hooks_with_dry_run_skips_commands():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = LOGGING_ANSWER
     flexmock(module).should_receive('interpolate_context').replace_with(
         lambda hook_description, command, context: command
     )
     flexmock(module).should_receive('make_environment').and_return({})
     flexmock(module.borgmatic.execute).should_receive('execute_command').never()
 
-    module.execute_hook([':', 'true'], None, 'config.yaml', 'pre-backup', dry_run=True)
+    module.execute_hooks([{'before': 'create', 'run': ['foo']}], umask=None, dry_run=True)
 
 
-def test_execute_hook_with_empty_commands_does_not_raise():
-    module.execute_hook([], None, 'config.yaml', 'post-backup', dry_run=False)
+def test_execute_hooks_with_empty_commands_does_not_raise():
+    module.execute_hooks([], umask=None, dry_run=True)
 
 
-def test_execute_hook_on_error_logs_as_error():
+def test_execute_hooks_with_error_logs_as_error():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = LOGGING_ANSWER
     flexmock(module).should_receive('interpolate_context').replace_with(
         lambda hook_description, command, context: command
     )
     flexmock(module).should_receive('make_environment').and_return({})
     flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
-        [':'],
+        ['foo'],
         output_log_level=logging.ERROR,
         shell=True,
         environment={},
     ).once()
 
-    module.execute_hook([':'], None, 'config.yaml', 'on-error', dry_run=False)
+    module.execute_hooks([{'after': 'error', 'run': ['foo']}], umask=None, dry_run=False)
 
 
 def test_considered_soft_failure_treats_soft_fail_exit_code_as_soft_fail():