Runs a corpus case that carries a host object (ADR-0070 decision 5):
the host registers the case's send_types (ADR-0069), and the sends
handed to it must be the case's expect_sends.
Statifier.Testing.Case.test_scxml/4 starts its session with no
registration, so a host case runs here instead, through the same public
session API: Statifier.start_session/2 with :send_types naming
Mix.Statifier.Corpus.HostCase.Processor for every registered type,
Statifier.Session.send_event/2 for each step, with the step's event
data, when it gives one, as the injected event's payload, and
Statifier.Session.status/1 and Statifier.Session.snapshot/1 to wait
for the chart to settle and read its active leaf states. The waiting
follows test_scxml/4's: a pending library timer is given a short window
to fire before the next event, and a configuration is read once the
session has had nothing queued and no timer pending on two consecutive
polls, or the chart has left the running status, or a deadline passed.
A case agrees when the active leaf states after initialization and after
every step are the ones it expects, and the sends handed to the processor
over the whole run, in order, are exactly its expect_sends. Each handed
send is written in the case's language-neutral item shape: type,
target, event with its name and, when the send carries a payload,
its data; delay_ms for a delayed send; and send_id only when the
author named the send, which is when the delivered event carries one.
A registered delayed send is the host's timer, and this host never fires
one.
An expect_sends item may carry an outcome the runner performs or
observes for that send (ADR-0070's 2026-09-23 Amendment). With
"fail", the runner reports the send handed at that item's position
through Statifier.Session.failed_send/3 as soon as it reads the
processor's message, before it reads the next configuration, so the
step that led to the send is the one whose configuration shows what
the sender made of the error.communication it got. With
"cancelled", a <cancel> naming the send must reach the processor
after it was handed the send: when a cancel reaches the processor, the
runner writes "outcome": "cancelled" on each delayed send handed
before it under the cancel's send id, generated or not, whose item
asks for it, so the comparison above refuses a marked item no cancel
reached. An item with no outcome claims nothing about a cancel: a
cancel naming its send is not compared.
A host object may also carry declared_events and expect_accepts,
present together or not at all (ADR-0071 decision 7). Before it starts the
session, the runner calls Statifier.Chart.check_accepts/2 on the compiled
chart with declared_events, and the case agrees only when both lists it
answers are exactly expect_accepts' unreachable and undeclared, order
included. Either key without the other is a disagreement.
A host object may carry a diff pair as well - to_source and
expect_diff, with an optional mapping and expect_compatible_at
(ADR-0072, conformance/schema/case.json) - and this runner compares
none of them: it runs such a case like any other host case. Nothing in
lib/ calls the chart diff or the position predicate (ADR-0072
decision 6), so the test suite compares those four keys
(test/corpus/diff_cases_test.exs), through run/2's :after_steps
option for the position the steps leave.
A step may carry an expect_position (ADR-0076). Once that step's
configuration agrees, the runner reads the session's state with
Statifier.Session.snapshot/1, renders its export with
Mix.Statifier.Corpus.PositionExpectation.render/1, and the case agrees
only when the rendering is exactly the step's expect_position. A case
that carries one on any step needs every state of its document to carry
an id, and disagrees before it starts a session when one has none. The
runner hands such a case here even when it carries no host object, as
a host that registers nothing.
A host object may carry event_io_processors, the one host key a w3c
case may carry (ADR-0075 decision 7): the Event I/O Processor URIs the
host runs with a location that reaches the running session. For each one
the runner starts a loopback front (Mix.Statifier.BasicHTTPFront) and
registers the processor under its URI and its short form with the
front's base URL, beside the case's send_types, so every send to it is
delivered through the front and none is handed to the recording
processor (with_event_io_processors/2). The front is stopped when the
case ends. The item set is closed; its one member is the Basic HTTP
Event I/O Processor, Statifier.Send.BasicHTTP.
Every wait above is bounded by the wall clock, on purpose. A
configuration is read at most four seconds after the step that led to it
(@configuration_deadline_ms), a pending library timer gets a window of
100 milliseconds to fire before the next event (@settle_window_ms),
and each Statifier.Session.status/1 and Statifier.Session.snapshot/1
call the wait makes carries GenServer.call/2's default timeout of five
seconds. The reason is that every case must end with a verdict: a chart
that keeps running, or a session that does not answer, would otherwise
hold the corpus stage open, so the runner bounds the wait by the clock
rather than waiting on the session, and the bounds sit far above what a
committed case takes on an idle machine. The cost is that a session held
longer than a bound makes the case fail whatever its document does: a
session inside a Basic HTTP POST, which the default transport bounds at
five seconds of its own (Statifier.Send.BasicHTTP.Transport.Httpc), can
outlast the deadline and leave a stale configuration to compare, or
outlast a call's timeout and exit the runner. Such a failure under heavy
load is the machine's, not the case's; running the case again on an idle
machine tells the two apart.
Summary
Functions
The Event I/O Processor URIs of host.event_io_processors' closed item
set that source names as a <send>'s type, sorted: what a corpus
case transformed from source declares in host.event_io_processors.
Runs one host case, returning :agree or {:disagree, message}.
Runs fun with the :send_types entries that register every processor
uris names, delivering through a loopback front started for the call
and stopped when fun returns or raises; answers what fun answers.
Functions
The Event I/O Processor URIs of host.event_io_processors' closed item
set that source names as a <send>'s type, sorted: what a corpus
case transformed from source declares in host.event_io_processors.
The match is a text search, not a parse: a URI is found only where
source holds the full URI as type="URI" (double quotes, no space
around =) inside a <send start tag. The processor's short form
(type="basichttp"), a typeexpr, a single-quoted type and a
namespace-prefixed <scxml:send> are not matched, so a document naming
its processor in one of those ways gets no declaration; and because the
search reads text, a matching <send> inside an XML comment counts too.
The search also matches two shapes that are not a <send>'s type,
because it bounds the names send and type only by a character other
than an ASCII letter, a digit or an underscore (in a well-formed name, a
hyphen, a dot or a colon). An attribute whose name ends in type after
such a character counts as the type: <send data-type="URI"> declares
the URI. And an element whose name is send followed by such a
character counts as a <send>: <send-log type="URI"> declares it too.
The search also misses one shape that is a <send>'s type, because
it reads a start tag only up to its first >. XML allows an unescaped
> inside an attribute value, and such a > in an attribute written
before type ends the search there: <send event="fines>0" type="URI">
declares nothing.
Examples
iex> Mix.Statifier.Corpus.HostCase.event_io_processors(
...> ~s|<send type="http://www.w3.org/TR/scxml/#BasicHTTPEventProcessor" event="e"/>|
...> )
["http://www.w3.org/TR/scxml/#BasicHTTPEventProcessor"]
iex> Mix.Statifier.Corpus.HostCase.event_io_processors(~s|<send event="e"/>|)
[]
iex> Mix.Statifier.Corpus.HostCase.event_io_processors(
...> ~s|<send event="fines>0" type="http://www.w3.org/TR/scxml/#BasicHTTPEventProcessor"/>|
...> )
[]
@spec run( corpus_case :: map(), opts :: [ {:after_steps, (Statifier.MachineState.t() -> :ok | {:disagree, String.t()})} ] ) :: :agree | {:disagree, String.t()}
Runs one host case, returning :agree or {:disagree, message}.
opts takes :after_steps, a function the runner calls once, after the
last step's configuration agrees and before it compares the handed sends,
with the session's settled Statifier.MachineState; it answers :ok or
{:disagree, message}, and a disagreement is the case's. A case that
disagrees before the sends are compared leaves them in the calling
process's mailbox, so each run belongs in a process of its own, as
Mix.Statifier.Corpus.Runner.run/1 gives it.
@spec with_event_io_processors( uris :: [String.t()], fun :: (%{required(String.t()) => {module(), keyword()}} -> result) ) :: result when result: term()
Runs fun with the :send_types entries that register every processor
uris names, delivering through a loopback front started for the call
and stopped when fun returns or raises; answers what fun answers.
Each processor is registered under its URI and its short form, with the
front's base URL as its :base_url (ADR-0075 decisions 2 and 3). With no
URI, no front is started and fun is handed an empty map. A URI outside
the closed set raises.