Skip to content

Commit 1a616da

Browse files
committed
fix: remapping paths during combining needs to follow relative_files=True. #1147
1 parent 2f5c7ae commit 1a616da

File tree

5 files changed

+112
-79
lines changed

5 files changed

+112
-79
lines changed

CHANGES.rst

+5
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ This list is detailed and covers changes in each pre-release version.
2222
Unreleased
2323
----------
2424

25+
- Fix: When remapping file paths through the ``[paths]`` setting while
26+
combining, the ``[run] relative_files`` setting was ignored, resulting in
27+
absolute paths for remapped file names (`issue 1147`_). This is now fixed.
28+
2529
- Fix: Complex conditionals over excluded lines could have incorrectly reported
2630
a missing branch (`issue 1271`_). This is now fixed.
2731

@@ -33,6 +37,7 @@ Unreleased
3337
I'd rather not "fix" unsupported interfaces, it's actually nicer with a
3438
default value.
3539

40+
.. _issue 1147: https://github.com/nedbat/coveragepy/issues/1147
3641
.. _issue 1271: https://github.com/nedbat/coveragepy/issues/1271
3742
.. _issue 1273: https://github.com/nedbat/coveragepy/issues/1273
3843

coverage/control.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -706,7 +706,7 @@ def combine(self, data_paths=None, strict=False, keep=False):
706706

707707
aliases = None
708708
if self.config.paths:
709-
aliases = PathAliases()
709+
aliases = PathAliases(relative=self.config.relative_files)
710710
for paths in self.config.paths.values():
711711
result = paths[0]
712712
for pattern in paths[1:]:

coverage/files.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -326,11 +326,13 @@ class PathAliases:
326326
map a path through those aliases to produce a unified path.
327327
328328
"""
329-
def __init__(self):
329+
def __init__(self, relative=False):
330330
self.aliases = []
331+
self.relative = relative
331332

332333
def pprint(self): # pragma: debugging
333334
"""Dump the important parts of the PathAliases, for debugging."""
335+
print(f"Aliases (relative={self.relative}):")
334336
for regex, result in self.aliases:
335337
print(f"{regex.pattern!r} --> {result!r}")
336338

@@ -393,7 +395,8 @@ def map(self, path):
393395
if m:
394396
new = path.replace(m.group(0), result)
395397
new = new.replace(sep(path), sep(result))
396-
new = canonical_filename(new)
398+
if not self.relative:
399+
new = canonical_filename(new)
397400
return new
398401
return path
399402

tests/test_api.py

+18-9
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
from coverage.misc import import_local_file
2424

2525
from tests.coveragetest import CoverageTest, TESTS_DIR, UsingModulesMixin
26-
from tests.helpers import assert_count_equal, assert_coverage_warnings, change_dir, nice_file
26+
from tests.helpers import assert_count_equal, assert_coverage_warnings
27+
from tests.helpers import change_dir, nice_file, os_sep
2728

2829

2930
class ApiTest(CoverageTest):
@@ -1151,16 +1152,19 @@ def test_moving_stuff_with_relative(self):
11511152
assert res == 100
11521153

11531154
def test_combine_relative(self):
1154-
self.make_file("dir1/foo.py", "a = 1")
1155-
self.make_file("dir1/.coveragerc", """\
1155+
self.make_file("foo.py", """\
1156+
import mod
1157+
a = 1
1158+
""")
1159+
self.make_file("lib/mod/__init__.py", "x = 1")
1160+
self.make_file(".coveragerc", """\
11561161
[run]
11571162
relative_files = true
11581163
""")
1159-
with change_dir("dir1"):
1160-
cov = coverage.Coverage(source=["."], data_suffix=True)
1161-
self.start_import_stop(cov, "foo")
1162-
cov.save()
1163-
shutil.move(glob.glob(".coverage.*")[0], "..")
1164+
sys.path.append("lib")
1165+
cov = coverage.Coverage(source=["."], data_suffix=True)
1166+
self.start_import_stop(cov, "foo")
1167+
cov.save()
11641168

