feat(tests): implement historical publication fixture preparation and validation tests

This commit is contained in:
maksyuki committed 2026-10-15 02:57:30 +08:00
1 parent 01e33b6209
commit 655c5871ea
6 files changed
+196 -4

No files matched your search

+8 -1
View File
@@ -13,10 +13,17 @@ to merge-commit integration. These documents do not configure branch protection.
`workflows/quality.yml` runs the quality gate. It restores the locked IHP130
and Tiny ICS55 PDKs (including the locked SRAM, IO and PLL views), third-party
simulation models, and APU/NPU references and installs the
locked Verilator, Icarus, sv2v, and Yosys tools before the complete Pytest
locked Verilator, Icarus, sv2v, Yosys and RISC-V GNU tools before the complete Pytest
suite. The reference cache includes the NPU MLPerf Tiny models, MFCC inputs,
TensorFlow/gemmlowp oracle sources, and decoded VWW corpus; setup still
verifies every locked revision and checksum before tests run.
The quality checkout retains repository history. After preparing current RTL
dependencies, `python3 tests/publication_history.py prepare` fetches any missing
dependency commits named by the immutable publication fixtures' historical
locks. It preserves active dependency HEADs and working trees; fixture export
and Pytest remain offline.
`workflows/regression-smoke.yml`,
`workflows/regression-ihp130.yml`, `workflows/regression-gf180.yml`,
`workflows/regression-ics55.yml`, `workflows/regression-sky130.yml`, and
+3
View File
@@ -91,6 +91,9 @@ jobs:
python3 rtl/managed/clusterip/setup.py
python3 rtl/ip/setup.py
- name: Prepare historical publication dependencies
run: python3 tests/publication_history.py prepare
- name: Set up locked IHP130 PDK
run: make CONFIG=configs/ci/ihp130.mk setup-pdk
+19
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
import hashlib
import os
import re
import subprocess
import tempfile
import time
@@ -28,6 +29,24 @@ def git_output(repo: Path, *args: str) -> str:
).strip()
def ensure_git_commit(url: str, destination: Path, revision: str) -> None:
"""Fetch a locked historical object without switching the active checkout."""
if re.fullmatch(r"[0-9a-f]{40}", revision) is None:
raise ValueError("historical dependency requires a full Git commit revision")
destination = destination.resolve()
if not (destination / ".git").exists():
raise RuntimeError(f"prepare the current dependency checkout first: {destination}")
try:
git_output(destination, "cat-file", "-e", f"{revision}^{{commit}}")
except subprocess.CalledProcessError:
# No checkout/refspec: preserve HEAD, index, worktree and FETCH_HEAD.
run(("git", "fetch", "--no-tags", "--no-write-fetch-head", "--depth", "1",
url, revision), cwd=destination)
if git_output(destination, "rev-parse", f"{revision}^{{commit}}") != revision:
raise RuntimeError(f"historical dependency commit mismatch: {destination}: {revision}")
print(f"[dependency] historical object available: {destination.name}: {revision}")
def ensure_git_repo(
url: str,
destination: Path,
+7
View File
@@ -116,3 +116,10 @@ python3 -m pytest -q
These tests complement, but do not replace, firmware builds, RTL simulation,
synthesis, timing, or hardware validation.
Historical Tiny publication fixtures also need the main repository history
and dependency commit objects named by their archived locks. After ordinary
dependency setup, run `python3 tests/publication_history.py prepare` before
Pytest on a fresh checkout. This only fetches missing immutable Git objects;
it does not switch current managed checkouts or alter either lock. Historical
fixture export remains offline and fails if required inputs are unavailable.
+34 -3
View File
@@ -1,4 +1,4 @@
"""Materialize immutable, locally available publication test inputs.
"""Prepare historical Git objects, then export offline publication fixtures.
This is a test fixture, never a publication promotion or a current-RTL verdict.
Each exported tree keeps its own adapter, technical contract, tests and lock.
@@ -8,12 +8,35 @@ from __future__ import annotations
import io
import json
import subprocess
import sys
import tarfile
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from scripts.setup_helpers import ensure_git_commit, git_output # noqa: E402
ORIGINAL_BOOK = "2805ea91833a6bede32f3a82df3d17d20eb5ec77"
P5_CONTRACT = "b3b30a72cd6ef5340c28e5044c96cd13258ec5a6"
HISTORICAL_REVISIONS = (ORIGINAL_BOOK, P5_CONTRACT)
HISTORICAL_SOURCES = ("cluster_common", "cluster_archinfo", "cluster_pwm",
"cluster_rtc", "cluster_wdg", "hazard3")
def prepare(root: Path = ROOT) -> None:
"""Restore each fixture's locked objects, never switch active dependencies."""
root = root.resolve()
inputs = set()
for revision in HISTORICAL_REVISIONS:
lock = json.loads(git_output(root, "show", f"{revision}:dependencies/dependencies.lock.json"))
for name in HISTORICAL_SOURCES:
source = lock["sources"][name]
destination = (root / source["destination"]).resolve()
if destination == root or not destination.is_relative_to(root):
raise ValueError(f"historical dependency destination escapes checkout: {name}")
inputs.add((source["url"], destination, source["revision"]))
for url, destination, revision in sorted(inputs):
ensure_git_commit(url, destination, revision)
def export(repository: Path, revision: str, destination: Path, paths: list[str] = ()) -> None:
@@ -32,9 +55,17 @@ def snapshot(destination: Path, revision: str) -> Path:
"configs", "docs", "dependencies", "LICENSE",
"Makefile", "README.md", "tests"])
lock = json.loads((destination / "dependencies/dependencies.lock.json").read_text())
for name in ("cluster_common", "cluster_archinfo", "cluster_pwm", "cluster_rtc",
"cluster_wdg", "hazard3"):
for name in HISTORICAL_SOURCES:
source = lock["sources"][name]
export(ROOT / source["destination"], source["revision"], destination / source["destination"],
["hdl"] if name == "hazard3" else [])
return destination
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("action", choices=("prepare",))
parser.parse_args()
prepare()
+125
View File
@@ -0,0 +1,125 @@
"""Cold shallow dependencies must support immutable publication snapshots."""
from __future__ import annotations
import json
import subprocess
from pathlib import Path
import pytest
import yaml
import publication_history as history
from scripts import setup_helpers
def git(repository: Path, *args: str) -> str:
return subprocess.check_output(["git", "-C", str(repository), *args], text=True).strip()
def init(repository: Path) -> None:
repository.mkdir(parents=True)
git(repository, "init")
git(repository, "config", "user.name", "Fixture")
git(repository, "config", "user.email", "fixture@example.com")
git(repository, "config", "commit.gpgsign", "false")
def commit(repository: Path, path: str, content: str) -> str:
target = repository / path
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(content)
git(repository, "add", path)
git(repository, "commit", "-m", "fixture")
return git(repository, "rev-parse", "HEAD")
@pytest.fixture
def shallow_dependency(tmp_path: Path):
upstream = tmp_path / "upstream"
init(upstream)
old = commit(upstream, "value.txt", "historical interface\n")
current = commit(upstream, "value.txt", "current interface\n")
checkout = tmp_path / "project/rtl/managed/pwm"
setup_helpers.ensure_git_repo(upstream.as_uri(), checkout, current)
assert git(checkout, "rev-parse", "--is-shallow-repository") == "true"
assert subprocess.run(["git", "-C", str(checkout), "cat-file", "-e", old + "^{commit}"],
capture_output=True).returncode != 0
return upstream, checkout, old, current
def test_prepare_uses_historical_lock_and_preserves_active_checkout(
shallow_dependency, tmp_path: Path, monkeypatch: pytest.MonkeyPatch,
) -> None:
upstream, checkout, old, current = shallow_dependency
root = tmp_path / "project"
# The dependency directory already exists; initialize only the parent repo.
git(root, "init")
git(root, "config", "user.name", "Fixture")
git(root, "config", "user.email", "fixture@example.com")
git(root, "config", "commit.gpgsign", "false")
spec = {"url": upstream.as_uri(), "destination": "rtl/managed/pwm", "revision": old}
lock_path = "dependencies/dependencies.lock.json"
historical = commit(root, lock_path, json.dumps({"sources": {"cluster_pwm": spec}}))
spec["revision"] = current
commit(root, lock_path, json.dumps({"sources": {"cluster_pwm": spec}}))
lock_before = (root / lock_path).read_bytes()
monkeypatch.setattr(history, "HISTORICAL_REVISIONS", (historical,))
monkeypatch.setattr(history, "HISTORICAL_SOURCES", ("cluster_pwm",))
# Reproduce CI: root history exists, but the old PWM object does not.
missing = subprocess.run(["git", "-C", str(checkout), "archive", old], capture_output=True)
assert missing.returncode != 0 and b"not a tree object" in missing.stderr
(checkout / "value.txt").write_text("user staged edit\n")
git(checkout, "add", "value.txt")
(checkout / "value.txt").write_text("user unstaged edit\n")
(checkout / "note.txt").write_text("user untracked file\n")
status_before = git(checkout, "status", "--porcelain")
index_before = git(checkout, "write-tree")
fetch_before = (checkout / ".git/FETCH_HEAD").read_bytes()
history.prepare(root)
assert git(checkout, "rev-parse", "HEAD") == current
assert git(checkout, "write-tree") == index_before
assert git(checkout, "status", "--porcelain") == status_before
assert (checkout / ".git/FETCH_HEAD").read_bytes() == fetch_before
assert (checkout / "value.txt").read_text() == "user unstaged edit\n"
assert (checkout / "note.txt").read_text() == "user untracked file\n"
assert (root / lock_path).read_bytes() == lock_before
history.export(checkout, old, tmp_path / "historical-export")
assert (tmp_path / "historical-export/value.txt").read_text() == "historical interface\n"
def no_fetch(*args, **kwargs):
pytest.fail("cached historical commits must be usable offline")
monkeypatch.setattr(setup_helpers, "run", no_fetch)
history.prepare(root)
def test_failed_historical_fetch_preserves_checkout(shallow_dependency, tmp_path: Path) -> None:
_, checkout, old, current = shallow_dependency
with pytest.raises(subprocess.CalledProcessError):
setup_helpers.ensure_git_commit((tmp_path / "missing-upstream").as_uri(), checkout, old)
assert git(checkout, "rev-parse", "HEAD") == current
assert git(checkout, "status", "--porcelain") == ""
assert (checkout / "value.txt").read_text() == "current interface\n"
@pytest.mark.parametrize("revision", ["main", "1234", "x" * 40, "--all"])
def test_historical_fetch_rejects_unpinned_revision(tmp_path: Path, revision: str) -> None:
with pytest.raises(ValueError, match="full Git commit"):
setup_helpers.ensure_git_commit("unused", tmp_path / "uncreated", revision)
assert not (tmp_path / "uncreated").exists()
def test_quality_prepares_history_before_offline_pytest() -> None:
root = Path(__file__).resolve().parents[1]
workflow = yaml.safe_load((root / ".github/workflows/quality.yml").read_text())
steps = workflow["jobs"]["scripts"]["steps"]
checkout = next(step for step in steps if step["name"] == "Check out repository")
assert checkout["with"]["fetch-depth"] == 0
dependencies = next(step for step in steps if step["name"] == "Set up locked RTL dependencies")
preparation = next(step for step in steps
if step.get("run") == "python3 tests/publication_history.py prepare")
tests = next(step for step in steps if step.get("run") == "python3 -m pytest -q")
assert steps.index(dependencies) < steps.index(preparation) < steps.index(tests)
assert "if" not in preparation and not preparation.get("continue-on-error", False)