Repository: spack/spack

Issue:
`spack clean -s` doesn't clean stage dirs
I'm working on a mac, running OSX 10.13.6, with a git tree that's at `cfc17e95062a8d974bd95b881070199ffcb343f9`.

I believe that `spack clean -s` should clean up the staging dirs created by `spack stage <package>` but it doesn't seem to.

Might be "problem exists between keyboard and chair", but thought I'd check.

If it's a bug, I'll go hunting.  If not, I'll get more coffee.

```
$ ./bin/spack stage git-lfs
==> Fetching https://github.com/git-lfs/git-lfs/archive/v2.8.0.tar.gz
######################################################################## 100.0%
==> Staging archive: /var/folders/st/vgtczy7j31xgkh9npzh_rg6c0000gn/T/hartzell/spack-stage/git-lfs-2.8.0-cdow5we7ypqz3kqos6xab2tymj32qexo/v2.8.0.tar.gz
==> Created stage in /var/folders/st/vgtczy7j31xgkh9npzh_rg6c0000gn/T/hartzell/spack-stage/git-lfs-2.8.0-cdow5we7ypqz3kqos6xab2tymj32qexo
$ ls /var/folders/st/vgtczy7j31xgkh9npzh_rg6c0000gn/T/hartzell/spack-stage/git-lfs-2.8.0-cdow5we7ypqz3kqos6xab2tymj32qexo
spack-src	v2.8.0.tar.gz
$ ./bin/spack clean -s
==> Removing all temporary build stages
$ ls /var/folders/st/vgtczy7j31xgkh9npzh_rg6c0000gn/T/hartzell/spack-stage/git-lfs-2.8.0-cdow5we7ypqz3kqos6xab2tymj32qexo
spack-src	v2.8.0.tar.gz
$ ./bin/spack clean -a
==> Removing all temporary build stages
==> Removing cached downloads
==> Removing cached information on repositories
==> Removing python cache files
$ ls /var/folders/st/vgtczy7j31xgkh9npzh_rg6c0000gn/T/hartzell/spack-stage/git-lfs-2.8.0-cdow5we7ypqz3kqos6xab2tymj32qexo
spack-src	v2.8.0.tar.gz
$
```




Discussion:

Resolving patch:
diff --git a/lib/spack/llnl/util/filesystem.py b/lib/spack/llnl/util/filesystem.py
--- a/lib/spack/llnl/util/filesystem.py
+++ b/lib/spack/llnl/util/filesystem.py
@@ -49,6 +49,8 @@
     'is_exe',
     'join_path',
     'mkdirp',
+    'partition_path',
+    'prefixes',
     'remove_dead_links',
     'remove_if_dead_link',
     'remove_linked_tree',
@@ -1607,3 +1609,70 @@ def search_paths_for_executables(*path_hints):
             executable_paths.append(bin_dir)
 
     return executable_paths
