-
Notifications
You must be signed in to change notification settings - Fork 338
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fill Readonly Collection Properties Behavior (#1177)
* Add behavior which fills readonly collection properties, such that the guidelines specified at https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/guidelines-for-collections?redirectedfrom=MSDN#collection-properties-and-return-values do not inhibit usage of the fixture. * Introduce `InstanceMethodQuery` to query for the `Add` method on collection properties. * Introduce the `IPropertyQuery` abstraction to separate away the responsibility of selecting applicable properties from the `ReadonlyCollectionPropertiesCommand`. * Swallow `NotSupportedException`s that arise from invoking `Add` methods in the `ReadonlyCollectionPropertiesCommand` (e.g. `SortedList`). * Determine the specimen type to be created based on the parameter type of the `Add` method, as opposed to the generic argument of `ICollection<>`. * Ensure that the `ReadonlyCollectionPropertiesBehavior` does not apply to `Fixture` or `IFixture`.
- Loading branch information
1 parent
7e26b35
commit c127ca4
Showing
24 changed files
with
1,048 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Reflection; | ||
|
||
namespace AutoFixture.Kernel | ||
{ | ||
/// <summary> | ||
/// A query which is used to compose multiple <see cref="IPropertyQuery"/>s, returning only those properties | ||
/// that are returned by all queries. | ||
/// </summary> | ||
public class AndPropertyQuery : IPropertyQuery | ||
{ | ||
/// <summary> | ||
/// Constructs an instance of <see cref="AndPropertyQuery"/>, that will select properties from specified types | ||
/// that are returned by <paramref name="queries"/>. | ||
/// </summary> | ||
/// <param name="queries">The queries that should be used to select properties.</param> | ||
public AndPropertyQuery(params IPropertyQuery[] queries) | ||
{ | ||
this.Queries = queries; | ||
} | ||
|
||
/// <summary> | ||
/// Gets the queries that are used to select properties from specified types. | ||
/// </summary> | ||
public IEnumerable<IPropertyQuery> Queries { get; } | ||
|
||
/// <summary> | ||
/// Selects properties from <paramref name="type"/> that are returned by <see cref="Queries"/>. | ||
/// </summary> | ||
/// <param name="type">The type which properties should be selected from.</param> | ||
/// <returns>Properties belonging to <paramref name="type"/> that meet <see cref="Queries"/>.</returns> | ||
public IEnumerable<PropertyInfo> SelectProperties(Type type) | ||
{ | ||
var properties = new HashSet<PropertyInfo>(this.Queries.First().SelectProperties(type)); | ||
|
||
foreach (var query in this.Queries.Skip(1)) | ||
{ | ||
properties.IntersectWith(query.SelectProperties(type)); | ||
} | ||
|
||
return properties; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Globalization; | ||
using System.Linq; | ||
|
||
namespace AutoFixture.Kernel | ||
{ | ||
internal static class EnumerableEnvy | ||
{ | ||
public static IEnumerable<object> ConvertObjectType(this IEnumerable<object> enumerable, Type type) | ||
{ | ||
return enumerable.Select(v => Convert.ChangeType(v, type, CultureInfo.CurrentCulture)); | ||
This comment has been minimized.
Sorry, something went wrong. |
||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Reflection; | ||
|
||
namespace AutoFixture.Kernel | ||
{ | ||
/// <summary> | ||
/// A query which returns generic collection properties from specified types. | ||
/// </summary> | ||
public class GenericCollectionPropertyQuery : IPropertyQuery | ||
{ | ||
/// <summary> | ||
/// Select those properties that are generic collections from <paramref name="type"/>. | ||
/// </summary> | ||
/// <param name="type">The type which generic collection properties should be selected from.</param> | ||
/// <returns>Properties belonging to <paramref name="type"/> that are collections.</returns> | ||
public IEnumerable<PropertyInfo> SelectProperties(Type type) | ||
{ | ||
return type.GetTypeInfo().GetProperties() | ||
.Where(p => | ||
p.PropertyType.Name == typeof(ICollection<>).Name || | ||
p.PropertyType.GetTypeInfo().GetInterface(typeof(ICollection<>).Name) != null); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Reflection; | ||
|
||
namespace AutoFixture.Kernel | ||
{ | ||
/// <summary> | ||
/// Defines a strategy for selecting properties from a type. | ||
/// </summary> | ||
public interface IPropertyQuery | ||
{ | ||
/// <summary> | ||
/// Selects the properties for the specified <paramref name="type"/>. | ||
/// </summary> | ||
/// <param name="type">The type.</param> | ||
/// <returns>Property information for properties belonging to <paramref name="type"/>.</returns> | ||
IEnumerable<PropertyInfo> SelectProperties(Type type); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Reflection; | ||
|
||
namespace AutoFixture.Kernel | ||
{ | ||
/// <summary> | ||
/// Selects a method from an instance. | ||
/// </summary> | ||
public class InstanceMethodQuery : IMethodQuery | ||
{ | ||
/// <summary> | ||
/// Constructs an instance of an <see cref="InstanceMethodQuery"/>, used to select a particular method | ||
/// from an instance. | ||
/// </summary> | ||
/// <param name="owner">The instance that should be selected from.</param> | ||
/// <param name="methodName">The name of the method that should be selected.</param> | ||
public InstanceMethodQuery(object owner, string methodName) | ||
{ | ||
this.Owner = owner ?? throw new ArgumentNullException(nameof(owner)); | ||
this.MethodName = methodName ?? throw new ArgumentNullException(nameof(methodName)); | ||
} | ||
|
||
/// <summary> | ||
/// Gets the instance that should be selected from. | ||
/// </summary> | ||
public object Owner { get; } | ||
|
||
/// <summary> | ||
/// Gets the name of the method that should be selected. | ||
/// </summary> | ||
public string MethodName { get; } | ||
|
||
/// <summary> | ||
/// Selects <see cref="MethodName"/> from <see cref="Owner"/>. | ||
/// </summary> | ||
/// <param name="type">Discarded.</param> | ||
/// <returns>Returns an empty enumerable if <see cref="MethodName"/> does not belong to <see cref="Owner"/>; | ||
/// returns an enumerable containing a single <see cref="InstanceMethod"/> otherwise.</returns> | ||
public IEnumerable<IMethod> SelectMethods(Type type = default) | ||
{ | ||
var method = this.Owner.GetType().GetTypeInfo().GetMethod(this.MethodName); | ||
|
||
return method == null | ||
? new IMethod[0] | ||
: new IMethod[] { new InstanceMethod(method, this.Owner) }; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
using System; | ||
|
||
namespace AutoFixture.Kernel | ||
{ | ||
/// <summary> | ||
/// A specification which omits the <see cref="Fixture"/> and <see cref="IFixture"/> types. | ||
/// </summary> | ||
public class OmitFixtureSpecification : IRequestSpecification | ||
{ | ||
/// <summary> | ||
/// Evaluates whether or not the <paramref name="request"/> is for a fixture type. | ||
/// </summary> | ||
/// <param name="request">The specimen request.</param> | ||
/// <returns> | ||
/// <see langword="false"/> if the <paramref name="request"/> is for a fixture type; | ||
/// <see langword="false"/> otherwise. | ||
/// </returns> | ||
public bool IsSatisfiedBy(object request) | ||
{ | ||
return !(request is Type requestType && (requestType == typeof(Fixture) || requestType == typeof(IFixture))); | ||
} | ||
} | ||
} |
83 changes: 83 additions & 0 deletions
83
Src/AutoFixture/Kernel/ReadonlyCollectionPropertiesCommand.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Reflection; | ||
|
||
namespace AutoFixture.Kernel | ||
{ | ||
/// <summary> | ||
/// A command which invokes <see cref="ICollection{T}.Add"/> to fill all readonly properties in a specimen that | ||
/// implement <see cref="ICollection{T}"/>. | ||
/// </summary> | ||
public class ReadonlyCollectionPropertiesCommand : ISpecimenCommand | ||
{ | ||
/// <summary> | ||
/// Constructs an instance of <see cref="ReadonlyCollectionPropertiesCommand"/>, used to fill all readonly | ||
/// properties in a specimen that implement <see cref="ICollection{T}"/>. | ||
/// </summary> | ||
public ReadonlyCollectionPropertiesCommand() | ||
: this(ReadonlyCollectionPropertiesSpecification.DefaultPropertyQuery) | ||
{ | ||
} | ||
|
||
/// <summary> | ||
/// Constructs an instance of <see cref="ReadonlyCollectionPropertiesCommand"/>, used to fill all readonly | ||
/// properties in a specimen that implement <see cref="ICollection{T}"/>. | ||
/// </summary> | ||
/// <param name="propertyQuery">The query that will be applied to select readonly collection properties.</param> | ||
public ReadonlyCollectionPropertiesCommand(IPropertyQuery propertyQuery) | ||
{ | ||
this.PropertyQuery = propertyQuery; | ||
} | ||
|
||
/// <summary> | ||
/// Gets the query used to determine whether or not a specified type has readonly collection properties. | ||
/// </summary> | ||
public IPropertyQuery PropertyQuery { get; } | ||
|
||
/// <summary> | ||
/// Invokes <see cref="ICollection{T}.Add"/> to fill all readonly properties in a specimen that implement | ||
/// <see cref="ICollection{T}"/>. | ||
/// </summary> | ||
/// <param name="specimen"> | ||
/// The specimen on which readonly collection properties should be filled. | ||
/// </param> | ||
/// <param name="context"> | ||
/// An <see cref="ISpecimenContext"/> that is used to create the elements used to fill collections. | ||
/// </param> | ||
/// <exception cref="ArgumentNullException"> | ||
/// Thrown if <paramref name="specimen"/> or <paramref name="context"/> is <see langword="null"/>. | ||
/// </exception> | ||
public void Execute(object specimen, ISpecimenContext context) | ||
{ | ||
if (specimen == null) throw new ArgumentNullException(nameof(specimen)); | ||
if (context == null) throw new ArgumentNullException(nameof(context)); | ||
|
||
var specimenType = specimen.GetType(); | ||
foreach (var pi in this.PropertyQuery.SelectProperties(specimenType)) | ||
{ | ||
var addMethod = new InstanceMethodQuery(pi.GetValue(specimen), nameof(ICollection<object>.Add)) | ||
.SelectMethods() | ||
.SingleOrDefault(); | ||
if (addMethod == null) continue; | ||
|
||
var valuesToAdd = SpecimenFactory.CreateMany( | ||
context, | ||
addMethod.Parameters.Single().ParameterType); | ||
|
||
foreach (var valueToAdd in valuesToAdd) | ||
{ | ||
try | ||
{ | ||
addMethod.Invoke(new[] { valueToAdd }); | ||
} | ||
catch (TargetInvocationException e) | ||
{ | ||
if (e.InnerException?.GetType() == typeof(NotSupportedException)) break; | ||
throw; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} |
61 changes: 61 additions & 0 deletions
61
Src/AutoFixture/Kernel/ReadonlyCollectionPropertiesSpecification.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
|
||
namespace AutoFixture.Kernel | ||
{ | ||
/// <summary> | ||
/// A specification that evaluates whether or not a request is for a type containing readonly properties that | ||
/// implement <see cref="ICollection{T}"/>. | ||
/// </summary> | ||
public class ReadonlyCollectionPropertiesSpecification : IRequestSpecification | ||
{ | ||
/// <summary> | ||
/// The default query that will be applied to select readonly collection properties. | ||
/// </summary> | ||
public static readonly IPropertyQuery DefaultPropertyQuery = new AndPropertyQuery( | ||
new ReadonlyPropertyQuery(), | ||
new GenericCollectionPropertyQuery()); | ||
|
||
/// <summary> | ||
/// Constructs an instance of <see cref="ReadonlyCollectionPropertiesSpecification"/> with a default | ||
/// query applied for selection of readonly collection properties. | ||
/// </summary> | ||
public ReadonlyCollectionPropertiesSpecification() | ||
: this(DefaultPropertyQuery) | ||
{ | ||
} | ||
|
||
/// <summary> | ||
/// Constructs an instance of <see cref="ReadonlyCollectionPropertiesSpecification"/>, which will use the query | ||
/// supplied in <paramref name="propertyQuery"/> to determine whether or not a type contains readonly collection | ||
/// properties. | ||
/// </summary> | ||
/// <param name="propertyQuery">The query that will be applied to select readonly collection properties.</param> | ||
public ReadonlyCollectionPropertiesSpecification(IPropertyQuery propertyQuery) | ||
{ | ||
this.PropertyQuery = propertyQuery; | ||
} | ||
|
||
/// <summary> | ||
/// Gets the query used to determine whether or not a specified type has readonly collection properties. | ||
/// </summary> | ||
public IPropertyQuery PropertyQuery { get; } | ||
|
||
/// <summary> | ||
/// Evaluates whether or not the <paramref name="request"/> is for a type containing readonly properties that | ||
/// implement <see cref="ICollection{T}"/>. | ||
/// </summary> | ||
/// <param name="request"> | ||
/// The specimen request. | ||
/// </param> | ||
/// <returns> | ||
/// <see langword="true"/> if the <paramref name="request"/> is for a type containing readonly properties that | ||
/// implement <see cref="ICollection{T}"/>; <see langword="false"/> otherwise. | ||
/// </returns> | ||
public bool IsSatisfiedBy(object request) | ||
{ | ||
return request is Type requestType && this.PropertyQuery.SelectProperties(requestType).Any(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Reflection; | ||
|
||
namespace AutoFixture.Kernel | ||
{ | ||
/// <summary> | ||
/// A query which returns readonly properties from specified types. | ||
/// </summary> | ||
public class ReadonlyPropertyQuery : IPropertyQuery | ||
{ | ||
/// <summary> | ||
/// Select those properties that are readonly from <paramref name="type"/>. | ||
/// </summary> | ||
/// <param name="type">The type which readonly properties should be selected from.</param> | ||
/// <returns>Properties belonging to <paramref name="type"/> that are readonly.</returns> | ||
public IEnumerable<PropertyInfo> SelectProperties(Type type) | ||
{ | ||
return type.GetTypeInfo().GetProperties().Where(p => p.GetSetMethod() == null); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Sorry, but this throws 'InvalidCastException: Object must implement IConvertible.' if I have a custom SpecimenBuilder for a class implementing an interface and not implementing IConvertible. Please check if type.IsAssignableFrom(v.GetType()) and don't call Convert.ChangeType in this case