| OLD | NEW |
| 1 # Copyright 2015 The Chromium Authors. All rights reserved. | 1 # Copyright 2015 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 base64 | 5 import base64 |
| 6 import copy |
| 6 import json | 7 import json |
| 7 import os | 8 import logging |
| 8 import re | |
| 9 | 9 |
| 10 from google.appengine.api import app_identity |
| 11 from google.appengine.ext import ndb |
| 10 import webapp2 | 12 import webapp2 |
| 11 import webtest | 13 from webtest.app import AppError |
| 12 | 14 |
| 15 from common import chrome_dependency_fetcher |
| 16 from lib.gitiles import gitiles_repository |
| 13 from crash import crash_pipeline | 17 from crash import crash_pipeline |
| 14 from crash.findit import Findit | 18 from crash.findit import Findit |
| 19 from crash.findit_for_chromecrash import FinditForFracas |
| 20 from crash.test.crash_pipeline_test import DummyCrashData |
| 15 from crash.test.crash_testcase import CrashTestCase | 21 from crash.test.crash_testcase import CrashTestCase |
| 22 from crash.type_enums import CrashClient |
| 16 from handlers.crash import crash_handler | 23 from handlers.crash import crash_handler |
| 24 from model import analysis_status |
| 25 from model.crash.fracas_crash_analysis import FracasCrashAnalysis |
| 26 |
| 27 |
| 28 class MockCulprit(object): |
| 29 """Construct a fake culprit where ``ToDicts`` returns whatever we please.""" |
| 30 |
| 31 def __init__(self, mock_result, mock_tags): |
| 32 self._result = mock_result |
| 33 self._tags = mock_tags |
| 34 |
| 35 def ToDicts(self): # pragma: no cover |
| 36 return self._result, self._tags |
| 17 | 37 |
| 18 | 38 |
| 19 class CrashHandlerTest(CrashTestCase): | 39 class CrashHandlerTest(CrashTestCase): |
| 20 app_module = webapp2.WSGIApplication([ | 40 app_module = webapp2.WSGIApplication([ |
| 21 ('/_ah/push-handlers/crash/fracas', crash_handler.CrashHandler), | 41 ('/_ah/push-handlers/crash/fracas', crash_handler.CrashHandler), |
| 22 ], debug=True) | 42 ], debug=True) |
| 23 | 43 |
| 44 def testScheduleNewAnalysisWithFailingPolicy(self): |
| 45 class _MockFindit(Findit): # pylint: disable=W0223 |
| 46 def __init__(self): |
| 47 super(_MockFindit, self).__init__(None) |
| 48 |
| 49 def CheckPolicy(self, crash_data): |
| 50 """This is the same as inherited, but just to be explicit.""" |
| 51 return None |
| 52 |
| 53 def _NeedsNewAnalysis(self, _crash_data): |
| 54 raise AssertionError('testScheduleNewAnalysisWithFailingPolicy: ' |
| 55 "called _MockFindit._NeedsNewAnalysis, when it shouldn't.") |
| 56 |
| 57 self.mock(crash_pipeline, 'FinditForClientID', lambda *_: _MockFindit()) |
| 58 self.assertFalse(crash_handler.ScheduleNewAnalysis(DummyCrashData( |
| 59 client_id = 'MOCK_CLIENT'))) |
| 60 |
| 61 def testScheduleNewAnalysisWithPlatformRename(self): |
| 62 original_crash_data = DummyCrashData( |
| 63 client_id = 'MOCK_CLIENT', |
| 64 version = None, |
| 65 platform = 'unix', |
| 66 crash_identifiers = {}) |
| 67 renamed_crash_data = copy.deepcopy(original_crash_data) |
| 68 renamed_crash_data['platform'] = 'linux' |
| 69 |
| 70 testcase = self |
| 71 class _MockFindit(Findit): # pylint: disable=W0223 |
| 72 def __init__(self): |
| 73 super(_MockFindit, self).__init__(None) |
| 74 |
| 75 @property |
| 76 def config(self): |
| 77 """Make PlatformRename work as expected.""" |
| 78 return {'platform_rename': {'unix': 'linux'}} |
| 79 |
| 80 def CheckPolicy(self, crash_data): |
| 81 """Call PlatformRename, and return successfully. |
| 82 |
| 83 N.B., if we did not override this method, then our overridden |
| 84 ``_NeedsNewAnalysis`` would never be called either.""" |
| 85 # TODO(wrengr): should we clone ``crash_data`` rather than mutating it? |
| 86 crash_data['platform'] = self.RenamePlatform(crash_data['platform']) |
| 87 return crash_data |
| 88 |
| 89 def _NeedsNewAnalysis(self, new_crash_data): |
| 90 logging.debug('Called _MockFindit._NeedsNewAnalysis, as desired') |
| 91 testcase.assertDictEqual(new_crash_data, renamed_crash_data) |
| 92 return False |
| 93 |
| 94 self.mock(crash_pipeline, 'FinditForClientID', |
| 95 lambda _client_id: _MockFindit()) |
| 96 self.assertFalse(crash_handler.ScheduleNewAnalysis(original_crash_data)) |
| 97 |
| 98 def testScheduleNewAnalysisSkipsUnsupportedChannel(self): |
| 99 self.assertFalse(crash_handler.ScheduleNewAnalysis(DummyCrashData( |
| 100 client_id = CrashClient.FRACAS, |
| 101 version = None, |
| 102 signature = None, |
| 103 crash_identifiers = {}, |
| 104 channel = 'unsupported_channel'))) |
| 105 |
| 106 def testScheduleNewAnalysisSkipsUnsupportedPlatform(self): |
| 107 self.assertFalse(crash_handler.ScheduleNewAnalysis(DummyCrashData( |
| 108 client_id = CrashClient.FRACAS, |
| 109 version = None, |
| 110 signature = None, |
| 111 platform = 'unsupported_platform', |
| 112 crash_identifiers = {}))) |
| 113 |
| 114 def testScheduleNewAnalysisSkipsBlackListSignature(self): |
| 115 self.assertFalse(crash_handler.ScheduleNewAnalysis(DummyCrashData( |
| 116 client_id = CrashClient.FRACAS, |
| 117 version = None, |
| 118 signature = 'Blacklist marker signature', |
| 119 crash_identifiers = {}))) |
| 120 |
| 121 def testScheduleNewAnalysisSkipsIfAlreadyCompleted(self): |
| 122 findit_client = FinditForFracas(None) |
| 123 crash_data = DummyCrashData(client_id = findit_client.client_id) |
| 124 crash_identifiers = crash_data['crash_identifiers'] |
| 125 analysis = findit_client.CreateAnalysis(crash_identifiers) |
| 126 analysis.status = analysis_status.COMPLETED |
| 127 analysis.put() |
| 128 self.assertFalse(crash_handler.ScheduleNewAnalysis(crash_data)) |
| 129 |
| 24 def testAnalysisScheduled(self): | 130 def testAnalysisScheduled(self): |
| 25 # We need to mock out the method on Findit itself (rather than using a | 131 # We need to mock out the method on Findit itself (rather than using a |
| 26 # subclass), since this method only gets called on objects we | 132 # subclass), since this method only gets called on objects we |
| 27 # ourselves don't construct. | 133 # ourselves don't construct. |
| 28 requested_crashes = [] | 134 requested_crashes = [] |
| 29 def _MockScheduleNewAnalysis(_self, crash_data, **_): | 135 def _MockScheduleNewAnalysis(crash_data): |
| 30 requested_crashes.append(crash_data) | 136 requested_crashes.append(crash_data) |
| 31 self.mock(Findit, 'ScheduleNewAnalysis', _MockScheduleNewAnalysis) | 137 self.mock(crash_handler, 'ScheduleNewAnalysis', _MockScheduleNewAnalysis) |
| 32 | 138 |
| 33 self.mock_current_user(user_email='test@chromium.org', is_admin=True) | 139 self.mock_current_user(user_email='test@chromium.org', is_admin=True) |
| 34 | 140 |
| 35 channel = 'supported_channel' | 141 channel = 'supported_channel' |
| 36 platform = 'supported_platform' | 142 platform = 'supported_platform' |
| 37 signature = 'signature/here' | 143 signature = 'signature/here' |
| 38 chrome_version = '50.2500.0.0' | 144 chrome_version = '50.2500.0.0' |
| 39 crash_data = { | 145 crash_data = { |
| 40 'client_id': 'fracas', | 146 'client_id': 'fracas', |
| 41 'platform': platform, | 147 'platform': platform, |
| (...skipping 20 matching lines...) Expand all Loading... |
| 62 'message_id': 'id', | 168 'message_id': 'id', |
| 63 }, | 169 }, |
| 64 'subscription': 'subscription', | 170 'subscription': 'subscription', |
| 65 } | 171 } |
| 66 | 172 |
| 67 self.test_app.post_json('/_ah/push-handlers/crash/fracas', | 173 self.test_app.post_json('/_ah/push-handlers/crash/fracas', |
| 68 request_json_data) | 174 request_json_data) |
| 69 | 175 |
| 70 self.assertEqual(1, len(requested_crashes)) | 176 self.assertEqual(1, len(requested_crashes)) |
| 71 self.assertEqual(crash_data, requested_crashes[0]) | 177 self.assertEqual(crash_data, requested_crashes[0]) |
| 178 |
| 179 # TODO: this function is a gross hack. We should figure out what the |
| 180 # semantic goal really is here, so we can avoid doing such intricate |
| 181 # and fragile mocking. |
| 182 def _TestRunningAnalysisForResult(self, analysis_result, analysis_tags): |
| 183 |
| 184 # Mock out the part of PublishResultPipeline that would go over the wire. |
| 185 pubsub_publish_requests = [] |
| 186 def Mocked_PublishMessagesToTopic(messages_data, topic): |
| 187 pubsub_publish_requests.append((messages_data, topic)) |
| 188 self.mock(crash_pipeline.pubsub_util, 'PublishMessagesToTopic', |
| 189 Mocked_PublishMessagesToTopic) |
| 190 |
| 191 MOCK_HOST = 'https://host.com' |
| 192 self.mock(app_identity, 'get_default_version_hostname', lambda: MOCK_HOST) |
| 193 |
| 194 testcase = self |
| 195 MOCK_KEY = 'MOCK_KEY' |
| 196 |
| 197 # Mock out the wrapper pipeline, so call the other pipelines directly |
| 198 # instead of doing the yielding loop and spawning off processes. |
| 199 def mock_start_pipeline(self, **kwargs): |
| 200 logging.info('Mock running on queue %s', kwargs['queue_name']) |
| 201 analysis_pipeline = crash_pipeline.CrashAnalysisPipeline( |
| 202 self._client_id, self._crash_identifiers) |
| 203 analysis_pipeline.run() |
| 204 analysis_pipeline.finalized() |
| 205 |
| 206 testcase.mock(ndb.Key, 'urlsafe', lambda _self: MOCK_KEY) |
| 207 publish_pipeline = crash_pipeline.PublishResultPipeline( |
| 208 self._client_id, self._crash_identifiers) |
| 209 publish_pipeline.run() |
| 210 publish_pipeline.finalized() |
| 211 self.mock(crash_pipeline.CrashWrapperPipeline, 'start', mock_start_pipeline) |
| 212 |
| 213 # Mock out FindCulprit to track the number of times it's called and |
| 214 # with which arguments. N.B., the pipeline will reconstruct Findit |
| 215 # objects form their client_id, so we can't mock via subclassing, |
| 216 # we must mock via ``self.mock``. |
| 217 mock_culprit = MockCulprit(analysis_result, analysis_tags) |
| 218 analyzed_crashes = [] |
| 219 def _MockFindCulprit(_self, model): |
| 220 analyzed_crashes.append(model) |
| 221 return mock_culprit |
| 222 self.mock(FinditForFracas, 'FindCulprit', _MockFindCulprit) |
| 223 |
| 224 # The real ``ParseStacktrace`` calls ``GetChromeDependency``, |
| 225 # which eventually calls ``GitRepository.GetSource`` and hence |
| 226 # goes over the wire. Since we mocked out ``FindCulprit`` to no |
| 227 # longer call ``ParseStacktrace``, it shouldn't matter what the real |
| 228 # ``ParseStacktrace`` does. However, since mocking is fragile and it's |
| 229 # hard to triage what actually went wrong if we do end up going over |
| 230 # the wire, we mock this out too just to be safe. |
| 231 def _MockParseStacktrace(_self, _model): |
| 232 raise AssertionError("ParseStacktrace shouldn't ever be called. " |
| 233 'That it was indicates some sort of problem with our mocking code.') |
| 234 self.mock(FinditForFracas, 'ParseStacktrace', _MockParseStacktrace) |
| 235 |
| 236 # More directly address the issue about ``GetChromeDependency`` going |
| 237 # over the wire. |
| 238 def _MockGetChromeDependency(_self, _revision, _platform): |
| 239 raise AssertionError("GetChromeDependency shouldn't ever be called. " |
| 240 'That it was indicates some sort of problem with our mocking code.') |
| 241 self.mock(chrome_dependency_fetcher.ChromeDependencyFetcher, |
| 242 'GetDependency', _MockGetChromeDependency) |
| 243 |
| 244 crash_data = DummyCrashData( |
| 245 client_id = CrashClient.FRACAS, |
| 246 version = '50.2500.0.1', |
| 247 stack_trace = 'frame1\nframe2\nframe3') |
| 248 # A fake repository, needed by the Findit constructor. We should never |
| 249 # go over the wire (e.g., in the call to ScheduleNewAnalysis below), |
| 250 # and this helps ensure that. |
| 251 self.mock(gitiles_repository, 'GitilesRepository', |
| 252 lambda *_args, **_kwargs: None) |
| 253 self.assertTrue(crash_handler.ScheduleNewAnalysis(crash_data)) |
| 254 |
| 255 # The catch/re-raise is to clean up the callstack that's reported |
| 256 # when things acciddentally go over the wire (and subsequently fail). |
| 257 try: |
| 258 self.execute_queued_tasks() |
| 259 except AppError, e: # pragma: no cover |
| 260 raise e |
| 261 |
| 262 self.assertEqual(1, len(pubsub_publish_requests)) |
| 263 |
| 264 processed_analysis_result = copy.deepcopy(analysis_result) |
| 265 processed_analysis_result['feedback_url'] = ( |
| 266 '%s/crash/fracas-result-feedback?key=%s' % (MOCK_HOST, MOCK_KEY)) |
| 267 |
| 268 for cl in processed_analysis_result.get('suspected_cls', []): |
| 269 cl['confidence'] = round(cl['confidence'], 2) |
| 270 cl.pop('reason', None) |
| 271 |
| 272 expected_messages_data = [json.dumps({ |
| 273 'crash_identifiers': crash_data['crash_identifiers'], |
| 274 'client_id': CrashClient.FRACAS, |
| 275 'result': processed_analysis_result, |
| 276 }, sort_keys=True)] |
| 277 self.assertListEqual(expected_messages_data, pubsub_publish_requests[0][0]) |
| 278 self.assertEqual(1, len(analyzed_crashes)) |
| 279 analysis = analyzed_crashes[0] |
| 280 self.assertTrue(isinstance(analysis, FracasCrashAnalysis)) |
| 281 self.assertEqual(crash_data['signature'], analysis.signature) |
| 282 self.assertEqual(crash_data['platform'], analysis.platform) |
| 283 self.assertEqual(crash_data['stack_trace'], analysis.stack_trace) |
| 284 self.assertEqual(crash_data['crashed_version'], analysis.crashed_version) |
| 285 self.assertEqual(crash_data['regression_range'], analysis.regression_range) |
| 286 |
| 287 analysis = FracasCrashAnalysis.Get(crash_data['crash_identifiers']) |
| 288 self.assertEqual(analysis_result, analysis.result) |
| 289 return analysis |
| 290 |
| 291 def testRunningAnalysis(self): |
| 292 analysis_result = { |
| 293 'found': True, |
| 294 'suspected_cls': [], |
| 295 'other_data': 'data', |
| 296 } |
| 297 analysis_tags = { |
| 298 'found_suspects': True, |
| 299 'has_regression_range': True, |
| 300 'solution': 'core', |
| 301 'unsupported_tag': '', |
| 302 } |
| 303 |
| 304 analysis = self._TestRunningAnalysisForResult( |
| 305 analysis_result, analysis_tags) |
| 306 self.assertTrue(analysis.has_regression_range) |
| 307 self.assertTrue(analysis.found_suspects) |
| 308 self.assertEqual('core', analysis.solution) |
| 309 |
| 310 def testRunningAnalysisNoSuspectsFound(self): |
| 311 analysis_result = { |
| 312 'found': False |
| 313 } |
| 314 analysis_tags = { |
| 315 'found_suspects': False, |
| 316 'has_regression_range': False, |
| 317 'solution': 'core', |
| 318 'unsupported_tag': '', |
| 319 } |
| 320 |
| 321 analysis = self._TestRunningAnalysisForResult( |
| 322 analysis_result, analysis_tags) |
| 323 self.assertFalse(analysis.has_regression_range) |
| 324 self.assertFalse(analysis.found_suspects) |
| 325 self.assertEqual('core', analysis.solution) |
| 326 |
| 327 def testRunningAnalysisWithSuspectsCls(self): |
| 328 analysis_result = { |
| 329 'found': True, |
| 330 'suspected_cls': [ |
| 331 {'confidence': 0.21434, |
| 332 'reason': ['reason1', 'reason2'], |
| 333 'other': 'data'} |
| 334 ], |
| 335 'other_data': 'data', |
| 336 } |
| 337 analysis_tags = { |
| 338 'found_suspects': True, |
| 339 'has_regression_range': True, |
| 340 'solution': 'core', |
| 341 'unsupported_tag': '', |
| 342 } |
| 343 |
| 344 analysis = self._TestRunningAnalysisForResult( |
| 345 analysis_result, analysis_tags) |
| 346 self.assertTrue(analysis.has_regression_range) |
| 347 self.assertTrue(analysis.found_suspects) |
| 348 self.assertEqual('core', analysis.solution) |
| OLD | NEW |