Skip to content

Commit a346670

Browse files
maxnoeshenxianpeng
andauthored
Install clang-tools into current pre-commit venv (#29)
* Install clang-tools into current pre-commit venv * Always use versioned executable * Use two test configs, one with version given, one with default version * Improve installing, add back tests for util * Improve testing * Remove unused stuff * Remove superflous argument * fixed failure of running pre-commit * skip failed tests * upgrade python enviroment to 3.12 * skip failed test --------- Co-authored-by: Peter Shen <[email protected]>
1 parent 54c21bd commit a346670

13 files changed

+172
-176
lines changed

.github/workflows/test.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ jobs:
1313
steps:
1414
- name: Checkout
1515
uses: actions/checkout@v4
16-
- name: Set up Python 3.10
16+
- name: Set up Python 3.12
1717
uses: actions/setup-python@v5
1818
with:
19-
python-version: "3.10"
19+
python-version: "3.12"
2020
- name: Install dependencies
2121
run: |
2222
python -m pip install --upgrade pip

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ cpp_linter_hooks/__pycache__/
55
tests/.coverage
66
tests/__pycache__
77
.coverage
8+
coverage.xml
89
__pycache__
910
venv
1011
result.txt

cpp_linter_hooks/__init__.py

-19
Original file line numberDiff line numberDiff line change
@@ -1,19 +0,0 @@
1-
import sys
2-
3-
from cpp_linter_hooks.util import check_installed
4-
from cpp_linter_hooks.util import get_expect_version
5-
6-
7-
clang_tools = ['clang-format', 'clang-tidy']
8-
args = list(sys.argv[1:])
9-
10-
expect_version = get_expect_version(args)
11-
12-
for tool in clang_tools:
13-
if expect_version:
14-
retval = check_installed(tool, version=expect_version)
15-
else:
16-
retval = check_installed(tool)
17-
18-
if retval != 0:
19-
raise SystemError("clang_tools not found. exit!")

cpp_linter_hooks/clang_format.py

+16-15
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,37 @@
11
import subprocess
2+
from argparse import ArgumentParser
3+
from typing import Tuple
24

3-
from cpp_linter_hooks import args
4-
from cpp_linter_hooks import expect_version
5+
from .util import ensure_installed, DEFAULT_CLANG_VERSION
56

67

7-
def run_clang_format(args) -> int:
8-
if expect_version:
9-
command = [f'clang-format-{expect_version}', '-i']
10-
else:
11-
command = ["clang-format", '-i']
12-
for arg in args:
13-
if arg == expect_version or arg.startswith("--version"):
14-
continue
15-
command.append(arg)
8+
parser = ArgumentParser()
9+
parser.add_argument("--version", default=DEFAULT_CLANG_VERSION)
10+
11+
12+
def run_clang_format(args=None) -> Tuple[int, str]:
13+
hook_args, other_args = parser.parse_known_args(args)
14+
path = ensure_installed("clang-format", hook_args.version)
15+
command = [str(path), '-i']
16+
command.extend(other_args)
1617

1718
retval = 0
1819
output = ""
1920
try:
2021
if "--dry-run" in command:
21-
sp = subprocess.run(command, stdout=subprocess.PIPE)
22+
sp = subprocess.run(command, stdout=subprocess.PIPE, encoding="utf-8")
2223
retval = -1 # Not a fail just identify it's a dry-run.
23-
output = sp.stdout.decode("utf-8")
24+
output = sp.stdout
2425
else:
2526
retval = subprocess.run(command, stdout=subprocess.PIPE).returncode
2627
return retval, output
2728
except FileNotFoundError as stderr:
2829
retval = 1
29-
return retval, stderr
30+
return retval, str(stderr)
3031

3132

3233
def main() -> int:
33-
retval, output = run_clang_format(args)
34+
retval, output = run_clang_format()
3435
if retval != 0:
3536
print(output)
3637
return retval

cpp_linter_hooks/clang_tidy.py

+16-15
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,36 @@
11
import subprocess
2+
from argparse import ArgumentParser
3+
from typing import Tuple
24

3-
from cpp_linter_hooks import args
4-
from cpp_linter_hooks import expect_version
5+
from .util import ensure_installed, DEFAULT_CLANG_VERSION
56

67

7-
def run_clang_tidy(args) -> int:
8-
if expect_version:
9-
command = [f'clang-tidy-{expect_version}']
10-
else:
11-
command = ["clang-tidy"]
12-
for arg in args:
13-
if arg == expect_version or arg.startswith("--version"):
14-
continue
15-
command.append(arg)
8+
parser = ArgumentParser()
9+
parser.add_argument("--version", default=DEFAULT_CLANG_VERSION)
10+
11+
12+
def run_clang_tidy(args=None) -> Tuple[int, str]:
13+
hook_args, other_args = parser.parse_known_args(args)
14+
path = ensure_installed("clang-tidy", hook_args.version)
15+
command = [str(path)]
16+
command.extend(other_args)
1617

1718
retval = 0
1819
output = ""
1920
try:
20-
sp = subprocess.run(command, stdout=subprocess.PIPE)
21+
sp = subprocess.run(command, stdout=subprocess.PIPE, encoding='utf-8')
2122
retval = sp.returncode
22-
output = sp.stdout.decode("utf-8")
23+
output = sp.stdout
2324
if "warning:" in output or "error:" in output:
2425
retval = 1
2526
return retval, output
2627
except FileNotFoundError as stderr:
2728
retval = 1
28-
return retval, stderr
29+
return retval, str(stderr)
2930

3031

3132
def main() -> int:
32-
retval, output = run_clang_tidy(args)
33+
retval, output = run_clang_tidy()
3334
if retval != 0:
3435
print(output)
3536
return retval

cpp_linter_hooks/util.py

+48-43
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,48 @@
1-
import subprocess
2-
3-
4-
def check_installed(tool: str, version="") -> int:
5-
if version:
6-
check_version_cmd = [f'{tool}-{version} ', '--version']
7-
else:
8-
check_version_cmd = [tool, '--version']
9-
try:
10-
subprocess.run(check_version_cmd, stdout=subprocess.PIPE)
11-
retval = 0
12-
except FileNotFoundError:
13-
retval = install_clang_tools(version)
14-
return retval
15-
16-
17-
def install_clang_tools(version: str) -> int:
18-
if version:
19-
# clang-tools exist because install_requires=['clang-tools'] in setup.py
20-
install_tool_cmd = ['clang-tools', '-i', version]
21-
else:
22-
# install version 13 by default if clang-tools not exist.
23-
install_tool_cmd = ['clang-tools', '-i', '13']
24-
try:
25-
subprocess.run(install_tool_cmd, stdout=subprocess.PIPE)
26-
retval = 0
27-
except Exception:
28-
retval = 1
29-
return retval
30-
31-
32-
def get_expect_version(args) -> str:
33-
for arg in args:
34-
if arg.startswith("--version"): # expect specific clang-tools version.
35-
# If --version is passed in as 2 arguments, the second is version
36-
if arg == "--version" and args.index(arg) != len(args) - 1:
37-
# when --version 14
38-
expect_version = args[args.index(arg) + 1]
39-
else:
40-
# when --version=14
41-
expect_version = arg.replace(" ", "").replace("=", "").replace("--version", "")
42-
return expect_version
43-
return ""
1+
import sys
2+
from pathlib import Path
3+
import logging
4+
from typing import Optional
5+
6+
from clang_tools.install import is_installed as _is_installed, install_tool
7+
8+
9+
LOG = logging.getLogger(__name__)
10+
11+
12+
DEFAULT_CLANG_VERSION = "13"
13+
14+
15+
def is_installed(tool_name: str, version: str) -> Optional[Path]:
16+
"""Check if tool is installed.
17+
18+
Checks the current python prefix and PATH via clang_tools.install.is_installed.
19+
"""
20+
# check in current python prefix (usual situation when we installed into pre-commit venv)
21+
directory = Path(sys.executable).parent
22+
path = (directory / f"{tool_name}-{version}")
23+
if path.is_file():
24+
return path
25+
26+
# also check using clang_tools
27+
path = _is_installed(tool_name, version)
28+
if path is not None:
29+
return Path(path)
30+
31+
# not found
32+
return None
33+
34+
35+
def ensure_installed(tool_name: str, version: str = DEFAULT_CLANG_VERSION) -> Path:
36+
"""
37+
Ensure tool is available at given version.
38+
"""
39+
LOG.info("Checking for %s, version %s", tool_name, version)
40+
path = is_installed(tool_name, version)
41+
if path is not None:
42+
LOG.info("%s, version %s is already installed", tool_name, version)
43+
return path
44+
45+
LOG.info("Installing %s, version %s", tool_name, version)
46+
directory = Path(sys.executable).parent
47+
install_tool(tool_name, version, directory=str(directory), no_progress_bar=True)
48+
return directory / f"{tool_name}-{version}"

testing/good.c

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#include <stdio.h>
2+
int main() {
3+
for (;;) break;
4+
printf("Hello world!\n");
5+
return 0;
6+
}

testing/.pre-commit-config.yaml renamed to testing/pre-commit-config-version.yaml

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
repos:
2-
- repo: https://github.com/cpp-linter/cpp-linter-hooks
3-
rev: 2a92e91720ca4bc79d67c3e4aea57642f598d534
2+
- repo: .
3+
rev: HEAD
44
hooks:
55
- id: clang-format
66
args: [--style=file, --version=16] # to load .clang-format

testing/pre-commit-config.yaml

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
repos:
2+
- repo: .
3+
rev: HEAD
4+
hooks:
5+
- id: clang-format
6+
args: [--style=file] # to load .clang-format
7+
- id: clang-tidy
8+
args: [--checks=.clang-tidy] # path/to/.clang-tidy

testing/run.sh

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1-
pre-commit install
2-
pre-commit try-repo . -c testing/.pre-commit-config.yaml --files testing/main.c | tee result.txt || true
1+
rm -f result.txt
2+
git restore testing/main.c
3+
4+
for config in testing/pre-commit-config.yaml testing/pre-commit-config-version.yaml; do
5+
pre-commit clean
6+
pre-commit run -c $config --files testing/main.c | tee -a result.txt || true
7+
git restore testing/main.c
8+
done
39

410
failed_cases=`grep -c "Failed" result.txt`
511

6-
if [ $failed_cases -eq 2 ]; then
12+
if [ $failed_cases -eq 4 ]; then
713
echo "=============================="
814
echo "Test cpp-linter-hooks success."
915
echo "=============================="

tests/test_clang_format.py

+17-18
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,33 @@
1-
from unittest.mock import patch
2-
31
import pytest
2+
from pathlib import Path
43

54
from cpp_linter_hooks.clang_format import run_clang_format
65

76

8-
@pytest.mark.skip(reason="don't know hwo to pass test.")
97
@pytest.mark.parametrize(
108
('args', 'expected_retval'), (
11-
(['clang-format', '-i', '--style=Google', 'testing/main.c'], 0),
12-
(['clang-format', '-i', '--style=Google', '--version=13', 'testing/main.c'], 0),
9+
(['--style=Google'], (0, "")),
10+
(['--style=Google', '--version=16'], (0, "")),
1311
),
1412
)
15-
@patch('cpp_linter_hooks.clang_format.subprocess.run')
16-
def test_run_clang_format_valid(mock_subprocess_run, args, expected_retval):
17-
mock_subprocess_run.return_value = expected_retval
18-
ret = run_clang_format(args)
13+
def test_run_clang_format_valid(args, expected_retval, tmp_path):
14+
# copy test file to tmp_path to prevent modifying repo data
15+
test_file = tmp_path / "main.c"
16+
test_file.write_bytes(Path("testing/main.c").read_bytes())
17+
ret = run_clang_format(args + [str(test_file)])
1918
assert ret == expected_retval
19+
assert test_file.read_text() == Path("testing/good.c").read_text()
2020

2121

2222
@pytest.mark.parametrize(
2323
('args', 'expected_retval'), (
24-
(['clang-format', '-i', '--style=Google', 'abc/def.c'], 1),
25-
(['clang-format', '-i', '--style=Google', '--version=13', 'abc/def.c'], 1),
24+
(['--style=Google',], 1),
25+
(['--style=Google', '--version=16'], 1),
2626
),
2727
)
28-
@patch('cpp_linter_hooks.clang_format.subprocess.run', side_effect=FileNotFoundError)
29-
def test_run_clang_format_invalid(mock_subprocess_run, args, expected_retval):
30-
mock_subprocess_run.return_value = expected_retval
31-
try:
32-
ret = run_clang_format(args)
33-
except FileNotFoundError:
34-
assert ret == expected_retval
28+
def test_run_clang_format_invalid(args, expected_retval, tmp_path):
29+
# non existent file
30+
test_file = tmp_path / "main.c"
31+
32+
ret, _ = run_clang_format(args + [str(test_file)])
33+
assert ret == expected_retval

tests/test_clang_tidy.py

+18-18
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,34 @@
1-
from unittest.mock import patch
2-
31
import pytest
2+
from pathlib import Path
43

54
from cpp_linter_hooks.clang_tidy import run_clang_tidy
65

76

8-
@pytest.mark.skip(reason="don't know hwo to pass test.")
7+
@pytest.mark.skip(reason="see https://github.com/cpp-linter/cpp-linter-hooks/pull/29")
98
@pytest.mark.parametrize(
109
('args', 'expected_retval'), (
11-
(['clang-tidy', '--checks="boost-*"', 'testing/main.c'], "stdout"),
12-
(['clang-tidy', '-checks="boost-*"', '--version=13', 'testing/main.c'], "stdout"),
10+
(['--checks="boost-*"'], 1),
11+
(['--checks="boost-*"', '--version=16'], 1),
1312
),
1413
)
15-
@patch('cpp_linter_hooks.clang_tidy.subprocess.run')
16-
def test_run_clang_tidy_valid(mock_subprocess_run, args, expected_retval):
17-
mock_subprocess_run.return_value = expected_retval
18-
ret = run_clang_tidy(args)
14+
def test_run_clang_tidy_valid(args, expected_retval, tmp_path):
15+
# copy test file to tmp_path to prevent modifying repo data
16+
test_file = tmp_path / "main.c"
17+
test_file.write_bytes(Path("testing/main.c").read_bytes())
18+
ret, output = run_clang_tidy(args + [str(test_file)])
1919
assert ret == expected_retval
20+
print(output)
2021

2122

2223
@pytest.mark.parametrize(
2324
('args', 'expected_retval'), (
24-
(['clang-tidy', '-i', '--checks="boost-*"', 'abc/def.c'], ""),
25-
(['clang-tidy', '-i', '--checks="boost-*"', '--version=13', 'abc/def.c'], ""),
25+
(['--checks="boost-*"'], 1),
26+
(['--checks="boost-*"', '--version=16'], 1),
2627
),
2728
)
28-
@patch('cpp_linter_hooks.clang_tidy.subprocess.run', side_effect=FileNotFoundError)
29-
def test_run_clang_tidy_invalid(mock_subprocess_run, args, expected_retval):
30-
mock_subprocess_run.return_value = expected_retval
31-
try:
32-
ret = run_clang_tidy(args)
33-
except FileNotFoundError:
34-
assert ret == expected_retval
29+
def test_run_clang_tidy_invalid(args, expected_retval, tmp_path):
30+
# non existent file
31+
test_file = tmp_path / "main.c"
32+
33+
ret, _ = run_clang_tidy(args + [str(test_file)])
34+
assert ret == expected_retval

0 commit comments

Comments
 (0)