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
93 changes: 93 additions & 0 deletions source/Lite.StateMachine.Tests/StateTests/CommandStateTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright Xeno Innovations, Inc. 2025
// See the LICENSE file in the project root for more information.

using System;
using System.Threading.Tasks;
using Lite.StateMachine.Tests.TestData;
using Lite.StateMachine.Tests.TestData.Models;
using Lite.StateMachine.Tests.TestData.Services;
using Lite.StateMachine.Tests.TestData.States.CommandL3DiStates;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace Lite.StateMachine.Tests.StateTests;

[TestClass]
public class CommandStateTests : TestBase
{
[TestMethod]
public async Task BasicState_Override_Executes_SuccessAsync()
{
// Assemble with Dependency Injection
var services = new ServiceCollection()
.AddLogging(InlineTraceLogger(LogLevel.Trace))
.AddSingleton<IMessageService, MessageService>()
.AddSingleton<IEventAggregator, EventAggregator>()
.BuildServiceProvider();

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

var ctxProperties = new PropertyBag()
{
{ ParameterType.Counter, 0 },
};

var machine = new StateMachine<StateId>(factory, events)
{
// Make sure we don't get stuck.
// And send some message after leaving Command state
// to make sure we unsubscribed successfully.
DefaultStateTimeoutMs = 3000,
IsContextPersistent = true,
};

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

events.Subscribe(msg =>
{
if (msg is ICustomCommand)
{
if (msg is UnlockCommand cmd)
{
// +100 check so we don't trigger this a 2nd time.
if (cmd.Counter > 100 && cmd.Counter < 200)
return;

// NOTE:
// First we purposely publish 'OpenCommand' to prove that our OnMessage
// filters out the bad message, followed by publishing the REAL message.
if (cmd.Counter < 200)
events.Publish(new UnlockCommand { Counter = cmd.Counter + 100 });

events.Publish(new UnlockResponse { Counter = cmd.Counter + 100 });

// NOTE: This doesn't reach State2_Sub2_Sub2 because it already left (GOOD)
events.Publish(new CloseResponse { Counter = cmd.Counter + 100 });
}
}
});

// Act - Start your engine!
await machine.RunAsync(StateId.State1, ctxProperties, null, TestContext.CancellationToken);

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

Assert.AreEqual(29, msgService.Counter1);
Assert.AreEqual(13, msgService.Counter2, "State2 Context.Param Count");
Assert.AreEqual(12, msgService.Counter3);
Assert.AreEqual(2, msgService.Counter4);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ public class CompositeStateTest : TestBase
public const string ParameterSubStateEntered = "SubEntered";
public const string SUCCESS = "success";

public TestContext TestContext { get; set; }

[TestMethod]
public async Task Level1_Basic_RegisterHelpers_SuccessTestAsync()
{
Expand Down
2 changes: 0 additions & 2 deletions source/Lite.StateMachine.Tests/StateTests/CustomStateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ namespace Lite.StateMachine.Tests.StateTests;
[TestClass]
public class CustomStateTests : TestBase
{
public TestContext TestContext { get; set; }

[TestMethod]
[DataRow(false, DisplayName = "Don't skip State3")]
[DataRow(true, DisplayName = "Skip State3")]
Expand Down
2 changes: 2 additions & 0 deletions source/Lite.StateMachine.Tests/StateTests/TestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

public class TestBase
{
public TestContext TestContext { get; set; }

Check warning on line 12 in source/Lite.StateMachine.Tests/StateTests/TestBase.cs

View workflow job for this annotation

GitHub Actions / build-test-pack

Non-nullable property 'TestContext' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 12 in source/Lite.StateMachine.Tests/StateTests/TestBase.cs

View workflow job for this annotation

GitHub Actions / build-test-pack

Non-nullable property 'TestContext' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

/// <summary>ILogger Helper for generating clean in-line logs.</summary>
/// <param name="logLevel">Log level (Default: Trace).</param>
/// <returns><see cref="ILoggingBuilder"/>.</returns>
Expand Down
31 changes: 31 additions & 0 deletions source/Lite.StateMachine.Tests/TestData/Models/CustomCommands.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright Xeno Innovations, Inc. 2025
// See the LICENSE file in the project root for more information.

namespace Lite.StateMachine.Tests.TestData.Models;

#pragma warning disable SA1649 // File name should match first type name
#pragma warning disable SA1402 // File may only contain a single type

/// <summary>Signifies it's one of our event packets.</summary>
public interface ICustomCommand;

/// <summary>Sample command sent by state machine.</summary>
public class UnlockCommand : ICustomCommand
{
public int Counter { get; set; } = 0;
}

/// <summary>Sample command response received by state machine.</summary>
public class UnlockResponse : ICustomCommand
{
public int Counter { get; set; } = 0;
}

/// <summary>Sample command response received by state machine.</summary>
public class CloseResponse : ICustomCommand
{
public int Counter { get; set; } = 0;
}

#pragma warning restore SA1402 // File may only contain a single type
#pragma warning restore SA1649 // File name should match first type name
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ public interface IMessageService
/// <summary>Gets or sets the user's custom counter.</summary>
int Counter3 { get; set; }

/// <summary>Gets or sets the user's custom counter.</summary>
int Counter4 { get; set; }

/// <summary>Gets a list of user's custom messages.</summary>
List<string> Messages { get; }

Expand All @@ -40,6 +43,9 @@ public class MessageService : IMessageService
/// <inheritdoc/>
public int Counter3 { get; set; }

/// <inheritdoc/>
public int Counter4 { get; set; }

/// <inheritdoc/>
public List<string> Messages { get; } = [];

Expand Down
Loading