Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 38 additions & 22 deletions source/Lite.StateMachine.Tests/StateTests/BasicStateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,18 @@ namespace Lite.StateMachine.Tests.StateTests;
[TestClass]
public class BasicStateTests
{
public const string ParameterCounter = "Counter";
public const string ParameterKeyTest = "TestKey";
public const string TestValue = "success";

public TestContext TestContext { get; set; }

/// <summary>Standard synchronous state registration exiting to completion.</summary>
[TestMethod]
public void Basic_RegisterState_Executes123_SuccessTest()
{
// Assemble
var counter = 0;
var ctxProperties = new PropertyBag() { { ParameterCounter, counter } };
var ctxProperties = new PropertyBag()
{
{ ParameterType.Counter, 0 },
{ ParameterType.TestExecutionOrder, true },
};

var machine = new StateMachine<BasicStateId>();
machine.RegisterState<BasicState1>(BasicStateId.State1, BasicStateId.State2);
Expand All @@ -41,7 +40,7 @@ public void Basic_RegisterState_Executes123_SuccessTest()

// Ensure all states are registered
var enums = Enum.GetValues<BasicStateId>().Cast<BasicStateId>();
Assert.AreEqual(enums.Count(), machine.States.Count());
Assert.HasCount(enums.Count(), machine.States);
Assert.IsTrue(enums.All(k => machine.States.Contains(k)));

// Ensure they're registered in order
Expand All @@ -54,8 +53,11 @@ public void Basic_RegisterState_Executes123_SuccessTest()
public async Task Basic_RegisterState_Executes123_SuccessTestAsync()
{
// Assemble
var counter = 0;
var ctxProperties = new PropertyBag() { { ParameterCounter, counter } };
var ctxProperties = new PropertyBag()
{
{ ParameterType.Counter, 0 },
{ ParameterType.TestExecutionOrder, true },
};

var machine = new StateMachine<BasicStateId>();
machine.RegisterState<BasicState1>(BasicStateId.State1, BasicStateId.State2);
Expand All @@ -71,7 +73,7 @@ public async Task Basic_RegisterState_Executes123_SuccessTestAsync()

// Ensure all states are registered
var enums = Enum.GetValues<BasicStateId>().Cast<BasicStateId>();
Assert.AreEqual(enums.Count(), machine.States.Count());
Assert.HasCount(enums.Count(), machine.States);
Assert.IsTrue(enums.All(k => machine.States.Contains(k)));

// Ensure they're registered in order
Expand All @@ -88,15 +90,16 @@ public async Task Basic_RegisterState_Executes132_SuccessTestAsync()
machine.RegisterState<BasicState2>(BasicStateId.State2);

// Act - Start your engine!
await machine.RunAsync(BasicStateId.State1);
var ctxProperties = new PropertyBag() { { ParameterType.TestExecutionOrder, false } };
await machine.RunAsync(BasicStateId.State1, ctxProperties);

// Assert Results
Assert.IsNotNull(machine);
Assert.IsNull(machine.Context);

// Ensure all states are registered
var enums = Enum.GetValues<BasicStateId>().Cast<BasicStateId>();
Assert.AreEqual(enums.Count(), machine.States.Count());
Assert.HasCount(enums.Count(), machine.States);
Assert.IsTrue(enums.All(k => machine.States.Contains(k)));

// Ensure they're NOT registered in order
Expand All @@ -109,8 +112,11 @@ public async Task Basic_RegisterState_Executes132_SuccessTestAsync()
public async Task Basic_RegisterState_Fluent_SuccessTestAsync()
{
// Assemble
var counter = 0;
var ctxProperties = new PropertyBag() { { ParameterCounter, counter } };
var ctxProperties = new PropertyBag()
{
{ ParameterType.Counter, 0 },
{ ParameterType.TestExecutionOrder, true },
};

// Assemble/Act - Start your engine!
var machine = await new StateMachine<BasicStateId>()
Expand All @@ -125,7 +131,7 @@ public async Task Basic_RegisterState_Fluent_SuccessTestAsync()

// Ensure all states are registered
var enums = Enum.GetValues<BasicStateId>().Cast<BasicStateId>();
Assert.AreEqual(enums.Count(), machine.States.Count());
Assert.HasCount(enums.Count(), machine.States);
Assert.IsTrue(enums.All(k => machine.States.Contains(k)));

// Ensure they're registered in order
Expand Down Expand Up @@ -168,23 +174,28 @@ public async Task HungState_Proceeds_DefaultStateTimeout_SuccessTestAsync()
var machine = new StateMachine<BasicStateId>();

// This test will take 1 full second to complete versus
var paramStack = new PropertyBag() { { ParameterType.HungStateAvoidance, true } };
var ctxProperties = new PropertyBag()
{
{ ParameterType.TestHungStateAvoidance, true },
{ ParameterType.TestExecutionOrder, true },
};

machine.DefaultStateTimeoutMs = 1000;

machine.RegisterState<BasicState1>(BasicStateId.State1, BasicStateId.State2);
machine.RegisterState<BasicState2>(BasicStateId.State2, BasicStateId.State3);
machine.RegisterState<BasicState3>(BasicStateId.State3);

// Act - Start your engine!
await machine.RunAsync(BasicStateId.State1, paramStack);
await machine.RunAsync(BasicStateId.State1, ctxProperties);

// Assert Results
Assert.IsNotNull(machine);
Assert.IsNull(machine.Context);

// Ensure all states are registered
var enums = Enum.GetValues<BasicStateId>().Cast<BasicStateId>();
Assert.AreEqual(enums.Count(), machine.States.Count());
Assert.HasCount(enums.Count(), machine.States);
Assert.IsTrue(enums.All(k => machine.States.Contains(k)));
}

Expand All @@ -194,16 +205,21 @@ public async Task HungState_Proceeds_DefaultStateTimeout_SuccessTestAsync()
[Ignore("vNext - Currently StateMachine destroys context after run completes.")]
public async Task RegisterState_ReturnsContext_SuccessTestAsync()
{
const string TestValue = "success";

// Assemble
var counter = 0;
var ctxProperties = new PropertyBag()
{
{ ParameterType.Counter, 0 },
{ ParameterType.TestExecutionOrder, true },
};

var machine = new StateMachine<BasicStateId>();
machine.RegisterState<BasicState1>(BasicStateId.State1, BasicStateId.State2);
machine.RegisterState<BasicState2>(BasicStateId.State2, BasicStateId.State3);
machine.RegisterState<BasicState3>(BasicStateId.State3);

// Act - Start your engine!
var ctxProperties = new PropertyBag() { { ParameterCounter, counter } };
var task = machine.RunAsync(BasicStateId.State1, ctxProperties);
await task; // Non async method: task.GetAwaiter().GetResult();

Expand All @@ -213,10 +229,10 @@ public async Task RegisterState_ReturnsContext_SuccessTestAsync()

var ctxFinalParams = machine.Context.Parameters;
Assert.IsNotNull(ctxFinalParams);
Assert.AreEqual(TestValue, ctxFinalParams[ParameterKeyTest]);
Assert.AreEqual(TestValue, ctxFinalParams[ParameterType.KeyTest]);

// NOTE: This should be 9 because each state has 3 hooks that increment the counter
// TODO (2025-12-22 DS): Fix last state not calling OnExit.
Assert.AreEqual(9, ctxFinalParams[ParameterCounter]);
Assert.AreEqual(9, ctxFinalParams[ParameterType.Counter]);
}
}
78 changes: 48 additions & 30 deletions source/Lite.StateMachine.Tests/StateTests/CompositeStateTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public async Task Level1_Basic_RegisterHelpers_SuccessTestAsync()

// Ensure all states are hit
var enums = Enum.GetValues<CompositeL1StateId>().Cast<CompositeL1StateId>();
Assert.AreEqual(enums.Count(), machine.States.Count());
Assert.HasCount(enums.Count(), machine.States);
Assert.IsTrue(enums.All(k => machine.States.Contains(k)));

// Ensure they're in order
Expand Down Expand Up @@ -89,7 +89,7 @@ public async Task Level1_Basic_RegisterState_SuccessTestAsync()

// Ensure all states are hit
var enums = Enum.GetValues<CompositeL1StateId>().Cast<CompositeL1StateId>();
Assert.AreEqual(enums.Count(), machine.States.Count());
Assert.HasCount(enums.Count(), machine.States);
Assert.IsTrue(enums.All(k => machine.States.Contains(k)));

// Ensure they're in order
Expand Down Expand Up @@ -139,7 +139,7 @@ public async Task Level1_Fluent_RegisterHelpers_SuccessTestAsync()

// Ensure all states are hit
var enums = Enum.GetValues<CompositeL1StateId>().Cast<CompositeL1StateId>();
Assert.AreEqual(enums.Count(), machine.States.Count());
Assert.HasCount(enums.Count(), machine.States);
Assert.IsTrue(enums.All(k => machine.States.Contains(k)));

// Ensure they're in order
Expand All @@ -166,7 +166,7 @@ public void Level1_Fluent_RegisterState_SuccessTest()

// Ensure all states are hit
var enums = Enum.GetValues<CompositeL1StateId>().Cast<CompositeL1StateId>();
Assert.AreEqual(enums.Count(), machine.States.Count());
Assert.HasCount(enums.Count(), machine.States);
Assert.IsTrue(enums.All(k => machine.States.Contains(k)));

// Ensure they're in order
Expand All @@ -186,20 +186,10 @@ public async Task Level3_ExportUml_SuccessTestAsync()
var msgService = services.GetRequiredService<IMessageService>();
Func<Type, object?> factory = t => ActivatorUtilities.CreateInstance(services, t);

var machine = new StateMachine<CompositeL3>(factory);

machine
.RegisterState<State1>(CompositeL3.State1, CompositeL3.State2)
.RegisterComposite<State2>(CompositeL3.State2, initialChildStateId: CompositeL3.State2_Sub1, onSuccess: CompositeL3.State3)
.RegisterSubState<State2_Sub1>(CompositeL3.State2_Sub1, parentStateId: CompositeL3.State2, onSuccess: CompositeL3.State2_Sub2)
.RegisterSubComposite<State2_Sub2>(CompositeL3.State2_Sub2, parentStateId: CompositeL3.State2, initialChildStateId: CompositeL3.State2_Sub2_Sub1, onSuccess: CompositeL3.State2_Sub3)
.RegisterSubState<State2_Sub2_Sub1>(CompositeL3.State2_Sub2_Sub1, parentStateId: CompositeL3.State2_Sub2, onSuccess: CompositeL3.State2_Sub2_Sub2)
.RegisterSubState<State2_Sub2_Sub2>(CompositeL3.State2_Sub2_Sub2, parentStateId: CompositeL3.State2_Sub2, onSuccess: CompositeL3.State2_Sub2_Sub3)
.RegisterSubState<State2_Sub2_Sub3>(CompositeL3.State2_Sub2_Sub3, parentStateId: CompositeL3.State2_Sub2, onSuccess: null)
.RegisterSubState<State2_Sub3>(CompositeL3.State2_Sub3, parentStateId: CompositeL3.State2, onSuccess: null)
.RegisterState<State3>(CompositeL3.State3, onSuccess: null);

// Act
var machine = GenerateStateMachineL3(new StateMachine<CompositeL3>(factory));
var uml = machine.ExportUml([CompositeL3.State1], includeLegend: false);

Assert.IsNotNull(uml);
Console.WriteLine(uml);
}
Expand Down Expand Up @@ -229,18 +219,7 @@ public async Task Level3_IsContextPersistent_False_SuccessTestAsync(bool context
var msgService = services.GetRequiredService<IMessageService>();
Func<Type, object?> factory = t => ActivatorUtilities.CreateInstance(services, t);

var machine = new StateMachine<CompositeL3>(factory, null, isContextPersistent: contextIsPersistent);

machine
.RegisterState<State1>(CompositeL3.State1, CompositeL3.State2)
.RegisterComposite<State2>(CompositeL3.State2, initialChildStateId: CompositeL3.State2_Sub1, onSuccess: CompositeL3.State3)
.RegisterSubState<State2_Sub1>(CompositeL3.State2_Sub1, parentStateId: CompositeL3.State2, onSuccess: CompositeL3.State2_Sub2)
.RegisterSubComposite<State2_Sub2>(CompositeL3.State2_Sub2, parentStateId: CompositeL3.State2, initialChildStateId: CompositeL3.State2_Sub2_Sub1, onSuccess: CompositeL3.State2_Sub3)
.RegisterSubState<State2_Sub2_Sub1>(CompositeL3.State2_Sub2_Sub1, parentStateId: CompositeL3.State2_Sub2, onSuccess: CompositeL3.State2_Sub2_Sub2)
.RegisterSubState<State2_Sub2_Sub2>(CompositeL3.State2_Sub2_Sub2, parentStateId: CompositeL3.State2_Sub2, onSuccess: CompositeL3.State2_Sub2_Sub3)
.RegisterSubState<State2_Sub2_Sub3>(CompositeL3.State2_Sub2_Sub3, parentStateId: CompositeL3.State2_Sub2, onSuccess: null)
.RegisterSubState<State2_Sub3>(CompositeL3.State2_Sub3, parentStateId: CompositeL3.State2, onSuccess: null)
.RegisterState<State3>(CompositeL3.State3, onSuccess: null);
var machine = GenerateStateMachineL3(new StateMachine<CompositeL3>(factory, null, isContextPersistent: contextIsPersistent));

// Act
await machine.RunAsync(CompositeL3.State1, cancellationToken: TestContext.CancellationToken);
Expand All @@ -251,7 +230,7 @@ public async Task Level3_IsContextPersistent_False_SuccessTestAsync(bool context

// Ensure all states are registered
var enums = Enum.GetValues<CompositeL3>().Cast<CompositeL3>();
Assert.AreEqual(enums.Count(), machine.States.Count());
Assert.HasCount(enums.Count(), machine.States);
Assert.IsTrue(enums.All(stateId => machine.States.Contains(stateId)));

// State Transition counter (9 states, 3 transitions)
Expand Down Expand Up @@ -287,4 +266,43 @@ public async Task Level3_IsContextPersistent_False_SuccessTestAsync(bool context
Assert.AreEqual(11, msgService.Counter3);
}
}

[TestMethod]
public async Task Level3_PreviousStateId_SuccessTestAsync()
{
// Assemble - Using DI for MessageService's counters
var services = new ServiceCollection()
.AddLogging(InlineTraceLogger(LogLevel.None))
.AddSingleton<IMessageService, MessageService>()
.BuildServiceProvider();

var msgService = services.GetRequiredService<IMessageService>();
Func<Type, object?> factory = t => ActivatorUtilities.CreateInstance(services, t);

var ctxProperties = new PropertyBag() { { ParameterType.TestExecutionOrder, true } };
var machine = GenerateStateMachineL3(new StateMachine<CompositeL3>(factory));

// Act
await machine.RunAsync(CompositeL3.State1, ctxProperties, null, TestContext.CancellationToken);

// Assert
Assert.IsNotNull(machine);
Assert.IsNull(machine.Context);
}

private static StateMachine<CompositeL3> GenerateStateMachineL3(StateMachine<CompositeL3> machine)
{
machine
.RegisterState<State1>(CompositeL3.State1, CompositeL3.State2)
.RegisterComposite<State2>(CompositeL3.State2, initialChildStateId: CompositeL3.State2_Sub1, onSuccess: CompositeL3.State3)
.RegisterSubState<State2_Sub1>(CompositeL3.State2_Sub1, parentStateId: CompositeL3.State2, onSuccess: CompositeL3.State2_Sub2)
.RegisterSubComposite<State2_Sub2>(CompositeL3.State2_Sub2, parentStateId: CompositeL3.State2, initialChildStateId: CompositeL3.State2_Sub2_Sub1, onSuccess: CompositeL3.State2_Sub3)
.RegisterSubState<State2_Sub2_Sub1>(CompositeL3.State2_Sub2_Sub1, parentStateId: CompositeL3.State2_Sub2, onSuccess: CompositeL3.State2_Sub2_Sub2)
.RegisterSubState<State2_Sub2_Sub2>(CompositeL3.State2_Sub2_Sub2, parentStateId: CompositeL3.State2_Sub2, onSuccess: CompositeL3.State2_Sub2_Sub3)
.RegisterSubState<State2_Sub2_Sub3>(CompositeL3.State2_Sub2_Sub3, parentStateId: CompositeL3.State2_Sub2, onSuccess: null)
.RegisterSubState<State2_Sub3>(CompositeL3.State2_Sub3, parentStateId: CompositeL3.State2, onSuccess: null)
.RegisterState<State3>(CompositeL3.State3, onSuccess: null);

return machine;
}
}
8 changes: 4 additions & 4 deletions source/Lite.StateMachine.Tests/StateTests/CustomStateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@ await Assert.ThrowsExactlyAsync<UnregisteredStateTransitionException>(()

[TestMethod]
[DataRow(false, DisplayName = "Run State2_Sub3")]
[DataRow(true, DisplayName = "Skip State2_Sub3")]
public async Task Composite_Override_Executes_SuccessAsync(bool skipSubState3)
[DataRow(true, DisplayName = "Skip State2_Sub2")]
public async Task Composite_Override_Executes_SuccessAsync(bool skipSubState2)
{
// Assemble with Dependency Injection
var services = new ServiceCollection()
Expand All @@ -110,7 +110,7 @@ public async Task Composite_Override_Executes_SuccessAsync(bool skipSubState3)
var ctxProperties = new PropertyBag()
{
{ ParameterType.Counter, 0 },
{ ParameterType.TestExitEarly2, skipSubState3 },
{ ParameterType.TestExitEarly2, skipSubState2 },
};

var machine = new StateMachine<CustomStateId>(factory, null, isContextPersistent: true);
Expand All @@ -130,7 +130,7 @@ public async Task Composite_Override_Executes_SuccessAsync(bool skipSubState3)
Assert.IsNotNull(machine);
Assert.IsNull(machine.Context);

Assert.AreEqual(skipSubState3 ? 2 : 3, msgService.Counter1, "State Counter1 failed.");
Assert.AreEqual(skipSubState2 ? 3 : 4, msgService.Counter1, "State Counter1 failed.");
Assert.AreEqual(0, msgService.Counter2, "State2Dummy should never enter");
Assert.AreEqual(1, msgService.Counter3, "Skip Substate Counter3 failed");
}
Expand Down
9 changes: 6 additions & 3 deletions source/Lite.StateMachine.Tests/TestData/ParameterType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,21 @@ public enum ParameterType
/// <summary>Generic counter.</summary>
Counter,

/// <summary>Tests for DoNotAllowHungStatesTest.</summary>
HungStateAvoidance,

/// <summary>Random test.</summary>
KeyTest,

/// <summary>Test states executed in order using context.LastStateId or states in a different order.</summary>
TestExecutionOrder,

/// <summary>Test triggers an early exit. Setting OnSuccess to NULL.</summary>
TestExitEarly,

/// <summary>Test triggers a 2nd early exit. Setting OnSuccess to NULL.</summary>
TestExitEarly2,

/// <summary>Tests for DoNotAllowHungStatesTest.</summary>
TestHungStateAvoidance,

/// <summary>Test trigger to go to an invalid state transition.</summary>
TestUnregisteredTransition,
}
Loading