Repository: spack/spack

Issue:
spack test leaves behind config.yaml file
When I run `spack test`, it leaves behind a `config.yaml` that shouldn't be there.

### Steps to reproduce the issue

```console
$ cat ~/.spack/config.yaml
cat: /Users/Adam/.spack/config.yaml: No such file or directory
$ spack test
...
$ cat ~/.spack/config.yaml 
config:
  build_stage: /private/var/folders/21/hwq39zyj4g36x6zjfyl5l8080000gn/T/pytest-of-Adam/pytest-1/mock-stage0
```

### Information on your system

* macOS 10.14.6
* Python 3.7.3

This may also be related to #12647

tests: test_stage fails when run with `-k`
It appears the `test_stage` test fails when run tests filtered on stage.

### Steps to reproduce the issue

```console
$ spack test -k stage
$ spack test -k test_stage
```

### Error Message

The test checks to ensure the `mock_stage` directory was used and fails.  In the first case, the first default`config:build_stage` directory is used for staging.  In the second case, the first path in the `config:build_stage` directory of `test/data/config.yaml` is used.

```console
spec = mpileaks@2.3%gcc@4.5.0~debug~opt+shared+static arch=test-debian6-x86_64 ^callp... arch=test-debian6-x86_64 ^mpich@3.0.4%gcc@4.5.0~debug arch=test-debian6-x86_64

    def check_stage(spec):
        spec = Spec(spec).concretized()
        for dep in spec.traverse():
            stage_name = "%s-%s-%s" % (dep.name, dep.version, dep.dag_hash())
>           assert os.path.isdir(os.path.join(root, stage_name))
E           AssertionError: assert False
E            +  where False = <function isdir at 0x2aaaaab64050>('/tmp/yser/pytest-of-user/pytest-0/mock-stage0/mpileaks-2.3-b4ervofanrurvlocnwdcezqekdphknd6')
E            +    where <function isdir at 0x2aaaaab64050> = <module 'posixpath' from '/usr/tce/packages/python/python-2.7.16/lib/python2.7/posixpath.pyc'>.isdir
E            +      where <module 'posixpath' from '/usr/tce/packages/python/python-2.7.16/lib/python2.7/posixpath.pyc'> = os.path
E            +    and   '/tmp/user/pytest-of-user/pytest-0/mock-stage0/mpileaks-2.3-b4ervofanrurvlocnwdcezqekdphknd6' = <function join at 0x2aaaaab64f50>('/tmp/user/pytest-of-user/pytest-0/mock-stage0', 'mpileaks-2.3-b4ervofanrurvlocnwdcezqekdphknd6')
E            +      where <function join at 0x2aaaaab64f50> = <module 'posixpath' from '/usr/tce/packages/python/python-2.7.16/lib/python2.7/posixpath.pyc'>.join
E            +        where <module 'posixpath' from '/usr/tce/packages/python/python-2.7.16/lib/python2.7/posixpath.pyc'> = os.path
```

### Information on your system

```console
Linux ... SMP Tue May 14 18:38:00 PDT 2019 x86_64 x86_64 x86_64 GNU/Linux
```



Discussion:

