vibefpmgui.window.coverage

Test coverage: collecting it after a test run, and showing it.

In the Coverage panel and the editors' gutters (see vibefpmgui.coverage).

 1"""Test coverage: collecting it after a test run, and showing it.
 2
 3In the Coverage panel and the editors' gutters (see `vibefpmgui.coverage`).
 4"""
 5
 6from __future__ import annotations
 7
 8import time
 9from pathlib import Path
10
11from .. import coverage as cov
12from ..background import call_in_background
13from ..editor import EditorTab
14from ..project_tree import fpm_folders
15
16
17class CoverageMixin:
18    """Test coverage (a part of `vibefpmgui.main_window.MainWindow`)."""
19
20    def _collect_coverage(self):
21        """After a test run with coverage: gcov reads the counts the tests
22        wrote (in the background), then the Coverage panel and the editors
23        show them."""
24        compiler = self.effective_compiler()[0]
25        gcov = cov.find_gcov(compiler)
26        if gcov is None:
27            self.coverage_panel.show_message(f"Coverage needs gcov (for {compiler}): it isn't on the PATH. "
28                                             "(It comes with gfortran.)")
29            return
30        if self._coverage_collector is not None:
31            return
32        self.coverage_panel.show_message("Collecting coverage…")
33        self._coverage_description = f"{time.strftime('%H:%M')} · {self._build_description()}"
34        collector = self._coverage_collector = cov.Collector(gcov, self.manifest_path.parent / "build")
35        call_in_background(collector.run, lambda result: self._coverage_collected(collector, *result), self)
36
37    def _stop_coverage(self):
38        """Stop gcov if it's still reading the counts (quitting, or another
39        project); its result is then dropped."""
40        collector, self._coverage_collector = self._coverage_collector, None
41        if collector is not None:
42            collector.stop()
43
44    def _coverage_collected(self, collector: cov.Collector, files: dict, error: str):
45        if collector is not self._coverage_collector:
46            return
47        self._coverage_collector = None
48        if error:
49            self.coverage_panel.show_message(f"gcov couldn't read the coverage: {error}")
50            return
51        self._coverage_read(files)
52
53    def _coverage_read(self, files: dict):
54        if not self.manifest_path:
55            return
56        if not files:
57            self.coverage_panel.show_message("The tests wrote no coverage counts: was the project built with "
58                                             "gfortran? (Coverage works with gfortran's --coverage.)")
59            return
60        self._apply_coverage(files)
61        tests = [Path(folder) for folder, role in fpm_folders(self.manifest_path).items() if role == "test"]
62        self.coverage_panel.show_report(files, self.manifest_path.parent, self._coverage_description, tests)
63        hit, found = cov.totals(self.coverage_panel.shown_files())["lines"]
64        if found:
65            self.statusBar().showMessage(f"Coverage: {100 * hit / found:.1f}% of the lines with code ran "
66                                         "(see the Coverage panel)", 8000)
67
68    def _apply_coverage(self, files: dict):
69        """The coverage shown in the editors' gutters."""
70        self.coverage = files
71        for editor in self.editors():
72            self._show_coverage(editor)
73
74    def _go_to_missed(self, editor: EditorTab, forward: bool):
75        if not editor.editor.go_to_missed(forward):
76            self.statusBar().showMessage("No missed lines in this file" if editor.editor.coverage else
77                                         "No coverage for this file: turn on Build options → Coverage "
78                                         "and run the tests", 5000)
79
80    def _show_coverage(self, editor: EditorTab):
81        found = self._coverage_for(editor)
82        editor.editor.set_coverage(found.lines if found else None, found.line_branches if found else None)
83
84    def _coverage_for(self, editor: EditorTab) -> cov.FileCoverage | None:
85        """A file's coverage for its gutter, if its text is still what the
86        tests ran: not changed on disk since the run started, and no unsaved
87        changes (its line numbers could differ: the bars would be on the
88        wrong lines). An open file edited later drops its bars itself."""
89        found = self.coverage.get(editor.path)
90        if found is None or editor.is_dirty():
91            return None
92        try:
93            if editor.path.stat().st_mtime > self._coverage_started:
94                return None
95        except OSError:
96            return None
97        return found
class CoverageMixin:
18class CoverageMixin:
19    """Test coverage (a part of `vibefpmgui.main_window.MainWindow`)."""
20
21    def _collect_coverage(self):
22        """After a test run with coverage: gcov reads the counts the tests
23        wrote (in the background), then the Coverage panel and the editors
24        show them."""
25        compiler = self.effective_compiler()[0]
26        gcov = cov.find_gcov(compiler)
27        if gcov is None:
28            self.coverage_panel.show_message(f"Coverage needs gcov (for {compiler}): it isn't on the PATH. "
29                                             "(It comes with gfortran.)")
30            return
31        if self._coverage_collector is not None:
32            return
33        self.coverage_panel.show_message("Collecting coverage…")
34        self._coverage_description = f"{time.strftime('%H:%M')} · {self._build_description()}"
35        collector = self._coverage_collector = cov.Collector(gcov, self.manifest_path.parent / "build")
36        call_in_background(collector.run, lambda result: self._coverage_collected(collector, *result), self)
37
38    def _stop_coverage(self):
39        """Stop gcov if it's still reading the counts (quitting, or another
40        project); its result is then dropped."""
41        collector, self._coverage_collector = self._coverage_collector, None
42        if collector is not None:
43            collector.stop()
44
45    def _coverage_collected(self, collector: cov.Collector, files: dict, error: str):
46        if collector is not self._coverage_collector:
47            return
48        self._coverage_collector = None
49        if error:
50            self.coverage_panel.show_message(f"gcov couldn't read the coverage: {error}")
51            return
52        self._coverage_read(files)
53
54    def _coverage_read(self, files: dict):
55        if not self.manifest_path:
56            return
57        if not files:
58            self.coverage_panel.show_message("The tests wrote no coverage counts: was the project built with "
59                                             "gfortran? (Coverage works with gfortran's --coverage.)")
60            return
61        self._apply_coverage(files)
62        tests = [Path(folder) for folder, role in fpm_folders(self.manifest_path).items() if role == "test"]
63        self.coverage_panel.show_report(files, self.manifest_path.parent, self._coverage_description, tests)
64        hit, found = cov.totals(self.coverage_panel.shown_files())["lines"]
65        if found:
66            self.statusBar().showMessage(f"Coverage: {100 * hit / found:.1f}% of the lines with code ran "
67                                         "(see the Coverage panel)", 8000)
68
69    def _apply_coverage(self, files: dict):
70        """The coverage shown in the editors' gutters."""
71        self.coverage = files
72        for editor in self.editors():
73            self._show_coverage(editor)
74
75    def _go_to_missed(self, editor: EditorTab, forward: bool):
76        if not editor.editor.go_to_missed(forward):
77            self.statusBar().showMessage("No missed lines in this file" if editor.editor.coverage else
78                                         "No coverage for this file: turn on Build options → Coverage "
79                                         "and run the tests", 5000)
80
81    def _show_coverage(self, editor: EditorTab):
82        found = self._coverage_for(editor)
83        editor.editor.set_coverage(found.lines if found else None, found.line_branches if found else None)
84
85    def _coverage_for(self, editor: EditorTab) -> cov.FileCoverage | None:
86        """A file's coverage for its gutter, if its text is still what the
87        tests ran: not changed on disk since the run started, and no unsaved
88        changes (its line numbers could differ: the bars would be on the
89        wrong lines). An open file edited later drops its bars itself."""
90        found = self.coverage.get(editor.path)
91        if found is None or editor.is_dirty():
92            return None
93        try:
94            if editor.path.stat().st_mtime > self._coverage_started:
95                return None
96        except OSError:
97            return None
98        return found

Test coverage (a part of vibefpmgui.main_window.MainWindow).