From 068b890db24f33f4e9122098fee75cffb270bc41 Mon Sep 17 00:00:00 2001 From: JohnnyT Date: Sun, 7 Sep 2025 14:47:22 -0600 Subject: [PATCH 1/3] Fixes multi-state initial configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes initial field from String.t() | nil to [String.t()] to support space-separated initial states per W3C SCXML specification. Adds parsing logic to split "state1 state2" syntax into ["state1", "state2"] list. Updates interpreter, validator, and test files to handle list format. Fixes parser bug where space-separated states were treated as single IDs. Test coverage: W3C tests test576 and test413 now pass, all 1028 internal tests pass. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- lib/statifier/document.ex | 2 +- lib/statifier/interpreter.ex | 63 ++++++----- lib/statifier/parser/scxml/element_builder.ex | 26 +++-- lib/statifier/state.ex | 2 +- .../validator/initial_state_validator.ex | 101 ++++++++++++------ test/passing_tests.json | 6 +- test/statifier/actions/invoke_action_test.exs | 2 +- test/statifier/actions/send_action_test.exs | 2 +- .../history/history_resolution_test.exs | 4 +- .../interpreter_history_simple_test.exs | 4 +- .../logging_configuration_test.exs | 4 +- test/statifier/interpreter_coverage_test.exs | 12 +-- .../parser/scxml/final_state_test.exs | 4 +- test/statifier/parser/scxml_test.exs | 12 +-- test/statifier/state_chart_test.exs | 2 +- test/statifier/validator/edge_cases_test.exs | 10 +- .../history_state_validator_test.exs | 14 +-- test/statifier_test.exs | 10 +- 18 files changed, 171 insertions(+), 109 deletions(-) diff --git a/lib/statifier/document.ex b/lib/statifier/document.ex index 6210cf6..b3ccab4 100644 --- a/lib/statifier/document.ex +++ b/lib/statifier/document.ex @@ -30,7 +30,7 @@ defmodule Statifier.Document do @type t :: %__MODULE__{ name: String.t() | nil, - initial: String.t() | nil, + initial: [String.t()], datamodel: String.t() | nil, version: String.t() | nil, xmlns: String.t() | nil, diff --git a/lib/statifier/interpreter.ex b/lib/statifier/interpreter.ex index 7f38484..5beb947 100644 --- a/lib/statifier/interpreter.ex +++ b/lib/statifier/interpreter.ex @@ -240,6 +240,8 @@ defmodule Statifier.Interpreter do end end + defp get_initial_configuration(%Document{initial: [], states: []}), do: %Configuration{} + defp get_initial_configuration(%Document{initial: nil, states: []}), do: %Configuration{} defp get_initial_configuration( @@ -249,16 +251,21 @@ defmodule Statifier.Interpreter do Configuration.new(initial_states) end - defp get_initial_configuration(%Document{initial: initial_id} = document) do - case Document.find_state(document, initial_id) do - # Invalid initial state - nil -> - %Configuration{} + defp get_initial_configuration(%Document{initial: [], states: [first_state | _rest]} = document) do + initial_states = enter_state(first_state, document) + Configuration.new(initial_states) + end - state -> - initial_states = enter_state(state, document) - Configuration.new(initial_states) - end + defp get_initial_configuration(%Document{initial: initial_ids} = document) + when length(initial_ids) > 0 do + initial_states = + initial_ids + |> Enum.map(&Document.find_state(document, &1)) + # Remove any invalid states + |> Enum.filter(&(&1 != nil)) + |> Enum.flat_map(&enter_state(&1, document)) + + Configuration.new(initial_states) end defp enter_state(%State{} = state, %Document{} = document), @@ -283,17 +290,14 @@ defmodule Statifier.Interpreter do end defp enter_state( - %State{type: :compound, states: child_states, initial: initial_id}, + %State{type: :compound, states: child_states, initial: initial_ids}, %StateChart{} = state_chart ) do - # Compound state - find and enter initial child (don't add compound state to active set) - initial_child = get_initial_child_state(initial_id, child_states) + # Compound state - find and enter initial children (don't add compound state to active set) + initial_children = get_initial_child_states(initial_ids, child_states) - case initial_child do - # No valid child - compound state with no children is not active - nil -> [] - child -> enter_state(child, state_chart) - end + initial_children + |> Enum.flat_map(&enter_state(&1, state_chart)) end defp enter_state( @@ -330,8 +334,23 @@ defmodule Statifier.Interpreter do end end - # Get the initial child state for a compound state - defp get_initial_child_state(nil, child_states) do + # Get the initial child states for a compound state (handles multiple initial states) + defp get_initial_child_states([], child_states) do + # No initial attribute - check for element first or use first child + case get_initial_child_state_legacy([], child_states) do + nil -> [] + child -> [child] + end + end + + defp get_initial_child_states(initial_ids, child_states) when is_list(initial_ids) do + initial_ids + |> Enum.map(&find_child_by_id(child_states, &1)) + |> Enum.filter(&(&1 != nil)) + end + + # Legacy function for backward compatibility (single child) + defp get_initial_child_state_legacy([], child_states) do # No initial attribute - check for element first case find_initial_element(child_states) do %State{type: :initial, transitions: [transition | _rest]} -> @@ -357,12 +376,6 @@ defmodule Statifier.Interpreter do end end - defp get_initial_child_state(initial_id, child_states) when is_binary(initial_id) do - Enum.find(child_states, &(&1.id == initial_id)) - end - - defp get_initial_child_state(_initial_id, []), do: nil - # Find the initial element among child states defp find_initial_element(child_states) do Enum.find(child_states, &(&1.type == :initial)) diff --git a/lib/statifier/parser/scxml/element_builder.ex b/lib/statifier/parser/scxml/element_builder.ex index 39e2d2a..b2ef60f 100644 --- a/lib/statifier/parser/scxml/element_builder.ex +++ b/lib/statifier/parser/scxml/element_builder.ex @@ -41,7 +41,7 @@ defmodule Statifier.Parser.SCXML.ElementBuilder do %Statifier.Document{ name: get_attr_value(attrs_map, "name"), - initial: get_attr_value(attrs_map, "initial"), + initial: parse_initial_attribute(get_attr_value(attrs_map, "initial")), datamodel: get_attr_value(attrs_map, "datamodel"), version: get_attr_value(attrs_map, "version"), xmlns: get_attr_value(attrs_map, "xmlns"), @@ -73,7 +73,7 @@ defmodule Statifier.Parser.SCXML.ElementBuilder do %Statifier.State{ id: get_attr_value(attrs_map, "id"), - initial: get_attr_value(attrs_map, "initial"), + initial: parse_initial_attribute(get_attr_value(attrs_map, "initial")), # Will be updated later based on children and structure type: :atomic, states: [], @@ -100,7 +100,7 @@ defmodule Statifier.Parser.SCXML.ElementBuilder do %Statifier.State{ id: get_attr_value(attrs_map, "id"), # Parallel states don't have initial attributes - initial: nil, + initial: [], # Set type directly during parsing type: :parallel, states: [], @@ -128,7 +128,7 @@ defmodule Statifier.Parser.SCXML.ElementBuilder do %Statifier.State{ id: get_attr_value(attrs_map, "id"), # Final states don't have initial attributes - initial: nil, + initial: [], # Set type directly during parsing type: :final, states: [], @@ -155,7 +155,7 @@ defmodule Statifier.Parser.SCXML.ElementBuilder do %Statifier.State{ # Initial states generate unique IDs since they don't have explicit IDs id: generate_initial_id(element_counts), - initial: nil, + initial: [], type: :initial, states: [], transitions: [], @@ -196,7 +196,7 @@ defmodule Statifier.Parser.SCXML.ElementBuilder do %Statifier.State{ id: get_attr_value(attrs_map, "id"), - initial: nil, + initial: [], type: :history, history_type: history_type, states: [], @@ -516,4 +516,18 @@ defmodule Statifier.Parser.SCXML.ElementBuilder do initial_count = Map.get(element_counts, "initial", 1) "__initial_#{initial_count}__" end + + # Parse the initial attribute which can be space-separated state IDs. + # + # Returns a list of state IDs. If the attribute is nil or empty, returns an empty list. + # If it contains space-separated values, splits them and returns the list. + defp parse_initial_attribute(nil), do: [] + defp parse_initial_attribute(""), do: [] + + defp parse_initial_attribute(initial_string) when is_binary(initial_string) do + initial_string + |> String.split() + |> Enum.map(&String.trim/1) + |> Enum.reject(&(&1 == "")) + end end diff --git a/lib/statifier/state.ex b/lib/statifier/state.ex index eef949e..303337f 100644 --- a/lib/statifier/state.ex +++ b/lib/statifier/state.ex @@ -32,7 +32,7 @@ defmodule Statifier.State do @type t :: %__MODULE__{ id: String.t(), - initial: String.t() | nil, + initial: [String.t()], type: state_type(), states: [Statifier.State.t()], transitions: [Statifier.Transition.t()], diff --git a/lib/statifier/validator/initial_state_validator.ex b/lib/statifier/validator/initial_state_validator.ex index 2e5d36d..de7db00 100644 --- a/lib/statifier/validator/initial_state_validator.ex +++ b/lib/statifier/validator/initial_state_validator.ex @@ -13,20 +13,29 @@ defmodule Statifier.Validator.InitialStateValidator do """ @spec validate_initial_state(Statifier.Validator.validation_result(), Statifier.Document.t()) :: Statifier.Validator.validation_result() - def validate_initial_state(%Statifier.Validator{} = result, %Statifier.Document{initial: nil}) do + def validate_initial_state(%Statifier.Validator{} = result, %Statifier.Document{initial: []}) do # No initial state specified - this is valid, first state becomes initial result end + def validate_initial_state(%Statifier.Validator{} = result, %Statifier.Document{initial: nil}) do + # Backward compatibility: nil initial state + result + end + def validate_initial_state( %Statifier.Validator{} = result, - %Statifier.Document{initial: initial} = document - ) do - if Utils.state_exists?(initial, document) do - result - else - Utils.add_error(result, "Initial state '#{initial}' does not exist") - end + %Statifier.Document{initial: initial_ids} = document + ) + when is_list(initial_ids) do + # Validate each initial state ID + Enum.reduce(initial_ids, result, fn initial_id, acc -> + if Utils.state_exists?(initial_id, document) do + acc + else + Utils.add_error(acc, "Initial state '#{initial_id}' does not exist") + end + end) end @doc """ @@ -56,28 +65,38 @@ defmodule Statifier.Validator.InitialStateValidator do Statifier.Document.t() ) :: Statifier.Validator.validation_result() + def validate_initial_state_hierarchy(%Statifier.Validator{} = result, %Statifier.Document{ + initial: [] + }) do + result + end + def validate_initial_state_hierarchy(%Statifier.Validator{} = result, %Statifier.Document{ initial: nil }) do + # Backward compatibility: nil initial state result end def validate_initial_state_hierarchy( %Statifier.Validator{} = result, - %Statifier.Document{initial: initial_id} = document - ) do - # Check if initial state is a direct child of the document (top-level) - if Enum.any?(document.states, &(&1.id == initial_id)) do - result - else - Utils.add_warning(result, "Document initial state '#{initial_id}' is not a top-level state") - end + %Statifier.Document{initial: initial_ids} = document + ) + when is_list(initial_ids) do + # Check if all initial states are direct children of the document (top-level) + Enum.reduce(initial_ids, result, fn initial_id, acc -> + if Enum.any?(document.states, &(&1.id == initial_id)) do + acc + else + Utils.add_warning(acc, "Document initial state '#{initial_id}' is not a top-level state") + end + end) end # Validate that compound states with initial attributes reference valid child states defp validate_compound_state_initial( %Statifier.Validator{} = result, - %Statifier.State{initial: nil} = state, + %Statifier.State{initial: []} = state, _document ) do # No initial attribute - check for initial element validation @@ -86,14 +105,24 @@ defmodule Statifier.Validator.InitialStateValidator do defp validate_compound_state_initial( %Statifier.Validator{} = result, - %Statifier.State{initial: initial_id} = state, + %Statifier.State{initial: nil} = state, _document ) do + # Backward compatibility: nil initial state + validate_initial_element(result, state) + end + + defp validate_compound_state_initial( + %Statifier.Validator{} = result, + %Statifier.State{initial: initial_ids} = state, + _document + ) + when is_list(initial_ids) and length(initial_ids) > 0 do result # First check if state has both initial attribute and initial element (invalid) |> validate_no_conflicting_initial_specs(state) - # Then validate the initial attribute reference - |> validate_initial_attribute_reference(state, initial_id) + # Then validate each initial attribute reference + |> validate_initial_attribute_references(state, initial_ids) end # Validate that a state doesn't have both initial attribute and initial element @@ -102,8 +131,9 @@ defmodule Statifier.Validator.InitialStateValidator do %Statifier.State{initial: initial_attr} = state ) do has_initial_element = Enum.any?(state.states, &(&1.type == :initial)) + has_initial_attr = is_list(initial_attr) and length(initial_attr) > 0 - if initial_attr && has_initial_element do + if has_initial_attr and has_initial_element do Utils.add_error( result, "State '#{state.id}' cannot have both initial attribute and initial element - use one or the other" @@ -113,21 +143,24 @@ defmodule Statifier.Validator.InitialStateValidator do end end - # Validate the initial attribute reference - defp validate_initial_attribute_reference( + # Validate multiple initial attribute references + defp validate_initial_attribute_references( %Statifier.Validator{} = result, %Statifier.State{} = state, - initial_id - ) do - # Check if the initial state is a direct child of this compound state - if Enum.any?(state.states, &(&1.id == initial_id)) do - result - else - Utils.add_error( - result, - "State '#{state.id}' specifies initial='#{initial_id}' but '#{initial_id}' is not a direct child" - ) - end + initial_ids + ) + when is_list(initial_ids) do + # Check each initial state is a direct child of this compound state + Enum.reduce(initial_ids, result, fn initial_id, acc -> + if Enum.any?(state.states, &(&1.id == initial_id)) do + acc + else + Utils.add_error( + acc, + "State '#{state.id}' specifies initial='#{initial_id}' but '#{initial_id}' is not a direct child" + ) + end + end) end # Validate initial element constraints diff --git a/test/passing_tests.json b/test/passing_tests.json index d400f76..26e75af 100644 --- a/test/passing_tests.json +++ b/test/passing_tests.json @@ -5,7 +5,7 @@ "test/statifier/**/*_test.exs", "test/mix/**/*_test.exs" ], - "last_updated": "2025-09-02", + "last_updated": "2025-09-07", "scion_tests": [ "test/scion_tests/actionSend/send1_test.exs", "test/scion_tests/actionSend/send2_test.exs", @@ -93,6 +93,7 @@ "test/scxml_tests/mandatory/SelectingTransitions/test403a_test.exs", "test/scxml_tests/mandatory/SelectingTransitions/test407_test.exs", "test/scxml_tests/mandatory/SelectingTransitions/test411_test.exs", + "test/scxml_tests/mandatory/SelectingTransitions/test413_test.exs", "test/scxml_tests/mandatory/SelectingTransitions/test419_test.exs", "test/scxml_tests/mandatory/SelectingTransitions/test503_test.exs", "test/scxml_tests/mandatory/data/test280_test.exs", @@ -108,6 +109,7 @@ "test/scxml_tests/mandatory/onexit/test377_test.exs", "test/scxml_tests/mandatory/onexit/test378_test.exs", "test/scxml_tests/mandatory/raise/test144_test.exs", - "test/scxml_tests/mandatory/scxml/test355_test.exs" + "test/scxml_tests/mandatory/scxml/test355_test.exs", + "test/scxml_tests/mandatory/scxml/test576_test.exs" ] } \ No newline at end of file diff --git a/test/statifier/actions/invoke_action_test.exs b/test/statifier/actions/invoke_action_test.exs index 4af7d7f..2cfa76c 100644 --- a/test/statifier/actions/invoke_action_test.exs +++ b/test/statifier/actions/invoke_action_test.exs @@ -8,7 +8,7 @@ defmodule Statifier.Actions.InvokeActionTest do defp create_test_state_chart(datamodel \\ %{}, invoke_handlers \\ %{}) do document = %Statifier.Document{ name: nil, - initial: "test", + initial: ["test"], states: [], state_lookup: %{}, transitions_by_source: %{} diff --git a/test/statifier/actions/send_action_test.exs b/test/statifier/actions/send_action_test.exs index 750f9e7..8c0b3b3 100644 --- a/test/statifier/actions/send_action_test.exs +++ b/test/statifier/actions/send_action_test.exs @@ -8,7 +8,7 @@ defmodule Statifier.Actions.SendActionTest do defp create_test_state_chart(datamodel \\ %{}) do document = %Statifier.Document{ name: nil, - initial: "test", + initial: ["test"], states: [], state_lookup: %{}, transitions_by_source: %{} diff --git a/test/statifier/history/history_resolution_test.exs b/test/statifier/history/history_resolution_test.exs index 54ed9d2..bfef376 100644 --- a/test/statifier/history/history_resolution_test.exs +++ b/test/statifier/history/history_resolution_test.exs @@ -24,14 +24,14 @@ defmodule Statifier.HistoryResolutionTest do %State{ id: "parent", type: :compound, - initial: "child1", + initial: ["child1"], states: [ %State{id: "child1", type: :atomic, parent: "parent"}, %State{id: "child2", type: :atomic, parent: "parent"}, %State{ id: "nested", type: :compound, - initial: "grandchild1", + initial: ["grandchild1"], parent: "parent", states: [ %State{id: "grandchild1", type: :atomic, parent: "nested"}, diff --git a/test/statifier/history/interpreter_history_simple_test.exs b/test/statifier/history/interpreter_history_simple_test.exs index 40715f1..4319d6e 100644 --- a/test/statifier/history/interpreter_history_simple_test.exs +++ b/test/statifier/history/interpreter_history_simple_test.exs @@ -12,7 +12,7 @@ defmodule Statifier.InterpreterHistorySimpleTest do %State{ id: "parent", type: :compound, - initial: "child1", + initial: ["child1"], states: [ %State{id: "child1", type: :atomic, parent: "parent"}, %State{id: "child2", type: :atomic, parent: "parent"}, @@ -62,7 +62,7 @@ defmodule Statifier.InterpreterHistorySimpleTest do %State{ id: "simple_parent", type: :compound, - initial: "child1", + initial: ["child1"], states: [ %State{id: "child1", type: :atomic, parent: "simple_parent"}, %State{id: "child2", type: :atomic, parent: "simple_parent"} diff --git a/test/statifier/interpreter/logging_configuration_test.exs b/test/statifier/interpreter/logging_configuration_test.exs index 1a5c19f..d37caf7 100644 --- a/test/statifier/interpreter/logging_configuration_test.exs +++ b/test/statifier/interpreter/logging_configuration_test.exs @@ -7,9 +7,9 @@ defmodule Statifier.Interpreter.LoggingConfigurationTest do # Simple test document for initialization tests @test_document %Document{ name: "test", - initial: "idle", + initial: ["idle"], states: [ - %Statifier.State{id: "idle", initial: nil, parent: nil, states: [], transitions: []} + %Statifier.State{id: "idle", initial: [], parent: nil, states: [], transitions: []} ], state_lookup: %{"idle" => %Statifier.State{id: "idle"}}, transitions_by_source: %{}, diff --git a/test/statifier/interpreter_coverage_test.exs b/test/statifier/interpreter_coverage_test.exs index c7700c2..37c19fd 100644 --- a/test/statifier/interpreter_coverage_test.exs +++ b/test/statifier/interpreter_coverage_test.exs @@ -31,7 +31,7 @@ defmodule Statifier.InterpreterCoverageTest do document = %Document{ states: [state1], - initial: "state1", + initial: ["state1"], state_lookup: %{"state1" => state1}, transitions_by_source: %{"state1" => []} } @@ -308,7 +308,7 @@ defmodule Statifier.InterpreterCoverageTest do test "initialize with validation error returns error tuple" do # Create an unvalidated document with validation errors unvalidated_document = %Document{ - initial: "nonexistent", + initial: ["nonexistent"], states: [%State{id: "s1", type: :atomic, states: [], transitions: []}], validated: false, state_lookup: %{"s1" => %State{id: "s1", type: :atomic, states: [], transitions: []}}, @@ -324,7 +324,7 @@ defmodule Statifier.InterpreterCoverageTest do test "get_initial_configuration with invalid initial state" do # Test document with nonexistent initial state - covers the nil case in get_initial_configuration document = %Document{ - initial: "nonexistent_state", + initial: ["nonexistent_state"], states: [%State{id: "s1", type: :atomic, states: [], transitions: []}], validated: true, state_lookup: %{"s1" => %State{id: "s1", type: :atomic, states: [], transitions: []}}, @@ -420,7 +420,7 @@ defmodule Statifier.InterpreterCoverageTest do test "compound state with no children" do # Test compound state with empty children list - covers the nil return case document = %Document{ - initial: "empty_compound", + initial: ["empty_compound"], states: [ %State{ id: "empty_compound", @@ -428,7 +428,7 @@ defmodule Statifier.InterpreterCoverageTest do # No children states: [], transitions: [], - initial: nil + initial: [] } ], validated: true, @@ -438,7 +438,7 @@ defmodule Statifier.InterpreterCoverageTest do type: :compound, states: [], transitions: [], - initial: nil + initial: [] } }, transitions_by_source: %{} diff --git a/test/statifier/parser/scxml/final_state_test.exs b/test/statifier/parser/scxml/final_state_test.exs index 9568c5f..55bd3ff 100644 --- a/test/statifier/parser/scxml/final_state_test.exs +++ b/test/statifier/parser/scxml/final_state_test.exs @@ -22,7 +22,7 @@ defmodule Statifier.Parser.SCXML.FinalStateTest do assert final_state != nil assert final_state.type == :final assert final_state.id == "final_state" - assert final_state.initial == nil + assert final_state.initial == [] assert final_state.states == [] assert final_state.transitions == [] end @@ -140,7 +140,7 @@ defmodule Statifier.Parser.SCXML.FinalStateTest do assert final_state != nil assert final_state.type == :final assert final_state.states == [] - assert final_state.initial == nil + assert final_state.initial == [] assert final_state.initial_location == nil end diff --git a/test/statifier/parser/scxml_test.exs b/test/statifier/parser/scxml_test.exs index 6edd651..67dbd28 100644 --- a/test/statifier/parser/scxml_test.exs +++ b/test/statifier/parser/scxml_test.exs @@ -16,12 +16,12 @@ defmodule Statifier.Parser.SCXMLTest do %Document{ xmlns: "http://www.w3.org/2005/07/scxml", version: "1.0", - initial: "a", + initial: ["a"], document_order: 1, states: [ %Statifier.State{ id: "a", - initial: nil, + initial: [], document_order: 2, states: [], transitions: [] @@ -103,7 +103,7 @@ defmodule Statifier.Parser.SCXMLTest do states: [ %Statifier.State{ id: "parent", - initial: "child1", + initial: ["child1"], states: [ %Statifier.State{ id: "child1", @@ -133,10 +133,10 @@ defmodule Statifier.Parser.SCXMLTest do assert {:ok, %Document{ - initial: nil, + initial: [], states: [ %Statifier.State{ - initial: nil, + initial: [], transitions: [ %Statifier.Transition{ event: nil, @@ -231,7 +231,7 @@ defmodule Statifier.Parser.SCXMLTest do name: nil, states: [ %Statifier.State{ - initial: nil, + initial: [], transitions: [ %Statifier.Transition{ event: nil, diff --git a/test/statifier/state_chart_test.exs b/test/statifier/state_chart_test.exs index 11ef702..17f051c 100644 --- a/test/statifier/state_chart_test.exs +++ b/test/statifier/state_chart_test.exs @@ -6,7 +6,7 @@ defmodule Statifier.StateChartTest do setup do document = %Document{ name: "test_chart", - initial: "state_a", + initial: ["state_a"], states: [] } diff --git a/test/statifier/validator/edge_cases_test.exs b/test/statifier/validator/edge_cases_test.exs index f947fc1..7ca4a3e 100644 --- a/test/statifier/validator/edge_cases_test.exs +++ b/test/statifier/validator/edge_cases_test.exs @@ -65,7 +65,7 @@ defmodule Statifier.Validator.EdgeCasesTest do test "handles invalid initial state reference in compound state" do child1 = %State{id: "child1", states: []} child2 = %State{id: "child2", states: []} - parent = %State{id: "parent", initial: "nonexistent", states: [child1, child2]} + parent = %State{id: "parent", initial: ["nonexistent"], states: [child1, child2]} document = %Document{states: [parent]} {:error, errors, _warnings} = Validator.validate(document) @@ -90,7 +90,7 @@ defmodule Statifier.Validator.EdgeCasesTest do test "handles document initial state that is not top-level" do child = %State{id: "nested_initial", states: []} parent = %State{id: "parent", states: [child]} - document = %Document{initial: "nested_initial", states: [parent]} + document = %Document{initial: ["nested_initial"], states: [parent]} {:ok, _document, warnings} = Validator.validate(document) @@ -151,7 +151,7 @@ defmodule Statifier.Validator.EdgeCasesTest do child = %State{id: "child", states: [grandchild]} parent = %State{id: "parent", states: [child]} unreachable = %State{id: "unreachable", states: []} - document = %Document{initial: "parent", states: [parent, unreachable]} + document = %Document{initial: ["parent"], states: [parent, unreachable]} {:ok, _document, warnings} = Validator.validate(document) @@ -165,7 +165,7 @@ defmodule Statifier.Validator.EdgeCasesTest do state1 = %State{id: "s1", transitions: [transition], states: []} state2 = %State{id: "s2", transitions: [], states: []} unreachable = %State{id: "unreachable", states: []} - document = %Document{initial: "s1", states: [state1, state2, unreachable]} + document = %Document{initial: ["s1"], states: [state1, state2, unreachable]} {:ok, _document, warnings} = Validator.validate(document) @@ -179,7 +179,7 @@ defmodule Statifier.Validator.EdgeCasesTest do transition2 = %Transition{event: "back", targets: ["s1"]} state1 = %State{id: "s1", transitions: [transition1], states: []} state2 = %State{id: "s2", transitions: [transition2], states: []} - document = %Document{initial: "s1", states: [state1, state2]} + document = %Document{initial: ["s1"], states: [state1, state2]} {:ok, _document, warnings} = Validator.validate(document) diff --git a/test/statifier/validator/history_state_validator_test.exs b/test/statifier/validator/history_state_validator_test.exs index f92c3e6..fd48acc 100644 --- a/test/statifier/validator/history_state_validator_test.exs +++ b/test/statifier/validator/history_state_validator_test.exs @@ -74,7 +74,7 @@ defmodule Statifier.Validator.HistoryStateValidatorTest do document = %Document{ version: "1.0", xmlns: "http://www.w3.org/2005/07/scxml", - initial: "main", + initial: ["main"], states: [ %Statifier.State{ id: "main", @@ -183,7 +183,7 @@ defmodule Statifier.Validator.HistoryStateValidatorTest do document = %Document{ version: "1.0", xmlns: "http://www.w3.org/2005/07/scxml", - initial: "main", + initial: ["main"], states: [ %Statifier.State{ id: "main", @@ -227,7 +227,7 @@ defmodule Statifier.Validator.HistoryStateValidatorTest do document = %Document{ version: "1.0", xmlns: "http://www.w3.org/2005/07/scxml", - initial: "main", + initial: ["main"], states: [ %Statifier.State{ id: "main", @@ -286,7 +286,7 @@ defmodule Statifier.Validator.HistoryStateValidatorTest do document = %Document{ version: "1.0", xmlns: "http://www.w3.org/2005/07/scxml", - initial: "main", + initial: ["main"], states: [ %Statifier.State{ id: "main", @@ -327,12 +327,12 @@ defmodule Statifier.Validator.HistoryStateValidatorTest do document = %Document{ version: "1.0", xmlns: "http://www.w3.org/2005/07/scxml", - initial: "main", + initial: ["main"], states: [ %Statifier.State{ id: "main", type: :compound, - initial: "sub1", + initial: ["sub1"], states: [ %Statifier.State{ id: "hist", @@ -382,7 +382,7 @@ defmodule Statifier.Validator.HistoryStateValidatorTest do document = %Document{ version: "1.0", xmlns: "http://www.w3.org/2005/07/scxml", - initial: "main", + initial: ["main"], states: [ %Statifier.State{ id: "rootHist", diff --git a/test/statifier_test.exs b/test/statifier_test.exs index 3390623..5dd9edf 100644 --- a/test/statifier_test.exs +++ b/test/statifier_test.exs @@ -16,7 +16,7 @@ defmodule StatifierTest do assert {:ok, document, warnings} = Statifier.parse(xml) assert %Document{} = document assert document.name == nil - assert document.initial == "start" + assert document.initial == ["start"] assert document.validated == true assert is_list(warnings) end @@ -68,7 +68,7 @@ defmodule StatifierTest do assert {:ok, document, warnings} = Statifier.parse(xml) assert document.validated == true - assert document.initial == "start" + assert document.initial == ["start"] assert document.xmlns == "http://www.w3.org/2005/07/scxml" assert document.version == "1.0" assert is_list(warnings) @@ -84,7 +84,7 @@ defmodule StatifierTest do assert {:ok, document, _warnings} = Statifier.parse(xml) assert document.validated == true - assert document.initial == "start" + assert document.initial == ["start"] assert document.xmlns == "http://www.w3.org/2005/07/scxml" assert document.version == "1.0" end @@ -98,7 +98,7 @@ defmodule StatifierTest do assert {:ok, document, _warnings} = Statifier.parse(xml) assert document.validated == true - assert document.initial == "start" + assert document.initial == ["start"] # XML declaration should not be added by default end @@ -111,7 +111,7 @@ defmodule StatifierTest do assert {:ok, document, _warnings} = Statifier.parse(xml, xml_declaration: true) assert document.validated == true - assert document.initial == "start" + assert document.initial == ["start"] end test "returns validation errors when document is invalid" do From f267a54a5fd499a933239b29a15e384f7ac77435 Mon Sep 17 00:00:00 2001 From: JohnnyT Date: Sun, 7 Sep 2025 16:20:19 -0600 Subject: [PATCH 2/3] Adds docs for W3C test improvements --- documentation/w3c_test_improvement_plan.md | 158 +++++++++++++++++++++ test/support/statifier_case.ex | 3 +- 2 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 documentation/w3c_test_improvement_plan.md diff --git a/documentation/w3c_test_improvement_plan.md b/documentation/w3c_test_improvement_plan.md new file mode 100644 index 0000000..62e8f3b --- /dev/null +++ b/documentation/w3c_test_improvement_plan.md @@ -0,0 +1,158 @@ +# W3C Test Improvement Plan + +## Current Status + +**Test Pass Rate:** 22/59 W3C tests passing (37.3%) +**Recent Improvement:** Fixed multi-state initial configuration parsing - test576 and test413 now pass +**Baseline:** Up from 20/59 (34%) before the multi-state initial fix + +## High-Impact Quick Wins (Estimated 2-4 weeks) + +### 1. Enhanced Data Model Support + +**Impact:** ~8-12 additional tests +**Effort:** Medium + +- **Missing Features:** + - `` / `` element initialization + - Enhanced variable storage and access patterns + - JavaScript-style expression evaluation improvements + +**Target Tests:** test277, test276sub1, test550, test551 (data manipulation tests) + +### 2. Improved Event Processing + +**Impact:** ~6-8 additional tests +**Effort:** Medium + +- **Missing Features:** + - Enhanced event queuing semantics + - Proper event data handling and propagation + - Cross-state event communication improvements + +**Target Tests:** test399, test401, test402 (event processing tests) + +### 3. Advanced State Machine Features + +**Impact:** ~4-6 additional tests +**Effort:** Medium-High + +- **Missing Features:** + - Targetless transitions (internal transitions) + - Enhanced transition conflict resolution + - Improved parallel state semantics + +**Target Tests:** test406, test412, test416, test419, test423 (transition selection tests) + +## Medium-Term Improvements (4-8 weeks) + +### 4. Complete Executable Content + +**Impact:** ~8-10 additional tests +**Effort:** High + +- **Missing Features:** + - `` elements with delay support + - `