• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2016 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import copy
6import datetime
7import json
8import unittest
9
10import mock
11import webapp2
12import webtest
13
14from dashboard import bisect_fyi
15from dashboard import bisect_fyi_test
16from dashboard import layered_cache
17from dashboard import rietveld_service
18from dashboard import stored_object
19from dashboard import testing_common
20from dashboard import update_bug_with_results
21from dashboard import utils
22from dashboard.models import anomaly
23from dashboard.models import bug_data
24from dashboard.models import try_job
25
26_SAMPLE_BISECT_RESULTS_JSON = {
27    'try_job_id': 6789,
28    'bug_id': 4567,
29    'status': 'completed',
30    'bisect_bot': 'linux',
31    'buildbot_log_url': '',
32    'command': ('tools/perf/run_benchmark -v '
33                '--browser=release page_cycler.intl_ar_fa_he'),
34    'metric': 'warm_times/page_load_time',
35    'change': '',
36    'score': 99.9,
37    'good_revision': '306475',
38    'bad_revision': '306478',
39    'warnings': None,
40    'abort_reason': None,
41    'issue_url': 'https://issue_url/123456',
42    'culprit_data': {
43        'subject': 'subject',
44        'author': 'author',
45        'email': 'author@email.com',
46        'cl_date': '1/2/2015',
47        'commit_info': 'commit_info',
48        'revisions_links': ['http://src.chromium.org/viewvc/chrome?view='
49                            'revision&revision=20798'],
50        'cl': '2a1781d64d'  # Should match config in bisect_fyi_test.py.
51    },
52    'revision_data': [
53        {
54            'depot_name': 'chromium',
55            'deps_revision': 1234,
56            'commit_hash': '1234abcdf',
57            'mean_value': 70,
58            'std_dev': 0,
59            'values': [70, 70, 70],
60            'result': 'good'
61        }, {
62            'depot_name': 'chromium',
63            'deps_revision': 1235,
64            'commit_hash': '1235abdcf',
65            'mean_value': 80,
66            'std_dev': 0,
67            'values': [80, 80, 80],
68            'result': 'bad'
69        }
70    ]
71}
72
73_REVISION_RESPONSE = """
74<html xmlns=....>
75<head><title>[chrome] Revision 207985</title></head><body><table>....
76<tr align="left">
77<th>Log Message:</th>
78<td> Message....</td>
79&gt; &gt; Review URL: <a href="https://codereview.chromium.org/81533002">\
80https://codereview.chromium.org/81533002</a>
81&gt;
82&gt; Review URL: <a href="https://codereview.chromium.org/96073002">\
83https://codereview.chromium.org/96073002</a>
84
85Review URL: <a href="https://codereview.chromium.org/17504006">\
86https://codereview.chromium.org/96363002</a></pre></td></tr></table>....</body>
87</html>
88"""
89
90_PERF_TEST_CONFIG = """config = {
91  'command': 'tools/perf/run_benchmark -v --browser=release\
92dromaeo.jslibstylejquery --profiler=trace',
93  'good_revision': '215806',
94  'bad_revision': '215828',
95  'repeat_count': '1',
96  'max_time_minutes': '120'
97}"""
98
99_ISSUE_RESPONSE = """
100    {
101      "description": "Issue Description.",
102      "cc": [
103              "chromium-reviews@chromium.org",
104              "cc-bugs@chromium.org",
105              "sullivan@google.com"
106            ],
107      "reviewers": [
108                      "prasadv@google.com"
109                   ],
110      "owner_email": "sullivan@google.com",
111      "private": false,
112      "base_url": "svn://chrome-svn/chrome/trunk/src/",
113      "owner":"sullivan",
114      "subject":"Issue Subject",
115      "created":"2013-06-20 22:23:27.227150",
116      "patchsets":[1,21001,29001],
117      "modified":"2013-06-22 00:59:38.530190",
118      "closed":true,
119      "commit":false,
120      "issue":17504006
121    }
122"""
123
124
125def _MockFetch(url=None):
126  url_to_response_map = {
127      'http://src.chromium.org/viewvc/chrome?view=revision&revision=20798': [
128          200, _REVISION_RESPONSE
129      ],
130      'http://src.chromium.org/viewvc/chrome?view=revision&revision=20799': [
131          200, 'REVISION REQUEST FAILED!'
132      ],
133      'https://codereview.chromium.org/api/17504006': [
134          200, json.dumps(json.loads(_ISSUE_RESPONSE))
135      ],
136  }
137
138  if url not in url_to_response_map:
139    assert False, 'Bad url %s' % url
140
141  response_code = url_to_response_map[url][0]
142  response = url_to_response_map[url][1]
143  return testing_common.FakeResponseObject(response_code, response)
144
145
146# In this class, we patch apiclient.discovery.build so as to not make network
147# requests, which are normally made when the IssueTrackerService is initialized.
148@mock.patch('apiclient.discovery.build', mock.MagicMock())
149@mock.patch.object(utils, 'TickMonitoringCustomMetric', mock.MagicMock())
150class UpdateBugWithResultsTest(testing_common.TestCase):
151
152  def setUp(self):
153    super(UpdateBugWithResultsTest, self).setUp()
154    app = webapp2.WSGIApplication([(
155        '/update_bug_with_results',
156        update_bug_with_results.UpdateBugWithResultsHandler)])
157    self.testapp = webtest.TestApp(app)
158    self._AddRietveldConfig()
159
160  def _AddRietveldConfig(self):
161    """Adds a RietveldConfig entity to the datastore.
162
163    This is used in order to get the Rietveld URL when requests are made to the
164    handler in te tests below. In the real datastore, the RietveldConfig entity
165    would contain credentials.
166    """
167    rietveld_service.RietveldConfig(
168        id='default_rietveld_config',
169        client_email='sullivan@google.com',
170        service_account_key='Fake Account Key',
171        server_url='https://test-rietveld.appspot.com',
172        internal_server_url='https://test-rietveld.appspot.com').put()
173
174  def _AddTryJob(self, bug_id, status, bot, **kwargs):
175    job = try_job.TryJob(bug_id=bug_id, status=status, bot=bot, **kwargs)
176    job.put()
177    bug_data.Bug(id=bug_id).put()
178    return job
179
180  @mock.patch(
181      'google.appengine.api.urlfetch.fetch',
182      mock.MagicMock(side_effect=_MockFetch))
183  @mock.patch.object(
184      update_bug_with_results.issue_tracker_service, 'IssueTrackerService',
185      mock.MagicMock())
186  def testGet(self):
187    # Put succeeded, failed, staled, and not yet finished jobs in the
188    # datastore.
189    self._AddTryJob(11111, 'started', 'win_perf',
190                    results_data=_SAMPLE_BISECT_RESULTS_JSON)
191    staled_timestamp = (datetime.datetime.now() -
192                        update_bug_with_results._STALE_TRYJOB_DELTA)
193    self._AddTryJob(22222, 'started', 'win_perf',
194                    last_ran_timestamp=staled_timestamp)
195    self._AddTryJob(33333, 'failed', 'win_perf')
196    self._AddTryJob(44444, 'started', 'win_perf')
197
198    self.testapp.get('/update_bug_with_results')
199    pending_jobs = try_job.TryJob.query().fetch()
200    # Expects no jobs to be deleted.
201    self.assertEqual(4, len(pending_jobs))
202    self.assertEqual(11111, pending_jobs[0].bug_id)
203    self.assertEqual('completed', pending_jobs[0].status)
204    self.assertEqual(22222, pending_jobs[1].bug_id)
205    self.assertEqual('staled', pending_jobs[1].status)
206    self.assertEqual(33333, pending_jobs[2].bug_id)
207    self.assertEqual('failed', pending_jobs[2].status)
208    self.assertEqual(44444, pending_jobs[3].bug_id)
209    self.assertEqual('started', pending_jobs[3].status)
210
211  @mock.patch(
212      'google.appengine.api.urlfetch.fetch',
213      mock.MagicMock(side_effect=_MockFetch))
214  @mock.patch.object(
215      update_bug_with_results.issue_tracker_service, 'IssueTrackerService',
216      mock.MagicMock())
217  def testCreateTryJob_WithoutExistingBug(self):
218    # Put succeeded job in the datastore.
219    try_job.TryJob(
220        bug_id=12345, status='started', bot='win_perf',
221        results_data=_SAMPLE_BISECT_RESULTS_JSON).put()
222
223    self.testapp.get('/update_bug_with_results')
224    pending_jobs = try_job.TryJob.query().fetch()
225
226    # Expects job to finish.
227    self.assertEqual(1, len(pending_jobs))
228    self.assertEqual(12345, pending_jobs[0].bug_id)
229    self.assertEqual('completed', pending_jobs[0].status)
230
231  @mock.patch.object(utils, 'ServiceAccountCredentials', mock.MagicMock())
232  @mock.patch(
233      'google.appengine.api.urlfetch.fetch',
234      mock.MagicMock(side_effect=_MockFetch))
235  @mock.patch.object(
236      update_bug_with_results.issue_tracker_service.IssueTrackerService,
237      'AddBugComment', mock.MagicMock(return_value=False))
238  @mock.patch('logging.error')
239  def testGet_FailsToUpdateBug_LogsErrorAndMovesOn(self, mock_logging_error):
240    # Put a successful job and a failed job with partial results.
241    # Note that AddBugComment is mocked to always returns false, which
242    # simulates failing to post results to the issue tracker for all bugs.
243    self._AddTryJob(12345, 'started', 'win_perf',
244                    results_data=_SAMPLE_BISECT_RESULTS_JSON)
245    self._AddTryJob(54321, 'started', 'win_perf',
246                    results_data=_SAMPLE_BISECT_RESULTS_JSON)
247    self.testapp.get('/update_bug_with_results')
248
249    # Two errors should be logged.
250    self.assertEqual(2, mock_logging_error.call_count)
251
252    # The pending jobs should still be there.
253    pending_jobs = try_job.TryJob.query().fetch()
254    self.assertEqual(2, len(pending_jobs))
255    self.assertEqual('started', pending_jobs[0].status)
256    self.assertEqual('started', pending_jobs[1].status)
257
258  @mock.patch(
259      'google.appengine.api.urlfetch.fetch',
260      mock.MagicMock(side_effect=_MockFetch))
261  @mock.patch.object(
262      update_bug_with_results.issue_tracker_service.IssueTrackerService,
263      'AddBugComment')
264  def testGet_BisectCulpritHasAuthor_AssignsAuthor(self, mock_update_bug):
265    # When a bisect has a culprit for a perf regression,
266    # author and reviewer of the CL should be cc'ed on issue update.
267    self._AddTryJob(12345, 'started', 'win_perf',
268                    results_data=_SAMPLE_BISECT_RESULTS_JSON)
269
270    self.testapp.get('/update_bug_with_results')
271    mock_update_bug.assert_called_once_with(
272        mock.ANY, mock.ANY,
273        cc_list=['author@email.com', 'prasadv@google.com'],
274        merge_issue=None, labels=None, owner='author@email.com')
275
276  @mock.patch(
277      'google.appengine.api.urlfetch.fetch',
278      mock.MagicMock(side_effect=_MockFetch))
279  @mock.patch.object(
280      update_bug_with_results.issue_tracker_service.IssueTrackerService,
281      'AddBugComment')
282  def testGet_FailedRevisionResponse(self, mock_add_bug):
283    # When a Rietveld CL link fails to respond, only update CL owner in CC
284    # list.
285    sample_bisect_results = copy.deepcopy(_SAMPLE_BISECT_RESULTS_JSON)
286    sample_bisect_results['revisions_links'] = [
287        'http://src.chromium.org/viewvc/chrome?view=revision&revision=20799']
288    self._AddTryJob(12345, 'started', 'win_perf',
289                    results_data=sample_bisect_results)
290
291    self.testapp.get('/update_bug_with_results')
292    mock_add_bug.assert_called_once_with(mock.ANY,
293                                         mock.ANY,
294                                         cc_list=['author@email.com',
295                                                  'prasadv@google.com'],
296                                         merge_issue=None,
297                                         labels=None,
298                                         owner='author@email.com')
299
300  @mock.patch(
301      'google.appengine.api.urlfetch.fetch',
302      mock.MagicMock(side_effect=_MockFetch))
303  @mock.patch.object(
304      update_bug_with_results.issue_tracker_service.IssueTrackerService,
305      'AddBugComment', mock.MagicMock())
306  def testGet_PositiveResult_StoresCommitHash(self):
307    self._AddTryJob(12345, 'started', 'win_perf',
308                    results_data=_SAMPLE_BISECT_RESULTS_JSON)
309
310    self.testapp.get('/update_bug_with_results')
311    self.assertEqual('12345',
312                     layered_cache.GetExternal('commit_hash_2a1781d64d'))
313
314  @mock.patch(
315      'google.appengine.api.urlfetch.fetch',
316      mock.MagicMock(side_effect=_MockFetch))
317  @mock.patch.object(
318      update_bug_with_results.issue_tracker_service.IssueTrackerService,
319      'AddBugComment', mock.MagicMock())
320  def testGet_NegativeResult_DoesNotStoreCommitHash(self):
321    sample_bisect_results = copy.deepcopy(_SAMPLE_BISECT_RESULTS_JSON)
322    sample_bisect_results['culprit_data'] = None
323    self._AddTryJob(12345, 'started', 'win_perf',
324                    results_data=sample_bisect_results)
325    self.testapp.get('/update_bug_with_results')
326
327    caches = layered_cache.CachedPickledString.query().fetch()
328    # Only 1 cache for bisect stats.
329    self.assertEqual(1, len(caches))
330
331  def testMapAnomaliesToMergeIntoBug(self):
332    # Add anomalies.
333    test_keys = map(utils.TestKey, [
334        'ChromiumGPU/linux-release/scrolling-benchmark/first_paint',
335        'ChromiumGPU/linux-release/scrolling-benchmark/mean_frame_time'])
336    anomaly.Anomaly(
337        start_revision=9990, end_revision=9997, test=test_keys[0],
338        median_before_anomaly=100, median_after_anomaly=200,
339        sheriff=None, bug_id=12345).put()
340    anomaly.Anomaly(
341        start_revision=9990, end_revision=9996, test=test_keys[0],
342        median_before_anomaly=100, median_after_anomaly=200,
343        sheriff=None, bug_id=54321).put()
344    # Map anomalies to base(dest_bug_id) bug.
345    update_bug_with_results._MapAnomaliesToMergeIntoBug(
346        dest_bug_id=12345, source_bug_id=54321)
347    anomalies = anomaly.Anomaly.query(
348        anomaly.Anomaly.bug_id == int(54321)).fetch()
349    self.assertEqual(0, len(anomalies))
350
351  @mock.patch(
352      'google.appengine.api.urlfetch.fetch',
353      mock.MagicMock(side_effect=_MockFetch))
354  @mock.patch.object(
355      update_bug_with_results.email_template,
356      'GetPerfTryJobEmailReport', mock.MagicMock(return_value=None))
357  def testSendPerfTryJobEmail_EmptyEmailReport_DontSendEmail(self):
358    self._AddTryJob(12345, 'started', 'win_perf', job_type='perf-try',
359                    results_data=_SAMPLE_BISECT_RESULTS_JSON)
360    self.testapp.get('/update_bug_with_results')
361    messages = self.mail_stub.get_sent_messages()
362    self.assertEqual(0, len(messages))
363
364  @mock.patch(
365      'google.appengine.api.urlfetch.fetch',
366      mock.MagicMock(side_effect=_MockFetch))
367  @mock.patch.object(
368      update_bug_with_results.issue_tracker_service.IssueTrackerService,
369      'AddBugComment')
370  def testGet_InternalOnlyTryJob_AddsInternalOnlyBugLabel(
371      self, mock_update_bug):
372    self._AddTryJob(12345, 'started', 'win_perf',
373                    results_data=_SAMPLE_BISECT_RESULTS_JSON,
374                    internal_only=True)
375
376    self.testapp.get('/update_bug_with_results')
377    mock_update_bug.assert_called_once_with(
378        mock.ANY, mock.ANY,
379        cc_list=mock.ANY,
380        merge_issue=None, labels=['Restrict-View-Google'], owner=mock.ANY)
381
382  @mock.patch(
383      'google.appengine.api.urlfetch.fetch',
384      mock.MagicMock(side_effect=_MockFetch))
385  @mock.patch.object(
386      update_bug_with_results.issue_tracker_service, 'IssueTrackerService',
387      mock.MagicMock())
388  def testFYI_Send_No_Email_On_Success(self):
389    stored_object.Set(
390        bisect_fyi._BISECT_FYI_CONFIGS_KEY,
391        bisect_fyi_test.TEST_FYI_CONFIGS)
392    test_config = bisect_fyi_test.TEST_FYI_CONFIGS['positive_culprit']
393    bisect_config = test_config.get('bisect_config')
394    self._AddTryJob(12345, 'started', 'win_perf',
395                    results_data=_SAMPLE_BISECT_RESULTS_JSON,
396                    internal_only=True,
397                    config=utils.BisectConfigPythonString(bisect_config),
398                    job_type='bisect-fyi',
399                    job_name='positive_culprit',
400                    email='chris@email.com')
401
402    self.testapp.get('/update_bug_with_results')
403    messages = self.mail_stub.get_sent_messages()
404    self.assertEqual(0, len(messages))
405
406  @mock.patch(
407      'google.appengine.api.urlfetch.fetch',
408      mock.MagicMock(side_effect=_MockFetch))
409  @mock.patch.object(
410      update_bug_with_results.bisect_fyi, 'IsBugUpdated',
411      mock.MagicMock(return_value=True))
412  @mock.patch.object(
413      update_bug_with_results.issue_tracker_service, 'IssueTrackerService',
414      mock.MagicMock())
415  def testFYI_Failed_Job_SendEmail(self):
416    stored_object.Set(
417        bisect_fyi._BISECT_FYI_CONFIGS_KEY,
418        bisect_fyi_test.TEST_FYI_CONFIGS)
419    test_config = bisect_fyi_test.TEST_FYI_CONFIGS['positive_culprit']
420    bisect_config = test_config.get('bisect_config')
421    sample_bisect_results = copy.deepcopy(_SAMPLE_BISECT_RESULTS_JSON)
422    sample_bisect_results['status'] = 'failed'
423    self._AddTryJob(12345, 'started', 'win_perf',
424                    results_data=sample_bisect_results,
425                    internal_only=True,
426                    config=utils.BisectConfigPythonString(bisect_config),
427                    job_type='bisect-fyi',
428                    job_name='positive_culprit',
429                    email='chris@email.com')
430
431    self.testapp.get('/update_bug_with_results')
432    messages = self.mail_stub.get_sent_messages()
433    self.assertEqual(1, len(messages))
434
435  @mock.patch.object(
436      update_bug_with_results.quick_logger.QuickLogger,
437      'Log', mock.MagicMock(return_value='record_key_123'))
438  @mock.patch('logging.error')
439  def testUpdateQuickLog_WithJobResults_NoError(self, mock_logging_error):
440    job = self._AddTryJob(111, 'started', 'win_perf',
441                          results_data=_SAMPLE_BISECT_RESULTS_JSON)
442    update_bug_with_results.UpdateQuickLog(job)
443    self.assertEqual(0, mock_logging_error.call_count)
444
445  @mock.patch('logging.error')
446  @mock.patch('update_bug_with_results.quick_logger.QuickLogger.Log')
447  def testUpdateQuickLog_NoResultsData_ReportsError(
448      self, mock_log, mock_logging_error):
449    job = self._AddTryJob(111, 'started', 'win_perf')
450    update_bug_with_results.UpdateQuickLog(job)
451    self.assertEqual(0, mock_log.call_count)
452    mock_logging_error.assert_called_once_with(
453        'Bisect report returns empty for job id %s, bug_id %s.', 1, 111)
454
455  @mock.patch(
456      'google.appengine.api.urlfetch.fetch',
457      mock.MagicMock(side_effect=_MockFetch))
458  @mock.patch.object(
459      update_bug_with_results.issue_tracker_service.IssueTrackerService,
460      'AddBugComment')
461  def testGet_PostResult_WithoutBugEntity(
462      self, mock_update_bug):
463    job = try_job.TryJob(bug_id=12345, status='started', bot='win_perf',
464                         results_data=_SAMPLE_BISECT_RESULTS_JSON)
465    job.put()
466    self.testapp.get('/update_bug_with_results')
467    mock_update_bug.assert_called_once_with(
468        12345, mock.ANY, cc_list=mock.ANY, merge_issue=mock.ANY,
469        labels=mock.ANY, owner=mock.ANY)
470
471
472if __name__ == '__main__':
473  unittest.main()
474