Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CoverageScript.command_line wipes out 1st sys.path entry. #715

Closed
jsirois opened this issue Oct 5, 2018 · 23 comments
Closed

CoverageScript.command_line wipes out 1st sys.path entry. #715

jsirois opened this issue Oct 5, 2018 · 23 comments
Labels
bug Something isn't working

Comments

@jsirois
Copy link

jsirois commented Oct 5, 2018

This can be problematic if the coverage distribution itself is not the first item on the classpath:

# We need to be able to import from the current directory, because
# plugins may try to, for example, to read Django settings.
sys.path[0] = ''

Is there any reason this couldn't be a sys.path.insert(0, '')?

I'm happy to do the work but I just wanted to run this by you first.

@nedbat
Copy link
Owner

nedbat commented Oct 5, 2018

I'd feel better about changing this if you could show me a bad behavior that the current code causes.

@jsirois
Copy link
Author

jsirois commented Oct 5, 2018

OK.

CI is here, but its a wall of text: https://travis-ci.org/pantsbuild/pants/jobs/437335398

The relevant bug presentation is configuring coverage with a plugin, that plugin being on the sys.path (in the 1st slot as it turns out) and then coverage mysteriously not being able to find the plugin::

11:20:19 00:02       [coverage-html]
                     pex: PEX.run invoking /home/jsirois/dev/pantsbuild/jsirois-pants/build-support/pants_dev_deps.venv/bin/python2.7 /home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/test/pytest-prep/CPython-2.7.15/bdd83c141bb9cfffde291c49aa9e41781cc24326 html -i --rcfile /home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/test/pytest/testprojects.tests.python.pants.constants_only.constants_only/tmpW10vvZ -d /home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/test/pytest/testprojects.tests.python.pants.constants_only.constants_only/coverage
Traceback (most recent call last):
                       File "/home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/test/pytest-prep/CPython-2.7.15/bdd83c141bb9cfffde291c49aa9e41781cc24326/.bootstrap/_pex/pex.py", line 351, in execute
                         self._wrap_coverage(self._wrap_profiling, self._execute)
                       File "/home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/test/pytest-prep/CPython-2.7.15/bdd83c141bb9cfffde291c49aa9e41781cc24326/.bootstrap/_pex/pex.py", line 281, in _wrap_coverage
                         runner(*args)
                       File "/home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/test/pytest-prep/CPython-2.7.15/bdd83c141bb9cfffde291c49aa9e41781cc24326/.bootstrap/_pex/pex.py", line 313, in _wrap_profiling
                         runner(*args)
                       File "/home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/test/pytest-prep/CPython-2.7.15/bdd83c141bb9cfffde291c49aa9e41781cc24326/.bootstrap/_pex/pex.py", line 390, in _execute
                         return self.execute_entry(self._pex_info_overrides.entry_point)
                       File "/home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/test/pytest-prep/CPython-2.7.15/bdd83c141bb9cfffde291c49aa9e41781cc24326/.bootstrap/_pex/pex.py", line 514, in execute_entry
                         return runner(entry_point)
                       File "/home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/test/pytest-prep/CPython-2.7.15/bdd83c141bb9cfffde291c49aa9e41781cc24326/.bootstrap/_pex/pex.py", line 532, in execute_pkg_resources
                         return runner()
                       File "/home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/test/pytest-prep/CPython-2.7.15/15d60b3366ef43c7a4ae6b5f5e2d7567a95fe5f4/.deps/coverage-4.5.1-cp27-cp27mu-manylinux1_x86_64.whl/coverage/cmdline.py", line 755, in main
                         status = CoverageScript().command_line(argv)
                       File "/home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/test/pytest-prep/CPython-2.7.15/15d60b3366ef43c7a4ae6b5f5e2d7567a95fe5f4/.deps/coverage-4.5.1-cp27-cp27mu-manylinux1_x86_64.whl/coverage/cmdline.py", line 507, in command_line
                         self.coverage.load()
                       File "/home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/test/pytest-prep/CPython-2.7.15/15d60b3366ef43c7a4ae6b5f5e2d7567a95fe5f4/.deps/coverage-4.5.1-cp27-cp27mu-manylinux1_x86_64.whl/coverage/control.py", line 676, in load
                         self._init()
                       File "/home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/test/pytest-prep/CPython-2.7.15/15d60b3366ef43c7a4ae6b5f5e2d7567a95fe5f4/.deps/coverage-4.5.1-cp27-cp27mu-manylinux1_x86_64.whl/coverage/control.py", line 227, in _init
                         self.plugins = Plugins.load_plugins(self.config.plugins, self.config, self.debug)
                       File "/home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/test/pytest-prep/CPython-2.7.15/15d60b3366ef43c7a4ae6b5f5e2d7567a95fe5f4/.deps/coverage-4.5.1-cp27-cp27mu-manylinux1_x86_64.whl/coverage/plugin_support.py", line 41, in load_plugins
                         __import__(module)
                     ImportError: No module named __pants_backend_python_tasks_pytest_prep__

