New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Correctness of Assert.Contain Assert.DoesNotContain for collections #2872
Comments
A bit of reproing later, the perf issue seems to be fixed in xunit.assert 2.5.0 |
The KeyCollection doesn't use dictionary's IComparer does seem to be a real phenomenon.
|
Seems that there's also a more general phenomenon of not using custom
Unhandled exception. System.NotImplementedException: The method or operation is not implemented. |
I should probably split this into two issues, but then one would just be closed as fixed. |
This was a lot for me to read, so I'm giving you a lot to read in return. 😂
It's irrelevant for us now, as you'll see below. It may still be a framework bug, though whether it would get fixed or not is unclear to me (since it's likely been that way for a looooooong time).
This is true, and I think the issue lies with
This behavior was definitely changed in 2.5.0. The usage of As noted, in 2.4.2 we used the dictionary's Keys collection and did a linear search across it, which doesn't have great performance, whereas we directly call It's also interesting to note that moving to .NET 6 and later dramatically increases the overload count (from 13 to 44). A good chunk of that is adding support for You'll also note that we don't offer overloads for sets and dictionaries which allow you to pass custom comparers, and that's because these containers generally require custom comparers to be passed to them during construction since Equals and GetHashCode must be aligned in order for the containers to function properly. I recently wrote a page about the differences between equality with sets vs. linear containers; it equally applies to searches with
AI was right (for < 2.5.0) and wrong (for >= 2.5.0). Does it stop suggesting that it was a performance problem if you upgrade to 2.5.0 or later? If so that would be exceptionally impressive, but I'm wagering that it's just going to throw the recommendation regardless. In 2.5.0, we do use That said, I did notice a bit of what I think is oversight on my part. The implementation of the generic public static void DoesNotContain<T>(
T expected,
IEnumerable<T> collection)
{
GuardArgumentNotNull(nameof(collection), collection);
// We special case HashSet<T> because it has a custom Contains implementation that is based on the comparer
// passed into their constructors, which we don't have access to.
var hashSet = collection as HashSet<T>;
if (hashSet != null)
DoesNotContain(expected, hashSet);
else
DoesNotContain(expected, collection, GetEqualityComparer<T>());
} There are a couple potential issues here:
It seems like an oversight on my part. I'm going to mull over if/how this should be fixed. If you have thoughts, please share them. It's also worth noting that if you call the |
Or We could just assume any And then I guess the way to check two containers are equal would logically be to check if they |
Yeah, it seems reasonable that any set (or dictionary) needs a custom comparer during construction and not just during search operations. And now I'm wondering whether a Contains() search against |
This was a dumb question. 😂 I had assumed |
The decompiler in Visual Studio says this is the code that is backing LINQ's public static bool Contains<T>(this IEnumerable<T> source, T value, IEqualityComparer<T> comparer)
{
if (comparer == null)
comparer = EqualityComparer<T>.Default;
if (source == null)
throw Error.ArgumentNull("source");
foreach (TSource item in source)
if (comparer.Equals(item, value))
return true;
return false;
} That's what I thought it probably did, and I think justifies our implementation of Now I just think I want to be a little more lenient than just hard-coding a check for |
This is really a signature thing. The dictionary support is dependent on signature mapping a dictionary of TKey/TValue and accepting just a TKey key value. The generic IEnumerable for a dictionary isn't In general we don't support the non-generic collections for things like this (as they're antiquated), so the only thing to be done here is support sets via interface rather than concrete. |
(Yes, these replies seem like stream of consciousness because I was poking around writing potential tests and implementations only to realize I should usually think more before posting 😂) |
The update to support Available in v2: |
…a6..574aebac4 574aebac4 xunit/xunit#2872: Expand special handling for sets in Assert.Contains/DoesNotContain 3b8edcbf1 xunit/xunit#2880: Update XML documentation for string-based Assert.Equal d9f8361d2 Consolidate string and span-based Assert.Equal primary implementation 9ad71163e Move span-of-char assertions to StringAsserts and update docs/param names to indicate they're treated like strings d70b34621 xunit/xunit#2871: Inner exception stack trace is missing from Assert.Collection failure 141681779 Missed #nullable enable in AsyncCollectionAsserts 22c89b0ea xunit/xunit#2367: Add IAsyncEnumerable<> overloads (.NET Core 3.0+) d5c32630a While formatting type names in Assert.Equal/NotEqual, convert generated type names to '<generated>' d7b807179 Add platform conditionals to support .NET 6 Roslyn Analyzers 6d9024665 xunit/xunit#2850: Assert.Equal failing value-slot collections of different concrete types (also fixed for KeyValuePair keys and values) 1f66b837a xunit/xunit#2811: Add SortedSet and ImmutableSortedSet overloads for Assert.Contains/DoesNotContain 1dab747d3 Update FuncEqualityComparer to throw if GetHashCode is called, and update EqualException/NotEqualException to process it 6e0a7cd70 xunit/xunit#2828: Prefer IEquatable<> over custom collection equality c35ef46d5 xunit/xunit#2824: Assert.Equal fails with null values in dictionary 455865ac8 xunit/xunit#2821: Assert.Equal for collections of IEquatable objects don't call custom Equals 9af2c9c12 Clarify names for range comparer 2e6d9b267 Updates for .NET 8 SDK git-subtree-dir: src/Microsoft.DotNet.XUnitAssert/src git-subtree-split: 574aebac41dbbbf9e5e98bb9c65c6c5fab9b47f5
Hopefully this issue might just a historical one already fixed? But anyway, here is the issue...
I'm using xunit 2.4.1, it looks like, so I'll have to check if this really repros with newer versions...
I got an interesting suggestion from an AI.
"The proposed code change can be improved by avoiding the use of Assert.DoesNotContain method to check if a key already exists in the dictionary. This method is not efficient because it iterates over the entire dictionary to check if the key exists. Instead, you can use the ContainsKey method of the dictionary which is more efficient because it uses hashing to check if the key exists."
My test is something like
Which could possibly call
right?
I doubted this suggestion at first. Because it looks like DoesNotContain tries to optimize for this kind of thing!:
But when I applied the AI's suggestion, lo and behold, it runs a lot faster!
So apparently this optimization, if that is what it is, doesn't work for dictionaries. I wonder why...
I think the problem might be that even when the collection implements a
Contains()
method, the optimization only applies to the NEGATIVE case, i.e. when the assertion must fails, because the collection contains the element.If the collection doesn't contain the element, then it should also be able to do a fast-path return, assuming that it implements
.Contains
in the expected fashion - but instead we'll continue enumerating the entire collection with the equality comparer, won't we.I tried to see if the problem might also be something that applies to the current code, but I'm a little confused about it since the implementation seems to have changed.
Now there's an implementation which special cases HashSet...
However I'm a little bit suspicious it will have similar problems, plus not fix the bug that was fixed for consistency with HashSet.Contains.
I mean, it seems like Dictionary key collection will also need some special casing for the same reasons as HashSet even though it doesn't inherit HashSet..., and passing the dictionary key collection to .DoesNotContain would end up treating it as a general collection and using the default equality comparer
https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.keycollection.contains?view=net-8.0
The text was updated successfully, but these errors were encountered: