|
1 """HTML reporting for Coverage.""" |
|
2 |
|
3 import os, re, shutil |
|
4 from . import __version__ # pylint: disable-msg=W0611 |
|
5 from .report import Reporter |
|
6 from .templite import Templite |
|
7 |
|
8 # Disable pylint msg W0612, because a bunch of variables look unused, but |
|
9 # they're accessed in a templite context via locals(). |
|
10 # pylint: disable-msg=W0612 |
|
11 |
|
12 def data_filename(fname): |
|
13 """Return the path to a data file of ours.""" |
|
14 return os.path.join(os.path.split(__file__)[0], fname) |
|
15 |
|
16 def data(fname): |
|
17 """Return the contents of a data file of ours.""" |
|
18 return open(data_filename(fname)).read() |
|
19 |
|
20 |
|
21 class HtmlReporter(Reporter): |
|
22 """HTML reporting.""" |
|
23 |
|
24 def __init__(self, coverage, ignore_errors=False): |
|
25 super(HtmlReporter, self).__init__(coverage, ignore_errors) |
|
26 self.directory = None |
|
27 self.source_tmpl = Templite(data("htmlfiles/pyfile.html"), globals()) |
|
28 |
|
29 self.files = [] |
|
30 |
|
31 def report(self, morfs, directory, omit_prefixes=None): |
|
32 """Generate an HTML report for `morfs`. |
|
33 |
|
34 `morfs` is a list of modules or filenames. `directory` is where to put |
|
35 the HTML files. `omit_prefixes` is a list of strings, prefixes of |
|
36 modules to omit from the report. |
|
37 |
|
38 """ |
|
39 assert directory, "must provide a directory for html reporting" |
|
40 |
|
41 # Process all the files. |
|
42 self.report_files(self.html_file, morfs, directory, omit_prefixes) |
|
43 |
|
44 # Write the index file. |
|
45 self.index_file() |
|
46 |
|
47 # Create the once-per-directory files. |
|
48 shutil.copyfile( |
|
49 data_filename("htmlfiles/style.css"), |
|
50 os.path.join(directory, "style.css") |
|
51 ) |
|
52 shutil.copyfile( |
|
53 data_filename("htmlfiles/jquery-1.3.2.min.js"), |
|
54 os.path.join(directory, "jquery-1.3.2.min.js") |
|
55 ) |
|
56 |
|
57 def html_file(self, cu, statements, excluded, missing): |
|
58 """Generate an HTML file for one source file.""" |
|
59 |
|
60 source = cu.source_file() |
|
61 source_lines = source.readlines() |
|
62 |
|
63 n_lin = len(source_lines) |
|
64 n_stm = len(statements) |
|
65 n_exc = len(excluded) |
|
66 n_mis = len(missing) |
|
67 n_run = n_stm - n_mis |
|
68 if n_stm > 0: |
|
69 pc_cov = 100.0 * n_run / n_stm |
|
70 else: |
|
71 pc_cov = 100.0 |
|
72 |
|
73 # These classes determine which lines are highlighted by default. |
|
74 c_run = " run hide" |
|
75 c_exc = " exc" |
|
76 c_mis = " mis" |
|
77 |
|
78 lines = [] |
|
79 for lineno, line in enumerate(source_lines): |
|
80 lineno += 1 # enum is 0-based, lines are 1-based. |
|
81 |
|
82 |
|
83 css_class = "" |
|
84 if lineno in statements: |
|
85 css_class += " stm" |
|
86 if lineno not in missing and lineno not in excluded: |
|
87 css_class += c_run |
|
88 if lineno in excluded: |
|
89 css_class += c_exc |
|
90 if lineno in missing: |
|
91 css_class += c_mis |
|
92 |
|
93 lineinfo = { |
|
94 'text': line, |
|
95 'number': lineno, |
|
96 'class': css_class.strip() or "pln" |
|
97 } |
|
98 lines.append(lineinfo) |
|
99 |
|
100 # Write the HTML page for this file. |
|
101 html_filename = cu.flat_rootname() + ".html" |
|
102 html_path = os.path.join(self.directory, html_filename) |
|
103 html = spaceless(self.source_tmpl.render(locals())) |
|
104 fhtml = open(html_path, 'w') |
|
105 fhtml.write(html) |
|
106 fhtml.close() |
|
107 |
|
108 # Save this file's information for the index file. |
|
109 self.files.append({ |
|
110 'stm': n_stm, |
|
111 'run': n_run, |
|
112 'exc': n_exc, |
|
113 'mis': n_mis, |
|
114 'pc_cov': pc_cov, |
|
115 'html_filename': html_filename, |
|
116 'cu': cu, |
|
117 }) |
|
118 |
|
119 def index_file(self): |
|
120 """Write the index.html file for this report.""" |
|
121 index_tmpl = Templite(data("htmlfiles/index.html"), globals()) |
|
122 |
|
123 files = self.files |
|
124 |
|
125 total_stm = sum([f['stm'] for f in files]) |
|
126 total_run = sum([f['run'] for f in files]) |
|
127 total_exc = sum([f['exc'] for f in files]) |
|
128 if total_stm: |
|
129 total_cov = 100.0 * total_run / total_stm |
|
130 else: |
|
131 total_cov = 100.0 |
|
132 |
|
133 fhtml = open(os.path.join(self.directory, "index.html"), "w") |
|
134 fhtml.write(index_tmpl.render(locals())) |
|
135 fhtml.close() |
|
136 |
|
137 |
|
138 # Helpers for templates |
|
139 |
|
140 def escape(t): |
|
141 """HTML-escape the text in t.""" |
|
142 return (t |
|
143 # Change all tabs to 4 spaces. |
|
144 .expandtabs(4) |
|
145 # Convert HTML special chars into HTML entities. |
|
146 .replace("&", "&").replace("<", "<").replace(">", ">") |
|
147 .replace("'", "'").replace('"', """) |
|
148 # Convert runs of spaces: " " -> " " |
|
149 .replace(" ", " ") |
|
150 # To deal with odd-length runs, convert the final pair of spaces |
|
151 # so that " " -> " " |
|
152 .replace(" ", " ") |
|
153 ) |
|
154 |
|
155 def not_empty(t): |
|
156 """Make sure HTML content is not completely empty.""" |
|
157 return t or " " |
|
158 |
|
159 def format_pct(p): |
|
160 """Format a percentage value for the HTML reports.""" |
|
161 return "%.0f" % p |
|
162 |
|
163 def spaceless(html): |
|
164 """Squeeze out some annoying extra space from an HTML string. |
|
165 |
|
166 Nicely-formatted templates mean lots of extra space in the result. Get |
|
167 rid of some. |
|
168 |
|
169 """ |
|
170 html = re.sub(">\s+<p ", ">\n<p ", html) |
|
171 return html |