blob: 24c7f03d2b1c3472be0e49e4e6ccb7ba7f97770b [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_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