diff --git a/src/Stateless/Graph/UmlDotGraphStyle.cs b/src/Stateless/Graph/UmlDotGraphStyle.cs index 7d2f5bd0..f86d21d0 100644 --- a/src/Stateless/Graph/UmlDotGraphStyle.cs +++ b/src/Stateless/Graph/UmlDotGraphStyle.cs @@ -29,21 +29,20 @@ public override string GetPrefix() public override string FormatOneCluster(SuperState stateInfo) { string stateRepresentationString = ""; - var sourceName = stateInfo.StateName; - StringBuilder label = new StringBuilder($"{sourceName}"); + StringBuilder label = new StringBuilder($"{EscapeLabel(stateInfo.StateName)}"); if (stateInfo.EntryActions.Count > 0 || stateInfo.ExitActions.Count > 0) { label.Append("\\n----------"); - label.Append(string.Concat(stateInfo.EntryActions.Select(act => "\\nentry / " + act))); - label.Append(string.Concat(stateInfo.ExitActions.Select(act => "\\nexit / " + act))); + label.Append(string.Concat(stateInfo.EntryActions.Select(act => "\\nentry / " + EscapeLabel(act)))); + label.Append(string.Concat(stateInfo.ExitActions.Select(act => "\\nexit / " + EscapeLabel(act)))); } stateRepresentationString = "\n" - + $"subgraph \"cluster{stateInfo.NodeName}\"" + "\n" + + $"subgraph \"cluster{EscapeLabel(stateInfo.NodeName)}\"" + "\n" + "\t{" + "\n" - + $"\tlabel = \"{label.ToString()}\"" + "\n"; + + $"\tlabel = \"{label}\"" + "\n"; foreach (var subState in stateInfo.SubStates) { @@ -62,16 +61,18 @@ public override string FormatOneCluster(SuperState stateInfo) /// public override string FormatOneState(State state) { + var escapedStateName = EscapeLabel(state.StateName); + if (state.EntryActions.Count == 0 && state.ExitActions.Count == 0) - return $"\"{state.StateName}\" [label=\"{state.StateName}\"];\n"; + return $"\"{escapedStateName}\" [label=\"{escapedStateName}\"];\n"; - string f = $"\"{state.StateName}\" [label=\"{state.StateName}|"; + string f = $"\"{escapedStateName}\" [label=\"{escapedStateName}|"; List es = new List(); - es.AddRange(state.EntryActions.Select(act => "entry / " + act)); - es.AddRange(state.ExitActions.Select(act => "exit / " + act)); + es.AddRange(state.EntryActions.Select(act => "entry / " + EscapeLabel(act))); + es.AddRange(state.ExitActions.Select(act => "exit / " + EscapeLabel(act))); - f += String.Join("\\n", es); + f += string.Join("\\n", es); f += "\"];\n"; @@ -110,7 +111,7 @@ public override string FormatOneTransition(string sourceNodeName, string trigger /// public override string FormatOneDecisionNode(string nodeName, string label) { - return $"\"{nodeName}\" [shape = \"diamond\", label = \"{label}\"];\n"; + return $"\"{EscapeLabel(nodeName)}\" [shape = \"diamond\", label = \"{EscapeLabel(label)}\"];\n"; } /// @@ -121,17 +122,22 @@ public override string FormatOneDecisionNode(string nodeName, string label) public override string GetInitialTransition(StateInfo initialState) { var initialStateName = initialState.UnderlyingState.ToString(); - string dirgraphText = System.Environment.NewLine + $" init [label=\"\", shape=point];"; - dirgraphText += System.Environment.NewLine + $" init -> \"{initialStateName}\"[style = \"solid\"]"; + string dirgraphText = Environment.NewLine + $" init [label=\"\", shape=point];"; + dirgraphText += Environment.NewLine + $" init -> \"{EscapeLabel(initialStateName)}\"[style = \"solid\"]"; - dirgraphText += System.Environment.NewLine + "}"; + dirgraphText += Environment.NewLine + "}"; return dirgraphText; } internal string FormatOneLine(string fromNodeName, string toNodeName, string label) { - return $"\"{fromNodeName}\" -> \"{toNodeName}\" [style=\"solid\", label=\"{label}\"];"; + return $"\"{EscapeLabel(fromNodeName)}\" -> \"{EscapeLabel(toNodeName)}\" [style=\"solid\", label=\"{EscapeLabel(label)}\"];"; + } + + private static string EscapeLabel(string label) + { + return label.Replace("\\", "\\\\").Replace("\"", "\\\""); } } } diff --git a/test/Stateless.Tests/DotGraphFixture.cs b/test/Stateless.Tests/DotGraphFixture.cs index 70b65aa3..98cfabe1 100644 --- a/test/Stateless.Tests/DotGraphFixture.cs +++ b/test/Stateless.Tests/DotGraphFixture.cs @@ -136,19 +136,32 @@ public void SimpleTransition() } [Fact] - public void SimpleTransitionUML() + public void SimpleTransitionWithEscaping() { - var expected = Prefix(Style.UML) + Box(Style.UML, "A") + Box(Style.UML, "B") + Line("A", "B", "X") + suffix; + var state1 = "\\state \"1\""; + var state2 = "\\state \"2\""; + var trigger1 = "\\trigger \"1\""; - var sm = new StateMachine(State.A); + string suffix = Environment.NewLine + + $" init [label=\"\", shape=point];" + Environment.NewLine + + $" init -> \"{EscapeLabel(state1)}\"[style = \"solid\"]" + Environment.NewLine + + "}"; - sm.Configure(State.A) - .Permit(Trigger.X, State.B); + var expected = + Prefix(Style.UML) + + Box(Style.UML, EscapeLabel(state1)) + + Box(Style.UML, EscapeLabel(state2)) + + Line(EscapeLabel(state1), EscapeLabel(state2), EscapeLabel(trigger1)) + suffix; + + var sm = new StateMachine(state1); + + sm.Configure(state1) + .Permit(trigger1, state2); string dotGraph = UmlDotGraph.Format(sm.GetInfo()); #if WRITE_DOTS_TO_FOLDER - System.IO.File.WriteAllText(DestinationFolder + "SimpleTransitionUML.dot", dotGraph); + System.IO.File.WriteAllText(DestinationFolder + "SimpleTransitionWithEscaping.dot", dotGraph); #endif Assert.Equal(expected, dotGraph); @@ -196,7 +209,7 @@ public void WhenDiscriminatedByAnonymousGuardWithDescription() var expected = Prefix(Style.UML) + Box(Style.UML, "A") + Box(Style.UML, "B") + Line("A", "B", "X [description]") - + suffix; + + suffix; var sm = new StateMachine(State.A); @@ -398,46 +411,50 @@ public void OnEntryWithTriggerParameter() Assert.Equal(expected, dotGraph); } - + [Fact] public void SpacedUmlWithSubstate() { - string StateA = "State A"; - string StateB = "State B"; - string StateC = "State C"; - string StateD = "State D"; - string TriggerX = "Trigger X"; - string TriggerY = "Trigger Y"; - + string StateA = "State \"A\""; + string StateB = "State \"B\""; + string StateC = "State \"C\""; + string StateD = "State \"D\""; + string TriggerX = "Trigger \"X\""; + string TriggerY = "Trigger \"Y\""; + string EnterA = "Enter \"A\""; + string EnterD = "Enter \"D\""; + string ExitA = "Exit \"A\""; + var expected = Prefix(Style.UML) - + Subgraph(Style.UML, StateD, $"{StateD}\\n----------\\nentry / Enter D", - Box(Style.UML, StateB) - + Box(Style.UML, StateC)) - + Box(Style.UML, StateA, new List { "Enter A" }, new List { "Exit A" }) - + Line(StateA, StateB, TriggerX) + Line(StateA, StateC, TriggerY) - + Environment.NewLine + + Subgraph(Style.UML, EscapeLabel(StateD), $"{EscapeLabel(StateD)}\\n----------\\nentry / {EscapeLabel(EnterD)}", + Box(Style.UML, EscapeLabel(StateB)) + + Box(Style.UML, EscapeLabel(StateC))) + + Box(Style.UML, EscapeLabel(StateA), new List { EscapeLabel(EnterA) }, new List { EscapeLabel(ExitA) }) + + Line(EscapeLabel(StateA), EscapeLabel(StateB), EscapeLabel(TriggerX)) + + Line(EscapeLabel(StateA), EscapeLabel(StateC), EscapeLabel(TriggerY)) + + Environment.NewLine + $" init [label=\"\", shape=point];" + Environment.NewLine - + $" init -> \"{StateA}\"[style = \"solid\"]" + Environment.NewLine + + $" init -> \"{EscapeLabel(StateA)}\"[style = \"solid\"]" + Environment.NewLine + "}"; - var sm = new StateMachine("State A"); + var sm = new StateMachine(StateA); sm.Configure(StateA) .Permit(TriggerX, StateB) .Permit(TriggerY, StateC) - .OnEntry(TestEntryAction, "Enter A") - .OnExit(TestEntryAction, "Exit A"); + .OnEntry(TestEntryAction, EnterA) + .OnExit(TestEntryAction, ExitA); sm.Configure(StateB) .SubstateOf(StateD); sm.Configure(StateC) .SubstateOf(StateD); sm.Configure(StateD) - .OnEntry(TestEntryAction, "Enter D"); + .OnEntry(TestEntryAction, EnterD); string dotGraph = UmlDotGraph.Format(sm.GetInfo()); #if WRITE_DOTS_TO_FOLDER - System.IO.File.WriteAllText(DestinationFolder + "UmlWithSubstate.dot", dotGraph); + System.IO.File.WriteAllText(DestinationFolder + "SpacedUmlWithSubstate.dot", dotGraph); #endif Assert.Equal(expected, dotGraph); @@ -493,7 +510,7 @@ public void UmlWithDynamic() var sm = new StateMachine(State.A); sm.Configure(State.A) - .PermitDynamic(Trigger.X, DestinationSelector, null, new DynamicStateInfos { { State.B, "ChoseB"}, { State.C, "ChoseC" } }); + .PermitDynamic(Trigger.X, DestinationSelector, null, new DynamicStateInfos { { State.B, "ChoseB" }, { State.C, "ChoseC" } }); sm.Configure(State.B); sm.Configure(State.C); @@ -513,7 +530,7 @@ public void TransitionWithIgnoreAndEntry() + Box(Style.UML, "A", new List { "DoEntry" }) + Box(Style.UML, "B", new List { "DoThisEntry" }) + Line("A", "B", "X") - + Line("A", "A", "Y") + + Line("A", "A", "Y") + Line("B", "B", "Z / DoThisEntry") + suffix; @@ -644,5 +661,6 @@ public void Reentrant_Transition_Shows_Entry_Action_When_Action_Is_Configured_Wi private void TestEntryAction() { } private void TestEntryActionString(string val) { } private State DestinationSelector() { return State.A; } + private static string EscapeLabel(string label) { return label.Replace("\\", "\\\\").Replace("\"", "\\\""); } } }