blob: 710a8d966b95370d6082f164d784f876c069ca44 [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
Michael Achenbachc850b962016-12-05 15:40:17 +010083def CheckAuthorizedAuthor(input_api, output_api):
84 """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 []
96 authors_path = input_api.os_path.join(
97 input_api.PresubmitLocalPath(), 'AUTHORS')
98 valid_authors = (
99 input_api.re.match(r'[^#]+\s+\<(.+?)\>\s*$', line)
100 for line in open(authors_path))
101 valid_authors = [item.group(1).lower() for item in valid_authors if item]
102 if not any(input_api.fnmatch.fnmatch(author.lower(), valid)
103 for valid in valid_authors):
104 input_api.logging.info('Valid authors are %s', ', '.join(valid_authors))
Michael Achenbach590420d2017-10-18 12:35:37 +0200105 return [error_type(
Michael Achenbachc850b962016-12-05 15:40:17 +0100106 ('%s is not in AUTHORS file. If you are a new contributor, please visit'
107 '\n'
Xiaoyin Liub5807972017-10-04 22:07:24 -0400108 'https://www.chromium.org/developers/contributing-code and read the '
Michael Achenbachc850b962016-12-05 15:40:17 +0100109 '"Legal" section\n'
110 'If you are a chromite, verify the contributor signed the CLA.') %
111 author)]
112 return []
113
114
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000115def CheckDoNotSubmitInFiles(input_api, output_api):
ilevy@chromium.orgc50d7612012-12-05 04:25:14 +0000116 """Checks that the user didn't add 'DO NOT ''SUBMIT' to any files."""
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000117 # We want to check every text file, not just source files.
118 file_filter = lambda x : x
ilevy@chromium.orgc50d7612012-12-05 04:25:14 +0000119 keyword = 'DO NOT ''SUBMIT'
bulach@chromium.orgbfffd452012-02-22 01:13:29 +0000120 errors = _FindNewViolationsOfRule(lambda _, line : keyword not in line,
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000121 input_api, file_filter)
122 text = '\n'.join('Found %s in %s' % (keyword, loc) for loc in errors)
123 if text:
124 return [output_api.PresubmitError(text)]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000125 return []
126
127
tfarina@chromium.orgb6795642014-12-12 00:03:49 +0000128def CheckChangeLintsClean(input_api, output_api, source_file_filter=None,
tfarina@chromium.orga62208f2015-02-25 03:23:11 +0000129 lint_filters=None, verbose_level=None):
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000130 """Checks that all '.cc' and '.h' files pass cpplint.py."""
erg@google.com26970fa2009-11-17 18:07:32 +0000131 _RE_IS_TEST = input_api.re.compile(r'.*tests?.(cc|h)$')
132 result = []
133
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000134 cpplint = input_api.cpplint
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000135 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800136 # pylint: disable=protected-access
erg@google.com26970fa2009-11-17 18:07:32 +0000137 cpplint._cpplint_state.ResetErrorCounts()
138
tfarina@chromium.orgb6795642014-12-12 00:03:49 +0000139 lint_filters = lint_filters or DEFAULT_LINT_FILTERS
danakj@chromium.org0ae71222016-01-11 19:37:11 +0000140 lint_filters.extend(BLACKLIST_LINT_FILTERS)
tfarina@chromium.orgb6795642014-12-12 00:03:49 +0000141 cpplint._SetFilters(','.join(lint_filters))
erg@google.com26970fa2009-11-17 18:07:32 +0000142
143 # We currently are more strict with normal code than unit tests; 4 and 5 are
144 # the verbosity level that would normally be passed to cpplint.py through
145 # --verbose=#. Hopefully, in the future, we can be more verbose.
146 files = [f.AbsoluteLocalPath() for f in
147 input_api.AffectedSourceFiles(source_file_filter)]
148 for file_name in files:
149 if _RE_IS_TEST.match(file_name):
150 level = 5
151 else:
152 level = 4
153
tfarina@chromium.orga62208f2015-02-25 03:23:11 +0000154 verbose_level = verbose_level or level
155 cpplint.ProcessFile(file_name, verbose_level)
erg@google.com26970fa2009-11-17 18:07:32 +0000156
157 if cpplint._cpplint_state.error_count > 0:
158 if input_api.is_committing:
159 res_type = output_api.PresubmitError
160 else:
161 res_type = output_api.PresubmitPromptWarning
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000162 result = [res_type('Changelist failed cpplint.py check.')]
erg@google.com26970fa2009-11-17 18:07:32 +0000163
164 return result
165
166
maruel@chromium.org3410d912009-06-09 20:56:16 +0000167def CheckChangeHasNoCR(input_api, output_api, source_file_filter=None):
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000168 """Checks no '\r' (CR) character is in any source files."""
169 cr_files = []
maruel@chromium.org3410d912009-06-09 20:56:16 +0000170 for f in input_api.AffectedSourceFiles(source_file_filter):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000171 if '\r' in input_api.ReadFile(f, 'rb'):
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000172 cr_files.append(f.LocalPath())
173 if cr_files:
174 return [output_api.PresubmitPromptWarning(
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000175 'Found a CR character in these files:', items=cr_files)]
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000176 return []
177
178
179def CheckChangeHasOnlyOneEol(input_api, output_api, source_file_filter=None):
180 """Checks the files ends with one and only one \n (LF)."""
181 eof_files = []
182 for f in input_api.AffectedSourceFiles(source_file_filter):
183 contents = input_api.ReadFile(f, 'rb')
184 # Check that the file ends in one and only one newline character.
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000185 if len(contents) > 1 and (contents[-1:] != '\n' or contents[-2:-1] == '\n'):
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000186 eof_files.append(f.LocalPath())
187
188 if eof_files:
189 return [output_api.PresubmitPromptWarning(
190 'These files should end in one (and only one) newline character:',
191 items=eof_files)]
192 return []
193
194
195def CheckChangeHasNoCrAndHasOnlyOneEol(input_api, output_api,
196 source_file_filter=None):
197 """Runs both CheckChangeHasNoCR and CheckChangeHasOnlyOneEOL in one pass.
198
199 It is faster because it is reading the file only once.
200 """
201 cr_files = []
202 eof_files = []
203 for f in input_api.AffectedSourceFiles(source_file_filter):
204 contents = input_api.ReadFile(f, 'rb')
205 if '\r' in contents:
206 cr_files.append(f.LocalPath())
207 # Check that the file ends in one and only one newline character.
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000208 if len(contents) > 1 and (contents[-1:] != '\n' or contents[-2:-1] == '\n'):
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000209 eof_files.append(f.LocalPath())
210 outputs = []
211 if cr_files:
212 outputs.append(output_api.PresubmitPromptWarning(
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000213 'Found a CR character in these files:', items=cr_files))
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000214 if eof_files:
215 outputs.append(output_api.PresubmitPromptWarning(
216 'These files should end in one (and only one) newline character:',
217 items=eof_files))
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000218 return outputs
219
seanmccullough0b670442016-06-07 10:45:58 -0700220def CheckGenderNeutral(input_api, output_api, source_file_filter=None):
221 """Checks that there are no gendered pronouns in any of the text files to be
222 submitted.
223 """
224 gendered_re = input_api.re.compile(
225 '(^|\s|\(|\[)([Hh]e|[Hh]is|[Hh]ers?|[Hh]im|[Ss]he|[Gg]uys?)\\b')
226
227 errors = []
228 for f in input_api.AffectedFiles(include_deletes=False,
229 file_filter=source_file_filter):
230 for line_num, line in f.ChangedContents():
231 if gendered_re.search(line):
232 errors.append('%s (%d): %s' % (f.LocalPath(), line_num, line))
233
234 if len(errors):
235 return [output_api.PresubmitPromptWarning('Found a gendered pronoun in:',
236 long_text='\n'.join(errors))]
237 return []
238
239
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000240
chrisha@google.com267d6592012-06-19 19:23:31 +0000241def _ReportErrorFileAndLine(filename, line_num, dummy_line):
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000242 """Default error formatter for _FindNewViolationsOfRule."""
danakj@chromium.orgc5965ba2013-08-14 00:27:24 +0000243 return '%s:%s' % (filename, line_num)
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000244
245
246def _FindNewViolationsOfRule(callable_rule, input_api, source_file_filter=None,
247 error_formatter=_ReportErrorFileAndLine):
248 """Find all newly introduced violations of a per-line rule (a callable).
249
250 Arguments:
bulach@chromium.orgbfffd452012-02-22 01:13:29 +0000251 callable_rule: a callable taking a file extension and line of input and
252 returning True if the rule is satisfied and False if there was a problem.
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000253 input_api: object to enumerate the affected files.
254 source_file_filter: a filter to be passed to the input api.
255 error_formatter: a callable taking (filename, line_number, line) and
256 returning a formatted error string.
257
258 Returns:
259 A list of the newly-introduced violations reported by the rule.
260 """
261 errors = []
sail@chromium.org5538e022011-05-12 17:53:16 +0000262 for f in input_api.AffectedFiles(include_deletes=False,
263 file_filter=source_file_filter):
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000264 # For speed, we do two passes, checking first the full file. Shelling out
265 # to the SCM to determine the changed region can be quite expensive on
266 # Win32. Assuming that most files will be kept problem-free, we can
267 # skip the SCM operations most of the time.
bulach@chromium.orgbfffd452012-02-22 01:13:29 +0000268 extension = str(f.LocalPath()).rsplit('.', 1)[-1]
269 if all(callable_rule(extension, line) for line in f.NewContents()):
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000270 continue # No violation found in full text: can skip considering diff.
271
272 for line_num, line in f.ChangedContents():
bulach@chromium.orgbfffd452012-02-22 01:13:29 +0000273 if not callable_rule(extension, line):
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000274 errors.append(error_formatter(f.LocalPath(), line_num, line))
275
276 return errors
277
278
maruel@chromium.org3410d912009-06-09 20:56:16 +0000279def CheckChangeHasNoTabs(input_api, output_api, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000280 """Checks that there are no tab characters in any of the text files to be
281 submitted.
282 """
maruel@chromium.org115ae6c2010-06-18 17:11:43 +0000283 # In addition to the filter, make sure that makefiles are blacklisted.
284 if not source_file_filter:
285 # It's the default filter.
286 source_file_filter = input_api.FilterSourceFile
287 def filter_more(affected_file):
cmp@chromium.orgcb5e57c2012-04-06 19:50:15 +0000288 basename = input_api.os_path.basename(affected_file.LocalPath())
289 return (not (basename in ('Makefile', 'makefile') or
290 basename.endswith('.mk')) and
maruel@chromium.org115ae6c2010-06-18 17:11:43 +0000291 source_file_filter(affected_file))
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000292
bulach@chromium.orgbfffd452012-02-22 01:13:29 +0000293 tabs = _FindNewViolationsOfRule(lambda _, line : '\t' not in line,
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000294 input_api, filter_more)
295
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000296 if tabs:
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000297 return [output_api.PresubmitPromptWarning('Found a tab character in:',
298 long_text='\n'.join(tabs))]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000299 return []
300
301
estade@chromium.orgfdcc9f72011-02-07 22:25:07 +0000302def CheckChangeTodoHasOwner(input_api, output_api, source_file_filter=None):
303 """Checks that the user didn't add TODO(name) without an owner."""
304
ilevy@chromium.orgc50d7612012-12-05 04:25:14 +0000305 unowned_todo = input_api.re.compile('TO''DO[^(]')
bulach@chromium.orgbfffd452012-02-22 01:13:29 +0000306 errors = _FindNewViolationsOfRule(lambda _, x : not unowned_todo.search(x),
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000307 input_api, source_file_filter)
ilevy@chromium.orgc50d7612012-12-05 04:25:14 +0000308 errors = ['Found TO''DO with no owner in ' + x for x in errors]
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000309 if errors:
310 return [output_api.PresubmitPromptWarning('\n'.join(errors))]
estade@chromium.orgfdcc9f72011-02-07 22:25:07 +0000311 return []
312
313
maruel@chromium.orgf5888bb2009-06-10 20:26:37 +0000314def CheckChangeHasNoStrayWhitespace(input_api, output_api,
315 source_file_filter=None):
316 """Checks that there is no stray whitespace at source lines end."""
bulach@chromium.orgbfffd452012-02-22 01:13:29 +0000317 errors = _FindNewViolationsOfRule(lambda _, line : line.rstrip() == line,
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000318 input_api, source_file_filter)
maruel@chromium.orgf5888bb2009-06-10 20:26:37 +0000319 if errors:
320 return [output_api.PresubmitPromptWarning(
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000321 'Found line ending with white spaces in:',
322 long_text='\n'.join(errors))]
maruel@chromium.orgf5888bb2009-06-10 20:26:37 +0000323 return []
324
325
jamesr@chromium.orgaf27f462013-04-04 21:44:22 +0000326def CheckLongLines(input_api, output_api, maxlen, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000327 """Checks that there aren't any lines longer than maxlen characters in any of
328 the text files to be submitted.
329 """
dpranke@chromium.orge3b1c3d2012-10-20 22:28:14 +0000330 maxlens = {
331 'java': 100,
torne@chromium.org0ecad9c2013-02-15 16:35:16 +0000332 # This is specifically for Android's handwritten makefiles (Android.mk).
333 'mk': 200,
bulach@chromium.orgbfffd452012-02-22 01:13:29 +0000334 '': maxlen,
335 }
maruel@chromium.orgeba64622011-06-20 18:26:48 +0000336
erikchen@google.com12816082013-12-03 02:04:20 +0000337 # Language specific exceptions to max line length.
338 # '.h' is considered an obj-c file extension, since OBJC_EXCEPTIONS are a
339 # superset of CPP_EXCEPTIONS.
340 CPP_FILE_EXTS = ('c', 'cc')
341 CPP_EXCEPTIONS = ('#define', '#endif', '#if', '#include', '#pragma')
Dave Schuyler6eea7c22017-03-03 18:46:25 -0800342 HTML_FILE_EXTS = ('html',)
Dave Schuyler9b9b94c2017-04-13 15:28:41 -0700343 HTML_EXCEPTIONS = ('<g ', '<link ', '<path ',)
erikchen@google.com12816082013-12-03 02:04:20 +0000344 JAVA_FILE_EXTS = ('java',)
345 JAVA_EXCEPTIONS = ('import ', 'package ')
dbeam@chromium.org5a2af6d2015-10-08 17:52:29 +0000346 JS_FILE_EXTS = ('js',)
347 JS_EXCEPTIONS = ("GEN('#include",)
erikchen@google.com12816082013-12-03 02:04:20 +0000348 OBJC_FILE_EXTS = ('h', 'm', 'mm')
349 OBJC_EXCEPTIONS = ('#define', '#endif', '#if', '#import', '#include',
350 '#pragma')
dbeam@chromium.org5a2af6d2015-10-08 17:52:29 +0000351 PY_FILE_EXTS = ('py',)
aiolos@chromium.org84033cc2015-07-16 01:27:15 +0000352 PY_EXCEPTIONS = ('import', 'from')
erikchen@google.com12816082013-12-03 02:04:20 +0000353
354 LANGUAGE_EXCEPTIONS = [
355 (CPP_FILE_EXTS, CPP_EXCEPTIONS),
Dave Schuyler6eea7c22017-03-03 18:46:25 -0800356 (HTML_FILE_EXTS, HTML_EXCEPTIONS),
erikchen@google.com12816082013-12-03 02:04:20 +0000357 (JAVA_FILE_EXTS, JAVA_EXCEPTIONS),
dbeam@chromium.org5a2af6d2015-10-08 17:52:29 +0000358 (JS_FILE_EXTS, JS_EXCEPTIONS),
erikchen@google.com12816082013-12-03 02:04:20 +0000359 (OBJC_FILE_EXTS, OBJC_EXCEPTIONS),
aiolos@chromium.org84033cc2015-07-16 01:27:15 +0000360 (PY_FILE_EXTS, PY_EXCEPTIONS),
erikchen@google.com12816082013-12-03 02:04:20 +0000361 ]
sivachandra@chromium.org270db5c2012-10-09 22:38:44 +0000362
bulach@chromium.orgbfffd452012-02-22 01:13:29 +0000363 def no_long_lines(file_extension, line):
erikchen@google.com12816082013-12-03 02:04:20 +0000364 # Check for language specific exceptions.
Dave Schuyler9b9b94c2017-04-13 15:28:41 -0700365 if any(file_extension in exts and line.lstrip().startswith(exceptions)
erikchen@google.com12816082013-12-03 02:04:20 +0000366 for exts, exceptions in LANGUAGE_EXCEPTIONS):
sivachandra@chromium.org270db5c2012-10-09 22:38:44 +0000367 return True
368
bulach@chromium.orgbfffd452012-02-22 01:13:29 +0000369 file_maxlen = maxlens.get(file_extension, maxlens[''])
370 # Stupidly long symbols that needs to be worked around if takes 66% of line.
371 long_symbol = file_maxlen * 2 / 3
372 # Hard line length limit at 50% more.
373 extra_maxlen = file_maxlen * 3 / 2
374
375 line_len = len(line)
376 if line_len <= file_maxlen:
maruel@chromium.orgeba64622011-06-20 18:26:48 +0000377 return True
378
dtu@chromium.org03f0c532015-03-27 22:22:07 +0000379 # Allow long URLs of any length.
treib@chromium.orgde3ee902014-07-28 14:23:11 +0000380 if any((url in line) for url in ('file://', 'http://', 'https://')):
381 return True
382
qyearsley12fa6ff2016-08-24 09:18:40 -0700383 # If 'line-too-long' is explicitly suppressed for the line, any length is
nednguyen@google.com6df90f32015-12-01 20:14:33 +0000384 # acceptable.
385 if 'pylint: disable=line-too-long' in line and file_extension == 'py':
386 return True
387
dtu@chromium.org03f0c532015-03-27 22:22:07 +0000388 if line_len > extra_maxlen:
389 return False
390
treib@chromium.orgde3ee902014-07-28 14:23:11 +0000391 if 'url(' in line and file_extension == 'css':
392 return True
393
dbeam@chromium.orgb5ccc9b2014-09-23 00:42:22 +0000394 if '<include' in line and file_extension in ('css', 'html', 'js'):
395 return True
396
treib@chromium.orgde3ee902014-07-28 14:23:11 +0000397 return input_api.re.match(
398 r'.*[A-Za-z][A-Za-z_0-9]{%d,}.*' % long_symbol, line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000399
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000400 def format_error(filename, line_num, line):
401 return '%s, line %s, %s chars' % (filename, line_num, len(line))
402
403 errors = _FindNewViolationsOfRule(no_long_lines, input_api,
404 source_file_filter,
405 error_formatter=format_error)
406 if errors:
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000407 msg = 'Found lines longer than %s characters (first 5 shown).' % maxlen
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000408 return [output_api.PresubmitPromptWarning(msg, items=errors[:5])]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000409 else:
410 return []
411
412
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000413def CheckLicense(input_api, output_api, license_re, source_file_filter=None,
maruel@chromium.org71626852010-11-03 13:14:25 +0000414 accept_empty_files=True):
maruel@chromium.orgb9e7ada2010-01-27 23:12:39 +0000415 """Verifies the license header.
416 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000417 license_re = input_api.re.compile(license_re, input_api.re.MULTILINE)
maruel@chromium.orgb9e7ada2010-01-27 23:12:39 +0000418 bad_files = []
419 for f in input_api.AffectedSourceFiles(source_file_filter):
420 contents = input_api.ReadFile(f, 'rb')
maruel@chromium.org71626852010-11-03 13:14:25 +0000421 if accept_empty_files and not contents:
422 continue
maruel@chromium.orgb9e7ada2010-01-27 23:12:39 +0000423 if not license_re.search(contents):
424 bad_files.append(f.LocalPath())
425 if bad_files:
phajdan.jr@chromium.orge27eb7e2015-11-16 12:47:53 +0000426 return [output_api.PresubmitPromptWarning(
bradnelson@google.com393460b2011-03-29 01:20:26 +0000427 'License must match:\n%s\n' % license_re.pattern +
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000428 'Found a bad license header in these files:', items=bad_files)]
maruel@chromium.orgb9e7ada2010-01-27 23:12:39 +0000429 return []
430
431
maruel@chromium.org3410d912009-06-09 20:56:16 +0000432### Other checks
433
434def CheckDoNotSubmit(input_api, output_api):
435 return (
436 CheckDoNotSubmitInDescription(input_api, output_api) +
437 CheckDoNotSubmitInFiles(input_api, output_api)
438 )
439
440
bradnelson@google.comc0b332a2010-08-26 00:30:37 +0000441def CheckTreeIsOpen(input_api, output_api,
442 url=None, closed=None, json_url=None):
443 """Check whether to allow commit without prompt.
444
445 Supports two styles:
446 1. Checks that an url's content doesn't match a regexp that would mean that
447 the tree is closed. (old)
448 2. Check the json_url to decide whether to allow commit without prompt.
449 Args:
450 input_api: input related apis.
451 output_api: output related apis.
452 url: url to use for regex based tree status.
453 closed: regex to match for closed status.
454 json_url: url to download json style status.
455 """
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000456 if not input_api.is_committing:
457 return []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000458 try:
bradnelson@google.comc0b332a2010-08-26 00:30:37 +0000459 if json_url:
460 connection = input_api.urllib2.urlopen(json_url)
461 status = input_api.json.loads(connection.read())
462 connection.close()
463 if not status['can_commit_freely']:
464 short_text = 'Tree state is: ' + status['general_state']
465 long_text = status['message'] + '\n' + json_url
466 return [output_api.PresubmitError(short_text, long_text=long_text)]
467 else:
468 # TODO(bradnelson): drop this once all users are gone.
469 connection = input_api.urllib2.urlopen(url)
470 status = connection.read()
471 connection.close()
472 if input_api.re.match(closed, status):
473 long_text = status + '\n' + url
474 return [output_api.PresubmitError('The tree is closed.',
475 long_text=long_text)]
rohitrao@chromium.orgd490ee82012-02-06 19:31:33 +0000476 except IOError as e:
477 return [output_api.PresubmitError('Error fetching tree status.',
478 long_text=str(e))]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000479 return []
maruel@chromium.org7b305e82009-05-19 18:24:20 +0000480
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000481def GetUnitTestsInDirectory(
smut@google.comac296202014-04-24 21:47:17 +0000482 input_api, output_api, directory, whitelist=None, blacklist=None, env=None):
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000483 """Lists all files in a directory and runs them. Doesn't recurse.
484
nick@chromium.orgff526192013-06-10 19:30:26 +0000485 It's mainly a wrapper for RunUnitTests. Use whitelist and blacklist to filter
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000486 tests accordingly.
487 """
488 unit_tests = []
489 test_path = input_api.os_path.abspath(
490 input_api.os_path.join(input_api.PresubmitLocalPath(), directory))
491
492 def check(filename, filters):
493 return any(True for i in filters if input_api.re.match(i, filename))
494
maruel@chromium.orgfae707b2011-09-15 18:57:58 +0000495 to_run = found = 0
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000496 for filename in input_api.os_listdir(test_path):
maruel@chromium.orgfae707b2011-09-15 18:57:58 +0000497 found += 1
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000498 fullpath = input_api.os_path.join(test_path, filename)
499 if not input_api.os_path.isfile(fullpath):
500 continue
501 if whitelist and not check(filename, whitelist):
502 continue
503 if blacklist and check(filename, blacklist):
504 continue
505 unit_tests.append(input_api.os_path.join(directory, filename))
maruel@chromium.orgfae707b2011-09-15 18:57:58 +0000506 to_run += 1
anatoly techtonikbdcdc592017-03-29 17:00:13 +0300507 input_api.logging.debug('Found %d files, running %d unit tests'
508 % (found, to_run))
maruel@chromium.orgfae707b2011-09-15 18:57:58 +0000509 if not to_run:
510 return [
511 output_api.PresubmitPromptWarning(
512 'Out of %d files, found none that matched w=%r, b=%r in directory %s'
513 % (found, whitelist, blacklist, directory))
514 ]
smut@google.comac296202014-04-24 21:47:17 +0000515 return GetUnitTests(input_api, output_api, unit_tests, env)
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000516
517
smut@google.comac296202014-04-24 21:47:17 +0000518def GetUnitTests(input_api, output_api, unit_tests, env=None):
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000519 """Runs all unit tests in a directory.
520
521 On Windows, sys.executable is used for unit tests ending with ".py".
522 """
523 # We don't want to hinder users from uploading incomplete patches.
524 if input_api.is_committing:
525 message_type = output_api.PresubmitError
526 else:
527 message_type = output_api.PresubmitPromptWarning
528
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000529 results = []
530 for unit_test in unit_tests:
Robert Iannucci50258932018-03-19 10:30:59 -0700531 cmd = [unit_test]
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000532 if input_api.verbose:
maruel@chromium.org6c7723e2011-04-12 19:04:55 +0000533 cmd.append('--verbose')
smut@google.comac296202014-04-24 21:47:17 +0000534 kwargs = {'cwd': input_api.PresubmitLocalPath()}
535 if env:
536 kwargs['env'] = env
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000537 results.append(input_api.Command(
538 name=unit_test,
539 cmd=cmd,
smut@google.comac296202014-04-24 21:47:17 +0000540 kwargs=kwargs,
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000541 message=message_type))
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000542 return results
543
nick@chromium.orgff526192013-06-10 19:30:26 +0000544
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000545def GetUnitTestsRecursively(input_api, output_api, directory,
546 whitelist, blacklist):
547 """Gets all files in the directory tree (git repo) that match the whitelist.
548
549 Restricts itself to only find files within the Change's source repo, not
550 dependencies.
551 """
552 def check(filename):
553 return (any(input_api.re.match(f, filename) for f in whitelist) and
554 not any(input_api.re.match(f, filename) for f in blacklist))
555
556 tests = []
557
558 to_run = found = 0
559 for filepath in input_api.change.AllFiles(directory):
560 found += 1
561 if check(filepath):
562 to_run += 1
563 tests.append(filepath)
564 input_api.logging.debug('Found %d files, running %d' % (found, to_run))
565 if not to_run:
566 return [
567 output_api.PresubmitPromptWarning(
568 'Out of %d files, found none that matched w=%r, b=%r in directory %s'
569 % (found, whitelist, blacklist, directory))
570 ]
571
572 return GetUnitTests(input_api, output_api, tests)
573
574
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000575def GetPythonUnitTests(input_api, output_api, unit_tests):
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000576 """Run the unit tests out of process, capture the output and use the result
577 code to determine success.
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000578
579 DEPRECATED.
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000580 """
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000581 # We don't want to hinder users from uploading incomplete patches.
582 if input_api.is_committing:
583 message_type = output_api.PresubmitError
584 else:
585 message_type = output_api.PresubmitNotifyResult
maruel@chromium.org0e766052011-04-06 13:32:51 +0000586 results = []
maruel@chromium.org7b305e82009-05-19 18:24:20 +0000587 for unit_test in unit_tests:
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000588 # Run the unit tests out of process. This is because some unit tests
589 # stub out base libraries and don't clean up their mess. It's too easy to
590 # get subtle bugs.
591 cwd = None
592 env = None
593 unit_test_name = unit_test
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000594 # 'python -m test.unit_test' doesn't work. We need to change to the right
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000595 # directory instead.
596 if '.' in unit_test:
597 # Tests imported in submodules (subdirectories) assume that the current
598 # directory is in the PYTHONPATH. Manually fix that.
599 unit_test = unit_test.replace('.', '/')
600 cwd = input_api.os_path.dirname(unit_test)
601 unit_test = input_api.os_path.basename(unit_test)
602 env = input_api.environ.copy()
kbr@google.comab318592009-09-04 00:54:55 +0000603 # At least on Windows, it seems '.' must explicitly be in PYTHONPATH
604 backpath = [
605 '.', input_api.os_path.pathsep.join(['..'] * (cwd.count('/') + 1))
606 ]
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000607 if env.get('PYTHONPATH'):
608 backpath.append(env.get('PYTHONPATH'))
ukai@chromium.orga301f1f2009-08-05 10:37:33 +0000609 env['PYTHONPATH'] = input_api.os_path.pathsep.join((backpath))
Robert Iannucci23bd7352018-03-23 11:51:40 -0700610 env.pop('VPYTHON_CLEAR_PYTHONPATH', None)
maruel@chromium.org0e766052011-04-06 13:32:51 +0000611 cmd = [input_api.python_executable, '-m', '%s' % unit_test]
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000612 results.append(input_api.Command(
613 name=unit_test_name,
614 cmd=cmd,
615 kwargs={'env': env, 'cwd': cwd},
616 message=message_type))
maruel@chromium.org0e766052011-04-06 13:32:51 +0000617 return results
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000618
619
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000620def RunUnitTestsInDirectory(input_api, *args, **kwargs):
621 """Run tests in a directory serially.
622
623 For better performance, use GetUnitTestsInDirectory and then
624 pass to input_api.RunTests.
625 """
626 return input_api.RunTests(
627 GetUnitTestsInDirectory(input_api, *args, **kwargs), False)
628
629
630def RunUnitTests(input_api, *args, **kwargs):
631 """Run tests serially.
632
633 For better performance, use GetUnitTests and then pass to
634 input_api.RunTests.
635 """
636 return input_api.RunTests(GetUnitTests(input_api, *args, **kwargs), False)
637
638
639def RunPythonUnitTests(input_api, *args, **kwargs):
640 """Run python tests in a directory serially.
641
642 DEPRECATED
643 """
644 return input_api.RunTests(
645 GetPythonUnitTests(input_api, *args, **kwargs), False)
646
647
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000648def _FetchAllFiles(input_api, white_list, black_list):
649 """Hack to fetch all files."""
650 # We cannot use AffectedFiles here because we want to test every python
651 # file on each single python change. It's because a change in a python file
652 # can break another unmodified file.
653 # Use code similar to InputApi.FilterSourceFile()
654 def Find(filepath, filters):
anatoly techtonikbdcdc592017-03-29 17:00:13 +0300655 if input_api.platform == 'win32':
656 filepath = filepath.replace('\\', '/')
657
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000658 for item in filters:
659 if input_api.re.match(item, filepath):
660 return True
661 return False
662
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000663 files = []
664 path_len = len(input_api.PresubmitLocalPath())
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000665 for dirpath, dirnames, filenames in input_api.os_walk(
666 input_api.PresubmitLocalPath()):
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000667 # Passes dirnames in black list to speed up search.
668 for item in dirnames[:]:
669 filepath = input_api.os_path.join(dirpath, item)[path_len + 1:]
670 if Find(filepath, black_list):
671 dirnames.remove(item)
672 for item in filenames:
673 filepath = input_api.os_path.join(dirpath, item)[path_len + 1:]
674 if Find(filepath, white_list) and not Find(filepath, black_list):
675 files.append(filepath)
676 return files
677
678
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000679def GetPylint(input_api, output_api, white_list=None, black_list=None,
dtu@chromium.orgf23524d2016-01-27 20:08:49 +0000680 disabled_warnings=None, extra_paths_list=None, pylintrc=None):
maruel@chromium.orgbf38a7e2010-12-14 18:15:54 +0000681 """Run pylint on python files.
682
chrisha@google.com267d6592012-06-19 19:23:31 +0000683 The default white_list enforces looking only at *.py files.
maruel@chromium.orgbf38a7e2010-12-14 18:15:54 +0000684 """
maruel@chromium.org69eaecb2011-06-14 13:09:13 +0000685 white_list = tuple(white_list or ('.*\.py$',))
686 black_list = tuple(black_list or input_api.DEFAULT_BLACK_LIST)
ilevy@chromium.org7b677f72013-01-07 18:49:26 +0000687 extra_paths_list = extra_paths_list or []
688
maruel@chromium.orgade9c592011-04-07 15:59:11 +0000689 if input_api.is_committing:
690 error_type = output_api.PresubmitError
691 else:
692 error_type = output_api.PresubmitPromptWarning
maruel@chromium.orgbf38a7e2010-12-14 18:15:54 +0000693
694 # Only trigger if there is at least one python file affected.
ilevy@chromium.org36576332013-01-08 03:16:15 +0000695 def rel_path(regex):
696 """Modifies a regex for a subject to accept paths relative to root."""
chrisha@chromium.org40e5d3d2013-01-18 17:46:57 +0000697 def samefile(a, b):
698 # Default implementation for platforms lacking os.path.samefile
699 # (like Windows).
700 return input_api.os_path.abspath(a) == input_api.os_path.abspath(b)
701 samefile = getattr(input_api.os_path, 'samefile', samefile)
702 if samefile(input_api.PresubmitLocalPath(),
703 input_api.change.RepositoryRoot()):
ilevy@chromium.orgdd4e9312013-01-15 03:22:04 +0000704 return regex
chrisha@chromium.org40e5d3d2013-01-18 17:46:57 +0000705
ilevy@chromium.org36576332013-01-08 03:16:15 +0000706 prefix = input_api.os_path.join(input_api.os_path.relpath(
707 input_api.PresubmitLocalPath(), input_api.change.RepositoryRoot()), '')
708 return input_api.re.escape(prefix) + regex
ilevy@chromium.org7b677f72013-01-07 18:49:26 +0000709 src_filter = lambda x: input_api.FilterSourceFile(
710 x, map(rel_path, white_list), map(rel_path, black_list))
maruel@chromium.orgbf38a7e2010-12-14 18:15:54 +0000711 if not input_api.AffectedSourceFiles(src_filter):
ilevy@chromium.org7b677f72013-01-07 18:49:26 +0000712 input_api.logging.info('Skipping pylint: no matching changes.')
maruel@chromium.orgbf38a7e2010-12-14 18:15:54 +0000713 return []
714
agable@chromium.org327d72b2015-04-21 20:22:50 +0000715 if pylintrc is not None:
716 pylintrc = input_api.os_path.join(input_api.PresubmitLocalPath(), pylintrc)
717 else:
718 pylintrc = input_api.os_path.join(_HERE, 'pylintrc')
dtu@chromium.orgf23524d2016-01-27 20:08:49 +0000719 extra_args = ['--rcfile=%s' % pylintrc]
maruel@chromium.orgff9a2172012-04-24 16:55:32 +0000720 if disabled_warnings:
721 extra_args.extend(['-d', ','.join(disabled_warnings)])
722
chrisha@google.com267d6592012-06-19 19:23:31 +0000723 files = _FetchAllFiles(input_api, white_list, black_list)
724 if not files:
maruel@chromium.orgfca53392010-12-21 18:42:57 +0000725 return []
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000726 files.sort()
chrisha@google.com267d6592012-06-19 19:23:31 +0000727
csharp@chromium.org40395342013-02-21 14:57:23 +0000728 input_api.logging.info('Running pylint on %d files', len(files))
729 input_api.logging.debug('Running pylint on: %s', files)
chrisha@google.com267d6592012-06-19 19:23:31 +0000730 env = input_api.environ.copy()
ilevy@chromium.org7b677f72013-01-07 18:49:26 +0000731 env['PYTHONPATH'] = input_api.os_path.pathsep.join(
Robert Iannucci82a64802018-03-23 16:40:16 -0700732 extra_paths_list).encode('utf8')
Robert Iannucci23bd7352018-03-23 11:51:40 -0700733 env.pop('VPYTHON_CLEAR_PYTHONPATH', None)
Robert Iannucci82a64802018-03-23 16:40:16 -0700734 input_api.logging.debug(' with extra PYTHONPATH: %r', extra_paths_list)
chrisha@google.com267d6592012-06-19 19:23:31 +0000735
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000736 def GetPylintCmd(flist, extra, parallel):
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000737 # Windows needs help running python files so we explicitly specify
738 # the interpreter to use. It also has limitations on the size of
739 # the command-line, so we pass arguments via a pipe.
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000740 cmd = [input_api.python_executable,
741 input_api.os_path.join(_HERE, 'third_party', 'pylint.py'),
742 '--args-on-stdin']
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000743 if len(flist) == 1:
744 description = flist[0]
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000745 else:
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000746 description = '%s files' % len(flist)
chrisha@chromium.org1b129e52012-06-22 17:08:11 +0000747
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000748 args = extra_args[:]
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000749 if extra:
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000750 args.extend(extra)
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000751 description += ' using %s' % (extra,)
752 if parallel:
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000753 args.append('--jobs=%s' % input_api.cpu_count)
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000754 description += ' on %d cores' % input_api.cpu_count
755
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000756 return input_api.Command(
757 name='Pylint (%s)' % description,
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000758 cmd=cmd,
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000759 kwargs={'env': env, 'stdin': '\n'.join(args + flist)},
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000760 message=error_type)
chrisha@chromium.org1b129e52012-06-22 17:08:11 +0000761
sbc@chromium.org7fc75ca2012-09-21 20:14:21 +0000762 # Always run pylint and pass it all the py files at once.
763 # Passing py files one at time is slower and can produce
764 # different results. input_api.verbose used to be used
765 # to enable this behaviour but differing behaviour in
766 # verbose mode is not desirable.
ilevy@chromium.org7b677f72013-01-07 18:49:26 +0000767 # Leave this unreachable code in here so users can make
768 # a quick local edit to diagnose pylint issues more
769 # easily.
sbc@chromium.org7fc75ca2012-09-21 20:14:21 +0000770 if True:
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000771 # pylint's cycle detection doesn't work in parallel, so spawn a second,
772 # single-threaded job for just that check.
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000773
774 # Some PRESUBMITs explicitly mention cycle detection.
775 if not any('R0401' in a or 'cyclic-import' in a for a in extra_args):
776 return [
777 GetPylintCmd(files, ["--disable=cyclic-import"], True),
778 GetPylintCmd(files, ["--disable=all", "--enable=cyclic-import"], False)
779 ]
780 else:
781 return [ GetPylintCmd(files, [], True) ]
782
chrisha@google.com267d6592012-06-19 19:23:31 +0000783 else:
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000784 return map(lambda x: GetPylintCmd([x], [], 1), files)
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000785
786
787def RunPylint(input_api, *args, **kwargs):
788 """Legacy presubmit function.
789
790 For better performance, get all tests and then pass to
791 input_api.RunTests.
792 """
793 return input_api.RunTests(GetPylint(input_api, *args, **kwargs), False)
maruel@chromium.orge94aedc2010-12-13 21:11:30 +0000794
maruel@chromium.orgade9c592011-04-07 15:59:11 +0000795
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000796def CheckBuildbotPendingBuilds(input_api, output_api, url, max_pendings,
797 ignored):
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000798 try:
799 connection = input_api.urllib2.urlopen(url)
800 raw_data = connection.read()
801 connection.close()
802 except IOError:
803 return [output_api.PresubmitNotifyResult('%s is not accessible' % url)]
804
805 try:
806 data = input_api.json.loads(raw_data)
807 except ValueError:
808 return [output_api.PresubmitNotifyResult('Received malformed json while '
809 'looking up buildbot status')]
810
811 out = []
812 for (builder_name, builder) in data.iteritems():
813 if builder_name in ignored:
814 continue
maruel@chromium.orgcf1982c2010-10-04 15:08:28 +0000815 if builder.get('state', '') == 'offline':
816 continue
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000817 pending_builds_len = len(builder.get('pending_builds', []))
818 if pending_builds_len > max_pendings:
819 out.append('%s has %d build(s) pending' %
820 (builder_name, pending_builds_len))
821 if out:
822 return [output_api.PresubmitPromptWarning(
823 'Build(s) pending. It is suggested to wait that no more than %d '
824 'builds are pending.' % max_pendings,
825 long_text='\n'.join(out))]
826 return []
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000827
828
Edward Lesmes067ef5d2018-04-19 17:54:45 -0400829def CheckOwnersFormat(input_api, output_api):
830 affected_files = set([
831 f.LocalPath()
832 for f in input_api.change.AffectedFiles()
833 if 'OWNERS' in f.LocalPath() and f.Action() != 'D'
834 ])
835 if not affected_files:
836 return []
837 try:
838 input_api.owners_db.load_data_needed_for(affected_files)
839 return []
840 except Exception as e:
841 return [output_api.PresubmitError(
842 'Error parsing OWNERS files:\n%s' % e)]
843
844
dpranke@chromium.orgbce925d2015-01-16 22:38:38 +0000845def CheckOwners(input_api, output_api, source_file_filter=None):
Aaron Gable8678d322018-04-02 13:28:19 -0700846 affected_files = set([f.LocalPath() for f in
847 input_api.change.AffectedFiles(file_filter=source_file_filter)])
848
pam@chromium.orgf46aed92012-03-08 09:18:17 +0000849 if input_api.is_committing:
Aaron Gable8678d322018-04-02 13:28:19 -0700850 if input_api.tbr and not any(['OWNERS' in name for name in affected_files]):
pam@chromium.orgf46aed92012-03-08 09:18:17 +0000851 return [output_api.PresubmitNotifyResult(
852 '--tbr was specified, skipping OWNERS check')]
Clemens Hammacherd0c226a2017-01-16 14:09:52 +0100853 needed = 'LGTM from an OWNER'
854 output_fn = output_api.PresubmitError
rmistry@google.com5fc6c8c2015-04-16 16:38:43 +0000855 if input_api.change.issue:
tandrii@chromium.org9dea2ac2016-04-28 06:26:20 +0000856 if input_api.dry_run:
Clemens Hammacherd0c226a2017-01-16 14:09:52 +0100857 output_fn = lambda text: output_api.PresubmitNotifyResult(
858 'This is a dry run, but these failures would be reported on ' +
859 'commit:\n' + text)
rmistry@google.com5fc6c8c2015-04-16 16:38:43 +0000860 else:
Andrii Shyshkalov63560ad2018-02-09 19:09:54 -0800861 return [output_api.PresubmitError(
862 'OWNERS check failed: this CL has no Gerrit change number, '
863 'so we can\'t check it for approvals.')]
pam@chromium.orgf46aed92012-03-08 09:18:17 +0000864 else:
865 needed = 'OWNER reviewers'
Clemens Hammacherd0c226a2017-01-16 14:09:52 +0100866 output_fn = output_api.PresubmitNotifyResult
dpranke@chromium.org3e331bd2011-03-24 23:13:04 +0000867
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000868 owners_db = input_api.owners_db
Jochen Eisingerd0573ec2017-04-13 10:55:06 +0200869 owners_db.override_files = input_api.change.OriginalOwnersFiles()
tandrii@chromium.org830dc0b2016-05-09 06:26:34 +0000870 owner_email, reviewers = GetCodereviewOwnerAndReviewers(
pam@chromium.orgf46aed92012-03-08 09:18:17 +0000871 input_api,
872 owners_db.email_regexp,
873 approval_needed=input_api.is_committing)
874
dpranke@chromium.orgf6ddfa42013-03-05 21:06:03 +0000875 owner_email = owner_email or input_api.change.author_email
876
Edward Lemur707d70b2018-02-07 00:50:14 +0100877 finder = input_api.owners_finder(
878 affected_files, input_api.change.RepositoryRoot(),
879 owner_email, reviewers, fopen=file, os_path=input_api.os_path,
880 email_postfix='', disable_color=True,
881 override_files=input_api.change.OriginalOwnersFiles())
882 missing_files = finder.unreviewed_files
maruel@chromium.orge4067ab2011-06-03 01:07:35 +0000883
dpranke@chromium.org6b1e3ee2013-02-23 00:06:38 +0000884 if missing_files:
zork@chromium.org046e1752012-05-07 05:56:12 +0000885 output_list = [
Clemens Hammacherd0c226a2017-01-16 14:09:52 +0100886 output_fn('Missing %s for these files:\n %s' %
887 (needed, '\n '.join(sorted(missing_files))))]
zork@chromium.org046e1752012-05-07 05:56:12 +0000888 if not input_api.is_committing:
dpranke@chromium.org31b42c02013-09-19 19:24:15 +0000889 suggested_owners = owners_db.reviewers_for(missing_files, owner_email)
Jochen Eisinger76f5fc62017-04-07 16:27:46 +0200890 owners_with_comments = []
891 def RecordComments(text):
892 owners_with_comments.append(finder.print_indent() + text)
893 finder.writeln = RecordComments
894 for owner in suggested_owners:
895 finder.print_comments(owner)
Clemens Hammacherd0c226a2017-01-16 14:09:52 +0100896 output_list.append(output_fn('Suggested OWNERS: ' +
scheib@chromium.org93276ab2013-10-14 23:55:32 +0000897 '(Use "git-cl owners" to interactively select owners.)\n %s' %
Jochen Eisinger76f5fc62017-04-07 16:27:46 +0200898 ('\n '.join(owners_with_comments))))
zork@chromium.org046e1752012-05-07 05:56:12 +0000899 return output_list
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000900
pam@chromium.orgf46aed92012-03-08 09:18:17 +0000901 if input_api.is_committing and not reviewers:
Clemens Hammacherd0c226a2017-01-16 14:09:52 +0100902 return [output_fn('Missing LGTM from someone other than %s' % owner_email)]
dpranke@chromium.org3e331bd2011-03-24 23:13:04 +0000903 return []
dpranke@chromium.org627ea672011-03-11 23:29:03 +0000904
Aaron Gable668c1d82018-04-03 10:19:16 -0700905
tandrii@chromium.org830dc0b2016-05-09 06:26:34 +0000906def GetCodereviewOwnerAndReviewers(input_api, email_regexp, approval_needed):
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000907 """Return the owner and reviewers of a change, if any.
908
909 If approval_needed is True, only reviewers who have approved the change
910 will be returned.
911 """
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000912 issue = input_api.change.issue
913 if not issue:
tandrii@chromium.org830dc0b2016-05-09 06:26:34 +0000914 return None, (set() if approval_needed else
915 _ReviewersFromChange(input_api.change))
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000916
917 owner_email = input_api.gerrit.GetChangeOwner(issue)
918 reviewers = set(
919 r for r in input_api.gerrit.GetChangeReviewers(issue, approval_needed)
920 if _match_reviewer_email(r, owner_email, email_regexp))
tandrii81665dc2016-08-29 09:16:19 -0700921 input_api.logging.debug('owner: %s; approvals given by: %s',
922 owner_email, ', '.join(sorted(reviewers)))
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000923 return owner_email, reviewers
924
925
Aaron Gable668c1d82018-04-03 10:19:16 -0700926def _ReviewersFromChange(change):
927 """Return the reviewers specified in the |change|, if any."""
928 reviewers = set()
929 reviewers.update(change.ReviewersFromDescription())
930 reviewers.update(change.TBRsFromDescription())
931
932 # Drop reviewers that aren't specified in email address format.
933 return set(reviewer for reviewer in reviewers if '@' in reviewer)
934
935
936def _match_reviewer_email(r, owner_email, email_regexp):
937 return email_regexp.match(r) and r != owner_email
938
939
bauerb@chromium.org4cea01e2012-03-20 19:49:05 +0000940def CheckSingletonInHeaders(input_api, output_api, source_file_filter=None):
glider@chromium.orgc3617f32015-02-19 16:33:33 +0000941 """Deprecated, must be removed."""
942 return [
943 output_api.PresubmitNotifyResult(
944 'CheckSingletonInHeaders is deprecated, please remove it.')
945 ]
bradnelson@google.com56e48bc2011-03-24 20:51:21 +0000946
947
948def PanProjectChecks(input_api, output_api,
949 excluded_paths=None, text_files=None,
bradnelson@google.comd57771d2011-03-31 19:18:32 +0000950 license_header=None, project_name=None,
jamesr@chromium.orgaf27f462013-04-04 21:44:22 +0000951 owners_check=True, maxlen=80):
bradnelson@google.com56e48bc2011-03-24 20:51:21 +0000952 """Checks that ALL chromium orbit projects should use.
953
954 These are checks to be run on all Chromium orbit project, including:
955 Chromium
956 Native Client
957 V8
958 When you update this function, please take this broad scope into account.
959 Args:
960 input_api: Bag of input related interfaces.
961 output_api: Bag of output related interfaces.
962 excluded_paths: Don't include these paths in common checks.
963 text_files: Which file are to be treated as documentation text files.
964 license_header: What license header should be on files.
965 project_name: What is the name of the project as it appears in the license.
966 Returns:
967 A list of warning or error objects.
968 """
maruel@chromium.org69eaecb2011-06-14 13:09:13 +0000969 excluded_paths = tuple(excluded_paths or [])
970 text_files = tuple(text_files or (
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000971 r'.+\.txt$',
972 r'.+\.json$',
maruel@chromium.org69eaecb2011-06-14 13:09:13 +0000973 ))
bradnelson@google.com56e48bc2011-03-24 20:51:21 +0000974 project_name = project_name or 'Chromium'
mark@chromium.org2cc66422012-08-07 21:22:32 +0000975
976 # Accept any year number from 2006 to the current year, or the special
rvargas@chromium.org02652602014-03-14 19:43:20 +0000977 # 2006-20xx string used on the oldest files. 2006-20xx is deprecated, but
978 # tolerated on old files.
mark@chromium.org2cc66422012-08-07 21:22:32 +0000979 current_year = int(input_api.time.strftime('%Y'))
980 allowed_years = (str(s) for s in reversed(xrange(2006, current_year + 1)))
rvargas@chromium.org02652602014-03-14 19:43:20 +0000981 years_re = '(' + '|'.join(allowed_years) + '|2006-2008|2006-2009|2006-2010)'
mark@chromium.org2cc66422012-08-07 21:22:32 +0000982
983 # The (c) is deprecated, but tolerate it until it's removed from all files.
bradnelson@google.com56e48bc2011-03-24 20:51:21 +0000984 license_header = license_header or (
mark@chromium.org2cc66422012-08-07 21:22:32 +0000985 r'.*? Copyright (\(c\) )?%(year)s The %(project)s Authors\. '
bradnelson@google.com56e48bc2011-03-24 20:51:21 +0000986 r'All rights reserved\.\n'
987 r'.*? Use of this source code is governed by a BSD-style license that '
988 r'can be\n'
dbeam@chromium.orgb2312102012-02-15 02:01:55 +0000989 r'.*? found in the LICENSE file\.(?: \*/)?\n'
bradnelson@google.com56e48bc2011-03-24 20:51:21 +0000990 ) % {
mark@chromium.org2cc66422012-08-07 21:22:32 +0000991 'year': years_re,
bradnelson@google.com56e48bc2011-03-24 20:51:21 +0000992 'project': project_name,
993 }
994
995 results = []
996 # This code loads the default black list (e.g. third_party, experimental, etc)
997 # and add our black list (breakpad, skia and v8 are still not following
998 # google style and are not really living this repository).
999 # See presubmit_support.py InputApi.FilterSourceFile for the (simple) usage.
1000 black_list = input_api.DEFAULT_BLACK_LIST + excluded_paths
1001 white_list = input_api.DEFAULT_WHITE_LIST + text_files
1002 sources = lambda x: input_api.FilterSourceFile(x, black_list=black_list)
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +00001003 text_files = lambda x: input_api.FilterSourceFile(
1004 x, black_list=black_list, white_list=white_list)
nick@chromium.orge06cb4e2011-04-23 01:20:38 +00001005
1006 snapshot_memory = []
1007 def snapshot(msg):
1008 """Measures & prints performance warning if a rule is running slow."""
1009 dt2 = input_api.time.clock()
1010 if snapshot_memory:
1011 delta_ms = int(1000*(dt2 - snapshot_memory[0]))
1012 if delta_ms > 500:
1013 print " %s took a long time: %dms" % (snapshot_memory[1], delta_ms)
1014 snapshot_memory[:] = (dt2, msg)
1015
bradnelson@google.comd57771d2011-03-31 19:18:32 +00001016 if owners_check:
nick@chromium.orge06cb4e2011-04-23 01:20:38 +00001017 snapshot("checking owners")
bradnelson@google.comd57771d2011-03-31 19:18:32 +00001018 results.extend(input_api.canned_checks.CheckOwners(
Dirk Pranke3c86cee2017-01-23 22:02:06 -08001019 input_api, output_api, source_file_filter=None))
bradnelson@google.com56e48bc2011-03-24 20:51:21 +00001020
nick@chromium.orge06cb4e2011-04-23 01:20:38 +00001021 snapshot("checking long lines")
bradnelson@google.com56e48bc2011-03-24 20:51:21 +00001022 results.extend(input_api.canned_checks.CheckLongLines(
jamesr@chromium.orgaf27f462013-04-04 21:44:22 +00001023 input_api, output_api, maxlen, source_file_filter=sources))
nick@chromium.orge06cb4e2011-04-23 01:20:38 +00001024 snapshot( "checking tabs")
bradnelson@google.com56e48bc2011-03-24 20:51:21 +00001025 results.extend(input_api.canned_checks.CheckChangeHasNoTabs(
1026 input_api, output_api, source_file_filter=sources))
nick@chromium.orge06cb4e2011-04-23 01:20:38 +00001027 snapshot( "checking stray whitespace")
bradnelson@google.com56e48bc2011-03-24 20:51:21 +00001028 results.extend(input_api.canned_checks.CheckChangeHasNoStrayWhitespace(
1029 input_api, output_api, source_file_filter=sources))
phajdan.jr@chromium.orgd965db32015-11-16 09:46:56 +00001030 snapshot("checking license")
1031 results.extend(input_api.canned_checks.CheckLicense(
1032 input_api, output_api, license_header, source_file_filter=sources))
maruel@chromium.orgb1ce7752011-05-08 13:50:16 +00001033
maruel@chromium.orgb1ce7752011-05-08 13:50:16 +00001034 if input_api.is_committing:
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001035 snapshot("checking was uploaded")
1036 results.extend(input_api.canned_checks.CheckChangeWasUploaded(
1037 input_api, output_api))
ilevy@chromium.orgc50d7612012-12-05 04:25:14 +00001038 snapshot("checking description")
1039 results.extend(input_api.canned_checks.CheckChangeHasDescription(
1040 input_api, output_api))
1041 results.extend(input_api.canned_checks.CheckDoNotSubmitInDescription(
1042 input_api, output_api))
ilevy@chromium.orgc50d7612012-12-05 04:25:14 +00001043 snapshot("checking do not submit in files")
1044 results.extend(input_api.canned_checks.CheckDoNotSubmitInFiles(
1045 input_api, output_api))
nick@chromium.orge06cb4e2011-04-23 01:20:38 +00001046 snapshot("done")
bradnelson@google.com56e48bc2011-03-24 20:51:21 +00001047 return results
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00001048
1049
Christopher Lamc5ba6922017-01-24 11:19:14 +11001050def CheckPatchFormatted(input_api, output_api, check_js=False):
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00001051 import git_cl
Andrew Grieved86c8032017-09-26 14:40:36 -04001052 cmd = ['-C', input_api.change.RepositoryRoot(),
1053 'cl', 'format', '--dry-run', '--presubmit']
Christopher Lamc5ba6922017-01-24 11:19:14 +11001054 if check_js:
1055 cmd.append('--js')
Andrew Grieved86c8032017-09-26 14:40:36 -04001056 presubmit_subdir = input_api.os_path.relpath(
1057 input_api.PresubmitLocalPath(), input_api.change.RepositoryRoot())
1058 if presubmit_subdir.startswith('..') or presubmit_subdir == '.':
1059 presubmit_subdir = ''
1060 # If the PRESUBMIT.py is in a parent repository, then format the entire
1061 # subrepository. Otherwise, format only the code in the directory that
1062 # contains the PRESUBMIT.py.
1063 if presubmit_subdir:
1064 cmd.append(input_api.PresubmitLocalPath())
enne@chromium.org555cfe42014-01-29 18:21:39 +00001065 code, _ = git_cl.RunGitWithCode(cmd, suppress_stderr=True)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00001066 if code == 2:
Andrew Grieved3b25482017-10-13 16:06:25 -04001067 if presubmit_subdir:
1068 short_path = presubmit_subdir
1069 else:
1070 short_path = input_api.basename(input_api.change.RepositoryRoot())
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00001071 return [output_api.PresubmitPromptWarning(
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00001072 'The %s directory requires source formatting. '
Andrew Grieved86c8032017-09-26 14:40:36 -04001073 'Please run: git cl format %s%s' %
1074 (short_path, '--js ' if check_js else '', presubmit_subdir))]
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00001075 # As this is just a warning, ignore all other errors if the user
1076 # happens to have a broken clang-format, doesn't use git, etc etc.
1077 return []
scottmg@chromium.orgd05ab352014-12-05 17:24:26 +00001078
1079
1080def CheckGNFormatted(input_api, output_api):
1081 import gn
1082 affected_files = input_api.AffectedFiles(
1083 include_deletes=False,
1084 file_filter=lambda x: x.LocalPath().endswith('.gn') or
kylechar58edce22016-06-17 06:07:51 -07001085 x.LocalPath().endswith('.gni') or
1086 x.LocalPath().endswith('.typemap'))
scottmg@chromium.orgd05ab352014-12-05 17:24:26 +00001087 warnings = []
1088 for f in affected_files:
1089 cmd = ['gn', 'format', '--dry-run', f.AbsoluteLocalPath()]
1090 rc = gn.main(cmd)
1091 if rc == 2:
1092 warnings.append(output_api.PresubmitPromptWarning(
brettw4b8ed592016-08-05 16:19:12 -07001093 '%s requires formatting. Please run:\n gn format %s' % (
scottmg@chromium.orgd05ab352014-12-05 17:24:26 +00001094 f.AbsoluteLocalPath(), f.LocalPath())))
1095 # It's just a warning, so ignore other types of failures assuming they'll be
1096 # caught elsewhere.
1097 return warnings
Dan Jacques94652a32017-10-09 23:18:46 -04001098
1099
1100def CheckCIPDManifest(input_api, output_api, path=None, content=None):
1101 """Verifies that a CIPD ensure file manifest is valid against all platforms.
1102
1103 Exactly one of "path" or "content" must be provided. An assertion will occur
1104 if neither or both are provided.
1105
1106 Args:
1107 path (str): If provided, the filesystem path to the manifest to verify.
1108 content (str): If provided, the raw content of the manifest to veirfy.
1109 """
1110 cipd_bin = 'cipd' if not input_api.is_windows else 'cipd.bat'
1111 cmd = [cipd_bin, 'ensure-file-verify']
1112 kwargs = {}
1113
1114 if input_api.is_windows:
1115 # Needs to be able to resolve "cipd.bat".
1116 kwargs['shell'] = True
1117
1118 if input_api.verbose:
1119 cmd += ['-log-level', 'debug']
1120
1121 if path:
1122 assert content is None, 'Cannot provide both "path" and "content".'
1123 cmd += ['-ensure-file', path]
Robert Iannucci7d47eb52017-12-06 12:18:18 -08001124 name = 'Check CIPD manifest %r' % path
Dan Jacques94652a32017-10-09 23:18:46 -04001125 elif content:
1126 assert path is None, 'Cannot provide both "path" and "content".'
1127 cmd += ['-ensure-file=-']
1128 kwargs['stdin'] = content
Robert Iannucci7d47eb52017-12-06 12:18:18 -08001129 # quick and dirty parser to extract checked packages.
1130 packages = [
1131 l.split()[0] for l in (ll.strip() for ll in content.splitlines())
1132 if ' ' in l and not l.startswith('$')
1133 ]
1134 name = 'Check CIPD packages from string: %r' % (packages,)
Dan Jacques94652a32017-10-09 23:18:46 -04001135 else:
1136 raise Exception('Exactly one of "path" or "content" must be provided.')
1137
1138 return input_api.Command(
Robert Iannucci7d47eb52017-12-06 12:18:18 -08001139 name,
Dan Jacques94652a32017-10-09 23:18:46 -04001140 cmd,
1141 kwargs,
1142 output_api.PresubmitError)
1143
1144
1145def CheckCIPDPackages(input_api, output_api, platforms, packages):
1146 """Verifies that all named CIPD packages can be resolved against all supplied
1147 platforms.
1148
1149 Args:
1150 platforms (list): List of CIPD platforms to verify.
1151 packages (dict): Mapping of package name to version.
1152 """
1153 manifest = []
1154 for p in platforms:
1155 manifest.append('$VerifiedPlatform %s' % (p,))
1156 for k, v in packages.iteritems():
1157 manifest.append('%s %s' % (k, v))
1158 return CheckCIPDManifest(input_api, output_api, content='\n'.join(manifest))
Sergiy Byelozyoroveba83472017-10-27 02:08:21 +02001159
1160
1161def CheckVPythonSpec(input_api, output_api, file_filter=None):
1162 """Validates any changed .vpython files with vpython verification tool.
1163
1164 Args:
1165 input_api: Bag of input related interfaces.
1166 output_api: Bag of output related interfaces.
1167 file_filter: Custom function that takes a path (relative to client root) and
1168 returns boolean, which is used to filter files for which to apply the
1169 verification to. Defaults to any path ending with .vpython, which captures
1170 both global .vpython and <script>.vpython files.
1171
1172 Returns:
1173 A list of input_api.Command objects containing verification commands.
1174 """
1175 file_filter = file_filter or (lambda f: f.LocalPath().endswith('.vpython'))
John Budorick16162372018-04-18 10:39:53 -07001176 affected_files = input_api.AffectedTestableFiles(file_filter=file_filter)
Sergiy Byelozyoroveba83472017-10-27 02:08:21 +02001177 affected_files = map(lambda f: f.AbsoluteLocalPath(), affected_files)
1178
1179 commands = []
1180 for f in affected_files:
1181 commands.append(input_api.Command(
1182 'Verify %s' % f,
1183 ['vpython', '-vpython-spec', f, '-vpython-tool', 'verify'],
1184 {'stderr': input_api.subprocess.STDOUT},
1185 output_api.PresubmitError))
1186
1187 return commands
Mun Yong Jang17995a92017-12-01 16:00:31 -08001188
1189
1190def CheckChangedLUCIConfigs(input_api, output_api):
1191 import collections
1192 import base64
1193 import json
Mun Yong Jangcfb9a232017-12-18 10:39:34 -08001194 import logging
Mun Yong Jang17995a92017-12-01 16:00:31 -08001195 import urllib2
1196
1197 import auth
1198 import git_cl
1199
1200 LUCI_CONFIG_HOST_NAME = 'luci-config.appspot.com'
1201
1202 cl = git_cl.Changelist()
Mun Yong Jang603d01e2017-12-19 16:38:30 -08001203 if input_api.change.issue and input_api.gerrit:
1204 remote_branch = input_api.gerrit.GetDestRef(input_api.change.issue)
1205 else:
1206 remote, remote_branch = cl.GetRemoteBranch()
1207 if remote_branch.startswith('refs/remotes/%s/' % remote):
1208 remote_branch = remote_branch.replace(
1209 'refs/remotes/%s/' % remote, 'refs/heads/', 1)
1210 if remote_branch.startswith('refs/remotes/branch-heads/'):
1211 remote_branch = remote_branch.replace(
1212 'refs/remotes/branch-heads/', 'refs/branch-heads/', 1)
Mun Yong Jang17995a92017-12-01 16:00:31 -08001213
1214 remote_host_url = cl.GetRemoteUrl()
1215 if not remote_host_url:
1216 return [output_api.PresubmitError(
1217 'Remote host url for git has not been defined')]
1218 remote_host_url = remote_host_url.rstrip('/')
1219 if remote_host_url.endswith('.git'):
1220 remote_host_url = remote_host_url[:-len('.git')]
1221
1222 # authentication
1223 try:
1224 authenticator = auth.get_authenticator_for_host(
1225 LUCI_CONFIG_HOST_NAME, auth.make_auth_config())
1226 acc_tkn = authenticator.get_access_token()
1227 except auth.AuthenticationError as e:
1228 return [output_api.PresubmitError(
1229 'Error in authenticating user.', long_text=str(e))]
1230
1231 def request(endpoint, body=None):
1232 api_url = ('https://%s/_ah/api/config/v1/%s'
1233 % (LUCI_CONFIG_HOST_NAME, endpoint))
1234 req = urllib2.Request(api_url)
1235 req.add_header('Authorization', 'Bearer %s' % acc_tkn.token)
1236 if body is not None:
1237 req.add_header('Content-Type', 'application/json')
1238 req.add_data(json.dumps(body))
1239 return json.load(urllib2.urlopen(req))
1240
1241 try:
1242 config_sets = request('config-sets').get('config_sets')
1243 except urllib2.HTTPError as e:
1244 return [output_api.PresubmitError(
1245 'Config set request to luci-config failed', long_text=str(e))]
1246 if not config_sets:
1247 return [output_api.PresubmitWarning('No config_sets were returned')]
1248 loc_pref = '%s/+/%s/' % (remote_host_url, remote_branch)
Mun Yong Jangcfb9a232017-12-18 10:39:34 -08001249 logging.debug('Derived location prefix: %s', loc_pref)
Mun Yong Jang17995a92017-12-01 16:00:31 -08001250 dir_to_config_set = {
1251 '%s/' % cs['location'][len(loc_pref):].rstrip('/'): cs['config_set']
1252 for cs in config_sets
1253 if cs['location'].startswith(loc_pref) or
1254 ('%s/' % cs['location']) == loc_pref
1255 }
1256 cs_to_files = collections.defaultdict(list)
1257 for f in input_api.AffectedFiles():
1258 # windows
1259 file_path = f.LocalPath().replace(_os.sep, '/')
Mun Yong Jangcfb9a232017-12-18 10:39:34 -08001260 logging.debug('Affected file path: %s', file_path)
Mun Yong Jang17995a92017-12-01 16:00:31 -08001261 for dr, cs in dir_to_config_set.iteritems():
1262 if dr == '/' or file_path.startswith(dr):
1263 cs_to_files[cs].append({
1264 'path': file_path[len(dr):] if dr != '/' else file_path,
1265 'content': base64.b64encode(
1266 '\n'.join(f.NewContents()).encode('utf-8'))
1267 })
1268 outputs = []
1269 for cs, f in cs_to_files.iteritems():
1270 try:
1271 # TODO(myjang): parallelize
1272 res = request(
1273 'validate-config', body={'config_set': cs, 'files': f})
1274 except urllib2.HTTPError as e:
1275 return [output_api.PresubmitError(
1276 'Validation request to luci-config failed', long_text=str(e))]
1277 for msg in res.get('messages', []):
1278 sev = msg['severity']
1279 if sev == 'WARNING':
1280 out_f = output_api.PresubmitPromptWarning
1281 elif sev == 'ERROR' or sev == 'CRITICAL':
1282 out_f = output_api.PresubmitError
1283 else:
1284 out_f = output_api.PresubmitNotifyResult
1285 outputs.append(out_f('Config validation: %s' % msg['text']))
1286 return outputs