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