DebugClients/Python/coverage/test_helpers.py

changeset 5141
bc64243b7672
parent 5126
d28b92dabc2b
parent 5140
01484c0afbc6
child 5144
1ab536d25072
equal deleted inserted replaced
5126:d28b92dabc2b 5141:bc64243b7672
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 old_stdout = sys.stdout
166 self.captured_stdout = StringIO()
167 sys.stdout = Tee(sys.stdout, self.captured_stdout)
168
169 old_stderr = sys.stderr
170 self.captured_stderr = StringIO()
171 sys.stderr = self.captured_stderr
172
173 self.addCleanup(self.cleanup_std_streams, old_stdout, old_stderr)
174
175 def cleanup_std_streams(self, old_stdout, old_stderr):
176 """Restore stdout and stderr."""
177 sys.stdout = old_stdout
178 sys.stderr = 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 DelayedAssertionMixin(TestCase):
190 """A test case mixin that provides a `delayed_assertions` context manager.
191
192 Use it like this::
193
194 with self.delayed_assertions():
195 self.assertEqual(x, y)
196 self.assertEqual(z, w)
197
198 All of the assertions will run. The failures will be displayed at the end
199 of the with-statement.
200
201 NOTE: this only works with some assertions. These are known to work:
202
203 - `assertEqual(str, str)`
204
205 - `assertMultilineEqual(str, str)`
206
207 """
208 def __init__(self, *args, **kwargs):
209 super(DelayedAssertionMixin, self).__init__(*args, **kwargs)
210 # This mixin only works with assert methods that call `self.fail`. In
211 # Python 2.7, `assertEqual` didn't, but we can do what Python 3 does,
212 # and use `assertMultiLineEqual` for comparing strings.
213 self.addTypeEqualityFunc(str, 'assertMultiLineEqual')
214 self._delayed_assertions = None
215
216 @contextlib.contextmanager
217 def delayed_assertions(self):
218 """The context manager: assert that we didn't collect any assertions."""
219 self._delayed_assertions = []
220 old_fail = self.fail
221 self.fail = self._delayed_fail
222 try:
223 yield
224 finally:
225 self.fail = old_fail
226 if self._delayed_assertions:
227 if len(self._delayed_assertions) == 1:
228 self.fail(self._delayed_assertions[0])
229 else:
230 self.fail(
231 "{0} failed assertions:\n{1}".format(
232 len(self._delayed_assertions),
233 "\n".join(self._delayed_assertions),
234 )
235 )
236
237 def _delayed_fail(self, msg=None):
238 """The stand-in for TestCase.fail during delayed_assertions."""
239 self._delayed_assertions.append(msg)
240
241
242 class TempDirMixin(SysPathAwareMixin, ModuleAwareMixin, TestCase):
243 """A test case mixin that creates a temp directory and files in it.
244
245 Includes SysPathAwareMixin and ModuleAwareMixin, because making and using
246 temp directories like this will also need that kind of isolation.
247
248 """
249
250 # Our own setting: most of these tests run in their own temp directory.
251 # Set this to False in your subclass if you don't want a temp directory
252 # created.
253 run_in_temp_dir = True
254
255 # Set this if you aren't creating any files with make_file, but still want
256 # the temp directory. This will stop the test behavior checker from
257 # complaining.
258 no_files_in_temp_dir = False
259
260 def setUp(self):
261 super(TempDirMixin, self).setUp()
262
263 if self.run_in_temp_dir:
264 # Create a temporary directory.
265 self.temp_dir = self.make_temp_dir("test_cover")
266 self.chdir(self.temp_dir)
267
268 # Modules should be importable from this temp directory. We don't
269 # use '' because we make lots of different temp directories and
270 # nose's caching importer can get confused. The full path prevents
271 # problems.
272 sys.path.insert(0, os.getcwd())
273
274 class_behavior = self.class_behavior()
275 class_behavior.tests += 1
276 class_behavior.temp_dir = self.run_in_temp_dir
277 class_behavior.no_files_ok = self.no_files_in_temp_dir
278
279 self.addCleanup(self.check_behavior)
280
281 def make_temp_dir(self, slug="test_cover"):
282 """Make a temp directory that is cleaned up when the test is done."""
283 name = "%s_%08d" % (slug, random.randint(0, 99999999))
284 temp_dir = os.path.join(tempfile.gettempdir(), name)
285 os.makedirs(temp_dir)
286 self.addCleanup(shutil.rmtree, temp_dir)
287 return temp_dir
288
289 def chdir(self, new_dir):
290 """Change directory, and change back when the test is done."""
291 old_dir = os.getcwd()
292 os.chdir(new_dir)
293 self.addCleanup(os.chdir, old_dir)
294
295 def check_behavior(self):
296 """Check that we did the right things."""
297
298 class_behavior = self.class_behavior()
299 if class_behavior.test_method_made_any_files:
300 class_behavior.tests_making_files += 1
301
302 def make_file(self, filename, text="", newline=None):
303 """Create a file for testing.
304
305 `filename` is the relative path to the file, including directories if
306 desired, which will be created if need be.
307
308 `text` is the content to create in the file, a native string (bytes in
309 Python 2, unicode in Python 3).
310
311 If `newline` is provided, it is a string that will be used as the line
312 endings in the created file, otherwise the line endings are as provided
313 in `text`.
314
315 Returns `filename`.
316
317 """
318 # Tests that call `make_file` should be run in a temp environment.
319 assert self.run_in_temp_dir
320 self.class_behavior().test_method_made_any_files = True
321
322 text = textwrap.dedent(text)
323 if newline:
324 text = text.replace("\n", newline)
325
326 # Make sure the directories are available.
327 dirs, _ = os.path.split(filename)
328 if dirs and not os.path.exists(dirs):
329 os.makedirs(dirs)
330
331 # Create the file.
332 with open(filename, 'wb') as f:
333 f.write(to_bytes(text))
334
335 return filename
336
337 # We run some tests in temporary directories, because they may need to make
338 # files for the tests. But this is expensive, so we can change per-class
339 # whether a temp directory is used or not. It's easy to forget to set that
340 # option properly, so we track information about what the tests did, and
341 # then report at the end of the process on test classes that were set
342 # wrong.
343
344 class ClassBehavior(object):
345 """A value object to store per-class."""
346 def __init__(self):
347 self.tests = 0
348 self.skipped = 0
349 self.temp_dir = True
350 self.no_files_ok = False
351 self.tests_making_files = 0
352 self.test_method_made_any_files = False
353
354 # Map from class to info about how it ran.
355 class_behaviors = collections.defaultdict(ClassBehavior)
356
357 @classmethod
358 def report_on_class_behavior(cls):
359 """Called at process exit to report on class behavior."""
360 for test_class, behavior in cls.class_behaviors.items():
361 bad = ""
362 if behavior.tests <= behavior.skipped:
363 bad = ""
364 elif behavior.temp_dir and behavior.tests_making_files == 0:
365 if not behavior.no_files_ok:
366 bad = "Inefficient"
367 elif not behavior.temp_dir and behavior.tests_making_files > 0:
368 bad = "Unsafe"
369
370 if bad:
371 if behavior.temp_dir:
372 where = "in a temp directory"
373 else:
374 where = "without a temp directory"
375 print(
376 "%s: %s ran %d tests, %d made files %s" % (
377 bad,
378 test_class.__name__,
379 behavior.tests,
380 behavior.tests_making_files,
381 where,
382 )
383 )
384
385 def class_behavior(self):
386 """Get the ClassBehavior instance for this test."""
387 return self.class_behaviors[self.__class__]
388
389 # When the process ends, find out about bad classes.
390 atexit.register(TempDirMixin.report_on_class_behavior)
391
392 #
393 # eflag: FileType = Python2

eric ide

mercurial