Skip to content

feat: add _is_version_installed function and corresponding tests#187

Merged
shenxianpeng merged 1 commit intomainfrom
fix/skip-install
Mar 18, 2026
Merged

feat: add _is_version_installed function and corresponding tests#187
shenxianpeng merged 1 commit intomainfrom
fix/skip-install

Conversation

@shenxianpeng
Copy link
Member

@shenxianpeng shenxianpeng commented Mar 18, 2026

Summary by CodeRabbit

  • Refactor

    • Installation workflow now detects and prefers existing matching tool versions, avoiding unnecessary re-installation and speeding setup.
  • Tests

    • Added tests covering version-detection, matching/mismatched scenarios, and installation verification to ensure correct install/resume behavior.

@shenxianpeng shenxianpeng added the enhancement New feature or request label Mar 18, 2026
@github-actions github-actions bot added the bug Something isn't working label Mar 18, 2026
@codecov
Copy link

codecov bot commented Mar 18, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.72%. Comparing base (d083c25) to head (19d86c3).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #187      +/-   ##
==========================================
+ Coverage   95.41%   95.72%   +0.31%     
==========================================
  Files           4        4              
  Lines         109      117       +8     
==========================================
+ Hits          104      112       +8     
  Misses          5        5              

☔ 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.

@coderabbitai
Copy link

coderabbitai bot commented Mar 18, 2026

Warning

Rate limit exceeded

@shenxianpeng has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 10 minutes and 4 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1435ac19-54c0-4150-a7bc-7f7e4918ccfe

📥 Commits

Reviewing files that changed from the base of the PR and between db23d05 and 19d86c3.

📒 Files selected for processing (2)
  • cpp_linter_hooks/util.py
  • tests/test_util.py

Walkthrough

Added a private helper _is_version_installed() to detect whether a specific tool version is already present and return its Path. resolve_install() now checks for an existing matching installation via that helper before invoking installation logic.

Changes

Cohort / File(s) Summary
Version detection & install flow
cpp_linter_hooks/util.py
Added _is_version_installed(tool: str, version: str) -> Optional[Path] to detect an existing matching tool executable in PATH. Updated resolve_install() to prefer the detected executable and skip re-installation when versions match; otherwise fall back to _install_tool.
Tests for detection & install behavior
tests/test_util.py
Added tests covering _is_version_installed() (tool not in PATH, version match, version mismatch) and extended resolve_install() tests to validate behavior when a matching version exists, when a mismatch triggers reinstall, and when installation via _install_tool is required. Uses mocks for shutil.which and subprocess.run.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 73.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 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: adding a new _is_version_installed function with tests. It is concise, specific, and clearly conveys the primary modification.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/skip-install
📝 Coding Plan
  • Generate coding plan for human review comments

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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/test_util.py (1)

336-351: ⚠️ Potential issue | 🟡 Minor

Test may not reflect actual code behavior when version is None.

This test mocks _install_tool but not _is_version_installed. With shutil.which returning None, _is_version_installed returns None without hitting the version check, so the test passes. However, if shutil.which returned a valid path, _is_version_installed would crash on version in result.stdout with None version.

Consider either:

  1. Fixing _is_version_installed to handle None (as suggested in util.py), or
  2. Explicitly mocking _is_version_installed in this test to make the test intent clearer
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_util.py` around lines 336 - 351, The test
test_resolve_install_with_none_default_version is relying on shutil.which
returning None so _is_version_installed is never exercised; to make the test
intent explicit, mock cpp_linter_hooks.util._is_version_installed in the test to
return False (or an appropriate sentinel) so resolve_install("clang-format",
None) follows the install path and you can assert _install_tool was called;
alternatively, if you prefer a code fix, update util._is_version_installed to
guard against a None version (e.g., return False immediately if version is None)
so calling _is_version_installed(version=None, ...) cannot crash when it checks
result.stdout.
🧹 Nitpick comments (2)
tests/test_util.py (1)

121-163: Consider adding a test for None version input.

The tests cover the main scenarios well. However, there's no test for when version is None, which can occur when pyproject.toml is missing and defaults are unavailable. Adding this test would ensure the edge case is properly handled (once the fix is applied).

🧪 Suggested additional test
`@pytest.mark.benchmark`
def test_is_version_installed_none_version():
    """Test _is_version_installed when version is None."""
    mock_path = "/usr/bin/clang-format"

    with patch("shutil.which", return_value=mock_path):
        result = _is_version_installed("clang-format", None)
        assert result is None
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_util.py` around lines 121 - 163, Add a unit test to cover the edge
case where version is None for the helper _is_version_installed: simulate the
tool existing in PATH by patching shutil.which to return a mock path (e.g.,
"/usr/bin/clang-format"), call _is_version_installed("clang-format", None) and
assert it returns None; place this new test (e.g.,
test_is_version_installed_none_version) alongside the other tests in
tests/test_util.py and follow the same pytest.mark.benchmark decoration and
mocking style used by test_is_version_installed_not_in_path.
cpp_linter_hooks/util.py (1)

