Skip to content

Commit

Permalink
Merge branch 'master' of github.com:pytest-dev/pytest-cov into 260-co…
Browse files Browse the repository at this point in the history
…v-fail-under-float
  • Loading branch information
graingert committed Aug 29, 2019
2 parents cc56caa + 746d750 commit c6c53c0
Show file tree
Hide file tree
Showing 13 changed files with 169 additions and 112 deletions.
58 changes: 30 additions & 28 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,62 +17,64 @@ jobs:
- env: TOXENV=docs

- stage: tests
env: TOXENV=py27-t310-c45
env: TOXENV=py27-pytest310-xdist27-coverage45
python: '2.7'
- env: TOXENV=py27-t43-c45
- env: TOXENV=py27-pytest43-xdist27-coverage45
python: '2.7'
- env: TOXENV=py27-t44-c45
- env: TOXENV=py27-pytest44-xdist28-coverage45
python: '2.7'
- env: TOXENV=py27-t45-c45
- env: TOXENV=py27-pytest45-xdist28-coverage45
python: '2.7'
- env: TOXENV=py34-t310-c45
- env: TOXENV=py34-pytest310-xdist27-coverage45
python: '3.4'
- env: TOXENV=py34-t43-c45
- env: TOXENV=py34-pytest43-xdist27-coverage45
python: '3.4'
- env: TOXENV=py34-t44-c45
- env: TOXENV=py34-pytest44-xdist28-coverage45
python: '3.4'
- env: TOXENV=py34-t45-c45
- env: TOXENV=py34-pytest45-xdist28-coverage45
python: '3.4'
- env: TOXENV=py35-t310-c45
- env: TOXENV=py35-pytest310-xdist27-coverage45
python: '3.5'
- env: TOXENV=py35-t43-c45
- env: TOXENV=py35-pytest43-xdist27-coverage45
python: '3.5'
- env: TOXENV=py35-t44-c45
- env: TOXENV=py35-pytest44-xdist28-coverage45
python: '3.5'
- env: TOXENV=py35-t45-c45
- env: TOXENV=py35-pytest45-xdist28-coverage45
python: '3.5'
- env: TOXENV=py36-t310-c45
- env: TOXENV=py36-pytest310-xdist27-coverage45
python: '3.6'
- env: TOXENV=py36-t43-c45
- env: TOXENV=py36-pytest43-xdist27-coverage45
python: '3.6'
- env: TOXENV=py36-t44-c45
- env: TOXENV=py36-pytest44-xdist28-coverage45
python: '3.6'
- env: TOXENV=py36-t45-c45
- env: TOXENV=py36-pytest45-xdist28-coverage45
python: '3.6'
- env: TOXENV=py37-t310-c45
- env: TOXENV=py37-pytest310-xdist27-coverage45
python: '3.7'
- env: TOXENV=py37-t43-c45
- env: TOXENV=py37-pytest43-xdist27-coverage45
python: '3.7'
- env: TOXENV=py37-t44-c45
- env: TOXENV=py37-pytest44-xdist28-coverage45
python: '3.7'
- env: TOXENV=py37-t45-c45
- env: TOXENV=py37-pytest45-xdist28-coverage45
python: '3.7'
- env: TOXENV=pypy-t310-c45
- env: TOXENV=pypy-pytest310-xdist27-coverage45
python: 'pypy2.7-6.0'
- env: TOXENV=pypy-t43-c45
- env: TOXENV=pypy-pytest43-xdist27-coverage45
python: 'pypy2.7-6.0'
- env: TOXENV=pypy-t44-c45
- env: TOXENV=pypy-pytest44-xdist28-coverage45
python: 'pypy2.7-6.0'
- env: TOXENV=pypy-t45-c45
- env: TOXENV=pypy-pytest45-xdist28-coverage45
python: 'pypy2.7-6.0'
- env: TOXENV=pypy3-t310-c45
- env: TOXENV=pypy3-pytest310-xdist27-coverage45
python: 'pypy3.5-6.0'
- env: TOXENV=pypy3-t43-c45
- env: TOXENV=pypy3-pytest43-xdist27-coverage45
python: 'pypy3.5-6.0'
- env: TOXENV=pypy3-t44-c45
- env: TOXENV=pypy3-pytest44-xdist28-coverage45
python: 'pypy3.5-6.0'
- env: TOXENV=pypy3-t45-c45
- env: TOXENV=pypy3-pytest45-xdist28-coverage45
python: 'pypy3.5-6.0'
- env: TOXENV=py37-pytest310-xdist22-coverage45
python: '3.7'

