| # -*- coding: utf-8 -*- |
| # Copyright 2014 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. |
| |
| """Unit tests for clactions methods.""" |
| |
| from __future__ import print_function |
| |
| import datetime |
| import itertools |
| import random |
| |
| from chromite.lib.const import waterfall |
| from chromite.lib import constants |
| from chromite.lib import metadata_lib |
| from chromite.cbuildbot import validation_pool |
| from chromite.lib import fake_cidb |
| from chromite.lib import clactions |
| from chromite.lib import cros_test_lib |
| |
| |
| class CLActionTest(cros_test_lib.TestCase): |
| """Placeholder for clactions unit tests.""" |
| |
| def runTest(self): |
| pass |
| |
| |
| class TestCLActionHistory(cros_test_lib.TestCase): |
| """Tests various methods related to CL action history.""" |
| |
| def setUp(self): |
| self.fake_db = fake_cidb.FakeCIDBConnection() |
| |
| def testGetCLHandlingTime(self): |
| """Test that we correctly compute a CL's handling time.""" |
| change = metadata_lib.GerritPatchTuple(1, 1, False) |
| launcher_id = self.fake_db.InsertBuild( |
| 'launcher', waterfall.WATERFALL_INTERNAL, 1, |
| constants.PRE_CQ_LAUNCHER_CONFIG, 'hostname') |
| trybot_id = self.fake_db.InsertBuild( |
| 'banana pre cq', waterfall.WATERFALL_INTERNAL, 1, |
| 'banana-pre-cq', 'hostname') |
| master_id = self.fake_db.InsertBuild( |
| 'CQ master', waterfall.WATERFALL_INTERNAL, 1, |
| constants.CQ_MASTER, 'hostname') |
| slave_id = self.fake_db.InsertBuild( |
| 'banana paladin', waterfall.WATERFALL_INTERNAL, 1, |
| 'banana-paladin', 'hostname') |
| |
| start_time = datetime.datetime.now() |
| c = itertools.count() |
| |
| def next_time(): |
| return start_time + datetime.timedelta(seconds=c.next()) |
| |
| def a(build_id, action, reason=None): |
| self._Act(build_id, change, action, reason=reason, timestamp=next_time()) |
| |
| strategies = {} |
| |
| # Change is screened, picked up, and rejected by the pre-cq, |
| # non-speculatively. |
| a(launcher_id, constants.CL_ACTION_VALIDATION_PENDING_PRE_CQ, |
| reason='banana-pre-cq') |
| a(launcher_id, constants.CL_ACTION_SCREENED_FOR_PRE_CQ) |
| a(launcher_id, constants.CL_ACTION_TRYBOT_LAUNCHING, |
| reason='banana-pre-cq') |
| a(trybot_id, constants.CL_ACTION_PICKED_UP) |
| a(trybot_id, constants.CL_ACTION_KICKED_OUT) |
| |
| # Change is re-marked by developer, picked up again by pre-cq, verified, and |
| # marked as passed. |
| a(launcher_id, constants.CL_ACTION_REQUEUED) |
| a(launcher_id, constants.CL_ACTION_TRYBOT_LAUNCHING, |
| reason='banana-pre-cq') |
| a(trybot_id, constants.CL_ACTION_PICKED_UP) |
| a(trybot_id, constants.CL_ACTION_VERIFIED) |
| a(launcher_id, constants.CL_ACTION_PRE_CQ_FULLY_VERIFIED) |
| a(launcher_id, constants.CL_ACTION_PRE_CQ_PASSED) |
| |
| # Change is picked up by the CQ and rejected. |
| a(master_id, constants.CL_ACTION_PICKED_UP) |
| a(slave_id, constants.CL_ACTION_PICKED_UP) |
| a(master_id, constants.CL_ACTION_KICKED_OUT) |
| |
| # Change is re-marked, picked up by the CQ, and forgiven. |
| a(launcher_id, constants.CL_ACTION_REQUEUED) |
| a(master_id, constants.CL_ACTION_PICKED_UP) |
| a(slave_id, constants.CL_ACTION_PICKED_UP) |
| a(master_id, constants.CL_ACTION_FORGIVEN) |
| |
| # Change is re-marked, picked up by the CQ, and forgiven. |
| a(master_id, constants.CL_ACTION_PICKED_UP) |
| a(slave_id, constants.CL_ACTION_PICKED_UP) |
| a(master_id, constants.CL_ACTION_SUBMITTED) |
| strategies[change] = constants.STRATEGY_CQ_SUCCESS |
| |
| action_history = self.fake_db.GetActionsForChanges([change]) |
| # Note: There are 2 ticks in the total handling time that are not accounted |
| # for in the sub-times. These are the time between VALIDATION_PENDING and |
| # SCREENED, and the time between FULLY_VERIFIED and PASSED. |
| self.assertEqual(18, clactions.GetCLHandlingTime(change, action_history)) |
| self.assertEqual(7, clactions.GetPreCQTime(change, action_history)) |
| self.assertEqual(3, clactions.GetCQWaitTime(change, action_history)) |
| self.assertEqual(6, clactions.GetCQRunTime(change, action_history)) |
| self.assertEqual(3, clactions.GetCQAttemptsCount(change, action_history)) |
| |
| def _Act(self, build_id, change, action, reason=None, timestamp=None): |
| self.fake_db.InsertCLActions( |
| build_id, |
| [clactions.CLAction.FromGerritPatchAndAction(change, action, reason)], |
| timestamp=timestamp) |
| |
| def _GetCLStatus(self, change): |
| """Helper method to get a CL's pre-CQ status from fake_db.""" |
| action_history = self.fake_db.GetActionsForChanges([change]) |
| return clactions.GetCLPreCQStatus(change, action_history) |
| |
| def testGetOldPreCQBuildActions(self): |
| """Test GetOldPreCQBuildActions.""" |
| c1 = metadata_lib.GerritPatchTuple(1, 1, False) |
| c2 = metadata_lib.GerritPatchTuple(1, 2, False) |
| changes = [c1, c2] |
| |
| build_id = self.fake_db.InsertBuild('n', 'w', 1, 'c', 'h') |
| |
| a1 = clactions.CLAction.FromGerritPatchAndAction( |
| c1, constants.CL_ACTION_TRYBOT_LAUNCHING, |
| reason='binhost-pre-cq', |
| timestamp=datetime.datetime.now() - datetime.timedelta(hours=5)) |
| a2 = clactions.CLAction.FromGerritPatchAndAction( |
| c1, constants.CL_ACTION_TRYBOT_LAUNCHING, |
| reason='binhost-pre-cq', |
| timestamp=datetime.datetime.now() - datetime.timedelta(hours=4), |
| buildbucket_id='1') |
| a3 = clactions.CLAction.FromGerritPatchAndAction( |
| c1, constants.CL_ACTION_TRYBOT_LAUNCHING, |
| reason='binhost-pre-cq', |
| timestamp=datetime.datetime.now() - datetime.timedelta(hours=3), |
| buildbucket_id='2') |
| a4 = clactions.CLAction.FromGerritPatchAndAction( |
| c1, constants.CL_ACTION_TRYBOT_LAUNCHING, |
| reason='pbinhost-pre-cq', |
| timestamp=datetime.datetime.now() - datetime.timedelta(hours=3), |
| buildbucket_id='3') |
| a5 = clactions.CLAction.FromGerritPatchAndAction( |
| c1, constants.CL_ACTION_TRYBOT_CANCELLED, |
| reason='binhost-pre-cq', |
| timestamp=datetime.datetime.now() - datetime.timedelta(hours=3), |
| buildbucket_id='3') |
| a6 = clactions.CLAction.FromGerritPatchAndAction( |
| c1, constants.CL_ACTION_TRYBOT_LAUNCHING, |
| reason='binhost-pre-cq', |
| timestamp=datetime.datetime.now() - datetime.timedelta(hours=1), |
| buildbucket_id='4') |
| a7 = clactions.CLAction.FromGerritPatchAndAction( |
| c2, constants.CL_ACTION_TRYBOT_LAUNCHING, |
| reason='binhost-pre-cq', |
| timestamp=datetime.datetime.now(), |
| buildbucket_id='5') |
| |
| cl_actions = [a1, a2, a3, a4, a5, a6, a7] |
| |
| self.fake_db.InsertCLActions(build_id, cl_actions) |
| action_history = self.fake_db.GetActionsForChanges(changes) |
| |
| timestamp = datetime.datetime.now() - datetime.timedelta(hours=2) |
| c1_old_actions = clactions.GetOldPreCQBuildActions( |
| c1, action_history, timestamp) |
| c2_old_actions = clactions.GetOldPreCQBuildActions( |
| c2, action_history, timestamp) |
| self.assertTrue(len(c1_old_actions) == 0) |
| self.assertTrue(len(c2_old_actions) == 1) |
| self.assertEqual([c.buildbucket_id for c in c2_old_actions], |
| [a6.buildbucket_id]) |
| |
| c1_old_actions = clactions.GetOldPreCQBuildActions( |
| c1, action_history) |
| c2_old_actions = clactions.GetOldPreCQBuildActions( |
| c2, action_history) |
| self.assertTrue(len(c1_old_actions) == 0) |
| self.assertTrue(len(c2_old_actions) == 3) |
| self.assertEqual([c.buildbucket_id for c in c2_old_actions], |
| [a2.buildbucket_id, a3.buildbucket_id, |
| a6.buildbucket_id]) |
| |
| def testGetCancelledPreCQBuilds(self): |
| """Test GetCancelledPreCQBuilds.""" |
| c1 = metadata_lib.GerritPatchTuple(1, 1, False) |
| build_id = self.fake_db.InsertBuild('n', 'w', 1, 'c', 'h') |
| a1 = clactions.CLAction.FromGerritPatchAndAction( |
| c1, constants.CL_ACTION_TRYBOT_CANCELLED, |
| reason='binhost-pre-cq') |
| a2 = clactions.CLAction.FromGerritPatchAndAction( |
| c1, constants.CL_ACTION_TRYBOT_LAUNCHING, |
| reason='binhost-pre-cq', |
| buildbucket_id='1') |
| a3 = clactions.CLAction.FromGerritPatchAndAction( |
| c1, constants.CL_ACTION_TRYBOT_CANCELLED, |
| reason='binhost-pre-cq', |
| buildbucket_id='2') |
| cl_actions = [a1, a2, a3] |
| self.fake_db.InsertCLActions(build_id, cl_actions) |
| action_history = self.fake_db.GetActionsForChanges([c1]) |
| builds = clactions.GetCancelledPreCQBuilds(action_history) |
| self.assertEqual(len(builds), 1) |
| self.assertEqual(builds.pop().buildbucket_id, '2') |
| |
| def testGetRequeuedOrSpeculative(self): |
| """Tests GetRequeuedOrSpeculative function.""" |
| change = metadata_lib.GerritPatchTuple(1, 1, False) |
| speculative_change = metadata_lib.GerritPatchTuple(2, 2, False) |
| changes = [change, speculative_change] |
| |
| build_id = self.fake_db.InsertBuild('n', 'w', 1, 'c', 'h') |
| |
| # A fresh change should not be marked requeued. A fresh specualtive |
| # change should be marked as speculative. |
| action_history = self.fake_db.GetActionsForChanges(changes) |
| a = clactions.GetRequeuedOrSpeculative(change, action_history, False) |
| self.assertEqual(a, None) |
| a = clactions.GetRequeuedOrSpeculative(speculative_change, action_history, |
| True) |
| self.assertEqual(a, constants.CL_ACTION_SPECULATIVE) |
| self._Act(build_id, speculative_change, a) |
| |
| # After picking up either change, neither should need an additional |
| # requeued or speculative action. |
| self._Act(build_id, speculative_change, constants.CL_ACTION_PICKED_UP) |
| self._Act(build_id, change, constants.CL_ACTION_PICKED_UP) |
| action_history = self.fake_db.GetActionsForChanges(changes) |
| a = clactions.GetRequeuedOrSpeculative(change, action_history, False) |
| self.assertEqual(a, None) |
| a = clactions.GetRequeuedOrSpeculative(speculative_change, action_history, |
| True) |
| self.assertEqual(a, None) |
| |
| # After being rejected, both changes need an action (requeued and |
| # speculative accordingly). |
| self._Act(build_id, speculative_change, constants.CL_ACTION_KICKED_OUT) |
| self._Act(build_id, change, constants.CL_ACTION_KICKED_OUT) |
| action_history = self.fake_db.GetActionsForChanges(changes) |
| a = clactions.GetRequeuedOrSpeculative(change, action_history, False) |
| self.assertEqual(a, constants.CL_ACTION_REQUEUED) |
| self._Act(build_id, change, a) |
| a = clactions.GetRequeuedOrSpeculative(speculative_change, action_history, |
| True) |
| self.assertEqual(a, constants.CL_ACTION_SPECULATIVE) |
| self._Act(build_id, speculative_change, a) |
| |
| # Once a speculative change becomes un-speculative, it needs a REQUEUD |
| # action. |
| action_history = self.fake_db.GetActionsForChanges(changes) |
| a = clactions.GetRequeuedOrSpeculative(speculative_change, action_history, |
| False) |
| self.assertEqual(a, constants.CL_ACTION_REQUEUED) |
| self._Act(build_id, speculative_change, a) |
| |
| def testGetCLPreCQStatus(self): |
| change = metadata_lib.GerritPatchTuple(1, 1, False) |
| # Initial pre-CQ status of a change is None. |
| self.assertEqual(self._GetCLStatus(change), None) |
| |
| # Builders can update the CL's pre-CQ status. |
| build_id = self.fake_db.InsertBuild( |
| constants.PRE_CQ_LAUNCHER_NAME, waterfall.WATERFALL_INTERNAL, 1, |
| constants.PRE_CQ_LAUNCHER_CONFIG, 'bot-hostname') |
| |
| self._Act(build_id, change, constants.CL_ACTION_PRE_CQ_WAITING) |
| self.assertEqual(self._GetCLStatus(change), constants.CL_STATUS_WAITING) |
| |
| self._Act(build_id, change, constants.CL_ACTION_PRE_CQ_INFLIGHT) |
| self.assertEqual(self._GetCLStatus(change), constants.CL_STATUS_INFLIGHT) |
| |
| # Recording a cl action that is not a valid pre-cq status should leave |
| # pre-cq status unaffected. |
| self._Act(build_id, change, 'polenta') |
| self.assertEqual(self._GetCLStatus(change), constants.CL_STATUS_INFLIGHT) |
| |
| self._Act(build_id, change, constants.CL_ACTION_PRE_CQ_RESET) |
| self.assertEqual(self._GetCLStatus(change), None) |
| |
| def testGetCLPreCQProgress(self): |
| change = metadata_lib.GerritPatchTuple(1, 1, False) |
| s = lambda: clactions.GetCLPreCQProgress( |
| change, self.fake_db.GetActionsForChanges([change])) |
| |
| self.assertEqual({}, s()) |
| |
| # Simulate the pre-cq-launcher screening changes for pre-cq configs |
| # to test with. |
| launcher_build_id = self.fake_db.InsertBuild( |
| constants.PRE_CQ_LAUNCHER_NAME, waterfall.WATERFALL_INTERNAL, |
| 1, constants.PRE_CQ_LAUNCHER_CONFIG, 'bot hostname 1') |
| |
| self._Act(launcher_build_id, change, |
| constants.CL_ACTION_VALIDATION_PENDING_PRE_CQ, |
| 'pineapple-pre-cq') |
| self._Act(launcher_build_id, change, |
| constants.CL_ACTION_VALIDATION_PENDING_PRE_CQ, |
| 'banana-pre-cq') |
| |
| configs = ['banana-pre-cq', 'pineapple-pre-cq'] |
| |
| self.assertEqual(configs, sorted(s().keys())) |
| for c in configs: |
| self.assertEqual(constants.CL_PRECQ_CONFIG_STATUS_PENDING, |
| s()[c][0]) |
| |
| # Simulate a prior build rejecting change |
| self._Act(launcher_build_id, change, |
| constants.CL_ACTION_KICKED_OUT, |
| 'pineapple-pre-cq') |
| self.assertEqual(constants.CL_PRECQ_CONFIG_STATUS_FAILED, |
| s()['pineapple-pre-cq'][0]) |
| |
| # Simulate the pre-cq-launcher launching tryjobs for all pending configs. |
| for c in configs: |
| self._Act(launcher_build_id, change, |
| constants.CL_ACTION_TRYBOT_LAUNCHING, c) |
| for c in configs: |
| self.assertEqual(constants.CL_PRECQ_CONFIG_STATUS_LAUNCHED, |
| s()[c][0]) |
| |
| # Simulate the tryjobs launching, and picking up the changes. |
| banana_build_id = self.fake_db.InsertBuild( |
| 'banana', waterfall.WATERFALL_SWARMING, 12, 'banana-pre-cq', |
| 'banana hostname') |
| pineapple_build_id = self.fake_db.InsertBuild( |
| 'pineapple', waterfall.WATERFALL_SWARMING, 87, 'pineapple-pre-cq', |
| 'pineapple hostname') |
| |
| self._Act(banana_build_id, change, constants.CL_ACTION_PICKED_UP) |
| self._Act(pineapple_build_id, change, constants.CL_ACTION_PICKED_UP) |
| for c in configs: |
| self.assertEqual(constants.CL_PRECQ_CONFIG_STATUS_INFLIGHT, |
| s()[c][0]) |
| |
| # Simulate the changes being retried. |
| self._Act(banana_build_id, change, constants.CL_ACTION_FORGIVEN) |
| self._Act(launcher_build_id, change, constants.CL_ACTION_FORGIVEN, |
| 'pineapple-pre-cq') |
| for c in configs: |
| self.assertEqual(constants.CL_PRECQ_CONFIG_STATUS_PENDING, |
| s()[c][0]) |
| # Simulate the changes being rejected, either by the configs themselves |
| # or by the pre-cq-launcher. |
| self._Act(banana_build_id, change, constants.CL_ACTION_KICKED_OUT) |
| self._Act(launcher_build_id, change, constants.CL_ACTION_KICKED_OUT, |
| 'pineapple-pre-cq') |
| for c in configs: |
| self.assertEqual(constants.CL_PRECQ_CONFIG_STATUS_FAILED, |
| s()[c][0]) |
| # Simulate the tryjobs verifying the changes. |
| self._Act(banana_build_id, change, constants.CL_ACTION_VERIFIED) |
| self._Act(pineapple_build_id, change, constants.CL_ACTION_VERIFIED) |
| for c in configs: |
| self.assertEqual(constants.CL_PRECQ_CONFIG_STATUS_VERIFIED, |
| s()[c][0]) |
| |
| # Simulate the pre-cq status being reset. |
| self._Act(launcher_build_id, change, constants.CL_ACTION_PRE_CQ_RESET) |
| self.assertEqual({}, s()) |
| |
| def testGetCLPreCQCategoriesAndPendingCLs(self): |
| c1 = metadata_lib.GerritPatchTuple(1, 1, False) |
| c2 = metadata_lib.GerritPatchTuple(2, 2, False) |
| c3 = metadata_lib.GerritPatchTuple(3, 3, False) |
| c4 = metadata_lib.GerritPatchTuple(4, 4, False) |
| c5 = metadata_lib.GerritPatchTuple(5, 5, False) |
| |
| launcher_build_id = self.fake_db.InsertBuild( |
| constants.PRE_CQ_LAUNCHER_NAME, waterfall.WATERFALL_INTERNAL, |
| 1, constants.PRE_CQ_LAUNCHER_CONFIG, 'bot hostname 1') |
| pineapple_build_id = self.fake_db.InsertBuild( |
| 'pineapple', waterfall.WATERFALL_SWARMING, 87, 'pineapple-pre-cq', |
| 'pineapple hostname') |
| guava_build_id = self.fake_db.InsertBuild( |
| 'guava', waterfall.WATERFALL_SWARMING, 7, 'guava-pre-cq', |
| 'guava hostname') |
| |
| # c1 has 3 pending verifications, but only 1 inflight and 1 |
| # launching, so it is not busy/inflight. |
| self._Act(launcher_build_id, c1, |
| constants.CL_ACTION_VALIDATION_PENDING_PRE_CQ, |
| 'pineapple-pre-cq') |
| self._Act(launcher_build_id, c1, |
| constants.CL_ACTION_VALIDATION_PENDING_PRE_CQ, |
| 'banana-pre-cq') |
| self._Act(launcher_build_id, c1, |
| constants.CL_ACTION_VALIDATION_PENDING_PRE_CQ, |
| 'guava-pre-cq') |
| self._Act(launcher_build_id, c1, |
| constants.CL_ACTION_TRYBOT_LAUNCHING, |
| 'banana-pre-cq') |
| self._Act(pineapple_build_id, c1, constants.CL_ACTION_PICKED_UP) |
| |
| # c2 has 3 pending verifications, 1 inflight and 1 launching, and 1 passed, |
| # so it is busy. |
| self._Act(launcher_build_id, c2, |
| constants.CL_ACTION_VALIDATION_PENDING_PRE_CQ, |
| 'pineapple-pre-cq') |
| self._Act(launcher_build_id, c2, |
| constants.CL_ACTION_VALIDATION_PENDING_PRE_CQ, |
| 'banana-pre-cq') |
| self._Act(launcher_build_id, c2, |
| constants.CL_ACTION_VALIDATION_PENDING_PRE_CQ, |
| 'guava-pre-cq') |
| self._Act(launcher_build_id, c2, constants.CL_ACTION_TRYBOT_LAUNCHING, |
| 'banana-pre-cq') |
| self._Act(pineapple_build_id, c2, constants.CL_ACTION_PICKED_UP) |
| self._Act(guava_build_id, c2, constants.CL_ACTION_VERIFIED) |
| |
| # c3 has 2 pending verifications, both passed, so it is passed. |
| self._Act(launcher_build_id, c3, |
| constants.CL_ACTION_VALIDATION_PENDING_PRE_CQ, |
| 'pineapple-pre-cq') |
| self._Act(launcher_build_id, c3, |
| constants.CL_ACTION_VALIDATION_PENDING_PRE_CQ, |
| 'guava-pre-cq') |
| self._Act(pineapple_build_id, c3, constants.CL_ACTION_VERIFIED) |
| self._Act(guava_build_id, c3, constants.CL_ACTION_VERIFIED) |
| |
| # c4 has 2 pending verifications: one is inflight and the other |
| # passed. It is considered inflight and busy. |
| self._Act(launcher_build_id, c4, |
| constants.CL_ACTION_VALIDATION_PENDING_PRE_CQ, |
| 'pineapple-pre-cq') |
| self._Act(launcher_build_id, c4, |
| constants.CL_ACTION_VALIDATION_PENDING_PRE_CQ, |
| 'guava-pre-cq') |
| self._Act(pineapple_build_id, c4, constants.CL_ACTION_PICKED_UP) |
| self._Act(guava_build_id, c4, constants.CL_ACTION_VERIFIED) |
| |
| # c5 has not even been screened. |
| |
| changes = [c1, c2, c3, c4, c5] |
| action_history = self.fake_db.GetActionsForChanges(changes) |
| progress_map = clactions.GetPreCQProgressMap(changes, action_history) |
| |
| self.assertEqual(({c2, c4}, {c4}, {c3}), |
| clactions.GetPreCQCategories(progress_map)) |
| |
| # Among changes c1, c2, c3, only the guava-pre-cq config is pending. The |
| # other configs are either inflight, launching, or passed everywhere. |
| screened_changes = set(changes).intersection(progress_map) |
| self.assertEqual({'guava-pre-cq'}, |
| clactions.GetPreCQConfigsToTest(screened_changes, |
| progress_map)) |
| |
| |
| class TestCLStatusCounter(cros_test_lib.TestCase): |
| """Tests that GetCLActionCount behaves as expected.""" |
| |
| def setUp(self): |
| self.fake_db = fake_cidb.FakeCIDBConnection() |
| |
| def testGetCLActionCount(self): |
| c1p1 = metadata_lib.GerritPatchTuple(1, 1, False) |
| c1p2 = metadata_lib.GerritPatchTuple(1, 2, False) |
| precq_build_id = self.fake_db.InsertBuild( |
| constants.PRE_CQ_LAUNCHER_NAME, waterfall.WATERFALL_INTERNAL, 1, |
| constants.PRE_CQ_LAUNCHER_CONFIG, 'bot-hostname') |
| melon_build_id = self.fake_db.InsertBuild( |
| 'melon builder name', waterfall.WATERFALL_INTERNAL, 1, |
| 'melon-config-name', 'grape-bot-hostname') |
| |
| # Count should be zero before any actions are recorded. |
| |
| action_history = self.fake_db.GetActionsForChanges([c1p1]) |
| self.assertEqual( |
| 0, |
| clactions.GetCLActionCount( |
| c1p1, validation_pool.CQ_PIPELINE_CONFIGS, |
| constants.CL_ACTION_KICKED_OUT, action_history)) |
| |
| # Record 3 failures for c1p1, and some other actions. Only count the |
| # actions from builders in validation_pool.CQ_PIPELINE_CONFIGS. |
| self.fake_db.InsertCLActions( |
| precq_build_id, |
| [clactions.CLAction.FromGerritPatchAndAction( |
| c1p1, constants.CL_ACTION_KICKED_OUT)]) |
| self.fake_db.InsertCLActions( |
| precq_build_id, |
| [clactions.CLAction.FromGerritPatchAndAction( |
| c1p1, constants.CL_ACTION_PICKED_UP)]) |
| self.fake_db.InsertCLActions( |
| precq_build_id, |
| [clactions.CLAction.FromGerritPatchAndAction( |
| c1p1, constants.CL_ACTION_KICKED_OUT)]) |
| self.fake_db.InsertCLActions( |
| melon_build_id, |
| [clactions.CLAction.FromGerritPatchAndAction( |
| c1p1, constants.CL_ACTION_KICKED_OUT)]) |
| |
| action_history = self.fake_db.GetActionsForChanges([c1p1]) |
| self.assertEqual( |
| 2, |
| clactions.GetCLActionCount( |
| c1p1, validation_pool.CQ_PIPELINE_CONFIGS, |
| constants.CL_ACTION_KICKED_OUT, action_history)) |
| |
| # Record a failure for c1p2. Now the latest patches failure count should be |
| # 1 (true weather we pass c1p1 or c1p2), whereas the total failure count |
| # should be 3. |
| self.fake_db.InsertCLActions( |
| precq_build_id, |
| [clactions.CLAction.FromGerritPatchAndAction( |
| c1p2, constants.CL_ACTION_KICKED_OUT)]) |
| |
| action_history = self.fake_db.GetActionsForChanges([c1p1]) |
| self.assertEqual( |
| 1, |
| clactions.GetCLActionCount( |
| c1p1, validation_pool.CQ_PIPELINE_CONFIGS, |
| constants.CL_ACTION_KICKED_OUT, action_history)) |
| self.assertEqual( |
| 1, |
| clactions.GetCLActionCount( |
| c1p2, validation_pool.CQ_PIPELINE_CONFIGS, |
| constants.CL_ACTION_KICKED_OUT, action_history)) |
| self.assertEqual( |
| 3, |
| clactions.GetCLActionCount( |
| c1p2, validation_pool.CQ_PIPELINE_CONFIGS, |
| constants.CL_ACTION_KICKED_OUT, action_history, |
| latest_patchset_only=False)) |
| |
| |
| class TestCLActionHistorySmoke(cros_test_lib.TestCase): |
| """A basic test for the simpler aggregating API for CLActionHistory.""" |
| |
| def setUp(self): |
| |
| self.cl1 = clactions.GerritChangeTuple(11111, True) |
| self.cl1_patch1 = clactions.GerritPatchTuple( |
| self.cl1.gerrit_number, 1, self.cl1.internal) |
| self.cl1_patch2 = clactions.GerritPatchTuple( |
| self.cl1.gerrit_number, 2, self.cl1.internal) |
| |
| self.cl2 = clactions.GerritChangeTuple(22222, True) |
| self.cl2_patch1 = clactions.GerritPatchTuple( |
| self.cl2.gerrit_number, 1, self.cl2.internal) |
| self.cl2_patch2 = clactions.GerritPatchTuple( |
| self.cl2.gerrit_number, 2, self.cl2.internal) |
| |
| self.cl3 = clactions.GerritChangeTuple(33333, True) |
| self.cl3_patch1 = clactions.GerritPatchTuple( |
| self.cl3.gerrit_number, 2, self.cl3.internal) |
| |
| # Expected actions in chronological order, most recent first. |
| self.action1 = clactions.CLAction.FromGerritPatchAndAction( |
| self.cl1_patch2, constants.CL_ACTION_SUBMITTED, |
| timestamp=self._NDaysAgo(1)) |
| self.action2 = clactions.CLAction.FromGerritPatchAndAction( |
| self.cl1_patch2, constants.CL_ACTION_KICKED_OUT, |
| timestamp=self._NDaysAgo(2)) |
| self.action3 = clactions.CLAction.FromGerritPatchAndAction( |
| self.cl2_patch2, constants.CL_ACTION_SUBMITTED, |
| timestamp=self._NDaysAgo(3)) |
| self.action4 = clactions.CLAction.FromGerritPatchAndAction( |
| self.cl1_patch1, constants.CL_ACTION_SUBMIT_FAILED, |
| timestamp=self._NDaysAgo(4)) |
| self.action5 = clactions.CLAction.FromGerritPatchAndAction( |
| self.cl1_patch1, constants.CL_ACTION_KICKED_OUT, |
| timestamp=self._NDaysAgo(5)) |
| self.action6 = clactions.CLAction.FromGerritPatchAndAction( |
| self.cl3_patch1, constants.CL_ACTION_SUBMITTED, |
| reason=constants.STRATEGY_NONMANIFEST, |
| timestamp=self._NDaysAgo(6)) |
| |
| # CLActionHistory does not require the history to be given in chronological |
| # order, so we provide them in reverse order, and expect them to be sorted |
| # as appropriate. |
| self.cl_action_stats = clactions.CLActionHistory([ |
| self.action1, self.action2, self.action3, self.action4, self.action5, |
| self.action6]) |
| |
| def _NDaysAgo(self, num_days): |
| return datetime.datetime.today() - datetime.timedelta(num_days) |
| |
| def testAffected(self): |
| """Tests that the Affected* methods DTRT.""" |
| self.assertEqual(set([self.cl1, self.cl2, self.cl3]), |
| self.cl_action_stats.affected_cls) |
| self.assertEqual( |
| set([self.cl1_patch1, self.cl1_patch2, self.cl2_patch2, |
| self.cl3_patch1]), |
| self.cl_action_stats.affected_patches) |
| |
| def testActions(self): |
| """Tests that different types of actions are listed correctly.""" |
| self.assertEqual([self.action5, self.action2], |
| self.cl_action_stats.reject_actions) |
| self.assertEqual([self.action6, self.action3, self.action1], |
| self.cl_action_stats.submit_actions) |
| self.assertEqual([self.action4], |
| self.cl_action_stats.submit_fail_actions) |
| |
| def testSubmitted(self): |
| """Tests that the list of submitted objects is correct.""" |
| self.assertEqual(set([self.cl1, self.cl2]), |
| self.cl_action_stats.GetSubmittedCLs()) |
| self.assertEqual(set([self.cl1, self.cl2, self.cl3]), |
| self.cl_action_stats.GetSubmittedCLs(False)) |
| self.assertEqual(set([self.cl1_patch2, self.cl2_patch2]), |
| self.cl_action_stats.GetSubmittedPatches()) |
| self.assertEqual(set([self.cl1_patch2, self.cl2_patch2, self.cl3_patch1]), |
| self.cl_action_stats.GetSubmittedPatches(False)) |
| |
| |
| class TestCLActionHistoryRejections(cros_test_lib.TestCase): |
| """Involved test of aggregation of rejections.""" |
| |
| CQ_BUILD_CONFIG = 'lumpy-paladin' |
| PRE_CQ_BUILD_CONFIG = 'pre-cq-group' |
| |
| def setUp(self): |
| self._days_forward = 1 |
| self._build_id = 1 |
| self.action_history = [] |
| self.cl_action_stats = None |
| |
| self.cl1 = clactions.GerritChangeTuple(11111, True) |
| self.cl1_patch1 = clactions.GerritPatchTuple( |
| self.cl1.gerrit_number, 1, self.cl1.internal) |
| self.cl1_patch2 = clactions.GerritPatchTuple( |
| self.cl1.gerrit_number, 2, self.cl1.internal) |
| |
| self.cl2 = clactions.GerritChangeTuple(22222, True) |
| self.cl2_patch1 = clactions.GerritPatchTuple( |
| self.cl2.gerrit_number, 1, self.cl2.internal) |
| self.cl2_patch2 = clactions.GerritPatchTuple( |
| self.cl2.gerrit_number, 2, self.cl2.internal) |
| |
| def _AppendToHistory(self, patch, action, **kwargs): |
| kwargs.setdefault('id', -1) |
| kwargs.setdefault('build_id', -1) |
| kwargs.setdefault('reason', '') |
| kwargs.setdefault('build_config', '') |
| kwargs['timestamp'] = (datetime.datetime.today() + |
| datetime.timedelta(self._days_forward)) |
| self._days_forward += 1 |
| kwargs['action'] = action |
| kwargs['change_number'] = int(patch.gerrit_number) |
| kwargs['patch_number'] = int(patch.patch_number) |
| kwargs['change_source'] = clactions.BoolToChangeSource(patch.internal) |
| kwargs['buildbucket_id'] = 'test-id' |
| kwargs['status'] = None |
| |
| action = clactions.CLAction(**kwargs) |
| self.action_history.append(action) |
| return action |
| |
| def _PickupAndRejectPatch(self, patch, **kwargs): |
| kwargs.setdefault('build_id', self._build_id) |
| self._build_id += 1 |
| pickup_action = self._AppendToHistory(patch, constants.CL_ACTION_PICKED_UP, |
| **kwargs) |
| reject_action = self._AppendToHistory(patch, constants.CL_ACTION_KICKED_OUT, |
| **kwargs) |
| return pickup_action, reject_action |
| |
| def _CreateCLActionHistory(self): |
| """Create the object under test, reordering the history. |
| |
| We reorder history in a fixed but arbitrary way, to test that order doesn't |
| matter for the object under test. |
| """ |
| random.seed(4) # Everyone knows this is the randomest number on earth. |
| random.shuffle(self.action_history) |
| self.cl_action_stats = clactions.CLActionHistory(self.action_history) |
| |
| def testRejectionsNoRejection(self): |
| """Tests the null case.""" |
| self._AppendToHistory(self.cl1_patch1, constants.CL_ACTION_SUBMITTED) |
| self._CreateCLActionHistory() |
| self.assertEqual({}, self.cl_action_stats.GetTrueRejections()) |
| self.assertEqual({}, self.cl_action_stats.GetFalseRejections()) |
| |
| def testTrueRejectionsSkipApplyFailure(self): |
| """Test that apply failures are not considered true rejections.""" |
| self._AppendToHistory(self.cl1_patch1, constants.CL_ACTION_KICKED_OUT) |
| self._AppendToHistory(self.cl1_patch2, constants.CL_ACTION_SUBMITTED) |
| self._CreateCLActionHistory() |
| self.assertEqual({}, self.cl_action_stats.GetTrueRejections()) |
| |
| def testTrueRejectionsIncludeLaterSubmitted(self): |
| """Tests that we include CLs which have a patch that was later submitted.""" |
| _, reject_action = self._PickupAndRejectPatch(self.cl1_patch1) |
| self._AppendToHistory(self.cl1_patch2, constants.CL_ACTION_SUBMITTED) |
| self._CreateCLActionHistory() |
| self.assertEqual({self.cl1_patch1: [reject_action]}, |
| self.cl_action_stats.GetTrueRejections()) |
| |
| def testTrueRejectionsMultipleRejectionsOnPatch(self): |
| """Tests that we include all rejection actions on a patch.""" |
| _, reject_action1 = self._PickupAndRejectPatch(self.cl1_patch1) |
| _, reject_action2 = self._PickupAndRejectPatch(self.cl1_patch1) |
| self._AppendToHistory(self.cl1_patch2, constants.CL_ACTION_SUBMITTED) |
| self._CreateCLActionHistory() |
| self.assertEqual({self.cl1_patch1: [reject_action1, reject_action2]}, |
| self.cl_action_stats.GetTrueRejections()) |
| |
| def testTrueRejectionsByCQ(self): |
| """A complex test filtering for rejections by the cq. |
| |
| For a patch that has been rejected by both the pre-cq and cq, only cq's |
| actions should be reported. For a patch that has been rejected by only the |
| pre-cq, the rejection should not be included at all. |
| """ |
| _, reject_action1 = self._PickupAndRejectPatch( |
| self.cl1_patch1, build_config=self.PRE_CQ_BUILD_CONFIG) |
| _, reject_action2 = self._PickupAndRejectPatch( |
| self.cl1_patch1, build_config=self.CQ_BUILD_CONFIG) |
| self._AppendToHistory(self.cl1_patch2, constants.CL_ACTION_SUBMITTED) |
| _, reject_action3 = self._PickupAndRejectPatch( |
| self.cl2_patch1, build_config=self.PRE_CQ_BUILD_CONFIG) |
| self._AppendToHistory(self.cl2_patch2, constants.CL_ACTION_SUBMITTED) |
| self._CreateCLActionHistory() |
| self.assertEqual({self.cl1_patch1: [reject_action1, reject_action2], |
| self.cl2_patch1: [reject_action3]}, |
| self.cl_action_stats.GetTrueRejections()) |
| self.assertEqual({self.cl1_patch1: [reject_action2]}, |
| self.cl_action_stats.GetTrueRejections(constants.CQ)) |
| |
| def testFalseRejectionsSkipApplyFailure(self): |
| """Test that apply failures are not considered false rejections.""" |
| self._AppendToHistory(self.cl1_patch1, constants.CL_ACTION_KICKED_OUT) |
| self._AppendToHistory(self.cl1_patch1, constants.CL_ACTION_SUBMITTED) |
| self._CreateCLActionHistory() |
| self.assertEqual({}, self.cl_action_stats.GetTrueRejections()) |
| |
| def testFalseRejectionMultiplePatchesFalselyRejected(self): |
| """Test the case when we reject mulitple patches falsely.""" |
| _, reject_action1 = self._PickupAndRejectPatch(self.cl1_patch1) |
| _, reject_action2 = self._PickupAndRejectPatch(self.cl1_patch1) |
| self._AppendToHistory(self.cl1_patch1, constants.CL_ACTION_SUBMITTED) |
| _, reject_action3 = self._PickupAndRejectPatch(self.cl1_patch2) |
| self._AppendToHistory(self.cl1_patch2, constants.CL_ACTION_SUBMITTED) |
| self._CreateCLActionHistory() |
| self.assertEqual({self.cl1_patch1: [reject_action1, reject_action2], |
| self.cl1_patch2: [reject_action3]}, |
| self.cl_action_stats.GetFalseRejections()) |
| |
| def testFalseRejectionsByCQ(self): |
| """Test that we list CQ spciefic rejections correctly.""" |
| self._PickupAndRejectPatch(self.cl1_patch1, |
| build_config=self.PRE_CQ_BUILD_CONFIG) |
| _, reject_action1 = self._PickupAndRejectPatch( |
| self.cl1_patch1, build_config=self.CQ_BUILD_CONFIG) |
| self._AppendToHistory(self.cl1_patch1, action=constants.CL_ACTION_SUBMITTED) |
| self._CreateCLActionHistory() |
| self.assertEqual({self.cl1_patch1: [reject_action1]}, |
| self.cl_action_stats.GetFalseRejections(constants.CQ)) |
| |
| def testFalseRejectionsSkipsBadPreCQRun(self): |
| """Test that we don't consider rejections on bad pre-cq buuilds false. |
| |
| We batch related CLs together on pre-cq runs. Rejections beause a certain |
| pre-cq build failed are considered to not be false because a CL was still to |
| blame. |
| """ |
| # Use our own build_ids to tie CLs together. |
| bad_build_id = 21 |
| # This false rejection is due to a bad build. |
| self._PickupAndRejectPatch(self.cl1_patch1, |
| build_config=self.PRE_CQ_BUILD_CONFIG, |
| build_id=bad_build_id) |
| # This is a true rejection, marking the pre-cq build as a bad build. |
| _, reject_action1 = self._PickupAndRejectPatch( |
| self.cl2_patch1, |
| build_config=self.PRE_CQ_BUILD_CONFIG, |
| build_id=bad_build_id) |
| self._AppendToHistory(self.cl1_patch1, |
| constants.CL_ACTION_SUBMITTED) |
| self._AppendToHistory(self.cl2_patch2, |
| constants.CL_ACTION_SUBMITTED) |
| self._CreateCLActionHistory() |
| self.assertEqual({self.cl2_patch1: [reject_action1]}, |
| self.cl_action_stats.GetTrueRejections()) |
| self.assertEqual({}, self.cl_action_stats.GetFalseRejections()) |
| |
| def testFalseRejectionsSkipsBadPreCQAction(self): |
| """Test that we skip only the bad pre-cq actions when skipping bad builds. |
| |
| If a patch is rejected by a bad pre-cq run, and then rejected again by |
| other builds, we should only skip the first action. |
| """ |
| # Use our own build_ids to tie CLs together. |
| bad_build_id = 21 |
| # This false rejection is due to a bad build. |
| self._PickupAndRejectPatch(self.cl1_patch1, |
| build_config=self.PRE_CQ_BUILD_CONFIG, |
| build_id=bad_build_id) |
| # This is a true rejection, marking the pre-cq build as a bad build. |
| _, reject_action1 = self._PickupAndRejectPatch( |
| self.cl2_patch1, |
| build_config=self.PRE_CQ_BUILD_CONFIG, |
| build_id=bad_build_id) |
| # This is a valid false rejection. |
| _, reject_action2 = self._PickupAndRejectPatch( |
| self.cl1_patch1, build_config=self.PRE_CQ_BUILD_CONFIG) |
| # This is also a valid false rejection. |
| _, reject_action3 = self._PickupAndRejectPatch( |
| self.cl1_patch1, build_config=self.CQ_BUILD_CONFIG) |
| self._AppendToHistory(self.cl1_patch1, |
| constants.CL_ACTION_SUBMITTED) |
| self._AppendToHistory(self.cl2_patch2, |
| constants.CL_ACTION_SUBMITTED) |
| self._CreateCLActionHistory() |
| self.assertEqual({self.cl2_patch1: [reject_action1]}, |
| self.cl_action_stats.GetTrueRejections()) |
| self.assertEqual({self.cl1_patch1: [reject_action2, reject_action3]}, |
| self.cl_action_stats.GetFalseRejections()) |
| self.assertEqual({self.cl1_patch1: [reject_action2]}, |
| self.cl_action_stats.GetFalseRejections(constants.PRE_CQ)) |
| self.assertEqual({self.cl1_patch1: [reject_action3]}, |
| self.cl_action_stats.GetFalseRejections(constants.CQ)) |
| |
| def testFalseRejectionsMergeConflictByBotType(self): |
| """Test the case when one bot has merge conflict. |
| |
| If pre-cq falsely rejects a patch, and CQ has a merge conflict, but later |
| submits the CL, the false rejection should only show up for pre-cq. |
| """ |
| _, reject_action1 = self._PickupAndRejectPatch( |
| self.cl1_patch1, |
| build_config=self.PRE_CQ_BUILD_CONFIG) |
| self._AppendToHistory(self.cl1_patch1, constants.CL_ACTION_KICKED_OUT, |
| build_config=self.CQ_BUILD_CONFIG) |
| self._AppendToHistory(self.cl1_patch1, constants.CL_ACTION_SUBMITTED, |
| build_config=self.CQ_BUILD_CONFIG) |
| self._CreateCLActionHistory() |
| self.assertEqual({self.cl1_patch1: [reject_action1]}, |
| self.cl_action_stats.GetFalseRejections(constants.PRE_CQ)) |
| self.assertEqual({}, self.cl_action_stats.GetFalseRejections(constants.CQ)) |
| |
| def testRejectionsPatchSubmittedThenUpdated(self): |
| """Test the case when a patch is submitted, then updated.""" |
| _, reject_action1 = self._PickupAndRejectPatch(self.cl1_patch1) |
| self._AppendToHistory(self.cl1_patch1, constants.CL_ACTION_SUBMITTED) |
| self._AppendToHistory(self.cl1_patch2, constants.CL_ACTION_PICKED_UP) |
| self._CreateCLActionHistory() |
| self.assertEqual({}, self.cl_action_stats.GetTrueRejections()) |
| self.assertEqual({self.cl1_patch1: [reject_action1]}, |
| self.cl_action_stats.GetFalseRejections()) |
| |
| |
| class TestGerritChangeTuple(cros_test_lib.TestCase): |
| """Tests of basic GerritChangeTuple functionality.""" |
| |
| def testUnknownHostRaises(self): |
| with self.assertRaises(clactions.UnknownGerritHostError): |
| clactions.GerritChangeTuple.FromHostAndNumber('foobar-host', 1234) |
| |
| def testKnownHosts(self): |
| self.assertEqual((31415, True), |
| clactions.GerritChangeTuple.FromHostAndNumber( |
| 'gerrit-int.chromium.org', 31415)) |
| self.assertEqual((31415, True), |
| clactions.GerritChangeTuple.FromHostAndNumber( |
| constants.INTERNAL_GERRIT_HOST, 31415)) |
| self.assertEqual((31415, False), |
| clactions.GerritChangeTuple.FromHostAndNumber( |
| 'gerrit.chromium.org', 31415)) |
| self.assertEqual((31415, False), |
| clactions.GerritChangeTuple.FromHostAndNumber( |
| constants.EXTERNAL_GERRIT_HOST, 31415)) |