Skip to content

Fix for issue5129#5130

Merged
OsirisTerje merged 13 commits intomainfrom
Issue5129
Mar 3, 2026
Merged

Fix for issue5129#5130
OsirisTerje merged 13 commits intomainfrom
Issue5129

Conversation

@OsirisTerje
Copy link
Copy Markdown
Member

Fixes #5129

@cursor
Copy link
Copy Markdown

cursor bot commented Feb 19, 2026

You have run out of free Bugbot PR reviews for this billing cycle. This will reset on March 31.

To receive reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

@stevenaw
Copy link
Copy Markdown
Member

Thanks @OsirisTerje
This looks ok at a glance but I'll try and take a closer look this evening

@OsirisTerje
Copy link
Copy Markdown
Member Author

I've simplified the code in the last two commits, moving the complex logic into extension methods, and added iunit tests for those. The code that was changed should now be more intention revealing.

Copy link
Copy Markdown
Member

@stevenaw stevenaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks again @OsirisTerje for the fix and for helping make some of this more readable.
I've left a few questions to check my own understandings for now. I'd appreciate if we could get another pair of eyes on this (perhaps @manfred-brands if they're free). I'll also try to take a second look tomorrow myself

public bool ShouldUnpackArrayAsArguments(Array array)
{
// A params parameter always absorbs the elements individually
if (parameters.LastParameterIsParamsArray())
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: If I'm reading this correctly, would this be handled correctly? I think I would expect expectedArgs to contain an array of [1, 2, 3] and that extra would have nothing

private static IEnumerable<Array> Testcases
    {
        get
        {
            yield return new[] { 1, 2, 3 };
        }
    }

public void MethodWhichReceivesArrayAndHasParams(Array expectedArgs, params int[] extra)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, in this case it looks wrong. (Funny how things become clearer when it gets a name :-)) .
I didn't change this logic though -- , but this is meant to handle signatures like:

public void MethodWhichReceivesIntAndHasParams(ìnt value, params int[] extra).

The signature with Array and params doesn't make sense, as you say, nothing will get into the extra.

Also, public void MethodWhichReceivesIntAndHasArray(ìnt value, Array extra) fails.

I added two tests to the repro for this case, the int case worked as it should, got one value, and the rest to the extra, but the Array + params crashed. It should have been caught in a check earlier imho. Or, we could handle it as a special case - if the earlier parameters contain a collection we simply ignore the last collection.

I believe we have to set up some "rules" on how this can be used, so that we don't have to cover all possible constructions here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @OsirisTerje It's good to hear that the logic was just moved and not changed.

Indeed, these have been hard to fully and properly test in the past. I can't speak to the original rules, though the general guideline we've taken in the past has been to keep it simple and to just try to ensure that we maintain consistent behaviour as if the args from the sources were passed to the test method directly via a regular invocation.

Though that of course is anything but simple :)

Ex:

private static IEnumerable<Array> Testcases
    {
        get
        {
            yield return new[] { 1, 2, 3 };
        }
    }

    [TestCaseSource(nameof(Testcases))]
    public void Foo(Array values)
    {
        Assert.That(values.Length, Is.EqualTo(3));
    }

    [TestCaseSource(nameof(Testcases))]
    public void Foo2(Array values, params int[] extra)
    {
        Assert.That(values.Length, Is.EqualTo(3));
    }

  // Should be marshaled the same as:
  Foo(Testcases.First())
  Foo2(Testcases.First())

I haven't tried yet to see how those actually behave

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test public void Foo2(Array values, params int[] extra) will crash. So you mean we should fix this, even if the code is meaningless ?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code isn't meaningless if the test data would be something with a nested array:
new object[] { new int[] { 1, 2 }, 3, 4 }

return false;

// Multiple parameters: each element maps to one parameter
if (argsNeeded > 1)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: I'm a little unclear about this one. I notice a comment in the previous implementation:

// If array has the same number of elements as parameters
// and it does not fit exactly into single existing parameter
// we believe that this array contains arguments

Would this short-circuit that check it's checked below this?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about what you mean here - believe I need some more context.

