| OLD | NEW |
| (Empty) | |
| 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 |
| 3 # found in the LICENSE file. |
| 4 |
| 5 import copy |
| 6 import math |
| 7 import pprint |
| 8 |
| 9 from common.dependency import DependencyRoll |
| 10 from common import chrome_dependency_fetcher |
| 11 from crash import changelist_classifier |
| 12 from crash.crash_report import CrashReport |
| 13 import crash.loglinear.changelist_classifier as loglinear_changelist_classifier |
| 14 from crash.loglinear.feature import ChangedFile |
| 15 from crash.loglinear.feature import FeatureValue |
| 16 from crash.suspect import AnalysisInfo |
| 17 from crash.suspect import Suspect |
| 18 from crash.suspect import StackInfo |
| 19 from crash.stacktrace import CallStack |
| 20 from crash.stacktrace import StackFrame |
| 21 from crash.stacktrace import Stacktrace |
| 22 from crash.test.crash_test_suite import CrashTestSuite |
| 23 from libs.gitiles.change_log import ChangeLog |
| 24 from libs.gitiles.gitiles_repository import GitilesRepository |
| 25 |
| 26 DUMMY_CHANGELOG1 = ChangeLog.FromDict({ |
| 27 'author_name': 'r@chromium.org', |
| 28 'message': 'dummy', |
| 29 'committer_email': 'r@chromium.org', |
| 30 'commit_position': 175900, |
| 31 'author_email': 'r@chromium.org', |
| 32 'touched_files': [ |
| 33 { |
| 34 'change_type': 'add', |
| 35 'new_path': 'a.cc', |
| 36 'old_path': None, |
| 37 }, |
| 38 ], |
| 39 'author_time': 'Thu Mar 31 21:24:43 2016', |
| 40 'committer_time': 'Thu Mar 31 21:28:39 2016', |
| 41 'commit_url': |
| 42 'https://repo.test/+/1', |
| 43 'code_review_url': 'https://codereview.chromium.org/3281', |
| 44 'committer_name': 'example@chromium.org', |
| 45 'revision': '1', |
| 46 'reverted_revision': None |
| 47 }) |
| 48 |
| 49 DUMMY_CHANGELOG2 = ChangeLog.FromDict({ |
| 50 'author_name': 'example@chromium.org', |
| 51 'message': 'dummy', |
| 52 'committer_email': 'example@chromium.org', |
| 53 'commit_position': 175976, |
| 54 'author_email': 'example@chromium.org', |
| 55 'touched_files': [ |
| 56 { |
| 57 'change_type': 'add', |
| 58 'new_path': 'f0.cc', |
| 59 'old_path': 'b/f0.cc' |
| 60 }, |
| 61 ], |
| 62 'author_time': 'Thu Mar 31 21:24:43 2016', |
| 63 'committer_time': 'Thu Mar 31 21:28:39 2016', |
| 64 'commit_url': |
| 65 'https://repo.test/+/2', |
| 66 'code_review_url': 'https://codereview.chromium.org/3281', |
| 67 'committer_name': 'example@chromium.org', |
| 68 'revision': '2', |
| 69 'reverted_revision': '1' |
| 70 }) |
| 71 |
| 72 DUMMY_CHANGELOG3 = ChangeLog.FromDict({ |
| 73 'author_name': 'e@chromium.org', |
| 74 'message': 'dummy', |
| 75 'committer_email': 'e@chromium.org', |
| 76 'commit_position': 176000, |
| 77 'author_email': 'e@chromium.org', |
| 78 'touched_files': [ |
| 79 { |
| 80 'change_type': 'modify', |
| 81 'new_path': 'f.cc', |
| 82 'old_path': 'f.cc' |
| 83 }, |
| 84 { |
| 85 'change_type': 'delete', |
| 86 'new_path': None, |
| 87 'old_path': 'f1.cc' |
| 88 }, |
| 89 ], |
| 90 'author_time': 'Thu Apr 1 21:24:43 2016', |
| 91 'committer_time': 'Thu Apr 1 21:28:39 2016', |
| 92 'commit_url': |
| 93 'https://repo.test/+/3', |
| 94 'code_review_url': 'https://codereview.chromium.org/3281', |
| 95 'committer_name': 'example@chromium.org', |
| 96 'revision': '3', |
| 97 'reverted_revision': None |
| 98 }) |
| 99 |
| 100 # TODO(crbug.com/674255): clean up the warning about the empty trace. |
| 101 DUMMY_REPORT = CrashReport(None, None, None, Stacktrace(), (None, None)) |
| 102 |
| 103 |
| 104 class LogLinearChangelistClassifierTest(CrashTestSuite): |
| 105 |
| 106 def setUp(self): |
| 107 super(LogLinearChangelistClassifierTest, self).setUp() |
| 108 weights = { |
| 109 'MinDistance': 1., |
| 110 'TopFrameIndex': 1., |
| 111 } |
| 112 |
| 113 self.changelist_classifier = ( |
| 114 loglinear_changelist_classifier.LogLinearChangelistClassifier( |
| 115 GitilesRepository(self.GetMockHttpClient()), weights)) |
| 116 |
| 117 def testAggregateChangedFilesAggreegates(self): |
| 118 """Test that ``AggregateChangedFiles`` does aggregate reasons per file. |
| 119 |
| 120 In the main/inner loop of ``AggregateChangedFiles``: if multiple |
| 121 features all blame the same file change, we try to aggregate those |
| 122 reasons so that we only report the file once (with all reasons). None |
| 123 of the other tests here actually check the case where the same file |
| 124 is blamed multiple times, so we check that here. |
| 125 |
| 126 In particular, we provide the same ``FeatureValue`` twice, and |
| 127 hence the same ``ChangedFile`` twice; so we should get back a single |
| 128 ``ChangedFile`` but with the ``reasons`` fields concatenated. |
| 129 """ |
| 130 file_reason = 'I blame you!' |
| 131 file_blame = ChangedFile( |
| 132 name = 'a.cc', |
| 133 blame_url = None, |
| 134 reasons = [file_reason] |
| 135 ) |
| 136 |
| 137 feature_value = FeatureValue( |
| 138 name = 'dummy feature', |
| 139 value = 42, |
| 140 reason = 'dummy reason', |
| 141 changed_files = [file_blame] |
| 142 ) |
| 143 |
| 144 expected_file_blame = file_blame._replace(reasons = [file_reason] * 2) |
| 145 |
| 146 self.assertListEqual( |
| 147 [expected_file_blame], |
| 148 self.changelist_classifier.AggregateChangedFiles( |
| 149 [feature_value] * 2)) |
| 150 |
| 151 def testSkipAddedAndDeletedRegressionRolls(self): |
| 152 self.mock(chrome_dependency_fetcher.ChromeDependencyFetcher, |
| 153 'GetDependency', lambda *_: {}) |
| 154 dep_rolls = { |
| 155 'src/dep': DependencyRoll('src/dep1', 'https://url_dep1', None, '9'), |
| 156 'src/': DependencyRoll('src/', ('https://chromium.googlesource.com/' |
| 157 'chromium/src.git'), '4', '5') |
| 158 } |
| 159 self.mock(chrome_dependency_fetcher.ChromeDependencyFetcher, |
| 160 'GetDependencyRollsDict', lambda *_: dep_rolls) |
| 161 |
| 162 passed_in_regression_deps_rolls = [] |
| 163 def _MockGetChangeLogsForFilesGroupedByDeps(regression_deps_rolls, *_): |
| 164 passed_in_regression_deps_rolls.append(regression_deps_rolls) |
| 165 return {}, None |
| 166 |
| 167 self.mock(changelist_classifier, 'GetChangeLogsForFilesGroupedByDeps', |
| 168 _MockGetChangeLogsForFilesGroupedByDeps) |
| 169 self.mock(changelist_classifier, 'GetStackInfosForFilesGroupedByDeps', |
| 170 lambda *_: {}) |
| 171 self.mock(changelist_classifier, 'FindSuspects', lambda *_: None) |
| 172 |
| 173 self.changelist_classifier(CrashReport(crashed_version = '5', |
| 174 signature = 'sig', |
| 175 platform = 'canary', |
| 176 stacktrace = Stacktrace([CallStack(0)]), |
| 177 regression_range = ['4', '5'])) |
| 178 expected_regression_deps_rolls = copy.deepcopy(dep_rolls) |
| 179 |
| 180 # Regression of a dep added/deleted (old_revision/new_revision is None) can |
| 181 # not be known for sure and this case rarely happens, so just filter them |
| 182 # out. |
| 183 del expected_regression_deps_rolls['src/dep'] |
| 184 self.assertEqual(passed_in_regression_deps_rolls[0], |
| 185 expected_regression_deps_rolls) |
| 186 |
| 187 # TODO(http://crbug.com/659346): why do these mocks give coverage |
| 188 # failures? That's almost surely hiding a bug in the tests themselves. |
| 189 def testFindItForCrashNoRegressionRange(self): # pragma: no cover |
| 190 self.mock(chrome_dependency_fetcher.ChromeDependencyFetcher, |
| 191 'GetDependencyRollsDict', lambda *_: {}) |
| 192 self.mock(chrome_dependency_fetcher.ChromeDependencyFetcher, |
| 193 'GetDependency', lambda *_: {}) |
| 194 # N.B., for this one test we really do want regression_range=None. |
| 195 report = DUMMY_REPORT._replace(regression_range=None) |
| 196 self.assertListEqual(self.changelist_classifier(report), []) |
| 197 |
| 198 def testFindItForCrashNoMatchFound(self): |
| 199 self.mock(changelist_classifier, 'FindSuspects', lambda *_: []) |
| 200 self.mock(chrome_dependency_fetcher.ChromeDependencyFetcher, |
| 201 'GetDependencyRollsDict', |
| 202 lambda *_: {'src/': DependencyRoll('src/', 'https://repo', '1', '2')}) |
| 203 self.mock(chrome_dependency_fetcher.ChromeDependencyFetcher, |
| 204 'GetDependency', lambda *_: {}) |
| 205 self.assertListEqual(self.changelist_classifier(DUMMY_REPORT), []) |
| 206 |
| 207 def testFindItForCrash(self): |
| 208 |
| 209 def _MockFindSuspects(*_): |
| 210 suspect1 = Suspect(DUMMY_CHANGELOG1, 'src/') |
| 211 frame1 = StackFrame(0, 'src/', 'func', 'a.cc', 'src/a.cc', [1]) |
| 212 frame2 = StackFrame(1, 'src/', 'func', 'a.cc', 'src/a.cc', [7]) |
| 213 suspect1.file_to_stack_infos = { |
| 214 'a.cc': [StackInfo(frame1, 0), StackInfo(frame2, 0)] |
| 215 } |
| 216 suspect1.file_to_analysis_info = { |
| 217 'a.cc': AnalysisInfo(min_distance=0, min_distance_frame=frame1) |
| 218 } |
| 219 |
| 220 suspect2 = Suspect(DUMMY_CHANGELOG3, 'src/') |
| 221 frame3 = StackFrame(5, 'src/', 'func', 'f.cc', 'src/f.cc', [1]) |
| 222 suspect2.file_to_stack_infos = { |
| 223 'f.cc': [StackInfo(frame3, 0)] |
| 224 } |
| 225 suspect2.file_to_analysis_info = { |
| 226 'a.cc': AnalysisInfo(min_distance=20, min_distance_frame=frame3) |
| 227 } |
| 228 |
| 229 return [suspect1, suspect2] |
| 230 |
| 231 self.mock(changelist_classifier, 'FindSuspects', _MockFindSuspects) |
| 232 self.mock(chrome_dependency_fetcher.ChromeDependencyFetcher, |
| 233 'GetDependencyRollsDict', |
| 234 lambda *_: {'src/': DependencyRoll('src/', 'https://repo', '1', '2')}) |
| 235 self.mock(chrome_dependency_fetcher.ChromeDependencyFetcher, |
| 236 'GetDependency', lambda *_: {}) |
| 237 |
| 238 suspects = self.changelist_classifier(DUMMY_REPORT) |
| 239 self.assertTrue(suspects, |
| 240 "Expected suspects, but the classifier didn't return any") |
| 241 |
| 242 expected_suspects = [ |
| 243 { |
| 244 'review_url': 'https://codereview.chromium.org/3281', |
| 245 'url': 'https://repo.test/+/3', |
| 246 'author': 'e@chromium.org', |
| 247 'time': 'Thu Apr 1 21:24:43 2016', |
| 248 'project_path': 'src/', |
| 249 'revision': '3', |
| 250 'confidence': math.log(0.2857142857142857 * 0.6), |
| 251 'reasons': [ |
| 252 ('MinDistance', math.log(0.6), 'Minimum distance is 20'), |
| 253 ('TopFrameIndex', math.log(0.2857142857142857), |
| 254 'Top frame is #5')], |
| 255 'changed_files': [ |
| 256 { |
| 257 'file': 'a.cc', |
| 258 'blame_url': None, |
| 259 'info': 'Minimum distance (LOC) 20, frame #5', |
| 260 }], |
| 261 }, { |
| 262 'review_url': 'https://codereview.chromium.org/3281', |
| 263 'url': 'https://repo.test/+/1', |
| 264 'author': 'r@chromium.org', |
| 265 'time': 'Thu Mar 31 21:24:43 2016', |
| 266 'project_path': 'src/', |
| 267 'revision': '1', |
| 268 'confidence': 0., |
| 269 'reasons': [ |
| 270 ('MinDistance', 0., 'Minimum distance is 0'), |
| 271 ('TopFrameIndex', 0., 'Top frame is #0')], |
| 272 'changed_files': [ |
| 273 { |
| 274 'file': 'a.cc', |
| 275 'blame_url': None, |
| 276 'info': 'Minimum distance (LOC) 0, frame #0', |
| 277 }], |
| 278 }, |
| 279 ] |
| 280 self.assertListEqual([suspect.ToDict() for suspect in suspects], |
| 281 expected_suspects) |
| 282 |
| 283 def testFinditForCrashFilterZeroConfidenceSuspects(self): |
| 284 def _MockFindSuspects(*_): |
| 285 suspect1 = Suspect(DUMMY_CHANGELOG1, 'src/') |
| 286 frame1 = StackFrame(0, 'src/', 'func', 'a.cc', 'src/a.cc', [1]) |
| 287 frame2 = StackFrame(1, 'src/', 'func', 'a.cc', 'src/a.cc', [7]) |
| 288 suspect1.file_to_stack_infos = { |
| 289 'a.cc': [StackInfo(frame1, 0), StackInfo(frame2, 0)] |
| 290 } |
| 291 suspect1.file_to_analysis_info = { |
| 292 'a.cc': AnalysisInfo(min_distance=1, min_distance_frame=frame1) |
| 293 } |
| 294 |
| 295 suspect2 = Suspect(DUMMY_CHANGELOG3, 'src/') |
| 296 frame3 = StackFrame(15, 'src/', 'func', 'f.cc', 'src/f.cc', [1]) |
| 297 suspect2.file_to_stack_infos = { |
| 298 'f.cc': [StackInfo(frame3, 0)] |
| 299 } |
| 300 suspect2.file_to_analysis_info = { |
| 301 'f.cc': AnalysisInfo(min_distance=20, min_distance_frame=frame3) |
| 302 } |
| 303 |
| 304 suspect3 = Suspect(DUMMY_CHANGELOG3, 'src/') |
| 305 frame4 = StackFrame(3, 'src/', 'func', 'ff.cc', 'src/ff.cc', [1]) |
| 306 suspect3.file_to_stack_infos = { |
| 307 'f.cc': [StackInfo(frame4, 0)] |
| 308 } |
| 309 suspect3.file_to_analysis_info = { |
| 310 'f.cc': AnalysisInfo(min_distance=60, min_distance_frame=frame4) |
| 311 } |
| 312 |
| 313 return [suspect1, suspect2, suspect3] |
| 314 |
| 315 self.mock(changelist_classifier, 'FindSuspects', _MockFindSuspects) |
| 316 self.mock(chrome_dependency_fetcher.ChromeDependencyFetcher, |
| 317 'GetDependencyRollsDict', |
| 318 lambda *_: {'src/': DependencyRoll('src/', 'https://repo', '1', '2')}) |
| 319 self.mock(chrome_dependency_fetcher.ChromeDependencyFetcher, |
| 320 'GetDependency', lambda *_: {}) |
| 321 |
| 322 suspects = self.changelist_classifier(DUMMY_REPORT) |
| 323 self.assertTrue(suspects, |
| 324 "Expected suspects, but the classifier didn't return any") |
| 325 |
| 326 expected_suspects = [ |
| 327 { |
| 328 'author': 'r@chromium.org', |
| 329 'changed_files': [ |
| 330 { |
| 331 'blame_url': None, |
| 332 'file': 'a.cc', |
| 333 'info': 'Minimum distance (LOC) 1, frame #0' |
| 334 } |
| 335 ], |
| 336 'confidence': math.log(0.98), |
| 337 'project_path': 'src/', |
| 338 'reasons': [ |
| 339 ('MinDistance', math.log(0.98), 'Minimum distance is 1'), |
| 340 ('TopFrameIndex', 0., 'Top frame is #0'), |
| 341 ], |
| 342 'review_url': 'https://codereview.chromium.org/3281', |
| 343 'revision': '1', |
| 344 'time': 'Thu Mar 31 21:24:43 2016', |
| 345 'url': 'https://repo.test/+/1' |
| 346 }, |
| 347 ] |
| 348 self.assertListEqual([suspect.ToDict() for suspect in suspects], |
| 349 expected_suspects) |
| 350 |
| 351 def testFinditForCrashAllSuspectsWithZeroConfidences(self): |
| 352 """Test that we filter out suspects with too-large frame indices. |
| 353 |
| 354 In the mock suspects below we return frames with indices |
| 355 15, 20, 21 which are all larger than the ``max_top_n`` of |
| 356 ``TopFrameIndexFeature``. Therefore we should get a score of zero |
| 357 for that feature, which should cause the suspects to be filtered out. |
| 358 """ |
| 359 def _MockFindSuspects(*_): |
| 360 suspect1 = Suspect(DUMMY_CHANGELOG1, 'src/') |
| 361 frame1 = StackFrame(20, 'src/', '', 'func', 'a.cc', [1]) |
| 362 frame2 = StackFrame(21, 'src/', '', 'func', 'a.cc', [7]) |
| 363 suspect1.file_to_stack_infos = { |
| 364 'a.cc': [StackInfo(frame1, 0), StackInfo(frame2, 0)] |
| 365 } |
| 366 suspect1.file_to_analysis_info = { |
| 367 'a.cc': AnalysisInfo(min_distance=1, min_distance_frame=frame1) |
| 368 } |
| 369 |
| 370 suspect2 = Suspect(DUMMY_CHANGELOG3, 'src/') |
| 371 frame3 = StackFrame(15, 'src/', '', 'func', 'f.cc', [1]) |
| 372 suspect2.file_to_stack_infos = { |
| 373 'f.cc': [StackInfo(frame3, 0)] |
| 374 } |
| 375 suspect2.min_distance = 20 |
| 376 suspect2.file_to_analysis_info = { |
| 377 'f.cc': AnalysisInfo(min_distance=20, min_distance_frame=frame3) |
| 378 } |
| 379 |
| 380 return [suspect1, suspect2] |
| 381 |
| 382 self.mock(changelist_classifier, 'FindSuspects', _MockFindSuspects) |
| 383 self.mock(chrome_dependency_fetcher.ChromeDependencyFetcher, |
| 384 'GetDependencyRollsDict', |
| 385 lambda *_: {'src/': DependencyRoll('src/', 'https://repo', '1', '2')}) |
| 386 self.mock(chrome_dependency_fetcher.ChromeDependencyFetcher, |
| 387 'GetDependency', lambda *_: {}) |
| 388 |
| 389 suspects = self.changelist_classifier(DUMMY_REPORT) |
| 390 self.assertFalse(suspects, 'Expected zero suspects, but found some:\n%s' |
| 391 % pprint.pformat([suspect.ToDict() for suspect in suspects])) |
| OLD | NEW |