diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a80edd28c..6cebeeb46 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,12 +10,7 @@ repos: hooks: - id: trailing-whitespace - id: end-of-file-fixer - - id: fix-encoding-pragma - args: [--remove] - id: check-yaml - - id: debug-statements - exclude: _pytest/(debugging|hookspec).py - language_version: python3 - repo: https://github.com/adamchainz/blacken-docs rev: 1.16.0 hooks: @@ -50,13 +45,13 @@ repos: additional_dependencies: ["tox>=4.9"] - repo: local hooks: - - id: pylint - name: pylint - entry: pylint - language: system - types: [python] - args: ["-rn", "-sn", "--fail-on=I"] - stages: [manual] + - id: pylint + name: pylint + entry: pylint + language: system + types: [python] + args: ["-rn", "-sn", "--fail-on=I"] + stages: [manual] - id: rst name: rst entry: rst-lint --encoding utf-8 diff --git a/changelog/12153.doc.rst b/changelog/12153.doc.rst new file mode 100644 index 000000000..ac36becf9 --- /dev/null +++ b/changelog/12153.doc.rst @@ -0,0 +1 @@ +Documented using :envvar:`PYTEST_VERSION` to detect if code is running from within a pytest run. diff --git a/changelog/7166.bugfix.rst b/changelog/7166.bugfix.rst new file mode 100644 index 000000000..98e6821f2 --- /dev/null +++ b/changelog/7166.bugfix.rst @@ -0,0 +1 @@ +Fixed progress percentages (the ``[ 87%]`` at the edge of the screen) sometimes not aligning correctly when running with pytest-xdist ``-n``. diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index 7064f61f0..dec1bed5f 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -405,35 +405,20 @@ Detect if running from within a pytest run Usually it is a bad idea to make application code behave differently if called from a test. But if you absolutely must find out if your application code is -running from a test you can do something like this: +running from a test you can do this: .. code-block:: python - # content of your_module.py + import os - _called_from_test = False - -.. code-block:: python - - # content of conftest.py - - - def pytest_configure(config): - your_module._called_from_test = True - -and then check for the ``your_module._called_from_test`` flag: - -.. code-block:: python - - if your_module._called_from_test: - # called from within a test run + if os.environ.get("PYTEST_VERSION") is not None: + # Things you want to to do if your code is called by pytest. ... else: - # called "normally" + # Things you want to to do if your code is not called by pytest. ... -accordingly in your application. Adding info to test report header -------------------------------------------------------------- diff --git a/pyproject.toml b/pyproject.toml index 43efacf09..2be02ee7e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -107,6 +107,7 @@ select = [ "PLE", # pylint error "PLW", # pylint warning "PLR1714", # Consider merging multiple comparisons + "T100", # flake8-debugger ] ignore = [ # bugbear ignore diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index 6ed0c5c7a..7beab563e 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -1,4 +1,5 @@ # mypy: allow-untyped-defs +# ruff: noqa: T100 """Interactive debugging with PDB, the Python Debugger.""" import argparse diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index acfe7eb95..01d3b6640 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -1,4 +1,5 @@ # mypy: allow-untyped-defs +# ruff: noqa: T100 """Hook specifications for pytest plugins which are invoked by pytest itself and by builtin plugins.""" diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 9bc544ea7..a551f715a 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -180,7 +180,7 @@ def pytest_runtest_call(item: Item) -> None: assert e.__traceback__ is not None # Skip *this* frame sys.last_traceback = e.__traceback__.tb_next - raise e + raise def pytest_runtest_teardown(item: Item, nextitem: Optional[Item]) -> None: @@ -512,7 +512,7 @@ class SetupState: col.setup() except TEST_OUTCOME as exc: self.stack[col] = (self.stack[col][0], exc) - raise exc + raise def addfinalizer(self, finalizer: Callable[[], object], node: Node) -> None: """Attach a finalizer to the given node. diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index bd12e56cf..ecb83733d 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -441,7 +441,7 @@ class TerminalReporter: char = {"xfailed": "x", "skipped": "s"}.get(char, char) return char in self.reportchars - def write_fspath_result(self, nodeid: str, res, **markup: bool) -> None: + def write_fspath_result(self, nodeid: str, res: str, **markup: bool) -> None: fspath = self.config.rootpath / nodeid.split("::")[0] if self.currentfspath is None or fspath != self.currentfspath: if self.currentfspath is not None and self._show_progress_info: @@ -574,10 +574,11 @@ class TerminalReporter: def pytest_runtest_logstart( self, nodeid: str, location: Tuple[str, Optional[int], str] ) -> None: + fspath, lineno, domain = location # Ensure that the path is printed before the # 1st test of a module starts running. if self.showlongtestinfo: - line = self._locationline(nodeid, *location) + line = self._locationline(nodeid, fspath, lineno, domain) self.write_ensure_prefix(line, "") self.flush() elif self.showfspath: @@ -600,7 +601,6 @@ class TerminalReporter: if not letter and not word: # Probably passed setup/teardown. return - running_xdist = hasattr(rep, "node") if markup is None: was_xfail = hasattr(report, "wasxfail") if rep.passed and not was_xfail: @@ -613,11 +613,20 @@ class TerminalReporter: markup = {"yellow": True} else: markup = {} + self._progress_nodeids_reported.add(rep.nodeid) if self.config.get_verbosity(Config.VERBOSITY_TEST_CASES) <= 0: self._tw.write(letter, **markup) + # When running in xdist, the logreport and logfinish of multiple + # items are interspersed, e.g. `logreport`, `logreport`, + # `logfinish`, `logfinish`. To avoid the "past edge" calculation + # from getting confused and overflowing (#7166), do the past edge + # printing here and not in logfinish, except for the 100% which + # should only be printed after all teardowns are finished. + if self._show_progress_info and not self._is_last_item: + self._write_progress_information_if_past_edge() else: - self._progress_nodeids_reported.add(rep.nodeid) line = self._locationline(rep.nodeid, *rep.location) + running_xdist = hasattr(rep, "node") if not running_xdist: self.write_ensure_prefix(line, word, **markup) if rep.skipped or hasattr(report, "wasxfail"): @@ -657,39 +666,29 @@ class TerminalReporter: assert self._session is not None return len(self._progress_nodeids_reported) == self._session.testscollected - def pytest_runtest_logfinish(self, nodeid: str) -> None: - assert self._session + @hookimpl(wrapper=True) + def pytest_runtestloop(self) -> Generator[None, object, object]: + result = yield + + # Write the final/100% progress -- deferred until the loop is complete. if ( self.config.get_verbosity(Config.VERBOSITY_TEST_CASES) <= 0 and self._show_progress_info + and self._progress_nodeids_reported ): - if self._show_progress_info == "count": - num_tests = self._session.testscollected - progress_length = len(f" [{num_tests}/{num_tests}]") - else: - progress_length = len(" [100%]") + self._write_progress_information_filling_space() - self._progress_nodeids_reported.add(nodeid) - - if self._is_last_item: - self._write_progress_information_filling_space() - else: - main_color, _ = self._get_main_color() - w = self._width_of_current_line - past_edge = w + progress_length + 1 >= self._screen_width - if past_edge: - msg = self._get_progress_information_message() - self._tw.write(msg + "\n", **{main_color: True}) + return result def _get_progress_information_message(self) -> str: assert self._session collected = self._session.testscollected if self._show_progress_info == "count": if collected: - progress = self._progress_nodeids_reported + progress = len(self._progress_nodeids_reported) counter_format = f"{{:{len(str(collected))}d}}" format_string = f" [{counter_format}/{{}}]" - return format_string.format(len(progress), collected) + return format_string.format(progress, collected) return f" [ {collected} / {collected} ]" else: if collected: @@ -698,6 +697,20 @@ class TerminalReporter: ) return " [100%]" + def _write_progress_information_if_past_edge(self) -> None: + w = self._width_of_current_line + if self._show_progress_info == "count": + assert self._session + num_tests = self._session.testscollected + progress_length = len(f" [{num_tests}/{num_tests}]") + else: + progress_length = len(" [100%]") + past_edge = w + progress_length + 1 >= self._screen_width + if past_edge: + main_color, _ = self._get_main_color() + msg = self._get_progress_information_message() + self._tw.write(msg + "\n", **{main_color: True}) + def _write_progress_information_filling_space(self) -> None: color, _ = self._get_main_color() msg = self._get_progress_information_message() @@ -946,7 +959,7 @@ class TerminalReporter: line += "[".join(values) return line - # collect_fspath comes from testid which has a "/"-normalized path. + # fspath comes from testid which has a "/"-normalized path. if fspath: res = mkrel(nodeid) if self.verbosity >= 2 and nodeid.split("::")[0] != fspath.replace(