Skip to content

Commit 73a4dfc

Browse files
Copilotshenxianpeng
andcommitted
Refactor testing/ integration tests into pytest suite
- Add tests/test_integration.py with 24 parametrized pytest tests covering all scenarios from the pre-commit config YAML files: style-from-file (.clang-format), versions 16-21, style presets (LLVM/Google/Microsoft/WebKit/Mozilla/Chromium with reformat and idempotence checks), verbose flags, and parallel clang-tidy. - Rewrite testing/run.sh: replace the fragile magic-number approach (grep -c 'Failed' == 21) with a run_test() helper that explicitly declares the expected outcome for each scenario and reports pass/fail per test with human-readable descriptions. Agent-Logs-Url: https://github.com/cpp-linter/cpp-linter-hooks/sessions/509b22bb-e65e-4a6f-bc53-52d404732235 Co-authored-by: shenxianpeng <3353385+shenxianpeng@users.noreply.github.com>
1 parent 0d4c4db commit 73a4dfc

2 files changed

Lines changed: 301 additions & 66 deletions

File tree

testing/run.sh

Lines changed: 92 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,108 @@
11
#!/bin/bash
2+
# Pre-commit integration tests for cpp-linter-hooks.
3+
#
4+
# Each test runs a specific pre-commit configuration against testing/main.c
5+
# and checks whether the expected outcome (pass or fail) is observed.
6+
# testing/main.c is restored after every test so they are independent.
7+
#
8+
# These tests exercise the full pre-commit pipeline (hook definitions,
9+
# entry points, argument passing) which the pytest unit tests do not cover.
10+
# The hook logic itself is tested in tests/test_integration.py.
211

3-
configs=(
4-
"pre-commit-config.yaml"
5-
"pre-commit-config-version.yaml"
6-
"pre-commit-config-verbose.yaml"
7-
"pre-commit-config-style.yaml"
8-
)
12+
set -euo pipefail
913

10-
for config in "${configs[@]}"; do
11-
echo "===================================="
12-
echo "Test $config"
13-
echo "===================================="
14-
uvx pre-commit clean
15-
uvx pre-commit run -c testing/$config --files testing/main.c | tee -a result.txt || true
16-
git restore testing/main.c
17-
done
14+
PASS=0
15+
FAIL=0
1816

19-
echo "=================================================================================="
20-
echo "Print result.txt"
21-
cat result.txt
22-
echo "=================================================================================="
17+
# run_test <description> <config-file> <files> <expect-nonzero>
18+
#
19+
# description – human-readable name shown in output
20+
# config-file – path to the pre-commit config (relative to repo root)
21+
# files – space-separated list of files passed to --files
22+
# expect-nonzero – "true" if the hook is expected to report failures,
23+
# "false" if it should exit cleanly
24+
run_test() {
25+
local description="$1"
26+
local config="$2"
27+
local files="$3"
28+
local expect_nonzero="$4"
2329

24-
failed_cases=$(grep -c "Failed" result.txt)
30+
echo "---- $description ----"
31+
uvx pre-commit clean -q
2532

26-
echo "$failed_cases cases failed."
33+
local output
34+
# shellcheck disable=SC2086 # word-splitting of $files is intentional
35+
if output=$(uvx pre-commit run -c "$config" --files $files 2>&1); then
36+
local got_nonzero=false
37+
else
38+
local got_nonzero=true
39+
fi
2740

28-
if [[ $failed_cases -eq 21 ]]; then
29-
echo "=============================="
30-
echo "Test cpp-linter-hooks success."
31-
echo "=============================="
32-
result="success"
33-
rm result.txt
34-
exit_code=0
35-
else
36-
echo "============================="
37-
echo "Test cpp-linter-hooks failed."
38-
echo "============================="
39-
result="failure"
40-
exit_code=1
41-
fi
41+
git restore testing/main.c 2>/dev/null || true
4242

43-
# Test parallel execution with multiple source files
44-
echo "===================================="
45-
echo "Test pre-commit-config-parallel.yaml"
46-
echo "===================================="
47-
uvx pre-commit clean
48-
uvx pre-commit run -c testing/pre-commit-config-parallel.yaml \
49-
--files testing/main.c testing/good.c | tee -a parallel_result.txt || true
50-
git restore testing/main.c
43+
if [[ "$expect_nonzero" == "$got_nonzero" ]]; then
44+
echo "PASS: $description"
45+
PASS=$((PASS + 1))
46+
else
47+
echo "FAIL: $description (expect_nonzero=$expect_nonzero, got_nonzero=$got_nonzero)"
48+
echo "$output"
49+
FAIL=$((FAIL + 1))
50+
fi
51+
echo ""
52+
}
5153

