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 mock | 7 import mock |
7 | 8 |
8 from google.appengine.datastore import datastore_stub_util | 9 from google.appengine.datastore import datastore_stub_util |
9 from google.appengine.ext import ndb | 10 from google.appengine.ext import ndb |
10 | 11 |
11 from handlers.flake_issues import ProcessIssue | 12 from handlers.flake_issues import ProcessIssue |
12 import main | 13 import main |
13 from model.flake import Flake, FlakyRun, FlakeOccurrence | 14 from model.flake import Flake, FlakyRun, FlakeOccurrence |
14 from model.build_run import PatchsetBuilderRuns, BuildRun | 15 from model.build_run import PatchsetBuilderRuns, BuildRun |
15 from testing_utils import testing | 16 from testing_utils import testing |
16 from time_functions.testing import mock_datetime_utc | 17 from time_functions.testing import mock_datetime_utc |
17 | 18 |
18 | 19 |
| 20 TEST_BUILDBOT_JSON_REPLY = json.dumps({ |
| 21 'steps': [ |
| 22 # Simple case. |
| 23 {'results': [2], 'name': 'foo1', 'text': ['bar1']}, |
| 24 |
| 25 # Invalid test results. |
| 26 {'results': [2], 'name': 'foo2', 'text': ['TEST RESULTS WERE INVALID']}, |
| 27 |
| 28 # GTest tests. |
| 29 { |
| 30 'results': [2], |
| 31 'name': 'foo3', |
| 32 'text': ['failures:<br/>bar2<br/>bar3<br/><br/>ignored:<br/>bar4'] |
| 33 }, |
| 34 |
| 35 # GPU tests. |
| 36 { |
| 37 'results': [2], |
| 38 'name': 'foo4', |
| 39 'text': ['<"http://url/path?query&tests=bar5,bar6,,bar7">'] |
| 40 }, |
| 41 |
| 42 # Ignore non-success non-failure results (7 is TRY_PENDING). |
| 43 {'results': [7], 'name': 'foo5', 'text': ['bar8']}, |
| 44 |
| 45 # Ignore steps that are failing without patch too (ToT is broken). |
| 46 {'results': [2], 'name': 'foo6 (with patch)', 'text': ['bar9']}, |
| 47 {'results': [2], 'name': 'foo6 (without patch)', 'text': ['bar9']}, |
| 48 |
| 49 # Ignore steps that are duplicating error in another step. |
| 50 {'results': [2], 'name': 'steps', 'text': ['bar10']}, |
| 51 {'results': [2], 'name': '[swarming] foo7', 'text': ['bar11']}, |
| 52 {'results': [2], 'name': 'presubmit', 'text': ['bar12']}, |
| 53 {'results': [2], 'name': 'recipe failure reason', 'text': ['bar12a']}, |
| 54 {'results': [2], 'name': 'test results', 'text': ['bar12b']}, |
| 55 {'results': [2], 'name': 'Uncaught Exception', 'text': ['bar12c']}, |
| 56 {'results': [2], 'name': 'bot_update', 'text': ['bot_update PATCH FAILED']}, |
| 57 |
| 58 # Only count first step (with patch) and ignore summary step. |
| 59 {'results': [2], 'name': 'foo8 (with patch)', 'text': ['bar13']}, |
| 60 {'results': [0], 'name': 'foo8 (without patch)', 'text': ['bar14']}, |
| 61 {'results': [2], 'name': 'foo8', 'text': ['bar15']}, |
| 62 |
| 63 # GTest without flakes. |
| 64 { |
| 65 'results': [2], |
| 66 'name': 'foo9', |
| 67 'text': ['failures:<br/><br/><br/>'] |
| 68 }, |
| 69 ] |
| 70 }) |
| 71 |
| 72 |
19 class MockComment(object): | 73 class MockComment(object): |
20 def __init__(self, created, author, comment=None): | 74 def __init__(self, created, author, comment=None): |
21 self.created = created | 75 self.created = created |
22 self.author = author | 76 self.author = author |
23 self.comment = comment | 77 self.comment = comment |
24 | 78 |
25 class MockIssue(object): | 79 class MockIssue(object): |
26 def __init__(self, issue_entry): | 80 def __init__(self, issue_entry): |
27 self.created = issue_entry.get('created') | 81 self.created = issue_entry.get('created') |
28 self.summary = issue_entry.get('summary') | 82 self.summary = issue_entry.get('summary') |
(...skipping 503 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
532 Flake(name='foo', occurrences=[fr1, fr2, fr3, fr4, fr5])), | 586 Flake(name='foo', occurrences=[fr1, fr2, fr3, fr4, fr5])), |
533 datetime.datetime(2015, 10, 18, 8, 0, 0)) | 587 datetime.datetime(2015, 10, 18, 8, 0, 0)) |
534 self.assertEqual( | 588 self.assertEqual( |
535 ProcessIssue._get_first_flake_occurrence_time( | 589 ProcessIssue._get_first_flake_occurrence_time( |
536 Flake(name='foo', occurrences=[fr1, fr2])), | 590 Flake(name='foo', occurrences=[fr1, fr2])), |
537 datetime.datetime(2015, 10, 12, 8, 0, 0)) | 591 datetime.datetime(2015, 10, 12, 8, 0, 0)) |
538 self.assertEqual( | 592 self.assertEqual( |
539 ProcessIssue._get_first_flake_occurrence_time( | 593 ProcessIssue._get_first_flake_occurrence_time( |
540 Flake(name='foo', occurrences=[fr5])), | 594 Flake(name='foo', occurrences=[fr5])), |
541 datetime.datetime(2015, 10, 19, 11, 0, 0)) | 595 datetime.datetime(2015, 10, 19, 11, 0, 0)) |
| 596 |
| 597 class CreateFlakyRunTestCase(testing.AppengineTestCase): |
| 598 app_module = main.app |
| 599 |
| 600 # This is needed to be able to test handlers using cross-group transactions. |
| 601 datastore_stub_consistency_policy = ( |
| 602 datastore_stub_util.PseudoRandomHRConsistencyPolicy(probability=1)) |
| 603 |
| 604 def _create_build_runs(self, ts, tf): |
| 605 pbr = PatchsetBuilderRuns( |
| 606 issue=123456789, patchset=20001, master='test.master', |
| 607 builder='test-builder').put() |
| 608 br_f = BuildRun(parent=pbr, buildnumber=100, result=2, time_started=ts, |
| 609 time_finished=tf).put() |
| 610 br_s = BuildRun(parent=pbr, buildnumber=101, result=0, time_started=ts, |
| 611 time_finished=tf).put() |
| 612 return br_f, br_s |
| 613 |
| 614 def test_get_flaky_run_reason_ignores_invalid_json(self): |
| 615 now = datetime.datetime.utcnow() |
| 616 br_f, br_s = self._create_build_runs(now - datetime.timedelta(hours=1), now) |
| 617 |
| 618 urlfetch_mock = mock.Mock() |
| 619 urlfetch_mock.return_value.content = 'invalid-json' |
| 620 |
| 621 with mock.patch('google.appengine.api.urlfetch.fetch', urlfetch_mock): |
| 622 self.test_app.post('/issues/create_flaky_run', |
| 623 {'failure_run_key': br_f.urlsafe(), |
| 624 'success_run_key': br_s.urlsafe()}) |
| 625 |
| 626 def test_handles_incorrect_parameters(self): |
| 627 self.test_app.post('/issues/create_flaky_run', {}, status=400) |
| 628 |
| 629 def test_get_flaky_run_reason(self): |
| 630 now = datetime.datetime.utcnow() |
| 631 br_f, br_s = self._create_build_runs(now - datetime.timedelta(hours=1), now) |
| 632 |
| 633 urlfetch_mock = mock.Mock() |
| 634 urlfetch_mock.return_value.content = TEST_BUILDBOT_JSON_REPLY |
| 635 |
| 636 # We also create one Flake to test that it is correctly updated. Other Flake |
| 637 # entities will be created automatically. |
| 638 Flake(id='bar5', name='bar5', occurrences=[], |
| 639 last_time_seen=datetime.datetime.min).put() |
| 640 |
| 641 with mock.patch('google.appengine.api.urlfetch.fetch', urlfetch_mock): |
| 642 self.test_app.post('/issues/create_flaky_run', |
| 643 {'failure_run_key': br_f.urlsafe(), |
| 644 'success_run_key': br_s.urlsafe()}) |
| 645 |
| 646 flaky_runs = FlakyRun.query().fetch(100) |
| 647 self.assertEqual(len(flaky_runs), 1) |
| 648 flaky_run = flaky_runs[0] |
| 649 self.assertEqual(flaky_run.failure_run, br_f) |
| 650 self.assertEqual(flaky_run.success_run, br_s) |
| 651 self.assertEqual(flaky_run.failure_run_time_finished, now) |
| 652 self.assertEqual(flaky_run.failure_run_time_started, |
| 653 now - datetime.timedelta(hours=1)) |
| 654 |
| 655 # Verify that we've used correct URL to access buildbot JSON endpoint. |
| 656 urlfetch_mock.assert_called_once_with( |
| 657 'http://build.chromium.org/p/test.master/json/builders/test-builder/' |
| 658 'builds/100') |
| 659 |
| 660 # Expected flakes to be found: list of (step_name, test_name). |
| 661 expected_flakes = [ |
| 662 ('foo1', 'bar1'), ('foo2', 'TEST RESULTS WERE INVALID'), |
| 663 ('foo3', 'bar2'), ('foo3', 'bar3'), ('foo4', 'bar5'), ('foo4', 'bar6'), |
| 664 ('foo4', 'bar7'), ('foo8 (with patch)', 'bar13'), |
| 665 ] |
| 666 |
| 667 flake_occurrences = flaky_run.flakes |
| 668 self.assertEqual(len(flake_occurrences), len(expected_flakes)) |
| 669 actual_flake_occurrences = [ |
| 670 (fo.name, fo.failure) for fo in flake_occurrences] |
| 671 self.assertEqual(expected_flakes, actual_flake_occurrences) |
| 672 |
| 673 # We compare sets below, because order of flakes returned by datastore |
| 674 # doesn't have to be same as steps above. |
| 675 flakes = Flake.query().fetch() |
| 676 self.assertEqual(len(flakes), len(expected_flakes)) |
| 677 expected_flake_names = set([ef[1] for ef in expected_flakes]) |
| 678 actual_flake_names = set([f.name for f in flakes]) |
| 679 self.assertEqual(expected_flake_names, actual_flake_names) |
| 680 |
| 681 for flake in flakes: |
| 682 self.assertEqual(flake.occurrences, [flaky_run.key]) |
OLD | NEW |