OLD | NEW |
(Empty) | |
| 1 #!/usr/bin/env python |
| 2 # Copyright 2016 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. |
| 5 """Tests for git_dates.""" |
| 6 |
| 7 import datetime |
| 8 import os |
| 9 import shutil |
| 10 import StringIO |
| 11 import sys |
| 12 import tempfile |
| 13 import unittest |
| 14 |
| 15 DEPOT_TOOLS_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
| 16 sys.path.insert(0, DEPOT_TOOLS_ROOT) |
| 17 |
| 18 from testing_support import coverage_utils |
| 19 from testing_support import git_test_utils |
| 20 |
| 21 import git_common |
| 22 |
| 23 |
| 24 class GitHyperBlameTestBase(git_test_utils.GitRepoReadOnlyTestBase): |
| 25 @classmethod |
| 26 def setUpClass(cls): |
| 27 super(GitHyperBlameTestBase, cls).setUpClass() |
| 28 import git_hyper_blame |
| 29 cls.git_hyper_blame = git_hyper_blame |
| 30 |
| 31 def run_hyperblame(self, ignored, filename, revision): |
| 32 stdout = StringIO.StringIO() |
| 33 stderr = StringIO.StringIO() |
| 34 ignored = [self.repo[c] for c in ignored] |
| 35 retval = self.repo.run(self.git_hyper_blame.hyper_blame, ignored, filename, |
| 36 revision=revision, out=stdout, err=stderr) |
| 37 return retval, stdout.getvalue().rstrip().split('\n') |
| 38 |
| 39 def blame_line(self, commit_name, rest, filename=None): |
| 40 """Generate a blame line from a commit. |
| 41 |
| 42 Args: |
| 43 commit_name: The commit's schema name. |
| 44 rest: The blame line after the timestamp. e.g., '2) file2 - merged'. |
| 45 """ |
| 46 short = self.repo[commit_name][:8] |
| 47 start = '%s %s' % (short, filename) if filename else short |
| 48 author = self.repo.show_commit(commit_name, format_string='%an %ai') |
| 49 return '%s (%s %s' % (start, author, rest) |
| 50 |
| 51 class GitHyperBlameMainTest(GitHyperBlameTestBase): |
| 52 """End-to-end tests on a very simple repo.""" |
| 53 REPO_SCHEMA = "A B C" |
| 54 |
| 55 COMMIT_A = { |
| 56 'some/files/file': {'data': 'line 1\nline 2\n'}, |
| 57 } |
| 58 |
| 59 COMMIT_B = { |
| 60 'some/files/file': {'data': 'line 1\nline 2.1\n'}, |
| 61 } |
| 62 |
| 63 COMMIT_C = { |
| 64 'some/files/file': {'data': 'line 1.1\nline 2.1\n'}, |
| 65 } |
| 66 |
| 67 def testBasicBlame(self): |
| 68 """Tests the main function (simple end-to-end test with no ignores).""" |
| 69 expected_output = [self.blame_line('C', '1) line 1.1'), |
| 70 self.blame_line('B', '2) line 2.1')] |
| 71 stdout = StringIO.StringIO() |
| 72 stderr = StringIO.StringIO() |
| 73 retval = self.repo.run(self.git_hyper_blame.main, |
| 74 args=['tag_C', 'some/files/file'], stdout=stdout, |
| 75 stderr=stderr) |
| 76 self.assertEqual(0, retval) |
| 77 self.assertEqual(expected_output, stdout.getvalue().rstrip().split('\n')) |
| 78 self.assertEqual('', stderr.getvalue()) |
| 79 |
| 80 def testIgnoreSimple(self): |
| 81 """Tests the main function (simple end-to-end test with ignores).""" |
| 82 expected_output = [self.blame_line('C', ' 1) line 1.1'), |
| 83 self.blame_line('A', '2*) line 2.1')] |
| 84 stdout = StringIO.StringIO() |
| 85 stderr = StringIO.StringIO() |
| 86 retval = self.repo.run(self.git_hyper_blame.main, |
| 87 args=['-i', 'tag_B', 'tag_C', 'some/files/file'], |
| 88 stdout=stdout, stderr=stderr) |
| 89 self.assertEqual(0, retval) |
| 90 self.assertEqual(expected_output, stdout.getvalue().rstrip().split('\n')) |
| 91 self.assertEqual('', stderr.getvalue()) |
| 92 |
| 93 def testBadRepo(self): |
| 94 """Tests the main function (not in a repo).""" |
| 95 # Make a temp dir that has no .git directory. |
| 96 curdir = os.getcwd() |
| 97 tempdir = tempfile.mkdtemp(suffix='_nogit', prefix='git_repo') |
| 98 try: |
| 99 os.chdir(tempdir) |
| 100 stdout = StringIO.StringIO() |
| 101 stderr = StringIO.StringIO() |
| 102 retval = self.git_hyper_blame.main( |
| 103 args=['-i', 'tag_B', 'tag_C', 'some/files/file'], stdout=stdout, |
| 104 stderr=stderr) |
| 105 finally: |
| 106 shutil.rmtree(tempdir) |
| 107 os.chdir(curdir) |
| 108 |
| 109 self.assertNotEqual(0, retval) |
| 110 self.assertEqual('', stdout.getvalue()) |
| 111 self.assertRegexpMatches(stderr.getvalue(), '^fatal: Not a git repository') |
| 112 |
| 113 def testBadFilename(self): |
| 114 """Tests the main function (bad filename).""" |
| 115 stdout = StringIO.StringIO() |
| 116 stderr = StringIO.StringIO() |
| 117 retval = self.repo.run(self.git_hyper_blame.main, |
| 118 args=['-i', 'tag_B', 'tag_C', 'some/files/xxxx'], |
| 119 stdout=stdout, stderr=stderr) |
| 120 self.assertNotEqual(0, retval) |
| 121 self.assertEqual('', stdout.getvalue()) |
| 122 self.assertEqual('fatal: no such path some/files/xxxx in %s\n' % |
| 123 self.repo['C'], stderr.getvalue()) |
| 124 |
| 125 def testBadRevision(self): |
| 126 """Tests the main function (bad revision to blame from).""" |
| 127 stdout = StringIO.StringIO() |
| 128 stderr = StringIO.StringIO() |
| 129 retval = self.repo.run(self.git_hyper_blame.main, |
| 130 args=['-i', 'tag_B', 'xxxx', 'some/files/file'], |
| 131 stdout=stdout, stderr=stderr) |
| 132 self.assertNotEqual(0, retval) |
| 133 self.assertEqual('', stdout.getvalue()) |
| 134 self.assertRegexpMatches(stderr.getvalue(), |
| 135 '^fatal: ambiguous argument \'xxxx\': unknown ' |
| 136 'revision or path not in the working tree.') |
| 137 |
| 138 def testBadIgnore(self): |
| 139 """Tests the main function (bad revision passed to -i).""" |
| 140 stdout = StringIO.StringIO() |
| 141 stderr = StringIO.StringIO() |
| 142 retval = self.repo.run(self.git_hyper_blame.main, |
| 143 args=['-i', 'xxxx', 'tag_C', 'some/files/file'], |
| 144 stdout=stdout, stderr=stderr) |
| 145 self.assertNotEqual(0, retval) |
| 146 self.assertEqual('', stdout.getvalue()) |
| 147 self.assertEqual('fatal: unknown revision \'xxxx\'.\n', stderr.getvalue()) |
| 148 |
| 149 class GitHyperBlameSimpleTest(GitHyperBlameTestBase): |
| 150 REPO_SCHEMA = """ |
| 151 A B D E F G H |
| 152 A C D |
| 153 """ |
| 154 |
| 155 COMMIT_A = { |
| 156 'some/files/file1': {'data': 'file1'}, |
| 157 'some/files/file2': {'data': 'file2'}, |
| 158 'some/files/empty': {'data': ''}, |
| 159 'some/other/file': {'data': 'otherfile'}, |
| 160 } |
| 161 |
| 162 COMMIT_B = { |
| 163 'some/files/file2': { |
| 164 'mode': 0755, |
| 165 'data': 'file2 - vanilla\n'}, |
| 166 'some/files/empty': {'data': 'not anymore'}, |
| 167 'some/files/file3': {'data': 'file3'}, |
| 168 } |
| 169 |
| 170 COMMIT_C = { |
| 171 'some/files/file2': {'data': 'file2 - merged\n'}, |
| 172 } |
| 173 |
| 174 COMMIT_D = { |
| 175 'some/files/file2': {'data': 'file2 - vanilla\nfile2 - merged\n'}, |
| 176 } |
| 177 |
| 178 COMMIT_E = { |
| 179 'some/files/file2': {'data': 'file2 - vanilla\nfile_x - merged\n'}, |
| 180 } |
| 181 |
| 182 COMMIT_F = { |
| 183 'some/files/file2': {'data': 'file2 - vanilla\nfile_y - merged\n'}, |
| 184 } |
| 185 |
| 186 # Move file2 from files to other. |
| 187 COMMIT_G = { |
| 188 'some/files/file2': {'data': None}, |
| 189 'some/other/file2': {'data': 'file2 - vanilla\nfile_y - merged\n'}, |
| 190 } |
| 191 |
| 192 COMMIT_H = { |
| 193 'some/other/file2': {'data': 'file2 - vanilla\nfile_z - merged\n'}, |
| 194 } |
| 195 |
| 196 def testBlameError(self): |
| 197 """Tests a blame on a non-existent file.""" |
| 198 expected_output = [''] |
| 199 retval, output = self.run_hyperblame([], 'some/other/file2', 'tag_D') |
| 200 self.assertNotEqual(0, retval) |
| 201 self.assertEqual(expected_output, output) |
| 202 |
| 203 def testBlameEmpty(self): |
| 204 """Tests a blame of an empty file with no ignores.""" |
| 205 expected_output = [''] |
| 206 retval, output = self.run_hyperblame([], 'some/files/empty', 'tag_A') |
| 207 self.assertEqual(0, retval) |
| 208 self.assertEqual(expected_output, output) |
| 209 |
| 210 def testBasicBlame(self): |
| 211 """Tests a basic blame with no ignores.""" |
| 212 # Expect to blame line 1 on B, line 2 on C. |
| 213 expected_output = [self.blame_line('B', '1) file2 - vanilla'), |
| 214 self.blame_line('C', '2) file2 - merged')] |
| 215 retval, output = self.run_hyperblame([], 'some/files/file2', 'tag_D') |
| 216 self.assertEqual(0, retval) |
| 217 self.assertEqual(expected_output, output) |
| 218 |
| 219 def testBlameRenamed(self): |
| 220 """Tests a blame with no ignores on a renamed file.""" |
| 221 # Expect to blame line 1 on B, line 2 on H. |
| 222 # Because the file has a different name than it had when (some of) these |
| 223 # lines were changed, expect the filenames to be displayed. |
| 224 expected_output = [self.blame_line('B', '1) file2 - vanilla', |
| 225 filename='some/files/file2'), |
| 226 self.blame_line('H', '2) file_z - merged', |
| 227 filename='some/other/file2')] |
| 228 retval, output = self.run_hyperblame([], 'some/other/file2', 'tag_H') |
| 229 self.assertEqual(0, retval) |
| 230 self.assertEqual(expected_output, output) |
| 231 |
| 232 def testIgnoreSimpleEdits(self): |
| 233 """Tests a blame with simple (line-level changes) commits ignored.""" |
| 234 # Expect to blame line 1 on B, line 2 on E. |
| 235 expected_output = [self.blame_line('B', '1) file2 - vanilla'), |
| 236 self.blame_line('E', '2) file_x - merged')] |
| 237 retval, output = self.run_hyperblame([], 'some/files/file2', 'tag_E') |
| 238 self.assertEqual(0, retval) |
| 239 self.assertEqual(expected_output, output) |
| 240 |
| 241 # Ignore E; blame line 1 on B, line 2 on C. |
| 242 expected_output = [self.blame_line('B', ' 1) file2 - vanilla'), |
| 243 self.blame_line('C', '2*) file_x - merged')] |
| 244 retval, output = self.run_hyperblame(['E'], 'some/files/file2', 'tag_E') |
| 245 self.assertEqual(0, retval) |
| 246 self.assertEqual(expected_output, output) |
| 247 |
| 248 # Ignore E and F; blame line 1 on B, line 2 on C. |
| 249 expected_output = [self.blame_line('B', ' 1) file2 - vanilla'), |
| 250 self.blame_line('C', '2*) file_y - merged')] |
| 251 retval, output = self.run_hyperblame(['E', 'F'], 'some/files/file2', |
| 252 'tag_F') |
| 253 self.assertEqual(0, retval) |
| 254 self.assertEqual(expected_output, output) |
| 255 |
| 256 def testIgnoreInitialCommit(self): |
| 257 """Tests a blame with the initial commit ignored.""" |
| 258 # Ignore A. Expect A to get blamed anyway. |
| 259 expected_output = [self.blame_line('A', '1) file1')] |
| 260 retval, output = self.run_hyperblame(['A'], 'some/files/file1', 'tag_A') |
| 261 self.assertEqual(0, retval) |
| 262 self.assertEqual(expected_output, output) |
| 263 |
| 264 def testIgnoreFileAdd(self): |
| 265 """Tests a blame ignoring the commit that added this file.""" |
| 266 # Ignore A. Expect A to get blamed anyway. |
| 267 expected_output = [self.blame_line('B', '1) file3')] |
| 268 retval, output = self.run_hyperblame(['B'], 'some/files/file3', 'tag_B') |
| 269 self.assertEqual(0, retval) |
| 270 self.assertEqual(expected_output, output) |
| 271 |
| 272 def testIgnoreFilePopulate(self): |
| 273 """Tests a blame ignoring the commit that added data to an empty file.""" |
| 274 # Ignore A. Expect A to get blamed anyway. |
| 275 expected_output = [self.blame_line('B', '1) not anymore')] |
| 276 retval, output = self.run_hyperblame(['B'], 'some/files/empty', 'tag_B') |
| 277 self.assertEqual(0, retval) |
| 278 self.assertEqual(expected_output, output) |
| 279 |
| 280 class GitHyperBlameLineMotionTest(GitHyperBlameTestBase): |
| 281 REPO_SCHEMA = """ |
| 282 A B C D E |
| 283 """ |
| 284 |
| 285 COMMIT_A = { |
| 286 'file': {'data': 'A\ngreen\nblue\n'}, |
| 287 } |
| 288 |
| 289 # Change "green" to "yellow". |
| 290 COMMIT_B = { |
| 291 'file': {'data': 'A\nyellow\nblue\n'}, |
| 292 } |
| 293 |
| 294 # Insert 2 lines at the top, |
| 295 # Change "yellow" to "red". |
| 296 COMMIT_C = { |
| 297 'file': {'data': 'X\nY\nA\nred\nblue\n'}, |
| 298 } |
| 299 |
| 300 # Insert 2 more lines at the top. |
| 301 COMMIT_D = { |
| 302 'file': {'data': 'earth\nfire\nX\nY\nA\nred\nblue\n'}, |
| 303 } |
| 304 |
| 305 # Insert a line before "red", and indent "red" and "blue". |
| 306 COMMIT_E = { |
| 307 'file': {'data': 'earth\nfire\nX\nY\nA\ncolors:\n red\n blue\n'}, |
| 308 } |
| 309 |
| 310 def testInterHunkLineMotion(self): |
| 311 """Tests a blame with line motion in another hunk in the ignored commit.""" |
| 312 # This test was mostly written as a demonstration of the limitations of the |
| 313 # current algorithm (it exhibits non-ideal behaviour). |
| 314 |
| 315 # Blame from D, ignoring C. |
| 316 # Lines 1, 2 were added by D. |
| 317 # Lines 3, 4 were added by C (but ignored, so blame A, B, respectively). |
| 318 # TODO(mgiuca): Ideally, this would blame both of these lines on A, because |
| 319 # they add lines nowhere near the changes made by B. |
| 320 # Line 5 was added by A. |
| 321 # Line 6 was modified by C (but ignored, so blame A). |
| 322 # TODO(mgiuca): Ideally, Line 6 would be blamed on B, because that was the |
| 323 # last commit to touch that line (changing "green" to "yellow"), but the |
| 324 # algorithm isn't yet able to figure out that Line 6 in D == Line 4 in C ~= |
| 325 # Line 2 in B. |
| 326 # Line 7 was added by A. |
| 327 expected_output = [self.blame_line('D', ' 1) earth'), |
| 328 self.blame_line('D', ' 2) fire'), |
| 329 self.blame_line('A', '3*) X'), |
| 330 self.blame_line('B', '4*) Y'), |
| 331 self.blame_line('A', ' 5) A'), |
| 332 self.blame_line('A', '6*) red'), |
| 333 self.blame_line('A', ' 7) blue'), |
| 334 ] |
| 335 retval, output = self.run_hyperblame(['C'], 'file', 'tag_D') |
| 336 self.assertEqual(0, retval) |
| 337 self.assertEqual(expected_output, output) |
| 338 |
| 339 def testIntraHunkLineMotion(self): |
| 340 """Tests a blame with line motion in the same hunk in the ignored commit.""" |
| 341 # This test was mostly written as a demonstration of the limitations of the |
| 342 # current algorithm (it exhibits non-ideal behaviour). |
| 343 |
| 344 # Blame from E, ignoring E. |
| 345 # Line 6 was added by E (but ignored, so blame C). |
| 346 # Lines 7, 8 were modified by E (but ignored, so blame A). |
| 347 # TODO(mgiuca): Ideally, this would blame Line 7 on C, because the line |
| 348 # "red" was added by C, and this is just a small change to that line. But |
| 349 # the current algorithm can't deal with line motion within a hunk, so it |
| 350 # just assumes Line 7 in E ~= Line 7 in D == Line 3 in A (which was "blue"). |
| 351 expected_output = [self.blame_line('D', ' 1) earth'), |
| 352 self.blame_line('D', ' 2) fire'), |
| 353 self.blame_line('C', ' 3) X'), |
| 354 self.blame_line('C', ' 4) Y'), |
| 355 self.blame_line('A', ' 5) A'), |
| 356 self.blame_line('C', '6*) colors:'), |
| 357 self.blame_line('A', '7*) red'), |
| 358 self.blame_line('A', '8*) blue'), |
| 359 ] |
| 360 retval, output = self.run_hyperblame(['E'], 'file', 'tag_E') |
| 361 self.assertEqual(0, retval) |
| 362 self.assertEqual(expected_output, output) |
| 363 |
| 364 |
| 365 if __name__ == '__main__': |
| 366 sys.exit(coverage_utils.covered_main( |
| 367 os.path.join(DEPOT_TOOLS_ROOT, 'git_hyper_blame.py'))) |
OLD | NEW |