git_hyper_blame_test.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641
  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. import datetime
  7. import os
  8. import re
  9. import shutil
  10. import StringIO
  11. import sys
  12. import tempfile
  13. import unittest
  14. DEPOT_TOOLS_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  15. sys.path.insert(0, DEPOT_TOOLS_ROOT)
  16. from testing_support import coverage_utils
  17. from testing_support import git_test_utils
  18. import git_common
  19. GitRepo = git_test_utils.GitRepo
  20. class GitHyperBlameTestBase(git_test_utils.GitRepoReadOnlyTestBase):
  21. @classmethod
  22. def setUpClass(cls):
  23. super(GitHyperBlameTestBase, cls).setUpClass()
  24. import git_hyper_blame
  25. cls.git_hyper_blame = git_hyper_blame
  26. def run_hyperblame(self, ignored, filename, revision):
  27. stdout = StringIO.StringIO()
  28. stderr = StringIO.StringIO()
  29. ignored = [self.repo[c] for c in ignored]
  30. retval = self.repo.run(self.git_hyper_blame.hyper_blame, ignored, filename,
  31. revision=revision, out=stdout, err=stderr)
  32. return retval, stdout.getvalue().rstrip().split('\n')
  33. def blame_line(self, commit_name, rest, author=None, filename=None):
  34. """Generate a blame line from a commit.
  35. Args:
  36. commit_name: The commit's schema name.
  37. rest: The blame line after the timestamp. e.g., '2) file2 - merged'.
  38. author: The author's name. If omitted, reads the name out of the commit.
  39. filename: The filename. If omitted, not shown in the blame line.
  40. """
  41. short = self.repo[commit_name][:8]
  42. start = '%s %s' % (short, filename) if filename else short
  43. if author is None:
  44. author = self.repo.show_commit(commit_name, format_string='%an %ai')
  45. else:
  46. author += self.repo.show_commit(commit_name, format_string=' %ai')
  47. return '%s (%s %s' % (start, author, rest)
  48. class GitHyperBlameMainTest(GitHyperBlameTestBase):
  49. """End-to-end tests on a very simple repo."""
  50. REPO_SCHEMA = "A B C D"
  51. COMMIT_A = {
  52. 'some/files/file': {'data': 'line 1\nline 2\n'},
  53. }
  54. COMMIT_B = {
  55. 'some/files/file': {'data': 'line 1\nline 2.1\n'},
  56. }
  57. COMMIT_C = {
  58. 'some/files/file': {'data': 'line 1.1\nline 2.1\n'},
  59. }
  60. COMMIT_D = {
  61. # This file should be automatically considered for ignore.
  62. '.git-blame-ignore-revs': {'data': 'tag_C'},
  63. # This file should not be considered.
  64. 'some/files/.git-blame-ignore-revs': {'data': 'tag_B'},
  65. }
  66. def setUp(self):
  67. super(GitHyperBlameMainTest, self).setUp()
  68. # Most tests want to check out C (so the .git-blame-ignore-revs is not
  69. # used).
  70. self.repo.git('checkout', '-f', 'tag_C')
  71. def testBasicBlame(self):
  72. """Tests the main function (simple end-to-end test with no ignores)."""
  73. expected_output = [self.blame_line('C', '1) line 1.1'),
  74. self.blame_line('B', '2) line 2.1')]
  75. stdout = StringIO.StringIO()
  76. stderr = StringIO.StringIO()
  77. retval = self.repo.run(self.git_hyper_blame.main,
  78. args=['tag_C', 'some/files/file'], stdout=stdout,
  79. stderr=stderr)
  80. self.assertEqual(0, retval)
  81. self.assertEqual(expected_output, stdout.getvalue().rstrip().split('\n'))
  82. self.assertEqual('', stderr.getvalue())
  83. def testIgnoreSimple(self):
  84. """Tests the main function (simple end-to-end test with ignores)."""
  85. expected_output = [self.blame_line('C', ' 1) line 1.1'),
  86. self.blame_line('A', '2*) line 2.1')]
  87. stdout = StringIO.StringIO()
  88. stderr = StringIO.StringIO()
  89. retval = self.repo.run(self.git_hyper_blame.main,
  90. args=['-i', 'tag_B', 'tag_C', 'some/files/file'],
  91. stdout=stdout, stderr=stderr)
  92. self.assertEqual(0, retval)
  93. self.assertEqual(expected_output, stdout.getvalue().rstrip().split('\n'))
  94. self.assertEqual('', stderr.getvalue())
  95. def testBadRepo(self):
  96. """Tests the main function (not in a repo)."""
  97. # Make a temp dir that has no .git directory.
  98. curdir = os.getcwd()
  99. tempdir = tempfile.mkdtemp(suffix='_nogit', prefix='git_repo')
  100. try:
  101. os.chdir(tempdir)
  102. stdout = StringIO.StringIO()
  103. stderr = StringIO.StringIO()
  104. retval = self.git_hyper_blame.main(
  105. args=['-i', 'tag_B', 'tag_C', 'some/files/file'], stdout=stdout,
  106. stderr=stderr)
  107. finally:
  108. shutil.rmtree(tempdir)
  109. os.chdir(curdir)
  110. self.assertNotEqual(0, retval)
  111. self.assertEqual('', stdout.getvalue())
  112. r = re.compile('^fatal: Not a git repository', re.I)
  113. self.assertRegexpMatches(stderr.getvalue(), r)
  114. def testBadFilename(self):
  115. """Tests the main function (bad filename)."""
  116. stdout = StringIO.StringIO()
  117. stderr = StringIO.StringIO()
  118. retval = self.repo.run(self.git_hyper_blame.main,
  119. args=['-i', 'tag_B', 'tag_C', 'some/files/xxxx'],
  120. stdout=stdout, stderr=stderr)
  121. self.assertNotEqual(0, retval)
  122. self.assertEqual('', stdout.getvalue())
  123. # TODO(mgiuca): This test used to test the exact string, but it broke due to
  124. # an upstream bug in git-blame. For now, just check the start of the string.
  125. # A patch has been sent upstream; when it rolls out we can revert back to
  126. # the original test logic.
  127. self.assertTrue(
  128. stderr.getvalue().startswith('fatal: no such path some/files/xxxx in '))
  129. def testBadRevision(self):
  130. """Tests the main function (bad revision to blame from)."""
  131. stdout = StringIO.StringIO()
  132. stderr = StringIO.StringIO()
  133. retval = self.repo.run(self.git_hyper_blame.main,
  134. args=['-i', 'tag_B', 'xxxx', 'some/files/file'],
  135. stdout=stdout, stderr=stderr)
  136. self.assertNotEqual(0, retval)
  137. self.assertEqual('', stdout.getvalue())
  138. self.assertRegexpMatches(stderr.getvalue(),
  139. '^fatal: ambiguous argument \'xxxx\': unknown '
  140. 'revision or path not in the working tree.')
  141. def testBadIgnore(self):
  142. """Tests the main function (bad revision passed to -i)."""
  143. expected_output = [self.blame_line('C', '1) line 1.1'),
  144. self.blame_line('B', '2) line 2.1')]
  145. stdout = StringIO.StringIO()
  146. stderr = StringIO.StringIO()
  147. retval = self.repo.run(self.git_hyper_blame.main,
  148. args=['-i', 'xxxx', 'tag_C', 'some/files/file'],
  149. stdout=stdout, stderr=stderr)
  150. self.assertEqual(0, retval)
  151. self.assertEqual(expected_output, stdout.getvalue().rstrip().split('\n'))
  152. self.assertEqual('warning: unknown revision \'xxxx\'.\n', stderr.getvalue())
  153. def testIgnoreFile(self):
  154. """Tests passing the ignore list in a file."""
  155. expected_output = [self.blame_line('C', ' 1) line 1.1'),
  156. self.blame_line('A', '2*) line 2.1')]
  157. stdout = StringIO.StringIO()
  158. stderr = StringIO.StringIO()
  159. with tempfile.NamedTemporaryFile(mode='w+', prefix='ignore') as ignore_file:
  160. ignore_file.write('# Line comments are allowed.\n'.format(self.repo['B']))
  161. ignore_file.write('\n')
  162. ignore_file.write('{}\n'.format(self.repo['B']))
  163. # A revision that is not in the repo (should be ignored).
  164. ignore_file.write('xxxx\n')
  165. ignore_file.flush()
  166. retval = self.repo.run(self.git_hyper_blame.main,
  167. args=['--ignore-file', ignore_file.name, 'tag_C',
  168. 'some/files/file'],
  169. stdout=stdout, stderr=stderr)
  170. self.assertEqual(0, retval)
  171. self.assertEqual(expected_output, stdout.getvalue().rstrip().split('\n'))
  172. self.assertEqual('warning: unknown revision \'xxxx\'.\n', stderr.getvalue())
  173. def testDefaultIgnoreFile(self):
  174. """Tests automatically using a default ignore list."""
  175. # Check out revision D. We expect the script to use the default ignore list
  176. # that is checked out, *not* the one committed at the given revision.
  177. self.repo.git('checkout', '-f', 'tag_D')
  178. expected_output = [self.blame_line('A', '1*) line 1.1'),
  179. self.blame_line('B', ' 2) line 2.1')]
  180. stdout = StringIO.StringIO()
  181. stderr = StringIO.StringIO()
  182. retval = self.repo.run(self.git_hyper_blame.main,
  183. args=['tag_D', 'some/files/file'],
  184. stdout=stdout, stderr=stderr)
  185. self.assertEqual(0, retval)
  186. self.assertEqual(expected_output, stdout.getvalue().rstrip().split('\n'))
  187. self.assertEqual('', stderr.getvalue())
  188. # Test blame from a different revision. Despite the default ignore file
  189. # *not* being committed at that revision, it should still be picked up
  190. # because D is currently checked out.
  191. stdout = StringIO.StringIO()
  192. stderr = StringIO.StringIO()
  193. retval = self.repo.run(self.git_hyper_blame.main,
  194. args=['tag_C', 'some/files/file'],
  195. stdout=stdout, stderr=stderr)
  196. self.assertEqual(0, retval)
  197. self.assertEqual(expected_output, stdout.getvalue().rstrip().split('\n'))
  198. self.assertEqual('', stderr.getvalue())
  199. def testNoDefaultIgnores(self):
  200. """Tests the --no-default-ignores switch."""
  201. # Check out revision D. This has a .git-blame-ignore-revs file, which we
  202. # expect to be ignored due to --no-default-ignores.
  203. self.repo.git('checkout', '-f', 'tag_D')
  204. expected_output = [self.blame_line('C', '1) line 1.1'),
  205. self.blame_line('B', '2) line 2.1')]
  206. stdout = StringIO.StringIO()
  207. stderr = StringIO.StringIO()
  208. retval = self.repo.run(
  209. self.git_hyper_blame.main,
  210. args=['tag_D', 'some/files/file', '--no-default-ignores'],
  211. stdout=stdout, stderr=stderr)
  212. self.assertEqual(0, retval)
  213. self.assertEqual(expected_output, stdout.getvalue().rstrip().split('\n'))
  214. self.assertEqual('', stderr.getvalue())
  215. class GitHyperBlameSimpleTest(GitHyperBlameTestBase):
  216. REPO_SCHEMA = """
  217. A B D E F G H
  218. A C D
  219. """
  220. COMMIT_A = {
  221. 'some/files/file1': {'data': 'file1'},
  222. 'some/files/file2': {'data': 'file2'},
  223. 'some/files/empty': {'data': ''},
  224. 'some/other/file': {'data': 'otherfile'},
  225. }
  226. COMMIT_B = {
  227. 'some/files/file2': {
  228. 'mode': 0o755,
  229. 'data': 'file2 - vanilla\n'},
  230. 'some/files/empty': {'data': 'not anymore'},
  231. 'some/files/file3': {'data': 'file3'},
  232. }
  233. COMMIT_C = {
  234. 'some/files/file2': {'data': 'file2 - merged\n'},
  235. }
  236. COMMIT_D = {
  237. 'some/files/file2': {'data': 'file2 - vanilla\nfile2 - merged\n'},
  238. }
  239. COMMIT_E = {
  240. 'some/files/file2': {'data': 'file2 - vanilla\nfile_x - merged\n'},
  241. }
  242. COMMIT_F = {
  243. 'some/files/file2': {'data': 'file2 - vanilla\nfile_y - merged\n'},
  244. }
  245. # Move file2 from files to other.
  246. COMMIT_G = {
  247. 'some/files/file2': {'data': None},
  248. 'some/other/file2': {'data': 'file2 - vanilla\nfile_y - merged\n'},
  249. }
  250. COMMIT_H = {
  251. 'some/other/file2': {'data': 'file2 - vanilla\nfile_z - merged\n'},
  252. }
  253. def testBlameError(self):
  254. """Tests a blame on a non-existent file."""
  255. expected_output = ['']
  256. retval, output = self.run_hyperblame([], 'some/other/file2', 'tag_D')
  257. self.assertNotEqual(0, retval)
  258. self.assertEqual(expected_output, output)
  259. def testBlameEmpty(self):
  260. """Tests a blame of an empty file with no ignores."""
  261. expected_output = ['']
  262. retval, output = self.run_hyperblame([], 'some/files/empty', 'tag_A')
  263. self.assertEqual(0, retval)
  264. self.assertEqual(expected_output, output)
  265. def testBasicBlame(self):
  266. """Tests a basic blame with no ignores."""
  267. # Expect to blame line 1 on B, line 2 on C.
  268. expected_output = [self.blame_line('B', '1) file2 - vanilla'),
  269. self.blame_line('C', '2) file2 - merged')]
  270. retval, output = self.run_hyperblame([], 'some/files/file2', 'tag_D')
  271. self.assertEqual(0, retval)
  272. self.assertEqual(expected_output, output)
  273. def testBlameRenamed(self):
  274. """Tests a blame with no ignores on a renamed file."""
  275. # Expect to blame line 1 on B, line 2 on H.
  276. # Because the file has a different name than it had when (some of) these
  277. # lines were changed, expect the filenames to be displayed.
  278. expected_output = [self.blame_line('B', '1) file2 - vanilla',
  279. filename='some/files/file2'),
  280. self.blame_line('H', '2) file_z - merged',
  281. filename='some/other/file2')]
  282. retval, output = self.run_hyperblame([], 'some/other/file2', 'tag_H')
  283. self.assertEqual(0, retval)
  284. self.assertEqual(expected_output, output)
  285. def testIgnoreSimpleEdits(self):
  286. """Tests a blame with simple (line-level changes) commits ignored."""
  287. # Expect to blame line 1 on B, line 2 on E.
  288. expected_output = [self.blame_line('B', '1) file2 - vanilla'),
  289. self.blame_line('E', '2) file_x - merged')]
  290. retval, output = self.run_hyperblame([], 'some/files/file2', 'tag_E')
  291. self.assertEqual(0, retval)
  292. self.assertEqual(expected_output, output)
  293. # Ignore E; blame line 1 on B, line 2 on C.
  294. expected_output = [self.blame_line('B', ' 1) file2 - vanilla'),
  295. self.blame_line('C', '2*) file_x - merged')]
  296. retval, output = self.run_hyperblame(['E'], 'some/files/file2', 'tag_E')
  297. self.assertEqual(0, retval)
  298. self.assertEqual(expected_output, output)
  299. # Ignore E and F; blame line 1 on B, line 2 on C.
  300. expected_output = [self.blame_line('B', ' 1) file2 - vanilla'),
  301. self.blame_line('C', '2*) file_y - merged')]
  302. retval, output = self.run_hyperblame(['E', 'F'], 'some/files/file2',
  303. 'tag_F')
  304. self.assertEqual(0, retval)
  305. self.assertEqual(expected_output, output)
  306. def testIgnoreInitialCommit(self):
  307. """Tests a blame with the initial commit ignored."""
  308. # Ignore A. Expect A to get blamed anyway.
  309. expected_output = [self.blame_line('A', '1) file1')]
  310. retval, output = self.run_hyperblame(['A'], 'some/files/file1', 'tag_A')
  311. self.assertEqual(0, retval)
  312. self.assertEqual(expected_output, output)
  313. def testIgnoreFileAdd(self):
  314. """Tests a blame ignoring the commit that added this file."""
  315. # Ignore A. Expect A to get blamed anyway.
  316. expected_output = [self.blame_line('B', '1) file3')]
  317. retval, output = self.run_hyperblame(['B'], 'some/files/file3', 'tag_B')
  318. self.assertEqual(0, retval)
  319. self.assertEqual(expected_output, output)
  320. def testIgnoreFilePopulate(self):
  321. """Tests a blame ignoring the commit that added data to an empty file."""
  322. # Ignore A. Expect A to get blamed anyway.
  323. expected_output = [self.blame_line('B', '1) not anymore')]
  324. retval, output = self.run_hyperblame(['B'], 'some/files/empty', 'tag_B')
  325. self.assertEqual(0, retval)
  326. self.assertEqual(expected_output, output)
  327. class GitHyperBlameLineMotionTest(GitHyperBlameTestBase):
  328. REPO_SCHEMA = """
  329. A B C D E F
  330. """
  331. COMMIT_A = {
  332. 'file': {'data': 'A\ngreen\nblue\n'},
  333. }
  334. # Change "green" to "yellow".
  335. COMMIT_B = {
  336. 'file': {'data': 'A\nyellow\nblue\n'},
  337. }
  338. # Insert 2 lines at the top,
  339. # Change "yellow" to "red".
  340. # Insert 1 line at the bottom.
  341. COMMIT_C = {
  342. 'file': {'data': 'X\nY\nA\nred\nblue\nZ\n'},
  343. }
  344. # Insert 2 more lines at the top.
  345. COMMIT_D = {
  346. 'file': {'data': 'earth\nfire\nX\nY\nA\nred\nblue\nZ\n'},
  347. }
  348. # Insert a line before "red", and indent "red" and "blue".
  349. COMMIT_E = {
  350. 'file': {'data': 'earth\nfire\nX\nY\nA\ncolors:\n red\n blue\nZ\n'},
  351. }
  352. # Insert a line between "A" and "colors".
  353. COMMIT_F = {
  354. 'file': {'data': 'earth\nfire\nX\nY\nA\nB\ncolors:\n red\n blue\nZ\n'},
  355. }
  356. def testCacheDiffHunks(self):
  357. """Tests the cache_diff_hunks internal function."""
  358. expected_hunks = [((0, 0), (1, 2)),
  359. ((2, 1), (4, 1)),
  360. ((3, 0), (6, 1)),
  361. ]
  362. hunks = self.repo.run(self.git_hyper_blame.cache_diff_hunks, 'tag_B',
  363. 'tag_C')
  364. self.assertEqual(expected_hunks, hunks)
  365. def testApproxLinenoAcrossRevs(self):
  366. """Tests the approx_lineno_across_revs internal function."""
  367. # Note: For all of these tests, the "old revision" and "new revision" are
  368. # reversed, which matches the usage by hyper_blame.
  369. # Test an unchanged line before any hunks in the diff. Should be unchanged.
  370. lineno = self.repo.run(self.git_hyper_blame.approx_lineno_across_revs,
  371. 'file', 'file', 'tag_B', 'tag_A', 1)
  372. self.assertEqual(1, lineno)
  373. # Test an unchanged line after all hunks in the diff. Should be matched to
  374. # the line's previous position in the file.
  375. lineno = self.repo.run(self.git_hyper_blame.approx_lineno_across_revs,
  376. 'file', 'file', 'tag_D', 'tag_C', 6)
  377. self.assertEqual(4, lineno)
  378. # Test a line added in a new hunk. Should be matched to the line *before*
  379. # where the hunk was inserted in the old version of the file.
  380. lineno = self.repo.run(self.git_hyper_blame.approx_lineno_across_revs,
  381. 'file', 'file', 'tag_F', 'tag_E', 6)
  382. self.assertEqual(5, lineno)
  383. # Test lines added in a new hunk at the very start of the file. This tests
  384. # an edge case: normally it would be matched to the line *before* where the
  385. # hunk was inserted (Line 0), but since the hunk is at the start of the
  386. # file, we match to Line 1.
  387. lineno = self.repo.run(self.git_hyper_blame.approx_lineno_across_revs,
  388. 'file', 'file', 'tag_C', 'tag_B', 1)
  389. self.assertEqual(1, lineno)
  390. lineno = self.repo.run(self.git_hyper_blame.approx_lineno_across_revs,
  391. 'file', 'file', 'tag_C', 'tag_B', 2)
  392. self.assertEqual(1, lineno)
  393. # Test an unchanged line in between hunks in the diff. Should be matched to
  394. # the line's previous position in the file.
  395. lineno = self.repo.run(self.git_hyper_blame.approx_lineno_across_revs,
  396. 'file', 'file', 'tag_C', 'tag_B', 3)
  397. self.assertEqual(1, lineno)
  398. # Test a changed line. Should be matched to the hunk's previous position in
  399. # the file.
  400. lineno = self.repo.run(self.git_hyper_blame.approx_lineno_across_revs,
  401. 'file', 'file', 'tag_C', 'tag_B', 4)
  402. self.assertEqual(2, lineno)
  403. # Test a line added in a new hunk at the very end of the file. Should be
  404. # matched to the line *before* where the hunk was inserted (the last line of
  405. # the file). Technically same as the case above but good to boundary test.
  406. lineno = self.repo.run(self.git_hyper_blame.approx_lineno_across_revs,
  407. 'file', 'file', 'tag_C', 'tag_B', 6)
  408. self.assertEqual(3, lineno)
  409. def testInterHunkLineMotion(self):
  410. """Tests a blame with line motion in another hunk in the ignored commit."""
  411. # Blame from D, ignoring C.
  412. # Lines 1, 2 were added by D.
  413. # Lines 3, 4 were added by C (but ignored, so blame A).
  414. # Line 5 was added by A.
  415. # Line 6 was modified by C (but ignored, so blame B). (Note: This requires
  416. # the algorithm to figure out that Line 6 in D == Line 4 in C ~= Line 2 in
  417. # B, so it blames B. Otherwise, it would blame A.)
  418. # Line 7 was added by A.
  419. # Line 8 was added by C (but ignored, so blame A).
  420. expected_output = [self.blame_line('D', ' 1) earth'),
  421. self.blame_line('D', ' 2) fire'),
  422. self.blame_line('A', '3*) X'),
  423. self.blame_line('A', '4*) Y'),
  424. self.blame_line('A', ' 5) A'),
  425. self.blame_line('B', '6*) red'),
  426. self.blame_line('A', ' 7) blue'),
  427. self.blame_line('A', '8*) Z'),
  428. ]
  429. retval, output = self.run_hyperblame(['C'], 'file', 'tag_D')
  430. self.assertEqual(0, retval)
  431. self.assertEqual(expected_output, output)
  432. def testIntraHunkLineMotion(self):
  433. """Tests a blame with line motion in the same hunk in the ignored commit."""
  434. # This test was mostly written as a demonstration of the limitations of the
  435. # current algorithm (it exhibits non-ideal behaviour).
  436. # Blame from E, ignoring E.
  437. # Line 6 was added by E (but ignored, so blame C).
  438. # Lines 7, 8 were modified by E (but ignored, so blame A).
  439. # TODO(mgiuca): Ideally, this would blame Line 7 on C, because the line
  440. # "red" was added by C, and this is just a small change to that line. But
  441. # the current algorithm can't deal with line motion within a hunk, so it
  442. # just assumes Line 7 in E ~= Line 7 in D == Line 3 in A (which was "blue").
  443. expected_output = [self.blame_line('D', ' 1) earth'),
  444. self.blame_line('D', ' 2) fire'),
  445. self.blame_line('C', ' 3) X'),
  446. self.blame_line('C', ' 4) Y'),
  447. self.blame_line('A', ' 5) A'),
  448. self.blame_line('C', '6*) colors:'),
  449. self.blame_line('A', '7*) red'),
  450. self.blame_line('A', '8*) blue'),
  451. self.blame_line('C', ' 9) Z'),
  452. ]
  453. retval, output = self.run_hyperblame(['E'], 'file', 'tag_E')
  454. self.assertEqual(0, retval)
  455. self.assertEqual(expected_output, output)
  456. class GitHyperBlameLineNumberTest(GitHyperBlameTestBase):
  457. REPO_SCHEMA = """
  458. A B C D
  459. """
  460. COMMIT_A = {
  461. 'file': {'data': 'red\nblue\n'},
  462. }
  463. # Change "blue" to "green".
  464. COMMIT_B = {
  465. 'file': {'data': 'red\ngreen\n'},
  466. }
  467. # Insert 2 lines at the top,
  468. COMMIT_C = {
  469. 'file': {'data': '\n\nred\ngreen\n'},
  470. }
  471. # Change "green" to "yellow".
  472. COMMIT_D = {
  473. 'file': {'data': '\n\nred\nyellow\n'},
  474. }
  475. def testTwoChangesWithAddedLines(self):
  476. """Regression test for https://crbug.com/709831.
  477. Tests a line with multiple ignored edits, and a line number change in
  478. between (such that the line number in the current revision is bigger than
  479. the file's line count at the older ignored revision).
  480. """
  481. expected_output = [self.blame_line('C', ' 1) '),
  482. self.blame_line('C', ' 2) '),
  483. self.blame_line('A', ' 3) red'),
  484. self.blame_line('A', '4*) yellow'),
  485. ]
  486. # Due to https://crbug.com/709831, ignoring both B and D would crash,
  487. # because of C (in between those revisions) which moves Line 2 to Line 4.
  488. # The algorithm would incorrectly think that Line 4 was still on Line 4 in
  489. # Commit B, even though it was Line 2 at that time. Its index is out of
  490. # range in the number of lines in Commit B.
  491. retval, output = self.run_hyperblame(['B', 'D'], 'file', 'tag_D')
  492. self.assertEqual(0, retval)
  493. self.assertEqual(expected_output, output)
  494. class GitHyperBlameUnicodeTest(GitHyperBlameTestBase):
  495. REPO_SCHEMA = """
  496. A B C
  497. """
  498. COMMIT_A = {
  499. GitRepo.AUTHOR_NAME: 'ASCII Author',
  500. 'file': {'data': 'red\nblue\n'},
  501. }
  502. # Add a line.
  503. COMMIT_B = {
  504. GitRepo.AUTHOR_NAME: u'\u4e2d\u56fd\u4f5c\u8005'.encode('utf-8'),
  505. 'file': {'data': 'red\ngreen\nblue\n'},
  506. }
  507. # Modify a line with non-UTF-8 author and file text.
  508. COMMIT_C = {
  509. GitRepo.AUTHOR_NAME: u'Lat\u00edn-1 Author'.encode('latin-1'),
  510. 'file': {'data': u'red\ngre\u00e9n\nblue\n'.encode('latin-1')},
  511. }
  512. def testNonASCIIAuthorName(self):
  513. """Ensures correct tabulation.
  514. Tests the case where there are non-ASCII (UTF-8) characters in the author
  515. name.
  516. Regression test for https://crbug.com/808905.
  517. """
  518. expected_output = [
  519. self.blame_line('A', '1) red', author='ASCII Author'),
  520. # Expect 8 spaces, to line up with the other name.
  521. self.blame_line('B', '2) green',
  522. author=u'\u4e2d\u56fd\u4f5c\u8005 '.encode('utf-8')),
  523. self.blame_line('A', '3) blue', author='ASCII Author'),
  524. ]
  525. retval, output = self.run_hyperblame([], 'file', 'tag_B')
  526. self.assertEqual(0, retval)
  527. self.assertEqual(expected_output, output)
  528. def testNonUTF8Data(self):
  529. """Ensures correct behaviour even if author or file data is not UTF-8.
  530. There is no guarantee that a file will be UTF-8-encoded, so this is
  531. realistic.
  532. """
  533. expected_output = [
  534. self.blame_line('A', '1) red', author='ASCII Author '),
  535. # The Author has been re-encoded as UTF-8. The file data is preserved as
  536. # raw byte data.
  537. self.blame_line('C', '2) gre\xe9n', author='Lat\xc3\xadn-1 Author'),
  538. self.blame_line('A', '3) blue', author='ASCII Author '),
  539. ]
  540. retval, output = self.run_hyperblame([], 'file', 'tag_C')
  541. self.assertEqual(0, retval)
  542. self.assertEqual(expected_output, output)
  543. if __name__ == '__main__':
  544. sys.exit(coverage_utils.covered_main(
  545. os.path.join(DEPOT_TOOLS_ROOT, 'git_hyper_blame.py')))