nedbat / coveragepy

The code coverage tool for Python
https://coverage.readthedocs.io
Apache License 2.0
3.01k stars 433 forks source link

Coverage needs to use `normcase` or CI-comparisons for file paths due to Python bug on Windows #951

Open arcivanov opened 4 years ago

arcivanov commented 4 years ago

Coverage uses realpath along with literal string comparison/SQLite literal storage of file names:

https://github.com/nedbat/coveragepy/blob/fa2e0e49fea9f6cbaab9e2dc3a203dc59b8fb2c2/coverage/files.py#L165

https://github.com/nedbat/coveragepy/blob/fa2e0e49fea9f6cbaab9e2dc3a203dc59b8fb2c2/coverage/sqldata.py#L291

https://github.com/nedbat/coveragepy/blob/fa2e0e49fea9f6cbaab9e2dc3a203dc59b8fb2c2/coverage/sqldata.py#L365

Unfortunately on Windows, as I just discovered to my astonishment and chagrin, Python replaced realpath with abspath until Python 3.8.0 (https://bugs.python.org/issue9949)

Therefore, on Windows for any code that is not pinned to 3.8+, no literal path string comparisons are permissible - only the CI-ones.

Observe:

C:\PyBuilder>C:\Python37\python.exe
Python 3.7.2 (tags/v3.7.2:9a3ffc0492, Dec 23 2018, 23:09:28) [MSC v.1916 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>> os.path.realpath("c:\\users")
'c:\\users'
>>> ^Z

C:\PyBuilder>C:\Python38\python.exe
Python 3.8.1 (tags/v3.8.1:1b293b6, Dec 18 2019, 23:11:46) [MSC v.1916 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>> os.path.realpath("c:\\users")
'C:\\Users'
>>> ^Z

Either all filenames must be normcased before entering the data store, or all comparisons must be case-insensitive. I personally would vote for normcase as it leaves all paths untouched on POSIX and lower-cases all paths on Windows.

arcivanov commented 4 years ago

Same with latest version of Python 3.7:

C:\PyBuilder>C:\Python37\python.exe
Python 3.7.6 (tags/v3.7.6:43364a7ae0, Dec 19 2019, 00:42:30) [MSC v.1916 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>> os.path.realpath("c:\\users")
'c:\\users'
>>> ^Z
arcivanov commented 4 years ago

Relevant runtime patch: https://github.com/pybuilder/pybuilder/blob/master/src/main/python/pybuilder/plugins/python/_coverage_util.py#L36

nedbat commented 4 years ago

Can you give me a specific scenario using coverage.py that isn't working properly?

arcivanov commented 4 years ago

I can give you a real one I bumped into and the hypothetical that a regular user may bump into. Mine is using coverage._analyze(filename) and finding no coverage for a file that I know was there, because I passed normcased filename and it didn't match realpathed filename recorded.

The one you may be more inclined to care about is recording coverage running python 3.8.x on Windows and then opening the same file with coverage on python 3.y; y < 8 also on Windows. In the latter case paths simply won't match and coverage will show up empty.

arcivanov commented 4 years ago

Combining coverage on Windows for the same set of files on the same path that ran with Python 3.8 and Python 3.7 will also produce nonsensical results as there will be coverage for file C:\User\Project\file.py and c:\user\project\file.py and the two will appear as different files in the database.