From 91ea4fb2f86485a33eb5caec64d5d29c359da0f1 Mon Sep 17 00:00:00 2001 From: Jonathan Gilbert Date: Mon, 14 Feb 2022 23:34:43 -0600 Subject: [PATCH] Added property ExcludeNonBrowsable to IEquivalencyAssertionOptions.cs. Implemented it in SelfReferenceEquivalencyAssertionOptions.cs and CollectionMemberAssertionOptionsDecorator.cs and UsersOfGetClosedGenericInterfaces.cs. Added property IsBrowsable to IMember.cs and implemented it in Field.cs and Property.cs. Added benchmark CheckIfMemberIsBrowsable. Adjusted the implementation of AssertMemberEquality in StructuralEqualityEquivalencyStep.cs to combine these fields to allow for non-browsable members to be skipped when checking equivalence. Added automated tests of the new functionality to SelectionRulesSpec.cs. Accepted API changes into the approved API. Updated objectgraphs.md to document the new non-browsable "hidden" members exclusion feature. Updated releases.md to describe the new feature. --- ...llectionMemberAssertionOptionsDecorator.cs | 2 + Src/FluentAssertions/Equivalency/Field.cs | 15 + .../IEquivalencyAssertionOptions.cs | 8 + Src/FluentAssertions/Equivalency/IMember.cs | 5 + Src/FluentAssertions/Equivalency/Property.cs | 15 + ...elfReferenceEquivalencyAssertionOptions.cs | 33 +++ .../StructuralEqualityEquivalencyStep.cs | 27 +- .../FluentAssertions/net47.verified.txt | 6 + .../netcoreapp2.1.verified.txt | 6 + .../netcoreapp3.0.verified.txt | 6 + .../netstandard2.0.verified.txt | 6 + .../netstandard2.1.verified.txt | 6 + Tests/Benchmarks/CheckIfMemberIsBrowsable.cs | 28 ++ Tests/Benchmarks/Program.cs | 2 +- .../UsersOfGetClosedGenericInterfaces.cs | 2 + .../SelectionRulesSpecs.cs | 258 ++++++++++++++++++ docs/_pages/objectgraphs.md | 21 ++ docs/_pages/releases.md | 3 +- 18 files changed, 435 insertions(+), 14 deletions(-) create mode 100644 Tests/Benchmarks/CheckIfMemberIsBrowsable.cs diff --git a/Src/FluentAssertions/Equivalency/Execution/CollectionMemberAssertionOptionsDecorator.cs b/Src/FluentAssertions/Equivalency/Execution/CollectionMemberAssertionOptionsDecorator.cs index 86c06cf999..868c0ba910 100644 --- a/Src/FluentAssertions/Equivalency/Execution/CollectionMemberAssertionOptionsDecorator.cs +++ b/Src/FluentAssertions/Equivalency/Execution/CollectionMemberAssertionOptionsDecorator.cs @@ -61,6 +61,8 @@ public IEnumerable UserEquivalencySteps public MemberVisibility IncludedFields => inner.IncludedFields; + public bool ExcludeNonBrowsable => inner.ExcludeNonBrowsable; + public bool CompareRecordsByValue => inner.CompareRecordsByValue; public EqualityStrategy GetEqualityStrategy(Type type) diff --git a/Src/FluentAssertions/Equivalency/Field.cs b/Src/FluentAssertions/Equivalency/Field.cs index 04b06c32da..9c1720b9c6 100644 --- a/Src/FluentAssertions/Equivalency/Field.cs +++ b/Src/FluentAssertions/Equivalency/Field.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.Reflection; using FluentAssertions.Common; @@ -10,6 +11,7 @@ namespace FluentAssertions.Equivalency public class Field : Node, IMember { private readonly FieldInfo fieldInfo; + private bool? isBrowsable; public Field(FieldInfo fieldInfo, INode parent) : this(fieldInfo.ReflectedType, fieldInfo, parent) @@ -42,5 +44,18 @@ public object GetValue(object obj) public CSharpAccessModifier GetterAccessibility => fieldInfo.GetCSharpAccessModifier(); public CSharpAccessModifier SetterAccessibility => fieldInfo.GetCSharpAccessModifier(); + + public bool IsBrowsable + { + get + { + if (isBrowsable == null) + { + isBrowsable = fieldInfo.GetCustomAttribute() is not { State: EditorBrowsableState.Never }; + } + + return isBrowsable.Value; + } + } } } diff --git a/Src/FluentAssertions/Equivalency/IEquivalencyAssertionOptions.cs b/Src/FluentAssertions/Equivalency/IEquivalencyAssertionOptions.cs index bcc5a61199..fa6c28021d 100644 --- a/Src/FluentAssertions/Equivalency/IEquivalencyAssertionOptions.cs +++ b/Src/FluentAssertions/Equivalency/IEquivalencyAssertionOptions.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.ComponentModel; + using FluentAssertions.Equivalency.Tracing; namespace FluentAssertions.Equivalency @@ -71,6 +73,12 @@ public interface IEquivalencyAssertionOptions /// MemberVisibility IncludedFields { get; } + /// + /// Gets a value indicating whether members marked with [EditorBrowsable] + /// and an EditorBrowsableState of Never should be excluded. + /// + bool ExcludeNonBrowsable { get; } + /// /// Gets a value indicating whether records should be compared by value instead of their members /// diff --git a/Src/FluentAssertions/Equivalency/IMember.cs b/Src/FluentAssertions/Equivalency/IMember.cs index f14c39ab95..80e9107dd0 100644 --- a/Src/FluentAssertions/Equivalency/IMember.cs +++ b/Src/FluentAssertions/Equivalency/IMember.cs @@ -32,5 +32,10 @@ public interface IMember : INode /// Gets the access modifier for the setter of this member. /// CSharpAccessModifier SetterAccessibility { get; } + + /// + /// Gets a value indicating whether the member is browsable in the source code editor. This is controlled with the [EditorBrowsable] attribute. + /// + bool IsBrowsable { get; } } } diff --git a/Src/FluentAssertions/Equivalency/Property.cs b/Src/FluentAssertions/Equivalency/Property.cs index f53eeab7a0..de3741bb9a 100644 --- a/Src/FluentAssertions/Equivalency/Property.cs +++ b/Src/FluentAssertions/Equivalency/Property.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.Reflection; using FluentAssertions.Common; @@ -11,6 +12,7 @@ namespace FluentAssertions.Equivalency public class Property : Node, IMember { private readonly PropertyInfo propertyInfo; + private bool? isBrowsable; public Property(PropertyInfo propertyInfo, INode parent) : this(propertyInfo.ReflectedType, propertyInfo, parent) @@ -43,5 +45,18 @@ public object GetValue(object obj) public CSharpAccessModifier GetterAccessibility => propertyInfo.GetGetMethod(nonPublic: true).GetCSharpAccessModifier(); public CSharpAccessModifier SetterAccessibility => propertyInfo.GetSetMethod(nonPublic: true).GetCSharpAccessModifier(); + + public bool IsBrowsable + { + get + { + if (isBrowsable == null) + { + isBrowsable = propertyInfo.GetCustomAttribute() is not { State: EditorBrowsableState.Never }; + } + + return isBrowsable.Value; + } + } } } diff --git a/Src/FluentAssertions/Equivalency/SelfReferenceEquivalencyAssertionOptions.cs b/Src/FluentAssertions/Equivalency/SelfReferenceEquivalencyAssertionOptions.cs index ac095d0df7..e1415be062 100644 --- a/Src/FluentAssertions/Equivalency/SelfReferenceEquivalencyAssertionOptions.cs +++ b/Src/FluentAssertions/Equivalency/SelfReferenceEquivalencyAssertionOptions.cs @@ -56,6 +56,7 @@ public abstract class SelfReferenceEquivalencyAssertionOptions : IEquival private MemberVisibility includedProperties; private MemberVisibility includedFields; + private bool excludeNonBrowsable; private bool compareRecordsByValue; @@ -80,6 +81,7 @@ protected SelfReferenceEquivalencyAssertionOptions(IEquivalencyAssertionOptions useRuntimeTyping = defaults.UseRuntimeTyping; includedProperties = defaults.IncludedProperties; includedFields = defaults.IncludedFields; + excludeNonBrowsable = defaults.ExcludeNonBrowsable; compareRecordsByValue = defaults.CompareRecordsByValue; ConversionSelector = defaults.ConversionSelector.Clone(); @@ -162,6 +164,8 @@ IEnumerable IEquivalencyAssertionOptions.SelectionRules MemberVisibility IEquivalencyAssertionOptions.IncludedFields => includedFields; + bool IEquivalencyAssertionOptions.ExcludeNonBrowsable => excludeNonBrowsable; + public bool CompareRecordsByValue => compareRecordsByValue; EqualityStrategy IEquivalencyAssertionOptions.GetEqualityStrategy(Type requestedType) @@ -312,6 +316,26 @@ public TSelf ExcludingProperties() return (TSelf)this; } + /// + /// Instructs the comparison to include non-browsable members (members with an EditorBrowsableState of Never). + /// + /// + public TSelf IncludingNonBrowsableMembers() + { + excludeNonBrowsable = false; + return (TSelf)this; + } + + /// + /// Instructs the comparison to exclude non-browsable members (members with an EditorBrowsableState of Never). + /// + /// + public TSelf ExcludingNonBrowsableMembers() + { + excludeNonBrowsable = true; + return (TSelf)this; + } + /// /// Instructs the comparison to respect the expectation's runtime type. /// @@ -727,6 +751,15 @@ public override string ToString() builder.AppendLine("- Compare records by their members"); } + if (excludeNonBrowsable) + { + builder.AppendLine("- Exclude non-browsable members"); + } + else + { + builder.AppendLine("- Include non-browsable members"); + } + foreach (Type valueType in valueTypes) { builder.AppendLine($"- Compare {valueType} by value"); diff --git a/Src/FluentAssertions/Equivalency/Steps/StructuralEqualityEquivalencyStep.cs b/Src/FluentAssertions/Equivalency/Steps/StructuralEqualityEquivalencyStep.cs index 845f8a2633..c2a5f8b69b 100644 --- a/Src/FluentAssertions/Equivalency/Steps/StructuralEqualityEquivalencyStep.cs +++ b/Src/FluentAssertions/Equivalency/Steps/StructuralEqualityEquivalencyStep.cs @@ -55,21 +55,24 @@ public class StructuralEqualityEquivalencyStep : IEquivalencyStep IMember matchingMember = FindMatchFor(selectedMember, context.CurrentNode, comparands.Subject, options); if (matchingMember is not null) { - var nestedComparands = new Comparands + if (!options.ExcludeNonBrowsable || matchingMember.IsBrowsable) { - Subject = matchingMember.GetValue(comparands.Subject), - Expectation = selectedMember.GetValue(comparands.Expectation), - CompileTimeType = selectedMember.Type - }; + var nestedComparands = new Comparands + { + Subject = matchingMember.GetValue(comparands.Subject), + Expectation = selectedMember.GetValue(comparands.Expectation), + CompileTimeType = selectedMember.Type + }; - if (selectedMember.Name != matchingMember.Name) - { - // In case the matching process selected a different member on the subject, - // adjust the current member so that assertion failures report the proper name. - selectedMember.Name = matchingMember.Name; - } + if (selectedMember.Name != matchingMember.Name) + { + // In case the matching process selected a different member on the subject, + // adjust the current member so that assertion failures report the proper name. + selectedMember.Name = matchingMember.Name; + } - parent.RecursivelyAssertEquality(nestedComparands, context.AsNestedMember(selectedMember)); + parent.RecursivelyAssertEquality(nestedComparands, context.AsNestedMember(selectedMember)); + } } } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt index f03027f71d..97519bb273 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt @@ -803,6 +803,7 @@ namespace FluentAssertions.Equivalency public System.Type DeclaringType { get; set; } public override string Description { get; } public FluentAssertions.Common.CSharpAccessModifier GetterAccessibility { get; } + public bool IsBrowsable { get; } public System.Type ReflectedType { get; } public FluentAssertions.Common.CSharpAccessModifier SetterAccessibility { get; } public object GetValue(object obj) { } @@ -823,6 +824,7 @@ namespace FluentAssertions.Equivalency FluentAssertions.Equivalency.ConversionSelector ConversionSelector { get; } FluentAssertions.Equivalency.CyclicReferenceHandling CyclicReferenceHandling { get; } FluentAssertions.Equivalency.EnumEquivalencyHandling EnumEquivalencyHandling { get; } + bool ExcludeNonBrowsable { get; } FluentAssertions.Equivalency.MemberVisibility IncludedFields { get; } FluentAssertions.Equivalency.MemberVisibility IncludedProperties { get; } bool IsRecursive { get; } @@ -858,6 +860,7 @@ namespace FluentAssertions.Equivalency { System.Type DeclaringType { get; } FluentAssertions.Common.CSharpAccessModifier GetterAccessibility { get; } + bool IsBrowsable { get; } System.Type ReflectedType { get; } FluentAssertions.Common.CSharpAccessModifier SetterAccessibility { get; } object GetValue(object obj); @@ -961,6 +964,7 @@ namespace FluentAssertions.Equivalency public System.Type DeclaringType { get; } public override string Description { get; } public FluentAssertions.Common.CSharpAccessModifier GetterAccessibility { get; } + public bool IsBrowsable { get; } public System.Type ReflectedType { get; } public FluentAssertions.Common.CSharpAccessModifier SetterAccessibility { get; } public object GetValue(object obj) { } @@ -989,6 +993,7 @@ namespace FluentAssertions.Equivalency public TSelf ExcludingFields() { } public TSelf ExcludingMissingMembers() { } public TSelf ExcludingNestedObjects() { } + public TSelf ExcludingNonBrowsableMembers() { } public TSelf ExcludingProperties() { } public TSelf IgnoringCyclicReferences() { } public TSelf Including(System.Linq.Expressions.Expression> predicate) { } @@ -998,6 +1003,7 @@ namespace FluentAssertions.Equivalency public TSelf IncludingInternalFields() { } public TSelf IncludingInternalProperties() { } public TSelf IncludingNestedObjects() { } + public TSelf IncludingNonBrowsableMembers() { } public TSelf IncludingProperties() { } public TSelf RespectingDeclaredTypes() { } public TSelf RespectingRuntimeTypes() { } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt index 2a2b438262..951b5bbc90 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt @@ -803,6 +803,7 @@ namespace FluentAssertions.Equivalency public System.Type DeclaringType { get; set; } public override string Description { get; } public FluentAssertions.Common.CSharpAccessModifier GetterAccessibility { get; } + public bool IsBrowsable { get; } public System.Type ReflectedType { get; } public FluentAssertions.Common.CSharpAccessModifier SetterAccessibility { get; } public object GetValue(object obj) { } @@ -823,6 +824,7 @@ namespace FluentAssertions.Equivalency FluentAssertions.Equivalency.ConversionSelector ConversionSelector { get; } FluentAssertions.Equivalency.CyclicReferenceHandling CyclicReferenceHandling { get; } FluentAssertions.Equivalency.EnumEquivalencyHandling EnumEquivalencyHandling { get; } + bool ExcludeNonBrowsable { get; } FluentAssertions.Equivalency.MemberVisibility IncludedFields { get; } FluentAssertions.Equivalency.MemberVisibility IncludedProperties { get; } bool IsRecursive { get; } @@ -858,6 +860,7 @@ namespace FluentAssertions.Equivalency { System.Type DeclaringType { get; } FluentAssertions.Common.CSharpAccessModifier GetterAccessibility { get; } + bool IsBrowsable { get; } System.Type ReflectedType { get; } FluentAssertions.Common.CSharpAccessModifier SetterAccessibility { get; } object GetValue(object obj); @@ -961,6 +964,7 @@ namespace FluentAssertions.Equivalency public System.Type DeclaringType { get; } public override string Description { get; } public FluentAssertions.Common.CSharpAccessModifier GetterAccessibility { get; } + public bool IsBrowsable { get; } public System.Type ReflectedType { get; } public FluentAssertions.Common.CSharpAccessModifier SetterAccessibility { get; } public object GetValue(object obj) { } @@ -989,6 +993,7 @@ namespace FluentAssertions.Equivalency public TSelf ExcludingFields() { } public TSelf ExcludingMissingMembers() { } public TSelf ExcludingNestedObjects() { } + public TSelf ExcludingNonBrowsableMembers() { } public TSelf ExcludingProperties() { } public TSelf IgnoringCyclicReferences() { } public TSelf Including(System.Linq.Expressions.Expression> predicate) { } @@ -998,6 +1003,7 @@ namespace FluentAssertions.Equivalency public TSelf IncludingInternalFields() { } public TSelf IncludingInternalProperties() { } public TSelf IncludingNestedObjects() { } + public TSelf IncludingNonBrowsableMembers() { } public TSelf IncludingProperties() { } public TSelf RespectingDeclaredTypes() { } public TSelf RespectingRuntimeTypes() { } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt index 53b96a5c6d..905660df9d 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt @@ -803,6 +803,7 @@ namespace FluentAssertions.Equivalency public System.Type DeclaringType { get; set; } public override string Description { get; } public FluentAssertions.Common.CSharpAccessModifier GetterAccessibility { get; } + public bool IsBrowsable { get; } public System.Type ReflectedType { get; } public FluentAssertions.Common.CSharpAccessModifier SetterAccessibility { get; } public object GetValue(object obj) { } @@ -823,6 +824,7 @@ namespace FluentAssertions.Equivalency FluentAssertions.Equivalency.ConversionSelector ConversionSelector { get; } FluentAssertions.Equivalency.CyclicReferenceHandling CyclicReferenceHandling { get; } FluentAssertions.Equivalency.EnumEquivalencyHandling EnumEquivalencyHandling { get; } + bool ExcludeNonBrowsable { get; } FluentAssertions.Equivalency.MemberVisibility IncludedFields { get; } FluentAssertions.Equivalency.MemberVisibility IncludedProperties { get; } bool IsRecursive { get; } @@ -858,6 +860,7 @@ namespace FluentAssertions.Equivalency { System.Type DeclaringType { get; } FluentAssertions.Common.CSharpAccessModifier GetterAccessibility { get; } + bool IsBrowsable { get; } System.Type ReflectedType { get; } FluentAssertions.Common.CSharpAccessModifier SetterAccessibility { get; } object GetValue(object obj); @@ -961,6 +964,7 @@ namespace FluentAssertions.Equivalency public System.Type DeclaringType { get; } public override string Description { get; } public FluentAssertions.Common.CSharpAccessModifier GetterAccessibility { get; } + public bool IsBrowsable { get; } public System.Type ReflectedType { get; } public FluentAssertions.Common.CSharpAccessModifier SetterAccessibility { get; } public object GetValue(object obj) { } @@ -989,6 +993,7 @@ namespace FluentAssertions.Equivalency public TSelf ExcludingFields() { } public TSelf ExcludingMissingMembers() { } public TSelf ExcludingNestedObjects() { } + public TSelf ExcludingNonBrowsableMembers() { } public TSelf ExcludingProperties() { } public TSelf IgnoringCyclicReferences() { } public TSelf Including(System.Linq.Expressions.Expression> predicate) { } @@ -998,6 +1003,7 @@ namespace FluentAssertions.Equivalency public TSelf IncludingInternalFields() { } public TSelf IncludingInternalProperties() { } public TSelf IncludingNestedObjects() { } + public TSelf IncludingNonBrowsableMembers() { } public TSelf IncludingProperties() { } public TSelf RespectingDeclaredTypes() { } public TSelf RespectingRuntimeTypes() { } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt index 97296cf147..1febb34719 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt @@ -796,6 +796,7 @@ namespace FluentAssertions.Equivalency public System.Type DeclaringType { get; set; } public override string Description { get; } public FluentAssertions.Common.CSharpAccessModifier GetterAccessibility { get; } + public bool IsBrowsable { get; } public System.Type ReflectedType { get; } public FluentAssertions.Common.CSharpAccessModifier SetterAccessibility { get; } public object GetValue(object obj) { } @@ -816,6 +817,7 @@ namespace FluentAssertions.Equivalency FluentAssertions.Equivalency.ConversionSelector ConversionSelector { get; } FluentAssertions.Equivalency.CyclicReferenceHandling CyclicReferenceHandling { get; } FluentAssertions.Equivalency.EnumEquivalencyHandling EnumEquivalencyHandling { get; } + bool ExcludeNonBrowsable { get; } FluentAssertions.Equivalency.MemberVisibility IncludedFields { get; } FluentAssertions.Equivalency.MemberVisibility IncludedProperties { get; } bool IsRecursive { get; } @@ -851,6 +853,7 @@ namespace FluentAssertions.Equivalency { System.Type DeclaringType { get; } FluentAssertions.Common.CSharpAccessModifier GetterAccessibility { get; } + bool IsBrowsable { get; } System.Type ReflectedType { get; } FluentAssertions.Common.CSharpAccessModifier SetterAccessibility { get; } object GetValue(object obj); @@ -954,6 +957,7 @@ namespace FluentAssertions.Equivalency public System.Type DeclaringType { get; } public override string Description { get; } public FluentAssertions.Common.CSharpAccessModifier GetterAccessibility { get; } + public bool IsBrowsable { get; } public System.Type ReflectedType { get; } public FluentAssertions.Common.CSharpAccessModifier SetterAccessibility { get; } public object GetValue(object obj) { } @@ -982,6 +986,7 @@ namespace FluentAssertions.Equivalency public TSelf ExcludingFields() { } public TSelf ExcludingMissingMembers() { } public TSelf ExcludingNestedObjects() { } + public TSelf ExcludingNonBrowsableMembers() { } public TSelf ExcludingProperties() { } public TSelf IgnoringCyclicReferences() { } public TSelf Including(System.Linq.Expressions.Expression> predicate) { } @@ -991,6 +996,7 @@ namespace FluentAssertions.Equivalency public TSelf IncludingInternalFields() { } public TSelf IncludingInternalProperties() { } public TSelf IncludingNestedObjects() { } + public TSelf IncludingNonBrowsableMembers() { } public TSelf IncludingProperties() { } public TSelf RespectingDeclaredTypes() { } public TSelf RespectingRuntimeTypes() { } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt index 812a1e0735..cb6de097da 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt @@ -803,6 +803,7 @@ namespace FluentAssertions.Equivalency public System.Type DeclaringType { get; set; } public override string Description { get; } public FluentAssertions.Common.CSharpAccessModifier GetterAccessibility { get; } + public bool IsBrowsable { get; } public System.Type ReflectedType { get; } public FluentAssertions.Common.CSharpAccessModifier SetterAccessibility { get; } public object GetValue(object obj) { } @@ -823,6 +824,7 @@ namespace FluentAssertions.Equivalency FluentAssertions.Equivalency.ConversionSelector ConversionSelector { get; } FluentAssertions.Equivalency.CyclicReferenceHandling CyclicReferenceHandling { get; } FluentAssertions.Equivalency.EnumEquivalencyHandling EnumEquivalencyHandling { get; } + bool ExcludeNonBrowsable { get; } FluentAssertions.Equivalency.MemberVisibility IncludedFields { get; } FluentAssertions.Equivalency.MemberVisibility IncludedProperties { get; } bool IsRecursive { get; } @@ -858,6 +860,7 @@ namespace FluentAssertions.Equivalency { System.Type DeclaringType { get; } FluentAssertions.Common.CSharpAccessModifier GetterAccessibility { get; } + bool IsBrowsable { get; } System.Type ReflectedType { get; } FluentAssertions.Common.CSharpAccessModifier SetterAccessibility { get; } object GetValue(object obj); @@ -961,6 +964,7 @@ namespace FluentAssertions.Equivalency public System.Type DeclaringType { get; } public override string Description { get; } public FluentAssertions.Common.CSharpAccessModifier GetterAccessibility { get; } + public bool IsBrowsable { get; } public System.Type ReflectedType { get; } public FluentAssertions.Common.CSharpAccessModifier SetterAccessibility { get; } public object GetValue(object obj) { } @@ -989,6 +993,7 @@ namespace FluentAssertions.Equivalency public TSelf ExcludingFields() { } public TSelf ExcludingMissingMembers() { } public TSelf ExcludingNestedObjects() { } + public TSelf ExcludingNonBrowsableMembers() { } public TSelf ExcludingProperties() { } public TSelf IgnoringCyclicReferences() { } public TSelf Including(System.Linq.Expressions.Expression> predicate) { } @@ -998,6 +1003,7 @@ namespace FluentAssertions.Equivalency public TSelf IncludingInternalFields() { } public TSelf IncludingInternalProperties() { } public TSelf IncludingNestedObjects() { } + public TSelf IncludingNonBrowsableMembers() { } public TSelf IncludingProperties() { } public TSelf RespectingDeclaredTypes() { } public TSelf RespectingRuntimeTypes() { } diff --git a/Tests/Benchmarks/CheckIfMemberIsBrowsable.cs b/Tests/Benchmarks/CheckIfMemberIsBrowsable.cs new file mode 100644 index 0000000000..fc7948ee47 --- /dev/null +++ b/Tests/Benchmarks/CheckIfMemberIsBrowsable.cs @@ -0,0 +1,28 @@ +using System.ComponentModel; +using System.Reflection; + +using BenchmarkDotNet.Attributes; + +namespace Benchmarks +{ + [MemoryDiagnoser] + public class CheckIfMemberIsBrowsableBenchmarks + { + [Params(true, false)] + public bool IsBrowsable { get; set; } + + public int BrowsableField; + [EditorBrowsable(EditorBrowsableState.Never)] + public int NonBrowsableField; + + public FieldInfo SubjectField => typeof(CheckIfMemberIsBrowsableBenchmarks) + .GetField(IsBrowsable ? nameof(BrowsableField) : nameof(NonBrowsableField)); + + [Benchmark] + public void CheckIfMemberIsBrowsable() + { + bool _ = + SubjectField.GetCustomAttribute() is not { State: EditorBrowsableState.Never }; + } + } +} diff --git a/Tests/Benchmarks/Program.cs b/Tests/Benchmarks/Program.cs index 4f917d4cad..7409b520f4 100644 --- a/Tests/Benchmarks/Program.cs +++ b/Tests/Benchmarks/Program.cs @@ -22,7 +22,7 @@ public static void Main() var config = ManualConfig.CreateMinimumViable().AddExporter(exporter); - _ = BenchmarkRunner.Run(config); + _ = BenchmarkRunner.Run(config); } } } diff --git a/Tests/Benchmarks/UsersOfGetClosedGenericInterfaces.cs b/Tests/Benchmarks/UsersOfGetClosedGenericInterfaces.cs index 0d087c06fd..c95bf80054 100644 --- a/Tests/Benchmarks/UsersOfGetClosedGenericInterfaces.cs +++ b/Tests/Benchmarks/UsersOfGetClosedGenericInterfaces.cs @@ -69,6 +69,8 @@ private class Config : IEquivalencyAssertionOptions public MemberVisibility IncludedFields => throw new NotImplementedException(); + public bool ExcludeNonBrowsable => throw new NotImplementedException(); + public bool CompareRecordsByValue => throw new NotImplementedException(); public ITraceWriter TraceWriter => throw new NotImplementedException(); diff --git a/Tests/FluentAssertions.Equivalency.Specs/SelectionRulesSpecs.cs b/Tests/FluentAssertions.Equivalency.Specs/SelectionRulesSpecs.cs index 84f742b874..97e548cb5f 100644 --- a/Tests/FluentAssertions.Equivalency.Specs/SelectionRulesSpecs.cs +++ b/Tests/FluentAssertions.Equivalency.Specs/SelectionRulesSpecs.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; @@ -1489,5 +1490,262 @@ public void Including_an_interface_property_through_inheritance_should_work() .Including(a => a.Value2) .RespectingRuntimeTypes()); } + + [Fact] + public void When_browsable_field_differs_including_non_browsable_members_should_not_affect_result() + { + // Arrange + var subject = new ClassWithNonBrowsableMembers() { BrowsableField = 0 }; + var expectation = new ClassWithNonBrowsableMembers() { BrowsableField = 1 }; + + // Act + Action action = + () => subject.Should().BeEquivalentTo(expectation, config => config.IncludingNonBrowsableMembers()); + + // Assert + action.Should().Throw(); + } + + [Fact] + public void When_browsable_property_differs_including_non_browsable_members_should_not_affect_result() + { + // Arrange + var subject = new ClassWithNonBrowsableMembers() { BrowsableProperty = 0 }; + var expectation = new ClassWithNonBrowsableMembers() { BrowsableProperty = 1 }; + + // Act + Action action = + () => subject.Should().BeEquivalentTo(expectation, config => config.IncludingNonBrowsableMembers()); + + // Assert + action.Should().Throw(); + } + + [Fact] + public void When_advanced_browsable_field_differs_including_non_browsable_members_should_not_affect_result() + { + // Arrange + var subject = new ClassWithNonBrowsableMembers() { AdvancedBrowsableField = 0 }; + var expectation = new ClassWithNonBrowsableMembers() { AdvancedBrowsableField = 1 }; + + // Act + Action action = + () => subject.Should().BeEquivalentTo(expectation, config => config.IncludingNonBrowsableMembers()); + + // Assert + action.Should().Throw(); + } + + [Fact] + public void When_advanced_browsable_property_differs_including_non_browsable_members_should_not_affect_result() + { + // Arrange + var subject = new ClassWithNonBrowsableMembers() { AdvancedBrowsableProperty = 0 }; + var expectation = new ClassWithNonBrowsableMembers() { AdvancedBrowsableProperty = 1 }; + + // Act + Action action = + () => subject.Should().BeEquivalentTo(expectation, config => config.IncludingNonBrowsableMembers()); + + // Assert + action.Should().Throw(); + } + + [Fact] + public void When_explicitly_browsable_field_differs_including_non_browsable_members_should_not_affect_result() + { + // Arrange + var subject = new ClassWithNonBrowsableMembers() { ExplicitlyBrowsableField = 0 }; + var expectation = new ClassWithNonBrowsableMembers() { ExplicitlyBrowsableField = 1 }; + + // Act + Action action = + () => subject.Should().BeEquivalentTo(expectation, config => config.IncludingNonBrowsableMembers()); + + // Assert + action.Should().Throw(); + } + + [Fact] + public void When_explicitly_browsable_property_differs_including_non_browsable_members_should_not_affect_result() + { + // Arrange + var subject = new ClassWithNonBrowsableMembers() { ExplicitlyBrowsableProperty = 0 }; + var expectation = new ClassWithNonBrowsableMembers() { ExplicitlyBrowsableProperty = 1 }; + + // Act + Action action = + () => subject.Should().BeEquivalentTo(expectation, config => config.IncludingNonBrowsableMembers()); + + // Assert + action.Should().Throw(); + } + + [Fact] + public void When_non_browsable_field_differs_including_non_browsable_members_should_not_affect_result() + { + // Arrange + var subject = new ClassWithNonBrowsableMembers() { NonBrowsableField = 0 }; + var expectation = new ClassWithNonBrowsableMembers() { NonBrowsableField = 1 }; + + // Act + Action action = + () => subject.Should().BeEquivalentTo(expectation, config => config.IncludingNonBrowsableMembers()); + + // Assert + action.Should().Throw(); + } + + [Fact] + public void When_non_browsable_property_differs_including_non_browsable_members_should_not_affect_result() + { + // Arrange + var subject = new ClassWithNonBrowsableMembers() { NonBrowsableProperty = 0 }; + var expectation = new ClassWithNonBrowsableMembers() { NonBrowsableProperty = 1 }; + + // Act + Action action = + () => subject.Should().BeEquivalentTo(expectation, config => config.IncludingNonBrowsableMembers()); + + // Assert + action.Should().Throw(); + } + + [Fact] + public void When_browsable_field_differs_excluding_non_browsable_members_should_not_affect_result() + { + // Arrange + var subject = new ClassWithNonBrowsableMembers() { BrowsableField = 0 }; + var expectation = new ClassWithNonBrowsableMembers() { BrowsableField = 1 }; + + // Act + Action action = + () => subject.Should().BeEquivalentTo(expectation, config => config.ExcludingNonBrowsableMembers()); + + // Assert + action.Should().Throw(); + } + + [Fact] + public void When_browsable_property_differs_excluding_non_browsable_members_should_not_affect_result() + { + // Arrange + var subject = new ClassWithNonBrowsableMembers() { BrowsableProperty = 0 }; + var expectation = new ClassWithNonBrowsableMembers() { BrowsableProperty = 1 }; + + // Act + Action action = + () => subject.Should().BeEquivalentTo(expectation, config => config.ExcludingNonBrowsableMembers()); + + // Assert + action.Should().Throw(); + } + + [Fact] + public void When_advanced_browsable_field_differs_excluding_non_browsable_members_should_not_affect_result() + { + // Arrange + var subject = new ClassWithNonBrowsableMembers() { AdvancedBrowsableField = 0 }; + var expectation = new ClassWithNonBrowsableMembers() { AdvancedBrowsableField = 1 }; + + // Act + Action action = + () => subject.Should().BeEquivalentTo(expectation, config => config.ExcludingNonBrowsableMembers()); + + // Assert + action.Should().Throw(); + } + + [Fact] + public void When_advanced_browsable_property_differs_excluding_non_browsable_members_should_not_affect_result() + { + // Arrange + var subject = new ClassWithNonBrowsableMembers() { AdvancedBrowsableProperty = 0 }; + var expectation = new ClassWithNonBrowsableMembers() { AdvancedBrowsableProperty = 1 }; + + // Act + Action action = + () => subject.Should().BeEquivalentTo(expectation, config => config.ExcludingNonBrowsableMembers()); + + // Assert + action.Should().Throw(); + } + + [Fact] + public void When_explicilty_browsable_field_differs_excluding_non_browsable_members_should_not_affect_result() + { + // Arrange + var subject = new ClassWithNonBrowsableMembers() { ExplicitlyBrowsableField = 0 }; + var expectation = new ClassWithNonBrowsableMembers() { ExplicitlyBrowsableField = 1 }; + + // Act + Action action = + () => subject.Should().BeEquivalentTo(expectation, config => config.ExcludingNonBrowsableMembers()); + + // Assert + action.Should().Throw(); + } + + [Fact] + public void When_explicilty_browsable_property_differs_excluding_non_browsable_members_should_not_affect_result() + { + // Arrange + var subject = new ClassWithNonBrowsableMembers() { ExplicitlyBrowsableProperty = 0 }; + var expectation = new ClassWithNonBrowsableMembers() { ExplicitlyBrowsableProperty = 1 }; + + // Act + Action action = + () => subject.Should().BeEquivalentTo(expectation, config => config.ExcludingNonBrowsableMembers()); + + // Assert + action.Should().Throw(); + } + + [Fact] + public void When_non_browsable_field_differs_excluding_non_browsable_members_should_make_it_succeed() + { + // Arrange + var subject = new ClassWithNonBrowsableMembers() { NonBrowsableField = 0 }; + var expectation = new ClassWithNonBrowsableMembers() { NonBrowsableField = 1 }; + + // Act & Assert + subject.Should().BeEquivalentTo(expectation, config => config.ExcludingNonBrowsableMembers()); + } + + [Fact] + public void When_non_browsable_property_differs_excluding_non_browsable_members_should_make_it_succeed() + { + // Arrange + var subject = new ClassWithNonBrowsableMembers() { NonBrowsableProperty = 0 }; + var expectation = new ClassWithNonBrowsableMembers() { NonBrowsableProperty = 1 }; + + // Act & Assert + subject.Should().BeEquivalentTo(expectation, config => config.ExcludingNonBrowsableMembers()); + } + + private class ClassWithNonBrowsableMembers + { + public int BrowsableField; + + public int BrowsableProperty { get; set; } + + [EditorBrowsable(EditorBrowsableState.Always)] + public int ExplicitlyBrowsableField; + + [EditorBrowsable(EditorBrowsableState.Always)] + public int ExplicitlyBrowsableProperty { get; set; } + + [EditorBrowsable(EditorBrowsableState.Advanced)] + public int AdvancedBrowsableField; + + [EditorBrowsable(EditorBrowsableState.Advanced)] + public int AdvancedBrowsableProperty { get; set; } + + [EditorBrowsable(EditorBrowsableState.Never)] + public int NonBrowsableField; + + [EditorBrowsable(EditorBrowsableState.Never)] + public int NonBrowsableProperty { get; set; } + } } } diff --git a/docs/_pages/objectgraphs.md b/docs/_pages/objectgraphs.md index 505e186e59..a21abb76ed 100644 --- a/docs/_pages/objectgraphs.md +++ b/docs/_pages/objectgraphs.md @@ -216,6 +216,27 @@ orderDto.Should().BeEquivalentTo(order, options => options Notice that you can also map properties to fields and vice-versa. +### Hidden Members + +Sometimes types have members out of necessity, to satisfy a contract, but they aren't logically a part of the type. In this case, they are often marked with the attribute `[EditorBrowsable(EditorBrowsableState.Never)]`, so that the object can satisfy the contract but the members don't show up in IntelliSense when writing code that uses the type. + +If you want to compare objects that have such fields, but you want to exclude the non-browsable "hidden" members (for instance, their implementations often simply throw `NotImplementedException`), you can call `ExcludingNonBrowsableMembers` on the options object: + +```csharp +class DataType +{ + public int X { get; } + + [EditorBrowsable(EditorBrowsableState.Never)] + public int Y => throw new NotImplementedException(); +} + +DataType original, derived; + +derived.Should().BeEquivalentTo(original, options => options + .ExcludingNonBrowsableMembers()); +``` + ### Equivalency Comparison Behavior In addition to influencing the members that are including in the comparison, you can also override the actual assertion operation that is executed on a particular member. diff --git a/docs/_pages/releases.md b/docs/_pages/releases.md index c3f0021dcb..40e458dbb2 100644 --- a/docs/_pages/releases.md +++ b/docs/_pages/releases.md @@ -13,6 +13,7 @@ sidebar: * Annotated `[Not]MatchRegex(string)` with `[StringSyntax("Regex")]` which IDEs can use to colorize the regular expression argument - [#1816](https://github.com/fluentassertions/fluentassertions/pull/1816) * Added support for .NET6 `DateOnly` struct - [#1844](https://github.com/fluentassertions/fluentassertions/pull/1844) * Added support for .NET6 `TimeOnly` struct - [#1848](https://github.com/fluentassertions/fluentassertions/pull/1848) +* Added the ability to exclude fields & properties marked as non-browsable in the code editor from structural equality equivalency comparisons - [#1807](https://github.com/fluentassertions/fluentassertions/pull/1807) & [#1812](https://github.com/fluentassertions/fluentassertions/pull/1812) ### Fixes * `EnumAssertions.Be` did not determine the caller name - [#1835](https://github.com/fluentassertions/fluentassertions/pull/1835) @@ -24,7 +25,7 @@ sidebar: ## 6.5.1 ### Fixes -* Fix regression introduced in 6.5.0 where `collection.Should().BeInAscendingOrder(x => x)` would fail - [#1802](https://github.com/fluentassertions/fluentassertions/pull/1802) +* Fixed regression introduced in 6.5.0 where `collection.Should().BeInAscendingOrder(x => x)` would fail - [#1802](https://github.com/fluentassertions/fluentassertions/pull/1802) ## 6.5.0