1 # -*- coding: utf-8 -*-
2 # The LLVM Compiler Infrastructure
4 # This file is distributed under the University of Illinois Open Source
5 # License. See LICENSE.TXT for details.
6 """ This module is responsible to generate 'index.html' for the report.
8 The input for this step is the output directory, where individual reports
9 could be found. It parses those reports and generates 'index.html'. """
22 from libscanbuild import duplicate_check
23 from libscanbuild.clang import get_version
25 __all__ = ['document']
29 """ Generates cover report and returns the number of bugs/crashes. """
31 html_reports_available = args.output_format in {'html', 'plist-html'}
33 logging.debug('count crashes and bugs')
34 crash_count = sum(1 for _ in read_crashes(args.output))
35 bug_counter = create_counters()
36 for bug in read_bugs(args.output, html_reports_available):
38 result = crash_count + bug_counter.total
40 if html_reports_available and result:
41 use_cdb = os.path.exists(args.cdb)
43 logging.debug('generate index.html file')
44 # common prefix for source files to have sorter path
45 prefix = commonprefix_from(args.cdb) if use_cdb else os.getcwd()
46 # assemble the cover from multiple fragments
50 fragments.append(bug_summary(args.output, bug_counter))
51 fragments.append(bug_report(args.output, prefix))
53 fragments.append(crash_report(args.output, prefix))
54 assemble_cover(args, prefix, fragments)
55 # copy additional files to the report
56 copy_resource_files(args.output)
58 shutil.copy(args.cdb, args.output)
60 for fragment in fragments:
65 def assemble_cover(args, prefix, fragments):
66 """ Put together the fragments into a final report. """
71 if args.html_title is None:
72 args.html_title = os.path.basename(prefix) + ' - analyzer results'
74 with open(os.path.join(args.output, 'index.html'), 'w') as handle:
76 handle.write(reindent("""
80 | <title>{html_title}</title>
81 | <link type="text/css" rel="stylesheet" href="scanview.css"/>
82 | <script type='text/javascript' src="sorttable.js"></script>
83 | <script type='text/javascript' src='selectable.js'></script>
84 | </head>""", indent).format(html_title=args.html_title))
85 handle.write(comment('SUMMARYENDHEAD'))
86 handle.write(reindent("""
88 | <h1>{html_title}</h1>
90 | <tr><th>User:</th><td>{user_name}@{host_name}</td></tr>
91 | <tr><th>Working Directory:</th><td>{current_dir}</td></tr>
92 | <tr><th>Command Line:</th><td>{cmd_args}</td></tr>
93 | <tr><th>Clang Version:</th><td>{clang_version}</td></tr>
94 | <tr><th>Date:</th><td>{date}</td></tr>
95 | </table>""", indent).format(html_title=args.html_title,
96 user_name=getpass.getuser(),
97 host_name=socket.gethostname(),
99 cmd_args=' '.join(sys.argv),
100 clang_version=get_version(args.clang),
101 date=datetime.datetime.today(
103 for fragment in fragments:
104 # copy the content of fragments
105 with open(fragment, 'r') as input_handle:
106 shutil.copyfileobj(input_handle, handle)
107 handle.write(reindent("""
109 |</html>""", indent))
112 def bug_summary(output_dir, bug_counter):
113 """ Bug summary is a HTML table to give a better overview of the bugs. """
115 name = os.path.join(output_dir, 'summary.html.fragment')
116 with open(name, 'w') as handle:
118 handle.write(reindent("""
119 |<h2>Bug Summary</h2>
125 | <td class="sorttable_nosort">Display?</td>
128 | <tbody>""", indent))
129 handle.write(reindent("""
130 | <tr style="font-weight:bold">
131 | <td class="SUMM_DESC">All Bugs</td>
132 | <td class="Q">{0}</td>
135 | <input checked type="checkbox" id="AllBugsCheck"
136 | onClick="CopyCheckedStateToCheckButtons(this);"/>
139 | </tr>""", indent).format(bug_counter.total))
140 for category, types in bug_counter.categories.items():
141 handle.write(reindent("""
143 | <th>{0}</th><th colspan=2></th>
144 | </tr>""", indent).format(category))
145 for bug_type in types.values():
146 handle.write(reindent("""
148 | <td class="SUMM_DESC">{bug_type}</td>
149 | <td class="Q">{bug_count}</td>
152 | <input checked type="checkbox"
153 | onClick="ToggleDisplay(this,'{bug_type_class}');"/>
156 | </tr>""", indent).format(**bug_type))
157 handle.write(reindent("""
159 |</table>""", indent))
160 handle.write(comment('SUMMARYBUGEND'))
164 def bug_report(output_dir, prefix):
165 """ Creates a fragment from the analyzer reports. """
167 pretty = prettify_bug(prefix, output_dir)
168 bugs = (pretty(bug) for bug in read_bugs(output_dir, True))
170 name = os.path.join(output_dir, 'bugs.html.fragment')
171 with open(name, 'w') as handle:
173 handle.write(reindent("""
175 |<table class="sortable" style="table-layout:automatic">
179 | <td class="sorttable_sorted">
181 | <span id="sorttable_sortfwdind"> ▾</span>
184 | <td>Function/Method</td>
185 | <td class="Q">Line</td>
186 | <td class="Q">Path Length</td>
187 | <td class="sorttable_nosort"></td>
190 | <tbody>""", indent))
191 handle.write(comment('REPORTBUGCOL'))
193 handle.write(reindent("""
194 | <tr class="{bug_type_class}">
195 | <td class="DESC">{bug_category}</td>
196 | <td class="DESC">{bug_type}</td>
197 | <td>{bug_file}</td>
198 | <td class="DESC">{bug_function}</td>
199 | <td class="Q">{bug_line}</td>
200 | <td class="Q">{bug_path_length}</td>
201 | <td><a href="{report_file}#EndPath">View Report</a></td>
202 | </tr>""", indent).format(**current))
203 handle.write(comment('REPORTBUG', {'id': current['report_file']}))
204 handle.write(reindent("""
206 |</table>""", indent))
207 handle.write(comment('REPORTBUGEND'))
211 def crash_report(output_dir, prefix):
212 """ Creates a fragment from the compiler crashes. """
214 pretty = prettify_crash(prefix, output_dir)
215 crashes = (pretty(crash) for crash in read_crashes(output_dir))
217 name = os.path.join(output_dir, 'crashes.html.fragment')
218 with open(name, 'w') as handle:
220 handle.write(reindent("""
221 |<h2>Analyzer Failures</h2>
222 |<p>The analyzer had problems processing the following files:</p>
227 | <td>Source File</td>
228 | <td>Preprocessed File</td>
229 | <td>STDERR Output</td>
232 | <tbody>""", indent))
233 for current in crashes:
234 handle.write(reindent("""
238 | <td><a href="{file}">preprocessor output</a></td>
239 | <td><a href="{stderr}">analyzer std err</a></td>
240 | </tr>""", indent).format(**current))
241 handle.write(comment('REPORTPROBLEM', current))
242 handle.write(reindent("""
244 |</table>""", indent))
245 handle.write(comment('REPORTCRASHES'))
249 def read_crashes(output_dir):
250 """ Generate a unique sequence of crashes from given output directory. """
252 return (parse_crash(filename)
253 for filename in glob.iglob(os.path.join(output_dir, 'failures',
257 def read_bugs(output_dir, html):
258 """ Generate a unique sequence of bugs from given output directory.
260 Duplicates can be in a project if the same module was compiled multiple
261 times with different compiler options. These would be better to show in
262 the final report (cover) only once. """
264 parser = parse_bug_html if html else parse_bug_plist
265 pattern = '*.html' if html else '*.plist'
267 duplicate = duplicate_check(
268 lambda bug: '{bug_line}.{bug_path_length}:{bug_file}'.format(**bug))
270 bugs = itertools.chain.from_iterable(
271 # parser creates a bug generator not the bug itself
273 for filename in glob.iglob(os.path.join(output_dir, pattern)))
275 return (bug for bug in bugs if not duplicate(bug))
278 def parse_bug_plist(filename):
279 """ Returns the generator of bugs from a single .plist file. """
281 content = plistlib.readPlist(filename)
282 files = content.get('files')
283 for bug in content.get('diagnostics', []):
284 if len(files) <= int(bug['location']['file']):
285 logging.warning('Parsing bug from "%s" failed', filename)
290 'bug_type': bug['type'],
291 'bug_category': bug['category'],
292 'bug_line': int(bug['location']['line']),
293 'bug_path_length': int(bug['location']['col']),
294 'bug_file': files[int(bug['location']['file'])]
298 def parse_bug_html(filename):
299 """ Parse out the bug information from HTML output. """
301 patterns = [re.compile(r'<!-- BUGTYPE (?P<bug_type>.*) -->$'),
302 re.compile(r'<!-- BUGFILE (?P<bug_file>.*) -->$'),
303 re.compile(r'<!-- BUGPATHLENGTH (?P<bug_path_length>.*) -->$'),
304 re.compile(r'<!-- BUGLINE (?P<bug_line>.*) -->$'),
305 re.compile(r'<!-- BUGCATEGORY (?P<bug_category>.*) -->$'),
306 re.compile(r'<!-- BUGDESC (?P<bug_description>.*) -->$'),
307 re.compile(r'<!-- FUNCTIONNAME (?P<bug_function>.*) -->$')]
308 endsign = re.compile(r'<!-- BUGMETAEND -->')
311 'report_file': filename,
312 'bug_function': 'n/a', # compatibility with < clang-3.5
313 'bug_category': 'Other',
318 with open(filename) as handler:
319 for line in handler.readlines():
320 # do not read the file further
321 if endsign.match(line):
323 # search for the right lines
324 for regex in patterns:
325 match = regex.match(line.strip())
327 bug.update(match.groupdict())
330 encode_value(bug, 'bug_line', int)
331 encode_value(bug, 'bug_path_length', int)
336 def parse_crash(filename):
337 """ Parse out the crash information from the report file. """
339 match = re.match(r'(.*)\.info\.txt', filename)
340 name = match.group(1) if match else None
341 with open(filename, mode='rb') as handler:
342 # this is a workaround to fix windows read '\r\n' as new lines.
343 lines = [line.decode().rstrip() for line in handler.readlines()]
348 'info': name + '.info.txt',
349 'stderr': name + '.stderr.txt'
353 def category_type_name(bug):
354 """ Create a new bug attribute from bug by category and type.
356 The result will be used as CSS class selector in the final report. """
359 """ Make value ready to be HTML attribute value. """
361 return bug.get(key, '').lower().replace(' ', '_').replace("'", '')
363 return escape('bt_' + smash('bug_category') + '_' + smash('bug_type'))
366 def create_counters():
367 """ Create counters for bug statistics.
369 Two entries are maintained: 'total' is an integer, represents the
370 number of bugs. The 'categories' is a two level categorisation of bug
371 counters. The first level is 'bug category' the second is 'bug type'.
372 Each entry in this classification is a dictionary of 'count', 'type'
376 bug_category = bug['bug_category']
377 bug_type = bug['bug_type']
378 current_category = predicate.categories.get(bug_category, dict())
379 current_type = current_category.get(bug_type, {
380 'bug_type': bug_type,
381 'bug_type_class': category_type_name(bug),
384 current_type.update({'bug_count': current_type['bug_count'] + 1})
385 current_category.update({bug_type: current_type})
386 predicate.categories.update({bug_category: current_category})
390 predicate.categories = dict()
394 def prettify_bug(prefix, output_dir):
396 """ Make safe this values to embed into HTML. """
398 bug['bug_type_class'] = category_type_name(bug)
400 encode_value(bug, 'bug_file', lambda x: escape(chop(prefix, x)))
401 encode_value(bug, 'bug_category', escape)
402 encode_value(bug, 'bug_type', escape)
403 encode_value(bug, 'report_file', lambda x: escape(chop(output_dir, x)))
409 def prettify_crash(prefix, output_dir):
410 def predicate(crash):
411 """ Make safe this values to embed into HTML. """
413 encode_value(crash, 'source', lambda x: escape(chop(prefix, x)))
414 encode_value(crash, 'problem', escape)
415 encode_value(crash, 'file', lambda x: escape(chop(output_dir, x)))
416 encode_value(crash, 'info', lambda x: escape(chop(output_dir, x)))
417 encode_value(crash, 'stderr', lambda x: escape(chop(output_dir, x)))
423 def copy_resource_files(output_dir):
424 """ Copy the javascript and css files to the report directory. """
426 this_dir = os.path.dirname(os.path.realpath(__file__))
427 for resource in os.listdir(os.path.join(this_dir, 'resources')):
428 shutil.copy(os.path.join(this_dir, 'resources', resource), output_dir)
431 def encode_value(container, key, encode):
432 """ Run 'encode' on 'container[key]' value and update it. """
435 value = encode(container[key])
436 container.update({key: value})
439 def chop(prefix, filename):
440 """ Create 'filename' from '/prefix/filename' """
442 return filename if not len(prefix) else os.path.relpath(filename, prefix)
446 """ Paranoid HTML escape method. (Python version independent) """
455 return ''.join(escape_table.get(c, c) for c in text)
458 def reindent(text, indent):
459 """ Utility function to format html output and keep indentation. """
462 for line in text.splitlines():
463 if len(line.strip()):
464 result += ' ' * indent + line.split('|')[1] + os.linesep
468 def comment(name, opts=dict()):
469 """ Utility function to format meta information as comment. """
472 for key, value in opts.items():
473 attributes += ' {0}="{1}"'.format(key, value)
475 return '<!-- {0}{1} -->{2}'.format(name, attributes, os.linesep)
478 def commonprefix_from(filename):
479 """ Create file prefix from a compilation database entries. """
481 with open(filename, 'r') as handle:
482 return commonprefix(item['file'] for item in json.load(handle))
485 def commonprefix(files):
486 """ Fixed version of os.path.commonprefix.
488 :param files: list of file names.
489 :return: the longest path prefix that is a prefix of all files. """
491 for current in files:
492 if result is not None:
493 result = os.path.commonprefix([result, current])
499 elif not os.path.isdir(result):
500 return os.path.dirname(result)
502 return os.path.abspath(result)