OSDN Git Service

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