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
18 changes: 18 additions & 0 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 @@ -7,6 +8,23 @@ namespace FluentAssertions.Common;

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="comparer"/>.
/// </summary>
public static int IndexOfFirstMismatch(this string value, string expected, IEqualityComparer<string> comparer)
{
for (int index = 0; index < value.Length; index++)
{
if (index >= expected.Length || !comparer.Equals(value[index..(index + 1)], expected[index..(index + 1)]))
{
return index;
}
}

return -1;
dennisdoomen marked this conversation as resolved.
Show resolved Hide resolved
}

/// <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"/>.
Expand Down
37 changes: 33 additions & 4 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(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(StringComparison.OrdinalIgnoreCase),
new StringEqualityStrategy(comparer),
because, becauseArgs);

expectation.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
Expand Up @@ -2013,6 +2013,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 Down
Expand Up @@ -2097,6 +2097,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 Down
Expand Up @@ -1964,6 +1964,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 Down
Expand Up @@ -2013,6 +2013,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 Down
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Xunit;
using Xunit.Sdk;
Expand All @@ -12,6 +13,33 @@ public partial class StringAssertionSpecs
{
public class BeEquivalentTo
{
[Fact]
public void Use_custom_comparer()
vbreuss marked this conversation as resolved.
Show resolved Hide resolved
{
// Arrange
var comparer = new DummyEqualityComparer((_, _) => true);
string actual = "test A";
string expect = "test B";

// Act / Assert
actual.Should().BeEquivalentTo(expect, comparer);
}

[Fact]
public void Fail_for_mismatch_using_custom_comparer()
{
// Arrange
var comparer = new DummyEqualityComparer((_, _) => false);
string actual = "foo";
string expect = "foo";

// Act
Action act = () => actual.Should().BeEquivalentTo(expect, comparer);

// Assert
act.Should().Throw<XunitException>();
}

[Fact]
public void When_strings_are_the_same_while_ignoring_case_it_should_not_throw()
{
Expand Down Expand Up @@ -104,6 +132,26 @@ public void
act.Should().Throw<XunitException>().WithMessage(
"Expected string to be equivalent to \"abc\" because I say so, but it has unexpected whitespace at the end.");
}

private sealed class DummyEqualityComparer : IEqualityComparer<string>
{
private readonly Func<string, string, bool> callback;

public DummyEqualityComparer(Func<string, string, bool> callback)
{
this.callback = callback;
}

public bool Equals(string x, string y)
{
return callback(x, y);
}

public int GetHashCode(string obj)
{
return obj.GetHashCode();
}
}
}

public class NotBeEquivalentTo
Expand Down
1 change: 1 addition & 0 deletions docs/_pages/releases.md
Expand Up @@ -13,6 +13,7 @@ sidebar:

### Improvements
* Improve failure message for string assertions when checking for equality - [#2307](https://github.com/fluentassertions/fluentassertions/pull/2307)
* Allow `IEqualityComparer` in string `BeEquivalentTo` assertion - [#2372](https://github.com/fluentassertions/fluentassertions/pull/2372)
vbreuss marked this conversation as resolved.
Show resolved Hide resolved

### Fixes
* Fixed formatting error when checking nullable `DateTimeOffset` with
Expand Down