Created
April 10, 2025 15:59
-
-
Save skarllot/3643f79ba60a726d33eb690ef85f5f9b to your computer and use it in GitHub Desktop.
Control the depth when building fixture instances using AutoFixture
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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