| OLD | NEW |
| 1 # Copyright 2014 The Chromium Authors. All rights reserved. | 1 # Copyright 2014 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 """This module is to handle manual triage of analysis result. | 5 """This module is to handle manual triage of analysis result. |
| 6 | 6 |
| 7 This handler will flag the analysis result as correct or incorrect. | 7 This handler will flag the analysis result as correct or incorrect. |
| 8 TODO: work on an automatic or semi-automatic way to triage analysis result. | 8 TODO: work on an automatic or semi-automatic way to triage analysis result. |
| 9 """ | 9 """ |
| 10 | 10 |
| (...skipping 72 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 83 | 83 |
| 84 # Both analyses must have non-empty potential culprit lists. | 84 # Both analyses must have non-empty potential culprit lists. |
| 85 if not potential_culprit_tuple_list_1 or not potential_culprit_tuple_list_2: | 85 if not potential_culprit_tuple_list_1 or not potential_culprit_tuple_list_2: |
| 86 return False | 86 return False |
| 87 | 87 |
| 88 # Both analyses must have matching potential culprit lists. | 88 # Both analyses must have matching potential culprit lists. |
| 89 return (sorted(potential_culprit_tuple_list_1) == | 89 return (sorted(potential_culprit_tuple_list_1) == |
| 90 sorted(potential_culprit_tuple_list_2)) | 90 sorted(potential_culprit_tuple_list_2)) |
| 91 | 91 |
| 92 | 92 |
| 93 def _AppendTriageHistoryRecord(analysis, is_correct, user_name): | 93 def _AppendTriageHistoryRecord( |
| 94 analysis, is_correct, user_name, is_duplicate=False): |
| 94 """Appends a triage history record to the given analysis. | 95 """Appends a triage history record to the given analysis. |
| 95 | 96 |
| 96 Args: | 97 Args: |
| 97 analysis: The analysis to which to append the history record. | 98 analysis: The analysis to which to append the history record. |
| 98 is_correct: True if the history record should indicate a correct judgement, | 99 is_correct: True if the history record should indicate a correct judgement, |
| 99 otherwise False. | 100 otherwise False. |
| 100 user_name: The user_name of the person to include in the triage record. | 101 user_name: The user_name of the person to include in the triage record. |
| 102 is_duplicate: Whether or not this analysis is a duplicate of another |
| 103 analysis. If this analysis is a duplicate, then set the result_status |
| 104 accordingly. If this analysis is not a duplicate, reset the reference to |
| 105 the 'first-cause' analaysis. |
| 101 """ | 106 """ |
| 102 if is_correct: | 107 if is_correct: |
| 103 if analysis.suspected_cls: | 108 if analysis.suspected_cls: |
| 104 analysis.result_status = result_status.FOUND_CORRECT | 109 if is_duplicate: |
| 110 analysis.result_status = result_status.FOUND_CORRECT_DUPLICATE |
| 111 else: |
| 112 analysis.result_status = result_status.FOUND_CORRECT |
| 105 analysis.culprit_cls = analysis.suspected_cls | 113 analysis.culprit_cls = analysis.suspected_cls |
| 106 else: | 114 else: |
| 107 analysis.result_status = result_status.NOT_FOUND_CORRECT | 115 analysis.result_status = result_status.NOT_FOUND_CORRECT |
| 108 analysis.culprit_cls = None | 116 analysis.culprit_cls = None |
| 109 else: | 117 else: |
| 110 analysis.culprit_cls = None | 118 analysis.culprit_cls = None |
| 111 if analysis.suspected_cls: | 119 if analysis.suspected_cls: |
| 112 analysis.result_status = result_status.FOUND_INCORRECT | 120 if is_duplicate: |
| 121 analysis.result_status = result_status.FOUND_INCORRECT_DUPLICATE |
| 122 else: |
| 123 analysis.result_status = result_status.FOUND_INCORRECT |
| 113 else: | 124 else: |
| 114 analysis.result_status = result_status.NOT_FOUND_INCORRECT | 125 analysis.result_status = result_status.NOT_FOUND_INCORRECT |
| 115 | 126 |
| 127 if not is_duplicate: |
| 128 # Resets the reference to the 'first-cause' triage analysis. |
| 129 # When another 'first-cause' build analysis is triaged, and this build |
| 130 # analysis is marked as a duplicate from that other 'first-cause' build |
| 131 # analysis, these are the variables that hold the reference back to that |
| 132 # 'first-cause' build analysis. It's possible that someone could then |
| 133 # manually re-triage this build analysis, in which case this build analysis |
| 134 # is no longer a duplicate, and we want to erase the reference to the |
| 135 # no-longer-relevant 'first-cause' build_analysis. |
| 136 analysis.triage_reference_analysis_master_name = None |
| 137 analysis.triage_reference_analysis_builder_name = None |
| 138 analysis.triage_reference_analysis_build_number = None |
| 139 |
| 116 triage_record = { | 140 triage_record = { |
| 117 'triage_timestamp': calendar.timegm(datetime.utcnow().timetuple()), | 141 'triage_timestamp': calendar.timegm(datetime.utcnow().timetuple()), |
| 118 'user_name': user_name, | 142 'user_name': user_name, |
| 119 'result_status': analysis.result_status, | 143 'result_status': analysis.result_status, |
| 120 'version': analysis.version, | 144 'version': analysis.version, |
| 121 } | 145 } |
| 122 if not analysis.triage_history: | 146 if not analysis.triage_history: |
| 123 analysis.triage_history = [] | 147 analysis.triage_history = [] |
| 124 analysis.triage_history.append(triage_record) | 148 analysis.triage_history.append(triage_record) |
| 125 | 149 |
| 126 analysis.put() | 150 analysis.put() |
| 127 | 151 |
| 128 | 152 |
| 129 @ndb.transactional | 153 @ndb.transactional |
| 130 def _UpdateAnalysisResultStatus( | 154 def _UpdateAnalysisResultStatus( |
| 131 master_name, builder_name, build_number, is_correct, user_name=None): | 155 master_name, builder_name, build_number, is_correct, user_name=None): |
| 132 analysis = WfAnalysis.Get(master_name, builder_name, build_number) | 156 analysis = WfAnalysis.Get(master_name, builder_name, build_number) |
| 133 if not analysis or not analysis.completed: | 157 if not analysis or not analysis.completed: |
| 134 return False, None | 158 return False, None |
| 135 | 159 |
| 136 _AppendTriageHistoryRecord(analysis, is_correct, user_name) | 160 _AppendTriageHistoryRecord(analysis, is_correct, user_name, |
| 161 is_duplicate=False) |
| 137 | 162 |
| 138 return True, analysis | 163 return True, analysis |
| 139 | 164 |
| 140 | 165 |
| 141 def _GetDuplicateAnalyses(original_analysis): | 166 def _GetDuplicateAnalyses(original_analysis): |
| 142 start_time = (original_analysis.build_start_time - | 167 start_time = (original_analysis.build_start_time - |
| 143 timedelta(hours=MATCHING_ANALYSIS_HOURS_AGO_START)) | 168 timedelta(hours=MATCHING_ANALYSIS_HOURS_AGO_START)) |
| 144 end_time = (original_analysis.build_start_time + | 169 end_time = (original_analysis.build_start_time + |
| 145 timedelta(hours=MATCHING_ANALYSIS_HOURS_AGO_END)) | 170 timedelta(hours=MATCHING_ANALYSIS_HOURS_AGO_END)) |
| 146 | 171 |
| 147 # Don't count any analyses from today (except for exactly at midnight local | 172 # Don't count any analyses from today (except for exactly at midnight local |
| 148 # time). | 173 # time). |
| 149 # Get current time (UTC). | 174 # Get current time (UTC). |
| 150 current_time_as_utc = pytz.utc.localize(datetime.utcnow()) | 175 current_time_as_utc = pytz.utc.localize(datetime.utcnow()) |
| 151 | 176 |
| 152 # Convert to local time. | 177 # Convert to local time. |
| 153 current_time_as_local = current_time_as_utc.astimezone( | 178 current_time_as_local = current_time_as_utc.astimezone( |
| 154 pytz.timezone(MATCHING_ANALYSIS_END_BOUND_TIME_ZONE)) | 179 pytz.timezone(MATCHING_ANALYSIS_END_BOUND_TIME_ZONE)) |
| 155 | 180 |
| 156 # Set hours and minutes to 0 to get midnight. | 181 # Set hours and minutes to 0 to get midnight. |
| 157 local_midnight_as_local = current_time_as_local.replace( | 182 local_midnight_as_local = current_time_as_local.replace( |
| 158 hour=0, minute=0, second=0, microsecond=0) | 183 hour=0, minute=0, second=0, microsecond=0) |
| 159 | 184 |
| 160 # Convert back to UTC time. | 185 # Convert back to UTC time. |
| 161 local_midnight_as_utc = local_midnight_as_local.astimezone(pytz.utc) | 186 local_midnight_as_utc = local_midnight_as_local.astimezone(pytz.utc) |
| 162 | 187 |
| 163 # Strip timezone. | 188 # Strip timezone. |
| 164 local_midnight = local_midnight_as_utc.replace(tzinfo=None) | 189 local_midnight = local_midnight_as_utc.replace(tzinfo=None) |
| 165 | 190 |
| 166 if end_time > local_midnight: # pragma: no branch | 191 if end_time > local_midnight: |
| 167 end_time = local_midnight | 192 end_time = local_midnight |
| 168 | 193 |
| 169 # Retrieve potential duplicate build analyses. | 194 # Retrieve potential duplicate build analyses. |
| 170 analysis_results = WfAnalysis.query(ndb.AND( | 195 analysis_results = WfAnalysis.query(ndb.AND( |
| 171 WfAnalysis.build_start_time >= start_time, | 196 WfAnalysis.build_start_time >= start_time, |
| 172 WfAnalysis.build_start_time <= end_time, | 197 WfAnalysis.build_start_time <= end_time, |
| 173 WfAnalysis.result_status == result_status.FOUND_UNTRIAGED | 198 WfAnalysis.result_status == result_status.FOUND_UNTRIAGED |
| 174 )).fetch() | 199 )).fetch() |
| 175 | 200 |
| 176 # Further filter potential duplicates and return them. | 201 # Further filter potential duplicates and return them. |
| 177 return [analysis for analysis in analysis_results if | 202 return [analysis for analysis in analysis_results if |
| 203 analysis.completed and |
| 204 analysis.result and |
| 178 _DoAnalysesMatch(original_analysis, analysis) and | 205 _DoAnalysesMatch(original_analysis, analysis) and |
| 179 original_analysis.key is not analysis.key and | 206 original_analysis.key is not analysis.key] |
| 180 analysis.completed] | |
| 181 | 207 |
| 182 | 208 |
| 183 def _TriageDuplicateResults(original_analysis, is_correct, user_name=None): | 209 def _TriageAndCountDuplicateResults(original_analysis, is_correct, |
| 210 user_name=None): |
| 184 matching_analyses = _GetDuplicateAnalyses(original_analysis) | 211 matching_analyses = _GetDuplicateAnalyses(original_analysis) |
| 185 | 212 |
| 186 for analysis in matching_analyses: | 213 for analysis in matching_analyses: |
| 187 _AppendTriageHistoryRecord(analysis, is_correct, user_name) | 214 analysis.triage_reference_analysis_master_name = ( |
| 215 original_analysis.master_name) |
| 216 analysis.triage_reference_analysis_builder_name = ( |
| 217 original_analysis.builder_name) |
| 218 analysis.triage_reference_analysis_build_number = ( |
| 219 original_analysis.build_number) |
| 220 _AppendTriageHistoryRecord(analysis, is_correct, user_name, |
| 221 is_duplicate=True) |
| 222 |
| 223 return len(matching_analyses) |
| 188 | 224 |
| 189 | 225 |
| 190 class TriageAnalysis(BaseHandler): | 226 class TriageAnalysis(BaseHandler): |
| 191 PERMISSION_LEVEL = Permission.CORP_USER | 227 PERMISSION_LEVEL = Permission.CORP_USER |
| 192 | 228 |
| 193 def HandleGet(self): # pragma: no cover | 229 def HandleGet(self): # pragma: no cover |
| 194 return self.HandlePost() | 230 return self.HandlePost() |
| 195 | 231 |
| 196 def HandlePost(self): | 232 def HandlePost(self): |
| 197 """Sets the manual triage result for the analysis. | 233 """Sets the manual triage result for the analysis. |
| 198 | 234 |
| 199 Mark the analysis result as correct/wrong/etc. | 235 Mark the analysis result as correct/wrong/etc. |
| 200 TODO: make it possible to set the real culprit CLs. | 236 TODO: make it possible to set the real culprit CLs. |
| 201 """ | 237 """ |
| 202 url = self.request.get('url').strip() | 238 url = self.request.get('url').strip() |
| 203 build_info = buildbot.ParseBuildUrl(url) | 239 build_info = buildbot.ParseBuildUrl(url) |
| 204 if not build_info: | 240 if not build_info: |
| 205 return {'data': {'success': False}} | 241 return {'data': {'success': False}} |
| 206 master_name, builder_name, build_number = build_info | 242 master_name, builder_name, build_number = build_info |
| 207 | 243 |
| 208 is_correct = self.request.get('correct').lower() == 'true' | 244 is_correct = self.request.get('correct').lower() == 'true' |
| 209 # As the permission level is CORP_USER, we could assume the current user | 245 # As the permission level is CORP_USER, we could assume the current user |
| 210 # already logged in. | 246 # already logged in. |
| 211 user_name = users.get_current_user().email().split('@')[0] | 247 user_name = users.get_current_user().email().split('@')[0] |
| 212 success, original_analysis = _UpdateAnalysisResultStatus( | 248 success, original_analysis = _UpdateAnalysisResultStatus( |
| 213 master_name, builder_name, build_number, is_correct, user_name) | 249 master_name, builder_name, build_number, is_correct, user_name) |
| 250 num_duplicate_analyses = 0 |
| 214 if success: | 251 if success: |
| 215 _TriageDuplicateResults(original_analysis, is_correct, user_name) | 252 num_duplicate_analyses = _TriageAndCountDuplicateResults( |
| 216 return {'data': {'success': success}} | 253 original_analysis, is_correct, user_name) |
| 254 return {'data': {'success': success, |
| 255 'num_duplicate_analyses': num_duplicate_analyses}} |
| OLD | NEW |