From 185b7d491d049e2ed9fb48fa038d4fbc72b87e37 Mon Sep 17 00:00:00 2001 From: JohnnyT Date: Wed, 30 Sep 2026 07:43:24 -0600 Subject: [PATCH 1/2] Claims the Basic HTTP documents in the corpus The twelve W3C documents that need the Basic HTTP Event I/O Processor leave the exclusions. Eleven join the ratchet and the registry; test201 enters the corpus unclaimed, for the reason ADR-0075 decision 7 gives. - The case schema gains host.event_io_processors, a closed set whose one member is the Basic HTTP URI, and the one host key a w3c case may carry. - The emitter declares it for a W3C document naming the processor as a send type; the corpus host and the generated modules register the processor behind a loopback front (HostCase). - conf:basicHTTPAccessURITarget emits the IRP's access URI spelling. - A guide, docs/basichttp.md, with a req transport recipe as text; the stale exclusion sentences and the emitted counts are updated. Refs: st-8kn8 --- README.md | 4 +- changelog.d/st-8kn8.md | 3 + conformance/RATCHET.md | 27 ++- conformance/corpus/w3c.json | 12 + conformance/exclusions.json | 72 ------ conformance/manifest.json | 4 +- conformance/registry.json | 13 +- conformance/schema/case.json | 19 +- docs/basichttp.md | 210 ++++++++++++++++++ docs/extending.md | 18 +- docs/testing.md | 10 +- lib/mix/statifier/corpus/host_case.ex | 108 +++++++-- lib/mix/statifier/corpus/json.ex | 7 +- lib/mix/statifier/corpus/runner.ex | 8 + lib/mix/statifier/corpus/upstream.ex | 19 +- mix.exs | 2 + test/corpus/schema_test.exs | 66 +++++- test/mix/statifier/corpus/host_case_test.exs | 104 +++++++++ test/mix/statifier/corpus/upstream_test.exs | 79 +++++++ test/passing_tests.json | 13 +- .../test509_test.exs | 63 ++++++ .../test510_test.exs | 70 ++++++ .../test518_test.exs | 68 ++++++ .../test519_test.exs | 66 ++++++ .../test520_test.exs | 67 ++++++ .../test522_test.exs | 64 ++++++ .../test531_test.exs | 66 ++++++ .../test532_test.exs | 66 ++++++ .../test534_test.exs | 63 ++++++ .../test567_test.exs | 81 +++++++ .../test577_test.exs | 61 +++++ .../optional/send/test201_test.exs | 62 ++++++ tools/corpus/README.md | 23 +- tools/corpus/scxml_w3/cases.exs | 25 ++- tools/corpus/scxml_w3/conf_predicator.xsl | 12 +- tools/corpus/scxml_w3/exclusions.exs | 23 +- 36 files changed, 1526 insertions(+), 152 deletions(-) create mode 100644 changelog.d/st-8kn8.md create mode 100644 docs/basichttp.md create mode 100644 test/mix/statifier/corpus/upstream_test.exs create mode 100644 test/scxml_tests/optional/basic_http_event_processor/test509_test.exs create mode 100644 test/scxml_tests/optional/basic_http_event_processor/test510_test.exs create mode 100644 test/scxml_tests/optional/basic_http_event_processor/test518_test.exs create mode 100644 test/scxml_tests/optional/basic_http_event_processor/test519_test.exs create mode 100644 test/scxml_tests/optional/basic_http_event_processor/test520_test.exs create mode 100644 test/scxml_tests/optional/basic_http_event_processor/test522_test.exs create mode 100644 test/scxml_tests/optional/basic_http_event_processor/test531_test.exs create mode 100644 test/scxml_tests/optional/basic_http_event_processor/test532_test.exs create mode 100644 test/scxml_tests/optional/basic_http_event_processor/test534_test.exs create mode 100644 test/scxml_tests/optional/basic_http_event_processor/test567_test.exs create mode 100644 test/scxml_tests/optional/basic_http_event_processor/test577_test.exs create mode 100644 test/scxml_tests/optional/send/test201_test.exs diff --git a/README.md b/README.md index c557d494..d36628b4 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,8 @@ conformance failures. v2 is: sessions and timers layered on top - **predicator as the datamodel** - safe, non-evaluative expressions; no ECMAScript, no eval -- built **corpus-first** - 275 generated SCION/W3C conformance tests (119 - SCION + 156 W3C) behind a forward-only regression ratchet inherited from +- built **corpus-first** - 287 generated SCION/W3C conformance tests (119 + SCION + 168 W3C) behind a forward-only regression ratchet inherited from v1, with the generator committed this time ## Installation diff --git a/changelog.d/st-8kn8.md b/changelog.d/st-8kn8.md new file mode 100644 index 00000000..90d55ee6 --- /dev/null +++ b/changelog.d/st-8kn8.md @@ -0,0 +1,3 @@ +### Added + +- The conformance corpus claims eleven more W3C documents, the Basic HTTP Event I/O Processor's: test509, test510, test518, test519, test520, test522, test531, test532, test534, test567 and test577. test201 is in the corpus and unclaimed. The case schema gains `host.event_io_processors`, the Event I/O Processors a host runs with a location that reaches the session, and the one `host` key a W3C case may carry. A new guide, `docs/basichttp.md`. diff --git a/conformance/RATCHET.md b/conformance/RATCHET.md index 713e596f..55ef5018 100644 --- a/conformance/RATCHET.md +++ b/conformance/RATCHET.md @@ -169,9 +169,30 @@ A case may carry an optional `host` object, reserved by ADR-0070 decision 5 for what a `statifier` case needs from its host: `send_types`, the send types the case registers in [ADR-0069](../docs/adr/0069-host-registered-send-types.md)'s sense, and -`expect_sends`, the sends the case expects handed to the host. Every W3C and -SCION case omits it, because upstream cases run with no registration, and -`schema/case.json` refuses a `scion` or `w3c` case that carries it. +`expect_sends`, the sends the case expects handed to the host. Every SCION +case omits it, because upstream cases run with no registration, and +`schema/case.json` refuses a `scion` case that carries it. A W3C case +carries it only for `event_io_processors`, its one key there, below. + +A `host` object may carry `event_io_processors`, the Event I/O Processors +the host runs, by URI +([ADR-0075](../docs/adr/0075-basichttp-event-io-processor.md) decision 7). +It is the one `host` key a W3C case may carry, because the documents that +need a processor cannot run without one; `schema/case.json` refuses every +other `host` key on a `w3c` case. The item set is closed, and its one member +is the Basic HTTP Event I/O Processor's URI, +`http://www.w3.org/TR/scxml/#BasicHTTPEventProcessor`. A runner honours it +by registering the processor under that URI and its short form `basichttp`, +with a location in `_ioprocessors` that reaches the running session, and by +delivering every send to it, so none is handed to the recording processor +above. statifier-ex's runner delivers through a loopback HTTP front it +starts for the case. An implementation that does not run a processor the +case names leaves the case unclaimed; that is what an optional processor +means in a registry. A case can be in the corpus and outside every claim: +`w3c/test201` needs the Basic HTTP processor and is unclaimed, because it +expects a send delivered from outside the session to arrive ahead of a send +the same step appends to the session's own external queue, which no +delivery over HTTP does (ADR-0075 decision 7). A runner honours the object by registering each of `send_types` with an Event I/O Processor of its own, which records every send it is handed and diff --git a/conformance/corpus/w3c.json b/conformance/corpus/w3c.json index ee53a507..da5d3243 100644 --- a/conformance/corpus/w3c.json +++ b/conformance/corpus/w3c.json @@ -32,6 +32,7 @@ {"id":"w3c/test198","suite":"w3c","spec":"send","conformance":"mandatory","description":"If neither the 'type' nor the 'typeexpr' is defined, the SCXML Processor MUST assume the default value of http://www.w3.org/TR/scxml/#SCXMLEventProcessor.","required_features":["basic_states","conditional_transitions","event_transitions","final_states","log_elements","onentry_actions","send_elements","wildcard_events"],"source":"\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n","initial_configuration":["pass"],"steps":[],"upstream":{"document":"198/test198.txml","license":"BSD-3-Clause-W3C","notice":"LICENSES/BSD-3-Clause-W3C.txt"}}, {"id":"w3c/test199","suite":"w3c","spec":"send","conformance":"mandatory","description":"f the SCXML Processor does not support the type that is specified, it MUST place the event error.execution on the internal event queue.","required_features":["basic_states","event_transitions","final_states","log_elements","onentry_actions","send_elements","wildcard_events"],"source":"\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n","initial_configuration":["pass"],"steps":[],"upstream":{"document":"199/test199.txml","license":"BSD-3-Clause-W3C","notice":"LICENSES/BSD-3-Clause-W3C.txt"}}, {"id":"w3c/test200","suite":"w3c","spec":"send","conformance":"mandatory","description":"SCXML Processors MUST support the type http://www.w3.org/TR/scxml/#SCXMLEventProcessor","required_features":["basic_states","event_transitions","final_states","log_elements","onentry_actions","send_elements","wildcard_events"],"source":"\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n","initial_configuration":["pass"],"steps":[],"upstream":{"document":"200/test200.txml","license":"BSD-3-Clause-W3C","notice":"LICENSES/BSD-3-Clause-W3C.txt"}}, + {"id":"w3c/test201","suite":"w3c","spec":"send","conformance":"optional","description":"Processors that support HTTP POST must use the value http://www.w3.org/TR/scxml/#BasicHTTPEventProcessor for the \"type\" attribute","required_features":["basic_states","event_transitions","final_states","log_elements","onentry_actions","send_elements","target_expressions","wildcard_events"],"source":"\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n","initial_configuration":["pass"],"steps":[],"upstream":{"document":"201/test201.txml","license":"BSD-3-Clause-W3C","notice":"LICENSES/BSD-3-Clause-W3C.txt"},"host":{"event_io_processors":["http://www.w3.org/TR/scxml/#BasicHTTPEventProcessor"]}}, {"id":"w3c/test205","suite":"w3c","spec":"send","conformance":"mandatory","description":"The sending SCXML Interpreter MUST not alter the content of the send and include it in the message that it sends to the destination specified in the target attribute of send.","required_features":["assign_elements","basic_states","compound_states","conditional_transitions","data_elements","datamodel","event_transitions","eventless_transitions","final_states","log_elements","onentry_actions","send_elements","send_param_elements","wildcard_events"],"source":"\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n","initial_configuration":["pass"],"steps":[],"upstream":{"document":"205/test205.txml","license":"BSD-3-Clause-W3C","notice":"LICENSES/BSD-3-Clause-W3C.txt"}}, {"id":"w3c/test207","suite":"w3c","spec":"cancel","conformance":"mandatory","description":"The SCXML Processor MUST NOT allow cancel to affect events that were not raised in the same session.","required_features":["basic_states","cancel_elements","compound_states","event_transitions","final_states","invoke_elements","log_elements","onentry_actions","send_content_elements","send_delay_expressions","send_elements","wildcard_events"],"source":"\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n","initial_configuration":["pass"],"steps":[],"upstream":{"document":"207/test207.txml","license":"BSD-3-Clause-W3C","notice":"LICENSES/BSD-3-Clause-W3C.txt"}}, {"id":"w3c/test208","suite":"w3c","spec":"cancel","conformance":"mandatory","description":"The Processor SHOULD make its best attempt to cancel all delayed events with the specified id.","required_features":["basic_states","cancel_elements","event_transitions","final_states","log_elements","onentry_actions","send_delay_expressions","send_elements","wildcard_events"],"source":"\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n","initial_configuration":["pass"],"steps":[],"upstream":{"document":"208/test208.txml","license":"BSD-3-Clause-W3C","notice":"LICENSES/BSD-3-Clause-W3C.txt"}}, @@ -141,20 +142,31 @@ {"id":"w3c/test504","suite":"w3c","spec":"SelectingTransitions","conformance":"mandatory","description":"if [a transition's] 'type' is \"external\", its exit set consists of all active states that are proper descendents of the Least Common Compound Ancestor (LCCA) of the source and target states.","required_features":["assign_elements","basic_states","compound_states","conditional_transitions","data_elements","datamodel","event_transitions","eventless_transitions","final_states","log_elements","onentry_actions","onexit_actions","parallel_states","raise_elements"],"source":"\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n","initial_configuration":["pass"],"steps":[],"upstream":{"document":"504/test504.txml","license":"BSD-3-Clause-W3C","notice":"LICENSES/BSD-3-Clause-W3C.txt"}}, {"id":"w3c/test505","suite":"w3c","spec":"SelectingTransitions","conformance":"mandatory","description":"Otherwise, if the transition has 'type' \"internal\", its source state is a compound state and all its target states are proper descendents of its source state, the target set consists of all active states that are proper descendents of its source state.","required_features":["assign_elements","basic_states","compound_states","conditional_transitions","data_elements","datamodel","event_transitions","eventless_transitions","final_states","internal_transitions","log_elements","onentry_actions","onexit_actions","raise_elements"],"source":"\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n","initial_configuration":["pass"],"steps":[],"upstream":{"document":"505/test505.txml","license":"BSD-3-Clause-W3C","notice":"LICENSES/BSD-3-Clause-W3C.txt"}}, {"id":"w3c/test506","suite":"w3c","spec":"SelectingTransitions","conformance":"mandatory","description":"If a transition has 'type' of \"internal\", but its source state is not a compound state or its target states are not all proper descendents of its source state, its exit set is defined as if it had 'type' of \"external\".","required_features":["assign_elements","basic_states","compound_states","conditional_transitions","data_elements","datamodel","event_transitions","eventless_transitions","final_states","internal_transitions","log_elements","onentry_actions","onexit_actions","raise_elements"],"source":"\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n","initial_configuration":["pass"],"steps":[],"upstream":{"document":"506/test506.txml","license":"BSD-3-Clause-W3C","notice":"LICENSES/BSD-3-Clause-W3C.txt"}}, + {"id":"w3c/test509","suite":"w3c","spec":"BasicHTTPEventProcessor","conformance":"optional","description":"An SCXML Processor that supports the Basic HTTP Event I/O Processor MUST accept messages at the access URI as HTTP POST requests","required_features":["basic_states","event_transitions","final_states","log_elements","onentry_actions","send_delay_expressions","send_elements","target_expressions","wildcard_events"],"source":"\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n","initial_configuration":["pass"],"steps":[],"upstream":{"document":"509/test509.txml","license":"BSD-3-Clause-W3C","notice":"LICENSES/BSD-3-Clause-W3C.txt"},"host":{"event_io_processors":["http://www.w3.org/TR/scxml/#BasicHTTPEventProcessor"]}}, + {"id":"w3c/test510","suite":"w3c","spec":"BasicHTTPEventProcessor","conformance":"optional","description":"The SCXML Processor MUST validate the message it receives [via the Basic HTTP Event I/O Processor] and then MUST build the appropriate SCXML event and MUST add it to the external event queue","required_features":["basic_states","compound_states","event_transitions","final_states","log_elements","onentry_actions","raise_elements","send_delay_expressions","send_elements","target_expressions","wildcard_events"],"source":"\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n","initial_configuration":["pass"],"steps":[],"upstream":{"document":"510/test510.txml","license":"BSD-3-Clause-W3C","notice":"LICENSES/BSD-3-Clause-W3C.txt"},"host":{"event_io_processors":["http://www.w3.org/TR/scxml/#BasicHTTPEventProcessor"]}}, + {"id":"w3c/test518","suite":"w3c","spec":"BasicHTTPEventProcessor","conformance":"optional","description":"If the namelist attribute is defined [in send], the SCXML Processor MUST map its variable names and values to HTTP POST parameters","required_features":["basic_states","data_elements","datamodel","event_transitions","final_states","log_elements","onentry_actions","send_delay_expressions","send_elements","target_expressions","wildcard_events"],"source":"\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n","initial_configuration":["pass"],"steps":[],"upstream":{"document":"518/test518.txml","license":"BSD-3-Clause-W3C","notice":"LICENSES/BSD-3-Clause-W3C.txt"},"host":{"event_io_processors":["http://www.w3.org/TR/scxml/#BasicHTTPEventProcessor"]}}, + {"id":"w3c/test519","suite":"w3c","spec":"BasicHTTPEventProcessor","conformance":"optional","description":"If one or more param children are present [in send], the SCXML Processor MUST map their names (i.e. name attributes) and values to HTTP POST parameters","required_features":["basic_states","event_transitions","final_states","log_elements","onentry_actions","send_delay_expressions","send_elements","send_param_elements","target_expressions","wildcard_events"],"source":"\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n","initial_configuration":["pass"],"steps":[],"upstream":{"document":"519/test519.txml","license":"BSD-3-Clause-W3C","notice":"LICENSES/BSD-3-Clause-W3C.txt"},"host":{"event_io_processors":["http://www.w3.org/TR/scxml/#BasicHTTPEventProcessor"]}}, + {"id":"w3c/test520","suite":"w3c","spec":"BasicHTTPEventProcessor","conformance":"optional","description":"If a content child is present, the SCXML Processor MUST use its value as the body of the message.","required_features":["basic_states","event_transitions","final_states","log_elements","onentry_actions","send_content_elements","send_delay_expressions","send_elements","target_expressions","wildcard_events"],"source":"\n\n \n \n \n \n this is some content\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n","initial_configuration":["pass"],"steps":[],"upstream":{"document":"520/test520.txml","license":"BSD-3-Clause-W3C","notice":"LICENSES/BSD-3-Clause-W3C.txt"},"host":{"event_io_processors":["http://www.w3.org/TR/scxml/#BasicHTTPEventProcessor"]}}, {"id":"w3c/test521","suite":"w3c","spec":"send","conformance":"mandatory","description":"f the Processor cannot dispatch the event, it MUST place the error error.communication on the internal event queue of the session that attempted to send the event.","required_features":["basic_states","event_transitions","final_states","log_elements","onentry_actions","send_elements","wildcard_events"],"source":"\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n","initial_configuration":["pass"],"steps":[],"upstream":{"document":"521/test521.txml","license":"BSD-3-Clause-W3C","notice":"LICENSES/BSD-3-Clause-W3C.txt"}}, + {"id":"w3c/test522","suite":"w3c","spec":"BasicHTTPEventProcessor","conformance":"optional","description":"SCXML Processors that support the BasicHTTP Event I/O Processor MUST maintain a 'basichttp' entry in the _ioprocessors system variable. The Processor MUST maintain a in 'location' field inside this entry whose value holds an address that external entities can use to communicate with this SCXML session using the Basic HTTP Event I/O Processor.","required_features":["basic_states","event_transitions","final_states","log_elements","onentry_actions","send_delay_expressions","send_elements","target_expressions","wildcard_events"],"source":"\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n","initial_configuration":["pass"],"steps":[],"upstream":{"document":"522/test522.txml","license":"BSD-3-Clause-W3C","notice":"LICENSES/BSD-3-Clause-W3C.txt"},"host":{"event_io_processors":["http://www.w3.org/TR/scxml/#BasicHTTPEventProcessor"]}}, {"id":"w3c/test525","suite":"w3c","spec":"foreach","conformance":"mandatory","description":"The SCXML processor MUST act as if it has made a shallow copy of the collection produced by the evaluation of 'array'. Specifically, modifications to the collection during the execution of foreach MUST NOT affect the iteration behavior.","required_features":["assign_elements","basic_states","conditional_transitions","data_elements","datamodel","event_transitions","eventless_transitions","final_states","foreach_elements","log_elements","onentry_actions"],"source":"\n\n \n [1,2,3]\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n","initial_configuration":["pass"],"steps":[],"upstream":{"document":"525/test525.txml","license":"BSD-3-Clause-W3C","notice":"LICENSES/BSD-3-Clause-W3C.txt"}}, {"id":"w3c/test527","suite":"w3c","spec":"content","conformance":"mandatory","description":"When the SCXML Processor evaluates the content element, if the 'expr' value expression is present, the Processor MUST evaluate it and use the result as the output of the content element.","required_features":["basic_states","compound_states","conditional_transitions","donedata_elements","event_transitions","eventless_transitions","final_states","log_elements","onentry_actions"],"source":"\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n","initial_configuration":["pass"],"steps":[],"upstream":{"document":"527/test527.txml","license":"BSD-3-Clause-W3C","notice":"LICENSES/BSD-3-Clause-W3C.txt"}}, {"id":"w3c/test528","suite":"w3c","spec":"content","conformance":"mandatory","description":"f the evaluation of 'expr' produces an error, the Processor MUST place error.execution in the internal event queue and use the empty string as the output of the content element.","required_features":["basic_states","compound_states","conditional_transitions","donedata_elements","event_transitions","eventless_transitions","final_states","log_elements","onentry_actions","wildcard_events"],"source":"\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n","initial_configuration":["pass"],"steps":[],"upstream":{"document":"528/test528.txml","license":"BSD-3-Clause-W3C","notice":"LICENSES/BSD-3-Clause-W3C.txt"}}, {"id":"w3c/test529","suite":"w3c","spec":"content","conformance":"mandatory","description":"If the 'expr' attribute is not present, the Processor MUST use the children of content as the output.","required_features":["basic_states","compound_states","conditional_transitions","donedata_elements","event_transitions","eventless_transitions","final_states","log_elements","onentry_actions"],"source":"\n\n \n \n \n \n \n \n \n \n 21\n \n \n \n \n \n \n \n \n \n \n \n \n \n\n","initial_configuration":["pass"],"steps":[],"upstream":{"document":"529/test529.txml","license":"BSD-3-Clause-W3C","notice":"LICENSES/BSD-3-Clause-W3C.txt"}}, {"id":"w3c/test530","suite":"w3c","spec":"invoke","conformance":"mandatory","description":"The SCXML Processor MUST evaluate a child content element when the parent invoke element is evaluated and pass the resulting data to the invoked service.","required_features":["assign_elements","basic_states","data_elements","datamodel","event_transitions","final_states","invoke_elements","log_elements","onentry_actions","send_content_elements","send_delay_expressions","send_elements","wildcard_events"],"source":"\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n","initial_configuration":["pass"],"steps":[],"upstream":{"document":"530/test530.txml","license":"BSD-3-Clause-W3C","notice":"LICENSES/BSD-3-Clause-W3C.txt"}}, + {"id":"w3c/test531","suite":"w3c","spec":"BasicHTTPEventProcessor","conformance":"optional","description":"If a single instance of the parameter '_scxmleventname' is present, the SCXML Processor MUST use its value as the name of the SCXML event that it raises.","required_features":["basic_states","event_transitions","final_states","log_elements","onentry_actions","send_delay_expressions","send_elements","send_param_elements","target_expressions","wildcard_events"],"source":"\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n","initial_configuration":["pass"],"steps":[],"upstream":{"document":"531/test531.txml","license":"BSD-3-Clause-W3C","notice":"LICENSES/BSD-3-Clause-W3C.txt"},"host":{"event_io_processors":["http://www.w3.org/TR/scxml/#BasicHTTPEventProcessor"]}}, + {"id":"w3c/test532","suite":"w3c","spec":"BasicHTTPEventProcessor","conformance":"optional","description":"If _scxmleventname is not present, the Processor MUST the name of the HTTP method that was used to deliver the event as name of the SCXML event that it raises","required_features":["basic_states","event_transitions","final_states","log_elements","onentry_actions","send_content_elements","send_delay_expressions","send_elements","target_expressions","wildcard_events"],"source":"\n\n \n \n \n \n some content\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n","initial_configuration":["pass"],"steps":[],"upstream":{"document":"532/test532.txml","license":"BSD-3-Clause-W3C","notice":"LICENSES/BSD-3-Clause-W3C.txt"},"host":{"event_io_processors":["http://www.w3.org/TR/scxml/#BasicHTTPEventProcessor"]}}, {"id":"w3c/test533","suite":"w3c","spec":"SelectingTransitions","conformance":"mandatory","description":"If a transition has 'type' of \"internal\", but its source state is not a compound state, its exit set is defined as if it had 'type' of \"external\".","required_features":["assign_elements","basic_states","compound_states","conditional_transitions","data_elements","datamodel","event_transitions","eventless_transitions","final_states","internal_transitions","log_elements","onentry_actions","onexit_actions","parallel_states","raise_elements"],"source":"\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n","initial_configuration":["pass"],"steps":[],"upstream":{"document":"533/test533.txml","license":"BSD-3-Clause-W3C","notice":"LICENSES/BSD-3-Clause-W3C.txt"}}, + {"id":"w3c/test534","suite":"w3c","spec":"BasicHTTPEventProcessor","conformance":"optional","description":"If the 'event' parameter of send is defined, the SCXML Processor MUST use its value as the value of the HTTP POST parameter _scxmleventname","required_features":["basic_states","event_transitions","final_states","log_elements","onentry_actions","send_delay_expressions","send_elements","target_expressions","wildcard_events"],"source":"\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n","initial_configuration":["pass"],"steps":[],"upstream":{"document":"534/test534.txml","license":"BSD-3-Clause-W3C","notice":"LICENSES/BSD-3-Clause-W3C.txt"},"host":{"event_io_processors":["http://www.w3.org/TR/scxml/#BasicHTTPEventProcessor"]}}, {"id":"w3c/test550","suite":"w3c","spec":"data","conformance":"mandatory","description":"If the 'expr' attribute is present, the Platform MUST evaluate the corresponding expression at the time specified by the 'binding' attribute of scxml and MUST assign the resulting value as the value of the data element","required_features":["basic_states","compound_states","conditional_transitions","data_elements","datamodel","event_transitions","eventless_transitions","final_states","log_elements","onentry_actions"],"source":"\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n","initial_configuration":["pass"],"steps":[],"upstream":{"document":"550/test550.txml","license":"BSD-3-Clause-W3C","notice":"LICENSES/BSD-3-Clause-W3C.txt"}}, {"id":"w3c/test551","suite":"w3c","spec":"data","conformance":"mandatory","description":"f child content is specified, the Platform MUST assign it as the value of the data element at the time specified by the 'binding' attribute of scxml.","required_features":["basic_states","compound_states","conditional_transitions","data_elements","datamodel","event_transitions","eventless_transitions","final_states","log_elements","onentry_actions"],"source":"\n\n \n \n \n \n \n \n [1,2,3]\n \n \n \n \n \n \n \n \n \n \n \n \n\n","initial_configuration":["pass"],"steps":[],"upstream":{"document":"551/test551.txml","license":"BSD-3-Clause-W3C","notice":"LICENSES/BSD-3-Clause-W3C.txt"}}, {"id":"w3c/test552","suite":"w3c","spec":"data","conformance":"mandatory","description":"If the 'src' attribute is present, the Platform MUST fetch the specified object at the time specified by the 'binding' attribute of scxml and MUST assign it as the value of the data element.","required_features":["basic_states","conditional_transitions","data_elements","datamodel","event_transitions","eventless_transitions","final_states","log_elements","onentry_actions"],"source":"\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n","initial_configuration":["pass"],"steps":[],"upstream":{"document":"552/test552.txml","license":"BSD-3-Clause-W3C","notice":"LICENSES/BSD-3-Clause-W3C.txt"}}, {"id":"w3c/test553","suite":"w3c","spec":"send","conformance":"mandatory","description":"If the evaluation of send's arguments produces an error, If the evaluation of send's arguments produces an error, the Processor MUST discard the message without attempting to deliver it.","required_features":["basic_states","event_transitions","final_states","log_elements","onentry_actions","send_delay_expressions","send_elements"],"source":"\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n","initial_configuration":["pass"],"steps":[],"upstream":{"document":"553/test553.txml","license":"BSD-3-Clause-W3C","notice":"LICENSES/BSD-3-Clause-W3C.txt"}}, {"id":"w3c/test554","suite":"w3c","spec":"invoke","conformance":"mandatory","description":"if the evaluation of the invoke element's arguments arguments produces an error, the SCXML Processor MUST terminate the processing of the element without further action.","required_features":["basic_states","event_transitions","final_states","invoke_elements","log_elements","onentry_actions","send_content_elements","send_delay_expressions","send_elements"],"source":"\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n","initial_configuration":["pass"],"steps":[],"upstream":{"document":"554/test554.txml","license":"BSD-3-Clause-W3C","notice":"LICENSES/BSD-3-Clause-W3C.txt"}}, + {"id":"w3c/test567","suite":"w3c","spec":"BasicHTTPEventProcessor","conformance":"optional","description":"The processor MUST use any message content other than '_scxmleventname' to populate _event.data.","required_features":["assign_elements","basic_states","compound_states","conditional_transitions","data_elements","datamodel","event_transitions","eventless_transitions","final_states","log_elements","onentry_actions","send_delay_expressions","send_elements","send_param_elements","target_expressions","wildcard_events"],"source":"\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n","initial_configuration":["pass"],"steps":[],"upstream":{"document":"567/test567.txml","license":"BSD-3-Clause-W3C","notice":"LICENSES/BSD-3-Clause-W3C.txt"},"host":{"event_io_processors":["http://www.w3.org/TR/scxml/#BasicHTTPEventProcessor"]}}, {"id":"w3c/test570","suite":"w3c","spec":"final","conformance":"mandatory","description":"Immediately after generating done.state.id upon entering a final child of state, if the parent state is a child of a parallel element, and all of the parallel's other children are also in final states, the Processor MUST generate the event done.state.id where id is the id of the parallel element.","required_features":["assign_elements","basic_states","compound_states","conditional_transitions","data_elements","datamodel","event_transitions","final_states","log_elements","onentry_actions","parallel_states","raise_elements","send_delay_expressions","send_elements","targetless_transitions","wildcard_events"],"source":"\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n","initial_configuration":["pass"],"steps":[],"upstream":{"document":"570/test570.txml","license":"BSD-3-Clause-W3C","notice":"LICENSES/BSD-3-Clause-W3C.txt"}}, {"id":"w3c/test576","suite":"w3c","spec":"scxml","conformance":"mandatory","description":"At system initialization time, the SCXML Processor MUST enter the states specified by the 'initial' attribute, if it is present.","required_features":["basic_states","compound_states","event_transitions","eventless_transitions","final_states","log_elements","onentry_actions","parallel_states","raise_elements","send_delay_expressions","send_elements"],"source":"\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n","initial_configuration":["pass"],"steps":[],"upstream":{"document":"576/test576.txml","license":"BSD-3-Clause-W3C","notice":"LICENSES/BSD-3-Clause-W3C.txt"}}, + {"id":"w3c/test577","suite":"w3c","spec":"BasicHTTPEventProcessor","conformance":"optional","description":"If neither the 'target' nor the 'targetexpr' attribute is specified, the SCXML Processor MUST add the event error.communication to the internal event queue of the sending session.","required_features":["basic_states","event_transitions","final_states","log_elements","onentry_actions","send_elements","wildcard_events"],"source":"\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n","initial_configuration":["pass"],"steps":[],"upstream":{"document":"577/test577.txml","license":"BSD-3-Clause-W3C","notice":"LICENSES/BSD-3-Clause-W3C.txt"},"host":{"event_io_processors":["http://www.w3.org/TR/scxml/#BasicHTTPEventProcessor"]}}, {"id":"w3c/test579","suite":"w3c","spec":"history","conformance":"mandatory","description":"Before the parent state has been visited for the first time, if a transition is executed that takes the history state as its target, the SCXML processor MUST execute any executable content in the transition after the parent state's onentry content and any content in a possible initial transition.","required_features":["assign_elements","basic_states","compound_states","conditional_transitions","data_elements","datamodel","event_transitions","eventless_transitions","final_states","history_states","initial_elements","log_elements","onentry_actions","onexit_actions","raise_elements","send_delay_expressions","send_elements","wildcard_events"],"source":"\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n","initial_configuration":["pass"],"steps":[],"upstream":{"document":"579/test579.txml","license":"BSD-3-Clause-W3C","notice":"LICENSES/BSD-3-Clause-W3C.txt"}}, {"id":"w3c/test580","suite":"w3c","spec":"history","conformance":"mandatory","description":"It follows from the semantics of history states that they never end up in the state configuration","required_features":["assign_elements","basic_states","compound_states","conditional_transitions","data_elements","datamodel","event_transitions","eventless_transitions","final_states","history_states","initial_elements","log_elements","onentry_actions","onexit_actions","parallel_states","send_delay_expressions","send_elements"],"source":"\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n","initial_configuration":["pass"],"steps":[],"upstream":{"document":"580/test580.txml","license":"BSD-3-Clause-W3C","notice":"LICENSES/BSD-3-Clause-W3C.txt"}} ] diff --git a/conformance/exclusions.json b/conformance/exclusions.json index 01f3ffa0..7363bdc0 100644 --- a/conformance/exclusions.json +++ b/conformance/exclusions.json @@ -39,12 +39,6 @@ "reason": "duplicates_w3c_corpus", "detail": "SCION framework's embedded copy of the W3C IRP suite; duplicates tools/corpus/scxml_w3's output with no predicator transform applied" }, - { - "suite": "w3c", - "key": "test201", - "reason": "needs_basichttp", - "detail": "BasicHTTP Event I/O Processor as a : the event is delivered only by a processor that implements HTTP POST" - }, { "suite": "w3c", "key": "test216", @@ -79,72 +73,6 @@ "reason": "needs_invoke_src", "detail": "top-level values supplied at instantiation: the child is loaded from src, which is never dereferenced (ADR-0038)", "adr": 38 - }, - { - "suite": "w3c", - "key": "test509", - "reason": "needs_basichttp", - "detail": "BasicHTTP Event I/O Processor MUST accept POST requests" - }, - { - "suite": "w3c", - "key": "test510", - "reason": "needs_basichttp", - "detail": "BasicHTTP Event I/O Processor MUST validate and enqueue the message" - }, - { - "suite": "w3c", - "key": "test518", - "reason": "needs_basichttp", - "detail": "BasicHTTP Event I/O Processor namelist -> POST parameter mapping" - }, - { - "suite": "w3c", - "key": "test519", - "reason": "needs_basichttp", - "detail": "BasicHTTP Event I/O Processor param children -> POST parameter mapping" - }, - { - "suite": "w3c", - "key": "test520", - "reason": "needs_basichttp", - "detail": "BasicHTTP Event I/O Processor content child -> message body" - }, - { - "suite": "w3c", - "key": "test522", - "reason": "needs_basichttp", - "detail": "BasicHTTP Event I/O Processor _ioprocessors['basichttp'] entry" - }, - { - "suite": "w3c", - "key": "test531", - "reason": "needs_basichttp", - "detail": "BasicHTTP Event I/O Processor _scxmleventname -> event name" - }, - { - "suite": "w3c", - "key": "test532", - "reason": "needs_basichttp", - "detail": "BasicHTTP Event I/O Processor HTTP method -> event name fallback" - }, - { - "suite": "w3c", - "key": "test534", - "reason": "needs_basichttp", - "detail": "BasicHTTP Event I/O Processor send/@event -> _scxmleventname" - }, - { - "suite": "w3c", - "key": "test567", - "reason": "needs_basichttp", - "detail": "BasicHTTP Event I/O Processor message content -> _event.data" - }, - { - "suite": "w3c", - "key": "test577", - "reason": "needs_basichttp", - "detail": "BasicHTTP Event I/O Processor missing target -> error.communication" } ] } diff --git a/conformance/manifest.json b/conformance/manifest.json index 1be8ce19..56df23d1 100644 --- a/conformance/manifest.json +++ b/conformance/manifest.json @@ -1,5 +1,5 @@ { - "corpus_hash": "sha256:c15e87820da654e8dde6ae97bed1631051cf6456c461cf67e7b617a36b24b02b", + "corpus_hash": "sha256:127a498eb4b44976bf6b1fe8d39d2c7412a4d31b63c991f2d54dd8a525e4eb1c", "suites": [ { "suite": "scion", @@ -9,7 +9,7 @@ { "suite": "w3c", "file": "corpus/w3c.json", - "case_count": 156 + "case_count": 168 }, { "suite": "statifier", diff --git a/conformance/registry.json b/conformance/registry.json index 1c435657..c30ef94e 100644 --- a/conformance/registry.json +++ b/conformance/registry.json @@ -1,6 +1,6 @@ { "implementation": "statifier-ex", - "corpus_hash": "sha256:c15e87820da654e8dde6ae97bed1631051cf6456c461cf67e7b617a36b24b02b", + "corpus_hash": "sha256:127a498eb4b44976bf6b1fe8d39d2c7412a4d31b63c991f2d54dd8a525e4eb1c", "claims": ["scion","statifier","w3c-mandatory","w3c-optional"], "entries": [ {"case_id":"scion/actionSend/send1","suite":"scion"}, @@ -292,19 +292,30 @@ {"case_id":"w3c/test504","suite":"w3c"}, {"case_id":"w3c/test505","suite":"w3c"}, {"case_id":"w3c/test506","suite":"w3c"}, + {"case_id":"w3c/test509","suite":"w3c"}, + {"case_id":"w3c/test510","suite":"w3c"}, + {"case_id":"w3c/test518","suite":"w3c"}, + {"case_id":"w3c/test519","suite":"w3c"}, + {"case_id":"w3c/test520","suite":"w3c"}, {"case_id":"w3c/test521","suite":"w3c"}, + {"case_id":"w3c/test522","suite":"w3c"}, {"case_id":"w3c/test525","suite":"w3c"}, {"case_id":"w3c/test527","suite":"w3c"}, {"case_id":"w3c/test528","suite":"w3c"}, {"case_id":"w3c/test529","suite":"w3c"}, {"case_id":"w3c/test530","suite":"w3c"}, + {"case_id":"w3c/test531","suite":"w3c"}, + {"case_id":"w3c/test532","suite":"w3c"}, {"case_id":"w3c/test533","suite":"w3c"}, + {"case_id":"w3c/test534","suite":"w3c"}, {"case_id":"w3c/test550","suite":"w3c"}, {"case_id":"w3c/test551","suite":"w3c"}, {"case_id":"w3c/test553","suite":"w3c"}, {"case_id":"w3c/test554","suite":"w3c"}, + {"case_id":"w3c/test567","suite":"w3c"}, {"case_id":"w3c/test570","suite":"w3c"}, {"case_id":"w3c/test576","suite":"w3c"}, + {"case_id":"w3c/test577","suite":"w3c"}, {"case_id":"w3c/test579","suite":"w3c"}, {"case_id":"w3c/test580","suite":"w3c"} ] diff --git a/conformance/schema/case.json b/conformance/schema/case.json index 9d20a469..5254c306 100644 --- a/conformance/schema/case.json +++ b/conformance/schema/case.json @@ -103,8 +103,15 @@ }, "host": { "type": "object", - "description": "Optional, and carried by no scion or w3c case: what a statifier case needs from the host running it. The host registers each of send_types with an Event I/O Processor of its own, which records every send it is handed and delivers none of them; a delayed send handed to it is its timer, and it never fires one. An expect_sends item's outcome says what more the host does with that send: reports it failed, or is handed a cancel for it. With no host object a case registers nothing. declared_events and expect_accepts are present together or not at all: the host checks the chart's declaration of the event names it accepts before it runs the case. to_source and expect_diff are present together or not at all, and mapping and expect_compatible_at each need them: the host diffs the case's chart to a second chart before it runs the case, and after the last step asks whether the position the steps left is untouched by the edit.", + "description": "Optional, carried by no scion case, and carried by a w3c case only with event_io_processors as its one key: what a case needs from the host running it. The host registers each of send_types with an Event I/O Processor of its own, which records every send it is handed and delivers none of them; a delayed send handed to it is its timer, and it never fires one. An expect_sends item's outcome says what more the host does with that send: reports it failed, or is handed a cancel for it. The host registers each of event_io_processors under its URI and its short form, with a location that reaches the running session, and delivers every send to it. With no host object a case registers nothing. declared_events and expect_accepts are present together or not at all: the host checks the chart's declaration of the event names it accepts before it runs the case. to_source and expect_diff are present together or not at all, and mapping and expect_compatible_at each need them: the host diffs the case's chart to a second chart before it runs the case, and after the last step asks whether the position the steps left is untouched by the edit.", "properties": { + "event_io_processors": { + "type": "array", + "description": "The Event I/O Processors the host runs, by URI (ADR-0075 decision 7): each is registered under its URI and its short form, with a location in _ioprocessors that reaches the running session through a loopback front, and every send to it is delivered, none recorded. The item set is closed; its one member is the Basic HTTP Event I/O Processor, whose short form is basichttp. A port that does not run a processor named here leaves the case unclaimed.", + "items": { "enum": ["http://www.w3.org/TR/scxml/#BasicHTTPEventProcessor"] }, + "minItems": 1, + "uniqueItems": true + }, "send_types": { "type": "array", "description": "The send type strings the case registers, in ADR-0069's sense: a send whose type is one of them is handed to the host's processor, and its target is that processor's own route string, which the SCXML processor never parses. None is a built-in spelling: not scxml, and not the SCXML Event I/O Processor URI.", @@ -504,10 +511,14 @@ "properties": { "id": { "pattern": "^w3c/" }, "conformance": { "enum": ["mandatory", "optional"] }, - "upstream": { "properties": { "license": { "const": "BSD-3-Clause-W3C" } } } + "upstream": { "properties": { "license": { "const": "BSD-3-Clause-W3C" } } }, + "host": { + "properties": { "event_io_processors": true }, + "required": ["event_io_processors"], + "additionalProperties": false + } }, - "required": ["upstream"], - "not": { "required": ["host"] } + "required": ["upstream"] } }, { diff --git a/docs/basichttp.md b/docs/basichttp.md new file mode 100644 index 00000000..ae12714a --- /dev/null +++ b/docs/basichttp.md @@ -0,0 +1,210 @@ +# The Basic HTTP Event I/O Processor + +SCXML appendix C.2 defines the Basic HTTP Event I/O Processor: a `` +whose `type` names it is delivered as an HTTP POST, and a session that runs +it accepts POSTs at an address of its own and raises each one as an +external event. Statifier ships it as `Statifier.Send.BasicHTTP`, a +`Statifier.Send.Processor` a host registers per session like any other send +type (see "The `` half" in [Extending Statifier](extending.md)). A +session that does not register it sees none of this page: no request is +made, nothing is started, and `_ioprocessors` holds the SCXML processor's +entry alone. + +The decisions behind the processor are +[ADR-0075](https://github.com/riddler/statifier-ex/blob/main/docs/adr/0075-basichttp-event-io-processor.md). + +## Registering it + +Register the processor under both of its type strings, the spec's URI and +the short form `basichttp`, with the base URL your own front answers at: + +```elixir +base = "https://example.org/scxml" +registration = {Statifier.Send.BasicHTTP, base_url: base} + +Statifier.Session.start_link(chart, + send_types: %{ + "http://www.w3.org/TR/scxml/#BasicHTTPEventProcessor" => registration, + "basichttp" => registration + } +) +``` + +A `:send_types` value is a bare module or `{module, opts}`; this processor +needs the options form. Its options: + +- `:base_url` (required) - the address your front answers at. A + registration without it is refused when the session starts, with an + `ArgumentError` naming the option. +- `:transport` - the module the POSTs go through, a + `Statifier.Send.BasicHTTP.Transport`. The default is + `Statifier.Send.BasicHTTP.Transport.Httpc`, on OTP's `:httpc`, which + verifies TLS peers against the system CA store and bounds each request + with a timeout. The package adds no dependency for it; the last section + below shows an adapter on `req`. + +Neither type string is a built-in spelling, so the registration redirects +no built-in send. + +## The location + +C.2.3 asks for an `_ioprocessors` entry holding a `location` that external +entities can use to reach the session. The session carries one entry under +each registered string, and both hold the same location: the base URL, a +`/`, and the session's `_sessionid`. + +```xml + +``` + +The entries are written once, when the session starts, and persist with +the datamodel, so a resumed session reads the location it started with. + +## The front you write around `decode/1` + +The library receives nothing on its own. Your host runs the HTTP endpoint +at the base URL, and for each request it: + +1. resolves the path after the base URL to a session id, and that id to a + running session; +2. hands the request to `Statifier.Send.BasicHTTP.decode/1`, a pure + function that knows no session; +3. enqueues the event it answers as an external event, for example with + `Statifier.Session.send_event/2`; +4. answers by the status rule below. + +`decode/1` takes a map of the request's `:method`, `:content_type` (`nil` +when absent), `:body` and `:query` (`nil` when the URL has none), and +answers `{:ok, event}` or `{:error, reason}`. The status rule: + +| `decode/1` answers | The front answers | +|---|---| +| `{:ok, event}` | 204, once the event is enqueued and before it is processed | +| `{:error, {:method_not_allowed, method}}` | 405, with `Allow: POST` | +| any other `{:error, _}` | 400 | +| (the path names no session the front can reach) | 404 | + +A front on Plug might read (text, not compiled; Plug is not a dependency of +this package): + +```elixir +def call(%Plug.Conn{path_info: [session_id]} = conn, _opts) do + {:ok, body, conn} = Plug.Conn.read_body(conn) + + request = %{ + method: conn.method, + content_type: conn |> Plug.Conn.get_req_header("content-type") |> List.first(), + body: body, + query: if(conn.query_string == "", do: nil, else: conn.query_string) + } + + with {:ok, pid} <- MyApp.Sessions.whereis(session_id), + {:ok, event} <- Statifier.Send.BasicHTTP.decode(request) do + :ok = Statifier.Session.send_event(pid, event) + Plug.Conn.send_resp(conn, 204, "") + else + :no_session -> Plug.Conn.send_resp(conn, 404, "") + {:error, {:method_not_allowed, _method}} -> + conn |> Plug.Conn.put_resp_header("allow", "POST") |> Plug.Conn.send_resp(405, "") + {:error, _reason} -> Plug.Conn.send_resp(conn, 400, "") + end +end +``` + +This repository's own loopback front, which its conformance runs deliver +through, is `Mix.Statifier.BasicHTTPFront`: repository tooling on OTP's +`:inets` httpd, not part of the package, and a small model of the steps +above. + +## The mapping, both ways + +Outbound, a `` to the processor becomes one POST (C.2.2): + +| The `` carries | The request carries | +|---|---| +| `event` | the form parameter `_scxmleventname` | +| `namelist` entries and `` children | one form parameter each, in an `application/x-www-form-urlencoded` body; a repeated name keeps the last value | +| a `` child | the content as the body, sent as `text/plain`; with an `event` too, `_scxmleventname` travels as a query parameter of the target URL | +| no parameters and no content | `_scxmleventname` alone, as a form body | +| a `` that evaluates to a map | a form-encoded body, as parameters would be | +| neither `target` nor `targetexpr` | no request: `error.communication` on the sender's internal queue, carrying the send id | + +A parameter value is written as text: a string as it is, a number or a +boolean as its literal, `nil` as `null`, and an undefined value as the +empty string. The processor makes one attempt. A transport error, or a +status outside 2xx, reaches the sender as `error.communication` carrying +the send id, through `Statifier.Session.failed_send/3`. A `` is +held by the processor's own timer, and a `` naming the send cancels +it while it has not fired. + +Inbound, `decode/1` turns one request into one event (C.2.1): + +| The request carries | The event carries | +|---|---| +| one or more `_scxmleventname` parameters | the first as its name, the query string read before the body | +| no `_scxmleventname` | `HTTP.` and the method in upper case as its name (`HTTP.POST`) | +| a form body | every other parameter, query string included, in `_event.data` | +| a body of any other content type | the body as `_event.data`; the query string gives the name only | +| (always) | the processor URI as `origintype` | + +Each value is read as a `` body's text is: a predicator literal +becomes that value, so `2` reads as the number 2, and anything else stays +a string. + +## What is not supported + +- JSON bodies and charset handling inbound: a non-form body is read as + text. +- A parameter value that is a list or a map outbound: its encoding is not + decided; today it is written with `inspect/1`. +- `_event.origin` on an inbound event: the decoder has no address a reply + could be sent to. +- A location after a resume on a host whose base URL moved: the persisted + `_ioprocessors` keeps the location written when the session started. +- Authentication of inbound POSTs: C.2 defines none, and a front that + needs it adds it itself. +- A session that is persisted and not running: the location reaches a + live registered session only. A durable host's front resolves the + location to its own execution. + +## In the conformance corpus + +The W3C documents that need the processor are in the corpus, each with a +`host` object whose `event_io_processors` names the processor's URI; a +runner registers it under both type strings and delivers through a +loopback front (`conformance/RATCHET.md`, "The `host` object"). Eleven of +them are claimed. `test201` is in the corpus and unclaimed: it expects an +event sent through the processor to arrive ahead of a `` the same +step appends to the session's own external queue, and a delivery from +outside the session never can. + +## A transport on `req` + +A host that already uses `req` writes an adapter of its own and passes it +as `:transport`. As text, not compiled; this package does not depend on +`req`: + +```elixir +defmodule MyApp.ReqTransport do + @behaviour Statifier.Send.BasicHTTP.Transport + + @impl true + def post(url, headers, body) do + case Req.post(url, headers: headers, body: body, retry: false) do + {:ok, %Req.Response{status: status}} -> {:ok, status} + {:error, exception} -> {:error, exception} + end + end +end +``` + +`retry: false` keeps the one attempt the processor makes: retrying is not +the transport's job, and a status outside 2xx is answered as +`{:ok, status}`, not as an error, so the processor reports it as a miss. + +```elixir +registration = + {Statifier.Send.BasicHTTP, base_url: base, transport: MyApp.ReqTransport} +``` diff --git a/docs/extending.md b/docs/extending.md index fde0e958..39c7894e 100644 --- a/docs/extending.md +++ b/docs/extending.md @@ -610,7 +610,9 @@ Statifier.Session.start_link(machine, ``` `:send_types` is a `%{type_string => module}` map, each module a -`Statifier.Send.Processor`. The default is `%{}`, which registers nothing: +`Statifier.Send.Processor`; a value may also be `{module, opts}`, whose +options reach the processor (`Statifier.Send.Processor`'s "Registration +options"). The default is `%{}`, which registers nothing: only the built-in spellings are supported, and with no `:send_types` passed nothing observable changes. The session derives the registered set from the map's own keys through `Statifier.Send.Types.from_send_types/1`, the one @@ -774,7 +776,10 @@ a session supports, and a registered type is one. A session registering The entry is keyed by the type string, and its value is the map your processor's optional `ioprocessors_entry/1` returns for that type - an -empty map when it does not implement the callback. The value must be +empty map when it does not implement the callback. A processor whose +entry must address the session, such as a location a receiver POSTs to, +implements the optional `ioprocessors_entry/2` instead, which is handed +the session id and the registration's options. The value must be string-keyed at every level, as every datamodel value is; `Statifier.Send.Types.from_send_types/1` raises `ArgumentError` otherwise, so a bad value fails the session's start. The SCXML processor's own entry, @@ -788,6 +793,15 @@ and does not rewrite `_ioprocessors`. Re-stamp the set the session started with; a different set after a resume would need mid-session registration, which ADR-0069 names as a trigger that would reopen it. +### The Basic HTTP processor + +The library ships one registered processor of its own: +`Statifier.Send.BasicHTTP`, SCXML appendix C.2's Basic HTTP Event I/O +Processor, which delivers a send as an HTTP POST and turns a POST your +host's front receives into an event. [The Basic HTTP Event I/O +Processor](basichttp.md) says how to register it, the front your host +writes around its decoder, and the mapping both ways. + ### After a resume Which processor holds which delayed send is the live session's own state, diff --git a/docs/testing.md b/docs/testing.md index ef5b56b2..3f7983eb 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -16,8 +16,8 @@ ADR-0053) 127 native upstream SCION cases; the rest are excluded at generation time, with the count and the reason per case in `tools/corpus/scion/exclusions.exs` (see `tools/corpus/README.md`). Excluded by default. -3. **W3C suite** (`test/scxml_tests/`, tag `:scxml_w3`) - 156 emitted tests (154 - mandatory + 2 optional) out of 193 upstream W3C cases. Excluded by default. +3. **W3C suite** (`test/scxml_tests/`, tag `:scxml_w3`) - 168 emitted tests (154 + mandatory + 14 optional) out of 193 upstream W3C cases. Excluded by default. Dependency documents an `` loads at runtime (manifest `` entries) are not emitted as standalone tests - see `tools/corpus/README.md`. @@ -299,13 +299,13 @@ printed. Surfacing the figures there would need a JSON summary mode for the task plus a guarded `.quality.exs` edit with its own ledger entry (see the "Which skipped stages" discipline in `CLAUDE.md`) - both out of scope here. -The denominator behind both figures is the emitted corpus: 119 SCION and 156 +The denominator behind both figures is the emitted corpus: 119 SCION and 168 W3C files on disk today (`test/scion_tests/`, `test/scxml_tests/` - the W3C -figure is 154 `mandatory/` plus 2 `optional/`), not the upstream suites (127 +figure is 154 `mandatory/` plus 14 `optional/`), not the upstream suites (127 native SCION cases, 193 W3C cases). The emitted count is the only one 100% is reachable against - some upstream cases have no predicator equivalent and are excluded at generation time (script tags, list -concatenation, the BasicHTTP Event I/O Processor tree, and more), so no build +concatenation, an `` the library never dereferences, and more), so no build of this engine could ever pass every upstream case under the predicator datamodel commitment (docs/datamodel.md). `tools/corpus/README.md` documents the exclusion counts and reasons; a future edit to those exclusions changes diff --git a/lib/mix/statifier/corpus/host_case.ex b/lib/mix/statifier/corpus/host_case.ex index 6f48f51d..b62edde0 100644 --- a/lib/mix/statifier/corpus/host_case.ex +++ b/lib/mix/statifier/corpus/host_case.ex @@ -57,11 +57,31 @@ defmodule Mix.Statifier.Corpus.HostCase do 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 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`. """ + alias Mix.Statifier.BasicHTTPFront alias Mix.Statifier.Corpus.HostCase.Processor alias Statifier.{MachineState, Session} + @basic_http "http://www.w3.org/TR/scxml/#BasicHTTPEventProcessor" + + # The closed item set of `host.event_io_processors` + # (conformance/schema/case.json): each URI with the processor module and + # the short form it is registered under beside the URI (ADR-0075 + # decision 2). + @event_io_processors %{@basic_http => {Statifier.Send.BasicHTTP, "basichttp"}} + @settle_window_ms 100 @configuration_deadline_ms 4_000 @poll_interval_ms 5 @@ -87,22 +107,82 @@ defmodule Mix.Statifier.Corpus.HostCase do session_id = MachineState.generate_session_id() :yes = :global.register_name({Processor, session_id}, self()) - send_types = Map.new(Map.get(host, "send_types", []), &{&1, Processor}) + recorded = Map.new(Map.get(host, "send_types", []), &{&1, Processor}) + + with_event_io_processors(Map.get(host, "event_io_processors", []), fn delivering -> + {:ok, session} = + Statifier.start_session(machine, + session_id: session_id, + send_types: Map.merge(recorded, delivering), + subscribers: [self()] + ) + + try do + after_steps = Keyword.get(opts, :after_steps, fn _settled -> :ok end) + drive(session, corpus_case, Map.get(host, "expect_sends", []), after_steps) + after + Session.stop(session) + :global.unregister_name({Processor, session_id}) + end + end) + end + end - {:ok, session} = - Statifier.start_session(machine, - session_id: session_id, - send_types: send_types, - subscribers: [self()] - ) + @doc """ + The Event I/O Processor URIs of `host.event_io_processors`' closed item + set that `source` names as a ``'s `type`, sorted: what a corpus + case transformed from `source` declares in `host.event_io_processors`. - try do - after_steps = Keyword.get(opts, :after_steps, fn _settled -> :ok end) - drive(session, corpus_case, Map.get(host, "expect_sends", []), after_steps) - after - Session.stop(session) - :global.unregister_name({Processor, session_id}) - end + ## Examples + + iex> Mix.Statifier.Corpus.HostCase.event_io_processors( + ...> ~s|| + ...> ) + ["http://www.w3.org/TR/scxml/#BasicHTTPEventProcessor"] + + iex> Mix.Statifier.Corpus.HostCase.event_io_processors(~s||) + [] + + """ + @spec event_io_processors(source :: String.t()) :: [String.t()] + def event_io_processors(source) do + @event_io_processors + |> Map.keys() + |> Enum.filter(&Regex.match?(~r/]*\btype="#{Regex.escape(&1)}"/, source)) + |> Enum.sort() + end + + @doc """ + 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. + """ + @spec with_event_io_processors( + uris :: [String.t()], + fun :: (%{String.t() => {module(), keyword()}} -> result) + ) :: result + when result: term() + def with_event_io_processors([], fun), do: fun.(%{}) + + def with_event_io_processors(uris, fun) do + {:ok, front} = BasicHTTPFront.start() + + try do + uris + |> Enum.flat_map(fn uri -> + {module, short} = Map.fetch!(@event_io_processors, uri) + registration = {module, base_url: front.base_url} + [{uri, registration}, {short, registration}] + end) + |> Map.new() + |> fun.() + after + BasicHTTPFront.stop(front) end end diff --git a/lib/mix/statifier/corpus/json.ex b/lib/mix/statifier/corpus/json.ex index ca169d89..8403e630 100644 --- a/lib/mix/statifier/corpus/json.ex +++ b/lib/mix/statifier/corpus/json.ex @@ -17,8 +17,9 @@ defmodule Mix.Statifier.Corpus.Json do # initial_configuration, steps, upstream, host; a registry is # implementation, corpus_hash, claims, entries, and an entry case_id, # suite; each other object's keys appear here in its schema's order too - - # a host is send_types, expect_sends, declared_events, expect_accepts, - # to_source, mapping, expect_diff, expect_compatible_at; an `expect_sends` + # a host is event_io_processors, send_types, expect_sends, + # declared_events, expect_accepts, to_source, mapping, expect_diff, + # expect_compatible_at; an `expect_sends` # item is type, target, event, delay_ms, send_id, outcome; an `expect_accepts` # object is unreachable, undeclared; an `expect_diff` object is class, # reasons; and a reason is reason, then whichever of state, to_state, @@ -27,7 +28,7 @@ defmodule Mix.Statifier.Corpus.Json do case_count name url revision key reason detail adr spec conformance description required_features source initial_configuration steps type target event data delay_ms send_id outcome configuration upstream document license notice - modified host send_types + modified host event_io_processors send_types expect_sends declared_events expect_accepts unreachable undeclared to_source mapping expect_diff class reasons state to_state fields index t_index descriptor data_id expect_compatible_at diff --git a/lib/mix/statifier/corpus/runner.ex b/lib/mix/statifier/corpus/runner.ex index 401143e5..b3d7d6d7 100644 --- a/lib/mix/statifier/corpus/runner.ex +++ b/lib/mix/statifier/corpus/runner.ex @@ -91,9 +91,17 @@ defmodule Mix.Statifier.Corpus.Runner do the session runtime a case using ``, `` or a delay runs through (ADR-0027: the library starts no processes of its own). Idempotent: a runtime already placed is left as it is. + + It also puts OTP's `:inets`, `:ssl` and `:public_key` on the code path, + which Mix leaves off for an application no dependency lists: a case whose + `host` runs an Event I/O Processor delivers through the processor's + default transport on `:httpc` and a loopback front on `:inets` httpd + (`Mix.Statifier.Corpus.HostCase`), and `:httpc` reads `:public_key` even + for a plain `http:` request. """ @spec start_runtime() :: :ok def start_runtime do + Enum.each([:inets, :public_key, :ssl], &Mix.ensure_application!/1) Mix.Task.run("app.start") case Statifier.Supervisor.start_link([]) do diff --git a/lib/mix/statifier/corpus/upstream.ex b/lib/mix/statifier/corpus/upstream.ex index 9db049ee..46e600f6 100644 --- a/lib/mix/statifier/corpus/upstream.ex +++ b/lib/mix/statifier/corpus/upstream.ex @@ -29,10 +29,14 @@ defmodule Mix.Statifier.Corpus.Upstream do the Apache License 2.0's section 4(b) requires of a modified file. A W3C case carries the transformed document as `Mix.Statifier.Corpus.XmlFormat` formats it, and the IRP's own expectation: - a test passes by reaching the final state `pass` with no event sent. + a test passes by reaching the final state `pass` with no event sent. A W3C + document that names an Event I/O Processor the corpus host runs as a + ``'s type (`Mix.Statifier.Corpus.HostCase.event_io_processors/1`) + also carries a `host` object whose one key, `event_io_processors`, names + it (ADR-0075 decision 7): the host delivers through it. """ - alias Mix.Statifier.Corpus.{Files, XmlFormat} + alias Mix.Statifier.Corpus.{Files, HostCase, XmlFormat} alias Statifier.Testing.FeatureDetector @scion_notice "LICENSES/Apache-2.0.txt" @@ -243,7 +247,7 @@ defmodule Mix.Statifier.Corpus.Upstream do with {:ok, description} <- Files.read(Path.rootname(input) <> ".description"), {:ok, uri} <- fetch_uri(uris, name) do {:ok, - %{ + put_host(%{ "id" => "w3c/#{name}", "suite" => "w3c", "spec" => spec, @@ -258,7 +262,14 @@ defmodule Mix.Statifier.Corpus.Upstream do "license" => "BSD-3-Clause-W3C", "notice" => @w3c_notice } - }} + })} + end + end + + defp put_host(%{"source" => source} = corpus_case) do + case HostCase.event_io_processors(source) do + [] -> corpus_case + uris -> Map.put(corpus_case, "host", %{"event_io_processors" => uris}) end end diff --git a/mix.exs b/mix.exs index f40daf67..8c1181de 100644 --- a/mix.exs +++ b/mix.exs @@ -57,6 +57,7 @@ defmodule Statifier.MixProject do "docs/architecture.md", "docs/datamodel.md", "docs/extending.md", + "docs/basichttp.md", "docs/persistence.md", "docs/hosting-without-session.md", "docs/durable-timers.md", @@ -92,6 +93,7 @@ defmodule Statifier.MixProject do docs/architecture.md docs/datamodel.md docs/extending.md + docs/basichttp.md docs/persistence.md docs/hosting-without-session.md docs/durable-timers.md diff --git a/test/corpus/schema_test.exs b/test/corpus/schema_test.exs index 6f878cec..62546ce0 100644 --- a/test/corpus/schema_test.exs +++ b/test/corpus/schema_test.exs @@ -17,6 +17,7 @@ defmodule Corpus.SchemaTest do @fixture_dir "test/fixtures/corpus_schema" @schemas ~w(case.json corpus.json manifest.json registry.json exclusions.json) @draft "https://json-schema.org/draft/2020-12/schema" + @basic_http "http://www.w3.org/TR/scxml/#BasicHTTPEventProcessor" defp schema(name), do: Checker.load(@schema_dir, name) defp fixture(name), do: Checker.load(@fixture_dir, name) @@ -99,12 +100,69 @@ defmodule Corpus.SchemaTest do end # sabotage: deleting the scion branch's "not" -> red - test "refuses host on a scion or w3c case" do - host = fixture("case-statifier.json")["host"] + test "refuses host on a scion case, event_io_processors included" do + scion = fixture("case-scion.json") - for name <- ~w(case-scion.json case-w3c.json) do - assert "" in pointers("case.json", Map.put(fixture(name), "host", host)) + assert "" in pointers( + "case.json", + Map.put(scion, "host", fixture("case-statifier.json")["host"]) + ) + + assert "" in pointers( + "case.json", + Map.put(scion, "host", %{"event_io_processors" => [@basic_http]}) + ) + end + + # sabotage: the w3c branch's host losing "additionalProperties": false -> + # red on send_types + test "refuses every host key on a w3c case but event_io_processors" do + host = Map.put(fixture("case-statifier.json")["host"], "event_io_processors", [@basic_http]) + pointers = pointers("case.json", Map.put(fixture("case-w3c.json"), "host", host)) + + for key <- Map.keys(host) -- ["event_io_processors"] do + assert "/host/#{key}" in pointers end + + refute "/host/event_io_processors" in pointers + end + + # sabotage: the w3c branch's host losing "required" -> red on the empty host + test "a w3c case's host carries event_io_processors, and nothing else" do + w3c = fixture("case-w3c.json") + + assert errors("case.json", Map.put(w3c, "host", %{"event_io_processors" => [@basic_http]})) == + [] + + assert {"/host", "missing required event_io_processors"} in errors( + "case.json", + Map.put(w3c, "host", %{}) + ) + end + + # sabotage: event_io_processors' items losing their "enum" -> red on the + # other URI + test "event_io_processors is a non-empty, unique list from its closed set" do + with_processors = &Map.put(fixture("case-w3c.json"), "host", %{"event_io_processors" => &1}) + + assert "/host/event_io_processors/0" in pointers( + "case.json", + with_processors.(["http://www.w3.org/TR/scxml/#SCXMLEventProcessor"]) + ) + + assert "/host/event_io_processors" in pointers("case.json", with_processors.([])) + + assert "/host/event_io_processors" in pointers( + "case.json", + with_processors.([@basic_http, @basic_http]) + ) + + statifier = fixture("case-statifier.json") + + assert errors( + "case.json", + put_in(statifier, ["host", "event_io_processors"], [@basic_http]) + ) == [] end # sabotage: host's send_types items losing "type": "string" -> red diff --git a/test/mix/statifier/corpus/host_case_test.exs b/test/mix/statifier/corpus/host_case_test.exs index 724edaf1..b752b49c 100644 --- a/test/mix/statifier/corpus/host_case_test.exs +++ b/test/mix/statifier/corpus/host_case_test.exs @@ -2,6 +2,10 @@ defmodule Mix.Statifier.Corpus.HostCaseTest do use ExUnit.Case, async: true alias Mix.Statifier.Corpus.{HostCase, Runner} + alias Statifier.Send.BasicHTTP + alias Statifier.Send.BasicHTTP.Transport.Httpc + + doctest Mix.Statifier.Corpus.HostCase # A host case run through `Runner.run_case/1`, which hands every case with # a `host` object to `Mix.Statifier.Corpus.HostCase`. @@ -410,4 +414,104 @@ defmodule Mix.Statifier.Corpus.HostCaseTest do assert HostCase.run(hold_case(host)) == :agree end end + + describe "a host that runs an Event I/O Processor" do + # A W3C-shaped case whose host object carries event_io_processors, its + # one key there (ADR-0075 decision 7). The document sends `ping` to its + # own Basic HTTP location and passes only when the event comes back. + + @basic_http "http://www.w3.org/TR/scxml/#BasicHTTPEventProcessor" + + @delivering """ + + + + + + + + + + + + + """ + + defp delivering_case(host) do + %{ + "id" => "w3c/delivering", + "source" => @delivering, + "description" => "", + "initial_configuration" => ["pass"], + "steps" => [], + "host" => host + } + end + + # sabotage: run/2 handing with_event_io_processors/2 [] instead of the + # host's list -> the send's type is unregistered and the chart reaches + # fail -> red + test "delivers the document's send through a loopback front, and agrees" do + assert Runner.run_case(delivering_case(%{"event_io_processors" => [@basic_http]})) == + :agree + end + + # sabotage: with_event_io_processors/2 registering the recording + # processor for the URI instead -> the send is handed, not delivered, + # and the chart reaches fail -> red + test "hands none of the processor's sends to the recording processor" do + host = %{ + "event_io_processors" => [@basic_http], + "send_types" => ["myapp:sink"], + "expect_sends" => [] + } + + assert Runner.run_case(delivering_case(host)) == :agree + end + + # sabotage: with_event_io_processors/2 dropping the short-form entry -> + # the map has one key -> red + test "registers the processor under its URI and its short form, at a live front" do + HostCase.with_event_io_processors([@basic_http], fn send_types -> + assert %{ + @basic_http => {BasicHTTP, [base_url: base_url]}, + "basichttp" => {BasicHTTP, [base_url: base_url]} + } = send_types + + assert map_size(send_types) == 2 + + assert Httpc.post(base_url <> "/sess_nobody", [{"content-type", "text/plain"}], "x") == + {:ok, 404} + + send(self(), {:base_url, base_url}) + end) + + assert_received {:base_url, base_url} + assert {:error, _reason} = Httpc.post(base_url <> "/sess_nobody", [], "") + end + + # sabotage: with_event_io_processors/2 without its `after` -> the front + # outlives the raise and still answers -> red + test "stops the front when the function raises, and raises on a URI outside the set" do + assert_raise RuntimeError, "inside", fn -> + HostCase.with_event_io_processors([@basic_http], fn %{@basic_http => {_m, opts}} -> + send(self(), {:base_url, opts[:base_url]}) + raise "inside" + end) + end + + assert_received {:base_url, base_url} + assert {:error, _reason} = Httpc.post(base_url <> "/sess_nobody", [], "") + + assert_raise KeyError, fn -> + HostCase.with_event_io_processors(["urn:example:other"], & &1) + end + end + + # sabotage: the `[]` clause handing `fun` a map with an entry -> red + test "starts no front and hands an empty map when no processor is named" do + assert HostCase.with_event_io_processors([], & &1) == %{} + end + end end diff --git a/test/mix/statifier/corpus/upstream_test.exs b/test/mix/statifier/corpus/upstream_test.exs new file mode 100644 index 00000000..710203e4 --- /dev/null +++ b/test/mix/statifier/corpus/upstream_test.exs @@ -0,0 +1,79 @@ +defmodule Mix.Statifier.Corpus.UpstreamTest do + use ExUnit.Case, async: true + + import Statifier.TmpDir, only: [setup_tmp_dir: 1] + + alias Mix.Statifier.Corpus.Upstream + + # Upstream.read/3 over a scratch tree built in a temporary directory: two + # W3C documents, one naming the Basic HTTP Event I/O Processor as a send + # type and one not, and an empty SCION tree. + + setup :setup_tmp_dir + + @basic_http "http://www.w3.org/TR/scxml/#BasicHTTPEventProcessor" + + @manifest """ + + + + + + + + + + + + """ + + defp document(send) do + """ + + + #{send} + + + + + """ + end + + defp scratch(root) do + File.mkdir_p!(Path.join(root, "scion/cases")) + w3c = Path.join(root, "scxml_w3/cases") + dir = Path.join(w3c, "optional/processors") + File.mkdir_p!(dir) + File.write!(Path.join(w3c, "manifest.xml"), @manifest) + + File.write!( + Path.join(dir, "test9101.scxml"), + document( + ~s|| + ) + ) + + File.write!(Path.join(dir, "test9102.scxml"), document(~s||)) + + for id <- ~w(test9101 test9102), + do: File.write!(Path.join(dir, id <> ".description"), "a description") + + root + end + + describe "read/3" do + # sabotage: w3c_predicator_case/6 not calling put_host/1 -> test9101 + # carries no host object -> red + @tag :isolated_tmp_dir + test "a W3C document naming the Basic HTTP processor as a send type declares it as the host's", + %{tmp_dir: root} do + assert {:ok, [with_processor, without]} = Upstream.read(scratch(root), [], []) + + assert with_processor["id"] == "w3c/test9101" + assert with_processor["host"] == %{"event_io_processors" => [@basic_http]} + + assert without["id"] == "w3c/test9102" + refute Map.has_key?(without, "host") + end + end +end diff --git a/test/passing_tests.json b/test/passing_tests.json index 228a1d50..d070da0f 100644 --- a/test/passing_tests.json +++ b/test/passing_tests.json @@ -4,7 +4,7 @@ "test/mix/**/*_test.exs", "test/statifier/**/*_test.exs" ], - "last_updated": "2026-09-26", + "last_updated": "2026-09-30", "scion_tests": [ "test/scion_tests/action_send/send1_test.exs", "test/scion_tests/action_send/send2_test.exs", @@ -312,6 +312,17 @@ "test/scxml_tests/mandatory/system_variables/test339_test.exs", "test/scxml_tests/mandatory/system_variables/test342_test.exs", "test/scxml_tests/mandatory/system_variables/test346_test.exs", + "test/scxml_tests/optional/basic_http_event_processor/test509_test.exs", + "test/scxml_tests/optional/basic_http_event_processor/test510_test.exs", + "test/scxml_tests/optional/basic_http_event_processor/test518_test.exs", + "test/scxml_tests/optional/basic_http_event_processor/test519_test.exs", + "test/scxml_tests/optional/basic_http_event_processor/test520_test.exs", + "test/scxml_tests/optional/basic_http_event_processor/test522_test.exs", + "test/scxml_tests/optional/basic_http_event_processor/test531_test.exs", + "test/scxml_tests/optional/basic_http_event_processor/test532_test.exs", + "test/scxml_tests/optional/basic_http_event_processor/test534_test.exs", + "test/scxml_tests/optional/basic_http_event_processor/test567_test.exs", + "test/scxml_tests/optional/basic_http_event_processor/test577_test.exs", "test/scxml_tests/optional/ecma_profile/test278_test.exs", "test/scxml_tests/optional/scxml_event_processor/test193_test.exs" ] diff --git a/test/scxml_tests/optional/basic_http_event_processor/test509_test.exs b/test/scxml_tests/optional/basic_http_event_processor/test509_test.exs new file mode 100644 index 00000000..1c597523 --- /dev/null +++ b/test/scxml_tests/optional/basic_http_event_processor/test509_test.exs @@ -0,0 +1,63 @@ +# Generated from conformance/corpus/w3c.json, case w3c/test509, by +# tools/corpus/scxml_w3/cases.exs. Regenerate with `mise run corpus:emit`; +# never edit by hand. +# +# The document in this test is transformed for the predicator datamodel from +# 509/test509.txml of the W3C SCXML Implementation Report Plan test suite +# (https://www.w3.org/Voice/2013/scxml-irp/). +# +# Copyright © 2015 W3C® (MIT, ERCIM, Keio, Beihang), All Rights Reserved. +# +# Redistributed under BSD-3-Clause-W3C, the W3C 3-clause BSD License; its +# conditions and disclaimer are in conformance/LICENSES/BSD-3-Clause-W3C.txt. +defmodule SCXMLTest.BasicHttpEventProcessor.Test509 do + use Statifier.Case, async: true + alias Mix.Statifier.Corpus.HostCase + + @moduletag :scxml_w3 + @tag required_features: [ + :basic_states, + :event_transitions, + :final_states, + :log_elements, + :onentry_actions, + :send_delay_expressions, + :send_elements, + :target_expressions, + :wildcard_events + ] + @tag conformance: "optional", spec: "BasicHTTPEventProcessor" + test "test509" do + xml = """ + + + + + + + + + + + + + + + + + + + + + + """ + + description = + "An SCXML Processor that supports the Basic HTTP Event I/O Processor MUST accept messages at the access URI as HTTP POST requests" + + HostCase.with_event_io_processors( + ["http://www.w3.org/TR/scxml/#BasicHTTPEventProcessor"], + fn send_types -> test_scxml(xml, description, ["pass"], [], send_types: send_types) end + ) + end +end diff --git a/test/scxml_tests/optional/basic_http_event_processor/test510_test.exs b/test/scxml_tests/optional/basic_http_event_processor/test510_test.exs new file mode 100644 index 00000000..4e002667 --- /dev/null +++ b/test/scxml_tests/optional/basic_http_event_processor/test510_test.exs @@ -0,0 +1,70 @@ +# Generated from conformance/corpus/w3c.json, case w3c/test510, by +# tools/corpus/scxml_w3/cases.exs. Regenerate with `mise run corpus:emit`; +# never edit by hand. +# +# The document in this test is transformed for the predicator datamodel from +# 510/test510.txml of the W3C SCXML Implementation Report Plan test suite +# (https://www.w3.org/Voice/2013/scxml-irp/). +# +# Copyright © 2015 W3C® (MIT, ERCIM, Keio, Beihang), All Rights Reserved. +# +# Redistributed under BSD-3-Clause-W3C, the W3C 3-clause BSD License; its +# conditions and disclaimer are in conformance/LICENSES/BSD-3-Clause-W3C.txt. +defmodule SCXMLTest.BasicHttpEventProcessor.Test510 do + use Statifier.Case, async: true + alias Mix.Statifier.Corpus.HostCase + + @moduletag :scxml_w3 + @tag required_features: [ + :basic_states, + :compound_states, + :event_transitions, + :final_states, + :log_elements, + :onentry_actions, + :raise_elements, + :send_delay_expressions, + :send_elements, + :target_expressions, + :wildcard_events + ] + @tag conformance: "optional", spec: "BasicHTTPEventProcessor" + test "test510" do + xml = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + """ + + description = + "The SCXML Processor MUST validate the message it receives [via the Basic HTTP Event I/O Processor] and then MUST build the appropriate SCXML event and MUST add it to the external event queue" + + HostCase.with_event_io_processors( + ["http://www.w3.org/TR/scxml/#BasicHTTPEventProcessor"], + fn send_types -> test_scxml(xml, description, ["pass"], [], send_types: send_types) end + ) + end +end diff --git a/test/scxml_tests/optional/basic_http_event_processor/test518_test.exs b/test/scxml_tests/optional/basic_http_event_processor/test518_test.exs new file mode 100644 index 00000000..b28d641b --- /dev/null +++ b/test/scxml_tests/optional/basic_http_event_processor/test518_test.exs @@ -0,0 +1,68 @@ +# Generated from conformance/corpus/w3c.json, case w3c/test518, by +# tools/corpus/scxml_w3/cases.exs. Regenerate with `mise run corpus:emit`; +# never edit by hand. +# +# The document in this test is transformed for the predicator datamodel from +# 518/test518.txml of the W3C SCXML Implementation Report Plan test suite +# (https://www.w3.org/Voice/2013/scxml-irp/). +# +# Copyright © 2015 W3C® (MIT, ERCIM, Keio, Beihang), All Rights Reserved. +# +# Redistributed under BSD-3-Clause-W3C, the W3C 3-clause BSD License; its +# conditions and disclaimer are in conformance/LICENSES/BSD-3-Clause-W3C.txt. +defmodule SCXMLTest.BasicHttpEventProcessor.Test518 do + use Statifier.Case, async: true + alias Mix.Statifier.Corpus.HostCase + + @moduletag :scxml_w3 + @tag required_features: [ + :basic_states, + :data_elements, + :datamodel, + :event_transitions, + :final_states, + :log_elements, + :onentry_actions, + :send_delay_expressions, + :send_elements, + :target_expressions, + :wildcard_events + ] + @tag conformance: "optional", spec: "BasicHTTPEventProcessor" + test "test518" do + xml = """ + + + + + + + + + + + + + + + + + + + + + + + + + """ + + description = + "If the namelist attribute is defined [in send], the SCXML Processor MUST map its variable names and values to HTTP POST parameters" + + HostCase.with_event_io_processors( + ["http://www.w3.org/TR/scxml/#BasicHTTPEventProcessor"], + fn send_types -> test_scxml(xml, description, ["pass"], [], send_types: send_types) end + ) + end +end diff --git a/test/scxml_tests/optional/basic_http_event_processor/test519_test.exs b/test/scxml_tests/optional/basic_http_event_processor/test519_test.exs new file mode 100644 index 00000000..f4418c7b --- /dev/null +++ b/test/scxml_tests/optional/basic_http_event_processor/test519_test.exs @@ -0,0 +1,66 @@ +# Generated from conformance/corpus/w3c.json, case w3c/test519, by +# tools/corpus/scxml_w3/cases.exs. Regenerate with `mise run corpus:emit`; +# never edit by hand. +# +# The document in this test is transformed for the predicator datamodel from +# 519/test519.txml of the W3C SCXML Implementation Report Plan test suite +# (https://www.w3.org/Voice/2013/scxml-irp/). +# +# Copyright © 2015 W3C® (MIT, ERCIM, Keio, Beihang), All Rights Reserved. +# +# Redistributed under BSD-3-Clause-W3C, the W3C 3-clause BSD License; its +# conditions and disclaimer are in conformance/LICENSES/BSD-3-Clause-W3C.txt. +defmodule SCXMLTest.BasicHttpEventProcessor.Test519 do + use Statifier.Case, async: true + alias Mix.Statifier.Corpus.HostCase + + @moduletag :scxml_w3 + @tag required_features: [ + :basic_states, + :event_transitions, + :final_states, + :log_elements, + :onentry_actions, + :send_delay_expressions, + :send_elements, + :send_param_elements, + :target_expressions, + :wildcard_events + ] + @tag conformance: "optional", spec: "BasicHTTPEventProcessor" + test "test519" do + xml = """ + + + + + + + + + + + + + + + + + + + + + + + + """ + + description = + "If one or more param children are present [in send], the SCXML Processor MUST map their names (i.e. name attributes) and values to HTTP POST parameters" + + HostCase.with_event_io_processors( + ["http://www.w3.org/TR/scxml/#BasicHTTPEventProcessor"], + fn send_types -> test_scxml(xml, description, ["pass"], [], send_types: send_types) end + ) + end +end diff --git a/test/scxml_tests/optional/basic_http_event_processor/test520_test.exs b/test/scxml_tests/optional/basic_http_event_processor/test520_test.exs new file mode 100644 index 00000000..0d57cbf4 --- /dev/null +++ b/test/scxml_tests/optional/basic_http_event_processor/test520_test.exs @@ -0,0 +1,67 @@ +# Generated from conformance/corpus/w3c.json, case w3c/test520, by +# tools/corpus/scxml_w3/cases.exs. Regenerate with `mise run corpus:emit`; +# never edit by hand. +# +# The document in this test is transformed for the predicator datamodel from +# 520/test520.txml of the W3C SCXML Implementation Report Plan test suite +# (https://www.w3.org/Voice/2013/scxml-irp/). +# +# Copyright © 2015 W3C® (MIT, ERCIM, Keio, Beihang), All Rights Reserved. +# +# Redistributed under BSD-3-Clause-W3C, the W3C 3-clause BSD License; its +# conditions and disclaimer are in conformance/LICENSES/BSD-3-Clause-W3C.txt. +defmodule SCXMLTest.BasicHttpEventProcessor.Test520 do + use Statifier.Case, async: true + alias Mix.Statifier.Corpus.HostCase + + @moduletag :scxml_w3 + @tag required_features: [ + :basic_states, + :event_transitions, + :final_states, + :log_elements, + :onentry_actions, + :send_content_elements, + :send_delay_expressions, + :send_elements, + :target_expressions, + :wildcard_events + ] + @tag conformance: "optional", spec: "BasicHTTPEventProcessor" + test "test520" do + xml = """ + + + + + + + this is some content + + + + + + + + + + + + + + + + + + """ + + description = + "If a content child is present, the SCXML Processor MUST use its value as the body of the message." + + HostCase.with_event_io_processors( + ["http://www.w3.org/TR/scxml/#BasicHTTPEventProcessor"], + fn send_types -> test_scxml(xml, description, ["pass"], [], send_types: send_types) end + ) + end +end diff --git a/test/scxml_tests/optional/basic_http_event_processor/test522_test.exs b/test/scxml_tests/optional/basic_http_event_processor/test522_test.exs new file mode 100644 index 00000000..2f2f530e --- /dev/null +++ b/test/scxml_tests/optional/basic_http_event_processor/test522_test.exs @@ -0,0 +1,64 @@ +# Generated from conformance/corpus/w3c.json, case w3c/test522, by +# tools/corpus/scxml_w3/cases.exs. Regenerate with `mise run corpus:emit`; +# never edit by hand. +# +# The document in this test is transformed for the predicator datamodel from +# 522/test522.txml of the W3C SCXML Implementation Report Plan test suite +# (https://www.w3.org/Voice/2013/scxml-irp/). +# +# Copyright © 2015 W3C® (MIT, ERCIM, Keio, Beihang), All Rights Reserved. +# +# Redistributed under BSD-3-Clause-W3C, the W3C 3-clause BSD License; its +# conditions and disclaimer are in conformance/LICENSES/BSD-3-Clause-W3C.txt. +defmodule SCXMLTest.BasicHttpEventProcessor.Test522 do + use Statifier.Case, async: true + alias Mix.Statifier.Corpus.HostCase + + @moduletag :scxml_w3 + @tag required_features: [ + :basic_states, + :event_transitions, + :final_states, + :log_elements, + :onentry_actions, + :send_delay_expressions, + :send_elements, + :target_expressions, + :wildcard_events + ] + @tag conformance: "optional", spec: "BasicHTTPEventProcessor" + test "test522" do + xml = """ + + + + + + + + + + + + + + + + + + + + + + + """ + + description = + "SCXML Processors that support the BasicHTTP Event I/O Processor MUST maintain a 'basichttp' entry in the _ioprocessors system variable. The Processor MUST maintain a in 'location' field inside this entry whose value holds an address that external entities can use to communicate with this SCXML session using the Basic HTTP Event I/O Processor." + + HostCase.with_event_io_processors( + ["http://www.w3.org/TR/scxml/#BasicHTTPEventProcessor"], + fn send_types -> test_scxml(xml, description, ["pass"], [], send_types: send_types) end + ) + end +end diff --git a/test/scxml_tests/optional/basic_http_event_processor/test531_test.exs b/test/scxml_tests/optional/basic_http_event_processor/test531_test.exs new file mode 100644 index 00000000..cc7148a1 --- /dev/null +++ b/test/scxml_tests/optional/basic_http_event_processor/test531_test.exs @@ -0,0 +1,66 @@ +# Generated from conformance/corpus/w3c.json, case w3c/test531, by +# tools/corpus/scxml_w3/cases.exs. Regenerate with `mise run corpus:emit`; +# never edit by hand. +# +# The document in this test is transformed for the predicator datamodel from +# 531/test531.txml of the W3C SCXML Implementation Report Plan test suite +# (https://www.w3.org/Voice/2013/scxml-irp/). +# +# Copyright © 2015 W3C® (MIT, ERCIM, Keio, Beihang), All Rights Reserved. +# +# Redistributed under BSD-3-Clause-W3C, the W3C 3-clause BSD License; its +# conditions and disclaimer are in conformance/LICENSES/BSD-3-Clause-W3C.txt. +defmodule SCXMLTest.BasicHttpEventProcessor.Test531 do + use Statifier.Case, async: true + alias Mix.Statifier.Corpus.HostCase + + @moduletag :scxml_w3 + @tag required_features: [ + :basic_states, + :event_transitions, + :final_states, + :log_elements, + :onentry_actions, + :send_delay_expressions, + :send_elements, + :send_param_elements, + :target_expressions, + :wildcard_events + ] + @tag conformance: "optional", spec: "BasicHTTPEventProcessor" + test "test531" do + xml = """ + + + + + + + + + + + + + + + + + + + + + + + + """ + + description = + "If a single instance of the parameter '_scxmleventname' is present, the SCXML Processor MUST use its value as the name of the SCXML event that it raises." + + HostCase.with_event_io_processors( + ["http://www.w3.org/TR/scxml/#BasicHTTPEventProcessor"], + fn send_types -> test_scxml(xml, description, ["pass"], [], send_types: send_types) end + ) + end +end diff --git a/test/scxml_tests/optional/basic_http_event_processor/test532_test.exs b/test/scxml_tests/optional/basic_http_event_processor/test532_test.exs new file mode 100644 index 00000000..388bd435 --- /dev/null +++ b/test/scxml_tests/optional/basic_http_event_processor/test532_test.exs @@ -0,0 +1,66 @@ +# Generated from conformance/corpus/w3c.json, case w3c/test532, by +# tools/corpus/scxml_w3/cases.exs. Regenerate with `mise run corpus:emit`; +# never edit by hand. +# +# The document in this test is transformed for the predicator datamodel from +# 532/test532.txml of the W3C SCXML Implementation Report Plan test suite +# (https://www.w3.org/Voice/2013/scxml-irp/). +# +# Copyright © 2015 W3C® (MIT, ERCIM, Keio, Beihang), All Rights Reserved. +# +# Redistributed under BSD-3-Clause-W3C, the W3C 3-clause BSD License; its +# conditions and disclaimer are in conformance/LICENSES/BSD-3-Clause-W3C.txt. +defmodule SCXMLTest.BasicHttpEventProcessor.Test532 do + use Statifier.Case, async: true + alias Mix.Statifier.Corpus.HostCase + + @moduletag :scxml_w3 + @tag required_features: [ + :basic_states, + :event_transitions, + :final_states, + :log_elements, + :onentry_actions, + :send_content_elements, + :send_delay_expressions, + :send_elements, + :target_expressions, + :wildcard_events + ] + @tag conformance: "optional", spec: "BasicHTTPEventProcessor" + test "test532" do + xml = """ + + + + + + + some content + + + + + + + + + + + + + + + + + """ + + description = + "If _scxmleventname is not present, the Processor MUST the name of the HTTP method that was used to deliver the event as name of the SCXML event that it raises" + + HostCase.with_event_io_processors( + ["http://www.w3.org/TR/scxml/#BasicHTTPEventProcessor"], + fn send_types -> test_scxml(xml, description, ["pass"], [], send_types: send_types) end + ) + end +end diff --git a/test/scxml_tests/optional/basic_http_event_processor/test534_test.exs b/test/scxml_tests/optional/basic_http_event_processor/test534_test.exs new file mode 100644 index 00000000..057b2464 --- /dev/null +++ b/test/scxml_tests/optional/basic_http_event_processor/test534_test.exs @@ -0,0 +1,63 @@ +# Generated from conformance/corpus/w3c.json, case w3c/test534, by +# tools/corpus/scxml_w3/cases.exs. Regenerate with `mise run corpus:emit`; +# never edit by hand. +# +# The document in this test is transformed for the predicator datamodel from +# 534/test534.txml of the W3C SCXML Implementation Report Plan test suite +# (https://www.w3.org/Voice/2013/scxml-irp/). +# +# Copyright © 2015 W3C® (MIT, ERCIM, Keio, Beihang), All Rights Reserved. +# +# Redistributed under BSD-3-Clause-W3C, the W3C 3-clause BSD License; its +# conditions and disclaimer are in conformance/LICENSES/BSD-3-Clause-W3C.txt. +defmodule SCXMLTest.BasicHttpEventProcessor.Test534 do + use Statifier.Case, async: true + alias Mix.Statifier.Corpus.HostCase + + @moduletag :scxml_w3 + @tag required_features: [ + :basic_states, + :event_transitions, + :final_states, + :log_elements, + :onentry_actions, + :send_delay_expressions, + :send_elements, + :target_expressions, + :wildcard_events + ] + @tag conformance: "optional", spec: "BasicHTTPEventProcessor" + test "test534" do + xml = """ + + + + + + + + + + + + + + + + + + + + + + """ + + description = + "If the 'event' parameter of send is defined, the SCXML Processor MUST use its value as the value of the HTTP POST parameter _scxmleventname" + + HostCase.with_event_io_processors( + ["http://www.w3.org/TR/scxml/#BasicHTTPEventProcessor"], + fn send_types -> test_scxml(xml, description, ["pass"], [], send_types: send_types) end + ) + end +end diff --git a/test/scxml_tests/optional/basic_http_event_processor/test567_test.exs b/test/scxml_tests/optional/basic_http_event_processor/test567_test.exs new file mode 100644 index 00000000..a35cd232 --- /dev/null +++ b/test/scxml_tests/optional/basic_http_event_processor/test567_test.exs @@ -0,0 +1,81 @@ +# Generated from conformance/corpus/w3c.json, case w3c/test567, by +# tools/corpus/scxml_w3/cases.exs. Regenerate with `mise run corpus:emit`; +# never edit by hand. +# +# The document in this test is transformed for the predicator datamodel from +# 567/test567.txml of the W3C SCXML Implementation Report Plan test suite +# (https://www.w3.org/Voice/2013/scxml-irp/). +# +# Copyright © 2015 W3C® (MIT, ERCIM, Keio, Beihang), All Rights Reserved. +# +# Redistributed under BSD-3-Clause-W3C, the W3C 3-clause BSD License; its +# conditions and disclaimer are in conformance/LICENSES/BSD-3-Clause-W3C.txt. +defmodule SCXMLTest.BasicHttpEventProcessor.Test567 do + use Statifier.Case, async: true + alias Mix.Statifier.Corpus.HostCase + + @moduletag :scxml_w3 + @tag required_features: [ + :assign_elements, + :basic_states, + :compound_states, + :conditional_transitions, + :data_elements, + :datamodel, + :event_transitions, + :eventless_transitions, + :final_states, + :log_elements, + :onentry_actions, + :send_delay_expressions, + :send_elements, + :send_param_elements, + :target_expressions, + :wildcard_events + ] + @tag conformance: "optional", spec: "BasicHTTPEventProcessor" + test "test567" do + xml = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + """ + + description = + "The processor MUST use any message content other than '_scxmleventname' to populate _event.data." + + HostCase.with_event_io_processors( + ["http://www.w3.org/TR/scxml/#BasicHTTPEventProcessor"], + fn send_types -> test_scxml(xml, description, ["pass"], [], send_types: send_types) end + ) + end +end diff --git a/test/scxml_tests/optional/basic_http_event_processor/test577_test.exs b/test/scxml_tests/optional/basic_http_event_processor/test577_test.exs new file mode 100644 index 00000000..0a9d0b88 --- /dev/null +++ b/test/scxml_tests/optional/basic_http_event_processor/test577_test.exs @@ -0,0 +1,61 @@ +# Generated from conformance/corpus/w3c.json, case w3c/test577, by +# tools/corpus/scxml_w3/cases.exs. Regenerate with `mise run corpus:emit`; +# never edit by hand. +# +# The document in this test is transformed for the predicator datamodel from +# 577/test577.txml of the W3C SCXML Implementation Report Plan test suite +# (https://www.w3.org/Voice/2013/scxml-irp/). +# +# Copyright © 2015 W3C® (MIT, ERCIM, Keio, Beihang), All Rights Reserved. +# +# Redistributed under BSD-3-Clause-W3C, the W3C 3-clause BSD License; its +# conditions and disclaimer are in conformance/LICENSES/BSD-3-Clause-W3C.txt. +defmodule SCXMLTest.BasicHttpEventProcessor.Test577 do + use Statifier.Case, async: true + alias Mix.Statifier.Corpus.HostCase + + @moduletag :scxml_w3 + @tag required_features: [ + :basic_states, + :event_transitions, + :final_states, + :log_elements, + :onentry_actions, + :send_elements, + :wildcard_events + ] + @tag conformance: "optional", spec: "BasicHTTPEventProcessor" + test "test577" do + xml = """ + + + + + + + + + + + + + + + + + + + + + + """ + + description = + "If neither the 'target' nor the 'targetexpr' attribute is specified, the SCXML Processor MUST add the event error.communication to the internal event queue of the sending session." + + HostCase.with_event_io_processors( + ["http://www.w3.org/TR/scxml/#BasicHTTPEventProcessor"], + fn send_types -> test_scxml(xml, description, ["pass"], [], send_types: send_types) end + ) + end +end diff --git a/test/scxml_tests/optional/send/test201_test.exs b/test/scxml_tests/optional/send/test201_test.exs new file mode 100644 index 00000000..87a6b588 --- /dev/null +++ b/test/scxml_tests/optional/send/test201_test.exs @@ -0,0 +1,62 @@ +# Generated from conformance/corpus/w3c.json, case w3c/test201, by +# tools/corpus/scxml_w3/cases.exs. Regenerate with `mise run corpus:emit`; +# never edit by hand. +# +# The document in this test is transformed for the predicator datamodel from +# 201/test201.txml of the W3C SCXML Implementation Report Plan test suite +# (https://www.w3.org/Voice/2013/scxml-irp/). +# +# Copyright © 2015 W3C® (MIT, ERCIM, Keio, Beihang), All Rights Reserved. +# +# Redistributed under BSD-3-Clause-W3C, the W3C 3-clause BSD License; its +# conditions and disclaimer are in conformance/LICENSES/BSD-3-Clause-W3C.txt. +defmodule SCXMLTest.Send.Test201 do + use Statifier.Case, async: true + alias Mix.Statifier.Corpus.HostCase + + @moduletag :scxml_w3 + @tag required_features: [ + :basic_states, + :event_transitions, + :final_states, + :log_elements, + :onentry_actions, + :send_elements, + :target_expressions, + :wildcard_events + ] + @tag conformance: "optional", spec: "send" + test "test201" do + xml = """ + + + + + + + + + + + + + + + + + + + + + + """ + + description = + "Processors that support HTTP POST must use the value http://www.w3.org/TR/scxml/#BasicHTTPEventProcessor for the \"type\" attribute" + + HostCase.with_event_io_processors( + ["http://www.w3.org/TR/scxml/#BasicHTTPEventProcessor"], + fn send_types -> test_scxml(xml, description, ["pass"], [], send_types: send_types) end + ) + end +end diff --git a/tools/corpus/README.md b/tools/corpus/README.md index 9621842e..9892ee10 100644 --- a/tools/corpus/README.md +++ b/tools/corpus/README.md @@ -126,7 +126,10 @@ Fetch and transform pull 198 W3C documents and 316 SCION cases (127 native + the `SCXMLTest.
.`, `use Statifier.Case`, `@moduletag :scxml_w3`, `@tag required_features: [...]` read from the corpus case, inline XML heredoc (4-space base indent, the corpus case's source: pretty-printed from the transformed -`.scxml`, comments stripped), and a single `test_scxml/4` call. `use +`.scxml`, comments stripped), and a single `test_scxml/4` call; for a case +whose `host` names an Event I/O Processor, the call is `test_scxml/5` inside +`Mix.Statifier.Corpus.HostCase.with_event_io_processors/2`, which registers +the processor behind a loopback front for the test. `use Statifier.Case` and `Statifier.FeatureDetector` in generated output are the `test/support` compatibility shims over `Statifier.Testing.Case` and `Statifier.Testing.FeatureDetector`, the real modules promoted into `lib/` @@ -134,8 +137,8 @@ Statifier.Case` and `Statifier.FeatureDetector` in generated output are the files need no regeneration on the promoting branch. Adopting the new names in generated output is a future regeneration's call, not something owed here. Of the 198 downloaded W3C documents, 5 are dependency documents an `` loads at -runtime rather than conformance cases, leaving 193 cases; 156 of those emit -(154 mandatory + 2 optional), and the rest are filtered out (see below). +runtime rather than conformance cases, leaving 193 cases; 168 of those emit +(154 mandatory + 14 optional), and the rest are filtered out (see below). `test/scxml_tests/` is populated. The **SCION generator** produces `SCIONTest..Test`, @@ -163,7 +166,7 @@ path-shape invariant directly so that class of drift fails a gate instead of waiting for a case-sensitive filesystem to surface it. `mix test.regression` and `mix test.baseline` report per-corpus coverage -against these emitted counts (119 SCION, 156 W3C), not the upstream suite +against these emitted counts (119 SCION, 168 W3C), not the upstream suite sizes above - see `docs/testing.md`'s regression ratchet section - so an edit to either exclusions file that changes what emits also changes what those tasks report as the denominator. `test/corpus/readme_counts_test.exs` pins @@ -180,9 +183,15 @@ Three filters apply before a W3C case is emitted, all applied by untouched, so they keep their original datamodel and are out of scope for the predicator commitment (docs/datamodel.md). - **exclusions.exs**: tests with no predicator equivalent (script, list - concatenation, string prefix, and the BasicHTTP Event I/O Processor tree), - and tests that pass only when an `` is resolved, which the - library never does (ADR-0038), recorded with a reason atom per ADR-0004. + concatenation, string prefix), and tests that pass only when an + `` is resolved, which the library never does (ADR-0038), + recorded with a reason atom per ADR-0004. The Basic HTTP Event I/O + Processor's documents are not excluded: each is emitted with a `host` + object naming the processor in `event_io_processors`, and its generated + module registers the processor behind a loopback front + (`Mix.Statifier.Corpus.HostCase`, ADR-0075). `test201` among them is + emitted and left out of the ratchet, for the reason ADR-0075 decision 7 + gives. - **sub_documents.exs**: manifest `` documents an `` loads at runtime rather than a `` document run as its own conformance test. This is a different category from `exclusions.exs`: an exclusion is a test diff --git a/tools/corpus/scxml_w3/cases.exs b/tools/corpus/scxml_w3/cases.exs index 27031cbd..ba59ad80 100644 --- a/tools/corpus/scxml_w3/cases.exs +++ b/tools/corpus/scxml_w3/cases.exs @@ -21,7 +21,9 @@ # disclaimer under conformance/LICENSES/. The document itself goes into the # heredoc exactly as the corpus holds it - already transformed for the # predicator datamodel and formatted - and `required_features` is the case's -# own field. +# own field. A case whose host runs an Event I/O Processor calls test_scxml/5 +# inside Mix.Statifier.Corpus.HostCase.with_event_io_processors/2, which +# registers the processor and starts its loopback front for the test. # # Plain `elixir`, not `mix run`: nothing here needs the project compiled. @@ -100,7 +102,7 @@ defmodule Cases.Emit do #{header(corpus_case, upstream, copyright)} defmodule #{inspect(module)} do use Statifier.Case, async: true - + #{aliases(corpus_case)} @moduletag :scxml_w3 @tag required_features: [#{features}] @tag conformance: #{inspect(conformance)}, spec: #{inspect(spec)} @@ -110,7 +112,7 @@ defmodule Cases.Emit do description = #{inspect(description)} - test_scxml(xml, description, #{inspect(conf)}, #{inspect(events)}) + #{run_call(corpus_case, conf, events)} end end """ @@ -120,6 +122,23 @@ defmodule Cases.Emit do File.write!(out, Code.format_string!(source) |> IO.iodata_to_binary() |> Kernel.<>("\n")) end + # A case whose host runs an Event I/O Processor (ADR-0075 decision 7) is + # run with that processor registered, delivering through a loopback front + # started for the test (Mix.Statifier.Corpus.HostCase, which the module + # aliases); every other case is the one test_scxml/4 call. + defp run_call(%{"host" => %{"event_io_processors" => uris}}, conf, events) do + "HostCase.with_event_io_processors(#{inspect(uris)}, fn send_types -> " <> + "test_scxml(xml, description, #{inspect(conf)}, #{inspect(events)}, send_types: send_types) end)" + end + + defp run_call(_corpus_case, conf, events), + do: "test_scxml(xml, description, #{inspect(conf)}, #{inspect(events)})" + + defp aliases(%{"host" => %{"event_io_processors" => _uris}}), + do: "alias Mix.Statifier.Corpus.HostCase\n" + + defp aliases(_corpus_case), do: "" + defp header(%{"id" => id, "upstream" => case_upstream}, upstream, copyright) do %{"document" => document, "license" => license, "notice" => notice} = case_upstream diff --git a/tools/corpus/scxml_w3/conf_predicator.xsl b/tools/corpus/scxml_w3/conf_predicator.xsl index 8e3bd89c..a0a0211e 100644 --- a/tools/corpus/scxml_w3/conf_predicator.xsl +++ b/tools/corpus/scxml_w3/conf_predicator.xsl @@ -630,9 +630,14 @@ Stubbed: multidimensional array comparison has no predicator equivalent. --> +The Basic HTTP Event I/O Processor is Statifier.Send.BasicHTTP (ADR-0075), +which a host registers; a corpus case that names it declares +host.event_io_processors, and the corpus host runs it behind a loopback +front. conf:basicHTTPAccessURITarget emits the IRP's own spelling of the +access URI, the short _ioprocessors key's location. Every other template +below is stubbed rather than emitting the regex-based ECMAScript forms the +upstream stylesheet used, so the checks they carried on the raw message +are not made. --> @@ -644,6 +649,7 @@ regex-based ECMAScript forms the upstream stylesheet used. --> + _ioprocessors['basichttp']['location'] diff --git a/tools/corpus/scxml_w3/exclusions.exs b/tools/corpus/scxml_w3/exclusions.exs index 29f6e383..25055890 100644 --- a/tools/corpus/scxml_w3/exclusions.exs +++ b/tools/corpus/scxml_w3/exclusions.exs @@ -2,7 +2,6 @@ # # Reasons: # :needs_predicator_feature - blocked on an upstream predicator capability -# :needs_basichttp - BasicHTTP Event I/O Processor, out of scope # :needs_invoke_src - the case passes only when is # resolved to a document; the library never # dereferences src (ADR-0038) and the corpus @@ -23,9 +22,6 @@ # names, not on an exclusion here. %{ - "test201" => - {:needs_basichttp, - "BasicHTTP Event I/O Processor as a : the event is delivered only by a processor that implements HTTP POST"}, "test216" => {:needs_invoke_src, " evaluated as src: the child is loaded from src, which is never dereferenced (ADR-0038)"}, @@ -40,22 +36,5 @@ " and treated identically: the src child is never loaded, as src is never dereferenced (ADR-0038)"}, "test276" => {:needs_invoke_src, - "top-level values supplied at instantiation: the child is loaded from src, which is never dereferenced (ADR-0038)"}, - "test509" => {:needs_basichttp, "BasicHTTP Event I/O Processor MUST accept POST requests"}, - "test510" => - {:needs_basichttp, "BasicHTTP Event I/O Processor MUST validate and enqueue the message"}, - "test518" => - {:needs_basichttp, "BasicHTTP Event I/O Processor namelist -> POST parameter mapping"}, - "test519" => - {:needs_basichttp, "BasicHTTP Event I/O Processor param children -> POST parameter mapping"}, - "test520" => {:needs_basichttp, "BasicHTTP Event I/O Processor content child -> message body"}, - "test522" => - {:needs_basichttp, "BasicHTTP Event I/O Processor _ioprocessors['basichttp'] entry"}, - "test531" => {:needs_basichttp, "BasicHTTP Event I/O Processor _scxmleventname -> event name"}, - "test532" => - {:needs_basichttp, "BasicHTTP Event I/O Processor HTTP method -> event name fallback"}, - "test534" => {:needs_basichttp, "BasicHTTP Event I/O Processor send/@event -> _scxmleventname"}, - "test567" => {:needs_basichttp, "BasicHTTP Event I/O Processor message content -> _event.data"}, - "test577" => - {:needs_basichttp, "BasicHTTP Event I/O Processor missing target -> error.communication"} + "top-level values supplied at instantiation: the child is loaded from src, which is never dereferenced (ADR-0038)"} } From cf0d3896f6a84dcc573556f362be6e51838ef712 Mon Sep 17 00:00:00 2001 From: JohnnyT Date: Wed, 30 Sep 2026 09:20:19 -0600 Subject: [PATCH 2/2] Documents the Basic HTTP dedup key header docs/basichttp.md now says what a host needs from ADR-0075's Amendment: every POST carries the send's dedup key in the scxml-send-key header, delivery is at-least-once, a front dedupes on the header's value and answers a malformed one 400, and the default adapter answers an error when :ssl or :public_key cannot be loaded. Refs: st-8kn8 --- docs/basichttp.md | 66 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 57 insertions(+), 9 deletions(-) diff --git a/docs/basichttp.md b/docs/basichttp.md index ae12714a..4728d0c8 100644 --- a/docs/basichttp.md +++ b/docs/basichttp.md @@ -11,7 +11,9 @@ made, nothing is started, and `_ioprocessors` holds the SCXML processor's entry alone. The decisions behind the processor are -[ADR-0075](https://github.com/riddler/statifier-ex/blob/main/docs/adr/0075-basichttp-event-io-processor.md). +[ADR-0075](https://github.com/riddler/statifier-ex/blob/main/docs/adr/0075-basichttp-event-io-processor.md) +and its Amendment of 2026-09-30, which adds the `scxml-send-key` header +below. ## Registering it @@ -41,7 +43,11 @@ needs the options form. Its options: `Statifier.Send.BasicHTTP.Transport.Httpc`, on OTP's `:httpc`, which verifies TLS peers against the system CA store and bounds each request with a timeout. The package adds no dependency for it; the last section - below shows an adapter on `req`. + below shows an adapter on `req`. When `:ssl` or `:public_key` cannot be + loaded, the default adapter answers + `{:error, {:not_loadable, module, reason}}` instead of making the + request, and the processor reports the miss to the sender like any + other failed delivery. Neither type string is a built-in spelling, so the registration redirects no built-in send. @@ -71,18 +77,22 @@ at the base URL, and for each request it: running session; 2. hands the request to `Statifier.Send.BasicHTTP.decode/1`, a pure function that knows no session; -3. enqueues the event it answers as an external event, for example with +3. unless it has already enqueued a request carrying the same + `scxml-send-key` header value (next section), enqueues the event the + decoder answers as an external event, for example with `Statifier.Session.send_event/2`; 4. answers by the status rule below. `decode/1` takes a map of the request's `:method`, `:content_type` (`nil` when absent), `:body` and `:query` (`nil` when the URL has none), and -answers `{:ok, event}` or `{:error, reason}`. The status rule: +optionally `:send_key`, the `scxml-send-key` header's value. It answers +`{:ok, event}` or `{:error, reason}`. The status rule: | `decode/1` answers | The front answers | |---|---| -| `{:ok, event}` | 204, once the event is enqueued and before it is processed | +| `{:ok, event}` | 204, once the event is enqueued and before it is processed; 204 again, with nothing enqueued, for a `scxml-send-key` value the front has already enqueued | | `{:error, {:method_not_allowed, method}}` | 405, with `Allow: POST` | +| `{:error, {:malformed_send_key, value}}` | 400 | | any other `{:error, _}` | 400 | | (the path names no session the front can reach) | 404 | @@ -97,12 +107,16 @@ def call(%Plug.Conn{path_info: [session_id]} = conn, _opts) do method: conn.method, content_type: conn |> Plug.Conn.get_req_header("content-type") |> List.first(), body: body, - query: if(conn.query_string == "", do: nil, else: conn.query_string) + query: if(conn.query_string == "", do: nil, else: conn.query_string), + send_key: conn |> Plug.Conn.get_req_header("scxml-send-key") |> List.first() } with {:ok, pid} <- MyApp.Sessions.whereis(session_id), {:ok, event} <- Statifier.Send.BasicHTTP.decode(request) do - :ok = Statifier.Session.send_event(pid, event) + # MyApp.Delivered records each key once and answers whether it was new. + if MyApp.Delivered.first?(session_id, request.send_key), + do: :ok = Statifier.Session.send_event(pid, event) + Plug.Conn.send_resp(conn, 204, "") else :no_session -> Plug.Conn.send_resp(conn, 404, "") @@ -116,7 +130,40 @@ end This repository's own loopback front, which its conformance runs deliver through, is `Mix.Statifier.BasicHTTPFront`: repository tooling on OTP's `:inets` httpd, not part of the package, and a small model of the steps -above. +above. It lives only as long as one test run and does not deduplicate. + +## At-least-once delivery and the `scxml-send-key` header + +The processor makes one POST each time an instruction is performed, and +keeps no memory between performs. A host that performs the same +instruction twice - after a crash and a retry, for example - POSTs +twice, so delivery is at-least-once. To let the receiver deliver each +send once, every POST the processor makes, immediate or delayed and +whatever its body, carries the send's deduplication key in a request +header: + +- **Name:** `scxml-send-key`. +- **Value:** eight fields joined by `/`: the session scope (the + sender's `_sessionid` for a live session), the send id, `macrostep`, + `microstep`, `round`, `c_index`, `owner` and `ordinal`. The session + scope and the send id are percent-encoded, so neither carries a `/`; + the counters are decimal integers; `owner` is spelled `onentry.S.B`, + `onexit.S.B`, `finalize.S.B` or `transition.T` with its indexes. A + field the send does not carry is the empty string. + +Every field is a deterministic counter or a static position, so a +re-performed send carries a byte-identical value. A front that enqueues +a request only when it has not already enqueued one with the same value +delivers each send once; a front that ignores the header sees +at-least-once delivery. The decoder does not deduplicate: it is pure and +remembers nothing, so the front keeps the record of the values it has +enqueued, and answers a repeat 204 with nothing enqueued. + +`decode/1` only checks the value's shape. It sets no field of the event +from it, and a value that is not eight `/`-separated fields whose second +field percent-decodes to UTF-8 is +`{:error, {:malformed_send_key, value}}`, which the front answers 400. A +request without the header decodes as before. ## The mapping, both ways @@ -135,7 +182,8 @@ A parameter value is written as text: a string as it is, a number or a boolean as its literal, `nil` as `null`, and an undefined value as the empty string. The processor makes one attempt. A transport error, or a status outside 2xx, reaches the sender as `error.communication` carrying -the send id, through `Statifier.Session.failed_send/3`. A `` is +the send id, through `Statifier.Session.failed_send/3`. Every request also +carries the `scxml-send-key` header above. A `` is held by the processor's own timer, and a `` naming the send cancels it while it has not fired.