blob: c8be0e37d5e552839be678217119b7cffda4fc69 [file] [log] [blame]
# 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',
'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.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 == []