diff --git a/Src/FluentAssertions/Equivalency/Execution/CollectionMemberAssertionOptionsDecorator.cs b/Src/FluentAssertions/Equivalency/Execution/CollectionMemberAssertionOptionsDecorator.cs index 86c06cf999..86ec59e34a 100644 --- a/Src/FluentAssertions/Equivalency/Execution/CollectionMemberAssertionOptionsDecorator.cs +++ b/Src/FluentAssertions/Equivalency/Execution/CollectionMemberAssertionOptionsDecorator.cs @@ -61,6 +61,10 @@ public IEnumerable UserEquivalencySteps public MemberVisibility IncludedFields => inner.IncludedFields; + public bool IgnoreNonBrowsableOnSubject => inner.IgnoreNonBrowsableOnSubject; + + public bool ExcludeNonBrowsableOnExpectation => inner.ExcludeNonBrowsableOnExpectation; + 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..6c0ff7279e 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,18 @@ public interface IEquivalencyAssertionOptions /// MemberVisibility IncludedFields { get; } + /// + /// Gets a value indicating whether members on the subject marked with [] + /// and should be treated as though they don't exist. + /// + bool IgnoreNonBrowsableOnSubject { get; } + + /// + /// Gets a value indicating whether members on the expectation marked with [] + /// and should be excluded. + /// + bool ExcludeNonBrowsableOnExpectation { 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..91f4acc169 100644 --- a/Src/FluentAssertions/Equivalency/IMember.cs +++ b/Src/FluentAssertions/Equivalency/IMember.cs @@ -1,4 +1,6 @@ using System; +using System.ComponentModel; + using FluentAssertions.Common; namespace FluentAssertions.Equivalency @@ -32,5 +34,11 @@ 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 + /// . + /// + bool IsBrowsable { get; } } } diff --git a/Src/FluentAssertions/Equivalency/Matching/MustMatchByNameRule.cs b/Src/FluentAssertions/Equivalency/Matching/MustMatchByNameRule.cs index e386b51d07..64ee48c4ea 100644 --- a/Src/FluentAssertions/Equivalency/Matching/MustMatchByNameRule.cs +++ b/Src/FluentAssertions/Equivalency/Matching/MustMatchByNameRule.cs @@ -36,6 +36,13 @@ public IMember Match(IMember expectedMember, object subject, INode parent, IEqui $"Expectation has {expectedMember.Description} that the other object does not have."); } + if (config.IgnoreNonBrowsableOnSubject && !subjectMember.IsBrowsable) + { + Execute.Assertion.FailWith( + $"Expectation has {expectedMember.Description} that is non-browsable in the other object, and non-browsable " + + $"members on the subject are ignored with the current configuration"); + } + return subjectMember; } 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/Selection/ExcludeNonBrowsableMembersRule.cs b/Src/FluentAssertions/Equivalency/Selection/ExcludeNonBrowsableMembersRule.cs new file mode 100644 index 0000000000..c5ee5f3f83 --- /dev/null +++ b/Src/FluentAssertions/Equivalency/Selection/ExcludeNonBrowsableMembersRule.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace FluentAssertions.Equivalency.Selection +{ + internal class ExcludeNonBrowsableMembersRule : IMemberSelectionRule + { + public bool IncludesMembers => false; + + public IEnumerable SelectMembers(INode currentNode, IEnumerable selectedMembers, MemberSelectionContext context) + { + return selectedMembers.Where(member => member.IsBrowsable).ToList(); + } + } +} diff --git a/Src/FluentAssertions/Equivalency/SelfReferenceEquivalencyAssertionOptions.cs b/Src/FluentAssertions/Equivalency/SelfReferenceEquivalencyAssertionOptions.cs index 8fa4a6cfa7..ca8d9dbe0a 100644 --- a/Src/FluentAssertions/Equivalency/SelfReferenceEquivalencyAssertionOptions.cs +++ b/Src/FluentAssertions/Equivalency/SelfReferenceEquivalencyAssertionOptions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics; using System.Globalization; using System.Linq; @@ -56,6 +57,8 @@ public abstract class SelfReferenceEquivalencyAssertionOptions : IEquival private MemberVisibility includedProperties; private MemberVisibility includedFields; + private bool ignoreNonBrowsableOnSubject; + private bool excludeNonBrowsableOnExpectation; private bool compareRecordsByValue; @@ -80,6 +83,8 @@ protected SelfReferenceEquivalencyAssertionOptions(IEquivalencyAssertionOptions useRuntimeTyping = defaults.UseRuntimeTyping; includedProperties = defaults.IncludedProperties; includedFields = defaults.IncludedFields; + ignoreNonBrowsableOnSubject = defaults.IgnoreNonBrowsableOnSubject; + excludeNonBrowsableOnExpectation = defaults.ExcludeNonBrowsableOnExpectation; compareRecordsByValue = defaults.CompareRecordsByValue; ConversionSelector = defaults.ConversionSelector.Clone(); @@ -115,6 +120,11 @@ IEnumerable IEquivalencyAssertionOptions.SelectionRules yield return new AllFieldsSelectionRule(); } + if (excludeNonBrowsableOnExpectation) + { + yield return new ExcludeNonBrowsableMembersRule(); + } + foreach (IMemberSelectionRule rule in selectionRules) { yield return rule; @@ -162,6 +172,10 @@ IEnumerable IEquivalencyAssertionOptions.SelectionRules MemberVisibility IEquivalencyAssertionOptions.IncludedFields => includedFields; + bool IEquivalencyAssertionOptions.IgnoreNonBrowsableOnSubject => ignoreNonBrowsableOnSubject; + + bool IEquivalencyAssertionOptions.ExcludeNonBrowsableOnExpectation => excludeNonBrowsableOnExpectation; + public bool CompareRecordsByValue => compareRecordsByValue; EqualityStrategy IEquivalencyAssertionOptions.GetEqualityStrategy(Type requestedType) @@ -312,6 +326,29 @@ public TSelf ExcludingProperties() return (TSelf)this; } + /// + /// Instructs the comparison to exclude non-browsable members in the expectation (members set to + /// ). It is not required that they be marked non-browsable in the subject. Use + /// to ignore non-browsable members in the subject. + /// + /// + public TSelf ExcludingNonBrowsableMembers() + { + excludeNonBrowsableOnExpectation = true; + return (TSelf)this; + } + + /// + /// Instructs the comparison to treat non-browsable members in the subject as though they do not exist. If you need to + /// ignore non-browsable members in the expectation, use . + /// + /// + public TSelf IgnoringNonBrowsableMembersOnSubject() + { + ignoreNonBrowsableOnSubject = true; + return (TSelf)this; + } + /// /// Instructs the comparison to respect the expectation's runtime type. /// @@ -698,6 +735,11 @@ public override string ToString() .Append(useRuntimeTyping ? "runtime" : "declared") .AppendLine(" types and members"); + if (ignoreNonBrowsableOnSubject) + { + builder.AppendLine("- Do not consider members marked non-browsable on the subject"); + } + if (isRecursive) { if (allowInfiniteRecursion) @@ -737,6 +779,15 @@ public override string ToString() builder.AppendLine($"- Compare {type} by its members"); } + if (excludeNonBrowsableOnExpectation) + { + builder.AppendLine("- Exclude non-browsable members"); + } + else + { + builder.AppendLine("- Include non-browsable members"); + } + foreach (IMemberSelectionRule rule in selectionRules) { builder.Append("- ").AppendLine(rule.ToString()); diff --git a/Src/FluentAssertions/Equivalency/Steps/StructuralEqualityEquivalencyStep.cs b/Src/FluentAssertions/Equivalency/Steps/StructuralEqualityEquivalencyStep.cs index 845f8a2633..382b686d73 100644 --- a/Src/FluentAssertions/Equivalency/Steps/StructuralEqualityEquivalencyStep.cs +++ b/Src/FluentAssertions/Equivalency/Steps/StructuralEqualityEquivalencyStep.cs @@ -82,6 +82,11 @@ public class StructuralEqualityEquivalencyStep : IEquivalencyStep where match is not null select match; + if (config.IgnoreNonBrowsableOnSubject) + { + query = query.Where(member => member.IsBrowsable); + } + return query.FirstOrDefault(); } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt index 3ac19edafa..269dc70b7c 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt @@ -829,6 +829,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) { } @@ -849,6 +850,8 @@ namespace FluentAssertions.Equivalency FluentAssertions.Equivalency.ConversionSelector ConversionSelector { get; } FluentAssertions.Equivalency.CyclicReferenceHandling CyclicReferenceHandling { get; } FluentAssertions.Equivalency.EnumEquivalencyHandling EnumEquivalencyHandling { get; } + bool ExcludeNonBrowsableOnExpectation { get; } + bool IgnoreNonBrowsableOnSubject { get; } FluentAssertions.Equivalency.MemberVisibility IncludedFields { get; } FluentAssertions.Equivalency.MemberVisibility IncludedProperties { get; } bool IsRecursive { get; } @@ -884,6 +887,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); @@ -987,6 +991,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) { } @@ -1015,8 +1020,10 @@ namespace FluentAssertions.Equivalency public TSelf ExcludingFields() { } public TSelf ExcludingMissingMembers() { } public TSelf ExcludingNestedObjects() { } + public TSelf ExcludingNonBrowsableMembers() { } public TSelf ExcludingProperties() { } public TSelf IgnoringCyclicReferences() { } + public TSelf IgnoringNonBrowsableMembersOnSubject() { } public TSelf Including(System.Linq.Expressions.Expression> predicate) { } public TSelf IncludingAllDeclaredProperties() { } public TSelf IncludingAllRuntimeProperties() { } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt index 36cdeff676..e0af6e27e0 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt @@ -841,6 +841,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) { } @@ -861,6 +862,8 @@ namespace FluentAssertions.Equivalency FluentAssertions.Equivalency.ConversionSelector ConversionSelector { get; } FluentAssertions.Equivalency.CyclicReferenceHandling CyclicReferenceHandling { get; } FluentAssertions.Equivalency.EnumEquivalencyHandling EnumEquivalencyHandling { get; } + bool ExcludeNonBrowsableOnExpectation { get; } + bool IgnoreNonBrowsableOnSubject { get; } FluentAssertions.Equivalency.MemberVisibility IncludedFields { get; } FluentAssertions.Equivalency.MemberVisibility IncludedProperties { get; } bool IsRecursive { get; } @@ -896,6 +899,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); @@ -999,6 +1003,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) { } @@ -1027,8 +1032,10 @@ namespace FluentAssertions.Equivalency public TSelf ExcludingFields() { } public TSelf ExcludingMissingMembers() { } public TSelf ExcludingNestedObjects() { } + public TSelf ExcludingNonBrowsableMembers() { } public TSelf ExcludingProperties() { } public TSelf IgnoringCyclicReferences() { } + public TSelf IgnoringNonBrowsableMembersOnSubject() { } public TSelf Including(System.Linq.Expressions.Expression> predicate) { } public TSelf IncludingAllDeclaredProperties() { } public TSelf IncludingAllRuntimeProperties() { } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt index 7de57cf1c1..bf5918f8da 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt @@ -829,6 +829,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) { } @@ -849,6 +850,8 @@ namespace FluentAssertions.Equivalency FluentAssertions.Equivalency.ConversionSelector ConversionSelector { get; } FluentAssertions.Equivalency.CyclicReferenceHandling CyclicReferenceHandling { get; } FluentAssertions.Equivalency.EnumEquivalencyHandling EnumEquivalencyHandling { get; } + bool ExcludeNonBrowsableOnExpectation { get; } + bool IgnoreNonBrowsableOnSubject { get; } FluentAssertions.Equivalency.MemberVisibility IncludedFields { get; } FluentAssertions.Equivalency.MemberVisibility IncludedProperties { get; } bool IsRecursive { get; } @@ -884,6 +887,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); @@ -987,6 +991,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) { } @@ -1015,8 +1020,10 @@ namespace FluentAssertions.Equivalency public TSelf ExcludingFields() { } public TSelf ExcludingMissingMembers() { } public TSelf ExcludingNestedObjects() { } + public TSelf ExcludingNonBrowsableMembers() { } public TSelf ExcludingProperties() { } public TSelf IgnoringCyclicReferences() { } + public TSelf IgnoringNonBrowsableMembersOnSubject() { } public TSelf Including(System.Linq.Expressions.Expression> predicate) { } public TSelf IncludingAllDeclaredProperties() { } public TSelf IncludingAllRuntimeProperties() { } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt index 9cff0a8291..002d020a8e 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt @@ -829,6 +829,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) { } @@ -849,6 +850,8 @@ namespace FluentAssertions.Equivalency FluentAssertions.Equivalency.ConversionSelector ConversionSelector { get; } FluentAssertions.Equivalency.CyclicReferenceHandling CyclicReferenceHandling { get; } FluentAssertions.Equivalency.EnumEquivalencyHandling EnumEquivalencyHandling { get; } + bool ExcludeNonBrowsableOnExpectation { get; } + bool IgnoreNonBrowsableOnSubject { get; } FluentAssertions.Equivalency.MemberVisibility IncludedFields { get; } FluentAssertions.Equivalency.MemberVisibility IncludedProperties { get; } bool IsRecursive { get; } @@ -884,6 +887,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); @@ -987,6 +991,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) { } @@ -1015,8 +1020,10 @@ namespace FluentAssertions.Equivalency public TSelf ExcludingFields() { } public TSelf ExcludingMissingMembers() { } public TSelf ExcludingNestedObjects() { } + public TSelf ExcludingNonBrowsableMembers() { } public TSelf ExcludingProperties() { } public TSelf IgnoringCyclicReferences() { } + public TSelf IgnoringNonBrowsableMembersOnSubject() { } public TSelf Including(System.Linq.Expressions.Expression> predicate) { } public TSelf IncludingAllDeclaredProperties() { } public TSelf IncludingAllRuntimeProperties() { } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt index 3391e7ae6c..e17cfba5d5 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt @@ -822,6 +822,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) { } @@ -842,6 +843,8 @@ namespace FluentAssertions.Equivalency FluentAssertions.Equivalency.ConversionSelector ConversionSelector { get; } FluentAssertions.Equivalency.CyclicReferenceHandling CyclicReferenceHandling { get; } FluentAssertions.Equivalency.EnumEquivalencyHandling EnumEquivalencyHandling { get; } + bool ExcludeNonBrowsableOnExpectation { get; } + bool IgnoreNonBrowsableOnSubject { get; } FluentAssertions.Equivalency.MemberVisibility IncludedFields { get; } FluentAssertions.Equivalency.MemberVisibility IncludedProperties { get; } bool IsRecursive { get; } @@ -877,6 +880,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); @@ -980,6 +984,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) { } @@ -1008,8 +1013,10 @@ namespace FluentAssertions.Equivalency public TSelf ExcludingFields() { } public TSelf ExcludingMissingMembers() { } public TSelf ExcludingNestedObjects() { } + public TSelf ExcludingNonBrowsableMembers() { } public TSelf ExcludingProperties() { } public TSelf IgnoringCyclicReferences() { } + public TSelf IgnoringNonBrowsableMembersOnSubject() { } public TSelf Including(System.Linq.Expressions.Expression> predicate) { } public TSelf IncludingAllDeclaredProperties() { } public TSelf IncludingAllRuntimeProperties() { } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt index e118ff6c79..3538b07fae 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt @@ -829,6 +829,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) { } @@ -849,6 +850,8 @@ namespace FluentAssertions.Equivalency FluentAssertions.Equivalency.ConversionSelector ConversionSelector { get; } FluentAssertions.Equivalency.CyclicReferenceHandling CyclicReferenceHandling { get; } FluentAssertions.Equivalency.EnumEquivalencyHandling EnumEquivalencyHandling { get; } + bool ExcludeNonBrowsableOnExpectation { get; } + bool IgnoreNonBrowsableOnSubject { get; } FluentAssertions.Equivalency.MemberVisibility IncludedFields { get; } FluentAssertions.Equivalency.MemberVisibility IncludedProperties { get; } bool IsRecursive { get; } @@ -884,6 +887,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); @@ -987,6 +991,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) { } @@ -1015,8 +1020,10 @@ namespace FluentAssertions.Equivalency public TSelf ExcludingFields() { } public TSelf ExcludingMissingMembers() { } public TSelf ExcludingNestedObjects() { } + public TSelf ExcludingNonBrowsableMembers() { } public TSelf ExcludingProperties() { } public TSelf IgnoringCyclicReferences() { } + public TSelf IgnoringNonBrowsableMembersOnSubject() { } public TSelf Including(System.Linq.Expressions.Expression> predicate) { } public TSelf IncludingAllDeclaredProperties() { } public TSelf IncludingAllRuntimeProperties() { } 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 b5d632b059..2d8c82b4c1 100644 --- a/Tests/Benchmarks/UsersOfGetClosedGenericInterfaces.cs +++ b/Tests/Benchmarks/UsersOfGetClosedGenericInterfaces.cs @@ -68,6 +68,10 @@ private class Config : IEquivalencyAssertionOptions public MemberVisibility IncludedFields => throw new NotImplementedException(); + public bool IgnoreNonBrowsableOnSubject => throw new NotImplementedException(); + + public bool ExcludeNonBrowsableOnExpectation => 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..7b87bd9631 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,452 @@ public void Including_an_interface_property_through_inheritance_should_work() .Including(a => a.Value2) .RespectingRuntimeTypes()); } + + [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_explicitly_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_explicitly_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()); + } + + [Fact] + public void When_property_is_non_browsable_only_in_subject_excluding_non_browsable_members_should_not_make_it_succeed() + { + // Arrange + var subject = new ClassWhereMemberThatCouldBeNonBrowsableIsNonBrowsable() { PropertyThatMightBeNonBrowsable = 0 }; + var expectation = new ClassWhereMemberThatCouldBeNonBrowsableIsBrowsable() { PropertyThatMightBeNonBrowsable = 1 }; + + // Act + Action action = + () => subject.Should().BeEquivalentTo(expectation, config => config.ExcludingNonBrowsableMembers()); + + // Assert + action.Should().Throw().WithMessage("Expected property subject.PropertyThatMightBeNonBrowsable to be 1, but found 0.*"); + } + + [Fact] + public void When_property_is_non_browsable_only_in_subject_ignoring_non_browsable_members_on_subject_should_make_it_succeed() + { + // Arrange + var subject = new ClassWhereMemberThatCouldBeNonBrowsableIsNonBrowsable() { PropertyThatMightBeNonBrowsable = 0 }; + var expectation = new ClassWhereMemberThatCouldBeNonBrowsableIsBrowsable() { PropertyThatMightBeNonBrowsable = 1 }; + + // Act & Assert + subject.Should().BeEquivalentTo( + expectation, + config => config.IgnoringNonBrowsableMembersOnSubject().ExcludingMissingMembers()); + } + + [Fact] + public void When_non_browsable_property_on_subject_is_ignored_but_is_present_on_expectation_it_should_fail() + { + // Arrange + var subject = new ClassWhereMemberThatCouldBeNonBrowsableIsNonBrowsable() { PropertyThatMightBeNonBrowsable = 0 }; + var expectation = new ClassWhereMemberThatCouldBeNonBrowsableIsBrowsable() { PropertyThatMightBeNonBrowsable = 1 }; + + // Act + Action action = + () => subject.Should().BeEquivalentTo(expectation, config => config.IgnoringNonBrowsableMembersOnSubject()); + + // Assert + action.Should().Throw().WithMessage( + $"Expectation has * subject.*ThatMightBeNonBrowsable that is non-browsable in the other object, and non-browsable " + + $"members on the subject are ignored with the current configuration*"); + } + + [Fact] + public void When_property_is_non_browsable_only_in_expectation_excluding_non_browsable_members_should_make_it_succeed() + { + // Arrange + var subject = new ClassWhereMemberThatCouldBeNonBrowsableIsBrowsable() { PropertyThatMightBeNonBrowsable = 0 }; + var expectation = new ClassWhereMemberThatCouldBeNonBrowsableIsNonBrowsable() { PropertyThatMightBeNonBrowsable = 1 }; + + // Act & Assert + subject.Should().BeEquivalentTo(expectation, config => config.ExcludingNonBrowsableMembers()); + } + + [Fact] + public void When_field_is_non_browsable_only_in_subject_excluding_non_browsable_members_should_not_make_it_succeed() + { + // Arrange + var subject = new ClassWhereMemberThatCouldBeNonBrowsableIsNonBrowsable() { FieldThatMightBeNonBrowsable = 0 }; + var expectation = new ClassWhereMemberThatCouldBeNonBrowsableIsBrowsable() { FieldThatMightBeNonBrowsable = 1 }; + + // Act + Action action = + () => subject.Should().BeEquivalentTo(expectation, config => config.ExcludingNonBrowsableMembers()); + + // Assert + action.Should().Throw().WithMessage("Expected field subject.FieldThatMightBeNonBrowsable to be 1, but found 0.*"); + } + + [Fact] + public void When_field_is_non_browsable_only_in_subject_ignoring_non_browsable_members_on_subject_should_make_it_succeed() + { + // Arrange + var subject = new ClassWhereMemberThatCouldBeNonBrowsableIsNonBrowsable() { FieldThatMightBeNonBrowsable = 0 }; + var expectation = new ClassWhereMemberThatCouldBeNonBrowsableIsBrowsable() { FieldThatMightBeNonBrowsable = 1 }; + + // Act & Assert + subject.Should().BeEquivalentTo( + expectation, + config => config.IgnoringNonBrowsableMembersOnSubject().ExcludingMissingMembers()); + } + + [Fact] + public void When_field_is_non_browsable_only_in_expectation_excluding_non_browsable_members_should_make_it_succeed() + { + // Arrange + var subject = new ClassWhereMemberThatCouldBeNonBrowsableIsBrowsable() { FieldThatMightBeNonBrowsable = 0 }; + var expectation = new ClassWhereMemberThatCouldBeNonBrowsableIsNonBrowsable() { FieldThatMightBeNonBrowsable = 1 }; + + // Act & Assert + subject.Should().BeEquivalentTo(expectation, config => config.ExcludingNonBrowsableMembers()); + } + + public class NonBrowsableOnOneButMissingFromTheOther + { + [Fact] + public void When_property_is_missing_from_subject_excluding_non_browsable_members_should_make_it_succeed() + { + // Arrange + var subject = + new + { + BrowsableField = 1, + BrowsableProperty = 1, + ExplicitlyBrowsableField = 1, + ExplicitlyBrowsableProperty = 1, + AdvancedBrowsableField = 1, + AdvancedBrowsableProperty = 1, + NonBrowsableField = 2, + /* NonBrowsableProperty missing */ + }; + + var expected = + new ClassWithNonBrowsableMembers + { + BrowsableField = 1, + BrowsableProperty = 1, + ExplicitlyBrowsableField = 1, + ExplicitlyBrowsableProperty = 1, + AdvancedBrowsableField = 1, + AdvancedBrowsableProperty = 1, + NonBrowsableField = 2, + NonBrowsableProperty = 2, + }; + + // Act & Assert + subject.Should().BeEquivalentTo(expected, opt => opt.ExcludingNonBrowsableMembers()); + } + + [Fact] + public void When_field_is_missing_from_subject_excluding_non_browsable_members_should_make_it_succeed() + { + // Arrange + var subject = + new + { + BrowsableField = 1, + BrowsableProperty = 1, + ExplicitlyBrowsableField = 1, + ExplicitlyBrowsableProperty = 1, + AdvancedBrowsableField = 1, + AdvancedBrowsableProperty = 1, + /* NonBrowsableField missing */ + NonBrowsableProperty = 2, + }; + + var expected = + new ClassWithNonBrowsableMembers + { + BrowsableField = 1, + BrowsableProperty = 1, + ExplicitlyBrowsableField = 1, + ExplicitlyBrowsableProperty = 1, + AdvancedBrowsableField = 1, + AdvancedBrowsableProperty = 1, + NonBrowsableField = 2, + NonBrowsableProperty = 2, + }; + + // Act & Assert + subject.Should().BeEquivalentTo(expected, opt => opt.ExcludingNonBrowsableMembers()); + } + + [Fact] + public void When_property_is_missing_from_expectation_excluding_non_browsable_members_should_make_it_succeed() + { + // Arrange + var subject = + new ClassWithNonBrowsableMembers + { + BrowsableField = 1, + BrowsableProperty = 1, + ExplicitlyBrowsableField = 1, + ExplicitlyBrowsableProperty = 1, + AdvancedBrowsableField = 1, + AdvancedBrowsableProperty = 1, + NonBrowsableField = 2, + NonBrowsableProperty = 2, + }; + + var expected = + new + { + BrowsableField = 1, + BrowsableProperty = 1, + ExplicitlyBrowsableField = 1, + ExplicitlyBrowsableProperty = 1, + AdvancedBrowsableField = 1, + AdvancedBrowsableProperty = 1, + NonBrowsableField = 2, + /* NonBrowsableProperty missing */ + }; + + // Act & Assert + subject.Should().BeEquivalentTo(expected, opt => opt.ExcludingNonBrowsableMembers()); + } + + [Fact] + public void When_field_is_missing_from_expectation_excluding_non_browsable_members_should_make_it_succeed() + { + // Arrange + var subject = + new ClassWithNonBrowsableMembers + { + BrowsableField = 1, + BrowsableProperty = 1, + ExplicitlyBrowsableField = 1, + ExplicitlyBrowsableProperty = 1, + AdvancedBrowsableField = 1, + AdvancedBrowsableProperty = 1, + NonBrowsableField = 2, + NonBrowsableProperty = 2, + }; + + var expected = + new + { + BrowsableField = 1, + BrowsableProperty = 1, + ExplicitlyBrowsableField = 1, + ExplicitlyBrowsableProperty = 1, + AdvancedBrowsableField = 1, + AdvancedBrowsableProperty = 1, + /* NonBrowsableField missing */ + NonBrowsableProperty = 2, + }; + + // Act & Assert + subject.Should().BeEquivalentTo(expected, opt => opt.ExcludingNonBrowsableMembers()); + } + + [Fact] + public void When_non_browsable_members_are_excluded_it_should_still_be_possible_to_explicitly_include_non_browsable_field() + { + // Arrange + var subject = + new ClassWithNonBrowsableMembers() + { + NonBrowsableField = 1, + }; + + var expectation = + new ClassWithNonBrowsableMembers() + { + NonBrowsableField = 2, + }; + + // Act + Action action = + () => subject.Should().BeEquivalentTo( + expectation, + opt => opt.IncludingFields().ExcludingNonBrowsableMembers().Including(e => e.NonBrowsableField)); + + // Assert + action.Should().Throw().WithMessage("Expected field subject.NonBrowsableField to be 2, but found 1.*"); + } + + [Fact] + public void When_non_browsable_members_are_excluded_it_should_still_be_possible_to_explicitly_include_non_browsable_property() + { + // Arrange + var subject = + new ClassWithNonBrowsableMembers() + { + NonBrowsableProperty = 1, + }; + + var expectation = + new ClassWithNonBrowsableMembers() + { + NonBrowsableProperty = 2, + }; + + // Act + Action action = + () => subject.Should().BeEquivalentTo( + expectation, + opt => opt.IncludingProperties().ExcludingNonBrowsableMembers().Including(e => e.NonBrowsableProperty)); + + // Assert + action.Should().Throw().WithMessage("Expected property subject.NonBrowsableProperty to be 2, but found 1.*"); + } + } + + private class ClassWithNonBrowsableMembers + { + public int BrowsableField = -1; + + public int BrowsableProperty { get; set; } + + [EditorBrowsable(EditorBrowsableState.Always)] + public int ExplicitlyBrowsableField = -1; + + [EditorBrowsable(EditorBrowsableState.Always)] + public int ExplicitlyBrowsableProperty { get; set; } + + [EditorBrowsable(EditorBrowsableState.Advanced)] + public int AdvancedBrowsableField = -1; + + [EditorBrowsable(EditorBrowsableState.Advanced)] + public int AdvancedBrowsableProperty { get; set; } + + [EditorBrowsable(EditorBrowsableState.Never)] + public int NonBrowsableField = -1; + + [EditorBrowsable(EditorBrowsableState.Never)] + public int NonBrowsableProperty { get; set; } + } + + private class ClassWhereMemberThatCouldBeNonBrowsableIsBrowsable + { + public int BrowsableField = -1; + + public int BrowsableProperty { get; set; } + + public int FieldThatMightBeNonBrowsable = -1; + + public int PropertyThatMightBeNonBrowsable { get; set; } + } + + private class ClassWhereMemberThatCouldBeNonBrowsableIsNonBrowsable + { + public int BrowsableField = -1; + + public int BrowsableProperty { get; set; } + + [EditorBrowsable(EditorBrowsableState.Never)] + public int FieldThatMightBeNonBrowsable = -1; + + [EditorBrowsable(EditorBrowsableState.Never)] + public int PropertyThatMightBeNonBrowsable { 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 9c28ca607b..7a998d4515 100644 --- a/docs/_pages/releases.md +++ b/docs/_pages/releases.md @@ -21,6 +21,7 @@ sidebar: * Added `NotBe` for nullable boolean values - [#1865](https://github.com/fluentassertions/fluentassertions/pull/1865) * Added a new overload to `MatchRegex()` to assert on the number of regex matches - [#1869](https://github.com/fluentassertions/fluentassertions/pull/1869) * Added difference to numeric assertion failure messages - [#1859](https://github.com/fluentassertions/fluentassertions/pull/1859) +* Added the ability to exclude fields & properties marked as non-browsable in the code editor from structural equality 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) @@ -34,7 +35,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