| 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 import datetime | 5 import datetime |
| 6 import json | 6 import json |
| 7 import mock | 7 import mock |
| 8 import urllib2 | 8 import urllib2 |
| 9 | 9 |
| 10 from google.appengine.datastore import datastore_stub_util | 10 from google.appengine.datastore import datastore_stub_util |
| 11 from google.appengine.ext import ndb | 11 from google.appengine.ext import ndb |
| 12 from google.appengine.runtime import DeadlineExceededError | 12 from google.appengine.runtime import DeadlineExceededError |
| 13 | 13 |
| 14 import main | 14 import main |
| 15 from model.fetch_status import FetchStatus | 15 from model.fetch_status import FetchStatus |
| 16 from model.flake import Flake, FlakyRun | 16 from model.flake import Flake |
| 17 from model.build_run import BuildRun, PatchsetBuilderRuns | 17 from model.build_run import BuildRun, PatchsetBuilderRuns |
| 18 from status import cq_status | 18 from status import cq_status |
| 19 from testing_utils import testing | 19 from testing_utils import testing |
| 20 | 20 |
| 21 | 21 |
| 22 TEST_BUILDBOT_JSON_REPLY = json.dumps({ | |
| 23 'steps': [ | |
| 24 # Simple case. | |
| 25 {'results': [2], 'name': 'foo1', 'text': ['bar1']}, | |
| 26 | |
| 27 # Invalid test results. | |
| 28 {'results': [2], 'name': 'foo2', 'text': ['TEST RESULTS WERE INVALID']}, | |
| 29 | |
| 30 # GTest tests. | |
| 31 { | |
| 32 'results': [2], | |
| 33 'name': 'foo3', | |
| 34 'text': ['failures:<br/>bar2<br/>bar3<br/><br/>ignored:<br/>bar4'] | |
| 35 }, | |
| 36 | |
| 37 # GPU tests. | |
| 38 { | |
| 39 'results': [2], | |
| 40 'name': 'foo4', | |
| 41 'text': ['<"http://url/path?query&tests=bar5,bar6,,bar7">'] | |
| 42 }, | |
| 43 | |
| 44 # Ignore non-success non-failure results (7 is TRY_PENDING). | |
| 45 {'results': [7], 'name': 'foo5', 'text': ['bar8']}, | |
| 46 | |
| 47 # Ignore steps that are failing without patch too (ToT is broken). | |
| 48 {'results': [2], 'name': 'foo6 (with patch)', 'text': ['bar9']}, | |
| 49 {'results': [2], 'name': 'foo6 (without patch)', 'text': ['bar9']}, | |
| 50 | |
| 51 # Ignore steps that are duplicating error in another step. | |
| 52 {'results': [2], 'name': 'steps', 'text': ['bar10']}, | |
| 53 {'results': [2], 'name': '[swarming] foo7', 'text': ['bar11']}, | |
| 54 {'results': [2], 'name': 'presubmit', 'text': ['bar12']}, | |
| 55 {'results': [2], 'name': 'recipe failure reason', 'text': ['bar12a']}, | |
| 56 {'results': [2], 'name': 'test results', 'text': ['bar12b']}, | |
| 57 {'results': [2], 'name': 'Uncaught Exception', 'text': ['bar12c']}, | |
| 58 {'results': [2], 'name': 'bot_update', 'text': ['bot_update PATCH FAILED']}, | |
| 59 | |
| 60 # Only count first step (with patch) and ignore summary step. | |
| 61 {'results': [2], 'name': 'foo8 (with patch)', 'text': ['bar13']}, | |
| 62 {'results': [0], 'name': 'foo8 (without patch)', 'text': ['bar14']}, | |
| 63 {'results': [2], 'name': 'foo8', 'text': ['bar15']}, | |
| 64 | |
| 65 # GTest without flakes. | |
| 66 { | |
| 67 'results': [2], | |
| 68 'name': 'foo9', | |
| 69 'text': ['failures:<br/><br/><br/>'] | |
| 70 }, | |
| 71 | |
| 72 ] | |
| 73 }) | |
| 74 | |
| 75 # Test results below capture various variants in which results may be processed. | 22 # Test results below capture various variants in which results may be processed. |
| 76 # Special attention should be paid to the 'issue' and 'patchset' fields as code | 23 # Special attention should be paid to the 'issue' and 'patchset' fields as code |
| 77 # is expected to correctly process results from different issues and patchsets | 24 # is expected to correctly process results from different issues and patchsets |
| 78 # independently of each other. | 25 # independently of each other. |
| 79 TEST_CQ_STATUS_RESPONSE = json.dumps({ | 26 TEST_CQ_STATUS_RESPONSE = json.dumps({ |
| 80 'more': False, | 27 'more': False, |
| 81 'cursor': '', | 28 'cursor': '', |
| 82 'results': [ | 29 'results': [ |
| 83 # Ignored because action field is missing. | 30 # Ignored because action field is missing. |
| 84 { | 31 { |
| (...skipping 312 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 397 path = '/cron/update_stale_issues' | 344 path = '/cron/update_stale_issues' |
| 398 response = self.test_app.get(path, headers={'X-AppEngine-Cron': 'true'}) | 345 response = self.test_app.get(path, headers={'X-AppEngine-Cron': 'true'}) |
| 399 self.assertEqual(200, response.status_int) | 346 self.assertEqual(200, response.status_int) |
| 400 | 347 |
| 401 tasks = self.taskqueue_stub.get_filtered_tasks(queue_names='issue-updates') | 348 tasks = self.taskqueue_stub.get_filtered_tasks(queue_names='issue-updates') |
| 402 self.assertEqual(len(tasks), 3) | 349 self.assertEqual(len(tasks), 3) |
| 403 self.assertEqual(tasks[0].url, '/issues/update-if-stale/123') | 350 self.assertEqual(tasks[0].url, '/issues/update-if-stale/123') |
| 404 self.assertEqual(tasks[1].url, '/issues/update-if-stale/234') | 351 self.assertEqual(tasks[1].url, '/issues/update-if-stale/234') |
| 405 self.assertEqual(tasks[2].url, '/issues/update-if-stale/345') | 352 self.assertEqual(tasks[2].url, '/issues/update-if-stale/345') |
| 406 | 353 |
| 407 def _create_flaky_run(self, ts, tf): | |
| 408 pbr = PatchsetBuilderRuns( | |
| 409 issue=123456789, patchset=20001, master='test.master', | |
| 410 builder='test-builder').put() | |
| 411 br_f = BuildRun(parent=pbr, buildnumber=100, result=2, time_started=ts, | |
| 412 time_finished=tf).put() | |
| 413 br_s = BuildRun(parent=pbr, buildnumber=101, result=0, time_started=ts, | |
| 414 time_finished=tf).put() | |
| 415 return FlakyRun(failure_run=br_f, success_run=br_s, | |
| 416 failure_run_time_started=ts, | |
| 417 failure_run_time_finished=tf).put() | |
| 418 | |
| 419 def test_get_flaky_run_reason_ignores_invalid_json(self): | |
| 420 now = datetime.datetime.utcnow() | |
| 421 fr_key = self._create_flaky_run(now - datetime.timedelta(hours=1), now) | |
| 422 | |
| 423 urlfetch_mock = mock.Mock() | |
| 424 urlfetch_mock.return_value.content = 'invalid-json' | |
| 425 | |
| 426 with mock.patch('google.appengine.api.urlfetch.fetch', urlfetch_mock): | |
| 427 cq_status.get_flaky_run_reason(fr_key) | |
| 428 | |
| 429 def test_get_flaky_run_reason(self): | |
| 430 now = datetime.datetime.utcnow() | |
| 431 fr_key = self._create_flaky_run(now - datetime.timedelta(hours=1), now) | |
| 432 | |
| 433 urlfetch_mock = mock.Mock() | |
| 434 urlfetch_mock.return_value.content = TEST_BUILDBOT_JSON_REPLY | |
| 435 | |
| 436 # We also create one Flake to test that it is correctly updated. Other Flake | |
| 437 # entities will be created automatically. | |
| 438 Flake(id='bar5', name='bar5', occurrences=[], | |
| 439 last_time_seen=datetime.datetime.min).put() | |
| 440 | |
| 441 with mock.patch('google.appengine.api.urlfetch.fetch', urlfetch_mock): | |
| 442 cq_status.get_flaky_run_reason(fr_key) | |
| 443 | |
| 444 # Verify that we've used correct URL to access buildbot JSON endpoint. | |
| 445 urlfetch_mock.assert_called_once_with( | |
| 446 'http://build.chromium.org/p/test.master/json/builders/test-builder/' | |
| 447 'builds/100') | |
| 448 | |
| 449 # Expected flakes to be found: list of (step_name, test_name). | |
| 450 expected_flakes = [ | |
| 451 ('foo1', 'bar1'), ('foo2', 'TEST RESULTS WERE INVALID'), | |
| 452 ('foo3', 'bar2'), ('foo3', 'bar3'), ('foo4', 'bar5'), ('foo4', 'bar6'), | |
| 453 ('foo4', 'bar7'), ('foo8 (with patch)', 'bar13'), | |
| 454 ] | |
| 455 | |
| 456 flake_occurrences = fr_key.get().flakes | |
| 457 print flake_occurrences | |
| 458 self.assertEqual(len(flake_occurrences), len(expected_flakes)) | |
| 459 actual_flake_occurrences = [ | |
| 460 (fo.name, fo.failure) for fo in flake_occurrences] | |
| 461 self.assertEqual(expected_flakes, actual_flake_occurrences) | |
| 462 | |
| 463 # We compare sets below, because order of flakes returned by datastore | |
| 464 # doesn't have to be same as steps above. | |
| 465 flakes = Flake.query().fetch() | |
| 466 self.assertEqual(len(flakes), len(expected_flakes)) | |
| 467 expected_flake_names = set([ef[1] for ef in expected_flakes]) | |
| 468 actual_flake_names = set([f.name for f in flakes]) | |
| 469 self.assertEqual(expected_flake_names, actual_flake_names) | |
| 470 | |
| 471 for flake in flakes: | |
| 472 self.assertEqual(flake.occurrences, [fr_key]) | |
| 473 self.assertEqual(flake.last_time_seen, now) | |
| 474 self.assertEqual(flake.count_hour, 1) | |
| 475 self.assertEqual(flake.count_day, 1) | |
| 476 self.assertEqual(flake.count_week, 1) | |
| 477 self.assertEqual(flake.count_month, 1) | |
| 478 self.assertEqual(flake.last_hour, True) | |
| 479 self.assertEqual(flake.last_day, True) | |
| 480 self.assertEqual(flake.last_week, True) | |
| 481 self.assertEqual(flake.last_month, True) | |
| 482 | |
| 483 def _mock_response(self, content): | 354 def _mock_response(self, content): |
| 484 m = mock.Mock() | 355 m = mock.Mock() |
| 485 if isinstance(content, basestring): | 356 if isinstance(content, basestring): |
| 486 m.content = content | 357 m.content = content |
| 487 else: | 358 else: |
| 488 m.content = json.dumps(content) | 359 m.content = json.dumps(content) |
| 489 return m | 360 return m |
| 490 | 361 |
| 491 def test_fetch_cq_status_handles_and_retries_non_json_replies(self): | 362 def test_fetch_cq_status_handles_and_retries_non_json_replies(self): |
| 492 urlfetch_mock = mock.Mock() | 363 urlfetch_mock = mock.Mock() |
| (...skipping 131 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 624 | 495 |
| 625 self.assertEqual(urlfetch_mock.call_count, 3) | 496 self.assertEqual(urlfetch_mock.call_count, 3) |
| 626 | 497 |
| 627 def test_cq_status_fetch_detects_flaky_runs_correctly(self): | 498 def test_cq_status_fetch_detects_flaky_runs_correctly(self): |
| 628 urlfetch_mock = mock.Mock() | 499 urlfetch_mock = mock.Mock() |
| 629 urlfetch_mock.return_value.content = TEST_CQ_STATUS_RESPONSE | 500 urlfetch_mock.return_value.content = TEST_CQ_STATUS_RESPONSE |
| 630 | 501 |
| 631 with mock.patch('google.appengine.api.urlfetch.fetch', urlfetch_mock): | 502 with mock.patch('google.appengine.api.urlfetch.fetch', urlfetch_mock): |
| 632 cq_status.fetch_cq_status() | 503 cq_status.fetch_cq_status() |
| 633 | 504 |
| 634 flaky_runs = FlakyRun.query().fetch(100) | 505 tasks = self.taskqueue_stub.get_filtered_tasks(queue_names='issue-updates') |
| 635 self.assertEqual(len(flaky_runs), 3) | 506 self.assertEqual(len(tasks), 3) |
| 636 | 507 |
| 637 # We only compare select few properties of the created FlakyRun entities. | 508 # We only compare select few properties of the created BuildRun entities. |
| 638 flaky_run_tuples = set() | 509 build_run_tuples = set() |
| 639 for flaky_run in flaky_runs: | 510 for task in tasks: |
| 640 failure_run = flaky_run.failure_run.get() | 511 params = task.extract_params() |
| 641 success_run = flaky_run.success_run.get() | 512 failure_run = ndb.Key(urlsafe=params['failure_run_key']).get() |
| 513 success_run = ndb.Key(urlsafe=params['success_run_key']).get() |
| 642 self.assertEqual(failure_run.key.parent(), success_run.key.parent()) | 514 self.assertEqual(failure_run.key.parent(), success_run.key.parent()) |
| 643 pbr = failure_run.key.parent().get() | 515 pbr = failure_run.key.parent().get() |
| 644 flaky_run_tuple = (pbr.master, pbr.builder, pbr.issue, pbr.patchset, | 516 build_run_tuple = (pbr.master, pbr.builder, pbr.issue, pbr.patchset, |
| 645 failure_run.buildnumber, success_run.buildnumber) | 517 failure_run.buildnumber, success_run.buildnumber) |
| 646 flaky_run_tuples.add(flaky_run_tuple) | 518 build_run_tuples.add(build_run_tuple) |
| 647 | 519 |
| 648 expected_flaky_runs = set([ | 520 expected_build_runs = set([ |
| 649 ('tryserver.test', 'test-builder', 987654321, 20001, 105, 110), | 521 ('tryserver.test', 'test-builder', 987654321, 20001, 105, 110), |
| 650 ('tryserver.test', 'test-builder', 123456789, 20001, 109, 101), | 522 ('tryserver.test', 'test-builder', 123456789, 20001, 109, 101), |
| 651 ('tryserver.test', 'test-builder', 123456789, 20001, 106, 101), | 523 ('tryserver.test', 'test-builder', 123456789, 20001, 106, 101), |
| 652 ]) | 524 ]) |
| 653 self.assertEqual(flaky_run_tuples, expected_flaky_runs) | 525 self.assertEqual(build_run_tuples, expected_build_runs) |
| 654 | 526 |
| 655 def test_cq_status_fetch_creates_deferred_tasks_correctly(self): | 527 def test_cq_status_fetch_creates_tasks_correctly(self): |
| 656 urlfetch_mock = mock.Mock() | 528 urlfetch_mock = mock.Mock() |
| 657 urlfetch_mock.return_value.content = TEST_CQ_STATUS_RESPONSE | 529 urlfetch_mock.return_value.content = TEST_CQ_STATUS_RESPONSE |
| 658 | 530 |
| 659 with mock.patch('google.appengine.api.urlfetch.fetch', urlfetch_mock): | 531 with mock.patch('google.appengine.api.urlfetch.fetch', urlfetch_mock): |
| 660 cq_status.fetch_cq_status() | 532 cq_status.fetch_cq_status() |
| 661 | 533 |
| 662 tasks = self.taskqueue_stub.get_filtered_tasks() | 534 tasks = self.taskqueue_stub.get_filtered_tasks(queue_names='issue-updates') |
| 663 self.assertEqual(len(tasks), 3) | 535 self.assertEqual(len(tasks), 3) |
| 536 self.assertEqual(tasks[0].url, '/issues/create_flaky_run') |
| 537 self.assertEqual(tasks[1].url, '/issues/create_flaky_run') |
| 538 self.assertEqual(tasks[2].url, '/issues/create_flaky_run') |
| 664 | 539 |
| 665 def test_cq_status_processes_timestamp_in_raw_json(self): | 540 def test_cq_status_processes_timestamp_in_raw_json(self): |
| 666 urlfetch_mock = mock.Mock() | 541 urlfetch_mock = mock.Mock() |
| 667 urlfetch_mock.return_value.content = ( | 542 urlfetch_mock.return_value.content = ( |
| 668 '{"more":false,"cursor":"","results":[],"timestamp":"foo"}') | 543 '{"more":false,"cursor":"","results":[],"timestamp":"foo"}') |
| 669 | 544 |
| 670 with mock.patch('google.appengine.api.urlfetch.fetch', urlfetch_mock): | 545 with mock.patch('google.appengine.api.urlfetch.fetch', urlfetch_mock): |
| 671 with mock.patch('logging.info') as logging_info_mock: | 546 with mock.patch('logging.info') as logging_info_mock: |
| 672 cq_status.fetch_cq_status() | 547 cq_status.fetch_cq_status() |
| 673 logging_info_mock.assert_any_call(' current fetch has time of foo') | 548 logging_info_mock.assert_any_call(' current fetch has time of foo') |
| OLD | NEW |