diff --git a/source/Lite.StateMachine.Tests/StateTests/BasicStateTests.cs b/source/Lite.StateMachine.Tests/StateTests/BasicStateTests.cs
index f3979c8..e5e05b4 100644
--- a/source/Lite.StateMachine.Tests/StateTests/BasicStateTests.cs
+++ b/source/Lite.StateMachine.Tests/StateTests/BasicStateTests.cs
@@ -3,6 +3,7 @@
using System;
using System.Linq;
+using System.Threading;
using System.Threading.Tasks;
using Lite.StateMachine.Tests.TestData;
using Lite.StateMachine.Tests.TestData.States;
@@ -10,10 +11,8 @@
namespace Lite.StateMachine.Tests.StateTests;
[TestClass]
-public class BasicStateTests
+public class BasicStateTests : TestBase
{
- public TestContext TestContext { get; set; }
-
/// Standard synchronous state registration exiting to completion.
[TestMethod]
public void Basic_RegisterState_Executes123_SuccessTest()
@@ -35,8 +34,9 @@ public void Basic_RegisterState_Executes123_SuccessTest()
task.GetAwaiter().GetResult();
// Assert Results
- Assert.IsNotNull(machine);
- Assert.IsNull(machine.Context);
+ AssertMachineNotNull(machine);
+
+ Assert.AreEqual(0, machine.Context.Parameters.Count);
// Ensure all states are registered
var enums = Enum.GetValues().Cast();
@@ -68,8 +68,7 @@ public async Task Basic_RegisterState_Executes123_SuccessTestAsync()
await machine.RunAsync(BasicStateId.State1, ctxProperties);
// Assert Results
- Assert.IsNotNull(machine);
- Assert.IsNull(machine.Context);
+ AssertMachineNotNull(machine);
// Ensure all states are registered
var enums = Enum.GetValues().Cast();
@@ -94,8 +93,7 @@ public async Task Basic_RegisterState_Executes132_SuccessTestAsync()
await machine.RunAsync(BasicStateId.State1, ctxProperties);
// Assert Results
- Assert.IsNotNull(machine);
- Assert.IsNull(machine.Context);
+ AssertMachineNotNull(machine);
// Ensure all states are registered
var enums = Enum.GetValues().Cast();
@@ -126,8 +124,7 @@ public async Task Basic_RegisterState_Fluent_SuccessTestAsync()
.RunAsync(BasicStateId.State1, ctxProperties, cancellationToken: TestContext.CancellationToken);
// Assert Results
- Assert.IsNotNull(machine);
- Assert.IsNull(machine.Context);
+ AssertMachineNotNull(machine);
// Ensure all states are registered
var enums = Enum.GetValues().Cast();
@@ -190,8 +187,7 @@ public async Task HungState_Proceeds_DefaultStateTimeout_SuccessTestAsync()
await machine.RunAsync(BasicStateId.State1, ctxProperties);
// Assert Results
- Assert.IsNotNull(machine);
- Assert.IsNull(machine.Context);
+ AssertMachineNotNull(machine);
// Ensure all states are registered
var enums = Enum.GetValues().Cast();
@@ -205,8 +201,6 @@ 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 ctxProperties = new PropertyBag()
{
@@ -224,15 +218,14 @@ public async Task RegisterState_ReturnsContext_SuccessTestAsync()
await task; // Non async method: task.GetAwaiter().GetResult();
// Assert Results
- Assert.IsNotNull(machine);
- Assert.IsNotNull(machine.Context);
+ AssertMachineNotNull(machine);
var ctxFinalParams = machine.Context.Parameters;
Assert.IsNotNull(ctxFinalParams);
- Assert.AreEqual(TestValue, ctxFinalParams[ParameterType.KeyTest]);
+ Assert.AreNotEqual(0, ctxFinalParams.Count);
// 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[ParameterType.Counter]);
+ ////Assert.AreEqual(9, ctxFinalParams[ParameterType.Counter]);
}
}
diff --git a/source/Lite.StateMachine.Tests/StateTests/CommandStateTests.cs b/source/Lite.StateMachine.Tests/StateTests/CommandStateTests.cs
index 7a5e325..ebd9f53 100644
--- a/source/Lite.StateMachine.Tests/StateTests/CommandStateTests.cs
+++ b/source/Lite.StateMachine.Tests/StateTests/CommandStateTests.cs
@@ -2,6 +2,8 @@
// See the LICENSE file in the project root for more information.
using System;
+using System.Collections.Generic;
+using System.Threading;
using System.Threading.Tasks;
using Lite.StateMachine.Tests.TestData;
using Lite.StateMachine.Tests.TestData.Models;
@@ -82,12 +84,97 @@ public async Task BasicState_Override_Executes_SuccessAsync()
await machine.RunAsync(StateId.State1, ctxProperties, null, TestContext.CancellationToken);
// Assert Results
- Assert.IsNotNull(machine);
- Assert.IsNull(machine.Context);
+ AssertMachineNotNull(machine);
Assert.AreEqual(29, msgService.Counter1);
Assert.AreEqual(13, msgService.Counter2, "State2 Context.Param Count");
Assert.AreEqual(12, msgService.Counter3);
Assert.AreEqual(2, msgService.Counter4);
}
+
+ [TestMethod]
+ public async Task CancelsInfiniteStateMachineTestAsync()
+ {
+ // Assemble with Dependency Injection
+ var services = new ServiceCollection()
+ .AddLogging(InlineTraceLogger(LogLevel.Trace))
+ .AddSingleton()
+ .AddSingleton()
+ .BuildServiceProvider();
+
+ var msgService = services.GetRequiredService();
+ var events = services.GetRequiredService();
+
+ Func factory = t => ActivatorUtilities.CreateInstance(services, t);
+
+ var machine = new StateMachine(factory, events)
+ .RegisterState(StateId.State1, StateId.State2)
+ .RegisterState(StateId.State2, StateId.State1);
+
+ var cts = new CancellationTokenSource();
+
+ var counter = 0;
+ events.Subscribe(msg =>
+ {
+ // All messages get received, even `CancelResponse`
+ if (msg is not CancelCommand cmd)
+ return;
+
+ counter++;
+ if (counter >= 100)
+ {
+ // Get outta here!!
+ cts.Cancel();
+ }
+
+ // Don't let it hang waiting for a response
+ events.Publish(new CancelResponse());
+ });
+
+ var result = await machine.RunAsync(StateId.State1, null, null, cts.Token);
+
+ // Assert
+ Assert.IsNotNull(result);
+ AssertMachineNotNull(machine);
+ Assert.AreEqual(100, counter);
+ }
+
+ private class InfState1 : IState
+ {
+ public Task OnEnter(Context context)
+ {
+ context.NextState(Result.Success);
+ return Task.CompletedTask;
+ }
+
+ public Task OnEntering(Context context) => Task.CompletedTask;
+
+ public Task OnExit(Context context) => Task.CompletedTask;
+ }
+
+ private class InfState2 : ICommandState
+ {
+ public IReadOnlyCollection SubscribedMessageTypes =>
+ [
+ typeof(CancelResponse),
+ ];
+
+ public Task OnEnter(Context context)
+ {
+ context.EventAggregator?.Publish(new CancelCommand());
+ return Task.CompletedTask;
+ }
+
+ public Task OnEntering(Context context) => Task.CompletedTask;
+
+ public Task OnExit(Context context) => Task.CompletedTask;
+
+ public Task OnMessage(Context context, object message)
+ {
+ context.NextState(Result.Success);
+ return Task.CompletedTask;
+ }
+
+ public Task OnTimeout(Context context) => Task.CompletedTask;
+ }
}
diff --git a/source/Lite.StateMachine.Tests/StateTests/CompositeStateTest.cs b/source/Lite.StateMachine.Tests/StateTests/CompositeStateTest.cs
index dc6e2e4..2545b92 100644
--- a/source/Lite.StateMachine.Tests/StateTests/CompositeStateTest.cs
+++ b/source/Lite.StateMachine.Tests/StateTests/CompositeStateTest.cs
@@ -54,8 +54,7 @@ public async Task Level1_Basic_RegisterHelpers_SuccessTestAsync()
Console.WriteLine(umlBasic);
// Assert
- Assert.IsNotNull(machine);
- Assert.IsNull(machine.Context);
+ AssertMachineNotNull(machine);
// Ensure all states are hit
var enums = Enum.GetValues().Cast();
@@ -82,8 +81,7 @@ public async Task Level1_Basic_RegisterState_SuccessTestAsync()
await machine.RunAsync(CompositeL1StateId.State1);
// Assert
- Assert.IsNotNull(machine);
- Assert.IsNull(machine.Context);
+ AssertMachineNotNull(machine);
// Ensure all states are hit
var enums = Enum.GetValues().Cast();
@@ -132,8 +130,7 @@ public async Task Level1_Fluent_RegisterHelpers_SuccessTestAsync()
.RunAsync(CompositeL1StateId.State1, cancellationToken: TestContext.CancellationToken);
// Assert
- Assert.IsNotNull(machine);
- Assert.IsNull(machine.Context);
+ AssertMachineNotNull(machine);
// Ensure all states are hit
var enums = Enum.GetValues().Cast();
@@ -159,8 +156,7 @@ public void Level1_Fluent_RegisterState_SuccessTest()
.GetResult();
// Assert
- Assert.IsNotNull(machine);
- Assert.IsNull(machine.Context);
+ AssertMachineNotNull(machine);
// Ensure all states are hit
var enums = Enum.GetValues().Cast();
@@ -223,8 +219,7 @@ public async Task Level3_IsContextPersistent_False_SuccessTestAsync(bool context
await machine.RunAsync(CompositeL3.State1, cancellationToken: TestContext.CancellationToken);
// Assert
- Assert.IsNotNull(machine);
- Assert.IsNull(machine.Context);
+ AssertMachineNotNull(machine);
// Ensure all states are registered
var enums = Enum.GetValues().Cast();
@@ -284,8 +279,7 @@ public async Task Level3_PreviousStateId_SuccessTestAsync()
await machine.RunAsync(CompositeL3.State1, ctxProperties, null, TestContext.CancellationToken);
// Assert
- Assert.IsNotNull(machine);
- Assert.IsNull(machine.Context);
+ AssertMachineNotNull(machine);
}
private static StateMachine GenerateStateMachineL3(StateMachine machine)
diff --git a/source/Lite.StateMachine.Tests/StateTests/ContextTests.cs b/source/Lite.StateMachine.Tests/StateTests/ContextTests.cs
index e7c4e41..cfe142c 100644
--- a/source/Lite.StateMachine.Tests/StateTests/ContextTests.cs
+++ b/source/Lite.StateMachine.Tests/StateTests/ContextTests.cs
@@ -9,7 +9,7 @@
namespace Lite.StateMachine.Tests.StateTests;
[TestClass]
-public class ContextTests
+public class ContextTests : TestBase
{
public const string ParameterCounter = "Counter";
public const string ParameterKeyTest = "TestKey";
@@ -29,8 +29,6 @@ private enum ParameterType
Param3,
}
- public TestContext TestContext { get; set; }
-
/// Standard synchronous state registration exiting to completion.
[TestMethod]
public void Basic_RegisterState_Executes123_SuccessTest()
@@ -48,8 +46,7 @@ public void Basic_RegisterState_Executes123_SuccessTest()
task.GetAwaiter().GetResult();
// Assert Results
- Assert.IsNotNull(machine);
- Assert.IsNull(machine.Context);
+ AssertMachineNotNull(machine);
// Ensure all states are registered
var enums = Enum.GetValues().Cast();
diff --git a/source/Lite.StateMachine.Tests/StateTests/CustomStateTests.cs b/source/Lite.StateMachine.Tests/StateTests/CustomStateTests.cs
index 0c7b9df..58fac61 100644
--- a/source/Lite.StateMachine.Tests/StateTests/CustomStateTests.cs
+++ b/source/Lite.StateMachine.Tests/StateTests/CustomStateTests.cs
@@ -45,8 +45,7 @@ public async Task BasicState_Override_Executes_SuccessAsync(bool skipState3)
await machine.RunAsync(CustomStateId.State1, ctxProperties, cancellationToken: TestContext.CancellationToken);
// Assert Results
- Assert.IsNotNull(machine);
- Assert.IsNull(machine.Context);
+ AssertMachineNotNull(machine);
Assert.AreEqual(1, msgService.Counter1);
Assert.AreEqual(0, msgService.Counter2, "State2Dummy should never enter");
@@ -83,8 +82,7 @@ await Assert.ThrowsExactlyAsync(()
=> machine.RunAsync(CustomStateId.State1, ctxProperties, null, TestContext.CancellationToken));
// Assert Results
- Assert.IsNotNull(machine);
- Assert.IsNull(machine.Context);
+ AssertMachineNotNull(machine);
Assert.AreEqual(0, msgService.Counter1);
Assert.AreEqual(0, msgService.Counter2, "State2Dummy should never enter");
@@ -125,8 +123,7 @@ public async Task Composite_Override_Executes_SuccessAsync(bool skipSubState2)
await machine.RunAsync(CustomStateId.State1, ctxProperties, cancellationToken: TestContext.CancellationToken);
// Assert Results
- Assert.IsNotNull(machine);
- Assert.IsNull(machine.Context);
+ AssertMachineNotNull(machine);
Assert.AreEqual(skipSubState2 ? 3 : 4, msgService.Counter1, "State Counter1 failed.");
Assert.AreEqual(0, msgService.Counter2, "State2Dummy should never enter");
diff --git a/source/Lite.StateMachine.Tests/StateTests/DiMsTests.cs b/source/Lite.StateMachine.Tests/StateTests/DiMsTests.cs
index 463a7bd..48bc1c0 100644
--- a/source/Lite.StateMachine.Tests/StateTests/DiMsTests.cs
+++ b/source/Lite.StateMachine.Tests/StateTests/DiMsTests.cs
@@ -18,7 +18,7 @@ namespace Lite.StateMachine.Tests.StateTests;
/// Microsoft Dependency Injection Tests.
[TestClass]
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1124:Do not use regions", Justification = "Allowed for this test class")]
-public class DiMsTests
+public class DiMsTests : TestBase
{
[TestMethod]
public async Task Basic_FlatStates_SuccessTestAsync()
@@ -44,8 +44,7 @@ public async Task Basic_FlatStates_SuccessTestAsync()
var result = await machine.RunAsync(BasicStateId.State1);
Assert.IsNotNull(result);
- Assert.IsNotNull(machine);
- Assert.IsNull(machine.Context);
+ AssertMachineNotNull(machine);
var msgService = services.GetRequiredService();
Assert.AreEqual(9, msgService.Counter1, "Message service should have 9 from the 3 states.");
@@ -142,8 +141,7 @@ public async Task Basic_LogLevelNone_SuccessTestAsync()
var result = await machine.RunAsync(BasicStateId.State1);
Assert.IsNotNull(result);
- Assert.IsNotNull(machine);
- Assert.IsNull(machine.Context);
+ AssertMachineNotNull(machine);
// Ensure all states are registered
var enums = Enum.GetValues().Cast();
@@ -277,6 +275,9 @@ public async Task RegisterState_MsDi_EventAggregatorOnly_SuccessTestAsync()
Console.WriteLine("MS.DI workflow finished.");
+ // Assert
+ AssertMachineNotNull(machine);
+
Assert.AreEqual(2, msgService.Counter2);
Assert.AreEqual(42, msgService.Counter1);
}
diff --git a/source/Lite.StateMachine.Tests/StateTests/TestBase.cs b/source/Lite.StateMachine.Tests/StateTests/TestBase.cs
index 241d697..0d385ff 100644
--- a/source/Lite.StateMachine.Tests/StateTests/TestBase.cs
+++ b/source/Lite.StateMachine.Tests/StateTests/TestBase.cs
@@ -9,7 +9,14 @@ namespace Lite.StateMachine.Tests.StateTests;
public class TestBase
{
- public TestContext TestContext { get; set; }
+ public required TestContext TestContext { get; set; }
+
+ protected static void AssertMachineNotNull(Lite.StateMachine.StateMachine machine)
+ where T : struct, Enum
+ {
+ Assert.IsNotNull(machine);
+ Assert.IsNotNull(machine.Context);
+ }
/// ILogger Helper for generating clean in-line logs.
/// Log level (Default: Trace).
diff --git a/source/Lite.StateMachine.Tests/TestData/Models/CustomCommands.cs b/source/Lite.StateMachine.Tests/TestData/Models/CustomCommands.cs
index b60b422..b394275 100644
--- a/source/Lite.StateMachine.Tests/TestData/Models/CustomCommands.cs
+++ b/source/Lite.StateMachine.Tests/TestData/Models/CustomCommands.cs
@@ -9,20 +9,27 @@ namespace Lite.StateMachine.Tests.TestData.Models;
/// Signifies it's one of our event packets.
public interface ICustomCommand;
-/// Sample command sent by state machine.
-public class UnlockCommand : ICustomCommand
+public class CancelCommand : ICustomCommand
{
- public int Counter { get; set; } = 0;
+ public int Counter { get; set; }
}
+public class CancelResponse : ICustomCommand;
+
/// Sample command response received by state machine.
-public class UnlockResponse : ICustomCommand
+public class CloseResponse : ICustomCommand
+{
+ public int Counter { get; set; } = 0;
+}
+
+/// Sample command sent by state machine.
+public class UnlockCommand : ICustomCommand
{
public int Counter { get; set; } = 0;
}
/// Sample command response received by state machine.
-public class CloseResponse : ICustomCommand
+public class UnlockResponse : ICustomCommand
{
public int Counter { get; set; } = 0;
}
diff --git a/source/Lite.StateMachine/StateMachine.cs b/source/Lite.StateMachine/StateMachine.cs
index 958a0d1..a16be39 100644
--- a/source/Lite.StateMachine/StateMachine.cs
+++ b/source/Lite.StateMachine/StateMachine.cs
@@ -65,7 +65,8 @@ public StateMachine(
}
///
- public Context Context { get; private set; } = default!;
+ public Context Context { get; private set; } = new Context(default, default, default!, null);
+ ////public Context Context { get; private set; } = default!;
///
public int DefaultCommandTimeoutMs { get; set; } = 3000;
@@ -209,6 +210,9 @@ public async Task> RunAsync(
TStateId? prevStateId = null;
var currentStateId = initialStateId;
+ // TBD
+ ////Context = new Context(currentStateId, default, default!, null);
+
while (!cancellationToken.IsCancellationRequested)
{
var reg = GetRegistration(currentStateId);
@@ -222,11 +226,15 @@ public async Task> RunAsync(
//// Errors = errorStack ?? [],
////};
+ ////Context.Parameters = parameterStack ?? [];
+ ////Context.Errors = errorStack ?? [];
+
parameterStack ??= [];
errorStack ??= [];
// Run any state (composite or leaf) recursively.
var result = await RunAnyStateRecursiveAsync(reg, parameterStack, errorStack, cancellationToken).ConfigureAwait(false);
+ ////var result = await RunAnyStateRecursiveAsync(reg, cancellationToken).ConfigureAwait(false);
if (result is null)
break;
@@ -292,11 +300,12 @@ private StateRegistration GetRegistration(TStateId stateId)
CancellationToken ct)
{
// Ensure we always operate on non-null, shared bags
- parameters ??= [];
- errors ??= [];
+ ////parameters ??= [];
+ ////errors ??= [];
// Run Normal or Command State
if (!reg.IsCompositeParent)
+ ////return await RunLeafAsync(reg, ct).ConfigureAwait(false);
return await RunLeafAsync(reg, parameters, errors, ct).ConfigureAwait(false);
// Composite States