blob: 1e6cd222b138acbd1e376de1fc620bfd8496d95d [file] [log] [blame]
# -*- coding: utf-8 -*-
# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Unittests for sync stages."""
from __future__ import print_function
import pickle
import datetime
import itertools
import json
import os
import time
import tempfile
import mock
from chromite.cbuildbot import commands
from chromite.cbuildbot import lkgm_manager
from chromite.cbuildbot import manifest_version
from chromite.cbuildbot import manifest_version_unittest
from chromite.cbuildbot import patch_series
from chromite.cbuildbot import repository
from chromite.cbuildbot import trybot_patch_pool
from chromite.cbuildbot import validation_pool
from chromite.cbuildbot.stages import generic_stages_unittest
from chromite.cbuildbot.stages import sync_stages
from chromite.lib import auth
from chromite.lib import buildbucket_lib
from chromite.lib import build_requests
from chromite.lib import cidb
from chromite.lib import clactions
from chromite.lib import cl_messages
from chromite.lib import config_lib
from chromite.lib import constants
from chromite.lib import cq_config
from chromite.lib import cros_test_lib
from chromite.lib import fake_cidb
from chromite.lib import failures_lib
from chromite.lib import gerrit
from chromite.lib import git
from chromite.lib import git_unittest
from chromite.lib import gob_util
from chromite.lib import metadata_lib
from chromite.lib import osutils
from chromite.lib import patch as cros_patch
from chromite.lib import patch_unittest
from chromite.lib.buildstore import FakeBuildStore
# It's normal for unittests to access protected members.
# pylint: disable=protected-access
class BootstrapStageTest(generic_stages_unittest.AbstractStageTestCase,
cros_test_lib.RunCommandTestCase):
"""Tests the Bootstrap stage."""
BOT_ID = 'sync-test-cbuildbot'
RELEASE_TAG = ''
def setUp(self):
# Pretend API version is always current.
self.PatchObject(
commands, 'GetTargetChromiteApiVersion',
return_value=(constants.REEXEC_API_MAJOR, constants.REEXEC_API_MINOR))
self._Prepare()
self.buildstore = FakeBuildStore()
def ConstructStage(self):
patch_pool = trybot_patch_pool.TrybotPatchPool()
return sync_stages.BootstrapStage(self._run, self.buildstore, patch_pool)
def testSimpleBootstrap(self):
"""Verify Bootstrap behavior in a simple case (with a branch)."""
self.RunStage()
# Clone next chromite checkout.
self.assertCommandContains([
'git',
'clone',
constants.CHROMITE_URL,
mock.ANY, # Can't predict new chromium checkout diretory.
'--reference',
mock.ANY
])
# Switch to the test branch.
self.assertCommandContains(['git', 'checkout', 'ooga_booga'])
# Re-exec cbuildbot. We mostly only want to test the CL options Bootstrap
# changes.
# '--sourceroot=%s'
# '--test-bootstrap'
# '--nobootstrap'
# '--manifest-repo-url'
self.assertCommandContains([
'chromite/cbuildbot/cbuildbot',
'sync-test-cbuildbot',
'-r',
os.path.join(self.tempdir, 'buildroot'),
'--buildbot',
'--noprebuilts',
'--buildnumber',
'1234321',
'--branch',
'ooga_booga',
'--sourceroot',
mock.ANY,
'--nobootstrap',
])
class ManifestVersionedSyncStageTest(
generic_stages_unittest.AbstractStageTestCase):
"""Tests the ManifestVersionedSync stage."""
# pylint: disable=abstract-method
def setUp(self):
self.source_repo = 'ssh://source/repo'
self.manifest_version_url = 'fake manifest url'
self.branch = 'master'
self.build_name = 'amd64-generic'
self.incr_type = 'branch'
self.next_version = 'next_version'
self.sync_stage = None
self.buildstore = FakeBuildStore()
self.repo = repository.RepoRepository(self.source_repo, self.tempdir,
self.branch)
self.manager = manifest_version.BuildSpecsManager(
self.repo,
self.manifest_version_url, [self.build_name],
self.incr_type,
force=False,
branch=self.branch,
buildstore=self.buildstore,
dry_run=True)
self._Prepare()
# Our API here is not great when it comes to kwargs passing.
def _Prepare(self, bot_id=None, **kwargs): # pylint: disable=arguments-differ
super(ManifestVersionedSyncStageTest, self)._Prepare(bot_id, **kwargs)
self._run.config['manifest_version'] = self.manifest_version_url
self.sync_stage = sync_stages.ManifestVersionedSyncStage(
self._run, self.buildstore)
self.sync_stage.manifest_manager = self.manager
self._run.attrs.manifest_manager = self.manager
def testManifestVersionedSyncOnePartBranch(self):
"""Tests basic ManifestVersionedSyncStage with branch ooga_booga"""
self.PatchObject(sync_stages.ManifestVersionedSyncStage, 'Initialize')
self.PatchObject(sync_stages.ManifestVersionedSyncStage,
'_SetAndroidVersionIfApplicable')
self.PatchObject(sync_stages.ManifestVersionedSyncStage,
'_SetChromeVersionIfApplicable')
self.PatchObject(
manifest_version.BuildSpecsManager,
'GetNextBuildSpec',
return_value=self.next_version)
self.PatchObject(manifest_version.BuildSpecsManager, 'GetLatestPassingSpec')
self.PatchObject(
sync_stages.SyncStage,
'ManifestCheckout',
return_value=self.next_version)
self.PatchObject(
sync_stages.ManifestVersionedSyncStage,
'_GetMasterVersion',
return_value='foo',
autospec=True)
self.PatchObject(
manifest_version.BuildSpecsManager,
'BootstrapFromVersion',
autospec=True)
self.PatchObject(repository.RepoRepository, 'Sync', autospec=True)
self.sync_stage.Run()
def testInitialize(self):
self.PatchObject(sync_stages.SyncStage, '_InitializeRepo')
self.sync_stage.repo = self.repo
self.sync_stage.Initialize()
class MockPatch(mock.MagicMock):
"""MagicMock for a GerritPatch-like object."""
gerrit_number = '1234'
patch_number = '1'
project = 'chromiumos/chromite'
status = 'NEW'
internal = False
current_patch_set = {
'number': patch_number,
'draft': False,
}
patch_dict = {
'currentPatchSet': current_patch_set,
}
remote = 'cros'
mock_diff_status = {}
def __init__(self, *args, **kwargs):
super(MockPatch, self).__init__(*args, **kwargs)
# Flags can vary per-patch.
self.flags = {
'CRVW': '2',
'VRIF': '1',
'COMR': '1',
'LCQ': '1',
}
def HasApproval(self, field, allowed):
"""Pretends the patch is good.
Pretend the patch has all of the values listed in
constants.DEFAULT_CQ_READY_FIELDS, but not any other fields.
Args:
field: The name of the field as a string. 'CRVW', etc.
allowed: Value, or list of values that are acceptable expressed as
strings.
"""
flag_value = self.flags.get(field, 0)
if isinstance(allowed, (tuple, list)):
return flag_value in allowed
else:
return flag_value == allowed
def IsDraft(self):
"""Return whether this patch is a draft patchset."""
return self.current_patch_set['draft']
def IsBeingMerged(self):
"""Return whether this patch is merged or in the middle of being merged."""
return self.status in ('SUBMITTED', 'MERGED')
def IsMergeable(self):
"""Default implementation of IsMergeable, stubbed out by some tests."""
return True
def GetDiffStatus(self, _):
return self.mock_diff_status
class SyncStageTest(generic_stages_unittest.AbstractStageTestCase):
"""Tests the SyncStage."""
BOT_ID = 'master-paladin'
def setUp(self):
self.PatchObject(
buildbucket_lib, 'GetServiceAccount', return_value='server_account')
self.PatchObject(auth.AuthorizedHttp, '__init__', return_value=None)
self.PatchObject(
buildbucket_lib.BuildbucketClient,
'_GetHost',
return_value=buildbucket_lib.BUILDBUCKET_TEST_HOST)
# Create and set up a fake cidb instance.
self.fake_db = fake_cidb.FakeCIDBConnection()
cidb.CIDBConnectionFactory.SetupMockCidb(self.fake_db)
self._Prepare()
self.buildstore = FakeBuildStore()
def ConstructStage(self):
return sync_stages.SyncStage(self._run, self.buildstore)
def testWriteChangesToMetadata(self):
"""Test whether WriteChangesToMetadata can handle duplicates properly."""
change_1 = cros_patch.GerritFetchOnlyPatch(
'https://host/chromite/tacos', 'chromite/tacos',
'refs/changes/11/12345/4', 'master', 'cros-internal',
'7181e4b5e182b6f7d68461b04253de095bad74f9',
'I47ea30385af60ae4cc2acc5d1a283a46423bc6e1', '12345', '4',
'foo@chromium.org', 1, 1, 3)
change_2 = cros_patch.GerritFetchOnlyPatch(
'https://host/chromite/foo', 'chromite/foo', 'refs/changes/11/12344/3',
'master', 'cros-internal', 'cf23df2207d99a74fbe169e3eba035e633b65d94',
'Iab9bf08b9b9bd4f72721cfc36e843ed302aca11a', '12344', '3',
'foo@chromium.org', 0, 0, 1)
stage = self.ConstructStage()
stage.WriteChangesToMetadata([change_1, change_1, change_2])
# Test whether the sort function works.
expected = [change_2.GetAttributeDict(), change_1.GetAttributeDict()]
result = self._run.attrs.metadata.GetValue('changes')
self.assertEqual(expected, result)
class BaseCQTestCase(generic_stages_unittest.StageTestCase):
"""Helper class for testing the CommitQueueSync stage"""
MANIFEST_CONTENTS = '<manifest/>'
def setUp(self):
"""Setup patchers for specified bot id."""
self._patch_factory = patch_unittest.MockPatchFactory()
# Mock out methods as needed.
self.PatchObject(lkgm_manager, 'GenerateBlameList')
self.PatchObject(
repository.RepoRepository,
'ExportManifest',
return_value=self.MANIFEST_CONTENTS,
autospec=True)
self.PatchObject(sync_stages.SyncStage, 'WriteChangesToMetadata')
self.StartPatcher(git_unittest.ManifestMock())
self.StartPatcher(git_unittest.ManifestCheckoutMock())
version_file = os.path.join(self.build_root, constants.VERSION_FILE)
manifest_version_unittest.VersionInfoTest.WriteFakeVersionFile(version_file)
rc_mock = self.StartPatcher(cros_test_lib.RunCommandMock())
rc_mock.SetDefaultCmdResult()
# Block the CQ from contacting GoB.
self.PatchObject(gerrit.GerritHelper, 'RemoveReady')
self.PatchObject(cl_messages.PaladinMessage, 'Send')
self.PatchObject(
validation_pool.ValidationPool, 'SubmitChanges', return_value=({}, {}))
# If a test is still contacting GoB, something is busted.
self.PatchObject(
gob_util,
'CreateHttpConn',
side_effect=AssertionError('Test should not contact GoB'))
self.PatchObject(
git, 'GitPush', side_effect=AssertionError('Test should not push.'))
self.PatchObject(
buildbucket_lib, 'GetServiceAccount', return_value='server_account')
self.PatchObject(auth.AuthorizedHttp, '__init__', return_value=None)
self.PatchObject(
buildbucket_lib.BuildbucketClient,
'SendBuildbucketRequest',
return_value=None)
self.PatchObject(
buildbucket_lib.BuildbucketClient,
'_GetHost',
return_value=buildbucket_lib.BUILDBUCKET_TEST_HOST)
# Create a fake repo / manifest on disk that is used by subclasses.
for subdir in ('repo', 'manifests'):
osutils.SafeMakedirs(os.path.join(self.build_root, '.repo', subdir))
self.manifest_path = os.path.join(self.build_root, '.repo', 'manifest.xml')
osutils.WriteFile(self.manifest_path, self.MANIFEST_CONTENTS)
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()
self.buildstore = FakeBuildStore(self.fake_db)
cidb.CIDBConnectionFactory.SetupMockCidb(self.fake_db)
self.sync_stage = None
self._Prepare()
def tearDown(self):
cidb.CIDBConnectionFactory.ClearMock()
# Our API here is not great when it comes to kwargs passing.
def _Prepare(self, bot_id=None, **kwargs): # pylint: disable=arguments-differ
super(BaseCQTestCase, self)._Prepare(bot_id, **kwargs)
self._run.config.overlays = constants.PUBLIC_OVERLAYS
self.sync_stage = sync_stages.CommitQueueSyncStage(self._run,
self.buildstore)
# BuildStart stage would have seeded the build.
build_id = self.buildstore.InsertBuild(
'test_builder',
666,
'test_config',
'test_hostname',
timeout_seconds=23456)
self._run.attrs.metadata.UpdateWithDict({'build_id': build_id})
def PerformSync(self,
committed=False,
num_patches=1,
pre_cq_status=constants.CL_STATUS_PASSED,
runs=0,
changes=None,
patch_objects=True,
**kwargs):
"""Helper to perform a basic sync for master commit queue.
Args:
committed: Value to be returned by mock patches' IsChangeCommitted.
Default: False.
num_patches: The number of mock patches to create. Default: 1.
pre_cq_status: PreCQ status for mock patches. Default: passed.
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.
changes: Optional list of MockPatch instances that should be available
in validation pool. If not specified, a set of |num_patches|
patches will be created.
patch_objects: If your test will call PerformSync more than once, set
this to false on subsequent calls to ensure that we do
not re-patch already patched methods with mocks.
**kwargs: Additional arguments to pass to MockPatch when creating patches.
Returns:
A list of MockPatch objects which were created and used in PerformSync.
"""
kwargs.setdefault(
'approval_timestamp',
time.time() - sync_stages.PreCQLauncherStage.LAUNCH_DELAY * 60)
changes = changes or [MockPatch(**kwargs)] * num_patches
if pre_cq_status is not None:
config = constants.PRE_CQ_DEFAULT_CONFIGS[0]
new_build_id = self.buildstore.InsertBuild('Pre cq group', 1, config,
'bot-hostname')
for change in changes:
action = clactions.TranslatePreCQStatusToAction(pre_cq_status)
self.fake_db.InsertCLActions(
new_build_id,
[clactions.CLAction.FromGerritPatchAndAction(change, action)])
if patch_objects:
self.PatchObject(
gerrit.GerritHelper,
'IsChangeCommitted',
return_value=committed,
autospec=True)
# Validation pool will mutate the return value it receives from
# Query, therefore return a copy of the changes list.
def Query(*_args, **_kwargs):
return list(changes)
self.PatchObject(
gerrit.GerritHelper, 'Query', side_effect=Query, autospec=True)
exit_it = itertools.chain([False] * runs, itertools.repeat(True))
self.PatchObject(
validation_pool.ValidationPool,
'ShouldExitEarly',
side_effect=exit_it)
self.sync_stage.PerformStage()
return changes
def ReloadPool(self):
"""Save the pool to disk and reload it."""
with tempfile.NamedTemporaryFile() as f:
pickle.dump(self.sync_stage.pool, f)
f.flush()
self._run.options.validation_pool = f.name
self.sync_stage = sync_stages.CommitQueueSyncStage(
self._run, self.buildstore)
self.sync_stage.HandleSkip()
class SlaveCQSyncTest(BaseCQTestCase):
"""Tests the CommitQueueSync stage for the paladin slaves."""
BOT_ID = 'eve-paladin'
MILESTONE_VERSION = '10'
def setUp(self):
self._run.options.master_buildbucket_id = 1234
self._run.options.master_build_id = self.buildstore.InsertBuild(
'master builder name', 1, 'master-paladin', 'bot hostname',
buildbucket_id=1234)
self.fake_db.UpdateMetadata(
self._run.options.master_build_id,
{'version': {
'milestone': self.MILESTONE_VERSION
}})
self.PatchObject(
sync_stages.ManifestVersionedSyncStage,
'_GetMasterVersion',
return_value='foo',
autospec=True)
self.PatchObject(
lkgm_manager.LKGMManager,
'BootstrapFromVersion',
return_value=self.manifest_path,
autospec=True)
self.PatchObject(repository.RepoRepository, 'Sync', autospec=True)
def testReload(self):
"""Test basic ability to sync and reload the patches from disk."""
self.sync_stage.PerformStage()
self.ReloadPool()
def testSupplantedMaster(self):
"""Test that stage fails if master has been supplanted."""
new_master_build_id = self.buildstore.InsertBuild(
'master builder name', 2, 'master-paladin', 'bot hostname')
self.fake_db.UpdateMetadata(
new_master_build_id, {'version': {
'milestone': self.MILESTONE_VERSION
}})
with self.assertRaises(failures_lib.MasterSlaveVersionMismatchFailure):
self.sync_stage.PerformStage()
def testSupplantedMasterDifferentBranch(self):
"""Test that master supplanting logic respects branch.
The master-was-supplanted logic should ignore masters for different
milestone version.
"""
self.buildstore.InsertBuild(
'master builder name', 2, 'master-paladin', 'bot hostname',
branch='foo')
self.sync_stage.PerformStage()
class MasterCQSyncTestCase(BaseCQTestCase):
"""Helper class for testing the CommitQueueSync stage masters."""
BOT_ID = 'master-paladin'
def setUp(self):
"""Setup patchers for specified bot id."""
self.AutoPatch([[validation_pool.ValidationPool, 'ApplyPoolIntoRepo']])
self.PatchObject(
lkgm_manager.LKGMManager,
'CreateNewCandidate',
return_value=self.manifest_path,
autospec=True)
self.PatchObject(
lkgm_manager.LKGMManager,
'CreateFromManifest',
return_value=self.manifest_path,
autospec=True)
def _testCommitNonManifestChange(self, **kwargs):
"""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)
kwargs.setdefault('tracking_branch', 'foo')
return self.PerformSync(**kwargs)
def _testFailedCommitOfNonManifestChange(self):
"""Test what happens when 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, changes=None, **kwargs):
"""Test committing a change to a project that's part of the manifest.
Args:
changes: Optional list of MockPatch instances to use in PerformSync.
Returns:
List of MockPatch objects that were used in PerformSync
"""
self.PatchObject(
validation_pool.ValidationPool,
'_FilterNonLcqProjects',
side_effect=lambda x, _: (x, []))
return self.PerformSync(changes=changes, **kwargs)
def _testDefaultSync(self):
"""Test basic ability to sync with standard options.
Returns:
List of MockPatch objects that were used in PerformSync
"""
return self.PerformSync()
class MasterCQSyncTest(MasterCQSyncTestCase):
"""Tests the CommitQueueSync stage for the paladin masters."""
def testCommitNonManifestChange(self):
"""See MasterCQSyncTestCase"""
changes = self._testCommitNonManifestChange()
self.assertCountEqual(self.sync_stage.pool.candidates, changes)
self.assertCountEqual(self.sync_stage.pool.non_manifest_changes, [])
def testFailedCommitOfNonManifestChange(self):
"""See MasterCQSyncTestCase"""
changes = self._testFailedCommitOfNonManifestChange()
self.assertCountEqual(self.sync_stage.pool.candidates, changes)
self.assertCountEqual(self.sync_stage.pool.non_manifest_changes, [])
def testCommitManifestChange(self):
"""See MasterCQSyncTestCase"""
changes = self._testCommitManifestChange()
self.assertCountEqual(self.sync_stage.pool.candidates, changes)
self.assertCountEqual(self.sync_stage.pool.non_manifest_changes, [])
def testCommitManifestChangeWithoutPreCQ(self):
"""Changes get ignored if they aren't approved by pre-cq."""
self._testCommitManifestChange(pre_cq_status=None)
self.assertCountEqual(self.sync_stage.pool.candidates, [])
self.assertCountEqual(self.sync_stage.pool.non_manifest_changes, [])
def testCommitManifestChangeWithoutPreCQAndOldPatches(self):
"""Changes get tested without pre-cq if the approval_timestamp is old."""
changes = self._testCommitManifestChange(
pre_cq_status=None, approval_timestamp=0)
self.assertCountEqual(self.sync_stage.pool.candidates, changes)
self.assertCountEqual(self.sync_stage.pool.non_manifest_changes, [])
def testDefaultSync(self):
"""See MasterCQSyncTestCase"""
changes = self._testDefaultSync()
self.assertCountEqual(self.sync_stage.pool.candidates, changes)
self.assertCountEqual(self.sync_stage.pool.non_manifest_changes, [])
def testReload(self):
"""Test basic ability to sync and reload the patches from disk."""
# Use zero patches because mock patches can't be pickled.
changes = self.PerformSync(num_patches=0, runs=0)
self.ReloadPool()
self.assertCountEqual(self.sync_stage.pool.candidates, changes)
self.assertCountEqual(self.sync_stage.pool.non_manifest_changes, [])
class PreCQLauncherStageTest(MasterCQSyncTestCase):
"""Tests for the PreCQLauncherStage."""
BOT_ID = constants.PRE_CQ_LAUNCHER_CONFIG
STATUS_LAUNCHING = constants.CL_STATUS_LAUNCHING
STATUS_WAITING = constants.CL_STATUS_WAITING
STATUS_FAILED = constants.CL_STATUS_FAILED
STATUS_READY_TO_SUBMIT = constants.CL_STATUS_READY_TO_SUBMIT
STATUS_INFLIGHT = constants.CL_STATUS_INFLIGHT
def setUp(self):
self.PatchObject(time, 'sleep', autospec=True)
self.PatchObject(
validation_pool.ValidationPool, 'HandlePreCQSuccess', autospec=True)
def _Prepare(self, bot_id=None, **kwargs):
self.build_id = self.buildstore.InsertBuild(
constants.PRE_CQ_LAUNCHER_NAME, 1, constants.PRE_CQ_LAUNCHER_CONFIG,
'bot-hostname')
super(PreCQLauncherStageTest, self)._Prepare(
bot_id, build_id=self.build_id, **kwargs)
self.sync_stage = sync_stages.PreCQLauncherStage(self._run, self.buildstore)
def testGetUpdatedActionHistoryAndStatusMaps(self):
"""Test _GetUpdatedActionHistoryAndStatusMaps."""
change = self._patch_factory.MockPatch()
self.fake_db.InsertCLActions(self.build_id, [
clactions.CLAction.FromGerritPatchAndAction(
change, constants.CL_ACTION_PRE_CQ_FAILED)
])
action_history, status_and_timestamp_map, status_map = (
self.sync_stage._GetUpdatedActionHistoryAndStatusMaps(
self.fake_db, [change]))
self.assertEqual(len(action_history), 1)
self.assertEqual(len(status_and_timestamp_map), 1)
self.assertEqual(len(status_map), 1)
self.assertEqual(action_history[0].action,
constants.CL_ACTION_PRE_CQ_FAILED)
self.assertEqual(status_map[change], constants.CL_STATUS_FAILED)
def testGetFailedPreCQConfigs(self):
"""Test _GetFailedPreCQConfigs."""
change = self._patch_factory.MockPatch()
pre_cq_1 = self.buildstore.InsertBuild('eve-pre-cq', 0, 'eve-pre-cq',
'bot hostname')
self.fake_db.InsertCLActions(pre_cq_1, [
clactions.CLAction.FromGerritPatchAndAction(
change, constants.CL_ACTION_PICKED_UP)
])
pre_cq_2 = self.buildstore.InsertBuild(
'cyan-pre-cq',
1,
'eve-pre-cq',
'bot hostname',
status=constants.BUILDER_STATUS_FAILED)
self.fake_db.InsertCLActions(pre_cq_2, [
clactions.CLAction.FromGerritPatchAndAction(
change, constants.CL_ACTION_PICKED_UP)
])
action_history = self.fake_db.GetActionsForChanges([change])
failed_configs = self.sync_stage._GetFailedPreCQConfigs(action_history)
self.assertCountEqual(failed_configs, ['eve-pre-cq'])
def testFailureStreakCounterExceedsThreshold(self):
"""Test FailureStreakCounterExceedsThreshold."""
pre_cq_1 = self.buildstore.InsertBuild('eve-pre-cq', 0, 'eve-pre-cq',
'bot hostname')
pre_cq_2 = self.buildstore.InsertBuild('eve-pre-cq', 1, 'eve-pre-cq',
'bot hostname')
pre_cq_3 = self.buildstore.InsertBuild('eve-pre-cq', 2, 'eve-pre-cq',
'bot hostname')
self.fake_db.FinishBuild(pre_cq_1, status=constants.BUILDER_STATUS_PASSED)
self.fake_db.FinishBuild(pre_cq_2, status=constants.BUILDER_STATUS_FAILED)
self.fake_db.FinishBuild(pre_cq_3, status=constants.BUILDER_STATUS_FAILED)
build_history = self.fake_db.GetBuildHistory('eve-pre-cq', -1, final=True)
self.assertFalse(
self.sync_stage._FailureStreakCounterExceedsThreshold(
'eve-pre-cq', build_history))
pre_cq_4 = self.buildstore.InsertBuild('eve-pre-cq', 2, 'eve-pre-cq',
'bot hostname')
self.fake_db.FinishBuild(pre_cq_4, status=constants.BUILDER_STATUS_FAILED)
build_history = self.fake_db.GetBuildHistory('eve-pre-cq', -1, final=True)
self.assertTrue(
self.sync_stage._FailureStreakCounterExceedsThreshold(
'eve-pre-cq', build_history))
def testGetBuildConfigsToSanityCheck(self):
"""Test _GetBuildConfigsToSanityCheck."""
build_configs = {'eve-pre-cq', 'cyan-pre-cq', 'betty-pre-cq'}
for build_config in ('eve-pre-cq', 'cyan-pre-cq'):
for _ in range(0, 3):
pre_cq = self.buildstore.InsertBuild(build_config, 0, build_config,
'bot hostname')
self.fake_db.FinishBuild(pre_cq, status=constants.BUILDER_STATUS_FAILED)
build_req_1 = build_requests.BuildRequest(None, self.build_id, 'eve-pre-cq',
None, 'bb_id_1', 'sanity-pre-cq',
datetime.datetime.now())
build_req_2 = build_requests.BuildRequest(
None, self.build_id, 'cyan-pre-cq', None, 'bb_id_2', 'sanity-pre-cq',
datetime.datetime.now() - datetime.timedelta(hours=10))
self.fake_db.InsertBuildRequests([build_req_1, build_req_2])
sanity_check_build_configs = self.sync_stage._GetBuildConfigsToSanityCheck(
self.fake_db, build_configs)
self.assertEqual(sanity_check_build_configs, ['cyan-pre-cq'])
def testLaunchSanityCheckPreCQsIfNeeded(self):
"""Test _LaunchSanityCheckPreCQsIfNeeded."""
mock_pool = mock.Mock()
configs = {'eve-pre-cq', 'cyan-pre-cq'}
self.PatchObject(
sync_stages.PreCQLauncherStage,
'_GetFailedPreCQConfigs',
return_value=configs)
self.PatchObject(
sync_stages.PreCQLauncherStage,
'_GetBuildConfigsToSanityCheck',
return_value=configs)
mock_launch = self.PatchObject(sync_stages.PreCQLauncherStage,
'LaunchSanityPreCQs')
self.sync_stage._LaunchSanityCheckPreCQsIfNeeded(self.build_id,
self.fake_db, mock_pool,
mock.Mock())
mock_launch.assert_called_once_with(self.build_id, self.fake_db, mock_pool,
configs)
def testLaunchTrybots(self):
"""Test _LaunchTrybots."""
mock_pool = mock.Mock()
change = MockPatch()
configs = ['cyan-pre-cq', 'eve-pre-cq']
self.assertDictEqual(self.sync_stage._LaunchTrybots(mock_pool, configs), {})
self.assertDictEqual(
self.sync_stage._LaunchTrybots(
mock_pool, configs, sanity_check_build=True), {})
self.assertDictEqual(
self.sync_stage._LaunchTrybots(mock_pool, configs, plan=[change]), {})
configs = ['test-pre-cq', 'eve-pre-cq']
self.assertDictEqual(self.sync_stage._LaunchTrybots(mock_pool, configs), {})
mock_pool.HandleNoConfigTargetFailure.assert_not_called()
self.assertDictEqual(
self.sync_stage._LaunchTrybots(mock_pool, configs, plan=[change]), {})
mock_pool.HandleNoConfigTargetFailure.assert_called_once()
def testLaunchSanityPreCQs(self):
"""Test LaunchSanityPreCQs."""
mock_pool = mock.Mock()
configs = ['test-pre-cq1', 'test-pre-cq2']
config_bb_id_map = {'test-pre-cq1': 'bb_id_1', 'test-pre-cq2': 'bb_id_2'}
self.PatchObject(
sync_stages.PreCQLauncherStage,
'_LaunchTrybots',
return_value=config_bb_id_map)
self.sync_stage.LaunchSanityPreCQs(self.build_id, self.fake_db, mock_pool,
configs)
requests = self.fake_db.GetBuildRequestsForBuildConfigs(['test-pre-cq1'])
self.assertEqual(len(requests), 1)
self.assertEqual(requests[0].request_build_config, 'test-pre-cq1')
self.assertEqual(requests[0].request_buildbucket_id, 'bb_id_1')
requests = self.fake_db.GetBuildRequestsForBuildConfigs(['test-pre-cq2'])
self.assertEqual(len(requests), 1)
self.assertEqual(requests[0].request_build_config, 'test-pre-cq2')
self.assertEqual(requests[0].request_buildbucket_id, 'bb_id_2')
def testVerificationsForChangeValidConfig(self):
change = MockPatch()
configs_to_test = list(config_lib.GetConfig())[:5]
return_string = ' '.join(configs_to_test)
self.PatchObject(
cq_config.CQConfigParser, 'GetOption', return_value=return_string)
self.assertCountEqual(
self.sync_stage.VerificationsForChange(change), configs_to_test)
def testVerificationsForChangeNoSuchConfig(self):
change = MockPatch()
self.PatchObject(
cq_config.CQConfigParser,
'GetOption',
return_value='this_config_does_not_exist')
self.assertCountEqual(
self.sync_stage.VerificationsForChange(change),
constants.PRE_CQ_DEFAULT_CONFIGS)
def testVerificationsForChangeEmptyField(self):
change = MockPatch()
self.PatchObject(cq_config.CQConfigParser, 'GetOption', return_value=' ')
self.assertCountEqual(
self.sync_stage.VerificationsForChange(change),
constants.PRE_CQ_DEFAULT_CONFIGS)
def testVerificationsForChangeNoneField(self):
change = MockPatch()
self.PatchObject(cq_config.CQConfigParser, 'GetOption', return_value=None)
self.assertCountEqual(
self.sync_stage.VerificationsForChange(change),
constants.PRE_CQ_DEFAULT_CONFIGS)
def testOverlayVerifications(self):
change = MockPatch(project='chromiumos/overlays/chromiumos-overlay')
self.PatchObject(cq_config.CQConfigParser, 'GetOption', return_value=None)
configs = constants.PRE_CQ_DEFAULT_CONFIGS + [constants.BINHOST_PRE_CQ]
self.assertCountEqual(
self.sync_stage.VerificationsForChange(change), configs)
def testRequestedDefaultVerifications(self):
change = MockPatch()
self.PatchObject(
cq_config.CQConfigParser,
'GetOption',
return_value='default eve-pre-cq')
configs = constants.PRE_CQ_DEFAULT_CONFIGS + ['eve-pre-cq']
self.assertCountEqual(
self.sync_stage.VerificationsForChange(change), configs)
def testVerificationsForChangeFromInvalidCommitMessage(self):
change = MockPatch(commit_message="""First line.
Third line.
pre-cq-configs: insect-pre-cq
""")
self.PatchObject(
cq_config.CQConfigParser, 'GetOption', return_value='eve-pre-cq')
self.assertCountEqual(
self.sync_stage.VerificationsForChange(change), ['eve-pre-cq'])
def testVerificationsForChangeFromCommitMessage(self):
change = MockPatch(commit_message="""First line.
Third line.
pre-cq-configs: chell-pre-cq
""")
self.PatchObject(
cq_config.CQConfigParser, 'GetOption', return_value='eve-pre-cq')
self.assertCountEqual(
self.sync_stage.VerificationsForChange(change), ['chell-pre-cq'])
def testMultiVerificationsForChangeFromCommitMessage(self):
change = MockPatch(commit_message="""First line.
Third line.
pre-cq-configs: chell-pre-cq
pre-cq-configs: grunt-pre-cq
""")
self.PatchObject(
cq_config.CQConfigParser, 'GetOption', return_value='eve-pre-cq')
self.assertCountEqual(
self.sync_stage.VerificationsForChange(change),
['chell-pre-cq', 'grunt-pre-cq'])
def _PrepareChangesWithPendingVerifications(self, verifications=None):
"""Prepare changes and pending verifications for them.
This helper creates changes in the validation pool, each of which
require its own set of verifications.
Args:
verifications: A list of lists of configs. Each element in the
outer list corresponds to a different CL. Defaults
to [constants.PRE_CQ_DEFAULT_CONFIGS]
Returns:
A list of len(verifications) MockPatch instances.
"""
verifications = verifications or [constants.PRE_CQ_DEFAULT_CONFIGS]
changes = [MockPatch(gerrit_number=n) for n in range(len(verifications))]
changes_to_verifications = {c: v for c, v in zip(changes, verifications)}
def VerificationsForChange(change):
return changes_to_verifications.get(change) or []
self.PatchObject(
sync_stages.PreCQLauncherStage,
'VerificationsForChange',
side_effect=VerificationsForChange)
return changes
def _PrepareSubmittableChange(self):
# Create a pre-cq submittable change, let it be screened,
# and have the trybot mark it as verified.
change = self._PrepareChangesWithPendingVerifications()[0]
self.PatchObject(
cq_config.CQConfigParser, 'CanSubmitChangeInPreCQ', return_value=True)
change.approval_timestamp = 0
self.PerformSync(pre_cq_status=None, changes=[change], runs=2)
for config in constants.PRE_CQ_DEFAULT_CONFIGS:
build_id = self.buildstore.InsertBuild('builder name', 2, config,
'bot hostname')
self.fake_db.InsertCLActions(build_id, [
clactions.CLAction.FromGerritPatchAndAction(
change, constants.CL_ACTION_VERIFIED)
])
return change
def testSubmitInPreCQ(self):
change = self._PrepareSubmittableChange()
# Change should be submitted by the pre-cq-launcher.
m = self.PatchObject(
validation_pool.ValidationPool,
'SubmitChanges',
return_value=({change}, {}))
self.PerformSync(pre_cq_status=None, changes=[change], patch_objects=False)
submit_reason = constants.STRATEGY_PRECQ_SUBMIT
verified_cls = {c: submit_reason for c in set([change])}
m.assert_called_with(verified_cls)
def testSubmitUnableInPreCQ(self):
change = self._PrepareSubmittableChange()
# Change should throw a DependencyError when trying to create a transaction
e = cros_patch.DependencyError(change, cros_patch.PatchException(change))
self.PatchObject(
patch_series.PatchSeries, 'CreateTransaction', side_effect=e)
self.PerformSync(pre_cq_status=None, changes=[change], patch_objects=False)
# Change should be marked as pre-cq failed as the approval time as exceeded
# the grace period.
self.assertEqual(constants.CL_STATUS_FAILED, self._GetPreCQStatus(change))
def mockLaunchTrybots(self, configs=None):
configs = configs or constants.PRE_CQ_DEFAULT_CONFIGS
config_buildbucket_id_map = {}
for i, config in enumerate(configs):
bb_id = 'bb_id_%s' % str(i)
config_buildbucket_id_map[config] = bb_id
self.PatchObject(
sync_stages.PreCQLauncherStage,
'_LaunchTrybots',
return_value=config_buildbucket_id_map)
def assertAllStatuses(self, changes, status):
"""Verify that all configs for |changes| all have status |status|.
Args:
changes: List of changes.
status: Desired status value.
"""
action_history = self.fake_db.GetActionsForChanges(changes)
progress_map = clactions.GetPreCQProgressMap(changes, action_history)
for change in changes:
for config in progress_map[change]:
self.assertEqual(progress_map[change][config][0], status)
def testNewPatches(self):
# Create a change that is ready to be tested.
change = self._PrepareChangesWithPendingVerifications()[0]
change.approval_timestamp = 0
self.mockLaunchTrybots()
# Change should be launched now.
self.PerformSync(pre_cq_status=None, changes=[change], runs=2)
self.assertAllStatuses([change], constants.CL_PRECQ_CONFIG_STATUS_LAUNCHED)
def testLaunchPerCycleLimit(self):
# Create 4x as many changes as we can launch in one cycle.
change_count = (
sync_stages.PreCQLauncherStage.MAX_LAUNCHES_PER_CYCLE_DERIVATIVE * 4)
changes = self._PrepareChangesWithPendingVerifications(
[['eve-pre-cq']] * change_count)
for c in changes:
c.approval_timestamp = 0
def count_launches():
action_history = self.fake_db.GetActionsForChanges(changes)
return len([
a for a in action_history
if a.action == constants.CL_ACTION_TRYBOT_LAUNCHING
])
self.mockLaunchTrybots(configs=['eve-pre-cq'])
# After one cycle of the launcher, exactly MAX_LAUNCHES_PER_CYCLE_DERIVATIVE
# should have launched.
self.PerformSync(pre_cq_status=None, changes=changes, runs=0)
self.assertEqual(
count_launches(),
sync_stages.PreCQLauncherStage.MAX_LAUNCHES_PER_CYCLE_DERIVATIVE)
# After the next cycle, exactly 3 * MAX_LAUNCHES_PER_CYCLE_DERIVATIVE should
# have launched in total.
self.PerformSync(
pre_cq_status=None, changes=changes, runs=0, patch_objects=False)
self.assertEqual(
count_launches(),
3 * sync_stages.PreCQLauncherStage.MAX_LAUNCHES_PER_CYCLE_DERIVATIVE)
def testDontTestSubmittedPatches(self):
# Create a change that has been submitted.
change = self._PrepareChangesWithPendingVerifications()[0]
change.approval_timestamp = 0
change.status = 'SUBMITTED'
# Change should not be touched by the Pre-CQ if it's submitted.
self.PerformSync(pre_cq_status=None, changes=[change], runs=1)
action_history = self.fake_db.GetActionsForChanges([change])
progress_map = clactions.GetPreCQProgressMap([change], action_history)
self.assertEqual(progress_map, {})
def testRetryInPreCQ(self):
# Create a change that is ready to be tested.
change = (
self._PrepareChangesWithPendingVerifications([['chromite-pre-cq']])[0])
change.approval_timestamp = 0
self.mockLaunchTrybots(configs=['chromite-pre-cq'])
# Change should be launched now.
self.PerformSync(pre_cq_status=None, changes=[change], runs=2)
self.assertAllStatuses([change], constants.CL_PRECQ_CONFIG_STATUS_LAUNCHED)
# Fake all these tryjobs starting
build_ids = self._FakeLaunchTryjobs([change])
# After 1 more Sync all configs should now be inflight.
self.PerformSync(pre_cq_status=None, changes=[change], patch_objects=False)
self.assertAllStatuses([change], constants.CL_PRECQ_CONFIG_STATUS_INFLIGHT)
# Pretend that the build failed with an infrastructure failure so the change
# should be retried.
self.fake_db.InsertCLActions(build_ids['chromite-pre-cq'], [
clactions.CLAction.FromGerritPatchAndAction(
change, constants.CL_ACTION_FORGIVEN)
])
# Change should relaunch again.
self.PerformSync(
pre_cq_status=None, changes=[change], runs=1, patch_objects=False)
self.assertAllStatuses([change], constants.CL_PRECQ_CONFIG_STATUS_LAUNCHED)
def testPreCQ(self):
changes = self._PrepareChangesWithPendingVerifications(
[['chromite-pre-cq', 'signer-pre-cq'], ['rambi-pre-cq'],
['rambi-pre-cq'], ['rambi-pre-cq'], ['rambi-pre-cq']])
# After 2 runs, the changes should be screened but not
# yet launched (due to pre-launch timeout).
for c in changes:
c.approval_timestamp = time.time()
# Mark a change as trybot ready, but not approved. It should also be tried
# by the pre-cq.
for change in changes[2:5]:
change.flags = {'TRY': '1', 'LCQ': '1'}
change.IsMergeable = lambda: False
self.mockLaunchTrybots(
configs=['chromite-pre-cq', 'signer-pre-cq', 'rambi-pre-cq'])
self.PerformSync(pre_cq_status=None, changes=changes, runs=2)
self.assertAllStatuses(changes, constants.CL_PRECQ_CONFIG_STATUS_PENDING)
action_history = self.fake_db.GetActionsForChanges(changes)
progress_map = clactions.GetPreCQProgressMap(changes, action_history)
self.assertEqual(2, len(progress_map[changes[0]]))
for change in changes[1:]:
self.assertEqual(1, len(progress_map[change]))
# Fake that launch delay has expired by changing change approval times.
for c in changes:
c.approval_timestamp = 0
# After 1 more Sync all configs for all changes should be launched.
self.PerformSync(pre_cq_status=None, changes=changes, patch_objects=False)
self.assertAllStatuses(changes, constants.CL_PRECQ_CONFIG_STATUS_LAUNCHED)
# Fake all these tryjobs starting
build_ids = self._FakeLaunchTryjobs(changes)
# After 1 more Sync all configs should now be inflight.
self.PerformSync(pre_cq_status=None, changes=changes, patch_objects=False)
self.assertAllStatuses(changes, constants.CL_PRECQ_CONFIG_STATUS_INFLIGHT)
# Fake INFLIGHT_TIMEOUT+1 passing with rambi-pre-cq and chromite-pre-cq
# config succeeding, and signer-pre-cq never launching. The first change
# should pass the pre-cq, the second should fail due to inflight timeout.
fake_time = datetime.datetime.now() + datetime.timedelta(
minutes=sync_stages.PreCQLauncherStage.INFLIGHT_TIMEOUT + 1)
self.fake_db.SetTime(fake_time)
self.fake_db.InsertCLActions(build_ids['chromite-pre-cq'], [
clactions.CLAction.FromGerritPatchAndAction(
changes[0], constants.CL_ACTION_VERIFIED)
])
for change in changes[1:3]:
self.fake_db.InsertCLActions(build_ids['rambi-pre-cq'], [
clactions.CLAction.FromGerritPatchAndAction(
change, constants.CL_ACTION_VERIFIED)
])
# Failed CLs that are marked ready should be tried again, and changes that
# aren't ready shouldn't be launched.
changes[4].flags = {'CRVW': '2', 'LCQ': '1'}
changes[4].HasReadyFlag = lambda: False
self.PerformSync(pre_cq_status=None, changes=changes, patch_objects=False)
self.assertEqual(
self._GetPreCQStatus(changes[0]), constants.CL_STATUS_FAILED)
self.assertEqual(
self._GetPreCQStatus(changes[1]), constants.CL_STATUS_PASSED)
self.assertEqual(
self._GetPreCQStatus(changes[2]), constants.CL_STATUS_FULLY_VERIFIED)
for change in changes[3:5]:
self.assertEqual(self._GetPreCQStatus(change), constants.CL_STATUS_FAILED)
self.PerformSync(
pre_cq_status=None, changes=changes, patch_objects=False, runs=3)
action_history = self.fake_db.GetActionsForChanges(changes)
progress_map = clactions.GetPreCQProgressMap(changes, action_history)
self.assertEqual(progress_map[changes[0]]['signer-pre-cq'][0],
constants.CL_PRECQ_CONFIG_STATUS_LAUNCHED)
self.assertEqual(progress_map[changes[1]]['rambi-pre-cq'][0],
constants.CL_PRECQ_CONFIG_STATUS_VERIFIED)
self.assertEqual(progress_map[changes[2]]['rambi-pre-cq'][0],
constants.CL_PRECQ_CONFIG_STATUS_VERIFIED)
self.assertEqual(progress_map[changes[3]]['rambi-pre-cq'][0],
constants.CL_PRECQ_CONFIG_STATUS_LAUNCHED)
self.assertEqual(progress_map[changes[4]]['rambi-pre-cq'][0],
constants.CL_PRECQ_CONFIG_STATUS_FAILED)
# These actions should only be recorded at most once for every
# patch. We did not upload any new patch for changes, so there
# should not be dupulicated actions.
unique_actions = (constants.CL_ACTION_PRE_CQ_FULLY_VERIFIED,
constants.CL_ACTION_PRE_CQ_READY_TO_SUBMIT,
constants.CL_ACTION_PRE_CQ_PASSED)
for change in changes:
actions = self.fake_db.GetActionsForChanges([change])
for action_type in unique_actions:
self.assertTrue(
len([x for x in actions if x.action == action_type]) <= 1)
# Fake a long time elapsing, see that passed or fully verified changes
# (changes 1 and 2 in this test) get status expired back to None.
fake_time = self.fake_db.GetTime() + datetime.timedelta(
minutes=sync_stages.PreCQLauncherStage.STATUS_EXPIRY_TIMEOUT + 1)
self.fake_db.SetTime(fake_time)
self.PerformSync(pre_cq_status=None, changes=changes, patch_objects=False)
for c in changes[1:2]:
self.assertEqual(self._GetPreCQStatus(c), None)
def testPreCQEarlyCrashes(self):
self.mockLaunchTrybots(
configs=['apple-pre-cq', 'banana-pre-cq', 'cinnamon-pre-cq'])
mock_buildbucket_client = mock.Mock()
self.sync_stage.buildbucket_client = mock_buildbucket_client
send_notification = self.PatchObject(validation_pool.ValidationPool,
'SendNotification')
changes = self._PrepareChangesWithPendingVerifications(
[['apple-pre-cq', 'banana-pre-cq'], ['cinnamon-pre-cq']])
# Turn our changes into speculatifve PreCQ candidates.
for change in changes:
change.flags.pop('COMR')
change.IsMergeable = lambda: False
change.HasReadyFlag = lambda: False
# Launch three Pre-CQs for two changes.
# At this point, buildbucket does not have any data.
mock_buildbucket_client.GetBuildRequest.return_value = None
self.PerformSync(pre_cq_status=None, changes=changes, runs=2)
self.assertAllStatuses(changes, constants.CL_PRECQ_CONFIG_STATUS_LAUNCHED)
# Make sure buildbucket IDs are assigned as we expect.
action_history = self.fake_db.GetActionsForChanges(changes)
progress_map = clactions.GetPreCQProgressMap(changes, action_history)
self.assertEqual(progress_map[changes[0]]['apple-pre-cq'].buildbucket_id,
'bb_id_0')
self.assertEqual(progress_map[changes[0]]['banana-pre-cq'].buildbucket_id,
'bb_id_1')
self.assertEqual(progress_map[changes[1]]['cinnamon-pre-cq'].buildbucket_id,
'bb_id_2')
# Set all buildbucket statuses to STARTED. Changes statuses are still
# LAUNCHED.
mock_buildbucket_client.GetBuildRequest.return_value = {
'build': {
'status': constants.BUILDBUCKET_BUILDER_STATUS_STARTED,
},
}
self.PerformSync(pre_cq_status=None, changes=changes, patch_objects=False)
self.assertAllStatuses(changes, constants.CL_PRECQ_CONFIG_STATUS_LAUNCHED)
# Set banana-pre-cq buildbucket status to COMPLETED. Since we did not insert
# any CL actions for banana-pre-cq, it is considered as an early crash.
def FakeGetBuildRequest(buildbucket_id, dryrun):
del dryrun # unused
status = (
constants.BUILDBUCKET_BUILDER_STATUS_COMPLETED
if buildbucket_id == 'bb_id_1' else
constants.BUILDBUCKET_BUILDER_STATUS_STARTED)
return {'build': {'status': status}}
mock_buildbucket_client.GetBuildRequest.side_effect = FakeGetBuildRequest
self.PerformSync(pre_cq_status=None, changes=changes, patch_objects=False)
action_history = self.fake_db.GetActionsForChanges(changes)
progress_map = clactions.GetPreCQProgressMap(changes, action_history)
self.assertEqual(progress_map[changes[0]]['apple-pre-cq'].status,
constants.CL_PRECQ_CONFIG_STATUS_LAUNCHED)
self.assertEqual(progress_map[changes[0]]['banana-pre-cq'].status,
constants.CL_PRECQ_CONFIG_STATUS_FAILED)
self.assertEqual(progress_map[changes[1]]['cinnamon-pre-cq'].status,
constants.CL_PRECQ_CONFIG_STATUS_LAUNCHED)
# Finally, make sure a notification message is sent as expected.
send_notification.assert_called_once()
self.assertIn('The banana-pre-cq trybot for your change crashed.',
send_notification.call_args[1]['details'])
self.assertIn(
'https://ci.chromium.org/b/bb_id_1',
send_notification.call_args[1]['details'])
def testSpeculativePreCQ(self):
changes = self._PrepareChangesWithPendingVerifications(
[constants.PRE_CQ_DEFAULT_CONFIGS] * 2)
# Turn our changes into speculatifve PreCQ candidates.
for change in changes:
change.flags.pop('COMR')
change.IsMergeable = lambda: False
change.HasReadyFlag = lambda: False
# Fake that launch delay has expired by changing change approval times.
for change in changes:
change.approval_timestamp = 0
self.mockLaunchTrybots()
# This should cause the changes to be launched.
self.PerformSync(pre_cq_status=None, changes=changes)
self.assertAllStatuses(changes, constants.CL_PRECQ_CONFIG_STATUS_LAUNCHED)
# Make sure every speculative change is marked that way.
for change in changes:
actions = [a.action for a in self.fake_db.GetActionsForChanges([change])]
self.assertIn(constants.CL_ACTION_SPECULATIVE, actions)
# Fake all these tryjobs starting.
build_ids = self._FakeLaunchTryjobs(changes)
# After 1 more Sync all configs should now be inflight.
self.PerformSync(pre_cq_status=None, changes=changes, patch_objects=False)
self.assertAllStatuses(changes, constants.CL_PRECQ_CONFIG_STATUS_INFLIGHT)
# Verify that we mark the change as inflight.
self.assertEqual(
self._GetPreCQStatus(changes[0]), constants.CL_STATUS_INFLIGHT)
# Fake CL 0 being verified by all configs.
for config in constants.PRE_CQ_DEFAULT_CONFIGS:
self.fake_db.InsertCLActions(build_ids[config], [
clactions.CLAction.FromGerritPatchAndAction(
changes[0], constants.CL_ACTION_VERIFIED)
])
# Fake CL 1 being rejected and failed by all configs except the first.
for config in constants.PRE_CQ_DEFAULT_CONFIGS[1:]:
self.fake_db.InsertCLActions(build_ids[config], [
clactions.CLAction.FromGerritPatchAndAction(
changes[1], constants.CL_ACTION_KICKED_OUT)
])
self.fake_db.InsertCLActions(build_ids[config], [
clactions.CLAction.FromGerritPatchAndAction(
changes[1], constants.CL_ACTION_PRE_CQ_FAILED)
])
self.PerformSync(pre_cq_status=None, changes=changes, patch_objects=False)
# Verify that we mark CL 0 as fully verified (not passed).
self.assertEqual(
self._GetPreCQStatus(changes[0]), constants.CL_STATUS_FULLY_VERIFIED)
# Verify that CL 1 has status failed.
self.assertEqual(
self._GetPreCQStatus(changes[1]), constants.CL_STATUS_FAILED)
# Mark our changes as ready, and see if they are immediately passed.
for change in changes:
change.flags['COMR'] = '1'
change.IsMergeable = lambda: True
change.HasReadyFlag = lambda: True
self.PerformSync(pre_cq_status=None, changes=changes, patch_objects=False)
self.assertEqual(
self._GetPreCQStatus(changes[0]), constants.CL_STATUS_PASSED)
def _FakeLaunchTryjobs(self, changes):
"""Pretend to start all launched tryjobs."""
action_history = self.fake_db.GetActionsForChanges(changes)
progress_map = clactions.GetPreCQProgressMap(changes, action_history)
build_ids_per_config = {}
for change, change_status_dict in progress_map.items():
for config, pre_cq_progress_tuple in change_status_dict.items():
if (pre_cq_progress_tuple.status == constants
.CL_PRECQ_CONFIG_STATUS_LAUNCHED):
if not config in build_ids_per_config:
build_ids_per_config[config] = self.buildstore.InsertBuild(
config, 1, config, config)
self.fake_db.InsertCLActions(build_ids_per_config[config], [
clactions.CLAction.FromGerritPatchAndAction(
change, constants.CL_ACTION_PICKED_UP)
])
return build_ids_per_config
def testCommitNonManifestChange(self):
"""See MasterCQSyncTestCase"""
self._testCommitNonManifestChange()
def testFailedCommitOfNonManifestChange(self):
"""See MasterCQSyncTestCase"""
self._testFailedCommitOfNonManifestChange()
def testCommitManifestChange(self):
"""See MasterCQSyncTestCase"""
self._testCommitManifestChange()
def testDefaultSync(self):
"""See MasterCQSyncTestCase"""
self._testDefaultSync()
def _GetPreCQStatus(self, change):
"""Helper method to get pre-cq status of a CL from fake_db."""
action_history = self.fake_db.GetActionsForChanges([change])
return clactions.GetCLPreCQStatus(change, action_history)
def testRequeued(self):
"""Test that a previously rejected patch gets marked as requeued."""
p = MockPatch()
previous_build_id = self.buildstore.InsertBuild(
'some name', 1, 'some_config', 'some_hostname')
action = clactions.CLAction.FromGerritPatchAndAction(
p, constants.CL_ACTION_KICKED_OUT)
self.fake_db.InsertCLActions(previous_build_id, [action])
self.PerformSync(changes=[p])
actions_for_patch = self.fake_db.GetActionsForChanges([p])
requeued_actions = [
a for a in actions_for_patch if a.action == constants.CL_ACTION_REQUEUED
]
self.assertEqual(1, len(requeued_actions))
def testGetConfigBuildbucketIdMap(self):
"""Test testGetConfigBuildbucketIdMap"""
output = json.dumps([])
result = self.sync_stage.GetConfigBuildbucketIdMap(output)
self.assertEqual(result, {})
output = json.dumps([
{
'build_config': 'config_a',
'buildbucket_id': 'bb_a',
'url': 'lego_a',
},
])
result = self.sync_stage.GetConfigBuildbucketIdMap(output)
self.assertEqual(result, {'config_a': 'bb_a'})
output = json.dumps([
{
'build_config': 'config_a',
'buildbucket_id': 'bb_a',
'url': 'lego_a',
},
{
'build_config': 'config_b',
'buildbucket_id': 'bb_b',
'url': 'lego_b',
},
])
result = self.sync_stage.GetConfigBuildbucketIdMap(output)
self.assertEqual(result, {'config_a': 'bb_a', 'config_b': 'bb_b'})
def testCancelPreCQIfNeededSkipsCancellation(self):
"""Test _CancelPreCQIfNeeded which skips cancellation."""
completed_content = {'build': {'status': 'COMPLETED'}}
self.PatchObject(
buildbucket_lib.BuildbucketClient,
'GetBuildRequest',
return_value=completed_content)
mock_cancel = self.PatchObject(buildbucket_lib.BuildbucketClient,
'CancelBuildRequest')
self.sync_stage._CancelPreCQIfNeeded(self.fake_db, mock.Mock())
mock_cancel.assert_not_called()
def testCancelPreCQIfNeededSucceedsCancellation(self):
"""Test CancelPreCQIfNeeded which succeeds cancellation."""
stated_content = {'build': {'status': 'STARTED'}}
self.PatchObject(
buildbucket_lib.BuildbucketClient,
'GetBuildRequest',
return_value=stated_content)
cancel_content = {'build': {'status': 'COMPLETED'}}
mock_cancel = self.PatchObject(
buildbucket_lib.BuildbucketClient,
'CancelBuildRequest',
return_value=cancel_content)
pre_cq_launcher_id = self.buildstore.InsertBuild(
'pre-cq-launcher', 1, 'pre-cq-launcher', 'test_hostname')
pre_cq_id = self.buildstore.InsertBuild(
'chromite-pre-cq',
2,
'chromite-pre-cq',
'test_hostname',
buildbucket_id='100')
c = clactions.GerritPatchTuple(1, 1, True)
old_build_action = clactions.CLAction(
0, pre_cq_launcher_id, constants.CL_ACTION_TRYBOT_LAUNCHING,
'chromite-pre-cq', 'config', c.gerrit_number, c.patch_number,
'internal' if c.internal else 'external',
datetime.datetime.now() - datetime.timedelta(hours=1), '100', None)
self.fake_db.InsertCLActions(pre_cq_launcher_id, [old_build_action])
self.sync_stage._CancelPreCQIfNeeded(self.fake_db, old_build_action)
cl_actions = self.fake_db.GetActionsForChanges([c])
self.assertEqual(len(cl_actions), 2)
expected = [(pre_cq_launcher_id, constants.CL_ACTION_TRYBOT_LAUNCHING),
(pre_cq_id, constants.CL_ACTION_TRYBOT_CANCELLED)]
result = []
for cl_action in cl_actions:
result.append((cl_action.build_id, cl_action.action))
self.assertCountEqual(result, expected)
mock_cancel.assert_called_once_with(
'100', dryrun=self.sync_stage._run.options.debug)
def testCancelPreCQIfNeededFailsCancellation(self):
"""Test CancelPreCQIfNeeded which fails cancellation."""
schedules_content = {'build': {'status': 'SCHEDULED'}}
self.PatchObject(
buildbucket_lib.BuildbucketClient,
'GetBuildRequest',
return_value=schedules_content)
cancel_content = {
'error': {
'message': 'Cannot cancel a completed build',
'reason': 'BUILD_IS_COMPLETED'
}
}
mock_cancel = self.PatchObject(
buildbucket_lib.BuildbucketClient,
'CancelBuildRequest',
return_value=cancel_content)
pre_cq_id = self.buildstore.InsertBuild(
'chromite-pre-cq',
2,
'chromite-pre-cq',
'test_hostname',
buildbucket_id='100')
old_build_action = mock.Mock()
self.sync_stage._CancelPreCQIfNeeded(self.fake_db, old_build_action)
build = self.buildstore.GetBuildStatuses(build_ids=[pre_cq_id])[0]
self.assertEqual(build['status'], constants.BUILDER_STATUS_PASSED)
mock_cancel.assert_called_once_with(
old_build_action.buildbucket_id,
dryrun=self.sync_stage._run.options.debug)
def test_ProcessOldPatchPreCQRuns(self):
"""Test _ProcessOldPatchPreCQRuns."""
db = mock.Mock()
change = metadata_lib.GerritPatchTuple(1, 3, False)
c1 = clactions.CLAction(
0, 1, constants.CL_ACTION_TRYBOT_LAUNCHING, 'chromite-pre-cq', 'config',
1, 1, 'external',
datetime.datetime.now() - datetime.timedelta(hours=5), '100', None)
c2 = clactions.CLAction(
0, 1, constants.CL_ACTION_TRYBOT_LAUNCHING, 'chromite-pre-cq', 'config',
1, 2, 'external',
datetime.datetime.now() - datetime.timedelta(hours=1), '100', None)
c3 = clactions.CLAction(0, 1, constants.CL_ACTION_VALIDATION_PENDING_PRE_CQ,
'chromite-pre-cq', 'config', 1, 3, 'external',
datetime.datetime.now(), None, None)
c4 = clactions.CLAction(0, 1, constants.CL_ACTION_TRYBOT_LAUNCHING,
'chromite-pre-cq', 'config', 1, 3, 'external',
datetime.datetime.now(), '101', None)
action_history = clactions.CLActionHistory([c1, c2, c3, c4])
mock_cancel = self.PatchObject(sync_stages.PreCQLauncherStage,
'_CancelPreCQIfNeeded')
self.sync_stage._ProcessOldPatchPreCQRuns(db, change, action_history)
mock_cancel.assert_called_once_with(db, c2)
def testGetPreCQConfigsFromOptionsNotUnioned(self):
"""Test _GetPreCQConfigsFromOptions for not unioned Pre-CQ config."""
change = MockPatch()
self.PatchObject(cq_config.CQConfigParser, '__init__', return_value=None)
self.PatchObject(
cq_config.CQConfigParser,
'GetUnionPreCQSubConfigsFlag',
return_value=False)
self.PatchObject(
cq_config.CQConfigParser,
'GetPreCQConfigs',
return_value={'default', 'chromite-pre-cq', 'eve-pre-cq'})
pre_cqs = self.sync_stage._GetPreCQConfigsFromOptions(change)
expected_pre_cq = set(constants.PRE_CQ_DEFAULT_CONFIGS +
['chromite-pre-cq', 'eve-pre-cq'])
self.assertCountEqual(pre_cqs, expected_pre_cq)
pre_cqs = self.sync_stage._GetPreCQConfigsFromOptions(
change, union_pre_cq_limit=2)
self.assertCountEqual(pre_cqs, expected_pre_cq)
def testGetPreCQConfigsFromOptionsUnioned(self):
"""Test _GetPreCQConfigsFromOptions for unioned Pre-CQ config."""
change = MockPatch()
self.PatchObject(cq_config.CQConfigParser, '__init__', return_value=None)
self.PatchObject(
cq_config.CQConfigParser,
'GetUnionPreCQSubConfigsFlag',
return_value=True)
self.PatchObject(
cq_config.CQConfigParser,
'GetUnionedPreCQConfigs',
return_value={'default', 'chromite-pre-cq', 'eve-pre-cq'})
pre_cqs = self.sync_stage._GetPreCQConfigsFromOptions(change)
expected_pre_cq = set(constants.PRE_CQ_DEFAULT_CONFIGS +
['chromite-pre-cq', 'eve-pre-cq'])
self.assertCountEqual(pre_cqs, expected_pre_cq)
self.assertRaises(
sync_stages.ExceedUnionPreCQLimitException,
self.sync_stage._GetPreCQConfigsFromOptions,
change,
union_pre_cq_limit=2)
def testConfiguredVerificationsForChangeTooManyConfigs(self):
"""Test ConfiguredVerificationsForChange when too many configs requested."""
change = MockPatch()
self.PatchObject(cros_patch, 'GetOptionLinesFromCommitMessage')
pre_cq_configs = {'pre-cq-%s' % x for x in range(0, 30)}
exceed_exception = sync_stages.ExceedUnionPreCQLimitException(
pre_cq_configs, 20)
self.PatchObject(
sync_stages.PreCQLauncherStage,
'_GetPreCQConfigsFromOptions',
side_effect=exceed_exception)
result = self.sync_stage._ConfiguredVerificationsForChange(change)
self.assertEqual(len(result), sync_stages.DEFAULT_UNION_PRE_CQ_LIMIT)
self.assertLess(
result,
{'pre-cq-%s' % x for x in range(0, 30)},
)
class MasterSlaveLKGMSyncTest(generic_stages_unittest.StageTestCase):
"""Unit tests for MasterSlaveLKGMSyncStage"""
BOT_ID = constants.MST_ANDROID_PFQ_MASTER
def setUp(self):
"""Setup"""
self.source_repo = 'ssh://source/repo'
self.manifest_version_url = 'fake manifest url'
self.branch = 'master'
self.build_name = 'master-mst-android-pfq'
self.incr_type = 'branch'
self.next_version = 'next_version'
self.sync_stage = None
self.repo = repository.RepoRepository(self.source_repo, self.tempdir,
self.branch)
# Create and set up a fake cidb instance.
self.fake_db = fake_cidb.FakeCIDBConnection()
self.buildstore = FakeBuildStore(self.fake_db)
cidb.CIDBConnectionFactory.SetupMockCidb(self.fake_db)
self.manager = lkgm_manager.LKGMManager(
source_repo=self.repo,
manifest_repo=self.manifest_version_url,
build_names=[self.build_name],
build_type=constants.ANDROID_PFQ_TYPE,
incr_type=self.incr_type,
force=False,
branch=self.branch,
buildstore=self.buildstore,
dry_run=True)
self.PatchObject(buildbucket_lib, 'GetServiceAccount', return_value=True)
self.PatchObject(auth.AuthorizedHttp, '__init__', return_value=None)
self.PatchObject(
buildbucket_lib.BuildbucketClient,
'_GetHost',
return_value=buildbucket_lib.BUILDBUCKET_TEST_HOST)
self._Prepare()
# Our API here is not great when it comes to kwargs passing.
def _Prepare(self, bot_id=None, **kwargs): # pylint: disable=arguments-differ
super(MasterSlaveLKGMSyncTest, self)._Prepare(bot_id, **kwargs)
self._run.config['manifest_version'] = self.manifest_version_url
self.sync_stage = sync_stages.MasterSlaveLKGMSyncStage(
self._run, self.buildstore)
self.sync_stage.manifest_manager = self.manager
self._run.attrs.manifest_manager = self.manager
def testGetLastChromeOSVersion(self):
"""Test GetLastChromeOSVersion"""
id1 = self.buildstore.InsertBuild(
builder_name='test_builder',
build_number=666,
build_config='master-mst-android-pfq',
bot_hostname='test_hostname')
id2 = self.buildstore.InsertBuild(
builder_name='test_builder',
build_number=667,
build_config='master-mst-android-pfq',
bot_hostname='test_hostname')
metadata_1 = metadata_lib.CBuildbotMetadata()
metadata_1.UpdateWithDict({'version': {'full': 'R42-7140.0.0-rc1'}})
metadata_2 = metadata_lib.CBuildbotMetadata()
metadata_2.UpdateWithDict({'version': {'full': 'R43-7141.0.0-rc1'}})
self._run.attrs.metadata.UpdateWithDict({
'version': {
'full': 'R44-7142.0.0-rc1'
}
})
self.fake_db.UpdateMetadata(id1, metadata_1)
self.fake_db.UpdateMetadata(id2, metadata_2)
v = self.sync_stage.GetLastChromeOSVersion()
self.assertEqual(v.milestone, '43')
self.assertEqual(v.platform, '7141.0.0-rc1')
def testGetInitializedManager(self):
self.sync_stage.repo = self.repo
self.sync_stage._GetInitializedManager(True)