67-69: Substring version check may produce false positives.

Using version in result.stdout can incorrectly match similar versions. For example, checking for "20.1.7" would match output containing "20.1.70". Consider using a word-boundary check or parsing the version more precisely.

♻️ Suggested fix using regex word boundary
+import re
+
 def _is_version_installed(tool: str, version: str) -> Optional[Path]:
     """Return the tool path if the installed version matches, otherwise None."""
     existing = shutil.which(tool)
     if not existing:
         return None
     result = subprocess.run([existing, "--version"], capture_output=True, text=True)
-    if version in result.stdout:
+    if re.search(rf"\b{re.escape(version)}\b", result.stdout):
         return Path(existing)
     return None
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp_linter_hooks/util.py` around lines 67 - 69, The current substring check
"version in result.stdout" can yield false positives; update the check in
util.py where subprocess.run([...], ...) returns result to perform a precise
match (e.g., use regex word-boundary matching or parse the version) — replace
the naive membership test on result.stdout with a call like
re.search(rf'\\b{re.escape(version)}\\b', result.stdout) (and import re) or
otherwise parse the tool's version output to compare exact components, keeping
variables existing, version, and result as the referenced identifiers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cpp_linter_hooks/util.py`:
- Around line 62-70: The _is_version_installed function can raise TypeError when
its version parameter is None; update _is_version_installed to explicitly guard
against a None version before checking membership in result.stdout (the check
"version in result.stdout"), e.g., return None immediately if version is None or
convert to a safe string, so the subprocess output check only runs when version
is a non-None string; modify the function surrounding the variables existing,
result, and version to perform this None check and return None early if needed.

---

Outside diff comments:
In `@tests/test_util.py`:
- Around line 336-351: The test test_resolve_install_with_none_default_version
is relying on shutil.which returning None so _is_version_installed is never
exercised; to make the test intent explicit, mock
cpp_linter_hooks.util._is_version_installed in the test to return False (or an
appropriate sentinel) so resolve_install("clang-format", None) follows the
install path and you can assert _install_tool was called; alternatively, if you
prefer a code fix, update util._is_version_installed to guard against a None
version (e.g., return False immediately if version is None) so calling
_is_version_installed(version=None, ...) cannot crash when it checks
result.stdout.

---

Nitpick comments:
In `@cpp_linter_hooks/util.py`:
- Around line 67-69: The current substring check "version in result.stdout" can
yield false positives; update the check in util.py where subprocess.run([...],
...) returns result to perform a precise match (e.g., use regex word-boundary
matching or parse the version) — replace the naive membership test on
result.stdout with a call like re.search(rf'\\b{re.escape(version)}\\b',
result.stdout) (and import re) or otherwise parse the tool's version output to
compare exact components, keeping variables existing, version, and result as the
referenced identifiers.

In `@tests/test_util.py`:
- Around line 121-163: Add a unit test to cover the edge case where version is
None for the helper _is_version_installed: simulate the tool existing in PATH by
patching shutil.which to return a mock path (e.g., "/usr/bin/clang-format"),
call _is_version_installed("clang-format", None) and assert it returns None;
place this new test (e.g., test_is_version_installed_none_version) alongside the
other tests in tests/test_util.py and follow the same pytest.mark.benchmark
decoration and mocking style used by test_is_version_installed_not_in_path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6a7e2a53-1018-4a1d-8ec7-097cd21f385b

