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