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 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> _requestsByThread = new( () => new Stack() ); private Stack GetMonitoredRequestsForCurrentThread() => _requestsByThread.Value!; public GenerationDepthGuard(ISpecimenBuilder builder) : this(builder, EqualityComparer.Default) { } public GenerationDepthGuard(ISpecimenBuilder builder, IGenerationDepthHandler depthHandler) : this(builder, depthHandler, EqualityComparer.Default, 1) { } public GenerationDepthGuard(ISpecimenBuilder builder, IGenerationDepthHandler depthHandler, int generationDepth) : this(builder, depthHandler, EqualityComparer.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 builders) { var composedBuilder = ComposeIfMultiple(builders); return new GenerationDepthGuard(composedBuilder, GenerationDepthHandler, Comparer, GenerationDepth); } internal static ISpecimenBuilder ComposeIfMultiple(IEnumerable builders) { ISpecimenBuilder? singleItem = null; List? 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 { singleItem }; } multipleItems.Add(enumerator.Current); } } } if (!hasItems) { return new CompositeSpecimenBuilder(); } if (multipleItems == null) { return singleItem!; } return new CompositeSpecimenBuilder(multipleItems); } public virtual IEnumerator GetEnumerator() { yield return Builder; } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } } public class GenerationDepthHandler : IGenerationDepthHandler { public object HandleGenerationDepthLimitRequest(object request, IEnumerable recordedRequests, int depth) { return new OmitSpecimen(); } }