Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
33 changes: 33 additions & 0 deletions TUnit.Engine/Services/ObjectLifecycleService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,11 @@ private void SetCachedPropertiesOnInstance(object instance, TestContext testCont

if (cachedProperties.TryGetValue(cacheKey, out var cachedValue) && cachedValue != null)
{
// Convert the value if the runtime type doesn't match the property type.
// This handles implicit/explicit conversion operators at runtime when the
// source generator doesn't know the data source type (e.g., custom data sources).
cachedValue = ConvertPropertyValueIfNeeded(cachedValue, metadata.PropertyType);

// Set the cached value on the new instance
metadata.SetProperty(instance, cachedValue);
}
Expand All @@ -205,6 +210,9 @@ private void SetCachedPropertiesOnInstance(object instance, TestContext testCont

if (cachedProperties.TryGetValue(cacheKey, out var cachedValue) && cachedValue != null)
{
// Convert the value if needed (same as above)
cachedValue = ConvertPropertyValueIfNeeded(cachedValue, property.PropertyType);

// Set the cached value on the new instance
var setter = PropertySetterFactory.CreateSetter(property);
setter(instance, cachedValue);
Expand All @@ -213,6 +221,31 @@ private void SetCachedPropertiesOnInstance(object instance, TestContext testCont
}
}

/// <summary>
/// Converts a resolved property value to the target property type if needed.
/// This handles implicit/explicit conversion operators at runtime, which is necessary when:
/// - The source generator doesn't know the data source type (e.g., custom data sources)
/// - The data source yields a type that differs from the property type but has a conversion operator
/// </summary>
[UnconditionalSuppressMessage("AOT", "IL3050", Justification = "CastHelper handles AOT scenarios with proper fallbacks")]
[UnconditionalSuppressMessage("Trimming", "IL2067", Justification = "PropertyType is preserved through source generation or reflection discovery")]
private static object? ConvertPropertyValueIfNeeded(object? value, Type targetType)
{
if (value == null)
{
return null;
}

var valueType = value.GetType();
if (valueType.IsAssignableTo(targetType))
{
return value;
}

// Use CastHelper which supports implicit/explicit operators, IConvertible, etc.
return CastHelper.Cast(targetType, value);
}

/// <summary>
/// Initializes all tracked objects depth-first (deepest objects first).
/// This is called during test execution (after BeforeClass hooks) to initialize IAsyncInitializer objects.
Expand Down
37 changes: 37 additions & 0 deletions TUnit.Engine/Services/PropertyInjector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ private Task InjectSourceGeneratedPropertiesAsync(
}

[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Source-gen properties are AOT-safe")]
[UnconditionalSuppressMessage("Trimming", "IL2072", Justification = "PropertyType is preserved through source generation")]
[UnconditionalSuppressMessage("Trimming", "IL2075", Justification = "ContainingType is annotated with DynamicallyAccessedMembers in PropertyInjectionMetadata")]
private async Task InjectSourceGeneratedPropertyAsync(
object instance,
Expand Down Expand Up @@ -285,6 +286,11 @@ private async Task InjectSourceGeneratedPropertyAsync(
}
}

// Convert the value if the runtime type doesn't match the property type.
// This handles implicit/explicit conversion operators when the source generator
// doesn't know the data source type (e.g., custom data sources).
resolvedValue = ConvertPropertyValueIfNeeded(resolvedValue, metadata.PropertyType);

// Set the property value
metadata.SetProperty(instance, resolvedValue);

Expand All @@ -311,6 +317,7 @@ private Task InjectReflectionPropertiesAsync(
}

[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Reflection mode is not used in AOT")]
[UnconditionalSuppressMessage("Trimming", "IL2072", Justification = "PropertyType is preserved through reflection discovery")]
private async Task InjectReflectionPropertyAsync(
object instance,
PropertyInfo property,
Expand All @@ -334,6 +341,11 @@ private async Task InjectReflectionPropertyAsync(
return;
}

// Convert the value if the runtime type doesn't match the property type.
// This handles implicit/explicit conversion operators when the source generator
// doesn't know the data source type (e.g., custom data sources).
resolvedValue = ConvertPropertyValueIfNeeded(resolvedValue, property.PropertyType);

propertySetter(instance, resolvedValue);
}

Expand Down Expand Up @@ -625,6 +637,31 @@ private DataGeneratorMetadata CreateSourceGeneratedDataGeneratorMetadata(
context.ObjectBag);
}

/// <summary>
/// Converts a resolved property value to the target property type if needed.
/// This handles implicit/explicit conversion operators at runtime, which is necessary when:
/// - The source generator doesn't know the data source type (e.g., custom data sources)
/// - The data source yields a type that differs from the property type but has a conversion operator
/// </summary>
[UnconditionalSuppressMessage("AOT", "IL3050", Justification = "CastHelper handles AOT scenarios with proper fallbacks")]
[UnconditionalSuppressMessage("Trimming", "IL2067", Justification = "PropertyType is preserved through source generation or reflection discovery")]
private static object? ConvertPropertyValueIfNeeded(object? value, Type targetType)
{
if (value == null)
{
return null;
}

var valueType = value.GetType();
if (valueType.IsAssignableTo(targetType))
{
return value;
}

// Use CastHelper which supports implicit/explicit operators, IConvertible, etc.
return CastHelper.Cast(targetType, value);
}

/// <summary>
/// Gets a cached PropertyInfo for the given type and property name, avoiding repeated reflection calls.
/// </summary>
Expand Down
28 changes: 28 additions & 0 deletions TUnit.TestProject/ImplicitOperatorPropertyInjectionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using TUnit.TestProject.Attributes;

namespace TUnit.TestProject;

[EngineTest(ExpectedResult.Pass)]
public class ImplicitOperatorPropertyInjectionTests
{
[ClassDataSource<ImplicitDbFixture>]
public required ImplicitDbContext Db { get; init; }

[Test]
public async Task Injection_Works_With_Implicit_Operator()
{
await Assert.That(Db).IsNotNull();
await Assert.That(Db.ConnStr).IsEqualTo("test-conn");
}
}

public class ImplicitDbContext(string connStr)
{
public string ConnStr { get; } = connStr;
}

public class ImplicitDbFixture : IAsyncDisposable
{
public static implicit operator ImplicitDbContext(ImplicitDbFixture f) => new("test-conn");
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}
Loading
Loading