| Ryan Harrison | 574366b | 2017-07-18 10:18:55 -0400 | [diff] [blame] | 1 | #!/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 Harrison | a7b65b8 | 2018-05-30 19:56:11 +0000 | [diff] [blame] | 5 | """Generates a coverage report for given tests. |
| Ryan Harrison | 574366b | 2017-07-18 10:18:55 -0400 | [diff] [blame] | 6 | |
| Ryan Harrison | a7b65b8 | 2018-05-30 19:56:11 +0000 | [diff] [blame] | 7 | Requires that 'use_clang_coverage = true' is set in args.gn. |
| 8 | Prefers that 'is_component_build = false' is set in args.gn. |
| Ryan Harrison | 574366b | 2017-07-18 10:18:55 -0400 | [diff] [blame] | 9 | """ |
| 10 | |
| 11 | import argparse |
| 12 | from collections import namedtuple |
| Ryan Harrison | a7b65b8 | 2018-05-30 19:56:11 +0000 | [diff] [blame] | 13 | import fnmatch |
| Ryan Harrison | 574366b | 2017-07-18 10:18:55 -0400 | [diff] [blame] | 14 | import os |
| Henrique Nakashima | d9b0dac | 2017-08-09 16:43:25 -0400 | [diff] [blame] | 15 | import pprint |
| Ryan Harrison | 574366b | 2017-07-18 10:18:55 -0400 | [diff] [blame] | 16 | import subprocess |
| 17 | import sys |
| 18 | |
| Henrique Nakashima | d9b0dac | 2017-08-09 16:43:25 -0400 | [diff] [blame] | 19 | # Add src dir to path to avoid having to set PYTHONPATH. |
| 20 | sys.path.append( |
| 21 | os.path.abspath( |
| Ryan Harrison | a7b65b8 | 2018-05-30 19:56:11 +0000 | [diff] [blame] | 22 | os.path.join( |
| 23 | os.path.dirname(__file__), os.path.pardir, os.path.pardir, |
| 24 | os.path.pardir))) |
| Henrique Nakashima | d9b0dac | 2017-08-09 16:43:25 -0400 | [diff] [blame] | 25 | |
| 26 | from testing.tools.common import GetBooleanGnArg |
| 27 | |
| Ryan Harrison | 574366b | 2017-07-18 10:18:55 -0400 | [diff] [blame] | 28 | # '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 Harrison | 0559092 | 2017-07-28 14:46:08 -0400 | [diff] [blame] | 31 | TestSpec = namedtuple('TestSpec', 'binary, use_test_runner') |
| Ryan Harrison | 574366b | 2017-07-18 10:18:55 -0400 | [diff] [blame] | 32 | |
| 33 | # All of the coverage tests that the script knows how to run. |
| 34 | COVERAGE_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 Harrison | 574366b | 2017-07-18 10:18:55 -0400 | [diff] [blame] | 42 | |
| 43 | class 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 Harrison | 574366b | 2017-07-18 10:18:55 -0400 | [diff] [blame] | 55 | 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 Harrison | a7b65b8 | 2018-05-30 19:56:11 +0000 | [diff] [blame] | 59 | 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 Harrison | 574366b | 2017-07-18 10:18:55 -0400 | [diff] [blame] | 65 | 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 Harrison | a7b65b8 | 2018-05-30 19:56:11 +0000 | [diff] [blame] | 69 | (self.coverage_tests, |
| 70 | self.build_targets) = self.calculate_coverage_tests(args) |
| Ryan Harrison | 574366b | 2017-07-18 10:18:55 -0400 | [diff] [blame] | 71 | 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 Harrison | a7b65b8 | 2018-05-30 19:56:11 +0000 | [diff] [blame] | 76 | if not GetBooleanGnArg('use_clang_coverage', self.build_directory, |
| 77 | self.verbose): |
| Ryan Harrison | 574366b | 2017-07-18 10:18:55 -0400 | [diff] [blame] | 78 | parser.error( |
| Ryan Harrison | a7b65b8 | 2018-05-30 19:56:11 +0000 | [diff] [blame] | 79 | 'use_clang_coverage does not appear to be set to true for build, but is ' |
| Ryan Harrison | 574366b | 2017-07-18 10:18:55 -0400 | [diff] [blame] | 80 | 'needed') |
| 81 | |
| Henrique Nakashima | d9b0dac | 2017-08-09 16:43:25 -0400 | [diff] [blame] | 82 | self.use_goma = GetBooleanGnArg('use_goma', self.build_directory, |
| 83 | self.verbose) |
| Ryan Harrison | 574366b | 2017-07-18 10:18:55 -0400 | [diff] [blame] | 84 | |
| 85 | self.output_directory = args['output_directory'] |
| 86 | if not os.path.exists(self.output_directory): |
| 87 | if not self.dry_run: |
| Ryan Harrison | 52f2429 | 2017-07-31 11:36:45 -0400 | [diff] [blame] | 88 | os.makedirs(self.output_directory) |
| Ryan Harrison | 574366b | 2017-07-18 10:18:55 -0400 | [diff] [blame] | 89 | elif not os.path.isdir(self.output_directory): |
| 90 | parser.error('%s exists, but is not a directory' % self.output_directory) |
| Ryan Harrison | a7b65b8 | 2018-05-30 19:56:11 +0000 | [diff] [blame] | 91 | 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 Harrison | 574366b | 2017-07-18 10:18:55 -0400 | [diff] [blame] | 96 | |
| Ryan Harrison | 574366b | 2017-07-18 10:18:55 -0400 | [diff] [blame] | 97 | 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 Harrison | a7b65b8 | 2018-05-30 19:56:11 +0000 | [diff] [blame] | 121 | 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 Harrison | 574366b | 2017-07-18 10:18:55 -0400 | [diff] [blame] | 133 | |
| 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 Harrison | a7b65b8 | 2018-05-30 19:56:11 +0000 | [diff] [blame] | 138 | tests = args['tests'] if args['tests'] else COVERAGE_TESTS.keys() |
| Ryan Harrison | 574366b | 2017-07-18 10:18:55 -0400 | [diff] [blame] | 139 | coverage_tests = {} |
| Ryan Harrison | a7b65b8 | 2018-05-30 19:56:11 +0000 | [diff] [blame] | 140 | build_targets = set() |
| 141 | for name in tests: |
| Ryan Harrison | 0559092 | 2017-07-28 14:46:08 -0400 | [diff] [blame] | 142 | 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 Zhang | 5a88d16 | 2018-11-29 18:37:30 +0000 | [diff] [blame] | 145 | build_targets.add('pdfium_diff') |
| Ryan Harrison | a7b65b8 | 2018-05-30 19:56:11 +0000 | [diff] [blame] | 146 | build_targets.add('pdfium_test') |
| Ryan Harrison | 574366b | 2017-07-18 10:18:55 -0400 | [diff] [blame] | 147 | else: |
| Ryan Harrison | 0559092 | 2017-07-28 14:46:08 -0400 | [diff] [blame] | 148 | binary_path = os.path.join(self.build_directory, test_spec.binary) |
| Ryan Harrison | a7b65b8 | 2018-05-30 19:56:11 +0000 | [diff] [blame] | 149 | build_targets.add(name) |
| Ryan Harrison | 0559092 | 2017-07-28 14:46:08 -0400 | [diff] [blame] | 150 | coverage_tests[name] = TestSpec(binary_path, test_spec.use_test_runner) |
| Ryan Harrison | 574366b | 2017-07-18 10:18:55 -0400 | [diff] [blame] | 151 | |
| Ryan Harrison | a7b65b8 | 2018-05-30 19:56:11 +0000 | [diff] [blame] | 152 | build_targets = list(build_targets) |
| Ryan Harrison | 574366b | 2017-07-18 10:18:55 -0400 | [diff] [blame] | 153 | |
| Ryan Harrison | a7b65b8 | 2018-05-30 19:56:11 +0000 | [diff] [blame] | 154 | return coverage_tests, build_targets |
| Ryan Harrison | 574366b | 2017-07-18 10:18:55 -0400 | [diff] [blame] | 155 | |
| 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 Harrison | a7b65b8 | 2018-05-30 19:56:11 +0000 | [diff] [blame] | 161 | call_args += ['-j', '250'] |
| 162 | call_args += ['-C', self.build_directory] |
| 163 | call_args += self.build_targets |
| Ryan Harrison | 574366b | 2017-07-18 10:18:55 -0400 | [diff] [blame] | 164 | 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 Harrison | 574366b | 2017-07-18 10:18:55 -0400 | [diff] [blame] | 183 | binary_args = [spec.binary] |
| Ryan Harrison | a7b65b8 | 2018-05-30 19:56:11 +0000 | [diff] [blame] | 184 | 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 Harrison | 574366b | 2017-07-18 10:18:55 -0400 | [diff] [blame] | 194 | if spec.use_test_runner: |
| 195 | # Test runner performs multi-threading in the wrapper script, not the test |
| Ryan Harrison | a7b65b8 | 2018-05-30 19:56:11 +0000 | [diff] [blame] | 196 | # 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 Harrison | 574366b | 2017-07-18 10:18:55 -0400 | [diff] [blame] | 200 | print('Running %s appears to have failed, which might affect ' |
| 201 | 'results') % spec.binary |
| 202 | |
| Ryan Harrison | 574366b | 2017-07-18 10:18:55 -0400 | [diff] [blame] | 203 | return True |
| 204 | |
| Ryan Harrison | a7b65b8 | 2018-05-30 19:56:11 +0000 | [diff] [blame] | 205 | 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 Harrison | 574366b | 2017-07-18 10:18:55 -0400 | [diff] [blame] | 208 | |
| Ryan Harrison | a7b65b8 | 2018-05-30 19:56:11 +0000 | [diff] [blame] | 209 | 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 Harrison | 574366b | 2017-07-18 10:18:55 -0400 | [diff] [blame] | 214 | |
| Ryan Harrison | a7b65b8 | 2018-05-30 19:56:11 +0000 | [diff] [blame] | 215 | return self.call( |
| 216 | [llvm_profdata_bin, 'merge', '-o', self.prof_data, '-sparse=true'] + |
| 217 | raw_data) == 0 |
| Ryan Harrison | 574366b | 2017-07-18 10:18:55 -0400 | [diff] [blame] | 218 | |
| Ryan Harrison | a7b65b8 | 2018-05-30 19:56:11 +0000 | [diff] [blame] | 219 | 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 Zhang | d4b59ae | 2018-10-17 17:05:28 +0000 | [diff] [blame] | 240 | coverage_args += ['-i', '.*test.*'] |
| Ryan Harrison | a7b65b8 | 2018-05-30 19:56:11 +0000 | [diff] [blame] | 241 | |
| 242 | return self.call([coverage_bin] + coverage_args) == 0 |
| Ryan Harrison | 574366b | 2017-07-18 10:18:55 -0400 | [diff] [blame] | 243 | |
| 244 | def run(self): |
| 245 | """Setup environment, execute the tests and generate coverage report""" |
| Ryan Harrison | a7b65b8 | 2018-05-30 19:56:11 +0000 | [diff] [blame] | 246 | if not self.fetch_profiling_tools(): |
| 247 | print 'Unable to fetch profiling tools' |
| 248 | return False |
| 249 | |
| Ryan Harrison | 574366b | 2017-07-18 10:18:55 -0400 | [diff] [blame] | 250 | 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 Harrison | a7b65b8 | 2018-05-30 19:56:11 +0000 | [diff] [blame] | 259 | if not self.merge_raw_coverage_results(): |
| 260 | print 'Failed to successfully merge raw coverage results' |
| Ryan Harrison | 574366b | 2017-07-18 10:18:55 -0400 | [diff] [blame] | 261 | return False |
| 262 | |
| Ryan Harrison | a7b65b8 | 2018-05-30 19:56:11 +0000 | [diff] [blame] | 263 | if not self.generate_html_report(): |
| 264 | print 'Failed to successfully generate HTML report' |
| Ryan Harrison | 574366b | 2017-07-18 10:18:55 -0400 | [diff] [blame] | 265 | return False |
| 266 | |
| 267 | return True |
| 268 | |
| Ryan Harrison | a7b65b8 | 2018-05-30 19:56:11 +0000 | [diff] [blame] | 269 | 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 Harrison | 574366b | 2017-07-18 10:18:55 -0400 | [diff] [blame] | 275 | |
| 276 | def main(): |
| 277 | parser = argparse.ArgumentParser() |
| 278 | parser.formatter_class = argparse.RawDescriptionHelpFormatter |
| Ryan Harrison | a7b65b8 | 2018-05-30 19:56:11 +0000 | [diff] [blame] | 279 | parser.description = 'Generates a coverage report for given tests.' |
| 280 | |
| Ryan Harrison | 574366b | 2017-07-18 10:18:55 -0400 | [diff] [blame] | 281 | parser.add_argument( |
| 282 | '-s', |
| 283 | '--source_directory', |
| Ryan Harrison | 574366b | 2017-07-18 10:18:55 -0400 | [diff] [blame] | 284 | 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 Harrison | 574366b | 2017-07-18 10:18:55 -0400 | [diff] [blame] | 290 | 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 Harrison | 574366b | 2017-07-18 10:18:55 -0400 | [diff] [blame] | 298 | 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 Harrison | 574366b | 2017-07-18 10:18:55 -0400 | [diff] [blame] | 312 | '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 | |
| 327 | if __name__ == '__main__': |
| 328 | sys.exit(main()) |