From 1ed3884da9339afba50b7d37246ab83e99efd97b Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 27 Jul 2018 17:46:25 -0300 Subject: [PATCH 1/7] Add pre-commit files and linting using pre-commit --- .pre-commit-config.yaml | 24 ++++++++++++++++++++++++ .travis.yml | 5 +++++ tox.ini | 16 +++++----------- 3 files changed, 34 insertions(+), 11 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..4033ee7 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,24 @@ +repos: +- repo: https://github.com/ambv/black + rev: 18.6b4 + hooks: + - id: black + args: [--safe, --quiet] + language_version: python3.6 +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v1.3.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: debug-statements + - id: flake8 +- repo: local + hooks: + - id: rst + name: rst + entry: rst-lint --encoding utf-8 + files: ^(CHANGELOG.rst|HOWTORELEASE.rst|README.rst|changelog/.*)$ + language: python + additional_dependencies: [pygments, restructuredtext_lint] + python_version: python3.6 diff --git a/.travis.yml b/.travis.yml index 080d750..9f142f6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,12 +25,17 @@ install: pip install tox setuptools_scm script: tox stages: +- linting - test - name: deploy if: repo = pytest-dev/pytest-xdist AND tag IS present jobs: include: + - stage: linting + python: '3.6' + script: + - tox -e linting - stage: test # python x env above are already included into this stage - python: "2.7" diff --git a/tox.ini b/tox.ini index 48b9e83..f0e63da 100644 --- a/tox.ini +++ b/tox.ini @@ -1,11 +1,10 @@ [tox] # if you change the envlist, please update .travis.yml file as well envlist= + linting py{27,34,35,36}-pytest{34,35,36} py{27,36}-pytest36-pexpect py{27,36}-pytest{master,features} - flakes - readme [testenv] @@ -28,16 +27,11 @@ commands= py.cleanup -aq pytest {posargs} -[testenv:flakes] +[testenv:linting] changedir= -deps = flake8 -commands = flake8 setup.py testing xdist - -[testenv:readme] -changedir = -deps = readme -skip_install = true -commands = python setup.py check -r -s +deps = + pre-commit +commands = pre-commit run --all [testenv:release] changedir= From fced1645cb96858286df2b624c493d95487c95b3 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 27 Jul 2018 17:50:55 -0300 Subject: [PATCH 2/7] Run pre-commit: black, whitespaces, rst --- LICENSE | 4 +- OVERVIEW.md | 66 +-- example/boxed.txt | 6 +- example/loadscope/test/test_delta.py | 2 - setup.py | 65 +-- testing/acceptance_test.py | 828 +++++++++++++++------------ testing/conftest.py | 11 +- testing/test_dsession.py | 60 +- testing/test_looponfail.py | 108 ++-- testing/test_newhooks.py | 48 +- testing/test_plugin.py | 53 +- testing/test_remote.py | 160 +++--- testing/test_slavemanage.py | 70 ++- tox.ini | 5 + xdist/__init__.py | 2 +- xdist/dsession.py | 71 ++- xdist/looponfail.py | 42 +- xdist/plugin.py | 145 +++-- xdist/remote.py | 82 +-- xdist/report.py | 15 +- xdist/scheduler/each.py | 10 +- xdist/scheduler/filescope.py | 4 +- xdist/scheduler/load.py | 21 +- xdist/scheduler/loadscope.py | 45 +- xdist/workermanage.py | 164 +++--- 25 files changed, 1153 insertions(+), 934 deletions(-) diff --git a/LICENSE b/LICENSE index 31ecdfb..8a30978 100644 --- a/LICENSE +++ b/LICENSE @@ -5,10 +5,10 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE diff --git a/OVERVIEW.md b/OVERVIEW.md index b15be41..45a20d6 100644 --- a/OVERVIEW.md +++ b/OVERVIEW.md @@ -1,78 +1,78 @@ # Overview # `xdist` works by spawning one or more **workers**, which are controlled -by the **master**. Each **worker** is responsible for performing +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 + 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. - + 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 + 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 + 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. Note that `pytest_xdist_make_scheduler` hook can be used to implement custom tests distribution logic. - + 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 + 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`. - + 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 + 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**) + 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 + 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 +> 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. - +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. + diff --git a/example/boxed.txt b/example/boxed.txt index f6c91d1..aabb27e 100644 --- a/example/boxed.txt +++ b/example/boxed.txt @@ -1,8 +1,8 @@ .. note:: - Since 1.19.0, the actual implementation of the ``--boxed`` option has been moved to a - separate plugin, `pytest-forked `_ - which can be installed independently. The ``--boxed`` command-line options remains + Since 1.19.0, the actual implementation of the ``--boxed`` option has been moved to a + separate plugin, `pytest-forked `_ + which can be installed independently. The ``--boxed`` command-line options remains for backward compatibility reasons. diff --git a/example/loadscope/test/test_delta.py b/example/loadscope/test/test_delta.py index dd1eaa0..b504882 100644 --- a/example/loadscope/test/test_delta.py +++ b/example/loadscope/test/test_delta.py @@ -3,7 +3,6 @@ from unittest import TestCase class Delta1(TestCase): - def test_delta0(self): sleep(5) assert True @@ -46,7 +45,6 @@ class Delta1(TestCase): class Delta2(TestCase): - def test_delta0(self): sleep(5) assert True diff --git a/setup.py b/setup.py index f8c3f98..4c57d37 100644 --- a/setup.py +++ b/setup.py @@ -1,47 +1,44 @@ from setuptools import setup, find_packages -install_requires = ['execnet>=1.1', 'pytest>=3.4', 'pytest-forked'] +install_requires = ["execnet>=1.1", "pytest>=3.4", "pytest-forked"] setup( name="pytest-xdist", - use_scm_version={'write_to': 'xdist/_version.py'}, - description='pytest xdist plugin for distributed testing' - ' and loop-on-failing modes', - long_description=open('README.rst').read(), - license='MIT', - author='holger krekel and contributors', - author_email='pytest-dev@python.org,holger@merlinux.eu', - url='https://github.com/pytest-dev/pytest-xdist', - platforms=['linux', 'osx', 'win32'], - packages=find_packages(exclude=['testing', 'example']), + use_scm_version={"write_to": "xdist/_version.py"}, + description="pytest xdist plugin for distributed testing" + " and loop-on-failing modes", + long_description=open("README.rst").read(), + license="MIT", + author="holger krekel and contributors", + author_email="pytest-dev@python.org,holger@merlinux.eu", + url="https://github.com/pytest-dev/pytest-xdist", + platforms=["linux", "osx", "win32"], + packages=find_packages(exclude=["testing", "example"]), entry_points={ - 'pytest11': [ - 'xdist = xdist.plugin', - 'xdist.looponfail = xdist.looponfail', - ], + "pytest11": ["xdist = xdist.plugin", "xdist.looponfail = xdist.looponfail"] }, zip_safe=False, - python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', + python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", install_requires=install_requires, - setup_requires=['setuptools_scm'], + setup_requires=["setuptools_scm"], classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Framework :: Pytest', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Operating System :: POSIX', - 'Operating System :: Microsoft :: Windows', - 'Operating System :: MacOS :: MacOS X', - 'Topic :: Software Development :: Testing', - 'Topic :: Software Development :: Quality Assurance', - 'Topic :: Utilities', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', + "Development Status :: 5 - Production/Stable", + "Framework :: Pytest", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: POSIX", + "Operating System :: Microsoft :: Windows", + "Operating System :: MacOS :: MacOS X", + "Topic :: Software Development :: Testing", + "Topic :: Software Development :: Quality Assurance", + "Topic :: Utilities", + "Programming Language :: Python", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", ], ) diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index ec76a1d..e986429 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -8,74 +8,80 @@ import pytest class TestDistribution: def test_n1_pass(self, testdir): - p1 = testdir.makepyfile(""" + p1 = testdir.makepyfile( + """ def test_ok(): pass - """) + """ + ) result = testdir.runpytest(p1, "-n1") assert result.ret == 0 - result.stdout.fnmatch_lines([ - "*1 passed*", - ]) + result.stdout.fnmatch_lines(["*1 passed*"]) def test_n1_fail(self, testdir): - p1 = testdir.makepyfile(""" + p1 = testdir.makepyfile( + """ def test_fail(): assert 0 - """) + """ + ) result = testdir.runpytest(p1, "-n1") assert result.ret == 1 - result.stdout.fnmatch_lines([ - "*1 failed*", - ]) + result.stdout.fnmatch_lines(["*1 failed*"]) def test_n1_import_error(self, testdir): - p1 = testdir.makepyfile(""" + p1 = testdir.makepyfile( + """ import __import_of_missing_module def test_import(): pass - """) + """ + ) result = testdir.runpytest(p1, "-n1") assert result.ret == 1 - result.stdout.fnmatch_lines([ - "E *Error: No module named *__import_of_missing_module*", - ]) + result.stdout.fnmatch_lines( + ["E *Error: No module named *__import_of_missing_module*"] + ) def test_n2_import_error(self, testdir): """Check that we don't report the same import error multiple times in distributed mode.""" - p1 = testdir.makepyfile(""" + p1 = testdir.makepyfile( + """ import __import_of_missing_module def test_import(): pass - """) + """ + ) result1 = testdir.runpytest(p1, "-n2") result2 = testdir.runpytest(p1, "-n1") assert len(result1.stdout.lines) == len(result2.stdout.lines) def test_n1_skip(self, testdir): - p1 = testdir.makepyfile(""" + p1 = testdir.makepyfile( + """ def test_skip(): import pytest pytest.skip("myreason") - """) + """ + ) result = testdir.runpytest(p1, "-n1") assert result.ret == 0 - result.stdout.fnmatch_lines([ - "*1 skipped*", - ]) + result.stdout.fnmatch_lines(["*1 skipped*"]) def test_manytests_to_one_import_error(self, testdir): - p1 = testdir.makepyfile(""" + p1 = testdir.makepyfile( + """ import __import_of_missing_module def test_import(): pass - """) - result = testdir.runpytest(p1, '--tx=popen', '--tx=popen') + """ + ) + result = testdir.runpytest(p1, "--tx=popen", "--tx=popen") assert result.ret in (1, 2) - result.stdout.fnmatch_lines([ - "E *Error: No module named *__import_of_missing_module*", - ]) + result.stdout.fnmatch_lines( + ["E *Error: No module named *__import_of_missing_module*"] + ) def test_manytests_to_one_popen(self, testdir): p1 = testdir.makepyfile( @@ -89,39 +95,37 @@ class TestDistribution: pass def test_skip(): pytest.skip("hello") - """, ) - result = testdir.runpytest(p1, "-v", '-d', '--tx=popen', '--tx=popen') - result.stdout.fnmatch_lines([ - "*1*Python*", - "*2 failed, 1 passed, 1 skipped*", - ]) + """ + ) + result = testdir.runpytest(p1, "-v", "-d", "--tx=popen", "--tx=popen") + result.stdout.fnmatch_lines(["*1*Python*", "*2 failed, 1 passed, 1 skipped*"]) assert result.ret == 1 def test_n1_fail_minus_x(self, testdir): - p1 = testdir.makepyfile(""" + p1 = testdir.makepyfile( + """ def test_fail1(): assert 0 def test_fail2(): assert 0 - """) + """ + ) result = testdir.runpytest(p1, "-x", "-v", "-n1") assert result.ret == 2 - result.stdout.fnmatch_lines([ - "*Interrupted: stopping*1*", - "*1 failed*", - ]) + result.stdout.fnmatch_lines(["*Interrupted: stopping*1*", "*1 failed*"]) def test_basetemp_in_subprocesses(self, testdir): - p1 = testdir.makepyfile(""" + p1 = testdir.makepyfile( + """ def test_send(tmpdir): import py assert tmpdir.relto(py.path.local(%r)), tmpdir - """ % str(testdir.tmpdir)) + """ + % str(testdir.tmpdir) + ) result = testdir.runpytest_subprocess(p1, "-n1") assert result.ret == 0 - result.stdout.fnmatch_lines([ - "*1 passed*", - ]) + result.stdout.fnmatch_lines(["*1 passed*"]) def test_dist_ini_specified(self, testdir): p1 = testdir.makepyfile( @@ -135,24 +139,25 @@ class TestDistribution: pass def test_skip(): pytest.skip("hello") - """, ) - testdir.makeini(""" + """ + ) + testdir.makeini( + """ [pytest] addopts = --tx=3*popen - """) - result = testdir.runpytest(p1, '-d', "-v") - result.stdout.fnmatch_lines([ - "*2*Python*", - "*2 failed, 1 passed, 1 skipped*", - ]) + """ + ) + result = testdir.runpytest(p1, "-d", "-v") + result.stdout.fnmatch_lines(["*2*Python*", "*2 failed, 1 passed, 1 skipped*"]) assert result.ret == 1 @pytest.mark.xfail("sys.platform.startswith('java')", run=False) def test_dist_tests_with_crash(self, testdir): - if not hasattr(os, 'kill'): + if not hasattr(os, "kill"): pytest.skip("no os.kill") - p1 = testdir.makepyfile(""" + p1 = testdir.makepyfile( + """ import pytest def test_fail0(): assert 0 @@ -167,12 +172,17 @@ class TestDistribution: import os time.sleep(0.5) os.kill(os.getpid(), 15) - """) - result = testdir.runpytest(p1, "-v", '-d', '-n1') - result.stdout.fnmatch_lines([ - "*Python*", "*PASS**test_ok*", "*node*down*", - "*3 failed, 1 passed, 1 skipped*" - ]) + """ + ) + result = testdir.runpytest(p1, "-v", "-d", "-n1") + result.stdout.fnmatch_lines( + [ + "*Python*", + "*PASS**test_ok*", + "*node*down*", + "*3 failed, 1 passed, 1 skipped*", + ] + ) assert result.ret == 1 def test_distribution_rsyncdirs_example(self, testdir): @@ -182,16 +192,22 @@ class TestDistribution: subdir.ensure("__init__.py") p = subdir.join("test_one.py") p.write("def test_5():\n assert not __file__.startswith(%r)" % str(p)) - result = testdir.runpytest("-v", "-d", - "--rsyncdir=%(subdir)s" % locals(), - "--tx=popen//chdir=%(dest)s" % locals(), p) + result = testdir.runpytest( + "-v", + "-d", + "--rsyncdir=%(subdir)s" % locals(), + "--tx=popen//chdir=%(dest)s" % locals(), + p, + ) assert result.ret == 0 - result.stdout.fnmatch_lines([ - "*0* *cwd*", - # "RSyncStart: [G1]", - # "RSyncFinished: [G1]", - "*1 passed*" - ]) + result.stdout.fnmatch_lines( + [ + "*0* *cwd*", + # "RSyncStart: [G1]", + # "RSyncFinished: [G1]", + "*1 passed*", + ] + ) assert dest.join(subdir.basename).check(dir=1) def test_backward_compatibility_worker_terminology(self, testdir): @@ -199,17 +215,20 @@ class TestDistribution: Keep in mind that removing this compatibility will break a ton of plugins and user code. """ - testdir.makepyfile(""" + testdir.makepyfile( + """ def test(pytestconfig): assert hasattr(pytestconfig, 'slaveinput') assert hasattr(pytestconfig, 'workerinput') - """) + """ + ) result = testdir.runpytest("-n1") result.stdout.fnmatch_lines("*1 passed*") assert result.ret == 0 def test_data_exchange(self, testdir): - testdir.makeconftest(""" + testdir.makeconftest( + """ # This hook only called on master. def pytest_configure_node(node): node.workerinput['a'] = 42 @@ -232,11 +251,13 @@ class TestDistribution: calc_result = terminalreporter.config.calc_result terminalreporter._tw.sep('-', 'calculated result is %s' % calc_result) - """) + """ + ) p1 = testdir.makepyfile("def test_func(): pass") - result = testdir.runpytest("-v", p1, '-d', '--tx=popen') + result = testdir.runpytest("-v", p1, "-d", "--tx=popen") result.stdout.fnmatch_lines( - ["*0*Python*", "*calculated result is 49*", "*1 passed*"]) + ["*0*Python*", "*calculated result is 49*", "*1 passed*"] + ) assert result.ret == 0 def test_keyboardinterrupt_hooks_issue79(self, testdir): @@ -245,8 +266,10 @@ class TestDistribution: test_one=""" def test_hello(): raise KeyboardInterrupt() - """) - testdir.makeconftest(""" + """, + ) + testdir.makeconftest( + """ def pytest_sessionfinish(session): # on the worker if hasattr(session.config, 'workeroutput'): @@ -255,21 +278,24 @@ class TestDistribution: def pytest_testnodedown(node, error): assert node.workeroutput['s2'] == 42 print ("s2call-finished") - """) + """ + ) args = ["-n1", "--debug"] result = testdir.runpytest_subprocess(*args) s = result.stdout.str() assert result.ret == 2 - assert 's2call' in s + assert "s2call" in s assert "Interrupted" in s def test_keyboard_interrupt_dist(self, testdir): # xxx could be refined to check for return code - testdir.makepyfile(""" + testdir.makepyfile( + """ def test_sleep(): import time time.sleep(10) - """) + """ + ) child = testdir.spawn_pytest("-n1 -v") child.expect(".*test_sleep.*") child.kill(2) # keyboard interrupt @@ -279,31 +305,32 @@ class TestDistribution: # assert ret == 2 def test_dist_with_collectonly(self, testdir): - p1 = testdir.makepyfile(""" + p1 = testdir.makepyfile( + """ def test_ok(): pass - """) + """ + ) result = testdir.runpytest(p1, "-n1", "--collect-only") assert result.ret == 0 - result.stdout.fnmatch_lines([ - "*collected 1 item*", - ]) + result.stdout.fnmatch_lines(["*collected 1 item*"]) class TestDistEach: def test_simple(self, testdir): - testdir.makepyfile(""" + testdir.makepyfile( + """ def test_hello(): pass - """) - result = testdir.runpytest_subprocess("--debug", "--dist=each", - "--tx=2*popen") + """ + ) + result = testdir.runpytest_subprocess("--debug", "--dist=each", "--tx=2*popen") assert not result.ret result.stdout.fnmatch_lines(["*2 pass*"]) @pytest.mark.xfail( - run=False, - reason="other python versions might not have py.test installed") + run=False, reason="other python versions might not have py.test installed" + ) def test_simple_diffoutput(self, testdir): interpreters = [] for name in ("python2.5", "python2.6"): @@ -319,7 +346,8 @@ class TestDistEach: def test_hello(): print("%s...%s" % sys.version_info[:2]) assert 0 - """) + """, + ) args = ["--dist=each", "-v"] args += ["--tx", "popen//python=%s" % interpreters[0]] args += ["--tx", "popen//python=%s" % interpreters[1]] @@ -331,7 +359,8 @@ class TestDistEach: class TestTerminalReporting: def test_pass_skip_fail(self, testdir): - testdir.makepyfile(""" + testdir.makepyfile( + """ import pytest def test_ok(): pass @@ -339,108 +368,122 @@ class TestTerminalReporting: pytest.skip("xx") def test_func(): assert 0 - """) + """ + ) result = testdir.runpytest("-n1", "-v") - result.stdout.fnmatch_lines_random([ - "*PASS*test_pass_skip_fail.py*test_ok*", - "*SKIP*test_pass_skip_fail.py*test_skip*", - "*FAIL*test_pass_skip_fail.py*test_func*", - ]) - result.stdout.fnmatch_lines([ - "*def test_func():", - "> assert 0", - "E assert 0", - ]) + result.stdout.fnmatch_lines_random( + [ + "*PASS*test_pass_skip_fail.py*test_ok*", + "*SKIP*test_pass_skip_fail.py*test_skip*", + "*FAIL*test_pass_skip_fail.py*test_func*", + ] + ) + result.stdout.fnmatch_lines( + ["*def test_func():", "> assert 0", "E assert 0"] + ) def test_fail_platinfo(self, testdir): - testdir.makepyfile(""" + testdir.makepyfile( + """ def test_func(): assert 0 - """) + """ + ) result = testdir.runpytest("-n1", "-v") - result.stdout.fnmatch_lines([ - "*FAIL*test_fail_platinfo.py*test_func*", - "*0*Python*", - "*def test_func():", - "> assert 0", - "E assert 0", - ]) + result.stdout.fnmatch_lines( + [ + "*FAIL*test_fail_platinfo.py*test_func*", + "*0*Python*", + "*def test_func():", + "> assert 0", + "E assert 0", + ] + ) - @pytest.mark.parametrize('n', ['-n0', '-n1']) - @pytest.mark.parametrize('warn_type', ['pytest', 'builtin']) + @pytest.mark.parametrize("n", ["-n0", "-n1"]) + @pytest.mark.parametrize("warn_type", ["pytest", "builtin"]) def test_logwarning(self, testdir, n, warn_type): from pkg_resources import parse_version - if parse_version(pytest.__version__) < parse_version('3.1'): - pytest.skip('pytest warnings requires >= 3.1') - if warn_type == 'builtin': + if parse_version(pytest.__version__) < parse_version("3.1"): + pytest.skip("pytest warnings requires >= 3.1") + + if warn_type == "builtin": warn_code = """warnings.warn(UserWarning('this is a warning'))""" - elif warn_type == 'pytest': + elif warn_type == "pytest": warn_code = """request.config.warn('', 'this is a warning', fslocation=py.path.local())""" else: assert False - testdir.makepyfile(""" + testdir.makepyfile( + """ import warnings, py def test_func(request): {warn_code} - """.format(warn_code=warn_code)) + """.format( + warn_code=warn_code + ) + ) result = testdir.runpytest(n) - result.stdout.fnmatch_lines([ - "*this is a warning*", - "*1 passed, 1 warnings*", - ]) + result.stdout.fnmatch_lines(["*this is a warning*", "*1 passed, 1 warnings*"]) def test_logfinish_hook(self, testdir): """Ensure the pytest_runtest_logfinish hook is being properly handled""" from _pytest import hookspec - if not hasattr(hookspec, 'pytest_runtest_logfinish'): - pytest.skip('test requires pytest_runtest_logfinish hook in pytest (3.4+)') - testdir.makeconftest(""" + if not hasattr(hookspec, "pytest_runtest_logfinish"): + pytest.skip("test requires pytest_runtest_logfinish hook in pytest (3.4+)") + + testdir.makeconftest( + """ def pytest_runtest_logfinish(): print('pytest_runtest_logfinish hook called') - """) - testdir.makepyfile(""" + """ + ) + testdir.makepyfile( + """ def test_func(): pass - """) + """ + ) result = testdir.runpytest("-n1", "-s") - result.stdout.fnmatch_lines([ - "*pytest_runtest_logfinish hook called*", - ]) + result.stdout.fnmatch_lines(["*pytest_runtest_logfinish hook called*"]) def test_teardownfails_one_function(testdir): - p = testdir.makepyfile(""" + p = testdir.makepyfile( + """ def test_func(): pass def teardown_function(function): assert 0 - """) - result = testdir.runpytest(p, '-n1', '--tx=popen') + """ + ) + result = testdir.runpytest(p, "-n1", "--tx=popen") result.stdout.fnmatch_lines( - ["*def teardown_function(function):*", "*1 passed*1 error*"]) + ["*def teardown_function(function):*", "*1 passed*1 error*"] + ) @pytest.mark.xfail def test_terminate_on_hangingnode(testdir): - p = testdir.makeconftest(""" + p = testdir.makeconftest( + """ def pytest_sessionfinish(session): if session.nodeid == "my": # running on worker import time time.sleep(3) - """) - result = testdir.runpytest(p, '--dist=each', '--tx=popen//id=my') + """ + ) + result = testdir.runpytest(p, "--dist=each", "--tx=popen//id=my") assert result.duration < 2.0 - result.stdout.fnmatch_lines([ - "*killed*my*", - ]) + result.stdout.fnmatch_lines(["*killed*my*"]) @pytest.mark.xfail(reason="works if run outside test suite", run=False) def test_session_hooks(testdir): - testdir.makeconftest(""" + testdir.makeconftest( + """ import sys def pytest_sessionstart(session): sys.pytestsessionhooks = session @@ -455,20 +498,20 @@ def test_session_hooks(testdir): # let's fail on the worker if name == "worker": raise ValueError(42) - """) - p = testdir.makepyfile(""" + """ + ) + p = testdir.makepyfile( + """ import sys def test_hello(): assert hasattr(sys, 'pytestsessionhooks') - """) + """ + ) result = testdir.runpytest(p, "--dist=each", "--tx=popen") - result.stdout.fnmatch_lines([ - "*ValueError*", - "*1 passed*", - ]) + result.stdout.fnmatch_lines(["*ValueError*", "*1 passed*"]) assert not result.ret d = result.parseoutcomes() - assert d['passed'] == 1 + assert d["passed"] == 1 assert testdir.tmpdir.join("worker").check() assert testdir.tmpdir.join("master").check() @@ -478,27 +521,32 @@ def test_session_testscollected(testdir): Make sure master node is updating the session object with the number of tests collected from the workers. """ - testdir.makepyfile(test_foo=""" + testdir.makepyfile( + test_foo=""" import pytest @pytest.mark.parametrize('i', range(3)) def test_ok(i): pass - """) - testdir.makeconftest(""" + """ + ) + testdir.makeconftest( + """ def pytest_sessionfinish(session): collected = getattr(session, 'testscollected', None) with open('testscollected', 'w') as f: f.write('collected = %s' % collected) - """) + """ + ) result = testdir.inline_run("-n1") result.assertoutcome(passed=3) - collected_file = testdir.tmpdir.join('testscollected') + collected_file = testdir.tmpdir.join("testscollected") assert collected_file.isfile() - assert collected_file.read() == 'collected = 3' + assert collected_file.read() == "collected = 3" def test_funcarg_teardown_failure(testdir): - p = testdir.makepyfile(""" + p = testdir.makepyfile( + """ import pytest @pytest.fixture def myarg(request): @@ -508,21 +556,20 @@ def test_funcarg_teardown_failure(testdir): scope="module") def test_hello(myarg): pass - """) + """ + ) result = testdir.runpytest_subprocess("--debug", p) # , "-n1") - result.stdout.fnmatch_lines([ - "*ValueError*42*", - "*1 passed*1 error*", - ]) + result.stdout.fnmatch_lines(["*ValueError*42*", "*1 passed*1 error*"]) assert result.ret -@pytest.mark.parametrize('when', ['setup', 'call', 'teardown']) +@pytest.mark.parametrize("when", ["setup", "call", "teardown"]) def test_crashing_item(testdir, when): """Ensure crashing item is correctly reported during all testing stages""" - code = dict(setup='', call='', teardown='') - code[when] = 'py.process.kill(os.getpid())' - p = testdir.makepyfile(""" + code = dict(setup="", call="", teardown="") + code[when] = "py.process.kill(os.getpid())" + p = testdir.makepyfile( + """ import os import py import pytest @@ -539,13 +586,15 @@ def test_crashing_item(testdir, when): def test_ok(): pass - """.format(**code)) - passes = 2 if when == 'teardown' else 1 + """.format( + **code + ) + ) + passes = 2 if when == "teardown" else 1 result = testdir.runpytest("-n2", p) - result.stdout.fnmatch_lines([ - "*crashed*test_crash*", - "*1 failed*%d passed*" % passes, - ]) + result.stdout.fnmatch_lines( + ["*crashed*test_crash*", "*1 failed*%d passed*" % passes] + ) def test_multiple_log_reports(testdir): @@ -554,7 +603,8 @@ def test_multiple_log_reports(testdir): (#206). Inspired by pytest-rerunfailures. """ - testdir.makeconftest(""" + testdir.makeconftest( + """ from _pytest.runner import runtestprotocol def pytest_runtest_protocol(item, nextitem): item.ihook.pytest_runtest_logstart(nodeid=item.nodeid, @@ -563,48 +613,55 @@ def test_multiple_log_reports(testdir): for report in reports: item.ihook.pytest_runtest_logreport(report=report) return True - """) - testdir.makepyfile(""" + """ + ) + testdir.makepyfile( + """ def test(): pass - """) + """ + ) result = testdir.runpytest("-n1") - result.stdout.fnmatch_lines([ - "*2 passed*", - ]) + result.stdout.fnmatch_lines(["*2 passed*"]) def test_skipping(testdir): - p = testdir.makepyfile(""" + p = testdir.makepyfile( + """ import pytest def test_crash(): pytest.skip("hello") - """) - result = testdir.runpytest("-n1", '-rs', p) + """ + ) + result = testdir.runpytest("-n1", "-rs", p) assert result.ret == 0 result.stdout.fnmatch_lines(["*hello*", "*1 skipped*"]) def test_issue34_pluginloading_in_subprocess(testdir): testdir.tmpdir.join("plugin123.py").write( - textwrap.dedent(""" + textwrap.dedent( + """ def pytest_namespace(): return {'sample_variable': 'testing'} - """)) - testdir.makepyfile(""" + """ + ) + ) + testdir.makepyfile( + """ import pytest def test_hello(): assert pytest.sample_variable == "testing" - """) + """ + ) result = testdir.runpytest_subprocess("-n1", "-p", "plugin123") assert result.ret == 0 - result.stdout.fnmatch_lines([ - "*1 passed*", - ]) + result.stdout.fnmatch_lines(["*1 passed*"]) def test_fixture_scope_caching_issue503(testdir): - p1 = testdir.makepyfile(""" + p1 = testdir.makepyfile( + """ import pytest @pytest.fixture(scope='session') @@ -619,12 +676,11 @@ def test_fixture_scope_caching_issue503(testdir): def test_b(fix): pass - """) - result = testdir.runpytest(p1, '-v', '-n1') + """ + ) + result = testdir.runpytest(p1, "-v", "-n1") assert result.ret == 0 - result.stdout.fnmatch_lines([ - "*2 passed*", - ]) + result.stdout.fnmatch_lines(["*2 passed*"]) def test_issue_594_random_parametrize(testdir): @@ -632,7 +688,8 @@ def test_issue_594_random_parametrize(testdir): Make sure that tests that are randomly parametrized display an appropriate error message, instead of silently skipping the entire test run. """ - p1 = testdir.makepyfile(""" + p1 = testdir.makepyfile( + """ import pytest import random @@ -641,171 +698,203 @@ def test_issue_594_random_parametrize(testdir): @pytest.mark.parametrize('x', xs) def test_foo(x): assert 1 - """) - result = testdir.runpytest(p1, '-v', '-n4') + """ + ) + result = testdir.runpytest(p1, "-v", "-n4") assert result.ret == 1 - result.stdout.fnmatch_lines([ - "Different tests were collected between gw* and gw*", - ]) + result.stdout.fnmatch_lines(["Different tests were collected between gw* and gw*"]) def test_tmpdir_disabled(testdir): """Test xdist doesn't break if internal tmpdir plugin is disabled (#22). """ - p1 = testdir.makepyfile(""" + p1 = testdir.makepyfile( + """ def test_ok(): pass - """) - result = testdir.runpytest(p1, "-n1", '-p', 'no:tmpdir') + """ + ) + 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']) +@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(""" + p1 = testdir.makepyfile( + """ def test_ok(): pass - """) - result = testdir.runpytest(p1, "-n1", '-p', 'no:%s' % plugin) + """ + ) + 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(""" + f = testdir.makepyfile( + """ import os def test_a(): os._exit(1) def test_b(): pass - """) - res = testdir.runpytest(f, '-n1') - res.stdout.fnmatch_lines([ - "*Replacing crashed worker*", - "*Worker*crashed while running*", - "*1 failed*1 passed*", - ]) + """ + ) + res = testdir.runpytest(f, "-n1") + res.stdout.fnmatch_lines( + [ + "*Replacing crashed worker*", + "*Worker*crashed while running*", + "*1 failed*1 passed*", + ] + ) def test_load_multiple(self, testdir): - f = testdir.makepyfile(""" + f = testdir.makepyfile( + """ import os def test_a(): pass def test_b(): os._exit(1) def test_c(): pass def test_d(): pass - """) - res = testdir.runpytest(f, '-n2') - res.stdout.fnmatch_lines([ - "*Replacing crashed worker*", - "*Worker*crashed while running*", - "*1 failed*3 passed*", - ]) + """ + ) + res = testdir.runpytest(f, "-n2") + res.stdout.fnmatch_lines( + [ + "*Replacing crashed worker*", + "*Worker*crashed while running*", + "*1 failed*3 passed*", + ] + ) def test_each_single(self, testdir): - f = testdir.makepyfile(""" + f = testdir.makepyfile( + """ import os def test_a(): os._exit(1) def test_b(): pass - """) - res = testdir.runpytest(f, '--dist=each', '--tx=popen') - res.stdout.fnmatch_lines([ - "*Replacing crashed worker*", - "*Worker*crashed while running*", - "*1 failed*1 passed*", - ]) + """ + ) + res = testdir.runpytest(f, "--dist=each", "--tx=popen") + res.stdout.fnmatch_lines( + [ + "*Replacing crashed worker*", + "*Worker*crashed while running*", + "*1 failed*1 passed*", + ] + ) - @pytest.mark.xfail(reason='#20: xdist race condition on node restart') + @pytest.mark.xfail(reason="#20: xdist race condition on node restart") def test_each_multiple(self, testdir): - f = testdir.makepyfile(""" + f = testdir.makepyfile( + """ import os def test_a(): os._exit(1) def test_b(): pass - """) - res = testdir.runpytest(f, '--dist=each', '--tx=2*popen') - res.stdout.fnmatch_lines([ - "*Replacing crashed worker*", - "*Worker*crashed while running*", - "*2 failed*2 passed*", - ]) + """ + ) + res = testdir.runpytest(f, "--dist=each", "--tx=2*popen") + res.stdout.fnmatch_lines( + [ + "*Replacing crashed worker*", + "*Worker*crashed while running*", + "*2 failed*2 passed*", + ] + ) def test_max_worker_restart(self, testdir): - f = testdir.makepyfile(""" + f = testdir.makepyfile( + """ import os def test_a(): pass def test_b(): os._exit(1) def test_c(): os._exit(1) def test_d(): pass - """) - res = testdir.runpytest(f, '-n4', '--max-worker-restart=1') - res.stdout.fnmatch_lines([ - "*Replacing crashed worker*", - "*Maximum crashed workers reached: 1*", - "*Worker*crashed while running*", - "*Worker*crashed while running*", - "*2 failed*2 passed*", - ]) + """ + ) + res = testdir.runpytest(f, "-n4", "--max-worker-restart=1") + res.stdout.fnmatch_lines( + [ + "*Replacing crashed worker*", + "*Maximum crashed workers reached: 1*", + "*Worker*crashed while running*", + "*Worker*crashed while running*", + "*2 failed*2 passed*", + ] + ) def test_max_worker_restart_die(self, testdir): - f = testdir.makepyfile(""" + f = testdir.makepyfile( + """ import os os._exit(1) - """) - res = testdir.runpytest(f, '-n4', '--max-worker-restart=0') - res.stdout.fnmatch_lines([ - "*Unexpectedly no active workers*", - "*INTERNALERROR*" - ]) + """ + ) + res = testdir.runpytest(f, "-n4", "--max-worker-restart=0") + res.stdout.fnmatch_lines( + ["*Unexpectedly no active workers*", "*INTERNALERROR*"] + ) def test_disable_restart(self, testdir): - f = testdir.makepyfile(""" + f = testdir.makepyfile( + """ import os def test_a(): pass def test_b(): os._exit(1) def test_c(): pass - """) - res = testdir.runpytest(f, '-n4', '--max-worker-restart=0') - res.stdout.fnmatch_lines([ - "*Worker restarting disabled*", - "*Worker*crashed while running*", - "*1 failed*2 passed*", - ]) + """ + ) + res = testdir.runpytest(f, "-n4", "--max-worker-restart=0") + res.stdout.fnmatch_lines( + [ + "*Worker restarting disabled*", + "*Worker*crashed while running*", + "*1 failed*2 passed*", + ] + ) -@pytest.mark.parametrize('n', [0, 2]) +@pytest.mark.parametrize("n", [0, 2]) def test_worker_id_fixture(testdir, n): import glob - f = testdir.makepyfile(""" + + 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 *') + 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 == {'master'} + assert worker_ids == {"master"} else: - assert worker_ids == {'gw0', 'gw1'} + assert worker_ids == {"gw0", "gw1"} -@pytest.mark.parametrize('tb', - ['auto', 'long', 'short', 'no', 'line', 'native']) +@pytest.mark.parametrize("tb", ["auto", "long", "short", "no", "line", "native"]) def test_error_report_styles(testdir, tb): - testdir.makepyfile(""" + testdir.makepyfile( + """ import pytest def test_error_report_styles(): raise RuntimeError('some failure happened') - """) - result = testdir.runpytest('-n1', '--tb=%s' % tb) - if tb != 'no': - result.stdout.fnmatch_lines('*some failure happened*') + """ + ) + result = testdir.runpytest("-n1", "--tb=%s" % tb) + if tb != "no": + result.stdout.fnmatch_lines("*some failure happened*") result.assert_outcomes(failed=1) @@ -815,27 +904,30 @@ def test_color_yes_collection_on_non_atty(testdir, request): 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(""" + 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'] + """ + ) + 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() + 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(""" + testdir.makepyfile( + """ import pytest @pytest.fixture(params=['1', '2']) @@ -846,14 +938,14 @@ def test_internal_error_with_maxfail(testdir): 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() + """ + ) + result = testdir.runpytest_subprocess("--maxfail=1", "-n1") + result.stdout.fnmatch_lines(["* 1 error in *"]) + assert "INTERNALERROR" not in result.stderr.str() class TestLoadScope: - def test_by_module(self, testdir): test_file = """ import pytest @@ -861,18 +953,18 @@ class TestLoadScope: def test(i): pass """ - testdir.makepyfile( - test_a=test_file, - test_b=test_file, - ) - result = testdir.runpytest('-n2', '--dist=loadscope', '-v') + testdir.makepyfile(test_a=test_file, test_b=test_file) + result = testdir.runpytest("-n2", "--dist=loadscope", "-v") assert get_workers_and_test_count_by_prefix( - 'test_a.py::test', result.outlines) in ({'gw0': 10}, {'gw1': 10}) + "test_a.py::test", result.outlines + ) in ({"gw0": 10}, {"gw1": 10}) assert get_workers_and_test_count_by_prefix( - 'test_b.py::test', result.outlines) in ({'gw0': 10}, {'gw1': 10}) + "test_b.py::test", result.outlines + ) in ({"gw0": 10}, {"gw1": 10}) def test_by_class(self, testdir): - testdir.makepyfile(test_a=""" + testdir.makepyfile( + test_a=""" import pytest class TestA: @pytest.mark.parametrize('i', range(10)) @@ -883,12 +975,15 @@ class TestLoadScope: @pytest.mark.parametrize('i', range(10)) def test(self, i): pass - """) - result = testdir.runpytest('-n2', '--dist=loadscope', '-v') + """ + ) + result = testdir.runpytest("-n2", "--dist=loadscope", "-v") assert get_workers_and_test_count_by_prefix( - 'test_a.py::TestA', result.outlines) in ({'gw0': 10}, {'gw1': 10}) + "test_a.py::TestA", result.outlines + ) in ({"gw0": 10}, {"gw1": 10}) assert get_workers_and_test_count_by_prefix( - 'test_a.py::TestB', result.outlines) in ({'gw0': 10}, {'gw1': 10}) + "test_a.py::TestB", result.outlines + ) in ({"gw0": 10}, {"gw1": 10}) def test_module_single_start(self, testdir): """Fix test suite never finishing in case all workers start with a single test (#277).""" @@ -904,28 +999,19 @@ class TestLoadScope: def test_2(): pass """ - testdir.makepyfile( - test_a=test_file1, - test_b=test_file1, - test_c=test_file2 - ) - result = testdir.runpytest('-n2', '--dist=loadscope', '-v') - a = get_workers_and_test_count_by_prefix('test_a.py::test', - result.outlines) - b = get_workers_and_test_count_by_prefix('test_b.py::test', - result.outlines) - c1 = get_workers_and_test_count_by_prefix('test_c.py::test_1', - result.outlines) - c2 = get_workers_and_test_count_by_prefix('test_c.py::test_2', - result.outlines) - assert a in ({'gw0': 1}, {'gw1': 1}) - assert b in ({'gw0': 1}, {'gw1': 1}) + testdir.makepyfile(test_a=test_file1, test_b=test_file1, test_c=test_file2) + result = testdir.runpytest("-n2", "--dist=loadscope", "-v") + a = get_workers_and_test_count_by_prefix("test_a.py::test", result.outlines) + b = get_workers_and_test_count_by_prefix("test_b.py::test", result.outlines) + c1 = get_workers_and_test_count_by_prefix("test_c.py::test_1", result.outlines) + c2 = get_workers_and_test_count_by_prefix("test_c.py::test_2", result.outlines) + assert a in ({"gw0": 1}, {"gw1": 1}) + assert b in ({"gw0": 1}, {"gw1": 1}) assert a.items() != b.items() assert c1 == c2 class TestFileScope: - def test_by_module(self, testdir): test_file = """ import pytest @@ -939,23 +1025,27 @@ class TestFileScope: def test(self, i): pass """ - testdir.makepyfile( - test_a=test_file, - test_b=test_file, - ) - result = testdir.runpytest('-n2', '--dist=loadfile', '-v') + testdir.makepyfile(test_a=test_file, test_b=test_file) + result = testdir.runpytest("-n2", "--dist=loadfile", "-v") test_a_workers_and_test_count = get_workers_and_test_count_by_prefix( - 'test_a.py::TestA', result.outlines) + "test_a.py::TestA", result.outlines + ) test_b_workers_and_test_count = get_workers_and_test_count_by_prefix( - 'test_b.py::TestB', result.outlines) + "test_b.py::TestB", result.outlines + ) - assert test_a_workers_and_test_count in ({'gw0': 10}, {'gw1': 0}) or \ - test_a_workers_and_test_count in ({'gw0': 0}, {'gw1': 10}) - assert test_b_workers_and_test_count in ({'gw0': 10}, {'gw1': 0}) or \ - test_b_workers_and_test_count in ({'gw0': 0}, {'gw1': 10}) + assert test_a_workers_and_test_count in ( + {"gw0": 10}, + {"gw1": 0}, + ) or test_a_workers_and_test_count in ({"gw0": 0}, {"gw1": 10}) + assert test_b_workers_and_test_count in ( + {"gw0": 10}, + {"gw1": 0}, + ) or test_b_workers_and_test_count in ({"gw0": 0}, {"gw1": 10}) def test_by_class(self, testdir): - testdir.makepyfile(test_a=""" + testdir.makepyfile( + test_a=""" import pytest class TestA: @pytest.mark.parametrize('i', range(10)) @@ -966,17 +1056,24 @@ class TestFileScope: @pytest.mark.parametrize('i', range(10)) def test(self, i): pass - """) - result = testdir.runpytest('-n2', '--dist=loadfile', '-v') + """ + ) + result = testdir.runpytest("-n2", "--dist=loadfile", "-v") test_a_workers_and_test_count = get_workers_and_test_count_by_prefix( - 'test_a.py::TestA', result.outlines) + "test_a.py::TestA", result.outlines + ) test_b_workers_and_test_count = get_workers_and_test_count_by_prefix( - 'test_a.py::TestB', result.outlines) + "test_a.py::TestB", result.outlines + ) - assert test_a_workers_and_test_count in ({'gw0': 10}, {'gw1': 0}) or \ - test_a_workers_and_test_count in ({'gw0': 0}, {'gw1': 10}) - assert test_b_workers_and_test_count in ({'gw0': 10}, {'gw1': 0}) or \ - test_b_workers_and_test_count in ({'gw0': 0}, {'gw1': 10}) + assert test_a_workers_and_test_count in ( + {"gw0": 10}, + {"gw1": 0}, + ) or test_a_workers_and_test_count in ({"gw0": 0}, {"gw1": 10}) + assert test_b_workers_and_test_count in ( + {"gw0": 10}, + {"gw1": 0}, + ) or test_b_workers_and_test_count in ({"gw0": 0}, {"gw1": 10}) def test_module_single_start(self, testdir): """Fix test suite never finishing in case all workers start with a single test (#277).""" @@ -992,22 +1089,14 @@ class TestFileScope: def test_2(): pass """ - testdir.makepyfile( - test_a=test_file1, - test_b=test_file1, - test_c=test_file2 - ) - result = testdir.runpytest('-n2', '--dist=loadfile', '-v') - a = get_workers_and_test_count_by_prefix('test_a.py::test', - result.outlines) - b = get_workers_and_test_count_by_prefix('test_b.py::test', - result.outlines) - c1 = get_workers_and_test_count_by_prefix('test_c.py::test_1', - result.outlines) - c2 = get_workers_and_test_count_by_prefix('test_c.py::test_2', - result.outlines) - assert a in ({'gw0': 1}, {'gw1': 1}) - assert b in ({'gw0': 1}, {'gw1': 1}) + testdir.makepyfile(test_a=test_file1, test_b=test_file1, test_c=test_file2) + result = testdir.runpytest("-n2", "--dist=loadfile", "-v") + a = get_workers_and_test_count_by_prefix("test_a.py::test", result.outlines) + b = get_workers_and_test_count_by_prefix("test_b.py::test", result.outlines) + c1 = get_workers_and_test_count_by_prefix("test_c.py::test_1", result.outlines) + c2 = get_workers_and_test_count_by_prefix("test_c.py::test_2", result.outlines) + assert a in ({"gw0": 1}, {"gw1": 1}) + assert b in ({"gw0": 1}, {"gw1": 1}) assert a.items() != b.items() assert c1 == c2 @@ -1016,21 +1105,24 @@ def parse_tests_and_workers_from_output(lines): result = [] for line in lines: # example match: "[gw0] PASSED test_a.py::test[7]" - m = re.match(r''' + m = re.match( + r""" \[(gw\d)\] # worker \s* (?:\[\s*\d+%\])? # progress indicator (pytest >=3.3) \s(.*?) # status string ("PASSED") \s(.*::.*) # nodeid - ''', line.strip(), re.VERBOSE) + """, + line.strip(), + re.VERBOSE, + ) if m: worker, status, nodeid = m.groups() result.append((worker, status, nodeid)) return result -def get_workers_and_test_count_by_prefix(prefix, lines, - expected_status='PASSED'): +def get_workers_and_test_count_by_prefix(prefix, lines, expected_status="PASSED"): result = {} for worker, status, nodeid in parse_tests_and_workers_from_output(lines): if expected_status == status and nodeid.startswith(prefix): diff --git a/testing/conftest.py b/testing/conftest.py index 8d28ff8..dd01f3e 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -20,6 +20,7 @@ pytest_plugins = "pytester" @pytest.fixture(autouse=True) def _divert_atexit(request, monkeypatch): import atexit + finalizers = [] def finish(): @@ -31,10 +32,12 @@ def _divert_atexit(request, monkeypatch): def pytest_addoption(parser): - parser.addoption('--gx', - action="append", - dest="gspecs", - help="add a global test environment, XSpec-syntax. ") + parser.addoption( + "--gx", + action="append", + dest="gspecs", + help="add a global test environment, XSpec-syntax. ", + ) @pytest.fixture diff --git a/testing/test_dsession.py b/testing/test_dsession.py index d57eddd..a75866f 100644 --- a/testing/test_dsession.py +++ b/testing/test_dsession.py @@ -1,9 +1,6 @@ from xdist.dsession import DSession from xdist.report import report_collection_diff -from xdist.scheduler import ( - EachScheduling, - LoadScheduling, -) +from xdist.scheduler import EachScheduling, LoadScheduling import py import pytest @@ -60,7 +57,7 @@ class TestEachScheduling: sched = EachScheduling(config) sched.add_node(node1) sched.add_node(node2) - collection = ["a.py::test_1", ] + collection = ["a.py::test_1"] assert not sched.collection_is_completed sched.add_node_collection(node1, collection) assert not sched.collection_is_completed @@ -70,8 +67,8 @@ class TestEachScheduling: assert sched.node2collection[node2] == collection sched.schedule() assert sched.tests_finished - assert node1.sent == ['ALL'] - assert node2.sent == ['ALL'] + assert node1.sent == ["ALL"] + assert node2.sent == ["ALL"] sched.mark_test_complete(node1, 0) assert sched.tests_finished sched.mark_test_complete(node2, 0) @@ -82,7 +79,7 @@ class TestEachScheduling: config = testdir.parseconfig("--tx=popen") sched = EachScheduling(config) sched.add_node(node1) - collection = ["a.py::test_1", ] + collection = ["a.py::test_1"] assert not sched.collection_is_completed sched.add_node_collection(node1, collection) assert sched.collection_is_completed @@ -230,7 +227,7 @@ class TestLoadScheduling: sched.schedule() assert len(collect_hook.reports) == 1 rep = collect_hook.reports[0] - assert 'Different tests were collected between' in rep.longrepr + assert "Different tests were collected between" in rep.longrepr class TestDistReporter: @@ -238,6 +235,7 @@ class TestDistReporter: def test_rsync_printing(self, testdir, linecomp): config = testdir.parseconfig() from _pytest.pytest_terminal import TerminalReporter + rep = TerminalReporter(config, file=linecomp.stringio) config.pluginmanager.register(rep, "terminalreporter") dsession = DSession(config) @@ -249,6 +247,7 @@ class TestDistReporter: class gw2: id = "X2" spec = execnet.XSpec("popen") + # class rinfo: # version_info = (2, 5, 1, 'final', 0) # executable = "hello" @@ -260,47 +259,50 @@ class TestDistReporter: # "*X1*popen*xyz*2.5*" # ]) dsession.pytest_xdist_rsyncstart(source="hello", gateways=[gw1, gw2]) - linecomp.assert_contains_lines(["[X1,X2] rsyncing: hello", ]) + linecomp.assert_contains_lines(["[X1,X2] rsyncing: hello"]) def test_report_collection_diff_equal(): """Test reporting of equal collections.""" - from_collection = to_collection = ['aaa', 'bbb', 'ccc'] + from_collection = to_collection = ["aaa", "bbb", "ccc"] assert report_collection_diff(from_collection, to_collection, 1, 2) is None def test_report_collection_diff_different(): """Test reporting of different collections.""" - from_collection = ['aaa', 'bbb', 'ccc', 'YYY'] - to_collection = ['aZa', 'bbb', 'XXX', 'ccc'] + from_collection = ["aaa", "bbb", "ccc", "YYY"] + to_collection = ["aZa", "bbb", "XXX", "ccc"] error_message = ( - 'Different tests were collected between 1 and 2. The difference is:\n' - '--- 1\n' - '\n' - '+++ 2\n' - '\n' - '@@ -1,4 +1,4 @@\n' - '\n' - '-aaa\n' - '+aZa\n' - ' bbb\n' - '+XXX\n' - ' ccc\n' - '-YYY') + "Different tests were collected between 1 and 2. The difference is:\n" + "--- 1\n" + "\n" + "+++ 2\n" + "\n" + "@@ -1,4 +1,4 @@\n" + "\n" + "-aaa\n" + "+aZa\n" + " bbb\n" + "+XXX\n" + " 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 @pytest.mark.xfail(reason="duplicate test ids not supported yet") def test_pytest_issue419(testdir): - testdir.makepyfile(""" + testdir.makepyfile( + """ import pytest @pytest.mark.parametrize('birth_year', [1988, 1988, ]) def test_2011_table(birth_year): pass - """) + """ + ) reprec = testdir.inline_run("-n1") reprec.assertoutcome(passed=2) assert 0 diff --git a/testing/test_looponfail.py b/testing/test_looponfail.py index 4114802..a534754 100644 --- a/testing/test_looponfail.py +++ b/testing/test_looponfail.py @@ -60,7 +60,7 @@ class TestStatRecorder: p.remove() # make check()'s visit() call return our just removed # path as if we were in a race condition - monkeypatch.setattr(tmp, 'visit', lambda *args: [p]) + monkeypatch.setattr(tmp, "visit", lambda *args: [p]) changed = sd.check() assert changed @@ -84,7 +84,7 @@ class TestStatRecorder: sd = StatRecorder([tmp]) ret_values = [True, False] - monkeypatch.setattr(StatRecorder, 'check', lambda self: ret_values.pop()) + monkeypatch.setattr(StatRecorder, "check", lambda self: ret_values.pop()) sd.waitonchange(checkinterval=0.2) assert not ret_values @@ -110,19 +110,25 @@ class TestRemoteControl: assert not failures def test_failure_change(self, testdir): - modcol = testdir.getitem(""" + modcol = testdir.getitem( + """ def test_func(): assert 0 - """) + """ + ) control = RemoteControl(modcol.config) control.loop_once() assert control.failures - modcol.fspath.write(py.code.Source(""" + modcol.fspath.write( + py.code.Source( + """ def test_func(): assert 1 def test_new(): assert 0 - """)) + """ + ) + ) removepyc(modcol.fspath) control.loop_once() assert not control.failures @@ -131,14 +137,17 @@ class TestRemoteControl: assert str(control.failures).find("test_new") != -1 def test_failure_subdir_no_init(self, testdir): - modcol = testdir.getitem(""" + modcol = testdir.getitem( + """ def test_func(): assert 0 - """) + """ + ) parent = modcol.fspath.dirpath().dirpath() parent.chdir() - modcol.config.args = [py.path.local(x).relto(parent) - for x in modcol.config.args] + modcol.config.args = [ + py.path.local(x).relto(parent) for x in modcol.config.args + ] control = RemoteControl(modcol.config) control.loop_once() assert control.failures @@ -148,70 +157,87 @@ class TestRemoteControl: class TestLooponFailing: def test_looponfail_from_fail_to_ok(self, testdir): - modcol = testdir.getmodulecol(""" + modcol = testdir.getmodulecol( + """ def test_one(): x = 0 assert x == 1 def test_two(): assert 1 - """) + """ + ) remotecontrol = RemoteControl(modcol.config) remotecontrol.loop_once() assert len(remotecontrol.failures) == 1 - modcol.fspath.write(py.code.Source(""" + modcol.fspath.write( + py.code.Source( + """ def test_one(): assert 1 def test_two(): assert 1 - """)) + """ + ) + ) removepyc(modcol.fspath) remotecontrol.loop_once() assert not remotecontrol.failures def test_looponfail_from_one_to_two_tests(self, testdir): - modcol = testdir.getmodulecol(""" + modcol = testdir.getmodulecol( + """ def test_one(): assert 0 - """) + """ + ) remotecontrol = RemoteControl(modcol.config) remotecontrol.loop_once() assert len(remotecontrol.failures) == 1 - assert 'test_one' in remotecontrol.failures[0] + assert "test_one" in remotecontrol.failures[0] - modcol.fspath.write(py.code.Source(""" + modcol.fspath.write( + py.code.Source( + """ def test_one(): assert 1 # passes now def test_two(): assert 0 # new and fails - """)) + """ + ) + ) removepyc(modcol.fspath) remotecontrol.loop_once() assert len(remotecontrol.failures) == 0 remotecontrol.loop_once() assert len(remotecontrol.failures) == 1 - assert 'test_one' not in remotecontrol.failures[0] - assert 'test_two' in remotecontrol.failures[0] + assert "test_one" not in remotecontrol.failures[0] + assert "test_two" in remotecontrol.failures[0] - @py.test.mark.xfail(py.test.__version__ >= "3.1", - reason="broken by pytest 3.1+") + @py.test.mark.xfail(py.test.__version__ >= "3.1", reason="broken by pytest 3.1+") def test_looponfail_removed_test(self, testdir): - modcol = testdir.getmodulecol(""" + modcol = testdir.getmodulecol( + """ def test_one(): assert 0 def test_two(): assert 0 - """) + """ + ) remotecontrol = RemoteControl(modcol.config) remotecontrol.loop_once() assert len(remotecontrol.failures) == 2 - modcol.fspath.write(py.code.Source(""" + modcol.fspath.write( + py.code.Source( + """ def test_xxx(): # renamed test assert 0 def test_two(): assert 1 # pass now - """)) + """ + ) + ) removepyc(modcol.fspath) remotecontrol.loop_once() assert len(remotecontrol.failures) == 0 @@ -220,10 +246,12 @@ class TestLooponFailing: assert len(remotecontrol.failures) == 1 def test_looponfail_multiple_errors(self, testdir, monkeypatch): - modcol = testdir.getmodulecol(""" + modcol = testdir.getmodulecol( + """ def test_one(): assert 0 - """) + """ + ) remotecontrol = RemoteControl(modcol.config) orig_runsession = remotecontrol.runsession @@ -233,18 +261,20 @@ class TestLooponFailing: print(failures) return failures * 2, reports, collection_failed - monkeypatch.setattr(remotecontrol, 'runsession', runsession_dups) + monkeypatch.setattr(remotecontrol, "runsession", runsession_dups) remotecontrol.loop_once() assert len(remotecontrol.failures) == 1 class TestFunctional: def test_fail_to_ok(self, testdir): - p = testdir.makepyfile(""" + p = testdir.makepyfile( + """ def test_one(): x = 0 assert x == 1 - """) + """ + ) # p = testdir.mkdir("sub").join(p1.basename) # p1.move(p) child = testdir.spawn_pytest("-f %s --traceconfig" % p) @@ -253,21 +283,27 @@ class TestFunctional: child.expect("1 failed") child.expect("### LOOPONFAILING ####") child.expect("waiting for changes") - p.write(py.code.Source(""" + p.write( + py.code.Source( + """ def test_one(): x = 1 assert x == 1 - """)) + """ + ) + ) child.expect(".*1 passed.*") child.kill(15) def test_xfail_passes(self, testdir): - p = testdir.makepyfile(""" + p = testdir.makepyfile( + """ import py @py.test.mark.xfail def test_one(): pass - """) + """ + ) child = testdir.spawn_pytest("-f %s" % p) child.expect("1 xpass") # child.expect("### LOOPONFAILING ####") diff --git a/testing/test_newhooks.py b/testing/test_newhooks.py index caad24a..22928fe 100644 --- a/testing/test_newhooks.py +++ b/testing/test_newhooks.py @@ -2,21 +2,23 @@ import pytest class TestHooks: - @pytest.fixture(autouse=True) def create_test_file(self, testdir): - testdir.makepyfile(""" + 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 "node", "nodeid" and "worker_id" attributes. (#8) """ - testdir.makeconftest(""" + testdir.makeconftest( + """ def pytest_runtest_logreport(report): if hasattr(report, 'node'): if report.when == "call": @@ -27,29 +29,31 @@ class TestHooks: else: print("HOOK: %s %s" % (report.nodeid, report.worker_id)) - """) - 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*', - ]) + """ + ) + 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(""" + testdir.makeconftest( + """ def pytest_xdist_node_collection_finished(node, ids): workerid = node.workerinput['workerid'] stripped_ids = [x.split('::')[1] for x in ids] print("HOOK: %s %s" % (workerid, ', '.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*', - ]) + """ + ) + 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*"]) diff --git a/testing/test_plugin.py b/testing/test_plugin.py index 7a4ac46..182a3e5 100644 --- a/testing/test_plugin.py +++ b/testing/test_plugin.py @@ -16,14 +16,15 @@ def test_dist_incompatibility_messages(testdir): def test_dist_options(testdir): from xdist.plugin import pytest_cmdline_main as check_options + config = testdir.parseconfigure("-n 2") check_options(config) assert config.option.dist == "load" - assert config.option.tx == ['popen'] * 2 + assert config.option.tx == ["popen"] * 2 config = testdir.parseconfigure("--numprocesses", "2") check_options(config) assert config.option.dist == "load" - assert config.option.tx == ['popen'] * 2 + assert config.option.tx == ["popen"] * 2 config = testdir.parseconfigure("-d") check_options(config) assert config.option.dist == "load" @@ -31,28 +32,31 @@ def test_dist_options(testdir): def test_auto_detect_cpus(testdir, monkeypatch): import os - if hasattr(os, 'sched_getaffinity'): - monkeypatch.setattr(os, 'sched_getaffinity', lambda _pid: set(range(99))) - elif hasattr(os, 'cpu_count'): - monkeypatch.setattr(os, 'cpu_count', lambda: 99) + + if hasattr(os, "sched_getaffinity"): + monkeypatch.setattr(os, "sched_getaffinity", lambda _pid: set(range(99))) + elif hasattr(os, "cpu_count"): + monkeypatch.setattr(os, "cpu_count", lambda: 99) else: import multiprocessing - monkeypatch.setattr(multiprocessing, 'cpu_count', lambda: 99) + + monkeypatch.setattr(multiprocessing, "cpu_count", lambda: 99) config = testdir.parseconfigure("-n2") - assert config.getoption('numprocesses') == 2 + assert config.getoption("numprocesses") == 2 config = testdir.parseconfigure("-nauto") - assert config.getoption('numprocesses') == 99 + assert config.getoption("numprocesses") == 99 - monkeypatch.delattr(os, 'sched_getaffinity', raising=False) - monkeypatch.setenv('TRAVIS', 'true') + monkeypatch.delattr(os, "sched_getaffinity", raising=False) + monkeypatch.setenv("TRAVIS", "true") config = testdir.parseconfigure("-nauto") - assert config.getoption('numprocesses') == 2 + assert config.getoption("numprocesses") == 2 def test_boxed_with_collect_only(testdir): from xdist.plugin import pytest_cmdline_main as check_options + config = testdir.parseconfigure("-n1", "--boxed") check_options(config) assert config.option.forked @@ -92,13 +96,13 @@ class TestDistOptions: assert xspecs[1].ssh == "xyz" def test_xspecs_multiplied(self, testdir): - config = testdir.parseconfigure("--tx=3*popen", ) + config = testdir.parseconfigure("--tx=3*popen") xspecs = NodeManager(config)._getxspecs() assert len(xspecs) == 3 assert xspecs[1].popen def test_getrsyncdirs(self, testdir): - config = testdir.parseconfigure('--rsyncdir=' + str(testdir.tmpdir)) + config = testdir.parseconfigure("--rsyncdir=" + str(testdir.tmpdir)) nm = NodeManager(config, specs=[execnet.XSpec("popen")]) assert not nm._getrsyncdirs() nm = NodeManager(config, specs=[execnet.XSpec("popen//chdir=qwe")]) @@ -106,23 +110,24 @@ class TestDistOptions: assert testdir.tmpdir in nm.roots def test_getrsyncignore(self, testdir): - config = testdir.parseconfigure('--rsyncignore=fo*') + config = testdir.parseconfigure("--rsyncignore=fo*") nm = NodeManager(config, specs=[execnet.XSpec("popen//chdir=qwe")]) - assert 'fo*' in nm.rsyncoptions['ignores'] + assert "fo*" in nm.rsyncoptions["ignores"] def test_getrsyncdirs_with_conftest(self, testdir): p = py.path.local() - for bn in 'x y z'.split(): + for bn in "x y z".split(): p.mkdir(bn) - testdir.makeini(""" + testdir.makeini( + """ [pytest] rsyncdirs= x - """) - config = testdir.parseconfigure( - testdir.tmpdir, '--rsyncdir=y', '--rsyncdir=z') + """ + ) + config = testdir.parseconfigure(testdir.tmpdir, "--rsyncdir=y", "--rsyncdir=z") nm = NodeManager(config, specs=[execnet.XSpec("popen//chdir=xyz")]) roots = nm._getrsyncdirs() # assert len(roots) == 3 + 1 # pylib - assert py.path.local('y') in roots - assert py.path.local('z') in roots - assert testdir.tmpdir.join('x') in roots + assert py.path.local("y") in roots + assert py.path.local("z") in roots + assert testdir.tmpdir.join("x") in roots diff --git a/testing/test_remote.py b/testing/test_remote.py index 38369e9..57a1e33 100644 --- a/testing/test_remote.py +++ b/testing/test_remote.py @@ -35,7 +35,7 @@ class WorkerSetup: self.testdir = testdir self.events = Queue() - def setup(self, ): + def setup(self,): self.testdir.chdir() # import os ; os.environ['EXECNET_DEBUG'] = "2" self.gateway = execnet.makegateway() @@ -45,8 +45,7 @@ class WorkerSetup: class DummyMananger: specs = [0, 1] - self.slp = WorkerController(DummyMananger, self.gateway, config, - putevent) + self.slp = WorkerController(DummyMananger, self.gateway, config, putevent) self.request.addfinalizer(self.slp.ensure_teardown) self.slp.setup() @@ -59,7 +58,7 @@ class WorkerSetup: ev = EventCall(data) if name is None or ev.name == name: return ev - print("skipping %s" % (ev, )) + print("skipping %s" % (ev,)) def sendcommand(self, name, **kwargs): self.slp.sendcommand(name, **kwargs) @@ -70,9 +69,10 @@ def worker(request, testdir): return WorkerSetup(request, testdir) -@pytest.mark.xfail(reason='#59') +@pytest.mark.xfail(reason="#59") def test_remoteinitconfig(testdir): from xdist.remote import remote_initconfig + config1 = testdir.parseconfig() config2 = remote_initconfig(config1.option.__dict__, config1.args) assert config2.option.__dict__ == config1.option.__dict__ @@ -81,29 +81,33 @@ def test_remoteinitconfig(testdir): class TestReportSerialization: def test_xdist_longrepr_to_str_issue_241(self, testdir): - testdir.makepyfile(""" + testdir.makepyfile( + """ import os def test_a(): assert False def test_b(): pass - """) - testdir.makeconftest(""" + """ + ) + testdir.makeconftest( + """ def pytest_runtest_logreport(report): print(report.longrepr) - """) - res = testdir.runpytest('-n1', '-s') - res.stdout.fnmatch_lines([ - '*1 failed, 1 passed *' - ]) + """ + ) + res = testdir.runpytest("-n1", "-s") + res.stdout.fnmatch_lines(["*1 failed, 1 passed *"]) def test_xdist_report_longrepr_reprcrash_130(self, testdir): - reprec = testdir.inline_runsource(""" + reprec = testdir.inline_runsource( + """ import py def test_fail(): assert False, 'Expected Message' - """) + """ + ) reports = reprec.getreports("pytest_runtest_logreport") assert len(reports) == 3 rep = reports[1] - added_section = ('Failure Metadata', str("metadata metadata"), "*") + added_section = ("Failure Metadata", str("metadata metadata"), "*") rep.longrepr.sections.append(added_section) d = serialize_report(rep) check_marshallable(d) @@ -111,28 +115,31 @@ class TestReportSerialization: # Check assembled == rep assert a.__dict__.keys() == rep.__dict__.keys() for key in rep.__dict__.keys(): - if key != 'longrepr': + if key != "longrepr": assert getattr(a, key) == getattr(rep, key) assert rep.longrepr.reprcrash.lineno == a.longrepr.reprcrash.lineno assert rep.longrepr.reprcrash.message == a.longrepr.reprcrash.message assert rep.longrepr.reprcrash.path == a.longrepr.reprcrash.path - assert rep.longrepr.reprtraceback.entrysep \ - == a.longrepr.reprtraceback.entrysep - assert rep.longrepr.reprtraceback.extraline \ - == a.longrepr.reprtraceback.extraline - assert rep.longrepr.reprtraceback.style \ - == a.longrepr.reprtraceback.style + assert rep.longrepr.reprtraceback.entrysep == a.longrepr.reprtraceback.entrysep + assert ( + rep.longrepr.reprtraceback.extraline == a.longrepr.reprtraceback.extraline + ) + assert rep.longrepr.reprtraceback.style == a.longrepr.reprtraceback.style assert rep.longrepr.sections == a.longrepr.sections # Missing section attribute PR171 assert added_section in a.longrepr.sections def test_reprentries_serialization_170(self, testdir): from _pytest._code.code import ReprEntry - reprec = testdir.inline_runsource(""" + + reprec = testdir.inline_runsource( + """ def test_repr_entry(): x = 0 assert x - """, '--showlocals') + """, + "--showlocals", + ) reports = reprec.getreports("pytest_runtest_logreport") assert len(reports) == 3 rep = reports[1] @@ -146,7 +153,9 @@ class TestReportSerialization: assert rep_entries[i].lines == a_entries[i].lines assert rep_entries[i].localssep == a_entries[i].localssep assert rep_entries[i].reprfileloc.lineno == a_entries[i].reprfileloc.lineno - assert rep_entries[i].reprfileloc.message == a_entries[i].reprfileloc.message + assert ( + rep_entries[i].reprfileloc.message == a_entries[i].reprfileloc.message + ) assert rep_entries[i].reprfileloc.path == a_entries[i].reprfileloc.path assert rep_entries[i].reprfuncargs.args == a_entries[i].reprfuncargs.args assert rep_entries[i].reprlocals.lines == a_entries[i].reprlocals.lines @@ -154,11 +163,15 @@ class TestReportSerialization: def test_reprentries_serialization_196(self, testdir): from _pytest._code.code import ReprEntryNative - reprec = testdir.inline_runsource(""" + + reprec = testdir.inline_runsource( + """ def test_repr_entry_native(): x = 0 assert x - """, '--tb=native') + """, + "--tb=native", + ) reports = reprec.getreports("pytest_runtest_logreport") assert len(reports) == 3 rep = reports[1] @@ -172,7 +185,8 @@ class TestReportSerialization: assert rep_entries[i].lines == a_entries[i].lines def test_itemreport_outcomes(self, testdir): - reprec = testdir.inline_runsource(""" + reprec = testdir.inline_runsource( + """ import py def test_pass(): pass def test_fail(): 0/0 @@ -184,7 +198,8 @@ class TestReportSerialization: def test_xfail(): 0/0 def test_xfail_imperative(): py.test.xfail("hello") - """) + """ + ) reports = reprec.getreports("pytest_runtest_logreport") assert len(reports) == 17 # with setup/teardown "passed" reports for rep in reports: @@ -246,10 +261,12 @@ class TestReportSerialization: class TestWorkerInteractor: def test_basic_collect_and_runtests(self, worker): - worker.testdir.makepyfile(""" + worker.testdir.makepyfile( + """ def test_func(): pass - """) + """ + ) worker.setup() ev = worker.popevent() assert ev.name == "workerready" @@ -257,8 +274,8 @@ class TestWorkerInteractor: assert ev.name == "collectionstart" assert not ev.kwargs ev = worker.popevent("collectionfinish") - assert ev.kwargs['topdir'] == worker.testdir.tmpdir - ids = ev.kwargs['ids'] + assert ev.kwargs["topdir"] == worker.testdir.tmpdir + ids = ev.kwargs["ids"] assert len(ids) == 1 worker.sendcommand("runtests", indices=list(range(len(ids)))) worker.sendcommand("shutdown") @@ -268,20 +285,23 @@ class TestWorkerInteractor: ev = worker.popevent("testreport") # setup ev = worker.popevent("testreport") assert ev.name == "testreport" - rep = unserialize_report(ev.name, ev.kwargs['data']) + rep = unserialize_report(ev.name, ev.kwargs["data"]) assert rep.nodeid.endswith("::test_func") assert rep.passed assert rep.when == "call" ev = worker.popevent("workerfinished") - assert 'workeroutput' in ev.kwargs + assert "workeroutput" in ev.kwargs - @pytest.mark.skipif(pytest.__version__ >= '3.0', - reason='skip at module level illegal in pytest 3.0') + @pytest.mark.skipif( + pytest.__version__ >= "3.0", reason="skip at module level illegal in pytest 3.0" + ) def test_remote_collect_skip(self, worker): - worker.testdir.makepyfile(""" + worker.testdir.makepyfile( + """ import py py.test.skip("hello") - """) + """ + ) worker.setup() ev = worker.popevent("collectionstart") assert not ev.kwargs @@ -289,10 +309,10 @@ class TestWorkerInteractor: assert ev.name == "collectreport" ev = worker.popevent() assert ev.name == "collectreport" - rep = unserialize_report(ev.name, ev.kwargs['data']) + rep = unserialize_report(ev.name, ev.kwargs["data"]) assert rep.skipped ev = worker.popevent("collectionfinish") - assert not ev.kwargs['ids'] + assert not ev.kwargs["ids"] def test_remote_collect_fail(self, worker): worker.testdir.makepyfile("""aasd qwe""") @@ -303,16 +323,18 @@ class TestWorkerInteractor: assert ev.name == "collectreport" ev = worker.popevent() assert ev.name == "collectreport" - rep = unserialize_report(ev.name, ev.kwargs['data']) + rep = unserialize_report(ev.name, ev.kwargs["data"]) assert rep.failed ev = worker.popevent("collectionfinish") - assert not ev.kwargs['ids'] + assert not ev.kwargs["ids"] def test_runtests_all(self, worker): - worker.testdir.makepyfile(""" + worker.testdir.makepyfile( + """ def test_func(): pass def test_func2(): pass - """) + """ + ) worker.setup() ev = worker.popevent() assert ev.name == "workerready" @@ -320,57 +342,63 @@ class TestWorkerInteractor: assert ev.name == "collectionstart" assert not ev.kwargs ev = worker.popevent("collectionfinish") - ids = ev.kwargs['ids'] + ids = ev.kwargs["ids"] assert len(ids) == 2 - worker.sendcommand("runtests_all", ) - worker.sendcommand("shutdown", ) + worker.sendcommand("runtests_all") + worker.sendcommand("shutdown") for func in "::test_func", "::test_func2": for i in range(3): # setup/call/teardown ev = worker.popevent("testreport") assert ev.name == "testreport" - rep = unserialize_report(ev.name, ev.kwargs['data']) + rep = unserialize_report(ev.name, ev.kwargs["data"]) assert rep.nodeid.endswith(func) ev = worker.popevent("workerfinished") - assert 'workeroutput' in ev.kwargs + assert "workeroutput" in ev.kwargs def test_happy_run_events_converted(self, testdir, worker): py.test.xfail("implement a simple test for event production") assert not worker.use_callback - worker.testdir.makepyfile(""" + worker.testdir.makepyfile( + """ def test_func(): pass - """) + """ + ) worker.setup() hookrec = testdir.getreportrecorder(worker.config) for data in worker.slp.channel: worker.slp.process_from_remote(data) worker.slp.process_from_remote(worker.slp.ENDMARK) pprint.pprint(hookrec.hookrecorder.calls) - hookrec.hookrecorder.contains([ - ("pytest_collectstart", "collector.fspath == aaa"), - ("pytest_pycollect_makeitem", "name == 'test_func'"), - ("pytest_collectreport", "report.collector.fspath == aaa"), - ("pytest_collectstart", "collector.fspath == bbb"), - ("pytest_pycollect_makeitem", "name == 'test_func'"), - ("pytest_collectreport", "report.collector.fspath == bbb"), - ]) + hookrec.hookrecorder.contains( + [ + ("pytest_collectstart", "collector.fspath == aaa"), + ("pytest_pycollect_makeitem", "name == 'test_func'"), + ("pytest_collectreport", "report.collector.fspath == aaa"), + ("pytest_collectstart", "collector.fspath == bbb"), + ("pytest_pycollect_makeitem", "name == 'test_func'"), + ("pytest_collectreport", "report.collector.fspath == bbb"), + ] + ) def test_process_from_remote_error_handling(self, worker, capsys): worker.use_callback = True worker.setup() - worker.slp.process_from_remote(('', ())) + worker.slp.process_from_remote(("", ())) out, err = capsys.readouterr() - assert 'INTERNALERROR> ValueError: unknown event: ' in out + assert "INTERNALERROR> ValueError: unknown event: " in out ev = worker.popevent() assert ev.name == "errordown" def test_remote_env_vars(testdir): - testdir.makepyfile(''' + 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-worker-restart=0') + """ + ) + result = testdir.runpytest("-n2", "--max-worker-restart=0") assert result.ret == 0 diff --git a/testing/test_slavemanage.py b/testing/test_slavemanage.py index 0b11ae9..c15d70f 100644 --- a/testing/test_slavemanage.py +++ b/testing/test_slavemanage.py @@ -42,7 +42,7 @@ def workercontroller(monkeypatch): def setup(self): pass - monkeypatch.setattr(workermanage, 'WorkerController', MockController) + monkeypatch.setattr(workermanage, "WorkerController", MockController) return MockController @@ -58,8 +58,7 @@ class TestNodeManagerPopen: for spec in NodeManager(config, specs, defaultchdir="abc").specs: assert spec.chdir == "abc" - def test_popen_makegateway_events(self, config, hookrecorder, - workercontroller): + def test_popen_makegateway_events(self, config, hookrecorder, workercontroller): hm = NodeManager(config, ["popen"] * 2) hm.setup_nodes(None) call = hookrecorder.popcall("pytest_xdist_setupnodes") @@ -108,15 +107,16 @@ class TestNodeManagerPopen: for gw in hm.group: hm.rsync(gw, source, notify=lambda *args: notifications.append(args)) assert len(notifications) == 1 - assert notifications[0] == ("rsyncrootready", hm.group['gw0'].spec, source) + assert notifications[0] == ("rsyncrootready", hm.group["gw0"].spec, source) hm.teardown_nodes() dest = dest.join(source.basename) assert dest.join("dir1").check() assert dest.join("dir1", "dir2").check() - assert dest.join("dir1", "dir2", 'hello').check() + assert dest.join("dir1", "dir2", "hello").check() - def test_rsync_same_popen_twice(self, config, mysetup, hookrecorder, - workercontroller): + def test_rsync_same_popen_twice( + self, config, mysetup, hookrecorder, workercontroller + ): source, dest = mysetup.source, mysetup.dest hm = NodeManager(config, ["popen//chdir=%s" % dest] * 2) hm.roots = [] @@ -142,9 +142,9 @@ class TestHRSync: files = list(source.visit(rec=syncer.filter, fil=syncer.filter)) assert len(files) == 3 basenames = [x.basename for x in files] - assert 'dir' in basenames - assert 'file.txt' in basenames - assert 'somedir' in basenames + assert "dir" in basenames + assert "file.txt" in basenames + assert "somedir" in basenames def test_hrsync_one_host(self, mysetup): source, dest = mysetup.source, mysetup.dest @@ -169,7 +169,8 @@ class TestNodeManager: nodemanager.makegateways() nodemanager.rsync_roots() p, = nodemanager.gwmanager.multi_exec( - "import os ; channel.send(os.getcwd())").receive_each() + "import os ; channel.send(os.getcwd())" + ).receive_each() p = py.path.local(p) print("remote curdir", p) assert p == mysetup.dest.join(config.topdir.basename) @@ -183,15 +184,17 @@ class TestNodeManager: dir2.ensure("hello") for rsyncroot in (dir1, source): dest.remove() - nodemanager = NodeManager(testdir.parseconfig( - "--tx", "popen//chdir=%s" % dest, "--rsyncdir", rsyncroot, - source, )) + nodemanager = NodeManager( + testdir.parseconfig( + "--tx", "popen//chdir=%s" % dest, "--rsyncdir", rsyncroot, source + ) + ) nodemanager.setup_nodes(None) # calls .rsync_roots() if rsyncroot == source: dest = dest.join("source") assert dest.join("dir1").check() assert dest.join("dir1", "dir2").check() - assert dest.join("dir1", "dir2", 'hello').check() + assert dest.join("dir1", "dir2", "hello").check() nodemanager.teardown_nodes() def test_init_rsync_roots(self, testdir, mysetup, workercontroller): @@ -200,10 +203,14 @@ class TestNodeManager: source.ensure("dir1", "somefile", dir=1) dir2.ensure("hello") source.ensure("bogusdir", "file") - source.join("tox.ini").write(textwrap.dedent(""" + source.join("tox.ini").write( + textwrap.dedent( + """ [pytest] rsyncdirs=dir1/dir2 - """)) + """ + ) + ) config = testdir.parseconfig(source) nodemanager = NodeManager(config, ["popen//chdir=%s" % dest]) nodemanager.setup_nodes(None) # calls .rsync_roots() @@ -219,27 +226,31 @@ class TestNodeManager: dir2.ensure("hello") source.ensure("foo", "bar") source.ensure("bar", "foo") - source.join("tox.ini").write(textwrap.dedent(""" + source.join("tox.ini").write( + textwrap.dedent( + """ [pytest] rsyncdirs = dir1 dir5 rsyncignore = dir1/dir2 dir5/dir6 foo* - """)) + """ + ) + ) config = testdir.parseconfig(source) - config.option.rsyncignore = ['bar'] + config.option.rsyncignore = ["bar"] nodemanager = NodeManager(config, ["popen//chdir=%s" % dest]) nodemanager.setup_nodes(None) # calls .rsync_roots() assert dest.join("dir1").check() assert not dest.join("dir1", "dir2").check() assert dest.join("dir5", "file").check() assert not dest.join("dir6").check() - assert not dest.join('foo').check() - assert not dest.join('bar').check() + assert not dest.join("foo").check() + assert not dest.join("bar").check() def test_optimise_popen(self, testdir, mysetup, workercontroller): source = mysetup.source specs = ["popen"] * 3 source.join("conftest.py").write("rsyncdirs = ['a']") - source.ensure('a', dir=1) + source.ensure("a", dir=1) config = testdir.parseconfig(source) nodemanager = NodeManager(config, specs) nodemanager.setup_nodes(None) # calls .rysnc_roots() @@ -248,12 +259,15 @@ class TestNodeManager: assert not gwspec.chdir def test_ssh_setup_nodes(self, specssh, testdir): - testdir.makepyfile(__init__="", - test_x=""" + testdir.makepyfile( + __init__="", + test_x=""" def test_one(): pass - """) - reprec = testdir.inline_run("-d", "--rsyncdir=%s" % testdir.tmpdir, - "--tx", specssh, testdir.tmpdir) + """, + ) + reprec = testdir.inline_run( + "-d", "--rsyncdir=%s" % testdir.tmpdir, "--tx", specssh, testdir.tmpdir + ) rep, = reprec.getreports("pytest_runtest_logreport") assert rep.passed diff --git a/tox.ini b/tox.ini index f0e63da..4532176 100644 --- a/tox.ini +++ b/tox.ini @@ -48,3 +48,8 @@ commands = [pytest] addopts = -rsfxX +[flake8] +max-line-length = 120 +ignore = E203,W503 + + diff --git a/xdist/__init__.py b/xdist/__init__.py index 7aa44d0..44d995d 100644 --- a/xdist/__init__.py +++ b/xdist/__init__.py @@ -1,3 +1,3 @@ from xdist._version import version as __version__ -__all__ = ['__version__'] +__all__ = ["__version__"] diff --git a/xdist/dsession.py b/xdist/dsession.py index 0b0b414..afc0f33 100644 --- a/xdist/dsession.py +++ b/xdist/dsession.py @@ -84,7 +84,7 @@ class DSession(object): def pytest_sessionfinish(self, session): """Shutdown all nodes.""" - nm = getattr(self, 'nodemanager', None) # if not fully initialized + nm = getattr(self, "nodemanager", None) # if not fully initialized if nm is not None: nm.teardown_nodes() self._session = None @@ -95,19 +95,18 @@ class DSession(object): @pytest.mark.trylast def pytest_xdist_make_scheduler(self, config, log): - dist = config.getvalue('dist') + dist = config.getvalue("dist") schedulers = { - 'each': EachScheduling, - 'load': LoadScheduling, - 'loadscope': LoadScopeScheduling, - 'loadfile': LoadFileScheduling, + "each": EachScheduling, + "load": LoadScheduling, + "loadscope": LoadScopeScheduling, + "loadfile": LoadFileScheduling, } return schedulers[dist](config, log) def pytest_runtestloop(self): self.sched = self.config.hook.pytest_xdist_make_scheduler( - config=self.config, - log=self.log + config=self.config, log=self.log ) assert self.sched is not None @@ -151,8 +150,8 @@ class DSession(object): collection without any further input. """ node.workerinfo = workerinfo - node.workerinfo['id'] = node.gateway.id - node.workerinfo['spec'] = node.gateway.spec + node.workerinfo["id"] = node.gateway.id + node.workerinfo["spec"] = node.gateway.spec # TODO: (#234 task) needs this for pytest. Remove when refactor in pytest repo node.slaveinfo = node.workerinfo @@ -172,7 +171,7 @@ class DSession(object): workerready before shutdown was triggered. """ self.config.hook.pytest_testnodedown(node=node, error=None) - if node.workeroutput['exitstatus'] == 2: # keyboard-interrupt + if node.workeroutput["exitstatus"] == 2: # keyboard-interrupt self.shouldstop = "%s received keyboard-interrupt" % (node,) self.worker_errordown(node, "keyboard-interrupt") return @@ -193,14 +192,15 @@ class DSession(object): self.handle_crashitem(crashitem, node) self._failed_nodes_count += 1 - maximum_reached = (self._max_worker_restart is not None and - self._failed_nodes_count > self._max_worker_restart) + maximum_reached = ( + self._max_worker_restart is not None + and self._failed_nodes_count > self._max_worker_restart + ) if maximum_reached: if self._max_worker_restart == 0: - msg = 'Worker restarting disabled' + msg = "Worker restarting disabled" else: - msg = "Maximum crashed workers reached: %d" % \ - self._max_worker_restart + msg = "Maximum crashed workers reached: %d" % self._max_worker_restart self.report_line(msg) else: self.report_line("Replacing crashed worker %s" % node.gateway.id) @@ -218,8 +218,7 @@ class DSession(object): """ if self.shuttingdown: return - self.config.hook.pytest_xdist_node_collection_finished(node=node, - ids=ids) + 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) @@ -230,19 +229,18 @@ class DSession(object): if self.terminal and not self.sched.has_pending: self.trdist.ensure_show_status() self.terminal.write_line("") - self.terminal.write_line("scheduling tests via %s" % ( - self.sched.__class__.__name__)) + self.terminal.write_line( + "scheduling tests via %s" % (self.sched.__class__.__name__) + ) self.sched.schedule() def worker_logstart(self, node, nodeid, location): """Emitted when a node calls the pytest_runtest_logstart hook.""" - self.config.hook.pytest_runtest_logstart( - nodeid=nodeid, location=location) + self.config.hook.pytest_runtest_logstart(nodeid=nodeid, location=location) def worker_logfinish(self, node, nodeid, location): """Emitted when a node calls the pytest_runtest_logfinish hook.""" - self.config.hook.pytest_runtest_logfinish( - nodeid=nodeid, location=location) + self.config.hook.pytest_runtest_logfinish(nodeid=nodeid, location=location) def worker_testreport(self, node, rep): """Emitted when a node calls the pytest_runtest_logreport hook.""" @@ -295,8 +293,7 @@ class DSession(object): if rep.failed: self.countfailures += 1 if self.maxfail and self.countfailures >= self.maxfail: - self.shouldstop = "stopping after %d failures" % ( - self.countfailures) + self.shouldstop = "stopping after %d failures" % (self.countfailures) def triggershutdown(self): self.log("triggering shutdown") @@ -310,8 +307,9 @@ class DSession(object): runner = self.config.pluginmanager.getplugin("runner") fspath = nodeid.split("::")[0] msg = "Worker %r crashed while running %r" % (worker.gateway.id, nodeid) - rep = runner.TestReport(nodeid, (fspath, None, fspath), - (), "failed", msg, "???") + rep = runner.TestReport( + nodeid, (fspath, None, fspath), (), "failed", msg, "???" + ) rep.node = worker self.config.hook.pytest_runtest_logreport(report=rep) @@ -322,7 +320,7 @@ class TerminalDistReporter(object): self.tr = config.pluginmanager.getplugin("terminalreporter") self._status = {} self._lastlen = 0 - self._isatty = getattr(self.tr, 'isatty', self.tr.hasmarkup) + self._isatty = getattr(self.tr, "isatty", self.tr.hasmarkup) def write_line(self, msg): self.tr.write_line(msg) @@ -337,8 +335,7 @@ class TerminalDistReporter(object): self.rewrite(self.getstatus()) def getstatus(self): - parts = ["%s %s" % (spec.id, self._status[spec.id]) - for spec in self._specs] + parts = ["%s %s" % (spec.id, self._status[spec.id]) for spec in self._specs] return " / ".join(parts) def rewrite(self, line, newline=False): @@ -361,17 +358,17 @@ class TerminalDistReporter(object): if self.config.option.verbose > 0: rinfo = gateway._rinfo() version = "%s.%s.%s" % rinfo.version_info[:3] - self.rewrite("[%s] %s Python %s cwd: %s" % ( - gateway.id, rinfo.platform, version, rinfo.cwd), - newline=True) + self.rewrite( + "[%s] %s Python %s cwd: %s" + % (gateway.id, rinfo.platform, version, rinfo.cwd), + newline=True, + ) self.setstatus(gateway.spec, "C") def pytest_testnodeready(self, node): if self.config.option.verbose > 0: d = node.workerinfo - infoline = "[%s] Python %s" % ( - d['id'], - d['version'].replace('\n', ' -- '),) + infoline = "[%s] Python %s" % (d["id"], d["version"].replace("\n", " -- ")) self.rewrite(infoline, newline=True) self.setstatus(node.gateway.spec, "ok") diff --git a/xdist/looponfail.py b/xdist/looponfail.py index de79592..d49a05b 100644 --- a/xdist/looponfail.py +++ b/xdist/looponfail.py @@ -17,19 +17,22 @@ import execnet def pytest_addoption(parser): group = parser.getgroup("xdist", "distributed and subprocess testing") group._addoption( - '-f', '--looponfail', - action="store_true", dest="looponfail", default=False, + "-f", + "--looponfail", + action="store_true", + dest="looponfail", + default=False, help="run tests in subprocess, wait for modified files " - "and re-run failing test set until all pass.") + "and re-run failing test set until all pass.", + ) def pytest_cmdline_main(config): if config.getoption("looponfail"): - usepdb = config.getoption('usepdb') # a core option + usepdb = config.getoption("usepdb") # a core option if usepdb: - raise pytest.UsageError( - "--pdb incompatible with --looponfail.") + raise pytest.UsageError("--pdb incompatible with --looponfail.") looponfail_main(config) return 2 # looponfail only can get stop with ctrl-C anyway @@ -45,8 +48,8 @@ def looponfail_main(config): # the last failures passed, let's immediately rerun all continue repr_pytest_looponfailinfo( - failreports=remotecontrol.failures, - rootdirs=rootdirs) + failreports=remotecontrol.failures, rootdirs=rootdirs + ) statrecorder.waitonchange(checkinterval=2.0) except KeyboardInterrupt: print() @@ -68,7 +71,7 @@ class RemoteControl(object): def setup(self, out=None): if out is None: out = py.io.TerminalWriter() - if hasattr(self, 'gateway'): + if hasattr(self, "gateway"): raise ValueError("already have gateway %r" % self.gateway) self.trace("setting up worker session") self.gateway = self.initgateway() @@ -82,15 +85,16 @@ class RemoteControl(object): def write(s): out._file.write(s) out._file.flush() + remote_outchannel.setcallback(write) def ensure_teardown(self): - if hasattr(self, 'channel'): + if hasattr(self, "channel"): if not self.channel.isclosed(): self.trace("closing", self.channel) self.channel.close() del self.channel - if hasattr(self, 'gateway'): + if hasattr(self, "gateway"): self.trace("exiting", self.gateway) self.gateway.exit() del self.gateway @@ -138,8 +142,9 @@ def repr_pytest_looponfailinfo(failreports, rootdirs): def init_worker_session(channel, args, option_dict): import os import sys + outchannel = channel.gateway.newchannel() - sys.stdout = sys.stderr = outchannel.makefile('w') + sys.stdout = sys.stderr = outchannel.makefile("w") channel.send(outchannel) # prune sys.path to not contain relative paths newpaths = [] @@ -152,9 +157,11 @@ def init_worker_session(channel, args, option_dict): # fullwidth, hasmarkup = channel.receive() from _pytest.config import Config + config = Config.fromdictargs(option_dict, list(args)) config.args = args from xdist.looponfail import WorkerFailSession + WorkerFailSession(config, channel).main() @@ -181,7 +188,8 @@ class WorkerFailSession(object): except pytest.UsageError: items = session.perform_collect(None) hook.pytest_collection_modifyitems( - session=session, config=session.config, items=items) + session=session, config=session.config, items=items + ) hook.pytest_collection_finish(session=session) return True @@ -207,7 +215,7 @@ class WorkerFailSession(object): for rep in self.recorded_failures: trails.append(rep.nodeid) loc = rep.longrepr - loc = str(getattr(loc, 'reprcrash', loc)) + loc = str(getattr(loc, "reprcrash", loc)) failreports.append(loc) self.channel.send((trails, failreports, self.collection_failed)) @@ -245,8 +253,10 @@ class StatRecorder(object): changed = True else: if oldstat: - if oldstat.mtime != curstat.mtime or \ - oldstat.size != curstat.size: + if ( + oldstat.mtime != curstat.mtime + or oldstat.size != curstat.size + ): changed = True print("# MODIFIED", path) if removepycfiles and path.ext == ".py": diff --git a/xdist/plugin.py b/xdist/plugin.py index 1ad8a63..9c7324c 100644 --- a/xdist/plugin.py +++ b/xdist/plugin.py @@ -8,7 +8,7 @@ def auto_detect_cpus(): try: from os import sched_getaffinity except ImportError: - if os.environ.get('TRAVIS') == 'true': + if os.environ.get("TRAVIS") == "true": # workaround https://bitbucket.org/pypy/pypy/issues/2375 return 2 try: @@ -16,6 +16,7 @@ def auto_detect_cpus(): except ImportError: from multiprocessing import cpu_count else: + def cpu_count(): return len(sched_getaffinity(0)) @@ -27,7 +28,7 @@ def auto_detect_cpus(): def parse_numprocesses(s): - if s == 'auto': + if s == "auto": return auto_detect_cpus() else: return int(s) @@ -36,60 +37,101 @@ def parse_numprocesses(s): def pytest_addoption(parser): group = parser.getgroup("xdist", "distributed and subprocess testing") group._addoption( - '-n', '--numprocesses', dest="numprocesses", metavar="numprocesses", + "-n", + "--numprocesses", + dest="numprocesses", + metavar="numprocesses", action="store", type=parse_numprocesses, help="shortcut for '--dist=load --tx=NUM*popen', " - "you can use 'auto' here for auto detection CPUs number on " - "host system") - group.addoption('--max-worker-restart', '--max-slave-restart', action="store", default=None, - dest="maxworkerrestart", - help="maximum number of workers that can be restarted " - "when crashed (set to zero to disable this feature)\n" - "'--max-slave-restart' option is deprecated and will be removed in " - "a future release") + "you can use 'auto' here for auto detection CPUs number on " + "host system", + ) group.addoption( - '--dist', metavar="distmode", - action="store", choices=['each', 'load', 'loadscope', 'loadfile', 'no'], - dest="dist", default="no", - help=("set mode for distributing tests to exec environments.\n\n" - "each: send each test to all available environments.\n\n" - "load: load balance by sending any pending test to any" - " available environment.\n\n" - "loadscope: load balance by sending pending groups of tests in" - " the same scope to any available environment.\n\n" - "loadfile: load balance by sending test grouped by file" - " to any available environment.\n\n" - "(default) no: run tests inprocess, don't distribute.")) + "--max-worker-restart", + "--max-slave-restart", + action="store", + default=None, + dest="maxworkerrestart", + help="maximum number of workers that can be restarted " + "when crashed (set to zero to disable this feature)\n" + "'--max-slave-restart' option is deprecated and will be removed in " + "a future release", + ) group.addoption( - '--tx', dest="tx", action="append", default=[], + "--dist", + metavar="distmode", + action="store", + choices=["each", "load", "loadscope", "loadfile", "no"], + dest="dist", + default="no", + help=( + "set mode for distributing tests to exec environments.\n\n" + "each: send each test to all available environments.\n\n" + "load: load balance by sending any pending test to any" + " available environment.\n\n" + "loadscope: load balance by sending pending groups of tests in" + " the same scope to any available environment.\n\n" + "loadfile: load balance by sending test grouped by file" + " to any available environment.\n\n" + "(default) no: run tests inprocess, don't distribute." + ), + ) + group.addoption( + "--tx", + dest="tx", + action="append", + default=[], metavar="xspec", - help=("add a test execution environment. some examples: " - "--tx popen//python=python2.5 --tx socket=192.168.1.102:8888 " - "--tx ssh=user@codespeak.net//chdir=testcache")) + help=( + "add a test execution environment. some examples: " + "--tx popen//python=python2.5 --tx socket=192.168.1.102:8888 " + "--tx ssh=user@codespeak.net//chdir=testcache" + ), + ) group._addoption( - '-d', - action="store_true", dest="distload", default=False, - help="load-balance tests. shortcut for '--dist=load'") + "-d", + action="store_true", + dest="distload", + default=False, + help="load-balance tests. shortcut for '--dist=load'", + ) group.addoption( - '--rsyncdir', action="append", default=[], metavar="DIR", - help="add directory for rsyncing to remote tx nodes.") + "--rsyncdir", + action="append", + default=[], + metavar="DIR", + help="add directory for rsyncing to remote tx nodes.", + ) group.addoption( - '--rsyncignore', action="append", default=[], metavar="GLOB", - help="add expression for ignores when rsyncing to remote tx nodes.") + "--rsyncignore", + action="append", + default=[], + metavar="GLOB", + help="add expression for ignores when rsyncing to remote tx nodes.", + ) group.addoption( - "--boxed", action="store_true", - help="backward compatibility alias for pytest-forked --forked") + "--boxed", + action="store_true", + help="backward compatibility alias for pytest-forked --forked", + ) parser.addini( - 'rsyncdirs', 'list of (relative) paths to be rsynced for' - ' remote distributed testing.', type="pathlist") + "rsyncdirs", + "list of (relative) paths to be rsynced for" " remote distributed testing.", + type="pathlist", + ) parser.addini( - 'rsyncignore', 'list of (relative) glob-style paths to be ignored ' - 'for rsyncing.', type="pathlist") + "rsyncignore", + "list of (relative) glob-style paths to be ignored " "for rsyncing.", + type="pathlist", + ) parser.addini( - "looponfailroots", type="pathlist", - help="directories to check for changes", default=[py.path.local()]) + "looponfailroots", + type="pathlist", + help="directories to check for changes", + default=[py.path.local()], + ) # ------------------------------------------------------------------------- @@ -99,12 +141,14 @@ def pytest_addoption(parser): def pytest_addhooks(pluginmanager): from xdist import newhooks + # avoid warnings with pytest-2.8 method = getattr(pluginmanager, "add_hookspecs", None) if method is None: method = pluginmanager.addhooks method(newhooks) + # ------------------------------------------------------------------------- # distributed testing initialization # ------------------------------------------------------------------------- @@ -114,6 +158,7 @@ def pytest_addhooks(pluginmanager): def pytest_configure(config): if config.getoption("dist") != "no" and not config.getvalue("collectonly"): from xdist.dsession import DSession + session = DSession(config) config.pluginmanager.register(session, "dsession") tr = config.pluginmanager.getplugin("terminalreporter") @@ -125,18 +170,20 @@ def pytest_configure(config): @pytest.mark.tryfirst def pytest_cmdline_main(config): if config.option.numprocesses: - if config.option.dist == 'no': + if config.option.dist == "no": config.option.dist = "load" - config.option.tx = ['popen'] * config.option.numprocesses + config.option.tx = ["popen"] * config.option.numprocesses if config.option.distload: config.option.dist = "load" val = config.getvalue if not val("collectonly"): - usepdb = config.getoption('usepdb') # a core option + usepdb = config.getoption("usepdb") # a core option if val("dist") != "no": if usepdb: raise pytest.UsageError( - "--pdb is incompatible with distributing tests; try using -n0.") # noqa: E501 + "--pdb is incompatible with distributing tests; try using -n0." + ) # noqa: E501 + # ------------------------------------------------------------------------- # fixtures @@ -148,7 +195,7 @@ def worker_id(request): """Return the id of the current worker ('gw0', 'gw1', etc) or 'master' if running on the master node. """ - if hasattr(request.config, 'workerinput'): - return request.config.workerinput['workerid'] + if hasattr(request.config, "workerinput"): + return request.config.workerinput["workerid"] else: - return 'master' + return "master" diff --git a/xdist/remote.py b/xdist/remote.py index 0c62c6c..5837c00 100644 --- a/xdist/remote.py +++ b/xdist/remote.py @@ -17,7 +17,7 @@ import pytest class WorkerInteractor(object): def __init__(self, config, channel): self.config = config - self.workerid = config.workerinput.get('workerid', "?") + self.workerid = config.workerinput.get("workerid", "?") self.log = py.log.Producer("worker-%s" % self.workerid) if not config.option.debug: py.log.setconsumer(self.log._keywords, None) @@ -39,7 +39,7 @@ class WorkerInteractor(object): @pytest.hookimpl(hookwrapper=True) def pytest_sessionfinish(self, exitstatus): - self.config.workeroutput['exitstatus'] = exitstatus + self.config.workeroutput["exitstatus"] = exitstatus yield self.sendevent("workerfinished", workeroutput=self.config.workeroutput) @@ -56,7 +56,7 @@ class WorkerInteractor(object): return True self.log("received command", name, kwargs) if name == "runtests": - torun.extend(kwargs['indices']) + torun.extend(kwargs["indices"]) elif name == "runtests_all": torun.extend(range(len(session.items))) self.log("items to run:", torun) @@ -79,24 +79,25 @@ class WorkerInteractor(object): nextitem = None start = time.time() - self.config.hook.pytest_runtest_protocol( - item=item, - nextitem=nextitem) + self.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem) duration = time.time() - start - self.sendevent("runtest_protocol_complete", item_index=self.item_index, - duration=duration) + self.sendevent( + "runtest_protocol_complete", item_index=self.item_index, duration=duration + ) def pytest_collection_finish(self, session): self.sendevent( "collectionfinish", topdir=str(session.fspath), - ids=[item.nodeid for item in session.items]) + ids=[item.nodeid for item in session.items], + ) def pytest_runtest_logstart(self, nodeid, location): self.sendevent("logstart", nodeid=nodeid, location=location) # the pytest_runtest_logfinish hook was introduced in pytest 3.4 - if hasattr(_pytest.hookspec, 'pytest_runtest_logfinish'): + if hasattr(_pytest.hookspec, "pytest_runtest_logfinish"): + def pytest_runtest_logfinish(self, nodeid, location): self.sendevent("logfinish", nodeid=nodeid, location=location) @@ -112,8 +113,13 @@ class WorkerInteractor(object): self.sendevent("collectreport", data=data) def pytest_logwarning(self, message, code, nodeid, fslocation): - self.sendevent("logwarning", message=message, code=code, nodeid=nodeid, - fslocation=str(fslocation)) + self.sendevent( + "logwarning", + message=message, + code=code, + nodeid=nodeid, + fslocation=str(fslocation), + ) def serialize_report(rep): @@ -122,34 +128,33 @@ def serialize_report(rep): reprcrash = rep.longrepr.reprcrash.__dict__.copy() new_entries = [] - for entry in reprtraceback['reprentries']: - entry_data = { - 'type': type(entry).__name__, - 'data': entry.__dict__.copy(), - } - for key, value in entry_data['data'].items(): - if hasattr(value, '__dict__'): - entry_data['data'][key] = value.__dict__.copy() + for entry in reprtraceback["reprentries"]: + entry_data = {"type": type(entry).__name__, "data": entry.__dict__.copy()} + for key, value in entry_data["data"].items(): + if hasattr(value, "__dict__"): + entry_data["data"][key] = value.__dict__.copy() new_entries.append(entry_data) - reprtraceback['reprentries'] = new_entries + reprtraceback["reprentries"] = new_entries return { - 'reprcrash': reprcrash, - 'reprtraceback': reprtraceback, - 'sections': rep.longrepr.sections + "reprcrash": reprcrash, + "reprtraceback": reprtraceback, + "sections": rep.longrepr.sections, } import py + d = rep.__dict__.copy() - if hasattr(rep.longrepr, 'toterminal'): - if hasattr(rep.longrepr, 'reprtraceback') \ - and hasattr(rep.longrepr, 'reprcrash'): - d['longrepr'] = disassembled_report(rep) + if hasattr(rep.longrepr, "toterminal"): + if hasattr(rep.longrepr, "reprtraceback") and hasattr( + rep.longrepr, "reprcrash" + ): + d["longrepr"] = disassembled_report(rep) else: - d['longrepr'] = str(rep.longrepr) + d["longrepr"] = str(rep.longrepr) else: - d['longrepr'] = rep.longrepr + d["longrepr"] = rep.longrepr for name in d: if isinstance(d[name], py.path.local): d[name] = str(d[name]) @@ -160,6 +165,7 @@ def serialize_report(rep): def getinfodict(): import platform + return dict( version=sys.version, version_info=tuple(sys.version_info), @@ -172,7 +178,8 @@ def getinfodict(): def remote_initconfig(option_dict, args): from _pytest.config import Config - option_dict['plugins'].append("no:terminal") + + option_dict["plugins"].append("no:terminal") config = Config.fromdictargs(option_dict, args) config.option.looponfail = False config.option.usepdb = False @@ -183,18 +190,19 @@ def remote_initconfig(option_dict, args): return config -if __name__ == '__channelexec__': +if __name__ == "__channelexec__": channel = channel # noqa workerinput, args, option_dict = channel.receive() importpath = os.getcwd() sys.path.insert(0, importpath) # XXX only for remote situations - os.environ['PYTHONPATH'] = ( - importpath + os.pathsep + - os.environ.get('PYTHONPATH', '')) - os.environ['PYTEST_XDIST_WORKER'] = workerinput['workerid'] - os.environ['PYTEST_XDIST_WORKER_COUNT'] = str(workerinput['workercount']) + os.environ["PYTHONPATH"] = ( + importpath + os.pathsep + os.environ.get("PYTHONPATH", "") + ) + os.environ["PYTEST_XDIST_WORKER"] = workerinput["workerid"] + os.environ["PYTEST_XDIST_WORKER_COUNT"] = str(workerinput["workercount"]) # os.environ['PYTHONPATH'] = importpath import py + config = remote_initconfig(option_dict, args) config.workerinput = workerinput config.workeroutput = {} diff --git a/xdist/report.py b/xdist/report.py index e5a844b..3d239f1 100644 --- a/xdist/report.py +++ b/xdist/report.py @@ -11,16 +11,11 @@ def report_collection_diff(from_collection, to_collection, from_id, to_id): if from_collection == to_collection: return None - diff = unified_diff( - from_collection, - to_collection, - fromfile=from_id, - tofile=to_id, - ) + diff = unified_diff(from_collection, to_collection, fromfile=from_id, tofile=to_id) error_message = ( - u'Different tests were collected between {from_id} and {to_id}. ' - u'The difference is:\n' - u'{diff}' - ).format(from_id=from_id, to_id=to_id, diff='\n'.join(diff)) + u"Different tests were collected between {from_id} and {to_id}. " + u"The difference is:\n" + u"{diff}" + ).format(from_id=from_id, to_id=to_id, diff="\n".join(diff)) msg = "\n".join([x.rstrip() for x in error_message.split("\n")]) return msg diff --git a/xdist/scheduler/each.py b/xdist/scheduler/each.py index 42bc356..250421f 100644 --- a/xdist/scheduler/each.py +++ b/xdist/scheduler/each.py @@ -86,10 +86,12 @@ class EachScheduling(object): if deadnode.gateway.spec == node.gateway.spec: dead_collection = self.node2collection[deadnode] if collection != dead_collection: - msg = report_collection_diff(dead_collection, - collection, - deadnode.gateway.id, - node.gateway.id) + msg = report_collection_diff( + dead_collection, + collection, + deadnode.gateway.id, + node.gateway.id, + ) self.log(msg) return pending = self._removed2pending.pop(deadnode) diff --git a/xdist/scheduler/filescope.py b/xdist/scheduler/filescope.py index b6c0301..4bacde0 100644 --- a/xdist/scheduler/filescope.py +++ b/xdist/scheduler/filescope.py @@ -23,7 +23,7 @@ class LoadFileScheduling(LoadScopeScheduling): def __init__(self, config, log=None): super(LoadFileScheduling, self).__init__(config, log) if log is None: - self.log = Producer('loadfilesched') + self.log = Producer("loadfilesched") else: self.log = log.loadfilesched @@ -49,4 +49,4 @@ class LoadFileScheduling(LoadScopeScheduling): example/loadsuite/test/test_delta.py example/loadsuite/epsilon/__init__.py """ - return nodeid.split('::', 1)[0] + return nodeid.split("::", 1)[0] diff --git a/xdist/scheduler/load.py b/xdist/scheduler/load.py index eb90bc7..dae43ee 100644 --- a/xdist/scheduler/load.py +++ b/xdist/scheduler/load.py @@ -133,10 +133,9 @@ class LoadScheduling(object): assert self.collection if collection != self.collection: other_node = next(iter(self.node2collection.keys())) - msg = report_collection_diff(self.collection, - collection, - other_node.gateway.id, - node.gateway.id) + msg = report_collection_diff( + self.collection, collection, other_node.gateway.id, node.gateway.id + ) self.log(msg) return self.node2collection[node] = list(collection) @@ -226,7 +225,7 @@ class LoadScheduling(object): # XXX allow nodes to have different collections if not self._check_nodes_have_same_collection(): - self.log('**Different tests collected, aborting run**') + self.log("**Different tests collected, aborting run**") return # Collections are identical, create the index of pending items. @@ -238,8 +237,7 @@ class LoadScheduling(object): # 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)) + 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) @@ -271,18 +269,15 @@ class LoadScheduling(object): same_collection = True for node, collection in node_collection_items[1:]: msg = report_collection_diff( - col, - collection, - first_node.gateway.id, - node.gateway.id, + col, collection, first_node.gateway.id, node.gateway.id ) if msg: same_collection = False self.log(msg) if self.config is not None: rep = CollectReport( - node.gateway.id, 'failed', - longrepr=msg, result=[]) + node.gateway.id, "failed", longrepr=msg, result=[] + ) self.config.hook.pytest_collectreport(report=rep) return same_collection diff --git a/xdist/scheduler/loadscope.py b/xdist/scheduler/loadscope.py index 3301133..eb3b23b 100644 --- a/xdist/scheduler/loadscope.py +++ b/xdist/scheduler/loadscope.py @@ -93,7 +93,7 @@ class LoadScopeScheduling(object): self.registered_collections = OrderedDict() if log is None: - self.log = Producer('loadscopesched') + self.log = Producer("loadscopesched") else: self.log = log.loadscopesched @@ -187,8 +187,7 @@ class LoadScopeScheduling(object): break else: raise RuntimeError( - 'Unable to identify crashitem on a workload with ' - 'pending items' + "Unable to identify crashitem on a workload with " "pending items" ) # Made uncompleted work unit available again @@ -224,10 +223,7 @@ class LoadScopeScheduling(object): other_node = next(iter(self.registered_collections.keys())) msg = report_collection_diff( - self.collection, - collection, - other_node.gateway.id, - node.gateway.id + self.collection, collection, other_node.gateway.id, node.gateway.id ) self.log(msg) return @@ -255,9 +251,7 @@ class LoadScopeScheduling(object): scope, work_unit = self.workqueue.popitem(last=False) # Keep track of the assigned work - assigned_to_node = self.assigned_work.setdefault( - node, default=OrderedDict() - ) + assigned_to_node = self.assigned_work.setdefault(node, default=OrderedDict()) assigned_to_node[scope] = work_unit # Ask the node to execute the workload @@ -292,14 +286,11 @@ class LoadScopeScheduling(object): example/loadsuite/test/test_delta.py::Delta1 example/loadsuite/epsilon/__init__.py """ - return nodeid.rsplit('::', 1)[0] + return nodeid.rsplit("::", 1)[0] def _pending_of(self, workload): """Return the number of pending tests in a workload.""" - pending = sum( - list(scope.values()).count(False) - for scope in workload.values() - ) + pending = sum(list(scope.values()).count(False) for scope in workload.values()) return pending def _reschedule(self, node): @@ -317,7 +308,7 @@ class LoadScopeScheduling(object): if not self.workqueue: return - self.log('Number of units waiting for node:', len(self.workqueue)) + self.log("Number of units waiting for node:", len(self.workqueue)) # Check that the node is almost depleted of work # 2: Heuristic of minimum tests to enqueue more work @@ -348,13 +339,11 @@ class LoadScopeScheduling(object): # Check that all nodes collected the same tests if not self._check_nodes_have_same_collection(): - self.log('**Different tests collected, aborting run**') + self.log("**Different tests collected, aborting run**") return # Collections are identical, create the final list of items - self.collection = list( - next(iter(self.registered_collections.values())) - ) + self.collection = list(next(iter(self.registered_collections.values()))) if not self.collection: return @@ -368,12 +357,12 @@ class LoadScopeScheduling(object): extra_nodes = len(self.nodes) - len(self.workqueue) if extra_nodes > 0: - self.log('Shuting down {0} nodes'.format(extra_nodes)) + self.log("Shuting down {0} nodes".format(extra_nodes)) for _ in range(extra_nodes): unused_node, assigned = self.assigned_work.popitem(last=True) - self.log('Shuting down unused node {0}'.format(unused_node)) + self.log("Shuting down unused node {0}".format(unused_node)) unused_node.shutdown() # Assign initial workload @@ -402,10 +391,7 @@ class LoadScopeScheduling(object): for node, collection in node_collection_items[1:]: msg = report_collection_diff( - col, - collection, - first_node.gateway.id, - node.gateway.id, + col, collection, first_node.gateway.id, node.gateway.id ) if not msg: continue @@ -416,12 +402,7 @@ class LoadScopeScheduling(object): if self.config is None: continue - rep = CollectReport( - node.gateway.id, - 'failed', - longrepr=msg, - result=[] - ) + rep = CollectReport(node.gateway.id, "failed", longrepr=msg, result=[]) self.config.hook.pytest_collectreport(report=rep) return same_collection diff --git a/xdist/workermanage.py b/xdist/workermanage.py index da7ecdf..54b6e2d 100644 --- a/xdist/workermanage.py +++ b/xdist/workermanage.py @@ -22,16 +22,17 @@ def parse_spec_config(config): except ValueError: xspeclist.append(xspec) else: - xspeclist.extend([xspec[i + 1:]] * num) + xspeclist.extend([xspec[i + 1 :]] * num) if not xspeclist: raise pytest.UsageError( - "MISSING test execution (tx) nodes: please specify --tx") + "MISSING test execution (tx) nodes: please specify --tx" + ) return xspeclist class NodeManager(object): EXIT_TIMEOUT = 10 - DEFAULT_IGNORES = ['.*', '*.pyc', '*.pyo', '*~'] + DEFAULT_IGNORES = [".*", "*.pyc", "*.pyo", "*~"] def __init__(self, config, specs=None, defaultchdir="pyexecnetcache"): self.config = config @@ -59,8 +60,7 @@ class NodeManager(object): self.rsync(gateway, root, **self.rsyncoptions) def setup_nodes(self, putevent): - self.config.hook.pytest_xdist_setupnodes(config=self.config, - specs=self.specs) + self.config.hook.pytest_xdist_setupnodes(config=self.config, specs=self.specs) self.trace("setting up nodes") nodes = [] for spec in self.specs: @@ -72,7 +72,7 @@ class NodeManager(object): self.config.hook.pytest_xdist_newgateway(gateway=gw) self.rsync_roots(gw) node = WorkerController(self, gw, self.config, putevent) - gw.node = node # keep the node alive + gw.node = node # keep the node alive node.setup() self.trace("started node %r" % node) return node @@ -91,6 +91,7 @@ class NodeManager(object): return [] import pytest import _pytest + pytestpath = pytest.__file__.rstrip("co") pytestdir = py.path.local(_pytest.__file__).dirpath() config = self.config @@ -114,10 +115,7 @@ class NodeManager(object): ignores += self.config.option.rsyncignore ignores += self.config.getini("rsyncignore") - return { - 'ignores': ignores, - 'verbose': self.config.option.verbose, - } + return {"ignores": ignores, "verbose": self.config.option.verbose} def rsync(self, gateway, source, notify=None, verbose=False, ignores=None): """Perform rsync to remote hosts for node.""" @@ -129,9 +127,12 @@ class NodeManager(object): if spec.popen and not spec.chdir: # XXX This assumes that sources are python-packages # and that adding the basedir does not hurt. - gateway.remote_exec(""" + gateway.remote_exec( + """ import sys ; sys.path.insert(0, %r) - """ % os.path.dirname(str(source))).waitclose() + """ + % os.path.dirname(str(source)) + ).waitclose() return if (spec, source) in self._rsynced_specs: return @@ -139,28 +140,24 @@ class NodeManager(object): def finished(): if notify: notify("rsyncrootready", spec, source) + rsync.add_target_host(gateway, finished=finished) self._rsynced_specs.add((spec, source)) - self.config.hook.pytest_xdist_rsyncstart( - source=source, - gateways=[gateway], - ) + self.config.hook.pytest_xdist_rsyncstart(source=source, gateways=[gateway]) rsync.send() - self.config.hook.pytest_xdist_rsyncfinish( - source=source, - gateways=[gateway], - ) + self.config.hook.pytest_xdist_rsyncfinish(source=source, gateways=[gateway]) class HostRSync(execnet.RSync): """ RSyncer that filters out common files """ + def __init__(self, sourcedir, *args, **kwargs): self._synced = {} self._ignores = [] - ignores = kwargs.pop('ignores', None) or [] + ignores = kwargs.pop("ignores", None) or [] for x in ignores: - x = getattr(x, 'strpath', x) + x = getattr(x, "strpath", x) self._ignores.append(re.compile(fnmatch.translate(x))) super(HostRSync, self).__init__(sourcedir=sourcedir, **kwargs) @@ -174,15 +171,15 @@ class HostRSync(execnet.RSync): def add_target_host(self, gateway, finished=None): remotepath = os.path.basename(self._sourcedir) - super(HostRSync, self).add_target(gateway, remotepath, - finishedcallback=finished, - delete=True,) + super(HostRSync, self).add_target( + gateway, remotepath, finishedcallback=finished, delete=True + ) def _report_send_file(self, gateway, modified_rel_path): if self._verbose: path = os.path.basename(self._sourcedir) + "/" + modified_rel_path remotepath = gateway.spec.chdir - print('%s:%s <= %s' % (gateway.spec, remotepath, path)) + print("%s:%s <= %s" % (gateway.spec, remotepath, path)) def make_reltoroot(roots, args): @@ -211,11 +208,12 @@ class WorkerController(object): self.putevent = putevent self.gateway = gateway self.config = config - self.workerinput = {'workerid': gateway.id, - 'workercount': len(nodemanager.specs), - 'slaveid': gateway.id, - 'slavecount': len(nodemanager.specs) - } + self.workerinput = { + "workerid": gateway.id, + "workercount": len(nodemanager.specs), + "slaveid": gateway.id, + "slavecount": len(nodemanager.specs), + } # TODO: deprecated name, backward compatibility only. Remove it in future self.slaveinput = self.workerinput self._down = False @@ -225,7 +223,7 @@ class WorkerController(object): py.log.setconsumer(self.log._keywords, None) 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): @@ -240,24 +238,22 @@ class WorkerController(object): option_dict = vars(self.config.option) if spec.popen: name = "popen-%s" % self.gateway.id - if hasattr(self.config, '_tmpdirhandler'): + if hasattr(self.config, "_tmpdirhandler"): basetemp = self.config._tmpdirhandler.getbasetemp() - option_dict['basetemp'] = str(basetemp.join(name)) + 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.workerinput, args, option_dict)) if self.putevent: - self.channel.setcallback( - self.process_from_remote, - endmarker=self.ENDMARK) + self.channel.setcallback(self.process_from_remote, endmarker=self.ENDMARK) def ensure_teardown(self): - if hasattr(self, 'channel'): + if hasattr(self, "channel"): if not self.channel.isclosed(): self.log("closing", self.channel) self.channel.close() # del self.channel - if hasattr(self, 'gateway'): + if hasattr(self, "gateway"): self.log("exiting", self.gateway) self.gateway.exit() # del self.gateway @@ -266,7 +262,7 @@ class WorkerController(object): self.sendcommand("runtests", indices=indices) def send_runtest_all(self): - self.sendcommand("runtests_all",) + self.sendcommand("runtests_all") def shutdown(self): if not self._down: @@ -309,25 +305,28 @@ class WorkerController(object): self.notify_inproc(eventname, node=self, **kwargs) elif eventname == "workerfinished": self._down = True - self.workeroutput = self.slaveoutput = kwargs['workeroutput'] + self.workeroutput = self.slaveoutput = kwargs["workeroutput"] self.notify_inproc("workerfinished", node=self) elif eventname in ("logstart", "logfinish"): self.notify_inproc(eventname, node=self, **kwargs) - elif eventname in ( - "testreport", "collectreport", "teardownreport"): + elif eventname in ("testreport", "collectreport", "teardownreport"): item_index = kwargs.pop("item_index", None) - rep = unserialize_report(eventname, kwargs['data']) + rep = unserialize_report(eventname, kwargs["data"]) if item_index is not None: rep.item_index = item_index self.notify_inproc(eventname, node=self, rep=rep) elif eventname == "collectionfinish": - self.notify_inproc(eventname, node=self, ids=kwargs['ids']) + self.notify_inproc(eventname, node=self, ids=kwargs["ids"]) elif eventname == "runtest_protocol_complete": self.notify_inproc(eventname, node=self, **kwargs) elif eventname == "logwarning": - self.notify_inproc(eventname, message=kwargs['message'], - code=kwargs['code'], nodeid=kwargs['nodeid'], - fslocation=kwargs['nodeid']) + self.notify_inproc( + eventname, + message=kwargs["message"], + code=kwargs["code"], + nodeid=kwargs["nodeid"], + fslocation=kwargs["nodeid"], + ) else: raise ValueError("unknown event: %s" % (eventname,)) except KeyboardInterrupt: @@ -335,6 +334,7 @@ class WorkerController(object): raise except: # noqa from _pytest._code import ExceptionInfo + excinfo = ExceptionInfo() print("!" * 20, excinfo) self.config.notify_exception(excinfo) @@ -351,56 +351,56 @@ def unserialize_report(name, reportdict): ReprFileLocation, ReprFuncArgs, ReprLocals, - ReprTraceback + ReprTraceback, ) - if reportdict['longrepr']: - if 'reprcrash' in reportdict['longrepr'] and 'reprtraceback' in reportdict['longrepr']: - reprtraceback = reportdict['longrepr']['reprtraceback'] - reprcrash = reportdict['longrepr']['reprcrash'] + if reportdict["longrepr"]: + if ( + "reprcrash" in reportdict["longrepr"] + and "reprtraceback" in reportdict["longrepr"] + ): + + reprtraceback = reportdict["longrepr"]["reprtraceback"] + reprcrash = reportdict["longrepr"]["reprcrash"] unserialized_entries = [] reprentry = None - for entry_data in reprtraceback['reprentries']: - data = entry_data['data'] - entry_type = entry_data['type'] - if entry_type == 'ReprEntry': + for entry_data in reprtraceback["reprentries"]: + data = entry_data["data"] + entry_type = entry_data["type"] + if entry_type == "ReprEntry": reprfuncargs = None reprfileloc = None reprlocals = None - if data['reprfuncargs']: - reprfuncargs = ReprFuncArgs( - **data['reprfuncargs']) - if data['reprfileloc']: - reprfileloc = ReprFileLocation( - **data['reprfileloc']) - if data['reprlocals']: - reprlocals = ReprLocals( - data['reprlocals']['lines']) + if data["reprfuncargs"]: + reprfuncargs = ReprFuncArgs(**data["reprfuncargs"]) + if data["reprfileloc"]: + reprfileloc = ReprFileLocation(**data["reprfileloc"]) + if data["reprlocals"]: + reprlocals = ReprLocals(data["reprlocals"]["lines"]) reprentry = ReprEntry( - lines=data['lines'], + lines=data["lines"], reprfuncargs=reprfuncargs, reprlocals=reprlocals, filelocrepr=reprfileloc, - style=data['style'] + style=data["style"], ) - elif entry_type == 'ReprEntryNative': - reprentry = ReprEntryNative(data['lines']) + elif entry_type == "ReprEntryNative": + reprentry = ReprEntryNative(data["lines"]) else: - report_unserialization_failure( - entry_type, name, reportdict) + report_unserialization_failure(entry_type, name, reportdict) unserialized_entries.append(reprentry) - reprtraceback['reprentries'] = unserialized_entries + reprtraceback["reprentries"] = unserialized_entries exception_info = ReprExceptionInfo( reprtraceback=ReprTraceback(**reprtraceback), reprcrash=ReprFileLocation(**reprcrash), ) - for section in reportdict['longrepr']['sections']: + for section in reportdict["longrepr"]["sections"]: exception_info.addsection(*section) - reportdict['longrepr'] = exception_info + reportdict["longrepr"] = exception_info return reportdict if name == "testreport": @@ -411,13 +411,13 @@ def unserialize_report(name, reportdict): def report_unserialization_failure(type_name, report_name, reportdict): from pprint import pprint - url = 'https://github.com/pytest-dev/pytest-xdist/issues' + + url = "https://github.com/pytest-dev/pytest-xdist/issues" stream = py.io.TextIO() - pprint('-' * 100, stream=stream) - pprint('INTERNALERROR: Unknown entry type returned: %s' % type_name, - stream=stream) - pprint('report_name: %s' % report_name, stream=stream) + pprint("-" * 100, stream=stream) + pprint("INTERNALERROR: Unknown entry type returned: %s" % type_name, stream=stream) + pprint("report_name: %s" % report_name, stream=stream) pprint(reportdict, stream=stream) - pprint('Please report this bug at %s' % url, stream=stream) - pprint('-' * 100, stream=stream) + pprint("Please report this bug at %s" % url, stream=stream) + pprint("-" * 100, stream=stream) assert 0, stream.getvalue() From da7ab1103c9b969f0d56bc448fa6bf9f2325c976 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 27 Jul 2018 17:52:22 -0300 Subject: [PATCH 3/7] Add badge for black --- README.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.rst b/README.rst index f4da32c..a863421 100644 --- a/README.rst +++ b/README.rst @@ -19,6 +19,9 @@ :alt: AppVeyor build status :target: https://ci.appveyor.com/project/pytestbot/pytest-xdist +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/ambv/black + xdist: pytest distributed testing plugin ======================================== From 9e14e78d078c18cd2d8ed63e757ed0db29bf9b17 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 27 Jul 2018 17:57:09 -0300 Subject: [PATCH 4/7] Add show-diff-on-failures to linting call --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 4532176..2f2b19c 100644 --- a/tox.ini +++ b/tox.ini @@ -31,7 +31,7 @@ commands= changedir= deps = pre-commit -commands = pre-commit run --all +commands = pre-commit run --all-files --show-diff-on-failure [testenv:release] changedir= From 7ec8e4310c43f29dade8e2d99e7b7a9848004c64 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 27 Jul 2018 18:15:58 -0300 Subject: [PATCH 5/7] Fix line ending issues --- .github/PULL_REQUEST_TEMPLATE.md | 2 -- LICENSE | 1 - OVERVIEW.md | 2 -- tox.ini | 2 -- 4 files changed, 7 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 234f6e7..c00a097 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -13,5 +13,3 @@ Here's a quick checklist that should be present in PRs: ``` Fix issue with non-ascii contents in doctest text files. ``` - - diff --git a/LICENSE b/LICENSE index 8a30978..ff33b8f 100644 --- a/LICENSE +++ b/LICENSE @@ -16,4 +16,3 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - diff --git a/OVERVIEW.md b/OVERVIEW.md index 45a20d6..87b9d1b 100644 --- a/OVERVIEW.md +++ b/OVERVIEW.md @@ -74,5 +74,3 @@ serialize collected items to send them through the wire, as workers live in anot 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. - - diff --git a/tox.ini b/tox.ini index 2f2b19c..f7a1c48 100644 --- a/tox.ini +++ b/tox.ini @@ -51,5 +51,3 @@ addopts = -rsfxX [flake8] max-line-length = 120 ignore = E203,W503 - - From f333588b6aef3d38dfd596e1fc375e46bcb07f98 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 27 Jul 2018 18:18:03 -0300 Subject: [PATCH 6/7] Skip sdist and usedevelop for linting --- tox.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index f7a1c48..f1273da 100644 --- a/tox.ini +++ b/tox.ini @@ -28,7 +28,8 @@ commands= pytest {posargs} [testenv:linting] -changedir= +skipsdist = True +usedevelop = True deps = pre-commit commands = pre-commit run --all-files --show-diff-on-failure From 9f9eee6b749580d5affcbd4c3fe4b8d831ac2ff2 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 27 Jul 2018 18:42:06 -0300 Subject: [PATCH 7/7] Remove flakes and readme from appveyor and travis --- .travis.yml | 4 ---- appveyor.yml | 2 -- 2 files changed, 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9f142f6..0a069cc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -46,10 +46,6 @@ jobs: env: TOXENV=py36-pytestmaster - python: "3.6" env: TOXENV=py36-pytestfeatures - - python: "3.6" - env: TOXENV=flakes - - python: "3.6" - env: TOXENV=readme - stage: deploy python: '3.6' diff --git a/appveyor.yml b/appveyor.yml index c42501d..7e8ef39 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -6,8 +6,6 @@ environment: - TOXENV: "py36-pytest36" - TOXENV: "py27-pytest36-pexpect" - TOXENV: "py36-pytest36-pexpect" - - TOXENV: "flakes" - - TOXENV: "readme" install: - C:\Python36\python -m pip install -U tox setuptools_scm pip