Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(214)

Side by Side Diff: third_party/coverage-3.6/coverage/html.py

Issue 14988009: First cut of testing infrastructure for recipes. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/build
Patch Set: nitfixen Created 7 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
1 """HTML reporting for Coverage."""
2
3 import os, re, shutil, sys
4
5 import coverage
6 from coverage.backward import pickle
7 from coverage.misc import CoverageException, Hasher
8 from coverage.phystokens import source_token_lines, source_encoding
9 from coverage.report import Reporter
10 from coverage.results import Numbers
11 from coverage.templite import Templite
12
13 # Disable pylint msg W0612, because a bunch of variables look unused, but
14 # they're accessed in a Templite context via locals().
15 # pylint: disable=W0612
16
17 def data_filename(fname):
18 """Return the path to a data file of ours."""
19 return os.path.join(os.path.split(__file__)[0], fname)
20
21 def data(fname):
22 """Return the contents of a data file of ours."""
23 data_file = open(data_filename(fname))
24 try:
25 return data_file.read()
26 finally:
27 data_file.close()
28
29
30 class HtmlReporter(Reporter):
31 """HTML reporting."""
32
33 # These files will be copied from the htmlfiles dir to the output dir.
34 STATIC_FILES = [
35 "style.css",
36 "jquery-1.4.3.min.js",
37 "jquery.hotkeys.js",
38 "jquery.isonscreen.js",
39 "jquery.tablesorter.min.js",
40 "coverage_html.js",
41 "keybd_closed.png",
42 "keybd_open.png",
43 ]
44
45 def __init__(self, cov, config):
46 super(HtmlReporter, self).__init__(cov, config)
47 self.directory = None
48 self.template_globals = {
49 'escape': escape,
50 'title': self.config.html_title,
51 '__url__': coverage.__url__,
52 '__version__': coverage.__version__,
53 }
54 self.source_tmpl = Templite(
55 data("htmlfiles/pyfile.html"), self.template_globals
56 )
57
58 self.coverage = cov
59
60 self.files = []
61 self.arcs = self.coverage.data.has_arcs()
62 self.status = HtmlStatus()
63 self.extra_css = None
64 self.totals = Numbers()
65
66 def report(self, morfs):
67 """Generate an HTML report for `morfs`.
68
69 `morfs` is a list of modules or filenames.
70
71 """
72 assert self.config.html_dir, "must give a directory for html reporting"
73
74 # Read the status data.
75 self.status.read(self.config.html_dir)
76
77 # Check that this run used the same settings as the last run.
78 m = Hasher()
79 m.update(self.config)
80 these_settings = m.digest()
81 if self.status.settings_hash() != these_settings:
82 self.status.reset()
83 self.status.set_settings_hash(these_settings)
84
85 # The user may have extra CSS they want copied.
86 if self.config.extra_css:
87 self.extra_css = os.path.basename(self.config.extra_css)
88
89 # Process all the files.
90 self.report_files(self.html_file, morfs, self.config.html_dir)
91
92 if not self.files:
93 raise CoverageException("No data to report.")
94
95 # Write the index file.
96 self.index_file()
97
98 self.make_local_static_report_files()
99
100 return self.totals.pc_covered
101
102 def make_local_static_report_files(self):
103 """Make local instances of static files for HTML report."""
104 # The files we provide must always be copied.
105 for static in self.STATIC_FILES:
106 shutil.copyfile(
107 data_filename("htmlfiles/" + static),
108 os.path.join(self.directory, static)
109 )
110
111 # The user may have extra CSS they want copied.
112 if self.extra_css:
113 shutil.copyfile(
114 self.config.extra_css,
115 os.path.join(self.directory, self.extra_css)
116 )
117
118 def write_html(self, fname, html):
119 """Write `html` to `fname`, properly encoded."""
120 fout = open(fname, "wb")
121 try:
122 fout.write(html.encode('ascii', 'xmlcharrefreplace'))
123 finally:
124 fout.close()
125
126 def file_hash(self, source, cu):
127 """Compute a hash that changes if the file needs to be re-reported."""
128 m = Hasher()
129 m.update(source)
130 self.coverage.data.add_to_hash(cu.filename, m)
131 return m.digest()
132
133 def html_file(self, cu, analysis):
134 """Generate an HTML file for one source file."""
135 source_file = cu.source_file()
136 try:
137 source = source_file.read()
138 finally:
139 source_file.close()
140
141 # Find out if the file on disk is already correct.
142 flat_rootname = cu.flat_rootname()
143 this_hash = self.file_hash(source, cu)
144 that_hash = self.status.file_hash(flat_rootname)
145 if this_hash == that_hash:
146 # Nothing has changed to require the file to be reported again.
147 self.files.append(self.status.index_info(flat_rootname))
148 return
149
150 self.status.set_file_hash(flat_rootname, this_hash)
151
152 # If need be, determine the encoding of the source file. We use it
153 # later to properly write the HTML.
154 if sys.version_info < (3, 0):
155 encoding = source_encoding(source)
156 # Some UTF8 files have the dreaded UTF8 BOM. If so, junk it.
157 if encoding.startswith("utf-8") and source[:3] == "\xef\xbb\xbf":
158 source = source[3:]
159 encoding = "utf-8"
160
161 # Get the numbers for this file.
162 nums = analysis.numbers
163
164 missing_branch_arcs = analysis.missing_branch_arcs()
165 arcs = self.arcs
166
167 # These classes determine which lines are highlighted by default.
168 c_run = "run hide_run"
169 c_exc = "exc"
170 c_mis = "mis"
171 c_par = "par " + c_run
172
173 lines = []
174
175 for lineno, line in enumerate(source_token_lines(source)):
176 lineno += 1 # 1-based line numbers.
177 # Figure out how to mark this line.
178 line_class = []
179 annotate_html = ""
180 annotate_title = ""
181 if lineno in analysis.statements:
182 line_class.append("stm")
183 if lineno in analysis.excluded:
184 line_class.append(c_exc)
185 elif lineno in analysis.missing:
186 line_class.append(c_mis)
187 elif self.arcs and lineno in missing_branch_arcs:
188 line_class.append(c_par)
189 annlines = []
190 for b in missing_branch_arcs[lineno]:
191 if b < 0:
192 annlines.append("exit")
193 else:
194 annlines.append(str(b))
195 annotate_html = "&nbsp;&nbsp; ".join(annlines)
196 if len(annlines) > 1:
197 annotate_title = "no jumps to these line numbers"
198 elif len(annlines) == 1:
199 annotate_title = "no jump to this line number"
200 elif lineno in analysis.statements:
201 line_class.append(c_run)
202
203 # Build the HTML for the line
204 html = []
205 for tok_type, tok_text in line:
206 if tok_type == "ws":
207 html.append(escape(tok_text))
208 else:
209 tok_html = escape(tok_text) or '&nbsp;'
210 html.append(
211 "<span class='%s'>%s</span>" % (tok_type, tok_html)
212 )
213
214 lines.append({
215 'html': ''.join(html),
216 'number': lineno,
217 'class': ' '.join(line_class) or "pln",
218 'annotate': annotate_html,
219 'annotate_title': annotate_title,
220 })
221
222 # Write the HTML page for this file.
223 html_filename = flat_rootname + ".html"
224 html_path = os.path.join(self.directory, html_filename)
225 extra_css = self.extra_css
226
227 html = spaceless(self.source_tmpl.render(locals()))
228 if sys.version_info < (3, 0):
229 html = html.decode(encoding)
230 self.write_html(html_path, html)
231
232 # Save this file's information for the index file.
233 index_info = {
234 'nums': nums,
235 'html_filename': html_filename,
236 'name': cu.name,
237 }
238 self.files.append(index_info)
239 self.status.set_index_info(flat_rootname, index_info)
240
241 def index_file(self):
242 """Write the index.html file for this report."""
243 index_tmpl = Templite(
244 data("htmlfiles/index.html"), self.template_globals
245 )
246
247 files = self.files
248 arcs = self.arcs
249
250 self.totals = totals = sum([f['nums'] for f in files])
251 extra_css = self.extra_css
252
253 html = index_tmpl.render(locals())
254 if sys.version_info < (3, 0):
255 html = html.decode("utf-8")
256 self.write_html(
257 os.path.join(self.directory, "index.html"),
258 html
259 )
260
261 # Write the latest hashes for next time.
262 self.status.write(self.directory)
263
264
265 class HtmlStatus(object):
266 """The status information we keep to support incremental reporting."""
267
268 STATUS_FILE = "status.dat"
269 STATUS_FORMAT = 1
270
271 def __init__(self):
272 self.reset()
273
274 def reset(self):
275 """Initialize to empty."""
276 self.settings = ''
277 self.files = {}
278
279 def read(self, directory):
280 """Read the last status in `directory`."""
281 usable = False
282 try:
283 status_file = os.path.join(directory, self.STATUS_FILE)
284 fstatus = open(status_file, "rb")
285 try:
286 status = pickle.load(fstatus)
287 finally:
288 fstatus.close()
289 except (IOError, ValueError):
290 usable = False
291 else:
292 usable = True
293 if status['format'] != self.STATUS_FORMAT:
294 usable = False
295 elif status['version'] != coverage.__version__:
296 usable = False
297
298 if usable:
299 self.files = status['files']
300 self.settings = status['settings']
301 else:
302 self.reset()
303
304 def write(self, directory):
305 """Write the current status to `directory`."""
306 status_file = os.path.join(directory, self.STATUS_FILE)
307 status = {
308 'format': self.STATUS_FORMAT,
309 'version': coverage.__version__,
310 'settings': self.settings,
311 'files': self.files,
312 }
313 fout = open(status_file, "wb")
314 try:
315 pickle.dump(status, fout)
316 finally:
317 fout.close()
318
319 def settings_hash(self):
320 """Get the hash of the coverage.py settings."""
321 return self.settings
322
323 def set_settings_hash(self, settings):
324 """Set the hash of the coverage.py settings."""
325 self.settings = settings
326
327 def file_hash(self, fname):
328 """Get the hash of `fname`'s contents."""
329 return self.files.get(fname, {}).get('hash', '')
330
331 def set_file_hash(self, fname, val):
332 """Set the hash of `fname`'s contents."""
333 self.files.setdefault(fname, {})['hash'] = val
334
335 def index_info(self, fname):
336 """Get the information for index.html for `fname`."""
337 return self.files.get(fname, {}).get('index', {})
338
339 def set_index_info(self, fname, info):
340 """Set the information for index.html for `fname`."""
341 self.files.setdefault(fname, {})['index'] = info
342
343
344 # Helpers for templates and generating HTML
345
346 def escape(t):
347 """HTML-escape the text in `t`."""
348 return (t
349 # Convert HTML special chars into HTML entities.
350 .replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
351 .replace("'", "&#39;").replace('"', "&quot;")
352 # Convert runs of spaces: "......" -> "&nbsp;.&nbsp;.&nbsp;."
353 .replace(" ", "&nbsp; ")
354 # To deal with odd-length runs, convert the final pair of spaces
355 # so that "....." -> "&nbsp;.&nbsp;&nbsp;."
356 .replace(" ", "&nbsp; ")
357 )
358
359 def spaceless(html):
360 """Squeeze out some annoying extra space from an HTML string.
361
362 Nicely-formatted templates mean lots of extra space in the result.
363 Get rid of some.
364
365 """
366 html = re.sub(r">\s+<p ", ">\n<p ", html)
367 return html
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698