| # Copyright 2017 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. |
| |
| """Tests for innocent_cls_precq.py""" |
| |
| from __future__ import absolute_import |
| from __future__ import division |
| from __future__ import print_function |
| |
| import datetime |
| |
| from chromite.lib import build_requests |
| from chromite.lib import clactions |
| from chromite.lib import constants |
| from chromite.lib import cros_test_lib |
| from chromite.lib import fake_cidb |
| from google.cloud import datastore # pylint: disable=E0611,import-error |
| |
| from exonerator import innocent_cls_cq |
| from exonerator import innocent_cls_cq_test |
| from exonerator import innocent_cls_precq |
| from exonerator import innocent_cls_precq_flake |
| from testing_utils import fake_datastore |
| |
| |
| # Make the tests a bit less verbose by making some aliases: |
| ChangeWithBuild = innocent_cls_cq.ChangeWithBuild |
| GerritPatchTuple = clactions.GerritPatchTuple |
| DateTime = innocent_cls_cq_test.DateTime |
| FakeDatastoreClient = fake_datastore.FakeDatastoreClient |
| |
| |
| # pylint: disable=protected-access |
| class TestInnocentCLsPreCQ(cros_test_lib.MockTestCase): |
| """Tests the innocent_cls module with a fake MySQL server.""" |
| |
| default_build_values = { |
| 'builder_name': 'my builder', |
| 'buildbot_generation': constants.BUILDBOT_GENERATION, |
| 'waterfall': 'chromiumos', |
| 'build_config': 'a build config', |
| 'bot_hostname': 'bot hostname', |
| } |
| |
| def setUp(self): |
| """Sets up a fake datastore and some fake cidb data.""" |
| self.build_ids = [] |
| self.build_requests = [] |
| self.db = fake_cidb.FakeCIDBConnection() |
| # datastore state is global. This resets its state between tests. |
| # TODO(phobbs) make the datastore state a fixture object with a .Client() |
| # method. |
| FakeDatastoreClient.reset_store() |
| self.ds = FakeDatastoreClient() |
| self._SetupPreCQs() |
| self.PatchObject(datastore, 'Client', lambda: self.ds) |
| |
| def _SetupPreCQs(self, |
| status=constants.BUILDER_STATUS_FAILED, |
| cl_number=1234): |
| """Creates rows in cidb for a finalized BAD_CL annotation.""" |
| self.build_ids.append( |
| self.db.InsertBuild( |
| 'test_build', 2, 'test_build', 'hostname', status=status)) |
| action_picked_up = clactions.CLAction.FromGerritPatchAndAction( |
| GerritPatchTuple(cl_number, 1, True), |
| constants.CL_ACTION_PICKED_UP) |
| pre_cq_status = ( |
| constants.CL_ACTION_PRE_CQ_FAILED |
| if status == constants.BUILDER_STATUS_FAILED |
| # 'verified' is inserted when a CL passes a given pre-cq builder |
| else constants.CL_ACTION_VERIFIED) |
| action_failed = clactions.CLAction.FromGerritPatchAndAction( |
| GerritPatchTuple(cl_number, 1, True), |
| pre_cq_status) |
| |
| self.db.InsertCLActions( |
| self.build_ids[-1], [action_picked_up, action_failed]) |
| |
| def _InsertSanityBuild(self, status): |
| self.build_ids.append( |
| self.db.InsertBuild( |
| 'test_build', 1, 'test_build', 'hostname', status=status)) |
| now = datetime.datetime.now() + datetime.timedelta(seconds=1) |
| self.build_requests.append( |
| build_requests.BuildRequest( |
| id=len(self.build_requests), |
| build_id=self.build_ids[-1], |
| request_build_config='test_build', |
| request_build_args='', |
| request_buildbucket_id='0', |
| request_reason='sanity-pre-cq', |
| timestamp=now)) |
| self.db.InsertBuildRequests([self.build_requests[-1]]) |
| |
| def test_NewInnocentCLs(self): |
| """An innocent CLs will get yielded.""" |
| self._InsertSanityBuild(constants.BUILDER_STATUS_FAILED) |
| new_cls = list(innocent_cls_precq.NewInnocentCLs(self.db, True)) |
| assert new_cls == [ |
| [ChangeWithBuild(GerritPatchTuple(1234, 1, internal=True), |
| self.build_ids[0]),] |
| ] |
| |
| def test_NewInnocentCLsWithNewerPreCQFailure(self): |
| """If the CL was picked up after the failure, we don't exonerate.""" |
| self._InsertSanityBuild(constants.BUILDER_STATUS_FAILED) |
| new_build_id = self.db.InsertBuild( |
| 'test_build', 1, 'test_build', 'hostname', |
| status=constants.BUILDER_STATUS_PASSED) |
| new_action = clactions.CLAction.FromGerritPatchAndAction( |
| GerritPatchTuple(1234, 1, True), |
| constants.CL_ACTION_PICKED_UP) |
| |
| self.db.InsertCLActions(new_build_id, [new_action]) |
| |
| new_cls = list(innocent_cls_precq.NewInnocentCLs(self.db, True)) |
| assert len(new_cls) == 0 |
| |
| def test_NewInnocentCLsAlreadyForgiven(self): |
| """If we already forgave the build, we don't forgive it again.""" |
| self._InsertSanityBuild(constants.BUILDER_STATUS_FAILED) |
| key = self.ds.key( |
| innocent_cls_precq.PRECQ_PROCESSED_KEY, self.build_ids[0]) |
| last_run = datastore.Entity(key=key) |
| last_run['1234'] = True |
| self.ds.put(last_run) |
| |
| new_cls = list(innocent_cls_precq.NewInnocentCLs(self.db, True)) |
| assert new_cls == [] |
| |
| def test_NewInnocentCLsCheckpointing(self): |
| """Test that checkpointing works. |
| |
| We shouldn't get the same CLs back after requesting NewInnocentCLs more |
| than once. |
| """ |
| self._InsertSanityBuild(constants.BUILDER_STATUS_FAILED) |
| first_pass = list(innocent_cls_precq.NewInnocentCLs(self.db, True)) |
| second_pass = list(innocent_cls_precq.NewInnocentCLs(self.db, True)) |
| third_pass = list(innocent_cls_precq.NewInnocentCLs(self.db, True)) |
| |
| assert first_pass == [ |
| [ChangeWithBuild( |
| GerritPatchTuple(1234, 1, internal=True), |
| self.build_ids[0]),] |
| ] |
| assert second_pass == [] |
| assert third_pass == [] |
| |
| def test_PassedSanityBuild(self): |
| """If the sanity build passed, don't exonerate any CLs.""" |
| self._InsertSanityBuild(constants.BUILDER_STATUS_PASSED) |
| got = list(innocent_cls_precq.NewInnocentCLs(self.db, True)) |
| assert got == [] |
| |
| def test_AlreadyExonerated(self): |
| """Don't re-exonerate a CL.""" |
| self._InsertSanityBuild(constants.BUILDER_STATUS_PASSED) |
| action_exonerated = clactions.CLAction.FromGerritPatchAndAction( |
| GerritPatchTuple(1234, 1, True), |
| constants.CL_ACTION_EXONERATED) |
| self.db.InsertCLActions(self.build_ids[0], [action_exonerated]) |
| |
| got = list(innocent_cls_precq.NewInnocentCLs(self.db, True)) |
| assert got == [] |
| |
| def test_NoSanity(self): |
| """Test that no CLs are yielded when there isn't a sanity failure.""" |
| got = list(innocent_cls_precq.NewInnocentCLs(self.db, True)) |
| assert got == [] |
| |
| def test_PreCQNaturalSanity(self): |
| """Test a "natural" sanity build. |
| |
| A natural sanity failure is when a pre-CQ config fails and passes for the |
| same patchset. |
| """ |
| # TODO(phobbs) this doesn't do any real testing of the MySQL queries, which |
| # are pretty complicated. Add an integration test which subclasses |
| # cidb_integration_test. |
| self._SetupPreCQs(status=constants.BUILDER_STATUS_PASSED) |
| self._SetupPreCQs(cl_number=9999) |
| self.PatchObject( |
| innocent_cls_precq_flake, |
| '_AffectedPreCQBuilds', |
| _FakeAffectedPreCQBuilds) |
| new_cls = list(innocent_cls_precq.NewInnocentCLs(self.db, True)) |
| assert new_cls == [ |
| [ChangeWithBuild( |
| GerritPatchTuple(9999, 1, internal=True), |
| self.build_ids[-1]),] |
| ] |
| |
| |
| def _FakeAffectedPreCQBuilds(conn, config): |
| """Finds the CLs with outstanding failures of |config|. |
| |
| Args: |
| conn: A FakeCIDBConnection instance. |
| config: A build config. |
| |
| Yields: |
| Tuples of (GerritPatchTuple, build_id) which are the patchsets whose |
| latest run of |config| was a failure. |
| """ |
| actions_by_cl = _ActionsByCL(conn) |
| builds_by_id = _BuildsById(conn) |
| def _BuildForAction(action): |
| """Returns the buildTable row associated with given the claction.""" |
| return builds_by_id[action['build_id']] |
| |
| cls_with_outstanding_failures = [] |
| for cl, actions in actions_by_cl.iteritems(): |
| actions_for_config = [ |
| a for a in actions |
| if _BuildForAction(a)['build_config'] == config |
| ] |
| if not actions_for_config: |
| continue |
| latest_build = max( |
| (_BuildForAction(action) for action in actions), |
| key=lambda build: build['start_time']) |
| |
| actions_for_latest_build = [ |
| action for action in actions |
| if action['build_id'] == latest_build['id'] |
| ] |
| # for each patchset with actions relevant to |config|, |
| # if the patchset's last action for |config| was a failure, include it. |
| if any(a['action'] == constants.CL_ACTION_PRE_CQ_FAILED |
| for a in actions_for_latest_build): |
| cls_with_outstanding_failures.append(( |
| GerritPatchTuple(*cl), latest_build['id'])) |
| |
| return cls_with_outstanding_failures |
| |
| |
| def _BuildsById(conn): |
| """Returns a mapping of build ids to buildTable rows.""" |
| builds_by_id = {} |
| for build in conn.buildTable: |
| builds_by_id[build['id']] = build |
| return builds_by_id |
| |
| |
| def _ActionsByCL(conn): |
| """Returns groups of CLActions rows grouped by CL patchset.""" |
| actions_by_cl = {} |
| for action in conn.clActionTable: |
| k = (action['change_number'], |
| action['patch_number'], |
| action['change_source'] == 'internal') |
| actions_by_cl.setdefault(k, []).append(action) |
| return actions_by_cl |