blob: 85342c3bc271f986e642cc522565e6378e699c58 [file] [log] [blame]
# 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):
TrialDirMixIn.setUp(self)
auto_stub.TestCase.setUp(self)
def tearDown(self):
auto_stub.TestCase.tearDown(self)
TrialDirMixIn.tearDown(self)