blob: 3a57490078bc2a8fd0af1de414d32f231fa4d404 [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.org1a0e3cb2009-06-10 18:03:04 +0000270def CheckChangeSvnEolStyle(input_api, output_api, source_file_filter=None):
maruel@chromium.orgb7d46902009-06-10 14:12:10 +0000271 """Checks that the source files have svn:eol-style=LF."""
maruel@chromium.org46e832a2009-06-18 19:58:07 +0000272 return CheckSvnProperty(input_api, output_api,
273 'svn:eol-style', 'LF',
274 input_api.AffectedSourceFiles(source_file_filter))
275
276
277def CheckSvnForCommonMimeTypes(input_api, output_api):
278 """Checks that common binary file types have the correct svn:mime-type."""
279 output = []
280 files = input_api.AffectedFiles(include_deletes=False)
maruel@chromium.orge49187c2009-06-26 22:44:53 +0000281 def IsExts(x, exts):
282 path = x.LocalPath()
283 for extension in exts:
284 if path.endswith(extension):
285 return True
286 return False
maruel@chromium.org46e832a2009-06-18 19:58:07 +0000287 def FilterFiles(extension):
maruel@chromium.orge49187c2009-06-26 22:44:53 +0000288 return filter(lambda x: IsExts(x, extension), files)
maruel@chromium.org46e832a2009-06-18 19:58:07 +0000289 def RunCheck(mime_type, files):
290 output.extend(CheckSvnProperty(input_api, output_api, 'svn:mime-type',
291 mime_type, files))
maruel@chromium.orge49187c2009-06-26 22:44:53 +0000292 RunCheck('application/pdf', FilterFiles(['.pdf']))
293 RunCheck('image/bmp', FilterFiles(['.bmp']))
294 RunCheck('image/gif', FilterFiles(['.gif']))
295 RunCheck('image/png', FilterFiles(['.png']))
296 RunCheck('image/jpeg', FilterFiles(['.jpg', '.jpeg', '.jpe']))
297 RunCheck('image/vnd.microsoft.icon', FilterFiles(['.ico']))
maruel@chromium.org46e832a2009-06-18 19:58:07 +0000298 return output
299
300
301def CheckSvnProperty(input_api, output_api, prop, expected, affected_files):
302 """Checks that affected_files files have prop=expected."""
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000303 if input_api.change.scm != 'svn':
304 return []
305
306 bad = filter(lambda f: f.Property(prop) != expected, affected_files)
maruel@chromium.orgb7d46902009-06-10 14:12:10 +0000307 if bad:
maruel@chromium.org0874d472009-06-10 19:08:33 +0000308 if input_api.is_committing:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000309 res_type = output_api.PresubmitError
maruel@chromium.org0874d472009-06-10 19:08:33 +0000310 else:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000311 res_type = output_api.PresubmitNotifyResult
maruel@chromium.org46e832a2009-06-18 19:58:07 +0000312 message = "Run `svn pset %s %s <item>` on these files:" % (prop, expected)
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000313 return [res_type(message, items=bad)]
maruel@chromium.orgb7d46902009-06-10 14:12:10 +0000314 return []
315
316
maruel@chromium.org3410d912009-06-09 20:56:16 +0000317### Other checks
318
319def CheckDoNotSubmit(input_api, output_api):
320 return (
321 CheckDoNotSubmitInDescription(input_api, output_api) +
322 CheckDoNotSubmitInFiles(input_api, output_api)
323 )
324
325
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000326def CheckTreeIsOpen(input_api, output_api, url, closed):
327 """Checks that an url's content doesn't match a regexp that would mean that
328 the tree is closed."""
maruel@chromium.org89491382009-06-06 18:58:39 +0000329 assert(input_api.is_committing)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000330 try:
331 connection = input_api.urllib2.urlopen(url)
332 status = connection.read()
333 connection.close()
334 if input_api.re.match(closed, status):
335 long_text = status + '\n' + url
maruel@chromium.org89491382009-06-06 18:58:39 +0000336 return [output_api.PresubmitPromptWarning("The tree is closed.",
337 long_text=long_text)]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000338 except IOError:
339 pass
340 return []
maruel@chromium.org7b305e82009-05-19 18:24:20 +0000341
342
343def RunPythonUnitTests(input_api, output_api, unit_tests):
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000344 """Run the unit tests out of process, capture the output and use the result
345 code to determine success.
346 """
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000347 # We don't want to hinder users from uploading incomplete patches.
348 if input_api.is_committing:
349 message_type = output_api.PresubmitError
350 else:
351 message_type = output_api.PresubmitNotifyResult
maruel@chromium.org7b305e82009-05-19 18:24:20 +0000352 outputs = []
353 for unit_test in unit_tests:
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000354 # Run the unit tests out of process. This is because some unit tests
355 # stub out base libraries and don't clean up their mess. It's too easy to
356 # get subtle bugs.
357 cwd = None
358 env = None
359 unit_test_name = unit_test
360 # "python -m test.unit_test" doesn't work. We need to change to the right
361 # directory instead.
362 if '.' in unit_test:
363 # Tests imported in submodules (subdirectories) assume that the current
364 # directory is in the PYTHONPATH. Manually fix that.
365 unit_test = unit_test.replace('.', '/')
366 cwd = input_api.os_path.dirname(unit_test)
367 unit_test = input_api.os_path.basename(unit_test)
368 env = input_api.environ.copy()
kbr@google.comab318592009-09-04 00:54:55 +0000369 # At least on Windows, it seems '.' must explicitly be in PYTHONPATH
370 backpath = [
371 '.', input_api.os_path.pathsep.join(['..'] * (cwd.count('/') + 1))
372 ]
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000373 if env.get('PYTHONPATH'):
374 backpath.append(env.get('PYTHONPATH'))
ukai@chromium.orga301f1f2009-08-05 10:37:33 +0000375 env['PYTHONPATH'] = input_api.os_path.pathsep.join((backpath))
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000376 subproc = input_api.subprocess.Popen(
377 [
378 input_api.python_executable,
379 "-m",
380 "%s" % unit_test
381 ],
382 cwd=cwd,
383 env=env,
384 stdin=input_api.subprocess.PIPE,
385 stdout=input_api.subprocess.PIPE,
386 stderr=input_api.subprocess.PIPE)
387 stdoutdata, stderrdata = subproc.communicate()
388 # Discard the output if returncode == 0
389 if subproc.returncode:
390 outputs.append("Test '%s' failed with code %d\n%s\n%s\n" % (
391 unit_test_name, subproc.returncode, stdoutdata, stderrdata))
392 if outputs:
393 return [message_type("%d unit tests failed." % len(outputs),
394 long_text='\n'.join(outputs))]
395 return []