| # Licensed to the Apache Software Foundation (ASF) under one | |
| # or more contributor license agreements. See the NOTICE file | |
| # distributed with this work for additional information | |
| # regarding copyright ownership. The ASF licenses this file | |
| # to you under the Apache License, Version 2.0 (the | |
| # "License"); you may not use this file except in compliance | |
| # with the License. You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, | |
| # software distributed under the License is distributed on an | |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| # KIND, either express or implied. See the License for the | |
| # specific language governing permissions and limitations | |
| # under the License. | |
| from __future__ import annotations | |
| import os | |
| import shlex | |
| import shutil | |
| import subprocess | |
| import tarfile | |
| from pathlib import Path | |
| import pytest | |
| import yaml | |
| from ci import prek_cache_markers | |
| ROOT = Path(__file__).resolve().parents[3] | |
| PREK_ACTION = ".github/actions/install-prek/action.yml" | |
| def load_yaml(path): | |
| return yaml.load((ROOT / path).read_text(), Loader=yaml.BaseLoader) | |
| def find_step(path, *, step_id=None, name=None): | |
| steps = load_yaml(path)["runs"]["steps"] | |
| return next( | |
| step | |
| for step in steps | |
| if (step_id is not None and step.get("id") == step_id) | |
| or (name is not None and step.get("name") == name) | |
| ) | |
| def run_shell(script, env): | |
| return subprocess.run( | |
| ["bash", "--noprofile", "--norc", "-e", "-o", "pipefail", "-c", script], | |
| cwd=ROOT, | |
| env={**os.environ, **env}, | |
| capture_output=True, | |
| text=True, | |
| timeout=15, | |
| check=False, | |
| ) | |
| @pytest.fixture | |
| def sandbox(tmp_path): | |
| home = tmp_path / "home" | |
| home.mkdir() | |
| output = tmp_path / "output" | |
| output.touch() | |
| return {"HOME": str(home), "GITHUB_OUTPUT": str(output)} | |
| def read_outputs(env): | |
| return dict(line.split("=", 1) for line in Path(env["GITHUB_OUTPUT"]).read_text().splitlines()) | |
| def run_cache_step(step, env, tmp_path): | |
| script = step["run"].replace("/tmp/cache-prek.tar.gz", str(tmp_path / "cache-prek.tar.gz")) | |
| return run_shell(script, env) | |
| @pytest.fixture | |
| def fake_tools(tmp_path): | |
| tools = tmp_path / "bin" | |
| tools.mkdir() | |
| log = tmp_path / "commands.log" | |
| command = tools / "command" | |
| command.write_text( | |
| r"""#!/usr/bin/env bash | |
| name="${0##*/}" | |
| printf '%s\n' "${name} $*" >> "${COMMAND_LOG}" | |
| if [[ -n "${FAIL_MATCH:-}" && "${name} $*" == *"${FAIL_MATCH}"* ]]; then | |
| exit 42 | |
| fi | |
| if [[ "${name}" == "prek" ]]; then | |
| count_file="${COMMAND_COUNT}" | |
| count=0 | |
| [[ ! -f "${count_file}" ]] || count=$(<"${count_file}") | |
| count=$((count + 1)) | |
| printf '%s' "${count}" > "${count_file}" | |
| if [[ "${WRITE_MARKER_ATTEMPT:-}" == "${count}" ]]; then | |
| marker="${HOME}/.cache/prek/hooks/python-new/.prek-hook.json" | |
| mkdir -p "${marker%/*}" | |
| printf '{"schema_version":1}' > "${marker}" | |
| fi | |
| if [[ "${WRITE_LOG_ONLY:-false}" == "true" ]]; then | |
| mkdir -p "${HOME}/.cache/prek" | |
| printf 'diagnostic\n' >> "${HOME}/.cache/prek/prek.log" | |
| fi | |
| if (( count <= ${FAIL_ATTEMPTS:-0} )); then | |
| exit 42 | |
| fi | |
| fi | |
| """ | |
| ) | |
| command.chmod(0o755) | |
| for name in ("prek", "sleep"): | |
| (tools / name).symlink_to(command) | |
| return { | |
| "PATH": f"{tools}:{os.environ['PATH']}", | |
| "COMMAND_LOG": str(log), | |
| "COMMAND_COUNT": str(tmp_path / "command-count"), | |
| } | |
| def read_commands(env): | |
| return [shlex.split(line) for line in Path(env["COMMAND_LOG"]).read_text().splitlines()] | |
| def test_cache_key_keeps_only_environment_inputs(): | |
| step = find_step(PREK_ACTION, step_id="cache-key") | |
| assert "cache-prek-v13-${PLATFORM}" in step["run"] | |
| assert "python${PYTHON_VERSION}" in step["run"] | |
| assert "uv${UV_VERSION}" in step["run"] | |
| assert "prek${PREK_VERSION}" in step["run"] | |
| assert step["env"]["PREK_CONFIG_HASH"] == "${{ hashFiles('**/.pre-commit-config.yaml') }}" | |
| @pytest.mark.parametrize( | |
| ("inputs", "expected_save", "expected_reason"), | |
| [ | |
| ({}, False, "reader"), | |
| ({"SAVE_CACHE": "true", "STASH_HIT": "false"}, True, "cache-miss"), | |
| ({"SAVE_CACHE": "true", "TAR_RESTORED": "false"}, True, "extraction-failed"), | |
| ({"SAVE_CACHE": "true", "CACHE_CHANGED": "true"}, True, "cache-repaired"), | |
| ({"SAVE_CACHE": "true", "EVENT_NAME": "schedule"}, True, "non-pr-republication"), | |
| ({"SAVE_CACHE": "true", "EVENT_NAME": "push"}, True, "non-pr-republication"), | |
| ({"SAVE_CACHE": "true", "EVENT_NAME": "workflow_dispatch"}, True, "non-pr-republication"), | |
| ( | |
| {"SAVE_CACHE": "true", "CHANGE_DETECTION_UNCERTAIN": "true"}, | |
| True, | |
| "change-detection-uncertain", | |
| ), | |
| ({"SAVE_CACHE": "true"}, False, "unchanged"), | |
| ], | |
| ) | |
| def test_cache_refresh_policy(sandbox, inputs, expected_save, expected_reason): | |
| env = { | |
| **sandbox, | |
| "SAVE_CACHE": "false", | |
| "STASH_HIT": "true", | |
| "TAR_RESTORED": "true", | |
| "CACHE_CHANGED": "false", | |
| "CHANGE_DETECTION_UNCERTAIN": "false", | |
| "EVENT_NAME": "pull_request", | |
| **inputs, | |
| } | |
| result = run_shell(find_step(PREK_ACTION, step_id="cache-policy")["run"], env) | |
| assert result.returncode == 0, result.stderr | |
| assert read_outputs(env)["save"] == str(expected_save).lower() | |
| assert read_outputs(env)["reason"] == expected_reason | |
| def test_restored_hooks_are_always_validated(sandbox, fake_tools): | |
| marker = Path(sandbox["HOME"]) / ".cache/prek/hooks/python-existing/.prek-hook.json" | |
| marker.parent.mkdir(parents=True) | |
| marker.write_text('{"schema_version":1}') | |
| step = find_step(PREK_ACTION, step_id="install-hooks") | |
| assert "if" not in step | |
| result = run_shell(step["run"], {**sandbox, **fake_tools, "WRITE_LOG_ONLY": "true"}) | |
| assert result.returncode == 0, result.stderr | |
| assert [ | |
| "prek", | |
| "install-hooks", | |
| "--skip", | |
| "run-skill-eval", | |
| "--skip", | |
| "run-skill-eval-codex", | |
| "--skip", | |
| "view-skill-eval", | |
| ] in read_commands(fake_tools) | |
| assert read_outputs(sandbox)["cache-changed"] == "false" | |
| assert read_outputs(sandbox)["change-detection-uncertain"] == "false" | |
| @pytest.mark.parametrize("cache_state", ("missing", "empty", "renamed-markers", "log-only")) | |
| @pytest.mark.parametrize("save_cache", ("true", "false")) | |
| def test_markerless_cache_refresh_policy(sandbox, fake_tools, cache_state, save_cache): | |
| cache = Path(sandbox["HOME"]) / ".cache/prek" | |
| if cache_state == "empty": | |
| cache.mkdir(parents=True) | |
| elif cache_state == "renamed-markers": | |
| marker = cache / "environments/python-one/.new-marker.json" | |
| marker.parent.mkdir(parents=True) | |
| marker.write_text("{}") | |
| result = run_shell( | |
| find_step(PREK_ACTION, step_id="install-hooks")["run"], | |
| {**sandbox, **fake_tools, "WRITE_LOG_ONLY": str(cache_state == "log-only").lower()}, | |
| ) | |
| assert result.returncode == 0, result.stderr | |
| outputs = read_outputs(sandbox) | |
| uncertain = cache_state in ("renamed-markers", "log-only") | |
| assert outputs["cache-changed"] == "false" | |
| assert outputs["change-detection-uncertain"] == str(uncertain).lower() | |
| result = run_shell( | |
| find_step(PREK_ACTION, step_id="cache-policy")["run"], | |
| { | |
| **sandbox, | |
| "SAVE_CACHE": save_cache, | |
| "STASH_HIT": "true", | |
| "TAR_RESTORED": "true", | |
| "CACHE_CHANGED": outputs["cache-changed"], | |
| "CHANGE_DETECTION_UNCERTAIN": outputs["change-detection-uncertain"], | |
| "EVENT_NAME": "pull_request", | |
| }, | |
| ) | |
| assert result.returncode == 0, result.stderr | |
| outputs = read_outputs(sandbox) | |
| assert outputs["save"] == str(uncertain and save_cache == "true").lower() | |
| assert outputs["reason"] == ( | |
| "reader" if save_cache == "false" else "change-detection-uncertain" if uncertain else "unchanged" | |
| ) | |
| def test_change_detection_spans_failed_then_successful_attempts(sandbox, fake_tools): | |
| result = run_shell( | |
| find_step(PREK_ACTION, step_id="install-hooks")["run"], | |
| {**sandbox, **fake_tools, "FAIL_ATTEMPTS": "1", "WRITE_MARKER_ATTEMPT": "1"}, | |
| ) | |
| assert result.returncode == 0, result.stderr | |
| assert len([cmd for cmd in read_commands(fake_tools) if cmd[:2] == ["prek", "install-hooks"]]) == 2 | |
| assert read_outputs(sandbox)["cache-changed"] == "true" | |
| def test_unreadable_installation_metadata_is_uncertain(sandbox, fake_tools): | |
| marker = Path(sandbox["HOME"]) / ".cache/prek/hooks/python-broken/.prek-hook.json" | |
| marker.mkdir(parents=True) | |
| result = run_shell(find_step(PREK_ACTION, step_id="install-hooks")["run"], {**sandbox, **fake_tools}) | |
| assert result.returncode == 0, result.stderr | |
| assert read_outputs(sandbox)["cache-changed"] == "false" | |
| assert read_outputs(sandbox)["change-detection-uncertain"] == "true" | |
| def test_hook_install_failure_remains_fatal(sandbox, fake_tools): | |
| assert "if" not in find_step(PREK_ACTION, step_id="cache-policy") | |
| assert "continue-on-error" not in find_step(PREK_ACTION, step_id="install-hooks") | |
| result = run_shell( | |
| find_step(PREK_ACTION, step_id="install-hooks")["run"], | |
| {**sandbox, **fake_tools, "FAIL_MATCH": "prek install-hooks"}, | |
| ) | |
| assert result.returncode != 0 | |
| assert len([cmd for cmd in read_commands(fake_tools) if cmd[:2] == ["prek", "install-hooks"]]) == 4 | |
| def test_marker_snapshot_uses_relative_paths_and_exact_contents(tmp_path): | |
| cache = tmp_path / "prek" | |
| hook_marker = cache / "hooks/python-one/.prek-hook.json" | |
| repo_marker = cache / "repos/repo-one/.prek-repo.json" | |
| hook_marker.parent.mkdir(parents=True) | |
| repo_marker.parent.mkdir(parents=True) | |
| hook_marker.write_bytes(b'{"hook": 1}\n') | |
| repo_marker.write_bytes(b'{"repo": 1}\n') | |
| snapshot = prek_cache_markers.snapshot_markers(cache) | |
| assert set(snapshot) == {"hooks/python-one/.prek-hook.json", "repos/repo-one/.prek-repo.json"} | |
| assert snapshot == prek_cache_markers.snapshot_markers(cache) | |
| def test_repaired_archive_is_reused_without_another_save(sandbox, fake_tools, tmp_path): | |
| cache = Path(sandbox["HOME"]) / ".cache/prek" | |
| (cache / "hooks/python-incomplete").mkdir(parents=True) | |
| marker = cache / "hooks/python-existing/.prek-hook.json" | |
| marker.parent.mkdir(parents=True) | |
| marker.write_text('{"schema_version":1}') | |
| install_step = find_step(PREK_ACTION, step_id="install-hooks") | |
| policy_step = find_step(PREK_ACTION, step_id="cache-policy") | |
| first_install = run_shell(install_step["run"], {**sandbox, **fake_tools, "WRITE_MARKER_ATTEMPT": "1"}) | |
| assert first_install.returncode == 0, first_install.stderr | |
| first_outputs = read_outputs(sandbox) | |
| assert first_outputs["cache-changed"] == "true" | |
| Path(sandbox["GITHUB_OUTPUT"]).write_text("") | |
| policy_env = { | |
| **sandbox, | |
| "SAVE_CACHE": "true", | |
| "STASH_HIT": "true", | |
| "TAR_RESTORED": "true", | |
| "CACHE_CHANGED": "true", | |
| "CHANGE_DETECTION_UNCERTAIN": "false", | |
| "EVENT_NAME": "pull_request", | |
| } | |
| assert run_shell(policy_step["run"], policy_env).returncode == 0 | |
| assert read_outputs(sandbox) == {"save": "true", "reason": "cache-repaired"} | |
| Path(sandbox["GITHUB_OUTPUT"]).write_text("") | |
| archive_step = find_step(PREK_ACTION, step_id="archive-prek") | |
| assert run_cache_step(archive_step, sandbox, tmp_path).returncode == 0 | |
| shutil.rmtree(cache) | |
| Path(sandbox["GITHUB_OUTPUT"]).write_text("") | |
| restore_step = find_step(PREK_ACTION, step_id="restore-prek-tar") | |
| assert run_cache_step(restore_step, sandbox, tmp_path).returncode == 0 | |
| Path(sandbox["GITHUB_OUTPUT"]).write_text("") | |
| second_install = run_shell(install_step["run"], {**sandbox, **fake_tools}) | |
| assert second_install.returncode == 0, second_install.stderr | |
| second_outputs = read_outputs(sandbox) | |
| assert second_outputs["cache-changed"] == "false" | |
| Path(sandbox["GITHUB_OUTPUT"]).write_text("") | |
| second_policy_env = { | |
| **policy_env, | |
| "CACHE_CHANGED": "false", | |
| } | |
| assert run_shell(policy_step["run"], second_policy_env).returncode == 0 | |
| assert read_outputs(sandbox) == {"save": "false", "reason": "unchanged"} | |
| @pytest.mark.skipif( | |
| os.environ.get("RUN_PREK_INTEGRATION") != "1", | |
| reason="Set RUN_PREK_INTEGRATION=1 to exercise real prek 0.5.2", | |
| ) | |
| def test_real_prek_repair_and_reuse(tmp_path): | |
| repository = tmp_path / "repository" | |
| repository.mkdir() | |
| subprocess.run(["git", "init", "-q"], cwd=repository, check=True) | |
| (repository / ".pre-commit-config.yaml").write_text( | |
| """\ | |
| repos: | |
| - repo: local | |
| hooks: | |
| - id: local-python | |
| name: local python | |
| entry: python -c 'print("ok")' | |
| language: python | |
| pass_filenames: false | |
| """ | |
| ) | |
| cache = tmp_path / "prek-cache" | |
| env = {**os.environ, "PREK_HOME": str(cache)} | |
| command = ["uvx", "--from", "prek==0.5.2", "prek", "install-hooks"] | |
| def install(): | |
| return subprocess.run( | |
| command, | |
| cwd=repository, | |
| env=env, | |
| capture_output=True, | |
| text=True, | |
| timeout=180, | |
| check=False, | |
| ) | |
| first_install = install() | |
| assert first_install.returncode == 0, first_install.stderr | |
| marker = next((cache / "hooks").glob("*/.prek-hook.json")) | |
| marker.unlink() | |
| before_repair = prek_cache_markers.snapshot_markers(cache) | |
| repair = install() | |
| assert repair.returncode == 0, repair.stderr | |
| repaired = prek_cache_markers.snapshot_markers(cache) | |
| assert repaired != before_repair | |
| archive = tmp_path / "prek-cache.tar.gz" | |
| with tarfile.open(archive, "w:gz") as handle: | |
| handle.add(cache, arcname="prek-cache") | |
| shutil.rmtree(cache) | |
| with tarfile.open(archive, "r:gz") as handle: | |
| handle.extractall(tmp_path, filter="fully_trusted") | |
| before_reuse = prek_cache_markers.snapshot_markers(cache) | |
| reuse = install() | |
| assert reuse.returncode == 0, reuse.stderr | |
| assert prek_cache_markers.snapshot_markers(cache) == before_reuse | |
| @pytest.mark.parametrize("payload", ("missing", "corrupt", "wrong-directory")) | |
| def test_unusable_archive_is_a_miss(sandbox, tmp_path, payload): | |
| archive = tmp_path / "cache-prek.tar.gz" | |
| if payload == "corrupt": | |
| archive.write_bytes(b"not a gzip archive") | |
| elif payload == "wrong-directory": | |
| other = tmp_path / "other" | |
| other.mkdir() | |
| with tarfile.open(archive, "w:gz") as handle: | |
| handle.add(other, arcname=".cache/other") | |
| result = run_cache_step(find_step(PREK_ACTION, step_id="restore-prek-tar"), sandbox, tmp_path) | |
| assert result.returncode == 0, result.stderr | |
| assert read_outputs(sandbox)["tar-restored"] == "false" | |
| def test_archive_roundtrip_preserves_permissions_links_and_contents(sandbox, tmp_path): | |
| cache = Path(sandbox["HOME"]) / ".cache/prek" | |
| cache.mkdir(parents=True) | |
| executable = cache / "python" | |
| executable.write_text("payload\n") | |
| executable.chmod(0o751) | |
| (cache / "link").symlink_to("python") | |
| result = run_cache_step(find_step(PREK_ACTION, step_id="archive-prek"), sandbox, tmp_path) | |
| assert result.returncode == 0, result.stderr | |
| shutil.rmtree(cache) | |
| result = run_cache_step(find_step(PREK_ACTION, step_id="restore-prek-tar"), sandbox, tmp_path) | |
| assert result.returncode == 0, result.stderr | |
| assert read_outputs(sandbox)["tar-restored"] == "true" | |
| assert executable.read_text() == "payload\n" | |
| assert executable.stat().st_mode & 0o777 == 0o751 | |
| assert (cache / "link").is_symlink() | |
| assert os.readlink(cache / "link") == "python" | |
| def test_missing_cache_cannot_be_saved(sandbox, tmp_path): | |
| result = run_cache_step(find_step(PREK_ACTION, step_id="archive-prek"), sandbox, tmp_path) | |
| assert result.returncode != 0 | |
| assert "duration-seconds" not in read_outputs(sandbox) | |
| def test_cache_cleanup_preserves_unrelated_cache(sandbox, tmp_path): | |
| home = Path(sandbox["HOME"]) | |
| for name in ("prek", "uv"): | |
| (home / ".cache" / name).mkdir(parents=True) | |
| archive = tmp_path / "cache-prek.tar.gz" | |
| archive.write_bytes(b"stale") | |
| result = run_cache_step( | |
| find_step(PREK_ACTION, name="Clear local prek cache before restore"), sandbox, tmp_path | |
| ) | |
| assert result.returncode == 0, result.stderr | |
| assert not (home / ".cache/prek").exists() | |
| assert (home / ".cache/uv").is_dir() | |
| assert not archive.exists() |