Compare commits
73 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b31db4809b | |||
| f6adebb990 | |||
| b90e7b84d0 | |||
| 19807ab79a | |||
| 3e52124185 | |||
| 1eca228bd5 | |||
| cab02e67d7 | |||
| 64dbc7a0a1 | |||
| 60d992677d | |||
| 0079decf29 | |||
| 3a58fc2d44 | |||
| 39b6bb551c | |||
| 9fbd67dd4b | |||
| eca93db05b | |||
| fb701b538c | |||
| 314e623304 | |||
| 62e75c7d55 | |||
| fd30759d94 | |||
| eb984a717a | |||
| 54f0fb3c63 | |||
| 49a4ed14cf | |||
| f513d33d5a | |||
| 857e34ef85 | |||
| 99dfc19fe6 | |||
| 56544c11b5 | |||
| 7710e18b4c | |||
| 791b51d0fa | |||
| bc4e70e048 | |||
| b817aa457c | |||
| 66b28912ac | |||
| cca029d55e | |||
| d5466b3917 | |||
| 4fce29f15d | |||
| 69e3973d86 | |||
| c842893b02 | |||
| 506b10d295 | |||
| 05061493cb | |||
| f97f3dc3a3 | |||
| 3c31b0132f | |||
| 593178d909 | |||
| 54d5a63d14 | |||
| b55e264a67 | |||
| 13d6114c0a | |||
| b635e16d30 | |||
| a092b3ab36 | |||
| a006dabf6e | |||
| aa7e9de91d | |||
| 6aec32163d | |||
| 2f33ea87c8 | |||
| 1ada62e237 | |||
| 50b232b0cb | |||
| 496196b15c | |||
| 0314b50c52 | |||
| 8e2de91bf8 | |||
| 692ab1160b | |||
| 549839bac5 | |||
| 646a46e5f4 | |||
| f07017f91b | |||
| a17d3b0c44 | |||
| bbec1ce67f | |||
| 5a040aef97 | |||
| c1d2168df6 | |||
| bbe7cbae4a | |||
| deae8f47f6 | |||
| 10f55f79af | |||
| a6d244343f | |||
| 2b552c2240 | |||
| 54d7b9a08e | |||
| 6afc02abca | |||
| e75e2d66a0 | |||
| 66db0b7522 | |||
| 3a68c08426 | |||
| 9e1804a6ee |
@@ -9,3 +9,9 @@ updates:
|
||||
allow:
|
||||
- dependency-type: direct
|
||||
- dependency-type: indirect
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
time: "03:00"
|
||||
open-pull-requests-limit: 10
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: true
|
||||
|
||||
@@ -23,34 +23,29 @@ jobs:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Build and Check Package
|
||||
uses: hynek/build-and-inspect-python-package@v1.5
|
||||
|
||||
- name: Download Package
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: Packages
|
||||
path: dist
|
||||
|
||||
- name: Publish package to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
password: ${{ secrets.pypi_token }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.7"
|
||||
|
||||
- name: Install tox
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install --upgrade tox
|
||||
pip install --upgrade build tox
|
||||
|
||||
- name: Build package
|
||||
run: |
|
||||
python -m build
|
||||
|
||||
- name: Publish package to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
password: ${{ secrets.pypi_token }}
|
||||
|
||||
- name: Publish GitHub release notes
|
||||
env:
|
||||
|
||||
@@ -27,12 +27,12 @@ jobs:
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.8"
|
||||
|
||||
|
||||
@@ -18,11 +18,6 @@ on:
|
||||
env:
|
||||
PYTEST_ADDOPTS: "--color=yes"
|
||||
|
||||
# Cancel running jobs for the same workflow and branch.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
# Set permissions at the job level.
|
||||
permissions: {}
|
||||
|
||||
@@ -194,10 +189,3 @@ jobs:
|
||||
fail_ci_if_error: true
|
||||
files: ./coverage.xml
|
||||
verbose: true
|
||||
|
||||
check-package:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Build and Check Package
|
||||
uses: hynek/build-and-inspect-python-package@v1.5
|
||||
|
||||
@@ -20,12 +20,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
run: python scripts/update-plugin-list.py
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@2455e1596942c2902952003bbb574afbbe2ab2e6
|
||||
uses: peter-evans/create-pull-request@2b011faafdcbc9ceb11414d64d0573f37c774b04
|
||||
with:
|
||||
commit-message: '[automated] Update plugin list'
|
||||
author: 'pytest bot <pytestbot@users.noreply.github.com>'
|
||||
|
||||
@@ -12,7 +12,7 @@ repos:
|
||||
- id: blacken-docs
|
||||
additional_dependencies: [black==20.8b1]
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.3.0
|
||||
rev: v4.4.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
@@ -23,7 +23,7 @@ repos:
|
||||
exclude: _pytest/(debugging|hookspec).py
|
||||
language_version: python3
|
||||
- repo: https://github.com/PyCQA/autoflake
|
||||
rev: v1.7.6
|
||||
rev: v2.0.0
|
||||
hooks:
|
||||
- id: autoflake
|
||||
name: autoflake
|
||||
@@ -31,7 +31,7 @@ repos:
|
||||
language: python
|
||||
files: \.py$
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 5.0.4
|
||||
rev: 6.0.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
language_version: python3
|
||||
@@ -39,7 +39,7 @@ repos:
|
||||
- flake8-typing-imports==1.12.0
|
||||
- flake8-docstrings==1.5.0
|
||||
- repo: https://github.com/asottile/reorder_python_imports
|
||||
rev: v3.8.5
|
||||
rev: v3.9.0
|
||||
hooks:
|
||||
- id: reorder-python-imports
|
||||
args: ['--application-directories=.:src', --py37-plus]
|
||||
@@ -49,7 +49,7 @@ repos:
|
||||
- id: pyupgrade
|
||||
args: [--py37-plus]
|
||||
- repo: https://github.com/asottile/setup-cfg-fmt
|
||||
rev: v2.1.0
|
||||
rev: v2.2.0
|
||||
hooks:
|
||||
- id: setup-cfg-fmt
|
||||
args: ["--max-py-version=3.11", "--include-version-classifiers"]
|
||||
@@ -58,7 +58,7 @@ repos:
|
||||
hooks:
|
||||
- id: python-use-type-annotations
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v0.982
|
||||
rev: v0.991
|
||||
hooks:
|
||||
- id: mypy
|
||||
files: ^(src/|testing/)
|
||||
|
||||
@@ -43,6 +43,7 @@ Ariel Pillemer
|
||||
Armin Rigo
|
||||
Aron Coyle
|
||||
Aron Curzon
|
||||
Ashish Kurmi
|
||||
Aviral Verma
|
||||
Aviv Palivoda
|
||||
Babak Keyvani
|
||||
@@ -57,6 +58,7 @@ Brian Maissy
|
||||
Brian Okken
|
||||
Brianna Laugher
|
||||
Bruno Oliveira
|
||||
Cal Jacobson
|
||||
Cal Leeming
|
||||
Carl Friedrich Bolz
|
||||
Carlos Jenkins
|
||||
@@ -183,8 +185,8 @@ Joseph Hunkeler
|
||||
Josh Karpel
|
||||
Joshua Bronson
|
||||
Jurko Gospodnetić
|
||||
Justyna Janczyszyn
|
||||
Justice Ndou
|
||||
Justyna Janczyszyn
|
||||
Kale Kundert
|
||||
Kamran Ahmad
|
||||
Karl O. Pinc
|
||||
@@ -223,6 +225,7 @@ Marcin Bachry
|
||||
Marco Gorelli
|
||||
Mark Abramowitz
|
||||
Mark Dickinson
|
||||
Marko Pacak
|
||||
Markus Unterwaditzer
|
||||
Martijn Faassen
|
||||
Martin Altmayer
|
||||
@@ -236,7 +239,6 @@ Matthias Hafner
|
||||
Maxim Filipenko
|
||||
Maximilian Cosmo Sitter
|
||||
mbyt
|
||||
Mickey Pashov
|
||||
Michael Aquilina
|
||||
Michael Birtwell
|
||||
Michael Droettboom
|
||||
@@ -245,6 +247,7 @@ Michael Krebs
|
||||
Michael Seifert
|
||||
Michal Wajszczuk
|
||||
Michał Zięba
|
||||
Mickey Pashov
|
||||
Mihai Capotă
|
||||
Mike Hoyle (hoylemd)
|
||||
Mike Lundy
|
||||
@@ -259,9 +262,9 @@ Niclas Olofsson
|
||||
Nicolas Delaby
|
||||
Nikolay Kondratyev
|
||||
Nipunn Koorapati
|
||||
Olga Matoula
|
||||
Oleg Pidsadnyi
|
||||
Oleg Sushchenko
|
||||
Olga Matoula
|
||||
Oliver Bestwalter
|
||||
Omar Kohl
|
||||
Omer Hadari
|
||||
@@ -277,6 +280,7 @@ Paweł Adamczak
|
||||
Pedro Algarvio
|
||||
Petter Strandmark
|
||||
Philipp Loose
|
||||
Pierre Sassoulas
|
||||
Pieter Mulder
|
||||
Piotr Banaszkiewicz
|
||||
Piotr Helm
|
||||
@@ -286,10 +290,9 @@ Prashant Sharma
|
||||
Pulkit Goyal
|
||||
Punyashloka Biswal
|
||||
Quentin Pradet
|
||||
q0w
|
||||
Ralf Schmitt
|
||||
Ram Rachum
|
||||
Ralph Giles
|
||||
Ram Rachum
|
||||
Ran Benita
|
||||
Raphael Castaneda
|
||||
Raphael Pierzina
|
||||
@@ -312,7 +315,6 @@ Samuel Searles-Bryant
|
||||
Samuele Pedroni
|
||||
Sanket Duthade
|
||||
Sankt Petersbug
|
||||
Saravanan Padmanaban
|
||||
Segev Finer
|
||||
Serhii Mozghovyi
|
||||
Seth Junot
|
||||
@@ -344,7 +346,6 @@ Thomas Grainger
|
||||
Thomas Hisch
|
||||
Tim Hoffmann
|
||||
Tim Strazny
|
||||
TJ Bruno
|
||||
Tobias Diez
|
||||
Tom Dalton
|
||||
Tom Viner
|
||||
@@ -376,7 +377,6 @@ Xixi Zhao
|
||||
Xuan Luong
|
||||
Xuecong Liao
|
||||
Yoav Caspi
|
||||
Yuliang Shao
|
||||
Yusuke Kadowaki
|
||||
Yuval Shimon
|
||||
Zac Hatfield-Dodds
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
If multiple errors are raised in teardown, we now re-raise an ``ExceptionGroup`` of them instead of discarding all but the last.
|
||||
@@ -0,0 +1 @@
|
||||
Fix 'importlib.abc.TraversableResources' deprecation warning in Python 3.12.
|
||||
@@ -0,0 +1 @@
|
||||
If a test is skipped from inside a fixture, the test summary now shows the test location instead of the fixture location.
|
||||
@@ -0,0 +1 @@
|
||||
Fix bug where sometimes pytest would use the file system root directory as :ref:`rootdir <rootdir>` on Windows.
|
||||
@@ -0,0 +1 @@
|
||||
Test methods decorated with ``@classmethod`` can now be discovered as tests, following the same rules as normal methods. This fills the gap that static methods were discoverable as tests but not class methods.
|
||||
@@ -0,0 +1,2 @@
|
||||
The full output of a test is no longer truncated if the truncation message would be longer than
|
||||
the hidden text. The line number shown has also been fixed.
|
||||
@@ -0,0 +1 @@
|
||||
``--log-disable`` CLI option added to disable individual loggers.
|
||||
@@ -0,0 +1,2 @@
|
||||
Added :confval:`tmp_path_retention_count` and :confval:`tmp_path_retention_policy` configuration options to control how directories created by the :fixture:`tmp_path` fixture are kept.
|
||||
The default behavior has changed to keep only directories for failed tests, equivalent to `tmp_path_retention_policy="failed"`.
|
||||
@@ -6,8 +6,6 @@ Release announcements
|
||||
:maxdepth: 2
|
||||
|
||||
|
||||
release-7.2.2
|
||||
release-7.2.1
|
||||
release-7.2.0
|
||||
release-7.1.3
|
||||
release-7.1.2
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
pytest-7.2.1
|
||||
=======================================
|
||||
|
||||
pytest 7.2.1 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Anthony Sottile
|
||||
* Bruno Oliveira
|
||||
* Daniel Valenzuela
|
||||
* Kadino
|
||||
* Prerak Patel
|
||||
* Ronny Pfannschmidt
|
||||
* Santiago Castro
|
||||
* s-padmanaban
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
@@ -1,25 +0,0 @@
|
||||
pytest-7.2.2
|
||||
=======================================
|
||||
|
||||
pytest 7.2.2 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Bruno Oliveira
|
||||
* Garvit Shubham
|
||||
* Mahesh Vashishtha
|
||||
* Ramsey
|
||||
* Ronny Pfannschmidt
|
||||
* Teejay
|
||||
* q0w
|
||||
* vin01
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
+2
-2
@@ -22,7 +22,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
cachedir: .pytest_cache
|
||||
rootdir: /home/sweet/project
|
||||
collected 0 items
|
||||
cache -- .../_pytest/cacheprovider.py:509
|
||||
cache -- .../_pytest/cacheprovider.py:510
|
||||
Return a cache object that can persist state between testing sessions.
|
||||
|
||||
cache.get(key, default)
|
||||
@@ -119,7 +119,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
|
||||
For more details: :ref:`doctest_namespace`.
|
||||
|
||||
pytestconfig [session scope] -- .../_pytest/fixtures.py:1356
|
||||
pytestconfig [session scope] -- .../_pytest/fixtures.py:1351
|
||||
Session-scoped fixture that returns the session's :class:`pytest.Config`
|
||||
object.
|
||||
|
||||
|
||||
@@ -28,63 +28,6 @@ with advance notice in the **Deprecations** section of releases.
|
||||
|
||||
.. towncrier release notes start
|
||||
|
||||
pytest 7.2.2 (2023-03-03)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#10533 <https://github.com/pytest-dev/pytest/issues/10533>`_: Fixed :func:`pytest.approx` handling of dictionaries containing one or more values of `0.0`.
|
||||
|
||||
|
||||
- `#10592 <https://github.com/pytest-dev/pytest/issues/10592>`_: Fixed crash if `--cache-show` and `--help` are passed at the same time.
|
||||
|
||||
|
||||
- `#10597 <https://github.com/pytest-dev/pytest/issues/10597>`_: Fixed bug where a fixture method named ``teardown`` would be called as part of ``nose`` teardown stage.
|
||||
|
||||
|
||||
- `#10626 <https://github.com/pytest-dev/pytest/issues/10626>`_: Fixed crash if ``--fixtures`` and ``--help`` are passed at the same time.
|
||||
|
||||
|
||||
- `#10660 <https://github.com/pytest-dev/pytest/issues/10660>`_: Fixed :py:func:`pytest.raises` to return a 'ContextManager' so that type-checkers could narrow
|
||||
:code:`pytest.raises(...) if ... else nullcontext()` down to 'ContextManager' rather than 'object'.
|
||||
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- `#10690 <https://github.com/pytest-dev/pytest/issues/10690>`_: Added `CI` and `BUILD_NUMBER` environment variables to the documentation.
|
||||
|
||||
|
||||
- `#10721 <https://github.com/pytest-dev/pytest/issues/10721>`_: Fixed entry-points declaration in the documentation example using Hatch.
|
||||
|
||||
|
||||
- `#10753 <https://github.com/pytest-dev/pytest/issues/10753>`_: Changed wording of the module level skip to be very explicit
|
||||
about not collecting tests and not executing the rest of the module.
|
||||
|
||||
|
||||
pytest 7.2.1 (2023-01-13)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#10452 <https://github.com/pytest-dev/pytest/issues/10452>`_: Fix 'importlib.abc.TraversableResources' deprecation warning in Python 3.12.
|
||||
|
||||
|
||||
- `#10457 <https://github.com/pytest-dev/pytest/issues/10457>`_: If a test is skipped from inside a fixture, the test summary now shows the test location instead of the fixture location.
|
||||
|
||||
|
||||
- `#10506 <https://github.com/pytest-dev/pytest/issues/10506>`_: Fix bug where sometimes pytest would use the file system root directory as :ref:`rootdir <rootdir>` on Windows.
|
||||
|
||||
|
||||
- `#10607 <https://github.com/pytest-dev/pytest/issues/10607>`_: Fix a race condition when creating junitxml reports, which could occur when multiple instances of pytest execute in parallel.
|
||||
|
||||
|
||||
- `#10641 <https://github.com/pytest-dev/pytest/issues/10641>`_: Fix a race condition when creating or updating the stepwise plugin's cache, which could occur when multiple xdist worker nodes try to simultaneously update the stepwise plugin's cache.
|
||||
|
||||
|
||||
pytest 7.2.0 (2022-10-23)
|
||||
=========================
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ def b(a, order):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def c(b, order):
|
||||
def c(a, b, order):
|
||||
order.append("c")
|
||||
|
||||
|
||||
|
||||
@@ -504,9 +504,9 @@ Running it results in some skips if we don't have all the python interpreters in
|
||||
. $ pytest -rs -q multipython.py
|
||||
sssssssssssssssssssssssssss [100%]
|
||||
========================= short test summary info ==========================
|
||||
SKIPPED [9] multipython.py:69: 'python3.5' not found
|
||||
SKIPPED [9] multipython.py:69: 'python3.6' not found
|
||||
SKIPPED [9] multipython.py:69: 'python3.7' not found
|
||||
SKIPPED [9] multipython.py:29: 'python3.5' not found
|
||||
SKIPPED [9] multipython.py:29: 'python3.6' not found
|
||||
SKIPPED [9] multipython.py:29: 'python3.7' not found
|
||||
27 skipped in 0.12s
|
||||
|
||||
Indirect parametrization of optional implementations/imports
|
||||
@@ -574,7 +574,7 @@ If you run this with reporting for skips enabled:
|
||||
test_module.py .s [100%]
|
||||
|
||||
========================= short test summary info ==========================
|
||||
SKIPPED [1] test_module.py:3: could not import 'opt2': No module named 'opt2'
|
||||
SKIPPED [1] conftest.py:12: could not import 'opt2': No module named 'opt2'
|
||||
======================= 1 passed, 1 skipped in 0.12s =======================
|
||||
|
||||
You'll see that we don't have an ``opt2`` module and thus the second test run
|
||||
|
||||
@@ -895,6 +895,8 @@ here is a little example implemented via a local plugin:
|
||||
|
||||
import pytest
|
||||
|
||||
phase_report_key = StashKey[Dict[str, CollectReport]]()
|
||||
|
||||
|
||||
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
|
||||
def pytest_runtest_makereport(item, call):
|
||||
@@ -902,10 +904,9 @@ here is a little example implemented via a local plugin:
|
||||
outcome = yield
|
||||
rep = outcome.get_result()
|
||||
|
||||
# set a report attribute for each phase of a call, which can
|
||||
# store test results for each phase of a call, which can
|
||||
# be "setup", "call", "teardown"
|
||||
|
||||
setattr(item, "rep_" + rep.when, rep)
|
||||
item.stash.setdefault(phase_report_key, {})[rep.when] = rep
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -913,11 +914,11 @@ here is a little example implemented via a local plugin:
|
||||
yield
|
||||
# request.node is an "item" because we use the default
|
||||
# "function" scope
|
||||
if request.node.rep_setup.failed:
|
||||
print("setting up a test failed!", request.node.nodeid)
|
||||
elif request.node.rep_setup.passed:
|
||||
if request.node.rep_call.failed:
|
||||
print("executing test failed", request.node.nodeid)
|
||||
report = request.node.stash[phase_report_key]
|
||||
if report["setup"].failed:
|
||||
print("setting up a test failed or skipped", request.node.nodeid)
|
||||
elif ("call" not in report) or report["call"].failed:
|
||||
print("executing test failed or skipped", request.node.nodeid)
|
||||
|
||||
|
||||
if you then have failing tests:
|
||||
|
||||
@@ -50,8 +50,8 @@ Conventions for Python test discovery
|
||||
* In those directories, search for ``test_*.py`` or ``*_test.py`` files, imported by their `test package name`_.
|
||||
* From those files, collect test items:
|
||||
|
||||
* ``test`` prefixed test functions or methods outside of class
|
||||
* ``test`` prefixed test functions or methods inside ``Test`` prefixed test classes (without an ``__init__`` method)
|
||||
* ``test`` prefixed test functions or methods outside of class.
|
||||
* ``test`` prefixed test functions or methods inside ``Test`` prefixed test classes (without an ``__init__`` method). Methods decorated with ``@staticmethod`` and ``@classmethods`` are also considered.
|
||||
|
||||
For examples of how to customize your test discovery :doc:`/example/pythoncollection`.
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import process can be controlled through the ``--import-mode`` command-line flag
|
||||
these values:
|
||||
|
||||
* ``prepend`` (default): the directory path containing each module will be inserted into the *beginning*
|
||||
of :py:data:`sys.path` if not already there, and then imported with the :func:`importlib.import_module <importlib.import_module>` function.
|
||||
of :py:data:`sys.path` if not already there, and then imported with the :func:`__import__ <__import__>` builtin.
|
||||
|
||||
This requires test module names to be unique when the test directory tree is not arranged in
|
||||
packages, because the modules will put in :py:data:`sys.modules` after importing.
|
||||
@@ -24,7 +24,7 @@ these values:
|
||||
This is the classic mechanism, dating back from the time Python 2 was still supported.
|
||||
|
||||
* ``append``: the directory containing each module is appended to the end of :py:data:`sys.path` if not already
|
||||
there, and imported with :func:`importlib.import_module <importlib.import_module>`.
|
||||
there, and imported with ``__import__``.
|
||||
|
||||
This better allows to run test modules against installed versions of a package even if the
|
||||
package under test has the same import root. For example:
|
||||
@@ -43,7 +43,7 @@ these values:
|
||||
Same as ``prepend``, requires test module names to be unique when the test directory tree is
|
||||
not arranged in packages, because the modules will put in :py:data:`sys.modules` after importing.
|
||||
|
||||
* ``importlib``: new in pytest-6.0, this mode uses more fine control mechanisms provided by :mod:`importlib` to import test modules. This gives full control over the import process, and doesn't require changing :py:data:`sys.path`.
|
||||
* ``importlib``: new in pytest-6.0, this mode uses :mod:`importlib` to import test modules. This gives full control over the import process, and doesn't require changing :py:data:`sys.path`.
|
||||
|
||||
For this reason this doesn't require test module names to be unique.
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ Install ``pytest``
|
||||
.. code-block:: bash
|
||||
|
||||
$ pytest --version
|
||||
pytest 7.2.2
|
||||
pytest 7.2.0
|
||||
|
||||
.. _`simpletest`:
|
||||
|
||||
|
||||
@@ -109,18 +109,6 @@ When a warning matches more than one option in the list, the action for the last
|
||||
is performed.
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
The ``-W`` flag and the ``filterwarnings`` ini option use warning filters that are
|
||||
similar in structure, but each configuration option interprets its filter
|
||||
differently. For example, *message* in ``filterwarnings`` is a string containing a
|
||||
regular expression that the start of the warning message must match,
|
||||
case-insensitively, while *message* in ``-W`` is a literal string that the start of
|
||||
the warning message must contain (case-insensitively), ignoring any whitespace at
|
||||
the start or end of message. Consult the `warning filter`_ documentation for more
|
||||
details.
|
||||
|
||||
|
||||
.. _`filterwarnings`:
|
||||
|
||||
``@pytest.mark.filterwarnings``
|
||||
@@ -282,34 +270,20 @@ which works in a similar manner to :ref:`raises <assertraises>` (except that
|
||||
warnings.warn("my warning", UserWarning)
|
||||
|
||||
The test will fail if the warning in question is not raised. Use the keyword
|
||||
argument ``match`` to assert that the warning matches a text or regex.
|
||||
To match a literal string that may contain regular expression metacharacters like ``(`` or ``.``, the pattern can
|
||||
first be escaped with ``re.escape``.
|
||||
argument ``match`` to assert that the warning matches a text or regex::
|
||||
|
||||
Some examples:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
|
||||
>>> with warns(UserWarning, match="must be 0 or None"):
|
||||
>>> with warns(UserWarning, match='must be 0 or None'):
|
||||
... warnings.warn("value must be 0 or None", UserWarning)
|
||||
...
|
||||
|
||||
>>> with warns(UserWarning, match=r"must be \d+$"):
|
||||
>>> with warns(UserWarning, match=r'must be \d+$'):
|
||||
... warnings.warn("value must be 42", UserWarning)
|
||||
...
|
||||
|
||||
>>> with warns(UserWarning, match=r"must be \d+$"):
|
||||
>>> with warns(UserWarning, match=r'must be \d+$'):
|
||||
... warnings.warn("this is not here", UserWarning)
|
||||
...
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
Failed: DID NOT WARN. No warnings of type ...UserWarning... were emitted...
|
||||
|
||||
>>> with warns(UserWarning, match=re.escape("issue with foo() func")):
|
||||
... warnings.warn("issue with foo() func")
|
||||
...
|
||||
|
||||
You can also call :func:`pytest.warns` on a function or code string:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@@ -55,6 +55,13 @@ These options can also be customized through ``pytest.ini`` file:
|
||||
log_format = %(asctime)s %(levelname)s %(message)s
|
||||
log_date_format = %Y-%m-%d %H:%M:%S
|
||||
|
||||
Specific loggers can be disabled via ``--log-disable={logger_name}``.
|
||||
This argument can be passed multiple times:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pytest --log-disable=main --log-disable=testing
|
||||
|
||||
Further it is possible to disable reporting of captured content (stdout,
|
||||
stderr and logs) on failed tests completely with:
|
||||
|
||||
|
||||
@@ -131,10 +131,13 @@ The default base temporary directory
|
||||
|
||||
Temporary directories are by default created as sub-directories of
|
||||
the system temporary directory. The base name will be ``pytest-NUM`` where
|
||||
``NUM`` will be incremented with each test run. Moreover, entries older
|
||||
than 3 temporary directories will be removed.
|
||||
``NUM`` will be incremented with each test run.
|
||||
By default, only the directories of failed tests will be kept.
|
||||
Also only the last 3 directries will remain at most.
|
||||
This behavior can be configured with :confval:`tmp_path_retention_count` and
|
||||
:confval:`tmp_path_retention_policy`.
|
||||
|
||||
The number of entries currently cannot be changed, but using the ``--basetemp``
|
||||
Using the ``--basetemp``
|
||||
option will remove the directory before every run, effectively meaning the temporary directories
|
||||
of only the most recent run will be kept.
|
||||
|
||||
|
||||
@@ -167,8 +167,13 @@ it in your ``pyproject.toml`` file.
|
||||
"Framework :: Pytest",
|
||||
]
|
||||
|
||||
[project.entry-points.pytest11]
|
||||
myproject = "myproject.pluginmodule"
|
||||
[tool.setuptools]
|
||||
packages = ["myproject"]
|
||||
|
||||
[project.entry_points]
|
||||
pytest11 = [
|
||||
"myproject = myproject.pluginmodule",
|
||||
]
|
||||
|
||||
If a package is installed this way, ``pytest`` will load
|
||||
``myproject.pluginmodule`` as a plugin which can define
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
.. sidebar:: Next Open Trainings
|
||||
|
||||
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_, March 7th to 9th 2023 (3 day in-depth training), Remote
|
||||
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_, March 7th to 9th 2023 (3 day in-depth training), Remote and Leipzig, Germany
|
||||
|
||||
Also see :doc:`previous talks and blogposts <talks>`.
|
||||
|
||||
|
||||
@@ -335,7 +335,7 @@ For example:
|
||||
|
||||
.. literalinclude:: /example/fixtures/test_fixtures_order_dependencies.py
|
||||
|
||||
If we map out what depends on what, we get something that looks like this:
|
||||
If we map out what depends on what, we get something that look like this:
|
||||
|
||||
.. image:: /example/fixtures/test_fixtures_order_dependencies.*
|
||||
:align: center
|
||||
|
||||
+793
-353
File diff suppressed because it is too large
Load Diff
@@ -1047,14 +1047,6 @@ Environment Variables
|
||||
|
||||
Environment variables that can be used to change pytest's behavior.
|
||||
|
||||
.. envvar:: CI
|
||||
|
||||
When set (regardless of value), pytest acknowledges that is running in a CI process. Alterative to ``BUILD_NUMBER`` variable.
|
||||
|
||||
.. envvar:: BUILD_NUMBER
|
||||
|
||||
When set (regardless of value), pytest acknowledges that is running in a CI process. Alterative to CI variable.
|
||||
|
||||
.. envvar:: PYTEST_ADDOPTS
|
||||
|
||||
This contains a command-line (parsed by the py:mod:`shlex` module) that will be **prepended** to the command line given
|
||||
@@ -1731,6 +1723,40 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||
directories when executing from the root directory.
|
||||
|
||||
|
||||
.. confval:: tmp_path_retention_count
|
||||
|
||||
|
||||
|
||||
How many sessions should we keep the `tmp_path` directories,
|
||||
according to `tmp_path_retention_policy`.
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[pytest]
|
||||
tmp_path_retention_count = 3
|
||||
|
||||
Default: 3
|
||||
|
||||
|
||||
.. confval:: tmp_path_retention_policy
|
||||
|
||||
|
||||
|
||||
Controls which directories created by the `tmp_path` fixture are kept around,
|
||||
based on test outcome.
|
||||
|
||||
* `all`: retains directories for all tests, regardless of the outcome.
|
||||
* `failed`: retains directories only for tests with outcome `error` or `failed`.
|
||||
* `none`: directories are always removed after each test ends, regardless of the outcome.
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[pytest]
|
||||
tmp_path_retention_policy = "all"
|
||||
|
||||
Default: failed
|
||||
|
||||
|
||||
.. confval:: usefixtures
|
||||
|
||||
List of fixtures that will be applied to all test functions; this is semantically the same to apply
|
||||
|
||||
@@ -114,8 +114,3 @@ template = "changelog/_template.rst"
|
||||
|
||||
[tool.black]
|
||||
target-version = ['py37']
|
||||
|
||||
# check-wheel-contents is executed by the build-and-inspect-python-package action.
|
||||
[tool.check-wheel-contents]
|
||||
# W009: Wheel contains multiple toplevel library entries
|
||||
ignore = "W009"
|
||||
|
||||
@@ -78,7 +78,7 @@ def iter_plugins():
|
||||
requires = "N/A"
|
||||
if info["requires_dist"]:
|
||||
for requirement in info["requires_dist"]:
|
||||
if requirement == "pytest" or "pytest " in requirement:
|
||||
if re.match(r"pytest(?![-.\w])", requirement):
|
||||
requires = requirement
|
||||
break
|
||||
releases = response.json()["releases"]
|
||||
@@ -90,7 +90,9 @@ def iter_plugins():
|
||||
last_release = release_date.strftime("%b %d, %Y")
|
||||
break
|
||||
name = f':pypi:`{info["name"]}`'
|
||||
summary = escape_rst(info["summary"].replace("\n", ""))
|
||||
summary = ""
|
||||
if info["summary"]:
|
||||
summary = escape_rst(info["summary"].replace("\n", ""))
|
||||
yield {
|
||||
"name": name,
|
||||
"summary": summary.strip(),
|
||||
|
||||
@@ -96,7 +96,6 @@ mypy_path = src
|
||||
check_untyped_defs = True
|
||||
disallow_any_generics = True
|
||||
ignore_missing_imports = True
|
||||
no_implicit_optional = True
|
||||
show_error_codes = True
|
||||
strict_equality = True
|
||||
warn_redundant_casts = True
|
||||
|
||||
@@ -78,15 +78,15 @@ class FastFilesCompleter:
|
||||
|
||||
def __call__(self, prefix: str, **kwargs: Any) -> List[str]:
|
||||
# Only called on non option completions.
|
||||
if os.path.sep in prefix[1:]:
|
||||
prefix_dir = len(os.path.dirname(prefix) + os.path.sep)
|
||||
if os.sep in prefix[1:]:
|
||||
prefix_dir = len(os.path.dirname(prefix) + os.sep)
|
||||
else:
|
||||
prefix_dir = 0
|
||||
completion = []
|
||||
globbed = []
|
||||
if "*" not in prefix and "?" not in prefix:
|
||||
# We are on unix, otherwise no bash.
|
||||
if not prefix or prefix[-1] == os.path.sep:
|
||||
if not prefix or prefix[-1] == os.sep:
|
||||
globbed.extend(glob(prefix + ".*"))
|
||||
prefix += "*"
|
||||
globbed.extend(glob(prefix))
|
||||
|
||||
@@ -24,6 +24,7 @@ from stat import S_ISLNK
|
||||
from stat import S_ISREG
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
from typing import overload
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@@ -146,7 +147,7 @@ class Visitor:
|
||||
self.fil = fil
|
||||
self.ignore = ignore
|
||||
self.breadthfirst = bf
|
||||
self.optsort = sort and sorted or (lambda x: x)
|
||||
self.optsort = cast(Callable[[Any], Any], sorted) if sort else (lambda x: x)
|
||||
|
||||
def gen(self, path):
|
||||
try:
|
||||
@@ -224,7 +225,7 @@ class Stat:
|
||||
raise NotImplementedError("XXX win32")
|
||||
import pwd
|
||||
|
||||
entry = error.checked_call(pwd.getpwuid, self.uid)
|
||||
entry = error.checked_call(pwd.getpwuid, self.uid) # type:ignore[attr-defined]
|
||||
return entry[0]
|
||||
|
||||
@property
|
||||
@@ -234,7 +235,7 @@ class Stat:
|
||||
raise NotImplementedError("XXX win32")
|
||||
import grp
|
||||
|
||||
entry = error.checked_call(grp.getgrgid, self.gid)
|
||||
entry = error.checked_call(grp.getgrgid, self.gid) # type:ignore[attr-defined]
|
||||
return entry[0]
|
||||
|
||||
def isdir(self):
|
||||
@@ -252,7 +253,7 @@ def getuserid(user):
|
||||
import pwd
|
||||
|
||||
if not isinstance(user, int):
|
||||
user = pwd.getpwnam(user)[2]
|
||||
user = pwd.getpwnam(user)[2] # type:ignore[attr-defined]
|
||||
return user
|
||||
|
||||
|
||||
@@ -260,7 +261,7 @@ def getgroupid(group):
|
||||
import grp
|
||||
|
||||
if not isinstance(group, int):
|
||||
group = grp.getgrnam(group)[2]
|
||||
group = grp.getgrnam(group)[2] # type:ignore[attr-defined]
|
||||
return group
|
||||
|
||||
|
||||
@@ -795,7 +796,7 @@ class LocalPath:
|
||||
kw = {"exists": 1}
|
||||
return Checkers(self)._evaluate(kw)
|
||||
|
||||
_patternchars = set("*?[" + os.path.sep)
|
||||
_patternchars = set("*?[" + os.sep)
|
||||
|
||||
def listdir(self, fil=None, sort=None):
|
||||
"""List directory contents, possibly filter by the given fil func
|
||||
@@ -1127,7 +1128,7 @@ class LocalPath:
|
||||
modfile = modfile[:-1]
|
||||
elif modfile.endswith("$py.class"):
|
||||
modfile = modfile[:-9] + ".py"
|
||||
if modfile.endswith(os.path.sep + "__init__.py"):
|
||||
if modfile.endswith(os.sep + "__init__.py"):
|
||||
if self.basename != "__init__.py":
|
||||
modfile = modfile[:-12]
|
||||
try:
|
||||
|
||||
@@ -180,7 +180,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
|
||||
for initial_path in self.session._initialpaths:
|
||||
# Make something as c:/projects/my_project/path.py ->
|
||||
# ['c:', 'projects', 'my_project', 'path.py']
|
||||
parts = str(initial_path).split(os.path.sep)
|
||||
parts = str(initial_path).split(os.sep)
|
||||
# add 'path' to basenames to be checked.
|
||||
self._basenames_to_check_rewrite.add(os.path.splitext(parts[-1])[0])
|
||||
|
||||
|
||||
@@ -38,9 +38,9 @@ def _truncate_explanation(
|
||||
"""Truncate given list of strings that makes up the assertion explanation.
|
||||
|
||||
Truncates to either 8 lines, or 640 characters - whichever the input reaches
|
||||
first. The remaining lines will be replaced by a usage message.
|
||||
first, taking the truncation explanation into account. The remaining lines
|
||||
will be replaced by a usage message.
|
||||
"""
|
||||
|
||||
if max_lines is None:
|
||||
max_lines = DEFAULT_MAX_LINES
|
||||
if max_chars is None:
|
||||
@@ -48,35 +48,56 @@ def _truncate_explanation(
|
||||
|
||||
# Check if truncation required
|
||||
input_char_count = len("".join(input_lines))
|
||||
if len(input_lines) <= max_lines and input_char_count <= max_chars:
|
||||
# The length of the truncation explanation depends on the number of lines
|
||||
# removed but is at least 68 characters:
|
||||
# The real value is
|
||||
# 64 (for the base message:
|
||||
# '...\n...Full output truncated (1 line hidden), use '-vv' to show")'
|
||||
# )
|
||||
# + 1 (for plural)
|
||||
# + int(math.log10(len(input_lines) - max_lines)) (number of hidden line, at least 1)
|
||||
# + 3 for the '...' added to the truncated line
|
||||
# But if there's more than 100 lines it's very likely that we're going to
|
||||
# truncate, so we don't need the exact value using log10.
|
||||
tolerable_max_chars = (
|
||||
max_chars + 70 # 64 + 1 (for plural) + 2 (for '99') + 3 for '...'
|
||||
)
|
||||
# The truncation explanation add two lines to the output
|
||||
tolerable_max_lines = max_lines + 2
|
||||
if (
|
||||
len(input_lines) <= tolerable_max_lines
|
||||
and input_char_count <= tolerable_max_chars
|
||||
):
|
||||
return input_lines
|
||||
|
||||
# Truncate first to max_lines, and then truncate to max_chars if max_chars
|
||||
# is exceeded.
|
||||
# Truncate first to max_lines, and then truncate to max_chars if necessary
|
||||
truncated_explanation = input_lines[:max_lines]
|
||||
truncated_explanation = _truncate_by_char_count(truncated_explanation, max_chars)
|
||||
|
||||
# Add ellipsis to final line
|
||||
truncated_explanation[-1] = truncated_explanation[-1] + "..."
|
||||
|
||||
# Append useful message to explanation
|
||||
truncated_line_count = len(input_lines) - len(truncated_explanation)
|
||||
truncated_line_count += 1 # Account for the part-truncated final line
|
||||
msg = "...Full output truncated"
|
||||
if truncated_line_count == 1:
|
||||
msg += f" ({truncated_line_count} line hidden)"
|
||||
truncated_char = True
|
||||
# We reevaluate the need to truncate chars following removal of some lines
|
||||
if len("".join(truncated_explanation)) > tolerable_max_chars:
|
||||
truncated_explanation = _truncate_by_char_count(
|
||||
truncated_explanation, max_chars
|
||||
)
|
||||
else:
|
||||
msg += f" ({truncated_line_count} lines hidden)"
|
||||
msg += f", {USAGE_MSG}"
|
||||
truncated_explanation.extend(["", str(msg)])
|
||||
return truncated_explanation
|
||||
truncated_char = False
|
||||
|
||||
truncated_line_count = len(input_lines) - len(truncated_explanation)
|
||||
if truncated_explanation[-1]:
|
||||
# Add ellipsis and take into account part-truncated final line
|
||||
truncated_explanation[-1] = truncated_explanation[-1] + "..."
|
||||
if truncated_char:
|
||||
# It's possible that we did not remove any char from this line
|
||||
truncated_line_count += 1
|
||||
else:
|
||||
# Add proper ellipsis when we were able to fit a full line exactly
|
||||
truncated_explanation[-1] = "..."
|
||||
return truncated_explanation + [
|
||||
"",
|
||||
f"...Full output truncated ({truncated_line_count} line"
|
||||
f"{'' if truncated_line_count == 1 else 's'} hidden), {USAGE_MSG}",
|
||||
]
|
||||
|
||||
|
||||
def _truncate_by_char_count(input_lines: List[str], max_chars: int) -> List[str]:
|
||||
# Check if truncation required
|
||||
if len("".join(input_lines)) <= max_chars:
|
||||
return input_lines
|
||||
|
||||
# Find point at which input length exceeds total allowed length
|
||||
iterated_char_count = 0
|
||||
for iterated_index, input_line in enumerate(input_lines):
|
||||
|
||||
@@ -32,6 +32,7 @@ from _pytest.python import Module
|
||||
from _pytest.python import Package
|
||||
from _pytest.reports import TestReport
|
||||
|
||||
|
||||
README_CONTENT = """\
|
||||
# pytest cache directory #
|
||||
|
||||
@@ -491,7 +492,7 @@ def pytest_addoption(parser: Parser) -> None:
|
||||
|
||||
|
||||
def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
|
||||
if config.option.cacheshow and not config.option.help:
|
||||
if config.option.cacheshow:
|
||||
from _pytest.main import wrap_session
|
||||
|
||||
return wrap_session(config, cacheshow)
|
||||
|
||||
@@ -998,8 +998,6 @@ class Config:
|
||||
self.hook.pytest_addoption.call_historic(
|
||||
kwargs=dict(parser=self._parser, pluginmanager=self.pluginmanager)
|
||||
)
|
||||
self.args_source = Config.ArgsSource.ARGS
|
||||
self.args: List[str] = []
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _pytest.cacheprovider import Cache
|
||||
@@ -1339,8 +1337,8 @@ class Config:
|
||||
|
||||
def parse(self, args: List[str], addopts: bool = True) -> None:
|
||||
# Parse given cmdline arguments into this config object.
|
||||
assert (
|
||||
self.args == []
|
||||
assert not hasattr(
|
||||
self, "args"
|
||||
), "can only parse cmdline args at most once per Config object"
|
||||
self.hook.pytest_addhooks.call_historic(
|
||||
kwargs=dict(pluginmanager=self.pluginmanager)
|
||||
|
||||
@@ -738,7 +738,7 @@ def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> No
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
|
||||
def pytest_report_header(
|
||||
def pytest_report_header( # type:ignore[empty-body]
|
||||
config: "Config", start_path: Path, startdir: "LEGACY_PATH"
|
||||
) -> Union[str, List[str]]:
|
||||
"""Return a string or list of strings to be displayed as header info for terminal reporting.
|
||||
@@ -767,7 +767,7 @@ def pytest_report_header(
|
||||
"""
|
||||
|
||||
|
||||
def pytest_report_collectionfinish(
|
||||
def pytest_report_collectionfinish( # type:ignore[empty-body]
|
||||
config: "Config",
|
||||
start_path: Path,
|
||||
startdir: "LEGACY_PATH",
|
||||
@@ -800,7 +800,7 @@ def pytest_report_collectionfinish(
|
||||
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_report_teststatus(
|
||||
def pytest_report_teststatus( # type:ignore[empty-body]
|
||||
report: Union["CollectReport", "TestReport"], config: "Config"
|
||||
) -> Tuple[str, str, Union[str, Mapping[str, bool]]]:
|
||||
"""Return result-category, shortletter and verbose word for status
|
||||
@@ -880,7 +880,9 @@ def pytest_warning_recorded(
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
|
||||
def pytest_markeval_namespace(config: "Config") -> Dict[str, Any]:
|
||||
def pytest_markeval_namespace( # type:ignore[empty-body]
|
||||
config: "Config",
|
||||
) -> Dict[str, Any]:
|
||||
"""Called when constructing the globals dictionary used for
|
||||
evaluating string conditions in xfail/skipif markers.
|
||||
|
||||
|
||||
@@ -645,8 +645,8 @@ class LogXML:
|
||||
|
||||
def pytest_sessionfinish(self) -> None:
|
||||
dirname = os.path.dirname(os.path.abspath(self.logfile))
|
||||
# exist_ok avoids filesystem race conditions between checking path existence and requesting creation
|
||||
os.makedirs(dirname, exist_ok=True)
|
||||
if not os.path.isdir(dirname):
|
||||
os.makedirs(dirname)
|
||||
|
||||
with open(self.logfile, "w", encoding="utf-8") as logfile:
|
||||
suite_stop_time = timing.time()
|
||||
|
||||
@@ -297,6 +297,13 @@ def pytest_addoption(parser: Parser) -> None:
|
||||
default=None,
|
||||
help="Auto-indent multiline messages passed to the logging module. Accepts true|on, false|off or an integer.",
|
||||
)
|
||||
group.addoption(
|
||||
"--log-disable",
|
||||
action="append",
|
||||
default=[],
|
||||
dest="logger_disable",
|
||||
help="Disable a logger by name. Can be passed multipe times.",
|
||||
)
|
||||
|
||||
|
||||
_HandlerType = TypeVar("_HandlerType", bound=logging.Handler)
|
||||
@@ -594,6 +601,15 @@ class LoggingPlugin:
|
||||
get_option_ini(config, "log_auto_indent"),
|
||||
)
|
||||
self.log_cli_handler.setFormatter(log_cli_formatter)
|
||||
self._disable_loggers(loggers_to_disable=config.option.logger_disable)
|
||||
|
||||
def _disable_loggers(self, loggers_to_disable: List[str]) -> None:
|
||||
if not loggers_to_disable:
|
||||
return
|
||||
|
||||
for name in loggers_to_disable:
|
||||
logger = logging.getLogger(name)
|
||||
logger.disabled = True
|
||||
|
||||
def _create_formatter(self, log_format, log_date_format, auto_indent):
|
||||
# Color option doesn't exist if terminal plugin is disabled.
|
||||
|
||||
@@ -157,12 +157,8 @@ def skip(
|
||||
The message to show the user as reason for the skip.
|
||||
|
||||
:param allow_module_level:
|
||||
Allows this function to be called at module level.
|
||||
Raising the skip exception at module level will stop
|
||||
the execution of the module and prevent the collection of all tests in the module,
|
||||
even those defined before the `skip` call.
|
||||
|
||||
Defaults to False.
|
||||
Allows this function to be called at module level, skipping the rest
|
||||
of the module. Defaults to False.
|
||||
|
||||
:param msg:
|
||||
Same as ``reason``, but deprecated. Will be removed in a future version, use ``reason`` instead.
|
||||
|
||||
+20
-7
@@ -335,15 +335,26 @@ def cleanup_candidates(root: Path, prefix: str, keep: int) -> Iterator[Path]:
|
||||
yield path
|
||||
|
||||
|
||||
def cleanup_dead_symlink(root: Path):
|
||||
for left_dir in root.iterdir():
|
||||
if left_dir.is_symlink():
|
||||
if not left_dir.resolve().exists():
|
||||
left_dir.unlink()
|
||||
|
||||
|
||||
def cleanup_numbered_dir(
|
||||
root: Path, prefix: str, keep: int, consider_lock_dead_if_created_before: float
|
||||
) -> None:
|
||||
"""Cleanup for lock driven numbered directories."""
|
||||
if not root.exists():
|
||||
return
|
||||
for path in cleanup_candidates(root, prefix, keep):
|
||||
try_cleanup(path, consider_lock_dead_if_created_before)
|
||||
for path in root.glob("garbage-*"):
|
||||
try_cleanup(path, consider_lock_dead_if_created_before)
|
||||
|
||||
cleanup_dead_symlink(root)
|
||||
|
||||
|
||||
def make_numbered_dir_with_cleanup(
|
||||
root: Path,
|
||||
@@ -357,8 +368,10 @@ def make_numbered_dir_with_cleanup(
|
||||
for i in range(10):
|
||||
try:
|
||||
p = make_numbered_dir(root, prefix, mode)
|
||||
lock_path = create_cleanup_lock(p)
|
||||
register_cleanup_lock_removal(lock_path)
|
||||
# Only lock the current dir when keep is not 0
|
||||
if keep != 0:
|
||||
lock_path = create_cleanup_lock(p)
|
||||
register_cleanup_lock_removal(lock_path)
|
||||
except Exception as exc:
|
||||
e = exc
|
||||
else:
|
||||
@@ -464,14 +477,14 @@ def import_path(
|
||||
|
||||
* `mode == ImportMode.prepend`: the directory containing the module (or package, taking
|
||||
`__init__.py` files into account) will be put at the *start* of `sys.path` before
|
||||
being imported with `importlib.import_module`.
|
||||
being imported with `__import__.
|
||||
|
||||
* `mode == ImportMode.append`: same as `prepend`, but the directory will be appended
|
||||
to the end of `sys.path`, if not already in `sys.path`.
|
||||
|
||||
* `mode == ImportMode.importlib`: uses more fine control mechanisms provided by `importlib`
|
||||
to import the module, which avoids having to muck with `sys.path` at all. It effectively
|
||||
allows having same-named test modules in different places.
|
||||
to import the module, which avoids having to use `__import__` and muck with `sys.path`
|
||||
at all. It effectively allows having same-named test modules in different places.
|
||||
|
||||
:param root:
|
||||
Used as an anchor when mode == ImportMode.importlib to obtain
|
||||
@@ -544,8 +557,8 @@ def import_path(
|
||||
|
||||
if module_file.endswith((".pyc", ".pyo")):
|
||||
module_file = module_file[:-1]
|
||||
if module_file.endswith(os.path.sep + "__init__.py"):
|
||||
module_file = module_file[: -(len(os.path.sep + "__init__.py"))]
|
||||
if module_file.endswith(os.sep + "__init__.py"):
|
||||
module_file = module_file[: -(len(os.sep + "__init__.py"))]
|
||||
|
||||
try:
|
||||
is_same = _is_same(str(path), module_file)
|
||||
|
||||
@@ -403,8 +403,8 @@ class PyCollector(PyobjMixin, nodes.Collector):
|
||||
|
||||
def istestfunction(self, obj: object, name: str) -> bool:
|
||||
if self.funcnamefilter(name) or self.isnosetest(obj):
|
||||
if isinstance(obj, staticmethod):
|
||||
# staticmethods need to be unwrapped.
|
||||
if isinstance(obj, (staticmethod, classmethod)):
|
||||
# staticmethods and classmethods need to be unwrapped.
|
||||
obj = safe_getattr(obj, "__func__", False)
|
||||
return callable(obj) and fixtures.getfixturemarker(obj) is None
|
||||
else:
|
||||
@@ -848,7 +848,7 @@ class Class(PyCollector):
|
||||
other fixtures (#517).
|
||||
"""
|
||||
setup_class = _get_first_non_fixture_func(self.obj, ("setup_class",))
|
||||
teardown_class = _get_first_non_fixture_func(self.obj, ("teardown_class",))
|
||||
teardown_class = getattr(self.obj, "teardown_class", None)
|
||||
if setup_class is None and teardown_class is None:
|
||||
return
|
||||
|
||||
@@ -885,12 +885,12 @@ class Class(PyCollector):
|
||||
emit_nose_setup_warning = True
|
||||
setup_method = _get_first_non_fixture_func(self.obj, (setup_name,))
|
||||
teardown_name = "teardown_method"
|
||||
teardown_method = _get_first_non_fixture_func(self.obj, (teardown_name,))
|
||||
teardown_method = getattr(self.obj, teardown_name, None)
|
||||
emit_nose_teardown_warning = False
|
||||
if teardown_method is None and has_nose:
|
||||
teardown_name = "teardown"
|
||||
emit_nose_teardown_warning = True
|
||||
teardown_method = _get_first_non_fixture_func(self.obj, (teardown_name,))
|
||||
teardown_method = getattr(self.obj, teardown_name, None)
|
||||
if setup_method is None and teardown_method is None:
|
||||
return
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from types import TracebackType
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
from typing import ContextManager
|
||||
from typing import Generic
|
||||
from typing import List
|
||||
from typing import Mapping
|
||||
from typing import Optional
|
||||
@@ -269,16 +269,10 @@ class ApproxMapping(ApproxBase):
|
||||
max_abs_diff = max(
|
||||
max_abs_diff, abs(approx_value.expected - other_value)
|
||||
)
|
||||
if approx_value.expected == 0.0:
|
||||
max_rel_diff = math.inf
|
||||
else:
|
||||
max_rel_diff = max(
|
||||
max_rel_diff,
|
||||
abs(
|
||||
(approx_value.expected - other_value)
|
||||
/ approx_value.expected
|
||||
),
|
||||
)
|
||||
max_rel_diff = max(
|
||||
max_rel_diff,
|
||||
abs((approx_value.expected - other_value) / approx_value.expected),
|
||||
)
|
||||
different_ids.append(approx_key)
|
||||
|
||||
message_data = [
|
||||
@@ -807,7 +801,7 @@ def raises( # noqa: F811
|
||||
r"""Assert that a code block/function call raises an exception.
|
||||
|
||||
:param typing.Type[E] | typing.Tuple[typing.Type[E], ...] expected_exception:
|
||||
The excpected exception type, or a tuple if one of multiple possible
|
||||
The expected exception type, or a tuple if one of multiple possible
|
||||
exception types are excepted.
|
||||
:kwparam str | typing.Pattern[str] | None match:
|
||||
If specified, a string containing a regular expression,
|
||||
@@ -963,7 +957,7 @@ raises.Exception = fail.Exception # type: ignore
|
||||
|
||||
|
||||
@final
|
||||
class RaisesContext(ContextManager[_pytest._code.ExceptionInfo[E]]):
|
||||
class RaisesContext(Generic[E]):
|
||||
def __init__(
|
||||
self,
|
||||
expected_exception: Union[Type[E], Tuple[Type[E], ...]],
|
||||
|
||||
+17
-7
@@ -35,6 +35,9 @@ from _pytest.outcomes import OutcomeException
|
||||
from _pytest.outcomes import Skipped
|
||||
from _pytest.outcomes import TEST_OUTCOME
|
||||
|
||||
if sys.version_info[:2] < (3, 11):
|
||||
from exceptiongroup import BaseExceptionGroup
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Literal
|
||||
|
||||
@@ -512,22 +515,29 @@ class SetupState:
|
||||
stack is torn down.
|
||||
"""
|
||||
needed_collectors = nextitem and nextitem.listchain() or []
|
||||
exc = None
|
||||
exceptions: List[BaseException] = []
|
||||
while self.stack:
|
||||
if list(self.stack.keys()) == needed_collectors[: len(self.stack)]:
|
||||
break
|
||||
node, (finalizers, _) = self.stack.popitem()
|
||||
these_exceptions = []
|
||||
while finalizers:
|
||||
fin = finalizers.pop()
|
||||
try:
|
||||
fin()
|
||||
except TEST_OUTCOME as e:
|
||||
# XXX Only first exception will be seen by user,
|
||||
# ideally all should be reported.
|
||||
if exc is None:
|
||||
exc = e
|
||||
if exc:
|
||||
raise exc
|
||||
these_exceptions.append(e)
|
||||
|
||||
if len(these_exceptions) == 1:
|
||||
exceptions.extend(these_exceptions)
|
||||
elif these_exceptions:
|
||||
msg = f"errors while tearing down {node!r}"
|
||||
exceptions.append(BaseExceptionGroup(msg, these_exceptions[::-1]))
|
||||
|
||||
if len(exceptions) == 1:
|
||||
raise exceptions[0]
|
||||
elif exceptions:
|
||||
raise BaseExceptionGroup("errors during test teardown", exceptions[::-1])
|
||||
if nextitem is None:
|
||||
assert not self.stack
|
||||
|
||||
|
||||
@@ -48,10 +48,6 @@ def pytest_configure(config: Config) -> None:
|
||||
def pytest_sessionfinish(session: Session) -> None:
|
||||
if not session.config.getoption("stepwise"):
|
||||
assert session.config.cache is not None
|
||||
if hasattr(session.config, "workerinput"):
|
||||
# Do not update cache if this process is a xdist worker to prevent
|
||||
# race conditions (#10641).
|
||||
return
|
||||
# Clear the list of failing tests if the plugin is not active.
|
||||
session.config.cache.set(STEPWISE_CACHE_DIR, [])
|
||||
|
||||
@@ -123,8 +119,4 @@ class StepwisePlugin:
|
||||
return None
|
||||
|
||||
def pytest_sessionfinish(self) -> None:
|
||||
if hasattr(self.config, "workerinput"):
|
||||
# Do not update cache if this process is a xdist worker to prevent
|
||||
# race conditions (#10641).
|
||||
return
|
||||
self.cache.set(STEPWISE_CACHE_DIR, self.lastfailed)
|
||||
|
||||
+116
-5
@@ -4,21 +4,42 @@ import re
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from shutil import rmtree
|
||||
from typing import Dict
|
||||
from typing import Generator
|
||||
from typing import Optional
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
from _pytest.nodes import Item
|
||||
from _pytest.reports import CollectReport
|
||||
from _pytest.stash import StashKey
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Literal
|
||||
|
||||
RetentionType = Literal["all", "failed", "none"]
|
||||
|
||||
|
||||
import attr
|
||||
from _pytest.config.argparsing import Parser
|
||||
|
||||
from .pathlib import LOCK_TIMEOUT
|
||||
from .pathlib import make_numbered_dir
|
||||
from .pathlib import make_numbered_dir_with_cleanup
|
||||
from .pathlib import rm_rf
|
||||
from .pathlib import cleanup_dead_symlink
|
||||
from _pytest.compat import final
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import ExitCode
|
||||
from _pytest.config import hookimpl
|
||||
from _pytest.deprecated import check_ispytest
|
||||
from _pytest.fixtures import fixture
|
||||
from _pytest.fixtures import FixtureRequest
|
||||
from _pytest.monkeypatch import MonkeyPatch
|
||||
|
||||
tmppath_result_key = StashKey[Dict[str, bool]]()
|
||||
|
||||
|
||||
@final
|
||||
@attr.s(init=False)
|
||||
@@ -31,10 +52,14 @@ class TempPathFactory:
|
||||
_given_basetemp = attr.ib(type=Optional[Path])
|
||||
_trace = attr.ib()
|
||||
_basetemp = attr.ib(type=Optional[Path])
|
||||
_retention_count = attr.ib(type=int)
|
||||
_retention_policy = attr.ib(type="RetentionType")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
given_basetemp: Optional[Path],
|
||||
retention_count: int,
|
||||
retention_policy: "RetentionType",
|
||||
trace,
|
||||
basetemp: Optional[Path] = None,
|
||||
*,
|
||||
@@ -49,6 +74,8 @@ class TempPathFactory:
|
||||
# Path.absolute() exists, but it is not public (see https://bugs.python.org/issue25012).
|
||||
self._given_basetemp = Path(os.path.abspath(str(given_basetemp)))
|
||||
self._trace = trace
|
||||
self._retention_count = retention_count
|
||||
self._retention_policy = retention_policy
|
||||
self._basetemp = basetemp
|
||||
|
||||
@classmethod
|
||||
@@ -63,9 +90,23 @@ class TempPathFactory:
|
||||
:meta private:
|
||||
"""
|
||||
check_ispytest(_ispytest)
|
||||
count = int(config.getini("tmp_path_retention_count"))
|
||||
if count < 0:
|
||||
raise ValueError(
|
||||
f"tmp_path_retention_count must be >= 0. Current input: {count}."
|
||||
)
|
||||
|
||||
policy = config.getini("tmp_path_retention_policy")
|
||||
if policy not in ("all", "failed", "none"):
|
||||
raise ValueError(
|
||||
f"tmp_path_retention_policy must be either all, failed, none. Current intput: {policy}."
|
||||
)
|
||||
|
||||
return cls(
|
||||
given_basetemp=config.option.basetemp,
|
||||
trace=config.trace.get("tmpdir"),
|
||||
retention_count=count,
|
||||
retention_policy=policy,
|
||||
_ispytest=True,
|
||||
)
|
||||
|
||||
@@ -146,10 +187,13 @@ class TempPathFactory:
|
||||
)
|
||||
if (rootdir_stat.st_mode & 0o077) != 0:
|
||||
os.chmod(rootdir, rootdir_stat.st_mode & ~0o077)
|
||||
keep = self._retention_count
|
||||
if self._retention_policy == "none":
|
||||
keep = 0
|
||||
basetemp = make_numbered_dir_with_cleanup(
|
||||
prefix="pytest-",
|
||||
root=rootdir,
|
||||
keep=3,
|
||||
keep=keep,
|
||||
lock_timeout=LOCK_TIMEOUT,
|
||||
mode=0o700,
|
||||
)
|
||||
@@ -184,6 +228,21 @@ def pytest_configure(config: Config) -> None:
|
||||
mp.setattr(config, "_tmp_path_factory", _tmp_path_factory, raising=False)
|
||||
|
||||
|
||||
def pytest_addoption(parser: Parser) -> None:
|
||||
parser.addini(
|
||||
"tmp_path_retention_count",
|
||||
help="How many sessions should we keep the `tmp_path` directories, according to `tmp_path_retention_policy`.",
|
||||
default=3,
|
||||
)
|
||||
|
||||
parser.addini(
|
||||
"tmp_path_retention_policy",
|
||||
help="Controls which directories created by the `tmp_path` fixture are kept around, based on test outcome. "
|
||||
"(all/failed/none)",
|
||||
default="failed",
|
||||
)
|
||||
|
||||
|
||||
@fixture(scope="session")
|
||||
def tmp_path_factory(request: FixtureRequest) -> TempPathFactory:
|
||||
"""Return a :class:`pytest.TempPathFactory` instance for the test session."""
|
||||
@@ -200,17 +259,69 @@ def _mk_tmp(request: FixtureRequest, factory: TempPathFactory) -> Path:
|
||||
|
||||
|
||||
@fixture
|
||||
def tmp_path(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> Path:
|
||||
def tmp_path(
|
||||
request: FixtureRequest, tmp_path_factory: TempPathFactory
|
||||
) -> Generator[Path, None, None]:
|
||||
"""Return a temporary directory path object which is unique to each test
|
||||
function invocation, created as a sub directory of the base temporary
|
||||
directory.
|
||||
|
||||
By default, a new base temporary directory is created each test session,
|
||||
and old bases are removed after 3 sessions, to aid in debugging. If
|
||||
``--basetemp`` is used then it is cleared each session. See :ref:`base
|
||||
and only the base of failed session is kept. Also it only keeps the last 3 bases
|
||||
at most. This can be configured with :confval:`tmp_path_retention_count` and
|
||||
:confval:`tmp_path_retention_policy`.
|
||||
If ``--basetemp`` is used then it is cleared each session. See :ref:`base
|
||||
temporary directory`.
|
||||
|
||||
The returned object is a :class:`pathlib.Path` object.
|
||||
"""
|
||||
|
||||
return _mk_tmp(request, tmp_path_factory)
|
||||
path = _mk_tmp(request, tmp_path_factory)
|
||||
yield path
|
||||
|
||||
# Remove the tmpdir if the policy is "failed" and the test passed.
|
||||
tmp_path_factory: TempPathFactory = request.session.config._tmp_path_factory # type: ignore
|
||||
policy = tmp_path_factory._retention_policy
|
||||
result_dict = request.node.stash[tmppath_result_key]
|
||||
|
||||
if policy == "failed" and result_dict.get("call", True):
|
||||
# We do a "best effort" to remove files, but it might not be possible due to some leaked resource,
|
||||
# permissions, etc, in which case we ignore it.
|
||||
rmtree(path, ignore_errors=True)
|
||||
|
||||
del request.node.stash[tmppath_result_key]
|
||||
|
||||
# remove dead symlink
|
||||
basetemp = tmp_path_factory._basetemp
|
||||
if basetemp is None:
|
||||
return
|
||||
cleanup_dead_symlink(basetemp)
|
||||
|
||||
|
||||
def pytest_sessionfinish(session, exitstatus: Union[int, ExitCode]):
|
||||
"""After each session, remove base directory if all the tests passed,
|
||||
the policy is "failed", and the basetemp is not specified by a user.
|
||||
"""
|
||||
tmp_path_factory: TempPathFactory = session.config._tmp_path_factory
|
||||
if tmp_path_factory._basetemp is None:
|
||||
return
|
||||
policy = tmp_path_factory._retention_policy
|
||||
if (
|
||||
exitstatus == 0
|
||||
and policy == "failed"
|
||||
and tmp_path_factory._given_basetemp is None
|
||||
):
|
||||
passed_dir = tmp_path_factory._basetemp
|
||||
if passed_dir.exists():
|
||||
# We do a "best effort" to remove files, but it might not be possible due to some leaked resource,
|
||||
# permissions, etc, in which case we ignore it.
|
||||
rmtree(passed_dir, ignore_errors=True)
|
||||
|
||||
|
||||
@hookimpl(tryfirst=True, hookwrapper=True)
|
||||
def pytest_runtest_makereport(item: Item, call):
|
||||
outcome = yield
|
||||
result: CollectReport = outcome.get_result()
|
||||
|
||||
empty: Dict[str, bool] = {}
|
||||
item.stash.setdefault(tmppath_result_key, empty)[result.when] = result.passed
|
||||
|
||||
@@ -803,7 +803,7 @@ class TestLocalPath(CommonFSTests):
|
||||
# depending on how the paths are used), but > 4096 (which is the
|
||||
# Linux' limitation) - the behaviour of paths with names > 4096 chars
|
||||
# is undetermined
|
||||
newfilename = "/test" * 60
|
||||
newfilename = "/test" * 60 # type:ignore[unreachable]
|
||||
l1 = tmpdir.join(newfilename)
|
||||
l1.ensure(file=True)
|
||||
l1.write("foo")
|
||||
@@ -1344,8 +1344,8 @@ class TestPOSIXLocalPath:
|
||||
assert realpath.basename == "file"
|
||||
|
||||
def test_owner(self, path1, tmpdir):
|
||||
from pwd import getpwuid
|
||||
from grp import getgrgid
|
||||
from pwd import getpwuid # type:ignore[attr-defined]
|
||||
from grp import getgrgid # type:ignore[attr-defined]
|
||||
|
||||
stat = path1.stat()
|
||||
assert stat.path == path1
|
||||
|
||||
@@ -694,14 +694,7 @@ class TestInvocationVariants:
|
||||
|
||||
# mixed module and filenames:
|
||||
monkeypatch.chdir("world")
|
||||
|
||||
# pgk_resources.declare_namespace has been deprecated in favor of implicit namespace packages.
|
||||
# While we could change the test to use implicit namespace packages, seems better
|
||||
# to still ensure the old declaration via declare_namespace still works.
|
||||
ignore_w = r"-Wignore:Deprecated call to `pkg_resources.declare_namespace"
|
||||
result = pytester.runpytest(
|
||||
"--pyargs", "-v", "ns_pkg.hello", "ns_pkg/world", ignore_w
|
||||
)
|
||||
result = pytester.runpytest("--pyargs", "-v", "ns_pkg.hello", "ns_pkg/world")
|
||||
assert result.ret == 0
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
|
||||
@@ -157,7 +157,6 @@ def color_mapping():
|
||||
"number": "\x1b[94m",
|
||||
"str": "\x1b[33m",
|
||||
"print": "\x1b[96m",
|
||||
"endline": "\x1b[90m\x1b[39;49;00m",
|
||||
}
|
||||
RE_COLORS = {k: re.escape(v) for k, v in COLORS.items()}
|
||||
|
||||
|
||||
@@ -254,7 +254,7 @@ class TestTerminalWriterLineWidth:
|
||||
pytest.param(
|
||||
True,
|
||||
True,
|
||||
"{kw}assert{hl-reset} {number}0{hl-reset}{endline}\n",
|
||||
"{kw}assert{hl-reset} {number}0{hl-reset}\n",
|
||||
id="with markup and code_highlight",
|
||||
),
|
||||
pytest.param(
|
||||
|
||||
@@ -1165,3 +1165,72 @@ def test_log_file_cli_subdirectories_are_successfully_created(
|
||||
result = pytester.runpytest("--log-file=foo/bar/logf.log")
|
||||
assert "logf.log" in os.listdir(expected)
|
||||
assert result.ret == ExitCode.OK
|
||||
|
||||
|
||||
def test_disable_loggers(testdir):
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
disabled_log = logging.getLogger('disabled')
|
||||
test_log = logging.getLogger('test')
|
||||
def test_logger_propagation(caplog):
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
disabled_log.warning("no log; no stderr")
|
||||
test_log.debug("Visible text!")
|
||||
assert caplog.record_tuples == [('test', 10, 'Visible text!')]
|
||||
"""
|
||||
)
|
||||
result = testdir.runpytest("--log-disable=disabled", "-s")
|
||||
assert result.ret == ExitCode.OK
|
||||
assert not result.stderr.lines
|
||||
|
||||
|
||||
def test_disable_loggers_does_not_propagate(testdir):
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
|
||||
parent_logger = logging.getLogger("parent")
|
||||
child_logger = parent_logger.getChild("child")
|
||||
|
||||
def test_logger_propagation_to_parent(caplog):
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
parent_logger.warning("some parent logger message")
|
||||
child_logger.warning("some child logger message")
|
||||
assert len(caplog.record_tuples) == 1
|
||||
assert caplog.record_tuples[0][0] == "parent"
|
||||
assert caplog.record_tuples[0][2] == "some parent logger message"
|
||||
"""
|
||||
)
|
||||
|
||||
result = testdir.runpytest("--log-disable=parent.child", "-s")
|
||||
assert result.ret == ExitCode.OK
|
||||
assert not result.stderr.lines
|
||||
|
||||
|
||||
def test_log_disabling_works_with_log_cli(testdir):
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
import logging
|
||||
disabled_log = logging.getLogger('disabled')
|
||||
test_log = logging.getLogger('test')
|
||||
|
||||
def test_log_cli_works(caplog):
|
||||
test_log.info("Visible text!")
|
||||
disabled_log.warning("This string will be suppressed.")
|
||||
"""
|
||||
)
|
||||
result = testdir.runpytest(
|
||||
"--log-cli-level=DEBUG",
|
||||
"--log-disable=disabled",
|
||||
)
|
||||
assert result.ret == ExitCode.OK
|
||||
result.stdout.fnmatch_lines(
|
||||
"INFO test:test_log_disabling_works_with_log_cli.py:6 Visible text!"
|
||||
)
|
||||
result.stdout.no_fnmatch_line(
|
||||
"WARNING disabled:test_log_disabling_works_with_log_cli.py:7 This string will be suppressed."
|
||||
)
|
||||
assert not result.stderr.lines
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
anyio[curio,trio]==3.6.1
|
||||
django==4.1.2
|
||||
pytest-asyncio==0.19.0
|
||||
pytest-bdd==6.0.1
|
||||
anyio[curio,trio]==3.6.2
|
||||
django==4.1.3
|
||||
pytest-asyncio==0.20.2
|
||||
pytest-bdd==6.1.1
|
||||
pytest-cov==4.0.0
|
||||
pytest-django==4.5.2
|
||||
pytest-flakes==4.0.5
|
||||
pytest-html==3.1.1
|
||||
pytest-html==3.2.0
|
||||
pytest-mock==3.10.0
|
||||
pytest-rerunfailures==10.2
|
||||
pytest-rerunfailures==10.3
|
||||
pytest-sugar==0.9.5
|
||||
pytest-trio==0.7.0
|
||||
pytest-twisted==1.14.0
|
||||
|
||||
@@ -630,19 +630,6 @@ class TestApprox:
|
||||
def test_dict_vs_other(self):
|
||||
assert 1 != approx({"a": 0})
|
||||
|
||||
def test_dict_for_div_by_zero(self, assert_approx_raises_regex):
|
||||
assert_approx_raises_regex(
|
||||
{"foo": 42.0},
|
||||
{"foo": 0.0},
|
||||
[
|
||||
r" comparison failed. Mismatched elements: 1 / 1:",
|
||||
rf" Max absolute difference: {SOME_FLOAT}",
|
||||
r" Max relative difference: inf",
|
||||
r" Index \| Obtained\s+\| Expected ",
|
||||
rf" foo | {SOME_FLOAT} \| {SOME_FLOAT} ± {SOME_FLOAT}",
|
||||
],
|
||||
)
|
||||
|
||||
def test_numpy_array(self):
|
||||
np = pytest.importorskip("numpy")
|
||||
|
||||
|
||||
@@ -3338,10 +3338,6 @@ class TestShowFixtures:
|
||||
config = pytester.parseconfigure("--funcargs")
|
||||
assert config.option.showfixtures
|
||||
|
||||
def test_show_help(self, pytester: Pytester) -> None:
|
||||
result = pytester.runpytest("--fixtures", "--help")
|
||||
assert not result.ret
|
||||
|
||||
def test_show_fixtures(self, pytester: Pytester) -> None:
|
||||
result = pytester.runpytest("--fixtures")
|
||||
result.stdout.fnmatch_lines(
|
||||
|
||||
@@ -416,7 +416,7 @@ def test_function_instance(pytester: Pytester) -> None:
|
||||
def test_static(): pass
|
||||
"""
|
||||
)
|
||||
assert len(items) == 3
|
||||
assert len(items) == 4
|
||||
assert isinstance(items[0], Function)
|
||||
assert items[0].name == "test_func"
|
||||
assert items[0].instance is None
|
||||
@@ -424,6 +424,6 @@ def test_function_instance(pytester: Pytester) -> None:
|
||||
assert items[1].name == "test_method"
|
||||
assert items[1].instance is not None
|
||||
assert items[1].instance.__class__.__name__ == "TestIt"
|
||||
assert isinstance(items[2], Function)
|
||||
assert items[2].name == "test_static"
|
||||
assert items[2].instance is None
|
||||
assert isinstance(items[3], Function)
|
||||
assert items[3].name == "test_static"
|
||||
assert items[3].instance is None
|
||||
|
||||
+37
-12
@@ -807,9 +807,9 @@ class TestAssert_reprcompare_dataclass:
|
||||
"E ['field_b']",
|
||||
"E ",
|
||||
"E Drill down into differing attribute field_b:",
|
||||
"E field_b: 'b' != 'c'...",
|
||||
"E ",
|
||||
"E ...Full output truncated (3 lines hidden), use '-vv' to show",
|
||||
"E field_b: 'b' != 'c'",
|
||||
"E - c",
|
||||
"E + b",
|
||||
],
|
||||
consecutive=True,
|
||||
)
|
||||
@@ -827,7 +827,7 @@ class TestAssert_reprcompare_dataclass:
|
||||
"E Drill down into differing attribute g:",
|
||||
"E g: S(a=10, b='ten') != S(a=20, b='xxx')...",
|
||||
"E ",
|
||||
"E ...Full output truncated (52 lines hidden), use '-vv' to show",
|
||||
"E ...Full output truncated (51 lines hidden), use '-vv' to show",
|
||||
],
|
||||
consecutive=True,
|
||||
)
|
||||
@@ -1188,30 +1188,55 @@ class TestTruncateExplanation:
|
||||
def test_truncates_at_8_lines_when_given_list_of_empty_strings(self) -> None:
|
||||
expl = ["" for x in range(50)]
|
||||
result = truncate._truncate_explanation(expl, max_lines=8, max_chars=100)
|
||||
assert len(result) != len(expl)
|
||||
assert result != expl
|
||||
assert len(result) == 8 + self.LINES_IN_TRUNCATION_MSG
|
||||
assert "Full output truncated" in result[-1]
|
||||
assert "43 lines hidden" in result[-1]
|
||||
assert "42 lines hidden" in result[-1]
|
||||
last_line_before_trunc_msg = result[-self.LINES_IN_TRUNCATION_MSG - 1]
|
||||
assert last_line_before_trunc_msg.endswith("...")
|
||||
|
||||
def test_truncates_at_8_lines_when_first_8_lines_are_LT_max_chars(self) -> None:
|
||||
expl = ["a" for x in range(100)]
|
||||
total_lines = 100
|
||||
expl = ["a" for x in range(total_lines)]
|
||||
result = truncate._truncate_explanation(expl, max_lines=8, max_chars=8 * 80)
|
||||
assert result != expl
|
||||
assert len(result) == 8 + self.LINES_IN_TRUNCATION_MSG
|
||||
assert "Full output truncated" in result[-1]
|
||||
assert "93 lines hidden" in result[-1]
|
||||
assert f"{total_lines - 8} lines hidden" in result[-1]
|
||||
last_line_before_trunc_msg = result[-self.LINES_IN_TRUNCATION_MSG - 1]
|
||||
assert last_line_before_trunc_msg.endswith("...")
|
||||
|
||||
def test_truncates_at_8_lines_when_there_is_one_line_to_remove(self) -> None:
|
||||
"""The number of line in the result is 9, the same number as if we truncated."""
|
||||
expl = ["a" for x in range(9)]
|
||||
result = truncate._truncate_explanation(expl, max_lines=8, max_chars=8 * 80)
|
||||
assert result == expl
|
||||
assert "truncated" not in result[-1]
|
||||
|
||||
def test_truncates_edgecase_when_truncation_message_makes_the_result_longer_for_chars(
|
||||
self,
|
||||
) -> None:
|
||||
line = "a" * 10
|
||||
expl = [line, line]
|
||||
result = truncate._truncate_explanation(expl, max_lines=10, max_chars=10)
|
||||
assert result == [line, line]
|
||||
|
||||
def test_truncates_edgecase_when_truncation_message_makes_the_result_longer_for_lines(
|
||||
self,
|
||||
) -> None:
|
||||
line = "a" * 10
|
||||
expl = [line, line]
|
||||
result = truncate._truncate_explanation(expl, max_lines=1, max_chars=100)
|
||||
assert result == [line, line]
|
||||
|
||||
def test_truncates_at_8_lines_when_first_8_lines_are_EQ_max_chars(self) -> None:
|
||||
expl = ["a" * 80 for x in range(16)]
|
||||
expl = [chr(97 + x) * 80 for x in range(16)]
|
||||
result = truncate._truncate_explanation(expl, max_lines=8, max_chars=8 * 80)
|
||||
assert result != expl
|
||||
assert len(result) == 8 + self.LINES_IN_TRUNCATION_MSG
|
||||
assert len(result) == 16 - 8 + self.LINES_IN_TRUNCATION_MSG
|
||||
assert "Full output truncated" in result[-1]
|
||||
assert "9 lines hidden" in result[-1]
|
||||
assert "8 lines hidden" in result[-1]
|
||||
last_line_before_trunc_msg = result[-self.LINES_IN_TRUNCATION_MSG - 1]
|
||||
assert last_line_before_trunc_msg.endswith("...")
|
||||
|
||||
@@ -1240,7 +1265,7 @@ class TestTruncateExplanation:
|
||||
|
||||
line_count = 7
|
||||
line_len = 100
|
||||
expected_truncated_lines = 2
|
||||
expected_truncated_lines = 1
|
||||
pytester.makepyfile(
|
||||
r"""
|
||||
def test_many_lines():
|
||||
@@ -1261,7 +1286,7 @@ class TestTruncateExplanation:
|
||||
"*+ 1*",
|
||||
"*+ 3*",
|
||||
"*+ 5*",
|
||||
"*truncated (%d lines hidden)*use*-vv*" % expected_truncated_lines,
|
||||
"*truncated (%d line hidden)*use*-vv*" % expected_truncated_lines,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -1249,8 +1249,3 @@ def test_cachedir_tag(pytester: Pytester) -> None:
|
||||
cache.set("foo", "bar")
|
||||
cachedir_tag_path = cache._cachedir.joinpath("CACHEDIR.TAG")
|
||||
assert cachedir_tag_path.read_bytes() == CACHEDIR_TAG_CONTENT
|
||||
|
||||
|
||||
def test_clioption_with_cacheshow_and_help(pytester: Pytester) -> None:
|
||||
result = pytester.runpytest("--cache-show", "--help")
|
||||
assert result.ret == 0
|
||||
|
||||
@@ -735,6 +735,20 @@ class Test_genitems:
|
||||
assert s.endswith("test_example_items1.testone")
|
||||
print(s)
|
||||
|
||||
def test_classmethod_is_discovered(self, pytester: Pytester) -> None:
|
||||
"""Test that classmethods are discovered"""
|
||||
p = pytester.makepyfile(
|
||||
"""
|
||||
class TestCase:
|
||||
@classmethod
|
||||
def test_classmethod(cls) -> None:
|
||||
pass
|
||||
"""
|
||||
)
|
||||
items, reprec = pytester.inline_genitems(p)
|
||||
ids = [x.getmodpath() for x in items] # type: ignore[attr-defined]
|
||||
assert ids == ["TestCase.test_classmethod"]
|
||||
|
||||
def test_class_and_functions_discovery_using_glob(self, pytester: Pytester) -> None:
|
||||
"""Test that Python_classes and Python_functions config options work
|
||||
as prefixes and glob-like patterns (#600)."""
|
||||
|
||||
@@ -92,7 +92,7 @@ class TestSetattrWithImportPath:
|
||||
mp.delattr("os.path.abspath")
|
||||
assert not hasattr(os.path, "abspath")
|
||||
mp.undo()
|
||||
assert os.path.abspath
|
||||
assert os.path.abspath # type:ignore[truthy-function]
|
||||
|
||||
|
||||
def test_delattr() -> None:
|
||||
@@ -425,9 +425,6 @@ def test_context_classmethod() -> None:
|
||||
assert A.x == 1
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings(
|
||||
"ignore:Deprecated call to `pkg_resources.declare_namespace"
|
||||
)
|
||||
def test_syspath_prepend_with_namespace_packages(
|
||||
pytester: Pytester, monkeypatch: MonkeyPatch
|
||||
) -> None:
|
||||
|
||||
@@ -496,24 +496,3 @@ def test_nose_setup_skipped_if_non_callable(pytester: Pytester) -> None:
|
||||
)
|
||||
result = pytester.runpytest(p, "-p", "nose")
|
||||
assert result.ret == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("fixture_name", ("teardown", "teardown_class"))
|
||||
def test_teardown_fixture_not_called_directly(fixture_name, pytester: Pytester) -> None:
|
||||
"""Regression test for #10597."""
|
||||
p = pytester.makepyfile(
|
||||
f"""
|
||||
import pytest
|
||||
|
||||
class TestHello:
|
||||
|
||||
@pytest.fixture
|
||||
def {fixture_name}(self):
|
||||
yield
|
||||
|
||||
def test_hello(self, {fixture_name}):
|
||||
assert True
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest(p, "-p", "nose")
|
||||
assert result.ret == 0
|
||||
|
||||
+30
-4
@@ -2,6 +2,7 @@ import inspect
|
||||
import os
|
||||
import sys
|
||||
import types
|
||||
from functools import partial
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
@@ -19,6 +20,9 @@ from _pytest.monkeypatch import MonkeyPatch
|
||||
from _pytest.outcomes import OutcomeException
|
||||
from _pytest.pytester import Pytester
|
||||
|
||||
if sys.version_info[:2] < (3, 11):
|
||||
from exceptiongroup import ExceptionGroup
|
||||
|
||||
|
||||
class TestSetupState:
|
||||
def test_setup(self, pytester: Pytester) -> None:
|
||||
@@ -77,8 +81,6 @@ class TestSetupState:
|
||||
assert r == ["fin3", "fin1"]
|
||||
|
||||
def test_teardown_multiple_fail(self, pytester: Pytester) -> None:
|
||||
# Ensure the first exception is the one which is re-raised.
|
||||
# Ideally both would be reported however.
|
||||
def fin1():
|
||||
raise Exception("oops1")
|
||||
|
||||
@@ -90,9 +92,14 @@ class TestSetupState:
|
||||
ss.setup(item)
|
||||
ss.addfinalizer(fin1, item)
|
||||
ss.addfinalizer(fin2, item)
|
||||
with pytest.raises(Exception) as err:
|
||||
with pytest.raises(ExceptionGroup) as err:
|
||||
ss.teardown_exact(None)
|
||||
assert err.value.args == ("oops2",)
|
||||
|
||||
# Note that finalizers are run LIFO, but because FIFO is more intuitive for
|
||||
# users we reverse the order of messages, and see the error from fin1 first.
|
||||
err1, err2 = err.value.exceptions
|
||||
assert err1.args == ("oops1",)
|
||||
assert err2.args == ("oops2",)
|
||||
|
||||
def test_teardown_multiple_scopes_one_fails(self, pytester: Pytester) -> None:
|
||||
module_teardown = []
|
||||
@@ -113,6 +120,25 @@ class TestSetupState:
|
||||
ss.teardown_exact(None)
|
||||
assert module_teardown == ["fin_module"]
|
||||
|
||||
def test_teardown_multiple_scopes_several_fail(self, pytester) -> None:
|
||||
def raiser(exc):
|
||||
raise exc
|
||||
|
||||
item = pytester.getitem("def test_func(): pass")
|
||||
mod = item.listchain()[-2]
|
||||
ss = item.session._setupstate
|
||||
ss.setup(item)
|
||||
ss.addfinalizer(partial(raiser, KeyError("from module scope")), mod)
|
||||
ss.addfinalizer(partial(raiser, TypeError("from function scope 1")), item)
|
||||
ss.addfinalizer(partial(raiser, ValueError("from function scope 2")), item)
|
||||
|
||||
with pytest.raises(ExceptionGroup, match="errors during test teardown") as e:
|
||||
ss.teardown_exact(None)
|
||||
mod, func = e.value.exceptions
|
||||
assert isinstance(mod, KeyError)
|
||||
assert isinstance(func.exceptions[0], TypeError) # type: ignore
|
||||
assert isinstance(func.exceptions[1], ValueError) # type: ignore
|
||||
|
||||
|
||||
class BaseFunctionalTests:
|
||||
def test_passfunction(self, pytester: Pytester) -> None:
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from _pytest.cacheprovider import Cache
|
||||
from _pytest.monkeypatch import MonkeyPatch
|
||||
from _pytest.pytester import Pytester
|
||||
from _pytest.stepwise import STEPWISE_CACHE_DIR
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -282,76 +278,3 @@ def test_stepwise_skip_is_independent(pytester: Pytester) -> None:
|
||||
def test_sw_skip_help(pytester: Pytester) -> None:
|
||||
result = pytester.runpytest("-h")
|
||||
result.stdout.fnmatch_lines("*Implicitly enables --stepwise.")
|
||||
|
||||
|
||||
def test_stepwise_xdist_dont_store_lastfailed(pytester: Pytester) -> None:
|
||||
pytester.makefile(
|
||||
ext=".ini",
|
||||
pytest=f"[pytest]\ncache_dir = {pytester.path}\n",
|
||||
)
|
||||
|
||||
pytester.makepyfile(
|
||||
conftest="""
|
||||
import pytest
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_configure(config) -> None:
|
||||
config.workerinput = True
|
||||
"""
|
||||
)
|
||||
pytester.makepyfile(
|
||||
test_one="""
|
||||
def test_one():
|
||||
assert False
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest("--stepwise")
|
||||
assert result.ret == pytest.ExitCode.INTERRUPTED
|
||||
|
||||
stepwise_cache_file = (
|
||||
pytester.path / Cache._CACHE_PREFIX_VALUES / STEPWISE_CACHE_DIR
|
||||
)
|
||||
assert not Path(stepwise_cache_file).exists()
|
||||
|
||||
|
||||
def test_disabled_stepwise_xdist_dont_clear_cache(pytester: Pytester) -> None:
|
||||
pytester.makefile(
|
||||
ext=".ini",
|
||||
pytest=f"[pytest]\ncache_dir = {pytester.path}\n",
|
||||
)
|
||||
|
||||
stepwise_cache_file = (
|
||||
pytester.path / Cache._CACHE_PREFIX_VALUES / STEPWISE_CACHE_DIR
|
||||
)
|
||||
stepwise_cache_dir = stepwise_cache_file.parent
|
||||
stepwise_cache_dir.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
stepwise_cache_file_relative = f"{Cache._CACHE_PREFIX_VALUES}/{STEPWISE_CACHE_DIR}"
|
||||
|
||||
expected_value = '"test_one.py::test_one"'
|
||||
content = {f"{stepwise_cache_file_relative}": expected_value}
|
||||
|
||||
pytester.makefile(ext="", **content)
|
||||
|
||||
pytester.makepyfile(
|
||||
conftest="""
|
||||
import pytest
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_configure(config) -> None:
|
||||
config.workerinput = True
|
||||
"""
|
||||
)
|
||||
pytester.makepyfile(
|
||||
test_one="""
|
||||
def test_one():
|
||||
assert True
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
assert result.ret == 0
|
||||
|
||||
assert Path(stepwise_cache_file).exists()
|
||||
with stepwise_cache_file.open() as file_handle:
|
||||
observed_value = file_handle.readlines()
|
||||
assert [expected_value] == observed_value
|
||||
|
||||
+12
-12
@@ -1265,14 +1265,14 @@ def test_color_yes(pytester: Pytester, color_mapping) -> None:
|
||||
"=*= FAILURES =*=",
|
||||
"{red}{bold}_*_ test_this _*_{reset}",
|
||||
"",
|
||||
" {kw}def{hl-reset} {function}test_this{hl-reset}():{endline}",
|
||||
"> fail(){endline}",
|
||||
" {kw}def{hl-reset} {function}test_this{hl-reset}():",
|
||||
"> fail()",
|
||||
"",
|
||||
"{bold}{red}test_color_yes.py{reset}:5: ",
|
||||
"_ _ * _ _*",
|
||||
"",
|
||||
" {kw}def{hl-reset} {function}fail{hl-reset}():{endline}",
|
||||
"> {kw}assert{hl-reset} {number}0{hl-reset}{endline}",
|
||||
" {kw}def{hl-reset} {function}fail{hl-reset}():",
|
||||
"> {kw}assert{hl-reset} {number}0{hl-reset}",
|
||||
"{bold}{red}E assert 0{reset}",
|
||||
"",
|
||||
"{bold}{red}test_color_yes.py{reset}:2: AssertionError",
|
||||
@@ -1292,9 +1292,9 @@ def test_color_yes(pytester: Pytester, color_mapping) -> None:
|
||||
"=*= FAILURES =*=",
|
||||
"{red}{bold}_*_ test_this _*_{reset}",
|
||||
"{bold}{red}test_color_yes.py{reset}:5: in test_this",
|
||||
" fail(){endline}",
|
||||
" fail()",
|
||||
"{bold}{red}test_color_yes.py{reset}:2: in fail",
|
||||
" {kw}assert{hl-reset} {number}0{hl-reset}{endline}",
|
||||
" {kw}assert{hl-reset} {number}0{hl-reset}",
|
||||
"{bold}{red}E assert 0{reset}",
|
||||
"{red}=*= {red}{bold}1 failed{reset}{red} in *s{reset}{red} =*={reset}",
|
||||
]
|
||||
@@ -2472,8 +2472,8 @@ class TestCodeHighlight:
|
||||
result.stdout.fnmatch_lines(
|
||||
color_mapping.format_for_fnmatch(
|
||||
[
|
||||
" {kw}def{hl-reset} {function}test_foo{hl-reset}():{endline}",
|
||||
"> {kw}assert{hl-reset} {number}1{hl-reset} == {number}10{hl-reset}{endline}",
|
||||
" {kw}def{hl-reset} {function}test_foo{hl-reset}():",
|
||||
"> {kw}assert{hl-reset} {number}1{hl-reset} == {number}10{hl-reset}",
|
||||
"{bold}{red}E assert 1 == 10{reset}",
|
||||
]
|
||||
)
|
||||
@@ -2494,9 +2494,9 @@ class TestCodeHighlight:
|
||||
result.stdout.fnmatch_lines(
|
||||
color_mapping.format_for_fnmatch(
|
||||
[
|
||||
" {kw}def{hl-reset} {function}test_foo{hl-reset}():{endline}",
|
||||
" {kw}def{hl-reset} {function}test_foo{hl-reset}():",
|
||||
" {print}print{hl-reset}({str}'''{hl-reset}{str}{hl-reset}",
|
||||
"> {str} {hl-reset}{str}'''{hl-reset}); {kw}assert{hl-reset} {number}0{hl-reset}{endline}",
|
||||
"> {str} {hl-reset}{str}'''{hl-reset}); {kw}assert{hl-reset} {number}0{hl-reset}",
|
||||
"{bold}{red}E assert 0{reset}",
|
||||
]
|
||||
)
|
||||
@@ -2517,8 +2517,8 @@ class TestCodeHighlight:
|
||||
result.stdout.fnmatch_lines(
|
||||
color_mapping.format_for_fnmatch(
|
||||
[
|
||||
" {kw}def{hl-reset} {function}test_foo{hl-reset}():{endline}",
|
||||
"> {kw}assert{hl-reset} {number}1{hl-reset} == {number}10{hl-reset}{endline}",
|
||||
" {kw}def{hl-reset} {function}test_foo{hl-reset}():",
|
||||
"> {kw}assert{hl-reset} {number}1{hl-reset} == {number}10{hl-reset}",
|
||||
"{bold}{red}E assert 1 == 10{reset}",
|
||||
]
|
||||
)
|
||||
|
||||
+129
-5
@@ -42,6 +42,14 @@ class FakeConfig:
|
||||
def get(self, key):
|
||||
return lambda *k: None
|
||||
|
||||
def getini(self, name):
|
||||
if name == "tmp_path_retention_count":
|
||||
return 3
|
||||
elif name == "tmp_path_retention_policy":
|
||||
return "failed"
|
||||
else:
|
||||
assert False
|
||||
|
||||
@property
|
||||
def option(self):
|
||||
return self
|
||||
@@ -84,6 +92,117 @@ class TestConfigTmpPath:
|
||||
assert mytemp.exists()
|
||||
assert not mytemp.joinpath("hello").exists()
|
||||
|
||||
def test_policy_failed_removes_only_passed_dir(self, pytester: Pytester) -> None:
|
||||
p = pytester.makepyfile(
|
||||
"""
|
||||
def test_1(tmp_path):
|
||||
assert 0 == 0
|
||||
def test_2(tmp_path):
|
||||
assert 0 == 1
|
||||
"""
|
||||
)
|
||||
|
||||
pytester.inline_run(p)
|
||||
root = pytester._test_tmproot
|
||||
|
||||
for child in root.iterdir():
|
||||
base_dir = list(
|
||||
filter(lambda x: x.is_dir() and not x.is_symlink(), child.iterdir())
|
||||
)
|
||||
assert len(base_dir) == 1
|
||||
test_dir = list(
|
||||
filter(
|
||||
lambda x: x.is_dir() and not x.is_symlink(), base_dir[0].iterdir()
|
||||
)
|
||||
)
|
||||
# Check only the failed one remains
|
||||
assert len(test_dir) == 1
|
||||
assert test_dir[0].name == "test_20"
|
||||
|
||||
def test_policy_failed_removes_basedir_when_all_passed(
|
||||
self, pytester: Pytester
|
||||
) -> None:
|
||||
p = pytester.makepyfile(
|
||||
"""
|
||||
def test_1(tmp_path):
|
||||
assert 0 == 0
|
||||
"""
|
||||
)
|
||||
|
||||
pytester.inline_run(p)
|
||||
root = pytester._test_tmproot
|
||||
for child in root.iterdir():
|
||||
# This symlink will be deleted by cleanup_numbered_dir **after**
|
||||
# the test finishes because it's triggered by atexit.
|
||||
# So it has to be ignored here.
|
||||
base_dir = filter(lambda x: not x.is_symlink(), child.iterdir())
|
||||
# Check the base dir itself is gone
|
||||
assert len(list(base_dir)) == 0
|
||||
|
||||
# issue #10502
|
||||
def test_policy_failed_removes_dir_when_skipped_from_fixture(
|
||||
self, pytester: Pytester
|
||||
) -> None:
|
||||
p = pytester.makepyfile(
|
||||
"""
|
||||
import pytest
|
||||
|
||||
@pytest.fixture
|
||||
def fixt(tmp_path):
|
||||
pytest.skip()
|
||||
|
||||
def test_fixt(fixt):
|
||||
pass
|
||||
"""
|
||||
)
|
||||
pytester.inline_run(p)
|
||||
|
||||
# Check if the whole directory is removed
|
||||
root = pytester._test_tmproot
|
||||
for child in root.iterdir():
|
||||
base_dir = list(
|
||||
filter(lambda x: x.is_dir() and not x.is_symlink(), child.iterdir())
|
||||
)
|
||||
assert len(base_dir) == 0
|
||||
|
||||
# issue #10502
|
||||
def test_policy_all_keeps_dir_when_skipped_from_fixture(
|
||||
self, pytester: Pytester
|
||||
) -> None:
|
||||
p = pytester.makepyfile(
|
||||
"""
|
||||
import pytest
|
||||
|
||||
@pytest.fixture
|
||||
def fixt(tmp_path):
|
||||
pytest.skip()
|
||||
|
||||
def test_fixt(fixt):
|
||||
pass
|
||||
"""
|
||||
)
|
||||
pytester.makepyprojecttoml(
|
||||
"""
|
||||
[tool.pytest.ini_options]
|
||||
tmp_path_retention_policy = "all"
|
||||
"""
|
||||
)
|
||||
pytester.inline_run(p)
|
||||
|
||||
# Check if the whole directory is kept
|
||||
root = pytester._test_tmproot
|
||||
for child in root.iterdir():
|
||||
base_dir = list(
|
||||
filter(lambda x: x.is_dir() and not x.is_symlink(), child.iterdir())
|
||||
)
|
||||
assert len(base_dir) == 1
|
||||
test_dir = list(
|
||||
filter(
|
||||
lambda x: x.is_dir() and not x.is_symlink(), base_dir[0].iterdir()
|
||||
)
|
||||
)
|
||||
assert len(test_dir) == 1
|
||||
|
||||
|
||||
testdata = [
|
||||
("mypath", True),
|
||||
@@ -275,12 +394,12 @@ class TestNumberedDir:
|
||||
|
||||
assert not lock.exists()
|
||||
|
||||
def _do_cleanup(self, tmp_path: Path) -> None:
|
||||
def _do_cleanup(self, tmp_path: Path, keep: int = 2) -> None:
|
||||
self.test_make(tmp_path)
|
||||
cleanup_numbered_dir(
|
||||
root=tmp_path,
|
||||
prefix=self.PREFIX,
|
||||
keep=2,
|
||||
keep=keep,
|
||||
consider_lock_dead_if_created_before=0,
|
||||
)
|
||||
|
||||
@@ -289,6 +408,11 @@ class TestNumberedDir:
|
||||
a, b = (x for x in tmp_path.iterdir() if not x.is_symlink())
|
||||
print(a, b)
|
||||
|
||||
def test_cleanup_keep_0(self, tmp_path: Path):
|
||||
self._do_cleanup(tmp_path, 0)
|
||||
dir_num = len(list(tmp_path.iterdir()))
|
||||
assert dir_num == 0
|
||||
|
||||
def test_cleanup_locked(self, tmp_path):
|
||||
p = make_numbered_dir(root=tmp_path, prefix=self.PREFIX)
|
||||
|
||||
@@ -446,7 +570,7 @@ def test_tmp_path_factory_create_directory_with_safe_permissions(
|
||||
"""Verify that pytest creates directories under /tmp with private permissions."""
|
||||
# Use the test's tmp_path as the system temproot (/tmp).
|
||||
monkeypatch.setenv("PYTEST_DEBUG_TEMPROOT", str(tmp_path))
|
||||
tmp_factory = TempPathFactory(None, lambda *args: None, _ispytest=True)
|
||||
tmp_factory = TempPathFactory(None, 3, "failed", lambda *args: None, _ispytest=True)
|
||||
basetemp = tmp_factory.getbasetemp()
|
||||
|
||||
# No world-readable permissions.
|
||||
@@ -466,14 +590,14 @@ def test_tmp_path_factory_fixes_up_world_readable_permissions(
|
||||
"""
|
||||
# Use the test's tmp_path as the system temproot (/tmp).
|
||||
monkeypatch.setenv("PYTEST_DEBUG_TEMPROOT", str(tmp_path))
|
||||
tmp_factory = TempPathFactory(None, lambda *args: None, _ispytest=True)
|
||||
tmp_factory = TempPathFactory(None, 3, "failed", lambda *args: None, _ispytest=True)
|
||||
basetemp = tmp_factory.getbasetemp()
|
||||
|
||||
# Before - simulate bad perms.
|
||||
os.chmod(basetemp.parent, 0o777)
|
||||
assert (basetemp.parent.stat().st_mode & 0o077) != 0
|
||||
|
||||
tmp_factory = TempPathFactory(None, lambda *args: None, _ispytest=True)
|
||||
tmp_factory = TempPathFactory(None, 3, "failed", lambda *args: None, _ispytest=True)
|
||||
basetemp = tmp_factory.getbasetemp()
|
||||
|
||||
# After - fixed.
|
||||
|
||||
@@ -3,11 +3,6 @@
|
||||
This file is not executed, it is only checked by mypy to ensure that
|
||||
none of the code triggers any mypy errors.
|
||||
"""
|
||||
import contextlib
|
||||
from typing import Optional
|
||||
|
||||
from typing_extensions import assert_type
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@@ -27,9 +22,3 @@ def check_fixture_ids_callable() -> None:
|
||||
@pytest.mark.parametrize("func", [str, int], ids=lambda x: str(x.__name__))
|
||||
def check_parametrize_ids_callable(func) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def check_raises_is_a_context_manager(val: bool) -> None:
|
||||
with pytest.raises(RuntimeError) if val else contextlib.nullcontext() as excinfo:
|
||||
pass
|
||||
assert_type(excinfo, Optional[pytest.ExceptionInfo[RuntimeError]])
|
||||
|
||||
Reference in New Issue
Block a user