diff --git a/.gitignore b/.gitignore index e10dac0..aca8fb7 100644 --- a/.gitignore +++ b/.gitignore @@ -47,4 +47,5 @@ tools/** !tools/packages.config # Artifacts -artifacts/** \ No newline at end of file +artifacts/** +/.vs diff --git a/src/Automatonymous.Tests/Dynamic Modify/Activity_Specs.cs b/src/Automatonymous.Tests/Dynamic Modify/Activity_Specs.cs new file mode 100644 index 0000000..92c7a4f --- /dev/null +++ b/src/Automatonymous.Tests/Dynamic Modify/Activity_Specs.cs @@ -0,0 +1,219 @@ +namespace Automatonymous.Tests.DynamicModify +{ + using NUnit.Framework; + + + [TestFixture(Category = "Dynamic Modify")] + public class When_specifying_an_event_activity + { + [Test] + public void Should_transition_to_the_proper_state() + { + Assert.AreEqual(Running, _instance.CurrentState); + } + + State Running; + Event Initialized; + + Instance _instance; + StateMachine _machine; + + [OneTimeSetUp] + public void Specifying_an_event_activity() + { + _instance = new Instance(); + _machine = AutomatonymousStateMachine + .Create(builder => builder + .State("Running", out Running) + .Event("Initialized", out Initialized) + .InstanceState(b => b.CurrentState) + .During(builder.Initial) + .When(Initialized, b => b.TransitionTo(Running)) + ); + + _machine.RaiseEvent(_instance, Initialized) + .Wait(); + } + + + class Instance + { + public State CurrentState { get; set; } + } + } + + + [TestFixture(Category = "Dynamic Modify")] + public class When_specifying_an_event_activity_using_initially + { + [Test] + public void Should_transition_to_the_proper_state() + { + Assert.AreEqual(Running, _instance.CurrentState); + } + + State Running; + Event Initialized; + + Instance _instance; + StateMachine _machine; + + [OneTimeSetUp] + public void Specifying_an_event_activity() + { + _instance = new Instance(); + _machine = AutomatonymousStateMachine + .Create(builder => builder + .State("Running", out Running) + .Event("Initialized", out Initialized) + .InstanceState(b => b.CurrentState) + .During(builder.Initial) + .When(Initialized, b => b.TransitionTo(Running)) + ); + + _machine.RaiseEvent(_instance, Initialized); + } + + + class Instance + { + public State CurrentState { get; set; } + } + + + class InstanceStateMachine : + AutomatonymousStateMachine + { + public InstanceStateMachine() + { + Initially( + When(Initialized) + .TransitionTo(Running)); + } + + public State Running { get; private set; } + + public Event Initialized { get; private set; } + } + } + + + [TestFixture(Category = "Dynamic Modify")] + public class When_specifying_an_event_activity_using_finally + { + [Test] + public void Should_have_called_the_finally_activity() + { + Assert.AreEqual(Finalized, _instance.Value); + } + + [Test] + public void Should_transition_to_the_proper_state() + { + Assert.AreEqual(_machine.Final, _instance.CurrentState); + } + + const string Finalized = "Finalized"; + + State Running; + Event Initialized; + + Instance _instance; + StateMachine _machine; + + [OneTimeSetUp] + public void Specifying_an_event_activity() + { + const string Finalized = "Finalized"; + + _instance = new Instance(); + _machine = AutomatonymousStateMachine + .Create(builder => builder + .State("Running", out Running) + .Event("Initialized", out Initialized) + .InstanceState(b => b.CurrentState) + .During(builder.Initial) + .When(Initialized, b => b.Finalize()) + .Finally(b => b.Then(context => context.Instance.Value = Finalized)) + ); + + _machine.RaiseEvent(_instance, Initialized) + .Wait(); + } + + + class Instance + { + public string Value { get; set; } + public State CurrentState { get; set; } + } + } + + + [TestFixture(Category = "Dynamic Modify")] + public class When_hooking_the_initial_enter_state_event + { + [Test] + public void Should_call_the_activity() + { + Assert.AreEqual(_machine.Final, _instance.CurrentState); + } + + [Test] + public void Should_have_trigger_the_final_before_enter_event() + { + Assert.AreEqual(Running, _instance.FinalState); + } + + [Test] + public void Should_have_triggered_the_after_leave_event() + { + Assert.AreEqual(_machine.Initial, _instance.LeftState); + } + + [Test] + public void Should_have_triggered_the_before_enter_event() + { + Assert.AreEqual(Initializing, _instance.EnteredState); + } + + State Running; + State Initializing; + Event Initialized; + + Instance _instance; + StateMachine _machine; + + [OneTimeSetUp] + public void Specifying_an_event_activity() + { + _instance = new Instance(); + _machine = AutomatonymousStateMachine + .Create(builder => builder + .State("Running", out Running) + .State("Initializing", out Initializing) + .Event("Initialized", out Initialized) + .InstanceState(b => b.CurrentState) + .During(Initializing) + .When(Initialized, b => b.TransitionTo(Running)) + .DuringAny() + .When(builder.Initial.Enter, b => b.TransitionTo(Initializing)) + .When(builder.Initial.AfterLeave, b => b.Then(context => context.Instance.LeftState = context.Data)) + .When(Initializing.BeforeEnter, b => b.Then(context => context.Instance.EnteredState = context.Data)) + .When(Running.Enter, b => b.Finalize()) + .When(builder.Final.BeforeEnter, b => b.Then(context => context.Instance.FinalState = context.Instance.CurrentState)) + ); + + _machine.RaiseEvent(_instance, Initialized) + .Wait(); + } + + class Instance + { + public State CurrentState { get; set; } + public State EnteredState { get; set; } + public State LeftState { get; set; } + public State FinalState { get; set; } + } + } +} diff --git a/src/Automatonymous.Tests/Dynamic Modify/AnyStateTransition_Specs.cs b/src/Automatonymous.Tests/Dynamic Modify/AnyStateTransition_Specs.cs new file mode 100644 index 0000000..beab03b --- /dev/null +++ b/src/Automatonymous.Tests/Dynamic Modify/AnyStateTransition_Specs.cs @@ -0,0 +1,65 @@ +namespace Automatonymous.Tests.DynamicModify +{ + using NUnit.Framework; + + + [TestFixture(Category = "Dynamic Modify")] + public class When_any_state_transition_occurs + { + [Test] + public void Should_be_running() + { + Assert.AreEqual(Running, _instance.CurrentState); + } + + [Test] + public void Should_have_entered_running() + { + Assert.AreEqual(Running, _instance.LastEntered); + } + + [Test] + public void Should_have_left_initial() + { + Assert.AreEqual(_machine.Initial, _instance.LastLeft); + } + + State Running; + Event Initialized; + Event Finish; + + Instance _instance; + StateMachine _machine; + + [OneTimeSetUp] + public void Setup() + { + _instance = new Instance(); + _machine = AutomatonymousStateMachine + .Create(builder => builder + .State("Running", out Running) + .Event("Initialized", out Initialized) + .Event("Finish", out Finish) + .InstanceState(b => b.CurrentState) + .During(builder.Initial) + .When(Initialized, b => b.TransitionTo(Running)) + .During(Running) + .When(Finish, b => b.Finalize()) + .BeforeEnterAny(b => b.Then(context => context.Instance.LastEntered = context.Data)) + .AfterLeaveAny(b => b.Then(context => context.Instance.LastLeft = context.Data)) + ); + + _machine.RaiseEvent(_instance, Initialized) + .Wait(); + } + + + class Instance + { + public State CurrentState { get; set; } + + public State LastEntered { get; set; } + public State LastLeft { get; set; } + } + } +} diff --git a/src/Automatonymous.Tests/Dynamic Modify/Anytime_Specs.cs b/src/Automatonymous.Tests/Dynamic Modify/Anytime_Specs.cs new file mode 100644 index 0000000..4e4d1d0 --- /dev/null +++ b/src/Automatonymous.Tests/Dynamic Modify/Anytime_Specs.cs @@ -0,0 +1,90 @@ +namespace Automatonymous.Tests.DynamicModify +{ + using System.Threading.Tasks; + using NUnit.Framework; + + + [TestFixture(Category = "Dynamic Modify")] + public class Anytime_events + { + [Test] + public async Task Should_be_called_regardless_of_state() + { + var instance = new Instance(); + + await _machine.RaiseEvent(instance, Init); + await _machine.RaiseEvent(instance, Hello); + + Assert.IsTrue(instance.HelloCalled); + Assert.AreEqual(_machine.Final, instance.CurrentState); + } + + [Test] + public async Task Should_have_value_of_event_data() + { + var instance = new Instance(); + + await _machine.RaiseEvent(instance, Init); + await _machine.RaiseEvent(instance, EventA, new A + { + Value = "Test" + }); + + Assert.AreEqual("Test", instance.AValue); + Assert.AreEqual(_machine.Final, instance.CurrentState); + } + + [Test] + public void Should_not_be_handled_on_initial() + { + var instance = new Instance(); + + Assert.That(async () => await _machine.RaiseEvent(instance, Hello), Throws.TypeOf()); + + Assert.IsFalse(instance.HelloCalled); + Assert.AreEqual(_machine.Initial, instance.CurrentState); + } + + State Ready; + Event Init; + Event Hello; + Event EventA; + + StateMachine _machine; + + [OneTimeSetUp] + public void A_state_is_declared() + { + _machine = AutomatonymousStateMachine + .Create(builder => builder + .State("Ready", out Ready) + .Event("Init", out Init) + .Event("Hello", out Hello) + .Event("EventA", out EventA) + .Initially() + .When(Init, b => b.TransitionTo(Ready)) + .DuringAny() + .When(Hello, b => b + .Then(context => context.Instance.HelloCalled = true) + .Finalize() + ) + .When(EventA, b => b + .Then(context => context.Instance.AValue = context.Data.Value) + .Finalize() + ) + ); + } + + class A + { + public string Value { get; set; } + } + + class Instance + { + public bool HelloCalled { get; set; } + public string AValue { get; set; } + public State CurrentState { get; set; } + } + } +} diff --git a/src/Automatonymous.Tests/Dynamic Modify/AsyncActivity_Specs.cs b/src/Automatonymous.Tests/Dynamic Modify/AsyncActivity_Specs.cs new file mode 100644 index 0000000..a0db641 --- /dev/null +++ b/src/Automatonymous.Tests/Dynamic Modify/AsyncActivity_Specs.cs @@ -0,0 +1,72 @@ +namespace Automatonymous.Tests.DynamicModify +{ + using System.Threading.Tasks; + using GreenPipes; + using NUnit.Framework; + + + [TestFixture(Category = "Dynamic Modify")] + public class Using_an_asynchronous_activity + { + [Test] + public async Task Should_capture_the_value() + { + Event Create = null; + + var claim = new TestInstance(); + var machine = AutomatonymousStateMachine + .Create(builder => builder + .State("Running", out State Running) + .Event("Create", out Create) + .InstanceState(b => b.CurrentState) + .During(builder.Initial) + .When(Create, b => b + .Execute(context => new SetValueAsyncActivity()) + .TransitionTo(Running) + ) + ); + + await machine.RaiseEvent(claim, Create, new CreateInstance()); + + Assert.AreEqual("ExecuteAsync", claim.Value); + } + + class TestInstance + { + public State CurrentState { get; set; } + public string Value { get; set; } + } + + class SetValueAsyncActivity : + Activity + { + async Task Activity.Execute(BehaviorContext context, + Behavior next) + { + context.Instance.Value = "ExecuteAsync"; + } + + Task Activity.Faulted( + BehaviorExceptionContext context, + Behavior next) + { + return next.Faulted(context); + } + + void Visitable.Accept(StateMachineVisitor visitor) + { + visitor.Visit(this); + } + + public void Probe(ProbeContext context) + { + } + } + + class CreateInstance + { + public int X { get; set; } + public int Y { get; set; } + } + } +} diff --git a/src/Automatonymous.Tests/Dynamic Modify/AutomatonymousStateMachine_Specs.cs b/src/Automatonymous.Tests/Dynamic Modify/AutomatonymousStateMachine_Specs.cs new file mode 100644 index 0000000..a27c0bc --- /dev/null +++ b/src/Automatonymous.Tests/Dynamic Modify/AutomatonymousStateMachine_Specs.cs @@ -0,0 +1,100 @@ +namespace Automatonymous.Tests.DynamicModify +{ + using System.Linq; + using NUnit.Framework; + + + [TestFixture(Category = "Dynamic Modify")] + public class Using_a_simple_state_machine + { + [Test] + public void Should_initialize_inherited_final_state_property() + { + var stateMachine = CreateStateMachine(); + + Assert.That(stateMachine.Final, Is.Not.Null); + } + + [Test] + public void Should_initialize_inherited_initial_state_property() + { + var stateMachine = CreateStateMachine(); + + Assert.That(stateMachine.Initial, Is.Not.Null); + } + + [Test] + public void Should_register_all_events() + { + var stateMachine = CreateStateMachine(); + + var events = stateMachine.Events.ToList(); + + Assert.That(events, Has.Count.EqualTo(2)); + } + + [Test] + public void Should_register_all_states() + { + var stateMachine = CreateStateMachine(); + + var states = stateMachine.States.ToList(); + + Assert.That(states, Has.Count.EqualTo(3)); + } + + [Test] + public void Should_register_declared_state() + { + var stateMachine = CreateStateMachine(); + + Assert.That(stateMachine.States, Contains.Item(ThisIsAState)); + } + + [Test] + public void Should_register_generic_event() + { + var stateMachine = CreateStateMachine(); + + Assert.That(stateMachine.Events, Contains.Item(ThisIsAnEventConsumingData)); + } + + [Test] + public void Should_register_inherited_final_state_property() + { + var stateMachine = CreateStateMachine(); + + Assert.That(stateMachine.States, Contains.Item(stateMachine.Final)); + } + + [Test] + public void Should_register_inherited_initial_state_property() + { + var stateMachine = CreateStateMachine(); + + Assert.That(stateMachine.States, Contains.Item(stateMachine.Initial)); + } + + [Test] + public void Should_register_simple_event() + { + var stateMachine = CreateStateMachine(); + + Assert.That(stateMachine.Events, Contains.Item(ThisIsASimpleEvent)); + } + + State ThisIsAState; + Event ThisIsASimpleEvent; + Event ThisIsAnEventConsumingData; + + class Instance { } + class EventData { } + + private StateMachine CreateStateMachine() + => AutomatonymousStateMachine.Create(builder => builder + .State("ThisIsAState", out ThisIsAState) + .Event("ThisIsASimpleEvent", out ThisIsASimpleEvent) + .Event("ThisIsAnEventConsumingData", out ThisIsAnEventConsumingData) + ); + } +} diff --git a/src/Automatonymous.Tests/Dynamic Modify/Combine_Specs.cs b/src/Automatonymous.Tests/Dynamic Modify/Combine_Specs.cs new file mode 100644 index 0000000..7c26e0a --- /dev/null +++ b/src/Automatonymous.Tests/Dynamic Modify/Combine_Specs.cs @@ -0,0 +1,178 @@ +namespace Automatonymous.Tests.DynamicModify +{ + using System.Threading.Tasks; + using NUnit.Framework; + + + [TestFixture(Category = "Dynamic Modify")] + public class When_combining_events_into_a_single_event + { + [Test] + public async Task Should_have_called_combined_event() + { + _machine = CreateStateMachine(); + _instance = new Instance(); + await _machine.RaiseEvent(_instance, Start); + + await _machine.RaiseEvent(_instance, First); + await _machine.RaiseEvent(_instance, Second); + + Assert.IsTrue(_instance.Called); + } + + [Test] + public async Task Should_not_call_for_one_event() + { + _machine = CreateStateMachine(); + _instance = new Instance(); + await _machine.RaiseEvent(_instance, Start); + + await _machine.RaiseEvent(_instance, First); + + Assert.IsFalse(_instance.Called); + } + + [Test] + public async Task Should_not_call_for_one_other_event() + { + _machine = CreateStateMachine(); + _instance = new Instance(); + await _machine.RaiseEvent(_instance, Start); + + await _machine.RaiseEvent(_instance, Second); + + Assert.IsFalse(_instance.Called); + } + + State Waiting; + Event Start; + Event First; + Event Second; + Event Third; + + StateMachine _machine; + Instance _instance; + + + class Instance + { + public CompositeEventStatus CompositeStatus { get; set; } + public bool Called { get; set; } + public State CurrentState { get; set; } + } + + private StateMachine CreateStateMachine() + { + return AutomatonymousStateMachine + .Create(builder => builder + .State("Waiting", out Waiting) + .Event("Start", out Start) + .Event("First", out First) + .Event("Second", out Second) + .Event("Third", out Third) + .CompositeEvent(Third, b => b.CompositeStatus, First, Second) + .Initially() + .When(Start, b => b.TransitionTo(Waiting)) + .During(Waiting) + .When(Third, b => b + .Then(context => context.Instance.Called = true) + .Finalize() + ) + ); + } + } + + + [TestFixture(Category = "Dynamic Modify")] + public class When_combining_events_with_an_int_for_state + { + [Test] + public async Task Should_have_called_combined_event() + { + _machine = CreateStateMachine(); + _instance = new Instance(); + await _machine.RaiseEvent(_instance, Start); + + Assert.IsFalse(_instance.Called); + + await _machine.RaiseEvent(_instance, First); + await _machine.RaiseEvent(_instance, Second); + + Assert.IsTrue(_instance.Called); + + Assert.AreEqual(2, _instance.CurrentState); + } + + [Test] + public async Task Should_have_initial_state_with_zero() + { + _machine = CreateStateMachine(); + _instance = new Instance(); + await _machine.RaiseEvent(_instance, Start); + + Assert.AreEqual(3, _instance.CurrentState); + } + + [Test] + public async Task Should_not_call_for_one_event() + { + _machine = CreateStateMachine(); + _instance = new Instance(); + await _machine.RaiseEvent(_instance, Start); + + await _machine.RaiseEvent(_instance, First); + + Assert.IsFalse(_instance.Called); + } + + [Test] + public async Task Should_not_call_for_one_other_event() + { + _machine = CreateStateMachine(); + _instance = new Instance(); + await _machine.RaiseEvent(_instance, Start); + + await _machine.RaiseEvent(_instance, Second); + + Assert.IsFalse(_instance.Called); + } + + State Waiting; + Event Start; + Event First; + Event Second; + Event Third; + + StateMachine _machine; + Instance _instance; + + + class Instance + { + public int CompositeStatus { get; set; } + public bool Called { get; set; } + public int CurrentState { get; set; } + } + + private StateMachine CreateStateMachine() + { + return AutomatonymousStateMachine + .Create(builder => builder + .State("Waiting", out Waiting) + .Event("Start", out Start) + .Event("First", out First) + .Event("Second", out Second) + .Event("Third", out Third) + .InstanceState(b => b.CurrentState) + .CompositeEvent(Third, b => b.CompositeStatus, First, Second) + .Initially() + .When(Start, b => b.TransitionTo(Waiting)) + .During(Waiting) + .When(Third, b => b + .Then(context => context.Instance.Called = true) + .Finalize() + ) + ); + } + } +} diff --git a/src/Automatonymous.Tests/Dynamic Modify/CompositeCondition_Specs.cs b/src/Automatonymous.Tests/Dynamic Modify/CompositeCondition_Specs.cs new file mode 100644 index 0000000..687ea64 --- /dev/null +++ b/src/Automatonymous.Tests/Dynamic Modify/CompositeCondition_Specs.cs @@ -0,0 +1,95 @@ +namespace Automatonymous.Tests.DynamicModify +{ + using System.Threading.Tasks; + using NUnit.Framework; + + + [TestFixture(Category = "Dynamic Modify")] + public class When_specifying_a_condition_on_a_composite_event + { + [Test] + public async Task Should_call_when_met() + { + _machine = CreateStateMachine(); + _instance = new Instance(); + await _machine.RaiseEvent(_instance, Start); + + await _machine.RaiseEvent(_instance, Second); + await _machine.RaiseEvent(_instance, First); + + Assert.IsTrue(_instance.Called); + Assert.IsTrue(_instance.SecondFirst); + } + + [Test] + public async Task Should_skip_when_not_met() + { + _machine = CreateStateMachine(); + _instance = new Instance(); + await _machine.RaiseEvent(_instance, Start); + + await _machine.RaiseEvent(_instance, First); + await _machine.RaiseEvent(_instance, Second); + + Assert.IsFalse(_instance.Called); + Assert.IsFalse(_instance.SecondFirst); + } + + State Waiting; + Event Start; + Event First; + Event Second; + Event Third; + + StateMachine _machine; + Instance _instance; + + + class Instance + { + public CompositeEventStatus CompositeStatus { get; set; } + public bool Called { get; set; } + public bool CalledAfterAll { get; set; } + public State CurrentState { get; set; } + public bool SecondFirst { get; set; } + public bool First { get; set; } + public bool Second { get; set; } + } + + private StateMachine CreateStateMachine() + { + return AutomatonymousStateMachine + .Create(builder => builder + .State("Waiting", out Waiting) + .Event("Start", out Start) + .Event("First", out First) + .Event("Second", out Second) + .Event("Third", out Third) + .Initially() + .When(Start, b => b.TransitionTo(Waiting)) + .During(Waiting) + .When(First, b => b.Then(context => + { + context.Instance.First = true; + context.Instance.CalledAfterAll = false; + })) + .When(Second, b => b.Then(context => + { + context.Instance.SecondFirst = !context.Instance.First; + context.Instance.Second = true; + context.Instance.CalledAfterAll = false; + })) + .CompositeEvent(Third, b => b.CompositeStatus, First, Second) + .During(Waiting) + .When(Third, context => context.Instance.SecondFirst, b => b + .Then(context => + { + context.Instance.Called = true; + context.Instance.CalledAfterAll = true; + }) + .Finalize() + ) + ); + } + } +} diff --git a/src/Automatonymous.Tests/Dynamic Modify/CompositeOrder_Specs.cs b/src/Automatonymous.Tests/Dynamic Modify/CompositeOrder_Specs.cs new file mode 100644 index 0000000..7e2e3fd --- /dev/null +++ b/src/Automatonymous.Tests/Dynamic Modify/CompositeOrder_Specs.cs @@ -0,0 +1,105 @@ +namespace Automatonymous.Tests.DynamicModify +{ + using System.Threading.Tasks; + using NUnit.Framework; + + + [TestFixture(Category = "Dynamic Modify")] + public class When_combining_events_into_a_single_event_happily + { + [Test] + public async Task Should_have_called_combined_event() + { + _machine = CreateStateMachine(); + _instance = new Instance(); + await _machine.RaiseEvent(_instance, Start); + + await _machine.RaiseEvent(_instance, First); + await _machine.RaiseEvent(_instance, Second); + + Assert.IsTrue(_instance.Called); + } + + [Test] + public async Task Should_have_called_combined_event_after_all_events() + { + _machine = CreateStateMachine(); + _instance = new Instance(); + await _machine.RaiseEvent(_instance, Start); + + await _machine.RaiseEvent(_instance, First); + await _machine.RaiseEvent(_instance, Second); + + Assert.IsTrue(_instance.CalledAfterAll); + } + + [Test] + public async Task Should_not_call_for_one_event() + { + _machine = CreateStateMachine(); + _instance = new Instance(); + await _machine.RaiseEvent(_instance, Start); + + await _machine.RaiseEvent(_instance, First); + + Assert.IsFalse(_instance.Called); + } + + [Test] + public async Task Should_not_call_for_one_other_event() + { + _machine = CreateStateMachine(); + _instance = new Instance(); + await _machine.RaiseEvent(_instance, Start); + + await _machine.RaiseEvent(_instance, Second); + + Assert.IsFalse(_instance.Called); + } + + class Instance + { + public CompositeEventStatus CompositeStatus { get; set; } + public bool Called { get; set; } + public bool CalledAfterAll { get; set; } + public State CurrentState { get; set; } + } + + State Waiting; + Event Start; + Event First; + Event Second; + Event Third; + + StateMachine _machine; + Instance _instance; + + private StateMachine CreateStateMachine() + { + return AutomatonymousStateMachine + .Create(builder => builder + .State("Waiting", out Waiting) + .Event("Start", out Start) + .Event("First", out First) + .Event("Second", out Second) + .Event("Third", out Third) + .Initially() + .When(Start, b => b.TransitionTo(Waiting)) + .During(Waiting) + .When(First, b => b.Then(context => { context.Instance.CalledAfterAll = false; })) + .When(Second, b => b.Then(context => { context.Instance.CalledAfterAll = false; })) + .CompositeEvent(Third, b => b.CompositeStatus, First, Second) + .During(Waiting) + .When(Third, b => b + .Then(context => + { + context.Instance.Called = true; + context.Instance.CalledAfterAll = true; + }) + .Finalize() + ) + + ); + } + } +} diff --git a/src/Automatonymous.Tests/Dynamic Modify/Condition_Specs.cs b/src/Automatonymous.Tests/Dynamic Modify/Condition_Specs.cs new file mode 100644 index 0000000..9f34d8c --- /dev/null +++ b/src/Automatonymous.Tests/Dynamic Modify/Condition_Specs.cs @@ -0,0 +1,199 @@ +namespace Automatonymous.Tests.DynamicModify +{ + using System; + using System.Threading.Tasks; + using NUnit.Framework; + + + [TestFixture(Category = "Dynamic Modify")] + public class Using_a_condition_in_a_state_machine + { + [Test] + public async Task Should_allow_if_condition_to_be_evaluated() + { + await _machine.RaiseEvent(_instance, ExplicitFilterStarted, new StartedExplicitFilter()); + + Assert.That(_instance.CurrentState, Is.Not.EqualTo(ShouldNotBeHere)); + } + + [Test] + public async Task Should_allow_the_condition_to_be_used() + { + await _machine.RaiseEvent(_instance, Started, new Start {InitializeOnly = true}); + + Assert.That(_instance.CurrentState, Is.EqualTo(Initialized)); + } + + [Test] + public async Task Should_evaluate_else_statement_when_if_condition__is_false() + { + await _machine.RaiseEvent(_instance, ExplicitFilterStarted, new StartedExplicitFilter()); + + Assert.That(_instance.ShouldBeCalled, Is.True); + } + + [Test] + public async Task Should_work() + { + await _machine.RaiseEvent(_instance, Started, new Start()); + + Assert.That(_instance.CurrentState, Is.EqualTo(Running)); + } + + + [SetUp] + public void Specifying_an_event_activity() + { + _instance = new Instance(); + _machine = AutomatonymousStateMachine + .Create(builder => builder + .State("Running", out Running) + .State("Initialized", out Initialized) + .State("ShouldNotBeHere", out ShouldNotBeHere) + .Event("Started", out Started) + .Event("ExplicitFilterStarted", out ExplicitFilterStarted) + .Event("Finish", out Finish) + .During(builder.Initial) + .When(Started, b => b + .Then(context => context.Instance.InitializeOnly = context.Data.InitializeOnly) + .If(context => context.Data.InitializeOnly, x => x.Then(context => Console.WriteLine("Initializing Only!"))) + .TransitionTo(Initialized) + ) + .During(builder.Initial) + .When(ExplicitFilterStarted, context => true, b => b + .IfElse(context => false, + binder => binder + .Then(context => Console.WriteLine("Should not be here!")) + .TransitionTo(ShouldNotBeHere), + binder => binder + .Then(context => context.Instance.ShouldBeCalled = true) + .Then(context => Console.WriteLine("Initializing Only!"))) + ) + .During(Running) + .When(Finish, b => b.Finalize()) + .WhenEnter(Initialized, b => b.If(context => !context.Instance.InitializeOnly, b => b.TransitionTo(Running))) + ); + } + + State Running; + State Initialized; + State ShouldNotBeHere; + Event Started; + Event ExplicitFilterStarted; + Event Finish; + + Instance _instance; + StateMachine _machine; + + class Instance + { + public bool InitializeOnly { get; set; } + public State CurrentState { get; set; } + + public bool ShouldBeCalled { get; set; } + } + + class Start + { + public bool InitializeOnly { get; set; } + } + + class StartedExplicitFilter { } + } + + + [TestFixture(Category = "Dynamic Modify")] + public class Using_an_async_condition_in_a_state_machine + { + [Test] + public async Task Should_allow_if_condition_to_be_evaluated() + { + await _machine.RaiseEvent(_instance, ExplicitFilterStarted, new StartedExplicitFilter()); + + Assert.That(_instance.CurrentState, Is.Not.EqualTo(ShouldNotBeHere)); + } + + [Test] + public async Task Should_allow_the_condition_to_be_used() + { + await _machine.RaiseEvent(_instance, Started, new Start {InitializeOnly = true}); + + Assert.That(_instance.CurrentState, Is.EqualTo(Initialized)); + } + + [Test] + public async Task Should_evaluate_else_statement_when_if_condition__is_false() + { + await _machine.RaiseEvent(_instance, ExplicitFilterStarted, new StartedExplicitFilter()); + + Assert.That(_instance.ShouldBeCalled, Is.True); + } + + [Test] + public async Task Should_work() + { + await _machine.RaiseEvent(_instance, Started, new Start()); + + Assert.That(_instance.CurrentState, Is.EqualTo(Running)); + } + + State Running; + State Initialized; + State ShouldNotBeHere; + Event Started; + Event ExplicitFilterStarted; + Event Finish; + + Instance _instance; + StateMachine _machine; + + class Instance + { + public bool InitializeOnly { get; set; } + public State CurrentState { get; set; } + public bool ShouldBeCalled { get; set; } + } + + [SetUp] + public void Specifying_an_event_activity() + { + _instance = new Instance(); + _machine = AutomatonymousStateMachine + .Create(builder => builder + .State("Running", out Running) + .State("Initialized", out Initialized) + .State("ShouldNotBeHere", out ShouldNotBeHere) + .Event("Started", out Started) + .Event("ExplicitFilterStarted", out ExplicitFilterStarted) + .Event("Finish", out Finish) + .During(builder.Initial) + .When(Started, b => b + .Then(context => context.Instance.InitializeOnly = context.Data.InitializeOnly) + .IfAsync(context => Task.FromResult(context.Data.InitializeOnly), + x => x.Then(context => Console.WriteLine("Initializing Only!"))) + .TransitionTo(Initialized) + ) + .During(builder.Initial) + .When(ExplicitFilterStarted, context => true, b => b + .IfElseAsync(context => Task.FromResult(false), + binder => binder + .Then(context => Console.WriteLine("Should not be here!")) + .TransitionTo(ShouldNotBeHere), + binder => binder + .Then(context => context.Instance.ShouldBeCalled = true) + .Then(context => Console.WriteLine("Initializing Only!"))) + ) + .During(Running) + .When(Finish, b => b.Finalize()) + .WhenEnter(Initialized, b => b.IfAsync(context => Task.FromResult(!context.Instance.InitializeOnly), b => b.TransitionTo(Running))) + ); + } + + class Start + { + public bool InitializeOnly { get; set; } + } + + class StartedExplicitFilter { } + } +} diff --git a/src/Automatonymous.Tests/Dynamic Modify/DataActivity_Specs.cs b/src/Automatonymous.Tests/Dynamic Modify/DataActivity_Specs.cs new file mode 100644 index 0000000..bf795b4 --- /dev/null +++ b/src/Automatonymous.Tests/Dynamic Modify/DataActivity_Specs.cs @@ -0,0 +1,75 @@ +namespace Automatonymous.Tests.DynamicModify +{ + using NUnit.Framework; + + + [TestFixture(Category = "Dynamic Modify")] + public class When_specifying_an_event_activity_with_data + { + [Test] + public void Should_capture_passed_value() + { + Assert.AreEqual(47, _instance.OtherValue); + } + + [Test] + public void Should_have_the_proper_value() + { + Assert.AreEqual("Hello", _instance.Value); + } + + [Test] + public void Should_transition_to_the_proper_state() + { + Assert.AreEqual(Running, _instance.CurrentState); + } + + State Running; + Event Initialized; + Event PassedValue; + + Instance _instance; + StateMachine _machine; + + [OneTimeSetUp] + public void Specifying_an_event_activity_with_data() + { + _instance = new Instance(); + _machine = AutomatonymousStateMachine + .Create(builder => builder + .State("Running", out Running) + .Event("Initialized", out Initialized) + .Event("PassedValue", out PassedValue) + .During(builder.Initial) + .When(Initialized, b => b + .Then(context => context.Instance.Value = context.Data.Value) + .TransitionTo(Running) + ) + .During(Running) + .When(PassedValue, b => b.Then(context => context.Instance.OtherValue = context.Data)) + ); + + _machine.RaiseEvent(_instance, Initialized, new Init + { + Value = "Hello" + }).Wait(); + + _machine.RaiseEvent(_instance, PassedValue, 47) + .Wait(); + } + + + class Instance + { + public string Value { get; set; } + public int OtherValue { get; set; } + public State CurrentState { get; set; } + } + + + class Init + { + public string Value { get; set; } + } + } +} diff --git a/src/Automatonymous.Tests/Dynamic Modify/Declarative_Specs.cs b/src/Automatonymous.Tests/Dynamic Modify/Declarative_Specs.cs new file mode 100644 index 0000000..83dec6c --- /dev/null +++ b/src/Automatonymous.Tests/Dynamic Modify/Declarative_Specs.cs @@ -0,0 +1,69 @@ +namespace Automatonymous.Tests.DynamicModify +{ + using NUnit.Framework; + + + [TestFixture(Category = "Dynamic Modify")] + public class When_an_instance_has_multiple_states + { + [Test] + public void Should_handle_both_states() + { + Assert.AreEqual(TopGreeted, _instance.Top); + Assert.AreEqual(BottomIgnored, _instance.Bottom); + } + + State TopGreeted; + Event TopInitialized; + State BottomIgnored; + Event BottomInitialized; + + MyState _instance; + StateMachine _top; + StateMachine _bottom; + + [OneTimeSetUp] + public void Specifying_an_event_activity_with_data() + { + _instance = new MyState(); + + _top = AutomatonymousStateMachine + .Create(builder => builder + .State("Greeted", out TopGreeted) + .Event("Initialized", out TopInitialized) + .InstanceState(b => b.Top) + .During(builder.Initial) + .When(TopInitialized, b => b.TransitionTo(TopGreeted)) + ); + _bottom = AutomatonymousStateMachine + .Create(builder => builder + .State("Ignored", out BottomIgnored) + .Event("Initialized", out BottomInitialized) + .InstanceState(b => b.Bottom) + .During(builder.Initial) + .When(BottomInitialized, b => b.TransitionTo(BottomIgnored)) + ); + + _top.RaiseEvent(_instance, TopInitialized, new Init + { + Value = "Hello" + }).Wait(); + + _bottom.RaiseEvent(_instance, BottomInitialized, new Init + { + Value = "Goodbye" + }).Wait(); + } + + class MyState + { + public State Top { get; set; } + public State Bottom { get; set; } + } + + class Init + { + public string Value { get; set; } + } + } +} diff --git a/src/Automatonymous.Tests/Dynamic Modify/Dependency_Specs.cs b/src/Automatonymous.Tests/Dynamic Modify/Dependency_Specs.cs new file mode 100644 index 0000000..b3e5026 --- /dev/null +++ b/src/Automatonymous.Tests/Dynamic Modify/Dependency_Specs.cs @@ -0,0 +1,123 @@ +namespace Automatonymous.Tests.DynamicModify +{ + using System; + using System.Threading.Tasks; + using Activities; + using GreenPipes; + using NUnit.Framework; + + + [TestFixture(Category = "Dynamic Modify")] + public class Having_a_dependency_available + { + [Test] + public void Should_capture_the_value() + { + Assert.AreEqual("79", _claim.Value); + } + + State Running; + Event Create; + ClaimAdjustmentInstance _claim; + StateMachine _machine; + + [OneTimeSetUp] + public void Specifying_an_event_activity() + { + _claim = new ClaimAdjustmentInstance(); + _machine = AutomatonymousStateMachine + .Create(builder => builder + .State("Running", out Running) + .Event("Create", out Create) + .InstanceState(x => x.CurrentState) + .During(builder.Initial) + .When(Create, b => b + .Execute(context => new CalculateValueActivity(new LocalCalculator())) + .Execute(context => new ActionActivity(x => { })) + .TransitionTo(Running) + ) + ); + + var data = new CreateClaim + { + X = 56, + Y = 23, + }; + + _machine.RaiseEvent(_claim, Create, data) + .Wait(); + } + + class ClaimAdjustmentInstance : + ClaimAdjustment + { + public State CurrentState { get; set; } + public string Value { get; set; } + } + + class CalculateValueActivity : + Activity + { + readonly CalculatorService _calculator; + + public CalculateValueActivity(CalculatorService calculator) + { + _calculator = calculator; + } + + public async Task Execute(BehaviorContext context, + Behavior next) + { + context.Instance.Value = _calculator.Add(context.Data.X, context.Data.Y); + } + + public Task Faulted(BehaviorExceptionContext context, + Behavior next) + where TException : Exception + { + return next.Faulted(context); + } + + public void Accept(StateMachineVisitor visitor) + { + visitor.Visit(this); + } + + public void Probe(ProbeContext context) + { + } + } + + interface ClaimAdjustment : + ClaimModel + { + State CurrentState { get; set; } + } + + interface ClaimModel + { + string Value { get; set; } + } + + class CreateClaim + { + public int X { get; set; } + public int Y { get; set; } + } + + interface CalculatorService + { + string Add(int x, int y); + } + + + class LocalCalculator : + CalculatorService + { + public string Add(int x, int y) + { + return (x + y).ToString(); + } + } + } +} diff --git a/src/Automatonymous.Tests/Dynamic Modify/EventLift_Specs.cs b/src/Automatonymous.Tests/Dynamic Modify/EventLift_Specs.cs new file mode 100644 index 0000000..7a7470c --- /dev/null +++ b/src/Automatonymous.Tests/Dynamic Modify/EventLift_Specs.cs @@ -0,0 +1,83 @@ +namespace Automatonymous.Tests.DynamicModify +{ + using NUnit.Framework; + + + [TestFixture(Category = "Dynamic Modify")] + public class When_using_an_event_raiser + { + [Test] + public void Should_raise_the_event() + { + Assert.AreEqual(Running, _instance.CurrentState); + } + + State Running; + Event Initialized; + Instance _instance; + StateMachine _machine; + + [OneTimeSetUp] + public void Specifying_an_event_activity() + { + _instance = new Instance(); + _machine = AutomatonymousStateMachine.Create(builder => builder + .State("Running", out Running) + .Event("Initialized", out Initialized) + .During(builder.Initial) + .When(Initialized, b => b.TransitionTo(Running)) + ); + + EventLift eventLift = _machine.CreateEventLift(Initialized); + + eventLift.Raise(_instance).Wait(); + } + + + class Instance + { + public State CurrentState { get; set; } + } + } + + + [TestFixture(Category = "Dynamic Modify")] + public class When_using_an_event_raiser_with_data + { + [Test] + public void Should_raise_the_event() + { + Assert.AreEqual(Running, _instance.CurrentState); + } + + State Running; + Event Initialized; + Instance _instance; + StateMachine _machine; + + [OneTimeSetUp] + public void Specifying_an_event_activity() + { + _instance = new Instance(); + _machine = AutomatonymousStateMachine + .Create(builder => builder + .State("Running", out Running) + .Event("Initialized", out Initialized) + .InstanceState(b => b.CurrentState) + .During(builder.Initial) + .When(Initialized, b => b.TransitionTo(Running)) + ); + + EventLift eventLift = _machine.CreateEventLift(Initialized); + + eventLift.Raise(_instance, new Init()).Wait(); + } + + class Init { } + + class Instance + { + public State CurrentState { get; set; } + } + } +} diff --git a/src/Automatonymous.Tests/Dynamic Modify/EventObservable_Specs.cs b/src/Automatonymous.Tests/Dynamic Modify/EventObservable_Specs.cs new file mode 100644 index 0000000..932e0a5 --- /dev/null +++ b/src/Automatonymous.Tests/Dynamic Modify/EventObservable_Specs.cs @@ -0,0 +1,50 @@ +namespace Automatonymous.Tests.DynamicModify +{ + using System; + using NUnit.Framework; + + + [TestFixture(Category = "Dynamic Modify")] + public class When_an_event_is_raised_on_an_instance + { + [Test] + public void Should_have_raised_the_initialized_event() + { + Assert.AreEqual(Initialized, _observer.Events[0].Event); + } + + [Test] + public void Should_raise_the_event() + { + Assert.AreEqual(1, _observer.Events.Count); + } + + State Running; + Event Initialized; + Instance _instance; + StateMachine _machine; + EventRaisedObserver _observer; + + [OneTimeSetUp] + public void Specifying_an_event_activity() + { + _instance = new Instance(); + _machine = AutomatonymousStateMachine + .Create(builder => builder + .Event("Initialized", out Initialized) + .State("Running", out Running) + .During(builder.Initial) + .When(Initialized, b => b.TransitionTo(Running)) + ); + _observer = new EventRaisedObserver(); + + using (IDisposable subscription = _machine.ConnectEventObserver(Initialized, _observer)) + _machine.RaiseEvent(_instance, Initialized).Wait(); + } + + class Instance + { + public State CurrentState { get; set; } + } + } +} diff --git a/src/Automatonymous.Tests/Dynamic Modify/Event_Specs.cs b/src/Automatonymous.Tests/Dynamic Modify/Event_Specs.cs new file mode 100644 index 0000000..3a1545c --- /dev/null +++ b/src/Automatonymous.Tests/Dynamic Modify/Event_Specs.cs @@ -0,0 +1,83 @@ +namespace Automatonymous.Tests.DynamicModify +{ + using System; + using Events; + using GreenPipes; + using GreenPipes.Introspection; + using NUnit.Framework; + + + [TestFixture(Category = "Dynamic Modify")] + public class When_an_event_is_declared + { + [Test] + public void It_should_capture_a_simple_event_name() + { + Assert.AreEqual("Hello", Hello.Name); + } + + [Test] + public void It_should_capture_the_data_event_name() + { + Assert.AreEqual("EventA", EventA.Name); + } + + [Test] + public void It_should_create_the_event_for_the_value_type() + { + Assert.IsInstanceOf>(EventInt); + } + + [Test] + public void It_should_create_the_proper_event_type_for_data_events() + { + Assert.IsInstanceOf>(EventA); + } + + [Test] + public void It_should_create_the_proper_event_type_for_simple_events() + { + Assert.IsInstanceOf(Hello); + } + + [Test] + public void Should_return_a_wonderful_breakdown_of_the_guts_inside_it() + { + ProbeResult result = _machine.GetProbeResult(); + + Console.WriteLine(result.ToJsonString()); + } + + Event Hello; + Event EventA; + Event EventInt; + StateMachine _machine; + + class Instance + { + public State CurrentState { get; set; } + } + + + [OneTimeSetUp] + public void A_state_is_declared() + { + _machine = AutomatonymousStateMachine + .Create(builder => builder + .Event("Hello", out Hello) + .Event("EventA", out EventA) + .Event("EventInt", out EventInt) + ); + } + + class A { } + + class TestStateMachine : + AutomatonymousStateMachine + { + public Event Hello { get; private set; } + public Event EventA { get; private set; } + public Event EventInt { get; private set; } + } + } +} diff --git a/src/Automatonymous.Tests/Dynamic Modify/Exception_Specs.cs b/src/Automatonymous.Tests/Dynamic Modify/Exception_Specs.cs new file mode 100644 index 0000000..638698b --- /dev/null +++ b/src/Automatonymous.Tests/Dynamic Modify/Exception_Specs.cs @@ -0,0 +1,454 @@ +namespace Automatonymous.Tests.DynamicModify +{ + using System; + using System.Threading.Tasks; + using GreenPipes; + using GreenPipes.Introspection; + using NUnit.Framework; + + + [TestFixture(Category = "Dynamic Modify")] + public class When_an_action_throws_an_exception + { + [Test] + public void Should_capture_the_exception_message() + { + Assert.AreEqual("Boom!", _instance.ExceptionMessage); + } + + [Test] + public void Should_capture_the_exception_type() + { + Assert.AreEqual(typeof(ApplicationException), _instance.ExceptionType); + } + + [Test] + public void Should_have_called_the_async_if_block() + { + Assert.IsTrue(_instance.CalledThenClauseAsync); + } + + [Test] + public void Should_have_called_the_async_then_block() + { + Assert.IsTrue(_instance.ThenAsyncShouldBeCalled); + } + + [Test] + public void Should_have_called_the_exception_handler() + { + Assert.AreEqual(Failed, _instance.CurrentState); + } + + [Test] + public void Should_have_called_the_false_async_condition_else_block() + { + Assert.IsTrue(_instance.ElseAsyncShouldBeCalled); + } + + [Test] + public void Should_have_called_the_false_condition_else_block() + { + Assert.IsTrue(_instance.ElseShouldBeCalled); + } + + [Test] + public void Should_have_called_the_first_action() + { + Assert.IsTrue(_instance.Called); + } + + [Test] + public void Should_have_called_the_first_if_block() + { + Assert.IsTrue(_instance.CalledThenClause); + } + + [Test] + public void Should_not_have_called_the_false_async_condition_then_block() + { + Assert.IsFalse(_instance.ThenAsyncShouldNotBeCalled); + } + + [Test] + public void Should_not_have_called_the_false_condition_then_block() + { + Assert.IsFalse(_instance.ThenShouldNotBeCalled); + } + + [Test] + public void Should_not_have_called_the_regular_exception() + { + Assert.IsFalse(_instance.ShouldNotBeCalled); + } + + [Test] + public void Should_not_have_called_the_second_action() + { + Assert.IsTrue(_instance.NotCalled); + } + + + [Test] + public void Should_return_a_wonderful_breakdown_of_the_guts_inside_it() + { + ProbeResult result = _machine.GetProbeResult(); + + Console.WriteLine(result.ToJsonString()); + } + + State Failed; + Event Initialized; + Instance _instance; + StateMachine _machine; + + [OneTimeSetUp] + public void Specifying_an_event_activity() + { + _instance = new Instance(); + _machine = AutomatonymousStateMachine + .Create(builder => builder + .State("Failed", out Failed) + .Event("Initialized", out Initialized) + .During(builder.Initial) + .When(Initialized, b => b + .Then(context => context.Instance.Called = true) + .Then(_ => { throw new ApplicationException("Boom!"); }) + .Then(context => context.Instance.NotCalled = false) + .Catch(ex => ex + .If(context => true, c => c + .Then(context => context.Instance.CalledThenClause = true) + ) + .IfAsync(context => Task.FromResult(true), c => c + .Then(context => context.Instance.CalledThenClauseAsync = true) + ) + .IfElse(context => false, + c => c.Then(context => context.Instance.ThenShouldNotBeCalled = true), + c => c.Then(context => context.Instance.ElseShouldBeCalled = true) + ) + .IfElseAsync(context => Task.FromResult(false), + c => c.Then(context => context.Instance.ThenAsyncShouldNotBeCalled = true), + c => c.Then(context => context.Instance.ElseAsyncShouldBeCalled = true) + ) + .Then(context => + { + context.Instance.ExceptionMessage = context.Exception.Message; + context.Instance.ExceptionType = context.Exception.GetType(); + }) + .ThenAsync(context => + { + context.Instance.ThenAsyncShouldBeCalled = true; + return Task.CompletedTask; + }) + .TransitionTo(Failed) + ) + .Catch(ex => ex + .Then(context => context.Instance.ShouldNotBeCalled = true) + ) + ) + ); + + _machine.RaiseEvent(_instance, Initialized).Wait(); + } + + + class Instance + { + public Instance() + { + NotCalled = true; + } + + public bool Called { get; set; } + public bool NotCalled { get; set; } + public Type ExceptionType { get; set; } + public string ExceptionMessage { get; set; } + public State CurrentState { get; set; } + + public bool ShouldNotBeCalled { get; set; } + + public bool CalledThenClause { get; set; } + public bool CalledThenClauseAsync { get; set; } + + public bool ThenShouldNotBeCalled { get; set; } + public bool ElseShouldBeCalled { get; set; } + public bool ThenAsyncShouldNotBeCalled { get; set; } + public bool ElseAsyncShouldBeCalled { get; set; } + + public bool ThenAsyncShouldBeCalled { get; set; } + } + } + + + [TestFixture(Category = "Dynamic Modify")] + public class When_the_exception_does_not_match_the_type + { + [Test] + public void Should_capture_the_exception_message() + { + Assert.AreEqual("Boom!", _instance.ExceptionMessage); + } + + [Test] + public void Should_capture_the_exception_type() + { + Assert.AreEqual(typeof(ApplicationException), _instance.ExceptionType); + } + + [Test] + public void Should_have_called_the_exception_handler() + { + Assert.AreEqual(Failed, _instance.CurrentState); + } + + [Test] + public void Should_have_called_the_first_action() + { + Assert.IsTrue(_instance.Called); + } + + + [Test] + public void Should_not_have_called_the_second_action() + { + Assert.IsTrue(_instance.NotCalled); + } + + State Failed; + Event Initialized; + Instance _instance; + StateMachine _machine; + + [OneTimeSetUp] + public void Specifying_an_event_activity() + { + _instance = new Instance(); + _machine = AutomatonymousStateMachine + .Create(builder => builder + .State("Failed", out Failed) + .Event("Initialized", out Initialized) + .InstanceState(b => b.CurrentState) + .During(builder.Initial) + .When(Initialized, b => b + .Then(context => context.Instance.Called = true) + .Then(_ => { throw new ApplicationException("Boom!"); }) + .Then(context => context.Instance.NotCalled = false) + .Catch(ex => ex + .Then(context => + { + context.Instance.ExceptionMessage = context.Exception.Message; + context.Instance.ExceptionType = context.Exception.GetType(); + }) + .TransitionTo(Failed) + ) + ) + ); + + _machine.RaiseEvent(_instance, Initialized).Wait(); + } + + + class Instance + { + public Instance() + { + NotCalled = true; + } + + public bool Called { get; set; } + public bool NotCalled { get; set; } + public Type ExceptionType { get; set; } + public string ExceptionMessage { get; set; } + public State CurrentState { get; set; } + } + } + + + [TestFixture(Category = "Dynamic Modify")] + public class When_the_exception_is_caught + { + [Test] + public void Should_have_called_the_subsequent_action() + { + Assert.IsTrue(_instance.Called); + } + + Instance _instance; + StateMachine _machine; + + [OneTimeSetUp] + public void Specifying_an_event_activity() + { + Event Initialized = null; + + _instance = new Instance(); + _machine = AutomatonymousStateMachine + .Create(builder => builder + .Event("Initialized", out Initialized) + .State("Failed", out State Failed) + .InstanceState(b => b.CurrentState) + .During(builder.Initial) + .When(Initialized, b => b + .Then(_ => { throw new ApplicationException("Boom!"); }) + .Catch(ex => ex) + .Then(context => context.Instance.Called = true) + ) + ); + + _machine.RaiseEvent(_instance, Initialized).Wait(); + } + + + class Instance + { + public bool Called { get; set; } + public State CurrentState { get; set; } + } + } + + + [TestFixture(Category = "Dynamic Modify")] + public class When_an_action_throws_an_exception_on_data_events + { + [Test] + public void Should_capture_the_exception_message() + { + Assert.AreEqual("Boom!", _instance.ExceptionMessage); + } + + [Test] + public void Should_capture_the_exception_type() + { + Assert.AreEqual(typeof(ApplicationException), _instance.ExceptionType); + } + + [Test] + public void Should_have_called_the_async_if_block() + { + Assert.IsTrue(_instance.CalledSecondThenClause); + } + + [Test] + public void Should_have_called_the_exception_handler() + { + Assert.AreEqual(Failed, _instance.CurrentState); + } + + [Test] + public void Should_have_called_the_false_async_condition_else_block() + { + Assert.IsTrue(_instance.ElseAsyncShouldBeCalled); + } + + [Test] + public void Should_have_called_the_false_condition_else_block() + { + Assert.IsTrue(_instance.ElseShouldBeCalled); + } + + [Test] + public void Should_have_called_the_first_action() + { + Assert.IsTrue(_instance.Called); + } + + [Test] + public void Should_have_called_the_first_if_block() + { + Assert.IsTrue(_instance.CalledThenClause); + } + + [Test] + public void Should_not_have_called_the_false_async_condition_then_block() + { + Assert.IsFalse(_instance.ThenAsyncShouldNotBeCalled); + } + + [Test] + public void Should_not_have_called_the_false_condition_then_block() + { + Assert.IsFalse(_instance.ThenShouldNotBeCalled); + } + + [Test] + public void Should_not_have_called_the_second_action() + { + Assert.IsTrue(_instance.NotCalled); + } + + State Failed; + Event Initialized; + Instance _instance; + StateMachine _machine; + + [OneTimeSetUp] + public void Specifying_an_event_activity() + { + _instance = new Instance(); + _machine = AutomatonymousStateMachine + .Create(builder => builder + .State("Failed", out Failed) + .Event("Initialized", out Initialized) + .InstanceState(b => b.CurrentState) + .During(builder.Initial) + .When(Initialized, b => b + .Then(context => context.Instance.Called = true) + .Then(_ => { throw new ApplicationException("Boom!"); }) + .Then(context => context.Instance.NotCalled = false) + .Catch(ex => ex + .If(context => true, b => b + .Then(context => context.Instance.CalledThenClause = true) + ) + .IfAsync(context => Task.FromResult(true), b => b + .Then(context => context.Instance.CalledSecondThenClause = true) + ) + .IfElse(context => false, + b => b.Then(context => context.Instance.ThenShouldNotBeCalled = true), + b => b.Then(context => context.Instance.ElseShouldBeCalled = true) + ) + .IfElseAsync(context => Task.FromResult(false), + b => b.Then(context => context.Instance.ThenAsyncShouldNotBeCalled = true), + b => b.Then(context => context.Instance.ElseAsyncShouldBeCalled = true) + ) + .Then(context => + { + context.Instance.ExceptionMessage = context.Exception.Message; + context.Instance.ExceptionType = context.Exception.GetType(); + }) + .TransitionTo(Failed) + ) + ) + ); + + _machine.RaiseEvent(_instance, Initialized, new Init()).Wait(); + } + + + class Instance + { + public Instance() + { + NotCalled = true; + } + + public bool Called { get; set; } + public bool NotCalled { get; set; } + public Type ExceptionType { get; set; } + public string ExceptionMessage { get; set; } + public State CurrentState { get; set; } + + public bool CalledThenClause { get; set; } + public bool CalledSecondThenClause { get; set; } + + public bool ThenShouldNotBeCalled { get; set; } + public bool ElseShouldBeCalled { get; set; } + public bool ThenAsyncShouldNotBeCalled { get; set; } + public bool ElseAsyncShouldBeCalled { get; set; } + } + + + class Init + { + } + } +} diff --git a/src/Automatonymous.Tests/Dynamic Modify/Faulted_Specs.cs b/src/Automatonymous.Tests/Dynamic Modify/Faulted_Specs.cs new file mode 100644 index 0000000..9ef6cff --- /dev/null +++ b/src/Automatonymous.Tests/Dynamic Modify/Faulted_Specs.cs @@ -0,0 +1,140 @@ +namespace Automatonymous.Tests.DynamicModify +{ + using System; + using System.Threading.Tasks; + using Activities; + using GreenPipes; + using NUnit.Framework; + + + [TestFixture(Category = "Dynamic Modify")] + public class Having_an_activity_with_faulted_handler + { + [Test] + public void Should_capture_the_value() + { + var data = new CreateClaim + { + X = 56, + Y = 23, + }; + + Assert.That(async () => await _machine.RaiseEvent(_claim, Create, data), Throws.TypeOf()); + + Assert.AreEqual(default, _claim.Value); + } + + Event Create; + ClaimAdjustmentInstance _claim; + StateMachine _machine; + + [OneTimeSetUp] + public void Specifying_an_event_activity() + { + State Running = null; + + _claim = new ClaimAdjustmentInstance(); + _machine = AutomatonymousStateMachine + .Create(builder => builder + .State("Running", out Running) + .Event("Create", out Create) + .InstanceState(b => b.CurrentState) + .During(builder.Initial) + .When(Create, b => b + .Execute(context => new CalculateValueActivity(new LocalCalculator())) + .Execute(context => new ActionActivity(x => throw new Exception())) + .TransitionTo(Running)) + ); + } + + + class ClaimAdjustmentInstance : + ClaimAdjustment + { + public State CurrentState { get; set; } + public string Value { get; set; } + } + + + class CalculateValueActivity : + Activity + { + readonly CalculatorService _calculator; + + public CalculateValueActivity(CalculatorService calculator) + { + _calculator = calculator; + } + + public async Task Execute(BehaviorContext context, + Behavior next) + { + var originalValue = context.Instance.Value; + try + { + context.Instance.Value = _calculator.Add(context.Data.X, context.Data.Y); + + await next.Execute(context); + } + catch (Exception) + { + context.Instance.Value = originalValue; + + throw; + } + } + + public Task Faulted(BehaviorExceptionContext context, + Behavior next) + where TException : Exception + { + return next.Faulted(context); + } + + public void Accept(StateMachineVisitor visitor) + { + visitor.Visit(this); + } + + public void Probe(ProbeContext context) + { + } + } + + + interface ClaimAdjustment : + ClaimModel + { + State CurrentState { get; set; } + } + + + interface ClaimModel + { + string Value { get; set; } + } + + + class CreateClaim + { + public int X { get; set; } + public int Y { get; set; } + } + + + interface CalculatorService + { + string Add(int x, int y); + } + + + class LocalCalculator : + CalculatorService + { + public string Add(int x, int y) + { + return (x + y).ToString(); + } + } + } +} diff --git a/src/Automatonymous.Tests/Dynamic Modify/FilterExpression_Specs.cs b/src/Automatonymous.Tests/Dynamic Modify/FilterExpression_Specs.cs new file mode 100644 index 0000000..dae2b88 --- /dev/null +++ b/src/Automatonymous.Tests/Dynamic Modify/FilterExpression_Specs.cs @@ -0,0 +1,72 @@ +namespace Automatonymous.Tests.DynamicModify +{ + using System.Threading.Tasks; + using NUnit.Framework; + + + [TestFixture(Category = "Dynamic Modify")] + public class When_specifying_a_conditional_event_activity + { + [Test] + public async Task Should_transition_to_the_proper_state() + { + State True = null; + State False = null; + Event Thing = null; + + var instance = new Instance(); + var machine = AutomatonymousStateMachine + .Create(builder => builder + .State("True", out True) + .State("False", out False) + .Event("Thing", out Thing) + .InstanceState(b => b.CurrentState) + .During(builder.Initial) + .When(Thing, context => context.Data.Condition, b => b.TransitionTo(True)) + .When(Thing, context => !context.Data.Condition, b => b.TransitionTo(False)) + ); + + await machine.RaiseEvent(instance, Thing, new Data {Condition = true}); + Assert.AreEqual(True, instance.CurrentState); + + // reset + instance.CurrentState = machine.Initial; + + await machine.RaiseEvent(instance, Thing, new Data {Condition = false}); + Assert.AreEqual(False, instance.CurrentState); + } + + + class Instance + { + public State CurrentState { get; set; } + } + + + class InstanceStateMachine : + AutomatonymousStateMachine + { + public InstanceStateMachine() + { + InstanceState(x => x.CurrentState); + + During(Initial, + When(Thing, context => context.Data.Condition) + .TransitionTo(True), + When(Thing, context => !context.Data.Condition) + .TransitionTo(False)); + } + + public State True { get; private set; } + public State False { get; private set; } + + public Event Thing { get; private set; } + } + + + class Data + { + public bool Condition { get; set; } + } + } +} diff --git a/src/Automatonymous.Tests/Dynamic Modify/Group_Specs.cs b/src/Automatonymous.Tests/Dynamic Modify/Group_Specs.cs new file mode 100644 index 0000000..0297cb5 --- /dev/null +++ b/src/Automatonymous.Tests/Dynamic Modify/Group_Specs.cs @@ -0,0 +1,181 @@ +namespace Automatonymous.Tests.DynamicModify +{ + using System; + using NUnit.Framework; + + // NOTE: This test was pulled from the non-dynamic set; Seems incomplete with the commented out code below. + [TestFixture(Category = "Dynamic Modify")] + public class Declaring_groups_in_a_state_machine + { + [Test] + public void Should_allow_parallel_execution_of_events() + { + } + + [Test] + public void Should_have_captured_initial_data() + { + Assert.AreEqual("Audi", _instance.VehicleMake); + Assert.AreEqual("A6", _instance.VehicleModel); + } + + State BeingServiced; + Event VehicleArrived; + + StateMachine _machine; + PitStopInstance _instance; + + [OneTimeSetUp] + public void Setup() + { + _instance = new PitStopInstance(); + _machine = AutomatonymousStateMachine + .Create(builder => builder + .State("BeingServiced", out BeingServiced) + .Event("VehicleArrived", out VehicleArrived) + .InstanceState(b => b.OverallState) + .During(builder.Initial) + .When(VehicleArrived, b => b + .Then(context => + { + context.Instance.VehicleMake = context.Data.Make; + context.Instance.VehicleModel = context.Data.Model; + }) + .TransitionTo(BeingServiced)) + ); + + var vehicle = new Vehicle + { + Make = "Audi", + Model = "A6", + }; + + _machine.RaiseEvent(_instance, VehicleArrived, vehicle).Wait(); + } + + + class PitStopInstance + { + public State OverallState { get; private set; } + public State FuelState { get; private set; } + public State OilState { get; private set; } + + public string VehicleMake { get; set; } + public string VehicleModel { get; set; } + + public decimal FuelGallons { get; set; } + public decimal FuelPricePerGallon { get; set; } + public decimal FuelCost { get; set; } + + public decimal OilQuarts { get; set; } + public decimal OilPricePerQuart { get; set; } + public decimal OilCost { get; set; } + } + + // NOTE: Left in place due to the incompleteness of this test. +// class PitStop : +// AutomatonymousStateMachine +// { +// public PitStop() +// { +// InstanceState(x => x.OverallState); + +// During(Initial, +// When(VehicleArrived) +// .Then(context => +// { +// context.Instance.VehicleMake = context.Data.Make; +// context.Instance.VehicleModel = context.Data.Model; +// }) +// .TransitionTo(BeingServiced) +//// .RunParallel(p => +//// { +//// p.Start(x => x.BeginFilling); +//// p.Start(x => x.BeginChecking); +//// })) +// ); +// } + +// public State BeingServiced { get; private set; } + +// public Event VehicleArrived { get; private set; } +// } + + + class FillTank : + AutomatonymousStateMachine + { + public FillTank() + { + InstanceState(x => x.FuelState); + + Initially( + When(Initial.Enter) + .TransitionTo(Filling)); + + During(Filling, + When(Full) + .Then(context => + { + context.Instance.FuelGallons = context.Data.Gallons; + context.Instance.FuelPricePerGallon = context.Data.PricePerGallon; + context.Instance.FuelCost = context.Data.Gallons * context.Data.PricePerGallon; + }) + .Finalize()); + } + + public State Filling { get; private set; } + + public Event Full { get; private set; } + } + + + class CheckOil : + AutomatonymousStateMachine + { + public CheckOil() + { + InstanceState(x => x.OilState); + + Initially( + When(Initial.Enter) + .TransitionTo(AddingOil)); + + During(AddingOil, + When(Done) + .Then(context => + { + context.Instance.OilQuarts = context.Data.Quarts; + context.Instance.OilPricePerQuart = context.Data.PricePerQuart; + context.Instance.OilCost = Math.Ceiling(context.Data.Quarts) * context.Data.PricePerQuart; + }) + .Finalize()); + } + + public State AddingOil { get; private set; } + + public Event Done { get; private set; } + } + + + class FuelDispensed + { + public decimal Gallons { get; set; } + public decimal PricePerGallon { get; set; } + } + + + class OilAdded + { + public decimal Quarts { get; set; } + public decimal PricePerQuart { get; set; } + } + + + class Vehicle + { + public string Make { get; set; } + public string Model { get; set; } + } + } +} diff --git a/src/Automatonymous.Tests/Dynamic Modify/InstanceLift_Specs.cs b/src/Automatonymous.Tests/Dynamic Modify/InstanceLift_Specs.cs new file mode 100644 index 0000000..5bcbb22 --- /dev/null +++ b/src/Automatonymous.Tests/Dynamic Modify/InstanceLift_Specs.cs @@ -0,0 +1,121 @@ +namespace Automatonymous.Tests.DynamicModify +{ + using NUnit.Framework; + + + [TestFixture(Category = "Dynamic Modify")] + public class When_using_an_instance_lift + { + [Test] + public void Should_raise_the_event() + { + Assert.AreEqual(Running, _instance.CurrentState); + } + + State Running; + Event Initialized; + Instance _instance; + StateMachine _machine; + + [OneTimeSetUp] + public void Specifying_an_event_activity() + { + _instance = new Instance(); + _machine = AutomatonymousStateMachine + .Create(builder => builder + .State("Running", out Running) + .Event("Initialized", out Initialized) + .During(builder.Initial) + .When(Initialized, b => b.TransitionTo(Running)) + ); + + InstanceLift> instanceLift = _machine.CreateInstanceLift(_instance); + + instanceLift.Raise(Initialized) + .Wait(); + } + + + class Instance + { + public State CurrentState { get; set; } + } + + + class InstanceStateMachine : + AutomatonymousStateMachine + { + public InstanceStateMachine() + { + During(Initial, + When(Initialized) + .TransitionTo(Running)); + } + + public State Running { get; private set; } + + public Event Initialized { get; private set; } + } + } + + + [TestFixture(Category = "Dynamic Modify")] + public class When_using_an_instance_lift_with_data + { + [Test] + public void Should_raise_the_event() + { + Assert.AreEqual(Running, _instance.CurrentState); + } + + State Running; + Event Initialized; + Instance _instance; + StateMachine _machine; + + [OneTimeSetUp] + public void Specifying_an_event_activity() + { + _instance = new Instance(); + _machine = AutomatonymousStateMachine + .Create(builder => builder + .State("Running", out Running) + .Event("Initialized", out Initialized) + .During(builder.Initial) + .When(Initialized, b => b.TransitionTo(Running)) + ); + + InstanceLift> instanceLift = _machine.CreateInstanceLift(_instance); + + instanceLift.Raise(Initialized, new Init()) + .Wait(); + } + + + class Init + { + } + + + class Instance + { + public State CurrentState { get; set; } + } + + + class InstanceStateMachine : + AutomatonymousStateMachine + { + public InstanceStateMachine() + { + During(Initial, + When(Initialized) + .TransitionTo(Running)); + } + + public State Running { get; private set; } + + public Event Initialized { get; private set; } + } + } +} diff --git a/src/Automatonymous.Tests/Dynamic Modify/Introspection_Specs.cs b/src/Automatonymous.Tests/Dynamic Modify/Introspection_Specs.cs new file mode 100644 index 0000000..b64924d --- /dev/null +++ b/src/Automatonymous.Tests/Dynamic Modify/Introspection_Specs.cs @@ -0,0 +1,134 @@ +namespace Automatonymous.Tests.DynamicModify +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using GreenPipes; + using GreenPipes.Introspection; + using NUnit.Framework; + + + [TestFixture(Category = "Dynamic Modify")] + public class Introspection_Specs + { + [Test] + public void Should_return_a_wonderful_breakdown_of_the_guts_inside_it() + { + ProbeResult result = _machine.GetProbeResult(); + + Console.WriteLine(result.ToJsonString()); + } + + [Test] + public void The_machine_shoud_report_its_instance_type() + { + Assert.AreEqual(typeof(Instance), ((StateMachine)_machine).InstanceType); + } + + [Test] + public void The_machine_should_expose_all_events() + { + var events = _machine.Events.ToList(); + + Assert.AreEqual(4, events.Count); + Assert.Contains(Ignored, events); + Assert.Contains(Handshake, events); + Assert.Contains(Hello, events); + Assert.Contains(YelledAt, events); + } + + [Test] + public void The_machine_should_expose_all_states() + { + Assert.AreEqual(5, ((StateMachine)_machine).States.Count()); + Assert.Contains(_machine.Initial, _machine.States.ToList()); + Assert.Contains(_machine.Final, _machine.States.ToList()); + Assert.Contains(Greeted, _machine.States.ToList()); + Assert.Contains(Loved, _machine.States.ToList()); + Assert.Contains(Pissed, _machine.States.ToList()); + } + + [Test] + public async Task The_next_events_should_be_known() + { + List events = (await _machine.NextEvents(_instance)).ToList(); + Assert.AreEqual(3, events.Count); + } + + Event Ignored; + Event Handshake; + Event Hello; + Event YelledAt; + State Greeted; + State Pissed; + State Loved; + Instance _instance; + StateMachine _machine; + + [OneTimeSetUp] + public void A_state_is_declared() + { + _instance = new Instance(); + _machine = AutomatonymousStateMachine + .Create(builder => builder + .Event("Ignored", out Ignored) + .Event("Handshake", out Handshake) + .Event("Hello", out Hello) + .Event("YelledAt", out YelledAt) + .State("Greeted", out Greeted) + .State("Pissed", out Pissed) + .State("Loved", out Loved) + .Initially() + .When(Hello, b => b.TransitionTo(Greeted)) + .During(Greeted) + .When(Handshake, b => b.TransitionTo(Loved)) + .When(Ignored, b => b.TransitionTo(Pissed)) + .WhenEnter(Greeted, b=> b.Then(context => { })) + .DuringAny() + .When(YelledAt, b => b.TransitionTo(builder.Final)) + ); + + _machine.RaiseEvent(_instance, Hello); + } + + class A { } + class B { } + + class Instance + { + public State CurrentState { get; set; } + } + + + class TestStateMachine : + AutomatonymousStateMachine + { + public TestStateMachine() + { + Initially( + When(Hello) + .TransitionTo(Greeted)); + + During(Greeted, + When(Handshake) + .TransitionTo(Loved), + When(Ignored) + .TransitionTo(Pissed)); + + WhenEnter(Greeted, x => x.Then(context => { })); + + DuringAny(When(YelledAt).TransitionTo(Final)); + } + + public State Greeted { get; set; } + public State Pissed { get; set; } + public State Loved { get; set; } + + public Event Hello { get; private set; } + public Event YelledAt { get; private set; } + public Event Handshake { get; private set; } + public Event Ignored { get; private set; } + } + } +} diff --git a/src/Automatonymous.Tests/Dynamic Modify/Models/DynamicModifyTestStateMachine.cs b/src/Automatonymous.Tests/Dynamic Modify/Models/DynamicModifyTestStateMachine.cs new file mode 100644 index 0000000..406aa51 --- /dev/null +++ b/src/Automatonymous.Tests/Dynamic Modify/Models/DynamicModifyTestStateMachine.cs @@ -0,0 +1,8 @@ +namespace Automatonymous.Tests.DynamicModify +{ + internal class DynamicModifyTestStateMachine : + AutomatonymousStateMachine + where TInstance : class + { + } +} diff --git a/src/Automatonymous.Tests/Dynamic Modify/Observable_Specs.cs b/src/Automatonymous.Tests/Dynamic Modify/Observable_Specs.cs new file mode 100644 index 0000000..3635ab9 --- /dev/null +++ b/src/Automatonymous.Tests/Dynamic Modify/Observable_Specs.cs @@ -0,0 +1,373 @@ +namespace Automatonymous.Tests.DynamicModify +{ + using System; + using GreenPipes; + using GreenPipes.Introspection; + using NUnit.Framework; + + + [TestFixture(Category = "Dynamic Modify")] + public class Observing_state_machine_instance_state_changes + { + [Test] + public void Should_have_first_moved_to_initial() + { + Assert.AreEqual(null, _observer.Events[0].Previous); + Assert.AreEqual(_machine.Initial, _observer.Events[0].Current); + } + + [Test] + public void Should_have_second_switched_to_running() + { + Assert.AreEqual(_machine.Initial, _observer.Events[1].Previous); + Assert.AreEqual(Running, _observer.Events[1].Current); + } + + [Test] + public void Should_raise_the_event() + { + Assert.AreEqual(3, _observer.Events.Count); + } + + State Running; + Instance _instance; + StateMachine _machine; + StateChangeObserver _observer; + + [OneTimeSetUp] + public void Specifying_an_event_activity() + { + Event Initialized = null; + Event Finish = null; + + _instance = new Instance(); + _machine = AutomatonymousStateMachine + .Create(builder => builder + .State("Running", out Running) + .Event("Initialized", out Initialized) + .Event("Finish", out Finish) + .During(builder.Initial) + .When(Initialized, b => b.TransitionTo(Running)) + .During(Running) + .When(Finish, b => b.Finalize()) + ); + _observer = new StateChangeObserver(); + + using (IDisposable subscription = _machine.ConnectStateObserver(_observer)) + { + _machine.RaiseEvent(_instance, Initialized).Wait(); + _machine.RaiseEvent(_instance, Finish).Wait(); + } + } + + + class Instance + { + public State CurrentState { get; set; } + } + } + + + [TestFixture(Category = "Dynamic Modify")] + public class Observing_events_with_substates + { + [Test] + public void Should_have_all_events() + { + Assert.AreEqual(2, _eventObserver.Events.Count); + } + + [Test] + public void Should_have_first_moved_to_initial() + { + Assert.AreEqual(null, _observer.Events[0].Previous); + Assert.AreEqual(_machine.Initial, _observer.Events[0].Current); + } + + [Test] + public void Should_have_fourth_switched_to_finished() + { + Assert.AreEqual(Resting, _observer.Events[3].Previous); + Assert.AreEqual(_machine.Final, _observer.Events[3].Current); + } + + [Test] + public void Should_have_second_switched_to_running() + { + Assert.AreEqual(_machine.Initial, _observer.Events[1].Previous); + Assert.AreEqual(Running, _observer.Events[1].Current); + } + + [Test] + public void Should_have_third_switched_to_resting() + { + Assert.AreEqual(Running, _observer.Events[2].Previous); + Assert.AreEqual(Resting, _observer.Events[2].Current); + } + + [Test] + public void Should_have_transition_1() + { + Assert.AreEqual("Initialized", _eventObserver.Events[0].Event.Name); + } + + [Test] + public void Should_have_transition_2() + { + Assert.AreEqual("LegCramped", _eventObserver.Events[1].Event.Name); + } + + [Test] + public void Should_raise_the_event() + { + Assert.AreEqual(4, _observer.Events.Count); + } + + + [Test] + public void Should_return_a_wonderful_breakdown_of_the_guts_inside_it() + { + ProbeResult result = _machine.GetProbeResult(); + + Console.WriteLine(result.ToJsonString()); + } + + State Resting; + State Running; + Instance _instance; + StateMachine _machine; + StateChangeObserver _observer; + EventRaisedObserver _eventObserver; + + [OneTimeSetUp] + public void Specifying_an_event_activity() + { + Event Initialized = null; + Event LegCramped = null; + Event Finish = null; + + _instance = new Instance(); + _machine = AutomatonymousStateMachine + .Create(builder => builder + .State("Running", out Running) + .Event("Initialized", out Initialized) + .Event("LegCramped", out LegCramped) + .Event("Finish", out Finish) + .SubState("Resting", Running, out Resting) + .During(builder.Initial) + .When(Initialized, b => b.TransitionTo(Running)) + .During(Running) + .When(LegCramped, b => b.TransitionTo(Resting)) + .When(Finish, b => b.Finalize()) + .WhenEnter(Running, b => b.Then(context => { })) + .WhenLeave(Running, b => b.Then(context => { })) + .BeforeEnter(Running, b => b.Then(context => { })) + .AfterLeave(Running, b => b.Then(context => { })) + ); + _observer = new StateChangeObserver(); + _eventObserver = new EventRaisedObserver(); + + using (IDisposable subscription = _machine.ConnectStateObserver(_observer)) + using (IDisposable beforeEnterSub = _machine.ConnectEventObserver(Initialized, _eventObserver)) + using (IDisposable afterLeaveSub = _machine.ConnectEventObserver(LegCramped, _eventObserver)) + { + _machine.RaiseEvent(_instance, Initialized).Wait(); + _machine.RaiseEvent(_instance, LegCramped).Wait(); + _machine.RaiseEvent(_instance, Finish).Wait(); + } + } + + + class Instance + { + public State CurrentState { get; set; } + } + + + class InstanceStateMachine : + AutomatonymousStateMachine + { + public InstanceStateMachine() + { + SubState(() => Resting, Running); + + During(Initial, + When(Initialized) + .TransitionTo(Running)); + + During(Running, + When(LegCramped) + .TransitionTo(Resting), + When(Finish) + .Finalize()); + + WhenEnter(Running, x => x.Then(context => { })); + WhenLeave(Running, x => x.Then(context => { })); + BeforeEnter(Running, x => x.Then(context => { })); + AfterLeave(Running, x => x.Then(context => { })); + } + + public State Running { get; private set; } + public State Resting { get; private set; } + public Event Initialized { get; private set; } + public Event LegCramped { get; private set; } + public Event Finish { get; private set; } + } + } + + + [TestFixture(Category = "Dynamic Modify")] + public class Observing_events_with_substates_part_deux + { + [Test] + public void Should_have_all_events() + { + Assert.AreEqual(2, _eventObserver.Events.Count); + } + + [Test] + public void Should_have_fifth_switched_to_finished() + { + Assert.AreEqual(Running, _observer.Events[4].Previous); + Assert.AreEqual(_machine.Final, _observer.Events[4].Current); + } + + [Test] + public void Should_have_first_moved_to_initial() + { + Assert.AreEqual(null, _observer.Events[0].Previous); + Assert.AreEqual(_machine.Initial, _observer.Events[0].Current); + } + + [Test] + public void Should_have_fourth_switched_to_finished() + { + Assert.AreEqual(Resting, _observer.Events[3].Previous); + Assert.AreEqual(Running, _observer.Events[3].Current); + } + + [Test] + public void Should_have_second_switched_to_running() + { + Assert.AreEqual(_machine.Initial, _observer.Events[1].Previous); + Assert.AreEqual(Running, _observer.Events[1].Current); + } + + [Test] + public void Should_have_third_switched_to_resting() + { + Assert.AreEqual(Running, _observer.Events[2].Previous); + Assert.AreEqual(Resting, _observer.Events[2].Current); + } + + [Test] + public void Should_have_transition_1() + { + Assert.AreEqual("Running.BeforeEnter", _eventObserver.Events[0].Event.Name); + } + + [Test] + public void Should_have_transition_2() + { + Assert.AreEqual("Running.AfterLeave", _eventObserver.Events[1].Event.Name); + } + + [Test] + public void Should_raise_the_event() + { + Assert.AreEqual(5, _observer.Events.Count); + } + + State Resting; + State Running; + Instance _instance; + StateMachine _machine; + StateChangeObserver _observer; + EventRaisedObserver _eventObserver; + + [OneTimeSetUp] + public void Specifying_an_event_activity() + { + Event Initialized = null; + Event LegCramped = null; + Event Finish = null; + Event Recovered = null; + + _instance = new Instance(); + _machine = AutomatonymousStateMachine + .Create(builder => builder + .State("Running", out Running) + .Event("Initialized", out Initialized) + .Event("LegCramped", out LegCramped) + .Event("Finish", out Finish) + .Event("Recovered", out Recovered) + .SubState("Resting", Running, out Resting) + .During(builder.Initial) + .When(Initialized, b => b.TransitionTo(Running)) + .During(Running) + .When(LegCramped, b => b.TransitionTo(Resting)) + .When(Finish, b => b.Finalize()) + .During(Resting) + .When(Recovered, b => b.TransitionTo(Running)) + .WhenEnter(Running, b => b.Then(context => { })) + .WhenLeave(Running, b => b.Then(context => { })) + .BeforeEnter(Running, b => b.Then(context => { })) + .AfterLeave(Running, b => b.Then(context => { })) + ); + _observer = new StateChangeObserver(); + _eventObserver = new EventRaisedObserver(); + + using (IDisposable subscription = _machine.ConnectStateObserver(_observer)) + using (IDisposable beforeEnterSub = _machine.ConnectEventObserver(Running.BeforeEnter, _eventObserver)) + using (IDisposable afterLeaveSub = _machine.ConnectEventObserver(Running.AfterLeave, _eventObserver)) + { + _machine.RaiseEvent(_instance, Initialized).Wait(); + _machine.RaiseEvent(_instance, LegCramped).Wait(); + _machine.RaiseEvent(_instance, Recovered).Wait(); + _machine.RaiseEvent(_instance, Finish).Wait(); + } + } + + + class Instance + { + public State CurrentState { get; set; } + } + + + class InstanceStateMachine : + AutomatonymousStateMachine + { + public InstanceStateMachine() + { + SubState(() => Resting, Running); + + During(Initial, + When(Initialized) + .TransitionTo(Running)); + + During(Running, + When(LegCramped) + .TransitionTo(Resting), + When(Finish) + .Finalize()); + + During(Resting, + When(Recovered) + .TransitionTo(Running)); + + WhenEnter(Running, x => x.Then(context => { })); + WhenLeave(Running, x => x.Then(context => { })); + BeforeEnter(Running, x => x.Then(context => { })); + AfterLeave(Running, x => x.Then(context => { })); + } + + public State Running { get; private set; } + public State Resting { get; private set; } + public Event Initialized { get; private set; } + public Event LegCramped { get; private set; } + public Event Recovered { get; private set; } + public Event Finish { get; private set; } + } + } +} diff --git a/src/Automatonymous.Tests/Dynamic Modify/RaiseEvent_Specs.cs b/src/Automatonymous.Tests/Dynamic Modify/RaiseEvent_Specs.cs new file mode 100644 index 0000000..ea669dc --- /dev/null +++ b/src/Automatonymous.Tests/Dynamic Modify/RaiseEvent_Specs.cs @@ -0,0 +1,81 @@ +namespace Automatonymous.Tests.DynamicModify +{ + using System; + using System.Threading.Tasks; + using NUnit.Framework; + + + [TestFixture(Category = "Dynamic Modify")] + public class Raising_an_event_within_an_event + { + [Test] + public async Task Should_include_payload() + { + Event Thing = null; + State True = null; + + var instance = new Instance(); + var machine = AutomatonymousStateMachine + .Create(builder => builder + .State("True", out True) + .State("False", out State False) + .Event("Thing", out Thing) + .Event("Initialize", out Event Initialize) + .During(builder.Initial) + .When(Thing, context => context.Data.Condition, b => b + .TransitionTo(True) + .Then(context => context.Raise(Initialize))) + .When(Thing, context => !context.Data.Condition, b => b + .TransitionTo(False)) + .DuringAny() + .When(Initialize, b => b + .Then(context => context.Instance.Initialized = DateTime.Now)) + ); + + await machine.RaiseEvent(instance, Thing, new Data + { + Condition = true + }); + Assert.AreEqual(True, instance.CurrentState); + Assert.IsTrue(instance.Initialized.HasValue); + } + + + class Instance + { + public State CurrentState { get; set; } + public DateTime? Initialized { get; set; } + } + + + class InstanceStateMachine : + AutomatonymousStateMachine + { + public InstanceStateMachine() + { + During(Initial, + When(Thing, context => context.Data.Condition) + .TransitionTo(True) + .Then(context => context.Raise(Initialize)), + When(Thing, context => !context.Data.Condition) + .TransitionTo(False)); + + DuringAny( + When(Initialize) + .Then(context => context.Instance.Initialized = DateTime.Now)); + } + + public State True { get; private set; } + public State False { get; private set; } + + public Event Thing { get; private set; } + public Event Initialize { get; private set; } + } + + + class Data + { + public bool Condition { get; set; } + } + } +} diff --git a/src/Automatonymous.Tests/Dynamic Modify/Request_Specs.cs b/src/Automatonymous.Tests/Dynamic Modify/Request_Specs.cs new file mode 100644 index 0000000..1bab352 --- /dev/null +++ b/src/Automatonymous.Tests/Dynamic Modify/Request_Specs.cs @@ -0,0 +1,400 @@ +namespace Automatonymous.Tests.DynamicModify +{ + namespace Request_Specs + { + using System; + using System.Linq.Expressions; + using System.Reflection; + using System.Threading.Tasks; + using Automatonymous.Builder; + using Binders; + using GreenPipes; + using NUnit.Framework; + + + [TestFixture(Category = "Dynamic Modify")] + public class Using_a_request_in_a_state_machine + { + [Test] + public async Task Should_property_initialize() + { + Request QuoteRequest = null; + Event QuoteRequested = null; + + var machine = AutomatonymousStateMachine + .Create(builder => builder + .InstanceState(x => x.CurrentState) + .Event("QuoteRequested", out QuoteRequested) + .Request(x => x.ServiceAddress = new Uri("loopback://localhost/my_queue"), "QuoteRequest", out QuoteRequest) + .Initially() + .When(QuoteRequested, b => b + .Then(context => Console.WriteLine("Quote requested: {0}", context.Data.Symbol)) + .Request(QuoteRequest, context => new GetQuote { Symbol = context.Message.Symbol }) + .TransitionTo(QuoteRequest.Pending) + ) + .During(QuoteRequest.Pending) + .When(QuoteRequest.Completed, b => b.Then((context) => Console.WriteLine("Request Completed!"))) + .When(QuoteRequest.Faulted, b => b.Then((context) => Console.WriteLine("Request Faulted"))) + .When(QuoteRequest.TimeoutExpired, b => b.Then((context) => Console.WriteLine("Request timed out"))) + ); + var instance = new TestState(); + + var requestQuote = new RequestQuote + { + Symbol = "MSFT", + TicketNumber = "8675309", + }; + + ConsumeContext consumeContext = new InternalConsumeContext(requestQuote); + + await machine.RaiseEvent(instance, QuoteRequested, requestQuote, consumeContext); + + await machine.RaiseEvent(instance, QuoteRequest.Completed, new Quote {Symbol = requestQuote.Symbol}); + } + } + + + interface Fault + where T : class + { + } + + + interface Request + where TRequest : class + where TResponse : class + { + /// + /// The name of the request + /// + string Name { get; } + + /// + /// The event that is raised when the request completes and the response is received + /// + Event Completed { get; } + + /// + /// The event raised when the request faults + /// + Event> Faulted { get; } + + /// + /// The event raised when the request times out with no response received + /// + Event TimeoutExpired { get; } + + /// + /// The state that is transitioned to once the request is pending + /// + State Pending { get; } + } + + + interface ConsumeContext + where T : class + { + T Message { get; } + } + + + interface RequestConfigurator + where T : class + where TRequest : class + where TResponse : class + { + Uri ServiceAddress { set; } + TimeSpan Timeout { set; } + } + + + class StateMachineRequestConfigurator : + RequestConfigurator, + RequestSettings + where T : class + where TRequest : class + where TResponse : class + { + Uri _serviceAddress; + TimeSpan _timeout; + + public StateMachineRequestConfigurator() + { + _timeout = TimeSpan.FromSeconds(30); + } + + public RequestSettings Settings + { + get + { + if (_serviceAddress == null) + throw new AutomatonymousException("The ServiceAddress was not specified."); + + return this; + } + } + + public Uri ServiceAddress + { + get { return _serviceAddress; } + set { _serviceAddress = value; } + } + + public TimeSpan Timeout + { + get { return _timeout; } + set { _timeout = value; } + } + } + + static class StateMachineExtensions + { + public static StateMachineModifier Request(this StateMachineModifier modifier, + Action> configureRequest, string requestName, out Request request) + where TInstance : class + where TRequest : class + where TResponse : class + { + var configurator = new StateMachineRequestConfigurator(); + + configureRequest(configurator); + + modifier.Request(requestName, configurator.Settings, out request); + return modifier; + } + + private static void Request( + this StateMachineModifier modifier, string requestName, + RequestSettings settings, out Request request) + where TInstance : class + where TRequest : class + where TResponse : class + { + var smRequest = new StateMachineRequest(requestName, settings); + + modifier.Event("Completed", out Event Completed); + smRequest.Completed = Completed; + + modifier.Event("Faulted", out Event> Faulted); + smRequest.Faulted = Faulted; + + modifier.Event("TimeoutExpired", out Event TimeoutExpired); + smRequest.TimeoutExpired = TimeoutExpired; + + modifier.State("Pending", out State Pending); + smRequest.Pending = Pending; + + request = smRequest; + } + } + + + interface RequestSettings + { + /// + /// The endpoint address of the service that handles the request + /// + Uri ServiceAddress { get; } + + /// + /// The timeout period before the request times out + /// + TimeSpan Timeout { get; } + } + + + class StateMachineRequest : + Request + where TRequest : class + where TResponse : class + { + readonly string _name; + readonly RequestSettings _settings; + + public StateMachineRequest(string requestName, RequestSettings settings) + { + _name = requestName; + _settings = settings; + } + + public string Name + { + get { return _name; } + } + + public Event Completed { get; set; } + public Event> Faulted { get; set; } + public Event TimeoutExpired { get; set; } + + public State Pending { get; set; } + + + public async Task SendRequest(ConsumeContext context, TRequest requestMessage) + where T : class + { + // capture requestId + // send request to endpoint + // schedule timeout for requestId + } + } + + + class InternalConsumeContext : + ConsumeContext + where T : class + { + readonly T _message; + + public InternalConsumeContext(T message) + { + _message = message; + } + + public T Message + { + get { return _message; } + } + } + + + class GetQuote + { + public string Symbol { get; set; } + } + + + class Quote + { + public string Symbol { get; set; } + public decimal Last { get; set; } + public decimal Bid { get; set; } + public decimal Ask { get; set; } + } + + + class TestState + { + public string TicketNumber { get; set; } + public int CurrentState { get; set; } + + public Guid QuoteRequestId { get; set; } + } + + + class RequestQuote + { + public string TicketNumber { get; set; } + public string Symbol { get; set; } + } + + + static class RequestExtensions + { + public static EventActivityBinder Request( + this EventActivityBinder binder, Request request, + Func, TRequest> requestMessageFactory) + // Action> action) + where TInstance : class + where TRequest : class + where TResponse : class where TData : class + { + var activity = new RequestActivity(request, requestMessageFactory); + + return binder.Add(activity); + } + } + + + class RequestActivity : + Activity + where TRequest : class + where TResponse : class + where TData : class + { + readonly Request _request; + readonly Func, TRequest> _requestMessageFactory; + + public RequestActivity(Request request, Func, TRequest> requestMessageFactory) + { + _request = request; + _requestMessageFactory = requestMessageFactory; + } + + public void Accept(StateMachineVisitor visitor) + { + visitor.Visit(this); + } + + public async Task Execute(BehaviorContext context, Behavior next) + { + ConsumeContext consumeContext; + if (!context.TryGetPayload(out consumeContext)) + throw new ArgumentException("The ConsumeContext was not available"); + + + TRequest requestMessage = _requestMessageFactory(consumeContext); + + await next.Execute(context); + } + + public Task Faulted(BehaviorExceptionContext context, Behavior next) + where TException : Exception + { + return next.Faulted(context); + } + + public void Probe(ProbeContext context) + { + } + } + + + static class ExpressionExtensions + { + public static PropertyInfo GetPropertyInfo(this Expression> expression) + { + return expression.GetMemberExpression().Member as PropertyInfo; + } + + public static PropertyInfo GetPropertyInfo(this Expression> expression) + { + return expression.GetMemberExpression().Member as PropertyInfo; + } + + public static MemberExpression GetMemberExpression(this Expression> expression) + { + if (expression == null) + throw new ArgumentNullException("expression"); + + return GetMemberExpression(expression.Body); + } + + public static MemberExpression GetMemberExpression(this Expression> expression) + { + if (expression == null) + throw new ArgumentNullException("expression"); + return GetMemberExpression(expression.Body); + } + + static MemberExpression GetMemberExpression(Expression body) + { + if (body == null) + throw new ArgumentNullException("body"); + + MemberExpression memberExpression = null; + if (body.NodeType == ExpressionType.Convert) + { + var unaryExpression = (UnaryExpression)body; + memberExpression = unaryExpression.Operand as MemberExpression; + } + else if (body.NodeType == ExpressionType.MemberAccess) + memberExpression = body as MemberExpression; + + if (memberExpression == null) + throw new ArgumentException("Expression is not a member access"); + + return memberExpression; + } + } + } +} diff --git a/src/Automatonymous.Tests/Dynamic Modify/SerializeState_Specs.cs b/src/Automatonymous.Tests/Dynamic Modify/SerializeState_Specs.cs new file mode 100644 index 0000000..57bfc40 --- /dev/null +++ b/src/Automatonymous.Tests/Dynamic Modify/SerializeState_Specs.cs @@ -0,0 +1,76 @@ +namespace Automatonymous.Tests.DynamicModify +{ + using System; + using System.Threading.Tasks; + using NUnit.Framework; + + + [TestFixture(Category = "Dynamic Modify")] + public class Serializing_a_state_instance + { + [Test] + public async Task Should_properly_handle_the_state_property() + { + State True = null; + State False = null; + Event Thing = null; + + var instance = new Instance(); + var machine = AutomatonymousStateMachine + .Create(builder => builder + .State("True", out True) + .State("False", out False) + .Event("Thing", out Thing) + .During(builder.Initial) + .When(Thing, context => context.Data.Condition, b => b.TransitionTo(True)) + .When(Thing, context => !context.Data.Condition, b => b.TransitionTo(False)) + ); + + await machine.RaiseEvent(instance, Thing, new Data + { + Condition = true + }); + Assert.AreEqual(True, instance.CurrentState); + + var serializer = new JsonStateSerializer, Instance>(machine); + + string body = serializer.Serialize(instance); + + Console.WriteLine("Body: {0}", body); + var reInstance = serializer.Deserialize(body); + + Assert.AreEqual(True, reInstance.CurrentState); + } + + + class Instance + { + public State CurrentState { get; set; } + } + + + class InstanceStateMachine : + AutomatonymousStateMachine + { + public InstanceStateMachine() + { + During(Initial, + When(Thing, context => context.Data.Condition) + .TransitionTo(True), + When(Thing, context => !context.Data.Condition) + .TransitionTo(False)); + } + + public State True { get; private set; } + public State False { get; private set; } + + public Event Thing { get; private set; } + } + + + class Data + { + public bool Condition { get; set; } + } + } +} diff --git a/src/Automatonymous.Tests/Dynamic Modify/StateExpression_Specs.cs b/src/Automatonymous.Tests/Dynamic Modify/StateExpression_Specs.cs new file mode 100644 index 0000000..934e518 --- /dev/null +++ b/src/Automatonymous.Tests/Dynamic Modify/StateExpression_Specs.cs @@ -0,0 +1,256 @@ +namespace Automatonymous.Tests.DynamicModify +{ + using System; + using System.Linq.Expressions; + using NUnit.Framework; + + + [TestFixture(Category = "Dynamic Modify")] + public class When_creating_a_state_expression_for_int + { + [Test] + public void It_should_match_the_state_requested() + { + var expression = ((StateMachine)_machine).Accessor.GetStateExpression(Running); + + Func filter = expression.Compile(); + + bool result = filter(_instance); + + Assert.That(result, Is.True); + } + + [Test] + public void It_should_not_match_the_state_requested() + { + var expression = ((StateMachine)_machine).Accessor.GetStateExpression(_machine.Initial); + + Func filter = expression.Compile(); + + bool result = filter(_instance); + + Assert.That(result, Is.False); + } + + [Test] + public void It_should_match_the_state_not_requested() + { + var expression = ((StateMachine)_machine).Accessor.GetStateExpression(_machine.Initial); + + var filter = Expression.Lambda>(Expression.Not(expression.Body), expression.Parameters).Compile(); + + bool result = filter(_instance); + + Assert.That(result, Is.True); + } + + State Running; + Event Started; + StateMachine _machine; + Instance _instance; + + [OneTimeSetUp] + public void A_state_is_declared() + { + _machine = AutomatonymousStateMachine + .Create(builder => builder + .State("Running", out Running) + .Event("Started", out Started) + .InstanceState(x => x.CurrentState, Running) + .Initially() + .When(Started, b => b.TransitionTo(Running)) + ); + _instance = new Instance(); + + _machine.RaiseEvent(_instance, Started).Wait(); + } + + + /// + /// For this instance, the state is actually stored as a string. Therefore, + /// it is important that the StateMachine property is initialized when the + /// instance is hydrated, as it is used to get the State for the name of + /// the current state. This makes it easier to save the instance using + /// an ORM that doesn't support user types (cough, EF, cough). + /// + class Instance + { + /// + /// The CurrentState is exposed as a string for the ORM + /// + public int CurrentState { get; private set; } + } + } + + + [TestFixture(Category = "Dynamic Modify")] + public class When_creating_a_state_expression_for_string + { + [Test] + public void It_should_match_the_state_requested() + { + var expression = ((StateMachine)_machine).Accessor.GetStateExpression(Running); + + Func filter = expression.Compile(); + + bool result = filter(_instance); + + Assert.That(result, Is.True); + } + + [Test] + public void It_should_not_match_the_state_requested() + { + var expression = ((StateMachine)_machine).Accessor.GetStateExpression(_machine.Initial); + + Func filter = expression.Compile(); + + bool result = filter(_instance); + + Assert.That(result, Is.False); + } + + [Test] + public void It_should_match_the_state_not_requested() + { + var expression = ((StateMachine)_machine).Accessor.GetStateExpression(_machine.Initial); + + var filter = Expression.Lambda>(Expression.Not(expression.Body), expression.Parameters).Compile(); + + bool result = filter(_instance); + + Assert.That(result, Is.True); + } + + State Running; + Event Started; + StateMachine _machine; + Instance _instance; + + [OneTimeSetUp] + public void A_state_is_declared() + { + _machine = AutomatonymousStateMachine + .Create(builder => builder + .State("Running", out Running) + .Event("Started", out Started) + .InstanceState(x => x.CurrentState) + .Initially() + .When(Started, b => b.TransitionTo(Running)) + ); + _instance = new Instance(); + + _machine.RaiseEvent(_instance, Started).Wait(); + } + + + /// + /// For this instance, the state is actually stored as a string. Therefore, + /// it is important that the StateMachine property is initialized when the + /// instance is hydrated, as it is used to get the State for the name of + /// the current state. This makes it easier to save the instance using + /// an ORM that doesn't support user types (cough, EF, cough). + /// + class Instance + { + /// + /// The CurrentState is exposed as a string for the ORM + /// + public string CurrentState { get; private set; } + } + } + + [TestFixture(Category = "Dynamic Modify")] + public class When_creating_a_state_expression_for_raw + { + [Test] + public void It_should_match_the_state_requested() + { + var expression = ((StateMachine)_machine).Accessor.GetStateExpression(Running); + + Func filter = expression.Compile(); + + bool result = filter(_instance); + + Assert.That(result, Is.True); + } + + [Test] + public void It_should_not_match_the_state_requested() + { + var expression = ((StateMachine)_machine).Accessor.GetStateExpression(_machine.Initial); + + Func filter = expression.Compile(); + + bool result = filter(_instance); + + Assert.That(result, Is.False); + } + + [Test] + public void It_should_match_the_state_not_requested() + { + var expression = ((StateMachine)_machine).Accessor.GetStateExpression(_machine.Initial); + + var filter = Expression.Lambda>(Expression.Not(expression.Body), expression.Parameters).Compile(); + + bool result = filter(_instance); + + Assert.That(result, Is.True); + } + + State Running; + Event Started; + StateMachine _machine; + Instance _instance; + + [OneTimeSetUp] + public void A_state_is_declared() + { + _machine = AutomatonymousStateMachine + .Create(builder => builder + .State("Running", out Running) + .Event("Started", out Started) + .InstanceState(x => x.CurrentState) + .Initially() + .When(Started, b => b.TransitionTo(Running)) + ); + _instance = new Instance(); + + _machine.RaiseEvent(_instance, Started).Wait(); + } + + + /// + /// For this instance, the state is actually stored as a string. Therefore, + /// it is important that the StateMachine property is initialized when the + /// instance is hydrated, as it is used to get the State for the name of + /// the current state. This makes it easier to save the instance using + /// an ORM that doesn't support user types (cough, EF, cough). + /// + class Instance + { + /// + /// The CurrentState is exposed as a string for the ORM + /// + public State CurrentState { get; private set; } + } + + + class TestStateMachine : + AutomatonymousStateMachine + { + public TestStateMachine() + { + InstanceState(x => x.CurrentState); + + Initially( + When(Started) + .TransitionTo(Running)); + } + + public Event Started { get; private set; } + public State Running { get; private set; } + } + } +} diff --git a/src/Automatonymous.Tests/Dynamic Modify/State_Specs.cs b/src/Automatonymous.Tests/Dynamic Modify/State_Specs.cs new file mode 100644 index 0000000..549dab5 --- /dev/null +++ b/src/Automatonymous.Tests/Dynamic Modify/State_Specs.cs @@ -0,0 +1,148 @@ +namespace Automatonymous.Tests.DynamicModify +{ + using NUnit.Framework; + using States; + + + [TestFixture(Category = "Dynamic Modify")] + public class When_a_state_is_declared + { + [Test] + public void It_should_capture_the_name_of_final() + { + Assert.AreEqual("Final", _machine.Final.Name); + } + + [Test] + public void It_should_capture_the_name_of_initial() + { + Assert.AreEqual("Initial", _machine.Initial.Name); + } + + [Test] + public void It_should_capture_the_name_of_running() + { + Assert.AreEqual("Running", Running.Name); + } + + [Test] + public void Should_be_an_instance_of_the_proper_type() + { + Assert.IsInstanceOf>(_machine.Initial); + } + + + class Instance + { + public State CurrentState { get; set; } + } + + State Running; + StateMachine _machine; + + [OneTimeSetUp] + public void A_state_is_declared() + { + _machine = AutomatonymousStateMachine + .Create(builder => builder + .State("Running", out Running) + .InstanceState(x => x.CurrentState) + ); + } + } + + + [TestFixture(Category = "Dynamic Modify")] + public class When_a_state_is_stored_another_way + { + [Test] + public void It_should_get_the_name_right() + { + Assert.AreEqual("Running", _instance.CurrentState); + } + + Event Started; + StateMachine _machine; + Instance _instance; + + [OneTimeSetUp] + public void A_state_is_declared() + { + _machine = AutomatonymousStateMachine + .Create(builder => builder + .Event("Started", out Started) + .State("Running", out State Running) + .InstanceState(x => x.CurrentState) + .Initially() + .When(Started, b => b.TransitionTo(Running)) + ); + _instance = new Instance(); + + _machine.RaiseEvent(_instance, Started).Wait(); + } + + + /// + /// For this instance, the state is actually stored as a string. Therefore, + /// it is important that the StateMachine property is initialized when the + /// instance is hydrated, as it is used to get the State for the name of + /// the current state. This makes it easier to save the instance using + /// an ORM that doesn't support user types (cough, EF, cough). + /// + class Instance + { + /// + /// The CurrentState is exposed as a string for the ORM + /// + public string CurrentState { get; private set; } + } + } + + + [TestFixture(Category = "Dynamic Modify")] + public class When_storing_state_as_an_int + { + [Test] + public void It_should_get_the_name_right() + { + Assert.AreEqual(Running, _machine.GetState(_instance).Result); + } + + State Running; + Event Started; + StateMachine _machine; + Instance _instance; + + [OneTimeSetUp] + public void A_state_is_declared() + { + _machine = AutomatonymousStateMachine + .Create(builder => builder + .State("Running", out Running) + .Event("Started", out Started) + .InstanceState(x => x.CurrentState, Running) + .Initially() + .When(Started, b => b.TransitionTo(Running)) + ); + _instance = new Instance(); + + _machine.RaiseEvent(_instance, Started).Wait(); + } + + + /// + /// For this instance, the state is actually stored as a string. Therefore, + /// it is important that the StateMachine property is initialized when the + /// instance is hydrated, as it is used to get the State for the name of + /// the current state. This makes it easier to save the instance using + /// an ORM that doesn't support user types (cough, EF, cough). + /// + class Instance + { + /// + /// The CurrentState is exposed as a string for the ORM + /// + public int CurrentState { get; private set; } + } + } +} diff --git a/src/Automatonymous.Tests/Dynamic Modify/Telephone_Sample.cs b/src/Automatonymous.Tests/Dynamic Modify/Telephone_Sample.cs new file mode 100644 index 0000000..c998a63 --- /dev/null +++ b/src/Automatonymous.Tests/Dynamic Modify/Telephone_Sample.cs @@ -0,0 +1,246 @@ +namespace Automatonymous.Tests.DynamicModify +{ + namespace Telephone_Sample + { + using System; + using System.Diagnostics; + using System.Threading.Tasks; + using Graphing; + using GreenPipes; + using GreenPipes.Introspection; + using NUnit.Framework; + using Visualizer; + + + [TestFixture(Category = "Dynamic Modify")] + public class A_simple_phone_call + { + [Test] + public async Task Should_be_short_and_sweet() + { + var phone = new PrincessModelTelephone(); + await _machine.RaiseEvent(phone, _model.ServiceEstablished, new PhoneServiceEstablished {Digits = "555-1212"}); + + await _machine.RaiseEvent(phone, _model.CallDialed); + await _machine.RaiseEvent(phone, _model.CallConnected); + + await Task.Delay(50); + + await _machine.RaiseEvent(phone, _model.HungUp); + + Assert.AreEqual(_model.OffHook.Name, phone.CurrentState); + Assert.GreaterOrEqual(phone.CallTimer.ElapsedMilliseconds, 45); + } + + PhoneServiceStateModel _model; + StateMachine _machine; + + [OneTimeSetUp] + public void Setup() + { + _model = new PhoneServiceStateModel(); + _machine = _model.Machine; + } + } + + + [TestFixture(Category = "Dynamic Modify")] + public class Visualize + { + [Test] + public void Draw() + { + var machine = new PhoneServiceStateModel().Machine; + var generator = new StateMachineGraphvizGenerator(machine.GetGraph()); + + var dotFile = generator.CreateDotFile(); + + Console.WriteLine(dotFile); + } + + [Test] + public void Should_return_a_wonderful_breakdown_of_the_guts_inside_it() + { + ProbeResult result = new PhoneServiceStateModel().Machine.GetProbeResult(); + + Console.WriteLine(result.ToJsonString()); + } + + PhoneServiceStateModel _model; + StateMachine _machine; + + [OneTimeSetUp] + public void Setup() + { + _model = new PhoneServiceStateModel(); + _machine = _model.Machine; + } + } + + + [TestFixture(Category = "Dynamic Modify")] + public class A_short_time_on_hold + { + [Test] + public async Task Should_be_short_and_sweet() + { + var phone = new PrincessModelTelephone(); + await _machine.RaiseEvent(phone, _model.ServiceEstablished, new PhoneServiceEstablished {Digits = "555-1212"}); + + await _machine.RaiseEvent(phone, _model.CallDialed); + await _machine.RaiseEvent(phone, _model.CallConnected); + + await Task.Delay(50); + + await _machine.RaiseEvent(phone, _model.PlacedOnHold); + await _machine.RaiseEvent(phone, _model.TakenOffHold); + await _machine.RaiseEvent(phone, _model.HungUp); + + Assert.AreEqual(_model.OffHook.Name, phone.CurrentState); + Assert.GreaterOrEqual(phone.CallTimer.ElapsedMilliseconds, 45); + } + + PhoneServiceStateModel _model; + StateMachine _machine; + + [OneTimeSetUp] + public void Setup() + { + _model = new PhoneServiceStateModel(); + _machine = _model.Machine; + } + } + + + [TestFixture(Category = "Dynamic Modify")] + public class An_extended_time_on_hold + { + [Test] + public async Task Should_end__badly() + { + var phone = new PrincessModelTelephone(); + await _machine.RaiseEvent(phone, _model.ServiceEstablished, new PhoneServiceEstablished {Digits = "555-1212"}); + + await _machine.RaiseEvent(phone, _model.CallDialed); + await _machine.RaiseEvent(phone, _model.CallConnected); + await _machine.RaiseEvent(phone, _model.PlacedOnHold); + + await Task.Delay(50); + + await _machine.RaiseEvent(phone, _model.HungUp); + + Assert.AreEqual(_model.OffHook.Name, phone.CurrentState); + Assert.GreaterOrEqual(phone.CallTimer.ElapsedMilliseconds, 45); + } + + PhoneServiceStateModel _model; + StateMachine _machine; + + [OneTimeSetUp] + public void Setup() + { + _model = new PhoneServiceStateModel(); + _machine = _model.Machine; + } + } + + + class PrincessModelTelephone + { + public PrincessModelTelephone() + { + CallTimer = new Stopwatch(); + } + + public string CurrentState { get; set; } + + public Stopwatch CallTimer { get; private set; } + + public string Number { get; set; } + } + + class PhoneServiceEstablished + { + public string Digits { get; set; } + } + + class PhoneServiceStateModel + { + public StateMachine Machine; + + public PhoneServiceStateModel() + { + Machine = CreateDynamically(); + } + + public State OffHook; + public State Ringing; + public State Connected; + public State OnHold; + public State PhoneDestroyed; + + public Event ServiceEstablished; + public Event CallDialed; + public Event HungUp; + public Event CallConnected; + public Event LeftMessage; + public Event PlacedOnHold; + public Event TakenOffHold; + public Event PhoneHurledAgainstWall; + + void StopCallTimer(PrincessModelTelephone instance) + { + instance.CallTimer.Stop(); + + Console.WriteLine("Stopped call timer at {0}ms", instance.CallTimer.ElapsedMilliseconds); + } + + void StartCallTimer(PrincessModelTelephone instance) + { + Console.WriteLine("Started call timer"); + + instance.CallTimer.Start(); + } + + public StateMachine CreateDynamically() + { + return AutomatonymousStateMachine + .Create(builder => builder + .State("OffHook", out OffHook) + .State("Ringing", out Ringing) + .State("Connected", out Connected) + .State("PhoneDestroyed", out PhoneDestroyed) + .Event("ServiceEstablished", out ServiceEstablished) + .Event("CallDialed", out CallDialed) + .Event("HungUp", out HungUp) + .Event("CallConnected", out CallConnected) + .Event("LeftMessage", out LeftMessage) + .Event("PlacedOnHold", out PlacedOnHold) + .Event("TakenOffHold", out TakenOffHold) + .Event("PhoneHurledAgainstWall", out PhoneHurledAgainstWall) + .InstanceState(x => x.CurrentState) + .SubState("OnHold", Connected, out OnHold) + .Initially() + .When(ServiceEstablished, b => b + .Then(context => context.Instance.Number = context.Data.Digits) + .TransitionTo(OffHook)) + .During(OffHook) + .When(CallDialed, b => b.TransitionTo(Ringing)) + .During(Ringing) + .When(HungUp, b => b.TransitionTo(OffHook)) + .When(CallConnected, b => b.TransitionTo(Connected)) + .During(Connected) + .When(LeftMessage, b => b.TransitionTo(OffHook)) + .When(HungUp, b => b.TransitionTo(OffHook)) + .When(PlacedOnHold, b => b.TransitionTo(OnHold)) + .During(OnHold) + .When(TakenOffHold, b => b.TransitionTo(Connected)) + .When(PhoneHurledAgainstWall, b => b.TransitionTo(PhoneDestroyed)) + .DuringAny() + .When(Connected.Enter, b => b.Then(context => StartCallTimer(context.Instance))) + .When(Connected.Leave, b => b.Then(context => StopCallTimer(context.Instance))) + ); + } + } + } +} diff --git a/src/Automatonymous.Tests/Dynamic Modify/Transition_Specs.cs b/src/Automatonymous.Tests/Dynamic Modify/Transition_Specs.cs new file mode 100644 index 0000000..6f32402 --- /dev/null +++ b/src/Automatonymous.Tests/Dynamic Modify/Transition_Specs.cs @@ -0,0 +1,158 @@ +namespace Automatonymous.Tests.DynamicModify +{ + using System; + using NUnit.Framework; + + + [TestFixture(Category = "Dynamic Modify")] + public class Explicitly_transitioning_to_a_state + { + [Test] + public void Should_call_the_enter_event() + { + Assert.IsTrue(_instance.EnterCalled); + } + + [Test] + public void Should_have_first_moved_to_initial() + { + Assert.AreEqual(null, _observer.Events[0].Previous); + Assert.AreEqual(_machine.Initial, _observer.Events[0].Current); + } + + [Test] + public void Should_have_second_moved_to_running() + { + Assert.AreEqual(_machine.Initial, _observer.Events[1].Previous); + Assert.AreEqual(Running, _observer.Events[1].Current); + } + + [Test] + public void Should_raise_the_event() + { + Assert.AreEqual(2, _observer.Events.Count); + } + + State Running; + Instance _instance; + StateMachine _machine; + StateChangeObserver _observer; + + [OneTimeSetUp] + public void Specifying_an_event_activity() + { + _instance = new Instance(); + _machine = AutomatonymousStateMachine + .Create(builder => builder + .State("Running", out Running) + .Event("Initialized", out Event Initialized) + .Event("Finish", out Event Finish) + .During(builder.Initial) + .When(Initialized, b => b.TransitionTo(Running)) + .During(Running) + .When(Finish, b => b.Finalize()) + .WhenEnter(Running, x => x.Then(context => context.Instance.EnterCalled = true)) + ); + _observer = new StateChangeObserver(); + + using (IDisposable subscription = _machine.ConnectStateObserver(_observer)) + { + _machine.TransitionToState(_instance, Running) + .Wait(); + } + } + + + class Instance + { + public State CurrentState { get; set; } + public bool EnterCalled { get; set; } + } + } + + + [TestFixture(Category = "Dynamic Modify")] + public class Transitioning_to_a_state_from_a_state + { + [Test] + public void Should_call_the_enter_event() + { + Assert.IsTrue(_instance.EnterCalled); + } + + [Test] + public void Should_have_first_moved_to_initial() + { + Assert.AreEqual(null, _observer.Events[0].Previous); + Assert.AreEqual(_machine.Initial, _observer.Events[0].Current); + } + + [Test] + public void Should_have_invoked_final_entered() + { + Assert.IsTrue(_instance.FinalEntered); + } + + [Test] + public void Should_have_second_moved_to_running() + { + Assert.AreEqual(_machine.Initial, _observer.Events[1].Previous); + Assert.AreEqual(Running, _observer.Events[1].Current); + } + + [Test] + public void Should_have_third_moved_to_final() + { + Assert.AreEqual(Running, _observer.Events[2].Previous); + Assert.AreEqual(_machine.Final, _observer.Events[2].Current); + } + + [Test] + public void Should_raise_the_event() + { + Assert.AreEqual(3, _observer.Events.Count); + } + + State Running; + Event Initialized; + Event Finish; + Instance _instance; + StateMachine _machine; + StateChangeObserver _observer; + + [OneTimeSetUp] + public void Specifying_an_event_activity() + { + _instance = new Instance(); + _machine = AutomatonymousStateMachine + .Create(builder => builder + .State("Running", out Running) + .Event("Initialized", out Initialized) + .Event("Finish", out Finish) + .During(builder.Initial) + .When(Initialized, b => b.TransitionTo(Running)) + .During(Running) + .When(Finish, b => b.Finalize()) + .BeforeEnter(builder.Final, x => x.Then(context => context.Instance.FinalEntered = true)) + .WhenEnter(Running, x => x.Then(context => context.Instance.EnterCalled = true)) + ); + _observer = new StateChangeObserver(); + + using (IDisposable subscription = _machine.ConnectStateObserver(_observer)) + { + _machine.RaiseEvent(_instance, Initialized) + .Wait(); + _machine.RaiseEvent(_instance, Finish); + } + } + + + class Instance + { + public State CurrentState { get; set; } + public bool EnterCalled { get; set; } + + public bool FinalEntered { get; set; } + } + } +} diff --git a/src/Automatonymous.Tests/Dynamic Modify/UnobservedEvent_Specs.cs b/src/Automatonymous.Tests/Dynamic Modify/UnobservedEvent_Specs.cs new file mode 100644 index 0000000..c9e2d93 --- /dev/null +++ b/src/Automatonymous.Tests/Dynamic Modify/UnobservedEvent_Specs.cs @@ -0,0 +1,216 @@ +namespace Automatonymous.Tests.DynamicModify +{ + using System; + using System.Linq; + using System.Threading.Tasks; + using GreenPipes; + using GreenPipes.Introspection; + using NUnit.Framework; + + + [TestFixture(Category = "Dynamic Modify")] + public class Raising_an_unhandled_event_in_a_state + { + [Test] + public async Task Should_throw_an_exception_when_event_is_not_allowed_in_current_state() + { + var instance = new Instance(); + + await _machine.RaiseEvent(instance, x => Start); + + Assert.That(async () => await _machine.RaiseEvent(instance, x => Start), Throws.TypeOf()); + } + + StateMachine _machine; + Event Start; + + class Instance + { + public State CurrentState { get; set; } + } + + [OneTimeSetUp] + public void A_state_is_declared() + { + _machine = AutomatonymousStateMachine + .Create(builder => builder + .State("Running", out State Running) + .Event("Start", out Start) + .Initially() + .When(Start, b => b.TransitionTo(Running)) + ); + } + } + + + [TestFixture(Category = "Dynamic Modify")] + public class Raising_an_ignored_event_that_is_not_filtered + { + [Test] + public async Task Should_throw_an_exception_when_event_is_not_allowed_in_current_state() + { + var instance = new Instance(); + + await _machine.RaiseEvent(instance, Start); + + Assert.That(async () => await _machine.RaiseEvent(instance, Charge, new A {Volts = 12}), + Throws.TypeOf()); + } + + Event Start; + Event Charge; + StateMachine _machine; + + + class Instance + { + public State CurrentState { get; set; } + public int Volts { get; set; } + } + + + [OneTimeSetUp] + public void A_state_is_declared() + { + _machine = AutomatonymousStateMachine + .Create(builder => builder + .State("Running", out State Running) + .Event("Start", out Start) + .Event("Charge", out Charge) + .Initially() + .When(Start, b => b.TransitionTo(Running)) + .During(Running) + .Ignore(Start) + .Ignore(Charge, x => x.Data.Volts == 9) + ); + } + + + class A + { + public int Volts { get; set; } + } + } + + + [TestFixture(Category = "Dynamic Modify")] + public class Raising_an_ignored_event + { + [Test] + public async Task Should_also_ignore_yet_process_invalid_events() + { + var instance = new Instance(); + + await _machine.RaiseEvent(instance, Start); + + await _machine.RaiseEvent(instance, Charge, new A {Volts = 12}); + + Assert.AreEqual(0, instance.Volts); + } + + [Test] + public async Task Should_have_the_next_event_even_though_ignored() + { + var instance = new Instance(); + + await _machine.RaiseEvent(instance, Start); + + Assert.AreEqual(Running, await _machine.GetState(instance)); + + var nextEvents = await _machine.NextEvents(instance); + + Assert.IsTrue(nextEvents.Any(x => x.Name.Equals("Charge"))); + } + + [Test] + public void Should_return_a_wonderful_breakdown_of_the_guts_inside_it() + { + ProbeResult result = _machine.GetProbeResult(); + + Console.WriteLine(result.ToJsonString()); + } + + [Test] + public async Task Should_silenty_ignore_the_invalid_event() + { + var instance = new Instance(); + + await _machine.RaiseEvent(instance, Start); + + await _machine.RaiseEvent(instance, Start); + } + + State Running; + Event Start; + Event Charge; + StateMachine _machine; + + + class Instance + { + public State CurrentState { get; set; } + public int Volts { get; set; } + } + + + [OneTimeSetUp] + public void A_state_is_declared() + { + _machine = AutomatonymousStateMachine + .Create(builder => builder + .State("Running", out Running) + .Event("Start", out Start) + .Event("Charge", out Charge) + .Initially() + .When(Start, b => b.TransitionTo(Running)) + .During(Running) + .Ignore(Start) + .Ignore(Charge) + ); + } + + + class A + { + public int Volts { get; set; } + } + } + + + [TestFixture(Category = "Dynamic Modify")] + public class Raising_an_unhandled_event_when_the_state_machine_ignores_all_unhandled_events + { + [Test] + public async Task Should_silenty_ignore_the_invalid_event() + { + var instance = new Instance(); + + await _machine.RaiseEvent(instance, Start); + + await _machine.RaiseEvent(instance, Start); + } + + Event Start; + StateMachine _machine; + + + class Instance + { + public State CurrentState { get; set; } + } + + + [OneTimeSetUp] + public void A_state_is_declared() + { + _machine = AutomatonymousStateMachine + .Create(builder => builder + .Event("Start", out Start) + .State("Running", out State Running) + .OnUnhandledEvent(x => x.Ignore()) + .Initially() + .When(Start, b => b.TransitionTo(Running)) + ); + } + } +} diff --git a/src/Automatonymous.Tests/Dynamic Modify/Visualizer_Specs.cs b/src/Automatonymous.Tests/Dynamic Modify/Visualizer_Specs.cs new file mode 100644 index 0000000..42baffb --- /dev/null +++ b/src/Automatonymous.Tests/Dynamic Modify/Visualizer_Specs.cs @@ -0,0 +1,104 @@ +namespace Automatonymous.Tests.DynamicModify +{ + using System; + using Graphing; + using NUnit.Framework; + using Visualizer; + + + [TestFixture(Category = "Dynamic Modify")] + public class When_visualizing_a_state_machine + { + [Test] + public void Should_parse_the_graph() + { + Assert.IsNotNull(_graph); + } + + [Test] + public void Should_show_the_goods() + { + var generator = new StateMachineGraphvizGenerator(_graph); + + string dots = generator.CreateDotFile(); + + Console.WriteLine(dots); + + var expected = Expected.Replace("\r", "").Replace("\n", Environment.NewLine); + + Assert.AreEqual(expected, dots); + } + + StateMachine _machine; + StateMachineGraph _graph; + + [OneTimeSetUp] + public void Setup() + { + _machine = AutomatonymousStateMachine + .Create(b => b + .State("Running", out State Running) + .State("Suspended", out State Suspended) + .State("Failed", out State Failed) + .Event("Initialized", out Event Initialized) + .Event("Suspend", out Event Suspend) + .Event("Resume", out Event Resume) + .Event("Finished", out Event Finished) + .Event("Restart", out Event Restart) + .During(b.Initial) + .When(Initialized, (binder) => binder + .TransitionTo(Running) + .Catch(h => h.TransitionTo(Failed)) + ) + .During(Running) + .When(Finished, (binder) => binder.TransitionTo(b.Final)) + .When(Suspend, (binder) => binder.TransitionTo(Suspended)) + .Ignore(Resume) + .During(Suspended) + .When(Resume, b => b.TransitionTo(Running)) + .During(Failed) + .When(Restart, context => context.Data.Name != null, b => b.TransitionTo(Running)) + ); + + _graph = _machine.GetGraph(); + } + + const string Expected = @"digraph G { +0 [shape=ellipse, label=""Initial""]; +1 [shape=ellipse, label=""Running""]; +2 [shape=ellipse, label=""Failed""]; +3 [shape=ellipse, label=""Final""]; +4 [shape=ellipse, label=""Suspended""]; +5 [shape=rectangle, label=""Initialized""]; +6 [shape=rectangle, label=""Exception""]; +7 [shape=rectangle, label=""Finished""]; +8 [shape=rectangle, label=""Suspend""]; +9 [shape=rectangle, label=""Resume""]; +10 [shape=rectangle, label=""Restart""]; +0 -> 5; +1 -> 7; +1 -> 8; +2 -> 10; +4 -> 9; +5 -> 1; +5 -> 6; +6 -> 2; +7 -> 3; +8 -> 4; +9 -> 1; +10 -> 1; +}"; + + + class Instance + { + public State CurrentState { get; set; } + } + + + class RestartData + { + public string Name { get; set; } + } + } +} diff --git a/src/Automatonymous/AutomatonymousStateMachine.cs b/src/Automatonymous/AutomatonymousStateMachine.cs index 4a993e9..8e33190 100644 --- a/src/Automatonymous/AutomatonymousStateMachine.cs +++ b/src/Automatonymous/AutomatonymousStateMachine.cs @@ -9,6 +9,7 @@ namespace Automatonymous using System.Threading.Tasks; using Accessors; using Activities; + using Automatonymous.Builder; using Binders; using Contexts; using Events; @@ -109,12 +110,15 @@ async Task StateMachine.RaiseEvent(EventContext cont public State GetState(string name) { - if (_stateCache.TryGetValue(name, out var result)) + if (TryGetState(name, out var result)) return result; throw new UnknownStateException(_name, name); } + public bool TryGetState(string name, out State state) + => _stateCache.TryGetValue(name, out state); + public IEnumerable States => _stateCache.Values; Event StateMachine.GetEvent(string name) @@ -196,7 +200,7 @@ Task DefaultUnhandledEventCallback(UnhandledEventContext context) /// Please note, the state machine can only manage one property at a given time per instance, /// and the best practice is to manage one property per machine. /// - protected void InstanceState(Expression> instanceStateProperty) + protected internal void InstanceState(Expression> instanceStateProperty) { var stateAccessor = new RawStateAccessor(this, instanceStateProperty, _stateObservers); @@ -207,7 +211,7 @@ protected void InstanceState(Expression> instanceStatePro /// Declares the property to hold the instance's state as a string (the state name is stored in the property) /// /// - protected void InstanceState(Expression> instanceStateProperty) + protected internal void InstanceState(Expression> instanceStateProperty) { var stateAccessor = new StringStateAccessor(this, instanceStateProperty, _stateObservers); @@ -219,7 +223,7 @@ protected void InstanceState(Expression> instanceStatePr /// /// /// Specifies the states, in order, to which the int values should be assigned - protected void InstanceState(Expression> instanceStateProperty, params State[] states) + protected internal void InstanceState(Expression> instanceStateProperty, params State[] states) { var stateIndex = new StateAccessorIndex(this, _initial, _final, states); @@ -232,7 +236,7 @@ protected void InstanceState(Expression> instanceStatePrope /// Specifies the name of the state machine /// /// - protected void Name(string machineName) + protected internal void Name(string machineName) { if (string.IsNullOrWhiteSpace(machineName)) throw new ArgumentException("The machine name must not be empty", nameof(machineName)); @@ -244,44 +248,41 @@ protected void Name(string machineName) /// Declares an event, and initializes the event property /// /// - protected virtual void Event(Expression> propertyExpression) - { - PropertyInfo property = propertyExpression.GetPropertyInfo(); - - DeclareTriggerEvent(property); - } + protected internal virtual void Event(Expression> propertyExpression) + => DeclarePropertyBasedEvent(prop => DeclareTriggerEvent(prop.Name), propertyExpression.GetPropertyInfo()); - void DeclareTriggerEvent(PropertyInfo property) - { - string name = property.Name; + protected internal virtual Event Event(string name) + => DeclareTriggerEvent(name); - var @event = new TriggerEvent(name); - - ConfigurationHelpers.InitializeEvent(this, property, @event); - - _eventCache[name] = new StateMachineEvent(@event, false); - } + Event DeclareTriggerEvent(string name) + => DeclareEvent(name => new TriggerEvent(name), name); /// /// Declares a data event on the state machine, and initializes the property /// /// The event property - protected virtual void Event(Expression>> propertyExpression) - { - PropertyInfo property = propertyExpression.GetPropertyInfo(); - - DeclareDataEvent(property); - } + protected internal virtual void Event(Expression>> propertyExpression) + => DeclarePropertyBasedEvent(prop => DeclareDataEvent(prop.Name), propertyExpression.GetPropertyInfo()); - void DeclareDataEvent(PropertyInfo property) - { - string name = property.Name; + protected internal virtual Event Event(string name) + => DeclareDataEvent(name); - var @event = new DataEvent(name); + Event DeclareDataEvent(string name) + => DeclareEvent(name => new DataEvent(name), name); + void DeclarePropertyBasedEvent(Func ctor, PropertyInfo property) + where TEvent : Event + { + TEvent @event = ctor(property); ConfigurationHelpers.InitializeEvent(this, property, @event); + } + TEvent DeclareEvent(Func ctor, string name) + where TEvent : Event + { + var @event = ctor(name); _eventCache[name] = new StateMachineEvent(@event, false); + return @event; } /// @@ -289,7 +290,7 @@ void DeclareDataEvent(PropertyInfo property) /// /// The property /// The event property on the property - protected virtual void Event(Expression> propertyExpression, + protected internal virtual void Event(Expression> propertyExpression, Expression>> eventPropertyExpression) where TProperty : class { @@ -317,7 +318,7 @@ protected virtual void Event(Expression> propertyE /// The composite event /// The property in the instance used to track the state of the composite event /// The events that must be raised before the composite event is raised - protected virtual void CompositeEvent(Expression> propertyExpression, + protected internal virtual void CompositeEvent(Expression> propertyExpression, Expression> trackingPropertyExpression, params Event[] events) { @@ -337,7 +338,7 @@ protected virtual void CompositeEvent(Expression> propertyExpression /// The property in the instance used to track the state of the composite event /// Options on the composite event /// The events that must be raised before the composite event is raised - protected virtual void CompositeEvent(Expression> propertyExpression, + protected internal virtual void CompositeEvent(Expression> propertyExpression, Expression> trackingPropertyExpression, CompositeEventOptions options, params Event[] events) @@ -357,7 +358,7 @@ protected virtual void CompositeEvent(Expression> propertyExpression /// The composite event /// The property in the instance used to track the state of the composite event /// The events that must be raised before the composite event is raised - protected virtual void CompositeEvent(Expression> propertyExpression, + protected internal virtual void CompositeEvent(Expression> propertyExpression, Expression> trackingPropertyExpression, params Event[] events) { @@ -377,7 +378,7 @@ protected virtual void CompositeEvent(Expression> propertyExpression /// The property in the instance used to track the state of the composite event /// Options on the composite event /// The events that must be raised before the composite event is raised - protected virtual void CompositeEvent(Expression> propertyExpression, + protected internal virtual void CompositeEvent(Expression> propertyExpression, Expression> trackingPropertyExpression, CompositeEventOptions options, params Event[] events) @@ -431,17 +432,144 @@ void CompositeEvent(Expression> propertyExpression, CompositeEventSt } } + internal virtual void CompositeEvent(string name, + Expression> trackingPropertyExpression, + params Event[] events) + => CompositeEvent(name, new StructCompositeEventStatusAccessor(trackingPropertyExpression.GetPropertyInfo()), CompositeEventOptions.None, events); + + internal virtual Event CompositeEvent(string name, + Expression> trackingPropertyExpression, + CompositeEventOptions options, + params Event[] events) + => CompositeEvent(name, new StructCompositeEventStatusAccessor(trackingPropertyExpression.GetPropertyInfo()), options, events); + + internal virtual Event CompositeEvent(string name, + Expression> trackingPropertyExpression, + params Event[] events) + => CompositeEvent(name, new IntCompositeEventStatusAccessor(trackingPropertyExpression.GetPropertyInfo()), CompositeEventOptions.None, events); + + internal virtual Event CompositeEvent(string name, + Expression> trackingPropertyExpression, + CompositeEventOptions options, + params Event[] events) + => CompositeEvent(name, new IntCompositeEventStatusAccessor(trackingPropertyExpression.GetPropertyInfo()), options, events); + + Event CompositeEvent(string name, CompositeEventStatusAccessor accessor, + CompositeEventOptions options, Event[] events) + { + if (events == null) + throw new ArgumentNullException(nameof(events)); + if (events.Length > 31) + throw new ArgumentException("No more than 31 events can be combined into a single event"); + if (events.Length == 0) + throw new ArgumentException("At least one event must be specified for a composite event"); + if (events.Any(x => x == null)) + throw new ArgumentException("One or more events specified has not yet been initialized"); + + var @event = new TriggerEvent(name); + + _eventCache[name] = new StateMachineEvent(@event, false); + + var complete = new CompositeEventStatus(Enumerable.Range(0, events.Length) + .Aggregate(0, (current, x) => current | (1 << x))); + + for (int i = 0; i < events.Length; i++) + { + int flag = 1 << i; + + var activity = new CompositeEventActivity(accessor, flag, complete, @event); + + bool Filter(State x) => options.HasFlag(CompositeEventOptions.IncludeInitial) || !Equals(x, Initial); + + foreach (var state in _stateCache.Values.Where(Filter)) + { + During(state, + When(events[i]) + .Execute(activity)); + } + } + + return @event; + } + + protected internal virtual void CompositeEvent(Event @event, + Expression> trackingPropertyExpression, + params Event[] events) + => CompositeEvent(@event, new StructCompositeEventStatusAccessor(trackingPropertyExpression.GetPropertyInfo()), CompositeEventOptions.None, events); + + protected internal virtual Event CompositeEvent(Event @event, + Expression> trackingPropertyExpression, + CompositeEventOptions options, + params Event[] events) + => CompositeEvent(@event, new StructCompositeEventStatusAccessor(trackingPropertyExpression.GetPropertyInfo()), options, events); + + protected internal virtual Event CompositeEvent(Event @event, + Expression> trackingPropertyExpression, + params Event[] events) + => CompositeEvent(@event, new IntCompositeEventStatusAccessor(trackingPropertyExpression.GetPropertyInfo()), CompositeEventOptions.None, events); + + protected internal virtual Event CompositeEvent(Event @event, + Expression> trackingPropertyExpression, + CompositeEventOptions options, + params Event[] events) + => CompositeEvent(@event, new IntCompositeEventStatusAccessor(trackingPropertyExpression.GetPropertyInfo()), options, events); + + Event CompositeEvent(Event @event, CompositeEventStatusAccessor accessor, + CompositeEventOptions options, Event[] events) + { + if (events == null) + throw new ArgumentNullException(nameof(events)); + if (events.Length > 31) + throw new ArgumentException("No more than 31 events can be combined into a single event"); + if (events.Length == 0) + throw new ArgumentException("At least one event must be specified for a composite event"); + if (events.Any(x => x == null)) + throw new ArgumentException("One or more events specified has not yet been initialized"); + + var complete = new CompositeEventStatus(Enumerable.Range(0, events.Length) + .Aggregate(0, (current, x) => current | (1 << x))); + + for (int i = 0; i < events.Length; i++) + { + int flag = 1 << i; + + var activity = new CompositeEventActivity(accessor, flag, complete, @event); + + bool Filter(State x) => options.HasFlag(CompositeEventOptions.IncludeInitial) || !Equals(x, Initial); + + foreach (var state in _stateCache.Values.Where(Filter)) + { + During(state, + When(events[i]) + .Execute(activity)); + } + } + + return @event; + } + /// /// Declares a state on the state machine, and initialized the property /// /// The state property - protected virtual void State(Expression> propertyExpression) + protected internal virtual void State(Expression> propertyExpression) { PropertyInfo property = propertyExpression.GetPropertyInfo(); DeclareState(property); } + protected internal virtual State State(string name) + { + if (TryGetState(name, out var foundState)) + return foundState; + + var state = new StateMachineState(this, name, _eventObservers); + SetState(name, state); + + return state; + } + void DeclareState(PropertyInfo property) { string name = property.Name; @@ -465,7 +593,7 @@ void DeclareState(PropertyInfo property) /// /// The property containing the state /// The state property - protected virtual void State(Expression> propertyExpression, + protected internal virtual void State(Expression> propertyExpression, Expression> statePropertyExpression) where TProperty : class { @@ -508,7 +636,7 @@ static StateMachineState GetStateProperty(PropertyInfo sta /// /// The state property expression /// The superstate of which this state is a substate - protected virtual void SubState(Expression> propertyExpression, State superState) + protected internal virtual void SubState(Expression> propertyExpression, State superState) { if (superState == null) throw new ArgumentNullException(nameof(superState)); @@ -533,13 +661,32 @@ protected virtual void SubState(Expression> propertyExpression, Stat SetState(name, state); } + protected internal virtual State SubState(string name, State superState) + { + if (superState == null) + throw new ArgumentNullException(nameof(superState)); + + State superStateInstance = GetState(superState.Name); + + // If the state was already defined, don't define it again + if (TryGetState(name, out var existingState) && + name.Equals(existingState?.Name) && + superState.Name.Equals(existingState?.SuperState?.Name)) + return existingState; + + var state = new StateMachineState(this, name, _eventObservers, superStateInstance); + + SetState(name, state); + return state; + } + /// /// Declares a state on the state machine, and initialized the property /// /// The property containing the state /// The state property /// The superstate of which this state is a substate - protected virtual void SubState(Expression> propertyExpression, + protected internal virtual void SubState(Expression> propertyExpression, Expression> statePropertyExpression, State superState) where TProperty : class { @@ -588,7 +735,7 @@ void SetState(string name, StateMachineState state) /// /// The state /// The event and activities - protected void During(State state, params EventActivities[] activities) + protected internal void During(State state, params EventActivities[] activities) { ActivityBinder[] activitiesBinder = activities.SelectMany(x => x.GetStateActivityBinders()).ToArray(); @@ -601,7 +748,7 @@ protected void During(State state, params EventActivities[] activitie /// The state /// The other state /// The event and activities - protected void During(State state1, State state2, params EventActivities[] activities) + protected internal void During(State state1, State state2, params EventActivities[] activities) { ActivityBinder[] activitiesBinder = activities.SelectMany(x => x.GetStateActivityBinders()).ToArray(); @@ -616,7 +763,7 @@ protected void During(State state1, State state2, params EventActivitiesThe other state /// The other other state /// The event and activities - protected void During(State state1, State state2, State state3, params EventActivities[] activities) + protected internal void During(State state1, State state2, State state3, params EventActivities[] activities) { ActivityBinder[] activitiesBinder = activities.SelectMany(x => x.GetStateActivityBinders()).ToArray(); @@ -633,7 +780,7 @@ protected void During(State state1, State state2, State state3, params EventActi /// The other other state /// Okay, this is getting a bit ridiculous at this point /// The event and activities - protected void During(State state1, State state2, State state3, State state4, + protected internal void During(State state1, State state2, State state3, State state4, params EventActivities[] activities) { ActivityBinder[] activitiesBinder = activities.SelectMany(x => x.GetStateActivityBinders()).ToArray(); @@ -649,7 +796,7 @@ protected void During(State state1, State state2, State state3, State state4, /// /// The states /// The event and activities - protected void During(IEnumerable states, params EventActivities[] activities) + protected internal void During(IEnumerable states, params EventActivities[] activities) { ActivityBinder[] activitiesBinder = activities.SelectMany(x => x.GetStateActivityBinders()).ToArray(); @@ -669,7 +816,7 @@ void BindActivitiesToState(State state, IEnumerable> e /// Declares the events and activities that are handled during the initial state /// /// The event and activities - protected void Initially(params EventActivities[] activities) + protected internal void Initially(params EventActivities[] activities) { During(Initial, activities); } @@ -678,7 +825,7 @@ protected void Initially(params EventActivities[] activities) /// Declares events and activities that are handled during any state exception Initial and Final /// /// The event and activities - protected void DuringAny(params EventActivities[] activities) + protected internal void DuringAny(params EventActivities[] activities) { IEnumerable> states = _stateCache.Values.Where(x => !Equals(x, Initial) && !Equals(x, Final)); @@ -695,7 +842,7 @@ protected void DuringAny(params EventActivities[] activities) /// When the Final state is entered, execute the chained activities. This occurs in any state that is not the initial or final state /// /// Specify the activities that are executes when the Final state is entered. - protected void Finally(Func, EventActivityBinder> activityCallback) + protected internal void Finally(Func, EventActivityBinder> activityCallback) { EventActivityBinder binder = When(Final.Enter); @@ -718,7 +865,7 @@ void BindTransitionEvents(State state, IEnumerable /// The fired event /// - protected EventActivityBinder When(Event @event) + protected internal EventActivityBinder When(Event @event) { return new TriggerEventActivityBinder(this, @event); } @@ -729,7 +876,7 @@ protected EventActivityBinder When(Event @event) /// The fired event /// The filter applied to the event /// - protected EventActivityBinder When(Event @event, StateMachineEventFilter filter) + protected internal EventActivityBinder When(Event @event, StateMachineEventFilter filter) { return new TriggerEventActivityBinder(this, @event, filter); } @@ -741,7 +888,7 @@ protected EventActivityBinder When(Event @event, StateMachineEventFil /// /// /// - protected void WhenEnter(State state, Func, EventActivityBinder> activityCallback) + protected internal void WhenEnter(State state, Func, EventActivityBinder> activityCallback) { State activityState = GetState(state.Name); @@ -757,7 +904,7 @@ protected void WhenEnter(State state, Func, Event /// /// /// - protected void WhenEnterAny(Func, EventActivityBinder> activityCallback) + protected internal void WhenEnterAny(Func, EventActivityBinder> activityCallback) { BindEveryTransitionEvent(activityCallback, x => x.Enter); } @@ -767,7 +914,7 @@ protected void WhenEnterAny(Func, EventActivityBi /// /// /// - protected void WhenLeaveAny(Func, EventActivityBinder> activityCallback) + protected internal void WhenLeaveAny(Func, EventActivityBinder> activityCallback) { BindEveryTransitionEvent(activityCallback, x => x.Leave); } @@ -798,7 +945,7 @@ void BindEveryTransitionEvent(Func, EventActivity /// /// /// - protected void BeforeEnterAny(Func, EventActivityBinder> activityCallback) + protected internal void BeforeEnterAny(Func, EventActivityBinder> activityCallback) { BindEveryTransitionEvent(activityCallback, x => x.BeforeEnter); } @@ -808,7 +955,7 @@ protected void BeforeEnterAny(Func, EventA /// /// /// - protected void AfterLeaveAny(Func, EventActivityBinder> activityCallback) + protected internal void AfterLeaveAny(Func, EventActivityBinder> activityCallback) { BindEveryTransitionEvent(activityCallback, x => x.AfterLeave); } @@ -840,7 +987,7 @@ void BindEveryTransitionEvent(Func, EventA /// /// /// - protected void WhenLeave(State state, Func, EventActivityBinder> activityCallback) + protected internal void WhenLeave(State state, Func, EventActivityBinder> activityCallback) { State activityState = GetState(state.Name); @@ -857,7 +1004,7 @@ protected void WhenLeave(State state, Func, Event /// /// /// - protected void BeforeEnter(State state, + protected internal void BeforeEnter(State state, Func, EventActivityBinder> activityCallback) { State activityState = GetState(state.Name); @@ -875,7 +1022,7 @@ protected void BeforeEnter(State state, /// /// /// - protected void AfterLeave(State state, + protected internal void AfterLeave(State state, Func, EventActivityBinder> activityCallback) { State activityState = GetState(state.Name); @@ -893,7 +1040,7 @@ protected void AfterLeave(State state, /// The event data type /// The fired event /// - protected EventActivityBinder When(Event @event) + protected internal EventActivityBinder When(Event @event) { return new DataEventActivityBinder(this, @event); } @@ -905,7 +1052,7 @@ protected EventActivityBinder When(Event @event) /// The fired event /// The filter applied to the event /// - protected EventActivityBinder When(Event @event, + protected internal EventActivityBinder When(Event @event, StateMachineEventFilter filter) { return new DataEventActivityBinder(this, @event, filter); @@ -916,7 +1063,7 @@ protected EventActivityBinder When(Event @event, /// /// The ignored event /// - protected EventActivities Ignore(Event @event) + protected internal EventActivities Ignore(Event @event) { ActivityBinder activityBinder = new IgnoreEventActivityBinder(@event); @@ -929,7 +1076,7 @@ protected EventActivities Ignore(Event @event) /// The event data type /// The ignored event /// - protected EventActivities Ignore(Event @event) + protected internal EventActivities Ignore(Event @event) { ActivityBinder activityBinder = new IgnoreEventActivityBinder(@event); @@ -943,7 +1090,7 @@ protected EventActivities Ignore(Event @event) /// The ignored event /// The filter to apply to the event data /// - protected EventActivities Ignore(Event @event, + protected internal EventActivities Ignore(Event @event, StateMachineEventFilter filter) { ActivityBinder activityBinder = new IgnoreEventActivityBinder(@event, filter); @@ -955,7 +1102,7 @@ protected EventActivities Ignore(Event @event, /// Specifies a callback to invoke when an event is raised in a state where the event is not handled /// /// The unhandled event callback - protected void OnUnhandledEvent(UnhandledEventCallback callback) + protected internal void OnUnhandledEvent(UnhandledEventCallback callback) { if (callback == null) throw new ArgumentNullException(nameof(callback)); @@ -979,7 +1126,6 @@ void RegisterImplicit() declaration.Declare(this); } - protected static class ConfigurationHelpers { public static StateMachineRegistration[] GetRegistrations(AutomatonymousStateMachine stateMachine) @@ -1158,7 +1304,7 @@ public void Declare(object stateMachine) if (existing != null) return; - machine.DeclareTriggerEvent(_propertyInfo); + machine.DeclarePropertyBasedEvent((prop) => machine.DeclareTriggerEvent(prop.Name), _propertyInfo); } } @@ -1181,9 +1327,27 @@ public void Declare(object stateMachine) if (existing != null) return; - machine.DeclareDataEvent(_propertyInfo); + machine.DeclarePropertyBasedEvent((prop) => machine.DeclareDataEvent(prop.Name), _propertyInfo); } } } + + private StateMachine Modify(Action> modifier) + { + StateMachineModifier builder = new InternalStateMachineModifier(this); + modifier(builder); + builder.Apply(); + + return this; + } + + private class BuilderStateMachine : AutomatonymousStateMachine { } + + public static AutomatonymousStateMachine Create(Action> modifier) + { + var machine = new BuilderStateMachine(); + machine.Modify(modifier); + return machine; + } } } diff --git a/src/Automatonymous/Builders/InternalStateMachineEventActivitiesBuilder.cs b/src/Automatonymous/Builders/InternalStateMachineEventActivitiesBuilder.cs new file mode 100644 index 0000000..50095fa --- /dev/null +++ b/src/Automatonymous/Builders/InternalStateMachineEventActivitiesBuilder.cs @@ -0,0 +1,171 @@ +using Automatonymous.Binders; +using System; +using System.Collections.Generic; +using System.Linq.Expressions; + +namespace Automatonymous.Builder +{ + internal class InternalStateMachineEventActivitiesBuilder : StateMachineEventActivitiesBuilder + where TInstance : class + { + readonly AutomatonymousStateMachine _machine; + readonly StateMachineModifier _modifier; + readonly Action[]> _committer; + readonly List> _activities; + + public InternalStateMachineEventActivitiesBuilder(AutomatonymousStateMachine machine, StateMachineModifier modifier, Action[]> committer) + { + _machine = machine ?? throw new ArgumentNullException(nameof(machine)); + _modifier = modifier ?? throw new ArgumentNullException(nameof(modifier)); + _committer = committer ?? throw new ArgumentNullException(nameof(committer)); + _activities = new List>(); + IsCommitted = false; + } + + public bool IsCommitted { get; private set; } + + public State Initial => _modifier.Initial; + public State Final => _modifier.Final; + + public StateMachineModifier CommitActivities() + { + _committer(_activities.ToArray()); + IsCommitted = true; + return _modifier; + } + + #region Pass-through Modifier + public StateMachineModifier AfterLeave(State state, Func, EventActivityBinder> activityCallback) + => CommitActivities().AfterLeave(state, activityCallback); + + public StateMachineModifier AfterLeaveAny(Func, EventActivityBinder> activityCallback) + => CommitActivities().AfterLeaveAny(activityCallback); + + public StateMachineModifier BeforeEnter(State state, Func, EventActivityBinder> activityCallback) + => CommitActivities().BeforeEnter(state, activityCallback); + + public StateMachineModifier BeforeEnterAny(Func, EventActivityBinder> activityCallback) + => CommitActivities().BeforeEnterAny(activityCallback); + + public StateMachineModifier CompositeEvent(Event @event, Expression> trackingPropertyExpression, params Event[] events) + => CommitActivities().CompositeEvent(@event, trackingPropertyExpression, events); + + public StateMachineModifier CompositeEvent(Event @event, Expression> trackingPropertyExpression, CompositeEventOptions options, params Event[] events) + => CommitActivities().CompositeEvent(@event, trackingPropertyExpression, options, events); + + public StateMachineModifier CompositeEvent(Event @event, Expression> trackingPropertyExpression, params Event[] events) + => CommitActivities().CompositeEvent(@event, trackingPropertyExpression, events); + + public StateMachineModifier CompositeEvent(Event @event, Expression> trackingPropertyExpression, CompositeEventOptions options, params Event[] events) + => CommitActivities().CompositeEvent(@event, trackingPropertyExpression, options, events); + + public StateMachineEventActivitiesBuilder During(params State[] states) + => CommitActivities().During(states); + + public StateMachineEventActivitiesBuilder DuringAny() + => CommitActivities().DuringAny(); + + public StateMachineModifier Event(string name, out Event @event) + => CommitActivities().Event(name, out @event); + + public StateMachineModifier Event(string name, out Event @event) + => CommitActivities().Event(name, out @event); + + public StateMachineModifier Event(Expression> propertyExpression, Expression>> eventPropertyExpression) where TProperty : class + => CommitActivities().Event(propertyExpression, eventPropertyExpression); + + public StateMachineModifier Finally(Func, EventActivityBinder> activityCallback) + => CommitActivities().Finally(activityCallback); + + public StateMachineEventActivitiesBuilder Initially() + => CommitActivities().Initially(); + + public StateMachineModifier InstanceState(Expression> instanceStateProperty) + => CommitActivities().InstanceState(instanceStateProperty); + + public StateMachineModifier InstanceState(Expression> instanceStateProperty) + => CommitActivities().InstanceState(instanceStateProperty); + + public StateMachineModifier InstanceState(Expression> instanceStateProperty, params State[] states) + => CommitActivities().InstanceState(instanceStateProperty, states); + + public StateMachineModifier Name(string machineName) + => CommitActivities().Name(machineName); + + public StateMachineModifier OnUnhandledEvent(UnhandledEventCallback callback) + => CommitActivities().OnUnhandledEvent(callback); + + public StateMachineModifier State(string name, out State state) + => CommitActivities().State(name, out state); + + public StateMachineModifier State(string name, out State state) + => CommitActivities().State(name, out state); + + public StateMachineModifier State(Expression> propertyExpression, Expression> statePropertyExpression) where TProperty : class + => CommitActivities().State(propertyExpression, statePropertyExpression); + + public StateMachineModifier SubState(string name, State superState, out State subState) + => CommitActivities().SubState(name, superState, out subState); + + public StateMachineModifier SubState(Expression> propertyExpression, Expression> statePropertyExpression, State superState) where TProperty : class + => CommitActivities().SubState(propertyExpression, statePropertyExpression, superState); + + public StateMachineModifier WhenEnter(State state, Func, EventActivityBinder> activityCallback) + => CommitActivities().WhenEnter(state, activityCallback); + + public StateMachineModifier WhenEnterAny(Func, EventActivityBinder> activityCallback) + => CommitActivities().WhenEnterAny(activityCallback); + + public StateMachineModifier WhenLeave(State state, Func, EventActivityBinder> activityCallback) + => CommitActivities().WhenLeave(state, activityCallback); + + public StateMachineModifier WhenLeaveAny(Func, EventActivityBinder> activityCallback) + => CommitActivities().WhenLeaveAny(activityCallback); + #endregion + + public StateMachineEventActivitiesBuilder When(Event @event, Func, EventActivityBinder> configure) + { + _activities.Add(configure(_machine.When(@event))); + return this; + } + + public StateMachineEventActivitiesBuilder When(Event @event, StateMachineEventFilter filter, Func, EventActivityBinder> configure) + { + _activities.Add(configure(_machine.When(@event, filter))); + return this; + } + + public StateMachineEventActivitiesBuilder When(Event @event, Func, EventActivityBinder> configure) + { + _activities.Add(configure(_machine.When(@event))); + return this; + } + + public StateMachineEventActivitiesBuilder When(Event @event, StateMachineEventFilter filter, Func, EventActivityBinder> configure) + { + _activities.Add(configure(_machine.When(@event, filter))); + return this; + } + + public StateMachineEventActivitiesBuilder Ignore(Event @event) + { + _activities.Add(_machine.Ignore(@event)); + return this; + } + + public StateMachineEventActivitiesBuilder Ignore(Event @event) + { + _activities.Add(_machine.Ignore(@event)); + return this; + } + + public StateMachineEventActivitiesBuilder Ignore(Event @event, StateMachineEventFilter filter) + { + _activities.Add(_machine.Ignore(@event, filter)); + return this; + } + + public void Apply() + => CommitActivities().Apply(); + } +} diff --git a/src/Automatonymous/Builders/InternalStateMachineModifier.cs b/src/Automatonymous/Builders/InternalStateMachineModifier.cs new file mode 100644 index 0000000..b6ecf84 --- /dev/null +++ b/src/Automatonymous/Builders/InternalStateMachineModifier.cs @@ -0,0 +1,224 @@ +using Automatonymous.Binders; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; + +namespace Automatonymous.Builder +{ + internal class InternalStateMachineModifier : StateMachineModifier + where TInstance : class + { + readonly AutomatonymousStateMachine _machine; + readonly List> _activityBuilders; + + public State Initial => _machine.Initial; + public State Final => _machine.Final; + + public InternalStateMachineModifier(AutomatonymousStateMachine machine) + { + _machine = machine ?? throw new ArgumentNullException(nameof(machine)); + _activityBuilders = new List>(); + } + + public void Apply() + { + var uncommittedActivities = _activityBuilders + .Where(builder => !builder.IsCommitted) + .ToArray(); + + foreach (var builder in uncommittedActivities) + builder.CommitActivities(); + } + + public StateMachineEventActivitiesBuilder During(params State[] states) + { + var builder = new InternalStateMachineEventActivitiesBuilder(_machine, this, (activities) => _machine.During(states, activities)); + _activityBuilders.Add(builder); + return builder; + } + + public StateMachineEventActivitiesBuilder DuringAny() + { + var builder = new InternalStateMachineEventActivitiesBuilder(_machine, this, (activities) => _machine.DuringAny(activities)); + _activityBuilders.Add(builder); + return builder; + } + + public StateMachineEventActivitiesBuilder Initially() + { + var builder = new InternalStateMachineEventActivitiesBuilder(_machine, this, (activities) => _machine.Initially(activities)); + _activityBuilders.Add(builder); + return builder; + } + + public StateMachineModifier AfterLeave(State state, Func, EventActivityBinder> activityCallback) + { + _machine.AfterLeave(state, activityCallback); + return this; + } + + public StateMachineModifier AfterLeaveAny(Func, EventActivityBinder> activityCallback) + { + _machine.AfterLeaveAny(activityCallback); + return this; + } + + public StateMachineModifier BeforeEnter(State state, Func, EventActivityBinder> activityCallback) + { + _machine.BeforeEnter(state, activityCallback); + return this; + } + + public StateMachineModifier BeforeEnterAny(Func, EventActivityBinder> activityCallback) + { + _machine.BeforeEnterAny(activityCallback); + return this; + } + + public StateMachineModifier CompositeEvent(Event @event, Expression> trackingPropertyExpression, params Event[] events) + { + _machine.CompositeEvent(@event, trackingPropertyExpression, events); + return this; + } + + public StateMachineModifier CompositeEvent(Event @event, Expression> trackingPropertyExpression, CompositeEventOptions options, params Event[] events) + { + _machine.CompositeEvent(@event, trackingPropertyExpression, options, events); + return this; + } + + public StateMachineModifier CompositeEvent(Event @event, Expression> trackingPropertyExpression, params Event[] events) + { + _machine.CompositeEvent(@event, trackingPropertyExpression, events); + return this; + } + + public StateMachineModifier CompositeEvent(Event @event, Expression> trackingPropertyExpression, CompositeEventOptions options, params Event[] events) + { + _machine.CompositeEvent(@event, trackingPropertyExpression, options, events); + return this; + } + + public StateMachineModifier Event(string name, out Event @event) + { + @event = _machine.Event(name); + return this; + } + + public StateMachineModifier Event(string name, out Event @event) + { + @event = _machine.Event(name); + return this; + } + + public StateMachineModifier Event(Expression> propertyExpression, Expression>> eventPropertyExpression) where TProperty : class + { + _machine.Event(propertyExpression, eventPropertyExpression); + return this; + } + + public StateMachineModifier Finally(Func, EventActivityBinder> activityCallback) + { + _machine.Finally(activityCallback); + return this; + } + + public StateMachineModifier InstanceState(Expression> instanceStateProperty) + { + _machine.InstanceState(instanceStateProperty); + return this; + } + + public StateMachineModifier InstanceState(Expression> instanceStateProperty) + { + _machine.InstanceState(instanceStateProperty); + return this; + } + + public StateMachineModifier InstanceState(Expression> instanceStateProperty, params State[] states) + { + _machine.InstanceState(instanceStateProperty, states); + return this; + } + + public StateMachineModifier InstanceState(Expression> instanceStateProperty, params string[] stateNames) + { + // NOTE: May need to re-think this; Assumes the states have already been declared. + State[] states = stateNames + .Select(name => _machine.GetState(name)) + .ToArray(); + + _machine.InstanceState(instanceStateProperty, states); + return this; + } + + public StateMachineModifier Name(string machineName) + { + _machine.Name(machineName); + return this; + } + + public StateMachineModifier OnUnhandledEvent(UnhandledEventCallback callback) + { + _machine.OnUnhandledEvent(callback); + return this; + } + + public StateMachineModifier State(string name, out State state) + { + state = _machine.State(name); + return this; + } + + public StateMachineModifier State(string name, out State state) + { + state = _machine.State(name); + return this; + } + + public StateMachineModifier State(Expression> propertyExpression, Expression> statePropertyExpression) where TProperty : class + { + _machine.State(propertyExpression, statePropertyExpression); + return this; + } + + public StateMachineModifier SubState(string name, State superState, out State subState) + { + subState = _machine.SubState(name, superState); + return this; + } + + public StateMachineModifier SubState(Expression> propertyExpression, Expression> statePropertyExpression, State superState) where TProperty : class + { + _machine.SubState(propertyExpression, statePropertyExpression, superState); + return this; + } + + public StateMachineModifier WhenEnter(State state, Func, EventActivityBinder> activityCallback) + { + _machine.WhenEnter(state, activityCallback); + return this; + } + + public StateMachineModifier WhenEnterAny(Func, EventActivityBinder> activityCallback) + { + _machine.WhenEnterAny(activityCallback); + return this; + } + + public StateMachineModifier WhenLeave(State state, Func, EventActivityBinder> activityCallback) + { + _machine.WhenLeave(state, activityCallback); + return this; + } + + public StateMachineModifier WhenLeaveAny(Func, EventActivityBinder> activityCallback) + { + _machine.WhenLeaveAny(activityCallback); + return this; + } + + + } +} diff --git a/src/Automatonymous/Builders/StateMachineEventActivitiesBuilder.cs b/src/Automatonymous/Builders/StateMachineEventActivitiesBuilder.cs new file mode 100644 index 0000000..53fe4e2 --- /dev/null +++ b/src/Automatonymous/Builders/StateMachineEventActivitiesBuilder.cs @@ -0,0 +1,23 @@ +using Automatonymous.Binders; +using System; + +namespace Automatonymous.Builder +{ + public interface StateMachineEventActivitiesBuilder : + StateMachineModifier + where TInstance : class + { + StateMachineEventActivitiesBuilder When(Event @event, Func, EventActivityBinder> configure); + StateMachineEventActivitiesBuilder When(Event @event, StateMachineEventFilter filter, Func, EventActivityBinder> configure); + StateMachineEventActivitiesBuilder When(Event @event, Func, EventActivityBinder> configure); + StateMachineEventActivitiesBuilder When(Event @event, + StateMachineEventFilter filter, Func, EventActivityBinder> configure); + StateMachineEventActivitiesBuilder Ignore(Event @event); + StateMachineEventActivitiesBuilder Ignore(Event @event); + StateMachineEventActivitiesBuilder Ignore(Event @event, + StateMachineEventFilter filter); + + bool IsCommitted { get; } + StateMachineModifier CommitActivities(); + } +} diff --git a/src/Automatonymous/Builders/StateMachineModifier.cs b/src/Automatonymous/Builders/StateMachineModifier.cs new file mode 100644 index 0000000..19ea149 --- /dev/null +++ b/src/Automatonymous/Builders/StateMachineModifier.cs @@ -0,0 +1,67 @@ +using Automatonymous.Binders; +using System; +using System.Linq.Expressions; + +namespace Automatonymous.Builder +{ + public interface StateMachineModifier + where TInstance : class + { + State Initial { get; } + State Final { get; } + + StateMachineModifier InstanceState(Expression> instanceStateProperty); + StateMachineModifier InstanceState(Expression> instanceStateProperty); + StateMachineModifier InstanceState(Expression> instanceStateProperty, params State[] states); + StateMachineModifier Name(string machineName); + StateMachineModifier Event(string name, out Event @event); + StateMachineModifier Event(string name, out Event @event); + + StateMachineModifier Event(Expression> propertyExpression, + Expression>> eventPropertyExpression) + where TProperty : class; + StateMachineModifier CompositeEvent(Event @event, + Expression> trackingPropertyExpression, + params Event[] events); + StateMachineModifier CompositeEvent(Event @event, + Expression> trackingPropertyExpression, + CompositeEventOptions options, + params Event[] events); + StateMachineModifier CompositeEvent(Event @event, + Expression> trackingPropertyExpression, + params Event[] events); + StateMachineModifier CompositeEvent(Event @event, + Expression> trackingPropertyExpression, + CompositeEventOptions options, + params Event[] events); + StateMachineModifier State(string name, out State state); + StateMachineModifier State(string name, out State state); + StateMachineModifier State(Expression> propertyExpression, + Expression> statePropertyExpression) + where TProperty : class; + + StateMachineModifier SubState(string name, State superState, out State subState); + StateMachineModifier SubState(Expression> propertyExpression, + Expression> statePropertyExpression, State superState) + where TProperty : class; + + StateMachineEventActivitiesBuilder During(params State[] states); + StateMachineEventActivitiesBuilder Initially(); + StateMachineEventActivitiesBuilder DuringAny(); + StateMachineModifier Finally(Func, EventActivityBinder> activityCallback); + + StateMachineModifier WhenEnter(State state, Func, EventActivityBinder> activityCallback); + StateMachineModifier WhenEnterAny(Func, EventActivityBinder> activityCallback); + StateMachineModifier WhenLeaveAny(Func, EventActivityBinder> activityCallback); + StateMachineModifier BeforeEnterAny(Func, EventActivityBinder> activityCallback); + StateMachineModifier AfterLeaveAny(Func, EventActivityBinder> activityCallback); + StateMachineModifier WhenLeave(State state, Func, EventActivityBinder> activityCallback); + StateMachineModifier BeforeEnter(State state, + Func, EventActivityBinder> activityCallback); + StateMachineModifier AfterLeave(State state, + Func, EventActivityBinder> activityCallback); + StateMachineModifier OnUnhandledEvent(UnhandledEventCallback callback); + + void Apply(); + } +} diff --git a/src/Automatonymous/States/StateMachineState.cs b/src/Automatonymous/States/StateMachineState.cs index 737ef86..8869e84 100644 --- a/src/Automatonymous/States/StateMachineState.cs +++ b/src/Automatonymous/States/StateMachineState.cs @@ -21,7 +21,7 @@ public class StateMachineState : readonly string _name; readonly EventObserver _observer; readonly HashSet> _subStates; - readonly State _superState; + State _superState; public StateMachineState(AutomatonymousStateMachine machine, string name, EventObserver observer, State superState = null) @@ -44,8 +44,8 @@ public StateMachineState(AutomatonymousStateMachine machine, string n Ignore(AfterLeave); _subStates = new HashSet>(); - _superState = superState; + _superState = superState; superState?.AddSubstate(this); } @@ -54,7 +54,7 @@ public bool Equals(State other) return string.CompareOrdinal(_name, other?.Name ?? "") == 0; } - public State SuperState => _superState; + public State SuperState { get; } public string Name => _name; public Event Enter { get; } public Event Leave { get; }