+
+
+def partition_path(path, entry=None):
+    """
+    Split the prefixes of the path at the first occurrence of entry and
+    return a 3-tuple containing a list of the prefixes before the entry, a
+    string of the prefix ending with the entry, and a list of the prefixes
+    after the entry.
+
+    If the entry is not a node in the path, the result will be the prefix list
+    followed by an empty string and an empty list.
+    """
+    paths = prefixes(path)
+
+    if entry is not None:
+        # Derive the index of entry within paths, which will correspond to
+        # the location of the entry in within the path.
+        try:
+            entries = path.split(os.sep)
+            i = entries.index(entry)
+            if '' in entries:
+                i -= 1
+            return paths[:i], paths[i], paths[i + 1:]
+        except ValueError:
+            pass
+
+    return paths, '', []
+
+
+def prefixes(path):
+    """
+    Returns a list containing the path and its ancestors, top-to-bottom.
+
+    The list for an absolute path will not include an ``os.sep`` entry.
+    For example, assuming ``os.sep`` is ``/``, given path ``/ab/cd/efg``
+    the resulting paths will be, in order: ``/ab``, ``/ab/cd``, and
+    ``/ab/cd/efg``
+
+    The list for a relative path starting ``./`` will not include ``.``.
+    For example, path ``./hi/jkl/mn`` results in a list with the following
+    paths, in order: ``./hi``, ``./hi/jkl``, and ``./hi/jkl/mn``.
+
+    Parameters:
+        path (str): the string used to derive ancestor paths
+
+    Returns:
+        A list containing ancestor paths in order and ending with the path
+    """
+    if not path:
+        return []
+
+    parts = path.strip(os.sep).split(os.sep)
+    if path.startswith(os.sep):
+        parts.insert(0, os.sep)
+    paths = [os.path.join(*parts[:i + 1]) for i in range(len(parts))]
+
+    try:
+        paths.remove(os.sep)
+    except ValueError:
+        pass
+
+    try:
+        paths.remove('.')
+    except ValueError:
+        pass
+
+    return paths
diff --git a/lib/spack/spack/package.py b/lib/spack/spack/package.py
--- a/lib/spack/spack/package.py
+++ b/lib/spack/spack/package.py
@@ -56,7 +56,7 @@
 from llnl.util.tty.color import colorize
 from spack.filesystem_view import YamlFilesystemView
 from spack.util.executable import which
-from spack.stage import Stage, ResourceStage, StageComposite
+from spack.stage import stage_prefix, Stage, ResourceStage, StageComposite
 from spack.util.environment import dump_environment
 from spack.util.package_hash import package_hash
 from spack.version import Version
@@ -753,7 +753,8 @@ def _make_root_stage(self, fetcher):
         mp = spack.mirror.mirror_archive_path(self.spec, fetcher)
         # Construct a path where the stage should build..
         s = self.spec
-        stage_name = "%s-%s-%s" % (s.name, s.version, s.dag_hash())
+        stage_name = "{0}{1}-{2}-{3}".format(stage_prefix, s.name, s.version,
+                                             s.dag_hash())
 
         def download_search():
             dynamic_fetcher = fs.from_list_url(self)
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
@@ -4,7 +4,6 @@
 # SPDX-License-Identifier: (Apache-2.0 OR MIT)
 
 import os
-import re
 import stat
 import sys
 import errno
@@ -17,7 +16,7 @@
 
 import llnl.util.tty as tty
 from llnl.util.filesystem import mkdirp, can_access, install, install_tree
-from llnl.util.filesystem import remove_linked_tree
+from llnl.util.filesystem import partition_path, remove_linked_tree
 
 import spack.paths
 import spack.caches
@@ -34,40 +33,63 @@
 _source_path_subdir = 'spack-src'
 
 # The temporary stage name prefix.
-_stage_prefix = 'spack-stage-'
+stage_prefix = 'spack-stage-'
 
 
 def _create_stage_root(path):
     """Create the stage root directory and ensure appropriate access perms."""
     assert path.startswith(os.path.sep) and len(path.strip()) > 1
 
