blob: e231c555bd08665e3d38d05f32f8e8b160ed55eb [file] [log] [blame]
# Copyright 2016 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.
"""Tests a recipe CL by running a chromium builder."""
from recipe_engine.recipe_api import Property
from recipe_engine.post_process import Filter
DEPS = [
'recipe_engine/context',
'recipe_engine/file',
'recipe_engine/json',
'recipe_engine/led',
'recipe_engine/path',
'recipe_engine/properties',
'recipe_engine/python',
'recipe_engine/raw_io',
'recipe_engine/step',
'depot_tools/gclient',
'depot_tools/bot_update',
'depot_tools/tryserver',
]
PROPERTIES = {
# This is set by CQ when triggering a job.
'repo_name': Property(kind=str),
}
# If present in a CL description, will override the existing default builders
# with a custom list. Format is expected to be "<bucket>.<builder>".
BUILDER_FOOTER = 'Led-Recipes-Tester-Builder'
DEFAULT_BUILDERS = [
'luci.chromium.try:linux_chromium_rel_ng',
]
# CL to use when testing a recipe which touches chromium source.
CHROMIUM_SRC_TEST_CL = (
'https://chromium-review.googlesource.com/c/chromium/src/+/1286761')
def _checkout_project(api, workdir, gclient_config, patch):
api.file.ensure_directory(
'%s checkout' % gclient_config.solutions[0].name, workdir)
with api.context(cwd=workdir):
api.bot_update.ensure_checkout(
patch=patch, gclient_config=gclient_config)
def should_trigger_recipe(api, recipe, repo_path, recipes_py_path):
recipes_cfg_path = repo_path.join('infra', 'config', 'recipes.cfg')
affected_files = api.tryserver.get_files_affected_by_patch(repo_path)
try:
roll_step = api.python(
'analyze %s' % recipe,
recipes_py_path, [
'--package', recipes_cfg_path,
'analyze',
api.json.input({
'files': affected_files,
'recipes': [recipe],
}),
api.json.output()],
venv=True)
except api.step.StepFailure:
roll_step = api.step.active_result
if roll_step.json.output:
roll_step.presentation.logs['error'] = roll_step.json.output['error']
if recipe in roll_step.json.output.get('invalid_recipes', []):
api.python.failing_step(
'recipe invalid',
'analyze reported that the recipe \'%s\' was invalid. Something may'
' be wrong with the swarming task this is based on.' % recipe)
return recipe in roll_step.json.output['recipes']
# TODO(martiniss): make this work if repo_name != 'build'
def RunSteps(api, repo_name):
workdir_base = api.path['cache']
cl_workdir = workdir_base.join(repo_name)
client_py_workdir = workdir_base.join('client_py')
recipes_dir = workdir_base.join('recipe_engine')
# Needed to run `recipes.py analyze`.
recipes_config = api.gclient.make_config('recipes_py')
_checkout_project(api, recipes_dir, recipes_config, False)
recipes_dir = recipes_dir.join('infra', 'recipes-py')
# Check out the repo for the CL, applying the patch.
cl_config = api.gclient.make_config(repo_name)
_checkout_project(api, cl_workdir, cl_config, True)
triggered_jobs = {}
cl_footers = api.tryserver.get_footers() or {}
builders = cl_footers.get(BUILDER_FOOTER, DEFAULT_BUILDERS)
# We don't currently check anything about the list of builders to trigger.
# This is because the only existing builder which runs this recipe uses a
# service account which is only allowed to trigger jobs in the
# luci.chromium.try bucket. That builder is not in that bucket, so there's no
# possibility for running a tryjob on itself.
for builder in builders:
with api.context(cwd=cl_workdir.join('build')):
# intermediate result
ir = (api.
led('get-builder', builder))
# Recipe we run probably doesn't change between slices.
job_slice = ir.result['job_slices'][0]
# TODO(martiniss): Use recipe_cipd_source to determine which repo this
# recipe lives in. For now we assume the recipe lives in the repo the CL
# lives in.
recipe = job_slice['userland']['recipe_name']
if not should_trigger_recipe(
api, recipe, cl_workdir.join(repo_name),
recipes_dir.join('recipes.py')):
result = api.python.succeeding_step(
'not running a tryjob for %s' % recipe,
'`recipes.py analyze` indicates this recipe is not affected by the'
' files changed by the CL.')
continue
# FIXME: We should check if the recipe we're testing tests patches to
# chromium/src. For now just assume this works.
ir = ir.then('edit-cr-cl', CHROMIUM_SRC_TEST_CL)
result = (ir.
then('edit-recipe-bundle').
# Force the job to be experimental, since we don't want it
# affecting production services.
then('edit', '-p', '$recipe_engine/runtime={'
'"is_experimental":true, "is_luci": true}').
then('launch')).result
triggered_jobs[builder] = result['swarming']
if not triggered_jobs:
api.python.succeeding_step('exiting', 'no tryjobs to run, exiting')
return
# Check out the client-py repo, which gives us swarming.py.
client_py_config = api.gclient.make_config()
soln = client_py_config.solutions.add()
soln.name = 'client-py'
soln.url = 'https://chromium.googlesource.com/infra/luci/client-py'
_checkout_project(api, client_py_workdir, client_py_config, False)
for builder, job in triggered_jobs.items():
result = None
try:
result = api.python(
'collect %s task' % builder, client_py_workdir.join(
'client-py', 'swarming.py'), [
'collect', '-S', job['host_name'], job['task_id'],
# Needed because these jobs often take >40 minutes, since they're
# regular tryjobs.
'--print-status-updates',
# Don't need task stdout; if the task fails then the user should
# just look at the task itself.
'--task-output-stdout=none',
])
finally:
if result:
result.presentation.links['Swarming task'] = 'https://%s/task?id=%s' % (
job['host_name'], job['task_id'])
def GenTests(api):
yield (
api.test('basic') +
api.properties.tryserver(repo_name='build') +
api.step_data('led launch',
stdout=api.json.output({
'swarming':{
'host_name': 'chromium-swarm.appspot.com',
'task_id': 'beeeeeeeee5',
}
})) +
api.step_data('led get-builder',
stdout=api.json.output({
'job_slices': [{
'userland': {
'recipe_name': 'foo_recipe',
},
}],
})) +
api.step_data('analyze foo_recipe',
api.json.output({
'recipes': ['foo_recipe'],
})) +
api.override_step_data(
'gerrit changes', api.json.output(
[{'revisions': {1: {'_number': 12, 'commit': {
'message': 'nothing important'}}}}])) +
api.override_step_data(
'parse description', api.json.output({}))
)
yield (
api.test('no_jobs_to_run') +
api.properties.tryserver(repo_name='build') +
api.step_data('led get-builder',
stdout=api.json.output({
'job_slices': [{
'userland': {
'recipe_name': 'foo_recipe',
},
}],
})) +
api.step_data('analyze foo_recipe',
api.json.output({
'recipes': [],
})) +
api.override_step_data(
'gerrit changes', api.json.output(
[{'revisions': {1: {'_number': 12, 'commit': {
'message': 'nothing important'}}}}])) +
api.override_step_data(
'parse description', api.json.output({})) +
api.post_process(Filter('exiting'))
)
yield (
api.test('analyze_failure') +
api.properties.tryserver(repo_name='build') +
api.step_data('led get-builder',
stdout=api.json.output({
'job_slices': [{
'userland': {
'recipe_name': 'foo_recipe',
},
}],
})) +
api.step_data('analyze foo_recipe',
api.json.output({
'error': 'Bad analyze!!!!',
'invalid_recipes': ['foo_recipe'],
}), retcode=1) +
api.override_step_data(
'gerrit changes', api.json.output(
[{'revisions': {1: {'_number': 12, 'commit': {
'message': 'nothing important'}}}}])) +
api.override_step_data(
'parse description', api.json.output({})) +
api.post_process(Filter('recipe invalid'))
)
yield (
api.test('custom_builder') +
api.properties.tryserver(repo_name='build') +
api.step_data('led launch',
stdout=api.json.output({
'swarming':{
'host_name': 'chromium-swarm.appspot.com',
'task_id': 'beeeeeeeee5',
}
})) +
api.step_data('led get-builder',
stdout=api.json.output({
'job_slices': [{
'userland': {
'recipe_name': 'foo_recipe',
},
}],
})) +
api.step_data('analyze foo_recipe',
api.json.output({
'recipes': ['foo_recipe'],
})) +
api.override_step_data(
'gerrit changes', api.json.output(
[{'revisions': {1: {'_number': 12, 'commit': {
'message': BUILDER_FOOTER + ': arbitrary.blah'}}}}])) +
api.override_step_data(
'parse description', api.json.output(
{BUILDER_FOOTER: ['arbitrary.blah']})) +
api.post_process(Filter('led get-builder'))
)