Skip to content

Commit

Permalink
SAVEPOINT
Browse files Browse the repository at this point in the history
  • Loading branch information
dennisdoomen committed Mar 16, 2024
1 parent 549f752 commit 026dafd
Show file tree
Hide file tree
Showing 17 changed files with 133 additions and 916 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
163 changes: 85 additions & 78 deletions Src/FluentAssertions/Execution/Assertion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,56 @@

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)
{
Instance.Value = assertion;
}

public static Assertion GetOrCreate()
{
this.GetCurrentScope = getCurrentScope;
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;
}

protected Assertion(IAssertion previousAssertion)
protected Assertion(Assertion previousAssertion)
: this(previousAssertion.GetCurrentScope, previousAssertion.GetCallerIdentifier, previousAssertion.Succeeded)
{
reason = previousAssertion.Reason;
}

internal Func<AssertionScope> GetCurrentScope() => getCurrentScope;

/// <summary>
/// Gets or sets the context of the current assertion scope, e.g. the path of the object graph
/// that is being asserted on. The context is provided by a <see cref="Lazy{String}"/> which
Expand All @@ -42,13 +67,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 +91,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 +201,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 +215,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 @@ -214,66 +232,55 @@ public void AddCallerPostfix(string postfix)
}

/// <summary>
/// Adds a block of tracing to the scope for reporting when an assertion fails.
/// Tracks a keyed object in the current scope that is excluded from the failure message in case an assertion fails.
/// </summary>
public void AppendTracing(string tracingBlock)
public void AddNonReportable(string key, object value)
{
tracing.Append(tracingBlock);
getCurrentScope().AddNonReportable(key, value);
}

internal void TrackComparands(object subject, object expectation)
/// <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("subject", subject, reportable: false, requiresFormatting: true));
contextData.Add(new ContextDataItems.DataItem("expectation", expectation, reportable: false, requiresFormatting: true));
getCurrentScope().AddReportable(key, value);
}

public Func<IAssertionScope> GetCurrentScope { get; }

public Func<string> GetCallerIdentifier => getCallerIdentifier;

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

public TAssertion UsingLineBreaks
/// <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)
{
get
{
GetCurrentScope().FormattingOptions.UseLineBreaks = true;
return (TAssertion)this;
}
getCurrentScope().AddReportable(key, valueFunc);
}

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)
/// <summary>
/// Adds a block of tracing to the scope for reporting when an assertion fails.
/// </summary>
public void AppendTracing(string tracingBlock)
{
tracing.Append(tracingBlock);
}

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

public static void ReuseOnce(Assertion assertion)
internal void TrackComparands(object subject, object expectation)
{
Instance.Value = assertion;
contextData.Add(new ContextDataItems.DataItem("subject", subject, reportable: false, requiresFormatting: true));
contextData.Add(new ContextDataItems.DataItem("expectation", expectation, reportable: false, requiresFormatting: true));
}

public static Assertion GetOrCreate()
{
return GetOrCreate(() => AssertionScope.Current, () => AssertionScope.Current.GetIdentifier());
}
public bool Succeeded => previousAssertionSucceeded && (succeeded is null or true);

public static Assertion GetOrCreate(Func<AssertionScope> getCurrent, Func<string> getCallerIdentifier)
public Assertion UsingLineBreaks
{
if (Instance.Value != null)
get
{
Assertion assertion = Instance.Value;
Instance.Value = null;
return assertion;
getCurrentScope().FormattingOptions.UseLineBreaks = true;
return this;
}

return new Assertion(getCurrent, getCallerIdentifier);
}

private Func<string> Reason => reason;
}

0 comments on commit 026dafd

Please sign in to comment.