- stage: examples
python: '3.6'
Expand Down
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ Authors
* Samuel Giffard - https://github.com/Mulugruntz
* Семён Марьясин - https://github.com/MarSoft
* Alexander Shadchin - https://github.com/shadchin
* Thomas Grainger - https://graingert.co.uk
6 changes: 6 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
Changelog
=========

2.7.2.dev0 (unreleased)
-----------------------

* Match pytest-xdist master/worker terminology.
Contributed in `#321 <https://github.com/pytest-dev/pytest-cov/pull/321>`_

2.7.1 (2019-05-03)
------------------

Expand Down
4 changes: 2 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,9 @@ examine it.
Limitations
===========

For distributed testing the slaves must have the pytest-cov package installed. This is needed since
For distributed testing the workers must have the pytest-cov package installed. This is needed since
the plugin must be registered through setuptools for pytest to start the plugin on the
slave.
worker.

For subprocess measurement environment variables must make it from the main process to the
subprocess. The python used by the subprocess must have pytest-cov installed. The subprocess must
Expand Down
11 changes: 7 additions & 4 deletions appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ cache:
environment:
matrix:
- TOXENV: check
- TOXENV: 'py27-t310-c45,py27-t43-c45,py27-t44-c45,py27-t45-c45'
- TOXENV: 'py34-t310-c45,py34-t43-c45,py34-t44-c45,py34-t45-c45'
- TOXENV: 'py35-t310-c45,py35-t43-c45,py35-t44-c45,py35-t45-c45'
- TOXENV: 'pypy-t310-c45,pypy-t43-c45,pypy-t44-c45,pypy-t45-c45'
- TOXENV: 'py27-pytest310-xdist27-coverage45,py27-pytest43-xdist27-coverage45,py27-pytest44-xdist28-coverage45,py27-pytest45-xdist28-coverage45'
- TOXENV: 'py34-pytest310-xdist27-coverage45,py34-pytest43-xdist27-coverage45,py34-pytest44-xdist28-coverage45,py34-pytest45-xdist28-coverage45'
- TOXENV: 'py35-pytest310-xdist27-coverage45,py35-pytest43-xdist27-coverage45,py35-pytest44-xdist28-coverage45,py35-pytest45-xdist28-coverage45'
- TOXENV: 'pypy-pytest310-xdist27-coverage45,pypy-pytest43-xdist27-coverage45,pypy-pytest44-xdist28-coverage45,pypy-pytest45-xdist28-coverage45'

init:
- ps: echo $env:TOXENV
Expand All @@ -23,6 +23,9 @@ install:
- echo PyPy installed
- pypy --version

# Upgrade virtualenv for e.g. more-itertools to be handled properly.
# Pin it to work around https://github.com/tox-dev/tox/issues/1389.
- C:\Python35\python -m pip install -U virtualenv==16.5.0
- C:\Python35\python -m pip install tox

test_script:
Expand Down
2 changes: 1 addition & 1 deletion ci/templates/.travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- stage: tests
{% for env in tox_environments %}
{%+ if not loop.first %}- {% else %} {% endif -%}
env: TOXENV={{ env }}{% if 'cover' in env %},report,coveralls,codecov{% endif -%}{{ '' }}
env: TOXENV={{ env }}
{% if env.startswith("pypy-") %}
python: 'pypy2.7-6.0'
{% elif env.startswith("pypy3-") %}
Expand Down
3 changes: 3 additions & 0 deletions ci/templates/appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ install:
- echo PyPy installed
- pypy --version

