Skip to content

Commit

Permalink
Allow specifying EquivalencyOptions in string assertions (#2413)
Browse files Browse the repository at this point in the history
  • Loading branch information
vbreuss committed Jan 14, 2024
1 parent ca87a81 commit f3ff0ff
Show file tree
Hide file tree
Showing 31 changed files with 1,760 additions and 259 deletions.
32 changes: 12 additions & 20 deletions Src/FluentAssertions/Common/StringExtensions.cs
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using FluentAssertions.Formatting;
Expand All @@ -9,15 +10,13 @@ internal static class StringExtensions
{
/// <summary>
/// Finds the first index at which the <paramref name="value"/> does not match the <paramref name="expected"/>
/// string anymore, accounting for the specified <paramref name="stringComparison"/>.
/// string anymore, accounting for the specified <paramref name="comparer"/>.
/// </summary>
public static int IndexOfFirstMismatch(this string value, string expected, StringComparison stringComparison)
public static int IndexOfFirstMismatch(this string value, string expected, IEqualityComparer<string> comparer)
{
Func<char, char, bool> comparer = GetCharComparer(stringComparison);

for (int index = 0; index < value.Length; index++)
{
if (index >= expected.Length || !comparer(value[index], expected[index]))
if (index >= expected.Length || !comparer.Equals(value[index..(index + 1)], expected[index..(index + 1)]))
{
return index;
}
Expand All @@ -26,11 +25,6 @@ public static int IndexOfFirstMismatch(this string value, string expected, Strin
return -1;
}

private static Func<char, char, bool> GetCharComparer(StringComparison stringComparison) =>
stringComparison == StringComparison.Ordinal
? (x, y) => x == y
: (x, y) => char.ToUpperInvariant(x) == char.ToUpperInvariant(y);

/// <summary>
/// Gets the quoted three characters at the specified index of a string, including the index itself.
/// </summary>
Expand Down Expand Up @@ -129,23 +123,21 @@ public static string RemoveNewLines(this string @this)
}

/// <summary>
/// Counts the number of times a substring appears within a string by using the specified <see cref="StringComparison"/>.
/// Counts the number of times the <paramref name="substring"/> appears within a string by using the specified <paramref name="comparer"/>.
/// </summary>
/// <param name="str">The string to search in.</param>
/// <param name="substring">The substring to search for.</param>
/// <param name="comparisonType">The <see cref="StringComparison"/> option to use for comparison.</param>
public static int CountSubstring(this string str, string substring, StringComparison comparisonType)
public static int CountSubstring(this string str, string substring, IEqualityComparer<string> comparer)
{
string actual = str ?? string.Empty;
string search = substring ?? string.Empty;

int count = 0;
int index = 0;

while ((index = actual.IndexOf(search, index, comparisonType)) >= 0)
int maxIndex = actual.Length - search.Length;
for (int index = 0; index <= maxIndex; index++)
{
index += search.Length;
count++;
if (comparer.Equals(actual[index..(index + search.Length)], search))
{
count++;
}
}

return count;
Expand Down
Expand Up @@ -73,5 +73,11 @@ public EqualityStrategy GetEqualityStrategy(Type type)
return inner.GetEqualityStrategy(type);
}

public bool IgnoreLeadingWhitespace => inner.IgnoreLeadingWhitespace;

public bool IgnoreTrailingWhitespace => inner.IgnoreTrailingWhitespace;

public bool IgnoreCase => inner.IgnoreCase;

public ITraceWriter TraceWriter => inner.TraceWriter;
}
15 changes: 15 additions & 0 deletions Src/FluentAssertions/Equivalency/IEquivalencyOptions.cs
Expand Up @@ -98,4 +98,19 @@ public interface IEquivalencyOptions
/// Determines the right strategy for evaluating the equality of objects of this type.
/// </summary>
EqualityStrategy GetEqualityStrategy(Type type);

/// <summary>
/// Gets a value indicating whether leading whitespace is ignored when comparing <see langword="string" />s.
/// </summary>
bool IgnoreLeadingWhitespace { get; }

/// <summary>
/// Gets a value indicating whether trailing whitespace is ignored when comparing <see langword="string" />s.
/// </summary>
bool IgnoreTrailingWhitespace { get; }

/// <summary>
/// Gets a value indicating whether a case-insensitive comparer is used when comparing <see langword="string" />s.
/// </summary>
bool IgnoreCase { get; }
}
Expand Up @@ -56,6 +56,8 @@ public abstract class SelfReferenceEquivalencyOptions<TSelf> : IEquivalencyOptio
private bool ignoreNonBrowsableOnSubject;
private bool excludeNonBrowsableOnExpectation;

private IEqualityComparer<string> stringComparer;

#endregion

private protected SelfReferenceEquivalencyOptions()
Expand Down Expand Up @@ -86,6 +88,9 @@ protected SelfReferenceEquivalencyOptions(IEquivalencyOptions defaults)
includedFields = defaults.IncludedFields;
ignoreNonBrowsableOnSubject = defaults.IgnoreNonBrowsableOnSubject;
excludeNonBrowsableOnExpectation = defaults.ExcludeNonBrowsableOnExpectation;
IgnoreLeadingWhitespace = defaults.IgnoreLeadingWhitespace;
IgnoreTrailingWhitespace = defaults.IgnoreTrailingWhitespace;
IgnoreCase = defaults.IgnoreCase;

ConversionSelector = defaults.ConversionSelector.Clone();

Expand Down Expand Up @@ -180,6 +185,12 @@ IEnumerable<IMemberSelectionRule> IEquivalencyOptions.SelectionRules
EqualityStrategy IEquivalencyOptions.GetEqualityStrategy(Type type)
=> equalityStrategyProvider.GetEqualityStrategy(type);

public bool IgnoreLeadingWhitespace { get; private set; }

public bool IgnoreTrailingWhitespace { get; private set; }

public bool IgnoreCase { get; private set; }

public ITraceWriter TraceWriter { get; private set; }

/// <summary>
Expand Down Expand Up @@ -497,6 +508,18 @@ public TSelf Using<T>(IEqualityComparer<T> comparer)
return (TSelf)this;
}