📥 Commits

Reviewing files that changed from the base of the PR and between 91a795b and ccf80d5.

📒 Files selected for processing (2)
  • cpp_linter_hooks/util.py
  • tests/test_util.py

@codspeed-hq
Copy link

codspeed-hq bot commented Mar 18, 2026

Merging this PR will degrade performance by 28.84%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

❌ 15 regressed benchmarks
✅ 45 untouched benchmarks
🆕 3 new benchmarks
⏩ 13 skipped benchmarks1

⚠️ Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
🆕 test_is_version_installed_not_in_path N/A 503.2 µs N/A
test_run_clang_tidy_valid[args5-1] 798.7 µs 894.7 µs -10.73%
test_run_clang_tidy_valid[args2-1] 801.6 µs 920.3 µs -12.89%
🆕 test_is_version_installed_version_mismatch N/A 841.2 µs N/A
🆕 test_is_version_installed_version_matches N/A 916.4 µs N/A
test_run_clang_tidy_valid[args4-1] 802.1 µs 1,071 µs -25.11%
test_resolve_install_tool_already_installed_correct_version 815.1 µs 926.4 µs -12.01%
test_run_clang_tidy_valid[args6-1] 803.2 µs 892.7 µs -10.03%
test_resolve_install_tool_version_mismatch 903.7 µs 1,270 µs -28.84%
test_run_clang_tidy_invalid[args3-1] 740 µs 842.1 µs -12.13%
test_run_clang_tidy_valid[args0-1] 776.6 µs 872.5 µs -11%
test_run_clang_tidy_invalid[args0-1] 757.3 µs 879.3 µs -13.87%
test_run_clang_tidy_invalid[args5-1] 737.4 µs 839.2 µs -12.12%
test_run_clang_tidy_invalid[args2-1] 740.1 µs 875.1 µs -15.42%
test_run_clang_tidy_invalid[args4-1] 748.9 µs 1,024.2 µs -26.88%
test_run_clang_tidy_valid[args1-1] 797.2 µs 917.2 µs -13.08%
test_run_clang_tidy_invalid[args6-1] 742.9 µs 847 µs -12.3%
test_run_clang_tidy_invalid[args1-1] 745.5 µs 878.1 µs -15.1%

Comparing fix/skip-install (19d86c3) with main (f63ed4a)2

Open in CodSpeed

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.

  2. No successful run was found on main (d083c25) during the generation of this report, so f63ed4a was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/test_util.py (1)

336-351: ⚠️ Potential issue | 🟡 Minor

Misleading comment and potential production issue.

The comment states "Should fallback to hardcoded version when DEFAULT is None" but the assertion shows _install_tool is called with None as the version. This would result in pip install clang-format==None, which will fail.

Either:

  1. The production code should have a hardcoded fallback (and test updated to reflect that), or
  2. The comment should be corrected to reflect the actual behavior
✏️ Option 1: Fix comment to match actual behavior
-        # Should fallback to hardcoded version when DEFAULT is None
+        # When DEFAULT is None and no version specified, None is passed through
         mock_install.assert_called_once_with("clang-format", None)
✏️ Option 2: Add hardcoded fallback in production code and update test

In cpp_linter_hooks/util.py, add a fallback:

FALLBACK_VERSION = "20.1.0"  # Or appropriate default

Then in resolve_install, after setting user_version from defaults:

if user_version is None:
    user_version = FALLBACK_VERSION

And update this test's assertion accordingly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_util.py` around lines 336 - 351, The test comment and assertion
are inconsistent with production behavior: resolve_install currently passes None
to _install_tool when DEFAULT_CLANG_FORMAT_VERSION/DEFAULT_CLANG_TIDY_VERSION
are None, which would try to install "==None"; fix by adding a real fallback
version constant (e.g., FALLBACK_VERSION) in cpp_linter_hooks.util and update
resolve_install to use FALLBACK_VERSION when the default/user version is None,
then update test_resolve_install_with_none_default_version to assert
mock_install.assert_called_once_with("clang-format", FALLBACK_VERSION) (or
alternatively, if you prefer to keep current behavior, change the test comment
to state that None is forwarded to _install_tool and leave resolve_install
unchanged).
🧹 Nitpick comments (1)
tests/test_util.py (1)