# Upgrade virtualenv for e.g. more-itertools to be handled properly.
# Pin it to work around https://github.com/tox-dev/tox/issues/1389.
- C:\Python35\python -m pip install -U virtualenv==16.5.0
- C:\Python35\python -m pip install tox

test_script:
Expand Down
10 changes: 5 additions & 5 deletions docs/xdist.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ Distributed testing (xdist)
"load" mode
===========

Distributed testing with dist mode set to load will report on the combined coverage of all slaves.
The slaves may be spread out over any number of hosts and each slave may be located anywhere on the
file system. Each slave will have its subprocesses measured.
Distributed testing with dist mode set to "load" will report on the combined coverage of all workers.
The workers may be spread out over any number of hosts and each worker may be located anywhere on the
file system. Each worker will have its subprocesses measured.

Running distributed testing with dist mode set to load::

Expand Down Expand Up @@ -48,8 +48,8 @@ Shows a terminal report::
"each" mode
===========

Distributed testing with dist mode set to each will report on the combined coverage of all slaves.
Since each slave is running all tests this allows generating a combined coverage report for multiple
Distributed testing with dist mode set to each will report on the combined coverage of all workers.
Since each worker is running all tests this allows generating a combined coverage report for multiple
environments.

Running distributed testing with dist mode set to each::
Expand Down
19 changes: 19 additions & 0 deletions src/pytest_cov/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,22 @@ def testsfailed(self):
@testsfailed.setter
def testsfailed(self, value):
setattr(self._session, self._attr, value)


def _attrgetter(attr):
"""
Return a callable object that fetches attr from its operand.
Unlike operator.attrgetter, the returned callable supports an extra two
arg form for a default.
"""
def fn(obj, *args):
return getattr(obj, attr, *args)

return fn


worker = 'slave' # for compatability with pytest-xdist<=1.22.0
workerid = worker + 'id'
workerinput = _attrgetter(worker + 'input')
workeroutput = _attrgetter(worker + 'output')
77 changes: 41 additions & 36 deletions src/pytest_cov/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from coverage.data import CoverageData

from .embed import cleanup
from .compat import StringIO
from .compat import StringIO, workeroutput, workerinput


