diff options
author | Todd Gamblin <tgamblin@llnl.gov> | 2019-12-29 02:04:28 -0800 |
---|---|---|
committer | Todd Gamblin <tgamblin@llnl.gov> | 2019-12-30 13:01:31 -0800 |
commit | e7dc8a2bea23b013a8af4938a5eee298a1f59141 (patch) | |
tree | 78230f5c9d4da0692b6c3500c8e0b49e5d8d6a09 /lib/spack | |
parent | e8394324720aa6bb041e3dddba46faa14626bd85 (diff) | |
download | spack-e7dc8a2bea23b013a8af4938a5eee298a1f59141.tar.gz spack-e7dc8a2bea23b013a8af4938a5eee298a1f59141.tar.bz2 spack-e7dc8a2bea23b013a8af4938a5eee298a1f59141.tar.xz spack-e7dc8a2bea23b013a8af4938a5eee298a1f59141.zip |
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.
Diffstat (limited to 'lib/spack')
-rw-r--r-- | lib/spack/spack/test/cmd/env.py | 2 | ||||
-rw-r--r-- | lib/spack/spack/test/cmd/module.py | 17 | ||||
-rw-r--r-- | lib/spack/spack/test/cmd/spec.py | 2 | ||||
-rw-r--r-- | lib/spack/spack/test/concretize.py | 2 | ||||
-rw-r--r-- | lib/spack/spack/test/concretize_preferences.py | 4 | ||||
-rw-r--r-- | lib/spack/spack/test/conftest.py | 138 | ||||
-rw-r--r-- | lib/spack/spack/test/git_fetch.py | 6 | ||||
-rw-r--r-- | lib/spack/spack/test/hg_fetch.py | 2 | ||||
-rw-r--r-- | lib/spack/spack/test/install.py | 6 | ||||
-rw-r--r-- | lib/spack/spack/test/mirror.py | 2 | ||||
-rw-r--r-- | lib/spack/spack/test/packages.py | 2 | ||||
-rw-r--r-- | lib/spack/spack/test/repo.py | 40 | ||||
-rw-r--r-- | lib/spack/spack/test/spec_dag.py | 2 | ||||
-rw-r--r-- | lib/spack/spack/test/spec_yaml.py | 2 | ||||
-rw-r--r-- | lib/spack/spack/test/svn_fetch.py | 2 | ||||
-rw-r--r-- | lib/spack/spack/test/url_fetch.py | 2 |
16 files changed, 141 insertions, 90 deletions
diff --git a/lib/spack/spack/test/cmd/env.py b/lib/spack/spack/test/cmd/env.py index fa7f9a640e..9d6facfeeb 100644 --- a/lib/spack/spack/test/cmd/env.py +++ b/lib/spack/spack/test/cmd/env.py @@ -26,7 +26,7 @@ import spack.util.spack_json as sjson # 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') diff --git a/lib/spack/spack/test/cmd/module.py b/lib/spack/spack/test/cmd/module.py index 6f2ffcbf85..fa23d7d5aa 100644 --- a/lib/spack/spack/test/cmd/module.py +++ b/lib/spack/spack/test/cmd/module.py @@ -10,9 +10,21 @@ import pytest 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 diff --git a/lib/spack/spack/test/cmd/spec.py b/lib/spack/spack/test/cmd/spec.py index 4637e14a1f..002e0aa1fe 100644 --- a/lib/spack/spack/test/cmd/spec.py +++ b/lib/spack/spack/test/cmd/spec.py @@ -9,7 +9,7 @@ import pytest 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') diff --git a/lib/spack/spack/test/concretize.py b/lib/spack/spack/test/concretize.py index 4b17e755ed..483cae7809 100644 --- a/lib/spack/spack/test/concretize.py +++ b/lib/spack/spack/test/concretize.py @@ -276,7 +276,7 @@ class TestConcretize(object): 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() diff --git a/lib/spack/spack/test/concretize_preferences.py b/lib/spack/spack/test/concretize_preferences.py index e3055192cd..13fc850c18 100644 --- a/lib/spack/spack/test/concretize_preferences.py +++ b/lib/spack/spack/test/concretize_preferences.py @@ -83,7 +83,7 @@ class TestConcretizePreferences(object): '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 @@ class TestConcretizePreferences(object): 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') diff --git a/lib/spack/spack/test/conftest.py b/lib/spack/spack/test/conftest.py index b23c53c20e..26cb0f453d 100644 --- a/lib/spack/spack/test/conftest.py +++ b/lib/spack/spack/test/conftest.py @@ -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 + +@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 + + +@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 repo_path(): - """Session scoped RepoPath object pointing to the mock repository""" - return spack.repo.RepoPath(spack.paths.mock_packages_path) +def mock_repo_path(): + yield spack.repo.RepoPath(spack.paths.mock_packages_path) -@pytest.fixture(scope='module') -def mock_packages(repo_path): +@pytest.fixture(scope='function') +def mock_packages(mock_repo_path): """Use the 'builtin.mock' repository instead of 'builtin'""" - mock_repo = copy.deepcopy(repo_path) - with spack.repo.swap(mock_repo): - yield + with use_repo(mock_repo_path): + yield mock_repo_path @pytest.fixture(scope='function') -def mutable_mock_packages(mock_packages, repo_path): +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. + + This does not actually activate the store for use by Spack -- see the + ``database`` fixture for that. + """ - 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 + 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') diff --git a/lib/spack/spack/test/git_fetch.py b/lib/spack/spack/test/git_fetch.py index ae1e027752..d0c2ca7e3d 100644 --- a/lib/spack/spack/test/git_fetch.py +++ b/lib/spack/spack/test/git_fetch.py @@ -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'): diff --git a/lib/spack/spack/test/hg_fetch.py b/lib/spack/spack/test/hg_fetch.py index 109e3d9d59..5e4fde978b 100644 --- a/lib/spack/spack/test/hg_fetch.py +++ b/lib/spack/spack/test/hg_fetch.py @@ -29,7 +29,7 @@ def test_fetch( secure, mock_hg_repository, config, - mutable_mock_packages + mutable_mock_repo ): """Tries to: diff --git a/lib/spack/spack/test/install.py b/lib/spack/spack/test/install.py index 4bed12456a..1fa892c760 100644 --- a/lib/spack/spack/test/install.py +++ b/lib/spack/spack/test/install.py @@ -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. diff --git a/lib/spack/spack/test/mirror.py b/lib/spack/spack/test/mirror.py index e1b31695e3..bc0c3ed0f7 100644 --- a/lib/spack/spack/test/mirror.py +++ b/lib/spack/spack/test/mirror.py @@ -16,7 +16,7 @@ from spack.util.executable import which 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'] diff --git a/lib/spack/spack/test/packages.py b/lib/spack/spack/test/packages.py index bd4ba95053..4a6957cc5a 100644 --- a/lib/spack/spack/test/packages.py +++ b/lib/spack/spack/test/packages.py @@ -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') diff --git a/lib/spack/spack/test/repo.py b/lib/spack/spack/test/repo.py index e171dbca68..35919f3462 100644 --- a/lib/spack/spack/test/repo.py +++ b/lib/spack/spack/test/repo.py @@ -10,14 +10,6 @@ import spack.repo 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 @@ repo: 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() diff --git a/lib/spack/spack/test/spec_dag.py b/lib/spack/spack/test/spec_dag.py index 628f67948f..e42cd60189 100644 --- a/lib/spack/spack/test/spec_dag.py +++ b/lib/spack/spack/test/spec_dag.py @@ -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): diff --git a/lib/spack/spack/test/spec_yaml.py b/lib/spack/spack/test/spec_yaml.py index 7fd2a36469..4f723988a3 100644 --- a/lib/spack/spack/test/spec_yaml.py +++ b/lib/spack/spack/test/spec_yaml.py @@ -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) diff --git a/lib/spack/spack/test/svn_fetch.py b/lib/spack/spack/test/svn_fetch.py index eb0d22ee7c..b065367cbe 100644 --- a/lib/spack/spack/test/svn_fetch.py +++ b/lib/spack/spack/test/svn_fetch.py @@ -30,7 +30,7 @@ def test_fetch( secure, mock_svn_repository, config, - mutable_mock_packages + mutable_mock_repo ): """Tries to: diff --git a/lib/spack/spack/test/url_fetch.py b/lib/spack/spack/test/url_fetch.py index b4df27336e..1b65be0fd7 100644 --- a/lib/spack/spack/test/url_fetch.py +++ b/lib/spack/spack/test/url_fetch.py @@ -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 |