-    curr_dir = os.path.sep
-    parts = path.strip(os.path.sep).split(os.path.sep)
-    for i, part in enumerate(parts):
-        curr_dir = os.path.join(curr_dir, part)
-        if not os.path.exists(curr_dir):
-            rest = os.path.join(os.path.sep, *parts[i + 1:])
-            user_parts = rest.partition(os.path.sep + getpass.getuser())
-            if len(user_parts[0]) > 0:
-                # Ensure access controls of subdirs created above `$user`
-                # inherit from the parent and share the group.
-                stats = os.stat(os.path.dirname(curr_dir))
-                curr_dir = ''.join([curr_dir, user_parts[0]])
-                mkdirp(curr_dir, group=stats.st_gid, mode=stats.st_mode)
-
-            user_subdirs = ''.join(user_parts[1:])
-            if len(user_subdirs) > 0:
-                # Ensure access controls of subdirs from `$user` on down are
-                # restricted to the user.
-                curr_dir = ''.join([curr_dir, user_subdirs])
-                mkdirp(curr_dir, mode=stat.S_IRWXU)
-
-            assert os.getuid() == os.stat(curr_dir).st_uid
-            break
-
-    if not can_access(path):
-        raise OSError(errno.EACCES,
-                      'Cannot access %s: Permission denied' % curr_dir)
+    err_msg = 'Cannot create stage root {0}: Access to {1} is denied'
+
+    user_uid = os.getuid()
+
+    # Obtain lists of ancestor and descendant paths of the $user node, if any.
+    group_paths, user_node, user_paths = partition_path(path,
+                                                        getpass.getuser())
+
+    for p in group_paths:
+        if not os.path.exists(p):
+            # Ensure access controls of subdirs created above `$user` inherit
+            # from the parent and share the group.
+            par_stat = os.stat(os.path.dirname(p))
+            mkdirp(p, group=par_stat.st_gid, mode=par_stat.st_mode)
+
+            p_stat = os.stat(p)
+            if par_stat.st_gid != p_stat.st_gid:
+                tty.warn("Expected {0} to have group {1}, but it is {2}"
+                         .format(p, par_stat.st_gid, p_stat.st_gid))
+
+            if par_stat.st_mode & p_stat.st_mode != par_stat.st_mode:
+                tty.warn("Expected {0} to support mode {1}, but it is {2}"
+                         .format(p, par_stat.st_mode, p_stat.st_mode))
+
+            if not can_access(p):
+                raise OSError(errno.EACCES, err_msg.format(path, p))
+
+    # 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:
+        # Ensure access controls of subdirs from `$user` on down are
+        # restricted to the user.
+        if not os.path.exists(p):
+            mkdirp(p, mode=stat.S_IRWXU)
+
+            p_stat = os.stat(p)
+            if p_stat.st_mode & stat.S_IRWXU != stat.S_IRWXU:
+                tty.error("Expected {0} to support mode {1}, but it is {2}"
+                          .format(p, stat.S_IRWXU, p_stat.st_mode))
+
+                raise OSError(errno.EACCES, err_msg.format(path, p))
+        else:
+            p_stat = os.stat(p)
+
+        if user_uid != p_stat.st_uid:
+            tty.warn("Expected user {0} to own {1}, but it is owned by {2}"
+                     .format(user_uid, p, p_stat.st_uid))
 
 
 def _first_accessible_path(paths):
@@ -106,7 +128,7 @@ def _resolve_paths(candidates):
         # 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:])
+            path = path.replace("/$user", "", 1)
 
         # Ensure the path is unique per user.
         can_path = sup.canonicalize_path(path)
@@ -250,7 +272,7 @@ def __init__(
         # TODO: temporary stage area in _stage_root.
         self.name = name
         if name is None:
-            self.name = _stage_prefix + next(tempfile._get_candidate_names())
+            self.name = stage_prefix + next(tempfile._get_candidate_names())
         self.mirror_path = mirror_path
 
         # Use the provided path or construct an optionally named stage path.
@@ -680,11 +702,8 @@ def purge():
     """Remove all build directories in the top-level stage path."""
     root = get_stage_root()
     if os.path.isdir(root):
-        # TODO: Figure out a "standard" way to identify the hash length
-        # TODO: Should this support alternate base represenations?
-        dir_expr = re.compile(r'.*-[0-9a-f]{32}$')
         for stage_dir in os.listdir(root):
-            if re.match(dir_expr, stage_dir) or stage_dir == '.lock':
+            if stage_dir.startswith(stage_prefix) or stage_dir == '.lock':
                 stage_path = os.path.join(root, stage_dir)
                 remove_linked_tree(stage_path)
 


Test patch:
diff --git a/lib/spack/spack/test/cmd/env.py b/lib/spack/spack/test/cmd/env.py
--- a/lib/spack/spack/test/cmd/env.py
+++ b/lib/spack/spack/test/cmd/env.py
@@ -17,6 +17,7 @@
 from spack.cmd.env import _env_create
 from spack.spec import Spec
 from spack.main import SpackCommand
+from spack.stage import stage_prefix
 
 from spack.spec_list import SpecListError
 from spack.test.conftest import MockPackage, MockPackageMultiRepo
@@ -576,7 +577,8 @@ def test_stage(mock_stage, mock_fetch, install_mockery):
     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())
