blob: f38e6fef195b223fed97bf3399b2b49ffc5c2275 [file] [log] [blame]
# 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.
import attr
import base64
import os
import re
from google.protobuf import json_format
from recipe_engine import recipe_api
from .test_runner import TestRunner
from RECIPE_MODULES.build.chromium_tests.resultdb import ResultDB
from PB.go.chromium.org.luci.buildbucket.proto import common as common_pb2
from PB.go.chromium.org.luci.resultdb.proto.v1 import common as common_rdb_pb
from PB.go.chromium.org.luci.resultdb.proto.v1 import (invocation as
invocation_pb)
# Skylab prioritizes tests by the Quota Scheduler account attached in the
# request. We applied account "lacros", which has limited high priority
# quota. It is supposed to grant to the production builders only.
# For fyi builders, we use 'lacros_fyi' which only contains the free quota,
# aka the lowest priority.
QS_ACCOUNT_PROD = 'lacros'
QS_ACCOUNT_FYI = 'lacros_fyi'
CTP_BUILDER = 'cros_test_platform'
CTP_BUILDER_DEV = 'cros_test_platform-dev'
CROS_BUCKET = 'gs://chromeos-image-archive/'
# Prefix of args that indicates variable names of Tast '-var' flags.
TAST_VARS_PREFIX = 'tast.'
def _base64_encode_str(s):
return base64.b64encode(s.encode('utf-8')).decode('ascii')
def _base64_encode_args(args, skip_prefix=None):
"""Base64-encode some args which contain shell-unsafe characters
Args:
args (list[str]): List of tast args, in the form of name=value
skip_prefix: If a name starts with this, it is copied to the result as-is.
Returns:
list of args, with some args encoded in base64 and _b64 suffix added to the name.
"""
find_unsafe = re.compile(r'[^\w@%+=:,./-]', re.ASCII).search
def encode_if_necessary(arg):
a = arg.split('=', 1)
if len(a) < 2:
return arg
name, value = a
if skip_prefix and name.startswith(skip_prefix):
return arg
if not find_unsafe(value):
return arg
b = _base64_encode_str(value)
return f'{name}_b64={b}'
return [encode_if_necessary(arg) for arg in args]
class SkylabApi(recipe_api.RecipeApi):
"""Module for issuing commands to Skylab"""
def get_lkgm_version(self, board: str, chrome_src: str,
use_external_config: bool) -> str:
"""Get LKGM or older latest version of ChromeOS available for the board.
The LKGM version is determined based on //chromeos/CHROMEOS_LKGM file
in the chrome checkout.
The returned version is usually the LKGM, but if the image for the board
is not available, an older version is searched as a fallback.
Also see chromite.ChromeLkgmSerivce/FindLkgm in BuildAPI description.
Args:
* board: The build target board with which tests are run.
* chrome_src: The location of the chrome checkout.
* use_external_config: Use the external (=chromiumos) configuration.
* When this is False, the internal configuration is used and
* the API call requires the read access of ChromeOS release images.
Returns:
The fully specified image name. e.g. "octopus-release/R89-13609".
"""
build_api = self.m.path.join(chrome_src, 'third_party', 'chromite', 'bin',
'build_api')
cmd = [
build_api,
'chromite.api.ChromeLkgmService/FindLkgm',
'--input-json',
self.m.json.input({
"build_target": {
"name": board,
},
"chrome_src": chrome_src,
"fallback_versions": 20,
"use_external_config": use_external_config
}),
'--output-json',
self.m.json.output(),
]
step_result = self.m.step(
'call build API',
cmd,
)
response = step_result.json.output
if response.get('error'):
raise recipe_api.StepFailure(
'chromite.api.ChromeLkgmService/FindLkgm returned error:' +
response.get('error'))
result = '/'.join([response.get('configName'), response.get('fullVersion')])
return result
def schedule_suite(self, test, suffix, retry_shards=None):
"""Schedule a Skylab test by invoking the cros_test_platform(CTP) build.
Translate each SkylabTest object into a CTP request and call Buildbucket
to schedule them.
Args:
* test (step.SkylabTest): a steps.SkylabTest to schedule.
* suffix: A string suffix.
* retry_shards (list[str]): the index for shards to retry. None by default.
"""
with self.m.step.nest(test.step_name(suffix)) as presentation:
cmd = [
'vpython3',
self.resource('skylab.py'),
'--chromium-src',
str(self.m.chromium_checkout.src_dir),
'--json-outfile',
self.m.json.output(),
'request',
]
cmd.extend(['--board', test.spec.cros_board])
if test.spec.cros_model:
cmd.extend(['--model', test.spec.cros_model])
if test.spec.bucket:
cmd.extend(['--bucket', test.spec.bucket])
if test.spec.public_builder and test.spec.public_builder_bucket:
cmd.extend(['--public-builder', test.spec.public_builder])
cmd.extend(['--public-builder-bucket', test.spec.public_builder_bucket])
cmd.extend([
'--pool',
test.spec.dut_pool if test.spec.dut_pool else 'DUT_POOL_QUOTA'
])
if test.spec.use_lkgm:
assert not test.spec.cros_img, 'cros_img should be empty when use_lkgm is True'
is_public = test.spec.bucket.startswith('chromiumos-')
cros_img = self.get_lkgm_version(test.spec.cros_board,
str(self.m.chromium_checkout.src_dir),
is_public)
assert cros_img, 'chromite build_api command not found'
else:
cros_img = test.spec.cros_img
cmd.extend(['--image', cros_img])
if test.spec.secondary_cros_board:
boards = test.spec.secondary_cros_board.split(',')
imgs = [''] * len(boards)
if test.spec.secondary_cros_img:
imgs = test.spec.secondary_cros_img.split(',')
if len(boards) != len(imgs):
raise recipe_api.StepFailure('Length of secondary_cros_img'
' must match secondary_cros_board')
for b, img in zip(boards, imgs):
if img == 'use_lkgm':
is_public = test.spec.bucket.startswith('chromiumos-')
img = self.get_lkgm_version(b,
str(self.m.chromium_checkout.src_dir),
is_public)
cmd.extend(['--secondary-boards', b])
cmd.extend(['--secondary-images', img])
cmd.extend(['--timeout-mins', str(int(test.spec.timeout_sec / 60))])
cmd.extend([
'--qs-account', QS_ACCOUNT_FYI
if 'fyi' in self.m.buildbucket.builder_name else QS_ACCOUNT_PROD
])
resultdb = self.gen_rdb_config(test, cros_img)
assert resultdb and resultdb.enable, ('Skylab tests should '
'have resultdb enabled.')
rdb_str = self.m.json.dumps({
k: getattr(resultdb, k)
for k in attr.fields_dict(ResultDB)
if not getattr(resultdb, k) in [None, '']
})
test_args = []
test_args.append('resultdb_settings=%s' % _base64_encode_str(rdb_str))
if test.spec.tast_expr:
# Due to crbug/1173329, skylab does not support arbitrary tast
# expressions. As a workaround, we encode test argument which may
# contain complicated patterns to base64.
test_args.append('tast_expr_b64=%s' %
_base64_encode_str(test.spec.tast_expr))
if test.spec.test_args:
if test.is_tast_test:
test_args.extend(
_base64_encode_args(test.spec.test_args, TAST_VARS_PREFIX))
else:
test_args.append('test_args_b64=%s' %
_base64_encode_str(' '.join(test.spec.test_args)))
test_retries = '2'
if test.spec.test_level_retries != None:
test_retries = test.spec.test_level_retries
test_args.append('retries=%s' % test_retries)
if test.exe_rel_path:
test_args.append('exe_rel_path=%s' % test.exe_rel_path)
if test.tast_expr_file:
test_args.append('tast_expr_file=%s' % test.tast_expr_file)
if test.spec.tast_expr_key:
test_args.append('tast_expr_key=%s' % test.spec.tast_expr_key)
if test.spec.extra_browser_args:
test_args.append('extra_browser_args_b64=%s' %
_base64_encode_str(test.spec.extra_browser_args))
if test.spec.benchmark:
test_args.append('benchmark=%s' % test.spec.benchmark)
if test.spec.results_label:
test_args.append('results_label=%s' % test.spec.results_label)
if test.spec.story_filter:
test_args.append('story_filter=%s' % test.spec.story_filter)
if test.spec.test_shard_map_filename:
test_args.append('test_shard_map_filename=%s' %
test.spec.test_shard_map_filename)
if test.spec.max_run_sec:
test_args.append('max_run_sec=%s' % test.spec.max_run_sec)
# TODO(crbug.com/1233676): Support chromium perf tests.
# if test.telemetry_shard_index is not None:
# test_args.append('test_shard_index=%s' % test.telemetry_shard_index)
if test.spec.bucket and 'chromium' in test.spec.bucket:
test_args.append('run_private_tests=False')
cmd.extend(['--test-args', ' '.join(test_args)])
lacros_gcs_path = os.path.join(test.lacros_gcs_path,
'lacros_compressed.squash')
cmd.extend(['--lacros-gcs-path', lacros_gcs_path])
if test.spec.secondary_cros_board:
should_provision_browser_files = test.spec.should_provision_browser_files or [
True
] * len(boards)
if len(should_provision_browser_files) != len(boards):
raise recipe_api.StepFailure(
'Length of should_provision_browser_files'
' must match secondary_cros_board')
for s in should_provision_browser_files:
cmd.extend(
['--secondary-lacros-gcs-path', lacros_gcs_path if s else ''])
cmd.extend(['--autotest-name', test.spec.autotest_name])
cmd.extend(['--total-shards', test.spec.shards])
for retry_shard in retry_shards or []:
cmd.extend(['--shard-indexes', retry_shard])
step_result = self.m.step(
'schedule',
cmd,
raise_on_failure=False,
step_test_data=lambda: self.m.json.test_api.output(
{'ctp_build_id': '889900'}))
if step_result.retcode == 0:
build_id = int(step_result.json.output['ctp_build_id'])
presentation.links[
test.name] = 'https://ci.chromium.org/b/%s' % build_id
test.ctp_build_ids[suffix] = build_id
def fetch_test_runners(self, test, suffix):
"""Fetch the CrOS test runner builds for each shard.
Each test runner build represents a shard like a swarming task, containing
its runtime info, e.g. std logs and infra status. Fetch them and attach
to the test's test_runner_builds for the given suffix.
Args:
* test (SkylabTest): a steps.SkylabTest.
* suffix: A string suffix.
"""
assert test.ctp_build_ids[suffix], ('No CTP build found.'
'Must call schedule_suite() first.')
cmd = [
'vpython3',
self.resource('skylab.py'),
'--chromium-src',
str(self.m.chromium_checkout.src_dir),
'--json-outfile',
self.m.json.output(),
'response',
'--ctp-build-id',
test.ctp_build_ids[suffix],
]
step_result = self.m.step(
'read_ctp_response',
cmd,
stdout=self.m.raw_io.output(),
stderr=self.m.raw_io.output(),
raise_on_failure=False,
step_test_data=lambda: self.m.json.test_api.output({
'0': {
'url':
'https://ci.chromium.org/p/chromeos/builders/test_runner/'
f'test_runner/b{test.ctp_build_ids[suffix]}0',
'log_url':
'https://cros-test-analytics.appspot.com/p/chromeos/logs/'
'browse/chromeos-test-logs/test-runner/prod/abcd',
'status': 'SUCCESS'
}
}))
if (hasattr(step_result, 'json') and step_result.json.output):
for shard, test_runner in step_result.json.output.items():
if test_runner:
tr = TestRunner.create(test, shard=int(shard), **test_runner)
test.test_runner_builds.setdefault(suffix, []).append(tr)
def gen_rdb_config(self, test, cros_img):
"""Generate the resultDB config for SkylabTest.
Args:
test: A step.SkylabTest object.
cros_img: A CrOS img name to be used for tests.
Returns:
A new config of ResultDB. See chromium_tests.ResultDB.
"""
var = dict(
test.spec.resultdb.base_variant or {}, test_suite=test.canonical_name)
var.update({
'device_type': test.spec.cros_board,
'os': 'ChromeOS',
'cros_img': cros_img,
})
result_format = 'gtest'
if test.is_tast_test:
result_format = 'tast'
elif test.is_GPU_test:
result_format = 'native'
gitiles_commit = self.m.buildbucket.build.output.gitiles_commit
sources = invocation_pb.Sources(
gitiles_commit=common_rdb_pb.GitilesCommit(
host=gitiles_commit.host,
project=gitiles_commit.project,
commit_hash=gitiles_commit.id,
ref=gitiles_commit.ref,
position=gitiles_commit.position,
),
changelists=[
common_rdb_pb.GerritChange(
host=change.host,
project=change.project,
change=change.change,
patchset=change.patchset)
for change in self.m.buildbucket.build.input.gerrit_changes
])
return attr.evolve(
test.spec.resultdb,
test_id_prefix=test.spec.test_id_prefix,
base_variant=var,
result_format=result_format,
# Skylab's result_file is hard-coded by the autotest wrapper in OS
# repo, and not required by callers. It suppose to be None, but then
# ResultDB will pass the default value ${ISOLATED_OUTDIR}/output.json
# which is confusing for Skylab test runner. So explicitly set it an
# empty string, as well as artifact_directory.
result_file='',
# Same with result_file, the abs path of artifact directory is
# determined at runtime on Skylab Drone server. We leave it empty here.
# CrOS recipe will feed that path to result adapter when uploading
# results.
artifact_directory='',
# The source code position of the test artifacts we send to Skylab.
sources=json_format.MessageToJson(sources),
)