| 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 from collections import defaultdict | 5 from collections import defaultdict |
| 6 import copy | |
| 7 from datetime import datetime | 6 from datetime import datetime |
| 8 import logging | |
| 9 import os | |
| 10 | 7 |
| 11 from google.appengine.api import users | 8 from google.appengine.api import users |
| 12 | 9 |
| 13 from common import constants | 10 from common import constants |
| 14 from common.base_handler import BaseHandler | 11 from common.base_handler import BaseHandler |
| 15 from common.base_handler import Permission | 12 from common.base_handler import Permission |
| 16 from common.waterfall import failure_type | 13 from common.waterfall import failure_type |
| 17 from handlers import handlers_util | 14 from handlers import handlers_util |
| 18 from handlers import result_status | 15 from handlers import result_status |
| 19 from handlers.result_status import NO_TRY_JOB_REASON_MAP | 16 from handlers.result_status import NO_TRY_JOB_REASON_MAP |
| (...skipping 120 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 140 'last_pass': test_result.get('last_pass'), | 137 'last_pass': test_result.get('last_pass'), |
| 141 'supported': supported, | 138 'supported': supported, |
| 142 'tests': group['tests'], | 139 'tests': group['tests'], |
| 143 'suspected_cls': group['suspected_cls'] | 140 'suspected_cls': group['suspected_cls'] |
| 144 } | 141 } |
| 145 organized_suspected_cls.append(shared_result) | 142 organized_suspected_cls.append(shared_result) |
| 146 | 143 |
| 147 return organized_results | 144 return organized_results |
| 148 | 145 |
| 149 | 146 |
| 150 def _GetAnalysisResultWithTryJobInfo( | 147 def _GetAnalysisResultWithTryJobInfo(show_debug_info, organized_results, |
| 151 organized_results, master_name, builder_name, build_number): | 148 master_name, builder_name, build_number): |
| 152 """Reorganizes analysis result and try job result by step_name and culprit. | 149 """Reorganizes analysis result and try job result by step_name and culprit. |
| 153 | 150 |
| 154 Returns: | 151 Returns: |
| 155 update_result (dict): A dict of classified results. | 152 update_result (dict): A dict of classified results. |
| 156 | 153 |
| 157 The format for those dicts are as below: | 154 The format for those dicts are as below: |
| 158 { | 155 { |
| 159 # A dict of results that contains both | 156 # A dict of results that contains both |
| 160 # heuristic analysis results and try job results. | 157 # heuristic analysis results and try job results. |
| 161 'reliable_failures': { | 158 'reliable_failures': { |
| (...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 198 # such as non-swarming tests or swarming rerun failed. | 195 # such as non-swarming tests or swarming rerun failed. |
| 199 'unclassified_failures': {...} | 196 'unclassified_failures': {...} |
| 200 } | 197 } |
| 201 """ | 198 """ |
| 202 updated_results = defaultdict(lambda: defaultdict(lambda: defaultdict(list))) | 199 updated_results = defaultdict(lambda: defaultdict(lambda: defaultdict(list))) |
| 203 | 200 |
| 204 if not organized_results: | 201 if not organized_results: |
| 205 return updated_results | 202 return updated_results |
| 206 | 203 |
| 207 try_job_info = handlers_util.GetAllTryJobResults( | 204 try_job_info = handlers_util.GetAllTryJobResults( |
| 208 master_name, builder_name, build_number) | 205 master_name, builder_name, build_number, show_debug_info) |
| 206 |
| 209 if not try_job_info: | 207 if not try_job_info: |
| 210 return updated_results | 208 return updated_results |
| 211 | 209 |
| 212 for step_name, try_jobs in try_job_info.iteritems(): | 210 for step_name, try_jobs in try_job_info.iteritems(): |
| 213 try_jobs = try_jobs['try_jobs'] | 211 try_jobs = try_jobs['try_jobs'] |
| 214 step_heuristic_results = organized_results[step_name] | 212 step_heuristic_results = organized_results[step_name] |
| 215 step_updated_results = updated_results[step_name]['results'] | 213 step_updated_results = updated_results[step_name]['results'] |
| 216 | 214 |
| 217 # Finds out try job result index and heuristic result index for each test. | 215 # Finds out try job result index and heuristic result index for each test. |
| 218 test_result_map = defaultdict(lambda: defaultdict(int)) | 216 test_result_map = defaultdict(lambda: defaultdict(int)) |
| (...skipping 27 matching lines...) Expand all Loading... |
| 246 'try_job': try_job_result, | 244 'try_job': try_job_result, |
| 247 'heuristic_analysis': { | 245 'heuristic_analysis': { |
| 248 'suspected_cls': heuristic_result['suspected_cls'] | 246 'suspected_cls': heuristic_result['suspected_cls'] |
| 249 }, | 247 }, |
| 250 'tests': tests if tests != [NON_SWARMING] else [], | 248 'tests': tests if tests != [NON_SWARMING] else [], |
| 251 'first_failure': heuristic_result['first_failure'], | 249 'first_failure': heuristic_result['first_failure'], |
| 252 'last_pass': heuristic_result['last_pass'], | 250 'last_pass': heuristic_result['last_pass'], |
| 253 'supported': heuristic_result['supported'] | 251 'supported': heuristic_result['supported'] |
| 254 } | 252 } |
| 255 | 253 |
| 256 if ('status' not in try_job_result or | 254 if (('status' not in try_job_result or |
| 257 try_job_result['status'] in NO_TRY_JOB_REASON_MAP.values()): | 255 try_job_result['status'] in NO_TRY_JOB_REASON_MAP.values()) or |
| 256 show_debug_info): |
| 258 # There is no try job info but only heuristic result. | 257 # There is no try job info but only heuristic result. |
| 259 try_job_result['status'] = try_job_result.get( | 258 try_job_result['status'] = try_job_result.get( |
| 260 'status', result_status.UNKNOWN) | 259 'status', result_status.UNKNOWN) |
| 261 step_updated_results['unclassified_failures'].append(final_result) | 260 step_updated_results['unclassified_failures'].append(final_result) |
| 262 elif try_job_result['status'] == result_status.FLAKY: | 261 elif try_job_result['status'] == result_status.FLAKY: |
| 263 step_updated_results['flaky_failures'].append(final_result) | 262 step_updated_results['flaky_failures'].append(final_result) |
| 264 else: | 263 else: |
| 265 step_updated_results['reliable_failures'].append(final_result) | 264 step_updated_results['reliable_failures'].append(final_result) |
| 266 | 265 |
| 267 return updated_results | 266 return updated_results |
| 268 | 267 |
| 269 | 268 |
| 270 class BuildFailure(BaseHandler): | 269 class BuildFailure(BaseHandler): |
| 271 PERMISSION_LEVEL = Permission.ANYONE | 270 PERMISSION_LEVEL = Permission.ANYONE |
| 272 | 271 |
| 273 def _ShowDebugInfo(self): | 272 def _ShowDebugInfo(self): |
| 274 # Show debug info only if the app is run locally during development, if the | 273 # Show debug info only if the app is run locally during development, if the |
| 275 # currently logged-in user is an admin, or if it is explicitly requested | 274 # currently logged-in user is an admin, or if it is explicitly requested |
| 276 # with parameter 'debug=1'. | 275 # with parameter 'debug=1'. |
| 277 return ( | 276 return users.is_current_user_admin() or self.request.get('debug') == '1' |
| 278 users.is_current_user_admin() or self.request.get('debug') == '1') | |
| 279 | 277 |
| 280 def _ShowTriageHelpButton(self): | 278 def _ShowTriageHelpButton(self): |
| 281 return users.is_current_user_admin() | 279 return users.is_current_user_admin() |
| 282 | 280 |
| 283 def _PrepareCommonDataForFailure(self, analysis): | 281 def _PrepareCommonDataForFailure(self, analysis): |
| 284 return { | 282 return { |
| 285 'master_name': analysis.master_name, | 283 'master_name': analysis.master_name, |
| 286 'builder_name': analysis.builder_name, | 284 'builder_name': analysis.builder_name, |
| 287 'build_number': analysis.build_number, | 285 'build_number': analysis.build_number, |
| 288 'pipeline_status_path': analysis.pipeline_status_path, | 286 'pipeline_status_path': analysis.pipeline_status_path, |
| (...skipping 18 matching lines...) Expand all Loading... |
| 307 return try_job_data # pragma: no cover. | 305 return try_job_data # pragma: no cover. |
| 308 | 306 |
| 309 referred_build_keys = analysis.failure_result_map[ | 307 referred_build_keys = analysis.failure_result_map[ |
| 310 constants.COMPILE_STEP_NAME].split('/') | 308 constants.COMPILE_STEP_NAME].split('/') |
| 311 try_job = WfTryJob.Get(*referred_build_keys) | 309 try_job = WfTryJob.Get(*referred_build_keys) |
| 312 if not try_job or not try_job.compile_results: | 310 if not try_job or not try_job.compile_results: |
| 313 return try_job_data # pragma: no cover. | 311 return try_job_data # pragma: no cover. |
| 314 result = try_job.compile_results[-1] | 312 result = try_job.compile_results[-1] |
| 315 | 313 |
| 316 try_job_data['status'] = analysis_status.STATUS_TO_DESCRIPTION.get( | 314 try_job_data['status'] = analysis_status.STATUS_TO_DESCRIPTION.get( |
| 317 try_job.status, 'unknown').lower() | 315 try_job.status, 'unknown').lower() |
| 318 try_job_data['url'] = result.get('url') | 316 try_job_data['url'] = result.get('url') |
| 319 try_job_data['completed'] = try_job.completed | 317 try_job_data['completed'] = try_job.completed |
| 320 try_job_data['failed'] = try_job.failed | 318 try_job_data['failed'] = try_job.failed |
| 321 try_job_data['culprit'] = result.get( | 319 try_job_data['culprit'] = result.get( |
| 322 'culprit', {}).get(constants.COMPILE_STEP_NAME) | 320 'culprit', {}).get(constants.COMPILE_STEP_NAME) |
| 323 | 321 |
| 324 return try_job_data | 322 return try_job_data |
| 325 | 323 |
| 326 @staticmethod | 324 @staticmethod |
| 327 def _PopulateHeuristicDataForCompileFailure(analysis, data): | 325 def _PopulateHeuristicDataForCompileFailure(analysis, data): |
| (...skipping 10 matching lines...) Expand all Loading... |
| 338 def _PrepareDataForCompileFailure(self, analysis): | 336 def _PrepareDataForCompileFailure(self, analysis): |
| 339 data = self._PrepareCommonDataForFailure(analysis) | 337 data = self._PrepareCommonDataForFailure(analysis) |
| 340 | 338 |
| 341 # Check result from heuristic analysis. | 339 # Check result from heuristic analysis. |
| 342 self._PopulateHeuristicDataForCompileFailure(analysis, data) | 340 self._PopulateHeuristicDataForCompileFailure(analysis, data) |
| 343 # Check result from try job. | 341 # Check result from try job. |
| 344 data['try_job'] = self._PrepareTryJobDataForCompileFailure(analysis) | 342 data['try_job'] = self._PrepareTryJobDataForCompileFailure(analysis) |
| 345 | 343 |
| 346 return data | 344 return data |
| 347 | 345 |
| 348 def _PrepareDataForTestFailures(self, analysis, build_info): | 346 def _PrepareDataForTestFailures(self, analysis, build_info, |
| 347 show_debug_info=False): |
| 349 data = self._PrepareCommonDataForFailure(analysis) | 348 data = self._PrepareCommonDataForFailure(analysis) |
| 350 data['status_message_map'] = result_status.STATUS_MESSAGE_MAP | 349 data['status_message_map'] = result_status.STATUS_MESSAGE_MAP |
| 351 | 350 |
| 352 organized_results = _GetOrganizedAnalysisResultBySuspectedCL( | 351 organized_results = _GetOrganizedAnalysisResultBySuspectedCL( |
| 353 analysis.result) | 352 analysis.result) |
| 354 analysis_result = _GetAnalysisResultWithTryJobInfo( | 353 analysis_result = _GetAnalysisResultWithTryJobInfo( |
| 355 organized_results, *build_info) | 354 show_debug_info, organized_results, *build_info) |
| 355 |
| 356 data['analysis_result'] = analysis_result | 356 data['analysis_result'] = analysis_result |
| 357 | 357 |
| 358 return data | 358 return data |
| 359 | 359 |
| 360 def HandleGet(self): | 360 def HandleGet(self): |
| 361 """Triggers analysis of a build failure on demand and return current result. | 361 """Triggers analysis of a build failure on demand and return current result. |
| 362 | 362 |
| 363 If the final analysis result is available, set cache-control to 1 day to | 363 If the final analysis result is available, set cache-control to 1 day to |
| 364 avoid overload by unnecessary and frequent query from clients; otherwise | 364 avoid overload by unnecessary and frequent query from clients; otherwise |
| 365 set cache-control to 5 seconds to allow repeated query. | 365 set cache-control to 5 seconds to allow repeated query. |
| (...skipping 15 matching lines...) Expand all Loading... |
| 381 if not analysis: | 381 if not analysis: |
| 382 return BaseHandler.CreateError( | 382 return BaseHandler.CreateError( |
| 383 'Master "%s" is not supported yet.' % master_name, 501) | 383 'Master "%s" is not supported yet.' % master_name, 501) |
| 384 | 384 |
| 385 if not analysis: | 385 if not analysis: |
| 386 # Only allow admin to force a re-run and set the build_completed. | 386 # Only allow admin to force a re-run and set the build_completed. |
| 387 force = (users.is_current_user_admin() and | 387 force = (users.is_current_user_admin() and |
| 388 self.request.get('force') == '1') | 388 self.request.get('force') == '1') |
| 389 build_completed = (users.is_current_user_admin() and | 389 build_completed = (users.is_current_user_admin() and |
| 390 self.request.get('build_completed') == '1') | 390 self.request.get('build_completed') == '1') |
| 391 force_try_job = (users.is_current_user_admin() and |
| 392 self.request.get('force_try_job') == '1') |
| 391 analysis = build_failure_analysis_pipelines.ScheduleAnalysisIfNeeded( | 393 analysis = build_failure_analysis_pipelines.ScheduleAnalysisIfNeeded( |
| 392 master_name, builder_name, build_number, | 394 master_name, builder_name, build_number, |
| 393 build_completed=build_completed, force=force, | 395 build_completed=build_completed, force=force, |
| 396 force_try_job=force_try_job, |
| 394 queue_name=constants.WATERFALL_ANALYSIS_QUEUE) | 397 queue_name=constants.WATERFALL_ANALYSIS_QUEUE) |
| 395 | 398 |
| 396 if analysis.failure_type == failure_type.COMPILE: | 399 if analysis.failure_type == failure_type.COMPILE: |
| 397 return { | 400 return { |
| 398 'template': 'waterfall/compile_failure.html', | 401 'template': 'waterfall/compile_failure.html', |
| 399 'data': self._PrepareDataForCompileFailure(analysis), | 402 'data': self._PrepareDataForCompileFailure(analysis), |
| 400 } | 403 } |
| 401 else: | 404 else: |
| 402 return { | 405 return { |
| 403 'template': 'build_failure.html', | 406 'template': 'build_failure.html', |
| 404 'data': self._PrepareDataForTestFailures(analysis, build_info), | 407 'data': self._PrepareDataForTestFailures(analysis, build_info, |
| 408 self._ShowDebugInfo()), |
| 405 } | 409 } |
| 406 | 410 |
| 407 def HandlePost(self): # pragma: no cover | 411 def HandlePost(self): # pragma: no cover |
| 408 return self.HandleGet() | 412 return self.HandleGet() |
| OLD | NEW |