From 9be29d798b7d7a48c515ad51576a56d03eae733c Mon Sep 17 00:00:00 2001 From: Piotr Zajac Date: Tue, 17 Oct 2023 19:11:00 +0200 Subject: [PATCH] Initial work on FromRange attribute --- .../Attributes/FromRangeAttributeTests.cs | 32 ++++++++++++ .../Attributes/BuildWithAttribute.cs | 25 +++++++++ .../Attributes/BuildWithAttribute[T].cs | 15 ++++++ .../Attributes/CustomizeAdapterAttribute.cs | 47 +++++++++++++++++ .../Attributes/CustomizeWithAttribute.cs | 33 ++---------- .../Attributes/FromRangeAttribute.cs | 52 +++++++++++++++++++ .../RandomRangedNumberBuilder.cs | 29 +++++++++++ 7 files changed, 203 insertions(+), 30 deletions(-) create mode 100644 src/Objectivity.AutoFixture.XUnit2.Core.Tests/Attributes/FromRangeAttributeTests.cs create mode 100644 src/Objectivity.AutoFixture.XUnit2.Core/Attributes/BuildWithAttribute.cs create mode 100644 src/Objectivity.AutoFixture.XUnit2.Core/Attributes/BuildWithAttribute[T].cs create mode 100644 src/Objectivity.AutoFixture.XUnit2.Core/Attributes/CustomizeAdapterAttribute.cs create mode 100644 src/Objectivity.AutoFixture.XUnit2.Core/Attributes/FromRangeAttribute.cs create mode 100644 src/Objectivity.AutoFixture.XUnit2.Core/SpeciminBuilders/RandomRangedNumberBuilder.cs diff --git a/src/Objectivity.AutoFixture.XUnit2.Core.Tests/Attributes/FromRangeAttributeTests.cs b/src/Objectivity.AutoFixture.XUnit2.Core.Tests/Attributes/FromRangeAttributeTests.cs new file mode 100644 index 00000000..fcd9fad8 --- /dev/null +++ b/src/Objectivity.AutoFixture.XUnit2.Core.Tests/Attributes/FromRangeAttributeTests.cs @@ -0,0 +1,32 @@ +namespace Objectivity.AutoFixture.XUnit2.Core.Tests.Attributes +{ + using FluentAssertions; + using global::AutoFixture.Xunit2; + + using Objectivity.AutoFixture.XUnit2.Core.Attributes; + + using Xunit; + + [Collection("FromRangeAttribute")] + [Trait("Category", "Attributes")] + public class FromRangeAttributeTests + { + [AutoData] + [Theory(DisplayName = "GIVEN renges specified WHEN data populated THEN only decorated parameters has values from ranges")] + public void GivenRengesSpecified_WhenDataPopulated_ThenOnlyDecoratedParametersHasValuesFromRanges( + [FromRange(-10, -1)] int value1, + [FromRange(-39.9, -30.1)] double value2, + [FromRange(-2.9, -2.1)] decimal value3, + int value4, + double value5, + decimal value6) + { + value1.Should().BeInRange(-10, -1); + value2.Should().BeInRange(-39.9, -30.1); + value3.Should().BeInRange(-2.9m, -2.1m); + value4.Should().BeGreaterThanOrEqualTo(0); + value5.Should().BeGreaterThanOrEqualTo(0); + value6.Should().BeGreaterThanOrEqualTo(0); + } + } +} diff --git a/src/Objectivity.AutoFixture.XUnit2.Core/Attributes/BuildWithAttribute.cs b/src/Objectivity.AutoFixture.XUnit2.Core/Attributes/BuildWithAttribute.cs new file mode 100644 index 00000000..a033f701 --- /dev/null +++ b/src/Objectivity.AutoFixture.XUnit2.Core/Attributes/BuildWithAttribute.cs @@ -0,0 +1,25 @@ +namespace Objectivity.AutoFixture.XUnit2.Core.Attributes +{ + using System; + using System.Reflection; + + using global::AutoFixture; + using global::AutoFixture.Kernel; + + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true)] + public abstract class BuildWithAttribute : CustomizeAdapterAttribute + { + protected BuildWithAttribute(Type type, params object[] args) + : base(type, args) + { + } + + public override ICustomization GetCustomization(ParameterInfo parameter) + { + var specimenBuilder = this.CreateInstance(parameter) as ISpecimenBuilder; + var builder = new FilteringSpecimenBuilder(specimenBuilder, new EqualRequestSpecification(parameter)); + + return builder.ToCustomization(); + } + } +} diff --git a/src/Objectivity.AutoFixture.XUnit2.Core/Attributes/BuildWithAttribute[T].cs b/src/Objectivity.AutoFixture.XUnit2.Core/Attributes/BuildWithAttribute[T].cs new file mode 100644 index 00000000..bcdb39e1 --- /dev/null +++ b/src/Objectivity.AutoFixture.XUnit2.Core/Attributes/BuildWithAttribute[T].cs @@ -0,0 +1,15 @@ +namespace Objectivity.AutoFixture.XUnit2.Core.Attributes +{ + using System.Diagnostics.CodeAnalysis; + using global::AutoFixture.Kernel; + + [SuppressMessage("Performance", "CA1813:Avoid unsealed attributes", Justification = "This attribute should be extendable by inheritance.")] + public class BuildWithAttribute : BuildWithAttribute + where T : ISpecimenBuilder + { + public BuildWithAttribute(params object[] args) + : base(typeof(T), args) + { + } + } +} diff --git a/src/Objectivity.AutoFixture.XUnit2.Core/Attributes/CustomizeAdapterAttribute.cs b/src/Objectivity.AutoFixture.XUnit2.Core/Attributes/CustomizeAdapterAttribute.cs new file mode 100644 index 00000000..781bf67d --- /dev/null +++ b/src/Objectivity.AutoFixture.XUnit2.Core/Attributes/CustomizeAdapterAttribute.cs @@ -0,0 +1,47 @@ +namespace Objectivity.AutoFixture.XUnit2.Core.Attributes +{ + using System; + using System.Linq; + using System.Reflection; + using global::AutoFixture.Xunit2; + + using Objectivity.AutoFixture.XUnit2.Core.Common; + + //// TODO: Consider composition over inheritance + public abstract class CustomizeAdapterAttribute : CustomizeAttribute + { + protected CustomizeAdapterAttribute(Type type, params object[] args) + { + this.Type = type.NotNull(nameof(type)); + + var builderType = typeof(T); + if (!builderType.IsAssignableFrom(type)) + { + throw new ArgumentException($"Specified argument {nameof(type)} must implement {builderType.Name}"); + } + + this.Args = args; + } + + public Type Type { get; } + + public object[] Args { get; } + + /// + /// Gets or sets a value indicating whether attribute target parameter type should included as a first argument when creating customization. + /// + /// Indicates whether attribute target parameter type should included as a first argument when creating customization. + public bool IncludeParameterType { get; set; } + + protected object CreateInstance(ParameterInfo parameter) + { + var args = this.IncludeParameterType + ? new object[] { parameter.NotNull(nameof(parameter)).ParameterType } + .Concat(this.Args ?? Array.Empty()) + .ToArray() + : this.Args; + + return Activator.CreateInstance(this.Type, args); + } + } +} diff --git a/src/Objectivity.AutoFixture.XUnit2.Core/Attributes/CustomizeWithAttribute.cs b/src/Objectivity.AutoFixture.XUnit2.Core/Attributes/CustomizeWithAttribute.cs index ee69b6d8..cc9f2bfe 100644 --- a/src/Objectivity.AutoFixture.XUnit2.Core/Attributes/CustomizeWithAttribute.cs +++ b/src/Objectivity.AutoFixture.XUnit2.Core/Attributes/CustomizeWithAttribute.cs @@ -2,49 +2,22 @@ { using System; using System.Diagnostics.CodeAnalysis; - using System.Linq; using System.Reflection; using global::AutoFixture; - using global::AutoFixture.Xunit2; - using Objectivity.AutoFixture.XUnit2.Core.Common; [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true)] [SuppressMessage("Performance", "CA1813:Avoid unsealed attributes", Justification = "This attribute should be extendable by inheritance.")] - public class CustomizeWithAttribute : CustomizeAttribute + public class CustomizeWithAttribute : CustomizeAdapterAttribute { public CustomizeWithAttribute(Type type, params object[] args) + : base(type, args) { - this.Type = type.NotNull(nameof(type)); - - var customizationType = typeof(ICustomization); - if (!customizationType.IsAssignableFrom(type)) - { - throw new ArgumentException($"Specified argument {nameof(type)} must implement {customizationType.Name}"); - } - - this.Args = args; } - public Type Type { get; } - - public object[] Args { get; } - - /// - /// Gets or sets a value indicating whether attribute target parameter type should included as a first argument when creating customization. - /// - /// Indicates whether attribute target parameter type should included as a first argument when creating customization. - public bool IncludeParameterType { get; set; } - public override ICustomization GetCustomization(ParameterInfo parameter) { - var args = this.IncludeParameterType - ? new object[] { parameter.NotNull(nameof(parameter)).ParameterType } - .Concat(this.Args ?? Array.Empty()) - .ToArray() - : this.Args; - - return Activator.CreateInstance(this.Type, args) as ICustomization; + return this.CreateInstance(parameter) as ICustomization; } } } diff --git a/src/Objectivity.AutoFixture.XUnit2.Core/Attributes/FromRangeAttribute.cs b/src/Objectivity.AutoFixture.XUnit2.Core/Attributes/FromRangeAttribute.cs new file mode 100644 index 00000000..c4a7f997 --- /dev/null +++ b/src/Objectivity.AutoFixture.XUnit2.Core/Attributes/FromRangeAttribute.cs @@ -0,0 +1,52 @@ +namespace Objectivity.AutoFixture.XUnit2.Core.Attributes +{ + using System; + + using Objectivity.AutoFixture.XUnit2.Core.SpeciminBuilders; + + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true)] + public sealed class FromRangeAttribute : BuildWithAttribute + { + public FromRangeAttribute(int minimum, int maximum) + : this((object)minimum, maximum) + { + } + + public FromRangeAttribute(uint minimum, uint maximum) + : this((object)minimum, maximum) + { + } + + public FromRangeAttribute(long minimum, long maximum) + : this((object)minimum, maximum) + { + } + + public FromRangeAttribute(ulong minimum, ulong maximum) + : this((object)minimum, maximum) + { + } + + public FromRangeAttribute(double minimum, double maximum) + : this((object)minimum, maximum) + { + } + + public FromRangeAttribute(float minimum, float maximum) + : this((object)minimum, maximum) + { + } + + public FromRangeAttribute(object minimum, object maximum) + : base(minimum, maximum) + { + this.IncludeParameterType = true; + this.Minimum = minimum; + this.Maximum = maximum; + } + + public object Minimum { get; } + + public object Maximum { get; } + } +} diff --git a/src/Objectivity.AutoFixture.XUnit2.Core/SpeciminBuilders/RandomRangedNumberBuilder.cs b/src/Objectivity.AutoFixture.XUnit2.Core/SpeciminBuilders/RandomRangedNumberBuilder.cs new file mode 100644 index 00000000..59b56997 --- /dev/null +++ b/src/Objectivity.AutoFixture.XUnit2.Core/SpeciminBuilders/RandomRangedNumberBuilder.cs @@ -0,0 +1,29 @@ +namespace Objectivity.AutoFixture.XUnit2.Core.SpeciminBuilders +{ + using System; + + using global::AutoFixture; + using global::AutoFixture.Kernel; + + using Objectivity.AutoFixture.XUnit2.Core.Common; + + public class RandomRangedNumberBuilder : ISpecimenBuilder + { + private readonly RandomRangedNumberGenerator randomRangedNumberGenerator = new(); + + public RandomRangedNumberBuilder(Type operandType, object minimum, object maximum) + { + this.Request = new RangedNumberRequest( + operandType.NotNull(nameof(operandType)), + minimum.NotNull(nameof(minimum)), + maximum.NotNull(nameof(maximum))); + } + + public RangedNumberRequest Request { get; } + + public object Create(object request, ISpecimenContext context) + { + return this.randomRangedNumberGenerator.Create(this.Request, context); + } + } +}