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 # Top level object so multiprocessing can pickle |
| 113 # Public access through OutputApi object. |
| 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 = items or [] |
| 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 |
| 150 # Top level object so multiprocessing can pickle |
| 151 # Public access through OutputApi object. |
| 152 class _PresubmitAddReviewers(_PresubmitResult): |
| 153 """Add some suggested reviewers to the change.""" |
| 154 def __init__(self, reviewers): |
| 155 super(_PresubmitAddReviewers, self).__init__('') |
| 156 self.reviewers = reviewers |
| 157 |
| 158 def handle(self, output): |
| 159 output.reviewers.extend(self.reviewers) |
| 160 |
| 161 |
| 162 # Top level object so multiprocessing can pickle |
| 163 # Public access through OutputApi object. |
| 164 class _PresubmitError(_PresubmitResult): |
| 165 """A hard presubmit error.""" |
| 166 fatal = True |
| 167 |
| 168 |
| 169 # Top level object so multiprocessing can pickle |
| 170 # Public access through OutputApi object. |
| 171 class _PresubmitPromptWarning(_PresubmitResult): |
| 172 """An warning that prompts the user if they want to continue.""" |
| 173 should_prompt = True |
| 174 |
| 175 |
| 176 # Top level object so multiprocessing can pickle |
| 177 # Public access through OutputApi object. |
| 178 class _PresubmitNotifyResult(_PresubmitResult): |
| 179 """Just print something to the screen -- but it's not even a warning.""" |
| 180 pass |
| 181 |
| 182 |
| 183 # Top level object so multiprocessing can pickle |
| 184 # Public access through OutputApi object. |
| 185 class _MailTextResult(_PresubmitResult): |
| 186 """A warning that should be included in the review request email.""" |
| 187 def __init__(self, *args, **kwargs): |
| 188 super(_MailTextResult, self).__init__() |
| 189 raise NotImplementedError() |
| 190 |
| 191 |
107 class OutputApi(object): | 192 class OutputApi(object): |
108 """An instance of OutputApi gets passed to presubmit scripts so that they | 193 """An instance of OutputApi gets passed to presubmit scripts so that they |
109 can output various types of results. | 194 can output various types of results. |
110 """ | 195 """ |
| 196 PresubmitResult = _PresubmitResult |
| 197 PresubmitAddReviewers = _PresubmitAddReviewers |
| 198 PresubmitError = _PresubmitError |
| 199 PresubmitPromptWarning = _PresubmitPromptWarning |
| 200 PresubmitNotifyResult = _PresubmitNotifyResult |
| 201 MailTextResult = _MailTextResult |
| 202 |
111 def __init__(self, is_committing): | 203 def __init__(self, is_committing): |
112 self.is_committing = is_committing | 204 self.is_committing = is_committing |
113 | 205 |
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): | 206 def PresubmitPromptOrNotify(self, *args, **kwargs): |
171 """Warn the user when uploading, but only notify if committing.""" | 207 """Warn the user when uploading, but only notify if committing.""" |
172 if self.is_committing: | 208 if self.is_committing: |
173 return self.PresubmitNotifyResult(*args, **kwargs) | 209 return self.PresubmitNotifyResult(*args, **kwargs) |
174 return self.PresubmitPromptWarning(*args, **kwargs) | 210 return self.PresubmitPromptWarning(*args, **kwargs) |
175 | 211 |
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 | 212 |
183 class InputApi(object): | 213 class InputApi(object): |
184 """An instance of this object is passed to presubmit scripts so they can | 214 """An instance of this object is passed to presubmit scripts so they can |
185 know stuff about the change they're looking at. | 215 know stuff about the change they're looking at. |
186 """ | 216 """ |
187 # Method could be a function | 217 # Method could be a function |
188 # pylint: disable=R0201 | 218 # pylint: disable=R0201 |
189 | 219 |
190 # File extensions that are considered source files from a style guide | 220 # File extensions that are considered source files from a style guide |
191 # perspective. Don't modify this list from a presubmit script! | 221 # 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) | 307 self._current_presubmit_path = os.path.dirname(presubmit_path) |
278 | 308 |
279 # We carry the canned checks so presubmit scripts can easily use them. | 309 # We carry the canned checks so presubmit scripts can easily use them. |
280 self.canned_checks = presubmit_canned_checks | 310 self.canned_checks = presubmit_canned_checks |
281 | 311 |
282 # TODO(dpranke): figure out a list of all approved owners for a repo | 312 # TODO(dpranke): figure out a list of all approved owners for a repo |
283 # in order to be able to handle wildcard OWNERS files? | 313 # in order to be able to handle wildcard OWNERS files? |
284 self.owners_db = owners.Database(change.RepositoryRoot(), | 314 self.owners_db = owners.Database(change.RepositoryRoot(), |
285 fopen=file, os_path=self.os_path, glob=self.glob) | 315 fopen=file, os_path=self.os_path, glob=self.glob) |
286 self.verbose = verbose | 316 self.verbose = verbose |
| 317 self.Command = CommandData |
287 | 318 |
288 # Replace <hash_map> and <hash_set> as headers that need to be included | 319 # Replace <hash_map> and <hash_set> as headers that need to be included |
289 # with "base/hash_tables.h" instead. | 320 # with "base/hash_tables.h" instead. |
290 # Access to a protected member _XX of a client class | 321 # Access to a protected member _XX of a client class |
291 # pylint: disable=W0212 | 322 # pylint: disable=W0212 |
292 self.cpplint._re_pattern_templates = [ | 323 self.cpplint._re_pattern_templates = [ |
293 (a, b, 'base/hash_tables.h') | 324 (a, b, 'base/hash_tables.h') |
294 if header in ('<hash_map>', '<hash_set>') else (a, b, header) | 325 if header in ('<hash_map>', '<hash_set>') else (a, b, header) |
295 for (a, b, header) in cpplint._re_pattern_templates | 326 for (a, b, header) in cpplint._re_pattern_templates |
296 ] | 327 ] |
(...skipping 133 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
430 file_item = file_item.AbsoluteLocalPath() | 461 file_item = file_item.AbsoluteLocalPath() |
431 if not file_item.startswith(self.change.RepositoryRoot()): | 462 if not file_item.startswith(self.change.RepositoryRoot()): |
432 raise IOError('Access outside the repository root is denied.') | 463 raise IOError('Access outside the repository root is denied.') |
433 return gclient_utils.FileRead(file_item, mode) | 464 return gclient_utils.FileRead(file_item, mode) |
434 | 465 |
435 @property | 466 @property |
436 def tbr(self): | 467 def tbr(self): |
437 """Returns if a change is TBR'ed.""" | 468 """Returns if a change is TBR'ed.""" |
438 return 'TBR' in self.change.tags | 469 return 'TBR' in self.change.tags |
439 | 470 |
| 471 @staticmethod |
| 472 def RunTests(tests_mix, parallel=True): |
| 473 tests = [] |
| 474 msgs = [] |
| 475 for t in tests_mix: |
| 476 if isinstance(t, OutputApi.PresubmitResult): |
| 477 msgs.append(t) |
| 478 else: |
| 479 assert issubclass(t.message, _PresubmitResult) |
| 480 tests.append(t) |
| 481 if parallel: |
| 482 pool = multiprocessing.Pool() |
| 483 # async recipe works around multiprocessing bug handling Ctrl-C |
| 484 msgs.extend(pool.map_async(CallCommand, tests).get(99999)) |
| 485 pool.close() |
| 486 pool.join() |
| 487 else: |
| 488 msgs.extend(map(CallCommand, tests)) |
| 489 return [m for m in msgs if m] |
| 490 |
440 | 491 |
441 class AffectedFile(object): | 492 class AffectedFile(object): |
442 """Representation of a file in a change.""" | 493 """Representation of a file in a change.""" |
443 # Method could be a function | 494 # Method could be a function |
444 # pylint: disable=R0201 | 495 # pylint: disable=R0201 |
445 def __init__(self, path, action, repository_root): | 496 def __init__(self, path, action, repository_root): |
446 self._path = path | 497 self._path = path |
447 self._action = action | 498 self._action = action |
448 self._local_root = repository_root | 499 self._local_root = repository_root |
449 self._is_directory = None | 500 self._is_directory = None |
(...skipping 781 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1231 for method_name in method_names: | 1282 for method_name in method_names: |
1232 if not hasattr(presubmit_canned_checks, method_name): | 1283 if not hasattr(presubmit_canned_checks, method_name): |
1233 raise NonexistantCannedCheckFilter(method_name) | 1284 raise NonexistantCannedCheckFilter(method_name) |
1234 filtered[method_name] = getattr(presubmit_canned_checks, method_name) | 1285 filtered[method_name] = getattr(presubmit_canned_checks, method_name) |
1235 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: []) | 1286 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: []) |
1236 yield | 1287 yield |
1237 finally: | 1288 finally: |
1238 for name, method in filtered.iteritems(): | 1289 for name, method in filtered.iteritems(): |
1239 setattr(presubmit_canned_checks, name, method) | 1290 setattr(presubmit_canned_checks, name, method) |
1240 | 1291 |
| 1292 def CallCommand(cmd_data): |
| 1293 # multiprocessing needs a top level function with a single argument. |
| 1294 cmd_data.kwargs['stdout'] = subprocess.PIPE |
| 1295 cmd_data.kwargs['stderr'] = subprocess.STDOUT |
| 1296 try: |
| 1297 (out, _), code = subprocess.communicate(cmd_data.cmd, **cmd_data.kwargs) |
| 1298 if code != 0: |
| 1299 return cmd_data.message('%s failed\n%s' % (cmd_data.name, out)) |
| 1300 except OSError as e: |
| 1301 return cmd_data.message( |
| 1302 '%s exec failure\n %s\n%s' % (cmd_data.name, e, out)) |
| 1303 |
1241 | 1304 |
1242 def Main(argv): | 1305 def Main(argv): |
1243 parser = optparse.OptionParser(usage="%prog [options] <files...>", | 1306 parser = optparse.OptionParser(usage="%prog [options] <files...>", |
1244 version="%prog " + str(__version__)) | 1307 version="%prog " + str(__version__)) |
1245 parser.add_option("-c", "--commit", action="store_true", default=False, | 1308 parser.add_option("-c", "--commit", action="store_true", default=False, |
1246 help="Use commit instead of upload checks") | 1309 help="Use commit instead of upload checks") |
1247 parser.add_option("-u", "--upload", action="store_false", dest='commit', | 1310 parser.add_option("-u", "--upload", action="store_false", dest='commit', |
1248 help="Use upload instead of commit checks") | 1311 help="Use upload instead of commit checks") |
1249 parser.add_option("-r", "--recursive", action="store_true", | 1312 parser.add_option("-r", "--recursive", action="store_true", |
1250 help="Act recursively") | 1313 help="Act recursively") |
(...skipping 60 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1311 except PresubmitFailure, e: | 1374 except PresubmitFailure, e: |
1312 print >> sys.stderr, e | 1375 print >> sys.stderr, e |
1313 print >> sys.stderr, 'Maybe your depot_tools is out of date?' | 1376 print >> sys.stderr, 'Maybe your depot_tools is out of date?' |
1314 print >> sys.stderr, 'If all fails, contact maruel@' | 1377 print >> sys.stderr, 'If all fails, contact maruel@' |
1315 return 2 | 1378 return 2 |
1316 | 1379 |
1317 | 1380 |
1318 if __name__ == '__main__': | 1381 if __name__ == '__main__': |
1319 fix_encoding.fix_encoding() | 1382 fix_encoding.fix_encoding() |
1320 sys.exit(Main(None)) | 1383 sys.exit(Main(None)) |
OLD | NEW |