Details | Last modification | View Log | RSS feed
| Rev | Author | Line No. | Line |
|---|---|---|---|
| 14 | pmbaty | 1 | # -*- coding: utf-8 -*- |
| 2 | # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
||
| 3 | # See https://llvm.org/LICENSE.txt for license information. |
||
| 4 | # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
||
| 5 | """ This module is responsible to generate 'index.html' for the report. |
||
| 6 | |||
| 7 | The input for this step is the output directory, where individual reports |
||
| 8 | could be found. It parses those reports and generates 'index.html'. """ |
||
| 9 | |||
| 10 | import re |
||
| 11 | import os |
||
| 12 | import os.path |
||
| 13 | import sys |
||
| 14 | import shutil |
||
| 15 | import plistlib |
||
| 16 | import glob |
||
| 17 | import json |
||
| 18 | import logging |
||
| 19 | import datetime |
||
| 20 | from libscanbuild import duplicate_check |
||
| 21 | from libscanbuild.clang import get_version |
||
| 22 | |||
| 23 | __all__ = ['document'] |
||
| 24 | |||
| 25 | |||
| 26 | def document(args): |
||
| 27 | """ Generates cover report and returns the number of bugs/crashes. """ |
||
| 28 | |||
| 29 | html_reports_available = args.output_format in {'html', 'plist-html', 'sarif-html'} |
||
| 30 | sarif_reports_available = args.output_format in {'sarif', 'sarif-html'} |
||
| 31 | |||
| 32 | logging.debug('count crashes and bugs') |
||
| 33 | crash_count = sum(1 for _ in read_crashes(args.output)) |
||
| 34 | bug_counter = create_counters() |
||
| 35 | for bug in read_bugs(args.output, html_reports_available): |
||
| 36 | bug_counter(bug) |
||
| 37 | result = crash_count + bug_counter.total |
||
| 38 | |||
| 39 | if html_reports_available and result: |
||
| 40 | use_cdb = os.path.exists(args.cdb) |
||
| 41 | |||
| 42 | logging.debug('generate index.html file') |
||
| 43 | # common prefix for source files to have sorter path |
||
| 44 | prefix = commonprefix_from(args.cdb) if use_cdb else os.getcwd() |
||
| 45 | # assemble the cover from multiple fragments |
||
| 46 | fragments = [] |
||
| 47 | try: |
||
| 48 | if bug_counter.total: |
||
| 49 | fragments.append(bug_summary(args.output, bug_counter)) |
||
| 50 | fragments.append(bug_report(args.output, prefix)) |
||
| 51 | if crash_count: |
||
| 52 | fragments.append(crash_report(args.output, prefix)) |
||
| 53 | assemble_cover(args, prefix, fragments) |
||
| 54 | # copy additional files to the report |
||
| 55 | copy_resource_files(args.output) |
||
| 56 | if use_cdb: |
||
| 57 | shutil.copy(args.cdb, args.output) |
||
| 58 | finally: |
||
| 59 | for fragment in fragments: |
||
| 60 | os.remove(fragment) |
||
| 61 | |||
| 62 | if sarif_reports_available: |
||
| 63 | logging.debug('merging sarif files') |
||
| 64 | merge_sarif_files(args.output) |
||
| 65 | |||
| 66 | return result |
||
| 67 | |||
| 68 | |||
| 69 | def assemble_cover(args, prefix, fragments): |
||
| 70 | """ Put together the fragments into a final report. """ |
||
| 71 | |||
| 72 | import getpass |
||
| 73 | import socket |
||
| 74 | |||
| 75 | if args.html_title is None: |
||
| 76 | args.html_title = os.path.basename(prefix) + ' - analyzer results' |
||
| 77 | |||
| 78 | with open(os.path.join(args.output, 'index.html'), 'w') as handle: |
||
| 79 | indent = 0 |
||
| 80 | handle.write(reindent(""" |
||
| 81 | |<!DOCTYPE html> |
||
| 82 | |<html> |
||
| 83 | | <head> |
||
| 84 | | <title>{html_title}</title> |
||
| 85 | | <link type="text/css" rel="stylesheet" href="scanview.css"/> |
||
| 86 | | <script type='text/javascript' src="sorttable.js"></script> |
||
| 87 | | <script type='text/javascript' src='selectable.js'></script> |
||
| 88 | | </head>""", indent).format(html_title=args.html_title)) |
||
| 89 | handle.write(comment('SUMMARYENDHEAD')) |
||
| 90 | handle.write(reindent(""" |
||
| 91 | | <body> |
||
| 92 | | <h1>{html_title}</h1> |
||
| 93 | | <table> |
||
| 94 | | <tr><th>User:</th><td>{user_name}@{host_name}</td></tr> |
||
| 95 | | <tr><th>Working Directory:</th><td>{current_dir}</td></tr> |
||
| 96 | | <tr><th>Command Line:</th><td>{cmd_args}</td></tr> |
||
| 97 | | <tr><th>Clang Version:</th><td>{clang_version}</td></tr> |
||
| 98 | | <tr><th>Date:</th><td>{date}</td></tr> |
||
| 99 | | </table>""", indent).format(html_title=args.html_title, |
||
| 100 | user_name=getpass.getuser(), |
||
| 101 | host_name=socket.gethostname(), |
||
| 102 | current_dir=prefix, |
||
| 103 | cmd_args=' '.join(sys.argv), |
||
| 104 | clang_version=get_version(args.clang), |
||
| 105 | date=datetime.datetime.today( |
||
| 106 | ).strftime('%c'))) |
||
| 107 | for fragment in fragments: |
||
| 108 | # copy the content of fragments |
||
| 109 | with open(fragment, 'r') as input_handle: |
||
| 110 | shutil.copyfileobj(input_handle, handle) |
||
| 111 | handle.write(reindent(""" |
||
| 112 | | </body> |
||
| 113 | |</html>""", indent)) |
||
| 114 | |||
| 115 | |||
| 116 | def bug_summary(output_dir, bug_counter): |
||
| 117 | """ Bug summary is a HTML table to give a better overview of the bugs. """ |
||
| 118 | |||
| 119 | name = os.path.join(output_dir, 'summary.html.fragment') |
||
| 120 | with open(name, 'w') as handle: |
||
| 121 | indent = 4 |
||
| 122 | handle.write(reindent(""" |
||
| 123 | |<h2>Bug Summary</h2> |
||
| 124 | |<table> |
||
| 125 | | <thead> |
||
| 126 | | <tr> |
||
| 127 | | <td>Bug Type</td> |
||
| 128 | | <td>Quantity</td> |
||
| 129 | | <td class="sorttable_nosort">Display?</td> |
||
| 130 | | </tr> |
||
| 131 | | </thead> |
||
| 132 | | <tbody>""", indent)) |
||
| 133 | handle.write(reindent(""" |
||
| 134 | | <tr style="font-weight:bold"> |
||
| 135 | | <td class="SUMM_DESC">All Bugs</td> |
||
| 136 | | <td class="Q">{0}</td> |
||
| 137 | | <td> |
||
| 138 | | <center> |
||
| 139 | | <input checked type="checkbox" id="AllBugsCheck" |
||
| 140 | | onClick="CopyCheckedStateToCheckButtons(this);"/> |
||
| 141 | | </center> |
||
| 142 | | </td> |
||
| 143 | | </tr>""", indent).format(bug_counter.total)) |
||
| 144 | for category, types in bug_counter.categories.items(): |
||
| 145 | handle.write(reindent(""" |
||
| 146 | | <tr> |
||
| 147 | | <th>{0}</th><th colspan=2></th> |
||
| 148 | | </tr>""", indent).format(category)) |
||
| 149 | for bug_type in types.values(): |
||
| 150 | handle.write(reindent(""" |
||
| 151 | | <tr> |
||
| 152 | | <td class="SUMM_DESC">{bug_type}</td> |
||
| 153 | | <td class="Q">{bug_count}</td> |
||
| 154 | | <td> |
||
| 155 | | <center> |
||
| 156 | | <input checked type="checkbox" |
||
| 157 | | onClick="ToggleDisplay(this,'{bug_type_class}');"/> |
||
| 158 | | </center> |
||
| 159 | | </td> |
||
| 160 | | </tr>""", indent).format(**bug_type)) |
||
| 161 | handle.write(reindent(""" |
||
| 162 | | </tbody> |
||
| 163 | |</table>""", indent)) |
||
| 164 | handle.write(comment('SUMMARYBUGEND')) |
||
| 165 | return name |
||
| 166 | |||
| 167 | |||
| 168 | def bug_report(output_dir, prefix): |
||
| 169 | """ Creates a fragment from the analyzer reports. """ |
||
| 170 | |||
| 171 | pretty = prettify_bug(prefix, output_dir) |
||
| 172 | bugs = (pretty(bug) for bug in read_bugs(output_dir, True)) |
||
| 173 | |||
| 174 | name = os.path.join(output_dir, 'bugs.html.fragment') |
||
| 175 | with open(name, 'w') as handle: |
||
| 176 | indent = 4 |
||
| 177 | handle.write(reindent(""" |
||
| 178 | |<h2>Reports</h2> |
||
| 179 | |<table class="sortable" style="table-layout:automatic"> |
||
| 180 | | <thead> |
||
| 181 | | <tr> |
||
| 182 | | <td>Bug Group</td> |
||
| 183 | | <td class="sorttable_sorted"> |
||
| 184 | | Bug Type |
||
| 185 | | <span id="sorttable_sortfwdind"> ▾</span> |
||
| 186 | | </td> |
||
| 187 | | <td>File</td> |
||
| 188 | | <td>Function/Method</td> |
||
| 189 | | <td class="Q">Line</td> |
||
| 190 | | <td class="Q">Path Length</td> |
||
| 191 | | <td class="sorttable_nosort"></td> |
||
| 192 | | </tr> |
||
| 193 | | </thead> |
||
| 194 | | <tbody>""", indent)) |
||
| 195 | handle.write(comment('REPORTBUGCOL')) |
||
| 196 | for current in bugs: |
||
| 197 | handle.write(reindent(""" |
||
| 198 | | <tr class="{bug_type_class}"> |
||
| 199 | | <td class="DESC">{bug_category}</td> |
||
| 200 | | <td class="DESC">{bug_type}</td> |
||
| 201 | | <td>{bug_file}</td> |
||
| 202 | | <td class="DESC">{bug_function}</td> |
||
| 203 | | <td class="Q">{bug_line}</td> |
||
| 204 | | <td class="Q">{bug_path_length}</td> |
||
| 205 | | <td><a href="{report_file}#EndPath">View Report</a></td> |
||
| 206 | | </tr>""", indent).format(**current)) |
||
| 207 | handle.write(comment('REPORTBUG', {'id': current['report_file']})) |
||
| 208 | handle.write(reindent(""" |
||
| 209 | | </tbody> |
||
| 210 | |</table>""", indent)) |
||
| 211 | handle.write(comment('REPORTBUGEND')) |
||
| 212 | return name |
||
| 213 | |||
| 214 | |||
| 215 | def crash_report(output_dir, prefix): |
||
| 216 | """ Creates a fragment from the compiler crashes. """ |
||
| 217 | |||
| 218 | pretty = prettify_crash(prefix, output_dir) |
||
| 219 | crashes = (pretty(crash) for crash in read_crashes(output_dir)) |
||
| 220 | |||
| 221 | name = os.path.join(output_dir, 'crashes.html.fragment') |
||
| 222 | with open(name, 'w') as handle: |
||
| 223 | indent = 4 |
||
| 224 | handle.write(reindent(""" |
||
| 225 | |<h2>Analyzer Failures</h2> |
||
| 226 | |<p>The analyzer had problems processing the following files:</p> |
||
| 227 | |<table> |
||
| 228 | | <thead> |
||
| 229 | | <tr> |
||
| 230 | | <td>Problem</td> |
||
| 231 | | <td>Source File</td> |
||
| 232 | | <td>Preprocessed File</td> |
||
| 233 | | <td>STDERR Output</td> |
||
| 234 | | </tr> |
||
| 235 | | </thead> |
||
| 236 | | <tbody>""", indent)) |
||
| 237 | for current in crashes: |
||
| 238 | handle.write(reindent(""" |
||
| 239 | | <tr> |
||
| 240 | | <td>{problem}</td> |
||
| 241 | | <td>{source}</td> |
||
| 242 | | <td><a href="{file}">preprocessor output</a></td> |
||
| 243 | | <td><a href="{stderr}">analyzer std err</a></td> |
||
| 244 | | </tr>""", indent).format(**current)) |
||
| 245 | handle.write(comment('REPORTPROBLEM', current)) |
||
| 246 | handle.write(reindent(""" |
||
| 247 | | </tbody> |
||
| 248 | |</table>""", indent)) |
||
| 249 | handle.write(comment('REPORTCRASHES')) |
||
| 250 | return name |
||
| 251 | |||
| 252 | |||
| 253 | def read_crashes(output_dir): |
||
| 254 | """ Generate a unique sequence of crashes from given output directory. """ |
||
| 255 | |||
| 256 | return (parse_crash(filename) |
||
| 257 | for filename in glob.iglob(os.path.join(output_dir, 'failures', |
||
| 258 | '*.info.txt'))) |
||
| 259 | |||
| 260 | |||
| 261 | def read_bugs(output_dir, html): |
||
| 262 | # type: (str, bool) -> Generator[Dict[str, Any], None, None] |
||
| 263 | """ Generate a unique sequence of bugs from given output directory. |
||
| 264 | |||
| 265 | Duplicates can be in a project if the same module was compiled multiple |
||
| 266 | times with different compiler options. These would be better to show in |
||
| 267 | the final report (cover) only once. """ |
||
| 268 | |||
| 269 | def empty(file_name): |
||
| 270 | return os.stat(file_name).st_size == 0 |
||
| 271 | |||
| 272 | duplicate = duplicate_check( |
||
| 273 | lambda bug: '{bug_line}.{bug_path_length}:{bug_file}'.format(**bug)) |
||
| 274 | |||
| 275 | # get the right parser for the job. |
||
| 276 | parser = parse_bug_html if html else parse_bug_plist |
||
| 277 | # get the input files, which are not empty. |
||
| 278 | pattern = os.path.join(output_dir, '*.html' if html else '*.plist') |
||
| 279 | bug_files = (file for file in glob.iglob(pattern) if not empty(file)) |
||
| 280 | |||
| 281 | for bug_file in bug_files: |
||
| 282 | for bug in parser(bug_file): |
||
| 283 | if not duplicate(bug): |
||
| 284 | yield bug |
||
| 285 | |||
| 286 | def merge_sarif_files(output_dir, sort_files=False): |
||
| 287 | """ Reads and merges all .sarif files in the given output directory. |
||
| 288 | |||
| 289 | Each sarif file in the output directory is understood as a single run |
||
| 290 | and thus appear separate in the top level runs array. This requires |
||
| 291 | modifying the run index of any embedded links in messages. |
||
| 292 | """ |
||
| 293 | |||
| 294 | def empty(file_name): |
||
| 295 | return os.stat(file_name).st_size == 0 |
||
| 296 | |||
| 297 | def update_sarif_object(sarif_object, runs_count_offset): |
||
| 298 | """ |
||
| 299 | Given a SARIF object, checks its dictionary entries for a 'message' property. |
||
| 300 | If it exists, updates the message index of embedded links in the run index. |
||
| 301 | |||
| 302 | Recursively looks through entries in the dictionary. |
||
| 303 | """ |
||
| 304 | if not isinstance(sarif_object, dict): |
||
| 305 | return sarif_object |
||
| 306 | |||
| 307 | if 'message' in sarif_object: |
||
| 308 | sarif_object['message'] = match_and_update_run(sarif_object['message'], runs_count_offset) |
||
| 309 | |||
| 310 | for key in sarif_object: |
||
| 311 | if isinstance(sarif_object[key], list): |
||
| 312 | # iterate through subobjects and update it. |
||
| 313 | arr = [update_sarif_object(entry, runs_count_offset) for entry in sarif_object[key]] |
||
| 314 | sarif_object[key] = arr |
||
| 315 | elif isinstance(sarif_object[key], dict): |
||
| 316 | sarif_object[key] = update_sarif_object(sarif_object[key], runs_count_offset) |
||
| 317 | else: |
||
| 318 | # do nothing |
||
| 319 | pass |
||
| 320 | |||
| 321 | return sarif_object |
||
| 322 | |||
| 323 | |||
| 324 | def match_and_update_run(message, runs_count_offset): |
||
| 325 | """ |
||
| 326 | Given a SARIF message object, checks if the text property contains an embedded link and |
||
| 327 | updates the run index if necessary. |
||
| 328 | """ |
||
| 329 | if 'text' not in message: |
||
| 330 | return message |
||
| 331 | |||
| 332 | # we only merge runs, so we only need to update the run index |
||
| 333 | pattern = re.compile(r'sarif:/runs/(\d+)') |
||
| 334 | |||
| 335 | text = message['text'] |
||
| 336 | matches = re.finditer(pattern, text) |
||
| 337 | matches_list = list(matches) |
||
| 338 | |||
| 339 | # update matches from right to left to make increasing character length (9->10) smoother |
||
| 340 | for idx in range(len(matches_list) - 1, -1, -1): |
||
| 341 | match = matches_list[idx] |
||
| 342 | new_run_count = str(runs_count_offset + int(match.group(1))) |
||
| 343 | text = text[0:match.start(1)] + new_run_count + text[match.end(1):] |
||
| 344 | |||
| 345 | message['text'] = text |
||
| 346 | return message |
||
| 347 | |||
| 348 | |||
| 349 | |||
| 350 | sarif_files = (file for file in glob.iglob(os.path.join(output_dir, '*.sarif')) if not empty(file)) |
||
| 351 | # exposed for testing since the order of files returned by glob is not guaranteed to be sorted |
||
| 352 | if sort_files: |
||
| 353 | sarif_files = list(sarif_files) |
||
| 354 | sarif_files.sort() |
||
| 355 | |||
| 356 | runs_count = 0 |
||
| 357 | merged = {} |
||
| 358 | for sarif_file in sarif_files: |
||
| 359 | with open(sarif_file) as fp: |
||
| 360 | sarif = json.load(fp) |
||
| 361 | if 'runs' not in sarif: |
||
| 362 | continue |
||
| 363 | |||
| 364 | # start with the first file |
||
| 365 | if not merged: |
||
| 366 | merged = sarif |
||
| 367 | else: |
||
| 368 | # extract the run and append it to the merged output |
||
| 369 | for run in sarif['runs']: |
||
| 370 | new_run = update_sarif_object(run, runs_count) |
||
| 371 | merged['runs'].append(new_run) |
||
| 372 | |||
| 373 | runs_count += len(sarif['runs']) |
||
| 374 | |||
| 375 | with open(os.path.join(output_dir, 'results-merged.sarif'), 'w') as out: |
||
| 376 | json.dump(merged, out, indent=4, sort_keys=True) |
||
| 377 | |||
| 378 | |||
| 379 | def parse_bug_plist(filename): |
||
| 380 | """ Returns the generator of bugs from a single .plist file. """ |
||
| 381 | |||
| 382 | with open(filename, 'rb') as fp: |
||
| 383 | content = plistlib.load(fp) |
||
| 384 | files = content.get('files') |
||
| 385 | for bug in content.get('diagnostics', []): |
||
| 386 | if len(files) <= int(bug['location']['file']): |
||
| 387 | logging.warning('Parsing bug from "%s" failed', filename) |
||
| 388 | continue |
||
| 389 | |||
| 390 | yield { |
||
| 391 | 'result': filename, |
||
| 392 | 'bug_type': bug['type'], |
||
| 393 | 'bug_category': bug['category'], |
||
| 394 | 'bug_line': int(bug['location']['line']), |
||
| 395 | 'bug_path_length': int(bug['location']['col']), |
||
| 396 | 'bug_file': files[int(bug['location']['file'])] |
||
| 397 | } |
||
| 398 | |||
| 399 | |||
| 400 | def parse_bug_html(filename): |
||
| 401 | """ Parse out the bug information from HTML output. """ |
||
| 402 | |||
| 403 | patterns = [re.compile(r'<!-- BUGTYPE (?P<bug_type>.*) -->$'), |
||
| 404 | re.compile(r'<!-- BUGFILE (?P<bug_file>.*) -->$'), |
||
| 405 | re.compile(r'<!-- BUGPATHLENGTH (?P<bug_path_length>.*) -->$'), |
||
| 406 | re.compile(r'<!-- BUGLINE (?P<bug_line>.*) -->$'), |
||
| 407 | re.compile(r'<!-- BUGCATEGORY (?P<bug_category>.*) -->$'), |
||
| 408 | re.compile(r'<!-- BUGDESC (?P<bug_description>.*) -->$'), |
||
| 409 | re.compile(r'<!-- FUNCTIONNAME (?P<bug_function>.*) -->$')] |
||
| 410 | endsign = re.compile(r'<!-- BUGMETAEND -->') |
||
| 411 | |||
| 412 | bug = { |
||
| 413 | 'report_file': filename, |
||
| 414 | 'bug_function': 'n/a', # compatibility with < clang-3.5 |
||
| 415 | 'bug_category': 'Other', |
||
| 416 | 'bug_line': 0, |
||
| 417 | 'bug_path_length': 1 |
||
| 418 | } |
||
| 419 | |||
| 420 | with open(filename, encoding='utf-8') as handler: |
||
| 421 | for line in handler.readlines(): |
||
| 422 | # do not read the file further |
||
| 423 | if endsign.match(line): |
||
| 424 | break |
||
| 425 | # search for the right lines |
||
| 426 | for regex in patterns: |
||
| 427 | match = regex.match(line.strip()) |
||
| 428 | if match: |
||
| 429 | bug.update(match.groupdict()) |
||
| 430 | break |
||
| 431 | |||
| 432 | encode_value(bug, 'bug_line', int) |
||
| 433 | encode_value(bug, 'bug_path_length', int) |
||
| 434 | |||
| 435 | yield bug |
||
| 436 | |||
| 437 | |||
| 438 | def parse_crash(filename): |
||
| 439 | """ Parse out the crash information from the report file. """ |
||
| 440 | |||
| 441 | match = re.match(r'(.*)\.info\.txt', filename) |
||
| 442 | name = match.group(1) if match else None |
||
| 443 | with open(filename, mode='rb') as handler: |
||
| 444 | # this is a workaround to fix windows read '\r\n' as new lines. |
||
| 445 | lines = [line.decode().rstrip() for line in handler.readlines()] |
||
| 446 | return { |
||
| 447 | 'source': lines[0], |
||
| 448 | 'problem': lines[1], |
||
| 449 | 'file': name, |
||
| 450 | 'info': name + '.info.txt', |
||
| 451 | 'stderr': name + '.stderr.txt' |
||
| 452 | } |
||
| 453 | |||
| 454 | |||
| 455 | def category_type_name(bug): |
||
| 456 | """ Create a new bug attribute from bug by category and type. |
||
| 457 | |||
| 458 | The result will be used as CSS class selector in the final report. """ |
||
| 459 | |||
| 460 | def smash(key): |
||
| 461 | """ Make value ready to be HTML attribute value. """ |
||
| 462 | |||
| 463 | return bug.get(key, '').lower().replace(' ', '_').replace("'", '') |
||
| 464 | |||
| 465 | return escape('bt_' + smash('bug_category') + '_' + smash('bug_type')) |
||
| 466 | |||
| 467 | |||
| 468 | def create_counters(): |
||
| 469 | """ Create counters for bug statistics. |
||
| 470 | |||
| 471 | Two entries are maintained: 'total' is an integer, represents the |
||
| 472 | number of bugs. The 'categories' is a two level categorisation of bug |
||
| 473 | counters. The first level is 'bug category' the second is 'bug type'. |
||
| 474 | Each entry in this classification is a dictionary of 'count', 'type' |
||
| 475 | and 'label'. """ |
||
| 476 | |||
| 477 | def predicate(bug): |
||
| 478 | bug_category = bug['bug_category'] |
||
| 479 | bug_type = bug['bug_type'] |
||
| 480 | current_category = predicate.categories.get(bug_category, dict()) |
||
| 481 | current_type = current_category.get(bug_type, { |
||
| 482 | 'bug_type': bug_type, |
||
| 483 | 'bug_type_class': category_type_name(bug), |
||
| 484 | 'bug_count': 0 |
||
| 485 | }) |
||
| 486 | current_type.update({'bug_count': current_type['bug_count'] + 1}) |
||
| 487 | current_category.update({bug_type: current_type}) |
||
| 488 | predicate.categories.update({bug_category: current_category}) |
||
| 489 | predicate.total += 1 |
||
| 490 | |||
| 491 | predicate.total = 0 |
||
| 492 | predicate.categories = dict() |
||
| 493 | return predicate |
||
| 494 | |||
| 495 | |||
| 496 | def prettify_bug(prefix, output_dir): |
||
| 497 | def predicate(bug): |
||
| 498 | """ Make safe this values to embed into HTML. """ |
||
| 499 | |||
| 500 | bug['bug_type_class'] = category_type_name(bug) |
||
| 501 | |||
| 502 | encode_value(bug, 'bug_file', lambda x: escape(chop(prefix, x))) |
||
| 503 | encode_value(bug, 'bug_category', escape) |
||
| 504 | encode_value(bug, 'bug_type', escape) |
||
| 505 | encode_value(bug, 'report_file', lambda x: escape(chop(output_dir, x))) |
||
| 506 | return bug |
||
| 507 | |||
| 508 | return predicate |
||
| 509 | |||
| 510 | |||
| 511 | def prettify_crash(prefix, output_dir): |
||
| 512 | def predicate(crash): |
||
| 513 | """ Make safe this values to embed into HTML. """ |
||
| 514 | |||
| 515 | encode_value(crash, 'source', lambda x: escape(chop(prefix, x))) |
||
| 516 | encode_value(crash, 'problem', escape) |
||
| 517 | encode_value(crash, 'file', lambda x: escape(chop(output_dir, x))) |
||
| 518 | encode_value(crash, 'info', lambda x: escape(chop(output_dir, x))) |
||
| 519 | encode_value(crash, 'stderr', lambda x: escape(chop(output_dir, x))) |
||
| 520 | return crash |
||
| 521 | |||
| 522 | return predicate |
||
| 523 | |||
| 524 | |||
| 525 | def copy_resource_files(output_dir): |
||
| 526 | """ Copy the javascript and css files to the report directory. """ |
||
| 527 | |||
| 528 | this_dir = os.path.dirname(os.path.realpath(__file__)) |
||
| 529 | for resource in os.listdir(os.path.join(this_dir, 'resources')): |
||
| 530 | shutil.copy(os.path.join(this_dir, 'resources', resource), output_dir) |
||
| 531 | |||
| 532 | |||
| 533 | def encode_value(container, key, encode): |
||
| 534 | """ Run 'encode' on 'container[key]' value and update it. """ |
||
| 535 | |||
| 536 | if key in container: |
||
| 537 | value = encode(container[key]) |
||
| 538 | container.update({key: value}) |
||
| 539 | |||
| 540 | |||
| 541 | def chop(prefix, filename): |
||
| 542 | """ Create 'filename' from '/prefix/filename' """ |
||
| 543 | |||
| 544 | return filename if not len(prefix) else os.path.relpath(filename, prefix) |
||
| 545 | |||
| 546 | |||
| 547 | def escape(text): |
||
| 548 | """ Paranoid HTML escape method. (Python version independent) """ |
||
| 549 | |||
| 550 | escape_table = { |
||
| 551 | '&': '&', |
||
| 552 | '"': '"', |
||
| 553 | "'": ''', |
||
| 554 | '>': '>', |
||
| 555 | '<': '<' |
||
| 556 | } |
||
| 557 | return ''.join(escape_table.get(c, c) for c in text) |
||
| 558 | |||
| 559 | |||
| 560 | def reindent(text, indent): |
||
| 561 | """ Utility function to format html output and keep indentation. """ |
||
| 562 | |||
| 563 | result = '' |
||
| 564 | for line in text.splitlines(): |
||
| 565 | if len(line.strip()): |
||
| 566 | result += ' ' * indent + line.split('|')[1] + os.linesep |
||
| 567 | return result |
||
| 568 | |||
| 569 | |||
| 570 | def comment(name, opts=dict()): |
||
| 571 | """ Utility function to format meta information as comment. """ |
||
| 572 | |||
| 573 | attributes = '' |
||
| 574 | for key, value in opts.items(): |
||
| 575 | attributes += ' {0}="{1}"'.format(key, value) |
||
| 576 | |||
| 577 | return '<!-- {0}{1} -->{2}'.format(name, attributes, os.linesep) |
||
| 578 | |||
| 579 | |||
| 580 | def commonprefix_from(filename): |
||
| 581 | """ Create file prefix from a compilation database entries. """ |
||
| 582 | |||
| 583 | with open(filename, 'r') as handle: |
||
| 584 | return commonprefix(item['file'] for item in json.load(handle)) |
||
| 585 | |||
| 586 | |||
| 587 | def commonprefix(files): |
||
| 588 | """ Fixed version of os.path.commonprefix. |
||
| 589 | |||
| 590 | :param files: list of file names. |
||
| 591 | :return: the longest path prefix that is a prefix of all files. """ |
||
| 592 | result = None |
||
| 593 | for current in files: |
||
| 594 | if result is not None: |
||
| 595 | result = os.path.commonprefix([result, current]) |
||
| 596 | else: |
||
| 597 | result = current |
||
| 598 | |||
| 599 | if result is None: |
||
| 600 | return '' |
||
| 601 | elif not os.path.isdir(result): |
||
| 602 | return os.path.dirname(result) |
||
| 603 | else: |
||
| 604 | return os.path.abspath(result) |