From 734d9b59ca23e287a35e798d60911e42a40c10a9 Mon Sep 17 00:00:00 2001 From: Christian-Manuel Butzke Date: Tue, 30 Jun 2026 16:54:28 +0900 Subject: [PATCH] =?UTF-8?q?lib:=20document=20+=20test=20the=20public=20lib?= =?UTF-8?q?rary=20API=20(SPEC=20=C2=A72)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The engine is embeddable without the CLI or the file store. Add a README 'Use as a library' section with a verified runnable example, and a test that drives a machine end-to-end through the public surface only (load+validate, register + create root with external esvs, deliver typed event + run to quiescence, advance the clock, read status/config/esvs, snapshot/restore) and pins harel.__all__. Closes #13. --- README.md | 33 +++++++++++++- tests/test_library_api.py | 93 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 tests/test_library_api.py diff --git a/README.md b/README.md index fa5c1d4..e38bb84 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ The normative `SPEC.md`, the JSON Schema for machine YAML, and the cross-languag Python and is correct **iff it passes the conformance suite**. Status: **passing the full conformance suite** — all 22 engine cases -(`conformance/01`–`22`) plus `conformance/cli/01`. Implements YAML 1.2 loading +(`conformance/01`–`22`) plus `conformance/cli/01`–`02`. Implements YAML 1.2 loading + validation, the full statechart semantics (RTC dispatch, hierarchy, orthogonal regions + `done`, shallow/deep history, esvs, CEL guards, structured actions, active objects + bus, defer, timers, faults), static contracts, snapshot @@ -43,6 +43,37 @@ passes the suite**. exporter interface so more formats (PlantUML, SCXML, …) can be added later. - A test harness that runs the upstream conformance cases against this engine. +## Use as a library +The CLI (`harel …`) is a thin wrapper over a programmatic API; an engine can be +embedded in a host program **without** the CLI or the file-backed store (SPEC §2): + +```python +import harel + +defs = harel.load_definitions(open("gate.yaml").read()) +harel.validate(defs[0].raw) # raises ValidationError if invalid + +host = harel.Host() +host.register_all(defs) +inst = host.create_root(host.machines["gate"], "g1", external={"fare": 50}) +host.run_to_quiescence() + +host.deliver("g1", "coin", {"amount": 100}) # typed event; False if rejected +host.run_to_quiescence() +assert inst.active_leaf_names() == ["unlocked"] +assert inst.resolved_esvs()["fare"] == 50 +assert inst.status is harel.Status.ACTIVE + +host.advance("30s") # virtual clock +snaps = host.snapshot_all() # persist / round-trip (§8) +host.restore_all(snaps) +``` + +The public surface is everything exported from the top-level `harel` package +(`harel.__all__`): `Host`, `Instance`, `Definition`, `Machine`, `Status`, `Event`, +`load_definitions` / `load_definition`, `validate` / `collect_errors`, and the +error types. See [`tests/test_library_api.py`](tests/test_library_api.py). + ## Layout - `src/harel/` — the package. - `tests/` — unit tests and the conformance harness. diff --git a/tests/test_library_api.py b/tests/test_library_api.py new file mode 100644 index 0000000..bbb3f1e --- /dev/null +++ b/tests/test_library_api.py @@ -0,0 +1,93 @@ +"""The public library API (SPEC §2 "Library API"). + +Drives a machine end-to-end through ``harel``'s public surface **only** — no +``harel.cli`` and no file-backed store — exercising every capability the spec +requires an embeddable API to provide. +""" + +from __future__ import annotations + +import harel + +GATE = """\ +id: gate +events: + coin: { payload: { amount: { type: int, required: true } } } + push: {} +top: + esvs: + fare: { type: int, external: true } # host-seeded, read-only (SPEC §4.4) + initial: { transition_to: locked } + states: + locked: + on_events: + coin: { transition_to: unlocked, guard: "event.payload.amount >= fare" } + unlocked: + on_events: + push: { transition_to: locked } +""" + + +def test_minimum_capability_set_via_public_api() -> None: + # 1. load + validate a definition (raises ValidationError if invalid). + defs = harel.load_definitions(GATE) + for d in defs: + harel.validate(d.raw) + assert harel.collect_errors(defs[0].raw) == [] + + # 2. register definitions + create a root instance with an id and external esvs. + host = harel.Host() + host.register_all(defs) + inst = host.create_root(host.machines["gate"], "g1", external={"fare": 50}) + host.run_to_quiescence() + + # 3. read status, active configuration, and esvs. + assert inst.status is harel.Status.ACTIVE + assert inst.active_leaf_names() == ["locked"] + assert inst.resolved_esvs()["fare"] == 50 + + # 4. deliver a typed event + run to quiescence. + assert host.deliver("g1", "coin", {"amount": 100}) is True + host.run_to_quiescence() + assert inst.active_leaf_names() == ["unlocked"] + + # an invalid payload is rejected, not enqueued (§4.3). + assert host.deliver("g1", "coin", {"amount": "nope"}) is False + + # 5. advance the virtual clock. + host.advance("30s") + assert host.now == 30_000 + + # 6. snapshot + restore an instance (§8) — state survives the round-trip. + snaps = host.snapshot_all() + host.deliver("g1", "push", None) + host.run_to_quiescence() + assert inst.active_leaf_names() == ["locked"] + host.restore_all(snaps) + assert host.instances["g1"].active_leaf_names() == ["unlocked"] + + +def test_public_surface_is_exported() -> None: + """The documented public API stays importable from the top-level package.""" + expected = { + "Definition", + "Host", + "Instance", + "Machine", + "State", + "Status", + "Event", + "HarelError", + "SchemaError", + "ValidationError", + "ErrorRecord", + "CelError", + "load_definition", + "load_definitions", + "validate", + "collect_errors", + "__version__", + } + assert expected <= set(harel.__all__) + for name in expected: + assert hasattr(harel, name), f"harel.{name} not exported"