blob: 67ac4459c57cdb320665ad5a7516e3414ffa5f04 [file] [log] [blame]
estade@chromium.orgae7af922012-01-27 14:51:13 +00001# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Generic presubmit checks that can be reused by other presubmit checks."""
6
maruel@chromium.orgff9a2172012-04-24 16:55:32 +00007import os as _os
8_HERE = _os.path.dirname(_os.path.abspath(__file__))
9
tfarina@chromium.orgb6795642014-12-12 00:03:49 +000010# Justifications for each filter:
11#
12# - build/include : Too many; fix in the future.
13# - build/include_order : Not happening; #ifdefed includes.
14# - build/namespace : I'm surprised by how often we violate this rule.
15# - readability/casting : Mistakes a whole bunch of function pointer.
16# - runtime/int : Can be fixed long term; volume of errors too high
17# - runtime/virtual : Broken now, but can be fixed in the future?
18# - whitespace/braces : We have a lot of explicit scoping in chrome code.
tfarina@chromium.orgb6795642014-12-12 00:03:49 +000019DEFAULT_LINT_FILTERS = [
20 '-build/include',
21 '-build/include_order',
22 '-build/namespace',
23 '-readability/casting',
24 '-runtime/int',
25 '-runtime/virtual',
26 '-whitespace/braces',
tfarina@chromium.orgb6795642014-12-12 00:03:49 +000027]
bradnelson@google.com56e48bc2011-03-24 20:51:21 +000028
danakj@chromium.org0ae71222016-01-11 19:37:11 +000029# These filters will always be removed, even if the caller specifies a filter
30# set, as they are problematic or broken in some way.
31#
32# Justifications for each filter:
33# - build/c++11 : Rvalue ref checks are unreliable (false positives),
34# include file and feature blacklists are
35# google3-specific.
36BLACKLIST_LINT_FILTERS = [
37 '-build/c++11',
38]
39
maruel@chromium.org3410d912009-06-09 20:56:16 +000040### Description checks
41
kuchhal@chromium.org00c41e42009-05-12 21:43:13 +000042def CheckChangeHasBugField(input_api, output_api):
Aaron Gablefc03e672017-05-15 14:09:42 -070043 """Requires that the changelist have a Bug: field."""
44 if input_api.change.BugsFromDescription():
kuchhal@chromium.org00c41e42009-05-12 21:43:13 +000045 return []
46 else:
47 return [output_api.PresubmitNotifyResult(
Aaron Gablefc03e672017-05-15 14:09:42 -070048 'If this change has an associated bug, add Bug: [bug number].')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000049
50
51def CheckDoNotSubmitInDescription(input_api, output_api):
ilevy@chromium.orgc50d7612012-12-05 04:25:14 +000052 """Checks that the user didn't add 'DO NOT ''SUBMIT' to the CL description.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000053 """
ilevy@chromium.orgc50d7612012-12-05 04:25:14 +000054 keyword = 'DO NOT ''SUBMIT'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000055 if keyword in input_api.change.DescriptionText():
56 return [output_api.PresubmitError(
maruel@chromium.org3fbcb082010-03-19 14:03:28 +000057 keyword + ' is present in the changelist description.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000058 else:
59 return []
60
61
maruel@chromium.orgbc50eb42009-06-10 18:22:47 +000062def CheckChangeHasDescription(input_api, output_api):
63 """Checks the CL description is not empty."""
64 text = input_api.change.DescriptionText()
65 if text.strip() == '':
66 if input_api.is_committing:
pgervais@chromium.orgbc3b3b52014-06-03 00:53:48 +000067 return [output_api.PresubmitError('Add a description to the CL.')]
maruel@chromium.orgbc50eb42009-06-10 18:22:47 +000068 else:
pgervais@chromium.orgbc3b3b52014-06-03 00:53:48 +000069 return [output_api.PresubmitNotifyResult('Add a description to the CL.')]
maruel@chromium.orgbc50eb42009-06-10 18:22:47 +000070 return []
71
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +000072
73def CheckChangeWasUploaded(input_api, output_api):
74 """Checks that the issue was uploaded before committing."""
maruel@chromium.orgd587f392011-07-26 00:41:18 +000075 if input_api.is_committing and not input_api.change.issue:
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +000076 return [output_api.PresubmitError(
77 'Issue wasn\'t uploaded. Please upload first.')]
78 return []
79
80
maruel@chromium.org3410d912009-06-09 20:56:16 +000081### Content checks
82
Sergiy Byelozyorov621fe6f2018-05-25 17:38:46 -070083def CheckAuthorizedAuthor(input_api, output_api, bot_whitelist=None):
Michael Achenbachc850b962016-12-05 15:40:17 +010084 """For non-googler/chromites committers, verify the author's email address is
85 in AUTHORS.
86 """
Michael Achenbach590420d2017-10-18 12:35:37 +020087 if input_api.is_committing:
88 error_type = output_api.PresubmitError
89 else:
90 error_type = output_api.PresubmitPromptWarning
91
Michael Achenbachc850b962016-12-05 15:40:17 +010092 author = input_api.change.author_email
93 if not author:
94 input_api.logging.info('No author, skipping AUTHOR check')
95 return []
Sergiy Byelozyorov621fe6f2018-05-25 17:38:46 -070096
97 # This is used for CLs created by trusted robot accounts.
98 if bot_whitelist and author in bot_whitelist:
99 return []
100
Michael Achenbachc850b962016-12-05 15:40:17 +0100101 authors_path = input_api.os_path.join(
102 input_api.PresubmitLocalPath(), 'AUTHORS')
103 valid_authors = (
104 input_api.re.match(r'[^#]+\s+\<(.+?)\>\s*$', line)
105 for line in open(authors_path))
106 valid_authors = [item.group(1).lower() for item in valid_authors if item]
107 if not any(input_api.fnmatch.fnmatch(author.lower(), valid)
108 for valid in valid_authors):
109 input_api.logging.info('Valid authors are %s', ', '.join(valid_authors))
Michael Achenbach590420d2017-10-18 12:35:37 +0200110 return [error_type(
Michael Achenbachc850b962016-12-05 15:40:17 +0100111 ('%s is not in AUTHORS file. If you are a new contributor, please visit'
112 '\n'
Xiaoyin Liub5807972017-10-04 22:07:24 -0400113 'https://www.chromium.org/developers/contributing-code and read the '
Michael Achenbachc850b962016-12-05 15:40:17 +0100114 '"Legal" section\n'
115 'If you are a chromite, verify the contributor signed the CLA.') %
116 author)]
117 return []
118
119
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000120def CheckDoNotSubmitInFiles(input_api, output_api):
ilevy@chromium.orgc50d7612012-12-05 04:25:14 +0000121 """Checks that the user didn't add 'DO NOT ''SUBMIT' to any files."""
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000122 # We want to check every text file, not just source files.
123 file_filter = lambda x : x
ilevy@chromium.orgc50d7612012-12-05 04:25:14 +0000124 keyword = 'DO NOT ''SUBMIT'
bulach@chromium.orgbfffd452012-02-22 01:13:29 +0000125 errors = _FindNewViolationsOfRule(lambda _, line : keyword not in line,
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000126 input_api, file_filter)
127 text = '\n'.join('Found %s in %s' % (keyword, loc) for loc in errors)
128 if text:
129 return [output_api.PresubmitError(text)]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000130 return []
131
132
tfarina@chromium.orgb6795642014-12-12 00:03:49 +0000133def CheckChangeLintsClean(input_api, output_api, source_file_filter=None,
tfarina@chromium.orga62208f2015-02-25 03:23:11 +0000134 lint_filters=None, verbose_level=None):
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000135 """Checks that all '.cc' and '.h' files pass cpplint.py."""
erg@google.com26970fa2009-11-17 18:07:32 +0000136 _RE_IS_TEST = input_api.re.compile(r'.*tests?.(cc|h)$')
137 result = []
138
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000139 cpplint = input_api.cpplint
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000140 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800141 # pylint: disable=protected-access
erg@google.com26970fa2009-11-17 18:07:32 +0000142 cpplint._cpplint_state.ResetErrorCounts()
143
tfarina@chromium.orgb6795642014-12-12 00:03:49 +0000144 lint_filters = lint_filters or DEFAULT_LINT_FILTERS
danakj@chromium.org0ae71222016-01-11 19:37:11 +0000145 lint_filters.extend(BLACKLIST_LINT_FILTERS)
tfarina@chromium.orgb6795642014-12-12 00:03:49 +0000146 cpplint._SetFilters(','.join(lint_filters))
erg@google.com26970fa2009-11-17 18:07:32 +0000147
148 # We currently are more strict with normal code than unit tests; 4 and 5 are
149 # the verbosity level that would normally be passed to cpplint.py through
150 # --verbose=#. Hopefully, in the future, we can be more verbose.
151 files = [f.AbsoluteLocalPath() for f in
152 input_api.AffectedSourceFiles(source_file_filter)]
153 for file_name in files:
154 if _RE_IS_TEST.match(file_name):
155 level = 5
156 else:
157 level = 4
158
tfarina@chromium.orga62208f2015-02-25 03:23:11 +0000159 verbose_level = verbose_level or level
160 cpplint.ProcessFile(file_name, verbose_level)
erg@google.com26970fa2009-11-17 18:07:32 +0000161
162 if cpplint._cpplint_state.error_count > 0:
163 if input_api.is_committing:
164 res_type = output_api.PresubmitError
165 else:
166 res_type = output_api.PresubmitPromptWarning
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000167 result = [res_type('Changelist failed cpplint.py check.')]
erg@google.com26970fa2009-11-17 18:07:32 +0000168
169 return result
170
171
maruel@chromium.org3410d912009-06-09 20:56:16 +0000172def CheckChangeHasNoCR(input_api, output_api, source_file_filter=None):
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000173 """Checks no '\r' (CR) character is in any source files."""
174 cr_files = []
maruel@chromium.org3410d912009-06-09 20:56:16 +0000175 for f in input_api.AffectedSourceFiles(source_file_filter):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000176 if '\r' in input_api.ReadFile(f, 'rb'):
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000177 cr_files.append(f.LocalPath())
178 if cr_files:
179 return [output_api.PresubmitPromptWarning(
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000180 'Found a CR character in these files:', items=cr_files)]
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000181 return []
182
183
184def CheckChangeHasOnlyOneEol(input_api, output_api, source_file_filter=None):
185 """Checks the files ends with one and only one \n (LF)."""
186 eof_files = []
187 for f in input_api.AffectedSourceFiles(source_file_filter):
188 contents = input_api.ReadFile(f, 'rb')
189 # Check that the file ends in one and only one newline character.
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000190 if len(contents) > 1 and (contents[-1:] != '\n' or contents[-2:-1] == '\n'):
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000191 eof_files.append(f.LocalPath())
192
193 if eof_files:
194 return [output_api.PresubmitPromptWarning(
195 'These files should end in one (and only one) newline character:',
196 items=eof_files)]
197 return []
198
199
200def CheckChangeHasNoCrAndHasOnlyOneEol(input_api, output_api,
201 source_file_filter=None):
202 """Runs both CheckChangeHasNoCR and CheckChangeHasOnlyOneEOL in one pass.
203
204 It is faster because it is reading the file only once.
205 """
206 cr_files = []
207 eof_files = []
208 for f in input_api.AffectedSourceFiles(source_file_filter):
209 contents = input_api.ReadFile(f, 'rb')
210 if '\r' in contents:
211 cr_files.append(f.LocalPath())
212 # Check that the file ends in one and only one newline character.
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000213 if len(contents) > 1 and (contents[-1:] != '\n' or contents[-2:-1] == '\n'):
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000214 eof_files.append(f.LocalPath())
215 outputs = []
216 if cr_files:
217 outputs.append(output_api.PresubmitPromptWarning(
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000218 'Found a CR character in these files:', items=cr_files))
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000219 if eof_files:
220 outputs.append(output_api.PresubmitPromptWarning(
221 'These files should end in one (and only one) newline character:',
222 items=eof_files))
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000223 return outputs
224
seanmccullough0b670442016-06-07 10:45:58 -0700225def CheckGenderNeutral(input_api, output_api, source_file_filter=None):
226 """Checks that there are no gendered pronouns in any of the text files to be
227 submitted.
228 """
229 gendered_re = input_api.re.compile(
230 '(^|\s|\(|\[)([Hh]e|[Hh]is|[Hh]ers?|[Hh]im|[Ss]he|[Gg]uys?)\\b')
231
232 errors = []
233 for f in input_api.AffectedFiles(include_deletes=False,
234 file_filter=source_file_filter):
235 for line_num, line in f.ChangedContents():
236 if gendered_re.search(line):
237 errors.append('%s (%d): %s' % (f.LocalPath(), line_num, line))
238
239 if len(errors):
240 return [output_api.PresubmitPromptWarning('Found a gendered pronoun in:',
241 long_text='\n'.join(errors))]
242 return []
243
244
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000245
chrisha@google.com267d6592012-06-19 19:23:31 +0000246def _ReportErrorFileAndLine(filename, line_num, dummy_line):
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000247 """Default error formatter for _FindNewViolationsOfRule."""
danakj@chromium.orgc5965ba2013-08-14 00:27:24 +0000248 return '%s:%s' % (filename, line_num)
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000249
250
251def _FindNewViolationsOfRule(callable_rule, input_api, source_file_filter=None,
252 error_formatter=_ReportErrorFileAndLine):
253 """Find all newly introduced violations of a per-line rule (a callable).
254
255 Arguments:
bulach@chromium.orgbfffd452012-02-22 01:13:29 +0000256 callable_rule: a callable taking a file extension and line of input and
257 returning True if the rule is satisfied and False if there was a problem.
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000258 input_api: object to enumerate the affected files.
259 source_file_filter: a filter to be passed to the input api.
260 error_formatter: a callable taking (filename, line_number, line) and
261 returning a formatted error string.
262
263 Returns:
264 A list of the newly-introduced violations reported by the rule.
265 """
266 errors = []
sail@chromium.org5538e022011-05-12 17:53:16 +0000267 for f in input_api.AffectedFiles(include_deletes=False,
268 file_filter=source_file_filter):
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000269 # For speed, we do two passes, checking first the full file. Shelling out
270 # to the SCM to determine the changed region can be quite expensive on
271 # Win32. Assuming that most files will be kept problem-free, we can
272 # skip the SCM operations most of the time.
bulach@chromium.orgbfffd452012-02-22 01:13:29 +0000273 extension = str(f.LocalPath()).rsplit('.', 1)[-1]
274 if all(callable_rule(extension, line) for line in f.NewContents()):
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000275 continue # No violation found in full text: can skip considering diff.
276
277 for line_num, line in f.ChangedContents():
bulach@chromium.orgbfffd452012-02-22 01:13:29 +0000278 if not callable_rule(extension, line):
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000279 errors.append(error_formatter(f.LocalPath(), line_num, line))
280
281 return errors
282
283
maruel@chromium.org3410d912009-06-09 20:56:16 +0000284def CheckChangeHasNoTabs(input_api, output_api, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000285 """Checks that there are no tab characters in any of the text files to be
286 submitted.
287 """
maruel@chromium.org115ae6c2010-06-18 17:11:43 +0000288 # In addition to the filter, make sure that makefiles are blacklisted.
289 if not source_file_filter:
290 # It's the default filter.
291 source_file_filter = input_api.FilterSourceFile
292 def filter_more(affected_file):
cmp@chromium.orgcb5e57c2012-04-06 19:50:15 +0000293 basename = input_api.os_path.basename(affected_file.LocalPath())
294 return (not (basename in ('Makefile', 'makefile') or
295 basename.endswith('.mk')) and
maruel@chromium.org115ae6c2010-06-18 17:11:43 +0000296 source_file_filter(affected_file))
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000297
bulach@chromium.orgbfffd452012-02-22 01:13:29 +0000298 tabs = _FindNewViolationsOfRule(lambda _, line : '\t' not in line,
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000299 input_api, filter_more)
300
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000301 if tabs:
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000302 return [output_api.PresubmitPromptWarning('Found a tab character in:',
303 long_text='\n'.join(tabs))]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000304 return []
305
306
estade@chromium.orgfdcc9f72011-02-07 22:25:07 +0000307def CheckChangeTodoHasOwner(input_api, output_api, source_file_filter=None):
308 """Checks that the user didn't add TODO(name) without an owner."""
309
ilevy@chromium.orgc50d7612012-12-05 04:25:14 +0000310 unowned_todo = input_api.re.compile('TO''DO[^(]')
bulach@chromium.orgbfffd452012-02-22 01:13:29 +0000311 errors = _FindNewViolationsOfRule(lambda _, x : not unowned_todo.search(x),
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000312 input_api, source_file_filter)
ilevy@chromium.orgc50d7612012-12-05 04:25:14 +0000313 errors = ['Found TO''DO with no owner in ' + x for x in errors]
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000314 if errors:
315 return [output_api.PresubmitPromptWarning('\n'.join(errors))]
estade@chromium.orgfdcc9f72011-02-07 22:25:07 +0000316 return []
317
318
maruel@chromium.orgf5888bb2009-06-10 20:26:37 +0000319def CheckChangeHasNoStrayWhitespace(input_api, output_api,
320 source_file_filter=None):
321 """Checks that there is no stray whitespace at source lines end."""
bulach@chromium.orgbfffd452012-02-22 01:13:29 +0000322 errors = _FindNewViolationsOfRule(lambda _, line : line.rstrip() == line,
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000323 input_api, source_file_filter)
maruel@chromium.orgf5888bb2009-06-10 20:26:37 +0000324 if errors:
325 return [output_api.PresubmitPromptWarning(
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000326 'Found line ending with white spaces in:',
327 long_text='\n'.join(errors))]
maruel@chromium.orgf5888bb2009-06-10 20:26:37 +0000328 return []
329
330
jamesr@chromium.orgaf27f462013-04-04 21:44:22 +0000331def CheckLongLines(input_api, output_api, maxlen, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000332 """Checks that there aren't any lines longer than maxlen characters in any of
333 the text files to be submitted.
334 """
dpranke@chromium.orge3b1c3d2012-10-20 22:28:14 +0000335 maxlens = {
336 'java': 100,
torne@chromium.org0ecad9c2013-02-15 16:35:16 +0000337 # This is specifically for Android's handwritten makefiles (Android.mk).
338 'mk': 200,
bulach@chromium.orgbfffd452012-02-22 01:13:29 +0000339 '': maxlen,
340 }
maruel@chromium.orgeba64622011-06-20 18:26:48 +0000341
erikchen@google.com12816082013-12-03 02:04:20 +0000342 # Language specific exceptions to max line length.
343 # '.h' is considered an obj-c file extension, since OBJC_EXCEPTIONS are a
344 # superset of CPP_EXCEPTIONS.
345 CPP_FILE_EXTS = ('c', 'cc')
346 CPP_EXCEPTIONS = ('#define', '#endif', '#if', '#include', '#pragma')
Dave Schuyler6eea7c22017-03-03 18:46:25 -0800347 HTML_FILE_EXTS = ('html',)
Dave Schuyler9b9b94c2017-04-13 15:28:41 -0700348 HTML_EXCEPTIONS = ('<g ', '<link ', '<path ',)
erikchen@google.com12816082013-12-03 02:04:20 +0000349 JAVA_FILE_EXTS = ('java',)
350 JAVA_EXCEPTIONS = ('import ', 'package ')
dbeam@chromium.org5a2af6d2015-10-08 17:52:29 +0000351 JS_FILE_EXTS = ('js',)
352 JS_EXCEPTIONS = ("GEN('#include",)
erikchen@google.com12816082013-12-03 02:04:20 +0000353 OBJC_FILE_EXTS = ('h', 'm', 'mm')
354 OBJC_EXCEPTIONS = ('#define', '#endif', '#if', '#import', '#include',
355 '#pragma')
dbeam@chromium.org5a2af6d2015-10-08 17:52:29 +0000356 PY_FILE_EXTS = ('py',)
aiolos@chromium.org84033cc2015-07-16 01:27:15 +0000357 PY_EXCEPTIONS = ('import', 'from')
erikchen@google.com12816082013-12-03 02:04:20 +0000358
359 LANGUAGE_EXCEPTIONS = [
360 (CPP_FILE_EXTS, CPP_EXCEPTIONS),
Dave Schuyler6eea7c22017-03-03 18:46:25 -0800361 (HTML_FILE_EXTS, HTML_EXCEPTIONS),
erikchen@google.com12816082013-12-03 02:04:20 +0000362 (JAVA_FILE_EXTS, JAVA_EXCEPTIONS),
dbeam@chromium.org5a2af6d2015-10-08 17:52:29 +0000363 (JS_FILE_EXTS, JS_EXCEPTIONS),
erikchen@google.com12816082013-12-03 02:04:20 +0000364 (OBJC_FILE_EXTS, OBJC_EXCEPTIONS),
aiolos@chromium.org84033cc2015-07-16 01:27:15 +0000365 (PY_FILE_EXTS, PY_EXCEPTIONS),
erikchen@google.com12816082013-12-03 02:04:20 +0000366 ]
sivachandra@chromium.org270db5c2012-10-09 22:38:44 +0000367
bulach@chromium.orgbfffd452012-02-22 01:13:29 +0000368 def no_long_lines(file_extension, line):
erikchen@google.com12816082013-12-03 02:04:20 +0000369 # Check for language specific exceptions.
Dave Schuyler9b9b94c2017-04-13 15:28:41 -0700370 if any(file_extension in exts and line.lstrip().startswith(exceptions)
erikchen@google.com12816082013-12-03 02:04:20 +0000371 for exts, exceptions in LANGUAGE_EXCEPTIONS):
sivachandra@chromium.org270db5c2012-10-09 22:38:44 +0000372 return True
373
bulach@chromium.orgbfffd452012-02-22 01:13:29 +0000374 file_maxlen = maxlens.get(file_extension, maxlens[''])
375 # Stupidly long symbols that needs to be worked around if takes 66% of line.
376 long_symbol = file_maxlen * 2 / 3
377 # Hard line length limit at 50% more.
378 extra_maxlen = file_maxlen * 3 / 2
379
380 line_len = len(line)
381 if line_len <= file_maxlen:
maruel@chromium.orgeba64622011-06-20 18:26:48 +0000382 return True
383
dtu@chromium.org03f0c532015-03-27 22:22:07 +0000384 # Allow long URLs of any length.
treib@chromium.orgde3ee902014-07-28 14:23:11 +0000385 if any((url in line) for url in ('file://', 'http://', 'https://')):
386 return True
387
qyearsley12fa6ff2016-08-24 09:18:40 -0700388 # If 'line-too-long' is explicitly suppressed for the line, any length is
nednguyen@google.com6df90f32015-12-01 20:14:33 +0000389 # acceptable.
390 if 'pylint: disable=line-too-long' in line and file_extension == 'py':
391 return True
392
dtu@chromium.org03f0c532015-03-27 22:22:07 +0000393 if line_len > extra_maxlen:
394 return False
395
treib@chromium.orgde3ee902014-07-28 14:23:11 +0000396 if 'url(' in line and file_extension == 'css':
397 return True
398
dbeam@chromium.orgb5ccc9b2014-09-23 00:42:22 +0000399 if '<include' in line and file_extension in ('css', 'html', 'js'):
400 return True
401
treib@chromium.orgde3ee902014-07-28 14:23:11 +0000402 return input_api.re.match(
403 r'.*[A-Za-z][A-Za-z_0-9]{%d,}.*' % long_symbol, line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000404
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000405 def format_error(filename, line_num, line):
406 return '%s, line %s, %s chars' % (filename, line_num, len(line))
407
408 errors = _FindNewViolationsOfRule(no_long_lines, input_api,
409 source_file_filter,
410 error_formatter=format_error)
411 if errors:
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000412 msg = 'Found lines longer than %s characters (first 5 shown).' % maxlen
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000413 return [output_api.PresubmitPromptWarning(msg, items=errors[:5])]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000414 else:
415 return []
416
417
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000418def CheckLicense(input_api, output_api, license_re, source_file_filter=None,
maruel@chromium.org71626852010-11-03 13:14:25 +0000419 accept_empty_files=True):
maruel@chromium.orgb9e7ada2010-01-27 23:12:39 +0000420 """Verifies the license header.
421 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000422 license_re = input_api.re.compile(license_re, input_api.re.MULTILINE)
maruel@chromium.orgb9e7ada2010-01-27 23:12:39 +0000423 bad_files = []
424 for f in input_api.AffectedSourceFiles(source_file_filter):
425 contents = input_api.ReadFile(f, 'rb')
maruel@chromium.org71626852010-11-03 13:14:25 +0000426 if accept_empty_files and not contents:
427 continue
maruel@chromium.orgb9e7ada2010-01-27 23:12:39 +0000428 if not license_re.search(contents):
429 bad_files.append(f.LocalPath())
430 if bad_files:
phajdan.jr@chromium.orge27eb7e2015-11-16 12:47:53 +0000431 return [output_api.PresubmitPromptWarning(
bradnelson@google.com393460b2011-03-29 01:20:26 +0000432 'License must match:\n%s\n' % license_re.pattern +
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000433 'Found a bad license header in these files:', items=bad_files)]
maruel@chromium.orgb9e7ada2010-01-27 23:12:39 +0000434 return []
435
436
maruel@chromium.org3410d912009-06-09 20:56:16 +0000437### Other checks
438
439def CheckDoNotSubmit(input_api, output_api):
440 return (
441 CheckDoNotSubmitInDescription(input_api, output_api) +
442 CheckDoNotSubmitInFiles(input_api, output_api)
443 )
444
445
bradnelson@google.comc0b332a2010-08-26 00:30:37 +0000446def CheckTreeIsOpen(input_api, output_api,
447 url=None, closed=None, json_url=None):
448 """Check whether to allow commit without prompt.
449
450 Supports two styles:
451 1. Checks that an url's content doesn't match a regexp that would mean that
452 the tree is closed. (old)
453 2. Check the json_url to decide whether to allow commit without prompt.
454 Args:
455 input_api: input related apis.
456 output_api: output related apis.
457 url: url to use for regex based tree status.
458 closed: regex to match for closed status.
459 json_url: url to download json style status.
460 """
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000461 if not input_api.is_committing:
462 return []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000463 try:
bradnelson@google.comc0b332a2010-08-26 00:30:37 +0000464 if json_url:
465 connection = input_api.urllib2.urlopen(json_url)
466 status = input_api.json.loads(connection.read())
467 connection.close()
468 if not status['can_commit_freely']:
469 short_text = 'Tree state is: ' + status['general_state']
470 long_text = status['message'] + '\n' + json_url
471 return [output_api.PresubmitError(short_text, long_text=long_text)]
472 else:
473 # TODO(bradnelson): drop this once all users are gone.
474 connection = input_api.urllib2.urlopen(url)
475 status = connection.read()
476 connection.close()
477 if input_api.re.match(closed, status):
478 long_text = status + '\n' + url
479 return [output_api.PresubmitError('The tree is closed.',
480 long_text=long_text)]
rohitrao@chromium.orgd490ee82012-02-06 19:31:33 +0000481 except IOError as e:
482 return [output_api.PresubmitError('Error fetching tree status.',
483 long_text=str(e))]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000484 return []
maruel@chromium.org7b305e82009-05-19 18:24:20 +0000485
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000486def GetUnitTestsInDirectory(
smut@google.comac296202014-04-24 21:47:17 +0000487 input_api, output_api, directory, whitelist=None, blacklist=None, env=None):
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000488 """Lists all files in a directory and runs them. Doesn't recurse.
489
nick@chromium.orgff526192013-06-10 19:30:26 +0000490 It's mainly a wrapper for RunUnitTests. Use whitelist and blacklist to filter
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000491 tests accordingly.
492 """
493 unit_tests = []
494 test_path = input_api.os_path.abspath(
495 input_api.os_path.join(input_api.PresubmitLocalPath(), directory))
496
497 def check(filename, filters):
498 return any(True for i in filters if input_api.re.match(i, filename))
499
maruel@chromium.orgfae707b2011-09-15 18:57:58 +0000500 to_run = found = 0
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000501 for filename in input_api.os_listdir(test_path):
maruel@chromium.orgfae707b2011-09-15 18:57:58 +0000502 found += 1
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000503 fullpath = input_api.os_path.join(test_path, filename)
504 if not input_api.os_path.isfile(fullpath):
505 continue
506 if whitelist and not check(filename, whitelist):
507 continue
508 if blacklist and check(filename, blacklist):
509 continue
510 unit_tests.append(input_api.os_path.join(directory, filename))
maruel@chromium.orgfae707b2011-09-15 18:57:58 +0000511 to_run += 1
anatoly techtonikbdcdc592017-03-29 17:00:13 +0300512 input_api.logging.debug('Found %d files, running %d unit tests'
513 % (found, to_run))
maruel@chromium.orgfae707b2011-09-15 18:57:58 +0000514 if not to_run:
515 return [
516 output_api.PresubmitPromptWarning(
517 'Out of %d files, found none that matched w=%r, b=%r in directory %s'
518 % (found, whitelist, blacklist, directory))
519 ]
smut@google.comac296202014-04-24 21:47:17 +0000520 return GetUnitTests(input_api, output_api, unit_tests, env)
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000521
522
smut@google.comac296202014-04-24 21:47:17 +0000523def GetUnitTests(input_api, output_api, unit_tests, env=None):
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000524 """Runs all unit tests in a directory.
525
526 On Windows, sys.executable is used for unit tests ending with ".py".
527 """
528 # We don't want to hinder users from uploading incomplete patches.
529 if input_api.is_committing:
530 message_type = output_api.PresubmitError
531 else:
532 message_type = output_api.PresubmitPromptWarning
533
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000534 results = []
535 for unit_test in unit_tests:
Robert Iannucci50258932018-03-19 10:30:59 -0700536 cmd = [unit_test]
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000537 if input_api.verbose:
maruel@chromium.org6c7723e2011-04-12 19:04:55 +0000538 cmd.append('--verbose')
smut@google.comac296202014-04-24 21:47:17 +0000539 kwargs = {'cwd': input_api.PresubmitLocalPath()}
540 if env:
541 kwargs['env'] = env
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000542 results.append(input_api.Command(
543 name=unit_test,
544 cmd=cmd,
smut@google.comac296202014-04-24 21:47:17 +0000545 kwargs=kwargs,
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000546 message=message_type))
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000547 return results
548
nick@chromium.orgff526192013-06-10 19:30:26 +0000549
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000550def GetUnitTestsRecursively(input_api, output_api, directory,
551 whitelist, blacklist):
552 """Gets all files in the directory tree (git repo) that match the whitelist.
553
554 Restricts itself to only find files within the Change's source repo, not
555 dependencies.
556 """
557 def check(filename):
558 return (any(input_api.re.match(f, filename) for f in whitelist) and
559 not any(input_api.re.match(f, filename) for f in blacklist))
560
561 tests = []
562
563 to_run = found = 0
564 for filepath in input_api.change.AllFiles(directory):
565 found += 1
566 if check(filepath):
567 to_run += 1
568 tests.append(filepath)
569 input_api.logging.debug('Found %d files, running %d' % (found, to_run))
570 if not to_run:
571 return [
572 output_api.PresubmitPromptWarning(
573 'Out of %d files, found none that matched w=%r, b=%r in directory %s'
574 % (found, whitelist, blacklist, directory))
575 ]
576
577 return GetUnitTests(input_api, output_api, tests)
578
579
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000580def GetPythonUnitTests(input_api, output_api, unit_tests):
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000581 """Run the unit tests out of process, capture the output and use the result
582 code to determine success.
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000583
584 DEPRECATED.
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000585 """
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000586 # We don't want to hinder users from uploading incomplete patches.
587 if input_api.is_committing:
588 message_type = output_api.PresubmitError
589 else:
590 message_type = output_api.PresubmitNotifyResult
maruel@chromium.org0e766052011-04-06 13:32:51 +0000591 results = []
maruel@chromium.org7b305e82009-05-19 18:24:20 +0000592 for unit_test in unit_tests:
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000593 # Run the unit tests out of process. This is because some unit tests
594 # stub out base libraries and don't clean up their mess. It's too easy to
595 # get subtle bugs.
596 cwd = None
597 env = None
598 unit_test_name = unit_test
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000599 # 'python -m test.unit_test' doesn't work. We need to change to the right
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000600 # directory instead.
601 if '.' in unit_test:
602 # Tests imported in submodules (subdirectories) assume that the current
603 # directory is in the PYTHONPATH. Manually fix that.
604 unit_test = unit_test.replace('.', '/')
605 cwd = input_api.os_path.dirname(unit_test)
606 unit_test = input_api.os_path.basename(unit_test)
607 env = input_api.environ.copy()
kbr@google.comab318592009-09-04 00:54:55 +0000608 # At least on Windows, it seems '.' must explicitly be in PYTHONPATH
609 backpath = [
610 '.', input_api.os_path.pathsep.join(['..'] * (cwd.count('/') + 1))
611 ]
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000612 if env.get('PYTHONPATH'):
613 backpath.append(env.get('PYTHONPATH'))
ukai@chromium.orga301f1f2009-08-05 10:37:33 +0000614 env['PYTHONPATH'] = input_api.os_path.pathsep.join((backpath))
Robert Iannucci23bd7352018-03-23 11:51:40 -0700615 env.pop('VPYTHON_CLEAR_PYTHONPATH', None)
maruel@chromium.org0e766052011-04-06 13:32:51 +0000616 cmd = [input_api.python_executable, '-m', '%s' % unit_test]
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000617 results.append(input_api.Command(
618 name=unit_test_name,
619 cmd=cmd,
620 kwargs={'env': env, 'cwd': cwd},
621 message=message_type))
maruel@chromium.org0e766052011-04-06 13:32:51 +0000622 return results
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000623
624
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000625def RunUnitTestsInDirectory(input_api, *args, **kwargs):
626 """Run tests in a directory serially.
627
628 For better performance, use GetUnitTestsInDirectory and then
629 pass to input_api.RunTests.
630 """
631 return input_api.RunTests(
632 GetUnitTestsInDirectory(input_api, *args, **kwargs), False)
633
634
635def RunUnitTests(input_api, *args, **kwargs):
636 """Run tests serially.
637
638 For better performance, use GetUnitTests and then pass to
639 input_api.RunTests.
640 """
641 return input_api.RunTests(GetUnitTests(input_api, *args, **kwargs), False)
642
643
644def RunPythonUnitTests(input_api, *args, **kwargs):
645 """Run python tests in a directory serially.
646
647 DEPRECATED
648 """
649 return input_api.RunTests(
650 GetPythonUnitTests(input_api, *args, **kwargs), False)
651
652
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000653def _FetchAllFiles(input_api, white_list, black_list):
654 """Hack to fetch all files."""
655 # We cannot use AffectedFiles here because we want to test every python
656 # file on each single python change. It's because a change in a python file
657 # can break another unmodified file.
658 # Use code similar to InputApi.FilterSourceFile()
659 def Find(filepath, filters):
anatoly techtonikbdcdc592017-03-29 17:00:13 +0300660 if input_api.platform == 'win32':
661 filepath = filepath.replace('\\', '/')
662
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000663 for item in filters:
664 if input_api.re.match(item, filepath):
665 return True
666 return False
667
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000668 files = []
669 path_len = len(input_api.PresubmitLocalPath())
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000670 for dirpath, dirnames, filenames in input_api.os_walk(
671 input_api.PresubmitLocalPath()):
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000672 # Passes dirnames in black list to speed up search.
673 for item in dirnames[:]:
674 filepath = input_api.os_path.join(dirpath, item)[path_len + 1:]
675 if Find(filepath, black_list):
676 dirnames.remove(item)
677 for item in filenames:
678 filepath = input_api.os_path.join(dirpath, item)[path_len + 1:]
679 if Find(filepath, white_list) and not Find(filepath, black_list):
680 files.append(filepath)
681 return files
682
683
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000684def GetPylint(input_api, output_api, white_list=None, black_list=None,
dtu@chromium.orgf23524d2016-01-27 20:08:49 +0000685 disabled_warnings=None, extra_paths_list=None, pylintrc=None):
maruel@chromium.orgbf38a7e2010-12-14 18:15:54 +0000686 """Run pylint on python files.
687
chrisha@google.com267d6592012-06-19 19:23:31 +0000688 The default white_list enforces looking only at *.py files.
maruel@chromium.orgbf38a7e2010-12-14 18:15:54 +0000689 """
maruel@chromium.org69eaecb2011-06-14 13:09:13 +0000690 white_list = tuple(white_list or ('.*\.py$',))
691 black_list = tuple(black_list or input_api.DEFAULT_BLACK_LIST)
ilevy@chromium.org7b677f72013-01-07 18:49:26 +0000692 extra_paths_list = extra_paths_list or []
693
maruel@chromium.orgade9c592011-04-07 15:59:11 +0000694 if input_api.is_committing:
695 error_type = output_api.PresubmitError
696 else:
697 error_type = output_api.PresubmitPromptWarning
maruel@chromium.orgbf38a7e2010-12-14 18:15:54 +0000698
699 # Only trigger if there is at least one python file affected.
ilevy@chromium.org36576332013-01-08 03:16:15 +0000700 def rel_path(regex):
701 """Modifies a regex for a subject to accept paths relative to root."""
chrisha@chromium.org40e5d3d2013-01-18 17:46:57 +0000702 def samefile(a, b):
703 # Default implementation for platforms lacking os.path.samefile
704 # (like Windows).
705 return input_api.os_path.abspath(a) == input_api.os_path.abspath(b)
706 samefile = getattr(input_api.os_path, 'samefile', samefile)
707 if samefile(input_api.PresubmitLocalPath(),
708 input_api.change.RepositoryRoot()):
ilevy@chromium.orgdd4e9312013-01-15 03:22:04 +0000709 return regex
chrisha@chromium.org40e5d3d2013-01-18 17:46:57 +0000710
ilevy@chromium.org36576332013-01-08 03:16:15 +0000711 prefix = input_api.os_path.join(input_api.os_path.relpath(
712 input_api.PresubmitLocalPath(), input_api.change.RepositoryRoot()), '')
713 return input_api.re.escape(prefix) + regex
ilevy@chromium.org7b677f72013-01-07 18:49:26 +0000714 src_filter = lambda x: input_api.FilterSourceFile(
715 x, map(rel_path, white_list), map(rel_path, black_list))
maruel@chromium.orgbf38a7e2010-12-14 18:15:54 +0000716 if not input_api.AffectedSourceFiles(src_filter):
ilevy@chromium.org7b677f72013-01-07 18:49:26 +0000717 input_api.logging.info('Skipping pylint: no matching changes.')
maruel@chromium.orgbf38a7e2010-12-14 18:15:54 +0000718 return []
719
agable@chromium.org327d72b2015-04-21 20:22:50 +0000720 if pylintrc is not None:
721 pylintrc = input_api.os_path.join(input_api.PresubmitLocalPath(), pylintrc)
722 else:
723 pylintrc = input_api.os_path.join(_HERE, 'pylintrc')
dtu@chromium.orgf23524d2016-01-27 20:08:49 +0000724 extra_args = ['--rcfile=%s' % pylintrc]
maruel@chromium.orgff9a2172012-04-24 16:55:32 +0000725 if disabled_warnings:
726 extra_args.extend(['-d', ','.join(disabled_warnings)])
727
chrisha@google.com267d6592012-06-19 19:23:31 +0000728 files = _FetchAllFiles(input_api, white_list, black_list)
729 if not files:
maruel@chromium.orgfca53392010-12-21 18:42:57 +0000730 return []
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000731 files.sort()
chrisha@google.com267d6592012-06-19 19:23:31 +0000732
csharp@chromium.org40395342013-02-21 14:57:23 +0000733 input_api.logging.info('Running pylint on %d files', len(files))
734 input_api.logging.debug('Running pylint on: %s', files)
chrisha@google.com267d6592012-06-19 19:23:31 +0000735 env = input_api.environ.copy()
ilevy@chromium.org7b677f72013-01-07 18:49:26 +0000736 env['PYTHONPATH'] = input_api.os_path.pathsep.join(
Robert Iannucci82a64802018-03-23 16:40:16 -0700737 extra_paths_list).encode('utf8')
Robert Iannucci23bd7352018-03-23 11:51:40 -0700738 env.pop('VPYTHON_CLEAR_PYTHONPATH', None)
Robert Iannucci82a64802018-03-23 16:40:16 -0700739 input_api.logging.debug(' with extra PYTHONPATH: %r', extra_paths_list)
chrisha@google.com267d6592012-06-19 19:23:31 +0000740
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000741 def GetPylintCmd(flist, extra, parallel):
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000742 # Windows needs help running python files so we explicitly specify
743 # the interpreter to use. It also has limitations on the size of
744 # the command-line, so we pass arguments via a pipe.
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000745 cmd = [input_api.python_executable,
746 input_api.os_path.join(_HERE, 'third_party', 'pylint.py'),
747 '--args-on-stdin']
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000748 if len(flist) == 1:
749 description = flist[0]
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000750 else:
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000751 description = '%s files' % len(flist)
chrisha@chromium.org1b129e52012-06-22 17:08:11 +0000752
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000753 args = extra_args[:]
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000754 if extra:
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000755 args.extend(extra)
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000756 description += ' using %s' % (extra,)
757 if parallel:
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000758 args.append('--jobs=%s' % input_api.cpu_count)
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000759 description += ' on %d cores' % input_api.cpu_count
760
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000761 return input_api.Command(
762 name='Pylint (%s)' % description,
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000763 cmd=cmd,
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000764 kwargs={'env': env, 'stdin': '\n'.join(args + flist)},
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000765 message=error_type)
chrisha@chromium.org1b129e52012-06-22 17:08:11 +0000766
sbc@chromium.org7fc75ca2012-09-21 20:14:21 +0000767 # Always run pylint and pass it all the py files at once.
768 # Passing py files one at time is slower and can produce
769 # different results. input_api.verbose used to be used
770 # to enable this behaviour but differing behaviour in
771 # verbose mode is not desirable.
ilevy@chromium.org7b677f72013-01-07 18:49:26 +0000772 # Leave this unreachable code in here so users can make
773 # a quick local edit to diagnose pylint issues more
774 # easily.
sbc@chromium.org7fc75ca2012-09-21 20:14:21 +0000775 if True:
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000776 # pylint's cycle detection doesn't work in parallel, so spawn a second,
777 # single-threaded job for just that check.
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000778
779 # Some PRESUBMITs explicitly mention cycle detection.
780 if not any('R0401' in a or 'cyclic-import' in a for a in extra_args):
781 return [
782 GetPylintCmd(files, ["--disable=cyclic-import"], True),
783 GetPylintCmd(files, ["--disable=all", "--enable=cyclic-import"], False)
784 ]
785 else:
786 return [ GetPylintCmd(files, [], True) ]
787
chrisha@google.com267d6592012-06-19 19:23:31 +0000788 else:
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000789 return map(lambda x: GetPylintCmd([x], [], 1), files)
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000790
791
792def RunPylint(input_api, *args, **kwargs):
793 """Legacy presubmit function.
794
795 For better performance, get all tests and then pass to
796 input_api.RunTests.
797 """
798 return input_api.RunTests(GetPylint(input_api, *args, **kwargs), False)
maruel@chromium.orge94aedc2010-12-13 21:11:30 +0000799
maruel@chromium.orgade9c592011-04-07 15:59:11 +0000800
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000801def CheckBuildbotPendingBuilds(input_api, output_api, url, max_pendings,
802 ignored):
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000803 try:
804 connection = input_api.urllib2.urlopen(url)
805 raw_data = connection.read()
806 connection.close()
807 except IOError:
808 return [output_api.PresubmitNotifyResult('%s is not accessible' % url)]
809
810 try:
811 data = input_api.json.loads(raw_data)
812 except ValueError:
813 return [output_api.PresubmitNotifyResult('Received malformed json while '
814 'looking up buildbot status')]
815
816 out = []
817 for (builder_name, builder) in data.iteritems():
818 if builder_name in ignored:
819 continue
maruel@chromium.orgcf1982c2010-10-04 15:08:28 +0000820 if builder.get('state', '') == 'offline':
821 continue
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000822 pending_builds_len = len(builder.get('pending_builds', []))
823 if pending_builds_len > max_pendings:
824 out.append('%s has %d build(s) pending' %
825 (builder_name, pending_builds_len))
826 if out:
827 return [output_api.PresubmitPromptWarning(
828 'Build(s) pending. It is suggested to wait that no more than %d '
829 'builds are pending.' % max_pendings,
830 long_text='\n'.join(out))]
831 return []
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000832
833
Edward Lesmes067ef5d2018-04-19 17:54:45 -0400834def CheckOwnersFormat(input_api, output_api):
835 affected_files = set([
836 f.LocalPath()
837 for f in input_api.change.AffectedFiles()
838 if 'OWNERS' in f.LocalPath() and f.Action() != 'D'
839 ])
840 if not affected_files:
841 return []
842 try:
843 input_api.owners_db.load_data_needed_for(affected_files)
844 return []
845 except Exception as e:
846 return [output_api.PresubmitError(
847 'Error parsing OWNERS files:\n%s' % e)]
848
849
dpranke@chromium.orgbce925d2015-01-16 22:38:38 +0000850def CheckOwners(input_api, output_api, source_file_filter=None):
Aaron Gable8678d322018-04-02 13:28:19 -0700851 affected_files = set([f.LocalPath() for f in
852 input_api.change.AffectedFiles(file_filter=source_file_filter)])
Jeremy Roman5ae86d22018-04-26 15:36:02 -0400853 affects_owners = any('OWNERS' in name for name in affected_files)
Aaron Gable8678d322018-04-02 13:28:19 -0700854
pam@chromium.orgf46aed92012-03-08 09:18:17 +0000855 if input_api.is_committing:
Jeremy Roman5ae86d22018-04-26 15:36:02 -0400856 if input_api.tbr and not affects_owners:
pam@chromium.orgf46aed92012-03-08 09:18:17 +0000857 return [output_api.PresubmitNotifyResult(
858 '--tbr was specified, skipping OWNERS check')]
Clemens Hammacherd0c226a2017-01-16 14:09:52 +0100859 needed = 'LGTM from an OWNER'
860 output_fn = output_api.PresubmitError
rmistry@google.com5fc6c8c2015-04-16 16:38:43 +0000861 if input_api.change.issue:
tandrii@chromium.org9dea2ac2016-04-28 06:26:20 +0000862 if input_api.dry_run:
Clemens Hammacherd0c226a2017-01-16 14:09:52 +0100863 output_fn = lambda text: output_api.PresubmitNotifyResult(
864 'This is a dry run, but these failures would be reported on ' +
865 'commit:\n' + text)
rmistry@google.com5fc6c8c2015-04-16 16:38:43 +0000866 else:
Andrii Shyshkalov63560ad2018-02-09 19:09:54 -0800867 return [output_api.PresubmitError(
868 'OWNERS check failed: this CL has no Gerrit change number, '
869 'so we can\'t check it for approvals.')]
pam@chromium.orgf46aed92012-03-08 09:18:17 +0000870 else:
871 needed = 'OWNER reviewers'
Clemens Hammacherd0c226a2017-01-16 14:09:52 +0100872 output_fn = output_api.PresubmitNotifyResult
dpranke@chromium.org3e331bd2011-03-24 23:13:04 +0000873
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000874 owners_db = input_api.owners_db
Jochen Eisingerd0573ec2017-04-13 10:55:06 +0200875 owners_db.override_files = input_api.change.OriginalOwnersFiles()
tandrii@chromium.org830dc0b2016-05-09 06:26:34 +0000876 owner_email, reviewers = GetCodereviewOwnerAndReviewers(
pam@chromium.orgf46aed92012-03-08 09:18:17 +0000877 input_api,
878 owners_db.email_regexp,
879 approval_needed=input_api.is_committing)
880
dpranke@chromium.orgf6ddfa42013-03-05 21:06:03 +0000881 owner_email = owner_email or input_api.change.author_email
882
Edward Lemur707d70b2018-02-07 00:50:14 +0100883 finder = input_api.owners_finder(
884 affected_files, input_api.change.RepositoryRoot(),
885 owner_email, reviewers, fopen=file, os_path=input_api.os_path,
886 email_postfix='', disable_color=True,
887 override_files=input_api.change.OriginalOwnersFiles())
888 missing_files = finder.unreviewed_files
maruel@chromium.orge4067ab2011-06-03 01:07:35 +0000889
dpranke@chromium.org6b1e3ee2013-02-23 00:06:38 +0000890 if missing_files:
zork@chromium.org046e1752012-05-07 05:56:12 +0000891 output_list = [
Clemens Hammacherd0c226a2017-01-16 14:09:52 +0100892 output_fn('Missing %s for these files:\n %s' %
893 (needed, '\n '.join(sorted(missing_files))))]
Jeremy Roman5ae86d22018-04-26 15:36:02 -0400894 if input_api.tbr and affects_owners:
895 output_list.append(output_fn('Note that TBR does not apply to changes '
896 'that affect OWNERS files.'))
zork@chromium.org046e1752012-05-07 05:56:12 +0000897 if not input_api.is_committing:
dpranke@chromium.org31b42c02013-09-19 19:24:15 +0000898 suggested_owners = owners_db.reviewers_for(missing_files, owner_email)
Jochen Eisinger76f5fc62017-04-07 16:27:46 +0200899 owners_with_comments = []
900 def RecordComments(text):
901 owners_with_comments.append(finder.print_indent() + text)
902 finder.writeln = RecordComments
903 for owner in suggested_owners:
904 finder.print_comments(owner)
Clemens Hammacherd0c226a2017-01-16 14:09:52 +0100905 output_list.append(output_fn('Suggested OWNERS: ' +
scheib@chromium.org93276ab2013-10-14 23:55:32 +0000906 '(Use "git-cl owners" to interactively select owners.)\n %s' %
Jochen Eisinger76f5fc62017-04-07 16:27:46 +0200907 ('\n '.join(owners_with_comments))))
zork@chromium.org046e1752012-05-07 05:56:12 +0000908 return output_list
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000909
pam@chromium.orgf46aed92012-03-08 09:18:17 +0000910 if input_api.is_committing and not reviewers:
Clemens Hammacherd0c226a2017-01-16 14:09:52 +0100911 return [output_fn('Missing LGTM from someone other than %s' % owner_email)]
dpranke@chromium.org3e331bd2011-03-24 23:13:04 +0000912 return []
dpranke@chromium.org627ea672011-03-11 23:29:03 +0000913
Aaron Gable668c1d82018-04-03 10:19:16 -0700914
tandrii@chromium.org830dc0b2016-05-09 06:26:34 +0000915def GetCodereviewOwnerAndReviewers(input_api, email_regexp, approval_needed):
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000916 """Return the owner and reviewers of a change, if any.
917
918 If approval_needed is True, only reviewers who have approved the change
919 will be returned.
920 """
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000921 issue = input_api.change.issue
922 if not issue:
tandrii@chromium.org830dc0b2016-05-09 06:26:34 +0000923 return None, (set() if approval_needed else
924 _ReviewersFromChange(input_api.change))
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000925
926 owner_email = input_api.gerrit.GetChangeOwner(issue)
927 reviewers = set(
928 r for r in input_api.gerrit.GetChangeReviewers(issue, approval_needed)
929 if _match_reviewer_email(r, owner_email, email_regexp))
tandrii81665dc2016-08-29 09:16:19 -0700930 input_api.logging.debug('owner: %s; approvals given by: %s',
931 owner_email, ', '.join(sorted(reviewers)))
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000932 return owner_email, reviewers
933
934
Aaron Gable668c1d82018-04-03 10:19:16 -0700935def _ReviewersFromChange(change):
936 """Return the reviewers specified in the |change|, if any."""
937 reviewers = set()
938 reviewers.update(change.ReviewersFromDescription())
939 reviewers.update(change.TBRsFromDescription())
940
941 # Drop reviewers that aren't specified in email address format.
942 return set(reviewer for reviewer in reviewers if '@' in reviewer)
943
944
945def _match_reviewer_email(r, owner_email, email_regexp):
946 return email_regexp.match(r) and r != owner_email
947
948
bauerb@chromium.org4cea01e2012-03-20 19:49:05 +0000949def CheckSingletonInHeaders(input_api, output_api, source_file_filter=None):
glider@chromium.orgc3617f32015-02-19 16:33:33 +0000950 """Deprecated, must be removed."""
951 return [
952 output_api.PresubmitNotifyResult(
953 'CheckSingletonInHeaders is deprecated, please remove it.')
954 ]
bradnelson@google.com56e48bc2011-03-24 20:51:21 +0000955
956
957def PanProjectChecks(input_api, output_api,
958 excluded_paths=None, text_files=None,
bradnelson@google.comd57771d2011-03-31 19:18:32 +0000959 license_header=None, project_name=None,
jamesr@chromium.orgaf27f462013-04-04 21:44:22 +0000960 owners_check=True, maxlen=80):
bradnelson@google.com56e48bc2011-03-24 20:51:21 +0000961 """Checks that ALL chromium orbit projects should use.
962
963 These are checks to be run on all Chromium orbit project, including:
964 Chromium
965 Native Client
966 V8
967 When you update this function, please take this broad scope into account.
968 Args:
969 input_api: Bag of input related interfaces.
970 output_api: Bag of output related interfaces.
971 excluded_paths: Don't include these paths in common checks.
972 text_files: Which file are to be treated as documentation text files.
973 license_header: What license header should be on files.
974 project_name: What is the name of the project as it appears in the license.
975 Returns:
976 A list of warning or error objects.
977 """
maruel@chromium.org69eaecb2011-06-14 13:09:13 +0000978 excluded_paths = tuple(excluded_paths or [])
979 text_files = tuple(text_files or (
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000980 r'.+\.txt$',
981 r'.+\.json$',
maruel@chromium.org69eaecb2011-06-14 13:09:13 +0000982 ))
bradnelson@google.com56e48bc2011-03-24 20:51:21 +0000983 project_name = project_name or 'Chromium'
mark@chromium.org2cc66422012-08-07 21:22:32 +0000984
985 # Accept any year number from 2006 to the current year, or the special
rvargas@chromium.org02652602014-03-14 19:43:20 +0000986 # 2006-20xx string used on the oldest files. 2006-20xx is deprecated, but
987 # tolerated on old files.
mark@chromium.org2cc66422012-08-07 21:22:32 +0000988 current_year = int(input_api.time.strftime('%Y'))
989 allowed_years = (str(s) for s in reversed(xrange(2006, current_year + 1)))
rvargas@chromium.org02652602014-03-14 19:43:20 +0000990 years_re = '(' + '|'.join(allowed_years) + '|2006-2008|2006-2009|2006-2010)'
mark@chromium.org2cc66422012-08-07 21:22:32 +0000991
992 # The (c) is deprecated, but tolerate it until it's removed from all files.
bradnelson@google.com56e48bc2011-03-24 20:51:21 +0000993 license_header = license_header or (
mark@chromium.org2cc66422012-08-07 21:22:32 +0000994 r'.*? Copyright (\(c\) )?%(year)s The %(project)s Authors\. '
bradnelson@google.com56e48bc2011-03-24 20:51:21 +0000995 r'All rights reserved\.\n'
996 r'.*? Use of this source code is governed by a BSD-style license that '
997 r'can be\n'
dbeam@chromium.orgb2312102012-02-15 02:01:55 +0000998 r'.*? found in the LICENSE file\.(?: \*/)?\n'
bradnelson@google.com56e48bc2011-03-24 20:51:21 +0000999 ) % {
mark@chromium.org2cc66422012-08-07 21:22:32 +00001000 'year': years_re,
bradnelson@google.com56e48bc2011-03-24 20:51:21 +00001001 'project': project_name,
1002 }
1003
1004 results = []
1005 # This code loads the default black list (e.g. third_party, experimental, etc)
1006 # and add our black list (breakpad, skia and v8 are still not following
1007 # google style and are not really living this repository).
1008 # See presubmit_support.py InputApi.FilterSourceFile for the (simple) usage.
1009 black_list = input_api.DEFAULT_BLACK_LIST + excluded_paths
1010 white_list = input_api.DEFAULT_WHITE_LIST + text_files
1011 sources = lambda x: input_api.FilterSourceFile(x, black_list=black_list)
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +00001012 text_files = lambda x: input_api.FilterSourceFile(
1013 x, black_list=black_list, white_list=white_list)
nick@chromium.orge06cb4e2011-04-23 01:20:38 +00001014
1015 snapshot_memory = []
1016 def snapshot(msg):
1017 """Measures & prints performance warning if a rule is running slow."""
1018 dt2 = input_api.time.clock()
1019 if snapshot_memory:
1020 delta_ms = int(1000*(dt2 - snapshot_memory[0]))
1021 if delta_ms > 500:
1022 print " %s took a long time: %dms" % (snapshot_memory[1], delta_ms)
1023 snapshot_memory[:] = (dt2, msg)
1024
Edward Lesmescb62e482018-04-19 18:29:35 -04001025 snapshot("checking owners files format")
1026 results.extend(input_api.canned_checks.CheckOwnersFormat(
1027 input_api, output_api))
1028
bradnelson@google.comd57771d2011-03-31 19:18:32 +00001029 if owners_check:
nick@chromium.orge06cb4e2011-04-23 01:20:38 +00001030 snapshot("checking owners")
bradnelson@google.comd57771d2011-03-31 19:18:32 +00001031 results.extend(input_api.canned_checks.CheckOwners(
Dirk Pranke3c86cee2017-01-23 22:02:06 -08001032 input_api, output_api, source_file_filter=None))
bradnelson@google.com56e48bc2011-03-24 20:51:21 +00001033
nick@chromium.orge06cb4e2011-04-23 01:20:38 +00001034 snapshot("checking long lines")
bradnelson@google.com56e48bc2011-03-24 20:51:21 +00001035 results.extend(input_api.canned_checks.CheckLongLines(
jamesr@chromium.orgaf27f462013-04-04 21:44:22 +00001036 input_api, output_api, maxlen, source_file_filter=sources))
nick@chromium.orge06cb4e2011-04-23 01:20:38 +00001037 snapshot( "checking tabs")
bradnelson@google.com56e48bc2011-03-24 20:51:21 +00001038 results.extend(input_api.canned_checks.CheckChangeHasNoTabs(
1039 input_api, output_api, source_file_filter=sources))
nick@chromium.orge06cb4e2011-04-23 01:20:38 +00001040 snapshot( "checking stray whitespace")
bradnelson@google.com56e48bc2011-03-24 20:51:21 +00001041 results.extend(input_api.canned_checks.CheckChangeHasNoStrayWhitespace(
1042 input_api, output_api, source_file_filter=sources))
phajdan.jr@chromium.orgd965db32015-11-16 09:46:56 +00001043 snapshot("checking license")
1044 results.extend(input_api.canned_checks.CheckLicense(
1045 input_api, output_api, license_header, source_file_filter=sources))
maruel@chromium.orgb1ce7752011-05-08 13:50:16 +00001046
maruel@chromium.orgb1ce7752011-05-08 13:50:16 +00001047 if input_api.is_committing:
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001048 snapshot("checking was uploaded")
1049 results.extend(input_api.canned_checks.CheckChangeWasUploaded(
1050 input_api, output_api))
ilevy@chromium.orgc50d7612012-12-05 04:25:14 +00001051 snapshot("checking description")
1052 results.extend(input_api.canned_checks.CheckChangeHasDescription(
1053 input_api, output_api))
1054 results.extend(input_api.canned_checks.CheckDoNotSubmitInDescription(
1055 input_api, output_api))
ilevy@chromium.orgc50d7612012-12-05 04:25:14 +00001056 snapshot("checking do not submit in files")
1057 results.extend(input_api.canned_checks.CheckDoNotSubmitInFiles(
1058 input_api, output_api))
nick@chromium.orge06cb4e2011-04-23 01:20:38 +00001059 snapshot("done")
bradnelson@google.com56e48bc2011-03-24 20:51:21 +00001060 return results
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00001061
1062
Nodir Turakulov0ae14e92018-05-31 14:53:53 -07001063def CheckPatchFormatted(
1064 input_api, output_api, check_js=False, check_python=False):
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00001065 import git_cl
Nodir Turakulov0ae14e92018-05-31 14:53:53 -07001066
1067 display_args = []
Christopher Lamc5ba6922017-01-24 11:19:14 +11001068 if check_js:
Nodir Turakulov0ae14e92018-05-31 14:53:53 -07001069 display_args.append('--js')
1070 if check_python:
1071 # --python requires --full
1072 display_args.extend(['--python', '--full'])
1073
1074 cmd = ['-C', input_api.change.RepositoryRoot(),
1075 'cl', 'format', '--dry-run', '--presubmit'] + display_args
Andrew Grieved86c8032017-09-26 14:40:36 -04001076 presubmit_subdir = input_api.os_path.relpath(
1077 input_api.PresubmitLocalPath(), input_api.change.RepositoryRoot())
1078 if presubmit_subdir.startswith('..') or presubmit_subdir == '.':
1079 presubmit_subdir = ''
1080 # If the PRESUBMIT.py is in a parent repository, then format the entire
1081 # subrepository. Otherwise, format only the code in the directory that
1082 # contains the PRESUBMIT.py.
1083 if presubmit_subdir:
1084 cmd.append(input_api.PresubmitLocalPath())
enne@chromium.org555cfe42014-01-29 18:21:39 +00001085 code, _ = git_cl.RunGitWithCode(cmd, suppress_stderr=True)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00001086 if code == 2:
Andrew Grieved3b25482017-10-13 16:06:25 -04001087 if presubmit_subdir:
1088 short_path = presubmit_subdir
1089 else:
1090 short_path = input_api.basename(input_api.change.RepositoryRoot())
Nodir Turakulov0ae14e92018-05-31 14:53:53 -07001091 display_args.append(presubmit_subdir)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00001092 return [output_api.PresubmitPromptWarning(
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00001093 'The %s directory requires source formatting. '
Nodir Turakulov0ae14e92018-05-31 14:53:53 -07001094 'Please run: git cl format %s' %
1095 (short_path, ' '.join(display_args)))]
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00001096 # As this is just a warning, ignore all other errors if the user
1097 # happens to have a broken clang-format, doesn't use git, etc etc.
1098 return []
scottmg@chromium.orgd05ab352014-12-05 17:24:26 +00001099
1100
1101def CheckGNFormatted(input_api, output_api):
1102 import gn
1103 affected_files = input_api.AffectedFiles(
1104 include_deletes=False,
1105 file_filter=lambda x: x.LocalPath().endswith('.gn') or
kylechar58edce22016-06-17 06:07:51 -07001106 x.LocalPath().endswith('.gni') or
1107 x.LocalPath().endswith('.typemap'))
scottmg@chromium.orgd05ab352014-12-05 17:24:26 +00001108 warnings = []
1109 for f in affected_files:
1110 cmd = ['gn', 'format', '--dry-run', f.AbsoluteLocalPath()]
1111 rc = gn.main(cmd)
1112 if rc == 2:
1113 warnings.append(output_api.PresubmitPromptWarning(
brettw4b8ed592016-08-05 16:19:12 -07001114 '%s requires formatting. Please run:\n gn format %s' % (
scottmg@chromium.orgd05ab352014-12-05 17:24:26 +00001115 f.AbsoluteLocalPath(), f.LocalPath())))
1116 # It's just a warning, so ignore other types of failures assuming they'll be
1117 # caught elsewhere.
1118 return warnings
Dan Jacques94652a32017-10-09 23:18:46 -04001119
1120
1121def CheckCIPDManifest(input_api, output_api, path=None, content=None):
1122 """Verifies that a CIPD ensure file manifest is valid against all platforms.
1123
1124 Exactly one of "path" or "content" must be provided. An assertion will occur
1125 if neither or both are provided.
1126
1127 Args:
1128 path (str): If provided, the filesystem path to the manifest to verify.
1129 content (str): If provided, the raw content of the manifest to veirfy.
1130 """
1131 cipd_bin = 'cipd' if not input_api.is_windows else 'cipd.bat'
1132 cmd = [cipd_bin, 'ensure-file-verify']
1133 kwargs = {}
1134
1135 if input_api.is_windows:
1136 # Needs to be able to resolve "cipd.bat".
1137 kwargs['shell'] = True
1138
1139 if input_api.verbose:
1140 cmd += ['-log-level', 'debug']
1141
1142 if path:
1143 assert content is None, 'Cannot provide both "path" and "content".'
1144 cmd += ['-ensure-file', path]
Robert Iannucci7d47eb52017-12-06 12:18:18 -08001145 name = 'Check CIPD manifest %r' % path
Dan Jacques94652a32017-10-09 23:18:46 -04001146 elif content:
1147 assert path is None, 'Cannot provide both "path" and "content".'
1148 cmd += ['-ensure-file=-']
1149 kwargs['stdin'] = content
Robert Iannucci7d47eb52017-12-06 12:18:18 -08001150 # quick and dirty parser to extract checked packages.
1151 packages = [
1152 l.split()[0] for l in (ll.strip() for ll in content.splitlines())
1153 if ' ' in l and not l.startswith('$')
1154 ]
1155 name = 'Check CIPD packages from string: %r' % (packages,)
Dan Jacques94652a32017-10-09 23:18:46 -04001156 else:
1157 raise Exception('Exactly one of "path" or "content" must be provided.')
1158
1159 return input_api.Command(
Robert Iannucci7d47eb52017-12-06 12:18:18 -08001160 name,
Dan Jacques94652a32017-10-09 23:18:46 -04001161 cmd,
1162 kwargs,
1163 output_api.PresubmitError)
1164
1165
1166def CheckCIPDPackages(input_api, output_api, platforms, packages):
1167 """Verifies that all named CIPD packages can be resolved against all supplied
1168 platforms.
1169
1170 Args:
1171 platforms (list): List of CIPD platforms to verify.
1172 packages (dict): Mapping of package name to version.
1173 """
1174 manifest = []
1175 for p in platforms:
1176 manifest.append('$VerifiedPlatform %s' % (p,))
1177 for k, v in packages.iteritems():
1178 manifest.append('%s %s' % (k, v))
1179 return CheckCIPDManifest(input_api, output_api, content='\n'.join(manifest))
Sergiy Byelozyoroveba83472017-10-27 02:08:21 +02001180
1181
1182def CheckVPythonSpec(input_api, output_api, file_filter=None):
1183 """Validates any changed .vpython files with vpython verification tool.
1184
1185 Args:
1186 input_api: Bag of input related interfaces.
1187 output_api: Bag of output related interfaces.
1188 file_filter: Custom function that takes a path (relative to client root) and
1189 returns boolean, which is used to filter files for which to apply the
1190 verification to. Defaults to any path ending with .vpython, which captures
1191 both global .vpython and <script>.vpython files.
1192
1193 Returns:
1194 A list of input_api.Command objects containing verification commands.
1195 """
1196 file_filter = file_filter or (lambda f: f.LocalPath().endswith('.vpython'))
John Budorick16162372018-04-18 10:39:53 -07001197 affected_files = input_api.AffectedTestableFiles(file_filter=file_filter)
Sergiy Byelozyoroveba83472017-10-27 02:08:21 +02001198 affected_files = map(lambda f: f.AbsoluteLocalPath(), affected_files)
1199
1200 commands = []
1201 for f in affected_files:
1202 commands.append(input_api.Command(
1203 'Verify %s' % f,
1204 ['vpython', '-vpython-spec', f, '-vpython-tool', 'verify'],
1205 {'stderr': input_api.subprocess.STDOUT},
1206 output_api.PresubmitError))
1207
1208 return commands
Mun Yong Jang17995a92017-12-01 16:00:31 -08001209
1210
1211def CheckChangedLUCIConfigs(input_api, output_api):
1212 import collections
1213 import base64
1214 import json
Mun Yong Jangcfb9a232017-12-18 10:39:34 -08001215 import logging
Mun Yong Jang17995a92017-12-01 16:00:31 -08001216 import urllib2
1217
1218 import auth
1219 import git_cl
1220
1221 LUCI_CONFIG_HOST_NAME = 'luci-config.appspot.com'
1222
1223 cl = git_cl.Changelist()
Mun Yong Jang603d01e2017-12-19 16:38:30 -08001224 if input_api.change.issue and input_api.gerrit:
1225 remote_branch = input_api.gerrit.GetDestRef(input_api.change.issue)
1226 else:
1227 remote, remote_branch = cl.GetRemoteBranch()
1228 if remote_branch.startswith('refs/remotes/%s/' % remote):
1229 remote_branch = remote_branch.replace(
1230 'refs/remotes/%s/' % remote, 'refs/heads/', 1)
1231 if remote_branch.startswith('refs/remotes/branch-heads/'):
1232 remote_branch = remote_branch.replace(
1233 'refs/remotes/branch-heads/', 'refs/branch-heads/', 1)
Mun Yong Jang17995a92017-12-01 16:00:31 -08001234
1235 remote_host_url = cl.GetRemoteUrl()
1236 if not remote_host_url:
1237 return [output_api.PresubmitError(
1238 'Remote host url for git has not been defined')]
1239 remote_host_url = remote_host_url.rstrip('/')
1240 if remote_host_url.endswith('.git'):
1241 remote_host_url = remote_host_url[:-len('.git')]
1242
1243 # authentication
1244 try:
1245 authenticator = auth.get_authenticator_for_host(
1246 LUCI_CONFIG_HOST_NAME, auth.make_auth_config())
1247 acc_tkn = authenticator.get_access_token()
1248 except auth.AuthenticationError as e:
1249 return [output_api.PresubmitError(
1250 'Error in authenticating user.', long_text=str(e))]
1251
1252 def request(endpoint, body=None):
1253 api_url = ('https://%s/_ah/api/config/v1/%s'
1254 % (LUCI_CONFIG_HOST_NAME, endpoint))
1255 req = urllib2.Request(api_url)
1256 req.add_header('Authorization', 'Bearer %s' % acc_tkn.token)
1257 if body is not None:
1258 req.add_header('Content-Type', 'application/json')
1259 req.add_data(json.dumps(body))
1260 return json.load(urllib2.urlopen(req))
1261
1262 try:
1263 config_sets = request('config-sets').get('config_sets')
1264 except urllib2.HTTPError as e:
1265 return [output_api.PresubmitError(
1266 'Config set request to luci-config failed', long_text=str(e))]
1267 if not config_sets:
1268 return [output_api.PresubmitWarning('No config_sets were returned')]
1269 loc_pref = '%s/+/%s/' % (remote_host_url, remote_branch)
Mun Yong Jangcfb9a232017-12-18 10:39:34 -08001270 logging.debug('Derived location prefix: %s', loc_pref)
Mun Yong Jang17995a92017-12-01 16:00:31 -08001271 dir_to_config_set = {
1272 '%s/' % cs['location'][len(loc_pref):].rstrip('/'): cs['config_set']
1273 for cs in config_sets
1274 if cs['location'].startswith(loc_pref) or
1275 ('%s/' % cs['location']) == loc_pref
1276 }
1277 cs_to_files = collections.defaultdict(list)
1278 for f in input_api.AffectedFiles():
1279 # windows
1280 file_path = f.LocalPath().replace(_os.sep, '/')
Mun Yong Jangcfb9a232017-12-18 10:39:34 -08001281 logging.debug('Affected file path: %s', file_path)
Mun Yong Jang17995a92017-12-01 16:00:31 -08001282 for dr, cs in dir_to_config_set.iteritems():
1283 if dr == '/' or file_path.startswith(dr):
1284 cs_to_files[cs].append({
1285 'path': file_path[len(dr):] if dr != '/' else file_path,
1286 'content': base64.b64encode(
1287 '\n'.join(f.NewContents()).encode('utf-8'))
1288 })
1289 outputs = []
1290 for cs, f in cs_to_files.iteritems():
1291 try:
1292 # TODO(myjang): parallelize
1293 res = request(
1294 'validate-config', body={'config_set': cs, 'files': f})
1295 except urllib2.HTTPError as e:
1296 return [output_api.PresubmitError(
1297 'Validation request to luci-config failed', long_text=str(e))]
1298 for msg in res.get('messages', []):
1299 sev = msg['severity']
1300 if sev == 'WARNING':
1301 out_f = output_api.PresubmitPromptWarning
1302 elif sev == 'ERROR' or sev == 'CRITICAL':
1303 out_f = output_api.PresubmitError
1304 else:
1305 out_f = output_api.PresubmitNotifyResult
1306 outputs.append(out_f('Config validation: %s' % msg['text']))
1307 return outputs