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

[WIP] Add more specific variants of contain (#818) #962

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
12 changes: 6 additions & 6 deletions Src/FluentAssertions/Primitives/StringAssertions.cs
Expand Up @@ -618,7 +618,7 @@ public AndConstraint<StringAssertions> NotEndWithEquivalentOf(string unexpected,
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <see cref="because" />.
/// </param>
public AndConstraint<StringAssertions> Contain(string expected, string because = "", params object[] becauseArgs)
public StringContainmentConstraints Contain(string expected, string because = "", params object[] becauseArgs)
{
if (expected == null)
{
Expand All @@ -633,9 +633,9 @@ public AndConstraint<StringAssertions> Contain(string expected, string because =
Execute.Assertion
.ForCondition(Contains(Subject, expected, StringComparison.Ordinal))
.BecauseOf(because, becauseArgs)
.FailWith("Expected {context:string} {0} to contain {1}{reason}.", Subject, expected);
.FailWith("Expected {context:string} {0} to contain {1}{reason}, but not found.", Subject, expected);

return new AndConstraint<StringAssertions>(this);
return new StringContainmentConstraints(this, expected, "contain", StringComparison.Ordinal);
}

/// <summary>
Expand All @@ -650,7 +650,7 @@ public AndConstraint<StringAssertions> Contain(string expected, string because =
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <see cref="because" />.
/// </param>
public AndConstraint<StringAssertions> ContainEquivalentOf(string expected, string because = "", params object[] becauseArgs)
public StringContainmentConstraints ContainEquivalentOf(string expected, string because = "", params object[] becauseArgs)
{
if (expected == null)
{
Expand All @@ -665,9 +665,9 @@ public AndConstraint<StringAssertions> ContainEquivalentOf(string expected, stri
Execute.Assertion
.ForCondition(Contains(Subject, expected, StringComparison.CurrentCultureIgnoreCase))
.BecauseOf(because, becauseArgs)
.FailWith("Expected {context:string} to contain equivalent of {0}{reason} but found {1}.", expected, Subject);
.FailWith("Expected {context:string} {0} to contain equivalent of {1}{reason}, but not found.", Subject, expected);

return new AndConstraint<StringAssertions>(this);
return new StringContainmentConstraints(this, expected, "contain equivalent of", StringComparison.CurrentCultureIgnoreCase);
}

/// <summary>
Expand Down
86 changes: 86 additions & 0 deletions Src/FluentAssertions/StringContainmentConstraints.cs
@@ -0,0 +1,86 @@
using System;
using System.Diagnostics;
using FluentAssertions.Primitives;

namespace FluentAssertions
{
[DebuggerNonUserCode]
public class StringContainmentConstraints : StringAssertions
{
private readonly StringComparison comparison;
private int? occurrences;

public StringContainmentConstraints(StringAssertions parentConstraint, string expected, string containmentMode,
StringComparison comparison) :
base(parentConstraint.Subject)
{
this.comparison = comparison;
Expected = expected;
ContainmentMode = containmentMode;
And = this;
}

internal string Expected { get; }

internal string ContainmentMode { get; }

public StringContainmentConstraints And { get; }

public StringWithOccurenceConstraint Exactly => new StringWithOccurenceConstraint(this,
"exactly",
(occuredTimes, expectedTimes) => occuredTimes == expectedTimes);

public StringWithOccurenceConstraint AtLeast => new StringWithOccurenceConstraint(this,
"at least",
(occuredTimes, expectedTimes) => occuredTimes >= expectedTimes);

public StringWithOccurenceConstraint MoreThan => new StringWithOccurenceConstraint(this,
"more than",
(occuredTimes, expectedTimes) => occuredTimes > expectedTimes);

public StringWithOccurenceConstraint AtMost => new StringWithOccurenceConstraint(this,
"at most",
(occuredTimes, expectedTimes) => occuredTimes <= expectedTimes);

public StringWithOccurenceConstraint LessThan => new StringWithOccurenceConstraint(this,
"less than",
(occuredTimes, expectedTimes) => occuredTimes < expectedTimes);

internal int Occurrences
mkolumb marked this conversation as resolved.
Show resolved Hide resolved
{
get
{
if (!occurrences.HasValue)
{
string actual = Subject ?? "";
string substring = Expected ?? "";

int count = 0;
int index = 0;

while ((index = actual.IndexOf(substring, index, comparison)) >= 0)
{
index += substring.Length;
count++;
}

occurrences = count;
}

return occurrences.Value;
}
}

public AndConstraint<StringContainmentConstraints> Once(string because = "", params object[] becauseArgs) =>
Exactly.Once(because, becauseArgs);

public AndConstraint<StringContainmentConstraints> Twice(string because = "", params object[] becauseArgs) =>
Exactly.Twice(because, becauseArgs);

public AndConstraint<StringContainmentConstraints> Thrice(string because = "", params object[] becauseArgs) =>
Exactly.Thrice(because, becauseArgs);

public AndConstraint<StringContainmentConstraints> Times(int times, string because = "", params object[] becauseArgs) =>
Exactly.Times(times, because, becauseArgs);
}
}
49 changes: 49 additions & 0 deletions Src/FluentAssertions/StringWithOccurenceConstraint.cs
@@ -0,0 +1,49 @@
using System.Diagnostics;
using FluentAssertions.Execution;

namespace FluentAssertions
{
[DebuggerNonUserCode]
public class StringWithOccurenceConstraint
{
public delegate bool StringOccurrenceComparer(int occuredTimes, int expectedTimes);

private readonly StringOccurrenceComparer comparator;
private readonly string occurrenceMode;
private readonly StringContainmentConstraints parentConstraint;

public StringWithOccurenceConstraint(StringContainmentConstraints parentConstraint,
string occurrenceMode,
StringOccurrenceComparer comparator)
mkolumb marked this conversation as resolved.
Show resolved Hide resolved
{
this.parentConstraint = parentConstraint;
this.occurrenceMode = occurrenceMode;
this.comparator = comparator;
}

public AndConstraint<StringContainmentConstraints> Once(string because = "", params object[] becauseArgs) =>
Times(1, "once", because, becauseArgs);

public AndConstraint<StringContainmentConstraints> Twice(string because = "", params object[] becauseArgs) =>
Times(2, "twice", because, becauseArgs);

public AndConstraint<StringContainmentConstraints> Thrice(string because = "", params object[] becauseArgs) =>
Times(3, "thrice", because, becauseArgs);

public AndConstraint<StringContainmentConstraints> Times(int times, string because = "", params object[] becauseArgs) =>
Times(times, times == 1 ? "1 time" : $"{times} times", because, becauseArgs);

private AndConstraint<StringContainmentConstraints> Times(int times, string displayValue, string because = "",
params object[] becauseArgs)
{
Execute.Assertion
.ForCondition(comparator(parentConstraint.Occurrences, times))
.BecauseOf(because, becauseArgs)
.FailWith(
$"Expected {{context:string}} {{0}} to {parentConstraint.ContainmentMode} {{1}} {occurrenceMode} {displayValue}{{reason}}, but found {(parentConstraint.Occurrences == 1 ? "1 time" : $"{parentConstraint.Occurrences} times")}.",
parentConstraint.Subject, parentConstraint.Expected);

return new AndConstraint<StringContainmentConstraints>(parentConstraint);
}
}
}