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 |