Chromium Code Reviews| 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 |
| 11 import calendar | 11 import calendar |
| 12 from datetime import datetime | 12 from datetime import datetime, timedelta |
|
chanli
2016/06/02 00:55:31
Nit: Imports should be on separate lines.
Please
josiahk
2016/06/07 18:34:41
Done.
| |
| 13 | 13 |
| 14 from google.appengine.api import users | 14 from google.appengine.api import users |
| 15 from google.appengine.ext import ndb | 15 from google.appengine.ext import ndb |
| 16 | 16 |
| 17 from common.base_handler import BaseHandler | 17 from common.base_handler import BaseHandler |
| 18 from common.base_handler import Permission | 18 from common.base_handler import Permission |
| 19 from model.wf_analysis import WfAnalysis | 19 from model.wf_analysis import WfAnalysis |
| 20 from model import result_status | 20 from model import result_status |
| 21 from waterfall import buildbot | 21 from waterfall import buildbot |
| 22 | 22 |
| 23 | 23 # TODO: Is there a better place to put this constant? |
|
chanli
2016/06/02 00:55:31
I think we can just keep this constant here.
josiahk
2016/06/07 18:34:41
Done.
| |
| 24 @ndb.transactional | 24 # TODO: Change this constant to 1 for production. 7 is helpful to get older |
| 25 # results. | |
|
chanli
2016/06/02 00:55:31
Please remove these TODOs before you committing th
josiahk
2016/06/10 18:34:26
Done.
| |
| 26 MATCHING_ANALYSIS_DAYS_AGO = 7 | |
|
stgao
2016/06/03 07:09:34
Leaving it here for this CL is fine. But seems bet
josiahk
2016/06/10 18:34:26
Acknowledged.
| |
| 27 | |
| 28 def genPotentialCulpritTupleList(analysis): | |
|
chanli
2016/06/02 00:55:31
Function name nit:
the convention for function na
josiahk
2016/06/07 18:34:41
Done.
| |
| 29 potentialCulpritTupleList = [] | |
|
chanli
2016/06/02 00:55:31
Naming nit: variable names should be like 'var_nam
josiahk
2016/06/10 18:34:26
Done.
| |
| 30 | |
| 31 # Iterate through the failures, tests, and suspected_cls, appending potential | |
|
chanli
2016/06/02 00:55:31
Nit: Iterates
josiahk
2016/06/07 18:34:41
Done.
| |
| 32 # (step_name, test_name, revision) and (step_name, revision) culprit tuples to | |
| 33 # the list | |
|
chanli
2016/06/02 00:55:31
Nit: ending .
josiahk
2016/06/07 18:34:41
Done.
| |
| 34 for failure in analysis.result["failures"]: | |
|
lijeffrey
2016/06/06 23:00:59
not sure if this has been mentioned in someone els
josiahk
2016/06/07 18:34:41
Done.
| |
| 35 if "tests" in failure: | |
| 36 for test in failure["tests"]: | |
| 37 if "suspected_cls" in test: | |
| 38 for suspected_cl in test["suspected_cls"]: | |
| 39 potentialCulpritTupleList.append(( | |
| 40 failure["step_name"], | |
|
lijeffrey
2016/06/06 23:00:59
4 spaces if putting arguments to a function call o
josiahk
2016/06/10 18:34:26
Done.
| |
| 41 test["test_name"], | |
| 42 suspected_cl["revision"])) | |
| 43 if "suspected_cls" in failure: | |
|
chanli
2016/06/02 00:55:30
Shouldn't here be elif?
josiahk
2016/06/07 18:34:41
I don't think so, but maybe you see a case I don't
josiahk
2016/06/10 18:34:26
Fixed with 'else'.
| |
| 44 for suspected_cl in failure["suspected_cls"]: | |
| 45 potentialCulpritTupleList.append(( | |
| 46 failure["step_name"], | |
| 47 suspected_cl["revision"])) | |
| 48 | |
| 49 return potentialCulpritTupleList | |
| 50 | |
| 51 def doAnalysesMatch(analysis1, analysis2): | |
|
chanli
2016/06/02 00:55:31
argument name nits
Please refer to:
https://engdo
josiahk
2016/06/07 18:34:41
I have renamed to analysis_1 and analysis_2. Is th
josiahk
2016/06/10 18:34:26
Done.
| |
| 52 # Both analyses must have result property | |
| 53 if not analysis1.result or not analysis2.result: | |
| 54 return False | |
| 55 | |
| 56 # Both analyses must have result['failures'] property | |
| 57 if not analysis1.result['failures'] or not analysis2.result['failures']: | |
|
chanli
2016/06/02 00:55:31
analysis1.result.get('failures') or not analysis2.
josiahk
2016/06/07 18:34:41
Ah, thanks! Also, could I use this instead?
if
josiahk
2016/06/10 18:34:26
Relevant code removed.
| |
| 58 return False | |
| 59 | |
| 60 # Get list of potential culprit tuples | |
| 61 potentialCulpritTupleList1 = genPotentialCulpritTupleList(analysis1) | |
| 62 potentialCulpritTupleList2 = genPotentialCulpritTupleList(analysis2) | |
| 63 | |
| 64 # Both analyses must have non-empty potential culprit lists | |
| 65 if not potentialCulpritTupleList1 or not potentialCulpritTupleList2: | |
| 66 return False | |
| 67 | |
| 68 # Both analyses must have matching potential culprit lists | |
| 69 if sorted(potentialCulpritTupleList1) != sorted(potentialCulpritTupleList2): | |
| 70 return False | |
| 71 | |
| 72 return True | |
| 73 | |
| 74 def doAnalysesMatchTests(): | |
|
chanli
2016/06/02 00:55:31
If this is a test for you to monitor some middle r
josiahk
2016/06/10 18:34:26
Done.
| |
| 75 analysis1 = WfAnalysis() | |
| 76 analysis2 = WfAnalysis() | |
| 77 analysis1760 = WfAnalysis() | |
| 78 analysis1761 = WfAnalysis() | |
| 79 analysis1762 = WfAnalysis() | |
| 80 analysis1.result = {u'failures': [{u'first_failure': 49103, u'step_name': | |
| 81 u'content_unittests on Windows-7-SP1', u'last_pass': 49102, u'supported': | |
| 82 True, u'suspected_cls': []}, {u'first_failure': 49103, u'step_name': | |
| 83 u'content_browsertests on Windows-7-SP1', u'last_pass': 49102, u'supported': | |
| 84 True, u'suspected_cls': []}, {u'first_failure': 49103, u'step_name': | |
| 85 u'browser_tests on Windows-7-SP1', u'last_pass': 49102, u'supported': True, | |
| 86 u'suspected_cls': []}]} | |
| 87 analysis2.result = {u'failures': [{u'first_failure': 1684, u'step_name': | |
| 88 u'interactive_ui_tests', u'suspected_cls': [{u'repo_name': u'chromium', | |
| 89 u'hints': {u'modified site_per_process_interactive_browsertest.cc[540, 464,'+\ | |
| 90 ' 580, 380] (and it was in log)': 4}, u'commit_position': 395500, | |
| 91 u'build_number': 1684, u'revision': | |
| 92 u'3cf9343f4602d4ec11717cb6ff56a793c1d5f84b', u'url': | |
|
stgao
2016/06/03 07:09:34
Format and move the test to the corresponding unit
josiahk
2016/06/10 18:34:26
Done.
| |
| 93 u'https://codereview.chromium.org/1914643005', u'score': 4}], u'supported': | |
| 94 True, u'last_pass': 1683}]} | |
| 95 analysis1760.result = {u'failures': [{u'first_failure': 1739, u'last_pass': | |
| 96 None, u'supported': True, u'suspected_cls': [], u'step_name': | |
| 97 u'browser_tests'}, {u'first_failure': 1739, u'last_pass': None, u'supported': | |
| 98 True, u'suspected_cls': [{u'hints': {u'modified'+\ | |
| 99 ' dump_accessibility_browsertest_base.cc (and it was in log)': 2}, | |
| 100 u'build_number': 1753, u'score': 2, u'url': | |
| 101 u'https://codereview.chromium.org/2023453003', u'commit_position': 396751, | |
| 102 u'revision': u'0e8dc209f5e4a6140e43551de0e036324c68a383', u'repo_name': | |
| 103 u'chromium'}], u'step_name': u'content_browsertests'}]} | |
| 104 analysis1761.result = {u'failures': [{u'supported': True, u'last_pass': None, | |
| 105 u'suspected_cls': [], u'step_name': u'browser_tests', u'first_failure': 1740}, | |
| 106 {u'supported': True, u'last_pass': None, u'suspected_cls': [{u'hints': | |
| 107 {u'modified dump_accessibility_browsertest_base.cc (and it was in log)': 2}, | |
| 108 u'url': u'https://codereview.chromium.org/2023453003', u'repo_name': | |
| 109 u'chromium', u'revision': u'0e8dc209f5e4a6140e43551de0e036324c68a383', | |
| 110 u'score': 2, u'commit_position': 396751, u'build_number': 1753}], | |
| 111 u'step_name': u'content_browsertests', u'first_failure': 1740}]} | |
| 112 analysis1762.result = {u'failures': [{u'suspected_cls': [], u'first_failure': | |
| 113 1741, u'step_name': u'browser_tests', u'last_pass': None, u'supported': True}, | |
| 114 {u'suspected_cls': [{u'url': u'https://codereview.chromium.org/2023453003', | |
| 115 u'build_number': 1753, u'revision': | |
| 116 u'0e8dc209f5e4a6140e43551de0e036324c68a383', u'commit_position': 396751, | |
| 117 u'hints': {u'modified dump_accessibility_browsertest_base.cc (and it was in'+\ | |
| 118 ' log)': 2}, u'repo_name': u'chromium', u'score': 2}, {u'url': | |
| 119 u'https://codereview.chromium.org/2024053003', u'build_number': 1762, | |
| 120 u'revision': u'292b41bbd603ae2f11d239f457a8a5f04387fa85', u'commit_position': | |
| 121 396830, | |
| 122 u'hints': {u'modified dump_accessibility_events_browsertest.cc (and it'+\ | |
| 123 ' was in log)': 2}, u'repo_name': u'chromium', u'score': 2}, {u'url': | |
| 124 u'https://codereview.chromium.org/2025923002', u'build_number': 1762, | |
| 125 u'revision': u'f6c9ef029e28a6bef28e727cd70751d782963e21', u'commit_position': | |
| 126 396833, u'hints': {u'modified dump_accessibility_events_browsertest.cc[166]'+\ | |
| 127 ' (and it was in log)': 4}, u'repo_name': u'chromium', u'score': 4}], | |
| 128 u'first_failure': 1741, u'step_name': u'content_browsertests', u'last_pass': | |
| 129 None, u'supported': True}]} | |
| 130 assert not doAnalysesMatch(analysis1, analysis2) # Different analyses | |
| 131 assert not doAnalysesMatch(analysis1, analysis1) # Zero culprit-tuples | |
| 132 assert doAnalysesMatch(analysis2, analysis2) # Has culprit-tuples | |
| 133 assert doAnalysesMatch(analysis1760, analysis1761) | |
| 134 assert not doAnalysesMatch(analysis1761, analysis1762) | |
| 135 | |
| 136 # TODO: Put this back (uncomment @ndb.transactional) | |
| 137 # @ndb.transactional | |
|
chanli
2016/06/02 00:55:31
Just a thought: I didn't do the experiment but it
stgao
2016/06/03 07:09:34
As we are now dealing with multiple NDB entities,
josiahk
2016/06/07 18:34:41
Yay! The concept of your idea works!
Except... _Tr
stgao
2016/06/13 23:39:49
That's due to Cross-NDB-group query. I had a comme
| |
| 25 def _UpdateAnalysisResultStatus( | 138 def _UpdateAnalysisResultStatus( |
| 26 master_name, builder_name, build_number, correct, user_name=None): | 139 master_name, builder_name, build_number, correct, user_name=None): |
| 140 doAnalysesMatchTests() | |
|
chanli
2016/06/02 00:55:31
Move this to ./test/triage_analysis_test.py
josiahk
2016/06/10 18:34:26
Done.
| |
| 141 | |
| 27 analysis = WfAnalysis.Get(master_name, builder_name, build_number) | 142 analysis = WfAnalysis.Get(master_name, builder_name, build_number) |
| 28 if not analysis or not analysis.completed: | 143 if not analysis or not analysis.completed: |
| 29 return False | 144 return False |
| 30 | 145 |
| 31 if correct: | 146 # TODO: Limit query range to only yesterday and not today and yesterday. |
|
chanli
2016/06/02 00:55:31
To do it, you can also specify end date
josiahk
2016/06/07 18:34:41
Done.
| |
| 32 if analysis.suspected_cls: | 147 start_date = datetime.utcnow() - timedelta(MATCHING_ANALYSIS_DAYS_AGO) |
| 33 analysis.result_status = result_status.FOUND_CORRECT | 148 start_date = start_date.replace(hour=0, minute=0, second=0, microsecond=0) |
| 34 analysis.culprit_cls = analysis.suspected_cls | 149 |
| 150 # TODO: Remove helpful-for-development FOUND_CORRECT and FOUND_INCORRECT | |
| 151 # (use commented code instead). | |
| 152 analysis_results = WfAnalysis.query(ndb.AND( | |
| 153 WfAnalysis.build_start_time >= start_date, | |
| 154 ndb.OR( | |
|
chanli
2016/06/02 00:55:31
IMO, You should only query for FOUND_UNTRIAGED her
josiahk
2016/06/10 18:34:25
Done.
| |
| 155 WfAnalysis.result_status == result_status.FOUND_UNTRIAGED, | |
| 156 WfAnalysis.result_status == result_status.FOUND_CORRECT, | |
| 157 WfAnalysis.result_status == result_status.FOUND_INCORRECT | |
| 158 ))).order( | |
| 159 -WfAnalysis.build_start_time).fetch() | |
|
chanli
2016/06/02 00:55:31
No need to order
josiahk
2016/06/07 18:34:41
Done.
| |
| 160 # analysis_results = WfAnalysis.query(ndb.AND( | |
| 161 # WfAnalysis.build_start_time >= start_date, | |
| 162 # WfAnalysis.result_status == result_status.FOUND_UNTRIAGED | |
| 163 # )).order( | |
| 164 # -WfAnalysis.build_start_time).fetch() | |
| 165 | |
| 166 # TODO: Remove helpful-for-development print statements | |
| 167 print | |
| 168 print "############## ALL ANALYSES (%d) ##############" % len( | |
| 169 analysis_results) | |
| 170 for i, a in enumerate(analysis_results): | |
|
chanli
2016/06/02 00:55:31
Use descriptive variable names
josiahk
2016/06/10 18:34:26
Relevant code removed.
| |
| 171 print "======= Analysis #%d, Build number: %d =======:" \ | |
| 172 % (i, a.build_number) | |
| 173 print a | |
| 174 print | |
| 175 | |
| 176 # Generate list of matching build analyses | |
| 177 matchingAnalyses = [a for a in analysis_results if | |
| 178 doAnalysesMatch(a, analysis)] | |
| 179 | |
| 180 # TODO: Detect if current analysis was already in the list of matching | |
| 181 # analyses; if so, don't re-add it. | |
| 182 | |
| 183 # Add current analysis to the list of matching analyses | |
| 184 matchingAnalyses.append(analysis) | |
| 185 | |
| 186 # TODO: Remove helpful-for-development print statements | |
| 187 print | |
| 188 print "############## MATCHING ANALYSES (%d) ##############" % len( | |
| 189 matchingAnalyses) | |
| 190 for i, ma in enumerate(matchingAnalyses): | |
| 191 print "======= MatchingAnalysis #%d, Build number: %d =======:" \ | |
| 192 % (i, ma.build_number) | |
| 193 print ma | |
| 194 print | |
| 195 | |
| 196 for a in matchingAnalyses: | |
| 197 if correct: | |
| 198 if a.suspected_cls: | |
| 199 a.result_status = result_status.FOUND_CORRECT | |
| 200 a.culprit_cls = a.suspected_cls | |
| 201 else: | |
| 202 a.result_status = result_status.NOT_FOUND_CORRECT | |
| 203 a.culprit_cls = None | |
| 35 else: | 204 else: |
| 36 analysis.result_status = result_status.NOT_FOUND_CORRECT | 205 a.culprit_cls = None |
| 37 analysis.culprit_cls = None | 206 if a.suspected_cls: |
| 38 else: | 207 a.result_status = result_status.FOUND_INCORRECT |
| 39 analysis.culprit_cls = None | 208 else: |
| 40 if analysis.suspected_cls: | 209 a.result_status = result_status.NOT_FOUND_INCORRECT |
| 41 analysis.result_status = result_status.FOUND_INCORRECT | 210 |
| 42 else: | 211 triage_record = { |
| 43 analysis.result_status = result_status.NOT_FOUND_INCORRECT | 212 'triage_timestamp': calendar.timegm(datetime.utcnow().timetuple()), |
| 44 | 213 'user_name': user_name, |
| 45 triage_record = { | 214 'result_status': a.result_status, |
| 46 'triage_timestamp': calendar.timegm(datetime.utcnow().timetuple()), | 215 'version': a.version, |
| 47 'user_name': user_name, | 216 } |
| 48 'result_status': analysis.result_status, | 217 if not a.triage_history: |
| 49 'version': analysis.version, | 218 a.triage_history = [] |
| 50 } | 219 a.triage_history.append(triage_record) |
| 51 if not analysis.triage_history: | 220 |
| 52 analysis.triage_history = [] | 221 a.put() |
| 53 analysis.triage_history.append(triage_record) | |
| 54 | |
| 55 analysis.put() | |
| 56 return True | 222 return True |
| 57 | 223 |
| 58 | 224 |
| 59 class TriageAnalysis(BaseHandler): | 225 class TriageAnalysis(BaseHandler): |
| 60 PERMISSION_LEVEL = Permission.CORP_USER | 226 PERMISSION_LEVEL = Permission.CORP_USER |
| 61 | 227 |
| 62 def HandleGet(self): # pragma: no cover | 228 def HandleGet(self): # pragma: no cover |
| 63 return self.HandlePost() | 229 return self.HandlePost() |
| 64 | 230 |
| 65 def HandlePost(self): | 231 def HandlePost(self): |
| 66 """Sets the manual triage result for the analysis. | 232 """Sets the manual triage result for the analysis. |
| 67 | 233 |
| 68 Mark the analysis result as correct/wrong/etc. | 234 Mark the analysis result as correct/wrong/etc. |
| 69 TODO: make it possible to set the real culprit CLs. | 235 TODO: make it possible to set the real culprit CLs. |
| 70 """ | 236 """ |
| 71 url = self.request.get('url').strip() | 237 url = self.request.get('url').strip() |
| 72 build_info = buildbot.ParseBuildUrl(url) | 238 build_info = buildbot.ParseBuildUrl(url) |
| 73 if not build_info: | 239 if not build_info: |
| 74 return {'data': {'success': False}} | 240 return {'data': {'success': False}} |
| 75 master_name, builder_name, build_number = build_info | 241 master_name, builder_name, build_number = build_info |
| 76 | 242 |
| 77 correct = self.request.get('correct').lower() == 'true' | 243 correct = self.request.get('correct').lower() == 'true' |
| 78 # As the permission level is CORP_USER, we could assume the current user | 244 # As the permission level is CORP_USER, we could assume the current user |
| 79 # already logged in. | 245 # already logged in. |
| 80 user_name = users.get_current_user().email().split('@')[0] | 246 user_name = users.get_current_user().email().split('@')[0] |
| 81 success = _UpdateAnalysisResultStatus( | 247 success = _UpdateAnalysisResultStatus( |
| 82 master_name, builder_name, build_number, correct, user_name) | 248 master_name, builder_name, build_number, correct, user_name) |
| 83 return {'data': {'success': success}} | 249 return {'data': {'success': success}} |
| OLD | NEW |