I read it as when I have an array (which is a prerequisite for this, and covers all collections), and I have more than one parameter we assume that is a set of individual parameters. That has to match exactly to the number of elements in the array. (If it doesn't, we get internal crashes on that too, so also something we should give a better error message on.)
I've debugged through the code, and it seems to me it works.

But I understand you see something here, so please elaborate :-)

PS: We don't really have a good spec of what TestCaseSource can do. The documentation we have show a few of the ways to use it, but everything regarding collections are absent. Perhaps we should do something about that.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I had a hard time mapping the logic in your new helper to the original code, at least when I was looking at it last night. I was concerned that a case like this, where argsNeeded is greater than 1 but not equal to the number of arguments could still cause the array to be unpacked even though the number of arguments in the test method differs from the number of elements in the array.

private static IEnumerable<Array> Testcases
{
    get
    {
        yield return new[] { 1, 2, 3 };
    }
}

[TestCaseSource(nameof(Testcases))]
public void Foo(int a, int b)
{
}

I admit the same disclaimer as my other comment. I haven't tried running the code yet to see how these behave.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PS: We don't really have a good spec of what TestCaseSource can do. The documentation we have show a few of the ways to use it, but everything regarding collections are absent. Perhaps we should do something about that.

This is a very good point. I have an issue for myself to add examples for optional, params, etc, but even after that I suspect there would still be gaps

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your example here will also crash. In these cases there has to be an exact match in number of elements and number of parameters. And the crashes are inside our code, so we 1) have to fix this - but then we have to be clear how we should handle overflow and underflow 2) we catch these earlier and provide good error message for it

Did this actually work earlier ?

Copy link
Copy Markdown
Member Author

@OsirisTerje OsirisTerje Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made a new repro, moved back to version 3.13, and noted:

For public void Foo(int a, int b)

  1. Too few parameters or too many parameters: Fails with message: Not enough arguments provided, provide at least 3 arguments.

For public void Foo(int value, params int[] extra)

  1. Fails in 3.13 with Not enough arguments provided, provide at least 2 arguments.

This one works directly in 5129 fix.

About 1: We should fix this to be equal to 3.13. I might have missed something in the latest refactoring here. I'll check this tomorrow, if we agree we should be equal to 3.13.

4.4.0 behaves just as 3.13

The image below show the results for all 3 versions we're talking about now:

image

Interesting to see that 4.5.0. and 5129 fix overflow/underflow is not even properly identified.

So with the fix, we only need to fix the overflow/underflow, and then at least make it back to 3.13-4.40 level.

One could argue that an overflow, we could ignore the overflow arguments, and for underflow we could assume the parameter overflow values have their default value. But for now, I think fixing the error messages is the way we go.

The overflow/underflow is not really errors either, they are failures in the test setup, so it should perhaps be Inconclusive ?

Copy link
Copy Markdown
Member

@stevenaw stevenaw Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for catching that!
And FWIW I would agree that any regressions from previous behaviour should be fixed. Bugs related to new features within 4.5 (such as anything specific to method signatures with params or optional args) should also be fixed but separately as I would suggest they are lower priority. They would also likely have workarounds in that a Test/Value/Fixture source could just be updated to pass those args explicitly where relevant.

@OsirisTerje
Copy link
Copy Markdown
Member Author

I have added the repro code, with a few extra tests, here: https://github.com/nunit/nunit.issues/tree/main/Issue5129

@OsirisTerje
Copy link
Copy Markdown
Member Author

@manfred-brands @stevenaw
Anything missing here now, or good to go merge?

@manfred-brands
Copy link
Copy Markdown
Member

@OsirisTerje I haven't had time to look at this in detail.
Your changes with the naming make it clearer, but it is hard to see what the actual fix was.

I will make some time tomorrow to look at this in detail.

@manfred-brands
Copy link
Copy Markdown
Member

@OsirisTerje I added a test which has an array inside the array:

        private static readonly object[] NestedArray =
        [
            new object[] { new int[] { 2, 3, 4 }, 2 + 3 + 4, 2 * 2 + 3 * 3 + 4 * 4 }
        ];

And the following two test signatures:

 public void TestWithArrayAndIndividualParameters(int[] values, int sum, int sumSquared)
public void TestWithArrayAndParamsArray(int[] values, params int[] sums)

Both pass!

However if I have a test with too many arguments:

