From 77c437007aa38021a195e6a49103c85f197057c6 Mon Sep 17 00:00:00 2001 From: Christian-Manuel Butzke Date: Tue, 30 Jun 2026 21:43:21 +0900 Subject: [PATCH] tests/release: replace submodules with version-pinned conformance fetch; adopt v0.0.1 Drop both git submodules. The conformance suite is fetched from fruwehq/harel-conformance at the release tag matching this package's version (falling back to main until the tag exists) into a gitignored .cache/ by a pytest fixture; HAREL_CONFORMANCE_DIR overrides with a local checkout for offline work. The schema-drift test fetches the spec schema by raw URL (HAREL_SPEC_DIR override), skipping when offline. Set the synchronized version to 0.0.1 and declare the implemented spec version in the README. CI no longer checks out submodules. Closes #20. --- .github/workflows/test.yml | 2 -- .gitignore | 1 + .gitmodules | 6 ------ README.md | 26 +++++++++++++++-------- pyproject.toml | 2 +- src/harel/__init__.py | 2 +- tests/conftest.py | 43 ++++++++++++++++++++++++++++++++++++++ tests/harness.py | 33 ++++++++++++++++++++--------- tests/test_conformance.py | 37 +++++++++++++++++++++++++++----- vendor/harel | 1 - vendor/harel-conformance | 1 - 11 files changed, 118 insertions(+), 36 deletions(-) delete mode 100644 .gitmodules create mode 100644 tests/conftest.py delete mode 160000 vendor/harel delete mode 160000 vendor/harel-conformance diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3e78128..e736541 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,8 +15,6 @@ jobs: os: [ubuntu-24.04] steps: - uses: actions/checkout@v4 - with: - submodules: true - uses: actions/setup-python@v5 with: python-version: "3.13" diff --git a/.gitignore b/.gitignore index 9727ad0..604e15f 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ venv/ .coverage .DS_Store .harel/ +.cache/ diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 74e4047..0000000 --- a/.gitmodules +++ /dev/null @@ -1,6 +0,0 @@ -[submodule "vendor/harel"] - path = vendor/harel - url = https://github.com/fruwehq/harel.git -[submodule "vendor/harel-conformance"] - path = vendor/harel-conformance - url = https://github.com/fruwehq/harel-conformance.git diff --git a/README.md b/README.md index 8d46f6a..cd923bd 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,9 @@ The normative `SPEC.md`, the JSON Schema for machine YAML, and the cross-languag **conformance suite** live in the spec repo. This repository implements that spec in Python and is correct **iff it passes the conformance suite**. +Implements the **harel spec v0.0.1** (early alpha; all fruwehq harel repos share one +[synchronized version](https://github.com/fruwehq/harel)). + Status: **passing the full conformance suite** — all 22 engine cases (`conformance/01`–`22`) plus `conformance/cli/01`–`02`. Implements YAML 1.2 loading + validation, the full statechart semantics (RTC dispatch, hierarchy, orthogonal @@ -19,13 +22,19 @@ the build order in [issue #3][issue]. ## Conformance suite -The cross-language **conformance suite** is consumed as a pinned git submodule at -[`vendor/harel-conformance`](vendor/harel-conformance) (single source of truth — no -copy-paste drift); the harness in `tests/` discovers `conformance/*/` from there. The -normative `SPEC.md` and JSON Schema live in -[`fruwehq/harel`](https://github.com/fruwehq/harel), pinned at -[`vendor/harel`](vendor/harel) solely for the schema-drift check. This repository is -correct **iff it passes the suite**. +The cross-language **conformance suite** is the single source of truth for correctness; +this repository is correct **iff it passes it**. The suite lives in +[`fruwehq/harel-conformance`](https://github.com/fruwehq/harel-conformance); the test +harness **fetches it at the matching release tag** (`v0.0.1`) into a gitignored +`.cache/` — no git submodule. The normative `SPEC.md` and JSON Schema live in +[`fruwehq/harel`](https://github.com/fruwehq/harel); the schema-drift test fetches the +schema at the same tag. + +For **offline** work, point the harness at a local checkout: +``` +export HAREL_CONFORMANCE_DIR=/path/to/harel-conformance # the suite +export HAREL_SPEC_DIR=/path/to/harel # the schema (optional) +``` ## Scope (per the spec) - Load and validate machine YAML against `schema/machine.schema.json`, parsed under @@ -82,10 +91,9 @@ error types. See [`tests/test_library_api.py`](tests/test_library_api.py). ## Develop ``` -git submodule update --init # fetch the conformance suite + schema (two submodules) python -m venv .venv && . .venv/bin/activate pip install -e '.[dev]' -ruff check . && mypy src/harel && pytest +ruff check . && mypy src/harel && pytest # the suite is fetched into .cache/ on first run ``` ## License diff --git a/pyproject.toml b/pyproject.toml index 4400b4d..62465cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "harel-python" -version = "0.1.0" +version = "0.0.1" description = "Python reference implementation of the harel statechart engine" readme = "README.md" requires-python = ">=3.11" diff --git a/src/harel/__init__.py b/src/harel/__init__.py index 6d172d0..e7b3760 100644 --- a/src/harel/__init__.py +++ b/src/harel/__init__.py @@ -37,4 +37,4 @@ "__version__", ] -__version__ = "0.1.0" +__version__ = "0.0.1" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..703d866 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,43 @@ +"""Fetch the language-agnostic conformance suite before test collection. + +The suite lives in ``fruwehq/harel-conformance`` (no git submodule). It is cloned at the +release tag matching this package's version (falling back to ``main`` while the tag does +not yet exist) into a gitignored ``.cache/`` directory and reused. Override with a local +checkout via ``HAREL_CONFORMANCE_DIR`` for offline work. If the suite cannot be obtained +(offline, no override), the conformance tests skip rather than error. +""" + +from __future__ import annotations + +import os +import subprocess +from pathlib import Path + +import harel + +_ROOT = Path(__file__).resolve().parent.parent +_CACHE = _ROOT / ".cache" / "harel-conformance" +_REPO = "https://github.com/fruwehq/harel-conformance.git" + + +def _ensure_conformance() -> None: + if os.environ.get("HAREL_CONFORMANCE_DIR"): + return # caller provides a local checkout + if (_CACHE / ".git").exists(): + return # already fetched; reuse (force a refresh by deleting .cache/) + _CACHE.parent.mkdir(parents=True, exist_ok=True) + # Prefer the release tag matching our version; fall back to main (tags may not exist + # yet pre-release). Network/tooling failure leaves the suite absent -> tests skip. + for ref in (f"v{harel.__version__}", "main"): + try: + subprocess.run( + ["git", "clone", "--depth", "1", "--branch", ref, _REPO, str(_CACHE)], + check=True, + capture_output=True, + ) + return + except (subprocess.CalledProcessError, OSError): + continue + + +_ensure_conformance() diff --git a/tests/harness.py b/tests/harness.py index fb0c6f3..0f3fd0e 100644 --- a/tests/harness.py +++ b/tests/harness.py @@ -1,14 +1,16 @@ """Conformance-suite harness helpers. -The suite is consumed from the ``vendor/harel`` git submodule (SPEC §9). These -helpers locate the suite, enumerate cases, and run engine cases against this -implementation (create the root as id ``root``, per step ``send``/``advance``, -run all instances to quiescence, then check ``expect``). +The language-agnostic suite (SPEC §9) lives in ``fruwehq/harel-conformance`` and is +fetched at the matching release tag by ``conftest.py`` (no git submodule). These helpers +locate the fetched suite, enumerate cases, and run engine cases against this +implementation (create the root as id ``root``, per step ``send``/``advance``, run all +instances to quiescence, then check ``expect``). """ from __future__ import annotations import importlib.util +import os import sys from dataclasses import dataclass from pathlib import Path @@ -18,10 +20,19 @@ from harel import Host REPO_ROOT = Path(__file__).resolve().parent.parent -# The spec repo (fruwehq/harel) is pinned only for the schema-drift check; the -# language-agnostic conformance suite lives in fruwehq/harel-conformance. -SPEC_DIR = REPO_ROOT / "vendor" / "harel" -CONFORMANCE_DIR = REPO_ROOT / "vendor" / "harel-conformance" / "conformance" + + +def conformance_root() -> Path: + """Root of the fetched ``harel-conformance`` checkout. + + ``HAREL_CONFORMANCE_DIR`` overrides with a local checkout (offline/dev); otherwise + the cache populated by ``conftest.py`` is used. + """ + env = os.environ.get("HAREL_CONFORMANCE_DIR") + return Path(env) if env else REPO_ROOT / ".cache" / "harel-conformance" + + +CONFORMANCE_DIR = conformance_root() / "conformance" # Cases the engine is known to pass. Others are skipped until their features # land; extend this set as build-order steps are completed. @@ -78,6 +89,8 @@ def _machine_files(case_dir: Path) -> list[Path]: def engine_cases() -> list[EngineCase]: """All engine conformance cases (``conformance/01``–``22``), sorted.""" + if not CONFORMANCE_DIR.exists(): + return [] cases: list[EngineCase] = [] for case_dir in sorted(p for p in CONFORMANCE_DIR.iterdir() if p.is_dir()): machine_files = _machine_files(case_dir) @@ -108,8 +121,8 @@ def run_cli_case(case_dir: Path) -> None: """Run a CLI case **black-box** via the spec repo's reference runner (§13.6). Invokes this package as a subprocess (``python -m harel``), so packaging and - entry-point regressions are caught — not an in-process import. Delegating to - ``vendor/harel/conformance/run_cli.py`` also avoids harness drift. + entry-point regressions are caught — not an in-process import. Delegating to the + suite's ``conformance/run_cli.py`` also avoids harness drift. """ runner = _load_cli_runner() rc = runner.main( diff --git a/tests/test_conformance.py b/tests/test_conformance.py index 471b849..52496bd 100644 --- a/tests/test_conformance.py +++ b/tests/test_conformance.py @@ -13,15 +13,19 @@ from __future__ import annotations import json +import os +import urllib.error +import urllib.request from pathlib import Path import pytest +import harel from harel import load_definitions from harel.validator import schema as bundled_schema from .harness import ( - SPEC_DIR, + CONFORMANCE_DIR, SUPPORTED, cli_cases, engine_cases, @@ -30,6 +34,26 @@ ) +def _spec_schema() -> dict | None: + """The normative schema from fruwehq/harel at the matching tag (or a local override). + + Returns ``None`` when offline and no ``HAREL_SPEC_DIR`` override is set, so the + drift test can skip rather than fail. + """ + override = os.environ.get("HAREL_SPEC_DIR") + if override: + p = Path(override) / "schema" / "machine.schema.json" + return json.loads(p.read_text(encoding="utf-8")) if p.exists() else None + for ref in (f"v{harel.__version__}", "main"): + url = f"https://raw.githubusercontent.com/fruwehq/harel/{ref}/schema/machine.schema.json" + try: + with urllib.request.urlopen(url, timeout=10) as resp: # noqa: S310 (fixed host) + return json.loads(resp.read()) + except (urllib.error.URLError, OSError, ValueError): + continue + return None + + def _each_machine_file() -> list[pytest.Param]: params: list[pytest.Param] = [] for case in engine_cases(): @@ -50,14 +74,17 @@ def test_machine_file_loads_and_validates(path: Path) -> None: assert d.id == d.raw["id"] -def test_bundled_schema_matches_submodule() -> None: +def test_bundled_schema_matches_spec() -> None: """The engine's bundled schema must equal the spec repo's schema (no drift).""" - upstream = SPEC_DIR / "schema" / "machine.schema.json" - assert upstream.exists(), "harel (spec) submodule not initialized" - assert json.loads(upstream.read_text(encoding="utf-8")) == bundled_schema() + upstream = _spec_schema() + if upstream is None: + pytest.skip("spec schema unavailable (offline; set HAREL_SPEC_DIR to a harel checkout)") + assert upstream == bundled_schema() def test_suite_present() -> None: + if not CONFORMANCE_DIR.exists(): + pytest.skip("conformance suite not fetched (offline; set HAREL_CONFORMANCE_DIR)") assert len(engine_cases()) == 22, "expected 22 engine cases" assert len(cli_cases()) == 2, "expected 2 CLI cases" diff --git a/vendor/harel b/vendor/harel deleted file mode 160000 index fd4f42b..0000000 --- a/vendor/harel +++ /dev/null @@ -1 +0,0 @@ -Subproject commit fd4f42ba6efe6e9533c68321bfa72beacbff8250 diff --git a/vendor/harel-conformance b/vendor/harel-conformance deleted file mode 160000 index dd61c00..0000000 --- a/vendor/harel-conformance +++ /dev/null @@ -1 +0,0 @@ -Subproject commit dd61c0051b9d7164b9eeaf95d9109d1a3eb2ef0b