diff --git a/Directory.Build.props b/Directory.Build.props
index fcab84345b..5c2d966030 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -2,6 +2,7 @@
11.0
false
+ $(WarningsNotAsErrors);NU1902;NU1903
true
@@ -13,7 +14,7 @@
true
- latest
+ 7.0
All
true
diff --git a/README.md b/README.md
index f9fdb27e17..5b93854331 100644
--- a/README.md
+++ b/README.md
@@ -32,14 +32,13 @@ This is a special set of tests that use the [Verify](https://github.com/VerifyTe
If you've verified the changes and decided they are valid, you can accept them using `AcceptApiChanges.ps1` or `AcceptApiChanges.sh`. Alternatively, you can use the [Verify Support](https://plugins.jetbrains.com/plugin/17240-verify-support) plug-in to compare the changes and accept them right from inside Rider. See also the [Contribution Guidelines](CONTRIBUTING.md).
# Powered By
-
With support from the following public [sponsors](https://github.com/sponsors/fluentassertions)
-
-
+
+
+
+
-
-
-
+
diff --git a/Src/FluentAssertions/CallerIdentifier.cs b/Src/FluentAssertions/CallerIdentifier.cs
index ff9c66f547..7c1b385a5d 100644
--- a/Src/FluentAssertions/CallerIdentifier.cs
+++ b/Src/FluentAssertions/CallerIdentifier.cs
@@ -26,9 +26,7 @@ public static string DetermineCallerIdentity()
{
var stack = new StackTrace(fNeedFileInfo: true);
- var allStackFrames = stack.GetFrames()
- .Where(frame => frame is not null && !IsCompilerServices(frame))
- .ToArray();
+ var allStackFrames = GetFrames(stack);
int searchStart = allStackFrames.Length - 1;
@@ -55,7 +53,8 @@ public static string DetermineCallerIdentity()
if (frame.GetMethod() is not null
&& !IsDynamic(frame)
&& !IsDotNet(frame)
- && !IsCustomAssertion(frame))
+ && !IsCustomAssertion(frame)
+ && !IsCurrentAssembly(frame))
{
caller = ExtractVariableNameFrom(frame);
break;
@@ -81,9 +80,7 @@ public StackFrameReference()
{
var stack = new StackTrace();
- var allStackFrames = stack.GetFrames()
- .Where(frame => frame is not null && !IsCompilerServices(frame))
- .ToArray();
+ var allStackFrames = GetFrames(stack);
int firstUserCodeFrameIndex = 0;
@@ -114,9 +111,7 @@ internal static IDisposable OverrideStackSearchUsingCurrentScope()
internal static bool OnlyOneFluentAssertionScopeOnCallStack()
{
- var allStackFrames = new StackTrace().GetFrames()
- .Where(frame => frame is not null && !IsCompilerServices(frame))
- .ToArray();
+ var allStackFrames = GetFrames(new StackTrace());
int firstNonFluentAssertionsStackFrameIndex = Array.FindIndex(
allStackFrames,
@@ -256,4 +251,18 @@ private static bool IsBooleanLiteral(string candidate)
{
return candidate is "true" or "false";
}
+
+ private static StackFrame[] GetFrames(StackTrace stack)
+ {
+ var frames = stack.GetFrames();
+#if !NETCOREAPP2_1_OR_GREATER
+ if (frames == null)
+ {
+ return Array.Empty();
+ }
+#endif
+ return frames
+ .Where(frame => !IsCompilerServices(frame))
+ .ToArray();
+ }
}
diff --git a/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs b/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs
index c3fcf0a65e..5c393ae628 100644
--- a/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs
+++ b/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs
@@ -286,15 +286,16 @@ public AndConstraint AllBeOfType(Type expectedType, string because
///
public AndConstraint BeEmpty(string because = "", params object[] becauseArgs)
{
+ var singleItemArray = Subject?.Take(1).ToArray();
Execute.Assertion
.BecauseOf(because, becauseArgs)
.WithExpectation("Expected {context:collection} to be empty{reason}, ")
- .Given(() => Subject)
+ .Given(() => singleItemArray)
.ForCondition(subject => subject is not null)
.FailWith("but found .")
.Then
- .ForCondition(subject => !subject.Any())
- .FailWith("but found {0}.", Subject)
+ .ForCondition(subject => subject.Length == 0)
+ .FailWith("but found at least one item {0}.", singleItemArray)
.Then
.ClearExpectation();
@@ -639,13 +640,14 @@ public AndConstraint> BeInDescendingOrder(Func
public AndConstraint BeNullOrEmpty(string because = "", params object[] becauseArgs)
{
- var nullOrEmpty = Subject is null || !Subject.Any();
+ var singleItemArray = Subject?.Take(1).ToArray();
+ var nullOrEmpty = singleItemArray is null || singleItemArray.Length == 0;
Execute.Assertion.ForCondition(nullOrEmpty)
.BecauseOf(because, becauseArgs)
.FailWith(
- "Expected {context:collection} to be null or empty{reason}, but found {0}.",
- Subject);
+ "Expected {context:collection} to be null or empty{reason}, but found at least one item {0}.",
+ singleItemArray);
return new AndConstraint((TAssertions)this);
}
@@ -811,13 +813,18 @@ public AndConstraint Contain(IEnumerable expected, string becaus
}
///
- /// Asserts that a collection of objects contains at least one object equivalent to another object.
+ /// Asserts that at least one element in the collection is equivalent to .
///
///
- /// Objects within the collection are equivalent to the expected object when both object graphs have equally named properties with the same
- /// value, irrespective of the type of those objects. Two properties are also equal if one type can be converted to another
- /// and the result is equal.
+ ///
+ /// Important: You cannot use this method to assert whether a subset of the collection is equivalent to the .
+ /// This usually means that the expectation is meant to be a single item.
+ ///
+ ///
+ /// By default, objects within the collection are seen as equivalent to the expected object when both object graphs have equally named properties with the same
+ /// value, irrespective of the type of those objects.
/// Notice that actual behavior is determined by the global defaults managed by .
+ ///
///
/// The expected element.
///
@@ -834,13 +841,18 @@ public AndWhichConstraint ContainEquivalentOf(TExp
}
///
- /// Asserts that a collection of objects contains at least one object equivalent to another object.
+ /// Asserts that at least one element in the collection is equivalent to .
///
///
- /// Objects within the collection are equivalent to the expected object when both object graphs have equally named properties with the same
- /// value, irrespective of the type of those objects. Two properties are also equal if one type can be converted to another
- /// and the result is equal.
+ ///
+ /// Important: You cannot use this method to assert whether a subset of the collection is equivalent to the .
+ /// This usually means that the expectation is meant to be a single item.
+ ///
+ ///
+ /// By default, objects within the collection are seen as equivalent to the expected object when both object graphs have equally named properties with the same
+ /// value, irrespective of the type of those objects.
/// Notice that actual behavior is determined by the global defaults managed by .
+ ///
///
/// The expected element.
///
@@ -1844,7 +1856,7 @@ public AndConstraint NotBeEquivalentTo(IEnumerable NotContain(IEnumerable unexpected, string b
}
///
- /// Asserts that a collection of objects does not contain any object equivalent to another object.
+ /// Asserts that no element in the collection is equivalent to .
///
///
- /// Objects within the collection are equivalent to the expected object when both object graphs have equally named properties with the same
- /// value, irrespective of the type of those objects. Two properties are also equal if one type can be converted to another
- /// and the result is equal.
+ ///
+ /// Important: You cannot use this method to assert whether a subset of the collection is not equivalent to the .
+ /// This usually means that the expectation is meant to be a single item.
+ ///
+ ///
+ /// By default, objects within the collection are seen as not equivalent to the expected object when both object graphs have unequally named properties with the same
+ /// value, irrespective of the type of those objects.
/// Notice that actual behavior is determined by the global defaults managed by .
+ ///
///
/// The unexpected element.
///
@@ -2315,13 +2332,18 @@ public AndConstraint NotContainEquivalentOf(TExpectat
}
///
- /// Asserts that a collection of objects does not contain any object equivalent to another object.
+ /// Asserts that no element in the collection is equivalent to .
///
///
- /// Objects within the collection are equivalent to the expected object when both object graphs have equally named properties with the same
- /// value, irrespective of the type of those objects. Two properties are also equal if one type can be converted to another
- /// and the result is equal.
+ ///
+ /// Important: You cannot use this method to assert whether a subset of the collection is not equivalent to the .
+ /// This usually means that the expectation is meant to be a single item.
+ ///
+ ///
+ /// By default, objects within the collection are seen as not equivalent to the expected object when both object graphs have unequally named properties with the same
+ /// value, irrespective of the type of those objects.
/// Notice that actual behavior is determined by the global defaults managed by .
+ ///
///
/// The unexpected element.
///
diff --git a/Src/FluentAssertions/Common/TypeExtensions.cs b/Src/FluentAssertions/Common/TypeExtensions.cs
index 25d3a58931..a84af752ad 100644
--- a/Src/FluentAssertions/Common/TypeExtensions.cs
+++ b/Src/FluentAssertions/Common/TypeExtensions.cs
@@ -15,9 +15,6 @@ internal static class TypeExtensions
private const BindingFlags PublicInstanceMembersFlag =
BindingFlags.Public | BindingFlags.Instance;
- private const BindingFlags AllInstanceMembersFlag =
- BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
-
private const BindingFlags AllStaticAndInstanceMembersFlag =
PublicInstanceMembersFlag | BindingFlags.NonPublic | BindingFlags.Static;
@@ -176,60 +173,49 @@ public static bool OverridesEquals(this Type type)
}
///
- /// Finds the property by a case-sensitive name.
+ /// Finds the property by a case-sensitive name and with a certain visibility.
///
+ ///
+ /// If both a normal property and one that was implemented through an explicit interface implementation with the same name exist,
+ /// then the normal property will be returned.
+ ///
///
/// Returns if no such property exists.
///
- public static PropertyInfo FindProperty(this Type type, string propertyName)
+ public static PropertyInfo FindProperty(this Type type, string propertyName, MemberVisibility memberVisibility)
{
- while (type != typeof(object))
- {
- if (type.GetProperty(propertyName, AllInstanceMembersFlag | BindingFlags.DeclaredOnly) is { } property)
- {
- return property;
- }
+ var properties = type.GetProperties(memberVisibility);
- type = type.BaseType;
- }
-
- return null;
+ return Array.Find(properties, p =>
+ p.Name == propertyName || p.Name.EndsWith("." + propertyName, StringComparison.Ordinal));
}
///
/// Finds the field by a case-sensitive name.
///
///
- /// Returns if no such property exists.
+ /// Returns if no such field exists.
///
- public static FieldInfo FindField(this Type type, string fieldName)
+ public static FieldInfo FindField(this Type type, string fieldName, MemberVisibility memberVisibility)
{
- while (type != typeof(object))
- {
- if (type.GetField(fieldName, AllInstanceMembersFlag | BindingFlags.DeclaredOnly) is { } field)
- {
- return field;
- }
+ var fields = type.GetFields(memberVisibility);
- type = type.BaseType;
- }
-
- return null;
+ return Array.Find(fields, p => p.Name == fieldName);
}
- public static MemberInfo[] GetNonPrivateMembers(this Type typeToReflect, MemberVisibility visibility)
+ public static MemberInfo[] GetMembers(this Type typeToReflect, MemberVisibility visibility)
{
- return GetTypeReflectorFor(typeToReflect, visibility).NonPrivateMembers;
+ return GetTypeReflectorFor(typeToReflect, visibility).Members;
}
- public static PropertyInfo[] GetNonPrivateProperties(this Type typeToReflect, MemberVisibility visibility)
+ public static PropertyInfo[] GetProperties(this Type typeToReflect, MemberVisibility visibility)
{
- return GetTypeReflectorFor(typeToReflect, visibility).NonPrivateProperties;
+ return GetTypeReflectorFor(typeToReflect, visibility).Properties;
}
- public static FieldInfo[] GetNonPrivateFields(this Type typeToReflect, MemberVisibility visibility)
+ public static FieldInfo[] GetFields(this Type typeToReflect, MemberVisibility visibility)
{
- return GetTypeReflectorFor(typeToReflect, visibility).NonPrivateFields;
+ return GetTypeReflectorFor(typeToReflect, visibility).Fields;
}
private static TypeMemberReflector GetTypeReflectorFor(Type typeToReflect, MemberVisibility visibility)
@@ -318,8 +304,11 @@ public static bool IsIndexer(this PropertyInfo member)
public static ConstructorInfo GetConstructor(this Type type, IEnumerable parameterTypes)
{
+ const BindingFlags allInstanceMembersFlag =
+ BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
+
return type
- .GetConstructors(AllInstanceMembersFlag)
+ .GetConstructors(allInstanceMembersFlag)
.SingleOrDefault(m => m.GetParameters().Select(p => p.ParameterType).SequenceEqual(parameterTypes));
}
@@ -362,9 +351,7 @@ private static bool IsImplementationOfOpenGeneric(this Type type, Type definitio
// check subject's interfaces against definition
return type.GetInterfaces()
- .Where(i => i.IsGenericType)
- .Select(i => i.GetGenericTypeDefinition())
- .Contains(definition);
+ .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == definition);
}
public static bool IsDerivedFromOpenGeneric(this Type type, Type definition)
diff --git a/Src/FluentAssertions/Common/TypeMemberReflector.cs b/Src/FluentAssertions/Common/TypeMemberReflector.cs
index 1706e4e32e..ea65969cdf 100644
--- a/Src/FluentAssertions/Common/TypeMemberReflector.cs
+++ b/Src/FluentAssertions/Common/TypeMemberReflector.cs
@@ -16,67 +16,86 @@ internal sealed class TypeMemberReflector
public TypeMemberReflector(Type typeToReflect, MemberVisibility visibility)
{
- NonPrivateProperties = LoadNonPrivateProperties(typeToReflect, visibility);
- NonPrivateFields = LoadNonPrivateFields(typeToReflect, visibility);
- NonPrivateMembers = NonPrivateProperties.Concat(NonPrivateFields).ToArray();
+ Properties = LoadProperties(typeToReflect, visibility);
+ Fields = LoadFields(typeToReflect, visibility);
+ Members = Properties.Concat(Fields).ToArray();
}
- public MemberInfo[] NonPrivateMembers { get; }
+ public MemberInfo[] Members { get; }
- public PropertyInfo[] NonPrivateProperties { get; }
+ public PropertyInfo[] Properties { get; }
- public FieldInfo[] NonPrivateFields { get; }
+ public FieldInfo[] Fields { get; }
- private static PropertyInfo[] LoadNonPrivateProperties(Type typeToReflect, MemberVisibility visibility)
+ private static PropertyInfo[] LoadProperties(Type typeToReflect, MemberVisibility visibility)
{
- IEnumerable query =
- from propertyInfo in GetPropertiesFromHierarchy(typeToReflect, visibility)
- where HasNonPrivateGetter(propertyInfo)
- where !propertyInfo.IsIndexer()
- select propertyInfo;
+ List query = GetPropertiesFromHierarchy(typeToReflect, visibility);
return query.ToArray();
}
private static List GetPropertiesFromHierarchy(Type typeToReflect, MemberVisibility memberVisibility)
{
- bool includeInternals = memberVisibility.HasFlag(MemberVisibility.Internal);
+ bool includeInternal = memberVisibility.HasFlag(MemberVisibility.Internal);
+ bool includeExplicitlyImplemented = memberVisibility.HasFlag(MemberVisibility.ExplicitlyImplemented);
return GetMembersFromHierarchy(typeToReflect, type =>
{
- return type
- .GetProperties(AllInstanceMembersFlag | BindingFlags.DeclaredOnly)
- .Where(property => property.GetMethod?.IsPrivate == false)
- .Where(property => includeInternals || property.GetMethod is { IsAssembly: false, IsFamilyOrAssembly: false })
- .ToArray();
+ return
+ from p in type.GetProperties(AllInstanceMembersFlag | BindingFlags.DeclaredOnly)
+ where p.GetMethod is { } getMethod
+ && (IsPublic(getMethod) || (includeExplicitlyImplemented && IsExplicitlyImplemented(getMethod)))
+ && (includeInternal || !IsInternal(getMethod))
+ && !p.IsIndexer()
+ orderby IsExplicitImplementation(p)
+ select p;
});
}
- private static FieldInfo[] LoadNonPrivateFields(Type typeToReflect, MemberVisibility visibility)
+ private static bool IsPublic(MethodBase getMethod) =>
+ !getMethod.IsPrivate && !getMethod.IsFamily && !getMethod.IsFamilyAndAssembly;
+
+ private static bool IsExplicitlyImplemented(MethodBase getMethod) =>
+ getMethod.IsPrivate && getMethod.IsFinal;
+
+ private static bool IsInternal(MethodBase getMethod) =>
+ getMethod.IsAssembly || getMethod.IsFamilyOrAssembly;
+
+ private static bool IsExplicitImplementation(PropertyInfo property)
+ {
+ return property.GetMethod!.IsPrivate &&
+ property.SetMethod?.IsPrivate != false &&
+ property.Name.Contains('.', StringComparison.Ordinal);
+ }
+
+ private static FieldInfo[] LoadFields(Type typeToReflect, MemberVisibility visibility)
{
- IEnumerable query =
- from fieldInfo in GetFieldsFromHierarchy(typeToReflect, visibility)
- where !fieldInfo.IsPrivate
- where !fieldInfo.IsFamily
- select fieldInfo;
+ List query = GetFieldsFromHierarchy(typeToReflect, visibility);
return query.ToArray();
}
private static List GetFieldsFromHierarchy(Type typeToReflect, MemberVisibility memberVisibility)
{
- bool includeInternals = memberVisibility.HasFlag(MemberVisibility.Internal);
+ bool includeInternal = memberVisibility.HasFlag(MemberVisibility.Internal);
return GetMembersFromHierarchy(typeToReflect, type =>
{
return type
.GetFields(AllInstanceMembersFlag)
- .Where(field => !field.IsPrivate)
- .Where(field => includeInternals || (!field.IsAssembly && !field.IsFamilyOrAssembly))
- .ToArray();
+ .Where(field => IsPublic(field))
+ .Where(field => includeInternal || !IsInternal(field));
});
}
+ private static bool IsPublic(FieldInfo field) =>
+ !field.IsPrivate && !field.IsFamily && !field.IsFamilyAndAssembly;
+
+ private static bool IsInternal(FieldInfo field)
+ {
+ return field.IsAssembly || field.IsFamilyOrAssembly;
+ }
+
private static List GetMembersFromHierarchy(
Type typeToReflect,
Func> getMembers)
@@ -147,10 +166,4 @@ private static List GetClassMembers(Type typeToReflect
return members;
}
-
- private static bool HasNonPrivateGetter(PropertyInfo propertyInfo)
- {
- MethodInfo getMethod = propertyInfo.GetGetMethod(nonPublic: true);
- return getMethod is { IsPrivate: false, IsFamily: false };
- }
}
diff --git a/Src/FluentAssertions/Equivalency/EqualityStrategyProvider.cs b/Src/FluentAssertions/Equivalency/EqualityStrategyProvider.cs
new file mode 100644
index 0000000000..21903e2cd2
--- /dev/null
+++ b/Src/FluentAssertions/Equivalency/EqualityStrategyProvider.cs
@@ -0,0 +1,126 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Text;
+using FluentAssertions.Common;
+using JetBrains.Annotations;
+
+namespace FluentAssertions.Equivalency;
+
+internal sealed class EqualityStrategyProvider
+{
+ private readonly List referenceTypes = new();
+ private readonly List valueTypes = new();
+ private readonly ConcurrentDictionary typeCache = new();
+
+ [CanBeNull]
+ private readonly Func defaultStrategy;
+
+ private bool? compareRecordsByValue;
+
+ public EqualityStrategyProvider()
+ {
+ }
+
+ public EqualityStrategyProvider(Func defaultStrategy)
+ {
+ this.defaultStrategy = defaultStrategy;
+ }
+
+ public bool? CompareRecordsByValue
+ {
+ get => compareRecordsByValue;
+ set
+ {
+ compareRecordsByValue = value;
+ typeCache.Clear();
+ }
+ }
+
+ public EqualityStrategy GetEqualityStrategy(Type type)
+ {
+ // As the valueFactory parameter captures instance members,
+ // be aware if the cache must be cleared on mutating the members.
+ return typeCache.GetOrAdd(type, typeKey =>
+ {
+ if (!typeKey.IsPrimitive && referenceTypes.Count > 0 && referenceTypes.Exists(t => typeKey.IsSameOrInherits(t)))
+ {
+ return EqualityStrategy.ForceMembers;
+ }
+ else if (valueTypes.Count > 0 && valueTypes.Exists(t => typeKey.IsSameOrInherits(t)))
+ {
+ return EqualityStrategy.ForceEquals;
+ }
+ else if (!typeKey.IsPrimitive && referenceTypes.Count > 0 &&
+ referenceTypes.Exists(t => typeKey.IsAssignableToOpenGeneric(t)))
+ {
+ return EqualityStrategy.ForceMembers;
+ }
+ else if (valueTypes.Count > 0 && valueTypes.Exists(t => typeKey.IsAssignableToOpenGeneric(t)))
+ {
+ return EqualityStrategy.ForceEquals;
+ }
+ else if ((compareRecordsByValue.HasValue || defaultStrategy is null) && typeKey.IsRecord())
+ {
+ return compareRecordsByValue is true ? EqualityStrategy.ForceEquals : EqualityStrategy.ForceMembers;
+ }
+ else if (defaultStrategy is not null)
+ {
+ return defaultStrategy(typeKey);
+ }
+
+ return typeKey.HasValueSemantics() ? EqualityStrategy.Equals : EqualityStrategy.Members;
+ });
+ }
+
+ public bool AddReferenceType(Type type)
+ {
+ if (valueTypes.Exists(t => type.IsSameOrInherits(t)))
+ {
+ return false;
+ }
+
+ referenceTypes.Add(type);
+ typeCache.Clear();
+ return true;
+ }
+
+ public bool AddValueType(Type type)
+ {
+ if (referenceTypes.Exists(t => type.IsSameOrInherits(t)))
+ {
+ return false;
+ }
+
+ valueTypes.Add(type);
+ typeCache.Clear();
+ return true;
+ }
+
+ public override string ToString()
+ {
+ var builder = new StringBuilder();
+
+ if (compareRecordsByValue is true)
+ {
+ builder.AppendLine("- Compare records by value");
+ }
+ else
+ {
+ builder.AppendLine("- Compare records by their members");
+ }
+
+ foreach (Type valueType in valueTypes)
+ {
+ builder.AppendLine(CultureInfo.InvariantCulture, $"- Compare {valueType} by value");
+ }
+
+ foreach (Type type in referenceTypes)
+ {
+ builder.AppendLine(CultureInfo.InvariantCulture, $"- Compare {type} by its members");
+ }
+
+ return builder.ToString();
+ }
+}
diff --git a/Src/FluentAssertions/Equivalency/EquivalencyValidator.cs b/Src/FluentAssertions/Equivalency/EquivalencyValidator.cs
index 332c2a185f..233e558afa 100644
--- a/Src/FluentAssertions/Equivalency/EquivalencyValidator.cs
+++ b/Src/FluentAssertions/Equivalency/EquivalencyValidator.cs
@@ -66,14 +66,16 @@ private void TryToProveNodesAreEquivalent(Comparands comparands, IEquivalencyVal
{
using var _ = context.Tracer.WriteBlock(node => node.Description);
- Func getMessage = step => _ => $"Equivalency was proven by {step.GetType().Name}";
-
foreach (IEquivalencyStep step in AssertionOptions.EquivalencyPlan)
{
var result = step.Handle(comparands, context, this);
if (result == EquivalencyResult.AssertionCompleted)
{
- context.Tracer.WriteLine(getMessage(step));
+ context.Tracer.WriteLine(GetMessage(step));
+
+ static GetTraceMessage GetMessage(IEquivalencyStep step) =>
+ _ => $"Equivalency was proven by {step.GetType().Name}";
+
return;
}
}
diff --git a/Src/FluentAssertions/Equivalency/Matching/MustMatchByNameRule.cs b/Src/FluentAssertions/Equivalency/Matching/MustMatchByNameRule.cs
index e06f7ea6cd..62a68d4fb9 100644
--- a/Src/FluentAssertions/Equivalency/Matching/MustMatchByNameRule.cs
+++ b/Src/FluentAssertions/Equivalency/Matching/MustMatchByNameRule.cs
@@ -15,19 +15,20 @@ public IMember Match(IMember expectedMember, object subject, INode parent, IEqui
if (options.IncludedProperties != MemberVisibility.None)
{
- PropertyInfo propertyInfo = subject.GetType().FindProperty(expectedMember.Name);
+ PropertyInfo propertyInfo = subject.GetType().FindProperty(
+ expectedMember.Name,
+ options.IncludedProperties | MemberVisibility.ExplicitlyImplemented);
+
subjectMember = propertyInfo is not null && !propertyInfo.IsIndexer() ? new Property(propertyInfo, parent) : null;
}
if (subjectMember is null && options.IncludedFields != MemberVisibility.None)
{
- FieldInfo fieldInfo = subject.GetType().FindField(expectedMember.Name);
- subjectMember = fieldInfo is not null ? new Field(fieldInfo, parent) : null;
- }
+ FieldInfo fieldInfo = subject.GetType().FindField(
+ expectedMember.Name,
+ options.IncludedFields);
- if ((subjectMember is null || !options.UseRuntimeTyping) && ExpectationImplementsMemberExplicitly(subject, expectedMember))
- {
- subjectMember = expectedMember;
+ subjectMember = fieldInfo is not null ? new Field(fieldInfo, parent) : null;
}
if (subjectMember is null)
@@ -49,11 +50,6 @@ public IMember Match(IMember expectedMember, object subject, INode parent, IEqui
return subjectMember;
}
- private static bool ExpectationImplementsMemberExplicitly(object expectation, IMember subjectMember)
- {
- return subjectMember.DeclaringType.IsInstanceOfType(expectation);
- }
-
///
/// 2
public override string ToString()
diff --git a/Src/FluentAssertions/Equivalency/Matching/TryMatchByNameRule.cs b/Src/FluentAssertions/Equivalency/Matching/TryMatchByNameRule.cs
index e44c0e6779..36f817acb8 100644
--- a/Src/FluentAssertions/Equivalency/Matching/TryMatchByNameRule.cs
+++ b/Src/FluentAssertions/Equivalency/Matching/TryMatchByNameRule.cs
@@ -10,14 +10,20 @@ internal class TryMatchByNameRule : IMemberMatchingRule
{
public IMember Match(IMember expectedMember, object subject, INode parent, IEquivalencyAssertionOptions options)
{
- PropertyInfo property = subject.GetType().FindProperty(expectedMember.Name);
-
- if (property is not null && !property.IsIndexer())
+ if (options.IncludedProperties != MemberVisibility.None)
{
- return new Property(property, parent);
+ PropertyInfo property = subject.GetType().FindProperty(expectedMember.Name,
+ options.IncludedProperties | MemberVisibility.ExplicitlyImplemented);
+
+ if (property is not null && !property.IsIndexer())
+ {
+ return new Property(property, parent);
+ }
}
- FieldInfo field = subject.GetType().FindField(expectedMember.Name);
+ FieldInfo field = subject.GetType()
+ .FindField(expectedMember.Name, options.IncludedFields);
+
return field is not null ? new Field(field, parent) : null;
}
diff --git a/Src/FluentAssertions/Equivalency/MemberFactory.cs b/Src/FluentAssertions/Equivalency/MemberFactory.cs
index 7f12299e4d..6fed0af560 100644
--- a/Src/FluentAssertions/Equivalency/MemberFactory.cs
+++ b/Src/FluentAssertions/Equivalency/MemberFactory.cs
@@ -18,14 +18,14 @@ public static IMember Create(MemberInfo memberInfo, INode parent)
internal static IMember Find(object target, string memberName, INode parent)
{
- PropertyInfo property = target.GetType().FindProperty(memberName);
+ PropertyInfo property = target.GetType().FindProperty(memberName, MemberVisibility.Public | MemberVisibility.ExplicitlyImplemented);
if (property is not null && !property.IsIndexer())
{
return new Property(property, parent);
}
- FieldInfo field = target.GetType().FindField(memberName);
+ FieldInfo field = target.GetType().FindField(memberName, MemberVisibility.Public);
return field is not null ? new Field(field, parent) : null;
}
}
diff --git a/Src/FluentAssertions/Equivalency/MemberVisibility.cs b/Src/FluentAssertions/Equivalency/MemberVisibility.cs
index fd341a0346..e64b798a81 100644
--- a/Src/FluentAssertions/Equivalency/MemberVisibility.cs
+++ b/Src/FluentAssertions/Equivalency/MemberVisibility.cs
@@ -11,5 +11,6 @@ public enum MemberVisibility
{
None = 0,
Internal = 1,
- Public = 2
+ Public = 2,
+ ExplicitlyImplemented = 4
}
diff --git a/Src/FluentAssertions/Equivalency/Selection/AllFieldsSelectionRule.cs b/Src/FluentAssertions/Equivalency/Selection/AllFieldsSelectionRule.cs
index 6ec2d32665..fadd165609 100644
--- a/Src/FluentAssertions/Equivalency/Selection/AllFieldsSelectionRule.cs
+++ b/Src/FluentAssertions/Equivalency/Selection/AllFieldsSelectionRule.cs
@@ -14,11 +14,11 @@ internal class AllFieldsSelectionRule : IMemberSelectionRule
public IEnumerable SelectMembers(INode currentNode, IEnumerable selectedMembers,
MemberSelectionContext context)
{
- IEnumerable selectedNonPrivateFields = context.Type
- .GetNonPrivateFields(context.IncludedFields)
+ IEnumerable selectedFields = context.Type
+ .GetFields(context.IncludedFields)
.Select(info => new Field(info, currentNode));
- return selectedMembers.Union(selectedNonPrivateFields).ToList();
+ return selectedMembers.Union(selectedFields).ToList();
}
///
diff --git a/Src/FluentAssertions/Equivalency/Selection/AllPropertiesSelectionRule.cs b/Src/FluentAssertions/Equivalency/Selection/AllPropertiesSelectionRule.cs
index 5c1ac9c709..ba61049c62 100644
--- a/Src/FluentAssertions/Equivalency/Selection/AllPropertiesSelectionRule.cs
+++ b/Src/FluentAssertions/Equivalency/Selection/AllPropertiesSelectionRule.cs
@@ -14,11 +14,11 @@ internal class AllPropertiesSelectionRule : IMemberSelectionRule
public IEnumerable SelectMembers(INode currentNode, IEnumerable selectedMembers,
MemberSelectionContext context)
{
- IEnumerable selectedNonPrivateProperties = context.Type
- .GetNonPrivateProperties(context.IncludedProperties)
+ IEnumerable selectedProperties = context.Type
+ .GetProperties(context.IncludedProperties)
.Select(info => new Property(context.Type, info, currentNode));
- return selectedMembers.Union(selectedNonPrivateProperties).ToList();
+ return selectedMembers.Union(selectedProperties).ToList();
}
///
diff --git a/Src/FluentAssertions/Equivalency/Selection/IncludeMemberByPathSelectionRule.cs b/Src/FluentAssertions/Equivalency/Selection/IncludeMemberByPathSelectionRule.cs
index c521d7ddc1..7646069aa2 100644
--- a/Src/FluentAssertions/Equivalency/Selection/IncludeMemberByPathSelectionRule.cs
+++ b/Src/FluentAssertions/Equivalency/Selection/IncludeMemberByPathSelectionRule.cs
@@ -21,7 +21,7 @@ public IncludeMemberByPathSelectionRule(MemberPath pathToInclude)
protected override void AddOrRemoveMembersFrom(List selectedMembers, INode parent, string parentPath,
MemberSelectionContext context)
{
- foreach (MemberInfo memberInfo in context.Type.GetNonPrivateMembers(MemberVisibility.Public | MemberVisibility.Internal))
+ foreach (MemberInfo memberInfo in context.Type.GetMembers(MemberVisibility.Public | MemberVisibility.Internal))
{
var memberPath = new MemberPath(context.Type, memberInfo.DeclaringType, parentPath.Combine(memberInfo.Name));
diff --git a/Src/FluentAssertions/Equivalency/Selection/IncludeMemberByPredicateSelectionRule.cs b/Src/FluentAssertions/Equivalency/Selection/IncludeMemberByPredicateSelectionRule.cs
index f5a2c68cd4..97000aad92 100644
--- a/Src/FluentAssertions/Equivalency/Selection/IncludeMemberByPredicateSelectionRule.cs
+++ b/Src/FluentAssertions/Equivalency/Selection/IncludeMemberByPredicateSelectionRule.cs
@@ -27,7 +27,7 @@ public IEnumerable SelectMembers(INode currentNode, IEnumerable(selectedMembers);
- foreach (MemberInfo memberInfo in currentNode.Type.GetNonPrivateMembers(MemberVisibility.Public |
+ foreach (MemberInfo memberInfo in currentNode.Type.GetMembers(MemberVisibility.Public |
MemberVisibility.Internal))
{
IMember member = MemberFactory.Create(memberInfo, currentNode);
diff --git a/Src/FluentAssertions/Equivalency/SelfReferenceEquivalencyAssertionOptions.cs b/Src/FluentAssertions/Equivalency/SelfReferenceEquivalencyAssertionOptions.cs
index 9417f1ab9b..3246847d7b 100644
--- a/Src/FluentAssertions/Equivalency/SelfReferenceEquivalencyAssertionOptions.cs
+++ b/Src/FluentAssertions/Equivalency/SelfReferenceEquivalencyAssertionOptions.cs
@@ -1,5 +1,4 @@
using System;
-using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
@@ -26,12 +25,7 @@ public abstract class SelfReferenceEquivalencyAssertionOptions : IEquival
{
#region Private Definitions
- // REFACTOR: group the next three fields in a dedicated class
- private readonly List referenceTypes = new();
- private readonly List valueTypes = new();
- private readonly Func getDefaultEqualityStrategy;
-
- private readonly ConcurrentDictionary equalityStrategyCache = new();
+ private readonly EqualityStrategyProvider equalityStrategyProvider;
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private readonly List selectionRules = new();
@@ -62,12 +56,12 @@ public abstract class SelfReferenceEquivalencyAssertionOptions : IEquival
private bool ignoreNonBrowsableOnSubject;
private bool excludeNonBrowsableOnExpectation;
- private bool? compareRecordsByValue;
-
#endregion
private protected SelfReferenceEquivalencyAssertionOptions()
{
+ equalityStrategyProvider = new EqualityStrategyProvider();
+
AddMatchingRule(new MustMatchByNameRule());
OrderingRules.Add(new ByteArrayOrderingRule());
@@ -78,6 +72,11 @@ private protected SelfReferenceEquivalencyAssertionOptions()
///
protected SelfReferenceEquivalencyAssertionOptions(IEquivalencyAssertionOptions defaults)
{
+ equalityStrategyProvider = new EqualityStrategyProvider(defaults.GetEqualityStrategy)
+ {
+ CompareRecordsByValue = defaults.CompareRecordsByValue
+ };
+
isRecursive = defaults.IsRecursive;
cyclicReferenceHandling = defaults.CyclicReferenceHandling;
allowInfiniteRecursion = defaults.AllowInfiniteRecursion;
@@ -87,7 +86,6 @@ protected SelfReferenceEquivalencyAssertionOptions(IEquivalencyAssertionOptions
includedFields = defaults.IncludedFields;
ignoreNonBrowsableOnSubject = defaults.IgnoreNonBrowsableOnSubject;
excludeNonBrowsableOnExpectation = defaults.ExcludeNonBrowsableOnExpectation;
- compareRecordsByValue = defaults.CompareRecordsByValue;
ConversionSelector = defaults.ConversionSelector.Clone();
@@ -96,7 +94,6 @@ protected SelfReferenceEquivalencyAssertionOptions(IEquivalencyAssertionOptions
matchingRules.AddRange(defaults.MatchingRules);
OrderingRules = new OrderingRuleCollection(defaults.OrderingRules);
- getDefaultEqualityStrategy = defaults.GetEqualityStrategy;
TraceWriter = defaults.TraceWriter;
RemoveSelectionRule();
@@ -178,42 +175,10 @@ IEnumerable IEquivalencyAssertionOptions.SelectionRules
bool IEquivalencyAssertionOptions.ExcludeNonBrowsableOnExpectation => excludeNonBrowsableOnExpectation;
- public bool? CompareRecordsByValue => compareRecordsByValue;
+ public bool? CompareRecordsByValue => equalityStrategyProvider.CompareRecordsByValue;
EqualityStrategy IEquivalencyAssertionOptions.GetEqualityStrategy(Type type)
- {
- // As the valueFactory parameter captures instance members,
- // be aware if the cache must be cleared on mutating the members.
- return equalityStrategyCache.GetOrAdd(type, typeKey =>
- {
- if (!typeKey.IsPrimitive && referenceTypes.Count > 0 && referenceTypes.Exists(t => typeKey.IsSameOrInherits(t)))
- {
- return EqualityStrategy.ForceMembers;
- }
- else if (valueTypes.Count > 0 && valueTypes.Exists(t => typeKey.IsSameOrInherits(t)))
- {
- return EqualityStrategy.ForceEquals;
- }
- else if (!typeKey.IsPrimitive && referenceTypes.Count > 0 && referenceTypes.Exists(t => typeKey.IsAssignableToOpenGeneric(t)))
- {
- return EqualityStrategy.ForceMembers;
- }
- else if (valueTypes.Count > 0 && valueTypes.Exists(t => typeKey.IsAssignableToOpenGeneric(t)))
- {
- return EqualityStrategy.ForceEquals;
- }
- else if ((compareRecordsByValue.HasValue || getDefaultEqualityStrategy is null) && typeKey.IsRecord())
- {
- return compareRecordsByValue is true ? EqualityStrategy.ForceEquals : EqualityStrategy.ForceMembers;
- }
- else if (getDefaultEqualityStrategy is not null)
- {
- return getDefaultEqualityStrategy(typeKey);
- }
-
- return typeKey.HasValueSemantics() ? EqualityStrategy.Equals : EqualityStrategy.Members;
- });
- }
+ => equalityStrategyProvider.GetEqualityStrategy(type);
public ITraceWriter TraceWriter { get; private set; }
@@ -603,8 +568,7 @@ public TSelf ComparingEnumsByValue()
///
public TSelf ComparingRecordsByValue()
{
- compareRecordsByValue = true;
- equalityStrategyCache.Clear();
+ equalityStrategyProvider.CompareRecordsByValue = true;
return (TSelf)this;
}
@@ -617,8 +581,7 @@ public TSelf ComparingRecordsByValue()
///
public TSelf ComparingRecordsByMembers()
{
- compareRecordsByValue = false;
- equalityStrategyCache.Clear();
+ equalityStrategyProvider.CompareRecordsByValue = false;
return (TSelf)this;
}
@@ -642,14 +605,12 @@ public TSelf ComparingByMembers(Type type)
throw new InvalidOperationException($"Cannot compare a primitive type such as {type.Name} by its members");
}
- if (valueTypes.Exists(t => type.IsSameOrInherits(t)))
+ if (!equalityStrategyProvider.AddReferenceType(type))
{
throw new InvalidOperationException(
$"Can't compare {type.Name} by its members if it already setup to be compared by value");
}
- referenceTypes.Add(type);
- equalityStrategyCache.Clear();
return (TSelf)this;
}
@@ -668,14 +629,12 @@ public TSelf ComparingByValue(Type type)
{
Guard.ThrowIfArgumentIsNull(type);
- if (referenceTypes.Exists(t => type.IsSameOrInherits(t)))
+ if (!equalityStrategyProvider.AddValueType(type))
{
throw new InvalidOperationException(
$"Can't compare {type.Name} by value if it already setup to be compared by its members");
}
- valueTypes.Add(type);
- equalityStrategyCache.Clear();
return (TSelf)this;
}
@@ -755,26 +714,8 @@ public override string ToString()
builder
.AppendLine("- Compare tuples by their properties")
- .AppendLine("- Compare anonymous types by their properties");
-
- if (compareRecordsByValue is true)
- {
- builder.AppendLine("- Compare records by value");
- }
- else
- {
- builder.AppendLine("- Compare records by their members");
- }
-
- foreach (Type valueType in valueTypes)
- {
- builder.AppendLine(CultureInfo.InvariantCulture, $"- Compare {valueType} by value");
- }
-
- foreach (Type type in referenceTypes)
- {
- builder.AppendLine(CultureInfo.InvariantCulture, $"- Compare {type} by its members");
- }
+ .AppendLine("- Compare anonymous types by their properties")
+ .Append(equalityStrategyProvider);
if (excludeNonBrowsableOnExpectation)
{
diff --git a/Src/FluentAssertions/Equivalency/Steps/EnumerableEquivalencyStep.cs b/Src/FluentAssertions/Equivalency/Steps/EnumerableEquivalencyStep.cs
index bb11a573f0..51e110041c 100644
--- a/Src/FluentAssertions/Equivalency/Steps/EnumerableEquivalencyStep.cs
+++ b/Src/FluentAssertions/Equivalency/Steps/EnumerableEquivalencyStep.cs
@@ -61,10 +61,17 @@ internal static object[] ToArray(object value)
{
return ((IEnumerable)value).Cast().ToArray();
}
- catch (InvalidOperationException) when (value.GetType().Name.Equals("ImmutableArray`1", StringComparison.Ordinal))
+ catch (InvalidOperationException) when (IsIgnorableArrayLikeType(value))
{
- // This is probably a default ImmutableArray
+ // This is probably a default ImmutableArray or an empty ArraySegment.
return Array.Empty();
}
}
+
+ private static bool IsIgnorableArrayLikeType(object value)
+ {
+ var type = value.GetType();
+ return type.Name.Equals("ImmutableArray`1", StringComparison.Ordinal) ||
+ (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ArraySegment<>));
+ }
}
diff --git a/Src/FluentAssertions/Equivalency/Steps/EqualityComparerEquivalencyStep.cs b/Src/FluentAssertions/Equivalency/Steps/EqualityComparerEquivalencyStep.cs
index d704ef80f6..42c9df3677 100644
--- a/Src/FluentAssertions/Equivalency/Steps/EqualityComparerEquivalencyStep.cs
+++ b/Src/FluentAssertions/Equivalency/Steps/EqualityComparerEquivalencyStep.cs
@@ -16,8 +16,16 @@ public EqualityComparerEquivalencyStep(IEqualityComparer comparer)
public EquivalencyResult Handle(Comparands comparands, IEquivalencyValidationContext context,
IEquivalencyValidator nestedValidator)
{
- if (comparands.GetExpectedType(context.Options) != typeof(T))
+ var expectedType = context.Options.UseRuntimeTyping ? comparands.RuntimeType : comparands.CompileTimeType;
+
+ if (expectedType != typeof(T))
+ {
+ return EquivalencyResult.ContinueWithNext;
+ }
+
+ if (comparands.Subject is null || comparands.Expectation is null)
{
+ // The later check for `comparands.Subject is T` leads to a failure even if the expectation is null.
return EquivalencyResult.ContinueWithNext;
}
diff --git a/Src/FluentAssertions/Equivalency/Steps/GenericDictionaryEquivalencyStep.cs b/Src/FluentAssertions/Equivalency/Steps/GenericDictionaryEquivalencyStep.cs
index cbbf9ec3d9..bffb1ec68b 100644
--- a/Src/FluentAssertions/Equivalency/Steps/GenericDictionaryEquivalencyStep.cs
+++ b/Src/FluentAssertions/Equivalency/Steps/GenericDictionaryEquivalencyStep.cs
@@ -1,5 +1,7 @@
using System;
+using System.Collections;
using System.Collections.Generic;
+using System.Linq;
using System.Reflection;
using FluentAssertions.Execution;
@@ -17,32 +19,53 @@ public class GenericDictionaryEquivalencyStep : IEquivalencyStep
public EquivalencyResult Handle(Comparands comparands, IEquivalencyValidationContext context,
IEquivalencyValidator nestedValidator)
{
- if (comparands.Expectation != null)
+ if (comparands.Expectation is null)
{
- Type expectationType = comparands.GetExpectedType(context.Options);
- if (DictionaryInterfaceInfo.FindFrom(expectationType, "expectation") is { } expectedDictionary)
- {
- if (AssertSubjectIsNotNull(comparands.Subject)
- && EnsureSubjectIsDictionary(comparands, expectedDictionary) is { } actualDictionary)
- {
- AssertDictionaryEquivalence(comparands, context, nestedValidator, actualDictionary, expectedDictionary);
- }
+ return EquivalencyResult.ContinueWithNext;
+ }
- return EquivalencyResult.AssertionCompleted;
- }
+ Type expectationType = comparands.GetExpectedType(context.Options);
+
+ if (DictionaryInterfaceInfo.FindFrom(expectationType, "expectation") is not { } expectedDictionary)
+ {
+ return EquivalencyResult.ContinueWithNext;
+ }
+
+ if (IsNonGenericDictionary(comparands.Subject))
+ {
+ // Because we handle non-generic dictionaries later
+ return EquivalencyResult.ContinueWithNext;
+ }
+
+ if (IsNotNull(comparands.Subject)
+ && EnsureSubjectIsOfTheExpectedDictionaryType(comparands, expectedDictionary) is { } actualDictionary)
+ {
+ AssertDictionaryEquivalence(comparands, context, nestedValidator, actualDictionary, expectedDictionary);
+ }
+
+ return EquivalencyResult.AssertionCompleted;
+ }
+
+ private static bool IsNonGenericDictionary(object subject)
+ {
+ if (subject is not IDictionary)
+ {
+ return false;
}
- return EquivalencyResult.ContinueWithNext;
+ return !subject.GetType().GetInterfaces()
+ .Any(@interface => @interface.IsGenericType
+ && @interface.GetGenericTypeDefinition() == typeof(IDictionary<,>));
}
- private static bool AssertSubjectIsNotNull(object subject)
+ private static bool IsNotNull(object subject)
{
return AssertionScope.Current
.ForCondition(subject is not null)
.FailWith("Expected {context:Subject} not to be {0}{reason}.", new object[] { null });
}
- private static DictionaryInterfaceInfo EnsureSubjectIsDictionary(Comparands comparands,
+ private static DictionaryInterfaceInfo EnsureSubjectIsOfTheExpectedDictionaryType(Comparands comparands,
DictionaryInterfaceInfo expectedDictionary)
{
var actualDictionary = DictionaryInterfaceInfo.FindFromWithKey(comparands.Subject.GetType(), "subject",
@@ -57,8 +80,8 @@ private static DictionaryInterfaceInfo EnsureSubjectIsDictionary(Comparands comp
if (actualDictionary is null)
{
AssertionScope.Current.FailWith(
- $"Expected {{context:subject}} to be a dictionary or collection of key-value pairs that is keyed to type {expectedDictionary.Key}. " +
- $"It implements {actualDictionary}.");
+ "Expected {context:subject} to be a dictionary or collection of key-value pairs that is keyed to " +
+ $"type {expectedDictionary.Key}.");
}
return actualDictionary;
diff --git a/Src/FluentAssertions/Execution/AssertionScope.cs b/Src/FluentAssertions/Execution/AssertionScope.cs
index 1114ab2712..71a9a32f41 100644
--- a/Src/FluentAssertions/Execution/AssertionScope.cs
+++ b/Src/FluentAssertions/Execution/AssertionScope.cs
@@ -28,7 +28,9 @@ public sealed class AssertionScope : IAssertionScope
private static readonly AsyncLocal CurrentScope = new();
private Func callerIdentityProvider = () => CallerIdentifier.DetermineCallerIdentity();
+#pragma warning disable CA2213
private AssertionScope parent;
+#pragma warning restore CA2213
private Func expectation;
private string fallbackIdentifier = "object";
private bool? succeeded;
@@ -48,25 +50,23 @@ public DeferredReportable(Func valueFunc)
#endregion
///
- /// Starts a named scope within which multiple assertions can be executed
+ /// Starts an unnamed scope within which multiple assertions can be executed
/// and which will not throw until the scope is disposed.
///
- public AssertionScope(string context)
- : this()
+ public AssertionScope()
+ : this(new CollectingAssertionStrategy(), context: null)
{
- if (!string.IsNullOrEmpty(context))
- {
- Context = new Lazy(() => context);
- }
+ SetCurrentAssertionScope(this);
}
///
- /// Starts an unnamed scope within which multiple assertions can be executed
+ /// Starts a named scope within which multiple assertions can be executed
/// and which will not throw until the scope is disposed.
///
- public AssertionScope()
- : this(new CollectingAssertionStrategy())
+ public AssertionScope(string context)
+ : this(new CollectingAssertionStrategy(), new Lazy(() => context))
{
+ SetCurrentAssertionScope(this);
}
///
@@ -75,7 +75,7 @@ public AssertionScope()
/// The assertion strategy for this scope.
/// is .
public AssertionScope(IAssertionStrategy assertionStrategy)
- : this(assertionStrategy, GetCurrentAssertionScope())
+ : this(assertionStrategy, context: null)
{
SetCurrentAssertionScope(this);
}
@@ -85,32 +85,51 @@ public AssertionScope(IAssertionStrategy assertionStrategy)
/// and which will not throw until the scope is disposed.
///
public AssertionScope(Lazy context)
- : this()
+ : this(new CollectingAssertionStrategy(), context)
{
- Context = context;
+ SetCurrentAssertionScope(this);
}
///
/// Starts a new scope based on the given assertion strategy and parent assertion scope
///
/// The assertion strategy for this scope.
- /// The parent assertion scope for this scope.
/// is .
- private AssertionScope(IAssertionStrategy assertionStrategy, AssertionScope parent)
+ private AssertionScope(IAssertionStrategy assertionStrategy, Lazy context)
{
this.assertionStrategy = assertionStrategy
?? throw new ArgumentNullException(nameof(assertionStrategy));
- this.parent = parent;
+ parent = GetCurrentAssertionScope();
if (parent is not null)
{
contextData.Add(parent.contextData);
- Context = parent.Context;
+ reason = parent.reason;
callerIdentityProvider = parent.callerIdentityProvider;
+ FormattingOptions = parent.FormattingOptions.Clone();
+ Context = JoinContexts(parent.Context, context);
+ }
+ else
+ {
+ Context = context;
}
}
+ private static Lazy JoinContexts(Lazy outer, Lazy inner)
+ {
+ return (outer, inner) switch
+ {
+ (null, null) => null,
+ ({ } a, null) => a,
+ (null, { } b) => b,
+ ({ } a, { } b) => Join(a, b)
+ };
+
+ static Lazy Join(Lazy outer, Lazy inner) =>
+ new(() => outer.Value + "/" + inner.Value);
+ }
+
///
/// Gets or sets the context of the current assertion scope, e.g. the path of the object graph
/// that is being asserted on. The context is provided by a which
@@ -126,7 +145,7 @@ public static AssertionScope Current
#pragma warning disable CA2000 // AssertionScope should not be disposed here
get
{
- return GetCurrentAssertionScope() ?? new AssertionScope(new DefaultAssertionStrategy(), parent: null);
+ return GetCurrentAssertionScope() ?? new AssertionScope(new DefaultAssertionStrategy(), context: null);
}
#pragma warning restore CA2000
private set => SetCurrentAssertionScope(value);
@@ -147,10 +166,7 @@ public AssertionScope UsingLineBreaks
///
public FormattingOptions FormattingOptions { get; } = AssertionOptions.FormattingOptions.Clone();
- internal bool Succeeded
- {
- get => succeeded == true;
- }
+ internal bool Succeeded => succeeded == true;
///
/// Adds an explanation of why the assertion is supposed to succeed to the scope.
diff --git a/Src/FluentAssertions/Execution/Continuation.cs b/Src/FluentAssertions/Execution/Continuation.cs
index 42264aae15..b59ad7f4e7 100644
--- a/Src/FluentAssertions/Execution/Continuation.cs
+++ b/Src/FluentAssertions/Execution/Continuation.cs
@@ -15,7 +15,7 @@ internal Continuation(AssertionScope sourceScope, bool continueAsserting)
}
///
- /// Continuous the assertion chain if the previous assertion was successful.
+ /// Continues the assertion chain if the previous assertion was successful.
///
public IAssertionScope Then
{
diff --git a/Src/FluentAssertions/Execution/ContinuationOfGiven.cs b/Src/FluentAssertions/Execution/ContinuationOfGiven.cs
index f080c83d08..47ca54a889 100644
--- a/Src/FluentAssertions/Execution/ContinuationOfGiven.cs
+++ b/Src/FluentAssertions/Execution/ContinuationOfGiven.cs
@@ -14,7 +14,7 @@ internal ContinuationOfGiven(GivenSelector parent, bool succeeded)
}
///
- /// Continuous the assertion chain if the previous assertion was successful.
+ /// Continues the assertion chain if the previous assertion was successful.
///
public GivenSelector Then { get; }
diff --git a/Src/FluentAssertions/Formatting/DefaultValueFormatter.cs b/Src/FluentAssertions/Formatting/DefaultValueFormatter.cs
index 723b04cbff..93775f7c99 100644
--- a/Src/FluentAssertions/Formatting/DefaultValueFormatter.cs
+++ b/Src/FluentAssertions/Formatting/DefaultValueFormatter.cs
@@ -56,7 +56,7 @@ public void Format(object value, FormattedObjectGraph formattedGraph, Formatting
/// The default is all non-private members.
protected virtual MemberInfo[] GetMembers(Type type)
{
- return type.GetNonPrivateMembers(MemberVisibility.Public);
+ return type.GetMembers(MemberVisibility.Public);
}
private static bool HasCompilerGeneratedToStringImplementation(object value)
diff --git a/Src/FluentAssertions/Numeric/NumericAssertions.cs b/Src/FluentAssertions/Numeric/NumericAssertions.cs
index d0b1b16eb2..c49a167111 100644
--- a/Src/FluentAssertions/Numeric/NumericAssertions.cs
+++ b/Src/FluentAssertions/Numeric/NumericAssertions.cs
@@ -449,12 +449,15 @@ public AndConstraint NotBeOfType(Type unexpectedType, string becaus
{
Guard.ThrowIfArgumentIsNull(unexpectedType);
- Execute.Assertion
+ bool success = Execute.Assertion
.ForCondition(Subject.HasValue)
.BecauseOf(because, becauseArgs)
.FailWith("Expected type not to be " + unexpectedType + "{reason}, but found .");
- Subject.GetType().Should().NotBe(unexpectedType, because, becauseArgs);
+ if (success)
+ {
+ Subject.GetType().Should().NotBe(unexpectedType, because, becauseArgs);
+ }
return new AndConstraint((TAssertions)this);
}
diff --git a/Src/FluentAssertions/Primitives/BooleanAssertions.cs b/Src/FluentAssertions/Primitives/BooleanAssertions.cs
index 69ba629c9a..c44bc7f2ff 100644
--- a/Src/FluentAssertions/Primitives/BooleanAssertions.cs
+++ b/Src/FluentAssertions/Primitives/BooleanAssertions.cs
@@ -32,7 +32,7 @@ public BooleanAssertions(bool? value)
}
///
- /// Gets the object which value is being asserted.
+ /// Gets the object whose value is being asserted.
///
public bool? Subject { get; }
@@ -51,7 +51,7 @@ public AndConstraint BeFalse(string because = "", params object[] b
Execute.Assertion
.ForCondition(Subject == false)
.BecauseOf(because, becauseArgs)
- .FailWith("Expected {context:boolean} to be false{reason}, but found {0}.", Subject);
+ .FailWith("Expected {context:boolean} to be {0}{reason}, but found {1}.", false, Subject);
return new AndConstraint((TAssertions)this);
}
@@ -71,7 +71,7 @@ public AndConstraint BeTrue(string because = "", params object[] be
Execute.Assertion
.ForCondition(Subject == true)
.BecauseOf(because, becauseArgs)
- .FailWith("Expected {context:boolean} to be true{reason}, but found {0}.", Subject);
+ .FailWith("Expected {context:boolean} to be {0}{reason}, but found {1}.", true, Subject);
return new AndConstraint((TAssertions)this);
}
diff --git a/Src/FluentAssertions/Primitives/DateOnlyAssertions.cs b/Src/FluentAssertions/Primitives/DateOnlyAssertions.cs
index f610c6ee6e..9d01383966 100644
--- a/Src/FluentAssertions/Primitives/DateOnlyAssertions.cs
+++ b/Src/FluentAssertions/Primitives/DateOnlyAssertions.cs
@@ -34,7 +34,7 @@ public DateOnlyAssertions(DateOnly? value)
}
///
- /// Gets the object which value is being asserted.
+ /// Gets the object whose value is being asserted.
///
public DateOnly? Subject { get; }
diff --git a/Src/FluentAssertions/Primitives/DateTimeAssertions.cs b/Src/FluentAssertions/Primitives/DateTimeAssertions.cs
index f511267c2b..0606a6a0ae 100644
--- a/Src/FluentAssertions/Primitives/DateTimeAssertions.cs
+++ b/Src/FluentAssertions/Primitives/DateTimeAssertions.cs
@@ -42,7 +42,7 @@ public DateTimeAssertions(DateTime? value)
}
///
- /// Gets the object which value is being asserted.
+ /// Gets the object whose value is being asserted.
///
public DateTime? Subject { get; }
diff --git a/Src/FluentAssertions/Primitives/DateTimeOffsetAssertions.cs b/Src/FluentAssertions/Primitives/DateTimeOffsetAssertions.cs
index 490bf3c113..dafcdbbdba 100644
--- a/Src/FluentAssertions/Primitives/DateTimeOffsetAssertions.cs
+++ b/Src/FluentAssertions/Primitives/DateTimeOffsetAssertions.cs
@@ -43,7 +43,7 @@ public DateTimeOffsetAssertions(DateTimeOffset? value)
}
///
- /// Gets the object which value is being asserted.
+ /// Gets the object whose value is being asserted.
///
public DateTimeOffset? Subject { get; }
diff --git a/Src/FluentAssertions/Primitives/DateTimeOffsetRangeAssertions.cs b/Src/FluentAssertions/Primitives/DateTimeOffsetRangeAssertions.cs
index ce99cc3736..d56cd78cb6 100644
--- a/Src/FluentAssertions/Primitives/DateTimeOffsetRangeAssertions.cs
+++ b/Src/FluentAssertions/Primitives/DateTimeOffsetRangeAssertions.cs
@@ -68,7 +68,7 @@ public AndConstraint Before(DateTimeOffset target, string because =
bool success = Execute.Assertion
.ForCondition(subject.HasValue)
.BecauseOf(because, becauseArgs)
- .FailWith("Expected {context:the date and time) to be " + predicate.DisplayText +
+ .FailWith("Expected {context:the date and time} to be " + predicate.DisplayText +
" {0} before {1}{reason}, but found a DateTime.", timeSpan, target);
if (success)
diff --git a/Src/FluentAssertions/Primitives/GuidAssertions.cs b/Src/FluentAssertions/Primitives/GuidAssertions.cs
index e52b51df41..eb9cb115a0 100644
--- a/Src/FluentAssertions/Primitives/GuidAssertions.cs
+++ b/Src/FluentAssertions/Primitives/GuidAssertions.cs
@@ -31,7 +31,7 @@ public GuidAssertions(Guid? value)
}
///
- /// Gets the object which value is being asserted.
+ /// Gets the object whose value is being asserted.
///
public Guid? Subject { get; }
diff --git a/Src/FluentAssertions/Primitives/ObjectAssertions.cs b/Src/FluentAssertions/Primitives/ObjectAssertions.cs
index 5458270c9f..f9c9880f37 100644
--- a/Src/FluentAssertions/Primitives/ObjectAssertions.cs
+++ b/Src/FluentAssertions/Primitives/ObjectAssertions.cs
@@ -361,7 +361,7 @@ public AndConstraint NotBeEquivalentTo(
using (var scope = new AssertionScope())
{
- Subject.Should().BeEquivalentTo(unexpected, config);
+ BeEquivalentTo(unexpected, config);
hasMismatches = scope.Discard().Length > 0;
}
diff --git a/Src/FluentAssertions/Primitives/ReferenceTypeAssertions.cs b/Src/FluentAssertions/Primitives/ReferenceTypeAssertions.cs
index 4d16b601ed..33cf54b052 100644
--- a/Src/FluentAssertions/Primitives/ReferenceTypeAssertions.cs
+++ b/Src/FluentAssertions/Primitives/ReferenceTypeAssertions.cs
@@ -21,7 +21,7 @@ protected ReferenceTypeAssertions(TSubject subject)
}
///
- /// Gets the object which value is being asserted.
+ /// Gets the object whose value is being asserted.
///
public TSubject Subject { get; }
@@ -81,7 +81,6 @@ public AndConstraint NotBeNull(string because = "", params object[]
public AndConstraint BeSameAs(TSubject expected, string because = "", params object[] becauseArgs)
{
Execute.Assertion
- .UsingLineBreaks
.ForCondition(ReferenceEquals(Subject, expected))
.BecauseOf(because, becauseArgs)
.WithDefaultIdentifier(Identifier)
@@ -104,7 +103,6 @@ public AndConstraint BeSameAs(TSubject expected, string because = "
public AndConstraint NotBeSameAs(TSubject unexpected, string because = "", params object[] becauseArgs)
{
Execute.Assertion
- .UsingLineBreaks
.ForCondition(!ReferenceEquals(Subject, unexpected))
.BecauseOf(because, becauseArgs)
.WithDefaultIdentifier(Identifier)
diff --git a/Src/FluentAssertions/Primitives/SimpleTimeSpanAssertions.cs b/Src/FluentAssertions/Primitives/SimpleTimeSpanAssertions.cs
index 9612a6e23b..df7af13c81 100644
--- a/Src/FluentAssertions/Primitives/SimpleTimeSpanAssertions.cs
+++ b/Src/FluentAssertions/Primitives/SimpleTimeSpanAssertions.cs
@@ -33,7 +33,7 @@ public SimpleTimeSpanAssertions(TimeSpan? value)
}
///
- /// Gets the object which value is being asserted.
+ /// Gets the object whose value is being asserted.
///
public TimeSpan? Subject { get; }
diff --git a/Src/FluentAssertions/Primitives/StringAssertions.cs b/Src/FluentAssertions/Primitives/StringAssertions.cs
index e171ec4cc9..57c2f1b021 100644
--- a/Src/FluentAssertions/Primitives/StringAssertions.cs
+++ b/Src/FluentAssertions/Primitives/StringAssertions.cs
@@ -141,7 +141,7 @@ public AndConstraint NotBeEquivalentTo(string unexpected, string be
using (var scope = new AssertionScope())
{
- Subject.Should().BeEquivalentTo(unexpected);
+ BeEquivalentTo(unexpected);
notEquivalent = scope.Discard().Length > 0;
}
diff --git a/Src/FluentAssertions/Primitives/TimeOnlyAssertions.cs b/Src/FluentAssertions/Primitives/TimeOnlyAssertions.cs
index b604aa0d28..70c48e5383 100644
--- a/Src/FluentAssertions/Primitives/TimeOnlyAssertions.cs
+++ b/Src/FluentAssertions/Primitives/TimeOnlyAssertions.cs
@@ -35,7 +35,7 @@ public TimeOnlyAssertions(TimeOnly? value)
}
///
- /// Gets the object which value is being asserted.
+ /// Gets the object whose value is being asserted.
///
public TimeOnly? Subject { get; }
diff --git a/Src/FluentAssertions/Specialized/AsyncFunctionAssertions.cs b/Src/FluentAssertions/Specialized/AsyncFunctionAssertions.cs
index e51556d434..2a80ef257e 100644
--- a/Src/FluentAssertions/Specialized/AsyncFunctionAssertions.cs
+++ b/Src/FluentAssertions/Specialized/AsyncFunctionAssertions.cs
@@ -138,12 +138,15 @@ public async Task> ThrowExactlyAsync
{
Exception exception = await InvokeWithInterceptionAsync(Subject);
- Execute.Assertion
+ success = Execute.Assertion
.ForCondition(exception is not null)
.BecauseOf(because, becauseArgs)
.FailWith("Expected {0}{reason}, but no exception was thrown.", expectedType);
- exception.Should().BeOfType(expectedType, because, becauseArgs);
+ if (success)
+ {
+ exception.Should().BeOfType(expectedType, because, becauseArgs);
+ }
return new ExceptionAssertions(new[] { exception as TException });
}
@@ -432,8 +435,8 @@ private protected async Task CompletesWithinTimeoutAsync(Task target, Time
{
using var delayCancellationTokenSource = new CancellationTokenSource();
- Task completedTask =
- await Task.WhenAny(target, Clock.DelayAsync(remainingTime, delayCancellationTokenSource.Token));
+ Task delayTask = Clock.DelayAsync(remainingTime, delayCancellationTokenSource.Token);
+ Task completedTask = await Task.WhenAny(target, delayTask);
if (completedTask.IsFaulted)
{
@@ -447,6 +450,12 @@ private protected async Task CompletesWithinTimeoutAsync(Task target, Time
return false;
}
+ if (target.IsCanceled)
+ {
+ // Rethrow the exception causing the task be canceled.
+ await target;
+ }
+
// The monitored task is completed, we shall cancel the clock.
delayCancellationTokenSource.Cancel();
return true;
diff --git a/Src/FluentAssertions/Specialized/DelegateAssertions.cs b/Src/FluentAssertions/Specialized/DelegateAssertions.cs
index 3329c17799..b267bc91c0 100644
--- a/Src/FluentAssertions/Specialized/DelegateAssertions.cs
+++ b/Src/FluentAssertions/Specialized/DelegateAssertions.cs
@@ -140,12 +140,15 @@ public ExceptionAssertions ThrowExactly(string because =
Type expectedType = typeof(TException);
- Execute.Assertion
+ success = Execute.Assertion
.ForCondition(exception is not null)
.BecauseOf(because, becauseArgs)
.FailWith("Expected {0}{reason}, but no exception was thrown.", expectedType);
- exception.Should().BeOfType(expectedType, because, becauseArgs);
+ if (success)
+ {
+ exception.Should().BeOfType(expectedType, because, becauseArgs);
+ }
return new ExceptionAssertions(new[] { exception as TException });
}
diff --git a/Src/FluentAssertions/Specialized/ExceptionAssertions.cs b/Src/FluentAssertions/Specialized/ExceptionAssertions.cs
index 5e221d402a..90c0957002 100644
--- a/Src/FluentAssertions/Specialized/ExceptionAssertions.cs
+++ b/Src/FluentAssertions/Specialized/ExceptionAssertions.cs
@@ -193,7 +193,7 @@ public ExceptionAssertions Where(Expression>
return this;
}
- private Exception[] AssertInnerExceptionExactly(Type innerException, string because = "",
+ private IEnumerable AssertInnerExceptionExactly(Type innerException, string because = "",
params object[] becauseArgs)
{
Execute.Assertion
@@ -213,7 +213,7 @@ private Exception[] AssertInnerExceptionExactly(Type innerException, string beca
return expectedExceptions;
}
- private Exception[] AssertInnerExceptions(Type innerException, string because = "",
+ private IEnumerable AssertInnerExceptions(Type innerException, string because = "",
params object[] becauseArgs)
{
Execute.Assertion
diff --git a/Src/FluentAssertions/Types/MethodInfoSelectorAssertions.cs b/Src/FluentAssertions/Types/MethodInfoSelectorAssertions.cs
index a6b5993dc9..f7f0b2f37b 100644
--- a/Src/FluentAssertions/Types/MethodInfoSelectorAssertions.cs
+++ b/Src/FluentAssertions/Types/MethodInfoSelectorAssertions.cs
@@ -30,7 +30,7 @@ public MethodInfoSelectorAssertions(params MethodInfo[] methods)
}
///
- /// Gets the object which value is being asserted.
+ /// Gets the object whose value is being asserted.
///
public IEnumerable SubjectMethods { get; }
diff --git a/Src/FluentAssertions/Types/PropertyInfoAssertions.cs b/Src/FluentAssertions/Types/PropertyInfoAssertions.cs
index e8f329d04f..ffc98e79d6 100644
--- a/Src/FluentAssertions/Types/PropertyInfoAssertions.cs
+++ b/Src/FluentAssertions/Types/PropertyInfoAssertions.cs
@@ -130,9 +130,17 @@ public AndConstraint BeWritable(CSharpAccessModifier acc
if (success)
{
- Subject.Should().BeWritable(because, becauseArgs);
+ success = Execute.Assertion
+ .ForCondition(Subject!.CanWrite)
+ .BecauseOf(because, becauseArgs)
+ .FailWith(
+ "Expected {context:property} {0} to have a setter{reason}.",
+ Subject);
- Subject.GetSetMethod(nonPublic: true).Should().HaveAccessModifier(accessModifier, because, becauseArgs);
+ if (success)
+ {
+ Subject.GetSetMethod(nonPublic: true).Should().HaveAccessModifier(accessModifier, because, becauseArgs);
+ }
}
return new AndConstraint(this);
@@ -221,9 +229,14 @@ public AndConstraint BeReadable(CSharpAccessModifier acc
if (success)
{
- Subject.Should().BeReadable(because, becauseArgs);
+ success = Execute.Assertion.ForCondition(Subject!.CanRead)
+ .BecauseOf(because, becauseArgs)
+ .FailWith("Expected property " + Subject.Name + " to have a getter{reason}, but it does not.");
- Subject.GetGetMethod(nonPublic: true).Should().HaveAccessModifier(accessModifier, because, becauseArgs);
+ if (success)
+ {
+ Subject.GetGetMethod(nonPublic: true).Should().HaveAccessModifier(accessModifier, because, becauseArgs);
+ }
}
return new AndConstraint(this);
diff --git a/Src/FluentAssertions/Types/PropertyInfoSelectorAssertions.cs b/Src/FluentAssertions/Types/PropertyInfoSelectorAssertions.cs
index 9f776c8e96..644652b364 100644
--- a/Src/FluentAssertions/Types/PropertyInfoSelectorAssertions.cs
+++ b/Src/FluentAssertions/Types/PropertyInfoSelectorAssertions.cs
@@ -17,7 +17,7 @@ namespace FluentAssertions.Types;
public class PropertyInfoSelectorAssertions
{
///
- /// Gets the object which value is being asserted.
+ /// Gets the object whose value is being asserted.
///
public IEnumerable SubjectProperties { get; }
diff --git a/Src/FluentAssertions/Types/TypeAssertions.cs b/Src/FluentAssertions/Types/TypeAssertions.cs
index 2c8c577072..8c25e0bc7d 100644
--- a/Src/FluentAssertions/Types/TypeAssertions.cs
+++ b/Src/FluentAssertions/Types/TypeAssertions.cs
@@ -462,22 +462,27 @@ public AndConstraint Implement(Type interfaceType, string becaus
{
Guard.ThrowIfArgumentIsNull(interfaceType);
- bool containsInterface = Subject.GetInterfaces().Contains(interfaceType);
-
- Execute.Assertion
- .BecauseOf(because, becauseArgs)
- .WithExpectation("Expected type {0} to implement interface {1}{reason}", Subject, interfaceType)
- .ForCondition(interfaceType.IsInterface)
- .FailWith(", but {0} is not an interface.", interfaceType)
- .Then
- .ForCondition(containsInterface)
- .FailWith(", but it does not.")
- .Then
- .ClearExpectation();
+ AssertSubjectImplements(interfaceType, because, becauseArgs);
return new AndConstraint(this);
}
+ private bool AssertSubjectImplements(Type interfaceType, string because = "", params object[] becauseArgs)
+ {
+ bool containsInterface = interfaceType.IsAssignableFrom(Subject) && interfaceType != Subject;
+
+ return Execute.Assertion
+ .BecauseOf(because, becauseArgs)
+ .WithExpectation("Expected type {0} to implement interface {1}{reason}", Subject, interfaceType)
+ .ForCondition(interfaceType.IsInterface)
+ .FailWith(", but {0} is not an interface.", interfaceType)
+ .Then
+ .ForCondition(containsInterface)
+ .FailWith(", but it does not.")
+ .Then
+ .ClearExpectation();
+ }
+
///
/// Asserts that the current implements interface .
///
@@ -511,7 +516,7 @@ public AndConstraint NotImplement(Type interfaceType, string bec
{
Guard.ThrowIfArgumentIsNull(interfaceType);
- bool containsInterface = Subject.GetInterfaces().Contains(interfaceType);
+ bool containsInterface = interfaceType.IsAssignableFrom(Subject) && interfaceType != Subject;
Execute.Assertion
.BecauseOf(because, becauseArgs)
@@ -973,15 +978,18 @@ public AndConstraint HaveExplicitProperty(
if (success)
{
- Subject.Should().Implement(interfaceType, because, becauseArgs);
-
- var explicitlyImplementsProperty = Subject.HasExplicitlyImplementedProperty(interfaceType, name);
-
- Execute.Assertion
- .BecauseOf(because, becauseArgs)
- .ForCondition(explicitlyImplementsProperty)
- .FailWith(
- $"Expected {Subject} to explicitly implement {interfaceType}.{name}{{reason}}, but it does not.");
+ success = AssertSubjectImplements(interfaceType, because, becauseArgs);
+
+ if (success)
+ {
+ var explicitlyImplementsProperty = Subject.HasExplicitlyImplementedProperty(interfaceType, name);
+
+ Execute.Assertion
+ .BecauseOf(because, becauseArgs)
+ .ForCondition(explicitlyImplementsProperty)
+ .FailWith(
+ $"Expected {Subject} to explicitly implement {interfaceType}.{name}{{reason}}, but it does not.");
+ }
}
return new AndConstraint(this);
@@ -1040,16 +1048,19 @@ public AndConstraint NotHaveExplicitProperty(
if (success)
{
- Subject.Should().Implement(interfaceType, because, becauseArgs);
-
- var explicitlyImplementsProperty = Subject.HasExplicitlyImplementedProperty(interfaceType, name);
-
- Execute.Assertion
- .BecauseOf(because, becauseArgs)
- .ForCondition(!explicitlyImplementsProperty)
- .FailWith(
- $"Expected {Subject} to not explicitly implement {interfaceType}.{name}{{reason}}" +
- ", but it does.");
+ success = AssertSubjectImplements(interfaceType, because, becauseArgs);
+
+ if (success)
+ {
+ var explicitlyImplementsProperty = Subject.HasExplicitlyImplementedProperty(interfaceType, name);
+
+ Execute.Assertion
+ .BecauseOf(because, becauseArgs)
+ .ForCondition(!explicitlyImplementsProperty)
+ .FailWith(
+ $"Expected {Subject} to not explicitly implement {interfaceType}.{name}{{reason}}" +
+ ", but it does.");
+ }
}
return new AndConstraint(this);
@@ -1111,16 +1122,19 @@ public AndConstraint HaveExplicitMethod(
if (success)
{
- Subject.Should().Implement(interfaceType, because, becauseArgs);
-
- var explicitlyImplementsMethod = Subject.HasMethod($"{interfaceType}.{name}", parameterTypes);
-
- Execute.Assertion
- .BecauseOf(because, becauseArgs)
- .ForCondition(explicitlyImplementsMethod)
- .FailWith(
- $"Expected {Subject} to explicitly implement {interfaceType}.{name}" +
- $"({GetParameterString(parameterTypes)}){{reason}}, but it does not.");
+ success = AssertSubjectImplements(interfaceType, because, becauseArgs);
+
+ if (success)
+ {
+ var explicitlyImplementsMethod = Subject.HasMethod($"{interfaceType}.{name}", parameterTypes);
+
+ Execute.Assertion
+ .BecauseOf(because, becauseArgs)
+ .ForCondition(explicitlyImplementsMethod)
+ .FailWith(
+ $"Expected {Subject} to explicitly implement {interfaceType}.{name}" +
+ $"({GetParameterString(parameterTypes)}){{reason}}, but it does not.");
+ }
}
return new AndConstraint(this);
@@ -1184,16 +1198,19 @@ public AndConstraint NotHaveExplicitMethod(
if (success)
{
- Subject.Should().Implement(interfaceType, because, becauseArgs);
-
- var explicitlyImplementsMethod = Subject.HasMethod($"{interfaceType}.{name}", parameterTypes);
-
- Execute.Assertion
- .BecauseOf(because, becauseArgs)
- .ForCondition(!explicitlyImplementsMethod)
- .FailWith(
- $"Expected {Subject} to not explicitly implement {interfaceType}.{name}" +
- $"({GetParameterString(parameterTypes)}){{reason}}, but it does.");
+ success = AssertSubjectImplements(interfaceType, because, becauseArgs);
+
+ if (success)
+ {
+ var explicitlyImplementsMethod = Subject.HasMethod($"{interfaceType}.{name}", parameterTypes);
+
+ Execute.Assertion
+ .BecauseOf(because, becauseArgs)
+ .ForCondition(!explicitlyImplementsMethod)
+ .FailWith(
+ $"Expected {Subject} to not explicitly implement {interfaceType}.{name}" +
+ $"({GetParameterString(parameterTypes)}){{reason}}, but it does.");
+ }
}
return new AndConstraint(this);
diff --git a/Src/FluentAssertions/Types/TypeSelectorAssertions.cs b/Src/FluentAssertions/Types/TypeSelectorAssertions.cs
index f1436e49a8..90ab465827 100644
--- a/Src/FluentAssertions/Types/TypeSelectorAssertions.cs
+++ b/Src/FluentAssertions/Types/TypeSelectorAssertions.cs
@@ -30,7 +30,7 @@ public TypeSelectorAssertions(params Type[] types)
}
///
- /// Gets the object which value is being asserted.
+ /// Gets the object whose value is being asserted.
///
public IEnumerable Subject { get; }
diff --git a/Tests/Approval.Tests/ApiApproval.cs b/Tests/Approval.Tests/ApiApproval.cs
index a1f0bbd9ca..c9b3726f6f 100644
--- a/Tests/Approval.Tests/ApiApproval.cs
+++ b/Tests/Approval.Tests/ApiApproval.cs
@@ -28,10 +28,12 @@ public Task ApproveApi(string frameworkVersion)
string assemblyPath = Uri.UnescapeDataString(uri.Path);
var containingDirectory = Path.GetDirectoryName(assemblyPath);
var configurationName = new DirectoryInfo(containingDirectory).Parent.Name;
+
var assemblyFile = Path.GetFullPath(
Path.Combine(
GetSourceDirectory(),
- Path.Combine("..", "..", "Src", "FluentAssertions", "bin", configurationName, frameworkVersion, "FluentAssertions.dll")));
+ Path.Combine("..", "..", "Src", "FluentAssertions", "bin", configurationName, frameworkVersion,
+ "FluentAssertions.dll")));
var assembly = Assembly.LoadFile(Path.GetFullPath(assemblyFile));
var publicApi = assembly.GeneratePublicApi(options: null);
@@ -53,6 +55,7 @@ public static Task OnlyIncludeChanges(string received, string ver
var diff = InlineDiffBuilder.Diff(verified, received);
var builder = new StringBuilder();
+
foreach (var line in diff.Lines)
{
switch (line.Type)
@@ -79,9 +82,12 @@ private class TargetFrameworksTheoryData : TheoryData
{
public TargetFrameworksTheoryData()
{
- var csproj = Path.Combine(GetSourceDirectory(), Path.Combine("..", "..", "Src", "FluentAssertions", "FluentAssertions.csproj"));
+ var csproj = Path.Combine(GetSourceDirectory(),
+ Path.Combine("..", "..", "Src", "FluentAssertions", "FluentAssertions.csproj"));
+
var project = XDocument.Load(csproj);
var targetFrameworks = project.XPathSelectElement("/Project/PropertyGroup/TargetFrameworks");
+
foreach (string targetFramework in targetFrameworks!.Value.Split(';'))
{
Add(targetFramework);
diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt
index 377122900a..89da67fff2 100644
--- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt
+++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt
@@ -974,6 +974,7 @@ namespace FluentAssertions.Equivalency
None = 0,
Internal = 1,
Public = 2,
+ ExplicitlyImplemented = 4,
}
public class NestedExclusionOptionBuilder
{
diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt
index fa1f44fa66..0db0cc0251 100644
--- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt
+++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt
@@ -987,6 +987,7 @@ namespace FluentAssertions.Equivalency
None = 0,
Internal = 1,
Public = 2,
+ ExplicitlyImplemented = 4,
}
public class NestedExclusionOptionBuilder
{
diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt
index 63d5985ea5..e76973c182 100644
--- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt
+++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt
@@ -974,6 +974,7 @@ namespace FluentAssertions.Equivalency
None = 0,
Internal = 1,
Public = 2,
+ ExplicitlyImplemented = 4,
}
public class NestedExclusionOptionBuilder
{
@@ -2803,4 +2804,4 @@ namespace FluentAssertions.Xml
public bool CanHandle(object value) { }
public void Format(object value, FluentAssertions.Formatting.FormattedObjectGraph formattedGraph, FluentAssertions.Formatting.FormattingContext context, FluentAssertions.Formatting.FormatChild formatChild) { }
}
-}
+}
\ No newline at end of file
diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt
index 63d5985ea5..e76973c182 100644
--- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt
+++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt
@@ -974,6 +974,7 @@ namespace FluentAssertions.Equivalency
None = 0,
Internal = 1,
Public = 2,
+ ExplicitlyImplemented = 4,
}
public class NestedExclusionOptionBuilder
{
@@ -2803,4 +2804,4 @@ namespace FluentAssertions.Xml
public bool CanHandle(object value) { }
public void Format(object value, FluentAssertions.Formatting.FormattedObjectGraph formattedGraph, FluentAssertions.Formatting.FormattingContext context, FluentAssertions.Formatting.FormatChild formatChild) { }
}
-}
+}
\ No newline at end of file
diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt
index 33bdb75873..d14c1711b3 100644
--- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt
+++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt
@@ -967,6 +967,7 @@ namespace FluentAssertions.Equivalency
None = 0,
Internal = 1,
Public = 2,
+ ExplicitlyImplemented = 4,
}
public class NestedExclusionOptionBuilder
{
diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt
index 63d5985ea5..99eb81a1e3 100644
--- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt
+++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt
@@ -974,6 +974,7 @@ namespace FluentAssertions.Equivalency
None = 0,
Internal = 1,
Public = 2,
+ ExplicitlyImplemented = 4,
}
public class NestedExclusionOptionBuilder
{
diff --git a/Tests/FluentAssertions.Equivalency.Specs/AssertionRuleSpecs.cs b/Tests/FluentAssertions.Equivalency.Specs/AssertionRuleSpecs.cs
index e13c43491b..48c75c217f 100644
--- a/Tests/FluentAssertions.Equivalency.Specs/AssertionRuleSpecs.cs
+++ b/Tests/FluentAssertions.Equivalency.Specs/AssertionRuleSpecs.cs
@@ -245,6 +245,72 @@ public void When_the_runtime_type_does_match_the_equality_comparer_type_it_shoul
act.Should().Throw().WithMessage("*ConcreteClassEqualityComparer*");
}
+ public class BeEquivalentTo
+ {
+ [Fact]
+ public void An_equality_comparer_of_non_nullable_type_is_not_invoked_on_nullable_member()
+ {
+ // Arrange
+ var subject = new { Timestamp = (DateTime?)22.March(2020) };
+
+ var expectation = new { Timestamp = (DateTime?)1.January(2020) };
+
+ // Act
+ Action act = () => subject.Should().BeEquivalentTo(expectation,
+ opt => opt.Using(new DateTimeByYearComparer()));
+
+ // Assert
+ act.Should().Throw().WithMessage("Expected*Timestamp*2020*");
+ }
+
+ [Fact]
+ public void An_equality_comparer_of_nullable_type_is_not_invoked_on_non_nullable_member()
+ {
+ // Arrange
+ var subject = new { Timestamp = 22.March(2020) };
+
+ var expectation = new { Timestamp = 1.January(2020) };
+
+ // Act
+ Action act = () => subject.Should().BeEquivalentTo(expectation,
+ opt => opt.Using(new NullableDateTimeByYearComparer()));
+
+ // Assert
+ act.Should().Throw().WithMessage("Expected*Timestamp*2020*");
+ }
+
+ [Fact]
+ public void An_equality_comparer_of_non_nullable_type_is_invoked_on_non_nullable_data()
+ {
+ // Arrange
+ var subject = new { Timestamp = (DateTime?)22.March(2020) };
+
+ var expectation = new { Timestamp = (DateTime?)1.January(2020) };
+
+ // Act
+ subject.Should().BeEquivalentTo(expectation, opt => opt
+ .RespectingRuntimeTypes()
+ .Using(new DateTimeByYearComparer()));
+ }
+
+ [Fact]
+ public void An_equality_comparer_of_nullable_type_is_not_invoked_on_non_nullable_data()
+ {
+ // Arrange
+ var subject = new { Timestamp = (DateTime?)22.March(2020) };
+
+ var expectation = new { Timestamp = (DateTime?)1.January(2020) };
+
+ // Act
+ Action act = () => subject.Should().BeEquivalentTo(expectation, opt => opt
+ .RespectingRuntimeTypes()
+ .Using(new NullableDateTimeByYearComparer()));
+
+ // Assert
+ act.Should().Throw().WithMessage("Expected*Timestamp*2020*");
+ }
+ }
+
private interface IInterface
{
}
@@ -270,4 +336,14 @@ public bool Equals(ConcreteClass x, ConcreteClass y)
public int GetHashCode(ConcreteClass obj) => obj.GetProperty().GetHashCode();
}
+
+ private class NullableDateTimeByYearComparer : IEqualityComparer
+ {
+ public bool Equals(DateTime? x, DateTime? y)
+ {
+ return x?.Year == y?.Year;
+ }
+
+ public int GetHashCode(DateTime? obj) => obj?.GetHashCode() ?? 0;
+ }
}
diff --git a/Tests/FluentAssertions.Equivalency.Specs/BasicSpecs.cs b/Tests/FluentAssertions.Equivalency.Specs/BasicSpecs.cs
index c8f3bcd5d9..85b8fd12a2 100644
--- a/Tests/FluentAssertions.Equivalency.Specs/BasicSpecs.cs
+++ b/Tests/FluentAssertions.Equivalency.Specs/BasicSpecs.cs
@@ -645,4 +645,20 @@ public void When_the_graph_contains_guids_it_should_properly_format_them()
// Assert
act.Should().Throw().WithMessage("Expected property actual[0].Id*to be *-*, but found *-*");
}
+
+ [Fact]
+ public void Empty_array_segments_can_be_compared_for_equivalency()
+ {
+ // Arrange
+ var actual = new ClassWithArraySegment();
+ var expected = new ClassWithArraySegment();
+
+ // Act / Assert
+ actual.Should().BeEquivalentTo(expected);
+ }
+
+ private class ClassWithArraySegment
+ {
+ public ArraySegment Segment { get; set; }
+ }
}
diff --git a/Tests/FluentAssertions.Equivalency.Specs/DictionarySpecs.cs b/Tests/FluentAssertions.Equivalency.Specs/DictionarySpecs.cs
index f84c4b9e09..53f32ed40f 100644
--- a/Tests/FluentAssertions.Equivalency.Specs/DictionarySpecs.cs
+++ b/Tests/FluentAssertions.Equivalency.Specs/DictionarySpecs.cs
@@ -2,6 +2,7 @@
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
+using System.Collections.Specialized;
using System.Globalization;
using System.Linq;
using FluentAssertions.Equivalency.Tracing;
@@ -1171,4 +1172,160 @@ public void When_a_custom_rule_is_applied_on_a_dictionary_it_should_apply_it_on_
.WhenTypeIs()
);
}
+
+ [Fact]
+ public void Passing_the_reason_to_the_inner_equivalency_assertion_works()
+ {
+ var subject = new Dictionary
+ {
+ ["a"] = new List()
+ };
+
+ var expected = new Dictionary
+ {
+ ["a"] = new List { 42 }
+ };
+
+ Action act = () => subject.Should().BeEquivalentTo(expected, "FOO {0}", "BAR");
+
+ act.Should().Throw().WithMessage("*FOO BAR*");
+ }
+
+ [Fact]
+ public void A_non_generic_subject_can_be_compared_with_a_generic_expectation()
+ {
+ var subject = new ListDictionary
+ {
+ ["id"] = 22,
+ ["CustomerId"] = 33
+ };
+
+ var expected = new Dictionary
+ {
+ ["id"] = 22,
+ ["CustomerId"] = 33
+ };
+
+ subject.Should().BeEquivalentTo(expected);
+ }
+
+ [Fact]
+ public void A_non_generic_subject_which_is_null_can_be_compared_with_a_generic_expectation()
+ {
+ var subject = (ListDictionary)null;
+
+ var expected = new Dictionary
+ {
+ ["id"] = 22,
+ ["CustomerId"] = 33
+ };
+
+ Action act = () => subject.Should().BeEquivalentTo(expected);
+
+ act.Should().Throw().WithMessage("**");
+ }
+
+ [Fact]
+ public void Excluding_nested_objects_when_dictionary_is_equivalent()
+ {
+ var subject = new Dictionary
+ {
+ ["id"] = 22,
+ ["CustomerId"] = 33
+ };
+
+ var expected = new Dictionary
+ {
+ ["id"] = 22,
+ ["CustomerId"] = 33
+ };
+
+ subject.Should().BeEquivalentTo(expected, opt => opt.ExcludingNestedObjects());
+ }
+
+ [Fact]
+ public void Custom_types_which_implementing_dictionaries_pass()
+ {
+ var subject = new NonGenericChildDictionary
+ {
+ ["id"] = 22,
+ ["CustomerId"] = 33
+ };
+
+ var expected = new Specs.NonGenericDictionary
+ {
+ ["id"] = 22,
+ ["CustomerId"] = 33
+ };
+
+ subject.Should().BeEquivalentTo(expected);
+ }
+
+ [Fact]
+ public void Custom_types_which_implementing_dictionaries_pass_with_swapped_subject_expectation()
+ {
+ var subject = new Specs.NonGenericDictionary
+ {
+ ["id"] = 22,
+ ["CustomerId"] = 33
+ };
+
+ var expected = new NonGenericChildDictionary
+ {
+ ["id"] = 22,
+ ["CustomerId"] = 33
+ };
+
+ subject.Should().BeEquivalentTo(expected);
+ }
+}
+
+internal class NonGenericChildDictionary : Dictionary
+{
+ public new void Add(string key, int value)
+ {
+ base.Add(key, value);
+ }
+}
+
+internal class NonGenericDictionary : IDictionary
+{
+ private readonly Dictionary innerDictionary = new();
+
+ public int this[string key]
+ {
+ get => innerDictionary[key];
+ set => innerDictionary[key] = value;
+ }
+
+ public ICollection Keys => innerDictionary.Keys;
+
+ public ICollection Values => innerDictionary.Values;
+
+ public int Count => innerDictionary.Count;
+
+ public bool IsReadOnly => false;
+
+ public void Add(string key, int value) => innerDictionary.Add(key, value);
+
+ public void Add(KeyValuePair item) => innerDictionary.Add(item.Key, item.Value);
+
+ public void Clear() => innerDictionary.Clear();
+
+ public bool Contains(KeyValuePair item) => innerDictionary.Contains(item);
+
+ public bool ContainsKey(string key) => innerDictionary.ContainsKey(key);
+
+ public void CopyTo(KeyValuePair[] array, int arrayIndex) =>
+ ((ICollection>)innerDictionary).CopyTo(array, arrayIndex);
+
+ public IEnumerator> GetEnumerator() => innerDictionary.GetEnumerator();
+
+ public bool Remove(string key) => innerDictionary.Remove(key);
+
+ public bool Remove(KeyValuePair item) => innerDictionary.Remove(item.Key);
+
+ public bool TryGetValue(string key, out int value) => innerDictionary.TryGetValue(key, out value);
+
+ IEnumerator IEnumerable.GetEnumerator() => innerDictionary.GetEnumerator();
}
diff --git a/Tests/FluentAssertions.Equivalency.Specs/ExtensibilitySpecs.cs b/Tests/FluentAssertions.Equivalency.Specs/ExtensibilitySpecs.cs
index f3a389c635..3dc3649b8a 100644
--- a/Tests/FluentAssertions.Equivalency.Specs/ExtensibilitySpecs.cs
+++ b/Tests/FluentAssertions.Equivalency.Specs/ExtensibilitySpecs.cs
@@ -4,6 +4,7 @@
using System.Linq;
using System.Reflection;
using FluentAssertions.Extensions;
+using JetBrains.Annotations;
using Xunit;
using Xunit.Sdk;
@@ -862,5 +863,268 @@ public EquivalencyResult Handle(Comparands comparands, IEquivalencyValidationCon
}
}
+ [Fact]
+ public void Can_compare_null_against_null_with_custom_comparer_for_nullable_property()
+ {
+ // Arrange
+ var subject = new ClassWithNullableStructProperty();
+
+ // Act / Assert
+ subject.Should().BeEquivalentTo(new ClassWithNullableStructProperty(), o => o
+ .Using()
+ );
+ }
+
+ [Fact]
+ public void Can_compare_null_against_not_null_with_custom_comparer_for_nullable_property()
+ {
+ // Arrange
+ var subject = new ClassWithNullableStructProperty();
+ var unexpected = new ClassWithNullableStructProperty
+ {
+ Value = new StructWithProperties
+ {
+ Value = 42
+ },
+ };
+
+ // Act / Assert
+ subject.Should().NotBeEquivalentTo(unexpected, o => o
+ .Using()
+ );
+ }
+
+ [Fact]
+ public void Can_compare_not_null_against_null_with_custom_comparer_for_nullable_property()
+ {
+ // Arrange
+ var subject = new ClassWithNullableStructProperty
+ {
+ Value = new StructWithProperties
+ {
+ Value = 42
+ },
+ };
+
+ // Act / Assert
+ subject.Should().NotBeEquivalentTo(new ClassWithNullableStructProperty(), o => o
+ .Using()
+ );
+ }
+
+ [Fact]
+ public void Can_compare_not_null_against_not_null_with_custom_comparer_for_nullable_property()
+ {
+ // Arrange
+ var subject = new ClassWithNullableStructProperty
+ {
+ Value = new StructWithProperties
+ {
+ Value = 42
+ },
+ };
+ var expected = new ClassWithNullableStructProperty
+ {
+ Value = new StructWithProperties
+ {
+ Value = 42
+ },
+ };
+
+ // Act / Assert
+ subject.Should().BeEquivalentTo(expected, o => o
+ .Using()
+ );
+ }
+
+ [Fact]
+ public void Can_compare_null_against_null_with_custom_nullable_comparer_for_nullable_property()
+ {
+ // Arrange
+ var subject = new ClassWithNullableStructProperty();
+
+ // Act / Assert
+ subject.Should().BeEquivalentTo(new ClassWithNullableStructProperty(), o => o
+ .Using()
+ );
+ }
+
+ [Fact]
+ public void Can_compare_null_against_not_null_with_custom_nullable_comparer_for_nullable_property()
+ {
+ // Arrange
+ var subject = new ClassWithNullableStructProperty();
+ var unexpected = new ClassWithNullableStructProperty
+ {
+ Value = new StructWithProperties
+ {
+ Value = 42
+ },
+ };
+
+ // Act / Assert
+ subject.Should().NotBeEquivalentTo(unexpected, o => o
+ .Using()
+ );
+ }
+
+ [Fact]
+ public void Can_compare_not_null_against_null_with_custom_nullable_comparer_for_nullable_property()
+ {
+ // Arrange
+ var subject = new ClassWithNullableStructProperty
+ {
+ Value = new StructWithProperties
+ {
+ Value = 42
+ },
+ };
+
+ // Act / Assert
+ subject.Should().NotBeEquivalentTo(new ClassWithNullableStructProperty(), o => o
+ .Using()
+ );
+ }
+
+ [Fact]
+ public void Can_compare_not_null_against_not_null_with_custom_nullable_comparer_for_nullable_property()
+ {
+ // Arrange
+ var subject = new ClassWithNullableStructProperty
+ {
+ Value = new StructWithProperties
+ {
+ Value = 42
+ },
+ };
+ var expected = new ClassWithNullableStructProperty
+ {
+ Value = new StructWithProperties
+ {
+ Value = 42
+ },
+ };
+
+ // Act / Assert
+ subject.Should().BeEquivalentTo(expected, o => o
+ .Using()
+ );
+ }
+
+ private class ClassWithNullableStructProperty
+ {
+ [UsedImplicitly]
+ public StructWithProperties? Value { get; set; }
+ }
+
+ private struct StructWithProperties
+ {
+ // ReSharper disable once UnusedAutoPropertyAccessor.Local
+ public int Value { get; set; }
+ }
+
+ private class StructWithPropertiesComparer : IEqualityComparer, IEqualityComparer
+ {
+ public bool Equals(StructWithProperties x, StructWithProperties y) => Equals(x.Value, y.Value);
+
+ public int GetHashCode(StructWithProperties obj) => obj.Value;
+
+ public bool Equals(StructWithProperties? x, StructWithProperties? y) => Equals(x?.Value, y?.Value);
+
+ public int GetHashCode(StructWithProperties? obj) => obj?.Value ?? 0;
+ }
+
+ [Fact]
+ public void Can_compare_null_against_null_with_custom_comparer_for_property()
+ {
+ // Arrange
+ var subject = new ClassWithClassProperty();
+
+ // Act / Assert
+ subject.Should().BeEquivalentTo(new ClassWithClassProperty(), o => o
+ .Using()
+ );
+ }
+
+ [Fact]
+ public void Can_compare_null_against_not_null_with_custom_comparer_for_property()
+ {
+ // Arrange
+ var subject = new ClassWithClassProperty();
+ var unexpected = new ClassWithClassProperty
+ {
+ Value = new ClassProperty
+ {
+ Value = 42
+ },
+ };
+
+ // Act / Assert
+ subject.Should().NotBeEquivalentTo(unexpected, o => o
+ .Using()
+ );
+ }
+
+ [Fact]
+ public void Can_compare_not_null_against_null_with_custom_comparer_for_property()
+ {
+ // Arrange
+ var subject = new ClassWithClassProperty
+ {
+ Value = new ClassProperty
+ {
+ Value = 42
+ },
+ };
+
+ // Act / Assert
+ subject.Should().NotBeEquivalentTo(new ClassWithClassProperty(), o => o
+ .Using()
+ );
+ }
+
+ [Fact]
+ public void Can_compare_not_null_against_not_null_with_custom_comparer_for_property()
+ {
+ // Arrange
+ var subject = new ClassWithClassProperty
+ {
+ Value = new ClassProperty
+ {
+ Value = 42
+ },
+ };
+ var expected = new ClassWithClassProperty
+ {
+ Value = new ClassProperty
+ {
+ Value = 42
+ },
+ };
+
+ // Act / Assert
+ subject.Should().BeEquivalentTo(expected, o => o
+ .Using()
+ );
+ }
+
+ private class ClassWithClassProperty
+ {
+ // ReSharper disable once UnusedAutoPropertyAccessor.Local
+ public ClassProperty Value { get; set; }
+ }
+
+ public class ClassProperty
+ {
+ public int Value { get; set; }
+ }
+
+ private class ClassPropertyComparer : IEqualityComparer
+ {
+ public bool Equals(ClassProperty x, ClassProperty y) => Equals(x?.Value, y?.Value);
+
+ public int GetHashCode(ClassProperty obj) => obj.Value;
+ }
+
#endregion
}
diff --git a/Tests/FluentAssertions.Equivalency.Specs/FluentAssertions.Equivalency.Specs.csproj b/Tests/FluentAssertions.Equivalency.Specs/FluentAssertions.Equivalency.Specs.csproj
index a3b8f16c2f..92f192b555 100644
--- a/Tests/FluentAssertions.Equivalency.Specs/FluentAssertions.Equivalency.Specs.csproj
+++ b/Tests/FluentAssertions.Equivalency.Specs/FluentAssertions.Equivalency.Specs.csproj
@@ -57,6 +57,7 @@
+
diff --git a/Tests/FluentAssertions.Equivalency.Specs/MemberMatchingSpecs.cs b/Tests/FluentAssertions.Equivalency.Specs/MemberMatchingSpecs.cs
index 7808394542..231c68de0a 100644
--- a/Tests/FluentAssertions.Equivalency.Specs/MemberMatchingSpecs.cs
+++ b/Tests/FluentAssertions.Equivalency.Specs/MemberMatchingSpecs.cs
@@ -62,8 +62,8 @@ public void Nested_properties_can_be_mapped_using_a_nested_expression()
subject.Should()
.BeEquivalentTo(expectation, opt => opt
.WithMapping(
- e => e.Parent[0].Property2,
- s => s.Parent[0].Property1));
+ e => e.Children[0].Property2,
+ s => s.Children[0].Property1));
}
[Fact]
@@ -83,6 +83,25 @@ public void Nested_properties_can_be_mapped_using_a_nested_type_and_property_nam
.WithMapping("Property2", "Property1"));
}
+ [Fact]
+ public void Nested_explicitly_implemented_properties_can_be_mapped_using_a_nested_type_and_property_names()
+ {
+ // Arrange
+ var subject = new ParentOfSubjectWithExplicitlyImplementedProperty(new[] { new SubjectWithExplicitImplementedProperty() });
+
+ ((IProperty)subject.Children[0]).Property = "Hello";
+
+ var expectation = new ParentOfExpectationWithProperty2(new[]
+ {
+ new ExpectationWithProperty2 { Property2 = "Hello" }
+ });
+
+ // Act / Assert
+ subject.Should()
+ .BeEquivalentTo(expectation, opt => opt
+ .WithMapping("Property2", "Property"));
+ }
+
[Fact]
public void Nested_fields_can_be_mapped_using_a_nested_type_and_field_names()
{
@@ -144,6 +163,25 @@ public void Properties_can_be_mapped_by_name()
.WithMapping("Property2", "Property1"));
}
+ [Fact]
+ public void Properties_can_be_mapped_by_name_to_an_explicitly_implemented_property()
+ {
+ // Arrange
+ var subject = new SubjectWithExplicitImplementedProperty();
+
+ ((IProperty)subject).Property = "Hello";
+
+ var expectation = new ExpectationWithProperty2
+ {
+ Property2 = "Hello"
+ };
+
+ // Act / Assert
+ subject.Should()
+ .BeEquivalentTo(expectation, opt => opt
+ .WithMapping("Property2", "Property"));
+ }
+
[Fact]
public void Fields_can_be_mapped_by_name()
{
@@ -515,21 +553,31 @@ private class EntityDto
internal class ParentOfExpectationWithProperty2
{
- public ExpectationWithProperty2[] Parent { get; }
+ public ExpectationWithProperty2[] Children { get; }
- public ParentOfExpectationWithProperty2(ExpectationWithProperty2[] parent)
+ public ParentOfExpectationWithProperty2(ExpectationWithProperty2[] children)
{
- Parent = parent;
+ Children = children;
}
}
internal class ParentOfSubjectWithProperty1
{
- public SubjectWithProperty1[] Parent { get; }
+ public SubjectWithProperty1[] Children { get; }
+
+ public ParentOfSubjectWithProperty1(SubjectWithProperty1[] children)
+ {
+ Children = children;
+ }
+ }
+
+ internal class ParentOfSubjectWithExplicitlyImplementedProperty
+ {
+ public SubjectWithExplicitImplementedProperty[] Children { get; }
- public ParentOfSubjectWithProperty1(SubjectWithProperty1[] parent)
+ public ParentOfSubjectWithExplicitlyImplementedProperty(SubjectWithExplicitImplementedProperty[] children)
{
- Parent = parent;
+ Children = children;
}
}
@@ -538,6 +586,16 @@ internal class SubjectWithProperty1
public string Property1 { get; set; }
}
+ internal class SubjectWithExplicitImplementedProperty : IProperty
+ {
+ string IProperty.Property { get; set; }
+ }
+
+ internal interface IProperty
+ {
+ string Property { get; set; }
+ }
+
internal class ExpectationWithProperty2
{
public string Property2 { get; set; }
diff --git a/Tests/FluentAssertions.Equivalency.Specs/SelectionRulesSpecs.cs b/Tests/FluentAssertions.Equivalency.Specs/SelectionRulesSpecs.cs
index 3d7508dac1..0b02f9b911 100644
--- a/Tests/FluentAssertions.Equivalency.Specs/SelectionRulesSpecs.cs
+++ b/Tests/FluentAssertions.Equivalency.Specs/SelectionRulesSpecs.cs
@@ -5,6 +5,7 @@
using System.Globalization;
using System.Linq;
using FluentAssertions.Common;
+using JetBrains.Annotations;
using Xunit;
using Xunit.Sdk;
@@ -14,6 +15,64 @@ public class SelectionRulesSpecs
{
public class Basic
{
+ [Fact]
+ public void Property_names_are_case_sensitive()
+ {
+ // Arrange
+ var subject = new
+ {
+ Name = "John"
+ };
+
+ var other = new
+ {
+ name = "John"
+ };
+
+ // Act
+ Action act = () => subject.Should().BeEquivalentTo(other);
+
+ // Assert
+ act.Should().Throw().WithMessage(
+ "Expectation*subject.name**other*not have*");
+ }
+
+ [Fact]
+ public void Field_names_are_case_sensitive()
+ {
+ // Arrange
+ var subject = new ClassWithFieldInUpperCase
+ {
+ Name = "John"
+ };
+
+ var other = new ClassWithFieldInLowerCase
+ {
+ name = "John"
+ };
+
+ // Act
+ Action act = () => subject.Should().BeEquivalentTo(other);
+
+ // Assert
+ act.Should().Throw().WithMessage(
+ "Expectation*subject.name**other*not have*");
+ }
+
+ private class ClassWithFieldInLowerCase
+ {
+ [UsedImplicitly]
+#pragma warning disable SA1307
+ public string name;
+#pragma warning restore SA1307
+ }
+
+ private class ClassWithFieldInUpperCase
+ {
+ [UsedImplicitly]
+ public string Name;
+ }
+
[Fact]
public void When_a_property_is_an_indexer_it_should_be_ignored()
{
@@ -37,6 +96,7 @@ public void When_a_property_is_an_indexer_it_should_be_ignored()
public class ClassWithIndexer
{
+ [UsedImplicitly]
public object Foo { get; set; }
public string this[int n] =>
@@ -231,8 +291,10 @@ public void A_nested_class_without_properties_inside_a_collection_is_fine()
internal class BaseClassPointingToClassWithoutProperties
{
+ [UsedImplicitly]
public string Name { get; set; }
+ [UsedImplicitly]
public ClassWithoutProperty ClassWithoutProperty { get; } = new();
}
@@ -395,6 +457,7 @@ private enum LocalType : byte
public class CustomType
{
+ [UsedImplicitly]
public string Name { get; set; }
}
@@ -847,13 +910,14 @@ public void When_members_are_excluded_by_the_access_modifier_of_the_getter_using
"internal", "protected-internal", "private", "private-protected");
var expected = new ClassWithAllAccessModifiersForMembers("public", "protected",
- "ignored-internal", "ignored-protected-internal", "private", "ignore-private-protected");
+ "ignored-internal", "ignored-protected-internal", "private", "private-protected");
// Act
- Action act = () => subject.Should().BeEquivalentTo(expected, config =>
- config.Excluding(ctx => ctx.WhichGetterHas(CSharpAccessModifier.Internal) ||
- ctx.WhichGetterHas(CSharpAccessModifier.ProtectedInternal) ||
- ctx.WhichGetterHas(CSharpAccessModifier.PrivateProtected)));
+ Action act = () => subject.Should().BeEquivalentTo(expected, config => config
+ .IncludingInternalFields()
+ .Excluding(ctx =>
+ ctx.WhichGetterHas(CSharpAccessModifier.Internal) ||
+ ctx.WhichGetterHas(CSharpAccessModifier.ProtectedInternal)));
// Assert
act.Should().NotThrow();
@@ -867,14 +931,15 @@ public void When_members_are_excluded_by_the_access_modifier_of_the_setter_using
"internal", "protected-internal", "private", "private-protected");
var expected = new ClassWithAllAccessModifiersForMembers("public", "protected",
- "ignored-internal", "ignored-protected-internal", "ignored-private", "ignore-private-protected");
+ "ignored-internal", "ignored-protected-internal", "ignored-private", "private-protected");
// Act
- Action act = () => subject.Should().BeEquivalentTo(expected, config =>
- config.Excluding(ctx => ctx.WhichSetterHas(CSharpAccessModifier.Internal) ||
+ Action act = () => subject.Should().BeEquivalentTo(expected, config => config
+ .IncludingInternalFields()
+ .Excluding(ctx =>
+ ctx.WhichSetterHas(CSharpAccessModifier.Internal) ||
ctx.WhichSetterHas(CSharpAccessModifier.ProtectedInternal) ||
- ctx.WhichSetterHas(CSharpAccessModifier.Private) ||
- ctx.WhichSetterHas(CSharpAccessModifier.PrivateProtected)));
+ ctx.WhichSetterHas(CSharpAccessModifier.Private)));
// Assert
act.Should().NotThrow();
@@ -1300,10 +1365,13 @@ public void When_a_property_is_internal_and_it_should_be_included_it_should_fail
private class ClassWithInternalProperty
{
+ [UsedImplicitly]
public string PublicProperty { get; set; }
+ [UsedImplicitly]
internal string InternalProperty { get; set; }
+ [UsedImplicitly]
protected internal string ProtectedInternalProperty { get; set; }
}
@@ -1356,10 +1424,13 @@ public void When_a_field_is_internal_and_it_should_be_included_it_should_fail_th
private class ClassWithInternalField
{
+ [UsedImplicitly]
public string PublicField;
+ [UsedImplicitly]
internal string InternalField;
+ [UsedImplicitly]
protected internal string ProtectedInternalField;
}
@@ -1384,6 +1455,58 @@ public void When_a_property_is_internal_it_should_be_excluded_from_the_compariso
// Act / Assert
actual.Should().BeEquivalentTo(expected);
}
+
+ [Fact]
+ public void Private_protected_properties_are_ignored()
+ {
+ // Arrange
+ var subject = new ClassWithPrivateProtectedProperty("Name", 13);
+ var other = new ClassWithPrivateProtectedProperty("Name", 37);
+
+ // Act/Assert
+ subject.Should().BeEquivalentTo(other);
+ }
+
+ private class ClassWithPrivateProtectedProperty
+ {
+ public ClassWithPrivateProtectedProperty(string name, int value)
+ {
+ Name = name;
+ Value = value;
+ }
+
+ [UsedImplicitly]
+ public string Name { get; }
+
+ [UsedImplicitly]
+ private protected int Value { get; }
+ }
+
+ [Fact]
+ public void Private_protected_fields_are_ignored()
+ {
+ // Arrange
+ var subject = new ClassWithPrivateProtectedField("Name", 13);
+ var other = new ClassWithPrivateProtectedField("Name", 37);
+
+ // Act/Assert
+ subject.Should().BeEquivalentTo(other);
+ }
+
+ private class ClassWithPrivateProtectedField
+ {
+ public ClassWithPrivateProtectedField(string name, int value)
+ {
+ Name = name;
+ this.value = value;
+ }
+
+ [UsedImplicitly]
+ public string Name;
+
+ [UsedImplicitly]
+ private protected int value;
+ }
}
public class MemberHiding
@@ -1524,26 +1647,31 @@ public void Excluding_the_property_hiding_the_base_class_one_does_not_reveal_the
private class BaseWithProperty
{
+ [UsedImplicitly]
public object Property { get; set; }
}
private class SubclassAHidingProperty : BaseWithProperty
{
+ [UsedImplicitly]
public new T Property { get; set; }
}
private class BaseWithStringProperty
{
+ [UsedImplicitly]
public string Property { get; set; }
}
private class SubclassHidingStringProperty : BaseWithStringProperty
{
+ [UsedImplicitly]
public new string Property { get; set; }
}
private class AnotherBaseWithProperty
{
+ [UsedImplicitly]
public object Property { get; set; }
}
@@ -1672,21 +1800,25 @@ public void Excluding_the_field_hiding_the_base_class_one_does_not_reveal_the_la
private class BaseWithField
{
+ [UsedImplicitly]
public string Field;
}
private class SubclassAHidingField : BaseWithField
{
+ [UsedImplicitly]
public new string Field;
}
private class AnotherBaseWithField
{
+ [UsedImplicitly]
public string Field;
}
private class SubclassBHidingField : AnotherBaseWithField
{
+ [UsedImplicitly]
public new string Field;
}
}
@@ -1763,7 +1895,7 @@ public void When_a_reference_to_an_explicit_interface_impl_is_provided_it_should
}
[Fact]
- public void When_respecting_declared_types_explicit_interface_member_on_interfaced_subject_should_be_used()
+ public void Explicitly_implemented_subject_properties_are_ignored_if_a_normal_property_exists_with_the_same_name()
{
// Arrange
IVehicle expected = new Vehicle
@@ -1773,122 +1905,118 @@ public void When_respecting_declared_types_explicit_interface_member_on_interfac
IVehicle subject = new ExplicitVehicle
{
- VehicleId = 2 // instance member
+ VehicleId = 2 // normal property
};
- subject.VehicleId = 1; // interface member
+ subject.VehicleId = 1; // explicitly implemented property
// Act
- Action action = () => subject.Should().BeEquivalentTo(expected, opt => opt.RespectingDeclaredTypes());
+ Action action = () => subject.Should().BeEquivalentTo(expected);
// Assert
- action.Should().NotThrow();
+ action.Should().Throw();
}
[Fact]
- public void When_respecting_declared_types_explicit_interface_member_on_interfaced_expectation_should_be_used()
+ public void Explicitly_implemented_read_only_subject_properties_are_ignored_if_a_normal_property_exists_with_the_same_name()
{
// Arrange
- IVehicle expected = new ExplicitVehicle
+ IReadOnlyVehicle subject = new ExplicitReadOnlyVehicle(explicitValue: 1)
{
- VehicleId = 2 // instance member
+ VehicleId = 2 // normal property
};
- expected.VehicleId = 1; // interface member
-
- IVehicle subject = new Vehicle
+ var expected = new Vehicle
{
VehicleId = 1
};
// Act
- Action action = () => subject.Should().BeEquivalentTo(expected, opt => opt.RespectingDeclaredTypes());
+ Action action = () => subject.Should().BeEquivalentTo(expected);
// Assert
- action.Should().NotThrow();
+ action.Should().Throw();
}
[Fact]
- public void When_respecting_runtime_types_explicit_interface_member_on_interfaced_subject_should_not_be_used()
+ public void Explicitly_implemented_subject_properties_are_ignored_if_only_fields_are_included()
{
// Arrange
- IVehicle expected = new Vehicle
+ var expected = new VehicleWithField
{
- VehicleId = 1
+ VehicleId = 1 // A field named like a property
};
- IVehicle subject = new ExplicitVehicle
+ var subject = new ExplicitVehicle
{
- VehicleId = 2 // instance member
+ VehicleId = 2 // A real property
};
- subject.VehicleId = 1; // interface member
-
// Act
- Action action = () => subject.Should().BeEquivalentTo(expected, opt => opt.RespectingRuntimeTypes());
+ Action action = () => subject.Should().BeEquivalentTo(expected, opt => opt
+ .IncludingFields()
+ .ExcludingProperties());
// Assert
- action.Should().Throw();
+ action.Should().Throw().WithMessage("*field*VehicleId*other*");
}
[Fact]
- public void When_respecting_runtime_types_explicit_interface_member_on_interfaced_expectation_should_not_be_used()
+ public void Explicitly_implemented_subject_properties_are_ignored_if_only_fields_are_included_and_they_may_be_missing()
{
// Arrange
- IVehicle expected = new ExplicitVehicle
+ var expected = new VehicleWithField
{
- VehicleId = 2 // instance member
+ VehicleId = 1 // A field named like a property
};
- expected.VehicleId = 1; // interface member
-
- IVehicle subject = new Vehicle
+ var subject = new ExplicitVehicle
{
- VehicleId = 1
+ VehicleId = 2 // A real property
};
- // Act
- Action action = () => subject.Should().BeEquivalentTo(expected, opt => opt.RespectingRuntimeTypes());
-
- // Assert
- action.Should().Throw();
+ // Act / Assert
+ subject.Should().BeEquivalentTo(expected, opt => opt
+ .IncludingFields()
+ .ExcludingProperties()
+ .ExcludingMissingMembers());
}
[Fact]
- public void When_respecting_declared_types_explicit_interface_member_on_subject_should_not_be_used()
+ public void Excluding_missing_members_does_not_affect_how_explicitly_implemented_subject_properties_are_dealt_with()
{
// Arrange
- var expected = new Vehicle
+ IVehicle expected = new Vehicle
{
VehicleId = 1
};
- var subject = new ExplicitVehicle
+ IVehicle subject = new ExplicitVehicle
{
- VehicleId = 2
+ VehicleId = 2 // instance member
};
- ((IVehicle)subject).VehicleId = 1;
+ subject.VehicleId = 1; // interface member
// Act
- Action action = () => subject.Should().BeEquivalentTo(expected, opt => opt.RespectingDeclaredTypes());
+ Action action = () => subject.Should().BeEquivalentTo(expected, opt => opt.ExcludingMissingMembers());
// Assert
action.Should().Throw();
}
[Fact]
- public void When_respecting_declared_types_explicit_interface_member_on_expectation_should_not_be_used()
+ public void When_respecting_declared_types_explicit_interface_member_on_interfaced_expectation_should_be_used()
{
// Arrange
- var expected = new ExplicitVehicle
+ IVehicle expected = new ExplicitVehicle
{
- VehicleId = 2
+ VehicleId = 2 // instance member
};
- ((IVehicle)expected).VehicleId = 1;
+ expected.VehicleId = 1; // interface member
- var subject = new Vehicle
+ IVehicle subject = new Vehicle
{
VehicleId = 1
};
@@ -1897,24 +2025,24 @@ public void When_respecting_declared_types_explicit_interface_member_on_expectat
Action action = () => subject.Should().BeEquivalentTo(expected, opt => opt.RespectingDeclaredTypes());
// Assert
- action.Should().Throw();
+ action.Should().NotThrow();
}
[Fact]
- public void When_respecting_runtime_types_explicit_interface_member_on_subject_should_not_be_used()
+ public void When_respecting_runtime_types_explicit_interface_member_on_interfaced_subject_should_not_be_used()
{
// Arrange
- var expected = new Vehicle
+ IVehicle expected = new Vehicle
{
VehicleId = 1
};
- var subject = new ExplicitVehicle
+ IVehicle subject = new ExplicitVehicle
{
- VehicleId = 2
+ VehicleId = 2 // instance member
};
- ((IVehicle)subject).VehicleId = 1;
+ subject.VehicleId = 1; // interface member
// Act
Action action = () => subject.Should().BeEquivalentTo(expected, opt => opt.RespectingRuntimeTypes());
@@ -1924,7 +2052,30 @@ public void When_respecting_runtime_types_explicit_interface_member_on_subject_s
}
[Fact]
- public void When_respecting_runtime_types_explicit_interface_member_on_expectation_should_not_be_used()
+ public void When_respecting_runtime_types_explicit_interface_member_on_interfaced_expectation_should_not_be_used()
+ {
+ // Arrange
+ IVehicle expected = new ExplicitVehicle
+ {
+ VehicleId = 2 // instance member
+ };
+
+ expected.VehicleId = 1; // interface member
+
+ IVehicle subject = new Vehicle
+ {
+ VehicleId = 1
+ };
+
+ // Act
+ Action action = () => subject.Should().BeEquivalentTo(expected, opt => opt.RespectingRuntimeTypes());
+
+ // Assert
+ action.Should().Throw();
+ }
+
+ [Fact]
+ public void When_respecting_declared_types_explicit_interface_member_on_expectation_should_not_be_used()
{
// Arrange
var expected = new ExplicitVehicle
@@ -1940,12 +2091,33 @@ public void When_respecting_runtime_types_explicit_interface_member_on_expectati
};
// Act
- Action action = () => subject.Should().BeEquivalentTo(expected, opt => opt.RespectingRuntimeTypes());
+ Action action = () => subject.Should().BeEquivalentTo(expected);
// Assert
action.Should().Throw();
}
+ [Fact]
+ public void Can_find_explicitly_implemented_property_on_the_subject()
+ {
+ // Arrange
+ IPerson person = new Person();
+ person.Name = "Bob";
+
+ // Act / Assert
+ person.Should().BeEquivalentTo(new { Name = "Bob" });
+ }
+
+ private interface IPerson
+ {
+ string Name { get; set; }
+ }
+
+ private class Person : IPerson
+ {
+ string IPerson.Name { get; set; }
+ }
+
[Fact]
public void Excluding_an_interface_property_through_inheritance_should_work()
{
@@ -2085,11 +2257,13 @@ public void Excluding_a_covariant_property_through_the_base_class_excludes_the_b
private class BaseWithProperty
{
+ [UsedImplicitly]
public string BaseProperty { get; set; }
}
private class DerivedWithProperty : BaseWithProperty
{
+ [UsedImplicitly]
public string DerivedProperty { get; set; }
}
@@ -2102,6 +2276,7 @@ private sealed class DerivedWithCovariantOverride : BaseWithAbstractProperty
{
public override DerivedWithProperty Property { get; }
+ [UsedImplicitly]
public string OtherProp { get; set; }
public DerivedWithCovariantOverride(DerivedWithProperty prop)
@@ -2649,45 +2824,59 @@ public void
private class ClassWithNonBrowsableMembers
{
+ [UsedImplicitly]
public int BrowsableField = -1;
+ [UsedImplicitly]
public int BrowsableProperty { get; set; }
+ [UsedImplicitly]
[EditorBrowsable(EditorBrowsableState.Always)]
public int ExplicitlyBrowsableField = -1;
+ [UsedImplicitly]
[EditorBrowsable(EditorBrowsableState.Always)]
public int ExplicitlyBrowsableProperty { get; set; }
+ [UsedImplicitly]
[EditorBrowsable(EditorBrowsableState.Advanced)]
public int AdvancedBrowsableField = -1;
+ [UsedImplicitly]
[EditorBrowsable(EditorBrowsableState.Advanced)]
public int AdvancedBrowsableProperty { get; set; }
+ [UsedImplicitly]
[EditorBrowsable(EditorBrowsableState.Never)]
public int NonBrowsableField = -1;
+ [UsedImplicitly]
[EditorBrowsable(EditorBrowsableState.Never)]
public int NonBrowsableProperty { get; set; }
}
private class ClassWhereMemberThatCouldBeNonBrowsableIsBrowsable
{
+ [UsedImplicitly]
public int BrowsableProperty { get; set; }
+ [UsedImplicitly]
public int FieldThatMightBeNonBrowsable = -1;
+ [UsedImplicitly]
public int PropertyThatMightBeNonBrowsable { get; set; }
}
private class ClassWhereMemberThatCouldBeNonBrowsableIsNonBrowsable
{
+ [UsedImplicitly]
public int BrowsableProperty { get; set; }
+ [UsedImplicitly]
[EditorBrowsable(EditorBrowsableState.Never)]
public int FieldThatMightBeNonBrowsable = -1;
+ [UsedImplicitly]
[EditorBrowsable(EditorBrowsableState.Never)]
public int PropertyThatMightBeNonBrowsable { get; set; }
}
diff --git a/Tests/FluentAssertions.Equivalency.Specs/TestTypes.cs b/Tests/FluentAssertions.Equivalency.Specs/TestTypes.cs
index 1ab9c89f23..01cef098d1 100644
--- a/Tests/FluentAssertions.Equivalency.Specs/TestTypes.cs
+++ b/Tests/FluentAssertions.Equivalency.Specs/TestTypes.cs
@@ -43,7 +43,7 @@ public class ClassWithWriteOnlyProperty
public int WriteOnlyProperty
{
- set { writeOnlyPropertyValue = value; }
+ set => writeOnlyPropertyValue = value;
}
public string SomeOtherProperty { get; set; }
@@ -616,6 +616,11 @@ public class Vehicle : IVehicle
public int VehicleId { get; set; }
}
+public class VehicleWithField
+{
+ public int VehicleId;
+}
+
public class ExplicitVehicle : IVehicle
{
int IVehicle.VehicleId { get; set; }
@@ -623,6 +628,25 @@ public class ExplicitVehicle : IVehicle
public int VehicleId { get; set; }
}
+public interface IReadOnlyVehicle
+{
+ int VehicleId { get; }
+}
+
+public class ExplicitReadOnlyVehicle : IReadOnlyVehicle
+{
+ private readonly int explicitValue;
+
+ public ExplicitReadOnlyVehicle(int explicitValue)
+ {
+ this.explicitValue = explicitValue;
+ }
+
+ int IReadOnlyVehicle.VehicleId => explicitValue;
+
+ public int VehicleId { get; set; }
+}
+
public interface ICar : IVehicle
{
int Wheels { get; set; }
diff --git a/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.BeEmpty.cs b/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.BeEmpty.cs
index 4274caa438..d26a2ebb3a 100644
--- a/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.BeEmpty.cs
+++ b/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.BeEmpty.cs
@@ -35,7 +35,7 @@ public void When_collection_is_not_empty_unexpectedly_it_should_throw()
// Assert
act.Should().Throw()
- .WithMessage("*to be empty because that's what we expect, but found*1*2*3*");
+ .WithMessage("*to be empty because that's what we expect, but found at least one item*1*");
}
[Fact]
@@ -119,6 +119,21 @@ public void When_asserting_collection_to_be_empty_it_should_enumerate_only_once(
collection.GetEnumeratorCallCount.Should().Be(1);
}
+ [Fact]
+ public void When_asserting_non_empty_collection_is_empty_it_should_enumerate_only_once()
+ {
+ // Arrange
+ var collection = new CountingGenericEnumerable(new[] { 1, 2, 3 });
+
+ // Act
+ Action act = () => collection.Should().BeEmpty();
+
+ // Assert
+ act.Should().Throw()
+ .WithMessage("*to be empty, but found at least one item {1}.");
+ collection.GetEnumeratorCallCount.Should().Be(1);
+ }
+
[Fact]
public void When_asserting_collection_to_not_be_empty_but_collection_is_null_it_should_throw()
{
diff --git a/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.BeNullOrEmpty.cs b/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.BeNullOrEmpty.cs
index 5015bf3f5f..28c1246187 100644
--- a/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.BeNullOrEmpty.cs
+++ b/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.BeNullOrEmpty.cs
@@ -46,7 +46,7 @@ public void
// Assert
act.Should().Throw()
.WithMessage(
- "Expected collection to be null or empty because we want to test the failure message, but found {1, 2, 3}.");
+ "Expected collection to be null or empty because we want to test the failure message, but found at least one item {1}.");
}
[Fact]
@@ -61,6 +61,21 @@ public void When_asserting_collection_to_be_null_or_empty_it_should_enumerate_on
// Assert
collection.GetEnumeratorCallCount.Should().Be(1);
}
+
+ [Fact]
+ public void When_asserting_non_empty_collection_is_null_or_empty_it_should_enumerate_only_once()
+ {
+ // Arrange
+ var collection = new CountingGenericEnumerable(new[] { 1, 2, 3 });
+
+ // Act
+ Action act = () => collection.Should().BeNullOrEmpty();
+
+ // Assert
+ act.Should().Throw()
+ .WithMessage("*to be null or empty, but found at least one item {1}.");
+ collection.GetEnumeratorCallCount.Should().Be(1);
+ }
}
public class NotBeNullOrEmpty
diff --git a/Tests/FluentAssertions.Specs/Collections/GenericCollectionAssertionOfStringSpecs.cs b/Tests/FluentAssertions.Specs/Collections/GenericCollectionAssertionOfStringSpecs.cs
index b03855eb1c..a4a7a9eb98 100644
--- a/Tests/FluentAssertions.Specs/Collections/GenericCollectionAssertionOfStringSpecs.cs
+++ b/Tests/FluentAssertions.Specs/Collections/GenericCollectionAssertionOfStringSpecs.cs
@@ -1339,7 +1339,7 @@ public void When_the_collection_is_not_empty_unexpectedly_it_should_throw()
act
.Should().Throw()
.WithMessage(
- "Expected collection to be empty because we want to test the failure message, but found*one*two*three*");
+ "Expected collection to be empty because we want to test the failure message, but found at least one item*one*");
}
[Fact]
diff --git a/Tests/FluentAssertions.Specs/Collections/GenericDictionaryAssertionSpecs.cs b/Tests/FluentAssertions.Specs/Collections/GenericDictionaryAssertionSpecs.cs
index 536d660726..3e7ce92c99 100644
--- a/Tests/FluentAssertions.Specs/Collections/GenericDictionaryAssertionSpecs.cs
+++ b/Tests/FluentAssertions.Specs/Collections/GenericDictionaryAssertionSpecs.cs
@@ -877,7 +877,7 @@ public void Should_fail_with_descriptive_message_when_asserting_dictionary_with_
// Assert
act.Should().Throw()
.WithMessage(
- "Expected dictionary to be empty because we want to test the failure message, but found {[1] = \"One\"}.");
+ "Expected dictionary to be empty because we want to test the failure message, but found at least one item {[1, One]}.");
}
[Fact]
diff --git a/Tests/FluentAssertions.Specs/Execution/AssertionScope.MessageFormatingSpecs.cs b/Tests/FluentAssertions.Specs/Execution/AssertionScope.MessageFormatingSpecs.cs
index b773d14759..53119f1f6e 100644
--- a/Tests/FluentAssertions.Specs/Execution/AssertionScope.MessageFormatingSpecs.cs
+++ b/Tests/FluentAssertions.Specs/Execution/AssertionScope.MessageFormatingSpecs.cs
@@ -66,6 +66,38 @@ public void Message_should_use_the_lazy_name_of_the_scope_as_context()
.WithMessage("Expected lazy foo to be equal to*");
}
+ [Fact]
+ public void Nested_scopes_use_the_name_of_their_outer_scope_as_context()
+ {
+ // Act
+ Action act = () =>
+ {
+ using var outerScope = new AssertionScope("outer");
+ using var innerScope = new AssertionScope("inner");
+ new[] { 1, 2, 3 }.Should().Equal(3, 2, 1);
+ };
+
+ // Assert
+ act.Should().Throw()
+ .WithMessage("Expected outer/inner to be equal to*");
+ }
+
+ [Fact]
+ public void The_inner_scope_is_used_when_the_outer_scope_does_not_have_a_context()
+ {
+ // Act
+ Action act = () =>
+ {
+ using var outerScope = new AssertionScope();
+ using var innerScope = new AssertionScope("inner");
+ new[] { 1, 2, 3 }.Should().Equal(3, 2, 1);
+ };
+
+ // Assert
+ act.Should().Throw()
+ .WithMessage("Expected inner to be equal to*");
+ }
+
[Fact]
public void Message_should_contain_each_unique_failed_assertion_seperately()
{
diff --git a/Tests/FluentAssertions.Specs/Execution/AssertionScopeSpecs.cs b/Tests/FluentAssertions.Specs/Execution/AssertionScopeSpecs.cs
index 09f14fad11..93707aa0e4 100644
--- a/Tests/FluentAssertions.Specs/Execution/AssertionScopeSpecs.cs
+++ b/Tests/FluentAssertions.Specs/Execution/AssertionScopeSpecs.cs
@@ -252,6 +252,40 @@ public void When_nested_scope_is_disposed_it_passes_reports_to_parent_scope()
outerScope.Get("innerReportable").Should().Be("bar");
}
+ [Fact]
+ public void Formatting_options_passed_to_inner_assertion_scopes()
+ {
+ // Arrange
+ var subject = new[]
+ {
+ new
+ {
+ Value = 42
+ }
+ };
+
+ var expected = new[]
+ {
+ new
+ {
+ Value = 42
+ },
+ new
+ {
+ Value = 42
+ }
+ };
+
+ // Act
+ using var scope = new AssertionScope();
+ scope.FormattingOptions.MaxDepth = 1;
+ subject.Should().BeEquivalentTo(expected);
+
+ // Assert
+ scope.Discard().Should().ContainSingle()
+ .Which.Should().Contain("Maximum recursion depth of 1 was reached");
+ }
+
public class CustomAssertionStrategy : IAssertionStrategy
{
private readonly List failureMessages = new();
diff --git a/Tests/FluentAssertions.Specs/Execution/CallerIdentifierSpecs.cs b/Tests/FluentAssertions.Specs/Execution/CallerIdentifierSpecs.cs
index 6af9c8ce86..f5a0d05f84 100644
--- a/Tests/FluentAssertions.Specs/Execution/CallerIdentifierSpecs.cs
+++ b/Tests/FluentAssertions.Specs/Execution/CallerIdentifierSpecs.cs
@@ -4,6 +4,7 @@
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
+using FluentAssertions.Equivalency;
using FluentAssertions.Execution;
using FluentAssertions.Extensions;
using Xunit;
@@ -548,6 +549,28 @@ public void An_object_initializer_preceding_an_assertion_is_not_an_identifier()
act.Should().Throw()
.WithMessage("Expected object to be*");
}
+
+ [Fact]
+ public void All_core_code_anywhere_in_the_stack_trace_is_ignored()
+ {
+ /*
+ We want to test this specific scenario.
+
+ 1. CallerIdentifier.DetermineCallerIdentity
+ 2. FluentAssertions code
+ 3. Custom extension <--- pointed to by lastUserStackFrameBeforeFluentAssertionsCodeIndex
+ 4. FluentAssertions code <--- this is where DetermineCallerIdentity tried to get the variable name from before the fix
+ 5. Test
+ */
+
+ var node = Node.From(GetSubjectId);
+
+ // Assert
+ node.Description.Should().StartWith("node.Description");
+ }
+
+ [CustomAssertion]
+ private string GetSubjectId() => AssertionScope.Current.CallerIdentity;
}
#pragma warning disable IDE0060, RCS1163 // Remove unused parameter
diff --git a/Tests/FluentAssertions.Specs/FluentAssertions.Specs.csproj b/Tests/FluentAssertions.Specs/FluentAssertions.Specs.csproj
index eee4d5e48c..d5c5a52b90 100644
--- a/Tests/FluentAssertions.Specs/FluentAssertions.Specs.csproj
+++ b/Tests/FluentAssertions.Specs/FluentAssertions.Specs.csproj
@@ -61,6 +61,7 @@
+
diff --git a/Tests/FluentAssertions.Specs/Primitives/BooleanAssertionSpecs.cs b/Tests/FluentAssertions.Specs/Primitives/BooleanAssertionSpecs.cs
index f956cb55af..63e8cf8cb5 100644
--- a/Tests/FluentAssertions.Specs/Primitives/BooleanAssertionSpecs.cs
+++ b/Tests/FluentAssertions.Specs/Primitives/BooleanAssertionSpecs.cs
@@ -41,7 +41,7 @@ public void Should_fail_with_descriptive_message_when_asserting_boolean_value_fa
// Assert
action
.Should().Throw()
- .WithMessage("Expected boolean to be true because we want to test the failure message, but found False.");
+ .WithMessage("Expected boolean to be True because we want to test the failure message, but found False.");
}
}
@@ -78,7 +78,7 @@ public void Should_fail_with_descriptive_message_when_asserting_boolean_value_tr
// Assert
action.Should().Throw()
- .WithMessage("Expected boolean to be false because we want to test the failure message, but found True.");
+ .WithMessage("Expected boolean to be False because we want to test the failure message, but found True.");
}
}
diff --git a/Tests/FluentAssertions.Specs/Primitives/DateTimeAssertionSpecs.cs b/Tests/FluentAssertions.Specs/Primitives/DateTimeAssertionSpecs.cs
index f8cb8105df..e45d43156c 100644
--- a/Tests/FluentAssertions.Specs/Primitives/DateTimeAssertionSpecs.cs
+++ b/Tests/FluentAssertions.Specs/Primitives/DateTimeAssertionSpecs.cs
@@ -2245,6 +2245,44 @@ public void Should_support_chaining_constraints_with_and()
// Assert
action.Should().NotThrow();
}
+
+ [Fact]
+ public void Should_throw_because_of_assertion_failure_when_asserting_null_is_within_second_before_specific_date()
+ {
+ // Arrange
+ DateTimeOffset? nullDateTime = null;
+ DateTimeOffset target = new DateTimeOffset(2000, 1, 1, 12, 0, 0, TimeSpan.Zero);
+
+ // Act
+ Action action = () =>
+ nullDateTime.Should()
+ .BeWithin(TimeSpan.FromSeconds(1))
+ .Before(target);
+
+ // Assert
+ action.Should().Throw()
+ .Which.Message
+ .Should().StartWith("Expected nullDateTime to be within 1s before <2000-01-01 12:00:00 +0h>, but found a DateTime");
+ }
+
+ [Fact]
+ public void Should_throw_because_of_assertion_failure_when_asserting_null_is_within_second_after_specific_date()
+ {
+ // Arrange
+ DateTimeOffset? nullDateTime = null;
+ DateTimeOffset target = new DateTimeOffset(2000, 1, 1, 12, 0, 0, TimeSpan.Zero);
+
+ // Act
+ Action action = () =>
+ nullDateTime.Should()
+ .BeWithin(TimeSpan.FromSeconds(1))
+ .After(target);
+
+ // Assert
+ action.Should().Throw()
+ .Which.Message
+ .Should().StartWith("Expected nullDateTime to be within 1s after <2000-01-01 12:00:00 +0h>, but found a DateTime");
+ }
}
public class BeOneOf
diff --git a/Tests/FluentAssertions.Specs/Primitives/ObjectAssertionSpecs.cs b/Tests/FluentAssertions.Specs/Primitives/ObjectAssertionSpecs.cs
index fe4874a31f..8d316590c0 100644
--- a/Tests/FluentAssertions.Specs/Primitives/ObjectAssertionSpecs.cs
+++ b/Tests/FluentAssertions.Specs/Primitives/ObjectAssertionSpecs.cs
@@ -10,6 +10,7 @@
using FluentAssertions.Execution;
using FluentAssertions.Extensions;
using FluentAssertions.Primitives;
+using JetBrains.Annotations;
using Xunit;
using Xunit.Sdk;
@@ -815,7 +816,10 @@ public void When_null_object_is_matched_negatively_against_a_type_it_should_thro
// Act
Action act = () =>
+ {
+ using var _ = new AssertionScope();
valueTypeObject.Should().NotBeOfType(typeof(int), "because we want to test the failure {0}", "message");
+ };
// Assert
act.Should().Throw()
@@ -1491,11 +1495,13 @@ public void When_an_object_is_xml_serializable_but_doesnt_restore_all_properties
internal class NonPublicClass
{
+ [UsedImplicitly]
public string Name { get; set; }
}
public class XmlSerializableClass
{
+ [UsedImplicitly]
public string Name { get; set; }
public int Id;
@@ -1503,6 +1509,7 @@ public class XmlSerializableClass
public class XmlSerializableClassNotRestoringAllProperties : IXmlSerializable
{
+ [UsedImplicitly]
public string Name { get; set; }
public DateTime BirthDay { get; set; }
@@ -1624,6 +1631,7 @@ public class NonDataContractSerializableClass
public class DataContractSerializableClass
{
+ [UsedImplicitly]
public string Name { get; set; }
public int Id;
diff --git a/Tests/FluentAssertions.Specs/Primitives/ReferenceTypeAssertionsSpecs.cs b/Tests/FluentAssertions.Specs/Primitives/ReferenceTypeAssertionsSpecs.cs
index 116284854e..6f87441bfb 100644
--- a/Tests/FluentAssertions.Specs/Primitives/ReferenceTypeAssertionsSpecs.cs
+++ b/Tests/FluentAssertions.Specs/Primitives/ReferenceTypeAssertionsSpecs.cs
@@ -56,14 +56,13 @@ Expected subject to refer to
[Fact]
public void When_a_derived_class_has_longer_formatting_than_the_base_class()
{
- var subject = new SimpleComplexBase[] { new Simple(), new Complex("goodbye") };
+ var subject = new SimpleComplexBase[] { new Complex("goodbye"), new Simple() };
Action act = () => subject.Should().BeEmpty();
act.Should().Throw()
.WithMessage(
"""
- Expected subject to be empty, but found
+ Expected subject to be empty, but found at least one item
{
- Simple(Hello),
FluentAssertions.Specs.Primitives.Complex
{
Statement = "goodbye"
diff --git a/Tests/FluentAssertions.Specs/Specialized/DelegateAssertionSpecs.cs b/Tests/FluentAssertions.Specs/Specialized/DelegateAssertionSpecs.cs
index 547c6ee46e..781d70b3a6 100644
--- a/Tests/FluentAssertions.Specs/Specialized/DelegateAssertionSpecs.cs
+++ b/Tests/FluentAssertions.Specs/Specialized/DelegateAssertionSpecs.cs
@@ -1,4 +1,5 @@
using System;
+using FluentAssertions.Execution;
using FluentAssertions.Specialized;
using Xunit;
@@ -34,4 +35,22 @@ public void When_injecting_a_null_clock_it_should_throw()
act.Should().ThrowExactly()
.WithParameterName("clock");
}
+
+ public class ThrowExactly
+ {
+ [Fact]
+ public void Does_not_continue_assertion_on_exact_exception_type()
+ {
+ // Arrange
+ var a = () => { };
+
+ // Act
+ using var scope = new AssertionScope();
+ a.Should().ThrowExactly();
+
+ // Assert
+ scope.Discard().Should().ContainSingle()
+ .Which.Should().Match("*InvalidOperationException*no exception*");
+ }
+ }
}
diff --git a/Tests/FluentAssertions.Specs/Specialized/TaskAssertionSpecs.cs b/Tests/FluentAssertions.Specs/Specialized/TaskAssertionSpecs.cs
index 2cfe99ee3b..dcab1926ff 100644
--- a/Tests/FluentAssertions.Specs/Specialized/TaskAssertionSpecs.cs
+++ b/Tests/FluentAssertions.Specs/Specialized/TaskAssertionSpecs.cs
@@ -464,6 +464,70 @@ await action.Should().ThrowAsync().WithMessage(
"Expected a to be thrown within 1s,"
+ " but found :*foo*");
}
+
+ [Fact]
+ public async Task When_task_is_canceled_before_timeout_it_succeeds()
+ {
+ // Arrange
+ var timer = new FakeClock();
+ var taskFactory = new TaskCompletionSource();
+
+ // Act
+ Func action = () =>
+ {
+ return Awaiting(() => (Task)taskFactory.Task)
+ .Should(timer).ThrowWithinAsync(1.Seconds());
+ };
+
+ _ = action.Invoke();
+
+ taskFactory.SetCanceled();
+ timer.Complete();
+
+ // Assert
+ await action.Should().NotThrowAsync();
+ }
+
+ [Fact]
+ public async Task When_task_is_canceled_after_timeout_it_fails()
+ {
+ // Arrange
+ var timer = new FakeClock();
+ var taskFactory = new TaskCompletionSource();
+
+ // Act
+ Func action = () =>
+ {
+ return Awaiting(() => (Task)taskFactory.Task)
+ .Should(timer).ThrowWithinAsync(1.Seconds());
+ };
+
+ _ = action.Invoke();
+
+ timer.Delay(1.Seconds());
+ taskFactory.SetCanceled();
+
+ // Assert
+ await action.Should().ThrowAsync();
+ }
+ }
+
+ public class ThrowExactlyAsync
+ {
+ [Fact]
+ public async Task Does_not_continue_assertion_on_exact_exception_type()
+ {
+ // Arrange
+ var a = () => Task.Delay(1);
+
+ // Act
+ using var scope = new AssertionScope();
+ await a.Should().ThrowExactlyAsync();
+
+ // Assert
+ scope.Discard().Should().ContainSingle()
+ .Which.Should().Match("*InvalidOperationException*no exception*");
+ }
}
[Collection("UIFacts")]
diff --git a/Tests/FluentAssertions.Specs/Types/PropertyInfoAssertionSpecs.cs b/Tests/FluentAssertions.Specs/Types/PropertyInfoAssertionSpecs.cs
index bffbe31bbc..167204cd69 100644
--- a/Tests/FluentAssertions.Specs/Types/PropertyInfoAssertionSpecs.cs
+++ b/Tests/FluentAssertions.Specs/Types/PropertyInfoAssertionSpecs.cs
@@ -2,6 +2,7 @@
using System.Linq.Expressions;
using System.Reflection;
using FluentAssertions.Common;
+using FluentAssertions.Execution;
using Xunit;
using Xunit.Sdk;
@@ -556,6 +557,24 @@ public void When_asserting_a_private_read_public_write_property_is_public_readab
"Expected method get_WritePrivateReadProperty to be Public because we want to test the error message, but it is Private.");
}
+ [Fact]
+ public void Do_not_the_check_access_modifier_when_the_property_is_not_readable()
+ {
+ // Arrange
+ PropertyInfo propertyInfo = typeof(ClassWithProperties).GetRuntimeProperty("WriteOnlyProperty");
+
+ // Act
+ Action action = () =>
+ {
+ using var _ = new AssertionScope();
+ propertyInfo.Should().BeReadable(CSharpAccessModifier.Private);
+ };
+
+ // Assert
+ action.Should().Throw()
+ .WithMessage("Expected property WriteOnlyProperty to have a getter, but it does not.");
+ }
+
[Fact]
public void When_subject_is_null_be_readable_with_accessmodifier_should_fail()
{
@@ -618,6 +637,24 @@ public void When_asserting_a_private_write_public_read_property_is_public_writab
"Expected method set_ReadPrivateWriteProperty to be Public because we want to test the error message, but it is Private.");
}
+ [Fact]
+ public void Do_not_the_check_access_modifier_when_the_property_is_not_writable()
+ {
+ // Arrange
+ PropertyInfo propertyInfo = typeof(ClassWithProperties).GetRuntimeProperty("ReadOnlyProperty");
+
+ // Act
+ Action action = () =>
+ {
+ using var _ = new AssertionScope();
+ propertyInfo.Should().BeWritable(CSharpAccessModifier.Private);
+ };
+
+ // Assert
+ action.Should().Throw()
+ .WithMessage("Expected propertyInfo ReadOnlyProperty to have a setter.");
+ }
+
[Fact]
public void When_subject_is_null_be_writable_with_accessmodifier_should_fail()
{
diff --git a/Tests/FluentAssertions.Specs/Types/TypeAssertionSpecs.HaveExplicitMethod.cs b/Tests/FluentAssertions.Specs/Types/TypeAssertionSpecs.HaveExplicitMethod.cs
index 7b3ba3e712..9af605c66c 100644
--- a/Tests/FluentAssertions.Specs/Types/TypeAssertionSpecs.HaveExplicitMethod.cs
+++ b/Tests/FluentAssertions.Specs/Types/TypeAssertionSpecs.HaveExplicitMethod.cs
@@ -1,4 +1,5 @@
using System;
+using FluentAssertions.Execution;
using Xunit;
using Xunit.Sdk;
@@ -183,6 +184,19 @@ public void When_asserting_a_type_has_an_explicit_method_with_an_empty_name_it_s
act.Should().ThrowExactly()
.WithParameterName("name");
}
+
+ [Fact]
+ public void Does_not_continue_assertion_on_explicit_interface_implementation_if_not_implemented_at_all()
+ {
+ var act = () =>
+ {
+ using var _ = new AssertionScope();
+ typeof(ClassWithMembers).Should().HaveExplicitMethod(typeof(IExplicitInterface), "Foo", new Type[0]);
+ };
+
+ act.Should().Throw()
+ .WithMessage("Expected type *ClassWithMembers* to*implement *IExplicitInterface, but it does not.");
+ }
}
public class HaveExplicitMethodOfT
@@ -440,6 +454,21 @@ public void When_asserting_a_type_does_not_have_an_explicit_method_with_an_empty
act.Should().ThrowExactly()
.WithParameterName("name");
}
+
+ [Fact]
+ public void Does_not_continue_assertion_on_explicit_interface_implementation_if_implemented()
+ {
+ var act = () =>
+ {
+ using var _ = new AssertionScope();
+ typeof(ClassExplicitlyImplementingInterface)
+ .Should().NotHaveExplicitMethod(typeof(IExplicitInterface), "ExplicitMethod", new Type[0]);
+ };
+
+ act.Should().Throw()
+ .WithMessage("Expected *ClassExplicitlyImplementingInterface* to not*implement " +
+ "*IExplicitInterface.ExplicitMethod(), but it does.");
+ }
}
public class NotHaveExplicitMethodOfT
diff --git a/Tests/FluentAssertions.Specs/Types/TypeAssertionSpecs.HaveExplicitProperty.cs b/Tests/FluentAssertions.Specs/Types/TypeAssertionSpecs.HaveExplicitProperty.cs
index 43e84e67a0..36ed2d36ce 100644
--- a/Tests/FluentAssertions.Specs/Types/TypeAssertionSpecs.HaveExplicitProperty.cs
+++ b/Tests/FluentAssertions.Specs/Types/TypeAssertionSpecs.HaveExplicitProperty.cs
@@ -1,4 +1,5 @@
using System;
+using FluentAssertions.Execution;
using Xunit;
using Xunit.Sdk;
@@ -168,6 +169,19 @@ public void When_asserting_a_type_has_an_explicit_property_with_an_empty_name_it
act.Should().ThrowExactly()
.WithParameterName("name");
}
+
+ [Fact]
+ public void Does_not_continue_assertion_on_explicit_interface_implementation_if_not_implemented_at_all()
+ {
+ var act = () =>
+ {
+ using var _ = new AssertionScope();
+ typeof(int).Should().HaveExplicitProperty(typeof(IExplicitInterface), "Foo");
+ };
+
+ act.Should().Throw()
+ .WithMessage("Expected type System.Int32 to*implement *IExplicitInterface, but it does not.");
+ }
}
public class HaveExplicitPropertyOfT
@@ -395,6 +409,21 @@ public void When_asserting_a_type_does_not_have_an_explicit_property_with_an_emp
act.Should().ThrowExactly()
.WithParameterName("name");
}
+
+ [Fact]
+ public void Does_not_continue_assertion_on_explicit_interface_implementation_if_implemented()
+ {
+ var act = () =>
+ {
+ using var _ = new AssertionScope();
+ typeof(ClassExplicitlyImplementingInterface)
+ .Should().NotHaveExplicitProperty(typeof(IExplicitInterface), "ExplicitStringProperty");
+ };
+
+ act.Should().Throw()
+ .WithMessage("Expected *ClassExplicitlyImplementingInterface* to*implement " +
+ "*IExplicitInterface.ExplicitStringProperty, but it does.");
+ }
}
public class NotHaveExplicitPropertyOfT
diff --git a/Tests/FluentAssertions.Specs/Types/TypeAssertionSpecs.Implement.cs b/Tests/FluentAssertions.Specs/Types/TypeAssertionSpecs.Implement.cs
index 14161430a7..a53e61e0ea 100644
--- a/Tests/FluentAssertions.Specs/Types/TypeAssertionSpecs.Implement.cs
+++ b/Tests/FluentAssertions.Specs/Types/TypeAssertionSpecs.Implement.cs
@@ -73,6 +73,20 @@ public void When_asserting_a_type_to_implement_null_it_should_throw()
act.Should().ThrowExactly()
.WithParameterName("interfaceType");
}
+
+ [Fact]
+ public void An_interface_does_not_implement_itself()
+ {
+ // Arrange
+ var type = typeof(IDummyInterface);
+
+ // Act
+ Action act = () =>
+ type.Should().Implement(typeof(IDummyInterface));
+
+ // Assert
+ act.Should().Throw();
+ }
}
public class ImplementOfT
@@ -156,6 +170,16 @@ public void When_asserting_a_type_not_to_implement_null_it_should_throw()
act.Should().ThrowExactly()
.WithParameterName("interfaceType");
}
+
+ [Fact]
+ public void An_interface_does_not_implement_itself()
+ {
+ // Arrange
+ var type = typeof(IDummyInterface);
+
+ // Act / Assert
+ type.Should().NotImplement(typeof(IDummyInterface));
+ }
}
public class NotImplementOfT
diff --git a/cSpell.json b/cSpell.json
index e3d0562a0f..4f9714acdc 100644
--- a/cSpell.json
+++ b/cSpell.json
@@ -5,6 +5,7 @@
"browsable",
"comparands",
"Doomen",
+ "Faqt",
"formattable",
"Guids",
"LINQ",
diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock
deleted file mode 100644
index 47bdddfd63..0000000000
--- a/docs/Gemfile.lock
+++ /dev/null
@@ -1,265 +0,0 @@
-GEM
- remote: https://rubygems.org/
- specs:
- activesupport (7.0.7)
- concurrent-ruby (~> 1.0, >= 1.0.2)
- i18n (>= 1.6, < 2)
- minitest (>= 5.1)
- tzinfo (~> 2.0)
- addressable (2.8.5)
- public_suffix (>= 2.0.2, < 6.0)
- coffee-script (2.4.1)
- coffee-script-source
- execjs
- coffee-script-source (1.11.1)
- colorator (1.1.0)
- commonmarker (0.23.10)
- concurrent-ruby (1.2.2)
- dnsruby (1.70.0)
- simpleidn (~> 0.2.1)
- em-websocket (0.5.3)
- eventmachine (>= 0.12.9)
- http_parser.rb (~> 0)
- ethon (0.16.0)
- ffi (>= 1.15.0)
- eventmachine (1.2.7)
- eventmachine (1.2.7-x64-mingw32)
- execjs (2.8.1)
- faraday (2.7.10)
- faraday-net_http (>= 2.0, < 3.1)
- ruby2_keywords (>= 0.0.4)
- faraday-net_http (3.0.2)
- ffi (1.15.5-x64-mingw-ucrt)
- ffi (1.15.5-x64-mingw32)
- forwardable-extended (2.6.0)
- gemoji (3.0.1)
- github-pages (228)
- github-pages-health-check (= 1.17.9)
- jekyll (= 3.9.3)
- jekyll-avatar (= 0.7.0)
- jekyll-coffeescript (= 1.1.1)
- jekyll-commonmark-ghpages (= 0.4.0)
- jekyll-default-layout (= 0.1.4)
- jekyll-feed (= 0.15.1)
- jekyll-gist (= 1.5.0)
- jekyll-github-metadata (= 2.13.0)
- jekyll-include-cache (= 0.2.1)
- jekyll-mentions (= 1.6.0)
- jekyll-optional-front-matter (= 0.3.2)
- jekyll-paginate (= 1.1.0)
- jekyll-readme-index (= 0.3.0)
- jekyll-redirect-from (= 0.16.0)
- jekyll-relative-links (= 0.6.1)
- jekyll-remote-theme (= 0.4.3)
- jekyll-sass-converter (= 1.5.2)
- jekyll-seo-tag (= 2.8.0)
- jekyll-sitemap (= 1.4.0)
- jekyll-swiss (= 1.0.0)
- jekyll-theme-architect (= 0.2.0)
- jekyll-theme-cayman (= 0.2.0)
- jekyll-theme-dinky (= 0.2.0)
- jekyll-theme-hacker (= 0.2.0)
- jekyll-theme-leap-day (= 0.2.0)
- jekyll-theme-merlot (= 0.2.0)
- jekyll-theme-midnight (= 0.2.0)
- jekyll-theme-minimal (= 0.2.0)
- jekyll-theme-modernist (= 0.2.0)
- jekyll-theme-primer (= 0.6.0)
- jekyll-theme-slate (= 0.2.0)
- jekyll-theme-tactile (= 0.2.0)
- jekyll-theme-time-machine (= 0.2.0)
- jekyll-titles-from-headings (= 0.5.3)
- jemoji (= 0.12.0)
- kramdown (= 2.3.2)
- kramdown-parser-gfm (= 1.1.0)
- liquid (= 4.0.4)
- mercenary (~> 0.3)
- minima (= 2.5.1)
- nokogiri (>= 1.13.6, < 2.0)
- rouge (= 3.26.0)
- terminal-table (~> 1.4)
- github-pages-health-check (1.17.9)
- addressable (~> 2.3)
- dnsruby (~> 1.60)
- octokit (~> 4.0)
- public_suffix (>= 3.0, < 5.0)
- typhoeus (~> 1.3)
- html-pipeline (2.14.3)
- activesupport (>= 2)
- nokogiri (>= 1.4)
- http_parser.rb (0.8.0)
- i18n (1.14.1)
- concurrent-ruby (~> 1.0)
- jekyll (3.9.3)
- addressable (~> 2.4)
- colorator (~> 1.0)
- em-websocket (~> 0.5)
- i18n (>= 0.7, < 2)
- jekyll-sass-converter (~> 1.0)
- jekyll-watch (~> 2.0)
- kramdown (>= 1.17, < 3)
- liquid (~> 4.0)
- mercenary (~> 0.3.3)
- pathutil (~> 0.9)
- rouge (>= 1.7, < 4)
- safe_yaml (~> 1.0)
- jekyll-avatar (0.7.0)
- jekyll (>= 3.0, < 5.0)
- jekyll-coffeescript (1.1.1)
- coffee-script (~> 2.2)
- coffee-script-source (~> 1.11.1)
- jekyll-commonmark (1.4.0)
- commonmarker (~> 0.22)
- jekyll-commonmark-ghpages (0.4.0)
- commonmarker (~> 0.23.7)
- jekyll (~> 3.9.0)
- jekyll-commonmark (~> 1.4.0)
- rouge (>= 2.0, < 5.0)
- jekyll-default-layout (0.1.4)
- jekyll (~> 3.0)
- jekyll-feed (0.15.1)
- jekyll (>= 3.7, < 5.0)
- jekyll-gist (1.5.0)
- octokit (~> 4.2)
- jekyll-github-metadata (2.13.0)
- jekyll (>= 3.4, < 5.0)
- octokit (~> 4.0, != 4.4.0)
- jekyll-include-cache (0.2.1)
- jekyll (>= 3.7, < 5.0)
- jekyll-mentions (1.6.0)
- html-pipeline (~> 2.3)
- jekyll (>= 3.7, < 5.0)
- jekyll-optional-front-matter (0.3.2)
- jekyll (>= 3.0, < 5.0)
- jekyll-paginate (1.1.0)
- jekyll-readme-index (0.3.0)
- jekyll (>= 3.0, < 5.0)
- jekyll-redirect-from (0.16.0)
- jekyll (>= 3.3, < 5.0)
- jekyll-relative-links (0.6.1)
- jekyll (>= 3.3, < 5.0)
- jekyll-remote-theme (0.4.3)
- addressable (~> 2.0)
- jekyll (>= 3.5, < 5.0)
- jekyll-sass-converter (>= 1.0, <= 3.0.0, != 2.0.0)
- rubyzip (>= 1.3.0, < 3.0)
- jekyll-sass-converter (1.5.2)
- sass (~> 3.4)
- jekyll-seo-tag (2.8.0)
- jekyll (>= 3.8, < 5.0)
- jekyll-sitemap (1.4.0)
- jekyll (>= 3.7, < 5.0)
- jekyll-swiss (1.0.0)
- jekyll-theme-architect (0.2.0)
- jekyll (> 3.5, < 5.0)
- jekyll-seo-tag (~> 2.0)
- jekyll-theme-cayman (0.2.0)
- jekyll (> 3.5, < 5.0)
- jekyll-seo-tag (~> 2.0)
- jekyll-theme-dinky (0.2.0)
- jekyll (> 3.5, < 5.0)
- jekyll-seo-tag (~> 2.0)
- jekyll-theme-hacker (0.2.0)
- jekyll (> 3.5, < 5.0)
- jekyll-seo-tag (~> 2.0)
- jekyll-theme-leap-day (0.2.0)
- jekyll (> 3.5, < 5.0)
- jekyll-seo-tag (~> 2.0)
- jekyll-theme-merlot (0.2.0)
- jekyll (> 3.5, < 5.0)
- jekyll-seo-tag (~> 2.0)
- jekyll-theme-midnight (0.2.0)
- jekyll (> 3.5, < 5.0)
- jekyll-seo-tag (~> 2.0)
- jekyll-theme-minimal (0.2.0)
- jekyll (> 3.5, < 5.0)
- jekyll-seo-tag (~> 2.0)
- jekyll-theme-modernist (0.2.0)
- jekyll (> 3.5, < 5.0)
- jekyll-seo-tag (~> 2.0)
- jekyll-theme-primer (0.6.0)
- jekyll (> 3.5, < 5.0)
- jekyll-github-metadata (~> 2.9)
- jekyll-seo-tag (~> 2.0)
- jekyll-theme-slate (0.2.0)
- jekyll (> 3.5, < 5.0)
- jekyll-seo-tag (~> 2.0)
- jekyll-theme-tactile (0.2.0)
- jekyll (> 3.5, < 5.0)
- jekyll-seo-tag (~> 2.0)
- jekyll-theme-time-machine (0.2.0)
- jekyll (> 3.5, < 5.0)
- jekyll-seo-tag (~> 2.0)
- jekyll-titles-from-headings (0.5.3)
- jekyll (>= 3.3, < 5.0)
- jekyll-watch (2.2.1)
- listen (~> 3.0)
- jemoji (0.12.0)
- gemoji (~> 3.0)
- html-pipeline (~> 2.2)
- jekyll (>= 3.0, < 5.0)
- kramdown (2.3.2)
- rexml
- kramdown-parser-gfm (1.1.0)
- kramdown (~> 2.0)
- liquid (4.0.4)
- listen (3.8.0)
- rb-fsevent (~> 0.10, >= 0.10.3)
- rb-inotify (~> 0.9, >= 0.9.10)
- mercenary (0.3.6)
- mini_portile2 (2.8.4)
- minima (2.5.1)
- jekyll (>= 3.5, < 5.0)
- jekyll-feed (~> 0.9)
- jekyll-seo-tag (~> 2.1)
- minitest (5.19.0)
- nokogiri (1.15.4)
- mini_portile2 (~> 2.8.2)
- racc (~> 1.4)
- octokit (4.25.1)
- faraday (>= 1, < 3)
- sawyer (~> 0.9)
- pathutil (0.16.2)
- forwardable-extended (~> 2.6)
- public_suffix (4.0.7)
- racc (1.7.1)
- rb-fsevent (0.11.2)
- rb-inotify (0.10.1)
- ffi (~> 1.0)
- rexml (3.2.6)
- rouge (3.26.0)
- ruby2_keywords (0.0.5)
- rubyzip (2.3.2)
- safe_yaml (1.0.5)
- sass (3.7.4)
- sass-listen (~> 4.0.0)
- sass-listen (4.0.0)
- rb-fsevent (~> 0.9, >= 0.9.4)
- rb-inotify (~> 0.9, >= 0.9.7)
- sawyer (0.9.2)
- addressable (>= 2.3.5)
- faraday (>= 0.17.3, < 3)
- simpleidn (0.2.1)
- unf (~> 0.1.4)
- terminal-table (1.8.0)
- unicode-display_width (~> 1.1, >= 1.1.1)
- typhoeus (1.4.0)
- ethon (>= 0.9.0)
- tzinfo (2.0.6)
- concurrent-ruby (~> 1.0)
- unf (0.1.4)
- unf_ext
- unf_ext (0.0.8.2)
- unicode-display_width (1.8.0)
- webrick (1.8.1)
-
-PLATFORMS
- x64-mingw-ucrt
- x64-mingw32
-
-DEPENDENCIES
- github-pages
- webrick (~> 1.8.1)
-
-BUNDLED WITH
- 2.3.7
diff --git a/docs/_data/navigation.yml b/docs/_data/navigation.yml
index d8866d3bde..74e4b85ee7 100644
--- a/docs/_data/navigation.yml
+++ b/docs/_data/navigation.yml
@@ -73,6 +73,8 @@ sidebar:
url: /executiontime
- title: HTTP response messages
url: /httpresponsemessages
+ - title: F# usage
+ url: /fsharp
- title: Extensibility
url: /extensibility
diff --git a/docs/_data/tips/datetimes.yml b/docs/_data/tips/datetimes.yml
new file mode 100644
index 0000000000..3d92d73ac6
--- /dev/null
+++ b/docs/_data/tips/datetimes.yml
@@ -0,0 +1,167 @@
+- old: |
+ actual.Date.Should().Be(expected.Date);
+
+ new: |
+ actual.Should().BeSameDateAs(expected);
+
+ old-message: |
+ Expected date and time to be <2017-01-01>, but found <2017-01-02>.
+
+ new-message: |
+ Expected a date and time with date <2017-01-01>, but found <2017-01-02 21:00:00>.
+
+- old: |
+ actual.Date.Should().NotBe(unexpected.Date);
+
+ new: |
+ actual.Should().NotBeSameDateAs(unexpected);
+
+ old-message: |
+ Expected date and time not to be <2017-01-01>, but it is.
+
+ new-message: |
+ Expected a date and time that does not have date <2017-01-01>, but found it does.
+
+- old: |
+ actual.Year.Should().Be(expected.Year);
+
+ new: |
+ actual.Should().HaveYear(expected.Year);
+
+ old-message: |
+ Expected actual.Year to be 2018, but found 2017 (difference of -1).
+
+ new-message: |
+ Expected the year part of actual to be 2018, but found 2017.
+
+- old: |
+ actual.Year.Should().NotBe(unexpected.Year);
+
+ new: |
+ actual.Should().NotHaveYear(unexpected.Year);
+
+ old-message: |
+ Did not expect the year part of actual to be 2017, but it was.
+
+ new-message: |
+ Did not expect actual.Year to be 2017.
+
+- old: |
+ actual.Month.Should().Be(expected.Month);
+
+ new: |
+ actual.Should().HaveMonth(expected.Month);
+
+ old-message: |
+ Expected actual.Month to be 2, but found 1.
+
+ new-message: |
+ Expected the month part of actual to be 2, but found 1.
+
+- old: |
+ actual.Month.Should().NotBe(unexpected.Month);
+
+ new: |
+ actual.Should().NotHaveMonth(unexpected.Month);
+
+ old-message: |
+ Did not expect actual.Month to be 1.
+
+ new-message: |
+ Did not expect the month part of actual to be 1, but it was.
+
+- old: |
+ actual.Day.Should().Be(expected.Day);
+
+ new: |
+ actual.Should().HaveDay(expected.Day);
+
+ old-message: |
+ Expected actual.Day to be 2, but found 1.
+
+ new-message: |
+ Expected the day part of actual to be 2, but found 1.
+
+- old: |
+ actual.Day.Should().NotBe(unexpected.Day);
+
+ new: |
+ actual.Should().NotHaveDay(unexpected.Day);
+
+ old-message: |
+ Did not expect actual.Day to be 1.
+
+ new-message: |
+ Did not expect the day part of actual to be 1, but it was.
+
+- old: |
+ actual.Hour.Should().Be(expected.Hour);
+
+ new: |
+ actual.Should().HaveHour(expected.Hour);
+
+ old-message: |
+ Expected actual.Hour to be 19, but found 16 (difference of -3).
+
+ new-message: |
+ Expected the hour part of actual to be 19, but found 16.
+
+- old: |
+ actual.Hour.Should().NotBe(unexpected.Hour);
+
+ new: |
+ actual.Should().NotHaveHour(unexpected.Hour);
+
+ old-message: |
+ Did not expect actual.Hour to be 16.
+
+ new-message: |
+ Did not expect the hour part of actual to be 16, but it was.
+
+- old: |
+ actual.Minute.Should().Be(expected.Minute);
+
+ new: |
+ actual.Should().HaveMinute(expected.Minute);
+
+ old-message: |
+ Expected actual.Minute to be 31, but found 30 (difference of -1).
+
+ new-message: |
+ Expected the minute part of actual to be 31, but found 30.
+
+- old: |
+ actual.Minute.Should().NotBe(unexpected.Minute);
+
+ new: |
+ actual.Should().NotHaveMinute(unexpected.Minute);
+
+ old-message: |
+ Did not expect actual.Minute to be 30.
+
+ new-message: |
+ Did not expect the minute part of actual to be 30, but it was.
+
+- old: |
+ actual.Second.Should().Be(expected.Second);
+
+ new: |
+ actual.Should().HaveSecond(expected.Second);
+
+ old-message: |
+ Expected actual.Second to be 18, but found 17 (difference of -1).
+
+ new-message: |
+ Expected the seconds part of actual to be 18, but found 17.
+
+- old: |
+ actual.Second.Should().NotBe(unexpected.Second);
+
+ new: |
+ actual.Should().NotHaveSecond(unexpected.Second);
+
+ old-message: |
+ Did not expect actual.Second to be 17.
+
+ new-message: |
+ Did not expect the seconds part of actual to be 17, but it was.
diff --git a/docs/_includes/head/custom.html b/docs/_includes/head/custom.html
index b78271828d..b531e730af 100644
--- a/docs/_includes/head/custom.html
+++ b/docs/_includes/head/custom.html
@@ -1,11 +1,13 @@
-
+
+
diff --git a/docs/_layouts/single.html b/docs/_layouts/single.html
index 2da9294be6..bb865f84b0 100644
--- a/docs/_layouts/single.html
+++ b/docs/_layouts/single.html
@@ -51,17 +51,6 @@
{% if page.link %}{% endif %}
-
-
-
-
diff --git a/docs/_pages/collections.md b/docs/_pages/collections.md
index a347e0a41f..a0a4d9cc36 100644
--- a/docs/_pages/collections.md
+++ b/docs/_pages/collections.md
@@ -162,9 +162,9 @@ Consider for instance two collections that contain some kind of domain entity pe
Since the actual object instance is different, if you want to make sure a particular property was properly persisted, you usually do something like this:
```csharp
-persistedCustomers.Select(c => c.Name).Should().Equal(customers.Select(c => c.Name);
-persistedCustomers.Select(c => c.Name).Should().StartWith(customers.Select(c => c.Name);
-persistedCustomers.Select(c => c.Name).Should().EndWith(customers.Select(c => c.Name);
+persistedCustomers.Select(c => c.Name).Should().Equal(customers.Select(c => c.Name));
+persistedCustomers.Select(c => c.Name).Should().StartWith(customers.Select(c => c.Name));
+persistedCustomers.Select(c => c.Name).Should().EndWith(customers.Select(c => c.Name));
```
With these new overloads, you can rewrite them into:
diff --git a/docs/_pages/extensibility.md b/docs/_pages/extensibility.md
index ba8c06e569..50545217a9 100644
--- a/docs/_pages/extensibility.md
+++ b/docs/_pages/extensibility.md
@@ -36,6 +36,7 @@ public class DirectoryInfoAssertions :
protected override string Identifier => "directory";
+ [CustomAssertion]
public AndConstraint ContainFile(
string filename, string because = "", params object[] becauseArgs)
{
@@ -57,6 +58,7 @@ public class DirectoryInfoAssertions :
This is quite an elaborate example which shows some of the more advanced extensibility features. Let me highlight some things:
* The `Subject` property is used to give the base-class extensions access to the current `DirectoryInfo` object.
+* `[CustomAssertion]` attribute enables correct subject identification, allowing Fluent Assertions to render more meaningful test fail messages
* `Execute.Assertion` is the point of entrance into the internal fluent assertion API.
* The optional `because` parameter can contain `string.Format` style place holders which will be filled using the values provided to the `becauseArgs`. They can be used by the caller to provide a reason why the assertion should succeed. By passing those into the `BecauseOf` method, you can refer to the expanded result using the `{reason}` tag in the `FailWith` method.
* The `Then` property is just there to chain multiple assertions together. You can have more than one.
diff --git a/docs/_pages/fsharp.md b/docs/_pages/fsharp.md
new file mode 100644
index 0000000000..733f798ffc
--- /dev/null
+++ b/docs/_pages/fsharp.md
@@ -0,0 +1,47 @@
+---
+title: F# Usage
+permalink: /fsharp/
+layout: single
+classes: wide
+sidebar:
+ nav: "sidebar"
+---
+
+### F# usage
+
+FluentAssertions was made with C# in mind. While it can be used from F#, the experience will not always be as intended.
+
+For a better F# experience we encourage F# developers to try out [Faqt](https://github.com/cmeeren/Faqt/), an assertion library inspired by FluentAssertions and made specifically for F#.
+
+Some of the awkwardness you might hit with FluentAssertions when using F# is:
+
+* To aid F#'s overload resolution, you often have to cast subject values. This does not work in all cases and will only give you a subset of assertions. For example:
+
+ ```fsharp
+ let x = [1; 2]
+ x.Should().Contain(1, "") // Overload resolution error for "Should"
+ (x :> seq<_>).Should().Contain(1, "") // OK, but only gives access to GenericCollectionAssertions<_>
+ ```
+
+ The need to cast will break your fluent chains, defeating some of the purpose of FluentAssertions.
+
+* Sometimes not even casting will solve the issue, and you will have to forgo `Should` entirely and instead directly construct the correct assertion type. For example:
+
+ ```fsharp
+ let x = dict ["A", 1; "B", 2]
+
+ // All of these give overload resolution errors for "Should"
+ x.Should().ContainKey(1, "")
+ (x :> IDictionary<_,_>).Should().ContainKey(1, "")
+ (x :> seq>).Should().Contain(KeyValuePair("A", 1), "")
+
+ // You have to construct the desired assertion directly
+ GenericDictionaryAssertions<_, _, _>(x).ContainKey("A", "")
+ GenericCollectionAssertions<_, _, _>(x).Contain(KeyValuePair("A", 1), "")
+ ```
+
+ As with casting, the need to construct the assertion directly will break your fluent chains, defeating one of the key points of FluentAssertions.
+
+* As can be seen above, the `because` parameter cannot be omitted when used from
+ F# ([#2225](https://github.com/fluentassertions/fluentassertions/issues/2225)). Adding overloads with a fixed `because` is [deemed out of scope](https://github.com/fluentassertions/fluentassertions/issues/2225#issuecomment-1636733116) for the main FluentAssertions package since it would double the API surface.
+* Several assertions (specifically, those that accept an `Action<_>`) require an explicit `ignore` when used from F# ([#2226](https://github.com/fluentassertions/fluentassertions/issues/2226)).
diff --git a/docs/_pages/releases.md b/docs/_pages/releases.md
index 5a4162bc80..bab190b1d1 100644
--- a/docs/_pages/releases.md
+++ b/docs/_pages/releases.md
@@ -7,6 +7,32 @@ sidebar:
nav: "sidebar"
---
+## 6.12.1
+
+### Improvements
+* Improve `BeEmpty()` and `BeNullOrEmpty()` performance for `IEnumerable`, by materializing only the first item - [#2530](https://github.com/fluentassertions/fluentassertions/pull/2530)
+
+### Fixes
+* Fixed formatting error when checking nullable `DateTimeOffset` with
+`BeWithin(...).Before(...)` - [#2312](https://github.com/fluentassertions/fluentassertions/pull/2312)
+* `BeEquivalentTo` will now find and can map subject properties that are implemented through an explicitly-implemented interface - [#2152](https://github.com/fluentassertions/fluentassertions/pull/2152)
+* Fixed that the `because` and `becauseArgs` were not passed down the equivalency tree - [#2318](https://github.com/fluentassertions/fluentassertions/pull/2318)
+* `BeEquivalentTo` can again compare a non-generic `IDictionary` with a generic one - [#2358](https://github.com/fluentassertions/fluentassertions/pull/2358)
+* Fixed that the `FormattingOptions` were not respected in inner `AssertionScope` - [#2329](https://github.com/fluentassertions/fluentassertions/pull/2329)
+* Capitalize `true` and `false` in failure messages and make them formattable to a custom `BooleanFormatter` - [#2390](https://github.com/fluentassertions/fluentassertions/pull/2390), [#2393](https://github.com/fluentassertions/fluentassertions/pull/2393)
+* Improved the failure message for `NotBeOfType` when wrapped in an `AssertionScope` and the subject is null - [#2399](https://github.com/fluentassertions/fluentassertions/pull/2399)
+* Improved the failure message for `BeWritable`/`BeReadable` when wrapped in an `AssertionScope` and the subject is read-only/write-only - [#2399](https://github.com/fluentassertions/fluentassertions/pull/2399)
+* Improved the failure message for `ThrowExactly[Async]` when wrapped in an `AssertionScope` and no exception is thrown - [#2398](https://github.com/fluentassertions/fluentassertions/pull/2398)
+* Improved the failure message for `[Not]HaveExplicitProperty` when wrapped in an `AssertionScope` and not implementing the interface - [#2403](https://github.com/fluentassertions/fluentassertions/pull/2403)
+* Improved the failure message for `[Not]HaveExplicitMethod` when wrapped in an `AssertionScope` and not implementing the interface - [#2403](https://github.com/fluentassertions/fluentassertions/pull/2403)
+* Changed `BeEquivalentTo` to exclude `private protected` members from the comparison - [#2417](https://github.com/fluentassertions/fluentassertions/pull/2417)
+* Fixed using `BeEquivalentTo` on an empty `ArraySegment` - [#2445](https://github.com/fluentassertions/fluentassertions/pull/2445), [#2511](https://github.com/fluentassertions/fluentassertions/pull/2511)
+* `BeEquivalentTo` with a custom comparer can now handle null values - [#2489](https://github.com/fluentassertions/fluentassertions/pull/2489)
+* Ensured that nested calls to `AssertionScope(context)` create a chained context - [#2607](https://github.com/fluentassertions/fluentassertions/pull/2607)
+* One overload of the `AssertionScope` constructor would not create an actual scope associated with the thread - [#2607](https://github.com/fluentassertions/fluentassertions/pull/2607)
+* Fixed `ThrowWithinAsync` not respecting `OperationCanceledException` - [#2614](https://github.com/fluentassertions/fluentassertions/pull/2614)
+* Fixed using `BeEquivalentTo` with an `IEqualityComparer` targeting nullable types - [#2648](https://github.com/fluentassertions/fluentassertions/pull/2648)
+
## 6.12.0
### What's new
@@ -19,6 +45,7 @@ sidebar:
* Improve caller identification for tests written in Visual Basic - [#2254](https://github.com/fluentassertions/fluentassertions/pull/2254)
* Improved auto conversion to enums for objects of different integral type - [#2261](https://github.com/fluentassertions/fluentassertions/pull/2261)
* Fixed exceptions when trying to auto convert strings or enums of different type to enums- [#2261](https://github.com/fluentassertions/fluentassertions/pull/2261)
+* Format records and anonymous objects with their member values instead of the generated `ToString` - [#2144](https://github.com/fluentassertions/fluentassertions/pull/2144)
## 6.11.0
@@ -33,7 +60,6 @@ sidebar:
* The maximum depth `BeEquivalentTo` uses for recursive comparisons was 9 instead of the expected 10 - [#2145](https://github.com/fluentassertions/fluentassertions/pull/2145)
* Fixed `.Excluding()` and `.For().Exclude()` not working if root is a collection - [#2135](https://github.com/fluentassertions/fluentassertions/pull/2135)
* Prevent `InvalidOperationException` when formatting a lambda expression calling a constructor - [#2176](https://github.com/fluentassertions/fluentassertions/pull/2176)
-* Format records and anonymous objects with their member values instead of the generated `ToString` - [#2144](https://github.com/fluentassertions/fluentassertions/pull/2144)
## 6.10.0
diff --git a/docs/assets/images/aws.png b/docs/assets/images/aws.png
deleted file mode 100644
index 2c11879288..0000000000
Binary files a/docs/assets/images/aws.png and /dev/null differ
diff --git a/docs/assets/images/swyfft.jpg b/docs/assets/images/swyfft.jpg
new file mode 100644
index 0000000000..090caeecac
Binary files /dev/null and b/docs/assets/images/swyfft.jpg differ
diff --git a/docs/index.html b/docs/index.html
index 0c58ef6f8d..a5556313c8 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -6,8 +6,8 @@
overlay_color: "#373737"
overlay_filter: "0.7"
overlay_image: "/assets/images/fluent_assertions_large_horizontal.svg"
- cta_label: "Fluent Assertions 6.11 is out!"
- cta_url: "https://fluentassertions.com/releases/#6110"
+ cta_label: "Fluent Assertions 6.12 is out now. What's next? 7.0!"
+ cta_url: "https://fluentassertions.com/releases/#6120"
caption: "Logo by [**IUserName**](https://github.com/IUsername) and icons by [**Zlatko Najdenovski**](https://www.flaticon.com/authors/zlatko-najdenovski) from [Flaticon](https://www.flaticon.com/)
"
excerpt: '_"There''s a life before Fluent Assertions, and there''s a life after it"_ - [Meisam Alifallahi](https://www.linkedin.com/in/meisam-alifallahi/)
@@ -21,7 +21,7 @@
intro:
- excerpt: '
[ @ddoomen](https://twitter.com/ddoomen){: .btn .btn--twitter}
- [ @ddoomen](https://mastodon.social/@ddoomen){: .btn .btn--mastodon}
+ [ @ddoomen](https://mastodon.social/@ddoomen){: .btn .btn--mastodon}
[ @jnyrup](https://twitter.com/jnyrup){: .btn .btn--twitter}
[ GitHub Sponsors](https://github.com/sponsors/fluentassertions){: .btn .btn--github}
[ Patreon](https://www.patreon.com/bePatron?u=9250052&redirect_uri=http%3A%2F%2Ffluentassertions.com%2F&utm_medium=widget){: .btn .btn--patreon}
@@ -59,7 +59,7 @@
- title: "Great Support"
image_path: /assets/images/customer-service.svg
excerpt: '
- Use [ StackOverflow](https://stackoverflow.com/questions/tagged/fluent-assertions?mixed=1) for general questions,
+ Use [ StackOverflow](https://stackoverflow.com/questions/tagged/fluent-assertions?mixed=1) for general questions,
go on [ Slack](https://fluentassertionsslack.herokuapp.com/) to contact the team directly, or
@@ -69,28 +69,29 @@
- title: "Passionate Team"
image_path: /assets/images/vote.svg
excerpt: '
- Originally authored by
- [Dennis Doomen](https://github.com/dennisdoomen), but
- [Jonas Nyrup](https://github.com/jnyrup) has joined since then. Notable contributions by
+ Originally authored by
+ [Dennis Doomen](https://github.com/dennisdoomen), but
+ [Jonas Nyrup](https://github.com/jnyrup) has joined since then. Notable contributions by
[Michaël Hompus](https://github.com/eNeRGy164) and [Gunnar Liljas](https://github.com/gliljas).
'
feature_row3:
- title: "Powered By"
image_path: /assets/images/gift.svg
excerpt: '
- This project is [supported](https://github.com/sponsors/fluentassertions) by [Albert Weinert](https://github.com/DerAlbertCom) and [Way We Do](https://github.com/waywedo) with special thanks to our super-sponsors
- ,
- and
+ This project is [supported](https://github.com/sponsors/fluentassertions) by [Hassan Rezk Habib](https://github.com/hassanhabib), [Best Kru](https://bestkru.com/) and [others](https://github.com/fluentassertions/fluentassertions/#sponsors), with special thanks to our Most Awesome sponsors
+ ,
+ ,
+ and
'
- title: "Community Extensions"
image_path: /assets/images/community.svg
- excerpt: 'Broad community support with extensions for
- [Json .NET](https://github.com/fluentassertions/fluentassertions.json),
- [Ninject](https://github.com/kevinkuszyk/FluentAssertions.Ioc.Ninject),
- [Autofac](https://github.com/awesome-inc/FluentAssertions.Autofac),
+ excerpt: 'Broad community support with extensions for
+ [Json .NET](https://github.com/fluentassertions/fluentassertions.json),
+ [Ninject](https://github.com/kevinkuszyk/FluentAssertions.Ioc.Ninject),
+ [Autofac](https://github.com/awesome-inc/FluentAssertions.Autofac),
[ASP.NET MVC](https://github.com/CaseyBurns/FluentAssertions.MVC),
- [ASP.NET Core MVC](https://github.com/fluentassertions/fluentassertions.aspnetcore.mvc) and even Roslyn analyzers through
+ [ASP.NET Core MVC](https://github.com/fluentassertions/fluentassertions.aspnetcore.mvc) and even Roslyn analyzers through
[FluentAssertions.Analyzers](https://github.com/fluentassertions/fluentassertions.analyzers)
'
- title: "What People Say"
diff --git a/profile.yaml b/profile.yaml
new file mode 100644
index 0000000000..1e0a0d7d1d
--- /dev/null
+++ b/profile.yaml
@@ -0,0 +1,12 @@
+name: "Custom profile"
+
+baseProfile: qodana.starter
+
+groups: # List of configured groups
+ - groupId: InspectionsToExclude
+ groups:
+ - "category:C#/Roslyn Analyzers"
+
+inspections: # Group invocation
+ - group: InspectionsToExclude
+ enabled: false # Disable the InspectionsToExclude group
diff --git a/qodana.yaml b/qodana.yaml
index 5bde305e79..03538ee20a 100644
--- a/qodana.yaml
+++ b/qodana.yaml
@@ -3,12 +3,20 @@ version: "1.0"
linter: jetbrains/qodana-dotnet:latest
failThreshold: 0
+profile:
+ path: profile.yaml
+
dotnet:
solution: FluentAssertions.sln
exclude:
- name: ConvertIfStatementToReturnStatement
- name: ConvertIfStatementToConditionalTernaryExpression
+ - name: InvertIf
+ - name: SimilarAnonymousTypeNearby
+ - name: UnusedMember.Local
+ paths:
+ - Tests
- name: All
paths:
- Build
@@ -16,4 +24,11 @@ exclude:
- Tests/AssemblyB
- Tests/Benchmarks
- Tests/UWP.Specs
-
\ No newline at end of file
+ - Src/FluentAssertions/Polyfill
+ - name: UnusedMember.Global
+ - name: ArrangeTrailingCommaInMultilineLists
+ - name: ArrangeTrailingCommaInSinglelineLists
+ - name: ConvertToLambdaExpression
+ - name: SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault
+ - name: UnusedMemberInSuper.Global
+
\ No newline at end of file