Skip to content

Commit

Permalink
Ensures expectations involving multiple assertions can be cleared
Browse files Browse the repository at this point in the history
  • Loading branch information
dennisdoomen committed Sep 26, 2018
1 parent 2dc2217 commit 9356b83
Show file tree
Hide file tree
Showing 13 changed files with 312 additions and 144 deletions.
8 changes: 5 additions & 3 deletions Src/FluentAssertions/Collections/CollectionAssertions.cs
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
Expand Down Expand Up @@ -246,7 +246,7 @@ public AndConstraint<TAssertions> Equal(IEnumerable expected, string because = "

ICollection<TExpected> expectedItems = expectation.ConvertOrCastToCollection<TExpected>();

AssertionScope assertion = Execute.Assertion.BecauseOf(because, becauseArgs);
IAssertionScope assertion = Execute.Assertion.BecauseOf(because, becauseArgs);
if (subjectIsNull)
{
assertion.FailWith("Expected {context:collection} to be equal to {0}{reason}, but found <null>.", expectedItems);
Expand Down Expand Up @@ -1371,7 +1371,9 @@ public AndConstraint<TAssertions> HaveElementPreceding(object successor, object
.Then
.Given(subject => PredecessorOf(successor, subject))
.ForCondition(predecessor => predecessor.IsSameOrEqualTo(expectation))
.FailWith("but found {0}.", predecessor => predecessor);
.FailWith("but found {0}.", predecessor => predecessor)
.Then
.ClearExpectation();

return new AndConstraint<TAssertions>((TAssertions)this);
}
Expand Down
Expand Up @@ -17,7 +17,7 @@ public static Continuation AssertCollectionsHaveSameCount<T>(ICollection<object>
.AssertCollectionHasNotTooManyItems(subject, expectation);
}

public static Continuation AssertEitherCollectionIsNotEmpty<T>(this AssertionScope scope, ICollection<object> subject, ICollection<T> expectation)
public static Continuation AssertEitherCollectionIsNotEmpty<T>(this IAssertionScope scope, ICollection<object> subject, ICollection<T> expectation)
{
return scope
.ForCondition((subject.Count > 0) || (expectation.Count == 0))
Expand All @@ -30,7 +30,7 @@ public static Continuation AssertEitherCollectionIsNotEmpty<T>(this AssertionSco
Environment.NewLine);
}

public static Continuation AssertCollectionHasEnoughItems<T>(this AssertionScope scope, ICollection<object> subject, ICollection<T> expectation)
public static Continuation AssertCollectionHasEnoughItems<T>(this IAssertionScope scope, ICollection<object> subject, ICollection<T> expectation)
{
return scope
.ForCondition(subject.Count >= expectation.Count)
Expand All @@ -41,7 +41,7 @@ public static Continuation AssertCollectionHasEnoughItems<T>(this AssertionScope
Environment.NewLine);
}

public static Continuation AssertCollectionHasNotTooManyItems<T>(this AssertionScope scope, ICollection<object> subject, ICollection<T> expectation)
public static Continuation AssertCollectionHasNotTooManyItems<T>(this IAssertionScope scope, ICollection<object> subject, ICollection<T> expectation)
{
return scope
.ForCondition(subject.Count <= expectation.Count)
Expand Down
Expand Up @@ -184,7 +184,9 @@ private static Type GetIDictionaryInterface(Type expectedType)
.FailWith("but has additional key(s) {0}", keyDifference.AdditionalKeys)
.Then
.ForCondition(!hasMissingKeys || !hasAdditionalKeys)
.FailWith("but it misses key(s) {0} and has additional key(s) {1}", keyDifference.MissingKeys, keyDifference.AdditionalKeys);
.FailWith("but it misses key(s) {0} and has additional key(s) {1}", keyDifference.MissingKeys, keyDifference.AdditionalKeys)
.Then
.ClearExpectation();
}

private static KeyDifference<TSubjectKey, TExpectedKey> CalculateKeyDifference<TSubjectKey, TSubjectValue, TExpectedKey,
Expand Down
120 changes: 28 additions & 92 deletions Src/FluentAssertions/Execution/AssertionScope.cs
Expand Up @@ -2,6 +2,7 @@

using System;
using System.Linq;
using System.Threading;
using FluentAssertions.Common;

#endregion
Expand All @@ -11,7 +12,7 @@ namespace FluentAssertions.Execution
/// <summary>
/// Represents an implicit or explicit scope within which multiple assertions can be collected.
/// </summary>
public class AssertionScope : IDisposable
public class AssertionScope : IAssertionScope
{
#region Private Definitions

Expand All @@ -25,9 +26,9 @@ public class AssertionScope : IDisposable
private static AssertionScope current;

private AssertionScope parent;
private Func<string> expectation = null;
private readonly bool evaluateCondition = true;
private Func<string> expectation;
private string fallbackIdentifier = "object";
private bool? succeeded;

#endregion

Expand Down Expand Up @@ -69,21 +70,6 @@ public AssertionScope(string context)
/// </summary>
public string Context { get; set; }

/// <summary>
/// Creates a nested scope used during chaining.
/// </summary>
internal AssertionScope(AssertionScope sourceScope, bool sourceSucceeded)
{
assertionStrategy = sourceScope.assertionStrategy;
contextData = sourceScope.contextData;
reason = sourceScope.reason;
useLineBreaks = sourceScope.useLineBreaks;
parent = sourceScope.parent;
expectation = sourceScope.expectation;
evaluateCondition = sourceSucceeded;
Context = sourceScope.Context;
}

/// <summary>
/// Gets the current thread-specific assertion scope.
/// </summary>
Expand All @@ -93,10 +79,7 @@ public static AssertionScope Current
private set => current = value;
}

/// <summary>
/// Indicates that every argument passed into <see cref="FailWith"/> is displayed on a separate line.
/// </summary>
public AssertionScope UsingLineBreaks
public IAssertionScope UsingLineBreaks
{
get
{
Expand All @@ -105,25 +88,12 @@ public AssertionScope UsingLineBreaks
}
}

/// <summary>
/// Gets a value indicating whether or not the last assertion executed through this scope succeeded.
/// </summary>
public bool Succeeded { get; private set; }
public bool Succeeded
{
get => succeeded.HasValue && succeeded.Value;
}

/// <summary>
/// Specify the reason why you expect the condition to be <c>true</c>.
/// </summary>
/// <param name="because">
/// A formatted phrase compatible with <see cref="string.Format(string,object[])"/> explaining why
/// the condition should be satisfied. If the phrase does not start with the word <i>because</i>,
/// it is prepended to the message. If the format of <paramref name="because"/> or
/// <paramref name="becauseArgs"/> is not compatible with <see cref="string.Format(string,object[])"/>,
/// then a warning message is returned instead.
/// </param>
/// <param name="becauseArgs">
/// Zero or more values to use for filling in any <see cref="string.Format(string,object[])"/> compatible placeholders.
/// </param>
public AssertionScope BecauseOf(string because, params object[] becauseArgs)
public IAssertionScope BecauseOf(string because, params object[] becauseArgs)
{
reason = () =>
{
Expand Down Expand Up @@ -156,7 +126,7 @@ public AssertionScope BecauseOf(string because, params object[] becauseArgs)
/// </remarks>
/// <param name="message">The format string that represents the failure message.</param>
/// <param name="args">Optional arguments to any numbered placeholders.</param>
public AssertionScope WithExpectation(string message, params object[] args)
public IAssertionScope WithExpectation(string message, params object[] args)
{
var localReason = reason;
expectation = () =>
Expand All @@ -171,44 +141,30 @@ public AssertionScope WithExpectation(string message, params object[] args)
return this;
}

/// <summary>
/// Allows to safely select the subject for successive assertions, even when the prior assertion has failed.
/// </summary>
/// <paramref name="selector">
/// Selector which result is passed to successive calls to <see cref="ForCondition"/>.
/// </paramref>
public Continuation ClearExpectation()
{
expectation = null;

return new Continuation(this, !succeeded.HasValue || succeeded.Value);
}

public GivenSelector<T> Given<T>(Func<T> selector)
{
return new GivenSelector<T>(selector, evaluateCondition, this);
return new GivenSelector<T>(selector, !succeeded.HasValue || succeeded.Value, this);
}

/// <summary>
/// Specify the condition that must be satisfied.
/// </summary>
/// <param name="condition">
/// If <c>true</c> the assertion will be treated as successful and no exceptions will be thrown.
/// </param>
public AssertionScope ForCondition(bool condition)
public IAssertionScope ForCondition(bool condition)
{
if (evaluateCondition)
{
Succeeded = condition;
}
succeeded = condition;

return this;
}

/// <summary>
/// Sets the failure message when the assertion is not met, or completes the failure message set to a
/// prior call to <see cref="FluentAssertions.Execution.AssertionScope.WithExpectation"/>.
/// <paramref name="failReasonFunc"/> will not be called unless the assertion is not met.
/// </summary>
/// <param name="failReasonFunc">Function returning <see cref="FailReason"/> object on demand. Called only when the assertion is not met.</param>
public Continuation FailWith(Func<FailReason> failReasonFunc)
{
try
{
if (evaluateCondition && !Succeeded)
if (!succeeded.HasValue || !succeeded.Value)
{
string localReason = reason != null ? reason() : "";
var messageBuilder = new MessageBuilder(useLineBreaks);
Expand All @@ -222,35 +178,18 @@ public Continuation FailWith(Func<FailReason> failReasonFunc)
}

assertionStrategy.HandleFailure(result.Capitalize());

succeeded = false;
}

return new Continuation(this, Succeeded);
return new Continuation(this, succeeded.Value);
}
finally
{
Succeeded = false;
succeeded = null;
}
}

/// <summary>
/// Sets the failure message when the assertion is not met, or completes the failure message set to a
/// prior call to <see cref="FluentAssertions.Execution.AssertionScope.WithExpectation"/>.
/// </summary>
/// <remarks>
/// In addition to the numbered <see cref="string.Format(string,object[])"/>-style placeholders, messages may contain a few
/// specialized placeholders as well. For instance, {reason} will be replaced with the reason of the assertion as passed
/// to <see cref="FluentAssertions.Execution.AssertionScope.BecauseOf"/>. Other named placeholders will be replaced with
/// the <see cref="FluentAssertions.Execution.AssertionScope.Current"/> scope data passed through
/// <see cref="FluentAssertions.Execution.AssertionScope.AddNonReportable"/> and
/// <see cref="FluentAssertions.Execution.AssertionScope.AddReportable"/>. Finally, a description of the
/// current subject can be passed through the {context:description} placeholder. This is used in the message if no
/// explicit context is specified through the <see cref="AssertionScope"/> constructor.
/// Note that only 10 <paramref name="args"/> are supported in combination with a {reason}.
/// If an expectation was set through a prior call to <see cref="FluentAssertions.Execution.AssertionScope.WithExpectation"/>,
/// then the failure message is appended to that expectation.
/// </remarks>
/// <param name="message">The format string that represents the failure message.</param>
/// <param name="args">Optional arguments to any numbered placeholders.</param>
public Continuation FailWith(string message, params object[] args)
{
return FailWith(() => new FailReason(message, args));
Expand Down Expand Up @@ -288,9 +227,6 @@ public void AddReportable(string key, string value)
contextData.Add(key, value, Reportability.Reportable);
}

/// <summary>
/// Discards and returns the failures that happened up to now.
/// </summary>
public string[] Discard()
{
return assertionStrategy.DiscardFailures().ToArray();
Expand Down Expand Up @@ -326,9 +262,9 @@ public void Dispose()
}
}

public AssertionScope WithDefaultIdentifier(string identifier)
public IAssertionScope WithDefaultIdentifier(string identifier)
{
this.fallbackIdentifier = identifier;
fallbackIdentifier = identifier;
return this;
}
}
Expand Down
92 changes: 92 additions & 0 deletions Src/FluentAssertions/Execution/ChainedAssertionScope.cs
@@ -0,0 +1,92 @@
using System;

namespace FluentAssertions.Execution
{
/// <summary>
/// Allows chaining multiple assertion scopes together using <see cref="Continuation.Then"/>.
/// </summary>
/// <remarks>
/// If the parent scope has captured a failed assertion, this class ensures that successive assertions
/// are no longer evaluated.
/// </remarks>
public class ContinuedAssertionScope : IAssertionScope
{
private readonly AssertionScope predecessor;
private readonly bool predecessorSucceeded;

public ContinuedAssertionScope(AssertionScope predecessor, bool predecessorSucceeded)
{
this.predecessorSucceeded = predecessorSucceeded;
this.predecessor = predecessor;
}

public GivenSelector<T> Given<T>(Func<T> selector)
{
return predecessor.Given<T>(selector);
}

public IAssertionScope ForCondition(bool condition)
{
if (predecessorSucceeded)
{
return predecessor.ForCondition(condition);
}

return this;
}

public Continuation FailWith(Func<FailReason> failReasonFunc)
{
if (predecessorSucceeded)
{
return predecessor.FailWith(failReasonFunc);
}

return new Continuation(predecessor, false);
}

public Continuation FailWith(string message, params object[] args)
{
if (predecessorSucceeded)
{
return predecessor.FailWith(message, args);
}

return new Continuation(predecessor, false);
}

public IAssertionScope BecauseOf(string because, params object[] becauseArgs)
{
return predecessor.BecauseOf(because, becauseArgs);
}

public Continuation ClearExpectation()
{
return predecessor.ClearExpectation();
}

public IAssertionScope WithExpectation(string message, params object[] args)
{
return predecessor.WithExpectation(message, args);
}

public IAssertionScope WithDefaultIdentifier(string identifier)
{
return predecessor.WithDefaultIdentifier(identifier);
}

public IAssertionScope UsingLineBreaks => predecessor.UsingLineBreaks;

public bool Succeeded => predecessor.Succeeded;

public string[] Discard()
{
return predecessor.Discard();
}

public void Dispose()
{
predecessor.Dispose();
}
}
}

0 comments on commit 9356b83

Please sign in to comment.