Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow IEqualityComparer in string (Not)BeEquivalentTo assertion #2372

Closed
14 changes: 4 additions & 10 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;
dennisdoomen marked this conversation as resolved.
Show resolved Hide resolved
}

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
81 changes: 73 additions & 8 deletions Src/FluentAssertions/Primitives/StringAssertions.cs
Expand Up @@ -54,7 +54,7 @@ public StringAssertions(string value)
public AndConstraint<TAssertions> Be(string expected, string because = "", params object[] becauseArgs)
{
var stringEqualityValidator = new StringValidator(
new StringEqualityStrategy(StringComparison.Ordinal),
new StringEqualityStrategy(StringComparer.Ordinal),
because, becauseArgs);

stringEqualityValidator.Validate(Subject, expected);
Expand Down Expand Up @@ -110,11 +110,40 @@ public AndConstraint<TAssertions> BeOneOf(IEnumerable<string> validValues, strin
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <paramref name="because" />.
/// </param>
public AndConstraint<TAssertions> BeEquivalentTo(string expected, string because = "",
params object[] becauseArgs)
public AndConstraint<TAssertions> BeEquivalentTo(string expected,
vbreuss marked this conversation as resolved.
Show resolved Hide resolved
string because = "", params object[] becauseArgs)
{
var expectation = new StringValidator(
new StringEqualityStrategy(StringComparison.OrdinalIgnoreCase),
new StringEqualityStrategy(StringComparer.OrdinalIgnoreCase),
because, becauseArgs);

expectation.Validate(Subject, expected);

return new AndConstraint<TAssertions>((TAssertions)this);
}

/// <summary>
/// Asserts that a string is exactly the same as another string, using the provided <paramref name="comparer"/>.
/// </summary>
/// <param name="expected">
/// The string that the subject is expected to be equivalent to.
/// </param>
/// <param name="comparer">
/// The string equality comparer.
/// </param>
/// <param name="because">
/// A formatted phrase as is supported by <see cref="string.Format(string,object[])" /> explaining why the assertion
/// is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
/// </param>
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <paramref name="because" />.
/// </param>
public AndConstraint<TAssertions> BeEquivalentTo(string expected,
IEqualityComparer<string> comparer,
string because = "", params object[] becauseArgs)
{
var expectation = new StringValidator(
new StringEqualityStrategy(comparer),
because, becauseArgs);

expectation.Validate(Subject, expected);
Expand All @@ -136,8 +165,8 @@ public AndConstraint<TAssertions> BeOneOf(IEnumerable<string> validValues, strin
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <paramref name="because" />.
/// </param>
public AndConstraint<TAssertions> NotBeEquivalentTo(string unexpected, string because = "",
params object[] becauseArgs)
public AndConstraint<TAssertions> NotBeEquivalentTo(string unexpected,
string because = "", params object[] becauseArgs)
{
bool notEquivalent;

Expand All @@ -155,6 +184,42 @@ public AndConstraint<TAssertions> BeOneOf(IEnumerable<string> validValues, strin
return new AndConstraint<TAssertions>((TAssertions)this);
}

/// <summary>
/// Asserts that a string is not exactly the same as another string, using the provided <paramref name="comparer"/>.
/// </summary>
/// <param name="unexpected">
/// The string that the subject is not expected to be equivalent to.
/// </param>
/// <param name="comparer">
/// The string equality comparer.
/// </param>
/// <param name="because">
/// A formatted phrase as is supported by <see cref="string.Format(string,object[])" /> explaining why the assertion
/// is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
/// </param>
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <paramref name="because" />.
/// </param>
public AndConstraint<TAssertions> NotBeEquivalentTo(string unexpected,
IEqualityComparer<string> comparer,
string because = "", params object[] becauseArgs)
{
bool notEquivalent;

using (var scope = new AssertionScope())
{
Subject.Should().BeEquivalentTo(unexpected, comparer);
notEquivalent = scope.Discard().Length > 0;
}

Execute.Assertion
.ForCondition(notEquivalent)
.BecauseOf(because, becauseArgs)
.FailWith("Expected {context:string} not to be equivalent to {0}{reason}, but they are.", unexpected);

return new AndConstraint<TAssertions>((TAssertions)this);
}

/// <summary>
/// Asserts that a string is not exactly the same as the specified <paramref name="unexpected"/>,
/// including the casing and any leading or trailing whitespace.
Expand Down Expand Up @@ -675,7 +740,7 @@ public AndConstraint<TAssertions> StartWith(string expected, string because = ""
Guard.ThrowIfArgumentIsNull(expected, nameof(expected), "Cannot compare start of string with <null>.");

var stringStartValidator = new StringValidator(
new StringStartStrategy(StringComparison.Ordinal),
new StringStartStrategy(StringComparer.Ordinal),
because, becauseArgs);

stringStartValidator.Validate(Subject, expected);
Expand Down Expand Up @@ -728,7 +793,7 @@ public AndConstraint<TAssertions> NotStartWith(string unexpected, string because
Guard.ThrowIfArgumentIsNull(expected, nameof(expected), "Cannot compare string start equivalence with <null>.");

var stringStartValidator = new StringValidator(
new StringStartStrategy(StringComparison.OrdinalIgnoreCase),
new StringStartStrategy(StringComparer.OrdinalIgnoreCase),
because, becauseArgs);

stringStartValidator.Validate(Subject, expected);
Expand Down
24 changes: 12 additions & 12 deletions Src/FluentAssertions/Primitives/StringEqualityStrategy.cs
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using FluentAssertions.Common;
Expand All @@ -8,11 +9,13 @@ namespace FluentAssertions.Primitives;

internal class StringEqualityStrategy : IStringComparisonStrategy
{
private readonly StringComparison comparisonMode;
private readonly IEqualityComparer<string> comparer;
private readonly bool ignoringCase;

public StringEqualityStrategy(StringComparison comparisonMode)
public StringEqualityStrategy(IEqualityComparer<string> comparer)
{
this.comparisonMode = comparisonMode;
this.comparer = comparer;
ignoringCase = comparer.Equals("A", "a");
}

public void ValidateAgainstMismatch(IAssertionScope assertion, string subject, string expected)
Expand All @@ -21,7 +24,7 @@ public void ValidateAgainstMismatch(IAssertionScope assertion, string subject, s

if (expected.IsLongOrMultiline() || subject.IsLongOrMultiline())
{
int indexOfMismatch = subject.IndexOfFirstMismatch(expected, comparisonMode);
int indexOfMismatch = subject.IndexOfFirstMismatch(expected, comparer);

if (indexOfMismatch == -1)
{
Expand All @@ -47,7 +50,7 @@ public void ValidateAgainstMismatch(IAssertionScope assertion, string subject, s
}
else if (ValidateAgainstLengthDifferences(assertion, subject, expected))
{
int indexOfMismatch = subject.IndexOfFirstMismatch(expected, comparisonMode);
int indexOfMismatch = subject.IndexOfFirstMismatch(expected, comparer);

if (indexOfMismatch != -1)
{
Expand All @@ -63,21 +66,18 @@ public string ExpectationDescription
{
get
{
string predicateDescription = IgnoreCase ? "be equivalent to" : "be";
string predicateDescription = ignoringCase ? "be equivalent to" : "be";
return "Expected {context:string} to " + predicateDescription + " ";
}
}

private bool IgnoreCase
=> comparisonMode == StringComparison.OrdinalIgnoreCase;

private void ValidateAgainstSuperfluousWhitespace(IAssertionScope assertion, string subject, string expected)
{
assertion
.ForCondition(!(expected.Length > subject.Length && expected.TrimEnd().Equals(subject, comparisonMode)))
.ForCondition(!(expected.Length > subject.Length && comparer.Equals(expected.TrimEnd(), subject)))
.FailWith(ExpectationDescription + "{0}{reason}, but it misses some extra whitespace at the end.", expected)
.Then
.ForCondition(!(subject.Length > expected.Length && subject.TrimEnd().Equals(expected, comparisonMode)))
.ForCondition(!(subject.Length > expected.Length && comparer.Equals(subject.TrimEnd(), expected)))
.FailWith(ExpectationDescription + "{0}{reason}, but it has unexpected whitespace at the end.", expected);
}

Expand All @@ -98,7 +98,7 @@ private bool ValidateAgainstLengthDifferences(IAssertionScope assertion, string

private string GetMismatchSegmentForStringsOfDifferentLengths(string subject, string expected)
{
int indexOfMismatch = subject.IndexOfFirstMismatch(expected, comparisonMode);
int indexOfMismatch = subject.IndexOfFirstMismatch(expected, comparer);

// If there is no difference it means that expected starts with subject and subject is shorter than expected
if (indexOfMismatch == -1)
Expand Down
21 changes: 10 additions & 11 deletions Src/FluentAssertions/Primitives/StringStartStrategy.cs
@@ -1,30 +1,29 @@
using System;
using System.Collections.Generic;
using FluentAssertions.Common;
using FluentAssertions.Execution;

namespace FluentAssertions.Primitives;

internal class StringStartStrategy : IStringComparisonStrategy
{
private readonly StringComparison stringComparison;
private readonly IEqualityComparer<string> comparer;
private readonly bool ignoringCase;

public StringStartStrategy(StringComparison stringComparison)
public StringStartStrategy(IEqualityComparer<string> comparer)
{
this.stringComparison = stringComparison;
this.comparer = comparer;
ignoringCase = comparer.Equals("A", "a");
}

public string ExpectationDescription
{
get
{
string predicateDescription = IgnoreCase ? "start with equivalent of" : "start with";
string predicateDescription = ignoringCase ? "start with equivalent of" : "start with";
return "Expected {context:string} to " + predicateDescription + " ";
}
}

private bool IgnoreCase
=> stringComparison == StringComparison.OrdinalIgnoreCase;

public void ValidateAgainstMismatch(IAssertionScope assertion, string subject, string expected)
{
if (!assertion
Expand All @@ -34,13 +33,13 @@ public void ValidateAgainstMismatch(IAssertionScope assertion, string subject, s
return;
}

if (subject.StartsWith(expected, stringComparison))
int indexOfMismatch = subject.IndexOfFirstMismatch(expected, comparer);

if (indexOfMismatch < 0 || indexOfMismatch >= expected.Length)
{
return;
}

int indexOfMismatch = subject.IndexOfFirstMismatch(expected, stringComparison);

assertion.FailWith(
ExpectationDescription + "{0}{reason}, but {1} differs near " + subject.IndexedSegmentAt(indexOfMismatch) +
".",
Expand Down
Expand Up @@ -2016,6 +2016,7 @@ namespace FluentAssertions.Primitives
public FluentAssertions.AndConstraint<TAssertions> Be(string expected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeEmpty(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeEquivalentTo(string expected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeEquivalentTo(string expected, System.Collections.Generic.IEqualityComparer<string> comparer, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeLowerCased(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeNullOrEmpty(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeNullOrWhiteSpace(string because = "", params object[] becauseArgs) { }
Expand All @@ -2042,6 +2043,7 @@ namespace FluentAssertions.Primitives
public FluentAssertions.AndConstraint<TAssertions> NotBe(string unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeEmpty(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeEquivalentTo(string unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeEquivalentTo(string unexpected, System.Collections.Generic.IEqualityComparer<string> comparer, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeLowerCased(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeNullOrEmpty(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeNullOrWhiteSpace(string because = "", params object[] becauseArgs) { }
Expand Down
Expand Up @@ -2100,6 +2100,7 @@ namespace FluentAssertions.Primitives
public FluentAssertions.AndConstraint<TAssertions> Be(string expected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeEmpty(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeEquivalentTo(string expected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeEquivalentTo(string expected, System.Collections.Generic.IEqualityComparer<string> comparer, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeLowerCased(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeNullOrEmpty(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeNullOrWhiteSpace(string because = "", params object[] becauseArgs) { }
Expand All @@ -2126,6 +2127,7 @@ namespace FluentAssertions.Primitives
public FluentAssertions.AndConstraint<TAssertions> NotBe(string unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeEmpty(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeEquivalentTo(string unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeEquivalentTo(string unexpected, System.Collections.Generic.IEqualityComparer<string> comparer, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeLowerCased(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeNullOrEmpty(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeNullOrWhiteSpace(string because = "", params object[] becauseArgs) { }
Expand Down
Expand Up @@ -1967,6 +1967,7 @@ namespace FluentAssertions.Primitives
public FluentAssertions.AndConstraint<TAssertions> Be(string expected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeEmpty(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeEquivalentTo(string expected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeEquivalentTo(string expected, System.Collections.Generic.IEqualityComparer<string> comparer, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeLowerCased(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeNullOrEmpty(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeNullOrWhiteSpace(string because = "", params object[] becauseArgs) { }
Expand All @@ -1993,6 +1994,7 @@ namespace FluentAssertions.Primitives
public FluentAssertions.AndConstraint<TAssertions> NotBe(string unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeEmpty(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeEquivalentTo(string unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeEquivalentTo(string unexpected, System.Collections.Generic.IEqualityComparer<string> comparer, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeLowerCased(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeNullOrEmpty(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeNullOrWhiteSpace(string because = "", params object[] becauseArgs) { }
Expand Down