Index: scripts/slave/recipe_modules/auto_bisect/revision_state.py |
diff --git a/scripts/slave/recipe_modules/auto_bisect/revision_state.py b/scripts/slave/recipe_modules/auto_bisect/revision_state.py |
index 4e0644bc4e14f7b9ef344d2258b9cfc6353f2b4d..fbd02936b17d4a00cbb58d531db0b78b0c4bd5be 100644 |
--- a/scripts/slave/recipe_modules/auto_bisect/revision_state.py |
+++ b/scripts/slave/recipe_modules/auto_bisect/revision_state.py |
@@ -10,6 +10,7 @@ class so that the bisect module and recipe can use it. |
See perf_revision_state for an example. |
""" |
+import collections |
import hashlib |
import json |
import math |
@@ -18,31 +19,18 @@ import tempfile |
import re |
import uuid |
-from . import depot_config |
+from auto_bisect import depot_config |
+from auto_bisect import bisect_exceptions |
# These relate to how to increase the number of repetitions during re-test |
MINIMUM_SAMPLE_SIZE = 5 |
INCREASE_FACTOR = 1.5 |
+NOT_SIGNIFICANTLY_DIFFERENT, SIGNIFICANTLY_DIFFERENT, NEED_MORE_DATA = range(3) |
dtu
2016/09/13 23:57:27
I think Nat or Tony? said that whenever you're goi
RobertoCN
2016/09/21 22:22:48
Done.
|
+ |
class RevisionState(object): |
"""Abstracts the state of a single revision on a bisect job.""" |
- # Possible values for the status attribute of RevisionState: |
- ( |
- NEW, # A revision_state object that has just been initialized. |
- BUILDING, # Requested a build for this revision, waiting for it. |
- TESTING, # A test job for this revision was triggered, waiting for it. |
- TESTED, # The test job completed with non-failing results. |
- FAILED, # Either the build or the test jobs failed or timed out. |
- ABORTED, # The build or test job was aborted. (For use in multi-secting). |
- SKIPPED, # A revision that was not built or tested for a special reason, |
- # such as those ranges that we know are broken, or when nudging |
- # revisions. |
- NEED_MORE_DATA, # Current number of test values is too small to establish |
- # a statistically significant difference between this |
- # revision and the revisions known to be good and bad. |
- ) = xrange(8) |
- |
def __init__(self, bisector, commit_hash, depot_name=None, |
base_revision=None): |
"""Creates a new instance to track the state of a revision. |
@@ -58,10 +46,10 @@ class RevisionState(object): |
super(RevisionState, self).__init__() |
self.bisector = bisector |
self._good = None |
+ self.failed = False |
self.deps = None |
self.test_results_url = None |
self.build_archived = False |
- self.status = RevisionState.NEW |
self.next_revision = None |
self.previous_revision = None |
self.job_name = None |
@@ -75,23 +63,18 @@ class RevisionState(object): |
self.revision_overrides = {} |
self.build_id = None |
if self.base_revision: |
- assert self.base_revision.deps_file_contents |
self.needs_patch = True |
self.revision_overrides[self.depot['src']] = self.commit_hash |
- self.deps_patch, self.deps_file_contents = self.bisector.make_deps_patch( |
- self.base_revision, self.base_revision.deps_file_contents, |
- self.depot, self.commit_hash) |
- self.deps_sha = hashlib.sha1(self.deps_patch).hexdigest() |
- self.deps_sha_patch = self.bisector.make_deps_sha_file(self.deps_sha) |
+ self.deps_sha = hashlib.sha1(self.revision_string()).hexdigest() |
self.deps = dict(base_revision.deps) |
self.deps[self.depot_name] = self.commit_hash |
else: |
self.needs_patch = False |
self.build_url = self.bisector.get_platform_gs_prefix() + self._gs_suffix() |
- self.values = [] |
- self.mean_value = None |
- self.overall_return_code = None |
- self.std_dev = None |
+ self.valueset_paths = [] |
+ self.chartjson_paths = [] |
+ self.debug_values = [] |
+ self.return_codes = [] |
self._test_config = None |
if self.bisector.test_type == 'perf': |
@@ -101,23 +84,6 @@ class RevisionState(object): |
'repeat_count', MINIMUM_SAMPLE_SIZE) |
@property |
- def tested(self): |
- return self.status in (RevisionState.TESTED,) |
- |
- @property |
- def in_progress(self): |
- return self.status in (RevisionState.BUILDING, RevisionState.TESTING, |
- RevisionState.NEED_MORE_DATA) |
- |
- @property |
- def failed(self): |
- return self.status == RevisionState.FAILED |
- |
- @property |
- def aborted(self): |
- return self.status == RevisionState.ABORTED |
- |
- @property |
def good(self): |
return self._good == True |
@@ -133,18 +99,51 @@ class RevisionState(object): |
def bad(self, value): |
self._good = not value |
+ @property |
+ def test_run_count(self): |
+ return max( |
+ len(self.valueset_paths), |
+ len(self.chartjson_paths), |
+ len(self.return_codes)) |
+ |
+ @property |
+ def mean(self): |
+ if self.debug_values: |
+ return float(sum(self.debug_values))/len(self.debug_values) |
+ |
+ @property |
+ def std_dev(self): |
+ if self.debug_values: |
+ mn = self.mean |
+ return math.sqrt(sum(pow(x - mn, 2) for x in self.debug_values)) |
+ |
def start_job(self): |
- """Starts a build, or a test job if the build is available.""" |
- if self.status == RevisionState.NEW and not self._is_build_archived(): |
- self._request_build() |
- self.status = RevisionState.BUILDING |
- return |
- |
- if self._is_build_archived() and self.status in ( |
- RevisionState.NEW, RevisionState.BUILDING, |
- RevisionState.NEED_MORE_DATA): |
+ api = self.bisector.api |
+ try: |
+ if not self._is_build_archived(): |
+ self._request_build() |
+ with api.m.step.nest('Waiting for build'): |
+ while not self._is_build_archived(): |
+ api.m.python.inline( |
+ 'sleeping', |
+ """ |
+ import sys |
+ import time |
+ time.sleep(20*60) |
+ sys.exit(0) |
+ """) |
+ if self._is_build_failed(): |
+ self.failed = True |
+ return |
+ |
self._do_test() |
- self.status = RevisionState.TESTING |
+ # TODO(robertocn): Add a test to remove this CL |
+ while not self._check_revision_good(): # pragma: no cover |
+ min(self, self.bisector.lkgr, self.bisector.fkbr, |
+ key=lambda(x): x.test_run_count)._do_test() |
+ |
+ except bisect_exceptions.UntestableRevisionException: |
+ self.failed = True |
def deps_change(self): |
"""Uses `git show` to see if a given commit contains a DEPS change.""" |
@@ -159,9 +158,9 @@ class RevisionState(object): |
name = 'Checking DEPS for ' + self.commit_hash |
step_result = api.m.git( |
'show', '--name-only', '--pretty=format:', |
- self.commit_hash, cwd=cwd, stdout=api.m.raw_io.output(), name=name) |
- if self.bisector.dummy_builds and not self.commit_hash.startswith('dcdc'): |
- return False |
+ self.commit_hash, cwd=cwd, stdout=api.m.raw_io.output(), name=name, |
+ step_test_data=lambda: api._test_data['deps_change'][self.commit_hash] |
+ ) |
if 'DEPS' in step_result.stdout.splitlines(): # pragma: no cover |
return True |
return False # pragma: no cover |
@@ -189,17 +188,15 @@ class RevisionState(object): |
def read_deps(self, recipe_tester_name): |
"""Sets the dependencies for this revision from the contents of DEPS.""" |
api = self.bisector.api |
- if self.deps: |
- return |
if self.bisector.internal_bisect: # pragma: no cover |
- self.deps_file_contents = self._read_content( |
+ deps_file_contents = self._read_content( |
depot_config.DEPOT_DEPS_NAME[self.depot_name]['url'], |
depot_config.DEPOT_DEPS_NAME[self.depot_name]['deps_file'], |
self.commit_hash) |
# On April 5th, 2016 .DEPS.git was changed to DEPS on android-chrome repo, |
# we are doing this in order to support both deps files. |
- if not self.deps_file_contents: |
- self.deps_file_contents = self._read_content( |
+ if not deps_file_contents: |
+ deps_file_contents = self._read_content( |
depot_config.DEPOT_DEPS_NAME[self.depot_name]['url'], |
depot_config.DEPS_FILENAME, |
self.commit_hash) |
@@ -208,11 +205,13 @@ class RevisionState(object): |
'fetch file %s:%s' % (self.commit_hash, depot_config.DEPS_FILENAME), |
api.resource('fetch_file.py'), |
[depot_config.DEPS_FILENAME, '--commit', self.commit_hash], |
- stdout=api.m.raw_io.output()) |
- self.deps_file_contents = step_result.stdout |
+ stdout=api.m.raw_io.output(), |
+ step_test_data=lambda: api._test_data['deps'][self.commit_hash] |
+ ) |
+ deps_file_contents = step_result.stdout |
try: |
deps_data = self._gen_deps_local_scope() |
- exec(self.deps_file_contents or 'deps = {}', {}, deps_data) |
+ exec (deps_file_contents or 'deps = {}') in {}, deps_data |
deps_data = deps_data['deps'] |
except ImportError: # pragma: no cover |
# TODO(robertocn): Implement manual parsing of DEPS when exec fails. |
@@ -243,40 +242,16 @@ class RevisionState(object): |
self.deps = results |
return |
- def update_status(self): |
- """Checks on the pending jobs and updates status accordingly. |
- |
- This method will check for the build to complete and then trigger the test, |
- or will wait for the test as appropriate. |
- |
- To wait for the test we try to get the buildbot job url from GS, and if |
- available, we query the status of such job. |
- """ |
- if self.status == RevisionState.BUILDING: |
- if self._is_build_archived(): |
- self.start_job() |
- elif self._is_build_failed(): # pragma: no cover |
- self.status = RevisionState.FAILED |
- elif (self.status in (RevisionState.TESTING, RevisionState.NEED_MORE_DATA) |
- and self._results_available()): |
- # If we have already decided whether the revision is good or bad we |
- # shouldn't check again |
- check_revision_goodness = not(self.good or self.bad) |
- self._read_test_results( |
- check_revision_goodness=check_revision_goodness) |
- # We assume _read_test_results may have changed the status to a broken |
- # state such as FAILED or ABORTED. |
- if self.status in (RevisionState.TESTING, RevisionState.NEED_MORE_DATA): |
- self.status = RevisionState.TESTED |
- |
def _is_build_archived(self): |
"""Checks if the revision is already built and archived.""" |
if not self.build_archived: |
api = self.bisector.api |
- self.build_archived = api.gsutil_file_exists(self.build_url) |
- |
- if self.bisector.dummy_builds: |
- self.build_archived = self.in_progress |
+ self.build_archived = api.gsutil_file_exists( |
+ self.build_url, |
+ step_test_data=lambda: api._test_data.get( |
+ 'gsutil_exists', {}).get(self.commit_hash).pop() |
+ if api._test_data.get('gsutil_exists', {}).get(self.commit_hash) |
+ else collections.namedtuple('retcode_attr', ['retcode'])(0)) |
dtu
2016/09/13 23:57:27
Maybe this is:
api._test_data.get('gsutil_exists'
RobertoCN
2016/09/21 22:22:48
That wouldn't work because after popping the last
|
return self.build_archived |
@@ -285,20 +260,11 @@ class RevisionState(object): |
result = api.m.buildbucket.get_build( |
self.build_id, |
api.m.service_account.get_json_path(api.SERVICE_ACCOUNT), |
- step_test_data=lambda: api.m.json.test_api.output_stream( |
- {'build': {'result': 'SUCCESS', 'status': 'COMPLETED'}} |
- )) |
+ step_test_data=lambda: api.test_api.buildbot_job_status_mock( |
+ api._test_data.get('build_status', {}).get(self.commit_hash, []))) |
return (result.stdout['build']['status'] == 'COMPLETED' and |
result.stdout['build'].get('result') != 'SUCCESS') |
- def _results_available(self): |
- """Checks if the results for the test job have been uploaded.""" |
- api = self.bisector.api |
- result = api.gsutil_file_exists(self.test_results_url) |
- if self.bisector.dummy_builds: |
- return self.in_progress |
- return result # pragma: no cover |
- |
def _gs_suffix(self): |
"""Provides the expected right half of the build filename. |
@@ -312,58 +278,23 @@ class RevisionState(object): |
name_parts.append(self.deps_sha) |
return '%s.zip' % '_'.join(name_parts) |
- def _read_test_results(self, check_revision_goodness=True): |
- """Gets the test results from GS and checks if the rev is good or bad.""" |
- test_results = self._get_test_results() |
- # Results will contain the keys 'results' and 'output' where output is the |
- # stdout of the command, and 'results' is itself a dict with the key |
- # 'values' unless the test failed, in which case 'results' will contain |
- # the 'error' key explaining the type of error. |
- results = test_results['results'] |
- if results.get('errors'): |
- self.status = RevisionState.FAILED |
- if 'MISSING_METRIC' in results.get('errors'): # pragma: no cover |
+ def _read_test_results(self, results): |
+ # Results will be a dictionary containing path to chartjsons, paths to |
+ # valueset, list of return codes. |
+ self.return_codes.extend(results.get('retcodes', [])) |
+ if results.get('errors'): # pragma: no cover |
+ self.failed = True |
+ if 'MISSING_METRIC' in results.get('errors'): |
self.bisector.surface_result('MISSING_METRIC') |
- return |
- self.values += results['values'] |
- api = self.bisector.api |
- if test_results.get('retcodes') and test_results['retcodes'][-1] != 0 and ( |
- api.m.chromium.c.TARGET_PLATFORM == 'android'): #pragma: no cover |
- api.m.chromium_android.device_status() |
- current_connected_devices = api.m.chromium_android.devices |
- current_device = api.m.bisect_tester.device_to_test |
- if current_device not in current_connected_devices: |
- # We need to manually raise step failure here because we are catching |
- # them further down the line to enable return_code bisects and bisecting |
- # on benchmarks that are a little flaky. |
- raise api.m.step.StepFailure('Test device disconnected.') |
- if self.bisector.is_return_code_mode(): |
- retcodes = test_results['retcodes'] |
- self.overall_return_code = 0 if all(v == 0 for v in retcodes) else 1 |
- # Keeping mean_value for compatibility with dashboard. |
- # TODO(robertocn): refactor mean_value, specially when uploading results |
- # to dashboard. |
- self.mean_value = self.overall_return_code |
- elif self.values: |
- api = self.bisector.api |
- self.mean_value = api.m.math_utils.mean(self.values) |
- self.std_dev = api.m.math_utils.standard_deviation(self.values) |
- # Values were not found, but the test did not otherwise fail. |
+ raise bisect_exceptions.UntestableRevisionException(results['errors']) |
+ elif self.bisector.is_return_code_mode(): |
+ assert len(results['retcodes']) |
else: |
- self.status = RevisionState.FAILED |
- self.bisector.surface_result('MISSING_METRIC') |
- return |
- # If we have already decided on the goodness of this revision, we shouldn't |
- # recheck it. |
- if self.good or self.bad: |
- check_revision_goodness = False |
- # We cannot test the goodness of the initial rev range. |
- if (self.bisector.good_rev != self and self.bisector.bad_rev != self and |
- check_revision_goodness): |
- if self._check_revision_good(): |
- self.good = True |
- else: |
- self.bad = True |
+ self.valueset_paths.extend(results.get('valueset_paths')) |
+ self.chartjson_paths.extend(results.get('chartjson_paths')) |
+ if results.get('retcodes') and 0 not in results['retcodes']: |
+ raise bisect_exceptions.UntestableRevisionException( |
+ 'got non-zero return code on all runs for ' + self.commit_hash) |
def _request_build(self): |
"""Posts a request to buildbot to build this revision and archive it.""" |
@@ -431,10 +362,10 @@ class RevisionState(object): |
the test will be run on the same machine. Otherwise, this posts |
a request to buildbot to download and perf-test this build. |
""" |
- if self.bisector.bisect_config.get('dummy_job_names'): |
- self.job_name = self.commit_hash + '-test' |
- else: # pragma: no cover |
- self.job_name = uuid.uuid4().hex |
+ if self.test_run_count: # pragma: no cover |
+ self.repeat_count = max(MINIMUM_SAMPLE_SIZE, math.ceil( |
+ self.test_run_count * 1.5)) - self.test_run_count |
+ |
api = self.bisector.api |
# Stores revision map for different repos eg, android-chrome, src, v8 etc. |
revision_ladder = {} |
@@ -444,102 +375,82 @@ class RevisionState(object): |
revision_ladder[top_revision.depot_name] = top_revision.commit_hash |
top_revision = top_revision.base_revision |
perf_test_properties = { |
- 'builder_name': self.bisector.get_perf_tester_name(), |
'properties': { |
'revision': top_revision.commit_hash, |
'parent_got_revision': top_revision.commit_hash, |
'parent_build_archive_url': self.build_url, |
'bisect_config': self._get_bisect_config_for_tester(), |
- 'job_name': self.job_name, |
'revision_ladder': revision_ladder, |
}, |
} |
- self.test_results_url = (self.bisector.api.GS_RESULTS_URL + |
- self.job_name + '.results') |
- if (api.m.bisect_tester.local_test_enabled() or |
- self.bisector.internal_bisect): # pragma: no cover |
- skip_download = self.bisector.last_tested_revision == self |
- self.bisector.last_tested_revision = self |
- overrides = perf_test_properties['properties'] |
- api.run_local_test_run(overrides, skip_download=skip_download) |
- else: |
- step_name = 'Triggering test job for ' + self.commit_hash |
- api.m.trigger(perf_test_properties, name=step_name) |
- |
- def retest(self): # pragma: no cover |
- # We need at least 5 samples for applying Mann-Whitney U test |
- # with P < 0.01, two-tailed . |
- target_sample_size = max(5, math.ceil(len(self.values) * 1.5)) |
- self.status = RevisionState.NEED_MORE_DATA |
- self.repeat_count = target_sample_size - len(self.values) |
- self.start_job() |
- self.bisector.wait_for(self) |
- |
- def _get_test_results(self): |
- """Tries to get the results of a test job from cloud storage.""" |
- api = self.bisector.api |
- try: |
- stdout = api.m.raw_io.output() |
- name = 'Get test results for build ' + self.commit_hash |
- step_result = api.m.gsutil.cat(self.test_results_url, stdout=stdout, |
- name=name) |
- if not step_result.stdout: |
- raise api.m.step.StepFailure('Test for build %s failed' % |
- self.revision_string()) |
- except api.m.step.StepFailure as sf: # pragma: no cover |
- self.bisector.surface_result('TEST_FAILURE') |
- return {'results': {'errors': str(sf)}} |
- else: |
- return json.loads(step_result.stdout) |
+ skip_download = self.bisector.last_tested_revision == self |
+ self.bisector.last_tested_revision = self |
+ overrides = perf_test_properties['properties'] |
- def _check_revision_good(self): |
- """Determines if a revision is good or bad. |
+ def run_test_step_test_data(): |
dtu
2016/09/13 23:57:27
Hmm, this feels out of place. Is there a way to pu
RobertoCN
2016/09/21 22:22:48
That makes sense. I'll give it a try in a separate
|
+ """Returns a single step data object when called. |
- Iteratively increment the sample size of the revision being tested, the last |
- known good revision, and the first known bad revision until a relationship |
- of significant difference can be established betweeb the results of the |
- revision being tested and one of the other two. |
+ These are expected to be populated by the test_api. |
+ """ |
+ if api._test_data['run_results'].get(self.commit_hash): |
+ return api._test_data['run_results'][self.commit_hash].pop(0) |
+ return api._test_data['run_results']['default'] |
- If the results do not converge towards finding a significant difference in |
- either direction, this is expected to timeout eventually. This scenario |
- should be rather rare, since it is expected that the fkbr and lkgr are |
- significantly different as a precondition. |
+ self._read_test_results(api.run_local_test_run( |
+ overrides, skip_download=skip_download, |
+ step_test_data=run_test_step_test_data |
+ )) |
+ |
+ def _check_revision_good(self): # pragma: no cover |
+ """Determines if a revision is good or bad. |
Returns: |
- True if the results of testing this revision are significantly different |
- from those of testing the earliest known bad revision. |
- False if they are instead significantly different form those of testing |
- the latest knwon good revision. |
+ True if the revision is either good or bad, False if it cannot be |
+ determined from the available data. |
""" |
+ # Do not reclassify revisions. Important for reference range. |
+ if self.good or self.bad: |
+ return True |
+ |
lkgr = self.bisector.lkgr |
fkbr = self.bisector.fkbr |
- |
if self.bisector.is_return_code_mode(): |
- return self.overall_return_code == lkgr.overall_return_code |
- |
- while True: |
- diff_from_good = self.bisector.significantly_different( |
- lkgr.values[:len(fkbr.values)], self.values) |
- diff_from_bad = self.bisector.significantly_different( |
- fkbr.values[:len(lkgr.values)], self.values) |
- |
- if diff_from_good and diff_from_bad: |
- # Multiple regressions. |
- # For now, proceed bisecting the biggest difference of the means. |
- dist_from_good = abs(self.mean_value - lkgr.mean_value) |
- dist_from_bad = abs(self.mean_value - fkbr.mean_value) |
- if dist_from_good > dist_from_bad: |
- # TODO(robertocn): Add way to handle the secondary regression |
- #self.bisector.handle_secondary_regression(self, fkbr) |
- return False |
- else: |
- #self.bisector.handle_secondary_regression(lkgr, self) |
- return True |
- |
- if diff_from_good or diff_from_bad: # pragma: no cover |
- return diff_from_bad |
+ if self.overall_return_code == lkgr.overall_return_code: |
+ self.good = True |
+ else: |
+ self.bad = True |
+ return True |
+ diff_from_good = self.bisector.compare_revisions(self, lkgr) |
+ diff_from_bad = self.bisector.compare_revisions(self, fkbr) |
+ if (diff_from_good == NOT_SIGNIFICANTLY_DIFFERENT and |
+ diff_from_bad == NOT_SIGNIFICANTLY_DIFFERENT): |
+ # We have reached the max number of samples and have not established |
+ # difference, give up. |
+ raise bisect_exceptions.InconclusiveBisectException() |
+ if (diff_from_good == SIGNIFICANTLY_DIFFERENT and |
+ diff_from_bad == SIGNIFICANTLY_DIFFERENT): |
+ # Multiple regressions. |
+ # For now, proceed bisecting the biggest difference of the means. |
+ dist_from_good = abs(self.mean - lkgr.mean) |
+ dist_from_bad = abs(self.mean - fkbr.mean) |
+ if dist_from_good > dist_from_bad: |
+ # TODO(robertocn): Add way to handle the secondary regression |
+ #self.bisector.handle_secondary_regression(self, fkbr) |
+ self.bad = True |
+ return True |
+ else: |
+ #self.bisector.handle_secondary_regression(lkgr, self) |
+ self.good = True |
+ return True |
+ if diff_from_good == SIGNIFICANTLY_DIFFERENT: |
+ self.bad = True |
+ return True |
+ elif diff_from_bad == SIGNIFICANTLY_DIFFERENT: |
+ self.good = True |
+ return True |
+ # NEED_MORE_DATA |
+ return False |
- self._next_retest() # pragma: no cover |
def revision_string(self): |
if self._rev_str: |
@@ -558,25 +469,24 @@ class RevisionState(object): |
self._rev_str = result |
return self._rev_str |
- def _next_retest(self): # pragma: no cover |
- """Chooses one of current, lkgr, fkbr to retest. |
- |
- Look for the smallest sample and retest that. If the last tested revision |
- is tied for the smallest sample, use that to take advantage of the fact |
- that it is already downloaded and unzipped. |
- """ |
- next_revision_to_test = min(self.bisector.lkgr, self, self.bisector.fkbr, |
- key=lambda x: len(x.values)) |
- if (len(self.bisector.last_tested_revision.values) == |
- next_revision_to_test.values): |
- self.bisector.last_tested_revision.retest() |
- else: |
- next_revision_to_test.retest() |
- |
- def __repr__(self): |
- if self.overall_return_code is not None: |
- return ('RevisionState(rev=%s, values=%r, overall_return_code=%r, ' |
- 'std_dev=%r)') % (self.revision_string(), self.values, |
+ def __repr__(self): # pragma: no cover |
+ if not self.test_run_count: |
+ return ('RevisionState(rev=%s), values=[]' % self.revision_string()) |
+ if self.bisector.is_return_code_mode(): |
+ return ('RevisionState(rev=%s, mean=%r, overall_return_code=%r, ' |
+ 'std_dev=%r)') % (self.revision_string(), self.mean, |
self.overall_return_code, self.std_dev) |
- return ('RevisionState(rev=%s, values=%r, mean_value=%r, std_dev=%r)' % ( |
- self.revision_string(), self.values, self.mean_value, self.std_dev)) |
+ return ('RevisionState(rev=%s, mean_value=%r, std_dev=%r)' % ( |
+ self.revision_string(), self.mean, self.std_dev)) |
+ |
+ @property |
+ def overall_return_code(self): |
+ if self.bisector.is_return_code_mode(): |
+ if self.return_codes: |
+ if max(self.return_codes): |
+ return 1 |
+ return 0 |
+ raise ValueError('overall_return_code needs non-empty sample' |
+ ) # pragma: no cover |
+ raise ValueError('overall_return_code only applies to return_code bisects' |
+ ) # pragma: no cover |