Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the ability to exclude non-browsable members from equivalency tests #1827

Merged
merged 4 commits into from Apr 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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)
jnyrup marked this conversation as resolved.
Show resolved Hide resolved
{
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()
logiclrd marked this conversation as resolved.
Show resolved Hide resolved
{
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 @@ -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) { }
Expand All @@ -823,6 +824,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 @@ -858,6 +861,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 @@ -961,6 +965,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 @@ -989,8 +994,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 @@ -815,6 +815,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 @@ -835,6 +836,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 @@ -870,6 +873,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 @@ -973,6 +977,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 @@ -1001,8 +1006,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