| # -*- 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 git_bisector module.""" |
| |
| from __future__ import print_function |
| |
| import copy |
| import os |
| |
| import mock |
| |
| 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 git_bisector |
| from chromite.lib import commandline |
| from chromite.lib import cros_build_lib |
| from chromite.lib import cros_test_lib |
| from chromite.lib import partial_mock |
| from chromite.lib import cros_logging as logging |
| |
| class GitMock(partial_mock.PartialCmdMock): |
| """Mocks git.RunGit. |
| |
| Examples: |
| mock = GitMock('/path/to/git_repository') |
| mock.AddRunGitResult(git_command, output=...) |
| # call git.RunGit(...) |
| """ |
| TARGET = 'chromite.lib.git' |
| ATTRS = ('RunGit', ) |
| DEFAULT_ATTR = 'RunGit' |
| |
| def __init__(self, cwd): |
| """Constructor. |
| |
| Args: |
| cwd: default git repository. |
| """ |
| super(GitMock, self).__init__() |
| self.cwd = cwd |
| |
| def RunGit(self, *args, **kwargs): |
| """Mocked RunGit.""" |
| try: |
| return self._results['RunGit'].LookupResult(args, kwargs=kwargs) |
| except cros_build_lib.RunCommandError as e: |
| # Copy the logic of check from run. |
| if not kwargs.get('check', True): |
| return e.result |
| raise e |
| |
| def AddRunGitResult(self, cmd, cwd=None, returncode=0, output='', error='', |
| kwargs=None, strict=False, side_effect=None): |
| """Adds git command and results.""" |
| cwd = self.cwd if cwd is None else cwd |
| result = cros_build_lib.CommandResult( |
| returncode=returncode, output=output, error=error) |
| if returncode != 0 and not side_effect: |
| side_effect = cros_build_lib.RunCommandError('non-zero returncode', |
| result) |
| self._results['RunGit'].AddResultForParams( |
| (cwd, cmd,), result, kwargs=kwargs, side_effect=side_effect, |
| strict=strict) |
| |
| |
| class TestGitBisector(cros_test_lib.MockTempDirTestCase): |
| """Tests GitBisector 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) |
| |
| 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 |
| |
| 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, |
| eval_raise_on_error=False, skip_failed_commit=False) |
| |
| self.evaluator = evaluator_module.Evaluator(self.options) |
| self.builder = builder_module.Builder(self.options) |
| self.bisector = git_bisector.GitBisector(self.options, self.builder, |
| self.evaluator) |
| self.repo_dir = os.path.join(self.tempdir, |
| builder_module.Builder.DEFAULT_REPO_DIR) |
| |
| 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 GitBisector() to expect default data members.""" |
| self.assertEqual(self.GOOD_COMMIT_SHA1, self.bisector.good_commit) |
| self.assertEqual(self.BAD_COMMIT_SHA1, self.bisector.bad_commit) |
| 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 GitBisector raises error when missing required arguments.""" |
| options = cros_test_lib.EasyAttr() |
| with self.assertRaises(common.MissingRequiredOptionsException) as cm: |
| git_bisector.GitBisector(options, self.builder, self.evaluator) |
| exception_message = str(cm.exception) |
| self.assertIn('Missing command line', exception_message) |
| self.assertIn('GitBisector', exception_message) |
| for arg in git_bisector.GitBisector.REQUIRED_ARGS: |
| self.assertIn(arg, exception_message) |
| |
| def testCheckCommitFormat(self): |
| """Tests CheckCommitFormat().""" |
| sha1 = '900d900d' |
| self.assertEqual(sha1, git_bisector.GitBisector.CheckCommitFormat(sha1)) |
| not_sha1 = 'bad_sha1' |
| self.assertIsNone(git_bisector.GitBisector.CheckCommitFormat(not_sha1)) |
| |
| def testSetUp(self): |
| """Tests that SetUp() calls builder.SetUp().""" |
| with mock.patch.object(builder_module.Builder, 'SetUp') as builder_mock: |
| self.bisector.SetUp() |
| builder_mock.assert_called_with() |
| |
| def testGit(self): |
| """Tests that Git() invokes git.RunGit() as expected.""" |
| git_mock = self.StartPatcher(GitMock(self.repo_dir)) |
| git_mock.AddRunGitResult(['ls']) |
| self.bisector.Git(['ls']) |
| |
| def testUpdateCurrentCommit(self): |
| """Tests that UpdateCurrentCommit() updates self.current_commit.""" |
| git_mock = self.StartPatcher(GitMock(self.repo_dir)) |
| git_mock.AddRunGitResult( |
| partial_mock.In('show'), |
| output='4fcbdaf6 1493010050 Add "cros bisect" command.\n') |
| self.bisector.UpdateCurrentCommit() |
| current_commit = self.bisector.current_commit |
| self.assertEqual('4fcbdaf6', current_commit.sha1) |
| self.assertEqual(1493010050, current_commit.timestamp) |
| self.assertEqual('Add "cros bisect" command.', current_commit.title) |
| |
| def testUpdateCurrentCommitFail(self): |
| """Tests UpdateCurrentCommit() when 'git show' returns nonzero.""" |
| git_mock = self.StartPatcher(GitMock(self.repo_dir)) |
| git_mock.AddRunGitResult(partial_mock.In('show'), returncode=128) |
| self.bisector.UpdateCurrentCommit() |
| self.assertTrue(not self.bisector.current_commit) |
| |
| def testUpdateCurrentCommitFailUnexpectedOutput(self): |
| """Tests UpdateCurrentCommit() when 'git show' gives unexpected output.""" |
| git_mock = self.StartPatcher(GitMock(self.repo_dir)) |
| git_mock.AddRunGitResult(partial_mock.In('show'), output='Not expected') |
| self.bisector.UpdateCurrentCommit() |
| self.assertTrue(not self.bisector.current_commit) |
| |
| def MockOutBuildDeployEvaluateForSanityCheck(self): |
| """Mocks out BuildDeployEvaluate behavior for SaintyCheck test. |
| |
| It mocks UpdateCurrentCommit() to emit good and bad commits. And mocks |
| CheckLastEvaluate() to return good and bad commit score. |
| """ |
| commit_info_list = [ |
| common.CommitInfo(sha1=self.GOOD_COMMIT_SHA1, title='good', |
| timestamp=self.GOOD_COMMIT_TIMESTAMP), |
| common.CommitInfo(sha1=self.BAD_COMMIT_SHA1, title='bad', |
| timestamp=self.BAD_COMMIT_TIMESTAMP)] |
| # This mock is problematic. The side effect should modify "self" when |
| # the member method is called. |
| def UpdateCurrentCommitSideEffect(): |
| self.bisector.current_commit = commit_info_list.pop(0) |
| self.PatchObject( |
| git_bisector.GitBisector, 'UpdateCurrentCommit', |
| side_effect=UpdateCurrentCommitSideEffect) |
| |
| self.PatchObject( |
| evaluator_module.Evaluator, 'CheckLastEvaluate', |
| side_effect=[self.GOOD_COMMIT_SCORE, self.BAD_COMMIT_SCORE]) |
| |
| def testGetCommitTimestamp(self): |
| """Tests GetCommitTimeStamp by mocking git.RunGit.""" |
| git_mock = self.StartPatcher(GitMock(self.repo_dir)) |
| |
| # Mock git result for GetCommitTimestamp. |
| git_mock.AddRunGitResult( |
| partial_mock.InOrder(['show', self.GOOD_COMMIT_SHA1]), |
| output=str(self.GOOD_COMMIT_TIMESTAMP)) |
| |
| self.assertEqual(self.GOOD_COMMIT_TIMESTAMP, |
| self.bisector.GetCommitTimestamp(self.GOOD_COMMIT_SHA1)) |
| |
| def testGetCommitTimestampNotFound(self): |
| """Tests GetCommitTimeStamp when the commit is not found.""" |
| git_mock = self.StartPatcher(GitMock(self.repo_dir)) |
| nonexist_sha1 = '12345' |
| # Mock git result for GetCommitTimestamp. |
| git_mock.AddRunGitResult( |
| partial_mock.InOrder(['show', nonexist_sha1]), |
| output='commit does not exist') |
| |
| self.assertIsNone(self.bisector.GetCommitTimestamp(nonexist_sha1)) |
| |
| def testSanityCheck(self): |
| """Tests SanityCheck(). |
| |
| It tests by mocking out git commands called by SanityCheck(). |
| """ |
| git_mock = self.StartPatcher(GitMock(self.repo_dir)) |
| # Mock git result for DoesCommitExistInRepo. |
| git_mock.AddRunGitResult( |
| partial_mock.InOrder(['rev-list', self.GOOD_COMMIT_SHA1])) |
| git_mock.AddRunGitResult( |
| partial_mock.InOrder(['rev-list', self.BAD_COMMIT_SHA1])) |
| |
| # Mock git result for GetCommitTimestamp. |
| 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)) |
| |
| self.assertTrue(self.bisector.SanityCheck()) |
| |
| def testSanityCheckGoodCommitNotExist(self): |
| """Tests SanityCheck() that good commit does not exist.""" |
| git_mock = self.StartPatcher(GitMock(self.repo_dir)) |
| sync_to_head_mock = self.PatchObject(builder_module.Builder, 'SyncToHead') |
| # Mock git result for DoesCommitExistInRepo to return False. |
| git_mock.AddRunGitResult( |
| partial_mock.InOrder(['rev-list', self.GOOD_COMMIT_SHA1]), |
| returncode=128) |
| |
| # Mock invalid result for GetCommitTimestamp to return None. |
| git_mock.AddRunGitResult( |
| partial_mock.InOrder(['show', self.GOOD_COMMIT_SHA1]), |
| output='invalid commit') |
| git_mock.AddRunGitResult( |
| partial_mock.InOrder(['show', self.BAD_COMMIT_SHA1]), |
| output='invalid commit') |
| |
| self.assertFalse(self.bisector.SanityCheck()) |
| |
| # SyncToHead() called because DoesCommitExistInRepo() returns False for |
| # good commit. |
| sync_to_head_mock.assert_called() |
| |
| def testSanityCheckSyncToHeadWorks(self): |
| """Tests SanityCheck() that good and bad commit do not exist. |
| |
| As good and bad commit do not exist, it calls SyncToHead(). |
| """ |
| git_mock = self.StartPatcher(GitMock(self.repo_dir)) |
| sync_to_head_mock = self.PatchObject(builder_module.Builder, 'SyncToHead') |
| # Mock git result for DoesCommitExistInRepo to return False. |
| git_mock.AddRunGitResult( |
| partial_mock.InOrder(['rev-list', self.GOOD_COMMIT_SHA1]), |
| returncode=128) |
| |
| # Mock git result for GetCommitTimestamp. |
| 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)) |
| |
| self.assertTrue(self.bisector.SanityCheck()) |
| |
| # After SyncToHead, found both bad and good commit. |
| sync_to_head_mock.assert_called() |
| |
| def testSanityCheckWrongTimeOrder(self): |
| """Tests SanityCheck() that good and bad commit have wrong time order.""" |
| git_mock = self.StartPatcher(GitMock(self.repo_dir)) |
| # Mock git result for DoesCommitExistInRepo. |
| git_mock.AddRunGitResult( |
| partial_mock.InOrder(['rev-list', self.GOOD_COMMIT_SHA1])) |
| git_mock.AddRunGitResult( |
| partial_mock.InOrder(['rev-list', self.BAD_COMMIT_SHA1])) |
| |
| # Mock git result for GetCommitTimestamp, but swap timestamp. |
| git_mock.AddRunGitResult( |
| partial_mock.InOrder(['show', self.GOOD_COMMIT_SHA1]), |
| output=str(self.BAD_COMMIT_TIMESTAMP)) |
| git_mock.AddRunGitResult( |
| partial_mock.InOrder(['show', self.BAD_COMMIT_SHA1]), |
| output=str(self.GOOD_COMMIT_TIMESTAMP)) |
| |
| self.assertFalse(self.bisector.SanityCheck()) |
| |
| def testObtainBisectBoundaryScoreImpl(self): |
| """Tests ObtainBisectBoundaryScoreImpl().""" |
| git_mock = self.StartPatcher(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( |
| git_bisector.GitBisector, 'BuildDeployEval', |
| 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(self.repo_dir, ['checkout', self.GOOD_COMMIT_SHA1], |
| check=False), |
| mock.call(self.repo_dir, ['checkout', self.BAD_COMMIT_SHA1], |
| check=False)], |
| git_mock.call_args_list) |
| build_deploy_eval_mock.assert_called() |
| |
| def testObtainBisectBoundaryScore(self): |
| """Tests ObtainBisectBoundaryScore(). Normal case.""" |
| |
| def MockedObtainBisectBoundaryScoreImpl(good_side): |
| if good_side: |
| self.bisector.current_commit = copy.deepcopy(self.GOOD_COMMIT_INFO) |
| else: |
| self.bisector.current_commit = copy.deepcopy(self.BAD_COMMIT_INFO) |
| return self.bisector.current_commit.score |
| |
| obtain_score_mock = self.PatchObject( |
| git_bisector.GitBisector, 'ObtainBisectBoundaryScoreImpl', |
| side_effect=MockedObtainBisectBoundaryScoreImpl) |
| |
| self.assertTrue(self.bisector.ObtainBisectBoundaryScore()) |
| self.assertEqual(self.GOOD_COMMIT_SCORE, |
| self.bisector.good_commit_info.score) |
| self.assertEqual('last-known-good ', self.bisector.good_commit_info.label) |
| self.assertEqual(self.BAD_COMMIT_SCORE, self.bisector.bad_commit_info.score) |
| self.assertEqual('last-known-bad ', self.bisector.bad_commit_info.label) |
| obtain_score_mock.assert_called() |
| |
| def testObtainBisectBoundaryScoreBadScoreUnavailable(self): |
| """Tests ObtainBisectBoundaryScore(). Bad score unavailable.""" |
| |
| def UpdateCurrentCommitSideEffect(good_side): |
| if good_side: |
| self.bisector.current_commit = copy.deepcopy(self.GOOD_COMMIT_INFO) |
| else: |
| self.bisector.current_commit = copy.deepcopy(self.BAD_COMMIT_INFO) |
| self.bisector.current_commit.score = None |
| return self.bisector.current_commit.score |
| |
| obtain_score_mock = self.PatchObject( |
| git_bisector.GitBisector, 'ObtainBisectBoundaryScoreImpl', |
| side_effect=UpdateCurrentCommitSideEffect) |
| |
| self.assertFalse(self.bisector.ObtainBisectBoundaryScore()) |
| self.assertEqual(self.GOOD_COMMIT_SCORE, |
| self.bisector.good_commit_info.score) |
| self.assertEqual('last-known-good ', self.bisector.good_commit_info.label) |
| self.assertIsNone(self.bisector.bad_commit_info.score) |
| self.assertEqual('last-known-bad ', self.bisector.bad_commit_info.label) |
| obtain_score_mock.assert_called() |
| |
| def testGetThresholdFromUser(self): |
| """Tests GetThresholdFromUser().""" |
| logging.notice('testGetThresholdFromUser') |
| self.setDefaultCommitInfo() |
| input_mock = self.PatchObject(cros_build_lib, 'GetInput', |
| return_value=self.THRESHOLD_SPLITTER) |
| self.assertTrue(self.bisector.GetThresholdFromUser()) |
| self.assertEqual(self.THRESHOLD, self.bisector.threshold) |
| input_mock.assert_called() |
| |
| def testGetThresholdFromUserAutoPick(self): |
| """Tests GetThresholdFromUser().""" |
| self.setDefaultCommitInfo() |
| self.bisector.auto_threshold = True |
| |
| self.assertTrue(self.bisector.GetThresholdFromUser()) |
| self.assertEqual(10, self.bisector.threshold) |
| |
| def testGetThresholdFromUserOutOfBoundFail(self): |
| """Tests GetThresholdFromUser() with out-of-bound input.""" |
| self.setDefaultCommitInfo() |
| input_mock = self.PatchObject(cros_build_lib, 'GetInput', |
| side_effect=['0', '1000', '-10']) |
| self.assertFalse(self.bisector.GetThresholdFromUser()) |
| self.assertIsNone(self.bisector.threshold) |
| self.assertEqual(3, input_mock.call_count) |
| |
| def testGetThresholdFromUserRetrySuccess(self): |
| """Tests GetThresholdFromUser() with retry.""" |
| self.setDefaultCommitInfo() |
| input_mock = self.PatchObject( |
| cros_build_lib, 'GetInput', |
| side_effect=['not_a_number', '1000', self.THRESHOLD_SPLITTER]) |
| self.assertTrue(self.bisector.GetThresholdFromUser()) |
| self.assertEqual(self.THRESHOLD, self.bisector.threshold) |
| self.assertEqual(3, input_mock.call_count) |
| |
| def testBuildDeploy(self): |
| """Tests BuildDeploy().""" |
| # Inject this as UpdateCurrentCommit's side effect. |
| self.bisector.current_commit = copy.deepcopy(self.GOOD_COMMIT_INFO) |
| |
| build_to_deploy = '/build/to/deploy' |
| build_mock = self.PatchObject(builder_module.Builder, 'Build', |
| return_value=build_to_deploy) |
| deploy_mock = self.PatchObject(builder_module.Builder, 'Deploy') |
| |
| self.assertTrue(self.bisector.BuildDeploy()) |
| |
| build_label = self.GOOD_COMMIT_INFO.sha1 |
| build_mock.assert_called_with(build_label) |
| deploy_mock.assert_called_with(self.DUT, build_to_deploy, build_label) |
| |
| def testBuildDeployBuildFail(self): |
| """Tests BuildDeploy() with Build() failure.""" |
| # Inject this as UpdateCurrentCommit's side effect. |
| self.bisector.current_commit = copy.deepcopy(self.GOOD_COMMIT_INFO) |
| |
| # Build() failed. |
| build_mock = self.PatchObject(builder_module.Builder, 'Build', |
| return_value=None) |
| deploy_mock = self.PatchObject(builder_module.Builder, 'Deploy') |
| |
| self.assertFalse(self.bisector.BuildDeploy()) |
| build_mock.assert_called() |
| deploy_mock.assert_not_called() |
| |
| def PatchObjectForBuildDeployEval(self): |
| """Returns a dict of patch objects. |
| |
| The patch objects are to mock: |
| git_bisector.UpdateCurrentCommit() |
| evaluator.CheckLastEvaluate() |
| git_bisector.BuildDeploy() |
| evaluator.Evaluate() |
| """ |
| return { |
| 'UpdateCurrentCommit': self.PatchObject(git_bisector.GitBisector, |
| 'UpdateCurrentCommit'), |
| 'CheckLastEvaluate': self.PatchObject(evaluator_module.Evaluator, |
| 'CheckLastEvaluate'), |
| 'BuildDeploy': self.PatchObject(git_bisector.GitBisector, 'BuildDeploy', |
| return_value=True), |
| 'Evaluate': self.PatchObject(evaluator_module.Evaluator, 'Evaluate')} |
| |
| def testBuildDeployEvalShortcutCheckLastEvaluate(self): |
| """Tests BuildDeployEval() with CheckLastEvaluate() found last score.""" |
| mocks = self.PatchObjectForBuildDeployEval() |
| |
| # Inject this as UpdateCurrentCommit's side effect. |
| self.bisector.current_commit = copy.deepcopy(self.GOOD_COMMIT_INFO) |
| |
| mocks['CheckLastEvaluate'].return_value = self.GOOD_COMMIT_SCORE |
| |
| score = self.bisector.BuildDeployEval() |
| self.assertEqual(self.GOOD_COMMIT_SCORE, score) |
| self.assertEqual(self.GOOD_COMMIT_SCORE, self.bisector.current_commit.score) |
| for method_called in ['UpdateCurrentCommit', 'CheckLastEvaluate']: |
| mocks[method_called].assert_called() |
| mocks['CheckLastEvaluate'].assert_called_with(self.GOOD_COMMIT_SHA1, |
| self.REPEAT) |
| |
| for method_called in ['BuildDeploy', 'Evaluate']: |
| mocks[method_called].assert_not_called() |
| |
| def AssertBuildDeployEvalMocksAllCalled(self, mocks): |
| for method_called in ['UpdateCurrentCommit', 'CheckLastEvaluate', |
| 'BuildDeploy', 'Evaluate']: |
| mocks[method_called].assert_called() |
| mocks['CheckLastEvaluate'].assert_called_with(self.GOOD_COMMIT_SHA1, |
| self.REPEAT) |
| mocks['Evaluate'].assert_called_with(self.DUT, self.GOOD_COMMIT_SHA1, |
| self.REPEAT) |
| |
| def testBuildDeployEvalNoCheckLastEvaluate(self): |
| """Tests BuildDeployEval() without last score.""" |
| mocks = self.PatchObjectForBuildDeployEval() |
| |
| # Inject this as UpdateCurrentCommit's side effect. |
| self.bisector.current_commit = copy.deepcopy(self.GOOD_COMMIT_INFO) |
| |
| mocks['CheckLastEvaluate'].return_value = common.Score() |
| mocks['Evaluate'].return_value = self.GOOD_COMMIT_SCORE |
| |
| self.assertEqual(self.GOOD_COMMIT_SCORE, self.bisector.BuildDeployEval()) |
| self.assertEqual(self.GOOD_COMMIT_SCORE, self.bisector.current_commit.score) |
| self.AssertBuildDeployEvalMocksAllCalled(mocks) |
| |
| def testBuildDeployEvalBuildFail(self): |
| """Tests BuildDeployEval() with BuildDeploy failure.""" |
| mocks = self.PatchObjectForBuildDeployEval() |
| |
| # Inject this as UpdateCurrentCommit's side effect. |
| self.bisector.current_commit = copy.deepcopy(self.GOOD_COMMIT_INFO) |
| |
| mocks['CheckLastEvaluate'].return_value = common.Score() |
| mocks['BuildDeploy'].return_value = False |
| |
| score = self.bisector.BuildDeployEval() |
| self.assertFalse(score) |
| self.assertFalse(self.bisector.current_commit.score) |
| |
| for method_called in ['UpdateCurrentCommit', 'CheckLastEvaluate', |
| 'BuildDeploy']: |
| mocks[method_called].assert_called() |
| mocks['CheckLastEvaluate'].assert_called_with(self.GOOD_COMMIT_SHA1, |
| self.REPEAT) |
| |
| mocks['Evaluate'].assert_not_called() |
| |
| def testBuildDeployEvalNoCheckLastEvaluateSpecifyEvalLabel(self): |
| """Tests BuildDeployEval() with eval_label specified.""" |
| mocks = self.PatchObjectForBuildDeployEval() |
| |
| # Inject this as UpdateCurrentCommit's side effect. |
| self.bisector.current_commit = copy.deepcopy(self.GOOD_COMMIT_INFO) |
| |
| mocks['CheckLastEvaluate'].return_value = common.Score() |
| mocks['Evaluate'].return_value = self.GOOD_COMMIT_SCORE |
| |
| eval_label = 'customized_label' |
| self.assertEqual(self.GOOD_COMMIT_SCORE, |
| self.bisector.BuildDeployEval(eval_label=eval_label)) |
| self.assertEqual(self.GOOD_COMMIT_SCORE, self.bisector.current_commit.score) |
| |
| for method_called in ['UpdateCurrentCommit', 'CheckLastEvaluate', |
| 'BuildDeploy', 'Evaluate']: |
| mocks[method_called].assert_called() |
| # Use given label instead of SHA1 as eval label. |
| mocks['CheckLastEvaluate'].assert_called_with(eval_label, self.REPEAT) |
| mocks['Evaluate'].assert_called_with(self.DUT, eval_label, self.REPEAT) |
| |
| @staticmethod |
| def _DummyMethod(): |
| """A dummy method for test to call and mock.""" |
| |
| def testBuildDeployEvalNoCheckLastEvaluateSpecifyBuildDeploy(self): |
| """Tests BuildDeployEval() with customize_build_deploy specified.""" |
| mocks = self.PatchObjectForBuildDeployEval() |
| |
| # Inject this as UpdateCurrentCommit's side effect. |
| self.bisector.current_commit = copy.deepcopy(self.GOOD_COMMIT_INFO) |
| |
| mocks['CheckLastEvaluate'].return_value = common.Score() |
| mocks['Evaluate'].return_value = self.GOOD_COMMIT_SCORE |
| |
| dummy_method = self.PatchObject( |
| TestGitBisector, '_DummyMethod', return_value=True) |
| |
| eval_label = 'customized_label' |
| self.assertEqual(self.GOOD_COMMIT_SCORE, |
| self.bisector.BuildDeployEval( |
| eval_label=eval_label, |
| customize_build_deploy=TestGitBisector._DummyMethod)) |
| self.assertEqual(self.GOOD_COMMIT_SCORE, self.bisector.current_commit.score) |
| |
| for method_called in ['UpdateCurrentCommit', 'CheckLastEvaluate', |
| 'Evaluate']: |
| mocks[method_called].assert_called() |
| mocks['BuildDeploy'].assert_not_called() |
| dummy_method.assert_called() |
| # Use given label instead of SHA1 as eval label. |
| mocks['CheckLastEvaluate'].assert_called_with(eval_label, self.REPEAT) |
| mocks['Evaluate'].assert_called_with(self.DUT, eval_label, self.REPEAT) |
| |
| def testBuildDeployEvalRaiseNoScore(self): |
| """Tests BuildDeployEval() without score with eval_raise_on_error set.""" |
| self.options.eval_raise_on_error = True |
| self.bisector = git_bisector.GitBisector(self.options, self.builder, |
| self.evaluator) |
| |
| mocks = self.PatchObjectForBuildDeployEval() |
| |
| # Inject this as UpdateCurrentCommit's side effect. |
| self.bisector.current_commit = copy.deepcopy(self.GOOD_COMMIT_INFO) |
| |
| mocks['CheckLastEvaluate'].return_value = common.Score() |
| mocks['Evaluate'].return_value = common.Score() |
| |
| self.assertRaises(Exception, self.bisector.BuildDeployEval) |
| self.assertFalse(self.bisector.current_commit.score) |
| self.AssertBuildDeployEvalMocksAllCalled(mocks) |
| |
| def testBuildDeployEvalSuppressRaiseNoScore(self): |
| """Tests BuildDeployEval() without score with eval_raise_on_error unset.""" |
| mocks = self.PatchObjectForBuildDeployEval() |
| |
| # Inject this as UpdateCurrentCommit's side effect. |
| self.bisector.current_commit = copy.deepcopy(self.GOOD_COMMIT_INFO) |
| |
| mocks['CheckLastEvaluate'].return_value = common.Score() |
| mocks['Evaluate'].return_value = common.Score() |
| |
| self.assertFalse(self.bisector.BuildDeployEval()) |
| self.assertFalse(self.bisector.current_commit.score) |
| self.AssertBuildDeployEvalMocksAllCalled(mocks) |
| |
| def testLabelBuild(self): |
| """Tests LabelBuild().""" |
| # Inject good(100), bad(80) score and threshold. |
| self.setDefaultCommitInfo() |
| self.bisector.threshold = self.THRESHOLD |
| good = 'good' |
| bad = 'bad' |
| |
| # Worse than given bad score. |
| self.assertEqual(bad, self.bisector.LabelBuild(common.Score([70]))) |
| |
| # Better than bad score, but not good enough. |
| self.assertEqual(bad, self.bisector.LabelBuild(common.Score([85]))) |
| self.assertEqual(bad, self.bisector.LabelBuild(common.Score([90]))) |
| |
| # On the margin. |
| self.assertEqual(good, self.bisector.LabelBuild(common.Score([95]))) |
| |
| # Better than the margin. |
| self.assertEqual(good, self.bisector.LabelBuild(common.Score([98]))) |
| |
| # Better than given good score. |
| self.assertEqual(good, self.bisector.LabelBuild(common.Score([110]))) |
| |
| # No score, default bad. |
| self.assertEqual(bad, self.bisector.LabelBuild(None)) |
| self.assertEqual(bad, self.bisector.LabelBuild(common.Score())) |
| |
| def testLabelBuildSkipNoScore(self): |
| """Tests LabelBuild().""" |
| self.options.skip_failed_commit = True |
| self.bisector = git_bisector.GitBisector(self.options, self.builder, |
| self.evaluator) |
| |
| # Inject good(100), bad(80) score and threshold. |
| self.setDefaultCommitInfo() |
| self.bisector.threshold = self.THRESHOLD |
| |
| # No score, skip. |
| self.assertEqual('skip', self.bisector.LabelBuild(None)) |
| self.assertEqual('skip', self.bisector.LabelBuild(common.Score())) |
| |
| |
| def testLabelBuildLowerIsBetter(self): |
| """Tests LabelBuild() in lower-is-better condition.""" |
| # Reverse good(80) and bad(100) score (lower is better), same threshold. |
| self.bisector.good_commit_info = copy.deepcopy(self.BAD_COMMIT_INFO) |
| self.bisector.bad_commit_info = copy.deepcopy(self.GOOD_COMMIT_INFO) |
| self.bisector.threshold = self.THRESHOLD |
| good = 'good' |
| bad = 'bad' |
| |
| # Better than given good score. |
| self.assertEqual(good, self.bisector.LabelBuild(common.Score([70]))) |
| |
| # Worse than good score, but still better than margin. |
| self.assertEqual(good, self.bisector.LabelBuild(common.Score([80]))) |
| self.assertEqual(good, self.bisector.LabelBuild(common.Score([82]))) |
| |
| # On the margin. |
| self.assertEqual(good, self.bisector.LabelBuild(common.Score([85]))) |
| |
| # Worse than the margin. |
| self.assertEqual(bad, self.bisector.LabelBuild(common.Score([88]))) |
| self.assertEqual(bad, self.bisector.LabelBuild(common.Score([90]))) |
| self.assertEqual(bad, self.bisector.LabelBuild(common.Score([95]))) |
| |
| # Worse than given bad score. |
| self.assertEqual(bad, self.bisector.LabelBuild(common.Score([110]))) |
| |
| def testGitBisect(self): |
| """Tests GitBisect().""" |
| git_mock = self.PatchObject( |
| git_bisector.GitBisector, 'Git', |
| return_value=cros_build_lib.CommandResult( |
| cmd=['git', 'bisect', 'reset'], output='We are not bisecting.', |
| returncode=0)) |
| |
| result, done = self.bisector.GitBisect(['reset']) |
| git_mock.assert_called_with(['bisect', 'reset']) |
| self.assertFalse(done) |
| self.assertEqual('We are not bisecting.', result.output) |
| self.assertListEqual(['git', 'bisect', 'reset'], result.cmd) |
| |
| def testGitBisectDone(self): |
| """Tests GitBisect() when culprit is found.""" |
| git_mock = self.PatchObject( |
| git_bisector.GitBisector, 'Git', |
| return_value=cros_build_lib.CommandResult( |
| cmd=['git', 'bisect', 'bad'], |
| output='abcedf is the first bad commit\ncommit abcdef', |
| returncode=0)) |
| |
| result, done = self.bisector.GitBisect(['bad']) |
| git_mock.assert_called_with(['bisect', 'bad']) |
| self.assertTrue(done) |
| self.assertListEqual(['git', 'bisect', 'bad'], result.cmd) |
| |
| def testRun(self): |
| """Tests Run().""" |
| bisector_mock = self.StartPatcher(GitBisectorMock()) |
| bisector_mock.good_commit_info = copy.deepcopy(self.GOOD_COMMIT_INFO) |
| bisector_mock.bad_commit_info = copy.deepcopy(self.BAD_COMMIT_INFO) |
| bisector_mock.threshold = self.THRESHOLD |
| bisector_mock.git_bisect_args_result = [ |
| (['reset'], (None, False)), |
| (['start'], (None, False)), |
| (['bad', self.BAD_COMMIT_SHA1], (None, False)), |
| (['good', self.GOOD_COMMIT_SHA1], (None, False)), |
| (['bad'], |
| (cros_build_lib.CommandResult( |
| cmd=['git', 'bisect', 'bad'], |
| output='%s is the first bad commit\ncommit %s' % ( |
| self.CULPRIT_COMMIT_SHA1, self.CULPRIT_COMMIT_SHA1), |
| returncode=0), |
| True)), # bisect bad (assume it found the first bad commit). |
| (['log'], (None, False)), |
| (['reset'], (None, False))] |
| bisector_mock.build_deploy_eval_current_commit = [self.CULPRIT_COMMIT_INFO] |
| bisector_mock.build_deploy_eval_result = [self.CULPRIT_COMMIT_SCORE] |
| bisector_mock.label_build_result = ['bad'] |
| |
| run_result = self.bisector.Run() |
| |
| self.assertTrue(bisector_mock.patched['PrepareBisect'].called) |
| self.assertEqual(7, bisector_mock.patched['GitBisect'].call_count) |
| self.assertTrue(bisector_mock.patched['BuildDeployEval'].called) |
| self.assertTrue(bisector_mock.patched['LabelBuild'].called) |
| self.assertEqual(self.CULPRIT_COMMIT_SHA1, run_result) |
| |
| |
| class GitBisectorMock(partial_mock.PartialMock): |
| """Partial mock GitBisector to test GitBisector.Run().""" |
| |
| TARGET = 'chromite.cros_bisect.git_bisector.GitBisector' |
| ATTRS = ('PrepareBisect', 'GitBisect', 'BuildDeployEval', 'LabelBuild') |
| |
| def __init__(self): |
| super(GitBisectorMock, self).__init__() |
| self.good_commit_info = None |
| self.bad_commit_info = None |
| self.threshold = None |
| self.git_bisect_args_result = [] |
| self.build_deploy_eval_current_commit = [] |
| self.build_deploy_eval_result = [] |
| self.label_build_result = None |
| |
| def PrepareBisect(self, this): |
| this.good_commit_info = self.good_commit_info |
| this.bad_commit_info = self.bad_commit_info |
| this.threshold = self.threshold |
| return True |
| |
| def GitBisect(self, _, bisect_op): |
| (expected_op, result) = self.git_bisect_args_result.pop(0) |
| assert expected_op == bisect_op |
| return result |
| |
| def BuildDeployEval(self, this): |
| this.current_commit = self.build_deploy_eval_current_commit.pop(0) |
| return self.build_deploy_eval_result.pop(0) |
| |
| def LabelBuild(self, _, unused_score): |
| return self.label_build_result.pop(0) |