test_attrs.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. # test_attrs.py -- tests for gitattributes
  2. # Copyright (C) 2019-2020 Collabora Ltd
  3. # Copyright (C) 2019-2020 Andrej Shadura <andrew.shadura@collabora.co.uk>
  4. #
  5. # SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
  6. # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
  7. # General Public License as public by the Free Software Foundation; version 2.0
  8. # or (at your option) any later version. You can redistribute it and/or
  9. # modify it under the terms of either of these two licenses.
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. #
  17. # You should have received a copy of the licenses; if not, see
  18. # <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
  19. # and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
  20. # License, Version 2.0.
  21. #
  22. """Tests for gitattributes parsing and matching."""
  23. import os
  24. import tempfile
  25. from io import BytesIO
  26. from dulwich.attrs import (
  27. GitAttributes,
  28. Pattern,
  29. _parse_attr,
  30. match_path,
  31. parse_git_attributes,
  32. parse_gitattributes_file,
  33. read_gitattributes,
  34. )
  35. from . import TestCase
  36. class ParseAttrTests(TestCase):
  37. """Test the _parse_attr function."""
  38. def test_parse_set_attr(self):
  39. """Test parsing a set attribute."""
  40. name, value = _parse_attr(b"text")
  41. self.assertEqual(name, b"text")
  42. self.assertEqual(value, True)
  43. def test_parse_unset_attr(self):
  44. """Test parsing an unset attribute."""
  45. name, value = _parse_attr(b"-text")
  46. self.assertEqual(name, b"text")
  47. self.assertEqual(value, False)
  48. def test_parse_unspecified_attr(self):
  49. """Test parsing an unspecified attribute."""
  50. name, value = _parse_attr(b"!text")
  51. self.assertEqual(name, b"text")
  52. self.assertEqual(value, None)
  53. def test_parse_value_attr(self):
  54. """Test parsing an attribute with a value."""
  55. name, value = _parse_attr(b"diff=python")
  56. self.assertEqual(name, b"diff")
  57. self.assertEqual(value, b"python")
  58. def test_parse_value_with_equals(self):
  59. """Test parsing an attribute value containing equals."""
  60. name, value = _parse_attr(b"filter=foo=bar")
  61. self.assertEqual(name, b"filter")
  62. self.assertEqual(value, b"foo=bar")
  63. class ParseGitAttributesTests(TestCase):
  64. """Test the parse_git_attributes function."""
  65. def test_parse_empty(self):
  66. """Test parsing empty file."""
  67. attrs = list(parse_git_attributes(BytesIO(b"")))
  68. self.assertEqual(attrs, [])
  69. def test_parse_comments(self):
  70. """Test parsing file with comments."""
  71. content = b"""# This is a comment
  72. # Another comment
  73. """
  74. attrs = list(parse_git_attributes(BytesIO(content)))
  75. self.assertEqual(attrs, [])
  76. def test_parse_single_pattern(self):
  77. """Test parsing single pattern."""
  78. content = b"*.txt text"
  79. attrs = list(parse_git_attributes(BytesIO(content)))
  80. self.assertEqual(len(attrs), 1)
  81. pattern, attributes = attrs[0]
  82. self.assertEqual(pattern, b"*.txt")
  83. self.assertEqual(attributes, {b"text": True})
  84. def test_parse_multiple_attributes(self):
  85. """Test parsing pattern with multiple attributes."""
  86. content = b"*.jpg -text -diff binary"
  87. attrs = list(parse_git_attributes(BytesIO(content)))
  88. self.assertEqual(len(attrs), 1)
  89. pattern, attributes = attrs[0]
  90. self.assertEqual(pattern, b"*.jpg")
  91. self.assertEqual(attributes, {b"text": False, b"diff": False, b"binary": True})
  92. def test_parse_attributes_with_values(self):
  93. """Test parsing attributes with values."""
  94. content = b"*.c filter=indent diff=cpp text"
  95. attrs = list(parse_git_attributes(BytesIO(content)))
  96. self.assertEqual(len(attrs), 1)
  97. pattern, attributes = attrs[0]
  98. self.assertEqual(pattern, b"*.c")
  99. self.assertEqual(
  100. attributes, {b"filter": b"indent", b"diff": b"cpp", b"text": True}
  101. )
  102. def test_parse_multiple_patterns(self):
  103. """Test parsing multiple patterns."""
  104. content = b"""*.txt text
  105. *.jpg -text binary
  106. *.py diff=python
  107. """
  108. attrs = list(parse_git_attributes(BytesIO(content)))
  109. self.assertEqual(len(attrs), 3)
  110. # First pattern
  111. pattern, attributes = attrs[0]
  112. self.assertEqual(pattern, b"*.txt")
  113. self.assertEqual(attributes, {b"text": True})
  114. # Second pattern
  115. pattern, attributes = attrs[1]
  116. self.assertEqual(pattern, b"*.jpg")
  117. self.assertEqual(attributes, {b"text": False, b"binary": True})
  118. # Third pattern
  119. pattern, attributes = attrs[2]
  120. self.assertEqual(pattern, b"*.py")
  121. self.assertEqual(attributes, {b"diff": b"python"})
  122. def test_parse_git_lfs_example(self):
  123. """Test parsing Git LFS example from docstring."""
  124. content = b"""*.tar.* filter=lfs diff=lfs merge=lfs -text
  125. # store signatures in Git
  126. *.tar.*.asc -filter -diff merge=binary -text
  127. # store .dsc verbatim
  128. *.dsc -filter !diff merge=binary !text
  129. """
  130. attrs = list(parse_git_attributes(BytesIO(content)))
  131. self.assertEqual(len(attrs), 3)
  132. # LFS pattern
  133. pattern, attributes = attrs[0]
  134. self.assertEqual(pattern, b"*.tar.*")
  135. self.assertEqual(
  136. attributes,
  137. {b"filter": b"lfs", b"diff": b"lfs", b"merge": b"lfs", b"text": False},
  138. )
  139. # Signatures pattern
  140. pattern, attributes = attrs[1]
  141. self.assertEqual(pattern, b"*.tar.*.asc")
  142. self.assertEqual(
  143. attributes,
  144. {b"filter": False, b"diff": False, b"merge": b"binary", b"text": False},
  145. )
  146. # .dsc pattern
  147. pattern, attributes = attrs[2]
  148. self.assertEqual(pattern, b"*.dsc")
  149. self.assertEqual(
  150. attributes,
  151. {b"filter": False, b"diff": None, b"merge": b"binary", b"text": None},
  152. )
  153. class PatternTests(TestCase):
  154. """Test the Pattern class."""
  155. def test_exact_match(self):
  156. """Test exact filename matching without path."""
  157. pattern = Pattern(b"README.txt")
  158. self.assertTrue(pattern.match(b"README.txt"))
  159. self.assertFalse(pattern.match(b"readme.txt"))
  160. # Patterns without slashes match at any level
  161. self.assertTrue(pattern.match(b"src/README.txt"))
  162. def test_wildcard_extension(self):
  163. """Test wildcard extension matching."""
  164. pattern = Pattern(b"*.txt")
  165. self.assertTrue(pattern.match(b"file.txt"))
  166. self.assertTrue(pattern.match(b"README.txt"))
  167. self.assertTrue(pattern.match(b"src/doc.txt"))
  168. self.assertFalse(pattern.match(b"file.txt.bak"))
  169. self.assertFalse(pattern.match(b"file.md"))
  170. def test_wildcard_in_name(self):
  171. """Test wildcard in filename."""
  172. pattern = Pattern(b"test_*.py")
  173. self.assertTrue(pattern.match(b"test_foo.py"))
  174. self.assertTrue(pattern.match(b"test_bar.py"))
  175. self.assertTrue(pattern.match(b"src/test_baz.py"))
  176. self.assertFalse(pattern.match(b"test.py"))
  177. self.assertFalse(pattern.match(b"tests.py"))
  178. def test_question_mark(self):
  179. """Test question mark matching."""
  180. pattern = Pattern(b"file?.txt")
  181. self.assertTrue(pattern.match(b"file1.txt"))
  182. self.assertTrue(pattern.match(b"fileA.txt"))
  183. self.assertFalse(pattern.match(b"file.txt"))
  184. self.assertFalse(pattern.match(b"file10.txt"))
  185. def test_character_class(self):
  186. """Test character class matching."""
  187. pattern = Pattern(b"file[0-9].txt")
  188. self.assertTrue(pattern.match(b"file0.txt"))
  189. self.assertTrue(pattern.match(b"file5.txt"))
  190. self.assertTrue(pattern.match(b"file9.txt"))
  191. self.assertFalse(pattern.match(b"fileA.txt"))
  192. self.assertFalse(pattern.match(b"file10.txt"))
  193. def test_negated_character_class(self):
  194. """Test negated character class."""
  195. pattern = Pattern(b"file[!0-9].txt")
  196. self.assertTrue(pattern.match(b"fileA.txt"))
  197. self.assertTrue(pattern.match(b"file_.txt"))
  198. self.assertFalse(pattern.match(b"file0.txt"))
  199. self.assertFalse(pattern.match(b"file5.txt"))
  200. def test_directory_pattern(self):
  201. """Test pattern with directory."""
  202. pattern = Pattern(b"src/*.py")
  203. self.assertTrue(pattern.match(b"src/foo.py"))
  204. self.assertTrue(pattern.match(b"src/bar.py"))
  205. self.assertFalse(pattern.match(b"foo.py"))
  206. self.assertFalse(pattern.match(b"src/sub/foo.py"))
  207. self.assertFalse(pattern.match(b"other/foo.py"))
  208. def test_double_asterisk(self):
  209. """Test double asterisk matching."""
  210. pattern = Pattern(b"**/foo.txt")
  211. self.assertTrue(pattern.match(b"foo.txt"))
  212. self.assertTrue(pattern.match(b"src/foo.txt"))
  213. self.assertTrue(pattern.match(b"src/sub/foo.txt"))
  214. self.assertTrue(pattern.match(b"a/b/c/foo.txt"))
  215. def test_double_asterisk_middle(self):
  216. """Test double asterisk in middle."""
  217. pattern = Pattern(b"src/**/foo.txt")
  218. self.assertTrue(pattern.match(b"src/foo.txt"))
  219. self.assertTrue(pattern.match(b"src/sub/foo.txt"))
  220. self.assertTrue(pattern.match(b"src/a/b/foo.txt"))
  221. self.assertFalse(pattern.match(b"foo.txt"))
  222. self.assertFalse(pattern.match(b"other/foo.txt"))
  223. def test_leading_slash(self):
  224. """Test pattern with leading slash."""
  225. pattern = Pattern(b"/README.txt")
  226. self.assertTrue(pattern.match(b"README.txt"))
  227. self.assertTrue(pattern.match(b"/README.txt"))
  228. self.assertFalse(pattern.match(b"src/README.txt"))
  229. class MatchPathTests(TestCase):
  230. """Test the match_path function."""
  231. def test_no_matches(self):
  232. """Test when no patterns match."""
  233. patterns = [
  234. (Pattern(b"*.txt"), {b"text": True}),
  235. (Pattern(b"*.jpg"), {b"binary": True}),
  236. ]
  237. attrs = match_path(patterns, b"file.py")
  238. self.assertEqual(attrs, {})
  239. def test_single_match(self):
  240. """Test single pattern match."""
  241. patterns = [
  242. (Pattern(b"*.txt"), {b"text": True}),
  243. (Pattern(b"*.jpg"), {b"binary": True}),
  244. ]
  245. attrs = match_path(patterns, b"README.txt")
  246. self.assertEqual(attrs, {b"text": True})
  247. def test_multiple_matches_override(self):
  248. """Test that later patterns override earlier ones."""
  249. patterns = [
  250. (Pattern(b"*"), {b"text": True}),
  251. (Pattern(b"*.jpg"), {b"text": False, b"binary": True}),
  252. ]
  253. attrs = match_path(patterns, b"image.jpg")
  254. self.assertEqual(attrs, {b"text": False, b"binary": True})
  255. def test_unspecified_removes_attribute(self):
  256. """Test that unspecified (None) removes attributes."""
  257. patterns = [
  258. (Pattern(b"*"), {b"text": True, b"diff": True}),
  259. (Pattern(b"*.bin"), {b"text": None, b"binary": True}),
  260. ]
  261. attrs = match_path(patterns, b"file.bin")
  262. self.assertEqual(attrs, {b"diff": True, b"binary": True})
  263. # 'text' should be removed
  264. self.assertNotIn(b"text", attrs)
  265. class FileOperationsTests(TestCase):
  266. """Test file operations."""
  267. def test_parse_gitattributes_file(self):
  268. """Test parsing a gitattributes file."""
  269. with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
  270. f.write(b"*.txt text\n")
  271. f.write(b"*.jpg -text binary\n")
  272. temp_path = f.name
  273. try:
  274. patterns = parse_gitattributes_file(temp_path)
  275. self.assertEqual(len(patterns), 2)
  276. # Check first pattern
  277. pattern, attrs = patterns[0]
  278. self.assertEqual(pattern.pattern, b"*.txt")
  279. self.assertEqual(attrs, {b"text": True})
  280. # Check second pattern
  281. pattern, attrs = patterns[1]
  282. self.assertEqual(pattern.pattern, b"*.jpg")
  283. self.assertEqual(attrs, {b"text": False, b"binary": True})
  284. finally:
  285. os.unlink(temp_path)
  286. def test_read_gitattributes(self):
  287. """Test reading gitattributes from a directory."""
  288. with tempfile.TemporaryDirectory() as tmpdir:
  289. # Create .gitattributes file
  290. attrs_path = os.path.join(tmpdir, ".gitattributes")
  291. with open(attrs_path, "wb") as f:
  292. f.write(b"*.py diff=python\n")
  293. patterns = read_gitattributes(tmpdir)
  294. self.assertEqual(len(patterns), 1)
  295. pattern, attrs = patterns[0]
  296. self.assertEqual(pattern.pattern, b"*.py")
  297. self.assertEqual(attrs, {b"diff": b"python"})
  298. def test_read_gitattributes_missing(self):
  299. """Test reading gitattributes when file doesn't exist."""
  300. with tempfile.TemporaryDirectory() as tmpdir:
  301. patterns = read_gitattributes(tmpdir)
  302. self.assertEqual(patterns, [])
  303. class GitAttributesTests(TestCase):
  304. """Test the GitAttributes class."""
  305. def test_empty_gitattributes(self):
  306. """Test GitAttributes with no patterns."""
  307. ga = GitAttributes()
  308. attrs = ga.match_path(b"file.txt")
  309. self.assertEqual(attrs, {})
  310. self.assertEqual(len(ga), 0)
  311. def test_gitattributes_with_patterns(self):
  312. """Test GitAttributes with patterns."""
  313. patterns = [
  314. (Pattern(b"*.txt"), {b"text": True}),
  315. (Pattern(b"*.jpg"), {b"binary": True, b"text": False}),
  316. ]
  317. ga = GitAttributes(patterns)
  318. # Test matching .txt file
  319. attrs = ga.match_path(b"README.txt")
  320. self.assertEqual(attrs, {b"text": True})
  321. # Test matching .jpg file
  322. attrs = ga.match_path(b"image.jpg")
  323. self.assertEqual(attrs, {b"binary": True, b"text": False})
  324. # Test non-matching file
  325. attrs = ga.match_path(b"script.py")
  326. self.assertEqual(attrs, {})
  327. self.assertEqual(len(ga), 2)
  328. def test_add_patterns(self):
  329. """Test adding patterns to GitAttributes."""
  330. ga = GitAttributes()
  331. self.assertEqual(len(ga), 0)
  332. # Add patterns
  333. ga.add_patterns(
  334. [
  335. (Pattern(b"*.py"), {b"diff": b"python"}),
  336. (Pattern(b"*.md"), {b"text": True}),
  337. ]
  338. )
  339. self.assertEqual(len(ga), 2)
  340. attrs = ga.match_path(b"test.py")
  341. self.assertEqual(attrs, {b"diff": b"python"})
  342. def test_iteration(self):
  343. """Test iterating over patterns."""
  344. patterns = [
  345. (Pattern(b"*.txt"), {b"text": True}),
  346. (Pattern(b"*.jpg"), {b"binary": True}),
  347. ]
  348. ga = GitAttributes(patterns)
  349. collected = list(ga)
  350. self.assertEqual(len(collected), 2)
  351. self.assertEqual(collected[0][0].pattern, b"*.txt")
  352. self.assertEqual(collected[1][0].pattern, b"*.jpg")
  353. def test_from_file(self):
  354. """Test creating GitAttributes from file."""
  355. with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
  356. f.write(b"*.txt text\n")
  357. f.write(b"*.bin -text binary\n")
  358. temp_path = f.name
  359. try:
  360. ga = GitAttributes.from_file(temp_path)
  361. self.assertEqual(len(ga), 2)
  362. attrs = ga.match_path(b"file.txt")
  363. self.assertEqual(attrs, {b"text": True})
  364. attrs = ga.match_path(b"file.bin")
  365. self.assertEqual(attrs, {b"text": False, b"binary": True})
  366. finally:
  367. os.unlink(temp_path)
  368. def test_from_path(self):
  369. """Test creating GitAttributes from directory path."""
  370. with tempfile.TemporaryDirectory() as tmpdir:
  371. # Create .gitattributes file
  372. attrs_path = os.path.join(tmpdir, ".gitattributes")
  373. with open(attrs_path, "wb") as f:
  374. f.write(b"*.py diff=python\n")
  375. f.write(b"*.rs diff=rust\n")
  376. ga = GitAttributes.from_path(tmpdir)
  377. self.assertEqual(len(ga), 2)
  378. attrs = ga.match_path(b"main.py")
  379. self.assertEqual(attrs, {b"diff": b"python"})
  380. attrs = ga.match_path(b"lib.rs")
  381. self.assertEqual(attrs, {b"diff": b"rust"})