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