Skip to content

Commit 3628319

Browse files
authored
Enhance property injection to support inherited data source attributes (#3144)
1 parent b6eb813 commit 3628319

File tree

2 files changed

+145
-7
lines changed

2 files changed

+145
-7
lines changed

TUnit.Core/PropertyInjection/PropertyInjectionPlanBuilder.cs

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,22 +36,40 @@ public static PropertyInjectionPlan BuildSourceGeneratedPlan(Type type)
3636
/// Creates an injection plan for reflection mode.
3737
/// </summary>
3838
[UnconditionalSuppressMessage("Trimming", "IL2070", Justification = "Reflection mode support")]
39+
[UnconditionalSuppressMessage("Trimming", "IL2075", Justification = "BaseType reflection is required for inheritance support")]
3940
public static PropertyInjectionPlan BuildReflectionPlan(Type type)
4041
{
41-
var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)
42-
.Where(p => p.CanWrite || p.SetMethod?.IsPublic == false); // Include init-only properties
43-
4442
var propertyDataSourcePairs = new List<(PropertyInfo property, IDataSourceAttribute dataSource)>();
43+
var processedProperties = new HashSet<string>();
4544

46-
foreach (var property in properties)
45+
// Walk up the inheritance chain to find all properties with data source attributes
46+
var currentType = type;
47+
while (currentType != null && currentType != typeof(object))
4748
{
48-
foreach (var attr in property.GetCustomAttributes())
49+
var properties = currentType.GetProperties(
50+
BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly)
51+
.Where(p => p.CanWrite || p.SetMethod?.IsPublic == false); // Include init-only properties
52+
53+
foreach (var property in properties)
4954
{
50-
if (attr is IDataSourceAttribute dataSourceAttr)
55+
// Skip if we've already processed a property with this name (overridden in derived class)
56+
if (!processedProperties.Add(property.Name))
5157
{
52-
propertyDataSourcePairs.Add((property, dataSourceAttr));
58+
continue;
59+
}
60+
61+
// Check for data source attributes, including inherited attributes
62+
foreach (var attr in property.GetCustomAttributes(inherit: true))
63+
{
64+
if (attr is IDataSourceAttribute dataSourceAttr)
65+
{
66+
propertyDataSourcePairs.Add((property, dataSourceAttr));
67+
break; // Only one data source per property
68+
}
5369
}
5470
}
71+
72+
currentType = currentType.BaseType;
5573
}
5674

5775
return new PropertyInjectionPlan
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
using TUnit.Core;
2+
using TUnit.Core.Interfaces;
3+
using TUnit.TestProject.Attributes;
4+
5+
namespace TUnit.TestProject.Bugs._2955;
6+
7+
// Reproducing the issue from GitHub issue #2955
8+
// https://github.com/thomhurst/TUnit/issues/2955
9+
10+
public class Data1 : IAsyncInitializer
11+
{
12+
public string Value { get; set; } = string.Empty;
13+
14+
public Task InitializeAsync()
15+
{
16+
Value = "Data1 Initialized";
17+
Console.WriteLine($"Data1 InitializeAsync called - Value: {Value}");
18+
return Task.CompletedTask;
19+
}
20+
}
21+
22+
public class Data2 : IAsyncInitializer
23+
{
24+
[ClassDataSource<Data1>]
25+
public required Data1 Data1 { get; init; }
26+
27+
public string Value { get; set; } = string.Empty;
28+
29+
public virtual Task InitializeAsync()
30+
{
31+
// This should be called after Data1 has been injected and initialized
32+
Value = $"Data2 Initialized (Data1: {Data1?.Value ?? "NULL"})";
33+
Console.WriteLine($"Data2 InitializeAsync called - Value: {Value}, Data1: {Data1?.Value ?? "NULL"}");
34+
return Task.CompletedTask;
35+
}
36+
}
37+
38+
// Data3 inherits from Data2, so it should inherit the Data1 property with its ClassDataSource attribute
39+
public class Data3 : Data2
40+
{
41+
public override Task InitializeAsync()
42+
{
43+
// This should be called after Data1 has been injected and initialized
44+
Value = $"Data3 Initialized (Data1: {Data1?.Value ?? "NULL"})";
45+
Console.WriteLine($"Data3 InitializeAsync called - Value: {Value}, Data1: {Data1?.Value ?? "NULL"}");
46+
return Task.CompletedTask;
47+
}
48+
}
49+
50+
[EngineTest(ExpectedResult.Pass)]
51+
public class InheritedDataSourceTests
52+
{
53+
[ClassDataSource<Data3>(Shared = SharedType.PerTestSession)]
54+
public required Data3 Data3 { get; init; }
55+
56+
[Test]
57+
public async Task Test_InheritedPropertyWithDataSource_ShouldBeInjected()
58+
{
59+
// The bug is that Data1 property (inherited from Data2) is not being injected
60+
// when Data3 is used as a ClassDataSource
61+
62+
Console.WriteLine($"Test - Data3.Value: {Data3.Value}");
63+
Console.WriteLine($"Test - Data3.Data1: {Data3.Data1}");
64+
Console.WriteLine($"Test - Data3.Data1?.Value: {Data3.Data1?.Value}");
65+
66+
// This assertion should pass but currently fails with the bug
67+
await Assert.That(Data3.Data1).IsNotNull();
68+
await Assert.That(Data3.Data1.Value).IsEqualTo("Data1 Initialized");
69+
await Assert.That(Data3.Value).Contains("Data1: Data1 Initialized");
70+
}
71+
72+
[Test]
73+
[ClassDataSource<Data2>]
74+
public async Task Test_DirectDataSource_WorksCorrectly(Data2 data2)
75+
{
76+
// This test uses Data2 directly (not through inheritance) and should work
77+
// The framework should inject Data1 into Data2 directly
78+
79+
// This should work because Data2's properties are defined directly on it
80+
await Assert.That(data2.Data1).IsNotNull();
81+
await Assert.That(data2.Data1.Value).IsEqualTo("Data1 Initialized");
82+
await Assert.That(data2.Value).Contains("Data1: Data1 Initialized");
83+
}
84+
}
85+
86+
// Additional test case with multiple levels of inheritance
87+
public class BaseDataWithSource
88+
{
89+
[ClassDataSource<Data1>]
90+
public required Data1 BaseData1 { get; init; }
91+
}
92+
93+
public class MiddleDataWithSource : BaseDataWithSource
94+
{
95+
[ClassDataSource<Data2>]
96+
public required Data2 MiddleData2 { get; init; }
97+
}
98+
99+
public class DerivedDataWithSource : MiddleDataWithSource
100+
{
101+
public string DerivedValue { get; set; } = "Derived";
102+
}
103+
104+
[EngineTest(ExpectedResult.Pass)]
105+
public class MultiLevelInheritanceTests
106+
{
107+
[ClassDataSource<DerivedDataWithSource>]
108+
public required DerivedDataWithSource DerivedData { get; init; }
109+
110+
[Test]
111+
public async Task Test_MultiLevelInheritance_AllDataSourcesShouldBeInjected()
112+
{
113+
// Both BaseData1 and MiddleData2 should be injected even though they're in base classes
114+
await Assert.That(DerivedData.BaseData1).IsNotNull();
115+
await Assert.That(DerivedData.BaseData1.Value).IsEqualTo("Data1 Initialized");
116+
117+
await Assert.That(DerivedData.MiddleData2).IsNotNull();
118+
await Assert.That(DerivedData.MiddleData2.Data1).IsNotNull();
119+
}
120+
}

0 commit comments

Comments
 (0)