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 %} - - - -