Python · 4987 bytes Raw Blame History
1 """`.dlm/ignore` grammar tests — comments, blanks, negation, anchors,
2 directory-only, globstar, malformed lines."""
3
4 from __future__ import annotations
5
6 import logging
7
8 import pytest
9
10 from dlm.directives.ignore_parser import (
11 IgnoreRule,
12 matches,
13 parse_ignore_file,
14 )
15
16 # ---- parse_ignore_file -----------------------------------------------------
17
18
19 def test_parse_empty() -> None:
20 assert parse_ignore_file("") == ()
21 assert parse_ignore_file(" \n\n\t\n") == ()
22
23
24 def test_parse_skips_comments_and_blanks() -> None:
25 text = "# top comment\n\n # indented comment\n*.log\n"
26 rules = parse_ignore_file(text)
27 assert len(rules) == 1
28 assert rules[0].pattern == "*.log"
29
30
31 def test_parse_negation() -> None:
32 rules = parse_ignore_file("!keep.txt\n")
33 assert rules[0].negate is True
34 assert rules[0].pattern == "keep.txt"
35
36
37 def test_parse_anchored() -> None:
38 rules = parse_ignore_file("/scripts/local.sh\n")
39 assert rules[0].anchored is True
40 assert rules[0].pattern == "scripts/local.sh"
41
42
43 def test_parse_directory_only() -> None:
44 rules = parse_ignore_file("build/\n")
45 assert rules[0].directory_only is True
46 assert rules[0].pattern == "build"
47
48
49 def test_parse_combined_flags() -> None:
50 rules = parse_ignore_file("!/scripts/build/\n")
51 assert rules[0].negate is True
52 assert rules[0].anchored is True
53 assert rules[0].directory_only is True
54 assert rules[0].pattern == "scripts/build"
55
56
57 def test_parse_bare_bang_skipped(caplog: pytest.LogCaptureFixture) -> None:
58 caplog.set_level(logging.WARNING, logger="dlm.directives.ignore_parser")
59 rules = parse_ignore_file("!\n")
60 assert rules == ()
61 assert any("bare '!'" in rec.message for rec in caplog.records)
62
63
64 def test_parse_bare_slash_skipped(caplog: pytest.LogCaptureFixture) -> None:
65 caplog.set_level(logging.WARNING, logger="dlm.directives.ignore_parser")
66 rules = parse_ignore_file("/\n")
67 assert rules == ()
68 assert any("bare '/'" in rec.message for rec in caplog.records)
69
70
71 def test_parse_pattern_reduced_to_empty_skipped(caplog: pytest.LogCaptureFixture) -> None:
72 caplog.set_level(logging.WARNING, logger="dlm.directives.ignore_parser")
73 rules = parse_ignore_file("//\n")
74 assert rules == ()
75 assert any("pattern reduced to empty" in rec.message for rec in caplog.records)
76
77
78 # ---- matches ---------------------------------------------------------------
79
80
81 def _rule(
82 pattern: str,
83 *,
84 anchored: bool = False,
85 directory_only: bool = False,
86 negate: bool = False,
87 ) -> IgnoreRule:
88 return IgnoreRule(
89 pattern=pattern,
90 anchored=anchored,
91 directory_only=directory_only,
92 negate=negate,
93 )
94
95
96 def test_matches_unanchored_globstar() -> None:
97 rule = _rule("**/__pycache__/**")
98 assert matches(rule, "src/__pycache__/foo.pyc", is_dir=False)
99 assert matches(rule, "a/b/__pycache__/c/d.pyc", is_dir=False)
100
101
102 def test_matches_unanchored_basename() -> None:
103 rule = _rule("*.log")
104 assert matches(rule, "debug.log", is_dir=False)
105 assert matches(rule, "a/b/debug.log", is_dir=False)
106
107
108 def test_matches_anchored_only_at_root() -> None:
109 rule = _rule("scripts/local.sh", anchored=True)
110 assert matches(rule, "scripts/local.sh", is_dir=False)
111 assert not matches(rule, "a/scripts/local.sh", is_dir=False)
112
113
114 def test_matches_directory_only_flags_subtree() -> None:
115 rule = _rule("build", directory_only=True)
116 # Any path under a "build" directory matches via ancestor-component
117 assert matches(rule, "build/x.txt", is_dir=False)
118 assert matches(rule, "a/build/x/y.txt", is_dir=False)
119 # No "build" component → no match
120 assert not matches(rule, "src/main.py", is_dir=False)
121
122
123 def test_matches_directory_only_with_file_not_dir() -> None:
124 # A file *named* "build" without the directory flag matches; with
125 # directory_only=True, a plain file at that path does NOT match
126 # (only directories and their descendants do).
127 rule = _rule("build", directory_only=True)
128 # A file literally named "build" at root: no ancestor "build/" path.
129 assert not matches(rule, "build", is_dir=False)
130 # But matches when is_dir=True:
131 assert matches(rule, "build", is_dir=True)
132
133
134 def test_matches_single_star_does_not_cross_slash() -> None:
135 rule = _rule("*.py")
136 assert matches(rule, "foo.py", is_dir=False)
137 # `*.py` anchored (via unanchored suffix-matching) should match the
138 # last component of a/b.py too, since we try suffixes.
139 assert matches(rule, "a/b.py", is_dir=False)
140
141
142 def test_matches_question_mark_single_char() -> None:
143 rule = _rule("foo?.txt")
144 assert matches(rule, "foo1.txt", is_dir=False)
145 assert not matches(rule, "foo12.txt", is_dir=False)
146
147
148 def test_matches_globstar_prefix() -> None:
149 rule = _rule("tests/**")
150 assert matches(rule, "tests/a.py", is_dir=False)
151 assert matches(rule, "tests/a/b/c.py", is_dir=False)
152 assert not matches(rule, "src/a.py", is_dir=False)