blob: 997fb7accb7a7a46c9cba8d3091f5c6fa979d3de [file] [log] [blame]
# Copyright (c) 2012 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.
"""Set of utilities to add commands to a buildbot factory (BuildFactory).
All the utility functions to add steps to a build factory here are not
project-specific. See the other *_commands.py for project-specific commands.
"""
import base64
import json
import ntpath
import posixpath
import re
import zlib
from buildbot.locks import SlaveLock
from buildbot.process.properties import WithProperties
from buildbot.status.builder import SUCCESS
from buildbot.steps import shell
from buildbot.steps.transfer import FileDownload
from buildbot.steps import trigger
from twisted.python import log
from common import chromium_utils
import config
from master import chromium_step
from master.log_parser import retcode_command
from master.optional_arguments import ListProperties
# DEFAULT_TESTS is a marker to specify that the default tests should be run for
# this builder. It's mainly used for try job; this is implicit for non-try
# builders that all steps are run.
DEFAULT_TESTS = 'defaulttests'
def CreateTriggerStep(trigger_name, trigger_set_properties=None,
trigger_copy_properties=None, do_step_if=True,
waitForFinish=False, trigger_drop_properties=None):
"""Returns a Trigger Step, with all the default values copied over.
Args:
trigger_name: the name of the triggered scheduler.
trigger_set_properties: a dict of all the properties to be set on the
triggered bot. If a default property has the same name, it will be
overwritten.
trigger_copy_properties: a list of all the additional properties to copy
over to the triggered bot.
waitForFinish: Wait for the triggered build to finish.
"""
trigger_set_properties = trigger_set_properties or {}
trigger_copy_properties = trigger_copy_properties or []
set_properties = {
# Here are the standard names of the parent build properties.
'parent_buildername': WithProperties('%(buildername:-)s'),
'parent_buildnumber': WithProperties('%(buildnumber:-)s'),
'parent_build_archive_url': WithProperties('%(build_archive_url:-)s'),
'parent_branch': WithProperties('%(branch:-)s'),
'parent_git_number': WithProperties('%(git_number:-)s'),
'parent_got_revision': WithProperties('%(got_revision:-)s'),
'parent_got_v8_revision': WithProperties('%(got_v8_revision:-)s'),
'parent_got_webkit_revision':
WithProperties('%(got_webkit_revision:-)s'),
'parent_got_nacl_revision':
WithProperties('%(got_nacl_revision:-)s'),
'parent_got_swarming_client_revision':
WithProperties('%(got_swarming_client_revision:-)s'),
'parent_got_clang_revision': WithProperties('%(got_clang_revision:-)s'),
'parent_got_angle_revision': WithProperties('%(got_angle_revision:-)s'),
'parent_revision': WithProperties('%(revision:-)s'),
'parent_scheduler': WithProperties('%(scheduler:-)s'),
'parent_slavename': WithProperties('%(slavename:-)s'),
'parent_builddir': WithProperties('%(builddir:-)s'),
'parent_try_job_key': WithProperties('%(try_job_key:-)s'),
'issue': WithProperties('%(issue:-)s'),
'patchset': WithProperties('%(patchset:-)s'),
'patch_url': WithProperties('%(patch_url:-)s'),
'rietveld': WithProperties('%(rietveld:-)s'),
'root': WithProperties('%(root:-)s'),
'requester': WithProperties('%(requester:-)s'),
# And some scripts were written to use non-standard names.
'parent_cr_revision': WithProperties('%(got_revision:-)s'),
'parent_wk_revision': WithProperties('%(got_webkit_revision:-)s'),
'parentname': WithProperties('%(buildername)s'),
'parentslavename': WithProperties('%(slavename:-)s'),
}
set_properties.update(trigger_set_properties)
if trigger_drop_properties:
for key in trigger_drop_properties:
if key in set_properties:
del set_properties[key]
return trigger.Trigger(
schedulerNames=[trigger_name],
updateSourceStamp=False,
waitForFinish=waitForFinish,
set_properties=set_properties,
copy_properties=trigger_copy_properties + ['testfilter'],
doStepIf=do_step_if)
def GetProp(bStep, name, default):
"""Returns a build step property or |default| if the property is not set."""
try:
return bStep.build.getProperty(name)
except KeyError:
return default
def GetTestfilter(bStep):
"""Returns testfilter build property as a dict of steps.
Each element found in the build property is split along ':' and the value is
optional gtest filter.
If no testfilter property is set, {DEFAULT_TESTS, ''} is returned.
"""
test_filters = GetProp(bStep, 'testfilter', None)
# testfilter could be a list or None.
if not test_filters:
return {DEFAULT_TESTS: ''}
# Actively look for bugs where 'testfilter' would be a string.
assert isinstance(test_filters, (list, tuple))
return dict(i.split(':', 1) if ':' in i else (i, '') for i in test_filters)
def BuildIsolatedFiles(test_filters, run_default_swarm_tests):
"""Returns True if this build should generate the hashfiles required to run
tests on swarm."""
return bool(GetSwarmTestsFromTestFilter(test_filters,
run_default_swarm_tests))
def GetSwarmTestsFromTestFilter(test_filters, run_default_swarm_tests):
"""Returns the dict of all the tests in the list that should be run with
swarm.
If 'run_default_swarm_tests' is set, it is automatically added to the list. It
must only be set on builder/tester configuration.
Any _swarm suffix is stripped.
"""
assert isinstance(test_filters, dict)
assert isinstance(run_default_swarm_tests, list)
# Always allow manually added swarm tests to run.
swarm_tests = dict(
(k[:-len('_swarm')], v) for k, v in test_filters.iteritems()
if k.endswith('_swarm')
)
# Only add the default swarm tests if the builder is marked as swarm enabled.
if DEFAULT_TESTS in test_filters and run_default_swarm_tests:
# TODO(maruel): This doesn't belong here at all.
for test in run_default_swarm_tests:
swarm_tests.setdefault(test, '')
return swarm_tests
def GetSwarmTests(bStep):
"""Gets the dict of all the swarm tests that this build testfilter will allow.
Arguments:
bStep: Any BuildStep inside the Build.
The items in the returned list have the '_swarm' suffix stripped.
"""
test_filters = GetTestfilter(bStep)
run_default_swarm_tests = GetProp(bStep, 'run_default_swarm_tests', [])
return GetSwarmTestsFromTestFilter(test_filters, run_default_swarm_tests)
class CompileWithRequiredSwarmTargets(shell.Compile):
def start(self):
test_filters = GetTestfilter(self)
run_default_swarm_tests = GetProp(self, 'run_default_swarm_tests', [])
command = self.command[:]
swarm_tests = list(GetSwarmTestsFromTestFilter(test_filters,
run_default_swarm_tests))
# Only add if not already present.
for t in swarm_tests:
if t not in command:
command.append(t)
if 'compile' in test_filters and not 'All' in command:
# ninja has an 'all' pseudo-target that tries to run all the targets knows
# about.
# 'All' is a target in build/all.gyp that contains the vast majority of
# targets but not all of them. :)
command.append('All')
self.setCommand(command)
return shell.Compile.start(self)
class WithJsonProperties(WithProperties):
def __init__(self, fmtstring, transform=None, *args, **kwargs):
WithProperties.__init__(self, fmtstring, *args, **kwargs)
self.transform = transform
if not self.transform:
self.transform = lambda x: x
def getRenderingFor(self, build):
ret = build.getProperties().asDict()
# asDict returns key -> (value, source), so get the values, and convert
# empty values to blank strings.
for k in ret:
ret[k] = ret[k][0] if ret[k][0] is not None else ''
for k, v in self.lambda_subs.iteritems():
ret[k] = v(build)
return self.fmtstring % self.transform(ret)
class FactoryCommands(object):
# Use this to prevent steps which cannot be run on the same
# slave from being done together (in the case where slaves are
# shared by multiple builds).
slave_exclusive_lock = SlaveLock('slave_exclusive', maxCount=1)
# --------------------------------------------------------------------------
# PERF TEST SETTINGS
# In each mapping below, the first key is the target and the second is the
# perf_id. The value is the directory name in the results URL.
# Configuration of most tests.
# TODO(stip): would be nice to get rid of this.
PERF_TEST_MAPPINGS = {
'Release': {
'chrome-linux32-beta': 'linux32-beta',
'chrome-linux32-stable': 'linux32-stable',
'chrome-linux64-beta': 'linux64-beta',
'chrome-linux64-stable': 'linux64-stable',
'chrome-mac-beta': 'mac-beta',
'chrome-mac-stable': 'mac-stable',
'chrome-win-beta': 'win-beta',
'chrome-win-stable': 'win-stable',
'chromium-linux-targets': 'linux-targets',
'chromium-mac-targets': 'mac-targets',
'chromium-rel-linux': 'linux-release',
'chromium-rel-linux-64': 'linux-release-64',
'chromium-rel-linux-hardy': 'linux-release-hardy',
'chromium-rel-linux-hardy-lowmem': 'linux-release-lowmem',
'chromium-rel-linux-webkit': 'linux-release-webkit-latest',
'chromium-rel-mac': 'mac-release',
'chromium-rel-mac5-v8': 'mac-release-10.5-v8-latest',
'chromium-rel-mac6-v8': 'mac-release-10.6-v8-latest',
'chromium-rel-mac6-webkit': 'mac-release-10.6-webkit-latest',
'chromium-rel-old-mac6': 'mac-release-old-10.6',
'chromium-rel-vista-dual': 'vista-release-dual-core',
'chromium-rel-vista-dual-v8': 'vista-release-v8-latest',
'chromium-rel-vista-single': 'vista-release-single-core',
'chromium-rel-vista-webkit': 'vista-release-webkit-latest',
'chromium-rel-xp': 'xp-release',
'chromium-rel-xp-dual': 'xp-release-dual-core',
'chromium-rel-xp-single': 'xp-release-single-core',
'chromium-win-targets': 'win-targets',
'nacl-lucid64-spec-x86': 'nacl-lucid64-spec-x86',
'nacl-lucid64-spec-arm': 'nacl-lucid64-spec-arm',
'nacl-lucid64-spec-trans': 'nacl-lucid64-spec-trans',
},
'Debug': {
'chromium-dbg-linux': 'linux-debug',
'chromium-dbg-mac': 'mac-debug',
'chromium-dbg-xp': 'xp-debug',
'chromium-dbg-win': 'win-debug',
'chromium-dbg-linux-try': 'linux-try-debug',
},
}
DEFAULT_GTEST_FILTER = ''
# TODO(maruel): DEFAULT_GTEST_FILTER = '-*.FLAKY_*:*.FAILS_*'
def __init__(self, factory=None, target=None, build_dir=None,
target_platform=None, target_arch=None, repository_root='src'):
"""Initializes the SlaveCommands class.
Args:
factory: BuildFactory to configure.
target: Build configuration, case-sensitive; probably 'Debug' or
'Release'
build_dir: name of the directory within the buildbot working directory
in which the solution, Debug, and Release directories are found.
target_platform: Slave's OS.
repository_root: Relative root directory of the sources (e.g. 'src' for
Chromium or 'v8' for stand-alone v8)
"""
self._factory = factory
self._target = target
self._build_dir = build_dir
self._target_platform = target_platform
self._target_arch = target_arch
# Starting from e.g. C:\b\build\slave\build_slave_path\build, find
# C:\b\build\scripts\slave.
self._script_dir = self.PathJoin('..', '..', '..', 'scripts', 'slave')
self._private_script_dir = self.PathJoin(self._script_dir, '..', '..', '..',
'build_internal', 'scripts',
'slave')
self._perl = self.GetExecutableName('perl')
if self._target_platform == 'win32':
# Steps run using a separate copy of python.exe, so it can be killed at
# the start of a build. But the kill_processes (taskkill) step has to use
# the original python.exe, or it kills itself.
self._python = 'python_slave'
else:
self._python = 'python'
self.working_dir = 'build'
self._repository_root = repository_root
self._kill_tool = self.PathJoin(self._script_dir, 'kill_processes.py')
self._runhooks_tool = self.PathJoin(self._script_dir, 'runhooks_wrapper.py')
self._mb_tool = self.PathJoin(repository_root, 'tools', 'mb', 'mb.py')
self._compile_tool = self.PathJoin(self._script_dir, 'compile.py')
self._test_tool = self.PathJoin(self._script_dir, 'runtest.py')
self._zip_tool = self.PathJoin(self._script_dir, 'zip_build.py')
self._extract_tool = self.PathJoin(self._script_dir, 'extract_build.py')
self._cleanup_temp_tool = self.PathJoin(self._script_dir, 'cleanup_temp.py')
self._bot_update_tool = self.PathJoin(self._script_dir, 'bot_update.py')
self._gclient_safe_revert_tool = self.PathJoin(self._script_dir,
'gclient_safe_revert.py')
self._update_clang_py_tool = self.PathJoin(
self._repository_root, 'tools', 'clang', 'scripts', 'update.py')
self._update_nacl_sdk_tool = self.PathJoin(self._script_dir,
'update_nacl_sdk.py')
# chrome_staging directory, relative to the build directory.
self._staging_dir = self.PathJoin('..', 'chrome_staging')
# scripts in scripts/slave
self._runbuild = self.PathJoin(self._script_dir, 'runbuild.py')
@property
def python(self):
return self._python
@property
def script_dir(self):
return self._script_dir
# Util methods.
def GetExecutableName(self, executable):
"""The executable name must be executable plus '.exe' on Windows, or else
just the test name."""
if self._target_platform == 'win32':
return executable + '.exe'
return executable
def PathJoin(self, *args):
if self._target_platform == 'win32':
return ntpath.normpath(ntpath.join(*args))
else:
return posixpath.normpath(posixpath.join(*args))
# Basic commands
def GetTestCommand(self, executable, arg_list=None, factory_properties=None,
wrapper_args=None):
cmd = [self._python, self._test_tool]
if self._target:
cmd.extend(['--target', self._target])
cmd = self.AddBuildProperties(cmd)
if factory_properties:
cmd = self.AddFactoryProperties(factory_properties, cmd)
# Must add test tool arg list before test arg list.
if wrapper_args:
cmd.extend(wrapper_args)
cmd.append(self.GetExecutableName(executable))
if arg_list is not None:
cmd.extend(arg_list)
return cmd
def GetPythonTestCommand(self, py_script, arg_list=None, wrapper_args=None,
factory_properties=None):
cmd = [self._python, self._test_tool, '--run-python-script']
if self._target:
cmd.extend(['--target', self._target])
cmd = self.AddBuildProperties(cmd)
if factory_properties:
cmd = self.AddFactoryProperties(factory_properties, cmd)
if wrapper_args is not None:
cmd.extend(wrapper_args)
cmd.append(py_script)
if arg_list is not None:
cmd.extend(arg_list)
return cmd
def GetShellTestCommand(self, sh_script, arg_list=None, wrapper_args=None,
factory_properties=None):
""" As above, arg_list goes to the shell script, wrapper_args come
before the script so the test tool uses them.
"""
cmd = [self._python,
self._test_tool,
'--run-shell-script',
'--target', self._target]
cmd = self.AddBuildProperties(cmd)
cmd = self.AddFactoryProperties(factory_properties, cmd)
if wrapper_args is not None:
cmd.extend(wrapper_args)
cmd.append(sh_script)
if arg_list is not None:
cmd.extend(arg_list)
return cmd
def AddB64GzBuildProperties(self, cmd=None):
"""Adds a WithProperties() call with build properties to cmd."""
# pylint: disable=R0201
cmd = cmd or []
cmd.append(WithJsonProperties(
'--build-properties-gz=%s',
transform=chromium_utils.b64_gz_json_encode))
return cmd
def AddB64GzFactoryProperties(self, factory_properties, cmd=None):
"""Adds factory properties to cmd."""
# pylint: disable=R0201
cmd = cmd or []
cmd.append(
'--factory-properties-gz=%s'
% chromium_utils.b64_gz_json_encode(factory_properties))
return cmd
def AddBuildProperties(self, cmd=None):
"""Adds a WithProperties() call with build properties to cmd."""
# pylint: disable=R0201
cmd = cmd or []
def gen_blamelist_string(build):
blame = ','.join(build.getProperty('blamelist'))
# Could be interpreted by the shell.
return re.sub(r'[\&\|\^]', '', blame.replace('<', '[').replace('>', ']'))
cmd.append(
WithJsonProperties(
'--build-properties=%s',
transform=lambda x: json.dumps(
x, sort_keys=True, separators=(',', ':')),
blamelist=gen_blamelist_string,
blamelist_real=lambda b: b.getProperty('blamelist')))
return cmd
def AddFactoryProperties(self, factory_properties, cmd=None):
"""Adds factory properties to cmd."""
# pylint: disable=R0201
cmd = cmd or []
cmd.append(
'--factory-properties=' + json.dumps(
factory_properties or {}, sort_keys=True, separators=(',', ':')))
return cmd
def AddTestStep(self, command_class, test_name, test_command,
test_description='', timeout=10*60, max_time=8*60*60,
workdir=None, env=None, locks=None, halt_on_failure=False,
do_step_if=True, br_do_step_if=None, hide_step_if=False,
alwaysRun=False, **kwargs):
"""Adds a step to the factory to run a test.
Args:
command_class: the command type to run, such as shell.ShellCommand
test_name: a string describing the test, used to build its logfile name
and its descriptions in the waterfall display
timeout: the buildbot timeout for the test, in seconds. If it doesn't
produce any output to stdout or stderr for this many seconds,
buildbot will cancel it and call it a failure.
max_time: the maxiumum time the command can run, in seconds. If the
command doesn't return in this many seconds, buildbot will cancel it
and call it a failure.
test_command: the command list to run
test_description: an auxiliary description to be appended to the
test_name in the buildbot display; for example, ' (single process)'
workdir: directory where the test executable will be launched. If None,
step will use default directory.
env: dictionary with environmental variable key value pairs that will be
set or overridden before launching the test executable. Does not do
anything if 'env' is None.
locks: any locks to acquire for this test
halt_on_failure: whether the current build should halt if this step fails
br_do_step_if: when run under buildrunner, execute this function to
determine whether to run a step or not. has no effect if not using
buildrunner.
"""
assert timeout <= max_time
if not br_do_step_if:
do_step_if = do_step_if or self.TestStepFilter
else:
# don't confuse CQ with duplicate step names
# runbuild.py will strip this suffix out and add via annotator
test_name += '_buildrunner_ignore'
self._factory.addStep(
command_class,
name=test_name,
timeout=timeout,
maxTime=max_time,
doStepIf=do_step_if,
brDoStepIf=br_do_step_if,
hideStepIf=hide_step_if,
workdir=workdir,
env=env,
# TODO(bradnelson): FIXME
#locks=locks,
description='running %s%s' % (test_name, test_description),
descriptionDone='%s%s' % (test_name, test_description),
haltOnFailure=halt_on_failure,
command=test_command,
alwaysRun=alwaysRun,
**kwargs)
self._factory.properties.setProperty('gtest_filter', None, 'BuildFactory')
def TestStepFilter(self, bStep):
"""The normal step filter to use on tests. Runs the test by default.
The test will run unless a build property 'testfilter' is specified *and* it
also doesn't include DEFAULT_TESTS. In particular, if the build property
'testfilter' == None, it is equivalent as if it were set to DEFAULT_TESTS.
See GetTestfilter() for the ugly details.
"""
return self.TestStepFilterImpl(bStep, True)
def TestStepFilterImpl(self, bStep, default):
"""Returns True if the step should be executed, instead of being skipped.
It examines the |testfilter| build property and determines if the step
should run.
There is 2 broad categories, either |testfilter| was specified or not.
If |testfilter| was specified, there's 3 possibilities:
- |testfilter| contains the test name. The test is run unconditionally.
- |testfilter| doesn't contain the test name neither DEFAULT_TESTS. The test
is not run.
- |testfilter| contains DEFAULT_TESTS but not the test name, see the next
section as if |testfilter| was not specified.
If |testfilter| was not specified or contained DEFAULT_TESTS, there's 3
possibilities:
- Neither |run_default_swarm_tests| nor |non_default| were specified, the
test runs.
- test is listed in |non_default|, it is not run.
- test is listed in |run_default_swarm_tests|, it is not run by default,
similar to |non_default| but it is run on swarm_triggered instead. This
is specific to builder/tester type of builder setup.
Both |run_default_swarm_tests| and |non_default| are 'hard coded' for the
builder in the factory. |testfilter| is optionally specified in Try Jobs via
the trigger.
"""
# TODO(maruel): This is bad hygiene to modify the build properties on the
# fly like this. There should be another way to communicate the command line
# properly.
bStep.setProperty('gtest_filter', None, 'Factory')
test_filters = GetTestfilter(bStep)
name = bStep.name
# TODO(maruel): Fix the step name.
if name.startswith('memory test: '):
name = name[len('memory test: '):]
# If it is set, it means that the step should be run through a swarm
# specific builder instead of the current step.
run_through_swarm = name in GetProp(bStep, 'run_default_swarm_tests', [])
# Continue if:
# - the test is specified in filters
# - DEFAULT_TESTS is listed, default is True and the test isn't running
# through swarm.
if not (name in test_filters or
(DEFAULT_TESTS in test_filters and
default and
not run_through_swarm)):
return False
# This is gtest specific, but other test types can safely ignore it.
# Defaults to excluding FAILS and FLAKY test if none is specified.
gtest_filter = test_filters.get(name, '') or self.DEFAULT_GTEST_FILTER
if gtest_filter:
flag = '--gtest_filter=%s' % gtest_filter
bStep.setProperty('gtest_filter', flag, 'Scheduler')
return True
def GetTestStepFilter(self, factory_properties):
"""Returns a TestStepFilter lambda with the right default according to
non_default factory property.
Note: the factory_properties 'non_default' MUST ONLY BE USED ON THE TRY
SERVER, since the only way to run non default tests is to supply a
'testfilter' build property.
"""
# TODO(maruel): Figure out a way to find out if it's currently running on a
# Try Server, and if not, refuse 'non_default'.
return lambda bStep: self.TestStepFilterImpl(
bStep,
bStep.name not in factory_properties.get('non_default', []))
def _GTestDoStep(self, test_name, factory_properties):
doStep = self.GetTestStepFilter(factory_properties)
if test_name.startswith('DISABLED_'):
test_name = test_name[len('DISABLED_'):]
doStep = False
return doStep
def AddAnnotatedGTestTestStep(self, *args, **kwargs):
"""Proxy for AddGTestTestStep() to allow two-phase commit.
This is temporarily needed to prevent breakage of internal code.
"""
self.AddGTestTestStep(*args, **kwargs)
def AddGTestTestStep(self, test_name, factory_properties=None, description='',
arg_list=None,
total_shards=None, shard_index=None,
test_tool_arg_list=None, hideStep=False, timeout=10*60,
max_time=8*60*60):
"""Adds an Annotated step to the factory to run the gtest tests.
Args:
test_name: If prefixed with DISABLED_ the prefix is removed, and the
step is flagged for not running, but is still added.
total_shards: Number of shards to split this test into.
shard_index: Shard to run. Must be between 1 and total_shards.
generate_gtest_json: generate JSON results file after running the tests.
"""
test_tool_arg_list = test_tool_arg_list or []
test_tool_arg_list.append('--annotate=gtest')
factory_properties = factory_properties or {}
if not arg_list:
arg_list = []
arg_list = arg_list[:]
if not hideStep:
doStep = self._GTestDoStep(test_name, factory_properties)
brDoStep = None
else:
doStep = False
brDoStep = self._GTestDoStep(test_name, factory_properties)
cmd = [self._python, self._test_tool,
'--target', self._target]
cmd = self.AddBuildProperties(cmd)
cmd = self.AddFactoryProperties(factory_properties, cmd)
# Must add test tool arg list before test arg list.
if test_tool_arg_list:
cmd.extend(test_tool_arg_list)
cmd.extend(['--test-type', test_name])
if total_shards and shard_index:
cmd.extend(['--total-shards', str(total_shards),
'--shard-index', str(shard_index)])
env = factory_properties.get('testing_env')
cmd.append(self.GetExecutableName(test_name))
arg_list.append('--gtest_print_time')
arg_list.append(WithProperties('%(gtest_filter)s'))
cmd.extend(arg_list)
self.AddTestStep(chromium_step.AnnotatedCommand, test_name,
ListProperties(cmd), description, timeout=timeout,
max_time=max_time, do_step_if=doStep,
env=env, br_do_step_if=brDoStep, hide_step_if=hideStep,
target=self._target, factory_properties=factory_properties)
def AddBuildStep(self, factory_properties, name='build', env=None,
timeout=6000):
"""Add annotated step to use the buildrunner to run steps on the slave."""
factory_properties['target'] = self._target
cmd = [self._python, self._runbuild, '--annotate']
cmd = self.AddBuildProperties(cmd)
cmd = self.AddFactoryProperties(factory_properties, cmd)
self._factory.addStep(chromium_step.AnnotatedCommand,
name=name,
description=name,
timeout=timeout,
haltOnFailure=True,
brDoStepIf=None,
command=cmd,
env=env,
factory_properties=factory_properties,
target=self._target)
def AddBuildrunnerGTest(self, test_name, factory_properties=None,
description='', arg_list=None,
total_shards=None, shard_index=None,
test_tool_arg_list=None, timeout=10*60,
max_time=8*60*60):
"""Add a buildrunner GTest step, which will be executed with runbuild.
This will appear hidden and skipped on the main waterfall, but executed when
run under runbuild.py. Note that a final runbuild step will need to be added
with AddBuildStep().
"""
self.AddGTestTestStep(test_name,
factory_properties=factory_properties,
description=description,
arg_list=arg_list,
total_shards=total_shards,
shard_index=shard_index,
test_tool_arg_list=test_tool_arg_list,
hideStep=True,
timeout=10*60,
max_time=8*60*60)
def AddBuildrunnerTestStep(self, command_class, test_name, test_command,
test_description='', timeout=10*60,
max_time=8*60*60, workdir=None, env=None,
locks=None, halt_on_failure=False,
do_step_if=True, **kwargs):
"""Add a buildrunner test step, which will be executed with runbuild.
This will appear hidden and skipped on the main waterfall, but executed when
run under runbuild.py. Note that a final runbuild step will need to be added
with AddBuildStep().
"""
do_step_if = do_step_if or self.TestStepFilter
self.AddTestStep(command_class, test_name, test_command,
test_description=test_description, timeout=timeout,
max_time=max_time, workdir=workdir, env=env, locks=locks,
halt_on_failure=halt_on_failure, do_step_if=False,
br_do_step_if=do_step_if, hide_step_if=True, **kwargs)
# GClient related commands.
def AddSvnKillStep(self):
"""Adds a step to the factory to kill svn.exe. Windows-only."""
self._factory.addStep(shell.ShellCommand, name='svnkill',
description='svnkill', timeout=60,
workdir='', # The build subdir may not exist yet.
command=[r'%WINDIR%\system32\taskkill',
'/f', '/im', 'svn.exe',
'||', 'set', 'ERRORLEVEL=0'])
def AddTempCleanupStep(self):
"""Runs script to cleanup acculumated cruft, including tmp directory."""
# Use ReturnCodeCommand so we can indicate a "warning" status (orange).
self._factory.addStep(retcode_command.ReturnCodeCommand,
name='cleanup_temp',
description='cleanup_temp',
timeout=1500,
workdir='', # Doesn't really matter where we are.
alwaysRun=True, # Run this even on update failures
flunkOnFailure=False,
warnOnFailure=True,
command=['python', self._cleanup_temp_tool])
def AddGClientRevertStep(self):
"""Adds a step to revert the checkout to an unmodified state."""
command = [self._python, self._gclient_safe_revert_tool, '.',
chromium_utils.GetGClientCommand(self._target_platform)]
self._factory.addStep(shell.ShellCommand,
name='gclient_revert',
description='gclient_revert',
timeout=60*10,
workdir=self.working_dir,
command=command,
haltOnFailure=False)
def AddUpdateScriptStep(self, gclient_jobs=None, solutions=None, args=None):
"""Adds a step to the factory to update the script folder."""
# This will be run in the '..' directory to udpate the slave's own script
# checkout.
command = [chromium_utils.GetGClientCommand(self._target_platform),
'sync', '--verbose', '--force', '--delete_unversioned_trees']
if gclient_jobs:
command.append('-j%d' % gclient_jobs)
if solutions:
spec = 'solutions=[%s]' % ''.join(s.GetSpec() for s in solutions)
spec = spec.replace(' ', '')
command.extend(['--spec', spec])
if args:
command.extend(args)
self._factory.addStep(shell.ShellCommand,
name='update_scripts',
description='update_scripts',
locks=[self.slave_exclusive_lock],
timeout=60*5,
workdir='../../..',
flunkOnFailure=False,
warnOnFailure=True,
command=command)
def AddUpdateStep(self, gclient_spec, env=None, timeout=None,
sudo_for_remove=False, gclient_deps=None,
gclient_nohooks=False, no_gclient_branch=False,
no_gclient_revision=False,
gclient_transitive=False, primary_repo=None,
gclient_jobs=None, blink_config=None):
"""Adds a step to the factory to update the workspace."""
env = env or {}
env['DEPOT_TOOLS_UPDATE'] = '0'
env['CHROMIUM_GYP_SYNTAX_CHECK'] = '1'
if timeout is None:
# svn timeout is 2 min; we allow 5
timeout = 60*5
self._factory.addStep(
chromium_step.GClient,
gclient_spec=gclient_spec,
gclient_deps=gclient_deps,
# TODO(maruel): Kept for compatibility but will be removed.
gclient_nohooks=gclient_nohooks,
workdir=self.working_dir,
mode='update',
env=env,
locks=[self.slave_exclusive_lock],
retry=(60*5, 4), # Try 4+1=5 more times, 5 min apart
timeout=timeout,
gclient_jobs=gclient_jobs,
sudo_for_remove=sudo_for_remove,
rm_timeout=60*15, # The step can take a long time.
no_gclient_branch=no_gclient_branch,
no_gclient_revision=no_gclient_revision,
gclient_transitive=gclient_transitive,
primary_repo=primary_repo,
blink_config=blink_config)
def AddBotUpdateStep(self, env, gclient_specs, revision_mapping,
server=None, blink_config=False):
"""Add a step to force checkout to some state.
This is meant to replace all gclient revert/sync steps.
"""
cmd = ['python', '-u', self._bot_update_tool, '--specs', gclient_specs]
# TODO(hinoka): Remove this when official builders have their own
# gclient runhooks step.
for env_key, env_value in env.iteritems():
# Extract out gyp envs.
if 'gyp' in env_key.lower():
cmd.extend(['--gyp_env', '%s=%s' % (env_key, env_value)])
def rev_factory(blink_config, gclient_specs):
"""Using a variety of signals, determine the revision resolver."""
# HACK(hinoka): Because WebKit schedulers watch both the Chromium and
# Blink repositories, the revision could be either a blink
# or chromium revision. We need to differentiate them.
def resolve_blink_revision(build):
# Ahem, so when WithProperties() is resolved with getRenderingFor(),
# if you pass in keyword arguments to WithProperties(), it will actually
# call the value (it expects a lambda/function) with "build" as the
# only argument, where the output is then passed to the format string.
properties = build.getProperties()
# 1. Revisions always default to parent revisions.
src_revision = properties.getProperty('parent_got_revision')
webkit_revision = properties.getProperty('parent_wk_revision')
# 2. If this is not a triggered build, then add special logic to
# resolve the revision based on if we got passed a blink sourcestamp
# or chromium sourcestamp.
if (properties.getProperty('branch') == 'trunk'
or properties.getProperty('parent_branch') == 'trunk'):
# Blink Mode, revision refers to webkit.
if not webkit_revision:
webkit_revision = properties.getProperty('revision') or 'HEAD'
else:
# Normal Mode, revision refers to chromium.
if not src_revision:
src_revision = properties.getProperty('revision') or 'HEAD'
# 3. Default uninitialized revisions to HEAD. This only happens when
# someone presses "Force build", but we want to handle this
# gracefully too.
webkit_revision = webkit_revision or 'HEAD'
src_revision = src_revision or 'HEAD'
return 'src@%s,src/third_party/WebKit@%s' % (src_revision,
webkit_revision)
def resolve_v8_revision(build):
# TODO(hinoka): Remove this once V8 is 100% recipes.
properties = build.getProperties()
v8_revision = (properties.getProperty('parent_got_v8_revision') or
properties.getProperty('revision') or 'HEAD')
lkgr = 'lkgr'
# HACK(hinoka): master.client.v8 sets this URL in the gclient
# spec to indicate it wants to sync to lkcr, we use this
# signal to sync to origin/lkcr.
if ('https://build.chromium.org/p/chromium/lkcr-status/lkgr'
in gclient_specs):
lkgr = 'lkcr'
return 'src@origin/%s,src/v8@%s' % (lkgr, v8_revision)
# This is where the rev factory starts. It uses blink_config and
# gclient_specs to determine what mode we're running in, and sets the
# revision property accordingly, defaulting to just passing the revision
# property through.
if blink_config == 'blink':
return resolve_blink_revision
elif '$$V8_REV$$' in gclient_specs:
# HACK(hinoka): We are relying on the fact that in the v8 config, a
# property is added in to be string replaced later in
# the gclient pipeline.
return resolve_v8_revision
else:
return lambda build: build.getProperties().getProperty('revision')
PROPERTIES = {
'root': '%(root:-)s',
'issue': '%(issue:-)s',
'patchset': '%(patchset:-)s',
'master': '%(mastername:-)s',
'revision': {
'fmtstring': '%(resolved_revision:-)s',
'resolved_revision': rev_factory(blink_config, gclient_specs)
},
'patch_url': '%(patch_url:-)s',
'slave_name': '%(slavename:-)s',
'builder_name': '%(buildername:-)s',
}
for property_name, property_expr in PROPERTIES.iteritems():
if isinstance(property_expr, dict):
property_value = WithProperties(**property_expr)
else:
property_value = WithProperties(property_expr)
cmd.extend(['--%s' % property_name, property_value])
if server:
cmd.extend(['--rietveld_server', server])
# Add "--bot_update_clobber" flag to the command-line if the
# 'bot_update_clobber' property is set.
cmd.append(WithProperties('%s', 'bot_update_clobber:+--bot_update_clobber'))
if revision_mapping:
cmd.extend(['--revision_mapping=%s' % json.dumps(revision_mapping)])
self._factory.addStep(
chromium_step.AnnotatedCommand,
name='bot_update',
description='bot_update',
haltOnFailure=True,
flunkOnFailure=True,
# TODO(hinoka): Change this back to 600 once Windows performance issues
# are resolved crbug.com/383455.
timeout=1800,
workdir=self.working_dir,
command=cmd)
def AddRunHooksStep(self, env=None, timeout=None, options=None):
"""Adds a step to the factory to run the gclient hooks."""
env = env or {}
env['LANDMINES_VERBOSE'] = '1'
env['DEPOT_TOOLS_UPDATE'] = '0'
env['CHROMIUM_GYP_SYNTAX_CHECK'] = '1'
if timeout is None:
timeout = 60*10
cmd = [self._python, self._runhooks_tool]
options = options or {}
self._factory.addStep(
shell.ShellCommand,
haltOnFailure=True,
name='runhooks',
description='gclient hooks',
env=env,
locks=[self.slave_exclusive_lock],
timeout=timeout,
command=cmd)
def AddGenerateBuildFilesStep(self, env=None, timeout=None, options=None,
config_file_path=None, target=None):
options = options or []
if not config_file_path:
config_file_path = self.PathJoin(self._repository_root,
'tools', 'mb', 'mb_config.pyl')
cmd = [self._python, self._mb_tool, 'gen',
'-m', WithProperties('%(mastername)s'),
'-b', WithProperties('%(buildername)s'),
'--config-file', config_file_path]
if '--compiler=goma' in options or '--compiler=goma-clang' in options:
cmd += ['--goma-dir', self.PathJoin('..', '..', '..', '..', 'goma')]
assert(target is not None)
cmd += ['//out/%s' % target]
self._factory.addStep(
shell.ShellCommand,
haltOnFailure=True,
name='generate_build_files',
description='generate build files',
env=env,
locks=[self.slave_exclusive_lock],
timeout=timeout,
command=cmd)
def AddClobberTreeStep(self, gclient_spec, env=None, timeout=None,
gclient_deps=None, gclient_nohooks=False,
no_gclient_branch=None, options=None):
"""This is not for pressing 'clobber' on the waterfall UI page. This is
for clobbering all the sources. Using mode='clobber' causes the entire
working directory to get moved aside (to build.dead) --OR-- if
build.dead already exists, it deletes build.dead. Strange, but true.
See GClient.doClobber() (for move vs. delete logic) or Gclient.start()
(for mode='clobber' trigger) in chromium_commands.py.
In theory, this means we can have a ClobberTree step at the beginning of
a build to quickly move the existing workdir and do a full clean
checkout. Then, if we add the same step at the end of a build, it will
delete the moved-out-of-the-way directory. Presuming neither step fails
or times out, this allows a builder to pull a full, clean tree for
every build.
This is exactly what we want for official release builds, so that the
builder can refresh its entire tree based on a new buildspec (which
might point to a completely different branch or an older revision than
the last build on the machine).
"""
if env is None:
env = {}
env['DEPOT_TOOLS_UPDATE'] = '0'
if timeout is None:
# svn timeout is 2 min; we allow 5
timeout = 60*5
self._factory.addStep(chromium_step.GClient,
gclient_spec=gclient_spec,
gclient_deps=gclient_deps,
gclient_nohooks=True,
no_gclient_branch=no_gclient_branch,
workdir=self.working_dir,
mode='clobber',
env=env,
timeout=timeout,
rm_timeout=60*60) # We don't care how long it takes.
if not gclient_nohooks:
self.AddRunHooksStep(env=env, timeout=timeout, options=options)
def AddTaskkillStep(self):
"""Adds a step to kill the running processes before a build."""
# Use ReturnCodeCommand so we can indicate a "warning" status (orange).
self._factory.addStep(retcode_command.ReturnCodeCommand, name='taskkill',
description='taskkill',
timeout=60,
workdir='', # Doesn't really matter where we are.
command=['python', self._kill_tool])
# Zip / Extract commands.
def AddZipBuild(self, build_url, factory_properties=None):
factory_properties = factory_properties or {}
build_url = build_url or factory_properties['build_url']
assert build_url
revision = factory_properties.get('got_revision')
cmd = [self._python, self._zip_tool,
'--target', self._target,
'--build-url', build_url or '']
if revision:
cmd.extend(['--build_revision', revision])
if 'webkit_dir' in factory_properties:
cmd += ['--webkit-dir', factory_properties['webkit_dir']]
cmd = self.AddBuildProperties(cmd)
cmd = self.AddFactoryProperties(factory_properties, cmd)
self._factory.addStep(chromium_step.AnnotatedCommand,
name='package_build',
timeout=600,
description='packaging build',
descriptionDone='packaged build',
haltOnFailure=True,
command=cmd)
def AddExtractBuild(self):
"""Extract a build.
Assumes the zip file has a directory like src/xcodebuild which
contains the actual build.
"""
cmd = [self._python, self._extract_tool,
'--target', self._target,
'--build-archive-url',
WithProperties('%(parent_build_archive_url:-)s')]
cmd = self.AddBuildProperties(cmd)
self.AddTestStep(retcode_command.ReturnCodeCommand, 'extract_build', cmd,
halt_on_failure=True)
# Build commands.
def GetBuildCommand(self, clobber, solution, mode, options=None):
"""Returns a command list to call the _compile_tool in the given build_dir,
optionally clobbering the build (that is, deleting the build directory)
first.
"""
cmd = [self._python, self._compile_tool]
assert not solution, 'no bots should still pass in solution'
cmd.extend(['--target', self._target])
if self._target_arch:
cmd.extend(['--arch', self._target_arch])
if mode:
cmd.extend(['--mode', mode])
if clobber:
cmd.append('--clobber')
else:
# Below, WithProperties is appended to the cmd and rendered into a string
# for each specific build at build-time. When clobber is None, it renders
# to an empty string. When clobber is not None, it renders to the string
# --clobber. Note: the :+ after clobber controls this behavior and is not
# a typo.
cmd.append(WithProperties('%s', 'clobber:+--clobber'))
if options:
cmd.extend(options)
# Using ListProperties will process and discard None and '' values,
# otherwise posix platforms will fail.
return ListProperties(cmd)
def AddCompileStep(self, solution, clobber=False,
description='compiling',
descriptionDone='compile',
timeout=600, mode=None,
options=None, haltOnFailure=True, env=None):
"""Adds a step to the factory to compile the solution.
Args:
solution: the solution/sub-project file to build
clobber: if True, clobber the build (that is, delete the build
directory) before building
description: for the waterfall
descriptionDone: for the waterfall
timeout: if no output is received in this many seconds, the compile step
will be killed
mode: if given, this will be passed as the --mode option to the compile
command
options: list of additional options to pass to the compile command
halfOnFailure: should stop the build if compile fails
"""
self._factory.addStep(
CompileWithRequiredSwarmTargets,
name='compile',
timeout=timeout,
description=description,
descriptionDone=descriptionDone,
command=self.GetBuildCommand(clobber, solution, mode, options),
haltOnFailure=haltOnFailure,
env=env)
def AddUpdateNaClSDKStep(self, pepper_channel='stable'):
"""Calls the NaCl SDK update script.
The required pepper bundle is copied to nacl_sdk/pepper_current.
"""
cmd = [self._python, self._update_nacl_sdk_tool, '--pepper-channel',
pepper_channel]
self._factory.addStep(shell.ShellCommand,
name='update NaCl SDK',
description='updating NaCl SDK',
descriptionDone='NaCl SDK updated',
timeout=600,
workdir=self.working_dir,
command=cmd)
def _PerfStepMappings(self, show_results, perf_id):
"""Looks up test IDs in PERF_TEST_MAPPINGS and returns test info."""
perf_name = None
if show_results:
perf_name = perf_id
if (self._target in self.PERF_TEST_MAPPINGS and
perf_id in self.PERF_TEST_MAPPINGS[self._target]):
perf_name = self.PERF_TEST_MAPPINGS[self._target][perf_id]
return perf_name
def AddProfileCreationStep(self, profile_type_to_create):
"""Generate a profile for use by Telemetry tests.
Args:
profile_type_to_create: A string specifying the profile type to create.
"""
cmd_name = self.PathJoin(self._script_dir,
'generate_profile_shim.py')
cmd_args = ['--target=' + self._target,
'--profile-type-to-generate=' + profile_type_to_create]
cmd = self.GetPythonTestCommand(cmd_name, arg_list=cmd_args)
self.AddTestStep(chromium_step.AnnotatedCommand,
'generate_telemetry_profiles', cmd, timeout=20*60)
# Checks out and builds clang
def AddUpdateClangStep(self):
cmd = [self._python, self._update_clang_py_tool]
self._factory.addStep(shell.ShellCommand,
name='update_clang',
timeout=600,
description='Updating and building clang and plugins',
descriptionDone='clang updated',
env={'LLVM_URL': config.Master.llvm_url},
command=cmd)
def AddDownloadFileStep(self, mastersrc, slavedest, halt_on_failure):
"""Download a file from master."""
self._factory.addStep(
FileDownload(mastersrc=mastersrc, slavedest=slavedest,
haltOnFailure=halt_on_failure))
def AddDiagnoseGomaStep(self):
"""Diagnose goma log."""
goma_dir = self.PathJoin('..', '..', '..', 'goma')
cmd = [self._python, self.PathJoin(goma_dir, 'diagnose_goma_log.py')]
self.AddTestStep(shell.ShellCommand, 'diagnose_goma', cmd, timeout=60)
class CanCancelBuildShellCommand(shell.ShellCommand):
"""Like ShellCommand but can terminate the build.
On failure (non-zero exit code of a shell command), this command
will fake a success but terminate the build. This keeps the tree
green but otherwise stops all action.
"""
def evaluateCommand(self, cmd):
if cmd.rc != 0:
reason = 'Build has been cancelled without being a failure.'
self.build.stopBuild(reason)
self.build.buildFinished(('Stopped Early', reason), SUCCESS)
return SUCCESS
class WaterfallLoggingShellCommand(shell.ShellCommand):
"""A shell command that can add messages to the main waterfall page.
Any string on stdio from this shell command with the prefix
WATERFALL_LOG will be added to the main waterfall page. To avoid
pollution these should be limited and important, such as a summary
number or version.
"""
def __init__(self, *args, **kwargs):
self.messages = []
# Argh... not a new style class?
# super(WaterfallLoggingShellCommand, self).__init__(self, *args, **kwargs)
shell.ShellCommand.__init__(self, *args, **kwargs)
def commandComplete(self, cmd):
out = cmd.logs['stdio'].getText()
self.messages = re.findall('WATERFALL_LOG (.*)', out)
def getText(self, cmd, results):
return self.describe(True) + self.messages
class SetBuildPropertyShellCommand(shell.ShellCommand):
"""A shell command that can set build property.
String on stdio from this shell command will be parsed for
BUILD_PROPERTY property_name=value
Property name and value will be set as build property.
"""
def __init__(self, *args, **kwargs):
self.messages = []
shell.ShellCommand.__init__(self, *args, **kwargs)
def commandComplete(self, cmd):
out = cmd.logs['stdio'].getText()
build_properties = re.findall('BUILD_PROPERTY ([^=]*)=(.*)', out)
for propname, value in build_properties:
# findall can return strings containing CR characters, remove with strip.
self.build.setProperty(propname, value.strip(), 'Step')
def getText(self, cmd, results):
return self.describe(True) + self.messages