blob: 2a868a57548fc8fcb3876e16870cccee669ae943 [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 +00007
maruel@chromium.org3410d912009-06-09 20:56:16 +00008### Description checks
9
kuchhal@chromium.org00c41e42009-05-12 21:43:13 +000010def CheckChangeHasTestField(input_api, output_api):
11 """Requires that the changelist have a TEST= field."""
maruel@chromium.orge1a524f2009-05-27 14:43:46 +000012 if input_api.change.TEST:
kuchhal@chromium.org00c41e42009-05-12 21:43:13 +000013 return []
14 else:
15 return [output_api.PresubmitNotifyResult(
jam@chromium.org5c76de92011-01-24 18:19:21 +000016 'If this change requires manual test instructions to QA team, add '
17 'TEST=[instructions].')]
kuchhal@chromium.org00c41e42009-05-12 21:43:13 +000018
19
20def CheckChangeHasBugField(input_api, output_api):
21 """Requires that the changelist have a BUG= field."""
maruel@chromium.orge1a524f2009-05-27 14:43:46 +000022 if input_api.change.BUG:
kuchhal@chromium.org00c41e42009-05-12 21:43:13 +000023 return []
24 else:
25 return [output_api.PresubmitNotifyResult(
jam@chromium.org5c76de92011-01-24 18:19:21 +000026 'If this change has an associated bug, add BUG=[bug number].')]
kuchhal@chromium.org00c41e42009-05-12 21:43:13 +000027
28
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000029def CheckChangeHasTestedField(input_api, output_api):
30 """Requires that the changelist have a TESTED= field."""
maruel@chromium.orge1a524f2009-05-27 14:43:46 +000031 if input_api.change.TESTED:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000032 return []
33 else:
maruel@chromium.org3fbcb082010-03-19 14:03:28 +000034 return [output_api.PresubmitError('Changelist must have a TESTED= field.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000035
36
37def CheckChangeHasQaField(input_api, output_api):
38 """Requires that the changelist have a QA= field."""
39 if input_api.change.QA:
40 return []
41 else:
maruel@chromium.org3fbcb082010-03-19 14:03:28 +000042 return [output_api.PresubmitError('Changelist must have a QA= field.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000043
44
45def CheckDoNotSubmitInDescription(input_api, output_api):
46 """Checks that the user didn't add 'DO NOT ' + 'SUBMIT' to the CL description.
47 """
48 keyword = 'DO NOT ' + 'SUBMIT'
49 if keyword in input_api.change.DescriptionText():
50 return [output_api.PresubmitError(
maruel@chromium.org3fbcb082010-03-19 14:03:28 +000051 keyword + ' is present in the changelist description.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000052 else:
53 return []
54
55
maruel@chromium.orgbc50eb42009-06-10 18:22:47 +000056def CheckChangeHasDescription(input_api, output_api):
57 """Checks the CL description is not empty."""
58 text = input_api.change.DescriptionText()
59 if text.strip() == '':
60 if input_api.is_committing:
maruel@chromium.org3fbcb082010-03-19 14:03:28 +000061 return [output_api.PresubmitError('Add a description.')]
maruel@chromium.orgbc50eb42009-06-10 18:22:47 +000062 else:
maruel@chromium.org3fbcb082010-03-19 14:03:28 +000063 return [output_api.PresubmitNotifyResult('Add a description.')]
maruel@chromium.orgbc50eb42009-06-10 18:22:47 +000064 return []
65
maruel@chromium.org3410d912009-06-09 20:56:16 +000066### Content checks
67
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000068def CheckDoNotSubmitInFiles(input_api, output_api):
69 """Checks that the user didn't add 'DO NOT ' + 'SUBMIT' to any files."""
70 keyword = 'DO NOT ' + 'SUBMIT'
maruel@chromium.org3410d912009-06-09 20:56:16 +000071 # We want to check every text files, not just source files.
72 for f, line_num, line in input_api.RightHandSideLines(lambda x: x):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000073 if keyword in line:
74 text = 'Found ' + keyword + ' in %s, line %s' % (f.LocalPath(), line_num)
75 return [output_api.PresubmitError(text)]
76 return []
77
78
erg@google.com26970fa2009-11-17 18:07:32 +000079def CheckChangeLintsClean(input_api, output_api, source_file_filter=None):
maruel@chromium.org3fbcb082010-03-19 14:03:28 +000080 """Checks that all '.cc' and '.h' files pass cpplint.py."""
erg@google.com26970fa2009-11-17 18:07:32 +000081 _RE_IS_TEST = input_api.re.compile(r'.*tests?.(cc|h)$')
82 result = []
83
84 # Initialize cpplint.
85 import cpplint
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +000086 # Access to a protected member _XX of a client class
87 # pylint: disable=W0212
erg@google.com26970fa2009-11-17 18:07:32 +000088 cpplint._cpplint_state.ResetErrorCounts()
89
90 # Justifications for each filter:
91 #
92 # - build/include : Too many; fix in the future.
93 # - build/include_order : Not happening; #ifdefed includes.
94 # - build/namespace : I'm surprised by how often we violate this rule.
95 # - readability/casting : Mistakes a whole bunch of function pointer.
96 # - runtime/int : Can be fixed long term; volume of errors too high
97 # - runtime/virtual : Broken now, but can be fixed in the future?
98 # - whitespace/braces : We have a lot of explicit scoping in chrome code.
maruel@chromium.org3fbcb082010-03-19 14:03:28 +000099 cpplint._SetFilters('-build/include,-build/include_order,-build/namespace,'
100 '-readability/casting,-runtime/int,-runtime/virtual,'
101 '-whitespace/braces')
erg@google.com26970fa2009-11-17 18:07:32 +0000102
103 # We currently are more strict with normal code than unit tests; 4 and 5 are
104 # the verbosity level that would normally be passed to cpplint.py through
105 # --verbose=#. Hopefully, in the future, we can be more verbose.
106 files = [f.AbsoluteLocalPath() for f in
107 input_api.AffectedSourceFiles(source_file_filter)]
108 for file_name in files:
109 if _RE_IS_TEST.match(file_name):
110 level = 5
111 else:
112 level = 4
113
114 cpplint.ProcessFile(file_name, level)
115
116 if cpplint._cpplint_state.error_count > 0:
117 if input_api.is_committing:
118 res_type = output_api.PresubmitError
119 else:
120 res_type = output_api.PresubmitPromptWarning
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000121 result = [res_type('Changelist failed cpplint.py check.')]
erg@google.com26970fa2009-11-17 18:07:32 +0000122
123 return result
124
125
maruel@chromium.org3410d912009-06-09 20:56:16 +0000126def CheckChangeHasNoCR(input_api, output_api, source_file_filter=None):
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000127 """Checks no '\r' (CR) character is in any source files."""
128 cr_files = []
maruel@chromium.org3410d912009-06-09 20:56:16 +0000129 for f in input_api.AffectedSourceFiles(source_file_filter):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000130 if '\r' in input_api.ReadFile(f, 'rb'):
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000131 cr_files.append(f.LocalPath())
132 if cr_files:
133 return [output_api.PresubmitPromptWarning(
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000134 'Found a CR character in these files:', items=cr_files)]
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000135 return []
136
137
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000138def CheckSvnModifiedDirectories(input_api, output_api, source_file_filter=None):
139 """Checks for files in svn modified directories.
140
141 They will get submitted on accident because svn commits recursively by
142 default, and that's very dangerous.
143 """
144 if input_api.change.scm != 'svn':
145 return []
146
147 errors = []
148 current_cl_files = input_api.change.GetModifiedFiles()
149 all_modified_files = input_api.change.GetAllModifiedFiles()
150 # Filter out files in the current CL.
151 modified_files = [f for f in all_modified_files if f not in current_cl_files]
152 modified_abspaths = [input_api.os_path.abspath(f) for f in modified_files]
153
154 for f in input_api.AffectedFiles(source_file_filter):
155 if f.Action() == 'M' and f.IsDirectory():
156 curpath = f.AbsoluteLocalPath()
157 bad_files = []
158 # Check if any of the modified files in other CLs are under curpath.
159 for i in xrange(len(modified_files)):
160 abspath = modified_abspaths[i]
161 if input_api.os_path.commonprefix([curpath, abspath]) == curpath:
162 bad_files.append(modified_files[i])
163 if bad_files:
164 if input_api.is_committing:
165 error_type = output_api.PresubmitPromptWarning
166 else:
167 error_type = output_api.PresubmitNotifyResult
168 errors.append(error_type(
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000169 'Potential accidental commits in changelist %s:' % f.LocalPath(),
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000170 items=bad_files))
171 return errors
172
173
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000174def CheckChangeHasOnlyOneEol(input_api, output_api, source_file_filter=None):
175 """Checks the files ends with one and only one \n (LF)."""
176 eof_files = []
177 for f in input_api.AffectedSourceFiles(source_file_filter):
178 contents = input_api.ReadFile(f, 'rb')
179 # Check that the file ends in one and only one newline character.
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000180 if len(contents) > 1 and (contents[-1:] != '\n' or contents[-2:-1] == '\n'):
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000181 eof_files.append(f.LocalPath())
182
183 if eof_files:
184 return [output_api.PresubmitPromptWarning(
185 'These files should end in one (and only one) newline character:',
186 items=eof_files)]
187 return []
188
189
190def CheckChangeHasNoCrAndHasOnlyOneEol(input_api, output_api,
191 source_file_filter=None):
192 """Runs both CheckChangeHasNoCR and CheckChangeHasOnlyOneEOL in one pass.
193
194 It is faster because it is reading the file only once.
195 """
196 cr_files = []
197 eof_files = []
198 for f in input_api.AffectedSourceFiles(source_file_filter):
199 contents = input_api.ReadFile(f, 'rb')
200 if '\r' in contents:
201 cr_files.append(f.LocalPath())
202 # Check that the file ends in one and only one newline character.
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000203 if len(contents) > 1 and (contents[-1:] != '\n' or contents[-2:-1] == '\n'):
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000204 eof_files.append(f.LocalPath())
205 outputs = []
206 if cr_files:
207 outputs.append(output_api.PresubmitPromptWarning(
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000208 'Found a CR character in these files:', items=cr_files))
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000209 if eof_files:
210 outputs.append(output_api.PresubmitPromptWarning(
211 'These files should end in one (and only one) newline character:',
212 items=eof_files))
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000213 return outputs
214
215
maruel@chromium.org3410d912009-06-09 20:56:16 +0000216def CheckChangeHasNoTabs(input_api, output_api, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000217 """Checks that there are no tab characters in any of the text files to be
218 submitted.
219 """
maruel@chromium.org115ae6c2010-06-18 17:11:43 +0000220 # In addition to the filter, make sure that makefiles are blacklisted.
221 if not source_file_filter:
222 # It's the default filter.
223 source_file_filter = input_api.FilterSourceFile
224 def filter_more(affected_file):
225 return (not input_api.os_path.basename(affected_file.LocalPath()) in
226 ('Makefile', 'makefile') and
227 source_file_filter(affected_file))
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000228 tabs = []
maruel@chromium.org115ae6c2010-06-18 17:11:43 +0000229 for f, line_num, line in input_api.RightHandSideLines(filter_more):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000230 if '\t' in line:
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000231 tabs.append('%s, line %s' % (f.LocalPath(), line_num))
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000232 if tabs:
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000233 return [output_api.PresubmitPromptWarning('Found a tab character in:',
234 long_text='\n'.join(tabs))]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000235 return []
236
237
estade@chromium.orgfdcc9f72011-02-07 22:25:07 +0000238def CheckChangeTodoHasOwner(input_api, output_api, source_file_filter=None):
239 """Checks that the user didn't add TODO(name) without an owner."""
240
maruel@chromium.org07ab60e2011-02-08 21:54:00 +0000241 unowned_todo = input_api.re.compile('TO' + 'DO[^(]')
estade@chromium.orgfdcc9f72011-02-07 22:25:07 +0000242 for f, line_num, line in input_api.RightHandSideLines(source_file_filter):
243 if unowned_todo.search(line):
244 text = ('Found TO' + 'DO with no owner in %s, line %s' %
245 (f.LocalPath(), line_num))
246 return [output_api.PresubmitPromptWarning(text)]
247 return []
248
249
maruel@chromium.orgf5888bb2009-06-10 20:26:37 +0000250def CheckChangeHasNoStrayWhitespace(input_api, output_api,
251 source_file_filter=None):
252 """Checks that there is no stray whitespace at source lines end."""
253 errors = []
254 for f, line_num, line in input_api.RightHandSideLines(source_file_filter):
255 if line.rstrip() != line:
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000256 errors.append('%s, line %s' % (f.LocalPath(), line_num))
maruel@chromium.orgf5888bb2009-06-10 20:26:37 +0000257 if errors:
258 return [output_api.PresubmitPromptWarning(
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000259 'Found line ending with white spaces in:',
260 long_text='\n'.join(errors))]
maruel@chromium.orgf5888bb2009-06-10 20:26:37 +0000261 return []
262
263
maruel@chromium.org3410d912009-06-09 20:56:16 +0000264def CheckLongLines(input_api, output_api, maxlen=80, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000265 """Checks that there aren't any lines longer than maxlen characters in any of
266 the text files to be submitted.
267 """
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000268 bad = []
maruel@chromium.org3410d912009-06-09 20:56:16 +0000269 for f, line_num, line in input_api.RightHandSideLines(source_file_filter):
maruel@chromium.org5c2720e2009-06-09 14:04:08 +0000270 # Allow lines with http://, https:// and #define/#pragma/#include/#if/#endif
271 # to exceed the maxlen rule.
272 if (len(line) > maxlen and
273 not 'http://' in line and
274 not 'https://' in line and
275 not line.startswith('#define') and
276 not line.startswith('#include') and
thakis@chromium.org09829bf2010-10-02 01:58:17 +0000277 not line.startswith('#import') and
maruel@chromium.org5c2720e2009-06-09 14:04:08 +0000278 not line.startswith('#pragma') and
279 not line.startswith('#if') and
280 not line.startswith('#endif')):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000281 bad.append(
282 '%s, line %s, %s chars' %
maruel@chromium.org1487d532009-06-06 00:22:57 +0000283 (f.LocalPath(), line_num, len(line)))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000284 if len(bad) == 5: # Just show the first 5 errors.
285 break
286
287 if bad:
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000288 msg = 'Found lines longer than %s characters (first 5 shown).' % maxlen
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000289 return [output_api.PresubmitPromptWarning(msg, items=bad)]
290 else:
291 return []
292
293
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000294def CheckLicense(input_api, output_api, license_re, source_file_filter=None,
maruel@chromium.org71626852010-11-03 13:14:25 +0000295 accept_empty_files=True):
maruel@chromium.orgb9e7ada2010-01-27 23:12:39 +0000296 """Verifies the license header.
297 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000298 license_re = input_api.re.compile(license_re, input_api.re.MULTILINE)
maruel@chromium.orgb9e7ada2010-01-27 23:12:39 +0000299 bad_files = []
300 for f in input_api.AffectedSourceFiles(source_file_filter):
301 contents = input_api.ReadFile(f, 'rb')
maruel@chromium.org71626852010-11-03 13:14:25 +0000302 if accept_empty_files and not contents:
303 continue
maruel@chromium.orgb9e7ada2010-01-27 23:12:39 +0000304 if not license_re.search(contents):
305 bad_files.append(f.LocalPath())
306 if bad_files:
307 if input_api.is_committing:
308 res_type = output_api.PresubmitPromptWarning
309 else:
310 res_type = output_api.PresubmitNotifyResult
311 return [res_type(
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000312 'Found a bad license header in these files:', items=bad_files)]
maruel@chromium.orgb9e7ada2010-01-27 23:12:39 +0000313 return []
314
315
maruel@chromium.org1a0e3cb2009-06-10 18:03:04 +0000316def CheckChangeSvnEolStyle(input_api, output_api, source_file_filter=None):
maruel@chromium.orgb7d46902009-06-10 14:12:10 +0000317 """Checks that the source files have svn:eol-style=LF."""
maruel@chromium.org46e832a2009-06-18 19:58:07 +0000318 return CheckSvnProperty(input_api, output_api,
319 'svn:eol-style', 'LF',
320 input_api.AffectedSourceFiles(source_file_filter))
321
322
323def CheckSvnForCommonMimeTypes(input_api, output_api):
324 """Checks that common binary file types have the correct svn:mime-type."""
325 output = []
326 files = input_api.AffectedFiles(include_deletes=False)
maruel@chromium.orge49187c2009-06-26 22:44:53 +0000327 def IsExts(x, exts):
328 path = x.LocalPath()
329 for extension in exts:
330 if path.endswith(extension):
331 return True
332 return False
maruel@chromium.org46e832a2009-06-18 19:58:07 +0000333 def FilterFiles(extension):
maruel@chromium.orge49187c2009-06-26 22:44:53 +0000334 return filter(lambda x: IsExts(x, extension), files)
maruel@chromium.org46e832a2009-06-18 19:58:07 +0000335 def RunCheck(mime_type, files):
336 output.extend(CheckSvnProperty(input_api, output_api, 'svn:mime-type',
337 mime_type, files))
maruel@chromium.orge49187c2009-06-26 22:44:53 +0000338 RunCheck('application/pdf', FilterFiles(['.pdf']))
339 RunCheck('image/bmp', FilterFiles(['.bmp']))
340 RunCheck('image/gif', FilterFiles(['.gif']))
341 RunCheck('image/png', FilterFiles(['.png']))
342 RunCheck('image/jpeg', FilterFiles(['.jpg', '.jpeg', '.jpe']))
343 RunCheck('image/vnd.microsoft.icon', FilterFiles(['.ico']))
maruel@chromium.org46e832a2009-06-18 19:58:07 +0000344 return output
345
346
347def CheckSvnProperty(input_api, output_api, prop, expected, affected_files):
348 """Checks that affected_files files have prop=expected."""
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000349 if input_api.change.scm != 'svn':
350 return []
351
352 bad = filter(lambda f: f.Property(prop) != expected, affected_files)
maruel@chromium.orgb7d46902009-06-10 14:12:10 +0000353 if bad:
maruel@chromium.org0874d472009-06-10 19:08:33 +0000354 if input_api.is_committing:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000355 res_type = output_api.PresubmitError
maruel@chromium.org0874d472009-06-10 19:08:33 +0000356 else:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000357 res_type = output_api.PresubmitNotifyResult
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000358 message = 'Run the command: svn pset %s %s \\' % (prop, expected)
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000359 return [res_type(message, items=bad)]
maruel@chromium.orgb7d46902009-06-10 14:12:10 +0000360 return []
361
362
maruel@chromium.org3410d912009-06-09 20:56:16 +0000363### Other checks
364
365def CheckDoNotSubmit(input_api, output_api):
366 return (
367 CheckDoNotSubmitInDescription(input_api, output_api) +
368 CheckDoNotSubmitInFiles(input_api, output_api)
369 )
370
371
bradnelson@google.comc0b332a2010-08-26 00:30:37 +0000372def CheckTreeIsOpen(input_api, output_api,
373 url=None, closed=None, json_url=None):
374 """Check whether to allow commit without prompt.
375
376 Supports two styles:
377 1. Checks that an url's content doesn't match a regexp that would mean that
378 the tree is closed. (old)
379 2. Check the json_url to decide whether to allow commit without prompt.
380 Args:
381 input_api: input related apis.
382 output_api: output related apis.
383 url: url to use for regex based tree status.
384 closed: regex to match for closed status.
385 json_url: url to download json style status.
386 """
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000387 if not input_api.is_committing:
388 return []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000389 try:
bradnelson@google.comc0b332a2010-08-26 00:30:37 +0000390 if json_url:
391 connection = input_api.urllib2.urlopen(json_url)
392 status = input_api.json.loads(connection.read())
393 connection.close()
394 if not status['can_commit_freely']:
395 short_text = 'Tree state is: ' + status['general_state']
396 long_text = status['message'] + '\n' + json_url
397 return [output_api.PresubmitError(short_text, long_text=long_text)]
398 else:
399 # TODO(bradnelson): drop this once all users are gone.
400 connection = input_api.urllib2.urlopen(url)
401 status = connection.read()
402 connection.close()
403 if input_api.re.match(closed, status):
404 long_text = status + '\n' + url
405 return [output_api.PresubmitError('The tree is closed.',
406 long_text=long_text)]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000407 except IOError:
408 pass
409 return []
maruel@chromium.org7b305e82009-05-19 18:24:20 +0000410
411
412def RunPythonUnitTests(input_api, output_api, unit_tests):
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000413 """Run the unit tests out of process, capture the output and use the result
414 code to determine success.
415 """
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000416 # We don't want to hinder users from uploading incomplete patches.
417 if input_api.is_committing:
418 message_type = output_api.PresubmitError
419 else:
420 message_type = output_api.PresubmitNotifyResult
maruel@chromium.org7b305e82009-05-19 18:24:20 +0000421 outputs = []
422 for unit_test in unit_tests:
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000423 # Run the unit tests out of process. This is because some unit tests
424 # stub out base libraries and don't clean up their mess. It's too easy to
425 # get subtle bugs.
426 cwd = None
427 env = None
428 unit_test_name = unit_test
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000429 # 'python -m test.unit_test' doesn't work. We need to change to the right
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000430 # directory instead.
431 if '.' in unit_test:
432 # Tests imported in submodules (subdirectories) assume that the current
433 # directory is in the PYTHONPATH. Manually fix that.
434 unit_test = unit_test.replace('.', '/')
435 cwd = input_api.os_path.dirname(unit_test)
436 unit_test = input_api.os_path.basename(unit_test)
437 env = input_api.environ.copy()
kbr@google.comab318592009-09-04 00:54:55 +0000438 # At least on Windows, it seems '.' must explicitly be in PYTHONPATH
439 backpath = [
440 '.', input_api.os_path.pathsep.join(['..'] * (cwd.count('/') + 1))
441 ]
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000442 if env.get('PYTHONPATH'):
443 backpath.append(env.get('PYTHONPATH'))
ukai@chromium.orga301f1f2009-08-05 10:37:33 +0000444 env['PYTHONPATH'] = input_api.os_path.pathsep.join((backpath))
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000445 subproc = input_api.subprocess.Popen(
446 [
447 input_api.python_executable,
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000448 '-m',
449 '%s' % unit_test
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000450 ],
451 cwd=cwd,
452 env=env,
453 stdin=input_api.subprocess.PIPE,
454 stdout=input_api.subprocess.PIPE,
455 stderr=input_api.subprocess.PIPE)
456 stdoutdata, stderrdata = subproc.communicate()
457 # Discard the output if returncode == 0
458 if subproc.returncode:
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000459 outputs.append('Test \'%s\' failed with code %d\n%s\n%s\n' % (
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000460 unit_test_name, subproc.returncode, stdoutdata, stderrdata))
461 if outputs:
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000462 return [message_type('%d unit tests failed.' % len(outputs),
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000463 long_text='\n'.join(outputs))]
464 return []
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000465
466
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000467def _FetchAllFiles(input_api, white_list, black_list):
468 """Hack to fetch all files."""
469 # We cannot use AffectedFiles here because we want to test every python
470 # file on each single python change. It's because a change in a python file
471 # can break another unmodified file.
472 # Use code similar to InputApi.FilterSourceFile()
473 def Find(filepath, filters):
474 for item in filters:
475 if input_api.re.match(item, filepath):
476 return True
477 return False
478
479 import os
480 files = []
481 path_len = len(input_api.PresubmitLocalPath())
482 for dirpath, dirnames, filenames in os.walk(input_api.PresubmitLocalPath()):
483 # Passes dirnames in black list to speed up search.
484 for item in dirnames[:]:
485 filepath = input_api.os_path.join(dirpath, item)[path_len + 1:]
486 if Find(filepath, black_list):
487 dirnames.remove(item)
488 for item in filenames:
489 filepath = input_api.os_path.join(dirpath, item)[path_len + 1:]
490 if Find(filepath, white_list) and not Find(filepath, black_list):
491 files.append(filepath)
492 return files
493
494
maruel@chromium.orgbf38a7e2010-12-14 18:15:54 +0000495def RunPylint(input_api, output_api, white_list=None, black_list=None):
496 """Run pylint on python files.
497
498 The default white_list enforces looking only a *.py files.
499 """
500 white_list = white_list or ['.*\.py$']
501 black_list = black_list or input_api.DEFAULT_BLACK_LIST
502
503 # Only trigger if there is at least one python file affected.
504 src_filter = lambda x: input_api.FilterSourceFile(x, white_list, black_list)
505 if not input_api.AffectedSourceFiles(src_filter):
506 return []
507
maruel@chromium.orge94aedc2010-12-13 21:11:30 +0000508 # On certain pylint/python version combination, running pylint throws a lot of
509 # warning messages.
maruel@chromium.orgbf38a7e2010-12-14 18:15:54 +0000510 import warnings
maruel@chromium.orge94aedc2010-12-13 21:11:30 +0000511 warnings.filterwarnings('ignore', category=DeprecationWarning)
512 try:
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000513 files = _FetchAllFiles(input_api, white_list, black_list)
514 if not files:
515 return []
maruel@chromium.orgbf38a7e2010-12-14 18:15:54 +0000516 # Now that at least one python file was modified and all the python files
517 # were listed, try to run pylint.
maruel@chromium.orge94aedc2010-12-13 21:11:30 +0000518 try:
519 from pylint import lint
maruel@chromium.orgfca53392010-12-21 18:42:57 +0000520 result = lint.Run(sorted(files))
521 except SystemExit, e:
522 # pylint has the bad habit of calling sys.exit(), trap it here.
523 result = e.code
maruel@chromium.orge94aedc2010-12-13 21:11:30 +0000524 except ImportError:
525 if input_api.platform == 'win32':
526 return [output_api.PresubmitNotifyResult(
527 'Warning: Can\'t run pylint because it is not installed. Please '
528 'install manually\n'
529 'Cannot do static analysis of python files.')]
530 return [output_api.PresubmitError(
531 'Please install pylint with "sudo apt-get install python-setuptools; '
532 'sudo easy_install pylint"\n'
533 'Cannot do static analysis of python files.')]
maruel@chromium.orgfca53392010-12-21 18:42:57 +0000534 if result:
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000535 if input_api.is_committing:
536 error_type = output_api.PresubmitError
537 else:
538 error_type = output_api.PresubmitPromptWarning
539 return [error_type('Fix pylint errors first.')]
maruel@chromium.orgfca53392010-12-21 18:42:57 +0000540 return []
maruel@chromium.orge94aedc2010-12-13 21:11:30 +0000541 finally:
542 warnings.filterwarnings('default', category=DeprecationWarning)
543
dpranke@chromium.org627ea672011-03-11 23:29:03 +0000544# TODO(dpranke): Get the host_url from the input_api instead
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000545def CheckRietveldTryJobExecution(input_api, output_api, host_url, platforms,
546 owner):
547 if not input_api.is_committing:
548 return []
549 if not input_api.change.issue or not input_api.change.patchset:
550 return []
551 url = '%s/%d/get_build_results/%d' % (
552 host_url, input_api.change.issue, input_api.change.patchset)
553 try:
554 connection = input_api.urllib2.urlopen(url)
555 # platform|status|url
556 values = [item.split('|', 2) for item in connection.read().splitlines()]
557 connection.close()
558 except input_api.urllib2.HTTPError, e:
559 if e.code == 404:
560 # Fallback to no try job.
561 return [output_api.PresubmitPromptWarning(
562 'You should try the patch first.')]
563 else:
564 # Another HTTP error happened, warn the user.
565 return [output_api.PresubmitPromptWarning(
566 'Got %s while looking for try job status.' % str(e))]
567
568 if not values:
569 # It returned an empty list. Probably a private review.
570 return []
571 # Reformat as an dict of platform: [status, url]
572 values = dict([[v[0], [v[1], v[2]]] for v in values if len(v) == 3])
573 if not values:
574 # It returned useless data.
575 return [output_api.PresubmitNotifyResult('Failed to parse try job results')]
576
577 for platform in platforms:
578 values.setdefault(platform, ['not started', ''])
579 message = None
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000580 non_success = [k.upper() for k, v in values.iteritems() if v[0] != 'success']
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000581 if 'failure' in [v[0] for v in values.itervalues()]:
582 message = 'Try job failures on %s!\n' % ', '.join(non_success)
583 elif non_success:
584 message = ('Unfinished (or not even started) try jobs on '
585 '%s.\n') % ', '.join(non_success)
586 if message:
587 message += (
588 'Is try server wrong or broken? Please notify %s. '
589 'Thanks.\n' % owner)
590 return [output_api.PresubmitPromptWarning(message=message)]
591 return []
592
593
594def CheckBuildbotPendingBuilds(input_api, output_api, url, max_pendings,
595 ignored):
596 if not input_api.json:
597 return [output_api.PresubmitPromptWarning(
598 'Please install simplejson or upgrade to python 2.6+')]
599 try:
600 connection = input_api.urllib2.urlopen(url)
601 raw_data = connection.read()
602 connection.close()
603 except IOError:
604 return [output_api.PresubmitNotifyResult('%s is not accessible' % url)]
605
606 try:
607 data = input_api.json.loads(raw_data)
608 except ValueError:
609 return [output_api.PresubmitNotifyResult('Received malformed json while '
610 'looking up buildbot status')]
611
612 out = []
613 for (builder_name, builder) in data.iteritems():
614 if builder_name in ignored:
615 continue
maruel@chromium.orgcf1982c2010-10-04 15:08:28 +0000616 if builder.get('state', '') == 'offline':
617 continue
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000618 pending_builds_len = len(builder.get('pending_builds', []))
619 if pending_builds_len > max_pendings:
620 out.append('%s has %d build(s) pending' %
621 (builder_name, pending_builds_len))
622 if out:
623 return [output_api.PresubmitPromptWarning(
624 'Build(s) pending. It is suggested to wait that no more than %d '
625 'builds are pending.' % max_pendings,
626 long_text='\n'.join(out))]
627 return []
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000628
629
dpranke@chromium.org3e331bd2011-03-24 23:13:04 +0000630def CheckOwners(input_api, output_api, source_file_filter=None):
631 if not input_api.is_committing:
632 return []
633 if input_api.tbr:
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +0000634 return [output_api.PresubmitNotifyResult(
635 '--tbr was specified, skipping OWNERS check')]
dpranke@chromium.org3e331bd2011-03-24 23:13:04 +0000636 if not input_api.change.issue:
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +0000637 return [output_api.PresubmitError(
638 "OWNERS check failed: this change has no Rietveld issue number, so "
639 "we can't check it for approvals.")]
dpranke@chromium.org3e331bd2011-03-24 23:13:04 +0000640
dpranke@chromium.orgadd5df42011-03-08 23:04:01 +0000641 affected_files = set([f.LocalPath() for f in
642 input_api.change.AffectedFiles(source_file_filter)])
dpranke@chromium.org3e331bd2011-03-24 23:13:04 +0000643
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000644 owners_db = input_api.owners_db
dpranke@chromium.org3e331bd2011-03-24 23:13:04 +0000645 owner_email, approvers = _RietveldOwnerAndApprovers(input_api,
646 owners_db.email_regexp)
647 approvers_plus_owner = approvers.union(set([owner_email]))
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000648
dpranke@chromium.org3e331bd2011-03-24 23:13:04 +0000649 missing_files = owners_db.files_not_covered_by(affected_files,
650 approvers_plus_owner)
651 if missing_files:
652 return [output_api.PresubmitError('Missing LGTM from an OWNER for: %s' %
653 ','.join(missing_files))]
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000654
dpranke@chromium.org3e331bd2011-03-24 23:13:04 +0000655 if not approvers:
656 return [output_api.PresubmitError('Missing LGTM from someone other than %s'
657 % owner_email)]
658 return []
dpranke@chromium.org627ea672011-03-11 23:29:03 +0000659
660
dpranke@chromium.org3e331bd2011-03-24 23:13:04 +0000661def _RietveldOwnerAndApprovers(input_api, email_regexp):
662 """Return the owner and approvers of a change, if any."""
dpranke@chromium.org1b98c432011-03-17 21:11:49 +0000663 # TODO(dpranke): Should figure out if input_api.host_url is supposed to
664 # be a host or a scheme+host and normalize it there.
665 host = input_api.host_url
666 if not host.startswith('http://') and not host.startswith('https://'):
667 host = 'http://' + host
668 url = '%s/api/%s?messages=true' % (host, input_api.change.issue)
dpranke@chromium.org627ea672011-03-11 23:29:03 +0000669
670 f = input_api.urllib2.urlopen(url)
671 issue_props = input_api.json.load(f)
dpranke@chromium.org3e331bd2011-03-24 23:13:04 +0000672 messages = issue_props.get('messages', [])
673 owner_email = issue_props['owner_email']
674 owner_regexp = input_api.re.compile(input_api.re.escape(owner_email))
675 approvers = _GetApprovers(messages, email_regexp, owner_regexp)
676
677 return (owner_email, set(approvers))
678
679
680def _IsApprovingComment(text):
681 """Implements the logic for parsing a change comment for approval."""
682
683 # Any comment that contains a non-quoted line containing an 'lgtm' is an
684 # approval.
685 #
686 # TODO(dpranke): this differs from the logic used inside Google in a few
687 # ways. Inside Google,
688 #
689 # 1) the approving phrase must appear at the beginning of the first non
690 # quoted-line in the comment.'
691 # 2) "LG", "Looks Good", and "Looks Good to Me" are also acceptable.
692 # 3) Subsequent comments from the reviewer can rescind approval, unless
693 # the phrase "LGTM++" was used.
694 # We should consider implementing some or all of this here.
695 for l in text.splitlines():
696 l = l.strip().lower()
697 if l.startswith('>'):
698 continue
699
700 if 'lgtm' in l:
701 return True
702 return False
703
704
705def _GetApprovers(messages, email_regexp, owner_regexp):
706 """Returns the set of approvers for a change given the owner and messages.
707
708 Messages should be a list of dicts containing 'sender' and 'text' keys."""
dpranke@chromium.org627ea672011-03-11 23:29:03 +0000709
710 # TODO(dpranke): This mimics the logic in
711 # /tools/commit-queue/verifiers/reviewer_lgtm.py
712 # We should share the code and/or remove the check there where it is
713 # redundant (since the commit queue also enforces the presubmit checks).
714 def match_reviewer(r):
dpranke@chromium.org3e331bd2011-03-24 23:13:04 +0000715 return email_regexp.match(r) and not owner_regexp.match(r)
dpranke@chromium.org627ea672011-03-11 23:29:03 +0000716
717 approvers = []
dpranke@chromium.org3e331bd2011-03-24 23:13:04 +0000718 for m in messages:
719 sender = m['sender']
720 if _IsApprovingComment(m['text']) and match_reviewer(sender):
721 approvers.append(sender)
dpranke@chromium.org627ea672011-03-11 23:29:03 +0000722 return set(approvers)
723
bradnelson@google.com56e48bc2011-03-24 20:51:21 +0000724
725def _CheckConstNSObject(input_api, output_api, source_file_filter):
726 """Checks to make sure no objective-c files have |const NSSomeClass*|."""
727 pattern = input_api.re.compile(r'const\s+NS\w*\s*\*')
728
729 def objective_c_filter(f):
730 return (source_file_filter(f) and
731 input_api.os_path.splitext(f.LocalPath())[1] in ('.h', '.mm'))
732
733 files = []
734 for f in input_api.AffectedSourceFiles(objective_c_filter):
735 contents = input_api.ReadFile(f)
736 if pattern.search(contents):
737 files.append(f)
738
739 if files:
740 if input_api.is_committing:
741 res_type = output_api.PresubmitPromptWarning
742 else:
743 res_type = output_api.PresubmitNotifyResult
744 return [ res_type('|const NSClass*| is wrong, see ' +
745 'http://dev.chromium.org/developers/clang-mac',
746 files) ]
747 return []
748
749
750def _CheckSingletonInHeaders(input_api, output_api, source_file_filter):
751 """Checks to make sure no header files have |Singleton<|."""
752 pattern = input_api.re.compile(r'Singleton<')
753 files = []
754 for f in input_api.AffectedSourceFiles(source_file_filter):
755 if (f.LocalPath().endswith('.h') or f.LocalPath().endswith('.hxx') or
756 f.LocalPath().endswith('.hpp') or f.LocalPath().endswith('.inl')):
757 contents = input_api.ReadFile(f)
758 if pattern.search(contents):
759 files.append(f)
760
761 if files:
762 return [ output_api.PresubmitError(
763 'Found Singleton<T> in the following header files.\n' +
764 'Please move them to an appropriate source file so that the ' +
765 'template gets instantiated in a single compilation unit.',
766 files) ]
767 return []
768
769
770def PanProjectChecks(input_api, output_api,
771 excluded_paths=None, text_files=None,
772 license_header=None, project_name=None):
773 """Checks that ALL chromium orbit projects should use.
774
775 These are checks to be run on all Chromium orbit project, including:
776 Chromium
777 Native Client
778 V8
779 When you update this function, please take this broad scope into account.
780 Args:
781 input_api: Bag of input related interfaces.
782 output_api: Bag of output related interfaces.
783 excluded_paths: Don't include these paths in common checks.
784 text_files: Which file are to be treated as documentation text files.
785 license_header: What license header should be on files.
786 project_name: What is the name of the project as it appears in the license.
787 Returns:
788 A list of warning or error objects.
789 """
790 excluded_paths = excluded_paths or tuple()
791 text_files = text_files or (
792 r'.*\.txt',
793 r'.*\.json',
794 )
795 project_name = project_name or 'Chromium'
796 license_header = license_header or (
797 r'.*? Copyright \(c\) %(year)s The %(project)s Authors\. '
798 r'All rights reserved\.\n'
799 r'.*? Use of this source code is governed by a BSD-style license that '
800 r'can be\n'
801 r'.*? found in the LICENSE file\.\n'
802 ) % {
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000803 'year': input_api.time.strftime('%Y'),
bradnelson@google.com56e48bc2011-03-24 20:51:21 +0000804 'project': project_name,
805 }
806
807 results = []
808 # This code loads the default black list (e.g. third_party, experimental, etc)
809 # and add our black list (breakpad, skia and v8 are still not following
810 # google style and are not really living this repository).
811 # See presubmit_support.py InputApi.FilterSourceFile for the (simple) usage.
812 black_list = input_api.DEFAULT_BLACK_LIST + excluded_paths
813 white_list = input_api.DEFAULT_WHITE_LIST + text_files
814 sources = lambda x: input_api.FilterSourceFile(x, black_list=black_list)
815 text_files = lambda x: input_api.FilterSourceFile(x, black_list=black_list,
816 white_list=white_list)
817
dpranke@chromium.org3e331bd2011-03-24 23:13:04 +0000818 results.extend(input_api.canned_checks.CheckOwners(
819 input_api, output_api, source_file_filter=sources))
bradnelson@google.com56e48bc2011-03-24 20:51:21 +0000820
821 results.extend(input_api.canned_checks.CheckLongLines(
822 input_api, output_api, source_file_filter=sources))
823 results.extend(input_api.canned_checks.CheckChangeHasNoTabs(
824 input_api, output_api, source_file_filter=sources))
825 results.extend(input_api.canned_checks.CheckChangeHasNoStrayWhitespace(
826 input_api, output_api, source_file_filter=sources))
827 results.extend(input_api.canned_checks.CheckChangeSvnEolStyle(
828 input_api, output_api, source_file_filter=text_files))
829 results.extend(input_api.canned_checks.CheckSvnForCommonMimeTypes(
830 input_api, output_api))
831 results.extend(input_api.canned_checks.CheckLicense(
832 input_api, output_api, license_header, source_file_filter=sources))
833 results.extend(_CheckConstNSObject(
834 input_api, output_api, source_file_filter=sources))
835 results.extend(_CheckSingletonInHeaders(
836 input_api, output_api, source_file_filter=sources))
837 return results