# Copyright 2013-2022 Lawrence Livermore National Security, LLC and other
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
import collections
import getpass
import os
import sys
import tempfile
import pytest
from six import StringIO
from llnl.util.filesystem import getuid, mkdirp, touch
import spack.config
import spack.environment as ev
import spack.main
import spack.paths
import spack.repo
import spack.schema.compilers
import spack.schema.config
import spack.schema.env
import spack.schema.mirrors
import spack.schema.packages
import spack.schema.repos
import spack.util.path as spack_path
import spack.util.spack_yaml as syaml
# sample config data
config_low = {
'config': {
'install_tree': {'root': 'install_tree_path'},
'build_stage': ['path1', 'path2', 'path3']}}
config_override_all = {
'config:': {
'install_tree:': {'root': 'override_all'}}}
config_override_key = {
'config': {
'install_tree:': {'root': 'override_key'}}}
config_merge_list = {
'config': {
'build_stage': ['patha', 'pathb']}}
config_override_list = {
'config': {
'build_stage:': ['pathd', 'pathe']}}
config_merge_dict = {
'config': {
'info': {
'a': 3,
'b': 4}}}
config_override_dict = {
'config': {
'info:': {
'a': 7,
'c': 9}}}
[docs]@pytest.fixture()
def write_config_file(tmpdir):
"""Returns a function that writes a config file."""
def _write(config, data, scope):
config_yaml = tmpdir.join(scope, config + '.yaml')
config_yaml.ensure()
with config_yaml.open('w') as f:
syaml.dump_config(data, f)
return _write
[docs]@pytest.fixture()
def env_yaml(tmpdir):
"""Return a sample env.yaml for test purposes"""
env_yaml = str(tmpdir.join("env.yaml"))
with open(env_yaml, 'w') as f:
f.write("""\
env:
config:
verify_ssl: False
dirty: False
packages:
libelf:
compiler: [ 'gcc@4.5.3' ]
repos:
- /x/y/z
""")
return env_yaml
[docs]def cross_plat_join(*pths):
"""os.path.join does not prepend paths to other paths
beginning with a Windows drive label i.e. D:\\
"""
return os.sep.join([pth for pth in pths])
[docs]def check_compiler_config(comps, *compiler_names):
"""Check that named compilers in comps match Spack's config."""
config = spack.config.get('compilers')
compiler_list = ['cc', 'cxx', 'f77', 'fc']
flag_list = ['cflags', 'cxxflags', 'fflags', 'cppflags',
'ldflags', 'ldlibs']
param_list = ['modules', 'paths', 'spec', 'operating_system']
for compiler in config:
conf = compiler['compiler']
if conf['spec'] in compiler_names:
comp = next((c['compiler'] for c in comps if
c['compiler']['spec'] == conf['spec']), None)
if not comp:
raise ValueError('Bad config spec')
for p in param_list:
assert conf[p] == comp[p]
for f in flag_list:
expected = comp.get('flags', {}).get(f, None)
actual = conf.get('flags', {}).get(f, None)
assert expected == actual
for c in compiler_list:
expected = comp['paths'][c]
actual = conf['paths'][c]
assert expected == actual
#
# Some sample compiler config data and tests.
#
a_comps = {
'compilers': [
{'compiler': {
'paths': {
"cc": "/gcc473",
"cxx": "/g++473",
"f77": None,
"fc": None
},
'modules': None,
'spec': 'gcc@4.7.3',
'operating_system': 'CNL10'
}},
{'compiler': {
'paths': {
"cc": "/gcc450",
"cxx": "/g++450",
"f77": 'gfortran',
"fc": 'gfortran'
},
'modules': None,
'spec': 'gcc@4.5.0',
'operating_system': 'CNL10'
}},
{'compiler': {
'paths': {
"cc": "/gcc422",
"cxx": "/g++422",
"f77": 'gfortran',
"fc": 'gfortran'
},
'flags': {
"cppflags": "-O0 -fpic",
"fflags": "-f77",
},
'modules': None,
'spec': 'gcc@4.2.2',
'operating_system': 'CNL10'
}},
{'compiler': {
'paths': {
"cc": "<overwritten>",
"cxx": "<overwritten>",
"f77": '<overwritten>',
"fc": '<overwritten>'},
'modules': None,
'spec': 'clang@3.3',
'operating_system': 'CNL10'
}}
]
}
b_comps = {
'compilers': [
{'compiler': {
'paths': {
"cc": "/icc100",
"cxx": "/icp100",
"f77": None,
"fc": None
},
'modules': None,
'spec': 'icc@10.0',
'operating_system': 'CNL10'
}},
{'compiler': {
'paths': {
"cc": "/icc111",
"cxx": "/icp111",
"f77": 'ifort',
"fc": 'ifort'
},
'modules': None,
'spec': 'icc@11.1',
'operating_system': 'CNL10'
}},
{'compiler': {
'paths': {
"cc": "/icc123",
"cxx": "/icp123",
"f77": 'ifort',
"fc": 'ifort'
},
'flags': {
"cppflags": "-O3",
"fflags": "-f77rtl",
},
'modules': None,
'spec': 'icc@12.3',
'operating_system': 'CNL10'
}},
{'compiler': {
'paths': {
"cc": "<overwritten>",
"cxx": "<overwritten>",
"f77": '<overwritten>',
"fc": '<overwritten>'},
'modules': None,
'spec': 'clang@3.3',
'operating_system': 'CNL10'
}}
]
}
[docs]@pytest.fixture()
def compiler_specs():
"""Returns a couple of compiler specs needed for the tests"""
a = [ac['compiler']['spec'] for ac in a_comps['compilers']]
b = [bc['compiler']['spec'] for bc in b_comps['compilers']]
CompilerSpecs = collections.namedtuple('CompilerSpecs', ['a', 'b'])
return CompilerSpecs(a=a, b=b)
[docs]def test_write_key_in_memory(mock_low_high_config, compiler_specs):
# Write b_comps "on top of" a_comps.
spack.config.set('compilers', a_comps['compilers'], scope='low')
spack.config.set('compilers', b_comps['compilers'], scope='high')
# Make sure the config looks how we expect.
check_compiler_config(a_comps['compilers'], *compiler_specs.a)
check_compiler_config(b_comps['compilers'], *compiler_specs.b)
[docs]def test_write_key_to_disk(mock_low_high_config, compiler_specs):
# Write b_comps "on top of" a_comps.
spack.config.set('compilers', a_comps['compilers'], scope='low')
spack.config.set('compilers', b_comps['compilers'], scope='high')
# Clear caches so we're forced to read from disk.
spack.config.config.clear_caches()
# Same check again, to ensure consistency.
check_compiler_config(a_comps['compilers'], *compiler_specs.a)
check_compiler_config(b_comps['compilers'], *compiler_specs.b)
[docs]def test_write_to_same_priority_file(mock_low_high_config, compiler_specs):
# Write b_comps in the same file as a_comps.
spack.config.set('compilers', a_comps['compilers'], scope='low')
spack.config.set('compilers', b_comps['compilers'], scope='low')
# Clear caches so we're forced to read from disk.
spack.config.config.clear_caches()
# Same check again, to ensure consistency.
check_compiler_config(a_comps['compilers'], *compiler_specs.a)
check_compiler_config(b_comps['compilers'], *compiler_specs.b)
#
# Sample repo data and tests
#
repos_low = {'repos': ["/some/path"]}
repos_high = {'repos': ["/some/other/path"]}
# Test setting config values via path in filename
[docs]def test_add_config_path(mutable_config):
# Try setting a new install tree root
path = "config:install_tree:root:/path/to/config.yaml"
spack.config.add(path)
set_value = spack.config.get('config')['install_tree']['root']
assert set_value == '/path/to/config.yaml'
# Now a package:all setting
path = "packages:all:compiler:[gcc]"
spack.config.add(path)
compilers = spack.config.get('packages')['all']['compiler']
assert "gcc" in compilers
[docs]@pytest.mark.regression('17543,23259')
def test_add_config_path_with_enumerated_type(mutable_config):
spack.config.add("config:concretizer:clingo")
assert spack.config.get('config')['concretizer'] == "clingo"
spack.config.add("config:concretizer:original")
assert spack.config.get('config')['concretizer'] == "original"
with pytest.raises(spack.config.ConfigError):
spack.config.add("config:concretizer:foo")
[docs]def test_add_config_filename(mock_low_high_config, tmpdir):
config_yaml = tmpdir.join('config-filename.yaml')
config_yaml.ensure()
with config_yaml.open('w') as f:
syaml.dump_config(config_low, f)
spack.config.add_from_file(str(config_yaml), scope="low")
assert "build_stage" in spack.config.get('config')
build_stages = spack.config.get('config')['build_stage']
for stage in config_low['config']['build_stage']:
assert stage in build_stages
# repos
[docs]def test_write_list_in_memory(mock_low_high_config):
spack.config.set('repos', repos_low['repos'], scope='low')
spack.config.set('repos', repos_high['repos'], scope='high')
config = spack.config.get('repos')
assert config == repos_high['repos'] + repos_low['repos']
[docs]class MockEnv(object):
def __init__(self, path):
self.path = path
[docs]def test_substitute_config_variables(mock_low_high_config, monkeypatch):
prefix = spack.paths.prefix.lstrip('/')
assert cross_plat_join(
os.sep + os.path.join('foo', 'bar', 'baz'), prefix
) == spack_path.canonicalize_path('/foo/bar/baz/$spack')
assert cross_plat_join(
spack.paths.prefix, os.path.join('foo', 'bar', 'baz')
) == spack_path.canonicalize_path('$spack/foo/bar/baz/')
assert cross_plat_join(
os.sep + os.path.join('foo', 'bar', 'baz'),
prefix, os.path.join('foo', 'bar', 'baz')
) == spack_path.canonicalize_path('/foo/bar/baz/$spack/foo/bar/baz/')
assert cross_plat_join(
os.sep + os.path.join('foo', 'bar', 'baz'), prefix
) == spack_path.canonicalize_path('/foo/bar/baz/${spack}')
assert cross_plat_join(
spack.paths.prefix, os.path.join('foo', 'bar', 'baz')
) == spack_path.canonicalize_path('${spack}/foo/bar/baz/')
assert cross_plat_join(
os.sep + os.path.join('foo', 'bar', 'baz'),
prefix, os.path.join('foo', 'bar', 'baz')
) == spack_path.canonicalize_path('/foo/bar/baz/${spack}/foo/bar/baz/')
assert cross_plat_join(
os.sep + os.path.join('foo', 'bar', 'baz'),
prefix, os.path.join('foo', 'bar', 'baz')
) != spack_path.canonicalize_path('/foo/bar/baz/${spack/foo/bar/baz/')
# $env replacement is a no-op when no environment is active
assert spack_path.canonicalize_path(
os.sep + os.path.join('foo', 'bar', 'baz', '$env')
) == os.sep + os.path.join('foo', 'bar', 'baz', '$env')
# Fake an active environment and $env is replaced properly
fake_env_path = os.sep + os.path.join('quux', 'quuux')
monkeypatch.setattr(ev, 'active_environment',
lambda: MockEnv(fake_env_path))
assert spack_path.canonicalize_path(
'$env/foo/bar/baz'
) == os.path.join(fake_env_path, os.path.join('foo', 'bar', 'baz'))
# relative paths without source information are relative to cwd
assert spack_path.canonicalize_path(
os.path.join('foo', 'bar', 'baz')
) == os.path.abspath(os.path.join('foo', 'bar', 'baz'))
# relative paths with source information are relative to the file
spack.config.set(
'modules:default',
{'roots': {'lmod': os.path.join('foo', 'bar', 'baz')}}, scope='low')
spack.config.config.clear_caches()
path = spack.config.get('modules:default:roots:lmod')
assert spack_path.canonicalize_path(path) == os.path.normpath(
os.path.join(mock_low_high_config.scopes['low'].path,
os.path.join('foo', 'bar', 'baz')))
packages_merge_low = {
'packages': {
'foo': {
'variants': ['+v1']
},
'bar': {
'variants': ['+v2']
}
}
}
packages_merge_high = {
'packages': {
'foo': {
'version': ['a']
},
'bar': {
'version': ['b'],
'variants': ['+v3']
},
'baz': {
'version': ['c']
}
}
}
[docs]@pytest.mark.regression('7924')
def test_merge_with_defaults(mock_low_high_config, write_config_file):
"""This ensures that specified preferences merge with defaults as
expected. Originally all defaults were initialized with the
exact same object, which led to aliasing problems. Therefore
the test configs used here leave 'version' blank for multiple
packages in 'packages_merge_low'.
"""
write_config_file('packages', packages_merge_low, 'low')
write_config_file('packages', packages_merge_high, 'high')
cfg = spack.config.get('packages')
assert cfg['foo']['version'] == ['a']
assert cfg['bar']['version'] == ['b']
assert cfg['baz']['version'] == ['c']
[docs]def test_substitute_user(mock_low_high_config):
user = getpass.getuser()
assert os.sep + os.path.join('foo', 'bar') + os.sep \
+ user + os.sep \
+ 'baz' == spack_path.canonicalize_path(
os.sep + os.path.join('foo', 'bar', '$user', 'baz')
)
[docs]def test_substitute_user_cache(mock_low_high_config):
user_cache_path = spack.paths.user_cache_path
assert user_cache_path + os.sep + 'baz' == spack_path.canonicalize_path(
os.path.join('$user_cache_path', 'baz')
)
[docs]def test_substitute_tempdir(mock_low_high_config):
tempdir = tempfile.gettempdir()
assert tempdir == spack_path.canonicalize_path('$tempdir')
assert tempdir + os.sep + \
os.path.join('foo', 'bar', 'baz') == spack_path.canonicalize_path(
os.path.join('$tempdir', 'foo', 'bar', 'baz')
)
PAD_STRING = spack.util.path.SPACK_PATH_PADDING_CHARS
MAX_PATH_LEN = spack.util.path.get_system_path_max()
MAX_PADDED_LEN = MAX_PATH_LEN - spack.util.path.SPACK_MAX_INSTALL_PATH_LENGTH
reps = [PAD_STRING for _ in range((MAX_PADDED_LEN // len(PAD_STRING) + 1) + 2)]
full_padded_string = os.path.join(
os.sep + 'path', os.sep.join(reps))[:MAX_PADDED_LEN]
[docs]@pytest.mark.parametrize('config_settings,expected', [
([], [None, None, None]),
([['config:install_tree:root', os.sep + 'path']], [os.sep + 'path', None, None]),
([['config:install_tree', os.sep + 'path']], [os.sep + 'path', None, None]),
([['config:install_tree:projections', {'all': '{name}'}]],
[None, None, {'all': '{name}'}]),
([['config:install_path_scheme', '{name}']],
[None, None, {'all': '{name}'}]),
])
def test_parse_install_tree(config_settings, expected, mutable_config):
expected_root = expected[0] or spack.store.default_install_tree_root
expected_unpadded_root = expected[1] or expected_root
expected_proj = expected[2] or spack.directory_layout.default_projections
# config settings is a list of 2-element lists, [path, value]
# where path is a config path and value is the value to set at that path
# these can be "splatted" in as the arguments to config.set
for config_setting in config_settings:
mutable_config.set(*config_setting)
config_dict = mutable_config.get('config')
root, unpadded_root, projections = spack.store.parse_install_tree(
config_dict)
assert root == expected_root
assert unpadded_root == expected_unpadded_root
assert projections == expected_proj
[docs]@pytest.mark.skipif(sys.platform == 'win32',
reason='Padding unsupported on Windows')
@pytest.mark.parametrize('config_settings,expected', [
([['config:install_tree:root', os.sep + 'path'],
['config:install_tree:padded_length', 11]],
[os.path.join(os.sep + 'path', PAD_STRING[:5]), os.sep + 'path', None]),
([['config:install_tree:root', '/path/$padding:11']],
[os.path.join(os.sep + 'path', PAD_STRING[:5]), os.sep + 'path', None]),
([['config:install_tree', '/path/${padding:11}']],
[os.path.join(os.sep + 'path', PAD_STRING[:5]), os.sep + 'path', None]),
([['config:install_tree:padded_length', False]], [None, None, None]),
([['config:install_tree:padded_length', True],
['config:install_tree:root', os.sep + 'path']],
[full_padded_string, os.sep + 'path', None]),
([['config:install_tree:', os.sep + 'path$padding']],
[full_padded_string, os.sep + 'path', None]),
([['config:install_tree:', os.sep + 'path' + os.sep + '${padding}']],
[full_padded_string, os.sep + 'path', None]),
])
def test_parse_install_tree_padded(config_settings, expected, mutable_config):
expected_root = expected[0] or spack.store.default_install_tree_root
expected_unpadded_root = expected[1] or expected_root
expected_proj = expected[2] or spack.directory_layout.default_projections
# config settings is a list of 2-element lists, [path, value]
# where path is a config path and value is the value to set at that path
# these can be "splatted" in as the arguments to config.set
for config_setting in config_settings:
mutable_config.set(*config_setting)
config_dict = mutable_config.get('config')
root, unpadded_root, projections = spack.store.parse_install_tree(
config_dict)
assert root == expected_root
assert unpadded_root == expected_unpadded_root
assert projections == expected_proj
[docs]def test_read_config(mock_low_high_config, write_config_file):
write_config_file('config', config_low, 'low')
assert spack.config.get('config') == config_low['config']
[docs]def test_read_config_override_all(mock_low_high_config, write_config_file):
write_config_file('config', config_low, 'low')
write_config_file('config', config_override_all, 'high')
assert spack.config.get('config') == {
'install_tree': {
'root': 'override_all'
}
}
[docs]def test_read_config_override_key(mock_low_high_config, write_config_file):
write_config_file('config', config_low, 'low')
write_config_file('config', config_override_key, 'high')
assert spack.config.get('config') == {
'install_tree': {
'root': 'override_key'
},
'build_stage': ['path1', 'path2', 'path3']
}
[docs]def test_read_config_merge_list(mock_low_high_config, write_config_file):
write_config_file('config', config_low, 'low')
write_config_file('config', config_merge_list, 'high')
assert spack.config.get('config') == {
'install_tree': {
'root': 'install_tree_path'
},
'build_stage': ['patha', 'pathb', 'path1', 'path2', 'path3']
}
[docs]def test_read_config_override_list(mock_low_high_config, write_config_file):
write_config_file('config', config_low, 'low')
write_config_file('config', config_override_list, 'high')
assert spack.config.get('config') == {
'install_tree': {
'root': 'install_tree_path'
},
'build_stage': config_override_list['config']['build_stage:']
}
[docs]def test_ordereddict_merge_order():
""""Test that source keys come before dest keys in merge_yaml results."""
source = syaml.syaml_dict([
("k1", "v1"),
("k2", "v2"),
("k3", "v3"),
])
dest = syaml.syaml_dict([
("k4", "v4"),
("k3", "WRONG"),
("k5", "v5"),
])
result = spack.config.merge_yaml(dest, source)
assert "WRONG" not in result.values()
expected_keys = ["k1", "k2", "k3", "k4", "k5"]
expected_items = [
("k1", "v1"), ("k2", "v2"), ("k3", "v3"), ("k4", "v4"), ("k5", "v5")
]
assert expected_keys == list(result.keys())
assert expected_items == list(result.items())
[docs]def test_list_merge_order():
""""Test that source lists are prepended to dest."""
source = ["a", "b", "c"]
dest = ["d", "e", "f"]
result = spack.config.merge_yaml(dest, source)
assert ["a", "b", "c", "d", "e", "f"] == result
[docs]def test_internal_config_update(mock_low_high_config, write_config_file):
write_config_file('config', config_low, 'low')
before = mock_low_high_config.get('config')
assert before['install_tree']['root'] == 'install_tree_path'
# add an internal configuration scope
scope = spack.config.InternalConfigScope('command_line')
assert 'InternalConfigScope' in repr(scope)
mock_low_high_config.push_scope(scope)
command_config = mock_low_high_config.get('config', scope='command_line')
command_config['install_tree'] = {'root': 'foo/bar'}
mock_low_high_config.set('config', command_config, scope='command_line')
after = mock_low_high_config.get('config')
assert after['install_tree']['root'] == 'foo/bar'
[docs]def test_internal_config_filename(mock_low_high_config, write_config_file):
write_config_file('config', config_low, 'low')
mock_low_high_config.push_scope(
spack.config.InternalConfigScope('command_line'))
with pytest.raises(NotImplementedError):
mock_low_high_config.get_config_filename('command_line', 'config')
[docs]def test_mark_internal():
data = {
'config': {
'bool': False,
'int': 6,
'numbers': [1, 2, 3],
'string': 'foo',
'dict': {
'more_numbers': [1, 2, 3],
'another_string': 'foo',
'another_int': 7,
}
}
}
marked = spack.config._mark_internal(data, 'x')
# marked version should be equal to the original
assert data == marked
def assert_marked(obj):
if type(obj) is bool:
return # can't subclass bool, so can't mark it
assert hasattr(obj, '_start_mark') and obj._start_mark.name == 'x'
assert hasattr(obj, '_end_mark') and obj._end_mark.name == 'x'
# everything in the marked version should have marks
checks = (marked.keys(), marked.values(),
marked['config'].keys(), marked['config'].values(),
marked['config']['numbers'],
marked['config']['dict'].keys(),
marked['config']['dict'].values(),
marked['config']['dict']['more_numbers'])
for seq in checks:
for obj in seq:
assert_marked(obj)
[docs]def test_internal_config_from_data():
config = spack.config.Configuration()
# add an internal config initialized from an inline dict
config.push_scope(spack.config.InternalConfigScope('_builtin', {
'config': {
'verify_ssl': False,
'build_jobs': 6,
}
}))
assert config.get('config:verify_ssl', scope='_builtin') is False
assert config.get('config:build_jobs', scope='_builtin') == 6
assert config.get('config:verify_ssl') is False
assert config.get('config:build_jobs') == 6
# push one on top and see what happens.
config.push_scope(spack.config.InternalConfigScope('higher', {
'config': {
'checksum': True,
'verify_ssl': True,
}
}))
assert config.get('config:verify_ssl', scope='_builtin') is False
assert config.get('config:build_jobs', scope='_builtin') == 6
assert config.get('config:verify_ssl', scope='higher') is True
assert config.get('config:build_jobs', scope='higher') is None
assert config.get('config:verify_ssl') is True
assert config.get('config:build_jobs') == 6
assert config.get('config:checksum') is True
assert config.get('config:checksum', scope='_builtin') is None
assert config.get('config:checksum', scope='higher') is True
[docs]def test_keys_are_ordered():
"""Test that keys in Spack YAML files retain their order from the file."""
expected_order = (
'bin',
'man',
'share/man',
'share/aclocal',
'lib',
'lib64',
'include',
'lib/pkgconfig',
'lib64/pkgconfig',
'share/pkgconfig',
''
)
config_scope = spack.config.ConfigScope(
'modules',
os.path.join(spack.paths.test_path, 'data', 'config')
)
data = config_scope.get_section('modules')
prefix_inspections = data['modules']['prefix_inspections']
for actual, expected in zip(prefix_inspections, expected_order):
assert actual == expected
[docs]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!')
[docs]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)
[docs]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)
[docs]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)
[docs]def test_bad_config_section(mock_low_high_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')
[docs]@pytest.mark.skipif(sys.platform == 'win32',
reason="Not supported on Windows (yet)")
@pytest.mark.skipif(getuid() == 0, reason='user is root')
def test_bad_command_line_scopes(tmpdir, mock_low_high_config):
cfg = spack.config.Configuration()
with tmpdir.as_cwd():
with pytest.raises(spack.config.ConfigError):
spack.config._add_command_line_scopes(cfg, ['bad_path'])
touch('unreadable_file')
with pytest.raises(spack.config.ConfigError):
spack.config._add_command_line_scopes(cfg, ['unreadable_file'])
mkdirp('unreadable_dir')
with pytest.raises(spack.config.ConfigError):
try:
os.chmod('unreadable_dir', 0)
spack.config._add_command_line_scopes(cfg, ['unreadable_dir'])
finally:
os.chmod('unreadable_dir', 0o700) # so tmpdir can be removed
[docs]def test_add_command_line_scopes(tmpdir, mutable_config):
config_yaml = str(tmpdir.join('config.yaml'))
with open(config_yaml, 'w') as f:
f.write("""\
config:
verify_ssl: False
dirty: False
""")
spack.config._add_command_line_scopes(mutable_config, [str(tmpdir)])
[docs]def test_nested_override():
"""Ensure proper scope naming of nested overrides."""
base_name = spack.config.overrides_base_name
def _check_scopes(num_expected, debug_values):
scope_names = [s.name for s in spack.config.config.scopes.values() if
s.name.startswith(base_name)]
for i in range(num_expected):
name = '{0}{1}'.format(base_name, i)
assert name in scope_names
data = spack.config.config.get_config('config', name)
assert data['debug'] == debug_values[i]
# Check results from single and nested override
with spack.config.override('config:debug', True):
with spack.config.override('config:debug', False):
_check_scopes(2, [True, False])
_check_scopes(1, [True])
[docs]def test_alternate_override(monkeypatch):
"""Ensure proper scope naming of override when conflict present."""
base_name = spack.config.overrides_base_name
def _matching_scopes(regexpr):
return [spack.config.InternalConfigScope('{0}1'.format(base_name))]
# Check that the alternate naming works
monkeypatch.setattr(spack.config.config, 'matching_scopes',
_matching_scopes)
with spack.config.override('config:debug', False):
name = '{0}2'.format(base_name)
scope_names = [s.name for s in spack.config.config.scopes.values() if
s.name.startswith(base_name)]
assert name in scope_names
data = spack.config.config.get_config('config', name)
assert data['debug'] is False
[docs]def test_immutable_scope(tmpdir):
config_yaml = str(tmpdir.join('config.yaml'))
with open(config_yaml, 'w') as f:
f.write("""\
config:
install_tree:
root: dummy_tree_value
""")
scope = spack.config.ImmutableConfigScope('test', str(tmpdir))
data = scope.get_section('config')
assert data['config']['install_tree'] == {'root': 'dummy_tree_value'}
with pytest.raises(spack.config.ConfigError):
scope._write_section('config')
[docs]def test_single_file_scope(config, env_yaml):
scope = spack.config.SingleFileScope(
'env', env_yaml, spack.schema.env.schema, ['env']
)
with spack.config.override(scope):
# from the single-file config
assert spack.config.get('config:verify_ssl') is False
assert spack.config.get('config:dirty') is False
assert spack.config.get('packages:libelf:compiler') == ['gcc@4.5.3']
# from the lower config scopes
assert spack.config.get('config:checksum') is True
assert spack.config.get('config:checksum') is True
assert spack.config.get('packages:externalmodule:buildable') is False
assert spack.config.get('repos') == [
'/x/y/z', '$spack/var/spack/repos/builtin']
[docs]def test_single_file_scope_section_override(tmpdir, config):
"""Check that individual config sections can be overridden in an
environment config. The config here primarily differs in that the
``packages`` section is intended to override all other scopes (using the
"::" syntax).
"""
env_yaml = str(tmpdir.join("env.yaml"))
with open(env_yaml, 'w') as f:
f.write("""\
env:
config:
verify_ssl: False
packages::
libelf:
compiler: [ 'gcc@4.5.3' ]
repos:
- /x/y/z
""")
scope = spack.config.SingleFileScope(
'env', env_yaml, spack.schema.env.schema, ['env'])
with spack.config.override(scope):
# from the single-file config
assert spack.config.get('config:verify_ssl') is False
assert spack.config.get('packages:libelf:compiler') == ['gcc@4.5.3']
# from the lower config scopes
assert spack.config.get('config:checksum') is True
assert not spack.config.get('packages:externalmodule')
assert spack.config.get('repos') == [
'/x/y/z', '$spack/var/spack/repos/builtin']
[docs]def test_write_empty_single_file_scope(tmpdir):
env_schema = spack.schema.env.schema
scope = spack.config.SingleFileScope(
'test', str(tmpdir.ensure('config.yaml')), env_schema, ['spack'])
scope._write_section('config')
# confirm we can write empty config
assert not scope.get_section('config')
[docs]def check_schema(name, file_contents):
"""Check a Spack YAML schema against some data"""
f = StringIO(file_contents)
data = syaml.load_config(f)
spack.config.validate(data, name)
[docs]def test_good_env_yaml(tmpdir):
check_schema(spack.schema.env.schema, """\
spack:
config:
verify_ssl: False
dirty: False
repos:
- ~/my/repo/location
mirrors:
remote: /foo/bar/baz
compilers:
- compiler:
spec: cce@2.1
operating_system: cnl
modules: []
paths:
cc: /path/to/cc
cxx: /path/to/cxx
fc: /path/to/fc
f77: /path/to/f77
""")
[docs]def test_bad_env_yaml(tmpdir):
with pytest.raises(spack.config.ConfigFormatError):
check_schema(spack.schema.env.schema, """\
env:
foobar:
verify_ssl: False
dirty: False
""")
[docs]def test_bad_config_yaml(tmpdir):
with pytest.raises(spack.config.ConfigFormatError):
check_schema(spack.schema.config.schema, """\
config:
verify_ssl: False
install_tree:
root:
extra_level: foo
""")
[docs]def test_bad_mirrors_yaml(tmpdir):
with pytest.raises(spack.config.ConfigFormatError):
check_schema(spack.schema.mirrors.schema, """\
mirrors:
local: True
""")
[docs]def test_bad_repos_yaml(tmpdir):
with pytest.raises(spack.config.ConfigFormatError):
check_schema(spack.schema.repos.schema, """\
repos:
True
""")
[docs]def test_bad_compilers_yaml(tmpdir):
with pytest.raises(spack.config.ConfigFormatError):
check_schema(spack.schema.compilers.schema, """\
compilers:
key_instead_of_list: 'value'
""")
with pytest.raises(spack.config.ConfigFormatError):
check_schema(spack.schema.compilers.schema, """\
compilers:
- shmompiler:
environment: /bad/value
""")
with pytest.raises(spack.config.ConfigFormatError):
check_schema(spack.schema.compilers.schema, """\
compilers:
- compiler:
fenfironfent: /bad/value
""")
[docs]def test_internal_config_section_override(mock_low_high_config,
write_config_file):
write_config_file('config', config_merge_list, 'low')
wanted_list = config_override_list['config']['build_stage:']
mock_low_high_config.push_scope(spack.config.InternalConfigScope
('high', {
'config:': {
'build_stage': wanted_list
}
}))
assert mock_low_high_config.get('config:build_stage') == wanted_list
[docs]def test_internal_config_dict_override(mock_low_high_config,
write_config_file):
write_config_file('config', config_merge_dict, 'low')
wanted_dict = config_override_dict['config']['info:']
mock_low_high_config.push_scope(spack.config.InternalConfigScope
('high', config_override_dict))
assert mock_low_high_config.get('config:info') == wanted_dict
[docs]def test_internal_config_list_override(mock_low_high_config,
write_config_file):
write_config_file('config', config_merge_list, 'low')
wanted_list = config_override_list['config']['build_stage:']
mock_low_high_config.push_scope(spack.config.InternalConfigScope
('high', config_override_list))
assert mock_low_high_config.get('config:build_stage') == wanted_list
[docs]def test_set_section_override(mock_low_high_config, write_config_file):
write_config_file('config', config_merge_list, 'low')
wanted_list = config_override_list['config']['build_stage:']
with spack.config.override('config::build_stage', wanted_list):
assert mock_low_high_config.get('config:build_stage') == wanted_list
assert config_merge_list['config']['build_stage'] == \
mock_low_high_config.get('config:build_stage')
[docs]def test_set_list_override(mock_low_high_config, write_config_file):
write_config_file('config', config_merge_list, 'low')
wanted_list = config_override_list['config']['build_stage:']
with spack.config.override('config:build_stage:', wanted_list):
assert wanted_list == mock_low_high_config.get('config:build_stage')
assert config_merge_list['config']['build_stage'] == \
mock_low_high_config.get('config:build_stage')
[docs]def test_set_dict_override(mock_low_high_config, write_config_file):
write_config_file('config', config_merge_dict, 'low')
wanted_dict = config_override_dict['config']['info:']
with spack.config.override('config:info:', wanted_dict):
assert wanted_dict == mock_low_high_config.get('config:info')
assert config_merge_dict['config']['info'] == \
mock_low_high_config.get('config:info')
[docs]def test_set_bad_path(config):
with pytest.raises(syaml.SpackYAMLError, match='Illegal leading'):
with spack.config.override(':bad:path', ''):
pass
[docs]def test_bad_path_double_override(config):
with pytest.raises(syaml.SpackYAMLError,
match='Meaningless second override'):
with spack.config.override('bad::double:override::directive', ''):
pass
[docs]def test_license_dir_config(mutable_config, mock_packages):
"""Ensure license directory is customizable"""
assert spack.config.get("config:license_dir") == spack.paths.default_license_dir
assert spack.package.Package.global_license_dir == spack.paths.default_license_dir
assert spack.repo.get("a").global_license_dir == spack.paths.default_license_dir
rel_path = os.path.join(os.path.sep, "foo", "bar", "baz")
spack.config.set("config:license_dir", rel_path)
assert spack.config.get("config:license_dir") == rel_path
assert spack.package.Package.global_license_dir == rel_path
assert spack.repo.get("a").global_license_dir == rel_path
[docs]@pytest.mark.regression('22547')
def test_single_file_scope_cache_clearing(env_yaml):
scope = spack.config.SingleFileScope(
'env', env_yaml, spack.schema.env.schema, ['env']
)
# Check that we can retrieve data from the single file scope
before = scope.get_section('config')
assert before
# Clear the cache of the Single file scope
scope.clear()
# Check that the section can be retireved again and it's
# the same as before
after = scope.get_section('config')
assert after
assert before == after
[docs]@pytest.mark.regression('22611')
def test_internal_config_scope_cache_clearing():
"""
An InternalConfigScope object is constructed from data that is already
in memory, therefore it doesn't have any cache to clear. Here we ensure
that calling the clear method is consistent with that..
"""
data = {
'config': {
'build_jobs': 10
}
}
internal_scope = spack.config.InternalConfigScope('internal', data)
# Ensure that the initial object is properly set
assert internal_scope.sections['config'] == data
# Call the clear method
internal_scope.clear()
# Check that this didn't affect the scope object
assert internal_scope.sections['config'] == data
[docs]def test_system_config_path_is_overridable(working_env):
p = "/some/path"
os.environ['SPACK_SYSTEM_CONFIG_PATH'] = p
assert spack.paths._get_system_config_path() == p
[docs]def test_system_config_path_is_default_when_env_var_is_empty(working_env):
os.environ['SPACK_SYSTEM_CONFIG_PATH'] = ''
assert os.sep + os.path.join('etc', 'spack') == \
spack.paths._get_system_config_path()
[docs]def test_user_config_path_is_overridable(working_env):
p = "/some/path"
os.environ['SPACK_USER_CONFIG_PATH'] = p
assert p == spack.paths._get_user_config_path()
[docs]def test_user_config_path_is_default_when_env_var_is_empty(working_env):
os.environ['SPACK_USER_CONFIG_PATH'] = ''
assert os.path.expanduser("~%s.spack" % os.sep) == \
spack.paths._get_user_config_path()
[docs]def test_local_config_can_be_disabled(working_env):
os.environ['SPACK_DISABLE_LOCAL_CONFIG'] = 'true'
cfg = spack.config._config()
assert "defaults" in cfg.scopes
assert "system" not in cfg.scopes
assert "site" in cfg.scopes
assert "user" not in cfg.scopes
os.environ['SPACK_DISABLE_LOCAL_CONFIG'] = ''
cfg = spack.config._config()
assert "defaults" in cfg.scopes
assert "system" not in cfg.scopes
assert "site" in cfg.scopes
assert "user" not in cfg.scopes
del os.environ['SPACK_DISABLE_LOCAL_CONFIG']
cfg = spack.config._config()
assert "defaults" in cfg.scopes
assert "system" in cfg.scopes
assert "site" in cfg.scopes
assert "user" in cfg.scopes
[docs]def test_user_cache_path_is_overridable(working_env):
p = "/some/path"
os.environ['SPACK_USER_CACHE_PATH'] = p
assert spack.paths._get_user_cache_path() == p
[docs]def test_user_cache_path_is_default_when_env_var_is_empty(working_env):
os.environ['SPACK_USER_CACHE_PATH'] = ''
assert os.path.expanduser("~%s.spack" % os.sep) == \
spack.paths._get_user_cache_path()