summaryrefslogtreecommitdiff
path: root/tests/mixins.py
blob: ff47a4da03a329c0a11fe030ffaab62f1588bd3d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt

"""
Test class mixins

Some of these are transitional while working toward pure-pytest style.
"""

import os
import os.path
import shutil
import sys

import pytest

from coverage.backward import importlib

from tests.helpers import change_dir, make_file, remove_files


class PytestBase(object):
    """A base class to connect to pytest in a test class hierarchy."""

    @pytest.fixture(autouse=True)
    def connect_to_pytest(self, request, monkeypatch):
        """Captures pytest facilities for use by other test helpers."""
        # pylint: disable=attribute-defined-outside-init
        self._pytest_request = request
        self._monkeypatch = monkeypatch
        self.setup_test()

    # Can't call this setUp or setup because pytest sniffs out unittest and
    # nosetest special names, and does things with them.
    # https://github.com/pytest-dev/pytest/issues/8424
    def setup_test(self):
        """Per-test initialization. Override this as you wish."""
        pass

    def addCleanup(self, fn, *args):
        """Like unittest's addCleanup: code to call when the test is done."""
        self._pytest_request.addfinalizer(lambda: fn(*args))

    def set_environ(self, name, value):
        """Set an environment variable `name` to be `value`."""
        self._monkeypatch.setenv(name, value)

    def del_environ(self, name):
        """Delete an environment variable, unless we set it."""
        self._monkeypatch.delenv(name, raising=False)


class TempDirMixin(object):
    """Provides temp dir and data file helpers for tests."""

    # Our own setting: most of these tests run in their own temp directory.
    # Set this to False in your subclass if you don't want a temp directory
    # created.
    run_in_temp_dir = True

    @pytest.fixture(autouse=True)
    def _temp_dir(self, tmpdir_factory):
        """Create a temp dir for the tests, if they want it."""
        if self.run_in_temp_dir:
            tmpdir = tmpdir_factory.mktemp("")
            self.temp_dir = str(tmpdir)
            with change_dir(self.temp_dir):
                # Modules should be importable from this temp directory.  We don't
                # use '' because we make lots of different temp directories and
                # nose's caching importer can get confused.  The full path prevents
                # problems.
                sys.path.insert(0, os.getcwd())

                yield None
        else:
            yield None

    def make_file(self, filename, text="", bytes=b"", newline=None):
        """Make a file. See `tests.helpers.make_file`"""
        # pylint: disable=redefined-builtin     # bytes
        assert self.run_in_temp_dir, "Only use make_file when running in a temp dir"
        return make_file(filename, text, bytes, newline)


class SysPathModulesMixin:
    """Auto-restore sys.path and the imported modules at the end of each test."""

    @pytest.fixture(autouse=True)
    def _save_sys_path(self):
        """Restore sys.path at the end of each test."""
        old_syspath = sys.path[:]
        try:
            yield
        finally:
            sys.path = old_syspath

    @pytest.fixture(autouse=True)
    def _module_saving(self):
        """Remove modules we imported during the test."""
        self._old_modules = list(sys.modules)
        try:
            yield
        finally:
            self._cleanup_modules()

    def _cleanup_modules(self):
        """Remove any new modules imported since our construction.

        This lets us import the same source files for more than one test, or
        if called explicitly, within one test.

        """
        for m in [m for m in sys.modules if m not in self._old_modules]:
            del sys.modules[m]

    def clean_local_file_imports(self):
        """Clean up the results of calls to `import_local_file`.

        Use this if you need to `import_local_file` the same file twice in
        one test.

        """
        # So that we can re-import files, clean them out first.
        self._cleanup_modules()

        # Also have to clean out the .pyc file, since the timestamp
        # resolution is only one second, a changed file might not be
        # picked up.
        remove_files("*.pyc", "*$py.class")
        if os.path.exists("__pycache__"):
            shutil.rmtree("__pycache__")

        if importlib and hasattr(importlib, "invalidate_caches"):
            importlib.invalidate_caches()


class StdStreamCapturingMixin:
    """
    Adapter from the pytest capsys fixture to more convenient methods.

    This doesn't also output to the real stdout, so we probably want to move
    to "real" capsys when we can use fixtures in test methods.

    Once you've used one of these methods, the capturing is reset, so another
    invocation will only return the delta.

    """
    @pytest.fixture(autouse=True)
    def _capcapsys(self, capsys):
        """Grab the fixture so our methods can use it."""
        self.capsys = capsys

    def stdouterr(self):
        """Returns (out, err), two strings for stdout and stderr."""
        return self.capsys.readouterr()

    def stdout(self):
        """Returns a string, the captured stdout."""
        return self.capsys.readouterr().out

    def stderr(self):
        """Returns a string, the captured stderr."""
        return self.capsys.readouterr().err