Dirk Pranke | c93434f | 2017-10-03 00:45:55 | [diff] [blame] | 1 | #!/usr/bin/env python |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 2 | # |
| 3 | # Copyright 2017 The Chromium Authors. All rights reserved. |
| 4 | # Use of this source code is governed by a BSD-style license that can be |
| 5 | # found in the LICENSE file. |
| 6 | |
| 7 | from __future__ import print_function |
| 8 | |
| 9 | import argparse |
| 10 | import logging |
| 11 | import os |
| 12 | import subprocess |
| 13 | import sys |
| 14 | |
Abhishek Arya | 6eb0ebc | 2017-10-03 17:09:56 | [diff] [blame] | 15 | import SimpleHTTPServer |
| 16 | import SocketServer |
| 17 | |
Abhishek Arya | 26bc519a | 2017-10-03 15:52:59 | [diff] [blame] | 18 | HELP_MESSAGE = """ |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 19 | This script helps to generate code coverage report. It uses Clang Source-based |
| 20 | Code coverage (https://clang.llvm.org/docs/SourceBasedCodeCoverage.html). |
| 21 | |
| 22 | The output is a directory with HTML files that can be inspected via local web |
| 23 | server (e.g. "python -m SimpleHTTPServer"). |
| 24 | |
| 25 | In order to generate code coverage report, you need to build the target program |
Jonathan Metzman | 301ce027 | 2017-10-16 15:43:28 | [diff] [blame] | 26 | with "use_clang_coverage=true" GN flag. You should also explicitly use the flag |
| 27 | "is_component_build=false" as explained at the end of this paragraph. |
| 28 | use_component_build is not compatible with sanitizer flags: "is_asan", |
| 29 | "is_msan", etc. It is also incompatible with "optimize_for_fuzzing" and with |
| 30 | "is_component_build". Beware that if "is_debug" is true (it defaults to true), |
| 31 | then "is_component_build" will be set to true unless specified false as an |
| 32 | argument. So it is best to pass is_component_build=false when using |
| 33 | "use_clang_coveage". |
Abhishek Arya | 26bc519a | 2017-10-03 15:52:59 | [diff] [blame] | 34 | |
Abhishek Arya | 8fe0015 | 2017-10-04 15:47:24 | [diff] [blame] | 35 | If you are building a fuzz target, you need to add "use_libfuzzer=true" GN flag |
| 36 | as well. |
| 37 | |
| 38 | Sample workflow for a fuzz target (e.g. pdfium_fuzzer): |
| 39 | |
| 40 | cd <chromium_checkout_dir>/src |
Jonathan Metzman | 301ce027 | 2017-10-16 15:43:28 | [diff] [blame] | 41 | gn gen //out/coverage --args='use_clang_coverage=true use_libfuzzer=true \ |
| 42 | is_component_build=false' |
Abhishek Arya | 8fe0015 | 2017-10-04 15:47:24 | [diff] [blame] | 43 | ninja -C out/coverage -j100 pdfium_fuzzer |
| 44 | ./testing/libfuzzer/coverage.py \\ |
| 45 | --output="coverage_out" \\ |
| 46 | --command="out/coverage/pdfium_fuzzer -runs=<runs> <corpus_dir>" |
Max Moroz | c9213a4 | 2017-10-17 23:07:25 | [diff] [blame] | 47 | --filter third_party/pdfium/ pdf/ |
Abhishek Arya | 8fe0015 | 2017-10-04 15:47:24 | [diff] [blame] | 48 | |
| 49 | where: |
| 50 | <corpus_dir> - directory containing samples files for this format. |
| 51 | <runs> - number of times to fuzz target function. Should be 0 when you just |
| 52 | want to see the coverage on corpus and don't want to fuzz at all. |
| 53 | Then, open http://localhost:9000/report.html to see coverage report. |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 54 | |
| 55 | For Googlers, there are examples available at go/chrome-code-coverage-examples. |
| 56 | |
Abhishek Arya | 8fe0015 | 2017-10-04 15:47:24 | [diff] [blame] | 57 | If you have any questions, please send an email to [email protected]. |
Abhishek Arya | 26bc519a | 2017-10-03 15:52:59 | [diff] [blame] | 58 | """ |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 59 | |
| 60 | HTML_FILE_EXTENSION = '.html' |
| 61 | |
Abhishek Arya | 26bc519a | 2017-10-03 15:52:59 | [diff] [blame] | 62 | CHROME_SRC_PATH = os.path.dirname( |
| 63 | os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 64 | LLVM_BUILD_PATH = os.path.join(CHROME_SRC_PATH, 'third_party', 'llvm-build') |
| 65 | LLVM_BIN_PATH = os.path.join(LLVM_BUILD_PATH, 'Release+Asserts', 'bin') |
| 66 | LLVM_COV_PATH = os.path.join(LLVM_BIN_PATH, 'llvm-cov') |
| 67 | LLVM_PROFDATA_PATH = os.path.join(LLVM_BIN_PATH, 'llvm-profdata') |
| 68 | |
| 69 | LLVM_PROFILE_FILE_NAME = 'coverage.profraw' |
| 70 | LLVM_COVERAGE_FILE_NAME = 'coverage.profdata' |
| 71 | |
| 72 | REPORT_FILENAME = 'report.html' |
| 73 | |
Abhishek Arya | 26bc519a | 2017-10-03 15:52:59 | [diff] [blame] | 74 | REPORT_TEMPLATE = """<!DOCTYPE html> |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 75 | <html> |
Abhishek Arya | 2b30f1b8 | 2017-10-03 22:29:37 | [diff] [blame] | 76 | <head> |
| 77 | <meta name='viewport' content='width=device-width,initial-scale=1'> |
| 78 | <meta charset='UTF-8'> |
| 79 | <link rel="stylesheet" type="text/css" href="/style.css"> |
| 80 | </head> |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 81 | <body> |
| 82 | {table_data} |
Abhishek Arya | 26bc519a | 2017-10-03 15:52:59 | [diff] [blame] | 83 | </body></html>""" |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 84 | |
| 85 | SINGLE_FILE_START_MARKER = '<!doctype html><html>' |
| 86 | SINGLE_FILE_END_MARKER = '</body></html>' |
| 87 | |
| 88 | SOURCE_FILENAME_START_MARKER = ( |
| 89 | "<body><div class='centered'><table><div class='source-name-title'><pre>") |
| 90 | SOURCE_FILENAME_END_MARKER = '</pre>' |
| 91 | |
| 92 | STYLE_START_MARKER = '<style>' |
| 93 | STYLE_END_MARKER = '</style>' |
| 94 | STYLE_FILENAME = 'style.css' |
| 95 | |
| 96 | ZERO_FUNCTION_FILE_TEXT = 'Files which contain no functions' |
| 97 | |
Abhishek Arya | 6eb0ebc | 2017-10-03 17:09:56 | [diff] [blame] | 98 | HTTP_PORT = 9000 |
| 99 | COVERAGE_REPORT_LINK = 'http://127.0.0.1:%d/report.html' % HTTP_PORT |
| 100 | |
Max Moroz | c9213a4 | 2017-10-17 23:07:25 | [diff] [blame] | 101 | LARGE_BINARY_THRESHOLD = 128 * 2 ** 20 |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 102 | |
Max Moroz | c9213a4 | 2017-10-17 23:07:25 | [diff] [blame] | 103 | |
| 104 | def CheckBinaryAndArgs(executable_path, filters): |
| 105 | """Verify that the given file has been built with coverage instrumentation, |
| 106 | also perform check for "--filter" argument and for the binary size.""" |
| 107 | CheckFilterArgument(filters) |
| 108 | |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 109 | with open(executable_path) as file_handle: |
| 110 | data = file_handle.read() |
| 111 | |
Max Moroz | c9213a4 | 2017-10-17 23:07:25 | [diff] [blame] | 112 | if len(data) > LARGE_BINARY_THRESHOLD and not filters: |
| 113 | logging.warning('The target binary is quite large. Generating the full ' |
| 114 | 'coverage report may take a while. To generate the report ' |
| 115 | 'faster, consider using the "--filter" argument to specify ' |
| 116 | 'the source code files and directories shown in the report.' |
| 117 | ) |
| 118 | |
Abhishek Arya | 8fe0015 | 2017-10-04 15:47:24 | [diff] [blame] | 119 | # For minimum threshold reference, tiny "Hello World" program has count of 34. |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 120 | if data.count('__llvm_profile') > 20: |
| 121 | return |
| 122 | |
Abhishek Arya | 26bc519a | 2017-10-03 15:52:59 | [diff] [blame] | 123 | logging.error('It looks like the target binary has been compiled without ' |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 124 | 'coverage instrumentation.') |
| 125 | print('Have you used use_clang_coverage=true flag in GN args? [y/N]') |
| 126 | answer = raw_input() |
| 127 | if not answer.lower().startswith('y'): |
| 128 | print('Exiting.') |
| 129 | sys.exit(-1) |
| 130 | |
| 131 | |
Max Moroz | c9213a4 | 2017-10-17 23:07:25 | [diff] [blame] | 132 | def CheckFilterArgument(filters): |
| 133 | """Verify that all the paths specified in --filter arg exist.""" |
| 134 | for path in filters: |
| 135 | if not os.path.exists(path): |
| 136 | logging.error('The path specified does not exist: %s.' % path) |
| 137 | sys.exit(-1) |
| 138 | |
| 139 | |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 140 | def CreateOutputDir(dir_path): |
| 141 | """Create a directory for the script output files.""" |
| 142 | if not os.path.exists(dir_path): |
| 143 | os.mkdir(dir_path) |
| 144 | return |
| 145 | |
| 146 | if os.path.isdir(dir_path): |
Abhishek Arya | 26bc519a | 2017-10-03 15:52:59 | [diff] [blame] | 147 | logging.warning('%s already exists.', dir_path) |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 148 | return |
| 149 | |
Abhishek Arya | 26bc519a | 2017-10-03 15:52:59 | [diff] [blame] | 150 | logging.error('%s exists and does not point to a directory.', dir_path) |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 151 | raise Exception('Invalid --output argument specified.') |
| 152 | |
| 153 | |
| 154 | def DownloadCoverageToolsIfNeeded(): |
| 155 | """Temporary solution to download llvm-profdata and llvm-cov tools.""" |
| 156 | # TODO(mmoroz): remove this function once tools get included to Clang bundle: |
| 157 | # https://chromium-review.googlesource.com/c/chromium/src/+/688221 |
| 158 | clang_script_path = os.path.join(CHROME_SRC_PATH, 'tools', 'clang', 'scripts') |
| 159 | sys.path.append(clang_script_path) |
| 160 | import update as clang_update |
| 161 | import urllib2 |
| 162 | |
Abhishek Arya | 8fe0015 | 2017-10-04 15:47:24 | [diff] [blame] | 163 | def _GetRevisionFromStampFile(file_path): |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 164 | """Read the build stamp file created by tools/clang/scripts/update.py.""" |
Abhishek Arya | 8fe0015 | 2017-10-04 15:47:24 | [diff] [blame] | 165 | if not os.path.exists(file_path): |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 166 | return 0, 0 |
Abhishek Arya | 8fe0015 | 2017-10-04 15:47:24 | [diff] [blame] | 167 | |
| 168 | with open(file_path) as file_handle: |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 169 | revision_stamp_data = file_handle.readline().strip() |
| 170 | revision_stamp_data = revision_stamp_data.split('-') |
| 171 | return int(revision_stamp_data[0]), int(revision_stamp_data[1]) |
| 172 | |
| 173 | clang_revision, clang_sub_revision = _GetRevisionFromStampFile( |
| 174 | clang_update.STAMP_FILE) |
| 175 | |
Abhishek Arya | 26bc519a | 2017-10-03 15:52:59 | [diff] [blame] | 176 | coverage_revision_stamp_file = os.path.join( |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 177 | os.path.dirname(clang_update.STAMP_FILE), 'cr_coverage_revision') |
| 178 | coverage_revision, coverage_sub_revision = _GetRevisionFromStampFile( |
| 179 | coverage_revision_stamp_file) |
| 180 | |
| 181 | if (coverage_revision == clang_revision and |
| 182 | coverage_sub_revision == clang_sub_revision): |
| 183 | # LLVM coverage tools are up to date, bail out. |
Max Moroz | c9213a4 | 2017-10-17 23:07:25 | [diff] [blame] | 184 | return clang_revision |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 185 | |
| 186 | package_version = '%d-%d' % (clang_revision, clang_sub_revision) |
| 187 | coverage_tools_file = 'llvm-code-coverage-%s.tgz' % package_version |
| 188 | |
| 189 | # The code bellow follows the code from tools/clang/scripts/update.py. |
| 190 | if sys.platform == 'win32' or sys.platform == 'cygwin': |
| 191 | coverage_tools_url = clang_update.CDS_URL + '/Win/' + coverage_tools_file |
| 192 | elif sys.platform == 'darwin': |
| 193 | coverage_tools_url = clang_update.CDS_URL + '/Mac/' + coverage_tools_file |
| 194 | else: |
| 195 | assert sys.platform.startswith('linux') |
| 196 | coverage_tools_url = ( |
| 197 | clang_update.CDS_URL + '/Linux_x64/' + coverage_tools_file) |
| 198 | |
| 199 | try: |
| 200 | clang_update.DownloadAndUnpack(coverage_tools_url, |
| 201 | clang_update.LLVM_BUILD_DIR) |
| 202 | print('Coverage tools %s unpacked.' % package_version) |
| 203 | with open(coverage_revision_stamp_file, 'w') as file_handle: |
| 204 | file_handle.write(package_version) |
| 205 | file_handle.write('\n') |
| 206 | except urllib2.URLError: |
| 207 | raise Exception( |
| 208 | 'Failed to download coverage tools: %s.' % coverage_tools_url) |
| 209 | |
Max Moroz | c9213a4 | 2017-10-17 23:07:25 | [diff] [blame] | 210 | return clang_revision |
| 211 | |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 212 | |
| 213 | def ExtractAndFixFilename(data, source_dir): |
| 214 | """Extract full paths to source code files and replace with relative paths.""" |
| 215 | filename_start = data.find(SOURCE_FILENAME_START_MARKER) |
| 216 | if filename_start == -1: |
| 217 | logging.error('Failed to extract source code filename.') |
| 218 | raise Exception('Failed to process coverage dump.') |
| 219 | |
| 220 | filename_start += len(SOURCE_FILENAME_START_MARKER) |
| 221 | filename_end = data[filename_start:].find(SOURCE_FILENAME_END_MARKER) |
| 222 | if filename_end == -1: |
| 223 | logging.error('Failed to extract source code filename.') |
| 224 | raise Exception('Failed to process coverage dump.') |
| 225 | |
| 226 | filename_end += filename_start |
| 227 | |
Abhishek Arya | 26bc519a | 2017-10-03 15:52:59 | [diff] [blame] | 228 | filename = data[filename_start:filename_end] |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 229 | |
| 230 | source_dir = os.path.abspath(source_dir) |
| 231 | |
| 232 | if not filename.startswith(source_dir): |
| 233 | logging.error('Invalid source code path ("%s") specified.\n' |
Abhishek Arya | 26bc519a | 2017-10-03 15:52:59 | [diff] [blame] | 234 | 'Coverage dump refers to "%s".', source_dir, filename) |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 235 | raise Exception('Failed to process coverage dump.') |
| 236 | |
Abhishek Arya | 26bc519a | 2017-10-03 15:52:59 | [diff] [blame] | 237 | filename = filename[len(source_dir):] |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 238 | filename = filename.lstrip('/\\') |
| 239 | |
| 240 | # Replace the filename with the shorter version. |
Abhishek Arya | 26bc519a | 2017-10-03 15:52:59 | [diff] [blame] | 241 | data = data[:filename_start] + filename + data[filename_end:] |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 242 | return filename, data |
| 243 | |
| 244 | |
| 245 | def GenerateReport(report_data): |
| 246 | """Build HTML page with the summary report and links to individual files.""" |
Abhishek Arya | 2b30f1b8 | 2017-10-03 22:29:37 | [diff] [blame] | 247 | table_data = '<table class="centered">\n' |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 248 | report_lines = report_data.splitlines() |
| 249 | |
| 250 | # Write header. |
Abhishek Arya | 2b30f1b8 | 2017-10-03 22:29:37 | [diff] [blame] | 251 | table_data += ' <tr class="source-name-title">\n' |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 252 | for column in report_lines[0].split(' '): |
| 253 | if not column: |
| 254 | continue |
Abhishek Arya | 2b30f1b8 | 2017-10-03 22:29:37 | [diff] [blame] | 255 | table_data += ' <th><pre>%s</pre></th>\n' % column |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 256 | table_data += ' </tr>\n' |
| 257 | |
| 258 | for line in report_lines[1:-1]: |
| 259 | if not line or line.startswith('---'): |
| 260 | continue |
| 261 | |
| 262 | if line.startswith(ZERO_FUNCTION_FILE_TEXT): |
Abhishek Arya | 2b30f1b8 | 2017-10-03 22:29:37 | [diff] [blame] | 263 | table_data += ' <tr class="source-name-title">\n' |
Abhishek Arya | 8fe0015 | 2017-10-04 15:47:24 | [diff] [blame] | 264 | table_data += ( |
| 265 | ' <th class="column-entry-left"><pre>%s</pre></th>\n' % line) |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 266 | table_data += ' </tr>\n' |
| 267 | continue |
| 268 | |
| 269 | table_data += ' <tr>\n' |
| 270 | |
| 271 | columns = line.split() |
| 272 | |
| 273 | # First column is a file name, build a link. |
Abhishek Arya | 2b30f1b8 | 2017-10-03 22:29:37 | [diff] [blame] | 274 | table_data += (' <td class="column-entry-left">\n' |
| 275 | ' <a href="/%s"><pre>%s</pre></a>\n' |
Abhishek Arya | 8fe0015 | 2017-10-04 15:47:24 | [diff] [blame] | 276 | ' </td>\n') % (columns[0] + HTML_FILE_EXTENSION, |
| 277 | columns[0]) |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 278 | |
Abhishek Arya | 8fe0015 | 2017-10-04 15:47:24 | [diff] [blame] | 279 | for column in columns[1:]: |
Abhishek Arya | 2b30f1b8 | 2017-10-03 22:29:37 | [diff] [blame] | 280 | table_data += ' <td class="column-entry"><pre>%s</pre></td>\n' % column |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 281 | table_data += ' </tr>\n' |
| 282 | |
| 283 | # Write the last "TOTAL" row. |
Abhishek Arya | 2b30f1b8 | 2017-10-03 22:29:37 | [diff] [blame] | 284 | table_data += ' <tr class="source-name-title">\n' |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 285 | for column in report_lines[-1].split(): |
Abhishek Arya | 2b30f1b8 | 2017-10-03 22:29:37 | [diff] [blame] | 286 | table_data += ' <td class="column-entry"><pre>%s</pre></td>\n' % column |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 287 | table_data += ' </tr>\n' |
| 288 | table_data += '</table>\n' |
| 289 | |
| 290 | return REPORT_TEMPLATE.format(table_data=table_data) |
| 291 | |
| 292 | |
Max Moroz | c9213a4 | 2017-10-17 23:07:25 | [diff] [blame] | 293 | def GenerateSources(executable_path, output_dir, source_dir, filters, |
| 294 | coverage_file): |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 295 | """Generate coverage visualization for source code files.""" |
Abhishek Arya | 26bc519a | 2017-10-03 15:52:59 | [diff] [blame] | 296 | llvm_cov_command = [ |
| 297 | LLVM_COV_PATH, 'show', '-format=html', executable_path, |
| 298 | '-instr-profile=%s' % coverage_file |
| 299 | ] |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 300 | |
Max Moroz | c9213a4 | 2017-10-17 23:07:25 | [diff] [blame] | 301 | for path in filters: |
| 302 | llvm_cov_command.append(path) |
| 303 | |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 304 | data = subprocess.check_output(llvm_cov_command) |
| 305 | |
| 306 | # Extract CSS style from the data. |
| 307 | style_start = data.find(STYLE_START_MARKER) |
| 308 | style_end = data.find(STYLE_END_MARKER) |
| 309 | if style_end <= style_start or style_start == -1: |
| 310 | logging.error('Failed to extract CSS style from coverage report.') |
| 311 | raise Exception('Failed to process coverage dump.') |
| 312 | |
Abhishek Arya | 26bc519a | 2017-10-03 15:52:59 | [diff] [blame] | 313 | style_data = data[style_start + len(STYLE_START_MARKER):style_end] |
Abhishek Arya | 2b30f1b8 | 2017-10-03 22:29:37 | [diff] [blame] | 314 | |
| 315 | # Add hover for table <tr>. |
Abhishek Arya | 8fe0015 | 2017-10-04 15:47:24 | [diff] [blame] | 316 | style_data += '\ntr:hover { background-color: #eee; }' |
Abhishek Arya | 2b30f1b8 | 2017-10-03 22:29:37 | [diff] [blame] | 317 | |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 318 | with open(os.path.join(output_dir, STYLE_FILENAME), 'w') as file_handle: |
| 319 | file_handle.write(style_data) |
| 320 | style_length = ( |
| 321 | len(style_data) + len(STYLE_START_MARKER) + len(STYLE_END_MARKER)) |
| 322 | |
Abhishek Arya | 8fe0015 | 2017-10-04 15:47:24 | [diff] [blame] | 323 | # Extract every source code file. Use "offset" to avoid creating new strings. |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 324 | offset = 0 |
| 325 | while True: |
| 326 | file_start = data.find(SINGLE_FILE_START_MARKER, offset) |
| 327 | if file_start == -1: |
| 328 | break |
| 329 | |
| 330 | file_end = data.find(SINGLE_FILE_END_MARKER, offset) |
| 331 | if file_end == -1: |
| 332 | break |
| 333 | |
| 334 | file_end += len(SINGLE_FILE_END_MARKER) |
| 335 | offset += file_end - file_start |
| 336 | |
| 337 | # Remove <style> as it's always the same and has been extracted separately. |
Abhishek Arya | 26bc519a | 2017-10-03 15:52:59 | [diff] [blame] | 338 | file_data = ReplaceStyleWithCss(data[file_start:file_end], style_length, |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 339 | STYLE_FILENAME) |
| 340 | |
| 341 | filename, file_data = ExtractAndFixFilename(file_data, source_dir) |
Abhishek Arya | 8fe0015 | 2017-10-04 15:47:24 | [diff] [blame] | 342 | file_path = os.path.join(output_dir, filename) |
| 343 | dirname = os.path.dirname(file_path) |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 344 | |
| 345 | try: |
| 346 | os.makedirs(dirname) |
| 347 | except OSError: |
| 348 | pass |
| 349 | |
Abhishek Arya | 8fe0015 | 2017-10-04 15:47:24 | [diff] [blame] | 350 | with open(file_path + HTML_FILE_EXTENSION, 'w') as file_handle: |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 351 | file_handle.write(file_data) |
| 352 | |
| 353 | |
Max Moroz | c9213a4 | 2017-10-17 23:07:25 | [diff] [blame] | 354 | def GenerateSummary(executable_path, output_dir, filters, coverage_file, |
| 355 | clang_revision): |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 356 | """Generate code coverage summary report (i.e. a table with all files).""" |
Abhishek Arya | 26bc519a | 2017-10-03 15:52:59 | [diff] [blame] | 357 | llvm_cov_command = [ |
| 358 | LLVM_COV_PATH, 'report', executable_path, |
| 359 | '-instr-profile=%s' % coverage_file |
| 360 | ] |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 361 | |
Max Moroz | c9213a4 | 2017-10-17 23:07:25 | [diff] [blame] | 362 | for path in filters: |
| 363 | llvm_cov_command.append(path) |
| 364 | |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 365 | data = subprocess.check_output(llvm_cov_command) |
| 366 | report = GenerateReport(data) |
| 367 | |
| 368 | with open(os.path.join(output_dir, REPORT_FILENAME), 'w') as file_handle: |
Max Moroz | c9213a4 | 2017-10-17 23:07:25 | [diff] [blame] | 369 | # TODO(mmoroz): remove this hacky warning after next clang roll. |
| 370 | if filters and clang_revision < 315685: |
| 371 | report = ('Warning: the report below contains information for all the ' |
| 372 | 'sources even though you used "--filter" option. This bug has ' |
| 373 | 'been fixed upstream. It will be fixed in Chromium after next ' |
| 374 | 'clang roll (https://reviews.llvm.org/rL315685).<br>' + report) |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 375 | file_handle.write(report) |
| 376 | |
| 377 | |
Abhishek Arya | 6eb0ebc | 2017-10-03 17:09:56 | [diff] [blame] | 378 | def ServeReportOnHTTP(output_directory): |
| 379 | """Serve report directory on HTTP.""" |
| 380 | os.chdir(output_directory) |
| 381 | |
| 382 | SocketServer.TCPServer.allow_reuse_address = True |
| 383 | httpd = SocketServer.TCPServer(('', HTTP_PORT), |
| 384 | SimpleHTTPServer.SimpleHTTPRequestHandler) |
| 385 | print('Load coverage report using %s. Press Ctrl+C to exit.' % |
| 386 | COVERAGE_REPORT_LINK) |
| 387 | |
| 388 | try: |
| 389 | httpd.serve_forever() |
| 390 | except KeyboardInterrupt: |
| 391 | httpd.server_close() |
| 392 | |
| 393 | |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 394 | def ProcessCoverageDump(profile_file, coverage_file): |
| 395 | """Process and convert raw LLVM profile data into coverage data format.""" |
| 396 | print('Processing coverage dump and generating visualization.') |
Abhishek Arya | 26bc519a | 2017-10-03 15:52:59 | [diff] [blame] | 397 | merge_command = [ |
| 398 | LLVM_PROFDATA_PATH, 'merge', '-sparse', profile_file, '-o', coverage_file |
| 399 | ] |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 400 | data = subprocess.check_output(merge_command) |
| 401 | |
| 402 | if not os.path.exists(coverage_file) or not os.path.getsize(coverage_file): |
Abhishek Arya | 26bc519a | 2017-10-03 15:52:59 | [diff] [blame] | 403 | logging.error('%s is either not created or empty:\n%s', coverage_file, data) |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 404 | raise Exception('Failed to merge coverage information after command run.') |
| 405 | |
| 406 | |
| 407 | def ReplaceStyleWithCss(data, style_data_length, css_file_path): |
| 408 | """Replace <style></style> data with the include of common style.css file.""" |
| 409 | style_start = data.find(STYLE_START_MARKER) |
| 410 | # Since "style" data is always the same, try some optimization here. |
| 411 | style_end = style_start + style_data_length |
| 412 | if (style_end > len(data) or |
Abhishek Arya | 26bc519a | 2017-10-03 15:52:59 | [diff] [blame] | 413 | data[style_end - len(STYLE_END_MARKER):style_end] != STYLE_END_MARKER): |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 414 | # Looks like our optimization has failed, find end of "style" data. |
| 415 | style_end = data.find(STYLE_END_MARKER) |
| 416 | if style_end <= style_start or style_start == -1: |
| 417 | logging.error('Failed to extract CSS style from coverage report.') |
| 418 | raise Exception('Failed to process coverage dump.') |
| 419 | style_end += len(STYLE_END_MARKER) |
| 420 | |
| 421 | css_include = ( |
| 422 | '<link rel="stylesheet" type="text/css" href="/%s">' % css_file_path) |
Abhishek Arya | 26bc519a | 2017-10-03 15:52:59 | [diff] [blame] | 423 | result = '\n'.join([data[:style_start], css_include, data[style_end:]]) |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 424 | return result |
| 425 | |
| 426 | |
| 427 | def RunCommand(command, profile_file): |
| 428 | """Run the given command in order to generate raw LLVM profile data.""" |
| 429 | print('Running "%s".' % command) |
| 430 | print('-' * 80) |
| 431 | os.environ['LLVM_PROFILE_FILE'] = profile_file |
| 432 | os.system(command) |
| 433 | print('-' * 80) |
| 434 | print('Finished command execution.') |
| 435 | |
| 436 | if not os.path.exists(profile_file) or not os.path.getsize(profile_file): |
Abhishek Arya | 26bc519a | 2017-10-03 15:52:59 | [diff] [blame] | 437 | logging.error('%s is either not created or empty.', profile_file) |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 438 | raise Exception('Failed to dump coverage information during command run.') |
| 439 | |
| 440 | |
| 441 | def main(): |
| 442 | """The main routing for processing the arguments and generating coverage.""" |
| 443 | parser = argparse.ArgumentParser( |
| 444 | description=HELP_MESSAGE, |
| 445 | formatter_class=argparse.RawDescriptionHelpFormatter) |
Abhishek Arya | 26bc519a | 2017-10-03 15:52:59 | [diff] [blame] | 446 | parser.add_argument( |
| 447 | '--command', |
| 448 | required=True, |
Abhishek Arya | 8fe0015 | 2017-10-04 15:47:24 | [diff] [blame] | 449 | help='The command to run target binary for which code coverage is ' |
| 450 | 'required.') |
Abhishek Arya | 26bc519a | 2017-10-03 15:52:59 | [diff] [blame] | 451 | parser.add_argument( |
| 452 | '--source', |
| 453 | required=False, |
| 454 | default=CHROME_SRC_PATH, |
Abhishek Arya | 8fe0015 | 2017-10-04 15:47:24 | [diff] [blame] | 455 | help='Location of chromium source checkout, if it differs from ' |
| 456 | 'current checkout: %s.' % CHROME_SRC_PATH) |
Abhishek Arya | 26bc519a | 2017-10-03 15:52:59 | [diff] [blame] | 457 | parser.add_argument( |
| 458 | '--output', |
| 459 | required=True, |
Abhishek Arya | 8fe0015 | 2017-10-04 15:47:24 | [diff] [blame] | 460 | help='Directory where code coverage files will be written to.') |
Max Moroz | c9213a4 | 2017-10-17 23:07:25 | [diff] [blame] | 461 | parser.add_argument( |
| 462 | '--filter', |
| 463 | required=False, |
| 464 | nargs='+', |
| 465 | default = [], |
| 466 | help='(Optional) Paths to source code files/directories shown in the ' |
| 467 | 'report. By default, the report shows all the sources compiled and ' |
| 468 | 'linked into the target executable.') |
Abhishek Arya | 8fe0015 | 2017-10-04 15:47:24 | [diff] [blame] | 469 | |
| 470 | if not len(sys.argv[1:]): |
| 471 | # Print help when no arguments are provided on command line. |
| 472 | parser.print_help() |
| 473 | parser.exit() |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 474 | |
| 475 | args = parser.parse_args() |
| 476 | |
| 477 | executable_path = args.command.split()[0] |
Abhishek Arya | 6eb0ebc | 2017-10-03 17:09:56 | [diff] [blame] | 478 | |
Max Moroz | c9213a4 | 2017-10-17 23:07:25 | [diff] [blame] | 479 | CheckBinaryAndArgs(executable_path, args.filter) |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 480 | |
Max Moroz | c9213a4 | 2017-10-17 23:07:25 | [diff] [blame] | 481 | clang_revision = DownloadCoverageToolsIfNeeded() |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 482 | |
| 483 | CreateOutputDir(args.output) |
| 484 | profile_file = os.path.join(args.output, LLVM_PROFILE_FILE_NAME) |
| 485 | RunCommand(args.command, profile_file) |
| 486 | |
| 487 | coverage_file = os.path.join(args.output, LLVM_COVERAGE_FILE_NAME) |
| 488 | ProcessCoverageDump(profile_file, coverage_file) |
| 489 | |
Max Moroz | c9213a4 | 2017-10-17 23:07:25 | [diff] [blame] | 490 | GenerateSummary(executable_path, args.output, args.filter, coverage_file, |
| 491 | clang_revision) |
| 492 | GenerateSources(executable_path, args.output, args.source, args.filter, |
| 493 | coverage_file) |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 494 | |
Abhishek Arya | 6eb0ebc | 2017-10-03 17:09:56 | [diff] [blame] | 495 | ServeReportOnHTTP(args.output) |
| 496 | |
| 497 | print('Done.') |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 498 | |
| 499 | |
Abhishek Arya | 26bc519a | 2017-10-03 15:52:59 | [diff] [blame] | 500 | if __name__ == '__main__': |
Max Moroz | 63d7ff9 | 2017-10-02 21:40:39 | [diff] [blame] | 501 | main() |