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 specifying EquivalencyOptions in string assertions #2413

Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
2839bdc
Use `IEqualityComparer<string>` instead of `StringComparison`
vbreuss Oct 29, 2023
e8346cb
Add overloads for `BeEquivalentTo` and `NotBeEquivalentTo`
vbreuss Oct 29, 2023
fc0d749
Use string options in `StringEqualityEquivalencyStep`
vbreuss Oct 29, 2023
6b4c273
Add further overloads
vbreuss Oct 30, 2023
4366f0c
Update releases.md
vbreuss Oct 30, 2023
79a6e75
Improve tests
vbreuss Oct 30, 2023
a1f887c
Apply string-specific options to both, the subject and expected value.
vbreuss Oct 31, 2023
21b4582
Remove overload for `Be` and use `BeEquivalentTo` in `StringEqualityE…
vbreuss Nov 4, 2023
42266fc
Implement review comments
vbreuss Nov 4, 2023
65d6ad8
Add second line to Readme.md
vbreuss Nov 4, 2023
0d0cadd
Fix rebase errors
vbreuss Nov 4, 2023
ebe7feb
Remove `NotContainEquivalentOf` with `OccurrenceConstraint` parameter
vbreuss Nov 4, 2023
ef007ef
Cleanup Qodana issues
vbreuss Nov 5, 2023
fb3d6e4
Document the new functionality
vbreuss Nov 9, 2023
e7bce06
Avoid using `comparer.Equals("A", "a")`
vbreuss Nov 9, 2023
4813473
Add explicit overload for `Using(IEqualityComparer<string> comparer)`
vbreuss Nov 9, 2023
025ea2d
Use predicateDescription instead of boolean parameter
vbreuss Nov 9, 2023
258a7f8
Use `Be` instead of `BeEquivalentTo` in `StringEqualityEquivalencyStep`
vbreuss Nov 9, 2023
f700beb
Rename `behaviour` to `behavior` to fix the spell check
vbreuss Nov 9, 2023
0d94aa5
Disable Qodana Rule `ArrangeAccessorOwnerBody` instead of refactoring…
vbreuss Nov 10, 2023
ef018e3
Replace `EquivalencyAssertionOptions` with `EquivalencyOptions` in re…
vbreuss Nov 16, 2023
6dfe278
Mention possible effect on index of first mismatch calculation when u…
vbreuss Nov 18, 2023
c53c7f9
Add tests to verify the behavior for `CollectionAssertions` `AllBeEqu…
vbreuss Nov 23, 2023
625947f
Remove `IgnoringNewlines` options
vbreuss Dec 4, 2023
0572cd4
Adapt documentation
vbreuss Dec 4, 2023
36bc298
Adjust releases.md
vbreuss Jan 7, 2024
4df51d9
Replaced "that does not" with "not to" in string assertion messages
vbreuss Jan 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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)]))
vbreuss marked this conversation as resolved.
Show resolved Hide resolved
{
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.