Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright (c) 2009 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2009 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 | 5 |
| 6 """Generate and process code coverage on POSIX systems. | 6 """Generate and process code coverage. |
| 7 | 7 |
| 8 Written for and tested on Mac and Linux. To use this script to | 8 TODO(jrg): rename this from coverage_posix.py to coverage_all.py! |
|
Randall Spangler
2009/07/07 16:14:14
Yes!
| |
| 9 generate coverage numbers, please run from within a gyp-generated | 9 |
| 10 Written for and tested on Mac, Linux, and Windows. To use this script | |
| 11 to generate coverage numbers, please run from within a gyp-generated | |
| 10 project. | 12 project. |
| 11 | 13 |
| 12 All platforms, to set up coverage: | 14 All platforms, to set up coverage: |
| 13 cd ...../chromium ; src/tools/gyp/gyp_dogfood -Dcoverage=1 src/build/all.gyp | 15 cd ...../chromium ; src/tools/gyp/gyp_dogfood -Dcoverage=1 src/build/all.gyp |
| 14 | 16 |
| 15 Run coverage on... | 17 Run coverage on... |
| 16 Mac: | 18 Mac: |
| 17 ( cd src/chrome ; xcodebuild -configuration Debug -target coverage ) | 19 ( cd src/chrome ; xcodebuild -configuration Debug -target coverage ) |
| 18 Linux: | 20 Linux: |
| 19 ( cd src/chrome ; hammer coverage ) | 21 ( cd src/chrome ; hammer coverage ) |
| (...skipping 26 matching lines...) Expand all Loading... | |
| 46 import os | 48 import os |
| 47 import shutil | 49 import shutil |
| 48 import subprocess | 50 import subprocess |
| 49 import sys | 51 import sys |
| 50 | 52 |
| 51 class Coverage(object): | 53 class Coverage(object): |
| 52 """Doitall class for code coverage.""" | 54 """Doitall class for code coverage.""" |
| 53 | 55 |
| 54 def __init__(self, directory, options, args): | 56 def __init__(self, directory, options, args): |
| 55 super(Coverage, self).__init__() | 57 super(Coverage, self).__init__() |
| 58 logging.basicConfig(level=logging.DEBUG) | |
| 56 self.directory = directory | 59 self.directory = directory |
| 57 self.options = options | 60 self.options = options |
| 58 self.args = args | 61 self.args = args |
| 59 self.directory_parent = os.path.dirname(self.directory) | 62 self.directory_parent = os.path.dirname(self.directory) |
| 60 self.output_directory = os.path.join(self.directory, 'coverage') | 63 self.output_directory = os.path.join(self.directory, 'coverage') |
| 61 if not os.path.exists(self.output_directory): | 64 if not os.path.exists(self.output_directory): |
| 62 os.mkdir(self.output_directory) | 65 os.mkdir(self.output_directory) |
| 63 self.lcov_directory = os.path.join(sys.path[0], | 66 # The "final" lcov-format file |
| 64 '../../third_party/lcov/bin') | |
| 65 self.lcov = os.path.join(self.lcov_directory, 'lcov') | |
| 66 self.mcov = os.path.join(self.lcov_directory, 'mcov') | |
| 67 self.genhtml = os.path.join(self.lcov_directory, 'genhtml') | |
| 68 self.coverage_info_file = os.path.join(self.directory, 'coverage.info') | 67 self.coverage_info_file = os.path.join(self.directory, 'coverage.info') |
| 68 # If needed, an intermediate VSTS-format file | |
| 69 self.vsts_output = os.path.join(self.directory, 'coverage.vsts') | |
| 70 # Needed for Windows. | |
| 71 self.src_root = options.src_root | |
| 72 self.FindPrograms() | |
| 69 self.ConfirmPlatformAndPaths() | 73 self.ConfirmPlatformAndPaths() |
| 70 self.tests = [] | 74 self.tests = [] |
| 71 | 75 |
| 76 def FindInPath(self, program): | |
| 77 """Find program in our path. Return abs path to it, or None.""" | |
| 78 if not 'PATH' in os.environ: | |
| 79 logging.fatal('No PATH environment variable?') | |
| 80 sys.exit(1) | |
| 81 paths = os.environ['PATH'].split(os.pathsep) | |
| 82 for path in paths: | |
| 83 fullpath = os.path.join(path, program) | |
| 84 if os.path.exists(fullpath): | |
| 85 return fullpath | |
| 86 return None | |
| 87 | |
| 88 def FindPrograms(self): | |
| 89 """Find programs we may want to run.""" | |
| 90 if self.IsPosix(): | |
| 91 self.lcov_directory = os.path.join(sys.path[0], | |
| 92 '../../third_party/lcov/bin') | |
| 93 self.lcov = os.path.join(self.lcov_directory, 'lcov') | |
| 94 self.mcov = os.path.join(self.lcov_directory, 'mcov') | |
| 95 self.genhtml = os.path.join(self.lcov_directory, 'genhtml') | |
| 96 self.programs = [self.lcov, self.mcov, self.genhtml] | |
| 97 else: | |
| 98 commands = ['vsperfcmd.exe', 'vsinstr.exe', 'coverage_analyzer.exe'] | |
| 99 self.perf = self.FindInPath('vsperfcmd.exe') | |
| 100 self.instrument = self.FindInPath('vsinstr.exe') | |
| 101 self.analyzer = self.FindInPath('coverage_analyzer.exe') | |
| 102 if not self.perf or not self.instrument or not self.analyzer: | |
| 103 logging.fatal('Could not find Win performance commands.') | |
| 104 logging.fatal('Commands needed in PATH: ' + str(commands)) | |
| 105 sys.exit(1) | |
| 106 self.programs = [self.perf, self.instrument, self.analyzer] | |
| 107 | |
| 72 def FindTests(self): | 108 def FindTests(self): |
| 73 """Find unit tests to run; set self.tests to this list. | 109 """Find unit tests to run; set self.tests to this list. |
| 74 | 110 |
| 75 Assume all non-option items in the arg list are tests to be run. | 111 Assume all non-option items in the arg list are tests to be run. |
| 76 """ | 112 """ |
| 77 # Small tests: can be run in the "chromium" directory. | 113 # Small tests: can be run in the "chromium" directory. |
| 78 # If asked, run all we can find. | 114 # If asked, run all we can find. |
| 79 if self.options.all_unittests: | 115 if self.options.all_unittests: |
| 80 self.tests += glob.glob(os.path.join(self.directory, '*_unittests')) | 116 self.tests += glob.glob(os.path.join(self.directory, '*_unittests')) |
| 81 | 117 |
| 82 # If told explicit tests, run those (after stripping the name as | 118 # If told explicit tests, run those (after stripping the name as |
| 83 # appropriate) | 119 # appropriate) |
| 84 for testname in self.args: | 120 for testname in self.args: |
| 85 if ':' in testname: | 121 if ':' in testname: |
| 86 self.tests += [os.path.join(self.directory, testname.split(':')[1])] | 122 self.tests += [os.path.join(self.directory, testname.split(':')[1])] |
| 87 else: | 123 else: |
| 88 self.tests += [os.path.join(self.directory, testname)] | 124 self.tests += [os.path.join(self.directory, testname)] |
| 89 | |
| 90 # Needs to be run in the "chrome" directory? | |
| 91 # ut = os.path.join(self.directory, 'unit_tests') | |
| 92 # if os.path.exists(ut): | |
| 93 # self.tests.append(ut) | |
| 94 # Medium tests? | 125 # Medium tests? |
| 95 # Not sure all of these work yet (e.g. page_cycler_tests) | 126 # Not sure all of these work yet (e.g. page_cycler_tests) |
| 96 # self.tests += glob.glob(os.path.join(self.directory, '*_tests')) | 127 # self.tests += glob.glob(os.path.join(self.directory, '*_tests')) |
| 97 | 128 |
| 129 # If needed, append .exe to tests since vsinstr.exe likes it that | |
| 130 # way. | |
| 131 if self.IsWindows(): | |
| 132 for ind in range(len(self.tests)): | |
| 133 test = self.tests[ind] | |
| 134 test_exe = test + '.exe' | |
| 135 if not test.endswith('.exe') and os.path.exists(test_exe): | |
| 136 self.tests[ind] = test_exe | |
| 137 | |
| 138 # Temporarily make Windows quick for bringup by filtering | |
| 139 # out all except base_unittests. Easier than a chrome.cyp change. | |
| 140 # TODO(jrg): remove this | |
| 141 if self.IsWindows(): | |
| 142 t2 = [] | |
| 143 for test in self.tests: | |
| 144 if 'base_unittests' in test: | |
| 145 t2.append(test) | |
| 146 self.tests = t2 | |
| 147 | |
| 148 | |
| 149 | |
| 98 def ConfirmPlatformAndPaths(self): | 150 def ConfirmPlatformAndPaths(self): |
| 99 """Confirm OS and paths (e.g. lcov).""" | 151 """Confirm OS and paths (e.g. lcov).""" |
| 100 if not self.IsPosix(): | 152 for program in self.programs: |
| 101 logging.fatal('Not posix.') | |
| 102 sys.exit(1) | |
| 103 programs = [self.lcov, self.genhtml] | |
| 104 if self.IsMac(): | |
| 105 programs.append(self.mcov) | |
| 106 for program in programs: | |
| 107 if not os.path.exists(program): | 153 if not os.path.exists(program): |
| 108 logging.fatal('lcov program missing: ' + program) | 154 logging.fatal('Program missing: ' + program) |
| 109 sys.exit(1) | 155 sys.exit(1) |
| 110 | 156 |
| 157 def Run(self, cmdlist, ignore_error=False, ignore_retcode=None, | |
| 158 explanation=None): | |
| 159 """Run the command list; exit fatally on error.""" | |
| 160 logging.info('Running ' + str(cmdlist)) | |
| 161 retcode = subprocess.call(cmdlist) | |
| 162 if retcode: | |
| 163 if ignore_error or retcode == ignore_retcode: | |
| 164 logging.warning('COVERAGE: %s unhappy but errors ignored %s' % | |
| 165 (str(cmdlist), explanation or '')) | |
| 166 else: | |
| 167 logging.fatal('COVERAGE: %s failed; return code: %d' % | |
| 168 (str(cmdlist), retcode)) | |
| 169 sys.exit(retcode) | |
| 170 | |
| 171 | |
| 111 def IsPosix(self): | 172 def IsPosix(self): |
| 112 """Return True if we are POSIX.""" | 173 """Return True if we are POSIX.""" |
| 113 return self.IsMac() or self.IsLinux() | 174 return self.IsMac() or self.IsLinux() |
| 114 | 175 |
| 115 def IsMac(self): | 176 def IsMac(self): |
| 116 return sys.platform == 'darwin' | 177 return sys.platform == 'darwin' |
| 117 | 178 |
| 118 def IsLinux(self): | 179 def IsLinux(self): |
| 119 return sys.platform == 'linux2' | 180 return sys.platform == 'linux2' |
| 120 | 181 |
| 182 def IsWindows(self): | |
| 183 """Return True if we are Windows.""" | |
| 184 return sys.platform in ('win32', 'cygwin') | |
| 185 | |
| 121 def ClearData(self): | 186 def ClearData(self): |
| 122 """Clear old gcda files""" | 187 """Clear old gcda files""" |
| 188 if not self.IsPosix(): | |
| 189 return | |
| 123 subprocess.call([self.lcov, | 190 subprocess.call([self.lcov, |
| 124 '--directory', self.directory_parent, | 191 '--directory', self.directory_parent, |
| 125 '--zerocounters']) | 192 '--zerocounters']) |
| 126 shutil.rmtree(os.path.join(self.directory, 'coverage')) | 193 shutil.rmtree(os.path.join(self.directory, 'coverage')) |
| 127 | 194 |
| 195 def BeforeRunTests(self): | |
| 196 """Do things before running tests.""" | |
| 197 if not self.IsWindows(): | |
| 198 return | |
| 199 # Stop old counters if needed | |
| 200 cmdlist = [self.perf, '-shutdown'] | |
| 201 self.Run(cmdlist, ignore_error=True) | |
| 202 # Instrument binaries | |
| 203 for fulltest in self.tests: | |
| 204 if os.path.exists(fulltest): | |
| 205 cmdlist = [self.instrument, '/COVERAGE', fulltest] | |
| 206 self.Run(cmdlist, ignore_retcode=4, | |
| 207 explanation='OK with a multiple-instrument') | |
| 208 # Start new counters | |
| 209 cmdlist = [self.perf, '-start:coverage', '-output:' + self.vsts_output] | |
| 210 self.Run(cmdlist) | |
| 211 | |
| 128 def RunTests(self): | 212 def RunTests(self): |
| 129 """Run all unit tests.""" | 213 """Run all unit tests.""" |
| 130 for fulltest in self.tests: | 214 for fulltest in self.tests: |
| 131 if not os.path.exists(fulltest): | 215 if not os.path.exists(fulltest): |
| 132 logging.fatal(fulltest + ' does not exist') | 216 logging.fatal(fulltest + ' does not exist') |
| 133 if self.options.strict: | 217 if self.options.strict: |
| 134 sys.exit(2) | 218 sys.exit(2) |
| 135 # TODO(jrg): add timeout? | 219 # TODO(jrg): add timeout? |
| 136 print >>sys.stderr, 'Running test: ' + fulltest | 220 print >>sys.stderr, 'Running test: ' + fulltest |
| 137 cmdlist = [fulltest, '--gtest_print_time'] | 221 cmdlist = [fulltest, '--gtest_print_time'] |
| 138 | 222 |
| 139 # If asked, make this REAL fast for testing. | 223 # If asked, make this REAL fast for testing. |
| 140 if self.options.fast_test: | 224 if self.options.fast_test: |
| 141 cmdlist.append('--gtest_filter=RenderWidgetHost*') | 225 # cmdlist.append('--gtest_filter=RenderWidgetHost*') |
| 226 cmdlist.append('--gtest_filter=CommandLine*') | |
| 142 | 227 |
| 143 retcode = subprocess.call(cmdlist) | 228 retcode = subprocess.call(cmdlist) |
| 144 if retcode: | 229 if retcode: |
| 145 logging.fatal('COVERAGE: test %s failed; return code: %d' % | 230 logging.fatal('COVERAGE: test %s failed; return code: %d' % |
| 146 (fulltest, retcode)) | 231 (fulltest, retcode)) |
| 147 if self.options.strict: | 232 if self.options.strict: |
| 148 sys.exit(retcode) | 233 sys.exit(retcode) |
| 149 | 234 |
| 150 def GenerateLcov(self): | 235 def AfterRunTests(self): |
| 236 """Do things right after running tests.""" | |
| 237 if not self.IsWindows(): | |
| 238 return | |
| 239 # Stop counters | |
| 240 cmdlist = [self.perf, '-shutdown'] | |
| 241 self.Run(cmdlist) | |
| 242 full_output = self.vsts_output + '.coverage' | |
| 243 shutil.move(full_output, self.vsts_output) | |
| 244 | |
| 245 def GenerateLcovPosix(self): | |
| 151 """Convert profile data to lcov.""" | 246 """Convert profile data to lcov.""" |
| 152 command = [self.mcov, | 247 command = [self.mcov, |
| 153 '--directory', self.directory_parent, | 248 '--directory', self.directory_parent, |
| 154 '--output', self.coverage_info_file] | 249 '--output', self.coverage_info_file] |
| 155 print >>sys.stderr, 'Assembly command: ' + ' '.join(command) | 250 print >>sys.stderr, 'Assembly command: ' + ' '.join(command) |
| 156 retcode = subprocess.call(command) | 251 retcode = subprocess.call(command) |
| 157 if retcode: | 252 if retcode: |
| 158 logging.fatal('COVERAGE: %s failed; return code: %d' % | 253 logging.fatal('COVERAGE: %s failed; return code: %d' % |
| 159 (command[0], retcode)) | 254 (command[0], retcode)) |
| 160 if self.options.strict: | 255 if self.options.strict: |
| 161 sys.exit(retcode) | 256 sys.exit(retcode) |
| 162 | 257 |
| 258 def GenerateLcovWindows(self): | |
| 259 """Convert VSTS format to lcov.""" | |
| 260 lcov_file = self.vsts_output + '.lcov' | |
| 261 if os.path.exists(lcov_file): | |
| 262 os.remove(lcov_file) | |
| 263 # generates the file (self.vsts_output + ".lcov") | |
| 264 | |
| 265 cmdlist = [self.analyzer, | |
| 266 '-sym_path=' + self.directory, | |
| 267 '-src_root=' + self.src_root, | |
| 268 self.vsts_output] | |
| 269 self.Run(cmdlist) | |
| 270 if not os.path.exists(lcov_file): | |
| 271 logging.fatal('Output file %s not created' % lcov_file) | |
| 272 sys.exit(1) | |
| 273 # So we name it appropriately | |
| 274 if os.path.exists(self.coverage_info_file): | |
| 275 os.remove(self.coverage_info_file) | |
| 276 logging.info('Renaming LCOV file to %s to be consistent' % | |
| 277 self.coverage_info_file) | |
| 278 shutil.move(self.vsts_output + '.lcov', self.coverage_info_file) | |
| 279 | |
| 280 def GenerateLcov(self): | |
| 281 if self.IsPosix(): | |
| 282 self.GenerateLcovPosix() | |
| 283 else: | |
| 284 self.GenerateLcovWindows() | |
| 285 | |
| 163 def GenerateHtml(self): | 286 def GenerateHtml(self): |
| 164 """Convert lcov to html.""" | 287 """Convert lcov to html.""" |
| 165 # TODO(jrg): This isn't happy when run with unit_tests since V8 has a | 288 # TODO(jrg): This isn't happy when run with unit_tests since V8 has a |
| 166 # different "base" so V8 includes can't be found in ".". Fix. | 289 # different "base" so V8 includes can't be found in ".". Fix. |
| 167 command = [self.genhtml, | 290 command = [self.genhtml, |
| 168 self.coverage_info_file, | 291 self.coverage_info_file, |
| 169 '--output-directory', | 292 '--output-directory', |
| 170 self.output_directory] | 293 self.output_directory] |
| 171 print >>sys.stderr, 'html generation command: ' + ' '.join(command) | 294 print >>sys.stderr, 'html generation command: ' + ' '.join(command) |
| 172 retcode = subprocess.call(command) | 295 retcode = subprocess.call(command) |
| (...skipping 26 matching lines...) Expand all Loading... | |
| 199 parser.add_option('-f', | 322 parser.add_option('-f', |
| 200 '--fast_test', | 323 '--fast_test', |
| 201 dest='fast_test', | 324 dest='fast_test', |
| 202 default=False, | 325 default=False, |
| 203 help='Make the tests run REAL fast by doing little.') | 326 help='Make the tests run REAL fast by doing little.') |
| 204 parser.add_option('-s', | 327 parser.add_option('-s', |
| 205 '--strict', | 328 '--strict', |
| 206 dest='strict', | 329 dest='strict', |
| 207 default=False, | 330 default=False, |
| 208 help='Be strict and die on test failure.') | 331 help='Be strict and die on test failure.') |
| 332 parser.add_option('-S', | |
| 333 '--src_root', | |
| 334 dest='src_root', | |
| 335 default='.', | |
| 336 help='Source root (only used on Windows)') | |
| 209 (options, args) = parser.parse_args() | 337 (options, args) = parser.parse_args() |
| 210 if not options.directory: | 338 if not options.directory: |
| 211 parser.error('Directory not specified') | 339 parser.error('Directory not specified') |
| 212 coverage = Coverage(options.directory, options, args) | 340 coverage = Coverage(options.directory, options, args) |
| 213 coverage.ClearData() | 341 coverage.ClearData() |
| 214 coverage.FindTests() | 342 coverage.FindTests() |
| 343 coverage.BeforeRunTests() | |
| 215 coverage.RunTests() | 344 coverage.RunTests() |
| 345 coverage.AfterRunTests() | |
| 216 coverage.GenerateLcov() | 346 coverage.GenerateLcov() |
| 217 if options.genhtml: | 347 if options.genhtml: |
| 218 coverage.GenerateHtml() | 348 coverage.GenerateHtml() |
| 219 return 0 | 349 return 0 |
| 220 | 350 |
| 221 | 351 |
| 222 if __name__ == '__main__': | 352 if __name__ == '__main__': |
| 223 sys.exit(main()) | 353 sys.exit(main()) |
| OLD | NEW |