OLD | NEW |
(Empty) | |
| 1 # Copyright (c) 2011 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. |
| 4 |
| 5 |
| 6 import atexit |
| 7 import logging |
| 8 import os |
| 9 import stat |
| 10 import subprocess |
| 11 import sys |
| 12 import tempfile |
| 13 import time |
| 14 |
| 15 from testing_support import auto_stub |
| 16 |
| 17 |
| 18 def rmtree(path): |
| 19 """shutil.rmtree() on steroids. |
| 20 |
| 21 Recursively removes a directory, even if it's marked read-only. |
| 22 |
| 23 shutil.rmtree() doesn't work on Windows if any of the files or directories |
| 24 are read-only, which svn repositories and some .svn files are. We need to |
| 25 be able to force the files to be writable (i.e., deletable) as we traverse |
| 26 the tree. |
| 27 |
| 28 Even with all this, Windows still sometimes fails to delete a file, citing |
| 29 a permission error (maybe something to do with antivirus scans or disk |
| 30 indexing). The best suggestion any of the user forums had was to wait a |
| 31 bit and try again, so we do that too. It's hand-waving, but sometimes it |
| 32 works. :/ |
| 33 |
| 34 On POSIX systems, things are a little bit simpler. The modes of the files |
| 35 to be deleted doesn't matter, only the modes of the directories containing |
| 36 them are significant. As the directory tree is traversed, each directory |
| 37 has its mode set appropriately before descending into it. This should |
| 38 result in the entire tree being removed, with the possible exception of |
| 39 *path itself, because nothing attempts to change the mode of its parent. |
| 40 Doing so would be hazardous, as it's not a directory slated for removal. |
| 41 In the ordinary case, this is not a problem: for our purposes, the user |
| 42 will never lack write permission on *path's parent. |
| 43 """ |
| 44 if not os.path.exists(path): |
| 45 return |
| 46 |
| 47 if os.path.islink(path) or not os.path.isdir(path): |
| 48 raise ValueError('Called rmtree(%s) in non-directory' % path) |
| 49 |
| 50 if sys.platform == 'win32': |
| 51 # Give up and use cmd.exe's rd command. |
| 52 path = os.path.normcase(path) |
| 53 for _ in xrange(3): |
| 54 exitcode = subprocess.call(['cmd.exe', '/c', 'rd', '/q', '/s', path]) |
| 55 if exitcode == 0: |
| 56 return |
| 57 else: |
| 58 print >> sys.stderr, 'rd exited with code %d' % exitcode |
| 59 time.sleep(3) |
| 60 raise Exception('Failed to remove path %s' % path) |
| 61 |
| 62 # On POSIX systems, we need the x-bit set on the directory to access it, |
| 63 # the r-bit to see its contents, and the w-bit to remove files from it. |
| 64 # The actual modes of the files within the directory is irrelevant. |
| 65 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) |
| 66 |
| 67 def remove(func, subpath): |
| 68 func(subpath) |
| 69 |
| 70 for fn in os.listdir(path): |
| 71 # If fullpath is a symbolic link that points to a directory, isdir will |
| 72 # be True, but we don't want to descend into that as a directory, we just |
| 73 # want to remove the link. Check islink and treat links as ordinary files |
| 74 # would be treated regardless of what they reference. |
| 75 fullpath = os.path.join(path, fn) |
| 76 if os.path.islink(fullpath) or not os.path.isdir(fullpath): |
| 77 remove(os.remove, fullpath) |
| 78 else: |
| 79 # Recurse. |
| 80 rmtree(fullpath) |
| 81 |
| 82 remove(os.rmdir, path) |
| 83 |
| 84 |
| 85 class TrialDir(object): |
| 86 """Manages a temporary directory. |
| 87 |
| 88 On first object creation, TrialDir.TRIAL_ROOT will be set to a new temporary |
| 89 directory created in /tmp or the equivalent. It will be deleted on process |
| 90 exit unless TrialDir.SHOULD_LEAK is set to True. |
| 91 """ |
| 92 # When SHOULD_LEAK is set to True, temporary directories created while the |
| 93 # tests are running aren't deleted at the end of the tests. Expect failures |
| 94 # when running more than one test due to inter-test side-effects. Helps with |
| 95 # debugging. |
| 96 SHOULD_LEAK = False |
| 97 |
| 98 # Main root directory. |
| 99 TRIAL_ROOT = None |
| 100 |
| 101 def __init__(self, subdir, leak=False): |
| 102 self.leak = self.SHOULD_LEAK or leak |
| 103 self.subdir = subdir |
| 104 self.root_dir = None |
| 105 |
| 106 def set_up(self): |
| 107 """All late initialization comes here.""" |
| 108 # You can override self.TRIAL_ROOT. |
| 109 if not self.TRIAL_ROOT: |
| 110 # Was not yet initialized. |
| 111 TrialDir.TRIAL_ROOT = os.path.realpath(tempfile.mkdtemp(prefix='trial')) |
| 112 atexit.register(self._clean) |
| 113 self.root_dir = os.path.join(TrialDir.TRIAL_ROOT, self.subdir) |
| 114 rmtree(self.root_dir) |
| 115 os.makedirs(self.root_dir) |
| 116 |
| 117 def tear_down(self): |
| 118 """Cleans the trial subdirectory for this instance.""" |
| 119 if not self.leak: |
| 120 logging.debug('Removing %s' % self.root_dir) |
| 121 rmtree(self.root_dir) |
| 122 else: |
| 123 logging.error('Leaking %s' % self.root_dir) |
| 124 self.root_dir = None |
| 125 |
| 126 @staticmethod |
| 127 def _clean(): |
| 128 """Cleans the root trial directory.""" |
| 129 if not TrialDir.SHOULD_LEAK: |
| 130 logging.debug('Removing %s' % TrialDir.TRIAL_ROOT) |
| 131 rmtree(TrialDir.TRIAL_ROOT) |
| 132 else: |
| 133 logging.error('Leaking %s' % TrialDir.TRIAL_ROOT) |
| 134 |
| 135 |
| 136 class TrialDirMixIn(object): |
| 137 def setUp(self): |
| 138 # Create a specific directory just for the test. |
| 139 self.trial = TrialDir(self.id()) |
| 140 self.trial.set_up() |
| 141 |
| 142 def tearDown(self): |
| 143 self.trial.tear_down() |
| 144 |
| 145 @property |
| 146 def root_dir(self): |
| 147 return self.trial.root_dir |
| 148 |
| 149 |
| 150 class TestCase(auto_stub.TestCase, TrialDirMixIn): |
| 151 """Base unittest class that cleans off a trial directory in tearDown().""" |
| 152 def setUp(self): |
| 153 auto_stub.TestCase.setUp(self) |
| 154 TrialDirMixIn.setUp(self) |
| 155 |
| 156 def tearDown(self): |
| 157 TrialDirMixIn.tearDown(self) |
| 158 auto_stub.TestCase.tearDown(self) |
OLD | NEW |