# -*- coding: utf-8 -*-
# Copyright 2017 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.

"""Test chrome_on_cros_bisector module."""

from __future__ import print_function

import copy
import itertools
import os

import mock

from chromite.cli import flash
from chromite.cros_bisect import common
from chromite.cros_bisect import builder as builder_module
from chromite.cros_bisect import evaluator as evaluator_module
from chromite.cros_bisect import chrome_on_cros_bisector
from chromite.cros_bisect import git_bisector_unittest
from chromite.lib import commandline
from chromite.lib import cros_test_lib
from chromite.lib import gs
from chromite.lib import gs_unittest
from chromite.lib import partial_mock


class DummyEvaluator(evaluator_module.Evaluator):
  """Evaluator which just return empty score."""

  # pylint: disable=unused-argument
  def Evaluate(self, remote, build_label, repeat):
    return common.Score()

  def CheckLastEvaluate(self, build_label, repeat=1):
    return common.Score()

class TestChromeOnCrosBisector(cros_test_lib.MockTempDirTestCase):
  """Tests ChromeOnCrosBisector class."""

  BOARD = 'samus'
  TEST_NAME = 'graphics_WebGLAquarium'
  METRIC = 'avg_fps_1000_fishes/summary/value'
  REPORT_FILE = 'reports.json'
  DUT_ADDR = '192.168.1.1'
  DUT = commandline.DeviceParser(commandline.DEVICE_SCHEME_SSH)(DUT_ADDR)

  # Be aware that GOOD_COMMIT_INFO and BAD_COMMIT_INFO should be assigned via
  # copy.deepcopy() as their users are likely to change the content.
  GOOD_COMMIT_SHA1 = '44af5c9a5505'
  GOOD_COMMIT_TIMESTAMP = 1486526594
  GOOD_COMMIT_SCORE = common.Score([100])
  GOOD_COMMIT_INFO = common.CommitInfo(
      sha1=GOOD_COMMIT_SHA1, timestamp=GOOD_COMMIT_TIMESTAMP, title='good',
      label='last-known-good  ', score=GOOD_COMMIT_SCORE)

  BAD_COMMIT_SHA1 = '6a163bb66c3e'
  BAD_COMMIT_TIMESTAMP = 1486530021
  BAD_COMMIT_SCORE = common.Score([80])
  BAD_COMMIT_INFO = common.CommitInfo(
      sha1=BAD_COMMIT_SHA1, timestamp=BAD_COMMIT_TIMESTAMP, title='bad',
      label='last-known-bad   ', score=BAD_COMMIT_SCORE)

  GOOD_CROS_VERSION = 'R60-9592.50.0'
  BAD_CROS_VERSION = 'R60-9592.51.0'

  CULPRIT_COMMIT_SHA1 = '12345abcde'
  CULPRIT_COMMIT_TIMESTAMP = 1486530000
  CULPRIT_COMMIT_SCORE = common.Score([81])
  CULPRIT_COMMIT_INFO = common.CommitInfo(
      sha1=CULPRIT_COMMIT_SHA1, timestamp=CULPRIT_COMMIT_TIMESTAMP, title='bad',
      score=CULPRIT_COMMIT_SCORE)

  THRESHOLD_SPLITTER = 95  # Score between good and bad, closer to good side.
  THRESHOLD = 5  # Distance between good score and splitter.

  REPEAT = 3

  GOOD_METADATA_CONTENT = '\n'.join([
      '{',
      '  "metadata-version": "2",',
      '  "toolchain-url": "2017/05/%(target)s-2017.05.25.101355.tar.xz",',
      '  "suite_scheduling": true,',
      '  "build_id": 1644146,',
      '  "version": {',
      '    "full": "R60-9592.50.0",',
      '    "android-branch": "git_mnc-dr-arc-m60",',
      '    "chrome": "60.0.3112.53",',
      '    "platform": "9592.50.0",',
      '    "milestone": "60",',
      '    "android": "4150402"',
      '  }',
      '}'])

  def setUp(self):
    """Sets up test case."""
    self.options = cros_test_lib.EasyAttr(
        base_dir=self.tempdir, board=self.BOARD, reuse_repo=True,
        good=self.GOOD_COMMIT_SHA1, bad=self.BAD_COMMIT_SHA1, remote=self.DUT,
        eval_repeat=self.REPEAT, auto_threshold=False, reuse_eval=False,
        cros_flash_sleep=0.01, cros_flash_retry=3, cros_flash_backoff=1,
        eval_raise_on_error=False, skip_failed_commit=False)

    self.repo_dir = os.path.join(self.tempdir,
                                 builder_module.Builder.DEFAULT_REPO_DIR)
    self.SetUpBisector()

  def SetUpBisector(self):
    """Instantiates self.bisector using self.options."""
    self.evaluator = DummyEvaluator(self.options)
    self.builder = builder_module.Builder(self.options)
    self.bisector = chrome_on_cros_bisector.ChromeOnCrosBisector(
        self.options, self.builder, self.evaluator)

  def SetUpBisectorWithCrosVersion(self):
    """Instantiates self.bisector using CrOS version as good and bad options."""
    self.options.good = self.GOOD_CROS_VERSION
    self.options.bad = self.BAD_CROS_VERSION
    self.SetUpBisector()

  def SetDefaultCommitInfo(self):
    """Sets up default commit info."""
    self.bisector.good_commit_info = copy.deepcopy(self.GOOD_COMMIT_INFO)
    self.bisector.bad_commit_info = copy.deepcopy(self.BAD_COMMIT_INFO)

  def testInit(self):
    """Tests __init__() with SHA1 as good and bad options."""
    self.assertEqual(self.GOOD_COMMIT_SHA1, self.bisector.good_commit)
    self.assertIsNone(self.bisector.good_cros_version)
    self.assertEqual(self.BAD_COMMIT_SHA1, self.bisector.bad_commit)
    self.assertIsNone(self.bisector.bad_cros_version)
    self.assertFalse(self.bisector.bisect_between_cros_version)

    self.assertEqual(self.DUT_ADDR, self.bisector.remote.raw)
    self.assertEqual(self.REPEAT, self.bisector.eval_repeat)
    self.assertEqual(self.builder, self.bisector.builder)
    self.assertEqual(self.repo_dir, self.bisector.repo_dir)

    self.assertIsNone(self.bisector.good_commit_info)
    self.assertIsNone(self.bisector.bad_commit_info)
    self.assertEqual(0, len(self.bisector.bisect_log))
    self.assertIsNone(self.bisector.threshold)
    self.assertTrue(not self.bisector.current_commit)

  def testInitCrosVersion(self):
    """Tests __init__() with CrOS version as good and bad options."""
    self.SetUpBisectorWithCrosVersion()

    self.assertEqual(self.GOOD_CROS_VERSION, self.bisector.good_cros_version)
    self.assertIsNone(self.bisector.good_commit)
    self.assertEqual(self.BAD_CROS_VERSION, self.bisector.bad_cros_version)
    self.assertIsNone(self.bisector.bad_commit)
    self.assertTrue(self.bisector.bisect_between_cros_version)

    self.assertEqual(self.DUT_ADDR, self.bisector.remote.raw)
    self.assertEqual(self.REPEAT, self.bisector.eval_repeat)
    self.assertEqual(self.builder, self.bisector.builder)
    self.assertEqual(self.repo_dir, self.bisector.repo_dir)

    self.assertIsNone(self.bisector.good_commit_info)
    self.assertIsNone(self.bisector.bad_commit_info)
    self.assertEqual(0, len(self.bisector.bisect_log))
    self.assertIsNone(self.bisector.threshold)
    self.assertTrue(not self.bisector.current_commit)

  def testInitMissingRequiredArgs(self):
    """Tests that ChromeOnCrosBisector raises for missing required argument."""
    options = cros_test_lib.EasyAttr()
    with self.assertRaises(common.MissingRequiredOptionsException) as cm:
      chrome_on_cros_bisector.ChromeOnCrosBisector(options, self.builder,
                                                   self.evaluator)
    exception_message = str(cm.exception)
    self.assertIn('Missing command line', exception_message)
    self.assertIn('ChromeOnCrosBisector', exception_message)
    for arg in chrome_on_cros_bisector.ChromeOnCrosBisector.REQUIRED_ARGS:
      self.assertIn(arg, exception_message)

  def testCheckCommitFormat(self):
    """Tests CheckCommitFormat()."""
    CheckCommitFormat = (
        chrome_on_cros_bisector.ChromeOnCrosBisector.CheckCommitFormat)
    self.assertEqual(self.GOOD_COMMIT_SHA1,
                     CheckCommitFormat(self.GOOD_COMMIT_SHA1))
    self.assertEqual(self.GOOD_CROS_VERSION,
                     CheckCommitFormat(self.GOOD_CROS_VERSION))
    self.assertEqual('R60-9592.50.0',
                     CheckCommitFormat('60.9592.50.0'))
    invalid = 'bad_sha1'
    self.assertIsNone(CheckCommitFormat(invalid))

  def testObtainBisectBoundaryScoreImpl(self):
    """Tests ObtainBisectBoundaryScoreImpl()."""
    git_mock = self.StartPatcher(git_bisector_unittest.GitMock(self.repo_dir))
    git_mock.AddRunGitResult(['checkout', self.GOOD_COMMIT_SHA1])
    git_mock.AddRunGitResult(['checkout', self.BAD_COMMIT_SHA1])

    build_deploy_eval_mock = self.PatchObject(
        chrome_on_cros_bisector.ChromeOnCrosBisector, 'BuildDeployEval')
    build_deploy_eval_mock.side_effect = [self.GOOD_COMMIT_SCORE,
                                          self.BAD_COMMIT_SCORE]

    self.assertEqual(self.GOOD_COMMIT_SCORE,
                     self.bisector.ObtainBisectBoundaryScoreImpl(True))
    self.assertEqual(self.BAD_COMMIT_SCORE,
                     self.bisector.ObtainBisectBoundaryScoreImpl(False))

    self.assertEqual(
        [mock.call(customize_build_deploy=None, eval_label=None),
         mock.call(customize_build_deploy=None, eval_label=None)],
        build_deploy_eval_mock.call_args_list)

  def testObtainBisectBoundaryScoreImplCrosVersion(self):
    """Tests ObtainBisectBoundaryScoreImpl() with CrOS version."""
    self.SetUpBisectorWithCrosVersion()
    # Inject good_commit and bad_commit as if
    # bisector.ResolveChromeBisectRangeFromCrosVersion() being run.
    self.bisector.good_commit = self.GOOD_COMMIT_SHA1
    self.bisector.bad_commit = self.BAD_COMMIT_SHA1

    git_mock = self.StartPatcher(git_bisector_unittest.GitMock(self.repo_dir))
    git_mock.AddRunGitResult(['checkout', self.GOOD_COMMIT_SHA1])
    git_mock.AddRunGitResult(['checkout', self.BAD_COMMIT_SHA1])

    self.PatchObject(chrome_on_cros_bisector.ChromeOnCrosBisector,
                     'UpdateCurrentCommit')
    evaluate_mock = self.PatchObject(DummyEvaluator, 'Evaluate')

    # Mock FlashCrosImage() to verify that customize_build_deploy is assigned
    # as expected.
    flash_cros_image_mock = self.PatchObject(
        chrome_on_cros_bisector.ChromeOnCrosBisector, 'FlashCrosImage')

    evaluate_mock.return_value = self.GOOD_COMMIT_SCORE
    self.assertEqual(self.GOOD_COMMIT_SCORE,
                     self.bisector.ObtainBisectBoundaryScoreImpl(True))
    flash_cros_image_mock.assert_called_with(
        self.bisector.GetCrosXbuddyPath(self.GOOD_CROS_VERSION))
    evaluate_mock.assert_called_with(
        self.DUT, 'cros_%s' % self.GOOD_CROS_VERSION, self.REPEAT)

    evaluate_mock.return_value = self.BAD_COMMIT_SCORE
    self.assertEqual(self.BAD_COMMIT_SCORE,
                     self.bisector.ObtainBisectBoundaryScoreImpl(False))
    flash_cros_image_mock.assert_called_with(
        self.bisector.GetCrosXbuddyPath(self.BAD_CROS_VERSION))
    evaluate_mock.assert_called_with(
        self.DUT, 'cros_%s' % self.BAD_CROS_VERSION, self.REPEAT)

  def testObtainBisectBoundaryScoreImplCrosVersionFlashError(self):
    """Tests ObtainBisectBoundaryScoreImpl() with CrOS version."""
    self.SetUpBisectorWithCrosVersion()
    # Inject good_commit and bad_commit as if
    # bisector.ResolveChromeBisectRangeFromCrosVersion() being run.
    self.bisector.good_commit = self.GOOD_COMMIT_SHA1
    self.bisector.bad_commit = self.BAD_COMMIT_SHA1

    git_mock = self.StartPatcher(git_bisector_unittest.GitMock(self.repo_dir))
    git_mock.AddRunGitResult(['checkout', self.GOOD_COMMIT_SHA1])
    git_mock.AddRunGitResult(['checkout', self.BAD_COMMIT_SHA1])

    self.PatchObject(chrome_on_cros_bisector.ChromeOnCrosBisector,
                     'UpdateCurrentCommit')
    evaluate_mock = self.PatchObject(DummyEvaluator, 'Evaluate')

    # Mock FlashCrosImage() to verify that customize_build_deploy is assigned
    # as expected.
    flash_cros_image_mock = self.PatchObject(
        chrome_on_cros_bisector.ChromeOnCrosBisector, 'FlashCrosImage')
    flash_cros_image_mock.side_effect = flash.FlashError('Flash failed.')

    with self.assertRaises(flash.FlashError):
      self.bisector.ObtainBisectBoundaryScoreImpl(True)

    flash_cros_image_mock.assert_called_with(
        self.bisector.GetCrosXbuddyPath(self.GOOD_CROS_VERSION))
    evaluate_mock.assert_not_called()

    with self.assertRaises(flash.FlashError):
      self.bisector.ObtainBisectBoundaryScoreImpl(False)
    flash_cros_image_mock.assert_called_with(
        self.bisector.GetCrosXbuddyPath(self.BAD_CROS_VERSION))
    evaluate_mock.assert_not_called()

  def testGetCrosXbuddyPath(self):
    """Tests GetCrosXbuddyPath()."""
    self.assertEqual(
        'xbuddy://remote/%s/%s/test' % (self.BOARD, self.GOOD_CROS_VERSION),
        self.bisector.GetCrosXbuddyPath(self.GOOD_CROS_VERSION))

  def testExchangeChromeSanityCheck(self):
    """Tests the flow of exchanging Chrome between good and bad CrOS."""
    self.SetUpBisectorWithCrosVersion()

    # Inject good_commit and bad_commit as if
    # bisector.ResolveChromeBisectRangeFromCrosVersion() has been run.
    self.bisector.good_commit = self.GOOD_COMMIT_SHA1
    self.bisector.bad_commit = self.BAD_COMMIT_SHA1

    # Inject commit_info and threshold as if
    # bisector.ObtainBisectBoundaryScore() and bisector.GetThresholdFromUser()
    # has been run.
    self.SetDefaultCommitInfo()
    self.bisector.threshold = self.THRESHOLD

    # Try bad Chrome first.
    git_mock = self.StartPatcher(git_bisector_unittest.GitMock(self.repo_dir))
    git_mock.AddRunGitResult(['checkout', self.BAD_COMMIT_SHA1])
    git_mock.AddRunGitResult(['checkout', self.GOOD_COMMIT_SHA1])

    self.PatchObject(chrome_on_cros_bisector.ChromeOnCrosBisector,
                     'UpdateCurrentCommit')

    evaluate_mock = self.PatchObject(DummyEvaluator, 'Evaluate')
    expected_evaluate_calls = [
        mock.call(self.DUT, x, self.REPEAT) for x in [
            'cros_%s_cr_%s' % (self.GOOD_CROS_VERSION, self.BAD_COMMIT_SHA1),
            'cros_%s_cr_%s' % (self.BAD_CROS_VERSION, self.GOOD_COMMIT_SHA1)]]

    # Mock FlashCrosImage() to verify that customize_build_deploy is assigned
    # as expected.
    flash_cros_image_mock = self.PatchObject(
        chrome_on_cros_bisector.ChromeOnCrosBisector, 'FlashCrosImage')
    expected_flash_cros_calls = [
        mock.call(self.bisector.GetCrosXbuddyPath(self.GOOD_CROS_VERSION)),
        mock.call(self.bisector.GetCrosXbuddyPath(self.BAD_CROS_VERSION))]

    # Make sure bisector.BuildDeploy() is also called.
    build_deploy_mock = self.PatchObject(
        chrome_on_cros_bisector.ChromeOnCrosBisector, 'BuildDeploy')

    # Assume culprit commit is in Chrome side, i.e. first score is bad.
    evaluate_mock.side_effect = [self.BAD_COMMIT_SCORE, self.GOOD_COMMIT_SCORE]

    self.assertTrue(self.bisector.ExchangeChromeSanityCheck())
    flash_cros_image_mock.assert_has_calls(expected_flash_cros_calls)
    evaluate_mock.assert_has_calls(expected_evaluate_calls)
    self.assertEqual(2, build_deploy_mock.call_count)

    flash_cros_image_mock.reset_mock()
    evaluate_mock.reset_mock()
    build_deploy_mock.reset_mock()

    # Assume culprit commit is not in Chrome side, i.e. first score is good.
    evaluate_mock.side_effect = [self.GOOD_COMMIT_SCORE, self.BAD_COMMIT_SCORE]

    self.assertFalse(self.bisector.ExchangeChromeSanityCheck())
    flash_cros_image_mock.assert_has_calls(expected_flash_cros_calls)
    evaluate_mock.assert_has_calls(expected_evaluate_calls)
    self.assertEqual(2, build_deploy_mock.call_count)

  def testExchangeChromeSanityCheckFlashError(self):
    """Tests the flow of exchanging Chrome between good and bad CrOS."""
    self.SetUpBisectorWithCrosVersion()

    # Inject good_commit and bad_commit as if
    # bisector.ResolveChromeBisectRangeFromCrosVersion() has been run.
    self.bisector.good_commit = self.GOOD_COMMIT_SHA1
    self.bisector.bad_commit = self.BAD_COMMIT_SHA1

    # Inject commit_info and threshold as if
    # bisector.ObtainBisectBoundaryScore() and bisector.GetThresholdFromUser()
    # has been run.
    self.SetDefaultCommitInfo()
    self.bisector.threshold = self.THRESHOLD

    # Try bad Chrome first.
    git_mock = self.StartPatcher(git_bisector_unittest.GitMock(self.repo_dir))
    git_mock.AddRunGitResult(['checkout', self.BAD_COMMIT_SHA1])
    git_mock.AddRunGitResult(['checkout', self.GOOD_COMMIT_SHA1])

    self.PatchObject(chrome_on_cros_bisector.ChromeOnCrosBisector,
                     'UpdateCurrentCommit')

    evaluate_mock = self.PatchObject(DummyEvaluator, 'Evaluate')

    # Mock FlashCrosImage() to verify that customize_build_deploy is assigned
    # as expected.
    flash_cros_image_mock = self.PatchObject(
        chrome_on_cros_bisector.ChromeOnCrosBisector, 'FlashCrosImage',
        side_effect=flash.FlashError('Flash failed.'))

    build_deploy_mock = self.PatchObject(
        chrome_on_cros_bisector.ChromeOnCrosBisector, 'BuildDeploy')

    with self.assertRaises(flash.FlashError):
      self.bisector.ExchangeChromeSanityCheck()

    evaluate_mock.assert_not_called()
    flash_cros_image_mock.assert_called()
    build_deploy_mock.assert_not_called()

  def testFlashImage(self):
    """Tests FlashImage()."""
    flash_mock = self.PatchObject(flash, 'Flash')
    xbuddy_path = self.bisector.GetCrosXbuddyPath(self.GOOD_CROS_VERSION)
    self.bisector.FlashCrosImage(xbuddy_path)
    flash_mock.assert_called_with(
        self.DUT, xbuddy_path, board=self.BOARD, clobber_stateful=True,
        disable_rootfs_verification=True)

  def testFlashImageRetry(self):
    """Tests FlashImage() with retry success."""

    flash_mock_call_counter = itertools.count()
    def flash_mock_return(*unused_args, **unused_kwargs):
      nth_call = next(flash_mock_call_counter)
      if nth_call < 3:
        raise flash.FlashError('Flash failed.')

    flash_mock = self.PatchObject(flash, 'Flash')
    flash_mock.side_effect = flash_mock_return
    xbuddy_path = self.bisector.GetCrosXbuddyPath(self.GOOD_CROS_VERSION)
    self.bisector.FlashCrosImage(xbuddy_path)
    flash_mock.assert_called_with(
        self.DUT, xbuddy_path, board=self.BOARD, clobber_stateful=True,
        disable_rootfs_verification=True)

  def testFlashImageRetryFailed(self):
    """Tests FlashImage() with retry failed."""
    flash_mock = self.PatchObject(flash, 'Flash')
    flash_mock.side_effect = flash.FlashError('Flash failed.')
    xbuddy_path = self.bisector.GetCrosXbuddyPath(self.GOOD_CROS_VERSION)
    with self.assertRaises(flash.FlashError):
      self.bisector.FlashCrosImage(xbuddy_path)
    flash_mock.assert_called_with(
        self.DUT, xbuddy_path, board=self.BOARD, clobber_stateful=True,
        disable_rootfs_verification=True)

  def testCrosVersionToChromeCommit(self):
    """Tests CrosVersionToChromeCommit()."""
    metadata_url = (
        'gs://chromeos-image-archive/%s-release/%s/partial-metadata.json' %
        (self.BOARD, self.GOOD_CROS_VERSION))
    gs_mock = self.StartPatcher(gs_unittest.GSContextMock())
    gs_mock.AddCmdResult(['cat', metadata_url],
                         output=self.GOOD_METADATA_CONTENT)

    git_log_content = '\n'.join([
        '8967dd66ad72 (tag: 60.0.3112.53) Publish DEPS for Chromium '
        '60.0.3112.53',
        '27ed0cc0c2f4 Incrementing VERSION to 60.0.3112.53'])
    git_mock = self.StartPatcher(git_bisector_unittest.GitMock(self.repo_dir))
    git_mock.AddRunGitResult(['log', '--oneline', '-n', '2', '60.0.3112.53'],
                             output=git_log_content)

    self.bisector.gs_ctx = gs.GSContext()
    self.assertEqual(
        '27ed0cc0c2f4',
        self.bisector.CrosVersionToChromeCommit(self.GOOD_CROS_VERSION))

  def testCrosVersionToChromeCommitFail(self):
    """Tests failure case of CrosVersionToChromeCommit()."""
    metadata_url = (
        'gs://chromeos-image-archive/%s-release/%s/partial-metadata.json' %
        (self.BOARD, self.GOOD_CROS_VERSION))
    gs_mock = self.StartPatcher(gs_unittest.GSContextMock())
    gs_mock.AddCmdResult(['cat', metadata_url], returncode=1)

    self.bisector.gs_ctx = gs.GSContext()
    self.assertIsNone(
        self.bisector.CrosVersionToChromeCommit(self.GOOD_CROS_VERSION))

    metadata_content = 'not_a_json'
    gs_mock.AddCmdResult(['cat', metadata_url], output=metadata_content)
    self.assertIsNone(
        self.bisector.CrosVersionToChromeCommit(self.GOOD_CROS_VERSION))

    metadata_content = '\n'.join([
        '{',
        '  "metadata-version": "2",',
        '  "toolchain-url": "2017/05/%(target)s-2017.05.25.101355.tar.xz",',
        '  "suite_scheduling": true,',
        '  "build_id": 1644146,',
        '  "version": {}',
        '}'])
    gs_mock.AddCmdResult(['cat', metadata_url], output=metadata_content)
    self.assertIsNone(
        self.bisector.CrosVersionToChromeCommit(self.GOOD_CROS_VERSION))

    gs_mock.AddCmdResult(['cat', metadata_url],
                         output=self.GOOD_METADATA_CONTENT)
    git_mock = self.StartPatcher(git_bisector_unittest.GitMock(self.repo_dir))
    git_mock.AddRunGitResult(['log', '--oneline', '-n', '2', '60.0.3112.53'],
                             returncode=128)
    self.assertIsNone(
        self.bisector.CrosVersionToChromeCommit(self.GOOD_CROS_VERSION))

  def testResolveChromeBisectRangeFromCrosVersion(self):
    """Tests ResolveChromeBisectRangeFromCrosVersion()."""
    self.SetUpBisectorWithCrosVersion()
    cros_to_chrome_mock = self.PatchObject(
        chrome_on_cros_bisector.ChromeOnCrosBisector,
        'CrosVersionToChromeCommit')
    cros_to_chrome_mock.side_effect = [self.GOOD_COMMIT_SHA1,
                                       self.BAD_COMMIT_SHA1]

    self.assertTrue(self.bisector.ResolveChromeBisectRangeFromCrosVersion())
    self.assertTrue(self.GOOD_COMMIT_SHA1, self.bisector.good_commit)
    self.assertTrue(self.BAD_COMMIT_SHA1, self.bisector.bad_commit)
    cros_to_chrome_mock.assert_has_calls([mock.call(self.GOOD_CROS_VERSION),
                                          mock.call(self.BAD_CROS_VERSION)])

    cros_to_chrome_mock.reset_mock()
    cros_to_chrome_mock.side_effect = [None]
    self.assertFalse(self.bisector.ResolveChromeBisectRangeFromCrosVersion())
    cros_to_chrome_mock.assert_called_with(self.GOOD_CROS_VERSION)

    cros_to_chrome_mock.reset_mock()
    cros_to_chrome_mock.side_effect = [self.GOOD_COMMIT_SHA1, None]
    self.assertFalse(self.bisector.ResolveChromeBisectRangeFromCrosVersion())
    cros_to_chrome_mock.assert_has_calls([mock.call(self.GOOD_CROS_VERSION),
                                          mock.call(self.BAD_CROS_VERSION)])

  def testPrepareBisect(self):
    """Tests PrepareBisect()."""
    # Pass SanityCheck().
    git_mock = self.StartPatcher(git_bisector_unittest.GitMock(self.repo_dir))
    git_mock.AddRunGitResult(
        partial_mock.InOrder(['rev-list', self.GOOD_COMMIT_SHA1]))
    git_mock.AddRunGitResult(
        partial_mock.InOrder(['rev-list', self.BAD_COMMIT_SHA1]))
    git_mock.AddRunGitResult(
        partial_mock.InOrder(['show', self.GOOD_COMMIT_SHA1]),
        output=str(self.GOOD_COMMIT_TIMESTAMP))
    git_mock.AddRunGitResult(
        partial_mock.InOrder(['show', self.BAD_COMMIT_SHA1]),
        output=str(self.BAD_COMMIT_TIMESTAMP))

    # Inject score for both side.
    git_mock.AddRunGitResult(['checkout', self.GOOD_COMMIT_SHA1])
    git_mock.AddRunGitResult(['checkout', self.BAD_COMMIT_SHA1])
    build_deploy_eval_mock = self.PatchObject(
        chrome_on_cros_bisector.ChromeOnCrosBisector, 'BuildDeployEval')
    build_deploy_eval_mock.side_effect = [self.GOOD_COMMIT_SCORE,
                                          self.BAD_COMMIT_SCORE]

    # Set auto_threshold.
    self.bisector.auto_threshold = True

    self.assertTrue(self.bisector.PrepareBisect())

  def testPrepareBisectCrosVersion(self):
    """Tests PrepareBisect() with CrOS version."""
    self.SetUpBisectorWithCrosVersion()

    self.StartPatcher(gs_unittest.GSContextMock())
    self.PatchObject(builder_module.Builder, 'SyncToHead')
    self.PatchObject(
        chrome_on_cros_bisector.ChromeOnCrosBisector,
        'ResolveChromeBisectRangeFromCrosVersion').return_value = True
    self.PatchObject(
        chrome_on_cros_bisector.ChromeOnCrosBisector,
        'SanityCheck').return_value = True
    self.PatchObject(
        chrome_on_cros_bisector.ChromeOnCrosBisector,
        'ObtainBisectBoundaryScore').return_value = True
    self.PatchObject(
        chrome_on_cros_bisector.ChromeOnCrosBisector,
        'GetThresholdFromUser').return_value = True

    self.PatchObject(
        chrome_on_cros_bisector.ChromeOnCrosBisector,
        'ExchangeChromeSanityCheck').return_value = True

    self.assertTrue(self.bisector.PrepareBisect())
