blob: 2928915cab3767654ebf6bbb7cce8dfbe30b259e [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
maruel@chromium.org3410d912009-06-09 20:56:16 +00007### Description checks
8
kuchhal@chromium.org00c41e42009-05-12 21:43:13 +00009def CheckChangeHasTestField(input_api, output_api):
10 """Requires that the changelist have a TEST= field."""
maruel@chromium.orge1a524f2009-05-27 14:43:46 +000011 if input_api.change.TEST:
kuchhal@chromium.org00c41e42009-05-12 21:43:13 +000012 return []
13 else:
14 return [output_api.PresubmitNotifyResult(
jam@chromium.org5c76de92011-01-24 18:19:21 +000015 'If this change requires manual test instructions to QA team, add '
16 'TEST=[instructions].')]
kuchhal@chromium.org00c41e42009-05-12 21:43:13 +000017
18
19def CheckChangeHasBugField(input_api, output_api):
20 """Requires that the changelist have a BUG= field."""
maruel@chromium.orge1a524f2009-05-27 14:43:46 +000021 if input_api.change.BUG:
kuchhal@chromium.org00c41e42009-05-12 21:43:13 +000022 return []
23 else:
24 return [output_api.PresubmitNotifyResult(
jam@chromium.org5c76de92011-01-24 18:19:21 +000025 'If this change has an associated bug, add BUG=[bug number].')]
kuchhal@chromium.org00c41e42009-05-12 21:43:13 +000026
27
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000028def CheckChangeHasTestedField(input_api, output_api):
29 """Requires that the changelist have a TESTED= field."""
maruel@chromium.orge1a524f2009-05-27 14:43:46 +000030 if input_api.change.TESTED:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000031 return []
32 else:
maruel@chromium.org3fbcb082010-03-19 14:03:28 +000033 return [output_api.PresubmitError('Changelist must have a TESTED= field.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000034
35
36def CheckChangeHasQaField(input_api, output_api):
37 """Requires that the changelist have a QA= field."""
38 if input_api.change.QA:
39 return []
40 else:
maruel@chromium.org3fbcb082010-03-19 14:03:28 +000041 return [output_api.PresubmitError('Changelist must have a QA= field.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000042
43
44def CheckDoNotSubmitInDescription(input_api, output_api):
45 """Checks that the user didn't add 'DO NOT ' + 'SUBMIT' to the CL description.
46 """
47 keyword = 'DO NOT ' + 'SUBMIT'
48 if keyword in input_api.change.DescriptionText():
49 return [output_api.PresubmitError(
maruel@chromium.org3fbcb082010-03-19 14:03:28 +000050 keyword + ' is present in the changelist description.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000051 else:
52 return []
53
54
maruel@chromium.orgbc50eb42009-06-10 18:22:47 +000055def CheckChangeHasDescription(input_api, output_api):
56 """Checks the CL description is not empty."""
57 text = input_api.change.DescriptionText()
58 if text.strip() == '':
59 if input_api.is_committing:
maruel@chromium.org3fbcb082010-03-19 14:03:28 +000060 return [output_api.PresubmitError('Add a description.')]
maruel@chromium.orgbc50eb42009-06-10 18:22:47 +000061 else:
maruel@chromium.org3fbcb082010-03-19 14:03:28 +000062 return [output_api.PresubmitNotifyResult('Add a description.')]
maruel@chromium.orgbc50eb42009-06-10 18:22:47 +000063 return []
64
maruel@chromium.org3410d912009-06-09 20:56:16 +000065### Content checks
66
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000067def CheckDoNotSubmitInFiles(input_api, output_api):
68 """Checks that the user didn't add 'DO NOT ' + 'SUBMIT' to any files."""
69 keyword = 'DO NOT ' + 'SUBMIT'
maruel@chromium.org3410d912009-06-09 20:56:16 +000070 # We want to check every text files, not just source files.
71 for f, line_num, line in input_api.RightHandSideLines(lambda x: x):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000072 if keyword in line:
73 text = 'Found ' + keyword + ' in %s, line %s' % (f.LocalPath(), line_num)
74 return [output_api.PresubmitError(text)]
75 return []
76
77
erg@google.com26970fa2009-11-17 18:07:32 +000078def CheckChangeLintsClean(input_api, output_api, source_file_filter=None):
maruel@chromium.org3fbcb082010-03-19 14:03:28 +000079 """Checks that all '.cc' and '.h' files pass cpplint.py."""
erg@google.com26970fa2009-11-17 18:07:32 +000080 _RE_IS_TEST = input_api.re.compile(r'.*tests?.(cc|h)$')
81 result = []
82
83 # Initialize cpplint.
84 import cpplint
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +000085 # Access to a protected member _XX of a client class
86 # pylint: disable=W0212
erg@google.com26970fa2009-11-17 18:07:32 +000087 cpplint._cpplint_state.ResetErrorCounts()
88
89 # Justifications for each filter:
90 #
91 # - build/include : Too many; fix in the future.
92 # - build/include_order : Not happening; #ifdefed includes.
93 # - build/namespace : I'm surprised by how often we violate this rule.
94 # - readability/casting : Mistakes a whole bunch of function pointer.
95 # - runtime/int : Can be fixed long term; volume of errors too high
96 # - runtime/virtual : Broken now, but can be fixed in the future?
97 # - whitespace/braces : We have a lot of explicit scoping in chrome code.
maruel@chromium.org3fbcb082010-03-19 14:03:28 +000098 cpplint._SetFilters('-build/include,-build/include_order,-build/namespace,'
99 '-readability/casting,-runtime/int,-runtime/virtual,'
100 '-whitespace/braces')
erg@google.com26970fa2009-11-17 18:07:32 +0000101
102 # We currently are more strict with normal code than unit tests; 4 and 5 are
103 # the verbosity level that would normally be passed to cpplint.py through
104 # --verbose=#. Hopefully, in the future, we can be more verbose.
105 files = [f.AbsoluteLocalPath() for f in
106 input_api.AffectedSourceFiles(source_file_filter)]
107 for file_name in files:
108 if _RE_IS_TEST.match(file_name):
109 level = 5
110 else:
111 level = 4
112
113 cpplint.ProcessFile(file_name, level)
114
115 if cpplint._cpplint_state.error_count > 0:
116 if input_api.is_committing:
117 res_type = output_api.PresubmitError
118 else:
119 res_type = output_api.PresubmitPromptWarning
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000120 result = [res_type('Changelist failed cpplint.py check.')]
erg@google.com26970fa2009-11-17 18:07:32 +0000121
122 return result
123
124
maruel@chromium.org3410d912009-06-09 20:56:16 +0000125def CheckChangeHasNoCR(input_api, output_api, source_file_filter=None):
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000126 """Checks no '\r' (CR) character is in any source files."""
127 cr_files = []
maruel@chromium.org3410d912009-06-09 20:56:16 +0000128 for f in input_api.AffectedSourceFiles(source_file_filter):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000129 if '\r' in input_api.ReadFile(f, 'rb'):
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000130 cr_files.append(f.LocalPath())
131 if cr_files:
132 return [output_api.PresubmitPromptWarning(
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000133 'Found a CR character in these files:', items=cr_files)]
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000134 return []
135
136
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000137def CheckSvnModifiedDirectories(input_api, output_api, source_file_filter=None):
138 """Checks for files in svn modified directories.
139
140 They will get submitted on accident because svn commits recursively by
141 default, and that's very dangerous.
142 """
143 if input_api.change.scm != 'svn':
144 return []
145
146 errors = []
147 current_cl_files = input_api.change.GetModifiedFiles()
148 all_modified_files = input_api.change.GetAllModifiedFiles()
149 # Filter out files in the current CL.
150 modified_files = [f for f in all_modified_files if f not in current_cl_files]
151 modified_abspaths = [input_api.os_path.abspath(f) for f in modified_files]
152
153 for f in input_api.AffectedFiles(source_file_filter):
154 if f.Action() == 'M' and f.IsDirectory():
155 curpath = f.AbsoluteLocalPath()
156 bad_files = []
157 # Check if any of the modified files in other CLs are under curpath.
158 for i in xrange(len(modified_files)):
159 abspath = modified_abspaths[i]
160 if input_api.os_path.commonprefix([curpath, abspath]) == curpath:
161 bad_files.append(modified_files[i])
162 if bad_files:
163 if input_api.is_committing:
164 error_type = output_api.PresubmitPromptWarning
165 else:
166 error_type = output_api.PresubmitNotifyResult
167 errors.append(error_type(
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000168 'Potential accidental commits in changelist %s:' % f.LocalPath(),
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000169 items=bad_files))
170 return errors
171
172
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000173def CheckChangeHasOnlyOneEol(input_api, output_api, source_file_filter=None):
174 """Checks the files ends with one and only one \n (LF)."""
175 eof_files = []
176 for f in input_api.AffectedSourceFiles(source_file_filter):
177 contents = input_api.ReadFile(f, 'rb')
178 # Check that the file ends in one and only one newline character.
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000179 if len(contents) > 1 and (contents[-1:] != '\n' or contents[-2:-1] == '\n'):
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000180 eof_files.append(f.LocalPath())
181
182 if eof_files:
183 return [output_api.PresubmitPromptWarning(
184 'These files should end in one (and only one) newline character:',
185 items=eof_files)]
186 return []
187
188
189def CheckChangeHasNoCrAndHasOnlyOneEol(input_api, output_api,
190 source_file_filter=None):
191 """Runs both CheckChangeHasNoCR and CheckChangeHasOnlyOneEOL in one pass.
192
193 It is faster because it is reading the file only once.
194 """
195 cr_files = []
196 eof_files = []
197 for f in input_api.AffectedSourceFiles(source_file_filter):
198 contents = input_api.ReadFile(f, 'rb')
199 if '\r' in contents:
200 cr_files.append(f.LocalPath())
201 # Check that the file ends in one and only one newline character.
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000202 if len(contents) > 1 and (contents[-1:] != '\n' or contents[-2:-1] == '\n'):
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000203 eof_files.append(f.LocalPath())
204 outputs = []
205 if cr_files:
206 outputs.append(output_api.PresubmitPromptWarning(
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000207 'Found a CR character in these files:', items=cr_files))
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000208 if eof_files:
209 outputs.append(output_api.PresubmitPromptWarning(
210 'These files should end in one (and only one) newline character:',
211 items=eof_files))
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000212 return outputs
213
214
maruel@chromium.org3410d912009-06-09 20:56:16 +0000215def CheckChangeHasNoTabs(input_api, output_api, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000216 """Checks that there are no tab characters in any of the text files to be
217 submitted.
218 """
maruel@chromium.org115ae6c2010-06-18 17:11:43 +0000219 # In addition to the filter, make sure that makefiles are blacklisted.
220 if not source_file_filter:
221 # It's the default filter.
222 source_file_filter = input_api.FilterSourceFile
223 def filter_more(affected_file):
224 return (not input_api.os_path.basename(affected_file.LocalPath()) in
225 ('Makefile', 'makefile') and
226 source_file_filter(affected_file))
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000227 tabs = []
maruel@chromium.org115ae6c2010-06-18 17:11:43 +0000228 for f, line_num, line in input_api.RightHandSideLines(filter_more):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000229 if '\t' in line:
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000230 tabs.append('%s, line %s' % (f.LocalPath(), line_num))
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000231 if tabs:
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000232 return [output_api.PresubmitPromptWarning('Found a tab character in:',
233 long_text='\n'.join(tabs))]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000234 return []
235
236
maruel@chromium.orgf5888bb2009-06-10 20:26:37 +0000237def CheckChangeHasNoStrayWhitespace(input_api, output_api,
238 source_file_filter=None):
239 """Checks that there is no stray whitespace at source lines end."""
240 errors = []
241 for f, line_num, line in input_api.RightHandSideLines(source_file_filter):
242 if line.rstrip() != line:
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000243 errors.append('%s, line %s' % (f.LocalPath(), line_num))
maruel@chromium.orgf5888bb2009-06-10 20:26:37 +0000244 if errors:
245 return [output_api.PresubmitPromptWarning(
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000246 'Found line ending with white spaces in:',
247 long_text='\n'.join(errors))]
maruel@chromium.orgf5888bb2009-06-10 20:26:37 +0000248 return []
249
250
maruel@chromium.org3410d912009-06-09 20:56:16 +0000251def CheckLongLines(input_api, output_api, maxlen=80, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000252 """Checks that there aren't any lines longer than maxlen characters in any of
253 the text files to be submitted.
254 """
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000255 bad = []
maruel@chromium.org3410d912009-06-09 20:56:16 +0000256 for f, line_num, line in input_api.RightHandSideLines(source_file_filter):
maruel@chromium.org5c2720e2009-06-09 14:04:08 +0000257 # Allow lines with http://, https:// and #define/#pragma/#include/#if/#endif
258 # to exceed the maxlen rule.
259 if (len(line) > maxlen and
260 not 'http://' in line and
261 not 'https://' in line and
262 not line.startswith('#define') and
263 not line.startswith('#include') and
thakis@chromium.org09829bf2010-10-02 01:58:17 +0000264 not line.startswith('#import') and
maruel@chromium.org5c2720e2009-06-09 14:04:08 +0000265 not line.startswith('#pragma') and
266 not line.startswith('#if') and
267 not line.startswith('#endif')):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000268 bad.append(
269 '%s, line %s, %s chars' %
maruel@chromium.org1487d532009-06-06 00:22:57 +0000270 (f.LocalPath(), line_num, len(line)))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000271 if len(bad) == 5: # Just show the first 5 errors.
272 break
273
274 if bad:
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000275 msg = 'Found lines longer than %s characters (first 5 shown).' % maxlen
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000276 return [output_api.PresubmitPromptWarning(msg, items=bad)]
277 else:
278 return []
279
280
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000281def CheckLicense(input_api, output_api, license_re, source_file_filter=None,
maruel@chromium.org71626852010-11-03 13:14:25 +0000282 accept_empty_files=True):
maruel@chromium.orgb9e7ada2010-01-27 23:12:39 +0000283 """Verifies the license header.
284 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000285 license_re = input_api.re.compile(license_re, input_api.re.MULTILINE)
maruel@chromium.orgb9e7ada2010-01-27 23:12:39 +0000286 bad_files = []
287 for f in input_api.AffectedSourceFiles(source_file_filter):
288 contents = input_api.ReadFile(f, 'rb')
maruel@chromium.org71626852010-11-03 13:14:25 +0000289 if accept_empty_files and not contents:
290 continue
maruel@chromium.orgb9e7ada2010-01-27 23:12:39 +0000291 if not license_re.search(contents):
292 bad_files.append(f.LocalPath())
293 if bad_files:
294 if input_api.is_committing:
295 res_type = output_api.PresubmitPromptWarning
296 else:
297 res_type = output_api.PresubmitNotifyResult
298 return [res_type(
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000299 'Found a bad license header in these files:', items=bad_files)]
maruel@chromium.orgb9e7ada2010-01-27 23:12:39 +0000300 return []
301
302
maruel@chromium.org1a0e3cb2009-06-10 18:03:04 +0000303def CheckChangeSvnEolStyle(input_api, output_api, source_file_filter=None):
maruel@chromium.orgb7d46902009-06-10 14:12:10 +0000304 """Checks that the source files have svn:eol-style=LF."""
maruel@chromium.org46e832a2009-06-18 19:58:07 +0000305 return CheckSvnProperty(input_api, output_api,
306 'svn:eol-style', 'LF',
307 input_api.AffectedSourceFiles(source_file_filter))
308
309
310def CheckSvnForCommonMimeTypes(input_api, output_api):
311 """Checks that common binary file types have the correct svn:mime-type."""
312 output = []
313 files = input_api.AffectedFiles(include_deletes=False)
maruel@chromium.orge49187c2009-06-26 22:44:53 +0000314 def IsExts(x, exts):
315 path = x.LocalPath()
316 for extension in exts:
317 if path.endswith(extension):
318 return True
319 return False
maruel@chromium.org46e832a2009-06-18 19:58:07 +0000320 def FilterFiles(extension):
maruel@chromium.orge49187c2009-06-26 22:44:53 +0000321 return filter(lambda x: IsExts(x, extension), files)
maruel@chromium.org46e832a2009-06-18 19:58:07 +0000322 def RunCheck(mime_type, files):
323 output.extend(CheckSvnProperty(input_api, output_api, 'svn:mime-type',
324 mime_type, files))
maruel@chromium.orge49187c2009-06-26 22:44:53 +0000325 RunCheck('application/pdf', FilterFiles(['.pdf']))
326 RunCheck('image/bmp', FilterFiles(['.bmp']))
327 RunCheck('image/gif', FilterFiles(['.gif']))
328 RunCheck('image/png', FilterFiles(['.png']))
329 RunCheck('image/jpeg', FilterFiles(['.jpg', '.jpeg', '.jpe']))
330 RunCheck('image/vnd.microsoft.icon', FilterFiles(['.ico']))
maruel@chromium.org46e832a2009-06-18 19:58:07 +0000331 return output
332
333
334def CheckSvnProperty(input_api, output_api, prop, expected, affected_files):
335 """Checks that affected_files files have prop=expected."""
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000336 if input_api.change.scm != 'svn':
337 return []
338
339 bad = filter(lambda f: f.Property(prop) != expected, affected_files)
maruel@chromium.orgb7d46902009-06-10 14:12:10 +0000340 if bad:
maruel@chromium.org0874d472009-06-10 19:08:33 +0000341 if input_api.is_committing:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000342 res_type = output_api.PresubmitError
maruel@chromium.org0874d472009-06-10 19:08:33 +0000343 else:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000344 res_type = output_api.PresubmitNotifyResult
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000345 message = 'Run the command: svn pset %s %s \\' % (prop, expected)
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000346 return [res_type(message, items=bad)]
maruel@chromium.orgb7d46902009-06-10 14:12:10 +0000347 return []
348
349
maruel@chromium.org3410d912009-06-09 20:56:16 +0000350### Other checks
351
352def CheckDoNotSubmit(input_api, output_api):
353 return (
354 CheckDoNotSubmitInDescription(input_api, output_api) +
355 CheckDoNotSubmitInFiles(input_api, output_api)
356 )
357
358
bradnelson@google.comc0b332a2010-08-26 00:30:37 +0000359def CheckTreeIsOpen(input_api, output_api,
360 url=None, closed=None, json_url=None):
361 """Check whether to allow commit without prompt.
362
363 Supports two styles:
364 1. Checks that an url's content doesn't match a regexp that would mean that
365 the tree is closed. (old)
366 2. Check the json_url to decide whether to allow commit without prompt.
367 Args:
368 input_api: input related apis.
369 output_api: output related apis.
370 url: url to use for regex based tree status.
371 closed: regex to match for closed status.
372 json_url: url to download json style status.
373 """
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000374 if not input_api.is_committing:
375 return []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000376 try:
bradnelson@google.comc0b332a2010-08-26 00:30:37 +0000377 if json_url:
378 connection = input_api.urllib2.urlopen(json_url)
379 status = input_api.json.loads(connection.read())
380 connection.close()
381 if not status['can_commit_freely']:
382 short_text = 'Tree state is: ' + status['general_state']
383 long_text = status['message'] + '\n' + json_url
384 return [output_api.PresubmitError(short_text, long_text=long_text)]
385 else:
386 # TODO(bradnelson): drop this once all users are gone.
387 connection = input_api.urllib2.urlopen(url)
388 status = connection.read()
389 connection.close()
390 if input_api.re.match(closed, status):
391 long_text = status + '\n' + url
392 return [output_api.PresubmitError('The tree is closed.',
393 long_text=long_text)]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000394 except IOError:
395 pass
396 return []
maruel@chromium.org7b305e82009-05-19 18:24:20 +0000397
398
399def RunPythonUnitTests(input_api, output_api, unit_tests):
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000400 """Run the unit tests out of process, capture the output and use the result
401 code to determine success.
402 """
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000403 # We don't want to hinder users from uploading incomplete patches.
404 if input_api.is_committing:
405 message_type = output_api.PresubmitError
406 else:
407 message_type = output_api.PresubmitNotifyResult
maruel@chromium.org7b305e82009-05-19 18:24:20 +0000408 outputs = []
409 for unit_test in unit_tests:
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000410 # Run the unit tests out of process. This is because some unit tests
411 # stub out base libraries and don't clean up their mess. It's too easy to
412 # get subtle bugs.
413 cwd = None
414 env = None
415 unit_test_name = unit_test
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000416 # 'python -m test.unit_test' doesn't work. We need to change to the right
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000417 # directory instead.
418 if '.' in unit_test:
419 # Tests imported in submodules (subdirectories) assume that the current
420 # directory is in the PYTHONPATH. Manually fix that.
421 unit_test = unit_test.replace('.', '/')
422 cwd = input_api.os_path.dirname(unit_test)
423 unit_test = input_api.os_path.basename(unit_test)
424 env = input_api.environ.copy()
kbr@google.comab318592009-09-04 00:54:55 +0000425 # At least on Windows, it seems '.' must explicitly be in PYTHONPATH
426 backpath = [
427 '.', input_api.os_path.pathsep.join(['..'] * (cwd.count('/') + 1))
428 ]
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000429 if env.get('PYTHONPATH'):
430 backpath.append(env.get('PYTHONPATH'))
ukai@chromium.orga301f1f2009-08-05 10:37:33 +0000431 env['PYTHONPATH'] = input_api.os_path.pathsep.join((backpath))
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000432 subproc = input_api.subprocess.Popen(
433 [
434 input_api.python_executable,
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000435 '-m',
436 '%s' % unit_test
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000437 ],
438 cwd=cwd,
439 env=env,
440 stdin=input_api.subprocess.PIPE,
441 stdout=input_api.subprocess.PIPE,
442 stderr=input_api.subprocess.PIPE)
443 stdoutdata, stderrdata = subproc.communicate()
444 # Discard the output if returncode == 0
445 if subproc.returncode:
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000446 outputs.append('Test \'%s\' failed with code %d\n%s\n%s\n' % (
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000447 unit_test_name, subproc.returncode, stdoutdata, stderrdata))
448 if outputs:
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000449 return [message_type('%d unit tests failed.' % len(outputs),
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000450 long_text='\n'.join(outputs))]
451 return []
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000452
453
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000454def _FetchAllFiles(input_api, white_list, black_list):
455 """Hack to fetch all files."""
456 # We cannot use AffectedFiles here because we want to test every python
457 # file on each single python change. It's because a change in a python file
458 # can break another unmodified file.
459 # Use code similar to InputApi.FilterSourceFile()
460 def Find(filepath, filters):
461 for item in filters:
462 if input_api.re.match(item, filepath):
463 return True
464 return False
465
466 import os
467 files = []
468 path_len = len(input_api.PresubmitLocalPath())
469 for dirpath, dirnames, filenames in os.walk(input_api.PresubmitLocalPath()):
470 # Passes dirnames in black list to speed up search.
471 for item in dirnames[:]:
472 filepath = input_api.os_path.join(dirpath, item)[path_len + 1:]
473 if Find(filepath, black_list):
474 dirnames.remove(item)
475 for item in filenames:
476 filepath = input_api.os_path.join(dirpath, item)[path_len + 1:]
477 if Find(filepath, white_list) and not Find(filepath, black_list):
478 files.append(filepath)
479 return files
480
481
maruel@chromium.orgbf38a7e2010-12-14 18:15:54 +0000482def RunPylint(input_api, output_api, white_list=None, black_list=None):
483 """Run pylint on python files.
484
485 The default white_list enforces looking only a *.py files.
486 """
487 white_list = white_list or ['.*\.py$']
488 black_list = black_list or input_api.DEFAULT_BLACK_LIST
489
490 # Only trigger if there is at least one python file affected.
491 src_filter = lambda x: input_api.FilterSourceFile(x, white_list, black_list)
492 if not input_api.AffectedSourceFiles(src_filter):
493 return []
494
maruel@chromium.orge94aedc2010-12-13 21:11:30 +0000495 # On certain pylint/python version combination, running pylint throws a lot of
496 # warning messages.
maruel@chromium.orgbf38a7e2010-12-14 18:15:54 +0000497 import warnings
maruel@chromium.orge94aedc2010-12-13 21:11:30 +0000498 warnings.filterwarnings('ignore', category=DeprecationWarning)
499 try:
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000500 files = _FetchAllFiles(input_api, white_list, black_list)
501 if not files:
502 return []
maruel@chromium.orgbf38a7e2010-12-14 18:15:54 +0000503 # Now that at least one python file was modified and all the python files
504 # were listed, try to run pylint.
maruel@chromium.orge94aedc2010-12-13 21:11:30 +0000505 try:
506 from pylint import lint
maruel@chromium.orgfca53392010-12-21 18:42:57 +0000507 result = lint.Run(sorted(files))
508 except SystemExit, e:
509 # pylint has the bad habit of calling sys.exit(), trap it here.
510 result = e.code
maruel@chromium.orge94aedc2010-12-13 21:11:30 +0000511 except ImportError:
512 if input_api.platform == 'win32':
513 return [output_api.PresubmitNotifyResult(
514 'Warning: Can\'t run pylint because it is not installed. Please '
515 'install manually\n'
516 'Cannot do static analysis of python files.')]
517 return [output_api.PresubmitError(
518 'Please install pylint with "sudo apt-get install python-setuptools; '
519 'sudo easy_install pylint"\n'
520 'Cannot do static analysis of python files.')]
maruel@chromium.orgfca53392010-12-21 18:42:57 +0000521 if result:
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000522 if input_api.is_committing:
523 error_type = output_api.PresubmitError
524 else:
525 error_type = output_api.PresubmitPromptWarning
526 return [error_type('Fix pylint errors first.')]
maruel@chromium.orgfca53392010-12-21 18:42:57 +0000527 return []
maruel@chromium.orge94aedc2010-12-13 21:11:30 +0000528 finally:
529 warnings.filterwarnings('default', category=DeprecationWarning)
530
531
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000532def CheckRietveldTryJobExecution(input_api, output_api, host_url, platforms,
533 owner):
534 if not input_api.is_committing:
535 return []
536 if not input_api.change.issue or not input_api.change.patchset:
537 return []
538 url = '%s/%d/get_build_results/%d' % (
539 host_url, input_api.change.issue, input_api.change.patchset)
540 try:
541 connection = input_api.urllib2.urlopen(url)
542 # platform|status|url
543 values = [item.split('|', 2) for item in connection.read().splitlines()]
544 connection.close()
545 except input_api.urllib2.HTTPError, e:
546 if e.code == 404:
547 # Fallback to no try job.
548 return [output_api.PresubmitPromptWarning(
549 'You should try the patch first.')]
550 else:
551 # Another HTTP error happened, warn the user.
552 return [output_api.PresubmitPromptWarning(
553 'Got %s while looking for try job status.' % str(e))]
554
555 if not values:
556 # It returned an empty list. Probably a private review.
557 return []
558 # Reformat as an dict of platform: [status, url]
559 values = dict([[v[0], [v[1], v[2]]] for v in values if len(v) == 3])
560 if not values:
561 # It returned useless data.
562 return [output_api.PresubmitNotifyResult('Failed to parse try job results')]
563
564 for platform in platforms:
565 values.setdefault(platform, ['not started', ''])
566 message = None
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000567 non_success = [k.upper() for k, v in values.iteritems() if v[0] != 'success']
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000568 if 'failure' in [v[0] for v in values.itervalues()]:
569 message = 'Try job failures on %s!\n' % ', '.join(non_success)
570 elif non_success:
571 message = ('Unfinished (or not even started) try jobs on '
572 '%s.\n') % ', '.join(non_success)
573 if message:
574 message += (
575 'Is try server wrong or broken? Please notify %s. '
576 'Thanks.\n' % owner)
577 return [output_api.PresubmitPromptWarning(message=message)]
578 return []
579
580
581def CheckBuildbotPendingBuilds(input_api, output_api, url, max_pendings,
582 ignored):
583 if not input_api.json:
584 return [output_api.PresubmitPromptWarning(
585 'Please install simplejson or upgrade to python 2.6+')]
586 try:
587 connection = input_api.urllib2.urlopen(url)
588 raw_data = connection.read()
589 connection.close()
590 except IOError:
591 return [output_api.PresubmitNotifyResult('%s is not accessible' % url)]
592
593 try:
594 data = input_api.json.loads(raw_data)
595 except ValueError:
596 return [output_api.PresubmitNotifyResult('Received malformed json while '
597 'looking up buildbot status')]
598
599 out = []
600 for (builder_name, builder) in data.iteritems():
601 if builder_name in ignored:
602 continue
maruel@chromium.orgcf1982c2010-10-04 15:08:28 +0000603 if builder.get('state', '') == 'offline':
604 continue
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000605 pending_builds_len = len(builder.get('pending_builds', []))
606 if pending_builds_len > max_pendings:
607 out.append('%s has %d build(s) pending' %
608 (builder_name, pending_builds_len))
609 if out:
610 return [output_api.PresubmitPromptWarning(
611 'Build(s) pending. It is suggested to wait that no more than %d '
612 'builds are pending.' % max_pendings,
613 long_text='\n'.join(out))]
614 return []