diff --git a/diff_cover/git_path.py b/diff_cover/git_path.py index a4f41bee..1f7b0cc6 100644 --- a/diff_cover/git_path.py +++ b/diff_cover/git_path.py @@ -36,6 +36,10 @@ def relative_path(cls, git_diff_path): """ Returns git_diff_path relative to cwd. """ + # If GitPathTool hasn't been initialized, return the path unchanged + if cls._cwd is None or cls._root is None: + return git_diff_path + # Remove git_root from src_path for searching the correct filename # If cwd is `/home/user/work/diff-cover/diff_cover` # and src_path is `diff_cover/violations_reporter.py` diff --git a/diff_cover/violationsreporters/base.py b/diff_cover/violationsreporters/base.py index 2a563868..3c30d787 100644 --- a/diff_cover/violationsreporters/base.py +++ b/diff_cover/violationsreporters/base.py @@ -6,6 +6,7 @@ from collections import defaultdict, namedtuple from diff_cover.command_runner import execute, run_command_for_code +from diff_cover.git_path import GitPathTool from diff_cover.util import to_unix_path Violation = namedtuple("Violation", "line, message") @@ -152,14 +153,19 @@ def violations(self, src_path): if not any(src_path.endswith(ext) for ext in self.driver.supported_extensions): return [] - if src_path not in self.violations_dict: + # `src_path` is relative to the git root. We convert it to be relative to + # the current working directory, since quality tools report paths relative + # to the current working directory. + relative_src_path = to_unix_path(GitPathTool.relative_path(src_path)) + + if relative_src_path not in self.violations_dict: if self.reports: self.violations_dict = self.driver.parse_reports(self.reports) - return self.violations_dict[src_path] + return self.violations_dict[relative_src_path] - if not os.path.exists(src_path): - self.violations_dict[src_path] = [] - return self.violations_dict[src_path] + if not os.path.exists(relative_src_path): + self.violations_dict[relative_src_path] = [] + return self.violations_dict[relative_src_path] if self.driver_tool_installed is None: self.driver_tool_installed = self.driver.installed() @@ -170,13 +176,13 @@ def violations(self, src_path): if self.options: for arg in self.options.split(): command.append(arg) - command.append(src_path.encode(sys.getfilesystemencoding())) + command.append(relative_src_path.encode(sys.getfilesystemencoding())) stdout, stderr = execute(command, self.driver.exit_codes) output = stderr if self.driver.output_stderr else stdout self.violations_dict.update(self.driver.parse_reports([output])) - return self.violations_dict[src_path] + return self.violations_dict[relative_src_path] def measured_lines(self, src_path): """ diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..e65ba4d5 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,17 @@ +import pytest + +from diff_cover.git_path import GitPathTool + + +@pytest.fixture(autouse=True) +def reset_git_path_tool(): + """Reset GitPathTool before each test to ensure test isolation. + + GitPathTool uses class variables (_cwd and _root) that persist across tests. + This fixture ensures each test starts with a clean state. + """ + GitPathTool._cwd = None + GitPathTool._root = None + yield + GitPathTool._cwd = None + GitPathTool._root = None diff --git a/tests/test_violations_reporter.py b/tests/test_violations_reporter.py index b2a7a0fa..6be46d42 100644 --- a/tests/test_violations_reporter.py +++ b/tests/test_violations_reporter.py @@ -2312,3 +2312,46 @@ def test_parse_report(self): driver = ClangFormatDriver() actual_violations = driver.parse_reports([report]) assert actual_violations == expected_violations + + +class TestQualityReporterSubdirectory: + """ + Test that QualityReporter works correctly when running from a subdirectory. + + When running diff-quality from a subdirectory: + - Git reports paths relative to the git root (e.g., "subdir/file.py") + - Quality tools report paths relative to the current working directory (e.g., "file.py") + """ + + def test_violations_from_subdirectory(self, mocker, process_patcher): + """ + Test that violations are found when running from a subdirectory. + + Simulates running diff-quality from "subdir/" where: + - Git reports the file as "subdir/file.py" (relative to git root) + - The quality tool reports violations on "file.py" (relative to cwd) + """ + from diff_cover.git_path import GitPathTool + + # Simulate running from a subdirectory by mocking relative_path + # to strip the "subdir/" prefix (as it would when cwd is inside subdir/) + mocker.patch.object( + GitPathTool, "relative_path", side_effect=lambda x: x.replace("subdir/", "") + ) + + # Quality tool outputs violations with paths relative to cwd + # (without the "subdir/" prefix) + tool_output = "file.py:10: error: Something is wrong [error-code]" + process_patcher((tool_output.encode("utf-8"), b"")) + + quality = QualityReporter(mypy_driver) + + # Request violations using the git-relative path (with "subdir/" prefix) + # This is what diff-quality would pass based on git diff output + violations = quality.violations("subdir/file.py") + + # Verify violations are found (the fix makes this work) + expected = [ + Violation(line=10, message="error: Something is wrong [error-code]") + ] + assert violations == expected