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
166 changes: 126 additions & 40 deletions src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions src/TUnit.Engine/Discovery/ReflectionTestDataCollector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2354,6 +2354,11 @@ private static (List<(MethodInfo Method, Type[] TypeArguments, IDataSourceAttrib
case GenericMethodInferencePolicy.Source.TypedDataSource when dataSource is not ArgumentsAttribute:
AddInstantiation(InferTypesFromTypedDataSource(dataSource, method, genericParameterCount, classGenericParameterCount), dataSource, source: rule.Source);
break;
// Instance sources use the explicit method types, even when a static source
// already inferred that instantiation. Join them below in declaration order.
case GenericMethodInferencePolicy.Source.MethodDataSource when dataSource is InstanceMethodDataSourceAttribute:
(skippedSources ??= []).Add(dataSource);
break;
case GenericMethodInferencePolicy.Source.MethodDataSource when dataSource is MethodDataSourceAttribute methodDataSource:
dataParameters ??= Array.FindAll(parameters, static parameter => parameter.ParameterType != typeof(CancellationToken));
AddInstantiation(InferTypesFromMethodDataSource(testClass, methodDataSource, dataParameters, genericParameterCount), methodDataSource, source: rule.Source);
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using TUnit.Core.SourceGenerator.Tests.Options;

namespace TUnit.Core.SourceGenerator.Tests;

internal class InstanceMethodTypeInferenceTests : TestsBase
{
[Test]
public Task Test() => RunTest(Path.Combine(Git.TestsDirectory.FullName,
"TUnit.TestProject", "Bugs", "_7222", "GenericInstanceSourceTests.cs"),
new RunTestOptions
{
AdditionalFiles = [Path.Combine(Git.TestsDirectory.FullName,
"TUnit.TestProject", "Bugs", "_7222", "InstanceSourceConstraintViolationTests.cs")]
},
async files =>
{
var source = string.Join("\n", files);
await Assert.That(source).Contains("Could not infer the type arguments of generic test method 'NullableElement'");
await Assert.That(string.Join("\n", files.Where(file =>
!file.Contains("ClosedClassUninferredInstanceSourceTests")
&& !file.Contains("ClosedClassRowUninferredInstanceSourceTests"))))
.DoesNotContain("Could not infer the type arguments");
await Assert.That(source).Contains("Type 'String' does not satisfy the 'struct' constraint for generic parameter 'T'");
await Assert.That(source).Contains("GenericInstanceSourceTests<int>");
await Assert.That(source).Contains("NonGenericInstanceSourceTests");
await Assert.That(source).Contains("BothSources<string>");
await Assert.That(source).Contains("Valid<int>");
await Assert.That(source).Contains("Valid<string>");
});
}
77 changes: 75 additions & 2 deletions tests/TUnit.Engine.Tests/Issue7222Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,12 +140,85 @@ await RunTestsWithFilter(
]);
}

[Test]
public async Task NonGeneric_Class_Instance_Source_Infers_Method_Types()
{
await RunTestsWithFilter(
"/*/TUnit.TestProject.Bugs._7222/NonGenericInstanceSourceTests/*",
[
result => result.ResultSummary.Counters.Total.ShouldBe(2),
result => result.ResultSummary.Counters.Passed.ShouldBe(2)
]);
}

[Test]
[Arguments("ExplicitClassInstanceSourcesTests*", 2)]
[Arguments("ExplicitClassMethodSourceCollisionTests*", 2)]
[Arguments("ClassRowMethodSourceCollisionTests*", 2)]
[Arguments("ExplicitMethodInstanceSourceTests", 1)]
[Arguments("ExplicitFactoryTupleInstanceCollisionTests", 4)]
[Arguments("ClosedClassParameterInferredSourceTests*", 1)]
[Arguments("ExplicitClassParameterInferredSourceTests*", 1)]
public async Task Explicit_And_Parameter_Inference_Preserve_Instance_Sources(string className, int count)
{
await RunTestsWithFilter(
$"/*/TUnit.TestProject.Bugs._7222/{className}/*",
[
result => result.ResultSummary.Counters.Total.ShouldBe(count),
result => result.ResultSummary.Counters.Passed.ShouldBe(count)
]);
}

[Test]
[Arguments("MixedClassRowSourcesTests*")]
[Arguments("MixedExplicitClassSourcesTests*")]
public async Task Mixed_Inferred_Sources_Preserve_Both_Declaration_Orders(string className)
{
await RunTestsWithFilter(
$"/*/TUnit.TestProject.Bugs._7222/{className}/*",
[
result => result.ResultSummary.Counters.Total.ShouldBe(4),
result => result.ResultSummary.Counters.Passed.ShouldBe(4),
result => result.Results.Count(x => x.TestName.StartsWith("InstanceFirst<string>", StringComparison.Ordinal)).ShouldBe(2),
result => result.Results.Count(x => x.TestName.StartsWith("StaticFirst<string>", StringComparison.Ordinal)).ShouldBe(2)
]);
}

