| Index: testing_support/trial_dir.py
|
| diff --git a/testing_support/trial_dir.py b/testing_support/trial_dir.py
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..ff58b215d608cfa27786bbe84f66a0b6144bb11f
|
| --- /dev/null
|
| +++ b/testing_support/trial_dir.py
|
| @@ -0,0 +1,158 @@
|
| +# Copyright (c) 2011 The Chromium Authors. All rights reserved.
|
| +# Use of this source code is governed by a BSD-style license that can be
|
| +# found in the LICENSE file.
|
| +
|
| +
|
| +import atexit
|
| +import logging
|
| +import os
|
| +import stat
|
| +import subprocess
|
| +import sys
|
| +import tempfile
|
| +import time
|
| +
|
| +from testing_support import auto_stub
|
| +
|
| +
|
| +def rmtree(path):
|
| + """shutil.rmtree() on steroids.
|
| +
|
| + Recursively removes a directory, even if it's marked read-only.
|
| +
|
| + shutil.rmtree() doesn't work on Windows if any of the files or directories
|
| + are read-only, which svn repositories and some .svn files are. We need to
|
| + be able to force the files to be writable (i.e., deletable) as we traverse
|
| + the tree.
|
| +
|
| + Even with all this, Windows still sometimes fails to delete a file, citing
|
| + a permission error (maybe something to do with antivirus scans or disk
|
| + indexing). The best suggestion any of the user forums had was to wait a
|
| + bit and try again, so we do that too. It's hand-waving, but sometimes it
|
| + works. :/
|
| +
|
| + On POSIX systems, things are a little bit simpler. The modes of the files
|
| + to be deleted doesn't matter, only the modes of the directories containing
|
| + them are significant. As the directory tree is traversed, each directory
|
| + has its mode set appropriately before descending into it. This should
|
| + result in the entire tree being removed, with the possible exception of
|
| + *path itself, because nothing attempts to change the mode of its parent.
|
| + Doing so would be hazardous, as it's not a directory slated for removal.
|
| + In the ordinary case, this is not a problem: for our purposes, the user
|
| + will never lack write permission on *path's parent.
|
| + """
|
| + if not os.path.exists(path):
|
| + return
|
| +
|
| + if os.path.islink(path) or not os.path.isdir(path):
|
| + raise ValueError('Called rmtree(%s) in non-directory' % path)
|
| +
|
| + if sys.platform == 'win32':
|
| + # Give up and use cmd.exe's rd command.
|
| + path = os.path.normcase(path)
|
| + for _ in xrange(3):
|
| + exitcode = subprocess.call(['cmd.exe', '/c', 'rd', '/q', '/s', path])
|
| + if exitcode == 0:
|
| + return
|
| + else:
|
| + print >> sys.stderr, 'rd exited with code %d' % exitcode
|
| + time.sleep(3)
|
| + raise Exception('Failed to remove path %s' % path)
|
| +
|
| + # On POSIX systems, we need the x-bit set on the directory to access it,
|
| + # the r-bit to see its contents, and the w-bit to remove files from it.
|
| + # The actual modes of the files within the directory is irrelevant.
|
| + os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
|
| +
|
| + def remove(func, subpath):
|
| + func(subpath)
|
| +
|
| + for fn in os.listdir(path):
|
| + # If fullpath is a symbolic link that points to a directory, isdir will
|
| + # be True, but we don't want to descend into that as a directory, we just
|
| + # want to remove the link. Check islink and treat links as ordinary files
|
| + # would be treated regardless of what they reference.
|
| + fullpath = os.path.join(path, fn)
|
| + if os.path.islink(fullpath) or not os.path.isdir(fullpath):
|
| + remove(os.remove, fullpath)
|
| + else:
|
| + # Recurse.
|
| + rmtree(fullpath)
|
| +
|
| + remove(os.rmdir, path)
|
| +
|
| +
|
| +class TrialDir(object):
|
| + """Manages a temporary directory.
|
| +
|
| + On first object creation, TrialDir.TRIAL_ROOT will be set to a new temporary
|
| + directory created in /tmp or the equivalent. It will be deleted on process
|
| + exit unless TrialDir.SHOULD_LEAK is set to True.
|
| + """
|
| + # When SHOULD_LEAK is set to True, temporary directories created while the
|
| + # tests are running aren't deleted at the end of the tests. Expect failures
|
| + # when running more than one test due to inter-test side-effects. Helps with
|
| + # debugging.
|
| + SHOULD_LEAK = False
|
| +
|
| + # Main root directory.
|
| + TRIAL_ROOT = None
|
| +
|
| + def __init__(self, subdir, leak=False):
|
| + self.leak = self.SHOULD_LEAK or leak
|
| + self.subdir = subdir
|
| + self.root_dir = None
|
| +
|
| + def set_up(self):
|
| + """All late initialization comes here."""
|
| + # You can override self.TRIAL_ROOT.
|
| + if not self.TRIAL_ROOT:
|
| + # Was not yet initialized.
|
| + TrialDir.TRIAL_ROOT = os.path.realpath(tempfile.mkdtemp(prefix='trial'))
|
| + atexit.register(self._clean)
|
| + self.root_dir = os.path.join(TrialDir.TRIAL_ROOT, self.subdir)
|
| + rmtree(self.root_dir)
|
| + os.makedirs(self.root_dir)
|
| +
|
| + def tear_down(self):
|
| + """Cleans the trial subdirectory for this instance."""
|
| + if not self.leak:
|
| + logging.debug('Removing %s' % self.root_dir)
|
| + rmtree(self.root_dir)
|
| + else:
|
| + logging.error('Leaking %s' % self.root_dir)
|
| + self.root_dir = None
|
| +
|
| + @staticmethod
|
| + def _clean():
|
| + """Cleans the root trial directory."""
|
| + if not TrialDir.SHOULD_LEAK:
|
| + logging.debug('Removing %s' % TrialDir.TRIAL_ROOT)
|
| + rmtree(TrialDir.TRIAL_ROOT)
|
| + else:
|
| + logging.error('Leaking %s' % TrialDir.TRIAL_ROOT)
|
| +
|
| +
|
| +class TrialDirMixIn(object):
|
| + def setUp(self):
|
| + # Create a specific directory just for the test.
|
| + self.trial = TrialDir(self.id())
|
| + self.trial.set_up()
|
| +
|
| + def tearDown(self):
|
| + self.trial.tear_down()
|
| +
|
| + @property
|
| + def root_dir(self):
|
| + return self.trial.root_dir
|
| +
|
| +
|
| +class TestCase(auto_stub.TestCase, TrialDirMixIn):
|
| + """Base unittest class that cleans off a trial directory in tearDown()."""
|
| + def setUp(self):
|
| + auto_stub.TestCase.setUp(self)
|
| + TrialDirMixIn.setUp(self)
|
| +
|
| + def tearDown(self):
|
| + TrialDirMixIn.tearDown(self)
|
| + auto_stub.TestCase.tearDown(self)
|
|
|