From 6a55818bb1398352ffde319e4f9e052d5681bb27 Mon Sep 17 00:00:00 2001 From: Salvatore Isaja Date: Mon, 10 Oct 2022 19:23:53 +0200 Subject: [PATCH 1/2] Treat record structs as records (issue #1808) --- Src/FluentAssertions/Common/TypeExtensions.cs | 18 +++-- .../RecordSpecs.cs | 15 +++++ .../Types/TypeExtensionsSpecs.cs | 66 +++++++++++++++++++ 3 files changed, 93 insertions(+), 6 deletions(-) diff --git a/Src/FluentAssertions/Common/TypeExtensions.cs b/Src/FluentAssertions/Common/TypeExtensions.cs index 996602dbbd..ba992b3b97 100644 --- a/Src/FluentAssertions/Common/TypeExtensions.cs +++ b/Src/FluentAssertions/Common/TypeExtensions.cs @@ -584,12 +584,18 @@ private static bool IsAnonymousType(this Type type) public static bool IsRecord(this Type type) { return TypeIsRecordCache.GetOrAdd(type, static t => - t.GetMethod("$") is not null && - t.GetTypeInfo() - .DeclaredProperties - .FirstOrDefault(p => p.Name == "EqualityContract")? - .GetMethod? - .GetCustomAttribute(typeof(CompilerGeneratedAttribute)) is not null); + { + bool isRecord = t.GetMethod("$") is not null && + t.GetTypeInfo() + .DeclaredProperties + .FirstOrDefault(p => p.Name == "EqualityContract")? + .GetMethod? + .GetCustomAttribute(typeof(CompilerGeneratedAttribute)) is not null; + bool isRecordStruct = t.BaseType == typeof(ValueType) && + t.GetMethods().Where(m => m.Name == "op_Inequality").SelectMany(m => m.GetCustomAttributes(typeof(CompilerGeneratedAttribute))).Any() && + t.GetMethods().Where(m => m.Name == "op_Equality").SelectMany(m => m.GetCustomAttributes(typeof(CompilerGeneratedAttribute))).Any(); + return isRecord || isRecordStruct; + }); } private static bool IsKeyValuePair(Type type) diff --git a/Tests/FluentAssertions.Equivalency.Specs/RecordSpecs.cs b/Tests/FluentAssertions.Equivalency.Specs/RecordSpecs.cs index d7931e25df..d70c9de403 100644 --- a/Tests/FluentAssertions.Equivalency.Specs/RecordSpecs.cs +++ b/Tests/FluentAssertions.Equivalency.Specs/RecordSpecs.cs @@ -16,6 +16,16 @@ public void When_the_subject_is_a_record_it_should_compare_it_by_its_members() actual.Should().BeEquivalentTo(expected); } + [Fact] + public void When_the_subject_is_a_readonly_record_struct_it_should_compare_it_by_its_members() + { + var actual = new MyReadonlyRecordStruct("foo", new[] { "bar", "zip", "foo" }); + + var expected = new MyReadonlyRecordStruct("foo", new[] { "bar", "zip", "foo" }); + + actual.Should().BeEquivalentTo(expected); + } + [Fact] public void When_the_subject_is_a_record_it_should_mention_that_in_the_configuration_output() { @@ -90,4 +100,9 @@ private record MyRecord public string[] CollectionProperty { get; init; } } + + private readonly record struct MyReadonlyRecordStruct(string StringField, string[] CollectionProperty) + { + public readonly string StringField = StringField; + } } diff --git a/Tests/FluentAssertions.Specs/Types/TypeExtensionsSpecs.cs b/Tests/FluentAssertions.Specs/Types/TypeExtensionsSpecs.cs index 382d15fa5d..c7f08cfa46 100644 --- a/Tests/FluentAssertions.Specs/Types/TypeExtensionsSpecs.cs +++ b/Tests/FluentAssertions.Specs/Types/TypeExtensionsSpecs.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Immutable; using System.Linq; using System.Reflection; using FluentAssertions.Common; @@ -124,6 +125,33 @@ public void When_getting_fake_implicit_conversion_operator_from_a_type_with_fake result.Should().NotBeNull(); } + [Theory] + [InlineData(typeof(MyRecord), true)] + [InlineData(typeof(MyRecordStruct), true)] + [InlineData(typeof(MyRecordStructWithOverriddenEquality), true)] + [InlineData(typeof(MyReadonlyRecordStruct), true)] + [InlineData(typeof(MyStruct), false)] + [InlineData(typeof(MyStructWithOverriddenEquality), false)] + [InlineData(typeof(MyClass), false)] + [InlineData(typeof(int), false)] + [InlineData(typeof(string), false)] + public void IsRecord_should_detect_records_correctly(Type type, bool expected) + { + type.IsRecord().Should().Be(expected); + } + + [Fact] + public void When_checking_if_anonymous_type_is_record_it_should_return_false() + { + new { Value = 42 }.GetType().IsRecord().Should().Be(false); + } + + [Fact] + public void When_checking_if_class_with_multiple_equality_methods_is_record_it_should_return_false() + { + typeof(ImmutableArray).IsRecord().Should().Be(false); + } + private static MethodInfo GetFakeConversionOperator(Type type, string name, BindingFlags bindingAttr, Type returnType) { MethodInfo[] methods = type.GetMethods(bindingAttr); @@ -153,4 +181,42 @@ private TypeWithFakeConversionOperators(int value) public static byte op_Explicit(TypeWithFakeConversionOperators typeWithFakeConversionOperators) => (byte)typeWithFakeConversionOperators.value; #pragma warning restore SA1300, IDE1006 } + + private record MyRecord(int Value); + + private record struct MyRecordStruct(int Value); + + private record struct MyRecordStructWithOverriddenEquality(int Value) + { + public bool Equals(MyRecordStructWithOverriddenEquality other) => Value == other.Value; + + public override int GetHashCode() => Value; + } + + private readonly record struct MyReadonlyRecordStruct(int Value); + + private struct MyStruct + { + public int Value { get; set; } + } + + private struct MyStructWithOverriddenEquality : IEquatable + { + public int Value { get; set; } + + public bool Equals(MyStructWithOverriddenEquality other) => Value == other.Value; + + public override bool Equals(object obj) => obj is MyStructWithOverriddenEquality other && Equals(other); + + public override int GetHashCode() => Value; + + public static bool operator ==(MyStructWithOverriddenEquality left, MyStructWithOverriddenEquality right) => left.Equals(right); + + public static bool operator !=(MyStructWithOverriddenEquality left, MyStructWithOverriddenEquality right) => !left.Equals(right); + } + + private class MyClass + { + public int Value { get; set; } + } } From 2045c6a700e7e304610810b9db1320744db5f954 Mon Sep 17 00:00:00 2001 From: Salvatore Isaja Date: Wed, 12 Oct 2022 21:03:48 +0200 Subject: [PATCH 2/2] Use BeFalse in IsRecord assertions --- Tests/FluentAssertions.Specs/Types/TypeExtensionsSpecs.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/FluentAssertions.Specs/Types/TypeExtensionsSpecs.cs b/Tests/FluentAssertions.Specs/Types/TypeExtensionsSpecs.cs index c7f08cfa46..3a1df75233 100644 --- a/Tests/FluentAssertions.Specs/Types/TypeExtensionsSpecs.cs +++ b/Tests/FluentAssertions.Specs/Types/TypeExtensionsSpecs.cs @@ -143,13 +143,13 @@ public void IsRecord_should_detect_records_correctly(Type type, bool expected) [Fact] public void When_checking_if_anonymous_type_is_record_it_should_return_false() { - new { Value = 42 }.GetType().IsRecord().Should().Be(false); + new { Value = 42 }.GetType().IsRecord().Should().BeFalse(); } [Fact] public void When_checking_if_class_with_multiple_equality_methods_is_record_it_should_return_false() { - typeof(ImmutableArray).IsRecord().Should().Be(false); + typeof(ImmutableArray).IsRecord().Should().BeFalse(); } private static MethodInfo GetFakeConversionOperator(Type type, string name, BindingFlags bindingAttr, Type returnType)