| # Copyright 2015 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 itertools |
| import json |
| import re |
| from recipe_engine.types import freeze |
| |
| |
| # pylint: disable=abstract-method |
| |
| class V8Variant(object): |
| """Immutable class representing testing variants passed to v8.""" |
| def __init__(self, *variants): |
| self.variants = variants |
| |
| def __str__(self): |
| return ' '.join(self.variants) |
| |
| def pack(self): |
| """Returns a serializable version of this object. |
| |
| This method is the counterpart to the method below. |
| """ |
| return str(self) |
| |
| @staticmethod |
| def unpack(packed): |
| """Constructs a variant object from a serialized version of this class. |
| |
| This method is the counterpart to the method above. |
| """ |
| return V8Variant(*(packed or '').split(' ')) |
| |
| |
| def test_args_from_variants(*variants): |
| """Merge variant specification from bot, test type and test step. |
| |
| Returns: Flags for the v8 test driver with either 1) all specific |
| variants if any, or 2) flags for exhaustive testing. |
| """ |
| specific_variants = [v for v in variants if v] |
| if specific_variants: |
| _variants = sorted(list(set(itertools.chain( |
| *[v.variants for v in specific_variants])))) |
| else: |
| _variants = ['more', 'dev'] |
| assert _variants |
| return ['--variants=' + ','.join(_variants)] |
| |
| |
| TEST_CONFIGS = freeze({ |
| 'benchmarks': { |
| 'name': 'Benchmarks', |
| 'tests': ['benchmarks'], |
| }, |
| 'd8testing': { |
| 'name': 'Check - d8', |
| 'tests': ['d8_default'], |
| 'suite_mapping': [ |
| 'debugger', |
| 'intl', |
| 'message', |
| 'mjsunit', |
| 'preparser', |
| 'webkit', |
| ], |
| }, |
| 'd8testing_random_gc': { |
| 'name': 'Check - d8', |
| 'tests': ['d8_default'], |
| 'suite_mapping': [ |
| 'debugger', |
| 'intl', |
| 'message', |
| 'mjsunit', |
| 'preparser', |
| 'webkit', |
| ], |
| 'test_args': ['--random-gc-stress'], |
| }, |
| 'jsfunfuzz': { |
| 'tool': 'jsfunfuzz', |
| 'isolated_target': 'jsfunfuzz', |
| }, |
| 'gcmole': { |
| 'tool': 'run-gcmole', |
| 'isolated_target': 'run-gcmole', |
| }, |
| 'mjsunit': { |
| 'name': 'Mjsunit', |
| 'tests': ['mjsunit'], |
| }, |
| 'mjsunit_sp_frame_access': { |
| 'name': 'Mjsunit - sp frame access', |
| 'tests': ['mjsunit'], |
| 'test_args': ['--extra-flags=--turbo_sp_frame_access'], |
| 'variants': V8Variant('default'), |
| }, |
| 'mozilla': { |
| 'name': 'Mozilla', |
| 'tests': ['mozilla'], |
| }, |
| 'numfuzz': { |
| 'name': 'Num Fuzz', |
| 'tool': 'run-num-fuzzer', |
| 'isolated_target': 'run-num-fuzzer', |
| 'idempotent': False, |
| 'use_random_seed': False, |
| 'variants': V8Variant('default'), |
| }, |
| 'optimize_for_size': { |
| 'name': 'OptimizeForSize', |
| 'tests': ['optimize_for_size'], |
| 'suite_mapping': [ |
| 'cctest', |
| 'debugger', |
| 'mjsunit', |
| 'inspector', |
| 'intl', |
| 'webkit', |
| ], |
| 'test_args': ['--extra-flags=--optimize-for-size'], |
| 'variants': V8Variant('default'), |
| }, |
| 'test262': { |
| 'name': 'Test262 - no variants', |
| 'tests': ['test262'], |
| 'variants': V8Variant('default'), |
| }, |
| 'test262_variants': { |
| 'name': 'Test262', |
| 'tests': ['test262'], |
| }, |
| 'unittests': { |
| 'name': 'Unittests', |
| 'tests': ['unittests'], |
| }, |
| 'v8initializers': { |
| 'tool': 'check-static-initializers', |
| 'isolated_target': 'check-static-initializers', |
| }, |
| 'v8testing': { |
| 'name': 'Check', |
| 'tests': ['bot_default'], |
| 'suite_mapping': [ |
| 'cctest', |
| 'debugger', |
| 'fuzzer', |
| 'inspector', |
| 'intl', |
| 'message', |
| 'mjsunit', |
| 'mkgrokdump', |
| 'preparser', |
| 'unittests', |
| 'wasm-spec-tests', |
| 'webkit', |
| ], |
| }, |
| 'webkit': { |
| 'name': 'Webkit', |
| 'tests': ['webkit'], |
| }, |
| }) |
| |
| |
| class NullCoverageContext(object): |
| """Null object to represent testing without collecting coverage.""" |
| def get_test_runner_args(self): |
| return [] |
| |
| def get_swarming_collect_args(self): |
| return [] |
| |
| def setup(self): |
| pass |
| |
| def post_run(self): |
| pass |
| |
| def maybe_upload(self): |
| pass |
| |
| NULL_COVERAGE = NullCoverageContext() |
| |
| |
| class SanitizerCoverageContext(object): |
| """Context during testing to collect coverage data. |
| |
| Only testing on swarming is supported. |
| """ |
| def __init__(self, api): |
| self.api = api |
| self.coverage_dir = api.path.mkdtemp('coverage_output') |
| |
| def get_test_runner_args(self): |
| """Returns the test runner arguments for collecting coverage data.""" |
| return ['--sancov-dir', '${ISOLATED_OUTDIR}'] |
| |
| def get_swarming_collect_args(self): |
| """Returns the swarming collect step's arguments for merging.""" |
| return [ |
| '--coverage-dir', self.coverage_dir, |
| '--sancov-merger', self.api.path['checkout'].join( |
| 'tools', 'sanitizers', 'sancov_merger.py'), |
| ] |
| |
| def setup(self): |
| """Build data file with initial zero coverage data. |
| |
| To be called before any coverage data from testing is merged in. |
| """ |
| self.api.python( |
| 'Initialize coverage data', |
| self.api.path['checkout'].join( |
| 'tools', 'sanitizers', 'sancov_formatter.py'), |
| [ |
| 'all', |
| '--json-output', self.coverage_dir.join('data.json'), |
| ], |
| ) |
| |
| def post_run(self): |
| """Merge coverage data from one test run. |
| |
| To be called after every test step. Requires existing initial zero |
| coverage data, obtained by calling setup(). |
| """ |
| self.api.python( |
| 'Merge coverage data', |
| self.api.path['checkout'].join( |
| 'tools', 'sanitizers', 'sancov_formatter.py'), |
| [ |
| 'merge', |
| '--json-input', self.coverage_dir.join('data.json'), |
| '--json-output', self.coverage_dir.join('data.json'), |
| '--coverage-dir', self.coverage_dir, |
| ], |
| ) |
| |
| self.api.python.inline( |
| 'Purge sancov files', |
| """ |
| import glob |
| import os |
| for f in glob.glob('%s'): |
| os.remove(f) |
| """ % self.coverage_dir.join('*.sancov'), |
| ) |
| |
| def maybe_upload(self): |
| """Uploads coverage data to google storage if on tryserver.""" |
| |
| if self.api.tryserver.gerrit_change: |
| cl = self.api.tryserver.gerrit_change |
| results_path = 'tryserver/sanitizer_coverage/gerrit/%d/%d/%s%d' % ( |
| cl.change, cl.patchset, self.api.platform.name, self.api.v8.target_bits) |
| |
| |
| self.api.gsutil.upload( |
| self.coverage_dir.join('data.json'), |
| 'chromium-v8', |
| results_path + '/data.json', |
| ) |
| |
| data_dir = self.api.path.mkdtemp('coverage_data') |
| self.api.python( |
| 'Split coverage data', |
| self.api.path['checkout'].join( |
| 'tools', 'sanitizers', 'sancov_formatter.py'), |
| [ |
| 'split', |
| '--json-input', self.coverage_dir.join('data.json'), |
| '--output-dir', data_dir, |
| ], |
| # Allow to work with older v8 revisions that don't have the split |
| # function in which case the directory will stay empty. |
| # TODO(machenbach): Remove this when v8's passed CP 34834 + 1000. |
| ok_ret='any', |
| ) |
| |
| self.api.gsutil( |
| [ |
| '-m', 'cp', '-a', 'public-read', '-R', data_dir.join('*'), |
| 'gs://chromium-v8/%s/' % results_path, |
| ], |
| 'coverage data', |
| # Same as in the step above. |
| ok_ret='any', |
| ) |
| |
| |
| class BaseTest(object): |
| def __init__(self, test_step_config, api): |
| self.test_step_config = test_step_config |
| self.name = test_step_config.name |
| self.api = api |
| |
| @property |
| def id(self): |
| """Identifier for deduping identical test configs.""" |
| return self.test_step_config.name + self.test_step_config.step_name_suffix |
| |
| def _get_isolated_hash(self, test): |
| isolated = test.get('isolated_target') |
| if not isolated: |
| # Normally we run only one test and the isolate name is the same as the |
| # test name. |
| assert len(test['tests']) == 1 |
| isolated = test['tests'][0] |
| |
| isolated_hash = self.api.v8.isolated_tests.get(isolated) |
| |
| # TODO(machenbach): Maybe this is too hard. Implement a more forgiving |
| # solution. |
| assert isolated_hash |
| return isolated_hash |
| |
| @property |
| def uses_swarming(self): |
| """Returns true if the test uses swarming.""" |
| return False |
| |
| def apply_filter(self): |
| # Run all tests by default. |
| return True |
| |
| def pre_run(self, test=None, coverage_context=NULL_COVERAGE, **kwargs): |
| pass # pragma: no cover |
| |
| def run(self, test=None, coverage_context=NULL_COVERAGE, **kwargs): |
| raise NotImplementedError() # pragma: no cover |
| |
| def rerun(self, failure_dict, **kwargs): # pragma: no cover |
| raise NotImplementedError() |
| |
| |
| class V8Test(BaseTest): |
| def __init__(self, *args, **kwargs): |
| super(V8Test, self).__init__(*args, **kwargs) |
| self.applied_test_filter = '' |
| |
| def apply_filter(self): |
| test_config = self.api.v8.test_configs[self.name] |
| self.applied_test_filter = self.api.v8._applied_test_filter(test_config) |
| if self.api.v8.test_filter and not self.applied_test_filter: |
| self.api.step(test_config['name'] + ' - skipped', cmd=None) |
| return False |
| return True |
| |
| def run(self, test=None, coverage_context=NULL_COVERAGE, **kwargs): |
| test = test or self.api.v8.test_configs[self.name] |
| |
| full_args, env = self.api.v8._setup_test_runner( |
| test, self.applied_test_filter, self.test_step_config) |
| full_args += [ |
| '--json-test-results', |
| self.api.json.output(add_json_log=False), |
| ] |
| with self.api.context(cwd=self.api.path['checkout'], env=env): |
| self.api.python( |
| test['name'] + self.test_step_config.step_name_suffix, |
| self.api.path['checkout'].join('tools', 'run-tests.py'), |
| full_args, |
| step_test_data=self.api.v8.test_api.output_json, |
| **kwargs |
| ) |
| return self.post_run(test) |
| |
| def post_run(self, test, coverage_context=NULL_COVERAGE): |
| # The active step was either a local test run or the swarming collect step. |
| step_result = self.api.step.active_result |
| json_output = step_result.json.output |
| |
| # Log used test filters. |
| if self.applied_test_filter: |
| step_result.presentation.logs['test filter'] = self.applied_test_filter |
| |
| # The output is expected to be a list of architecture dicts that |
| # each contain a results list. On buildbot, there is only one |
| # architecture. |
| assert len(json_output) == 1 |
| self.api.v8._update_durations(json_output[0], step_result.presentation) |
| failure_factory = Failure.factory_func(self) |
| failure_log, failures, flake_log, flakes = ( |
| self.api.v8._get_failure_logs(json_output[0], failure_factory)) |
| self.api.v8._update_failure_presentation( |
| failure_log, failures, step_result.presentation) |
| |
| if failure_log and failures: |
| # Mark the test step as failure only if there were real failures (i.e. |
| # non-flakes) present. |
| step_result.presentation.status = self.api.step.FAILURE |
| |
| if flake_log and flakes: |
| # Emit a separate step to show flakes from the previous step |
| # to not close the tree. |
| step_result = self.api.step( |
| test['name'] + self.test_step_config.step_name_suffix + ' (flakes)', |
| cmd=None) |
| # TODO(sergiyb): Use WARNING result type after crbug.com/854099 is fixed. |
| step_result.presentation.status = self.api.step.FAILURE |
| self.api.v8._update_failure_presentation( |
| flake_log, flakes, step_result.presentation) |
| |
| coverage_context.post_run() |
| |
| return TestResults(failures, flakes, []) |
| |
| def _setup_rerun_config(self, failure_dict): |
| """Return: A test config that reproduces a specific failure.""" |
| # Make sure bisection is only activated on builders that give enough |
| # information to retry. |
| assert failure_dict.get('variant') |
| assert failure_dict.get('random_seed') |
| |
| orig_config = self.api.v8.test_configs[self.name] |
| |
| # If not specified, the isolated target is the same as the first test of |
| # the original list. We need to set it explicitly now, as the tests |
| # parameter changes on rerun, but the isolated target is still the same. |
| isolated_target = orig_config.get( |
| 'isolated_target', orig_config['tests'][0]) |
| |
| test_args = list(orig_config.get('test_args', [])) + [ |
| '--random-seed', failure_dict['random_seed'], |
| ] |
| |
| rerun_config = { |
| 'name': 'Retry', |
| 'isolated_target': isolated_target, |
| 'tests': [failure_dict['name']], |
| 'test_args': test_args, |
| 'variants': V8Variant(failure_dict['variant']) |
| } |
| |
| # Switch off test filters on rerun. |
| self.applied_test_filter = None |
| return rerun_config |
| |
| def rerun(self, failure_dict, **kwargs): |
| return self.run(test=self._setup_rerun_config(failure_dict), **kwargs) |
| |
| |
| def _trigger_swarming_task(api, task, test_step_config): |
| """Triggers a task on swarming setting custom dimensions and task attributes. |
| |
| Args: |
| api: Recipe modules api. |
| task: Task object from swarming recipe module. |
| test_step_config: Configuration object used to configure this task. Contains |
| e.g. dimension and task-attribute overrides. |
| """ |
| # Add custom dimensions. |
| task.dimensions.update(api.v8.bot_config.get('swarming_dimensions', {})) |
| |
| # Override with per-test dimensions. |
| task.dimensions.update(test_step_config.swarming_dimensions or {}) |
| |
| # Override cpu and gpu defaults for Android as such devices don't have these |
| # dimensions. |
| if task.dimensions['os'] == 'Android': |
| task.dimensions.pop('cpu') |
| task.dimensions.pop('gpu') |
| |
| # Override attributes with per-test settings. |
| for k, v in test_step_config.swarming_task_attrs.iteritems(): |
| setattr(task, k, v) |
| |
| api.swarming.trigger_task(task) |
| |
| |
| class V8SwarmingTest(V8Test): |
| def __init__(self, *args, **kwargs): |
| super(V8SwarmingTest, self).__init__(*args, **kwargs) |
| self.task = None |
| self.test = None |
| |
| @property |
| def uses_swarming(self): |
| """Returns true if the test uses swarming.""" |
| return True |
| |
| def _v8_collect_step(self, task, coverage_context=NULL_COVERAGE, **kwargs): |
| """Produces a step that collects and processes a result of a v8 task.""" |
| # Placeholder for the merged json output. |
| json_output = self.api.json.output(add_json_log=False) |
| |
| # Shim script's own arguments. |
| args = [ |
| '--swarming-client-dir', self.api.swarming_client.path, |
| '--temp-root-dir', self.api.path['tmp_base'], |
| '--merged-test-output', json_output, |
| ] + coverage_context.get_swarming_collect_args() |
| |
| # Arguments for actual 'collect' command. |
| args.append('--') |
| args.extend(self.api.swarming.get_collect_cmd_args_for_python(task)) |
| |
| return self.api.build.python( |
| name=self.test['name'] + self.test_step_config.step_name_suffix, |
| script=self.api.v8.resource('collect_v8_task.py'), |
| args=args, |
| allow_subannotations=True, |
| infra_step=True, |
| step_test_data=kwargs.pop('step_test_data', None), |
| **kwargs) |
| |
| def pre_run(self, test=None, coverage_context=NULL_COVERAGE, **kwargs): |
| # Set up arguments for test runner. |
| self.test = test or self.api.v8.test_configs[self.name] |
| extra_args, _ = self.api.v8._setup_test_runner( |
| self.test, self.applied_test_filter, self.test_step_config) |
| |
| # Let json results be stored in swarming's output folder. The collect |
| # step will copy the folder's contents back to the client. |
| extra_args += [ |
| '--swarming', |
| '--json-test-results', |
| '${ISOLATED_OUTDIR}/output.json', |
| ] + coverage_context.get_test_runner_args() |
| |
| # Initialize number of shards, either per test or per builder. |
| shards = 1 |
| if self.api.v8.c.testing.may_shard: |
| shards = self.test_step_config.shards |
| |
| command = 'tools/%s.py' % self.test.get('tool', 'run-tests') |
| idempotent = self.test.get('idempotent') |
| |
| # Initialize swarming task with custom data-collection step for v8 |
| # test-runner output. |
| self.task = self.api.swarming.task( |
| title=self.test['name'] + self.test_step_config.step_name_suffix, |
| idempotent=idempotent, |
| isolated_hash=self._get_isolated_hash(self.test), |
| shards=shards, |
| raw_cmd=[command] + extra_args, |
| ) |
| self.task.collect_step = lambda task, **kw: ( |
| self._v8_collect_step(task, coverage_context, **kw)) |
| |
| _trigger_swarming_task(self.api, self.task, self.test_step_config) |
| |
| def run(self, coverage_context=NULL_COVERAGE, **kwargs): |
| # TODO(machenbach): Soften this when softening 'assert isolated_hash' |
| # above. |
| assert self.task |
| result = TestResults.empty() |
| try: |
| # Collect swarming results. Use the same test simulation data for the |
| # swarming collect step like for local testing. |
| self.api.swarming.collect_task( |
| self.task, |
| step_test_data=self.api.v8.test_api.output_json, |
| ) |
| except self.api.step.InfraFailure as e: |
| result += TestResults.infra_failure(e) |
| |
| return result + self.post_run(self.test, coverage_context) |
| |
| def rerun(self, failure_dict, **kwargs): |
| self.pre_run(test=self._setup_rerun_config(failure_dict), **kwargs) |
| return self.run(**kwargs) |
| |
| |
| class V8GenericSwarmingTest(BaseTest): |
| # FIXME: BaseTest.rerun is an abstract method which isn't implemented in this |
| # class. Should it be abstract? |
| def __init__(self, test_step_config, api, title=None, command=None): |
| super(V8GenericSwarmingTest, self).__init__(test_step_config, api) |
| self._command = command or [] |
| self._title = ( |
| title or |
| self.api.v8.test_configs[self.name].get('name', 'Generic test')) |
| self.test = None |
| self.task = None |
| |
| @property |
| def title(self): |
| return self._title + self.test_step_config.step_name_suffix |
| |
| @property |
| def command(self): |
| """Command to pass to the swarming task.""" |
| return self._command |
| |
| @property |
| def task_output_dir(self): |
| return None # pragma: no cover |
| |
| @property |
| def uses_swarming(self): |
| """Returns true if the test uses swarming.""" |
| return True |
| |
| def pre_run(self, test=None, **kwargs): |
| self.test = test or self.api.v8.test_configs[self.name] |
| self.task = self.api.swarming.task( |
| title=self.title, |
| isolated_hash=self._get_isolated_hash(self.test), |
| task_output_dir=self.task_output_dir, |
| raw_cmd=self.command, |
| ) |
| |
| _trigger_swarming_task(self.api, self.task, self.test_step_config) |
| |
| def run(self, **kwargs): |
| assert self.task |
| self.api.swarming.collect_task(self.task) |
| return TestResults.empty() |
| |
| |
| class V8CompositeSwarmingTest(BaseTest): |
| # FIXME: BaseTest.rerun is an abstract method which isn't implemented in this |
| # class. Should it be abstract? |
| def __init__(self, *args, **kwargs): |
| super(V8CompositeSwarmingTest, self).__init__(*args, **kwargs) |
| self.composites = [] |
| |
| @property |
| def composite_tests(self): |
| """Returns: An iterable of V8GenericSwarmingTest instances.""" |
| raise NotImplementedError() # pragma: no cover |
| |
| @property |
| def uses_swarming(self): |
| """Returns true if the test uses swarming.""" |
| return True |
| |
| def pre_run(self, test=None, **kwargs): |
| self.composites = list(self.composite_tests) |
| for c in self.composites: |
| c.pre_run(test, **kwargs) |
| |
| def run(self, **kwargs): |
| for c in self.composites: |
| c.run(**kwargs) |
| return TestResults.empty() |
| |
| class V8CheckInitializers(V8GenericSwarmingTest): |
| @property |
| def title(self): |
| return 'Static-Initializers' |
| |
| @property |
| def command(self): |
| return [ |
| 'tools/check-static-initializers.sh', |
| self.api.v8.relative_path_to_d8, |
| ] |
| |
| |
| class V8Fuzzer(V8GenericSwarmingTest): |
| def __init__(self, test_step_config, api, title='Generic test', |
| command=None): |
| self.output_dir = api.path.mkdtemp('swarming_output') |
| self.archive = 'fuzz-results-%s.tar.bz2' % ( |
| api.properties['parent_got_revision']) |
| super(V8Fuzzer, self).__init__( |
| test_step_config, api, |
| title='Fuzz', |
| command=[ |
| 'tools/jsfunfuzz/fuzz-harness.sh', |
| api.v8.relative_path_to_d8, |
| '${ISOLATED_OUTDIR}/%s' % self.archive, |
| ], |
| ) |
| |
| @property |
| def task_output_dir(self): |
| return self.output_dir |
| |
| def run(self, **kwargs): |
| try: |
| super(V8Fuzzer, self).run(**kwargs) |
| except self.api.step.StepFailure as e: |
| self.api.gsutil.upload( |
| self.output_dir.join('0', self.archive), |
| 'chromium-v8', |
| self.api.path.join('fuzzer-archives', self.archive), |
| ) |
| raise e |
| return TestResults.empty() |
| |
| |
| class V8GCMole(V8CompositeSwarmingTest): |
| @property |
| def composite_tests(self): |
| return [ |
| V8GenericSwarmingTest( |
| self.test_step_config, self.api, |
| title='GCMole %s' % arch, |
| command=['tools/gcmole/run-gcmole.py', arch], |
| ) for arch in ['ia32', 'x64', 'arm', 'arm64'] |
| ] |
| |
| |
| TOOL_TO_TEST = freeze({ |
| 'run-tests': V8Test, |
| }) |
| |
| |
| TOOL_TO_TEST_SWARMING = freeze({ |
| 'check-static-initializers': V8CheckInitializers, |
| 'jsfunfuzz': V8Fuzzer, |
| 'run-gcmole': V8GCMole, |
| 'run-num-fuzzer': V8SwarmingTest, |
| 'run-tests': V8SwarmingTest, |
| }) |
| |
| |
| class Failure(object): |
| """Represents a test run leading to a failure (possibly re-run several times). |
| """ |
| def __init__(self, test, results): |
| """ |
| Args: |
| test: Test (type V8Test) that led to this failure. |
| results: List of failure dicts with one item per run of the test. The |
| first item is the original failure, the other items are re-runs for |
| flake checking. Each failure dict consists of the data as returned by |
| the V8-side test runner. |
| """ |
| assert results |
| assert test |
| self.test = test |
| self.results = results |
| # A failure is flaky if not all results are the same (e.g. all 'FAIL'). |
| self.is_flaky = not all( |
| x['result'] == results[0]['result'] for x in results) |
| |
| @property |
| def failure_dict(self): |
| return self.results[0] |
| |
| @property |
| def duration(self): |
| return self.failure_dict['duration'] |
| |
| @property |
| def test_step_config(self): |
| return self.test.test_step_config |
| |
| @property |
| def api(self): |
| return self.test.api |
| |
| def _format_swarming_dimensions(self, dims): |
| return ['%s:%s' % (k, dims[k]) for k in sorted(dims.keys())] |
| |
| def _flako_cmd_line(self): |
| """Returns the command line for bisecting this failure with flako.""" |
| test_config = self.api.v8.test_configs[self.test.name] |
| properties = { |
| # This assumes the builder's master is the same as the tester. |
| 'bisect_mastername': self.api.properties['mastername'], |
| # Use builds from parent builder to bisect if any. |
| 'bisect_buildername': self.api.properties.get( |
| 'parent_buildername') or self.api.buildbucket.builder_name, |
| # Start bisecting backwards at the revision that was tested. |
| 'to_revision': self.api.buildbucket.gitiles_commit.id, |
| # Use the same dimensions as the swarming task that ran this test. |
| 'swarming_dimensions': self._format_swarming_dimensions( |
| self.test.task.dimensions), |
| # The isolated name is either specified in the test configurations or |
| # corresponds to the name of the test suite. |
| 'isolated_name': test_config.get('isolated_target') or |
| test_config['tests'][0], |
| # Full qualified test name that failed (e.g. mjsunit/foo/bar). |
| 'test_name': self.results[0]['name'], |
| # Release or Debug. |
| 'build_config': self.api.chromium.c.build_config_fs, |
| # Add timeout default for convenience. |
| 'timeout_sec': 60, |
| # Add total timeout default for convenience. |
| 'total_timeout_sec': 120, |
| # The variant the failing test ran in. |
| 'variant': self.results[0]['variant'], |
| # Extra arguments passed to the V8 test runner. |
| # TODO(machenbach): The api should hide the details how to get the args. |
| 'extra_args': list(test_config.get('test_args', [])) + |
| list(self.api.v8.c.testing.test_args) + |
| list(self.test_step_config.test_args), |
| } |
| return ( |
| 'echo \'%s\' | buildbucket.py put -b luci.v8.try -n v8_flako -p -' |
| % json.dumps(properties, sort_keys=True) |
| ) |
| |
| def log_lines(self): |
| """Return a list of lines for logging all runs of this failure.""" |
| lines = [] |
| |
| # Add common description for multiple runs. |
| flaky_suffix = ' (flaky in a repeated run)' if self.is_flaky else '' |
| lines.append('Test: %s%s' % (self.results[0]['name'], flaky_suffix)) |
| lines.append('Flags: %s' % ' '.join(self.results[0]['flags'])) |
| lines.append('Command: %s' % self.results[0]['command']) |
| lines.append('Variant: %s' % self.results[0]['variant']) |
| lines.append('') |
| lines.append('Build environment:') |
| if self.api.v8.build_environment is None: |
| lines.append( |
| 'Not available. Please look up the builder\'s configuration.') |
| else: |
| for key in sorted(self.api.v8.build_environment): |
| lines.append(' %s: %s' % (key, self.api.v8.build_environment[key])) |
| lines.append('') |
| |
| # Print the command line for flake bisect if the test is flaky. Only |
| # supports tests run on swarming and CI. |
| if (self.is_flaky and |
| isinstance(self.test, V8SwarmingTest) and |
| not self.api.tryserver.is_tryserver): |
| lines.append('Trigger flake bisect on command line:') |
| lines.append(self._flako_cmd_line()) |
| lines.append('') |
| |
| # Add results for each run of a command. |
| for result in sorted(self.results, key=lambda r: int(r['run'])): |
| lines.append('Run #%d' % int(result['run'])) |
| lines.append('Exit code: %s' % result['exit_code']) |
| lines.append('Result: %s' % result['result']) |
| if result.get('expected'): |
| lines.append('Expected outcomes: %s' % ", ".join(result['expected'])) |
| lines.append( |
| 'Duration: %s' % self.api.v8.format_duration(result['duration'])) |
| lines.append('') |
| if result['stdout']: |
| lines.append('Stdout:') |
| lines.extend(result['stdout'].splitlines()) |
| lines.append('') |
| if result['stderr']: |
| lines.append('Stderr:') |
| lines.extend(result['stderr'].splitlines()) |
| lines.append('') |
| return lines |
| |
| @staticmethod |
| def factory_func(test): |
| def create(results): |
| return Failure(test, results) |
| return create |
| |
| |
| class TestResults(object): |
| def __init__(self, failures, flakes, infra_failures): |
| self.failures = failures |
| self.flakes = flakes |
| self.infra_failures = infra_failures |
| |
| @staticmethod |
| def empty(): |
| return TestResults([], [], []) |
| |
| @staticmethod |
| def infra_failure(exception): |
| return TestResults([], [], [exception]) |
| |
| @property |
| def is_negative(self): |
| return bool(self.failures or self.flakes or self.infra_failures) |
| |
| @property |
| def has_failures(self): |
| return bool(self.failures or self.infra_failures) |
| |
| def __add__(self, other): |
| return TestResults( |
| self.failures + other.failures, |
| self.flakes + other.flakes, |
| self.infra_failures + other.infra_failures, |
| ) |
| |
| |
| def create_test(test_step_config, api): |
| if api.v8.bot_config.get('enable_swarming', True): |
| tools_mapping = TOOL_TO_TEST_SWARMING |
| else: |
| tools_mapping = TOOL_TO_TEST |
| |
| # The tool the test is going to use. Default: V8 test runner (run-tests). |
| tool = api.v8.test_configs[test_step_config.name].get('tool', 'run-tests') |
| test_cls = tools_mapping[tool] |
| return test_cls(test_step_config, api) |