Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fill Readonly Collection Properties Behavior #1177

Merged
merged 15 commits into from
Apr 14, 2021
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Src/AutoFixture/Fixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ public Fixture(ISpecimenBuilder engine, MultipleRelay multiple)
this.UpdateGraphAndSetupAdapters(newGraph, Enumerable.Empty<ISpecimenBuilderTransformation>());

this.Behaviors.Add(new ThrowingRecursionBehavior());
this.Behaviors.Add(new ReadonlyCollectionPropertiesBehavior());
charles-salmon marked this conversation as resolved.
Show resolved Hide resolved
}

/// <inheritdoc />
Expand Down
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;
}
}
}
26 changes: 26 additions & 0 deletions Src/AutoFixture/Kernel/CollectionPropertyQuery.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 collection properties from specified types.
/// </summary>
public class CollectionPropertyQuery : IPropertyQuery
{
/// <summary>
/// Select those properties that are collections from <paramref name="type"/>.
/// </summary>
/// <param name="type">The type which 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 ||
charles-salmon marked this conversation as resolved.
Show resolved Hide resolved
p.PropertyType.GetTypeInfo().GetInterface(typeof(ICollection<>).Name) != null);
}
}
}
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));
}
}
}
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);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am getting AmbiguosMatchFound Exception here, the Collection that I am using is RepeatedField from protobuf, Not sure if this can happen anywhere else, but maybe worth checking in.
I have solved it like:

public IEnumerable<IMethod> SelectMethods()
        {
            if(Owner == null) return Array.Empty<IMethod>();
            var methods = this.Owner.GetType().GetMethods();
            foreach (var methodInfo in methods)
            {
                if (methodInfo.Name == MethodName && methodInfo.GetParameters().Length == 1)
                {
                    return new IMethod[]
                    {
                        new InstanceMethod(methodInfo, this.Owner)
                    };
                }
            }
            
            return Array.Empty<IMethod>();
        }

Not sure if ideal, but desperate time - require desperate measures :)


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 CollectionPropertyQuery());

/// <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