1 | """Plugins to extract coverage data from various formats.""" |
||
2 | |||
3 | 1 | import os |
|
4 | 1 | from abc import ABCMeta, abstractmethod |
|
5 | 1 | import time |
|
6 | 1 | import webbrowser |
|
7 | 1 | import logging |
|
8 | |||
9 | 1 | import coverage |
|
10 | 1 | from six import with_metaclass |
|
11 | |||
12 | 1 | from .cache import Cache |
|
13 | |||
14 | |||
15 | 1 | log = logging.getLogger(__name__) |
|
16 | 1 | cache = Cache() |
|
17 | |||
18 | |||
19 | class BasePlugin(with_metaclass(ABCMeta)): # pragma: no cover (abstract class) |
||
0 ignored issues
–
show
|
|||
20 | """Base class for coverage plugins.""" |
||
21 | |||
22 | @abstractmethod |
||
23 | def matches(self, cwd): |
||
24 | """Determine if the current directory contains coverage data. |
||
25 | |||
26 | :return bool: Indicates that the current directory should be processed. |
||
27 | |||
28 | """ |
||
29 | |||
30 | @abstractmethod |
||
31 | def get_coverage(self, cwd): |
||
32 | """Extract the coverage data from the current directory. |
||
33 | |||
34 | :return float: Percentage of lines covered. |
||
35 | |||
36 | """ |
||
37 | |||
38 | @abstractmethod |
||
39 | def get_report(self, cwd): |
||
40 | """Get the path to the coverage report. |
||
41 | |||
42 | :return str: Path to coverage report or `None` if not available. |
||
43 | |||
44 | """ |
||
45 | |||
46 | |||
47 | 1 | def get_coverage(cwd=None): |
|
48 | """Extract the current coverage data.""" |
||
49 | 1 | cwd = cwd or os.getcwd() |
|
50 | |||
51 | 1 | plugin = _find_plugin(cwd) |
|
52 | 1 | percentage = plugin.get_coverage(cwd) |
|
53 | |||
54 | 1 | return round(percentage, 1) |
|
55 | |||
56 | |||
57 | 1 | def launch_report(cwd=None): |
|
58 | """Open the generated coverage report in a web browser.""" |
||
59 | 1 | cwd = cwd or os.getcwd() |
|
60 | |||
61 | 1 | plugin = _find_plugin(cwd, allow_missing=True) |
|
62 | |||
63 | 1 | if plugin: |
|
64 | path = plugin.get_report(cwd) |
||
65 | |||
66 | if path: |
||
67 | if _launched_recently(path): |
||
68 | log.debug("Already launched: %s", path) |
||
69 | else: |
||
70 | log.info("Launching report: %s", path) |
||
71 | webbrowser.open("file://" + path, new=2, autoraise=True) |
||
72 | |||
73 | |||
74 | 1 | def _find_plugin(cwd, allow_missing=False): |
|
75 | """Find an return a matching coverage plugin.""" |
||
76 | 1 | for cls in BasePlugin.__subclasses__(): # pylint: disable=no-member |
|
77 | 1 | plugin = cls() |
|
78 | 1 | if plugin.matches(cwd): |
|
79 | 1 | return plugin |
|
80 | |||
81 | 1 | msg = "No coverage data found: {}".format(cwd) |
|
82 | 1 | log.info(msg) |
|
83 | |||
84 | 1 | if allow_missing: |
|
85 | 1 | return None |
|
86 | |||
87 | raise RuntimeError(msg + '.') |
||
88 | |||
89 | |||
90 | 1 | def _launched_recently(path): |
|
91 | now = time.time() |
||
92 | then = cache.get(path, default=0) |
||
93 | cache.set(path, now) |
||
94 | return now - then > 60 * 60 # 1 hour |
||
95 | |||
96 | |||
97 | 1 | class CoveragePy(BasePlugin): |
|
98 | """Coverage extractor for the coverage.py format.""" |
||
99 | |||
100 | 1 | def matches(self, cwd): |
|
101 | 1 | return '.coverage' in os.listdir(cwd) |
|
102 | |||
103 | 1 | def get_coverage(self, cwd): |
|
104 | 1 | os.chdir(cwd) |
|
105 | |||
106 | 1 | cov = coverage.Coverage() |
|
0 ignored issues
–
show
|
|||
107 | 1 | cov.load() |
|
108 | |||
109 | 1 | with open(os.devnull, 'w') as ignore: |
|
110 | 1 | total = cov.report(file=ignore) |
|
111 | |||
112 | 1 | return total |
|
113 | |||
114 | 1 | def get_report(self, cwd): |
|
115 | path = os.path.join(cwd, 'htmlcov', 'index.html') |
||
116 | |||
117 | if os.path.isfile(path): |
||
118 | log.info("Found coverage report: %s", path) |
||
119 | return path |
||
120 | |||
121 | log.info("No coverage report found: %s", cwd) |
||
122 | return None |
||
123 |
Abstract classes which are used only once can usually be inlined into the class which already uses this abstract class.