conformance

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 30, 2026 License: Apache-2.0 Imports: 7 Imported by: 0

Documentation

Overview

Package conformance proves that a state machine behaves correctly.

It rests on the three pillars from the suite's conformance design:

  1. Oracle comparison — the effects a machine's Fire produces are diffed against a trusted reference implementation for the same input.
  2. Golden scenarios — committed event sequences are replayed and the final state, emitted effects, and trace are asserted.
  3. Round-trip identity — a machine authored in Go and the same machine loaded from JSON (then bound via Provide) are proven to behave identically.

Scenarios are derived from the machine graph: GenerateScenarios enumerates the shortest event path to every reachable state by breadth-first search over the IR, mirroring the path-planning model so a small machine yields full coverage without hand-authored fixtures. A Scenario and the Trace a run produces are both first-class, JSON-serializable artifacts: scenarios can be committed as goldens under testdata and replayed in CI, and a captured run can be diffed against a committed expectation.

The package depends only on the state kernel and the standard library, so it adds no third-party dependencies to a consumer that vendors it to prove its own machines correct.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func CompareMachines

func CompareMachines[S comparable, E comparable, C any](
	reference, subject *state.Machine[S, E, C],
	scenarios []Scenario,
	codec EventCodec[E],
	startState S,
	newEntity freshEntity[C],
	opts ...CompareOption,
) error

CompareMachines runs every scenario against two machines built from the same state/event/context types and reports the divergences. It is the oracle pillar generalized to two machine implementations: the reference (canonical) and the subject (under test). Both are Cast from an entity drawn fresh per scenario so a mutated run never bleeds into the next comparison.

A nil error means the subject conforms to the reference across every scenario.

func RoundTripIdentity

func RoundTripIdentity[S comparable, E comparable, C any](
	forged *state.Machine[S, E, C],
	reg *state.Registry[C],
	scenarios []Scenario,
	codec EventCodec[E],
	startState S,
	newEntity freshEntity[C],
	opts ...CompareOption,
) error

RoundTripIdentity proves the config/implementation split honest: a machine authored in Go and the same machine after ToJSON -> LoadFromJSON -> Provide -> Quench are the same machine, on both structure and behavior.

It performs two checks:

  1. Structural — the IR is byte-stable under a round-trip (serialize, reload, reserialize, compare).
  2. Behavioral — every scenario produces an identical result against the code-built machine and the JSON-loaded machine: same final state, same effects, same trace. Because behavior is rebound by name from the same registry, identity here is exact, not approximate.

The caller supplies the registry the JSON-loaded machine binds against (the same host palette the DSL registered), a fresh entity per run, and the start state. A divergence means the IR is lossy or the registry binding drifted, and is returned as an *ErrConformance.

Types

type Assertion

type Assertion struct {
	Type     AssertionType `json:"type"`
	Expected any           `json:"expected"`
}

Assertion is a declarative expectation about a scenario run. Assertions are descriptions, not predicates: a run records each as pass or fail and leaves the caller to decide whether a failure is fatal.

type AssertionResult

type AssertionResult struct {
	Type     AssertionType `json:"type"`
	Expected any           `json:"expected"`
	Actual   any           `json:"actual"`
	Pass     bool          `json:"pass"`
}

AssertionResult records one assertion's verdict after a run.

type AssertionType

type AssertionType string

AssertionType names a declarative scenario assertion.

const (
	AssertFinalState     AssertionType = "FinalState"
	AssertEffectsEmitted AssertionType = "EffectsEmitted"
	AssertTraceLength    AssertionType = "TraceLength"
	AssertNoErrors       AssertionType = "NoErrors"
)

The v1 assertion set covers final-state, emitted effects, trace length, and the absence of errors — enough for generated and hand-authored scenarios.

type CompareOption

type CompareOption func(*compareConfig)

CompareOption configures an oracle or round-trip comparison.

func IgnoreEffects

func IgnoreEffects() CompareOption

IgnoreEffects skips the emitted-effects comparison. Use it only when the two sides legitimately differ on effects (each use is a coverage hole).

func IgnoreTrace

func IgnoreTrace() CompareOption

IgnoreTrace skips the per-step trace comparison, comparing final state and effects only.

type ErrConformance

type ErrConformance struct {
	Mismatches []Mismatch
}

ErrConformance aggregates the mismatches found across an oracle comparison or a round-trip identity check. A nil error means the two sides agreed.

func (*ErrConformance) Error

func (e *ErrConformance) Error() string

type ErrSchemaVersion

type ErrSchemaVersion struct {
	Got  int
	Want int
}

ErrSchemaVersion is returned when a serialized artifact carries a schema version this package does not understand.

func (*ErrSchemaVersion) Error

func (e *ErrSchemaVersion) Error() string

type ErrUnknownEvent

type ErrUnknownEvent struct {
	Name string
}

ErrUnknownEvent is returned when a scenario names an event the codec cannot resolve to a typed value.

func (*ErrUnknownEvent) Error

func (e *ErrUnknownEvent) Error() string

type Event

type Event struct {
	Event string `json:"event"`
}

Event is one step of a scenario: the event to fire, named so the artifact is portable across the typed event domain.

type EventCodec

type EventCodec[E comparable] struct {
	Named   EventNamer[E]
	Resolve EventResolver[E]
}

