Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ venv/
.coverage
.DS_Store
.harel/
.cache/
6 changes: 0 additions & 6 deletions .gitmodules

This file was deleted.

26 changes: 17 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion src/harel/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,4 @@
"__version__",
]

__version__ = "0.1.0"
__version__ = "0.0.1"
43 changes: 43 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -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()
33 changes: 23 additions & 10 deletions tests/harness.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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(
Expand Down
37 changes: 32 additions & 5 deletions tests/test_conformance.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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():
Expand All @@ -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"

Expand Down
1 change: 0 additions & 1 deletion vendor/harel
Submodule harel deleted from fd4f42
1 change: 0 additions & 1 deletion vendor/harel-conformance
Submodule harel-conformance deleted from dd61c0
Loading