Spack tests no longer clutter var/spack/stage

- Tests use a session-scoped mock stage directory so as not to interfere
  with the real install.

- Every test is forced to clean up after itself with an additional check.
  We now automatically assert that no new files have been added to
  `spack.stage_path` during each test.

  - This means that tests that fail installs now need to clean up their
    stages, but in all other cases the check is useful.
This commit is contained in:
Todd Gamblin 2017-10-15 23:59:53 -07:00
parent 44bebd7a8f
commit d14816cbaf
6 changed files with 158 additions and 113 deletions

View file

@ -1405,6 +1405,7 @@ def build_process():
echo = logger.echo echo = logger.echo
self.log() self.log()
# Run post install hooks before build stage is removed. # Run post install hooks before build stage is removed.
spack.hooks.post_install(self.spec) spack.hooks.post_install(self.spec)

View file

@ -150,6 +150,7 @@ def test_package_output(tmpdir, capsys, install_mockery, mock_fetch):
assert "'install'\nAFTER INSTALL" in out assert "'install'\nAFTER INSTALL" in out
@pytest.mark.disable_clean_stage_check
def test_install_output_on_build_error(builtin_mock, mock_archive, mock_fetch, def test_install_output_on_build_error(builtin_mock, mock_archive, mock_fetch,
config, install_mockery, capfd): config, install_mockery, capfd):
# capfd interferes with Spack's capturing # capfd interferes with Spack's capturing
@ -161,6 +162,7 @@ def test_install_output_on_build_error(builtin_mock, mock_archive, mock_fetch,
assert 'configure: error: cannot run C compiled programs.' in out assert 'configure: error: cannot run C compiled programs.' in out
@pytest.mark.disable_clean_stage_check
def test_install_output_on_python_error(builtin_mock, mock_archive, mock_fetch, def test_install_output_on_python_error(builtin_mock, mock_archive, mock_fetch,
config, install_mockery): config, install_mockery):
out = install('failing-build', fail_on_error=False) out = install('failing-build', fail_on_error=False)
@ -169,6 +171,7 @@ def test_install_output_on_python_error(builtin_mock, mock_archive, mock_fetch,
assert 'raise InstallError("Expected failure.")' in out assert 'raise InstallError("Expected failure.")' in out
@pytest.mark.disable_clean_stage_check
def test_install_with_source( def test_install_with_source(
builtin_mock, mock_archive, mock_fetch, config, install_mockery): builtin_mock, mock_archive, mock_fetch, config, install_mockery):
"""Verify that source has been copied into place.""" """Verify that source has been copied into place."""
@ -180,6 +183,7 @@ def test_install_with_source(
os.path.join(src, 'configure')) os.path.join(src, 'configure'))
@pytest.mark.disable_clean_stage_check
def test_show_log_on_error(builtin_mock, mock_archive, mock_fetch, def test_show_log_on_error(builtin_mock, mock_archive, mock_fetch,
config, install_mockery, capfd): config, install_mockery, capfd):
"""Make sure --show-log-on-error works.""" """Make sure --show-log-on-error works."""

View file

@ -25,14 +25,16 @@
import collections import collections
import copy import copy
import os import os
import re
import shutil import shutil
import re
import ordereddict_backport import ordereddict_backport
import py import py
import pytest import pytest
from llnl.util.filesystem import remove_linked_tree
import spack import spack
import spack.architecture import spack.architecture
import spack.database import spack.database
@ -71,6 +73,64 @@ def no_chdir():
assert os.getcwd() == original_wd assert os.getcwd() == original_wd
@pytest.fixture(scope='session', autouse=True)
def mock_stage(tmpdir_factory):
"""Mocks up a fake stage directory for use by tests."""
stage_path = spack.stage_path
new_stage = str(tmpdir_factory.mktemp('mock_stage'))
spack.stage_path = new_stage
yield new_stage
spack.stage_path = stage_path
@pytest.fixture(scope='session')
def _ignore_stage_files():
"""Session-scoped helper for check_for_leftover_stage_files.
Used to track which leftover files in the stage have been seen.
"""
# to start with, ignore the .lock file at the stage root.
return set(['.lock'])
def remove_whatever_it_is(path):
"""Type-agnostic remove."""
if os.path.isfile(path):
os.remove(path)
elif os.path.islink(path):
remove_linked_tree(path)
else:
shutil.rmtree(path)
@pytest.fixture(scope='function', autouse=True)
def check_for_leftover_stage_files(request, mock_stage, _ignore_stage_files):
"""Ensure that each test leaves a clean stage when done.
This can be disabled for tests that are expected to dirty the stage
by adding::
@pytest.mark.disable_clean_stage_check
to tests that need it.
"""
yield
files_in_stage = set()
if os.path.exists(spack.stage_path):
files_in_stage = set(
os.listdir(spack.stage_path)) - _ignore_stage_files
if 'disable_clean_stage_check' in request.keywords:
# clean up after tests that are expected to be dirty
for f in files_in_stage:
path = os.path.join(spack.stage_path, f)
remove_whatever_it_is(path)
else:
_ignore_stage_files |= files_in_stage
assert not files_in_stage
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def mock_fetch_cache(monkeypatch): def mock_fetch_cache(monkeypatch):
"""Substitutes spack.fetch_cache with a mock object that does nothing """Substitutes spack.fetch_cache with a mock object that does nothing
@ -193,7 +253,7 @@ def config(configuration_dir):
@pytest.fixture(scope='module') @pytest.fixture(scope='module')
def database(mock_stage, tmpdir_factory, builtin_mock, config): def database(tmpdir_factory, builtin_mock, config):
"""Creates a mock database with some packages installed note that """Creates a mock database with some packages installed note that
the ref count for dyninst here will be 3, as it's recycled the ref count for dyninst here will be 3, as it's recycled
across each install. across each install.
@ -296,18 +356,8 @@ def refresh_db_on_exit(database):
database.refresh() database.refresh()
@pytest.fixture(scope='session')
def mock_stage(tmpdir_factory):
"""Mocks up a fake stage directory for use by tests."""
stage_path = spack.stage_path
new_stage = str(tmpdir_factory.mktemp('mock_stage'))
spack.stage_path = new_stage
yield
spack.stage_path = stage_path
@pytest.fixture() @pytest.fixture()
def install_mockery(mock_stage, tmpdir, config, builtin_mock): def install_mockery(tmpdir, config, builtin_mock):
"""Hooks a fake install directory, DB, and stage directory into Spack.""" """Hooks a fake install directory, DB, and stage directory into Spack."""
layout = spack.store.layout layout = spack.store.layout
db = spack.store.db db = spack.store.db
@ -350,14 +400,13 @@ def fake_fn(self):
@pytest.fixture(scope='session') @pytest.fixture(scope='session')
def mock_archive(mock_stage): def mock_archive(tmpdir_factory):
"""Creates a very simple archive directory with a configure script and a """Creates a very simple archive directory with a configure script and a
makefile that installs to a prefix. Tars it up into an archive. makefile that installs to a prefix. Tars it up into an archive.
""" """
tar = spack.util.executable.which('tar', required=True) tar = spack.util.executable.which('tar', required=True)
stage = spack.stage.Stage('mock-archive-stage')
tmpdir = py.path.local(stage.path) tmpdir = tmpdir_factory.mktemp('mock-archive-dir')
repo_name = 'mock-archive-repo' repo_name = 'mock-archive-repo'
tmpdir.ensure(repo_name, dir=True) tmpdir.ensure(repo_name, dir=True)
repodir = tmpdir.join(repo_name) repodir = tmpdir.join(repo_name)
@ -392,17 +441,16 @@ def mock_archive(mock_stage):
url=('file://' + archive_file), url=('file://' + archive_file),
archive_file=archive_file, archive_file=archive_file,
path=str(repodir)) path=str(repodir))
stage.destroy()
@pytest.fixture(scope='session') @pytest.fixture(scope='session')
def mock_git_repository(mock_stage): def mock_git_repository(tmpdir_factory):
"""Creates a very simple git repository with two branches and """Creates a very simple git repository with two branches and
two commits. two commits.
""" """
git = spack.util.executable.which('git', required=True) git = spack.util.executable.which('git', required=True)
stage = spack.stage.Stage('mock-git-stage')
tmpdir = py.path.local(stage.path) tmpdir = tmpdir_factory.mktemp('mock-git-repo-dir')
repo_name = 'mock-git-repo' repo_name = 'mock-git-repo'
tmpdir.ensure(repo_name, dir=True) tmpdir.ensure(repo_name, dir=True)
repodir = tmpdir.join(repo_name) repodir = tmpdir.join(repo_name)
@ -449,7 +497,6 @@ def mock_git_repository(mock_stage):
r1_file = branch_file r1_file = branch_file
Bunch = spack.util.pattern.Bunch Bunch = spack.util.pattern.Bunch
checks = { checks = {
'master': Bunch( 'master': Bunch(
revision='master', file=r0_file, args={'git': str(repodir)} revision='master', file=r0_file, args={'git': str(repodir)}
@ -469,15 +516,14 @@ def mock_git_repository(mock_stage):
t = Bunch(checks=checks, url=url, hash=rev_hash, path=str(repodir)) t = Bunch(checks=checks, url=url, hash=rev_hash, path=str(repodir))
yield t yield t
stage.destroy()
@pytest.fixture(scope='session') @pytest.fixture(scope='session')
def mock_hg_repository(mock_stage): def mock_hg_repository(tmpdir_factory):
"""Creates a very simple hg repository with two commits.""" """Creates a very simple hg repository with two commits."""
hg = spack.util.executable.which('hg', required=True) hg = spack.util.executable.which('hg', required=True)
stage = spack.stage.Stage('mock-hg-stage')
tmpdir = py.path.local(stage.path) tmpdir = tmpdir_factory.mktemp('mock-hg-repo-dir')
repo_name = 'mock-hg-repo' repo_name = 'mock-hg-repo'
tmpdir.ensure(repo_name, dir=True) tmpdir.ensure(repo_name, dir=True)
repodir = tmpdir.join(repo_name) repodir = tmpdir.join(repo_name)
@ -504,7 +550,6 @@ def mock_hg_repository(mock_stage):
r1 = get_rev() r1 = get_rev()
Bunch = spack.util.pattern.Bunch Bunch = spack.util.pattern.Bunch
checks = { checks = {
'default': Bunch( 'default': Bunch(
revision=r1, file=r1_file, args={'hg': str(repodir)} revision=r1, file=r1_file, args={'hg': str(repodir)}
@ -517,17 +562,15 @@ def mock_hg_repository(mock_stage):
} }
t = Bunch(checks=checks, url=url, hash=get_rev, path=str(repodir)) t = Bunch(checks=checks, url=url, hash=get_rev, path=str(repodir))
yield t yield t
stage.destroy()
@pytest.fixture(scope='session') @pytest.fixture(scope='session')
def mock_svn_repository(mock_stage): def mock_svn_repository(tmpdir_factory):
"""Creates a very simple svn repository with two commits.""" """Creates a very simple svn repository with two commits."""
svn = spack.util.executable.which('svn', required=True) svn = spack.util.executable.which('svn', required=True)
svnadmin = spack.util.executable.which('svnadmin', required=True) svnadmin = spack.util.executable.which('svnadmin', required=True)
stage = spack.stage.Stage('mock-svn-stage')
tmpdir = py.path.local(stage.path) tmpdir = tmpdir_factory.mktemp('mock-svn-stage')
repo_name = 'mock-svn-repo' repo_name = 'mock-svn-repo'
tmpdir.ensure(repo_name, dir=True) tmpdir.ensure(repo_name, dir=True)
repodir = tmpdir.join(repo_name) repodir = tmpdir.join(repo_name)
@ -564,7 +607,6 @@ def mock_svn_repository(mock_stage):
r1 = '2' r1 = '2'
Bunch = spack.util.pattern.Bunch Bunch = spack.util.pattern.Bunch
checks = { checks = {
'default': Bunch( 'default': Bunch(
revision=r1, file=r1_file, args={'svn': url}), revision=r1, file=r1_file, args={'svn': url}),
@ -582,7 +624,6 @@ def get_rev():
return match.group(1) return match.group(1)
t = Bunch(checks=checks, url=url, hash=get_rev, path=str(repodir)) t = Bunch(checks=checks, url=url, hash=get_rev, path=str(repodir))
yield t yield t

View file

@ -69,46 +69,49 @@ def __init__(self, wrapped_stage):
self.test_destroyed = False self.test_destroyed = False
def __enter__(self): def __enter__(self):
self.create()
return self return self
def __exit__(self, *exc): def __exit__(self, exc_type, exc_val, exc_tb):
return True if exc_type is None:
self.destroy()
def destroy(self): def destroy(self):
self.test_destroyed = True self.test_destroyed = True
self.wrapped_stage.destroy() self.wrapped_stage.destroy()
def create(self):
self.wrapped_stage.create()
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self.wrapped_stage, attr) return getattr(self.wrapped_stage, attr)
def test_partial_install_delete_prefix_and_stage(install_mockery, mock_fetch): def test_partial_install_delete_prefix_and_stage(install_mockery, mock_fetch):
spec = Spec('canfail') spec = Spec('canfail').concretized()
spec.concretize()
pkg = spack.repo.get(spec) pkg = spack.repo.get(spec)
remove_prefix = spack.package.Package.remove_prefix remove_prefix = spack.package.Package.remove_prefix
instance_rm_prefix = pkg.remove_prefix instance_rm_prefix = pkg.remove_prefix
try: try:
pkg.succeed = False
spack.package.Package.remove_prefix = mock_remove_prefix spack.package.Package.remove_prefix = mock_remove_prefix
with pytest.raises(MockInstallError): with pytest.raises(MockInstallError):
pkg.do_install() pkg.do_install()
assert os.path.isdir(pkg.prefix) assert os.path.isdir(pkg.prefix)
rm_prefix_checker = RemovePrefixChecker(instance_rm_prefix) rm_prefix_checker = RemovePrefixChecker(instance_rm_prefix)
spack.package.Package.remove_prefix = rm_prefix_checker.remove_prefix spack.package.Package.remove_prefix = rm_prefix_checker.remove_prefix
setattr(pkg, 'succeed', True)
pkg.succeed = True
pkg.stage = MockStage(pkg.stage) pkg.stage = MockStage(pkg.stage)
pkg.do_install(restage=True) pkg.do_install(restage=True)
assert rm_prefix_checker.removed assert rm_prefix_checker.removed
assert pkg.stage.test_destroyed assert pkg.stage.test_destroyed
assert pkg.installed assert pkg.installed
finally: finally:
spack.package.Package.remove_prefix = remove_prefix spack.package.Package.remove_prefix = remove_prefix
pkg._stage = None
try:
delattr(pkg, 'succeed')
except AttributeError:
pass
def test_dont_add_patches_to_installed_package(install_mockery, mock_fetch): def test_dont_add_patches_to_installed_package(install_mockery, mock_fetch):
@ -141,53 +144,50 @@ def test_installed_dependency_request_conflicts(
dependent.concretize() dependent.concretize()
@pytest.mark.disable_clean_stage_check
def test_partial_install_keep_prefix(install_mockery, mock_fetch): def test_partial_install_keep_prefix(install_mockery, mock_fetch):
spec = Spec('canfail') spec = Spec('canfail').concretized()
spec.concretize()
pkg = spack.repo.get(spec) pkg = spack.repo.get(spec)
# Normally the stage should start unset, but other tests set it # Normally the stage should start unset, but other tests set it
pkg._stage = None pkg._stage = None
remove_prefix = spack.package.Package.remove_prefix remove_prefix = spack.package.Package.remove_prefix
try: try:
# If remove_prefix is called at any point in this test, that is an # If remove_prefix is called at any point in this test, that is an
# error # error
pkg.succeed = False # make the build fail
spack.package.Package.remove_prefix = mock_remove_prefix spack.package.Package.remove_prefix = mock_remove_prefix
with pytest.raises(spack.build_environment.ChildError): with pytest.raises(spack.build_environment.ChildError):
pkg.do_install(keep_prefix=True) pkg.do_install(keep_prefix=True)
assert os.path.exists(pkg.prefix) assert os.path.exists(pkg.prefix)
setattr(pkg, 'succeed', True)
pkg.succeed = True # make the build succeed
pkg.stage = MockStage(pkg.stage) pkg.stage = MockStage(pkg.stage)
pkg.do_install(keep_prefix=True) pkg.do_install(keep_prefix=True)
assert pkg.installed assert pkg.installed
assert not pkg.stage.test_destroyed assert not pkg.stage.test_destroyed
finally: finally:
spack.package.Package.remove_prefix = remove_prefix spack.package.Package.remove_prefix = remove_prefix
pkg._stage = None
try:
delattr(pkg, 'succeed')
except AttributeError:
pass
def test_second_install_no_overwrite_first(install_mockery, mock_fetch): def test_second_install_no_overwrite_first(install_mockery, mock_fetch):
spec = Spec('canfail') spec = Spec('canfail').concretized()
spec.concretize()
pkg = spack.repo.get(spec) pkg = spack.repo.get(spec)
remove_prefix = spack.package.Package.remove_prefix remove_prefix = spack.package.Package.remove_prefix
try: try:
spack.package.Package.remove_prefix = mock_remove_prefix spack.package.Package.remove_prefix = mock_remove_prefix
setattr(pkg, 'succeed', True)
pkg.succeed = True
pkg.do_install() pkg.do_install()
assert pkg.installed assert pkg.installed
# If Package.install is called after this point, it will fail # If Package.install is called after this point, it will fail
delattr(pkg, 'succeed') pkg.succeed = False
pkg.do_install() pkg.do_install()
finally: finally:
spack.package.Package.remove_prefix = remove_prefix spack.package.Package.remove_prefix = remove_prefix
try:
delattr(pkg, 'succeed')
except AttributeError:
pass
def test_store(install_mockery, mock_fetch): def test_store(install_mockery, mock_fetch):
@ -196,9 +196,11 @@ def test_store(install_mockery, mock_fetch):
pkg.do_install() pkg.do_install()
@pytest.mark.disable_clean_stage_check
def test_failing_build(install_mockery, mock_fetch): def test_failing_build(install_mockery, mock_fetch):
spec = Spec('failing-build').concretized() spec = Spec('failing-build').concretized()
pkg = spec.package pkg = spec.package
with pytest.raises(spack.build_environment.ChildError): with pytest.raises(spack.build_environment.ChildError):
pkg.do_install() pkg.do_install()

View file

@ -26,11 +26,12 @@
import collections import collections
import os import os
from llnl.util.filesystem import join_path, working_dir
import pytest import pytest
import spack import spack
import spack.stage import spack.stage
import spack.util.executable import spack.util.executable
from llnl.util.filesystem import join_path, working_dir
from spack.stage import Stage from spack.stage import Stage
@ -236,24 +237,20 @@ def test_fetch(self, mock_archive):
check_destroy(stage, self.stage_name) check_destroy(stage, self.stage_name)
def test_no_search_if_default_succeeds( def test_no_search_if_default_succeeds(
self, mock_archive, failing_search_fn self, mock_archive, failing_search_fn):
): stage = Stage(mock_archive.url,
with Stage(
mock_archive.url,
name=self.stage_name, name=self.stage_name,
search_fn=failing_search_fn search_fn=failing_search_fn)
) as stage: with stage:
stage.fetch() stage.fetch()
check_destroy(stage, self.stage_name) check_destroy(stage, self.stage_name)
def test_no_search_mirror_only( def test_no_search_mirror_only(
self, failing_fetch_strategy, failing_search_fn self, failing_fetch_strategy, failing_search_fn):
): stage = Stage(failing_fetch_strategy,
with Stage(
failing_fetch_strategy,
name=self.stage_name, name=self.stage_name,
search_fn=failing_search_fn search_fn=failing_search_fn)
) as stage: with stage:
try: try:
stage.fetch(mirror_only=True) stage.fetch(mirror_only=True)
except spack.fetch_strategy.FetchError: except spack.fetch_strategy.FetchError:
@ -261,11 +258,10 @@ def test_no_search_mirror_only(
check_destroy(stage, self.stage_name) check_destroy(stage, self.stage_name)
def test_search_if_default_fails(self, failing_fetch_strategy, search_fn): def test_search_if_default_fails(self, failing_fetch_strategy, search_fn):
with Stage( stage = Stage(failing_fetch_strategy,
failing_fetch_strategy,
name=self.stage_name, name=self.stage_name,
search_fn=search_fn search_fn=search_fn)
) as stage: with stage:
try: try:
stage.fetch(mirror_only=False) stage.fetch(mirror_only=False)
except spack.fetch_strategy.FetchError: except spack.fetch_strategy.FetchError:
@ -303,40 +299,43 @@ def test_restage(self, mock_archive):
check_destroy(stage, self.stage_name) check_destroy(stage, self.stage_name)
def test_no_keep_without_exceptions(self, mock_archive): def test_no_keep_without_exceptions(self, mock_archive):
with Stage( stage = Stage(mock_archive.url, name=self.stage_name, keep=False)
mock_archive.url, name=self.stage_name, keep=False with stage:
) as stage:
pass pass
check_destroy(stage, self.stage_name) check_destroy(stage, self.stage_name)
@pytest.mark.disable_clean_stage_check
def test_keep_without_exceptions(self, mock_archive): def test_keep_without_exceptions(self, mock_archive):
with Stage( stage = Stage(mock_archive.url, name=self.stage_name, keep=True)
mock_archive.url, name=self.stage_name, keep=True with stage:
) as stage:
pass pass
path = get_stage_path(stage, self.stage_name) path = get_stage_path(stage, self.stage_name)
assert os.path.isdir(path) assert os.path.isdir(path)
@pytest.mark.disable_clean_stage_check
def test_no_keep_with_exceptions(self, mock_archive): def test_no_keep_with_exceptions(self, mock_archive):
class ThisMustFailHere(Exception): class ThisMustFailHere(Exception):
pass pass
stage = Stage(mock_archive.url, name=self.stage_name, keep=False)
try: try:
with Stage( with stage:
mock_archive.url, name=self.stage_name, keep=False
) as stage:
raise ThisMustFailHere() raise ThisMustFailHere()
except ThisMustFailHere: except ThisMustFailHere:
path = get_stage_path(stage, self.stage_name) path = get_stage_path(stage, self.stage_name)
assert os.path.isdir(path) assert os.path.isdir(path)
@pytest.mark.disable_clean_stage_check
def test_keep_exceptions(self, mock_archive): def test_keep_exceptions(self, mock_archive):
class ThisMustFailHere(Exception): class ThisMustFailHere(Exception):
pass pass
stage = Stage(mock_archive.url, name=self.stage_name, keep=True)
try: try:
with Stage( with stage:
mock_archive.url, name=self.stage_name, keep=True
) as stage:
raise ThisMustFailHere() raise ThisMustFailHere()
except ThisMustFailHere: except ThisMustFailHere:
path = get_stage_path(stage, self.stage_name) path = get_stage_path(stage, self.stage_name)
assert os.path.isdir(path) assert os.path.isdir(path)

View file

@ -33,11 +33,9 @@ class Canfail(Package):
version('1.0', '0123456789abcdef0123456789abcdef') version('1.0', '0123456789abcdef0123456789abcdef')
succeed = False
def install(self, spec, prefix): def install(self, spec, prefix):
try: if not self.succeed:
succeed = getattr(self, 'succeed')
if not succeed:
raise InstallError("'succeed' was false") raise InstallError("'succeed' was false")
touch(join_path(prefix, 'an_installation_file')) touch(join_path(prefix, 'an_installation_file'))
except AttributeError:
raise InstallError("'succeed' attribute was not set")