blob: 31a8681ea38218627061533e25b4fa3745c2dda4 [file]
# Copyright 2023 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Checks test configs specified in starlark versus testing/buildbot.
Once migration of all specs to starlark is complete, this recipe will be
irrelevant.
"""
from collections.abc import Iterable
from google.protobuf import json_format
from recipe_engine import post_process
from recipe_engine import recipe_test_api
from RECIPE_MODULES.build import chromium_tests_builder_config as ctbc
from RECIPE_MODULES.build import proto_validation
from PB.go.chromium.org.luci.buildbucket.proto import common as common_pb
from PB.recipe_engine import result as result_pb
from PB.recipes.build.chromium import (
targets_config_verifier as targets_config_verifier_pb,
)
PROPERTIES = targets_config_verifier_pb.InputProperties
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_tests,
chromium_tests_builder_config,
chromium_tests_targets_config_verifier,
)
from RECIPE_MODULES.depot_tools import gclient, tryserver
from RECIPE_MODULES.recipe_engine import (
buildbucket,
file,
path,
properties,
)
@dataclass
class DEPS(RecipeScriptApi):
buildbucket: buildbucket.API
chromium_tests: chromium_tests.API
chromium_tests_builder_config: chromium_tests_builder_config.API
chromium_tests_targets_config_verifier: (
chromium_tests_targets_config_verifier.API
)
file: file.API
gclient: gclient.API
path: path.API
properties: properties.API
tryserver: tryserver.API
@dataclass
class TEST_DEPS(RecipeTestApi):
buildbucket: buildbucket.TEST_API
chromium_tests: chromium_tests.TEST_API
chromium_tests_builder_config: chromium_tests_builder_config.TEST_API
file: file.TEST_API
path: path.TEST_API
properties: properties.TEST_API
tryserver: tryserver.TEST_API
def RunSteps(api: DEPS, properties: targets_config_verifier_pb.InputProperties):
api.tryserver.require_is_tryserver()
errors = VALIDATORS.validate(properties)
if errors:
return _result(
status=common_pb.INFRA_FAILURE,
elements=errors,
header='The following errors were found with the input properties:',
)
gclient_config = api.gclient.make_config('chromium')
return api.chromium_tests_targets_config_verifier.verify_target_configs(
gclient_config,
properties.builder_config_directory,
properties.precommit_buckets,
)
VALIDATORS = proto_validation.Registry()
@VALIDATORS.register(targets_config_verifier_pb.InputProperties)
def _validate_properties(message, ctx):
ctx.validate_field(message, 'builder_config_directory')
def _result(
status: common_pb.Status,
header: str,
elements: Iterable[str],
) -> result_pb.RawResult:
summary = [header, '']
summary.extend('* {}'.format(e) for e in elements)
return result_pb.RawResult(status=status, summary_markdown='\n'.join(summary))
def GenTests(api: TEST_DEPS):
builder_config_dir = 'builder-config-dir'
ctbc_api = api.chromium_tests_builder_config
def test_data(
*,
bucket: str,
builder: str,
builder_group: str,
try_bucket: str | None = None,
try_builder: str | None = None,
starlark_targets_spec: dict | None = None,
testing_buildbot_targets_spec: dict | None = None,
) -> recipe_test_api.StepTestData:
"""Set necessary step test data for calling verify_builder_configs.
Args:
bucket: The bucket of the builder to verify targets spec for or
the bucket of the mirrored builder if try_bucket and try_builder
are set.
builder: The builder to verify targets spec for or the mirrored
builder if try_bucket and try_builder are set.
builder_group: The group of the builder to verify targets spec for
or the group of the mirrored builder if try_bucket and
try_builder are set.
try_bucket: The bucket of the try builder to verify targets spec
for. Must be set iff try_builder is set.
try_builder: The try builder to verify targets spec for. Must be
set iff try_builder is set.
starlark_targets_spec: The targets spec generated from starlark.
testing_buildbot_targets_spec: The targets spec generated from
//testing/buildbot.
"""
t = api.buildbucket.try_build()
t += api.properties(
targets_config_verifier_pb.InputProperties(
builder_config_directory=builder_config_dir,
precommit_buckets=[try_bucket] if try_bucket is not None else [],
)
)
def assert_set_together(name1, val1, name2, val2):
assert (val1 is None) == (val2 is None), (
f'{name1} and {name2} must both be set or both be unset'
)
assert_set_together('try_bucket', try_bucket, 'try_builder', try_builder)
builder_dir = (
f'{builder_config_dir}/{try_bucket or bucket}/{try_builder or builder}'
)
t += api.tryserver.get_files_affected_by_patch(
[f'{builder_dir}/targets/{builder_group}.json'],
step_name=(
'determine affected targets spec files.git diff to analyze patch'
),
)
get_targets_config_step = f'get patched targets configs.{builder_dir}'
verify_step = f'verify {builder_dir}'
existing_paths = [
api.path.cache_dir / f'builder/src/{builder_dir}/properties.json'
]
builder_spec = ctbc.BuilderSpec.create(
gclient_config='chromium',
chromium_config='chromium',
chromium_apply_config=['mb'],
)
if try_bucket:
ctbc_prop = (
ctbc_api.properties_assembler_for_try_builder().with_mirrored_builder(
bucket=bucket,
builder=builder,
builder_group=builder_group,
builder_spec=builder_spec,
)
)
else:
ctbc_prop = ctbc_api.properties_assembler_for_ci_builder(
bucket=bucket,
builder=builder,
builder_group=builder_group,
builder_spec=builder_spec,
)
t += api.step_data(
f'{verify_step}.read properties file',
api.file.read_json(
{
'$build/chromium_tests_builder_config': json_format.MessageToDict(
ctbc_prop.assemble()
),
}
),
)
ctbc_prop = ctbc_prop.with_targets_spec_directory(f'{builder_dir}/targets')
t += api.step_data(
f'{get_targets_config_step}.read properties file',
api.file.read_json(
{
'$build/chromium_tests_builder_config': json_format.MessageToDict(
ctbc_prop.assemble()
),
}
),
)
def read_targets_spec(targets_spec, step_prefix):
return api.chromium_tests.read_targets_spec(
builder_group, {builder: targets_spec}, step_prefix=step_prefix
)
if starlark_targets_spec is not None:
t += read_targets_spec(
starlark_targets_spec,
step_prefix=f'{get_targets_config_step}.get starlark targets config.',
)
if testing_buildbot_targets_spec is not None:
existing_paths.append(
api.path.cache_dir
/ f'builder/src/testing/buildbot/{builder_group}.json'
)
t += read_targets_spec(
testing_buildbot_targets_spec,
step_prefix=f'{verify_step}.get pyl targets config.',
)
t += api.path.exists(*existing_paths)
return t
yield api.test(
'basic',
test_data(
bucket='fake-bucket',
builder='fake-builder',
builder_group='fake-group',
starlark_targets_spec={
'additional_compile_targets': ['foo'],
'gtest_tests': [
{
'test': 'foo-test',
'swarming': {},
'merge': {
'script': '//merge-script',
},
}
],
'scripts': [
{
'name': 'bar',
'script': 'bar.py',
}
],
},
testing_buildbot_targets_spec={
'additional_compile_targets': ['foo'],
'gtest_tests': [
{
'test': 'foo-test',
'swarming': {},
'merge': {
'script': '//merge-script',
},
}
],
'scripts': [
{
'name': 'bar',
'script': 'bar.py',
}
],
},
),
api.post_check(
post_process.StepTextContains,
'verify builder-config-dir/fake-bucket/fake-builder',
['starlark config matches pyl config'],
),
api.post_process(post_process.DropExpectation),
)
yield api.test(
'mismatch-compile-targets',
test_data(
bucket='fake-bucket',
builder='fake-builder',
builder_group='fake-group',
starlark_targets_spec={
'additional_compile_targets': ['foo'],
},
testing_buildbot_targets_spec={
'additional_compile_targets': ['bar'],
},
),
api.expect_status('FAILURE'),
# Keep just the verify step so that we can see when the diff changes
api.post_process(
post_process.Filter('verify builder-config-dir/fake-bucket/fake-builder')
),
)
yield api.test(
'mismatch-tests',
test_data(
bucket='fake-bucket',
builder='fake-builder',
builder_group='fake-group',
starlark_targets_spec={
'gtest_tests': [
{
'test': 'foo-test',
}
],
},
testing_buildbot_targets_spec={
'gtest_tests': [
{
'test': 'bar-test',
}
],
},
),
api.expect_status('FAILURE'),
# Keep just the verify step so that we can see when the diff changes
api.post_process(
post_process.Filter('verify builder-config-dir/fake-bucket/fake-builder')
),
)
yield api.test(
'mismatch-tests-non-precommit',
test_data(
bucket='fake-bucket',
builder='fake-builder',
builder_group='fake-group',
starlark_targets_spec={
'gtest_tests': [
{
'test': 'foo-test',
}
],
},
testing_buildbot_targets_spec={
'gtest_tests': [
{
'test': 'foo-test',
'non_precommit_args': ['foo'],
}
],
},
),
api.expect_status('FAILURE'),
# Keep just the verify step so that we can see when the diff changes
api.post_process(
post_process.Filter('verify builder-config-dir/fake-bucket/fake-builder')
),
)
yield api.test(
'mismatch-tests-precommit',
test_data(
bucket='fake-bucket',
builder='fake-builder',
builder_group='fake-group',
try_bucket='fake-try-bucket',
try_builder='fake-try-builder',
starlark_targets_spec={
'gtest_tests': [
{
'test': 'foo-test',
}
],
},
testing_buildbot_targets_spec={
'gtest_tests': [
{
'test': 'foo-test',
'precommit_args': ['foo'],
}
],
},
),
api.expect_status('FAILURE'),
# Keep just the verify step so that we can see when the diff changes
api.post_process(
post_process.Filter(
'verify builder-config-dir/fake-try-bucket/fake-try-builder'
)
),
)
# Must appear last since it drops expectations
def invalid_properties(*errors):
test_data = api.expect_status('INFRA_FAILURE')
test_data += api.post_check(
post_process.SummaryMarkdownRE,
'^The following errors were found with the input properties',
)
for error in errors:
test_data += api.post_check(post_process.SummaryMarkdownRE, error)
test_data += api.post_process(post_process.DropExpectation)
return test_data
yield api.test(
'builder-config-dir-not-set',
api.buildbucket.try_build(),
invalid_properties('builder_config_directory is not set'),
)