Skip to content

Instantly share code, notes, and snippets.

@skarllot
Created April 10, 2025 15:59
Show Gist options
  • Save skarllot/3643f79ba60a726d33eb690ef85f5f9b to your computer and use it in GitHub Desktop.
Save skarllot/3643f79ba60a726d33eb690ef85f5f9b to your computer and use it in GitHub Desktop.
Control the depth when building fixture instances using AutoFixture
using System.Collections;
using AutoFixture.Kernel;
public class GenerationDepthBehavior : ISpecimenBuilderTransformation
{
private const int DefaultGenerationDepth = 1;
private readonly int _generationDepth;
public GenerationDepthBehavior(int generationDepth = DefaultGenerationDepth)
{
if (generationDepth < 1)
throw new ArgumentOutOfRangeException(nameof(generationDepth), "Generation depth must be greater than 0.");
_generationDepth = generationDepth;
}
public ISpecimenBuilderNode Transform(ISpecimenBuilder builder)
{
if (builder == null)
throw new ArgumentNullException(nameof(builder));
return new GenerationDepthGuard(builder, new GenerationDepthHandler(), _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;
if (request is not Type innerRequest)
return;
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 (var generic in innerRequest.GetGenericArguments())
{
level++;
level += GetGenerationLevel(generic);
}
}
return level;
}
}
public class GenerationDepthGuard : ISpecimenBuilderNode
{
private readonly ThreadLocal<Stack<DepthSeededRequest>> _requestsByThread = new(
() => new Stack<DepthSeededRequest>()
);
private Stack<DepthSeededRequest> GetMonitoredRequestsForCurrentThread() => _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)
{
Builder = builder ?? throw new ArgumentNullException(nameof(builder));
Comparer = comparer ?? throw new ArgumentNullException(nameof(comparer));
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 (generationDepth < 1)
throw new ArgumentOutOfRangeException(nameof(generationDepth), "Generation depth must be greater than 0.");
Builder = builder ?? throw new ArgumentNullException(nameof(builder));
GenerationDepthHandler = depthHandler ?? throw new ArgumentNullException(nameof(depthHandler));
Comparer = comparer ?? throw new ArgumentNullException(nameof(comparer));
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 => GetMonitoredRequestsForCurrentThread();
public virtual object HandleGenerationDepthLimitRequest(object request, int currentDepth)
{
return GenerationDepthHandler.HandleGenerationDepthLimitRequest(
request,
GetMonitoredRequestsForCurrentThread(),
currentDepth
);
}
public object Create(object request, ISpecimenContext context)
{
if (request is SeededRequest seededRequest)
{
int currentDepth = 0;
var requestsForCurrentThread = GetMonitoredRequestsForCurrentThread();
if (requestsForCurrentThread.Count > 0)
{
currentDepth = requestsForCurrentThread.Max(x => x.Depth) + 1;
}
var depthRequest = new DepthSeededRequest(seededRequest.Request, seededRequest.Seed, currentDepth);
if (depthRequest.Depth >= GenerationDepth)
{
var parentRequest = requestsForCurrentThread.Peek();
depthRequest.MaxDepth = parentRequest.Depth + parentRequest.GenerationLevel;
if (!(parentRequest.ContinueSeed && currentDepth < depthRequest.MaxDepth))
{
return HandleGenerationDepthLimitRequest(seededRequest, depthRequest.Depth);
}
}
requestsForCurrentThread.Push(depthRequest);
try
{
return Builder.Create(seededRequest, 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, GenerationDepthHandler, Comparer, 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 Builder;
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
public class GenerationDepthHandler : IGenerationDepthHandler
{
public object HandleGenerationDepthLimitRequest(object request, IEnumerable<object> recordedRequests, int depth)
{
return new OmitSpecimen();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment