Skip to content

Commit 89a3611

Browse files
Convert violation to relative path for subfolder support (#574)
* Convert violation to relative path for subfolder support * Fix lint error * Pytest fixes: resetting `GitPathTool` between tests
1 parent 4b0cef9 commit 89a3611

File tree

4 files changed

+77
-7
lines changed

4 files changed

+77
-7
lines changed

diff_cover/git_path.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ def relative_path(cls, git_diff_path):
3636
"""
3737
Returns git_diff_path relative to cwd.
3838
"""
39+
# If GitPathTool hasn't been initialized, return the path unchanged
40+
if cls._cwd is None or cls._root is None:
41+
return git_diff_path
42+
3943
# Remove git_root from src_path for searching the correct filename
4044
# If cwd is `/home/user/work/diff-cover/diff_cover`
4145
# and src_path is `diff_cover/violations_reporter.py`

diff_cover/violationsreporters/base.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from collections import defaultdict, namedtuple
77

88
from diff_cover.command_runner import execute, run_command_for_code
9+
from diff_cover.git_path import GitPathTool
910
from diff_cover.util import to_unix_path
1011

1112
Violation = namedtuple("Violation", "line, message")
@@ -152,14 +153,19 @@ def violations(self, src_path):
152153
if not any(src_path.endswith(ext) for ext in self.driver.supported_extensions):
153154
return []
154155

155-
if src_path not in self.violations_dict:
156+
# `src_path` is relative to the git root. We convert it to be relative to
157+
# the current working directory, since quality tools report paths relative
158+
# to the current working directory.
159+
relative_src_path = to_unix_path(GitPathTool.relative_path(src_path))
160+
161+
if relative_src_path not in self.violations_dict:
156162
if self.reports:
157163
self.violations_dict = self.driver.parse_reports(self.reports)
158-
return self.violations_dict[src_path]
164+
return self.violations_dict[relative_src_path]
159165

160-
if not os.path.exists(src_path):
161-
self.violations_dict[src_path] = []
162-
return self.violations_dict[src_path]
166+
if not os.path.exists(relative_src_path):
167+
self.violations_dict[relative_src_path] = []
168+
return self.violations_dict[relative_src_path]
163169

164170
if self.driver_tool_installed is None:
165171
self.driver_tool_installed = self.driver.installed()
@@ -170,13 +176,13 @@ def violations(self, src_path):
170176
if self.options:
171177
for arg in self.options.split():
172178
command.append(arg)
173-
command.append(src_path.encode(sys.getfilesystemencoding()))
179+
command.append(relative_src_path.encode(sys.getfilesystemencoding()))
174180

175181
stdout, stderr = execute(command, self.driver.exit_codes)
176182
output = stderr if self.driver.output_stderr else stdout
177183
self.violations_dict.update(self.driver.parse_reports([output]))
178184

179-
return self.violations_dict[src_path]
185+
return self.violations_dict[relative_src_path]
180186

181187
def measured_lines(self, src_path):
182188
"""

tests/conftest.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import pytest
2+
3+
from diff_cover.git_path import GitPathTool
4+
5+
6+
@pytest.fixture(autouse=True)
7+
def reset_git_path_tool():
8+
"""Reset GitPathTool before each test to ensure test isolation.
9+
10+
GitPathTool uses class variables (_cwd and _root) that persist across tests.
11+
This fixture ensures each test starts with a clean state.
12+
"""
13+
GitPathTool._cwd = None
14+
GitPathTool._root = None
15+
yield
16+
GitPathTool._cwd = None
17+
GitPathTool._root = None

tests/test_violations_reporter.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2312,3 +2312,46 @@ def test_parse_report(self):
23122312
driver = ClangFormatDriver()
23132313
actual_violations = driver.parse_reports([report])
23142314
assert actual_violations == expected_violations
2315+
2316+
2317+
class TestQualityReporterSubdirectory:
2318+
"""
2319+
Test that QualityReporter works correctly when running from a subdirectory.
2320+
2321+
When running diff-quality from a subdirectory:
2322+
- Git reports paths relative to the git root (e.g., "subdir/file.py")
2323+
- Quality tools report paths relative to the current working directory (e.g., "file.py")
2324+
"""
2325+
2326+
def test_violations_from_subdirectory(self, mocker, process_patcher):
2327+
"""
2328+
Test that violations are found when running from a subdirectory.
2329+
2330+
Simulates running diff-quality from "subdir/" where:
2331+
- Git reports the file as "subdir/file.py" (relative to git root)
2332+
- The quality tool reports violations on "file.py" (relative to cwd)
2333+
"""
2334+
from diff_cover.git_path import GitPathTool
2335+
2336+
# Simulate running from a subdirectory by mocking relative_path
2337+
# to strip the "subdir/" prefix (as it would when cwd is inside subdir/)
2338+
mocker.patch.object(
2339+
GitPathTool, "relative_path", side_effect=lambda x: x.replace("subdir/", "")
2340+
)
2341+
2342+
# Quality tool outputs violations with paths relative to cwd
2343+
# (without the "subdir/" prefix)
2344+
tool_output = "file.py:10: error: Something is wrong [error-code]"
2345+
process_patcher((tool_output.encode("utf-8"), b""))
2346+
2347+
quality = QualityReporter(mypy_driver)
2348+
2349+
# Request violations using the git-relative path (with "subdir/" prefix)
2350+
# This is what diff-quality would pass based on git diff output
2351+
violations = quality.violations("subdir/file.py")
2352+
2353+
# Verify violations are found (the fix makes this work)
2354+
expected = [
2355+
Violation(line=10, message="error: Something is wrong [error-code]")
2356+
]
2357+
assert violations == expected

0 commit comments

Comments
 (0)