presubmit_support_test.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. #!/usr/bin/env python3
  2. # Copyright 2024 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. import io
  6. import os.path
  7. import sys
  8. import unittest
  9. from unittest import mock
  10. ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  11. sys.path.insert(0, ROOT_DIR)
  12. import gclient_utils
  13. import presubmit_support
  14. import subprocess2
  15. from testing_support import fake_repos
  16. class PresubmitSupportTest(unittest.TestCase):
  17. def test_environ(self):
  18. self.assertIsNone(os.environ.get('PRESUBMIT_FOO_ENV', None))
  19. kv = {'PRESUBMIT_FOO_ENV': 'FOOBAR'}
  20. with presubmit_support.setup_environ(kv):
  21. self.assertEqual(os.environ.get('PRESUBMIT_FOO_ENV', None),
  22. 'FOOBAR')
  23. self.assertIsNone(os.environ.get('PRESUBMIT_FOO_ENV', None))
  24. class ProvidedDiffChangeFakeRepo(fake_repos.FakeReposBase):
  25. NB_GIT_REPOS = 1
  26. def populateGit(self):
  27. self._commit_git(
  28. 'repo_1', {
  29. 'to_be_modified': 'please change me\n',
  30. 'to_be_deleted': 'delete\nme\n',
  31. 'somewhere/else': 'not a top level file!\n',
  32. })
  33. self._commit_git(
  34. 'repo_1', {
  35. 'to_be_modified': 'changed me!\n',
  36. 'to_be_deleted': None,
  37. 'somewhere/else': 'still not a top level file!\n',
  38. 'added': 'a new file\n',
  39. })
  40. class ProvidedDiffChangeTest(fake_repos.FakeReposTestBase):
  41. FAKE_REPOS_CLASS = ProvidedDiffChangeFakeRepo
  42. def setUp(self):
  43. super(ProvidedDiffChangeTest, self).setUp()
  44. self.enabled = self.FAKE_REPOS.set_up_git()
  45. if not self.enabled:
  46. self.skipTest('git fake repos not available')
  47. self.repo = os.path.join(self.FAKE_REPOS.git_base, 'repo_1')
  48. diff = subprocess2.check_output(['git', 'show'],
  49. cwd=self.repo).decode('utf-8')
  50. self.change = self._create_change(diff)
  51. def _create_change(self, diff):
  52. with gclient_utils.temporary_file() as tmp:
  53. gclient_utils.FileWrite(tmp, diff)
  54. options = mock.Mock(root=self.repo,
  55. all_files=False,
  56. generate_diff=False,
  57. description='description',
  58. files=None,
  59. diff_file=tmp)
  60. change = presubmit_support._parse_change(None, options)
  61. assert isinstance(change, presubmit_support.ProvidedDiffChange)
  62. return change
  63. def _get_affected_file_from_name(self, change, name):
  64. for file in change._affected_files:
  65. if file.LocalPath() == os.path.normpath(name):
  66. return file
  67. self.fail(f'No file named {name}')
  68. def _test(self, name, old, new):
  69. affected_file = self._get_affected_file_from_name(self.change, name)
  70. self.assertEqual(affected_file.OldContents(), old)
  71. self.assertEqual(affected_file.NewContents(), new)
  72. def test_old_contents_of_added_file_returns_empty(self):
  73. self._test('added', [], ['a new file'])
  74. def test_old_contents_of_deleted_file_returns_whole_file(self):
  75. self._test('to_be_deleted', ['delete', 'me'], [])
  76. def test_old_contents_of_modified_file(self):
  77. self._test('to_be_modified', ['please change me'], ['changed me!'])
  78. def test_old_contents_of_file_with_nested_dirs(self):
  79. self._test('somewhere/else', ['not a top level file!'],
  80. ['still not a top level file!'])
  81. def test_unix_local_paths(self):
  82. if sys.platform == 'win32':
  83. self.assertIn(r'somewhere\else', self.change.LocalPaths())
  84. else:
  85. self.assertIn('somewhere/else', self.change.LocalPaths())
  86. self.assertIn('somewhere/else', self.change.UnixLocalPaths())
  87. class TestGenerateDiff(fake_repos.FakeReposTestBase):
  88. """ Tests for --generate_diff.
  89. The option is used to generate diffs of given files against the upstream
  90. server as base.
  91. """
  92. FAKE_REPOS_CLASS = ProvidedDiffChangeFakeRepo
  93. def setUp(self):
  94. super().setUp()
  95. self.repo = os.path.join(self.FAKE_REPOS.git_base, 'repo_1')
  96. self.parser = mock.Mock()
  97. self.parser.error.side_effect = SystemExit
  98. def test_with_diff_file(self):
  99. """Tests that only either --generate_diff or --diff_file is allowed."""
  100. options = mock.Mock(root=self.repo,
  101. all_files=False,
  102. generate_diff=True,
  103. description='description',
  104. files=None,
  105. diff_file="patch.diff")
  106. with self.assertRaises(SystemExit):
  107. presubmit_support._parse_change(self.parser, options)
  108. self.parser.error.assert_called_once_with(
  109. '<diff_file> cannot be specified when <generate_diff> is set.', )
  110. @mock.patch('presubmit_diff.create_diffs')
  111. def test_with_all_files(self, create_diffs):
  112. """Ensures --generate_diff is noop if --all_files is specified."""
  113. options = mock.Mock(root=self.repo,
  114. all_files=True,
  115. generate_diff=True,
  116. description='description',
  117. files=None,
  118. source_controlled_only=False,
  119. diff_file=None)
  120. changes = presubmit_support._parse_change(self.parser, options)
  121. self.assertEqual(changes.AllFiles(),
  122. ['added', 'somewhere/else', 'to_be_modified'])
  123. create_diffs.assert_not_called()
  124. @mock.patch('presubmit_diff.fetch_content')
  125. def test_with_files(self, fetch_content):
  126. """Tests --generate_diff with files, which should call create_diffs()."""
  127. # fetch_content would return the old content of a given file.
  128. # In this test case, the mocked file is a newly added file.
  129. # hence, empty content.
  130. fetch_content.side_effect = ['']
  131. options = mock.Mock(root=self.repo,
  132. all_files=False,
  133. gerrit_url='https://chromium.googlesource.com',
  134. generate_diff=True,
  135. description='description',
  136. files=['added'],
  137. source_controlled_only=False,
  138. diff_file=None)
  139. change = presubmit_support._parse_change(self.parser, options)
  140. affected_files = change.AffectedFiles()
  141. self.assertEqual(len(affected_files), 1)
  142. self.assertEqual(affected_files[0].LocalPath(), 'added')
  143. class TestParseDiff(unittest.TestCase):
  144. """A suite of tests related to diff parsing and processing."""
  145. def _test_diff_to_change_files(self, diff, expected):
  146. with gclient_utils.temporary_file() as tmp:
  147. gclient_utils.FileWrite(tmp, diff, mode='w+')
  148. content, change_files = presubmit_support._process_diff_file(tmp)
  149. self.assertCountEqual(content, diff)
  150. self.assertCountEqual(change_files, expected)
  151. def test_diff_to_change_files_raises_on_empty_diff_header(self):
  152. diff = """
  153. diff --git a/foo b/foo
  154. """
  155. with self.assertRaises(presubmit_support.PresubmitFailure):
  156. self._test_diff_to_change_files(diff=diff, expected=[])
  157. def test_diff_to_change_files_simple_add(self):
  158. diff = """
  159. diff --git a/foo b/foo
  160. new file mode 100644
  161. index 0000000..9daeafb
  162. --- /dev/null
  163. +++ b/foo
  164. @@ -0,0 +1 @@
  165. +add
  166. """
  167. self._test_diff_to_change_files(diff=diff, expected=[('A', 'foo')])
  168. def test_diff_to_change_files_simple_delete(self):
  169. diff = """
  170. diff --git a/foo b/foo
  171. deleted file mode 100644
  172. index f675c2a..0000000
  173. --- a/foo
  174. +++ /dev/null
  175. @@ -1,1 +0,0 @@
  176. -delete
  177. """
  178. self._test_diff_to_change_files(diff=diff, expected=[('D', 'foo')])
  179. def test_diff_to_change_files_simple_modification(self):
  180. diff = """
  181. diff --git a/foo b/foo
  182. index d7ba659f..b7957f3 100644
  183. --- a/foo
  184. +++ b/foo
  185. @@ -29,7 +29,7 @@
  186. other
  187. random
  188. text
  189. - foo1
  190. + foo2
  191. other
  192. random
  193. text
  194. """
  195. self._test_diff_to_change_files(diff=diff, expected=[('M', 'foo')])
  196. def test_diff_to_change_files_multiple_changes(self):
  197. diff = """
  198. diff --git a/foo b/foo
  199. index d7ba659f..b7957f3 100644
  200. --- a/foo
  201. +++ b/foo
  202. @@ -29,7 +29,7 @@
  203. other
  204. random
  205. text
  206. - foo1
  207. + foo2
  208. other
  209. random
  210. text
  211. diff --git a/bar b/bar
  212. new file mode 100644
  213. index 0000000..9daeafb
  214. --- /dev/null
  215. +++ b/bar
  216. @@ -0,0 +1 @@
  217. +add
  218. diff --git a/baz b/baz
  219. deleted file mode 100644
  220. index f675c2a..0000000
  221. --- a/baz
  222. +++ /dev/null
  223. @@ -1,1 +0,0 @@
  224. -delete
  225. """
  226. self._test_diff_to_change_files(diff=diff,
  227. expected=[('M', 'foo'), ('A', 'bar'),
  228. ('D', 'baz')])
  229. def test_parse_unified_diff_with_valid_diff(self):
  230. diff = """
  231. diff --git a/foo b/foo
  232. new file mode 100644
  233. index 0000000..9daeafb
  234. --- /dev/null
  235. +++ b/foo
  236. @@ -0,0 +1 @@
  237. +add
  238. """
  239. res = presubmit_support._parse_unified_diff(diff)
  240. self.assertCountEqual(
  241. res, {
  242. 'foo':
  243. """
  244. new file mode 100644
  245. index 0000000..9daeafb
  246. --- /dev/null
  247. +++ b/foo
  248. @@ -0,0 +1 @@
  249. +add
  250. """
  251. })
  252. def test_parse_unified_diff_with_valid_diff_noprefix(self):
  253. diff = """
  254. diff --git foo foo
  255. new file mode 100644
  256. index 0000000..9daeafb
  257. --- /dev/null
  258. +++ foo
  259. @@ -0,0 +1 @@
  260. +add
  261. """
  262. res = presubmit_support._parse_unified_diff(diff)
  263. self.assertCountEqual(
  264. res, {
  265. 'foo':
  266. """
  267. new file mode 100644
  268. index 0000000..9daeafb
  269. --- /dev/null
  270. +++ foo
  271. @@ -0,0 +1 @@
  272. +add
  273. """
  274. })
  275. def test_parse_unified_diff_with_invalid_diff(self):
  276. diff = """
  277. diff --git a/ffoo b/foo
  278. """
  279. with self.assertRaises(presubmit_support.PresubmitFailure):
  280. presubmit_support._parse_unified_diff(diff)
  281. def test_diffs_to_change_files_with_empty_diff(self):
  282. res = presubmit_support._diffs_to_change_files({'file': ''})
  283. self.assertEqual(res, [('M', 'file')])
  284. class PresubmitResultLocationTest(unittest.TestCase):
  285. def test_invalid_missing_filepath(self):
  286. with self.assertRaisesRegex(ValueError, 'file path is required'):
  287. presubmit_support._PresubmitResultLocation(file_path='').validate()
  288. def test_invalid_abs_filepath_except_for_commit_msg(self):
  289. loc = presubmit_support._PresubmitResultLocation(file_path='/foo')
  290. with self.assertRaisesRegex(ValueError,
  291. 'file path must be relative path'):
  292. loc.validate()
  293. loc = presubmit_support._PresubmitResultLocation(
  294. file_path='/COMMIT_MSG')
  295. try:
  296. loc.validate()
  297. except ValueError:
  298. self.fail("validate should not fail for /COMMIT_MSG path")
  299. def test_invalid_end_line_without_start_line(self):
  300. loc = presubmit_support._PresubmitResultLocation(file_path='foo',
  301. end_line=5)
  302. with self.assertRaisesRegex(ValueError, 'end_line must be empty'):
  303. loc.validate()
  304. def test_invalid_start_col_without_start_line(self):
  305. loc = presubmit_support._PresubmitResultLocation(file_path='foo',
  306. start_col=5)
  307. with self.assertRaisesRegex(ValueError, 'start_col must be empty'):
  308. loc.validate()
  309. def test_invalid_end_col_without_start_line(self):
  310. loc = presubmit_support._PresubmitResultLocation(file_path='foo',
  311. end_col=5)
  312. with self.assertRaisesRegex(ValueError, 'end_col must be empty'):
  313. loc.validate()
  314. def test_invalid_negative_start_line(self):
  315. loc = presubmit_support._PresubmitResultLocation(file_path='foo',
  316. start_line=-1)
  317. with self.assertRaisesRegex(ValueError,
  318. 'start_line MUST not be negative'):
  319. loc.validate()
  320. def test_invalid_non_positive_end_line(self):
  321. loc = presubmit_support._PresubmitResultLocation(file_path='foo',
  322. start_line=1,
  323. end_line=0)
  324. with self.assertRaisesRegex(ValueError, 'end_line must be positive'):
  325. loc.validate()
  326. def test_invalid_negative_start_col(self):
  327. loc = presubmit_support._PresubmitResultLocation(file_path='foo',
  328. start_line=1,
  329. end_line=1,
  330. start_col=-1)
  331. with self.assertRaisesRegex(ValueError,
  332. 'start_col MUST not be negative'):
  333. loc.validate()
  334. def test_invalid_negative_end_col(self):
  335. loc = presubmit_support._PresubmitResultLocation(file_path='foo',
  336. start_line=1,
  337. end_line=1,
  338. end_col=-1)
  339. with self.assertRaisesRegex(ValueError, 'end_col MUST not be negative'):
  340. loc.validate()
  341. def test_invalid_start_after_end_line(self):
  342. loc = presubmit_support._PresubmitResultLocation(file_path='foo',
  343. start_line=6,
  344. end_line=5)
  345. with self.assertRaisesRegex(ValueError, 'must not be after'):
  346. loc.validate()
  347. def test_invalid_start_after_end_col(self):
  348. loc = presubmit_support._PresubmitResultLocation(file_path='foo',
  349. start_line=5,
  350. start_col=11,
  351. end_line=5,
  352. end_col=10)
  353. with self.assertRaisesRegex(ValueError, 'must not be after'):
  354. loc.validate()
  355. class PresubmitResultTest(unittest.TestCase):
  356. def test_handle_message_only(self):
  357. result = presubmit_support._PresubmitResult('Simple message')
  358. out = io.StringIO()
  359. result.handle(out)
  360. self.assertEqual(out.getvalue(), 'Simple message\n')
  361. def test_handle_full_args(self):
  362. result = presubmit_support._PresubmitResult(
  363. 'This is a message',
  364. items=['item1', 'item2'],
  365. long_text='Long text here.',
  366. locations=[
  367. presubmit_support._PresubmitResultLocation(
  368. file_path=presubmit_support._PresubmitResultLocation.
  369. COMMIT_MSG_PATH),
  370. presubmit_support._PresubmitResultLocation(
  371. file_path='file1',
  372. start_line=10,
  373. end_line=10,
  374. ),
  375. presubmit_support._PresubmitResultLocation(
  376. file_path='file2',
  377. start_line=11,
  378. end_line=15,
  379. ),
  380. presubmit_support._PresubmitResultLocation(
  381. file_path='file3',
  382. start_line=5,
  383. start_col=0,
  384. end_line=8,
  385. end_col=5,
  386. )
  387. ])
  388. out = io.StringIO()
  389. result.handle(out)
  390. expected = ('This is a message\n'
  391. ' item1\n'
  392. ' item2\n'
  393. 'Found in:\n'
  394. ' - Commit Message\n'
  395. ' - file1 [Ln 10]\n'
  396. ' - file2 [Ln 11 - 15]\n'
  397. ' - file3 [Ln 5, Col 0 - Ln 8, Col 5]\n'
  398. '\n***************\n'
  399. 'Long text here.\n'
  400. '***************\n')
  401. self.assertEqual(out.getvalue(), expected)
  402. def test_json_format(self):
  403. loc1 = presubmit_support._PresubmitResultLocation(file_path='file1',
  404. start_line=1,
  405. end_line=1)
  406. loc2 = presubmit_support._PresubmitResultLocation(file_path='file2',
  407. start_line=5,
  408. start_col=2,
  409. end_line=6,
  410. end_col=10)
  411. result = presubmit_support._PresubmitResult('This is a message',
  412. items=['item1', 'item2'],
  413. long_text='Long text here.',
  414. locations=[loc1, loc2])
  415. expected = {
  416. 'message':
  417. 'This is a message',
  418. 'items': ['item1', 'item2'],
  419. 'locations': [
  420. {
  421. 'file_path': 'file1',
  422. 'start_line': 1,
  423. 'start_col': 0,
  424. 'end_line': 1,
  425. 'end_col': 0
  426. },
  427. {
  428. 'file_path': 'file2',
  429. 'start_line': 5,
  430. 'start_col': 2,
  431. 'end_line': 6,
  432. 'end_col': 10
  433. },
  434. ],
  435. 'long_text':
  436. 'Long text here.',
  437. 'fatal':
  438. False,
  439. }
  440. self.assertEqual(result.json_format(), expected)
  441. if __name__ == "__main__":
  442. unittest.main()