At the handoff point from executing code, the particular sys.path is:

 >>> Executing coverage.cmdline:main using sys.path:
                     	/home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/test/pytest-prep/CPython-2.7.15/bdd83c141bb9cfffde291c49aa9e41781cc24326
                     	/home/jsirois/dev/pantsbuild/jsirois-pants/build-support/pants_dev_deps.venv/lib/python27.zip
                     	/home/jsirois/dev/pantsbuild/jsirois-pants/build-support/pants_dev_deps.venv/lib/python2.7
                     	/home/jsirois/dev/pantsbuild/jsirois-pants/build-support/pants_dev_deps.venv/lib/python2.7/plat-linux2
                     	/home/jsirois/dev/pantsbuild/jsirois-pants/build-support/pants_dev_deps.venv/lib/python2.7/lib-tk
                     	/home/jsirois/dev/pantsbuild/jsirois-pants/build-support/pants_dev_deps.venv/lib/python2.7/lib-old
                     	/home/jsirois/dev/pantsbuild/jsirois-pants/build-support/pants_dev_deps.venv/lib/python2.7/lib-dynload
                     	/usr/lib64/python2.7
                     	/usr/lib/python2.7
                     	/usr/lib/python2.7/plat-linux2
                     	/usr/lib64/python2.7/lib-tk
                     	/usr/lib/python2.7/lib-tk
                     	/home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/test/pytest-prep/CPython-2.7.15/15d60b3366ef43c7a4ae6b5f5e2d7567a95fe5f4
                     	/home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/pyprep/requirements/CPython-2.7.15/5e0b9a73f7766ef6381bac7241ece3a52aefebf3-DefaultFingerprintStrategy_f4bc9a770d1b
                     	/home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/pyprep/sources/e9d7f65130ec5c9a2a5d05db01a0075427938983
                     	/home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/test/pytest-prep/CPython-2.7.15/15d60b3366ef43c7a4ae6b5f5e2d7567a95fe5f4/.deps/pytest-3.6.4-py2.py3-none-any.whl
                     	/home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/test/pytest-prep/CPython-2.7.15/15d60b3366ef43c7a4ae6b5f5e2d7567a95fe5f4/.deps/setuptools-40.4.3-py2.py3-none-any.whl
                     	/home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/test/pytest-prep/CPython-2.7.15/15d60b3366ef43c7a4ae6b5f5e2d7567a95fe5f4/.deps/funcsigs-1.0.2-py2.py3-none-any.whl
                     	/home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/test/pytest-prep/CPython-2.7.15/15d60b3366ef43c7a4ae6b5f5e2d7567a95fe5f4/.deps/linecache2-1.0.0-py2.py3-none-any.whl
                     	/home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/test/pytest-prep/CPython-2.7.15/15d60b3366ef43c7a4ae6b5f5e2d7567a95fe5f4/.deps/pytest_timeout-1.2.1-py2.py3-none-any.whl
                     	/home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/test/pytest-prep/CPython-2.7.15/15d60b3366ef43c7a4ae6b5f5e2d7567a95fe5f4/.deps/attrs-18.2.0-py2.py3-none-any.whl
                     	/home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/test/pytest-prep/CPython-2.7.15/15d60b3366ef43c7a4ae6b5f5e2d7567a95fe5f4/.deps/pytest_cov-2.4.0-py2.py3-none-any.whl
                     	/home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/test/pytest-prep/CPython-2.7.15/15d60b3366ef43c7a4ae6b5f5e2d7567a95fe5f4/.deps/more_itertools-4.3.0-py2-none-any.whl
                     	/home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/test/pytest-prep/CPython-2.7.15/15d60b3366ef43c7a4ae6b5f5e2d7567a95fe5f4/.deps/unittest2-1.1.0-py2.py3-none-any.whl
                     	/home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/test/pytest-prep/CPython-2.7.15/15d60b3366ef43c7a4ae6b5f5e2d7567a95fe5f4/.deps/six-1.11.0-py2.py3-none-any.whl
                     	/home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/test/pytest-prep/CPython-2.7.15/15d60b3366ef43c7a4ae6b5f5e2d7567a95fe5f4/.deps/atomicwrites-1.2.1-py2.py3-none-any.whl
                     	/home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/test/pytest-prep/CPython-2.7.15/15d60b3366ef43c7a4ae6b5f5e2d7567a95fe5f4/.deps/py-1.6.0-py2.py3-none-any.whl
                     	/home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/test/pytest-prep/CPython-2.7.15/15d60b3366ef43c7a4ae6b5f5e2d7567a95fe5f4/.deps/coverage-4.5.1-cp27-cp27mu-manylinux1_x86_64.whl
                     	/home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/test/pytest-prep/CPython-2.7.15/15d60b3366ef43c7a4ae6b5f5e2d7567a95fe5f4/.deps/pluggy-0.7.1-py2.py3-none-any.whl
                     	/home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/test/pytest-prep/CPython-2.7.15/15d60b3366ef43c7a4ae6b5f5e2d7567a95fe5f4/.deps/argparse-1.4.0-py2.py3-none-any.whl
                     	/home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/test/pytest-prep/CPython-2.7.15/15d60b3366ef43c7a4ae6b5f5e2d7567a95fe5f4/.deps/traceback2-1.4.0-py2.py3-none-any.whl
                     	/home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/pyprep/requirements/CPython-2.7.15/5e0b9a73f7766ef6381bac7241ece3a52aefebf3-DefaultFingerprintStrategy_f4bc9a770d1b/.deps/six-1.11.0-py2.py3-none-any.whl
                     	/home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/pyprep/requirements/CPython-2.7.15/5e0b9a73f7766ef6381bac7241ece3a52aefebf3-DefaultFingerprintStrategy_f4bc9a770d1b/.deps/thrift-0.11.0-cp27-cp27mu-linux_x86_64.whl
                     	/home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/test/pytest-prep/CPython-2.7.15/bdd83c141bb9cfffde291c49aa9e41781cc24326/.bootstrap

