git_hyper_blame_test.py 24 KB

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