| OLD | NEW |
| 1 # Copyright 2016 The Chromium Authors. All rights reserved. | 1 # Copyright 2016 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 mock | 5 import mock |
| 6 | 6 |
| 7 from common import chrome_dependency_fetcher | 7 from common import chrome_dependency_fetcher |
| 8 from common.dependency import DependencyRoll | 8 from common.dependency import DependencyRoll |
| 9 from crash import chromecrash_parser | 9 from crash import chromecrash_parser |
| 10 from crash import detect_regression_range | 10 from crash import detect_regression_range |
| 11 from crash import findit | 11 from crash import findit |
| 12 from crash import findit_for_chromecrash | 12 from crash import findit_for_chromecrash |
| 13 from crash.loglinear.changelist_classifier import LogLinearChangelistClassifier | 13 from crash.loglinear.changelist_classifier import LogLinearChangelistClassifier |
| 14 from crash.chromecrash_parser import ChromeCrashParser | 14 from crash.chromecrash_parser import ChromeCrashParser |
| 15 from crash.component_classifier import ComponentClassifier | 15 from crash.component_classifier import ComponentClassifier |
| 16 from crash.crash_report import CrashReport | 16 from crash.crash_report import CrashReport |
| 17 from crash.culprit import Culprit | 17 from crash.culprit import Culprit |
| 18 from crash.findit_for_chromecrash import FinditForChromeCrash | 18 from crash.findit_for_chromecrash import FinditForChromeCrash |
| 19 from crash.findit_for_chromecrash import FinditForFracas | 19 from crash.findit_for_chromecrash import FinditForFracas |
| 20 from crash.project_classifier import ProjectClassifier | 20 from crash.project_classifier import ProjectClassifier |
| 21 from crash.suspect import Suspect | 21 from crash.suspect import Suspect |
| 22 from crash.stacktrace import CallStack | 22 from crash.stacktrace import CallStack |
| 23 from crash.stacktrace import Stacktrace | 23 from crash.stacktrace import Stacktrace |
| 24 from crash.test.predator_testcase import PredatorTestCase | 24 from crash.test.predator_testcase import PredatorTestCase |
| 25 from crash.type_enums import CrashClient | 25 from crash.type_enums import CrashClient |
| 26 from gae_libs.http.http_client_appengine import HttpClientAppengine | 26 from gae_libs.http.http_client_appengine import HttpClientAppengine |
| 27 from libs.gitiles.gitiles_repository import GitilesRepository | 27 from libs.gitiles.gitiles_repository import GitilesRepository |
| 28 from model import analysis_status | 28 from model import analysis_status |
| 29 from model.crash.crash_analysis import CrashAnalysis | 29 from model.crash.crash_analysis import CrashAnalysis |
| 30 from model.crash.crash_config import CrashConfig |
| 30 from model.crash.fracas_crash_analysis import FracasCrashAnalysis | 31 from model.crash.fracas_crash_analysis import FracasCrashAnalysis |
| 31 | 32 |
| 32 MOCK_GET_REPOSITORY = lambda _: None # pragma: no cover | 33 MOCK_GET_REPOSITORY = lambda _: None # pragma: no cover |
| 33 | 34 |
| 35 |
| 34 class _FinditForChromeCrash(FinditForChromeCrash): # pylint: disable = W | 36 class _FinditForChromeCrash(FinditForChromeCrash): # pylint: disable = W |
| 35 # We allow overriding the default ``get_repository`` because one unittest | 37 # We allow overriding the default ``get_repository`` because one unittest |
| 36 # needs to. | 38 # needs to. |
| 37 def __init__(self, get_repository=MOCK_GET_REPOSITORY): | 39 def __init__(self, get_repository=MOCK_GET_REPOSITORY, config=None): |
| 38 super(_FinditForChromeCrash, self).__init__(get_repository) | 40 super(_FinditForChromeCrash, self).__init__(get_repository, config or {}) |
| 39 | 41 |
| 40 @classmethod | 42 @classmethod |
| 41 def _ClientID(cls): # pragma: no cover | 43 def _ClientID(cls): # pragma: no cover |
| 42 """Avoid throwing a NotImplementedError. | 44 """Avoid throwing a NotImplementedError. |
| 43 | 45 |
| 44 Since this method is called from ``FinditForChromeCrash.__init__`` | 46 Since this method is called from ``FinditForChromeCrash.__init__`` |
| 45 in order to construct the Azalea object, we need to not throw | 47 in order to construct the Azalea object, we need to not throw |
| 46 exceptions since we want to be able to test the FinditForChromeCrash | 48 exceptions since we want to be able to test the FinditForChromeCrash |
| 47 class itself. | 49 class itself. |
| 48 """ | 50 """ |
| 49 return '' | 51 return 'ChromeCrash' |
| 50 | |
| 51 @property | |
| 52 def config(self): | |
| 53 """Avoid returning None. | |
| 54 | |
| 55 The default ``Findit.config`` will return None if the client | |
| 56 id is not found in the CrashConfig. This in turn will cause | |
| 57 ``FinditForChromeCrash.__init__`` to crash, since NoneType doesn't | |
| 58 have a ``get`` method. In general it's fine for things to crash, since | |
| 59 noone should make instances of Findit subclasses which don't define | |
| 60 ``_clientID``; but for this test suite, we want to permit instances | |
| 61 of FinditForChromeCrash, so that we can test that class directly. | |
| 62 """ | |
| 63 return {} | |
| 64 | 52 |
| 65 | 53 |
| 66 def _FinditForFracas(): | 54 def _FinditForFracas(config=None): |
| 67 """A helper to pass in the standard pipeline class.""" | 55 """A helper to pass in the standard pipeline class.""" |
| 68 return FinditForFracas(MOCK_GET_REPOSITORY) | 56 return FinditForFracas(MOCK_GET_REPOSITORY, config or {}) |
| 69 | 57 |
| 70 | 58 |
| 71 class FinditForChromeCrashTest(PredatorTestCase): | 59 class FinditForChromeCrashTest(PredatorTestCase): |
| 72 | 60 |
| 73 # TODO(wrengr): what was the purpose of this test? As written it's | 61 # TODO(wrengr): what was the purpose of this test? As written it's |
| 74 # just testing that mocking works. I'm guessing it was to check that | 62 # just testing that mocking works. I'm guessing it was to check that |
| 75 # we fail when the analysis is for the wrong client_id; but if so, | 63 # we fail when the analysis is for the wrong client_id; but if so, |
| 76 # then we shouldn't need to mock FindCulprit... | 64 # then we shouldn't need to mock FindCulprit... |
| 77 def testFindCulprit(self): | 65 def testFindCulprit(self): |
| 78 self.mock(FinditForChromeCrash, 'FindCulprit', lambda self, *_: None) | 66 self.mock(FinditForChromeCrash, 'FindCulprit', lambda self, *_: None) |
| 79 | 67 |
| 80 # TODO(wrengr): would be less fragile to call | 68 # TODO(wrengr): would be less fragile to call |
| 81 # FinditForFracas.CreateAnalysis instead; though if I'm right about | 69 # FinditForFracas.CreateAnalysis instead; though if I'm right about |
| 82 # the original purpose of this test, then this is one of the few | 70 # the original purpose of this test, then this is one of the few |
| 83 # places where calling FracasCrashAnalysis directly would actually | 71 # places where calling FracasCrashAnalysis directly would actually |
| 84 # make sense. | 72 # make sense. |
| 85 analysis = FracasCrashAnalysis.Create({'signature': 'sig'}) | 73 analysis = FracasCrashAnalysis.Create({'signature': 'sig'}) |
| 86 # TODO(wrengr): shouldn't FracasCrashAnalysis.Create already have set | 74 findit_client = _FinditForChromeCrash( |
| 87 # the client_id? | 75 GitilesRepository.Factory(HttpClientAppengine()), CrashConfig.Get()) |
| 88 analysis.client_id = CrashClient.FRACAS | |
| 89 | |
| 90 findit_client = ( | |
| 91 _FinditForChromeCrash(GitilesRepository.Factory(HttpClientAppengine()))) | |
| 92 self.assertIsNone(findit_client.FindCulprit(analysis)) | 76 self.assertIsNone(findit_client.FindCulprit(analysis)) |
| 93 | 77 |
| 94 | 78 |
| 95 class FinditForFracasTest(PredatorTestCase): | 79 class FinditForFracasTest(PredatorTestCase): |
| 96 | 80 |
| 97 def testPlatformRename(self): | 81 def setUp(self): |
| 98 self.assertEqual(_FinditForFracas().RenamePlatform('linux'), 'unix') | 82 super(FinditForFracasTest, self).setUp() |
| 83 self._client = _FinditForFracas(config=CrashConfig.Get()) |
| 84 |
| 85 def testCheckPolicyBlacklistSignature(self): |
| 86 crash_data = self.GetDummyCrashData( |
| 87 client_id = CrashClient.FRACAS, |
| 88 signature='Blacklist marker signature') |
| 89 crash_buffer = self._client.GetCrashBuffer(crash_data) |
| 90 self.assertFalse(self._client._CheckPolicy(crash_buffer)) |
| 99 | 91 |
| 100 def testCheckPolicyUnsupportedPlatform(self): | 92 def testCheckPolicyUnsupportedPlatform(self): |
| 101 self.assertIsNone(_FinditForFracas().CheckPolicy(self.GetDummyCrashData( | 93 crash_data = self.GetDummyCrashData( |
| 102 platform = 'unsupported_platform'))) | 94 client_id = CrashClient.FRACAS, |
| 95 platform='unsupported_platform') |
| 96 crash_buffer = self._client.GetCrashBuffer(crash_data) |
| 97 self.assertFalse(self._client._CheckPolicy(crash_buffer)) |
| 103 | 98 |
| 104 def testCheckPolicyBlacklistedSignature(self): | 99 def testScheduleNewAnalysisSkipsUnsupportedChannel(self): |
| 105 self.assertIsNone(_FinditForFracas().CheckPolicy(self.GetDummyCrashData( | 100 crash_data = self.GetDummyCrashData( |
| 106 signature = 'Blacklist marker signature'))) | 101 client_id = CrashClient.FRACAS, |
| 102 channel='unsupported_channel') |
| 103 crash_buffer = self._client.GetCrashBuffer(crash_data) |
| 104 self.assertFalse(self._client._CheckPolicy(crash_buffer)) |
| 107 | 105 |
| 108 def testCheckPolicyPlatformRename(self): | 106 def testCheckPolicySuccess(self): |
| 109 new_crash_data = _FinditForFracas().CheckPolicy(self.GetDummyCrashData( | 107 crash_buffer = self._client.GetCrashBuffer(self.GetDummyCrashData( |
| 110 platform = 'linux')) | 108 client_id = CrashClient.FRACAS)) |
| 111 self.assertIsNotNone(new_crash_data, | 109 self.assertTrue(self._client._CheckPolicy(crash_buffer)) |
| 112 'FinditForFracas.CheckPolicy unexpectedly returned None') | |
| 113 self.assertEqual(new_crash_data['platform'], 'unix') | |
| 114 | 110 |
| 115 def testCreateAnalysis(self): | 111 def testCreateAnalysis(self): |
| 116 self.assertIsNotNone(_FinditForFracas().CreateAnalysis( | 112 self.assertIsNotNone(self._client.CreateAnalysis( |
| 117 {'signature': 'sig'})) | 113 {'signature': 'sig'})) |
| 118 | 114 |
| 119 def testGetAnalysis(self): | 115 def testGetAnalysis(self): |
| 120 crash_identifiers = {'signature': 'sig'} | 116 crash_identifiers = {'signature': 'sig'} |
| 121 # TODO(wrengr): would be less fragile to call | 117 # TODO(wrengr): would be less fragile to call |
| 122 # FinditForFracas.CreateAnalysis instead. | 118 # FinditForFracas.CreateAnalysis instead. |
| 123 analysis = FracasCrashAnalysis.Create(crash_identifiers) | 119 analysis = FracasCrashAnalysis.Create(crash_identifiers) |
| 124 analysis.put() | 120 analysis.put() |
| 125 self.assertEqual(_FinditForFracas().GetAnalysis(crash_identifiers), | 121 self.assertEqual(self._client.GetAnalysis(crash_identifiers), analysis) |
| 126 analysis) | |
| 127 | |
| 128 def testInitializeAnalysisForFracas(self): | |
| 129 crash_data = self.GetDummyCrashData(platform = 'linux') | |
| 130 crash_identifiers = crash_data['crash_identifiers'] | |
| 131 | |
| 132 findit_client = _FinditForFracas() | |
| 133 analysis = findit_client.CreateAnalysis(crash_identifiers) | |
| 134 findit_client._InitializeAnalysis(analysis, crash_data) | |
| 135 analysis.put() | |
| 136 analysis = findit_client.GetAnalysis(crash_identifiers) | |
| 137 self.assertIsNotNone(analysis, | |
| 138 'FinditForFracas.GetAnalysis unexpectedly returned None') | |
| 139 | |
| 140 self.assertEqual(analysis.crashed_version, crash_data['chrome_version']) | |
| 141 self.assertEqual(analysis.signature, crash_data['signature']) | |
| 142 self.assertEqual(analysis.platform, crash_data['platform']) | |
| 143 self.assertEqual(analysis.stack_trace, crash_data['stack_trace']) | |
| 144 channel = crash_data['customized_data'].get('channel', None) | |
| 145 self.assertIsNotNone(channel, | |
| 146 'channel is unexpectedly not defined in crash_data') | |
| 147 self.assertEqual(analysis.channel, channel) | |
| 148 | |
| 149 def testNeedsNewAnalysisIsTrueIfNoAnalysisYet(self): | |
| 150 self.assertTrue(_FinditForFracas()._NeedsNewAnalysis( | |
| 151 self.GetDummyCrashData())) | |
| 152 | |
| 153 def testNeedsNewAnalysisIsTrueIfLastOneFailed(self): | |
| 154 findit_client = _FinditForFracas() | |
| 155 crash_data = self.GetDummyCrashData() | |
| 156 analysis = findit_client.CreateAnalysis(crash_data['crash_identifiers']) | |
| 157 analysis.status = analysis_status.ERROR | |
| 158 analysis.put() | |
| 159 self.assertTrue(findit_client._NeedsNewAnalysis(crash_data)) | |
| 160 | |
| 161 def testNeedsNewAnalysisIsFalseIfLastOneIsNotFailed(self): | |
| 162 findit_client = _FinditForFracas() | |
| 163 crash_data = self.GetDummyCrashData() | |
| 164 crash_identifiers = crash_data['crash_identifiers'] | |
| 165 for status in (analysis_status.PENDING, analysis_status.RUNNING, | |
| 166 analysis_status.COMPLETED, analysis_status.SKIPPED): | |
| 167 analysis = findit_client.CreateAnalysis(crash_identifiers) | |
| 168 analysis.status = status | |
| 169 analysis.put() | |
| 170 self.assertFalse(findit_client._NeedsNewAnalysis(crash_data)) | |
| 171 | |
| 172 def testFindCulpritForChromeCrashEmptyStacktrace(self): | |
| 173 self.mock(chrome_dependency_fetcher.ChromeDependencyFetcher, | |
| 174 'GetDependency', lambda *_: {}) | |
| 175 self.mock(ChromeCrashParser, 'Parse', lambda *_: None) | |
| 176 | |
| 177 analysis = CrashAnalysis() | |
| 178 analysis.signature = 'signature' | |
| 179 analysis.platform = 'win' | |
| 180 analysis.stack_trace = 'frame1\nframe2' | |
| 181 analysis.crashed_version = '50.0.1234.0' | |
| 182 analysis.historical_metadata = [ | |
| 183 {'chrome_version': '51.0.1234.0', 'cpm': 0.6}] | |
| 184 self.assertIsNone(_FinditForChromeCrash().FindCulprit(analysis)) | |
| 185 | |
| 186 # TODO(http://crbug.com/659346): why do these mocks give coverage | |
| 187 # failures? That's almost surely hiding a bug in the tests themselves. | |
| 188 def testFindCulpritForChromeCrash(self): # pragma: no cover | |
| 189 self.mock(chrome_dependency_fetcher.ChromeDependencyFetcher, | |
| 190 'GetDependency', lambda *_: {}) | |
| 191 stack = CallStack(0) | |
| 192 self.mock(ChromeCrashParser, 'Parse', | |
| 193 lambda *_: Stacktrace([stack], stack)) | |
| 194 self.mock(chrome_dependency_fetcher.ChromeDependencyFetcher, | |
| 195 'GetDependencyRollsDict', | |
| 196 lambda *_: { | |
| 197 'src/': DependencyRoll('src/', 'https://repo', '1', '2'), | |
| 198 'src/add': DependencyRoll('src/add', 'https://repo1', None, '2'), | |
| 199 'src/delete': DependencyRoll('src/delete', 'https://repo2', '2', | |
| 200 None) | |
| 201 }) | |
| 202 | |
| 203 dummy_suspect = Suspect(self.GetDummyChangeLog(), 'src/') | |
| 204 self.mock(LogLinearChangelistClassifier, '__call__', | |
| 205 lambda _self, report: | |
| 206 [dummy_suspect] if report.regression_range else []) | |
| 207 | |
| 208 self.mock(ComponentClassifier, 'ClassifySuspects', lambda *_: []) | |
| 209 self.mock(ComponentClassifier, 'ClassifyCallStack', lambda *_: []) | |
| 210 self.mock(ProjectClassifier, 'ClassifySuspects', lambda *_: '') | |
| 211 self.mock(ProjectClassifier, 'ClassifyCallStack', lambda *_: '') | |
| 212 | |
| 213 # TODO(wrengr): for both these tests, we should compare Culprit | |
| 214 # objects directly rather than calling ToDicts and comparing the | |
| 215 # dictionaries. | |
| 216 self._testFindCulpritForChromeCrashSucceeds(dummy_suspect) | |
| 217 self._testFindCulpritForChromeCrashFails() | |
| 218 | |
| 219 def _testFindCulpritForChromeCrashSucceeds(self, dummy_suspect): | |
| 220 analysis = CrashAnalysis() | |
| 221 analysis.signature = 'signature' | |
| 222 analysis.platform = 'win' | |
| 223 analysis.stack_trace = 'frame1\nframe2' | |
| 224 analysis.crashed_version = '50.0.1234.0' | |
| 225 dummy_regression_range = ('50.0.1233.0', '50.0.1234.0') | |
| 226 analysis.regression_range = dummy_regression_range | |
| 227 culprit = _FinditForChromeCrash().FindCulprit(analysis) | |
| 228 self.assertIsNotNone(culprit, 'FindCulprit failed unexpectedly') | |
| 229 suspects, tag = culprit.ToDicts() | |
| 230 | |
| 231 expected_suspects = { | |
| 232 'found': True, | |
| 233 'suspected_cls': [dummy_suspect.ToDict()], | |
| 234 'regression_range': dummy_regression_range | |
| 235 } | |
| 236 expected_tag = { | |
| 237 'found_suspects': True, | |
| 238 'found_project': False, | |
| 239 'found_components': False, | |
| 240 'has_regression_range': True, | |
| 241 'solution': 'core_algorithm', | |
| 242 } | |
| 243 | |
| 244 self.assertDictEqual(expected_suspects, suspects) | |
| 245 self.assertDictEqual(expected_tag, tag) | |
| 246 | |
| 247 def _testFindCulpritForChromeCrashFails(self): | |
| 248 analysis = CrashAnalysis() | |
| 249 analysis.signature = 'signature' | |
| 250 analysis.platform = 'win' | |
| 251 analysis.stack_trace = 'frame1\nframe2' | |
| 252 analysis.crashed_version = '50.0.1234.0' | |
| 253 # N.B., analysis.regression_range is None | |
| 254 suspects, tag = _FinditForChromeCrash().FindCulprit(analysis).ToDicts() | |
| 255 | |
| 256 expected_suspects = {'found': False} | |
| 257 expected_tag = { | |
| 258 'found_suspects': False, | |
| 259 'found_project': False, | |
| 260 'found_components': False, | |
| 261 'has_regression_range': False, | |
| 262 'solution': 'core_algorithm', | |
| 263 } | |
| 264 | |
| 265 self.assertDictEqual(expected_suspects, suspects) | |
| 266 self.assertDictEqual(expected_tag, tag) | |
| 267 | 122 |
| 268 @mock.patch('google.appengine.ext.ndb.Key.urlsafe') | 123 @mock.patch('google.appengine.ext.ndb.Key.urlsafe') |
| 269 @mock.patch('common.appengine_util.GetDefaultVersionHostname') | 124 @mock.patch('common.appengine_util.GetDefaultVersionHostname') |
| 270 def testProcessResultForPublishing(self, mocked_get_default_host, | 125 def testProcessResultForPublishing(self, mocked_get_default_host, |
| 271 mocked_urlsafe): | 126 mocked_urlsafe): |
| 272 mocked_host = 'http://host' | 127 mocked_host = 'http://host' |
| 273 mocked_get_default_host.return_value = mocked_host | 128 mocked_get_default_host.return_value = mocked_host |
| 274 urlsafe_key = 'abcde' | 129 urlsafe_key = 'abcde' |
| 275 mocked_urlsafe.return_value = urlsafe_key | 130 mocked_urlsafe.return_value = urlsafe_key |
| 276 | 131 |
| 277 crash_identifiers = {'signature': 'sig'} | 132 crash_identifiers = {'signature': 'sig'} |
| 278 analysis = FracasCrashAnalysis.Create(crash_identifiers) | 133 analysis = FracasCrashAnalysis.Create(crash_identifiers) |
| 279 analysis.result = {'other': 'data'} | 134 analysis.result = {'other': 'data'} |
| 280 findit_object = FinditForFracas(MOCK_GET_REPOSITORY) | |
| 281 expected_processed_suspect = { | 135 expected_processed_suspect = { |
| 282 'client_id': findit_object.client_id, | 136 'client_id': self._client.client_id, |
| 283 'crash_identifiers': {'signature': 'sig'}, | 137 'crash_identifiers': {'signature': 'sig'}, |
| 284 'result': { | 138 'result': { |
| 285 'feedback_url': ( | 139 'feedback_url': ( |
| 286 findit_for_chromecrash._FRACAS_FEEDBACK_URL_TEMPLATE % ( | 140 findit_for_chromecrash._FRACAS_FEEDBACK_URL_TEMPLATE % ( |
| 287 mocked_host, urlsafe_key)), | 141 mocked_host, urlsafe_key)), |
| 288 'other': 'data' | 142 'other': 'data' |
| 289 } | 143 } |
| 290 } | 144 } |
| 291 | 145 |
| 292 self.assertDictEqual(findit_object.GetPublishableResult(crash_identifiers, | 146 self.assertDictEqual(self._client.GetPublishableResult(crash_identifiers, |
| 293 analysis), | 147 analysis), |
| 294 expected_processed_suspect) | 148 expected_processed_suspect) |
| OLD | NEW |