Skip to content

Conversation

@shenxianpeng
Copy link
Collaborator

@shenxianpeng shenxianpeng commented Nov 4, 2025

closes #141

Summary by CodeRabbit

  • Refactor

    • Improved Python 3.11+ compatibility by selecting the modern TOML parser when available; modernized tool installation to use return-code-based invocation with captured output and clearer logging on failure while preserving public APIs and behavior.
  • Tests

    • Updated tests to validate the run-based installation flow and enhanced success/failure assertions capturing stdout/stderr.
@github-actions github-actions bot added the bug Something isn't working label Nov 4, 2025
@2bndy5 2bndy5 force-pushed the fix-141 branch 3 times, most recently from c04607c to edf3d32 Compare November 4, 2025 20:42
@codecov
Copy link

codecov bot commented Nov 4, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.41%. Comparing base (96fbc91) to head (3f12fff).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@ Coverage Diff @@ ## main #142 +/- ## ========================================== + Coverage 94.44% 95.41% +0.96%  ========================================== Files 4 4 Lines 108 109 +1 ========================================== + Hits 102 104 +2  + Misses 6 5 -1 

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
@2bndy5
Copy link
Contributor

2bndy5 commented Nov 4, 2025

I had to get rid of the try ... except blocks. In that scenario we would have had to pipe stdout and stderr to open temporary files.

Instead, I just checked the exit code and return None if error and which() output if success. This way the stderr and stdout is captured regardless.

@codspeed-hq
Copy link

codspeed-hq bot commented Nov 4, 2025

CodSpeed Performance Report

Merging #142 will degrade performances by 43.16%

Comparing fix-141 (3f12fff) with main (96fbc91)

Summary

⚡ 2 improvements
❌ 25 (👁 25) regressions
✅ 33 untouched
⏩ 13 skipped1

Benchmarks breakdown

Benchmark BASE HEAD Change
👁 test_run_clang_format_dry_run[args0-1] 697.3 µs 798.6 µs -12.69%
👁 test_run_clang_format_invalid[args0-1] 700.6 µs 800.8 µs -12.51%
👁 test_run_clang_format_invalid[args1-1] 749.4 µs 842.8 µs -11.08%
👁 test_run_clang_format_invalid[args2-1] 744.9 µs 837.6 µs -11.07%
👁 test_run_clang_format_invalid[args3-1] 758.2 µs 850.6 µs -10.87%
👁 test_run_clang_format_invalid[args4-1] 751.7 µs 846.3 µs -11.18%
👁 test_run_clang_format_invalid[args5-1] 748.2 µs 847.6 µs -11.73%
👁 test_run_clang_format_invalid[args6-1] 732.8 µs 835.9 µs -12.34%
👁 test_run_clang_format_verbose_error 808.7 µs 903.9 µs -10.54%
👁 test_run_clang_tidy_invalid[args0-1] 600.9 µs 713.7 µs -15.82%
👁 test_run_clang_tidy_invalid[args1-1] 631.2 µs 742.3 µs -14.97%
👁 test_run_clang_tidy_invalid[args2-1] 623.4 µs 737.4 µs -15.46%
👁 test_run_clang_tidy_invalid[args3-1] 624.3 µs 740.5 µs -15.69%
👁 test_run_clang_tidy_invalid[args4-1] 631.6 µs 744.5 µs -15.17%
👁 test_run_clang_tidy_invalid[args5-1] 623 µs 735.8 µs -15.33%
👁 test_run_clang_tidy_invalid[args6-1] 625.4 µs 738.8 µs -15.35%
👁 test_run_clang_tidy_valid[args0-1] 664.9 µs 777.6 µs -14.49%
👁 test_run_clang_tidy_valid[args1-1] 683 µs 796.6 µs -14.26%
👁 test_run_clang_tidy_valid[args2-1] 687.2 µs 800.8 µs -14.19%
👁 test_run_clang_tidy_valid[args3-1] 686.3 µs 799.4 µs -14.15%
... ... ... ... ...

ℹ️ Only the first 20 benchmarks are displayed. Go to the app to view all benchmarks.

Footnotes

  1. 13 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@2bndy5
Copy link
Contributor

2bndy5 commented Nov 4, 2025

