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
34 changes: 33 additions & 1 deletion TUnit.Core/Attributes/TestData/MethodDataSourceAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ public MethodDataSourceAttribute(
instance = await GetOrCreateInstanceAsync(dataGeneratorMetadata, targetType);
}

methodResult = methodInfo.Invoke(instance, Arguments);
methodResult = methodInfo.Invoke(instance, BuildInvokeArgs(methodInfo, Arguments));
}
else
{
Expand Down Expand Up @@ -324,6 +324,38 @@ public MethodDataSourceAttribute(
}
}

// MethodInfo.Invoke does not auto-fill optional parameters the way a C# call site does.
private static object?[] BuildInvokeArgs(MethodInfo methodInfo, object?[] suppliedArguments)
{
var parameters = methodInfo.GetParameters();
if (parameters.Length <= suppliedArguments.Length)
{
// Exact match passes through; surplus supplied args are left to Invoke to surface as a mismatch.
return suppliedArguments;
}

var args = new object?[parameters.Length];
Array.Copy(suppliedArguments, args, suppliedArguments.Length);
for (var i = suppliedArguments.Length; i < parameters.Length; i++)
{
var p = parameters[i];
if (p.HasDefaultValue)
{
args[i] = Type.Missing;
}
else if (p.ParameterType == typeof(CancellationToken))
{
args[i] = CancellationToken.None;
}
else
{
// Required param missing — fall back so Invoke surfaces the original mismatch.
return suppliedArguments;
}
}
return args;
}

private static Type[]? GetMemberTypes(IMemberMetadata[]? members)
{
if (members == null || members.Length == 0)
Expand Down
41 changes: 41 additions & 0 deletions TUnit.TestProject/Bugs/5879/Repro.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System.Runtime.CompilerServices;
using TUnit.TestProject.Attributes;

namespace TUnit.TestProject.Bugs._5879;

/// <summary>
/// Regression test for https://github.com/thomhurst/TUnit/issues/5879.
///
/// `[InheritsTests]` derived class extends a generic abstract base whose
/// `[MethodDataSource]` method has an optional `[EnumeratorCancellation] CancellationToken ct = default`
/// parameter. Source generation can't emit a `Factory` for this case, so the engine
/// falls back to <c>MethodDataSourceAttribute.GetDataRowsAsync</c>'s reflection
/// invoke path. <c>MethodInfo.Invoke</c> does not auto-fill defaults like a C# call
/// site does, so before the fix the call threw <c>TargetParameterCountException</c>.
/// </summary>
[EngineTest(ExpectedResult.Pass)]
[InheritsTests]
public class Issue5879Tests : Issue5879AbstractBase<string, string>
{
protected override (string, string) Value => ("test", "test");
}

public abstract class Issue5879AbstractBase<TInput, TExpected>
{
[Test]
[MethodDataSource(nameof(DataSource))]
public async ValueTask Reproduce((TInput, TExpected) value)
{
var (input, expected) = value;
await Assert.That(input).IsEquivalentTo(expected);
}

public async IAsyncEnumerable<(TInput, TExpected)> DataSource(
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
yield return Value;
await Task.CompletedTask;
}

protected abstract (TInput, TExpected) Value { get; }
}
Loading