123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458 |
- # test_attrs.py -- tests for gitattributes
- # Copyright (C) 2019-2020 Collabora Ltd
- # Copyright (C) 2019-2020 Andrej Shadura <andrew.shadura@collabora.co.uk>
- #
- # SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
- # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
- # General Public License as public by the Free Software Foundation; version 2.0
- # or (at your option) any later version. You can redistribute it and/or
- # modify it under the terms of either of these two licenses.
- #
- # Unless required by applicable law or agreed to in writing, software
- # distributed under the License is distributed on an "AS IS" BASIS,
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- # See the License for the specific language governing permissions and
- # limitations under the License.
- #
- # You should have received a copy of the licenses; if not, see
- # <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
- # and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
- # License, Version 2.0.
- #
- """Tests for gitattributes parsing and matching."""
- import os
- import tempfile
- from io import BytesIO
- from dulwich.attrs import (
- GitAttributes,
- Pattern,
- _parse_attr,
- match_path,
- parse_git_attributes,
- parse_gitattributes_file,
- read_gitattributes,
- )
- from . import TestCase
- class ParseAttrTests(TestCase):
- """Test the _parse_attr function."""
- def test_parse_set_attr(self):
- """Test parsing a set attribute."""
- name, value = _parse_attr(b"text")
- self.assertEqual(name, b"text")
- self.assertEqual(value, True)
- def test_parse_unset_attr(self):
- """Test parsing an unset attribute."""
- name, value = _parse_attr(b"-text")
- self.assertEqual(name, b"text")
- self.assertEqual(value, False)
- def test_parse_unspecified_attr(self):
- """Test parsing an unspecified attribute."""
- name, value = _parse_attr(b"!text")
- self.assertEqual(name, b"text")
- self.assertEqual(value, None)
- def test_parse_value_attr(self):
- """Test parsing an attribute with a value."""
- name, value = _parse_attr(b"diff=python")
- self.assertEqual(name, b"diff")
- self.assertEqual(value, b"python")
- def test_parse_value_with_equals(self):
- """Test parsing an attribute value containing equals."""
- name, value = _parse_attr(b"filter=foo=bar")
- self.assertEqual(name, b"filter")
- self.assertEqual(value, b"foo=bar")
- class ParseGitAttributesTests(TestCase):
- """Test the parse_git_attributes function."""
- def test_parse_empty(self):
- """Test parsing empty file."""
- attrs = list(parse_git_attributes(BytesIO(b"")))
- self.assertEqual(attrs, [])
- def test_parse_comments(self):
- """Test parsing file with comments."""
- content = b"""# This is a comment
- # Another comment
- """
- attrs = list(parse_git_attributes(BytesIO(content)))
- self.assertEqual(attrs, [])
- def test_parse_single_pattern(self):
- """Test parsing single pattern."""
- content = b"*.txt text"
- attrs = list(parse_git_attributes(BytesIO(content)))
- self.assertEqual(len(attrs), 1)
- pattern, attributes = attrs[0]
- self.assertEqual(pattern, b"*.txt")
- self.assertEqual(attributes, {b"text": True})
- def test_parse_multiple_attributes(self):
- """Test parsing pattern with multiple attributes."""
- content = b"*.jpg -text -diff binary"
- attrs = list(parse_git_attributes(BytesIO(content)))
- self.assertEqual(len(attrs), 1)
- pattern, attributes = attrs[0]
- self.assertEqual(pattern, b"*.jpg")
- self.assertEqual(attributes, {b"text": False, b"diff": False, b"binary": True})
- def test_parse_attributes_with_values(self):
- """Test parsing attributes with values."""
- content = b"*.c filter=indent diff=cpp text"
- attrs = list(parse_git_attributes(BytesIO(content)))
- self.assertEqual(len(attrs), 1)
- pattern, attributes = attrs[0]
- self.assertEqual(pattern, b"*.c")
- self.assertEqual(
- attributes, {b"filter": b"indent", b"diff": b"cpp", b"text": True}
- )
- def test_parse_multiple_patterns(self):
- """Test parsing multiple patterns."""
- content = b"""*.txt text
- *.jpg -text binary
- *.py diff=python
- """
- attrs = list(parse_git_attributes(BytesIO(content)))
- self.assertEqual(len(attrs), 3)
- # First pattern
- pattern, attributes = attrs[0]
- self.assertEqual(pattern, b"*.txt")
- self.assertEqual(attributes, {b"text": True})
- # Second pattern
- pattern, attributes = attrs[1]
- self.assertEqual(pattern, b"*.jpg")
- self.assertEqual(attributes, {b"text": False, b"binary": True})
- # Third pattern
- pattern, attributes = attrs[2]
- self.assertEqual(pattern, b"*.py")
- self.assertEqual(attributes, {b"diff": b"python"})
- def test_parse_git_lfs_example(self):
- """Test parsing Git LFS example from docstring."""
- content = b"""*.tar.* filter=lfs diff=lfs merge=lfs -text
- # store signatures in Git
- *.tar.*.asc -filter -diff merge=binary -text
- # store .dsc verbatim
- *.dsc -filter !diff merge=binary !text
- """
- attrs = list(parse_git_attributes(BytesIO(content)))
- self.assertEqual(len(attrs), 3)
- # LFS pattern
- pattern, attributes = attrs[0]
- self.assertEqual(pattern, b"*.tar.*")
- self.assertEqual(
- attributes,
- {b"filter": b"lfs", b"diff": b"lfs", b"merge": b"lfs", b"text": False},
- )
- # Signatures pattern
- pattern, attributes = attrs[1]
- self.assertEqual(pattern, b"*.tar.*.asc")
- self.assertEqual(
- attributes,
- {b"filter": False, b"diff": False, b"merge": b"binary", b"text": False},
- )
- # .dsc pattern
- pattern, attributes = attrs[2]
- self.assertEqual(pattern, b"*.dsc")
- self.assertEqual(
- attributes,
- {b"filter": False, b"diff": None, b"merge": b"binary", b"text": None},
- )
- class PatternTests(TestCase):
- """Test the Pattern class."""
- def test_exact_match(self):
- """Test exact filename matching without path."""
- pattern = Pattern(b"README.txt")
- self.assertTrue(pattern.match(b"README.txt"))
- self.assertFalse(pattern.match(b"readme.txt"))
- # Patterns without slashes match at any level
- self.assertTrue(pattern.match(b"src/README.txt"))
- def test_wildcard_extension(self):
- """Test wildcard extension matching."""
- pattern = Pattern(b"*.txt")
- self.assertTrue(pattern.match(b"file.txt"))
- self.assertTrue(pattern.match(b"README.txt"))
- self.assertTrue(pattern.match(b"src/doc.txt"))
- self.assertFalse(pattern.match(b"file.txt.bak"))
- self.assertFalse(pattern.match(b"file.md"))
- def test_wildcard_in_name(self):
- """Test wildcard in filename."""
- pattern = Pattern(b"test_*.py")
- self.assertTrue(pattern.match(b"test_foo.py"))
- self.assertTrue(pattern.match(b"test_bar.py"))
- self.assertTrue(pattern.match(b"src/test_baz.py"))
- self.assertFalse(pattern.match(b"test.py"))
- self.assertFalse(pattern.match(b"tests.py"))
- def test_question_mark(self):
- """Test question mark matching."""
- pattern = Pattern(b"file?.txt")
- self.assertTrue(pattern.match(b"file1.txt"))
- self.assertTrue(pattern.match(b"fileA.txt"))
- self.assertFalse(pattern.match(b"file.txt"))
- self.assertFalse(pattern.match(b"file10.txt"))
- def test_character_class(self):
- """Test character class matching."""
- pattern = Pattern(b"file[0-9].txt")
- self.assertTrue(pattern.match(b"file0.txt"))
- self.assertTrue(pattern.match(b"file5.txt"))
- self.assertTrue(pattern.match(b"file9.txt"))
- self.assertFalse(pattern.match(b"fileA.txt"))
- self.assertFalse(pattern.match(b"file10.txt"))
- def test_negated_character_class(self):
- """Test negated character class."""
- pattern = Pattern(b"file[!0-9].txt")
- self.assertTrue(pattern.match(b"fileA.txt"))
- self.assertTrue(pattern.match(b"file_.txt"))
- self.assertFalse(pattern.match(b"file0.txt"))
- self.assertFalse(pattern.match(b"file5.txt"))
- def test_directory_pattern(self):
- """Test pattern with directory."""
- pattern = Pattern(b"src/*.py")
- self.assertTrue(pattern.match(b"src/foo.py"))
- self.assertTrue(pattern.match(b"src/bar.py"))
- self.assertFalse(pattern.match(b"foo.py"))
- self.assertFalse(pattern.match(b"src/sub/foo.py"))
- self.assertFalse(pattern.match(b"other/foo.py"))
- def test_double_asterisk(self):
- """Test double asterisk matching."""
- pattern = Pattern(b"**/foo.txt")
- self.assertTrue(pattern.match(b"foo.txt"))
- self.assertTrue(pattern.match(b"src/foo.txt"))
- self.assertTrue(pattern.match(b"src/sub/foo.txt"))
- self.assertTrue(pattern.match(b"a/b/c/foo.txt"))
- def test_double_asterisk_middle(self):
- """Test double asterisk in middle."""
- pattern = Pattern(b"src/**/foo.txt")
- self.assertTrue(pattern.match(b"src/foo.txt"))
- self.assertTrue(pattern.match(b"src/sub/foo.txt"))
- self.assertTrue(pattern.match(b"src/a/b/foo.txt"))
- self.assertFalse(pattern.match(b"foo.txt"))
- self.assertFalse(pattern.match(b"other/foo.txt"))
- def test_leading_slash(self):
- """Test pattern with leading slash."""
- pattern = Pattern(b"/README.txt")
- self.assertTrue(pattern.match(b"README.txt"))
- self.assertTrue(pattern.match(b"/README.txt"))
- self.assertFalse(pattern.match(b"src/README.txt"))
- class MatchPathTests(TestCase):
- """Test the match_path function."""
- def test_no_matches(self):
- """Test when no patterns match."""
- patterns = [
- (Pattern(b"*.txt"), {b"text": True}),
- (Pattern(b"*.jpg"), {b"binary": True}),
- ]
- attrs = match_path(patterns, b"file.py")
- self.assertEqual(attrs, {})
- def test_single_match(self):
- """Test single pattern match."""
- patterns = [
- (Pattern(b"*.txt"), {b"text": True}),
- (Pattern(b"*.jpg"), {b"binary": True}),
- ]
- attrs = match_path(patterns, b"README.txt")
- self.assertEqual(attrs, {b"text": True})
- def test_multiple_matches_override(self):
- """Test that later patterns override earlier ones."""
- patterns = [
- (Pattern(b"*"), {b"text": True}),
- (Pattern(b"*.jpg"), {b"text": False, b"binary": True}),
- ]
- attrs = match_path(patterns, b"image.jpg")
- self.assertEqual(attrs, {b"text": False, b"binary": True})
- def test_unspecified_removes_attribute(self):
- """Test that unspecified (None) removes attributes."""
- patterns = [
- (Pattern(b"*"), {b"text": True, b"diff": True}),
- (Pattern(b"*.bin"), {b"text": None, b"binary": True}),
- ]
- attrs = match_path(patterns, b"file.bin")
- self.assertEqual(attrs, {b"diff": True, b"binary": True})
- # 'text' should be removed
- self.assertNotIn(b"text", attrs)
- class FileOperationsTests(TestCase):
- """Test file operations."""
- def test_parse_gitattributes_file(self):
- """Test parsing a gitattributes file."""
- with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
- f.write(b"*.txt text\n")
- f.write(b"*.jpg -text binary\n")
- temp_path = f.name
- try:
- patterns = parse_gitattributes_file(temp_path)
- self.assertEqual(len(patterns), 2)
- # Check first pattern
- pattern, attrs = patterns[0]
- self.assertEqual(pattern.pattern, b"*.txt")
- self.assertEqual(attrs, {b"text": True})
- # Check second pattern
- pattern, attrs = patterns[1]
- self.assertEqual(pattern.pattern, b"*.jpg")
- self.assertEqual(attrs, {b"text": False, b"binary": True})
- finally:
- os.unlink(temp_path)
- def test_read_gitattributes(self):
- """Test reading gitattributes from a directory."""
- with tempfile.TemporaryDirectory() as tmpdir:
- # Create .gitattributes file
- attrs_path = os.path.join(tmpdir, ".gitattributes")
- with open(attrs_path, "wb") as f:
- f.write(b"*.py diff=python\n")
- patterns = read_gitattributes(tmpdir)
- self.assertEqual(len(patterns), 1)
- pattern, attrs = patterns[0]
- self.assertEqual(pattern.pattern, b"*.py")
- self.assertEqual(attrs, {b"diff": b"python"})
- def test_read_gitattributes_missing(self):
- """Test reading gitattributes when file doesn't exist."""
- with tempfile.TemporaryDirectory() as tmpdir:
- patterns = read_gitattributes(tmpdir)
- self.assertEqual(patterns, [])
- class GitAttributesTests(TestCase):
- """Test the GitAttributes class."""
- def test_empty_gitattributes(self):
- """Test GitAttributes with no patterns."""
- ga = GitAttributes()
- attrs = ga.match_path(b"file.txt")
- self.assertEqual(attrs, {})
- self.assertEqual(len(ga), 0)
- def test_gitattributes_with_patterns(self):
- """Test GitAttributes with patterns."""
- patterns = [
- (Pattern(b"*.txt"), {b"text": True}),
- (Pattern(b"*.jpg"), {b"binary": True, b"text": False}),
- ]
- ga = GitAttributes(patterns)
- # Test matching .txt file
- attrs = ga.match_path(b"README.txt")
- self.assertEqual(attrs, {b"text": True})
- # Test matching .jpg file
- attrs = ga.match_path(b"image.jpg")
- self.assertEqual(attrs, {b"binary": True, b"text": False})
- # Test non-matching file
- attrs = ga.match_path(b"script.py")
- self.assertEqual(attrs, {})
- self.assertEqual(len(ga), 2)
- def test_add_patterns(self):
- """Test adding patterns to GitAttributes."""
- ga = GitAttributes()
- self.assertEqual(len(ga), 0)
- # Add patterns
- ga.add_patterns(
- [
- (Pattern(b"*.py"), {b"diff": b"python"}),
- (Pattern(b"*.md"), {b"text": True}),
- ]
- )
- self.assertEqual(len(ga), 2)
- attrs = ga.match_path(b"test.py")
- self.assertEqual(attrs, {b"diff": b"python"})
- def test_iteration(self):
- """Test iterating over patterns."""
- patterns = [
- (Pattern(b"*.txt"), {b"text": True}),
- (Pattern(b"*.jpg"), {b"binary": True}),
- ]
- ga = GitAttributes(patterns)
- collected = list(ga)
- self.assertEqual(len(collected), 2)
- self.assertEqual(collected[0][0].pattern, b"*.txt")
- self.assertEqual(collected[1][0].pattern, b"*.jpg")
- def test_from_file(self):
- """Test creating GitAttributes from file."""
- with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
- f.write(b"*.txt text\n")
- f.write(b"*.bin -text binary\n")
- temp_path = f.name
- try:
- ga = GitAttributes.from_file(temp_path)
- self.assertEqual(len(ga), 2)
- attrs = ga.match_path(b"file.txt")
- self.assertEqual(attrs, {b"text": True})
- attrs = ga.match_path(b"file.bin")
- self.assertEqual(attrs, {b"text": False, b"binary": True})
- finally:
- os.unlink(temp_path)
- def test_from_path(self):
- """Test creating GitAttributes from directory path."""
- with tempfile.TemporaryDirectory() as tmpdir:
- # Create .gitattributes file
- attrs_path = os.path.join(tmpdir, ".gitattributes")
- with open(attrs_path, "wb") as f:
- f.write(b"*.py diff=python\n")
- f.write(b"*.rs diff=rust\n")
- ga = GitAttributes.from_path(tmpdir)
- self.assertEqual(len(ga), 2)
- attrs = ga.match_path(b"main.py")
- self.assertEqual(attrs, {b"diff": b"python"})
- attrs = ga.match_path(b"lib.rs")
- self.assertEqual(attrs, {b"diff": b"rust"})
|