• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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