Skip to content

Commit 085fee7

Browse files
committed
test: improve branch coverage for invoke, SCXML parser, and orderedset
1 parent bbb8112 commit 085fee7

1 file changed

Lines changed: 150 additions & 0 deletions

File tree

tests/test_scxml_units.py

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -868,3 +868,153 @@ def test_invoke_with_text_content(self):
868868
assert "s1" in definition.states
869869
invoke_def = definition.states["s1"].invocations[0]
870870
assert "some text content" in invoke_def.content
871+
872+
def test_invoke_with_content_expr(self):
873+
"""<invoke> <content expr="..."> is parsed as dynamic content."""
874+
scxml = """
875+
<scxml xmlns="http://www.w3.org/2005/07/scxml" initial="s1">
876+
<state id="s1">
877+
<invoke type="http://www.w3.org/TR/scxml/">
878+
<content expr="'dynamic'"/>
879+
</invoke>
880+
</state>
881+
</scxml>
882+
"""
883+
definition = parse_scxml(scxml)
884+
invoke_def = definition.states["s1"].invocations[0]
885+
assert invoke_def.content == "'dynamic'"
886+
887+
def test_invoke_with_inline_scxml_no_namespace(self):
888+
"""<invoke> <content> with inline <scxml> (no namespace) is parsed."""
889+
scxml = """
890+
<scxml xmlns="http://www.w3.org/2005/07/scxml" initial="s1">
891+
<state id="s1">
892+
<invoke type="http://www.w3.org/TR/scxml/">
893+
<content><scxml><final id="f"/></scxml></content>
894+
</invoke>
895+
</state>
896+
</scxml>
897+
"""
898+
definition = parse_scxml(scxml)
899+
invoke_def = definition.states["s1"].invocations[0]
900+
assert "<final" in invoke_def.content
901+
902+
def test_invoke_with_empty_content(self):
903+
"""<invoke> with empty <content/> results in content=None."""
904+
scxml = """
905+
<scxml xmlns="http://www.w3.org/2005/07/scxml" initial="s1">
906+
<state id="s1">
907+
<invoke type="http://www.w3.org/TR/scxml/">
908+
<content/>
909+
</invoke>
910+
</state>
911+
</scxml>
912+
"""
913+
definition = parse_scxml(scxml)
914+
invoke_def = definition.states["s1"].invocations[0]
915+
assert invoke_def.content is None
916+
917+
def test_invoke_with_finalize_block(self):
918+
"""<invoke> with <finalize> block is parsed."""
919+
scxml = """
920+
<scxml xmlns="http://www.w3.org/2005/07/scxml" initial="s1">
921+
<state id="s1">
922+
<invoke type="http://www.w3.org/TR/scxml/">
923+
<content>child content</content>
924+
<finalize>
925+
<log label="finalized"/>
926+
</finalize>
927+
</invoke>
928+
</state>
929+
</scxml>
930+
"""
931+
definition = parse_scxml(scxml)
932+
invoke_def = definition.states["s1"].invocations[0]
933+
assert invoke_def.finalize is not None
934+
assert len(invoke_def.finalize.actions) == 1
935+
936+
937+
class TestParserAssignEdgeCases:
938+
def test_assign_without_children_or_text(self):
939+
"""<assign> with neither children nor text results in expr=None."""
940+
scxml = """
941+
<scxml xmlns="http://www.w3.org/2005/07/scxml" initial="s1">
942+
<datamodel>
943+
<data id="mydata" expr="0"/>
944+
</datamodel>
945+
<state id="s1">
946+
<onentry>
947+
<assign location="mydata"/>
948+
</onentry>
949+
<transition event="error.execution" target="err"/>
950+
</state>
951+
<final id="err"/>
952+
</scxml>
953+
"""
954+
definition = parse_scxml(scxml)
955+
assert "s1" in definition.states
956+
957+
958+
class TestSCXMLInvokerResolveContentAbsolutePath:
959+
def test_resolve_content_absolute_path(self, tmp_path):
960+
"""_resolve_content with absolute src path doesn't prepend base_dir."""
961+
scxml_file = tmp_path / "child.scxml"
962+
scxml_file.write_text("<scxml/>")
963+
964+
defn = InvokeDefinition(src=str(scxml_file))
965+
invoker = _make_invoker(definition=defn, base_dir="/some/other/dir")
966+
967+
result = invoker._resolve_content(Mock())
968+
assert result == "<scxml/>"
969+
970+
971+
class TestSCXMLInvokerEvaluateParamsNoExprNoLocation:
972+
def test_param_without_expr_or_location_skipped(self):
973+
"""_evaluate_params skips params with neither expr nor location."""
974+
defn = InvokeDefinition(
975+
params=[Param(name="p1", expr=None, location=None)],
976+
)
977+
invoker = _make_invoker(definition=defn)
978+
machine = Mock(model=type("M", (), {})())
979+
machine.model.__dict__ = {}
980+
981+
result = invoker._evaluate_params(machine)
982+
assert result == {}
983+
984+
985+
class TestInvokeInitMachineNone:
986+
def test_invoke_init_without_machine_is_noop(self):
987+
"""invoke_init does nothing when machine is not in kwargs."""
988+
from statemachine.io.scxml.actions import create_invoke_init_callable
989+
990+
callback = create_invoke_init_callable()
991+
# Call without machine kwarg — should not raise
992+
callback()
993+
994+
995+
class TestInvokeCallableWrapperRunInstance:
996+
def test_run_with_instance_not_class(self):
997+
"""_InvokeCallableWrapper.run() works with an instance (not a class)."""
998+
from statemachine.invoke import _InvokeCallableWrapper
999+
1000+
class Handler:
1001+
def run(self, ctx):
1002+
return "result"
1003+
1004+
handler_instance = Handler()
1005+
wrapper = _InvokeCallableWrapper(handler_instance)
1006+
assert not wrapper._is_class
1007+
1008+
ctx = Mock()
1009+
result = wrapper.run(ctx)
1010+
assert result == "result"
1011+
assert wrapper._instance is handler_instance
1012+
1013+
1014+
class TestOrderedSetStr:
1015+
def test_str_representation(self):
1016+
"""OrderedSet.__str__ returns a set-like string."""
1017+
from statemachine.orderedset import OrderedSet
1018+
1019+
os = OrderedSet([1, 2, 3])
1020+
assert str(os) == "{1, 2, 3}"

0 commit comments

Comments
 (0)