Skip to content

Commit

Permalink
SAVEPOINT
Browse files Browse the repository at this point in the history
  • Loading branch information
dennisdoomen committed Sep 15, 2024
1 parent 4e57658 commit f18dc76
Show file tree
Hide file tree
Showing 25 changed files with 398 additions and 236 deletions.
26 changes: 19 additions & 7 deletions Src/FluentAssertions/Collections/GenericCollectionAssertions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public GenericCollectionAssertions(TCollection actualValue, AssertionChain asser
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <paramref name="because" />.
/// </param>
public AndWhichConstraint<TAssertions, IEnumerable<TExpectation>> AllBeAssignableTo<TExpectation>(
public AndWhich<TAssertions, IEnumerable<TExpectation>> AllBeAssignableTo<TExpectation>(
[StringSyntax("CompositeFormat")] string because = "",
params object[] becauseArgs)
{
Expand All @@ -92,7 +92,7 @@ public AndWhichConstraint<TAssertions, IEnumerable<TExpectation>> AllBeAssignabl
matches = Subject.OfType<TExpectation>();
}

return new AndWhichConstraint<TAssertions, IEnumerable<TExpectation>>((TAssertions)this, matches);
return new AndWhich<TAssertions, IEnumerable<TExpectation>>((TAssertions)this, matches);
}

/// <summary>
Expand Down Expand Up @@ -211,7 +211,7 @@ public AndConstraint<TAssertions> AllBeEquivalentTo<TExpectation>(TExpectation e
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <paramref name="because" />.
/// </param>
public AndWhichConstraint<TAssertions, IEnumerable<TExpectation>> AllBeOfType<TExpectation>(
public AndWhich<TAssertions, IEnumerable<TExpectation>> AllBeOfType<TExpectation>(
[StringSyntax("CompositeFormat")] string because = "",
params object[] becauseArgs)
{
Expand All @@ -237,7 +237,7 @@ public AndWhichConstraint<TAssertions, IEnumerable<TExpectation>> AllBeOfType<TE
matches = Subject.OfType<TExpectation>();
}

return new AndWhichConstraint<TAssertions, IEnumerable<TExpectation>>((TAssertions)this, matches);
return new AndWhich<TAssertions, IEnumerable<TExpectation>>((TAssertions)this, matches);
}

/// <summary>
Expand Down Expand Up @@ -739,7 +739,7 @@ public AndWhichConstraint<TAssertions, T> Contain(T expected, [StringSyntax("Com
/// Zero or more objects to format using the placeholders in <paramref name="because"/>.
/// </param>
/// <exception cref="ArgumentNullException"><paramref name="predicate"/> is <see langword="null"/>.</exception>
public AndWhichConstraint<TAssertions, T> Contain(Expression<Func<T, bool>> predicate,
public AndWhich<TAssertions, T> Contain(Expression<Func<T, bool>> predicate,
[StringSyntax("CompositeFormat")] string because = "",
params object[] becauseArgs)
{
Expand All @@ -752,19 +752,31 @@ public AndWhichConstraint<TAssertions, T> Contain(Expression<Func<T, bool>> pred

IEnumerable<T> matches = [];

int? firstMatchingIndex = null;
if (assertionChain.Succeeded)
{
Func<T, bool> func = predicate.Compile();

foreach (var (item, index) in Subject!.Select((item, index) => (item, index)))
{
if (func(item))
{
firstMatchingIndex = index;
break;
}
}

assertionChain
.ForCondition(Subject!.Any(func))
.ForCondition(firstMatchingIndex.HasValue)
.BecauseOf(because, becauseArgs)
.FailWith("Expected {context:collection} {0} to have an item matching {1}{reason}.", Subject, predicate.Body);

matches = Subject.Where(func);
}

return new AndWhichConstraint<TAssertions, T>((TAssertions)this, matches);
assertionChain.WithCallerPostfix($"[{firstMatchingIndex}]").ReuseOnce();

return new AndWhich<TAssertions, T>((TAssertions)this, matches);
}

/// <summary>
Expand Down
9 changes: 9 additions & 0 deletions Src/FluentAssertions/Common/ObjectExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using FluentAssertions.Formatting;

namespace FluentAssertions.Common;

Expand Down Expand Up @@ -77,4 +78,12 @@ ushort or
uint or
ulong);
}

/// <summary>
/// Convenience method to format an object to a string using the <see cref="Formatter"/> class.
/// </summary>
public static string ToFormattedString(this object source)
{
return Formatter.ToString(source);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Collections;
using System.Diagnostics.CodeAnalysis;
using FluentAssertions.Common;
using FluentAssertions.Execution;
using FluentAssertions.Formatting;
using static System.FormattableString;
Expand Down Expand Up @@ -35,8 +36,7 @@ protected override EquivalencyResult OnHandle(Comparands comparands,
Invariant(
$"Comparing dictionary item {key} at {member.Description} between subject and expectation"));

assertionChain.ReuseOnce();
assertionChain.WithCallerPostfix($"[{Formatter.ToString(key)}]");
assertionChain.WithCallerPostfix($"[{key.ToFormattedString()}]").ReuseOnce();
subject[key].Should().Be(expectation[key], context.Reason.FormattedMessage, context.Reason.Arguments);
}
}
Expand Down
23 changes: 18 additions & 5 deletions Src/FluentAssertions/Execution/AssertionChain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public sealed class AssertionChain

// We need to keep track of this because we don't want the second successful assertion hide the first unsuccessful assertion
private Func<string> expectation;
private string callerPostfix = string.Empty;
private string callerPrefix = string.Empty;

private static readonly AsyncLocal<AssertionChain> Instance = new();

Expand Down Expand Up @@ -67,7 +69,7 @@ private AssertionChain(Func<AssertionScope> getCurrentScope, Func<string> getCal
};
}

public string CallerIdentifier => getCallerIdentifier();
public string CallerIdentifier => callerPrefix + getCallerIdentifier() + callerPostfix;

/// <summary>
/// Adds an explanation of why the assertion is supposed to succeed to the scope.
Expand Down Expand Up @@ -161,7 +163,7 @@ private Continuation WithExpectation(string message, Action<AssertionChain> chai
var formatter = new FailureMessageFormatter(getCurrentScope().FormattingOptions)
.WithReason(reason?.Invoke() ?? string.Empty)
.WithContext(contextData)
.WithIdentifier(getCallerIdentifier())
.WithIdentifier(CallerIdentifier)
.WithFallbackIdentifier(fallbackIdentifier);
return formatter.Format(message, args);
Expand Down Expand Up @@ -216,7 +218,7 @@ public Continuation FailWith(Func<FailReason> getFailureReason)
var formatter = new FailureMessageFormatter(getCurrentScope().FormattingOptions)
.WithReason(reason?.Invoke() ?? string.Empty)
.WithContext(contextData)
.WithIdentifier(getCallerIdentifier())
.WithIdentifier(CallerIdentifier)
.WithFallbackIdentifier(fallbackIdentifier);
FailReason failReason = getFailureReason();
Expand Down Expand Up @@ -253,12 +255,21 @@ private Continuation FailWith(Func<string> getFailureReason)
public void OverrideCallerIdentifier(Func<string> getCallerIdentifier)
{
this.getCallerIdentifier = getCallerIdentifier;
HasOverriddenCallerIdentifier = true;
}

public AssertionChain WithCallerPostfix(string postfix)
{
var originalCallerIdentifier = getCallerIdentifier;
getCallerIdentifier = () => originalCallerIdentifier() + postfix;
callerPostfix = postfix;
HasOverriddenCallerIdentifier = true;

return this;
}

public AssertionChain WithCallerPrefix(string prefix)
{
callerPrefix = prefix;
HasOverriddenCallerIdentifier = true;

return this;
}
Expand Down Expand Up @@ -305,4 +316,6 @@ public AssertionChain UsingLineBreaks
return this;
}
}

public bool HasOverriddenCallerIdentifier { get; private set; }
}
1 change: 1 addition & 0 deletions Src/FluentAssertions/Formatting/Formatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public static class Formatter
new XElementValueFormatter(),
new XAttributeValueFormatter(),
new PropertyInfoFormatter(),
new MethodInfoFormatter(),
new NullValueFormatter(),
new GuidValueFormatter(),
new DateTimeOffsetValueFormatter(),
Expand Down
31 changes: 31 additions & 0 deletions Src/FluentAssertions/Formatting/MethodInfoFormatter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System.Reflection;

namespace FluentAssertions.Formatting;

public class MethodInfoFormatter : IValueFormatter
{
/// <summary>
/// Indicates whether the current <see cref="IValueFormatter"/> can handle the specified <paramref name="value"/>.
/// </summary>
/// <param name="value">The value for which to create a <see cref="string"/>.</param>
/// <returns>
/// <see langword="true"/> if the current <see cref="IValueFormatter"/> can handle the specified value; otherwise, <see langword="false"/>.
/// </returns>
public bool CanHandle(object value)
{
return value is MethodInfo;
}

public void Format(object value, FormattedObjectGraph formattedGraph, FormattingContext context, FormatChild formatChild)
{
var method = (MethodInfo)value;
if (method is null)
{
formattedGraph.AddFragment("<null>");
}
else
{
formattedGraph.AddFragment($"{method!.DeclaringType!.Name + "." + method.Name}");
}
}
}
11 changes: 10 additions & 1 deletion Src/FluentAssertions/Formatting/PropertyInfoFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ public bool CanHandle(object value)

public void Format(object value, FormattedObjectGraph formattedGraph, FormattingContext context, FormatChild formatChild)
{
formattedGraph.AddFragment(((PropertyInfo)value).Name);
var property = (PropertyInfo)value;

if (property is null)
{
formattedGraph.AddFragment("<null>");
}
else
{
formattedGraph.AddFragment($"{property.DeclaringType?.Name ?? string.Empty}.{property.Name}");
}
}
}
3 changes: 1 addition & 2 deletions Src/FluentAssertions/Primitives/ObjectAssertions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,7 @@ public AndConstraint<TAssertions> Be(TSubject expected,
.BecauseOf(because, becauseArgs)
.ForCondition(ObjectExtensions.GetComparer<TSubject>()(Subject, expected))
.WithDefaultIdentifier(Identifier)
.FailWith("Expected {context} to be {0}{reason}, but found {1}.", expected,
Subject);
.FailWith("Expected {context} to be {0}{reason}, but found {1}.", expected, Subject);

return new AndConstraint<TAssertions>((TAssertions)this);
}
Expand Down
2 changes: 1 addition & 1 deletion Src/FluentAssertions/Types/ConstructorInfoAssertions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ internal static string GetDescriptionFor(ConstructorInfo constructorInfo)
return $"{constructorInfo.DeclaringType}({GetParameterString(constructorInfo)})";
}

internal override string SubjectDescription => GetDescriptionFor(Subject);
protected override string SubjectDescription => GetDescriptionFor(Subject);

protected override string Identifier => "constructor";
}
2 changes: 1 addition & 1 deletion Src/FluentAssertions/Types/MemberInfoAssertions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -153,5 +153,5 @@ public AndConstraint<TAssertions> NotBeDecoratedWith<TAttribute>(

protected override string Identifier => "member";

internal virtual string SubjectDescription => $"{Subject.DeclaringType}.{Subject.Name}";
protected virtual string SubjectDescription => $"{Subject.DeclaringType}.{Subject.Name}";
}
15 changes: 11 additions & 4 deletions Src/FluentAssertions/Types/MethodBaseAssertions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,20 @@ public AndConstraint<TAssertions> HaveAccessModifier(
assertionChain
.BecauseOf(because, becauseArgs)
.ForCondition(Subject is not null)
.FailWith($"Expected method to be {accessModifier}{{reason}}, but {{context:member}} is <null>.");
.FailWith($"Expected method to be {accessModifier}{{reason}}, but {{context:method}} is <null>.");

if (assertionChain.Succeeded)
{
CSharpAccessModifier subjectAccessModifier = Subject.GetCSharpAccessModifier();

var subject = assertionChain.HasOverriddenCallerIdentifier
? assertionChain.CallerIdentifier
: "method " + Subject.ToFormattedString();

assertionChain
.ForCondition(accessModifier == subjectAccessModifier)
.BecauseOf(because, becauseArgs)
.FailWith(
$"Expected method {Subject!.Name} to be {accessModifier}{{reason}}, but it is {subjectAccessModifier}.");
.FailWith($"Expected {subject} to be {accessModifier}{{reason}}, but it is {subjectAccessModifier}.");
}

return new AndConstraint<TAssertions>((TAssertions)this);
Expand Down Expand Up @@ -90,10 +93,14 @@ public AndConstraint<TAssertions> NotHaveAccessModifier(CSharpAccessModifier acc
{
CSharpAccessModifier subjectAccessModifier = Subject.GetCSharpAccessModifier();

var subject = assertionChain.HasOverriddenCallerIdentifier
? assertionChain.CallerIdentifier
: "method " + Subject.ToFormattedString();

assertionChain
.ForCondition(accessModifier != subjectAccessModifier)
.BecauseOf(because, becauseArgs)
.FailWith($"Expected method {Subject!.Name} not to be {accessModifier}{{reason}}, but it is.");
.FailWith($"Expected {subject} not to be {accessModifier}{{reason}}, but it is.");
}

return new AndConstraint<TAssertions>((TAssertions)this);
Expand Down
2 changes: 1 addition & 1 deletion Src/FluentAssertions/Types/MethodInfoAssertions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ internal static string GetDescriptionFor(MethodInfo method)
return $"{returnTypeName} {method.DeclaringType}.{method.Name}";
}

internal override string SubjectDescription => GetDescriptionFor(Subject);
protected override string SubjectDescription => GetDescriptionFor(Subject);

protected override string Identifier => "method";
}
Loading

0 comments on commit f18dc76

Please sign in to comment.