Conversation
|
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. |
|
Thanks @OsirisTerje |
|
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. |
stevenaw
left a comment
There was a problem hiding this comment.
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()) |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
I made a new repro, moved back to version 3.13, and noted:
For public void Foo(int a, int b)
- 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)
- 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:
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 ?
There was a problem hiding this comment.
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.
|
I have added the repro code, with a few extra tests, here: https://github.com/nunit/nunit.issues/tree/main/Issue5129 |
|
@manfred-brands @stevenaw |
|
@OsirisTerje I haven't had time to look at this in detail. I will make some time tomorrow to look at this in detail. |
|
@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: However, if there are not enough arguments if fails with an public void TestWithArrayAndNotEnoughIndividualParameters(int[] values, int sum)This is caused by a loop iterating over 2 arrays (Arguments and parameters) but testing one Ok, I found that issue. I want to move these two tests to the testdata project so I can run them here and assert the error message. |
|
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. |
|
Thanks a ton @manfred-brands ! We should get rid of exceptions like IndexOutOfRangeException. |
The higher code will check the actual vs declared parameter count.
Don't Unpack and then Repack.
… if it matches the parameter type.
There was a problem hiding this comment.
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 updatesTestCaseSourceAttributeto use it. - Adjusts optional/
paramsargument 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.
| // 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; |
There was a problem hiding this comment.
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.
src/NUnitFramework/tests/Internal/Extensions/ArgumentExtensionsTest.cs
Outdated
Show resolved
Hide resolved
|
I made some changes:
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 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 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 |
…onsTest.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
|
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. |
stevenaw
left a comment
There was a problem hiding this comment.
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.
|
I'll think we just merge this now. If we get more flak on this, we can rework a bit more. |
Fixes #5129