121-164: Add a test case for None version parameter.

The tests cover the main scenarios well, but there's no test verifying behavior when version is None. This would help validate the fix for the potential TypeError in _is_version_installed.

🧪 Proposed additional test
`@pytest.mark.benchmark`
def test_is_version_installed_none_version():
    """Test _is_version_installed when version is None."""
    mock_path = "/usr/bin/clang-format"

    with patch("shutil.which", return_value=mock_path):
        result = _is_version_installed("clang-format", None)
        assert result is None
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_util.py` around lines 121 - 164, Add a new test in
tests/test_util.py that calls _is_version_installed("clang-format", None) and
asserts it returns None; mock shutil.which to return a fake path (e.g.,
"/usr/bin/clang-format") so the function proceeds to the early-version handling
and ensure no exception is raised—place the test alongside the other
test_is_version_installed_* tests and use the same pytest.mark.benchmark
decorator and patching style used in test_is_version_installed_version_matches
to keep consistency.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@tests/test_util.py`:
- Around line 336-351: The test comment and assertion are inconsistent with
production behavior: resolve_install currently passes None to _install_tool when
DEFAULT_CLANG_FORMAT_VERSION/DEFAULT_CLANG_TIDY_VERSION are None, which would
try to install "==None"; fix by adding a real fallback version constant (e.g.,
FALLBACK_VERSION) in cpp_linter_hooks.util and update resolve_install to use
FALLBACK_VERSION when the default/user version is None, then update
test_resolve_install_with_none_default_version to assert
mock_install.assert_called_once_with("clang-format", FALLBACK_VERSION) (or
alternatively, if you prefer to keep current behavior, change the test comment
to state that None is forwarded to _install_tool and leave resolve_install
unchanged).

---

Nitpick comments:
In `@tests/test_util.py`:
- Around line 121-164: Add a new test in tests/test_util.py that calls
_is_version_installed("clang-format", None) and asserts it returns None; mock
shutil.which to return a fake path (e.g., "/usr/bin/clang-format") so the
function proceeds to the early-version handling and ensure no exception is
raised—place the test alongside the other test_is_version_installed_* tests and
use the same pytest.mark.benchmark decorator and patching style used in
test_is_version_installed_version_matches to keep consistency.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f5b075c9-2054-40b6-83a6-2df7c60e5199

📥 Commits

Reviewing files that changed from the base of the PR and between ccf80d5 and db23d05.

📒 Files selected for processing (2)
  • cpp_linter_hooks/util.py
  • tests/test_util.py

@shenxianpeng shenxianpeng requested a review from Copilot March 18, 2026 19:52
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a utility helper to detect whether a requested clang tool version is already present on PATH and updates the install resolution logic (plus tests) to reuse an existing matching installation instead of always reinstalling.

Changes:

  • Added _is_version_installed(tool, version) to check tool --version output and return the executable path on a match.
  • Updated resolve_install() to prefer an already-installed matching version before falling back to _install_tool().
  • Extended tests/test_util.py to cover version-detection and correct/mismatched scenarios.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
cpp_linter_hooks/util.py Adds _is_version_installed and updates resolve_install to short-circuit installation when a matching version is already available.
tests/test_util.py Adds tests for _is_version_installed and adjusts resolve_install tests to account for version checking via --version.

You can also share your feedback on Copilot code review. Take the survey.

@shenxianpeng shenxianpeng removed the bug Something isn't working label Mar 18, 2026
@sonarqubecloud
Copy link

@shenxianpeng shenxianpeng merged commit 8b18318 into main Mar 18, 2026
18 of 19 checks passed
@shenxianpeng shenxianpeng deleted the fix/skip-install branch March 18, 2026 20:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants