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