/// <summary>
/// Ensures the equivalency comparison will use the specified implementation of <see cref="IEqualityComparer{String}"/>
/// any time when a property is a <see langword="string" />.
/// </summary>
public TSelf Using(IEqualityComparer<string> comparer)
{
userEquivalencySteps.Insert(0, new EqualityComparerEquivalencyStep<string>(comparer));
stringComparer = comparer;

return (TSelf)this;
}

/// <summary>
/// Causes all collections to be compared in the order in which the items appear in the expectation.
/// </summary>
Expand Down Expand Up @@ -679,6 +702,44 @@ public TSelf WithoutAutoConversionFor(Expression<Func<IObjectInfo, bool>> predic
return (TSelf)this;
}

/// <summary>
/// Instructs the comparison to ignore leading whitespace when comparing <see langword="string" />s.
/// </summary>
/// <remarks>
/// Note: This affects the index of first mismatch, as the removed whitespace is also ignored for the index calculation!
/// </remarks>
public TSelf IgnoringLeadingWhitespace()
{
IgnoreLeadingWhitespace = true;
return (TSelf)this;
}

/// <summary>
/// Instructs the comparison to ignore trailing whitespace when comparing <see langword="string" />s.
/// </summary>
public TSelf IgnoringTrailingWhitespace()
{
IgnoreTrailingWhitespace = true;
return (TSelf)this;
}

/// <summary>
/// Instructs the comparison to compare <see langword="string" />s case-insensitive.
/// </summary>
public TSelf IgnoringCase()
{
IgnoreCase = true;
return (TSelf)this;
}

/// <summary>
/// Returns the comparer for strings, which is either an explicitly specified comparer via <see cref="Using{T}(System.Collections.Generic.IEqualityComparer{T})"/> or an ordinal comparer depending on <see cref="IgnoringCase()" />.
/// </summary>
internal IEqualityComparer<string> GetStringComparerOrDefault()
{
return stringComparer ?? (IgnoreCase ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal);
}

/// <summary>
/// Returns a string that represents the current object.
/// </summary>
Expand Down
Expand Up @@ -28,12 +28,40 @@ public class StringEqualityEquivalencyStep : IEquivalencyStep
string expectation = (string)comparands.Expectation;

subject.Should()
.Be(expectation, context.Reason.FormattedMessage, context.Reason.Arguments);
.Be(expectation, CreateOptions(context.Options),
context.Reason.FormattedMessage, context.Reason.Arguments);
}

return EquivalencyResult.AssertionCompleted;
}

private static Func<EquivalencyOptions<string>, EquivalencyOptions<string>>
CreateOptions(IEquivalencyOptions existingOptions) =>
o =>
{
if (existingOptions is EquivalencyOptions<string> equivalencyOptions)
{
return equivalencyOptions;
}
if (existingOptions.IgnoreLeadingWhitespace)
{
o.IgnoringLeadingWhitespace();
}
if (existingOptions.IgnoreTrailingWhitespace)
{
o.IgnoringTrailingWhitespace();
}
if (existingOptions.IgnoreCase)
{
o.IgnoringCase();
}
return o;
};

private static bool ValidateAgainstNulls(Comparands comparands, INode currentNode)
{
object expected = comparands.Expectation;
Expand Down
33 changes: 0 additions & 33 deletions Src/FluentAssertions/Primitives/NegatedStringStartStrategy.cs

This file was deleted.

0 comments on commit f3ff0ff

Please sign in to comment.