# 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. """ from __future__ import annotations import importlib import os import os.path import sys from typing import Any, Callable, Iterable, Iterator, Optional, Tuple, cast import pytest from coverage.misc import SysModuleSaver from tests.helpers import change_dir, make_file, remove_tree class PytestBase: """A base class to connect to pytest in a test class hierarchy.""" @pytest.fixture(autouse=True) def connect_to_pytest( self, request: pytest.FixtureRequest, monkeypatch: pytest.MonkeyPatch, ) -> None: """Captures pytest facilities for use by other test helpers.""" # pylint: disable=attribute-defined-outside-init self._pytest_request = request self._monkeypatch = monkeypatch self.setUp() def setUp(self) -> None: """Per-test initialization. Override this as you wish.""" pass def addCleanup(self, fn: Callable[..., None], *args: Any) -> None: """Like unittest's addCleanup: code to call when the test is done.""" self._pytest_request.addfinalizer(lambda: fn(*args)) def set_environ(self, name: str, value: str) -> None: """Set an environment variable `name` to be `value`.""" self._monkeypatch.setenv(name, value) def del_environ(self, name: str) -> None: """Delete an environment variable, unless we set it.""" self._monkeypatch.delenv(name, raising=False) class TempDirMixin: """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, tmp_path_factory: pytest.TempPathFactory) -> Iterator[None]: """Create a temp dir for the tests, if they want it.""" if self.run_in_temp_dir: tmpdir = tmp_path_factory.mktemp("t") 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 else: yield def make_file( self, filename: str, text: str = "", bytes: bytes = b"", newline: Optional[str] = None, ) -> str: """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 RestoreModulesMixin: """Auto-restore the imported modules at the end of each test.""" @pytest.fixture(autouse=True) def _module_saving(self) -> Iterable[None]: """Remove modules we imported during the test.""" self._sys_module_saver = SysModuleSaver() try: yield finally: self._sys_module_saver.restore() def clean_local_file_imports(self) -> None: """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._sys_module_saver.restore() # Also have to clean out the .pyc files, since the time stamp # resolution is only one second, a changed file might not be # picked up. remove_tree("__pycache__") 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: pytest.CaptureFixture[str]) -> None: """Grab the fixture so our methods can use it.""" self.capsys = capsys def stdouterr(self) -> Tuple[str, str]: """Returns (out, err), two strings for stdout and stderr.""" return cast(Tuple[str, str], self.capsys.readouterr()) def stdout(self) -> str: """Returns a string, the captured stdout.""" return self.capsys.readouterr().out def stderr(self) -> str: """Returns a string, the captured stderr.""" return self.capsys.readouterr().err