Skip to content

Commit f009179

Browse files
committed
fix: preserve initial states order from SCXML declaration
The SCXML `initial` attribute specifies states in declaration order (e.g., `initial="s2p112 s2p122"`), but `_parse_initial` returned a `set`, losing ordering due to hash randomization. This caused flaky test413 — the parallel state's children were entered in random order, sometimes triggering eventless transitions to `fail` before both children were in the configuration. Change `initial_states` from `Set[str]` to `List[str]` in `StateMachineDefinition` and `_parse_initial`, preserving declaration order for `start_configuration_values`. The `all_initial_states` set is still used for O(1) membership checks in `parse_state`.
1 parent 261b5d7 commit f009179

2 files changed

Lines changed: 11 additions & 10 deletions

File tree

statemachine/io/scxml/parser.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import re
22
import xml.etree.ElementTree as ET
3+
from typing import List
34
from typing import Set
45
from urllib.parse import urlparse
56

@@ -35,10 +36,10 @@ def strip_namespaces(tree: ET.Element):
3536
attrib[new_name] = attrib.pop(name)
3637

3738

38-
def _parse_initial(initial_content: "str | None") -> Set[str]:
39+
def _parse_initial(initial_content: "str | None") -> List[str]:
3940
if initial_content is None:
40-
return set()
41-
return set(initial_content.split())
41+
return []
42+
return initial_content.split()
4243

4344

4445
def parse_scxml(scxml_content: str) -> StateMachineDefinition: # noqa: C901
@@ -75,9 +76,10 @@ def parse_scxml(scxml_content: str) -> StateMachineDefinition: # noqa: C901
7576

7677
# If no initial state was specified, pick the first state
7778
if not all_initial_states and definition.states:
78-
all_initial_states = {next(key for key in definition.states.keys())}
79-
for s in all_initial_states:
80-
definition.states[s].initial = True
79+
first_state = next(iter(definition.states.keys()))
80+
all_initial_states = {first_state}
81+
definition.initial_states = [first_state]
82+
definition.states[first_state].initial = True
8183

8284
return definition
8385

@@ -158,13 +160,13 @@ def parse_state( # noqa: C901
158160
state.transitions.append(transition)
159161

160162
# Parse child states
161-
initial_states |= _parse_initial(state_elem.get("initial"))
163+
initial_states.update(_parse_initial(state_elem.get("initial")))
162164
initial_elem = state_elem.find("initial")
163165
if initial_elem is not None:
164166
for trans_elem in initial_elem.findall("transition"):
165167
transition = parse_transition(trans_elem, initial=True)
166168
state.transitions.append(transition)
167-
initial_states |= _parse_initial(trans_elem.get("target"))
169+
initial_states.update(_parse_initial(trans_elem.get("target")))
168170

169171
for child_state_elem in state_elem.findall("state"):
170172
child_state = parse_state(child_state_elem, initial_states=initial_states)

statemachine/io/scxml/schema.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
from dataclasses import field
33
from typing import Dict
44
from typing import List
5-
from typing import Set
65
from urllib.parse import ParseResult
76

87

@@ -149,5 +148,5 @@ class DataModel:
149148
class StateMachineDefinition:
150149
name: "str | None" = None
151150
states: Dict[str, State] = field(default_factory=dict)
152-
initial_states: Set[str] = field(default_factory=set)
151+
initial_states: List[str] = field(default_factory=list)
153152
datamodel: "DataModel | None" = None

0 commit comments

Comments
 (0)