Skip to content
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python 3.10
- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: "3.10"
python-version: "3.12"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ cpp_linter_hooks/__pycache__/
tests/.coverage
tests/__pycache__
.coverage
coverage.xml
__pycache__
venv
result.txt
Expand Down
19 changes: 0 additions & 19 deletions cpp_linter_hooks/__init__.py
Original file line number Diff line number Diff line change
@@ -1,19 +0,0 @@
import sys

from cpp_linter_hooks.util import check_installed
from cpp_linter_hooks.util import get_expect_version


clang_tools = ['clang-format', 'clang-tidy']
args = list(sys.argv[1:])

expect_version = get_expect_version(args)

for tool in clang_tools:
if expect_version:
retval = check_installed(tool, version=expect_version)
else:
retval = check_installed(tool)

if retval != 0:
raise SystemError("clang_tools not found. exit!")
31 changes: 16 additions & 15 deletions cpp_linter_hooks/clang_format.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,37 @@
import subprocess
from argparse import ArgumentParser
from typing import Tuple

from cpp_linter_hooks import args
from cpp_linter_hooks import expect_version
from .util import ensure_installed, DEFAULT_CLANG_VERSION


def run_clang_format(args) -> int:
if expect_version:
command = [f'clang-format-{expect_version}', '-i']
else:
command = ["clang-format", '-i']
for arg in args:
if arg == expect_version or arg.startswith("--version"):
continue
command.append(arg)
parser = ArgumentParser()
parser.add_argument("--version", default=DEFAULT_CLANG_VERSION)


def run_clang_format(args=None) -> Tuple[int, str]:
hook_args, other_args = parser.parse_known_args(args)
path = ensure_installed("clang-format", hook_args.version)
command = [str(path), '-i']
command.extend(other_args)

retval = 0
output = ""
try:
if "--dry-run" in command:
sp = subprocess.run(command, stdout=subprocess.PIPE)
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.decode("utf-8")
output = sp.stdout
else:
retval = subprocess.run(command, stdout=subprocess.PIPE).returncode
return retval, output
except FileNotFoundError as stderr:
retval = 1
return retval, stderr
return retval, str(stderr)


def main() -> int:
retval, output = run_clang_format(args)
retval, output = run_clang_format()
if retval != 0:
print(output)
return retval
Expand Down
31 changes: 16 additions & 15 deletions cpp_linter_hooks/clang_tidy.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,36 @@
import subprocess
from argparse import ArgumentParser
from typing import Tuple

from cpp_linter_hooks import args
from cpp_linter_hooks import expect_version
from .util import ensure_installed, DEFAULT_CLANG_VERSION


def run_clang_tidy(args) -> int:
if expect_version:
command = [f'clang-tidy-{expect_version}']
else:
command = ["clang-tidy"]
for arg in args:
if arg == expect_version or arg.startswith("--version"):
continue
command.append(arg)
parser = ArgumentParser()
parser.add_argument("--version", default=DEFAULT_CLANG_VERSION)


def run_clang_tidy(args=None) -> Tuple[int, str]:
hook_args, other_args = parser.parse_known_args(args)
path = ensure_installed("clang-tidy", hook_args.version)
command = [str(path)]
command.extend(other_args)

retval = 0
output = ""
try:
sp = subprocess.run(command, stdout=subprocess.PIPE)
sp = subprocess.run(command, stdout=subprocess.PIPE, encoding='utf-8')
retval = sp.returncode
output = sp.stdout.decode("utf-8")
output = sp.stdout
if "warning:" in output or "error:" in output:
retval = 1
return retval, output
except FileNotFoundError as stderr:
retval = 1
return retval, stderr
return retval, str(stderr)


def main() -> int:
retval, output = run_clang_tidy(args)
retval, output = run_clang_tidy()
if retval != 0:
print(output)
return retval
Expand Down
91 changes: 48 additions & 43 deletions cpp_linter_hooks/util.py
Original file line number Diff line number Diff line change
@@ -1,43 +1,48 @@
import subprocess


def check_installed(tool: str, version="") -> int:
if version:
check_version_cmd = [f'{tool}-{version} ', '--version']
else:
check_version_cmd = [tool, '--version']
try:
subprocess.run(check_version_cmd, stdout=subprocess.PIPE)
retval = 0
except FileNotFoundError:
retval = install_clang_tools(version)
return retval


def install_clang_tools(version: str) -> int:
if version:
# clang-tools exist because install_requires=['clang-tools'] in setup.py
install_tool_cmd = ['clang-tools', '-i', version]
else:
# install version 13 by default if clang-tools not exist.
install_tool_cmd = ['clang-tools', '-i', '13']
try:
subprocess.run(install_tool_cmd, stdout=subprocess.PIPE)
retval = 0
except Exception:
retval = 1
return retval


def get_expect_version(args) -> str:
for arg in args:
if arg.startswith("--version"): # expect specific clang-tools version.
# If --version is passed in as 2 arguments, the second is version
if arg == "--version" and args.index(arg) != len(args) - 1:
# when --version 14
expect_version = args[args.index(arg) + 1]
else:
# when --version=14
expect_version = arg.replace(" ", "").replace("=", "").replace("--version", "")
return expect_version
return ""
import sys
from pathlib import Path
import logging
from typing import Optional

