Home | History | Annotate | Download | only in code_coverage
      1 #!/bin/env python
      2 # Copyright (c) 2011 The Chromium 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.
      5 
      6 
      7 """Module to setup and generate code coverage data
      8 
      9 This module first sets up the environment for code coverage, instruments the
     10 binaries, runs the tests and collects the code coverage data.
     11 
     12 
     13 Usage:
     14   coverage.py --upload=<upload_location>
     15               --revision=<revision_number>
     16               --src_root=<root_of_source_tree>
     17               [--tools_path=<tools_path>]
     18 """
     19 
     20 import logging
     21 import optparse
     22 import os
     23 import shutil
     24 import subprocess
     25 import sys
     26 import tempfile
     27 
     28 import google.logging_utils
     29 import google.process_utils as proc
     30 
     31 
     32 # The list of binaries that will be instrumented for code coverage
     33 # TODO(niranjan): Re-enable instrumentation of chrome.exe and chrome.dll once we
     34 # resolve the issue where vsinstr.exe is confused while reading symbols.
     35 windows_binaries = [#'chrome.exe',
     36                     #'chrome.dll',
     37                     'unit_tests.exe',
     38                     'automated_ui_tests.exe',
     39                     'installer_util_unittests.exe',
     40                     'ipc_tests.exe',
     41                     'memory_test.exe',
     42                     'page_cycler_tests.exe',
     43                     'perf_tests.exe',
     44                     'reliability_tests.exe',
     45                     'security_tests.dll',
     46                     'startup_tests.exe',
     47                     'tab_switching_test.exe',
     48                     'test_shell.exe']
     49 
     50 # The list of [tests, args] that will be run.
     51 # Failing tests have been commented out.
     52 # TODO(niranjan): Need to add layout tests that excercise the test shell.
     53 windows_tests = [
     54                  ['unit_tests.exe', ''],
     55 #                 ['automated_ui_tests.exe', ''],
     56                  ['installer_util_unittests.exe', ''],
     57                  ['ipc_tests.exe', ''],
     58                  ['page_cycler_tests.exe', '--gtest_filter=*File --no-sandbox'],
     59                  ['reliability_tests.exe', '--no-sandbox'],
     60                  ['startup_tests.exe', '--no-sandbox'],
     61                  ['tab_switching_test.exe', '--no-sandbox'],
     62                 ]
     63 
     64 
     65 def IsWindows():
     66   """Checks if the current platform is Windows.
     67   """
     68   return sys.platform[:3] == 'win'
     69 
     70 
     71 class Coverage(object):
     72   """Class to set up and generate code coverage.
     73 
     74   This class contains methods that are useful to set up the environment for
     75   code coverage.
     76 
     77   Attributes:
     78     instrumented: A boolean indicating if all the binaries have been
     79                   instrumented.
     80   """
     81 
     82   def __init__(self,
     83                revision,
     84                src_path = None,
     85                tools_path = None,
     86                archive=None):
     87     """Init method for the Coverage class.
     88 
     89     Args:
     90       revision: Revision number of the Chromium source tree.
     91       src_path: Location of the Chromium source base.
     92       tools_path: Location of the Visual Studio Team Tools. (Win32 only)
     93       archive: Archive location for the intermediate .coverage results.
     94     """
     95     google.logging_utils.config_root()
     96     self.revision = revision
     97     self.instrumented = False
     98     self.tools_path = tools_path
     99     self.src_path = src_path
    100     self._dir = tempfile.mkdtemp()
    101     self._archive = archive
    102 
    103   def SetUp(self, binaries):
    104     """Set up the platform specific environment and instrument the binaries for
    105     coverage.
    106 
    107     This method sets up the environment, instruments all the compiled binaries
    108     and sets up the code coverage counters.
    109 
    110     Args:
    111       binaries: List of binaries that need to be instrumented.
    112 
    113     Returns:
    114       True on success.
    115       False on error.
    116     """
    117     if self.instrumented:
    118       logging.error('Binaries already instrumented')
    119       return False
    120     if IsWindows():
    121       # Stop all previous instance of VSPerfMon counters
    122       counters_command = ('%s -shutdown' %
    123                           (os.path.join(self.tools_path, 'vsperfcmd.exe')))
    124       (retcode, output) = proc.RunCommandFull(counters_command,
    125                                               collect_output=True)
    126       # TODO(niranjan): Add a check that to verify that the binaries were built
    127       # using the /PROFILE linker flag.
    128       if self.tools_path == None:
    129         logging.error('Could not locate Visual Studio Team Server tools')
    130         return False
    131       # Remove trailing slashes
    132       self.tools_path = self.tools_path.rstrip('\\')
    133       # Add this to the env PATH.
    134       os.environ['PATH'] = os.environ['PATH'] + ';' + self.tools_path
    135       instrument_command = '%s /COVERAGE ' % (os.path.join(self.tools_path,
    136                                                            'vsinstr.exe'))
    137       for binary in binaries:
    138         logging.info('binary = %s' % (binary))
    139         logging.info('instrument_command = %s' % (instrument_command))
    140         # Instrument each binary in the list
    141         binary = os.path.join(self.src_path, 'chrome', 'Release', binary)
    142         (retcode, output) = proc.RunCommandFull(instrument_command + binary,
    143                                                 collect_output=True)
    144         # Check if the file has been instrumented correctly.
    145         if output.pop().rfind('Successfully instrumented') == -1:
    146           logging.error('Error instrumenting %s' % (binary))
    147           return False
    148       # We are now ready to run tests and measure code coverage.
    149       self.instrumented = True
    150       return True
    151 
    152   def TearDown(self):
    153     """Tear down method.
    154 
    155     This method shuts down the counters, and cleans up all the intermediate
    156     artifacts.
    157     """
    158     if self.instrumented == False:
    159       return
    160 
    161     if IsWindows():
    162       # Stop counters
    163       counters_command = ('%s -shutdown' %
    164                          (os.path.join(self.tools_path, 'vsperfcmd.exe')))
    165       (retcode, output) = proc.RunCommandFull(counters_command,
    166                                               collect_output=True)
    167       logging.info('Counters shut down: %s' % (output))
    168       # TODO(niranjan): Revert the instrumented binaries to their original
    169       # versions.
    170     else:
    171       return
    172     if self._archive:
    173       shutil.copytree(self._dir, os.path.join(self._archive, self.revision))
    174       logging.info('Archived the .coverage files')
    175     # Delete all the temp files and folders
    176     if self._dir != None:
    177       shutil.rmtree(self._dir, ignore_errors=True)
    178       logging.info('Cleaned up temporary files and folders')
    179     # Reset the instrumented flag.
    180     self.instrumented = False
    181 
    182   def RunTest(self, src_root, test):
    183     """Run tests and collect the .coverage file
    184 
    185     Args:
    186       src_root: Path to the root of the source.
    187       test: Path to the test to be run.
    188 
    189     Returns:
    190       Path of the intermediate .coverage file on success.
    191       None on error.
    192     """
    193     # Generate the intermediate file name for the coverage results
    194     test_name = os.path.split(test[0])[1].strip('.exe')
    195     # test_command = binary + args
    196     test_command = '%s %s' % (os.path.join(src_root,
    197                                            'chrome',
    198                                            'Release',
    199                                            test[0]),
    200                               test[1])
    201 
    202     coverage_file = os.path.join(self._dir, '%s_win32_%s.coverage' %
    203                                             (test_name, self.revision))
    204     logging.info('.coverage file for test %s: %s' % (test_name, coverage_file))
    205 
    206     # After all the binaries have been instrumented, we start the counters.
    207     counters_command = ('%s -start:coverage -output:%s' %
    208                         (os.path.join(self.tools_path, 'vsperfcmd.exe'),
    209                          coverage_file))
    210     # Here we use subprocess.call() instead of the RunCommandFull because the
    211     # VSPerfCmd spawns another process before terminating and this confuses
    212     # the subprocess.Popen() used by RunCommandFull.
    213     retcode = subprocess.call(counters_command)
    214 
    215     # Run the test binary
    216     logging.info('Executing test %s: ' % test_command)
    217     (retcode, output) = proc.RunCommandFull(test_command, collect_output=True)
    218     if retcode != 0: # Return error if the tests fail
    219       logging.error('One or more tests failed in %s.' % test_command)
    220       return None
    221 
    222     # Stop the counters
    223     counters_command = ('%s -shutdown' %
    224                         (os.path.join(self.tools_path, 'vsperfcmd.exe')))
    225     (retcode, output) = proc.RunCommandFull(counters_command,
    226                                             collect_output=True)
    227     logging.info('Counters shut down: %s' % (output))
    228     # Return the intermediate .coverage file
    229     return coverage_file
    230 
    231   def Upload(self, list_coverage, upload_path, sym_path=None, src_root=None):
    232     """Upload the results to the dashboard.
    233 
    234     This method uploads the coverage data to a dashboard where it will be
    235     processed. On Windows, this method will first convert the .coverage file to
    236     the lcov format. This method needs to be called before the TearDown method.
    237 
    238     Args:
    239       list_coverage: The list of coverage data files to consoliate and upload.
    240       upload_path: Destination where the coverage data will be processed.
    241       sym_path: Symbol path for the build (Win32 only)
    242       src_root: Root folder of the source tree (Win32 only)
    243 
    244     Returns:
    245       True on success.
    246       False on failure.
    247     """
    248     if upload_path == None:
    249       logging.info('Upload path not specified. Will not convert to LCOV')
    250       return True
    251 
    252     if IsWindows():
    253       # Stop counters
    254       counters_command = ('%s -shutdown' %
    255                           (os.path.join(self.tools_path, 'vsperfcmd.exe')))
    256       (retcode, output) = proc.RunCommandFull(counters_command,
    257                                               collect_output=True)
    258       logging.info('Counters shut down: %s' % (output))
    259       lcov_file = os.path.join(upload_path, 'chrome_win32_%s.lcov' %
    260                                             (self.revision))
    261       lcov = open(lcov_file, 'w')
    262       for coverage_file in list_coverage:
    263         # Convert the intermediate .coverage file to lcov format
    264         if self.tools_path == None:
    265           logging.error('Lcov converter tool not found')
    266           return False
    267         self.tools_path = self.tools_path.rstrip('\\')
    268         convert_command = ('%s -sym_path=%s -src_root=%s %s' %
    269                            (os.path.join(self.tools_path,
    270                                          'coverage_analyzer.exe'),
    271                            sym_path,
    272                            src_root,
    273                            coverage_file))
    274         (retcode, output) = proc.RunCommandFull(convert_command,
    275                                                 collect_output=True)
    276         # TODO(niranjan): Fix this to check for the correct return code.
    277 #        if output != 0:
    278 #          logging.error('Conversion to LCOV failed. Exiting.')
    279         tmp_lcov_file = coverage_file + '.lcov'
    280         logging.info('Conversion to lcov complete for %s' % (coverage_file))
    281         # Now append this .lcov file to the cumulative lcov file
    282         logging.info('Consolidating LCOV file: %s' % (tmp_lcov_file))
    283         tmp_lcov = open(tmp_lcov_file, 'r')
    284         lcov.write(tmp_lcov.read())
    285         tmp_lcov.close()
    286       lcov.close()
    287       logging.info('LCOV file uploaded to %s' % (upload_path))
    288 
    289 
    290 def main():
    291   # Command line parsing
    292   parser = optparse.OptionParser()
    293   # Path where the .coverage to .lcov converter tools are stored.
    294   parser.add_option('-t',
    295                     '--tools_path',
    296                     dest='tools_path',
    297                     default=None,
    298                     help='Location of the coverage tools (windows only)')
    299   parser.add_option('-u',
    300                     '--upload',
    301                     dest='upload_path',
    302                     default=None,
    303                     help='Location where the results should be uploaded')
    304   # We need the revision number so that we can generate the output file of the
    305   # format chrome_<platform>_<revision>.lcov
    306   parser.add_option('-r',
    307                     '--revision',
    308                     dest='revision',
    309                     default=None,
    310                     help='Revision number of the Chromium source repo')
    311   # Root of the source tree. Needed for converting the generated .coverage file
    312   # on Windows to the open source lcov format.
    313   parser.add_option('-s',
    314                     '--src_root',
    315                     dest='src_root',
    316                     default=None,
    317                     help='Root of the source repository')
    318   parser.add_option('-a',
    319                     '--archive',
    320                     dest='archive',
    321                     default=None,
    322                     help='Archive location of the intermediate .coverage data')
    323 
    324   (options, args) = parser.parse_args()
    325 
    326   if options.revision == None:
    327     parser.error('Revision number not specified')
    328   if options.src_root == None:
    329     parser.error('Source root not specified')
    330 
    331   if IsWindows():
    332     # Initialize coverage
    333     cov = Coverage(options.revision,
    334                    options.src_root,
    335                    options.tools_path,
    336                    options.archive)
    337     list_coverage = []
    338     # Instrument the binaries
    339     if cov.SetUp(windows_binaries):
    340       # Run all the tests
    341       for test in windows_tests:
    342         coverage = cov.RunTest(options.src_root, test)
    343         if coverage == None: # Indicate failure to the buildbots.
    344           return 1
    345         # Collect the intermediate file
    346         list_coverage.append(coverage)
    347     else:
    348       logging.error('Error during instrumentation.')
    349       sys.exit(1)
    350 
    351     cov.Upload(list_coverage,
    352                options.upload_path,
    353                os.path.join(options.src_root, 'chrome', 'Release'),
    354                options.src_root)
    355     cov.TearDown()
    356 
    357 
    358 if __name__ == '__main__':
    359   sys.exit(main())
    360