-
-
Notifications
You must be signed in to change notification settings - Fork 379
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
ShouldBe does a sequence comparison for ReadOnlyMemory #610
Conversation
|
||
public static bool IsMemory(this Type type, out Type elementType) | ||
{ | ||
if (type.IsGenericType() && type.GetGenericTypeDefinition()?.FullName == "System.Memory`1") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (type.IsGenericType() && type.GetGenericTypeDefinition()?.FullName == "System.Memory`1") | |
if (type.IsGenericType() && type.GetGenericTypeDefinition().FullName == "System.Memory`1") |
A couple of questions.
|
There's an extension method that we could have used but it's only available in .NET Core and .NET Standard. As shouldly targets .NET Framework, I don't think there's another way. The code blocks were added by @jnm2 as it shows in #562 Perhaps he will have more info. |
Shouldly used to build for .NET Framework 4.0 which meant it couldn't reference https://www.nuget.org/packages/System.Memory in its net40 compile output. Reflection seemed easy and simpler than splitting Shouldly's behavior. (Remember, net40 builds can run on net48 and even netcoreapp3.1.) Since then, Shouldly has dropped support for net40. That means you can add a package reference rather than reflection. |
As This is what I found. |
I talked about explicit check for |
Ah, and I forgot generics just now, quite right. @sungam3r It would be indistinguishable to first check for IEnumerable, then fall back to using reflection to check for Memory/ReadOnlyMemory. Those are the essential checks. Before we add additional checks on top of that, we need hard numbers on which scenarios it speeds up and which it slows down. |
I think that all scenarios will get a slowdown due to the fact that |
@sungam3r With regard to that, this PR doesn't change anything. It's good to keep changes small and focused. Because of how much more complex it makes the code to use generic IEnumerable, I'd also prefer to see hard measurements of real-world projects demonstrating the problem so that we know we're doing something meaningful before starting a PR to avoid boxing via |
Also, if we were to go the direction of avoiding boxing, it would be both simpler and more general to first have a stage where a non-generic IEnumerable is obtained and then a second stage where generic IEnumerable is detected with a specialized iteration implementation that we invoke using MakeGenericMethod. That way not just |
I understand all this and do not insist. |
@sungam3r I appreciate that. Thanks for your input. Would you be interested in starting a new issue and doing a benchmark to help us understand how big of an effect we're talking about, or do you want to wait and see if someone starts noticing some pain over it? |
No, I don’t think it matters much. |
It doesn't look like any more changes are expected in this pr. Can I get the approval then @sungam3r ? |
should be | ||
System.ReadOnlyMemory<Byte>[2] | ||
but was | ||
System.ReadOnlyMemory<Byte>[3] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Normally the elements are listed in the error message. Could you see what it would take to enumerate here too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The method that generates the error method takes in the original and expected objects and passes it to this method -
internal static void AssertAwesomely<T>( |
We can convert actual and expected Memory
items into Enumerables in
ShouldBe()
and work with Enumerables- this line while constructing
ExpectedActualShouldlyMessage
throw new ShouldAssertException(new ExpectedActualShouldlyMessage(originalExpected, originalActual, customMessage, shouldlyMethod).ToString());
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, that helps. It looks like those values are used here:
shouldly/src/Shouldly/MessageGenerators/ShouldBeMessageGenerator.cs
Lines 20 to 21 in 5fc845c
var expected = context.Expected.ToStringAwesomely(); | |
var actualValue = context.Actual.ToStringAwesomely(); |
And the string is built here:
shouldly/src/Shouldly/Internals/StringHelpers.cs
Lines 42 to 51 in 5fc845c
if (value is IEnumerable) | |
{ | |
var objects = value.As<IEnumerable>().Cast<object>(); | |
var inspect = "[" + objects.Select(o => o.ToStringAwesomely()).CommaDelimited() + "]"; | |
if (inspect == "[]" && value.ToString() != type.FullName) | |
{ | |
inspect += " (" + value + ")"; | |
} | |
return inspect; | |
} |
I'm thinking we should check for the memory types and convert to IEnumerable in ToStringAwesomely for consistency. It sometimes uses the type full name and ToString of the original instance, and we wouldn't want to use the instance returned from MemoryMarshal.ToEnumerable for that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pushed a change
private static IEnumerable ToEnumerable(object readOnlyMemory, Type elementType) | ||
{ | ||
return (IEnumerable)Type.GetType("System.Runtime.InteropServices.MemoryMarshal, System.Memory") | ||
?.GetMethod("ToEnumerable", BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's best to specify parameter types so that this doesn't crash if an overload is added.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wanted to use this overload of GetMethod()
but it only support .NET Standard 2.0 and above - https://docs.microsoft.com/en-us/dotnet/api/system.type.getmethod?view=netframework-4.8#System_Type_GetMethod_System_String_System_Reflection_BindingFlags_System_Reflection_Binder_System_Type___System_Reflection_ParameterModifier___
Below is the modified code which fails to compile.
return (IEnumerable)Type.GetType("System.Runtime.InteropServices.MemoryMarshal, System.Memory")
?.GetMethod("ToEnumerable", BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly, null, new[] { typeof(ReadOnlyMemory<T>) }, null)
?.MakeGenericMethod(elementType)
.Invoke(null, new[] { readOnlyMemory });
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, that's too bad. We could cross our fingers and hope that an overload of MemoryMarshal.ToEnumerable is never added, or we could use GetMethods+SingleOrDefault like you did above, or we could replace netstandard1.3 with netstandard2.0.
@josephwoodward What are your thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GetMethods+SingleOrDefault sounds like a small enough change as opposed to upgrading to netstandard2.0
The library may stop supporting older .NET versions which may have unintended consequences.
{ | ||
var readOnlyMemory = type.GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) | ||
.SingleOrDefault(method => | ||
method.Name == "op_Implicit" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also you can check method.IsSpecialName
for operators.
<PropertyGroup> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any chance you could reformat back to the original (standard) 2-space csproj indentation, and squash it so the most recent line history for every line in the file doesn't point back to this PR?
else if (objectType.IsReadOnlyMemory(out genericParameterType)) | ||
{ | ||
value = value.ToEnumerable(genericParameterType); | ||
} | ||
|
||
if (value is IEnumerable) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are your thoughts on using TryGetEnumerable
from here?
thanks peoples |
Closes #562
@jnm2 I've created this as there was no update on the linked issue so I assumed no one was working on the fix.