1#!/usr/bin/env python 2# 3# Copyright 2018, The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17import argparse 18import datetime 19import logging 20import json 21import os 22import shutil 23import subprocess 24import sys 25import webbrowser 26 27from run_host_unit_tests import * 28""" 29This script is used to generate code coverage results host supported libraries. 30The script by default will generate an html report that summarizes the coverage 31results of the specified tests. The results can also be browsed to provide a 32report of which lines have been traveled upon execution of the binary. 33 34NOTE: Code that is compiled out or hidden by a #DEFINE will be listed as 35having been executed 0 times, thus reducing overall coverage. 36 37The steps in order to add coverage support to a new library and its 38corrisponding host test are as follows. 39 401. Add "clang_file_coverage" (defined in //build/Android.bp) as a default to the 41 source library(s) you want statistics for. 42 NOTE: Forgoing this default will cause no coverage data to be generated for 43 the source files in the library. 44 452. Add "clang_coverage_bin" as a default to the host supported test binary that 46 excercises the libraries that you covered in step 1. 47 NOTE: Forgetting to add this will cause there to be *NO* coverage data 48 generated when the binary is run. 49 503. Add the host test binary name and the files/directories you want coverage 51 statistics for to the COVERAGE_TESTS variable defined below. You may add 52 individual filenames or a directory to be tested. 53 NOTE: Avoid using a / at the beginning of a covered_files entry as this 54 breaks how the coverage generator resolves filenames. 55 56TODO: Support generating XML data and printing results to standard out. 57""" 58 59COVERAGE_TESTS = [ 60 { 61 "test_name": "net_test_avrcp", 62 "covered_files": [ 63 "packages/modules/Bluetooth/system/profile/avrcp", 64 ], 65 }, 66 { 67 "test_name": "bluetooth_test_sdp", 68 "covered_files": [ 69 "packages/modules/Bluetooth/system/profile/sdp", 70 ], 71 }, 72 { 73 "test_name": "rootcanal_test_host", 74 "covered_files": [ 75 "packages/modules/Bluetooth/tools/rootcanal/include", 76 "packages/modules/Bluetooth/tools/rootcanal/src", 77 ], 78 }, 79 { 80 "test_name": "rootcanal-packets_test_host", 81 "covered_files": [ 82 "packages/modules/Bluetooth/tools/rootcanal/packets", 83 ], 84 }, 85 { 86 "test_name": "bluetooth_test_common", 87 "covered_files": [ 88 "packages/modules/Bluetooth/system/common", 89 ], 90 }, 91] 92 93WORKING_DIR = '/tmp/coverage' 94SOONG_UI_BASH = 'build/soong/soong_ui.bash' 95LLVM_DIR = 'prebuilts/clang/host/linux-x86/clang-r353983b/bin' 96LLVM_MERGE = LLVM_DIR + '/llvm-profdata' 97LLVM_COV = LLVM_DIR + '/llvm-cov' 98 99 100def write_root_html_head(f): 101 # Write the header part of the root html file. This was pulled from the 102 # page source of one of the generated html files. 103 f.write("<!doctype html><html><head>" \ 104 "<meta name='viewport' content='width=device-width,initial-scale=1'><met" \ 105 "a charset='UTF-8'><link rel='stylesheet' type='text/css' href='style.cs" \ 106 "s'></head><body><h2>Coverage Report</h2><h4>Created: " + 107 str(datetime.datetime.now().strftime('%Y-%m-%d %H:%M')) + 108 "</h4><p>Click <a href='http://clang.llvm.org/docs/SourceBasedCodeCovera" \ 109 "ge.html#interpreting-reports'>here</a> for information about interpreti" \ 110 "ng this report.</p><div class='centered'><table><tr><td class='column-e" \ 111 "ntry-bold'>Filename</td><td class='column-entry-bold'>Function Coverage" \ 112 "</td><td class='column-entry-bold'>Instantiation Coverage</td><td class" \ 113 "='column-entry-bold'>Line Coverage</td><td class='column-entry-bold'>Re" \ 114 "gion Coverage</td></tr>" 115 ) 116 117 118def write_root_html_column(f, covered, count): 119 percent = covered * 100.0 / count 120 value = "%.2f%% (%d/%d) " % (percent, covered, count) 121 color = 'column-entry-yellow' 122 if percent == 100: 123 color = 'column-entry-green' 124 if percent < 80.0: 125 color = 'column-entry-red' 126 f.write("<td class=\'" + color + "\'><pre>" + value + "</pre></td>") 127 128 129def write_root_html_rows(f, tests): 130 totals = { 131 "functions": { 132 "covered": 0, 133 "count": 0 134 }, 135 "instantiations": { 136 "covered": 0, 137 "count": 0 138 }, 139 "lines": { 140 "covered": 0, 141 "count": 0 142 }, 143 "regions": { 144 "covered": 0, 145 "count": 0 146 } 147 } 148 149 # Write the tests with their coverage summaries. 150 for test in tests: 151 test_name = test['test_name'] 152 covered_files = test['covered_files'] 153 json_results = generate_coverage_json(test) 154 test_totals = json_results['data'][0]['totals'] 155 156 f.write("<tr class='light-row'><td><pre><a href=\'" + os.path.join(test_name, "index.html") + "\'>" + 157 test_name + "</a></pre></td>") 158 for field_name in ['functions', 'instantiations', 'lines', 'regions']: 159 field = test_totals[field_name] 160 totals[field_name]['covered'] += field['covered'] 161 totals[field_name]['count'] += field['count'] 162 write_root_html_column(f, field['covered'], field['count']) 163 f.write("</tr>") 164 165 #Write the totals row. 166 f.write("<tr class='light-row-bold'><td><pre>Totals</a></pre></td>") 167 for field_name in ['functions', 'instantiations', 'lines', 'regions']: 168 field = totals[field_name] 169 write_root_html_column(f, field['covered'], field['count']) 170 f.write("</tr>") 171 172 173def write_root_html_tail(f): 174 # Pulled from the generated html coverage report. 175 f.write("</table></div><h5>Generated by llvm-cov -- llvm version 7.0.2svn<" \ 176 "/h5></body></html>") 177 178 179def generate_root_html(tests): 180 # Copy the css file from one of the coverage reports. 181 source_file = os.path.join(os.path.join(WORKING_DIR, tests[0]['test_name']), "style.css") 182 dest_file = os.path.join(WORKING_DIR, "style.css") 183 shutil.copy2(source_file, dest_file) 184 185 # Write the root index.html file that sumarizes all the tests. 186 f = open(os.path.join(WORKING_DIR, "index.html"), "w") 187 write_root_html_head(f) 188 write_root_html_rows(f, tests) 189 write_root_html_tail(f) 190 191 192def get_profraw_for_test(test_name): 193 test_root = get_native_test_root_or_die() 194 test_cmd = os.path.join(os.path.join(test_root, test_name), test_name) 195 if not os.path.isfile(test_cmd): 196 logging.error('The test ' + test_name + ' does not exist, please compile first') 197 sys.exit(1) 198 199 profraw_file_name = test_name + ".profraw" 200 profraw_path = os.path.join(WORKING_DIR, os.path.join(test_name, profraw_file_name)) 201 llvm_env_var = "LLVM_PROFILE_FILE=\"" + profraw_path + "\"" 202 203 test_cmd = llvm_env_var + " " + test_cmd 204 logging.info('Generating profraw data for ' + test_name) 205 logging.debug('cmd: ' + test_cmd) 206 if subprocess.call(test_cmd, shell=True) != 0: 207 logging.error('Test ' + test_name + ' failed. Please fix the test before generating coverage.') 208 sys.exit(1) 209 210 if not os.path.isfile(profraw_path): 211 logging.error( 212 'Generating the profraw file failed. Did you remember to add the proper compiler flags to your build?') 213 sys.exit(1) 214 215 return profraw_file_name 216 217 218def merge_profraw_data(test_name): 219 cmd = [] 220 cmd.append(os.path.join(get_android_root_or_die(), LLVM_MERGE + " merge ")) 221 222 test_working_dir = os.path.join(WORKING_DIR, test_name) 223 cmd.append(os.path.join(test_working_dir, test_name + ".profraw")) 224 profdata_file = os.path.join(test_working_dir, test_name + ".profdata") 225 226 cmd.append('-o ' + profdata_file) 227 logging.info('Combining profraw files into profdata for ' + test_name) 228 logging.debug('cmd: ' + " ".join(cmd)) 229 if subprocess.call(" ".join(cmd), shell=True) != 0: 230 logging.error('Failed to merge profraw files for ' + test_name) 231 sys.exit(1) 232 233 234def generate_coverage_html(test): 235 COVERAGE_ROOT = '/proc/self/cwd' 236 237 test_name = test['test_name'] 238 file_list = test['covered_files'] 239 240 test_working_dir = os.path.join(WORKING_DIR, test_name) 241 test_profdata_file = os.path.join(test_working_dir, test_name + ".profdata") 242 243 cmd = [ 244 os.path.join(get_android_root_or_die(), LLVM_COV), "show", "-format=html", "-summary-only", 245 "-show-line-counts-or-regions", "-show-instantiation-summary", "-instr-profile=" + test_profdata_file, 246 "-path-equivalence=\"" + COVERAGE_ROOT + "\",\"" + get_android_root_or_die() + "\"", 247 "-output-dir=" + test_working_dir 248 ] 249 250 # We have to have one object file not as an argument otherwise we can't specify source files. 251 test_cmd = os.path.join(os.path.join(get_native_test_root_or_die(), test_name), test_name) 252 cmd.append(test_cmd) 253 254 # Filter out the specific files we want coverage for 255 for filename in file_list: 256 cmd.append(os.path.join(get_android_root_or_die(), filename)) 257 258 logging.info('Generating coverage report for ' + test['test_name']) 259 logging.debug('cmd: ' + " ".join(cmd)) 260 if subprocess.call(" ".join(cmd), shell=True) != 0: 261 logging.error('Failed to generate coverage for ' + test['test_name']) 262 sys.exit(1) 263 264 265def generate_coverage_json(test): 266 COVERAGE_ROOT = '/proc/self/cwd' 267 test_name = test['test_name'] 268 file_list = test['covered_files'] 269 270 test_working_dir = os.path.join(WORKING_DIR, test_name) 271 test_profdata_file = os.path.join(test_working_dir, test_name + ".profdata") 272 273 cmd = [ 274 os.path.join(get_android_root_or_die(), LLVM_COV), 275 "export", 276 "-summary-only", 277 "-show-region-summary", 278 "-instr-profile=" + test_profdata_file, 279 "-path-equivalence=\"" + COVERAGE_ROOT + "\",\"" + get_android_root_or_die() + "\"", 280 ] 281 282 test_cmd = os.path.join(os.path.join(get_native_test_root_or_die(), test_name), test_name) 283 cmd.append(test_cmd) 284 285 # Filter out the specific files we want coverage for 286 for filename in file_list: 287 cmd.append(os.path.join(get_android_root_or_die(), filename)) 288 289 logging.info('Generating coverage json for ' + test['test_name']) 290 logging.debug('cmd: ' + " ".join(cmd)) 291 292 json_str = subprocess.check_output(" ".join(cmd), shell=True) 293 return json.loads(json_str) 294 295 296def write_json_summary(test): 297 test_name = test['test_name'] 298 test_working_dir = os.path.join(WORKING_DIR, test_name) 299 test_json_summary_file = os.path.join(test_working_dir, test_name + '.json') 300 logging.debug('Writing json summary file: ' + test_json_summary_file) 301 json_file = open(test_json_summary_file, 'w') 302 json.dump(generate_coverage_json(test), json_file) 303 json_file.close() 304 305 306def list_tests(): 307 for test in COVERAGE_TESTS: 308 print "Test Name: " + test['test_name'] 309 print "Covered Files: " 310 for covered_file in test['covered_files']: 311 print " " + covered_file 312 print 313 314 315def main(): 316 parser = argparse.ArgumentParser(description='Generate code coverage for enabled tests.') 317 parser.add_argument( 318 '-l', 319 '--list-tests', 320 action='store_true', 321 dest='list_tests', 322 help='List all the available tests to be run as well as covered files.') 323 parser.add_argument( 324 '-a', '--all', 325 action='store_true', 326 help='Runs all available tests and prints their outputs. If no tests ' \ 327 'are specified via the -t option all tests will be run.') 328 parser.add_argument( 329 '-t', '--test', 330 dest='tests', 331 action='append', 332 type=str, 333 metavar='TESTNAME', 334 default=[], 335 help='Specifies a test to be run. Multiple tests can be specified by ' \ 336 'using this option multiple times. ' \ 337 'Example: \"gen_coverage.py -t test1 -t test2\"') 338 parser.add_argument( 339 '-o', '--output', 340 type=str, 341 metavar='DIRECTORY', 342 default='/tmp/coverage', 343 help='Specifies the directory to store all files. The directory will be ' \ 344 'created if it does not exist. Default is \"/tmp/coverage\"') 345 parser.add_argument( 346 '-s', 347 '--skip-html', 348 dest='skip_html', 349 action='store_true', 350 help='Skip opening up the results of the coverage report in a browser.') 351 parser.add_argument( 352 '-j', 353 '--json-file', 354 dest='json_file', 355 action='store_true', 356 help='Write out summary results to json file in test directory.') 357 358 logging.basicConfig(stream=sys.stderr, level=logging.DEBUG, format='%(levelname)s %(message)s') 359 logging.addLevelName(logging.DEBUG, "[\033[1;34m%s\033[0m]" % logging.getLevelName(logging.DEBUG)) 360 logging.addLevelName(logging.INFO, "[\033[1;34m%s\033[0m]" % logging.getLevelName(logging.INFO)) 361 logging.addLevelName(logging.WARNING, "[\033[1;31m%s\033[0m]" % logging.getLevelName(logging.WARNING)) 362 logging.addLevelName(logging.ERROR, "[\033[1;31m%s\033[0m]" % logging.getLevelName(logging.ERROR)) 363 364 args = parser.parse_args() 365 logging.debug("Args: " + str(args)) 366 367 # Set the working directory 368 global WORKING_DIR 369 WORKING_DIR = os.path.abspath(args.output) 370 logging.debug("Working Dir: " + WORKING_DIR) 371 372 # Print out the list of tests then exit 373 if args.list_tests: 374 list_tests() 375 sys.exit(0) 376 377 # Check to see if a test was specified and if so only generate coverage for 378 # that test. 379 if len(args.tests) == 0: 380 args.all = True 381 382 tests_to_run = [] 383 for test in COVERAGE_TESTS: 384 if args.all or test['test_name'] in args.tests: 385 tests_to_run.append(test) 386 if test['test_name'] in args.tests: 387 args.tests.remove(test['test_name']) 388 389 # Error if a test was specified but doesn't exist. 390 if len(args.tests) != 0: 391 for test_name in args.tests: 392 logging.error('\"' + test_name + '\" was not found in the list of available tests.') 393 sys.exit(1) 394 395 # Generate the info for the tests 396 for test in tests_to_run: 397 logging.info('Getting coverage for ' + test['test_name']) 398 get_profraw_for_test(test['test_name']) 399 merge_profraw_data(test['test_name']) 400 if args.json_file: 401 write_json_summary(test) 402 generate_coverage_html(test) 403 404 # Generate the root index.html page that sumarizes all of the coverage reports. 405 generate_root_html(tests_to_run) 406 407 # Open the results in a browser. 408 if not args.skip_html: 409 webbrowser.open('file://' + os.path.join(WORKING_DIR, 'index.html')) 410 411 412if __name__ == '__main__': 413 main() 414