Skip to content
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

Compiling xunit.assert with XUNIT_SPAN define results in ambiguous calls #2793

Closed
ViktorHofer opened this issue Oct 12, 2023 · 2 comments
Closed

Comments

@ViktorHofer
Copy link
Contributor

ViktorHofer commented Oct 12, 2023

When building xunit.assert.csproj (from the 2.5.1 tag with the TFM upgraded to net6.0) with the XUNIT_SPAN define added and referencing that built assembly in my test application (using xunit.core/2.5.1), some Assert method invocations result in ambiguous calls:

CS0121 The call is ambiguous between the following methods or properties: 'Assert.Equal(IEnumerable?, IEnumerable?)' and 'Assert.Equal(Span, ReadOnlySpan)'

Repro

TestProject1.zip -> I included the compiled xunit.assert assembly and reference it from the project file.
dotnet build the TestProject1 project

cc @bradwilson

@bradwilson
Copy link
Member

This is expected behavior, and I can repro this with your test against xUnit.net v3.

Assert.Equal uses generics liberally. When you have two types which are not exactly the same (as you do here), the compiler is forced to try to guess your intention and slot you into the right call. This isn't always possible for the compiler to do unambiguously, and as we add more assertions, it gives more opportunities for previously "correct guesses" to become ambiguous. Without XUNIT_SPAN, the only thing you could match was IEnumerable<char> vs. IEnumerable<char>, but now you can also match spans (since strings are implicitly span-able, as are arrays).

I should point out that there's no way for us to disambiguate this with extra signatures; as soon as we add support for spans, you're out of luck with the compiler.

You have at least three resolutions, and which you choose depends on what your goal is.

  1. If you want them to be compared as collections, then write:

    Assert.Equal(charactersToWrite, receivedLine.ToCharArray());

    You will get a message like this:

    Assert.Equal() Failure: Collections differ
                              ↓ (pos 3)
    Expected: ['a', 'b', 'c', 'd']
    Actual:   ['a', 'b', 'c']
    

    (As it turns out, this calls neither the IEnumerable<char> nor Span<char> overloads, but instead calls the unmanaged T[] version instead, which has much better performance than the IEnumerable<T> version. That's maybe not relevant here, but I thought I'd mention it.)

  2. If you want them compared as strings, then write:

    Assert.Equal(charactersToWrite, receivedLine.AsSpan());

    You will get a message like this:

    Assert.Equal() Failure: Strings differ
                  ↓ (pos 3)
    Expected: "abcd"
    Actual:   "abc"
    

    This is because .NET made string implicitly convertible to ReadOnlySpan<char>, so we treat spans of char like strings. The string assertions (when XUNIT_SPAN is defined) just forward on to the span versions.

  3. If you want equivalence rather than equality, then write:

    Assert.Equivalent(charactersToWrite, receivedLine);

    You will get a message like this:

    Assert.Equivalent() Failure: Collection value not found
    Expected: 'd'
    In:       ['a', 'b', 'c']
    

    This probably isn't what you want, because collection equivalence is done without regard to order. It says "Does the actual container contain at least the items in the expected container?". This test would actually pass (because order is irrelevant):

    char[] charactersToWrite = new char[] { 'a', 'b', 'c', 'd' };
    string receivedLine = "dcba";
    
    Assert.Equivalent(charactersToWrite, receivedLine);

    As would this (because of the "at least" in the above statement):

    char[] charactersToWrite = new char[] { 'a', 'b', 'c', 'd' };
    string receivedLine = "abcde";
    
    Assert.Equivalent(charactersToWrite, receivedLine);

    There is a strict flag that you can use that then modifies the behavior to change "at least" to "only", so this test would fail:

    char[] charactersToWrite = new char[] { 'a', 'b', 'c', 'd' };
    string receivedLine = "abcde";
    
    Assert.Equivalent(charactersToWrite, receivedLine, strict: true);

    with this message:

    Assert.Equivalent() Failure: Extra values found
    Expected: ['a', 'b', 'c', 'd']
    Actual:   ['e'] left over from ['a', 'b', 'c', 'd', 'e']
    

    I added this option to the list, not because I thought this was what you want, but rather because the next person who runs across this issue may want it. 😄

@ViktorHofer
Copy link
Contributor Author

ViktorHofer commented Oct 25, 2023

Thank you for that detailed response. The errors now make more sense to me. Feel free to close.

@bradwilson bradwilson closed this as not planned Won't fix, can't repro, duplicate, stale Oct 25, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants