blob: d6b6f80e820c141fd9c43cc311e4ace50cab7baf [file] [log] [blame]
Ryan Harrison574366b2017-07-18 10:18:55 -04001#!/usr/bin/env python
2# Copyright 2017 The PDFium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
Ryan Harrisona7b65b82018-05-30 19:56:11 +00005"""Generates a coverage report for given tests.
Ryan Harrison574366b2017-07-18 10:18:55 -04006
Ryan Harrisona7b65b82018-05-30 19:56:11 +00007Requires that 'use_clang_coverage = true' is set in args.gn.
8Prefers that 'is_component_build = false' is set in args.gn.
Ryan Harrison574366b2017-07-18 10:18:55 -04009"""
10
11import argparse
12from collections import namedtuple
Ryan Harrisona7b65b82018-05-30 19:56:11 +000013import fnmatch
Ryan Harrison574366b2017-07-18 10:18:55 -040014import os
Henrique Nakashimad9b0dac2017-08-09 16:43:25 -040015import pprint
Ryan Harrison574366b2017-07-18 10:18:55 -040016import subprocess
17import sys
18
Henrique Nakashimad9b0dac2017-08-09 16:43:25 -040019# Add src dir to path to avoid having to set PYTHONPATH.
20sys.path.append(
21 os.path.abspath(
Ryan Harrisona7b65b82018-05-30 19:56:11 +000022 os.path.join(
23 os.path.dirname(__file__), os.path.pardir, os.path.pardir,
24 os.path.pardir)))
Henrique Nakashimad9b0dac2017-08-09 16:43:25 -040025
26from testing.tools.common import GetBooleanGnArg
27
Ryan Harrison574366b2017-07-18 10:18:55 -040028# 'binary' is the file that is to be run for the test.
29# 'use_test_runner' indicates if 'binary' depends on test_runner.py and thus
30# requires special handling.
Ryan Harrison05590922017-07-28 14:46:08 -040031TestSpec = namedtuple('TestSpec', 'binary, use_test_runner')
Ryan Harrison574366b2017-07-18 10:18:55 -040032
33# All of the coverage tests that the script knows how to run.
34COVERAGE_TESTS = {
35 'pdfium_unittests': TestSpec('pdfium_unittests', False),
36 'pdfium_embeddertests': TestSpec('pdfium_embeddertests', False),
37 'corpus_tests': TestSpec('run_corpus_tests.py', True),
38 'javascript_tests': TestSpec('run_javascript_tests.py', True),
39 'pixel_tests': TestSpec('run_pixel_tests.py', True),
40}
41
Ryan Harrison574366b2017-07-18 10:18:55 -040042
43class CoverageExecutor(object):
44
45 def __init__(self, parser, args):
46 """Initialize executor based on the current script environment
47
48 Args:
49 parser: argparse.ArgumentParser for handling improper inputs.
50 args: Dictionary of arguments passed into the calling script.
51 """
52 self.dry_run = args['dry_run']
53 self.verbose = args['verbose']
54
Ryan Harrison574366b2017-07-18 10:18:55 -040055 self.source_directory = args['source_directory']
56 if not os.path.isdir(self.source_directory):
57 parser.error("'%s' needs to be a directory" % self.source_directory)
58
Ryan Harrisona7b65b82018-05-30 19:56:11 +000059 self.llvm_directory = os.path.join(self.source_directory, 'third_party',
60 'llvm-build', 'Release+Asserts', 'bin')
61 if not os.path.isdir(self.llvm_directory):
62 parser.error("Cannot find LLVM bin directory , expected it to be in '%s'"
63 % self.llvm_directory)
64
Ryan Harrison574366b2017-07-18 10:18:55 -040065 self.build_directory = args['build_directory']
66 if not os.path.isdir(self.build_directory):
67 parser.error("'%s' needs to be a directory" % self.build_directory)
68
Ryan Harrisona7b65b82018-05-30 19:56:11 +000069 (self.coverage_tests,
70 self.build_targets) = self.calculate_coverage_tests(args)
Ryan Harrison574366b2017-07-18 10:18:55 -040071 if not self.coverage_tests:
72 parser.error(
73 'No valid tests in set to be run. This is likely due to bad command '
74 'line arguments')
75
Ryan Harrisona7b65b82018-05-30 19:56:11 +000076 if not GetBooleanGnArg('use_clang_coverage', self.build_directory,
77 self.verbose):
Ryan Harrison574366b2017-07-18 10:18:55 -040078 parser.error(
Ryan Harrisona7b65b82018-05-30 19:56:11 +000079 'use_clang_coverage does not appear to be set to true for build, but is '
Ryan Harrison574366b2017-07-18 10:18:55 -040080 'needed')
81
Henrique Nakashimad9b0dac2017-08-09 16:43:25 -040082 self.use_goma = GetBooleanGnArg('use_goma', self.build_directory,
83 self.verbose)
Ryan Harrison574366b2017-07-18 10:18:55 -040084
85 self.output_directory = args['output_directory']
86 if not os.path.exists(self.output_directory):
87 if not self.dry_run:
Ryan Harrison52f24292017-07-31 11:36:45 -040088 os.makedirs(self.output_directory)
Ryan Harrison574366b2017-07-18 10:18:55 -040089 elif not os.path.isdir(self.output_directory):
90 parser.error('%s exists, but is not a directory' % self.output_directory)
Ryan Harrisona7b65b82018-05-30 19:56:11 +000091 elif len(os.listdir(self.output_directory)) > 0:
92 parser.error('%s is not empty, cowardly refusing to continue' %
93 self.output_directory)
94
95 self.prof_data = os.path.join(self.output_directory, 'pdfium.profdata')
Ryan Harrison574366b2017-07-18 10:18:55 -040096
Ryan Harrison574366b2017-07-18 10:18:55 -040097 def check_output(self, args, dry_run=False, env=None):
98 """Dry run aware wrapper of subprocess.check_output()"""
99 if dry_run:
100 print "Would have run '%s'" % ' '.join(args)
101 return ''
102
103 output = subprocess.check_output(args, env=env)
104
105 if self.verbose:
106 print "check_output(%s) returned '%s'" % (args, output)
107 return output
108
109 def call(self, args, dry_run=False, env=None):
110 """Dry run aware wrapper of subprocess.call()"""
111 if dry_run:
112 print "Would have run '%s'" % ' '.join(args)
113 return 0
114
115 output = subprocess.call(args, env=env)
116
117 if self.verbose:
118 print 'call(%s) returned %s' % (args, output)
119 return output
120
Ryan Harrisona7b65b82018-05-30 19:56:11 +0000121 def call_silent(self, args, dry_run=False, env=None):
122 """Dry run aware wrapper of subprocess.call() that eats output from call"""
123 if dry_run:
124 print "Would have run '%s'" % ' '.join(args)
125 return 0
126
127 with open(os.devnull, 'w') as f:
128 output = subprocess.call(args, env=env, stdout=f)
129
130 if self.verbose:
131 print 'call_silent(%s) returned %s' % (args, output)
132 return output
Ryan Harrison574366b2017-07-18 10:18:55 -0400133
134 def calculate_coverage_tests(self, args):
135 """Determine which tests should be run."""
136 testing_tools_directory = os.path.join(self.source_directory, 'testing',
137 'tools')
Ryan Harrisona7b65b82018-05-30 19:56:11 +0000138 tests = args['tests'] if args['tests'] else COVERAGE_TESTS.keys()
Ryan Harrison574366b2017-07-18 10:18:55 -0400139 coverage_tests = {}
Ryan Harrisona7b65b82018-05-30 19:56:11 +0000140 build_targets = set()
141 for name in tests:
Ryan Harrison05590922017-07-28 14:46:08 -0400142 test_spec = COVERAGE_TESTS[name]
143 if test_spec.use_test_runner:
144 binary_path = os.path.join(testing_tools_directory, test_spec.binary)
Lei Zhang5a88d162018-11-29 18:37:30 +0000145 build_targets.add('pdfium_diff')
Ryan Harrisona7b65b82018-05-30 19:56:11 +0000146 build_targets.add('pdfium_test')
Ryan Harrison574366b2017-07-18 10:18:55 -0400147 else:
Ryan Harrison05590922017-07-28 14:46:08 -0400148 binary_path = os.path.join(self.build_directory, test_spec.binary)
Ryan Harrisona7b65b82018-05-30 19:56:11 +0000149 build_targets.add(name)
Ryan Harrison05590922017-07-28 14:46:08 -0400150 coverage_tests[name] = TestSpec(binary_path, test_spec.use_test_runner)
Ryan Harrison574366b2017-07-18 10:18:55 -0400151
Ryan Harrisona7b65b82018-05-30 19:56:11 +0000152 build_targets = list(build_targets)
Ryan Harrison574366b2017-07-18 10:18:55 -0400153
Ryan Harrisona7b65b82018-05-30 19:56:11 +0000154 return coverage_tests, build_targets
Ryan Harrison574366b2017-07-18 10:18:55 -0400155
156 def build_binaries(self):
157 """Build all the binaries that are going to be needed for coverage
158 generation."""
159 call_args = ['ninja']
160 if self.use_goma:
Ryan Harrisona7b65b82018-05-30 19:56:11 +0000161 call_args += ['-j', '250']
162 call_args += ['-C', self.build_directory]
163 call_args += self.build_targets
Ryan Harrison574366b2017-07-18 10:18:55 -0400164 return self.call(call_args, dry_run=self.dry_run) == 0
165
166 def generate_coverage(self, name, spec):
167 """Generate the coverage data for a test
168
169 Args:
170 name: Name associated with the test to be run. This is used as a label
171 in the coverage data, so should be unique across all of the tests
172 being run.
173 spec: Tuple containing the path to the binary to run, and if this test
174 uses test_runner.py.
175 """
176 if self.verbose:
177 print "Generating coverage for test '%s', using data '%s'" % (name, spec)
178 if not os.path.exists(spec.binary):
179 print('Unable to generate coverage for %s, since it appears to not exist'
180 ' @ %s') % (name, spec.binary)
181 return False
182
Ryan Harrison574366b2017-07-18 10:18:55 -0400183 binary_args = [spec.binary]
Ryan Harrisona7b65b82018-05-30 19:56:11 +0000184 profile_pattern_string = '%8m'
185 expected_profraw_file = '%s.%s.profraw' % (name, profile_pattern_string)
186 expected_profraw_path = os.path.join(self.output_directory,
187 expected_profraw_file)
188
189 env = {
190 'LLVM_PROFILE_FILE': expected_profraw_path,
191 'PATH': os.getenv('PATH') + os.pathsep + self.llvm_directory
192 }
193
Ryan Harrison574366b2017-07-18 10:18:55 -0400194 if spec.use_test_runner:
195 # Test runner performs multi-threading in the wrapper script, not the test
Ryan Harrisona7b65b82018-05-30 19:56:11 +0000196 # binary, so need to limit the number of instances of the binary being run
197 # to the max value in LLVM_PROFILE_FILE, which is 8.
198 binary_args.extend(['-j', '8', '--build-dir', self.build_directory])
199 if self.call(binary_args, dry_run=self.dry_run, env=env) and self.verbose:
Ryan Harrison574366b2017-07-18 10:18:55 -0400200 print('Running %s appears to have failed, which might affect '
201 'results') % spec.binary
202
Ryan Harrison574366b2017-07-18 10:18:55 -0400203 return True
204
Ryan Harrisona7b65b82018-05-30 19:56:11 +0000205 def merge_raw_coverage_results(self):
206 """Merge raw coverage data sets into one one file for report generation."""
207 llvm_profdata_bin = os.path.join(self.llvm_directory, 'llvm-profdata')
Ryan Harrison574366b2017-07-18 10:18:55 -0400208
Ryan Harrisona7b65b82018-05-30 19:56:11 +0000209 raw_data = []
210 raw_data_pattern = '*.profraw'
211 for file_name in os.listdir(self.output_directory):
212 if fnmatch.fnmatch(file_name, raw_data_pattern):
213 raw_data.append(os.path.join(self.output_directory, file_name))
Ryan Harrison574366b2017-07-18 10:18:55 -0400214
Ryan Harrisona7b65b82018-05-30 19:56:11 +0000215 return self.call(
216 [llvm_profdata_bin, 'merge', '-o', self.prof_data, '-sparse=true'] +
217 raw_data) == 0
Ryan Harrison574366b2017-07-18 10:18:55 -0400218
Ryan Harrisona7b65b82018-05-30 19:56:11 +0000219 def generate_html_report(self):
220 """Generate HTML report by calling upstream coverage.py"""
221 coverage_bin = os.path.join(self.source_directory, 'tools', 'code_coverage',
222 'coverage.py')
223 report_directory = os.path.join(self.output_directory, 'HTML')
224
225 coverage_args = ['-p', self.prof_data]
226 coverage_args += ['-b', self.build_directory]
227 coverage_args += ['-o', report_directory]
228 coverage_args += self.build_targets
229
230 # Whitelist the directories of interest
231 coverage_args += ['-f', 'core']
232 coverage_args += ['-f', 'fpdfsdk']
233 coverage_args += ['-f', 'fxbarcode']
234 coverage_args += ['-f', 'fxjs']
235 coverage_args += ['-f', 'public']
236 coverage_args += ['-f', 'samples']
237 coverage_args += ['-f', 'xfa']
238
239 # Blacklist test files
Lei Zhangd4b59ae2018-10-17 17:05:28 +0000240 coverage_args += ['-i', '.*test.*']
Ryan Harrisona7b65b82018-05-30 19:56:11 +0000241
242 return self.call([coverage_bin] + coverage_args) == 0
Ryan Harrison574366b2017-07-18 10:18:55 -0400243
244 def run(self):
245 """Setup environment, execute the tests and generate coverage report"""
Ryan Harrisona7b65b82018-05-30 19:56:11 +0000246 if not self.fetch_profiling_tools():
247 print 'Unable to fetch profiling tools'
248 return False
249
Ryan Harrison574366b2017-07-18 10:18:55 -0400250 if not self.build_binaries():
251 print 'Failed to successfully build binaries'
252 return False
253
254 for name in self.coverage_tests.keys():
255 if not self.generate_coverage(name, self.coverage_tests[name]):
256 print 'Failed to successfully generate coverage data'
257 return False
258
Ryan Harrisona7b65b82018-05-30 19:56:11 +0000259 if not self.merge_raw_coverage_results():
260 print 'Failed to successfully merge raw coverage results'
Ryan Harrison574366b2017-07-18 10:18:55 -0400261 return False
262
Ryan Harrisona7b65b82018-05-30 19:56:11 +0000263 if not self.generate_html_report():
264 print 'Failed to successfully generate HTML report'
Ryan Harrison574366b2017-07-18 10:18:55 -0400265 return False
266
267 return True
268
Ryan Harrisona7b65b82018-05-30 19:56:11 +0000269 def fetch_profiling_tools(self):
270 """Call coverage.py with no args to ensure profiling tools are present."""
271 return self.call_silent(
272 os.path.join(self.source_directory, 'tools', 'code_coverage',
273 'coverage.py')) == 0
274
Ryan Harrison574366b2017-07-18 10:18:55 -0400275
276def main():
277 parser = argparse.ArgumentParser()
278 parser.formatter_class = argparse.RawDescriptionHelpFormatter
Ryan Harrisona7b65b82018-05-30 19:56:11 +0000279 parser.description = 'Generates a coverage report for given tests.'
280
Ryan Harrison574366b2017-07-18 10:18:55 -0400281 parser.add_argument(
282 '-s',
283 '--source_directory',
Ryan Harrison574366b2017-07-18 10:18:55 -0400284 help='Location of PDFium source directory, defaults to CWD',
285 default=os.getcwd())
286 build_default = os.path.join('out', 'Coverage')
287 parser.add_argument(
288 '-b',
289 '--build_directory',
Ryan Harrison574366b2017-07-18 10:18:55 -0400290 help=
291 'Location of PDFium build directory with coverage enabled, defaults to '
292 '%s under CWD' % build_default,
293 default=os.path.join(os.getcwd(), build_default))
294 output_default = 'coverage_report'
295 parser.add_argument(
296 '-o',
297 '--output_directory',
Ryan Harrison574366b2017-07-18 10:18:55 -0400298 help='Location to write out coverage report to, defaults to %s under CWD '
299 % output_default,
300 default=os.path.join(os.getcwd(), output_default))
301 parser.add_argument(
302 '-n',
303 '--dry-run',
304 help='Output commands instead of executing them',
305 action='store_true')
306 parser.add_argument(
307 '-v',
308 '--verbose',
309 help='Output additional diagnostic information',
310 action='store_true')
311 parser.add_argument(
Ryan Harrison574366b2017-07-18 10:18:55 -0400312 'tests',
313 help='Tests to be run, defaults to all. Valid entries are %s' %
314 COVERAGE_TESTS.keys(),
315 nargs='*')
316
317 args = vars(parser.parse_args())
318 if args['verbose']:
319 pprint.pprint(args)
320
321 executor = CoverageExecutor(parser, args)
322 if executor.run():
323 return 0
324 return 1
325
326
327if __name__ == '__main__':
328 sys.exit(main())