| 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 from common import chrome_dependency_fetcher | 5 from common import chrome_dependency_fetcher |
| 6 from common import git_repository | 6 from common import git_repository |
| 7 from common.dependency import DependencyRoll | 7 from common.dependency import DependencyRoll |
| 8 from common.http_client_appengine import HttpClientAppengine | 8 from common.http_client_appengine import HttpClientAppengine |
| 9 from crash import chromecrash_parser |
| 9 from crash import detect_regression_range | 10 from crash import detect_regression_range |
| 10 from crash import findit_for_chromecrash | 11 from crash import findit_for_chromecrash |
| 11 from crash import chromecrash_parser | 12 from crash.changelist_classifier import ChangelistClassifier |
| 12 from crash import findit_for_crash | 13 from crash.chromecrash_parser import ChromeCrashParser |
| 13 from crash.component_classifier import ComponentClassifier | 14 from crash.component_classifier import ComponentClassifier |
| 15 from crash.crash_report import CrashReport |
| 16 from crash.culprit import Culprit |
| 17 from crash.culprit import NullCulprit |
| 18 from crash.findit_for_chromecrash import FinditForChromeCrash |
| 19 from crash.findit_for_chromecrash import FinditForFracas |
| 20 from crash.findit import Findit |
| 14 from crash.project_classifier import ProjectClassifier | 21 from crash.project_classifier import ProjectClassifier |
| 15 from crash.results import MatchResult | 22 from crash.results import MatchResult |
| 16 from crash.stacktrace import CallStack | 23 from crash.stacktrace import CallStack |
| 17 from crash.stacktrace import Stacktrace | 24 from crash.stacktrace import Stacktrace |
| 25 from crash.test.crash_pipeline_test import DummyCrashData |
| 18 from crash.test.crash_testcase import CrashTestCase | 26 from crash.test.crash_testcase import CrashTestCase |
| 27 from crash.type_enums import CrashClient |
| 28 from model import analysis_status |
| 29 from model.crash.crash_analysis import CrashAnalysis |
| 30 from model.crash.fracas_crash_analysis import FracasCrashAnalysis |
| 31 |
| 32 # In production we'd use CrashWrapperPipeline. And that'd work fine here, |
| 33 # since we never actually call the method that uses it. But just to be |
| 34 # absolutely sure we don't go over the wire due to some mocking failure, |
| 35 # we'll use this dummy class instead. (In fact, since it's never used, |
| 36 # we don't even need to give a real class; |None| works just fine.) |
| 37 MOCK_PIPELINE_CLS = None |
| 38 |
| 39 MOCK_REPOSITORY = None |
| 40 |
| 41 class _FinditForChromeCrash(FinditForChromeCrash): |
| 42 # We allow overriding the default MOCK_REPOSITORY because one unittest |
| 43 # needs to. |
| 44 def __init__(self, repository=MOCK_REPOSITORY): |
| 45 super(_FinditForChromeCrash, self).__init__(repository, MOCK_PIPELINE_CLS) |
| 46 |
| 47 @classmethod |
| 48 def _ClientID(cls): # pragma: no cover |
| 49 """Avoid throwing a NotImplementedError. |
| 50 |
| 51 Since this method is called from |FinditForChromeCrash.__init__| |
| 52 in order to construct the Azalea object, we need to not throw |
| 53 exceptions since we want to be able to test the FinditForChromeCrash |
| 54 class itself. |
| 55 """ |
| 56 return '' |
| 57 |
| 58 @property |
| 59 def config(self): |
| 60 """Avoid returning None. |
| 61 |
| 62 The default |Findit.config| will return None if the client |
| 63 id is not found in the CrashConfig. This in turn will cause |
| 64 |FinditForChromeCrash.__init__| to crash, since NoneType doesn't |
| 65 have a |get| method. In general it's fine for things to crash, since |
| 66 noone should make instances of Findit subclasses which don't define |
| 67 |_clientID|; but for this test suite, we want to permit instances |
| 68 of FinditForChromeCrash, so that we can test that class directly. |
| 69 """ |
| 70 return {} |
| 71 |
| 72 def _FinditForFracas(): |
| 73 """A helper to pass in the standard pipeline class.""" |
| 74 return FinditForFracas(MOCK_REPOSITORY, MOCK_PIPELINE_CLS) |
| 19 | 75 |
| 20 | 76 |
| 21 class FinditForChromeCrashTest(CrashTestCase): | 77 class FinditForChromeCrashTest(CrashTestCase): |
| 22 | 78 |
| 23 chrome_dep_fetcher = chrome_dependency_fetcher.ChromeDependencyFetcher( | 79 chrome_dep_fetcher = chrome_dependency_fetcher.ChromeDependencyFetcher( |
| 24 git_repository.GitRepository(http_client=HttpClientAppengine())) | 80 git_repository.GitRepository(http_client=HttpClientAppengine())) |
| 25 | 81 |
| 82 # TODO(wrengr): what was the purpose of this test? As written it's |
| 83 # just testing that mocking works. I'm guessing it was to check that |
| 84 # we fail when the analysis is for the wrong client_id; but if so, |
| 85 # then we shouldn't need to mock FindCulprit... |
| 86 def testFindCulprit(self): |
| 87 self.mock(FinditForChromeCrash, 'FindCulprit', |
| 88 lambda self, *_: NullCulprit()) |
| 89 |
| 90 # TODO(wrengr): would be less fragile to call |
| 91 # FinditForFracas.CreateAnalysis instead; though if I'm right about |
| 92 # the original purpose of this test, then this is one of the few |
| 93 # places where calling FracasCrashAnalysis directly would actually |
| 94 # make sense. |
| 95 analysis = FracasCrashAnalysis.Create({'signature': 'sig'}) |
| 96 # TODO(wrengr): shouldn't FracasCrashAnalysis.Create already have set |
| 97 # the client_id? |
| 98 analysis.client_id = CrashClient.FRACAS |
| 99 |
| 100 findit_client = _FinditForChromeCrash( |
| 101 git_repository.GitRepository(http_client=HttpClientAppengine())) |
| 102 result, tags = findit_client.FindCulprit(analysis).ToDicts() |
| 103 # TODO(wrengr): just test for the NullCulprit directly; instead of |
| 104 # going through |ToDicts|. |
| 105 expected_result, expected_tags = NullCulprit().ToDicts() |
| 106 self.assertDictEqual(result, expected_result) |
| 107 self.assertDictEqual(tags, expected_tags) |
| 108 |
| 109 |
| 110 class FinditForFracasTest(CrashTestCase): |
| 111 |
| 112 def testPlatformRename(self): |
| 113 self.assertEqual(_FinditForFracas().RenamePlatform('linux'), 'unix') |
| 114 |
| 115 def testCheckPolicyUnsupportedPlatform(self): |
| 116 self.assertIsNone(_FinditForFracas().CheckPolicy(DummyCrashData( |
| 117 platform = 'unsupported_platform'))) |
| 118 |
| 119 def testCheckPolicyBlacklistedSignature(self): |
| 120 self.assertIsNone(_FinditForFracas().CheckPolicy(DummyCrashData( |
| 121 signature = 'Blacklist marker signature'))) |
| 122 |
| 123 def testCheckPolicyPlatformRename(self): |
| 124 new_crash_data = _FinditForFracas().CheckPolicy(DummyCrashData( |
| 125 platform = 'linux')) |
| 126 self.assertIsNotNone(new_crash_data, |
| 127 'FinditForFracas.CheckPolicy unexpectedly returned None') |
| 128 self.assertEqual(new_crash_data['platform'], 'unix') |
| 129 |
| 130 def testCreateAnalysis(self): |
| 131 self.assertIsNotNone(_FinditForFracas().CreateAnalysis( |
| 132 {'signature': 'sig'})) |
| 133 |
| 134 def testGetAnalysis(self): |
| 135 crash_identifiers = {'signature': 'sig'} |
| 136 # TODO(wrengr): would be less fragile to call |
| 137 # FinditForFracas.CreateAnalysis instead. |
| 138 analysis = FracasCrashAnalysis.Create(crash_identifiers) |
| 139 analysis.put() |
| 140 self.assertEqual(_FinditForFracas().GetAnalysis(crash_identifiers), |
| 141 analysis) |
| 142 |
| 143 def testInitializeAnalysisForFracas(self): |
| 144 crash_data = DummyCrashData(platform = 'linux') |
| 145 crash_identifiers = crash_data['crash_identifiers'] |
| 146 |
| 147 findit_client = _FinditForFracas() |
| 148 analysis = findit_client.CreateAnalysis(crash_identifiers) |
| 149 findit_client._InitializeAnalysis(analysis, crash_data) |
| 150 analysis.put() |
| 151 analysis = findit_client.GetAnalysis(crash_identifiers) |
| 152 self.assertIsNotNone(analysis, |
| 153 'FinditForFracas.GetAnalysis unexpectedly returned None') |
| 154 |
| 155 self.assertEqual(analysis.crashed_version, crash_data['crashed_version']) |
| 156 self.assertEqual(analysis.signature, crash_data['signature']) |
| 157 self.assertEqual(analysis.platform, crash_data['platform']) |
| 158 self.assertEqual(analysis.stack_trace, crash_data['stack_trace']) |
| 159 channel = crash_data['customized_data'].get('channel', None) |
| 160 self.assertIsNotNone(channel, |
| 161 'channel is unexpectedly not defined in crash_data') |
| 162 self.assertEqual(analysis.channel, channel) |
| 163 |
| 164 def testNeedsNewAnalysisIsTrueIfNoAnalysisYet(self): |
| 165 self.assertTrue(_FinditForFracas()._NeedsNewAnalysis(DummyCrashData())) |
| 166 |
| 167 def testNeedsNewAnalysisIsTrueIfLastOneFailed(self): |
| 168 findit_client = _FinditForFracas() |
| 169 crash_data = DummyCrashData() |
| 170 analysis = findit_client.CreateAnalysis(crash_data['crash_identifiers']) |
| 171 analysis.status = analysis_status.ERROR |
| 172 analysis.put() |
| 173 self.assertTrue(findit_client._NeedsNewAnalysis(crash_data)) |
| 174 |
| 175 def testNeedsNewAnalysisIsFalseIfLastOneIsNotFailed(self): |
| 176 findit_client = _FinditForFracas() |
| 177 crash_data = DummyCrashData() |
| 178 crash_identifiers = crash_data['crash_identifiers'] |
| 179 for status in (analysis_status.PENDING, analysis_status.RUNNING, |
| 180 analysis_status.COMPLETED, analysis_status.SKIPPED): |
| 181 analysis = findit_client.CreateAnalysis(crash_identifiers) |
| 182 analysis.status = status |
| 183 analysis.put() |
| 184 self.assertFalse(findit_client._NeedsNewAnalysis(crash_data)) |
| 185 |
| 186 def testScheduleNewAnalysisSkipsUnsupportedChannel(self): |
| 187 self.assertFalse(_FinditForFracas().ScheduleNewAnalysis(DummyCrashData( |
| 188 version = None, |
| 189 signature = None, |
| 190 crash_identifiers = {}, |
| 191 channel = 'unsupported_channel'))) |
| 192 |
| 193 def testScheduleNewAnalysisSkipsUnsupportedPlatform(self): |
| 194 self.assertFalse(_FinditForFracas().ScheduleNewAnalysis(DummyCrashData( |
| 195 version = None, |
| 196 signature = None, |
| 197 platform = 'unsupported_platform', |
| 198 crash_identifiers = {}))) |
| 199 |
| 200 def testScheduleNewAnalysisSkipsBlackListSignature(self): |
| 201 self.assertFalse(_FinditForFracas().ScheduleNewAnalysis(DummyCrashData( |
| 202 version = None, |
| 203 signature = 'Blacklist marker signature', |
| 204 crash_identifiers = {}))) |
| 205 |
| 206 def testScheduleNewAnalysisSkipsIfAlreadyCompleted(self): |
| 207 findit_client = _FinditForFracas() |
| 208 crash_data = DummyCrashData() |
| 209 crash_identifiers = crash_data['crash_identifiers'] |
| 210 analysis = findit_client.CreateAnalysis(crash_identifiers) |
| 211 analysis.status = analysis_status.COMPLETED |
| 212 analysis.put() |
| 213 self.assertFalse(findit_client.ScheduleNewAnalysis(crash_data)) |
| 214 |
| 26 def testFindCulpritForChromeCrashEmptyStacktrace(self): | 215 def testFindCulpritForChromeCrashEmptyStacktrace(self): |
| 27 def _MockGetDependency(*_): | 216 self.mock(chrome_dependency_fetcher.ChromeDependencyFetcher, |
| 28 return {} | 217 'GetDependency', lambda *_: {}) |
| 29 | 218 self.mock(ChromeCrashParser, 'Parse', lambda *_: Stacktrace()) |
| 30 def _MockParse(*_): | 219 |
| 31 return Stacktrace() | 220 # TODO(wrengr): use NullCulprit instead |
| 32 | |
| 33 findit_client = findit_for_chromecrash.FinditForChromeCrash( | |
| 34 git_repository.GitRepository(http_client=HttpClientAppengine())) | |
| 35 self.mock(findit_client.dep_fetcher, 'GetDependency', | |
| 36 _MockGetDependency) | |
| 37 self.mock(chromecrash_parser.ChromeCrashParser, 'Parse', _MockParse) | |
| 38 | |
| 39 expected_results = {'found': False} | 221 expected_results = {'found': False} |
| 40 expected_tag = {'found_suspects': False, | 222 expected_tag = {'found_suspects': False, |
| 41 'has_regression_range': False} | 223 'has_regression_range': False} |
| 42 | 224 |
| 43 results, tag = findit_client.FindCulprit( | 225 analysis = CrashAnalysis() |
| 44 'signature', 'win', 'frame1\nframe2', '50.0.1234.0', | 226 analysis.signature = 'signature' |
| 45 [{'chrome_version': '50.0.1234.0', 'cpm': 0.6}]).ToDicts() | 227 analysis.platform = 'win' |
| 46 | 228 analysis.stack_trace = 'frame1\nframe2' |
| 47 self.assertEqual(expected_results, results) | 229 analysis.crashed_version = '50.0.1234.0' |
| 48 self.assertEqual(expected_tag, tag) | 230 analysis.historical_metadata = [ |
| 49 | 231 {'chrome_version': '51.0.1234.0', 'cpm': 0.6}] |
| 50 def testFindCulpritForChromeCrash(self): | 232 results, tag = _FinditForChromeCrash().FindCulprit(analysis).ToDicts() |
| 51 def _MockGetDependency(*_): | 233 |
| 52 return {} | 234 self.assertDictEqual(expected_results, results) |
| 53 | 235 self.assertDictEqual(expected_tag, tag) |
| 54 def _MockParse(*_): | 236 |
| 55 stack = Stacktrace() | 237 # TODO(http://crbug.com/659346): why do these mocks give coverage |
| 56 stack.append(CallStack(0)) | 238 # failures? That's almost surely hiding a bug in the tests themselves. |
| 57 return stack | 239 def testFindCulpritForChromeCrash(self): # pragma: no cover |
| 58 | 240 self.mock(chrome_dependency_fetcher.ChromeDependencyFetcher, |
| 59 def _MockGetDependencyRollsDict(*_): | 241 'GetDependency', lambda *_: {}) |
| 60 return {'src/': DependencyRoll('src/', 'https://repo', '1', '2'), | 242 self.mock(ChromeCrashParser, 'Parse', lambda *_: Stacktrace([CallStack(0)])) |
| 61 'src/add': DependencyRoll('src/add', 'https://repo1', None, '2'), | 243 self.mock(chrome_dependency_fetcher.ChromeDependencyFetcher, |
| 62 'src/delete': DependencyRoll('src/delete', 'https://repo2', | 244 'GetDependencyRollsDict', |
| 63 '2', None)} | 245 lambda *_: { |
| 246 'src/': DependencyRoll('src/', 'https://repo', '1', '2'), |
| 247 'src/add': DependencyRoll('src/add', 'https://repo1', None, '2'), |
| 248 'src/delete': DependencyRoll('src/delete', 'https://repo2', '2', |
| 249 None) |
| 250 }) |
| 64 | 251 |
| 65 dummy_match_result = MatchResult(self.GetDummyChangeLog(), 'src/') | 252 dummy_match_result = MatchResult(self.GetDummyChangeLog(), 'src/') |
| 66 def _MockFindItForCrash(*args): | 253 self.mock(ChangelistClassifier, '__call__', |
| 67 regression_deps_rolls = args[1] | 254 lambda _self, report: |
| 68 if regression_deps_rolls: | 255 [dummy_match_result] if report.regression_range else []) |
| 69 return [dummy_match_result] | 256 |
| 70 | 257 self.mock(ComponentClassifier, 'Classify', lambda *_: []) |
| 71 return [] | 258 self.mock(ProjectClassifier, 'Classify', lambda *_: '') |
| 72 | 259 |
| 73 def _MockComponentClassify(*_): | 260 # TODO(wrengr): for both these tests, we should compare Culprit |
| 74 return [] | 261 # objects directly rather than calling ToDicts and comparing the |
| 75 | 262 # dictionaries. |
| 76 def _MockProjectClassify(*_): | 263 self._testFindCulpritForChromeCrashSucceeds(dummy_match_result) |
| 77 return '' | 264 self._testFindCulpritForChromeCrashFails() |
| 78 | 265 |
| 79 findit_client = findit_for_chromecrash.FinditForChromeCrash( | 266 def _testFindCulpritForChromeCrashSucceeds(self, dummy_match_result): |
| 80 git_repository.GitRepository(http_client=HttpClientAppengine())) | 267 analysis = CrashAnalysis() |
| 81 | 268 analysis.signature = 'signature' |
| 82 self.mock(findit_client.dep_fetcher, 'GetDependency', | 269 analysis.platform = 'win' |
| 83 _MockGetDependency) | 270 analysis.stack_trace = 'frame1\nframe2' |
| 84 self.mock(chromecrash_parser.ChromeCrashParser, 'Parse', _MockParse) | 271 analysis.crashed_version = '50.0.1234.0' |
| 85 self.mock(findit_client.dep_fetcher, 'GetDependencyRollsDict', | 272 dummy_regression_range = ['50.0.1233.0', '50.0.1234.0'] |
| 86 _MockGetDependencyRollsDict) | 273 analysis.regression_range = dummy_regression_range |
| 87 self.mock(findit_for_crash, 'FindItForCrash', _MockFindItForCrash) | 274 results, tag = _FinditForChromeCrash().FindCulprit(analysis).ToDicts() |
| 88 | 275 |
| 89 self.mock(ComponentClassifier, 'Classify', _MockComponentClassify) | |
| 90 self.mock(ProjectClassifier, 'Classify', _MockProjectClassify) | |
| 91 | |
| 92 expected_results = {'found': False} | |
| 93 expected_tag = {'found_suspects': False} | |
| 94 | |
| 95 results, tag = findit_client.FindCulprit( | |
| 96 'signature', 'win', 'frame1\nframe2', '50.0.1234.0', | |
| 97 ['50.0.1233.0', '50.0.1234.0']).ToDicts() | |
| 98 | |
| 99 # TODO(wrengr): compare the Culprit object directly to these values, | |
| 100 # rather than converting to dicts first. We can make a different | |
| 101 # unit test for comparing the dicts, if we actually need/want to. | |
| 102 expected_results = { | 276 expected_results = { |
| 103 'found': True, | 277 'found': True, |
| 104 'suspected_project': '', | 278 'suspected_project': '', |
| 105 'suspected_components': [], | 279 'suspected_components': [], |
| 106 'suspected_cls': [dummy_match_result.ToDict()], | 280 'suspected_cls': [dummy_match_result.ToDict()], |
| 107 'regression_range': ['50.0.1233.0', '50.0.1234.0'] | 281 'regression_range': dummy_regression_range |
| 108 } | 282 } |
| 109 expected_tag = { | 283 expected_tag = { |
| 110 'found_suspects': True, | 284 'found_suspects': True, |
| 111 'found_project': False, | 285 'found_project': False, |
| 112 'found_components': False, | 286 'found_components': False, |
| 113 'has_regression_range': True, | 287 'has_regression_range': True, |
| 114 'solution': 'core_algorithm', | 288 'solution': 'core_algorithm', |
| 115 } | 289 } |
| 116 | 290 |
| 117 self.assertEqual(expected_results, results) | 291 self.assertDictEqual(expected_results, results) |
| 118 self.assertEqual(expected_tag, tag) | 292 self.assertDictEqual(expected_tag, tag) |
| 119 | 293 |
| 120 results, tag = findit_client.FindCulprit( | 294 def _testFindCulpritForChromeCrashFails(self): |
| 121 'signature', 'win', 'frame1\nframe2', '50.0.1234.0', None).ToDicts() | 295 analysis = CrashAnalysis() |
| 296 analysis.signature = 'signature' |
| 297 analysis.platform = 'win' |
| 298 analysis.stack_trace = 'frame1\nframe2' |
| 299 analysis.crashed_version = '50.0.1234.0' |
| 300 results, tag = _FinditForChromeCrash().FindCulprit(analysis).ToDicts() |
| 122 | 301 |
| 123 expected_results = { | 302 expected_results = { |
| 124 'found': False, | 303 'found': False, |
| 125 'suspected_project': '', | 304 'suspected_project': '', |
| 126 'suspected_components': [], | 305 'suspected_components': [], |
| 127 'suspected_cls': [], | 306 'suspected_cls': [], |
| 128 'regression_range': None | 307 'regression_range': None |
| 129 } | 308 } |
| 130 expected_tag = { | 309 expected_tag = { |
| 131 'found_suspects': False, | 310 'found_suspects': False, |
| 132 'found_project': False, | 311 'found_project': False, |
| 133 'found_components': False, | 312 'found_components': False, |
| 134 'has_regression_range': False, | 313 'has_regression_range': False, |
| 135 'solution': 'core_algorithm', | 314 'solution': 'core_algorithm', |
| 136 } | 315 } |
| 137 | 316 |
| 138 self.assertEqual(expected_results, results) | 317 self.assertDictEqual(expected_results, results) |
| 139 self.assertEqual(expected_tag, tag) | 318 self.assertDictEqual(expected_tag, tag) |
| 140 | |
| 141 def testFieldsProperty(self): | |
| 142 culprit = findit_for_chromecrash.Culprit('proj', ['Blink>API'], [], | |
| 143 ['50.0.1234.0', '50.0.1234.1']) | |
| 144 self.assertEqual(culprit.fields, | |
| 145 ('project', 'components', 'cls', 'regression_range')) | |
| OLD | NEW |