class CovController(object):
Expand All @@ -29,7 +29,7 @@ def __init__(self, cov_source, cov_report, cov_config, cov_append, cov_branch, c
self.combining_cov = None
self.data_file = None
self.node_descs = set()
self.failed_slaves = []
self.failed_workers = []
self.topdir = os.getcwd()

def pause(self):
Expand Down Expand Up @@ -131,12 +131,12 @@ def summary(self, stream):
total = self.cov.xml_report(ignore_errors=True, outfile=self.cov_report['xml'])
stream.write('Coverage XML written to file %s\n' % self.cov.config.xml_output)

# Report on any failed slaves.
if self.failed_slaves:
self.sep(stream, '-', 'coverage: failed slaves')
stream.write('The following slaves failed to return coverage data, '
'ensure that pytest-cov is installed on these slaves.\n')
for node in self.failed_slaves:
# Report on any failed workers.
if self.failed_workers:
self.sep(stream, '-', 'coverage: failed workers')
stream.write('The following workers failed to return coverage data, '
'ensure that pytest-cov is installed on these workers.\n')
for node in self.failed_workers:
stream.write('%s\n' % node.gateway.id)

return total
Expand Down Expand Up @@ -205,28 +205,31 @@ def start(self):
self.cov.config.paths['source'] = [self.topdir]

def configure_node(self, node):
"""Slaves need to know if they are collocated and what files have moved."""
"""Workers need to know if they are collocated and what files have moved."""

node.slaveinput['cov_master_host'] = socket.gethostname()
node.slaveinput['cov_master_topdir'] = self.topdir
node.slaveinput['cov_master_rsync_roots'] = [str(root) for root in node.nodemanager.roots]
workerinput(node).update({
'cov_master_host': socket.gethostname(),
'cov_master_topdir': self.topdir,
'cov_master_rsync_roots': [str(root) for root in node.nodemanager.roots],
})

def testnodedown(self, node, error):
"""Collect data file name from slave."""
"""Collect data file name from worker."""

# If slave doesn't return any data then it is likely that this
# plugin didn't get activated on the slave side.
if not (hasattr(node, 'slaveoutput') and 'cov_slave_node_id' in node.slaveoutput):
self.failed_slaves.append(node)
# If worker doesn't return any data then it is likely that this
# plugin didn't get activated on the worker side.
output = workeroutput(node, {})
if 'cov_worker_node_id' not in output:
self.failed_workers.append(node)
return

# If slave is not collocated then we must save the data file
# If worker is not collocated then we must save the data file
# that it returns to us.
if 'cov_slave_data' in node.slaveoutput:
if 'cov_worker_data' in output:
data_suffix = '%s.%s.%06d.%s' % (
socket.gethostname(), os.getpid(),
random.randint(0, 999999),
node.slaveoutput['cov_slave_node_id']
output['cov_worker_node_id']
)

cov = coverage.Coverage(source=self.cov_source,
Expand All @@ -235,14 +238,14 @@ def testnodedown(self, node, error):
config_file=self.cov_config)
cov.start()
data = CoverageData()
data.read_fileobj(StringIO(node.slaveoutput['cov_slave_data']))
data.read_fileobj(StringIO(output['cov_worker_data']))
cov.data.update(data)
cov.stop()
cov.save()
path = node.slaveoutput['cov_slave_path']
path = output['cov_worker_path']
self.cov.config.paths['source'].append(path)

# Record the slave types that contribute to the data file.
# Record the worker types that contribute to the data file.
rinfo = node.gateway._rinfo()
node_desc = self.get_node_desc(rinfo.platform, rinfo.version_info)
self.node_descs.add(node_desc)
Expand All @@ -259,24 +262,24 @@ def finish(self):
self.cov.save()


class DistSlave(CovController):
"""Implementation for distributed slaves."""
class DistWorker(CovController):
"""Implementation for distributed workers."""

def start(self):
cleanup()

# Determine whether we are collocated with master.
self.is_collocated = (socket.gethostname() == self.config.slaveinput['cov_master_host'] and
self.topdir == self.config.slaveinput['cov_master_topdir'])
self.is_collocated = (socket.gethostname() == workerinput(self.config)['cov_master_host'] and
self.topdir == workerinput(self.config)['cov_master_topdir'])

# If we are not collocated then rewrite master paths to slave paths.
# If we are not collocated then rewrite master paths to worker paths.
if not self.is_collocated:
master_topdir = self.config.slaveinput['cov_master_topdir']
slave_topdir = self.topdir
master_topdir = workerinput(self.config)['cov_master_topdir']
worker_topdir = self.topdir
if self.cov_source is not None:
self.cov_source = [source.replace(master_topdir, slave_topdir)
self.cov_source = [source.replace(master_topdir, worker_topdir)
for source in self.cov_source]
self.cov_config = self.cov_config.replace(master_topdir, slave_topdir)
self.cov_config = self.cov_config.replace(master_topdir, worker_topdir)

# Erase any previous data and start coverage.
self.cov = coverage.Coverage(source=self.cov_source,
Expand All @@ -303,7 +306,7 @@ def finish(self):

# If we are collocated then just inform the master of our
# data file to indicate that we have finished.
self.config.slaveoutput['cov_slave_node_id'] = self.nodeid
workeroutput(self.config)['cov_worker_node_id'] = self.nodeid
else:
self.cov.combine()
self.cov.save()
Expand All @@ -312,11 +315,13 @@ def finish(self):
# it on the master node.

# Send all the data to the master over the channel.
self.config.slaveoutput['cov_slave_path'] = self.topdir
self.config.slaveoutput['cov_slave_node_id'] = self.nodeid
buff = StringIO()
self.cov.data.write_fileobj(buff)
self.config.slaveoutput['cov_slave_data'] = buff.getvalue()
workeroutput(self.config).update({
'cov_worker_path': self.topdir,
'cov_worker_node_id': self.nodeid,
'cov_worker_data': buff.getvalue(),
})

def summary(self, stream):
"""Only the master reports so do nothing."""
Expand Down

0 comments on commit c6c53c0

Please sign in to comment.