Upstreaming browser/ui/uikit_ui_util from iOS.
[chromium-blink-merge.git] / tools / auto_bisect / bisect_utils.py
blob82fc93fdc241a9aa925e646439400f4bb6c14fd5
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 # svn: URL of SVN repository. Needed for git workflow to resolve hashes to
83 # SVN revisions.
84 # from: Parent depot that must be bisected before this is bisected.
85 # deps_var: Key name in vars variable in DEPS file that has revision
86 # information.
87 DEPOT_DEPS_NAME = {
88 'chromium': {
89 'src': 'src',
90 'recurse': True,
91 'from': ['android-chrome'],
92 'viewvc': 'https://chromium.googlesource.com/chromium/src/+/',
93 'deps_var': 'chromium_rev'
95 'webkit': {
96 'src': 'src/third_party/WebKit',
97 'recurse': True,
98 'from': ['chromium'],
99 'viewvc': 'https://chromium.googlesource.com/chromium/blink/+/',
100 'deps_var': 'webkit_revision'
102 'angle': {
103 'src': 'src/third_party/angle',
104 'src_old': 'src/third_party/angle_dx11',
105 'recurse': True,
106 'from': ['chromium'],
107 'platform': 'nt',
108 'viewvc': 'https://chromium.googlesource.com/angle/angle/+/',
109 'deps_var': 'angle_revision'
111 'v8': {
112 'src': 'src/v8',
113 'recurse': True,
114 'from': ['chromium'],
115 'custom_deps': GCLIENT_CUSTOM_DEPS_V8,
116 'viewvc': 'https://chromium.googlesource.com/v8/v8.git/+/',
117 'deps_var': 'v8_revision'
119 'v8_bleeding_edge': {
120 'src': 'src/v8_bleeding_edge',
121 'recurse': True,
122 'svn': 'https://v8.googlecode.com/svn/branches/bleeding_edge',
123 'from': ['v8'],
124 'viewvc': 'https://chromium.googlesource.com/v8/v8.git/+/',
125 'deps_var': 'v8_revision'
127 'skia/src': {
128 'src': 'src/third_party/skia/src',
129 'recurse': True,
130 'from': ['chromium'],
131 'viewvc': 'https://chromium.googlesource.com/skia/+/',
132 'deps_var': 'skia_revision'
136 DEPOT_NAMES = DEPOT_DEPS_NAME.keys()
138 # The possible values of the --bisect_mode flag, which determines what to
139 # use when classifying a revision as "good" or "bad".
140 BISECT_MODE_MEAN = 'mean'
141 BISECT_MODE_STD_DEV = 'std_dev'
142 BISECT_MODE_RETURN_CODE = 'return_code'
145 def AddAdditionalDepotInfo(depot_info):
146 """Adds additional depot info to the global depot variables."""
147 global DEPOT_DEPS_NAME
148 global DEPOT_NAMES
149 DEPOT_DEPS_NAME = dict(DEPOT_DEPS_NAME.items() + depot_info.items())
150 DEPOT_NAMES = DEPOT_DEPS_NAME.keys()
153 def OutputAnnotationStepStart(name):
154 """Outputs annotation to signal the start of a step to a try bot.
156 Args:
157 name: The name of the step.
159 print
160 print '@@@SEED_STEP %s@@@' % name
161 print '@@@STEP_CURSOR %s@@@' % name
162 print '@@@STEP_STARTED@@@'
163 print
164 sys.stdout.flush()
167 def OutputAnnotationStepClosed():
168 """Outputs annotation to signal the closing of a step to a try bot."""
169 print
170 print '@@@STEP_CLOSED@@@'
171 print
172 sys.stdout.flush()
175 def OutputAnnotationStepText(text):
176 """Outputs appropriate annotation to print text.
178 Args:
179 name: The text to print.
181 print
182 print '@@@STEP_TEXT@%s@@@' % text
183 print
184 sys.stdout.flush()
187 def OutputAnnotationStepWarning():
188 """Outputs appropriate annotation to signal a warning."""
189 print
190 print '@@@STEP_WARNINGS@@@'
191 print
194 def OutputAnnotationStepFailure():
195 """Outputs appropriate annotation to signal a warning."""
196 print
197 print '@@@STEP_FAILURE@@@'
198 print
201 def OutputAnnotationStepLink(label, url):
202 """Outputs appropriate annotation to print a link.
204 Args:
205 label: The name to print.
206 url: The URL to print.
208 print
209 print '@@@STEP_LINK@%s@%s@@@' % (label, url)
210 print
211 sys.stdout.flush()
214 def LoadExtraSrc(path_to_file):
215 """Attempts to load an extra source file, and overrides global values.
217 If the extra source file is loaded successfully, then it will use the new
218 module to override some global values, such as gclient spec data.
220 Args:
221 path_to_file: File path.
223 Returns:
224 The loaded module object, or None if none was imported.
226 try:
227 global GCLIENT_SPEC_DATA
228 global GCLIENT_SPEC_ANDROID
229 extra_src = imp.load_source('data', path_to_file)
230 GCLIENT_SPEC_DATA = extra_src.GetGClientSpec()
231 GCLIENT_SPEC_ANDROID = extra_src.GetGClientSpecExtraParams()
232 return extra_src
233 except ImportError:
234 return None
237 def IsTelemetryCommand(command):
238 """Attempts to discern whether or not a given command is running telemetry."""
239 return 'tools/perf/run_' in command or 'tools\\perf\\run_' in command
242 def _CreateAndChangeToSourceDirectory(working_directory):
243 """Creates a directory 'bisect' as a subdirectory of |working_directory|.
245 If successful, the current working directory will be changed to the new
246 'bisect' directory.
248 Args:
249 working_directory: The directory to create the new 'bisect' directory in.
251 Returns:
252 True if the directory was successfully created (or already existed).
254 cwd = os.getcwd()
255 os.chdir(working_directory)
256 try:
257 os.mkdir(BISECT_DIR)
258 except OSError, e:
259 if e.errno != errno.EEXIST: # EEXIST indicates that it already exists.
260 os.chdir(cwd)
261 return False
262 os.chdir(BISECT_DIR)
263 return True
266 def _SubprocessCall(cmd, cwd=None):
267 """Runs a command in a subprocess.
269 Args:
270 cmd: The command to run.
271 cwd: Working directory to run from.
273 Returns:
274 The return code of the call.
276 if os.name == 'nt':
277 # "HOME" isn't normally defined on windows, but is needed
278 # for git to find the user's .netrc file.
279 if not os.getenv('HOME'):
280 os.environ['HOME'] = os.environ['USERPROFILE']
281 shell = os.name == 'nt'
282 return subprocess.call(cmd, shell=shell, cwd=cwd)
285 def RunGClient(params, cwd=None):
286 """Runs gclient with the specified parameters.
288 Args:
289 params: A list of parameters to pass to gclient.
290 cwd: Working directory to run from.
292 Returns:
293 The return code of the call.
295 cmd = ['gclient'] + params
296 return _SubprocessCall(cmd, cwd=cwd)
299 def RunGClientAndCreateConfig(opts, custom_deps=None, cwd=None):
300 """Runs gclient and creates a config containing both src and src-internal.
302 Args:
303 opts: The options parsed from the command line through parse_args().
304 custom_deps: A dictionary of additional dependencies to add to .gclient.
305 cwd: Working directory to run from.
307 Returns:
308 The return code of the call.
310 spec = GCLIENT_SPEC_DATA
312 if custom_deps:
313 for k, v in custom_deps.iteritems():
314 spec[0]['custom_deps'][k] = v
316 # Cannot have newlines in string on windows
317 spec = 'solutions =' + str(spec)
318 spec = ''.join([l for l in spec.splitlines()])
320 if 'android' in opts.target_platform:
321 spec += GCLIENT_SPEC_ANDROID
323 return_code = RunGClient(
324 ['config', '--spec=%s' % spec], cwd=cwd)
325 return return_code
328 def OnAccessError(func, path, _):
329 """Error handler for shutil.rmtree.
331 Source: http://goo.gl/DEYNCT
333 If the error is due to an access error (read only file), it attempts to add
334 write permissions, then retries.
336 If the error is for another reason it re-raises the error.
338 Args:
339 func: The function that raised the error.
340 path: The path name passed to func.
341 _: Exception information from sys.exc_info(). Not used.
343 if not os.access(path, os.W_OK):
344 os.chmod(path, stat.S_IWUSR)
345 func(path)
346 else:
347 raise
350 def _CleanupPreviousGitRuns(cwd=os.getcwd()):
351 """Cleans up any leftover index.lock files after running git."""
352 # If a previous run of git crashed, or bot was reset, etc., then we might
353 # end up with leftover index.lock files.
354 for path, _, files in os.walk(cwd):
355 for cur_file in files:
356 if cur_file.endswith('index.lock'):
357 path_to_file = os.path.join(path, cur_file)
358 os.remove(path_to_file)
361 def RunGClientAndSync(revisions=None, cwd=None):
362 """Runs gclient and does a normal sync.
364 Args:
365 revisions: List of revisions that need to be synced.
366 E.g., "src@2ae43f...", "src/third_party/webkit@asr1234" etc.
367 cwd: Working directory to run from.
369 Returns:
370 The return code of the call.
372 params = ['sync', '--verbose', '--nohooks', '--force',
373 '--delete_unversioned_trees']
374 if revisions is not None:
375 for revision in revisions:
376 if revision is not None:
377 params.extend(['--revision', revision])
378 return RunGClient(params, cwd=cwd)
381 def SetupGitDepot(opts, custom_deps):
382 """Sets up the depot for the bisection.
384 The depot will be located in a subdirectory called 'bisect'.
386 Args:
387 opts: The options parsed from the command line through parse_args().
388 custom_deps: A dictionary of additional dependencies to add to .gclient.
390 Returns:
391 True if gclient successfully created the config file and did a sync, False
392 otherwise.
394 name = 'Setting up Bisection Depot'
395 try:
396 if opts.output_buildbot_annotations:
397 OutputAnnotationStepStart(name)
399 if RunGClientAndCreateConfig(opts, custom_deps):
400 return False
402 _CleanupPreviousGitRuns()
403 RunGClient(['revert'])
404 return not RunGClientAndSync()
405 finally:
406 if opts.output_buildbot_annotations:
407 OutputAnnotationStepClosed()
410 def CheckIfBisectDepotExists(opts):
411 """Checks if the bisect directory already exists.
413 Args:
414 opts: The options parsed from the command line through parse_args().
416 Returns:
417 Returns True if it exists.
419 path_to_dir = os.path.join(opts.working_directory, BISECT_DIR, 'src')
420 return os.path.exists(path_to_dir)
423 def CheckRunGit(command, cwd=None):
424 """Run a git subcommand, returning its output and return code. Asserts if
425 the return code of the call is non-zero.
427 Args:
428 command: A list containing the args to git.
430 Returns:
431 A tuple of the output and return code.
433 output, return_code = RunGit(command, cwd=cwd)
435 assert not return_code, 'An error occurred while running'\
436 ' "git %s"' % ' '.join(command)
437 return output
440 def RunGit(command, cwd=None):
441 """Run a git subcommand, returning its output and return code.
443 Args:
444 command: A list containing the args to git.
445 cwd: A directory to change to while running the git command (optional).
447 Returns:
448 A tuple of the output and return code.
450 command = ['git'] + command
451 return RunProcessAndRetrieveOutput(command, cwd=cwd)
454 def CreateBisectDirectoryAndSetupDepot(opts, custom_deps):
455 """Sets up a subdirectory 'bisect' and then retrieves a copy of the depot
456 there using gclient.
458 Args:
459 opts: The options parsed from the command line through parse_args().
460 custom_deps: A dictionary of additional dependencies to add to .gclient.
462 if CheckIfBisectDepotExists(opts):
463 path_to_dir = os.path.join(os.path.abspath(opts.working_directory),
464 BISECT_DIR, 'src')
465 output, _ = RunGit(['rev-parse', '--is-inside-work-tree'], 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, cwd=None, shell=False):
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 = shell or IsWindowsHost()
491 return subprocess.call(command, cwd=cwd, 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(
517 command, shell=shell, stdout=subprocess.PIPE,
518 stderr=subprocess.STDOUT)
519 output, _ = proc.communicate()
521 if cwd:
522 os.chdir(original_cwd)
524 return (output, proc.returncode)
527 def IsStringInt(string_to_check):
528 """Checks whether or not the given string can be converted to an int."""
529 try:
530 int(string_to_check)
531 return True
532 except ValueError:
533 return False
536 def IsStringFloat(string_to_check):
537 """Checks whether or not the given string can be converted to a float."""
538 try:
539 float(string_to_check)
540 return True
541 except ValueError:
542 return False
545 def IsWindowsHost():
546 return sys.platform == 'cygwin' or sys.platform.startswith('win')
549 def Is64BitWindows():
550 """Checks whether or not Windows is a 64-bit version."""
551 platform = os.environ.get('PROCESSOR_ARCHITEW6432')
552 if not platform:
553 # Must not be running in WoW64, so PROCESSOR_ARCHITECTURE is correct.
554 platform = os.environ.get('PROCESSOR_ARCHITECTURE')
555 return platform and platform in ['AMD64', 'I64']
558 def IsLinuxHost():
559 return sys.platform.startswith('linux')
562 def IsMacHost():
563 return sys.platform.startswith('darwin')