diff options
-rwxr-xr-x | cbuildbot/cbuildbot_config.py | 6 | ||||
-rw-r--r-- | cbuildbot/constants.py | 23 | ||||
-rwxr-xr-x | cbuildbot/stages/report_stages_unittest.py | 2 | ||||
-rw-r--r-- | cbuildbot/stages/sync_stages.py | 50 | ||||
-rwxr-xr-x | cbuildbot/stages/sync_stages_unittest.py | 180 | ||||
-rw-r--r-- | cbuildbot/validation_pool.py | 299 | ||||
-rwxr-xr-x | cbuildbot/validation_pool_unittest.py | 139 | ||||
-rw-r--r-- | lib/cidb.py | 18 | ||||
-rwxr-xr-x | lib/cidb_setup_unittest.py | 7 |
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) |