Skip to content

Commit a295473

Browse files
committed
feat: enable frozen value reuse in MemberAutoNSubstituteDataAttribute
1 parent 960e4d1 commit a295473

File tree

1 file changed

+73
-12
lines changed

1 file changed

+73
-12
lines changed

src/Atc.Test/MemberAutoNSubstituteDataAttribute.cs

Lines changed: 73 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
namespace Atc.Test;
22

33
/// <summary>
4-
/// Provides a data source for a data theory, with the data coming from
5-
/// one of the following sources and combined with auto-generated data
6-
/// specimens generated by AutoFixture and NSubstitute.
7-
/// <list type="number">
8-
/// <item>A static property</item>
9-
/// <item>A static field</item>
10-
/// <item>A static method (with parameters)</item>
11-
/// </list>
12-
/// The member must return something compatible with
13-
/// IEnumerable&lt;object[]&gt; with the test data.
4+
/// Data attribute combining <see cref="MemberDataAttributeBase"/> semantics with AutoFixture + NSubstitute specimen generation.
145
/// </summary>
6+
/// <remarks>
7+
/// 1. The referenced member (field / property / method) must return a type assignable to <c>IEnumerable&lt;object?[]&gt;</c>.<br/>
8+
/// 2. Supplied row values are appended with generated specimens for any remaining test method parameters.<br/>
9+
/// 3. Parameters decorated with <see cref="FrozenAttribute"/> participate in a two-phase reuse model:
10+
/// <list type="number">
11+
/// <item><b>Direct positional reuse</b>: If the row already supplies a value at the frozen parameter's index, that instance is injected (frozen) into the fixture.</item>
12+
/// <item><b>Promotion</b>: If the frozen parameter appears later (no supplied value at its index yet) an earlier supplied argument whose runtime type is assignable to the frozen parameter type is promoted and injected.</item>
13+
/// </list>
14+
/// This mirrors the behavior of <c>ClassAutoNSubstituteDataAttribute</c> while extending it with the promotion scenario common in member data ordering.
15+
/// </remarks>
1516
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
1617
public sealed class MemberAutoNSubstituteDataAttribute : MemberDataAttributeBase
1718
{
@@ -36,14 +37,22 @@ public override async ValueTask<IReadOnlyCollection<ITheoryDataRow>> GetData(
3637
MethodInfo testMethod,
3738
DisposalTracker disposalTracker)
3839
{
40+
// Retrieve the original member data rows (without any AutoFixture augmentation yet).
3941
var baseRows = await base.GetData(testMethod, disposalTracker).ConfigureAwait(false);
4042
var parameters = testMethod.GetParameters();
4143
var augmented = new List<ITheoryDataRow>(baseRows.Count);
4244

45+
// Pre-compute an injector tailored to the frozen parameters of this method.
46+
var frozenInjector = BuildFrozenInjector(parameters);
47+
4348
foreach (var row in baseRows)
4449
{
45-
var data = row.GetData();
46-
var fixture = FixtureFactory.Create();
50+
var data = row.GetData(); // The raw supplied data (could be shorter than parameter list)
51+
var fixture = FixtureFactory.Create(); // Fresh fixture per row for isolation
52+
53+
// Apply frozen injections (positional + promotions) before generating remaining specimens.
54+
frozenInjector(data, fixture);
55+
4756
var extendedData = data
4857
.Concat(parameters
4958
.Skip(data.Length)
@@ -64,22 +73,74 @@ public override async ValueTask<IReadOnlyCollection<ITheoryDataRow>> GetData(
6473
return augmented;
6574
}
6675

76+
private static Action<object?[], IFixture> BuildFrozenInjector(ParameterInfo[] parameters)
77+
{
78+
// Identify parameters decorated with [Frozen]; capture index + type for later injection/promotion.
79+
var frozenParameters = parameters
80+
.Select((p, i) => (Index: i, Type: p.ParameterType, Frozen: p.GetCustomAttribute<FrozenAttribute>()))
81+
.Where(x => x.Frozen is not null)
82+
.ToArray();
83+
84+
if (frozenParameters.Length == 0)
85+
{
86+
// Fast path: no frozen parameters -> no-op.
87+
return static (_, _) => { };
88+
}
89+
90+
var injectMethod = typeof(FixtureRegistrar).GetMethod(
91+
nameof(FixtureRegistrar.Inject),
92+
BindingFlags.Public | BindingFlags.Static);
93+
94+
return (suppliedData, fixture) =>
95+
{
96+
// Phase 1: Direct positional injections for frozen parameters already covered by supplied row data.
97+
foreach (var frozen in frozenParameters)
98+
{
99+
if (suppliedData.Length > frozen.Index)
100+
{
101+
injectMethod?
102+
.MakeGenericMethod(frozen.Type)
103+
.Invoke(null, [fixture, suppliedData[frozen.Index]]);
104+
}
105+
}
106+
107+
// Phase 2: Promotions – for frozen parameters whose index is beyond supplied data length,
108+
// attempt to reuse an earlier compatible supplied argument (interface / base type friendly).
109+
foreach (var frozen in frozenParameters)
110+
{
111+
if (suppliedData.Length <= frozen.Index)
112+
{
113+
var promoted = suppliedData.FirstOrDefault(d => d is not null && frozen.Type.IsInstanceOfType(d));
114+
if (promoted is not null)
115+
{
116+
injectMethod?
117+
.MakeGenericMethod(frozen.Type)
118+
.Invoke(null, [fixture, promoted]);
119+
}
120+
}
121+
}
122+
};
123+
}
124+
67125
private static object GetSpecimen(
68126
IFixture fixture,
69127
ParameterInfo parameter)
70128
{
129+
// Gather parameter-level customization sources (e.g. [Frozen], [Greedy], etc.)
71130
var attributes = parameter
72131
.GetCustomAttributes()
73132
.OfType<IParameterCustomizationSource>()
74133
.OrderBy(x => x is FrozenAttribute);
75134

76135
foreach (var attribute in attributes)
77136
{
137+
// Each customization mutates the fixture before resolving the specimen.
78138
attribute
79139
.GetCustomization(parameter)
80140
.Customize(fixture);
81141
}
82142

143+
// Resolve the final specimen through AutoFixture's pipeline honoring prior customizations.
83144
return new SpecimenContext(fixture)
84145
.Resolve(parameter);
85146
}

0 commit comments

Comments
 (0)