blob: fb7e6c0ea6d5cb18c07402a3219f4d80039a6c74 [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)])
Jeremy Roman5ae86d22018-04-26 15:36:02 -0400848 affects_owners = any('OWNERS' in name for name in affected_files)
Aaron Gable8678d322018-04-02 13:28:19 -0700849
pam@chromium.orgf46aed92012-03-08 09:18:17 +0000850 if input_api.is_committing:
Jeremy Roman5ae86d22018-04-26 15:36:02 -0400851 if input_api.tbr and not affects_owners:
pam@chromium.orgf46aed92012-03-08 09:18:17 +0000852 return [output_api.PresubmitNotifyResult(
853 '--tbr was specified, skipping OWNERS check')]
Clemens Hammacherd0c226a2017-01-16 14:09:52 +0100854 needed = 'LGTM from an OWNER'
855 output_fn = output_api.PresubmitError
rmistry@google.com5fc6c8c2015-04-16 16:38:43 +0000856 if input_api.change.issue:
tandrii@chromium.org9dea2ac2016-04-28 06:26:20 +0000857 if input_api.dry_run:
Clemens Hammacherd0c226a2017-01-16 14:09:52 +0100858 output_fn = lambda text: output_api.PresubmitNotifyResult(
859 'This is a dry run, but these failures would be reported on ' +
860 'commit:\n' + text)
rmistry@google.com5fc6c8c2015-04-16 16:38:43 +0000861 else:
Andrii Shyshkalov63560ad2018-02-09 19:09:54 -0800862 return [output_api.PresubmitError(
863 'OWNERS check failed: this CL has no Gerrit change number, '
864 'so we can\'t check it for approvals.')]
pam@chromium.orgf46aed92012-03-08 09:18:17 +0000865 else:
866 needed = 'OWNER reviewers'
Clemens Hammacherd0c226a2017-01-16 14:09:52 +0100867 output_fn = output_api.PresubmitNotifyResult
dpranke@chromium.org3e331bd2011-03-24 23:13:04 +0000868
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000869 owners_db = input_api.owners_db
Jochen Eisingerd0573ec2017-04-13 10:55:06 +0200870 owners_db.override_files = input_api.change.OriginalOwnersFiles()
tandrii@chromium.org830dc0b2016-05-09 06:26:34 +0000871 owner_email, reviewers = GetCodereviewOwnerAndReviewers(
pam@chromium.orgf46aed92012-03-08 09:18:17 +0000872 input_api,
873 owners_db.email_regexp,
874 approval_needed=input_api.is_committing)
875
dpranke@chromium.orgf6ddfa42013-03-05 21:06:03 +0000876 owner_email = owner_email or input_api.change.author_email
877
Edward Lemur707d70b2018-02-07 00:50:14 +0100878 finder = input_api.owners_finder(
879 affected_files, input_api.change.RepositoryRoot(),
880 owner_email, reviewers, fopen=file, os_path=input_api.os_path,
881 email_postfix='', disable_color=True,
882 override_files=input_api.change.OriginalOwnersFiles())
883 missing_files = finder.unreviewed_files
maruel@chromium.orge4067ab2011-06-03 01:07:35 +0000884
dpranke@chromium.org6b1e3ee2013-02-23 00:06:38 +0000885 if missing_files:
zork@chromium.org046e1752012-05-07 05:56:12 +0000886 output_list = [
Clemens Hammacherd0c226a2017-01-16 14:09:52 +0100887 output_fn('Missing %s for these files:\n %s' %
888 (needed, '\n '.join(sorted(missing_files))))]
Jeremy Roman5ae86d22018-04-26 15:36:02 -0400889 if input_api.tbr and affects_owners:
890 output_list.append(output_fn('Note that TBR does not apply to changes '
891 'that affect OWNERS files.'))
zork@chromium.org046e1752012-05-07 05:56:12 +0000892 if not input_api.is_committing:
dpranke@chromium.org31b42c02013-09-19 19:24:15 +0000893 suggested_owners = owners_db.reviewers_for(missing_files, owner_email)
Jochen Eisinger76f5fc62017-04-07 16:27:46 +0200894 owners_with_comments = []
895 def RecordComments(text):
896 owners_with_comments.append(finder.print_indent() + text)
897 finder.writeln = RecordComments
898 for owner in suggested_owners:
899 finder.print_comments(owner)
Clemens Hammacherd0c226a2017-01-16 14:09:52 +0100900 output_list.append(output_fn('Suggested OWNERS: ' +
scheib@chromium.org93276ab2013-10-14 23:55:32 +0000901 '(Use "git-cl owners" to interactively select owners.)\n %s' %
Jochen Eisinger76f5fc62017-04-07 16:27:46 +0200902 ('\n '.join(owners_with_comments))))
zork@chromium.org046e1752012-05-07 05:56:12 +0000903 return output_list
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000904
pam@chromium.orgf46aed92012-03-08 09:18:17 +0000905 if input_api.is_committing and not reviewers:
Clemens Hammacherd0c226a2017-01-16 14:09:52 +0100906 return [output_fn('Missing LGTM from someone other than %s' % owner_email)]
dpranke@chromium.org3e331bd2011-03-24 23:13:04 +0000907 return []
dpranke@chromium.org627ea672011-03-11 23:29:03 +0000908
Aaron Gable668c1d82018-04-03 10:19:16 -0700909
tandrii@chromium.org830dc0b2016-05-09 06:26:34 +0000910def GetCodereviewOwnerAndReviewers(input_api, email_regexp, approval_needed):
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000911 """Return the owner and reviewers of a change, if any.
912
913 If approval_needed is True, only reviewers who have approved the change
914 will be returned.
915 """
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000916 issue = input_api.change.issue
917 if not issue:
tandrii@chromium.org830dc0b2016-05-09 06:26:34 +0000918 return None, (set() if approval_needed else
919 _ReviewersFromChange(input_api.change))
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000920
921 owner_email = input_api.gerrit.GetChangeOwner(issue)
922 reviewers = set(
923 r for r in input_api.gerrit.GetChangeReviewers(issue, approval_needed)
924 if _match_reviewer_email(r, owner_email, email_regexp))
tandrii81665dc2016-08-29 09:16:19 -0700925 input_api.logging.debug('owner: %s; approvals given by: %s',
926 owner_email, ', '.join(sorted(reviewers)))
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000927 return owner_email, reviewers
928
929
Aaron Gable668c1d82018-04-03 10:19:16 -0700930def _ReviewersFromChange(change):
931 """Return the reviewers specified in the |change|, if any."""
932 reviewers = set()
933 reviewers.update(change.ReviewersFromDescription())
934 reviewers.update(change.TBRsFromDescription())
935
936 # Drop reviewers that aren't specified in email address format.
937 return set(reviewer for reviewer in reviewers if '@' in reviewer)
938
939
940def _match_reviewer_email(r, owner_email, email_regexp):
941 return email_regexp.match(r) and r != owner_email
942
943
bauerb@chromium.org4cea01e2012-03-20 19:49:05 +0000944def CheckSingletonInHeaders(input_api, output_api, source_file_filter=None):
glider@chromium.orgc3617f32015-02-19 16:33:33 +0000945 """Deprecated, must be removed."""
946 return [
947 output_api.PresubmitNotifyResult(
948 'CheckSingletonInHeaders is deprecated, please remove it.')
949 ]
bradnelson@google.com56e48bc2011-03-24 20:51:21 +0000950
951
952def PanProjectChecks(input_api, output_api,
953 excluded_paths=None, text_files=None,
bradnelson@google.comd57771d2011-03-31 19:18:32 +0000954 license_header=None, project_name=None,
jamesr@chromium.orgaf27f462013-04-04 21:44:22 +0000955 owners_check=True, maxlen=80):
bradnelson@google.com56e48bc2011-03-24 20:51:21 +0000956 """Checks that ALL chromium orbit projects should use.
957
958 These are checks to be run on all Chromium orbit project, including:
959 Chromium
960 Native Client
961 V8
962 When you update this function, please take this broad scope into account.
963 Args:
964 input_api: Bag of input related interfaces.
965 output_api: Bag of output related interfaces.
966 excluded_paths: Don't include these paths in common checks.
967 text_files: Which file are to be treated as documentation text files.
968 license_header: What license header should be on files.
969 project_name: What is the name of the project as it appears in the license.
970 Returns:
971 A list of warning or error objects.
972 """
maruel@chromium.org69eaecb2011-06-14 13:09:13 +0000973 excluded_paths = tuple(excluded_paths or [])
974 text_files = tuple(text_files or (
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000975 r'.+\.txt$',
976 r'.+\.json$',
maruel@chromium.org69eaecb2011-06-14 13:09:13 +0000977 ))
bradnelson@google.com56e48bc2011-03-24 20:51:21 +0000978 project_name = project_name or 'Chromium'
mark@chromium.org2cc66422012-08-07 21:22:32 +0000979
980 # Accept any year number from 2006 to the current year, or the special
rvargas@chromium.org02652602014-03-14 19:43:20 +0000981 # 2006-20xx string used on the oldest files. 2006-20xx is deprecated, but
982 # tolerated on old files.
mark@chromium.org2cc66422012-08-07 21:22:32 +0000983 current_year = int(input_api.time.strftime('%Y'))
984 allowed_years = (str(s) for s in reversed(xrange(2006, current_year + 1)))
rvargas@chromium.org02652602014-03-14 19:43:20 +0000985 years_re = '(' + '|'.join(allowed_years) + '|2006-2008|2006-2009|2006-2010)'
mark@chromium.org2cc66422012-08-07 21:22:32 +0000986
987 # The (c) is deprecated, but tolerate it until it's removed from all files.
bradnelson@google.com56e48bc2011-03-24 20:51:21 +0000988 license_header = license_header or (
mark@chromium.org2cc66422012-08-07 21:22:32 +0000989 r'.*? Copyright (\(c\) )?%(year)s The %(project)s Authors\. '
bradnelson@google.com56e48bc2011-03-24 20:51:21 +0000990 r'All rights reserved\.\n'
991 r'.*? Use of this source code is governed by a BSD-style license that '
992 r'can be\n'
dbeam@chromium.orgb2312102012-02-15 02:01:55 +0000993 r'.*? found in the LICENSE file\.(?: \*/)?\n'
bradnelson@google.com56e48bc2011-03-24 20:51:21 +0000994 ) % {
mark@chromium.org2cc66422012-08-07 21:22:32 +0000995 'year': years_re,
bradnelson@google.com56e48bc2011-03-24 20:51:21 +0000996 'project': project_name,
997 }
998
999 results = []
1000 # This code loads the default black list (e.g. third_party, experimental, etc)
1001 # and add our black list (breakpad, skia and v8 are still not following
1002 # google style and are not really living this repository).
1003 # See presubmit_support.py InputApi.FilterSourceFile for the (simple) usage.
1004 black_list = input_api.DEFAULT_BLACK_LIST + excluded_paths
1005 white_list = input_api.DEFAULT_WHITE_LIST + text_files
1006 sources = lambda x: input_api.FilterSourceFile(x, black_list=black_list)
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +00001007 text_files = lambda x: input_api.FilterSourceFile(
1008 x, black_list=black_list, white_list=white_list)
nick@chromium.orge06cb4e2011-04-23 01:20:38 +00001009
1010 snapshot_memory = []
1011 def snapshot(msg):
1012 """Measures & prints performance warning if a rule is running slow."""
1013 dt2 = input_api.time.clock()
1014 if snapshot_memory:
1015 delta_ms = int(1000*(dt2 - snapshot_memory[0]))
1016 if delta_ms > 500:
1017 print " %s took a long time: %dms" % (snapshot_memory[1], delta_ms)
1018 snapshot_memory[:] = (dt2, msg)
1019
Edward Lesmescb62e482018-04-19 18:29:35 -04001020 snapshot("checking owners files format")
1021 results.extend(input_api.canned_checks.CheckOwnersFormat(
1022 input_api, output_api))
1023
bradnelson@google.comd57771d2011-03-31 19:18:32 +00001024 if owners_check:
nick@chromium.orge06cb4e2011-04-23 01:20:38 +00001025 snapshot("checking owners")
bradnelson@google.comd57771d2011-03-31 19:18:32 +00001026 results.extend(input_api.canned_checks.CheckOwners(
Dirk Pranke3c86cee2017-01-23 22:02:06 -08001027 input_api, output_api, source_file_filter=None))
bradnelson@google.com56e48bc2011-03-24 20:51:21 +00001028
nick@chromium.orge06cb4e2011-04-23 01:20:38 +00001029 snapshot("checking long lines")
bradnelson@google.com56e48bc2011-03-24 20:51:21 +00001030 results.extend(input_api.canned_checks.CheckLongLines(
jamesr@chromium.orgaf27f462013-04-04 21:44:22 +00001031 input_api, output_api, maxlen, source_file_filter=sources))
nick@chromium.orge06cb4e2011-04-23 01:20:38 +00001032 snapshot( "checking tabs")
bradnelson@google.com56e48bc2011-03-24 20:51:21 +00001033 results.extend(input_api.canned_checks.CheckChangeHasNoTabs(
1034 input_api, output_api, source_file_filter=sources))
nick@chromium.orge06cb4e2011-04-23 01:20:38 +00001035 snapshot( "checking stray whitespace")
bradnelson@google.com56e48bc2011-03-24 20:51:21 +00001036 results.extend(input_api.canned_checks.CheckChangeHasNoStrayWhitespace(
1037 input_api, output_api, source_file_filter=sources))
phajdan.jr@chromium.orgd965db32015-11-16 09:46:56 +00001038 snapshot("checking license")
1039 results.extend(input_api.canned_checks.CheckLicense(
1040 input_api, output_api, license_header, source_file_filter=sources))
maruel@chromium.orgb1ce7752011-05-08 13:50:16 +00001041
maruel@chromium.orgb1ce7752011-05-08 13:50:16 +00001042 if input_api.is_committing:
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001043 snapshot("checking was uploaded")
1044 results.extend(input_api.canned_checks.CheckChangeWasUploaded(
1045 input_api, output_api))
ilevy@chromium.orgc50d7612012-12-05 04:25:14 +00001046 snapshot("checking description")
1047 results.extend(input_api.canned_checks.CheckChangeHasDescription(
1048 input_api, output_api))
1049 results.extend(input_api.canned_checks.CheckDoNotSubmitInDescription(
1050 input_api, output_api))
ilevy@chromium.orgc50d7612012-12-05 04:25:14 +00001051 snapshot("checking do not submit in files")
1052 results.extend(input_api.canned_checks.CheckDoNotSubmitInFiles(
1053 input_api, output_api))
nick@chromium.orge06cb4e2011-04-23 01:20:38 +00001054 snapshot("done")
bradnelson@google.com56e48bc2011-03-24 20:51:21 +00001055 return results
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00001056
1057
Nodir Turakulov0ae14e92018-05-31 14:53:53 -07001058def CheckPatchFormatted(
1059 input_api, output_api, check_js=False, check_python=False):
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00001060 import git_cl
Nodir Turakulov0ae14e92018-05-31 14:53:53 -07001061
1062 display_args = []
Christopher Lamc5ba6922017-01-24 11:19:14 +11001063 if check_js:
Nodir Turakulov0ae14e92018-05-31 14:53:53 -07001064 display_args.append('--js')
1065 if check_python:
1066 # --python requires --full
1067 display_args.extend(['--python', '--full'])
1068
1069 cmd = ['-C', input_api.change.RepositoryRoot(),
1070 'cl', 'format', '--dry-run', '--presubmit'] + display_args
Andrew Grieved86c8032017-09-26 14:40:36 -04001071 presubmit_subdir = input_api.os_path.relpath(
1072 input_api.PresubmitLocalPath(), input_api.change.RepositoryRoot())
1073 if presubmit_subdir.startswith('..') or presubmit_subdir == '.':
1074 presubmit_subdir = ''
1075 # If the PRESUBMIT.py is in a parent repository, then format the entire
1076 # subrepository. Otherwise, format only the code in the directory that
1077 # contains the PRESUBMIT.py.
1078 if presubmit_subdir:
1079 cmd.append(input_api.PresubmitLocalPath())
enne@chromium.org555cfe42014-01-29 18:21:39 +00001080 code, _ = git_cl.RunGitWithCode(cmd, suppress_stderr=True)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00001081 if code == 2:
Andrew Grieved3b25482017-10-13 16:06:25 -04001082 if presubmit_subdir:
1083 short_path = presubmit_subdir
1084 else:
1085 short_path = input_api.basename(input_api.change.RepositoryRoot())
Nodir Turakulov0ae14e92018-05-31 14:53:53 -07001086 display_args.append(presubmit_subdir)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00001087 return [output_api.PresubmitPromptWarning(
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00001088 'The %s directory requires source formatting. '
Nodir Turakulov0ae14e92018-05-31 14:53:53 -07001089 'Please run: git cl format %s' %
1090 (short_path, ' '.join(display_args)))]
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00001091 # As this is just a warning, ignore all other errors if the user
1092 # happens to have a broken clang-format, doesn't use git, etc etc.
1093 return []
scottmg@chromium.orgd05ab352014-12-05 17:24:26 +00001094
1095
1096def CheckGNFormatted(input_api, output_api):
1097 import gn
1098 affected_files = input_api.AffectedFiles(
1099 include_deletes=False,
1100 file_filter=lambda x: x.LocalPath().endswith('.gn') or
kylechar58edce22016-06-17 06:07:51 -07001101 x.LocalPath().endswith('.gni') or
1102 x.LocalPath().endswith('.typemap'))
scottmg@chromium.orgd05ab352014-12-05 17:24:26 +00001103 warnings = []
1104 for f in affected_files:
1105 cmd = ['gn', 'format', '--dry-run', f.AbsoluteLocalPath()]
1106 rc = gn.main(cmd)
1107 if rc == 2:
1108 warnings.append(output_api.PresubmitPromptWarning(
brettw4b8ed592016-08-05 16:19:12 -07001109 '%s requires formatting. Please run:\n gn format %s' % (
scottmg@chromium.orgd05ab352014-12-05 17:24:26 +00001110 f.AbsoluteLocalPath(), f.LocalPath())))
1111 # It's just a warning, so ignore other types of failures assuming they'll be
1112 # caught elsewhere.
1113 return warnings
Dan Jacques94652a32017-10-09 23:18:46 -04001114
1115
1116def CheckCIPDManifest(input_api, output_api, path=None, content=None):
1117 """Verifies that a CIPD ensure file manifest is valid against all platforms.
1118
1119 Exactly one of "path" or "content" must be provided. An assertion will occur
1120 if neither or both are provided.
1121
1122 Args:
1123 path (str): If provided, the filesystem path to the manifest to verify.
1124 content (str): If provided, the raw content of the manifest to veirfy.
1125 """
1126 cipd_bin = 'cipd' if not input_api.is_windows else 'cipd.bat'
1127 cmd = [cipd_bin, 'ensure-file-verify']
1128 kwargs = {}
1129
1130 if input_api.is_windows:
1131 # Needs to be able to resolve "cipd.bat".
1132 kwargs['shell'] = True
1133
1134 if input_api.verbose:
1135 cmd += ['-log-level', 'debug']
1136
1137 if path:
1138 assert content is None, 'Cannot provide both "path" and "content".'
1139 cmd += ['-ensure-file', path]
Robert Iannucci7d47eb52017-12-06 12:18:18 -08001140 name = 'Check CIPD manifest %r' % path
Dan Jacques94652a32017-10-09 23:18:46 -04001141 elif content:
1142 assert path is None, 'Cannot provide both "path" and "content".'
1143 cmd += ['-ensure-file=-']
1144 kwargs['stdin'] = content
Robert Iannucci7d47eb52017-12-06 12:18:18 -08001145 # quick and dirty parser to extract checked packages.
1146 packages = [
1147 l.split()[0] for l in (ll.strip() for ll in content.splitlines())
1148 if ' ' in l and not l.startswith('$')
1149 ]
1150 name = 'Check CIPD packages from string: %r' % (packages,)
Dan Jacques94652a32017-10-09 23:18:46 -04001151 else:
1152 raise Exception('Exactly one of "path" or "content" must be provided.')
1153
1154 return input_api.Command(
Robert Iannucci7d47eb52017-12-06 12:18:18 -08001155 name,
Dan Jacques94652a32017-10-09 23:18:46 -04001156 cmd,
1157 kwargs,
1158 output_api.PresubmitError)
1159
1160
1161def CheckCIPDPackages(input_api, output_api, platforms, packages):
1162 """Verifies that all named CIPD packages can be resolved against all supplied
1163 platforms.
1164
1165 Args:
1166 platforms (list): List of CIPD platforms to verify.
1167 packages (dict): Mapping of package name to version.
1168 """
1169 manifest = []
1170 for p in platforms:
1171 manifest.append('$VerifiedPlatform %s' % (p,))
1172 for k, v in packages.iteritems():
1173 manifest.append('%s %s' % (k, v))
1174 return CheckCIPDManifest(input_api, output_api, content='\n'.join(manifest))
Sergiy Byelozyoroveba83472017-10-27 02:08:21 +02001175
1176
1177def CheckVPythonSpec(input_api, output_api, file_filter=None):
1178 """Validates any changed .vpython files with vpython verification tool.
1179
1180 Args:
1181 input_api: Bag of input related interfaces.
1182 output_api: Bag of output related interfaces.
1183 file_filter: Custom function that takes a path (relative to client root) and
1184 returns boolean, which is used to filter files for which to apply the
1185 verification to. Defaults to any path ending with .vpython, which captures
1186 both global .vpython and <script>.vpython files.
1187
1188 Returns:
1189 A list of input_api.Command objects containing verification commands.
1190 """
1191 file_filter = file_filter or (lambda f: f.LocalPath().endswith('.vpython'))
John Budorick16162372018-04-18 10:39:53 -07001192 affected_files = input_api.AffectedTestableFiles(file_filter=file_filter)
Sergiy Byelozyoroveba83472017-10-27 02:08:21 +02001193 affected_files = map(lambda f: f.AbsoluteLocalPath(), affected_files)
1194
1195 commands = []
1196 for f in affected_files:
1197 commands.append(input_api.Command(
1198 'Verify %s' % f,
1199 ['vpython', '-vpython-spec', f, '-vpython-tool', 'verify'],
1200 {'stderr': input_api.subprocess.STDOUT},
1201 output_api.PresubmitError))
1202
1203 return commands
Mun Yong Jang17995a92017-12-01 16:00:31 -08001204
1205
1206def CheckChangedLUCIConfigs(input_api, output_api):
1207 import collections
1208 import base64
1209 import json
Mun Yong Jangcfb9a232017-12-18 10:39:34 -08001210 import logging
Mun Yong Jang17995a92017-12-01 16:00:31 -08001211 import urllib2
1212
1213 import auth
1214 import git_cl
1215
1216 LUCI_CONFIG_HOST_NAME = 'luci-config.appspot.com'
1217
1218 cl = git_cl.Changelist()
Mun Yong Jang603d01e2017-12-19 16:38:30 -08001219 if input_api.change.issue and input_api.gerrit:
1220 remote_branch = input_api.gerrit.GetDestRef(input_api.change.issue)
1221 else:
1222 remote, remote_branch = cl.GetRemoteBranch()
1223 if remote_branch.startswith('refs/remotes/%s/' % remote):
1224 remote_branch = remote_branch.replace(
1225 'refs/remotes/%s/' % remote, 'refs/heads/', 1)
1226 if remote_branch.startswith('refs/remotes/branch-heads/'):
1227 remote_branch = remote_branch.replace(
1228 'refs/remotes/branch-heads/', 'refs/branch-heads/', 1)
Mun Yong Jang17995a92017-12-01 16:00:31 -08001229
1230 remote_host_url = cl.GetRemoteUrl()
1231 if not remote_host_url:
1232 return [output_api.PresubmitError(
1233 'Remote host url for git has not been defined')]
1234 remote_host_url = remote_host_url.rstrip('/')
1235 if remote_host_url.endswith('.git'):
1236 remote_host_url = remote_host_url[:-len('.git')]
1237
1238 # authentication
1239 try:
1240 authenticator = auth.get_authenticator_for_host(
1241 LUCI_CONFIG_HOST_NAME, auth.make_auth_config())
1242 acc_tkn = authenticator.get_access_token()
1243 except auth.AuthenticationError as e:
1244 return [output_api.PresubmitError(
1245 'Error in authenticating user.', long_text=str(e))]
1246
1247 def request(endpoint, body=None):
1248 api_url = ('https://%s/_ah/api/config/v1/%s'
1249 % (LUCI_CONFIG_HOST_NAME, endpoint))
1250 req = urllib2.Request(api_url)
1251 req.add_header('Authorization', 'Bearer %s' % acc_tkn.token)
1252 if body is not None:
1253 req.add_header('Content-Type', 'application/json')
1254 req.add_data(json.dumps(body))
1255 return json.load(urllib2.urlopen(req))
1256
1257 try:
1258 config_sets = request('config-sets').get('config_sets')
1259 except urllib2.HTTPError as e:
1260 return [output_api.PresubmitError(
1261 'Config set request to luci-config failed', long_text=str(e))]
1262 if not config_sets:
1263 return [output_api.PresubmitWarning('No config_sets were returned')]
1264 loc_pref = '%s/+/%s/' % (remote_host_url, remote_branch)
Mun Yong Jangcfb9a232017-12-18 10:39:34 -08001265 logging.debug('Derived location prefix: %s', loc_pref)
Mun Yong Jang17995a92017-12-01 16:00:31 -08001266 dir_to_config_set = {
1267 '%s/' % cs['location'][len(loc_pref):].rstrip('/'): cs['config_set']
1268 for cs in config_sets
1269 if cs['location'].startswith(loc_pref) or
1270 ('%s/' % cs['location']) == loc_pref
1271 }
1272 cs_to_files = collections.defaultdict(list)
1273 for f in input_api.AffectedFiles():
1274 # windows
1275 file_path = f.LocalPath().replace(_os.sep, '/')
Mun Yong Jangcfb9a232017-12-18 10:39:34 -08001276 logging.debug('Affected file path: %s', file_path)
Mun Yong Jang17995a92017-12-01 16:00:31 -08001277 for dr, cs in dir_to_config_set.iteritems():
1278 if dr == '/' or file_path.startswith(dr):
1279 cs_to_files[cs].append({
1280 'path': file_path[len(dr):] if dr != '/' else file_path,
1281 'content': base64.b64encode(
1282 '\n'.join(f.NewContents()).encode('utf-8'))
1283 })
1284 outputs = []
1285 for cs, f in cs_to_files.iteritems():
1286 try:
1287 # TODO(myjang): parallelize
1288 res = request(
1289 'validate-config', body={'config_set': cs, 'files': f})
1290 except urllib2.HTTPError as e:
1291 return [output_api.PresubmitError(
1292 'Validation request to luci-config failed', long_text=str(e))]
1293 for msg in res.get('messages', []):
1294 sev = msg['severity']
1295 if sev == 'WARNING':
1296 out_f = output_api.PresubmitPromptWarning
1297 elif sev == 'ERROR' or sev == 'CRITICAL':
1298 out_f = output_api.PresubmitError
1299 else:
1300 out_f = output_api.PresubmitNotifyResult
1301 outputs.append(out_f('Config validation: %s' % msg['text']))
1302 return outputs