8
votes

Here is a link to a project and output that you can use to reproduce the problem I describe below.

I'm using coverage with tox against multiple versions of python. My tox.ini file looks something like this:

[tox]
envlist =
    py27
    py34

[testenv]
deps =
    coverage

commands =
    coverage run --source=modules/ -m pytest
    coverage report -m

My problem is that coverage will run using only one version of python (in my case, py27), not both py27 and py34. This is a problem whenever I have code execution dependent on the python version, e.g.:

def add(a, b):
    import sys
    if sys.version.startswith('2.7'):
        print('2.7')
    if sys.version.startswith('3'):
        print('3')
    return a + b

Running coverage against the above code will incorrectly report that line 6 ("print('3')") is "Missing" for both py27 and py34. It should only be Missing for py34.

I know why this is happening: coverage is installed on my base OS (which uses python2.7). Thus, when tox is run, it notices that coverage is already installed and inherits coverage from the base OS rather than installing it in the virtualenv it creates.

This is fine and dandy for py27, but causes incorrect results in the coverage report for py34. I have a hacky, temporary work-around: I require a slightly earlier version of coverage (relative to the one installed on my base OS) so that tox will be forced to install a separate copy of coverage in the virtualenv. E.g.

[testenv]
deps =
    coverage==4.0.2
    pytest==2.9.0
    py==1.4.30

I don't like this workaround, but it's the best I've found for now. Any suggestions on a way to force tox to install the current version of coverage in its virtualenv's, even when I already have it installed on my base OS?

2
I can't reproduce this: coverage is telling me it's missing line 8 for the py27 env, and line 6 for the py35 env. I have a global coverage command installed (a Python 3.5 script). The only difference is that I added pytest as an extra dependency, because otherwise I get an InvocationError (I don't have a global pytest command installed).user707650
Thank you for trying. I've uploaded both the code that should be able to reproduce this problem along with output from running tox (and other tools) to help with debugging. This content is located here. I'm running this on OS X 10.11.4. Just run "tox" in the "test_project" directory and (fingers crossed) you should see similar results as mine.bfrizb
Strange, Tox virtualenv shouldn't inherit site packages by default. Check Tox sitepackages option, maybe it's set somewhere. Or, maybe, you have some really old virtualenv (IIRC ancient versions required --no-site-packages explicitly)?drdaeman
I tried adding sitepackages=False to tox.ini, but no luck (makes sense, since False is the default value). I doubt this issue is due to an old virtualenv as I created this project and its virtualenv within the last 2 weeks. In my (albeit limited) experience, I've always seen tox use modules/packages on the host machines, rather than installing a separate copy, if they satisfy requirements for the virtualenv. Can you link to an example project where this is not the case?bfrizb

2 Answers

9
votes

I came upon this problem today, but couldn't find an easy answer. So, for future reference, here is the solution that I came up with.

  1. Create an envlist that contains each version of Python that will be tested and a custom env for cov.
  2. For all versions of Python, set COVERAGE_FILE environment varible to store the .coverage file in {envdir}.
  3. For the cov env I use two commands.
    1. coverage combine that combines the reports, and
    2. coverage html to generate the report and, if necessary, fail the test.
  4. Create a .coveragerc file that contains a [paths] section to lists the source= locations.
    1. The first line is where the actual source code is found.
    2. The subsequent lines are the subpaths that will be eliminated by `coverage combine'.

tox.ini:

[tox]
envlist=py27,py36,py35,py34,py33,cov

[testenv]
deps=
    pytest
    pytest-cov
    pytest-xdist
setenv=
    py{27,36,35,34,33}: COVERAGE_FILE={envdir}/.coverage
commands=
    py{27,36,35,34,33}: python -m pytest --cov=my_project  --cov-report=term-missing --no-cov-on-fail
    cov: /usr/bin/env bash -c '{envpython} -m coverage combine {toxworkdir}/py*/.coverage'
    cov: coverage html --fail-under=85

.coveragerc:

[paths]
source=
    src/
    .tox/py*/lib/python*/site-packages/

The most peculiar part of the configuration is the invocation of coverage combine. Here's a breakdown of the command:

  • tox does not handle Shell expansions {toxworkdir}/py*/.coverage, so we need to invoke a shell (bash -c) to get the necessary expansion.
    • If one were inclined, you could just type out all the paths individually and not jump through all of these hoops, but that would add maintenance and .coverage file dependency for each pyNN env.
  • /usr/bin/env bash -c '...' to ensure we get the correct version of bash. Using the fullpath to env avoids the need for setting whitelist_externals.
  • '{envpython} -m coverage ...' ensures that we invoke the correct python and coverage for the cov env.
  • NOTE: The unfortunate problem of this solution is that the cov env is dependent on the invocation of py{27,36,35,34,33} which has some not so desirable side effects.
    • My suggestion would be to only invoke cov through tox.
    • Never invoke tox -ecov because, either
      • It will likely fail due to a missing .coverage file, or
      • It could give bizarre results (combining differing tests).
    • If you must invoke it as a subset (tox -epy27,py36,cov), then wipe out the .tox directory first (rm -rf .tox) to avoid the missing .coverage file problem.
1
votes

I don't understand why tox wouldn't install coverage in each virtualenv properly. You should get two different coverage reports, one for py27 and one for py35. A nicer option might be to produce one combined report. Use coverage run -p to record separate data for each run, and then coverage combine to combine them before reporting.