diff --git a/borgmatic/commands/borgmatic.py b/borgmatic/commands/borgmatic.py index dd37b0bc5..0b5b68154 100644 --- a/borgmatic/commands/borgmatic.py +++ b/borgmatic/commands/borgmatic.py @@ -403,8 +403,7 @@ def collect_configuration_run_summary_logs(config_filenames, args): ) yield logging.makeLogRecord(dict(levelno=logging.CRITICAL, msg=error)) - # TODO: What to do if the given repository doesn't match any configured repositories (across all config - # files)? Where to validate and error on that? + validate.guard_configuration_contains_repository(args.repository, configs) for config_filename, config in configs.items(): try: diff --git a/borgmatic/config/validate.py b/borgmatic/config/validate.py index 231e15114..ba1306c0d 100644 --- a/borgmatic/config/validate.py +++ b/borgmatic/config/validate.py @@ -107,3 +107,29 @@ def parse_configuration(config_filename, schema_filename): apply_logical_validation(config_filename, parsed_result) return parsed_result + + +def guard_configuration_contains_repository(repository, configurations): + ''' + Given a repository path and a dict mapping from config filename to corresponding parsed config + dict, ensure that the repository is declared exactly once in all of the configurations. + + Raise ValueError if the repository is not found in a configuration, or is declared multiple + times. + ''' + if not repository: + return + + count = len( + tuple( + config_repository + for config in configurations.values() + for config_repository in config['repositories'] + if repository == config_repository + ) + ) + + if count == 0: + raise ValueError('Repository {} not found in configuration files'.format(repository)) + if count > 1: + raise ValueError('Repository {} found in multiple configuration files'.format(repository)) diff --git a/tests/unit/commands/test_borgmatic.py b/tests/unit/commands/test_borgmatic.py index 13fe13aab..278b6be6b 100644 --- a/tests/unit/commands/test_borgmatic.py +++ b/tests/unit/commands/test_borgmatic.py @@ -49,38 +49,42 @@ def test_run_commands_handles_multiple_json_outputs_in_array(): def test_collect_configuration_run_summary_logs_info_for_success(): flexmock(module.validate).should_receive('parse_configuration').and_return({'test.yaml': {}}) + flexmock(module.validate).should_receive('guard_configuration_contains_repository') flexmock(module).should_receive('run_configuration') + args = flexmock(repository=None) - logs = tuple(module.collect_configuration_run_summary_logs(('test.yaml',), args=())) + logs = tuple(module.collect_configuration_run_summary_logs(('test.yaml',), args=args)) assert any(log for log in logs if log.levelno == module.logging.INFO) def test_collect_configuration_run_summary_logs_critical_for_parse_error(): flexmock(module.validate).should_receive('parse_configuration').and_raise(ValueError) - flexmock(module).should_receive('run_configuration') + flexmock(module.validate).should_receive('guard_configuration_contains_repository') + args = flexmock(repository=None) - logs = tuple(module.collect_configuration_run_summary_logs(('test.yaml',), args=())) + logs = tuple(module.collect_configuration_run_summary_logs(('test.yaml',), args=args)) assert any(log for log in logs if log.levelno == module.logging.CRITICAL) def test_collect_configuration_run_summary_logs_critical_for_run_error(): flexmock(module.validate).should_receive('parse_configuration').and_return({'test.yaml': {}}) + flexmock(module.validate).should_receive('guard_configuration_contains_repository') flexmock(module).should_receive('run_configuration').and_raise(ValueError) + args = flexmock(repository=None) - logs = tuple(module.collect_configuration_run_summary_logs(('test.yaml',), args=())) + logs = tuple(module.collect_configuration_run_summary_logs(('test.yaml',), args=args)) assert any(log for log in logs if log.levelno == module.logging.CRITICAL) def test_collect_configuration_run_summary_logs_critical_for_missing_configs(): flexmock(module.validate).should_receive('parse_configuration').and_return({'test.yaml': {}}) + flexmock(module.validate).should_receive('guard_configuration_contains_repository') + flexmock(module).should_receive('run_configuration') + args = flexmock(config_paths=(), repository=None) - logs = tuple( - module.collect_configuration_run_summary_logs( - config_filenames=(), args=flexmock(config_paths=()) - ) - ) + logs = tuple(module.collect_configuration_run_summary_logs(config_filenames=(), args=args)) assert any(log for log in logs if log.levelno == module.logging.CRITICAL) diff --git a/tests/unit/config/test_validate.py b/tests/unit/config/test_validate.py index a542e21f3..41d5c99e5 100644 --- a/tests/unit/config/test_validate.py +++ b/tests/unit/config/test_validate.py @@ -90,3 +90,33 @@ def test_apply_logical_validation_does_not_raise_or_warn_if_archive_name_format_ def test_apply_logical_validation_does_not_raise_otherwise(): module.apply_logical_validation('config.yaml', {'retention': {'keep_secondly': 1000}}) + + +def test_guard_configuration_contains_repository_does_not_raise_when_repository_in_config(): + module.guard_configuration_contains_repository( + repository='repo', configurations={'config.yaml': {'repositories': ['repo']}} + ) + + +def test_guard_configuration_contains_repository_does_not_raise_when_repository_not_given(): + module.guard_configuration_contains_repository( + repository=None, configurations={'config.yaml': {'repositories': ['repo']}} + ) + + +def test_guard_configuration_contains_repository_errors_when_repository_missing_from_config(): + with pytest.raises(ValueError): + module.guard_configuration_contains_repository( + repository='nope', configurations={'config.yaml': {'repositories': ['repo', 'repo2']}} + ) + + +def test_guard_configuration_contains_repository_errors_when_repository_matches_config_twice(): + with pytest.raises(ValueError): + module.guard_configuration_contains_repository( + repository='repo', + configurations={ + 'config.yaml': {'repositories': ['repo', 'repo2']}, + 'other.yaml': {'repositories': ['repo']}, + }, + )