| # 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_cq.py""" |
| |
| from __future__ import absolute_import |
| from __future__ import division |
| from __future__ import print_function |
| |
| import datetime |
| |
| 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 |
| import pytz |
| |
| from exonerator import innocent_cls_cq |
| 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 |
| FakeDatastoreClient = fake_datastore.FakeDatastoreClient |
| |
| |
| def DateTime(year=2017, month=1, day=1, with_tz=True): |
| """Helper function to reduce the verbosity of creating datetime objects. |
| |
| Args: |
| year: The year of the datetime to create. |
| month: The month. |
| day: The day of the month. |
| with_tz: Localize to UTC |
| """ |
| dt = datetime.datetime(year, month, day) |
| if with_tz: |
| return pytz.utc.localize(dt) |
| else: |
| return dt |
| |
| |
| # pylint: disable=protected-access |
| class TestInnocentCLs(cros_test_lib.MockTestCase): |
| """Tests the innocent_cls module with a fake MySQL server.""" |
| |
| default_build_values = { |
| 'builder_name': 'my builder', |
| 'build_config': 'a build config', |
| 'bot_hostname': 'bot hostname', |
| } |
| |
| def setUp(self): |
| """Sets up a fake datastore and some fake cidb data.""" |
| self.db = fake_cidb.FakeCIDBConnection() |
| self._SetupAnnotationsAndFinalizeMessage() |
| FakeDatastoreClient.reset_store() |
| self.ds = FakeDatastoreClient() |
| self.PatchObject(datastore, 'Client', lambda: self.ds) |
| |
| def _SetupAnnotationsAndFinalizeMessage(self): |
| """Creates rows in cidb for a finalized BAD_CL annotation.""" |
| # pylint: disable=protected-access |
| build_id_1 = self.db.InsertBuild( |
| start_time=DateTime(month=1, with_tz=False), |
| build_number=1, |
| **self.default_build_values) |
| build_id_2 = self.db.InsertBuild( |
| start_time=DateTime(month=2, with_tz=False), |
| build_number=2, |
| **self.default_build_values) |
| |
| action_1234 = clactions.CLAction.FromGerritPatchAndAction( |
| GerritPatchTuple(1234, 1, True), |
| constants.CL_ACTION_KICKED_OUT) |
| |
| action_4242 = clactions.CLAction.FromGerritPatchAndAction( |
| GerritPatchTuple(4242, 1, False), |
| constants.CL_ACTION_KICKED_OUT) |
| |
| action_4321 = clactions.CLAction.FromGerritPatchAndAction( |
| GerritPatchTuple(4321, 1, False), |
| constants.CL_ACTION_KICKED_OUT) |
| |
| action_9999 = clactions.CLAction.FromGerritPatchAndAction( |
| GerritPatchTuple(9999, 1, True), |
| constants.CL_ACTION_KICKED_OUT) |
| |
| self.db.InsertCLActions(build_id_1, [action_1234, action_4242]) |
| self.db.InsertCLActions(build_id_2, [action_4321, action_9999]) |
| |
| self.db._Insert('annotationsTable', { |
| 'build_id': build_id_1, |
| 'failure_category': constants.FAILURE_CATEGORY_BAD_CL, |
| 'last_annotator': 'bert', |
| 'failure_message': 'whoops!', |
| 'blame_url': 'crrev.com/c/4242', |
| }) |
| self.db._Insert('annotationsTable', { |
| 'build_id': build_id_2, |
| 'failure_category': constants.FAILURE_CATEGORY_BAD_CL, |
| 'last_annotator': 'ernie', |
| 'failure_message': 'yikes!', |
| 'blame_url': 'crrev.com/i/9999', |
| }) |
| |
| self.db._Insert('buildMessageTable', { |
| 'build_id': build_id_1, |
| 'message_type': constants.MESSAGE_TYPE_ANNOTATIONS_FINALIZED, |
| 'timestamp': '2017-01-01 00:00:00' |
| }) |
| self.db._Insert('buildMessageTable', { |
| 'build_id': build_id_2, |
| 'message_type': constants.MESSAGE_TYPE_ANNOTATIONS_FINALIZED, |
| 'timestamp': '2017-02-01 00:00:00' |
| }) |
| |
| def test_NewInnocentCLsWithMultipleCLsPerBatch(self): |
| """Tests that multiple innocent CLs get yielded in the same batch.""" |
| action_77 = clactions.CLAction.FromGerritPatchAndAction( |
| GerritPatchTuple(77, 1, True), |
| constants.CL_ACTION_KICKED_OUT) |
| action_88 = clactions.CLAction.FromGerritPatchAndAction( |
| GerritPatchTuple(88, 1, True), |
| constants.CL_ACTION_KICKED_OUT) |
| |
| build_id = self.db.InsertBuild( |
| start_time=DateTime(month=3), |
| build_number=3, |
| **self.default_build_values) |
| self.db.InsertCLActions(build_id, [action_77, action_88]) |
| |
| self.db._Insert('buildMessageTable', { |
| 'build_id': build_id, |
| 'message_type': constants.MESSAGE_TYPE_ANNOTATIONS_FINALIZED, |
| 'timestamp': '2017-03-01 00:00:00' |
| }) |
| |
| new_cls = list(innocent_cls_cq.NewInnocentCLs(self.db)) |
| assert len(new_cls) == 3 |
| assert new_cls[:2] == [ |
| [ChangeWithBuild(GerritPatchTuple(1234, 1, internal=True), 0)], |
| [ChangeWithBuild(GerritPatchTuple(4321, 1, internal=False), 1)], |
| ] |
| assert sorted(new_cls[2]) == [ |
| ChangeWithBuild(GerritPatchTuple(77, 1, internal=True), 2), |
| ChangeWithBuild(GerritPatchTuple(88, 1, internal=True), 2) |
| ] |
| |
| def test_NewInnocentCLsWithOldLastBuild(self): |
| key = self.ds.key( |
| innocent_cls_cq._LAST_BUILD_KEY, innocent_cls_cq._LAST_BUILD_KEY) |
| last_run = datastore.Entity(key=key) |
| last_run['timestamp'] = DateTime(year=1999, month=1) |
| self.ds.put(last_run) |
| |
| new_cls = list(innocent_cls_cq.NewInnocentCLs(self.db)) |
| assert new_cls == [ |
| [ChangeWithBuild(GerritPatchTuple(1234, 1, internal=True), 0)], |
| [ChangeWithBuild(GerritPatchTuple(4321, 1, internal=False), 1)], |
| ] |
| |
| def test_NewInnocentCLsWithNoLastBuild(self): |
| new_cls = list(innocent_cls_cq.NewInnocentCLs(self.db)) |
| second_pass = list(innocent_cls_cq.NewInnocentCLs(self.db)) |
| |
| assert new_cls == [ |
| [ChangeWithBuild(GerritPatchTuple(1234, 1, internal=True), 0)], |
| [ChangeWithBuild(GerritPatchTuple(4321, 1, internal=False), 1)], |
| ] |
| |
| assert second_pass == [] |
| |
| def test_NewInnocentCLsCheckpointingWithLimit(self): |
| """Tests that checkpointing with datastore works.""" |
| first_pass = list(innocent_cls_cq.NewInnocentCLs(self.db, limit=1)) |
| second_pass = list(innocent_cls_cq.NewInnocentCLs(self.db, limit=1)) |
| third_pass = list(innocent_cls_cq.NewInnocentCLs(self.db, limit=1)) |
| |
| assert first_pass == [ |
| [ChangeWithBuild(GerritPatchTuple(1234, 1, internal=True), 0)], |
| ] |
| |
| assert second_pass == [ |
| [ChangeWithBuild(GerritPatchTuple(4321, 1, internal=False), 1)], |
| ] |
| |
| assert third_pass == [] |
| |
| def test_NewInnocentCLsWithIntermediateLastBuild(self): |
| key = self.ds.key( |
| innocent_cls_cq._LAST_BUILD_KEY, innocent_cls_cq._LAST_BUILD_KEY) |
| last_run = datastore.Entity(key=key) |
| last_run['timestamp'] = DateTime(month=1, day=23) |
| self.ds.put(last_run) |
| |
| new_cls = list(innocent_cls_cq.NewInnocentCLs(self.db)) |
| assert new_cls == [ |
| [ChangeWithBuild(GerritPatchTuple(4321, 1, internal=False), 1)], |
| ] |
| |
| def test_NewInnocentCLsWithSameTimestampAsLastBuild(self): |
| """Test that we don't have an off-by-one-error with the last build. |
| |
| If we've processed the last build, don't reprocess it. |
| """ |
| key = self.ds.key( |
| innocent_cls_cq._LAST_BUILD_KEY, innocent_cls_cq._LAST_BUILD_KEY) |
| last_run = datastore.Entity(key=key) |
| last_run['timestamp'] = DateTime(month=2) |
| self.ds.put(last_run) |
| |
| # There's a grace period where messages earlier than LastBuild can be |
| # processed. The entities with CQ_PROCESSED_KEY have the final say about |
| # whether a row was processed yet. |
| build_ids = [0, 1, 2] |
| for build_id in build_ids: |
| processed_entity = datastore.Entity( |
| key=self.ds.key(innocent_cls_cq.CQ_PROCESSED_KEY, build_id)) |
| self.ds.put(processed_entity) |
| |
| new_cls = list(innocent_cls_cq.NewInnocentCLs(self.db)) |
| assert new_cls == [] |
| |
| def test_NewInnocentCLsWithNewLastBuild(self): |
| key = self.ds.key( |
| innocent_cls_cq._LAST_BUILD_KEY, innocent_cls_cq._LAST_BUILD_KEY) |
| last_run = datastore.Entity(key=key) |
| last_run['timestamp'] = DateTime(year=2099) |
| self.ds.put(last_run) |
| |
| new_cls = list(innocent_cls_cq.NewInnocentCLs(self.db)) |
| assert new_cls == [] |