| # 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. |
| |
| import ast |
| import os |
| import re |
| |
| from recipe_engine import recipe_api |
| |
| |
| class SyzygyApi(recipe_api.RecipeApi): |
| # Used for constructing URLs to the Syzygy archives. |
| _SYZYGY_ARCHIVE_URL = ( |
| 'https://syzygy-archive.commondatastorage.googleapis.com') |
| _SYZYGY_GS = 'gs://syzygy-archive' |
| _SYZYGY_GITHUB = ('https://chromium.googlesource.com/external/' + |
| 'github.com/google/syzygy.git/+/') |
| |
| # Fake unittests.gypi contents. |
| _FAKE_UNITTESTS_GYPI_DATA = repr({ |
| 'variables': { |
| 'unittests': [ |
| '<(src)/syzygy/agent/asan/asan.gyp:foo_unittests', |
| '<(src)/syzygy/agent/common/common.gyp:bar_unittests', |
| '<(src)/syzygy/agent/coverage/coverage.gyp:baz_unittests', |
| ] |
| } |
| }) |
| |
| # Fake version file data. |
| _FAKE_VERSION_DATA = """# Copyright 2012 Google Inc. All Rights Reserved. |
| # |
| # Boilerplate! |
| # |
| # http://url/to/nowhere |
| # |
| # And some more boilerplate, followed by a blank line! |
| |
| MAJOR=0 |
| MINOR=0 |
| BUILD=0 |
| PATCH=1 |
| """ |
| |
| def __init__(self, *args, **kwargs): |
| super(SyzygyApi, self).__init__(*args, **kwargs) |
| # This is populated by the first call to 'version'. |
| self._version = None |
| # This is populated by the sync step. |
| self._revision = None |
| |
| def get_config_defaults(self): |
| return { |
| 'CHECKOUT_PATH': self.m.path['checkout'], |
| } |
| |
| @property |
| def build_dir(self): |
| """Returns the build directory for the project.""" |
| return self.m.path['checkout'].join('out') |
| |
| @property |
| def public_scripts_dir(self): |
| """Returns the public Syzygy build scripts directory.""" |
| return self.m.path['checkout'].join('syzygy', 'build') |
| |
| @property |
| def internal_scripts_dir(self): |
| """Returns the internal Syzygy build scripts directory.""" |
| return self.m.path['checkout'].join('syzygy', 'internal', 'build') |
| |
| @property |
| def version(self): |
| """Returns the version tuple associated with the checkout.""" |
| # Only read the value if it hasn't yet been read. |
| if not self._version: |
| version = self.c.version_file |
| version = self.m.file.read_text('read_version', version, |
| test_data=self._FAKE_VERSION_DATA) |
| d = {} |
| for l in version.splitlines(): |
| # Look for a 'NAME=VALUE' pair. |
| m = re.match('^\s*([A-Z]+)\s*=\s*(\d+)\s*$', l) |
| if not m: |
| continue |
| key = m.group(1) |
| value = m.group(2) |
| d[key] = int(value) |
| self._version = (d['MAJOR'], d['MINOR'], d['BUILD'], d['PATCH']) |
| |
| # Return the cached value. |
| return self._version |
| |
| @property |
| def revision(self): |
| """Returns the revision that is inferred by the gclient step. |
| |
| If this is not yet set then returns the global 'revision' property. If this |
| is not yet set, then simply returns an empty string. |
| """ |
| r = '' |
| if 'revision' in self.m.properties: |
| r = self.m.properties['revision'] |
| if self._revision: |
| r = self._revision |
| return r |
| |
| def _gen_step_gs_util_cp_dir(self, step_name, src_dir, dst_rel_path): |
| """Returns a gsutil_cp_dir step. Internal use only. |
| |
| Args: |
| step_name: The step name as a string. |
| src_dir: The source directory on the local file system. This should be a |
| Path object. |
| dst_rel_path: The destination path relative to the syzygy_archive root. |
| This should be a string. |
| |
| Returns: |
| The generated python step. |
| """ |
| gsutil_cp_dir_py = self.package_repo_resource( |
| 'scripts', 'slave', 'syzygy', 'gsutil_cp_dir.py') |
| dst_dir = '%s/%s' % (self._SYZYGY_GS, dst_rel_path) |
| args = ['--public-read', src_dir, dst_dir] |
| return self.m.build.python(step_name, gsutil_cp_dir_py, args) |
| |
| def _gen_step_gs_util_cp(self, step_name, src_path, dst_rel_path): |
| """Returns a gsutil.py step. Internal use only. |
| |
| Args: |
| step_name: The step name as a string. |
| src_path: The source path on the local file system. This should be a |
| Path object. |
| dst_rel_path: The destination path relative to the syzygy_archive root. |
| This should be a string. |
| |
| Returns: |
| The generated python step. |
| """ |
| gsutil_bat = self.package_repo_resource( |
| 'scripts', 'slave', 'gsutil.bat') |
| dst_dir = '%s/%s' % (self._SYZYGY_GS, dst_rel_path) |
| args = ['cp', '-t', '-a', 'public-read', src_path, dst_dir] |
| return self.m.step(step_name, [gsutil_bat] + list(args or [])) |
| |
| def taskkill(self): |
| """Run chromium.taskkill. |
| |
| This invokes a dummy step on the test slave as killing all instances of |
| Chrome seriously impairs development. |
| """ |
| if self.m.properties['bot_id'] == 'fake_slave': |
| return self.m.python.inline('taskkill', 'print "dummy taskkill"') |
| return self.m.chromium.taskkill() |
| |
| def checkout(self): |
| """Checks out the Syzygy code using the current gclient configuration.""" |
| step = self.m.bot_update.ensure_checkout() |
| self._revision = step.presentation.properties['got_revision'] |
| github_url = self._SYZYGY_GITHUB + str(self._revision) |
| step.presentation.links[str(self._revision)] = github_url |
| return step |
| |
| def runhooks(self): |
| return self.m.chromium.runhooks() |
| |
| def compile(self): |
| """Generates a step to compile the project.""" |
| # TODO(chrisha): Migrate this to Ninja! |
| return self.m.chromium.compile() |
| |
| def read_unittests_gypi(self): |
| """Reads and parses unittests.gypi from the checkout, returning a list.""" |
| gypi = self.c.unittests_gypi |
| gypi = self.m.file.read_text('read_unittests_gypi', gypi, |
| test_data=self._FAKE_UNITTESTS_GYPI_DATA) |
| gypi = ast.literal_eval(gypi) |
| unittests = [t.split(':')[1] for t in gypi['variables']['unittests']] |
| return sorted(unittests) |
| |
| def run_unittests(self, unittests): |
| # Set up the environment. This ensures that the tests emit metrics to a |
| # global log. |
| # TODO(chrisha): Make this use JSON, and make the log be specified on the |
| # command-line. |
| os.environ['SYZYGY_UNITTEST_METRICS'] = '--emit-to-log' |
| |
| # Generate a test step for each unittest. |
| for unittest in unittests: |
| unittest_path = self.m.chromium.output_dir.join(unittest + '.exe') |
| args = ['--single-process-tests', # Our VMs are single core. |
| '--test-launcher-timeout=300000', # 5 minutes in milliseconds. |
| '--gtest_print_time'] |
| self.m.chromium.runtest(unittest_path, args, name=unittest, |
| test_type=unittest) |
| |
| def randomly_reorder_chrome(self): |
| """Returns a test step that randomly reorders Chrome and ensures it runs.""" |
| randomize_chrome_py = self.internal_scripts_dir.join( |
| 'randomize_chrome.py') |
| args = ['--build-dir', self.build_dir, |
| '--target', self.m.chromium.c.BUILD_CONFIG, |
| '--verbose'] |
| return self.m.python('randomly_reorder_chrome', randomize_chrome_py, args) |
| |
| def benchmark_chrome(self): |
| """Returns a test step that benchmarks an optimized Chrome.""" |
| benchmark_chrome_py = self.internal_scripts_dir.join( |
| 'benchmark_chrome.py') |
| args = ['--build-dir', self.build_dir, |
| '--target', self.m.chromium.c.BUILD_CONFIG, |
| '--verbose'] |
| return self.m.python('benchmark_chrome', benchmark_chrome_py, args) |
| |
| def capture_unittest_coverage(self): |
| """Returns a step that runs the coverage script. |
| |
| Only meant to be called from the 'Coverage' configuration. |
| """ |
| assert self.m.chromium.c.BUILD_CONFIG == 'Coverage' |
| generate_coverage_py = self.public_scripts_dir.join( |
| 'generate_coverage.py') |
| args = ['--verbose', |
| '--syzygy', |
| '--build-dir', self.m.chromium.output_dir] |
| return self.m.python( |
| 'capture_unittest_coverage', generate_coverage_py, args) |
| |
| def archive_coverage(self): |
| """Returns a step that archives the coverage report. |
| |
| Only meant to be called from the 'Coverage' configuration. |
| """ |
| assert self.m.chromium.c.BUILD_CONFIG == 'Coverage' |
| cov_dir = self.m.chromium.output_dir.join('cov') |
| archive_path = 'builds/coverage/%s' % self.revision |
| if self.m.properties['bot_id'] == 'fake_slave': |
| archive_path = 'test/' + archive_path |
| report_url = '%s/%s/index.html' % (self._SYZYGY_ARCHIVE_URL, archive_path) |
| step = self._gen_step_gs_util_cp_dir( |
| 'archive_coverage', cov_dir, archive_path) |
| step.presentation.links['coverage_report'] = report_url |
| return step |
| |
| def archive_binaries(self): |
| """Returns a step that archives the official binaries. |
| |
| Only meant to be called from an official build. |
| """ |
| assert self.m.chromium.c.BUILD_CONFIG == 'Release' and self.c.official_build |
| bin_dir = self.m.chromium.output_dir.join('archive') |
| archive_path = 'builds/official/%s' % self.revision |
| if self.m.properties['bot_id'] == 'fake_slave': |
| archive_path = 'test/' + archive_path |
| bin_url = '%s/index.html?path=%s/' % ( |
| self._SYZYGY_ARCHIVE_URL, archive_path) |
| link_text = '.'.join(str(i) for i in self.version) + ' archive' |
| step = self._gen_step_gs_util_cp_dir( |
| 'archive_binaries', bin_dir, archive_path) |
| step.presentation.links[link_text] = bin_url |
| return step |
| |
| def upload_symbols(self): |
| """Returns a step that source indexes and uploads symbols. |
| |
| Only meant to be called from an official build. |
| """ |
| assert self.m.chromium.c.BUILD_CONFIG == 'Release' and self.c.official_build |
| archive_symbols_py = self.m.path['checkout'].join( |
| 'syzygy', 'internal', 'scripts', 'archive_symbols.py') |
| asan_rtl_dll = self.m.chromium.output_dir.join('*asan_rtl.dll') |
| client_dlls = self.m.chromium.output_dir.join('*client.dll') |
| args = ['-s', '-b', asan_rtl_dll, client_dlls] |
| return self.m.python('upload_symbols', archive_symbols_py, args) |
| |
| def upload_kasko_symbols(self): |
| """Returns a step that source indexes and uploads symbols for Kasko. |
| |
| Only meant to be called from an official build. |
| """ |
| assert self.m.chromium.c.BUILD_CONFIG == 'Release' and self.c.official_build |
| archive_symbols_py = self.m.path['checkout'].join( |
| 'syzygy', 'internal', 'scripts', 'archive_symbols.py') |
| kasko_dll = self.m.chromium.output_dir.join('*kasko.dll') |
| args = ['-s', '-b', kasko_dll] |
| return self.m.python('upload_symbols', archive_symbols_py, args) |
| |
| def clobber_metrics(self): |
| """Returns a step that clobbers an existing metrics file.""" |
| # TODO(chrisha): Make this whole thing use the JSON output mechanism. |
| return self.m.file.rmglob('clobber metrics', self.m.chromium.output_dir, |
| 'metrics.csv') |
| |
| def archive_metrics(self): |
| """Returns a step that archives any metrics collected by the unittests. |
| This can be called from any build configuration. |
| """ |
| # Determine the name of the archive. |
| config = self.m.chromium.c.BUILD_CONFIG |
| if config == 'Release' and self.c.official_build: |
| config = 'Official' |
| archive_path = 'builds/metrics/%s/%s.csv' % (self.revision, config.lower()) |
| step = self._gen_step_gs_util_cp( |
| 'archive_metrics', self.m.chromium.output_dir.join('metrics.csv'), |
| archive_path) |
| url = '%s/index.html?path=%s/' % ( |
| self._SYZYGY_ARCHIVE_URL, archive_path) |
| step.presentation.links['archive'] = url |
| return step |
| |
| def download_binaries(self): |
| """Returns a step that downloads the current official binaries.""" |
| get_syzygy_binaries_py = self.public_scripts_dir.join( |
| 'get_syzygy_binaries.py') |
| output_dir = self.m.path['checkout'].join('syzygy', 'binaries') |
| args = ['--output-dir', output_dir, |
| '--revision', self.revision, |
| '--overwrite', |
| '--verbose'] |
| return self.m.python('download_binaries', get_syzygy_binaries_py, args) |
| |
| def smoke_test(self): |
| """Returns a step that launches the smoke test script.""" |
| smoke_test_py = self.internal_scripts_dir.join('smoke_test.py') |
| build_dir = self.m.path['checkout'].join('build') |
| args = ['--verbose', '--build-dir', build_dir] |
| return self.m.python('smoke_test', smoke_test_py, args) |