summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xcbuildbot/cbuildbot_config.py6
-rw-r--r--cbuildbot/constants.py23
-rwxr-xr-xcbuildbot/stages/report_stages_unittest.py2
-rw-r--r--cbuildbot/stages/sync_stages.py50
-rwxr-xr-xcbuildbot/stages/sync_stages_unittest.py180
-rw-r--r--cbuildbot/validation_pool.py299
-rwxr-xr-xcbuildbot/validation_pool_unittest.py139
-rw-r--r--lib/cidb.py18
-rwxr-xr-xlib/cidb_setup_unittest.py7
9 files changed, 435 insertions, 289 deletions
diff --git a/cbuildbot/cbuildbot_config.py b/cbuildbot/cbuildbot_config.py
index ecdae1063..87d885cd4 100755
--- a/cbuildbot/cbuildbot_config.py
+++ b/cbuildbot/cbuildbot_config.py
@@ -24,9 +24,9 @@ CONFIG_TYPE_RELEASE_AFDO = 'release-afdo'
CONFIG_TYPE_DUMP_ORDER = (
CONFIG_TYPE_PALADIN,
- constants.PRE_CQ_BUILDER_NAME,
+ constants.PRE_CQ_GROUP_CONFIG,
'pre-cq',
- 'pre-cq-launcher',
+ constants.PRE_CQ_LAUNCHER_CONFIG,
'incremental',
'telemetry',
CONFIG_TYPE_FULL,
@@ -1686,7 +1686,7 @@ compile_only_pre_cq = pre_cq.derive(
# TODO(davidjames): Add peach_pit, nyan, and beaglebone to pre-cq.
# TODO(davidjames): Revert CL:221326 so daisy_spring and duck can build
# images again
-_config.add_group(constants.PRE_CQ_BUILDER_NAME,
+_config.add_group(constants.PRE_CQ_GROUP_CONFIG,
# amd64 w/kernel 3.10. This builder runs VMTest so it's going to be
# the slowest one.
pre_cq.add_config('rambi-pre-cq', boards=['rambi']),
diff --git a/cbuildbot/constants.py b/cbuildbot/constants.py
index 052b9cab7..3b477e870 100644
--- a/cbuildbot/constants.py
+++ b/cbuildbot/constants.py
@@ -42,11 +42,17 @@ CIDB_PROD_BOT_CREDS = os.path.join(HOME_DIRECTORY, '.cidb_creds',
CIDB_DEBUG_BOT_CREDS = os.path.join(HOME_DIRECTORY, '.cidb_creds',
'debug_cidb_bot')
-CIDB_KNOWN_WATERFALLS = ('chromeos',
- 'chromiumos',
- 'chromiumos.tryserver',
- 'chromeos_release',
- 'chromeos.branch')
+WATERFALL_INTERNAL = 'chromeos'
+WATERFALL_EXTERNAL = 'chromiumos'
+WATERFALL_TRYBOT = 'chromiumos.tryserver'
+WATERFALL_RELEASE = 'chromeos_release'
+WATERFALL_BRANCH = 'chromeos.branch'
+
+CIDB_KNOWN_WATERFALLS = (WATERFALL_INTERNAL,
+ WATERFALL_EXTERNAL,
+ WATERFALL_TRYBOT,
+ WATERFALL_RELEASE,
+ WATERFALL_BRANCH)
# TODO: Eliminate these or merge with manifest_version.py:STATUS_PASSED
# crbug.com/318930
@@ -286,8 +292,11 @@ VALID_BUILD_TYPES = (
PAYLOADS_TYPE,
)
-# The name of the builder used to launch the pre-CQ.
-PRE_CQ_BUILDER_NAME = 'pre-cq-group'
+# The name of the standard pre-cq testing config.
+PRE_CQ_GROUP_CONFIG = 'pre-cq-group'
+
+# The name of the pre-cq launching config.
+PRE_CQ_LAUNCHER_CONFIG = 'pre-cq-launcher'
# The name of the Pre-CQ launcher on the waterfall.
PRE_CQ_LAUNCHER_NAME = 'Pre-CQ Launcher'
diff --git a/cbuildbot/stages/report_stages_unittest.py b/cbuildbot/stages/report_stages_unittest.py
index 4da178c00..a9926e849 100755
--- a/cbuildbot/stages/report_stages_unittest.py
+++ b/cbuildbot/stages/report_stages_unittest.py
@@ -112,8 +112,6 @@ class ReportStageTest(generic_stages_unittest.AbstractStageTest):
self.StartPatcher(mock.patch.object(*cmd, autospec=True))
self.StartPatcher(BuilderRunMock())
- self.cq = sync_stages_unittest.CLStatusMock()
- self.StartPatcher(self.cq)
self.sync_stage = None
# Set up a general purpose cidb mock. Tests with more specific
diff --git a/cbuildbot/stages/sync_stages.py b/cbuildbot/stages/sync_stages.py
index 47d661120..5826bc718 100644
--- a/cbuildbot/stages/sync_stages.py
+++ b/cbuildbot/stages/sync_stages.py
@@ -19,7 +19,6 @@ from chromite.cbuildbot import failures_lib
from chromite.cbuildbot import constants
from chromite.cbuildbot import lkgm_manager
from chromite.cbuildbot import manifest_version
-from chromite.cbuildbot import metadata_lib
from chromite.cbuildbot import repository
from chromite.cbuildbot import tree_status
from chromite.cbuildbot import trybot_patch_pool
@@ -701,7 +700,7 @@ class CommitQueueSyncStage(MasterSlaveLKGMSyncStage):
# First, look for changes that were tested by the Pre-CQ.
changes_to_test = []
for change in changes:
- status = pool.GetCLStatus(PRE_CQ, change)
+ status = pool.GetCLPreCQStatus(change)
if status == validation_pool.ValidationPool.STATUS_PASSED:
changes_to_test.append(change)
@@ -818,23 +817,23 @@ class PreCQSyncStage(SyncStage):
if len(self.pool.changes) == 0:
cros_build_lib.Die('No changes have been applied.')
- # Mark changes with pre-cq status inflight in database.
- # This will be replaced by a call to UpdateCLPreCQStatus in a future CL.
+ # Mark changes that are not passed with pre-cq status inflight.
if (cidb.CIDBConnectionFactory.IsCIDBSetup() and
cidb.CIDBConnectionFactory.GetCIDBConnectionForBuilder()):
build_id = self._run.attrs.metadata.GetValue('build_id')
- db = cidb.CIDBConnectionFactory.GetCIDBConnectionForBuilder()
for change in self.pool.changes:
- db.InsertCLActions(
- build_id,
- [metadata_lib.GetCLActionTuple(
- change, constants.CL_ACTION_PRE_CQ_INFLIGHT)])
+ current_status = validation_pool.ValidationPool.GetCLPreCQStatus(
+ change)
+ if current_status != validation_pool.ValidationPool.STATUS_PASSED:
+ validation_pool.ValidationPool.UpdateCLPreCQStatus(
+ change, validation_pool.ValidationPool.STATUS_INFLIGHT,
+ build_id)
class PreCQLauncherStage(SyncStage):
"""Scans for CLs and automatically launches Pre-CQ jobs to test them."""
- # The CL is currently being tested by a Pre-CQ builder.
+ # The CL is currently being tested by a Pre-CQ trybot.
STATUS_INFLIGHT = validation_pool.ValidationPool.STATUS_INFLIGHT
# The CL has passed the Pre-CQ.
@@ -959,16 +958,14 @@ class PreCQLauncherStage(SyncStage):
pool.SendNotification(change, '%(details)s', details=msg)
pool.RemoveCommitReady(change)
- pool.UpdateCLStatus(PRE_CQ, change, self.STATUS_FAILED,
- self._run.options.debug,
- build_id=self._build_id)
+ pool.UpdateCLPreCQStatus(change, self.STATUS_FAILED,
+ self._build_id)
self.retried.discard(change)
else:
# Try the change again.
self.retried.add(change)
- pool.UpdateCLStatus(PRE_CQ, change, self.STATUS_WAITING,
- self._run.options.debug,
- build_id=self._build_id)
+ pool.UpdateCLPreCQStatus(change, self.STATUS_WAITING,
+ self._build_id)
elif status == self.STATUS_INFLIGHT:
# Once a Pre-CQ run actually starts, it'll set the status to
# STATUS_INFLIGHT.
@@ -987,18 +984,16 @@ class PreCQLauncherStage(SyncStage):
pool.SendNotification(change, '%(details)s', details=msg)
pool.RemoveCommitReady(change)
- pool.UpdateCLStatus(PRE_CQ, change, self.STATUS_FAILED,
- self._run.options.debug,
- build_id=self._build_id)
+ pool.UpdateCLPreCQStatus(change, self.STATUS_FAILED,
+ self._build_id)
elif status == self.STATUS_FAILED:
# The Pre-CQ run failed for this change. It's possible that we got
# unlucky and this change was just marked as 'Not Ready' by a bot. To
# test this, mark the CL as 'waiting' for now. If the CL is still marked
# as 'Ready' next time we check, we'll know the CL is truly still ready.
busy.add(change)
- pool.UpdateCLStatus(PRE_CQ, change, self.STATUS_WAITING,
- self._run.options.debug,
- build_id=self._build_id)
+ pool.UpdateCLPreCQStatus(change, self.STATUS_WAITING,
+ self._build_id)
self._PrintPatchStatus(change, status)
elif status == self.STATUS_PASSED:
passed.add(change)
@@ -1016,7 +1011,7 @@ class PreCQLauncherStage(SyncStage):
pool: ValidationPool corresponding to |plan|.
plan: The list of patches to test in the Pre-CQ run.
"""
- cmd = ['cbuildbot', '--remote', constants.PRE_CQ_BUILDER_NAME,
+ cmd = ['cbuildbot', '--remote', constants.PRE_CQ_GROUP_CONFIG,
'--timeout', str(self.INFLIGHT_DELAY * 60)]
if self._run.options.debug:
cmd.append('--debug')
@@ -1025,10 +1020,9 @@ class PreCQLauncherStage(SyncStage):
self._PrintPatchStatus(patch, 'testing')
cros_build_lib.RunCommand(cmd, cwd=self._build_root)
for patch in plan:
- if pool.GetCLStatus(PRE_CQ, patch) != self.STATUS_PASSED:
- pool.UpdateCLStatus(PRE_CQ, patch, self.STATUS_LAUNCHING,
- self._run.options.debug,
- build_id=self._build_id)
+ if pool.GetCLPreCQStatus(patch) != self.STATUS_PASSED:
+ pool.UpdateCLPreCQStatus(patch, self.STATUS_LAUNCHING,
+ self._build_id)
def GetDisjointTransactionsToTest(self, pool, changes, status_map):
"""Get the list of disjoint transactions to test.
@@ -1082,7 +1076,7 @@ class PreCQLauncherStage(SyncStage):
# Get change status.
status_map = {}
for change in changes:
- status = pool.GetCLStatus(PRE_CQ, change)
+ status = pool.GetCLPreCQStatus(change)
status_map[change] = status
# Launch trybots for manifest changes.
diff --git a/cbuildbot/stages/sync_stages_unittest.py b/cbuildbot/stages/sync_stages_unittest.py
index 7bf23012b..20a2d8b61 100755
--- a/cbuildbot/stages/sync_stages_unittest.py
+++ b/cbuildbot/stages/sync_stages_unittest.py
@@ -26,11 +26,12 @@ from chromite.cbuildbot.stages import sync_stages
from chromite.cbuildbot.stages import generic_stages_unittest
from chromite.lib import cros_build_lib_unittest
from chromite.lib import cros_test_lib
+from chromite.lib import cidb
+from chromite.lib import fake_cidb
from chromite.lib import gerrit
from chromite.lib import git_unittest
from chromite.lib import gob_util
from chromite.lib import osutils
-from chromite.lib import partial_mock
from chromite.lib import timeout_util
@@ -127,7 +128,6 @@ class MockPatch(mock.MagicMock):
class BaseCQTestCase(generic_stages_unittest.StageTest):
"""Helper class for testing the CommitQueueSync stage"""
- PALADIN_BOT_ID = None
MANIFEST_CONTENTS = '<manifest/>'
def setUp(self):
@@ -160,9 +160,16 @@ class BaseCQTestCase(generic_stages_unittest.StageTest):
self.PatchObject(validation_pool.ValidationPool, 'ReloadChanges',
side_effect=lambda x: x)
+ # Create and set up a fake cidb instance.
+ self.fake_db = fake_cidb.FakeCIDBConnection()
+ cidb.CIDBConnectionFactory.SetupMockCidb(self.fake_db)
+
self.sync_stage = None
self._Prepare()
+ def tearDown(self):
+ cidb.CIDBConnectionFactory.ClearMock()
+
def _Prepare(self, bot_id=None, **kwargs):
super(BaseCQTestCase, self)._Prepare(bot_id, **kwargs)
@@ -185,6 +192,9 @@ class BaseCQTestCase(generic_stages_unittest.StageTest):
runs: The maximum number of times to allow validation_pool.AcquirePool
to wait for additional changes. runs=0 means never wait for
additional changes. Default: 0.
+
+ Returns:
+ A list of MockPatch objects which were created and used in PerformSync.
"""
p = MockPatch(remote=remote, tracking_branch=tracking_branch)
my_patches = [p] * num_patches
@@ -207,6 +217,8 @@ class BaseCQTestCase(generic_stages_unittest.StageTest):
side_effect=exit_it)
self.sync_stage.PerformStage()
+ return my_patches
+
def ReloadPool(self):
"""Save the pool to disk and reload it."""
with tempfile.NamedTemporaryFile() as f:
@@ -243,24 +255,40 @@ class MasterCQSyncTestCase(BaseCQTestCase):
return_value=self.manifest_path, autospec=True)
def _testCommitNonManifestChange(self, **kwargs):
- """Test the commit of a non-manifest change."""
+ """Test the commit of a non-manifest change.
+
+ Returns:
+ List of MockPatch objects that were used in PerformSync
+ """
# Setting tracking_branch=foo makes this a non-manifest change.
kwargs.setdefault('committed', True)
- self.PerformSync(tracking_branch='foo', **kwargs)
+ return self.PerformSync(tracking_branch='foo', **kwargs)
def _testFailedCommitOfNonManifestChange(self):
- """Test that the commit of a non-manifest change fails."""
- self._testCommitNonManifestChange(committed=False)
+ """Test that the commit of a non-manifest change fails.
+
+ Returns:
+ List of MockPatch objects that were used in PerformSync
+ """
+ return self._testCommitNonManifestChange(committed=False)
def _testCommitManifestChange(self, **kwargs):
- """Test committing a change to a project that's part of the manifest."""
+ """Test committing a change to a project that's part of the manifest.
+
+ Returns:
+ List of MockPatch objects that were used in PerformSync
+ """
self.PatchObject(validation_pool.ValidationPool, '_FilterNonCrosProjects',
side_effect=lambda x, _: (x, []))
- self.PerformSync(**kwargs)
+ return self.PerformSync(**kwargs)
def _testDefaultSync(self):
- """Test basic ability to sync with standard options."""
- self.PerformSync()
+ """Test basic ability to sync with standard options.
+
+ Returns:
+ List of MockPatch objects that were used in PerformSync
+ """
+ return self.PerformSync()
class MasterCQSyncTest(MasterCQSyncTestCase):
@@ -268,19 +296,19 @@ class MasterCQSyncTest(MasterCQSyncTestCase):
def testCommitNonManifestChange(self):
"""See MasterCQSyncTestCase"""
- self._testCommitNonManifestChange()
+ return self._testCommitNonManifestChange()
def testFailedCommitOfNonManifestChange(self):
"""See MasterCQSyncTestCase"""
- self._testFailedCommitOfNonManifestChange()
+ return self._testFailedCommitOfNonManifestChange()
def testCommitManifestChange(self):
"""See MasterCQSyncTestCase"""
- self._testCommitManifestChange()
+ return self._testCommitManifestChange()
def testDefaultSync(self):
"""See MasterCQSyncTestCase"""
- self._testDefaultSync()
+ return self._testDefaultSync()
def testReload(self):
"""Test basic ability to sync and reload the patches from disk."""
@@ -300,99 +328,85 @@ class MasterCQSyncTest(MasterCQSyncTestCase):
mock.ANY, constants.THROTTLED_CQ_READY_QUERY,
sort='lastUpdated')
-
-class CLStatusMock(partial_mock.PartialMock):
- """Partial mock for CLStatus methods in ValidationPool."""
-
- TARGET = 'chromite.cbuildbot.validation_pool.ValidationPool'
- ATTRS = ('GetCLStatus', 'GetCLStatusCount', 'UpdateCLStatus',)
-
- def __init__(self, treat_launching_as_inflight=False):
- """CLStatusMock constructor.
-
- Args:
- treat_launching_as_inflight: When getting a CL's status via
- GetCLStatus, treat any change with status LAUNCHING as if
- it has status INFLIGHT. This simulates pre-cq tryjobs getting
- immediately launched. Default: False.
- """
- partial_mock.PartialMock.__init__(self)
- self.calls = {}
- self.status = {}
- self.status_count = {}
- self._treat_launching_as_inflight = treat_launching_as_inflight
-
- def GetCLStatus(self, _bot, change):
- status = self.status.get(change)
- if (self._treat_launching_as_inflight and
- status == validation_pool.ValidationPool.STATUS_LAUNCHING):
- return validation_pool.ValidationPool.STATUS_INFLIGHT
- return status
-
- def GetCLStatusCount(self, _bot, change, count, latest_patchset_only=True):
- # pylint: disable=W0613
- return self.status_count.get(change, 0)
-
- def UpdateCLStatus(self, _bot, change, status, dry_run, build_id=None):
- # pylint: disable=W0613
- self.calls[status] = self.calls.get(status, 0) + 1
- self.status[change] = status
- self.status_count[change] = self.status_count.get(change, 0) + 1
-
-
class PreCQLauncherStageTest(MasterCQSyncTestCase):
"""Tests for the PreCQLauncherStage."""
- BOT_ID = 'pre-cq-launcher'
+ BOT_ID = constants.PRE_CQ_LAUNCHER_CONFIG
STATUS_LAUNCHING = validation_pool.ValidationPool.STATUS_LAUNCHING
STATUS_WAITING = validation_pool.ValidationPool.STATUS_WAITING
STATUS_FAILED = validation_pool.ValidationPool.STATUS_FAILED
STATUS_READY_TO_SUBMIT = validation_pool.ValidationPool.STATUS_READY_TO_SUBMIT
+ STATUS_INFLIGHT = validation_pool.ValidationPool.STATUS_INFLIGHT
def setUp(self):
self.PatchObject(time, 'sleep', autospec=True)
- def _PrepareValidationPoolMock(self, auto_launch=False):
- # pylint: disable-msg=W0201
- self.pre_cq = CLStatusMock(treat_launching_as_inflight=auto_launch)
- self.StartPatcher(self.pre_cq)
+ def _PrepareAutoLaunch(self):
+ """Cause CLs with launching status to be automatically launched."""
+ # Mock out UpdateCLPreCQStatus so that when a "Launching" action is
+ # recorded, automatically pretend to start a new build which records
+ # an "Inflight" action for the same change.
+ original_method = validation_pool.ValidationPool.UpdateCLPreCQStatus
+
+ def new_method(change, status, build_id):
+ original_method(change, status, build_id)
+ if (status == self.STATUS_LAUNCHING):
+ new_build_id = self.fake_db.InsertBuild('Pre cq group',
+ constants.WATERFALL_TRYBOT,
+ 1,
+ constants.PRE_CQ_GROUP_CONFIG,
+ 'bot-hostname')
+ original_method(change, self.STATUS_INFLIGHT,
+ new_build_id)
+
+ self.PatchObject(validation_pool.ValidationPool, 'UpdateCLPreCQStatus',
+ side_effect=new_method)
+
def _Prepare(self, bot_id=None, **kwargs):
- super(PreCQLauncherStageTest, self)._Prepare(bot_id, **kwargs)
+ build_id = self.fake_db.InsertBuild(
+ constants.PRE_CQ_LAUNCHER_NAME, constants.WATERFALL_INTERNAL, 1,
+ constants.PRE_CQ_LAUNCHER_CONFIG, 'bot-hostname')
+
+ super(PreCQLauncherStageTest, self)._Prepare(
+ bot_id, build_id=build_id, **kwargs)
self.sync_stage = sync_stages.PreCQLauncherStage(self._run)
def testCommitNonManifestChange(self):
"""See MasterCQSyncTestCase"""
- self._PrepareValidationPoolMock()
self._testCommitNonManifestChange()
def testFailedCommitOfNonManifestChange(self):
"""See MasterCQSyncTestCase"""
- self._PrepareValidationPoolMock()
self._testFailedCommitOfNonManifestChange()
def testCommitManifestChange(self):
"""See MasterCQSyncTestCase"""
- self._PrepareValidationPoolMock()
self._testCommitManifestChange()
def testDefaultSync(self):
"""See MasterCQSyncTestCase"""
- self._PrepareValidationPoolMock()
self._testDefaultSync()
def testTreeClosureIsOK(self):
"""Test that tree closures block commits."""
- self._PrepareValidationPoolMock()
self._testCommitNonManifestChange(tree_open=False)
def testLaunchTrybot(self):
"""Test launching a trybot."""
- self._PrepareValidationPoolMock()
- self._testCommitManifestChange()
- self.assertEqual(self.pre_cq.status.values(), [self.STATUS_LAUNCHING])
- self.assertEqual(self.pre_cq.calls.keys(), [self.STATUS_LAUNCHING])
+ change = self._testCommitManifestChange()[0]
+
+ self.assertEqual(validation_pool.ValidationPool.GetCLPreCQStatus(change),
+ self.STATUS_LAUNCHING)
+
+ def testLaunchTrybotWithAutolaunch(self):
+ """Test launching a trybot with auto-launch."""
+ self._PrepareAutoLaunch()
+ change = self._testCommitManifestChange()[0]
+
+ self.assertEqual(validation_pool.ValidationPool.GetCLPreCQStatus(change),
+ self.STATUS_INFLIGHT)
def runTrybotTest(self, launching=0, waiting=0, failed=0, runs=0):
"""Helper function for testing PreCQLauncher.
@@ -402,16 +416,24 @@ class PreCQLauncherStageTest(MasterCQSyncTestCase):
LAUNCHING, WAITING, and FAILED |launching|, |waiting|, and |failed| times
respectively.
"""
- self._testCommitManifestChange(runs=runs)
- self.assertEqual(self.pre_cq.calls.get(self.STATUS_LAUNCHING, 0), launching)
- self.assertEqual(self.pre_cq.calls.get(self.STATUS_WAITING, 0), waiting)
- self.assertEqual(self.pre_cq.calls.get(self.STATUS_FAILED, 0), failed)
- self.assertEqual(sum(self.pre_cq.calls.values()),
- launching + waiting + failed)
+ change = self._testCommitManifestChange(runs=runs)[0]
+ # Count the number of recorded actions corresponding to launching, watiting,
+ # and failed, and ensure they are correct.
+ validation_pool.ValidationPool.ClearActionCache()
+
+ expected = (launching, waiting, failed)
+ actions = (constants.CL_ACTION_PRE_CQ_LAUNCHING,
+ constants.CL_ACTION_PRE_CQ_WAITING,
+ constants.CL_ACTION_PRE_CQ_FAILED)
+
+ for exp, action in zip(expected, actions):
+ self.assertEqual(
+ exp,
+ validation_pool.ValidationPool.GetCLActionCount(
+ change, [constants.PRE_CQ_LAUNCHER_CONFIG], action))
def testLaunchTrybotTimesOutOnce(self):
"""Test what happens when a trybot launch times out."""
- self._PrepareValidationPoolMock()
it = itertools.chain([True], itertools.repeat(False))
self.PatchObject(sync_stages.PreCQLauncherStage, '_HasLaunchTimedOut',
side_effect=it)
@@ -419,14 +441,13 @@ class PreCQLauncherStageTest(MasterCQSyncTestCase):
def testLaunchTrybotTimesOutTwice(self):
"""Test what happens when a trybot launch times out."""
- self._PrepareValidationPoolMock()
self.PatchObject(sync_stages.PreCQLauncherStage, '_HasLaunchTimedOut',
return_value=True)
self.runTrybotTest(launching=2, waiting=1, failed=1, runs=3)
def testInflightTrybotTimesOutOnce(self):
"""Test what happens when an inflight trybot times out."""
- self._PrepareValidationPoolMock(auto_launch=True)
+ self._PrepareAutoLaunch()
it = itertools.chain([True], itertools.repeat(False))
self.PatchObject(sync_stages.PreCQLauncherStage, '_HasInflightTimedOut',
side_effect=it)
@@ -434,8 +455,7 @@ class PreCQLauncherStageTest(MasterCQSyncTestCase):
def testSubmit(self):
"""Test submission of patches."""
- self._PrepareValidationPoolMock()
- self.PatchObject(validation_pool.ValidationPool, 'GetCLStatus',
+ self.PatchObject(validation_pool.ValidationPool, 'GetCLPreCQStatus',
return_value=self.STATUS_READY_TO_SUBMIT)
m = self.PatchObject(validation_pool.ValidationPool, 'SubmitChanges')
self.runTrybotTest(runs=1)
diff --git a/cbuildbot/validation_pool.py b/cbuildbot/validation_pool.py
index 1bdd348ed..a4ea19741 100644
--- a/cbuildbot/validation_pool.py
+++ b/cbuildbot/validation_pool.py
@@ -34,7 +34,6 @@ from chromite.lib import cros_build_lib
from chromite.lib import gerrit
from chromite.lib import git
from chromite.lib import gob_util
-from chromite.lib import gs
from chromite.lib import parallel
from chromite.lib import patch as cros_patch
from chromite.lib import portage_util
@@ -55,6 +54,13 @@ except ImportError:
PRE_CQ = constants.PRE_CQ
CQ = constants.CQ
+CQ_CONFIG = constants.CQ_MASTER
+PRE_CQ_GROUP_CONFIG = constants.PRE_CQ_GROUP_CONFIG
+PRE_CQ_LAUNCHER_CONFIG = constants.PRE_CQ_LAUNCHER_CONFIG
+
+# Set of configs that can reject a CL from the pre-CQ / CQ pipeline.
+CQ_PIPELINE_CONFIGS = {CQ_CONFIG, PRE_CQ_GROUP_CONFIG, PRE_CQ_LAUNCHER_CONFIG}
+
# The gerrit-on-borg team tells us that delays up to 2 minutes can be
# normal. Setting timeout to 3 minutes to be safe-ish.
SUBMITTED_WAIT_TIMEOUT = 3 * 60 # Time in seconds.
@@ -1103,21 +1109,6 @@ class CalculateSuspects(object):
return suspects
@classmethod
- def _FindPreviouslyFailedChanges(cls, candidates):
- """Find what changes that have previously failed the CQ.
-
- The first time a change is included in a build that fails due to a
- flaky (or apparently unrelated) failure, we assume that it is innocent. If
- this happens more than once, we kick out the CL.
- """
- suspects = set()
- for change in candidates:
- if ValidationPool.GetCLStatusCount(
- CQ, change, ValidationPool.STATUS_FAILED):
- suspects.add(change)
- return suspects
-
- @classmethod
def FilterChromiteChanges(cls, changes):
"""Returns a list of chromite changes in |changes|."""
return [x for x in changes if x.project == constants.CHROMITE_PROJECT]
@@ -1363,8 +1354,25 @@ class ValidationPool(object):
# errors.
REJECTION_GRACE_PERIOD = 30 * 60
- # Cache for the status of CLs.
- _CL_STATUS_CACHE = {}
+ # Cache for the action history of CLs.
+ _CL_ACTION_HISTORY_CACHE = {}
+
+ # Bidirectional mapping between pre-cq status strings and CL action strings.
+ _PRECQ_STATUS_TO_ACTION = {
+ STATUS_INFLIGHT: constants.CL_ACTION_PRE_CQ_INFLIGHT,
+ STATUS_PASSED: constants.CL_ACTION_PRE_CQ_PASSED,
+ STATUS_FAILED: constants.CL_ACTION_PRE_CQ_FAILED,
+ STATUS_LAUNCHING: constants.CL_ACTION_PRE_CQ_LAUNCHING,
+ STATUS_WAITING: constants.CL_ACTION_PRE_CQ_WAITING,
+ STATUS_READY_TO_SUBMIT: constants.CL_ACTION_PRE_CQ_READY_TO_SUBMIT
+ }
+
+ _PRECQ_ACTION_TO_STATUS = dict(
+ (v, k) for k, v in _PRECQ_STATUS_TO_ACTION.items())
+
+ assert len(_PRECQ_STATUS_TO_ACTION) == len(_PRECQ_ACTION_TO_STATUS), \
+ '_PRECQ_STATUS_TO_ACTION values are not unique.'
+
def __init__(self, overlays, build_root, build_number, builder_name,
is_master, dryrun, changes=None, non_os_changes=None,
@@ -1933,15 +1941,16 @@ class ValidationPool(object):
self._HandleApplyFailure(errors)
raise
- # Completely fill the status cache in parallel.
- self.FillCLStatusCache(CQ, applied)
+ # Completely fill the action cache.
+ self._FillCLActionCache(applied)
for change in applied:
- change.total_fail_count = self.GetCLStatusCount(
- CQ, change, self.STATUS_FAILED, latest_patchset_only=False)
- change.fail_count = self.GetCLStatusCount(
- CQ, change, self.STATUS_FAILED)
- change.pass_count = self.GetCLStatusCount(
- CQ, change, self.STATUS_PASSED)
+ change.total_fail_count = self.GetCLActionCount(
+ change, CQ_PIPELINE_CONFIGS, constants.CL_ACTION_KICKED_OUT,
+ latest_patchset_only=False)
+ change.fail_count = self.GetCLActionCount(
+ change, CQ_PIPELINE_CONFIGS, constants.CL_ACTION_KICKED_OUT)
+ change.pass_count = self.GetCLActionCount(
+ change, CQ_PIPELINE_CONFIGS, constants.CL_ACTION_SUBMIT_FAILED)
else:
# Slaves do not need to create transactions and should simply
@@ -2151,15 +2160,22 @@ class ValidationPool(object):
If self._metadata is None, then this function does nothing.
"""
- if self._metadata:
- timestamp = int(time.time())
- build_id = self._metadata.GetValue('build_id')
- for change in self.changes:
- self._metadata.RecordCLAction(change, constants.CL_ACTION_PICKED_UP,
- timestamp)
- # TODO(akeshet): If a separate query for each insert here becomes
- # a performance issue, consider batch inserting all the cl actions
- # with a single query.
+ if not self._metadata:
+ return
+
+ using_db = (cidb.CIDBConnectionFactory.IsCIDBSetup() and
+ cidb.CIDBConnectionFactory.GetCIDBConnectionForBuilder())
+
+ build_id = self._metadata.GetValue('build_id') if using_db else 0
+
+ timestamp = int(time.time())
+ for change in self.changes:
+ self._metadata.RecordCLAction(change, constants.CL_ACTION_PICKED_UP,
+ timestamp)
+ # TODO(akeshet): If a separate query for each insert here becomes
+ # a performance issue, consider batch inserting all the cl actions
+ # with a single query.
+ if using_db:
ValidationPool._InsertCLActionToDatabase(build_id, change,
constants.CL_ACTION_PICKED_UP)
@@ -2319,11 +2335,6 @@ class ValidationPool(object):
FailedToSubmitAllChangesNonFatalException: if we can't submit a change
due to non-fatal errors.
"""
- # Mark all changes as successful.
- inputs = [[self.bot, change, self.STATUS_PASSED, self.dryrun]
- for change in self.changes]
- parallel.RunTasksInProcessPool(self.UpdateCLStatus, inputs)
-
# Note that SubmitChanges can throw an exception if it can't
# submit all changes; in that particular case, don't mark the inflight
# failures patches as failed in gerrit- some may apply next time we do
@@ -2511,17 +2522,15 @@ class ValidationPool(object):
submit = all(ShouldSubmitChangeInPreCQ(self.build_root, change)
for change in self.changes)
new_status = self.STATUS_READY_TO_SUBMIT if submit else self.STATUS_PASSED
- new_action = (constants.CL_ACTION_PRE_CQ_READY_TO_SUBMIT if submit
- else constants.CL_ACTION_PRE_CQ_PASSED)
ok_statuses = (self.STATUS_PASSED, self.STATUS_READY_TO_SUBMIT)
+ build_id = self._metadata.GetValue('build_id')
+
def ProcessChange(change):
- if self.GetCLStatus(self.bot, change) not in ok_statuses:
+ if self.GetCLPreCQStatus(change) not in ok_statuses:
self.SendNotification(change, msg)
- self.UpdateCLStatus(PRE_CQ, change, new_status, self.dryrun)
if self._metadata:
timestamp = int(time.time())
- build_id = self._metadata.GetValue('build_id')
# Record both a VERIFIED action for this builder, and also a pre-CQ
# PASSED status. In the future, not all pre-cq builders will
# necessarily write a PASSED status. Instead, the pre-cq-launcher will
@@ -2531,11 +2540,7 @@ class ValidationPool(object):
timestamp)
ValidationPool._InsertCLActionToDatabase(build_id, change,
constants.CL_ACTION_VERIFIED)
- # Mark changes with the new pre-cq status in database.
- # This will be replaced by a call to UpdateCLPreCQStatus in a future
- # CL.
- ValidationPool._InsertCLActionToDatabase(
- build_id, change, new_action)
+ self.UpdateCLPreCQStatus(change, new_status, build_id)
# Set the new statuses in parallel.
inputs = [[change] for change in self.changes]
@@ -2667,11 +2672,6 @@ class ValidationPool(object):
if change in suspects:
self.RemoveCommitReady(change)
- # Mark the change as failed. If the Ready bit is still set, the change
- # will be retried automatically.
- self.UpdateCLStatus(self.bot, change, self.STATUS_FAILED,
- dry_run=self.dryrun)
-
def HandleValidationFailure(self, messages, changes=None, sanity=True,
no_stat=None):
"""Handles a list of validation failure messages from slave builders.
@@ -2700,7 +2700,7 @@ class ValidationPool(object):
candidates = []
for change in changes:
# Pre-CQ ignores changes that were already verified.
- if self.pre_cq and self.GetCLStatus(PRE_CQ, change) == self.STATUS_PASSED:
+ if self.pre_cq and self.GetCLPreCQStatus(change) == self.STATUS_PASSED:
continue
candidates.append(change)
@@ -2745,7 +2745,7 @@ class ValidationPool(object):
self.RemoveCommitReady(change)
def _HandleApplySuccess(self, change):
- """Handler for when Paladin successfully applies a change.
+ """Handler for when Paladin successfully applies (picks up) a change.
This handler notifies a developer that their change is being tried as
part of a Paladin run defined by a build_log.
@@ -2754,73 +2754,51 @@ class ValidationPool(object):
change: GerritPatch instance to operate upon.
"""
if self.pre_cq:
- status = self.GetCLStatus(self.bot, change)
+ status = self.GetCLPreCQStatus(change)
if status == self.STATUS_PASSED:
return
msg = ('%(queue)s has picked up your change. '
'You can follow along at %(build_log)s .')
self.SendNotification(change, msg)
- if not self.pre_cq or status == self.STATUS_LAUNCHING:
- self.UpdateCLStatus(self.bot, change, self.STATUS_INFLIGHT,
- dry_run=self.dryrun)
+
@classmethod
- def GetCLStatusURL(cls, bot, change, latest_patchset_only=True):
- """Get the status URL for |change| on |bot|.
+ def GetCLPreCQStatus(cls, change):
+ """Get the status for |change| on |bot|.
+
+ To ensure that the latest status is used, the action cache for |change|
+ will be filled.
Args:
- bot: Which bot to look at. Can be CQ or PRE_CQ.
change: GerritPatch instance to operate upon.
- latest_patchset_only: If True, return the URL for tracking the latest
- patchset. If False, return the URL for tracking all patchsets. Defaults
- to True.
Returns:
- The status URL, as a string.
+ The status, as a string, or None if there is no recorded pre-cq status.
"""
- internal = 'int' if change.internal else 'ext'
- components = [constants.MANIFEST_VERSIONS_GS_URL, bot,
- internal, str(change.gerrit_number)]
- if latest_patchset_only:
- components.append(str(change.patch_number))
- return '/'.join(components)
+ # Always refresh the action cache, so we get the latest status.
+ cls._FillCLActionCache([change])
- @classmethod
- def GetCLStatus(cls, bot, change):
- """Get the status for |change| on |bot|.
+ patch_number = int(change.patch_number)
- Args:
- change: GerritPatch instance to operate upon.
- bot: Which bot to look at. Can be CQ or PRE_CQ.
+ # Filter out actions to other patch numbers and actions that are not
+ # pre-cq status actions.
+ actions_for_patch = [a for a in cls._CL_ACTION_HISTORY_CACHE[change]
+ if a['patch_number'] == patch_number and
+ a['action'] in cls._PRECQ_ACTION_TO_STATUS]
- Returns:
- The status, as a string.
- """
- url = cls.GetCLStatusURL(bot, change)
- ctx = gs.GSContext()
- try:
- return ctx.Cat('%s/status' % url)
- except gs.GSNoSuchKey:
- logging.debug('No status yet for %r', url)
+ if not actions_for_patch:
+ logging.debug('No status yet for %s', change)
return None
- @classmethod
- def UpdateCLStatus(cls, bot, change, status, dry_run, build_id=None):
- """Update the |status| of |change| on |bot|.
+ return cls._TranslatePreCQActionToStatus(actions_for_patch[-1]['action'])
- For the pre-cq-launcher bot, if |build_id| is specified, this also writes
- a cl action indicating the status change to cidb (if cidb is in use).
- """
- for latest_patchset_only in (False, True):
- url = cls.GetCLStatusURL(bot, change, latest_patchset_only)
- ctx = gs.GSContext(dry_run=dry_run)
- ctx.Copy('-', '%s/status' % url, input=status)
- ctx.Counter('%s/%s' % (url, status)).Increment()
- # Currently only pre-cq status changes are translated into cl actions.
- if bot == PRE_CQ and build_id is not None:
- action = ValidationPool._TranslatePreCQStatusToAction(status)
- ValidationPool._InsertCLActionToDatabase(build_id, change, action)
+ @classmethod
+ def UpdateCLPreCQStatus(cls, change, status, build_id):
+ """Update the pre-CQ |status| of |change|."""
+ action = ValidationPool._TranslatePreCQStatusToAction(status)
+ ValidationPool._InsertCLActionToDatabase(build_id, change, action)
+
@classmethod
def _TranslatePreCQStatusToAction(cls, status):
@@ -2832,69 +2810,86 @@ class ValidationPool(object):
Raises:
KeyError if |status| is not a known pre-cq status.
"""
- status_translation = {
- cls.STATUS_INFLIGHT: constants.CL_ACTION_PRE_CQ_INFLIGHT,
- cls.STATUS_PASSED: constants.CL_ACTION_PRE_CQ_PASSED,
- cls.STATUS_FAILED: constants.CL_ACTION_PRE_CQ_FAILED,
- cls.STATUS_LAUNCHING: constants.CL_ACTION_PRE_CQ_LAUNCHING,
- cls.STATUS_WAITING: constants.CL_ACTION_PRE_CQ_WAITING,
- cls.STATUS_READY_TO_SUBMIT: constants.CL_ACTION_PRE_CQ_READY_TO_SUBMIT
- }
- return status_translation[status]
+ return cls._PRECQ_STATUS_TO_ACTION[status]
@classmethod
- def GetCLStatusCount(cls, bot, change, status, latest_patchset_only=True):
- """Return how many times |change| has been set to |status| on |bot|.
+ def _TranslatePreCQActionToStatus(cls, action):
+ """Translate a cl |action| into a pre-cq status.
+
+ Returns:
+ A pre-cq status string corresponding to the given |action|.
+
+ Raises:
+ KeyError if |status| is not a known pre-cq status.
+ """
+ return cls._PRECQ_ACTION_TO_STATUS[action]
+
+
+ @classmethod
+ def GetCLActionCount(cls, change, configs, action, latest_patchset_only=True):
+ """Return how many times |action| has occured on |change|.
+
+ If |change|'s action history cache has not already been filled by a call
+ to _FillCLActionCache, it will be filled.
Args:
- bot: Which bot to look at. Can be CQ or PRE_CQ.
+ configs: List or set of config names to consider.
change: GerritPatch instance to operate upon.
- status: The status string to look for.
- latest_patchset_only: If True, only how many times the latest patchset has
- been set to |status|. If False, count how many times any patchset has
- been set to |status|. Defaults to False.
+ action: The action string to look for.
+ latest_patchset_only: If True, only count actions that occured to the
+ latest patch number. Note, this may be different than the patch
+ number specified in |change|. Default: True.
Returns:
- The number of times |change| has been set to |status| on |bot|, as an
- integer.
+ The count of how many times |action| occured on |change| by the given
+ |config|.
"""
- cache_key = (bot, change, status, latest_patchset_only)
- if cache_key not in cls._CL_STATUS_CACHE:
- base_url = cls.GetCLStatusURL(bot, change, latest_patchset_only)
- url = '%s/%s' % (base_url, status)
- cls._CL_STATUS_CACHE[cache_key] = gs.GSContext().Counter(url).Get()
- return cls._CL_STATUS_CACHE[cache_key]
+ if change not in cls._CL_ACTION_HISTORY_CACHE:
+ cls._FillCLActionCache([change])
+
+ actions_for_change = cls._CL_ACTION_HISTORY_CACHE[change]
+
+ if actions_for_change and latest_patchset_only:
+ latest_patch_number = max(a['patch_number'] for a in actions_for_change)
+ actions_for_change = [a for a in actions_for_change
+ if a['patch_number'] == latest_patch_number]
+
+ actions_for_change = [a for a in actions_for_change
+ if (a['build_config'] in configs and
+ a['action'] == action)]
+
+ return len(actions_for_change)
+
@classmethod
- def FillCLStatusCache(cls, bot, changes, statuses=None):
- """Cache all of the stats about the given |changes| in parallel.
+ def _FillCLActionCache(cls, changes):
+ """Cache action history of |changes|.
+
+ If no cidb connection is set up, or a None connection has been set up,
+ pretend that all the |changes| have an empty list of actions.
Args:
- bot: Bot to pull down stats for.
- changes: Changes to cache.
- statuses: Statuses to cache. By default, cache the PASSED and FAILED
- counts.
- """
- if statuses is None:
- statuses = (cls.STATUS_PASSED, cls.STATUS_FAILED)
- inputs = []
+ changes: Changes to cache, of type GerritPatch.
+ """
+ if (not cidb.CIDBConnectionFactory.IsCIDBSetup() or
+ not cidb.CIDBConnectionFactory.GetCIDBConnectionForBuilder()):
+ for c in changes:
+ cls._CL_ACTION_HISTORY_CACHE[c] = []
+ return
+
+ db = cidb.CIDBConnectionFactory.GetCIDBConnectionForBuilder()
+ # Fetch action history for changes, serially. If this becomes a
+ # performance concern we can consider parallelizing.
for change in changes:
- for status in statuses:
- for latest_patchset_only in (False, True):
- cache_key = (bot, change, status, latest_patchset_only)
- if cache_key not in cls._CL_STATUS_CACHE:
- inputs.append(cache_key)
+ cls._CL_ACTION_HISTORY_CACHE[change] = db.GetActionsForChange(change)
+
+
+ @classmethod
+ def ClearActionCache(cls):
+ """Clear action history cache."""
+ cls._CL_ACTION_HISTORY_CACHE = {}
- with parallel.Manager() as manager:
- # Grab the CL status of all of the CLs in the background, into a proxied
- # dictionary.
- cls._CL_STATUS_CACHE = manager.dict(cls._CL_STATUS_CACHE)
- parallel.RunTasksInProcessPool(cls.GetCLStatusCount, inputs)
-
- # Convert the cache back into a regular dictionary before we shut down
- # the manager.
- cls._CL_STATUS_CACHE = dict(cls._CL_STATUS_CACHE)
def CreateDisjointTransactions(self, manifest, max_txn_length=None):
"""Create a list of disjoint transactions from the changes in the pool.
diff --git a/cbuildbot/validation_pool_unittest.py b/cbuildbot/validation_pool_unittest.py
index 23ba157de..2a7ce6f27 100755
--- a/cbuildbot/validation_pool_unittest.py
+++ b/cbuildbot/validation_pool_unittest.py
@@ -30,8 +30,10 @@ from chromite.cbuildbot import metadata_lib
from chromite.cbuildbot import repository
from chromite.cbuildbot import tree_status
from chromite.cbuildbot import validation_pool
+from chromite.lib import cidb
from chromite.lib import cros_build_lib
from chromite.lib import cros_test_lib
+from chromite.lib import fake_cidb
from chromite.lib import gerrit
from chromite.lib import gob_util
from chromite.lib import gs
@@ -87,6 +89,11 @@ class Base(cros_test_lib.MockTestCase):
self.PatchObject(tree_status, 'IsTreeOpen', return_value=True)
self.PatchObject(tree_status, 'WaitForTreeStatus',
return_value=constants.TREE_OPEN)
+ self.fake_db = fake_cidb.FakeCIDBConnection()
+ cidb.CIDBConnectionFactory.SetupMockCidb(self.fake_db)
+
+ def tearDown(self):
+ cidb.CIDBConnectionFactory.ClearMock()
def MockPatch(self, change_id=None, patch_number=None, is_merged=False,
project='chromiumos/chromite', remote=constants.EXTERNAL_REMOTE,
@@ -711,7 +718,7 @@ class ValidationFailureOrTimeout(MoxBase):
self._pool = MakePool(changes=self._patches)
self.PatchObject(
- validation_pool.ValidationPool, 'GetCLStatus',
+ validation_pool.ValidationPool, 'GetCLPreCQStatus',
return_value=validation_pool.ValidationPool.STATUS_PASSED)
self.PatchObject(
validation_pool.CalculateSuspects, 'FindSuspects',
@@ -721,7 +728,7 @@ class ValidationFailureOrTimeout(MoxBase):
return_value=self._PATCH_MESSAGE)
self.PatchObject(validation_pool.ValidationPool, 'SendNotification')
self.PatchObject(validation_pool.ValidationPool, 'RemoveCommitReady')
- self.PatchObject(validation_pool.ValidationPool, 'UpdateCLStatus')
+ self.PatchObject(validation_pool.ValidationPool, 'UpdateCLPreCQStatus')
self.PatchObject(validation_pool.ValidationPool, 'ReloadChanges',
return_value=self._patches)
self.PatchObject(validation_pool.CalculateSuspects, 'OnlyLabFailures',
@@ -1342,23 +1349,127 @@ class TestFindSuspects(MoxBase):
self.assertEquals(candidates, changes[1:])
-class TestCLStatus(MoxBase):
- """Tests methods that get the CL status."""
-
+class TestPrintLinks(MoxBase):
+ """Tests that change links can be printed."""
def testPrintLinks(self):
changes = self.GetPatches(3)
with parallel_unittest.ParallelMock():
validation_pool.ValidationPool.PrintLinksToChanges(changes)
- def testStatusCache(self):
- validation_pool.ValidationPool._CL_STATUS_CACHE = {}
- changes = self.GetPatches(3)
- with parallel_unittest.ParallelMock():
- validation_pool.ValidationPool.FillCLStatusCache(validation_pool.CQ,
- changes)
- self.assertEqual(len(validation_pool.ValidationPool._CL_STATUS_CACHE), 12)
- validation_pool.ValidationPool.PrintLinksToChanges(changes)
- self.assertEqual(len(validation_pool.ValidationPool._CL_STATUS_CACHE), 12)
+
+class TestCLPreCQStatus(MoxBase):
+ """Tests methods related to CL pre-CQ status."""
+ def testGetAndUpdateCLPreCQStatus(self):
+ change = self.GetPatches()
+ # Initial pre-CQ status of a change is None.
+ self.assertEqual(validation_pool.ValidationPool.GetCLPreCQStatus(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')
+ validation_pool.ValidationPool.UpdateCLPreCQStatus(
+ change, validation_pool.ValidationPool.STATUS_WAITING, build_id)
+ self.assertEqual(validation_pool.ValidationPool.GetCLPreCQStatus(change),
+ validation_pool.ValidationPool.STATUS_WAITING)
+
+ validation_pool.ValidationPool.UpdateCLPreCQStatus(
+ change, validation_pool.ValidationPool.STATUS_INFLIGHT, build_id)
+ self.assertEqual(validation_pool.ValidationPool.GetCLPreCQStatus(change),
+ validation_pool.ValidationPool.STATUS_INFLIGHT)
+
+ # Updating to an invalid status should raise a KeyError, and leave status
+ # unaffected.
+ with self.assertRaises(KeyError):
+ validation_pool.ValidationPool.UpdateCLPreCQStatus(
+ change, 'invalid status', build_id)
+ self.assertEqual(validation_pool.ValidationPool.GetCLPreCQStatus(change),
+ validation_pool.ValidationPool.STATUS_INFLIGHT)
+
+ # Recording a cl action that is not a valid pre-cq status should leave
+ # pre-cq status unaffected.
+ self.fake_db.InsertCLActions(
+ build_id, [metadata_lib.GetCLActionTuple(change, 'unknown action')])
+ self.assertEqual(validation_pool.ValidationPool.GetCLPreCQStatus(change),
+ validation_pool.ValidationPool.STATUS_INFLIGHT)
+
+
+class TestCLStatusCounter(MoxBase):
+ """Tests that GetCLActionCount behaves as expected."""
+
+ def setUp(self):
+ validation_pool.ValidationPool.ClearActionCache()
+
+ def tearDown(self):
+ validation_pool.ValidationPool.ClearActionCache()
+
+ 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.
+ self.assertEqual(
+ 0,
+ validation_pool.ValidationPool.GetCLActionCount(
+ c1p1, validation_pool.CQ_PIPELINE_CONFIGS,
+ constants.CL_ACTION_KICKED_OUT))
+
+ # 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,
+ [metadata_lib.GetCLActionTuple(c1p1, constants.CL_ACTION_KICKED_OUT)])
+ self.fake_db.InsertCLActions(
+ precq_build_id,
+ [metadata_lib.GetCLActionTuple(c1p1, constants.CL_ACTION_PICKED_UP)])
+ self.fake_db.InsertCLActions(
+ precq_build_id,
+ [metadata_lib.GetCLActionTuple(c1p1, constants.CL_ACTION_KICKED_OUT)])
+ self.fake_db.InsertCLActions(
+ melon_build_id,
+ [metadata_lib.GetCLActionTuple(c1p1, constants.CL_ACTION_KICKED_OUT)])
+
+ # Clear action cache so that it gets refilled.
+ validation_pool.ValidationPool.ClearActionCache()
+
+ self.assertEqual(
+ 2,
+ validation_pool.ValidationPool.GetCLActionCount(
+ c1p1, validation_pool.CQ_PIPELINE_CONFIGS,
+ constants.CL_ACTION_KICKED_OUT))
+
+ # 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,
+ [metadata_lib.GetCLActionTuple(c1p2, constants.CL_ACTION_KICKED_OUT)])
+
+ validation_pool.ValidationPool.ClearActionCache()
+
+ self.assertEqual(
+ 1,
+ validation_pool.ValidationPool.GetCLActionCount(
+ c1p1, validation_pool.CQ_PIPELINE_CONFIGS,
+ constants.CL_ACTION_KICKED_OUT))
+ self.assertEqual(
+ 1,
+ validation_pool.ValidationPool.GetCLActionCount(
+ c1p2, validation_pool.CQ_PIPELINE_CONFIGS,
+ constants.CL_ACTION_KICKED_OUT))
+ self.assertEqual(
+ 3,
+ validation_pool.ValidationPool.GetCLActionCount(
+ c1p2, validation_pool.CQ_PIPELINE_CONFIGS,
+ constants.CL_ACTION_KICKED_OUT,
+ latest_patchset_only=False))
class TestCreateValidationFailureMessage(Base):
diff --git a/lib/cidb.py b/lib/cidb.py
index 757770b80..d3396251d 100644
--- a/lib/cidb.py
+++ b/lib/cidb.py
@@ -726,8 +726,8 @@ class CIDBConnection(SchemaVersionedMySQLConnection):
Note, this includes all patches of the given change.
Args:
- change: A GerritChangeTuple or GerritPatchTuple specifing the
- change.
+ change: A GerritChangeTuple, GerritPatchTuple or GerritPatch
+ specifying the change.
Returns:
A list of actions, in timestamp order, each of which is a dict
@@ -856,6 +856,20 @@ class CIDBConnectionFactory(object):
@classmethod
+ def ClearMock(cls):
+ """Clear a mock CIDB object.
+
+ This method clears a cidb mock object, but leaves the connection factory
+ in _CONNECTION_TYPE_MOCK, so that future calls to set up a non-mock
+ cidb will fail.
+ """
+ assert cls._ConnectionType == cls._CONNECTION_TYPE_MOCK, (
+ 'CIDB is not set for mock use.')
+ cls._ConnectionIsSetup = False
+ cls._MockCIDB = None
+
+
+ @classmethod
def GetCIDBConnectionForBuilder(cls):
"""Get a CIDBConnection.
diff --git a/lib/cidb_setup_unittest.py b/lib/cidb_setup_unittest.py
index 8917e786d..34a025a1c 100755
--- a/lib/cidb_setup_unittest.py
+++ b/lib/cidb_setup_unittest.py
@@ -67,7 +67,7 @@ class CIDBConnectionFactoryTest(cros_test_lib.MoxTestCase):
cidb.CIDBConnectionFactory.GetCIDBConnectionForBuilder)
def testSetupMock(self):
- """Test that SetupDebug behaves as expected."""
+ """Test that SetupMock behaves as expected."""
# Set the CIDB to mock mode, but without supplying a mock
cidb.CIDBConnectionFactory.SetupMockCidb()
self.assertFalse(cidb.CIDBConnectionFactory.IsCIDBSetup())
@@ -91,6 +91,11 @@ class CIDBConnectionFactoryTest(cros_test_lib.MoxTestCase):
self.assertEqual(cidb.CIDBConnectionFactory.GetCIDBConnectionForBuilder(),
b)
+ # Mock object can be cleared by future ClearMock call.
+ cidb.CIDBConnectionFactory.ClearMock()
+ self.assertRaises(AssertionError,
+ cidb.CIDBConnectionFactory.GetCIDBConnectionForBuilder)
+
# Calls to non-mock Setup methods should still fail.
self.assertRaises(AssertionError, cidb.CIDBConnectionFactory.SetupProdCidb)
self.assertRaises(AssertionError, cidb.CIDBConnectionFactory.SetupDebugCidb)