[Test]
[Arguments("ClosedClassUninferredInstanceSourceTests*")]
[Arguments("ClosedClassRowUninferredInstanceSourceTests*")]
public async Task Closed_Class_Uninferred_Instance_Source_Is_Reported(string className)
{
await RunTestsWithFilter(
$"/*/TUnit.TestProject.Bugs._7222/{className}/*",
[
result => result.ResultSummary.Counters.Total.ShouldBe(2),
result => result.ResultSummary.Counters.Passed.ShouldBe(1),
result => result.ResultSummary.Counters.Failed.ShouldBe(1)
]);
}

[Test]
public async Task Generic_Class_Instance_Source_Infers_Method_Types()
{
await RunTestsWithFilter(
"/*/TUnit.TestProject.Bugs._7222/GenericInstanceSourceTests*/*",
[
result => result.ResultSummary.Counters.Total.ShouldBe(5),
result => result.ResultSummary.Counters.Passed.ShouldBe(3),
result => result.ResultSummary.Counters.Failed.ShouldBe(2),
result => result.Results.Where(x => x.Outcome == "Failed")
.ShouldAllBe(x => x.Output != null && x.Output.ErrorInfo != null
&& x.Output.ErrorInfo.Message != null && x.Output.ErrorInfo.Message.Contains(
"Type 'String' does not satisfy the 'struct' constraint for generic parameter 'T'"))
]);
}

[Test]
public async Task Failing_Instance_Source_Rows_Keep_The_Class_Constructor_Arguments()
{
// Each row of the instance data source fails on its own. Without the class data sources, source-generated
// mode could not build the instance that the rows come from and reported one constructor error instead.
// The message differs by mode: source-generated mode cannot infer the type from [InstanceMethodDataSource].
await RunTestsWithFilter(
"/*/TUnit.TestProject.Bugs._7222/InstanceSourceConstraintViolationTests/*",
[
Expand All @@ -154,7 +227,7 @@ await RunTestsWithFilter(
result => result.Results.ShouldAllBe(x => x.Output != null
&& x.Output.ErrorInfo != null
&& x.Output.ErrorInfo.Message != null
&& !x.Output.ErrorInfo.Message.Contains("Index was outside the bounds of the array"))
&& x.Output.ErrorInfo.Message.Contains("Type 'String' does not satisfy the 'struct' constraint for generic parameter 'T'"))
]);
}

Expand Down
175 changes: 175 additions & 0 deletions tests/TUnit.TestProject/Bugs/_7222/GenericInstanceSourceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
namespace TUnit.TestProject.Bugs._7222;

[Arguments<int>(1)]
public class GenericInstanceSourceTests<TClass>(TClass value)
{
public IEnumerable<string> Rows()
{
yield return value!.ToString()!;
yield return "b";
}

[Test]
[InstanceMethodDataSource(nameof(Rows))]
[InstanceMethodDataSource(nameof(MoreRows))]
public void Valid<T>(T item) where T : class
{
}

public IEnumerable<string> MoreRows() => ["extra"];

// Intentional failures: checked in both modes by Issue7222Tests.
[Test]
[InstanceMethodDataSource(nameof(Rows))]
public void StructOnly<T>(T item) where T : struct
{
}
}

[Arguments(1)]
public class NonGenericInstanceSourceTests(int value)
{
public IEnumerable<int> Rows() => [value, 2];

[Test]
[InstanceMethodDataSource(nameof(Rows))]
public void Valid<T>(T item) where T : struct
{
}
}

[GenerateGenericTest(typeof(int))]
public class ExplicitClassInstanceSourcesTests<TClass>
{
public IEnumerable<string> Rows() => ["first"];
public IEnumerable<string> MoreRows() => ["second"];

[Test]
[InstanceMethodDataSource(nameof(Rows))]
[InstanceMethodDataSource(nameof(MoreRows))]
public void BothSources<T>(T item) where T : class
{
if (item is not string) throw new InvalidOperationException("Expected string");
}
}

[Arguments<int>(1)]
public class MixedClassRowSourcesTests<TClass>(TClass value)
{
public IEnumerable<string> InstanceRows() => [value!.ToString()!];
public static IEnumerable<string> StaticRows() => ["static"];

[Test]
[InstanceMethodDataSource(nameof(InstanceRows))]
[MethodDataSource(nameof(StaticRows))]
public void InstanceFirst<T>(T item) where T : class
{
}

[Test]
[MethodDataSource(nameof(StaticRows))]
[InstanceMethodDataSource(nameof(InstanceRows))]
public void StaticFirst<T>(T item) where T : class
{
}
}

