Skip to content

Commit

Permalink
SAVEPOINT
Browse files Browse the repository at this point in the history
  • Loading branch information
dennisdoomen committed Mar 3, 2024
1 parent 2f85480 commit eda0f53
Show file tree
Hide file tree
Showing 136 changed files with 2,057 additions and 1,837 deletions.
198 changes: 125 additions & 73 deletions Src/FluentAssertions/AssertionExtensions.cs

Large diffs are not rendered by default.

347 changes: 149 additions & 198 deletions Src/FluentAssertions/Collections/GenericCollectionAssertions.cs

Large diffs are not rendered by default.

99 changes: 51 additions & 48 deletions Src/FluentAssertions/Collections/GenericDictionaryAssertions.cs

Large diffs are not rendered by default.

15 changes: 9 additions & 6 deletions Src/FluentAssertions/Collections/StringCollectionAssertions.cs
Expand Up @@ -35,12 +35,15 @@ public class StringCollectionAssertions<TCollection, TAssertions> : GenericColle
where TCollection : IEnumerable<string>
where TAssertions : StringCollectionAssertions<TCollection, TAssertions>
{
private readonly Assertion assertion;

/// <summary>
/// Initializes a new instance of the <see cref="StringCollectionAssertions{TCollection, TAssertions}"/> class.
/// </summary>
public StringCollectionAssertions(TCollection actualValue, Assertion assertion)
: base(actualValue, assertion)
{
this.assertion = assertion;
}

/// <summary>
Expand Down Expand Up @@ -248,16 +251,16 @@ public AndConstraint<TAssertions> BeEquivalentTo(params string[] expectation)
Guard.ThrowIfArgumentIsEmpty(wildcardPattern, nameof(wildcardPattern),
"Cannot match strings in collection against an empty string. Provide a wildcard pattern or use the Contain method.");

bool success = Execute.Assertion
assertion
.BecauseOf(because, becauseArgs)
.ForCondition(Subject is not null)
.FailWith("Expected {context:collection} to contain a match of {0}{reason}, but found <null>.", wildcardPattern);

IEnumerable<string> matched = [];

if (success)
if (assertion.Succeeded)
{
Execute.Assertion
assertion
.BecauseOf(because, becauseArgs)
.ForCondition(ContainsMatch(wildcardPattern))
.FailWith("Expected {context:collection} {0} to contain a match of {1}{reason}.", Subject, wildcardPattern);
Expand Down Expand Up @@ -333,15 +336,15 @@ private IEnumerable<string> AllThatMatch(string wildcardPattern)
Guard.ThrowIfArgumentIsEmpty(wildcardPattern, nameof(wildcardPattern),
"Cannot match strings in collection against an empty string. Provide a wildcard pattern or use the NotContain method.");

bool success = Execute.Assertion
assertion
.BecauseOf(because, becauseArgs)
.ForCondition(Subject is not null)
.FailWith("Did not expect {context:collection} to contain a match of {0}{reason}, but found <null>.",
wildcardPattern);

if (success)
if (assertion.Succeeded)
{
Execute.Assertion
assertion
.BecauseOf(because, becauseArgs)
.ForCondition(NotContainsMatch(wildcardPattern))
.FailWith("Did not expect {context:collection} {0} to contain a match of {1}{reason}.", Subject, wildcardPattern);
Expand Down
1 change: 0 additions & 1 deletion Src/FluentAssertions/Common/MethodInfoExtensions.cs
Expand Up @@ -30,7 +30,6 @@ internal static bool IsAsync(this MethodInfo methodInfo)
if (typeof(TAttribute) == typeof(MethodImplAttribute) && memberInfo is MethodBase methodBase)
{
(bool success, MethodImplAttribute methodImplAttribute) = RecreateMethodImplAttribute(methodBase);

if (success)
{
customAttributes.Add(methodImplAttribute as TAttribute);
Expand Down
2 changes: 1 addition & 1 deletion Src/FluentAssertions/CustomAssertionAttribute.cs
Expand Up @@ -4,7 +4,7 @@ namespace FluentAssertions;

/// <summary>
/// Marks a method as an extension to Fluent Assertions that either uses the built-in assertions
/// internally, or directly uses the <c>Execute.Assertion</c>.
/// internally, or directly uses the <c>assertion</c>.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
#pragma warning disable CA1813 // Avoid unsealed attributes. This type has shipped.
Expand Down
2 changes: 1 addition & 1 deletion Src/FluentAssertions/CustomAssertionsAssemblyAttribute.cs
Expand Up @@ -4,7 +4,7 @@ namespace FluentAssertions;

/// <summary>
/// Marks an assembly as containing extensions to Fluent Assertions that either uses the built-in assertions
/// internally, or directly uses the <c>Execute.Assertion</c>.
/// internally, or directly uses the <c>assertion</c>.
/// </summary>
[AttributeUsage(AttributeTargets.Assembly)]
public sealed class CustomAssertionsAssemblyAttribute : Attribute;
5 changes: 3 additions & 2 deletions Src/FluentAssertions/EnumAssertionsExtensions.cs
@@ -1,6 +1,7 @@
using System;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using FluentAssertions.Execution;
using FluentAssertions.Primitives;

namespace FluentAssertions;
Expand All @@ -19,7 +20,7 @@ public static class EnumAssertionsExtensions
public static EnumAssertions<TEnum> Should<TEnum>(this TEnum @enum)
where TEnum : struct, Enum
{
return new EnumAssertions<TEnum>(@enum);
return new EnumAssertions<TEnum>(@enum, Assertion.GetOrCreate());
}

/// <summary>
Expand All @@ -30,6 +31,6 @@ public static EnumAssertions<TEnum> Should<TEnum>(this TEnum @enum)
public static NullableEnumAssertions<TEnum> Should<TEnum>(this TEnum? @enum)
where TEnum : struct, Enum
{
return new NullableEnumAssertions<TEnum>(@enum);
return new NullableEnumAssertions<TEnum>(@enum, Assertion.GetOrCreate());
}
}
2 changes: 1 addition & 1 deletion Src/FluentAssertions/Equivalency/EquivalencyResult.cs
Expand Up @@ -3,5 +3,5 @@ namespace FluentAssertions.Equivalency;
public enum EquivalencyResult
{
ContinueWithNext,
AssertionCompleted
EquivalencyProven
}
15 changes: 9 additions & 6 deletions Src/FluentAssertions/Equivalency/EquivalencyStep.cs
@@ -1,24 +1,27 @@
namespace FluentAssertions.Equivalency;
using FluentAssertions.Execution;

namespace FluentAssertions.Equivalency;

/// <summary>
/// Convenient implementation of <see cref="IEquivalencyStep"/> that will only invoke
/// </summary>
public abstract class EquivalencyStep<T> : IEquivalencyStep
{
public EquivalencyResult Handle(Comparands comparands, IEquivalencyValidationContext context,
IEquivalencyValidator nestedValidator)
public EquivalencyResult Handle(Comparands comparands, Assertion assertion, IEquivalencyValidationContext context,
IValidateChildNodeEquivalency nestedValidator)
{
if (!typeof(T).IsAssignableFrom(comparands.GetExpectedType(context.Options)))
{
return EquivalencyResult.ContinueWithNext;
}

return OnHandle(comparands, context, nestedValidator);
return OnHandle(comparands, assertion, context, nestedValidator);
}

/// <summary>
/// Implements <see cref="IEquivalencyStep.Handle"/>, but only gets called when the expected type matches <typeparamref name="T"/>.
/// </summary>
protected abstract EquivalencyResult OnHandle(Comparands comparands, IEquivalencyValidationContext context,
IEquivalencyValidator nestedValidator);
protected abstract EquivalencyResult OnHandle(Comparands comparands, Assertion assertion,
IEquivalencyValidationContext context,
IValidateChildNodeEquivalency nestedValidator);
}
45 changes: 25 additions & 20 deletions Src/FluentAssertions/Equivalency/EquivalencyValidator.cs
Expand Up @@ -7,71 +7,76 @@ namespace FluentAssertions.Equivalency;
/// <summary>
/// Is responsible for validating the equivalency of a subject with another object.
/// </summary>
public class EquivalencyValidator : IEquivalencyValidator
public class EquivalencyValidator : IValidateChildNodeEquivalency
{
private const int MaxDepth = 10;

public void AssertEquality(Comparands comparands, EquivalencyValidationContext context)
{
using var scope = new AssertionScope();

scope.AssumeSingleCaller();
scope.AddReportable("configuration", () => context.Options.ToString());
scope.BecauseOf(context.Reason);
var getIdentifierOnce = new Lazy<string>(() => scope.GetIdentifier());

RecursivelyAssertEquality(comparands, context);
var assertion = Assertion.GetOrCreate(() => scope, () => getIdentifierOnce.Value);

assertion.WithReportable("configuration", () => context.Options.ToString());
assertion.BecauseOf(context.Reason);

RecursivelyAssertEquivalencyOf(comparands, assertion, context);

if (context.TraceWriter is not null)
{
scope.AppendTracing(context.TraceWriter.ToString());
assertion.AppendTracing(context.TraceWriter.ToString());
}
}

public void RecursivelyAssertEquality(Comparands comparands, IEquivalencyValidationContext context)
private void RecursivelyAssertEquivalencyOf(Comparands comparands, Assertion assertion, IEquivalencyValidationContext context)
{
var scope = AssertionScope.Current;
AssertEquivalencyOf(comparands, assertion, context);
}

if (ShouldContinueThisDeep(context.CurrentNode, context.Options, scope))
public void AssertEquivalencyOf(Comparands comparands, Assertion assertion, IEquivalencyValidationContext context)
{
if (ShouldContinueThisDeep(context.CurrentNode, context.Options, assertion))
{
TrackWhatIsNeededToProvideContextToFailures(scope, comparands, context.CurrentNode);
TrackWhatIsNeededToProvideContextToFailures(assertion, comparands, context.CurrentNode);

if (!context.IsCyclicReference(comparands.Expectation))
{
TryToProveNodesAreEquivalent(comparands, context);
TryToProveNodesAreEquivalent(assertion, comparands, context);
}
}
}

private static bool ShouldContinueThisDeep(INode currentNode, IEquivalencyOptions options,
AssertionScope assertionScope)
Assertion assertion)
{
bool shouldRecurse = options.AllowInfiniteRecursion || currentNode.Depth <= MaxDepth;
if (!shouldRecurse)
{
// This will throw, unless we're inside an AssertionScope
assertionScope.FailWith($"The maximum recursion depth of {MaxDepth} was reached. ");
assertion.FailWith($"The maximum recursion depth of {MaxDepth} was reached. ");
}

return shouldRecurse;
}

private static void TrackWhatIsNeededToProvideContextToFailures(AssertionScope scope, Comparands comparands, INode currentNode)
private static void TrackWhatIsNeededToProvideContextToFailures(Assertion assertion, Comparands comparands, INode currentNode)
{
scope.Context = new Lazy<string>(() => currentNode.Description);

scope.TrackComparands(comparands.Subject, comparands.Expectation);
assertion.Context = new Lazy<string>(() => currentNode.Description);
assertion.TrackComparands(comparands.Subject, comparands.Expectation);
}

private void TryToProveNodesAreEquivalent(Comparands comparands, IEquivalencyValidationContext context)
private void TryToProveNodesAreEquivalent(Assertion assertion, Comparands comparands, IEquivalencyValidationContext context)
{
using var _ = context.Tracer.WriteBlock(node => node.Description);

Func<IEquivalencyStep, GetTraceMessage> 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)
var result = step.Handle(comparands, assertion, context, this);
if (result == EquivalencyResult.EquivalencyProven)
{
context.Tracer.WriteLine(getMessage(step));
return;
Expand Down
7 changes: 5 additions & 2 deletions Src/FluentAssertions/Equivalency/IEquivalencyStep.cs
@@ -1,3 +1,5 @@
using FluentAssertions.Execution;

namespace FluentAssertions.Equivalency;

/// <summary>
Expand All @@ -9,11 +11,12 @@ public interface IEquivalencyStep
/// Executes an operation such as an equivalency assertion on the provided <paramref name="comparands"/>.
/// </summary>
/// <value>
/// Should return <see cref="EquivalencyResult.AssertionCompleted"/> if the subject matches the expectation or if no additional assertions
/// Should return <see cref="EquivalencyResult.EquivalencyProven"/> if the subject matches the expectation or if no additional assertions
/// have to be executed. Should return <see cref="EquivalencyResult.ContinueWithNext"/> otherwise.
/// </value>
/// <remarks>
/// May throw when preconditions are not met or if it detects mismatching data.
/// </remarks>
EquivalencyResult Handle(Comparands comparands, IEquivalencyValidationContext context, IEquivalencyValidator nestedValidator);
EquivalencyResult Handle(Comparands comparands, Assertion assertion, IEquivalencyValidationContext context,
IValidateChildNodeEquivalency nestedValidator);
}
9 changes: 0 additions & 9 deletions Src/FluentAssertions/Equivalency/IEquivalencyValidator.cs

This file was deleted.

11 changes: 7 additions & 4 deletions Src/FluentAssertions/Equivalency/IMemberMatchingRule.cs
@@ -1,3 +1,5 @@
using FluentAssertions.Execution;

namespace FluentAssertions.Equivalency;

/// <summary>
Expand All @@ -15,17 +17,18 @@ public interface IMemberMatchingRule
/// simply return <see langword="null"/>.
/// </remarks>
/// <param name="expectedMember">
/// The <see cref="IMember"/> of the subject's member for which a match must be found. Can never
/// be <see langword="null"/>.
/// The <see cref="IMember"/> of the subject's member for which a match must be found. Can never
/// be <see langword="null"/>.
/// </param>
/// <param name="subject">
/// The subject object for which a matching member must be returned. Can never be <see langword="null"/>.
/// The subject object for which a matching member must be returned. Can never be <see langword="null"/>.
/// </param>
/// <param name="parent"></param>
/// <param name="options"></param>
/// <param name="assertion"></param>
/// <returns>
/// Returns the <see cref="IMember"/> of the property with which to compare the subject with, or <see langword="null"/>
/// if no match was found.
/// </returns>
IMember Match(IMember expectedMember, object subject, INode parent, IEquivalencyOptions options);
IMember Match(IMember expectedMember, object subject, INode parent, IEquivalencyOptions options, Assertion assertion);
}
11 changes: 11 additions & 0 deletions Src/FluentAssertions/Equivalency/IValidateChildNodeEquivalency.cs
@@ -0,0 +1,11 @@
using FluentAssertions.Execution;

namespace FluentAssertions.Equivalency;

public interface IValidateChildNodeEquivalency
{
/// <summary>
/// Runs a deep recursive equivalency assertion on the provided <paramref name="comparands"/>.
/// </summary>
void AssertEquivalencyOf(Comparands comparands, Assertion assertion, IEquivalencyValidationContext context);
}
@@ -1,6 +1,7 @@
using System;
using System.Text.RegularExpressions;
using FluentAssertions.Common;
using FluentAssertions.Execution;

namespace FluentAssertions.Equivalency.Matching;

Expand Down Expand Up @@ -29,7 +30,7 @@ public MappedMemberMatchingRule(string expectationMemberName, string subjectMemb
this.subjectMemberName = subjectMemberName;
}

public IMember Match(IMember expectedMember, object subject, INode parent, IEquivalencyOptions options)
public IMember Match(IMember expectedMember, object subject, INode parent, IEquivalencyOptions options, Assertion assertion)
{
if (parent.Type.IsSameOrInherits(typeof(TExpectation)) && subject is TSubject &&
expectedMember.Name == expectationMemberName)
Expand Down
@@ -1,5 +1,6 @@
using System;
using FluentAssertions.Common;
using FluentAssertions.Execution;

namespace FluentAssertions.Equivalency.Matching;

Expand Down Expand Up @@ -42,7 +43,7 @@ public MappedPathMatchingRule(string expectationMemberPath, string subjectMember
}
}

public IMember Match(IMember expectedMember, object subject, INode parent, IEquivalencyOptions options)
public IMember Match(IMember expectedMember, object subject, INode parent, IEquivalencyOptions options, Assertion assertion)
{
MemberPath path = expectationPath;

Expand Down
Expand Up @@ -9,7 +9,7 @@ namespace FluentAssertions.Equivalency.Matching;
/// </summary>
internal class MustMatchByNameRule : IMemberMatchingRule
{
public IMember Match(IMember expectedMember, object subject, INode parent, IEquivalencyOptions options)
public IMember Match(IMember expectedMember, object subject, INode parent, IEquivalencyOptions options, Assertion assertion)
{
IMember subjectMember = null;

Expand All @@ -33,12 +33,12 @@ public IMember Match(IMember expectedMember, object subject, INode parent, IEqui

if (subjectMember is null)
{
Execute.Assertion.FailWith(
assertion.FailWith(
$"Expectation has {expectedMember.Description} that the other object does not have.");
}
else if (options.IgnoreNonBrowsableOnSubject && !subjectMember.IsBrowsable)
{
Execute.Assertion.FailWith(
assertion.FailWith(
$"Expectation has {expectedMember.Description} that is non-browsable in the other object, and non-browsable " +
"members on the subject are ignored with the current configuration");
}
Expand Down
@@ -1,5 +1,6 @@
using System.Reflection;
using FluentAssertions.Common;
using FluentAssertions.Execution;

namespace FluentAssertions.Equivalency.Matching;

Expand All @@ -8,7 +9,7 @@ namespace FluentAssertions.Equivalency.Matching;
/// </summary>
internal class TryMatchByNameRule : IMemberMatchingRule
{
public IMember Match(IMember expectedMember, object subject, INode parent, IEquivalencyOptions options)
public IMember Match(IMember expectedMember, object subject, INode parent, IEquivalencyOptions options, Assertion assertion)
{
if (options.IncludedProperties != MemberVisibility.None)
{
Expand Down

0 comments on commit eda0f53

Please sign in to comment.