blob: 821073a4eeed7680aa3fff91b03ec4331bf8a99f [file]
# Copyright 2025 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
"""Insert additional annotations to RawResult messages."""
from __future__ import annotations
import functools
import re
from collections.abc import Callable
from recipe_engine import recipe_api
from PB.go.chromium.org.luci.buildbucket.proto import common as common_pb
from PB.recipe_engine import result as result_pb
def _get_reasons(*exceptions: Exception) -> list[str]:
reasons = []
for exception in exceptions:
if isinstance(exception, ExceptionGroup):
for exc in exception.exceptions:
reasons.extend(_get_reasons(exc))
elif isinstance(exception, recipe_api.StepFailure):
reasons.append(exception.reason)
else:
reasons.append(str(exception))
return reasons
def _trim_summary_markdown(summary_markdown: str) -> str:
summary_markdown = re.sub(r'\n\n\n+', '\n\n', summary_markdown)
summary_markdown = re.sub(r'^\n+', '', summary_markdown)
summary_markdown = re.sub(r'\n\n+$', '\n', summary_markdown)
return summary_markdown
def wrap_run_steps(f: Callable[..., result_pb.RawResult | None]):
@functools.wraps(f)
def _wrapper(
api: recipe_api.RecipeScriptApi,
*args,
**kwargs,
) -> result_pb.RawResult:
experiments = [
x
for x in api.raw_result.buildbucket.build.input.experiments
if x.startswith('pigweed.')
]
experiment_lines: list[str] = []
if experiments:
experiment_lines.append('\n**Experiments**:')
for experiment in experiments:
experiment_lines.append(f'* {experiment}')
experiment_lines.append('')
experiment_part = '\n'.join(experiment_lines)
# If we're not doing bisection and there are no experiments, then
# there's nothing to add to the result, so directly pass through the
# return value.
if not api.raw_result.bisector.enabled and not experiment_part:
return f(api, *args, **kwargs)
infra_failure: list[Exception] = []
step_failure: list[Exception] = []
other_exception: list[Exception] = []
try:
result = f(api, *args, **kwargs)
if not result:
result = result_pb.RawResult(
summary_markdown='',
status=common_pb.SUCCESS,
)
summary_markdown = api.raw_result.bisector.summary(
f'{result.summary_markdown}\n{experiment_part}',
)
return result_pb.RawResult(
summary_markdown=_trim_summary_markdown(summary_markdown),
status=result.status,
)
except* recipe_api.InfraFailure as exc:
infra_failure.append(exc)
except* recipe_api.StepFailure as exc:
step_failure.append(exc)
except* Exception as exc:
other_exception.append(exc)
all_exceptions = [*infra_failure, *step_failure, *other_exception]
result: list[str] = []
result.append('\n\n'.join(_get_reasons(*all_exceptions)))
result.append(experiment_part)
summary_markdown = api.raw_result.bisector.summary('\n'.join(result))
status = common_pb.FAILURE
if other_exception or infra_failure:
status = common_pb.INFRA_FAILURE
if 'LUCI CV no longer needs this Tryjob' in summary_markdown:
status = common_pb.CANCELED
return result_pb.RawResult(
summary_markdown=_trim_summary_markdown(summary_markdown),
status=status,
)
return _wrapper
class RawResultApi(recipe_api.RecipeApi):
@property
def bisector(self):
# Don't make all recipes using this module also explicitly depend on
# the bisector.
return self.m.bisector
@property
def buildbucket(self):
# Don't make all recipes using this module also explicitly depend on
# the buildbucket.
return self.m.buildbucket