blob: 40940d3c5fd672aa8f5baf7061540bd27d2caeda [file] [log] [blame]
# Copyright 2019 The LUCI Authors. All rights reserved.
# Use of this source code is governed under the Apache License, Version 2.0
# that can be found in the LICENSE file.
from enum import Enum
from google.protobuf import json_format as json_pb
from PB.go.chromium.org.luci.cq.api.recipe.v1 import cq as cq_pb2
from recipe_engine import recipe_api
class CQApi(recipe_api.RecipeApi):
"""This module provides recipe API of LUCI CQ, aka pre-commit testing system.
More information about CQ:
https://chromium.googlesource.com/infra/luci/luci-go/+/master/cq
"""
class State(Enum):
INACTIVE = 0
DRY = 1
FULL = 2
# Re-bind constants for easier usage of CQApi:
# >>> if api.cq.state == api.cq.DRY:
INACTIVE = State.INACTIVE
DRY = State.DRY
FULL = State.FULL
class CQInactive(Exception):
"""Incorrect usage of CQApi method requring active CQ."""
def __init__(self, input_props, **kwargs):
super(CQApi, self).__init__(**kwargs)
self._input = input_props
self._state = None
self._triggered_build_ids = []
def initialize(self):
if self._input.active is False:
dry_run = self.m.properties.get('$recipe_engine/cq', {}).get('dry_run')
else:
dry_run = self._input.dry_run
if dry_run is None:
self._state = self.INACTIVE
elif dry_run:
self._state = self.DRY
else:
self._state = self.FULL
@property
def state(self):
"""CQ state pertaining to this recipe execution."""
return self._state
@property
def experimental(self):
"""Returns whether this build is triggered for a CQ experimental builder.
See `Builder.experiment_percentage` doc in [CQ
config](https://chromium.googlesource.com/infra/luci/luci-go/+/master/cq/api/config/v2/cq.proto)
Raises:
CQInactive if CQ is `INACTIVE` for this build.
"""
self._enforce_active()
return self._input.experimental
@property
def top_level(self):
"""Returns whether CQ triggered this build directly.
Can be spoofed. *DO NOT USE FOR SECURITY CHECKS.*
Raises:
CQInactive if CQ is `INACTIVE` for this build.
"""
self._enforce_active()
return self._input.top_level
@property
def ordered_gerrit_changes(self):
"""Returns list[bb_common_pb2.GerritChange] in order in which CLs should be
applied or submitted.
Raises:
CQInactive if CQ is `INACTIVE` for this build.
"""
self._enforce_active()
assert self.m.buildbucket.build.input.gerrit_changes, (
'you must simulate buildbucket.input.gerrit_changes in your test '
'in order to use api.cq.ordered_gerrit_changes')
return self.m.buildbucket.build.input.gerrit_changes
@property
def props_for_child_build(self):
"""Returns properties dict meant to be passed to child builds.
These will preserve the CQ context of the current build in the
about-to-be-triggered child build.
```python
properties = {'foo': bar, 'protolike': proto_message}
properties.update(api.cq.props_for_child_build)
req = api.buildbucket.schedule_request(
builder='child',
gerrit_changes=list(api.buildbucket.build.input.gerrit_changes),
properties=properties)
child_builds = api.buildbucket.schedule([req])
api.cq.record_triggered_builds(*child_builds)
```
The contents of returned dict should be treated as opaque blob,
it may be changed without notice.
"""
if not self._input.active:
return {}
msg = cq_pb2.Input()
msg.CopyFrom(self._input)
msg.top_level = False
return {'$recipe_engine/cq':
json_pb.MessageToDict(msg, preserving_proto_field_name=True)}
@property
def triggered_build_ids(self):
"""Returns recorded Buildbucket build ids as a list of integers."""
return self._triggered_build_ids
def record_triggered_builds(self, *builds):
"""Adds given Buildbucket builds to the list of triggered builds for CQ
to wait on corresponding build completion later.
Must be called after some step.
Expected usage:
```python
api.cq.record_triggered_builds(*api.buildbucket.schedule([req1, req2]))
```
Args:
* [`Build`](https://chromium.googlesource.com/infra/luci/luci-go/+/master/buildbucket/proto/build.proto)
objects, typically returned by `api.buildbucket.schedule`.
"""
return self.record_triggered_build_ids(*[b.id for b in builds])
def record_triggered_build_ids(self, *build_ids):
"""Adds given Buildbucket build ids to the list of triggered builds for CQ
to wait on corresponding build completion later.
Must be called after some step.
Args:
* build_id (int or string): Buildbucket build id.
"""
if not build_ids:
return
self._triggered_build_ids.extend(int(bid) for bid in build_ids)
assert self.m.step.active_result, 'must be called after some step'
self.m.step.active_result.presentation.properties['triggered_build_ids'] = [
str(build_id) for build_id in self._triggered_build_ids]
def _enforce_active(self):
if not self._input.active:
raise self.CQInactive()