| OLD | NEW |
| 1 # Copyright 2015 The Chromium Authors. All rights reserved. | 1 # Copyright 2015 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 """An interface for holding state and result of revisions in a bisect job. | 5 """An interface for holding state and result of revisions in a bisect job. |
| 6 | 6 |
| 7 When implementing support for tests other than perf, one should extend this | 7 When implementing support for tests other than perf, one should extend this |
| 8 class so that the bisect module and recipe can use it. | 8 class so that the bisect module and recipe can use it. |
| 9 | 9 |
| 10 See perf_revision_state for an example. | 10 See perf_revision_state for an example. |
| 11 """ | 11 """ |
| 12 | 12 |
| 13 import hashlib | 13 import hashlib |
| 14 import json | 14 import json |
| 15 import math | 15 import math |
| 16 import os | 16 import os |
| 17 import tempfile | 17 import tempfile |
| 18 import re | 18 import re |
| 19 import uuid | 19 import uuid |
| 20 | 20 |
| 21 from . import depot_config | 21 from . import depot_config |
| 22 | 22 |
| 23 # These relate to how to increase the number of repetitions during re-test | 23 # These relate to how to increase the number of repetitions during re-test |
| 24 MINIMUM_SAMPLE_SIZE = 5 | 24 MINIMUM_SAMPLE_SIZE = 5 |
| 25 INCREASE_FACTOR = 1.5 | 25 INCREASE_FACTOR = 1.5 |
| 26 | 26 # Buildbot job result codes. |
| 27 # See http://docs.buildbot.net/current/developer/results.html |
| 28 SUCCESS, WARNINGS, FAILURE, SKIPPED, EXCEPTION = range(5) |
| 27 | 29 |
| 28 class RevisionState(object): | 30 class RevisionState(object): |
| 29 """Abstracts the state of a single revision on a bisect job.""" | 31 """Abstracts the state of a single revision on a bisect job.""" |
| 30 | 32 |
| 31 # Possible values for the status attribute of RevisionState: | 33 # Possible values for the status attribute of RevisionState: |
| 32 ( | 34 ( |
| 33 NEW, # A revision_state object that has just been initialized. | 35 NEW, # A revision_state object that has just been initialized. |
| 34 BUILDING, # Requested a build for this revision, waiting for it. | 36 BUILDING, # Requested a build for this revision, waiting for it. |
| 35 TESTING, # A test job for this revision was triggered, waiting for it. | 37 TESTING, # A test job for this revision was triggered, waiting for it. |
| 36 TESTED, # The test job completed with non-failing results. | 38 TESTED, # The test job completed with non-failing results. |
| (...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 84 self.deps = dict(base_revision.deps) | 86 self.deps = dict(base_revision.deps) |
| 85 self.deps[self.depot_name] = self.commit_hash | 87 self.deps[self.depot_name] = self.commit_hash |
| 86 else: | 88 else: |
| 87 self.needs_patch = False | 89 self.needs_patch = False |
| 88 self.build_url = self.bisector.get_platform_gs_prefix() + self._gs_suffix() | 90 self.build_url = self.bisector.get_platform_gs_prefix() + self._gs_suffix() |
| 89 self.values = [] | 91 self.values = [] |
| 90 self.mean_value = None | 92 self.mean_value = None |
| 91 self.std_dev = None | 93 self.std_dev = None |
| 92 self.repeat_count = MINIMUM_SAMPLE_SIZE | 94 self.repeat_count = MINIMUM_SAMPLE_SIZE |
| 93 self._test_config = None | 95 self._test_config = None |
| 96 self.build_number = None |
| 94 | 97 |
| 95 @property | 98 @property |
| 96 def tested(self): | 99 def tested(self): |
| 97 return self.status in (RevisionState.TESTED,) | 100 return self.status in (RevisionState.TESTED,) |
| 98 | 101 |
| 99 @property | 102 @property |
| 100 def in_progress(self): | 103 def in_progress(self): |
| 101 return self.status in (RevisionState.BUILDING, RevisionState.TESTING, | 104 return self.status in (RevisionState.BUILDING, RevisionState.TESTING, |
| 102 RevisionState.NEED_MORE_DATA) | 105 RevisionState.NEED_MORE_DATA) |
| 103 | 106 |
| (...skipping 108 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 212 | 215 |
| 213 def update_status(self): | 216 def update_status(self): |
| 214 """Checks on the pending jobs and updates status accordingly. | 217 """Checks on the pending jobs and updates status accordingly. |
| 215 | 218 |
| 216 This method will check for the build to complete and then trigger the test, | 219 This method will check for the build to complete and then trigger the test, |
| 217 or will wait for the test as appropriate. | 220 or will wait for the test as appropriate. |
| 218 | 221 |
| 219 To wait for the test we try to get the buildbot job url from GS, and if | 222 To wait for the test we try to get the buildbot job url from GS, and if |
| 220 available, we query the status of such job. | 223 available, we query the status of such job. |
| 221 """ | 224 """ |
| 222 if self.status == RevisionState.BUILDING and self._is_build_archived(): | 225 if self.status == RevisionState.BUILDING: |
| 223 self.start_job() | 226 if self._is_build_archived(): |
| 227 self.start_job() |
| 228 elif self._is_build_failed(): |
| 229 self.status = RevisionState.FAILED |
| 224 elif (self.status in (RevisionState.TESTING, RevisionState.NEED_MORE_DATA) | 230 elif (self.status in (RevisionState.TESTING, RevisionState.NEED_MORE_DATA) |
| 225 and self._results_available()): | 231 and self._results_available()): |
| 226 # If we have already decided whether the revision is good or bad we | 232 # If we have already decided whether the revision is good or bad we |
| 227 # shouldn't check again | 233 # shouldn't check again |
| 228 check_revision_goodness = not(self.good or self.bad) | 234 check_revision_goodness = not(self.good or self.bad) |
| 229 self._read_test_results( | 235 self._read_test_results( |
| 230 check_revision_goodness=check_revision_goodness) | 236 check_revision_goodness=check_revision_goodness) |
| 231 # We assume _read_test_results may have changed the status to a broken | 237 # We assume _read_test_results may have changed the status to a broken |
| 232 # state such as FAILED or ABORTED. | 238 # state such as FAILED or ABORTED. |
| 233 if self.status in (RevisionState.TESTING, RevisionState.NEED_MORE_DATA): | 239 if self.status in (RevisionState.TESTING, RevisionState.NEED_MORE_DATA): |
| 234 self.status = RevisionState.TESTED | 240 self.status = RevisionState.TESTED |
| 235 | 241 |
| 236 def _is_build_archived(self): | 242 def _is_build_archived(self): |
| 237 """Checks if the revision is already built and archived.""" | 243 """Checks if the revision is already built and archived.""" |
| 238 if not self.build_archived: | 244 if not self.build_archived: |
| 239 api = self.bisector.api | 245 api = self.bisector.api |
| 240 self.build_archived = api.gsutil_file_exists(self.build_url) | 246 self.build_archived = api.gsutil_file_exists(self.build_url) |
| 241 | 247 |
| 242 if self.bisector.dummy_builds: | 248 if self.bisector.dummy_builds: |
| 243 self.build_archived = self.in_progress | 249 self.build_archived = self.in_progress |
| 244 | 250 |
| 245 return self.build_archived | 251 return self.build_archived |
| 246 | 252 |
| 253 def _fetch_build_info(self, base_url, build_number): |
| 254 api = self.bisector.api |
| 255 build_url = '%s/builds/%s?as_text=1' % (base_url, build_number) |
| 256 fetch_result = api.m.url.fetch_to_file( |
| 257 build_url, None, step_name='fetch build details', |
| 258 stdout=api.m.raw_io.output()) |
| 259 return json.loads(fetch_result.stdout or '{}') |
| 260 |
| 261 def _is_build_failed(self): |
| 262 api = self.bisector.api |
| 263 current_build = None |
| 264 base_url = '%sjson/builders/%s'% ( |
| 265 # If this variable is not set assume local dev master. |
| 266 os.environ.get('BUILDBOT_URL', 'http://localhost:8041/'), |
| 267 self.bisector.get_builder_bot_for_this_platform()) |
| 268 if self.build_number is None: |
| 269 try: |
| 270 # Get all the current builds. |
| 271 builder_state_url = base_url + '?as_text=1' |
| 272 fetch_step = api.m.url.fetch_to_file( |
| 273 builder_state_url, None, |
| 274 step_name='fetch builder state', |
| 275 stdout=api.m.raw_io.output()) |
| 276 builder_state = fetch_step.stdout |
| 277 builder_state = json.loads(builder_state or '{}') |
| 278 for build_number in builder_state.get('cachedBuilds', []): |
| 279 build = self._fetch_build_info(base_url, build_number) |
| 280 build_properties = dict(build.get('properties', [])) |
| 281 if build_properties.get('build_archive_url') == self.build_url: |
| 282 self.build_number = build_number |
| 283 current_build = build |
| 284 break |
| 285 except api.m.step.StepFailure: # pragma: no cover |
| 286 # If we cannot get json from buildbot, we cannot determine if a build is |
| 287 # failed, hence we consider it in progress until it times out. |
| 288 return False |
| 289 if self.build_number is None: |
| 290 # The build hasn't started yet, therefore it's not failed. |
| 291 return False |
| 292 if not current_build: |
| 293 current_build = self._fetch_build_info(base_url, self.build_number) |
| 294 return current_build.get('results') in [FAILURE, SKIPPED, EXCEPTION] |
| 295 |
| 247 def _results_available(self): | 296 def _results_available(self): |
| 248 """Checks if the results for the test job have been uploaded.""" | 297 """Checks if the results for the test job have been uploaded.""" |
| 249 api = self.bisector.api | 298 api = self.bisector.api |
| 250 result = api.gsutil_file_exists(self.test_results_url) | 299 result = api.gsutil_file_exists(self.test_results_url) |
| 251 if self.bisector.dummy_builds: | 300 if self.bisector.dummy_builds: |
| 252 return self.in_progress | 301 return self.in_progress |
| 253 return result # pragma: no cover | 302 return result # pragma: no cover |
| 254 | 303 |
| 255 def _gs_suffix(self): | 304 def _gs_suffix(self): |
| 256 """Provides the expected right half of the build filename. | 305 """Provides the expected right half of the build filename. |
| (...skipping 53 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 310 file_name = str(api.m.path['tmp_base'].join(build_name + '.diff')) | 359 file_name = str(api.m.path['tmp_base'].join(build_name + '.diff')) |
| 311 api.m.file.write('Saving diff patch for ' + self.commit_hash, | 360 api.m.file.write('Saving diff patch for ' + self.commit_hash, |
| 312 file_name, self.deps_patch + self.deps_sha_patch) | 361 file_name, self.deps_patch + self.deps_sha_patch) |
| 313 return file_name | 362 return file_name |
| 314 | 363 |
| 315 def _request_build(self): | 364 def _request_build(self): |
| 316 """Posts a request to buildbot to build this revision and archive it.""" | 365 """Posts a request to buildbot to build this revision and archive it.""" |
| 317 # TODO: Rewrite using the trigger module. | 366 # TODO: Rewrite using the trigger module. |
| 318 api = self.bisector.api | 367 api = self.bisector.api |
| 319 bot_name = self.bisector.get_builder_bot_for_this_platform() | 368 bot_name = self.bisector.get_builder_bot_for_this_platform() |
| 320 if self.bisector.dummy_builds: | 369 if self.bisector.bisect_config.get('dummy_job_names'): |
| 321 self.job_name = self.commit_hash + '-build' | 370 self.job_name = self.commit_hash + '-build' |
| 322 else: # pragma: no cover | 371 else: # pragma: no cover |
| 323 self.job_name = uuid.uuid4().hex | 372 self.job_name = uuid.uuid4().hex |
| 324 if self.needs_patch: | 373 if self.needs_patch: |
| 325 self.patch_file = self._write_deps_patch_file( | 374 self.patch_file = self._write_deps_patch_file( |
| 326 self.job_name) | 375 self.job_name) |
| 327 else: | 376 else: |
| 328 self.patch_file = '/dev/null' | 377 self.patch_file = '/dev/null' |
| 329 | 378 |
| 330 # To allow multiple nested levels, we go to the topmost revision. | 379 # To allow multiple nested levels, we go to the topmost revision. |
| (...skipping 45 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 376 self._test_config = result | 425 self._test_config = result |
| 377 return result | 426 return result |
| 378 | 427 |
| 379 def _do_test(self): | 428 def _do_test(self): |
| 380 """Triggers tests for a revision, either locally or via try job. | 429 """Triggers tests for a revision, either locally or via try job. |
| 381 | 430 |
| 382 If local testing is enabled (i.e. director/tester merged) then | 431 If local testing is enabled (i.e. director/tester merged) then |
| 383 the test will be run on the same machine. Otherwise, this posts | 432 the test will be run on the same machine. Otherwise, this posts |
| 384 a request to buildbot to download and perf-test this build. | 433 a request to buildbot to download and perf-test this build. |
| 385 """ | 434 """ |
| 386 if self.bisector.dummy_builds: | 435 if self.bisector.bisect_config.get('dummy_job_names'): |
| 387 self.job_name = self.commit_hash + '-test' | 436 self.job_name = self.commit_hash + '-test' |
| 388 else: # pragma: no cover | 437 else: # pragma: no cover |
| 389 self.job_name = uuid.uuid4().hex | 438 self.job_name = uuid.uuid4().hex |
| 390 api = self.bisector.api | 439 api = self.bisector.api |
| 391 top_revision = self | 440 top_revision = self |
| 392 while top_revision.base_revision: # pragma: no cover | 441 while top_revision.base_revision: # pragma: no cover |
| 393 top_revision = top_revision.base_revision | 442 top_revision = top_revision.base_revision |
| 394 perf_test_properties = { | 443 perf_test_properties = { |
| 395 'builder_name': self.bisector.get_perf_tester_name(), | 444 'builder_name': self.bisector.get_perf_tester_name(), |
| 396 'properties': { | 445 'properties': { |
| (...skipping 116 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 513 key=lambda x: len(x.values)) | 562 key=lambda x: len(x.values)) |
| 514 if (len(self.bisector.last_tested_revision.values) == | 563 if (len(self.bisector.last_tested_revision.values) == |
| 515 next_revision_to_test.values): | 564 next_revision_to_test.values): |
| 516 self.bisector.last_tested_revision.retest() | 565 self.bisector.last_tested_revision.retest() |
| 517 else: | 566 else: |
| 518 next_revision_to_test.retest() | 567 next_revision_to_test.retest() |
| 519 | 568 |
| 520 def __repr__(self): | 569 def __repr__(self): |
| 521 return ('RevisionState(rev=%s, values=%r, mean_value=%r, std_dev=%r)' % ( | 570 return ('RevisionState(rev=%s, values=%r, mean_value=%r, std_dev=%r)' % ( |
| 522 self.revision_string(), self.values, self.mean_value, self.std_dev)) | 571 self.revision_string(), self.values, self.mean_value, self.std_dev)) |
| OLD | NEW |