Skip to content

Commit

Permalink
SAVEPOINT
Browse files Browse the repository at this point in the history
  • Loading branch information
dennisdoomen committed Mar 9, 2024
1 parent 549f752 commit f251b5b
Show file tree
Hide file tree
Showing 17 changed files with 112 additions and 938 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,9 @@ private static bool AssertIsNotNull(object expectation, object[] subject)
.FailWith("Expected {context:subject} to be <null>, but found {0}.", new object[] { subject });
}

private static Continuation AssertCollectionsHaveSameCount<T>(ICollection<object> subject, ICollection<T> expectation)
private NewContinuation<Assertion> AssertCollectionsHaveSameCount<T>(ICollection<object> subject, ICollection<T> expectation)
{
return AssertionScope.Current
return assertion
.WithExpectation("Expected {context:subject} to be a collection with {0} item(s){reason}", expectation.Count)
.AssertEitherCollectionIsNotEmpty(subject, expectation)
.Then
Expand Down Expand Up @@ -186,7 +186,7 @@ private bool LooselyMatchAgainst<T>(IList<object> subjects, T expectation, int e

foreach (string failure in results.SelectClosestMatchFor(expectationIndex))
{
AssertionScope.Current.AddPreFormattedFailure(failure);
assertion.AddPreFormattedFailure(failure);
}

return indexToBeRemoved != -1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ namespace FluentAssertions.Equivalency.Steps;

internal static class EnumerableEquivalencyValidatorExtensions
{
public static Continuation AssertEitherCollectionIsNotEmpty<T>(this IAssertionScope scope, ICollection<object> subject,
public static NewContinuation<ContinuedAssertion> AssertEitherCollectionIsNotEmpty<T>(this Assertion<Assertion> assertion, ICollection<object> subject,
ICollection<T> expectation)
{
return scope
return assertion
.ForCondition(subject.Count > 0 || expectation.Count == 0)
.FailWith(", but found an empty collection.")
.Then
Expand All @@ -19,21 +19,21 @@ internal static class EnumerableEquivalencyValidatorExtensions
subject.Count);
}

public static Continuation AssertCollectionHasEnoughItems<T>(this IAssertionScope scope, ICollection<object> subject,
public static NewContinuation<ContinuedAssertion> AssertCollectionHasEnoughItems<T>(this Assertion<Assertion> assertion, ICollection<object> subject,
ICollection<T> expectation)
{
return scope
return assertion
.ForCondition(subject.Count >= expectation.Count)
.FailWith($", but {{0}}{Environment.NewLine}contains {{1}} item(s) less than{Environment.NewLine}{{2}}.",
subject,
expectation.Count - subject.Count,
expectation);
}

public static Continuation AssertCollectionHasNotTooManyItems<T>(this IAssertionScope scope, ICollection<object> subject,
public static NewContinuation<ContinuedAssertion> AssertCollectionHasNotTooManyItems<T>(this Assertion<Assertion> assertion, ICollection<object> subject,
ICollection<T> expectation)
{
return scope
return assertion
.ForCondition(subject.Count <= expectation.Count)
.FailWith($", but {{0}}{Environment.NewLine}contains {{1}} item(s) more than{Environment.NewLine}{{2}}.",
subject,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public class GenericEnumerableEquivalencyStep : IEquivalencyStep

Type[] interfaceTypes = GetIEnumerableInterfaces(expectedType);

AssertionScope.Current
assertion
.ForCondition(interfaceTypes.Length == 1)
.FailWith(() => new FailReason("{context:Expectation} implements {0}, so cannot determine which one " +
"to use for asserting the equivalency of the collection. ",
Expand Down
155 changes: 87 additions & 68 deletions Src/FluentAssertions/Execution/Assertion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,45 @@

namespace FluentAssertions.Execution;

public class Assertion<TAssertion> : IAssertion
where TAssertion : Assertion<TAssertion>
public class Assertion
{
private readonly StringBuilder tracing = new();
private readonly ContextDataItems contextData = new();
private readonly Func<AssertionScope> getCurrentScope;
private string fallbackIdentifier = "object";
private Func<string> getCallerIdentifier;
private bool previousAssertionSucceeded;
private Func<string> reason;
private bool? succeeded;
private Func<string> expectation;

protected Assertion(Func<IAssertionScope> getCurrentScope, Func<string> getCallerIdentifier, bool previousAssertionSucceeded = true)
private static readonly AsyncLocal<Assertion> Instance = new();

public static void ReuseOnce(Assertion assertion)
{
this.GetCurrentScope = getCurrentScope;
Instance.Value = assertion;
}

public static Assertion GetOrCreate()
{
return GetOrCreate(() => AssertionScope.Current, () => AssertionScope.Current.GetIdentifier());
}

public static Assertion GetOrCreate(Func<AssertionScope> getCurrent, Func<string> getCallerIdentifier)
{
if (Instance.Value != null)
{
Assertion assertion = Instance.Value;
Instance.Value = null;
return assertion;
}

return new Assertion(getCurrent, getCallerIdentifier);
}

private Assertion(Func<AssertionScope> getCurrentScope, Func<string> getCallerIdentifier, bool previousAssertionSucceeded = true)
{
this.getCurrentScope = getCurrentScope;
this.getCallerIdentifier = getCallerIdentifier;
this.previousAssertionSucceeded = previousAssertionSucceeded;
}
Expand All @@ -42,13 +66,12 @@ protected Assertion(IAssertion previousAssertion)
/// <summary>
/// Adds an explanation of why the assertion is supposed to succeed to the scope.
/// </summary>
public TAssertion BecauseOf(Reason reason)
public Assertion BecauseOf(Reason reason)
{
return BecauseOf(reason.FormattedMessage, reason.Arguments);
}

/// <inheritdoc cref="IAssertionScope.BecauseOf(string, object[])"/>
public TAssertion BecauseOf(string because, params object[] becauseArgs)
public Assertion BecauseOf(string because, params object[] becauseArgs)
{
reason = () =>
{
Expand All @@ -67,107 +90,101 @@ public TAssertion BecauseOf(string because, params object[] becauseArgs)
}
};

return (TAssertion)this;
return this;
}

/// <inheritdoc cref="IAssertionScope.ForCondition(bool)"/>
public TAssertion ForCondition(bool condition)
public Assertion ForCondition(bool condition)
{
if (previousAssertionSucceeded)
{
succeeded = condition;
}

return (TAssertion)this;
return this;
}

void IAssertion.ForCondition(bool predicate) => ForCondition(predicate);

void IAssertion.FailWith(string message, object[] args) => FailWith(message, args);

public TAssertion ForConstraint(OccurrenceConstraint constraint, int actualOccurrences)
public Assertion ForConstraint(OccurrenceConstraint constraint, int actualOccurrences)
{
if (previousAssertionSucceeded)
{
constraint.RegisterReportables(GetCurrentScope());
constraint.RegisterReportables(this);
succeeded = constraint.Assert(actualOccurrences);
}

return (TAssertion)this;
return this;
}

public TAssertion WithExpectation(string message, params object[] args)
public Assertion WithExpectation(string message, params object[] args)
{
if (previousAssertionSucceeded)
{
Func<string> localReason = reason;

expectation = () =>
{
var messageBuilder = new MessageBuilder(GetCurrentScope().FormattingOptions);
var messageBuilder = new MessageBuilder(getCurrentScope().FormattingOptions);
string actualReason = localReason?.Invoke() ?? string.Empty;
string identifier = getCallerIdentifier();
return messageBuilder.Build(
message,
args,
actualReason,
GetCurrentScope().ContextData,
contextData,
identifier,
fallbackIdentifier);
};
}

return (TAssertion)this;
return this;
}

public void WithReportable(string name, Func<string> content)
{
GetCurrentScope().AddReportable(name, content);
AddReportable(name, content);
}

/// <inheritdoc cref="IAssertionScope.WithDefaultIdentifier(string)"/>
public TAssertion WithDefaultIdentifier(string identifier)
public Assertion WithDefaultIdentifier(string identifier)
{
fallbackIdentifier = identifier;
return (TAssertion)this;
return this;
}

public NewGivenSelector<T> Given<T>(Func<T> selector)
{
return new NewGivenSelector<T>(selector, this);
}

internal NewContinuation<ContinuedAssertion> FailWithPreFormatted(string formattedFailReason)
internal NewContinuation<Assertion> FailWithPreFormatted(string formattedFailReason)
{
return FailWith(() => formattedFailReason);
}

public NewContinuation<ContinuedAssertion> FailWith(string message)
public NewContinuation<Assertion> FailWith(string message)
{
return FailWith(() => new FailReason(message));
}

public NewContinuation<ContinuedAssertion> FailWith(string message, params object[] args)
public NewContinuation<Assertion> FailWith(string message, params object[] args)
{
return FailWith(() => new FailReason(message, args));
}

public NewContinuation<ContinuedAssertion> FailWith(string message, params Func<object>[] argProviders)
public NewContinuation<Assertion> FailWith(string message, params Func<object>[] argProviders)
{
return FailWith(
() => new FailReason(
message,
argProviders.Select(a => a()).ToArray()));
}

public NewContinuation<ContinuedAssertion> FailWith(Func<FailReason> failReasonFunc)
public NewContinuation<Assertion> FailWith(Func<FailReason> failReasonFunc)
{
return FailWith(
() =>
{
string localReason = reason?.Invoke() ?? string.Empty;
var messageBuilder = new MessageBuilder(GetCurrentScope().FormattingOptions);
var messageBuilder = new MessageBuilder(getCurrentScope().FormattingOptions);
string identifier = getCallerIdentifier();
FailReason failReason = failReasonFunc();
Expand All @@ -183,7 +200,7 @@ public NewContinuation<ContinuedAssertion> FailWith(Func<FailReason> failReasonF
});
}

private NewContinuation<ContinuedAssertion> FailWith(Func<string> failReasonFunc)
private NewContinuation<Assertion> FailWith(Func<string> failReasonFunc)
{
if (previousAssertionSucceeded)
{
Expand All @@ -197,14 +214,14 @@ private NewContinuation<ContinuedAssertion> FailWith(Func<string> failReasonFunc
result = expectation() + result;
}

GetCurrentScope().AddPreFormattedFailure(result.Capitalize());
getCurrentScope().AddPreFormattedFailure(result.Capitalize());
}
}

// Reset the state for successive assertions on this object
succeeded = null;

return new NewContinuation<ContinuedAssertion>(this);
return new NewContinuation<Assertion>(this);
}

public void AddCallerPostfix(string postfix)
Expand All @@ -213,6 +230,33 @@ public void AddCallerPostfix(string postfix)
getCallerIdentifier = () => originalCallerIdentifier() + postfix;
}

/// <summary>
/// Tracks a keyed object in the current scope that is excluded from the failure message in case an assertion fails.
/// </summary>
public void AddNonReportable(string key, object value)
{
contextData.Add(new ContextDataItems.DataItem(key, value, reportable: false, requiresFormatting: false));
}

/// <summary>
/// Adds some information to the assertion scope that will be included in the message
/// that is emitted if an assertion fails.
/// </summary>
public void AddReportable(string key, string value)
{
contextData.Add(new ContextDataItems.DataItem(key, value, reportable: true, requiresFormatting: false));
}

/// <summary>
/// Adds some information to the assertion scope that will be included in the message
/// that is emitted if an assertion fails. The value is only calculated on failure.
/// </summary>
public void AddReportable(string key, Func<string> valueFunc)
{
contextData.Add(new ContextDataItems.DataItem(key, new DeferredReportable(valueFunc), reportable: true,
requiresFormatting: false));
}

/// <summary>
/// Adds a block of tracing to the scope for reporting when an assertion fails.
/// </summary>
Expand All @@ -227,53 +271,28 @@ internal void TrackComparands(object subject, object expectation)
contextData.Add(new ContextDataItems.DataItem("expectation", expectation, reportable: false, requiresFormatting: true));
}

public Func<IAssertionScope> GetCurrentScope { get; }

public Func<string> GetCallerIdentifier => getCallerIdentifier;

public bool Succeeded => previousAssertionSucceeded && (succeeded is null or true);

public TAssertion UsingLineBreaks
public Assertion UsingLineBreaks
{
get
{
GetCurrentScope().FormattingOptions.UseLineBreaks = true;
return (TAssertion)this;
getCurrentScope().FormattingOptions.UseLineBreaks = true;
return this;
}
}

public Func<string> Reason => reason;
}

public sealed class Assertion : Assertion<Assertion>
{
// REFACTOR: Do we really need to pass in the scope and identifier?
private Assertion(Func<IAssertionScope> currentScope, Func<string> getCallerIdentifier)
: base(currentScope, getCallerIdentifier, previousAssertionSucceeded: true)
private sealed class DeferredReportable
{
}

private static readonly AsyncLocal<Assertion> Instance = new();
private readonly Lazy<string> lazyValue;

public static void ReuseOnce(Assertion assertion)
{
Instance.Value = assertion;
}

public static Assertion GetOrCreate()
{
return GetOrCreate(() => AssertionScope.Current, () => AssertionScope.Current.GetIdentifier());
}

public static Assertion GetOrCreate(Func<AssertionScope> getCurrent, Func<string> getCallerIdentifier)
{
if (Instance.Value != null)
public DeferredReportable(Func<string> valueFunc)
{
Assertion assertion = Instance.Value;
Instance.Value = null;
return assertion;
lazyValue = new Lazy<string>(valueFunc);
}

return new Assertion(getCurrent, getCallerIdentifier);
public override string ToString() => lazyValue.Value;
}
}

0 comments on commit f251b5b

Please sign in to comment.