git_hyper_blame_test.py 25 KB

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