Skip to content

Commit

Permalink
SAVEPOINT
Browse files Browse the repository at this point in the history
  • Loading branch information
dennisdoomen committed Sep 8, 2024
1 parent 4e57658 commit 4c468e5
Show file tree
Hide file tree
Showing 14 changed files with 168 additions and 100 deletions.
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);
}
}
13 changes: 13 additions & 0 deletions Src/FluentAssertions/Execution/AssertionChain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -253,12 +253,23 @@ 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;
HasOverriddenCallerIdentifier = true;

return this;
}

public AssertionChain WithCallerPrefix(string prefix)
{
var originalCallerIdentifier = getCallerIdentifier;
getCallerIdentifier = () => prefix + originalCallerIdentifier();
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}");
}
}
}
13 changes: 12 additions & 1 deletion Src/FluentAssertions/Formatting/PropertyInfoFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,17 @@ 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
{
var propTypeName = property.PropertyType.Name;

formattedGraph.AddFragment($"{propTypeName} {property.DeclaringType}.{property.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
93 changes: 36 additions & 57 deletions Src/FluentAssertions/Types/PropertyInfoAssertions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Reflection;
using FluentAssertions.Common;
using FluentAssertions.Execution;
using FluentAssertions.Formatting;

namespace FluentAssertions.Types;

Expand Down Expand Up @@ -44,7 +45,7 @@ public AndConstraint<PropertyInfoAssertions> BeVirtual(
assertionChain
.ForCondition(Subject.IsVirtual())
.BecauseOf(because, becauseArgs)
.FailWith($"Expected property {GetDescriptionFor(Subject)} to be virtual{{reason}}, but it is not.");
.FailWith("Expected property {0} to be virtual{{reason}}, but it is not.", Subject);
}

return new AndConstraint<PropertyInfoAssertions>(this);
Expand All @@ -60,7 +61,8 @@ public AndConstraint<PropertyInfoAssertions> BeVirtual(
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <paramref name="because" />.
/// </param>
public AndConstraint<PropertyInfoAssertions> NotBeVirtual([StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs)
public AndConstraint<PropertyInfoAssertions> NotBeVirtual([StringSyntax("CompositeFormat")] string because = "",
params object[] becauseArgs)
{
assertionChain
.BecauseOf(because, becauseArgs)
Expand All @@ -72,7 +74,7 @@ public AndConstraint<PropertyInfoAssertions> NotBeVirtual([StringSyntax("Composi
assertionChain
.ForCondition(!Subject.IsVirtual())
.BecauseOf(because, becauseArgs)
.FailWith($"Expected property {GetDescriptionFor(Subject)} not to be virtual{{reason}}, but it is.");
.FailWith("Expected property {0} not to be virtual{{reason}}, but it is.", Subject);
}

return new AndConstraint<PropertyInfoAssertions>(this);
Expand Down Expand Up @@ -130,21 +132,17 @@ public AndConstraint<PropertyInfoAssertions> BeWritable(CSharpAccessModifier acc
assertionChain
.BecauseOf(because, becauseArgs)
.ForCondition(Subject is not null)
.FailWith($"Expected {Identifier} to be {accessModifier}{{reason}}, but {{context:property}} is <null>.");
.FailWith($"Expected {{context:property}} to be {accessModifier}{{reason}}, but it is <null>.")
.Then
.ForCondition(Subject!.CanWrite)
.BecauseOf(because, becauseArgs)
.FailWith("Expected {0} to have a setter {{reason}}.", assertionChain.HasOverriddenCallerIdentifier ? assertionChain.CallerIdentifier : Subject);

if (assertionChain.Succeeded)
{
assertionChain
.ForCondition(Subject!.CanWrite)
.BecauseOf(because, becauseArgs)
.FailWith(
"Expected {context:property} {0} to have a setter{reason}.",
Subject);

if (assertionChain.Succeeded)
{
Subject!.GetSetMethod(nonPublic: true).Should().HaveAccessModifier(accessModifier, because, becauseArgs);
}
assertionChain.WithCallerPrefix("setter of ");
assertionChain.ReuseOnce();
Subject!.GetSetMethod(nonPublic: true).Should().HaveAccessModifier(accessModifier, because, becauseArgs);
}

return new AndConstraint<PropertyInfoAssertions>(this);
Expand All @@ -166,17 +164,11 @@ public AndConstraint<PropertyInfoAssertions> NotBeWritable(
assertionChain
.BecauseOf(because, becauseArgs)
.ForCondition(Subject is not null)
.FailWith("Expected property not to have a setter{reason}, but {context:property} is <null>.");

if (assertionChain.Succeeded)
{
assertionChain
.ForCondition(!Subject!.CanWrite)
.BecauseOf(because, becauseArgs)
.FailWith(
"Expected {context:property} {0} not to have a setter{reason}.",
Subject);
}
.FailWith("Expected {context:property} not to have a setter{reason}, but it is <null>.")
.Then
.ForCondition(!Subject!.CanWrite)
.BecauseOf(because, becauseArgs)
.FailWith("Expected {0} not to have a setter {{reason}}.", Subject);

return new AndConstraint<PropertyInfoAssertions>(this);
}
Expand All @@ -191,7 +183,8 @@ public AndConstraint<PropertyInfoAssertions> NotBeWritable(
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <paramref name="because" />.
/// </param>
public AndConstraint<PropertyInfoAssertions> BeReadable([StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs)
public AndConstraint<PropertyInfoAssertions> BeReadable([StringSyntax("CompositeFormat")] string because = "",
params object[] becauseArgs)
{
assertionChain
.BecauseOf(because, becauseArgs)
Expand All @@ -202,7 +195,7 @@ public AndConstraint<PropertyInfoAssertions> BeReadable([StringSyntax("Composite
{
assertionChain.ForCondition(Subject!.CanRead)
.BecauseOf(because, becauseArgs)
.FailWith("Expected property " + Subject.Name + " to have a getter{reason}, but it does not.");
.FailWith("Expected property {0}, to have a getter{reason}, but it does not.", Subject);
}

return new AndConstraint<PropertyInfoAssertions>(this);
Expand All @@ -229,18 +222,15 @@ public AndConstraint<PropertyInfoAssertions> BeReadable(CSharpAccessModifier acc
assertionChain
.BecauseOf(because, becauseArgs)
.ForCondition(Subject is not null)
.FailWith($"Expected {Identifier} to be {accessModifier}{{reason}}, but {{context:property}} is <null>.");
.FailWith($"Expected {{context:property}} to be {accessModifier}{{reason}}, but it is <null>.")
.Then
.ForCondition(Subject!.CanRead)
.BecauseOf(because, becauseArgs)
.FailWith("Expected {0} to have a getter {{reason}}, but it does not.", Subject);

if (assertionChain.Succeeded)
{
assertionChain.ForCondition(Subject!.CanRead)
.BecauseOf(because, becauseArgs)
.FailWith("Expected property " + Subject.Name + " to have a getter{reason}, but it does not.");

if (assertionChain.Succeeded)
{
Subject!.GetGetMethod(nonPublic: true).Should().HaveAccessModifier(accessModifier, because, becauseArgs);
}
Subject!.GetGetMethod(nonPublic: true).Should().HaveAccessModifier(accessModifier, because, becauseArgs);
}

return new AndConstraint<PropertyInfoAssertions>(this);
Expand Down Expand Up @@ -270,8 +260,7 @@ public AndConstraint<PropertyInfoAssertions> NotBeReadable(
.ForCondition(!Subject!.CanRead)
.BecauseOf(because, becauseArgs)
.FailWith(
"Expected {context:property} {0} not to have a getter{reason}.",
Subject);
"Expected {0} not to have a getter {{reason}}.", Subject);
}

return new AndConstraint<PropertyInfoAssertions>(this);
Expand Down Expand Up @@ -303,8 +292,8 @@ public AndConstraint<PropertyInfoAssertions> Return(Type propertyType,
{
assertionChain.ForCondition(Subject!.PropertyType == propertyType)
.BecauseOf(because, becauseArgs)
.FailWith("Expected Type of property " + Subject.Name + " to be {0}{reason}, but it is {1}.",
propertyType, Subject.PropertyType);
.FailWith("Expected type of property {2} to be {0}{reason}, but it is {1}.",
propertyType, Subject.PropertyType, Subject);
}

return new AndConstraint<PropertyInfoAssertions>(this);
Expand All @@ -321,7 +310,8 @@ public AndConstraint<PropertyInfoAssertions> Return(Type propertyType,
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <paramref name="because" />.
/// </param>
public AndConstraint<PropertyInfoAssertions> Return<TReturn>([StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs)
public AndConstraint<PropertyInfoAssertions> Return<TReturn>([StringSyntax("CompositeFormat")] string because = "",
params object[] becauseArgs)
{
return Return(typeof(TReturn), because, becauseArgs);
}
Expand Down Expand Up @@ -353,7 +343,7 @@ public AndConstraint<PropertyInfoAssertions> NotReturn(Type propertyType,
assertionChain
.ForCondition(Subject!.PropertyType != propertyType)
.BecauseOf(because, becauseArgs)
.FailWith("Expected Type of property " + Subject.Name + " not to be {0}{reason}, but it is.", propertyType);
.FailWith("Expected Type of property {1} not to be {0}{reason}, but it is.", propertyType, Subject);
}

return new AndConstraint<PropertyInfoAssertions>(this);
Expand All @@ -370,24 +360,13 @@ public AndConstraint<PropertyInfoAssertions> NotReturn(Type propertyType,
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <paramref name="because" />.
/// </param>
public AndConstraint<PropertyInfoAssertions> NotReturn<TReturn>([StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs)
public AndConstraint<PropertyInfoAssertions> NotReturn<TReturn>([StringSyntax("CompositeFormat")] string because = "",
params object[] becauseArgs)
{
return NotReturn(typeof(TReturn), because, becauseArgs);
}

internal static string GetDescriptionFor(PropertyInfo property)
{
if (property is null)
{
return string.Empty;
}

var propTypeName = property.PropertyType.Name;

return $"{propTypeName} {property.DeclaringType}.{property.Name}";
}

internal override string SubjectDescription => GetDescriptionFor(Subject);
internal override string SubjectDescription => Formatter.ToString(Subject);

/// <summary>
/// Returns the type of the subject the assertion applies on.
Expand Down
3 changes: 2 additions & 1 deletion Src/FluentAssertions/Types/PropertyInfoSelectorAssertions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Reflection;
using FluentAssertions.Common;
using FluentAssertions.Execution;
using FluentAssertions.Formatting;

namespace FluentAssertions.Types;

Expand Down Expand Up @@ -221,7 +222,7 @@ private PropertyInfo[] GetPropertiesWith<TAttribute>()

private static string GetDescriptionsFor(IEnumerable<PropertyInfo> properties)
{
IEnumerable<string> descriptions = properties.Select(property => PropertyInfoAssertions.GetDescriptionFor(property));
IEnumerable<string> descriptions = properties.Select(property => Formatter.ToString(property));

return string.Join(Environment.NewLine, descriptions);
}
Expand Down
Loading

0 comments on commit 4c468e5

Please sign in to comment.