blob: 1f9db37f39147f4d4260a6eff5e84277f9b5671e [file]
# Copyright 2018 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.
import re
from recipe_engine import post_process
from recipe_engine.config_types import Path
from PB.go.chromium.org.luci.buildbucket.proto import common as common_pb
from RECIPE_MODULES.build import chromium_tests_builder_config as ctbc
from dataclasses import dataclass
from recipe_engine.recipe_api import RecipeScriptApi
from recipe_engine.recipe_test_api import RecipeTestApi
from RECIPE_MODULES.build import (
builder_group,
chromium,
chromium_checkout,
chromium_tests,
)
from RECIPE_MODULES.depot_tools import bot_update, gclient, gsutil
from RECIPE_MODULES.infra import zip as zip_module
from RECIPE_MODULES.recipe_engine import (
buildbucket,
cipd,
context,
file,
path,
platform,
properties,
resultdb,
step,
time,
)
@dataclass
class DEPS(RecipeScriptApi):
bot_update: bot_update.API
buildbucket: buildbucket.API
builder_group: builder_group.API
chromium: chromium.API
chromium_checkout: chromium_checkout.API
chromium_tests: chromium_tests.API
cipd: cipd.API
context: context.API
file: file.API
gclient: gclient.API
gsutil: gsutil.API
path: path.API
platform: platform.API
properties: properties.API
resultdb: resultdb.API
step: step.API
time: time.API
zip: zip_module.API
@dataclass
class TEST_DEPS(RecipeTestApi):
bot_update: bot_update.TEST_API
buildbucket: buildbucket.TEST_API
builder_group: builder_group.TEST_API
chromium: chromium.TEST_API
chromium_checkout: chromium_checkout.TEST_API
chromium_tests: chromium_tests.TEST_API
cipd: cipd.TEST_API
context: context.TEST_API
file: file.TEST_API
gclient: gclient.TEST_API
path: path.TEST_API
platform: platform.TEST_API
properties: properties.TEST_API
resultdb: resultdb.TEST_API
step: step.TEST_API
time: time.TEST_API
CELAB_REPO = 'https://chromium.googlesource.com/enterprise/cel'
CHROMIUM_REPO = 'https://chromium.googlesource.com/chromium/src'
def _get_bin_directory(api: DEPS, bin_root):
bin_dir = bin_root
if api.platform.is_linux:
bin_dir = bin_dir.joinpath('linux_amd64', 'bin')
elif api.platform.is_win:
bin_dir = bin_dir.joinpath('windows_amd64', 'bin')
return bin_dir
def _get_ctl_binary_name(api: DEPS):
suffix = '.exe' if api.platform.is_win else ''
return 'cel_ctl' + suffix
def _get_python_packages(api: DEPS, checkout):
"""Returns the full path of the python whl package files."""
out_dir = checkout / 'out'
return api.file.glob_paths(
'find python packages', out_dir, '*.whl', test_data=['test.whl']
)
def RunSteps(api: DEPS):
project = api.buildbucket.build.builder.project
if project == 'celab':
_RunStepsCelab(api)
elif project in ('chromium', 'chrome'):
compile_failure = _RunStepsChromium(api)
if compile_failure:
return compile_failure
else:
raise ValueError(
'Invalid `project`. Accepted values: celab, chromium, chrome.'
)
def _RunStepsCelab(api: DEPS):
checkout = _CheckoutCelabRepo(api)
# Build CELab binaries from source.
bin_dir = _BuildCelabFromSource(api, checkout)
# Upload binaries (cel_ctl and resources/*, plus the python package of the
# test framework) for CI builds
bucket = api.buildbucket.build.builder.bucket
if bucket == 'ci':
_UploadCelabBinariesToStorage(api, checkout, bin_dir)
# Run tests for CI/Try builders that specify it.
tests = api.properties.get('tests')
if tests:
_RunTests(
api,
checkout.joinpath('test'),
checkout / 'scripts' / 'tests',
'../../examples/schema/host/example.host.textpb',
tests,
)
def _GetCelabVersionFromVPython(api: DEPS, path):
output = api.file.read_text('read vpython file', path)
anything_except_closing = '[^>]*'
pattern = '<'
pattern += anything_except_closing
pattern += 'name:\s*"infra/celab/celab/windows-amd64"'
pattern += anything_except_closing
pattern += 'version:\s*"(.*)"'
pattern += anything_except_closing
pattern += '>'
match = re.search(pattern, output)
if match:
return match.groups(1)[0]
raise ValueError('Couldn\'t find CELab version in vpython file: %s' % path)
def _RunStepsChromium(api: DEPS):
tests = api.properties.get('tests')
if not tests:
raise ValueError('Chromium bots must define `tests`.')
# Build Chromium binaries from source and get CELab from CIPD.
source_dir, build_dir = _CheckoutChromiumRepo(api)
test_root = source_dir / 'chrome/test/enterprise/e2e'
raw_result = _BuildChromiumFromSource(api, source_dir, build_dir)
if raw_result.status != common_pb.SUCCESS:
return raw_result
version = _GetCelabVersionFromVPython(api, test_root / '.vpython3')
celab_bin_dir = _GetCelabFromCipd(api, version)
# Run tests for all chromium bots.
cel_ctl = celab_bin_dir.joinpath(_get_ctl_binary_name(api))
omaha_updater = build_dir / 'updater.exe'
omaha_installer = build_dir / 'UpdaterSetup.exe'
installer = build_dir / 'mini_installer.exe'
chromedriver = build_dir / 'chromedriver.exe'
test_py_args = '--cel_ctl=%s' % cel_ctl
test_py_args += ' --test_arg=--omaha_updater=%s' % omaha_updater
test_py_args += ' --test_arg=--omaha_installer=%s' % omaha_installer
test_py_args += ' --test_arg=--chrome_installer=%s' % installer
test_py_args += ' --test_arg=--chromedriver=%s' % chromedriver
_RunTests(
api,
test_root,
test_root / 'infra',
'template.host.textpb',
tests,
test_py_args,
)
def _GetCelabFromCipd(api: DEPS, version):
packages_root = api.path.start_dir / 'packages'
ensure_file = api.cipd.EnsureFile().add_package(
'infra/celab/celab/${platform}', version
)
api.cipd.ensure(packages_root, ensure_file)
return _get_bin_directory(api, packages_root)
def _CheckoutCelabRepo(api: DEPS):
# Checkout the CELab repo
go_root = api.path.start_dir / 'go'
src_root = go_root.joinpath('src', 'chromium.googlesource.com', 'enterprise')
api.file.ensure_directory('init src_root if not exists', src_root)
with api.context(cwd=src_root):
api.gclient.set_config('celab')
update_result = api.bot_update.ensure_checkout()
api.gclient.runhooks()
return update_result.source_root.path
def _BuildCelabFromSource(api: DEPS, checkout):
go_root = api.path.start_dir / 'go'
# Install Go & Protoc
packages_root = api.path.start_dir / 'packages'
ensure_file = api.cipd.EnsureFile()
ensure_file.add_package('infra/3pp/tools/go/${platform}', 'version:3@1.24.8')
ensure_file.add_package(
'infra/tools/protoc/${platform}', 'protobuf_version:v3.17.0'
)
ensure_file.add_package('infra/third_party/cacert', 'date:2017-01-18')
api.cipd.ensure(packages_root, ensure_file)
add_paths = [
go_root / 'bin',
packages_root,
packages_root / 'bin',
]
# Build CELab
cert_file = packages_root / 'cacert.pem'
goenv = {'GOPATH': go_root, 'GIT_SSL_CAINFO': cert_file}
with api.context(cwd=checkout, env=goenv, env_suffixes={'PATH': add_paths}):
api.step(
'install deps', ['python3', 'build.py', 'deps', '--install', '--verbose']
)
api.step('build', ['python3', 'build.py', 'build', '--verbose'])
api.step(
'create python package',
['python3', 'build.py', 'create_package', '--verbose'],
)
return _get_bin_directory(api, checkout / 'out')
def _CheckoutChromiumRepo(api: DEPS):
project = api.buildbucket.build.builder.project
with api.chromium.chromium_layout():
builder_config = {
'chromium_config': 'chromium',
'gclient_config': 'chromium',
'chromium_apply_config': ['mb'],
'chromium_config_kwargs': {
'BUILD_CONFIG': 'Release',
'TARGET_BITS': 64,
},
}
if project == 'chrome':
builder_config['gclient_apply_config'] = [
'chrome_internal',
'checkout_pgo_profiles',
]
builder_config = ctbc.BuilderSpec.create(**builder_config)
api.chromium_tests.configure_build(builder_config)
update_result = api.chromium_checkout.ensure_checkout(
clobber=builder_config.clobber
)
source_dir = update_result.source_root.path
build_dir = api.chromium.default_build_dir(source_dir)
api.chromium.runhooks(source_dir, build_dir)
return source_dir, build_dir
def _BuildChromiumFromSource(api: DEPS, source_dir: Path, build_dir: Path):
with api.chromium.chromium_layout():
compile_targets = [
'chrome/updater',
'chrome/installer/mini_installer',
'chromedriver',
]
raw_result = api.chromium_tests.run_mb_and_compile(
source_dir,
build_dir,
api.chromium.get_builder_id(),
compile_targets,
isolated_targets=[],
name_suffix=' (with patch)',
)
return raw_result
def _UploadCelabBinariesToStorage(api: DEPS, checkout, bin_dir):
cel_ctl = _get_ctl_binary_name(api)
zip_out = api.path.start_dir / 'cel.zip'
pkg = api.zip.make_package(checkout / 'out', zip_out)
pkg.add_file(bin_dir / cel_ctl)
pkg.add_directory(bin_dir / 'resources')
for package_file in _get_python_packages(api, checkout):
pkg.add_file(package_file)
pkg.zip('zip archive')
today = api.time.utcnow().date()
gs_dest = '%s/%s/%s/cel.zip' % (
api.buildbucket.builder_name,
today.strftime('%Y/%m/%d'),
api.buildbucket.build.id,
)
api.gsutil.upload(
source=zip_out,
bucket='celab',
dest=gs_dest,
name='upload CELab binaries',
link_name='CELab binaries',
)
def _RunTests(
api: DEPS,
test_root,
test_scripts_root,
host_file_template,
tests,
test_py_args='',
):
pool_name = api.properties.get('pool_name')
pool_size = api.properties.get('pool_size')
if not pool_name or not pool_size:
raise ValueError('pool_name and pool_size must be defined with `tests`.')
host_dir = api.path.start_dir / 'hosts'
logs_dir = api.path.start_dir / 'logs'
with api.step.nest('setup tests'):
api.file.ensure_directory('init host_dir if not exists', host_dir)
api.file.ensure_directory('init logs_dir if not exists', logs_dir)
# Install required package for gsutil.
packages_root = api.path.start_dir / 'packages_tests'
ensure_file = api.cipd.EnsureFile().add_package(
'infra/gcloud/${platform}', 'version:251.0.0.chromium0'
)
api.cipd.ensure(packages_root, ensure_file)
add_paths = [packages_root / 'bin']
# Get a unique storage prefix for these tests (diff runs share the bucket)
storage_prefix = 'test-run-%s' % api.buildbucket.build.id
# Generate the host files that we'll use in ./run_tests.py.
with api.context(cwd=test_scripts_root):
api.step(
'generate host files',
[
'python3',
'generate_host_files.py',
'--template',
host_file_template,
'--projects',
';'.join(
['%s-%03d' % (pool_name, i) for i in range(1, pool_size + 1)]
),
'--storage_bucket',
'%s-assets' % pool_name,
'--storage_prefix',
storage_prefix,
'--destination_dir',
host_dir,
],
)
# Run our tests and catch test failures.
storage_logs = '%s-logs' % pool_name
with api.context(cwd=test_root, env_suffixes={'PATH': add_paths}):
extra_args = []
test_py_args += ' --no_external_access=True'
extra_args += ['--test_py_args=%s' % test_py_args.strip()]
include_tests = api.properties.get('include')
if include_tests:
extra_args += ['--include', include_tests]
exclude_tests = api.properties.get('exclude')
if exclude_tests:
extra_args += ['--exclude', exclude_tests]
try:
variant = {
'builder': api.buildbucket.builder_name,
}
api.step(
'run all tests',
api.resultdb.wrap(
[
'vpython3',
'-u',
'run_tests.py',
'--tests',
tests,
'--hosts',
host_dir,
'--test_py',
'test.py',
'--shared_provider_storage',
'%s-assets' % pool_name,
'--error_logs_dir',
logs_dir,
'--noprogress',
'-v',
'1',
]
+ extra_args,
base_variant=variant,
),
)
except:
# We upload *all* logs, including those we reupload in _ParseTestSummary.
# It's better to upload (small) logs twice than to not upload them at
# all. They are automatically deleted after 30 days (bucket policy).
_ZipAndUploadDirectory(
api, storage_logs, logs_dir, 'all_logs.zip', 'CELab Test Logs'
)
raise
finally:
# TODO: Clean up storage prefix when the test run ends.
# It's already automatically deleted after 1 day.
# Parse the test summary file and organize results in a readable way.
_ParseTestSummary(api, storage_logs, logs_dir)
# Zips the content of a directory and uploads the zip file to a given bucket.
def _ZipAndUploadDirectory(
api: DEPS, bucket, directory, zip_filename, display_name
):
zip_out = api.path.start_dir / zip_filename
pkg = api.zip.make_package(directory, zip_out)
pkg.add_directory(directory)
pkg.zip('zip logs archive')
today = api.time.utcnow().date()
gs_dest = '%s/%s/%s/%s' % (
api.buildbucket.builder_name,
today.strftime('%Y/%m/%d'),
api.buildbucket.build.id,
zip_filename,
)
return api.gsutil.upload(
source=zip_out,
bucket=bucket,
dest=gs_dest,
name='upload %s' % display_name,
link_name=display_name,
)
# Parses the summary.json file created by run_tests.py, organizes the steps
# presentation of tests and creates separate zips for each test logs.
def _ParseTestSummary(api: DEPS, storage_logs, logs_dir):
summary_path = logs_dir / 'summary.json'
with api.step.nest('test summary') as summary_step:
tests_summary = api.file.read_json('parse summary', summary_path)
if not tests_summary:
return None
for test in tests_summary:
try:
with api.step.nest(test) as test_step:
result = tests_summary[test]
test_step.status = api.step.SUCCESS
if not result['success']:
test_step.status = api.step.FAILURE
summary_step.status = api.step.FAILURE
if 'output' in result:
logs = api.file.read_text('read logs', result['output'])
test_step.logs['test.py output'] = logs.splitlines()
# Upload logs if they exist (test fails after Deployment starts)
compute_logs_dir = logs_dir / test
if api.path.exists(compute_logs_dir):
upload_step = _ZipAndUploadDirectory(
api,
storage_logs,
compute_logs_dir,
test + '.zip',
'Compute logs',
)
# Merge the gsutil links in the Test step.
upload_presentation = upload_step.presentation
for link in upload_presentation.links:
test_step.links[link] = upload_presentation.links[link]
except Exception as e:
summary_step.logs['exception %s' % test] = repr(e).splitlines()
return tests_summary
def GenTests(api: TEST_DEPS):
yield api.test(
'basic_try',
api.buildbucket.try_build(
project='celab', bucket='try', git_repo=CELAB_REPO
),
)
yield api.test(
'basic_ci_linux',
api.platform('linux', 64),
api.buildbucket.ci_build(project='celab', bucket='ci', git_repo=CELAB_REPO),
)
yield api.test(
'basic_ci_windows',
api.platform('win', 64),
api.buildbucket.ci_build(project='celab', bucket='ci', git_repo=CELAB_REPO),
)
yield api.test(
'failed_tests_ci_linux',
api.platform('linux', 64),
api.properties(tests='*', pool_name='celab-ci', pool_size=5),
api.buildbucket.ci_build(project='celab', bucket='ci', git_repo=CELAB_REPO),
api.step_data('run all tests', retcode=1),
api.step_data(
'test summary.parse summary',
api.file.read_json(
{
'1st test': {'success': False, 'output': '/some/file'},
'2nd test': {'success': True, 'output': '/other/file'},
'3rd test': {'success': False, 'output': '/missing'},
}
),
),
api.step_data(
'test summary.1st test.read logs', api.file.read_text('first\ntest\nlogs')
),
api.step_data('test summary.3rd test.read logs', api.file.errno('EEXIST')),
api.path.exists(api.path.start_dir.joinpath('logs', '1st test')),
api.expect_status('FAILURE'),
)
yield api.test(
'failed_tests_no_summary_ci_linux',
api.platform('linux', 64),
api.properties(tests='*', pool_name='celab-ci', pool_size=5),
api.buildbucket.ci_build(project='celab', bucket='ci', git_repo=CELAB_REPO),
api.step_data('run all tests', retcode=1),
api.step_data('test summary.parse summary', retcode=1),
api.expect_status('INFRA_FAILURE'),
)
yield api.test(
'windows_quick_tests',
api.properties(
tests='sample.test.*',
include='quick_test',
exclude='long_test',
pool_name='celab-try',
pool_size=5,
),
api.platform('win', 64),
api.buildbucket.ci_build(
project='celab',
bucket='try',
builder='windows-quick-tests',
git_repo=CELAB_REPO,
),
)
yield api.test(
'misconfigured_tests',
api.properties(tests='sample.test'),
api.platform('win', 64),
api.buildbucket.ci_build(
project='celab',
bucket='try',
builder='misconfigured-quick-tests',
git_repo=CELAB_REPO,
),
api.expect_exception('ValueError'),
)
yield api.test(
'chromium_try',
api.builder_group.for_current('tryserver.chromium.win'),
api.properties(
tests='chromium.test',
pool_name='chromium-try',
pool_size=5,
bot_id='test_bot',
),
api.platform('win', 64),
api.buildbucket.try_build(
project='chromium',
bucket='luci.chromium.try',
builder='win-celab-try-rel',
git_repo=CHROMIUM_REPO,
),
api.step_data(
'read vpython file',
api.file.read_text('''wheel: <
name: "infra/celab/celab/windows-amd64"
version: "celab_package_version"
>'''),
),
api.step_data(
'test summary.parse summary',
api.file.read_json({'1st test': {'success': False, 'output': '/file'}}),
),
)
yield api.test(
'chromium_no_tests',
api.builder_group.for_current('tryserver.chromium.win'),
api.properties(bot_id='test_bot'),
api.platform('win', 64),
api.buildbucket.try_build(
project='chromium',
bucket='luci.chromium.try',
builder='win-celab-try-rel',
git_repo=CHROMIUM_REPO,
),
api.expect_exception('ValueError'),
)
yield api.test(
'chromium_no_celab_package',
api.builder_group.for_current('tryserver.chromium.win'),
api.properties(tests='chromium.test', bot_id='test_bot'),
api.platform('win', 64),
api.buildbucket.try_build(
project='chromium',
bucket='luci.chromium.try',
builder='win-celab-try-rel',
git_repo=CHROMIUM_REPO,
),
api.step_data(
'read vpython file',
api.file.read_text('''wheel: <
name: "infra/other/package"
version: "package_version"
>'''),
),
api.expect_exception('ValueError'),
)
yield api.test(
'invalid_project',
api.buildbucket.ci_build(project='other-project'),
api.expect_exception('ValueError'),
)
yield api.test(
'compile_failure',
api.builder_group.for_current('tryserver.chromium.win'),
api.properties(
tests='chromium.test',
pool_name='chromium-try',
pool_size=5,
bot_id='test_bot',
),
api.platform('win', 64),
api.buildbucket.try_build(
project='chromium',
bucket='luci.chromium.try',
builder='win-celab-try-rel',
git_repo=CHROMIUM_REPO,
),
api.step_data('compile (with patch)', retcode=1),
api.expect_status('FAILURE'),
api.post_process(post_process.DropExpectation),
)
yield api.test(
'chrome_try',
api.builder_group.for_current('tryserver.chrome.win'),
api.properties(
tests='chrome.test',
pool_name='chrome-try',
pool_size=5,
bot_id='test_bot',
),
api.platform('win', 64),
api.buildbucket.try_build(
project='chrome',
bucket='luci.chrome.try',
builder='win-celab-try-rel',
git_repo=CHROMIUM_REPO,
),
api.step_data(
'read vpython file',
api.file.read_text('''wheel: <
name: "infra/celab/celab/windows-amd64"
version: "celab_package_version"
>'''),
),
api.step_data(
'test summary.parse summary',
api.file.read_json({'1st test': {'success': False, 'output': '/file'}}),
),
)