From: Joel Rosdahl Date: Tue, 18 Nov 2025 18:15:38 +0000 (+0100) Subject: test: Add basic MSVC integration test suite X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=d2d90c504e9a36387185f6581813cde20d234838;p=thirdparty%2Fccache.git test: Add basic MSVC integration test suite We need some basic MSVC integration/smoke tests. Since I think it's a dead end to try to adapt the bash-based test suite for MSVC, this commit adds a separate Python-based test suite instead. It's not a full solution but rather something quick to cover the most basic testing needs. --- diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 5788a7dd..04d6b410 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -418,6 +418,35 @@ jobs: path: install/bin/ccache.exe retention-days: 3 + windows_tests: + timeout-minutes: 30 + name: Windows integration tests + runs-on: windows-2022 + steps: + - name: Get source + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Install pytest + run: pip install pytest + + - name: Build ccache + shell: cmd + run: | + call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat" + cmake -S . -B build-win64 -G Ninja -DCMAKE_BUILD_TYPE=Release + ninja -C build-win64 ccache + + - name: Run MSVC integration tests + shell: cmd + run: | + call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat" + pytest test/msvc/ --ccache "%GITHUB_WORKSPACE%/build-win64/ccache.exe" -v + specific_tests: name: ${{ matrix.name }} runs-on: ${{ matrix.os }} diff --git a/test/msvc/conftest.py b/test/msvc/conftest.py new file mode 100644 index 00000000..b7257a0e --- /dev/null +++ b/test/msvc/conftest.py @@ -0,0 +1,123 @@ +""" +pytest configuration and shared fixtures for MSVC integration tests. +""" + +import json +import os +import shutil +import tempfile +from pathlib import Path +from subprocess import PIPE, CompletedProcess +from subprocess import run as sp_run +from typing import Optional + +import pytest + + +class CcacheTest: + def __init__(self, ccache_exe: Path): + self.ccache_exe = ccache_exe + self.tmpdir = Path(tempfile.gettempdir()) + self.cache_dir = None + self.log_file = None + self.workdir = None + self.env = None + + def __enter__(self): + self.cache_dir = Path(tempfile.mkdtemp(prefix="ccache_", dir=self.tmpdir)) + self.workdir = Path(tempfile.mkdtemp(prefix="work_", dir=self.tmpdir)) + log_dir = tempfile.mkdtemp(prefix="log_", dir=self.tmpdir) + self.log_file = Path(log_dir) / "ccache.log" + + self.env = os.environ.copy() + self.env["CCACHE_DIR"] = str(self.cache_dir) + self.env["CCACHE_LOGFILE"] = str(self.log_file) + + self.reset_stats() + + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + assert self.cache_dir + assert self.workdir + assert self.log_file + + # Print log on failure for debugging + if exc_type is not None and self.log_file.exists(): + print(f"\n--- CCACHE_LOGFILE content ({self.log_file}) ---") + print(self.log_file.read_text(errors="replace")) + print("--- End of CCACHE_LOGFILE ---\n") + + shutil.rmtree(self.cache_dir, ignore_errors=True) + shutil.rmtree(self.workdir, ignore_errors=True) + shutil.rmtree(self.log_file.parent, ignore_errors=True) + + def _run( + self, args: list[str], *, cwd: Optional[Path] = None, check: bool = True + ) -> CompletedProcess: + return sp_run( + args, + cwd=cwd or self.workdir, + env=self.env, + stdout=PIPE, + stderr=PIPE, + text=True, + check=check, + ) + + def run( + self, args: list[str], *, cwd: Optional[Path] = None, check: bool = True + ) -> CompletedProcess: + return self._run(args, cwd=cwd, check=check) + + def compile(self, cl_args: list, *, cwd: Optional[Path] = None) -> CompletedProcess: + """Compile with ccache + cl.""" + cmd = [self.ccache_exe, "cl", *cl_args] + return self._run(cmd, cwd=cwd) + + def stats(self) -> dict[str, int]: + result = self._run([self.ccache_exe, "--print-stats", "--format", "json"]) + stats_data = json.loads(result.stdout) + + direct_hit = stats_data.get("direct_cache_hit", 0) + preprocessed_hit = stats_data.get("preprocessed_cache_hit", 0) + miss = stats_data.get("cache_miss", 0) + + return { + "direct_hit": direct_hit, + "preprocessed_hit": preprocessed_hit, + "miss": miss, + "total_hit": direct_hit + preprocessed_hit, + } + + def reset_stats(self) -> None: + self._run([self.ccache_exe, "-z"]) + + +def pytest_addoption(parser): + parser.addoption( + "--ccache", action="store", help="Path to ccache.exe", required=True + ) + + +@pytest.fixture(scope="session") +def ccache_exe(request): + ccache_path = request.config.getoption("--ccache") + ccache_exe = Path(ccache_path).resolve() + if not ccache_exe.exists(): + pytest.fail(f"ccache.exe not found at {ccache_exe}") + return ccache_exe + + +@pytest.fixture(scope="session") +def verify_cl_available(): + cl = shutil.which("cl") + if not cl: + pytest.fail("cl.exe not found in PATH") + return cl + + +@pytest.fixture +def ccache_test(ccache_exe, verify_cl_available): # noqa: ARG001 + with CcacheTest(ccache_exe) as test: + yield test diff --git a/test/msvc/test_basic_functionality.py b/test/msvc/test_basic_functionality.py new file mode 100644 index 00000000..af73bbb7 --- /dev/null +++ b/test/msvc/test_basic_functionality.py @@ -0,0 +1,55 @@ +def test_basic_compilation(ccache_test): + source = ccache_test.workdir / "test.c" + source.write_text("int main() {}\n") + + ccache_test.compile(["/c", "test.c", "/Fohello.obj"]) + stats_1 = ccache_test.stats() + assert stats_1["miss"] == 1 + assert stats_1["total_hit"] == 0 + + ccache_test.compile(["/c", "test.c", "/Fohello.obj"]) + stats_2 = ccache_test.stats() + assert stats_2["miss"] == 1 + assert stats_2["total_hit"] == 1 + assert (ccache_test.workdir / "hello.obj").exists() + + ccache_test.compile(["/c", "test.c"]) + stats_2 = ccache_test.stats() + assert stats_2["miss"] == 1 + assert stats_2["total_hit"] == 2 + assert (ccache_test.workdir / "test.obj").exists() + + +def test_define_change_is_miss(ccache_test): + source = ccache_test.workdir / "test.c" + source.write_text("int x = VALUE;\n") + + ccache_test.compile(["/c", "/DVALUE=1", "test.c"]) + stats_1 = ccache_test.stats() + assert stats_1["miss"] == 1 + + ccache_test.compile(["/c", "/DVALUE=2", "test.c"]) + stats_2 = ccache_test.stats() + assert stats_2["miss"] == 2 + assert stats_2["total_hit"] == 0 + + +def test_basedir_normalizes_paths(ccache_test): + ccache_test.env["CCACHE_NOHASHDIR"] = "1" + ccache_test.env["CCACHE_BASEDIR"] = str(ccache_test.workdir) + + dirs = [] + for name in ["dir1", "dir2"]: + d = ccache_test.workdir / name + d.mkdir() + (d / "test.c").write_text("int x;\n") + dirs.append(d) + + ccache_test.compile(["/c", "test.c"], cwd=dirs[0]) + stats_1 = ccache_test.stats() + assert stats_1["miss"] == 1 + + ccache_test.compile(["/c", "test.c"], cwd=dirs[1]) + stats_2 = ccache_test.stats() + assert stats_2["miss"] == 1 + assert stats_2["total_hit"] == 1