Here the 1st entry, /home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/test/pytest-prep/CPython-2.7.15/bdd83c141bb9cfffde291c49aa9e41781cc24326 is the executing code and it contains the plugin.
Coverage is on the sys.path in the /home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/test/pytest-prep/CPython-2.7.15/15d60b3366ef43c7a4ae6b5f5e2d7567a95fe5f4/.deps/coverage-4.5.1-cp27-cp27mu-manylinux1_x86_64.whl entry lower down.

I went down the rabbit hole and things were ok at coverage/cmdline.py line 755:

>>> In coverage.cmdline with sys.path:
                     	/home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/test/pytest-prep/CPython-2.7.15/bdd83c141bb9cfffde291c49aa9e41781cc24326
                     	/home/jsirois/dev/pantsbuild/jsirois-pants/build-support/pants_dev_deps.venv/lib/python27.zip
...

But not ok here:

>>> In coverage.cmdline about to call self.coverage.load() with sys.path:
                     	
                     	/home/jsirois/dev/pantsbuild/jsirois-pants/build-support/pants_dev_deps.venv/lib/python27.zip
...

@jsirois
Copy link
Author

jsirois commented Oct 5, 2018

FWIW - I'm hacking around presently by ensuring the 1st sys.path entry (the user code), is on the sys.path twice.

@jsirois
Copy link
Author

jsirois commented Oct 5, 2018

Also probably a wall of text, but the fix commit downstream is here: pantsbuild/pants@2d95756

@nedbat
Copy link
Owner

nedbat commented Oct 8, 2018

The problem with making the change you suggest is that then "coverage run prog.py" creates a different sys.path than "python prog.py". Making the two the same has been my guiding star, since it is difficult to decide what the "correct" behavior should be other than that.

I don't know what to do about your plugin problem....

@jsirois
Copy link
Author

jsirois commented Oct 8, 2018

