blob: 03836af96f0ece1657d787cd0c86f9681e90f98e [file] [log] [blame]
# Copyright 2017 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.
"""Recipe for building Fuchsia and running tests."""
from contextlib import contextmanager
from recipe_engine.config import Enum, List, ReturnSchema, Single
from recipe_engine.recipe_api import Property
import hashlib
import os
import re
DEPS = [
'infra/cipd',
'infra/goma',
'infra/gsutil',
'infra/hash',
'infra/isolate',
'infra/jiri',
'infra/qemu',
'infra/swarming',
'infra/tar',
'recipe_engine/context',
'recipe_engine/json',
'recipe_engine/file',
'recipe_engine/path',
'recipe_engine/platform',
'recipe_engine/properties',
'recipe_engine/raw_io',
'recipe_engine/source_manifest',
'recipe_engine/step',
]
TARGETS = ['arm64', 'x86-64']
TEST_SUMMARY = r'SUMMARY: Ran (\d+) tests: (?P<failed>\d+) failed'
TEST_SHUTDOWN = 'ready for fuchsia shutdown'
TEST_RUNNER_PORT = 8342
PROPERTIES = {
'category': Property(kind=str, help='Build category', default=None),
'patch_gerrit_url': Property(kind=str, help='Gerrit host', default=None),
'patch_project': Property(kind=str, help='Gerrit project', default=None),
'patch_ref': Property(kind=str, help='Gerrit patch ref', default=None),
'patch_storage': Property(kind=str, help='Patch location', default=None),
'patch_repository_url': Property(kind=str, help='URL to a Git repository',
default=None),
'project': Property(kind=str, help='Jiri remote manifest project', default=None),
'manifest': Property(kind=str, help='Jiri manifest to use'),
'remote': Property(kind=str, help='Remote manifest repository'),
'target': Property(kind=Enum(*TARGETS), help='Target to build'),
'build_type': Property(kind=Enum('debug', 'release', 'thinlto', 'lto'),
help='The build type', default='debug'),
'modules': Property(kind=List(basestring), help='Packages to build',
default=['build/gn/default']),
'tests': Property(kind=str,
help='Path to config file listing tests to run, or (when using autorun) command to run tests',
default=None),
'use_autorun': Property(kind=bool,
help='Whether to use autorun for tests',
default=True),
'use_isolate': Property(kind=bool,
help='Whether to run tests on another machine',
default=False),
'goma_dir': Property(kind=str, help='Path to goma', default=None),
'gn_args': Property(kind=List(basestring), help='Extra args to pass to GN',
default=[]),
}
def Checkout(api, patch_project, patch_ref, patch_gerrit_url, project, manifest,
remote):
with api.context(infra_steps=True):
api.jiri.checkout(manifest, remote, project, patch_ref, patch_gerrit_url,
patch_project)
if manifest in ['garnet', 'peridot']:
revision = api.jiri.project([manifest]).json.output[0]['revision']
api.step.active_result.presentation.properties['got_revision'] = revision
if patch_ref:
api.jiri.update(gc=True, rebase_tracked=True, local_manifest=True)
if not api.properties.get('tryjob', False):
snapshot_file = api.path['tmp_base'].join('jiri.snapshot')
api.jiri.snapshot(snapshot_file)
digest = api.hash.sha1('hash snapshot', snapshot_file,
test_data='8ac5404b688b34f2d34d1c8a648413aca30b7a97')
api.gsutil.upload('fuchsia-snapshots', snapshot_file, digest,
link_name='jiri.snapshot',
name='upload jiri.snapshot',
unauthenticated_url=True)
def BuildZircon(api, zircon_project):
build_zircon_cmd = [
api.path['start_dir'].join('scripts', 'build-zircon.sh'),
'-c',
'-H',
'-p', zircon_project,
]
api.step('build zircon', build_zircon_cmd)
def BuildFuchsia(api, build_type, target, gn_target, fuchsia_build_dir,
modules, tests, use_autorun, use_isolate, gn_args):
autorun_path = None
if tests:
if use_autorun or use_isolate:
autorun = {
True: ['msleep 500', tests, 'msleep 15000', 'dm poweroff'],
False: ['msleep 500', tests, 'echo "%s"' % TEST_SHUTDOWN],
}[use_isolate]
autorun_path = api.path['tmp_base'].join('autorun')
api.file.write_text('write autorun', autorun_path, '\n'.join(autorun))
api.step.active_result.presentation.logs['autorun.sh'] = autorun
else:
modules.append('build/gn/boot_test_runner')
goma_env = {}
if api.properties.get('goma_local_cache', False):
goma_env['GOMA_LOCAL_OUTPUT_CACHE_DIR'] = api.path['cache'].join('goma', 'localoutputcache')
with api.step.nest('build fuchsia'):
with api.goma.build_with_goma(env=goma_env):
gen_cmd = [
api.path['start_dir'].join('build', 'gn', 'gen.py'),
'--target_cpu=%s' % gn_target,
'--packages=%s' % ','.join(modules),
]
if autorun_path:
gen_cmd.append('--autorun=%s' % autorun_path)
gen_cmd.append('--goma=%s' % api.goma.goma_dir)
if build_type in ['release', 'lto', 'thinlto']:
gen_cmd.append('--release')
if build_type == 'lto':
gen_cmd.append('--lto=full')
elif build_type == 'thinlto':
gen_cmd.append('--lto=thin')
gn_args.append('thinlto_cache_dir=\"%s\"' %
str(api.path['cache'].join('thinlto')))
for arg in gn_args:
gen_cmd.append('--args')
gen_cmd.append(arg)
api.step('gen', gen_cmd)
ninja_cmd = [
api.path['start_dir'].join('buildtools', 'ninja'),
'-C', fuchsia_build_dir,
]
ninja_cmd.extend(['-j', api.goma.recommended_goma_jobs])
api.step('ninja', ninja_cmd)
def IsolateArtifacts(api, target, zircon_build_dir, fuchsia_build_dir):
zircon_image_name = {
'arm64': 'zircon.elf',
'x86-64': 'zircon.bin',
}[target]
# Copy the images to CWD so that when we later download the artifacts appear
# in CWD. This eliminates having to do any arcane path logic later.
api.file.copy('copy zircon image', zircon_build_dir.join(zircon_image_name), api.path['start_dir'])
api.file.copy('copy fs image', fuchsia_build_dir.join('user.bootfs'), api.path['start_dir'])
# Hack that creates an isolate file suitable for consumption by the client.
#
# TODO(mknyszek): Replace this with new interface once the isolate recipe
# module is updated to use the golang isolated client.
isolate_str = str(api.json.dumps({
'variables': {
'files': [
os.path.relpath(str(api.path['start_dir'].join(zircon_image_name)), str(api.path['start_dir'])),
os.path.relpath(str(api.path['start_dir'].join('user.bootfs')), str(api.path['start_dir'])),
]
}
}))
isolate_path = api.path.join(api.path['start_dir'], 'result.isolate')
api.file.write_text('write isolate', isolate_path, isolate_str.replace('"', '\''))
# Archive, then extract and return digest for isolated.
isolated_path = api.path['tmp_base'].join('result.isolated')
return api.isolate.archive(isolate_path, isolated_path)['result']
def RunTestsInTask(api, target, isolated_hash):
zircon_image_name = {
'arm64': 'zircon.elf',
'x86-64': 'zircon.bin',
}[target]
qemu_arch = {
'arm64': 'aarch64',
'x86-64': 'x86_64',
}[target]
qemu_cmd = [
'./qemu/bin/qemu-system-' + qemu_arch, # Dropped in by CIPD.
'-m', '4096',
'-smp', '4',
'-nographic',
'-machine', {'aarch64': 'virt', 'x86_64': 'q35'}[qemu_arch],
'-kernel', zircon_image_name,
'-serial', 'stdio',
'-monitor', 'none',
'-initrd', 'user.bootfs',
'-enable-kvm', '-cpu', 'host',
]
qemu_cipd_arch = {
'arm64': 'arm64',
'x86-64': 'amd64',
}[target]
with api.context(infra_steps=True):
# Trigger task.
trigger_result = api.swarming.trigger(
'trigger tests',
qemu_cmd,
isolated=isolated_hash,
dump_json=api.path.join(api.path['tmp_base'], 'qemu_test_results.json'),
dimensions={
'pool': 'Fuchsia',
'os': 'Debian',
'cpu': target,
'kvm': '1',
},
io_timeout=60,
cipd_packages=[('qemu', 'fuchsia/qemu/linux-%s' % qemu_cipd_arch, 'latest')],
)
# Collect results. This is not listed as an infra step because we want the
# results to be not infra steps. The collect operation itself is always an
# infra step.
api.swarming.collect('20m', requests_json=api.json.input(trigger_result.json.output))
def RunTestsWithTCP(api, target, fuchsia_build_dir, tests):
zircon_build_dir = {
'arm64': 'build-zircon-qemu-arm64',
'x86-64': 'build-zircon-pc-x86-64',
}[target]
zircon_image_name = {
'arm64': 'zircon.elf',
'x86-64': 'zircon.bin',
}[target]
zircon_image_path = api.path['start_dir'].join(
'out', 'build-zircon', zircon_build_dir, zircon_image_name)
bootfs_path = fuchsia_build_dir.join('user.bootfs')
qemu_arch = {
'arm64': 'aarch64',
'x86-64': 'x86_64',
}[target]
netdev = 'user,id=net0,hostfwd=tcp::%d-:%d' % (
TEST_RUNNER_PORT, TEST_RUNNER_PORT)
qemu = api.qemu.background_run(
qemu_arch,
zircon_image_path,
kvm=True,
memory=4096,
initrd=bootfs_path,
netdev=netdev,
devices=['e1000,netdev=net0'])
with qemu:
run_tests_cmd = [
api.path['start_dir'].join('garnet', 'bin', 'test_runner', 'run_test'),
'--test_file', api.path['start_dir'].join(tests),
'--server', '127.0.0.1',
'--port', str(TEST_RUNNER_PORT),
]
try:
api.step('run tests', run_tests_cmd)
finally:
# Give time for output to get flushed before reading the QEMU log.
# TODO(bgoldman): Capture diagnostic information like FTL_LOG and
# backtraces synchronously.
api.step('sleep', ['sleep', '3'])
symbolize_cmd = [
api.path['start_dir'].join('zircon', 'scripts', 'symbolize'),
'--no-echo',
'--file', 'qemu.stdout',
'--build-dir', fuchsia_build_dir,
]
step_result = api.step('symbolize', symbolize_cmd,
stdout=api.raw_io.output(),
step_test_data=lambda: api.raw_io.test_api.stream_output(''))
lines = step_result.stdout.splitlines()
if lines:
# If symbolize found any backtraces in qemu.stdout, mark the symbolize
# step as failed to indicate that it should be looked at.
step_result.presentation.logs['symbolized backtraces'] = lines
step_result.presentation.status = api.step.FAILURE
def RunTestsWithAutorun(api, target, fuchsia_build_dir):
zircon_build_dir = {
'arm64': 'build-zircon-qemu-arm64',
'x86-64': 'build-zircon-pc-x86-64',
}[target]
zircon_image_name = {
'arm64': 'zircon.elf',
'x86-64': 'zircon.bin',
}[target]
zircon_image_path = api.path['start_dir'].join(
'out', 'build-zircon', zircon_build_dir, zircon_image_name)
bootfs_path = fuchsia_build_dir.join('user.bootfs')
qemu_arch = {
'arm64': 'aarch64',
'x86-64': 'x86_64',
}[target]
run_tests_result = None
failure_reason = None
try:
run_tests_result = api.qemu.run(
'run tests',
qemu_arch,
zircon_image_path,
kvm=True,
memory=4096,
initrd=bootfs_path,
shutdown_pattern=TEST_SHUTDOWN)
except api.step.StepFailure as error:
run_tests_result = error.result
if error.retcode == 2:
failure_reason = 'Tests timed out'
else:
raise api.step.InfraFailure('QEMU failure')
qemu_log = run_tests_result.stdout
run_tests_result.presentation.logs['qemu log'] = qemu_log.splitlines()
if failure_reason is None:
m = re.search(TEST_SUMMARY, qemu_log)
if not m:
# This is an infrastructure failure because the TEST_SHUTDOWN string
# should have been triggered to get to the this point, which means the
# runtests command completed. runtests is supposed to output a string
# matching TEST_SUMMARY.
run_tests_result.presentation.status = api.step.EXCEPTION
failure_reason = 'Test output missing'
elif int(m.group('failed')) > 0:
run_tests_result.presentation.status = api.step.FAILURE
failure_reason = m.group(0)
if failure_reason is not None:
symbolize_cmd = [
api.path['start_dir'].join('zircon', 'scripts', 'symbolize'),
'--no-echo',
'--build-dir', fuchsia_build_dir,
]
symbolize_result = api.step('symbolize', symbolize_cmd,
stdin=api.raw_io.input(data=qemu_log),
stdout=api.raw_io.output(),
step_test_data=lambda: api.raw_io.test_api.stream_output(''))
symbolized_lines = symbolize_result.stdout.splitlines()
if symbolized_lines:
symbolize_result.presentation.logs['symbolized backtraces'] = symbolized_lines
symbolize_result.presentation.status = api.step.FAILURE
raise api.step.StepFailure(failure_reason)
def RunSteps(api, category, patch_gerrit_url, patch_project, patch_ref,
patch_storage, patch_repository_url, project, manifest, remote,
target, build_type, modules, tests, use_autorun, use_isolate,
goma_dir, gn_args):
# Tests are too slow on arm64.
if target == 'arm64':
tests = None
gn_target = {'arm64': 'aarch64', 'x86-64': 'x86-64'}[target]
fuchsia_out_dir = api.path['start_dir'].join('out')
if build_type in ['release', 'lto', 'thinlto']:
build_dir = 'release'
else:
build_dir = 'debug'
fuchsia_build_dir = fuchsia_out_dir.join('%s-%s' % (build_dir, gn_target))
zircon_project = {
'arm64': 'zircon-qemu-arm64',
'x86-64': 'zircon-pc-x86-64'
}[target]
zircon_build_dir = fuchsia_out_dir.join('build-zircon', 'build-%s' % zircon_project)
if goma_dir:
api.goma.set_goma_dir(goma_dir)
api.jiri.ensure_jiri()
api.gsutil.ensure_gsutil()
api.goma.ensure_goma()
if tests:
if use_isolate:
api.swarming.ensure_swarming(version='latest')
api.isolate.ensure_isolate(version='latest')
else:
api.qemu.ensure_qemu()
Checkout(api, patch_project, patch_ref, patch_gerrit_url, project, manifest,
remote)
BuildZircon(api, zircon_project)
BuildFuchsia(api, build_type, target, gn_target, fuchsia_build_dir,
modules, tests, use_autorun, use_isolate, gn_args)
if tests:
if use_isolate:
isolated = IsolateArtifacts(api, target, zircon_build_dir, fuchsia_build_dir)
RunTestsInTask(api, target, isolated)
elif use_autorun:
RunTestsWithAutorun(api, target, fuchsia_build_dir)
else:
RunTestsWithTCP(api, target, fuchsia_build_dir, tests)
def GenTests(api):
# Test cases for running Fuchsia tests over TCP.
yield api.test('tests') + api.properties(
manifest='fuchsia',
remote='https://fuchsia.googlesource.com/manifest',
target='x86-64',
tests='tests.json',
use_autorun=False,
)
yield api.test('failed_tests') + api.properties(
manifest='fuchsia',
remote='https://fuchsia.googlesource.com/manifest',
target='x86-64',
tests='tests.json',
use_autorun=False,
) + api.step_data('run tests', retcode=1)
yield api.test('backtrace') + api.properties(
manifest='fuchsia',
remote='https://fuchsia.googlesource.com/manifest',
target='x86-64',
tests='tests.json',
use_autorun=False,
) + api.step_data('run tests', retcode=1,
) + api.step_data('symbolize', api.raw_io.stream_output('bt1\nbt2\n'))
# Test cases for running Fuchsia tests with autorun.
yield api.test('autorun_tests') + api.properties(
manifest='fuchsia',
remote='https://fuchsia.googlesource.com/manifest',
target='x86-64',
tests='runtests',
) + api.step_data('run tests', api.raw_io.stream_output('SUMMARY: Ran 2 tests: 0 failed\n' + TEST_SHUTDOWN))
yield api.test('autorun_failed_qemu') + api.properties(
manifest='fuchsia',
remote='https://fuchsia.googlesource.com/manifest',
target='x86-64',
tests='runtests',
) + api.step_data('run tests', retcode=1)
yield api.test('autorun_no_results') + api.properties(
manifest='fuchsia',
remote='https://fuchsia.googlesource.com/manifest',
target='x86-64',
tests='runtests',
) + api.step_data('run tests', api.raw_io.stream_output(TEST_SHUTDOWN))
yield api.test('autorun_tests_timeout') + api.properties(
manifest='fuchsia',
remote='https://fuchsia.googlesource.com/manifest',
target='x86-64',
tests='runtests',
) + api.step_data('run tests', retcode=2)
yield api.test('autorun_failed_tests') + api.properties(
manifest='fuchsia',
remote='https://fuchsia.googlesource.com/manifest',
target='x86-64',
tests='runtests',
) + api.step_data('run tests', api.raw_io.stream_output('SUMMARY: Ran 2 tests: 1 failed\n' + TEST_SHUTDOWN))
yield api.test('autorun_backtrace') + api.properties(
manifest='fuchsia',
remote='https://fuchsia.googlesource.com/manifest',
target='x86-64',
tests='runtests',
) + api.step_data('run tests', api.raw_io.stream_output('SUMMARY: Ran 2 tests: 1 failed'),
) + api.step_data('symbolize', api.raw_io.stream_output('bt1\nbt2\n'))
# Test cases for running Fuchsia tests as a swarming task.
yield api.test('isolated_tests') + api.properties(
manifest='fuchsia',
remote='https://fuchsia.googlesource.com/manifest',
target='x86-64',
tests='runtests',
use_isolate=True,
) + api.step_data('collect', api.swarming.collect_result())
yield api.test('isolated_tests_task_failure') + api.properties(
manifest='fuchsia',
remote='https://fuchsia.googlesource.com/manifest',
target='x86-64',
tests='runtests',
use_isolate=True,
) + api.step_data('collect', api.swarming.collect_result(task_failure=True))
yield api.test('isolated_tests_infra_failure') + api.properties(
manifest='fuchsia',
remote='https://fuchsia.googlesource.com/manifest',
target='x86-64',
tests='runtests',
use_isolate=True,
) + api.step_data('collect', api.swarming.collect_result(infra_failure=True))
# Test cases for skipping Fuchsia tests.
yield api.test('default') + api.properties(
manifest='fuchsia',
remote='https://fuchsia.googlesource.com/manifest',
target='x86-64',
autorun=False,
)
yield api.test('garnet') + api.properties(
project='garnet',
manifest='manifest/garnet',
remote='https://fuchsia.googlesource.com/garnet',
target='x86-64',
autorun=False,
)
yield api.test('peridot') + api.properties(
manifest='peridot',
remote='https://fuchsia.googlesource.com/manifest',
target='x86-64',
autorun=False,
)
yield api.test('no_goma') + api.properties(
manifest='fuchsia',
remote='https://fuchsia.googlesource.com/manifest',
target='x86-64',
goma_dir='/path/to/goma',
autorun=False,
)
yield api.test('goma_local_cache') + api.properties(
manifest='fuchsia',
remote='https://fuchsia.googlesource.com/manifest',
target='x86-64',
goma_local_cache=True,
autorun=False,
)
yield api.test('arm64_skip_tests') + api.properties(
manifest='fuchsia',
remote='https://fuchsia.googlesource.com/manifest',
target='arm64',
tests='tests.json',
autorun=False,
)
yield api.test('release') + api.properties(
manifest='fuchsia',
remote='https://fuchsia.googlesource.com/manifest',
target='x86-64',
build_type='release',
autorun=False,
)
yield api.test('lto') + api.properties(
manifest='fuchsia',
remote='https://fuchsia.googlesource.com/manifest',
target='x86-64',
build_type='lto',
autorun=False,
)
yield api.test('thinlto') + api.properties(
manifest='fuchsia',
remote='https://fuchsia.googlesource.com/manifest',
target='x86-64',
build_type='thinlto',
autorun=False,
)
yield api.test('cq') + api.properties.tryserver(
gerrit_project='fuchsia',
patch_gerrit_url='fuchsia-review.googlesource.com',
manifest='fuchsia',
remote='https://fuchsia.googlesource.com/manifest',
target='x86-64',
autorun=False,
tryjob=True,
)
yield api.test('gn_args') + api.properties.tryserver(
gerrit_project='fuchsia',
patch_gerrit_url='fuchsia-review.googlesource.com',
manifest='fuchsia',
remote='https://fuchsia.googlesource.com/manifest',
target='x86-64',
tryjob=True,
autorun=False,
gn_args=['super_arg=false', 'less_super_arg=true'],
)
yield api.test('manifest') + api.properties.tryserver(
gerrit_project='fuchsia',
patch_project='manifest',
patch_gerrit_url='fuchsia-review.googlesource.com',
manifest='fuchsia',
remote='https://fuchsia.googlesource.com/manifest',
target='x86-64',
autorun=False,
tryjob=True,
)