Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(89)

Side by Side Diff: appengine/chromium_try_flakes/status/test/cq_status_test.py

Issue 1660043002: Move flaky run processing into a taskqueue (Closed) Base URL: https://chromium.googlesource.com/infra/infra.git@master
Patch Set: Created 4 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
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
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
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')
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698