diff --git a/CHANGES.rst b/CHANGES.rst index 2fd7447c7..7e6e1be7d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -29,6 +29,8 @@ Unreleased - Using ``--format=total`` will write a single total number to the output. This can be useful for making badges or writing status updates. +- TODO: implicit path mapping during reporting. + - Combining data files with ``coverage combine`` now quickly hashes the data files to skip files that provide no new information. This can reduce the time needed. Many details affect the results, but for coverage.py's own test diff --git a/coverage/control.py b/coverage/control.py index 2e58ad85c..c0497478b 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -733,6 +733,18 @@ def save(self): data = self.get_data() data.write() + def _make_aliases(self): + """Create a PathAliases from our configuration.""" + aliases = PathAliases( + debugfn=(self._debug.write if self._debug.should("pathmap") else None), + relative=self.config.relative_files, + ) + for paths in self.config.paths.values(): + result = paths[0] + for pattern in paths[1:]: + aliases.add(pattern, result) + return aliases + def combine(self, data_paths=None, strict=False, keep=False): """Combine together a number of similarly-named coverage data files. @@ -764,18 +776,9 @@ def combine(self, data_paths=None, strict=False, keep=False): self._post_init() self.get_data() - aliases = PathAliases( - debugfn=(self._debug.write if self._debug.should("pathmap") else None), - relative=self.config.relative_files, - ) - for paths in self.config.paths.values(): - result = paths[0] - for pattern in paths[1:]: - aliases.add(pattern, result) - combine_parallel_data( self._data, - aliases=aliases, + aliases=self._make_aliases(), data_paths=data_paths, strict=strict, keep=keep, @@ -925,6 +928,13 @@ def _get_file_reporters(self, morfs=None): file_reporters = [self._get_file_reporter(morf) for morf in morfs] return file_reporters + def _prepare_data_for_reporting(self): + """Re-map data before reporting, to get implicit 'combine' behavior.""" + if self.config.paths: + mapped_data = CoverageData(warn=self._warn, debug=self._debug, no_disk=True) + mapped_data.update(self._data, aliases=self._make_aliases()) + self._data = mapped_data + def report( self, morfs=None, @@ -990,6 +1000,7 @@ def report( The `format` parameter. """ + self._prepare_data_for_reporting() with override_config( self, ignore_errors=ignore_errors, @@ -1034,6 +1045,7 @@ def annotate( print("The annotate command will be removed in a future version.") print("Get in touch if you still use it: ned@nedbatchelder.com") + self._prepare_data_for_reporting() with override_config( self, ignore_errors=ignore_errors, @@ -1083,6 +1095,7 @@ def html_report( changing the files in the report folder. """ + self._prepare_data_for_reporting() with override_config( self, ignore_errors=ignore_errors, @@ -1123,6 +1136,7 @@ def xml_report( Returns a float, the total percentage covered. """ + self._prepare_data_for_reporting() with override_config( self, ignore_errors=ignore_errors, @@ -1157,6 +1171,7 @@ def json_report( .. versionadded:: 5.0 """ + self._prepare_data_for_reporting() with override_config( self, ignore_errors=ignore_errors, @@ -1187,6 +1202,7 @@ def lcov_report( .. versionadded:: 6.3 """ + self._prepare_data_for_reporting() with override_config( self, ignore_errors=ignore_errors, diff --git a/coverage/sqldata.py b/coverage/sqldata.py index ea6b1199f..68663715c 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -648,7 +648,12 @@ def update(self, other_data, aliases=None): "inner join file on file.id = line_bits.file_id " + "inner join context on context.id = line_bits.context_id" ) - lines = {(files[path], context): numbits for (path, context, numbits) in cur} + lines = {} + for path, context, numbits in cur: + key = (files[path], context) + if key in lines: + numbits = numbits_union(lines[key], numbits) + lines[key] = numbits cur.close() # Get tracer data. diff --git a/tests/coveragetest.py b/tests/coveragetest.py index 54ae4eb42..56e788533 100644 --- a/tests/coveragetest.py +++ b/tests/coveragetest.py @@ -219,11 +219,14 @@ def check_coverage( return cov - def make_data_file(self, basename=None, suffix=None, lines=None, file_tracers=None): + def make_data_file(self, basename=None, suffix=None, lines=None, arcs=None, file_tracers=None): """Write some data into a coverage data file.""" data = coverage.CoverageData(basename=basename, suffix=suffix) + assert lines is None or arcs is None if lines: data.add_lines(lines) + if arcs: + data.add_arcs(arcs) if file_tracers: data.add_file_tracers(file_tracers) data.write() diff --git a/tests/test_api.py b/tests/test_api.py index 195452323..970800512 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -23,7 +23,8 @@ from coverage.misc import import_local_file from tests.coveragetest import CoverageTest, TESTS_DIR, UsingModulesMixin -from tests.helpers import assert_count_equal, assert_coverage_warnings +from tests.goldtest import contains, doesnt_contain +from tests.helpers import arcz_to_arcs, assert_count_equal, assert_coverage_warnings from tests.helpers import change_dir, nice_file, os_sep BAD_SQLITE_REGEX = r"file( is encrypted or)? is not a database" @@ -1456,3 +1457,145 @@ def test_combine_parallel_data_keep(self): # After combining, the .coverage file & the original combined file should still be there. self.assert_exists(".coverage") self.assert_file_count(".coverage.*", 2) + + +class ReportMapsPathsTest(CoverageTest): + """Check that reporting implicitly maps paths.""" + + def make_files(self, data, settings=False): + """Create the test files we need for line coverage.""" + src = """\ + if VER == 1: + print("line 2") + if VER == 2: + print("line 4") + if VER == 3: + print("line 6") + """ + self.make_file("src/program.py", src) + self.make_file("ver1/program.py", src) + self.make_file("ver2/program.py", src) + + if data == "line": + self.make_data_file( + lines={ + abs_file("ver1/program.py"): [1, 2, 3, 5], + abs_file("ver2/program.py"): [1, 3, 4, 5], + } + ) + else: + self.make_data_file( + arcs={ + abs_file("ver1/program.py"): arcz_to_arcs(".1 12 23 35 5."), + abs_file("ver2/program.py"): arcz_to_arcs(".1 13 34 45 5."), + } + ) + + if settings: + self.make_file(".coveragerc", """\ + [paths] + source = + src + ver1 + ver2 + """) + + def test_map_paths_during_line_report_without_setting(self): + self.make_files(data="line") + cov = coverage.Coverage() + cov.load() + cov.report(show_missing=True) + expected = textwrap.dedent(os_sep("""\ + Name Stmts Miss Cover Missing + ----------------------------------------------- + ver1/program.py 6 2 67% 4, 6 + ver2/program.py 6 2 67% 2, 6 + ----------------------------------------------- + TOTAL 12 4 67% + """)) + assert expected == self.stdout() + + def test_map_paths_during_line_report(self): + self.make_files(data="line", settings=True) + cov = coverage.Coverage() + cov.load() + cov.report(show_missing=True) + expected = textwrap.dedent(os_sep("""\ + Name Stmts Miss Cover Missing + ---------------------------------------------- + src/program.py 6 1 83% 6 + ---------------------------------------------- + TOTAL 6 1 83% + """)) + assert expected == self.stdout() + + def test_map_paths_during_branch_report_without_setting(self): + self.make_files(data="arcs") + cov = coverage.Coverage(branch=True) + cov.load() + cov.report(show_missing=True) + expected = textwrap.dedent(os_sep("""\ + Name Stmts Miss Branch BrPart Cover Missing + ------------------------------------------------------------- + ver1/program.py 6 2 6 3 58% 1->3, 4, 6 + ver2/program.py 6 2 6 3 58% 2, 3->5, 6 + ------------------------------------------------------------- + TOTAL 12 4 12 6 58% + """)) + assert expected == self.stdout() + + def test_map_paths_during_branch_report(self): + self.make_files(data="arcs", settings=True) + cov = coverage.Coverage(branch=True) + cov.load() + cov.report(show_missing=True) + expected = textwrap.dedent(os_sep("""\ + Name Stmts Miss Branch BrPart Cover Missing + ------------------------------------------------------------ + src/program.py 6 1 6 1 83% 6 + ------------------------------------------------------------ + TOTAL 6 1 6 1 83% + """)) + assert expected == self.stdout() + + def test_map_paths_during_annotate(self): + self.make_files(data="line", settings=True) + cov = coverage.Coverage() + cov.load() + cov.annotate() + self.assert_exists(os_sep("src/program.py,cover")) + self.assert_doesnt_exist(os_sep("ver1/program.py,cover")) + self.assert_doesnt_exist(os_sep("ver2/program.py,cover")) + + def test_map_paths_during_html_report(self): + self.make_files(data="line", settings=True) + cov = coverage.Coverage() + cov.load() + cov.html_report() + contains("htmlcov/index.html", os_sep("src/program.py")) + doesnt_contain("htmlcov/index.html", os_sep("ver1/program.py"), os_sep("ver2/program.py")) + + def test_map_paths_during_xml_report(self): + self.make_files(data="line", settings=True) + cov = coverage.Coverage() + cov.load() + cov.xml_report() + print(open("coverage.xml").read()) + contains("coverage.xml", os_sep("src/program.py")) + doesnt_contain("coverage.xml", os_sep("ver1/program.py"), os_sep("ver2/program.py")) + + def test_map_paths_during_json_report(self): + self.make_files(data="line", settings=True) + cov = coverage.Coverage() + cov.load() + cov.json_report() + contains("coverage.json", os_sep("src/program.py")) + doesnt_contain("coverage.json", os_sep("ver1/program.py"), os_sep("ver2/program.py")) + + def test_map_paths_during_lcov_report(self): + self.make_files(data="line", settings=True) + cov = coverage.Coverage() + cov.load() + cov.lcov_report() + contains("coverage.lcov", os_sep("src/program.py")) + doesnt_contain("coverage.lcov", os_sep("ver1/program.py"), os_sep("ver2/program.py"))