Skip to content

Second Proposed Solution for Issue 4998 - #5003

Merged
OsirisTerje merged 10 commits into
nunit:mainfrom
LeQuackers:main
Jul 14, 2025
Merged

OsirisTerje merged 10 commits into
nunit:mainfrom
LeQuackers:main

Conversation

@LeQuackers

Copy link
Copy Markdown
Contributor

This proposal is similar to the first one; however, it's a bit cleaner and is applied to more constraints.

As of writing this, some constraints cast the actual value to an object, which ends up causing inconsistencies / unexpected behavior when interacting with other constraints, in particular DefaultConstraint. DefaultConstraint was designed to match BCL behavior, so modifying it has caused a lot of discussion. Although it's a roundabout way of doing it, this proposal maintains the DefaultConstraint's behavior and resolves #4998, so it seems like a reasonable compromise.

As of writing this the following constraints have been modified:

  • AllItemsConstraint
  • ExactCountConstraint
  • NoItemConstraint
  • PropertyConstraint
  • SomeItemsConstraint

If I missed any, let me know and I'll get them in. Feel free to edit as needed.

Modified `AllItemsConstraint`, `ExactCountConstraint`, `NoItemConstraint`, `SomeItemsConstraint` and `PropertyConstraint` to invoke base constraints with the actual value's type instead of object
Added `ConstraintCastingTests`
@LeQuackers

Copy link
Copy Markdown
Contributor Author

@manfred-brands @mikkelbu @stevenaw @Dreamescaper
This is one of the ideas from the comment the other day; when you have time let me know what you think.

@manfred-brands

Copy link
Copy Markdown
Member

@LeQuackers Can you fix the build please?
Check all warnings in Visual Studio as they become errors in CI

LeQuackers added 2 commits June 26, 2025 19:29
Slight changes to `Repect.BuildApplyToDelegate(Type)`
Fixed issue where nullable value types would fail in the unit tests because the nullable was cast to either its underlying type or null instead of itself
Added `TypeHelper.GetNullableValueTypesFromDeclaredEnumerableInterfaces(Type)`
@LeQuackers

Copy link
Copy Markdown
Contributor Author

@manfred-brands
The warnings should be resolved. The IEnumerable in the collection constraints can return null items, but in some of them the item was cast to just object instead of object?. The nullable changes ended up flowing down to EachItemConstraintResult and ExactCountConstraintResult, which are a bit out of scope.

There was an unexpected issue with nullable value types; they're boxed to either their underlying value or null instead of itself (docs). That ended up adding more complexity than I'd like, but at the moment the unit tests are passing. Any ideas are welcome.

@LeQuackers

LeQuackers commented Jun 27, 2025 •

Copy link
Copy Markdown
Contributor Author

After thinking about it a little more, there could probably be some kind of inner
ConstraintResult ApplyToEnumerable<TActual, TItem>(TActual actual, IEnumerable<TItem>) method for the given collection constraint that does the actual iteration. If TActual has a single/"primary" IEnumerable<T> interface defined on it, then we could invoke it at runtime with something similar to Reflect.InvokeApplyTo; if not or if it's just an IEnumerable, we could call it normally with object? as the TItem type argument.

I think that'd clean up the nullable value type issue; I'll experiment with it tomorrow.

Added `Reflect.InvokeApplyToCollection` method
Added `TypeHelper.FindPrimaryEnumerableInterface` method
Modified collection constraints to use `ICollectionConstraint`
@LeQuackers

Copy link
Copy Markdown
Contributor Author

I implemented the idea; let me know if you have any questions/comments/concerns.

LeQuackers added 3 commits June 27, 2025 23:02
@LeQuackers

Copy link
Copy Markdown
Contributor Author

@manfred-brands
That should be the final simplification/cleanup; feel free to review the changes when you have time. let me know if you have any questions/comments/concerns.

@manfred-brands
manfred-brands self-requested a review June 28, 2025 07:11

@manfred-brands manfred-brands left a comment

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 added one commit addressing the naming issue: collection => enumerable
and simplifying the code a bit knowing that non of the parameters can be null.

Comment thread src/NUnitFramework/framework/Constraints/AllItemsConstraint.cs
Comment thread src/NUnitFramework/framework/Constraints/ICollectionConstraint.cs Outdated
Comment thread src/NUnitFramework/framework/Constraints/ICollectionConstraint.cs Outdated
Comment thread src/NUnitFramework/framework/Internal/Reflect.cs Outdated
Comment thread src/NUnitFramework/framework/Internal/Reflect.cs Outdated
Comment thread src/NUnitFramework/framework/Internal/Reflect.cs
Comment thread src/NUnitFramework/framework/Internal/Reflect.cs Outdated
@OsirisTerje

Copy link
Copy Markdown
Member

Is this WIP or ready to be merged?

@manfred-brands

Copy link
Copy Markdown
Member

Is this WIP or ready to be merged?

It is ready, but I wouldn't mind a 2nd review.

Comment thread src/NUnitFramework/framework/Internal/TypeHelper.cs
Comment thread src/NUnitFramework/framework/Internal/TypeHelper.cs
Comment thread src/NUnitFramework/tests/Constraints/ConstraintCastingTests.cs
@OsirisTerje
OsirisTerje merged commit 5f561d8 into nunit:main Jul 14, 2025
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.

PropertyConstraint Causes Unexpected Behavior Because the Property's Value Is object Instead of the Property’s Actual Type

3 participants