3 # Copyright (c) 2011-2012 The Chromium OS Authors. All rights reserved.
4 # Use of this source code is governed by a BSD-style license that can be
5 # found in the LICENSE file.
7 """Module that contains unittests for validation_pool module."""
23 sys.path.insert(0, constants.SOURCE_ROOT)
25 from chromite.cbuildbot import failures_lib
26 from chromite.cbuildbot import results_lib
27 from chromite.cbuildbot import metadata_lib
28 from chromite.cbuildbot import repository
29 from chromite.cbuildbot import tree_status
30 from chromite.cbuildbot import validation_pool
31 from chromite.lib import cros_build_lib
32 from chromite.lib import cros_build_lib_unittest
33 from chromite.lib import cros_test_lib
34 from chromite.lib import gerrit
35 from chromite.lib import gob_util
36 from chromite.lib import gs
37 from chromite.lib import osutils
38 from chromite.lib import parallel_unittest
39 from chromite.lib import partial_mock
40 from chromite.lib import patch as cros_patch
41 from chromite.lib import patch_unittest
47 _GetNumber = iter(itertools.count()).next
49 # Some tests require the kernel, and fail with buildtools only repo.
50 KERNEL_AVAILABLE = os.path.exists(os.path.join(
51 constants.SOURCE_ROOT, 'src', 'third_party', 'kernel'))
53 def GetTestJson(change_id=None):
54 """Get usable fake Gerrit patch json data
57 change_id: If given, force this ChangeId
59 data = copy.deepcopy(patch_unittest.FAKE_PATCH_JSON)
60 if change_id is not None:
61 data['id'] = str(change_id)
65 class MockManifest(object):
66 """Helper class for Mocking Manifest objects."""
68 def __init__(self, path, **kwargs):
70 for key, attr in kwargs.iteritems():
71 setattr(self, key, attr)
74 # pylint: disable=W0212,R0904
75 class Base(cros_test_lib.MockTestCase):
76 """Test case base class with helpers for other test suites."""
79 self.patch_mock = None
80 self._patch_counter = (itertools.count(1)).next
81 self.build_root = 'fakebuildroot'
82 self.PatchObject(gob_util, 'CreateHttpConn',
83 side_effect=AssertionError('Test should not contact GoB'))
84 self.PatchObject(tree_status, 'IsTreeOpen', return_value=True)
85 self.PatchObject(tree_status, 'WaitForTreeStatus',
86 return_value=constants.TREE_OPEN)
88 def MockPatch(self, change_id=None, patch_number=None, is_merged=False,
89 project='chromiumos/chromite', remote=constants.EXTERNAL_REMOTE,
90 tracking_branch='refs/heads/master', is_draft=False,
92 """Helper function to create mock GerritPatch objects."""
94 change_id = self._patch_counter()
95 gerrit_number = str(change_id)
96 change_id = hex(change_id)[2:].rstrip('L').lower()
97 change_id = 'I%s' % change_id.rjust(40, '0')
98 sha1 = hex(_GetNumber())[2:].rstrip('L').lower().rjust(40, '0')
99 patch_number = (patch_number if patch_number is not None else _GetNumber())
100 fake_url = 'http://foo/bar'
102 approvals = [{'type': 'VRIF', 'value': '1', 'grantedOn': 1391733002},
103 {'type': 'CRVW', 'value': '2', 'grantedOn': 1391733002},
104 {'type': 'COMR', 'value': '1', 'grantedOn': 1391733002},]
106 current_patch_set = {
107 'number': patch_number,
110 'approvals': approvals,
113 'currentPatchSet': current_patch_set,
115 'number': gerrit_number,
117 'branch': tracking_branch,
118 'owner': {'email': 'elmer.fudd@chromium.org'},
120 'status': 'MERGED' if is_merged else 'NEW',
121 'url': '%s/%s' % (fake_url, change_id),
124 patch = cros_patch.GerritPatch(patch_dict, remote, fake_url)
127 patch.total_fail_count = 3
130 def GetPatches(self, how_many=1, always_use_list=False, **kwargs):
131 """Get a sequential list of patches.
134 how_many: How many patches to return.
135 always_use_list: Whether to use a list for a single item list.
136 **kwargs: Keyword arguments for self.MockPatch.
138 patches = [self.MockPatch(**kwargs) for _ in xrange(how_many)]
140 for i, patch in enumerate(patches):
141 self.patch_mock.SetGerritDependencies(patch, patches[:i + 1])
142 if how_many == 1 and not always_use_list:
147 class MoxBase(Base, cros_test_lib.MoxTestCase):
148 """Base class for other test suites with numbers mocks patched in."""
151 self.mox.StubOutWithMock(validation_pool, '_RunCommand')
152 # Suppress all gerrit access; having this occur is generally a sign
153 # the code is either misbehaving, or that the tests are bad.
154 self.mox.StubOutWithMock(gerrit.GerritHelper, 'Query')
155 self.PatchObject(gs.GSContext, 'Cat', side_effect=gs.GSNoSuchKey())
156 self.PatchObject(gs.GSContext, 'Copy')
157 self.PatchObject(gs.GSContext, 'Exists', return_value=False)
158 self.PatchObject(gs.GSCounter, 'Increment')
160 def MakeHelper(self, cros_internal=None, cros=None):
161 # pylint: disable=W0201
163 cros_internal = self.mox.CreateMock(gerrit.GerritHelper)
164 cros_internal.version = '2.2'
165 cros_internal.remote = constants.INTERNAL_REMOTE
167 cros = self.mox.CreateMock(gerrit.GerritHelper)
168 cros.remote = constants.EXTERNAL_REMOTE
170 return validation_pool.HelperPool(cros_internal=cros_internal,
174 class IgnoredStagesTest(Base):
175 """Tests for functions that calculate what stages to ignore."""
177 def testBadConfigFile(self):
178 """Test if we can handle an incorrectly formatted config file."""
179 with osutils.TempDir(set_global=True) as tempdir:
180 path = os.path.join(tempdir, 'foo.ini')
181 osutils.WriteFile(path, 'foobar')
182 ignored = validation_pool.GetStagesToIgnoreFromConfigFile(path)
183 self.assertEqual([], ignored)
185 def testMissingConfigFile(self):
186 """Test if we can handle a missing config file."""
187 with osutils.TempDir(set_global=True) as tempdir:
188 path = os.path.join(tempdir, 'foo.ini')
189 ignored = validation_pool.GetStagesToIgnoreFromConfigFile(path)
190 self.assertEqual([], ignored)
192 def testGoodConfigFile(self):
193 """Test if we can handle a good config file."""
194 with osutils.TempDir(set_global=True) as tempdir:
195 path = os.path.join(tempdir, 'foo.ini')
196 osutils.WriteFile(path, '[GENERAL]\nignored-stages: bar baz\n')
197 ignored = validation_pool.GetStagesToIgnoreFromConfigFile(path)
198 self.assertEqual(['bar', 'baz'], ignored)
201 class TestPatchSeries(MoxBase):
202 """Tests resolution and applying logic of validation_pool.ValidationPool."""
204 @contextlib.contextmanager
205 def _ValidateTransactionCall(self, _changes):
208 def GetPatchSeries(self, helper_pool=None):
209 if helper_pool is None:
210 helper_pool = self.MakeHelper(cros_internal=True, cros=True)
211 series = validation_pool.PatchSeries(self.build_root, helper_pool)
213 # Suppress transactions.
214 series._Transaction = self._ValidateTransactionCall
215 series.GetGitRepoForChange = \
216 lambda change, **kwargs: os.path.join(self.build_root, change.project)
220 def assertPath(self, _patch, return_value, path):
221 self.assertEqual(path, os.path.join(self.build_root, _patch.project))
222 if isinstance(return_value, Exception):
226 def SetPatchDeps(self, patch, parents=(), cq=()):
227 """Set the dependencies of |patch|.
230 patch: The patch to process.
231 parents: A set of strings to set as parents of |patch|.
232 cq: A set of strings to set as paladin dependencies of |patch|.
234 patch.GerritDependencies = (
235 lambda: [cros_patch.ParsePatchDep(x) for x in parents])
236 patch.PaladinDependencies = functools.partial(
237 self.assertPath, patch, [cros_patch.ParsePatchDep(x) for x in cq])
238 patch.Fetch = functools.partial(
239 self.assertPath, patch, patch.sha1)
241 def _ValidatePatchApplyManifest(self, value):
242 self.assertTrue(isinstance(value, MockManifest))
243 self.assertEqual(value.root, self.build_root)
246 def SetPatchApply(self, patch, trivial=False):
247 self.mox.StubOutWithMock(patch, 'ApplyAgainstManifest')
248 return patch.ApplyAgainstManifest(
249 mox.Func(self._ValidatePatchApplyManifest),
252 def assertResults(self, series, changes, applied=(), failed_tot=(),
253 failed_inflight=(), frozen=True):
254 manifest = MockManifest(self.build_root)
255 result = series.Apply(changes, frozen=frozen, manifest=manifest)
257 _GetIds = lambda seq:[x.id for x in seq]
258 _GetFailedIds = lambda seq: _GetIds(x.patch for x in seq)
260 applied_result = _GetIds(result[0])
261 failed_tot_result, failed_inflight_result = map(_GetFailedIds, result[1:])
263 applied = _GetIds(applied)
264 failed_tot = _GetIds(failed_tot)
265 failed_inflight = _GetIds(failed_inflight)
268 self.assertEqual(applied, applied_result)
269 self.assertItemsEqual(failed_inflight, failed_inflight_result)
270 self.assertItemsEqual(failed_tot, failed_tot_result)
273 def testApplyWithDeps(self):
274 """Test that we can apply changes correctly and respect deps.
276 This tests a simple out-of-order change where change1 depends on change2
277 but tries to get applied before change2. What should happen is that
278 we should notice change2 is a dep of change1 and apply it first.
280 series = self.GetPatchSeries()
282 patch1, patch2 = patches = self.GetPatches(2)
284 self.SetPatchDeps(patch2)
285 self.SetPatchDeps(patch1, [patch2.id])
287 self.SetPatchApply(patch2)
288 self.SetPatchApply(patch1)
291 self.assertResults(series, patches, [patch2, patch1])
294 def testSha1Deps(self):
295 """Test that we can apply changes correctly and respect sha1 deps.
297 This tests a simple out-of-order change where change1 depends on change2
298 but tries to get applied before change2. What should happen is that
299 we should notice change2 is a dep of change1 and apply it first.
301 series = self.GetPatchSeries()
303 patch1, patch2, patch3 = patches = self.GetPatches(3)
304 patch2.change_id = patch2.id = patch2.sha1
305 patch3.change_id = patch3.id = '*' + patch3.sha1
306 patch3.remote = constants.INTERNAL_REMOTE
308 self.SetPatchDeps(patch1, [patch2.sha1])
309 self.SetPatchDeps(patch2, ['*%s' % patch3.sha1])
310 self.SetPatchDeps(patch3)
312 self.SetPatchApply(patch2)
313 self.SetPatchApply(patch3)
314 self.SetPatchApply(patch1)
317 self.assertResults(series, patches, [patch3, patch2, patch1])
320 def testGerritNumberDeps(self):
321 """Test that we can apply changes correctly and respect gerrit number deps.
323 This tests a simple out-of-order change where change1 depends on change2
324 but tries to get applied before change2. What should happen is that
325 we should notice change2 is a dep of change1 and apply it first.
327 series = self.GetPatchSeries()
329 patch1, patch2, patch3 = patches = self.GetPatches(3)
331 self.SetPatchDeps(patch3, cq=[patch1.gerrit_number])
332 self.SetPatchDeps(patch2, cq=[patch3.gerrit_number])
333 self.SetPatchDeps(patch1, cq=[patch2.id])
335 self.SetPatchApply(patch3)
336 self.SetPatchApply(patch2)
337 self.SetPatchApply(patch1)
340 self.assertResults(series, patches, patches)
343 def testGerritLazyMapping(self):
344 """Given a patch lacking a gerrit number, via gerrit, map it to that change.
346 Literally, this ensures that local patches pushed up- lacking a gerrit
347 number- are mapped back to a changeid via asking gerrit for that number,
348 then the local matching patch is used if available.
350 series = self.GetPatchSeries()
352 patch1 = self.MockPatch()
353 self.PatchObject(patch1, 'LookupAliases', return_value=[patch1.id])
354 patch2 = self.MockPatch(change_id=int(patch1.change_id[1:]))
355 patch3 = self.MockPatch()
357 self.SetPatchDeps(patch3, cq=[patch2.gerrit_number])
358 self.SetPatchDeps(patch2)
359 self.SetPatchDeps(patch1)
361 self.SetPatchApply(patch1)
362 self.SetPatchApply(patch3)
364 self._SetQuery(series, patch2, query=patch2.gerrit_number).AndReturn(patch2)
367 applied = self.assertResults(series, [patch1, patch3], [patch3, patch1])[0]
368 self.assertTrue(applied[0] is patch3)
369 self.assertTrue(applied[1] is patch1)
372 def testCrosGerritDeps(self, cros_internal=True):
373 """Test that we can apply changes correctly and respect deps.
375 This tests a simple out-of-order change where change1 depends on change3
376 but tries to get applied before change2. What should happen is that
377 we should notice change2 is a dep of change1 and apply it first.
379 helper_pool = self.MakeHelper(cros_internal=cros_internal, cros=True)
380 series = self.GetPatchSeries(helper_pool=helper_pool)
382 patch1 = self.MockPatch(remote=constants.EXTERNAL_REMOTE)
383 patch2 = self.MockPatch(remote=constants.INTERNAL_REMOTE)
384 patch3 = self.MockPatch(remote=constants.EXTERNAL_REMOTE)
385 patches = [patch1, patch2, patch3]
387 applied_patches = [patch3, patch1, patch2]
389 applied_patches = [patch3, patch1]
391 self.SetPatchDeps(patch1, [patch3.id])
392 self.SetPatchDeps(patch2)
393 self.SetPatchDeps(patch3, cq=[patch2.id])
396 self.SetPatchApply(patch2)
397 self.SetPatchApply(patch1)
398 self.SetPatchApply(patch3)
401 self.assertResults(series, patches, applied_patches)
404 def testExternalCrosGerritDeps(self):
405 """Test that we exclude internal deps on external trybot."""
406 self.testCrosGerritDeps(cros_internal=False)
409 def _SetQuery(series, change, query=None):
410 helper = series._helper_pool.GetHelper(change.remote)
411 query = change.id if query is None else query
412 return helper.QuerySingleRecord(query, must_match=True)
414 def testApplyMissingDep(self):
415 """Test that we don't try to apply a change without met dependencies.
417 Patch2 is in the validation pool that depends on Patch1 (which is not)
418 Nothing should get applied.
420 series = self.GetPatchSeries()
422 patch1, patch2 = self.GetPatches(2)
424 self.SetPatchDeps(patch2, [patch1.id])
425 self._SetQuery(series, patch1).AndReturn(patch1)
428 self.assertResults(series, [patch2],
432 def testApplyWithCommittedDeps(self):
433 """Test that we apply a change with dependency already committed."""
434 series = self.GetPatchSeries()
436 # Use for basic commit check.
437 patch1 = self.GetPatches(1, is_merged=True)
438 patch2 = self.GetPatches(1)
440 self.SetPatchDeps(patch2, [patch1.id])
441 self._SetQuery(series, patch1).AndReturn(patch1)
442 self.SetPatchApply(patch2)
444 # Used to ensure that an uncommitted change put in the lookup cache
445 # isn't invalidly pulled into the graph...
446 patch3, patch4, patch5 = self.GetPatches(3)
448 self._SetQuery(series, patch3).AndReturn(patch3)
449 self.SetPatchDeps(patch4, [patch3.id])
450 self.SetPatchDeps(patch5, [patch3.id])
453 self.assertResults(series, [patch2, patch4, patch5], [patch2],
457 def testCyclicalDeps(self):
458 """Verify that the machinery handles cycles correctly."""
459 series = self.GetPatchSeries()
461 patch1, patch2, patch3 = patches = self.GetPatches(3)
463 self.SetPatchDeps(patch1, [patch2.id])
464 self.SetPatchDeps(patch2, cq=[patch3.id])
465 self.SetPatchDeps(patch3, [patch1.id])
467 self.SetPatchApply(patch1)
468 self.SetPatchApply(patch2)
469 self.SetPatchApply(patch3)
472 self.assertResults(series, patches, [patch2, patch1, patch3])
475 def testComplexCyclicalDeps(self, fail=False):
476 """Verify handling of two interdependent cycles."""
477 series = self.GetPatchSeries()
479 # Create two cyclically interdependent patch chains.
480 # Example: Two patch series A1<-A2<-A3<-A4 and B1<-B2<-B3<-B4. A1 has a
481 # CQ-DEPEND on B4 and B1 has a CQ-DEPEND on A4, so all of the patches must
482 # be committed together.
483 chain1, chain2 = chains = self.GetPatches(4), self.GetPatches(4)
485 (other_chain,) = [x for x in chains if x != chain]
486 self.SetPatchDeps(chain[0], [], cq=[other_chain[-1].id])
487 for i in range(1, len(chain)):
488 self.SetPatchDeps(chain[i], [chain[i-1].id])
490 # Apply the second-last patch first, so that the last patch in the series
491 # will be pulled in via the CQ-DEPEND on the other patch chain.
492 to_apply = [chain1[-2]] + [x for x in (chain1 + chain2) if x != chain1[-2]]
494 # All of the patches but chain[-1] were applied successfully.
495 for patch in chain1[:-1] + chain2:
496 self.SetPatchApply(patch)
499 # Pretend that chain[-1] failed to apply.
500 res = self.SetPatchApply(chain1[-1])
501 res.AndRaise(cros_patch.ApplyPatchException(chain1[-1]))
503 failed_tot = to_apply
505 # We apply the patches in this order since the last patch in chain1
506 # is pulled in via CQ-DEPEND.
507 self.SetPatchApply(chain1[-1])
508 applied = chain1[:-1] + chain2 + [chain1[-1]]
512 self.assertResults(series, to_apply, applied=applied, failed_tot=failed_tot)
515 def testFailingComplexCyclicalDeps(self):
516 """Verify handling of failing interlocked cycles."""
517 self.testComplexCyclicalDeps(fail=True)
519 def testApplyPartialFailures(self):
520 """Test that can apply changes correctly when one change fails to apply.
522 This tests a simple change order where 1 depends on 2 and 1 fails to apply.
523 Only 1 should get tried as 2 will abort once it sees that 1 can't be
524 applied. 3 with no dependencies should go through fine.
526 Since patch1 fails to apply, we should also get a call to handle the
529 series = self.GetPatchSeries()
531 patch1, patch2, patch3, patch4 = patches = self.GetPatches(4)
533 self.SetPatchDeps(patch1)
534 self.SetPatchDeps(patch2, [patch1.id])
535 self.SetPatchDeps(patch3)
536 self.SetPatchDeps(patch4)
538 self.SetPatchApply(patch1).AndRaise(
539 cros_patch.ApplyPatchException(patch1))
541 self.SetPatchApply(patch3)
542 self.SetPatchApply(patch4).AndRaise(
543 cros_patch.ApplyPatchException(patch1, inflight=True))
546 self.assertResults(series, patches,
547 [patch3], [patch2, patch1], [patch4])
550 def testComplexApply(self):
551 """More complex deps test.
553 This tests a total of 2 change chains where the first change we see
554 only has a partial chain with the 3rd change having the whole chain i.e.
555 1->2, 3->1->2. Since we get these in the order 1,2,3,4,5 the order we
556 should apply is 2,1,3,4,5.
558 This test also checks the patch order to verify that Apply re-orders
559 correctly based on the chain.
561 series = self.GetPatchSeries()
563 patch1, patch2, patch3, patch4, patch5 = patches = self.GetPatches(5)
565 self.SetPatchDeps(patch1, [patch2.id])
566 self.SetPatchDeps(patch2)
567 self.SetPatchDeps(patch3, [patch1.id, patch2.id])
568 self.SetPatchDeps(patch4, cq=[patch5.id])
569 self.SetPatchDeps(patch5)
571 for patch in (patch2, patch1, patch3, patch4, patch5):
572 self.SetPatchApply(patch)
576 series, patches, [patch2, patch1, patch3, patch4, patch5])
579 def testApplyStandalonePatches(self):
580 """Simple apply of two changes with no dependent CL's."""
581 series = self.GetPatchSeries()
583 patches = self.GetPatches(3)
585 for patch in patches:
586 self.SetPatchDeps(patch)
588 for patch in patches:
589 self.SetPatchApply(patch)
592 self.assertResults(series, patches, patches)
596 def MakePool(overlays=constants.PUBLIC_OVERLAYS, build_number=1,
597 builder_name='foon', is_master=True, dryrun=True, **kwargs):
598 """Helper for creating ValidationPool objects for tests."""
599 kwargs.setdefault('changes', [])
600 build_root = kwargs.pop('build_root', '/fake_root')
602 pool = validation_pool.ValidationPool(
603 overlays, build_root, build_number, builder_name, is_master,
608 class MockPatchSeries(partial_mock.PartialMock):
609 """Mock the PatchSeries functions."""
610 TARGET = 'chromite.cbuildbot.validation_pool.PatchSeries'
611 ATTRS = ('GetDepsForChange', '_GetGerritPatch', '_LookupHelper')
614 partial_mock.PartialMock.__init__(self)
618 def SetGerritDependencies(self, patch, deps):
619 """Add |deps| to the Gerrit dependencies of |patch|."""
620 self.deps[patch] = deps
622 def SetCQDependencies(self, patch, deps):
623 """Add |deps| to the CQ dependencies of |patch|."""
624 self.cq_deps[patch] = deps
626 def GetDepsForChange(self, _inst, patch):
627 return self.deps.get(patch, []), self.cq_deps.get(patch, [])
629 def _GetGerritPatch(self, _inst, dep, **_kwargs):
632 _LookupHelper = mock.MagicMock()
635 class TestSubmitChange(MoxBase):
636 """Test suite related to submitting changes."""
639 self.orig_timeout = validation_pool.SUBMITTED_WAIT_TIMEOUT
640 validation_pool.SUBMITTED_WAIT_TIMEOUT = 4
643 validation_pool.SUBMITTED_WAIT_TIMEOUT = self.orig_timeout
645 def _TestSubmitChange(self, results):
646 """Test submitting a change with the given results."""
647 results = [cros_test_lib.EasyAttr(status=r) for r in results]
648 change = self.MockPatch(change_id=12345, patch_number=1)
649 pool = self.mox.CreateMock(validation_pool.ValidationPool)
651 pool._metadata = metadata_lib.CBuildbotMetadata()
652 pool._helper_pool = self.mox.CreateMock(validation_pool.HelperPool)
653 helper = self.mox.CreateMock(validation_pool.gerrit.GerritHelper)
655 # Prepare replay script.
656 pool._helper_pool.ForChange(change).AndReturn(helper)
657 helper.SubmitChange(change, dryrun=False)
658 for result in results:
659 helper.QuerySingleRecord(change.gerrit_number).AndReturn(result)
663 retval = validation_pool.ValidationPool._SubmitChange(pool, change)
667 def testSubmitChangeMerged(self):
668 """Submit one change to gerrit, status MERGED."""
669 self.assertTrue(self._TestSubmitChange(['MERGED']))
671 def testSubmitChangeSubmitted(self):
672 """Submit one change to gerrit, stuck on SUBMITTED."""
673 # The query will be retried 1 more time than query timeout.
674 results = ['SUBMITTED' for _i in
675 xrange(validation_pool.SUBMITTED_WAIT_TIMEOUT + 1)]
676 self.assertTrue(self._TestSubmitChange(results))
678 def testSubmitChangeSubmittedToMerged(self):
679 """Submit one change to gerrit, status SUBMITTED then MERGED."""
680 results = ['SUBMITTED', 'SUBMITTED', 'MERGED']
681 self.assertTrue(self._TestSubmitChange(results))
683 def testSubmitChangeFailed(self):
684 """Submit one change to gerrit, reported back as NEW."""
685 self.assertFalse(self._TestSubmitChange(['NEW']))
688 class ValidationFailureOrTimeout(MoxBase):
689 """Tests that HandleValidationFailure and HandleValidationTimeout functions.
691 These tests check that HandleValidationTimeout and HandleValidationFailure
692 reject (i.e. zero out the CQ field) of the correct number of patches, under
693 various circumstances.
696 _PATCH_MESSAGE = 'Your patch failed.'
697 _BUILD_MESSAGE = 'Your build failed.'
700 self._patches = self.GetPatches(3)
701 self._pool = MakePool(changes=self._patches)
704 validation_pool.ValidationPool, 'GetCLStatus',
705 return_value=validation_pool.ValidationPool.STATUS_PASSED)
707 validation_pool.CalculateSuspects, 'FindSuspects',
708 return_value=self._patches)
710 validation_pool.ValidationPool, '_CreateValidationFailureMessage',
711 return_value=self._PATCH_MESSAGE)
712 self.PatchObject(validation_pool.ValidationPool, 'SendNotification')
713 self.PatchObject(validation_pool.ValidationPool, 'RemoveCommitReady')
714 self.PatchObject(validation_pool.ValidationPool, 'UpdateCLStatus')
715 self.PatchObject(validation_pool.ValidationPool, 'ReloadChanges',
716 return_value=self._patches)
717 self.PatchObject(validation_pool.CalculateSuspects, 'OnlyLabFailures',
719 self.PatchObject(validation_pool.CalculateSuspects, 'OnlyInfraFailures',
721 self.StartPatcher(parallel_unittest.ParallelMock())
723 def testPatchesWereRejectedByFailure(self):
724 """Tests that all patches are rejected by failure."""
725 self._pool.HandleValidationFailure([self._BUILD_MESSAGE])
727 len(self._patches), self._pool.RemoveCommitReady.call_count)
729 def testPatchesWereRejectedByTimeout(self):
730 self._pool.HandleValidationTimeout()
732 len(self._patches), self._pool.RemoveCommitReady.call_count)
734 def testNoSuspectsWithFailure(self):
735 """Tests no change is blamed when there is no suspect."""
736 self.PatchObject(validation_pool.CalculateSuspects, 'FindSuspects',
738 self._pool.HandleValidationFailure([self._BUILD_MESSAGE])
739 self.assertEqual(0, self._pool.RemoveCommitReady.call_count)
742 self._pool.pre_cq = True
743 self._pool.HandleValidationFailure([self._BUILD_MESSAGE])
744 self.assertEqual(0, self._pool.RemoveCommitReady.call_count)
746 def testPatchesWereNotRejectedByInsaneFailure(self):
747 self._pool.HandleValidationFailure([self._BUILD_MESSAGE], sanity=False)
748 self.assertEqual(0, self._pool.RemoveCommitReady.call_count)
751 class TestCoreLogic(MoxBase):
752 """Tests resolution and applying logic of validation_pool.ValidationPool."""
755 self.mox.StubOutWithMock(validation_pool.PatchSeries, 'Apply')
756 self.mox.StubOutWithMock(validation_pool.PatchSeries, 'ApplyChange')
757 self.patch_mock = self.StartPatcher(MockPatchSeries())
758 funcs = ['SendNotification', '_SubmitChange']
760 self.mox.StubOutWithMock(validation_pool.ValidationPool, func)
761 self.PatchObject(validation_pool.ValidationPool, 'ReloadChanges',
762 side_effect=lambda x: x)
763 self.StartPatcher(parallel_unittest.ParallelMock())
765 def MakePool(self, *args, **kwargs):
766 """Helper for creating ValidationPool objects for Mox tests."""
767 handlers = kwargs.pop('handlers', False)
768 kwargs['build_root'] = self.build_root
769 pool = MakePool(*args, **kwargs)
770 funcs = ['_HandleApplySuccess', '_HandleApplyFailure',
771 '_HandleCouldNotApply', '_HandleCouldNotSubmit']
774 self.mox.StubOutWithMock(pool, func)
777 def MakeFailure(self, patch, inflight=True):
778 return cros_patch.ApplyPatchException(patch, inflight=inflight)
780 def GetPool(self, changes, applied=(), tot=(), inflight=(), **kwargs):
781 pool = self.MakePool(changes=changes, **kwargs)
782 applied = list(applied)
783 tot = [self.MakeFailure(x, inflight=False) for x in tot]
784 inflight = [self.MakeFailure(x, inflight=True) for x in inflight]
785 # pylint: disable=E1120,E1123
786 validation_pool.PatchSeries.Apply(
787 changes, manifest=mox.IgnoreArg()
788 ).AndReturn((applied, tot, inflight))
790 for patch in applied:
791 pool._HandleApplySuccess(patch).AndReturn(None)
794 pool._HandleApplyFailure(tot).AndReturn(None)
796 # We stash this on the pool object so we can reuse it during validation.
797 # We could stash this in the test instances, but that would break
798 # for any tests that do multiple pool instances.
800 pool._test_data = (changes, applied, tot, inflight)
804 def testApplySlavePool(self):
805 """Verifies that slave calls ApplyChange() directly for each patch."""
806 slave_pool = self.MakePool(is_master=False)
807 patches = self.GetPatches(3)
808 slave_pool.changes = patches
809 for patch in patches:
810 # pylint: disable=E1120, E1123
811 validation_pool.PatchSeries.ApplyChange(patch, manifest=mox.IgnoreArg())
814 self.assertEqual(True, slave_pool.ApplyPoolIntoRepo())
817 def runApply(self, pool, result):
818 self.assertEqual(result, pool.ApplyPoolIntoRepo())
819 self.assertEqual(pool.changes, pool._test_data[1])
820 failed_inflight = pool.changes_that_failed_to_apply_earlier
821 expected_inflight = set(pool._test_data[3])
822 # Intersect the results, since it's possible there were results failed
823 # results that weren't related to the ApplyPoolIntoRepo call.
824 self.assertEqual(set(failed_inflight).intersection(expected_inflight),
827 self.assertEqual(pool.changes, pool._test_data[1])
829 def testPatchSeriesInteraction(self):
830 """Verify the interaction between PatchSeries and ValidationPool.
832 Effectively, this validates data going into PatchSeries, and coming back
833 out; verifies the hand off to _Handle* functions, but no deeper.
835 patches = self.GetPatches(3)
837 apply_pool = self.GetPool(patches, applied=patches, handlers=True)
838 all_inflight = self.GetPool(patches, inflight=patches, handlers=True)
839 all_tot = self.GetPool(patches, tot=patches, handlers=True)
840 mixed = self.GetPool(patches, tot=patches[0:1], inflight=patches[1:2],
841 applied=patches[2:3], handlers=True)
844 self.runApply(apply_pool, True)
845 self.runApply(all_inflight, False)
846 self.runApply(all_tot, False)
847 self.runApply(mixed, True)
850 def testHandleApplySuccess(self):
851 """Validate steps taken for successfull application."""
852 patch = self.GetPatches(1)
853 pool = self.MakePool()
854 pool.SendNotification(patch, mox.StrContains('has picked up your change'))
856 pool._HandleApplySuccess(patch)
859 def testHandleApplyFailure(self):
860 failures = [cros_patch.ApplyPatchException(x) for x in self.GetPatches(4)]
862 notified_patches = failures[:2]
863 unnotified_patches = failures[2:]
864 master_pool = self.MakePool(dryrun=False)
865 slave_pool = self.MakePool(is_master=False)
867 self.mox.StubOutWithMock(gerrit.GerritHelper, 'RemoveCommitReady')
869 for failure in notified_patches:
870 master_pool.SendNotification(
872 mox.StrContains('failed to apply your change'),
873 failure=mox.IgnoreArg())
874 # This pylint suppressin shouldn't be necessary, but pylint is invalidly
875 # thinking that the first arg isn't passed in; we suppress it to suppress
877 # pylint: disable=E1120
878 gerrit.GerritHelper.RemoveCommitReady(failure.patch, dryrun=False)
881 master_pool._HandleApplyFailure(notified_patches)
882 slave_pool._HandleApplyFailure(unnotified_patches)
885 def _setUpSubmit(self):
886 pool = self.MakePool(dryrun=False, handlers=True)
887 patches = self.GetPatches(3)
888 failed = self.GetPatches(3)
889 pool.changes = patches[:]
890 # While we don't do anything w/ these patches, that's
891 # intentional; we're verifying that it isn't submitted
892 # if there is a failure.
893 pool.changes_that_failed_to_apply_earlier = failed[:]
895 return (pool, patches, failed)
897 def testSubmitPoolFailures(self):
898 """Tests that a fatal exception is raised."""
899 pool, patches, _failed = self._setUpSubmit()
900 patch1, patch2, patch3 = patches
902 pool._SubmitChange(patch1).AndReturn(True)
903 pool._SubmitChange(patch2).AndReturn(False)
905 pool._HandleCouldNotSubmit(patch2, mox.IgnoreArg()).InAnyOrder()
906 pool._HandleCouldNotSubmit(patch3, mox.IgnoreArg()).InAnyOrder()
909 self.assertRaises(validation_pool.FailedToSubmitAllChangesException,
913 def testSubmitPartialPass(self):
914 """Tests that a non-fatal exception is raised."""
915 pool, patches, _failed = self._setUpSubmit()
916 patch1, patch2, patch3 = patches
917 # Make patch2 not commit-ready.
918 patch2._approvals = []
920 pool._SubmitChange(patch1).AndReturn(True)
922 pool._HandleCouldNotSubmit(patch2, mox.IgnoreArg()).InAnyOrder()
923 pool._HandleCouldNotSubmit(patch3, mox.IgnoreArg()).InAnyOrder()
926 self.assertRaises(validation_pool.FailedToSubmitAllChangesNonFatalException,
930 def testSubmitPool(self):
931 """Tests that we can submit a pool of patches."""
932 pool, patches, failed = self._setUpSubmit()
934 for patch in patches:
935 pool._SubmitChange(patch).AndReturn(True)
937 pool._HandleApplyFailure(failed)
943 def testSubmitNonManifestChanges(self):
944 """Simple test to make sure we can submit non-manifest changes."""
945 pool, patches, _failed = self._setUpSubmit()
946 pool.non_manifest_changes = patches[:]
948 for patch in patches:
949 pool._SubmitChange(patch).AndReturn(True)
952 pool.SubmitNonManifestChanges()
955 def testUnhandledExceptions(self):
956 """Test that CQ doesn't loop due to unhandled Exceptions."""
957 pool, patches, _failed = self._setUpSubmit()
959 class MyException(Exception):
960 """"Unique Exception used for testing."""
962 def VerifyCQError(patch, error):
963 cq_error = validation_pool.InternalCQError(patch, error.message)
964 return str(error) == str(cq_error)
966 # pylint: disable=E1120,E1123
967 validation_pool.PatchSeries.Apply(
968 patches, manifest=mox.IgnoreArg()).AndRaise(MyException)
969 errors = [mox.Func(functools.partial(VerifyCQError, x)) for x in patches]
970 pool._HandleApplyFailure(errors).AndReturn(None)
973 self.assertRaises(MyException, pool.ApplyPoolIntoRepo)
976 def testFilterDependencyErrors(self):
977 """Verify that dependency errors are correctly filtered out."""
978 failures = [cros_patch.ApplyPatchException(x) for x in self.GetPatches(2)]
979 failures += [cros_patch.DependencyError(x, y) for x, y in
980 zip(self.GetPatches(2), failures)]
981 failures[0].patch.approval_timestamp = time.time()
982 failures[-1].patch.approval_timestamp = time.time()
984 result = validation_pool.ValidationPool._FilterDependencyErrors(failures)
985 self.assertEquals(set(failures[:-1]), set(result))
988 def testFilterNonCrosProjects(self):
989 """Runs through a filter of own manifest and fake changes.
991 This test should filter out the tacos/chromite project as its not real.
993 base_func = itertools.cycle(['chromiumos', 'chromeos']).next
994 patches = self.GetPatches(8)
995 for patch in patches:
996 patch.project = '%s/%i' % (base_func(), _GetNumber())
997 patch.tracking_branch = str(_GetNumber())
999 non_cros_patches = self.GetPatches(2)
1000 for patch in non_cros_patches:
1001 patch.project = str(_GetNumber())
1003 filtered_patches = patches[:4]
1004 allowed_patches = []
1006 for idx, patch in enumerate(patches[4:]):
1007 fails = bool(idx % 2)
1008 # Vary the revision so we can validate that it checks the branch.
1009 revision = ('monkeys' if fails
1010 else 'refs/heads/%s' % patch.tracking_branch)
1012 filtered_patches.append(patch)
1014 allowed_patches.append(patch)
1015 projects.setdefault(patch.project, {})['revision'] = revision
1017 manifest = MockManifest(self.build_root, projects=projects)
1018 for patch in allowed_patches:
1019 patch.GetCheckout = lambda *_args, **_kwargs: True
1020 for patch in filtered_patches:
1021 patch.GetCheckout = lambda *_args, **_kwargs: False
1023 self.mox.ReplayAll()
1024 results = validation_pool.ValidationPool._FilterNonCrosProjects(
1025 patches + non_cros_patches, manifest)
1027 def compare(list1, list2):
1028 mangle = lambda c:(c.id, c.project, c.tracking_branch)
1029 self.assertEqual(list1, list2,
1030 msg="Comparison failed:\n list1: %r\n list2: %r"
1031 % (map(mangle, list1), map(mangle, list2)))
1033 compare(results[0], allowed_patches)
1034 compare(results[1], filtered_patches)
1037 class TestPickling(cros_test_lib.TempDirTestCase):
1038 """Tests to validate pickling of ValidationPool, covering CQ's needs"""
1040 def testSelfCompatibility(self):
1041 """Verify compatibility of current git HEAD against itself."""
1042 self._CheckTestData(self._GetTestData())
1044 def testToTCompatibility(self):
1045 """Validate that ToT can use our pickles, and that we can use ToT's data."""
1046 repo = os.path.join(self.tempdir, 'chromite')
1047 reference = os.path.abspath(__file__)
1048 reference = os.path.normpath(os.path.join(reference, '../../'))
1050 repository.CloneGitRepo(
1052 '%s/chromiumos/chromite' % constants.EXTERNAL_GOB_URL,
1053 reference=reference)
1057 from chromite.cbuildbot import validation_pool_unittest
1058 if not hasattr(validation_pool_unittest, 'TestPickling'):
1060 sys.stdout.write(validation_pool_unittest.TestPickling.%s)
1063 # Verify ToT can take our pickle.
1064 cros_build_lib.RunCommand(
1065 ['python', '-c', code % '_CheckTestData(sys.stdin.read())'],
1066 cwd=self.tempdir, print_cmd=False, capture_output=True,
1067 input=self._GetTestData())
1069 # Verify we can handle ToT's pickle.
1070 ret = cros_build_lib.RunCommand(
1071 ['python', '-c', code % '_GetTestData()'],
1072 cwd=self.tempdir, print_cmd=False, capture_output=True)
1074 self._CheckTestData(ret.output)
1077 def _GetCrosInternalPatch(patch_info):
1078 return cros_patch.GerritPatch(
1080 constants.INTERNAL_REMOTE,
1081 constants.INTERNAL_GERRIT_URL)
1084 def _GetCrosPatch(patch_info):
1085 return cros_patch.GerritPatch(
1087 constants.EXTERNAL_REMOTE,
1088 constants.EXTERNAL_GERRIT_URL)
1091 def _GetTestData(cls):
1092 ids = [cros_patch.MakeChangeId() for _ in xrange(3)]
1093 changes = [cls._GetCrosInternalPatch(GetTestJson(ids[0]))]
1094 non_os = [cls._GetCrosPatch(GetTestJson(ids[1]))]
1095 conflicting = [cls._GetCrosInternalPatch(GetTestJson(ids[2]))]
1096 conflicting = [cros_patch.PatchException(x)for x in conflicting]
1097 pool = validation_pool.ValidationPool(
1098 constants.PUBLIC_OVERLAYS,
1100 'testing', True, True,
1101 changes=changes, non_os_changes=non_os,
1102 conflicting_changes=conflicting)
1103 return pickle.dumps([pool, changes, non_os, conflicting])
1106 def _CheckTestData(data):
1107 results = pickle.loads(data)
1108 pool, changes, non_os, conflicting = results
1109 def _f(source, value, getter=None):
1111 getter = lambda x: x
1112 assert len(source) == len(value)
1113 for s_item, v_item in zip(source, value):
1114 assert getter(s_item).id == getter(v_item).id
1115 assert getter(s_item).remote == getter(v_item).remote
1116 _f(pool.changes, changes)
1117 _f(pool.non_manifest_changes, non_os)
1118 _f(pool.changes_that_failed_to_apply_earlier, conflicting,
1119 getter=lambda s:getattr(s, 'patch', s))
1123 class TestFindSuspects(MoxBase):
1124 """Tests validation_pool.ValidationPool._FindSuspects"""
1127 overlay = 'chromiumos/overlays/chromiumos-overlay'
1128 self.overlay_patch = self.GetPatches(project=overlay)
1129 chromite = 'chromiumos/chromite'
1130 self.chromite_patch = self.GetPatches(project=chromite)
1131 self.power_manager = 'chromiumos/platform2/power_manager'
1132 self.power_manager_pkg = 'chromeos-base/power_manager'
1133 self.power_manager_patch = self.GetPatches(project=self.power_manager)
1134 self.kernel = 'chromiumos/third_party/kernel'
1135 self.kernel_pkg = 'sys-kernel/chromeos-kernel'
1136 self.kernel_patch = self.GetPatches(project=self.kernel)
1137 self.secret = 'chromeos/secret'
1138 self.secret_patch = self.GetPatches(project=self.secret,
1139 remote=constants.INTERNAL_REMOTE)
1140 self.PatchObject(cros_patch.GitRepoPatch, 'GetCheckout')
1141 self.PatchObject(cros_patch.GitRepoPatch, 'GetDiffStatus')
1144 def _GetBuildFailure(pkg):
1145 """Create a PackageBuildFailure for the specified |pkg|.
1148 pkg: Package that failed to build.
1150 ex = cros_build_lib.RunCommandError('foo', cros_build_lib.CommandResult())
1151 return failures_lib.PackageBuildFailure(ex, 'bar', [pkg])
1153 def _GetFailedMessage(self, exceptions, stage='Build', internal=False,
1154 bot='daisy_spring-paladin'):
1155 """Returns a BuildFailureMessage object."""
1157 for ex in exceptions:
1158 tracebacks.append(results_lib.RecordedTraceback('Build', 'Build', ex,
1160 reason = 'failure reason string'
1161 return failures_lib.BuildFailureMessage(
1162 'Stage %s failed' % stage, tracebacks, internal, reason, bot)
1164 def _AssertSuspects(self, patches, suspects, pkgs=(), exceptions=(),
1165 internal=False, infra_fail=False, lab_fail=False):
1166 """Run _FindSuspects and verify its output.
1169 patches: List of patches to look at.
1170 suspects: Expected list of suspects returned by _FindSuspects.
1171 pkgs: List of packages that failed with exceptions in the build.
1172 exceptions: List of other exceptions that occurred during the build.
1173 internal: Whether the failures occurred on an internal bot.
1174 infra_fail: Whether the build failed due to infrastructure issues.
1175 lab_fail: Whether the build failed due to lab infrastructure issues.
1177 all_exceptions = list(exceptions) + [self._GetBuildFailure(x) for x in pkgs]
1178 message = self._GetFailedMessage(all_exceptions, internal=internal)
1179 results = validation_pool.CalculateSuspects.FindSuspects(
1180 constants.SOURCE_ROOT, patches, [message], lab_fail=lab_fail,
1181 infra_fail=infra_fail)
1182 self.assertEquals(set(suspects), results)
1184 @unittest.skipIf(not KERNEL_AVAILABLE, 'Full checkout is required.')
1185 def testFailSameProject(self):
1186 """Patches to the package that failed should be marked as failing."""
1187 suspects = [self.kernel_patch]
1188 patches = suspects + [self.power_manager_patch, self.secret_patch]
1189 self._AssertSuspects(patches, suspects, [self.kernel_pkg])
1191 @unittest.skipIf(not KERNEL_AVAILABLE, 'Full checkout is required.')
1192 def testFailSameProjectPlusOverlay(self):
1193 """Patches to the overlay should be marked as failing."""
1194 suspects = [self.overlay_patch, self.kernel_patch]
1195 patches = suspects + [self.power_manager_patch, self.secret_patch]
1196 self._AssertSuspects(patches, suspects, [self.kernel_pkg])
1198 def testFailUnknownPackage(self):
1199 """If no patches changed the package, all patches should fail."""
1200 suspects = [self.overlay_patch, self.power_manager_patch]
1201 changes = suspects + [self.secret_patch]
1202 self._AssertSuspects(changes, suspects, [self.kernel_pkg])
1204 def testFailUnknownException(self):
1205 """An unknown exception should cause all [public] patches to fail."""
1206 suspects = [self.kernel_patch, self.power_manager_patch]
1207 changes = suspects + [self.secret_patch]
1208 self._AssertSuspects(changes, suspects, exceptions=[Exception('foo bar')])
1210 def testFailUnknownInternalException(self):
1211 """An unknown exception should cause all [internal] patches to fail."""
1212 suspects = [self.kernel_patch, self.power_manager_patch, self.secret_patch]
1213 self._AssertSuspects(suspects, suspects, exceptions=[Exception('foo bar')],
1216 def testFailUnknownCombo(self):
1217 """Unknown exceptions should cause all patches to fail.
1219 Even if there are also build failures that we can explain.
1221 suspects = [self.kernel_patch, self.power_manager_patch]
1222 changes = suspects + [self.secret_patch]
1223 self._AssertSuspects(changes, suspects, [self.kernel_pkg],
1224 [Exception('foo bar')])
1226 def testFailNoExceptions(self):
1227 """If there are no exceptions, all patches should be failed."""
1228 suspects = [self.kernel_patch, self.power_manager_patch]
1229 changes = suspects + [self.secret_patch]
1230 self._AssertSuspects(changes, suspects)
1232 def testLabFail(self):
1233 """If there are only lab failures, no suspect is chosen."""
1235 changes = [self.kernel_patch, self.power_manager_patch]
1236 self._AssertSuspects(changes, suspects, lab_fail=True, infra_fail=True)
1238 def testInfraFail(self):
1239 """If there are only non-lab infra faliures, pick chromite changes."""
1240 suspects = [self.chromite_patch]
1241 changes = [self.kernel_patch, self.power_manager_patch] + suspects
1242 self._AssertSuspects(changes, suspects, lab_fail=False, infra_fail=True)
1244 def _GetMessages(self, lab_fail=0, infra_fail=0, other_fail=0):
1245 """Returns a list of BuildFailureMessage objects."""
1248 [self._GetFailedMessage([failures_lib.TestLabFailure()])
1249 for _ in range(lab_fail)])
1251 [self._GetFailedMessage([failures_lib.InfrastructureFailure()])
1252 for _ in range(infra_fail)])
1254 [self._GetFailedMessage(Exception())
1255 for _ in range(other_fail)])
1258 def testOnlyLabFailures(self):
1259 """Tests the OnlyLabFailures function."""
1260 messages = self._GetMessages(lab_fail=2)
1263 validation_pool.CalculateSuspects.OnlyLabFailures(messages, no_stat))
1265 no_stat = ['foo', 'bar']
1266 # Some builders did not start. This is not a lab failure.
1268 validation_pool.CalculateSuspects.OnlyLabFailures(messages, no_stat))
1270 messages = self._GetMessages(lab_fail=1, infra_fail=1)
1272 # Non-lab infrastructure failures are present.
1274 validation_pool.CalculateSuspects.OnlyLabFailures(messages, no_stat))
1276 def testOnlyInfraFailures(self):
1277 """Tests the OnlyInfraFailures function."""
1278 messages = self._GetMessages(infra_fail=2)
1281 validation_pool.CalculateSuspects.OnlyInfraFailures(messages, no_stat))
1283 messages = self._GetMessages(lab_fail=2)
1285 # Lab failures are infrastructure failures.
1287 validation_pool.CalculateSuspects.OnlyInfraFailures(messages, no_stat))
1289 no_stat = ['orange']
1291 # 'Builders failed to report statuses' belong to infrastructure failures.
1293 validation_pool.CalculateSuspects.OnlyInfraFailures(messages, no_stat))
1295 def testSkipInnocentOverlayPatches(self):
1296 """Test that we don't blame innocent overlay patches."""
1297 changes = self.GetPatches(4)
1298 overlay_dir = os.path.join(constants.SOURCE_ROOT, 'src/overlays')
1299 m = mock.MagicMock()
1300 self.PatchObject(cros_patch.GitRepoPatch, 'GetCheckout', return_value=m)
1301 self.PatchObject(m, 'GetPath', return_value=overlay_dir)
1302 self.PatchObject(changes[0], 'GetDiffStatus',
1303 return_value={'overlay-x86-generic/make.conf': 'M'})
1304 self.PatchObject(changes[1], 'GetDiffStatus',
1305 return_value={'make.conf': 'M'})
1306 self.PatchObject(changes[2], 'GetDiffStatus',
1307 return_value={'overlay-daisy/make.conf': 'M'})
1308 self.PatchObject(changes[3], 'GetDiffStatus',
1309 return_value={'overlay-daisy_spring/make.conf': 'M'})
1311 self._AssertSuspects(changes, changes[1:], [self.kernel_pkg])
1314 class TestCLStatus(MoxBase):
1315 """Tests methods that get the CL status."""
1317 def testPrintLinks(self):
1318 changes = self.GetPatches(3)
1319 with parallel_unittest.ParallelMock():
1320 validation_pool.ValidationPool.PrintLinksToChanges(changes)
1322 def testStatusCache(self):
1323 validation_pool.ValidationPool._CL_STATUS_CACHE = {}
1324 changes = self.GetPatches(3)
1325 with parallel_unittest.ParallelMock():
1326 validation_pool.ValidationPool.FillCLStatusCache(validation_pool.CQ,
1328 self.assertEqual(len(validation_pool.ValidationPool._CL_STATUS_CACHE), 12)
1329 validation_pool.ValidationPool.PrintLinksToChanges(changes)
1330 self.assertEqual(len(validation_pool.ValidationPool._CL_STATUS_CACHE), 12)
1333 class TestCreateValidationFailureMessage(Base):
1334 """Tests validation_pool.ValidationPool._CreateValidationFailureMessage"""
1336 def _AssertMessage(self, change, suspects, messages, sanity=True,
1337 infra_fail=False, lab_fail=False, no_stat=None):
1338 """Call the _CreateValidationFailureMessage method.
1341 change: The change we are commenting on.
1342 suspects: List of suspected changes.
1343 messages: List of messages should appear in the failure message.
1344 sanity: Bool indicating sanity of build, default: True.
1345 infra_fail: True if build failed due to infrastructure issues.
1346 lab_fail: True if build failed due to lab infrastructure issues.
1347 no_stat: List of builders that did not start.
1349 msg = validation_pool.ValidationPool._CreateValidationFailureMessage(
1350 False, change, set(suspects), [], sanity=sanity,
1351 infra_fail=infra_fail, lab_fail=lab_fail, no_stat=no_stat)
1353 self.assertTrue(x in msg)
1356 def testSuspectChange(self):
1357 """Test case where 1 is the only change and is suspect."""
1358 patch = self.GetPatches(1)
1359 self._AssertMessage(patch, [patch], ['probably caused by your change'])
1361 def testInnocentChange(self):
1362 """Test case where 1 is innocent."""
1363 patch1, patch2 = self.GetPatches(2)
1364 self._AssertMessage(patch1, [patch2],
1365 ['This failure was probably caused by',
1366 'retry your change automatically'])
1368 def testSuspectChanges(self):
1369 """Test case where 1 is suspected, but so is 2."""
1370 patches = self.GetPatches(2)
1371 self._AssertMessage(patches[0], patches,
1372 ['may have caused this failure'])
1374 def testInnocentChangeWithMultipleSuspects(self):
1375 """Test case where 2 and 3 are suspected."""
1376 patches = self.GetPatches(3)
1377 self._AssertMessage(patches[0], patches[1:],
1378 ['One of the following changes is probably'])
1380 def testNoMessages(self):
1381 """Test case where there are no messages."""
1382 patch1 = self.GetPatches(1)
1383 self._AssertMessage(patch1, [patch1], [])
1385 def testInsaneBuild(self):
1386 """Test case where the build was not sane."""
1387 patches = self.GetPatches(3)
1388 self._AssertMessage(
1389 patches[0], patches, ['The build was consider not sane',
1390 'retry your change automatically'],
1393 def testLabFailMessage(self):
1394 """Test case where the build failed due to lab failures."""
1395 patches = self.GetPatches(3)
1396 self._AssertMessage(
1397 patches[0], patches, ['Lab infrastructure',
1398 'retry your change automatically'],
1401 def testInfraFailMessage(self):
1402 """Test case where the build failed due to infrastructure failures."""
1403 patches = self.GetPatches(2)
1404 self._AssertMessage(
1405 patches[0], [patches[0]],
1406 ['may have been caused by infrastructure',
1407 'This failure was probably caused by your change'],
1409 self._AssertMessage(
1410 patches[1], [patches[0]], ['may have been caused by infrastructure',
1411 'retry your change automatically'],
1415 class TestCreateDisjointTransactions(Base):
1416 """Test the CreateDisjointTransactions function."""
1419 self.patch_mock = self.StartPatcher(MockPatchSeries())
1421 def GetPatches(self, how_many, **kwargs):
1422 return Base.GetPatches(self, how_many, always_use_list=True, **kwargs)
1424 def verifyTransactions(self, txns, max_txn_length=None, circular=False):
1425 """Verify the specified list of transactions are processed correctly.
1428 txns: List of transactions to process.
1429 max_txn_length: Maximum length of any given transaction. This is passed
1430 to the CreateDisjointTransactions function.
1431 circular: Whether the transactions contain circular dependencies.
1433 remove = self.PatchObject(gerrit.GerritHelper, 'RemoveCommitReady')
1434 patches = list(itertools.chain.from_iterable(txns))
1435 expected_plans = txns
1436 if max_txn_length is not None:
1437 # When max_txn_length is specified, transactions should be truncated to
1438 # the specified length, ignoring any remaining patches.
1439 expected_plans = [txn[:max_txn_length] for txn in txns]
1441 pool = MakePool(changes=patches)
1442 plans = pool.CreateDisjointTransactions(None, max_txn_length=max_txn_length)
1444 # If the dependencies are circular, the order of the patches is not
1445 # guaranteed, so compare them in sorted order.
1447 plans = [sorted(plan) for plan in plans]
1448 expected_plans = [sorted(plan) for plan in expected_plans]
1450 # Verify the plans match, and that no changes were rejected.
1451 self.assertEqual(set(map(str, plans)), set(map(str, expected_plans)))
1452 self.assertEqual(0, remove.call_count)
1454 def testPlans(self, max_txn_length=None):
1455 """Verify that independent sets are distinguished."""
1456 for num in range(0, 5):
1457 txns = [self.GetPatches(num) for _ in range(0, num)]
1458 self.verifyTransactions(txns, max_txn_length=max_txn_length)
1460 def runUnresolvedPlan(self, changes, max_txn_length=None):
1461 """Helper for testing unresolved plans."""
1462 notify = self.PatchObject(validation_pool.ValidationPool,
1464 remove = self.PatchObject(gerrit.GerritHelper, 'RemoveCommitReady')
1465 pool = MakePool(changes=changes)
1466 plans = pool.CreateDisjointTransactions(None, max_txn_length=max_txn_length)
1467 self.assertEqual(plans, [])
1468 self.assertEqual(remove.call_count, notify.call_count)
1469 return remove.call_count
1471 def testUnresolvedPlan(self):
1472 """Test plan with old approval_timestamp."""
1473 changes = self.GetPatches(5)[1:]
1474 with cros_test_lib.LoggingCapturer():
1475 call_count = self.runUnresolvedPlan(changes)
1476 self.assertEqual(4, call_count)
1478 def testRecentUnresolvedPlan(self):
1479 """Test plan with recent approval_timestamp."""
1480 changes = self.GetPatches(5)[1:]
1481 for change in changes:
1482 change.approval_timestamp = time.time()
1483 with cros_test_lib.LoggingCapturer():
1484 call_count = self.runUnresolvedPlan(changes)
1485 self.assertEqual(0, call_count)
1487 def testTruncatedPlan(self):
1488 """Test that plans can be truncated correctly."""
1489 # Long lists of patches should be truncated, and we should not see any
1490 # errors when this happens.
1491 self.testPlans(max_txn_length=3)
1493 def testCircularPlans(self):
1494 """Verify that circular plans are handled correctly."""
1495 patches = self.GetPatches(5)
1496 self.patch_mock.SetGerritDependencies(patches[0], [patches[-1]])
1498 # Verify that all patches can be submitted normally.
1499 self.verifyTransactions([patches], circular=True)
1501 # It is not possible to truncate a circular plan. Verify that an error
1502 # is reported in this case.
1503 with cros_test_lib.LoggingCapturer():
1504 call_count = self.runUnresolvedPlan(patches, max_txn_length=3)
1505 self.assertEqual(5, call_count)
1508 class MockValidationPool(partial_mock.PartialMock):
1509 """Mock out a ValidationPool instance."""
1511 TARGET = 'chromite.cbuildbot.validation_pool.ValidationPool'
1512 ATTRS = ('ReloadChanges', 'RemoveCommitReady', '_SubmitChange',
1516 partial_mock.PartialMock.__init__(self)
1517 self.submit_results = {}
1518 self.max_submits = None
1520 def GetSubmittedChanges(self):
1522 for call in self.patched['_SubmitChange'].call_args_list:
1524 calls.append(call_args[1])
1527 def _SubmitChange(self, _inst, change):
1528 result = self.submit_results.get(change, True)
1529 if isinstance(result, Exception):
1531 if result and self.max_submits is not None:
1532 if self.max_submits <= 0:
1534 self.max_submits -= 1
1538 def ReloadChanges(cls, changes):
1541 RemoveCommitReady = None
1542 SendNotification = None
1545 class BaseSubmitPoolTestCase(Base, cros_build_lib_unittest.RunCommandTestCase):
1546 """Test full ability to submit and reject CL pools."""
1549 self.pool_mock = self.StartPatcher(MockValidationPool())
1550 self.patch_mock = self.StartPatcher(MockPatchSeries())
1551 self.PatchObject(gerrit.GerritHelper, 'QuerySingleRecord')
1552 self.patches = self.GetPatches(2)
1554 # By default, don't ignore any errors.
1555 self.ignores = dict((patch, []) for patch in self.patches)
1557 def SetUpPatchPool(self, failed_to_apply=False):
1558 pool = MakePool(changes=self.patches, dryrun=False)
1560 # Create some phony errors and add them to the pool.
1562 for patch in self.GetPatches(2):
1563 errors.append(validation_pool.InternalCQError(patch, str('foo')))
1564 pool.changes_that_failed_to_apply_earlier = errors[:]
1567 def GetTracebacks(self):
1570 def SubmitPool(self, submitted=(), rejected=(), **kwargs):
1571 """Helper function for testing that we can submit a pool successfully.
1574 submitted: List of changes that we expect to be submitted.
1575 rejected: List of changes that we expect to be rejected.
1576 **kwargs: Keyword arguments for SetUpPatchPool.
1578 # self.ignores maps changes to a list of stages to ignore. Use it.
1580 validation_pool, 'GetStagesToIgnoreForChange',
1581 side_effect=lambda _, change: self.ignores[change])
1583 # Set up our pool and submit the patches.
1584 pool = self.SetUpPatchPool(**kwargs)
1585 tracebacks = self.GetTracebacks()
1587 actually_rejected = sorted(pool.SubmitPartialPool(self.GetTracebacks()))
1589 actually_rejected = pool.SubmitChanges(self.patches)
1591 # Check that the right patches were submitted and rejected.
1592 self.assertItemsEqual(list(rejected), list(actually_rejected))
1593 actually_submitted = self.pool_mock.GetSubmittedChanges()
1594 self.assertEqual(list(submitted), actually_submitted)
1597 class SubmitPoolTest(BaseSubmitPoolTestCase):
1598 """Test suite related to the Submit Pool."""
1600 def GetNotifyArg(self, change, key):
1601 """Look up a call to notify about |change| and grab |key| from it.
1604 change: The change to look up.
1605 key: The key to look up. If this is an integer, look up a positional
1606 argument by index. Otherwise, look up a keyword argument.
1609 for call in self.pool_mock.patched['SendNotification'].call_args_list:
1610 call_args, call_kwargs = call
1611 if change == call_args[1]:
1612 if isinstance(key, int):
1613 return call_args[key]
1614 return call_kwargs[key]
1615 names.append(call_args[1])
1617 # Verify that |change| is present at all. This should always fail.
1618 self.assertIn(change, names)
1620 def assertEqualNotifyArg(self, value, change, idx):
1621 """Verify that |value| equals self.GetNotifyArg(|change|, |idx|)."""
1622 self.assertEqual(str(value), str(self.GetNotifyArg(change, idx)))
1624 def testSubmitPool(self):
1625 """Test that we can submit a pool successfully."""
1626 self.SubmitPool(submitted=self.patches)
1628 def testRejectCLs(self):
1629 """Test that we can reject a CL successfully."""
1630 self.SubmitPool(submitted=self.patches, failed_to_apply=True)
1632 def testSubmitCycle(self):
1633 """Submit a cyclic set of dependencies"""
1634 self.patch_mock.SetCQDependencies(self.patches[0], [self.patches[1]])
1635 self.SubmitPool(submitted=self.patches)
1637 def testSubmitReverseCycle(self):
1638 """Submit a cyclic set of dependencies, specified in reverse order."""
1639 self.patch_mock.SetCQDependencies(self.patches[1], [self.patches[0]])
1640 self.patch_mock.SetGerritDependencies(self.patches[1], [])
1641 self.patch_mock.SetGerritDependencies(self.patches[0], [self.patches[1]])
1642 self.SubmitPool(submitted=self.patches[::-1])
1644 def testRedundantCQDepend(self):
1645 """Submit a cycle with redundant CQ-DEPEND specifications."""
1646 self.patches = self.GetPatches(4)
1647 self.patch_mock.SetCQDependencies(self.patches[0], [self.patches[-1]])
1648 self.patch_mock.SetCQDependencies(self.patches[1], [self.patches[-1]])
1649 self.SubmitPool(submitted=self.patches)
1651 def testSubmitPartialCycle(self):
1652 """Submit a failed cyclic set of dependencies"""
1653 self.pool_mock.max_submits = 1
1654 self.patch_mock.SetCQDependencies(self.patches[0], [self.patches[1]])
1655 self.SubmitPool(submitted=self.patches, rejected=[self.patches[1]])
1656 (submitted, rejected) = self.pool_mock.GetSubmittedChanges()
1657 failed_submit = validation_pool.PatchFailedToSubmit(
1658 rejected, validation_pool.ValidationPool.INCONSISTENT_SUBMIT_MSG)
1659 bad_submit = validation_pool.PatchSubmittedWithoutDeps(
1660 submitted, failed_submit)
1661 self.assertEqualNotifyArg(failed_submit, rejected, 'error')
1662 self.assertEqualNotifyArg(bad_submit, submitted, 'failure')
1664 def testSubmitFailedCycle(self):
1665 """Submit a failed cyclic set of dependencies"""
1666 self.pool_mock.max_submits = 0
1667 self.patch_mock.SetCQDependencies(self.patches[0], [self.patches[1]])
1668 self.SubmitPool(submitted=[self.patches[0]], rejected=self.patches)
1669 (attempted,) = self.pool_mock.GetSubmittedChanges()
1670 (rejected,) = [x for x in self.patches if x != attempted]
1671 failed_submit = validation_pool.PatchFailedToSubmit(
1672 attempted, validation_pool.ValidationPool.INCONSISTENT_SUBMIT_MSG)
1673 dep_failed = cros_patch.DependencyError(rejected, failed_submit)
1674 self.assertEqualNotifyArg(failed_submit, attempted, 'error')
1675 self.assertEqualNotifyArg(dep_failed, rejected, 'error')
1677 def testConflict(self):
1678 """Submit a change that conflicts with TOT."""
1679 error = gob_util.GOBError(httplib.CONFLICT, 'Conflict')
1680 self.pool_mock.submit_results[self.patches[0]] = error
1681 self.SubmitPool(submitted=[self.patches[0]], rejected=self.patches[::-1])
1682 notify_error = validation_pool.PatchConflict(self.patches[0])
1683 self.assertEqualNotifyArg(notify_error, self.patches[0], 'error')
1685 def testServerError(self):
1686 """Test case where GOB returns a server error."""
1687 error = gerrit.GerritException('Internal server error')
1688 self.pool_mock.submit_results[self.patches[0]] = error
1689 self.SubmitPool(submitted=[self.patches[0]], rejected=self.patches[::-1])
1690 notify_error = validation_pool.PatchFailedToSubmit(self.patches[0], error)
1691 self.assertEqualNotifyArg(notify_error, self.patches[0], 'error')
1693 def testNotCommitReady(self):
1694 """Test that a CL is rejected if its approvals were pulled."""
1695 def _ReloadPatches(patches):
1696 reloaded = copy.deepcopy(patches)
1697 self.PatchObject(reloaded[1], 'HasApproval', return_value=False)
1699 self.PatchObject(validation_pool.ValidationPool, 'ReloadChanges',
1700 side_effect=_ReloadPatches)
1701 self.SubmitPool(submitted=self.patches[:1], rejected=self.patches[1:])
1703 def testAlreadyMerged(self):
1704 """Test that a CL that was chumped during the run was not rejected."""
1705 self.PatchObject(self.patches[0], 'IsAlreadyMerged', return_value=True)
1706 self.SubmitPool(submitted=self.patches[1:], rejected=[])
1708 def testModified(self):
1709 """Test that a CL that was modified during the run is rejected."""
1710 def _ReloadPatches(patches):
1711 reloaded = copy.deepcopy(patches)
1712 reloaded[1].patch_number += 1
1714 self.PatchObject(validation_pool.ValidationPool, 'ReloadChanges',
1715 side_effect=_ReloadPatches)
1716 self.SubmitPool(submitted=self.patches[:1], rejected=self.patches[1:])
1719 class SubmitPartialPoolTest(BaseSubmitPoolTestCase):
1720 """Test the SubmitPartialPool function."""
1723 # Set up each patch to be in its own project, so that we can easily
1724 # request to ignore failures for the specified patch.
1725 for patch in self.patches:
1726 patch.project = str(patch)
1728 self.stage_name = 'MyHWTest'
1730 def GetTracebacks(self):
1731 """Return a list containing a single traceback."""
1732 traceback = results_lib.RecordedTraceback(
1733 self.stage_name, self.stage_name, Exception(), '')
1736 def IgnoreFailures(self, patch):
1737 """Set us up to ignore failures for the specified |patch|."""
1738 self.ignores[patch] = [self.stage_name]
1740 def testSubmitNone(self):
1741 """Submit no changes."""
1742 self.SubmitPool(submitted=(), rejected=self.patches)
1744 def testSubmitAll(self):
1745 """Submit all changes."""
1746 self.IgnoreFailures(self.patches[0])
1747 self.IgnoreFailures(self.patches[1])
1748 self.SubmitPool(submitted=self.patches, rejected=[])
1750 def testSubmitFirst(self):
1751 """Submit the first change in a series."""
1752 self.IgnoreFailures(self.patches[0])
1753 self.SubmitPool(submitted=[self.patches[0]], rejected=[self.patches[1]])
1755 def testSubmitSecond(self):
1756 """Attempt to submit the second change in a series."""
1757 self.IgnoreFailures(self.patches[1])
1758 self.SubmitPool(submitted=[], rejected=[self.patches[0]])
1761 class LoadManifestTest(cros_test_lib.TempDirTestCase):
1762 """Tests loading the manifest."""
1764 manifest_content = (
1765 '<?xml version="1.0" ?><manifest>'
1766 '<pending_commit branch="master" '
1767 'change_id="Ieeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee1" '
1768 'commit="1ddddddddddddddddddddddddddddddddddddddd" '
1769 'fail_count="2" gerrit_number="17000" owner_email="foo@chromium.org" '
1770 'pass_count="0" patch_number="2" project="chromiumos/taco/bar" '
1771 'project_url="https://base_url/chromiumos/taco/bar" '
1772 'ref="refs/changes/51/17000/2" remote="cros" total_fail_count="3"/>'
1776 """Sets up a pool."""
1777 self.pool = MakePool()
1779 def testAddPendingCommitsIntoPool(self):
1780 """Test reading the pending commits and add them into the pool."""
1781 with tempfile.NamedTemporaryFile() as f:
1782 f.write(self.manifest_content)
1784 self.pool.AddPendingCommitsIntoPool(f.name)
1786 self.assertEqual(self.pool.changes[0].owner_email, 'foo@chromium.org')
1787 self.assertEqual(self.pool.changes[0].tracking_branch, 'master')
1788 self.assertEqual(self.pool.changes[0].remote, 'cros')
1789 self.assertEqual(self.pool.changes[0].gerrit_number, '17000')
1790 self.assertEqual(self.pool.changes[0].project, 'chromiumos/taco/bar')
1791 self.assertEqual(self.pool.changes[0].project_url,
1792 'https://base_url/chromiumos/taco/bar')
1793 self.assertEqual(self.pool.changes[0].change_id,
1794 'Ieeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee1')
1795 self.assertEqual(self.pool.changes[0].commit,
1796 '1ddddddddddddddddddddddddddddddddddddddd')
1797 self.assertEqual(self.pool.changes[0].fail_count, 2)
1798 self.assertEqual(self.pool.changes[0].pass_count, 0)
1799 self.assertEqual(self.pool.changes[0].total_fail_count, 3)
1802 if __name__ == '__main__':
1803 cros_test_lib.main()