Add TabModel#closeAllTabs(boolean allowDelegation, boolean uponExit) and TabModelSele...
[chromium-blink-merge.git] / tools / auto_bisect / bisect_utils.py
blobfb87a106830bf3f94870c9f873f1b3ed882c042f
1 # Copyright 2014 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
5 """Utility functions used by the bisect tool.
7 This includes functions related to checking out the depot and outputting
8 annotations for the Buildbot waterfall.
9 """
11 import errno
12 import imp
13 import os
14 import stat
15 import subprocess
16 import sys
18 DEFAULT_GCLIENT_CUSTOM_DEPS = {
19 'src/data/page_cycler': 'https://chrome-internal.googlesource.com/'
20 'chrome/data/page_cycler/.git',
21 'src/data/dom_perf': 'https://chrome-internal.googlesource.com/'
22 'chrome/data/dom_perf/.git',
23 'src/data/mach_ports': 'https://chrome-internal.googlesource.com/'
24 'chrome/data/mach_ports/.git',
25 'src/tools/perf/data': 'https://chrome-internal.googlesource.com/'
26 'chrome/tools/perf/data/.git',
27 'src/third_party/adobe/flash/binaries/ppapi/linux':
28 'https://chrome-internal.googlesource.com/'
29 'chrome/deps/adobe/flash/binaries/ppapi/linux/.git',
30 'src/third_party/adobe/flash/binaries/ppapi/linux_x64':
31 'https://chrome-internal.googlesource.com/'
32 'chrome/deps/adobe/flash/binaries/ppapi/linux_x64/.git',
33 'src/third_party/adobe/flash/binaries/ppapi/mac':
34 'https://chrome-internal.googlesource.com/'
35 'chrome/deps/adobe/flash/binaries/ppapi/mac/.git',
36 'src/third_party/adobe/flash/binaries/ppapi/mac_64':
37 'https://chrome-internal.googlesource.com/'
38 'chrome/deps/adobe/flash/binaries/ppapi/mac_64/.git',
39 'src/third_party/adobe/flash/binaries/ppapi/win':
40 'https://chrome-internal.googlesource.com/'
41 'chrome/deps/adobe/flash/binaries/ppapi/win/.git',
42 'src/third_party/adobe/flash/binaries/ppapi/win_x64':
43 'https://chrome-internal.googlesource.com/'
44 'chrome/deps/adobe/flash/binaries/ppapi/win_x64/.git',
45 'src/chrome/tools/test/reference_build/chrome_win': None,
46 'src/chrome/tools/test/reference_build/chrome_mac': None,
47 'src/chrome/tools/test/reference_build/chrome_linux': None,
48 'src/third_party/WebKit/LayoutTests': None,
49 'src/tools/valgrind': None,
52 GCLIENT_SPEC_DATA = [
54 'name': 'src',
55 'url': 'https://chromium.googlesource.com/chromium/src.git',
56 'deps_file': '.DEPS.git',
57 'managed': True,
58 'custom_deps': {},
59 'safesync_url': '',
62 GCLIENT_SPEC_ANDROID = "\ntarget_os = ['android']"
63 GCLIENT_CUSTOM_DEPS_V8 = {
64 'src/v8_bleeding_edge': 'https://chromium.googlesource.com/v8/v8.git'
66 FILE_DEPS_GIT = '.DEPS.git'
67 FILE_DEPS = 'DEPS'
69 # Bisect working directory.
70 BISECT_DIR = 'bisect'
72 # The percentage at which confidence is considered high.
73 HIGH_CONFIDENCE = 95
75 # Below is the map of "depot" names to information about each depot. Each depot
76 # is a repository, and in the process of bisecting, revision ranges in these
77 # repositories may also be bisected.
79 # Each depot information dictionary may contain:
80 # src: Path to the working directory.
81 # recurse: True if this repository will get bisected.
82 # depends: A list of other repositories that are actually part of the same
83 # repository in svn. If the repository has any dependent repositories
84 # (e.g. skia/src needs skia/include and skia/gyp to be updated), then
85 # they are specified here.
86 # svn: URL of SVN repository. Needed for git workflow to resolve hashes to
87 # SVN revisions.
88 # from: Parent depot that must be bisected before this is bisected.
89 # deps_var: Key name in vars variable in DEPS file that has revision
90 # information.
91 DEPOT_DEPS_NAME = {
92 'chromium': {
93 'src': 'src',
94 'recurse': True,
95 'depends': None,
96 'from': ['android-chrome'],
97 'viewvc':
98 'http://src.chromium.org/viewvc/chrome?view=revision&revision=',
99 'deps_var': 'chromium_rev'
101 'webkit': {
102 'src': 'src/third_party/WebKit',
103 'recurse': True,
104 'depends': None,
105 'from': ['chromium'],
106 'viewvc':
107 'http://src.chromium.org/viewvc/blink?view=revision&revision=',
108 'deps_var': 'webkit_revision'
110 'angle': {
111 'src': 'src/third_party/angle',
112 'src_old': 'src/third_party/angle_dx11',
113 'recurse': True,
114 'depends': None,
115 'from': ['chromium'],
116 'platform': 'nt',
117 'deps_var': 'angle_revision'
119 'v8': {
120 'src': 'src/v8',
121 'recurse': True,
122 'depends': None,
123 'from': ['chromium'],
124 'custom_deps': GCLIENT_CUSTOM_DEPS_V8,
125 'viewvc': 'https://code.google.com/p/v8/source/detail?r=',
126 'deps_var': 'v8_revision'
128 'v8_bleeding_edge': {
129 'src': 'src/v8_bleeding_edge',
130 'recurse': True,
131 'depends': None,
132 'svn': 'https://v8.googlecode.com/svn/branches/bleeding_edge',
133 'from': ['v8'],
134 'viewvc': 'https://code.google.com/p/v8/source/detail?r=',
135 'deps_var': 'v8_revision'
137 'skia/src': {
138 'src': 'src/third_party/skia/src',
139 'recurse': True,
140 'svn': 'http://skia.googlecode.com/svn/trunk/src',
141 'depends': ['skia/include', 'skia/gyp'],
142 'from': ['chromium'],
143 'viewvc': 'https://code.google.com/p/skia/source/detail?r=',
144 'deps_var': 'skia_revision'
146 'skia/include': {
147 'src': 'src/third_party/skia/include',
148 'recurse': False,
149 'svn': 'http://skia.googlecode.com/svn/trunk/include',
150 'depends': None,
151 'from': ['chromium'],
152 'viewvc': 'https://code.google.com/p/skia/source/detail?r=',
153 'deps_var': 'None'
155 'skia/gyp': {
156 'src': 'src/third_party/skia/gyp',
157 'recurse': False,
158 'svn': 'http://skia.googlecode.com/svn/trunk/gyp',
159 'depends': None,
160 'from': ['chromium'],
161 'viewvc': 'https://code.google.com/p/skia/source/detail?r=',
162 'deps_var': 'None'
166 DEPOT_NAMES = DEPOT_DEPS_NAME.keys()
168 # The possible values of the --bisect_mode flag, which determines what to
169 # use when classifying a revision as "good" or "bad".
170 BISECT_MODE_MEAN = 'mean'
171 BISECT_MODE_STD_DEV = 'std_dev'
172 BISECT_MODE_RETURN_CODE = 'return_code'
175 def AddAdditionalDepotInfo(depot_info):
176 """Adds additional depot info to the global depot variables."""
177 global DEPOT_DEPS_NAME
178 global DEPOT_NAMES
179 DEPOT_DEPS_NAME = dict(DEPOT_DEPS_NAME.items() + depot_info.items())
180 DEPOT_NAMES = DEPOT_DEPS_NAME.keys()
183 def OutputAnnotationStepStart(name):
184 """Outputs annotation to signal the start of a step to a try bot.
186 Args:
187 name: The name of the step.
189 print
190 print '@@@SEED_STEP %s@@@' % name
191 print '@@@STEP_CURSOR %s@@@' % name
192 print '@@@STEP_STARTED@@@'
193 print
194 sys.stdout.flush()
197 def OutputAnnotationStepClosed():
198 """Outputs annotation to signal the closing of a step to a try bot."""
199 print
200 print '@@@STEP_CLOSED@@@'
201 print
202 sys.stdout.flush()
205 def OutputAnnotationStepLink(label, url):
206 """Outputs appropriate annotation to print a link.
208 Args:
209 label: The name to print.
210 url: The URL to print.
212 print
213 print '@@@STEP_LINK@%s@%s@@@' % (label, url)
214 print
215 sys.stdout.flush()
218 def LoadExtraSrc(path_to_file):
219 """Attempts to load an extra source file, and overrides global values.
221 If the extra source file is loaded successfully, then it will use the new
222 module to override some global values, such as gclient spec data.
224 Args:
225 path_to_file: File path.
227 Returns:
228 The loaded module object, or None if none was imported.
230 try:
231 global GCLIENT_SPEC_DATA
232 global GCLIENT_SPEC_ANDROID
233 extra_src = imp.load_source('data', path_to_file)
234 GCLIENT_SPEC_DATA = extra_src.GetGClientSpec()
235 GCLIENT_SPEC_ANDROID = extra_src.GetGClientSpecExtraParams()
236 return extra_src
237 except ImportError:
238 return None
241 def IsTelemetryCommand(command):
242 """Attempts to discern whether or not a given command is running telemetry."""
243 return 'tools/perf/run_' in command or 'tools\\perf\\run_' in command
246 def _CreateAndChangeToSourceDirectory(working_directory):
247 """Creates a directory 'bisect' as a subdirectory of |working_directory|.
249 If successful, the current working directory will be changed to the new
250 'bisect' directory.
252 Args:
253 working_directory: The directory to create the new 'bisect' directory in.
255 Returns:
256 True if the directory was successfully created (or already existed).
258 cwd = os.getcwd()
259 os.chdir(working_directory)
260 try:
261 os.mkdir(BISECT_DIR)
262 except OSError, e:
263 if e.errno != errno.EEXIST: # EEXIST indicates that it already exists.
264 os.chdir(cwd)
265 return False
266 os.chdir(BISECT_DIR)
267 return True
270 def _SubprocessCall(cmd, cwd=None):
271 """Runs a command in a subprocess.
273 Args:
274 cmd: The command to run.
275 cwd: Working directory to run from.
277 Returns:
278 The return code of the call.
280 if os.name == 'nt':
281 # "HOME" isn't normally defined on windows, but is needed
282 # for git to find the user's .netrc file.
283 if not os.getenv('HOME'):
284 os.environ['HOME'] = os.environ['USERPROFILE']
285 shell = os.name == 'nt'
286 return subprocess.call(cmd, shell=shell, cwd=cwd)
289 def RunGClient(params, cwd=None):
290 """Runs gclient with the specified parameters.
292 Args:
293 params: A list of parameters to pass to gclient.
294 cwd: Working directory to run from.
296 Returns:
297 The return code of the call.
299 cmd = ['gclient'] + params
300 return _SubprocessCall(cmd, cwd=cwd)
303 def RunGClientAndCreateConfig(opts, custom_deps=None, cwd=None):
304 """Runs gclient and creates a config containing both src and src-internal.
306 Args:
307 opts: The options parsed from the command line through parse_args().
308 custom_deps: A dictionary of additional dependencies to add to .gclient.
309 cwd: Working directory to run from.
311 Returns:
312 The return code of the call.
314 spec = GCLIENT_SPEC_DATA
316 if custom_deps:
317 for k, v in custom_deps.iteritems():
318 spec[0]['custom_deps'][k] = v
320 # Cannot have newlines in string on windows
321 spec = 'solutions =' + str(spec)
322 spec = ''.join([l for l in spec.splitlines()])
324 if 'android' in opts.target_platform:
325 spec += GCLIENT_SPEC_ANDROID
327 return_code = RunGClient(
328 ['config', '--spec=%s' % spec], cwd=cwd)
329 return return_code
333 def OnAccessError(func, path, _):
334 """Error handler for shutil.rmtree.
336 Source: http://goo.gl/DEYNCT
338 If the error is due to an access error (read only file), it attempts to add
339 write permissions, then retries.
341 If the error is for another reason it re-raises the error.
343 Args:
344 func: The function that raised the error.
345 path: The path name passed to func.
346 _: Exception information from sys.exc_info(). Not used.
348 if not os.access(path, os.W_OK):
349 os.chmod(path, stat.S_IWUSR)
350 func(path)
351 else:
352 raise
355 def _CleanupPreviousGitRuns(cwd=os.getcwd()):
356 """Cleans up any leftover index.lock files after running git."""
357 # If a previous run of git crashed, or bot was reset, etc., then we might
358 # end up with leftover index.lock files.
359 for path, _, files in os.walk(cwd):
360 for cur_file in files:
361 if cur_file.endswith('index.lock'):
362 path_to_file = os.path.join(path, cur_file)
363 os.remove(path_to_file)
366 def RunGClientAndSync(cwd=None):
367 """Runs gclient and does a normal sync.
369 Args:
370 cwd: Working directory to run from.
372 Returns:
373 The return code of the call.
375 params = ['sync', '--verbose', '--nohooks', '--reset', '--force',
376 '--delete_unversioned_trees']
377 return RunGClient(params, cwd=cwd)
380 def SetupGitDepot(opts, custom_deps):
381 """Sets up the depot for the bisection.
383 The depot will be located in a subdirectory called 'bisect'.
385 Args:
386 opts: The options parsed from the command line through parse_args().
387 custom_deps: A dictionary of additional dependencies to add to .gclient.
389 Returns:
390 True if gclient successfully created the config file and did a sync, False
391 otherwise.
393 name = 'Setting up Bisection Depot'
394 try:
395 if opts.output_buildbot_annotations:
396 OutputAnnotationStepStart(name)
398 if RunGClientAndCreateConfig(opts, custom_deps):
399 return False
401 _CleanupPreviousGitRuns()
402 RunGClient(['revert'])
403 return not RunGClientAndSync()
404 finally:
405 if opts.output_buildbot_annotations:
406 OutputAnnotationStepClosed()
409 def CheckIfBisectDepotExists(opts):
410 """Checks if the bisect directory already exists.
412 Args:
413 opts: The options parsed from the command line through parse_args().
415 Returns:
416 Returns True if it exists.
418 path_to_dir = os.path.join(opts.working_directory, BISECT_DIR, 'src')
419 return os.path.exists(path_to_dir)
422 def CheckRunGit(command, cwd=None):
423 """Run a git subcommand, returning its output and return code. Asserts if
424 the return code of the call is non-zero.
426 Args:
427 command: A list containing the args to git.
429 Returns:
430 A tuple of the output and return code.
432 (output, return_code) = RunGit(command, cwd=cwd)
434 assert not return_code, 'An error occurred while running'\
435 ' "git %s"' % ' '.join(command)
436 return output
439 def RunGit(command, cwd=None):
440 """Run a git subcommand, returning its output and return code.
442 Args:
443 command: A list containing the args to git.
444 cwd: A directory to change to while running the git command (optional).
446 Returns:
447 A tuple of the output and return code.
449 command = ['git'] + command
450 return RunProcessAndRetrieveOutput(command, cwd=cwd)
453 def CreateBisectDirectoryAndSetupDepot(opts, custom_deps):
454 """Sets up a subdirectory 'bisect' and then retrieves a copy of the depot
455 there using gclient.
457 Args:
458 opts: The options parsed from the command line through parse_args().
459 custom_deps: A dictionary of additional dependencies to add to .gclient.
461 if CheckIfBisectDepotExists(opts):
462 path_to_dir = os.path.join(os.path.abspath(opts.working_directory),
463 BISECT_DIR, 'src')
464 (output, _) = RunGit(['rev-parse', '--is-inside-work-tree'],
465 cwd=path_to_dir)
466 if output.strip() == 'true':
467 # Before checking out master, cleanup up any leftover index.lock files.
468 _CleanupPreviousGitRuns(path_to_dir)
469 # Checks out the master branch, throws an exception if git command fails.
470 CheckRunGit(['checkout', '-f', 'master'], cwd=path_to_dir)
471 if not _CreateAndChangeToSourceDirectory(opts.working_directory):
472 raise RuntimeError('Could not create bisect directory.')
474 if not SetupGitDepot(opts, custom_deps):
475 raise RuntimeError('Failed to grab source.')
478 def RunProcess(command):
479 """Runs an arbitrary command.
481 If output from the call is needed, use RunProcessAndRetrieveOutput instead.
483 Args:
484 command: A list containing the command and args to execute.
486 Returns:
487 The return code of the call.
489 # On Windows, use shell=True to get PATH interpretation.
490 shell = IsWindowsHost()
491 return subprocess.call(command, shell=shell)
494 def RunProcessAndRetrieveOutput(command, cwd=None):
495 """Runs an arbitrary command, returning its output and return code.
497 Since output is collected via communicate(), there will be no output until
498 the call terminates. If you need output while the program runs (ie. so
499 that the buildbot doesn't terminate the script), consider RunProcess().
501 Args:
502 command: A list containing the command and args to execute.
503 cwd: A directory to change to while running the command. The command can be
504 relative to this directory. If this is None, the command will be run in
505 the current directory.
507 Returns:
508 A tuple of the output and return code.
510 if cwd:
511 original_cwd = os.getcwd()
512 os.chdir(cwd)
514 # On Windows, use shell=True to get PATH interpretation.
515 shell = IsWindowsHost()
516 proc = subprocess.Popen(command, shell=shell, stdout=subprocess.PIPE)
517 (output, _) = proc.communicate()
519 if cwd:
520 os.chdir(original_cwd)
522 return (output, proc.returncode)
525 def IsStringInt(string_to_check):
526 """Checks whether or not the given string can be converted to an int."""
527 try:
528 int(string_to_check)
529 return True
530 except ValueError:
531 return False
534 def IsStringFloat(string_to_check):
535 """Checks whether or not the given string can be converted to a float."""
536 try:
537 float(string_to_check)
538 return True
539 except ValueError:
540 return False
543 def IsWindowsHost():
544 return sys.platform == 'cygwin' or sys.platform.startswith('win')
547 def Is64BitWindows():
548 """Checks whether or not Windows is a 64-bit version."""
549 platform = os.environ.get('PROCESSOR_ARCHITEW6432')
550 if not platform:
551 # Must not be running in WoW64, so PROCESSOR_ARCHITECTURE is correct.
552 platform = os.environ.get('PROCESSOR_ARCHITECTURE')
553 return platform and platform in ['AMD64', 'I64']
556 def IsLinuxHost():
557 return sys.platform.startswith('linux')
560 def IsMacHost():
561 return sys.platform.startswith('darwin')