Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(923)

Side by Side Diff: presubmit_support.py

Issue 14247012: Add support for parallel presubmit unit testing. (Closed) Base URL: https://chromium.googlesource.com/chromium/tools/depot_tools.git@master
Patch Set: fix nits Created 7 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « presubmit_canned_checks.py ('k') | tests/presubmit_unittest.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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
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
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
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
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
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))
OLDNEW
« no previous file with comments | « presubmit_canned_checks.py ('k') | tests/presubmit_unittest.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698