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