blob: 1a03d959ffc5d945672e3bfafb617b5dd935775e [file] [log] [blame]
# Copyright 2019 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.
"""Recipe to verify flaky tests (aka progression testing).
The recipe reads flake config and for each entry triggers flako recipe in
reproduce_only mode. If any flake fails to reproduce, the build is marked as
failed, which can then be used to alert sheriffs via a gatekeeper rule.
"""
import ast
import re
from PB.go.chromium.org.luci.buildbucket.proto import (builds_service as
builds_service_pb2)
from PB.go.chromium.org.luci.buildbucket.proto import common as common_pb2
from recipe_engine.post_process import (
DropExpectation, Filter, MustRun, ResultReasonRE, StatusException,
StatusFailure, StatusSuccess, StepException, StepFailure)
DEPS = [
'depot_tools/gitiles',
'recipe_engine/buildbucket',
'recipe_engine/json',
'recipe_engine/step',
'v8',
]
MAX_CONFIGS = 16
URL_FORMAT_RE = re.compile(r'(?:https?://)?(.+)')
TEST_CONFIG = [{
'bisect_buildername': 'V8 Linux64 - debug builder',
'bisect_builder_group': 'client.v8',
# TODO(https://crbug.com/1109276) Don't set bisect_mastername
'bisect_mastername': 'client.v8',
'bug_url': 'https://crbug.com/v8/8744',
'build_config': 'Debug',
'extra_args': [],
'isolated_name': 'bot_default',
'num_shards': 2,
'repetitions': 5000,
'swarming_dimensions': [
'cpu:x86-64-avx2', 'gpu:none', 'os:Ubuntu-16.04', 'pool:Chrome'
],
'test_name': 'cctest/test-cpu-profiler/FunctionCallSample',
'timeout_sec': 60,
'total_timeout_sec': 120,
'variant': 'interpreted_regexp'
}]
def update_step_presentation(api, presentation, build, flake_config):
presentation.links['build %s' % build.id] = (
api.buildbucket.build_url(build_id=build.id))
bug_url = flake_config.get('bug_url')
if bug_url:
presentation.links[URL_FORMAT_RE.match(bug_url).group(1)] = bug_url
presentation.logs['flake config'] = api.json.dumps(
flake_config, indent=2).splitlines()
if build.status == common_pb2.FAILURE:
presentation.step_text = (
'failed to reproduce<br/>please consider re-enabling this test')
presentation.status = api.step.FAILURE
elif build.status == common_pb2.SUCCESS:
presentation.step_text = 'reproduced'
else:
presentation.step_text = 'failed to execute'
presentation.status = api.step.EXCEPTION
def RunSteps(api):
configs = ast.literal_eval(api.gitiles.download_file(
'https://chromium.googlesource.com/v8/v8', 'flakes/flakes.pyl',
branch='infra/config', step_name='read flake config'))
if not configs:
api.step('No flakes to reproduce', cmd=None)
return
if len(configs) > MAX_CONFIGS:
too_many_flakes = api.step('Too many flake configs', cmd=None)
too_many_flakes.presentation.status = api.step.FAILURE
too_many_flakes.presentation.step_text = (
'Running on first %d configs only' % MAX_CONFIGS
)
configs = configs[:MAX_CONFIGS]
v8_commits, _ = api.gitiles.log(
'https://chromium.googlesource.com/v8/v8/', 'refs/heads/master', limit=1,
step_name='read V8 ToT revision')
v8_tot = v8_commits[0]['commit']
# TODO(sergiyb): Stop setting got_revision property when build manifests
# are supported in Milo, in which case using set_output_gitiles_commit should
# be sufficient.
api.step.active_result.presentation.properties['got_revision'] = v8_tot
api.buildbucket.set_output_gitiles_commit(common_pb2.GitilesCommit(
host='chromium.googlesource.com', project='v8/v8',
ref='refs/heads/master', id=v8_tot))
builds = api.v8.buildbucket_trigger([
(
'v8_flako',
dict(
flako_properties,
repro_only=True,
swarming_priority=40,
to_revision=v8_tot,
max_calibration_attempts=1,
swarming_expiration=7200, # 2 hours
)
) for flako_properties in configs
], step_name='trigger flako builds')
# Collect builds.
build_results = []
with api.step.nest('collect builds'):
for index, build in enumerate(builds):
label = api.v8.ui_test_label(configs[index]['test_name'])
build_results.append(api.buildbucket.collect_build(
int(build.id), step_name=label, mirror_status=True, timeout=4*3600))
# Emit summary steps for each build.
non_flaky_tests = []
for index, build in enumerate(build_results):
label = api.v8.ui_test_label(configs[index]['test_name'])
step_result = api.step(label, cmd=None)
update_step_presentation(
api, step_result.presentation, build, configs[index])
if build.status == common_pb2.FAILURE:
non_flaky_tests.append(label)
if non_flaky_tests:
raise api.step.StepFailure(
'Some flakes failed to reproduce: %s' % ', '.join(non_flaky_tests))
else:
api.step('No flakes that fail to reproduce', cmd=None)
def GenTests(api):
def test(name, results, ui_test_name=None):
return api.test(
name,
api.step_data(
'read flake config',
api.gitiles.make_encoded_file(api.json.dumps(TEST_CONFIG))),
api.step_data('read V8 ToT revision',
api.gitiles.make_log_test_data('deadbeef')),
api.buildbucket.generic_build(
project='v8',
bucket='try.triggered',
builder='v8_verify_flakes'),
api.buildbucket.simulated_schedule_output(
builds_service_pb2.BatchResponse(
responses=[dict(schedule_build=dict(id=123))],),
step_name='trigger flako builds'),
api.buildbucket.simulated_collect_output(
[
api.buildbucket.ci_build_message(build_id=123, status=result)
for result in results
],
step_name='collect builds.' + (ui_test_name or
'FunctionCallSample')),
)
yield (
test('success', ['SUCCESS']) +
api.post_process(StatusSuccess)
)
yield (
test('failure', ['FAILURE']) +
api.post_process(StepFailure, 'FunctionCallSample') +
api.post_process(StatusFailure) +
api.post_process(
ResultReasonRE, 'Some flakes failed to reproduce: FunctionCallSample') +
api.post_process(DropExpectation)
)
yield (
test('infra_failure', ['INFRA_FAILURE']) +
api.post_process(StepException, 'FunctionCallSample') +
api.post_process(StatusSuccess) +
api.post_process(Filter().include_re(r'.*FunctionCallSample.*'))
)
yield api.test(
'no_flakes',
api.step_data('read flake config', api.gitiles.make_encoded_file('[]')),
api.post_process(MustRun, 'No flakes to reproduce'),
api.post_process(StatusSuccess),
api.post_process(DropExpectation),
)
yield (
test('too_many_flakes', ['SUCCESS'] * 20, ui_test_name='baz') +
api.override_step_data(
'read flake config',
api.gitiles.make_encoded_file(api.json.dumps(
[{'test_name': 'foo/bar/baz'}] * 20))) +
api.post_process(MustRun, 'Too many flake configs') +
api.post_process(DropExpectation)
)