OLD | NEW |
---|---|
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
5 | 5 |
6 """Enables directory-specific presubmit checks to run at upload and/or commit. | 6 """Enables directory-specific presubmit checks to run at upload and/or commit. |
7 """ | 7 """ |
8 | 8 |
9 __version__ = '1.6.2' | 9 __version__ = '1.6.2' |
10 | 10 |
11 # TODO(joi) Add caching where appropriate/needed. The API is designed to allow | 11 # TODO(joi) Add caching where appropriate/needed. The API is designed to allow |
12 # caching (between all different invocations of presubmit scripts for a given | 12 # caching (between all different invocations of presubmit scripts for a given |
13 # change). We should add it as our presubmit scripts start feeling slow. | 13 # change). We should add it as our presubmit scripts start feeling slow. |
14 | 14 |
15 import cpplint | 15 import cpplint |
16 import cPickle # Exposed through the API. | 16 import cPickle # Exposed through the API. |
17 import cStringIO # Exposed through the API. | 17 import cStringIO # Exposed through the API. |
18 import collections | |
18 import contextlib | 19 import contextlib |
19 import fnmatch | 20 import fnmatch |
20 import glob | 21 import glob |
21 import inspect | 22 import inspect |
22 import json # Exposed through the API. | 23 import json # Exposed through the API. |
23 import logging | 24 import logging |
24 import marshal # Exposed through the API. | 25 import marshal # Exposed through the API. |
26 import multiprocessing | |
25 import optparse | 27 import optparse |
26 import os # Somewhat exposed through the API. | 28 import os # Somewhat exposed through the API. |
27 import pickle # Exposed through the API. | 29 import pickle # Exposed through the API. |
28 import random | 30 import random |
29 import re # Exposed through the API. | 31 import re # Exposed through the API. |
30 import sys # Parts exposed through API. | 32 import sys # Parts exposed through API. |
31 import tempfile # Exposed through the API. | 33 import tempfile # Exposed through the API. |
32 import time | 34 import time |
33 import traceback # Exposed through the API. | 35 import traceback # Exposed through the API. |
34 import types | 36 import types |
(...skipping 12 matching lines...) Expand all Loading... | |
47 | 49 |
48 | 50 |
49 # Ask for feedback only once in program lifetime. | 51 # Ask for feedback only once in program lifetime. |
50 _ASKED_FOR_FEEDBACK = False | 52 _ASKED_FOR_FEEDBACK = False |
51 | 53 |
52 | 54 |
53 class PresubmitFailure(Exception): | 55 class PresubmitFailure(Exception): |
54 pass | 56 pass |
55 | 57 |
56 | 58 |
59 CommandData = collections.namedtuple('CommandData', | |
60 ['name', 'cmd', 'kwargs', 'message']) | |
61 | |
57 def normpath(path): | 62 def normpath(path): |
58 '''Version of os.path.normpath that also changes backward slashes to | 63 '''Version of os.path.normpath that also changes backward slashes to |
59 forward slashes when not running on Windows. | 64 forward slashes when not running on Windows. |
60 ''' | 65 ''' |
61 # This is safe to always do because the Windows version of os.path.normpath | 66 # This is safe to always do because the Windows version of os.path.normpath |
62 # will replace forward slashes with backward slashes. | 67 # will replace forward slashes with backward slashes. |
63 path = path.replace(os.sep, '/') | 68 path = path.replace(os.sep, '/') |
64 return os.path.normpath(path) | 69 return os.path.normpath(path) |
65 | 70 |
66 | 71 |
(...skipping 30 matching lines...) Expand all Loading... | |
97 | 102 |
98 def write(self, s): | 103 def write(self, s): |
99 self.written_output.append(s) | 104 self.written_output.append(s) |
100 if self.output_stream: | 105 if self.output_stream: |
101 self.output_stream.write(s) | 106 self.output_stream.write(s) |
102 | 107 |
103 def getvalue(self): | 108 def getvalue(self): |
104 return ''.join(self.written_output) | 109 return ''.join(self.written_output) |
105 | 110 |
106 | 111 |
112 class _PresubmitResult(object): | |
113 """Base class for result objects.""" | |
114 fatal = False | |
115 should_prompt = False | |
116 | |
117 def __init__(self, message, items=None, long_text=''): | |
118 """ | |
119 message: A short one-line message to indicate errors. | |
120 items: A list of short strings to indicate where errors occurred. | |
121 long_text: multi-line text output, e.g. from another tool | |
122 """ | |
123 self._message = message | |
124 self._items = [] | |
125 if items: | |
126 self._items = items | |
127 self._long_text = long_text.rstrip() | |
128 | |
129 def handle(self, output): | |
130 output.write(self._message) | |
131 output.write('\n') | |
132 for index, item in enumerate(self._items): | |
133 output.write(' ') | |
134 # Write separately in case it's unicode. | |
135 output.write(str(item)) | |
136 if index < len(self._items) - 1: | |
137 output.write(' \\') | |
138 output.write('\n') | |
139 if self._long_text: | |
140 output.write('\n***************\n') | |
141 # Write separately in case it's unicode. | |
142 output.write(self._long_text) | |
143 output.write('\n***************\n') | |
144 if self.fatal: | |
145 output.fail() | |
146 | |
M-A Ruel
2013/04/19 16:49:39
two lines between file level symbols
Isaac (away)
2013/04/20 00:41:36
Done.
| |
147 class _PresubmitAddReviewers(_PresubmitResult): | |
148 """Add some suggested reviewers to the change.""" | |
149 def __init__(self, reviewers): | |
150 super(OutputApi.PresubmitAddReviewers, self).__init__('') | |
M-A Ruel
2013/04/19 16:49:39
Stale reference to OutputApi (and below)
Isaac (away)
2013/04/20 00:41:36
Done.
| |
151 self.reviewers = reviewers | |
152 | |
153 def handle(self, output): | |
154 output.reviewers.extend(self.reviewers) | |
155 | |
156 class _PresubmitError(_PresubmitResult): | |
157 """A hard presubmit error.""" | |
158 fatal = True | |
159 | |
160 class _PresubmitPromptWarning(_PresubmitResult): | |
161 """An warning that prompts the user if they want to continue.""" | |
162 should_prompt = True | |
163 | |
164 class _PresubmitNotifyResult(_PresubmitResult): | |
165 """Just print something to the screen -- but it's not even a warning.""" | |
166 pass | |
167 | |
168 class _MailTextResult(_PresubmitResult): | |
169 """A warning that should be included in the review request email.""" | |
170 def __init__(self, *args, **kwargs): | |
171 super(OutputApi.MailTextResult, self).__init__() | |
172 raise NotImplementedError() | |
173 | |
107 class OutputApi(object): | 174 class OutputApi(object): |
108 """An instance of OutputApi gets passed to presubmit scripts so that they | 175 """An instance of OutputApi gets passed to presubmit scripts so that they |
109 can output various types of results. | 176 can output various types of results. |
110 """ | 177 """ |
178 # multiprocessing requires these be top level modules | |
179 PresubmitResult = _PresubmitResult | |
180 PresubmitAddReviewers = _PresubmitAddReviewers | |
181 PresubmitError = _PresubmitError | |
182 PresubmitPromptWarning = _PresubmitPromptWarning | |
183 PresubmitNotifyResult = _PresubmitNotifyResult | |
184 MailTextResult = _MailTextResult | |
185 | |
111 def __init__(self, is_committing): | 186 def __init__(self, is_committing): |
112 self.is_committing = is_committing | 187 self.is_committing = is_committing |
113 | 188 |
114 class PresubmitResult(object): | |
115 """Base class for result objects.""" | |
116 fatal = False | |
117 should_prompt = False | |
118 | |
119 def __init__(self, message, items=None, long_text=''): | |
120 """ | |
121 message: A short one-line message to indicate errors. | |
122 items: A list of short strings to indicate where errors occurred. | |
123 long_text: multi-line text output, e.g. from another tool | |
124 """ | |
125 self._message = message | |
126 self._items = [] | |
127 if items: | |
128 self._items = items | |
129 self._long_text = long_text.rstrip() | |
130 | |
131 def handle(self, output): | |
132 output.write(self._message) | |
133 output.write('\n') | |
134 for index, item in enumerate(self._items): | |
135 output.write(' ') | |
136 # Write separately in case it's unicode. | |
137 output.write(str(item)) | |
138 if index < len(self._items) - 1: | |
139 output.write(' \\') | |
140 output.write('\n') | |
141 if self._long_text: | |
142 output.write('\n***************\n') | |
143 # Write separately in case it's unicode. | |
144 output.write(self._long_text) | |
145 output.write('\n***************\n') | |
146 if self.fatal: | |
147 output.fail() | |
148 | |
149 class PresubmitAddReviewers(PresubmitResult): | |
150 """Add some suggested reviewers to the change.""" | |
151 def __init__(self, reviewers): | |
152 super(OutputApi.PresubmitAddReviewers, self).__init__('') | |
153 self.reviewers = reviewers | |
154 | |
155 def handle(self, output): | |
156 output.reviewers.extend(self.reviewers) | |
157 | |
158 class PresubmitError(PresubmitResult): | |
159 """A hard presubmit error.""" | |
160 fatal = True | |
161 | |
162 class PresubmitPromptWarning(PresubmitResult): | |
163 """An warning that prompts the user if they want to continue.""" | |
164 should_prompt = True | |
165 | |
166 class PresubmitNotifyResult(PresubmitResult): | |
167 """Just print something to the screen -- but it's not even a warning.""" | |
168 pass | |
169 | |
170 def PresubmitPromptOrNotify(self, *args, **kwargs): | 189 def PresubmitPromptOrNotify(self, *args, **kwargs): |
171 """Warn the user when uploading, but only notify if committing.""" | 190 """Warn the user when uploading, but only notify if committing.""" |
172 if self.is_committing: | 191 if self.is_committing: |
173 return self.PresubmitNotifyResult(*args, **kwargs) | 192 return self.PresubmitNotifyResult(*args, **kwargs) |
174 return self.PresubmitPromptWarning(*args, **kwargs) | 193 return self.PresubmitPromptWarning(*args, **kwargs) |
175 | 194 |
176 class MailTextResult(PresubmitResult): | |
177 """A warning that should be included in the review request email.""" | |
178 def __init__(self, *args, **kwargs): | |
179 super(OutputApi.MailTextResult, self).__init__() | |
180 raise NotImplementedError() | |
181 | |
182 | 195 |
183 class InputApi(object): | 196 class InputApi(object): |
184 """An instance of this object is passed to presubmit scripts so they can | 197 """An instance of this object is passed to presubmit scripts so they can |
185 know stuff about the change they're looking at. | 198 know stuff about the change they're looking at. |
186 """ | 199 """ |
187 # Method could be a function | 200 # Method could be a function |
188 # pylint: disable=R0201 | 201 # pylint: disable=R0201 |
189 | 202 |
190 # File extensions that are considered source files from a style guide | 203 # File extensions that are considered source files from a style guide |
191 # perspective. Don't modify this list from a presubmit script! | 204 # perspective. Don't modify this list from a presubmit script! |
(...skipping 85 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
277 self._current_presubmit_path = os.path.dirname(presubmit_path) | 290 self._current_presubmit_path = os.path.dirname(presubmit_path) |
278 | 291 |
279 # We carry the canned checks so presubmit scripts can easily use them. | 292 # We carry the canned checks so presubmit scripts can easily use them. |
280 self.canned_checks = presubmit_canned_checks | 293 self.canned_checks = presubmit_canned_checks |
281 | 294 |
282 # TODO(dpranke): figure out a list of all approved owners for a repo | 295 # TODO(dpranke): figure out a list of all approved owners for a repo |
283 # in order to be able to handle wildcard OWNERS files? | 296 # in order to be able to handle wildcard OWNERS files? |
284 self.owners_db = owners.Database(change.RepositoryRoot(), | 297 self.owners_db = owners.Database(change.RepositoryRoot(), |
285 fopen=file, os_path=self.os_path, glob=self.glob) | 298 fopen=file, os_path=self.os_path, glob=self.glob) |
286 self.verbose = verbose | 299 self.verbose = verbose |
300 self.Command = CommandData | |
287 | 301 |
288 # Replace <hash_map> and <hash_set> as headers that need to be included | 302 # Replace <hash_map> and <hash_set> as headers that need to be included |
289 # with "base/hash_tables.h" instead. | 303 # with "base/hash_tables.h" instead. |
290 # Access to a protected member _XX of a client class | 304 # Access to a protected member _XX of a client class |
291 # pylint: disable=W0212 | 305 # pylint: disable=W0212 |
292 self.cpplint._re_pattern_templates = [ | 306 self.cpplint._re_pattern_templates = [ |
293 (a, b, 'base/hash_tables.h') | 307 (a, b, 'base/hash_tables.h') |
294 if header in ('<hash_map>', '<hash_set>') else (a, b, header) | 308 if header in ('<hash_map>', '<hash_set>') else (a, b, header) |
295 for (a, b, header) in cpplint._re_pattern_templates | 309 for (a, b, header) in cpplint._re_pattern_templates |
296 ] | 310 ] |
(...skipping 133 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
430 file_item = file_item.AbsoluteLocalPath() | 444 file_item = file_item.AbsoluteLocalPath() |
431 if not file_item.startswith(self.change.RepositoryRoot()): | 445 if not file_item.startswith(self.change.RepositoryRoot()): |
432 raise IOError('Access outside the repository root is denied.') | 446 raise IOError('Access outside the repository root is denied.') |
433 return gclient_utils.FileRead(file_item, mode) | 447 return gclient_utils.FileRead(file_item, mode) |
434 | 448 |
435 @property | 449 @property |
436 def tbr(self): | 450 def tbr(self): |
437 """Returns if a change is TBR'ed.""" | 451 """Returns if a change is TBR'ed.""" |
438 return 'TBR' in self.change.tags | 452 return 'TBR' in self.change.tags |
439 | 453 |
454 def RunTests(self, tests_mix, parallel=True): | |
M-A Ruel
2013/04/19 16:49:39
Will parallel=False ever make sense?
Isaac (away)
2013/04/19 19:52:04
Yes, it is used by the legacy commands in presubmi
| |
455 tests = [] | |
456 msgs = [] | |
457 for t in tests_mix: | |
458 if isinstance(t, OutputApi.PresubmitResult): | |
459 msgs.append(t) | |
460 else: | |
461 t = t._replace(kwargs=t.kwargs.copy()) | |
462 t.kwargs.setdefault('env', self.environ) | |
463 t.kwargs.setdefault('cwd', self.PresubmitLocalPath()) | |
464 tests.append(t) | |
465 if parallel: | |
466 pool = multiprocessing.Pool() | |
467 msgs.extend(pool.map_async(CallCommand, tests).get(99999)) | |
M-A Ruel
2013/04/19 16:49:39
Can you note why this is used? (e.g. the Ctrl-C ha
Isaac (away)
2013/04/20 00:41:36
Done.
| |
468 pool.close() | |
469 pool.join() | |
470 else: | |
471 msgs.extend(map(CallCommand, tests)) | |
472 return [m for m in msgs if m is not None] | |
M-A Ruel
2013/04/19 16:49:39
return [m for m in msgs if m]
or
return filter(Non
Isaac (away)
2013/04/19 19:52:04
The intention is that each item in msg list is eit
| |
473 | |
440 | 474 |
441 class AffectedFile(object): | 475 class AffectedFile(object): |
442 """Representation of a file in a change.""" | 476 """Representation of a file in a change.""" |
443 # Method could be a function | 477 # Method could be a function |
444 # pylint: disable=R0201 | 478 # pylint: disable=R0201 |
445 def __init__(self, path, action, repository_root): | 479 def __init__(self, path, action, repository_root): |
446 self._path = path | 480 self._path = path |
447 self._action = action | 481 self._action = action |
448 self._local_root = repository_root | 482 self._local_root = repository_root |
449 self._is_directory = None | 483 self._is_directory = None |
(...skipping 781 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
1231 for method_name in method_names: | 1265 for method_name in method_names: |
1232 if not hasattr(presubmit_canned_checks, method_name): | 1266 if not hasattr(presubmit_canned_checks, method_name): |
1233 raise NonexistantCannedCheckFilter(method_name) | 1267 raise NonexistantCannedCheckFilter(method_name) |
1234 filtered[method_name] = getattr(presubmit_canned_checks, method_name) | 1268 filtered[method_name] = getattr(presubmit_canned_checks, method_name) |
1235 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: []) | 1269 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: []) |
1236 yield | 1270 yield |
1237 finally: | 1271 finally: |
1238 for name, method in filtered.iteritems(): | 1272 for name, method in filtered.iteritems(): |
1239 setattr(presubmit_canned_checks, name, method) | 1273 setattr(presubmit_canned_checks, name, method) |
1240 | 1274 |
1275 # multiprocessing requires a top level fun with a single argument | |
M-A Ruel
2013/04/19 16:49:39
fun -> function
I'd prefer the code to be inside t
Isaac (away)
2013/04/20 00:41:36
Done.
| |
1276 def CallCommand(cmd_data): | |
1277 cmd_data.kwargs['stdout'] = subprocess.PIPE | |
1278 cmd_data.kwargs['stderr'] = subprocess.STDOUT | |
1279 try: | |
1280 (out, _), code = subprocess.communicate(cmd_data.cmd, **cmd_data.kwargs) | |
1281 if code != 0: | |
1282 #import pdb; pdb.set_trace() | |
M-A Ruel
2013/04/19 16:49:39
Remove before committing
Isaac (away)
2013/04/20 00:41:36
Done.
| |
1283 return cmd_data.message('%s failed\n%s' % (cmd_data.name, out)) | |
1284 except OSError as e: | |
1285 return cmd_data.message( | |
1286 '%s exec failure\n %s\n%s' % (cmd_data.name, e, out)) | |
1287 | |
1241 | 1288 |
1242 def Main(argv): | 1289 def Main(argv): |
1243 parser = optparse.OptionParser(usage="%prog [options] <files...>", | 1290 parser = optparse.OptionParser(usage="%prog [options] <files...>", |
1244 version="%prog " + str(__version__)) | 1291 version="%prog " + str(__version__)) |
1245 parser.add_option("-c", "--commit", action="store_true", default=False, | 1292 parser.add_option("-c", "--commit", action="store_true", default=False, |
1246 help="Use commit instead of upload checks") | 1293 help="Use commit instead of upload checks") |
1247 parser.add_option("-u", "--upload", action="store_false", dest='commit', | 1294 parser.add_option("-u", "--upload", action="store_false", dest='commit', |
1248 help="Use upload instead of commit checks") | 1295 help="Use upload instead of commit checks") |
1249 parser.add_option("-r", "--recursive", action="store_true", | 1296 parser.add_option("-r", "--recursive", action="store_true", |
1250 help="Act recursively") | 1297 help="Act recursively") |
(...skipping 60 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
1311 except PresubmitFailure, e: | 1358 except PresubmitFailure, e: |
1312 print >> sys.stderr, e | 1359 print >> sys.stderr, e |
1313 print >> sys.stderr, 'Maybe your depot_tools is out of date?' | 1360 print >> sys.stderr, 'Maybe your depot_tools is out of date?' |
1314 print >> sys.stderr, 'If all fails, contact maruel@' | 1361 print >> sys.stderr, 'If all fails, contact maruel@' |
1315 return 2 | 1362 return 2 |
1316 | 1363 |
1317 | 1364 |
1318 if __name__ == '__main__': | 1365 if __name__ == '__main__': |
1319 fix_encoding.fix_encoding() | 1366 fix_encoding.fix_encoding() |
1320 sys.exit(Main(None)) | 1367 sys.exit(Main(None)) |
OLD | NEW |