mirror of
https://github.com/retroSoC/retroSoC.git
synced 2026-11-04 22:32:36 +00:00
feat(tests): implement historical publication fixture preparation and validation tests
This commit is contained in:
6 files changed
+196
-4
No files matched your search
+8
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user