Source code for spack.test.stage

# 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)

"""Test that the Stage class works correctly."""
import collections
import errno
import getpass
import os
import shutil
import stat
import sys

import pytest

from llnl.util.filesystem import getuid, mkdirp, partition_path, touch, working_dir

import spack.paths
import spack.stage
import spack.util.executable
from spack.resource import Resource
from spack.stage import DIYStage, ResourceStage, Stage, StageComposite
from spack.util.path import canonicalize_path

# The following values are used for common fetch and stage mocking fixtures:
_archive_base = 'test-files'
_archive_fn = '%s.tar.gz' % _archive_base
_extra_fn = 'extra.sh'
_hidden_fn = '.hidden'
_readme_fn = 'README.txt'

_extra_contents = '#!/bin/sh\n'
_hidden_contents = ''
_readme_contents = 'hello world!\n'

# TODO: Replace the following with an enum once guarantee supported (or
# include enum34 for python versions < 3.4.
_include_readme = 1
_include_hidden = 2
_include_extra = 3

_file_prefix = 'file://'
if sys.platform == 'win32':
    _file_prefix += '/'


# Mock fetch directories are expected to appear as follows:
#
# TMPDIR/
#     _archive_fn     archive_url = file:///path/to/_archive_fn
#
# Mock expanded stage directories are expected to have one of two forms,
# depending on how the tarball expands.  Non-exploding tarballs are expected
# to have the following structure:
#
# TMPDIR/                 temp stage dir
#     spack-src/          well-known stage source directory
#         _readme_fn      Optional test_readme (contains _readme_contents)
#     _hidden_fn          Optional hidden file (contains _hidden_contents)
#     _archive_fn         archive_url = file:///path/to/_archive_fn
#
# while exploding tarball directories are expected to be structured as follows:
#
# TMPDIR/                 temp stage dir
#     spack-src/          well-known stage source directory
#         archive_name/   archive dir
#             _readme_fn  test_readme (contains _readme_contents)
#         _extra_fn       test_extra file (contains _extra_contents)
#     _archive_fn         archive_url = file:///path/to/_archive_fn
#


[docs]@pytest.fixture def clear_stage_root(monkeypatch): """Ensure spack.stage._stage_root is not set at test start.""" monkeypatch.setattr(spack.stage, '_stage_root', None) yield
[docs]def check_expand_archive(stage, stage_name, expected_file_list): """ Ensure the expanded archive directory contains the expected structure and files as described in the module-level comments above. """ stage_path = get_stage_path(stage, stage_name) archive_dir = spack.stage._source_path_subdir stage_contents = os.listdir(stage_path) assert _archive_fn in stage_contents assert archive_dir in stage_contents source_path = os.path.join(stage_path, archive_dir) assert source_path == stage.source_path source_contents = os.listdir(source_path) for _include in expected_file_list: if _include == _include_hidden: # The hidden file represent the HFS metadata associated with Mac # OS X tar files so is expected to be in the same directory as # the archive directory. assert _hidden_fn in stage_contents fn = os.path.join(stage_path, _hidden_fn) contents = _hidden_contents elif _include == _include_readme: # The standard README.txt file will be in the source directory if # the tarball didn't explode; otherwise, it will be in the # original archive subdirectory of it. if _archive_base in source_contents: fn = os.path.join(source_path, _archive_base, _readme_fn) else: fn = os.path.join(source_path, _readme_fn) contents = _readme_contents elif _include == _include_extra: assert _extra_fn in source_contents fn = os.path.join(source_path, _extra_fn) contents = _extra_contents else: assert False assert os.path.isfile(fn) with open(fn) as _file: _file.read() == contents
[docs]def check_fetch(stage, stage_name): """ Ensure the fetch resulted in a properly placed archive file as described in the module-level comments. """ stage_path = get_stage_path(stage, stage_name) assert _archive_fn in os.listdir(stage_path) assert os.path.join(stage_path, _archive_fn) == stage.fetcher.archive_file
[docs]def check_destroy(stage, stage_name): """Figure out whether a stage was destroyed correctly.""" stage_path = get_stage_path(stage, stage_name) # check that the stage dir/link was removed. assert not os.path.exists(stage_path) # tmp stage needs to remove tmp dir too. if not stage.managed_by_spack: target = os.path.realpath(stage_path) assert not os.path.exists(target)
[docs]def check_setup(stage, stage_name, archive): """Figure out whether a stage was set up correctly.""" stage_path = get_stage_path(stage, stage_name) # Ensure stage was created in the spack stage directory assert os.path.isdir(stage_path) # Make sure it points to a valid directory target = os.path.realpath(stage_path) assert os.path.isdir(target) assert not os.path.islink(target) # Make sure the directory is in the place we asked it to # be (see setUp, tearDown, and use_tmp) assert target.startswith(str(archive.stage_path))
[docs]def get_stage_path(stage, stage_name): """Figure out where a stage should be living. This depends on whether it's named. """ stage_path = spack.stage.get_stage_root() if stage_name is not None: # If it is a named stage, we know where the stage should be return os.path.join(stage_path, stage_name) else: # If it's unnamed, ensure that we ran mkdtemp in the right spot. assert stage.path is not None assert stage.path.startswith(stage_path) return stage.path
# TODO: Revisit use of the following fixture (and potentially leveraging # the `mock_stage` path in `mock_stage_archive`) per discussions in # #12857. See also #13065.
[docs]@pytest.fixture def tmp_build_stage_dir(tmpdir, clear_stage_root): """Use a temporary test directory for the stage root.""" test_path = str(tmpdir.join('stage')) with spack.config.override('config:build_stage', test_path): yield tmpdir, spack.stage.get_stage_root() shutil.rmtree(test_path)
[docs]@pytest.fixture def mock_stage_archive(tmp_build_stage_dir): """Create the directories and files for the staged mock archive.""" # Mock up a stage area that looks like this: # # tmpdir/ test_files_dir # stage/ test_stage_path (where stage should be) # <_archive_base>/ archive_dir_path # <_readme_fn> Optional test_readme (contains _readme_contents) # <_extra_fn> Optional extra file (contains _extra_contents) # <_hidden_fn> Optional hidden file (contains _hidden_contents) # <_archive_fn> archive_url = file:///path/to/<_archive_fn> # def create_stage_archive(expected_file_list=[_include_readme]): tmpdir, test_stage_path = tmp_build_stage_dir mkdirp(test_stage_path) # Create the archive directory and associated file archive_dir = tmpdir.join(_archive_base) archive = tmpdir.join(_archive_fn) archive_url = _file_prefix + str(archive) archive_dir.ensure(dir=True) # Create the optional files as requested and make sure expanded # archive peers are included. tar_args = ['czf', str(_archive_fn), _archive_base] for _include in expected_file_list: if _include == _include_hidden: # The hidden file case stands in for the way Mac OS X tar files # represent HFS metadata. Locate in the same directory as the # archive file. tar_args.append(_hidden_fn) fn, contents = (tmpdir.join(_hidden_fn), _hidden_contents) elif _include == _include_readme: # The usual README.txt file is contained in the archive dir. fn, contents = (archive_dir.join(_readme_fn), _readme_contents) elif _include == _include_extra: # The extra file stands in for exploding tar files so needs # to be in the same directory as the archive file. tar_args.append(_extra_fn) fn, contents = (tmpdir.join(_extra_fn), _extra_contents) else: break fn.write(contents) # Create the archive file with tmpdir.as_cwd(): tar = spack.util.executable.which('tar', required=True) tar(*tar_args) Archive = collections.namedtuple( 'Archive', ['url', 'tmpdir', 'stage_path', 'archive_dir'] ) return Archive(url=archive_url, tmpdir=tmpdir, stage_path=test_stage_path, archive_dir=archive_dir) return create_stage_archive
[docs]@pytest.fixture def mock_noexpand_resource(tmpdir): """Set up a non-expandable resource in the tmpdir prior to staging.""" test_resource = tmpdir.join('resource-no-expand.sh') test_resource.write("an example resource") return str(test_resource)
[docs]@pytest.fixture def mock_expand_resource(tmpdir): """Sets up an expandable resource in tmpdir prior to staging.""" # Mock up an expandable resource: # # tmpdir/ test_files_dir # resource-expand/ resource source dir # resource-file.txt resource contents (contains 'test content') # resource.tar.gz archive of resource content # subdir = 'resource-expand' resource_dir = tmpdir.join(subdir) resource_dir.ensure(dir=True) archive_name = 'resource.tar.gz' archive = tmpdir.join(archive_name) archive_url = _file_prefix + str(archive) filename = 'resource-file.txt' test_file = resource_dir.join(filename) test_file.write('test content\n') with tmpdir.as_cwd(): tar = spack.util.executable.which('tar', required=True) tar('czf', str(archive_name), subdir) MockResource = collections.namedtuple( 'MockResource', ['url', 'files']) return MockResource(archive_url, [filename])
[docs]@pytest.fixture def composite_stage_with_expanding_resource( mock_stage_archive, mock_expand_resource): """Sets up a composite for expanding resources prior to staging.""" composite_stage = StageComposite() archive = mock_stage_archive() root_stage = Stage(archive.url) composite_stage.append(root_stage) test_resource_fetcher = spack.fetch_strategy.from_kwargs( url=mock_expand_resource.url) # Specify that the resource files are to be placed in the 'resource-dir' # directory test_resource = Resource( 'test_resource', test_resource_fetcher, '', 'resource-dir') resource_stage = ResourceStage( test_resource_fetcher, root_stage, test_resource) composite_stage.append(resource_stage) return composite_stage, root_stage, resource_stage, mock_expand_resource
[docs]@pytest.fixture def failing_search_fn(): """Returns a search function that fails! Always!""" def _mock(): raise Exception("This should not have been called") return _mock
[docs]@pytest.fixture def failing_fetch_strategy(): """Returns a fetch strategy that fails.""" class FailingFetchStrategy(spack.fetch_strategy.FetchStrategy): def fetch(self): raise spack.fetch_strategy.FailedDownloadError( "<non-existent URL>", "This implementation of FetchStrategy always fails" ) return FailingFetchStrategy()
[docs]@pytest.fixture def search_fn(): """Returns a search function that always succeeds.""" class _Mock(object): performed_search = False def __call__(self): self.performed_search = True return [] return _Mock()
[docs]def check_stage_dir_perms(prefix, path): """Check the stage directory perms to ensure match expectations.""" # Ensure the path's subdirectories -- to `$user` -- have their parent's # perms while those from `$user` on are owned and restricted to the # user. assert path.startswith(prefix) user = getpass.getuser() prefix_status = os.stat(prefix) uid = getuid() # Obtain lists of ancestor and descendant paths of the $user node, if any. # # Skip processing prefix ancestors since no guarantee they will be in the # required group (e.g. $TEMPDIR on HPC machines). skip = prefix if prefix.endswith(os.sep) else prefix + os.sep group_paths, user_node, user_paths = partition_path(path.replace(skip, ""), user) for p in group_paths: p_status = os.stat(os.path.join(prefix, p)) assert p_status.st_gid == prefix_status.st_gid assert p_status.st_mode == prefix_status.st_mode # Add the path ending with the $user node to the user paths to ensure paths # from $user (on down) meet the ownership and permission requirements. if user_node: user_paths.insert(0, user_node) for p in user_paths: p_status = os.stat(os.path.join(prefix, p)) assert uid == p_status.st_uid assert p_status.st_mode & stat.S_IRWXU == stat.S_IRWXU
[docs]@pytest.mark.usefixtures('mock_packages') class TestStage(object): stage_name = 'spack-test-stage'
[docs] def test_setup_and_destroy_name_with_tmp(self, mock_stage_archive): archive = mock_stage_archive() with Stage(archive.url, name=self.stage_name) as stage: check_setup(stage, self.stage_name, archive) check_destroy(stage, self.stage_name)
[docs] def test_setup_and_destroy_name_without_tmp(self, mock_stage_archive): archive = mock_stage_archive() with Stage(archive.url, name=self.stage_name) as stage: check_setup(stage, self.stage_name, archive) check_destroy(stage, self.stage_name)
[docs] def test_setup_and_destroy_no_name_with_tmp(self, mock_stage_archive): archive = mock_stage_archive() with Stage(archive.url) as stage: check_setup(stage, None, archive) check_destroy(stage, None)
[docs] def test_noexpand_stage_file( self, mock_stage_archive, mock_noexpand_resource): """When creating a stage with a nonexpanding URL, the 'archive_file' property of the stage should refer to the path of that file. """ test_noexpand_fetcher = spack.fetch_strategy.from_kwargs( url=_file_prefix + mock_noexpand_resource, expand=False) with Stage(test_noexpand_fetcher) as stage: stage.fetch() stage.expand_archive() assert os.path.exists(stage.archive_file)
[docs] @pytest.mark.disable_clean_stage_check def test_composite_stage_with_noexpand_resource( self, mock_stage_archive, mock_noexpand_resource): archive = mock_stage_archive() composite_stage = StageComposite() root_stage = Stage(archive.url) composite_stage.append(root_stage) resource_dst_name = 'resource-dst-name.sh' test_resource_fetcher = spack.fetch_strategy.from_kwargs( url=_file_prefix + mock_noexpand_resource, expand=False) test_resource = Resource( 'test_resource', test_resource_fetcher, resource_dst_name, None) resource_stage = ResourceStage( test_resource_fetcher, root_stage, test_resource) composite_stage.append(resource_stage) composite_stage.create() composite_stage.fetch() composite_stage.expand_archive() assert composite_stage.expanded # Archive is expanded assert os.path.exists( os.path.join(composite_stage.source_path, resource_dst_name))
[docs] @pytest.mark.disable_clean_stage_check def test_composite_stage_with_expand_resource( self, composite_stage_with_expanding_resource): composite_stage, root_stage, resource_stage, mock_resource = ( composite_stage_with_expanding_resource) composite_stage.create() composite_stage.fetch() composite_stage.expand_archive() assert composite_stage.expanded # Archive is expanded for fname in mock_resource.files: file_path = os.path.join( root_stage.source_path, 'resource-dir', fname) assert os.path.exists(file_path) # Perform a little cleanup shutil.rmtree(root_stage.path)
[docs] @pytest.mark.disable_clean_stage_check def test_composite_stage_with_expand_resource_default_placement( self, composite_stage_with_expanding_resource): """For a resource which refers to a compressed archive which expands to a directory, check that by default the resource is placed in the source_path of the root stage with the name of the decompressed directory. """ composite_stage, root_stage, resource_stage, mock_resource = ( composite_stage_with_expanding_resource) resource_stage.resource.placement = None composite_stage.create() composite_stage.fetch() composite_stage.expand_archive() for fname in mock_resource.files: file_path = os.path.join( root_stage.source_path, 'resource-expand', fname) assert os.path.exists(file_path) # Perform a little cleanup shutil.rmtree(root_stage.path)
[docs] def test_setup_and_destroy_no_name_without_tmp(self, mock_stage_archive): archive = mock_stage_archive() with Stage(archive.url) as stage: check_setup(stage, None, archive) check_destroy(stage, None)
[docs] @pytest.mark.parametrize('debug', [False, True]) def test_fetch(self, mock_stage_archive, debug): archive = mock_stage_archive() with spack.config.override('config:debug', debug): with Stage(archive.url, name=self.stage_name) as stage: stage.fetch() check_setup(stage, self.stage_name, archive) check_fetch(stage, self.stage_name) check_destroy(stage, self.stage_name)
[docs] def test_no_search_if_default_succeeds( self, mock_stage_archive, failing_search_fn): archive = mock_stage_archive() stage = Stage(archive.url, name=self.stage_name, search_fn=failing_search_fn) with stage: stage.fetch() check_destroy(stage, self.stage_name)
[docs] def test_no_search_mirror_only( self, failing_fetch_strategy, failing_search_fn): stage = Stage(failing_fetch_strategy, name=self.stage_name, search_fn=failing_search_fn) with stage: try: stage.fetch(mirror_only=True) except spack.fetch_strategy.FetchError: pass check_destroy(stage, self.stage_name)
[docs] @pytest.mark.parametrize( "err_msg,expected", [('Fetch from fetch.test.com', 'Fetch from fetch.test.com'), (None, 'All fetchers failed')]) def test_search_if_default_fails(self, failing_fetch_strategy, search_fn, err_msg, expected): stage = Stage(failing_fetch_strategy, name=self.stage_name, search_fn=search_fn) with stage: with pytest.raises(spack.fetch_strategy.FetchError, match=expected): stage.fetch(mirror_only=False, err_msg=err_msg) check_destroy(stage, self.stage_name) assert search_fn.performed_search
[docs] def test_ensure_one_stage_entry(self, mock_stage_archive): archive = mock_stage_archive() with Stage(archive.url, name=self.stage_name) as stage: stage.fetch() stage_path = get_stage_path(stage, self.stage_name) spack.fetch_strategy._ensure_one_stage_entry(stage_path) check_destroy(stage, self.stage_name)
[docs] @pytest.mark.parametrize("expected_file_list", [ [], [_include_readme], [_include_extra, _include_readme], [_include_hidden, _include_readme]]) def test_expand_archive(self, expected_file_list, mock_stage_archive): archive = mock_stage_archive(expected_file_list) with Stage(archive.url, name=self.stage_name) as stage: stage.fetch() check_setup(stage, self.stage_name, archive) check_fetch(stage, self.stage_name) stage.expand_archive() check_expand_archive(stage, self.stage_name, expected_file_list) check_destroy(stage, self.stage_name)
[docs] def test_expand_archive_extra_expand(self, mock_stage_archive): """Test expand with an extra expand after expand (i.e., no-op).""" archive = mock_stage_archive() with Stage(archive.url, name=self.stage_name) as stage: stage.fetch() check_setup(stage, self.stage_name, archive) check_fetch(stage, self.stage_name) stage.expand_archive() stage.fetcher.expand() check_expand_archive(stage, self.stage_name, [_include_readme]) check_destroy(stage, self.stage_name)
[docs] def test_restage(self, mock_stage_archive): archive = mock_stage_archive() with Stage(archive.url, name=self.stage_name) as stage: stage.fetch() stage.expand_archive() with working_dir(stage.source_path): check_expand_archive(stage, self.stage_name, [_include_readme]) # Try to make a file in the old archive dir with open('foobar', 'w') as file: file.write("this file is to be destroyed.") assert 'foobar' in os.listdir(stage.source_path) # Make sure the file is not there after restage. stage.restage() check_fetch(stage, self.stage_name) assert 'foobar' not in os.listdir(stage.source_path) check_destroy(stage, self.stage_name)
[docs] def test_no_keep_without_exceptions(self, mock_stage_archive): archive = mock_stage_archive() stage = Stage(archive.url, name=self.stage_name, keep=False) with stage: pass check_destroy(stage, self.stage_name)
[docs] @pytest.mark.disable_clean_stage_check def test_keep_without_exceptions(self, mock_stage_archive): archive = mock_stage_archive() stage = Stage(archive.url, name=self.stage_name, keep=True) with stage: pass path = get_stage_path(stage, self.stage_name) assert os.path.isdir(path)
[docs] @pytest.mark.disable_clean_stage_check def test_no_keep_with_exceptions(self, mock_stage_archive): class ThisMustFailHere(Exception): pass archive = mock_stage_archive() stage = Stage(archive.url, name=self.stage_name, keep=False) try: with stage: raise ThisMustFailHere() except ThisMustFailHere: path = get_stage_path(stage, self.stage_name) assert os.path.isdir(path)
[docs] @pytest.mark.disable_clean_stage_check def test_keep_exceptions(self, mock_stage_archive): class ThisMustFailHere(Exception): pass archive = mock_stage_archive() stage = Stage(archive.url, name=self.stage_name, keep=True) try: with stage: raise ThisMustFailHere() except ThisMustFailHere: path = get_stage_path(stage, self.stage_name) assert os.path.isdir(path)
[docs] def test_source_path_available(self, mock_stage_archive): """Ensure source path available but does not exist on instantiation.""" archive = mock_stage_archive() stage = Stage(archive.url, name=self.stage_name) source_path = stage.source_path assert source_path assert source_path.endswith(spack.stage._source_path_subdir) assert not os.path.exists(source_path)
[docs] @pytest.mark.skipif(sys.platform == 'win32', reason="Not supported on Windows (yet)") @pytest.mark.skipif(getuid() == 0, reason='user is root') def test_first_accessible_path(self, tmpdir): """Test _first_accessible_path names.""" spack_dir = tmpdir.join('paths') name = str(spack_dir) files = [os.path.join(os.path.sep, 'no', 'such', 'path'), name] # Ensure the tmpdir path is returned since the user should have access path = spack.stage._first_accessible_path(files) assert path == name assert os.path.isdir(path) check_stage_dir_perms(str(tmpdir), path) # Ensure an existing path is returned spack_subdir = spack_dir.join('existing').ensure(dir=True) subdir = str(spack_subdir) path = spack.stage._first_accessible_path([subdir]) assert path == subdir # Ensure a path with a `$user` node has the right permissions # for its subdirectories. user = getpass.getuser() user_dir = spack_dir.join(user, 'has', 'paths') user_path = str(user_dir) path = spack.stage._first_accessible_path([user_path]) assert path == user_path check_stage_dir_perms(str(tmpdir), path) # Cleanup shutil.rmtree(str(name))
[docs] @pytest.mark.skipif(sys.platform == 'win32', reason="Not supported on Windows (yet)") def test_create_stage_root(self, tmpdir, no_path_access): """Test create_stage_root permissions.""" test_dir = tmpdir.join('path') test_path = str(test_dir) try: if getpass.getuser() in str(test_path).split(os.sep): # Simply ensure directory created if tmpdir includes user spack.stage.create_stage_root(test_path) assert os.path.exists(test_path) p_stat = os.stat(test_path) assert p_stat.st_mode & stat.S_IRWXU == stat.S_IRWXU else: # Ensure an OS Error is raised on created, non-user directory with pytest.raises(OSError) as exc_info: spack.stage.create_stage_root(test_path) assert exc_info.value.errno == errno.EACCES finally: try: shutil.rmtree(test_path) except OSError: pass
[docs] @pytest.mark.nomockstage def test_create_stage_root_bad_uid(self, tmpdir, monkeypatch): """ Test the code path that uses an existing user path -- whether `$user` in `$tempdir` or not -- and triggers the generation of the UID mismatch warning. This situation can happen with some `config:build_stage` settings for teams using a common service account for installing software. """ orig_stat = os.stat class MinStat: st_mode = -1 st_uid = -1 def _stat(path): p_stat = orig_stat(path) fake_stat = MinStat() fake_stat.st_mode = p_stat.st_mode return fake_stat user_dir = tmpdir.join(getpass.getuser()) user_dir.ensure(dir=True) user_path = str(user_dir) # TODO: If we could guarantee access to the monkeypatch context # function (i.e., 3.6.0 on), the call and assertion could be moved # to a with block, such as: # # with monkeypatch.context() as m: # m.setattr(os, 'stat', _stat) # spack.stage.create_stage_root(user_path) # assert os.stat(user_path).st_uid != os.getuid() monkeypatch.setattr(os, 'stat', _stat) spack.stage.create_stage_root(user_path) # The following check depends on the patched os.stat as a poor # substitute for confirming the generated warnings. assert os.stat(user_path).st_uid != getuid()
[docs] def test_resolve_paths(self): """Test _resolve_paths.""" assert spack.stage._resolve_paths([]) == [] # resolved path without user appends user paths = [os.path.join(os.path.sep, 'a', 'b', 'c')] user = getpass.getuser() can_paths = [os.path.join(paths[0], user)] assert spack.stage._resolve_paths(paths) == can_paths # resolved path with node including user does not append user paths = [os.path.join(os.path.sep, 'spack-{0}'.format(user), 'stage')] assert spack.stage._resolve_paths(paths) == paths tempdir = '$tempdir' can_tempdir = canonicalize_path(tempdir) user = getpass.getuser() temp_has_user = user in can_tempdir.split(os.sep) paths = [os.path.join(tempdir, 'stage'), os.path.join(tempdir, '$user'), os.path.join(tempdir, '$user', '$user'), os.path.join(tempdir, '$user', 'stage', '$user')] res_paths = [canonicalize_path(p) for p in paths] if temp_has_user: res_paths[1] = can_tempdir res_paths[2] = os.path.join(can_tempdir, user) res_paths[3] = os.path.join(can_tempdir, 'stage', user) else: res_paths[0] = os.path.join(res_paths[0], user) assert spack.stage._resolve_paths(paths) == res_paths
[docs] @pytest.mark.skipif(sys.platform == 'win32', reason="Not supported on Windows (yet)") @pytest.mark.skipif(getuid() == 0, reason='user is root') def test_get_stage_root_bad_path(self, clear_stage_root): """Ensure an invalid stage path root raises a StageError.""" with spack.config.override('config:build_stage', '/no/such/path'): with pytest.raises(spack.stage.StageError, match="No accessible stage paths in"): spack.stage.get_stage_root() # Make sure the cached stage path values are unchanged. assert spack.stage._stage_root is None
[docs] @pytest.mark.parametrize( 'path,purged', [('spack-stage-1234567890abcdef1234567890abcdef', True), ('spack-stage-anything-goes-here', True), ('stage-spack', False)]) def test_stage_purge(self, tmpdir, clear_stage_root, path, purged): """Test purging of stage directories.""" stage_dir = tmpdir.join('stage') stage_path = str(stage_dir) test_dir = stage_dir.join(path) test_dir.ensure(dir=True) test_path = str(test_dir) with spack.config.override('config:build_stage', stage_path): stage_root = spack.stage.get_stage_root() assert stage_path == stage_root spack.stage.purge() if purged: assert not os.path.exists(test_path) else: assert os.path.exists(test_path) shutil.rmtree(test_path)
[docs] def test_stage_constructor_no_fetcher(self): """Ensure Stage constructor with no URL or fetch strategy fails.""" with pytest.raises(ValueError): with Stage(None): pass
[docs] def test_stage_constructor_with_path(self, tmpdir): """Ensure Stage constructor with a path uses it.""" testpath = str(tmpdir) with Stage('file:///does-not-exist', path=testpath) as stage: assert stage.path == testpath
[docs] def test_diystage_path_none(self): """Ensure DIYStage for path=None behaves as expected.""" with pytest.raises(ValueError): DIYStage(None)
[docs] def test_diystage_path_invalid(self): """Ensure DIYStage for an invalid path behaves as expected.""" with pytest.raises(spack.stage.StagePathError): DIYStage('/path/does/not/exist')
[docs] def test_diystage_path_valid(self, tmpdir): """Ensure DIYStage for a valid path behaves as expected.""" path = str(tmpdir) stage = DIYStage(path) assert stage.path == path assert stage.source_path == path # Order doesn't really matter for DIYStage since they are # basically NOOPs; however, call each since they are part # of the normal stage usage and to ensure full test coverage. stage.create() # Only sets the flag value assert stage.created stage.cache_local() # Only outputs a message stage.fetch() # Only outputs a message stage.check() # Only outputs a message stage.expand_archive() # Only outputs a message assert stage.expanded # The path/source_path does exist with pytest.raises(spack.stage.RestageError): stage.restage() stage.destroy() # A no-op assert stage.path == path # Ensure can still access attributes assert os.path.exists(stage.source_path) # Ensure path still exists
[docs] def test_diystage_preserve_file(self, tmpdir): """Ensure DIYStage preserves an existing file.""" # Write a file to the temporary directory fn = tmpdir.join(_readme_fn) fn.write(_readme_contents) # Instantiate the DIYStage and ensure the above file is unchanged. path = str(tmpdir) stage = DIYStage(path) assert os.path.isdir(path) assert os.path.isfile(str(fn)) stage.create() # Only sets the flag value readmefn = str(fn) assert os.path.isfile(readmefn) with open(readmefn) as _file: _file.read() == _readme_contents
[docs]@pytest.mark.skipif(sys.platform == 'win32', reason="Not supported on Windows (yet)") def test_stage_create_replace_path(tmp_build_stage_dir): """Ensure stage creation replaces a non-directory path.""" _, test_stage_path = tmp_build_stage_dir mkdirp(test_stage_path) nondir = os.path.join(test_stage_path, 'afile') touch(nondir) path = str(nondir) stage = Stage(path, name='') stage.create() # Ensure the stage path is "converted" to a directory assert os.path.isdir(stage.path)
[docs]@pytest.mark.skipif(sys.platform == 'win32', reason="Not supported on Windows (yet)") def test_cannot_access(capsys): """Ensure can_access dies with the expected error.""" with pytest.raises(SystemExit): # It's far more portable to use a non-existent filename. spack.stage.ensure_access('/no/such/file') captured = capsys.readouterr() assert 'Insufficient permissions' in str(captured)