blob: 06dcb71b325e2fcfccdfb435ae2db00a92d3f62e [file] [log] [blame]
# -*- coding: utf-8 -*-
# Copyright 2018 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Wrapper of bisector scripts"""
from __future__ import print_function
import importlib
import io
import logging
import subprocess
import sys
from bisect_kit import bisector_cli
from bisect_kit import core
logger = logging.getLogger(__name__)
class BisectorWrapper:
"""Wrapper of bisector scripts as python module."""
def __init__(self, name, session):
self.name = name
self.module = importlib.import_module(name)
self.session = session
self.switch_cmds = []
self.domain_cls = None
for x in vars(self.module).values():
if isinstance(x, type) and issubclass(x, core.BisectDomain):
self.domain_cls = x
break
assert self.domain_cls
def call(self, cmd, *args, **kwargs):
bisector = bisector_cli.BisectorCommandLine(self.domain_cls)
full_args = [cmd, '--session', self.session] + list(args)
logger.debug('call %s: %s', self.name, full_args)
stdout_bak = sys.stdout
try:
if kwargs.get('capture_output'):
sys.stdout = fake_stdout = io.StringIO()
bisector.main(*full_args, prog=self.name)
finally:
sys.stdout = stdout_bak
if kwargs.get('capture_output'):
return fake_stdout.getvalue()
return None
def current_status(self):
bisector = bisector_cli.BisectorCommandLine(self.domain_cls)
return bisector.current_status(session=self.session)
def init_if_necessary(self,
old,
new,
init_args,
switch_cmd,
eval_cmd,
old_value=None,
new_value=None,
term_old=None,
term_new=None,
recompute_init_values=False,
noisy=None):
status = self.current_status()
if not status['inited']:
common_init_args = ['--old', old, '--new', new]
if term_old:
common_init_args += ['--term_old', term_old]
if term_new:
common_init_args += ['--term_new', term_new]
if noisy:
common_init_args += ['--noisy', noisy]
if old_value is not None:
common_init_args += ['--old_value', str(old_value)]
if new_value is not None:
common_init_args += ['--new_value', str(new_value)]
if recompute_init_values:
common_init_args.append('--recompute_init_values')
# Note that, if 'init' failed, no session file for this bisector is
# created. In other words, the error event is only recorded in
# diagnoser's log.
self.call('init', *(common_init_args + init_args))
self.switch_cmds = [switch_cmd]
self.call('config', 'switch', *switch_cmd)
self.call('config', 'eval', *eval_cmd)
def _set_switch_commands(self):
cmdline = ' && '.join(
subprocess.list2cmdline(cmd) for cmd in self.switch_cmds)
self.call('config', 'switch', 'sh', '-c', cmdline)
def append_switch_command(self, cmd):
self.switch_cmds.append(cmd)
self._set_switch_commands()
def narrow_down(self):
status = self.current_status()
logger.info('%s old=%s, new=%s, done=%s', self.name, status['old'],
status['new'], status['done'])
try:
self.call('run')
except Exception:
# If bisector failed after bisect range verified, we can continue with
# knowledge of the partial results. For example,
# - ChromeOS prebuilt bisector may cut down half the bisect range and
# failed. Although it didn't find the narrowest range, we can still
# continue to bisect Android and Chrome using the new range.
# - If Android prebuilt bisection failed after verification, although
# the range may not shrink, it indicates the culprit is inside Android
# and we should continue bisect Android localbuild.
status = self.current_status()
if status['verified']:
logger.exception(
'got exception; still can continue with partial results')
else:
raise
else:
status = self.current_status()
logger.info('%s result old=%s, new=%s, noisy=%s, done=%s', self.name,
status['old'], status['new'], status['estimated_noise'],
status['done'])
self.call('view')
return status['old'], status['new'], status['estimated_noise']