Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 25 additions & 7 deletions TUnit.Core/PropertyInjection/PropertyInjectionPlanBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,22 +36,40 @@ public static PropertyInjectionPlan BuildSourceGeneratedPlan(Type type)
/// Creates an injection plan for reflection mode.
/// </summary>
[UnconditionalSuppressMessage("Trimming", "IL2070", Justification = "Reflection mode support")]
[UnconditionalSuppressMessage("Trimming", "IL2075", Justification = "BaseType reflection is required for inheritance support")]
public static PropertyInjectionPlan BuildReflectionPlan(Type type)
{
var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)
.Where(p => p.CanWrite || p.SetMethod?.IsPublic == false); // Include init-only properties

var propertyDataSourcePairs = new List<(PropertyInfo property, IDataSourceAttribute dataSource)>();
var processedProperties = new HashSet<string>();

foreach (var property in properties)
// Walk up the inheritance chain to find all properties with data source attributes
var currentType = type;
while (currentType != null && currentType != typeof(object))
{
foreach (var attr in property.GetCustomAttributes())
var properties = currentType.GetProperties(
BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly)
.Where(p => p.CanWrite || p.SetMethod?.IsPublic == false); // Include init-only properties

foreach (var property in properties)
{
if (attr is IDataSourceAttribute dataSourceAttr)
// Skip if we've already processed a property with this name (overridden in derived class)
if (!processedProperties.Add(property.Name))
{
propertyDataSourcePairs.Add((property, dataSourceAttr));
continue;
}

// Check for data source attributes, including inherited attributes
foreach (var attr in property.GetCustomAttributes(inherit: true))
{
if (attr is IDataSourceAttribute dataSourceAttr)
{
propertyDataSourcePairs.Add((property, dataSourceAttr));
break; // Only one data source per property
}
}
}

currentType = currentType.BaseType;
}

return new PropertyInjectionPlan
Expand Down
120 changes: 120 additions & 0 deletions TUnit.TestProject/Bugs/2955/InheritedDataSourceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
using TUnit.Core;
using TUnit.Core.Interfaces;
using TUnit.TestProject.Attributes;

namespace TUnit.TestProject.Bugs._2955;

// Reproducing the issue from GitHub issue #2955
// https://github.com/thomhurst/TUnit/issues/2955

public class Data1 : IAsyncInitializer
{
public string Value { get; set; } = string.Empty;

public Task InitializeAsync()
{
Value = "Data1 Initialized";
Console.WriteLine($"Data1 InitializeAsync called - Value: {Value}");
return Task.CompletedTask;
}
}

public class Data2 : IAsyncInitializer
{
[ClassDataSource<Data1>]
public required Data1 Data1 { get; init; }

public string Value { get; set; } = string.Empty;

public virtual Task InitializeAsync()
{
// This should be called after Data1 has been injected and initialized
Value = $"Data2 Initialized (Data1: {Data1?.Value ?? "NULL"})";
Console.WriteLine($"Data2 InitializeAsync called - Value: {Value}, Data1: {Data1?.Value ?? "NULL"}");
return Task.CompletedTask;
}
}

// Data3 inherits from Data2, so it should inherit the Data1 property with its ClassDataSource attribute
public class Data3 : Data2
{
public override Task InitializeAsync()
{
// This should be called after Data1 has been injected and initialized
Value = $"Data3 Initialized (Data1: {Data1?.Value ?? "NULL"})";
Console.WriteLine($"Data3 InitializeAsync called - Value: {Value}, Data1: {Data1?.Value ?? "NULL"}");
return Task.CompletedTask;
}
}

[EngineTest(ExpectedResult.Pass)]
public class InheritedDataSourceTests
{
[ClassDataSource<Data3>(Shared = SharedType.PerTestSession)]
public required Data3 Data3 { get; init; }

[Test]
public async Task Test_InheritedPropertyWithDataSource_ShouldBeInjected()
{
// The bug is that Data1 property (inherited from Data2) is not being injected
// when Data3 is used as a ClassDataSource

Console.WriteLine($"Test - Data3.Value: {Data3.Value}");
Console.WriteLine($"Test - Data3.Data1: {Data3.Data1}");
Console.WriteLine($"Test - Data3.Data1?.Value: {Data3.Data1?.Value}");

// This assertion should pass but currently fails with the bug
await Assert.That(Data3.Data1).IsNotNull();
await Assert.That(Data3.Data1.Value).IsEqualTo("Data1 Initialized");
await Assert.That(Data3.Value).Contains("Data1: Data1 Initialized");
}

[Test]
[ClassDataSource<Data2>]
public async Task Test_DirectDataSource_WorksCorrectly(Data2 data2)
{
// This test uses Data2 directly (not through inheritance) and should work
// The framework should inject Data1 into Data2 directly

// This should work because Data2's properties are defined directly on it
await Assert.That(data2.Data1).IsNotNull();
await Assert.That(data2.Data1.Value).IsEqualTo("Data1 Initialized");
await Assert.That(data2.Value).Contains("Data1: Data1 Initialized");
}
}

// Additional test case with multiple levels of inheritance
public class BaseDataWithSource
{
[ClassDataSource<Data1>]
public required Data1 BaseData1 { get; init; }
}

public class MiddleDataWithSource : BaseDataWithSource
{
[ClassDataSource<Data2>]
public required Data2 MiddleData2 { get; init; }
}

public class DerivedDataWithSource : MiddleDataWithSource
{
public string DerivedValue { get; set; } = "Derived";
}

[EngineTest(ExpectedResult.Pass)]
public class MultiLevelInheritanceTests
{
[ClassDataSource<DerivedDataWithSource>]
public required DerivedDataWithSource DerivedData { get; init; }

[Test]
public async Task Test_MultiLevelInheritance_AllDataSourcesShouldBeInjected()
{
// Both BaseData1 and MiddleData2 should be injected even though they're in base classes
await Assert.That(DerivedData.BaseData1).IsNotNull();
await Assert.That(DerivedData.BaseData1.Value).IsEqualTo("Data1 Initialized");

await Assert.That(DerivedData.MiddleData2).IsNotNull();
await Assert.That(DerivedData.MiddleData2.Data1).IsNotNull();
}
}
Loading