diff --git a/src/Stateless/Graph/MermaidGraph.cs b/src/Stateless/Graph/MermaidGraph.cs
index 699562ee..c3d99fff 100644
--- a/src/Stateless/Graph/MermaidGraph.cs
+++ b/src/Stateless/Graph/MermaidGraph.cs
@@ -1,4 +1,5 @@
using Stateless.Reflection;
+using System.Collections;
namespace Stateless.Graph
{
@@ -11,13 +12,15 @@ public static class MermaidGraph
/// Generate a Mermaid graph from the state machine info
///
///
+ ///
+ /// When set, includes a direction setting in the output indicating the direction of flow.
+ ///
///
- public static string Format(StateMachineInfo machineInfo)
+ public static string Format(StateMachineInfo machineInfo, MermaidGraphDirection? direction = null)
{
var graph = new StateGraph(machineInfo);
- return graph.ToGraph(new MermaidGraphStyle());
+ return graph.ToGraph(new MermaidGraphStyle(graph, direction));
}
-
}
}
diff --git a/src/Stateless/Graph/MermaidGraphDirection.cs b/src/Stateless/Graph/MermaidGraphDirection.cs
new file mode 100644
index 00000000..344d6810
--- /dev/null
+++ b/src/Stateless/Graph/MermaidGraphDirection.cs
@@ -0,0 +1,17 @@
+namespace Stateless.Graph
+{
+ ///
+ /// The directions of flow that can be chosen for a Mermaid graph.
+ ///
+ public enum MermaidGraphDirection
+ {
+ /// Left-to-right flow
+ LeftToRight,
+ /// Right-to-left flow
+ RightToLeft,
+ /// Top-to-bottom flow
+ TopToBottom,
+ /// Bottom-to-top flow
+ BottomToTop
+ }
+}
diff --git a/src/Stateless/Graph/MermaidGraphStyle.cs b/src/Stateless/Graph/MermaidGraphStyle.cs
index bf1fc5ef..7080c080 100644
--- a/src/Stateless/Graph/MermaidGraphStyle.cs
+++ b/src/Stateless/Graph/MermaidGraphStyle.cs
@@ -1,7 +1,7 @@
using Stateless.Reflection;
using System;
using System.Collections.Generic;
-using System.Reflection.Emit;
+using System.Linq;
using System.Text;
namespace Stateless.Graph
@@ -11,16 +11,37 @@ namespace Stateless.Graph
///
public class MermaidGraphStyle : GraphStyleBase
{
+ private readonly StateGraph _graph;
+ private readonly MermaidGraphDirection? _direction;
+ private readonly Dictionary _stateMap = new Dictionary();
+ private bool _stateMapInitialized = false;
+
///
- /// Returns the formatted text for a single superstate and its substates.
- /// For example, for DOT files this would be a subgraph containing nodes for all the substates.
+ /// Create a new instance of
///
- /// The superstate to generate text for
- /// Description of the superstate, and all its substates, in the desired format
+ /// The state graph
+ /// When non-null, sets the flow direction in the output.
+ public MermaidGraphStyle(StateGraph graph, MermaidGraphDirection? direction)
+ : base()
+ {
+ _graph = graph;
+ _direction = direction;
+ }
+
+ ///
public override string FormatOneCluster(SuperState stateInfo)
{
- string stateRepresentationString = "";
- return stateRepresentationString;
+ StringBuilder sb = new StringBuilder();
+ sb.AppendLine();
+ sb.AppendLine($"\tstate {GetSanitizedStateName(stateInfo.StateName)} {{");
+ foreach (var subState in stateInfo.SubStates)
+ {
+ sb.AppendLine($"\t\t{GetSanitizedStateName(subState.StateName)}");
+ }
+
+ sb.Append("\t}");
+
+ return sb.ToString();
}
///
@@ -31,57 +52,122 @@ public override string FormatOneCluster(SuperState stateInfo)
///
public override string FormatOneDecisionNode(string nodeName, string label)
{
- return String.Empty;
+ return $"{Environment.NewLine}\tstate {nodeName} <>";
}
- ///
- /// Generate the text for a single state
- ///
- /// The state to generate text for
- ///
+ ///
public override string FormatOneState(State state)
{
- return String.Empty;
+ return string.Empty;
}
/// Get the text that starts a new graph
///
public override string GetPrefix()
{
- return "stateDiagram-v2";
+ BuildSanitizedNamedStateMap();
+ string prefix = "stateDiagram-v2";
+ if (_direction.HasValue)
+ {
+ prefix += $"{Environment.NewLine}\tdirection {GetDirectionCode(_direction.Value)}";
+ }
+
+ foreach (var state in _stateMap.Where(x => !x.Key.Equals(x.Value.StateName, StringComparison.Ordinal)))
+ {
+ prefix += $"{Environment.NewLine}\t{state.Key} : {state.Value.StateName}";
+ }
+
+ return prefix;
}
- ///
- ///
- ///
- ///
- ///
+ ///
public override string GetInitialTransition(StateInfo initialState)
{
- return $"\r\n[*] --> {initialState}";
- }
+ var sanitizedStateName = GetSanitizedStateName(initialState.ToString());
-
+ return $"{Environment.NewLine}[*] --> {sanitizedStateName}";
+ }
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
+ ///
public override string FormatOneTransition(string sourceNodeName, string trigger, IEnumerable actions, string destinationNodeName, IEnumerable guards)
{
string label = trigger ?? "";
- return FormatOneLine(sourceNodeName, destinationNodeName, label);
+ if (actions?.Count() > 0)
+ label += " / " + string.Join(", ", actions);
+
+ if (guards.Any())
+ {
+ foreach (var info in guards)
+ {
+ if (label.Length > 0)
+ label += " ";
+ label += "[" + info + "]";
+ }
+ }
+
+ var sanitizedSourceNodeName = GetSanitizedStateName(sourceNodeName);
+ var sanitizedDestinationNodeName = GetSanitizedStateName(destinationNodeName);
+
+ return FormatOneLine(sanitizedSourceNodeName, sanitizedDestinationNodeName, label);
}
internal string FormatOneLine(string fromNodeName, string toNodeName, string label)
{
return $"\t{fromNodeName} --> {toNodeName} : {label}";
}
+
+ private static string GetDirectionCode(MermaidGraphDirection direction)
+ {
+ switch(direction)
+ {
+ case MermaidGraphDirection.TopToBottom:
+ return "TB";
+ case MermaidGraphDirection.BottomToTop:
+ return "BT";
+ case MermaidGraphDirection.LeftToRight:
+ return "LR";
+ case MermaidGraphDirection.RightToLeft:
+ return "RL";
+ default:
+ throw new ArgumentOutOfRangeException(nameof(direction), direction, $"Unsupported {nameof(MermaidGraphDirection)}: {direction}.");
+ }
+ }
+
+ private void BuildSanitizedNamedStateMap()
+ {
+ if (_stateMapInitialized)
+ {
+ return;
+ }
+
+ // Ensures that state names are unique and do not contain characters that would cause an invalid Mermaid graph.
+ var uniqueAliases = new HashSet();
+ foreach (var state in _graph.States)
+ {
+ var sanitizedStateName = string.Concat(state.Value.StateName.Where(c => !(char.IsWhiteSpace(c) || c == ':' || c == '-')));
+ if (!sanitizedStateName.Equals(state.Value.StateName, StringComparison.Ordinal))
+ {
+ int count = 1;
+ var tempName = sanitizedStateName;
+ while (uniqueAliases.Contains(tempName) || _graph.States.ContainsKey(tempName))
+ {
+ tempName = $"{sanitizedStateName}_{count++}";
+ }
+
+ sanitizedStateName = tempName;
+ uniqueAliases.Add(sanitizedStateName);
+ }
+
+ _stateMap[sanitizedStateName] = state.Value;
+ }
+
+ _stateMapInitialized = true;
+ }
+
+ private string GetSanitizedStateName(string stateName)
+ {
+ return _stateMap.FirstOrDefault(x => x.Value.StateName == stateName).Key ?? stateName;
+ }
}
}
diff --git a/src/Stateless/Graph/StateGraph.cs b/src/Stateless/Graph/StateGraph.cs
index 3477bfd4..ff32e7aa 100644
--- a/src/Stateless/Graph/StateGraph.cs
+++ b/src/Stateless/Graph/StateGraph.cs
@@ -58,12 +58,12 @@ public StateGraph(StateMachineInfo machineInfo)
///
public string ToGraph(GraphStyleBase style)
{
- string dirgraphText = style.GetPrefix().Replace("\n", System.Environment.NewLine);
+ string dirgraphText = style.GetPrefix();
// Start with the clusters
foreach (var state in States.Values.Where(x => x is SuperState))
{
- dirgraphText += style.FormatOneCluster((SuperState)state).Replace("\n", System.Environment.NewLine);
+ dirgraphText += style.FormatOneCluster((SuperState)state);
}
// Next process all non-cluster states
@@ -71,14 +71,13 @@ public string ToGraph(GraphStyleBase style)
{
if (state is SuperState || state is Decision || state.SuperState != null)
continue;
- dirgraphText += style.FormatOneState(state).Replace("\n", System.Environment.NewLine);
+ dirgraphText += style.FormatOneState(state);
}
// Finally, add decision nodes
foreach (var dec in Decisions)
{
- dirgraphText += style.FormatOneDecisionNode(dec.NodeName, dec.Method.Description)
- .Replace("\n", System.Environment.NewLine);
+ dirgraphText += style.FormatOneDecisionNode(dec.NodeName, dec.Method.Description);
}
// now build behaviours
diff --git a/src/Stateless/Graph/UmlDotGraphStyle.cs b/src/Stateless/Graph/UmlDotGraphStyle.cs
index 7d2f5bd0..8f1b53f1 100644
--- a/src/Stateless/Graph/UmlDotGraphStyle.cs
+++ b/src/Stateless/Graph/UmlDotGraphStyle.cs
@@ -7,63 +7,67 @@
namespace Stateless.Graph
{
///
- /// Generate DOT graphs in basic UML style.
+ /// Generate DOT graphs in basic UML style
///
public class UmlDotGraphStyle : GraphStyleBase
{
- /// Get the text that starts a new graph.
- /// The prefix for the DOT graph document.
+ /// Get the text that starts a new graph
+ ///
public override string GetPrefix()
{
- return "digraph {\n"
- + "compound=true;\n"
- + "node [shape=Mrecord]\n"
- + "rankdir=\"LR\"\n";
+ var sb = new StringBuilder();
+ sb.AppendLine("digraph {")
+ .AppendLine("compound=true;")
+ .AppendLine("node [shape=Mrecord]")
+ .AppendLine("rankdir=\"LR\"");
+
+ return sb.ToString();
}
///
/// Returns the formatted text for a single superstate and its substates.
+ /// For example, for DOT files this would be a subgraph containing nodes for all the substates.
///
- /// A DOT graph representation of the superstate and all its substates.
- ///
+ /// The superstate to generate text for
+ /// Description of the superstate, and all its substates, in the desired format
public override string FormatOneCluster(SuperState stateInfo)
{
- string stateRepresentationString = "";
+ var sb = new StringBuilder();
var sourceName = stateInfo.StateName;
- StringBuilder label = new StringBuilder($"{sourceName}");
+ StringBuilder label = new StringBuilder(sourceName);
- if (stateInfo.EntryActions.Count > 0 || stateInfo.ExitActions.Count > 0)
+ if (stateInfo.EntryActions.Any() || stateInfo.ExitActions.Any())
{
- 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($"{Environment.NewLine}----------")
+ .Append(string.Concat(stateInfo.EntryActions.Select(act => $"{Environment.NewLine}entry / {act}")))
+ .Append(string.Concat(stateInfo.ExitActions.Select(act => $"{Environment.NewLine}exit / {act}")));
}
- stateRepresentationString = "\n"
- + $"subgraph \"cluster{stateInfo.NodeName}\"" + "\n"
- + "\t{" + "\n"
- + $"\tlabel = \"{label.ToString()}\"" + "\n";
+ sb.AppendLine()
+ .AppendLine($"subgraph \"cluster{stateInfo.NodeName}\"")
+ .AppendLine("\t{")
+ .AppendLine($"\tlabel = \"{label.ToString()}\"");
foreach (var subState in stateInfo.SubStates)
{
- stateRepresentationString += FormatOneState(subState);
+ sb.Append(FormatOneState(subState));
}
- stateRepresentationString += "}\n";
+ sb.AppendLine("}");
- return stateRepresentationString;
+ return sb.ToString();
}
///
- /// Generate the text for a single state.
+ /// Generate the text for a single state
///
- /// A DOT graph representation of the state.
- ///
+ /// The state to generate text for
+ ///
public override string FormatOneState(State state)
{
if (state.EntryActions.Count == 0 && state.ExitActions.Count == 0)
- return $"\"{state.StateName}\" [label=\"{state.StateName}\"];\n";
+ return $"\"{state.StateName}\" [label=\"{state.StateName}\"];{Environment.NewLine}";
string f = $"\"{state.StateName}\" [label=\"{state.StateName}|";
@@ -71,18 +75,22 @@ public override string FormatOneState(State state)
es.AddRange(state.EntryActions.Select(act => "entry / " + act));
es.AddRange(state.ExitActions.Select(act => "exit / " + act));
- f += String.Join("\\n", es);
+ f += string.Join(Environment.NewLine, es);
- f += "\"];\n";
+ f += $"\"];{Environment.NewLine}";
return f;
}
///
- /// Generate text for a single transition.
+ /// Generate text for a single transition
///
- /// A DOT graph representation of a state transition.
- ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
public override string FormatOneTransition(string sourceNodeName, string trigger, IEnumerable actions, string destinationNodeName, IEnumerable guards)
{
string label = trigger ?? "";
@@ -104,20 +112,26 @@ public override string FormatOneTransition(string sourceNodeName, string trigger
}
///
- /// Generate the text for a single decision node.
+ /// Generate the text for a single decision node
///
- /// A DOT graph representation of the decision node for a dynamic transition.
- ///
+ /// Name of the node
+ /// Label for the node
+ ///
public override string FormatOneDecisionNode(string nodeName, string label)
{
- return $"\"{nodeName}\" [shape = \"diamond\", label = \"{label}\"];\n";
+ return $"\"{nodeName}\" [shape = \"diamond\", label = \"{label}\"];{Environment.NewLine}";
+ }
+
+ internal string FormatOneLine(string fromNodeName, string toNodeName, string label)
+ {
+ return $"\"{fromNodeName}\" -> \"{toNodeName}\" [style=\"solid\", label=\"{label}\"];";
}
///
- /// Get initial transition if present.
+ ///
///
- /// A DOT graph representation of the initial state transition.
- ///
+ ///
+ ///
public override string GetInitialTransition(StateInfo initialState)
{
var initialStateName = initialState.UnderlyingState.ToString();
@@ -128,10 +142,5 @@ public override string GetInitialTransition(StateInfo initialState)
return dirgraphText;
}
-
- internal string FormatOneLine(string fromNodeName, string toNodeName, string label)
- {
- return $"\"{fromNodeName}\" -> \"{toNodeName}\" [style=\"solid\", label=\"{label}\"];";
- }
}
}
diff --git a/test/Stateless.Tests/DotGraphFixture.cs b/test/Stateless.Tests/DotGraphFixture.cs
index b16234b4..5704c695 100644
--- a/test/Stateless.Tests/DotGraphFixture.cs
+++ b/test/Stateless.Tests/DotGraphFixture.cs
@@ -71,7 +71,7 @@ string Box(Style style, string label, List entries = null, List
b = $"\"{label}\" [label=\"{label}\"];\n";
else
{
- b = $"\"{label}\"" + " [label=\"" + label + "|" + String.Join("\\n", es) + "\"];\n";
+ b = $"\"{label}\"" + " [label=\"" + label + "|" + String.Join("\n", es) + "\"];\n";
}
return b.Replace("\n", Environment.NewLine);
@@ -135,25 +135,6 @@ public void SimpleTransition()
Assert.Equal(expected, dotGraph);
}
- [Fact]
- public void SimpleTransitionUML()
- {
- var expected = Prefix(Style.UML) + Box(Style.UML, "A") + Box(Style.UML, "B") + Line("A", "B", "X") + suffix;
-
- var sm = new StateMachine(State.A);
-
- sm.Configure(State.A)
- .Permit(Trigger.X, State.B);
-
- string dotGraph = UmlDotGraph.Format(sm.GetInfo());
-
-#if WRITE_DOTS_TO_FOLDER
- System.IO.File.WriteAllText(DestinationFolder + "SimpleTransitionUML.dot", dotGraph);
-#endif
-
- Assert.Equal(expected, dotGraph);
- }
-
[Fact]
public void TwoSimpleTransitions()
{
@@ -168,7 +149,13 @@ public void TwoSimpleTransitions()
.Permit(Trigger.X, State.B)
.Permit(Trigger.Y, State.C);
- Assert.Equal(expected, UmlDotGraph.Format(sm.GetInfo()));
+ string dotGraph = UmlDotGraph.Format(sm.GetInfo());
+
+#if WRITE_DOTS_TO_FOLDER
+ System.IO.File.WriteAllText(DestinationFolder + "TwoSimpleTransitions.dot", dotGraph);
+#endif
+
+ Assert.Equal(expected, dotGraph);
}
[Fact]
@@ -185,7 +172,13 @@ public void WhenDiscriminatedByAnonymousGuard()
.PermitIf(Trigger.X, State.B, anonymousGuard);
sm.Configure(State.B);
- Assert.Equal(expected, UmlDotGraph.Format(sm.GetInfo()));
+ string dotGraph = UmlDotGraph.Format(sm.GetInfo());
+
+#if WRITE_DOTS_TO_FOLDER
+ System.IO.File.WriteAllText(DestinationFolder + "WhenDiscriminatedByAnonymousGuard.dot", dotGraph);
+#endif
+
+ Assert.Equal(expected, dotGraph);
}
[Fact]
@@ -225,7 +218,13 @@ public void WhenDiscriminatedByNamedDelegate()
sm.Configure(State.A)
.PermitIf(Trigger.X, State.B, IsTrue);
- Assert.Equal(expected, UmlDotGraph.Format(sm.GetInfo()));
+ string dotGraph = UmlDotGraph.Format(sm.GetInfo());
+
+#if WRITE_DOTS_TO_FOLDER
+ System.IO.File.WriteAllText(DestinationFolder + "WhenDiscriminatedByNamedDelegate.dot", dotGraph);
+#endif
+
+ Assert.Equal(expected, dotGraph);
}
[Fact]
@@ -362,7 +361,13 @@ public void TransitionWithIgnore()
.Ignore(Trigger.Y)
.Permit(Trigger.X, State.B);
- Assert.Equal(expected, UmlDotGraph.Format(sm.GetInfo()));
+ string dotGraph = UmlDotGraph.Format(sm.GetInfo());
+
+#if WRITE_DOTS_TO_FOLDER
+ System.IO.File.WriteAllText(DestinationFolder + "TransitionWithIgnore.dot", dotGraph);
+#endif
+
+ Assert.Equal(expected, dotGraph);
}
[Fact]
@@ -410,7 +415,7 @@ public void SpacedUmlWithSubstate()
string TriggerY = "Trigger Y";
var expected = Prefix(Style.UML)
- + Subgraph(Style.UML, StateD, $"{StateD}\\n----------\\nentry / Enter D",
+ + 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" })
@@ -437,7 +442,7 @@ public void SpacedUmlWithSubstate()
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);
@@ -447,7 +452,7 @@ public void SpacedUmlWithSubstate()
public void UmlWithSubstate()
{
var expected = Prefix(Style.UML)
- + Subgraph(Style.UML, "D", "D\\n----------\\nentry / EnterD",
+ + Subgraph(Style.UML, "D", "D\n----------\nentry / EnterD",
Box(Style.UML, "B")
+ Box(Style.UML, "C"))
+ Box(Style.UML, "A", new List { "EnterA" }, new List { "ExitA" })
diff --git a/test/Stateless.Tests/MermaidGraphFixture.cs b/test/Stateless.Tests/MermaidGraphFixture.cs
index 18b84404..02042ee5 100644
--- a/test/Stateless.Tests/MermaidGraphFixture.cs
+++ b/test/Stateless.Tests/MermaidGraphFixture.cs
@@ -1,4 +1,5 @@
-using Xunit;
+using System.Text;
+using Xunit;
namespace Stateless.Tests
{
@@ -7,20 +8,28 @@ public class MermaidGraphFixture
[Fact]
public void Format_InitialTransition_ShouldReturns()
{
- var expected = "stateDiagram-v2\r\n[*] --> A";
+ var expected = new StringBuilder()
+ .AppendLine("stateDiagram-v2")
+ .AppendLine("[*] --> A")
+ .ToString().TrimEnd();
var sm = new StateMachine(State.A);
var result = Graph.MermaidGraph.Format(sm.GetInfo());
- Assert.Equal(expected, result);
+ WriteToFile(nameof(Format_InitialTransition_ShouldReturns), result);
+ Assert.Equal(expected, result);
}
[Fact]
- public void Format_SimpleTransition()
+ public void SimpleTransition()
{
- var expected = "stateDiagram-v2\r\n\tA --> B : X\r\n[*] --> A";
+ var expected = new StringBuilder()
+ .AppendLine("stateDiagram-v2")
+ .AppendLine(" A --> B : X")
+ .AppendLine("[*] --> A")
+ .ToString().TrimEnd();
var sm = new StateMachine(State.A);
@@ -29,18 +38,42 @@ public void Format_SimpleTransition()
var result = Graph.MermaidGraph.Format(sm.GetInfo());
+ WriteToFile(nameof(SimpleTransition), result);
+
Assert.Equal(expected, result);
+ }
+ [Fact]
+ public void SimpleTransition_LeftToRight()
+ {
+ var expected = new StringBuilder()
+ .AppendLine("stateDiagram-v2")
+ .AppendLine(" direction LR")
+ .AppendLine(" A --> B : X")
+ .AppendLine("[*] --> A")
+ .ToString().TrimEnd();
+
+ var sm = new StateMachine(State.A);
+
+ sm.Configure(State.A)
+ .Permit(Trigger.X, State.B);
+
+ var result = Graph.MermaidGraph.Format(sm.GetInfo(), Graph.MermaidGraphDirection.LeftToRight);
+
+ WriteToFile(nameof(SimpleTransition_LeftToRight), result);
+
+ Assert.Equal(expected, result);
}
[Fact]
public void TwoSimpleTransitions()
{
- var expected = """
- stateDiagram-v2
- A --> B : X
- A --> C : Y
- """;
+ var expected = new StringBuilder()
+ .AppendLine("stateDiagram-v2")
+ .AppendLine(" A --> B : X")
+ .AppendLine(" A --> C : Y")
+ .AppendLine("[*] --> A")
+ .ToString().TrimEnd();
var sm = new StateMachine(State.A);
@@ -50,8 +83,324 @@ public void TwoSimpleTransitions()
var result = Graph.MermaidGraph.Format(sm.GetInfo());
+ WriteToFile(nameof(TwoSimpleTransitions), result);
+
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void WhenDiscriminatedByAnonymousGuard()
+ {
+ var expected = new StringBuilder()
+ .AppendLine("stateDiagram-v2")
+ .AppendLine(" A --> B : X [Function]")
+ .AppendLine("[*] --> A")
+ .ToString().TrimEnd();
+
+ bool anonymousGuard() => true;
+
+ var sm = new StateMachine(State.A);
+
+ sm.Configure(State.A)
+ .PermitIf(Trigger.X, State.B, anonymousGuard);
+ sm.Configure(State.B);
+
+ var result = Graph.MermaidGraph.Format(sm.GetInfo());
+
+ WriteToFile(nameof(WhenDiscriminatedByAnonymousGuard), result);
+
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void WhenDiscriminatedByAnonymousGuardWithDescription()
+ {
+ var expected = new StringBuilder()
+ .AppendLine("stateDiagram-v2")
+ .AppendLine(" A --> B : X [description]")
+ .AppendLine("[*] --> A")
+ .ToString().TrimEnd();
+
+ bool guardFunction() => true;
+
+ var sm = new StateMachine(State.A);
+
+ sm.Configure(State.A)
+ .PermitIf(Trigger.X, State.B, guardFunction, "description");
+ sm.Configure(State.B);
+
+ var result = Graph.MermaidGraph.Format(sm.GetInfo());
+
+ WriteToFile(nameof(WhenDiscriminatedByAnonymousGuard), result);
+
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void WhenDiscriminatedByNamedDelegate()
+ {
+ var expected = new StringBuilder()
+ .AppendLine("stateDiagram-v2")
+ .AppendLine(" A --> B : X [IsTrue]")
+ .AppendLine("[*] --> A")
+ .ToString().TrimEnd();
+
+ var sm = new StateMachine(State.A);
+
+ sm.Configure(State.A)
+ .PermitIf(Trigger.X, State.B, IsTrue);
+
+ var result = Graph.MermaidGraph.Format(sm.GetInfo());
+
+ WriteToFile(nameof(WhenDiscriminatedByNamedDelegate), result);
+
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void WhenDiscriminatedByNamedDelegateWithDescription()
+ {
+ var expected = new StringBuilder()
+ .AppendLine("stateDiagram-v2")
+ .AppendLine(" A --> B : X [description]")
+ .AppendLine("[*] --> A")
+ .ToString().TrimEnd();
+
+ var sm = new StateMachine(State.A);
+
+ sm.Configure(State.A)
+ .PermitIf(Trigger.X, State.B, IsTrue, "description");
+
+ var result = Graph.MermaidGraph.Format(sm.GetInfo());
+
+ WriteToFile(nameof(WhenDiscriminatedByNamedDelegateWithDescription), result);
+
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void DestinationStateIsDynamic()
+ {
+ var expected = new StringBuilder()
+ .AppendLine("stateDiagram-v2")
+ .AppendLine(" state Decision1 <>")
+ .AppendLine(" A --> Decision1 : X")
+ .AppendLine("[*] --> A")
+ .ToString().TrimEnd();
+
+ var sm = new StateMachine(State.A);
+ sm.Configure(State.A)
+ .PermitDynamic(Trigger.X, () => State.B);
+
+ var result = Graph.MermaidGraph.Format(sm.GetInfo());
+
+ WriteToFile(nameof(DestinationStateIsDynamic), result);
+
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void DestinationStateIsCalculatedBasedOnTriggerParameters()
+ {
+ var expected = new StringBuilder()
+ .AppendLine("stateDiagram-v2")
+ .AppendLine(" state Decision1 <>")
+ .AppendLine(" A --> Decision1 : X")
+ .AppendLine("[*] --> A")
+ .ToString().TrimEnd();
+
+ var sm = new StateMachine(State.A);
+ var trigger = sm.SetTriggerParameters(Trigger.X);
+ sm.Configure(State.A)
+ .PermitDynamic(trigger, i => i == 1 ? State.B : State.C);
+
+ var result = Graph.MermaidGraph.Format(sm.GetInfo());
+
+ WriteToFile(nameof(DestinationStateIsCalculatedBasedOnTriggerParameters), result);
+
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void TransitionWithIgnore()
+ {
+ // This test duplicates the behaviour expressed in the TransitionWithIgnore test in DotGraphFixture,
+ // but it seems counter-intuitive to show the ignored trigger as a transition back to the same state.
+ var expected = new StringBuilder()
+ .AppendLine("stateDiagram-v2")
+ .AppendLine(" A --> B : X")
+ .AppendLine(" A --> A : Y")
+ .AppendLine("[*] --> A")
+ .ToString().TrimEnd();
+
+ var sm = new StateMachine(State.A);
+
+ sm.Configure(State.A)
+ .Ignore(Trigger.Y)
+ .Permit(Trigger.X, State.B);
+
+ var result = Graph.MermaidGraph.Format(sm.GetInfo());
+
+ WriteToFile(nameof(TransitionWithIgnore), result);
+
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void OnEntryWithTriggerParameter()
+ {
+ var expected = new StringBuilder()
+ .AppendLine("stateDiagram-v2")
+ .AppendLine(" A --> B : X / BX")
+ .AppendLine(" A --> C : Y / TestEntryActionString [IsTriggerY]")
+ .AppendLine(" A --> B : Z [IsTriggerZ]")
+ .AppendLine("[*] --> A")
+ .ToString().TrimEnd();
+
+ bool anonymousGuard() => true;
+ var sm = new StateMachine(State.A);
+ var parmTrig = sm.SetTriggerParameters(Trigger.Y);
+
+ sm.Configure(State.A)
+ .OnEntry(() => { }, "OnEntry")
+ .Permit(Trigger.X, State.B)
+ .PermitIf(Trigger.Y, State.C, anonymousGuard, "IsTriggerY")
+ .PermitIf(Trigger.Z, State.B, anonymousGuard, "IsTriggerZ");
+
+ sm.Configure(State.B)
+ .OnEntryFrom(Trigger.X, TestEntryAction, "BX");
+
+ sm.Configure(State.C)
+ .OnEntryFrom(parmTrig, TestEntryActionString);
+
+ var result = Graph.MermaidGraph.Format(sm.GetInfo());
+
+ WriteToFile(nameof(TransitionWithIgnore), result);
+
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void SpacedWithSubstate()
+ {
+ string StateA = "State A";
+ string StateB = "State B";
+ string StateC = "State C";
+ string StateD = "State D";
+ string TriggerX = "Trigger X";
+ string TriggerY = "Trigger Y";
+
+ var expected = new StringBuilder()
+ .AppendLine("stateDiagram-v2")
+ .AppendLine(" StateD : State D")
+ .AppendLine(" StateB : State B")
+ .AppendLine(" StateC : State C")
+ .AppendLine(" StateA : State A")
+ .AppendLine(" state StateD {")
+ .AppendLine(" StateB")
+ .AppendLine(" StateC")
+ .AppendLine(" }")
+ .AppendLine(" StateA --> StateB : Trigger X")
+ .AppendLine(" StateA --> StateC : Trigger Y")
+ .AppendLine("[*] --> StateA")
+ .ToString().TrimEnd();
+
+ var sm = new StateMachine("State A");
+
+ sm.Configure(StateA)
+ .Permit(TriggerX, StateB)
+ .Permit(TriggerY, StateC)
+ .OnEntry(TestEntryAction, "Enter A")
+ .OnExit(TestEntryAction, "Exit A");
+
+ sm.Configure(StateB)
+ .SubstateOf(StateD);
+ sm.Configure(StateC)
+ .SubstateOf(StateD);
+ sm.Configure(StateD)
+ .OnEntry(TestEntryAction, "Enter D");
+
+ var result = Graph.MermaidGraph.Format(sm.GetInfo());
+
+ WriteToFile(nameof(SpacedWithSubstate), result);
+
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void WithSubstate()
+ {
+ var expected = new StringBuilder()
+ .AppendLine("stateDiagram-v2")
+ .AppendLine(" state D {")
+ .AppendLine(" B")
+ .AppendLine(" C")
+ .AppendLine(" }")
+ .AppendLine(" A --> B : X")
+ .AppendLine(" A --> C : Y")
+ .AppendLine("[*] --> A")
+ .ToString().TrimEnd();
+
+ var sm = new StateMachine(State.A);
+
+ sm.Configure(State.A)
+ .Permit(Trigger.X, State.B)
+ .Permit(Trigger.Y, State.C);
+
+ sm.Configure(State.B)
+ .SubstateOf(State.D);
+ sm.Configure(State.C)
+ .SubstateOf(State.D);
+ sm.Configure(State.D);
+
+ var result = Graph.MermaidGraph.Format(sm.GetInfo());
+
+ WriteToFile(nameof(WithSubstate), result);
+
Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void StateNamesWithSpacesAreAliased()
+ {
+ var expected = new StringBuilder()
+ .AppendLine("stateDiagram-v2")
+ .AppendLine(" AA : A A")
+ .AppendLine(" AA_1 : A A")
+ .AppendLine(" AA_2 : A A")
+ .AppendLine(" AA --> B : X")
+ .AppendLine(" AA_1 --> B : X")
+ .AppendLine(" AA_2 --> B : X")
+ .AppendLine("[*] --> AA")
+ .ToString().TrimEnd();
+
+ var sm = new StateMachine("A A");
+
+ sm.Configure("A A").Permit(Trigger.X, "B");
+ sm.Configure("A A").Permit(Trigger.X, "B");
+ sm.Configure("A A").Permit(Trigger.X, "B");
+
+ var result = Graph.MermaidGraph.Format(sm.GetInfo());
+ WriteToFile(nameof(StateNamesWithSpacesAreAliased), result);
+
+ Assert.Equal(expected, result);
+ }
+
+ private bool IsTrue()
+ {
+ return true;
+ }
+
+ private void TestEntryAction() { }
+
+ private void TestEntryActionString(string val) { }
+
+ private void WriteToFile(string fileName, string content)
+ {
+#if WRITE_DOTS_TO_FOLDER
+ System.IO.File.WriteAllText(System.IO.Path.Combine("c:\\temp", $"{fileName}.txt"), content);
+#endif
}
}
}