public void TestWithArrayAndTooManyIndividualParameters(int[] values, int sum, int sumSquared, int sumCubed)

The behaviour is correct, just the error message could be improved:
It fails with Method requires 4 arguments but only 1 were supplied
Either the array wasn't unpacked or the error counts the pre-unpacking number of parameters.

However, if there are not enough arguments if fails with an IndexOutOfRangeException.

 public void TestWithArrayAndNotEnoughIndividualParameters(int[] values, int sum)

This is caused by a loop iterating over 2 arrays (Arguments and parameters) but testing one .Length.
It could be cause by the same error as above as this test is inside an argsProvided == argsNeeded where both are 2, even though Arguments.Length is 3

Ok, I found that issue.
Still looking for the first one.
Debugging is a crime as the code is used for all other tests somewhere.

I want to move these two tests to the testdata project so I can run them here and assert the error message.

@manfred-brands
Copy link
Copy Markdown
Member

One other test that fails:

[TestCase(new int[] { 7, 8 }, new int[] { 9, 10 })]
[TestCase(new int[] { 11, 12 })]
public void TestWithArrayOfArrays(params int[][] arrays)

The one with two elements passes, the one with one doesn't.

When calling it directly in C# they both pass:

TestWithArrayOfArrays(new int[] { 7, 8 }, new int[] { 9, 10 });
TestWithArrayOfArrays(new int[] { 11, 12 });

So some more work to be done.

I'll see if I can figure this out later today.

@OsirisTerje
Copy link
Copy Markdown
Member Author

OsirisTerje commented Feb 28, 2026

Thanks a ton @manfred-brands !

We should get rid of exceptions like IndexOutOfRangeException.

Copilot AI review requested due to automatic review settings March 2, 2026 05:44
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes #5129 by correcting how TestCaseSource interprets arrays returned from sources—deciding when an Array should be treated as the argument list vs. a single argument value (e.g., passing int[] into an Array parameter).

Changes:

  • Introduces new argument/parameter helper logic (ShouldUnpackArrayAsArguments, Unpack, etc.) and updates TestCaseSourceAttribute to use it.
  • Adjusts optional/params argument population and argument-count tracking to avoid internal crashes and improve NotRunnable behavior.
  • Adds/extends tests covering array parameters, IEnumerable/IList<T> targets, wrapped vs. unwrapped arrays, and under/over-supplied argument counts.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/NUnitFramework/framework/Internal/Extensions/ArgumentsExtensions.cs Adds Unpack and ShouldUnpackArrayAsArguments plus related helpers to centralize array unpacking decisions.
src/NUnitFramework/framework/Attributes/TestCaseSourceAttribute.cs Switches array handling to the new helper logic when building test cases from source items.
src/NUnitFramework/framework/Internal/TestCaseParameters.cs Fixes argsProvided tracking after optional/params adjustment by using the adjusted Arguments.Length.
src/NUnitFramework/framework/Internal/Reflect.cs Refines optional-argument handling and tightens when params arrays are considered “already provided”.
src/NUnitFramework/tests/Internal/Extensions/ArgumentExtensionsTest.cs Adds unit tests for Unpack, HasSingleParameterOfType, and ShouldUnpackArrayAsArguments.
src/NUnitFramework/tests/Attributes/TestCaseSourceTests.cs Adds integration tests for receiving arrays into Array/IEnumerable targets and for argument count mismatch messaging.
src/NUnitFramework/testdata/TestCaseSourceAttributeFixture.cs Adds fixture methods/source data for mismatch cases (too many/too few individual parameters).
src/NUnitFramework/tests/Attributes/TestCaseAttributeTests.cs Adds test ensuring params-of-arrays cases are runnable and succeed.
src/NUnitFramework/testdata/TestCaseAttributeFixture.cs Adds a params int[][] test method with multiple [TestCase] shapes (0/1/2 args).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +110 to +125
// Classic argument-container pattern: new object[] { actualArg }
// Except when the parameter is of type object — then the array IS the argument, not a container.
// Note that it is impossible to pass a single length object array to a method expecting an object array.
if (arrayType == typeof(object[]) && array.Length == 1 && paramType != typeof(object))
return true;

