Compare commits
132 Commits
Author | SHA1 | Date |
---|---|---|
|
f8fd5ec8dd | |
|
da7ca9e732 | |
|
90aaeebc8e | |
|
be26da84f4 | |
|
2262734edf | |
|
5644437c1f | |
|
1c465bd32f | |
|
049f5b513a | |
|
d5843f89d3 | |
|
180f93158e | |
|
f1d7aa60b1 | |
|
ded772b288 | |
|
3d470555e8 | |
|
2a5ca51fe8 | |
|
a6029ff2b7 | |
|
020831d868 | |
|
c5831ac98f | |
|
f606fef19d | |
|
24898e0640 | |
|
b39b867967 | |
|
f6a5578d5c | |
|
3f94cc9e35 | |
|
897f1a3ef4 | |
|
035f51ab71 | |
|
621028c58d | |
|
d622f12f69 | |
|
e49282f72c | |
|
197c996345 | |
|
2d398d8706 | |
|
9ab4032f74 | |
|
53b08730e4 | |
|
1deb60f02f | |
|
fb8395d93f | |
|
b08c599bad | |
|
51fd451dc9 | |
|
1d021540a3 | |
|
8bfe434f75 | |
|
f9ebe3c607 | |
|
bd54116d03 | |
|
8b9482e39c | |
|
943f4ac236 | |
|
6f43eee106 | |
|
e1f3c0f9c3 | |
|
192f6992d2 | |
|
6465244269 | |
|
097acaf11b | |
|
3d8649b206 | |
|
a8c16d9b75 | |
|
3edf417969 | |
|
0084fd9783 | |
|
e89efa8325 | |
|
3edcc71c41 | |
|
866daf57fe | |
|
5b499bafb2 | |
|
62c0d82d64 | |
|
d526053af3 | |
|
2c7614a0e1 | |
|
b9a8465ce4 | |
|
1cc974c95d | |
|
c03e46f1ad | |
|
f2d87dcf6c | |
|
914441557c | |
|
8aba863a63 | |
|
aa79b1c00c | |
|
117f52dcf3 | |
|
9191857b5f | |
|
7718d8c972 | |
|
7a96f3f970 | |
|
2fbea0e5e4 | |
|
4910036b76 | |
|
0b039b14aa | |
|
7807c263bc | |
|
b71f873189 | |
|
a19ae2af22 | |
|
0274c08b8a | |
|
829941a061 | |
|
2e345fd277 | |
|
400393cfe4 | |
|
459c5f4e49 | |
|
f06ae5297b | |
|
30de66944d | |
|
02c737fe4e | |
|
01655b114e | |
|
a92ac0d4f6 | |
|
802c77ad2f | |
|
acb62ba619 | |
|
df0cff18ac | |
|
46a0888352 | |
|
34b4e21606 | |
|
a886015bfd | |
|
09dee292ca | |
|
2301fa61de | |
|
d3549df5b9 | |
|
b85d98edbb | |
|
f4b1c1184f | |
|
86a4eb6008 | |
|
013d0e66c7 | |
|
554bff8cc1 | |
|
d2f74d342e | |
|
430de12f35 | |
|
d5eed3bb9c | |
|
4b104ba222 | |
|
c765b83a2a | |
|
443af11861 | |
|
4e02248b84 | |
|
43a499e6fa | |
|
e2fa2b621c | |
|
0fc11b6f3c | |
|
d2c1a04532 | |
|
b8e65d03bf | |
|
f37ea715d8 | |
|
45d36ddb47 | |
|
355954df5d | |
|
a93c50ccb9 | |
|
1cae76b0fe | |
|
1b7597ac91 | |
|
21680ffa77 | |
|
8076f48eae | |
|
0ae27714d1 | |
|
92432ac45c | |
|
937f945946 | |
|
829a5986e8 | |
|
54dbfb5167 | |
|
70f0b77c72 | |
|
2a8b463b38 | |
|
12bf458719 | |
|
114dba56f8 | |
|
abb853f482 | |
|
8208a376cc | |
|
f078984c2e | |
|
dba62f8a46 | |
|
f7bf914108 |
|
@ -0,0 +1,191 @@
|
||||||
|
name: main
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 4.6.x
|
||||||
|
tags:
|
||||||
|
- "*"
|
||||||
|
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- 4.6.x
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
name: [
|
||||||
|
"windows-py27",
|
||||||
|
"windows-py35",
|
||||||
|
"windows-py36",
|
||||||
|
"windows-py37",
|
||||||
|
"windows-py37-pluggy",
|
||||||
|
"windows-py38",
|
||||||
|
|
||||||
|
"ubuntu-py27-pluggy",
|
||||||
|
"ubuntu-py27-nobyte",
|
||||||
|
"ubuntu-py37",
|
||||||
|
"ubuntu-py37-pluggy",
|
||||||
|
"ubuntu-py37-pexpect-py37-twisted",
|
||||||
|
"ubuntu-py37-freeze",
|
||||||
|
"ubuntu-pypy",
|
||||||
|
"ubuntu-pypy3",
|
||||||
|
|
||||||
|
"macos-py27",
|
||||||
|
"macos-py38",
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
include:
|
||||||
|
# Windows jobs
|
||||||
|
- name: "windows-py27"
|
||||||
|
python: "2.7"
|
||||||
|
os: windows-latest
|
||||||
|
tox_env: "py27-xdist"
|
||||||
|
use_coverage: true
|
||||||
|
- name: "windows-py35"
|
||||||
|
python: "3.5"
|
||||||
|
os: windows-latest
|
||||||
|
tox_env: "py35-xdist"
|
||||||
|
use_coverage: true
|
||||||
|
- name: "windows-py36"
|
||||||
|
python: "3.6"
|
||||||
|
os: windows-latest
|
||||||
|
tox_env: "py36-xdist"
|
||||||
|
use_coverage: true
|
||||||
|
- name: "windows-py37"
|
||||||
|
python: "3.7"
|
||||||
|
os: windows-latest
|
||||||
|
tox_env: "py37-twisted-numpy"
|
||||||
|
use_coverage: true
|
||||||
|
- name: "windows-py37-pluggy"
|
||||||
|
python: "3.7"
|
||||||
|
os: windows-latest
|
||||||
|
tox_env: "py37-pluggymaster-xdist"
|
||||||
|
use_coverage: true
|
||||||
|
- name: "windows-py38"
|
||||||
|
python: "3.8"
|
||||||
|
os: windows-latest
|
||||||
|
tox_env: "py38-xdist"
|
||||||
|
use_coverage: true
|
||||||
|
|
||||||
|
# Ubuntu jobs – find the rest of them in .travis.yml
|
||||||
|
- name: "ubuntu-py27-pluggy"
|
||||||
|
python: "2.7"
|
||||||
|
os: ubuntu-latest
|
||||||
|
tox_env: "py27-pluggymaster-xdist"
|
||||||
|
use_coverage: true
|
||||||
|
- name: "ubuntu-py27-nobyte"
|
||||||
|
python: "2.7"
|
||||||
|
os: ubuntu-latest
|
||||||
|
tox_env: "py27-nobyte-numpy-xdist"
|
||||||
|
use_coverage: true
|
||||||
|
- name: "ubuntu-py37"
|
||||||
|
python: "3.7"
|
||||||
|
os: ubuntu-latest
|
||||||
|
tox_env: "py37-lsof-numpy-xdist"
|
||||||
|
use_coverage: true
|
||||||
|
- name: "ubuntu-py37-pluggy"
|
||||||
|
python: "3.7"
|
||||||
|
os: ubuntu-latest
|
||||||
|
tox_env: "py37-pluggymaster-xdist"
|
||||||
|
use_coverage: true
|
||||||
|
- name: "ubuntu-py37-pexpect-py37-twisted"
|
||||||
|
python: "3.7"
|
||||||
|
os: ubuntu-latest
|
||||||
|
tox_env: "py37-pexpect,py37-twisted"
|
||||||
|
use_coverage: true
|
||||||
|
- name: "ubuntu-py37-freeze"
|
||||||
|
python: "3.7"
|
||||||
|
os: ubuntu-latest
|
||||||
|
tox_env: "py37-freeze"
|
||||||
|
- name: "ubuntu-pypy"
|
||||||
|
python: "pypy2"
|
||||||
|
os: ubuntu-latest
|
||||||
|
tox_env: "pypy-xdist"
|
||||||
|
use_coverage: true
|
||||||
|
- name: "ubuntu-pypy3"
|
||||||
|
python: "pypy3"
|
||||||
|
os: ubuntu-latest
|
||||||
|
tox_env: "pypy3-xdist"
|
||||||
|
use_coverage: true
|
||||||
|
|
||||||
|
# MacOS jobs
|
||||||
|
- name: "macos-py27"
|
||||||
|
python: "2.7"
|
||||||
|
os: macos-latest
|
||||||
|
tox_env: "py27-xdist"
|
||||||
|
use_coverage: true
|
||||||
|
- name: "macos-py38"
|
||||||
|
python: "3.8"
|
||||||
|
os: macos-latest
|
||||||
|
tox_env: "py38-xdist"
|
||||||
|
use_coverage: true
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- name: Set up Python ${{ matrix.python }} on ${{ matrix.os }}
|
||||||
|
uses: actions/setup-python@v1
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python }}
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install tox coverage
|
||||||
|
- name: Test without coverage
|
||||||
|
if: "! matrix.use_coverage"
|
||||||
|
run: "tox -e ${{ matrix.tox_env }}"
|
||||||
|
|
||||||
|
- name: Test with coverage
|
||||||
|
if: "matrix.use_coverage"
|
||||||
|
env:
|
||||||
|
_PYTEST_TOX_COVERAGE_RUN: "coverage run -m"
|
||||||
|
COVERAGE_PROCESS_START: ".coveragerc"
|
||||||
|
_PYTEST_TOX_EXTRA_DEP: "coverage-enable-subprocess"
|
||||||
|
run: "tox -vv -e ${{ matrix.tox_env }}"
|
||||||
|
|
||||||
|
- name: Prepare coverage token
|
||||||
|
if: (matrix.use_coverage && ( github.repository == 'pytest-dev/pytest' || github.event_name == 'pull_request' ))
|
||||||
|
run: |
|
||||||
|
python scripts/append_codecov_token.py
|
||||||
|
- name: Report coverage
|
||||||
|
if: (matrix.use_coverage)
|
||||||
|
env:
|
||||||
|
CODECOV_NAME: ${{ matrix.name }}
|
||||||
|
run: bash scripts/report-coverage.sh -F GHA,${{ runner.os }}
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') && github.repository == 'pytest-dev/pytest'
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
needs: [build]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v1
|
||||||
|
with:
|
||||||
|
python-version: "3.7"
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install --upgrade wheel setuptools tox
|
||||||
|
- name: Build package
|
||||||
|
run: |
|
||||||
|
python setup.py sdist bdist_wheel
|
||||||
|
- name: Publish package to PyPI
|
||||||
|
uses: pypa/gh-action-pypi-publish@master
|
||||||
|
with:
|
||||||
|
user: __token__
|
||||||
|
password: ${{ secrets.pypi_token }}
|
||||||
|
- name: Publish GitHub release notes
|
||||||
|
env:
|
||||||
|
GH_RELEASE_NOTES_TOKEN: ${{ secrets.release_notes }}
|
||||||
|
run: |
|
||||||
|
sudo apt-get install pandoc
|
||||||
|
tox -e publish-gh-release-notes
|
|
@ -1,7 +1,7 @@
|
||||||
exclude: doc/en/example/py2py3/test_py2.py
|
exclude: doc/en/example/py2py3/test_py2.py
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/python/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 19.3b0
|
rev: 19.10b0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
args: [--safe, --quiet]
|
args: [--safe, --quiet]
|
||||||
|
|
157
.travis.yml
157
.travis.yml
|
@ -1,121 +1,60 @@
|
||||||
language: python
|
language: python
|
||||||
dist: xenial
|
dist: xenial
|
||||||
stages:
|
python: '3.7.4'
|
||||||
- baseline
|
|
||||||
- name: test
|
|
||||||
if: repo = pytest-dev/pytest AND tag IS NOT present
|
|
||||||
- name: deploy
|
|
||||||
if: repo = pytest-dev/pytest AND tag IS present
|
|
||||||
python: '3.7'
|
|
||||||
cache: false
|
cache: false
|
||||||
|
|
||||||
env:
|
env:
|
||||||
global:
|
global:
|
||||||
- PYTEST_ADDOPTS=-vv
|
- PYTEST_ADDOPTS="-vv --showlocals --durations=100 --exitfirst"
|
||||||
|
- PYTEST_COVERAGE=1
|
||||||
|
|
||||||
|
# setuptools-scm needs all tags in order to obtain a proper version
|
||||||
|
git:
|
||||||
|
depth: false
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- python -m pip install --upgrade --pre tox
|
- python -m pip install --upgrade --pre tox
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
include:
|
include:
|
||||||
# OSX tests - first (in test stage), since they are the slower ones.
|
|
||||||
- &test-macos
|
|
||||||
os: osx
|
|
||||||
osx_image: xcode10.1
|
|
||||||
language: generic
|
|
||||||
# Coverage for:
|
# Coverage for:
|
||||||
# - py2 with symlink in test_cmdline_python_package_symlink.
|
|
||||||
env: TOXENV=py27-xdist PYTEST_COVERAGE=1
|
|
||||||
before_install:
|
|
||||||
- python -V
|
|
||||||
- test $(python -c 'import sys; print("%d%d" % sys.version_info[0:2])') = 27
|
|
||||||
- <<: *test-macos
|
|
||||||
env: TOXENV=py37-pexpect,py37-xdist PYTEST_COVERAGE=1
|
|
||||||
before_install:
|
|
||||||
- which python3
|
|
||||||
- python3 -V
|
|
||||||
- ln -sfn "$(which python3)" /usr/local/bin/python
|
|
||||||
- python -V
|
|
||||||
- test $(python -c 'import sys; print("%d%d" % sys.version_info[0:2])') = 37
|
|
||||||
|
|
||||||
# Full run of latest (major) supported versions, without xdist.
|
|
||||||
- env: TOXENV=py27
|
|
||||||
python: '2.7'
|
|
||||||
- env: TOXENV=py37
|
|
||||||
python: '3.7'
|
|
||||||
|
|
||||||
# Coverage tracking is slow with pypy, skip it.
|
|
||||||
- env: TOXENV=pypy-xdist
|
|
||||||
python: 'pypy'
|
|
||||||
- env: TOXENV=pypy3-xdist
|
|
||||||
python: 'pypy3'
|
|
||||||
|
|
||||||
- env: TOXENV=py34-xdist
|
|
||||||
python: '3.4'
|
|
||||||
- env: TOXENV=py35-xdist
|
|
||||||
python: '3.5'
|
|
||||||
|
|
||||||
# Coverage for:
|
|
||||||
# - pytester's LsofFdLeakChecker
|
|
||||||
# - TestArgComplete (linux only)
|
# - TestArgComplete (linux only)
|
||||||
# - numpy
|
# - numpy
|
||||||
# Empty PYTEST_ADDOPTS to run this non-verbose.
|
# - verbose=0
|
||||||
- env: TOXENV=py37-lsof-numpy-xdist PYTEST_COVERAGE=1 PYTEST_ADDOPTS=
|
|
||||||
|
|
||||||
# Specialized factors for py27.
|
|
||||||
- env: TOXENV=py27-nobyte-numpy-xdist
|
|
||||||
python: '2.7'
|
|
||||||
- env: TOXENV=py27-pluggymaster-xdist
|
|
||||||
python: '2.7'
|
|
||||||
|
|
||||||
# Specialized factors for py37.
|
|
||||||
# Coverage for:
|
|
||||||
# - test_sys_breakpoint_interception (via pexpect).
|
|
||||||
- env: TOXENV=py37-pexpect,py37-twisted PYTEST_COVERAGE=1
|
|
||||||
- env: TOXENV=py37-pluggymaster-xdist
|
|
||||||
- env: TOXENV=py37-freeze
|
|
||||||
|
|
||||||
# Jobs only run via Travis cron jobs (currently daily).
|
|
||||||
- env: TOXENV=py38-xdist
|
|
||||||
python: '3.8-dev'
|
|
||||||
if: type = cron
|
|
||||||
|
|
||||||
- stage: baseline
|
- stage: baseline
|
||||||
# Coverage for:
|
env: TOXENV=py27-xdist
|
||||||
# - _pytest.unittest._handle_skip (via pexpect).
|
|
||||||
env: TOXENV=py27-pexpect,py27-twisted PYTEST_COVERAGE=1
|
|
||||||
python: '2.7'
|
python: '2.7'
|
||||||
# Use py36 here for faster baseline.
|
|
||||||
- env: TOXENV=py36-xdist
|
- env: TOXENV=py38-xdist
|
||||||
python: '3.6'
|
python: '3.8'
|
||||||
- env: TOXENV=linting,docs,doctesting PYTEST_COVERAGE=1
|
|
||||||
|
- stage: tests
|
||||||
|
# - _pytest.unittest._handle_skip (via pexpect).
|
||||||
|
env: TOXENV=py27-pexpect,py27-twisted
|
||||||
|
python: '2.7'
|
||||||
|
|
||||||
|
- env: TOXENV=py35-xdist
|
||||||
|
python: '3.5.9'
|
||||||
|
|
||||||
|
- env: TOXENV=py36-xdist PYTEST_REORDER_TESTS=0
|
||||||
|
python: '3.6.9'
|
||||||
|
|
||||||
|
- env: TOXENV=py37-numpy-pexpect-twisted
|
||||||
|
python: '3.7.4'
|
||||||
|
|
||||||
|
# - test_sys_breakpoint_interception (via pexpect).
|
||||||
|
- env: TOXENV=py37-pexpect,py37-twisted
|
||||||
|
python: '3.7.4'
|
||||||
|
|
||||||
|
# Run also non-verbosely, to gain coverage
|
||||||
|
- env: TOXENV=py38-xdist PYTEST_ADDOPTS=""
|
||||||
|
python: '3.8'
|
||||||
|
|
||||||
|
- env: TOXENV=linting,docs,doctesting
|
||||||
cache:
|
cache:
|
||||||
directories:
|
directories:
|
||||||
- $HOME/.cache/pre-commit
|
- $HOME/.cache/pre-commit
|
||||||
|
|
||||||
- stage: deploy
|
|
||||||
python: '3.6'
|
|
||||||
install: pip install -U setuptools setuptools_scm
|
|
||||||
script: skip
|
|
||||||
deploy:
|
|
||||||
provider: pypi
|
|
||||||
user: nicoddemus
|
|
||||||
distributions: sdist bdist_wheel
|
|
||||||
skip_upload_docs: true
|
|
||||||
password:
|
|
||||||
secure: xanTgTUu6XDQVqB/0bwJQXoDMnU5tkwZc5koz6mBkkqZhKdNOi2CLoC1XhiSZ+ah24l4V1E0GAqY5kBBcy9d7NVe4WNg4tD095LsHw+CRU6/HCVIFfyk2IZ+FPAlguesCcUiJSXOrlBF+Wj68wEvLoK7EoRFbJeiZ/f91Ww1sbtDlqXABWGHrmhPJL5Wva7o7+wG7JwJowqdZg1pbQExsCc7b53w4v2RBu3D6TJaTAzHiVsW+nUSI67vKI/uf+cR/OixsTfy37wlHgSwihYmrYLFls3V0bSpahCim3bCgMaFZx8S8xrdgJ++PzBCof2HeflFKvW+VCkoYzGEG4NrTWJoNz6ni4red9GdvfjGH3YCjAKS56h9x58zp2E5rpsb/kVq5/45xzV+dq6JRuhQ1nJWjBC6fSKAc/bfwnuFK3EBxNLkvBssLHvsNjj5XG++cB8DdS9wVGUqjpoK4puaXUWFqy4q3S9F86HEsKNgExtieA9qNx+pCIZVs6JCXZNjr0I5eVNzqJIyggNgJG6RyravsU35t9Zd9doL5g4Y7UKmAGTn1Sz24HQ4sMQgXdm2SyD8gEK5je4tlhUvfGtDvMSlstq71kIn9nRpFnqB6MFlbYSEAZmo8dGbCquoUc++6Rum208wcVbrzzVtGlXB/Ow9AbFMYeAGA0+N/K1e59c=
|
|
||||||
on:
|
|
||||||
tags: true
|
|
||||||
repo: pytest-dev/pytest
|
|
||||||
|
|
||||||
matrix:
|
|
||||||
allow_failures:
|
|
||||||
- python: '3.8-dev'
|
|
||||||
env: TOXENV=py38-xdist
|
|
||||||
# Temporary (https://github.com/pytest-dev/pytest/pull/5334).
|
|
||||||
- env: TOXENV=pypy3-xdist
|
|
||||||
python: 'pypy3'
|
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
- |
|
- |
|
||||||
# Do not (re-)upload coverage with cron runs.
|
# Do not (re-)upload coverage with cron runs.
|
||||||
|
@ -129,27 +68,13 @@ before_script:
|
||||||
export _PYTEST_TOX_COVERAGE_RUN="coverage run -m"
|
export _PYTEST_TOX_COVERAGE_RUN="coverage run -m"
|
||||||
export _PYTEST_TOX_EXTRA_DEP=coverage-enable-subprocess
|
export _PYTEST_TOX_EXTRA_DEP=coverage-enable-subprocess
|
||||||
fi
|
fi
|
||||||
|
script: env COLUMNS=120 python -m tox
|
||||||
script: tox --recreate
|
|
||||||
|
|
||||||
after_success:
|
after_success:
|
||||||
- |
|
- |
|
||||||
if [[ "$PYTEST_COVERAGE" = 1 ]]; then
|
if [[ "$PYTEST_COVERAGE" = 1 ]]; then
|
||||||
set -e
|
env CODECOV_NAME="$TOXENV-$TRAVIS_OS_NAME" scripts/report-coverage.sh
|
||||||
# Add last TOXENV to $PATH.
|
|
||||||
PATH="$PWD/.tox/${TOXENV##*,}/bin:$PATH"
|
|
||||||
coverage combine
|
|
||||||
coverage xml
|
|
||||||
coverage report -m
|
|
||||||
bash <(curl -s https://codecov.io/bash) -Z -X gcov -X coveragepy -X search -X xcode -X gcovout -X fix -f coverage.xml -n $TOXENV-$TRAVIS_OS_NAME
|
|
||||||
fi
|
fi
|
||||||
|
branches:
|
||||||
notifications:
|
only:
|
||||||
irc:
|
- 4.6.x
|
||||||
channels:
|
|
||||||
- "chat.freenode.net#pytest"
|
|
||||||
on_success: change
|
|
||||||
on_failure: change
|
|
||||||
skip_join: true
|
|
||||||
email:
|
|
||||||
- pytest-commit@python.org
|
|
||||||
|
|
4
AUTHORS
4
AUTHORS
|
@ -58,6 +58,7 @@ Christian Theunert
|
||||||
Christian Tismer
|
Christian Tismer
|
||||||
Christopher Gilling
|
Christopher Gilling
|
||||||
Christopher Dignam
|
Christopher Dignam
|
||||||
|
Claudio Madotto
|
||||||
CrazyMerlyn
|
CrazyMerlyn
|
||||||
Cyrus Maden
|
Cyrus Maden
|
||||||
Damian Skrzypczak
|
Damian Skrzypczak
|
||||||
|
@ -91,6 +92,7 @@ Evan Kepner
|
||||||
Fabien Zarifian
|
Fabien Zarifian
|
||||||
Fabio Zadrozny
|
Fabio Zadrozny
|
||||||
Feng Ma
|
Feng Ma
|
||||||
|
Fernando Mezzabotta Rey
|
||||||
Florian Bruhin
|
Florian Bruhin
|
||||||
Floris Bruynooghe
|
Floris Bruynooghe
|
||||||
Gabriel Reis
|
Gabriel Reis
|
||||||
|
@ -112,6 +114,7 @@ Ilya Konstantinov
|
||||||
Ionuț Turturică
|
Ionuț Turturică
|
||||||
Iwan Briquemont
|
Iwan Briquemont
|
||||||
Jaap Broekhuizen
|
Jaap Broekhuizen
|
||||||
|
James Frost
|
||||||
Jan Balster
|
Jan Balster
|
||||||
Janne Vanhala
|
Janne Vanhala
|
||||||
Jason R. Coombs
|
Jason R. Coombs
|
||||||
|
@ -135,6 +138,7 @@ Kale Kundert
|
||||||
Katarzyna Jachim
|
Katarzyna Jachim
|
||||||
Katerina Koukiou
|
Katerina Koukiou
|
||||||
Kevin Cox
|
Kevin Cox
|
||||||
|
Kevin J. Foley
|
||||||
Kodi B. Arfer
|
Kodi B. Arfer
|
||||||
Kostis Anagnostopoulos
|
Kostis Anagnostopoulos
|
||||||
Kristoffer Nordström
|
Kristoffer Nordström
|
||||||
|
|
195
CHANGELOG.rst
195
CHANGELOG.rst
|
@ -18,6 +18,201 @@ with advance notice in the **Deprecations** section of releases.
|
||||||
|
|
||||||
.. towncrier release notes start
|
.. towncrier release notes start
|
||||||
|
|
||||||
|
pytest 4.6.11 (2020-06-04)
|
||||||
|
==========================
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- `#6334 <https://github.com/pytest-dev/pytest/issues/6334>`_: Fix summary entries appearing twice when ``f/F`` and ``s/S`` report chars were used at the same time in the ``-r`` command-line option (for example ``-rFf``).
|
||||||
|
|
||||||
|
The upper case variants were never documented and the preferred form should be the lower case.
|
||||||
|
|
||||||
|
|
||||||
|
- `#7310 <https://github.com/pytest-dev/pytest/issues/7310>`_: Fix ``UnboundLocalError: local variable 'letter' referenced before
|
||||||
|
assignment`` in ``_pytest.terminal.pytest_report_teststatus()``
|
||||||
|
when plugins return report objects in an unconventional state.
|
||||||
|
|
||||||
|
This was making ``pytest_report_teststatus()`` skip
|
||||||
|
entering if-block branches that declare the ``letter`` variable.
|
||||||
|
|
||||||
|
The fix was to set the initial value of the ``letter`` before
|
||||||
|
the if-block cascade so that it always has a value.
|
||||||
|
|
||||||
|
|
||||||
|
pytest 4.6.10 (2020-05-08)
|
||||||
|
==========================
|
||||||
|
|
||||||
|
Features
|
||||||
|
--------
|
||||||
|
|
||||||
|
- `#6870 <https://github.com/pytest-dev/pytest/issues/6870>`_: New ``Config.invocation_args`` attribute containing the unchanged arguments passed to ``pytest.main()``.
|
||||||
|
|
||||||
|
Remark: while this is technically a new feature and according to our `policy <https://docs.pytest.org/en/latest/py27-py34-deprecation.html#what-goes-into-4-6-x-releases>`_ it should not have been backported, we have opened an exception in this particular case because it fixes a serious interaction with ``pytest-xdist``, so it can also be considered a bugfix.
|
||||||
|
|
||||||
|
Trivial/Internal Changes
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
- `#6404 <https://github.com/pytest-dev/pytest/issues/6404>`_: Remove usage of ``parser`` module, deprecated in Python 3.9.
|
||||||
|
|
||||||
|
|
||||||
|
pytest 4.6.9 (2020-01-04)
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- `#6301 <https://github.com/pytest-dev/pytest/issues/6301>`_: Fix assertion rewriting for egg-based distributions and ``editable`` installs (``pip install --editable``).
|
||||||
|
|
||||||
|
|
||||||
|
pytest 4.6.8 (2019-12-19)
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Features
|
||||||
|
--------
|
||||||
|
|
||||||
|
- `#5471 <https://github.com/pytest-dev/pytest/issues/5471>`_: JUnit XML now includes a timestamp and hostname in the testsuite tag.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- `#5430 <https://github.com/pytest-dev/pytest/issues/5430>`_: junitxml: Logs for failed test are now passed to junit report in case the test fails during call phase.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Trivial/Internal Changes
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
- `#6345 <https://github.com/pytest-dev/pytest/issues/6345>`_: Pin ``colorama`` to ``0.4.1`` only for Python 3.4 so newer Python versions can still receive colorama updates.
|
||||||
|
|
||||||
|
|
||||||
|
pytest 4.6.7 (2019-12-05)
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- `#5477 <https://github.com/pytest-dev/pytest/issues/5477>`_: The XML file produced by ``--junitxml`` now correctly contain a ``<testsuites>`` root element.
|
||||||
|
|
||||||
|
|
||||||
|
- `#6044 <https://github.com/pytest-dev/pytest/issues/6044>`_: Properly ignore ``FileNotFoundError`` (``OSError.errno == NOENT`` in Python 2) exceptions when trying to remove old temporary directories,
|
||||||
|
for instance when multiple processes try to remove the same directory (common with ``pytest-xdist``
|
||||||
|
for example).
|
||||||
|
|
||||||
|
|
||||||
|
pytest 4.6.6 (2019-10-11)
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- `#5523 <https://github.com/pytest-dev/pytest/issues/5523>`_: Fixed using multiple short options together in the command-line (for example ``-vs``) in Python 3.8+.
|
||||||
|
|
||||||
|
|
||||||
|
- `#5537 <https://github.com/pytest-dev/pytest/issues/5537>`_: Replace ``importlib_metadata`` backport with ``importlib.metadata`` from the
|
||||||
|
standard library on Python 3.8+.
|
||||||
|
|
||||||
|
|
||||||
|
- `#5806 <https://github.com/pytest-dev/pytest/issues/5806>`_: Fix "lexer" being used when uploading to bpaste.net from ``--pastebin`` to "text".
|
||||||
|
|
||||||
|
|
||||||
|
- `#5902 <https://github.com/pytest-dev/pytest/issues/5902>`_: Fix warnings about deprecated ``cmp`` attribute in ``attrs>=19.2``.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Trivial/Internal Changes
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
- `#5801 <https://github.com/pytest-dev/pytest/issues/5801>`_: Fixes python version checks (detected by ``flake8-2020``) in case python4 becomes a thing.
|
||||||
|
|
||||||
|
|
||||||
|
pytest 4.6.5 (2019-08-05)
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- `#4344 <https://github.com/pytest-dev/pytest/issues/4344>`_: Fix RuntimeError/StopIteration when trying to collect package with "__init__.py" only.
|
||||||
|
|
||||||
|
|
||||||
|
- `#5478 <https://github.com/pytest-dev/pytest/issues/5478>`_: Fix encode error when using unicode strings in exceptions with ``pytest.raises``.
|
||||||
|
|
||||||
|
|
||||||
|
- `#5524 <https://github.com/pytest-dev/pytest/issues/5524>`_: Fix issue where ``tmp_path`` and ``tmpdir`` would not remove directories containing files marked as read-only,
|
||||||
|
which could lead to pytest crashing when executed a second time with the ``--basetemp`` option.
|
||||||
|
|
||||||
|
|
||||||
|
- `#5547 <https://github.com/pytest-dev/pytest/issues/5547>`_: ``--step-wise`` now handles ``xfail(strict=True)`` markers properly.
|
||||||
|
|
||||||
|
|
||||||
|
- `#5650 <https://github.com/pytest-dev/pytest/issues/5650>`_: Improved output when parsing an ini configuration file fails.
|
||||||
|
|
||||||
|
|
||||||
|
pytest 4.6.4 (2019-06-28)
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- `#5404 <https://github.com/pytest-dev/pytest/issues/5404>`_: Emit a warning when attempting to unwrap a broken object raises an exception,
|
||||||
|
for easier debugging (`#5080 <https://github.com/pytest-dev/pytest/issues/5080>`__).
|
||||||
|
|
||||||
|
|
||||||
|
- `#5444 <https://github.com/pytest-dev/pytest/issues/5444>`_: Fix ``--stepwise`` mode when the first file passed on the command-line fails to collect.
|
||||||
|
|
||||||
|
|
||||||
|
- `#5482 <https://github.com/pytest-dev/pytest/issues/5482>`_: Fix bug introduced in 4.6.0 causing collection errors when passing
|
||||||
|
more than 2 positional arguments to ``pytest.mark.parametrize``.
|
||||||
|
|
||||||
|
|
||||||
|
- `#5505 <https://github.com/pytest-dev/pytest/issues/5505>`_: Fix crash when discovery fails while using ``-p no:terminal``.
|
||||||
|
|
||||||
|
|
||||||
|
pytest 4.6.3 (2019-06-11)
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- `#5383 <https://github.com/pytest-dev/pytest/issues/5383>`_: ``-q`` has again an impact on the style of the collected items
|
||||||
|
(``--collect-only``) when ``--log-cli-level`` is used.
|
||||||
|
|
||||||
|
|
||||||
|
- `#5389 <https://github.com/pytest-dev/pytest/issues/5389>`_: Fix regressions of `#5063 <https://github.com/pytest-dev/pytest/pull/5063>`__ for ``importlib_metadata.PathDistribution`` which have their ``files`` attribute being ``None``.
|
||||||
|
|
||||||
|
|
||||||
|
- `#5390 <https://github.com/pytest-dev/pytest/issues/5390>`_: Fix regression where the ``obj`` attribute of ``TestCase`` items was no longer bound to methods.
|
||||||
|
|
||||||
|
|
||||||
|
pytest 4.6.2 (2019-06-03)
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- `#5370 <https://github.com/pytest-dev/pytest/issues/5370>`_: Revert unrolling of ``all()`` to fix ``NameError`` on nested comprehensions.
|
||||||
|
|
||||||
|
|
||||||
|
- `#5371 <https://github.com/pytest-dev/pytest/issues/5371>`_: Revert unrolling of ``all()`` to fix incorrect handling of generators with ``if``.
|
||||||
|
|
||||||
|
|
||||||
|
- `#5372 <https://github.com/pytest-dev/pytest/issues/5372>`_: Revert unrolling of ``all()`` to fix incorrect assertion when using ``all()`` in an expression.
|
||||||
|
|
||||||
|
|
||||||
|
pytest 4.6.1 (2019-06-02)
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- `#5354 <https://github.com/pytest-dev/pytest/issues/5354>`_: Fix ``pytest.mark.parametrize`` when the argvalues is an iterator.
|
||||||
|
|
||||||
|
|
||||||
|
- `#5358 <https://github.com/pytest-dev/pytest/issues/5358>`_: Fix assertion rewriting of ``all()`` calls to deal with non-generators.
|
||||||
|
|
||||||
|
|
||||||
pytest 4.6.0 (2019-05-31)
|
pytest 4.6.0 (2019-05-31)
|
||||||
=========================
|
=========================
|
||||||
|
|
||||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2004-2019 Holger Krekel and others
|
Copyright (c) 2004-2020 Holger Krekel and others
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
this software and associated documentation files (the "Software"), to deal in
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|
|
@ -131,7 +131,7 @@ Tidelift will coordinate the fix and disclosure.
|
||||||
License
|
License
|
||||||
-------
|
-------
|
||||||
|
|
||||||
Copyright Holger Krekel and others, 2004-2019.
|
Copyright Holger Krekel and others, 2004-2020.
|
||||||
|
|
||||||
Distributed under the terms of the `MIT`_ license, pytest is free and open source software.
|
Distributed under the terms of the `MIT`_ license, pytest is free and open source software.
|
||||||
|
|
||||||
|
|
|
@ -48,12 +48,6 @@ jobs:
|
||||||
# pypy3:
|
# pypy3:
|
||||||
# python.version: 'pypy3'
|
# python.version: 'pypy3'
|
||||||
# tox.env: 'pypy3'
|
# tox.env: 'pypy3'
|
||||||
py34-xdist:
|
|
||||||
python.version: '3.4'
|
|
||||||
tox.env: 'py34-xdist'
|
|
||||||
# Coverage for:
|
|
||||||
# - _pytest.compat._bytes_to_ascii
|
|
||||||
PYTEST_COVERAGE: '1'
|
|
||||||
py35-xdist:
|
py35-xdist:
|
||||||
python.version: '3.5'
|
python.version: '3.5'
|
||||||
tox.env: 'py35-xdist'
|
tox.env: 'py35-xdist'
|
||||||
|
@ -91,7 +85,7 @@ jobs:
|
||||||
condition: eq(variables['python.needs_vc'], True)
|
condition: eq(variables['python.needs_vc'], True)
|
||||||
displayName: 'Install VC for py27'
|
displayName: 'Install VC for py27'
|
||||||
|
|
||||||
- script: python -m pip install --upgrade pip && python -m pip install tox
|
- script: python -m pip install tox
|
||||||
displayName: 'Install tox'
|
displayName: 'Install tox'
|
||||||
|
|
||||||
- script: |
|
- script: |
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
The documentation webpages now links to a canonical version to reduce outdated documentation in search engine results.
|
|
@ -0,0 +1,7 @@
|
||||||
|
coverage:
|
||||||
|
status:
|
||||||
|
project: true
|
||||||
|
patch: true
|
||||||
|
changes: true
|
||||||
|
|
||||||
|
comment: off
|
|
@ -6,6 +6,17 @@ Release announcements
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
|
|
||||||
|
release-4.6.11
|
||||||
|
release-4.6.10
|
||||||
|
release-4.6.9
|
||||||
|
release-4.6.8
|
||||||
|
release-4.6.7
|
||||||
|
release-4.6.6
|
||||||
|
release-4.6.5
|
||||||
|
release-4.6.4
|
||||||
|
release-4.6.3
|
||||||
|
release-4.6.2
|
||||||
|
release-4.6.1
|
||||||
release-4.6.0
|
release-4.6.0
|
||||||
release-4.5.0
|
release-4.5.0
|
||||||
release-4.4.2
|
release-4.4.2
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
pytest-4.6.1
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
pytest 4.6.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/latest/changelog.html.
|
||||||
|
|
||||||
|
Thanks to all who contributed to this release, among them:
|
||||||
|
|
||||||
|
* Anthony Sottile
|
||||||
|
* Bruno Oliveira
|
||||||
|
|
||||||
|
|
||||||
|
Happy testing,
|
||||||
|
The pytest Development Team
|
|
@ -0,0 +1,20 @@
|
||||||
|
pytest-4.6.10
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
pytest 4.6.10 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/latest/changelog.html.
|
||||||
|
|
||||||
|
Thanks to all who contributed to this release, among them:
|
||||||
|
|
||||||
|
* Anthony Sottile
|
||||||
|
* Bruno Oliveira
|
||||||
|
* Fernando Mez
|
||||||
|
|
||||||
|
|
||||||
|
Happy testing,
|
||||||
|
The pytest Development Team
|
|
@ -0,0 +1,20 @@
|
||||||
|
pytest-4.6.11
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
pytest 4.6.11 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/latest/changelog.html.
|
||||||
|
|
||||||
|
Thanks to all who contributed to this release, among them:
|
||||||
|
|
||||||
|
* Anthony Sottile
|
||||||
|
* Bruno Oliveira
|
||||||
|
* Sviatoslav Sydorenko
|
||||||
|
|
||||||
|
|
||||||
|
Happy testing,
|
||||||
|
The pytest Development Team
|
|
@ -0,0 +1,18 @@
|
||||||
|
pytest-4.6.2
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
pytest 4.6.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/latest/changelog.html.
|
||||||
|
|
||||||
|
Thanks to all who contributed to this release, among them:
|
||||||
|
|
||||||
|
* Anthony Sottile
|
||||||
|
|
||||||
|
|
||||||
|
Happy testing,
|
||||||
|
The pytest Development Team
|
|
@ -0,0 +1,21 @@
|
||||||
|
pytest-4.6.3
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
pytest 4.6.3 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/latest/changelog.html.
|
||||||
|
|
||||||
|
Thanks to all who contributed to this release, among them:
|
||||||
|
|
||||||
|
* Anthony Sottile
|
||||||
|
* Bruno Oliveira
|
||||||
|
* Daniel Hahler
|
||||||
|
* Dirk Thomas
|
||||||
|
|
||||||
|
|
||||||
|
Happy testing,
|
||||||
|
The pytest Development Team
|
|
@ -0,0 +1,22 @@
|
||||||
|
pytest-4.6.4
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
pytest 4.6.4 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/latest/changelog.html.
|
||||||
|
|
||||||
|
Thanks to all who contributed to this release, among them:
|
||||||
|
|
||||||
|
* Anthony Sottile
|
||||||
|
* Bruno Oliveira
|
||||||
|
* Daniel Hahler
|
||||||
|
* Thomas Grainger
|
||||||
|
* Zac Hatfield-Dodds
|
||||||
|
|
||||||
|
|
||||||
|
Happy testing,
|
||||||
|
The pytest Development Team
|
|
@ -0,0 +1,21 @@
|
||||||
|
pytest-4.6.5
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
pytest 4.6.5 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/latest/changelog.html.
|
||||||
|
|
||||||
|
Thanks to all who contributed to this release, among them:
|
||||||
|
|
||||||
|
* Anthony Sottile
|
||||||
|
* Bruno Oliveira
|
||||||
|
* Daniel Hahler
|
||||||
|
* Thomas Grainger
|
||||||
|
|
||||||
|
|
||||||
|
Happy testing,
|
||||||
|
The pytest Development Team
|
|
@ -0,0 +1,20 @@
|
||||||
|
pytest-4.6.6
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
pytest 4.6.6 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/latest/changelog.html.
|
||||||
|
|
||||||
|
Thanks to all who contributed to this release, among them:
|
||||||
|
|
||||||
|
* Anthony Sottile
|
||||||
|
* Bruno Oliveira
|
||||||
|
* Michael Goerz
|
||||||
|
|
||||||
|
|
||||||
|
Happy testing,
|
||||||
|
The pytest Development Team
|
|
@ -0,0 +1,19 @@
|
||||||
|
pytest-4.6.7
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
pytest 4.6.7 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/latest/changelog.html.
|
||||||
|
|
||||||
|
Thanks to all who contributed to this release, among them:
|
||||||
|
|
||||||
|
* Bruno Oliveira
|
||||||
|
* Daniel Hahler
|
||||||
|
|
||||||
|
|
||||||
|
Happy testing,
|
||||||
|
The pytest Development Team
|
|
@ -0,0 +1,20 @@
|
||||||
|
pytest-4.6.8
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
pytest 4.6.8 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/latest/changelog.html.
|
||||||
|
|
||||||
|
Thanks to all who contributed to this release, among them:
|
||||||
|
|
||||||
|
* Anthony Sottile
|
||||||
|
* Bruno Oliveira
|
||||||
|
* Ryan Mast
|
||||||
|
|
||||||
|
|
||||||
|
Happy testing,
|
||||||
|
The pytest Development Team
|
|
@ -0,0 +1,21 @@
|
||||||
|
pytest-4.6.9
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
pytest 4.6.9 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/latest/changelog.html.
|
||||||
|
|
||||||
|
Thanks to all who contributed to this release, among them:
|
||||||
|
|
||||||
|
* Anthony Sottile
|
||||||
|
* Bruno Oliveira
|
||||||
|
* Felix Yan
|
||||||
|
* Hugo
|
||||||
|
|
||||||
|
|
||||||
|
Happy testing,
|
||||||
|
The pytest Development Team
|
|
@ -65,7 +65,7 @@ master_doc = "contents"
|
||||||
# General information about the project.
|
# General information about the project.
|
||||||
project = u"pytest"
|
project = u"pytest"
|
||||||
year = datetime.datetime.utcnow().year
|
year = datetime.datetime.utcnow().year
|
||||||
copyright = u"2015–2019 , holger krekel and pytest-dev team"
|
copyright = u"2015–2020, holger krekel and pytest-dev team"
|
||||||
|
|
||||||
|
|
||||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
|
@ -218,6 +218,9 @@ html_show_sourcelink = False
|
||||||
# Output file base name for HTML help builder.
|
# Output file base name for HTML help builder.
|
||||||
htmlhelp_basename = "pytestdoc"
|
htmlhelp_basename = "pytestdoc"
|
||||||
|
|
||||||
|
# The base URL which points to the root of the HTML documentation. It is used
|
||||||
|
# to indicate the location of document using the canonical link relation (#12363).
|
||||||
|
html_baseurl = "https://docs.pytest.org/en/stable/"
|
||||||
|
|
||||||
# -- Options for LaTeX output --------------------------------------------------
|
# -- Options for LaTeX output --------------------------------------------------
|
||||||
|
|
||||||
|
@ -275,7 +278,7 @@ man_pages = [("usage", "pytest", u"pytest usage", [u"holger krekel at merlinux e
|
||||||
epub_title = u"pytest"
|
epub_title = u"pytest"
|
||||||
epub_author = u"holger krekel at merlinux eu"
|
epub_author = u"holger krekel at merlinux eu"
|
||||||
epub_publisher = u"holger krekel at merlinux eu"
|
epub_publisher = u"holger krekel at merlinux eu"
|
||||||
epub_copyright = u"2013, holger krekel et alii"
|
epub_copyright = u"2013-2020, holger krekel et alii"
|
||||||
|
|
||||||
# The language of the text. It defaults to the language option
|
# The language of the text. It defaults to the language option
|
||||||
# or en if the language is not set.
|
# or en if the language is not set.
|
||||||
|
|
|
@ -434,10 +434,11 @@ Running it results in some skips if we don't have all the python interpreters in
|
||||||
.. code-block:: pytest
|
.. code-block:: pytest
|
||||||
|
|
||||||
. $ pytest -rs -q multipython.py
|
. $ pytest -rs -q multipython.py
|
||||||
...sss...sssssssss...sss... [100%]
|
...ssssssssssssssssssssssss [100%]
|
||||||
========================= short test summary info ==========================
|
========================= short test summary info ==========================
|
||||||
SKIPPED [15] $REGENDOC_TMPDIR/CWD/multipython.py:31: 'python3.4' not found
|
SKIPPED [12] $REGENDOC_TMPDIR/CWD/multipython.py:31: 'python3.4' not found
|
||||||
12 passed, 15 skipped in 0.12 seconds
|
SKIPPED [12] $REGENDOC_TMPDIR/CWD/multipython.py:31: 'python3.5' not found
|
||||||
|
3 passed, 24 skipped in 0.12 seconds
|
||||||
|
|
||||||
Indirect parametrization of optional implementations/imports
|
Indirect parametrization of optional implementations/imports
|
||||||
--------------------------------------------------------------------
|
--------------------------------------------------------------------
|
||||||
|
|
|
@ -436,7 +436,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
items = [1, 2, 3]
|
items = [1, 2, 3]
|
||||||
print("items is %r" % items)
|
print("items is %r" % items)
|
||||||
> a, b = items.pop()
|
> a, b = items.pop()
|
||||||
E TypeError: 'int' object is not iterable
|
E TypeError: cannot unpack non-iterable int object
|
||||||
|
|
||||||
failure_demo.py:182: TypeError
|
failure_demo.py:182: TypeError
|
||||||
--------------------------- Captured stdout call ---------------------------
|
--------------------------- Captured stdout call ---------------------------
|
||||||
|
@ -515,7 +515,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
def test_z2_type_error(self):
|
def test_z2_type_error(self):
|
||||||
items = 3
|
items = 3
|
||||||
> a, b = items
|
> a, b = items
|
||||||
E TypeError: 'int' object is not iterable
|
E TypeError: cannot unpack non-iterable int object
|
||||||
|
|
||||||
failure_demo.py:222: TypeError
|
failure_demo.py:222: TypeError
|
||||||
______________________ TestMoreErrors.test_startswith ______________________
|
______________________ TestMoreErrors.test_startswith ______________________
|
||||||
|
|
|
@ -440,7 +440,7 @@ Now we can profile which test functions execute the slowest:
|
||||||
test_some_are_slow.py ... [100%]
|
test_some_are_slow.py ... [100%]
|
||||||
|
|
||||||
========================= slowest 3 test durations =========================
|
========================= slowest 3 test durations =========================
|
||||||
0.30s call test_some_are_slow.py::test_funcslow2
|
0.31s call test_some_are_slow.py::test_funcslow2
|
||||||
0.20s call test_some_are_slow.py::test_funcslow1
|
0.20s call test_some_are_slow.py::test_funcslow1
|
||||||
0.10s call test_some_are_slow.py::test_funcfast
|
0.10s call test_some_are_slow.py::test_funcfast
|
||||||
========================= 3 passed in 0.12 seconds =========================
|
========================= 3 passed in 0.12 seconds =========================
|
||||||
|
|
|
@ -28,7 +28,7 @@ Install ``pytest``
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ pytest --version
|
$ pytest --version
|
||||||
This is pytest version 4.x.y, imported from $PYTHON_PREFIX/lib/python3.6/site-packages/pytest.py
|
This is pytest version 4.x.y, imported from $PYTHON_PREFIX/lib/python3.7/site-packages/pytest.py
|
||||||
|
|
||||||
.. _`simpletest`:
|
.. _`simpletest`:
|
||||||
|
|
||||||
|
|
|
@ -87,7 +87,7 @@ Consult the :ref:`Changelog <changelog>` page for fixes and enhancements of each
|
||||||
License
|
License
|
||||||
-------
|
-------
|
||||||
|
|
||||||
Copyright Holger Krekel and others, 2004-2017.
|
Copyright Holger Krekel and others, 2004-2020.
|
||||||
|
|
||||||
Distributed under the terms of the `MIT`_ license, pytest is free and open source software.
|
Distributed under the terms of the `MIT`_ license, pytest is free and open source software.
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ Distributed under the terms of the `MIT`_ license, pytest is free and open sourc
|
||||||
|
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2004-2017 Holger Krekel and others
|
Copyright (c) 2004-2020 Holger Krekel and others
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
this software and associated documentation files (the "Software"), to deal in
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|
|
@ -17,9 +17,9 @@ are available on PyPI.
|
||||||
|
|
||||||
While pytest ``5.0`` will be the new mainstream and development version, until **January 2020**
|
While pytest ``5.0`` will be the new mainstream and development version, until **January 2020**
|
||||||
the pytest core team plans to make bug-fix releases of the pytest ``4.6`` series by
|
the pytest core team plans to make bug-fix releases of the pytest ``4.6`` series by
|
||||||
back-porting patches to the ``4.6-maintenance`` branch that affect Python 2 users.
|
back-porting patches to the ``4.6.x`` branch that affect Python 2 users.
|
||||||
|
|
||||||
**After 2020**, the core team will no longer actively backport patches, but the ``4.6-maintenance``
|
**After 2020**, the core team will no longer actively backport patches, but the ``4.6.x``
|
||||||
branch will continue to exist so the community itself can contribute patches. The core team will
|
branch will continue to exist so the community itself can contribute patches. The core team will
|
||||||
be happy to accept those patches and make new ``4.6`` releases **until mid-2020**.
|
be happy to accept those patches and make new ``4.6`` releases **until mid-2020**.
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Appends the codecov token to the 'codecov.yml' file at the root of the repository.
|
||||||
|
This is done by CI during PRs and builds on the pytest-dev repository so we can upload coverage, at least
|
||||||
|
until codecov grows some native integration like it has with Travis and AppVeyor.
|
||||||
|
See discussion in https://github.com/pytest-dev/pytest/pull/6441 for more information.
|
||||||
|
"""
|
||||||
|
import os.path
|
||||||
|
from textwrap import dedent
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
this_dir = os.path.dirname(__file__)
|
||||||
|
cov_file = os.path.join(this_dir, "..", "codecov.yml")
|
||||||
|
|
||||||
|
assert os.path.isfile(cov_file), "{cov_file} does not exist".format(
|
||||||
|
cov_file=cov_file
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(cov_file, "a") as f:
|
||||||
|
# token from: https://codecov.io/gh/pytest-dev/pytest/settings
|
||||||
|
# use same URL to regenerate it if needed
|
||||||
|
text = dedent(
|
||||||
|
"""
|
||||||
|
codecov:
|
||||||
|
token: "1eca3b1f-31a2-4fb8-a8c3-138b441b50a7"
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
f.write(text)
|
||||||
|
|
||||||
|
print("Token updated:", cov_file)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
|
@ -0,0 +1,96 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Script used to publish GitHub release notes extracted from CHANGELOG.rst.
|
||||||
|
|
||||||
|
This script is meant to be executed after a successful deployment in Travis.
|
||||||
|
|
||||||
|
Uses the following environment variables:
|
||||||
|
|
||||||
|
* GIT_TAG: the name of the tag of the current commit.
|
||||||
|
* GH_RELEASE_NOTES_TOKEN: a personal access token with 'repo' permissions. It should be encrypted using:
|
||||||
|
|
||||||
|
$travis encrypt GH_RELEASE_NOTES_TOKEN=<token> -r pytest-dev/pytest
|
||||||
|
|
||||||
|
And the contents pasted in the ``deploy.env.secure`` section in the ``travis.yml`` file.
|
||||||
|
|
||||||
|
The script also requires ``pandoc`` to be previously installed in the system.
|
||||||
|
|
||||||
|
Requires Python3.6+.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import github3
|
||||||
|
import pypandoc
|
||||||
|
|
||||||
|
|
||||||
|
def publish_github_release(slug, token, tag_name, body):
|
||||||
|
github = github3.login(token=token)
|
||||||
|
owner, repo = slug.split("/")
|
||||||
|
repo = github.repository(owner, repo)
|
||||||
|
return repo.create_release(tag_name=tag_name, body=body)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_changelog(tag_name):
|
||||||
|
p = Path(__file__).parent.parent / "CHANGELOG.rst"
|
||||||
|
changelog_lines = p.read_text(encoding="UTF-8").splitlines()
|
||||||
|
|
||||||
|
title_regex = re.compile(r"pytest (\d\.\d+\.\d+) \(\d{4}-\d{2}-\d{2}\)")
|
||||||
|
consuming_version = False
|
||||||
|
version_lines = []
|
||||||
|
for line in changelog_lines:
|
||||||
|
m = title_regex.match(line)
|
||||||
|
if m:
|
||||||
|
# found the version we want: start to consume lines until we find the next version title
|
||||||
|
if m.group(1) == tag_name:
|
||||||
|
consuming_version = True
|
||||||
|
# found a new version title while parsing the version we want: break out
|
||||||
|
elif consuming_version:
|
||||||
|
break
|
||||||
|
if consuming_version:
|
||||||
|
version_lines.append(line)
|
||||||
|
|
||||||
|
return "\n".join(version_lines)
|
||||||
|
|
||||||
|
|
||||||
|
def convert_rst_to_md(text):
|
||||||
|
return pypandoc.convert_text(text, "md", format="rst")
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
if len(argv) > 1:
|
||||||
|
tag_name = argv[1]
|
||||||
|
else:
|
||||||
|
tag_name = os.environ.get("TRAVIS_TAG")
|
||||||
|
if not tag_name:
|
||||||
|
print("tag_name not given and $TRAVIS_TAG not set", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
token = os.environ.get("GH_RELEASE_NOTES_TOKEN")
|
||||||
|
if not token:
|
||||||
|
print("GH_RELEASE_NOTES_TOKEN not set", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
slug = os.environ.get("TRAVIS_REPO_SLUG")
|
||||||
|
if not slug:
|
||||||
|
print("TRAVIS_REPO_SLUG not set", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
rst_body = parse_changelog(tag_name)
|
||||||
|
md_body = convert_rst_to_md(rst_body)
|
||||||
|
if not publish_github_release(slug, token, tag_name, md_body):
|
||||||
|
print("Could not publish release notes:", file=sys.stderr)
|
||||||
|
print(md_body, file=sys.stderr)
|
||||||
|
return 5
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(f"Release notes for {tag_name} published successfully:")
|
||||||
|
print(f"https://github.com/{slug}/releases/tag/{tag_name}")
|
||||||
|
print()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main(sys.argv))
|
|
@ -0,0 +1,18 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
set -x
|
||||||
|
|
||||||
|
if [ -z "$TOXENV" ]; then
|
||||||
|
python -m pip install coverage
|
||||||
|
else
|
||||||
|
# Add last TOXENV to $PATH.
|
||||||
|
PATH="$PWD/.tox/${TOXENV##*,}/bin:$PATH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
python -m coverage combine
|
||||||
|
python -m coverage xml
|
||||||
|
python -m coverage report -m
|
||||||
|
# Set --connect-timeout to work around https://github.com/curl/curl/issues/4461
|
||||||
|
curl -S -L --connect-timeout 5 --retry 6 -s https://codecov.io/bash -o codecov-upload.sh
|
||||||
|
bash codecov-upload.sh -Z -X fix -f coverage.xml "$@"
|
|
@ -30,6 +30,8 @@ classifiers =
|
||||||
Programming Language :: Python :: 3.5
|
Programming Language :: Python :: 3.5
|
||||||
Programming Language :: Python :: 3.6
|
Programming Language :: Python :: 3.6
|
||||||
Programming Language :: Python :: 3.7
|
Programming Language :: Python :: 3.7
|
||||||
|
Programming Language :: Python :: 3.8
|
||||||
|
Programming Language :: Python :: 3.9
|
||||||
platforms = unix, linux, osx, cygwin, win32
|
platforms = unix, linux, osx, cygwin, win32
|
||||||
|
|
||||||
[options]
|
[options]
|
||||||
|
|
5
setup.py
5
setup.py
|
@ -13,9 +13,10 @@ INSTALL_REQUIRES = [
|
||||||
"atomicwrites>=1.0",
|
"atomicwrites>=1.0",
|
||||||
'funcsigs>=1.0;python_version<"3.0"',
|
'funcsigs>=1.0;python_version<"3.0"',
|
||||||
'pathlib2>=2.2.0;python_version<"3.6"',
|
'pathlib2>=2.2.0;python_version<"3.6"',
|
||||||
'colorama;sys_platform=="win32"',
|
'colorama<=0.4.1;sys_platform=="win32" and python_version=="3.4"',
|
||||||
|
'colorama;sys_platform=="win32" and python_version!="3.4"',
|
||||||
"pluggy>=0.12,<1.0",
|
"pluggy>=0.12,<1.0",
|
||||||
"importlib-metadata>=0.12",
|
'importlib-metadata>=0.12;python_version<"3.8"',
|
||||||
"wcwidth",
|
"wcwidth",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -572,8 +572,13 @@ class ExceptionInfo(object):
|
||||||
raised.
|
raised.
|
||||||
"""
|
"""
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
if not re.search(regexp, str(self.value)):
|
value = (
|
||||||
assert 0, "Pattern '{!s}' not found in '{!s}'".format(regexp, self.value)
|
text_type(self.value) if isinstance(regexp, text_type) else str(self.value)
|
||||||
|
)
|
||||||
|
if not re.search(regexp, value):
|
||||||
|
raise AssertionError(
|
||||||
|
u"Pattern {!r} not found in {!r}".format(regexp, value)
|
||||||
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -123,18 +123,13 @@ class Source(object):
|
||||||
""" return True if source is parseable, heuristically
|
""" return True if source is parseable, heuristically
|
||||||
deindenting it by default.
|
deindenting it by default.
|
||||||
"""
|
"""
|
||||||
from parser import suite as syntax_checker
|
|
||||||
|
|
||||||
if deindent:
|
if deindent:
|
||||||
source = str(self.deindent())
|
source = str(self.deindent())
|
||||||
else:
|
else:
|
||||||
source = str(self)
|
source = str(self)
|
||||||
try:
|
try:
|
||||||
# compile(source+'\n', "x", "exec")
|
ast.parse(source)
|
||||||
syntax_checker(source + "\n")
|
except (SyntaxError, ValueError, TypeError):
|
||||||
except KeyboardInterrupt:
|
|
||||||
raise
|
|
||||||
except Exception:
|
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -953,8 +953,6 @@ warn_explicit(
|
||||||
"""
|
"""
|
||||||
visit `ast.Call` nodes on Python3.5 and after
|
visit `ast.Call` nodes on Python3.5 and after
|
||||||
"""
|
"""
|
||||||
if isinstance(call.func, ast.Name) and call.func.id == "all":
|
|
||||||
return self._visit_all(call)
|
|
||||||
new_func, func_expl = self.visit(call.func)
|
new_func, func_expl = self.visit(call.func)
|
||||||
arg_expls = []
|
arg_expls = []
|
||||||
new_args = []
|
new_args = []
|
||||||
|
@ -978,27 +976,6 @@ warn_explicit(
|
||||||
outer_expl = "%s\n{%s = %s\n}" % (res_expl, res_expl, expl)
|
outer_expl = "%s\n{%s = %s\n}" % (res_expl, res_expl, expl)
|
||||||
return res, outer_expl
|
return res, outer_expl
|
||||||
|
|
||||||
def _visit_all(self, call):
|
|
||||||
"""Special rewrite for the builtin all function, see #5062"""
|
|
||||||
if not isinstance(call.args[0], (ast.GeneratorExp, ast.ListComp)):
|
|
||||||
return
|
|
||||||
gen_exp = call.args[0]
|
|
||||||
assertion_module = ast.Module(
|
|
||||||
body=[ast.Assert(test=gen_exp.elt, lineno=1, msg="", col_offset=1)]
|
|
||||||
)
|
|
||||||
AssertionRewriter(module_path=None, config=None).run(assertion_module)
|
|
||||||
for_loop = ast.For(
|
|
||||||
iter=gen_exp.generators[0].iter,
|
|
||||||
target=gen_exp.generators[0].target,
|
|
||||||
body=assertion_module.body,
|
|
||||||
orelse=[],
|
|
||||||
)
|
|
||||||
self.statements.append(for_loop)
|
|
||||||
return (
|
|
||||||
ast.Num(n=1),
|
|
||||||
"",
|
|
||||||
) # Return an empty expression, all the asserts are in the for_loop
|
|
||||||
|
|
||||||
def visit_Starred(self, starred):
|
def visit_Starred(self, starred):
|
||||||
# From Python 3.5, a Starred node can appear in a function call
|
# From Python 3.5, a Starred node can appear in a function call
|
||||||
res, expl = self.visit(starred.value)
|
res, expl = self.visit(starred.value)
|
||||||
|
@ -1009,8 +986,6 @@ warn_explicit(
|
||||||
"""
|
"""
|
||||||
visit `ast.Call nodes on 3.4 and below`
|
visit `ast.Call nodes on 3.4 and below`
|
||||||
"""
|
"""
|
||||||
if isinstance(call.func, ast.Name) and call.func.id == "all":
|
|
||||||
return self._visit_all(call)
|
|
||||||
new_func, func_expl = self.visit(call.func)
|
new_func, func_expl = self.visit(call.func)
|
||||||
arg_expls = []
|
arg_expls = []
|
||||||
new_args = []
|
new_args = []
|
||||||
|
|
|
@ -12,6 +12,7 @@ import _pytest._code
|
||||||
from ..compat import Sequence
|
from ..compat import Sequence
|
||||||
from _pytest import outcomes
|
from _pytest import outcomes
|
||||||
from _pytest._io.saferepr import saferepr
|
from _pytest._io.saferepr import saferepr
|
||||||
|
from _pytest.compat import ATTRS_EQ_FIELD
|
||||||
|
|
||||||
# The _reprcompare attribute on the util module is used by the new assertion
|
# The _reprcompare attribute on the util module is used by the new assertion
|
||||||
# interpretation code and assertion rewriter to detect this plugin was
|
# interpretation code and assertion rewriter to detect this plugin was
|
||||||
|
@ -374,7 +375,9 @@ def _compare_eq_cls(left, right, verbose, type_fns):
|
||||||
fields_to_check = [field for field, info in all_fields.items() if info.compare]
|
fields_to_check = [field for field, info in all_fields.items() if info.compare]
|
||||||
elif isattrs(left):
|
elif isattrs(left):
|
||||||
all_fields = left.__attrs_attrs__
|
all_fields = left.__attrs_attrs__
|
||||||
fields_to_check = [field.name for field in all_fields if field.cmp]
|
fields_to_check = [
|
||||||
|
field.name for field in all_fields if getattr(field, ATTRS_EQ_FIELD)
|
||||||
|
]
|
||||||
|
|
||||||
same = []
|
same = []
|
||||||
diff = []
|
diff = []
|
||||||
|
|
|
@ -21,7 +21,7 @@ import pytest
|
||||||
from .compat import _PY2 as PY2
|
from .compat import _PY2 as PY2
|
||||||
from .pathlib import Path
|
from .pathlib import Path
|
||||||
from .pathlib import resolve_from_str
|
from .pathlib import resolve_from_str
|
||||||
from .pathlib import rmtree
|
from .pathlib import rm_rf
|
||||||
|
|
||||||
README_CONTENT = u"""\
|
README_CONTENT = u"""\
|
||||||
# pytest cache directory #
|
# pytest cache directory #
|
||||||
|
@ -51,7 +51,7 @@ class Cache(object):
|
||||||
def for_config(cls, config):
|
def for_config(cls, config):
|
||||||
cachedir = cls.cache_dir_from_config(config)
|
cachedir = cls.cache_dir_from_config(config)
|
||||||
if config.getoption("cacheclear") and cachedir.exists():
|
if config.getoption("cacheclear") and cachedir.exists():
|
||||||
rmtree(cachedir, force=True)
|
rm_rf(cachedir)
|
||||||
cachedir.mkdir()
|
cachedir.mkdir()
|
||||||
return cls(cachedir, config)
|
return cls(cachedir, config)
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ import re
|
||||||
import sys
|
import sys
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
import attr
|
||||||
import py
|
import py
|
||||||
import six
|
import six
|
||||||
from six import text_type
|
from six import text_type
|
||||||
|
@ -61,6 +62,12 @@ else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 8):
|
||||||
|
from importlib import metadata as importlib_metadata # noqa
|
||||||
|
else:
|
||||||
|
import importlib_metadata # noqa
|
||||||
|
|
||||||
|
|
||||||
def _format_args(func):
|
def _format_args(func):
|
||||||
return str(signature(func))
|
return str(signature(func))
|
||||||
|
|
||||||
|
@ -377,7 +384,7 @@ if _PY3:
|
||||||
else:
|
else:
|
||||||
|
|
||||||
def safe_str(v):
|
def safe_str(v):
|
||||||
"""returns v as string, converting to ascii if necessary"""
|
"""returns v as string, converting to utf-8 if necessary"""
|
||||||
try:
|
try:
|
||||||
return str(v)
|
return str(v)
|
||||||
except UnicodeError:
|
except UnicodeError:
|
||||||
|
@ -406,8 +413,8 @@ def _setup_collect_fakemodule():
|
||||||
|
|
||||||
pytest.collect = ModuleType("pytest.collect")
|
pytest.collect = ModuleType("pytest.collect")
|
||||||
pytest.collect.__all__ = [] # used for setns
|
pytest.collect.__all__ = [] # used for setns
|
||||||
for attr in COLLECT_FAKEMODULE_ATTRIBUTES:
|
for attribute in COLLECT_FAKEMODULE_ATTRIBUTES:
|
||||||
setattr(pytest.collect, attr, getattr(pytest, attr))
|
setattr(pytest.collect, attribute, getattr(pytest, attribute))
|
||||||
|
|
||||||
|
|
||||||
if _PY2:
|
if _PY2:
|
||||||
|
@ -455,3 +462,9 @@ if six.PY2:
|
||||||
|
|
||||||
else:
|
else:
|
||||||
from functools import lru_cache # noqa: F401
|
from functools import lru_cache # noqa: F401
|
||||||
|
|
||||||
|
|
||||||
|
if getattr(attr, "__version_info__", ()) >= (19, 2):
|
||||||
|
ATTRS_EQ_FIELD = "eq"
|
||||||
|
else:
|
||||||
|
ATTRS_EQ_FIELD = "cmp"
|
||||||
|
|
|
@ -13,7 +13,7 @@ import sys
|
||||||
import types
|
import types
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
import importlib_metadata
|
import attr
|
||||||
import py
|
import py
|
||||||
import six
|
import six
|
||||||
from packaging.version import Version
|
from packaging.version import Version
|
||||||
|
@ -31,10 +31,12 @@ from .findpaths import exists
|
||||||
from _pytest import deprecated
|
from _pytest import deprecated
|
||||||
from _pytest._code import ExceptionInfo
|
from _pytest._code import ExceptionInfo
|
||||||
from _pytest._code import filter_traceback
|
from _pytest._code import filter_traceback
|
||||||
|
from _pytest.compat import importlib_metadata
|
||||||
from _pytest.compat import lru_cache
|
from _pytest.compat import lru_cache
|
||||||
from _pytest.compat import safe_str
|
from _pytest.compat import safe_str
|
||||||
from _pytest.outcomes import fail
|
from _pytest.outcomes import fail
|
||||||
from _pytest.outcomes import Skipped
|
from _pytest.outcomes import Skipped
|
||||||
|
from _pytest.pathlib import Path
|
||||||
from _pytest.warning_types import PytestConfigWarning
|
from _pytest.warning_types import PytestConfigWarning
|
||||||
|
|
||||||
hookimpl = HookimplMarker("pytest")
|
hookimpl = HookimplMarker("pytest")
|
||||||
|
@ -116,13 +118,13 @@ def directory_arg(path, optname):
|
||||||
|
|
||||||
|
|
||||||
# Plugins that cannot be disabled via "-p no:X" currently.
|
# Plugins that cannot be disabled via "-p no:X" currently.
|
||||||
essential_plugins = ( # fmt: off
|
essential_plugins = (
|
||||||
"mark",
|
"mark",
|
||||||
"main",
|
"main",
|
||||||
"runner",
|
"runner",
|
||||||
"fixtures",
|
"fixtures",
|
||||||
"helpconfig", # Provides -p.
|
"helpconfig", # Provides -p.
|
||||||
) # fmt: on
|
)
|
||||||
|
|
||||||
default_plugins = essential_plugins + (
|
default_plugins = essential_plugins + (
|
||||||
"python",
|
"python",
|
||||||
|
@ -154,10 +156,15 @@ builtin_plugins = set(default_plugins)
|
||||||
builtin_plugins.add("pytester")
|
builtin_plugins.add("pytester")
|
||||||
|
|
||||||
|
|
||||||
def get_config(args=None):
|
def get_config(args=None, plugins=None):
|
||||||
# subsequent calls to main will create a fresh instance
|
# subsequent calls to main will create a fresh instance
|
||||||
pluginmanager = PytestPluginManager()
|
pluginmanager = PytestPluginManager()
|
||||||
config = Config(pluginmanager)
|
config = Config(
|
||||||
|
pluginmanager,
|
||||||
|
invocation_params=Config.InvocationParams(
|
||||||
|
args=args, plugins=plugins, dir=Path().resolve()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
if args is not None:
|
if args is not None:
|
||||||
# Handle any "-p no:plugin" args.
|
# Handle any "-p no:plugin" args.
|
||||||
|
@ -190,7 +197,7 @@ def _prepareconfig(args=None, plugins=None):
|
||||||
msg = "`args` parameter expected to be a list or tuple of strings, got: {!r} (type: {})"
|
msg = "`args` parameter expected to be a list or tuple of strings, got: {!r} (type: {})"
|
||||||
raise TypeError(msg.format(args, type(args)))
|
raise TypeError(msg.format(args, type(args)))
|
||||||
|
|
||||||
config = get_config(args)
|
config = get_config(args, plugins)
|
||||||
pluginmanager = config.pluginmanager
|
pluginmanager = config.pluginmanager
|
||||||
try:
|
try:
|
||||||
if plugins:
|
if plugins:
|
||||||
|
@ -622,25 +629,116 @@ notset = Notset()
|
||||||
|
|
||||||
|
|
||||||
def _iter_rewritable_modules(package_files):
|
def _iter_rewritable_modules(package_files):
|
||||||
|
"""
|
||||||
|
Given an iterable of file names in a source distribution, return the "names" that should
|
||||||
|
be marked for assertion rewrite (for example the package "pytest_mock/__init__.py" should
|
||||||
|
be added as "pytest_mock" in the assertion rewrite mechanism.
|
||||||
|
|
||||||
|
This function has to deal with dist-info based distributions and egg based distributions
|
||||||
|
(which are still very much in use for "editable" installs).
|
||||||
|
|
||||||
|
Here are the file names as seen in a dist-info based distribution:
|
||||||
|
|
||||||
|
pytest_mock/__init__.py
|
||||||
|
pytest_mock/_version.py
|
||||||
|
pytest_mock/plugin.py
|
||||||
|
pytest_mock.egg-info/PKG-INFO
|
||||||
|
|
||||||
|
Here are the file names as seen in an egg based distribution:
|
||||||
|
|
||||||
|
src/pytest_mock/__init__.py
|
||||||
|
src/pytest_mock/_version.py
|
||||||
|
src/pytest_mock/plugin.py
|
||||||
|
src/pytest_mock.egg-info/PKG-INFO
|
||||||
|
LICENSE
|
||||||
|
setup.py
|
||||||
|
|
||||||
|
We have to take in account those two distribution flavors in order to determine which
|
||||||
|
names should be considered for assertion rewriting.
|
||||||
|
|
||||||
|
More information:
|
||||||
|
https://github.com/pytest-dev/pytest-mock/issues/167
|
||||||
|
"""
|
||||||
|
package_files = list(package_files)
|
||||||
|
seen_some = False
|
||||||
for fn in package_files:
|
for fn in package_files:
|
||||||
is_simple_module = "/" not in fn and fn.endswith(".py")
|
is_simple_module = "/" not in fn and fn.endswith(".py")
|
||||||
is_package = fn.count("/") == 1 and fn.endswith("__init__.py")
|
is_package = fn.count("/") == 1 and fn.endswith("__init__.py")
|
||||||
if is_simple_module:
|
if is_simple_module:
|
||||||
module_name, _ = os.path.splitext(fn)
|
module_name, _ = os.path.splitext(fn)
|
||||||
|
# we ignore "setup.py" at the root of the distribution
|
||||||
|
if module_name != "setup":
|
||||||
|
seen_some = True
|
||||||
yield module_name
|
yield module_name
|
||||||
elif is_package:
|
elif is_package:
|
||||||
package_name = os.path.dirname(fn)
|
package_name = os.path.dirname(fn)
|
||||||
|
seen_some = True
|
||||||
yield package_name
|
yield package_name
|
||||||
|
|
||||||
|
if not seen_some:
|
||||||
|
# at this point we did not find any packages or modules suitable for assertion
|
||||||
|
# rewriting, so we try again by stripping the first path component (to account for
|
||||||
|
# "src" based source trees for example)
|
||||||
|
# this approach lets us have the common case continue to be fast, as egg-distributions
|
||||||
|
# are rarer
|
||||||
|
new_package_files = []
|
||||||
|
for fn in package_files:
|
||||||
|
parts = fn.split("/")
|
||||||
|
new_fn = "/".join(parts[1:])
|
||||||
|
if new_fn:
|
||||||
|
new_package_files.append(new_fn)
|
||||||
|
if new_package_files:
|
||||||
|
for _module in _iter_rewritable_modules(new_package_files):
|
||||||
|
yield _module
|
||||||
|
|
||||||
|
|
||||||
class Config(object):
|
class Config(object):
|
||||||
""" access to configuration values, pluginmanager and plugin hooks. """
|
"""
|
||||||
|
Access to configuration values, pluginmanager and plugin hooks.
|
||||||
|
|
||||||
|
:ivar PytestPluginManager pluginmanager: the plugin manager handles plugin registration and hook invocation.
|
||||||
|
|
||||||
|
:ivar argparse.Namespace option: access to command line option as attributes.
|
||||||
|
|
||||||
|
:ivar InvocationParams invocation_params:
|
||||||
|
|
||||||
|
Object containing the parameters regarding the ``pytest.main``
|
||||||
|
invocation.
|
||||||
|
Contains the followinig read-only attributes:
|
||||||
|
* ``args``: list of command-line arguments as passed to ``pytest.main()``.
|
||||||
|
* ``plugins``: list of extra plugins, might be None
|
||||||
|
* ``dir``: directory where ``pytest.main()`` was invoked from.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@attr.s(frozen=True)
|
||||||
|
class InvocationParams(object):
|
||||||
|
"""Holds parameters passed during ``pytest.main()``
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Currently the environment variable PYTEST_ADDOPTS is also handled by
|
||||||
|
pytest implicitly, not being part of the invocation.
|
||||||
|
|
||||||
|
Plugins accessing ``InvocationParams`` must be aware of that.
|
||||||
|
"""
|
||||||
|
|
||||||
|
args = attr.ib()
|
||||||
|
plugins = attr.ib()
|
||||||
|
dir = attr.ib()
|
||||||
|
|
||||||
|
def __init__(self, pluginmanager, invocation_params=None, *args):
|
||||||
|
from .argparsing import Parser, FILE_OR_DIR
|
||||||
|
|
||||||
|
if invocation_params is None:
|
||||||
|
invocation_params = self.InvocationParams(
|
||||||
|
args=(), plugins=None, dir=Path().resolve()
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, pluginmanager):
|
|
||||||
#: access to command line option as attributes.
|
#: access to command line option as attributes.
|
||||||
#: (deprecated), use :py:func:`getoption() <_pytest.config.Config.getoption>` instead
|
#: (deprecated), use :py:func:`getoption() <_pytest.config.Config.getoption>` instead
|
||||||
self.option = argparse.Namespace()
|
self.option = argparse.Namespace()
|
||||||
from .argparsing import Parser, FILE_OR_DIR
|
|
||||||
|
self.invocation_params = invocation_params
|
||||||
|
|
||||||
_a = FILE_OR_DIR
|
_a = FILE_OR_DIR
|
||||||
self._parser = Parser(
|
self._parser = Parser(
|
||||||
|
@ -657,9 +755,13 @@ class Config(object):
|
||||||
self._cleanup = []
|
self._cleanup = []
|
||||||
self.pluginmanager.register(self, "pytestconfig")
|
self.pluginmanager.register(self, "pytestconfig")
|
||||||
self._configured = False
|
self._configured = False
|
||||||
self.invocation_dir = py.path.local()
|
|
||||||
self.hook.pytest_addoption.call_historic(kwargs=dict(parser=self._parser))
|
self.hook.pytest_addoption.call_historic(kwargs=dict(parser=self._parser))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def invocation_dir(self):
|
||||||
|
"""Backward compatibility"""
|
||||||
|
return py.path.local(str(self.invocation_params.dir))
|
||||||
|
|
||||||
def add_cleanup(self, func):
|
def add_cleanup(self, func):
|
||||||
""" Add a function to be called when the config object gets out of
|
""" Add a function to be called when the config object gets out of
|
||||||
use (usually coninciding with pytest_unconfigure)."""
|
use (usually coninciding with pytest_unconfigure)."""
|
||||||
|
@ -800,7 +902,7 @@ class Config(object):
|
||||||
str(file)
|
str(file)
|
||||||
for dist in importlib_metadata.distributions()
|
for dist in importlib_metadata.distributions()
|
||||||
if any(ep.group == "pytest11" for ep in dist.entry_points)
|
if any(ep.group == "pytest11" for ep in dist.entry_points)
|
||||||
for file in dist.files
|
for file in dist.files or []
|
||||||
)
|
)
|
||||||
|
|
||||||
for name in _iter_rewritable_modules(package_files):
|
for name in _iter_rewritable_modules(package_files):
|
||||||
|
|
|
@ -33,7 +33,11 @@ def getcfg(args, config=None):
|
||||||
for inibasename in inibasenames:
|
for inibasename in inibasenames:
|
||||||
p = base.join(inibasename)
|
p = base.join(inibasename)
|
||||||
if exists(p):
|
if exists(p):
|
||||||
|
try:
|
||||||
iniconfig = py.iniconfig.IniConfig(p)
|
iniconfig = py.iniconfig.IniConfig(p)
|
||||||
|
except py.iniconfig.ParseError as exc:
|
||||||
|
raise UsageError(str(exc))
|
||||||
|
|
||||||
if (
|
if (
|
||||||
inibasename == "setup.cfg"
|
inibasename == "setup.cfg"
|
||||||
and "tool:pytest" in iniconfig.sections
|
and "tool:pytest" in iniconfig.sections
|
||||||
|
|
|
@ -40,8 +40,8 @@ GETFUNCARGVALUE = RemovedInPytest4Warning(
|
||||||
RAISES_MESSAGE_PARAMETER = PytestDeprecationWarning(
|
RAISES_MESSAGE_PARAMETER = PytestDeprecationWarning(
|
||||||
"The 'message' parameter is deprecated.\n"
|
"The 'message' parameter is deprecated.\n"
|
||||||
"(did you mean to use `match='some regex'` to check the exception message?)\n"
|
"(did you mean to use `match='some regex'` to check the exception message?)\n"
|
||||||
"Please comment on https://github.com/pytest-dev/pytest/issues/3974 "
|
"Please see:\n"
|
||||||
"if you have concerns about removal of this parameter."
|
" https://docs.pytest.org/en/4.6-maintenance/deprecations.html#message-parameter-of-pytest-raises"
|
||||||
)
|
)
|
||||||
|
|
||||||
RESULT_LOG = PytestDeprecationWarning(
|
RESULT_LOG = PytestDeprecationWarning(
|
||||||
|
|
|
@ -8,6 +8,7 @@ import inspect
|
||||||
import platform
|
import platform
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
|
import warnings
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -17,6 +18,7 @@ from _pytest._code.code import TerminalRepr
|
||||||
from _pytest.compat import safe_getattr
|
from _pytest.compat import safe_getattr
|
||||||
from _pytest.fixtures import FixtureRequest
|
from _pytest.fixtures import FixtureRequest
|
||||||
from _pytest.outcomes import Skipped
|
from _pytest.outcomes import Skipped
|
||||||
|
from _pytest.warning_types import PytestWarning
|
||||||
|
|
||||||
DOCTEST_REPORT_CHOICE_NONE = "none"
|
DOCTEST_REPORT_CHOICE_NONE = "none"
|
||||||
DOCTEST_REPORT_CHOICE_CDIFF = "cdiff"
|
DOCTEST_REPORT_CHOICE_CDIFF = "cdiff"
|
||||||
|
@ -374,10 +376,18 @@ def _patch_unwrap_mock_aware():
|
||||||
else:
|
else:
|
||||||
|
|
||||||
def _mock_aware_unwrap(obj, stop=None):
|
def _mock_aware_unwrap(obj, stop=None):
|
||||||
if stop is None:
|
try:
|
||||||
|
if stop is None or stop is _is_mocked:
|
||||||
return real_unwrap(obj, stop=_is_mocked)
|
return real_unwrap(obj, stop=_is_mocked)
|
||||||
else:
|
|
||||||
return real_unwrap(obj, stop=lambda obj: _is_mocked(obj) or stop(obj))
|
return real_unwrap(obj, stop=lambda obj: _is_mocked(obj) or stop(obj))
|
||||||
|
except Exception as e:
|
||||||
|
warnings.warn(
|
||||||
|
"Got %r when unwrapping %r. This is usually caused "
|
||||||
|
"by a violation of Python's object protocol; see e.g. "
|
||||||
|
"https://github.com/pytest-dev/pytest/issues/5080" % (e, obj),
|
||||||
|
PytestWarning,
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
inspect.unwrap = _mock_aware_unwrap
|
inspect.unwrap = _mock_aware_unwrap
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -15,9 +15,11 @@ from __future__ import print_function
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
import os
|
import os
|
||||||
|
import platform
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
import py
|
import py
|
||||||
import six
|
import six
|
||||||
|
@ -595,6 +597,8 @@ class LogXML(object):
|
||||||
if report.when == "call":
|
if report.when == "call":
|
||||||
reporter.append_failure(report)
|
reporter.append_failure(report)
|
||||||
self.open_reports.append(report)
|
self.open_reports.append(report)
|
||||||
|
if not self.log_passing_tests:
|
||||||
|
reporter.write_captured_output(report)
|
||||||
else:
|
else:
|
||||||
reporter.append_error(report)
|
reporter.append_error(report)
|
||||||
elif report.skipped:
|
elif report.skipped:
|
||||||
|
@ -667,8 +671,7 @@ class LogXML(object):
|
||||||
)
|
)
|
||||||
logfile.write('<?xml version="1.0" encoding="utf-8"?>')
|
logfile.write('<?xml version="1.0" encoding="utf-8"?>')
|
||||||
|
|
||||||
logfile.write(
|
suite_node = Junit.testsuite(
|
||||||
Junit.testsuite(
|
|
||||||
self._get_global_properties_node(),
|
self._get_global_properties_node(),
|
||||||
[x.to_xml() for x in self.node_reporters_ordered],
|
[x.to_xml() for x in self.node_reporters_ordered],
|
||||||
name=self.suite_name,
|
name=self.suite_name,
|
||||||
|
@ -677,8 +680,10 @@ class LogXML(object):
|
||||||
skipped=self.stats["skipped"],
|
skipped=self.stats["skipped"],
|
||||||
tests=numtests,
|
tests=numtests,
|
||||||
time="%.3f" % suite_time_delta,
|
time="%.3f" % suite_time_delta,
|
||||||
).unicode(indent=0)
|
timestamp=datetime.fromtimestamp(self.suite_start_time).isoformat(),
|
||||||
|
hostname=platform.node(),
|
||||||
)
|
)
|
||||||
|
logfile.write(Junit.testsuites([suite_node]).unicode(indent=0))
|
||||||
logfile.close()
|
logfile.close()
|
||||||
|
|
||||||
def pytest_terminal_summary(self, terminalreporter):
|
def pytest_terminal_summary(self, terminalreporter):
|
||||||
|
|
|
@ -424,10 +424,6 @@ class LoggingPlugin(object):
|
||||||
"""
|
"""
|
||||||
self._config = config
|
self._config = config
|
||||||
|
|
||||||
# enable verbose output automatically if live logging is enabled
|
|
||||||
if self._log_cli_enabled() and config.getoption("verbose") < 1:
|
|
||||||
config.option.verbose = 1
|
|
||||||
|
|
||||||
self.print_logs = get_option_ini(config, "log_print")
|
self.print_logs = get_option_ini(config, "log_print")
|
||||||
self.formatter = self._create_formatter(
|
self.formatter = self._create_formatter(
|
||||||
get_option_ini(config, "log_format"),
|
get_option_ini(config, "log_format"),
|
||||||
|
@ -644,6 +640,15 @@ class LoggingPlugin(object):
|
||||||
@pytest.hookimpl(hookwrapper=True)
|
@pytest.hookimpl(hookwrapper=True)
|
||||||
def pytest_runtestloop(self, session):
|
def pytest_runtestloop(self, session):
|
||||||
"""Runs all collected test items."""
|
"""Runs all collected test items."""
|
||||||
|
|
||||||
|
if session.config.option.collectonly:
|
||||||
|
yield
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._log_cli_enabled() and self._config.getoption("verbose") < 1:
|
||||||
|
# setting verbose flag is needed to avoid messy test progress output
|
||||||
|
self._config.option.verbose = 1
|
||||||
|
|
||||||
with self.live_logs_context():
|
with self.live_logs_context():
|
||||||
if self.log_file_handler is not None:
|
if self.log_file_handler is not None:
|
||||||
with catching_logs(self.log_file_handler, level=self.log_file_level):
|
with catching_logs(self.log_file_handler, level=self.log_file_level):
|
||||||
|
|
|
@ -621,7 +621,13 @@ class Session(nodes.FSCollector):
|
||||||
# Module itself, so just use that. If this special case isn't taken, then all
|
# Module itself, so just use that. If this special case isn't taken, then all
|
||||||
# the files in the package will be yielded.
|
# the files in the package will be yielded.
|
||||||
if argpath.basename == "__init__.py":
|
if argpath.basename == "__init__.py":
|
||||||
|
try:
|
||||||
yield next(m[0].collect())
|
yield next(m[0].collect())
|
||||||
|
except StopIteration:
|
||||||
|
# The package collects nothing with only an __init__.py
|
||||||
|
# file in it, which gets ignored by the default
|
||||||
|
# "python_files" option.
|
||||||
|
pass
|
||||||
return
|
return
|
||||||
for y in m:
|
for y in m:
|
||||||
yield y
|
yield y
|
||||||
|
|
|
@ -8,6 +8,7 @@ import attr
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from ..compat import ascii_escaped
|
from ..compat import ascii_escaped
|
||||||
|
from ..compat import ATTRS_EQ_FIELD
|
||||||
from ..compat import getfslineno
|
from ..compat import getfslineno
|
||||||
from ..compat import MappingMixin
|
from ..compat import MappingMixin
|
||||||
from ..compat import NOTSET
|
from ..compat import NOTSET
|
||||||
|
@ -104,23 +105,24 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")):
|
||||||
return cls(parameterset, marks=[], id=None)
|
return cls(parameterset, marks=[], id=None)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_parametrize_args(argnames, argvalues, **_):
|
def _parse_parametrize_args(argnames, argvalues, *args, **kwargs):
|
||||||
"""It receives an ignored _ (kwargs) argument so this function can
|
|
||||||
take also calls from parametrize ignoring scope, indirect, and other
|
|
||||||
arguments..."""
|
|
||||||
if not isinstance(argnames, (tuple, list)):
|
if not isinstance(argnames, (tuple, list)):
|
||||||
argnames = [x.strip() for x in argnames.split(",") if x.strip()]
|
argnames = [x.strip() for x in argnames.split(",") if x.strip()]
|
||||||
force_tuple = len(argnames) == 1
|
force_tuple = len(argnames) == 1
|
||||||
else:
|
else:
|
||||||
force_tuple = False
|
force_tuple = False
|
||||||
parameters = [
|
return argnames, force_tuple
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_parametrize_parameters(argvalues, force_tuple):
|
||||||
|
return [
|
||||||
ParameterSet.extract_from(x, force_tuple=force_tuple) for x in argvalues
|
ParameterSet.extract_from(x, force_tuple=force_tuple) for x in argvalues
|
||||||
]
|
]
|
||||||
return argnames, parameters
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _for_parametrize(cls, argnames, argvalues, func, config, function_definition):
|
def _for_parametrize(cls, argnames, argvalues, func, config, function_definition):
|
||||||
argnames, parameters = cls._parse_parametrize_args(argnames, argvalues)
|
argnames, force_tuple = cls._parse_parametrize_args(argnames, argvalues)
|
||||||
|
parameters = cls._parse_parametrize_parameters(argvalues, force_tuple)
|
||||||
del argvalues
|
del argvalues
|
||||||
|
|
||||||
if parameters:
|
if parameters:
|
||||||
|
@ -376,7 +378,8 @@ class NodeKeywords(MappingMixin):
|
||||||
return "<NodeKeywords for node %s>" % (self.node,)
|
return "<NodeKeywords for node %s>" % (self.node,)
|
||||||
|
|
||||||
|
|
||||||
@attr.s(cmp=False, hash=False)
|
# mypy cannot find this overload, remove when on attrs>=19.2
|
||||||
|
@attr.s(hash=False, **{ATTRS_EQ_FIELD: False}) # type: ignore
|
||||||
class NodeMarkers(object):
|
class NodeMarkers(object):
|
||||||
"""
|
"""
|
||||||
internal structure for storing marks belonging to a node
|
internal structure for storing marks belonging to a node
|
||||||
|
|
|
@ -329,7 +329,7 @@ class Collector(Node):
|
||||||
|
|
||||||
# Respect explicit tbstyle option, but default to "short"
|
# Respect explicit tbstyle option, but default to "short"
|
||||||
# (None._repr_failure_py defaults to "long" without "fulltrace" option).
|
# (None._repr_failure_py defaults to "long" without "fulltrace" option).
|
||||||
tbstyle = self.config.getoption("tbstyle")
|
tbstyle = self.config.getoption("tbstyle", "auto")
|
||||||
if tbstyle == "auto":
|
if tbstyle == "auto":
|
||||||
tbstyle = "short"
|
tbstyle = "short"
|
||||||
|
|
||||||
|
|
|
@ -77,11 +77,7 @@ def create_new_paste(contents):
|
||||||
from urllib.request import urlopen
|
from urllib.request import urlopen
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
params = {
|
params = {"code": contents, "lexer": "text", "expiry": "1week"}
|
||||||
"code": contents,
|
|
||||||
"lexer": "python3" if sys.version_info[0] == 3 else "python",
|
|
||||||
"expiry": "1week",
|
|
||||||
}
|
|
||||||
url = "https://bpaste.net"
|
url = "https://bpaste.net"
|
||||||
response = urlopen(url, data=urlencode(params).encode("ascii")).read()
|
response = urlopen(url, data=urlencode(params).encode("ascii")).read()
|
||||||
m = re.search(r'href="/raw/(\w+)"', response.decode("utf-8"))
|
m = re.search(r'href="/raw/(\w+)"', response.decode("utf-8"))
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
import atexit
|
import atexit
|
||||||
import errno
|
import errno
|
||||||
import fnmatch
|
import fnmatch
|
||||||
|
@ -8,6 +10,8 @@ import os
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
import uuid
|
import uuid
|
||||||
|
import warnings
|
||||||
|
from functools import partial
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
from os.path import expanduser
|
from os.path import expanduser
|
||||||
from os.path import expandvars
|
from os.path import expandvars
|
||||||
|
@ -19,6 +23,7 @@ import six
|
||||||
from six.moves import map
|
from six.moves import map
|
||||||
|
|
||||||
from .compat import PY36
|
from .compat import PY36
|
||||||
|
from _pytest.warning_types import PytestWarning
|
||||||
|
|
||||||
if PY36:
|
if PY36:
|
||||||
from pathlib import Path, PurePath
|
from pathlib import Path, PurePath
|
||||||
|
@ -38,17 +43,72 @@ def ensure_reset_dir(path):
|
||||||
ensures the given path is an empty directory
|
ensures the given path is an empty directory
|
||||||
"""
|
"""
|
||||||
if path.exists():
|
if path.exists():
|
||||||
rmtree(path, force=True)
|
rm_rf(path)
|
||||||
path.mkdir()
|
path.mkdir()
|
||||||
|
|
||||||
|
|
||||||
def rmtree(path, force=False):
|
def on_rm_rf_error(func, path, exc, **kwargs):
|
||||||
if force:
|
"""Handles known read-only errors during rmtree.
|
||||||
# NOTE: ignore_errors might leave dead folders around.
|
|
||||||
# Python needs a rm -rf as a followup.
|
The returned value is used only by our own tests.
|
||||||
shutil.rmtree(str(path), ignore_errors=True)
|
"""
|
||||||
else:
|
start_path = kwargs["start_path"]
|
||||||
shutil.rmtree(str(path))
|
exctype, excvalue = exc[:2]
|
||||||
|
|
||||||
|
# another process removed the file in the middle of the "rm_rf" (xdist for example)
|
||||||
|
# more context: https://github.com/pytest-dev/pytest/issues/5974#issuecomment-543799018
|
||||||
|
if isinstance(excvalue, OSError) and excvalue.errno == errno.ENOENT:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not isinstance(excvalue, OSError) or excvalue.errno not in (
|
||||||
|
errno.EACCES,
|
||||||
|
errno.EPERM,
|
||||||
|
):
|
||||||
|
warnings.warn(
|
||||||
|
PytestWarning(
|
||||||
|
"(rm_rf) error removing {}\n{}: {}".format(path, exctype, excvalue)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if func not in (os.rmdir, os.remove, os.unlink):
|
||||||
|
warnings.warn(
|
||||||
|
PytestWarning(
|
||||||
|
"(rm_rf) unknown function {} when removing {}:\n{}: {}".format(
|
||||||
|
path, func, exctype, excvalue
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Chmod + retry.
|
||||||
|
import stat
|
||||||
|
|
||||||
|
def chmod_rw(p):
|
||||||
|
mode = os.stat(p).st_mode
|
||||||
|
os.chmod(p, mode | stat.S_IRUSR | stat.S_IWUSR)
|
||||||
|
|
||||||
|
# For files, we need to recursively go upwards in the directories to
|
||||||
|
# ensure they all are also writable.
|
||||||
|
p = Path(path)
|
||||||
|
if p.is_file():
|
||||||
|
for parent in p.parents:
|
||||||
|
chmod_rw(str(parent))
|
||||||
|
# stop when we reach the original path passed to rm_rf
|
||||||
|
if parent == start_path:
|
||||||
|
break
|
||||||
|
chmod_rw(str(path))
|
||||||
|
|
||||||
|
func(path)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def rm_rf(path):
|
||||||
|
"""Remove the path contents recursively, even if some elements
|
||||||
|
are read-only.
|
||||||
|
"""
|
||||||
|
onerror = partial(on_rm_rf_error, start_path=path)
|
||||||
|
shutil.rmtree(str(path), onerror=onerror)
|
||||||
|
|
||||||
|
|
||||||
def find_prefixed(root, prefix):
|
def find_prefixed(root, prefix):
|
||||||
|
@ -186,7 +246,7 @@ def maybe_delete_a_numbered_dir(path):
|
||||||
|
|
||||||
garbage = parent.joinpath("garbage-{}".format(uuid.uuid4()))
|
garbage = parent.joinpath("garbage-{}".format(uuid.uuid4()))
|
||||||
path.rename(garbage)
|
path.rename(garbage)
|
||||||
rmtree(garbage, force=True)
|
rm_rf(garbage)
|
||||||
except (OSError, EnvironmentError):
|
except (OSError, EnvironmentError):
|
||||||
# known races:
|
# known races:
|
||||||
# * other process did a cleanup at the same time
|
# * other process did a cleanup at the same time
|
||||||
|
|
|
@ -1124,7 +1124,7 @@ class Testdir(object):
|
||||||
|
|
||||||
if timeout is None:
|
if timeout is None:
|
||||||
ret = popen.wait()
|
ret = popen.wait()
|
||||||
elif six.PY3:
|
elif not six.PY2:
|
||||||
try:
|
try:
|
||||||
ret = popen.wait(timeout)
|
ret = popen.wait(timeout)
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
|
|
|
@ -694,7 +694,7 @@ def raises(expected_exception, *args, **kwargs):
|
||||||
return RaisesContext(expected_exception, message, match_expr)
|
return RaisesContext(expected_exception, message, match_expr)
|
||||||
elif isinstance(args[0], str):
|
elif isinstance(args[0], str):
|
||||||
warnings.warn(deprecated.RAISES_EXEC, stacklevel=2)
|
warnings.warn(deprecated.RAISES_EXEC, stacklevel=2)
|
||||||
code, = args
|
(code,) = args
|
||||||
assert isinstance(code, str)
|
assert isinstance(code, str)
|
||||||
frame = sys._getframe(1)
|
frame = sys._getframe(1)
|
||||||
loc = frame.f_locals.copy()
|
loc = frame.f_locals.copy()
|
||||||
|
|
|
@ -95,7 +95,7 @@ def warns(expected_warning, *args, **kwargs):
|
||||||
return WarningsChecker(expected_warning, match_expr=match_expr)
|
return WarningsChecker(expected_warning, match_expr=match_expr)
|
||||||
elif isinstance(args[0], str):
|
elif isinstance(args[0], str):
|
||||||
warnings.warn(WARNS_EXEC, stacklevel=2)
|
warnings.warn(WARNS_EXEC, stacklevel=2)
|
||||||
code, = args
|
(code,) = args
|
||||||
assert isinstance(code, str)
|
assert isinstance(code, str)
|
||||||
frame = sys._getframe(1)
|
frame = sys._getframe(1)
|
||||||
loc = frame.f_locals.copy()
|
loc = frame.f_locals.copy()
|
||||||
|
|
|
@ -29,6 +29,7 @@ class StepwisePlugin:
|
||||||
self.config = config
|
self.config = config
|
||||||
self.active = config.getvalue("stepwise")
|
self.active = config.getvalue("stepwise")
|
||||||
self.session = None
|
self.session = None
|
||||||
|
self.report_status = ""
|
||||||
|
|
||||||
if self.active:
|
if self.active:
|
||||||
self.lastfailed = config.cache.get("cache/stepwise", None)
|
self.lastfailed = config.cache.get("cache/stepwise", None)
|
||||||
|
@ -70,15 +71,8 @@ class StepwisePlugin:
|
||||||
|
|
||||||
config.hook.pytest_deselected(items=already_passed)
|
config.hook.pytest_deselected(items=already_passed)
|
||||||
|
|
||||||
def pytest_collectreport(self, report):
|
|
||||||
if self.active and report.failed:
|
|
||||||
self.session.shouldstop = (
|
|
||||||
"Error when collecting test, stopping test execution."
|
|
||||||
)
|
|
||||||
|
|
||||||
def pytest_runtest_logreport(self, report):
|
def pytest_runtest_logreport(self, report):
|
||||||
# Skip this hook if plugin is not active or the test is xfailed.
|
if not self.active:
|
||||||
if not self.active or "xfail" in report.keywords:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if report.failed:
|
if report.failed:
|
||||||
|
@ -104,7 +98,7 @@ class StepwisePlugin:
|
||||||
self.lastfailed = None
|
self.lastfailed = None
|
||||||
|
|
||||||
def pytest_report_collectionfinish(self):
|
def pytest_report_collectionfinish(self):
|
||||||
if self.active and self.config.getoption("verbose") >= 0:
|
if self.active and self.config.getoption("verbose") >= 0 and self.report_status:
|
||||||
return "stepwise: %s" % self.report_status
|
return "stepwise: %s" % self.report_status
|
||||||
|
|
||||||
def pytest_sessionfinish(self, session):
|
def pytest_sessionfinish(self, session):
|
||||||
|
|
|
@ -166,7 +166,11 @@ def getreportopt(config):
|
||||||
reportchars += "w"
|
reportchars += "w"
|
||||||
elif config.option.disable_warnings and "w" in reportchars:
|
elif config.option.disable_warnings and "w" in reportchars:
|
||||||
reportchars = reportchars.replace("w", "")
|
reportchars = reportchars.replace("w", "")
|
||||||
|
aliases = {"F", "S"}
|
||||||
for char in reportchars:
|
for char in reportchars:
|
||||||
|
# handle old aliases
|
||||||
|
if char in aliases:
|
||||||
|
char = char.lower()
|
||||||
if char == "a":
|
if char == "a":
|
||||||
reportopts = "sxXwEf"
|
reportopts = "sxXwEf"
|
||||||
elif char == "A":
|
elif char == "A":
|
||||||
|
@ -179,15 +183,18 @@ def getreportopt(config):
|
||||||
|
|
||||||
@pytest.hookimpl(trylast=True) # after _pytest.runner
|
@pytest.hookimpl(trylast=True) # after _pytest.runner
|
||||||
def pytest_report_teststatus(report):
|
def pytest_report_teststatus(report):
|
||||||
|
letter = "F"
|
||||||
if report.passed:
|
if report.passed:
|
||||||
letter = "."
|
letter = "."
|
||||||
elif report.skipped:
|
elif report.skipped:
|
||||||
letter = "s"
|
letter = "s"
|
||||||
elif report.failed:
|
|
||||||
letter = "F"
|
outcome = report.outcome
|
||||||
if report.when != "call":
|
if report.when in ("collect", "setup", "teardown") and outcome == "failed":
|
||||||
letter = "f"
|
outcome = "error"
|
||||||
return report.outcome, letter, report.outcome.upper()
|
letter = "E"
|
||||||
|
|
||||||
|
return outcome, letter, outcome.upper()
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
@attr.s
|
||||||
|
@ -935,9 +942,7 @@ class TerminalReporter(object):
|
||||||
"x": show_xfailed,
|
"x": show_xfailed,
|
||||||
"X": show_xpassed,
|
"X": show_xpassed,
|
||||||
"f": partial(show_simple, "failed"),
|
"f": partial(show_simple, "failed"),
|
||||||
"F": partial(show_simple, "failed"),
|
|
||||||
"s": show_skipped,
|
"s": show_skipped,
|
||||||
"S": show_skipped,
|
|
||||||
"p": partial(show_simple, "passed"),
|
"p": partial(show_simple, "passed"),
|
||||||
"E": partial(show_simple, "error"),
|
"E": partial(show_simple, "error"),
|
||||||
}
|
}
|
||||||
|
|
|
@ -114,6 +114,7 @@ class TestCaseFunction(Function):
|
||||||
def setup(self):
|
def setup(self):
|
||||||
self._testcase = self.parent.obj(self.name)
|
self._testcase = self.parent.obj(self.name)
|
||||||
self._fix_unittest_skip_decorator()
|
self._fix_unittest_skip_decorator()
|
||||||
|
self._obj = getattr(self._testcase, self.name)
|
||||||
if hasattr(self, "_request"):
|
if hasattr(self, "_request"):
|
||||||
self._request._fillfixtures()
|
self._request._fillfixtures()
|
||||||
|
|
||||||
|
@ -132,6 +133,7 @@ class TestCaseFunction(Function):
|
||||||
|
|
||||||
def teardown(self):
|
def teardown(self):
|
||||||
self._testcase = None
|
self._testcase = None
|
||||||
|
self._obj = None
|
||||||
|
|
||||||
def startTest(self, testcase):
|
def startTest(self, testcase):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -9,11 +9,11 @@ import textwrap
|
||||||
import types
|
import types
|
||||||
|
|
||||||
import attr
|
import attr
|
||||||
import importlib_metadata
|
|
||||||
import py
|
import py
|
||||||
import six
|
import six
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from _pytest.compat import importlib_metadata
|
||||||
from _pytest.main import EXIT_NOTESTSCOLLECTED
|
from _pytest.main import EXIT_NOTESTSCOLLECTED
|
||||||
from _pytest.main import EXIT_USAGEERROR
|
from _pytest.main import EXIT_USAGEERROR
|
||||||
from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG
|
from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG
|
||||||
|
@ -223,7 +223,7 @@ class TestGeneralUsage(object):
|
||||||
"conftest.py:2: in foo",
|
"conftest.py:2: in foo",
|
||||||
" import qwerty",
|
" import qwerty",
|
||||||
"E {}: No module named {q}qwerty{q}".format(
|
"E {}: No module named {q}qwerty{q}".format(
|
||||||
exc_name, q="'" if six.PY3 else ""
|
exc_name, q="" if six.PY2 else "'"
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
@ -501,7 +501,7 @@ def test_getfslineno():
|
||||||
class B(object):
|
class B(object):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
B.__name__ = "B2"
|
B.__name__ = B.__qualname__ = "B2"
|
||||||
assert getfslineno(B)[1] == -1
|
assert getfslineno(B)[1] == -1
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,21 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
import sys
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
if sys.gettrace():
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def restore_tracing():
|
||||||
|
"""Restore tracing function (when run with Coverage.py).
|
||||||
|
|
||||||
|
https://bugs.python.org/issue37011
|
||||||
|
"""
|
||||||
|
orig_trace = sys.gettrace()
|
||||||
|
yield
|
||||||
|
if sys.gettrace() != orig_trace:
|
||||||
|
sys.settrace(orig_trace)
|
||||||
|
|
||||||
|
|
||||||
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
|
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
|
||||||
def pytest_collection_modifyitems(config, items):
|
def pytest_collection_modifyitems(config, items):
|
||||||
|
|
|
@ -152,7 +152,7 @@ def test_pytest_plugins_in_non_top_level_conftest_unsupported_pyargs(
|
||||||
|
|
||||||
|
|
||||||
def test_pytest_plugins_in_non_top_level_conftest_unsupported_no_top_level_conftest(
|
def test_pytest_plugins_in_non_top_level_conftest_unsupported_no_top_level_conftest(
|
||||||
testdir
|
testdir,
|
||||||
):
|
):
|
||||||
from _pytest.deprecated import PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST
|
from _pytest.deprecated import PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST
|
||||||
|
|
||||||
|
@ -181,7 +181,7 @@ def test_pytest_plugins_in_non_top_level_conftest_unsupported_no_top_level_conft
|
||||||
|
|
||||||
|
|
||||||
def test_pytest_plugins_in_non_top_level_conftest_unsupported_no_false_positives(
|
def test_pytest_plugins_in_non_top_level_conftest_unsupported_no_false_positives(
|
||||||
testdir
|
testdir,
|
||||||
):
|
):
|
||||||
from _pytest.deprecated import PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST
|
from _pytest.deprecated import PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST
|
||||||
|
|
||||||
|
|
|
@ -921,13 +921,44 @@ def test_collection_live_logging(testdir):
|
||||||
|
|
||||||
result = testdir.runpytest("--log-cli-level=INFO")
|
result = testdir.runpytest("--log-cli-level=INFO")
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
|
["*--- live log collection ---*", "*Normal message*", "collected 0 items"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("verbose", ["", "-q", "-qq"])
|
||||||
|
def test_collection_collect_only_live_logging(testdir, verbose):
|
||||||
|
testdir.makepyfile(
|
||||||
|
"""
|
||||||
|
def test_simple():
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
result = testdir.runpytest("--collect-only", "--log-cli-level=INFO", verbose)
|
||||||
|
|
||||||
|
expected_lines = []
|
||||||
|
|
||||||
|
if not verbose:
|
||||||
|
expected_lines.extend(
|
||||||
[
|
[
|
||||||
"collecting*",
|
"*collected 1 item*",
|
||||||
"*--- live log collection ---*",
|
"*<Module test_collection_collect_only_live_logging.py>*",
|
||||||
"*Normal message*",
|
"*no tests ran*",
|
||||||
"collected 0 items",
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
elif verbose == "-q":
|
||||||
|
assert "collected 1 item*" not in result.stdout.str()
|
||||||
|
expected_lines.extend(
|
||||||
|
[
|
||||||
|
"*test_collection_collect_only_live_logging.py::test_simple*",
|
||||||
|
"no tests ran in * seconds",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
elif verbose == "-qq":
|
||||||
|
assert "collected 1 item*" not in result.stdout.str()
|
||||||
|
expected_lines.extend(["*test_collection_collect_only_live_logging.py: 1*"])
|
||||||
|
|
||||||
|
result.stdout.fnmatch_lines(expected_lines)
|
||||||
|
|
||||||
|
|
||||||
def test_collection_logging_to_file(testdir):
|
def test_collection_logging_to_file(testdir):
|
||||||
|
|
|
@ -464,7 +464,7 @@ class TestRequestBasic(object):
|
||||||
assert repr(req).find(req.function.__name__) != -1
|
assert repr(req).find(req.function.__name__) != -1
|
||||||
|
|
||||||
def test_request_attributes_method(self, testdir):
|
def test_request_attributes_method(self, testdir):
|
||||||
item, = testdir.getitems(
|
(item,) = testdir.getitems(
|
||||||
"""
|
"""
|
||||||
import pytest
|
import pytest
|
||||||
class TestB(object):
|
class TestB(object):
|
||||||
|
@ -492,7 +492,7 @@ class TestRequestBasic(object):
|
||||||
pass
|
pass
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
item1, = testdir.genitems([modcol])
|
(item1,) = testdir.genitems([modcol])
|
||||||
assert item1.name == "test_method"
|
assert item1.name == "test_method"
|
||||||
arg2fixturedefs = fixtures.FixtureRequest(item1)._arg2fixturedefs
|
arg2fixturedefs = fixtures.FixtureRequest(item1)._arg2fixturedefs
|
||||||
assert len(arg2fixturedefs) == 1
|
assert len(arg2fixturedefs) == 1
|
||||||
|
@ -756,7 +756,7 @@ class TestRequestBasic(object):
|
||||||
|
|
||||||
def test_request_getmodulepath(self, testdir):
|
def test_request_getmodulepath(self, testdir):
|
||||||
modcol = testdir.getmodulecol("def test_somefunc(): pass")
|
modcol = testdir.getmodulecol("def test_somefunc(): pass")
|
||||||
item, = testdir.genitems([modcol])
|
(item,) = testdir.genitems([modcol])
|
||||||
req = fixtures.FixtureRequest(item)
|
req = fixtures.FixtureRequest(item)
|
||||||
assert req.fspath == modcol.fspath
|
assert req.fspath == modcol.fspath
|
||||||
|
|
||||||
|
|
|
@ -1765,3 +1765,16 @@ class TestMarkersWithParametrization(object):
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
["*test_func_a*0*PASS*", "*test_func_a*2*PASS*", "*test_func_b*10*PASS*"]
|
["*test_func_a*0*PASS*", "*test_func_a*2*PASS*", "*test_func_b*10*PASS*"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_parametrize_positional_args(self, testdir):
|
||||||
|
testdir.makepyfile(
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("a", [1], False)
|
||||||
|
def test_foo(a):
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = testdir.runpytest()
|
||||||
|
result.assert_outcomes(passed=1)
|
||||||
|
|
|
@ -4,6 +4,7 @@ import sys
|
||||||
import six
|
import six
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from _pytest.compat import dummy_context_manager
|
||||||
from _pytest.outcomes import Failed
|
from _pytest.outcomes import Failed
|
||||||
from _pytest.warning_types import PytestDeprecationWarning
|
from _pytest.warning_types import PytestDeprecationWarning
|
||||||
|
|
||||||
|
@ -220,7 +221,7 @@ class TestRaises(object):
|
||||||
int("asdf")
|
int("asdf")
|
||||||
|
|
||||||
msg = "with base 16"
|
msg = "with base 16"
|
||||||
expr = r"Pattern '{}' not found in 'invalid literal for int\(\) with base 10: 'asdf''".format(
|
expr = r"Pattern '{}' not found in \"invalid literal for int\(\) with base 10: 'asdf'\"".format(
|
||||||
msg
|
msg
|
||||||
)
|
)
|
||||||
with pytest.raises(AssertionError, match=expr):
|
with pytest.raises(AssertionError, match=expr):
|
||||||
|
@ -278,3 +279,47 @@ class TestRaises(object):
|
||||||
with pytest.raises(CrappyClass()):
|
with pytest.raises(CrappyClass()):
|
||||||
pass
|
pass
|
||||||
assert "via __class__" in excinfo.value.args[0]
|
assert "via __class__" in excinfo.value.args[0]
|
||||||
|
|
||||||
|
|
||||||
|
class TestUnicodeHandling:
|
||||||
|
"""Test various combinations of bytes and unicode with pytest.raises (#5478)
|
||||||
|
|
||||||
|
https://github.com/pytest-dev/pytest/pull/5479#discussion_r298852433
|
||||||
|
"""
|
||||||
|
|
||||||
|
success = dummy_context_manager
|
||||||
|
py2_only = pytest.mark.skipif(
|
||||||
|
not six.PY2, reason="bytes in raises only supported in Python 2"
|
||||||
|
)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"message, match, expectation",
|
||||||
|
[
|
||||||
|
(u"\u2603", u"\u2603", success()),
|
||||||
|
(u"\u2603", u"\u2603foo", pytest.raises(AssertionError)),
|
||||||
|
pytest.param(b"hello", b"hello", success(), marks=py2_only),
|
||||||
|
pytest.param(
|
||||||
|
b"hello", b"world", pytest.raises(AssertionError), marks=py2_only
|
||||||
|
),
|
||||||
|
pytest.param(u"hello", b"hello", success(), marks=py2_only),
|
||||||
|
pytest.param(
|
||||||
|
u"hello", b"world", pytest.raises(AssertionError), marks=py2_only
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
u"😊".encode("UTF-8"),
|
||||||
|
b"world",
|
||||||
|
pytest.raises(AssertionError),
|
||||||
|
marks=py2_only,
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
u"world",
|
||||||
|
u"😊".encode("UTF-8"),
|
||||||
|
pytest.raises(AssertionError),
|
||||||
|
marks=py2_only,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_handling(self, message, match, expectation):
|
||||||
|
with expectation:
|
||||||
|
with pytest.raises(RuntimeError, match=match):
|
||||||
|
raise RuntimeError(message)
|
||||||
|
|
|
@ -14,6 +14,7 @@ import pytest
|
||||||
from _pytest import outcomes
|
from _pytest import outcomes
|
||||||
from _pytest.assertion import truncate
|
from _pytest.assertion import truncate
|
||||||
from _pytest.assertion import util
|
from _pytest.assertion import util
|
||||||
|
from _pytest.compat import ATTRS_EQ_FIELD
|
||||||
|
|
||||||
PY3 = sys.version_info >= (3, 0)
|
PY3 = sys.version_info >= (3, 0)
|
||||||
|
|
||||||
|
@ -179,7 +180,8 @@ class TestImportHookInstallation(object):
|
||||||
return check
|
return check
|
||||||
""",
|
""",
|
||||||
"mainwrapper.py": """\
|
"mainwrapper.py": """\
|
||||||
import pytest, importlib_metadata
|
import pytest
|
||||||
|
from _pytest.compat import importlib_metadata
|
||||||
|
|
||||||
class DummyEntryPoint(object):
|
class DummyEntryPoint(object):
|
||||||
name = 'spam'
|
name = 'spam'
|
||||||
|
@ -687,7 +689,7 @@ class TestAssert_reprcompare_attrsclass(object):
|
||||||
@attr.s
|
@attr.s
|
||||||
class SimpleDataObject(object):
|
class SimpleDataObject(object):
|
||||||
field_a = attr.ib()
|
field_a = attr.ib()
|
||||||
field_b = attr.ib(cmp=False)
|
field_b = attr.ib(**{ATTRS_EQ_FIELD: False})
|
||||||
|
|
||||||
left = SimpleDataObject(1, "b")
|
left = SimpleDataObject(1, "b")
|
||||||
right = SimpleDataObject(1, "b")
|
right = SimpleDataObject(1, "b")
|
||||||
|
|
|
@ -656,12 +656,6 @@ class TestAssertionRewrite(object):
|
||||||
else:
|
else:
|
||||||
assert lines == ["assert 0 == 1\n + where 1 = \\n{ \\n~ \\n}.a"]
|
assert lines == ["assert 0 == 1\n + where 1 = \\n{ \\n~ \\n}.a"]
|
||||||
|
|
||||||
def test_unroll_expression(self):
|
|
||||||
def f():
|
|
||||||
assert all(x == 1 for x in range(10))
|
|
||||||
|
|
||||||
assert "0 == 1" in getmsg(f)
|
|
||||||
|
|
||||||
def test_custom_repr_non_ascii(self):
|
def test_custom_repr_non_ascii(self):
|
||||||
def f():
|
def f():
|
||||||
class A(object):
|
class A(object):
|
||||||
|
@ -677,53 +671,6 @@ class TestAssertionRewrite(object):
|
||||||
assert "UnicodeDecodeError" not in msg
|
assert "UnicodeDecodeError" not in msg
|
||||||
assert "UnicodeEncodeError" not in msg
|
assert "UnicodeEncodeError" not in msg
|
||||||
|
|
||||||
def test_unroll_generator(self, testdir):
|
|
||||||
testdir.makepyfile(
|
|
||||||
"""
|
|
||||||
def check_even(num):
|
|
||||||
if num % 2 == 0:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def test_generator():
|
|
||||||
odd_list = list(range(1,9,2))
|
|
||||||
assert all(check_even(num) for num in odd_list)"""
|
|
||||||
)
|
|
||||||
result = testdir.runpytest()
|
|
||||||
result.stdout.fnmatch_lines(["*assert False*", "*where False = check_even(1)*"])
|
|
||||||
|
|
||||||
def test_unroll_list_comprehension(self, testdir):
|
|
||||||
testdir.makepyfile(
|
|
||||||
"""
|
|
||||||
def check_even(num):
|
|
||||||
if num % 2 == 0:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def test_list_comprehension():
|
|
||||||
odd_list = list(range(1,9,2))
|
|
||||||
assert all([check_even(num) for num in odd_list])"""
|
|
||||||
)
|
|
||||||
result = testdir.runpytest()
|
|
||||||
result.stdout.fnmatch_lines(["*assert False*", "*where False = check_even(1)*"])
|
|
||||||
|
|
||||||
def test_for_loop(self, testdir):
|
|
||||||
testdir.makepyfile(
|
|
||||||
"""
|
|
||||||
def check_even(num):
|
|
||||||
if num % 2 == 0:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def test_for_loop():
|
|
||||||
odd_list = list(range(1,9,2))
|
|
||||||
for num in odd_list:
|
|
||||||
assert check_even(num)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
result = testdir.runpytest()
|
|
||||||
result.stdout.fnmatch_lines(["*assert False*", "*where False = check_even(1)*"])
|
|
||||||
|
|
||||||
|
|
||||||
class TestRewriteOnImport(object):
|
class TestRewriteOnImport(object):
|
||||||
def test_pycache_is_a_file(self, testdir):
|
def test_pycache_is_a_file(self, testdir):
|
||||||
|
|
|
@ -494,7 +494,7 @@ class TestSession(object):
|
||||||
p = testdir.makepyfile("def test_func(): pass")
|
p = testdir.makepyfile("def test_func(): pass")
|
||||||
id = "::".join([p.basename, "test_func"])
|
id = "::".join([p.basename, "test_func"])
|
||||||
items, hookrec = testdir.inline_genitems(id)
|
items, hookrec = testdir.inline_genitems(id)
|
||||||
item, = items
|
(item,) = items
|
||||||
assert item.name == "test_func"
|
assert item.name == "test_func"
|
||||||
newid = item.nodeid
|
newid = item.nodeid
|
||||||
assert newid == id
|
assert newid == id
|
||||||
|
@ -613,9 +613,9 @@ class TestSession(object):
|
||||||
testdir.makepyfile("def test_func(): pass")
|
testdir.makepyfile("def test_func(): pass")
|
||||||
items, hookrec = testdir.inline_genitems()
|
items, hookrec = testdir.inline_genitems()
|
||||||
assert len(items) == 1
|
assert len(items) == 1
|
||||||
item, = items
|
(item,) = items
|
||||||
items2, hookrec = testdir.inline_genitems(item.nodeid)
|
items2, hookrec = testdir.inline_genitems(item.nodeid)
|
||||||
item2, = items2
|
(item2,) = items2
|
||||||
assert item2.name == item.name
|
assert item2.name == item.name
|
||||||
assert item2.fspath == item.fspath
|
assert item2.fspath == item.fspath
|
||||||
|
|
||||||
|
@ -630,7 +630,7 @@ class TestSession(object):
|
||||||
arg = p.basename + "::TestClass::test_method"
|
arg = p.basename + "::TestClass::test_method"
|
||||||
items, hookrec = testdir.inline_genitems(arg)
|
items, hookrec = testdir.inline_genitems(arg)
|
||||||
assert len(items) == 1
|
assert len(items) == 1
|
||||||
item, = items
|
(item,) = items
|
||||||
assert item.nodeid.endswith("TestClass::test_method")
|
assert item.nodeid.endswith("TestClass::test_method")
|
||||||
# ensure we are reporting the collection of the single test item (#2464)
|
# ensure we are reporting the collection of the single test item (#2464)
|
||||||
assert [x.name for x in self.get_reported_items(hookrec)] == ["test_method"]
|
assert [x.name for x in self.get_reported_items(hookrec)] == ["test_method"]
|
||||||
|
@ -1211,6 +1211,18 @@ def test_collect_pkg_init_and_file_in_args(testdir):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_collect_pkg_init_only(testdir):
|
||||||
|
subdir = testdir.mkdir("sub")
|
||||||
|
init = subdir.ensure("__init__.py")
|
||||||
|
init.write("def test_init(): pass")
|
||||||
|
|
||||||
|
result = testdir.runpytest(str(init))
|
||||||
|
result.stdout.fnmatch_lines(["*no tests ran in*"])
|
||||||
|
|
||||||
|
result = testdir.runpytest("-v", "-o", "python_files=*.py", str(init))
|
||||||
|
result.stdout.fnmatch_lines(["sub/__init__.py::test_init PASSED*", "*1 passed in*"])
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
not hasattr(py.path.local, "mksymlinkto"),
|
not hasattr(py.path.local, "mksymlinkto"),
|
||||||
reason="symlink not available on this platform",
|
reason="symlink not available on this platform",
|
||||||
|
|
|
@ -6,19 +6,20 @@ from __future__ import print_function
|
||||||
import sys
|
import sys
|
||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
import importlib_metadata
|
|
||||||
|
|
||||||
import _pytest._code
|
import _pytest._code
|
||||||
import pytest
|
import pytest
|
||||||
|
from _pytest.compat import importlib_metadata
|
||||||
from _pytest.config import _iter_rewritable_modules
|
from _pytest.config import _iter_rewritable_modules
|
||||||
from _pytest.config.exceptions import UsageError
|
from _pytest.config.exceptions import UsageError
|
||||||
from _pytest.config.findpaths import determine_setup
|
from _pytest.config.findpaths import determine_setup
|
||||||
from _pytest.config.findpaths import get_common_ancestor
|
from _pytest.config.findpaths import get_common_ancestor
|
||||||
from _pytest.config.findpaths import getcfg
|
from _pytest.config.findpaths import getcfg
|
||||||
|
from _pytest.main import EXIT_INTERRUPTED
|
||||||
from _pytest.main import EXIT_NOTESTSCOLLECTED
|
from _pytest.main import EXIT_NOTESTSCOLLECTED
|
||||||
from _pytest.main import EXIT_OK
|
from _pytest.main import EXIT_OK
|
||||||
from _pytest.main import EXIT_TESTSFAILED
|
from _pytest.main import EXIT_TESTSFAILED
|
||||||
from _pytest.main import EXIT_USAGEERROR
|
from _pytest.main import EXIT_USAGEERROR
|
||||||
|
from _pytest.pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
class TestParseIni(object):
|
class TestParseIni(object):
|
||||||
|
@ -130,6 +131,12 @@ class TestParseIni(object):
|
||||||
config = testdir.parseconfigure(sub)
|
config = testdir.parseconfigure(sub)
|
||||||
assert config.getini("minversion") == "2.0"
|
assert config.getini("minversion") == "2.0"
|
||||||
|
|
||||||
|
def test_ini_parse_error(self, testdir):
|
||||||
|
testdir.tmpdir.join("pytest.ini").write("addopts = -x")
|
||||||
|
result = testdir.runpytest()
|
||||||
|
assert result.ret != 0
|
||||||
|
result.stderr.fnmatch_lines(["ERROR: *pytest.ini:1: no section header defined"])
|
||||||
|
|
||||||
@pytest.mark.xfail(reason="probably not needed")
|
@pytest.mark.xfail(reason="probably not needed")
|
||||||
def test_confcutdir(self, testdir):
|
def test_confcutdir(self, testdir):
|
||||||
sub = testdir.mkdir("sub")
|
sub = testdir.mkdir("sub")
|
||||||
|
@ -425,15 +432,21 @@ class TestConfigAPI(object):
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"names, expected",
|
"names, expected",
|
||||||
[
|
[
|
||||||
|
# dist-info based distributions root are files as will be put in PYTHONPATH
|
||||||
(["bar.py"], ["bar"]),
|
(["bar.py"], ["bar"]),
|
||||||
(["foo", "bar.py"], []),
|
(["foo/bar.py"], ["bar"]),
|
||||||
(["foo", "bar.pyc"], []),
|
(["foo/bar.pyc"], []),
|
||||||
(["foo", "__init__.py"], ["foo"]),
|
(["foo/__init__.py"], ["foo"]),
|
||||||
(["foo", "bar", "__init__.py"], []),
|
(["bar/__init__.py", "xz.py"], ["bar", "xz"]),
|
||||||
|
(["setup.py"], []),
|
||||||
|
# egg based distributions root contain the files from the dist root
|
||||||
|
(["src/bar/__init__.py"], ["bar"]),
|
||||||
|
(["src/bar/__init__.py", "setup.py"], ["bar"]),
|
||||||
|
(["source/python/bar/__init__.py", "setup.py"], ["bar"]),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_iter_rewritable_modules(self, names, expected):
|
def test_iter_rewritable_modules(self, names, expected):
|
||||||
assert list(_iter_rewritable_modules(["/".join(names)])) == expected
|
assert list(_iter_rewritable_modules(names)) == expected
|
||||||
|
|
||||||
|
|
||||||
class TestConfigFromdictargs(object):
|
class TestConfigFromdictargs(object):
|
||||||
|
@ -586,6 +599,29 @@ def test_setuptools_importerror_issue1479(testdir, monkeypatch):
|
||||||
testdir.parseconfig()
|
testdir.parseconfig()
|
||||||
|
|
||||||
|
|
||||||
|
def test_importlib_metadata_broken_distribution(testdir, monkeypatch):
|
||||||
|
"""Integration test for broken distributions with 'files' metadata being None (#5389)"""
|
||||||
|
monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
|
||||||
|
|
||||||
|
class DummyEntryPoint:
|
||||||
|
name = "mytestplugin"
|
||||||
|
group = "pytest11"
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
return object()
|
||||||
|
|
||||||
|
class Distribution:
|
||||||
|
version = "1.0"
|
||||||
|
files = None
|
||||||
|
entry_points = (DummyEntryPoint(),)
|
||||||
|
|
||||||
|
def distributions():
|
||||||
|
return (Distribution(),)
|
||||||
|
|
||||||
|
monkeypatch.setattr(importlib_metadata, "distributions", distributions)
|
||||||
|
testdir.parseconfig()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("block_it", [True, False])
|
@pytest.mark.parametrize("block_it", [True, False])
|
||||||
def test_plugin_preparse_prevents_setuptools_loading(testdir, monkeypatch, block_it):
|
def test_plugin_preparse_prevents_setuptools_loading(testdir, monkeypatch, block_it):
|
||||||
monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
|
monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
|
||||||
|
@ -729,7 +765,7 @@ def test_config_in_subdirectory_colon_command_line_issue2148(testdir):
|
||||||
**{
|
**{
|
||||||
"conftest": conftest_source,
|
"conftest": conftest_source,
|
||||||
"subdir/conftest": conftest_source,
|
"subdir/conftest": conftest_source,
|
||||||
"subdir/test_foo": """
|
"subdir/test_foo": """\
|
||||||
def test_foo(pytestconfig):
|
def test_foo(pytestconfig):
|
||||||
assert pytestconfig.getini('foo') == 'subdir'
|
assert pytestconfig.getini('foo') == 'subdir'
|
||||||
""",
|
""",
|
||||||
|
@ -765,6 +801,12 @@ def test_notify_exception(testdir, capfd):
|
||||||
assert "ValueError" in err
|
assert "ValueError" in err
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_terminal_discovery_error(testdir):
|
||||||
|
testdir.makepyfile("raise TypeError('oops!')")
|
||||||
|
result = testdir.runpytest("-p", "no:terminal", "--collect-only")
|
||||||
|
assert result.ret == EXIT_INTERRUPTED
|
||||||
|
|
||||||
|
|
||||||
def test_load_initial_conftest_last_ordering(testdir, _config_for_test):
|
def test_load_initial_conftest_last_ordering(testdir, _config_for_test):
|
||||||
pm = _config_for_test.pluginmanager
|
pm = _config_for_test.pluginmanager
|
||||||
|
|
||||||
|
@ -1181,6 +1223,29 @@ def test_config_does_not_load_blocked_plugin_from_args(testdir):
|
||||||
assert result.ret == EXIT_USAGEERROR
|
assert result.ret == EXIT_USAGEERROR
|
||||||
|
|
||||||
|
|
||||||
|
def test_invocation_args(testdir):
|
||||||
|
"""Ensure that Config.invocation_* arguments are correctly defined"""
|
||||||
|
|
||||||
|
class DummyPlugin(object):
|
||||||
|
pass
|
||||||
|
|
||||||
|
p = testdir.makepyfile("def test(): pass")
|
||||||
|
plugin = DummyPlugin()
|
||||||
|
rec = testdir.inline_run(p, "-v", plugins=[plugin])
|
||||||
|
calls = rec.getcalls("pytest_runtest_protocol")
|
||||||
|
assert len(calls) == 1
|
||||||
|
call = calls[0]
|
||||||
|
config = call.item.config
|
||||||
|
|
||||||
|
assert config.invocation_params.args == [p, "-v"]
|
||||||
|
assert config.invocation_params.dir == Path(str(testdir.tmpdir))
|
||||||
|
|
||||||
|
plugins = config.invocation_params.plugins
|
||||||
|
assert len(plugins) == 2
|
||||||
|
assert plugins[0] is plugin
|
||||||
|
assert type(plugins[1]).__name__ == "Collect" # installed by testdir.inline_run()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"plugin",
|
"plugin",
|
||||||
[
|
[
|
||||||
|
|
|
@ -3,11 +3,14 @@ from __future__ import absolute_import
|
||||||
from __future__ import division
|
from __future__ import division
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import inspect
|
||||||
import sys
|
import sys
|
||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from _pytest.compat import MODULE_NOT_FOUND_ERROR
|
from _pytest.compat import MODULE_NOT_FOUND_ERROR
|
||||||
|
from _pytest.doctest import _is_mocked
|
||||||
|
from _pytest.doctest import _patch_unwrap_mock_aware
|
||||||
from _pytest.doctest import DoctestItem
|
from _pytest.doctest import DoctestItem
|
||||||
from _pytest.doctest import DoctestModule
|
from _pytest.doctest import DoctestModule
|
||||||
from _pytest.doctest import DoctestTextfile
|
from _pytest.doctest import DoctestTextfile
|
||||||
|
@ -1237,3 +1240,25 @@ def test_doctest_mock_objects_dont_recurse_missbehaved(mock_module, testdir):
|
||||||
)
|
)
|
||||||
result = testdir.runpytest("--doctest-modules")
|
result = testdir.runpytest("--doctest-modules")
|
||||||
result.stdout.fnmatch_lines(["* 1 passed *"])
|
result.stdout.fnmatch_lines(["* 1 passed *"])
|
||||||
|
|
||||||
|
|
||||||
|
class Broken:
|
||||||
|
def __getattr__(self, _):
|
||||||
|
raise KeyError("This should be an AttributeError")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not hasattr(inspect, "unwrap"), reason="nothing to patch")
|
||||||
|
@pytest.mark.parametrize( # pragma: no branch (lambdas are not called)
|
||||||
|
"stop", [None, _is_mocked, lambda f: None, lambda f: False, lambda f: True]
|
||||||
|
)
|
||||||
|
def test_warning_on_unwrap_of_broken_object(stop):
|
||||||
|
bad_instance = Broken()
|
||||||
|
assert inspect.unwrap.__module__ == "inspect"
|
||||||
|
with _patch_unwrap_mock_aware():
|
||||||
|
assert inspect.unwrap.__module__ != "inspect"
|
||||||
|
with pytest.warns(
|
||||||
|
pytest.PytestWarning, match="^Got KeyError.* when unwrapping"
|
||||||
|
):
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
inspect.unwrap(bad_instance, stop=stop)
|
||||||
|
assert inspect.unwrap.__module__ == "inspect"
|
||||||
|
|
|
@ -3,7 +3,7 @@ from __future__ import absolute_import
|
||||||
from __future__ import division
|
from __future__ import division
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
|
|
||||||
import importlib_metadata
|
from _pytest.compat import importlib_metadata
|
||||||
|
|
||||||
|
|
||||||
def test_pytest_entry_points_are_identical():
|
def test_pytest_entry_points_are_identical():
|
||||||
|
|
|
@ -4,7 +4,9 @@ from __future__ import division
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import platform
|
||||||
import sys
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
from xml.dom import minidom
|
from xml.dom import minidom
|
||||||
|
|
||||||
import py
|
import py
|
||||||
|
@ -47,6 +49,16 @@ class DomNode(object):
|
||||||
def _by_tag(self, tag):
|
def _by_tag(self, tag):
|
||||||
return self.__node.getElementsByTagName(tag)
|
return self.__node.getElementsByTagName(tag)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def children(self):
|
||||||
|
return [type(self)(x) for x in self.__node.childNodes]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def get_unique_child(self):
|
||||||
|
children = self.children
|
||||||
|
assert len(children) == 1
|
||||||
|
return children[0]
|
||||||
|
|
||||||
def find_nth_by_tag(self, tag, n):
|
def find_nth_by_tag(self, tag, n):
|
||||||
items = self._by_tag(tag)
|
items = self._by_tag(tag)
|
||||||
try:
|
try:
|
||||||
|
@ -81,7 +93,7 @@ class DomNode(object):
|
||||||
return self.__node.tagName
|
return self.__node.tagName
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def next_siebling(self):
|
def next_sibling(self):
|
||||||
return type(self)(self.__node.nextSibling)
|
return type(self)(self.__node.nextSibling)
|
||||||
|
|
||||||
|
|
||||||
|
@ -135,6 +147,30 @@ class TestPython(object):
|
||||||
node = dom.find_first_by_tag("testsuite")
|
node = dom.find_first_by_tag("testsuite")
|
||||||
node.assert_attr(name="pytest", errors=1, failures=2, skipped=1, tests=5)
|
node.assert_attr(name="pytest", errors=1, failures=2, skipped=1, tests=5)
|
||||||
|
|
||||||
|
def test_hostname_in_xml(self, testdir):
|
||||||
|
testdir.makepyfile(
|
||||||
|
"""
|
||||||
|
def test_pass():
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result, dom = runandparse(testdir)
|
||||||
|
node = dom.find_first_by_tag("testsuite")
|
||||||
|
node.assert_attr(hostname=platform.node())
|
||||||
|
|
||||||
|
def test_timestamp_in_xml(self, testdir):
|
||||||
|
testdir.makepyfile(
|
||||||
|
"""
|
||||||
|
def test_pass():
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
start_time = datetime.now()
|
||||||
|
result, dom = runandparse(testdir)
|
||||||
|
node = dom.find_first_by_tag("testsuite")
|
||||||
|
timestamp = datetime.strptime(node["timestamp"], "%Y-%m-%dT%H:%M:%S.%f")
|
||||||
|
assert start_time <= timestamp < datetime.now()
|
||||||
|
|
||||||
def test_timing_function(self, testdir):
|
def test_timing_function(self, testdir):
|
||||||
testdir.makepyfile(
|
testdir.makepyfile(
|
||||||
"""
|
"""
|
||||||
|
@ -390,11 +426,11 @@ class TestPython(object):
|
||||||
fnode = tnode.find_first_by_tag("failure")
|
fnode = tnode.find_first_by_tag("failure")
|
||||||
fnode.assert_attr(message="ValueError: 42")
|
fnode.assert_attr(message="ValueError: 42")
|
||||||
assert "ValueError" in fnode.toxml()
|
assert "ValueError" in fnode.toxml()
|
||||||
systemout = fnode.next_siebling
|
systemout = fnode.next_sibling
|
||||||
assert systemout.tag == "system-out"
|
assert systemout.tag == "system-out"
|
||||||
assert "hello-stdout" in systemout.toxml()
|
assert "hello-stdout" in systemout.toxml()
|
||||||
assert "info msg" not in systemout.toxml()
|
assert "info msg" not in systemout.toxml()
|
||||||
systemerr = systemout.next_siebling
|
systemerr = systemout.next_sibling
|
||||||
assert systemerr.tag == "system-err"
|
assert systemerr.tag == "system-err"
|
||||||
assert "hello-stderr" in systemerr.toxml()
|
assert "hello-stderr" in systemerr.toxml()
|
||||||
assert "info msg" not in systemerr.toxml()
|
assert "info msg" not in systemerr.toxml()
|
||||||
|
@ -1101,6 +1137,20 @@ def test_random_report_log_xdist(testdir, monkeypatch):
|
||||||
assert failed == ["test_x[22]"]
|
assert failed == ["test_x[22]"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_root_testsuites_tag(testdir):
|
||||||
|
testdir.makepyfile(
|
||||||
|
"""
|
||||||
|
def test_x():
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
_, dom = runandparse(testdir)
|
||||||
|
root = dom.get_unique_child
|
||||||
|
assert root.tag == "testsuites"
|
||||||
|
suite_node = root.get_unique_child
|
||||||
|
assert suite_node.tag == "testsuite"
|
||||||
|
|
||||||
|
|
||||||
def test_runs_twice(testdir):
|
def test_runs_twice(testdir):
|
||||||
f = testdir.makepyfile(
|
f = testdir.makepyfile(
|
||||||
"""
|
"""
|
||||||
|
@ -1359,3 +1409,39 @@ def test_logging_passing_tests_disabled_does_not_log_test_output(testdir):
|
||||||
node = dom.find_first_by_tag("testcase")
|
node = dom.find_first_by_tag("testcase")
|
||||||
assert len(node.find_by_tag("system-err")) == 0
|
assert len(node.find_by_tag("system-err")) == 0
|
||||||
assert len(node.find_by_tag("system-out")) == 0
|
assert len(node.find_by_tag("system-out")) == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("junit_logging", ["no", "system-out", "system-err"])
|
||||||
|
def test_logging_passing_tests_disabled_logs_output_for_failing_test_issue5430(
|
||||||
|
testdir, junit_logging
|
||||||
|
):
|
||||||
|
testdir.makeini(
|
||||||
|
"""
|
||||||
|
[pytest]
|
||||||
|
junit_log_passing_tests=False
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
testdir.makepyfile(
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def test_func():
|
||||||
|
logging.warning('hello')
|
||||||
|
assert 0
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result, dom = runandparse(testdir, "-o", "junit_logging=%s" % junit_logging)
|
||||||
|
assert result.ret == 1
|
||||||
|
node = dom.find_first_by_tag("testcase")
|
||||||
|
if junit_logging == "system-out":
|
||||||
|
assert len(node.find_by_tag("system-err")) == 0
|
||||||
|
assert len(node.find_by_tag("system-out")) == 1
|
||||||
|
elif junit_logging == "system-err":
|
||||||
|
assert len(node.find_by_tag("system-err")) == 1
|
||||||
|
assert len(node.find_by_tag("system-out")) == 0
|
||||||
|
else:
|
||||||
|
assert junit_logging == "no"
|
||||||
|
assert len(node.find_by_tag("system-err")) == 0
|
||||||
|
assert len(node.find_by_tag("system-out")) == 0
|
||||||
|
|
|
@ -413,6 +413,28 @@ def test_parametrized_with_kwargs(testdir):
|
||||||
assert result.ret == 0
|
assert result.ret == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_parametrize_iterator(testdir):
|
||||||
|
"""parametrize should work with generators (#5354)."""
|
||||||
|
py_file = testdir.makepyfile(
|
||||||
|
"""\
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
def gen():
|
||||||
|
yield 1
|
||||||
|
yield 2
|
||||||
|
yield 3
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('a', gen())
|
||||||
|
def test(a):
|
||||||
|
assert a >= 1
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = testdir.runpytest(py_file)
|
||||||
|
assert result.ret == 0
|
||||||
|
# should not skip any tests
|
||||||
|
result.stdout.fnmatch_lines(["*3 passed*"])
|
||||||
|
|
||||||
|
|
||||||
class TestFunctional(object):
|
class TestFunctional(object):
|
||||||
def test_merging_markers_deep(self, testdir):
|
def test_merging_markers_deep(self, testdir):
|
||||||
# issue 199 - propagate markers into nested classes
|
# issue 199 - propagate markers into nested classes
|
||||||
|
@ -986,7 +1008,7 @@ def test_markers_from_parametrize(testdir):
|
||||||
def test_pytest_param_id_requires_string():
|
def test_pytest_param_id_requires_string():
|
||||||
with pytest.raises(TypeError) as excinfo:
|
with pytest.raises(TypeError) as excinfo:
|
||||||
pytest.param(id=True)
|
pytest.param(id=True)
|
||||||
msg, = excinfo.value.args
|
(msg,) = excinfo.value.args
|
||||||
if six.PY2:
|
if six.PY2:
|
||||||
assert msg == "Expected id to be a string, got <type 'bool'>: True"
|
assert msg == "Expected id to be a string, got <type 'bool'>: True"
|
||||||
else:
|
else:
|
||||||
|
@ -1003,7 +1025,7 @@ def test_pytest_param_warning_on_unknown_kwargs():
|
||||||
# typo, should be marks=
|
# typo, should be marks=
|
||||||
pytest.param(1, 2, mark=pytest.mark.xfail())
|
pytest.param(1, 2, mark=pytest.mark.xfail())
|
||||||
assert warninfo[0].filename == __file__
|
assert warninfo[0].filename == __file__
|
||||||
msg, = warninfo[0].message.args
|
(msg,) = warninfo[0].message.args
|
||||||
assert msg == (
|
assert msg == (
|
||||||
"pytest.param() got unexpected keyword arguments: ['mark'].\n"
|
"pytest.param() got unexpected keyword arguments: ['mark'].\n"
|
||||||
"This will be an error in future versions."
|
"This will be an error in future versions."
|
||||||
|
|
|
@ -209,7 +209,7 @@ class TestEnvironWarnings(object):
|
||||||
|
|
||||||
VAR_NAME = u"PYTEST_INTERNAL_MY_VAR"
|
VAR_NAME = u"PYTEST_INTERNAL_MY_VAR"
|
||||||
|
|
||||||
@pytest.mark.skipif(six.PY3, reason="Python 2 only test")
|
@pytest.mark.skipif(not six.PY2, reason="Python 2 only test")
|
||||||
def test_setenv_unicode_key(self, monkeypatch):
|
def test_setenv_unicode_key(self, monkeypatch):
|
||||||
with pytest.warns(
|
with pytest.warns(
|
||||||
pytest.PytestWarning,
|
pytest.PytestWarning,
|
||||||
|
@ -217,7 +217,7 @@ class TestEnvironWarnings(object):
|
||||||
):
|
):
|
||||||
monkeypatch.setenv(self.VAR_NAME, "2")
|
monkeypatch.setenv(self.VAR_NAME, "2")
|
||||||
|
|
||||||
@pytest.mark.skipif(six.PY3, reason="Python 2 only test")
|
@pytest.mark.skipif(not six.PY2, reason="Python 2 only test")
|
||||||
def test_delenv_unicode_key(self, monkeypatch):
|
def test_delenv_unicode_key(self, monkeypatch):
|
||||||
with pytest.warns(
|
with pytest.warns(
|
||||||
pytest.PytestWarning,
|
pytest.PytestWarning,
|
||||||
|
|
|
@ -74,7 +74,7 @@ class TestPasteCapture(object):
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result = testdir.runpytest("--pastebin=all")
|
result = testdir.runpytest("--pastebin=all")
|
||||||
if sys.version_info[0] == 3:
|
if sys.version_info[0] >= 3:
|
||||||
expected_msg = "*assert '☺' == 1*"
|
expected_msg = "*assert '☺' == 1*"
|
||||||
else:
|
else:
|
||||||
expected_msg = "*assert '\\xe2\\x98\\xba' == 1*"
|
expected_msg = "*assert '\\xe2\\x98\\xba' == 1*"
|
||||||
|
@ -126,7 +126,7 @@ class TestPaste(object):
|
||||||
assert len(mocked_urlopen) == 1
|
assert len(mocked_urlopen) == 1
|
||||||
url, data = mocked_urlopen[0]
|
url, data = mocked_urlopen[0]
|
||||||
assert type(data) is bytes
|
assert type(data) is bytes
|
||||||
lexer = "python3" if sys.version_info[0] == 3 else "python"
|
lexer = "text"
|
||||||
assert url == "https://bpaste.net"
|
assert url == "https://bpaste.net"
|
||||||
assert "lexer=%s" % lexer in data.decode()
|
assert "lexer=%s" % lexer in data.decode()
|
||||||
assert "code=full-paste-contents" in data.decode()
|
assert "code=full-paste-contents" in data.decode()
|
||||||
|
|
|
@ -245,8 +245,8 @@ class TestInlineRunModulesCleanup(object):
|
||||||
):
|
):
|
||||||
spy_factory = self.spy_factory()
|
spy_factory = self.spy_factory()
|
||||||
monkeypatch.setattr(pytester, "SysModulesSnapshot", spy_factory)
|
monkeypatch.setattr(pytester, "SysModulesSnapshot", spy_factory)
|
||||||
original = dict(sys.modules)
|
|
||||||
testdir.syspathinsert()
|
testdir.syspathinsert()
|
||||||
|
original = dict(sys.modules)
|
||||||
testdir.makepyfile(import1="# you son of a silly person")
|
testdir.makepyfile(import1="# you son of a silly person")
|
||||||
testdir.makepyfile(import2="# my hovercraft is full of eels")
|
testdir.makepyfile(import2="# my hovercraft is full of eels")
|
||||||
test_mod = testdir.makepyfile(
|
test_mod = testdir.makepyfile(
|
||||||
|
|
|
@ -225,7 +225,7 @@ class TestWarns(object):
|
||||||
assert len(warninfo) == 3
|
assert len(warninfo) == 3
|
||||||
for w in warninfo:
|
for w in warninfo:
|
||||||
assert w.filename == __file__
|
assert w.filename == __file__
|
||||||
msg, = w.message.args
|
(msg,) = w.message.args
|
||||||
assert msg.startswith("warns(..., 'code(as_a_string)') is deprecated")
|
assert msg.startswith("warns(..., 'code(as_a_string)') is deprecated")
|
||||||
|
|
||||||
def test_function(self):
|
def test_function(self):
|
||||||
|
|
|
@ -336,8 +336,10 @@ class BaseFunctionalTests(object):
|
||||||
assert reps[2].failed
|
assert reps[2].failed
|
||||||
assert reps[2].when == "teardown"
|
assert reps[2].when == "teardown"
|
||||||
assert reps[2].longrepr.reprcrash.message in (
|
assert reps[2].longrepr.reprcrash.message in (
|
||||||
# python3 error
|
# python3 < 3.10 error
|
||||||
"TypeError: teardown_method() missing 2 required positional arguments: 'y' and 'z'",
|
"TypeError: teardown_method() missing 2 required positional arguments: 'y' and 'z'",
|
||||||
|
# python3 >= 3.10 error
|
||||||
|
"TypeError: TestClass.teardown_method() missing 2 required positional arguments: 'y' and 'z'",
|
||||||
# python2 error
|
# python2 error
|
||||||
"TypeError: teardown_method() takes exactly 4 arguments (2 given)",
|
"TypeError: teardown_method() takes exactly 4 arguments (2 given)",
|
||||||
)
|
)
|
||||||
|
|
|
@ -136,7 +136,7 @@ class TestEvaluator(object):
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_skipif_class(self, testdir):
|
def test_skipif_class(self, testdir):
|
||||||
item, = testdir.getitems(
|
(item,) = testdir.getitems(
|
||||||
"""
|
"""
|
||||||
import pytest
|
import pytest
|
||||||
class TestClass(object):
|
class TestClass(object):
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
import sys
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@ -157,14 +159,66 @@ def test_change_testfile(stepwise_testdir):
|
||||||
assert "test_success PASSED" in stdout
|
assert "test_success PASSED" in stdout
|
||||||
|
|
||||||
|
|
||||||
def test_stop_on_collection_errors(broken_testdir):
|
@pytest.mark.parametrize("broken_first", [True, False])
|
||||||
result = broken_testdir.runpytest(
|
def test_stop_on_collection_errors(broken_testdir, broken_first):
|
||||||
"-v",
|
"""Stop during collection errors. Broken test first or broken test last
|
||||||
"--strict-markers",
|
actually surfaced a bug (#5444), so we test both situations."""
|
||||||
"--stepwise",
|
files = ["working_testfile.py", "broken_testfile.py"]
|
||||||
"working_testfile.py",
|
if broken_first:
|
||||||
"broken_testfile.py",
|
files.reverse()
|
||||||
|
result = broken_testdir.runpytest("-v", "--strict-markers", "--stepwise", *files)
|
||||||
|
result.stdout.fnmatch_lines("*errors during collection*")
|
||||||
|
|
||||||
|
|
||||||
|
def test_xfail_handling(testdir):
|
||||||
|
"""Ensure normal xfail is ignored, and strict xfail interrupts the session in sw mode
|
||||||
|
|
||||||
|
(#5547)
|
||||||
|
"""
|
||||||
|
contents = """
|
||||||
|
import pytest
|
||||||
|
def test_a(): pass
|
||||||
|
|
||||||
|
@pytest.mark.xfail(strict={strict})
|
||||||
|
def test_b(): assert {assert_value}
|
||||||
|
|
||||||
|
def test_c(): pass
|
||||||
|
def test_d(): pass
|
||||||
|
"""
|
||||||
|
testdir.makepyfile(contents.format(assert_value="0", strict="False"))
|
||||||
|
result = testdir.runpytest("--sw", "-v")
|
||||||
|
result.stdout.fnmatch_lines(
|
||||||
|
[
|
||||||
|
"*::test_a PASSED *",
|
||||||
|
"*::test_b XFAIL *",
|
||||||
|
"*::test_c PASSED *",
|
||||||
|
"*::test_d PASSED *",
|
||||||
|
"* 3 passed, 1 xfailed in *",
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
stdout = result.stdout.str()
|
testdir.makepyfile(contents.format(assert_value="1", strict="True"))
|
||||||
assert "errors during collection" in stdout
|
result = testdir.runpytest("--sw", "-v")
|
||||||
|
result.stdout.fnmatch_lines(
|
||||||
|
[
|
||||||
|
"*::test_a PASSED *",
|
||||||
|
"*::test_b FAILED *",
|
||||||
|
"* Interrupted*",
|
||||||
|
"* 1 failed, 1 passed in *",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# because we are writing to the same file, mtime might not be affected enough to
|
||||||
|
# invalidate the cache, making this next run flaky
|
||||||
|
if not sys.dont_write_bytecode:
|
||||||
|
testdir.tmpdir.join("__pycache__").remove()
|
||||||
|
testdir.makepyfile(contents.format(assert_value="0", strict="True"))
|
||||||
|
result = testdir.runpytest("--sw", "-v")
|
||||||
|
result.stdout.fnmatch_lines(
|
||||||
|
[
|
||||||
|
"*::test_b XFAIL *",
|
||||||
|
"*::test_c PASSED *",
|
||||||
|
"*::test_d PASSED *",
|
||||||
|
"* 2 passed, 1 deselected, 1 xfailed in *",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
|
@ -759,6 +759,35 @@ class TestTerminalFunctional(object):
|
||||||
result = testdir.runpytest(*params)
|
result = testdir.runpytest(*params)
|
||||||
result.stdout.fnmatch_lines(["collected 3 items", "hello from hook: 3 items"])
|
result.stdout.fnmatch_lines(["collected 3 items", "hello from hook: 3 items"])
|
||||||
|
|
||||||
|
def test_summary_f_alias(self, testdir):
|
||||||
|
"""Test that 'f' and 'F' report chars are aliases and don't show up twice in the summary (#6334)"""
|
||||||
|
testdir.makepyfile(
|
||||||
|
"""
|
||||||
|
def test():
|
||||||
|
assert False
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = testdir.runpytest("-rfF")
|
||||||
|
expected = "FAILED test_summary_f_alias.py::test - assert False"
|
||||||
|
result.stdout.fnmatch_lines([expected])
|
||||||
|
assert result.stdout.lines.count(expected) == 1
|
||||||
|
|
||||||
|
def test_summary_s_alias(self, testdir):
|
||||||
|
"""Test that 's' and 'S' report chars are aliases and don't show up twice in the summary"""
|
||||||
|
testdir.makepyfile(
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
@pytest.mark.skip
|
||||||
|
def test():
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = testdir.runpytest("-rsS")
|
||||||
|
expected = "SKIPPED [1] test_summary_s_alias.py:3: unconditional skip"
|
||||||
|
result.stdout.fnmatch_lines([expected])
|
||||||
|
assert result.stdout.lines.count(expected) == 1
|
||||||
|
|
||||||
|
|
||||||
def test_fail_extra_reporting(testdir, monkeypatch):
|
def test_fail_extra_reporting(testdir, monkeypatch):
|
||||||
monkeypatch.setenv("COLUMNS", "80")
|
monkeypatch.setenv("COLUMNS", "80")
|
||||||
|
@ -1551,12 +1580,16 @@ class TestProgressWithTeardown(object):
|
||||||
testdir.makepyfile(
|
testdir.makepyfile(
|
||||||
"""
|
"""
|
||||||
def test_foo(fail_teardown):
|
def test_foo(fail_teardown):
|
||||||
assert False
|
assert 0
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
output = testdir.runpytest()
|
output = testdir.runpytest("-rfE")
|
||||||
output.stdout.re_match_lines(
|
output.stdout.re_match_lines(
|
||||||
[r"test_teardown_with_test_also_failing.py FE\s+\[100%\]"]
|
[
|
||||||
|
r"test_teardown_with_test_also_failing.py FE\s+\[100%\]",
|
||||||
|
"FAILED test_teardown_with_test_also_failing.py::test_foo - assert 0",
|
||||||
|
"ERROR test_teardown_with_test_also_failing.py::test_foo - assert False",
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_teardown_many(self, testdir, many_files):
|
def test_teardown_many(self, testdir, many_files):
|
||||||
|
|
|
@ -3,6 +3,9 @@ from __future__ import absolute_import
|
||||||
from __future__ import division
|
from __future__ import division
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import errno
|
||||||
|
import os
|
||||||
|
import stat
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import attr
|
import attr
|
||||||
|
@ -270,7 +273,7 @@ class TestNumberedDir(object):
|
||||||
registry = []
|
registry = []
|
||||||
register_cleanup_lock_removal(lock, register=registry.append)
|
register_cleanup_lock_removal(lock, register=registry.append)
|
||||||
|
|
||||||
cleanup_func, = registry
|
(cleanup_func,) = registry
|
||||||
|
|
||||||
assert lock.is_file()
|
assert lock.is_file()
|
||||||
|
|
||||||
|
@ -317,22 +320,6 @@ class TestNumberedDir(object):
|
||||||
p, consider_lock_dead_if_created_before=p.stat().st_mtime + 1
|
p, consider_lock_dead_if_created_before=p.stat().st_mtime + 1
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_rmtree(self, tmp_path):
|
|
||||||
from _pytest.pathlib import rmtree
|
|
||||||
|
|
||||||
adir = tmp_path / "adir"
|
|
||||||
adir.mkdir()
|
|
||||||
rmtree(adir)
|
|
||||||
|
|
||||||
assert not adir.exists()
|
|
||||||
|
|
||||||
adir.mkdir()
|
|
||||||
afile = adir / "afile"
|
|
||||||
afile.write_bytes(b"aa")
|
|
||||||
|
|
||||||
rmtree(adir, force=True)
|
|
||||||
assert not adir.exists()
|
|
||||||
|
|
||||||
def test_cleanup_ignores_symlink(self, tmp_path):
|
def test_cleanup_ignores_symlink(self, tmp_path):
|
||||||
the_symlink = tmp_path / (self.PREFIX + "current")
|
the_symlink = tmp_path / (self.PREFIX + "current")
|
||||||
attempt_symlink_to(the_symlink, tmp_path / (self.PREFIX + "5"))
|
attempt_symlink_to(the_symlink, tmp_path / (self.PREFIX + "5"))
|
||||||
|
@ -345,6 +332,91 @@ class TestNumberedDir(object):
|
||||||
assert folder.is_dir()
|
assert folder.is_dir()
|
||||||
|
|
||||||
|
|
||||||
|
class TestRmRf:
|
||||||
|
def test_rm_rf(self, tmp_path):
|
||||||
|
from _pytest.pathlib import rm_rf
|
||||||
|
|
||||||
|
adir = tmp_path / "adir"
|
||||||
|
adir.mkdir()
|
||||||
|
rm_rf(adir)
|
||||||
|
|
||||||
|
assert not adir.exists()
|
||||||
|
|
||||||
|
adir.mkdir()
|
||||||
|
afile = adir / "afile"
|
||||||
|
afile.write_bytes(b"aa")
|
||||||
|
|
||||||
|
rm_rf(adir)
|
||||||
|
assert not adir.exists()
|
||||||
|
|
||||||
|
def test_rm_rf_with_read_only_file(self, tmp_path):
|
||||||
|
"""Ensure rm_rf can remove directories with read-only files in them (#5524)"""
|
||||||
|
from _pytest.pathlib import rm_rf
|
||||||
|
|
||||||
|
fn = tmp_path / "dir/foo.txt"
|
||||||
|
fn.parent.mkdir()
|
||||||
|
|
||||||
|
fn.touch()
|
||||||
|
|
||||||
|
self.chmod_r(fn)
|
||||||
|
|
||||||
|
rm_rf(fn.parent)
|
||||||
|
|
||||||
|
assert not fn.parent.is_dir()
|
||||||
|
|
||||||
|
def chmod_r(self, path):
|
||||||
|
mode = os.stat(str(path)).st_mode
|
||||||
|
os.chmod(str(path), mode & ~stat.S_IWRITE)
|
||||||
|
|
||||||
|
def test_rm_rf_with_read_only_directory(self, tmp_path):
|
||||||
|
"""Ensure rm_rf can remove read-only directories (#5524)"""
|
||||||
|
from _pytest.pathlib import rm_rf
|
||||||
|
|
||||||
|
adir = tmp_path / "dir"
|
||||||
|
adir.mkdir()
|
||||||
|
|
||||||
|
(adir / "foo.txt").touch()
|
||||||
|
self.chmod_r(adir)
|
||||||
|
|
||||||
|
rm_rf(adir)
|
||||||
|
|
||||||
|
assert not adir.is_dir()
|
||||||
|
|
||||||
|
def test_on_rm_rf_error(self, tmp_path):
|
||||||
|
from _pytest.pathlib import on_rm_rf_error
|
||||||
|
|
||||||
|
adir = tmp_path / "dir"
|
||||||
|
adir.mkdir()
|
||||||
|
|
||||||
|
fn = adir / "foo.txt"
|
||||||
|
fn.touch()
|
||||||
|
self.chmod_r(fn)
|
||||||
|
|
||||||
|
# unknown exception
|
||||||
|
with pytest.warns(pytest.PytestWarning):
|
||||||
|
exc_info = (None, RuntimeError(), None)
|
||||||
|
on_rm_rf_error(os.unlink, str(fn), exc_info, start_path=tmp_path)
|
||||||
|
assert fn.is_file()
|
||||||
|
|
||||||
|
# we ignore FileNotFoundError
|
||||||
|
file_not_found = OSError()
|
||||||
|
file_not_found.errno = errno.ENOENT
|
||||||
|
exc_info = (None, file_not_found, None)
|
||||||
|
assert not on_rm_rf_error(None, str(fn), exc_info, start_path=tmp_path)
|
||||||
|
|
||||||
|
permission_error = OSError()
|
||||||
|
permission_error.errno = errno.EACCES
|
||||||
|
# unknown function
|
||||||
|
with pytest.warns(pytest.PytestWarning):
|
||||||
|
exc_info = (None, permission_error, None)
|
||||||
|
on_rm_rf_error(None, str(fn), exc_info, start_path=tmp_path)
|
||||||
|
assert fn.is_file()
|
||||||
|
|
||||||
|
exc_info = (None, permission_error, None)
|
||||||
|
on_rm_rf_error(os.unlink, str(fn), exc_info, start_path=tmp_path)
|
||||||
|
assert not fn.is_file()
|
||||||
|
|
||||||
|
|
||||||
def attempt_symlink_to(path, to_path):
|
def attempt_symlink_to(path, to_path):
|
||||||
"""Try to make a symlink from "path" to "to_path", skipping in case this platform
|
"""Try to make a symlink from "path" to "to_path", skipping in case this platform
|
||||||
does not support it or we don't have sufficient privileges (common on Windows)."""
|
does not support it or we don't have sufficient privileges (common on Windows)."""
|
||||||
|
@ -358,3 +430,24 @@ def attempt_symlink_to(path, to_path):
|
||||||
|
|
||||||
def test_tmpdir_equals_tmp_path(tmpdir, tmp_path):
|
def test_tmpdir_equals_tmp_path(tmpdir, tmp_path):
|
||||||
assert Path(tmpdir) == tmp_path
|
assert Path(tmpdir) == tmp_path
|
||||||
|
|
||||||
|
|
||||||
|
def test_basetemp_with_read_only_files(testdir):
|
||||||
|
"""Integration test for #5524"""
|
||||||
|
testdir.makepyfile(
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import stat
|
||||||
|
|
||||||
|
def test(tmp_path):
|
||||||
|
fn = tmp_path / 'foo.txt'
|
||||||
|
fn.write_text(u'hello')
|
||||||
|
mode = os.stat(str(fn)).st_mode
|
||||||
|
os.chmod(str(fn), mode & ~stat.S_IREAD)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = testdir.runpytest("--basetemp=tmp")
|
||||||
|
assert result.ret == 0
|
||||||
|
# running a second time and ensure we don't crash
|
||||||
|
result = testdir.runpytest("--basetemp=tmp")
|
||||||
|
assert result.ret == 0
|
||||||
|
|
|
@ -144,6 +144,29 @@ def test_new_instances(testdir):
|
||||||
reprec.assertoutcome(passed=2)
|
reprec.assertoutcome(passed=2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_function_item_obj_is_instance(testdir):
|
||||||
|
"""item.obj should be a bound method on unittest.TestCase function items (#5390)."""
|
||||||
|
testdir.makeconftest(
|
||||||
|
"""
|
||||||
|
def pytest_runtest_makereport(item, call):
|
||||||
|
if call.when == 'call':
|
||||||
|
class_ = item.parent.obj
|
||||||
|
assert isinstance(item.obj.__self__, class_)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
testdir.makepyfile(
|
||||||
|
"""
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
class Test(unittest.TestCase):
|
||||||
|
def test_foo(self):
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = testdir.runpytest_inprocess()
|
||||||
|
result.stdout.fnmatch_lines(["* 1 passed in*"])
|
||||||
|
|
||||||
|
|
||||||
def test_teardown(testdir):
|
def test_teardown(testdir):
|
||||||
testpath = testdir.makepyfile(
|
testpath = testdir.makepyfile(
|
||||||
"""
|
"""
|
||||||
|
@ -365,7 +388,7 @@ def test_testcase_custom_exception_info(testdir, type):
|
||||||
|
|
||||||
|
|
||||||
def test_testcase_totally_incompatible_exception_info(testdir):
|
def test_testcase_totally_incompatible_exception_info(testdir):
|
||||||
item, = testdir.getitems(
|
(item,) = testdir.getitems(
|
||||||
"""
|
"""
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
class MyTestCase(TestCase):
|
class MyTestCase(TestCase):
|
||||||
|
|
|
@ -569,7 +569,7 @@ class TestDeprecationWarningsByDefault:
|
||||||
assert WARNINGS_SUMMARY_HEADER not in result.stdout.str()
|
assert WARNINGS_SUMMARY_HEADER not in result.stdout.str()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(six.PY3, reason="Python 2 only issue")
|
@pytest.mark.skipif(not six.PY2, reason="Python 2 only issue")
|
||||||
def test_infinite_loop_warning_against_unicode_usage_py2(testdir):
|
def test_infinite_loop_warning_against_unicode_usage_py2(testdir):
|
||||||
"""
|
"""
|
||||||
We need to be careful when raising the warning about unicode usage with "warnings.warn"
|
We need to be careful when raising the warning about unicode usage with "warnings.warn"
|
||||||
|
|
17
tox.ini
17
tox.ini
|
@ -11,6 +11,7 @@ envlist =
|
||||||
py36
|
py36
|
||||||
py37
|
py37
|
||||||
py38
|
py38
|
||||||
|
py39
|
||||||
pypy
|
pypy
|
||||||
pypy3
|
pypy3
|
||||||
{py27,py37}-{pexpect,xdist,twisted,numpy,pluggymaster}
|
{py27,py37}-{pexpect,xdist,twisted,numpy,pluggymaster}
|
||||||
|
@ -93,7 +94,7 @@ commands =
|
||||||
[testenv:regen]
|
[testenv:regen]
|
||||||
changedir = doc/en
|
changedir = doc/en
|
||||||
skipsdist = True
|
skipsdist = True
|
||||||
basepython = python3.6
|
basepython = python3
|
||||||
deps =
|
deps =
|
||||||
sphinx
|
sphinx
|
||||||
PyYAML
|
PyYAML
|
||||||
|
@ -119,13 +120,14 @@ changedir = testing/freeze
|
||||||
# Disable PEP 517 with pip, which does not work with PyInstaller currently.
|
# Disable PEP 517 with pip, which does not work with PyInstaller currently.
|
||||||
deps =
|
deps =
|
||||||
pyinstaller
|
pyinstaller
|
||||||
|
setuptools < 45.0.0
|
||||||
commands =
|
commands =
|
||||||
{envpython} create_executable.py
|
{envpython} create_executable.py
|
||||||
{envpython} tox_run.py
|
{envpython} tox_run.py
|
||||||
|
|
||||||
[testenv:release]
|
[testenv:release]
|
||||||
decription = do a release, required posarg of the version number
|
decription = do a release, required posarg of the version number
|
||||||
basepython = python3.6
|
basepython = python3
|
||||||
usedevelop = True
|
usedevelop = True
|
||||||
passenv = *
|
passenv = *
|
||||||
deps =
|
deps =
|
||||||
|
@ -136,6 +138,17 @@ deps =
|
||||||
wheel
|
wheel
|
||||||
commands = python scripts/release.py {posargs}
|
commands = python scripts/release.py {posargs}
|
||||||
|
|
||||||
|
[testenv:publish_gh_release_notes]
|
||||||
|
description = create GitHub release after deployment
|
||||||
|
basepython = python3
|
||||||
|
usedevelop = True
|
||||||
|
passenv = GH_RELEASE_NOTES_TOKEN TRAVIS_TAG TRAVIS_REPO_SLUG
|
||||||
|
deps =
|
||||||
|
github3.py
|
||||||
|
pypandoc
|
||||||
|
commands = python scripts/publish_gh_release_notes.py
|
||||||
|
|
||||||
|
|
||||||
[pytest]
|
[pytest]
|
||||||
minversion = 2.0
|
minversion = 2.0
|
||||||
addopts = -ra -p pytester --strict-markers
|
addopts = -ra -p pytester --strict-markers
|
||||||
|
|
Loading…
Reference in New Issue