presubmit_diff_test.py 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. #!/usr/bin/env vpython3
  2. # Copyright (c) 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. """Unit tests for presubmit_diff.py."""
  6. import os
  7. import sys
  8. import tempfile
  9. import unittest
  10. from typing import Dict, List
  11. from unittest import mock
  12. sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
  13. import gclient_utils
  14. import presubmit_diff
  15. class PresubmitDiffTest(unittest.TestCase):
  16. def setUp(self):
  17. # State of the local directory.
  18. self.root = tempfile.mkdtemp()
  19. os.makedirs(os.path.join(self.root, "nested"))
  20. # On Windows, writing "\n" in text mode becomes "\r\n". Write in binary
  21. # so that doesn't happen, otherwise tests will fail.
  22. with open(os.path.join(self.root, "unchanged.txt"), "wb") as f:
  23. f.write("unchanged\n".encode("utf-8"))
  24. with open(os.path.join(self.root, "added.txt"), "wb") as f:
  25. f.write("added\n".encode("utf-8"))
  26. with open(os.path.join(self.root, "modified.txt"), "wb") as f:
  27. f.write("modified... foo\n".encode("utf-8"))
  28. with open(os.path.join(self.root, "nested/modified.txt"), "wb") as f:
  29. f.write("goodbye\n".encode("utf-8"))
  30. # State of the remote repository.
  31. fetch_data = {
  32. "unchanged.txt": "unchanged\n".encode("utf-8"),
  33. "deleted.txt": "deleted\n".encode("utf-8"),
  34. "modified.txt": "modified... bar\n".encode("utf-8"),
  35. "nested/modified.txt": "hello\n".encode("utf-8"),
  36. # Intenionally invalid start byte for utf-8.
  37. "deleted_binary": b"\xff\x00",
  38. }
  39. def fetch_side_effect(host, repo, ref, file):
  40. return fetch_data.get(file, b"")
  41. fetch_content_mock = mock.patch("presubmit_diff.fetch_content",
  42. side_effect=fetch_side_effect)
  43. fetch_content_mock.start()
  44. self.addCleanup(mock.patch.stopall)
  45. def tearDown(self):
  46. gclient_utils.rmtree(self.root)
  47. def _test_create_diffs(self, files: List[str], expected: Dict[str, str]):
  48. actual = presubmit_diff.create_diffs("host", "repo", "ref", self.root,
  49. files)
  50. self.assertEqual(actual.keys(), expected.keys())
  51. # Manually check each line in the diffs except the "index" line because
  52. # hashes can differ in length.
  53. for file, diff in actual.items():
  54. expected_lines = expected[file].splitlines()
  55. for idx, line in enumerate(diff.splitlines()):
  56. if line.startswith("index "):
  57. continue
  58. self.assertEqual(line, expected_lines[idx])
  59. def test_create_diffs_with_nonexistent_file_raises_error(self):
  60. self.assertRaises(
  61. RuntimeError,
  62. presubmit_diff.create_diffs,
  63. "host",
  64. "repo",
  65. "ref",
  66. self.root,
  67. ["doesnotexist.txt"],
  68. )
  69. def test_create_diffs_with_unchanged_file(self):
  70. self._test_create_diffs(
  71. ["unchanged.txt"],
  72. {"unchanged.txt": ""},
  73. )
  74. @mock.patch('subprocess2.capture', return_value="".encode("utf-8"))
  75. def test_create_diffs_executes_git_diff_with_unified(self, capture):
  76. create_diffs = presubmit_diff.create_diffs
  77. # None => no -U
  78. create_diffs("host", "repo", "ref", self.root, ["unchanged.txt"], None)
  79. capture.assert_called_with(
  80. ["git", "diff", "--no-index", "--", mock.ANY, mock.ANY])
  81. # 0 => -U0
  82. create_diffs("host", "repo", "ref", self.root, ["unchanged.txt"], 0)
  83. capture.assert_called_with(
  84. ["git", "diff", "--no-index", "-U0", "--", mock.ANY, mock.ANY])
  85. # 3 => -U3
  86. create_diffs("host", "repo", "ref", self.root, ["unchanged.txt"], 3)
  87. capture.assert_called_with(
  88. ["git", "diff", "--no-index", "-U3", "--", mock.ANY, mock.ANY])
  89. def test_create_diffs_with_added_file(self):
  90. expected_diff = """diff --git a/added.txt b/added.txt
  91. new file mode 100644
  92. index 00000000..d5f7fc3f
  93. --- /dev/null
  94. +++ b/added.txt
  95. @@ -0,0 +1 @@
  96. +added
  97. """
  98. self._test_create_diffs(
  99. ["added.txt"],
  100. {"added.txt": expected_diff},
  101. )
  102. def test_create_diffs_with_deleted_file(self):
  103. expected_diff = """diff --git a/deleted.txt b/deleted.txt
  104. deleted file mode 100644
  105. index 71779d2c..00000000
  106. --- a/deleted.txt
  107. +++ /dev/null
  108. @@ -1 +0,0 @@
  109. -deleted
  110. """
  111. self._test_create_diffs(
  112. ["deleted.txt"],
  113. {"deleted.txt": expected_diff},
  114. )
  115. def test_create_diffs_with_binary_file(self):
  116. expected_diff = """diff --git a/deleted_binary b/deleted_binary
  117. deleted file mode 100644
  118. index ce542efaa..00000000
  119. Binary files a/deleted_binary and /dev/null differ
  120. """
  121. self._test_create_diffs(
  122. ["deleted_binary"],
  123. {"deleted_binary": expected_diff},
  124. )
  125. # pylint: disable=line-too-long
  126. def test_create_diffs_with_modified_files(self):
  127. expected_diff = """diff --git a/modified.txt b/modified.txt
  128. index a7dd0b00..12d68703 100644
  129. --- a/modified.txt
  130. +++ b/modified.txt
  131. @@ -1 +1 @@
  132. -modified... bar
  133. +modified... foo
  134. """
  135. expected_nested_diff = """diff --git a/nested/modified.txt b/nested/modified.txt
  136. index ce013625..dd7e1c6f 100644
  137. --- a/nested/modified.txt
  138. +++ b/nested/modified.txt
  139. @@ -1 +1 @@
  140. -hello
  141. +goodbye
  142. """
  143. self._test_create_diffs(
  144. ["modified.txt", "nested/modified.txt"],
  145. {
  146. "modified.txt": expected_diff,
  147. "nested/modified.txt": expected_nested_diff,
  148. },
  149. )
  150. # Test cases for _process_diff.
  151. def test_process_diff_with_no_changes(self):
  152. self.assertEqual(
  153. presubmit_diff._process_diff(
  154. "",
  155. "/path/to/src",
  156. "/path/to/dst",
  157. ),
  158. "",
  159. )
  160. @mock.patch("platform.system", return_value="Linux")
  161. @mock.patch("os.sep", new="/")
  162. def test_process_diff_handles_unix_paths(self, sys_mock):
  163. diff = """diff --git a/path/to/src/file.txt b/path/to/dst/file.txt
  164. index ce013625..dd7e1c6f 100644
  165. --- a/path/to/file.txt
  166. +++ b/path/to/file.txt
  167. @@ -1 +1 @@
  168. -random
  169. +content
  170. """
  171. expected = """diff --git a/file.txt b/file.txt
  172. index ce013625..dd7e1c6f 100644
  173. --- a/path/to/file.txt
  174. +++ b/path/to/file.txt
  175. @@ -1 +1 @@
  176. -random
  177. +content
  178. """
  179. self.assertEqual(
  180. presubmit_diff._process_diff(
  181. diff,
  182. "/path/to/src",
  183. "/path/to/dst",
  184. ),
  185. expected,
  186. )
  187. # Trailing slashes are handled.
  188. self.assertEqual(
  189. presubmit_diff._process_diff(
  190. diff,
  191. "/path/to/src/",
  192. "/path/to/dst/",
  193. ),
  194. expected,
  195. )
  196. @mock.patch("platform.system", return_value="Windows")
  197. @mock.patch("os.sep", new="\\")
  198. def test_process_diff_handles_windows_paths(self, sys_mock):
  199. diff = """diff --git "a/C:\\\\path\\\\to\\\\src\\\\file.txt" "b/C:\\\\path\\\\to\\\\dst\\\\file.txt"
  200. index ce013625..dd7e1c6f 100644
  201. --- "a/C:\\\\path\\\\to\\\\src\\\\file.txt
  202. +++ "b/C:\\\\path\\\\to\\\\dst\\\\file.txt"
  203. @@ -1 +1 @@
  204. -random
  205. +content
  206. """
  207. expected = """diff --git a/file.txt b/file.txt
  208. index ce013625..dd7e1c6f 100644
  209. --- a/file.txt
  210. +++ b/file.txt
  211. @@ -1 +1 @@
  212. -random
  213. +content
  214. """
  215. self.assertEqual(
  216. expected,
  217. presubmit_diff._process_diff(diff, "C:\\path\\to\\src",
  218. "C:\\path\\to\\dst"),
  219. )
  220. # Trailing slashes are handled.
  221. self.assertEqual(
  222. expected,
  223. presubmit_diff._process_diff(diff, "C:\\path\\to\\src\\",
  224. "C:\\path\\to\\dst\\"),
  225. )
  226. @mock.patch("platform.system", return_value="Linux")
  227. def test_process_diff_without_chunk_header(self, sys_mock):
  228. diff = """diff --git a/path/to/src/file.txt b/path/to/dst/file.txt
  229. old mode 100644
  230. new mode 100755
  231. """
  232. expected = """diff --git a/file.txt b/file.txt
  233. old mode 100644
  234. new mode 100755
  235. """
  236. self.assertEqual(
  237. presubmit_diff._process_diff(
  238. diff,
  239. "/path/to/src",
  240. "/path/to/dst",
  241. ),
  242. expected,
  243. )
  244. if __name__ == "__main__":
  245. unittest.main()