Added a context manager to swap architectures

This solves a few FIXMEs in conftest.py, where
we were manipulating globals and seeing side
effects prior to registering fixtures.

This commit solves the FIXMEs, but introduces
a performance regression on tests that may need
to be investigated

(cherry picked from commit 4558dc06e2)
This commit is contained in:
Massimiliano Culpo 2021-02-02 09:57:09 +01:00
parent 095ace9028
commit 2a5f46d8d3
5 changed files with 89 additions and 35 deletions

View file

@ -56,6 +56,7 @@
attributes front_os and back_os. The operating system as described earlier, attributes front_os and back_os. The operating system as described earlier,
will be responsible for compiler detection. will be responsible for compiler detection.
""" """
import contextlib
import functools import functools
import inspect import inspect
import warnings import warnings
@ -67,6 +68,8 @@
from llnl.util.lang import memoized, list_modules, key_ordering from llnl.util.lang import memoized, list_modules, key_ordering
import spack.compiler import spack.compiler
import spack.compilers
import spack.config
import spack.paths import spack.paths
import spack.error as serr import spack.error as serr
import spack.util.executable import spack.util.executable
@ -491,7 +494,7 @@ def arch_for_spec(arch_spec):
@memoized @memoized
def all_platforms(): def _all_platforms():
classes = [] classes = []
mod_path = spack.paths.platform_path mod_path = spack.paths.platform_path
parent_module = "spack.platforms" parent_module = "spack.platforms"
@ -512,7 +515,7 @@ def all_platforms():
@memoized @memoized
def platform(): def _platform():
"""Detects the platform for this machine. """Detects the platform for this machine.
Gather a list of all available subclasses of platforms. Gather a list of all available subclasses of platforms.
@ -521,7 +524,7 @@ def platform():
a file path (/opt/cray...) a file path (/opt/cray...)
""" """
# Try to create a Platform object using the config file FIRST # Try to create a Platform object using the config file FIRST
platform_list = all_platforms() platform_list = _all_platforms()
platform_list.sort(key=lambda a: a.priority) platform_list.sort(key=lambda a: a.priority)
for platform_cls in platform_list: for platform_cls in platform_list:
@ -529,6 +532,19 @@ def platform():
return platform_cls() return platform_cls()
#: The "real" platform of the host running Spack. This should not be changed
#: by any method and is here as a convenient way to refer to the host platform.
real_platform = _platform
#: The current platform used by Spack. May be swapped by the use_platform
#: context manager.
platform = _platform
#: The list of all platform classes. May be swapped by the use_platform
#: context manager.
all_platforms = _all_platforms
@memoized @memoized
def default_arch(): def default_arch():
"""Default ``Arch`` object for this machine. """Default ``Arch`` object for this machine.
@ -563,3 +579,39 @@ def compatible_sys_types():
arch = Arch(platform(), 'default_os', target) arch = Arch(platform(), 'default_os', target)
compatible_archs.append(str(arch)) compatible_archs.append(str(arch))
return compatible_archs return compatible_archs
class _PickleableCallable(object):
"""Class used to pickle a callable that may substitute either
_platform or _all_platforms. Lambda or nested functions are
not pickleable.
"""
def __init__(self, return_value):
self.return_value = return_value
def __call__(self):
return self.return_value
@contextlib.contextmanager
def use_platform(new_platform):
global platform, all_platforms
msg = '"{0}" must be an instance of Platform'
assert isinstance(new_platform, Platform), msg.format(new_platform)
original_platform_fn, original_all_platforms_fn = platform, all_platforms
platform = _PickleableCallable(new_platform)
all_platforms = _PickleableCallable([type(new_platform)])
# Clear configuration and compiler caches
spack.config.config.clear_caches()
spack.compilers._cache_config_files = []
yield new_platform
platform, all_platforms = original_platform_fn, original_all_platforms_fn
# Clear configuration and compiler caches
spack.config.config.clear_caches()
spack.compilers._cache_config_files = []

View file

