tests: refactor tests to avoid persistent global state
Previously, fixtures like `config`, `database`, and `store` were module-scoped, but frequently used as test function arguments. These fixtures swap out global on setup and restore them on teardown. As function arguments, they would do the right set-up, but they'd leave the global changes in place for the whole module the function lived in. This meant that if you use `config` once, other functions in the same module would inadvertently inherit the mock Spack configuration, as it would only be torn down once all tests in the module were complete. In general, we should module- or session-scope the *STATE* required for these global objects (as it's expensive to create0, but we shouldn't module-or session scope the activation/use of them, or things can get really confusing. - [x] Make generic context managers for global-modifying fixtures. - [x] Make session- and module-scoped fixtures that ONLY build filesystem state and create objects, but do not swap out any variables. - [x] Make seeparate function-scoped fixtures that *use* the session scoped fixtures and actually swap out (and back in) the global variables like `config`, `database`, and `store`. These changes make it so that global changes are *only* ever alive for a singlee test function, and we don't get weird dependencies because a global fixture hasn't been destroyed.
This commit is contained in:
parent
e839432472
commit
e7dc8a2bea
@ -26,7 +26,7 @@
|
||||
|
||||
# everything here uses the mock_env_path
|
||||
pytestmark = pytest.mark.usefixtures(
|
||||
'mutable_mock_env_path', 'config', 'mutable_mock_packages')
|
||||
'mutable_mock_env_path', 'config', 'mutable_mock_repo')
|
||||
|
||||
env = SpackCommand('env')
|
||||
install = SpackCommand('install')
|
||||
|
@ -10,9 +10,21 @@
|
||||
|
||||
import spack.main
|
||||
import spack.modules
|
||||
from spack.test.conftest import use_store, use_configuration, use_repo
|
||||
|
||||
module = spack.main.SpackCommand('module')
|
||||
|
||||
#: make sure module files are generated for all the tests here
|
||||
@pytest.fixture(scope='module', autouse=True)
|
||||
def ensure_module_files_are_there(
|
||||
mock_repo_path, mock_store, mock_configuration):
|
||||
"""Generate module files for module tests."""
|
||||
module = spack.main.SpackCommand('module')
|
||||
with use_store(mock_store):
|
||||
with use_configuration(mock_configuration):
|
||||
with use_repo(mock_repo_path):
|
||||
module('tcl', 'refresh', '-y')
|
||||
|
||||
|
||||
def _module_files(module_type, *specs):
|
||||
specs = [spack.spec.Spec(x).concretized() for x in specs]
|
||||
@ -20,11 +32,6 @@ def _module_files(module_type, *specs):
|
||||
return [writer_cls(spec).layout.filename for spec in specs]
|
||||
|
||||
|
||||
@pytest.fixture(scope='module', autouse=True)
|
||||
def ensure_module_files_are_there(database):
|
||||
module('tcl', 'refresh', '-y')
|
||||
|
||||
|
||||
@pytest.fixture(
|
||||
params=[
|
||||
['rm', 'doesnotexist'], # Try to remove a non existing module
|
||||
|
@ -9,7 +9,7 @@
|
||||
import spack.spec
|
||||
from spack.main import SpackCommand
|
||||
|
||||
pytestmark = pytest.mark.usefixtures('config', 'mutable_mock_packages')
|
||||
pytestmark = pytest.mark.usefixtures('config', 'mutable_mock_repo')
|
||||
|
||||
spec = SpackCommand('spec')
|
||||
|
||||
|
@ -276,7 +276,7 @@ def test_concretize_two_virtuals(self):
|
||||
Spec('hypre').concretize()
|
||||
|
||||
def test_concretize_two_virtuals_with_one_bound(
|
||||
self, mutable_mock_packages
|
||||
self, mutable_mock_repo
|
||||
):
|
||||
"""Test a package with multiple virtual dependencies and one preset."""
|
||||
Spec('hypre ^openblas').concretize()
|
||||
|
@ -83,7 +83,7 @@ def test_preferred_variants(self):
|
||||
'mpileaks', debug=True, opt=True, shared=False, static=False
|
||||
)
|
||||
|
||||
def test_preferred_compilers(self, mutable_mock_packages):
|
||||
def test_preferred_compilers(self, mutable_mock_repo):
|
||||
"""Test preferred compilers are applied correctly
|
||||
"""
|
||||
update_packages('mpileaks', 'compiler', ['clang@3.3'])
|
||||
@ -94,7 +94,7 @@ def test_preferred_compilers(self, mutable_mock_packages):
|
||||
spec = concretize('mpileaks')
|
||||
assert spec.compiler == spack.spec.CompilerSpec('gcc@4.5.0')
|
||||
|
||||
def test_preferred_target(self, mutable_mock_packages):
|
||||
def test_preferred_target(self, mutable_mock_repo):
|
||||
"""Test preferred compilers are applied correctly
|
||||
"""
|
||||
spec = concretize('mpich')
|
||||
|
@ -4,7 +4,7 @@
|
||||
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
|
||||
|
||||
import collections
|
||||
import copy
|
||||
import contextlib
|
||||
import errno
|
||||
import inspect
|
||||
import itertools
|
||||
@ -270,31 +270,75 @@ def _skip_if_missing_executables(request):
|
||||
spack.architecture.real_platform = spack.architecture.platform
|
||||
spack.architecture.platform = lambda: spack.platforms.test.Test()
|
||||
|
||||
##########
|
||||
# Test-specific fixtures
|
||||
##########
|
||||
|
||||
#
|
||||
# Context managers used by fixtures
|
||||
#
|
||||
# Because these context managers modify global state, they should really
|
||||
# ONLY be used persistently (i.e., around yield statements) in
|
||||
# function-scoped fixtures, OR in autouse session- or module-scoped
|
||||
# fixtures.
|
||||
#
|
||||
# If they're used in regular tests or in module-scoped fixtures that are
|
||||
# then injected as function arguments, weird things can happen, because
|
||||
# the original state won't be restored until *after* the fixture is
|
||||
# destroyed. This makes sense for an autouse fixture, where you know
|
||||
# everything in the module/session is going to need the modified
|
||||
# behavior, but modifying global state for one function in a way that
|
||||
# won't be restored until after the module or session is done essentially
|
||||
# leaves garbage behind for other tests.
|
||||
#
|
||||
# In general, we should module- or session-scope the *STATE* required for
|
||||
# these global objects, but we shouldn't module- or session-scope their
|
||||
# *USE*, or things can get really confusing.
|
||||
#
|
||||
|
||||
@contextlib.contextmanager
|
||||
def use_configuration(config):
|
||||
"""Context manager to swap out the global Spack configuration."""
|
||||
saved = spack.config.config
|
||||
spack.config.config = config
|
||||
yield
|
||||
spack.config.config = saved
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def repo_path():
|
||||
"""Session scoped RepoPath object pointing to the mock repository"""
|
||||
return spack.repo.RepoPath(spack.paths.mock_packages_path)
|
||||
@contextlib.contextmanager
|
||||
def use_store(store):
|
||||
"""Context manager to swap out the global Spack store."""
|
||||
saved = spack.store.store
|
||||
spack.store.store = store
|
||||
yield
|
||||
spack.store.store = saved
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def mock_packages(repo_path):
|
||||
"""Use the 'builtin.mock' repository instead of 'builtin'"""
|
||||
mock_repo = copy.deepcopy(repo_path)
|
||||
with spack.repo.swap(mock_repo):
|
||||
@contextlib.contextmanager
|
||||
def use_repo(repo):
|
||||
"""Context manager to swap out the global Spack repo path."""
|
||||
with spack.repo.swap(repo):
|
||||
yield
|
||||
|
||||
|
||||
#
|
||||
# Test-specific fixtures
|
||||
#
|
||||
@pytest.fixture(scope='session')
|
||||
def mock_repo_path():
|
||||
yield spack.repo.RepoPath(spack.paths.mock_packages_path)
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def mutable_mock_packages(mock_packages, repo_path):
|
||||
def mock_packages(mock_repo_path):
|
||||
"""Use the 'builtin.mock' repository instead of 'builtin'"""
|
||||
with use_repo(mock_repo_path):
|
||||
yield mock_repo_path
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def mutable_mock_repo(mock_repo_path):
|
||||
"""Function-scoped mock packages, for tests that need to modify them."""
|
||||
mock_repo = copy.deepcopy(repo_path)
|
||||
with spack.repo.swap(mock_repo):
|
||||
yield
|
||||
mock_repo_path = spack.repo.RepoPath(spack.paths.mock_packages_path)
|
||||
with use_repo(mock_repo_path):
|
||||
yield mock_repo_path
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
@ -345,12 +389,9 @@ def configuration_dir(tmpdir_factory, linux_os):
|
||||
shutil.rmtree(str(tmpdir))
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def config(configuration_dir):
|
||||
"""Hooks the mock configuration files into spack.config"""
|
||||
# Set up a mock config scope
|
||||
real_configuration = spack.config.config
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def mock_configuration(configuration_dir):
|
||||
"""Create a persistent Configuration object from the configuration_dir."""
|
||||
defaults = spack.config.InternalConfigScope(
|
||||
'_builtin', spack.config.config_defaults
|
||||
)
|
||||
@ -360,11 +401,14 @@ def config(configuration_dir):
|
||||
for name in ['site', 'system', 'user']]
|
||||
test_scopes.append(spack.config.InternalConfigScope('command_line'))
|
||||
|
||||
spack.config.config = spack.config.Configuration(*test_scopes)
|
||||
yield spack.config.Configuration(*test_scopes)
|
||||
|
||||
yield spack.config.config
|
||||
|
||||
spack.config.config = real_configuration
|
||||
@pytest.fixture(scope='function')
|
||||
def config(mock_configuration):
|
||||
"""This fixture activates/deactivates the mock configuration."""
|
||||
with use_configuration(mock_configuration):
|
||||
yield mock_configuration
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
@ -376,27 +420,24 @@ def mutable_config(tmpdir_factory, configuration_dir, monkeypatch):
|
||||
cfg = spack.config.Configuration(
|
||||
*[spack.config.ConfigScope(name, str(mutable_dir))
|
||||
for name in ['site', 'system', 'user']])
|
||||
monkeypatch.setattr(spack.config, 'config', cfg)
|
||||
|
||||
# This is essential, otherwise the cache will create weird side effects
|
||||
# that will compromise subsequent tests if compilers.yaml is modified
|
||||
monkeypatch.setattr(spack.compilers, '_cache_config_file', [])
|
||||
|
||||
yield spack.config.config
|
||||
with use_configuration(cfg):
|
||||
yield cfg
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def mock_config(tmpdir):
|
||||
"""Mocks two configuration scopes: 'low' and 'high'."""
|
||||
real_configuration = spack.config.config
|
||||
|
||||
spack.config.config = spack.config.Configuration(
|
||||
config = spack.config.Configuration(
|
||||
*[spack.config.ConfigScope(name, str(tmpdir.join(name)))
|
||||
for name in ['low', 'high']])
|
||||
|
||||
yield spack.config.config
|
||||
|
||||
spack.config.config = real_configuration
|
||||
with use_configuration(config):
|
||||
yield config
|
||||
|
||||
|
||||
def _populate(mock_db):
|
||||
@ -443,30 +484,41 @@ def _store_dir_and_cache(tmpdir_factory):
|
||||
return store, cache
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def database(tmpdir_factory, mock_packages, config, _store_dir_and_cache):
|
||||
@pytest.fixture(scope='session')
|
||||
def mock_store(tmpdir_factory, mock_repo_path, mock_configuration,
|
||||
_store_dir_and_cache):
|
||||
"""Creates a read-only mock database with some packages installed note
|
||||
that the ref count for dyninst here will be 3, as it's recycled
|
||||
across each install.
|
||||
"""
|
||||
real_store = spack.store.store
|
||||
store_path, store_cache = _store_dir_and_cache
|
||||
|
||||
mock_store = spack.store.Store(str(store_path))
|
||||
spack.store.store = mock_store
|
||||
This does not actually activate the store for use by Spack -- see the
|
||||
``database`` fixture for that.
|
||||
|
||||
"""
|
||||
store_path, store_cache = _store_dir_and_cache
|
||||
store = spack.store.Store(str(store_path))
|
||||
|
||||
# If the cache does not exist populate the store and create it
|
||||
if not os.path.exists(str(store_cache.join('.spack-db'))):
|
||||
_populate(mock_store.db)
|
||||
with use_configuration(mock_configuration):
|
||||
with use_store(store):
|
||||
with use_repo(mock_repo_path):
|
||||
_populate(store.db)
|
||||
store_path.copy(store_cache, mode=True, stat=True)
|
||||
|
||||
# Make the database read-only to ensure we can't modify entries
|
||||
# Make the DB filesystem read-only to ensure we can't modify entries
|
||||
store_path.join('.spack-db').chmod(mode=0o555, rec=1)
|
||||
|
||||
yield mock_store.db
|
||||
yield store
|
||||
|
||||
store_path.join('.spack-db').chmod(mode=0o755, rec=1)
|
||||
spack.store.store = real_store
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def database(mock_store, mock_packages, config):
|
||||
"""This activates the mock store, packages, AND config."""
|
||||
with use_store(mock_store):
|
||||
yield mock_store.db
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
|
@ -88,7 +88,7 @@ def test_fetch(type_of_test,
|
||||
secure,
|
||||
mock_git_repository,
|
||||
config,
|
||||
mutable_mock_packages,
|
||||
mutable_mock_repo,
|
||||
git_version):
|
||||
"""Tries to:
|
||||
|
||||
@ -137,7 +137,7 @@ def test_fetch(type_of_test,
|
||||
|
||||
|
||||
@pytest.mark.parametrize("type_of_test", ['branch', 'commit'])
|
||||
def test_debug_fetch(type_of_test, mock_git_repository, config):
|
||||
def test_debug_fetch(mock_packages, type_of_test, mock_git_repository, config):
|
||||
"""Fetch the repo with debug enabled."""
|
||||
# Retrieve the right test parameters
|
||||
t = mock_git_repository.checks[type_of_test]
|
||||
@ -176,7 +176,7 @@ def test_needs_stage():
|
||||
|
||||
@pytest.mark.parametrize("get_full_repo", [True, False])
|
||||
def test_get_full_repo(get_full_repo, git_version, mock_git_repository,
|
||||
config, mutable_mock_packages):
|
||||
config, mutable_mock_repo):
|
||||
"""Ensure that we can clone a full repository."""
|
||||
|
||||
if git_version < ver('1.7.1'):
|
||||
|
@ -29,7 +29,7 @@ def test_fetch(
|
||||
secure,
|
||||
mock_hg_repository,
|
||||
config,
|
||||
mutable_mock_packages
|
||||
mutable_mock_repo
|
||||
):
|
||||
"""Tries to:
|
||||
|
||||
|
@ -128,7 +128,7 @@ def test_dont_add_patches_to_installed_package(install_mockery, mock_fetch):
|
||||
|
||||
|
||||
def test_installed_dependency_request_conflicts(
|
||||
install_mockery, mock_fetch, mutable_mock_packages):
|
||||
install_mockery, mock_fetch, mutable_mock_repo):
|
||||
dependency = Spec('dependency-install')
|
||||
dependency.concretize()
|
||||
dependency.package.do_install()
|
||||
@ -141,7 +141,7 @@ def test_installed_dependency_request_conflicts(
|
||||
|
||||
|
||||
def test_install_dependency_symlinks_pkg(
|
||||
install_mockery, mock_fetch, mutable_mock_packages):
|
||||
install_mockery, mock_fetch, mutable_mock_repo):
|
||||
"""Test dependency flattening/symlinks mock package."""
|
||||
spec = Spec('flatten-deps')
|
||||
spec.concretize()
|
||||
@ -154,7 +154,7 @@ def test_install_dependency_symlinks_pkg(
|
||||
|
||||
|
||||
def test_flatten_deps(
|
||||
install_mockery, mock_fetch, mutable_mock_packages):
|
||||
install_mockery, mock_fetch, mutable_mock_repo):
|
||||
"""Explicitly test the flattening code for coverage purposes."""
|
||||
# Unfortunately, executing the 'flatten-deps' spec's installation does
|
||||
# not affect code coverage results, so be explicit here.
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
from llnl.util.filesystem import resolve_link_target_relative_to_the_link
|
||||
|
||||
pytestmark = pytest.mark.usefixtures('config', 'mutable_mock_packages')
|
||||
pytestmark = pytest.mark.usefixtures('config', 'mutable_mock_repo')
|
||||
|
||||
# paths in repos that shouldn't be in the mirror tarballs.
|
||||
exclude = ['.hg', '.git', '.svn']
|
||||
|
@ -182,7 +182,7 @@ def test_urls_for_versions(mock_packages, config):
|
||||
assert url == 'http://www.doesnotexist.org/url_override-0.8.1.tar.gz'
|
||||
|
||||
|
||||
def test_url_for_version_with_no_urls():
|
||||
def test_url_for_version_with_no_urls(mock_packages, config):
|
||||
pkg = spack.repo.get('git-test')
|
||||
with pytest.raises(spack.package.NoURLError):
|
||||
pkg.url_for_version('1.0')
|
||||
|
@ -10,14 +10,6 @@
|
||||
import spack.paths
|
||||
|
||||
|
||||
# Unlike the repo_path fixture defined in conftest, this has a test-level
|
||||
# scope rather than a session level scope, since we want to edit the
|
||||
# given RepoPath
|
||||
@pytest.fixture()
|
||||
def repo_for_test():
|
||||
return spack.repo.RepoPath(spack.paths.mock_packages_path)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def extra_repo(tmpdir_factory):
|
||||
repo_namespace = 'extra_test_repo'
|
||||
@ -32,31 +24,31 @@ def extra_repo(tmpdir_factory):
|
||||
return spack.repo.Repo(str(repo_dir))
|
||||
|
||||
|
||||
def test_repo_getpkg(repo_for_test):
|
||||
repo_for_test.get('a')
|
||||
repo_for_test.get('builtin.mock.a')
|
||||
def test_repo_getpkg(mutable_mock_repo):
|
||||
mutable_mock_repo.get('a')
|
||||
mutable_mock_repo.get('builtin.mock.a')
|
||||
|
||||
|
||||
def test_repo_multi_getpkg(repo_for_test, extra_repo):
|
||||
repo_for_test.put_first(extra_repo)
|
||||
repo_for_test.get('a')
|
||||
repo_for_test.get('builtin.mock.a')
|
||||
def test_repo_multi_getpkg(mutable_mock_repo, extra_repo):
|
||||
mutable_mock_repo.put_first(extra_repo)
|
||||
mutable_mock_repo.get('a')
|
||||
mutable_mock_repo.get('builtin.mock.a')
|
||||
|
||||
|
||||
def test_repo_multi_getpkgclass(repo_for_test, extra_repo):
|
||||
repo_for_test.put_first(extra_repo)
|
||||
repo_for_test.get_pkg_class('a')
|
||||
repo_for_test.get_pkg_class('builtin.mock.a')
|
||||
def test_repo_multi_getpkgclass(mutable_mock_repo, extra_repo):
|
||||
mutable_mock_repo.put_first(extra_repo)
|
||||
mutable_mock_repo.get_pkg_class('a')
|
||||
mutable_mock_repo.get_pkg_class('builtin.mock.a')
|
||||
|
||||
|
||||
def test_repo_pkg_with_unknown_namespace(repo_for_test):
|
||||
def test_repo_pkg_with_unknown_namespace(mutable_mock_repo):
|
||||
with pytest.raises(spack.repo.UnknownNamespaceError):
|
||||
repo_for_test.get('unknown.a')
|
||||
mutable_mock_repo.get('unknown.a')
|
||||
|
||||
|
||||
def test_repo_unknown_pkg(repo_for_test):
|
||||
def test_repo_unknown_pkg(mutable_mock_repo):
|
||||
with pytest.raises(spack.repo.UnknownPackageError):
|
||||
repo_for_test.get('builtin.mock.nonexistentpackage')
|
||||
mutable_mock_repo.get('builtin.mock.nonexistentpackage')
|
||||
|
||||
|
||||
@pytest.mark.maybeslow
|
||||
@ -66,7 +58,7 @@ def test_repo_last_mtime():
|
||||
assert spack.repo.path.last_mtime() == latest_mtime
|
||||
|
||||
|
||||
def test_repo_invisibles(repo_for_test, extra_repo):
|
||||
def test_repo_invisibles(mutable_mock_repo, extra_repo):
|
||||
with open(os.path.join(extra_repo.root, 'packages', '.invisible'), 'w'):
|
||||
pass
|
||||
extra_repo.all_package_names()
|
||||
|
@ -189,7 +189,7 @@ def test_conditional_dep_with_user_constraints():
|
||||
assert ('y@3' in spec)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures('mutable_mock_packages')
|
||||
@pytest.mark.usefixtures('mutable_mock_repo')
|
||||
class TestSpecDag(object):
|
||||
|
||||
def test_conflicting_package_constraints(self, set_dependency):
|
||||
|
@ -61,7 +61,7 @@ def test_concrete_spec(config, mock_packages):
|
||||
check_yaml_round_trip(spec)
|
||||
|
||||
|
||||
def test_yaml_multivalue():
|
||||
def test_yaml_multivalue(config, mock_packages):
|
||||
spec = Spec('multivalue_variant foo="bar,baz"')
|
||||
spec.concretize()
|
||||
check_yaml_round_trip(spec)
|
||||
|
@ -30,7 +30,7 @@ def test_fetch(
|
||||
secure,
|
||||
mock_svn_repository,
|
||||
config,
|
||||
mutable_mock_packages
|
||||
mutable_mock_repo
|
||||
):
|
||||
"""Tries to:
|
||||
|
||||
|
@ -75,7 +75,7 @@ def test_fetch(
|
||||
secure,
|
||||
checksum_type,
|
||||
config,
|
||||
mutable_mock_packages
|
||||
mutable_mock_repo
|
||||
):
|
||||
"""Fetch an archive and make sure we can checksum it."""
|
||||
mock_archive.url
|
||||
|
Loading…
Reference in New Issue
Block a user