+            stage_name = "{0}{1}-{2}-{3}".format(stage_prefix, dep.name,
+                                                 dep.version, dep.dag_hash())
             assert os.path.isdir(os.path.join(root, stage_name))
 
     check_stage('mpileaks')
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
@@ -5,6 +5,7 @@
 
 import collections
 import copy
+import errno
 import inspect
 import itertools
 import os
@@ -41,6 +42,14 @@
 from spack.version import Version
 
 
+@pytest.fixture
+def no_path_access(monkeypatch):
+    def _can_access(path, perms):
+        return False
+
+    monkeypatch.setattr(os, 'access', _can_access)
+
+
 #
 # Disable any activate Spack environment BEFORE all tests
 #
@@ -184,23 +193,29 @@ def working_env():
 
 @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.
+    """
+    Ensure that each (mock_stage) test leaves a clean stage when done.
 
-    This can be disabled for tests that are expected to dirty the stage
-    by adding::
+    Tests that are expected to dirty the stage can disable the check by
+    adding::
 
         @pytest.mark.disable_clean_stage_check
 
-    to tests that need it.
+    and the associated stage files will be removed.
     """
     stage_path = mock_stage
 
     yield
 
     files_in_stage = set()
-    if os.path.exists(stage_path):
+    try:
         stage_files = os.listdir(stage_path)
         files_in_stage = set(stage_files) - ignore_stage_files
+    except OSError as err:
+        if err.errno == errno.ENOENT:
+            pass
+        else:
+            raise
 
     if 'disable_clean_stage_check' in request.keywords:
         # clean up after tests that are expected to be dirty
diff --git a/lib/spack/spack/test/llnl/util/filesystem.py b/lib/spack/spack/test/llnl/util/filesystem.py
--- a/lib/spack/spack/test/llnl/util/filesystem.py
+++ b/lib/spack/spack/test/llnl/util/filesystem.py
@@ -312,6 +312,30 @@ def test_headers_directory_setter():
     assert hl.directories == []
 
 
