Fix for the "bootstrap" action ignoring the "--verbosity" flag when no configuration is present (#1336).

This commit is contained in:
Dan Helfman
2026-08-01 17:04:31 -07:00
parent dbc3fc356c
commit 8fea29cf8d
5 changed files with 91 additions and 8 deletions
+2
View File
@@ -18,6 +18,8 @@
segment compaction.
* #1334: Add the "CAP_FOWNER" capability to "CapabilityBoundingSet" in the sample systemd service,
so that Borg can open source files without changing file access times.
* #1336: Fix for the "bootstrap" action ignoring the "--verbosity" flag when no configuration is
present.
* Fix a bug in which the "compact" action does not pass a compact threshold of zero to Borg.
2.1.6
+7
View File
@@ -665,6 +665,13 @@ def load_configurations(config_filenames, arguments, overrides=None, resolve_env
config_paths = set()
logs = []
# As a special case for the "bootstrap" action, parse configuration for a non-existent
# configuration file with None for a filename. This sets any command-line arguments into an
# empty configuration dict, so for instance a "--verbosity" flag gets used even if there is no
# configuration file yet.
if 'bootstrap' in arguments and not config_filenames:
config_filenames = (None,)
# Parse and load each configuration file.
for config_filename in config_filenames:
logs.extend(
+11 -2
View File
@@ -108,7 +108,8 @@ def parse_configuration(
rendition of JSON Schema format, arguments as dict from action name to argparse.Namespace, a
sequence of configuration file override strings in the form of "option.suboption=value", and
whether to resolve environment variables, return the parsed configuration as a data structure of
nested dicts and lists corresponding to the schema. Example return value.
nested dicts and lists corresponding to the schema. Apply the given arguments to the config,
modifying it based on the values of those arguments.
Example return value:
@@ -122,13 +123,21 @@ def parse_configuration(
Also return a set of loaded configuration paths and a sequence of logging.LogRecord instances
containing any warnings about the configuration.
If the given config filename is None, then create a configuration dict from whole cloth,
applying the given arguments to it. This is useful for the "bootstrap" action, for which
configuration may not yet exist.
Raise FileNotFoundError if the file does not exist, PermissionError if the user does not
have permissions to read the file, or Validation_error if the config does not match the schema.
'''
config_paths = set()
try:
config = load.load_configuration(config_filename, config_paths)
config = (
load.load_configuration(config_filename, config_paths)
if config_filename
else {'repositories': []}
)
schema = load.load_configuration(schema_filename)
except (ruamel.yaml.error.YAMLError, RecursionError) as error:
raise Validation_error(config_filename, (str(error),))
+27 -6
View File
@@ -15,14 +15,17 @@ def test_schema_filename_returns_plausible_path():
assert schema_path.endswith('/schema.yaml')
def mock_config_and_schema(config_yaml, schema_yaml=None):
def mock_config_and_schema(config_yaml=None, schema_yaml=None):
'''
Set up mocks for the given config config YAML string and the schema YAML string, or the default
schema if no schema is provided. The idea is that that the code under test consumes these mocks
when parsing the configuration.
Set up mocks for the given config config YAML string (if provided) and the schema YAML string or
the default schema if no schema is provided. The idea is that that the code under test consumes
these mocks when parsing the configuration.
'''
config_stream = io.StringIO(config_yaml)
config_stream.name = 'config.yaml'
if config_yaml is None:
config_stream = None
else:
config_stream = io.StringIO(config_yaml)
config_stream.name = 'config.yaml'
if schema_yaml is None:
schema_stream = open(module.schema_filename())
@@ -81,6 +84,24 @@ def test_parse_configuration_transforms_file_into_mapping():
assert logs == []
def test_parse_configuration_with_none_config_filename_creates_configuration_from_whole_cloth():
mock_config_and_schema()
config, config_paths, logs = module.parse_configuration(
None,
'/tmp/schema.yaml',
arguments={'global': flexmock(verbosity=2)},
)
assert config == {
'bootstrap': {},
'repositories': [],
'verbosity': 2,
}
assert config_paths == set()
assert logs == []
def test_parse_configuration_passes_through_quoted_punctuation():
escaped_punctuation = string.punctuation.replace('\\', r'\\').replace('"', r'\"')
+44
View File
@@ -1801,6 +1801,50 @@ def test_load_configurations_logs_critical_for_parse_error():
assert max(log.levelno for log in logs) == logging.CRITICAL
def test_load_configurations_with_bootstrap_action_and_no_configuration_file_creates_configuration_from_whole_cloth():
configuration = flexmock()
test_expected_logs = [flexmock(), flexmock()]
flexmock(module.validate).should_receive('parse_configuration').and_return(
configuration,
[None],
test_expected_logs,
)
configs, config_paths, logs = tuple(
module.load_configurations(
(),
arguments={'bootstrap': flexmock()},
resolve_env=False,
),
)
assert configs == {None: configuration}
assert config_paths == [None]
assert logs
def test_load_configurations_with_bootstrap_action_and_existing_configuration_file_uses_it():
configuration = flexmock()
test_expected_logs = [flexmock(), flexmock()]
flexmock(module.validate).should_receive('parse_configuration').and_return(
configuration,
['/tmp/test.yaml'],
test_expected_logs,
)
configs, config_paths, logs = tuple(
module.load_configurations(
('test.yaml',),
arguments={'bootstrap': flexmock()},
resolve_env=False,
),
)
assert configs == {'test.yaml': configuration}
assert config_paths == ['/tmp/test.yaml']
assert logs
def test_log_record_does_not_raise():
module.log_record(levelno=1, foo='bar', baz='quux')