Coverage for tests / unit / ai / test_filters.py: 100%
129 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
1"""Tests for AI issue filtering (path/rule allow/deny policy)."""
3from __future__ import annotations
5from assertpy import assert_that
7from lintro.ai.config import AIConfig
8from lintro.ai.filters import filter_issues, should_process_issue
9from lintro.parsers.base_issue import BaseIssue
10from tests.unit.ai.conftest import MockIssue
12# -- should_process_issue: no filters -----------------------------------------
15def test_no_filters_allows_all() -> None:
16 """With no filters configured, all issues pass."""
17 config = AIConfig()
18 issue = MockIssue(file="src/main.py", code="E501")
19 assert_that(should_process_issue(issue, config)).is_true()
22def test_no_filters_allows_empty_fields() -> None:
23 """Issues with empty file/code pass when no filters are set."""
24 config = AIConfig()
25 issue = MockIssue(file="", code="")
26 assert_that(should_process_issue(issue, config)).is_true()
29# -- include_paths -------------------------------------------------------------
32def test_include_paths_matches() -> None:
33 """Issue with matching path is included."""
34 config = AIConfig(include_paths=["src/*.py"])
35 issue = MockIssue(file="src/main.py", code="E501")
36 assert_that(should_process_issue(issue, config)).is_true()
39def test_include_paths_no_match() -> None:
40 """Issue with non-matching path is excluded."""
41 config = AIConfig(include_paths=["src/*.py"])
42 issue = MockIssue(file="tests/test_main.py", code="E501")
43 assert_that(should_process_issue(issue, config)).is_false()
46def test_include_paths_multiple_patterns() -> None:
47 """Issue matching any include pattern is included."""
48 config = AIConfig(include_paths=["src/*.py", "lib/*.py"])
49 issue = MockIssue(file="lib/utils.py", code="E501")
50 assert_that(should_process_issue(issue, config)).is_true()
53def test_include_paths_glob_star_star() -> None:
54 """Recursive glob pattern matches at all nesting depths."""
55 config = AIConfig(include_paths=["src/**/*.py"])
56 # Zero segments: src/foo.py
57 assert_that(
58 should_process_issue(MockIssue(file="src/foo.py", code="E501"), config),
59 ).is_true()
60 # One segment: src/a/foo.py
61 assert_that(
62 should_process_issue(MockIssue(file="src/a/foo.py", code="E501"), config),
63 ).is_true()
64 # Deep nesting: src/a/b/c/foo.py
65 assert_that(
66 should_process_issue(
67 MockIssue(file="src/deep/nested/module.py", code="E501"),
68 config,
69 ),
70 ).is_true()
71 # Non-matching prefix
72 assert_that(
73 should_process_issue(MockIssue(file="other/foo.py", code="E501"), config),
74 ).is_false()
75 # Non-matching extension
76 assert_that(
77 should_process_issue(MockIssue(file="src/foo.txt", code="E501"), config),
78 ).is_false()
81# -- exclude_paths -------------------------------------------------------------
84def test_exclude_paths_matches() -> None:
85 """Issue with matching exclude path is rejected."""
86 config = AIConfig(exclude_paths=["tests/*"])
87 issue = MockIssue(file="tests/test_main.py", code="E501")
88 assert_that(should_process_issue(issue, config)).is_false()
91def test_exclude_paths_no_match() -> None:
92 """Issue not matching exclude path is allowed."""
93 config = AIConfig(exclude_paths=["tests/*"])
94 issue = MockIssue(file="src/main.py", code="E501")
95 assert_that(should_process_issue(issue, config)).is_true()
98def test_exclude_paths_multiple_patterns() -> None:
99 """Issue matching any exclude pattern is rejected."""
100 config = AIConfig(exclude_paths=["tests/*", "docs/*"])
101 issue = MockIssue(file="docs/readme.py", code="E501")
102 assert_that(should_process_issue(issue, config)).is_false()
105# -- include_paths + exclude_paths together ------------------------------------
108def test_include_and_exclude_paths_include_wins_when_not_excluded() -> None:
109 """Include passes, exclude does not match: issue is processed."""
110 config = AIConfig(include_paths=["src/*.py"], exclude_paths=["src/vendor/*"])
111 issue = MockIssue(file="src/main.py", code="E501")
112 assert_that(should_process_issue(issue, config)).is_true()
115def test_include_and_exclude_paths_exclude_overrides() -> None:
116 """Include passes but exclude also matches: issue is rejected."""
117 config = AIConfig(include_paths=["src/**"], exclude_paths=["src/vendor/*"])
118 issue = MockIssue(file="src/vendor/lib.py", code="E501")
119 assert_that(should_process_issue(issue, config)).is_false()
122def test_include_paths_rejects_before_exclude_checked() -> None:
123 """If include_paths doesn't match, exclude_paths is irrelevant."""
124 config = AIConfig(include_paths=["lib/*"], exclude_paths=["tests/*"])
125 issue = MockIssue(file="src/main.py", code="E501")
126 assert_that(should_process_issue(issue, config)).is_false()
129# -- include_rules -------------------------------------------------------------
132def test_include_rules_matches() -> None:
133 """Issue with matching rule code is included."""
134 config = AIConfig(include_rules=["E5*"])
135 issue = MockIssue(file="src/main.py", code="E501")
136 assert_that(should_process_issue(issue, config)).is_true()
139def test_include_rules_no_match() -> None:
140 """Issue with non-matching rule code is excluded."""
141 config = AIConfig(include_rules=["E5*"])
142 issue = MockIssue(file="src/main.py", code="W123")
143 assert_that(should_process_issue(issue, config)).is_false()
146def test_include_rules_exact_match() -> None:
147 """Exact rule code match works."""
148 config = AIConfig(include_rules=["B101"])
149 issue = MockIssue(file="src/main.py", code="B101")
150 assert_that(should_process_issue(issue, config)).is_true()
153# -- exclude_rules -------------------------------------------------------------
156def test_exclude_rules_matches() -> None:
157 """Issue with matching exclude rule is rejected."""
158 config = AIConfig(exclude_rules=["E501"])
159 issue = MockIssue(file="src/main.py", code="E501")
160 assert_that(should_process_issue(issue, config)).is_false()
163def test_exclude_rules_no_match() -> None:
164 """Issue not matching exclude rule is allowed."""
165 config = AIConfig(exclude_rules=["E501"])
166 issue = MockIssue(file="src/main.py", code="B101")
167 assert_that(should_process_issue(issue, config)).is_true()
170def test_exclude_rules_glob_pattern() -> None:
171 """Glob pattern in exclude_rules matches multiple codes."""
172 config = AIConfig(exclude_rules=["E*"])
173 issue = MockIssue(file="src/main.py", code="E501")
174 assert_that(should_process_issue(issue, config)).is_false()
177# -- include_rules + exclude_rules together ------------------------------------
180def test_include_and_exclude_rules_together() -> None:
181 """Include passes but exclude also matches: issue is rejected."""
182 config = AIConfig(include_rules=["E*"], exclude_rules=["E501"])
183 issue = MockIssue(file="src/main.py", code="E501")
184 assert_that(should_process_issue(issue, config)).is_false()
187def test_include_rules_passes_exclude_rules_does_not_match() -> None:
188 """Include passes, exclude does not match: issue is processed."""
189 config = AIConfig(include_rules=["E*"], exclude_rules=["E501"])
190 issue = MockIssue(file="src/main.py", code="E302")
191 assert_that(should_process_issue(issue, config)).is_true()
194# -- Combined path and rule filters -------------------------------------------
197def test_path_and_rule_filters_both_pass() -> None:
198 """Both path and rule filters must pass for issue to be processed."""
199 config = AIConfig(include_paths=["src/*"], include_rules=["E*"])
200 issue = MockIssue(file="src/main.py", code="E501")
201 assert_that(should_process_issue(issue, config)).is_true()
204def test_path_passes_rule_fails() -> None:
205 """Path passes but rule does not: issue is rejected."""
206 config = AIConfig(include_paths=["src/*"], include_rules=["E*"])
207 issue = MockIssue(file="src/main.py", code="B101")
208 assert_that(should_process_issue(issue, config)).is_false()
211def test_rule_passes_path_fails() -> None:
212 """Rule passes but path does not: issue is rejected."""
213 config = AIConfig(include_paths=["src/*"], include_rules=["E*"])
214 issue = MockIssue(file="tests/test.py", code="E501")
215 assert_that(should_process_issue(issue, config)).is_false()
218# -- filter_issues -------------------------------------------------------------
221def test_filter_issues_returns_matching_only() -> None:
222 """filter_issues returns only issues that pass the filter."""
223 config = AIConfig(include_paths=["src/*"])
224 issues: list[BaseIssue] = [
225 MockIssue(file="src/main.py", code="E501"),
226 MockIssue(file="tests/test.py", code="E501"),
227 MockIssue(file="src/utils.py", code="B101"),
228 ]
229 result = filter_issues(issues, config)
230 assert_that(result).is_length(2)
231 assert_that([i.file for i in result]).contains("src/main.py", "src/utils.py")
234def test_filter_issues_empty_list() -> None:
235 """filter_issues handles empty list."""
236 config = AIConfig(include_paths=["src/*"])
237 result = filter_issues([], config)
238 assert_that(result).is_empty()
241def test_filter_issues_no_filters() -> None:
242 """filter_issues with no filters returns all issues."""
243 config = AIConfig()
244 issues: list[BaseIssue] = [
245 MockIssue(file="src/main.py", code="E501"),
246 MockIssue(file="tests/test.py", code="B101"),
247 ]
248 result = filter_issues(issues, config)
249 assert_that(result).is_length(2)
252def test_filter_issues_all_excluded() -> None:
253 """filter_issues can exclude all issues."""
254 config = AIConfig(exclude_paths=["**"])
255 issues: list[BaseIssue] = [
256 MockIssue(file="src/main.py", code="E501"),
257 MockIssue(file="tests/test.py", code="B101"),
258 ]
259 result = filter_issues(issues, config)
260 assert_that(result).is_empty()
263# -- Edge cases ----------------------------------------------------------------
266def test_issue_without_code_attribute() -> None:
267 """BaseIssue without code attribute is handled gracefully."""
268 config = AIConfig(include_rules=["E*"])
269 issue = BaseIssue(file="src/main.py", line=1, message="test")
270 # BaseIssue has no code attr, getattr returns ""
271 assert_that(should_process_issue(issue, config)).is_false()
274def test_issue_without_file() -> None:
275 """Issue with empty file and include_paths filter is excluded."""
276 config = AIConfig(include_paths=["src/*"])
277 issue = MockIssue(file="", code="E501")
278 assert_that(should_process_issue(issue, config)).is_false()