Skip to content

A tuple parameter followed by an optional parameter is spread instead of bound whole #7212

Closed

Description

@thomhurst

What happens

A row that supplies only a tuple is spread into its elements when the tuple parameter is followed by an optional parameter. The test then fails in both modes:

public static IEnumerable<object[]> Rows()
{
    yield return [(25, 26)];
}

[Test]
[MethodDataSource(nameof(Rows))]
public async Task Tuple_And_Optional((int, int) pair, int extra = 5)
{
    await Assert.That(pair).IsEqualTo((25, 26));
    await Assert.That(extra).IsEqualTo(5);
}
  • Source-gen: InvalidCastException: Unable to cast object of type 'System.Int32' to type 'System.ValueTuple2[System.Int32,System.Int32]'`
  • Reflection: ArgumentException: Object of type 'System.Int32' cannot be converted to type 'System.ValueTuple2[System.Int32,System.Int32]'`

With a typed source (IEnumerable<(int, int)>), TUnit0001 rejects the method at compile time ("Attribute argument types 'int, int' don't match method parameter types '(int, int), int'"). That is consistent with the runtime, but the object[] row above gets through the analyzer and fails at runtime.

Where

DataUnwrapper.UnwrapWithTypesInternal (src/TUnit.Engine/Helpers/DataUnwrapper.cs) keeps a tuple whole only when the method has exactly one data parameter. Here it has two (the tuple and the optional int), so the tuple is spread. A CancellationToken does not change this. I found it while checking edge cases for #7185 (#7173), and it reproduces on that branch with or without a token.

Expected

When a row's single value is already an instance of the first data parameter's tuple type, and the remaining parameters are optional (or params), bind the tuple whole and use the defaults for the rest, in both modes. Otherwise, document the "one tuple data parameter only" rule and have TUnit0001 or the runtime give a clear error instead of a cast exception.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Medium: edge case, mode divergence, flaky test, or workaround existsbugSomething isn't workingin-progressWork is in progress

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions