• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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