Skip to content

Commit

Permalink
Fill Readonly Collection Properties Behavior (#1177)
Browse files Browse the repository at this point in the history
* 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
charles-salmon committed Apr 14, 2021
1 parent 7e26b35 commit c127ca4
Show file tree
Hide file tree
Showing 24 changed files with 1,048 additions and 3 deletions.
46 changes: 46 additions & 0 deletions Src/AutoFixture/Kernel/AndPropertyQuery.cs
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;
}
}
}
15 changes: 15 additions & 0 deletions Src/AutoFixture/Kernel/EnumerableEnvy.cs
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.

Copy link
@doboczyakos

doboczyakos Sep 5, 2023

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

}
}
}
26 changes: 26 additions & 0 deletions Src/AutoFixture/Kernel/GenericCollectionPropertyQuery.cs
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);
}
}
}
19 changes: 19 additions & 0 deletions Src/AutoFixture/Kernel/IPropertyQuery.cs
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);
}
}
49 changes: 49 additions & 0 deletions Src/AutoFixture/Kernel/InstanceMethodQuery.cs
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) };
}
}
}
23 changes: 23 additions & 0 deletions Src/AutoFixture/Kernel/OmitFixtureSpecification.cs
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 Src/AutoFixture/Kernel/ReadonlyCollectionPropertiesCommand.cs
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;
}
}
}
}
}
}
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();
}
}
}
23 changes: 23 additions & 0 deletions Src/AutoFixture/Kernel/ReadonlyPropertyQuery.cs
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);
}
}
}
5 changes: 5 additions & 0 deletions Src/AutoFixture/Kernel/TypeEnvy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,10 @@ public static bool IsNumberType(this Type type)
return false;
}
}

public static object GetDefaultValue(this Type type)
{
return type.GetTypeInfo().IsValueType ? Activator.CreateInstance(type) : null;
}
}
}
Loading

0 comments on commit c127ca4

Please sign in to comment.