diff --git a/README.md b/README.md index cd4b8bf..f5990dc 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,44 @@ pre-commit run --files $(git diff --name-only) This approach ensures that only modified files are checked, further speeding up the linting process during development. +### Verbose Output and Debugging + +For debugging issues or getting more detailed information about what the hooks are doing, you can enable verbose output: + +```yaml +repos: + - repo: https://github.com/cpp-linter/cpp-linter-hooks + rev: v0.8.1 + hooks: + - id: clang-format + args: [--style=file, --version=18, --verbose] # Enable verbose output + - id: clang-tidy + args: [--checks=.clang-tidy, --version=18, --verbose] # Enable verbose output +``` + +When you run pre-commit with verbose mode, you'll get detailed debug information: + +```bash +# Run pre-commit with verbose output +pre-commit run --verbose --all-files + +# Run specific hook with verbose output +pre-commit run clang-format --verbose +``` + +The verbose output includes: +- The exact command being executed +- The path to the clang tool executable +- The tool version being used +- Exit codes and detailed error messages +- Both stdout and stderr output from the tools + +This is particularly useful when debugging issues like: +- Exit code 247 (tool execution errors) +- Missing compilation databases for clang-tidy +- Configuration file loading issues +- Tool installation problems + ## Output ### clang-format Example @@ -148,7 +186,6 @@ Use -header-filter=.* to display errors from all non-system headers. Use -system for (;;) ^ { - ``` ## Contributing diff --git a/cpp_linter_hooks/clang_format.py b/cpp_linter_hooks/clang_format.py index cd0eb91..a773ba8 100644 --- a/cpp_linter_hooks/clang_format.py +++ b/cpp_linter_hooks/clang_format.py @@ -1,12 +1,19 @@ -import subprocess from argparse import ArgumentParser from typing import Tuple -from .util import ensure_installed, DEFAULT_CLANG_VERSION +from .util import ( + ensure_installed, + DEFAULT_CLANG_VERSION, + debug_print, + run_subprocess_with_logging, +) parser = ArgumentParser() parser.add_argument("--version", default=DEFAULT_CLANG_VERSION) +parser.add_argument( + "-v", "--verbose", action="store_true", help="Enable verbose output" +) def run_clang_format(args=None) -> Tuple[int, str]: @@ -15,27 +22,27 @@ def run_clang_format(args=None) -> Tuple[int, str]: command = [str(path), "-i"] command.extend(other_args) - retval = 0 - output = "" - try: - if "--dry-run" in command: - sp = subprocess.run(command, stdout=subprocess.PIPE, encoding="utf-8") - retval = -1 # Not a fail just identify it's a dry-run. - output = sp.stdout - else: - retval = subprocess.run(command, stdout=subprocess.PIPE).returncode - return retval, output - except FileNotFoundError as stderr: - retval = 1 - return retval, str(stderr) + verbose = hook_args.verbose + + # Log initial debug information + debug_print(f"clang-format version: {hook_args.version}", verbose) + debug_print(f"clang-format executable: {path}", verbose) + + # Check for dry-run mode + dry_run = "--dry-run" in command + + # Use the utility function for subprocess execution + return run_subprocess_with_logging( + command=command, tool_name="clang-format", verbose=verbose, dry_run=dry_run + ) def main() -> int: retval, output = run_clang_format() - if retval != 0: - print(output) + if retval != 0: # pragma: no cover + print(output) # pragma: no cover return retval -if __name__ == "__main__": +if __name__ == "__main__": # pragma: no cover raise SystemExit(main()) diff --git a/cpp_linter_hooks/clang_tidy.py b/cpp_linter_hooks/clang_tidy.py index 2a51334..4c6f45c 100644 --- a/cpp_linter_hooks/clang_tidy.py +++ b/cpp_linter_hooks/clang_tidy.py @@ -1,12 +1,19 @@ -import subprocess from argparse import ArgumentParser from typing import Tuple -from .util import ensure_installed, DEFAULT_CLANG_VERSION +from .util import ( + ensure_installed, + DEFAULT_CLANG_VERSION, + debug_print, + run_subprocess_with_logging, +) parser = ArgumentParser() parser.add_argument("--version", default=DEFAULT_CLANG_VERSION) +parser.add_argument( + "-v", "--verbose", action="store_true", help="Enable verbose output" +) def run_clang_tidy(args=None) -> Tuple[int, str]: @@ -15,26 +22,24 @@ def run_clang_tidy(args=None) -> Tuple[int, str]: command = [str(path)] command.extend(other_args) - retval = 0 - output = "" - try: - sp = subprocess.run(command, stdout=subprocess.PIPE, encoding="utf-8") - retval = sp.returncode - output = sp.stdout - if "warning:" in output or "error:" in output: - retval = 1 - return retval, output - except FileNotFoundError as stderr: - retval = 1 - return retval, str(stderr) + verbose = hook_args.verbose + + # Log initial debug information + debug_print(f"clang-tidy version: {hook_args.version}", verbose) + debug_print(f"clang-tidy executable: {path}", verbose) + + # Use the utility function for subprocess execution + return run_subprocess_with_logging( + command=command, tool_name="clang-tidy", verbose=verbose, dry_run=False + ) def main() -> int: retval, output = run_clang_tidy() - if retval != 0: - print(output) + if retval != 0: # pragma: no cover + print(output) # pragma: no cover return retval -if __name__ == "__main__": +if __name__ == "__main__": # pragma: no cover raise SystemExit(main()) diff --git a/cpp_linter_hooks/util.py b/cpp_linter_hooks/util.py index 0907dac..70ad8f0 100644 --- a/cpp_linter_hooks/util.py +++ b/cpp_linter_hooks/util.py @@ -1,7 +1,8 @@ import sys from pathlib import Path import logging -from typing import Optional +import subprocess +from typing import Optional, Tuple, List from clang_tools.util import Version from clang_tools.install import is_installed as _is_installed, install_tool @@ -13,6 +14,80 @@ DEFAULT_CLANG_VERSION = "18" # Default version for clang tools, can be overridden +def debug_print(message: str, verbose: bool = False) -> None: + """Print debug message to stderr if verbose mode is enabled.""" + if verbose: + print(f"[DEBUG] {message}", file=sys.stderr) + + +def run_subprocess_with_logging( + command: List[str], tool_name: str, verbose: bool = False, dry_run: bool = False +) -> Tuple[int, str]: + """ + Run subprocess with comprehensive logging and error handling. + + Args: + command: Command list to execute + tool_name: Name of the tool (for logging) + verbose: Enable verbose debug output + dry_run: Whether this is a dry run (affects return code for clang-format) + + Returns: + Tuple of (return_code, combined_output) + """ + debug_print(f"{tool_name} command: {' '.join(command)}", verbose) + + retval = 0 + output = "" + stderr_output = "" + + try: + sp = subprocess.run( + command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf-8" + ) + + output = sp.stdout + stderr_output = sp.stderr + retval = sp.returncode + + if dry_run and tool_name == "clang-format": + debug_print("Dry-run mode detected", verbose) + debug_print(f"Exit code: {retval} (converted to -1 for dry-run)", verbose) + retval = -1 # Special handling for clang-format dry-run + else: + debug_print(f"Exit code: {retval}", verbose) + + # Log outputs if verbose + if verbose and (output or stderr_output): + debug_print(f"stdout: {repr(output)}", verbose) + debug_print(f"stderr: {repr(stderr_output)}", verbose) + + # Combine stdout and stderr for comprehensive output + combined_output = "" + if output: + combined_output += output + if stderr_output: + if combined_output: + combined_output += "\n" + combined_output += stderr_output + + # Special handling for clang-tidy warnings/errors + if tool_name == "clang-tidy" and ( + "warning:" in combined_output or "error:" in combined_output + ): + if retval == 0: # Only override if it was successful + retval = 1 + debug_print("Found warnings/errors, setting exit code to 1", verbose) + + return retval, combined_output + + except FileNotFoundError as error: + error_msg = f"{tool_name} executable not found: {error}" + debug_print(f"FileNotFoundError: {error}", verbose) + debug_print(f"Command attempted: {' '.join(command)}", verbose) + return 1, error_msg + + def is_installed(tool_name: str, version: str) -> Optional[Path]: """Check if tool is installed. diff --git a/testing/pre-commit-config-verbose.yaml b/testing/pre-commit-config-verbose.yaml new file mode 100644 index 0000000..efeeac8 --- /dev/null +++ b/testing/pre-commit-config-verbose.yaml @@ -0,0 +1,25 @@ +# Example .pre-commit-config.yaml with verbose debugging enabled + +repos: + - repo: https://github.com/cpp-linter/cpp-linter-hooks + rev: v0.8.1 # Use the tag or commit you want + hooks: + - id: clang-format + args: [--style=file, --version=18, --verbose] # Added --verbose + files: ^(src|include)/.*\.(cpp|cc|cxx|h|hpp)$ + - id: clang-tidy + args: [--checks=.clang-tidy, --version=18, --verbose] # Added --verbose + files: ^(src|include)/.*\.(cpp|cc|cxx|h|hpp)$ + +# To use this configuration: +# 1. Save this as .pre-commit-config.yaml in your project root +# 2. Run: pre-commit run --verbose --all-files +# 3. The --verbose flag to pre-commit combined with --verbose in args +# will give you maximum debugging information + +# For your specific exit code 247 issue, you'll now see: +# - The exact clang-format command being executed +# - The path to the clang-format executable +# - The version being used +# - Both stdout and stderr output +# - The actual exit code from clang-format diff --git a/testing/run.sh b/testing/run.sh index 34725f1..5bc4a22 100644 --- a/testing/run.sh +++ b/testing/run.sh @@ -1,7 +1,7 @@ rm -f result.txt git restore testing/main.c -for config in testing/pre-commit-config.yaml testing/pre-commit-config-version.yaml; do +for config in testing/pre-commit-config.yaml testing/pre-commit-config-version.yaml testing/pre-commit-config-verbose.yaml; do pre-commit clean pre-commit run -c $config --files testing/main.c | tee -a result.txt || true git restore testing/main.c diff --git a/tests/test_clang_format.py b/tests/test_clang_format.py index 38a8fe0..78fd4e2 100644 --- a/tests/test_clang_format.py +++ b/tests/test_clang_format.py @@ -1,7 +1,8 @@ import pytest from pathlib import Path +from unittest.mock import patch -from cpp_linter_hooks.clang_format import run_clang_format +from cpp_linter_hooks.clang_format import run_clang_format, main @pytest.mark.parametrize( @@ -64,3 +65,105 @@ def test_run_clang_format_dry_run(args, expected_retval, tmp_path): test_file = tmp_path / "main.c" ret, _ = run_clang_format(["--dry-run", str(test_file)]) assert ret == -1 # Dry run should not fail + + +def test_main_empty_output(): + """Test main() function when clang-format returns error with empty output""" + with patch( + "cpp_linter_hooks.clang_format.run_clang_format" + ) as mock_run_clang_format: + # Mock failed run with empty output + mock_run_clang_format.return_value = (1, "") + + with patch("builtins.print") as mock_print: + result = main() + + # Should return 1 and print empty string + assert result == 1 + mock_print.assert_called_once_with("") + + +def test_verbose_output(tmp_path, capsys): + """Test that verbose mode produces debug output to stderr""" + test_file = tmp_path / "test.c" + test_file.write_text("#include \nint main(){return 0;}") + + with patch("cpp_linter_hooks.clang_format.ensure_installed") as mock_ensure: + mock_ensure.return_value = "/fake/clang-format" + with patch( + "cpp_linter_hooks.clang_format.run_subprocess_with_logging" + ) as mock_run: + mock_run.return_value = (0, "") + + # Test verbose mode + retval, output = run_clang_format( + ["--verbose", "--style=Google", str(test_file)] + ) + + # Check that debug messages were printed to stderr + captured = capsys.readouterr() + assert "[DEBUG] clang-format version:" in captured.err + assert "[DEBUG] clang-format executable:" in captured.err + + # Verify that run_subprocess_with_logging was called correctly + mock_run.assert_called_once() + call_args = mock_run.call_args + assert call_args[1]["tool_name"] == "clang-format" + assert call_args[1]["verbose"] is True + + +def test_verbose_with_error(tmp_path, capsys): + """Test verbose output when there's an error""" + test_file = tmp_path / "test.c" + + with patch("cpp_linter_hooks.clang_format.ensure_installed") as mock_ensure: + mock_ensure.return_value = "/fake/clang-format" + with patch( + "cpp_linter_hooks.clang_format.run_subprocess_with_logging" + ) as mock_run: + mock_run.return_value = (1, "error output\nerror in stderr") + + # Test verbose mode with error + retval, output = run_clang_format( + ["--verbose", "--style=Google", str(test_file)] + ) + + # Check return values + assert retval == 1 + assert "error output" in output + assert "error in stderr" in output + + # Verify that run_subprocess_with_logging was called correctly + mock_run.assert_called_once() + call_args = mock_run.call_args + assert call_args[1]["tool_name"] == "clang-format" + assert call_args[1]["verbose"] is True + + +def test_verbose_dry_run(tmp_path, capsys): + """Test verbose output in dry-run mode""" + test_file = tmp_path / "test.c" + test_file.write_text("#include \nint main(){return 0;}") + + with patch("cpp_linter_hooks.clang_format.ensure_installed") as mock_ensure: + mock_ensure.return_value = "/fake/clang-format" + with patch( + "cpp_linter_hooks.clang_format.run_subprocess_with_logging" + ) as mock_run: + mock_run.return_value = (-1, "dry run output") + + # Test verbose dry-run mode + retval, output = run_clang_format( + ["--verbose", "--dry-run", str(test_file)] + ) + + # Check return values (dry-run should return -1) + assert retval == -1 + assert "dry run output" in output + + # Verify that run_subprocess_with_logging was called correctly + mock_run.assert_called_once() + call_args = mock_run.call_args + assert call_args[1]["tool_name"] == "clang-format" + assert call_args[1]["verbose"] is True + assert call_args[1]["dry_run"] is True diff --git a/tests/test_clang_tidy.py b/tests/test_clang_tidy.py index 2dfb4a8..0ef259c 100644 --- a/tests/test_clang_tidy.py +++ b/tests/test_clang_tidy.py @@ -1,6 +1,7 @@ import pytest import subprocess from pathlib import Path +from unittest.mock import patch from cpp_linter_hooks.clang_tidy import run_clang_tidy @@ -49,3 +50,102 @@ def test_run_clang_tidy_invalid(args, expected_retval, tmp_path): ret, _ = run_clang_tidy(args + [str(test_file)]) assert ret == expected_retval + + +def test_main_as_script(): + """Test the if __name__ == '__main__' behavior""" + with patch("cpp_linter_hooks.clang_tidy.main") as mock_main: + mock_main.return_value = 42 + + # This would normally raise SystemExit, but we're mocking main() + with pytest.raises(SystemExit) as exc_info: + # Simulate running the script directly + exec("if __name__ == '__main__': raise SystemExit(main())") + + assert exc_info.value.code == 42 + + +def test_verbose_output(tmp_path, capsys): + """Test that verbose mode produces debug output to stderr""" + test_file = tmp_path / "test.c" + test_file.write_text("#include \nint main(){return 0;}") + + with patch("cpp_linter_hooks.clang_tidy.ensure_installed") as mock_ensure: + mock_ensure.return_value = "/fake/clang-tidy" + with patch( + "cpp_linter_hooks.clang_tidy.run_subprocess_with_logging" + ) as mock_run: + mock_run.return_value = (0, "") + + # Test verbose mode + retval, output = run_clang_tidy( + ["--verbose", "--checks=boost-*", str(test_file)] + ) + + # Check that debug messages were printed to stderr + captured = capsys.readouterr() + assert "[DEBUG] clang-tidy version:" in captured.err + assert "[DEBUG] clang-tidy executable:" in captured.err + + # Verify that run_subprocess_with_logging was called correctly + mock_run.assert_called_once() + call_args = mock_run.call_args + assert call_args[1]["tool_name"] == "clang-tidy" + assert call_args[1]["verbose"] is True + + +def test_verbose_with_warnings(tmp_path, capsys): + """Test verbose output when there are warnings""" + test_file = tmp_path / "test.c" + + with patch("cpp_linter_hooks.clang_tidy.ensure_installed") as mock_ensure: + mock_ensure.return_value = "/fake/clang-tidy" + with patch( + "cpp_linter_hooks.clang_tidy.run_subprocess_with_logging" + ) as mock_run: + mock_run.return_value = ( + 1, + "warning: some issue found\ncompilation database warning", + ) + + # Test verbose mode with warnings + retval, output = run_clang_tidy( + ["--verbose", "--checks=boost-*", str(test_file)] + ) + + # Check return values (should be 1 due to warnings) + assert retval == 1 + assert "warning: some issue found" in output + assert "compilation database warning" in output + + # Verify that run_subprocess_with_logging was called correctly + mock_run.assert_called_once() + call_args = mock_run.call_args + assert call_args[1]["tool_name"] == "clang-tidy" + assert call_args[1]["verbose"] is True + + +def test_verbose_with_file_not_found(capsys): + """Test verbose output when clang-tidy executable is not found""" + with patch("cpp_linter_hooks.clang_tidy.ensure_installed") as mock_ensure: + mock_ensure.return_value = "/fake/clang-tidy" + with patch( + "cpp_linter_hooks.clang_tidy.run_subprocess_with_logging" + ) as mock_run: + mock_run.return_value = ( + 1, + "clang-tidy executable not found: No such file or directory", + ) + + # Test verbose mode with FileNotFoundError + retval, output = run_clang_tidy(["--verbose", "--checks=boost-*", "test.c"]) + + # Check return values + assert retval == 1 + assert "clang-tidy executable not found" in output + + # Verify that run_subprocess_with_logging was called correctly + mock_run.assert_called_once() + call_args = mock_run.call_args + assert call_args[1]["tool_name"] == "clang-tidy" + assert call_args[1]["verbose"] is True