11651169
self.make_file("dir2/bar.py", "a = 1")
11661170
self.make_file("dir2/.coveragerc", """\
@@ -1176,17 +1180,22 @@ def test_combine_relative(self):
11761180
self.make_file(".coveragerc", """\
11771181
[run]
11781182
relative_files = true
1183+
[paths]
1184+
source =
1185+
modsrc
1186+
*/mod
11791187
""")
11801188
cov = coverage.Coverage()
11811189
cov.combine()
11821190
cov.save()
11831191

11841192
self.make_file("foo.py", "a = 1")
11851193
self.make_file("bar.py", "a = 1")
1194+
self.make_file("modsrc/__init__.py", "x = 1")
11861195

11871196
cov = coverage.Coverage()
11881197
cov.load()
11891198
files = cov.get_data().measured_files()
1190-
assert files == {'foo.py', 'bar.py'}
1199+
assert files == {'foo.py', 'bar.py', os_sep('modsrc/__init__.py')}
11911200
res = cov.report()
11921201
assert res == 100

tests/test_files.py

+83-67
Original file line numberDiff line numberDiff line change
@@ -227,66 +227,72 @@ def test_fnmatch_windows_paths(self):
227227
self.assertMatches(fnm, r"dir\foo.py", True)
228228

229229

230+
@pytest.fixture(params=[False, True], name="rel_yn")
231+
def relative_setting(request):
232+
"""Parameterized fixture to choose whether PathAliases is relative or not."""
233+
return request.param
234+
235+
230236
class PathAliasesTest(CoverageTest):
231237
"""Tests for coverage/files.py:PathAliases"""
232238

233239
run_in_temp_dir = False
234240

235-
def assert_mapped(self, aliases, inp, out):
241+
def assert_mapped(self, aliases, inp, out, relative=False):
236242
"""Assert that `inp` mapped through `aliases` produces `out`.
237243
238-
`out` is canonicalized first, since aliases always produce
239-
canonicalized paths.
244+
`out` is canonicalized first, since aliases produce canonicalized
245+
paths by default.
240246
241247
"""
242-
aliases.pprint()
243-
print(inp)
244-
print(out)
245-
assert aliases.map(inp) == files.canonical_filename(out)
248+
mapped = aliases.map(inp)
249+
expected = files.canonical_filename(out) if not relative else out
250+
assert mapped == expected
246251

247252
def assert_unchanged(self, aliases, inp):
248253
"""Assert that `inp` mapped through `aliases` is unchanged."""
249254
assert aliases.map(inp) == inp
250255

251-
def test_noop(self):
252-
aliases = PathAliases()
256+
def test_noop(self, rel_yn):
257+
aliases = PathAliases(relative=rel_yn)
253258
self.assert_unchanged(aliases, '/ned/home/a.py')
254259

255-
def test_nomatch(self):
256-
aliases = PathAliases()
260+
def test_nomatch(self, rel_yn):
261+
aliases = PathAliases(relative=rel_yn)
257262
aliases.add('/home/*/src', './mysrc')
258263
self.assert_unchanged(aliases, '/home/foo/a.py')
259264

260-
def test_wildcard(self):
261-
aliases = PathAliases()
265+
def test_wildcard(self, rel_yn):
266+
aliases = PathAliases(relative=rel_yn)
262267
aliases.add('/ned/home/*/src', './mysrc')
263-
self.assert_mapped(aliases, '/ned/home/foo/src/a.py', './mysrc/a.py')
268+
self.assert_mapped(aliases, '/ned/home/foo/src/a.py', './mysrc/a.py', relative=rel_yn)
264269

265-
aliases = PathAliases()
270+
aliases = PathAliases(relative=rel_yn)
266271
aliases.add('/ned/home/*/src/', './mysrc')
267-
self.assert_mapped(aliases, '/ned/home/foo/src/a.py', './mysrc/a.py')
272+
self.assert_mapped(aliases, '/ned/home/foo/src/a.py', './mysrc/a.py', relative=rel_yn)
268273

269-
def test_no_accidental_match(self):
270-
aliases = PathAliases()
274+
def test_no_accidental_match(self, rel_yn):
275+
aliases = PathAliases(relative=rel_yn)
271276
aliases.add('/home/*/src', './mysrc')
272277
self.assert_unchanged(aliases, '/home/foo/srcetc')
273278

274-
def test_multiple_patterns(self):
275-
aliases = PathAliases()
279+
def test_multiple_patterns(self, rel_yn):
280+
aliases = PathAliases(relative=rel_yn)
276281
aliases.add('/home/*/src', './mysrc')
277282
aliases.add('/lib/*/libsrc', './mylib')
278-
self.assert_mapped(aliases, '/home/foo/src/a.py', './mysrc/a.py')
279-
self.assert_mapped(aliases, '/lib/foo/libsrc/a.py', './mylib/a.py')
280-
281-
def test_cant_have_wildcard_at_end(self):
283+
self.assert_mapped(aliases, '/home/foo/src/a.py', './mysrc/a.py', relative=rel_yn)
284+
self.assert_mapped(aliases, '/lib/foo/libsrc/a.py', './mylib/a.py', relative=rel_yn)
285+
286+
@pytest.mark.parametrize("badpat", [
287+
"/ned/home/*",
288+
"/ned/home/*/",
289+
"/ned/home/*/*/",
290+
])
291+
def test_cant_have_wildcard_at_end(self, badpat):
282292
aliases = PathAliases()
283293
msg = "Pattern must not end with wildcards."
284294
with pytest.raises(CoverageException, match=msg):
285-
aliases.add("/ned/home/*", "fooey")
286-
with pytest.raises(CoverageException, match=msg):
287-
aliases.add("/ned/home/*/", "fooey")
288-
with pytest.raises(CoverageException, match=msg):
289-
aliases.add("/ned/home/*/*/", "fooey")
295+
aliases.add(badpat, "fooey")
290296

291297
def test_no_accidental_munging(self):
292298
aliases = PathAliases()
@@ -295,94 +301,104 @@ def test_no_accidental_munging(self):
295301
self.assert_mapped(aliases, r'c:\Zoo\boo\foo.py', 'src/foo.py')
296302
self.assert_mapped(aliases, r'/home/ned$/foo.py', 'src/foo.py')
297303

298-
def test_paths_are_os_corrected(self):
299-
aliases = PathAliases()
304+
def test_paths_are_os_corrected(self, rel_yn):
305+
aliases = PathAliases(relative=rel_yn)
300306
aliases.add('/home/ned/*/src', './mysrc')
301307
aliases.add(r'c:\ned\src', './mysrc')
302-
self.assert_mapped(aliases, r'C:\Ned\src\sub\a.py', './mysrc/sub/a.py')
308+
self.assert_mapped(aliases, r'C:\Ned\src\sub\a.py', './mysrc/sub/a.py', relative=rel_yn)
303309

304-
aliases = PathAliases()
310+
aliases = PathAliases(relative=rel_yn)
305311
aliases.add('/home/ned/*/src', r'.\mysrc')
306312
aliases.add(r'c:\ned\src', r'.\mysrc')
307-
self.assert_mapped(aliases, r'/home/ned/foo/src/sub/a.py', r'.\mysrc\sub\a.py')
313+
self.assert_mapped(
314+
aliases,
315+
r'/home/ned/foo/src/sub/a.py',
316+
r'.\mysrc\sub\a.py',
317+
relative=rel_yn,
318+
)
308319

309-
def test_windows_on_linux(self):
320+
def test_windows_on_linux(self, rel_yn):
310321
# https://github.com/nedbat/coveragepy/issues/618
311322
lin = "*/project/module/"
312323
win = "*\\project\\module\\"
313324

314325
# Try the paths in both orders.
315326
for paths in [[lin, win], [win, lin]]:
316-
aliases = PathAliases()
327+
aliases = PathAliases(relative=rel_yn)
317328
for path in paths:
318329
aliases.add(path, "project/module")
319330
self.assert_mapped(
320331
aliases,
321332
"C:\\a\\path\\somewhere\\coveragepy_test\\project\\module\\tests\\file.py",
322-
"project/module/tests/file.py"
333+
"project/module/tests/file.py",
334+
relative=rel_yn,
323335
)
324336

325-
def test_linux_on_windows(self):
337+
def test_linux_on_windows(self, rel_yn):
326338
# https://github.com/nedbat/coveragepy/issues/618
327339
lin = "*/project/module/"
328340
win = "*\\project\\module\\"
329341

330342
# Try the paths in both orders.
331343
for paths in [[lin, win], [win, lin]]:
332-
aliases = PathAliases()
344+
aliases = PathAliases(relative=rel_yn)
333345
for path in paths:
334346
aliases.add(path, "project\\module")
335347
self.assert_mapped(
336348
aliases,
337349
"C:/a/path/somewhere/coveragepy_test/project/module/tests/file.py",
338-
"project\\module\\tests\\file.py"
350+
"project\\module\\tests\\file.py",
351+
relative=rel_yn,
339352
)
340353

341-
def test_multiple_wildcard(self):
342-
aliases = PathAliases()
354+
def test_multiple_wildcard(self, rel_yn):
355+
aliases = PathAliases(relative=rel_yn)
343356
aliases.add('/home/jenkins/*/a/*/b/*/django', './django')
344357
self.assert_mapped(
345358
aliases,
346359
'/home/jenkins/xx/a/yy/b/zz/django/foo/bar.py',
347-
'./django/foo/bar.py'
360+
'./django/foo/bar.py',
361+
relative=rel_yn,
348362
)
349363

350-
def test_windows_root_paths(self):
351-
aliases = PathAliases()
364+
def test_windows_root_paths(self, rel_yn):
365+
aliases = PathAliases(relative=rel_yn)
352366
aliases.add('X:\\', '/tmp/src')
353367
self.assert_mapped(
354368
aliases,
355369
"X:\\a\\file.py",
356-
"/tmp/src/a/file.py"
370+
"/tmp/src/a/file.py",
371+
relative=rel_yn,
357372
)
358373
self.assert_mapped(
359374
aliases,
360375
"X:\\file.py",
361-
"/tmp/src/file.py"
376+
"/tmp/src/file.py",
377+
relative=rel_yn,
362378
)
363379

364-
def test_leading_wildcard(self):
365-
aliases = PathAliases()
380+
def test_leading_wildcard(self, rel_yn):
381+
aliases = PathAliases(relative=rel_yn)
366382
aliases.add('*/d1', './mysrc1')
367383
aliases.add('*/d2', './mysrc2')
368-
self.assert_mapped(aliases, '/foo/bar/d1/x.py', './mysrc1/x.py')
369-
self.assert_mapped(aliases, '/foo/bar/d2/y.py', './mysrc2/y.py')
370-
371-
def test_dot(self):
372-
cases = ['.', '..', '../other']
373-
if not env.WINDOWS:
374-
# The root test case was added for the manylinux Docker images,
375-
# and I'm not sure how it should work on Windows, so skip it.
376-
cases += ['/']
377-
for d in cases:
378-
aliases = PathAliases()
379-
aliases.add(d, '/the/source')
380-
the_file = os.path.join(d, 'a.py')
381-
the_file = os.path.expanduser(the_file)
382-
the_file = os.path.abspath(os.path.realpath(the_file))
383-
384-
assert '~' not in the_file # to be sure the test is pure.
385-
self.assert_mapped(aliases, the_file, '/the/source/a.py')
384+
self.assert_mapped(aliases, '/foo/bar/d1/x.py', './mysrc1/x.py', relative=rel_yn)
385+
self.assert_mapped(aliases, '/foo/bar/d2/y.py', './mysrc2/y.py', relative=rel_yn)
386+
387+
# The root test case was added for the manylinux Docker images,
388+
# and I'm not sure how it should work on Windows, so skip it.
389+
cases = [".", "..", "../other"]
390+
if not env.WINDOWS:
391+
cases += ["/"]
392+
@pytest.mark.parametrize("dirname", cases)
393+
def test_dot(self, dirname):
394+
aliases = PathAliases()
395+
aliases.add(dirname, '/the/source')
396+
the_file = os.path.join(dirname, 'a.py')
397+
the_file = os.path.expanduser(the_file)
398+
the_file = os.path.abspath(os.path.realpath(the_file))
399+
400+
assert '~' not in the_file # to be sure the test is pure.
401+
self.assert_mapped(aliases, the_file, '/the/source/a.py')
386402

387403

388404
class FindPythonFilesTest(CoverageTest):

0 commit comments

Comments
 (0)