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
|
`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
|
and Tiny ICS55 PDKs (including the locked SRAM, IO and PLL views), third-party
|
||||||
simulation models, and APU/NPU references and installs the
|
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,
|
suite. The reference cache includes the NPU MLPerf Tiny models, MFCC inputs,
|
||||||
TensorFlow/gemmlowp oracle sources, and decoded VWW corpus; setup still
|
TensorFlow/gemmlowp oracle sources, and decoded VWW corpus; setup still
|
||||||
verifies every locked revision and checksum before tests run.
|
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-smoke.yml`,
|
||||||
`workflows/regression-ihp130.yml`, `workflows/regression-gf180.yml`,
|
`workflows/regression-ihp130.yml`, `workflows/regression-gf180.yml`,
|
||||||
`workflows/regression-ics55.yml`, `workflows/regression-sky130.yml`, and
|
`workflows/regression-ics55.yml`, `workflows/regression-sky130.yml`, and
|
||||||
|
|||||||
@@ -91,6 +91,9 @@ jobs:
|
|||||||
python3 rtl/managed/clusterip/setup.py
|
python3 rtl/managed/clusterip/setup.py
|
||||||
python3 rtl/ip/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
|
- name: Set up locked IHP130 PDK
|
||||||
run: make CONFIG=configs/ci/ihp130.mk setup-pdk
|
run: make CONFIG=configs/ci/ihp130.mk setup-pdk
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
@@ -28,6 +29,24 @@ def git_output(repo: Path, *args: str) -> str:
|
|||||||
).strip()
|
).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(
|
def ensure_git_repo(
|
||||||
url: str,
|
url: str,
|
||||||
destination: Path,
|
destination: Path,
|
||||||
|
|||||||
@@ -116,3 +116,10 @@ python3 -m pytest -q
|
|||||||
|
|
||||||
These tests complement, but do not replace, firmware builds, RTL simulation,
|
These tests complement, but do not replace, firmware builds, RTL simulation,
|
||||||
synthesis, timing, or hardware validation.
|
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.
|
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.
|
Each exported tree keeps its own adapter, technical contract, tests and lock.
|
||||||
@@ -8,12 +8,35 @@ from __future__ import annotations
|
|||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
import tarfile
|
import tarfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[1]
|
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"
|
ORIGINAL_BOOK = "2805ea91833a6bede32f3a82df3d17d20eb5ec77"
|
||||||
P5_CONTRACT = "b3b30a72cd6ef5340c28e5044c96cd13258ec5a6"
|
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:
|
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",
|
"configs", "docs", "dependencies", "LICENSE",
|
||||||
"Makefile", "README.md", "tests"])
|
"Makefile", "README.md", "tests"])
|
||||||
lock = json.loads((destination / "dependencies/dependencies.lock.json").read_text())
|
lock = json.loads((destination / "dependencies/dependencies.lock.json").read_text())
|
||||||
for name in ("cluster_common", "cluster_archinfo", "cluster_pwm", "cluster_rtc",
|
for name in HISTORICAL_SOURCES:
|
||||||
"cluster_wdg", "hazard3"):
|
|
||||||
source = lock["sources"][name]
|
source = lock["sources"][name]
|
||||||
export(ROOT / source["destination"], source["revision"], destination / source["destination"],
|
export(ROOT / source["destination"], source["revision"], destination / source["destination"],
|
||||||
["hdl"] if name == "hazard3" else [])
|
["hdl"] if name == "hazard3" else [])
|
||||||
return destination
|
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