52-
parallel_failed=$(grep -c "Failed" parallel_result.txt 2>/dev/null || echo "0")
53-
echo "$parallel_failed parallel cases failed."
54+
# ── Basic config: clang-format + clang-tidy on the unformatted file ──────────
55+
# Expected: clang-format rewrites main.c → pre-commit reports failure.
56+
run_test \
57+
"basic: clang-format + clang-tidy on unformatted file" \
58+
"testing/pre-commit-config.yaml" \
59+
"testing/main.c" \
60+
"true"
5461

55-
if [[ $parallel_failed -ge 1 ]]; then
56-
echo "========================================================"
57-
echo "Parallel test passed (expected failures detected: $parallel_failed)."
58-
echo "========================================================"
59-
parallel_result="success"
60-
rm parallel_result.txt
61-
else
62-
echo "==========================================="
63-
echo "Parallel test failed (no failures detected)."
64-
echo "==========================================="
65-
parallel_result="failure"
66-
exit_code=1
67-
fi
62+
# ── Version config: explicit versions 16–21 ──────────────────────────────────
63+
# Expected: at least clang-format v16 rewrites the file → failure reported.
64+
run_test \
65+
"versions 16-21: clang-format + clang-tidy" \
66+
"testing/pre-commit-config-version.yaml" \
67+
"testing/main.c" \
68+
"true"
69+
70+
# ── Verbose config: --verbose and -v flags ────────────────────────────────────
71+
# Expected: clang-format rewrites main.c (verbose output goes to stderr).
72+
run_test \
73+
"verbose: --verbose and -v flags" \
74+
"testing/pre-commit-config-verbose.yaml" \
75+
"testing/main.c" \
76+
"true"
77+
78+
# ── Style config: LLVM / Google / Microsoft / WebKit / Mozilla / Chromium ────
79+
# Expected: each style reformats main.c differently → failure reported.
80+
run_test \
81+
"styles: LLVM, Google, Microsoft, WebKit, Mozilla, Chromium" \
82+
"testing/pre-commit-config-style.yaml" \
83+
"testing/main.c" \
84+
"true"
6885

69-
# Add result to GitHub summary if running in GitHub Actions
70-
if [[ -n "$GITHUB_STEP_SUMMARY" ]]; then
86+
# ── Parallel config: clang-tidy --jobs=2 on two source files ─────────────────
87+
# Expected: clang-tidy finds issues in at least one file → failure reported.
88+
run_test \
89+
"parallel: clang-tidy --jobs=2 on main.c + good.c" \
90+
"testing/pre-commit-config-parallel.yaml" \
91+
"testing/main.c testing/good.c" \
92+
"true"
93+
94+
# ── Summary ───────────────────────────────────────────────────────────────────
95+
echo "Results: $PASS passed, $FAIL failed"
96+
97+
if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then
7198
{
72-
echo "### cpp-linter-hooks Test Result"
73-
echo ""
74-
echo "**Result:** $result"
75-
echo ""
76-
echo "**Failed cases:** $failed_cases"
99+
echo "### cpp-linter-hooks pre-commit integration test results"
77100
echo ""
78-
echo "**Parallel test:** $parallel_result"
101+
echo "| Result | Count |"
102+
echo "|--------|-------|"
103+
echo "| Passed | $PASS |"
104+
echo "| Failed | $FAIL |"
79105
} >> "$GITHUB_STEP_SUMMARY"
80106
fi
81107

82-
exit $exit_code
108+
[[ $FAIL -eq 0 ]]