from clang_tools.install import is_installed as _is_installed, install_tool


LOG = logging.getLogger(__name__)


DEFAULT_CLANG_VERSION = "13"


def is_installed(tool_name: str, version: str) -> Optional[Path]:
"""Check if tool is installed.

Checks the current python prefix and PATH via clang_tools.install.is_installed.
"""
# check in current python prefix (usual situation when we installed into pre-commit venv)
directory = Path(sys.executable).parent
path = (directory / f"{tool_name}-{version}")
if path.is_file():
return path

# also check using clang_tools
path = _is_installed(tool_name, version)
if path is not None:
return Path(path)

# not found
return None


def ensure_installed(tool_name: str, version: str = DEFAULT_CLANG_VERSION) -> Path:
"""
Ensure tool is available at given version.
"""
LOG.info("Checking for %s, version %s", tool_name, version)
path = is_installed(tool_name, version)
if path is not None:
LOG.info("%s, version %s is already installed", tool_name, version)
return path

LOG.info("Installing %s, version %s", tool_name, version)
directory = Path(sys.executable).parent
install_tool(tool_name, version, directory=str(directory), no_progress_bar=True)
return directory / f"{tool_name}-{version}"
6 changes: 6 additions & 0 deletions testing/good.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#include <stdio.h>
int main() {
for (;;) break;
printf("Hello world!\n");
return 0;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/cpp-linter/cpp-linter-hooks
rev: 2a92e91720ca4bc79d67c3e4aea57642f598d534
- repo: .
rev: HEAD
hooks:
- id: clang-format
args: [--style=file, --version=16] # to load .clang-format
Expand Down
8 changes: 8 additions & 0 deletions testing/pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
repos:
- repo: .
rev: HEAD
hooks:
- id: clang-format
args: [--style=file] # to load .clang-format
- id: clang-tidy
args: [--checks=.clang-tidy] # path/to/.clang-tidy
12 changes: 9 additions & 3 deletions testing/run.sh
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
pre-commit install
pre-commit try-repo . -c testing/.pre-commit-config.yaml --files testing/main.c | tee result.txt || true
rm -f result.txt
git restore testing/main.c

for config in testing/pre-commit-config.yaml testing/pre-commit-config-version.yaml; do
pre-commit clean
pre-commit run -c $config --files testing/main.c | tee -a result.txt || true
git restore testing/main.c
done

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

if [ $failed_cases -eq 2 ]; then
if [ $failed_cases -eq 4 ]; then
echo "=============================="
echo "Test cpp-linter-hooks success."
echo "=============================="
Expand Down
35 changes: 17 additions & 18 deletions tests/test_clang_format.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,33 @@
from unittest.mock import patch

import pytest
from pathlib import Path

from cpp_linter_hooks.clang_format import run_clang_format


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


@pytest.mark.parametrize(
('args', 'expected_retval'), (
(['clang-format', '-i', '--style=Google', 'abc/def.c'], 1),
(['clang-format', '-i', '--style=Google', '--version=13', 'abc/def.c'], 1),
(['--style=Google',], 1),
(['--style=Google', '--version=16'], 1),
),
)
@patch('cpp_linter_hooks.clang_format.subprocess.run', side_effect=FileNotFoundError)
def test_run_clang_format_invalid(mock_subprocess_run, args, expected_retval):
mock_subprocess_run.return_value = expected_retval
try:
ret = run_clang_format(args)
except FileNotFoundError:
assert ret == expected_retval
def test_run_clang_format_invalid(args, expected_retval, tmp_path):
# non existent file
test_file = tmp_path / "main.c"

ret, _ = run_clang_format(args + [str(test_file)])
assert ret == expected_retval
36 changes: 18 additions & 18 deletions tests/test_clang_tidy.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,34 @@
from unittest.mock import patch

import pytest
from pathlib import Path

from cpp_linter_hooks.clang_tidy import run_clang_tidy


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


@pytest.mark.parametrize(
('args', 'expected_retval'), (
(['clang-tidy', '-i', '--checks="boost-*"', 'abc/def.c'], ""),
(['clang-tidy', '-i', '--checks="boost-*"', '--version=13', 'abc/def.c'], ""),
(['--checks="boost-*"'], 1),
(['--checks="boost-*"', '--version=16'], 1),
),
)
@patch('cpp_linter_hooks.clang_tidy.subprocess.run', side_effect=FileNotFoundError)
def test_run_clang_tidy_invalid(mock_subprocess_run, args, expected_retval):
mock_subprocess_run.return_value = expected_retval
try:
ret = run_clang_tidy(args)
except FileNotFoundError:
assert ret == expected_retval
def test_run_clang_tidy_invalid(args, expected_retval, tmp_path):
# non existent file
test_file = tmp_path / "main.c"

ret, _ = run_clang_tidy(args + [str(test_file)])
assert ret == expected_retval
Loading