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).