blob: f54f1ec57bb64fff55f70b024137f5f76a70586f [file]
# Copyright 2020 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Tests a recipe CL by running a chromium builder."""
import attr
import collections
import re
from recipe_engine import post_process
from PB.go.chromium.org.luci.buildbucket.proto import common as common_pb2
from PB.go.chromium.org.luci.led.job import job as job_pb2
from RECIPE_MODULES.build.attr_utils import (attrib, attrs, cached_property,
enum, sequence)
from dataclasses import dataclass
from recipe_engine.recipe_api import RecipeScriptApi
from recipe_engine.recipe_test_api import RecipeTestApi
from RECIPE_MODULES.build import chromium_gerrit_utils
from RECIPE_MODULES.depot_tools import (
bot_update,
gclient,
gerrit,
gitiles,
tryserver,
)
from RECIPE_MODULES.recipe_engine import (
buildbucket,
cipd,
context,
defer,
file,
futures,
json,
led,
path,
properties,
raw_io,
step,
swarming,
)
@dataclass
class DEPS(RecipeScriptApi):
bot_update: bot_update.API
buildbucket: buildbucket.API
chromium_gerrit_utils: chromium_gerrit_utils.API
cipd: cipd.API
context: context.API
defer: defer.API
file: file.API
futures: futures.API
gclient: gclient.API
gerrit: gerrit.API
gitiles: gitiles.API
json: json.API
led: led.API
path: path.API
properties: properties.API
raw_io: raw_io.API
step: step.API
swarming: swarming.API
tryserver: tryserver.API
@dataclass
class TEST_DEPS(RecipeTestApi):
buildbucket: buildbucket.TEST_API
context: context.TEST_API
json: json.TEST_API
led: led.TEST_API
path: path.TEST_API
raw_io: raw_io.TEST_API
step: step.TEST_API
tryserver: tryserver.TEST_API
GERRIT_TOPIC = 'led-recipes-tester'
# If present in a CL description, will override the existing default builders
# with a custom list. Format is expected to be:
# "luci.<project>.<bucket>:<builder>"
# For example: "luci.chromium.try:some-builder"
BUILDER_FOOTER = 'Led-Recipes-Tester-Builder'
# When creating a chromium/src.git CL, the following are the options of files
# to modify in the CL depending on how extensive we want the testing to be in
# the subsequent chromium.try led builds.
# Touching `DEPS` causes analyze to compile and test all targets.
FILE_TO_TRIGGER_EXTENSIVE_TESTING = 'DEPS'
# Touching `content/test/content_browser_test_test.cc` results in just
# content_browsertests being built and run. This is a relatively fast, but still
# swarmed w/ multiple shards, test suite. This fast verification is used for
# "upstream-only-changes" on the assumption that no upstream code would (should)
# have variable effects based on WHICH test suite is executed, so we just need
# to pick SOME test suite.
FILE_TO_TRIGGER_SHORT_TESTING = 'content/test/content_browser_test_test.cc'
# Touching //ui/android/run_all_unittests.cc should similarly trigger just
# "ui_android_unittests suite on android. It should be relatively quick to both
# compile and test.
FILE_TO_TRIGGER_SHORT_ANDROID_TESTING = 'ui/android/run_all_unittests.cc'
ALL_FILES = [
FILE_TO_TRIGGER_EXTENSIVE_TESTING,
FILE_TO_TRIGGER_SHORT_TESTING,
FILE_TO_TRIGGER_SHORT_ANDROID_TESTING,
]
@attrs()
class BuilderToTrigger:
# The buildbucket v1 style name of the builder
name = attrib(str)
# The key of the CL to use when triggering the builder
file_to_change_for_quick_testing = attrib(enum(ALL_FILES))
file_to_change_for_full_testing = attrib(enum(ALL_FILES), default=None)
DEFAULT_BUILDERS = (
BuilderToTrigger(
'luci.chromium.try:linux-presubmit',
file_to_change_for_quick_testing=FILE_TO_TRIGGER_SHORT_TESTING),
BuilderToTrigger(
'luci.chromium.try:android-x64-rel',
file_to_change_for_quick_testing=FILE_TO_TRIGGER_SHORT_ANDROID_TESTING),
BuilderToTrigger(
'luci.chromium.try:linux-rel',
file_to_change_for_quick_testing=FILE_TO_TRIGGER_SHORT_TESTING,
file_to_change_for_full_testing=FILE_TO_TRIGGER_EXTENSIVE_TESTING),
BuilderToTrigger(
'luci.chromium.try:win-rel',
file_to_change_for_quick_testing=FILE_TO_TRIGGER_SHORT_TESTING),
)
@attrs()
class FilesToIgnore:
# A list of strings containing regex patterns of files to ignore. The patterns
# will be matched against the repo-root-relative paths of the affected files
# (e.g. recipes/recipe_modules/chromium_tests_builder_config/trybots.py). The
# patterns will be implicitly anchored to match the entire relative path.
patterns = attrib(sequence[str])
# If any files match `ignore_patterns`, a step will be created with the name
# `step_name` and the step text will combine `step_text` and the list of
# excluded files.
step_name = attrib(str)
step_text = attrib(str)
@cached_property
def regex(self):
# Create a single pattern that has all of the patterns as options. Surround
# each individual pattern with parentheses so that the | applies to the
# whole pattern, not just the boundary characters.
pattern = '|'.join('({})'.format(p) for p in self.patterns)
# Anchor the combined pattern so that the whole string must be matched.
# Parentheses are used so that the anchors apply to the entire combined
# pattern, not just the first and last options.
pattern = '^({})$'.format(pattern)
return re.compile(pattern)
# The builders and trybots files affect specific builders, so when changes only
# affect these files, launching the default set of builders will only provide a
# useful signal in the small percentage of CLs that affect those default
# builders and unnecesarily consume resources and time in the rest of the CLs
DEFAULT_FILES_TO_IGNORE = (FilesToIgnore(
patterns=[
'recipes/recipe_modules/chromium_tests_builder_config/{}'.format(r)
for r in (r'builders/.*\.py', r'trybots\.py')
],
step_name='ignoring per-builder config',
step_text=('The following affected files are being ignored because they'
' contain per-builder config that is unlikely to affect the'
' default builders:'),
),)
FILES_TO_ALWAYS_IGNORE = (
FilesToIgnore(
patterns=[
r'(.+/)?recipe_modules/[^/]+/examples/.+',
r'(.+/)?recipe_modules/[^/]+/tests/.+',
],
step_name='ignoring recipe tests',
step_text=('The following affected files'
' do not contain production recipe code:'),
),
FilesToIgnore(
patterns=[r'(.+/)*[A-Z_]*OWNERS'],
step_name='ignoring OWNERS files',
step_text=(
'The following affected files are being ignored because they are'
' OWNERS files, which contain repository metadata and are not part'
' of the recipes:'),
),
FilesToIgnore(
patterns=[
r'(.+/)?recipe_modules/chromium_tests_builder_config/migration/.+',
],
step_name='ignoring src-side config migration files',
step_text=(
'The following affected files are being ignored because they are'
' used for tracking the migration of builder configs src-side and'
' are not part of the recipes:'),
),
FilesToIgnore(
patterns=[r'(.+/)*PRESUBMIT.py'],
step_name='ignoring PRESUBMIT.py scripts',
step_text=('PRESUBMIT.py scripts are executed by the presubmit builder'
' and are not consumed by our recipes:'),
),
)
def _get_recipe(led_builder):
build_proto = led_builder.result.buildbucket.bbagent_args.build
try:
return build_proto.input.properties['recipe']
except ValueError as ex: # pragma: no cover
# If you see this in simulations, it's possible that you are missing a
# led_get_builder clause.
message = "build has no recipe set (%s): %r" % (ex, build_proto)
raise ValueError(message) from ex
def _process_footer_builders(api, builders):
bad_builders = sorted(b for b in builders if ':' not in b)
if bad_builders:
step_name = 'bad builders'
result = api.step(step_name, [])
result.presentation.status = api.step.FAILURE
result.presentation.step_text = ''.join(['\n ' + b for b in bad_builders])
raise api.step.StepFailure(step_name, result)
buckets = set(b.split(':', 1)[0] for b in builders)
unknown_buckets = set(b for b in buckets if b != 'luci.chromium.try')
if unknown_buckets:
step_name = 'unknown buckets'
result = api.step(step_name, [])
result.presentation.status = api.step.FAILURE
result.presentation.step_text = ''.join(
['\n ' + b for b in sorted(unknown_buckets)])
raise api.step.StepFailure(step_name, result)
return [
BuilderToTrigger(builder, FILE_TO_TRIGGER_EXTENSIVE_TESTING)
for builder in builders
]
def _get_builders_to_check(api):
"""Get the set of builders to test the recipe change against.
If the CL has Led-Recipes-Tester-Builder footer in its description,
then those builders will be the ones tested, otherwise a default set
of builders will be the ones tested. In both cases, branched versions
of the builders will be provisonally included.
Args:
* api - The recipe API object.
Returns:
A 2-element tuple:
* A sequence of BuilderToTrigger for the builders that should be
checked.
* A list of FilesToIgnore indicating files that should be ignored
when determining if a recipe is affected.
"""
files_to_ignore = []
footer_builders = api.tryserver.get_footer(BUILDER_FOOTER)
if footer_builders:
builders = _process_footer_builders(api, footer_builders)
else:
builders = DEFAULT_BUILDERS
files_to_ignore.extend(DEFAULT_FILES_TO_IGNORE)
files_to_ignore.extend(FILES_TO_ALWAYS_IGNORE)
return builders, files_to_ignore
def _ignore_affected_files(api, repo_path, affected_files, files_to_ignore):
"""Ignore files for analysis that match a regex.
Args:
api - The recipe API object.
repo_path - The path to the repo root.
affected_files - The list of files affected by the change.
files_to_ignore - A list of `FilesToIgnore` that detail the files to
ignore for analysis.
Returns:
The list of affected files with ignored files removed.
"""
ignored_files = {i: [] for i in files_to_ignore}
new_affected_files = []
for f in affected_files:
rel_path = api.path.relpath(f, repo_path)
ignored = False
for i in files_to_ignore:
if i.regex.match(rel_path):
ignored_files[i].append(f)
ignored = True
if not ignored:
new_affected_files.append(f)
for i, files in ignored_files.items():
if files:
step_result = api.step(i.step_name, [])
message = ['\n' + i.step_text]
message.extend('* {}'.format(f.replace('*', r'\*').replace('_', r'\_'))
for f in sorted(files))
step_result.presentation.step_text = '\n'.join(message)
return new_affected_files
def _get_led_builders(api, builders):
"""Get the led job definitions for the builders.
Args:
api - The recipe API object.
builders - A sequence of BuilderToTrigger.
Returns:
An OrderedDict mapping builder name to the led job definition for
the builder. Any provisional builders that do not exist will not
have an entry in the returned dictionary.
Raises:
StepFailure if getting the job definition for a builder fails and
the builder was not a provisional builder.
"""
led_builders = collections.OrderedDict()
with api.step.nest('get led builders'):
for builder in builders:
# Nest the led get-builder call because we don't get to control the step
# name and having the step name identify the buidler we're getting is more
# helpful than 'led get-builder', 'led get-builder (2)',
# 'led get-builder (3)', etc.
with api.step.nest('get ' + builder.name):
# By default, the priority of the tasks will be increased by 10, but
# since this builder runs as part of CQ for the recipe repos, we want
# the builds to run at regular priority
led_builders[builder.name] = api.led('get-builder', '-real-build',
'-adjust-priority', '0',
builder.name)
return led_builders
def _determine_affected_recipes(api, affected_files, recipes, recipes_py_path,
recipes_cfg_path):
"""Determine the set of recipes that are affected by the change.
Args:
api - The recipe API object.
affected_files - The set of files affected by the change.
recipes - The set of recipes that the prospective builders run.
recipes_py_path - A Path object identifying the location of the
recipes.py script.
recipes_cfg_path - A Path object identifying the location of the
recipes.cfg file.
Returns:
A set of the recipes that are affected by the change.
Raises:
StepFailure if analyzing the recipes fails.
"""
cmd = [
'vpython3',
recipes_py_path,
'--package',
recipes_cfg_path,
'analyze',
api.json.input({
'files': sorted(affected_files),
'recipes': sorted(recipes),
}),
api.json.output(),
]
step_name = 'determine affected recipes'
result = api.step(
step_name,
cmd,
ok_ret='any',
step_test_data=lambda: api.json.test_api.output({'recipes': []}),
)
json = getattr(result, 'json', None)
json_output = getattr(json, 'output', None)
if json_output is None:
result.presentation.status = api.step.EXCEPTION
result.presentation.step_text = 'Missing json output'
raise api.step.InfraFailure(step_name, result)
error = json_output.get('error', None)
if error:
result.presentation.logs['error'] = json_output['error']
invalid_recipes = json_output.get('invalid_recipes', [])
if invalid_recipes:
result.presentation.step_text = (
'\nanalyze reported that the recipes {!r} were invalid. '
'The associated builders may be incorrectly configured.'.format(
invalid_recipes))
result.presentation.logs['invalid recipes'] = invalid_recipes
if error or invalid_recipes:
result.presentation.status = api.step.FAILURE
raise api.step.StepFailure(step_name, result)
affected_recipes = json_output['recipes']
result.presentation.logs['recipes'] = '\n'.join(affected_recipes)
return set(affected_recipes)
def _get_filepath_to_change(affected_files, affected_recipes, builder, recipe,
recipes_cfg_path):
"""Returns the chromium/src.git filepath to modify for the Chromium CL.
Args:
affected_files - The set of files affected by the CL.
affected_recipes - The set of recipes affected by the CL.
builder - The name of the builder to get the CL category for.
recipe - The recipe of the builder.
recipes_cfg_path - A Path object identifying the location of the
recipes.cfg file.
Returns:
None if nothing should be triggered. Otherwise the chromium/src filepath
to modify.
"""
if recipe in affected_recipes:
return (builder.file_to_change_for_full_testing or
builder.file_to_change_for_quick_testing)
if str(recipes_cfg_path) in affected_files:
return builder.file_to_change_for_quick_testing
return None
def _test_builder(api, builder, led_builder, cl):
"""Try running a builder with the patched recipe.
Args:
api - The recipe API object.
builder - The name of the builder to test.
led_builder - The led job definition for the builder. It must
already have had its recipe bundle modified.
cl - Gerrit URL for the chromium/src.git CL to run the Chromium tryjob on.
Raises:
InfraFailure if any of the led calls fail.
StepFailure if the triggered task failed.
"""
with api.step.nest('test {}'.format(builder.name)) as presentation:
with api.step.nest('trigger'), api.context(infra_steps=True):
# FIXME: We should check if the recipe we're testing tests patches to
# chromium/src. For now just assume this works.
ir = led_builder.then('edit-cr-cl', cl)
# TODO(gbeaty) Once the recipe engine no longer supports annotations and
# nest step presentation is reflected in the UI before it's closed, we can
# just update the nest step's presentation
step_result = api.step.active_result
step_result.presentation.links['Test CL'] = cl
presentation.links.update(step_result.presentation.links)
# We used to set `is_experimental` to true, but the chromium recipe
# currently uses that to deprioritize swarming tasks, which results in
# very slow runtimes for the led task. Because this recipe blocks the
# build.git CQ, we decided the tradeoff to run these edited recipes in
# production mode instead would be better.
ir = ir.then('edit', '-exp', 'false')
ir = ir.then('launch', '-resultdb', 'on', '-bound-to-parent')
job = ir.launch_result
presentation.links['Build'] = job.build_url
build = api.buildbucket.collect_build(job.build_id, timeout=7200)
step_status = (
api.step.SUCCESS
if build.status == common_pb2.SUCCESS else api.step.FAILURE)
api.step.empty(
'build ends with {}'.format(common_pb2.Status.Name(build.status)),
status=step_status)
def RunSteps(api: DEPS):
gclient_config = api.gclient.make_config()
s = gclient_config.solutions.add()
s.url = api.tryserver.gerrit_change_repo_url
s.name = s.url.rsplit('/', 1)[-1]
gclient_config.got_revision_mapping[s.name] = 'got_revision'
with api.context(cwd=api.path.cache_dir / 'builder'):
update_result = api.bot_update.ensure_checkout(
patch=True, gclient_config=gclient_config)
repo_path = update_result.source_root.path
with api.context(cwd=repo_path):
affected_files = api.tryserver.get_files_affected_by_patch(repo_path)
builders_to_trigger, files_to_ignore = _get_builders_to_check(api)
affected_files = _ignore_affected_files(api, repo_path, affected_files,
files_to_ignore)
if not affected_files:
result = api.step('all affected files ignored', [])
result.presentation.step_text = (
'all affected files have been ignored, there is nothing to test')
return
led_builders = _get_led_builders(api, builders_to_trigger)
recipes = set(
_get_recipe(led_builder) for led_builder in led_builders.values())
recipes_py_path = api.cipd.ensure_tool(
'infra/recipe_bundles/chromium.googlesource.com/infra/luci/recipes-py',
version='refs/heads/main',
executable_path='recipe_engine/recipes.py')
recipes_cfg_path = repo_path.joinpath('infra', 'config', 'recipes.cfg')
affected_recipes = _determine_affected_recipes(api, affected_files, recipes,
recipes_py_path,
recipes_cfg_path)
api.swarming.ensure_client()
cls_by_filepath = {}
futures = []
for builder in builders_to_trigger:
# Edit the recipe bundle in the main greenlet so that it is serialized to
# avoid the concurrency issues of edit-recipe-bundle.
#
# TODO(crbug.com/1088020) If edit-recipe-bundle is made concurrency safe, it
# can be done in the _test_builder call. If a solution is added that
# performs the bundle once and has the same bootstrap support as
# edit-recipe-bundle, switch to that.
with api.step.nest('edit recipe bundle for {}'.format(builder.name)):
with api.context(cwd=repo_path):
led_builder = led_builders[builder.name]
led_builder = led_builder.then('edit-recipe-bundle')
file_path = _get_filepath_to_change(affected_files, affected_recipes,
builder, _get_recipe(led_builder),
recipes_cfg_path)
if not file_path:
with api.step.nest('test {}'.format(builder.name)) as presentation:
presentation.step_text = (
'\nNot running a tryjob for {!r}. The CL does not affect the '
'{!r} recipe and the CL does not affect recipes.cfg'.format(
builder.name, _get_recipe(led_builder)))
continue
cl = cls_by_filepath.get(file_path)
if not cl:
_, cl = api.chromium_gerrit_utils.create_temp_cl(
file_path,
GERRIT_TOPIC,
[f'Created for {api.tryserver.gerrit_change_review_url}'],
)
cls_by_filepath[file_path] = cl
futures.append(
api.futures.spawn_immediate(_test_builder, api, builder, led_builder,
cl))
# While we wait for the led builds to finish, let's clean-up any stale
# CLs uploaded from prev runs of this builder that we were unable to close
# in their original builds (due to a build crash, for example).
api.chromium_gerrit_utils.abandon_old_cls(GERRIT_TOPIC, '24h')
# Defer the resultant StepFailures from the led jobs until we've had a chance
# to abandon the Gerrit CLs.
with api.defer.context() as defer:
for f in api.futures.wait(futures):
defer(f.result)
for cl in cls_by_filepath.values():
change_num = cl.split('/')[-1]
api.chromium_gerrit_utils.abandon_cl(change_num)
def GenTests(api: TEST_DEPS):
RECIPE = 'foo_recipe'
def gerrit_change(footer_builder=None, experiments=()):
patch_set = 12
t = api.buildbucket.try_build(
git_repo='https://chromium.googlesource.com/foo/bar/baz',
change_number=456789,
patch_set=patch_set,
experiments=experiments,
)
message = 'nothing important'
parse_description_json = {}
if footer_builder:
message = '{}: {}'.format(BUILDER_FOOTER, footer_builder)
parse_description_json = {BUILDER_FOOTER: [footer_builder]}
t += api.override_step_data(
'gerrit changes',
api.json.output([{
'revisions': {
1: {
'_number': patch_set,
'commit': {
'message': message,
}
}
}
}]))
t += api.override_step_data('parse description',
api.json.output(parse_description_json))
return t
def affected_files(*affected_files):
return api.override_step_data(
'git diff to analyze patch',
stdout=api.raw_io.output('\n'.join(affected_files)))
def affected_recipes(*affected_recipes):
return api.step_data('determine affected recipes',
api.json.output({
'recipes': affected_recipes,
}))
def led_get_builder_name(name):
return 'get led builders.get {}.led get-builder'.format(name)
def parse_legacy_buildername(name):
# TODO: stop using buildbucket v1 buildernames.
bucket, buildername = name.split(':', 1)
assert bucket.startswith('luci.')
project, bucket = bucket[len('luci.'):].split('.', 1)
return project, bucket, buildername
def non_existent_builder(name):
return api.led.mock_get_builder(None, *parse_legacy_buildername(name))
def led_job(recipe):
job = job_pb2.Definition()
build = job.buildbucket.bbagent_args.build
build.input.properties['recipe'] = recipe
build.infra.backend.config.CopyFrom(
api.buildbucket.dict_to_struct({
'priority': 40,
},))
return job
def default_builders():
return api.led.mock_get_builder(led_job(RECIPE))
def affected_recipes_input_files(steps):
json_input = steps['determine affected recipes'].cmd[-2]
return api.json.loads(json_input)['files']
def affected_recipes_input_files_does_not_contain(check, steps, *rel_paths):
input_files = affected_recipes_input_files(steps)
for rel_path in rel_paths:
path = str(
api.path.cache_dir.joinpath('builder', 'baz', *rel_path.split('/')))
check(path not in input_files)
def affected_recipes_input_files_contains(check, steps, *rel_paths):
input_files = affected_recipes_input_files(steps)
for rel_path in rel_paths:
path = str(
api.path.cache_dir.joinpath('builder', 'baz', *rel_path.split('/')))
check(path in input_files)
yield api.test(
'basic',
gerrit_change(),
affected_recipes(RECIPE),
default_builders(),
)
def builder_config_path(p):
return 'recipes/recipe_modules/chromium_tests_builder_config/{}'.format(p)
yield api.test(
'per_builder_config_ignored',
gerrit_change(),
affected_recipes(RECIPE),
affected_files(
'recipes/foo.py',
builder_config_path('builders/__init__.py'),
builder_config_path('builders/chromium.py'),
builder_config_path('trybots.py'),
),
default_builders(),
api.post_check(post_process.MustRun, 'ignoring per-builder config'),
api.post_check(post_process.StepTextContains,
'ignoring per-builder config', [r'\_\_init\_\_.py']),
api.post_check(affected_recipes_input_files_does_not_contain,
builder_config_path('builders/__init__.py'),
builder_config_path('builders/chromium.py'),
builder_config_path('trybots.py')),
api.post_process(post_process.DropExpectation),
)
yield api.test(
'recipe_test_ignored',
gerrit_change(),
affected_recipes(RECIPE),
affected_files(
'recipes/foo.py',
'recipes/recipe_modules/chromium_swarming/examples/full.py',
builder_config_path('tests/builders.py'),
),
default_builders(),
api.post_check(post_process.MustRun, 'ignoring recipe tests'),
api.post_check(
affected_recipes_input_files_does_not_contain,
'recipes/recipe_modules/chromium_swarming/examples/full.py',
builder_config_path('tests/builders.py')),
api.post_process(post_process.DropExpectation),
)
yield api.test(
'owners_files_ignored',
gerrit_change(),
affected_recipes(RECIPE),
affected_files(
'recipes/foo.py',
builder_config_path('OWNERS'),
'recipes/recipe_modules/chromium_tests/CHROMIUM_TESTS_OWNERS',
),
default_builders(),
api.post_check(post_process.MustRun, 'ignoring OWNERS files'),
api.post_check(
affected_recipes_input_files_does_not_contain,
builder_config_path('OWNERS'),
'recipes/recipe_modules/chromium_tests/CHROMIUM_TESTS_OWNERS'),
api.post_process(post_process.DropExpectation),
)
yield api.test(
'src_side_migration_files_ignored',
gerrit_change(),
affected_recipes(RECIPE),
affected_files(
'recipes/foo.py',
builder_config_path('migration/chromium.json'),
),
default_builders(),
api.post_check(post_process.MustRun,
'ignoring src-side config migration files'),
api.post_check(affected_recipes_input_files_does_not_contain,
builder_config_path('migration/chromium.json')),
api.post_process(post_process.DropExpectation),
)
yield api.test(
'presubmit_scripts_ignored',
gerrit_change(),
affected_recipes(RECIPE),
affected_files(
'recipes/foo.py',
builder_config_path('PRESUBMIT.py'),
),
default_builders(),
api.post_check(post_process.MustRun, 'ignoring PRESUBMIT.py scripts'),
api.post_check(affected_recipes_input_files_does_not_contain,
builder_config_path('PRESUBMIT.py')),
api.post_process(post_process.DropExpectation),
)
yield api.test(
'all_affected_files_are_ignored',
gerrit_change(),
affected_files(builder_config_path('builders/__init__.py')),
api.post_check(post_process.MustRun, 'all affected files ignored'),
api.post_check(post_process.DoesNotRun, 'determine affected recipes'),
api.post_process(post_process.DropExpectation),
)
yield api.test(
'no_jobs_to_run',
gerrit_change(),
default_builders(),
api.post_check(post_process.DoesNotRunRE, 'test .*\.trigger'),
api.post_process(post_process.DropExpectation),
)
yield api.test(
'recipe_roller',
gerrit_change(),
affected_files(
'random/file.py',
'infra/config/recipes.cfg',
),
default_builders(),
)
yield api.test(
'manual_roll_with_changes',
gerrit_change(),
affected_files(
'random/file.py',
'infra/config/recipes.cfg',
),
default_builders(),
)
yield api.test(
'analyze_missing_json',
gerrit_change(),
default_builders(),
api.override_step_data('determine affected recipes', retcode=1),
api.post_check(post_process.StepException, 'determine affected recipes'),
api.expect_status('INFRA_FAILURE'),
api.post_process(post_process.DropExpectation),
)
yield api.test(
'analyze_failure',
gerrit_change(),
default_builders(),
api.step_data(
'determine affected recipes',
api.json.output({
'error': 'Bad analyze!!!!',
'invalid_recipes': [RECIPE],
}),
retcode=1),
api.post_check(post_process.StepFailure, 'determine affected recipes'),
api.post_check(
lambda check, steps: \
check(RECIPE in
steps['determine affected recipes'].logs['invalid recipes'])
),
api.expect_status('FAILURE'),
api.post_process(post_process.DropExpectation),
)
def step_text_lines(step):
return [l.strip() for l in step.step_text.split('<br/>')]
yield api.test(
'footer_builder_with_invalid_format',
gerrit_change(footer_builder='bad-builder'),
api.post_check(post_process.StepFailure, 'bad builders'),
api.post_check(
lambda check, steps: \
check('bad-builder' in step_text_lines(steps['bad builders']))
),
api.expect_status('FAILURE'),
api.post_process(post_process.DropExpectation),
)
yield api.test(
'footer_builder_with_unknown_bucket',
gerrit_change(footer_builder='arbitrary-bucket:arbitrary-builder'),
api.post_check(post_process.StepFailure, 'unknown buckets'),
api.post_check(
lambda check, steps: \
check('arbitrary-bucket' in step_text_lines(steps['unknown buckets']))
),
api.expect_status('FAILURE'),
api.post_process(post_process.DropExpectation),
)
yield api.test(
'footer_builder',
gerrit_change(footer_builder='luci.chromium.try:arbitrary-builder'),
affected_recipes(RECIPE),
default_builders(),
api.post_check(post_process.DoesNotRun,
*[led_get_builder_name(b) for b in DEFAULT_BUILDERS]),
api.post_process(post_process.DropExpectation),
)
yield api.test(
'per_builder_config_not_ignored_for_footer_builders',
gerrit_change(footer_builder='luci.chromium.try:arbitrary-builder'),
affected_recipes(RECIPE),
affected_files(
builder_config_path('builders/chromium.py'),
builder_config_path('trybots.py')),
default_builders(),
api.post_check(post_process.DoesNotRun, 'ignoring builder config'),
api.post_check(affected_recipes_input_files_contains,
builder_config_path('builders/chromium.py'),
builder_config_path('trybots.py')),
api.post_process(post_process.DropExpectation),
)
yield api.test(
'footer_builder_does_not_exist',
gerrit_change(footer_builder='luci.chromium.try:arbitrary-builder'),
non_existent_builder('luci.chromium.try:arbitrary-builder'),
api.post_check(
post_process.StepFailure,
led_get_builder_name('luci.chromium.try:arbitrary-builder')),
api.expect_status('FAILURE'),
api.post_process(post_process.DropExpectation),
)