]> git.ipfire.org Git - thirdparty/ccache.git/commitdiff
test: Add basic MSVC integration test suite
authorJoel Rosdahl <joel@rosdahl.net>
Tue, 18 Nov 2025 18:15:38 +0000 (19:15 +0100)
committerJoel Rosdahl <joel@rosdahl.net>
Sat, 22 Nov 2025 09:34:17 +0000 (10:34 +0100)
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.

.github/workflows/build.yaml
test/msvc/conftest.py [new file with mode: 0644]
test/msvc/test_basic_functionality.py [new file with mode: 0644]

index 5788a7dd65d81788ef35681899d8e525dad0cceb..04d6b410f5c0ccc8cb6ddfc028fcfb1fac3b3951 100644 (file)
@@ -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 (file)
index 0000000..b7257a0
--- /dev/null
@@ -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 (file)
index 0000000..af73bbb
--- /dev/null
@@ -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