Home | History | Annotate | Download | only in webrtc
      1 # Copyright (c) 2012 The WebRTC project authors. All Rights Reserved.
      2 #
      3 # Use of this source code is governed by a BSD-style license
      4 # that can be found in the LICENSE file in the root of the source
      5 # tree. An additional intellectual property rights grant can be found
      6 # in the file PATENTS.  All contributing project authors may
      7 # be found in the AUTHORS file in the root of the source tree.
      8 
      9 import json
     10 import os
     11 import platform
     12 import re
     13 import subprocess
     14 import sys
     15 
     16 
     17 # Directories that will be scanned by cpplint by the presubmit script.
     18 CPPLINT_DIRS = [
     19   'webrtc/audio',
     20   'webrtc/call',
     21   'webrtc/common_video',
     22   'webrtc/examples',
     23   'webrtc/modules/remote_bitrate_estimator',
     24   'webrtc/modules/rtp_rtcp',
     25   'webrtc/modules/video_coding',
     26   'webrtc/modules/video_processing',
     27   'webrtc/sound',
     28   'webrtc/tools',
     29   'webrtc/video',
     30 ]
     31 
     32 # List of directories of "supported" native APIs. That means changes to headers
     33 # will be done in a compatible way following this scheme:
     34 # 1. Non-breaking changes are made.
     35 # 2. The old APIs as marked as deprecated (with comments).
     36 # 3. Deprecation is announced to discuss-webrtc (at] googlegroups.com and
     37 #    webrtc-users (at] google.com (internal list).
     38 # 4. (later) The deprecated APIs are removed.
     39 # Directories marked as DEPRECATED should not be used. They're only present in
     40 # the list to support legacy downstream code.
     41 NATIVE_API_DIRS = (
     42   'talk/app/webrtc',
     43   'webrtc',
     44   'webrtc/base',  # DEPRECATED.
     45   'webrtc/common_audio/include',  # DEPRECATED.
     46   'webrtc/modules/audio_coding/include',
     47   'webrtc/modules/audio_conference_mixer/include',  # DEPRECATED.
     48   'webrtc/modules/audio_device/include',
     49   'webrtc/modules/audio_processing/include',
     50   'webrtc/modules/bitrate_controller/include',
     51   'webrtc/modules/include',
     52   'webrtc/modules/remote_bitrate_estimator/include',
     53   'webrtc/modules/rtp_rtcp/include',
     54   'webrtc/modules/rtp_rtcp/source',  # DEPRECATED.
     55   'webrtc/modules/utility/include',
     56   'webrtc/modules/video_coding/codecs/h264/include',
     57   'webrtc/modules/video_coding/codecs/i420/include',
     58   'webrtc/modules/video_coding/codecs/vp8/include',
     59   'webrtc/modules/video_coding/codecs/vp9/include',
     60   'webrtc/modules/video_coding/include',
     61   'webrtc/system_wrappers/include',  # DEPRECATED.
     62   'webrtc/voice_engine/include',
     63 )
     64 
     65 
     66 def _VerifyNativeApiHeadersListIsValid(input_api, output_api):
     67   """Ensures the list of native API header directories is up to date."""
     68   non_existing_paths = []
     69   native_api_full_paths = [
     70       input_api.os_path.join(input_api.PresubmitLocalPath(),
     71                              *path.split('/')) for path in NATIVE_API_DIRS]
     72   for path in native_api_full_paths:
     73     if not os.path.isdir(path):
     74       non_existing_paths.append(path)
     75   if non_existing_paths:
     76     return [output_api.PresubmitError(
     77         'Directories to native API headers have changed which has made the '
     78         'list in PRESUBMIT.py outdated.\nPlease update it to the current '
     79         'location of our native APIs.',
     80         non_existing_paths)]
     81   return []
     82 
     83 
     84 def _CheckNativeApiHeaderChanges(input_api, output_api):
     85   """Checks to remind proper changing of native APIs."""
     86   files = []
     87   for f in input_api.AffectedSourceFiles(input_api.FilterSourceFile):
     88     if f.LocalPath().endswith('.h'):
     89       for path in NATIVE_API_DIRS:
     90         if os.path.dirname(f.LocalPath()) == path:
     91           files.append(f)
     92 
     93   if files:
     94     return [output_api.PresubmitNotifyResult(
     95         'You seem to be changing native API header files. Please make sure '
     96         'you:\n'
     97         '  1. Make compatible changes that don\'t break existing clients.\n'
     98         '  2. Mark the old APIs as deprecated.\n'
     99         '  3. Create a timeline and plan for when the deprecated method will '
    100         'be removed (preferably 3 months or so).\n'
    101         '  4. Update/inform existing downstream code owners to stop using the '
    102         'deprecated APIs: \n'
    103         'send announcement to discuss-webrtc (at] googlegroups.com and '
    104         'webrtc-users (at] google.com.\n'
    105         '  5. (after ~3 months) remove the deprecated API.\n'
    106         'Related files:',
    107         files)]
    108   return []
    109 
    110 
    111 def _CheckNoIOStreamInHeaders(input_api, output_api):
    112   """Checks to make sure no .h files include <iostream>."""
    113   files = []
    114   pattern = input_api.re.compile(r'^#include\s*<iostream>',
    115                                  input_api.re.MULTILINE)
    116   for f in input_api.AffectedSourceFiles(input_api.FilterSourceFile):
    117     if not f.LocalPath().endswith('.h'):
    118       continue
    119     contents = input_api.ReadFile(f)
    120     if pattern.search(contents):
    121       files.append(f)
    122 
    123   if len(files):
    124     return [output_api.PresubmitError(
    125         'Do not #include <iostream> in header files, since it inserts static ' +
    126         'initialization into every file including the header. Instead, ' +
    127         '#include <ostream>. See http://crbug.com/94794',
    128         files)]
    129   return []
    130 
    131 
    132 def _CheckNoFRIEND_TEST(input_api, output_api):
    133   """Make sure that gtest's FRIEND_TEST() macro is not used, the
    134   FRIEND_TEST_ALL_PREFIXES() macro from testsupport/gtest_prod_util.h should be
    135   used instead since that allows for FLAKY_, FAILS_ and DISABLED_ prefixes."""
    136   problems = []
    137 
    138   file_filter = lambda f: f.LocalPath().endswith(('.cc', '.h'))
    139   for f in input_api.AffectedFiles(file_filter=file_filter):
    140     for line_num, line in f.ChangedContents():
    141       if 'FRIEND_TEST(' in line:
    142         problems.append('    %s:%d' % (f.LocalPath(), line_num))
    143 
    144   if not problems:
    145     return []
    146   return [output_api.PresubmitPromptWarning('WebRTC\'s code should not use '
    147       'gtest\'s FRIEND_TEST() macro. Include testsupport/gtest_prod_util.h and '
    148       'use FRIEND_TEST_ALL_PREFIXES() instead.\n' + '\n'.join(problems))]
    149 
    150 
    151 def _IsLintWhitelisted(whitelist_dirs, file_path):
    152   """ Checks if a file is whitelisted for lint check."""
    153   for path in whitelist_dirs:
    154     if os.path.dirname(file_path).startswith(path):
    155       return True
    156   return False
    157 
    158 
    159 def _CheckApprovedFilesLintClean(input_api, output_api,
    160                                  source_file_filter=None):
    161   """Checks that all new or whitelisted .cc and .h files pass cpplint.py.
    162   This check is based on _CheckChangeLintsClean in
    163   depot_tools/presubmit_canned_checks.py but has less filters and only checks
    164   added files."""
    165   result = []
    166 
    167   # Initialize cpplint.
    168   import cpplint
    169   # Access to a protected member _XX of a client class
    170   # pylint: disable=W0212
    171   cpplint._cpplint_state.ResetErrorCounts()
    172 
    173   # Create a platform independent whitelist for the CPPLINT_DIRS.
    174   whitelist_dirs = [input_api.os_path.join(*path.split('/'))
    175                     for path in CPPLINT_DIRS]
    176 
    177   # Use the strictest verbosity level for cpplint.py (level 1) which is the
    178   # default when running cpplint.py from command line.
    179   # To make it possible to work with not-yet-converted code, we're only applying
    180   # it to new (or moved/renamed) files and files listed in LINT_FOLDERS.
    181   verbosity_level = 1
    182   files = []
    183   for f in input_api.AffectedSourceFiles(source_file_filter):
    184     # Note that moved/renamed files also count as added.
    185     if f.Action() == 'A' or _IsLintWhitelisted(whitelist_dirs, f.LocalPath()):
    186       files.append(f.AbsoluteLocalPath())
    187 
    188   for file_name in files:
    189     cpplint.ProcessFile(file_name, verbosity_level)
    190 
    191   if cpplint._cpplint_state.error_count > 0:
    192     if input_api.is_committing:
    193       # TODO(kjellander): Change back to PresubmitError below when we're
    194       # confident with the lint settings.
    195       res_type = output_api.PresubmitPromptWarning
    196     else:
    197       res_type = output_api.PresubmitPromptWarning
    198     result = [res_type('Changelist failed cpplint.py check.')]
    199 
    200   return result
    201 
    202 def _CheckNoRtcBaseDeps(input_api, gyp_files, output_api):
    203   pattern = input_api.re.compile(r"base.gyp:rtc_base\s*'")
    204   violating_files = []
    205   for f in gyp_files:
    206     gyp_exceptions = (
    207         'base_tests.gyp',
    208         'desktop_capture.gypi',
    209         'libjingle.gyp',
    210         'libjingle_tests.gyp',
    211         'p2p.gyp',
    212         'sound.gyp',
    213         'webrtc_test_common.gyp',
    214         'webrtc_tests.gypi',
    215     )
    216     if f.LocalPath().endswith(gyp_exceptions):
    217       continue
    218     contents = input_api.ReadFile(f)
    219     if pattern.search(contents):
    220       violating_files.append(f)
    221   if violating_files:
    222     return [output_api.PresubmitError(
    223         'Depending on rtc_base is not allowed. Change your dependency to '
    224         'rtc_base_approved and possibly sanitize and move the desired source '
    225         'file(s) to rtc_base_approved.\nChanged GYP files:',
    226         items=violating_files)]
    227   return []
    228 
    229 def _CheckNoSourcesAboveGyp(input_api, gyp_files, output_api):
    230   # Disallow referencing source files with paths above the GYP file location.
    231   source_pattern = input_api.re.compile(r'sources.*?\[(.*?)\]',
    232                                         re.MULTILINE | re.DOTALL)
    233   file_pattern = input_api.re.compile(r"'((\.\./.*?)|(<\(webrtc_root\).*?))'")
    234   violating_gyp_files = set()
    235   violating_source_entries = []
    236   for gyp_file in gyp_files:
    237     contents = input_api.ReadFile(gyp_file)
    238     for source_block_match in source_pattern.finditer(contents):
    239       # Find all source list entries starting with ../ in the source block
    240       # (exclude overrides entries).
    241       for file_list_match in file_pattern.finditer(source_block_match.group(0)):
    242         source_file = file_list_match.group(0)
    243         if 'overrides/' not in source_file:
    244           violating_source_entries.append(source_file)
    245           violating_gyp_files.add(gyp_file)
    246   if violating_gyp_files:
    247     return [output_api.PresubmitError(
    248         'Referencing source files above the directory of the GYP file is not '
    249         'allowed. Please introduce new GYP targets and/or GYP files in the '
    250         'proper location instead.\n'
    251         'Invalid source entries:\n'
    252         '%s\n'
    253         'Violating GYP files:' % '\n'.join(violating_source_entries),
    254         items=violating_gyp_files)]
    255   return []
    256 
    257 def _CheckGypChanges(input_api, output_api):
    258   source_file_filter = lambda x: input_api.FilterSourceFile(
    259       x, white_list=(r'.+\.(gyp|gypi)$',))
    260 
    261   gyp_files = []
    262   for f in input_api.AffectedSourceFiles(source_file_filter):
    263     if f.LocalPath().startswith('webrtc'):
    264       gyp_files.append(f)
    265 
    266   result = []
    267   if gyp_files:
    268     result.append(output_api.PresubmitNotifyResult(
    269         'As you\'re changing GYP files: please make sure corresponding '
    270         'BUILD.gn files are also updated.\nChanged GYP files:',
    271         items=gyp_files))
    272     result.extend(_CheckNoRtcBaseDeps(input_api, gyp_files, output_api))
    273     result.extend(_CheckNoSourcesAboveGyp(input_api, gyp_files, output_api))
    274   return result
    275 
    276 def _CheckUnwantedDependencies(input_api, output_api):
    277   """Runs checkdeps on #include statements added in this
    278   change. Breaking - rules is an error, breaking ! rules is a
    279   warning.
    280   """
    281   # Copied from Chromium's src/PRESUBMIT.py.
    282 
    283   # We need to wait until we have an input_api object and use this
    284   # roundabout construct to import checkdeps because this file is
    285   # eval-ed and thus doesn't have __file__.
    286   original_sys_path = sys.path
    287   try:
    288     checkdeps_path = input_api.os_path.join(input_api.PresubmitLocalPath(),
    289                                             'buildtools', 'checkdeps')
    290     if not os.path.exists(checkdeps_path):
    291       return [output_api.PresubmitError(
    292           'Cannot find checkdeps at %s\nHave you run "gclient sync" to '
    293           'download Chromium and setup the symlinks?' % checkdeps_path)]
    294     sys.path.append(checkdeps_path)
    295     import checkdeps
    296     from cpp_checker import CppChecker
    297     from rules import Rule
    298   finally:
    299     # Restore sys.path to what it was before.
    300     sys.path = original_sys_path
    301 
    302   added_includes = []
    303   for f in input_api.AffectedFiles():
    304     if not CppChecker.IsCppFile(f.LocalPath()):
    305       continue
    306 
    307     changed_lines = [line for _, line in f.ChangedContents()]
    308     added_includes.append([f.LocalPath(), changed_lines])
    309 
    310   deps_checker = checkdeps.DepsChecker(input_api.PresubmitLocalPath())
    311 
    312   error_descriptions = []
    313   warning_descriptions = []
    314   for path, rule_type, rule_description in deps_checker.CheckAddedCppIncludes(
    315       added_includes):
    316     description_with_path = '%s\n    %s' % (path, rule_description)
    317     if rule_type == Rule.DISALLOW:
    318       error_descriptions.append(description_with_path)
    319     else:
    320       warning_descriptions.append(description_with_path)
    321 
    322   results = []
    323   if error_descriptions:
    324     results.append(output_api.PresubmitError(
    325         'You added one or more #includes that violate checkdeps rules.',
    326         error_descriptions))
    327   if warning_descriptions:
    328     results.append(output_api.PresubmitPromptOrNotify(
    329         'You added one or more #includes of files that are temporarily\n'
    330         'allowed but being removed. Can you avoid introducing the\n'
    331         '#include? See relevant DEPS file(s) for details and contacts.',
    332         warning_descriptions))
    333   return results
    334 
    335 
    336 def _RunPythonTests(input_api, output_api):
    337   def join(*args):
    338     return input_api.os_path.join(input_api.PresubmitLocalPath(), *args)
    339 
    340   test_directories = [
    341     join('tools', 'autoroller', 'unittests'),
    342   ]
    343 
    344   tests = []
    345   for directory in test_directories:
    346     tests.extend(
    347       input_api.canned_checks.GetUnitTestsInDirectory(
    348           input_api,
    349           output_api,
    350           directory,
    351           whitelist=[r'.+_test\.py$']))
    352   return input_api.RunTests(tests, parallel=True)
    353 
    354 
    355 def _CommonChecks(input_api, output_api):
    356   """Checks common to both upload and commit."""
    357   results = []
    358   # Filter out files that are in objc or ios dirs from being cpplint-ed since
    359   # they do not follow C++ lint rules.
    360   black_list = input_api.DEFAULT_BLACK_LIST + (
    361     r".*\bobjc[\\\/].*",
    362   )
    363   source_file_filter = lambda x: input_api.FilterSourceFile(x, None, black_list)
    364   results.extend(_CheckApprovedFilesLintClean(
    365       input_api, output_api, source_file_filter))
    366   results.extend(input_api.canned_checks.RunPylint(input_api, output_api,
    367       black_list=(r'^.*gviz_api\.py$',
    368                   r'^.*gaeunit\.py$',
    369                   # Embedded shell-script fakes out pylint.
    370                   r'^build[\\\/].*\.py$',
    371                   r'^buildtools[\\\/].*\.py$',
    372                   r'^chromium[\\\/].*\.py$',
    373                   r'^google_apis[\\\/].*\.py$',
    374                   r'^net.*[\\\/].*\.py$',
    375                   r'^out.*[\\\/].*\.py$',
    376                   r'^testing[\\\/].*\.py$',
    377                   r'^third_party[\\\/].*\.py$',
    378                   r'^tools[\\\/]find_depot_tools.py$',
    379                   r'^tools[\\\/]clang[\\\/].*\.py$',
    380                   r'^tools[\\\/]generate_library_loader[\\\/].*\.py$',
    381                   r'^tools[\\\/]gn[\\\/].*\.py$',
    382                   r'^tools[\\\/]gyp[\\\/].*\.py$',
    383                   r'^tools[\\\/]isolate_driver.py$',
    384                   r'^tools[\\\/]protoc_wrapper[\\\/].*\.py$',
    385                   r'^tools[\\\/]python[\\\/].*\.py$',
    386                   r'^tools[\\\/]python_charts[\\\/]data[\\\/].*\.py$',
    387                   r'^tools[\\\/]refactoring[\\\/].*\.py$',
    388                   r'^tools[\\\/]swarming_client[\\\/].*\.py$',
    389                   r'^tools[\\\/]vim[\\\/].*\.py$',
    390                   # TODO(phoglund): should arguably be checked.
    391                   r'^tools[\\\/]valgrind-webrtc[\\\/].*\.py$',
    392                   r'^tools[\\\/]valgrind[\\\/].*\.py$',
    393                   r'^tools[\\\/]win[\\\/].*\.py$',
    394                   r'^xcodebuild.*[\\\/].*\.py$',),
    395       disabled_warnings=['F0401',  # Failed to import x
    396                          'E0611',  # No package y in x
    397                          'W0232',  # Class has no __init__ method
    398                         ],
    399       pylintrc='pylintrc'))
    400   # WebRTC can't use the presubmit_canned_checks.PanProjectChecks function since
    401   # we need to have different license checks in talk/ and webrtc/ directories.
    402   # Instead, hand-picked checks are included below.
    403 
    404   # Skip long-lines check for DEPS, GN and GYP files.
    405   long_lines_sources = lambda x: input_api.FilterSourceFile(x,
    406       black_list=(r'.+\.gyp$', r'.+\.gypi$', r'.+\.gn$', r'.+\.gni$', 'DEPS'))
    407   results.extend(input_api.canned_checks.CheckLongLines(
    408       input_api, output_api, maxlen=80, source_file_filter=long_lines_sources))
    409   results.extend(input_api.canned_checks.CheckChangeHasNoTabs(
    410       input_api, output_api))
    411   results.extend(input_api.canned_checks.CheckChangeHasNoStrayWhitespace(
    412       input_api, output_api))
    413   results.extend(input_api.canned_checks.CheckChangeTodoHasOwner(
    414       input_api, output_api))
    415   results.extend(_CheckNativeApiHeaderChanges(input_api, output_api))
    416   results.extend(_CheckNoIOStreamInHeaders(input_api, output_api))
    417   results.extend(_CheckNoFRIEND_TEST(input_api, output_api))
    418   results.extend(_CheckGypChanges(input_api, output_api))
    419   results.extend(_CheckUnwantedDependencies(input_api, output_api))
    420   results.extend(_RunPythonTests(input_api, output_api))
    421   return results
    422 
    423 
    424 def CheckChangeOnUpload(input_api, output_api):
    425   results = []
    426   results.extend(_CommonChecks(input_api, output_api))
    427   results.extend(
    428       input_api.canned_checks.CheckGNFormatted(input_api, output_api))
    429   return results
    430 
    431 
    432 def CheckChangeOnCommit(input_api, output_api):
    433   results = []
    434   results.extend(_CommonChecks(input_api, output_api))
    435   results.extend(_VerifyNativeApiHeadersListIsValid(input_api, output_api))
    436   results.extend(input_api.canned_checks.CheckOwners(input_api, output_api))
    437   results.extend(input_api.canned_checks.CheckChangeWasUploaded(
    438       input_api, output_api))
    439   results.extend(input_api.canned_checks.CheckChangeHasDescription(
    440       input_api, output_api))
    441   results.extend(input_api.canned_checks.CheckChangeHasBugField(
    442       input_api, output_api))
    443   results.extend(input_api.canned_checks.CheckChangeHasTestField(
    444       input_api, output_api))
    445   results.extend(input_api.canned_checks.CheckTreeIsOpen(
    446       input_api, output_api,
    447       json_url='http://webrtc-status.appspot.com/current?format=json'))
    448   return results
    449 
    450 
    451 # pylint: disable=W0613
    452 def GetPreferredTryMasters(project, change):
    453   cq_config_path = os.path.join(
    454       change.RepositoryRoot(), 'infra', 'config', 'cq.cfg')
    455   # commit_queue.py below is a script in depot_tools directory, which has a
    456   # 'builders' command to retrieve a list of CQ builders from the CQ config.
    457   is_win = platform.system() == 'Windows'
    458   masters = json.loads(subprocess.check_output(
    459       ['commit_queue', 'builders', cq_config_path], shell=is_win))
    460 
    461   try_config = {}
    462   for master in masters:
    463     try_config.setdefault(master, {})
    464     for builder in masters[master]:
    465       if 'presubmit' in builder:
    466         # Do not trigger presubmit builders, since they're likely to fail
    467         # (e.g. OWNERS checks before finished code review), and we're running
    468         # local presubmit anyway.
    469         pass
    470       else:
    471         try_config[master][builder] = ['defaulttests']
    472 
    473   return try_config
    474