diff options
-rw-r--r-- | coverage/control.py | 66 | ||||
-rw-r--r-- | coverage/python.py | 41 | ||||
-rw-r--r-- | tests/modules/usepkgs.py | 2 | ||||
-rw-r--r-- | tests/test_api.py | 7 | ||||
-rw-r--r-- | tests/test_python.py | 27 |
5 files changed, 91 insertions, 52 deletions
diff --git a/coverage/control.py b/coverage/control.py index 037fc6d2..ebb449c3 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -26,7 +26,7 @@ from coverage.misc import CoverageException, bool_or_none, join_regex from coverage.misc import file_be_gone, isolate_module from coverage.plugin import FileReporter from coverage.plugin_support import Plugins -from coverage.python import PythonFileReporter +from coverage.python import PythonFileReporter, source_for_file from coverage.results import Analysis, Numbers from coverage.summary import SummaryReporter from coverage.xmlreport import XmlReporter @@ -164,6 +164,7 @@ class Coverage(object): # Other instance attributes, set later. self.omit = self.include = self.source = None + self.source_pkgs_unmatched = None self.source_pkgs = None self.data = self.data_files = self.collector = None self.plugins = None @@ -228,6 +229,7 @@ class Coverage(object): self.source.append(files.canonical_filename(src)) else: self.source_pkgs.append(src) + self.source_pkgs_unmatched = self.source_pkgs[:] self.omit = prep_patterns(self.config.omit) self.include = prep_patterns(self.config.include) @@ -367,38 +369,6 @@ class Coverage(object): morf_filename = PythonFileReporter(morf, self).filename return os.path.split(morf_filename)[0] - def _source_for_file(self, filename): - """Return the source file for `filename`. - - Given a file name being traced, return the best guess as to the source - file to attribute it to. - - """ - if filename.endswith(".py"): - # .py files are themselves source files. - return filename - - elif filename.endswith((".pyc", ".pyo")): - # Bytecode files probably have source files near them. - py_filename = filename[:-1] - if os.path.exists(py_filename): - # Found a .py file, use that. - return py_filename - if env.WINDOWS: - # On Windows, it could be a .pyw file. - pyw_filename = py_filename + "w" - if os.path.exists(pyw_filename): - return pyw_filename - # Didn't find source, but it's probably the .py file we want. - return py_filename - - elif filename.endswith("$py.class"): - # Jython is easy to guess. - return filename[:-9] + ".py" - - # No idea, just use the file name as-is. - return filename - def _name_for_module(self, module_globals, filename): """Get the name of the module for a set of globals and file name. @@ -461,7 +431,7 @@ class Coverage(object): # co_filename value. dunder_file = frame.f_globals.get('__file__') if dunder_file: - filename = self._source_for_file(dunder_file) + filename = source_for_file(dunder_file) if original_filename and not original_filename.startswith('<'): orig = os.path.basename(original_filename) if orig != os.path.basename(filename): @@ -558,8 +528,8 @@ class Coverage(object): # stdlib and coverage.py directories. if self.source_match: if self.source_pkgs_match.match(modulename): - if modulename in self.source_pkgs: - self.source_pkgs.remove(modulename) + if modulename in self.source_pkgs_unmatched: + self.source_pkgs_unmatched.remove(modulename) return None # There's no reason to skip this file. if not self.source_match.match(filename): @@ -824,10 +794,10 @@ class Coverage(object): self.collector.save_data(self.data) - # If there are still entries in the source_pkgs list, then we never + # If there are still entries in the source_pkgs_unmatched list, then we never # encountered those packages. if self._warn_unimported_source: - for pkg in self.source_pkgs: + for pkg in self.source_pkgs_unmatched: if pkg not in sys.modules: self._warn("Module %s was never imported." % pkg) elif not ( @@ -842,8 +812,26 @@ class Coverage(object): if not self.data and self._warn_no_data: self._warn("No data was collected.") + src_directories = self.source[:] + + for pkg in self.source_pkgs: + if (not pkg in sys.modules or + not hasattr(sys.modules[pkg], '__file__') or + not os.path.exists(sys.modules[pkg].__file__)): + continue + pkg_file = source_for_file(sys.modules[pkg].__file__) + # + # Do not explore the souce tree of a module that is + # not a directory tree. For instance if + # sys.modules['args'].__file__ == /lib/python2.7/site-packages/args.pyc + # we do not want to explore all of /lib/python2.7/site-packages + # + if not pkg_file.endswith('__init__.py'): + continue + src_directories.append(self._canonical_dir(sys.modules[pkg])) + # Find files that were never executed at all. - for src in self.source: + for src in src_directories: for py_file in find_python_files(src): py_file = files.canonical_filename(py_file) diff --git a/coverage/python.py b/coverage/python.py index c3ca0e1e..f75be60a 100644 --- a/coverage/python.py +++ b/coverage/python.py @@ -91,6 +91,39 @@ def get_zip_bytes(filename): return None +def source_for_file(filename): + """Return the source file for `filename`. + + Given a file name being traced, return the best guess as to the source + file to attribute it to. + + """ + if filename.endswith(".py"): + # .py files are themselves source files. + return filename + + elif filename.endswith((".pyc", ".pyo")): + # Bytecode files probably have source files near them. + py_filename = filename[:-1] + if os.path.exists(py_filename): + # Found a .py file, use that. + return py_filename + if env.WINDOWS: + # On Windows, it could be a .pyw file. + pyw_filename = py_filename + "w" + if os.path.exists(pyw_filename): + return pyw_filename + # Didn't find source, but it's probably the .py file we want. + return py_filename + + elif filename.endswith("$py.class"): + # Jython is easy to guess. + return filename[:-9] + ".py" + + # No idea, just use the file name as-is. + return filename + + class PythonFileReporter(FileReporter): """Report support for a Python file.""" @@ -106,13 +139,7 @@ class PythonFileReporter(FileReporter): else: filename = morf - filename = files.unicode_filename(filename) - - # .pyc files should always refer to a .py instead. - if filename.endswith(('.pyc', '.pyo')): - filename = filename[:-1] - elif filename.endswith('$py.class'): # Jython - filename = filename[:-9] + ".py" + filename = source_for_file(files.unicode_filename(filename)) super(PythonFileReporter, self).__init__(files.canonical_filename(filename)) diff --git a/tests/modules/usepkgs.py b/tests/modules/usepkgs.py index 4e94acaa..222e68ce 100644 --- a/tests/modules/usepkgs.py +++ b/tests/modules/usepkgs.py @@ -1,7 +1,7 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt -import pkg1.p1a, pkg1.p1b +import pkg1.p1a, pkg1.p1b, pkg1.sub import pkg2.p2a, pkg2.p2b import othermods.othera, othermods.otherb import othermods.sub.osa, othermods.sub.osb diff --git a/tests/test_api.py b/tests/test_api.py index 9de83fb2..c7fa5ca2 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -461,12 +461,11 @@ class SourceOmitIncludeTest(OmitIncludeTestsMixin, CoverageTest): summary[k[:-3]] = v return summary - def test_source_package(self): - lines = self.coverage_usepkgs(source=["pkg1"]) - self.filenames_in(lines, "p1a p1b") + def test_source_package_unexecuted(self): + lines = self.coverage_usepkgs(source=["pkg1.sub"]) self.filenames_not_in(lines, "p2a p2b othera otherb osa osb") # Because source= was specified, we do search for unexecuted files. - self.assertEqual(lines['p1c'], 0) + self.assertEqual(lines['runmod3'], 0) def test_source_package_dotted(self): lines = self.coverage_usepkgs(source=["pkg1.p1b"]) diff --git a/tests/test_python.py b/tests/test_python.py index ee1e1f95..c4e333ef 100644 --- a/tests/test_python.py +++ b/tests/test_python.py @@ -5,8 +5,9 @@ import os import sys +from coverage import env -from coverage.python import get_zip_bytes +from coverage.python import get_zip_bytes, source_for_file from tests.coveragetest import CoverageTest @@ -28,3 +29,27 @@ class GetZipBytesTest(CoverageTest): self.assertIn('All OK', zip_text) # Run the code to see that we really got it encoded properly. __import__("encoded_"+encoding) + +def test_source_for_file(tmpdir): + path = tmpdir.join("a.py") + src = str(path) + assert src == source_for_file(src) + assert src == source_for_file(src + 'c') + assert src == source_for_file(src + 'o') + unknown = src + 'FOO' + assert unknown == source_for_file(unknown) + # + # On windows if a pyw exists, it is an acceptable source + # + windows = env.WINDOWS + env.WINDOWS = True + path_windows = tmpdir.ensure("a.pyw") + assert str(path_windows) == source_for_file(src + 'c') + # + # If both pyw and py exist, py is preferred + # + path.ensure(file=True) + assert src == source_for_file(src + 'c') + env.WINDOWS = windows + jython_src = "a" + assert jython_src + ".py" == source_for_file(jython_src + "$py.class") |