Merge remote-tracking branch 'upstream/master' into cpu-auto-errors
This commit is contained in:
21
.hgtags
21
.hgtags
@@ -1,21 +0,0 @@
|
|||||||
42c6503ee48fae9c4c96d406afb12bfc86f15803 1.0
|
|
||||||
eca7ce17eabf296983c36812c8b8be901e7055a3 1.1
|
|
||||||
56d8e5280be224a0ad3220a9deed55334710bd23 1.2
|
|
||||||
e6c4ce20db4bf65086ff55807a3c306cad7ca393 1.3
|
|
||||||
e6c4ce20db4bf65086ff55807a3c306cad7ca393 1.3
|
|
||||||
eaf8b1cb7c312883598677231be5bbeea3b5c127 1.3
|
|
||||||
a423748bf17ee778a37853225210257699cad9c1 1.4
|
|
||||||
cd44a941c833c098e4899fe3d42a96703754d0d5 1.5
|
|
||||||
4815040bdad8f182a5487f57a9da385483836e75 1.6
|
|
||||||
20875fed94e7f3dff50bdf762df91153b15ceca6 1.7
|
|
||||||
20875fed94e7f3dff50bdf762df91153b15ceca6 1.7
|
|
||||||
29c38e195526f5f0fdd651fb51f59d6efaaafbb0 1.7
|
|
||||||
0d1c00018008433956aa7d93007bab6ea7de96e4 1.8
|
|
||||||
0d1c00018008433956aa7d93007bab6ea7de96e4 1.8
|
|
||||||
1d27987c267577899350a25ba5828d55d87083ad 1.8
|
|
||||||
5c5cb6d59e12e566fbb0217aea718dc31578bee1 1.9
|
|
||||||
4406fc2a6427fadc021ed7e43e7aa5032b1ea91f 1.10
|
|
||||||
220f6e46eb71a6212ccbe6b67b9e6edcf8ee4fa5 1.11
|
|
||||||
39ef85dbc893cc63dede11601208098a667b58e9 1.12
|
|
||||||
4e25f4c568be2d7cb4d1739638a9e66bbf28f588 v1.13
|
|
||||||
67ff3aa4d294f75ade0ebf03267e5739e1bd9473 v1.13.1
|
|
||||||
36
.travis.yml
36
.travis.yml
@@ -1,32 +1,32 @@
|
|||||||
sudo: false
|
sudo: false
|
||||||
language: python
|
language: python
|
||||||
python:
|
python:
|
||||||
- '3.5.0b3'
|
- '3.5'
|
||||||
# command to install dependencies
|
# command to install dependencies
|
||||||
install: "pip install -U tox setuptools_scm"
|
install: "pip install -U tox setuptools_scm"
|
||||||
# # command to run tests
|
# # command to run tests
|
||||||
env:
|
env:
|
||||||
matrix:
|
matrix:
|
||||||
- TESTENV=flakes
|
# note: please use "tox --listenvs" to populate the build matrix
|
||||||
- TESTENV=readme
|
|
||||||
# matrix was trimmed to skip
|
|
||||||
# some builds that are unnecessary/perceived redundant
|
|
||||||
- TESTENV=py26-pytest24
|
|
||||||
- TESTENV=py26-pytest25
|
|
||||||
- TESTENV=py26-pytest26
|
- TESTENV=py26-pytest26
|
||||||
- TESTENV=py26-pytest27
|
- TESTENV=py26-pytest27
|
||||||
- TESTENV=py33-pytest27
|
- TESTENV=py26-pytest28
|
||||||
- TESTENV=py34-pytest24
|
- TESTENV=py26-pytest29
|
||||||
- TESTENV=py34-pytest25
|
|
||||||
- TESTENV=py34-pytest26
|
|
||||||
- TESTENV=py27-pytest24
|
|
||||||
- TESTENV=py27-pytest25
|
|
||||||
- TESTENV=py27-pytest26
|
- TESTENV=py27-pytest26
|
||||||
|
- TESTENV=py27-pytest27
|
||||||
- TESTENV=py27-pytest27-pexpect
|
- TESTENV=py27-pytest28
|
||||||
- TESTENV=py34-pytest27-pexpect
|
- TESTENV=py27-pytest29
|
||||||
# - TESTENV=py35-pytest27
|
- TESTENV=py34-pytest26
|
||||||
- TESTENV=pypy-pytest27
|
- TESTENV=py34-pytest27
|
||||||
|
- TESTENV=py34-pytest28
|
||||||
|
- TESTENV=py34-pytest29
|
||||||
|
- TESTENV=py35-pytest27
|
||||||
|
- TESTENV=py35-pytest28
|
||||||
|
- TESTENV=py35-pytest29
|
||||||
|
- TESTENV=py27-pytest28-pexpect
|
||||||
|
- TESTENV=py35-pytest28-pexpect
|
||||||
|
- TESTENV=flakes
|
||||||
|
- TESTENV=readme
|
||||||
|
|
||||||
script: tox --recreate -e $TESTENV
|
script: tox --recreate -e $TESTENV
|
||||||
|
|
||||||
|
|||||||
29
CHANGELOG
29
CHANGELOG
@@ -1,7 +1,30 @@
|
|||||||
1.13.2
|
1.15.0.dev
|
||||||
-------
|
----------
|
||||||
|
|
||||||
- fix readme display on pypi
|
- new ``worker_id`` fixture, returns the id of the worker in a test or fixture.
|
||||||
|
Thanks Jared Hellman for the PR.
|
||||||
|
|
||||||
|
- display progress during collection only when in a terminal, similar to pytest #1397 issue.
|
||||||
|
Thanks Bruno Oliveira for the PR.
|
||||||
|
|
||||||
|
- fix internal error message when ``--maxfail`` is used (#62, #65).
|
||||||
|
Thanks Collin RM Stocks and Bryan A. Jones for reports and Bruno Oliveira for the PR.
|
||||||
|
|
||||||
|
|
||||||
|
1.14
|
||||||
|
----
|
||||||
|
|
||||||
|
- new hook: ``pytest_xdist_node_collection_finished(node, ids)``, called when
|
||||||
|
a worker has finished collection. Thanks Omer Katz for the request and
|
||||||
|
Bruno Oliveira for the PR.
|
||||||
|
|
||||||
|
- fix README display on pypi
|
||||||
|
|
||||||
|
- fix #22: xdist now works if the internal tmpdir plugin is disabled.
|
||||||
|
Thanks Bruno Oliveira for the PR.
|
||||||
|
|
||||||
|
- fix #32: xdist now works if looponfail or boxed are disabled.
|
||||||
|
Thanks Bruno Oliveira for the PR.
|
||||||
|
|
||||||
|
|
||||||
1.13.1
|
1.13.1
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ include README.txt
|
|||||||
include setup.py
|
include setup.py
|
||||||
include tox.ini
|
include tox.ini
|
||||||
graft testing
|
graft testing
|
||||||
prune .hg
|
prune .git
|
||||||
|
|||||||
76
OVERVIEW.md
Normal file
76
OVERVIEW.md
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# Overview #
|
||||||
|
|
||||||
|
`xdist` works by spawning one or more **workers**, which are controlled
|
||||||
|
by the **master**. Each **worker** is responsible for performing
|
||||||
|
a full test collection and afterwards running tests as dictated by the **master**.
|
||||||
|
|
||||||
|
The execution flow is:
|
||||||
|
|
||||||
|
1. **master** spawns one or more **workers** at the beginning of
|
||||||
|
the test session. The communication between **master** and **worker** nodes makes use of
|
||||||
|
[execnet](http://codespeak.net/execnet/) and its [gateways](http://codespeak.net/execnet/basics.html#gateways-bootstrapping-python-interpreters).
|
||||||
|
The actual interpreters executing the code for the **workers** might
|
||||||
|
be remote or local.
|
||||||
|
|
||||||
|
1. Each **worker** itself is a mini pytest runner. **workers** at this
|
||||||
|
point perform a full test collection, sending back the collected
|
||||||
|
test-ids back to the **master** which does not
|
||||||
|
perform any collection itself.
|
||||||
|
|
||||||
|
1. The **master** receives the result of the collection from all nodes.
|
||||||
|
At this point the **master** performs some sanity check to ensure that
|
||||||
|
all **workers** collected the same tests (including order), bailing out otherwise.
|
||||||
|
If all is well, it converts the list of test-ids into a list of simple
|
||||||
|
indexes, where each index corresponds to the position of that test in the
|
||||||
|
original collection list. This works because all nodes have the same
|
||||||
|
collection list, and saves bandwidth because the **master** can now tell
|
||||||
|
one of the workers to just *execute test index 3* index of passing the
|
||||||
|
full test id.
|
||||||
|
|
||||||
|
1. If **dist-mode** is **each**: the **master** just sends the full list
|
||||||
|
of test indexes to each node at this moment.
|
||||||
|
|
||||||
|
1. If **dist-mode** is **load**: the **master** takes around 25% of the
|
||||||
|
tests and sends them one by one to each **worker** in a round robin
|
||||||
|
fashion. The rest of the tests will be distributed later as **workers**
|
||||||
|
finish tests (see below).
|
||||||
|
|
||||||
|
1. **workers** re-implement `pytest_runtestloop`: pytest's default implementation
|
||||||
|
basically loops over all collected items in the `session` object and executes
|
||||||
|
the `pytest_runtest_protocol` for each test item, but in xdist **workers** sit idly
|
||||||
|
waiting for **master** to send tests for execution. As tests are
|
||||||
|
received by **workers**, `pytest_runtest_protocol` is executed for each test.
|
||||||
|
Here it worth noting an implementation detail: **workers** always must keep at
|
||||||
|
least one test item on their queue due to how the `pytest_runtest_protocol(item, nextitem)`
|
||||||
|
hook is defined: in order to pass the `nextitem` to the hook, the worker must wait for more
|
||||||
|
instructions from master before executing that remaining test. If it receives more tests,
|
||||||
|
then it can safely call `pytest_runtest_protocol` because it knows what the `nextitem` parameter will be.
|
||||||
|
If it receives a "shutdown" signal, then it can execute the hook passing `nextitem` as `None`.
|
||||||
|
|
||||||
|
1. As tests are started and completed at the **workers**, the results are sent
|
||||||
|
back to the **master**, which then just forwards the results to
|
||||||
|
the appropriate pytest hooks: `pytest_runtest_logstart` and
|
||||||
|
`pytest_runtest_logreport`. This way other plugins (for example `junitxml`)
|
||||||
|
can work normally. The **master** (when in dist-mode **load**)
|
||||||
|
decides to send more tests to a node when a test completes, using
|
||||||
|
some heuristics such as test durations and how many tests each **worker**
|
||||||
|
still has to run.
|
||||||
|
|
||||||
|
1. When the **master** has no more pending tests it will
|
||||||
|
send a "shutdown" signal to all **workers**, which will then run their
|
||||||
|
remaining tests to completion and shut down. At this point the
|
||||||
|
**master** will sit waiting for **workers** to shut down, still
|
||||||
|
processing events such as `pytest_runtest_logreport`.
|
||||||
|
|
||||||
|
## FAQ ##
|
||||||
|
|
||||||
|
> Why does each worker do its own collection, as opposed to having
|
||||||
|
the master collect once and distribute from that collection to the workers?
|
||||||
|
|
||||||
|
If collection was performed by master then it would have to
|
||||||
|
serialize collected items to send them through the wire, as workers live in another process.
|
||||||
|
The problem is that test items are not easily (impossible?) to serialize, as they contain references to
|
||||||
|
the test functions, fixture managers, config objects, etc. Even if one manages to serialize it,
|
||||||
|
it seems it would be very hard to get it right and easy to break by any small change in pytest.
|
||||||
|
|
||||||
|
|
||||||
41
README.rst
41
README.rst
@@ -5,6 +5,9 @@
|
|||||||
.. image:: http://img.shields.io/pypi/v/pytest-xdist.svg
|
.. image:: http://img.shields.io/pypi/v/pytest-xdist.svg
|
||||||
:target: https://pypi.python.org/pypi/pytest-xdist
|
:target: https://pypi.python.org/pypi/pytest-xdist
|
||||||
|
|
||||||
|
.. image:: https://ci.appveyor.com/api/projects/status/56eq1a1avd4sdd7e/branch/master?svg=true
|
||||||
|
:target: https://ci.appveyor.com/project/pytestbot/pytest-xdist
|
||||||
|
|
||||||
xdist: pytest distributed testing plugin
|
xdist: pytest distributed testing plugin
|
||||||
=========================================
|
=========================================
|
||||||
|
|
||||||
@@ -182,17 +185,47 @@ at once. The specifications strings use the `xspec syntax`_.
|
|||||||
|
|
||||||
.. _`execnet`: http://codespeak.net/execnet
|
.. _`execnet`: http://codespeak.net/execnet
|
||||||
|
|
||||||
|
Identifying the worker process during a test
|
||||||
|
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
|
||||||
|
|
||||||
|
If you need to determine the identity of a worker process in
|
||||||
|
a test or fixture, you may use the ``worker_id`` fixture to do so:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def user_account(worker_id):
|
||||||
|
""" use a different account in each xdist worker """
|
||||||
|
return "account_%s" % worker_id
|
||||||
|
|
||||||
|
When ``xdist`` is disabled (running with ``-n0`` for example), then
|
||||||
|
``worker_id`` will return ``"master"``.
|
||||||
|
|
||||||
|
Additionally, worker processes have the following environment variables
|
||||||
|
defined:
|
||||||
|
|
||||||
|
* ``PYTEST_XDIST_WORKER``: the name of the worker, e.g., ``"gw2"``.
|
||||||
|
* ``PYTEST_XDIST_WORKER_COUNT``: the total number of workers in this session,
|
||||||
|
e.g., ``"4"`` when ``-n 4`` is given in the command-line.
|
||||||
|
|
||||||
|
*New in version 1.15.*
|
||||||
|
|
||||||
Specifying test exec environments in an ini file
|
Specifying test exec environments in an ini file
|
||||||
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
|
||||||
pytest (since version 2.0) supports ini-style cofiguration.
|
pytest (since version 2.0) supports ini-style cofiguration.
|
||||||
You can for example make running with three subprocesses
|
You can for example make running with three subprocesses
|
||||||
your default like this::
|
your default like this:
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
[pytest]
|
[pytest]
|
||||||
addopts = -n3
|
addopts = -n3
|
||||||
|
|
||||||
You can also add default environments like this::
|
You can also add default environments like this:
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
[pytest]
|
[pytest]
|
||||||
addopts = --tx ssh=myhost//python=python2.5 --tx ssh=myhost//python=python2.6
|
addopts = --tx ssh=myhost//python=python2.5 --tx ssh=myhost//python=python2.6
|
||||||
@@ -207,7 +240,9 @@ Specifying "rsync" dirs in an ini-file
|
|||||||
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
|
||||||
In a ``tox.ini`` or ``setup.cfg`` file in your root project directory
|
In a ``tox.ini`` or ``setup.cfg`` file in your root project directory
|
||||||
you may specify directories to include or to exclude in synchronisation::
|
you may specify directories to include or to exclude in synchronisation:
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
[pytest]
|
[pytest]
|
||||||
rsyncdirs = . mypkg helperpkg
|
rsyncdirs = . mypkg helperpkg
|
||||||
|
|||||||
82
appveyor.yml
82
appveyor.yml
@@ -1,85 +1,7 @@
|
|||||||
environment:
|
|
||||||
global:
|
|
||||||
# SDK v7.0 MSVC Express 2008's SetEnv.cmd script will fail if the
|
|
||||||
# /E:ON and /V:ON options are not enabled in the batch script intepreter
|
|
||||||
# See: http://stackoverflow.com/a/13751649/163740
|
|
||||||
CMD_IN_ENV: "cmd /E:ON /V:ON /C .\\appveyor\\run_with_env.cmd"
|
|
||||||
|
|
||||||
matrix:
|
|
||||||
|
|
||||||
# Pre-installed Python versions, which Appveyor may upgrade to
|
|
||||||
# a later point release.
|
|
||||||
|
|
||||||
- PYTHON: "C:\\Python27"
|
|
||||||
PYTHON_VERSION: "2.7.x" # currently 2.7.9
|
|
||||||
PYTHON_ARCH: "32"
|
|
||||||
TESTENV: "py27"
|
|
||||||
|
|
||||||
- PYTHON: "C:\\Python27-x64"
|
|
||||||
PYTHON_VERSION: "2.7.x" # currently 2.7.9
|
|
||||||
PYTHON_ARCH: "64"
|
|
||||||
TESTENV: "py27"
|
|
||||||
|
|
||||||
- PYTHON: "C:\\Python33"
|
|
||||||
PYTHON_VERSION: "3.3.x" # currently 3.3.5
|
|
||||||
PYTHON_ARCH: "32"
|
|
||||||
TESTENV: "py33"
|
|
||||||
|
|
||||||
- PYTHON: "C:\\Python33-x64"
|
|
||||||
PYTHON_VERSION: "3.3.x" # currently 3.3.5
|
|
||||||
PYTHON_ARCH: "64"
|
|
||||||
TESTENV: "py33"
|
|
||||||
|
|
||||||
- PYTHON: "C:\\Python34"
|
|
||||||
PYTHON_VERSION: "3.4.x" # currently 3.4.3
|
|
||||||
PYTHON_ARCH: "32"
|
|
||||||
TESTENV: "py34"
|
|
||||||
|
|
||||||
- PYTHON: "C:\\Python34-x64"
|
|
||||||
PYTHON_VERSION: "3.4.x" # currently 3.4.3
|
|
||||||
PYTHON_ARCH: "64"
|
|
||||||
TESTENV: "py34"
|
|
||||||
|
|
||||||
# Also test a Python version not pre-installed
|
|
||||||
# See: https://github.com/ogrisel/python-appveyor-demo/issues/10
|
|
||||||
|
|
||||||
- PYTHON: "C:\\Python266"
|
|
||||||
PYTHON_VERSION: "2.6.6"
|
|
||||||
PYTHON_ARCH: "32"
|
|
||||||
TESTENV: "py26"
|
|
||||||
|
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- ECHO "Filesystem root:"
|
- C:\Python35\python -m pip install tox setuptools_scm
|
||||||
- ps: "ls \"C:/\""
|
|
||||||
|
|
||||||
- ECHO "Installed SDKs:"
|
|
||||||
- ps: "ls \"C:/Program Files/Microsoft SDKs/Windows\""
|
|
||||||
|
|
||||||
# Install Python (from the official .msi of http://python.org) and pip when
|
|
||||||
# not already installed.
|
|
||||||
- ps: if (-not(Test-Path($env:PYTHON))) { & appveyor\install.ps1 }
|
|
||||||
|
|
||||||
# Prepend newly installed Python to the PATH of this build (this cannot be
|
|
||||||
# done from inside the powershell script as it would require to restart
|
|
||||||
# the parent CMD process).
|
|
||||||
- "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%"
|
|
||||||
|
|
||||||
# Check that we have the expected version and architecture for Python
|
|
||||||
- "python --version"
|
|
||||||
- "python -c \"import struct; print(struct.calcsize('P') * 8)\""
|
|
||||||
|
|
||||||
# Install the build dependencies of the project. If some dependencies contain
|
|
||||||
# compiled extensions and are not provided as pre-built wheel packages,
|
|
||||||
# pip will build them from source using the MSVC compiler matching the
|
|
||||||
# target Python version and architecture
|
|
||||||
- "%CMD_IN_ENV% pip install tox setuptools_scm"
|
|
||||||
|
|
||||||
build: false # Not a C# project, build stuff at the test step instead.
|
build: false # Not a C# project, build stuff at the test step instead.
|
||||||
|
|
||||||
test_script:
|
test_script:
|
||||||
# Build the compiled extension and run the project tests
|
- C:\Python35\python -m tox
|
||||||
- "%CMD_IN_ENV% tox -e %TESTENV%-pytest24"
|
|
||||||
- "%CMD_IN_ENV% tox -e %TESTENV%-pytest25"
|
|
||||||
- "%CMD_IN_ENV% tox -e %TESTENV%-pytest26"
|
|
||||||
- "%CMD_IN_ENV% tox -e %TESTENV%-pytest27,readme,flakes"
|
|
||||||
|
|||||||
@@ -1,180 +0,0 @@
|
|||||||
# Sample script to install Python and pip under Windows
|
|
||||||
# Authors: Olivier Grisel, Jonathan Helmus and Kyle Kastner
|
|
||||||
# License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/
|
|
||||||
|
|
||||||
$MINICONDA_URL = "http://repo.continuum.io/miniconda/"
|
|
||||||
$BASE_URL = "https://www.python.org/ftp/python/"
|
|
||||||
$GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py"
|
|
||||||
$GET_PIP_PATH = "C:\get-pip.py"
|
|
||||||
|
|
||||||
|
|
||||||
function DownloadPython ($python_version, $platform_suffix) {
|
|
||||||
$webclient = New-Object System.Net.WebClient
|
|
||||||
$filename = "python-" + $python_version + $platform_suffix + ".msi"
|
|
||||||
$url = $BASE_URL + $python_version + "/" + $filename
|
|
||||||
|
|
||||||
$basedir = $pwd.Path + "\"
|
|
||||||
$filepath = $basedir + $filename
|
|
||||||
if (Test-Path $filename) {
|
|
||||||
Write-Host "Reusing" $filepath
|
|
||||||
return $filepath
|
|
||||||
}
|
|
||||||
|
|
||||||
# Download and retry up to 3 times in case of network transient errors.
|
|
||||||
Write-Host "Downloading" $filename "from" $url
|
|
||||||
$retry_attempts = 2
|
|
||||||
for($i=0; $i -lt $retry_attempts; $i++){
|
|
||||||
try {
|
|
||||||
$webclient.DownloadFile($url, $filepath)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
Catch [Exception]{
|
|
||||||
Start-Sleep 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (Test-Path $filepath) {
|
|
||||||
Write-Host "File saved at" $filepath
|
|
||||||
} else {
|
|
||||||
# Retry once to get the error message if any at the last try
|
|
||||||
$webclient.DownloadFile($url, $filepath)
|
|
||||||
}
|
|
||||||
return $filepath
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function InstallPython ($python_version, $architecture, $python_home) {
|
|
||||||
Write-Host "Installing Python" $python_version "for" $architecture "bit architecture to" $python_home
|
|
||||||
if (Test-Path $python_home) {
|
|
||||||
Write-Host $python_home "already exists, skipping."
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
if ($architecture -eq "32") {
|
|
||||||
$platform_suffix = ""
|
|
||||||
} else {
|
|
||||||
$platform_suffix = ".amd64"
|
|
||||||
}
|
|
||||||
$msipath = DownloadPython $python_version $platform_suffix
|
|
||||||
Write-Host "Installing" $msipath "to" $python_home
|
|
||||||
$install_log = $python_home + ".log"
|
|
||||||
$install_args = "/qn /log $install_log /i $msipath TARGETDIR=$python_home"
|
|
||||||
$uninstall_args = "/qn /x $msipath"
|
|
||||||
RunCommand "msiexec.exe" $install_args
|
|
||||||
if (-not(Test-Path $python_home)) {
|
|
||||||
Write-Host "Python seems to be installed else-where, reinstalling."
|
|
||||||
RunCommand "msiexec.exe" $uninstall_args
|
|
||||||
RunCommand "msiexec.exe" $install_args
|
|
||||||
}
|
|
||||||
if (Test-Path $python_home) {
|
|
||||||
Write-Host "Python $python_version ($architecture) installation complete"
|
|
||||||
} else {
|
|
||||||
Write-Host "Failed to install Python in $python_home"
|
|
||||||
Get-Content -Path $install_log
|
|
||||||
Exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function RunCommand ($command, $command_args) {
|
|
||||||
Write-Host $command $command_args
|
|
||||||
Start-Process -FilePath $command -ArgumentList $command_args -Wait -Passthru
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function InstallPip ($python_home) {
|
|
||||||
$pip_path = $python_home + "\Scripts\pip.exe"
|
|
||||||
$python_path = $python_home + "\python.exe"
|
|
||||||
if (-not(Test-Path $pip_path)) {
|
|
||||||
Write-Host "Installing pip..."
|
|
||||||
$webclient = New-Object System.Net.WebClient
|
|
||||||
$webclient.DownloadFile($GET_PIP_URL, $GET_PIP_PATH)
|
|
||||||
Write-Host "Executing:" $python_path $GET_PIP_PATH
|
|
||||||
Start-Process -FilePath "$python_path" -ArgumentList "$GET_PIP_PATH" -Wait -Passthru
|
|
||||||
} else {
|
|
||||||
Write-Host "pip already installed."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function DownloadMiniconda ($python_version, $platform_suffix) {
|
|
||||||
$webclient = New-Object System.Net.WebClient
|
|
||||||
if ($python_version -eq "3.4") {
|
|
||||||
$filename = "Miniconda3-3.5.5-Windows-" + $platform_suffix + ".exe"
|
|
||||||
} else {
|
|
||||||
$filename = "Miniconda-3.5.5-Windows-" + $platform_suffix + ".exe"
|
|
||||||
}
|
|
||||||
$url = $MINICONDA_URL + $filename
|
|
||||||
|
|
||||||
$basedir = $pwd.Path + "\"
|
|
||||||
$filepath = $basedir + $filename
|
|
||||||
if (Test-Path $filename) {
|
|
||||||
Write-Host "Reusing" $filepath
|
|
||||||
return $filepath
|
|
||||||
}
|
|
||||||
|
|
||||||
# Download and retry up to 3 times in case of network transient errors.
|
|
||||||
Write-Host "Downloading" $filename "from" $url
|
|
||||||
$retry_attempts = 2
|
|
||||||
for($i=0; $i -lt $retry_attempts; $i++){
|
|
||||||
try {
|
|
||||||
$webclient.DownloadFile($url, $filepath)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
Catch [Exception]{
|
|
||||||
Start-Sleep 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (Test-Path $filepath) {
|
|
||||||
Write-Host "File saved at" $filepath
|
|
||||||
} else {
|
|
||||||
# Retry once to get the error message if any at the last try
|
|
||||||
$webclient.DownloadFile($url, $filepath)
|
|
||||||
}
|
|
||||||
return $filepath
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function InstallMiniconda ($python_version, $architecture, $python_home) {
|
|
||||||
Write-Host "Installing Python" $python_version "for" $architecture "bit architecture to" $python_home
|
|
||||||
if (Test-Path $python_home) {
|
|
||||||
Write-Host $python_home "already exists, skipping."
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
if ($architecture -eq "32") {
|
|
||||||
$platform_suffix = "x86"
|
|
||||||
} else {
|
|
||||||
$platform_suffix = "x86_64"
|
|
||||||
}
|
|
||||||
$filepath = DownloadMiniconda $python_version $platform_suffix
|
|
||||||
Write-Host "Installing" $filepath "to" $python_home
|
|
||||||
$install_log = $python_home + ".log"
|
|
||||||
$args = "/S /D=$python_home"
|
|
||||||
Write-Host $filepath $args
|
|
||||||
Start-Process -FilePath $filepath -ArgumentList $args -Wait -Passthru
|
|
||||||
if (Test-Path $python_home) {
|
|
||||||
Write-Host "Python $python_version ($architecture) installation complete"
|
|
||||||
} else {
|
|
||||||
Write-Host "Failed to install Python in $python_home"
|
|
||||||
Get-Content -Path $install_log
|
|
||||||
Exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function InstallMinicondaPip ($python_home) {
|
|
||||||
$pip_path = $python_home + "\Scripts\pip.exe"
|
|
||||||
$conda_path = $python_home + "\Scripts\conda.exe"
|
|
||||||
if (-not(Test-Path $pip_path)) {
|
|
||||||
Write-Host "Installing pip..."
|
|
||||||
$args = "install --yes pip"
|
|
||||||
Write-Host $conda_path $args
|
|
||||||
Start-Process -FilePath "$conda_path" -ArgumentList $args -Wait -Passthru
|
|
||||||
} else {
|
|
||||||
Write-Host "pip already installed."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function main () {
|
|
||||||
InstallPython $env:PYTHON_VERSION $env:PYTHON_ARCH $env:PYTHON
|
|
||||||
InstallPip $env:PYTHON
|
|
||||||
}
|
|
||||||
|
|
||||||
main
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
:: To build extensions for 64 bit Python 3, we need to configure environment
|
|
||||||
:: variables to use the MSVC 2010 C++ compilers from GRMSDKX_EN_DVD.iso of:
|
|
||||||
:: MS Windows SDK for Windows 7 and .NET Framework 4 (SDK v7.1)
|
|
||||||
::
|
|
||||||
:: To build extensions for 64 bit Python 2, we need to configure environment
|
|
||||||
:: variables to use the MSVC 2008 C++ compilers from GRMSDKX_EN_DVD.iso of:
|
|
||||||
:: MS Windows SDK for Windows 7 and .NET Framework 3.5 (SDK v7.0)
|
|
||||||
::
|
|
||||||
:: 32 bit builds do not require specific environment configurations.
|
|
||||||
::
|
|
||||||
:: Note: this script needs to be run with the /E:ON and /V:ON flags for the
|
|
||||||
:: cmd interpreter, at least for (SDK v7.0)
|
|
||||||
::
|
|
||||||
:: More details at:
|
|
||||||
:: https://github.com/cython/cython/wiki/64BitCythonExtensionsOnWindows
|
|
||||||
:: http://stackoverflow.com/a/13751649/163740
|
|
||||||
::
|
|
||||||
:: Author: Olivier Grisel
|
|
||||||
:: License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/
|
|
||||||
@ECHO OFF
|
|
||||||
|
|
||||||
SET COMMAND_TO_RUN=%*
|
|
||||||
SET WIN_SDK_ROOT=C:\Program Files\Microsoft SDKs\Windows
|
|
||||||
|
|
||||||
SET MAJOR_PYTHON_VERSION="%PYTHON_VERSION:~0,1%"
|
|
||||||
IF %MAJOR_PYTHON_VERSION% == "2" (
|
|
||||||
SET WINDOWS_SDK_VERSION="v7.0"
|
|
||||||
) ELSE IF %MAJOR_PYTHON_VERSION% == "3" (
|
|
||||||
SET WINDOWS_SDK_VERSION="v7.1"
|
|
||||||
) ELSE (
|
|
||||||
ECHO Unsupported Python version: "%MAJOR_PYTHON_VERSION%"
|
|
||||||
EXIT 1
|
|
||||||
)
|
|
||||||
|
|
||||||
IF "%PYTHON_ARCH%"=="64" (
|
|
||||||
ECHO Configuring Windows SDK %WINDOWS_SDK_VERSION% for Python %MAJOR_PYTHON_VERSION% on a 64 bit architecture
|
|
||||||
SET DISTUTILS_USE_SDK=1
|
|
||||||
SET MSSdk=1
|
|
||||||
"%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Setup\WindowsSdkVer.exe" -q -version:%WINDOWS_SDK_VERSION%
|
|
||||||
"%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Bin\SetEnv.cmd" /x64 /release
|
|
||||||
ECHO Executing: %COMMAND_TO_RUN%
|
|
||||||
call %COMMAND_TO_RUN% || EXIT 1
|
|
||||||
) ELSE (
|
|
||||||
ECHO Using default MSVC build environment for 32 bit architecture
|
|
||||||
ECHO Executing: %COMMAND_TO_RUN%
|
|
||||||
call %COMMAND_TO_RUN% || EXIT 1
|
|
||||||
)
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
If your testing involves C or C++ libraries you might have to deal
|
If your testing involves C or C++ libraries you might have to deal
|
||||||
with crashing processes. The xdist-plugin provides the ``--boxed`` option
|
with crashing processes. The xdist-plugin provides the ``--boxed`` option
|
||||||
to run each test in a controled subprocess. Here is a basic example::
|
to run each test in a controlled subprocess. Here is a basic example::
|
||||||
|
|
||||||
# content of test_module.py
|
# content of test_module.py
|
||||||
|
|
||||||
|
|||||||
1
setup.py
1
setup.py
@@ -24,6 +24,7 @@ setup(
|
|||||||
setup_requires=['setuptools_scm'],
|
setup_requires=['setuptools_scm'],
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Development Status :: 5 - Production/Stable',
|
'Development Status :: 5 - Production/Stable',
|
||||||
|
'Framework :: Pytest',
|
||||||
'Intended Audience :: Developers',
|
'Intended Audience :: Developers',
|
||||||
'License :: OSI Approved :: MIT License',
|
'License :: OSI Approved :: MIT License',
|
||||||
'Operating System :: POSIX',
|
'Operating System :: POSIX',
|
||||||
|
|||||||
@@ -353,19 +353,6 @@ def test_terminate_on_hangingnode(testdir):
|
|||||||
result.stdout.fnmatch_lines(["*killed*my*", ])
|
result.stdout.fnmatch_lines(["*killed*my*", ])
|
||||||
|
|
||||||
|
|
||||||
def test_auto_detect_cpus(testdir, monkeypatch):
|
|
||||||
import multiprocessing
|
|
||||||
monkeypatch.setattr(multiprocessing, 'cpu_count', lambda: 3)
|
|
||||||
testdir.makeconftest("""
|
|
||||||
def pytest_unconfigure(config):
|
|
||||||
with open('cpus', 'w') as f:
|
|
||||||
f.write('cpus = %s' % config.option.numprocesses)
|
|
||||||
""")
|
|
||||||
testdir.inline_run('-n=auto')
|
|
||||||
cpus_file = testdir.tmpdir.join('cpus')
|
|
||||||
assert cpus_file.read() == 'cpus = 3'
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(reason="works if run outside test suite", run=False)
|
@pytest.mark.xfail(reason="works if run outside test suite", run=False)
|
||||||
def test_session_hooks(testdir):
|
def test_session_hooks(testdir):
|
||||||
testdir.makeconftest("""
|
testdir.makeconftest("""
|
||||||
@@ -424,7 +411,9 @@ def test_session_testscollected(testdir):
|
|||||||
|
|
||||||
def test_funcarg_teardown_failure(testdir):
|
def test_funcarg_teardown_failure(testdir):
|
||||||
p = testdir.makepyfile("""
|
p = testdir.makepyfile("""
|
||||||
def pytest_funcarg__myarg(request):
|
import pytest
|
||||||
|
@pytest.fixture
|
||||||
|
def myarg(request):
|
||||||
def teardown(val):
|
def teardown(val):
|
||||||
raise ValueError(val)
|
raise ValueError(val)
|
||||||
return request.cached_setup(setup=lambda: 42, teardown=teardown,
|
return request.cached_setup(setup=lambda: 42, teardown=teardown,
|
||||||
@@ -522,6 +511,31 @@ def test_issue_594_random_parametrize(testdir):
|
|||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def test_tmpdir_disabled(testdir):
|
||||||
|
"""Test xdist doesn't break if internal tmpdir plugin is disabled (#22).
|
||||||
|
"""
|
||||||
|
p1 = testdir.makepyfile("""
|
||||||
|
def test_ok():
|
||||||
|
pass
|
||||||
|
""")
|
||||||
|
result = testdir.runpytest(p1, "-n1", '-p', 'no:tmpdir')
|
||||||
|
assert result.ret == 0
|
||||||
|
result.stdout.fnmatch_lines("*1 passed*")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('plugin', ['xdist.looponfail', 'xdist.boxed'])
|
||||||
|
def test_sub_plugins_disabled(testdir, plugin):
|
||||||
|
"""Test that xdist doesn't break if we disable any of its sub-plugins. (#32)
|
||||||
|
"""
|
||||||
|
p1 = testdir.makepyfile("""
|
||||||
|
def test_ok():
|
||||||
|
pass
|
||||||
|
""")
|
||||||
|
result = testdir.runpytest(p1, "-n1", '-p', 'no:%s' % plugin)
|
||||||
|
assert result.ret == 0
|
||||||
|
result.stdout.fnmatch_lines("*1 passed*")
|
||||||
|
|
||||||
|
|
||||||
class TestNodeFailure:
|
class TestNodeFailure:
|
||||||
def test_load_single(self, testdir):
|
def test_load_single(self, testdir):
|
||||||
f = testdir.makepyfile("""
|
f = testdir.makepyfile("""
|
||||||
@@ -564,6 +578,7 @@ class TestNodeFailure:
|
|||||||
"*1 failed*1 passed*",
|
"*1 failed*1 passed*",
|
||||||
])
|
])
|
||||||
|
|
||||||
|
@pytest.mark.xfail(reason='#20: xdist race condition on node restart')
|
||||||
def test_each_multiple(self, testdir):
|
def test_each_multiple(self, testdir):
|
||||||
f = testdir.makepyfile("""
|
f = testdir.makepyfile("""
|
||||||
import os
|
import os
|
||||||
@@ -607,3 +622,68 @@ class TestNodeFailure:
|
|||||||
"*Slave*crashed while running*",
|
"*Slave*crashed while running*",
|
||||||
"*1 failed*2 passed*",
|
"*1 failed*2 passed*",
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('n', [0, 2])
|
||||||
|
def test_worker_id_fixture(testdir, n):
|
||||||
|
import glob
|
||||||
|
f = testdir.makepyfile("""
|
||||||
|
import pytest
|
||||||
|
@pytest.mark.parametrize("run_num", range(2))
|
||||||
|
def test_worker_id1(worker_id, run_num):
|
||||||
|
with open("worker_id%s.txt" % run_num, "w") as f:
|
||||||
|
f.write(worker_id)
|
||||||
|
""")
|
||||||
|
result = testdir.runpytest(f, "-n%d" % n)
|
||||||
|
result.stdout.fnmatch_lines('* 2 passed in *')
|
||||||
|
worker_ids = set()
|
||||||
|
for fname in glob.glob(str(testdir.tmpdir.join("*.txt"))):
|
||||||
|
with open(fname) as f:
|
||||||
|
worker_ids.add(f.read().strip())
|
||||||
|
if n == 0:
|
||||||
|
assert worker_ids == set(['master'])
|
||||||
|
else:
|
||||||
|
assert worker_ids == set(['gw0', 'gw1'])
|
||||||
|
|
||||||
|
|
||||||
|
def test_color_yes_collection_on_non_atty(testdir, request):
|
||||||
|
"""skip collect progress report when working on non-terminals.
|
||||||
|
|
||||||
|
Similar to pytest-dev/pytest#1397
|
||||||
|
"""
|
||||||
|
tr = request.config.pluginmanager.getplugin("terminalreporter")
|
||||||
|
if not hasattr(tr, 'isatty'):
|
||||||
|
pytest.skip('only valid for newer pytest versions')
|
||||||
|
testdir.makepyfile("""
|
||||||
|
import pytest
|
||||||
|
@pytest.mark.parametrize('i', range(10))
|
||||||
|
def test_this(i):
|
||||||
|
assert 1
|
||||||
|
""")
|
||||||
|
args = ['--color=yes', '-n2']
|
||||||
|
result = testdir.runpytest(*args)
|
||||||
|
assert 'test session starts' in result.stdout.str()
|
||||||
|
assert '\x1b[1m' in result.stdout.str()
|
||||||
|
assert 'gw0 [10] / gw1 [10]' in result.stdout.str()
|
||||||
|
assert 'gw0 C / gw1 C' not in result.stdout.str()
|
||||||
|
|
||||||
|
|
||||||
|
def test_internal_error_with_maxfail(testdir):
|
||||||
|
"""
|
||||||
|
Internal error when using --maxfail option (#62, #65).
|
||||||
|
"""
|
||||||
|
testdir.makepyfile("""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
@pytest.fixture(params=['1', '2'])
|
||||||
|
def crasher():
|
||||||
|
raise RuntimeError
|
||||||
|
|
||||||
|
def test_aaa0(crasher):
|
||||||
|
pass
|
||||||
|
def test_aaa1(crasher):
|
||||||
|
pass
|
||||||
|
""")
|
||||||
|
result = testdir.runpytest_subprocess('--maxfail=1', '-n1')
|
||||||
|
result.stdout.fnmatch_lines(['* 1 error in *'])
|
||||||
|
assert 'INTERNALERROR' not in result.stderr.str()
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ def pytest_addoption(parser):
|
|||||||
help=("add a global test environment, XSpec-syntax. "))
|
help=("add a global test environment, XSpec-syntax. "))
|
||||||
|
|
||||||
|
|
||||||
def pytest_funcarg__specssh(request):
|
@pytest.fixture
|
||||||
|
def specssh(request):
|
||||||
return getspecssh(request.config)
|
return getspecssh(request.config)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class MockNode:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.sent = []
|
self.sent = []
|
||||||
self.gateway = MockGateway()
|
self.gateway = MockGateway()
|
||||||
|
self._shutdown = False
|
||||||
|
|
||||||
def send_runtest_some(self, indices):
|
def send_runtest_some(self, indices):
|
||||||
self.sent.extend(indices)
|
self.sent.extend(indices)
|
||||||
@@ -37,6 +38,10 @@ class MockNode:
|
|||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
self._shutdown = True
|
self._shutdown = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def shutting_down(self):
|
||||||
|
return self._shutdown
|
||||||
|
|
||||||
|
|
||||||
def dumpqueue(queue):
|
def dumpqueue(queue):
|
||||||
while queue.qsize():
|
while queue.qsize():
|
||||||
@@ -100,16 +105,15 @@ class TestLoadScheduling:
|
|||||||
assert sched.node2collection[node2] == collection
|
assert sched.node2collection[node2] == collection
|
||||||
sched.init_distribute()
|
sched.init_distribute()
|
||||||
assert not sched.pending
|
assert not sched.pending
|
||||||
assert not sched.tests_finished()
|
assert sched.tests_finished()
|
||||||
assert len(node1.sent) == 2
|
assert len(node1.sent) == 1
|
||||||
assert len(node2.sent) == 0
|
assert len(node2.sent) == 1
|
||||||
assert node1.sent == [0, 1]
|
assert node1.sent == [0]
|
||||||
|
assert node2.sent == [1]
|
||||||
sched.remove_item(node1, node1.sent[0])
|
sched.remove_item(node1, node1.sent[0])
|
||||||
assert sched.tests_finished()
|
assert sched.tests_finished()
|
||||||
sched.remove_item(node1, node1.sent[1])
|
|
||||||
assert sched.tests_finished()
|
|
||||||
|
|
||||||
def test_init_distribute_chunksize(self):
|
def test_init_distribute_batch_size(self):
|
||||||
sched = LoadScheduling(2)
|
sched = LoadScheduling(2)
|
||||||
sched.addnode(MockNode())
|
sched.addnode(MockNode())
|
||||||
sched.addnode(MockNode())
|
sched.addnode(MockNode())
|
||||||
@@ -121,18 +125,56 @@ class TestLoadScheduling:
|
|||||||
# assert not sched.tests_finished()
|
# assert not sched.tests_finished()
|
||||||
sent1 = node1.sent
|
sent1 = node1.sent
|
||||||
sent2 = node2.sent
|
sent2 = node2.sent
|
||||||
assert sent1 == [0, 1]
|
assert sent1 == [0, 2]
|
||||||
assert sent2 == [2, 3]
|
assert sent2 == [1, 3]
|
||||||
assert sched.pending == [4, 5]
|
assert sched.pending == [4, 5]
|
||||||
assert sched.node2pending[node1] == sent1
|
assert sched.node2pending[node1] == sent1
|
||||||
assert sched.node2pending[node2] == sent2
|
assert sched.node2pending[node2] == sent2
|
||||||
assert len(sched.pending) == 2
|
assert len(sched.pending) == 2
|
||||||
sched.remove_item(node1, 0)
|
sched.remove_item(node1, 0)
|
||||||
assert node1.sent == [0, 1, 4]
|
assert node1.sent == [0, 2, 4]
|
||||||
assert sched.pending == [5]
|
assert sched.pending == [5]
|
||||||
assert node2.sent == [2, 3]
|
assert node2.sent == [1, 3]
|
||||||
sched.remove_item(node1, 1)
|
sched.remove_item(node1, 2)
|
||||||
assert node1.sent == [0, 1, 4, 5]
|
assert node1.sent == [0, 2, 4, 5]
|
||||||
|
assert not sched.pending
|
||||||
|
|
||||||
|
def test_init_distribute_fewer_tests_than_nodes(self):
|
||||||
|
sched = LoadScheduling(2)
|
||||||
|
sched.addnode(MockNode())
|
||||||
|
sched.addnode(MockNode())
|
||||||
|
sched.addnode(MockNode())
|
||||||
|
node1, node2, node3 = sched.nodes
|
||||||
|
col = ["xyz"] * 2
|
||||||
|
sched.addnode_collection(node1, col)
|
||||||
|
sched.addnode_collection(node2, col)
|
||||||
|
sched.init_distribute()
|
||||||
|
# assert not sched.tests_finished()
|
||||||
|
sent1 = node1.sent
|
||||||
|
sent2 = node2.sent
|
||||||
|
sent3 = node3.sent
|
||||||
|
assert sent1 == [0]
|
||||||
|
assert sent2 == [1]
|
||||||
|
assert sent3 == []
|
||||||
|
assert not sched.pending
|
||||||
|
|
||||||
|
def test_init_distribute_fewer_than_two_tests_per_node(self):
|
||||||
|
sched = LoadScheduling(2)
|
||||||
|
sched.addnode(MockNode())
|
||||||
|
sched.addnode(MockNode())
|
||||||
|
sched.addnode(MockNode())
|
||||||
|
node1, node2, node3 = sched.nodes
|
||||||
|
col = ["xyz"] * 5
|
||||||
|
sched.addnode_collection(node1, col)
|
||||||
|
sched.addnode_collection(node2, col)
|
||||||
|
sched.init_distribute()
|
||||||
|
# assert not sched.tests_finished()
|
||||||
|
sent1 = node1.sent
|
||||||
|
sent2 = node2.sent
|
||||||
|
sent3 = node3.sent
|
||||||
|
assert sent1 == [0, 3]
|
||||||
|
assert sent2 == [1, 4]
|
||||||
|
assert sent3 == [2]
|
||||||
assert not sched.pending
|
assert not sched.pending
|
||||||
|
|
||||||
def test_add_remove_node(self):
|
def test_add_remove_node(self):
|
||||||
@@ -235,7 +277,7 @@ def test_report_collection_diff_different():
|
|||||||
' ccc\n'
|
' ccc\n'
|
||||||
'-YYY')
|
'-YYY')
|
||||||
|
|
||||||
msg = report_collection_diff(from_collection, to_collection, 1, 2)
|
msg = report_collection_diff(from_collection, to_collection, '1', '2')
|
||||||
assert msg == error_message
|
assert msg == error_message
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
50
testing/test_newhooks.py
Normal file
50
testing/test_newhooks.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
class TestHooks:
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def create_test_file(self, testdir):
|
||||||
|
testdir.makepyfile("""
|
||||||
|
import os
|
||||||
|
def test_a(): pass
|
||||||
|
def test_b(): pass
|
||||||
|
def test_c(): pass
|
||||||
|
""")
|
||||||
|
|
||||||
|
def test_runtest_logreport(self, testdir):
|
||||||
|
"""Test that log reports from pytest_runtest_logreport when running
|
||||||
|
with xdist contain a "node" attribute. (#8)
|
||||||
|
"""
|
||||||
|
testdir.makeconftest("""
|
||||||
|
def pytest_runtest_logreport(report):
|
||||||
|
if hasattr(report, 'node'):
|
||||||
|
slaveid = report.node.slaveinput['slaveid']
|
||||||
|
if report.when == "call":
|
||||||
|
print("HOOK: %s %s" % (report.nodeid, slaveid))
|
||||||
|
""")
|
||||||
|
res = testdir.runpytest('-n1', '-s')
|
||||||
|
res.stdout.fnmatch_lines([
|
||||||
|
'*HOOK: test_runtest_logreport.py::test_a gw0*',
|
||||||
|
'*HOOK: test_runtest_logreport.py::test_b gw0*',
|
||||||
|
'*HOOK: test_runtest_logreport.py::test_c gw0*',
|
||||||
|
'*3 passed*',
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_node_collection_finished(self, testdir):
|
||||||
|
"""Test pytest_xdist_node_collection_finished hook (#8).
|
||||||
|
"""
|
||||||
|
testdir.makeconftest("""
|
||||||
|
def pytest_xdist_node_collection_finished(node, ids):
|
||||||
|
slaveid = node.slaveinput['slaveid']
|
||||||
|
stripped_ids = [x.split('::')[1] for x in ids]
|
||||||
|
print("HOOK: %s %s" % (slaveid, ', '.join(stripped_ids)))
|
||||||
|
""")
|
||||||
|
res = testdir.runpytest('-n2', '-s')
|
||||||
|
res.stdout.fnmatch_lines_random([
|
||||||
|
'*HOOK: gw0 test_a, test_b, test_c',
|
||||||
|
'*HOOK: gw1 test_a, test_b, test_c',
|
||||||
|
])
|
||||||
|
res.stdout.fnmatch_lines([
|
||||||
|
'*3 passed*',
|
||||||
|
])
|
||||||
@@ -25,6 +25,17 @@ def test_dist_options(testdir):
|
|||||||
assert config.option.dist == "load"
|
assert config.option.dist == "load"
|
||||||
|
|
||||||
|
|
||||||
|
def test_auto_detect_cpus(testdir, monkeypatch):
|
||||||
|
import multiprocessing
|
||||||
|
monkeypatch.setattr(multiprocessing, 'cpu_count', lambda: 99)
|
||||||
|
|
||||||
|
config = testdir.parseconfigure("-n2")
|
||||||
|
assert config.getoption('numprocesses') == 2
|
||||||
|
|
||||||
|
config = testdir.parseconfigure("-nauto")
|
||||||
|
assert config.getoption('numprocesses') == 99
|
||||||
|
|
||||||
|
|
||||||
class TestDistOptions:
|
class TestDistOptions:
|
||||||
def test_getxspecs(self, testdir):
|
def test_getxspecs(self, testdir):
|
||||||
config = testdir.parseconfigure("--tx=popen", "--tx", "ssh=xyz")
|
config = testdir.parseconfigure("--tx=popen", "--tx", "ssh=xyz")
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import py
|
import py
|
||||||
|
import pytest
|
||||||
from xdist.slavemanage import SlaveController, unserialize_report
|
from xdist.slavemanage import SlaveController, unserialize_report
|
||||||
from xdist.remote import serialize_report
|
from xdist.remote import serialize_report
|
||||||
import execnet
|
import execnet
|
||||||
queue = py.builtin._tryimport("queue", "Queue")
|
|
||||||
import marshal
|
import marshal
|
||||||
|
|
||||||
|
queue = py.builtin._tryimport("queue", "Queue")
|
||||||
|
|
||||||
WAIT_TIMEOUT = 10.0
|
WAIT_TIMEOUT = 10.0
|
||||||
|
|
||||||
|
|
||||||
@@ -38,7 +40,12 @@ class SlaveSetup:
|
|||||||
self.gateway = execnet.makegateway()
|
self.gateway = execnet.makegateway()
|
||||||
self.config = config = self.testdir.parseconfigure()
|
self.config = config = self.testdir.parseconfigure()
|
||||||
putevent = self.use_callback and self.events.put or None
|
putevent = self.use_callback and self.events.put or None
|
||||||
self.slp = SlaveController(None, self.gateway, config, putevent)
|
|
||||||
|
class DummyMananger:
|
||||||
|
specs = [0, 1]
|
||||||
|
|
||||||
|
self.slp = SlaveController(DummyMananger, self.gateway, config,
|
||||||
|
putevent)
|
||||||
self.request.addfinalizer(self.slp.ensure_teardown)
|
self.request.addfinalizer(self.slp.ensure_teardown)
|
||||||
self.slp.setup()
|
self.slp.setup()
|
||||||
|
|
||||||
@@ -57,10 +64,12 @@ class SlaveSetup:
|
|||||||
self.slp.sendcommand(name, **kwargs)
|
self.slp.sendcommand(name, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def pytest_funcarg__slave(request):
|
@pytest.fixture
|
||||||
|
def slave(request):
|
||||||
return SlaveSetup(request)
|
return SlaveSetup(request)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.xfail(reason='#59')
|
||||||
def test_remoteinitconfig(testdir):
|
def test_remoteinitconfig(testdir):
|
||||||
from xdist.remote import remote_initconfig
|
from xdist.remote import remote_initconfig
|
||||||
config1 = testdir.parseconfig()
|
config1 = testdir.parseconfig()
|
||||||
@@ -174,6 +183,8 @@ class TestSlaveInteractor:
|
|||||||
ev = slave.popevent("slavefinished")
|
ev = slave.popevent("slavefinished")
|
||||||
assert 'slaveoutput' in ev.kwargs
|
assert 'slaveoutput' in ev.kwargs
|
||||||
|
|
||||||
|
@pytest.mark.skipif(pytest.__version__ >= '3.0',
|
||||||
|
reason='skip at module level illegal in pytest 3.0')
|
||||||
def test_remote_collect_skip(self, slave):
|
def test_remote_collect_skip(self, slave):
|
||||||
slave.testdir.makepyfile("""
|
slave.testdir.makepyfile("""
|
||||||
import py
|
import py
|
||||||
@@ -251,3 +262,14 @@ class TestSlaveInteractor:
|
|||||||
("pytest_pycollect_makeitem", "name == 'test_func'"),
|
("pytest_pycollect_makeitem", "name == 'test_func'"),
|
||||||
("pytest_collectreport", "report.collector.fspath == bbb"),
|
("pytest_collectreport", "report.collector.fspath == bbb"),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def test_remote_env_vars(testdir):
|
||||||
|
testdir.makepyfile('''
|
||||||
|
import os
|
||||||
|
def test():
|
||||||
|
assert os.environ['PYTEST_XDIST_WORKER'] in ('gw0', 'gw1')
|
||||||
|
assert os.environ['PYTEST_XDIST_WORKER_COUNT'] == '2'
|
||||||
|
''')
|
||||||
|
result = testdir.runpytest('-n2', '--max-slave-restart=0')
|
||||||
|
assert result.ret == 0
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ from xdist.slavemanage import HostRSync, NodeManager
|
|||||||
pytest_plugins = "pytester"
|
pytest_plugins = "pytester"
|
||||||
|
|
||||||
|
|
||||||
def pytest_funcarg__hookrecorder(request, config):
|
@pytest.fixture
|
||||||
|
def hookrecorder(request, config):
|
||||||
hookrecorder = HookRecorder(config.pluginmanager)
|
hookrecorder = HookRecorder(config.pluginmanager)
|
||||||
if hasattr(hookrecorder, "start_recording"):
|
if hasattr(hookrecorder, "start_recording"):
|
||||||
hookrecorder.start_recording(newhooks)
|
hookrecorder.start_recording(newhooks)
|
||||||
@@ -16,11 +17,13 @@ def pytest_funcarg__hookrecorder(request, config):
|
|||||||
return hookrecorder
|
return hookrecorder
|
||||||
|
|
||||||
|
|
||||||
def pytest_funcarg__config(testdir):
|
@pytest.fixture
|
||||||
|
def config(testdir):
|
||||||
return testdir.parseconfig()
|
return testdir.parseconfig()
|
||||||
|
|
||||||
|
|
||||||
def pytest_funcarg__mysetup(tmpdir):
|
@pytest.fixture
|
||||||
|
def mysetup(tmpdir):
|
||||||
class mysetup:
|
class mysetup:
|
||||||
source = tmpdir.mkdir("source")
|
source = tmpdir.mkdir("source")
|
||||||
dest = tmpdir.mkdir("dest")
|
dest = tmpdir.mkdir("dest")
|
||||||
|
|||||||
26
tox.ini
26
tox.ini
@@ -1,19 +1,27 @@
|
|||||||
[tox]
|
[tox]
|
||||||
|
# if you change the envlist, please update .travis.yml file as well
|
||||||
envlist=
|
envlist=
|
||||||
py{26,33,34,27}-pytest2{4,5,6,7},py{27,34}-pytest27-pexpect,flakes,readme
|
py{26,27,34}-pytest2{6,7,8,9}
|
||||||
|
py35-pytest2{7,8,9}
|
||||||
|
py{27,35}-pytest28-pexpect
|
||||||
|
flakes
|
||||||
|
readme
|
||||||
|
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
changedir=testing
|
changedir=testing
|
||||||
|
passenv = USER USERNAME
|
||||||
deps =
|
deps =
|
||||||
pycmd
|
pycmd
|
||||||
setuptools_scm # to avoid .eggs
|
# to avoid .eggs
|
||||||
pytest24: pytest~=2.4.0
|
setuptools_scm
|
||||||
pytest25: pytest~=2.5.0
|
pytest26: pytest~=2.6.1
|
||||||
|
pytest27: pytest~=2.7.2
|
||||||
pytest26: pytest~=2.6.1
|
pytest28: pytest~=2.8.7
|
||||||
pytest27: pytest~=2.7.2
|
pytest29: pytest~=2.9.1
|
||||||
pexpect: pexpect
|
pexpect: pexpect
|
||||||
|
platform=
|
||||||
|
pexpect: linux|darwin
|
||||||
commands=
|
commands=
|
||||||
# always clean to avoid code unmarshal mismatch on old python/pytest
|
# always clean to avoid code unmarshal mismatch on old python/pytest
|
||||||
py.cleanup -aq
|
py.cleanup -aq
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
__all__ = ['__version__']
|
|
||||||
from xdist._version import version as __version__
|
from xdist._version import version as __version__
|
||||||
|
|
||||||
|
__all__ = ['__version__']
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import difflib
|
import difflib
|
||||||
|
import itertools
|
||||||
from _pytest.runner import CollectReport
|
from _pytest.runner import CollectReport
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -14,12 +15,12 @@ class EachScheduling:
|
|||||||
|
|
||||||
If a node gets added after the test run is started then it is
|
If a node gets added after the test run is started then it is
|
||||||
assumed to replace a node which got removed before it finished
|
assumed to replace a node which got removed before it finished
|
||||||
it's collection. In this case it will only be used if a a node
|
its collection. In this case it will only be used if a node
|
||||||
with the same spec got removed earlier.
|
with the same spec got removed earlier.
|
||||||
|
|
||||||
Any nodes added after the run is started will only get items
|
Any nodes added after the run is started will only get items
|
||||||
assigned if a node with matching spec was removed before it
|
assigned if a node with a matching spec was removed before it
|
||||||
finished all it's pending items. The new node will then be
|
finished all its pending items. The new node will then be
|
||||||
assigned the remaining items from the removed node.
|
assigned the remaining items from the removed node.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -47,8 +48,8 @@ class EachScheduling:
|
|||||||
"""Return True if there are pending test items
|
"""Return True if there are pending test items
|
||||||
|
|
||||||
This indicates that collection has finished and nodes are
|
This indicates that collection has finished and nodes are
|
||||||
still processing test items, so can be thought of as "the
|
still processing test items, so this can be thought of as
|
||||||
scheduler is active".
|
"the scheduler is active".
|
||||||
"""
|
"""
|
||||||
for pending in self.node2pending.values():
|
for pending in self.node2pending.values():
|
||||||
if pending:
|
if pending:
|
||||||
@@ -73,10 +74,10 @@ class EachScheduling:
|
|||||||
"""Add the collected test items from a node
|
"""Add the collected test items from a node
|
||||||
|
|
||||||
Collection is complete once all nodes have submitted their
|
Collection is complete once all nodes have submitted their
|
||||||
collection. In this case it's peding list is set to an empty
|
collection. In this case its pending list is set to an empty
|
||||||
list. When the collection is already completed this
|
list. When the collection is already completed this
|
||||||
submission is from a node which was restarted to replace a
|
submission is from a node which was restarted to replace a
|
||||||
dead node. In this case we already assing the pending items
|
dead node. In this case we already assign the pending items
|
||||||
here. In either case ``.init_distribute()`` will instruct the
|
here. In either case ``.init_distribute()`` will instruct the
|
||||||
node to start running the required tests.
|
node to start running the required tests.
|
||||||
"""
|
"""
|
||||||
@@ -120,7 +121,7 @@ class EachScheduling:
|
|||||||
If the node's pending list is empty it is a new node which
|
If the node's pending list is empty it is a new node which
|
||||||
needs to run all the tests. If the pending list is already
|
needs to run all the tests. If the pending list is already
|
||||||
populated (by ``.addnode_collection()``) then it replaces a
|
populated (by ``.addnode_collection()``) then it replaces a
|
||||||
died node and we only need to run those tests.
|
dead node and we only need to run those tests.
|
||||||
"""
|
"""
|
||||||
assert self.collection_is_completed
|
assert self.collection_is_completed
|
||||||
for node, pending in self.node2pending.items():
|
for node, pending in self.node2pending.items():
|
||||||
@@ -135,19 +136,19 @@ class EachScheduling:
|
|||||||
|
|
||||||
|
|
||||||
class LoadScheduling:
|
class LoadScheduling:
|
||||||
"""Implement load scheduling accross nodes.
|
"""Implement load scheduling across nodes.
|
||||||
|
|
||||||
This distributes the tests collected across all nodes so each test
|
This distributes the tests collected across all nodes so each test
|
||||||
is run just once. All nodes collect and submit the test suit and
|
is run just once. All nodes collect and submit the test suite and
|
||||||
when all collections are received it is verified they are
|
when all collections are received it is verified they are
|
||||||
identical collections. Then the collection gets devided up in
|
identical collections. Then the collection gets divided up in
|
||||||
chunks and chunks get submitted to nodes. Whenver a node finishes
|
chunks and chunks get submitted to nodes. Whenever a node finishes
|
||||||
an item they call ``.remove_item()`` which will trigger the
|
an item, it calls ``.remove_item()`` which will trigger the
|
||||||
scheduler to assign more tests if the number of pending tests for
|
scheduler to assign more tests if the number of pending tests for
|
||||||
the node falls below a low-watermark.
|
the node falls below a low-watermark.
|
||||||
|
|
||||||
When created ``numnodes`` defines how many nodes are expected to
|
When created, ``numnodes`` defines how many nodes are expected to
|
||||||
submit a collection, this is used to know when all nodes have
|
submit a collection. This is used to know when all nodes have
|
||||||
finished collection or how large the chunks need to be created.
|
finished collection or how large the chunks need to be created.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
@@ -155,7 +156,7 @@ class LoadScheduling:
|
|||||||
:numnodes: The expected number of nodes taking part. The actual
|
:numnodes: The expected number of nodes taking part. The actual
|
||||||
number of nodes will vary during the scheduler's lifetime as
|
number of nodes will vary during the scheduler's lifetime as
|
||||||
nodes are added by the DSession as they are brought up and
|
nodes are added by the DSession as they are brought up and
|
||||||
removed either because of a died node or normal shutdown. This
|
removed either because of a dead node or normal shutdown. This
|
||||||
number is primarily used to know when the initial collection is
|
number is primarily used to know when the initial collection is
|
||||||
completed.
|
completed.
|
||||||
|
|
||||||
@@ -211,8 +212,8 @@ class LoadScheduling:
|
|||||||
"""Return True if there are pending test items
|
"""Return True if there are pending test items
|
||||||
|
|
||||||
This indicates that collection has finished and nodes are
|
This indicates that collection has finished and nodes are
|
||||||
still processing test items, so can be thought of as "the
|
still processing test items, so this can be thought of as
|
||||||
scheduler is active".
|
"the scheduler is active".
|
||||||
"""
|
"""
|
||||||
if self.pending:
|
if self.pending:
|
||||||
return True
|
return True
|
||||||
@@ -226,13 +227,13 @@ class LoadScheduling:
|
|||||||
return bool(self.node2pending)
|
return bool(self.node2pending)
|
||||||
|
|
||||||
def addnode(self, node):
|
def addnode(self, node):
|
||||||
"""Add a new node in the scheduler.
|
"""Add a new node to the scheduler.
|
||||||
|
|
||||||
From now on the node will be allocated chunks of tests to
|
From now on the node will be allocated chunks of tests to
|
||||||
execute.
|
execute.
|
||||||
|
|
||||||
Called by the ``DSession.slave_slaveready`` hook when it
|
Called by the ``DSession.slave_slaveready`` hook when it
|
||||||
sucessfully bootstrapped a new node.
|
successfully bootstraps a new node.
|
||||||
"""
|
"""
|
||||||
assert node not in self.node2pending
|
assert node not in self.node2pending
|
||||||
self.node2pending[node] = []
|
self.node2pending[node] = []
|
||||||
@@ -289,6 +290,9 @@ class LoadScheduling:
|
|||||||
``duration`` of the last test is optionally used as a
|
``duration`` of the last test is optionally used as a
|
||||||
heuristic to influence how many tests the node is assigned.
|
heuristic to influence how many tests the node is assigned.
|
||||||
"""
|
"""
|
||||||
|
if node.shutting_down:
|
||||||
|
return
|
||||||
|
|
||||||
if self.pending:
|
if self.pending:
|
||||||
# how many nodes do we have?
|
# how many nodes do we have?
|
||||||
num_nodes = len(self.node2pending)
|
num_nodes = len(self.node2pending)
|
||||||
@@ -308,7 +312,7 @@ class LoadScheduling:
|
|||||||
self.log("num items waiting for node:", len(self.pending))
|
self.log("num items waiting for node:", len(self.pending))
|
||||||
|
|
||||||
def remove_node(self, node):
|
def remove_node(self, node):
|
||||||
"""Remove an node from the scheduler
|
"""Remove a node from the scheduler
|
||||||
|
|
||||||
This should be called either when the node crashed or at
|
This should be called either when the node crashed or at
|
||||||
shutdown time. In the former case any pending items assigned
|
shutdown time. In the former case any pending items assigned
|
||||||
@@ -346,7 +350,7 @@ class LoadScheduling:
|
|||||||
"""
|
"""
|
||||||
assert self.collection_is_completed
|
assert self.collection_is_completed
|
||||||
|
|
||||||
# Initial distribution already happend, reschedule on all nodes
|
# Initial distribution already happened, reschedule on all nodes
|
||||||
if self.collection is not None:
|
if self.collection is not None:
|
||||||
for node in self.nodes:
|
for node in self.nodes:
|
||||||
self.check_schedule(node)
|
self.check_schedule(node)
|
||||||
@@ -363,13 +367,22 @@ class LoadScheduling:
|
|||||||
if not self.collection:
|
if not self.collection:
|
||||||
return
|
return
|
||||||
|
|
||||||
# how many items per node do we have about?
|
# Send a batch of tests to run. If we don't have at least two
|
||||||
items_per_node = len(self.collection) // len(self.node2pending)
|
# tests per node, we have to send them all so that we can send
|
||||||
# take a fraction of tests for initial distribution
|
# shutdown signals and get all nodes working.
|
||||||
node_chunksize = max(items_per_node // 4, 2)
|
initial_batch = max(len(self.pending) // 4,
|
||||||
# and initialize each node with a chunk of tests
|
2 * len(self.nodes))
|
||||||
for node in self.nodes:
|
|
||||||
self._send_tests(node, node_chunksize)
|
# distribute tests round-robin up to the batch size
|
||||||
|
# (or until we run out)
|
||||||
|
nodes = itertools.cycle(self.nodes)
|
||||||
|
for i in range(initial_batch):
|
||||||
|
self._send_tests(next(nodes), 1)
|
||||||
|
|
||||||
|
if not self.pending:
|
||||||
|
# initial distribution sent all tests, start node shutdown
|
||||||
|
for node in self.nodes:
|
||||||
|
node.shutdown()
|
||||||
|
|
||||||
def _send_tests(self, node, num):
|
def _send_tests(self, node, num):
|
||||||
tests_per_node = self.pending[:num]
|
tests_per_node = self.pending[:num]
|
||||||
@@ -477,7 +490,7 @@ class DSession:
|
|||||||
"""Return True if the distributed session has finished
|
"""Return True if the distributed session has finished
|
||||||
|
|
||||||
This means all nodes have executed all test items. This is
|
This means all nodes have executed all test items. This is
|
||||||
used to by pytest_runtestloop to break out of it's loop.
|
used by pytest_runtestloop to break out of its loop.
|
||||||
"""
|
"""
|
||||||
return bool(self.shuttingdown and not self._active_nodes)
|
return bool(self.shuttingdown and not self._active_nodes)
|
||||||
|
|
||||||
@@ -522,6 +535,7 @@ class DSession:
|
|||||||
while not self.session_finished:
|
while not self.session_finished:
|
||||||
self.loop_once()
|
self.loop_once()
|
||||||
if self.shouldstop:
|
if self.shouldstop:
|
||||||
|
self.triggershutdown()
|
||||||
raise Interrupted(str(self.shouldstop))
|
raise Interrupted(str(self.shouldstop))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -566,7 +580,7 @@ class DSession:
|
|||||||
|
|
||||||
Removes the node from the scheduler.
|
Removes the node from the scheduler.
|
||||||
|
|
||||||
The node might not be the scheduler if it had not emitted
|
The node might not be in the scheduler if it had not emitted
|
||||||
slaveready before shutdown was triggered.
|
slaveready before shutdown was triggered.
|
||||||
"""
|
"""
|
||||||
self.config.hook.pytest_testnodedown(node=node, error=None)
|
self.config.hook.pytest_testnodedown(node=node, error=None)
|
||||||
@@ -610,12 +624,14 @@ class DSession:
|
|||||||
|
|
||||||
This adds the collection for this node to the scheduler. If
|
This adds the collection for this node to the scheduler. If
|
||||||
the scheduler indicates collection is finished (i.e. all
|
the scheduler indicates collection is finished (i.e. all
|
||||||
initial nodes have submitted their collection), then tells the
|
initial nodes have submitted their collections), then tells the
|
||||||
scheduler to schedule the collected items. When initiating
|
scheduler to schedule the collected items. When initiating
|
||||||
scheduling the first time it logs which scheduler is in use.
|
scheduling the first time it logs which scheduler is in use.
|
||||||
"""
|
"""
|
||||||
if self.shuttingdown:
|
if self.shuttingdown:
|
||||||
return
|
return
|
||||||
|
self.config.hook.pytest_xdist_node_collection_finished(node=node,
|
||||||
|
ids=ids)
|
||||||
# tell session which items were effectively collected otherwise
|
# tell session which items were effectively collected otherwise
|
||||||
# the master node will finish the session with EXIT_NOTESTSCOLLECTED
|
# the master node will finish the session with EXIT_NOTESTSCOLLECTED
|
||||||
self._session.testscollected = len(ids)
|
self._session.testscollected = len(ids)
|
||||||
@@ -638,7 +654,7 @@ class DSession:
|
|||||||
def slave_testreport(self, node, rep):
|
def slave_testreport(self, node, rep):
|
||||||
"""Emitted when a node calls the pytest_runtest_logreport hook.
|
"""Emitted when a node calls the pytest_runtest_logreport hook.
|
||||||
|
|
||||||
If the node indicates it is finished with a test item remove
|
If the node indicates it is finished with a test item, remove
|
||||||
the item from the pending list in the scheduler.
|
the item from the pending list in the scheduler.
|
||||||
"""
|
"""
|
||||||
if rep.when == "call" or (rep.when == "setup" and not rep.passed):
|
if rep.when == "call" or (rep.when == "setup" and not rep.passed):
|
||||||
@@ -656,9 +672,9 @@ class DSession:
|
|||||||
def _clone_node(self, node):
|
def _clone_node(self, node):
|
||||||
"""Return new node based on an existing one.
|
"""Return new node based on an existing one.
|
||||||
|
|
||||||
This is normally for when a node died, this will copy the spec
|
This is normally for when a node dies, this will copy the spec
|
||||||
of the existing node and create a new one with a new id. The
|
of the existing node and create a new one with a new id. The
|
||||||
new node will have been setup so will start calling the
|
new node will have been setup so it will start calling the
|
||||||
"slave_*" hooks and do work soon.
|
"slave_*" hooks and do work soon.
|
||||||
"""
|
"""
|
||||||
spec = node.gateway.spec
|
spec = node.gateway.spec
|
||||||
@@ -707,17 +723,18 @@ class TerminalDistReporter:
|
|||||||
self.tr = config.pluginmanager.getplugin("terminalreporter")
|
self.tr = config.pluginmanager.getplugin("terminalreporter")
|
||||||
self._status = {}
|
self._status = {}
|
||||||
self._lastlen = 0
|
self._lastlen = 0
|
||||||
|
self._isatty = getattr(self.tr, 'isatty', self.tr.hasmarkup)
|
||||||
|
|
||||||
def write_line(self, msg):
|
def write_line(self, msg):
|
||||||
self.tr.write_line(msg)
|
self.tr.write_line(msg)
|
||||||
|
|
||||||
def ensure_show_status(self):
|
def ensure_show_status(self):
|
||||||
if not self.tr.hasmarkup:
|
if not self._isatty:
|
||||||
self.write_line(self.getstatus())
|
self.write_line(self.getstatus())
|
||||||
|
|
||||||
def setstatus(self, spec, status, show=True):
|
def setstatus(self, spec, status, show=True):
|
||||||
self._status[spec.id] = status
|
self._status[spec.id] = status
|
||||||
if show and self.tr.hasmarkup:
|
if show and self._isatty:
|
||||||
self.rewrite(self.getstatus())
|
self.rewrite(self.getstatus())
|
||||||
|
|
||||||
def getstatus(self):
|
def getstatus(self):
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ def pytest_addoption(parser):
|
|||||||
def pytest_cmdline_main(config):
|
def pytest_cmdline_main(config):
|
||||||
|
|
||||||
if config.getoption("looponfail"):
|
if config.getoption("looponfail"):
|
||||||
|
usepdb = config.getoption('usepdb') # a core option
|
||||||
|
if usepdb:
|
||||||
|
raise pytest.UsageError(
|
||||||
|
"--pdb incompatible with --looponfail.")
|
||||||
looponfail_main(config)
|
looponfail_main(config)
|
||||||
return 2 # looponfail only can get stop with ctrl-C anyway
|
return 2 # looponfail only can get stop with ctrl-C anyway
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,17 @@
|
|||||||
|
"""
|
||||||
|
xdist hooks.
|
||||||
|
|
||||||
|
Additionally, pytest-xdist will also decorate a few other hooks
|
||||||
|
with the worker instance that executed the hook originally:
|
||||||
|
|
||||||
|
``pytest_runtest_logreport``: ``rep`` parameter has a ``node`` attribute.
|
||||||
|
|
||||||
|
You can use this hooks just as you would use normal pytest hooks, but some care
|
||||||
|
must be taken in plugins in case ``xdist`` is not installed. Please see:
|
||||||
|
|
||||||
|
http://pytest.org/latest/writing_plugins.html#optionally-using-hooks-from-3rd-party-plugins
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def pytest_xdist_setupnodes(config, specs):
|
def pytest_xdist_setupnodes(config, specs):
|
||||||
""" called before any remote node is set up. """
|
""" called before any remote node is set up. """
|
||||||
@@ -25,3 +39,8 @@ def pytest_testnodeready(node):
|
|||||||
|
|
||||||
def pytest_testnodedown(node, error):
|
def pytest_testnodedown(node, error):
|
||||||
""" Test Node is down. """
|
""" Test Node is down. """
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_xdist_node_collection_finished(node, ids):
|
||||||
|
"""called by the master node when a node finishes collecting.
|
||||||
|
"""
|
||||||
|
|||||||
@@ -101,12 +101,20 @@ def pytest_cmdline_main(config):
|
|||||||
config.option.dist = "load"
|
config.option.dist = "load"
|
||||||
val = config.getvalue
|
val = config.getvalue
|
||||||
if not val("collectonly"):
|
if not val("collectonly"):
|
||||||
usepdb = config.option.usepdb # a core option
|
usepdb = config.getoption('usepdb') # a core option
|
||||||
if val("looponfail"):
|
if val("dist") != "no":
|
||||||
if usepdb:
|
|
||||||
raise pytest.UsageError(
|
|
||||||
"--pdb incompatible with --looponfail.")
|
|
||||||
elif val("dist") != "no":
|
|
||||||
if usepdb:
|
if usepdb:
|
||||||
raise pytest.UsageError(
|
raise pytest.UsageError(
|
||||||
"--pdb incompatible with distributing tests.")
|
"--pdb incompatible with distributing tests.")
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# fixtures
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def worker_id(request):
|
||||||
|
if hasattr(request.config, 'slaveinput'):
|
||||||
|
return request.config.slaveinput['slaveid']
|
||||||
|
else:
|
||||||
|
return 'master'
|
||||||
|
|||||||
@@ -46,7 +46,10 @@ class SlaveInteractor:
|
|||||||
self.log("entering main loop")
|
self.log("entering main loop")
|
||||||
torun = []
|
torun = []
|
||||||
while 1:
|
while 1:
|
||||||
name, kwargs = self.channel.receive()
|
try:
|
||||||
|
name, kwargs = self.channel.receive()
|
||||||
|
except EOFError:
|
||||||
|
return True
|
||||||
self.log("received command", name, kwargs)
|
self.log("received command", name, kwargs)
|
||||||
if name == "runtests":
|
if name == "runtests":
|
||||||
torun.extend(kwargs['indices'])
|
torun.extend(kwargs['indices'])
|
||||||
@@ -145,6 +148,8 @@ if __name__ == '__channelexec__':
|
|||||||
os.environ['PYTHONPATH'] = (
|
os.environ['PYTHONPATH'] = (
|
||||||
importpath + os.pathsep +
|
importpath + os.pathsep +
|
||||||
os.environ.get('PYTHONPATH', ''))
|
os.environ.get('PYTHONPATH', ''))
|
||||||
|
os.environ['PYTEST_XDIST_WORKER'] = slaveinput['slaveid']
|
||||||
|
os.environ['PYTEST_XDIST_WORKER_COUNT'] = str(slaveinput['slavecount'])
|
||||||
# os.environ['PYTHONPATH'] = importpath
|
# os.environ['PYTHONPATH'] = importpath
|
||||||
import py
|
import py
|
||||||
config = remote_initconfig(option_dict, args)
|
config = remote_initconfig(option_dict, args)
|
||||||
|
|||||||
@@ -205,8 +205,10 @@ class SlaveController(object):
|
|||||||
self.putevent = putevent
|
self.putevent = putevent
|
||||||
self.gateway = gateway
|
self.gateway = gateway
|
||||||
self.config = config
|
self.config = config
|
||||||
self.slaveinput = {'slaveid': gateway.id}
|
self.slaveinput = {'slaveid': gateway.id,
|
||||||
|
'slavecount': len(nodemanager.specs)}
|
||||||
self._down = False
|
self._down = False
|
||||||
|
self._shutdown_sent = False
|
||||||
self.log = py.log.Producer("slavectl-%s" % gateway.id)
|
self.log = py.log.Producer("slavectl-%s" % gateway.id)
|
||||||
if not self.config.option.debug:
|
if not self.config.option.debug:
|
||||||
py.log.setconsumer(self.log._keywords, None)
|
py.log.setconsumer(self.log._keywords, None)
|
||||||
@@ -214,6 +216,10 @@ class SlaveController(object):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<%s %s>" % (self.__class__.__name__, self.gateway.id,)
|
return "<%s %s>" % (self.__class__.__name__, self.gateway.id,)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def shutting_down(self):
|
||||||
|
return self._down or self._shutdown_sent
|
||||||
|
|
||||||
def setup(self):
|
def setup(self):
|
||||||
self.log("setting up slave session")
|
self.log("setting up slave session")
|
||||||
spec = self.gateway.spec
|
spec = self.gateway.spec
|
||||||
@@ -223,8 +229,9 @@ class SlaveController(object):
|
|||||||
option_dict = vars(self.config.option)
|
option_dict = vars(self.config.option)
|
||||||
if spec.popen:
|
if spec.popen:
|
||||||
name = "popen-%s" % self.gateway.id
|
name = "popen-%s" % self.gateway.id
|
||||||
basetemp = self.config._tmpdirhandler.getbasetemp()
|
if hasattr(self.config, '_tmpdirhandler'):
|
||||||
option_dict['basetemp'] = str(basetemp.join(name))
|
basetemp = self.config._tmpdirhandler.getbasetemp()
|
||||||
|
option_dict['basetemp'] = str(basetemp.join(name))
|
||||||
self.config.hook.pytest_configure_node(node=self)
|
self.config.hook.pytest_configure_node(node=self)
|
||||||
self.channel = self.gateway.remote_exec(xdist.remote)
|
self.channel = self.gateway.remote_exec(xdist.remote)
|
||||||
self.channel.send((self.slaveinput, args, option_dict))
|
self.channel.send((self.slaveinput, args, option_dict))
|
||||||
@@ -256,6 +263,7 @@ class SlaveController(object):
|
|||||||
self.sendcommand("shutdown")
|
self.sendcommand("shutdown")
|
||||||
except IOError:
|
except IOError:
|
||||||
pass
|
pass
|
||||||
|
self._shutdown_sent = True
|
||||||
|
|
||||||
def sendcommand(self, name, **kwargs):
|
def sendcommand(self, name, **kwargs):
|
||||||
""" send a named parametrized command to the other side. """
|
""" send a named parametrized command to the other side. """
|
||||||
|
|||||||
Reference in New Issue
Block a user