OLD | NEW |
(Empty) | |
| 1 #!python |
| 2 # Copyright 2012 Google Inc. All Rights Reserved. |
| 3 # |
| 4 # Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 # you may not use this file except in compliance with the License. |
| 6 # You may obtain a copy of the License at |
| 7 # |
| 8 # http://www.apache.org/licenses/LICENSE-2.0 |
| 9 # |
| 10 # Unless required by applicable law or agreed to in writing, software |
| 11 # distributed under the License is distributed on an "AS IS" BASIS, |
| 12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 # See the License for the specific language governing permissions and |
| 14 # limitations under the License. |
| 15 # |
| 16 # Presubmit script for Pipa, highly inspired by Syzygy's presubmit checks. |
| 17 |
| 18 import itertools |
| 19 import json |
| 20 import os |
| 21 import re |
| 22 import subprocess |
| 23 import sys |
| 24 |
| 25 |
| 26 # Determine the root of the source tree. We use getcwd() instead of __file__ |
| 27 # because gcl loads this script as text and runs it using eval(). In this |
| 28 # context the variable __file__ is undefined. However, gcl assures us that |
| 29 # the current working directory will be the directory containing this file. |
| 30 PIPA_ROOT_DIR = os.path.join(os.path.abspath(os.getcwd()), 'pipa') |
| 31 |
| 32 |
| 33 # Bring in some presubmit tools. |
| 34 sys.path.insert(0, os.path.join(PIPA_ROOT_DIR, 'py')) |
| 35 import deps_utils |
| 36 from test_utils import presubmit |
| 37 |
| 38 _UNITTEST_MESSAGE = """\ |
| 39 Your %%s unittests must succeed before submitting! To clear this error, |
| 40 run: %s""" % os.path.join(PIPA_ROOT_DIR, 'run_all_tests.bat') |
| 41 |
| 42 |
| 43 # License header and copyright line. |
| 44 _LICENSE_HEADER = """\ |
| 45 (#!python\n\ |
| 46 )?.*? Copyright 20[0-9][0-9] (Google Inc|The Chromium Authors)\. \ |
| 47 All Rights Reserved\.\n\ |
| 48 .*?\n\ |
| 49 """ |
| 50 |
| 51 |
| 52 # Regular expressions to recognize source and header files. |
| 53 # These are lifted from presubmit_support.py in depot_tools and are |
| 54 # formulated as a list of regex strings so that they can be passed to |
| 55 # input_api.FilterSourceFile() as the white_list parameter. |
| 56 _CC_SOURCES = (r'.+\.c$', r'.+\.cc$', r'.+\.cpp$', r'.+\.rc$') |
| 57 _CC_HEADERS = (r'.+\.h$', r'.+\.inl$', r'.+\.hxx$', r'.+\.hpp$') |
| 58 _CC_FILES = _CC_SOURCES + _CC_HEADERS |
| 59 _CC_SOURCES_RE = re.compile('|'.join('(?:%s)' % x for x in _CC_SOURCES)) |
| 60 |
| 61 # When no files to blacklist. |
| 62 _CC_FILES_BLACKLIST = [] |
| 63 |
| 64 # Regular expressions used to extract headers and recognize empty lines. |
| 65 _INCLUDE_RE = re.compile(r'^\s*#\s*include\s+(?P<header>[<"][^<"]+[>"])' |
| 66 r'\s*(?://.*(?P<nolint>NOLINT).*)?$') |
| 67 _COMMENT_OR_BLANK_RE = re.compile(r'^\s*(?://.)?$') |
| 68 |
| 69 |
| 70 def _IsSourceHeaderPair(source_path, header): |
| 71 # Returns true if source and header are a matched pair. |
| 72 # Source is the path on disk to the source file and header is the include |
| 73 # reference to the header (i.e., "blah/foo.h" or <blah/foo.h> including the |
| 74 # outer quotes or brackets. |
| 75 if not _CC_SOURCES_RE.match(source_path): |
| 76 return False |
| 77 |
| 78 source_root = os.path.splitext(source_path)[0] |
| 79 if source_root.endswith('_unittest'): |
| 80 source_root = source_root[0:-9] |
| 81 include = os.path.normpath(source_root + '.h') |
| 82 header = os.path.normpath(header[1:-1]) |
| 83 |
| 84 return include.endswith(header) |
| 85 |
| 86 |
| 87 def _GetHeaderCompareKey(source_path, header): |
| 88 if _IsSourceHeaderPair(source_path, header): |
| 89 # We put the header that corresponds to this source file first. |
| 90 group = 0 |
| 91 elif header.startswith('<'): |
| 92 # C++ system headers should come after C system headers. |
| 93 group = 1 if header.endswith('.h>') else 2 |
| 94 else: |
| 95 group = 3 |
| 96 dirname, basename = os.path.split(header[1:-1]) |
| 97 return (group, dirname.lower(), basename.lower()) |
| 98 |
| 99 |
| 100 def _GetHeaderCompareKeyFunc(source): |
| 101 return lambda header : _GetHeaderCompareKey(source, header) |
| 102 |
| 103 |
| 104 def _HeaderGroups(source_lines): |
| 105 # Generates lists of headers in source, one per block of headers. |
| 106 # Each generated value is a tuple (line, headers) denoting on which |
| 107 # line of the source file an uninterrupted sequences of includes begins, |
| 108 # and the list of included headers (paths including the quotes or angle |
| 109 # brackets). |
| 110 start_line, headers = None, [] |
| 111 for line, num in itertools.izip(source_lines, itertools.count(1)): |
| 112 match = _INCLUDE_RE.match(line) |
| 113 if match: |
| 114 # The win32 api has all sorts of implicit include order dependencies. |
| 115 # Rather than encode exceptions for these, we require that they be |
| 116 # excluded from the ordering by a // NOLINT comment. |
| 117 if not match.group('nolint'): |
| 118 headers.append(match.group('header')) |
| 119 if start_line is None: |
| 120 # We just started a new run of headers. |
| 121 start_line = num |
| 122 elif headers and not _COMMENT_OR_BLANK_RE.match(line): |
| 123 # Any non-empty or non-comment line interrupts a sequence of includes. |
| 124 assert start_line is not None |
| 125 yield (start_line, headers) |
| 126 start_line = None |
| 127 headers = [] |
| 128 |
| 129 # Just in case we have some headers we haven't yielded yet, this is our |
| 130 # last chance to do so. |
| 131 if headers: |
| 132 assert start_line is not None |
| 133 yield (start_line, headers) |
| 134 |
| 135 |
| 136 # Stolen from depot_tool's my_activity.py |
| 137 def _DateTimeFromRietveld(date_string): |
| 138 try: |
| 139 return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S.%f') |
| 140 except ValueError: |
| 141 # Sometimes rietveld returns a value without the milliseconds part, so we |
| 142 # attempt to parse those cases as well. |
| 143 return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S') |
| 144 |
| 145 |
| 146 def _ReadFile(input_api, path): |
| 147 file_path = input_api.change.RepositoryRoot() + '/' + path |
| 148 return input_api.ReadFile(file_path) |
| 149 |
| 150 |
| 151 def CheckIncludeOrder(input_api, output_api): |
| 152 """Checks that the C/C++ include order is correct.""" |
| 153 errors = [] |
| 154 is_cc_file = lambda x: input_api.FilterSourceFile(x, white_list=_CC_FILES) |
| 155 for f in input_api.AffectedFiles(include_deletes=False, |
| 156 file_filter=is_cc_file): |
| 157 for line_num, group in _HeaderGroups(f.NewContents()): |
| 158 sorted_group = sorted(group, key=_GetHeaderCompareKeyFunc(f.LocalPath())) |
| 159 if group != sorted_group: |
| 160 message = '%s, line %s: Out of order includes. ' \ |
| 161 'Expected:\n\t#include %s' % ( |
| 162 f.LocalPath(), |
| 163 line_num, |
| 164 '\n\t#include '.join(sorted_group)) |
| 165 errors.append(output_api.PresubmitPromptWarning(message)) |
| 166 return errors |
| 167 |
| 168 |
| 169 def CheckUnittestsRan(input_api, output_api, committing, configuration): |
| 170 """Checks that the unittests success file is newer than any modified file""" |
| 171 return presubmit.CheckTestSuccess(input_api, output_api, committing, |
| 172 configuration, 'ALL', |
| 173 message=_UNITTEST_MESSAGE % configuration) |
| 174 |
| 175 |
| 176 def CheckAllDepsInGitignore(input_api, output_api): |
| 177 gitignore = str(_ReadFile(input_api, '.gitignore')) |
| 178 ignored_folders = filter(lambda line: line.startswith('/'), |
| 179 gitignore.split('\n')) |
| 180 # Remove trailing whitespaces and forward slashes and the beginning and at |
| 181 # the end of .gitignore lines. |
| 182 ignored = set([folder.strip().strip('/') for folder in ignored_folders]) |
| 183 |
| 184 messages = [] |
| 185 def AddErrorIfNotIgnored(path): |
| 186 # Check if |path| or its parent directories are ignored. |
| 187 path_segments = path.strip('/').split('/') |
| 188 for length in range(1, len(path_segments) + 1): |
| 189 if '/'.join(path_segments[:length]) in ignored: |
| 190 return |
| 191 messages.append(output_api.PresubmitError( |
| 192 'External dependency path %s is not in .gitignore file' % path)) |
| 193 |
| 194 if 'DEPS' in input_api.LocalPaths(): |
| 195 deps_content = _ReadFile(input_api, 'DEPS') |
| 196 deps = deps_utils.ParseDeps(deps_content, 'DEPS') |
| 197 for root_dir in deps: |
| 198 if root_dir.startswith('src/'): |
| 199 root_dir = root_dir[3:] |
| 200 AddErrorIfNotIgnored(root_dir) |
| 201 |
| 202 if 'GITDEPS' in input_api.LocalPaths(): |
| 203 deps_content = _ReadFile(input_api, 'GITDEPS') |
| 204 deps = deps_utils.ParseDeps(deps_content, 'GITDEPS') |
| 205 for root_dir, (_, subdirs, _) in deps.iteritems(): |
| 206 if subdirs: |
| 207 for subdir in subdirs: |
| 208 AddErrorIfNotIgnored(root_dir + '/' + subdir) |
| 209 else: |
| 210 AddErrorIfNotIgnored(root_dir) |
| 211 |
| 212 return messages |
| 213 |
| 214 |
| 215 def CheckChange(input_api, output_api, committing): |
| 216 if 'dcommit' in sys.argv: |
| 217 return [output_api.PresubmitPromptWarning( |
| 218 'You must use "git cl land" and not "git cl dcommit".')] |
| 219 |
| 220 # The list of (canned) checks we perform on all files in all changes. |
| 221 checks = [ |
| 222 CheckAllDepsInGitignore, |
| 223 CheckIncludeOrder, |
| 224 input_api.canned_checks.CheckChangeHasDescription, |
| 225 input_api.canned_checks.CheckChangeHasNoCrAndHasOnlyOneEol, |
| 226 input_api.canned_checks.CheckChangeHasNoTabs, |
| 227 input_api.canned_checks.CheckChangeHasNoStrayWhitespace, |
| 228 input_api.canned_checks.CheckDoNotSubmit, |
| 229 input_api.canned_checks.CheckGenderNeutral, |
| 230 input_api.canned_checks.CheckPatchFormatted, |
| 231 ] |
| 232 if committing: |
| 233 checks.append(input_api.canned_checks.CheckDoNotSubmit) |
| 234 |
| 235 results = [] |
| 236 for check in checks: |
| 237 results += check(input_api, output_api) |
| 238 |
| 239 results += input_api.canned_checks.CheckLongLines(input_api, output_api, 80) |
| 240 |
| 241 # We run lint only on C/C++ files so that we avoid getting notices about |
| 242 # files being ignored. |
| 243 is_cc_file = lambda x: input_api.FilterSourceFile(x, white_list=_CC_FILES, |
| 244 black_list=_CC_FILES_BLACKLIST) |
| 245 results += input_api.canned_checks.CheckChangeLintsClean( |
| 246 input_api, output_api, source_file_filter=is_cc_file) |
| 247 |
| 248 # We check the license on the default recognized source file types, as well |
| 249 # as GN files. |
| 250 gn_file_re = r'.+\.gn?$' |
| 251 gni_file_re = r'.+\.gni?$' |
| 252 white_list = input_api.DEFAULT_WHITE_LIST + (gn_file_re, gni_file_re) |
| 253 sources = lambda x: input_api.FilterSourceFile(x, white_list=white_list) |
| 254 results += input_api.canned_checks.CheckLicense(input_api, |
| 255 output_api, |
| 256 _LICENSE_HEADER, |
| 257 source_file_filter=sources) |
| 258 |
| 259 # results += CheckUnittestsRan(input_api, output_api, committing, "Debug") |
| 260 # results += CheckUnittestsRan(input_api, output_api, committing, "Release") |
| 261 |
| 262 return results |
| 263 |
| 264 |
| 265 def CheckChangeOnUpload(input_api, output_api): |
| 266 return CheckChange(input_api, output_api, False) |
| 267 |
| 268 |
| 269 def CheckChangeOnCommit(input_api, output_api): |
| 270 return CheckChange(input_api, output_api, True) |
OLD | NEW |