Chromium Code Reviews| 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 |