// If the parameter accepts this array type
// directly (e.g. Array, IList<T>, IEnumerable) then the array IS the argument.
// But only if we don't need more arguments.
// i.e. We expect one argument or one argument plus a params array.
if (paramType.IsAssignableFrom(arrayType) && (argsNeeded == 1 || argsNeeded == 2 && parameters.LastParameterIsParamsArray()))
return false;

// A params parameter always absorbs the elements individually
if (parameters.LastParameterIsParamsArray())
return true;
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ShouldUnpackArrayAsArguments currently treats any array whose runtime type is assignable to the first parameter type as a single argument when the method has exactly one parameter plus a params array. That breaks the common TestCaseSource pattern where an object[] represents the argument list, e.g. for void M(Array first, params int[] rest) with source new object[] { new int[]{1,2}, 3, 4 } the method would not unpack and first would receive the whole object[]. The previous logic always unpacked when a params parameter exists. Consider excluding object[] from the IsAssignableFrom short-circuit (treat object[] as the argument-container by default unless explicitly wrapped) so that object[] continues to represent multiple arguments for multi-parameter methods.

Copilot uses AI. Check for mistakes.
@manfred-brands
Copy link
Copy Markdown
Member

I made some changes:

  • Added a few more case and have the argument count issue solved. It was dealt with in two places and one wasn't needed.
  • Optimize one Unpack/Repack scenario. Not sure how often it would be used.
  • Don't unpack an array if it matches the only argument, even if it is a params array.
  • We cannot keep an array argument if it has the wrong type. Fixes issue mentioned above

But I initially broke one specific oddity (i.e. classic feature):

private static readonly object[] IgnoreWhiteSpaceData =
{
    new object[] { new SimpleObjectCollection("x", "y", "z", " z ") },
    new object[] { new[] { "a", "b", "c", " c " } }
};

[TestCaseSource(nameof(IgnoreWhiteSpaceData))]
public void HonorsIgnoreWhiteSpace(IEnumerable actual)

The test accept an IEnumerable and object[] is one, so it doesn't unpack the array.
However the test actually expects the first element in that array, matching the declaration below:

private static readonly object[] IgnoreWhiteSpaceData =
{
    new SimpleObjectCollection("x", "y", "z", " z "),
    new[] { "a", "b", "c", " c " }
};

This is an old test and I have restored the behaviour to unwrap an object[] array with 1 element.

private static readonly object[] OneElementWrapped =
[
    new object[] { 42 }
];
private static readonly object[] TwoElementsWrapped =
[
    new object[] { 40, 2 }
];

[TestCaseSource(nameof(OneElementWrapped))]
[TestCaseSource(nameof(TwoElementsWrapped))]
public void TestObjectArray(Array array)
{
    Assert.That(array, Is.Not.Null);
}

The first case failed with Object of type 'System.Int32' cannot be converted to type 'System.Array'. because it unwrapped the single integer, the second case succeeds as the 2 element array is not unwrapped.

I have confirmed that this is the same with NUnit 3.

Not sure how relevant it is as newer code would declare the arrays as new int[] which works for both cases.

…onsTest.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@stevenaw
Copy link
Copy Markdown
Member

stevenaw commented Mar 3, 2026

Thanks for the thorough analysis and descriptive summary @manfred-brands . This area of the codebase is challenging and it's good we're keeping all these behaviors while cleaning things up and adding further coverage and documentary discussion here.

It's been difficult to find a sufficient amount of focused time for me to do a thorough and accurate follow-up review on this but from your description it sounds like you've covered my earlier concerns.

@OsirisTerje Please don't wait on my review on this if you happen to get a chance to look first and are happy with @manfred-brands 's changes.

Copy link
Copy Markdown
Member

@stevenaw stevenaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @manfred-brands @OsirisTerje
I was able to get some time this morning. No concerns on my end but I'd appreciate one more review + approval from either of you just in case.

@OsirisTerje
Copy link
Copy Markdown
Member Author

I'll think we just merge this now. If we get more flak on this, we can rework a bit more.

@OsirisTerje OsirisTerje merged commit 743ac78 into main Mar 3, 2026
5 checks passed
@OsirisTerje OsirisTerje deleted the Issue5129 branch March 3, 2026 17:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Passing Array as test parameter fails with 4.5.0

4 participants