1# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 2# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt 3 4"""Mixin classes to help make good tests.""" 5 6import atexit 7import collections 8import contextlib 9import os 10import random 11import shutil 12import sys 13import tempfile 14import textwrap 15 16from coverage.backunittest import TestCase 17from coverage.backward import StringIO, to_bytes 18 19 20class Tee(object): 21 """A file-like that writes to all the file-likes it has.""" 22 23 def __init__(self, *files): 24 """Make a Tee that writes to all the files in `files.`""" 25 self._files = files 26 if hasattr(files[0], "encoding"): 27 self.encoding = files[0].encoding 28 29 def write(self, data): 30 """Write `data` to all the files.""" 31 for f in self._files: 32 f.write(data) 33 34 def flush(self): 35 """Flush the data on all the files.""" 36 for f in self._files: 37 f.flush() 38 39 if 0: 40 # Use this if you need to use a debugger, though it makes some tests 41 # fail, I'm not sure why... 42 def __getattr__(self, name): 43 return getattr(self._files[0], name) 44 45 46@contextlib.contextmanager 47def change_dir(new_dir): 48 """Change directory, and then change back. 49 50 Use as a context manager, it will give you the new directory, and later 51 restore the old one. 52 53 """ 54 old_dir = os.getcwd() 55 os.chdir(new_dir) 56 try: 57 yield os.getcwd() 58 finally: 59 os.chdir(old_dir) 60 61 62@contextlib.contextmanager 63def saved_sys_path(): 64 """Save sys.path, and restore it later.""" 65 old_syspath = sys.path[:] 66 try: 67 yield 68 finally: 69 sys.path = old_syspath 70 71 72def setup_with_context_manager(testcase, cm): 73 """Use a contextmanager to setUp a test case. 74 75 If you have a context manager you like:: 76 77 with ctxmgr(a, b, c) as v: 78 # do something with v 79 80 and you want to have that effect for a test case, call this function from 81 your setUp, and it will start the context manager for your test, and end it 82 when the test is done:: 83 84 def setUp(self): 85 self.v = setup_with_context_manager(self, ctxmgr(a, b, c)) 86 87 def test_foo(self): 88 # do something with self.v 89 90 """ 91 val = cm.__enter__() 92 testcase.addCleanup(cm.__exit__, None, None, None) 93 return val 94 95 96class ModuleAwareMixin(TestCase): 97 """A test case mixin that isolates changes to sys.modules.""" 98 99 def setUp(self): 100 super(ModuleAwareMixin, self).setUp() 101 102 # Record sys.modules here so we can restore it in cleanup_modules. 103 self.old_modules = list(sys.modules) 104 self.addCleanup(self.cleanup_modules) 105 106 def cleanup_modules(self): 107 """Remove any new modules imported during the test run. 108 109 This lets us import the same source files for more than one test. 110 111 """ 112 for m in [m for m in sys.modules if m not in self.old_modules]: 113 del sys.modules[m] 114 115 116class SysPathAwareMixin(TestCase): 117 """A test case mixin that isolates changes to sys.path.""" 118 119 def setUp(self): 120 super(SysPathAwareMixin, self).setUp() 121 setup_with_context_manager(self, saved_sys_path()) 122 123 124class EnvironmentAwareMixin(TestCase): 125 """A test case mixin that isolates changes to the environment.""" 126 127 def setUp(self): 128 super(EnvironmentAwareMixin, self).setUp() 129 130 # Record environment variables that we changed with set_environ. 131 self.environ_undos = {} 132 133 self.addCleanup(self.cleanup_environ) 134 135 def set_environ(self, name, value): 136 """Set an environment variable `name` to be `value`. 137 138 The environment variable is set, and record is kept that it was set, 139 so that `cleanup_environ` can restore its original value. 140 141 """ 142 if name not in self.environ_undos: 143 self.environ_undos[name] = os.environ.get(name) 144 os.environ[name] = value 145 146 def cleanup_environ(self): 147 """Undo all the changes made by `set_environ`.""" 148 for name, value in self.environ_undos.items(): 149 if value is None: 150 del os.environ[name] 151 else: 152 os.environ[name] = value 153 154 155class StdStreamCapturingMixin(TestCase): 156 """A test case mixin that captures stdout and stderr.""" 157 158 def setUp(self): 159 super(StdStreamCapturingMixin, self).setUp() 160 161 # Capture stdout and stderr so we can examine them in tests. 162 # nose keeps stdout from littering the screen, so we can safely Tee it, 163 # but it doesn't capture stderr, so we don't want to Tee stderr to the 164 # real stderr, since it will interfere with our nice field of dots. 165 self.old_stdout = sys.stdout 166 self.captured_stdout = StringIO() 167 sys.stdout = Tee(sys.stdout, self.captured_stdout) 168 169 self.old_stderr = sys.stderr 170 self.captured_stderr = StringIO() 171 sys.stderr = self.captured_stderr 172 173 self.addCleanup(self.cleanup_std_streams) 174 175 def cleanup_std_streams(self): 176 """Restore stdout and stderr.""" 177 sys.stdout = self.old_stdout 178 sys.stderr = self.old_stderr 179 180 def stdout(self): 181 """Return the data written to stdout during the test.""" 182 return self.captured_stdout.getvalue() 183 184 def stderr(self): 185 """Return the data written to stderr during the test.""" 186 return self.captured_stderr.getvalue() 187 188 189class TempDirMixin(SysPathAwareMixin, ModuleAwareMixin, TestCase): 190 """A test case mixin that creates a temp directory and files in it. 191 192 Includes SysPathAwareMixin and ModuleAwareMixin, because making and using 193 temp directories like this will also need that kind of isolation. 194 195 """ 196 197 # Our own setting: most of these tests run in their own temp directory. 198 # Set this to False in your subclass if you don't want a temp directory 199 # created. 200 run_in_temp_dir = True 201 202 # Set this if you aren't creating any files with make_file, but still want 203 # the temp directory. This will stop the test behavior checker from 204 # complaining. 205 no_files_in_temp_dir = False 206 207 def setUp(self): 208 super(TempDirMixin, self).setUp() 209 210 if self.run_in_temp_dir: 211 # Create a temporary directory. 212 self.temp_dir = self.make_temp_dir("test_cover") 213 self.chdir(self.temp_dir) 214 215 # Modules should be importable from this temp directory. We don't 216 # use '' because we make lots of different temp directories and 217 # nose's caching importer can get confused. The full path prevents 218 # problems. 219 sys.path.insert(0, os.getcwd()) 220 221 class_behavior = self.class_behavior() 222 class_behavior.tests += 1 223 class_behavior.temp_dir = self.run_in_temp_dir 224 class_behavior.no_files_ok = self.no_files_in_temp_dir 225 226 self.addCleanup(self.check_behavior) 227 228 def make_temp_dir(self, slug="test_cover"): 229 """Make a temp directory that is cleaned up when the test is done.""" 230 name = "%s_%08d" % (slug, random.randint(0, 99999999)) 231 temp_dir = os.path.join(tempfile.gettempdir(), name) 232 os.makedirs(temp_dir) 233 self.addCleanup(shutil.rmtree, temp_dir) 234 return temp_dir 235 236 def chdir(self, new_dir): 237 """Change directory, and change back when the test is done.""" 238 old_dir = os.getcwd() 239 os.chdir(new_dir) 240 self.addCleanup(os.chdir, old_dir) 241 242 def check_behavior(self): 243 """Check that we did the right things.""" 244 245 class_behavior = self.class_behavior() 246 if class_behavior.test_method_made_any_files: 247 class_behavior.tests_making_files += 1 248 249 def make_file(self, filename, text="", newline=None): 250 """Create a file for testing. 251 252 `filename` is the relative path to the file, including directories if 253 desired, which will be created if need be. 254 255 `text` is the content to create in the file, a native string (bytes in 256 Python 2, unicode in Python 3). 257 258 If `newline` is provided, it is a string that will be used as the line 259 endings in the created file, otherwise the line endings are as provided 260 in `text`. 261 262 Returns `filename`. 263 264 """ 265 # Tests that call `make_file` should be run in a temp environment. 266 assert self.run_in_temp_dir 267 self.class_behavior().test_method_made_any_files = True 268 269 text = textwrap.dedent(text) 270 if newline: 271 text = text.replace("\n", newline) 272 273 # Make sure the directories are available. 274 dirs, _ = os.path.split(filename) 275 if dirs and not os.path.exists(dirs): 276 os.makedirs(dirs) 277 278 # Create the file. 279 with open(filename, 'wb') as f: 280 f.write(to_bytes(text)) 281 282 return filename 283 284 # We run some tests in temporary directories, because they may need to make 285 # files for the tests. But this is expensive, so we can change per-class 286 # whether a temp directory is used or not. It's easy to forget to set that 287 # option properly, so we track information about what the tests did, and 288 # then report at the end of the process on test classes that were set 289 # wrong. 290 291 class ClassBehavior(object): 292 """A value object to store per-class.""" 293 def __init__(self): 294 self.tests = 0 295 self.skipped = 0 296 self.temp_dir = True 297 self.no_files_ok = False 298 self.tests_making_files = 0 299 self.test_method_made_any_files = False 300 301 # Map from class to info about how it ran. 302 class_behaviors = collections.defaultdict(ClassBehavior) 303 304 @classmethod 305 def report_on_class_behavior(cls): 306 """Called at process exit to report on class behavior.""" 307 for test_class, behavior in cls.class_behaviors.items(): 308 bad = "" 309 if behavior.tests <= behavior.skipped: 310 bad = "" 311 elif behavior.temp_dir and behavior.tests_making_files == 0: 312 if not behavior.no_files_ok: 313 bad = "Inefficient" 314 elif not behavior.temp_dir and behavior.tests_making_files > 0: 315 bad = "Unsafe" 316 317 if bad: 318 if behavior.temp_dir: 319 where = "in a temp directory" 320 else: 321 where = "without a temp directory" 322 print( 323 "%s: %s ran %d tests, %d made files %s" % ( 324 bad, 325 test_class.__name__, 326 behavior.tests, 327 behavior.tests_making_files, 328 where, 329 ) 330 ) 331 332 def class_behavior(self): 333 """Get the ClassBehavior instance for this test.""" 334 return self.class_behaviors[self.__class__] 335 336# When the process ends, find out about bad classes. 337atexit.register(TempDirMixin.report_on_class_behavior) 338