blob: 7575b82602db62cd19a671e24a7fb2c1128d2019 [file] [log] [blame]
# Copyright 2023 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Check the status of builders."""
import dataclasses
import datetime
import functools
from typing import Optional, Sequence, Union
from PB.go.chromium.org.luci.buildbucket.proto import (
build as build_pb2,
builder_common as builder_common_pb2,
common as common_pb2,
)
from recipe_engine import recipe_api
import RECIPE_MODULES.fuchsia.buildbucket_util.api as buildbucket_util
@dataclasses.dataclass
class BuilderStatus:
_api: recipe_api.RecipeApi
project: str
bucket: str
builder: str
exists: bool
builds: tuple
incomplete_included: bool
@property
def link(self) -> str:
self._api.buildbucket.builder_url(
project=self.project,
bucket=self.bucket,
builder=self.builder,
)
class BuilderStatusApi(recipe_api.RecipeApi):
"""Check the status of builders."""
BuilderStatus = BuilderStatus
@functools.cached_property
def buildbucket_config(self):
return self.m.luci_config.buildbucket()
def _does_builder_exist(
self,
project: str,
bucket: str,
builder: str,
) -> bool:
if project != self.m.buildbucket.build.builder.project:
raise Exception("only the current project is supported") # pragma: no cover
# Most test cases won't hinge on whether a builder exists, so if no buckets
# or builders are present in the buildbucket config just pretend any
# requested builders exist.
if self._test_data.enabled and not self.buildbucket_config.buckets:
return True
return any(
bkt.name == bucket and b.name == builder
for bkt in self.buildbucket_config.buckets
for b in bkt.swarming.builders
)
# BuilderStatus is defined here but pylint thinks it isn't.
def retrieve(
self,
project: Optional[str] = None,
bucket: Optional[str] = None,
builder: Optional[str] = None,
n: int = 10,
include_incomplete: bool = True,
max_age: Optional[datetime.timedelta] = None,
timeout: Union[datetime.timedelta, int, float] = (
buildbucket_util.DEFAULT_SEARCH_TIMEOUT
),
) -> BuilderStatus: # pylint: disable=undefined-variable
this_builder: builder_common_pb2.BuilderID = self.m.buildbucket.build.builder
project: str = project or this_builder.project
bucket: str = bucket or this_builder.bucket
builder: str = builder or this_builder.builder
exists: bool = self._does_builder_exist(project, bucket, builder)
builds: Sequence[build_pb2.Build] = ()
if exists:
status: Optional[common_pb2.Status] = common_pb2.ENDED_MASK
if include_incomplete:
status = None
builds: Sequence[build_pb2.Build] = tuple(
self.m.buildbucket_util.last_n_builds(
project=project,
bucket=bucket,
builder=builder,
status=status,
n=n,
fields=("status",),
max_age=max_age,
timeout=timeout,
)
)
return BuilderStatus(
_api=self.m,
project=project,
bucket=bucket,
builder=builder,
exists=exists,
builds=builds,
incomplete_included=include_incomplete,
)
def is_incomplete(self, status: BuilderStatus) -> bool:
"""Is a build running or scheduled?"""
# This method doesn't make sense if incomplete builds weren't included
# in the search.
assert status.incomplete_included
for build in status.builds:
if build.status in (common_pb2.SCHEDULED, common_pb2.STARTED):
return True
return False
def is_passing(self, status: BuilderStatus) -> bool:
"""Did the most recently completed build pass?"""
for build in status.builds:
if build.status in (common_pb2.SCHEDULED, common_pb2.STARTED):
continue
return build.status == common_pb2.SUCCESS
return False
def has_recently_passed(self, status: BuilderStatus) -> bool:
"""Has any recent build passed?"""
# Most test cases want most builders to appear passing. Make that the default
# in cases where test data was not supplied.
if self._test_data.enabled and not status.builds:
return True
for build in status.builds:
if build.status == common_pb2.SUCCESS:
return True
return False