@ -6,6 +6,7 @@
""" Test checks if the architecture class is created correctly and also that """ Test checks if the architecture class is created correctly and also that
the functions are looking for the correct architecture name the functions are looking for the correct architecture name
""" """
import itertools
import os import os
import platform as py_platform import platform as py_platform
@ -116,20 +117,26 @@ def test_user_defaults(config):
assert default_target == default_spec.architecture.target assert default_target == default_spec.architecture.target
@pytest.mark.parametrize('operating_system', [ def test_user_input_combination(config):
x for x in spack.architecture.platform().operating_sys valid_keywords = ["fe", "be", "frontend", "backend"]
] + ["fe", "be", "frontend", "backend"])
@pytest.mark.parametrize('target', [ possible_targets = ([x for x in spack.architecture.platform().targets]
x for x in spack.architecture.platform().targets + valid_keywords)
] + ["fe", "be", "frontend", "backend"])
def test_user_input_combination(config, operating_system, target): possible_os = ([x for x in spack.architecture.platform().operating_sys]
platform = spack.architecture.platform() + valid_keywords)
spec = Spec("libelf os=%s target=%s" % (operating_system, target))
spec.concretize() for target, operating_system in itertools.product(
assert spec.architecture.os == str( possible_targets, possible_os
platform.operating_system(operating_system) ):
) platform = spack.architecture.platform()
assert spec.architecture.target == platform.target(target) spec_str = "libelf os={0} target={1}".format(operating_system, target)
spec = Spec(spec_str)
spec.concretize()
assert spec.architecture.os == str(
platform.operating_system(operating_system)
)
assert spec.architecture.target == platform.target(target)
def test_operating_system_conversion_to_dict(): def test_operating_system_conversion_to_dict():

View file

@ -27,7 +27,7 @@ def python_database(mock_packages, mutable_database):
@pytest.mark.db @pytest.mark.db
def test_extensions(mock_packages, python_database, capsys): def test_extensions(mock_packages, python_database, config, capsys):
ext2 = Spec("py-extension2").concretized() ext2 = Spec("py-extension2").concretized()
def check_output(ni, na): def check_output(ni, na):

View file

@ -12,7 +12,7 @@
concretize = SpackCommand('concretize') concretize = SpackCommand('concretize')
def test_undevelop(tmpdir, mock_packages, mutable_mock_env_path): def test_undevelop(tmpdir, config, mock_packages, mutable_mock_env_path):
# setup environment # setup environment
envdir = tmpdir.mkdir('env') envdir = tmpdir.mkdir('env')
with envdir.as_cwd(): with envdir.as_cwd():
@ -39,7 +39,7 @@ def test_undevelop(tmpdir, mock_packages, mutable_mock_env_path):
assert not after.satisfies('dev_path=*') assert not after.satisfies('dev_path=*')
def test_undevelop_nonexistent(tmpdir, mock_packages, mutable_mock_env_path): def test_undevelop_nonexistent(tmpdir, config, mock_packages, mutable_mock_env_path):
# setup environment # setup environment
envdir = tmpdir.mkdir('env') envdir = tmpdir.mkdir('env')
with envdir.as_cwd(): with envdir.as_cwd():

View file

@ -1,4 +1,4 @@
# Copyright 2013-2020 Lawrence Livermore National Security, LLC and other # Copyright 2013-2021 Lawrence Livermore National Security, LLC and other
# Spack Project Developers. See the top-level COPYRIGHT file for details. # Spack Project Developers. See the top-level COPYRIGHT file for details.
# #
# SPDX-License-Identifier: (Apache-2.0 OR MIT) # SPDX-License-Identifier: (Apache-2.0 OR MIT)
@ -315,24 +315,18 @@ def _skip_if_missing_executables(request):
pytest.skip(msg.format(', '.join(missing_execs))) pytest.skip(msg.format(', '.join(missing_execs)))
# FIXME: The lines below should better be added to a fixture with @pytest.fixture(scope='session')
# FIXME: session-scope. Anyhow doing it is not easy, as it seems
# FIXME: there's some weird interaction with compilers during concretization.
spack.architecture.real_platform = spack.architecture.platform
def test_platform(): def test_platform():
return spack.platforms.test.Test() return spack.platforms.test.Test()
spack.architecture.platform = test_platform @pytest.fixture(autouse=True, scope='session')
def _use_test_platform(test_platform):
# This is the only context manager used at session scope (see note
# FIXME: Since we change the architecture above, we have to (re)initialize # below for more insight) since we want to use the test platform as
# FIXME: the config singleton. If it gets initialized too early with the # a default during tests.
# FIXME: actual architecture, tests will fail. with spack.architecture.use_platform(test_platform):
spack.config.config = spack.config._config() yield
# #
# Note on context managers used by fixtures # Note on context managers used by fixtures
@ -356,6 +350,7 @@ def test_platform():
# *USE*, or things can get really confusing. # *USE*, or things can get really confusing.
# #
# #
# Test-specific fixtures # Test-specific fixtures
# #