Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Controlling the depth of generation of an object tree with Autofixture

Tags:

c#

autofixture

I'm trying to control the depth of generation of an object tree with Autofixture. In some cases I want just to generate the root object and in another set of cases I may want to generate the tree up to a certain depth (2, 3, let's say).

class Foo {
    public string Name {get;set;}
    public Bar Bar {get;set;}
    public AnotherType Xpto {get;set;}
    public YetAnotherType Xpto {get;set;}
}

class Bar {
    public string Name {get;set;}
    public string Description {get;set;}
    public AnotherType Xpto {get;set;}
    public YetAnotherType Xpto {get;set;}
    public Xpto Xpto {get;set;}
}

class Xpto {
    public string Description {get;set;}
    public AnotherType Xpto {get;set;}
    public YetAnotherType Xpto {get;set;}
}

With the example above I would want (depth 1) to control the generation process so that only the Foo class is instantiated and the Bar property or any other reference type on that class is not populated or (depth 2) I would want the Foo class instantiated, the Bar property populated with a new instance of Bar but the Xpto property or any other reference type on that class not populated.

In case I did not spot it in the codebase does Autofixture have a customisation or behaviour to allow us to have that kind of control?

Again, it's not recursion that I want to control but the depth of population of the object graph.

like image 841
Jay Avatar asked Nov 13 '13 10:11

Jay


3 Answers

You can use the below GenerationDepthBehavior class as follows:

fixture.Behaviors.Add(new GenerationDepthBehavior(2));

public class GenerationDepthBehavior : ISpecimenBuilderTransformation
{
    private const int DefaultGenerationDepth = 1;
    private readonly int generationDepth;

    public GenerationDepthBehavior() : this(DefaultGenerationDepth)
    {
    }

    public GenerationDepthBehavior(int generationDepth)
    {
        if (generationDepth < 1)
            throw new ArgumentOutOfRangeException(nameof(generationDepth), "Generation depth must be greater than 0.");

        this.generationDepth = generationDepth;
    }

    public ISpecimenBuilderNode Transform(ISpecimenBuilder builder)
    {
        if (builder == null) throw new ArgumentNullException(nameof(builder));

        return new GenerationDepthGuard(builder, new GenerationDepthHandler(), this.generationDepth);
    }
}

public interface IGenerationDepthHandler
{
    object HandleGenerationDepthLimitRequest(object request, IEnumerable<object> recordedRequests, int depth);
}

public class DepthSeededRequest : SeededRequest
{
    public int Depth { get; }

    public int MaxDepth { get; set; }

    public bool ContinueSeed { get; }

    public int GenerationLevel { get; private set; }

    public DepthSeededRequest(object request, object seed, int depth) : base(request, seed)
    {
        Depth = depth;

        Type innerRequest = request as Type;

        if (innerRequest != null)
        {
            bool nullable = Nullable.GetUnderlyingType(innerRequest) != null;

            ContinueSeed = nullable || innerRequest.IsGenericType;

            if (ContinueSeed)
            {
                GenerationLevel = GetGenerationLevel(innerRequest);
            }
        }
    }

    private int GetGenerationLevel(Type innerRequest)
    {
        int level = 0;

        if (Nullable.GetUnderlyingType(innerRequest) != null)
        {
            level = 1;
        }

        if (innerRequest.IsGenericType)
        {
            foreach (Type generic in innerRequest.GetGenericArguments())
            {
                level++;

                level += GetGenerationLevel(generic);
            }
        }

        return level;
    }
}

public class GenerationDepthGuard : ISpecimenBuilderNode
{
    private readonly ThreadLocal<Stack<DepthSeededRequest>> requestsByThread
        = new ThreadLocal<Stack<DepthSeededRequest>>(() => new Stack<DepthSeededRequest>());

    private Stack<DepthSeededRequest> GetMonitoredRequestsForCurrentThread() => this.requestsByThread.Value;

    public GenerationDepthGuard(ISpecimenBuilder builder)
        : this(builder, EqualityComparer<object>.Default)
    {
    }

    public GenerationDepthGuard(
        ISpecimenBuilder builder,
        IGenerationDepthHandler depthHandler)
        : this(
            builder,
            depthHandler,
            EqualityComparer<object>.Default,
            1)
    {
    }

    public GenerationDepthGuard(
        ISpecimenBuilder builder,
        IGenerationDepthHandler depthHandler,
        int generationDepth)
        : this(
            builder,
            depthHandler,
            EqualityComparer<object>.Default,
            generationDepth)
    {
    }

    public GenerationDepthGuard(ISpecimenBuilder builder, IEqualityComparer comparer)
    {
        this.Builder = builder ?? throw new ArgumentNullException(nameof(builder));
        this.Comparer = comparer ?? throw new ArgumentNullException(nameof(comparer));
        this.GenerationDepth = 1;
    }

    public GenerationDepthGuard(
        ISpecimenBuilder builder,
        IGenerationDepthHandler depthHandler,
        IEqualityComparer comparer)
        : this(
        builder,
        depthHandler,
        comparer,
        1)
    {
    }

    public GenerationDepthGuard(
        ISpecimenBuilder builder,
        IGenerationDepthHandler depthHandler,
        IEqualityComparer comparer,
        int generationDepth)
    {
        if (builder == null) throw new ArgumentNullException(nameof(builder));
        if (depthHandler == null) throw new ArgumentNullException(nameof(depthHandler));
        if (comparer == null) throw new ArgumentNullException(nameof(comparer));
        if (generationDepth < 1)
            throw new ArgumentOutOfRangeException(nameof(generationDepth), "Generation depth must be greater than 0.");

        this.Builder = builder;
        this.GenerationDepthHandler = depthHandler;
        this.Comparer = comparer;
        this.GenerationDepth = generationDepth;
    }

