OLD | NEW |
1 # Copyright 2016 The Chromium Authors. All rights reserved. | 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 | 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 collections | 5 import collections |
6 import logging | 6 import logging |
7 import os | 7 import os |
8 import traceback | 8 import traceback |
9 | 9 |
10 from google.appengine.api import taskqueue | 10 from google.appengine.api import taskqueue |
(...skipping 96 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
107 return 'https://%s/job/%s' % (os.environ['HTTP_HOST'], self.job_id) | 107 return 'https://%s/job/%s' % (os.environ['HTTP_HOST'], self.job_id) |
108 | 108 |
109 def AddChange(self, change): | 109 def AddChange(self, change): |
110 self.state.AddChange(change) | 110 self.state.AddChange(change) |
111 | 111 |
112 def Start(self): | 112 def Start(self): |
113 self.Schedule() | 113 self.Schedule() |
114 self._PostBugComment('started') | 114 self._PostBugComment('started') |
115 | 115 |
116 def Complete(self): | 116 def Complete(self): |
117 self._PostBugComment('completed') | 117 self._PostBugComment('completed', include_differences=True) |
118 | 118 |
119 def Fail(self): | 119 def Fail(self): |
120 self.exception = traceback.format_exc() | 120 self.exception = traceback.format_exc() |
121 self._PostBugComment('stopped with an error') | 121 self._PostBugComment('stopped with an error', include_differences=True) |
122 | 122 |
123 def Schedule(self): | 123 def Schedule(self): |
124 task = taskqueue.add(queue_name='job-queue', url='/api/run/' + self.job_id, | 124 task = taskqueue.add(queue_name='job-queue', url='/api/run/' + self.job_id, |
125 countdown=_TASK_INTERVAL) | 125 countdown=_TASK_INTERVAL) |
126 self.task = task.name | 126 self.task = task.name |
127 | 127 |
128 def Run(self): | 128 def Run(self): |
129 self.exception = None # In case the Job succeeds on retry. | 129 self.exception = None # In case the Job succeeds on retry. |
130 self.task = None # In case an exception is thrown. | 130 self.task = None # In case an exception is thrown. |
131 | 131 |
(...skipping 21 matching lines...) Expand all Loading... |
153 | 153 |
154 'created': self.created.isoformat(), | 154 'created': self.created.isoformat(), |
155 'updated': self.updated.isoformat(), | 155 'updated': self.updated.isoformat(), |
156 'exception': self.exception, | 156 'exception': self.exception, |
157 'status': self.status, | 157 'status': self.status, |
158 } | 158 } |
159 if include_state: | 159 if include_state: |
160 d.update(self.state.AsDict()) | 160 d.update(self.state.AsDict()) |
161 return d | 161 return d |
162 | 162 |
163 def _PostBugComment(self, status): | 163 def _PostBugComment(self, status, include_differences=False): |
164 if not self.bug_id: | 164 if not self.bug_id: |
165 return | 165 return |
166 | 166 |
167 title = '%s Pinpoint job %s.' % (_ROUND_PUSHPIN, status) | 167 title = '%s Pinpoint job %s.' % (_ROUND_PUSHPIN, status) |
168 header = '\n'.join((title, self.url)) | 168 header = '\n'.join((title, self.url)) |
169 | 169 |
170 # Include list of Changes. | |
171 change_details = [] | 170 change_details = [] |
172 for _, change in self.state.Differences(): | 171 if include_differences: |
173 # TODO: Store the commit info in the Commit. | 172 # Include list of Changes. |
174 commit = change.last_commit | 173 differences = tuple(self.state.Differences()) |
175 commit_info = gitiles_service.CommitInfo(commit.repository_url, | 174 if differences: |
176 commit.git_hash) | 175 if len(differences) == 1: |
177 subject = '<b>%s</b>' % commit_info['message'].split('\n', 1)[0] | 176 change_details.append( |
178 author = commit_info['author']['email'] | 177 '<b>Found significant differences after 1 commit:</b>') |
179 time = commit_info['committer']['time'] | 178 else: |
180 | 179 change_details.append( |
181 byline = 'By %s %s %s' % (author, _MIDDLE_DOT, time) | 180 '<b>Found significant differences after each of %d commits:</b>' % |
182 git_link = commit.repository + '@' + commit.git_hash | 181 len(differences)) |
183 change_details.append('\n'.join((subject, byline, git_link))) | 182 for _, change in differences: |
| 183 change_details.append(_FormatChangeForBug(change)) |
| 184 else: |
| 185 change_details.append("<b>Couldn't reproduce a difference.</b>") |
184 | 186 |
185 comment = '\n\n'.join([header] + change_details) | 187 comment = '\n\n'.join([header] + change_details) |
186 | 188 |
187 issue_tracker = issue_tracker_service.IssueTrackerService( | 189 issue_tracker = issue_tracker_service.IssueTrackerService( |
188 utils.ServiceAccountHttp()) | 190 utils.ServiceAccountHttp()) |
189 issue_tracker.AddBugComment(self.bug_id, comment, send_email=False) | 191 issue_tracker.AddBugComment(self.bug_id, comment, send_email=False) |
190 | 192 |
191 | 193 |
192 class _JobState(object): | 194 class _JobState(object): |
193 """The internal state of a Job. | 195 """The internal state of a Job. |
(...skipping 151 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
345 # Here, "the same" means that we fail to reject the null hypothesis. We can | 347 # Here, "the same" means that we fail to reject the null hypothesis. We can |
346 # never be completely sure that the two Changes have the same results, but | 348 # never be completely sure that the two Changes have the same results, but |
347 # we've run everything that we planned to, and didn't detect any difference. | 349 # we've run everything that we planned to, and didn't detect any difference. |
348 if (len(attempts_a) >= self._repeat_count and | 350 if (len(attempts_a) >= self._repeat_count and |
349 len(attempts_b) >= self._repeat_count): | 351 len(attempts_b) >= self._repeat_count): |
350 return _SAME | 352 return _SAME |
351 | 353 |
352 return _UNKNOWN | 354 return _UNKNOWN |
353 | 355 |
354 | 356 |
| 357 def _FormatChangeForBug(change): |
| 358 # TODO: Store the commit info in the Commit. |
| 359 commit = change.last_commit |
| 360 commit_info = gitiles_service.CommitInfo(commit.repository_url, |
| 361 commit.git_hash) |
| 362 subject = '<b>%s</b>' % commit_info['message'].split('\n', 1)[0] |
| 363 author = commit_info['author']['email'] |
| 364 time = commit_info['committer']['time'] |
| 365 |
| 366 byline = 'By %s %s %s' % (author, _MIDDLE_DOT, time) |
| 367 git_link = commit.repository + '@' + commit.git_hash |
| 368 return '\n'.join((subject, byline, git_link)) |
| 369 |
| 370 |
355 def _CombineResultsPerQuest(attempts): | 371 def _CombineResultsPerQuest(attempts): |
356 aggregate_results = collections.defaultdict(list) | 372 aggregate_results = collections.defaultdict(list) |
357 for attempt in attempts: | 373 for attempt in attempts: |
358 if not attempt.completed: | 374 if not attempt.completed: |
359 continue | 375 continue |
360 | 376 |
361 for quest, results in attempt.result_values.iteritems(): | 377 for quest, results in attempt.result_values.iteritems(): |
362 aggregate_results[quest] += results | 378 aggregate_results[quest] += results |
363 | 379 |
364 return aggregate_results | 380 return aggregate_results |
365 | 381 |
366 | 382 |
367 def _CompareValues(values_a, values_b): | 383 def _CompareValues(values_a, values_b): |
368 if not (values_a and values_b): | 384 if not (values_a and values_b): |
369 return _UNKNOWN | 385 return _UNKNOWN |
370 | 386 |
371 try: | 387 try: |
372 p_value = mann_whitney_u.MannWhitneyU(values_a, values_b) | 388 p_value = mann_whitney_u.MannWhitneyU(values_a, values_b) |
373 except ValueError: | 389 except ValueError: |
374 return _UNKNOWN | 390 return _UNKNOWN |
375 | 391 |
376 if p_value < _SIGNIFICANCE_LEVEL: | 392 if p_value < _SIGNIFICANCE_LEVEL: |
377 return _DIFFERENT | 393 return _DIFFERENT |
378 else: | 394 else: |
379 return _UNKNOWN | 395 return _UNKNOWN |
OLD | NEW |