The performance will decrease because this wasn't capturing the stdout or stderr streams previously.

@shenxianpeng
Copy link
Collaborator Author

make sense

@shenxianpeng shenxianpeng marked this pull request as ready for review November 4, 2025 21:07
@coderabbitai
Copy link

coderabbitai bot commented Nov 4, 2025

Walkthrough

Selects tomllib on Python ≥3.11 (falls back to tomli), changes _install_tool() to use subprocess.run(..., capture_output=True, text=True) with return-code-based handling; on success returns shutil.which(tool), on failure logs stdout/stderr and returns None. Tests updated to mock subprocess.run.

Changes

Cohort / File(s) Summary
Implementation
cpp_linter_hooks/util.py
Add Python-version guard to import tomllib (fallback to tomli). Replace subprocess.check_call usage in _install_tool() with subprocess.run(..., capture_output=True, text=True) and return shutil.which(tool) on success; on non-zero return code log stdout/stderr and return None. Update docstring to mention logging on failure.
Tests
tests/test_util.py
Replace mocks of subprocess.check_call with mocks of subprocess.run. Success tests use CompletedProcess(returncode=0); failure tests use CompletedProcess with non-zero returncode and populated stdout/stderr. Adjust assertions to verify run(..., capture_output=True, text=True) invocation and logged output handling.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

  • Verify capture_output=True, text=True is used consistently in implementation and tests.
  • Confirm logged stdout/stderr are strings and tests assert logged messages correctly.
  • Check behavior when shutil.which returns None (tool not found) is covered by tests.

Possibly related PRs

Pre-merge checks and finishing touches

✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: improving error handling by printing stdout and stderr when _install_tool encounters errors.
Linked Issues check ✅ Passed The implementation meets issue #141's requirements: captures pip stdout/stderr on failure and logs them, though using subprocess.run with return-code checks instead of the proposed try/except with CalledProcessError.
Out of Scope Changes check ✅ Passed All changes are directly related to issue #141: the tomllib import guard update and subprocess refactoring support the core objective of capturing and logging pip output on errors.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-141

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
tests/test_util.py (2)

146-156: Add assertions to verify error logging.

The test patches LOG but doesn't verify that LOG.error was called with the decoded stdout/stderr. Since logging the output on failure is a key objective of this PR, the test should validate this behavior.

Apply this diff:

 with ( patch("subprocess.run", side_effect=patched_run), - patch("cpp_linter_hooks.util.LOG"), + patch("cpp_linter_hooks.util.LOG") as mock_log, ): result = _install_tool("clang-format", "20.1.7") assert result is None +  + # Verify error logging + assert mock_log.error.call_count == 3 + mock_log.error.assert_any_call("pip failed to install %s %s", "clang-format", "20.1.7") + mock_log.error.assert_any_call("Installation failed") + mock_log.error.assert_any_call("Error")

162-164: Make the subprocess.run mock explicit.

The test patches subprocess.run without specifying a return value, which implicitly returns a MagicMock. For clarity and maintainability, explicitly return a CompletedProcess with returncode=0.

Apply this diff:

- with patch("subprocess.run"), patch("shutil.which", return_value=None): + def patched_run(*args, **kwargs): + return subprocess.CompletedProcess(args, returncode=0) +  + with ( + patch("subprocess.run", side_effect=patched_run), + patch("shutil.which", return_value=None), + ): result = _install_tool("clang-format", "20.1.7") assert result is None
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 069417d and edf3d32.