EventCodec carries both directions of the event-name mapping. A consumer supplies one per event type; for an int-backed enum with a String method the Named direction is fmt.Sprint and the Resolve direction is a small lookup map.

type EventNamer

type EventNamer[E comparable] func(E) string

EventNamer renders a typed event to the stable name used in scenarios and traces. It must agree with the kernel's own rendering (fmt.Sprint of the event), which is what GenerateScenarios reads back from the IR.

type EventResolver

type EventResolver[E comparable] func(name string) (E, bool)

EventResolver maps a scenario's event name back to its typed value so a serialized scenario can be replayed against a typed machine. It returns false for an unknown name.

type GenerateOption

type GenerateOption func(*generateConfig)

GenerateOption configures scenario generation.

func WithMaxDepth

func WithMaxDepth(d int) GenerateOption

WithMaxDepth caps the length of generated event paths. A non-positive value (the default) means no cap beyond the reachable graph.

type Mismatch

type Mismatch struct {
	// Scenario is the name of the scenario whose run diverged.
	Scenario string
	// Field names what diverged (e.g. "finalState", "effects", "trace.len").
	Field string
	// Reference and Subject are the diverging values from each side.
	Reference string
	Subject   string
}

Mismatch is one field-level divergence found by an oracle comparison.

func (Mismatch) String

func (m Mismatch) String() string

type Scenario

type Scenario struct {
	SchemaVersion int         `json:"schemaVersion"`
	MachineID     string      `json:"machineId"`
	Name          string      `json:"name,omitempty"`
	InitialState  string      `json:"initialState"`
	Events        []Event     `json:"events"`
	Assertions    []Assertion `json:"assertions,omitempty"`
}

Scenario describes what to do — fire this sequence of events against a machine from a starting state — plus the assertions to evaluate. It is a serializable artifact: generated scenarios can be committed as goldens and replayed.

func GenerateScenarios

func GenerateScenarios[S comparable, E comparable, C any](
	m *state.Machine[S, E, C],
	namer EventNamer[E],
	opts ...GenerateOption,
) ([]Scenario, error)

GenerateScenarios derives a scenario for the shortest event path to every reachable state, by breadth-first search over the machine's IR graph. This is the model-based layer: it mirrors path planning so a machine's own structure produces its coverage, with no hand-authored fixtures.

Each generated scenario asserts the final state it targets, the trace length (the number of events fired), and that no errors occurred. The namer renders each typed event to the stable name the kernel records, so generated scenarios are directly serializable and replayable.

Generation walks the IR — the same exported, serializable graph ToJSON emits — so it is fully generic: it works for any machine, flat or hierarchical, and never reaches into kernel internals.

func LoadScenario

func LoadScenario(data []byte) (Scenario, error)

LoadScenario parses a scenario from its JSON form, rejecting an unsupported schema version.

func (Scenario) MarshalJSON

func (s Scenario) MarshalJSON() ([]byte, error)

MarshalJSON emits the scenario with its schema version pinned.

type ScenarioResult

type ScenarioResult[S comparable] struct {
	FinalState S
	Trace      Trace
	Assertions []AssertionResult
	Effects    []string
	Err        error
}

ScenarioResult is the outcome of running a scenario against a machine: the resulting state, the captured trace, the per-assertion verdicts, and any kernel error encountered along the way.

func RunAgainst

func RunAgainst[S comparable, E comparable, C any](
	m *state.Machine[S, E, C],
	sc Scenario,
	entity C,
	codec EventCodec[E],
	startState S,
) ScenarioResult[S]

RunAgainst fires the scenario's event sequence against a freshly Cast instance of the machine and builds a ScenarioResult. The codec resolves each event name to its typed value; an unresolved name is a fatal scenario error. The entity is supplied by the caller (the kernel binds guards and actions to it) and the starting state is taken from the scenario.

func (ScenarioResult[S]) Passed

func (r ScenarioResult[S]) Passed() bool

Passed reports whether every assertion in the result passed.

type Trace

type Trace struct {
	SchemaVersion int         `json:"schemaVersion"`
	MachineID     string      `json:"machineId"`
	FromState     string      `json:"fromState"`
	ToState       string      `json:"toState"`
	Steps         []TraceStep `json:"steps"`
}

Trace is the serializable record of a whole scenario run: the ordered steps plus the spanning from/to state. It is the unifying primitive — it renders a past run and is diffable against a committed expectation.

func (Trace) MarshalJSON

func (t Trace) MarshalJSON() ([]byte, error)

MarshalJSON emits the trace with its schema version pinned.

type TraceStep

type TraceStep struct {
	Event           string   `json:"event"`
	FromState       string   `json:"fromState"`
	ToState         string   `json:"toState"`
	MatchedAt       string   `json:"matchedAt,omitempty"`
	GuardsEvaluated []string `json:"guardsEvaluated,omitempty"`
	EffectsEmitted  []string `json:"effectsEmitted,omitempty"`
	ExitedStates    []string `json:"exitedStates,omitempty"`
	EnteredStates   []string `json:"enteredStates,omitempty"`
	Outcome         string   `json:"outcome"`
	Err             string   `json:"err,omitempty"`
}

TraceStep is one Fire's worth of recorded behavior, in serializable form. It mirrors the kernel Trace but renders effects and outcome as their stable string names so a step is portable and diffable.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL