DebugClients/Python/coverage/test_helpers.py

changeset 4489
d0d6e4ad31bd
child 5051
3586ebd9fac8
equal deleted inserted replaced
4481:456c58fc64b0 4489:d0d6e4ad31bd
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)

eric ide

mercurial