| # Copyright 2013 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import os |
| import sys |
| |
| from recipe_engine import recipe_api |
| |
| WEBRTC_GS_BUCKET = 'chromium-webrtc' |
| |
| from . import builders as webrtc_builders |
| from . import steps |
| |
| THIS_DIR = os.path.dirname(__file__) |
| sys.path.append(os.path.join(os.path.dirname(THIS_DIR))) |
| |
| from chromium_tests.steps import SwarmingTest |
| |
| |
| CHROMIUM_REPO = 'https://chromium.googlesource.com/chromium/src' |
| # WebRTC's dependencies on Chromium's subtree mirrors like |
| # https://chromium.googlesource.com/chromium/src/build.git |
| CHROMIUM_DEPS = ['base', 'build', 'ios', 'testing', 'third_party', 'tools'] |
| |
| |
| class Bot(object): |
| def __init__(self, builders, recipe_configs, bucket, builder): |
| self._builders = builders |
| self._recipe_configs = recipe_configs |
| self.bucket = bucket |
| self.builder = builder |
| |
| def __repr__(self): # pragma: no cover |
| return '<Bot %s/%s>' % (self.bucket, self.builder) |
| |
| @property |
| def config(self): |
| return self._builders[self.bucket]['builders'][self.builder] |
| |
| @property |
| def bot_type(self): |
| return self.config.get('bot_type', 'builder_tester') |
| |
| def triggered_bots(self): |
| for builder in self.config.get('triggers', []): |
| bucketname, buildername = builder.split('/') |
| yield (bucketname, buildername) |
| |
| @property |
| def recipe_config(self): |
| return self._recipe_configs[self.config['recipe_config']] |
| |
| @property |
| def test_suite(self): |
| return self.recipe_config.get('test_suite') |
| |
| @property |
| def should_build(self): |
| return self.bot_type in ('builder', 'builder_tester') |
| |
| @property |
| def should_test(self): |
| return self.bot_type in ('tester', 'builder_tester') |
| |
| @property |
| def should_upload_build(self): |
| return 'buildbot_triggers' in self.config |
| |
| @property |
| def should_download_build(self): |
| return self.config.get('parent_buildername') |
| |
| @property |
| def should_test_android_studio_project_generation(self): |
| return self.config.get('test_android_studio_project_generation', False) |
| |
| |
| |
| class WebRTCApi(recipe_api.RecipeApi): |
| WEBRTC_GS_BUCKET = WEBRTC_GS_BUCKET |
| |
| def __init__(self, **kwargs): |
| super(WebRTCApi, self).__init__(**kwargs) |
| self._env = {} |
| self._isolated_targets = None |
| |
| # Keep track of working directory (which contains the checkout). |
| # None means "default value". |
| self._working_dir = None |
| |
| self._builders = None |
| self._recipe_configs = None |
| self.bot = None |
| |
| self.revision = '' |
| self.revision_cp = '' |
| self.revision_number = '' |
| |
| BUILDERS = webrtc_builders.BUILDERS |
| RECIPE_CONFIGS = webrtc_builders.RECIPE_CONFIGS |
| |
| |
| def apply_bot_config(self, builders, recipe_configs): |
| self._builders = builders |
| self._recipe_configs = recipe_configs |
| |
| self.bot = self.get_bot(self.bucketname, self.buildername) |
| |
| self.set_config('webrtc', TEST_SUITE=self.bot.test_suite, |
| PERF_ID=self.bot.config.get('perf_id')) |
| |
| chromium_kwargs = self.bot.config.get('chromium_config_kwargs', {}) |
| if self.bot.recipe_config.get('chromium_android_config'): |
| self.m.chromium_android.set_config( |
| self.bot.recipe_config['chromium_android_config'], **chromium_kwargs) |
| |
| self.m.chromium.set_config(self.bot.recipe_config['chromium_config'], |
| **chromium_kwargs) |
| gclient_config = self.bot.recipe_config['gclient_config'] |
| self.m.gclient.set_config(gclient_config) |
| |
| # Support applying configs both at the bot and the recipe config level. |
| for c in self.bot.config.get('chromium_apply_config', []): |
| self.m.chromium.apply_config(c) |
| for c in self.bot.config.get('gclient_apply_config', []): |
| self.m.gclient.apply_config(c) |
| for c in self.bot.recipe_config.get('gclient_apply_config', []): |
| self.m.gclient.apply_config(c) |
| |
| if self.m.tryserver.is_tryserver: |
| self.m.chromium.apply_config('trybot_flavor') |
| |
| if self.bot.config.get('perf_id'): |
| assert not self.m.tryserver.is_tryserver |
| assert self.m.chromium.c.BUILD_CONFIG == 'Release', ( |
| 'Perf tests should only be run with Release builds.') |
| |
| @property |
| def bucketname(self): |
| if self.m.runtime.is_luci: |
| return self.m.buildbucket.bucket_v1 |
| else: |
| return self.m.properties.get('mastername') |
| |
| @property |
| def buildername(self): |
| return self.m.buildbucket.builder_name |
| |
| def get_bot(self, bucketname, buildername): |
| return Bot(self._builders, self._recipe_configs, bucketname, buildername) |
| |
| @property |
| def master_config(self): |
| return self._builders[self.bucketname].get('settings', {}) |
| |
| @property |
| def mastername(self): |
| return self.master_config.get('mastername', self.bucketname) |
| |
| def related_bots(self): |
| yield self.bot |
| for triggered_bot in self.bot.triggered_bots(): |
| yield self.get_bot(*triggered_bot) |
| |
| @property |
| def should_download_audio_quality_tools(self): |
| if not self.c.enable_swarming: |
| return 'perf' in self.bot.test_suite and self.bot.should_test |
| |
| for bot in self.related_bots(): |
| if 'perf' in bot.test_suite: |
| return self.bot.should_build |
| return False |
| |
| @property |
| def should_download_video_quality_tools(self): |
| if not self.c.enable_swarming: |
| return 'android_perf' in self.bot.test_suite and self.bot.should_test |
| |
| for bot in self.related_bots(): |
| if 'android_perf' in bot.test_suite: |
| return self.bot.should_build |
| return False |
| |
| def configure_isolate(self, phase=None): |
| if self.c.enable_swarming: |
| isolated_targets = set() |
| for bot in self.related_bots(): |
| if bot.should_test: |
| for test in steps.generate_tests( |
| self.m, phase, self.revision, self.revision_number, bot): |
| if isinstance(test, SwarmingTest): |
| isolated_targets.add(test._name) |
| |
| self._isolated_targets = sorted(isolated_targets) |
| |
| def configure_swarming(self): |
| self.c.enable_swarming = self.bot.config.get('enable_swarming') |
| if self.c.enable_swarming: |
| self.m.chromium_swarming.configure_swarming( |
| 'webrtc', |
| precommit=self.m.tryserver.is_tryserver, |
| mastername=self.mastername) |
| self.m.swarming.set_default_dimension( |
| 'os', |
| self.m.swarming.prefered_os_dimension( |
| self.m.platform.name).split('-', 1)[0]) |
| for key, value in self.bot.config.get( |
| 'swarming_dimensions', {}).iteritems(): |
| self.m.swarming.set_default_dimension(key, value) |
| if self.bot.config.get('swarming_timeout'): |
| self.m.swarming.default_hard_timeout = self.bot.config[ |
| 'swarming_timeout'] |
| self.m.swarming.default_io_timeout = self.bot.config['swarming_timeout'] |
| |
| def _apply_patch(self, repository_url, patch_ref, include_subdirs=()): |
| """Applies a patch by downloading the text diff from Gitiles.""" |
| with self.m.context(cwd=self.m.path['checkout']): |
| patch_diff = self.m.gitiles.download_file( |
| repository_url, '', patch_ref + '^!', |
| step_name='download patch', |
| step_test_data=self.test_api.example_patch) |
| |
| includes = ['--include=%s/*' % subdir for subdir in include_subdirs] |
| try: |
| self.m.git('apply', *includes, |
| stdin=self.m.raw_io.input_text(patch_diff), |
| name='apply patch', infra_step=False) |
| except recipe_api.StepFailure: # pragma: no cover |
| self.m.step.active_result.presentation.step_text = 'Patch failure' |
| self.m.tryserver.set_patch_failure_tryjob_result() |
| raise |
| |
| def checkout(self, **kwargs): |
| if (self.bot and self.bot.bot_type == 'tester' and |
| not self.m.properties.get('parent_got_revision')): |
| raise self.m.step.InfraFailure( |
| 'Testers must not be started without providing revision information.') |
| |
| self._working_dir = self.m.chromium_checkout.get_checkout_dir({}) |
| |
| is_chromium = self.m.tryserver.gerrit_change_repo_url == CHROMIUM_REPO |
| |
| if is_chromium: |
| for subdir in CHROMIUM_DEPS: |
| self.m.gclient.c.revisions['src/%s' % subdir] = 'HEAD' |
| |
| kwargs.setdefault('patch', False) |
| |
| with self.m.context(cwd=self.m.context.cwd or self._working_dir): |
| update_step = self.m.bot_update.ensure_checkout(**kwargs) |
| assert update_step.json.output['did_run'] |
| |
| # Whatever step is run right before this line needs to emit got_revision. |
| revs = update_step.presentation.properties |
| self.revision = revs['got_revision'] |
| self.revision_cp = revs['got_revision_cp'] |
| self.revision_number = str(self.m.commit_position.parse_revision( |
| self.revision_cp)) |
| |
| if is_chromium: |
| self._apply_patch(self.m.tryserver.gerrit_change_repo_url, |
| self.m.tryserver.gerrit_change_fetch_ref, |
| include_subdirs=CHROMIUM_DEPS) |
| |
| def download_audio_quality_tools(self): |
| with self.m.depot_tools.on_path(): |
| self.m.python('download audio quality tools', |
| self.m.path['checkout'].join('tools_webrtc', |
| 'download_tools.py'), |
| args=[self.m.path['checkout'].join('tools_webrtc', |
| 'audio_quality')]) |
| |
| def download_video_quality_tools(self): |
| with self.m.depot_tools.on_path(): |
| self.m.python('download video quality tools', |
| self.m.path['checkout'].join('tools_webrtc', |
| 'download_tools.py'), |
| args=[self.m.path['checkout'].join( |
| 'tools_webrtc', 'video_quality_toolchain', 'linux')]) |
| self.m.python('download apprtc', |
| self.m.depot_tools.download_from_google_storage_path, |
| args=['--bucket=chromium-webrtc-resources', |
| '--directory', |
| self.m.path['checkout'].join('rtc_tools', 'testing')]) |
| self.m.python('download golang', |
| self.m.depot_tools.download_from_google_storage_path, |
| args=['--bucket=chromium-webrtc-resources', |
| '--directory', |
| self.m.path['checkout'].join( |
| 'rtc_tools', 'testing', 'golang', 'linux')]) |
| |
| |
| def check_swarming_version(self): |
| if self.c.enable_swarming: |
| self.m.swarming.check_client_version() |
| |
| @classmethod |
| def _sanitize_dir_name(cls, name): |
| safe_with_spaces = ''.join(c if c.isalnum() else ' ' for c in name) |
| return '_'.join(safe_with_spaces.split()) |
| |
| def run_mb(self, phase=None): |
| if phase: |
| # Set the out folder to be the same as the phase name, so caches of |
| # consecutive builds don't interfere with each other. |
| self.m.chromium.c.build_config_fs = self._sanitize_dir_name(phase) |
| elif self.m.runtime.is_luci: |
| # Set the out folder to be the same as the builder name, so the whole |
| # 'src' folder can be shared between builder types. |
| self.m.chromium.c.build_config_fs = ( |
| self._sanitize_dir_name(self.buildername)) |
| |
| self.m.chromium.mb_gen( |
| self.mastername, self.buildername, phase=phase, use_goma=True, |
| mb_path=self.m.path['checkout'].join('tools_webrtc', 'mb'), |
| isolated_targets=self._isolated_targets) |
| |
| def compile(self, phase=None): |
| self.run_mb(phase) |
| |
| targets = self._isolated_targets |
| if targets: |
| targets = ['default'] + targets |
| |
| # TODO(oprypin): remove this after migration to swarming is done. |
| if self.bot.test_suite == 'android_perf': |
| targets = sorted(steps.ANDROID_PERF_TESTS) |
| |
| self.m.chromium.compile(targets=targets, use_goma_module=True) |
| |
| if self.c.enable_swarming: |
| self.m.isolate.isolate_tests(self.m.chromium.output_dir, |
| targets=self._isolated_targets) |
| |
| def get_binary_sizes(self, files=None, base_dir=None): |
| if files is None: |
| files = self.bot.config.get('binary_size_files') |
| if not files: |
| return |
| |
| result = self.m.python( |
| 'get binary sizes', |
| self.resource('binary_sizes.py'), |
| ['--base-dir', base_dir or self.m.chromium.output_dir, |
| '--output', self.m.json.output(), |
| '--'] + list(files), |
| infra_step=True, |
| step_test_data=self.test_api.example_binary_sizes) |
| result.presentation.properties['binary_sizes'] = result.json.output |
| |
| def runtests(self, phase=None): |
| """Add a suite of test steps. |
| |
| Args: |
| test_suite=The name of the test suite. |
| """ |
| with self.m.context(cwd=self._working_dir): |
| tests = steps.generate_tests( |
| self.m, phase, self.revision, self.revision_number, self.bot) |
| with self.m.step.defer_results(): |
| if tests: |
| run_android_device_steps = (not self.c.enable_swarming and |
| self.m.chromium.c.TARGET_PLATFORM == 'android') |
| |
| if run_android_device_steps: |
| self.m.chromium_android.common_tests_setup_steps() |
| |
| for test in tests: |
| test.pre_run(self.m, suffix='') |
| |
| # Build + upload archives while waiting for swarming tasks to finish. |
| if self.bot.config.get('build_android_archive'): |
| self.build_android_archive() |
| if self.bot.config.get('archive_apprtc'): |
| self.package_apprtcmobile() |
| |
| for test in tests: |
| test.run(self.m, suffix='') |
| |
| if run_android_device_steps: |
| self.m.chromium_android.common_tests_final_steps( |
| logcat_gs_bucket=self.master_config.get('build_gs_bucket'), |
| force_latest_version=True) |
| |
| def maybe_trigger(self): |
| properties = { |
| 'revision': self.revision, |
| 'parent_got_revision': self.revision, |
| 'parent_got_revision_cp': self.revision_cp, |
| } |
| |
| buildbot_triggers = self.bot.config.get('buildbot_triggers') |
| if buildbot_triggers: |
| self.m.trigger(*[{ |
| 'builder_name': builder_name, |
| 'properties': properties, |
| } for builder_name in buildbot_triggers]) |
| |
| triggered_bots = list(self.bot.triggered_bots()) |
| if triggered_bots: |
| if self.c.enable_swarming: |
| properties['swarm_hashes'] = self.m.isolate.isolated_tests |
| |
| self.m.scheduler.emit_trigger( |
| self.m.scheduler.BuildbucketTrigger(properties=properties), |
| project='webrtc', |
| jobs=[buildername for _, buildername in triggered_bots]) |
| |
| def package_build(self): |
| upload_url = self.m.archive.legacy_upload_url( |
| self.master_config.get('build_gs_bucket'), |
| extra_url_components=self.mastername) |
| self.m.archive.zip_and_upload_build( |
| 'package build', |
| self.m.chromium.c.build_config_fs, |
| upload_url, |
| build_revision=self.revision) |
| |
| def build_android_archive(self): |
| # Build the Android .aar archive and upload it to Google storage (except for |
| # trybots). This should only be run on a single bot or the archive will be |
| # overwritten (and it's a multi-arch build so one is enough). |
| goma_dir = self.m.goma.ensure_goma() |
| self.m.goma.start() |
| build_exit_status = 1 |
| try: |
| build_script = self.m.path['checkout'].join('tools_webrtc', 'android', |
| 'build_aar.py') |
| args = ['--use-goma', |
| '--verbose', |
| '--extra-gn-args', 'goma_dir=\"%s\"' % goma_dir] |
| if self.m.tryserver.is_tryserver: |
| # To benefit from incremental builds for speed. |
| args.append('--build-dir=out/android-archive') |
| |
| with self.m.context(cwd=self.m.path['checkout']): |
| with self.m.depot_tools.on_path(): |
| step_result = self.m.python( |
| 'build android archive', |
| build_script, |
| args=args, |
| ) |
| build_exit_status = step_result.retcode |
| except self.m.step.StepFailure as e: |
| build_exit_status = e.retcode |
| raise e |
| finally: |
| self.m.goma.stop(ninja_log_compiler='goma', |
| build_exit_status=build_exit_status) |
| |
| if not self.m.tryserver.is_tryserver and not self.m.runtime.is_experimental: |
| self.m.gsutil.upload( |
| self.m.path['checkout'].join('libwebrtc.aar'), |
| 'chromium-webrtc', |
| 'android_archive/webrtc_android_%s.aar' % self.revision_number, |
| args=['-a', 'public-read'], |
| unauthenticated_url=True) |
| |
| |
| def package_apprtcmobile(self): |
| # Zip and upload out/{Debug,Release}/apks/AppRTCMobile.apk |
| apk_root = self.m.chromium.c.build_dir.join( |
| self.m.chromium.c.build_config_fs, 'apks') |
| zip_path = self.m.path['start_dir'].join('AppRTCMobile_apk.zip') |
| |
| pkg = self.m.zip.make_package(apk_root, zip_path) |
| pkg.add_file(apk_root.join('AppRTCMobile.apk')) |
| pkg.zip('AppRTCMobile zip archive') |
| |
| apk_upload_url = 'client.webrtc/%s/AppRTCMobile_apk_%s.zip' % ( |
| self.buildername, self.revision_number) |
| if not self.m.runtime.is_experimental: |
| self.m.gsutil.upload(zip_path, WEBRTC_GS_BUCKET, apk_upload_url, |
| args=['-a', 'public-read'], unauthenticated_url=True) |
| |
| def extract_build(self): |
| if self.c.enable_swarming: |
| self.m.isolate.check_swarm_hashes(self._isolated_targets) |
| return |
| |
| # Ensure old build directory isn't being used by removing it. |
| self.m.file.rmtree( |
| 'build directory', |
| self.m.chromium.c.build_dir.join(self.m.chromium.c.build_config_fs)) |
| |
| download_url = self.m.archive.legacy_download_url( |
| self.master_config.get('build_gs_bucket'), |
| extra_url_components=self.mastername) |
| self.m.archive.download_and_unzip_build( |
| 'extract build', |
| self.m.chromium.c.build_config_fs, |
| download_url, |
| build_revision=self.revision) |
| |
| def cleanup(self): |
| self.clean_test_output() |
| if self.m.chromium.c.TARGET_PLATFORM == 'android': |
| self.m.chromium_android.clean_local_files(clean_pyc_files=False) |
| if self.c.enable_swarming: |
| self.m.isolate.clean_isolated_files(self.m.chromium.output_dir) |
| |
| def clean_test_output(self): |
| """Remove all test output in out/, since we have tests leaking files.""" |
| out_dir = self.m.path['checkout'].join('out') |
| self.m.python('clean test output files', |
| script=self.resource('cleanup_files.py'), |
| args=[out_dir], |
| infra_step=True) |