Skip to content

Commit a40efab

Browse files
committed
fix: generate flat file names differently
Fixes a few unusual issues with reports: - #580: HTML report generation fails on too long path - #584: File collisions in coverage report html - #1167: Remove leading underscore in coverage html
1 parent 0ff5a1c commit a40efab

10 files changed

+63
-26
lines changed

CHANGES.rst

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,20 @@ Unreleased
4646

4747
- TOML parsing now uses the `tomli`_ library.
4848

49-
- Use a modern hash algorithm when fingerprinting to speed HTML reports
50-
(`issue 1189`_).
49+
- Some minor changes to usually invisible details of the HTML report:
50+
51+
- Use a modern hash algorithm when fingerprinting, for high-security
52+
environments (`issue 1189`_).
53+
54+
- Change how report file names are generated, to avoid leading underscores
55+
(`issue 1167`_), to avoid rare file name collisions (`issue 584`_), and to
56+
avoid file names becoming too long (`issue 580`_).
5157

5258
.. _Django coverage plugin: https://pypi.org/project/django-coverage-plugin/
59+
.. _issue 580: https://github.com/nedbat/coveragepy/issues/580
60+
.. _issue 584: https://github.com/nedbat/coveragepy/issues/584
5361
.. _issue 1150: https://github.com/nedbat/coveragepy/issues/1150
62+
.. _issue 1167: https://github.com/nedbat/coveragepy/issues/1167
5463
.. _issue 1168: https://github.com/nedbat/coveragepy/issues/1168
5564
.. _issue 1189: https://github.com/nedbat/coveragepy/issues/1189
5665
.. _tomli: https://pypi.org/project/tomli/

coverage/files.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def canonical_filename(filename):
7777
return CANONICAL_FILENAME_CACHE[filename]
7878

7979

80-
MAX_FLAT = 200
80+
MAX_FLAT = 100
8181

