| 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 |