3e2ffd74b5
Bug: skia:6704 Change-Id: Id3cefa643316e5676d2038a75240e16d929d6d63 Reviewed-on: https://skia-review.googlesource.com/20146 Commit-Queue: Eric Boren <borenet@google.com> Reviewed-by: Ravi Mistry <rmistry@google.com>
255 lines
9.4 KiB
Python
255 lines
9.4 KiB
Python
# Copyright 2014 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.
|
|
|
|
|
|
# TODO(borenet): This module was copied from build.git and heavily modified to
|
|
# remove dependencies on other modules in build.git. It belongs in a different
|
|
# repo. Remove this once it has been moved.
|
|
|
|
|
|
import itertools
|
|
from recipe_engine import recipe_api
|
|
|
|
|
|
class IsolateApi(recipe_api.RecipeApi):
|
|
"""APIs for interacting with isolates."""
|
|
|
|
def __init__(self, **kwargs):
|
|
super(IsolateApi, self).__init__(**kwargs)
|
|
self._isolate_server = 'https://isolateserver.appspot.com'
|
|
self._isolated_tests = {}
|
|
|
|
@property
|
|
def isolate_server(self):
|
|
"""URL of Isolate server to use, default is a production one."""
|
|
return self._isolate_server
|
|
|
|
@isolate_server.setter
|
|
def isolate_server(self, value):
|
|
"""Changes URL of Isolate server to use."""
|
|
self._isolate_server = value
|
|
|
|
def clean_isolated_files(self, build_dir):
|
|
"""Cleans out all *.isolated files from the build directory in
|
|
preparation for the compile. Needed in order to ensure isolates
|
|
are rebuilt properly because their dependencies are currently not
|
|
completely described to gyp.
|
|
"""
|
|
self.m.python(
|
|
'clean isolated files',
|
|
self.resource('find_isolated_tests.py'),
|
|
[
|
|
'--build-dir', build_dir,
|
|
'--clean-isolated-files'
|
|
])
|
|
|
|
def find_isolated_tests(self, build_dir, targets=None, **kwargs):
|
|
"""Returns a step which finds all *.isolated files in a build directory.
|
|
|
|
Useful only with 'archive' isolation mode.
|
|
In 'prepare' mode use 'isolate_tests' instead.
|
|
|
|
Assigns the dict {target name -> *.isolated file hash} to the swarm_hashes
|
|
build property. This implies this step can currently only be run once
|
|
per recipe.
|
|
|
|
If |targets| is None, the step will use all *.isolated files it finds.
|
|
Otherwise, it will verify that all |targets| are found and will use only
|
|
them. If some expected targets are missing, will abort the build.
|
|
"""
|
|
step_result = self.m.python(
|
|
'find isolated tests',
|
|
self.resource('find_isolated_tests.py'),
|
|
[
|
|
'--build-dir', build_dir,
|
|
'--output-json', self.m.json.output(),
|
|
],
|
|
step_test_data=lambda: (self.test_api.output_json(targets)),
|
|
**kwargs)
|
|
|
|
assert isinstance(step_result.json.output, dict)
|
|
self._isolated_tests = step_result.json.output
|
|
if targets is not None and (
|
|
step_result.presentation.status != self.m.step.FAILURE):
|
|
found = set(step_result.json.output)
|
|
expected = set(targets)
|
|
if found >= expected: # pragma: no cover
|
|
# Limit result only to |expected|.
|
|
self._isolated_tests = {
|
|
target: step_result.json.output[target] for target in expected
|
|
}
|
|
else:
|
|
# Some expected targets are missing? Fail the step.
|
|
step_result.presentation.status = self.m.step.FAILURE
|
|
step_result.presentation.logs['missing.isolates'] = (
|
|
['Failed to find *.isolated files:'] + list(expected - found))
|
|
step_result.presentation.properties['swarm_hashes'] = self._isolated_tests
|
|
# No isolated files found? That looks suspicious, emit warning.
|
|
if (not self._isolated_tests and
|
|
step_result.presentation.status != self.m.step.FAILURE):
|
|
step_result.presentation.status = self.m.step.WARNING
|
|
|
|
def isolate_tests(self, build_dir, targets=None, verbose=False,
|
|
set_swarm_hashes=True, always_use_exparchive=False,
|
|
**kwargs):
|
|
"""Archives prepared tests in |build_dir| to isolate server.
|
|
|
|
src/tools/isolate_driver.py is invoked by ninja during compilation
|
|
to produce *.isolated.gen.json files that describe how to archive tests.
|
|
|
|
This step then uses *.isolated.gen.json files to actually performs the
|
|
archival. By archiving all tests at once it is able to reduce the total
|
|
amount of work. Tests share many common files, and such files are processed
|
|
only once.
|
|
|
|
Assigns the dict {target name -> *.isolated file hash} to the swarm_hashes
|
|
build property (also accessible as 'isolated_tests' property). This implies
|
|
this step can currently only be run once per recipe.
|
|
"""
|
|
# TODO(tansell): Make all steps in this function nested under one overall
|
|
# 'isolate tests' master step.
|
|
|
|
# TODO(vadimsh): Always require |targets| to be passed explicitly. Currently
|
|
# chromium_trybot, blink_trybot and swarming/canary recipes rely on targets
|
|
# autodiscovery. The code path in chromium_trybot that needs it is being
|
|
# deprecated in favor of to *_ng builders, that pass targets explicitly.
|
|
if targets is None:
|
|
# Ninja builds <target>.isolated.gen.json files via isolate_driver.py.
|
|
paths = self.m.file.glob_paths(
|
|
'find isolated targets',
|
|
build_dir,
|
|
'*.isolated.gen.json',
|
|
test_data=[
|
|
'dummy_target_%d.isolated.gen.json' % i
|
|
for i in (1, 2)
|
|
])
|
|
targets = []
|
|
for p in paths:
|
|
name = self.m.path.basename(p)
|
|
assert name.endswith('.isolated.gen.json'), name
|
|
targets.append(name[:-len('.isolated.gen.json')])
|
|
|
|
# No isolated tests found.
|
|
if not targets: # pragma: no cover
|
|
return
|
|
|
|
batch_targets = []
|
|
exparchive_targets = []
|
|
for t in targets:
|
|
if t.endswith('_exparchive') or always_use_exparchive:
|
|
exparchive_targets.append(t)
|
|
else:
|
|
batch_targets.append(t)
|
|
|
|
isolate_steps = []
|
|
try:
|
|
args = [
|
|
self.m.swarming_client.path,
|
|
'exparchive',
|
|
'--dump-json', self.m.json.output(),
|
|
'--isolate-server', self._isolate_server,
|
|
'--eventlog-endpoint', 'prod',
|
|
] + (['--verbose'] if verbose else [])
|
|
|
|
for target in exparchive_targets:
|
|
isolate_steps.append(
|
|
self.m.python(
|
|
'isolate %s' % target,
|
|
self.resource('isolate.py'),
|
|
args + [
|
|
'--isolate', build_dir.join('%s.isolate' % target),
|
|
'--isolated', build_dir.join('%s.isolated' % target),
|
|
],
|
|
step_test_data=lambda: self.test_api.output_json([target]),
|
|
**kwargs))
|
|
|
|
if batch_targets:
|
|
# TODO(vadimsh): Differentiate between bad *.isolate and upload errors.
|
|
# Raise InfraFailure on upload errors.
|
|
args = [
|
|
self.m.swarming_client.path,
|
|
'batcharchive',
|
|
'--dump-json', self.m.json.output(),
|
|
'--isolate-server', self._isolate_server,
|
|
] + (['--verbose'] if verbose else []) + [
|
|
build_dir.join('%s.isolated.gen.json' % t) for t in batch_targets
|
|
]
|
|
isolate_steps.append(
|
|
self.m.python(
|
|
'isolate tests', self.resource('isolate.py'), args,
|
|
step_test_data=lambda: self.test_api.output_json(batch_targets),
|
|
**kwargs))
|
|
|
|
# TODO(tansell): Change this to return a dummy "isolate results" or the
|
|
# top level master step.
|
|
return isolate_steps[-1]
|
|
finally:
|
|
step_result = self.m.step.active_result
|
|
swarm_hashes = {}
|
|
for step in isolate_steps:
|
|
if not step.json.output:
|
|
continue # pragma: no cover
|
|
|
|
for k, v in step.json.output.iteritems():
|
|
# TODO(tansell): Raise an error here when it can't clobber an
|
|
# existing error. This code is currently inside a finally block,
|
|
# meaning it could be executed when an existing error is occurring.
|
|
# See https://chromium-review.googlesource.com/c/437024/
|
|
#assert k not in swarm_hashes or swarm_hashes[k] == v, (
|
|
# "Duplicate hash for target %s was found at step %s."
|
|
# "Existing hash: %s, New hash: %s") % (
|
|
# k, step, swarm_hashes[k], v)
|
|
swarm_hashes[k] = v
|
|
|
|
if swarm_hashes:
|
|
self._isolated_tests = swarm_hashes
|
|
|
|
if set_swarm_hashes:
|
|
step_result.presentation.properties['swarm_hashes'] = swarm_hashes
|
|
|
|
missing = sorted(
|
|
t for t, h in self._isolated_tests.iteritems() if not h)
|
|
if missing:
|
|
step_result.presentation.logs['failed to isolate'] = (
|
|
['Failed to isolate following targets:'] +
|
|
missing +
|
|
['', 'See logs for more information.']
|
|
)
|
|
for k in missing:
|
|
self._isolated_tests.pop(k)
|
|
|
|
@property
|
|
def isolated_tests(self):
|
|
"""The dictionary of 'target name -> isolated hash' for this run.
|
|
|
|
These come either from the incoming swarm_hashes build property,
|
|
or from calling find_isolated_tests, above, at some point during the run.
|
|
"""
|
|
hashes = self.m.properties.get('swarm_hashes', self._isolated_tests)
|
|
# Be robust in the case where swarm_hashes is an empty string
|
|
# instead of an empty dictionary, or similar.
|
|
if not hashes:
|
|
return {} # pragma: no covergae
|
|
return {
|
|
k.encode('ascii'): v.encode('ascii')
|
|
for k, v in hashes.iteritems()
|
|
}
|
|
|
|
@property
|
|
def _run_isolated_path(self):
|
|
"""Returns the path to run_isolated.py."""
|
|
return self.m.swarming_client.path.join('run_isolated.py')
|
|
|
|
def run_isolated(self, name, isolate_hash, args=None, **kwargs):
|
|
"""Runs an isolated test."""
|
|
cmd = [
|
|
'--isolated', isolate_hash,
|
|
'-I', self.isolate_server,
|
|
'--verbose',
|
|
]
|
|
if args:
|
|
cmd.append('--')
|
|
cmd.extend(args)
|
|
self.m.python(name, self._run_isolated_path, cmd, **kwargs)
|