+@pytest.mark.parametrize('path,entry,expected', [
+    ('/tmp/user/root', None,
+     (['/tmp', '/tmp/user', '/tmp/user/root'], '', [])),
+    ('/tmp/user/root', 'tmp', ([], '/tmp', ['/tmp/user', '/tmp/user/root'])),
+    ('/tmp/user/root', 'user', (['/tmp'], '/tmp/user', ['/tmp/user/root'])),
+    ('/tmp/user/root', 'root', (['/tmp', '/tmp/user'], '/tmp/user/root', [])),
+    ('relative/path', None, (['relative', 'relative/path'], '', [])),
+    ('relative/path', 'relative', ([], 'relative', ['relative/path'])),
+    ('relative/path', 'path', (['relative'], 'relative/path', []))
+])
+def test_partition_path(path, entry, expected):
+    assert fs.partition_path(path, entry) == expected
+
+
+@pytest.mark.parametrize('path,expected', [
+    ('', []),
+    ('/tmp/user/dir', ['/tmp', '/tmp/user', '/tmp/user/dir']),
+    ('./some/sub/dir', ['./some', './some/sub', './some/sub/dir']),
+    ('another/sub/dir', ['another', 'another/sub', 'another/sub/dir'])
+])
+def test_prefixes(path, expected):
+    assert fs.prefixes(path) == expected
+
+
 @pytest.mark.regression('7358')
 @pytest.mark.parametrize('regex,replacement,filename,keyword_args', [
     (r"\<malloc\.h\>", "<stdlib.h>", 'x86_cpuid_info.c', {}),
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
@@ -4,6 +4,7 @@
 # SPDX-License-Identifier: (Apache-2.0 OR MIT)
 
 """Test that the Stage class works correctly."""
+import errno
 import os
 import collections
 import shutil
@@ -13,7 +14,7 @@
 
 import pytest
 
-from llnl.util.filesystem import mkdirp, touch, working_dir
+from llnl.util.filesystem import mkdirp, partition_path, touch, working_dir
 
 import spack.paths
 import spack.stage
@@ -354,23 +355,34 @@ def check_stage_dir_perms(prefix, path):
     # Ensure the path's subdirectories -- to `$user` -- have their parent's
     # perms while those from `$user` on are owned and restricted to the
     # user.
-    status = os.stat(prefix)
-    gid = status.st_gid
-    uid = os.getuid()
+    assert path.startswith(prefix)
+
     user = getpass.getuser()
-    parts = path[len(prefix) + 1:].split(os.path.sep)
-    have_user = False
-    for part in parts:
-        if part == user:
-            have_user = True
-        prefix = os.path.join(prefix, part)
-        prefix_status = os.stat(prefix)
-        if not have_user:
-            assert gid == prefix_status.st_gid
-            assert status.st_mode == prefix_status.st_mode
-        else:
-            assert uid == status.st_uid
-            assert status.st_mode & stat.S_IRWXU == stat.S_IRWXU
+    prefix_status = os.stat(prefix)
+    uid = os.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
 
 
 @pytest.mark.usefixtures('mock_packages')
@@ -643,7 +655,7 @@ def test_source_path_available(self, mock_stage_archive):
 
     def test_first_accessible_path(self, tmpdir):
         """Test _first_accessible_path names."""
-        spack_dir = tmpdir.join('spack-test-fap')
+        spack_dir = tmpdir.join('paths')
         name = str(spack_dir)
         files = [os.path.join(os.path.sep, 'no', 'such', 'path'), name]
 
@@ -671,9 +683,75 @@ def test_first_accessible_path(self, tmpdir):
         # Cleanup
         shutil.rmtree(str(name))
 
+    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
+
+    @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 != os.getuid()
+
     def test_resolve_paths(self):
         """Test _resolve_paths."""
-
         assert spack.stage._resolve_paths([]) == []
 
         # resolved path without user appends user
@@ -686,19 +764,24 @@ def test_resolve_paths(self):
         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(p) for p in 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:
-            can_paths[1] = can_tmpdir
+            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:
-            can_paths[0] = os.path.join(can_paths[0], user)
+            res_paths[0] = os.path.join(res_paths[0], user)
 
-        assert spack.stage._resolve_paths(paths) == can_paths
+        assert spack.stage._resolve_paths(paths) == res_paths
 
     def test_get_stage_root_bad_path(self, clear_stage_root):
         """Ensure an invalid stage path root raises a StageError."""
@@ -711,9 +794,9 @@ def test_get_stage_root_bad_path(self, clear_stage_root):
         assert spack.stage._stage_root is None
 
     @pytest.mark.parametrize(
-        'path,purged', [('stage-1234567890abcdef1234567890abcdef', True),
-                        ('stage-abcdef12345678900987654321fedcba', True),
-                        ('stage-a1b2c3', False)])
+        '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')
@@ -737,7 +820,6 @@ def test_stage_purge(self, tmpdir, clear_stage_root, path, purged):
 
     def test_get_stage_root_in_spack(self, clear_stage_root):
         """Ensure an instance path is an accessible build stage path."""
-
         base = canonicalize_path(os.path.join('$spack', '.spack-test-stage'))
         mkdirp(base)
         test_path = tempfile.mkdtemp(dir=base)