blob: f401fcabb77245f34c55b9f3ffa1a9262a27c88b [file] [log] [blame]
# 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 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', constants.WATERFALL_INTERNAL, 1,
constants.PRE_CQ_LAUNCHER_CONFIG, 'hostname')
trybot_id = self.fake_db.InsertBuild(
'banana pre cq', constants.WATERFALL_INTERNAL, 1,
'banana-pre-cq', 'hostname')
master_id = self.fake_db.InsertBuild(
'CQ master', constants.WATERFALL_INTERNAL, 1,
constants.CQ_MASTER, 'hostname')
slave_id = self.fake_db.InsertBuild(
'banana paladin', constants.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))
clactions.RecordSubmissionMetrics(
clactions.CLActionHistory(action_history), strategies)
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 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, constants.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, constants.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', constants.WATERFALL_TRYBOT, 12, 'banana-pre-cq',
'banana hostname')
pineapple_build_id = self.fake_db.InsertBuild(
'pineapple', constants.WATERFALL_TRYBOT, 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, constants.WATERFALL_INTERNAL,
1, constants.PRE_CQ_LAUNCHER_CONFIG, 'bot hostname 1')
pineapple_build_id = self.fake_db.InsertBuild(
'pineapple', constants.WATERFALL_TRYBOT, 87, 'pineapple-pre-cq',
'pineapple hostname')
guava_build_id = self.fake_db.InsertBuild(
'guava', constants.WATERFALL_TRYBOT, 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, constants.WATERFALL_INTERNAL, 1,
constants.PRE_CQ_LAUNCHER_CONFIG, 'bot-hostname')
melon_build_id = self.fake_db.InsertBuild(
'melon builder name', constants.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'
action = clactions.CLAction.GetCLAction(**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))