diff --git a/src/Stateless/Graph/GraphStyleBase.cs b/src/Stateless/Graph/GraphStyleBase.cs index 0b79c5df..881a5d36 100644 --- a/src/Stateless/Graph/GraphStyleBase.cs +++ b/src/Stateless/Graph/GraphStyleBase.cs @@ -64,8 +64,11 @@ public abstract class GraphStyleBase /// Description of all transitions, in the desired format. public virtual List FormatAllTransitions(List transitions) { - List lines = new List(); - if (transitions == null) return lines; + if (transitions == null) + return new List(); + + // Eagerly set the initial capacity to minimize re-allocation of internal array. + List lines = new List(transitions.Count); foreach (var transit in transitions) { @@ -84,26 +87,23 @@ public virtual List FormatAllTransitions(List transitions) stay.SourceState.NodeName, stay.Guards.Select(x => x.Description)); } } - else + else if (transit is FixedTransition fix) { - if (transit is FixedTransition fix) - { - line = FormatOneTransition(fix.SourceState.NodeName, fix.Trigger.UnderlyingTrigger.ToString(), + line = FormatOneTransition(fix.SourceState.NodeName, fix.Trigger.UnderlyingTrigger.ToString(), fix.DestinationEntryActions.Select(x => x.Method.Description), fix.DestinationState.NodeName, fix.Guards.Select(x => x.Description)); - } - else - { - if (transit is DynamicTransition dyn) - { - line = FormatOneTransition(dyn.SourceState.NodeName, dyn.Trigger.UnderlyingTrigger.ToString(), + } + else if (transit is DynamicTransition dyn) + { + line = FormatOneTransition(dyn.SourceState.NodeName, dyn.Trigger.UnderlyingTrigger.ToString(), dyn.DestinationEntryActions.Select(x => x.Method.Description), dyn.DestinationState.NodeName, new List { dyn.Criterion }); - } - else - throw new ArgumentException("Unexpected transition type"); - } } + else + { + throw new ArgumentException("Unexpected transition type"); + } + if (line != null) lines.Add(line); } diff --git a/src/Stateless/ParameterConversion.cs b/src/Stateless/ParameterConversion.cs index ff28c4f7..631390db 100644 --- a/src/Stateless/ParameterConversion.cs +++ b/src/Stateless/ParameterConversion.cs @@ -11,7 +11,7 @@ public static object Unpack(object[] args, Type argType, int index) if (args.Length == 0) return null; - if (args.Length <= index) + if (args.Length <= index || index < 0) throw new ArgumentException( string.Format(ParameterConversionResources.ArgOfTypeRequiredInPosition, argType, index)); diff --git a/src/Stateless/Reflection/DynamicTransitionInfo.cs b/src/Stateless/Reflection/DynamicTransitionInfo.cs index a8179256..ca12090a 100644 --- a/src/Stateless/Reflection/DynamicTransitionInfo.cs +++ b/src/Stateless/Reflection/DynamicTransitionInfo.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace Stateless.Reflection { @@ -19,12 +20,12 @@ public DynamicStateInfo(string destinationState, string criterion) /// /// The name of the destination state /// - public string DestinationState { get; set; } + public string DestinationState { get; } /// /// The reason this destination state was chosen /// - public string Criterion { get; set; } + public string Criterion { get; } } /// @@ -80,15 +81,13 @@ public class DynamicTransitionInfo : TransitionInfo public static DynamicTransitionInfo Create(TTrigger trigger, IEnumerable guards, InvocationInfo selector, DynamicStateInfos possibleStates) { - var transition = new DynamicTransitionInfo + return new DynamicTransitionInfo { Trigger = new TriggerInfo(trigger), - GuardConditionsMethodDescriptions = guards ?? new List(), + GuardConditionsMethodDescriptions = guards ?? Array.Empty(), DestinationStateSelectorDescription = selector, PossibleDestinationStates = possibleStates // behaviour.PossibleDestinationStates?.Select(x => x.ToString()).ToArray() }; - - return transition; } private DynamicTransitionInfo() { } diff --git a/src/Stateless/Reflection/FixedTransitionInfo.cs b/src/Stateless/Reflection/FixedTransitionInfo.cs index 60275476..acd46896 100644 --- a/src/Stateless/Reflection/FixedTransitionInfo.cs +++ b/src/Stateless/Reflection/FixedTransitionInfo.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System; using System.Linq; namespace Stateless.Reflection @@ -10,16 +10,14 @@ public class FixedTransitionInfo : TransitionInfo { internal static FixedTransitionInfo Create(StateMachine.TriggerBehaviour behaviour, StateInfo destinationStateInfo) { - var transition = new FixedTransitionInfo + return new FixedTransitionInfo { Trigger = new TriggerInfo(behaviour.Trigger), DestinationState = destinationStateInfo, GuardConditionsMethodDescriptions = behaviour.Guard == null - ? new List() : behaviour.Guard.Conditions.Select(c => c.MethodDescription), + ? Array.Empty() : behaviour.Guard.Conditions.Select(c => c.MethodDescription), IsInternalTransition = behaviour is StateMachine.InternalTriggerBehaviour }; - - return transition; } private FixedTransitionInfo() { } diff --git a/src/Stateless/Reflection/IgnoredTransitionInfo.cs b/src/Stateless/Reflection/IgnoredTransitionInfo.cs index 6e669950..47607881 100644 --- a/src/Stateless/Reflection/IgnoredTransitionInfo.cs +++ b/src/Stateless/Reflection/IgnoredTransitionInfo.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System; using System.Linq; namespace Stateless.Reflection @@ -14,7 +14,7 @@ internal static IgnoredTransitionInfo Create(StateMachine() : behaviour.Guard.Conditions.Select(c => c.MethodDescription) + ? Array.Empty() : behaviour.Guard.Conditions.Select(c => c.MethodDescription) }; return transition; diff --git a/src/Stateless/Reflection/InvocationInfo.cs b/src/Stateless/Reflection/InvocationInfo.cs index 736425b6..3dfb62d9 100644 --- a/src/Stateless/Reflection/InvocationInfo.cs +++ b/src/Stateless/Reflection/InvocationInfo.cs @@ -7,6 +7,8 @@ namespace Stateless.Reflection /// public class InvocationInfo { + private static readonly char[] methodNameChars = { '<', '>', '`' }; + readonly string _description; // _description can be null if user didn't specify a description /// @@ -65,7 +67,7 @@ public string Description return _description; if (MethodName == null) return SpecialConstants.NullString; - if (MethodName.IndexOfAny(new char[] { '<', '>', '`' }) >= 0) + if (MethodName.IndexOfAny(methodNameChars) >= 0) return DefaultFunctionDescription; return MethodName; } diff --git a/test/Stateless.Tests/ParameterConversionTests.cs b/test/Stateless.Tests/ParameterConversionTests.cs new file mode 100644 index 00000000..314f726d --- /dev/null +++ b/test/Stateless.Tests/ParameterConversionTests.cs @@ -0,0 +1,76 @@ +using System; +using Xunit; + +namespace Stateless.Tests +{ + public class ParameterConversionTests + { + private readonly object[] args = { 5, 10, 15 }; + + [Fact] + public void Unpack_ShouldReturnArg_WhenValidationSucceeds() + { + Assert.Equal(5, ParameterConversion.Unpack(args, typeof(int), 0)); + Assert.Equal(10, ParameterConversion.Unpack(args, typeof(int), 1)); + Assert.Equal(15, ParameterConversion.Unpack(args, typeof(int), 2)); + Assert.Null(ParameterConversion.Unpack(new object[] { "John", null, "Doe" }, typeof(string), 1)); + } + + [Fact] + public void Unpack_ShouldThrowArgumentNullException_WhenArgsIsNull() + { + Assert.Throws(() => ParameterConversion.Unpack(null, null, 0)); + } + + [Fact] + public void Unpack_ShouldReturnNull_WhenArgsLengthIsZero() + { + Assert.Null(ParameterConversion.Unpack(Array.Empty(), null, 0)); + } + + [Fact] + public void Unpack_ShouldThrowArgumentException_WhenArgsLengthLessThanIndex() + { + Assert.Throws(() => ParameterConversion.Unpack(args, typeof(int), 5)); + } + + [Fact] + public void Unpack_ShouldThrowArgumentException_WhenIndexLessThanZero() + { + Assert.Throws(() => ParameterConversion.Unpack(args, typeof(int), -1)); + } + + [Fact] + public void Unpack_ShouldThrowArgumentException_WhenTypeIsNotAssignable() + { + Assert.Throws(() => ParameterConversion.Unpack(args, typeof(char), 0)); + } + + [Fact] + public void Unpack_ShouldReturnDefault_When2ParameterMethodCalled() + { + Assert.Equal(0, ParameterConversion.Unpack(Array.Empty(), 0)); + Assert.Equal(char.MinValue, ParameterConversion.Unpack(Array.Empty(), 0)); + Assert.Equal(false, ParameterConversion.Unpack(Array.Empty(), 0)); + } + + [Fact] + public void Validate_ShouldThrowArgumentException_WhenArgsGreaterThanExpected() + { + Assert.Throws(() => ParameterConversion.Validate(args, new Type[] { null, null })); + } + + [Fact] + public void Validate_ShouldWorkWithoutException_WhenEverythingExpectedIsProvided() + { + try + { + ParameterConversion.Validate(args, new Type[] { typeof(int), typeof(int), typeof(int) }); + } + catch (Exception) + { + throw; + } + } + } +}