📒 Files selected for processing (2)
  • cpp_linter_hooks/util.py (2 hunks)
  • tests/test_util.py (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
tests/test_util.py (1)
cpp_linter_hooks/util.py (1)
  • _install_tool (62-74)
🪛 Ruff (0.14.3)
cpp_linter_hooks/util.py

64-64: subprocess call: check for execution of untrusted input

(S603)

tests/test_util.py

126-126: Unused function argument: kwargs

(ARG001)


146-146: Unused function argument: kwargs

(ARG001)

🔇 Additional comments (2)
cpp_linter_hooks/util.py (1)

8-11: LGTM: Clean version-guarded import.

The version-guarded import for tomllib/tomli is well-structured and follows best practices for supporting both Python 3.11+ (stdlib tomllib) and earlier versions (tomli package).

tests/test_util.py (1)

126-139: LGTM: Test correctly updated for subprocess.run.

The test properly mocks subprocess.run with a CompletedProcess and verifies that capture_output=True is passed, aligning with the implementation changes.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
tests/test_util.py (1)

126-127: Optional: Remove unused kwargs parameter.

The kwargs parameter is unused and flagged by Ruff (ARG001). Consider using *args only if no keyword arguments are expected, or remove both if neither is used.

Apply this diff:

- def patched_run(*args, **kwargs): + def patched_run(*args): return subprocess.CompletedProcess(args, returncode=0)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 193effb and f65b08f.

📒 Files selected for processing (2)
  • cpp_linter_hooks/util.py (2 hunks)
  • tests/test_util.py (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
tests/test_util.py (1)
cpp_linter_hooks/util.py (1)
  • _install_tool (62-75)
🪛 Ruff (0.14.3)
tests/test_util.py

126-126: Unused function argument: kwargs

(ARG001)


147-147: Unused function argument: kwargs

(ARG001)

cpp_linter_hooks/util.py

64-64: subprocess call: check for execution of untrusted input

(S603)

🔇 Additional comments (2)
cpp_linter_hooks/util.py (2)

8-11: LGTM! Clean version-specific import.

Using sys.version_info for the conditional import is more explicit and idiomatic than try/except.


62-75: LGTM! Implementation correctly captures and logs output on failure.

The use of text=True automatically handles encoding, eliminating UnicodeDecodeError risks. The error logging on non-zero return codes addresses the original issue effectively.

Note: The static analysis S603 warning about untrusted subprocess input is a false positive—sys.executable is a trusted system path.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
tests/test_util.py (1)

126-140: LGTM! Test correctly updated for subprocess.run.

The test properly mocks subprocess.run to return a CompletedProcess with returncode=0 and verifies the call includes capture_output=True and text=True, matching the actual implementation.

The unused kwargs parameter (flagged by Ruff ARG001) could optionally be removed or prefixed with _ to suppress the warning, but accepting **kwargs is a common pattern in test mocks for flexibility.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f65b08f and 02db34f.

📒 Files selected for processing (2)
  • cpp_linter_hooks/util.py (2 hunks)
  • tests/test_util.py (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
tests/test_util.py (1)
cpp_linter_hooks/util.py (1)
  • _install_tool (62-75)
🪛 Ruff (0.14.3)
tests/test_util.py

126-126: Unused function argument: kwargs

(ARG001)


147-147: Unused function argument: kwargs

(ARG001)


164-164: Unused function argument: kwargs

(ARG001)

cpp_linter_hooks/util.py

64-64: subprocess call: check for execution of untrusted input

(S603)

🔇 Additional comments (4)
cpp_linter_hooks/util.py (2)

8-11: LGTM! Proper version-gated import.

The version check correctly uses tomllib from the standard library on Python 3.11+ and falls back to the tomli package for older versions. This is the idiomatic approach for handling this transition.


62-75: LGTM! Implementation correctly captures and logs pip output.

The refactored implementation properly addresses the PR objectives:

  • Uses subprocess.run with capture_output=True to capture stdout/stderr
  • Uses text=True to automatically handle string decoding (avoiding encoding issues)
  • Logs both stdout and stderr when pip fails (returncode != 0)
  • Returns None on failure or the tool path on success

The static analysis hint (S603) about untrusted input is a false positive here—tool and version are controlled parameters resolved from the codebase's version lists and pyproject.toml, not arbitrary user input.

tests/test_util.py (2)

147-150: LGTM! Test correctly simulates pip failure with string output.

The mock now correctly returns stdout and stderr as strings (not bytes), which matches the behavior of subprocess.run with text=True. This addresses the previous review comment about data type mismatch.


164-172: LGTM! Test now correctly simulates successful pip install.

The mock properly returns a CompletedProcess with returncode=0, ensuring the code follows the success path where result.returncode == 0 evaluates to True. This addresses the previous review comment where the mock wasn't simulating success correctly. The shutil.which returning None properly tests the edge case where installation succeeds but the tool isn't found in PATH.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
cpp_linter_hooks/util.py (1)

62-74: Consider logging when the tool isn't found in PATH despite successful installation.

The implementation correctly captures and logs pip output on failure. However, if pip succeeds but shutil.which(tool) returns None (Line 70), this edge case goes unlogged. This could happen if pip's install location isn't in PATH or the executable name differs from the package name.

Apply this diff to add diagnostic logging:

 if result.returncode == 0: - return shutil.which(tool) + tool_path = shutil.which(tool) + if tool_path is None: + LOG.warning("pip install succeeded but %s not found in PATH", tool) + return tool_path LOG.error("pip failed to install %s %s", tool, version)

Static analysis warning S603 is a false positive.

Ruff flagged the subprocess call as potentially executing untrusted input. However, tool is constrained to "clang-format" or "clang-tidy" in resolve_install (Line 77-90), and version is validated against CLANG_FORMAT_VERSIONS/CLANG_TIDY_VERSIONS or defaults through _resolve_version. The inputs are effectively whitelisted.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 02db34f and 755dbf4.

📒 Files selected for processing (1)
  • cpp_linter_hooks/util.py (2 hunks)
🧰 Additional context used
🪛 Ruff (0.14.3)
cpp_linter_hooks/util.py

64-64: subprocess call: check for execution of untrusted input

(S603)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: test (3.10)
  • GitHub Check: test (3.9)
  • GitHub Check: test (3.14)
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.13)
  • GitHub Check: Run benchmarks
🔇 Additional comments (1)
cpp_linter_hooks/util.py (1)

8-11: LGTM! Cleaner version-based import.

Using sys.version_info >= (3, 11) is the idiomatic approach for version-specific imports and is more explicit than try/except for this use case.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
cpp_linter_hooks/util.py (1)

8-11: Unrelated but correct refactor.

This change from try/except to an explicit version check is functionally equivalent and slightly clearer. However, it's unrelated to the PR's stated objective (fixing pip output logging on errors).

tests/test_util.py (1)

126-127: Silence the linter warnings for unused kwargs.

The **kwargs parameter in patched_run is necessary because subprocess.run is invoked with keyword arguments (capture_output=True, text=True). Although the parameter is not used within the function body, removing it would cause the mock to fail when called.

To silence Ruff's ARG001 warnings, prefix the parameter with an underscore:

-def patched_run(*args, **kwargs): +def patched_run(*args, **_kwargs):

Apply this change to all three patched_run definitions (lines 126, 147, and 164).

Also applies to: 147-150, 164-165

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 755dbf4 and 3f12fff.

📒 Files selected for processing (2)
  • cpp_linter_hooks/util.py (2 hunks)
  • tests/test_util.py (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
tests/test_util.py (1)
cpp_linter_hooks/util.py (1)
  • _install_tool (62-74)
🪛 Ruff (0.14.3)
tests/test_util.py

126-126: Unused function argument: kwargs

(ARG001)


147-147: Unused function argument: kwargs

(ARG001)


164-164: Unused function argument: kwargs

(ARG001)

cpp_linter_hooks/util.py

64-64: subprocess call: check for execution of untrusted input

(S603)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run benchmarks
🔇 Additional comments (2)
cpp_linter_hooks/util.py (1)

62-74: Implementation correctly addresses the PR objective.

The function now captures stdout and stderr using subprocess.run with capture_output=True and text=True, logging both streams on failure. This successfully resolves issue #141.

Regarding the static analysis hint (S603): the warning about untrusted input is a false positive. The command executes sys.executable (the current Python interpreter) with internally validated arguments—tool and version are resolved and validated by _resolve_version before reaching this function.

tests/test_util.py (1)

126-172: Test coverage appropriately validates the new behavior.

The three test cases correctly cover success, failure, and edge-case scenarios (installation succeeds but tool not found in PATH). The mocks properly simulate subprocess.run with capture_output=True and text=True, and stdout/stderr are correctly provided as strings.

Copy link
Contributor

@2bndy5 2bndy5 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have nothing left to add here

@shenxianpeng shenxianpeng merged commit 7882d75 into main Nov 5, 2025
20 checks passed
@shenxianpeng shenxianpeng deleted the fix-141 branch November 5, 2025 07:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

3 participants