I do have a workaround - so, bottom line, no worries. That said, commenting out the sys.path[0] mutation leads to the same sys.path via python test.py or coverage run test.py since python places CWD at the head of sys.path already. IOW sys.path[0] = '' is a no-op in this vanilla case. Presumably it fixes up a sys.path in a less standard case. The comment references django which I have little familiarity with. It would seem though that there is an assumption that whatever is in the 0th slot is unimportant. I'm not sure how coverage can be confident what is in that slot (except in the vanilla case mentioned above, in which case the line is a no-op). Perhaps the django case puts a safely-nukeable path element there?

@nedbat
Copy link
Owner

nedbat commented Oct 8, 2018

@jsirois Hmm, commenting out that line might be fine: the coverage.py tests all pass. Does it solve your problem? There is another sys.path manipulation later when we exec the file.

@jsirois
Copy link
Author

jsirois commented Oct 8, 2018

I'm not positive, but I don't think it will affects us. Here I assume?:

# Set sys.path correctly.
old_path0 = sys.path[0]
sys.path[0] = path0 if path0 is not None else my_path0

We use pytest-cov to actually collect coverage data under the pytest runner and they appear to use the coverage API (ie: coverage.run(), etc) - not execfile. We only have this issue trying to produce reports after the coverage run using the coverage command reporting tools.

@nedbat
Copy link
Owner

nedbat commented Oct 8, 2018

@jsirois Could you try removing the line?

@jsirois
Copy link
Author

jsirois commented Oct 8, 2018

Will do. If I don't get a PR out tonight I will tomorrow. Thanks Ned!

@nedbat
Copy link
Owner

nedbat commented Oct 8, 2018

A PR isn't necessary (the change is simple enough), but I'd like to know how your scenario behaves with the change.

@jsirois
Copy link
Author

jsirois commented Oct 8, 2018

Ah, gotcha. I can guaranty it solves our problem. The digging I hinted at above with the injected print statements proved that. That said, I can back up that guaranty with a full test by tomorrow. I'll report back.

@nedbat
Copy link
Owner

nedbat commented Oct 9, 2018

@jsirois #678 is also about this.

@nedbat nedbat added the bug Something isn't working label Oct 9, 2018
@jsirois
Copy link
Author

jsirois commented Oct 9, 2018

Aha, yes I duped #678.

I just had a chance to burn a local coverage dist with:

$ git diff coverage/cmdline.py
diff --git a/coverage/cmdline.py b/coverage/cmdline.py
index edbc1d25..e6ea6e23 100644
--- a/coverage/cmdline.py
+++ b/coverage/cmdline.py
@@ -465,10 +465,6 @@ class CoverageScript(object):
         if self.do_help(options, args, parser):
             return OK
 
-        # We need to be able to import from the current directory, because
-        # plugins may try to, for example, to read Django settings.
-        sys.path[0] = ''
-
         # Listify the list options.
         source = unshell_list(options.source)
         omit = unshell_list(options.omit)

That did in fact solve the problem pants was having and coverage now works without hacks.

@nedbat
Copy link
Owner

nedbat commented Oct 17, 2018

Fixed in e82d24d.

@nedbat nedbat closed this as completed Oct 17, 2018
nedbat added a commit that referenced this issue Oct 17, 2018
@jsirois
Copy link
Author

jsirois commented Oct 17, 2018

Thanks @nedbat!

@nedbat
Copy link
Owner

nedbat commented Oct 21, 2018

Hmm, wait a minute: looks like this breaks Windows (and my Windows CI is missing some builds!?). Re-opening for the moment.

@nedbat nedbat reopened this Oct 21, 2018
nedbat added a commit that referenced this issue Oct 21, 2018
nedbat added a commit that referenced this issue Oct 22, 2018
nedbat added a commit that referenced this issue Oct 23, 2018
@nedbat
Copy link
Owner

nedbat commented Nov 13, 2018

It wasn't a Windows problem. Coverage.py tests fail if you don't run them with pytest-xdist, reported here: pytest-dev/pytest-xdist#376. I'm still looking for the right solution.

@nedbat
Copy link
Owner

nedbat commented Nov 14, 2018