tests/test_integration.py

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
"""Integration tests that mirror the scenarios from testing/pre-commit-config-*.yaml.
2+
3+
These tests call the hook functions directly and validate end-to-end behaviour
4+
for the key scenarios exercised by the pre-commit configuration files:
5+
6+
* pre-commit-config.yaml – basic style-from-file with .clang-format
7+
* pre-commit-config-version.yaml – explicit tool versions 16–21
8+
* pre-commit-config-verbose.yaml – --verbose / -v flags
9+
* pre-commit-config-style.yaml – LLVM, Google, Microsoft, WebKit, Mozilla, Chromium
10+
11+
Each scenario is a named, parametrized pytest function with explicit assertions
12+
so failures report the exact case that broke, and new cases can be added by
13+
extending the parametrize list.
14+
"""
15+
16+
import shutil
17+
18+
import pytest
19+
from pathlib import Path
20+
21+
from cpp_linter_hooks.clang_format import run_clang_format
22+
from cpp_linter_hooks.clang_tidy import run_clang_tidy
23+
24+
TESTING_DIR = Path("testing")
25+
MAIN_C = TESTING_DIR / "main.c"
26+
GOOD_C = TESTING_DIR / "good.c"
27+
28+
29+
# ---------------------------------------------------------------------------
30+
# Shared fixtures
31+
# ---------------------------------------------------------------------------
32+
33+
34+
@pytest.fixture()
35+
def unformatted_file(tmp_path):
36+
"""Return a temporary copy of the unformatted source file."""
37+
test_file = tmp_path / "main.c"
38+
test_file.write_bytes(MAIN_C.read_bytes())
39+
return test_file
40+
41+
42+
@pytest.fixture()
43+
def clang_format_workspace(tmp_path, monkeypatch):
44+
"""Temp directory that contains .clang-format + an unformatted main.c.
45+
46+
monkeypatch.chdir ensures --style=file discovers the config file.
47+
"""
48+
shutil.copy(TESTING_DIR / ".clang-format", tmp_path / ".clang-format")
49+
test_file = tmp_path / "main.c"
50+
test_file.write_bytes(MAIN_C.read_bytes())
51+
monkeypatch.chdir(tmp_path)
52+
return test_file
53+
54+
55+
# ---------------------------------------------------------------------------
56+
# pre-commit-config.yaml scenario – style-from-file
57+
# ---------------------------------------------------------------------------
58+
59+
60+
@pytest.mark.benchmark
61+
def test_clang_format_style_from_file_reformats(clang_format_workspace):
62+
"""--style=file reads .clang-format and reformats the unformatted file."""
63+
test_file = clang_format_workspace
64+
original = test_file.read_text()
65+
66+
ret, output = run_clang_format(["--style=file", str(test_file)])
67+
68+
assert ret == 0
69+
assert isinstance(output, str)
70+
assert test_file.read_text() != original, "file should have been reformatted"
71+
72+
73+
@pytest.mark.benchmark
74+
def test_clang_format_style_from_file_is_idempotent(clang_format_workspace):
75+
"""Running --style=file twice on an already-formatted file is a no-op."""
76+
test_file = clang_format_workspace
77+
78+
# First pass: reformat
79+
run_clang_format(["--style=file", str(test_file)])
80+
after_first = test_file.read_text()
81+
82+
# Second pass: file should be unchanged
83+
ret, output = run_clang_format(["--style=file", str(test_file)])
84+
85+
assert ret == 0
86+
assert isinstance(output, str)
87+
assert test_file.read_text() == after_first, "second pass must not change the file"
88+
89+
90+
# ---------------------------------------------------------------------------
91+
# pre-commit-config-version.yaml scenario – explicit versions 16–21
92+
# ---------------------------------------------------------------------------
93+
94+
95+
@pytest.mark.benchmark
96+
@pytest.mark.parametrize("version", ["16", "17", "18", "19", "20", "21"])
97+
def test_clang_format_style_from_file_with_version(tmp_path, monkeypatch, version):
98+
"""--style=file combined with an explicit --version reformats the file."""
99+
shutil.copy(TESTING_DIR / ".clang-format", tmp_path / ".clang-format")
100+
test_file = tmp_path / "main.c"
101+
test_file.write_bytes(MAIN_C.read_bytes())
102+
monkeypatch.chdir(tmp_path)
103+
104+
original = test_file.read_text()
105+
ret, output = run_clang_format(
106+
["--style=file", f"--version={version}", str(test_file)]
107+
)
108+
109+
assert ret == 0
110+
assert isinstance(output, str)
111+
assert test_file.read_text() != original, (
112+
f"version {version}: file should have been reformatted"
113+
)
114+
115+
116+
# ---------------------------------------------------------------------------
117+
# pre-commit-config-style.yaml scenario – various style presets
118+
# ---------------------------------------------------------------------------
119+
120+
STYLE_PRESETS = ["LLVM", "Google", "Microsoft", "WebKit", "Mozilla", "Chromium"]
121+
122+
123+
@pytest.mark.benchmark
124+
@pytest.mark.parametrize("style", STYLE_PRESETS)
125+
def test_clang_format_style_preset_reformats_unformatted_file(style, unformatted_file):
126+
"""Each style preset should reformat the unformatted main.c."""
127+
original = unformatted_file.read_text()
128+
129+
ret, output = run_clang_format([f"--style={style}", str(unformatted_file)])
130+
131+
assert ret == 0
132+
assert isinstance(output, str)
133+
assert unformatted_file.read_text() != original, (
134+
f"style={style}: file should have been reformatted"
135+
)
136+
137+
138+
@pytest.mark.benchmark
139+
@pytest.mark.parametrize("style", STYLE_PRESETS)
140+
def test_clang_format_style_preset_is_idempotent(style, tmp_path):
141+
"""Running clang-format twice with the same style preset is a no-op."""
142+
test_file = tmp_path / "main.c"
143+
test_file.write_bytes(MAIN_C.read_bytes())
144+
145+
# First pass: reformat
146+
run_clang_format([f"--style={style}", str(test_file)])
147+
after_first = test_file.read_text()
148+
149+
# Second pass: file must not change
150+
ret, output = run_clang_format([f"--style={style}", str(test_file)])
151+
152+
assert ret == 0
153+
assert isinstance(output, str)
154+
assert test_file.read_text() == after_first, (
155+
f"style={style}: second pass must not change the file"
156+
)
157+
158+
159+
@pytest.mark.benchmark
160+
def test_clang_format_google_style_matches_good_c(unformatted_file):
161+
"""Google-style formatting of main.c must exactly match testing/good.c."""
162+
ret, output = run_clang_format(["--style=Google", str(unformatted_file)])
163+
164+
assert ret == 0
165+
assert isinstance(output, str)
166+
assert unformatted_file.read_text() == GOOD_C.read_text()
167+
168+
169+
# ---------------------------------------------------------------------------
170+
# pre-commit-config-verbose.yaml scenario – verbose flags
171+
# ---------------------------------------------------------------------------
172+
173+
174+
@pytest.mark.benchmark
175+
@pytest.mark.parametrize("verbose_flag", ["--verbose", "-v"])
176+
def test_clang_format_verbose_flag_succeeds(verbose_flag, unformatted_file):
177+
"""Both --verbose and -v flags must be accepted and not break formatting."""
178+
ret, output = run_clang_format([verbose_flag, "--style=Google", str(unformatted_file)])
179+
180+
assert ret == 0
181+
assert isinstance(output, str)
182+
assert unformatted_file.read_text() == GOOD_C.read_text()
183+
184+
185+
# ---------------------------------------------------------------------------
186+
# pre-commit-config-parallel.yaml scenario – parallel clang-tidy
187+
# ---------------------------------------------------------------------------
188+
189+
190+
@pytest.mark.benchmark
191+
def test_clang_tidy_parallel_execution_completes(tmp_path):
192+
"""clang-tidy --jobs=2 processes multiple source files without crashing."""
193+
main_file = tmp_path / "main.c"
194+
good_file = tmp_path / "good.c"
195+
main_file.write_bytes(MAIN_C.read_bytes())
196+
good_file.write_bytes(GOOD_C.read_bytes())
197+
198+
ret, output = run_clang_tidy(
199+
[
200+
"--no-compile-commands",
201+
"--jobs=2",
202+
str(main_file),
203+
str(good_file),
204+
]
205+
)
206+
207+
# Return code must be a valid integer (0 = clean, 1 = issues found).
208+
assert isinstance(ret, int)
209+
assert ret in (0, 1)

0 commit comments

Comments
 (0)