Chromium Code Reviews| 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. |
| (...skipping 73 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 84 self.deps = dict(base_revision.deps) | 84 self.deps = dict(base_revision.deps) |
| 85 self.deps[self.depot_name] = self.commit_hash | 85 self.deps[self.depot_name] = self.commit_hash |
| 86 else: | 86 else: |
| 87 self.needs_patch = False | 87 self.needs_patch = False |
| 88 self.build_url = self.bisector.get_platform_gs_prefix() + self._gs_suffix() | 88 self.build_url = self.bisector.get_platform_gs_prefix() + self._gs_suffix() |
| 89 self.values = [] | 89 self.values = [] |
| 90 self.mean_value = None | 90 self.mean_value = None |
| 91 self.std_dev = None | 91 self.std_dev = None |
| 92 self.repeat_count = MINIMUM_SAMPLE_SIZE | 92 self.repeat_count = MINIMUM_SAMPLE_SIZE |
| 93 self._test_config = None | 93 self._test_config = None |
| 94 self.build_number = None | |
| 94 | 95 |
| 95 @property | 96 @property |
| 96 def tested(self): | 97 def tested(self): |
| 97 return self.status in (RevisionState.TESTED,) | 98 return self.status in (RevisionState.TESTED,) |
| 98 | 99 |
| 99 @property | 100 @property |
| 100 def in_progress(self): | 101 def in_progress(self): |
| 101 return self.status in (RevisionState.BUILDING, RevisionState.TESTING, | 102 return self.status in (RevisionState.BUILDING, RevisionState.TESTING, |
| 102 RevisionState.NEED_MORE_DATA) | 103 RevisionState.NEED_MORE_DATA) |
| 103 | 104 |
| (...skipping 108 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 212 | 213 |
| 213 def update_status(self): | 214 def update_status(self): |
| 214 """Checks on the pending jobs and updates status accordingly. | 215 """Checks on the pending jobs and updates status accordingly. |
| 215 | 216 |
| 216 This method will check for the build to complete and then trigger the test, | 217 This method will check for the build to complete and then trigger the test, |
| 217 or will wait for the test as appropriate. | 218 or will wait for the test as appropriate. |
| 218 | 219 |
| 219 To wait for the test we try to get the buildbot job url from GS, and if | 220 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. | 221 available, we query the status of such job. |
| 221 """ | 222 """ |
| 222 if self.status == RevisionState.BUILDING and self._is_build_archived(): | 223 if self.status == RevisionState.BUILDING: |
| 223 self.start_job() | 224 if self._is_build_archived(): |
| 225 self.start_job() | |
| 226 elif self._is_build_failed(): | |
| 227 self.status = RevisionState.FAILED | |
| 224 elif (self.status in (RevisionState.TESTING, RevisionState.NEED_MORE_DATA) | 228 elif (self.status in (RevisionState.TESTING, RevisionState.NEED_MORE_DATA) |
| 225 and self._results_available()): | 229 and self._results_available()): |
| 226 # If we have already decided whether the revision is good or bad we | 230 # If we have already decided whether the revision is good or bad we |
| 227 # shouldn't check again | 231 # shouldn't check again |
| 228 check_revision_goodness = not(self.good or self.bad) | 232 check_revision_goodness = not(self.good or self.bad) |
| 229 self._read_test_results( | 233 self._read_test_results( |
| 230 check_revision_goodness=check_revision_goodness) | 234 check_revision_goodness=check_revision_goodness) |
| 231 # We assume _read_test_results may have changed the status to a broken | 235 # We assume _read_test_results may have changed the status to a broken |
| 232 # state such as FAILED or ABORTED. | 236 # state such as FAILED or ABORTED. |
| 233 if self.status in (RevisionState.TESTING, RevisionState.NEED_MORE_DATA): | 237 if self.status in (RevisionState.TESTING, RevisionState.NEED_MORE_DATA): |
| 234 self.status = RevisionState.TESTED | 238 self.status = RevisionState.TESTED |
| 235 | 239 |
| 236 def _is_build_archived(self): | 240 def _is_build_archived(self): |
| 237 """Checks if the revision is already built and archived.""" | 241 """Checks if the revision is already built and archived.""" |
| 238 if not self.build_archived: | 242 if not self.build_archived: |
| 239 api = self.bisector.api | 243 api = self.bisector.api |
| 240 self.build_archived = api.gsutil_file_exists(self.build_url) | 244 self.build_archived = api.gsutil_file_exists(self.build_url) |
| 241 | 245 |
| 242 if self.bisector.dummy_builds: | 246 if self.bisector.dummy_builds: |
| 243 self.build_archived = self.in_progress | 247 self.build_archived = self.in_progress |
| 244 | 248 |
| 245 return self.build_archived | 249 return self.build_archived |
| 246 | 250 |
| 251 def _is_build_failed(self): | |
| 252 api = self.bisector.api | |
| 253 current_build = None | |
| 254 base_url = '%sjson/builders/%s'% ( | |
| 255 os.environ.get('BUILDBOT_URL', 'http://localhost:8041/'), | |
|
qyearsley
2016/03/04 22:06:28
Why http://localhost:8041/?
RobertoCN
2016/03/14 01:00:25
The reasonable assumption if this env variable is
| |
| 256 self.bisector.get_builder_bot_for_this_platform()) | |
| 257 if self.build_number is None: | |
|
qyearsley
2016/03/04 22:06:28
Does this mean no build started yet, or we're not
RobertoCN
2016/03/14 01:00:25
It means we don't know the build number yet. The b
| |
| 258 try: | |
| 259 builder_state_url = base_url + '?as_text=1' | |
| 260 fetch_step = api.m.url.fetch_to_file( | |
| 261 builder_state_url, None, | |
| 262 step_name='fetch builder state', | |
| 263 stdout=api.m.raw_io.output()) | |
| 264 builder_state = fetch_step.stdout | |
| 265 builder_state = json.loads(builder_state or '{}') | |
| 266 for build_number in builder_state.get('cachedBuilds', []): | |
| 267 build_url = '%s/builds/%s?as_text=1' % (base_url, build_number) | |
| 268 fetch_result = api.m.url.fetch_to_file( | |
| 269 build_url, None, | |
| 270 step_name='fetch build details', | |
| 271 stdout=api.m.raw_io.output()) | |
| 272 build = json.loads(fetch_result.stdout or '{}') | |
| 273 for p in build.get('properties', []): | |
| 274 if p[0] == 'build_archive_url' and p[1] == self.build_url: | |
| 275 current_build = build | |
| 276 self.build_number = build_number | |
| 277 break | |
|
qyearsley
2016/03/04 22:06:28
Is build.get('properties') expected to be a list o
RobertoCN
2016/03/14 01:00:25
Thanks! I don't know how I didn't see it.
Done.
| |
| 278 if self.build_number: | |
| 279 break | |
| 280 except api.m.step.StepFailure: # pragma: no cover | |
| 281 # If we cannot get json from buildbot, we cannot determine if a build is | |
| 282 # failed, hence we consider it in progress until it times out. | |
| 283 return False | |
| 284 if self.build_number is None: | |
| 285 # The build hasn't started yet, therefore it's not failed. | |
| 286 return False | |
| 287 if not current_build: | |
| 288 build_url = '%s/builds/%s?as_text=1' % (base_url, self.build_number) | |
| 289 fetch_result = api.m.url.fetch_to_file( | |
| 290 build_url, None, | |
| 291 step_name='fetch build details', | |
| 292 stdout=api.m.raw_io.output()) | |
| 293 current_build = json.loads(fetch_result.stdout or '{}') | |
|
qyearsley
2016/03/04 22:06:28
This section of code (where the URL is put togethe
RobertoCN
2016/03/14 01:00:25
Done.
| |
| 294 return current_build.get('results') in [2, 3, 4] | |
|
qyearsley
2016/03/04 22:06:28
These could be set as constants, ideally with some
RobertoCN
2016/03/14 01:00:25
Works for me.
| |
| 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 |