Use pinned nextest for full native tests while retaining doctests, production-feature checks and both macOS release checks. Publish shared dependency snapshots from successful default-branch runs, keep compiled artifacts architecture-specific, and preserve cross-process exclusion for shared theme fixtures. Run the full platform matrix for both draft and ready pull requests without repeating it for readiness-only transitions.
210 lines
11 KiB
Python
210 lines
11 KiB
Python
"""Exercise the cache identity code used by the composite action itself."""
|
|
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
import textwrap
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
|
|
ACTION = Path(__file__).resolve().parents[2] / ".github/actions/rust-cache/action.yml"
|
|
|
|
|
|
class CacheIdentityTests(unittest.TestCase):
|
|
@staticmethod
|
|
def action_text() -> str:
|
|
return ACTION.read_text(encoding="utf-8")
|
|
|
|
def identity(self, *, compiler="rustc 1.97.1", sdk="15.0", **changes):
|
|
source = ACTION.read_text(encoding="utf-8")
|
|
body = re.search(r"python - <<'PY'\n(.*?)\n PY", source, re.S)
|
|
self.assertIsNotNone(body, "cache identity must remain executable from this action")
|
|
code = textwrap.dedent(body.group(1))
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
output = Path(temporary) / "outputs"
|
|
environment = {
|
|
"CACHE_OS": "Linux", "CACHE_ARCH": "X64", "CACHE_WORKLOAD": "ci-product",
|
|
"CACHE_MANIFESTS": "pinned-manifests", "GITHUB_OUTPUT": str(output),
|
|
"CACHE_REVISION": "source-revision",
|
|
"CARGO_HOME": str(Path(temporary) / "cargo"), **changes,
|
|
}
|
|
|
|
def command(arguments, **kwargs):
|
|
if arguments == ["rustc", "-vV"]:
|
|
return compiler
|
|
self.assertEqual(arguments, ["xcrun", "--sdk", "macosx", "--show-sdk-version"])
|
|
return sdk
|
|
|
|
with patch.dict(os.environ, environment, clear=True), patch("subprocess.check_output", side_effect=command):
|
|
exec(compile(code, str(ACTION), "exec"), {})
|
|
return dict(line.split("=", 1) for line in output.read_text(encoding="utf-8").splitlines())
|
|
|
|
def test_compiler_flags_and_workload_isolate_compiled_targets(self):
|
|
baseline = self.identity()["key"]
|
|
for change in (
|
|
{"compiler": "rustc 1.98.0"}, {"RUSTFLAGS": "-C opt-level=1"},
|
|
{"CARGO_PROFILE_RELEASE_LTO": "false"}, {"CACHE_WORKLOAD": "release"},
|
|
{"CACHE_ARCH": "ARM64"}, {"ImageOS": "a-different-runner-image"},
|
|
):
|
|
with self.subTest(change=change):
|
|
self.assertNotEqual(baseline, self.identity(**change)["key"])
|
|
|
|
def test_macos_sdk_and_deployment_floor_are_part_of_identity(self):
|
|
baseline = self.identity(CACHE_OS="macOS", sdk="15.0")["key"]
|
|
self.assertNotEqual(baseline, self.identity(CACHE_OS="macOS", sdk="16.0")["key"])
|
|
self.assertNotEqual(baseline, self.identity(CACHE_OS="macOS", MACOSX_DEPLOYMENT_TARGET="13.0")["key"])
|
|
|
|
def test_dependency_change_can_restore_compatible_previous_targets(self):
|
|
before = self.identity(CACHE_MANIFESTS="before")
|
|
after = self.identity(CACHE_MANIFESTS="after")
|
|
self.assertNotEqual(before["key"], after["key"])
|
|
self.assertEqual(before["restore-key"], after["restore-key"])
|
|
self.assertTrue(after["key"].startswith(after["restore-key"]))
|
|
|
|
def test_source_only_revision_and_package_labels_do_not_bust_dependencies(self):
|
|
before = self.identity(CACHE_REVISION="one", PREVIEW_ID="first")
|
|
after = self.identity(CACHE_REVISION="two", PREVIEW_ID="second")
|
|
self.assertNotEqual(before["key"], after["key"])
|
|
self.assertEqual(before["manifest-key"], after["manifest-key"])
|
|
self.assertEqual(before["restore-key"], after["restore-key"])
|
|
self.assertEqual(before["key"], self.identity(CACHE_REVISION="one", PREVIEW_ID="third")["key"])
|
|
|
|
def test_summary_exposes_real_target_cache_outputs(self):
|
|
source = self.action_text()
|
|
self.assertIn(
|
|
"value: ${{ steps.target.outputs.cache-matched-key }}",
|
|
source,
|
|
)
|
|
self.assertIn(
|
|
"value: ${{ steps.cache-summary.outputs.legacy-fallback-invoked }}",
|
|
source,
|
|
)
|
|
self.assertIn(
|
|
"value: ${{ steps.cache-summary.outputs.legacy-fallback-outcome }}",
|
|
source,
|
|
)
|
|
self.assertIn(
|
|
"TARGET_CACHE_HIT: ${{ steps.target.outputs.cache-hit }}",
|
|
source,
|
|
)
|
|
self.assertIn(
|
|
"TARGET_CACHE_MATCHED_KEY: ${{ steps.target.outputs.cache-matched-key }}",
|
|
source,
|
|
)
|
|
self.assertIn("SAVE_KEY: ${{ steps.identity.outputs.key }}", source)
|
|
self.assertIn("CACHE_WORKLOAD: ${{ inputs.key }}", source)
|
|
self.assertIn("RUNNER_OS_VALUE: ${{ runner.os }}", source)
|
|
self.assertIn("RUNNER_ARCH_VALUE: ${{ runner.arch }}", source)
|
|
|
|
summary = source.split("- name: Summarize compiled target cache", 1)[1]
|
|
summary = summary.split("\n - name:", 1)[0]
|
|
run_body = summary.split(" run: |", 1)[1]
|
|
self.assertNotIn("${{", run_body)
|
|
for field in (
|
|
"Target cache exact hit",
|
|
"Target cache matched key",
|
|
"Save key",
|
|
"Cache publishing enabled",
|
|
"Legacy fallback invoked",
|
|
"Legacy fallback outcome",
|
|
):
|
|
self.assertIn(field, run_body)
|
|
|
|
def test_legacy_fallback_is_distinct_from_target_cache_hit(self):
|
|
source = self.action_text()
|
|
legacy = source.split("- name: Recover an existing combined cache during migration", 1)[1]
|
|
self.assertIn("id: legacy-fallback", legacy)
|
|
self.assertIn(
|
|
"if: steps.target.outputs.cache-matched-key == '' && inputs.legacy-key != ''",
|
|
legacy,
|
|
)
|
|
self.assertIn("LEGACY_OUTCOME: ${{ steps.legacy-fallback.outcome }}", source)
|
|
self.assertIn("legacy-fallback-invoked=", source)
|
|
self.assertIn("Legacy fallback status is separate and is not counted as target exact hit", source)
|
|
self.assertNotIn("LEGACY_KEY:", source)
|
|
self.assertNotIn("ACTIONS_RUNTIME_TOKEN", source)
|
|
self.assertNotIn("GITHUB_TOKEN", source)
|
|
|
|
@unittest.skipUnless(os.name != "nt" and shutil.which("bash"), "POSIX summary shell")
|
|
def test_summary_executes_exact_partial_and_legacy_cache_cases(self):
|
|
source = self.action_text().split("- name: Summarize compiled target cache", 1)[1]
|
|
script = textwrap.dedent(source.split(" run: |\n", 1)[1].split("\n - name:", 1)[0])
|
|
cases = (
|
|
("true", "current-key", "false", "skipped", "false"),
|
|
("false", "older-compatible-key", "true", "skipped", "false"),
|
|
("", "", "true", "success", "true"),
|
|
("", "", "true", "failure", "true"),
|
|
)
|
|
for hit, matched, configured, outcome, invoked in cases:
|
|
with self.subTest(hit=hit, matched=matched, outcome=outcome), tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
marker = root / "must-not-be-created"
|
|
workload = f"$(touch {marker})"
|
|
environment = {
|
|
"PATH": os.environ["PATH"],
|
|
"GITHUB_STEP_SUMMARY": str(root / "summary"),
|
|
"GITHUB_OUTPUT": str(root / "outputs"),
|
|
"TARGET_CACHE_HIT": hit, "TARGET_CACHE_MATCHED_KEY": matched,
|
|
"SAVE_KEY": "new-save-key", "LEGACY_CONFIGURED": configured,
|
|
"CACHE_SAVE_ENABLED": "false",
|
|
"LEGACY_OUTCOME": outcome, "CACHE_WORKLOAD": workload,
|
|
"RUNNER_OS_VALUE": "Linux", "RUNNER_ARCH_VALUE": "X64",
|
|
}
|
|
result = subprocess.run([shutil.which("bash"), "-c", script], env=environment,
|
|
text=True, capture_output=True, check=False)
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
summary = (root / "summary").read_text(encoding="utf-8")
|
|
outputs = (root / "outputs").read_text(encoding="utf-8")
|
|
self.assertIn(f"Target cache exact hit: `{hit or 'false'}`", summary)
|
|
self.assertIn(f"Target cache matched key: `{matched or '<none>'}`", summary)
|
|
self.assertIn("Cache publishing enabled: `false`", summary)
|
|
self.assertIn(f"legacy-fallback-invoked={invoked}\n", outputs)
|
|
self.assertIn(f"legacy-fallback-outcome={outcome}\n", outputs)
|
|
self.assertIn(workload, summary)
|
|
self.assertFalse(marker.exists(), "summary values must never execute as shell code")
|
|
|
|
def test_dependency_snapshot_revision_preserves_compatibility_boundaries(self):
|
|
source = self.action_text()
|
|
self.assertIn("CACHE_REVISION: ${{ inputs.revision || github.sha }}", source)
|
|
snapshot = self.identity(CACHE_REVISION="dependencies-v1")
|
|
changed = self.identity(CACHE_REVISION="dependencies-v1", CACHE_MANIFESTS="new-dependencies")
|
|
self.assertNotEqual(snapshot["key"], changed["key"])
|
|
self.assertEqual(snapshot["restore-key"], changed["restore-key"])
|
|
|
|
def test_source_downloads_share_architectures_but_targets_do_not(self):
|
|
source = self.action_text()
|
|
self.assertIn("key: cargo-downloads-v3-${{ runner.os }}-${{ hashFiles('Cargo.lock', '**/Cargo.toml') }}", source)
|
|
self.assertIn("cargo-downloads-v2-${{ runner.os }}-${{ runner.arch }}-", source)
|
|
fetch = source.split("- name: Populate complete downloads before sharing the cache", 1)[1]
|
|
for targets in (
|
|
"--target x86_64-pc-windows-msvc --target aarch64-pc-windows-msvc",
|
|
"--target x86_64-apple-darwin --target aarch64-apple-darwin",
|
|
):
|
|
self.assertIn(f"cargo fetch --locked {targets}", fetch)
|
|
self.assertNotEqual(self.identity(CACHE_ARCH="X64")["restore-key"],
|
|
self.identity(CACHE_ARCH="ARM64")["restore-key"])
|
|
|
|
def test_read_only_consumers_neither_prefetch_other_architectures_nor_save(self):
|
|
source = self.action_text()
|
|
self.assertIn("value: ${{ inputs.save-if }}", source)
|
|
for step in ("Populate complete downloads before sharing the cache", "Save complete shared Cargo downloads"):
|
|
body = source.split(f"- name: {step}", 1)[1].split("\n - name:", 1)[0]
|
|
self.assertIn("if: inputs.save-if == 'true' && steps.downloads.outputs.cache-hit != 'true'", body)
|
|
|
|
@unittest.skipUnless(os.name == "nt", "Windows cache migration")
|
|
def test_windows_migration_preserves_compiler_and_workload_boundaries(self):
|
|
before = self.identity()
|
|
after = self.identity(ImageOS="win22")
|
|
self.assertEqual(before["restore-key"], after["migration-key"])
|
|
for change in ({"compiler": "rustc 1.98.0"}, {"CACHE_WORKLOAD": "release"},
|
|
{"RUSTFLAGS": "-C opt-level=1"}):
|
|
self.assertNotEqual(after["migration-key"], self.identity(**change)["migration-key"])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|