summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNed Batchelder <ned@nedbatchelder.com>2012-11-11 12:22:03 -0500
committerNed Batchelder <ned@nedbatchelder.com>2012-11-11 12:22:03 -0500
commitde89438a0346e3dca4f7e8afe24efa38810deebe (patch)
tree82acb47b22bf967a9573651adb6cc18d9bc216a2
parent99480be7da89cb82cfff01e5d10a2514546faf39 (diff)
downloadpython-coveragepy-git-de89438a0346e3dca4f7e8afe24efa38810deebe.tar.gz
Windows now reports file names in their correct case. #89 and #203.
-rw-r--r--CHANGES.txt5
-rw-r--r--coverage/__init__.py7
-rw-r--r--coverage/control.py1
-rw-r--r--coverage/files.py49
-rw-r--r--coverage/fullcoverage/encodings.py8
-rw-r--r--test/test_summary.py30
6 files changed, 96 insertions, 4 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index 752738c4..87a75705 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -27,6 +27,9 @@ Version 3.5.4b1
coverage.xml_report() now all return a float, the total percentage covered
measurement.
+- On Windows, files are now reported in their correct case. Fixes
+ `issue 89`_ and `issue 203`_.
+
- Running an HTML report in Python 3 in the same directory as an old Python 2
HTML report would fail with a UnicodeDecodeError. This issue (`issue 193`_)
is now fixed.
@@ -43,10 +46,12 @@ Version 3.5.4b1
- The pydoc tool can now show docmentation for the class `coverage.coverage`.
Closes `issue 206`_.
+.. _issue 89: https://bitbucket.org/ned/coveragepy/issue/89/on-windows-all-packages-are-reported-in
.. _issue 139: https://bitbucket.org/ned/coveragepy/issue/139/easy-check-for-a-certain-coverage-in-tests
.. _issue 163: https://bitbucket.org/ned/coveragepy/issue/163/problem-with-include-and-omit-filename
.. _issue 193: https://bitbucket.org/ned/coveragepy/issue/193/unicodedecodeerror-on-htmlpy
.. _issue 201: https://bitbucket.org/ned/coveragepy/issue/201/coverage-using-django-14-with-pydb-on
+.. _issue 203: https://bitbucket.org/ned/coveragepy/issue/203/duplicate-filenames-reported-when-filename
.. _issue 205: https://bitbucket.org/ned/coveragepy/issue/205/make-pydoc-coverage-more-friendly
.. _issue 206: https://bitbucket.org/ned/coveragepy/issue/206/pydoc-coveragecoverage-fails-with-an-error
.. _issue 210: https://bitbucket.org/ned/coveragepy/issue/210/if-theres-no-coverage-data-coverage-xml
diff --git a/coverage/__init__.py b/coverage/__init__.py
index a10b4a67..2653853b 100644
--- a/coverage/__init__.py
+++ b/coverage/__init__.py
@@ -78,6 +78,12 @@ analysis2 = _singleton_method('analysis2')
report = _singleton_method('report')
annotate = _singleton_method('annotate')
+
+# On Windows, we encode and decode deep enough that something goes wrong and
+# the encodings.utf_8 module is loaded and then unloaded, I don't know why.
+# Adding a reference here prevents it from being unloaded. Yuk.
+import encodings.utf_8
+
# Because of the "from coverage.control import fooey" lines at the top of the
# file, there's an entry for coverage.coverage in sys.modules, mapped to None.
# This makes some inspection tools (like pydoc) unable to find the class
@@ -88,6 +94,7 @@ try:
except KeyError:
pass
+
# COPYRIGHT AND LICENSE
#
# Copyright 2001 Gareth Rees. All rights reserved.
diff --git a/coverage/control.py b/coverage/control.py
index 9523defa..0391352d 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -475,6 +475,7 @@ class coverage(object):
# Find files that were never executed at all.
for src in self.source:
for py_file in find_python_files(src):
+ py_file = self.file_locator.canonical_filename(py_file)
self.data.touch_file(py_file)
self._harvested = True
diff --git a/coverage/files.py b/coverage/files.py
index 84466504..65b1dc01 100644
--- a/coverage/files.py
+++ b/coverage/files.py
@@ -9,7 +9,7 @@ class FileLocator(object):
def __init__(self):
# The absolute path to our current directory.
- self.relative_dir = abs_file(os.curdir) + os.sep
+ self.relative_dir = os.path.normcase(abs_file(os.curdir) + os.sep)
# Cache of results of calling the canonical_filename() method, to
# avoid duplicating work.
@@ -22,8 +22,9 @@ class FileLocator(object):
`FileLocator` was constructed.
"""
- if filename.startswith(self.relative_dir):
- filename = filename.replace(self.relative_dir, "", 1)
+ fnorm = os.path.normcase(filename)
+ if fnorm.startswith(self.relative_dir):
+ filename = filename[len(self.relative_dir):]
return filename
def canonical_filename(self, filename):
@@ -74,9 +75,49 @@ class FileLocator(object):
return None
+if sys.platform == 'win32':
+ import ctypes
+
+ def getLongPathName(path):
+ """Call Windows' GetLongPathNameW, unicode to unicode."""
+ buf = ctypes.create_unicode_buffer(260)
+ rv = ctypes.windll.kernel32.GetLongPathNameW(path, buf, 260)
+ if rv == 0 or rv > 260:
+ return path
+ else:
+ return buf.value
+
+ def getShortPathName(path):
+ """Call Windows' GetShortPathNameW, unicode to unicode."""
+ buf = ctypes.create_unicode_buffer(260)
+ rv = ctypes.windll.kernel32.GetShortPathNameW(path, buf, 260)
+ if rv == 0 or rv > 260:
+ return path
+ else:
+ return buf.value
+
+ def actual_path(path):
+ """Get the actual path of `path`, including the correct case."""
+ if sys.version_info >= (3,0):
+ path = path.encode('utf-16')
+ else:
+ path = path.decode('utf8')
+ actual = getLongPathName(getShortPathName(path))
+ if sys.version_info >= (3,0):
+ actual = actual.decode('utf-16')
+ else:
+ actual = actual.encode('utf8')
+ return actual
+else:
+ def actual_filename(filename):
+ """The actual filename for non-Windows platforms."""
+ return filename
+
def abs_file(filename):
"""Return the absolute normalized form of `filename`."""
- return os.path.normcase(os.path.abspath(os.path.realpath(filename)))
+ path = os.path.abspath(os.path.realpath(filename))
+ path = actual_filename(path)
+ return path
def prep_patterns(patterns):
diff --git a/coverage/fullcoverage/encodings.py b/coverage/fullcoverage/encodings.py
index ad350bc0..6a258d67 100644
--- a/coverage/fullcoverage/encodings.py
+++ b/coverage/fullcoverage/encodings.py
@@ -37,6 +37,14 @@ class FullCoverageTracer(object):
sys.settrace(FullCoverageTracer().fullcoverage_trace)
+# In coverage/files.py is actual_filename(), which uses glob.glob. I don't
+# understand why, but that use of glob borks everything if fullcoverage is in
+# effect. So here we make an ugly hail-mary pass to switch off glob.glob over
+# there. This means when using fullcoverage, Windows path names will not be
+# their actual case.
+
+#sys.fullcoverage = True
+
# Finally, remove our own directory from sys.path; remove ourselves from
# sys.modules; and re-import "encodings", which will be the real package
# this time. Note that the delete from sys.modules dictionary has to
diff --git a/test/test_summary.py b/test/test_summary.py
index d2be95d9..4ca82656 100644
--- a/test/test_summary.py
+++ b/test/test_summary.py
@@ -179,6 +179,36 @@ class SummaryTest(CoverageTest):
self.assertEqual(self.line_count(report), 2)
+ def run_TheCode_and_report_it(self):
+ """A helper for the next few tests."""
+ cov = coverage.coverage()
+ cov.start()
+ import TheCode
+ cov.stop()
+
+ repout = StringIO()
+ cov.report(file=repout, show_missing=False)
+ report = repout.getvalue().replace('\\', '/')
+ report = re.sub(r"\s+", " ", report)
+ return report
+
+ def test_bug_203_mixed_case_listed_twice_with_rc(self):
+ self.make_file("TheCode.py", "a = 1\n")
+ self.make_file(".coveragerc", "[run]\nsource = .\n")
+
+ report = self.run_TheCode_and_report_it()
+
+ self.assertIn("TheCode", report)
+ self.assertNotIn("thecode", report)
+
+ def test_bug_203_mixed_case_listed_twice(self):
+ self.make_file("TheCode.py", "a = 1\n")
+
+ report = self.run_TheCode_and_report_it()
+
+ self.assertIn("TheCode", report)
+ self.assertNotIn("thecode", report)
+
class SummaryTest2(CoverageTest):
"""Another bunch of summary tests."""