Ouch, that means very likely that we have tests that depend on the state left by previous tests to pass :cry: As soon as we have time it could be good to run tests locally with [this plugin](https://pypi.org/project/pytest-random-order/) to find which other tests are showing the same issue.

Resolving patch:
diff --git a/lib/spack/spack/config.py b/lib/spack/spack/config.py
--- a/lib/spack/spack/config.py
+++ b/lib/spack/spack/config.py
@@ -32,6 +32,7 @@
 
 import copy
 import os
+import re
 import sys
 import multiprocessing
 from contextlib import contextmanager
@@ -109,6 +110,9 @@
 #: this is shorter and more readable than listing all choices
 scopes_metavar = '{defaults,system,site,user}[/PLATFORM]'
 
+#: Base name for the (internal) overrides scope.
+overrides_base_name = 'overrides-'
+
 
 def first_existing(dictionary, keys):
     """Get the value of the first key in keys that is in the dictionary."""
@@ -354,6 +358,15 @@ def highest_precedence_scope(self):
         """Non-internal scope with highest precedence."""
         return next(reversed(self.file_scopes), None)
 
+    def matching_scopes(self, reg_expr):
+        """
+        List of all scopes whose names match the provided regular expression.
+
+        For example, matching_scopes(r'^command') will return all scopes
+        whose names begin with `command`.
+        """
+        return [s for s in self.scopes.values() if re.search(reg_expr, s.name)]
+
     def _validate_scope(self, scope):
         """Ensure that scope is valid in this configuration.
 
@@ -543,9 +556,21 @@ def override(path_or_scope, value=None):
         overrides = path_or_scope
         config.push_scope(path_or_scope)
     else:
-        overrides = InternalConfigScope('overrides')
+        base_name = overrides_base_name
+        # Ensure the new override gets a unique scope name
+        current_overrides = [s.name for s in
+                             config.matching_scopes(r'^{0}'.format(base_name))]
+        num_overrides = len(current_overrides)
+        while True:
+            scope_name = '{0}{1}'.format(base_name, num_overrides)
+            if scope_name in current_overrides:
+                num_overrides += 1
+            else:
+                break
+
+        overrides = InternalConfigScope(scope_name)
         config.push_scope(overrides)
-        config.set(path_or_scope, value, scope='overrides')
+        config.set(path_or_scope, value, scope=scope_name)
 
     yield config
 
diff --git a/lib/spack/spack/stage.py b/lib/spack/spack/stage.py
--- a/lib/spack/spack/stage.py
+++ b/lib/spack/spack/stage.py
@@ -91,18 +91,29 @@ def _first_accessible_path(paths):
 
 
 def _resolve_paths(candidates):
-    """Resolve paths, removing extra $user from $tempdir if needed."""
+    """
+    Resolve candidate paths and make user-related adjustments.
+
+    Adjustments involve removing extra $user from $tempdir if $tempdir includes
+    $user and appending $user if it is not present in the path.
+    """
     temp_path = sup.canonicalize_path('$tempdir')
-    tmp_has_usr = getpass.getuser() in temp_path.split(os.path.sep)
+    user = getpass.getuser()
+    tmp_has_usr = user in temp_path.split(os.path.sep)
 
     paths = []
     for path in candidates:
-        # First remove the extra `$user` node from a `$tempdir/$user` entry
-        # for hosts that automatically append `$user` to `$tempdir`.
+        # Remove the extra `$user` node from a `$tempdir/$user` entry for
+        # hosts that automatically append `$user` to `$tempdir`.
         if path.startswith(os.path.join('$tempdir', '$user')) and tmp_has_usr:
             path = os.path.join('$tempdir', path[15:])
 
-        paths.append(sup.canonicalize_path(path))
+        # Ensure the path is unique per user.
+        can_path = sup.canonicalize_path(path)
+        if user not in can_path:
+            can_path = os.path.join(can_path, user)
+
+        paths.append(can_path)
 
     return paths
 
@@ -125,14 +136,6 @@ def get_stage_root():
             raise StageError("No accessible stage paths in:",
                              ' '.join(resolved_candidates))
 
-        # Ensure that path is unique per user in an attempt to avoid
-        # conflicts in shared temporary spaces.  Emulate permissions from
-        # `tempfile.mkdtemp`.
-        user = getpass.getuser()
-        if user not in path:
-            path = os.path.join(path, user)
-            mkdirp(path, mode=stat.S_IRWXU)
-
         _stage_root = path
 
     return _stage_root


Test patch:
diff --git a/lib/spack/spack/test/config.py b/lib/spack/spack/test/config.py
--- a/lib/spack/spack/test/config.py
+++ b/lib/spack/spack/test/config.py
@@ -625,6 +625,51 @@ def test_add_command_line_scopes(tmpdir, mutable_config):
     spack.config._add_command_line_scopes(mutable_config, [str(tmpdir)])
 
 
+def test_nested_override():
+    """Ensure proper scope naming of nested overrides."""
+    base_name = spack.config.overrides_base_name
+
+    def _check_scopes(num_expected, debug_values):
+        scope_names = [s.name for s in spack.config.config.scopes.values() if
+                       s.name.startswith(base_name)]
+
+        for i in range(num_expected):
+            name = '{0}{1}'.format(base_name, i)
+            assert name in scope_names
+
+            data = spack.config.config.get_config('config', name)
+            assert data['debug'] == debug_values[i]
+
+    # Check results from single and nested override
+    with spack.config.override('config:debug', True):
+        with spack.config.override('config:debug', False):
+            _check_scopes(2, [True, False])
+
+        _check_scopes(1, [True])
+
+
+def test_alternate_override(monkeypatch):
+    """Ensure proper scope naming of override when conflict present."""
+    base_name = spack.config.overrides_base_name
+
+    def _matching_scopes(regexpr):
+        return [spack.config.InternalConfigScope('{0}1'.format(base_name))]
+
+    # Check that the alternate naming works
+    monkeypatch.setattr(spack.config.config, 'matching_scopes',
+                        _matching_scopes)
+
+    with spack.config.override('config:debug', False):
+        name = '{0}2'.format(base_name)
+
+        scope_names = [s.name for s in spack.config.config.scopes.values() if
+                       s.name.startswith(base_name)]
+        assert name in scope_names
+
+        data = spack.config.config.get_config('config', name)
+        assert data['debug'] is False
+
+
 def test_immutable_scope(tmpdir):
     config_yaml = str(tmpdir.join('config.yaml'))
     with open(config_yaml, 'w') as f:
diff --git a/lib/spack/spack/test/conftest.py b/lib/spack/spack/test/conftest.py
--- a/lib/spack/spack/test/conftest.py
+++ b/lib/spack/spack/test/conftest.py
@@ -16,7 +16,7 @@
 import pytest
 import ruamel.yaml as yaml
 
-from llnl.util.filesystem import remove_linked_tree
+from llnl.util.filesystem import mkdirp, remove_linked_tree
 
 import spack.architecture
 import spack.compilers
@@ -121,34 +121,27 @@ def reset_compiler_cache():
     spack.compilers._compiler_cache = {}
 
 
-@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
-
-
 @pytest.fixture(scope='function', autouse=True)
-def mock_stage(clear_stage_root, tmpdir_factory, request):
+def mock_stage(tmpdir_factory, monkeypatch, request):
     """Establish the temporary build_stage for the mock archive."""
-    # Workaround to skip mock_stage for 'nomockstage' test cases
+    # The approach with this autouse fixture is to set the stage root
+    # instead of using spack.config.override() to avoid configuration
+    # conflicts with dozens of tests that rely on other configuration
+    # fixtures, such as config.
     if 'nomockstage' not in request.keywords:
+        # Set the build stage to the requested path
         new_stage = tmpdir_factory.mktemp('mock-stage')
         new_stage_path = str(new_stage)
 
-        # Set test_stage_path as the default directory to use for test stages.
-        current = spack.config.get('config:build_stage')
-        spack.config.set('config',
-                         {'build_stage': new_stage_path}, scope='user')
+        # Ensure the source directory exists within the new stage path
+        source_path = os.path.join(new_stage_path,
+                                   spack.stage._source_path_subdir)
+        mkdirp(source_path)
 
-        # Ensure the source directory exists
-        source_path = new_stage.join(spack.stage._source_path_subdir)
-        source_path.ensure(dir=True)
+        monkeypatch.setattr(spack.stage, '_stage_root', new_stage_path)
 
         yield new_stage_path
 
-        spack.config.set('config', {'build_stage': current}, scope='user')
-
         # Clean up the test stage directory
         if os.path.isdir(new_stage_path):
             shutil.rmtree(new_stage_path)
diff --git a/lib/spack/spack/test/stage.py b/lib/spack/spack/test/stage.py
--- a/lib/spack/spack/test/stage.py
+++ b/lib/spack/spack/test/stage.py
@@ -13,7 +13,7 @@
 
 import pytest
 
-from llnl.util.filesystem import mkdirp, working_dir
+from llnl.util.filesystem import mkdirp, touch, working_dir
 
 import spack.paths
 import spack.stage
@@ -40,9 +40,6 @@
 _include_hidden = 2
 _include_extra = 3
 
-# Some standard unix directory that does NOT include the username
-_non_user_root = os.path.join(os.path.sep, 'opt')
-
 
 # Mock fetch directories are expected to appear as follows:
 #
@@ -70,6 +67,13 @@
 #
 
 
+@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
+
+
 def check_expand_archive(stage, stage_name, expected_file_list):
     """
     Ensure the expanded archive directory contains the expected structure and
@@ -176,83 +180,22 @@ def get_stage_path(stage, stage_name):
         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.
 @pytest.fixture
-def bad_stage_path():
-    """Temporarily ensure there is no accessible path for staging."""
-    current = spack.config.get('config:build_stage')
-    spack.config.set('config', {'build_stage': '/no/such/path'}, scope='user')
-    yield
-    spack.config.set('config', {'build_stage': current}, scope='user')
-
-
-@pytest.fixture
-def non_user_path_for_stage(monkeypatch):
-    """Temporarily use a Linux-standard non-user path for staging. """
-    def _can_access(path, perms):
-        return True
-
-    current = spack.config.get('config:build_stage')
-    spack.config.set('config', {'build_stage': [_non_user_root]}, scope='user')
-    monkeypatch.setattr(os, 'access', _can_access)
-    yield
-    spack.config.set('config', {'build_stage': current}, scope='user')
-
-
-@pytest.fixture
-def instance_path_for_stage():
-    """
-    Temporarily use the "traditional" spack instance stage path for staging.
-
-    Note that it can be important for other tests that the previous settings be
-    restored when the test case is over.
-    """
-    current = spack.config.get('config:build_stage')
-    base = canonicalize_path(os.path.join('$spack', 'test-stage'))
-    mkdirp(base)
-    path = tempfile.mkdtemp(dir=base)
-    spack.config.set('config', {'build_stage': path}, scope='user')
-    yield
-    spack.config.set('config', {'build_stage': current}, scope='user')
-    shutil.rmtree(base)
-
-
-@pytest.fixture
-def tmp_path_for_stage(tmpdir):
-    """
-    Use a temporary test directory for staging.
-
-    Note that it can be important for other tests that the previous settings be
-    restored when the test case is over.
-    """
-    current = spack.config.get('config:build_stage')
-    spack.config.set('config', {'build_stage': [str(tmpdir)]}, scope='user')
-    yield tmpdir
-    spack.config.set('config', {'build_stage': current}, scope='user')
-
-
-@pytest.fixture
-def tmp_build_stage_dir(tmpdir):
-    """Establish the temporary build_stage for the mock archive."""
-    test_stage_path = tmpdir.join('stage')
-
-    # Set test_stage_path as the default directory to use for test stages.
-    current = spack.config.get('config:build_stage')
-    spack.config.set('config',
-                     {'build_stage': [str(test_stage_path)]}, scope='user')
+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()
 
-    yield (tmpdir, test_stage_path)
-
-    spack.config.set('config', {'build_stage': current}, scope='user')
+    shutil.rmtree(test_path)
 
 
 @pytest.fixture
-def mock_stage_archive(clear_stage_root, tmp_build_stage_dir, request):
-    """
-    Create the directories and files for the staged mock archive.
-
-    Note that it can be important for other tests that the previous settings be
-    restored when the test case is over.
-    """
+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
@@ -265,7 +208,7 @@ def mock_stage_archive(clear_stage_root, tmp_build_stage_dir, request):
     #
     def create_stage_archive(expected_file_list=[_include_readme]):
         tmpdir, test_stage_path = tmp_build_stage_dir
-        test_stage_path.ensure(dir=True)
+        mkdirp(test_stage_path)
 
         # Create the archive directory and associated file
         archive_dir = tmpdir.join(_archive_base)
@@ -733,82 +676,87 @@ def test_resolve_paths(self):
 
         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
 
+        # resolve paths where user
         tmp = '$tempdir'
+        can_tmpdir = canonicalize_path(tmp)
+        temp_has_user = user in can_tmpdir.split(os.sep)
         paths = [os.path.join(tmp, 'stage'), os.path.join(tmp, '$user')]
-        can_paths = [canonicalize_path(paths[0]), canonicalize_path(tmp)]
-        user = getpass.getuser()
-        if user not in can_paths[1].split(os.path.sep):
-            can_paths[1] = os.path.join(can_paths[1], user)
+        can_paths = [canonicalize_path(p) for p in paths]
+
+        if temp_has_user:
+            can_paths[1] = can_tmpdir
+        else:
+            can_paths[0] = os.path.join(can_paths[0], user)
+
         assert spack.stage._resolve_paths(paths) == can_paths
 
-    def test_get_stage_root_bad_path(self, clear_stage_root, bad_stage_path):
+    def test_get_stage_root_bad_path(self, clear_stage_root):
         """Ensure an invalid stage path root raises a StageError."""
-        with pytest.raises(spack.stage.StageError,
-                           match="No accessible stage paths in"):
-            assert spack.stage.get_stage_root() is None
+        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
 
-    def test_get_stage_root_non_user_path(self, clear_stage_root,
-                                          non_user_path_for_stage):
-        """Ensure a non-user stage root includes the username."""
-        # The challenge here is whether the user has access to the standard
-        # non-user path.  If not, the path should still appear in the error.
-        try:
-            path = spack.stage.get_stage_root()
-            assert getpass.getuser() in path.split(os.path.sep)
+    @pytest.mark.parametrize(
+        'path,purged', [('stage-1234567890abcdef1234567890abcdef', True),
+                        ('stage-abcdef12345678900987654321fedcba', True),
+                        ('stage-a1b2c3', 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)
 
-            # Make sure the cached stage path values are changed appropriately.
-            assert spack.stage._stage_root == path
-        except OSError as e:
-            expected = os.path.join(_non_user_root, getpass.getuser())
-            assert expected in str(e)
+        test_dir = stage_dir.join(path)
+        test_dir.ensure(dir=True)
+        test_path = str(test_dir)
 
-            # Make sure the cached stage path values are unchanged.
-            assert spack.stage._stage_root is None
+        with spack.config.override('config:build_stage', stage_path):
+            stage_root = spack.stage.get_stage_root()
+            assert stage_path == stage_root
 
-    def test_get_stage_root_tmp(self, clear_stage_root, tmp_path_for_stage):
-        """Ensure a temp path stage root is a suitable temp path."""
-        assert spack.stage._stage_root is None
+            spack.stage.purge()
 
-        tmpdir = tmp_path_for_stage
-        path = spack.stage.get_stage_root()
-        assert path == str(tmpdir)
-        assert 'test_get_stage_root_tmp' in path
+            if purged:
+                assert not os.path.exists(test_path)
+            else:
+                assert os.path.exists(test_path)
+                shutil.rmtree(test_path)
 
-        # Make sure the cached stage path values are changed appropriately.
-        assert spack.stage._stage_root == path
+    def test_get_stage_root_in_spack(self, clear_stage_root):
+        """Ensure an instance path is an accessible build stage path."""
 
-        # Add then purge a few directories
-        dir1 = tmpdir.join('stage-1234567890abcdef1234567890abcdef')
-        dir1.ensure(dir=True)
-        dir2 = tmpdir.join('stage-abcdef12345678900987654321fedcba')
-        dir2.ensure(dir=True)
-        dir3 = tmpdir.join('stage-a1b2c3')
-        dir3.ensure(dir=True)
+        base = canonicalize_path(os.path.join('$spack', '.spack-test-stage'))
+        mkdirp(base)
+        test_path = tempfile.mkdtemp(dir=base)
 
-        spack.stage.purge()
-        assert not os.path.exists(str(dir1))
-        assert not os.path.exists(str(dir2))
-        assert os.path.exists(str(dir3))
+        try:
+            with spack.config.override('config:build_stage',  test_path):
+                path = spack.stage.get_stage_root()
 
-        # Cleanup
-        shutil.rmtree(str(dir3))
+                assert 'spack' in path.split(os.path.sep)
 
-    def test_get_stage_root_in_spack(self, clear_stage_root,
-                                     instance_path_for_stage):
-        """Ensure an instance path stage root is a suitable path."""
-        assert spack.stage._stage_root is None
+                # Make sure cached stage path value was changed appropriately
+                assert spack.stage._stage_root == test_path
 
-        path = spack.stage.get_stage_root()
-        assert 'spack' in path.split(os.path.sep)
+                # Make sure the directory exists
+                assert os.path.isdir(spack.stage._stage_root)
 
-        # Make sure the cached stage path values are changed appropriately.
-        assert spack.stage._stage_root == path
+        finally:
+            # Clean up regardless of outcome
+            shutil.rmtree(base)
 
     def test_stage_constructor_no_fetcher(self):
         """Ensure Stage constructor with no URL or fetch strategy fails."""
@@ -879,34 +827,19 @@ def test_diystage_preserve_file(self, tmpdir):
             _file.read() == _readme_contents
 
 
-@pytest.fixture
-def tmp_build_stage_nondir(tmpdir):
-    """Establish the temporary build_stage pointing to non-directory."""
-    test_stage_path = tmpdir.join('stage', 'afile')
-    test_stage_path.ensure(dir=False)
-
-    # Set test_stage_path as the default directory to use for test stages.
-    current = spack.config.get('config:build_stage')
-    stage_dir = os.path.dirname(str(test_stage_path))
-    spack.config.set('config', {'build_stage': [stage_dir]}, scope='user')
-
-    yield test_stage_path
-
-    spack.config.set('config', {'build_stage': current}, scope='user')
-
-
 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
-    test_stage_path.ensure(dir=True)
+    mkdirp(test_stage_path)
 
-    nondir = test_stage_path.join('afile')
-    nondir.ensure(dir=False)
+    nondir = os.path.join(test_stage_path, 'afile')
+    touch(nondir)
     path = str(nondir)
 
     stage = Stage(path, name='')
-    stage.create()  # Should ensure the path is converted to a dir
+    stage.create()
 
+    # Ensure the stage path is "converted" to a directory
     assert os.path.isdir(stage.path)