@jsirois Looking into this more, I have a few questions. The sys.path docs (https://docs.python.org/3/library/sys.html#sys.path) say, "As initialized upon program startup, the first item of this list, path[0], is the directory containing the script that was used to invoke the Python interpreter." What is sys.path[0] for you, and why is it different than this?

Also, I am just curious about your coverage plugin: what does it do?

@jsirois
Copy link
Author

jsirois commented Nov 14, 2018

@nedbat we run pytest and coverage from within a pex - roughly a zipapp, and so the sys.path[0] entry points to our pex.

In other words - in the debug output above snipped and annotated here:

[0] /home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/test/pytest-prep/CPython-2.7.15/bdd83c141bb9cfffde291c49aa9e41781cc24326
/usr/lib64/python2.7
...
/usr/lib/python2.7/lib-tk
[12] /home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/test/pytest-prep/CPython-2.7.15/15d60b3366ef43c7a4ae6b5f5e2d7567a95fe5f4
[13] /home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/pyprep/requirements/CPython-2.7.15/5e0b9a73f7766ef6381bac7241ece3a52aefebf3-DefaultFingerprintStrategy_f4bc9a770d1b
[14] /home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/pyprep/sources/e9d7f65130ec5c9a2a5d05db01a0075427938983
/home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/test/pytest-prep/CPython-2.7.15/15d60b3366ef43c7a4ae6b5f5e2d7567a95fe5f4/.deps/pytest-3.6.4-py2.py3-none-any.whl
...
/home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/test/pytest-prep/CPython-2.7.15/15d60b3366ef43c7a4ae6b5f5e2d7567a95fe5f4/.deps/py-1.6.0-py2.py3-none-any.whl
[27] /home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/test/pytest-prep/CPython-2.7.15/15d60b3366ef43c7a4ae6b5f5e2d7567a95fe5f4/.deps/coverage-4.5.1-cp27-cp27mu-manylinux1_x86_64.whl
/home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/test/pytest-prep/CPython-2.7.15/15d60b3366ef43c7a4ae6b5f5e2d7567a95fe5f4/.deps/pluggy-0.7.1-py2.py3-none-any.whl
...

The 0th entry is our pex, then a bunch of system python entries, then more bits of our pex (we combine 4) in 12, 13 & 14 - then 3rdparty dependencies of the pex - including coverage down in entry 27. The critical point which applies here afaict is coverage is not guaranteed to be in the 0th slot if some other tool is running coverage. As such coverage should not blow away any sys.path entry it is not sure it owns. In this case the 0th entry is us, the bootstrap code that runs coverage and contains the coverage plugins we wish to activate. More generally, inserting or appending a sys.path entry as is appropriate to the case at hand is almost always what you want to do afaict. If there might be multiple versions of the thing and you need to have your version seen, insert 0, if you know you're unique and just need to be visible, append.

The plugin just attempts to remap filenames. The scenario is as follows:

  1. A user wants to run tests and get coverage data for those tests.
  2. Pants (a build tool), gathers up the relevant test code and production code under test and forms a hermetic pex containing the user code as well as pytest and coverage.
  3. Pants executes pytest and coverage against the user code in the context of the pex.

Because the coverage is gathered in step 3, it sees and reports on paths different from the original loose filesystem paths the user expects; and as such we have a coverage plugin that remaps pex pathnames back to user-space.

@nedbat
Copy link
Owner

nedbat commented Nov 18, 2018

@jsirois Thanks for the explanation.

BTW, your description of your plugin sounds like what the [paths] configuration setting does: https://coverage.readthedocs.io/en/v4.5.x/config.html#paths . Or is your situation more complex?

@jsirois
Copy link
Author

jsirois commented Nov 19, 2018

The situation is a bit more complex. See a comment here: pantsbuild/pants#5426 (comment) and our discussion in #646.

The issue boils down to this code here:

aliases = None
if self.config.paths:
aliases = PathAliases()
for paths in self.config.paths.values():
result = paths[0]
for pattern in paths[1:]:
aliases.add(pattern, result)

Pants runs tests for projects that can be split over multiple source trees; ie:

project/
  widget/src/python/
    widget/__init__.py  # A namespace package
    widget/module1.py
  util/src/python
    widget/__init__.py  # A namespace package
    widget/util.py

As such, a test against the widget package in this example can produce coverage that is spanned multiple different local source roots. The PathAliases code does not allow for this as things stand and 1 local source root "wins" leading to trampling of data from the losers. So my options were to contribute a patch to support the concept of multiple local source roots or else write a plugin. I'd be happy to ditch the plugin in favor of using combine if you think a patch to add support for this concept is desirable.

@nedbat
Copy link
Owner

nedbat commented Nov 25, 2018

This is re-fixed in commit b7e0eec, released as part of 5.0a4.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants