tests: test file/line attribution in config errors
This commit is contained in:
parent
970b558f7f
commit
15910debb2
@ -187,7 +187,6 @@ def get_section(self, section):
|
|||||||
return self.sections[section]
|
return self.sections[section]
|
||||||
|
|
||||||
def write_section(self, section):
|
def write_section(self, section):
|
||||||
import jsonschema
|
|
||||||
filename = self.get_section_filename(section)
|
filename = self.get_section_filename(section)
|
||||||
data = self.get_section(section)
|
data = self.get_section(section)
|
||||||
try:
|
try:
|
||||||
@ -195,8 +194,6 @@ def write_section(self, section):
|
|||||||
with open(filename, 'w') as f:
|
with open(filename, 'w') as f:
|
||||||
_validate_section(data, section_schemas[section])
|
_validate_section(data, section_schemas[section])
|
||||||
syaml.dump(data, stream=f, default_flow_style=False)
|
syaml.dump(data, stream=f, default_flow_style=False)
|
||||||
except jsonschema.ValidationError as e:
|
|
||||||
raise ConfigSanityError(e, data)
|
|
||||||
except (yaml.YAMLError, IOError) as e:
|
except (yaml.YAMLError, IOError) as e:
|
||||||
raise ConfigFileError(
|
raise ConfigFileError(
|
||||||
"Error writing to config file: '%s'" % str(e))
|
"Error writing to config file: '%s'" % str(e))
|
||||||
@ -531,7 +528,8 @@ def scopes():
|
|||||||
def _validate_section_name(section):
|
def _validate_section_name(section):
|
||||||
"""Exit if the section is not a valid section."""
|
"""Exit if the section is not a valid section."""
|
||||||
if section not in section_schemas:
|
if section not in section_schemas:
|
||||||
tty.die("Invalid config section: '%s'. Options are: %s"
|
raise ConfigSectionError(
|
||||||
|
"Invalid config section: '%s'. Options are: %s"
|
||||||
% (section, " ".join(section_schemas.keys())))
|
% (section, " ".join(section_schemas.keys())))
|
||||||
|
|
||||||
|
|
||||||
@ -697,6 +695,10 @@ class ConfigError(SpackError):
|
|||||||
"""Superclass for all Spack config related errors."""
|
"""Superclass for all Spack config related errors."""
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigSectionError(ConfigError):
|
||||||
|
"""Error for referring to a bad config section name in a configuration."""
|
||||||
|
|
||||||
|
|
||||||
class ConfigFileError(ConfigError):
|
class ConfigFileError(ConfigError):
|
||||||
"""Issue reading or accessing a configuration file."""
|
"""Issue reading or accessing a configuration file."""
|
||||||
|
|
||||||
@ -705,10 +707,38 @@ class ConfigFormatError(ConfigError):
|
|||||||
"""Raised when a configuration format does not match its schema."""
|
"""Raised when a configuration format does not match its schema."""
|
||||||
|
|
||||||
def __init__(self, validation_error, data):
|
def __init__(self, validation_error, data):
|
||||||
# Try to get line number from erroneous instance and its parent
|
location = '<unknown file>'
|
||||||
instance_mark = getattr(validation_error.instance, '_start_mark', None)
|
mark = self._get_mark(validation_error, data)
|
||||||
parent_mark = getattr(validation_error.parent, '_start_mark', None)
|
if mark:
|
||||||
path = [str(s) for s in getattr(validation_error, 'path', None)]
|
location = '%s' % mark.name
|
||||||
|
if mark.line is not None:
|
||||||
|
location += ':%d' % (mark.line + 1)
|
||||||
|
|
||||||
|
message = '%s: %s' % (location, validation_error.message)
|
||||||
|
super(ConfigError, self).__init__(message)
|
||||||
|
|
||||||
|
def _get_mark(self, validation_error, data):
|
||||||
|
"""Get the file/line mark fo a validation error from a Spack YAML file.
|
||||||
|
"""
|
||||||
|
def _get_mark_or_first_member_mark(obj):
|
||||||
|
# mark of object itelf
|
||||||
|
mark = getattr(obj, '_start_mark', None)
|
||||||
|
if mark:
|
||||||
|
return mark
|
||||||
|
|
||||||
|
# mark of first member if it is a container
|
||||||
|
if isinstance(obj, (list, dict)):
|
||||||
|
first_member = next(iter(obj), None)
|
||||||
|
if first_member:
|
||||||
|
mark = getattr(first_member, '_start_mark', None)
|
||||||
|
if mark:
|
||||||
|
return mark
|
||||||
|
|
||||||
|
# Try various places, starting with instance and parent
|
||||||
|
for obj in (validation_error.instance, validation_error.parent):
|
||||||
|
mark = _get_mark_or_first_member_mark(obj)
|
||||||
|
if mark:
|
||||||
|
return mark
|
||||||
|
|
||||||
def get_path(path, data):
|
def get_path(path, data):
|
||||||
if path:
|
if path:
|
||||||
@ -719,29 +749,18 @@ def get_path(path, data):
|
|||||||
# Try really hard to get the parent (which sometimes is not
|
# Try really hard to get the parent (which sometimes is not
|
||||||
# set) This digs it out of the validated structure if it's not
|
# set) This digs it out of the validated structure if it's not
|
||||||
# on the validation_error.
|
# on the validation_error.
|
||||||
if path and not parent_mark:
|
path = validation_error.path
|
||||||
parent_path = list(path)[:-1]
|
if path:
|
||||||
parent = get_path(parent_path, data)
|
parent = get_path(list(path)[:-1], data)
|
||||||
if path[-1] in parent:
|
if path[-1] in parent:
|
||||||
if isinstance(parent, dict):
|
if isinstance(parent, dict):
|
||||||
keylist = parent.keys()
|
keylist = list(parent.keys())
|
||||||
elif isinstance(parent, list):
|
elif isinstance(parent, list):
|
||||||
keylist = parent
|
keylist = parent
|
||||||
idx = keylist.index(path[-1])
|
idx = keylist.index(path[-1])
|
||||||
parent_mark = getattr(keylist[idx], '_start_mark', None)
|
mark = getattr(keylist[idx], '_start_mark', None)
|
||||||
|
if mark:
|
||||||
|
return mark
|
||||||
|
|
||||||
if instance_mark:
|
# give up and return None if nothing worked
|
||||||
location = '%s:%d' % (instance_mark.name, instance_mark.line + 1)
|
return None
|
||||||
elif parent_mark:
|
|
||||||
location = '%s:%d' % (parent_mark.name, parent_mark.line + 1)
|
|
||||||
elif path:
|
|
||||||
location = 'At ' + ':'.join(path)
|
|
||||||
else:
|
|
||||||
location = '<unknown line>'
|
|
||||||
|
|
||||||
message = '%s: %s' % (location, validation_error.message)
|
|
||||||
super(ConfigError, self).__init__(message)
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigSanityError(ConfigFormatError):
|
|
||||||
"""Same as ConfigFormatError, raised when config is written by Spack."""
|
|
||||||
|
@ -518,7 +518,7 @@ def test_internal_config_from_data():
|
|||||||
|
|
||||||
|
|
||||||
def test_keys_are_ordered():
|
def test_keys_are_ordered():
|
||||||
|
"""Test that keys in Spack YAML files retain their order from the file."""
|
||||||
expected_order = (
|
expected_order = (
|
||||||
'bin',
|
'bin',
|
||||||
'man',
|
'man',
|
||||||
@ -543,3 +543,74 @@ def test_keys_are_ordered():
|
|||||||
|
|
||||||
for actual, expected in zip(prefix_inspections, expected_order):
|
for actual, expected in zip(prefix_inspections, expected_order):
|
||||||
assert actual == expected
|
assert actual == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_format_error(mutable_config):
|
||||||
|
"""This is raised when we try to write a bad configuration."""
|
||||||
|
with pytest.raises(spack.config.ConfigFormatError):
|
||||||
|
spack.config.set('compilers', {'bad': 'data'}, scope='site')
|
||||||
|
|
||||||
|
|
||||||
|
def get_config_error(filename, schema, yaml_string):
|
||||||
|
"""Parse a YAML string and return the resulting ConfigFormatError.
|
||||||
|
|
||||||
|
Fail if there is no ConfigFormatError
|
||||||
|
"""
|
||||||
|
with open(filename, 'w') as f:
|
||||||
|
f.write(yaml_string)
|
||||||
|
|
||||||
|
# parse and return error, or fail.
|
||||||
|
try:
|
||||||
|
spack.config._read_config_file(filename, schema)
|
||||||
|
except spack.config.ConfigFormatError as e:
|
||||||
|
return e
|
||||||
|
else:
|
||||||
|
pytest.fail('ConfigFormatError was not raised!')
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_parse_dict_in_list(tmpdir):
|
||||||
|
with tmpdir.as_cwd():
|
||||||
|
e = get_config_error(
|
||||||
|
'repos.yaml', spack.schema.repos.schema, """\
|
||||||
|
repos:
|
||||||
|
- https://foobar.com/foo
|
||||||
|
- https://foobar.com/bar
|
||||||
|
- error:
|
||||||
|
- abcdef
|
||||||
|
- https://foobar.com/baz
|
||||||
|
""")
|
||||||
|
assert "repos.yaml:4" in str(e)
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_parse_str_not_bool(tmpdir):
|
||||||
|
with tmpdir.as_cwd():
|
||||||
|
e = get_config_error(
|
||||||
|
'config.yaml', spack.schema.config.schema, """\
|
||||||
|
config:
|
||||||
|
verify_ssl: False
|
||||||
|
checksum: foobar
|
||||||
|
dirty: True
|
||||||
|
""")
|
||||||
|
assert "config.yaml:3" in str(e)
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_parse_list_in_dict(tmpdir):
|
||||||
|
with tmpdir.as_cwd():
|
||||||
|
e = get_config_error(
|
||||||
|
'mirrors.yaml', spack.schema.mirrors.schema, """\
|
||||||
|
mirrors:
|
||||||
|
foo: http://foobar.com/baz
|
||||||
|
bar: http://barbaz.com/foo
|
||||||
|
baz: http://bazfoo.com/bar
|
||||||
|
travis: [1, 2, 3]
|
||||||
|
""")
|
||||||
|
assert "mirrors.yaml:5" in str(e)
|
||||||
|
|
||||||
|
|
||||||
|
def test_bad_config_section(config):
|
||||||
|
"""Test that getting or setting a bad section gives an error."""
|
||||||
|
with pytest.raises(spack.config.ConfigSectionError):
|
||||||
|
spack.config.set('foobar', 'foobar')
|
||||||
|
|
||||||
|
with pytest.raises(spack.config.ConfigSectionError):
|
||||||
|
spack.config.get('foobar')
|
||||||
|
@ -254,6 +254,26 @@ def config(configuration_dir):
|
|||||||
spack.package_prefs.PackagePrefs.clear_caches()
|
spack.package_prefs.PackagePrefs.clear_caches()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='function')
|
||||||
|
def mutable_config(tmpdir_factory, configuration_dir, config):
|
||||||
|
"""Like config, but tests can modify the configuration."""
|
||||||
|
spack.package_prefs.PackagePrefs.clear_caches()
|
||||||
|
|
||||||
|
mutable_dir = tmpdir_factory.mktemp('mutable_config').join('tmp')
|
||||||
|
configuration_dir.copy(mutable_dir)
|
||||||
|
|
||||||
|
real_configuration = spack.config.config
|
||||||
|
|
||||||
|
spack.config.config = spack.config.Configuration(
|
||||||
|
*[spack.config.ConfigScope(name, str(mutable_dir))
|
||||||
|
for name in ['site', 'system', 'user']])
|
||||||
|
|
||||||
|
yield spack.config.config
|
||||||
|
|
||||||
|
spack.config.config = real_configuration
|
||||||
|
spack.package_prefs.PackagePrefs.clear_caches()
|
||||||
|
|
||||||
|
|
||||||
def _populate(mock_db):
|
def _populate(mock_db):
|
||||||
"""Populate a mock database with packages.
|
"""Populate a mock database with packages.
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user