blob: 577dfc99526935b7f4b997cfa944647bd4523713 [file] [log] [blame]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001#!/usr/bin/env python
2# Copyright (c) 2006-2009 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Generic presubmit checks that can be reused by other presubmit checks."""
7
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(
16 "Changelist should have a TEST= field. TEST=none is allowed.")]
17
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(
25 "Changelist should have a BUG= field. BUG=none is allowed.")]
26
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:
33 return [output_api.PresubmitError("Changelist must have a TESTED= field.")]
34
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:
41 return [output_api.PresubmitError("Changelist must have a QA= field.")]
42
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(
50 keyword + " is present in the changelist description.")]
51 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:
60 return [output_api.PresubmitError("Add a description.")]
61 else:
62 return [output_api.PresubmitNotifyResult("Add a description.")]
63 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):
79 """Checks that all ".cc" and ".h" files pass cpplint.py."""
80 _RE_IS_TEST = input_api.re.compile(r'.*tests?.(cc|h)$')
81 result = []
82
83 # Initialize cpplint.
84 import cpplint
85 cpplint._cpplint_state.ResetErrorCounts()
86
87 # Justifications for each filter:
88 #
89 # - build/include : Too many; fix in the future.
90 # - build/include_order : Not happening; #ifdefed includes.
91 # - build/namespace : I'm surprised by how often we violate this rule.
92 # - readability/casting : Mistakes a whole bunch of function pointer.
93 # - runtime/int : Can be fixed long term; volume of errors too high
94 # - runtime/virtual : Broken now, but can be fixed in the future?
95 # - whitespace/braces : We have a lot of explicit scoping in chrome code.
96 cpplint._SetFilters("-build/include,-build/include_order,-build/namespace,"
97 "-readability/casting,-runtime/int,-runtime/virtual,"
98 "-whitespace/braces")
99
100 # We currently are more strict with normal code than unit tests; 4 and 5 are
101 # the verbosity level that would normally be passed to cpplint.py through
102 # --verbose=#. Hopefully, in the future, we can be more verbose.
103 files = [f.AbsoluteLocalPath() for f in
104 input_api.AffectedSourceFiles(source_file_filter)]
105 for file_name in files:
106 if _RE_IS_TEST.match(file_name):
107 level = 5
108 else:
109 level = 4
110
111 cpplint.ProcessFile(file_name, level)
112
113 if cpplint._cpplint_state.error_count > 0:
114 if input_api.is_committing:
115 res_type = output_api.PresubmitError
116 else:
117 res_type = output_api.PresubmitPromptWarning
118 result = [res_type("Changelist failed cpplint.py check.")]
119
120 return result
121
122
maruel@chromium.org3410d912009-06-09 20:56:16 +0000123def CheckChangeHasNoCR(input_api, output_api, source_file_filter=None):
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000124 """Checks no '\r' (CR) character is in any source files."""
125 cr_files = []
maruel@chromium.org3410d912009-06-09 20:56:16 +0000126 for f in input_api.AffectedSourceFiles(source_file_filter):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000127 if '\r' in input_api.ReadFile(f, 'rb'):
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000128 cr_files.append(f.LocalPath())
129 if cr_files:
130 return [output_api.PresubmitPromptWarning(
131 "Found a CR character in these files:", items=cr_files)]
132 return []
133
134
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000135def CheckSvnModifiedDirectories(input_api, output_api, source_file_filter=None):
136 """Checks for files in svn modified directories.
137
138 They will get submitted on accident because svn commits recursively by
139 default, and that's very dangerous.
140 """
141 if input_api.change.scm != 'svn':
142 return []
143
144 errors = []
145 current_cl_files = input_api.change.GetModifiedFiles()
146 all_modified_files = input_api.change.GetAllModifiedFiles()
147 # Filter out files in the current CL.
148 modified_files = [f for f in all_modified_files if f not in current_cl_files]
149 modified_abspaths = [input_api.os_path.abspath(f) for f in modified_files]
150
151 for f in input_api.AffectedFiles(source_file_filter):
152 if f.Action() == 'M' and f.IsDirectory():
153 curpath = f.AbsoluteLocalPath()
154 bad_files = []
155 # Check if any of the modified files in other CLs are under curpath.
156 for i in xrange(len(modified_files)):
157 abspath = modified_abspaths[i]
158 if input_api.os_path.commonprefix([curpath, abspath]) == curpath:
159 bad_files.append(modified_files[i])
160 if bad_files:
161 if input_api.is_committing:
162 error_type = output_api.PresubmitPromptWarning
163 else:
164 error_type = output_api.PresubmitNotifyResult
165 errors.append(error_type(
166 "Potential accidental commits in changelist %s:" % f.LocalPath(),
167 items=bad_files))
168 return errors
169
170
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000171def CheckChangeHasOnlyOneEol(input_api, output_api, source_file_filter=None):
172 """Checks the files ends with one and only one \n (LF)."""
173 eof_files = []
174 for f in input_api.AffectedSourceFiles(source_file_filter):
175 contents = input_api.ReadFile(f, 'rb')
176 # Check that the file ends in one and only one newline character.
177 if len(contents) > 1 and (contents[-1:] != "\n" or contents[-2:-1] == "\n"):
178 eof_files.append(f.LocalPath())
179
180 if eof_files:
181 return [output_api.PresubmitPromptWarning(
182 'These files should end in one (and only one) newline character:',
183 items=eof_files)]
184 return []
185
186
187def CheckChangeHasNoCrAndHasOnlyOneEol(input_api, output_api,
188 source_file_filter=None):
189 """Runs both CheckChangeHasNoCR and CheckChangeHasOnlyOneEOL in one pass.
190
191 It is faster because it is reading the file only once.
192 """
193 cr_files = []
194 eof_files = []
195 for f in input_api.AffectedSourceFiles(source_file_filter):
196 contents = input_api.ReadFile(f, 'rb')
197 if '\r' in contents:
198 cr_files.append(f.LocalPath())
199 # Check that the file ends in one and only one newline character.
200 if len(contents) > 1 and (contents[-1:] != "\n" or contents[-2:-1] == "\n"):
201 eof_files.append(f.LocalPath())
202 outputs = []
203 if cr_files:
204 outputs.append(output_api.PresubmitPromptWarning(
205 "Found a CR character in these files:", items=cr_files))
206 if eof_files:
207 outputs.append(output_api.PresubmitPromptWarning(
208 'These files should end in one (and only one) newline character:',
209 items=eof_files))
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000210 return outputs
211
212
maruel@chromium.org3410d912009-06-09 20:56:16 +0000213def CheckChangeHasNoTabs(input_api, output_api, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000214 """Checks that there are no tab characters in any of the text files to be
215 submitted.
216 """
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000217 tabs = []
maruel@chromium.org3410d912009-06-09 20:56:16 +0000218 for f, line_num, line in input_api.RightHandSideLines(source_file_filter):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000219 if '\t' in line:
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000220 tabs.append("%s, line %s" % (f.LocalPath(), line_num))
221 if tabs:
222 return [output_api.PresubmitPromptWarning("Found a tab character in:",
223 long_text="\n".join(tabs))]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000224 return []
225
226
maruel@chromium.orgf5888bb2009-06-10 20:26:37 +0000227def CheckChangeHasNoStrayWhitespace(input_api, output_api,
228 source_file_filter=None):
229 """Checks that there is no stray whitespace at source lines end."""
230 errors = []
231 for f, line_num, line in input_api.RightHandSideLines(source_file_filter):
232 if line.rstrip() != line:
233 errors.append("%s, line %s" % (f.LocalPath(), line_num))
234 if errors:
235 return [output_api.PresubmitPromptWarning(
236 "Found line ending with white spaces in:",
237 long_text="\n".join(errors))]
238 return []
239
240
maruel@chromium.org3410d912009-06-09 20:56:16 +0000241def CheckLongLines(input_api, output_api, maxlen=80, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000242 """Checks that there aren't any lines longer than maxlen characters in any of
243 the text files to be submitted.
244 """
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000245 bad = []
maruel@chromium.org3410d912009-06-09 20:56:16 +0000246 for f, line_num, line in input_api.RightHandSideLines(source_file_filter):
maruel@chromium.org5c2720e2009-06-09 14:04:08 +0000247 # Allow lines with http://, https:// and #define/#pragma/#include/#if/#endif
248 # to exceed the maxlen rule.
249 if (len(line) > maxlen and
250 not 'http://' in line and
251 not 'https://' in line and
252 not line.startswith('#define') and
253 not line.startswith('#include') and
254 not line.startswith('#pragma') and
255 not line.startswith('#if') and
256 not line.startswith('#endif')):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000257 bad.append(
258 '%s, line %s, %s chars' %
maruel@chromium.org1487d532009-06-06 00:22:57 +0000259 (f.LocalPath(), line_num, len(line)))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000260 if len(bad) == 5: # Just show the first 5 errors.
261 break
262
263 if bad:
264 msg = "Found lines longer than %s characters (first 5 shown)." % maxlen
265 return [output_api.PresubmitPromptWarning(msg, items=bad)]
266 else:
267 return []
268
269
maruel@chromium.orgb9e7ada2010-01-27 23:12:39 +0000270def CheckLicense(input_api, output_api, license, source_file_filter=None):
271 """Verifies the license header.
272 """
273 license_re = input_api.re.compile(license, input_api.re.MULTILINE)
274 bad_files = []
275 for f in input_api.AffectedSourceFiles(source_file_filter):
276 contents = input_api.ReadFile(f, 'rb')
277 if not license_re.search(contents):
278 bad_files.append(f.LocalPath())
279 if bad_files:
280 if input_api.is_committing:
281 res_type = output_api.PresubmitPromptWarning
282 else:
283 res_type = output_api.PresubmitNotifyResult
284 return [res_type(
285 "Found a bad license header in these files:", items=bad_files)]
286 return []
287
288
maruel@chromium.org1a0e3cb2009-06-10 18:03:04 +0000289def CheckChangeSvnEolStyle(input_api, output_api, source_file_filter=None):
maruel@chromium.orgb7d46902009-06-10 14:12:10 +0000290 """Checks that the source files have svn:eol-style=LF."""
maruel@chromium.org46e832a2009-06-18 19:58:07 +0000291 return CheckSvnProperty(input_api, output_api,
292 'svn:eol-style', 'LF',
293 input_api.AffectedSourceFiles(source_file_filter))
294
295
296def CheckSvnForCommonMimeTypes(input_api, output_api):
297 """Checks that common binary file types have the correct svn:mime-type."""
298 output = []
299 files = input_api.AffectedFiles(include_deletes=False)
maruel@chromium.orge49187c2009-06-26 22:44:53 +0000300 def IsExts(x, exts):
301 path = x.LocalPath()
302 for extension in exts:
303 if path.endswith(extension):
304 return True
305 return False
maruel@chromium.org46e832a2009-06-18 19:58:07 +0000306 def FilterFiles(extension):
maruel@chromium.orge49187c2009-06-26 22:44:53 +0000307 return filter(lambda x: IsExts(x, extension), files)
maruel@chromium.org46e832a2009-06-18 19:58:07 +0000308 def RunCheck(mime_type, files):
309 output.extend(CheckSvnProperty(input_api, output_api, 'svn:mime-type',
310 mime_type, files))
maruel@chromium.orge49187c2009-06-26 22:44:53 +0000311 RunCheck('application/pdf', FilterFiles(['.pdf']))
312 RunCheck('image/bmp', FilterFiles(['.bmp']))
313 RunCheck('image/gif', FilterFiles(['.gif']))
314 RunCheck('image/png', FilterFiles(['.png']))
315 RunCheck('image/jpeg', FilterFiles(['.jpg', '.jpeg', '.jpe']))
316 RunCheck('image/vnd.microsoft.icon', FilterFiles(['.ico']))
maruel@chromium.org46e832a2009-06-18 19:58:07 +0000317 return output
318
319
320def CheckSvnProperty(input_api, output_api, prop, expected, affected_files):
321 """Checks that affected_files files have prop=expected."""
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000322 if input_api.change.scm != 'svn':
323 return []
324
325 bad = filter(lambda f: f.Property(prop) != expected, affected_files)
maruel@chromium.orgb7d46902009-06-10 14:12:10 +0000326 if bad:
maruel@chromium.org0874d472009-06-10 19:08:33 +0000327 if input_api.is_committing:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000328 res_type = output_api.PresubmitError
maruel@chromium.org0874d472009-06-10 19:08:33 +0000329 else:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000330 res_type = output_api.PresubmitNotifyResult
maruel@chromium.org46e832a2009-06-18 19:58:07 +0000331 message = "Run `svn pset %s %s <item>` on these files:" % (prop, expected)
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000332 return [res_type(message, items=bad)]
maruel@chromium.orgb7d46902009-06-10 14:12:10 +0000333 return []
334
335
maruel@chromium.org3410d912009-06-09 20:56:16 +0000336### Other checks
337
338def CheckDoNotSubmit(input_api, output_api):
339 return (
340 CheckDoNotSubmitInDescription(input_api, output_api) +
341 CheckDoNotSubmitInFiles(input_api, output_api)
342 )
343
344
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000345def CheckTreeIsOpen(input_api, output_api, url, closed):
346 """Checks that an url's content doesn't match a regexp that would mean that
347 the tree is closed."""
maruel@chromium.org89491382009-06-06 18:58:39 +0000348 assert(input_api.is_committing)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000349 try:
350 connection = input_api.urllib2.urlopen(url)
351 status = connection.read()
352 connection.close()
353 if input_api.re.match(closed, status):
354 long_text = status + '\n' + url
maruel@chromium.org89491382009-06-06 18:58:39 +0000355 return [output_api.PresubmitPromptWarning("The tree is closed.",
356 long_text=long_text)]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000357 except IOError:
358 pass
359 return []
maruel@chromium.org7b305e82009-05-19 18:24:20 +0000360
361
362def RunPythonUnitTests(input_api, output_api, unit_tests):
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000363 """Run the unit tests out of process, capture the output and use the result
364 code to determine success.
365 """
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000366 # We don't want to hinder users from uploading incomplete patches.
367 if input_api.is_committing:
368 message_type = output_api.PresubmitError
369 else:
370 message_type = output_api.PresubmitNotifyResult
maruel@chromium.org7b305e82009-05-19 18:24:20 +0000371 outputs = []
372 for unit_test in unit_tests:
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000373 # Run the unit tests out of process. This is because some unit tests
374 # stub out base libraries and don't clean up their mess. It's too easy to
375 # get subtle bugs.
376 cwd = None
377 env = None
378 unit_test_name = unit_test
379 # "python -m test.unit_test" doesn't work. We need to change to the right
380 # directory instead.
381 if '.' in unit_test:
382 # Tests imported in submodules (subdirectories) assume that the current
383 # directory is in the PYTHONPATH. Manually fix that.
384 unit_test = unit_test.replace('.', '/')
385 cwd = input_api.os_path.dirname(unit_test)
386 unit_test = input_api.os_path.basename(unit_test)
387 env = input_api.environ.copy()
kbr@google.comab318592009-09-04 00:54:55 +0000388 # At least on Windows, it seems '.' must explicitly be in PYTHONPATH
389 backpath = [
390 '.', input_api.os_path.pathsep.join(['..'] * (cwd.count('/') + 1))
391 ]
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000392 if env.get('PYTHONPATH'):
393 backpath.append(env.get('PYTHONPATH'))
ukai@chromium.orga301f1f2009-08-05 10:37:33 +0000394 env['PYTHONPATH'] = input_api.os_path.pathsep.join((backpath))
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000395 subproc = input_api.subprocess.Popen(
396 [
397 input_api.python_executable,
398 "-m",
399 "%s" % unit_test
400 ],
401 cwd=cwd,
402 env=env,
403 stdin=input_api.subprocess.PIPE,
404 stdout=input_api.subprocess.PIPE,
405 stderr=input_api.subprocess.PIPE)
406 stdoutdata, stderrdata = subproc.communicate()
407 # Discard the output if returncode == 0
408 if subproc.returncode:
409 outputs.append("Test '%s' failed with code %d\n%s\n%s\n" % (
410 unit_test_name, subproc.returncode, stdoutdata, stderrdata))
411 if outputs:
412 return [message_type("%d unit tests failed." % len(outputs),
413 long_text='\n'.join(outputs))]
414 return []