Coverage for vibefpmgui/window/coverage.py: 82%

64 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-11-01 20:51 +0000

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: 26 ↛ 27line 26 didn't jump to line 27 because the condition on line 26 was never true

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: 30 ↛ 31line 30 didn't jump to line 31 because the condition on line 30 was never true

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: 48 ↛ 49line 48 didn't jump to line 49 because the condition on line 48 was never true

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: 54 ↛ 55line 54 didn't jump to line 55 because the condition on line 54 was never true

55 return 

56 if not files: 56 ↛ 57line 56 didn't jump to line 57 because the condition on line 56 was never true

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: 64 ↛ exitline 64 didn't return from function '_coverage_read' because the condition on line 64 was always true

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