8

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?

bfrizb
  • 113
  • 1
  • 5
  • 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). –  Apr 14 '16 at 01:46
  • 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](https://drive.google.com/open?id=0B0bHr4crS9cpaWlockpxcmJxelE). 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 Apr 14 '16 at 16:48
  • Strange, Tox virtualenv shouldn't inherit site packages by default. Check Tox [`sitepackages`](http://codespeak.net/tox/config.html#confval-sitepackages=True|False) option, maybe it's set somewhere. Or, maybe, you have some really old virtualenv (IIRC ancient versions required `--no-site-packages` explicitly)? – drdaeman Apr 16 '16 at 01:22
  • 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 Apr 18 '16 at 23:13

2 Answers2

9

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.
sanscore
  • 529
  • 1
  • 4
  • 11
  • 1
    Helpful, however relying on bash isn't helpful. Instead using [`COVERAGE_FILE=.coverage.{envname}`](https://github.com/pallets/itsdangerous/blob/master/tox.ini) you can change `coverage combine {toxworkdir}/py*/.coverage` to just `coverage combine`. – Peilonrayz May 10 '19 at 04:28
  • @Peilonrayz I tried your approach. `coverage combine` when called by tox will always fail with no data found, whereas it "just works" when I call it from the CLi. – user1129682 Feb 25 '21 at 19:35
  • @user1129682 Sorry it has been a while since I've used Tox, and debugging in comments is not great. Do the tests build the desired `.coverage.{envname}` files with Tox? (Sounds like a yes) Do you, or Tox, change the PWD for the combine environment? (you can try running `pwd` in the combine environment and compare what you get with what you expect, then you could try `echo {toxinidir}` to compare) I'd assume `coverage combine {toxinidir}` would work. If the above answer works, don't fix something that's not broke ;) – Peilonrayz Feb 25 '21 at 21:37
  • @Peilonrayz i fixed it. Your were spot on. I thought I could provide a directory to `coverage combine` and it would scan each file and discover coverage data. The truth is, that `coverage` has a pattern, BASED ON THE OUTPUT FILENAME, for accepted files and it simply applys that pattern to the directory. I fixed my output file name and everything clicked into place. – user1129682 Feb 26 '21 at 09:43
1

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.

Ned Batchelder
  • 364,293
  • 75
  • 561
  • 662
  • My understanding is that tox checks the requirements(-dev).txt file against what is already installed on the host machine. If modules are already installed on the host, it doesn't bother installing them into the virtualenv since that would use up more disk space. Take a look at the tox log file (py27-1.log) in the [link](https://drive.google.com/folderview?id=0B0bHr4crS9cpSDd3YjI0Qk8tdjQ&usp=drive_web&tid=0B0bHr4crS9cpaWlockpxcmJxelE) I posted. (continued below...) – bfrizb Apr 18 '16 at 23:03
  • (continued) You'll see it states "Requirement already satisfied (use --upgrade to upgrade): coverage in /Library/Python/2.7/site-packages", which basically means tox said "I see you already have coverage installed on your host, so I won't install it in the virtualenv; I'll just use the coverage from your host instead". This line in the log file changes if I change the requirement to coverage==4.0.2. Then tox says "Hmmm, you have coverage v4.0.3 installed on the host machine, and that doesn't satisfy the having v4.0.2 installed, so I'll go ahead an install coverage v4.0.2 in the virtualenv's." – bfrizb Apr 18 '16 at 23:03