blob: ad18d67f4197f281d1fa14b44cf5f84920156166 [file] [log] [blame]
# Copyright 2017 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.
import datetime
import json
from google.protobuf import duration_pb2
from google.protobuf import json_format
from google.protobuf import timestamp_pb2
from recipe_engine import recipe_test_api
from PB.go.chromium.org.luci.buildbucket.proto import build as build_pb2
from PB.go.chromium.org.luci.buildbucket.proto import builder as builder_pb2
from PB.go.chromium.org.luci.buildbucket.proto import common as common_pb2
from PB.go.chromium.org.luci.buildbucket.proto \
import builds_service as builds_service_pb2
from PB.go.chromium.org.luci.lucictx import sections as sections_pb2
from . import util
class BuildbucketTestApi(recipe_test_api.RecipeTestApi):
def build(self, build_message):
"""Emulates a buildbucket build.
build_message is a buildbucket.build_pb2.Build.
"""
ret = self.test(None)
ret.properties.update(**{
'$recipe_engine/buildbucket': {
'build': json.loads(json_format.MessageToJson(build_message)),
},
})
# Mock luci_context based on the build info.
realm_ctx = sections_pb2.Realm(name='%s:%s' % (
build_message.builder.project, build_message.builder.bucket))
rdb_ctx = sections_pb2.ResultDB(
current_invocation=sections_pb2.ResultDBInvocation(
name=build_message.infra.resultdb.invocation,
update_token='token',
),
hostname='rdbhost',
)
ret.luci_context.update({
'realm': json_format.MessageToDict(realm_ctx),
'resultdb': json_format.MessageToDict(rdb_ctx),
})
return ret
def _default_git_repo(self, project): # pragma: no cover
if 'internal' in project:
return 'https://chrome-internal.googlesource.com/' + project
return 'https://chromium.googlesource.com/' + project
def ci_build_message(
self,
project='project',
bucket='ci', # shortname.
builder='builder',
git_repo=None,
git_ref='refs/heads/main',
revision='2d72510e447ab60a9728aeea2362d8be2cbd7789',
build_number=0,
build_id=8945511751514863184,
priority=30,
tags=None,
status=None,
experiments=(),
exe=None,
execution_timeout=None,
start_time=None,
):
"""Returns a typical buildbucket CI build scheduled by luci-scheduler."""
git_repo = git_repo or self._default_git_repo(project)
gitiles_host, gitiles_project = util.parse_gitiles_repo_url(git_repo)
if not gitiles_host or not gitiles_project:
raise ValueError('invalid repo %s' % (git_repo,))
build = build_pb2.Build(
id=build_id,
number=build_number,
tags=tags or [],
builder=builder_pb2.BuilderID(
project=project,
bucket=bucket,
builder=builder,
),
created_by='user:luci-scheduler@appspot.gserviceaccount.com',
create_time=timestamp_pb2.Timestamp(seconds=1527292217),
input=build_pb2.Build.Input(
gitiles_commit=common_pb2.GitilesCommit(
host=gitiles_host,
project=gitiles_project,
ref=git_ref,
id=revision,
),
experiments=experiments),
infra=build_pb2.BuildInfra(
swarming=build_pb2.BuildInfra.Swarming(priority=priority),
resultdb=build_pb2.BuildInfra.ResultDB(
invocation='invocations/build:%d' % build_id),
))
if execution_timeout:
build.execution_timeout.FromSeconds(execution_timeout)
if start_time:
assert isinstance(start_time, datetime.datetime), start_time
build.start_time.FromDatetime(start_time)
if status:
build.status = common_pb2.Status.Value(status)
if exe:
build.exe.CopyFrom(exe)
return build
def ci_build(self, *args, **kwargs):
"""Returns a typical buildbucket CI build scheduled by luci-scheduler.
A shortcut for api.buildbucket.build(api.buildbucket.ci_build_message()).
Usage:
yield (api.test('basic') +
api.buildbucket.ci_build(project='my-proj', builder='win'))
"""
return self.build(self.ci_build_message(*args, **kwargs))
def try_build_message(
self,
project='project',
bucket='try', # shortname.
builder='builder',
gerrit_changes=None,
git_repo=None,
git_ref='refs/heads/main',
change_number=123456,
patch_set=7,
revision=None,
build_number=0,
build_id=8945511751514863184,
critical='UNSET', # other accepted values are ['YES', 'NO']
priority=30,
created_by=None,
tags=None,
status=None,
experiments=(),
exe=None,
execution_timeout=None,
start_time=None,
):
"""Emulate typical buildbucket try build scheduled by CQ.
Usage:
yield (api.test('basic') +
api.buildbucket.try_build(project='my-proj', builder='win'))
"""
if created_by is None:
created_by = 'project:' + project
git_repo = git_repo or self._default_git_repo(project)
git_host, git_project = util.parse_gitiles_repo_url(git_repo)
gerrit_host = git_host
gs_suffix = '.googlesource.com'
if gerrit_host.endswith(gs_suffix):
prefix = gerrit_host[:-len(gs_suffix)]
if not prefix.endswith('-review'):
gerrit_host = '%s-review%s' % (prefix, gs_suffix)
tags = list(tags) if tags else []
# CQ always sets "cq_experimental:" tag, which is then used by Gerrit
# Buildbucket plugin to hide "cq_experimental:true" builds.
if all(t.key != 'cq_experimental' for t in tags):
tags.append(common_pb2.StringPair(key='cq_experimental', value='false'))
if gerrit_changes is None:
gerrit_changes = [
common_pb2.GerritChange(
host=gerrit_host,
project=git_project,
change=change_number,
patchset=patch_set,
),
]
build = build_pb2.Build(
id=build_id,
number=build_number,
tags=tags,
builder=builder_pb2.BuilderID(
project=project,
bucket=bucket,
builder=builder,
),
created_by=created_by,
create_time=timestamp_pb2.Timestamp(seconds=1527292217),
critical=common_pb2.Trinary.Value(critical),
input=build_pb2.Build.Input(gerrit_changes=gerrit_changes,
experiments=experiments),
infra=build_pb2.BuildInfra(
swarming=build_pb2.BuildInfra.Swarming(priority=priority),
resultdb=build_pb2.BuildInfra.ResultDB(
invocation='invocations/build:%d' % build_id),
),
)
if execution_timeout:
build.execution_timeout.FromSeconds(execution_timeout)
if start_time:
assert isinstance(start_time, datetime.datetime), start_time
build.start_time.FromDatetime(start_time)
if revision:
c = build.input.gitiles_commit
c.host = git_host
c.project = git_project
c.ref = git_ref
c.id = revision
if status:
build.status = common_pb2.Status.Value(status)
if exe:
build.exe.CopyFrom(exe)
return build
def try_build(self, *args, **kwargs):
"""Emulates a typical buildbucket try build scheduled by CQ.
Shortcut for api.buildbucket.build(api.buildbucket.try_build_message()).
Usage:
yield (api.test('basic') +
api.buildbucket.try_build(project='my-proj', builder='win'))
"""
return self.build(self.try_build_message(*args, **kwargs))
def generic_build(
self,
project='project',
bucket='cron', # shortname.
builder='builder',
build_number=0,
build_id=8945511751514863184,
priority=30,
tags=None,
experiments=()):
"""Emulates a generic build w/o input GitilesCommit or GerritChanges."""
build = build_pb2.Build(
id=build_id,
number=build_number,
tags=tags,
builder=builder_pb2.BuilderID(
project=project,
bucket=bucket,
builder=builder,
),
created_by='user:user@example.com',
create_time=timestamp_pb2.Timestamp(seconds=1527292217),
infra=build_pb2.BuildInfra(
swarming=build_pb2.BuildInfra.Swarming(priority=priority),
resultdb=build_pb2.BuildInfra.ResultDB(
invocation='invocations/build:%d' % build_id),
),
input=build_pb2.Build.Input(experiments=experiments),
)
return self.build(build)
def tags(self, **tags):
"""Alias for tags in util.py. See doc there."""
return util.tags(**tags)
def exe(self, cipd_pkg, cipd_ver=None, cmd=None):
"""Emulates a build executable."""
return common_pb2.Executable(
cipd_package=cipd_pkg,
cipd_version=cipd_ver,
cmd=cmd,
)
def simulated_collect_output(self, builds, step_name=None):
"""Simulates a buildbucket.collect call."""
step_name = step_name or 'buildbucket.collect'
return self.simulated_get_multi(builds, step_name='%s.get' % step_name)
def simulated_schedule_output(self, batch_response, step_name=None):
"""Simulates a buildbucket.schedule call."""
return self._simulated_batch_response(
batch_response, step_name or 'buildbucket.schedule')
def simulated_search_results(self, builds, step_name=None):
"""Simulates a buildbucket.search call."""
assert isinstance(builds, list), builds
assert all(isinstance(b, build_pb2.Build) for b in builds), builds
step_name = step_name or 'buildbucket.search'
lines = [
json.dumps(json_format.MessageToDict(b), sort_keys=True) for b in builds
]
output = "\n".join(lines)
return self.step_data(step_name, self.m.raw_io.stream_output_text(output))
def simulated_get(self, build, step_name=None):
"""Simulates a buildbucket.get call."""
return self._simulated_batch_response(
builds_service_pb2.BatchResponse(responses=[dict(get_build=build)]),
step_name or 'buildbucket.get')
def simulated_get_multi(self, builds, step_name=None):
"""Simulates a buildbucket.get_multi call."""
return self._simulated_batch_response(
builds_service_pb2.BatchResponse(
responses=[dict(get_build=b) for b in builds],
),
step_name or 'buildbucket.get_multi')
def simulated_cancel_output(self, batch_response, step_name=None):
"""Simulates a buildbucket.cancel call"""
return self._simulated_batch_response(
batch_response, step_name or 'buildbucket.cancel')
def _simulated_batch_response(self, batch_response, step_name):
"""Simulate that the given step will write the provided batch response into
step data. The return code will be 1 for step data if the responses contain
error. Otherwise, 0
"""
assert isinstance(batch_response, builds_service_pb2.BatchResponse)
ret_code = int(any(r.HasField('error') for r in batch_response.responses))
jsonish = json_format.MessageToDict(batch_response)
return self.step_data(
step_name, self.m.json.output_stream(jsonish, retcode=ret_code))