Skip to content

Commit

Permalink
Add the ability to exclude non-browsable members from equivalency tes…
Browse files Browse the repository at this point in the history
…ts (#1827)
  • Loading branch information
logiclrd committed Apr 16, 2022
1 parent 6ecd349 commit e87aa94
Show file tree
Hide file tree
Showing 21 changed files with 681 additions and 2 deletions.
Expand Up @@ -61,6 +61,10 @@ public IEnumerable<IEquivalencyStep> 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)
Expand Down
15 changes: 15 additions & 0 deletions Src/FluentAssertions/Equivalency/Field.cs
@@ -1,4 +1,5 @@
using System;
using System.ComponentModel;
using System.Reflection;
using FluentAssertions.Common;

Expand All @@ -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)
Expand Down Expand Up @@ -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<EditorBrowsableAttribute>() is not { State: EditorBrowsableState.Never };
}

return isBrowsable.Value;
}
}
}
}
14 changes: 14 additions & 0 deletions 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
Expand Down Expand Up @@ -71,6 +73,18 @@ public interface IEquivalencyAssertionOptions
/// </summary>
MemberVisibility IncludedFields { get; }

/// <summary>
/// Gets a value indicating whether members on the subject marked with [<see cref="EditorBrowsableAttribute"/>]
/// and <see cref="EditorBrowsableState.Never"/> should be treated as though they don't exist.
/// </summary>
bool IgnoreNonBrowsableOnSubject { get; }

/// <summary>
/// Gets a value indicating whether members on the expectation marked with [<see cref="EditorBrowsableAttribute"/>]
/// and <see cref="EditorBrowsableState.Never"/> should be excluded.
/// </summary>
bool ExcludeNonBrowsableOnExpectation { get; }

/// <summary>
/// Gets a value indicating whether records should be compared by value instead of their members
/// </summary>
Expand Down
8 changes: 8 additions & 0 deletions Src/FluentAssertions/Equivalency/IMember.cs
@@ -1,4 +1,6 @@
using System;
using System.ComponentModel;

using FluentAssertions.Common;

namespace FluentAssertions.Equivalency
Expand Down Expand Up @@ -32,5 +34,11 @@ public interface IMember : INode
/// Gets the access modifier for the setter of this member.
/// </summary>
CSharpAccessModifier SetterAccessibility { get; }

/// <summary>
/// Gets a value indicating whether the member is browsable in the source code editor. This is controlled with
/// <see cref="EditorBrowsableAttribute"/>.
/// </summary>
bool IsBrowsable { get; }
}
}
Expand Up @@ -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;
}

Expand Down
15 changes: 15 additions & 0 deletions Src/FluentAssertions/Equivalency/Property.cs
@@ -1,4 +1,5 @@
using System;
using System.ComponentModel;
using System.Reflection;
using FluentAssertions.Common;

Expand All @@ -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)
Expand Down Expand Up @@ -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<EditorBrowsableAttribute>() is not { State: EditorBrowsableState.Never };
}

return isBrowsable.Value;
}
}
}
}
@@ -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<IMember> SelectMembers(INode currentNode, IEnumerable<IMember> selectedMembers, MemberSelectionContext context)
{
return selectedMembers.Where(member => member.IsBrowsable).ToList();
}
}
}
@@ -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;
Expand Down Expand Up @@ -56,6 +57,8 @@ public abstract class SelfReferenceEquivalencyAssertionOptions<TSelf> : IEquival

private MemberVisibility includedProperties;
private MemberVisibility includedFields;
private bool ignoreNonBrowsableOnSubject;
private bool excludeNonBrowsableOnExpectation;

private bool compareRecordsByValue;

Expand All @@ -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();
Expand Down Expand Up @@ -115,6 +120,11 @@ IEnumerable<IMemberSelectionRule> IEquivalencyAssertionOptions.SelectionRules
yield return new AllFieldsSelectionRule();
}

if (excludeNonBrowsableOnExpectation)
{
yield return new ExcludeNonBrowsableMembersRule();
}

foreach (IMemberSelectionRule rule in selectionRules)
{
yield return rule;
Expand Down Expand Up @@ -162,6 +172,10 @@ IEnumerable<IMemberSelectionRule> IEquivalencyAssertionOptions.SelectionRules

MemberVisibility IEquivalencyAssertionOptions.IncludedFields => includedFields;

bool IEquivalencyAssertionOptions.IgnoreNonBrowsableOnSubject => ignoreNonBrowsableOnSubject;

bool IEquivalencyAssertionOptions.ExcludeNonBrowsableOnExpectation => excludeNonBrowsableOnExpectation;

public bool CompareRecordsByValue => compareRecordsByValue;

EqualityStrategy IEquivalencyAssertionOptions.GetEqualityStrategy(Type requestedType)
Expand Down Expand Up @@ -312,6 +326,29 @@ public TSelf ExcludingProperties()
return (TSelf)this;
}

/// <summary>
/// Instructs the comparison to exclude non-browsable members in the expectation (members set to
/// <see cref="EditorBrowsableState.Never"/>). It is not required that they be marked non-browsable in the subject. Use
/// <see cref="IgnoringNonBrowsableMembersOnSubject"/> to ignore non-browsable members in the subject.
/// </summary>
/// <returns></returns>
public TSelf ExcludingNonBrowsableMembers()
{
excludeNonBrowsableOnExpectation = true;
return (TSelf)this;
}

/// <summary>
/// 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 <see cref="ExcludingNonBrowsableMembers"/>.
/// </summary>
/// <returns></returns>
public TSelf IgnoringNonBrowsableMembersOnSubject()
{
ignoreNonBrowsableOnSubject = true;
return (TSelf)this;
}

/// <summary>
/// Instructs the comparison to respect the expectation's runtime type.
/// </summary>
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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());
Expand Down
Expand Up @@ -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();
}

Expand Down
Expand Up @@ -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) { }
Expand All @@ -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; }
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) { }
Expand Down Expand Up @@ -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<System.Func<FluentAssertions.Equivalency.IMemberInfo, bool>> predicate) { }
public TSelf IncludingAllDeclaredProperties() { }
public TSelf IncludingAllRuntimeProperties() { }
Expand Down
Expand Up @@ -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) { }
Expand All @@ -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; }
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) { }
Expand Down Expand Up @@ -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<System.Func<FluentAssertions.Equivalency.IMemberInfo, bool>> predicate) { }
public TSelf IncludingAllDeclaredProperties() { }
public TSelf IncludingAllRuntimeProperties() { }
Expand Down

0 comments on commit e87aa94

Please sign in to comment.