blob: 9372305647a82fc323b5665566363cdca2e9404b [file] [log] [blame]
# Copyright 2018 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
from recipe_engine import recipe_api
import collections
import os
import pipes
# Host architecture -> number of bits -> host platform name.
# Add to this dictionary as we support building on more devices.
HOST_PLATFORMS = {
'intel': {
64: 'x64',
},
}
# List of available targets.
TARGETS = ['x64', 'arm64']
# List of available build types.
BUILD_TYPES = ['debug', 'release', 'thinlto', 'lto']
# The FVM block name.
FVM_BLOCK_NAME = 'fvm.blk'
# The GUID string for the system partition.
# Defined in //zircon/system/public/zircon/hw/gpt.h
GUID_SYSTEM_STRING = '606B000B-B7C7-4653-A7D5-B737332C899D'
# The PCI address to use for the block device to contain test results.
TEST_FS_PCI_ADDR = '06.0'
# How long to wait (in seconds) before killing the test swarming task if there's
# no output being produced.
TEST_IO_TIMEOUT_SECS = 180
# The path in the BootFS manifest that we want runcmds to show up at.
RUNCMDS_BOOTFS_PATH = 'infra/runcmds'
# (variant_name, switch) mapping Fuchsia GN variant names (as used in the
# variant property) to build-zircon.sh switches.
VARIANTS_ZIRCON = [
('host_asan', '-H'),
# TODO(ZX-2197): Don't build Zircon with ASan when building Fuchsia
# with ASan due to linking problems. Long run, unclear whether we
# want to enable ASan in Zircon pieces on Fuchsia ASan bots.
#('asan', '-A'),
]
# Images needed for testing.
IMAGES_FOR_TESTING = [
# Boot images.
'zircon-a',
'netboot',
'zircon-r',
# Images needed for running QEMU
'qemu-kernel',
'storage-full',
# Images needed for paving.
'efi',
'storage-sparse',
]
class FuchsiaCheckoutResults(object):
"""Represents a Fuchsia source checkout."""
def __init__(self, api, root_dir, snapshot_file):
self._api = api
self._root_dir = root_dir
self._snapshot_file = snapshot_file
@property
def root_dir(self):
"""The path to the root directory of the jiri checkout."""
return self._root_dir
@property
def snapshot_file(self):
"""The path to the jiri snapshot file."""
return self._snapshot_file
def upload_results(self, gcs_bucket):
"""Upload snapshot to a given GCS bucket."""
assert gcs_bucket
with self._api.m.step.nest('upload checkout results'):
self._api._upload_file_to_gcs(self.snapshot_file, gcs_bucket)
class FuchsiaBuildResults(object):
"""Represents a completed build of Fuchsia."""
def __init__(self, api, target, zircon_build_dir, fuchsia_build_dir):
assert target in TARGETS
self._api = api
self._zircon_build_dir = zircon_build_dir
self._fuchsia_build_dir = fuchsia_build_dir
self._target = target
self._images = {}
self._includes_package_archive = False
@property
def target(self):
"""The build target for this build."""
return self._target
@property
def zircon_build_dir(self):
"""The directory where Zircon build artifacts may be found."""
return self._zircon_build_dir
@property
def fuchsia_build_dir(self):
"""The directory where Fuchsia build artifacts may be found."""
return self._fuchsia_build_dir
@property
def ids(self):
return self._fuchsia_build_dir.join('ids.txt')
@property
def images(self):
"""Mapping between the canonical name of an image produced by the Fuchsia
build to the path to that image on the local disk."""
return self._images
@property
def includes_package_archive(self):
return self._includes_package_archive
@includes_package_archive.setter
def includes_package_archive(self, value):
self._includes_package_archive = value
def upload_results(self,
gcs_bucket,
include_breakpad_symbols=False,
include_symbol_archive=False):
"""Upload build results to a given GCS bucket."""
assert gcs_bucket
with self._api.m.step.nest('upload build results'):
self._api._upload_build_results(self,
gcs_bucket, include_breakpad_symbols, include_symbol_archive)
class FuchsiaTestResults(object):
"""Represents the result of testing of a Fuchsia build."""
# Constants representing the result of running a test. These enumerate the
# values of the 'results' field of the entries in the summary.json file
# obtained from the target device.
_TEST_RESULT_PASS = 'PASS'
_TEST_RESULT_FAIL = 'FAIL'
def __init__(self, name, build_dir, results_dir, zircon_kernel_log,
outputs, api, symbolizer_output=None):
self._name = name
self._build_dir = build_dir
self._results_dir = results_dir
self._zircon_kernel_log = zircon_kernel_log
self._outputs = outputs
self._api = api
self._symbolizer_output = symbolizer_output
# Default to empty values if the summary file is missing.
if 'summary.json' in outputs:
# Cache the summary file if present.
self._raw_summary = outputs['summary.json']
# TODO(kjharland): Raise explicit step failure when parsing fails so
# that it's clear that the summary file is malformed.
self._summary = api.json.loads(outputs['summary.json'])
api.fuchsia._analyze_test_results(self)
else:
self._raw_summary = ''
self._summary = {}
@property
def name(self):
"""The unique name for this set of test results."""
return self._name
@property
def build_dir(self):
"""A path to the build directory for symbolization artifacts."""
return self._build_dir
@property
def results_dir(self):
"""A path to the directory that contains test results."""
return self._results_dir
@property
def outputs(self):
"""A dict that maps test outputs to their contents.
The output paths are relative paths to files containing stdout+stderr data
and the values are strings containing those contents.
"""
return self._outputs
@property
def raw_summary(self):
"""The raw contents of the JSON summary file or "" if missing."""
return self._raw_summary
@property
def summary(self):
"""The parsed summary file as a Dict or {} if missing."""
return self._summary
@property
def passed_test_outputs(self):
"""All entries in |self.outputs| for tests that passed."""
return self._filter_outputs_by_test_result(self._TEST_RESULT_PASS)
@property
def failed_test_outputs(self):
"""All entries in |self.outputs| for tests that failed."""
return self._filter_outputs_by_test_result(self._TEST_RESULT_FAIL)
def _filter_outputs_by_test_result(self, result):
"""Returns all entries in |self.outputs| whose result is |result|.
Args:
result (String): one of the _TEST_RESULT_* constants from this class.
Returns:
A dict whose keys are paths to the files containing each test's
stderr+stdout data and whose values are strings containing those
contents.
"""
matches = collections.OrderedDict()
# TODO(kjharland): Sort test names first.
for test in self.summary.get('tests', ()):
if test['result'] == result:
# By convention we use the full path to the test binary as the 'name'
# field in the summary. The 'output_file' field is a path to the file
# containing the stderr+stdout data for the test, and we inline the
# contents of that file as the value in the returned dict.
matches[test['name']] = self.outputs[test['output_file']]
return matches
def upload_results(self, gcs_bucket, process_coverage_data):
"""Upload select test results (e.g., coverage data) to a given GCS bucket."""
assert gcs_bucket
if process_coverage_data:
assert self._symbolizer_output
self._api.fuchsia._process_coverage(self, gcs_bucket, self._symbolizer_output)
def raise_failures(self):
"""Raises a step failure if there were test failures."""
if not self._summary:
# Halt with step failure if summary file is missing.
raise self._api.step.StepFailure(
'Test summary JSON not found, see kernel log for details')
failed_tests = self.failed_test_outputs.keys()
if failed_tests:
# Halt with a step failure.
raise self._api.step.StepFailure('Test failure(s): ' + ', '.join(failed_tests))
class FuchsiaApi(recipe_api.RecipeApi):
"""APIs for checking out, building, and testing Fuchsia."""
FuchsiaTestResults = FuchsiaTestResults
def __init__(self, *args, **kwargs):
super(FuchsiaApi, self).__init__(*args, **kwargs)
def checkout(self,
build,
manifest,
remote,
project=None,
timeout_secs=40 * 60):
"""Uses Jiri to check out a Fuchsia project.
The root of the checkout is returned via FuchsiaCheckoutResults.root_dir.
Args:
build (buildbucket.build_pb2.Build): A buildbucket build.
manifest (str): A path to the manifest in the remote (e.g. manifest/minimal)
remote (str): A URL to the remote repository which Jiri will be pointed at
project (str): The name of the project
timeout_secs (int): How long to wait for the checkout to complete
before failing
Returns:
A FuchsiaCheckoutResults containing details of the checkout.
"""
with self.m.step.nest("checkout"):
with self.m.context(infra_steps=True):
is_global_integration = project == 'integration' and 'global' in build.builder.bucket
self.m.checkout(
manifest,
remote,
project=project,
build_input=build.input if build else None,
timeout_secs=timeout_secs,
override=not is_global_integration,
)
snapshot_file = self.m.path['cleanup'].join('jiri.snapshot')
self.m.jiri.snapshot(snapshot_file)
return self._finalize_checkout(snapshot_file=snapshot_file)
def checkout_snapshot(self,
repository=None,
revision=None,
gitiles_commit=None,
timeout_secs=20 * 60):
"""Uses Jiri to check out Fuchsia from a Jiri snapshot.
The root of the checkout is returned via FuchsiaCheckoutResults.root_dir.
Args:
repository (str): A URL to the remote repository containing the snapshot.
The snapshot should be available at the top-level in a file called
'snapshot'.
revision (str): The git revision to check out from the repository.
gitiles_commit (buildbucket.common_pb2.GitilesCommit): A Gitiles commit.
timeout_secs (int): How long to wait for the checkout to complete
before failing.
Returns:
A FuchsiaCheckoutResults containing details of the checkout.
"""
if gitiles_commit:
repository = repository or 'https://%s/%s' % (
gitiles_commit.host,
gitiles_commit.project,
)
revision = revision or gitiles_commit.id
with self.m.context(infra_steps=True):
snapshot_repo_dir = self.m.path['cleanup'].join('snapshot_repo')
# Without any patch information, we just want to fetch whatever we're
# told via repository and revision.
self.m.git.checkout(
url=repository,
ref=revision,
path=snapshot_repo_dir,
)
return self._checkout_snapshot(snapshot_repo_dir=snapshot_repo_dir)
def checkout_patched_snapshot(self,
gerrit_change,
timeout_secs=20 * 60):
"""Uses Jiri to check out Fuchsia from a Jiri snapshot from a Gerrit patch.
The root of the checkout is returned via FuchsiaCheckoutResults.root_dir.
Args:
gerrit_change (buildbucket.common_pb2.GerritChange): A Gerrit change.
timeout_secs (int): How long to wait for the checkout to complete
before failing
Returns:
A FuchsiaCheckoutResults containing details of the checkout.
"""
with self.m.context(infra_steps=True):
snapshot_repo_dir = self.m.path['cleanup'].join('snapshot_repo')
# 1) Check out the patch from Gerrit (initializing the repo also).
# 2) Learn the destination branch for the Gerrit change.
# 3) Fetch and rebase the patch against the destination branch.
#
# Firstly, we want to rebase on top of the upstream branch because when
# we're testing, we want to test the rebased change to make the tryjob as
# accurate as possible before submitting.
#
# Secondly, we need to fetch the destination branch for a Gerrit change
# via the Gerrit recipe module because CQ does not provide this
# information. This is the canonical way in which other Chrome Infra
# tryjob recipes are able to rebase onto the destination branch.
self.m.gerrit.ensure_gerrit()
details = self.m.gerrit.change_details(
name='get change details',
change_id='%s~%s' % (gerrit_change.project, gerrit_change.change),
gerrit_host='https://%s' % gerrit_change.host,
query_params=["CURRENT_REVISION"],
test_data=self.m.json.test_api.output({
'branch': 'master',
'current_revision': 'a1b2c3',
'revisions': {
'a1b2c3': {
'ref': 'refs/changes/00/100/5'
}
}
}),
)
current_revision = details['current_revision']
self.m.git.checkout(
url='https://%s/%s' % (gerrit_change.host, gerrit_change.project),
ref=details['revisions'][current_revision]['ref'],
path=snapshot_repo_dir,
)
with self.m.context(cwd=snapshot_repo_dir):
git_host = gerrit_change.host
gs_suffix = '-review.googlesource.com'
assert git_host.endswith(gs_suffix)
git_host = '%s.googlesource.com' % git_host[:-len(gs_suffix)]
self.m.git(
'fetch',
'https://%s/%s' % (git_host, gerrit_change.project),
details['branch'],
)
self.m.git('rebase', 'FETCH_HEAD')
return self._checkout_snapshot(snapshot_repo_dir=snapshot_repo_dir)
def _checkout_snapshot(self, snapshot_repo_dir):
# Read the snapshot so it shows up in the step presentation.
snapshot_file = snapshot_repo_dir.join('snapshot')
cherrypick_file = snapshot_repo_dir.join('cherrypick.json')
# Create a checkout from the snapshot.
self.m.checkout.from_snapshot(snapshot_file)
# Perform cherrypicks if there is a cherrypick file.
if self.m.path.exists(cherrypick_file):
# Get the cherrypick json
cherrypick_json = self.m.file.read_raw(
'read cherrypick file', cherrypick_file, '{\"topaz\":[\"test\"]}')
cherrypick_dict = self.m.json.loads(cherrypick_json)
for project, cherrypicks in cherrypick_dict.items():
# Get the project relative checkout path and join it to the start_dir.
# path always exists in the snapshot file, so we don't need to check for its existance.
project_path = self.m.path['start_dir'].join(
self.m.jiri.read_manifest_element(
manifest=snapshot_file,
element_type='project',
element_name=project)['path'])
for cherrypick in cherrypicks:
self.m.git('-C', project_path, 'cherry-pick', cherrypick,
'--keep-redundant-commits')
return self._finalize_checkout(snapshot_file=snapshot_file)
def _finalize_checkout(self, snapshot_file):
"""Finalizes a Fuchsia checkout; constructs a FuchsiaCheckoutResults object."""
snapshot_contents = self.m.file.read_text('read snapshot', snapshot_file)
# Always log snapshot contents (even if uploading to GCS) to help debug
# things like tryjob failures during roller-commits.
snapshot_step_logs = self.m.step.active_result.presentation.logs
snapshot_step_logs['snapshot_contents'] = snapshot_contents.split('\n')
return FuchsiaCheckoutResults(
api=self,
root_dir=self.m.path['start_dir'],
snapshot_file=snapshot_file,
)
def _build_zircon(self, target, variants, zircon_args):
"""Builds zircon for the specified target."""
cmd = [
self.m.path['start_dir'].join('scripts', 'build-zircon.sh'),
'-v', # level one verbosity
'-t',
target,
]
for variant, switch in VARIANTS_ZIRCON:
if variant in variants:
cmd.append(switch)
cmd += [
'-j',
self.m.goma.jobs,
'GOMACC=%s' % self.m.goma.goma_dir.join('gomacc'),
]
if zircon_args:
cmd.append(' '.join(zircon_arg for zircon_arg in zircon_args))
self.m.step('zircon', cmd)
def _build_fuchsia(self, build, build_type, packages, variants, gn_args,
ninja_targets, board, product, collect_build_metrics,
build_for_testing, build_archive, build_package_archive):
"""Builds fuchsia given a FuchsiaBuildResults and other GN options."""
with self.m.step.nest('build fuchsia'):
# Set the path to GN and Ninja executables since they are not installed from CIPD.
self.m.gn.set_path(self.m.path['start_dir'].join('buildtools', 'gn'))
self.m.ninja.set_path(self.m.path['start_dir'].join('buildtools', 'ninja'))
fuchsia_gn_args = self._gn_args(
board=board,
build_type=build_type,
goma_dir=self.m.goma.goma_dir,
is_debug=build_type == 'debug',
packages=packages,
product=product,
target=build.target,
variants=variants,
) + list(gn_args)
full_gn_args = [
build.fuchsia_build_dir,
'--check',
'--args=%s' % ' '.join(fuchsia_gn_args),
]
if collect_build_metrics:
tracelog_path = str(self.m.path['cleanup'].join('gn_trace.json'))
full_gn_args.append('--tracelog=%s' % tracelog_path)
self.m.gn('gen', *full_gn_args)
# gn gen will have produced the image manifest. Read it in, ensure that
# images needed for testing will be built, and record the paths on disk
# where the produced images will be found.
image_manifest = self.m.json.read(
'read image manifest',
build.fuchsia_build_dir.join('images.json'),
step_test_data=lambda: self.test_api.mock_image_manifest(),
).json.output
zedboot_image_manifest = self.m.json.read(
'read zedboot image manifest',
build.fuchsia_build_dir.join('zedboot_images.json'),
step_test_data=lambda: self.test_api.mock_zedboot_image_manifest(),
).json.output
image_manifest.extend(zedboot_image_manifest)
for image in image_manifest:
name = image['name']
path = image['path']
type = image['type']
include_image = build_for_testing and name in IMAGES_FOR_TESTING
include_image = include_image or (
name == 'archive' and type == 'zip' and build_archive
)
# There might be multiple images under the name "netboot"; only take
# netboot.zbi.
if build_for_testing and name == 'netboot':
include_image = type == 'zbi'
if include_image:
ninja_targets.append(path)
build.images[name] = (
self.m.path.abs_to_path(self.m.path.realpath(
build.fuchsia_build_dir.join(path))
)
)
# ids.txt is needed for symbolization.
if build_for_testing:
ninja_targets.append('ids.txt')
if build_package_archive:
ninja_targets.append('updates')
build.includes_package_archive = True
self.m.ninja(
build_dir=build.fuchsia_build_dir,
targets=ninja_targets,
job_count=self.m.goma.jobs
)
def build(self,
target,
build_type,
packages,
variants=(),
gn_args=[],
ninja_targets=(),
board=None,
product=None,
zircon_args=[],
collect_build_metrics=False,
build_for_testing=False,
build_archive=False,
build_package_archive=False):
"""Builds Fuchsia from a Jiri checkout.
Expects a Fuchsia Jiri checkout at api.path['start_dir'].
Args:
target (str): The build target, see TARGETS for allowed targets
build_type (str): One of the build types in BUILD_TYPES
packages (sequence[str]): A sequence of packages to pass to GN to build
variants (sequence[str]): A sequence of build variant selectors to pass
to GN in `select_variant`
gn_args (sequence[str]): Additional arguments to pass to GN
ninja_targets (sequence[str]): Additional target args to pass to ninja
board (str): A board to pass to GN to build
product (str): A product to pass to GN to build
zircon_args (sequence[str]): A sequence of Make arguments to pass when
building zircon.
collect_build_metrics (bool): Whether to collect build metrics.
build_for_testing (bool): Whether to build the basic images needed to boot
and test on fuchsia.
build_archive (bool): Whether to build an image archive to be uploaded.
build_package_archive (bool): Whether to build a package archive to be
uploaded, to be used for updating.
Returns:
A FuchsiaBuildResults, representing the recently completed build.
"""
assert target in TARGETS
assert build_type in BUILD_TYPES
if build_type == 'debug':
build_dir = 'debug'
else:
build_dir = 'release'
out_dir = self.m.path['start_dir'].join('out')
build = FuchsiaBuildResults(
api=self,
target=target,
zircon_build_dir=out_dir.join('build-zircon', 'build-%s' % target),
fuchsia_build_dir=out_dir.join('%s-%s' % (build_dir, target)),
)
with self.m.step.nest('build'):
self.m.goma.ensure_goma()
with self.m.goma.build_with_goma():
self._build_zircon(target, variants, zircon_args)
self._build_fuchsia(
build=build,
build_type=build_type,
packages=packages,
variants=variants,
gn_args=gn_args,
ninja_targets=list(ninja_targets),
board=board,
product=product,
collect_build_metrics=collect_build_metrics,
build_for_testing=build_for_testing,
build_archive=build_archive,
build_package_archive=build_package_archive,
)
self.m.minfs.minfs_path = out_dir.join('build-zircon', 'tools', 'minfs')
self.m.zbi.zbi_path = out_dir.join('build-zircon', 'tools', 'zbi')
return build
def _symbolize(self, build_dir, data, json_output=None):
"""Invokes zircon's symbolization script to symbolize the given data."""
downloads_dir = self.m.path['start_dir'].join('zircon', 'prebuilt',
'downloads')
symbolize_cmd = [
downloads_dir.join('symbolize'),
'-ids',
build_dir.join('ids.txt'),
'-llvm-symbolizer',
downloads_dir.join('clang', 'bin', 'llvm-symbolizer'),
]
if json_output:
symbolize_cmd.extend(['-json-output', json_output])
symbolize_result = self.m.step(
'symbolize logs',
symbolize_cmd,
stdin=self.m.raw_io.input(data=data),
stdout=self.m.raw_io.output(),
step_test_data=
lambda: self.m.raw_io.test_api.stream_output('blah\nblah\n'))
symbolized_lines = symbolize_result.stdout.splitlines()
if symbolized_lines:
symbolize_result.presentation.logs['symbolized logs'] = symbolized_lines
def _isolate_files_at_isolated_root(self, files):
"""Isolates a set of files such that they all appear at the top-level.
Args:
files (seq[Path]): A list of paths which point to files to be isolated.
Returns:
The isolated hash that may be used to reference and download the
artifacts.
"""
assert files
self.m.isolated.ensure_isolated(version='latest')
isolated = self.m.isolated.isolated()
# Add the extra files to isolated at the top-level.
names = set()
for path in files:
# Ensure we don't have duplicate names at the top-level.
name = self.m.path.basename(path)
assert name not in names
names.add(name)
isolated.add_file(
path, wd=self.m.path.abs_to_path(self.m.path.dirname(path)))
# Archive the isolated.
return isolated.archive('isolate artifacts')
@property
def results_dir_on_target(self):
"""The directory on target to which target test results will be written."""
return '/tmp/infra-test-output'
@property
def results_dir_on_host(self):
"""The directory on host to which host and target test results will be written.
Target test results will be copied over to this location and host test
results will be written here. Host and target tests on should write to
separate subdirectories so as not to collide.
"""
return self.m.path['start_dir'].join('test_results')
def test_on_host(self, build):
"""Tests a Fuchsia build from the host machine.
Args:
build (FuchsiaBuildResults): The Fuchsia build to test.
Returns:
A FuchsiaTestResults representing the completed test.
"""
runtests = build.zircon_build_dir.join('tools', 'runtests')
host_test_dir = build.fuchsia_build_dir.join('host_tests')
# Write test results to the 'host' subdirectory of |results_dir_on_host|
# so as not to collide with target test results.
test_results_dir = self.results_dir_on_host.join('host')
# The following executes a script that (1) sets sanitizer-related environment
# variables, given a path to the clang pre-built binaries as its first
# argument, and then (2) executes the remaining arguments as an appended
# command.
host_platform = HOST_PLATFORMS[self.m.platform.arch][self.m.platform.bits]
set_vars_and_run_cmd = [
self.m.path['start_dir'].join('build', 'gn_run_binary.sh'),
self.m.path['start_dir'].join(
'buildtools', '%s-%s' % (self.m.platform.name, host_platform),
'clang', 'bin'),
]
# Allow the runtests invocation to fail without resulting in a step failure.
# The relevant, individual test failures will be reported during the
# processing of summary.json - and an early step failure will prevent this.
self.m.step(
'run host tests',
set_vars_and_run_cmd + [
runtests,
'-o',
self.m.raw_io.output_dir(leak_to=test_results_dir),
host_test_dir,
],
ok_ret='any')
# Extract test results.
test_results_map = self.m.step.active_result.raw_io.output_dir
return FuchsiaTestResults(
name='host',
build_dir=build.fuchsia_build_dir,
results_dir=test_results_dir,
zircon_kernel_log=None, # We did not run tests on target.
outputs=test_results_map,
api=self.m,
)
def _create_runcmds_script(self, device_type, test_cmds, output_path):
"""Creates a script for running tests on boot."""
# The device topological path is the toplogical path to the block device
# which will contain test output.
device_topological_path = '/dev/sys/pci/00:%s/virtio-block/block' % (
TEST_FS_PCI_ADDR)
# Script that mounts the block device to contain test output and runs tests,
# dropping test output into the block device.
results_dir = self.results_dir_on_target
runcmds = [
'mkdir %s' % results_dir,
]
if device_type == 'QEMU':
runcmds.extend([
# Wait until the MinFS test image shows up (max <timeout> ms).
'waitfor class=block topo=%s timeout=60000' % device_topological_path,
'mount %s %s' % (device_topological_path, results_dir),
] + test_cmds + [
'umount %s' % results_dir,
'dm poweroff',
])
else:
runcmds.extend(test_cmds)
self.m.file.write_text('write runcmds', output_path, '\n'.join(runcmds))
self.m.step.active_result.presentation.logs['runcmds'] = runcmds
def _construct_qemu_task_request(self, task_name, zbi_path, build, test_pool,
timeout_secs, external_network,
secret_bytes):
"""Constructs a Swarming task request which runs Fuchsia tests inside QEMU.
Expects the build and artifacts to be at the same place they were at
the end of the build.
Args:
build (FuchsiaBuildResults): The Fuchsia build to test.
test_pool (str): Swarming pool from which the test task will be drawn.
timeout_secs (int): The amount of seconds to wait for the tests to
execute before giving up.
external_network (bool): Whether to give Fuchsia inside QEMU access
to the external network.
secret_bytes (str): secret bytes to pass to the QEMU task.
Returns:
An api.swarming.TaskRequest representing the swarming task request.
"""
self.m.swarming.ensure_swarming(version='latest')
# Use canonical image names to refer to images to extract the path to that
# image. These dict keys come from the images.json format which is produced
# by a Fuchsia build.
# TODO(BLD-253): Point to the schema once there is one.
storage_full_path = build.images['storage-full']
qemu_kernel_path = build.images['qemu-kernel']
# All the *_name references below act as relative paths to the corresponding
# artifacts in the test Swarming task, since we isolate all of the artifacts
# into the root directory where the isolate is extracted.
zbi_name = self.m.path.basename(zbi_path)
storage_full_name = self.m.path.basename(storage_full_path)
qemu_kernel_name = self.m.path.basename(qemu_kernel_path)
# As part of running tests, we'll send a MinFS image over to another machine
# which will be declared as a block device in QEMU, at which point
# Fuchsia will mount it and write test output to.
minfs_image_name = 'output.fs'
# Create MinFS image (which will hold test output). We choose 1G for the
# MinFS image arbitrarily, and it appears it can hold our test output
# comfortably without going overboard on size.
minfs_image_path = self.m.path['start_dir'].join(minfs_image_name)
self.m.minfs.create(minfs_image_path, '1G', name='create test image')
botanist_cmd = [
'./botanist/botanist',
'qemu',
'-qemu-dir', './qemu/bin',
'-qemu-kernel', qemu_kernel_name,
'-zircon-a', zbi_name,
'-storage-full', storage_full_name,
'-arch', build.target,
'-minfs', minfs_image_name,
'-pci-addr', TEST_FS_PCI_ADDR,
'-use-kvm'
] # yapf: disable
if external_network:
botanist_cmd.append('-enable-networking')
botanist_cmd.append(
'zircon.autorun.system=/boot/bin/sh+/boot/%s' % RUNCMDS_BOOTFS_PATH)
# Isolate the Fuchsia build artifacts in addition to the test image and the
# qemu runner.
isolated_hash = self._isolate_files_at_isolated_root([
zbi_path,
storage_full_path,
qemu_kernel_path,
minfs_image_path,
])
cipd_arch = {
'arm64': 'arm64',
'x64': 'amd64',
}[build.target]
dimension_cpu = {
'arm64': 'arm64',
'x64': 'x86-64',
}[build.target]
return self.m.swarming.task_request(
name=task_name,
cmd=botanist_cmd,
isolated=isolated_hash,
dimensions={
'pool': test_pool,
'os': 'Debian',
'cpu': dimension_cpu,
'kvm': '1',
},
io_timeout_secs=TEST_IO_TIMEOUT_SECS,
hard_timeout_secs=timeout_secs,
secret_bytes=secret_bytes,
outputs=[minfs_image_name],
cipd_packages=[
('qemu', 'fuchsia/qemu/linux-%s' % cipd_arch, 'latest'),
('botanist', 'fuchsia/tools/botanist/linux-%s' % cipd_arch,
'latest'),
],
)
def _construct_device_task_request(self, task_name, device_type, zbi_path,
build, test_pool, pave, timeout_secs,
extra_dimensions={}, host_cmd=[]):
"""Constructs a Swarming task request to run Fuchsia tests on a device.
Expects the build and artifacts to be at the same place they were at
the end of the build.
Args:
build (FuchsiaBuildResults): The Fuchsia build to test.
test_pool (str): Swarming pool from which the test task will be drawn.
pave (bool): Whether or not the build artifacts should be paved.
timeout_secs (int): The amount of seconds to wait for the tests to
execute before giving up.
extra_dimensions (dict): Additional dimensions for swarming allocation
host_cmd (List(string)): Command to run on host after paving
Returns:
An api.swarming.TaskRequest representing the swarming task request.
"""
# TODO(BLD-253): images in doc string should point to the schema once there is one.
# TODO(IN-831): Remove "extra_dimensions" and "host_cmd" once ACTS is migrated to
# use standard host-target-interation test infra
self.m.swarming.ensure_swarming(version='latest')
# All the *_name references below act as relative paths to the corresponding
# artifacts in the test Swarming task, since we isolate all of the artifacts
# into the root directory where the isolate is extracted.
zbi_name = self.m.path.basename(zbi_path)
# Construct the botanist command.
output_archive_name = 'out.tar'
botanist_cmd = [
'./botanist/botanist',
'zedboot',
'-properties', '/etc/botanist/config.json',
'-kernel', zbi_name,
'-results-dir', self.results_dir_on_target,
'-out', output_archive_name,
] # yapf: disable
# image_paths collects the paths to all artifacts consumed by bonanist.
# Paving builds will add additional paths.
image_paths = [
zbi_path,
]
if build.target == 'arm64':
# zircon-r is Zedboot
zircon_r_path = build.images['zircon-r']
zircon_r_name = self.m.path.basename(zircon_r_path)
image_paths.append(zircon_r_path)
botanist_cmd.extend(['-fastboot', '-zedboot', zircon_r_name])
# If we're paving, ensure we add the additional necessary artifacts.
if pave:
if build.target == 'x64':
efi_path = build.images['efi']
efi_name = self.m.path.basename(efi_path)
image_paths.append(efi_path)
botanist_cmd.extend([
'-efi',
efi_name,
])
storage_sparse_path = build.images['storage-sparse']
storage_sparse_name = self.m.path.basename(storage_sparse_path)
image_paths.append(storage_sparse_path)
botanist_cmd.extend([
'-fvm',
storage_sparse_name,
])
if host_cmd:
botanist_cmd.extend(['-hacky-host-cmd'] + host_cmd)
# Add the kernel command line arg for invoking runcmds.
botanist_cmd.append(
'zircon.autorun.system=/boot/bin/sh+/boot/%s' % RUNCMDS_BOOTFS_PATH)
# Isolate all the necessary artifacts used by the botanist command.
isolated_hash = self._isolate_files_at_isolated_root(image_paths)
dimensions = {
'pool': test_pool,
'device_type': device_type,
}
dimensions.update(extra_dimensions)
return self.m.swarming.task_request(
name=task_name,
cmd=botanist_cmd,
isolated=isolated_hash,
dimensions=dimensions,
io_timeout_secs=TEST_IO_TIMEOUT_SECS,
hard_timeout_secs=timeout_secs,
outputs=[output_archive_name],
cipd_packages=[('botanist', 'fuchsia/tools/botanist/linux-amd64',
'latest'), ('fastboot', 'fuchsia/infra/external/fastboot/linux-amd64', 'latest')],
)
def _extract_test_results(self, device_type, archive_path, shard_name='', leak_to=None):
"""Extracts test results from an archive.
The format of the archive depends on device_type, so that is used to
determine the method we use to extract test results.
Args:
device_type (str): The type of device tests were run on.
archive_path (Path): The path to the archive which contains test results.
leak_to (Path): Optionally leak the contents of the archive to a
directory.
shard_name (str): The optional name of the shard for which we're
extracting test results. This will be included in the step name for
testing purposes.
Returns:
A dict mapping a filepath relative to the root of the archive to the
contents of that file in the archive.
"""
step_name = 'extract results'
if shard_name:
step_name = 'extract %s results' % shard_name
if device_type == 'QEMU':
return self.m.minfs.copy_image(
step_name=step_name,
image_path=archive_path,
out_dir=leak_to,
).raw_io.output_dir
else:
self.m.tar.ensure_tar()
return self.m.tar.extract(
step_name=step_name,
path=archive_path,
directory=self.m.raw_io.output_dir(leak_to=leak_to),
).raw_io.output_dir
def _decrypt_secrets(self, build):
"""Decrypts the secrets included in the build.
Args:
build (FuchsiaBuildResults): The build for which secret specs were
generated.
Returns:
The dictionary that maps secret spec name to the corresponding plaintext.
"""
self.m.cloudkms.ensure_cloudkms()
secret_spec_dir = build.fuchsia_build_dir.join('secret_specs')
secrets_map = {}
with self.m.step.nest('process secret specs'):
secret_spec_files = self.m.file.listdir('list', secret_spec_dir)
for secret_spec_file in secret_spec_files:
basename = self.m.path.basename(secret_spec_file)
# Skip the 'ciphertext' subdirectory.
if basename == 'ciphertext':
continue
secret_name, _ = basename.split('.json', 1)
secret_spec = self.m.json.read('read spec for %s' % secret_name,
secret_spec_file).json.output
# For each test spec file <name>.json in this directory, there is an
# associated ciphertext file at ciphertext/<name>.ciphertext.
ciphertext_file = secret_spec_dir.join('ciphertext',
'%s.ciphertext' % secret_name)
key_path = secret_spec['cloudkms_key_path']
secrets_map[secret_name] = self.m.cloudkms.decrypt(
'decrypt secret for %s' % secret_name, key_path, ciphertext_file,
self.m.raw_io.output()).raw_io.output
return secrets_map
def test(self,
build,
test_pool,
test_cmds,
device_type,
timeout_secs=40 * 60,
pave=True,
external_network=False,
requires_secrets=False,
extra_dimensions={},
host_cmd=[]):
"""Tests a Fuchsia build on the specified device.
Expects the build and artifacts to be at the same place they were at
the end of the build.
Args:
build (FuchsiaBuildResults): The Fuchsia build to test.
test_pool (str): Swarming pool from which the test task will be drawn.
timeout_secs (int): The amount of seconds to wait for the tests to
execute before giving up.
external_network (bool): Whether to enable access to the external
network when executing tests. Ignored if device_type != 'QEMU'.
requires_secrets (bool): Whether tests require plaintext secrets;
ignored if device_type == 'QEMU'.
pave (bool): Whether to pave the image to disk. Ignored if
device_type == 'QEMU'.
extra_dimensions (dict): Additional dimensions for swarming allocation
host_cmd (List(string)): Command to run on host after paving
Returns:
A FuchsiaTestResults representing the completed test.
"""
# TODO(IN-831): Remove "extra_dimensions" and "host_cmd" once ACTS is migrated to
# use standard host-target-interation test infra
assert bool(test_cmds)
assert bool(device_type)
runcmds_path = self.m.path['cleanup'].join('runcmds')
self._create_runcmds_script(device_type, test_cmds, runcmds_path)
# Inject the runcmds script into the bootfs image.
if not pave and device_type != 'QEMU':
zbi_path = build.images['netboot']
else:
zbi_path = build.images['zircon-a']
new_zbi_path = build.fuchsia_build_dir.join('test-infra.zbi')
self.m.zbi.copy_and_extend(
step_name='create test zbi',
input_image=zbi_path,
output_image=new_zbi_path,
manifest={RUNCMDS_BOOTFS_PATH: runcmds_path},
)
if device_type == 'QEMU':
secret_bytes = ''
if requires_secrets:
secret_bytes = self.m.json.dumps(self._decrypt_secrets(build))
task = self._construct_qemu_task_request(
task_name='all tests',
zbi_path=new_zbi_path,
build=build,
test_pool=test_pool,
timeout_secs=timeout_secs,
external_network=external_network,
secret_bytes=secret_bytes,
)
else:
task = self._construct_device_task_request(
task_name='all tests',
device_type=device_type,
zbi_path=new_zbi_path,
build=build,
test_pool=test_pool,
pave=pave,
timeout_secs=timeout_secs,
extra_dimensions=extra_dimensions,
host_cmd=host_cmd,
)
with self.m.context(infra_steps=True):
# Spawn task.
tasks_json = self.m.swarming.spawn_tasks(tasks=[task])
# Collect results.
results = self.m.swarming.collect(
tasks_json=self.m.json.input(tasks_json))
assert len(results) == 1, 'len(%s) != 1' % repr(results)
result = results[0]
symbolizer_output = self.m.path['cleanup'].join('symbolizer-output.json')
self.analyze_collect_result(
'task results',
result=result,
build_dir=build.fuchsia_build_dir,
symbolizer_output=symbolizer_output,
)
with self.m.context(infra_steps=True):
# result.outputs contains the file outputs produced by the Swarming task,
# returned via isolate. It's a mapping of the 'name' of the output,
# represented as its relative path within the isolated it was returned in,
# to a Path object pointing to its location on the local disk. For each of
# the above tasks, there should be exactly one output.
assert len(result.outputs) == 1, 'len(%s) != 1' % repr(result.outputs)
archive_name = result.outputs.keys()[0]
test_results_dir = self.results_dir_on_host.join('target', result.id)
test_results_map = self._extract_test_results(
device_type=device_type,
archive_path=result.outputs[archive_name],
# Write test results to a subdirectory of |results_dir_on_host|
# so as not to collide with host test results.
leak_to=test_results_dir,
)
return FuchsiaTestResults(
name='all',
build_dir=build.fuchsia_build_dir,
results_dir=test_results_dir,
zircon_kernel_log=result.output,
outputs=test_results_map,
api=self.m,
symbolizer_output=symbolizer_output,
)
# TODO(mknyszek): Rename to test and delete test when this is stable.
def test_in_shards(self, test_pool, build, environment_label='',
timeout_secs=40 * 60):
"""Tests a Fuchsia build by sharding.
Expects the build and artifacts to be at the same place they were at
the end of the build.
Args:
test_pool (str): The Swarming pool to schedule test tasks in.
build (FuchsiaBuildResults): The Fuchsia build to test.
environment_label (str): test environment label on which the testsharder
will key.
timeout_secs (int): The amount of seconds to wait for the tests to
execute before giving up.
Returns:
A list of FuchsiaTestResults representing the completed test tasks that
were not subject to an infra failure.
"""
# Run the testsharder to collect test specifications and shard them.
self.m.testsharder.ensure_testsharder()
shards = self.m.testsharder.execute(
'create test shards',
fuchsia_build_dir=build.fuchsia_build_dir,
label=environment_label,
)
self.m.swarming.ensure_swarming(version='latest')
self.m.isolated.ensure_isolated(version='latest')
# Generate Swarming task requests.
task_requests = []
shard_name_to_device_type = {}
for shard in shards:
with self.m.step.nest('shard %s' % shard.name):
shard_name_to_device_type[shard.name] = shard.device_type
# Produce runtests file for shard.
test_locations = []
for test in shard.tests:
test_locations.append(test.location)
runtests_file = self.m.path['cleanup'].join('tests-%s' % shard.name)
self.m.file.write_text(
name='write test list',
dest=runtests_file,
text_data='\n'.join(test_locations) + '\n',
)
self.m.step.active_result.presentation.logs['tests-%s' % shard.name] = test_locations
# Produce runcmds script for shard.
runtests_file_bootfs_path = 'infra/shard.run'
runcmds_path = self.m.path['cleanup'].join('runcmds-%s' % shard.name)
self._create_runcmds_script(
device_type=shard.device_type,
test_cmds=[
'runtests -o %s -f /boot/%s' % (
self.results_dir_on_target,
runtests_file_bootfs_path,
)
],
output_path=runcmds_path,
)
# Create new zbi image for shard.
shard_zbi_path = build.fuchsia_build_dir.join('fuchsia-%s.zbi' % shard.name)
self.m.zbi.copy_and_extend(
step_name='create zbi',
# TODO(IN-655): Add support for using the netboot image in non-paving
# cases.
input_image=build.images['zircon-a'],
output_image=shard_zbi_path,
manifest={
RUNCMDS_BOOTFS_PATH: runcmds_path,
runtests_file_bootfs_path: runtests_file,
},
)
if shard.device_type == 'QEMU':
task_requests.append(self._construct_qemu_task_request(
task_name=shard.name,
zbi_path=shard_zbi_path,
test_pool=test_pool,
build=build,
timeout_secs=timeout_secs,
# TODO(IN-654): Add support for external_network and secret_bytes.
external_network=False,
secret_bytes='',
))
else:
task_requests.append(self._construct_device_task_request(
task_name=shard.name,
test_pool=test_pool,
device_type=shard.device_type,
zbi_path=shard_zbi_path,
build=build,
timeout_secs=timeout_secs,
# TODO(IN-655): Add support for non-paving tests.
pave=True,
))
with self.m.context(infra_steps=True):
# Spawn tasks.
tasks_json = self.m.swarming.spawn_tasks(tasks=task_requests)
# Collect results.
results = self.m.swarming.collect(
tasks_json=self.m.json.input(tasks_json))
# Iterate over all task results, check them, and collect test results.
fuchsia_test_results = []
symbolizer_output = self.m.path['cleanup'].join('symbolizer-output.json')
with self.m.step.defer_results():
for result in results:
with self.m.context(infra_steps=True):
# Figure out what happened to the swarming tasks.
self.analyze_collect_result(
step_name='%s task results' % result.name,
result=result,
build_dir=build.fuchsia_build_dir,
symbolizer_output=symbolizer_output,
)
# Extract results if the task was not subject to an infra failure;
# otherwise, a step failure will raised on exiting the defer_results()
# scope.
if result.state == self.m.swarming.TaskState.SUCCESS:
# There should only be one archive.
assert len(result.outputs) == 1, 'len(%s) != 1' % repr(result.outputs)
archive_name = result.outputs.keys()[0]
results_dir = self.results_dir_on_host.join(result.id)
test_results_map = self._extract_test_results(
shard_name=result.name,
device_type=shard_name_to_device_type[result.name],
archive_path=result.outputs[archive_name],
# Write test results to the a subdirectory of |results_dir_on_host|
# so as not to collide with host test results.
leak_to=results_dir,
)
# Analyze results if extraction was successful; otherwise, a step
# failure will raised on exiting the defer_results() scope.
if test_results_map.is_ok:
with self.m.context(infra_steps=False):
test_results = FuchsiaTestResults(
name=result.name,
build_dir=build.fuchsia_build_dir,
results_dir=results_dir,
zircon_kernel_log=result.output,
outputs=test_results_map.get_result(),
api=self.m,
symbolizer_output=symbolizer_output,
)
fuchsia_test_results.append(test_results)
return fuchsia_test_results
def analyze_collect_result(self, step_name, result, build_dir,
symbolizer_output=None):
"""Analyzes a swarming.CollectResult and reports results as a step.
Args:
step_name (str): The display name of the step for this analysis.
result (swarming.CollectResult): The swarming collection result to analyze.
build_dir (Path): A path to the build directory for symbolization artifacts.
symbolizer_output (Path|None): A path to a file to write symbolized output.
Raises:
A StepFailure if a kernel panic is detected, or if the tests timed out.
An InfraFailure if the swarming task failed for a different reason.
"""
if result.state == self.m.swarming.TaskState.RPC_FAILURE:
raise self.m.step.InfraFailure('Failed to collect: %s' % result.output)
elif result.state == self.m.swarming.TaskState.NO_RESOURCE:
raise self.m.step.InfraFailure('Found no bots to run this task')
elif result.state == self.m.swarming.TaskState.EXPIRED:
raise self.m.step.InfraFailure('Timed out waiting for a bot to run on')
elif result.state == self.m.swarming.TaskState.BOT_DIED:
raise self.m.step.InfraFailure('The bot running this task died')
elif result.state == self.m.swarming.TaskState.CANCELED:
raise self.m.step.InfraFailure('The task was canceled before it could run')
elif result.state == self.m.swarming.TaskState.KILLED:
raise self.m.step.InfraFailure('The task was killed mid-execution')
with self.m.step.nest(step_name) as step_result:
if result.output:
if symbolizer_output:
# Always symbolize the result output if present in this case.
self._symbolize(build_dir, result.output, symbolizer_output)
kernel_output_lines = result.output.split('\n')
step_result.presentation.logs['kernel log'] = kernel_output_lines
# A kernel panic may be present in the logs even if the task timed out, so
# check for that first.
if 'KERNEL PANIC' in result.output:
step_result.presentation.step_text = 'kernel panic'
step_result.presentation.status = self.m.step.FAILURE
raise self.m.step.StepFailure(
'Found kernel panic. See symbolized output for details.')
elif result.state == self.m.swarming.TaskState.TIMED_OUT:
# If we have a timeout with a successful collect, then this must be an
# io_timeout failure, since task timeout > collect timeout.
step_result.presentation.step_text = 'i/o timeout'
step_result.presentation.status = self.m.step.FAILURE
failure_lines = [
'I/O timed out. Last 10 lines of kernel output:',
] + kernel_output_lines[-10:]
raise self.m.step.StepTimeout('\n'.join(failure_lines),
'%s seconds' % TEST_IO_TIMEOUT_SECS)
elif result.state == self.m.swarming.TaskState.TASK_FAILURE:
step_result.presentation.status = self.m.step.EXCEPTION
raise self.m.step.InfraFailure(
'Swarming task failed:\n%s' % result.output)
def _analyze_test_results(self, test_results):
"""Analyzes test results represented by FuchsiaTestResults objects
Logs individual test results in separate steps.
Args:
test_results (FuchsiaTestResults): Fuchsia test result object
"""
assert test_results.summary
with self.m.step.nest('%s test results' % test_results.name):
# Log the summary file's contents.
raw_summary_log = test_results.raw_summary.split('\n')
self.m.step.active_result.presentation.logs[
'summary.json'] = raw_summary_log
# Log the contents of each output file mentioned in the summary.
# Note this assumes the outputs are all valid unicode (See IN-656).
for output_name, output_path in test_results.summary.get('outputs',
{}).iteritems():
output_str = test_results.outputs[output_path]
self.m.step.active_result.presentation.logs[output_name] = (
output_str.split('\n'))
root_dir = str(self.m.path['start_dir'])
def report_test_result(test, output, passed):
# For host paths, replace the Fuchsia root with '//', the standard
# shorthand used in GN and documentation.
if test.startswith(root_dir):
test = '//%s' % os.path.relpath(test, root_dir)
step_result = self.m.step(test, None)
step_result.presentation.logs['stdio'] = output.split('\n')
if not passed:
step_result.presentation.status = self.m.step.FAILURE
for test, output in test_results.failed_test_outputs.iteritems():
report_test_result(test, output, passed=False)
with self.m.step.nest('all passed tests'):
for test, output in test_results.passed_test_outputs.iteritems():
report_test_result(test, output, passed=True)
def _tar_fuchsia_packages(self, build_results):
"""Collects Fuchsia packages generated by the build into a tarball.
Args:
build_results (FuchsiaBuildResults): The Fuchsia build results to get
artifacts from.
Returns:
A Path to a tarball containing Fuchsia packages.
"""
# Begin creating Fuchsia packages archive.
self.m.tar.ensure_tar()
archive = self.m.tar.create(
self.m.path['cleanup'].join('packages.tar.gz'), compression='gzip')
# Add targets and blobs under 'targets' and 'blobs'. These directories
# together make up complete Fuchsia packages which may be pushed into the
# system.
amber_repo_dir = build_results.fuchsia_build_dir.join(
'amber-files', 'repository')
archive.add(amber_repo_dir.join('targets'), directory=amber_repo_dir)
archive.add(amber_repo_dir.join('blobs'), directory=amber_repo_dir)
host_build_dir = build_results.fuchsia_build_dir.join('host_x64')
archive.add(
host_build_dir.join('pm'), directory=build_results.fuchsia_build_dir)
# Add the keys used to sign the OTA metadata.
# TODO(jmatt): This is a near-term solution for shuffling keys around, we'll
# do something better in the future.
# TODO(dbort): Get the source root from a FuchsiaCheckoutResults instead
# of hard-coding. See if we can stuff the FuchsiaCheckoutResults in the
# FuchsiaBuildResults; maybe even move the api.build() method onto the
# checkout. That may let us remove most 'start_dir' hard-coding from this
# module.
root_dir = self.m.path['start_dir']
amber_src_dir = root_dir.join('garnet', 'go', 'src', 'amber')
archive.add(amber_src_dir.join('keys'), amber_src_dir)
# Tar the files.
archive.tar('tar fuchsia packages')
# Return a Path to the tarball.
return archive.path
def _tar_breakpad_symbols(self, symbol_files, build_dir):
"""Collects Breakpad symbol files generated by the build into a tarball.
These are necessary to enable crash reporting.
Args:
symbol_files (List(string)): The list of absolute paths to symbol files.
build_dir (Path): The build directory, which must be a parent path
of the paths in $symbol_files.
Returns:
A Path to a tarball containing symbol files.
"""
# Create the archive.
self.m.tar.ensure_tar()
archive = self.m.tar.create(
self.m.path['cleanup'].join('breakpad_symbols.tar.gz'),
compression='gzip')
# Add the symbol files.
for sf in symbol_files:
archive.add(sf, build_dir)
# Tar the files.
archive.tar('tar breakpad symbols')
# Return a path to the tarball.
return archive.path
def _upload_file_to_gcs(self, path, bucket, hash=True):
"""Uploads a file to a GCS bucket, using the appropriate naming scheme.
Args:
path (Path): A path to the file to upload.
bucket (str): The name of the GCS bucket to upload to.
Returns:
The upload step.
"""
self.m.gsutil.ensure_gsutil()
# The destination path is based on the buildbucket ID and the basename
# of the local file.
if not self.m.buildbucket.build_id: # pragma: no cover
raise self.m.step.StepFailure('buildbucket.build_id is not set')
basename = self.m.path.basename(path)
dst = 'builds/%s/%s' % (self.m.buildbucket.build_id, basename)
return self.m.gsutil.upload(
bucket=bucket,
src=path,
dst=dst,
link_name=basename,
name='upload %s to %s' % (basename, bucket))
def _upload_build_results(self,
build_results,
gcs_bucket,
include_breakpad_symbols=False,
include_symbol_archive=False):
"""Uploads artifacts from the build to Google Cloud Storage.
More specifically, provided archive_gcs_bucket is set, this method uploads
multiple sets of artifacts:
* Images and tools necessary to boot Fuchsia
* Package artifacts
* GN and Ninja tracing data
* Bloaty McBloatface data
* Optionally, symbols for the Fuchsia binaries
Args:
build_results (FuchsiaBuildResults): The Fuchsia build results to get
artifacts from.
gcs_bucket (str): GCS bucket name to upload build results to.
include_breakpad_symbols (bool): Whether to include breakpad symbols.
include_symbol_archive (bool): Whether to include the symbolized binary archive.
"""
assert gcs_bucket
self.m.gsutil.ensure_gsutil()
if 'archive' in build_results.images:
self._upload_file_to_gcs(build_results.images['archive'], gcs_bucket)
# Upload fuchsia packages.
if build_results.includes_package_archive:
archive = self._tar_fuchsia_packages(build_results)
self._upload_file_to_gcs(archive, gcs_bucket)
# Upload build metrics.
self._upload_tracing_data(build_results, gcs_bucket)
self._run_bloaty(build_results, gcs_bucket)
# Upload breakpad symbol files.
if include_breakpad_symbols:
symbol_files = self._get_breakpad_symbol_files(build_results)
if symbol_files:
archive = self._tar_breakpad_symbols(symbol_files,
build_results.fuchsia_build_dir)
self._upload_file_to_gcs(archive, gcs_bucket)
# Upload archive of symbolized binaries.
if include_symbol_archive:
archive = build_results.fuchsia_build_dir.join('symbol-archive.tgz')
self._upload_file_to_gcs(archive, gcs_bucket)
def _get_breakpad_symbol_files(self, build_results):
"""Extracts the list of generated symbol files.
These symbol files are generated from the build's unstripped binaries and
are eventually uploaded to the Crash servers to enable crash reporting for
release builds.
Args:
build_results (FuchsiaBuildResults): The build results.
Returns:
A list of absolute Paths to each symbol file. All paths in the returned
list are subpaths of build_results.fuchsia_build_dir.
"""
# Read the summary generated by //tools/cmd/dump_breakpad_symbols. The
# summary is a JSON object whose keys are absolute paths to binaries and
# values are absolute paths to the generated breakpad symbol files for
# those binaries.
binary_to_symbol_file = self.m.json.read(
name='read symbol file summary',
path=build_results.fuchsia_build_dir.join('breakpad_symbols',
'symbol_file_mappings.json'),
step_test_data=lambda:self.m.json.test_api.output({
'/path/to/bin': str(build_results.fuchsia_build_dir.join('bin.sym')),
}),
).json.output
return list(
map(self.m.path.abs_to_path, binary_to_symbol_file.itervalues()))
def _process_coverage(self, test_results, gcs_bucket, symbolizer_output):
self.m.gsutil.ensure_gsutil()
with self.m.context(infra_steps=True):
cipd_dir = self.m.path['start_dir'].join('cipd')
pkgs = self.m.cipd.EnsureFile()
pkgs.add_package('fuchsia/tools/covargs/${platform}', 'latest')
self.m.cipd.ensure(cipd_dir, pkgs)
host_platform = HOST_PLATFORMS[self.m.platform.arch][self.m.platform.bits]
downloads_dir = self.m.path['start_dir'].join('zircon', 'prebuilt',
'downloads')
clang_dir = downloads_dir.join('clang', 'bin')
output_dir = self.m.path['cleanup'].join('coverage')
self.m.step(
'covargs',
[
cipd_dir.join('covargs'),
'-summary',
test_results.results_dir.join('summary.json'),
# TODO: this is already in build_results, maybe we should pass it
# to this method rather than constructing it manually.
'-ids',
test_results.build_dir.join('ids.txt'),
'-symbolize-dump',
symbolizer_output,
'-output-dir',
output_dir,
'-llvm-profdata',
clang_dir.join('llvm-profdata'),
'-llvm-cov',
clang_dir.join('llvm-cov'),
])
# TODO: move this into gsutil module/deduplicate this with other GCS logic
dst = 'builds/%s/coverage' % self.m.buildbucket.build_id
step_result = self.m.gsutil(
'cp',
'-r',
'-z',
'html',
'-a',
'public-read',
output_dir,
'gs://%s/%s' % (gcs_bucket, dst),
name='upload coverage',
multithreaded=True)
step_result.presentation.links['index.html'] = self.m.gsutil._http_url(
gcs_bucket, self.m.gsutil.join(dst, 'index.html'),
True)
def _upload_tracing_data(self, build_results, gcs_bucket):
"""Uploads GN and ninja tracing results for this build to GCS"""
# Only upload if the bucket is specified.
gn_data = self._extract_gn_tracing_data(build_results)
ninja_data = self._extract_ninja_tracing_data(build_results)
self._upload_file_to_gcs(gn_data, gcs_bucket, hash=False)
self._upload_file_to_gcs(ninja_data, gcs_bucket, hash=False)
def _extract_gn_tracing_data(self, build_results):
"""Extracts the tracing data from this GN run.
Args:
build_results (FuchsiaBuildResults): The build results.
Returns:
A Path to the file containing the gn tracing data in Chromium's
about:tracing html format.
"""
return self._trace2html('gn trace2html',
self.m.path['cleanup'].join('gn_trace.json'),
self.m.path['cleanup'].join('gn_trace.html'))
def _extract_ninja_tracing_data(self, build_results):
"""Extracts the tracing data from the .ninja_log.
Args:
build_results (FuchsiaBuildResults): The build results.
Returns:
A Path to the file containing the gn tracing data in Chromium's
about:tracing html format.
"""
with self.m.step.nest('ensure ninjatrace'):
with self.m.context(infra_steps=True):
cipd_dir = self.m.path['start_dir'].join('cipd')
pkgs = self.m.cipd.EnsureFile()
pkgs.add_package('fuchsia/tools/ninjatrace/${platform}', 'latest')
self.m.cipd.ensure(cipd_dir, pkgs)
trace = self.m.path['cleanup'].join('ninja_trace.json')
html = self.m.path['cleanup'].join('ninja_trace.html')
self.m.step(
'ninja tracing', [
self.m.path['start_dir'].join('cipd').join('ninjatrace'),
'-filename',
build_results.fuchsia_build_dir.join('.ninja_log'),
'-trace-json',
trace,
],
stdout=self.m.raw_io.output(leak_to=trace))
return self._trace2html('ninja trace2html', trace, html)
def _trace2html(self, name, trace, html):
"""Converts an about:tracing file to HTML using the trace2html tool"""
# Catapult is imported in manifest/garnet, so we abort if it wasn't included
# in this checkout.
# if self.m.path['third_party'].join('catapult') not in self.m.path:
# return
self.m.python(
name=name,
script=self.m.path['start_dir'].join('third_party', 'catapult',
'tracing', 'bin', 'trace2html'),
args=['--output', html, trace])
return html
def _run_bloaty(self, build_results, gcs_bucket):
"""Runs bloaty on the specified build results.
The data is generated by running Bloaty McBloatface on the binaries in the
build results. If this is called more than once, it will only run once.
This function requires the build_metrics_gcs_bucket property to have been
set.
Returns:
A Path to the file containing the resulting bloaty data.
"""
assert gcs_bucket
with self.m.step.nest('ensure bloaty'):
with self.m.context(infra_steps=True):
cipd_dir = self.m.path['start_dir'].join('cipd')
pkgs = self.m.cipd.EnsureFile()
pkgs.add_package('fuchsia/tools/bloatalyzer/${platform}', 'latest')
pkgs.add_package('fuchsia/third_party/bloaty/${platform}', 'latest')
self.m.cipd.ensure(cipd_dir, pkgs)
bloaty_file = self.m.path['cleanup'].join('bloaty.html')
self.m.step(
'bloaty',
[
self.m.path['start_dir'].join('cipd', 'bloatalyzer'),
'-bloaty',
self.m.path['start_dir'].join('cipd', 'bloaty'),
'-input',
build_results.ids,
'-output',
bloaty_file,
# We can't include all targets because the page won't load, so limit the output.
'-top-files',
'50',
'-top-syms',
'50',
'-format',
'html',
# Limit the number of jobs so that we don't overwhelm the bot.
'-jobs',
str(min(self.m.platform.cpu_count, 32)),
])
self._upload_file_to_gcs(bloaty_file, gcs_bucket, hash=False)
def _gn_args(self,
goma_dir,
target,
is_debug=False,
board=None,
build_type=None,
packages=(),
product=None,
variants=()):
"""Creates Fuchsia-specific GN command line arguments.
GN's declare_args() macro allows the GN client to define their own command line flags.
This adapter converts Fuchia's declared args to a list of strings for GN.
"""
args = [
'target_cpu="%s"' % target,
'use_goma=true',
'goma_dir="%s"' % goma_dir,
'is_debug=%s' % ('true' if is_debug else 'false'),
]
if board:
args.append('import("//%s") ' % board)
if packages:
fuchsia_packages_format = 'fuchsia_packages=[%s]'
# if board is set, append to fuchsia_packages.
# board set fuchsia_packages, we don't want to overwrite.
if board:
fuchsia_packages_format = 'fuchsia_packages+=[%s]'
args.append(
fuchsia_packages_format % ','.join('"%s"' % pkg for pkg in packages))
if product:
args.append('import("//%s") ' % product)
args.extend({
'lto': [
'use_lto=true',
'use_thinlto=false',
],
'thinlto': [
'use_lto=true',
'use_thinlto=true',
'thinlto_cache_dir="%s"' % self.m.path['cache'].join('thinlto'),
],
}.get(build_type, []))
if variants:
args.append(
'select_variant=[%s]' % ','.join(['"%s"' % v for v in variants]))
return args