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
|
||||
language: python
|
||||
python:
|
||||
- '3.5.0b3'
|
||||
- '3.5'
|
||||
# command to install dependencies
|
||||
install: "pip install -U tox setuptools_scm"
|
||||
# # command to run tests
|
||||
env:
|
||||
matrix:
|
||||
- TESTENV=flakes
|
||||
- TESTENV=readme
|
||||
# matrix was trimmed to skip
|
||||
# some builds that are unnecessary/perceived redundant
|
||||
- TESTENV=py26-pytest24
|
||||
- TESTENV=py26-pytest25
|
||||
# note: please use "tox --listenvs" to populate the build matrix
|
||||
- TESTENV=py26-pytest26
|
||||
- TESTENV=py26-pytest27
|
||||
- TESTENV=py33-pytest27
|
||||
- TESTENV=py34-pytest24
|
||||
- TESTENV=py34-pytest25
|
||||
- TESTENV=py34-pytest26
|
||||
- TESTENV=py27-pytest24
|
||||
- TESTENV=py27-pytest25
|
||||
- TESTENV=py26-pytest28
|
||||
- TESTENV=py26-pytest29
|
||||
- TESTENV=py27-pytest26
|
||||
|
||||
- TESTENV=py27-pytest27-pexpect
|
||||
- TESTENV=py34-pytest27-pexpect
|
||||
# - TESTENV=py35-pytest27
|
||||
- TESTENV=pypy-pytest27
|
||||
- TESTENV=py27-pytest27
|
||||
- TESTENV=py27-pytest28
|
||||
- TESTENV=py27-pytest29
|
||||
- TESTENV=py34-pytest26
|
||||
- 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
|
||||
|
||||
|
||||
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
|
||||
|
||||
@@ -4,4 +4,4 @@ include README.txt
|
||||
include setup.py
|
||||
include tox.ini
|
||||
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
|
||||
: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
|
||||
=========================================
|
||||
|
||||
@@ -182,17 +185,47 @@ at once. The specifications strings use the `xspec syntax`_.
|
||||
|
||||
.. _`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
|
||||
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
pytest (since version 2.0) supports ini-style cofiguration.
|
||||
You can for example make running with three subprocesses
|
||||
your default like this::
|
||||
your default like this:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[pytest]
|
||||
addopts = -n3
|
||||
|
||||
You can also add default environments like this::
|
||||
You can also add default environments like this:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[pytest]
|
||||
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
|
||||
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]
|
||||
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:
|
||||
- ECHO "Filesystem root:"
|
||||
- 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"
|
||||
- C:\Python35\python -m pip install tox setuptools_scm
|
||||
|
||||
build: false # Not a C# project, build stuff at the test step instead.
|
||||
|
||||
test_script:
|
||||
# Build the compiled extension and run the project tests
|
||||
- "%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"
|
||||
- C:\Python35\python -m tox
|
||||
|
||||
@@ -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
|
||||
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
|
||||
|
||||
|
||||
1
setup.py
1
setup.py
@@ -24,6 +24,7 @@ setup(
|
||||
setup_requires=['setuptools_scm'],
|
||||
classifiers=[
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'Framework :: Pytest',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Operating System :: POSIX',
|
||||
|
||||
@@ -353,19 +353,6 @@ def test_terminate_on_hangingnode(testdir):
|
||||
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)
|
||||
def test_session_hooks(testdir):
|
||||
testdir.makeconftest("""
|
||||
@@ -424,7 +411,9 @@ def test_session_testscollected(testdir):
|
||||
|
||||
def test_funcarg_teardown_failure(testdir):
|
||||
p = testdir.makepyfile("""
|
||||
def pytest_funcarg__myarg(request):
|
||||
import pytest
|
||||
@pytest.fixture
|
||||
def myarg(request):
|
||||
def teardown(val):
|
||||
raise ValueError(val)
|
||||
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:
|
||||
def test_load_single(self, testdir):
|
||||
f = testdir.makepyfile("""
|
||||
@@ -564,6 +578,7 @@ class TestNodeFailure:
|
||||
"*1 failed*1 passed*",
|
||||
])
|
||||
|
||||
@pytest.mark.xfail(reason='#20: xdist race condition on node restart')
|
||||
def test_each_multiple(self, testdir):
|
||||
f = testdir.makepyfile("""
|
||||
import os
|
||||
@@ -607,3 +622,68 @@ class TestNodeFailure:
|
||||
"*Slave*crashed while running*",
|
||||
"*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. "))
|
||||
|
||||
|
||||
def pytest_funcarg__specssh(request):
|
||||
@pytest.fixture
|
||||
def specssh(request):
|
||||
return getspecssh(request.config)
|
||||
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ class MockNode:
|
||||
def __init__(self):
|
||||
self.sent = []
|
||||
self.gateway = MockGateway()
|
||||
self._shutdown = False
|
||||
|
||||
def send_runtest_some(self, indices):
|
||||
self.sent.extend(indices)
|
||||
@@ -37,6 +38,10 @@ class MockNode:
|
||||
def shutdown(self):
|
||||
self._shutdown = True
|
||||
|
||||
@property
|
||||
def shutting_down(self):
|
||||
return self._shutdown
|
||||
|
||||
|
||||
def dumpqueue(queue):
|
||||
while queue.qsize():
|
||||
@@ -100,16 +105,15 @@ class TestLoadScheduling:
|
||||
assert sched.node2collection[node2] == collection
|
||||
sched.init_distribute()
|
||||
assert not sched.pending
|
||||
assert not sched.tests_finished()
|
||||
assert len(node1.sent) == 2
|
||||
assert len(node2.sent) == 0
|
||||
assert node1.sent == [0, 1]
|
||||
assert sched.tests_finished()
|
||||
assert len(node1.sent) == 1
|
||||
assert len(node2.sent) == 1
|
||||
assert node1.sent == [0]
|
||||
assert node2.sent == [1]
|
||||
sched.remove_item(node1, node1.sent[0])
|
||||
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.addnode(MockNode())
|
||||
sched.addnode(MockNode())
|
||||
@@ -121,18 +125,56 @@ class TestLoadScheduling:
|
||||
# assert not sched.tests_finished()
|
||||
sent1 = node1.sent
|
||||
sent2 = node2.sent
|
||||
assert sent1 == [0, 1]
|
||||
assert sent2 == [2, 3]
|
||||
assert sent1 == [0, 2]
|
||||
assert sent2 == [1, 3]
|
||||
assert sched.pending == [4, 5]
|
||||
assert sched.node2pending[node1] == sent1
|
||||
assert sched.node2pending[node2] == sent2
|
||||
assert len(sched.pending) == 2
|
||||
sched.remove_item(node1, 0)
|
||||
assert node1.sent == [0, 1, 4]
|
||||
assert node1.sent == [0, 2, 4]
|
||||
assert sched.pending == [5]
|
||||
assert node2.sent == [2, 3]
|
||||
sched.remove_item(node1, 1)
|
||||
assert node1.sent == [0, 1, 4, 5]
|
||||
assert node2.sent == [1, 3]
|
||||
sched.remove_item(node1, 2)
|
||||
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
|
||||
|
||||
def test_add_remove_node(self):
|
||||
@@ -235,7 +277,7 @@ def test_report_collection_diff_different():
|
||||
' ccc\n'
|
||||
'-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
|
||||
|
||||
|
||||
|
||||
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"
|
||||
|
||||
|
||||
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:
|
||||
def test_getxspecs(self, testdir):
|
||||
config = testdir.parseconfigure("--tx=popen", "--tx", "ssh=xyz")
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import py
|
||||
import pytest
|
||||
from xdist.slavemanage import SlaveController, unserialize_report
|
||||
from xdist.remote import serialize_report
|
||||
import execnet
|
||||
queue = py.builtin._tryimport("queue", "Queue")
|
||||
import marshal
|
||||
|
||||
queue = py.builtin._tryimport("queue", "Queue")
|
||||
|
||||
WAIT_TIMEOUT = 10.0
|
||||
|
||||
|
||||
@@ -38,7 +40,12 @@ class SlaveSetup:
|
||||
self.gateway = execnet.makegateway()
|
||||
self.config = config = self.testdir.parseconfigure()
|
||||
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.slp.setup()
|
||||
|
||||
@@ -57,10 +64,12 @@ class SlaveSetup:
|
||||
self.slp.sendcommand(name, **kwargs)
|
||||
|
||||
|
||||
def pytest_funcarg__slave(request):
|
||||
@pytest.fixture
|
||||
def slave(request):
|
||||
return SlaveSetup(request)
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason='#59')
|
||||
def test_remoteinitconfig(testdir):
|
||||
from xdist.remote import remote_initconfig
|
||||
config1 = testdir.parseconfig()
|
||||
@@ -174,6 +183,8 @@ class TestSlaveInteractor:
|
||||
ev = slave.popevent("slavefinished")
|
||||
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):
|
||||
slave.testdir.makepyfile("""
|
||||
import py
|
||||
@@ -251,3 +262,14 @@ class TestSlaveInteractor:
|
||||
("pytest_pycollect_makeitem", "name == 'test_func'"),
|
||||
("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"
|
||||
|
||||
|
||||
def pytest_funcarg__hookrecorder(request, config):
|
||||
@pytest.fixture
|
||||
def hookrecorder(request, config):
|
||||
hookrecorder = HookRecorder(config.pluginmanager)
|
||||
if hasattr(hookrecorder, "start_recording"):
|
||||
hookrecorder.start_recording(newhooks)
|
||||
@@ -16,11 +17,13 @@ def pytest_funcarg__hookrecorder(request, config):
|
||||
return hookrecorder
|
||||
|
||||
|
||||
def pytest_funcarg__config(testdir):
|
||||
@pytest.fixture
|
||||
def config(testdir):
|
||||
return testdir.parseconfig()
|
||||
|
||||
|
||||
def pytest_funcarg__mysetup(tmpdir):
|
||||
@pytest.fixture
|
||||
def mysetup(tmpdir):
|
||||
class mysetup:
|
||||
source = tmpdir.mkdir("source")
|
||||
dest = tmpdir.mkdir("dest")
|
||||
|
||||
26
tox.ini
26
tox.ini
@@ -1,19 +1,27 @@
|
||||
[tox]
|
||||
# if you change the envlist, please update .travis.yml file as well
|
||||
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]
|
||||
changedir=testing
|
||||
passenv = USER USERNAME
|
||||
deps =
|
||||
pycmd
|
||||
setuptools_scm # to avoid .eggs
|
||||
pytest24: pytest~=2.4.0
|
||||
pytest25: pytest~=2.5.0
|
||||
|
||||
pytest26: pytest~=2.6.1
|
||||
pytest27: pytest~=2.7.2
|
||||
pexpect: pexpect
|
||||
pycmd
|
||||
# to avoid .eggs
|
||||
setuptools_scm
|
||||
pytest26: pytest~=2.6.1
|
||||
pytest27: pytest~=2.7.2
|
||||
pytest28: pytest~=2.8.7
|
||||
pytest29: pytest~=2.9.1
|
||||
pexpect: pexpect
|
||||
platform=
|
||||
pexpect: linux|darwin
|
||||
commands=
|
||||
# always clean to avoid code unmarshal mismatch on old python/pytest
|
||||
py.cleanup -aq
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
__all__ = ['__version__']
|
||||
from xdist._version import version as __version__
|
||||
|
||||
__all__ = ['__version__']
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import difflib
|
||||
import itertools
|
||||
from _pytest.runner import CollectReport
|
||||
|
||||
import pytest
|
||||
@@ -14,12 +15,12 @@ class EachScheduling:
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
Any nodes added after the run is started will only get items
|
||||
assigned if a node with matching spec was removed before it
|
||||
finished all it's pending items. The new node will then be
|
||||
assigned if a node with a matching spec was removed before it
|
||||
finished all its pending items. The new node will then be
|
||||
assigned the remaining items from the removed node.
|
||||
"""
|
||||
|
||||
@@ -47,8 +48,8 @@ class EachScheduling:
|
||||
"""Return True if there are pending test items
|
||||
|
||||
This indicates that collection has finished and nodes are
|
||||
still processing test items, so can be thought of as "the
|
||||
scheduler is active".
|
||||
still processing test items, so this can be thought of as
|
||||
"the scheduler is active".
|
||||
"""
|
||||
for pending in self.node2pending.values():
|
||||
if pending:
|
||||
@@ -73,10 +74,10 @@ class EachScheduling:
|
||||
"""Add the collected test items from a node
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
needs to run all the tests. If the pending list is already
|
||||
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
|
||||
for node, pending in self.node2pending.items():
|
||||
@@ -135,19 +136,19 @@ class EachScheduling:
|
||||
|
||||
|
||||
class LoadScheduling:
|
||||
"""Implement load scheduling accross nodes.
|
||||
"""Implement load scheduling across nodes.
|
||||
|
||||
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
|
||||
identical collections. Then the collection gets devided up in
|
||||
chunks and chunks get submitted to nodes. Whenver a node finishes
|
||||
an item they call ``.remove_item()`` which will trigger the
|
||||
identical collections. Then the collection gets divided up in
|
||||
chunks and chunks get submitted to nodes. Whenever a node finishes
|
||||
an item, it calls ``.remove_item()`` which will trigger the
|
||||
scheduler to assign more tests if the number of pending tests for
|
||||
the node falls below a low-watermark.
|
||||
|
||||
When created ``numnodes`` defines how many nodes are expected to
|
||||
submit a collection, this is used to know when all nodes have
|
||||
When created, ``numnodes`` defines how many nodes are expected to
|
||||
submit a collection. This is used to know when all nodes have
|
||||
finished collection or how large the chunks need to be created.
|
||||
|
||||
Attributes:
|
||||
@@ -155,7 +156,7 @@ class LoadScheduling:
|
||||
:numnodes: The expected number of nodes taking part. The actual
|
||||
number of nodes will vary during the scheduler's lifetime as
|
||||
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
|
||||
completed.
|
||||
|
||||
@@ -211,8 +212,8 @@ class LoadScheduling:
|
||||
"""Return True if there are pending test items
|
||||
|
||||
This indicates that collection has finished and nodes are
|
||||
still processing test items, so can be thought of as "the
|
||||
scheduler is active".
|
||||
still processing test items, so this can be thought of as
|
||||
"the scheduler is active".
|
||||
"""
|
||||
if self.pending:
|
||||
return True
|
||||
@@ -226,13 +227,13 @@ class LoadScheduling:
|
||||
return bool(self.node2pending)
|
||||
|
||||
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
|
||||
execute.
|
||||
|
||||
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
|
||||
self.node2pending[node] = []
|
||||
@@ -289,6 +290,9 @@ class LoadScheduling:
|
||||
``duration`` of the last test is optionally used as a
|
||||
heuristic to influence how many tests the node is assigned.
|
||||
"""
|
||||
if node.shutting_down:
|
||||
return
|
||||
|
||||
if self.pending:
|
||||
# how many nodes do we have?
|
||||
num_nodes = len(self.node2pending)
|
||||
@@ -308,7 +312,7 @@ class LoadScheduling:
|
||||
self.log("num items waiting for node:", len(self.pending))
|
||||
|
||||
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
|
||||
shutdown time. In the former case any pending items assigned
|
||||
@@ -346,7 +350,7 @@ class LoadScheduling:
|
||||
"""
|
||||
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:
|
||||
for node in self.nodes:
|
||||
self.check_schedule(node)
|
||||
@@ -363,13 +367,22 @@ class LoadScheduling:
|
||||
if not self.collection:
|
||||
return
|
||||
|
||||
# how many items per node do we have about?
|
||||
items_per_node = len(self.collection) // len(self.node2pending)
|
||||
# take a fraction of tests for initial distribution
|
||||
node_chunksize = max(items_per_node // 4, 2)
|
||||
# and initialize each node with a chunk of tests
|
||||
for node in self.nodes:
|
||||
self._send_tests(node, node_chunksize)
|
||||
# Send a batch of tests to run. If we don't have at least two
|
||||
# tests per node, we have to send them all so that we can send
|
||||
# shutdown signals and get all nodes working.
|
||||
initial_batch = max(len(self.pending) // 4,
|
||||
2 * len(self.nodes))
|
||||
|
||||
# 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):
|
||||
tests_per_node = self.pending[:num]
|
||||
@@ -477,7 +490,7 @@ class DSession:
|
||||
"""Return True if the distributed session has finished
|
||||
|
||||
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)
|
||||
|
||||
@@ -522,6 +535,7 @@ class DSession:
|
||||
while not self.session_finished:
|
||||
self.loop_once()
|
||||
if self.shouldstop:
|
||||
self.triggershutdown()
|
||||
raise Interrupted(str(self.shouldstop))
|
||||
return True
|
||||
|
||||
@@ -566,7 +580,7 @@ class DSession:
|
||||
|
||||
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.
|
||||
"""
|
||||
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
|
||||
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
|
||||
scheduling the first time it logs which scheduler is in use.
|
||||
"""
|
||||
if self.shuttingdown:
|
||||
return
|
||||
self.config.hook.pytest_xdist_node_collection_finished(node=node,
|
||||
ids=ids)
|
||||
# tell session which items were effectively collected otherwise
|
||||
# the master node will finish the session with EXIT_NOTESTSCOLLECTED
|
||||
self._session.testscollected = len(ids)
|
||||
@@ -638,7 +654,7 @@ class DSession:
|
||||
def slave_testreport(self, node, rep):
|
||||
"""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.
|
||||
"""
|
||||
if rep.when == "call" or (rep.when == "setup" and not rep.passed):
|
||||
@@ -656,9 +672,9 @@ class DSession:
|
||||
def _clone_node(self, node):
|
||||
"""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
|
||||
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.
|
||||
"""
|
||||
spec = node.gateway.spec
|
||||
@@ -707,17 +723,18 @@ class TerminalDistReporter:
|
||||
self.tr = config.pluginmanager.getplugin("terminalreporter")
|
||||
self._status = {}
|
||||
self._lastlen = 0
|
||||
self._isatty = getattr(self.tr, 'isatty', self.tr.hasmarkup)
|
||||
|
||||
def write_line(self, msg):
|
||||
self.tr.write_line(msg)
|
||||
|
||||
def ensure_show_status(self):
|
||||
if not self.tr.hasmarkup:
|
||||
if not self._isatty:
|
||||
self.write_line(self.getstatus())
|
||||
|
||||
def setstatus(self, spec, status, show=True):
|
||||
self._status[spec.id] = status
|
||||
if show and self.tr.hasmarkup:
|
||||
if show and self._isatty:
|
||||
self.rewrite(self.getstatus())
|
||||
|
||||
def getstatus(self):
|
||||
|
||||
@@ -25,6 +25,10 @@ def pytest_addoption(parser):
|
||||
def pytest_cmdline_main(config):
|
||||
|
||||
if config.getoption("looponfail"):
|
||||
usepdb = config.getoption('usepdb') # a core option
|
||||
if usepdb:
|
||||
raise pytest.UsageError(
|
||||
"--pdb incompatible with --looponfail.")
|
||||
looponfail_main(config)
|
||||
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):
|
||||
""" called before any remote node is set up. """
|
||||
@@ -25,3 +39,8 @@ def pytest_testnodeready(node):
|
||||
|
||||
def pytest_testnodedown(node, error):
|
||||
""" 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"
|
||||
val = config.getvalue
|
||||
if not val("collectonly"):
|
||||
usepdb = config.option.usepdb # a core option
|
||||
if val("looponfail"):
|
||||
if usepdb:
|
||||
raise pytest.UsageError(
|
||||
"--pdb incompatible with --looponfail.")
|
||||
elif val("dist") != "no":
|
||||
usepdb = config.getoption('usepdb') # a core option
|
||||
if val("dist") != "no":
|
||||
if usepdb:
|
||||
raise pytest.UsageError(
|
||||
"--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")
|
||||
torun = []
|
||||
while 1:
|
||||
name, kwargs = self.channel.receive()
|
||||
try:
|
||||
name, kwargs = self.channel.receive()
|
||||
except EOFError:
|
||||
return True
|
||||
self.log("received command", name, kwargs)
|
||||
if name == "runtests":
|
||||
torun.extend(kwargs['indices'])
|
||||
@@ -145,6 +148,8 @@ if __name__ == '__channelexec__':
|
||||
os.environ['PYTHONPATH'] = (
|
||||
importpath + os.pathsep +
|
||||
os.environ.get('PYTHONPATH', ''))
|
||||
os.environ['PYTEST_XDIST_WORKER'] = slaveinput['slaveid']
|
||||
os.environ['PYTEST_XDIST_WORKER_COUNT'] = str(slaveinput['slavecount'])
|
||||
# os.environ['PYTHONPATH'] = importpath
|
||||
import py
|
||||
config = remote_initconfig(option_dict, args)
|
||||
|
||||
@@ -205,8 +205,10 @@ class SlaveController(object):
|
||||
self.putevent = putevent
|
||||
self.gateway = gateway
|
||||
self.config = config
|
||||
self.slaveinput = {'slaveid': gateway.id}
|
||||
self.slaveinput = {'slaveid': gateway.id,
|
||||
'slavecount': len(nodemanager.specs)}
|
||||
self._down = False
|
||||
self._shutdown_sent = False
|
||||
self.log = py.log.Producer("slavectl-%s" % gateway.id)
|
||||
if not self.config.option.debug:
|
||||
py.log.setconsumer(self.log._keywords, None)
|
||||
@@ -214,6 +216,10 @@ class SlaveController(object):
|
||||
def __repr__(self):
|
||||
return "<%s %s>" % (self.__class__.__name__, self.gateway.id,)
|
||||
|
||||
@property
|
||||
def shutting_down(self):
|
||||
return self._down or self._shutdown_sent
|
||||
|
||||
def setup(self):
|
||||
self.log("setting up slave session")
|
||||
spec = self.gateway.spec
|
||||
@@ -223,8 +229,9 @@ class SlaveController(object):
|
||||
option_dict = vars(self.config.option)
|
||||
if spec.popen:
|
||||
name = "popen-%s" % self.gateway.id
|
||||
basetemp = self.config._tmpdirhandler.getbasetemp()
|
||||
option_dict['basetemp'] = str(basetemp.join(name))
|
||||
if hasattr(self.config, '_tmpdirhandler'):
|
||||
basetemp = self.config._tmpdirhandler.getbasetemp()
|
||||
option_dict['basetemp'] = str(basetemp.join(name))
|
||||
self.config.hook.pytest_configure_node(node=self)
|
||||
self.channel = self.gateway.remote_exec(xdist.remote)
|
||||
self.channel.send((self.slaveinput, args, option_dict))
|
||||
@@ -256,6 +263,7 @@ class SlaveController(object):
|
||||
self.sendcommand("shutdown")
|
||||
except IOError:
|
||||
pass
|
||||
self._shutdown_sent = True
|
||||
|
||||
def sendcommand(self, name, **kwargs):
|
||||
""" send a named parametrized command to the other side. """
|
||||
|
||||
Reference in New Issue
Block a user