[GenerateGenericTest(typeof(int))]
public class MixedExplicitClassSourcesTests<TClass>
{
public IEnumerable<string> InstanceRows() => ["instance"];
public static IEnumerable<string> StaticRows() => ["static"];

[Test]
[InstanceMethodDataSource(nameof(InstanceRows))]
[MethodDataSource(nameof(StaticRows))]
public void InstanceFirst<T>(T item) where T : class
{
}

[Test]
[MethodDataSource(nameof(StaticRows))]
[InstanceMethodDataSource(nameof(InstanceRows))]
public void StaticFirst<T>(T item) where T : class
{
}
}

public class ExplicitMethodInstanceSourceTests
{
public IEnumerable<int> Rows() => [1];
[Test]
[GenerateGenericTest(typeof(long))]
[InstanceMethodDataSource(nameof(Rows))]
public void Explicit<T>(T item)
{
if (typeof(T) != typeof(long)) throw new InvalidOperationException("Expected explicit long");
}
}

// The analyzer cannot close TClass before comparing the static row with T?.
#pragma warning disable TUnit0001
[GenerateGenericTest(typeof(int))]
public class ExplicitClassMethodSourceCollisionTests<TClass>
{
public static IEnumerable<TClass> StaticRows() => [default!];
public IEnumerable<int> InstanceRows() => [1];

[Test]
[GenerateGenericTest(typeof(int))]
[MethodDataSource(nameof(StaticRows))]
[InstanceMethodDataSource(nameof(InstanceRows))]
public void BothSources<T>(T? item) where T : struct
{
if (typeof(T) != typeof(int)) throw new InvalidOperationException("Expected explicit int");
}
}

[Arguments<int>(1)]
public class ClassRowMethodSourceCollisionTests<TClass>(TClass value)
{
public static IEnumerable<TClass> StaticRows() => [default!];
public IEnumerable<int> InstanceRows()
{
_ = value;
return [1];
}

[Test]
[GenerateGenericTest(typeof(int))]
[MethodDataSource(nameof(StaticRows))]
[InstanceMethodDataSource(nameof(InstanceRows))]
public void BothSources<T>(T? item) where T : struct
{
if (typeof(T) != typeof(int)) throw new InvalidOperationException("Expected explicit int");
}
}

#pragma warning restore TUnit0001

// Factory tuples exercise the static inference path in reflection discovery too.
public class ExplicitFactoryTupleInstanceCollisionTests
{
public static IEnumerable<Func<(int, string)>> StaticRows() => [() => (1, "static")];
public IEnumerable<Func<(int, string)>> InstanceRows() => [() => (2, "instance")];

[Test]
[GenerateGenericTest(typeof(int))]
[MethodDataSource(nameof(StaticRows))]
[InstanceMethodDataSource(nameof(InstanceRows))]
public void StaticFirst<T>(T item, string label)
{
if (typeof(T) != typeof(int)) throw new InvalidOperationException("Expected explicit int");
if (label != (Equals(item, 1) ? "static" : "instance")) throw new InvalidOperationException("Unexpected row");
}

[Test]
[GenerateGenericTest(typeof(int))]
[InstanceMethodDataSource(nameof(InstanceRows))]
[MethodDataSource(nameof(StaticRows))]
public void InstanceFirst<T>(T item, string label)
{
if (typeof(T) != typeof(int)) throw new InvalidOperationException("Expected explicit int");
if (label != (Equals(item, 1) ? "static" : "instance")) throw new InvalidOperationException("Unexpected row");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,54 @@ public void StructOnly<T>(T item) where T : struct
{
}
}

// The int class closes T? successfully; the string class must report its rows rather than disappear.
[GenerateGenericTest(typeof(int))]
[GenerateGenericTest(typeof(string))]
public class ClosedClassUninferredInstanceSourceTests<TClass>
{
public IEnumerable<TClass> Rows() => [default!];

[Test]
[InstanceMethodDataSource(nameof(Rows))]
public void NullableElement<T>(T? row) where T : struct { }
}

[Arguments(1)]
[Arguments("value")]
public class ClosedClassRowUninferredInstanceSourceTests<TClass>(TClass value)
{
public IEnumerable<TClass> Rows() => [value];

[Test]
[InstanceMethodDataSource(nameof(Rows))]
public void NullableElement<T>(T? row) where T : struct { }
}

// Parameter inference already closes the method and runs the weakly typed source.
[Arguments(1)]
public class ClosedClassParameterInferredSourceTests<TClass>(TClass value)
{
public IEnumerable<object> Rows() => [1];

[Test]
[InstanceMethodDataSource(nameof(Rows))]
public async Task ParameterInferred<T>([Matrix<int>(1)] T item)
{
await Assert.That(item).IsEqualTo((T)(object)1);
await Assert.That(value is int).IsTrue();
}
}

[GenerateGenericTest(typeof(int))]
public class ExplicitClassParameterInferredSourceTests<TClass>
{
public IEnumerable<object> Rows() => [1];

[Test]
[InstanceMethodDataSource(nameof(Rows))]
public async Task ParameterInferred<T>([Matrix<int>(1)] T item)
{
await Assert.That(item).IsEqualTo((T)(object)1);
}
}
Loading