8282
@contract(filename='unicode', returns='unicode')
8383
def flat_rootname(filename):
@@ -87,15 +87,16 @@ def flat_rootname(filename):
8787
the same directory, but need to differentiate same-named files from
8888
different directories.
8989
90-
For example, the file a/b/c.py will return 'a_b_c_py'
90+
For example, the file a/b/c.py will return 'd_86bbcbe134d28fd2_c_py'
9191
9292
"""
93-
name = ntpath.splitdrive(filename)[1]
94-
name = re.sub(r"[\\/.:]", "_", name)
95-
if len(name) > MAX_FLAT:
96-
h = hashlib.sha1(name.encode('UTF-8')).hexdigest()
97-
name = name[-(MAX_FLAT-len(h)-1):] + '_' + h
98-
return name
93+
dirname, basename = ntpath.split(filename)
94+
if dirname:
95+
fp = hashlib.new("sha3_256", dirname.encode("UTF-8")).hexdigest()[:16]
96+
prefix = f"d_{fp}_"
97+
else:
98+
prefix = ""
99+
return prefix + basename.replace(".", "_")
99100

100101

101102
if env.WINDOWS:

tests/goldtest.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ def compare(
8181

8282
actual_file = os.path.join(actual_dir, f)
8383
with open(actual_file) as fobj:
84-
actual = fobj.read()
84+
actual = actual_original = fobj.read()
8585
if actual_file.endswith(".xml"):
8686
actual = canonicalize_xml(actual)
8787

@@ -95,6 +95,20 @@ def compare(
9595
print(f":::: diff {expected_file!r} and {actual_file!r}")
9696
print("\n".join(difflib.Differ().compare(expected, actual)))
9797
print(f":::: end diff {expected_file!r} and {actual_file!r}")
98+
99+
save_path = expected_dir.replace("/gold/", "/actual/")
100+
os.makedirs(save_path, exist_ok=True)
101+
with open(os.path.join(save_path, f), "w") as savef:
102+
savef.write(actual_original)
103+
104+
if not actual_extra:
105+
for f in actual_only:
106+
save_path = expected_dir.replace("/gold/", "/actual/")
107+
os.makedirs(save_path, exist_ok=True)
108+
with open(os.path.join(save_path, f), "w") as savef:
109+
with open(os.path.join(actual_dir, f)) as readf:
110+
savef.write(readf.read())
111+
98112
assert not text_diff, "Files differ: %s" % '\n'.join(text_diff)
99113

100114
assert not expected_only, f"Files in {expected_dir} only: {expected_only}"

tests/test_files.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -69,18 +69,23 @@ def test_canonical_filename_ensure_cache_hit(self):
6969

7070

7171
@pytest.mark.parametrize("original, flat", [
72-
("a/b/c.py", "a_b_c_py"),
73-
(r"c:\foo\bar.html", "_foo_bar_html"),
74-
("Montréal/☺/conf.py", "Montréal_☺_conf_py"),
72+
("abc.py", "abc_py"),
73+
("hellothere", "hellothere"),
74+
("a/b/c.py", "d_86bbcbe134d28fd2_c_py"),
75+
("a/b/defghi.py", "d_86bbcbe134d28fd2_defghi_py"),
76+
("/a/b/c.py", "d_bb25e0ada04227c6_c_py"),
77+
("/a/b/defghi.py", "d_bb25e0ada04227c6_defghi_py"),
78+
(r"c:\foo\bar.html", "d_e7c107482373f299_bar_html"),
79+
(r"d:\foo\bar.html", "d_584a05dcebc67b46_bar_html"),
80+
("Montréal/☺/conf.py", "d_c840497a2c647ce0_conf_py"),
7581
( # original:
76-
r"c:\lorem\ipsum\quia\dolor\sit\amet\consectetur\adipisci\velit\sed\quia\non"
77-
r"\numquam\eius\modi\tempora\incidunt\ut\labore\et\dolore\magnam\aliquam"
78-
r"\quaerat\voluptatem\ut\enim\ad\minima\veniam\quis\nostrum\exercitationem"
79-
r"\ullam\corporis\suscipit\laboriosam\Montréal\☺\my_program.py",
82+
r"c:\lorem\ipsum\quia\dolor\sit\amet\consectetur\adipisci\velit\sed" +
83+
r"\quia\non\numquam\eius\modi\tempora\incidunt\ut\labore\et\dolore" +
84+
r"\magnam\aliquam\quaerat\voluptatem\ut\enim\ad\minima\veniam\quis" +
85+
r"\nostrum\exercitationem\ullam\corporis\suscipit\laboriosam" +
86+
r"\Montréal\☺\my_program.py",
8087
# flat:
81-
"re_et_dolore_magnam_aliquam_quaerat_voluptatem_ut_enim_ad_minima_veniam_quis_"
82-
"nostrum_exercitationem_ullam_corporis_suscipit_laboriosam_Montréal_☺_my_program_py_"
83-
"97eaca41b860faaa1a21349b1f3009bb061cf0a8"
88+
"d_e597dfacb73a23d5_my_program_py"
8489
),
8590
])
8691
def test_flat_rootname(original, flat):

tests/test_html.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def run_coverage(self, covargs=None, htmlargs=None):
5656

5757
def get_html_report_content(self, module):
5858
"""Return the content of the HTML report for `module`."""
59-
filename = module.replace(".", "_").replace("/", "_") + ".html"
59+
filename = flat_rootname(module) + ".html"
6060
filename = os.path.join("htmlcov", filename)
6161
with open(filename) as f:
6262
return f.read()
@@ -617,7 +617,7 @@ def filepath_to_regex(path):
617617
return regex
618618

619619

620-
def compare_html(expected, actual):
620+
def compare_html(expected, actual, extra_scrubs=None):
621621
"""Specialized compare function for our HTML files."""
622622
scrubs = [
623623
(r'/coverage.readthedocs.io/?[-.\w/]*', '/coverage.readthedocs.io/VER'),
@@ -640,6 +640,8 @@ def compare_html(expected, actual):
640640
if env.WINDOWS:
641641
# For file paths...
642642
scrubs += [(r"\\", "/")]
643+
if extra_scrubs:
644+
scrubs += extra_scrubs
643645
compare(expected, actual, file_pattern="*.html", scrubs=scrubs)
644646

645647

@@ -897,7 +899,12 @@ def test_other(self):
897899
for p in glob.glob("out/other/*_other_py.html"):
898900
os.rename(p, "out/other/blah_blah_other_py.html")
899901

900-
compare_html(gold_path("html/other"), "out/other")
902+
compare_html(
903+
gold_path("html/other"), "out/other",
904+
extra_scrubs=[
905+
(r'href="d_[0-9a-z]{16}_', 'href="_TEST_TMPDIR_othersrc_'),
906+
],
907+
)
901908
contains(
902909
"out/other/index.html",
903910
'<a href="here_py.html">here.py</a>',

tests/test_process.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1332,10 +1332,11 @@ def test_accented_directory(self):
13321332
# The HTML report uses ascii-encoded HTML entities.
13331333
out = self.run_command("coverage html")
13341334
assert out == ""
1335-
self.assert_exists("htmlcov/\xe2_accented_py.html")
1335+
self.assert_exists("htmlcov/d_5786906b6f0ffeb4_accented_py.html")
13361336
with open("htmlcov/index.html") as indexf:
13371337
index = indexf.read()
1338-
assert '<a href="&#226;_accented_py.html">&#226;%saccented.py</a>' % os.sep in index
1338+
expected = '<a href="d_5786906b6f0ffeb4_accented_py.html">&#226;%saccented.py</a>'
1339+
assert expected % os.sep in index
13391340

13401341
# The XML report is always UTF8-encoded.
13411342
out = self.run_command("coverage xml")

0 commit comments

Comments
 (0)