blob: 38001c19ef1421968abffced4f2413a1134b82f0 [file] [log] [blame]
# Copyright 2014 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Fake CIDB for unit testing."""
import datetime
from chromite.lib import cidb
from chromite.lib import constants
from chromite.lib import failure_message_lib
class FakeCIDBConnection:
"""Fake connection to a Continuous Integration database.
This class is a partial re-implementation of CIDBConnection, using
in-memory lists rather than a backing database.
"""
NUM_RESULTS_NO_LIMIT = -1
def __init__(self):
self.buildTable = []
self.buildStageTable = {}
self.failureTable = {}
self.fake_time = None
self.buildMessageTable = {}
def _TrimStatus(self, status):
"""Trim a build row to keys that should be returned by GetBuildStatus"""
return {
k: v
for (k, v) in status.items()
if k in cidb.CIDBConnection.BUILD_STATUS_KEYS
}
def SetTime(self, fake_time):
"""Sets a fake time to be retrieved by GetTime.
Args:
fake_time: datetime.datetime object.
"""
self.fake_time = fake_time
def GetTime(self):
"""Gets the current database time."""
return self.fake_time or datetime.datetime.now()
def InsertBuild(
self,
builder_name,
build_number,
build_config,
bot_hostname,
master_build_id=None,
timeout_seconds=None,
status=constants.BUILDER_STATUS_PASSED,
important=None,
buildbucket_id=None,
milestone_version=None,
platform_version=None,
start_time=None,
build_type=None,
branch=None,
):
"""Insert a build row.
Note this API slightly differs from cidb as we pass status to avoid
having to have a later FinishBuild call in testing.
"""
if start_time is None:
start_time = datetime.datetime.now()
deadline = None
if timeout_seconds is not None:
timediff = datetime.timedelta(seconds=timeout_seconds)
deadline = start_time + timediff
build_id = len(self.buildTable)
row = {
"id": build_id,
"builder_name": builder_name,
"buildbot_generation": cidb.BUILDBOT_GENERATION,
# While waterfall is nullable all non waterfall entries show empty
# string, sticking to the convention.
"waterfall": "",
"build_number": build_number,
"build_config": build_config,
"bot_hostname": bot_hostname,
"start_time": start_time,
"master_build_id": master_build_id,
"deadline": deadline,
"status": status,
"finish_time": start_time,
"important": important,
"buildbucket_id": buildbucket_id,
"final": False,
"milestone_version": milestone_version,
"platform_version": platform_version,
"build_type": build_type,
"branch": branch,
}
self.buildTable.append(row)
return build_id
def FinishBuild(self, build_id, status=None, summary=None, strict=True):
"""Update the build with finished status."""
build = self.buildTable[build_id]
if strict and build["final"]:
return 0
values = {}
if status is not None:
values.update(status=status)
if summary is not None:
values.update(summary=summary)
values.update(finish_time=datetime.datetime.now(), final=True)
if values:
build.update(values)
return 1
else:
return 0
def UpdateMetadata(self, build_id, metadata):
"""See cidb.UpdateMetadata.
Args:
build_id: The build to update.
metadata: A cbuildbot metadata object. Or, a dictionary (note: using
a dictionary is not supported by the base cidb API, but is
provided for this fake class for ease of use in test set-up
code).
"""
d = metadata if isinstance(metadata, dict) else metadata.GetDict()
versions = d.get("version") or {}
self.buildTable[build_id].update(
{
"chrome_version": versions.get("chrome"),
"milestone_version": versions.get("milestone"),
"platform_version": versions.get("platform"),
"full_version": versions.get("full"),
"sdk_version": d.get("sdk-versions"),
"toolchain_url": d.get("toolchain-url"),
"build_type": d.get("build_type"),
"important": d.get("important"),
}
)
return 1
def InsertBuildStage(
self,
build_id,
name,
board=None,
status=constants.BUILDER_STATUS_PLANNED,
):
build_stage_id = len(self.buildStageTable)
row = {
"build_id": build_id,
"name": name,
"board": board,
"status": status,
}
self.buildStageTable[build_stage_id] = row
return build_stage_id
def InsertBoardPerBuild(self, build_id, board):
# TODO(akeshet): Fill this placeholder.
pass
def InsertBuildMessage(
self,
build_id,
message_type=None,
message_subtype=None,
message_value=None,
board=None,
):
"""Insert a build message.
Args:
build_id: primary key of build recording this message.
message_type: Optional str name of message type.
message_subtype: Optional str name of message subtype.
message_value: Optional value of message.
board: Optional str name of the board.
Returns:
The build message id (string).
"""
if message_type:
message_type = message_type[:240]
if message_subtype:
message_subtype = message_subtype[:240]
if message_value:
message_value = message_value[:480]
if board:
board = board[:240]
build_message_id = len(self.buildMessageTable)
values = {
"build_id": build_id,
"message_type": message_type,
"message_subtype": message_subtype,
"message_value": message_value,
"board": board,
}
self.buildMessageTable[build_message_id] = values
return build_message_id
def GetBuildMessages(
self, build_id, message_type=None, message_subtype=None
):
"""Get the build messages of the given build id.
Args:
build_id: build id (string) of the build to get messages.
message_type: Get messages with the specific message_type (string)
if message_type is not None.
message_subtype: Get messages with the specific message_subtype
(string) if message_subtype is not None.
Returns:
A list of build messages (in the format of dict).
"""
messages = []
for v in self.buildMessageTable.values():
if (
v["build_id"] == build_id
and (message_type is None or v["message_type"] == message_type)
and (
message_subtype is None
or v["message_subtype"] == message_subtype
)
):
messages.append(v)
return messages
def StartBuildStage(self, build_stage_id):
if build_stage_id > len(self.buildStageTable):
return
self.buildStageTable[build_stage_id][
"status"
] = constants.BUILDER_STATUS_INFLIGHT
def WaitBuildStage(self, build_stage_id):
if build_stage_id > len(self.buildStageTable):
return
self.buildStageTable[build_stage_id][
"status"
] = constants.BUILDER_STATUS_WAITING
def FinishBuildStage(self, build_stage_id, status):
if build_stage_id > len(self.buildStageTable):
return
self.buildStageTable[build_stage_id]["status"] = status
def GetBuildStatus(self, build_id):
"""Gets the status of the build."""
try:
return self._TrimStatus(self.buildTable[build_id])
except IndexError:
return None
def GetBuildStatuses(self, build_ids):
"""Gets the status of the builds."""
return [self.GetBuildStatus(x) for x in build_ids]
def GetSlaveStatuses(self, master_build_id, buildbucket_ids=None):
"""Gets the slaves of given build."""
if buildbucket_ids is None:
return [
self._TrimStatus(b)
for b in self.buildTable
if b["master_build_id"] == master_build_id
]
else:
return [
self._TrimStatus(b)
for b in self.buildTable
if b["master_build_id"] == master_build_id
and b["buildbucket_id"] in buildbucket_ids
]
def GetBuildsStages(self, build_ids):
"""Quick implementation of fake GetBuildsStages."""
build_stages = []
build_statuses = {
b["id"]: b for b in self.buildTable if b["id"] in build_ids
}
# TODO(b/236161656): Fix.
# pylint: disable-next=consider-using-dict-items
for _id in self.buildStageTable:
build_id = self.buildStageTable[_id]["build_id"]
if build_id in build_ids:
stage = self.buildStageTable[_id].copy()
stage["build_config"] = build_statuses[build_id]["build_config"]
build_stages.append(stage)
return build_stages
def GetBuildsStagesWithBuildbucketIds(self, buildbucket_ids):
"""Quick implementation of fake GetBuildsStagesWithBuildbucketIds."""
build_stages = []
build_statuses = {
b["id"]: b
for b in self.buildTable
if b["buildbucket_id"] in buildbucket_ids
}
# TODO(b/236161656): Fix.
# pylint: disable-next=consider-using-dict-items
for _id in self.buildStageTable:
build_id = self.buildStageTable[_id]["build_id"]
if build_id in build_statuses:
stage = self.buildStageTable[_id].copy()
stage["build_config"] = build_statuses[build_id]["build_config"]
build_stages.append(stage)
return build_stages
# pylint: disable=unused-argument
def GetBuildHistory(
self,
build_config,
num_results,
ignore_build_id=None,
start_date=None,
end_date=None,
branch=None,
milestone_version=None,
platform_version=None,
starting_build_id=None,
ending_build_id=None,
final=False,
reverse=False,
):
"""Returns the build history for the given |build_config|."""
return self.GetBuildsHistory(
build_configs=[build_config],
num_results=num_results,
ignore_build_id=ignore_build_id,
start_date=start_date,
end_date=end_date,
milestone_version=milestone_version,
platform_version=platform_version,
starting_build_id=starting_build_id,
final=final,
reverse=reverse,
)
def GetBuildsHistory(
self,
build_configs,
num_results,
ignore_build_id=None,
start_date=None,
end_date=None,
milestone_version=None,
platform_version=None,
starting_build_id=None,
final=False,
reverse=False,
):
"""Returns the build history for the given |build_configs|."""
builds = sorted(
self.buildTable, reverse=(not reverse), key=lambda x: x["id"]
)
# Filter results.
if build_configs:
builds = [b for b in builds if b["build_config"] in build_configs]
if ignore_build_id is not None:
builds = [
b for b in builds if b["buildbucket_id"] != ignore_build_id
]
if start_date is not None:
builds = [b for b in builds if b["start_time"].date() >= start_date]
if end_date is not None:
builds = [
b
for b in builds
if "finish_time" in b
and b["finish_time"]
and b["finish_time"].date() <= end_date
]
if milestone_version is not None:
builds = [
b
for b in builds
if b.get("milestone_version") == milestone_version
]
if platform_version is not None:
builds = [
b
for b in builds
if b.get("platform_version") == platform_version
]
if starting_build_id is not None:
builds = [b for b in builds if b["id"] >= starting_build_id]
if final:
builds = [b for b in builds if b.get("final") is True]
if num_results != -1:
return builds[:num_results]
else:
return builds
def GetBuildStatusesWithBuildbucketIds(self, buildbucket_ids):
rows = []
for row in self.buildTable:
if (
row["buildbucket_id"] in buildbucket_ids
or str(row["buildbucket_id"]) in buildbucket_ids
):
rows.append(self._TrimStatus(row))
return rows
def GetBuildsFailures(self, build_ids):
"""Gets the failure entries for all listed build_ids.
Args:
build_ids: list of build ids of the builds to fetch failures for.
Returns:
A list of failure_message_lib.StageFailure instances.
"""
stage_failures = []
for build_id in build_ids:
b_dict = self.buildTable[build_id]
bs_table = {
k: v
for k, v in self.buildStageTable.items()
if v["build_id"] == build_id
}
for f_dict in self.failureTable.values():
if f_dict["build_stage_id"] in bs_table:
bs_dict = bs_table[f_dict["build_stage_id"]]
# pylint: disable=line-too-long
stage_failures.append(
failure_message_lib.StageFailure.GetStageFailureFromDicts(
f_dict, bs_dict, b_dict
)
)
# pylint: enable=line-too-long
return stage_failures
def UpdateBoardPerBuildMetadata(self, build_id, board, board_metadata):
"""Update the given board-per-build metadata.
This function is not being tested. A function stub to spare a
unittest error.
"""