|
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 |
|
6 import atexit |
|
7 import collections |
|
8 import contextlib |
|
9 import os |
|
10 import random |
|
11 import shutil |
|
12 import sys |
|
13 import tempfile |
|
14 import textwrap |
|
15 |
|
16 from coverage.backunittest import TestCase |
|
17 from coverage.backward import StringIO, to_bytes |
|
18 |
|
19 |
|
20 class 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 |
|
47 def 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 |
|
63 def 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 |
|
72 def 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 |
|
96 class 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 |
|
116 class 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 |
|
124 class 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 |
|
155 class 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 |
|
189 class 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. |
|
337 atexit.register(TempDirMixin.report_on_class_behavior) |