blob: 25a3460212422783dde6e84a8752bb32ee5e5cbe [file] [log] [blame]
maruel@chromium.orgba551772010-02-03 18:21:42 +00001# Copyright (c) 2010 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
bradnelson@google.com56e48bc2011-03-24 20:51:21 +00007import time
8
9
maruel@chromium.org3410d912009-06-09 20:56:16 +000010### Description checks
11
kuchhal@chromium.org00c41e42009-05-12 21:43:13 +000012def CheckChangeHasTestField(input_api, output_api):
13 """Requires that the changelist have a TEST= field."""
maruel@chromium.orge1a524f2009-05-27 14:43:46 +000014 if input_api.change.TEST:
kuchhal@chromium.org00c41e42009-05-12 21:43:13 +000015 return []
16 else:
17 return [output_api.PresubmitNotifyResult(
jam@chromium.org5c76de92011-01-24 18:19:21 +000018 'If this change requires manual test instructions to QA team, add '
19 'TEST=[instructions].')]
kuchhal@chromium.org00c41e42009-05-12 21:43:13 +000020
21
22def CheckChangeHasBugField(input_api, output_api):
23 """Requires that the changelist have a BUG= field."""
maruel@chromium.orge1a524f2009-05-27 14:43:46 +000024 if input_api.change.BUG:
kuchhal@chromium.org00c41e42009-05-12 21:43:13 +000025 return []
26 else:
27 return [output_api.PresubmitNotifyResult(
jam@chromium.org5c76de92011-01-24 18:19:21 +000028 'If this change has an associated bug, add BUG=[bug number].')]
kuchhal@chromium.org00c41e42009-05-12 21:43:13 +000029
30
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000031def CheckChangeHasTestedField(input_api, output_api):
32 """Requires that the changelist have a TESTED= field."""
maruel@chromium.orge1a524f2009-05-27 14:43:46 +000033 if input_api.change.TESTED:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000034 return []
35 else:
maruel@chromium.org3fbcb082010-03-19 14:03:28 +000036 return [output_api.PresubmitError('Changelist must have a TESTED= field.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000037
38
39def CheckChangeHasQaField(input_api, output_api):
40 """Requires that the changelist have a QA= field."""
41 if input_api.change.QA:
42 return []
43 else:
maruel@chromium.org3fbcb082010-03-19 14:03:28 +000044 return [output_api.PresubmitError('Changelist must have a QA= field.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000045
46
47def CheckDoNotSubmitInDescription(input_api, output_api):
48 """Checks that the user didn't add 'DO NOT ' + 'SUBMIT' to the CL description.
49 """
50 keyword = 'DO NOT ' + 'SUBMIT'
51 if keyword in input_api.change.DescriptionText():
52 return [output_api.PresubmitError(
maruel@chromium.org3fbcb082010-03-19 14:03:28 +000053 keyword + ' is present in the changelist description.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000054 else:
55 return []
56
57
maruel@chromium.orgbc50eb42009-06-10 18:22:47 +000058def CheckChangeHasDescription(input_api, output_api):
59 """Checks the CL description is not empty."""
60 text = input_api.change.DescriptionText()
61 if text.strip() == '':
62 if input_api.is_committing:
maruel@chromium.org3fbcb082010-03-19 14:03:28 +000063 return [output_api.PresubmitError('Add a description.')]
maruel@chromium.orgbc50eb42009-06-10 18:22:47 +000064 else:
maruel@chromium.org3fbcb082010-03-19 14:03:28 +000065 return [output_api.PresubmitNotifyResult('Add a description.')]
maruel@chromium.orgbc50eb42009-06-10 18:22:47 +000066 return []
67
maruel@chromium.org3410d912009-06-09 20:56:16 +000068### Content checks
69
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000070def CheckDoNotSubmitInFiles(input_api, output_api):
71 """Checks that the user didn't add 'DO NOT ' + 'SUBMIT' to any files."""
72 keyword = 'DO NOT ' + 'SUBMIT'
maruel@chromium.org3410d912009-06-09 20:56:16 +000073 # We want to check every text files, not just source files.
74 for f, line_num, line in input_api.RightHandSideLines(lambda x: x):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000075 if keyword in line:
76 text = 'Found ' + keyword + ' in %s, line %s' % (f.LocalPath(), line_num)
77 return [output_api.PresubmitError(text)]
78 return []
79
80
erg@google.com26970fa2009-11-17 18:07:32 +000081def CheckChangeLintsClean(input_api, output_api, source_file_filter=None):
maruel@chromium.org3fbcb082010-03-19 14:03:28 +000082 """Checks that all '.cc' and '.h' files pass cpplint.py."""
erg@google.com26970fa2009-11-17 18:07:32 +000083 _RE_IS_TEST = input_api.re.compile(r'.*tests?.(cc|h)$')
84 result = []
85
86 # Initialize cpplint.
87 import cpplint
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +000088 # Access to a protected member _XX of a client class
89 # pylint: disable=W0212
erg@google.com26970fa2009-11-17 18:07:32 +000090 cpplint._cpplint_state.ResetErrorCounts()
91
92 # Justifications for each filter:
93 #
94 # - build/include : Too many; fix in the future.
95 # - build/include_order : Not happening; #ifdefed includes.
96 # - build/namespace : I'm surprised by how often we violate this rule.
97 # - readability/casting : Mistakes a whole bunch of function pointer.
98 # - runtime/int : Can be fixed long term; volume of errors too high
99 # - runtime/virtual : Broken now, but can be fixed in the future?
100 # - whitespace/braces : We have a lot of explicit scoping in chrome code.
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000101 cpplint._SetFilters('-build/include,-build/include_order,-build/namespace,'
102 '-readability/casting,-runtime/int,-runtime/virtual,'
103 '-whitespace/braces')
erg@google.com26970fa2009-11-17 18:07:32 +0000104
105 # We currently are more strict with normal code than unit tests; 4 and 5 are
106 # the verbosity level that would normally be passed to cpplint.py through
107 # --verbose=#. Hopefully, in the future, we can be more verbose.
108 files = [f.AbsoluteLocalPath() for f in
109 input_api.AffectedSourceFiles(source_file_filter)]
110 for file_name in files:
111 if _RE_IS_TEST.match(file_name):
112 level = 5
113 else:
114 level = 4
115
116 cpplint.ProcessFile(file_name, level)
117
118 if cpplint._cpplint_state.error_count > 0:
119 if input_api.is_committing:
120 res_type = output_api.PresubmitError
121 else:
122 res_type = output_api.PresubmitPromptWarning
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000123 result = [res_type('Changelist failed cpplint.py check.')]
erg@google.com26970fa2009-11-17 18:07:32 +0000124
125 return result
126
127
maruel@chromium.org3410d912009-06-09 20:56:16 +0000128def CheckChangeHasNoCR(input_api, output_api, source_file_filter=None):
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000129 """Checks no '\r' (CR) character is in any source files."""
130 cr_files = []
maruel@chromium.org3410d912009-06-09 20:56:16 +0000131 for f in input_api.AffectedSourceFiles(source_file_filter):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000132 if '\r' in input_api.ReadFile(f, 'rb'):
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000133 cr_files.append(f.LocalPath())
134 if cr_files:
135 return [output_api.PresubmitPromptWarning(
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000136 'Found a CR character in these files:', items=cr_files)]
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000137 return []
138
139
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000140def CheckSvnModifiedDirectories(input_api, output_api, source_file_filter=None):
141 """Checks for files in svn modified directories.
142
143 They will get submitted on accident because svn commits recursively by
144 default, and that's very dangerous.
145 """
146 if input_api.change.scm != 'svn':
147 return []
148
149 errors = []
150 current_cl_files = input_api.change.GetModifiedFiles()
151 all_modified_files = input_api.change.GetAllModifiedFiles()
152 # Filter out files in the current CL.
153 modified_files = [f for f in all_modified_files if f not in current_cl_files]
154 modified_abspaths = [input_api.os_path.abspath(f) for f in modified_files]
155
156 for f in input_api.AffectedFiles(source_file_filter):
157 if f.Action() == 'M' and f.IsDirectory():
158 curpath = f.AbsoluteLocalPath()
159 bad_files = []
160 # Check if any of the modified files in other CLs are under curpath.
161 for i in xrange(len(modified_files)):
162 abspath = modified_abspaths[i]
163 if input_api.os_path.commonprefix([curpath, abspath]) == curpath:
164 bad_files.append(modified_files[i])
165 if bad_files:
166 if input_api.is_committing:
167 error_type = output_api.PresubmitPromptWarning
168 else:
169 error_type = output_api.PresubmitNotifyResult
170 errors.append(error_type(
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000171 'Potential accidental commits in changelist %s:' % f.LocalPath(),
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000172 items=bad_files))
173 return errors
174
175
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000176def CheckChangeHasOnlyOneEol(input_api, output_api, source_file_filter=None):
177 """Checks the files ends with one and only one \n (LF)."""
178 eof_files = []
179 for f in input_api.AffectedSourceFiles(source_file_filter):
180 contents = input_api.ReadFile(f, 'rb')
181 # Check that the file ends in one and only one newline character.
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000182 if len(contents) > 1 and (contents[-1:] != '\n' or contents[-2:-1] == '\n'):
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000183 eof_files.append(f.LocalPath())
184
185 if eof_files:
186 return [output_api.PresubmitPromptWarning(
187 'These files should end in one (and only one) newline character:',
188 items=eof_files)]
189 return []
190
191
192def CheckChangeHasNoCrAndHasOnlyOneEol(input_api, output_api,
193 source_file_filter=None):
194 """Runs both CheckChangeHasNoCR and CheckChangeHasOnlyOneEOL in one pass.
195
196 It is faster because it is reading the file only once.
197 """
198 cr_files = []
199 eof_files = []
200 for f in input_api.AffectedSourceFiles(source_file_filter):
201 contents = input_api.ReadFile(f, 'rb')
202 if '\r' in contents:
203 cr_files.append(f.LocalPath())
204 # Check that the file ends in one and only one newline character.
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000205 if len(contents) > 1 and (contents[-1:] != '\n' or contents[-2:-1] == '\n'):
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000206 eof_files.append(f.LocalPath())
207 outputs = []
208 if cr_files:
209 outputs.append(output_api.PresubmitPromptWarning(
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000210 'Found a CR character in these files:', items=cr_files))
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000211 if eof_files:
212 outputs.append(output_api.PresubmitPromptWarning(
213 'These files should end in one (and only one) newline character:',
214 items=eof_files))
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000215 return outputs
216
217
maruel@chromium.org3410d912009-06-09 20:56:16 +0000218def CheckChangeHasNoTabs(input_api, output_api, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000219 """Checks that there are no tab characters in any of the text files to be
220 submitted.
221 """
maruel@chromium.org115ae6c2010-06-18 17:11:43 +0000222 # In addition to the filter, make sure that makefiles are blacklisted.
223 if not source_file_filter:
224 # It's the default filter.
225 source_file_filter = input_api.FilterSourceFile
226 def filter_more(affected_file):
227 return (not input_api.os_path.basename(affected_file.LocalPath()) in
228 ('Makefile', 'makefile') and
229 source_file_filter(affected_file))
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000230 tabs = []
maruel@chromium.org115ae6c2010-06-18 17:11:43 +0000231 for f, line_num, line in input_api.RightHandSideLines(filter_more):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000232 if '\t' in line:
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000233 tabs.append('%s, line %s' % (f.LocalPath(), line_num))
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000234 if tabs:
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000235 return [output_api.PresubmitPromptWarning('Found a tab character in:',
236 long_text='\n'.join(tabs))]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000237 return []
238
239
estade@chromium.orgfdcc9f72011-02-07 22:25:07 +0000240def CheckChangeTodoHasOwner(input_api, output_api, source_file_filter=None):
241 """Checks that the user didn't add TODO(name) without an owner."""
242
maruel@chromium.org07ab60e2011-02-08 21:54:00 +0000243 unowned_todo = input_api.re.compile('TO' + 'DO[^(]')
estade@chromium.orgfdcc9f72011-02-07 22:25:07 +0000244 for f, line_num, line in input_api.RightHandSideLines(source_file_filter):
245 if unowned_todo.search(line):
246 text = ('Found TO' + 'DO with no owner in %s, line %s' %
247 (f.LocalPath(), line_num))
248 return [output_api.PresubmitPromptWarning(text)]
249 return []
250
251
maruel@chromium.orgf5888bb2009-06-10 20:26:37 +0000252def CheckChangeHasNoStrayWhitespace(input_api, output_api,
253 source_file_filter=None):
254 """Checks that there is no stray whitespace at source lines end."""
255 errors = []
256 for f, line_num, line in input_api.RightHandSideLines(source_file_filter):
257 if line.rstrip() != line:
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000258 errors.append('%s, line %s' % (f.LocalPath(), line_num))
maruel@chromium.orgf5888bb2009-06-10 20:26:37 +0000259 if errors:
260 return [output_api.PresubmitPromptWarning(
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000261 'Found line ending with white spaces in:',
262 long_text='\n'.join(errors))]
maruel@chromium.orgf5888bb2009-06-10 20:26:37 +0000263 return []
264
265
maruel@chromium.org3410d912009-06-09 20:56:16 +0000266def CheckLongLines(input_api, output_api, maxlen=80, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000267 """Checks that there aren't any lines longer than maxlen characters in any of
268 the text files to be submitted.
269 """
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000270 bad = []
maruel@chromium.org3410d912009-06-09 20:56:16 +0000271 for f, line_num, line in input_api.RightHandSideLines(source_file_filter):
maruel@chromium.org5c2720e2009-06-09 14:04:08 +0000272 # Allow lines with http://, https:// and #define/#pragma/#include/#if/#endif
273 # to exceed the maxlen rule.
274 if (len(line) > maxlen and
275 not 'http://' in line and
276 not 'https://' in line and
277 not line.startswith('#define') and
278 not line.startswith('#include') and
thakis@chromium.org09829bf2010-10-02 01:58:17 +0000279 not line.startswith('#import') and
maruel@chromium.org5c2720e2009-06-09 14:04:08 +0000280 not line.startswith('#pragma') and
281 not line.startswith('#if') and
282 not line.startswith('#endif')):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000283 bad.append(
284 '%s, line %s, %s chars' %
maruel@chromium.org1487d532009-06-06 00:22:57 +0000285 (f.LocalPath(), line_num, len(line)))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000286 if len(bad) == 5: # Just show the first 5 errors.
287 break
288
289 if bad:
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000290 msg = 'Found lines longer than %s characters (first 5 shown).' % maxlen
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000291 return [output_api.PresubmitPromptWarning(msg, items=bad)]
292 else:
293 return []
294
295
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000296def CheckLicense(input_api, output_api, license_re, source_file_filter=None,
maruel@chromium.org71626852010-11-03 13:14:25 +0000297 accept_empty_files=True):
maruel@chromium.orgb9e7ada2010-01-27 23:12:39 +0000298 """Verifies the license header.
299 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000300 license_re = input_api.re.compile(license_re, input_api.re.MULTILINE)
maruel@chromium.orgb9e7ada2010-01-27 23:12:39 +0000301 bad_files = []
302 for f in input_api.AffectedSourceFiles(source_file_filter):
303 contents = input_api.ReadFile(f, 'rb')
maruel@chromium.org71626852010-11-03 13:14:25 +0000304 if accept_empty_files and not contents:
305 continue
maruel@chromium.orgb9e7ada2010-01-27 23:12:39 +0000306 if not license_re.search(contents):
307 bad_files.append(f.LocalPath())
308 if bad_files:
309 if input_api.is_committing:
310 res_type = output_api.PresubmitPromptWarning
311 else:
312 res_type = output_api.PresubmitNotifyResult
313 return [res_type(
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000314 'Found a bad license header in these files:', items=bad_files)]
maruel@chromium.orgb9e7ada2010-01-27 23:12:39 +0000315 return []
316
317
maruel@chromium.org1a0e3cb2009-06-10 18:03:04 +0000318def CheckChangeSvnEolStyle(input_api, output_api, source_file_filter=None):
maruel@chromium.orgb7d46902009-06-10 14:12:10 +0000319 """Checks that the source files have svn:eol-style=LF."""
maruel@chromium.org46e832a2009-06-18 19:58:07 +0000320 return CheckSvnProperty(input_api, output_api,
321 'svn:eol-style', 'LF',
322 input_api.AffectedSourceFiles(source_file_filter))
323
324
325def CheckSvnForCommonMimeTypes(input_api, output_api):
326 """Checks that common binary file types have the correct svn:mime-type."""
327 output = []
328 files = input_api.AffectedFiles(include_deletes=False)
maruel@chromium.orge49187c2009-06-26 22:44:53 +0000329 def IsExts(x, exts):
330 path = x.LocalPath()
331 for extension in exts:
332 if path.endswith(extension):
333 return True
334 return False
maruel@chromium.org46e832a2009-06-18 19:58:07 +0000335 def FilterFiles(extension):
maruel@chromium.orge49187c2009-06-26 22:44:53 +0000336 return filter(lambda x: IsExts(x, extension), files)
maruel@chromium.org46e832a2009-06-18 19:58:07 +0000337 def RunCheck(mime_type, files):
338 output.extend(CheckSvnProperty(input_api, output_api, 'svn:mime-type',
339 mime_type, files))
maruel@chromium.orge49187c2009-06-26 22:44:53 +0000340 RunCheck('application/pdf', FilterFiles(['.pdf']))
341 RunCheck('image/bmp', FilterFiles(['.bmp']))
342 RunCheck('image/gif', FilterFiles(['.gif']))
343 RunCheck('image/png', FilterFiles(['.png']))
344 RunCheck('image/jpeg', FilterFiles(['.jpg', '.jpeg', '.jpe']))
345 RunCheck('image/vnd.microsoft.icon', FilterFiles(['.ico']))
maruel@chromium.org46e832a2009-06-18 19:58:07 +0000346 return output
347
348
349def CheckSvnProperty(input_api, output_api, prop, expected, affected_files):
350 """Checks that affected_files files have prop=expected."""
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000351 if input_api.change.scm != 'svn':
352 return []
353
354 bad = filter(lambda f: f.Property(prop) != expected, affected_files)
maruel@chromium.orgb7d46902009-06-10 14:12:10 +0000355 if bad:
maruel@chromium.org0874d472009-06-10 19:08:33 +0000356 if input_api.is_committing:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000357 res_type = output_api.PresubmitError
maruel@chromium.org0874d472009-06-10 19:08:33 +0000358 else:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000359 res_type = output_api.PresubmitNotifyResult
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000360 message = 'Run the command: svn pset %s %s \\' % (prop, expected)
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000361 return [res_type(message, items=bad)]
maruel@chromium.orgb7d46902009-06-10 14:12:10 +0000362 return []
363
364
maruel@chromium.org3410d912009-06-09 20:56:16 +0000365### Other checks
366
367def CheckDoNotSubmit(input_api, output_api):
368 return (
369 CheckDoNotSubmitInDescription(input_api, output_api) +
370 CheckDoNotSubmitInFiles(input_api, output_api)
371 )
372
373
bradnelson@google.comc0b332a2010-08-26 00:30:37 +0000374def CheckTreeIsOpen(input_api, output_api,
375 url=None, closed=None, json_url=None):
376 """Check whether to allow commit without prompt.
377
378 Supports two styles:
379 1. Checks that an url's content doesn't match a regexp that would mean that
380 the tree is closed. (old)
381 2. Check the json_url to decide whether to allow commit without prompt.
382 Args:
383 input_api: input related apis.
384 output_api: output related apis.
385 url: url to use for regex based tree status.
386 closed: regex to match for closed status.
387 json_url: url to download json style status.
388 """
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000389 if not input_api.is_committing:
390 return []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000391 try:
bradnelson@google.comc0b332a2010-08-26 00:30:37 +0000392 if json_url:
393 connection = input_api.urllib2.urlopen(json_url)
394 status = input_api.json.loads(connection.read())
395 connection.close()
396 if not status['can_commit_freely']:
397 short_text = 'Tree state is: ' + status['general_state']
398 long_text = status['message'] + '\n' + json_url
399 return [output_api.PresubmitError(short_text, long_text=long_text)]
400 else:
401 # TODO(bradnelson): drop this once all users are gone.
402 connection = input_api.urllib2.urlopen(url)
403 status = connection.read()
404 connection.close()
405 if input_api.re.match(closed, status):
406 long_text = status + '\n' + url
407 return [output_api.PresubmitError('The tree is closed.',
408 long_text=long_text)]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000409 except IOError:
410 pass
411 return []
maruel@chromium.org7b305e82009-05-19 18:24:20 +0000412
413
414def RunPythonUnitTests(input_api, output_api, unit_tests):
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000415 """Run the unit tests out of process, capture the output and use the result
416 code to determine success.
417 """
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000418 # We don't want to hinder users from uploading incomplete patches.
419 if input_api.is_committing:
420 message_type = output_api.PresubmitError
421 else:
422 message_type = output_api.PresubmitNotifyResult
maruel@chromium.org7b305e82009-05-19 18:24:20 +0000423 outputs = []
424 for unit_test in unit_tests:
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000425 # Run the unit tests out of process. This is because some unit tests
426 # stub out base libraries and don't clean up their mess. It's too easy to
427 # get subtle bugs.
428 cwd = None
429 env = None
430 unit_test_name = unit_test
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000431 # 'python -m test.unit_test' doesn't work. We need to change to the right
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000432 # directory instead.
433 if '.' in unit_test:
434 # Tests imported in submodules (subdirectories) assume that the current
435 # directory is in the PYTHONPATH. Manually fix that.
436 unit_test = unit_test.replace('.', '/')
437 cwd = input_api.os_path.dirname(unit_test)
438 unit_test = input_api.os_path.basename(unit_test)
439 env = input_api.environ.copy()
kbr@google.comab318592009-09-04 00:54:55 +0000440 # At least on Windows, it seems '.' must explicitly be in PYTHONPATH
441 backpath = [
442 '.', input_api.os_path.pathsep.join(['..'] * (cwd.count('/') + 1))
443 ]
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000444 if env.get('PYTHONPATH'):
445 backpath.append(env.get('PYTHONPATH'))
ukai@chromium.orga301f1f2009-08-05 10:37:33 +0000446 env['PYTHONPATH'] = input_api.os_path.pathsep.join((backpath))
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000447 subproc = input_api.subprocess.Popen(
448 [
449 input_api.python_executable,
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000450 '-m',
451 '%s' % unit_test
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000452 ],
453 cwd=cwd,
454 env=env,
455 stdin=input_api.subprocess.PIPE,
456 stdout=input_api.subprocess.PIPE,
457 stderr=input_api.subprocess.PIPE)
458 stdoutdata, stderrdata = subproc.communicate()
459 # Discard the output if returncode == 0
460 if subproc.returncode:
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000461 outputs.append('Test \'%s\' failed with code %d\n%s\n%s\n' % (
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000462 unit_test_name, subproc.returncode, stdoutdata, stderrdata))
463 if outputs:
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000464 return [message_type('%d unit tests failed.' % len(outputs),
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000465 long_text='\n'.join(outputs))]
466 return []
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000467
468
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000469def _FetchAllFiles(input_api, white_list, black_list):
470 """Hack to fetch all files."""
471 # We cannot use AffectedFiles here because we want to test every python
472 # file on each single python change. It's because a change in a python file
473 # can break another unmodified file.
474 # Use code similar to InputApi.FilterSourceFile()
475 def Find(filepath, filters):
476 for item in filters:
477 if input_api.re.match(item, filepath):
478 return True
479 return False
480
481 import os
482 files = []
483 path_len = len(input_api.PresubmitLocalPath())
484 for dirpath, dirnames, filenames in os.walk(input_api.PresubmitLocalPath()):
485 # Passes dirnames in black list to speed up search.
486 for item in dirnames[:]:
487 filepath = input_api.os_path.join(dirpath, item)[path_len + 1:]
488 if Find(filepath, black_list):
489 dirnames.remove(item)
490 for item in filenames:
491 filepath = input_api.os_path.join(dirpath, item)[path_len + 1:]
492 if Find(filepath, white_list) and not Find(filepath, black_list):
493 files.append(filepath)
494 return files
495
496
maruel@chromium.orgbf38a7e2010-12-14 18:15:54 +0000497def RunPylint(input_api, output_api, white_list=None, black_list=None):
498 """Run pylint on python files.
499
500 The default white_list enforces looking only a *.py files.
501 """
502 white_list = white_list or ['.*\.py$']
503 black_list = black_list or input_api.DEFAULT_BLACK_LIST
504
505 # Only trigger if there is at least one python file affected.
506 src_filter = lambda x: input_api.FilterSourceFile(x, white_list, black_list)
507 if not input_api.AffectedSourceFiles(src_filter):
508 return []
509
maruel@chromium.orge94aedc2010-12-13 21:11:30 +0000510 # On certain pylint/python version combination, running pylint throws a lot of
511 # warning messages.
maruel@chromium.orgbf38a7e2010-12-14 18:15:54 +0000512 import warnings
maruel@chromium.orge94aedc2010-12-13 21:11:30 +0000513 warnings.filterwarnings('ignore', category=DeprecationWarning)
514 try:
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000515 files = _FetchAllFiles(input_api, white_list, black_list)
516 if not files:
517 return []
maruel@chromium.orgbf38a7e2010-12-14 18:15:54 +0000518 # Now that at least one python file was modified and all the python files
519 # were listed, try to run pylint.
maruel@chromium.orge94aedc2010-12-13 21:11:30 +0000520 try:
521 from pylint import lint
maruel@chromium.orgfca53392010-12-21 18:42:57 +0000522 result = lint.Run(sorted(files))
523 except SystemExit, e:
524 # pylint has the bad habit of calling sys.exit(), trap it here.
525 result = e.code
maruel@chromium.orge94aedc2010-12-13 21:11:30 +0000526 except ImportError:
527 if input_api.platform == 'win32':
528 return [output_api.PresubmitNotifyResult(
529 'Warning: Can\'t run pylint because it is not installed. Please '
530 'install manually\n'
531 'Cannot do static analysis of python files.')]
532 return [output_api.PresubmitError(
533 'Please install pylint with "sudo apt-get install python-setuptools; '
534 'sudo easy_install pylint"\n'
535 'Cannot do static analysis of python files.')]
maruel@chromium.orgfca53392010-12-21 18:42:57 +0000536 if result:
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000537 if input_api.is_committing:
538 error_type = output_api.PresubmitError
539 else:
540 error_type = output_api.PresubmitPromptWarning
541 return [error_type('Fix pylint errors first.')]
maruel@chromium.orgfca53392010-12-21 18:42:57 +0000542 return []
maruel@chromium.orge94aedc2010-12-13 21:11:30 +0000543 finally:
544 warnings.filterwarnings('default', category=DeprecationWarning)
545
dpranke@chromium.org627ea672011-03-11 23:29:03 +0000546# TODO(dpranke): Get the host_url from the input_api instead
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000547def CheckRietveldTryJobExecution(input_api, output_api, host_url, platforms,
548 owner):
549 if not input_api.is_committing:
550 return []
551 if not input_api.change.issue or not input_api.change.patchset:
552 return []
553 url = '%s/%d/get_build_results/%d' % (
554 host_url, input_api.change.issue, input_api.change.patchset)
555 try:
556 connection = input_api.urllib2.urlopen(url)
557 # platform|status|url
558 values = [item.split('|', 2) for item in connection.read().splitlines()]
559 connection.close()
560 except input_api.urllib2.HTTPError, e:
561 if e.code == 404:
562 # Fallback to no try job.
563 return [output_api.PresubmitPromptWarning(
564 'You should try the patch first.')]
565 else:
566 # Another HTTP error happened, warn the user.
567 return [output_api.PresubmitPromptWarning(
568 'Got %s while looking for try job status.' % str(e))]
569
570 if not values:
571 # It returned an empty list. Probably a private review.
572 return []
573 # Reformat as an dict of platform: [status, url]
574 values = dict([[v[0], [v[1], v[2]]] for v in values if len(v) == 3])
575 if not values:
576 # It returned useless data.
577 return [output_api.PresubmitNotifyResult('Failed to parse try job results')]
578
579 for platform in platforms:
580 values.setdefault(platform, ['not started', ''])
581 message = None
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000582 non_success = [k.upper() for k, v in values.iteritems() if v[0] != 'success']
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000583 if 'failure' in [v[0] for v in values.itervalues()]:
584 message = 'Try job failures on %s!\n' % ', '.join(non_success)
585 elif non_success:
586 message = ('Unfinished (or not even started) try jobs on '
587 '%s.\n') % ', '.join(non_success)
588 if message:
589 message += (
590 'Is try server wrong or broken? Please notify %s. '
591 'Thanks.\n' % owner)
592 return [output_api.PresubmitPromptWarning(message=message)]
593 return []
594
595
596def CheckBuildbotPendingBuilds(input_api, output_api, url, max_pendings,
597 ignored):
598 if not input_api.json:
599 return [output_api.PresubmitPromptWarning(
600 'Please install simplejson or upgrade to python 2.6+')]
601 try:
602 connection = input_api.urllib2.urlopen(url)
603 raw_data = connection.read()
604 connection.close()
605 except IOError:
606 return [output_api.PresubmitNotifyResult('%s is not accessible' % url)]
607
608 try:
609 data = input_api.json.loads(raw_data)
610 except ValueError:
611 return [output_api.PresubmitNotifyResult('Received malformed json while '
612 'looking up buildbot status')]
613
614 out = []
615 for (builder_name, builder) in data.iteritems():
616 if builder_name in ignored:
617 continue
maruel@chromium.orgcf1982c2010-10-04 15:08:28 +0000618 if builder.get('state', '') == 'offline':
619 continue
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000620 pending_builds_len = len(builder.get('pending_builds', []))
621 if pending_builds_len > max_pendings:
622 out.append('%s has %d build(s) pending' %
623 (builder_name, pending_builds_len))
624 if out:
625 return [output_api.PresubmitPromptWarning(
626 'Build(s) pending. It is suggested to wait that no more than %d '
627 'builds are pending.' % max_pendings,
628 long_text='\n'.join(out))]
629 return []
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000630
631
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000632def CheckOwners(input_api, output_api, email_regexp=None,
633 source_file_filter=None):
dpranke@chromium.orgadd5df42011-03-08 23:04:01 +0000634 affected_files = set([f.LocalPath() for f in
635 input_api.change.AffectedFiles(source_file_filter)])
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000636 owners_db = input_api.owners_db
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000637 if email_regexp:
638 owners_db.email_regexp = input_api.re.compile(email_regexp)
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000639
dpranke@chromium.org627ea672011-03-11 23:29:03 +0000640 if input_api.is_committing and input_api.tbr:
641 return [output_api.PresubmitNotifyResult(
642 '--tbr was specified, skipping OWNERS check')]
643 elif input_api.is_committing:
644 approvers = _Approvers(input_api, owners_db.email_regexp)
645 missing_files = owners_db.files_not_covered_by(affected_files, approvers)
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000646 if missing_files:
dpranke@chromium.org627ea672011-03-11 23:29:03 +0000647 return [output_api.PresubmitError('Missing LGTM from an OWNER for: %s' %
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000648 ','.join(missing_files))]
649 return []
dpranke@chromium.orgadd5df42011-03-08 23:04:01 +0000650 elif input_api.change.tags.get('R'):
651 return []
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000652
dpranke@chromium.org3ae183f2011-03-09 21:40:32 +0000653 suggested_reviewers = owners_db.reviewers_for(affected_files)
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000654 return [output_api.PresubmitAddReviewers(suggested_reviewers)]
dpranke@chromium.org627ea672011-03-11 23:29:03 +0000655
656
657def _Approvers(input_api, email_regexp):
658 if not input_api.change.issue:
659 return []
660
dpranke@chromium.org1b98c432011-03-17 21:11:49 +0000661 # TODO(dpranke): Should figure out if input_api.host_url is supposed to
662 # be a host or a scheme+host and normalize it there.
663 host = input_api.host_url
664 if not host.startswith('http://') and not host.startswith('https://'):
665 host = 'http://' + host
666 url = '%s/api/%s?messages=true' % (host, input_api.change.issue)
dpranke@chromium.org627ea672011-03-11 23:29:03 +0000667
668 f = input_api.urllib2.urlopen(url)
669 issue_props = input_api.json.load(f)
670 owner = input_api.re.escape(issue_props['owner'])
671
672 # TODO(dpranke): This mimics the logic in
673 # /tools/commit-queue/verifiers/reviewer_lgtm.py
674 # We should share the code and/or remove the check there where it is
675 # redundant (since the commit queue also enforces the presubmit checks).
676 def match_reviewer(r):
677 return email_regexp.match(r) and not input_api.re.match(owner, r)
678
679 approvers = []
dpranke@chromium.org4ea24732011-03-14 20:19:22 +0000680 for m in issue_props.get('messages', []):
dpranke@chromium.org627ea672011-03-11 23:29:03 +0000681 if 'lgtm' in m['text'].lower() and match_reviewer(m['sender']):
682 approvers.append(m['sender'])
683 return set(approvers)
684
bradnelson@google.com56e48bc2011-03-24 20:51:21 +0000685
686def _CheckConstNSObject(input_api, output_api, source_file_filter):
687 """Checks to make sure no objective-c files have |const NSSomeClass*|."""
688 pattern = input_api.re.compile(r'const\s+NS\w*\s*\*')
689
690 def objective_c_filter(f):
691 return (source_file_filter(f) and
692 input_api.os_path.splitext(f.LocalPath())[1] in ('.h', '.mm'))
693
694 files = []
695 for f in input_api.AffectedSourceFiles(objective_c_filter):
696 contents = input_api.ReadFile(f)
697 if pattern.search(contents):
698 files.append(f)
699
700 if files:
701 if input_api.is_committing:
702 res_type = output_api.PresubmitPromptWarning
703 else:
704 res_type = output_api.PresubmitNotifyResult
705 return [ res_type('|const NSClass*| is wrong, see ' +
706 'http://dev.chromium.org/developers/clang-mac',
707 files) ]
708 return []
709
710
711def _CheckSingletonInHeaders(input_api, output_api, source_file_filter):
712 """Checks to make sure no header files have |Singleton<|."""
713 pattern = input_api.re.compile(r'Singleton<')
714 files = []
715 for f in input_api.AffectedSourceFiles(source_file_filter):
716 if (f.LocalPath().endswith('.h') or f.LocalPath().endswith('.hxx') or
717 f.LocalPath().endswith('.hpp') or f.LocalPath().endswith('.inl')):
718 contents = input_api.ReadFile(f)
719 if pattern.search(contents):
720 files.append(f)
721
722 if files:
723 return [ output_api.PresubmitError(
724 'Found Singleton<T> in the following header files.\n' +
725 'Please move them to an appropriate source file so that the ' +
726 'template gets instantiated in a single compilation unit.',
727 files) ]
728 return []
729
730
731def PanProjectChecks(input_api, output_api,
732 excluded_paths=None, text_files=None,
733 license_header=None, project_name=None):
734 """Checks that ALL chromium orbit projects should use.
735
736 These are checks to be run on all Chromium orbit project, including:
737 Chromium
738 Native Client
739 V8
740 When you update this function, please take this broad scope into account.
741 Args:
742 input_api: Bag of input related interfaces.
743 output_api: Bag of output related interfaces.
744 excluded_paths: Don't include these paths in common checks.
745 text_files: Which file are to be treated as documentation text files.
746 license_header: What license header should be on files.
747 project_name: What is the name of the project as it appears in the license.
748 Returns:
749 A list of warning or error objects.
750 """
751 excluded_paths = excluded_paths or tuple()
752 text_files = text_files or (
753 r'.*\.txt',
754 r'.*\.json',
755 )
756 project_name = project_name or 'Chromium'
757 license_header = license_header or (
758 r'.*? Copyright \(c\) %(year)s The %(project)s Authors\. '
759 r'All rights reserved\.\n'
760 r'.*? Use of this source code is governed by a BSD-style license that '
761 r'can be\n'
762 r'.*? found in the LICENSE file\.\n'
763 ) % {
764 'year': time.strftime('%Y'),
765 'project': project_name,
766 }
767
768 results = []
769 # This code loads the default black list (e.g. third_party, experimental, etc)
770 # and add our black list (breakpad, skia and v8 are still not following
771 # google style and are not really living this repository).
772 # See presubmit_support.py InputApi.FilterSourceFile for the (simple) usage.
773 black_list = input_api.DEFAULT_BLACK_LIST + excluded_paths
774 white_list = input_api.DEFAULT_WHITE_LIST + text_files
775 sources = lambda x: input_api.FilterSourceFile(x, black_list=black_list)
776 text_files = lambda x: input_api.FilterSourceFile(x, black_list=black_list,
777 white_list=white_list)
778
779 # TODO(dpranke): enable upload as well
780 if input_api.is_committing:
781 results.extend(input_api.canned_checks.CheckOwners(
782 input_api, output_api, source_file_filter=sources))
783
784 results.extend(input_api.canned_checks.CheckLongLines(
785 input_api, output_api, source_file_filter=sources))
786 results.extend(input_api.canned_checks.CheckChangeHasNoTabs(
787 input_api, output_api, source_file_filter=sources))
788 results.extend(input_api.canned_checks.CheckChangeHasNoStrayWhitespace(
789 input_api, output_api, source_file_filter=sources))
790 results.extend(input_api.canned_checks.CheckChangeSvnEolStyle(
791 input_api, output_api, source_file_filter=text_files))
792 results.extend(input_api.canned_checks.CheckSvnForCommonMimeTypes(
793 input_api, output_api))
794 results.extend(input_api.canned_checks.CheckLicense(
795 input_api, output_api, license_header, source_file_filter=sources))
796 results.extend(_CheckConstNSObject(
797 input_api, output_api, source_file_filter=sources))
798 results.extend(_CheckSingletonInHeaders(
799 input_api, output_api, source_file_filter=sources))
800 return results