    public ISpecimenBuilder Builder { get; }

    public IGenerationDepthHandler GenerationDepthHandler { get; }

    public int GenerationDepth { get; }

    public int CurrentDepth { get; }

    public IEqualityComparer Comparer { get; }

    protected IEnumerable RecordedRequests => this.GetMonitoredRequestsForCurrentThread();

    public virtual object HandleGenerationDepthLimitRequest(object request, int currentDepth)
    {
        return this.GenerationDepthHandler.HandleGenerationDepthLimitRequest(
            request,
            this.GetMonitoredRequestsForCurrentThread(), currentDepth);
    }

    public object Create(object request, ISpecimenContext context)
    {
        if (request is SeededRequest)
        {
            int currentDepth = 0;

            var requestsForCurrentThread = GetMonitoredRequestsForCurrentThread();

            if (requestsForCurrentThread.Count > 0)
            {
                currentDepth = requestsForCurrentThread.Max(x => x.Depth) + 1;
            }

            DepthSeededRequest depthRequest = new DepthSeededRequest(((SeededRequest)request).Request, ((SeededRequest)request).Seed, currentDepth);

            if (depthRequest.Depth >= GenerationDepth)
            {
                var parentRequest = requestsForCurrentThread.Peek();

                depthRequest.MaxDepth = parentRequest.Depth + parentRequest.GenerationLevel;

                if (!(parentRequest.ContinueSeed && currentDepth < depthRequest.MaxDepth))
                {
                    return HandleGenerationDepthLimitRequest(request, depthRequest.Depth);
                }
            }

            requestsForCurrentThread.Push(depthRequest);
            try
            {
                return Builder.Create(request, context);
            }
            finally
            {
                requestsForCurrentThread.Pop();
            }
        }
        else
        {
            return Builder.Create(request, context);
        }
    }

    public virtual ISpecimenBuilderNode Compose(
        IEnumerable<ISpecimenBuilder> builders)
    {
        var composedBuilder = ComposeIfMultiple(
            builders);
        return new GenerationDepthGuard(
            composedBuilder,
            this.GenerationDepthHandler,
            this.Comparer,
            this.GenerationDepth);
    }

    internal static ISpecimenBuilder ComposeIfMultiple(IEnumerable<ISpecimenBuilder> builders)
    {
        ISpecimenBuilder singleItem = null;
        List<ISpecimenBuilder> multipleItems = null;
        bool hasItems = false;

        using (var enumerator = builders.GetEnumerator())
        {
            if (enumerator.MoveNext())
            {
                singleItem = enumerator.Current;
                hasItems = true;

                while (enumerator.MoveNext())
                {
                    if (multipleItems == null)
                    {
                        multipleItems = new List<ISpecimenBuilder> { singleItem };
                    }

                    multipleItems.Add(enumerator.Current);
                }
            }
        }

        if (!hasItems)
        {
            return new CompositeSpecimenBuilder();
        }

        if (multipleItems == null)
        {
            return singleItem;
        }

        return new CompositeSpecimenBuilder(multipleItems);
    }

    public virtual IEnumerator<ISpecimenBuilder> GetEnumerator()
    {
        yield return this.Builder;
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }
}

public class GenerationDepthHandler : IGenerationDepthHandler
{
    public object HandleGenerationDepthLimitRequest(
        object request,
        IEnumerable<object> recordedRequests, int depth)
    {
        return new OmitSpecimen();
    }
}
like image 176
Maly Lemire Avatar answered Nov 02 '22 12:11

Maly Lemire


No Bar

One-off:

var f = fixture.Build<Foo>().Without(f => f.Bar).Create();

Reusable:

fixture.Customize<Foo>(c => c.Without(f => f.Bar));
var f = fixture.Create<Foo>();

No Xpto

One-off:

var f = fixture
    .Build<Foo>()
    .With(
        f => f.Bar,
        fixture.Build<Bar>().Without(b => b.Xpto).Create())
    .Create();

Reusable:

fixture.Customize<Bar>(c => c.Without(b => b.Xpto));
var f = fixture.Create<Foo>();
like image 6
Mark Seemann Avatar answered Nov 02 '22 13:11

Mark Seemann


This feature was requested in a github issue. It was ultimately rejected. However, it was rejected because there was a nice, simple solution posted within the issue.

public class GenerationDepthBehavior: ISpecimenBuilderTransformation
{
    public int Depth { get; }

    public GenerationDepthBehavior(int depth)
    {
        Depth = depth;
    }

    public ISpecimenBuilderNode Transform(ISpecimenBuilder builder)
    {
        return new RecursionGuard(builder, new OmitOnRecursionHandler(), new IsSeededRequestComparer(), Depth);
    }

    private class IsSeededRequestComparer : IEqualityComparer
    {
        bool IEqualityComparer.Equals(object x, object y)
        {
            return x is SeededRequest && y is SeededRequest;
        }

        int IEqualityComparer.GetHashCode(object obj)
        {
            return obj is SeededRequest ? 0 : EqualityComparer<object>.Default.GetHashCode(obj);
        }
    }
}

You can then use this as follows:

fixture.Behaviors.Add(new GenerationDepthBehavior(2));

like image 1
Tim Wilson Avatar answered Nov 02 '22 13:11

Tim Wilson