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
33 changes: 32 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
93 changes: 93 additions & 0 deletions tests/test_library_api.py
Original file line number Diff line number Diff line change
@@ -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"
Loading