Skip to content

Commit

Permalink
Resolved a significant performance degradation in BeEquivalentTo (#…
Browse files Browse the repository at this point in the history
…1660)

This was caused by the increased dependency on the CallerIdentifier, and the additional work caller identification takes in v6.
  • Loading branch information
dennisdoomen committed Aug 22, 2021
1 parent a38a54a commit 577d555
Show file tree
Hide file tree
Showing 20 changed files with 159 additions and 49 deletions.
1 change: 1 addition & 0 deletions Src/FluentAssertions/CallerIdentifier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ namespace FluentAssertions
/// <summary>
/// Tries to extract the name of the variable or invocation on which the assertion is executed.
/// </summary>
// REFACTOR: Should be internal and treated as an implementation detail of the AssertionScope
public static class CallerIdentifier
{
public static Action<string> Logger { get; set; } = _ => { };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ public AndConstraint<TAssertions> BeEmpty(string because = "", params object[] b

EquivalencyAssertionOptions<IEnumerable<TExpectation>> options = config(AssertionOptions.CloneDefaults<TExpectation>()).AsCollection();

var context = new EquivalencyValidationContext(Node.From<IEnumerable<TExpectation>>(() => CallerIdentifier.DetermineCallerIdentity()), options)
var context = new EquivalencyValidationContext(Node.From<IEnumerable<TExpectation>>(() => AssertionScope.Current.CallerIdentity), options)
{
Reason = new Reason(because, becauseArgs),
TraceWriter = options.TraceWriter,
Expand Down Expand Up @@ -848,7 +848,7 @@ public AndConstraint<TAssertions> Contain(IEnumerable<T> expected, string becaus

foreach (T actualItem in Subject)
{
var context = new EquivalencyValidationContext(Node.From<TExpectation>(() => CallerIdentifier.DetermineCallerIdentity()), options)
var context = new EquivalencyValidationContext(Node.From<TExpectation>(() => AssertionScope.Current.CallerIdentity), options)
{
Reason = new Reason(because, becauseArgs),
TraceWriter = options.TraceWriter
Expand Down Expand Up @@ -2170,7 +2170,7 @@ public AndConstraint<TAssertions> NotContain(IEnumerable<T> unexpected, string b
int index = 0;
foreach (T actualItem in Subject)
{
var context = new EquivalencyValidationContext(Node.From<TExpectation>(() => CallerIdentifier.DetermineCallerIdentity()), options)
var context = new EquivalencyValidationContext(Node.From<TExpectation>(() => AssertionScope.Current.CallerIdentity), options)
{
Reason = new Reason(because, becauseArgs),
TraceWriter = options.TraceWriter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ public GenericDictionaryAssertions(TCollection keyValuePairs)

EquivalencyAssertionOptions<TExpectation> options = config(AssertionOptions.CloneDefaults<TExpectation>());

var context = new EquivalencyValidationContext(Node.From<TExpectation>(() => CallerIdentifier.DetermineCallerIdentity()), options)
var context = new EquivalencyValidationContext(Node.From<TExpectation>(() => AssertionScope.Current.CallerIdentity), options)
{
Reason = new Reason(because, becauseArgs),
TraceWriter = options.TraceWriter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ public AndConstraint<TAssertions> BeEquivalentTo(IEnumerable<string> expectation

EquivalencyAssertionOptions<IEnumerable<string>> options = config(AssertionOptions.CloneDefaults<string>()).AsCollection();

var context = new EquivalencyValidationContext(Node.From<IEnumerable<string>>(() => CallerIdentifier.DetermineCallerIdentity()), options)
var context = new EquivalencyValidationContext(Node.From<IEnumerable<string>>(() => AssertionScope.Current.CallerIdentity), options)
{
Reason = new Reason(because, becauseArgs),
TraceWriter = options.TraceWriter
Expand Down
2 changes: 1 addition & 1 deletion Src/FluentAssertions/Data/DataColumnAssertions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ public AndConstraint<DataColumnAssertions> BeEquivalentTo(DataColumn expectation

IDataEquivalencyAssertionOptions<DataColumn> options = config(AssertionOptions.CloneDefaults<DataColumn, DataEquivalencyAssertionOptions<DataColumn>>(e => new(e)));

var context = new EquivalencyValidationContext(Node.From<DataColumn>(() => CallerIdentifier.DetermineCallerIdentity()), options)
var context = new EquivalencyValidationContext(Node.From<DataColumn>(() => AssertionScope.Current.CallerIdentity), options)
{
Reason = new Reason(because, becauseArgs),
TraceWriter = options.TraceWriter
Expand Down
2 changes: 1 addition & 1 deletion Src/FluentAssertions/Data/DataRowAssertions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ public AndConstraint<DataRowAssertions<TDataRow>> BeEquivalentTo(DataRow expecta

IDataEquivalencyAssertionOptions<DataRow> options = config(AssertionOptions.CloneDefaults<DataRow, DataEquivalencyAssertionOptions<DataRow>>(e => new(e)));

var context = new EquivalencyValidationContext(Node.From<DataRow>(() => CallerIdentifier.DetermineCallerIdentity()), options)
var context = new EquivalencyValidationContext(Node.From<DataRow>(() => AssertionScope.Current.CallerIdentity), options)
{
Reason = new Reason(because, becauseArgs),
TraceWriter = options.TraceWriter
Expand Down
2 changes: 1 addition & 1 deletion Src/FluentAssertions/Data/DataSetAssertions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ public AndConstraint<DataSetAssertions<TDataSet>> BeEquivalentTo(DataSet expecta
CompileTimeType = typeof(TDataSet)
};

var context = new EquivalencyValidationContext(Node.From<DataSet>(() => CallerIdentifier.DetermineCallerIdentity()), options)
var context = new EquivalencyValidationContext(Node.From<DataSet>(() => AssertionScope.Current.CallerIdentity), options)
{
Reason = new Reason(because, becauseArgs),
TraceWriter = options.TraceWriter,
Expand Down
2 changes: 1 addition & 1 deletion Src/FluentAssertions/Data/DataTableAssertions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ public AndConstraint<DataTableAssertions<TDataTable>> BeEquivalentTo(DataTable e

IDataEquivalencyAssertionOptions<DataTable> options = config(AssertionOptions.CloneDefaults<DataTable, DataEquivalencyAssertionOptions<DataTable>>(e => new(e)));

var context = new EquivalencyValidationContext(Node.From<DataTable>(() => CallerIdentifier.DetermineCallerIdentity()), options)
var context = new EquivalencyValidationContext(Node.From<DataTable>(() => AssertionScope.Current.CallerIdentity), options)
{
Reason = new Reason(because, becauseArgs),
TraceWriter = options.TraceWriter
Expand Down
2 changes: 2 additions & 0 deletions Src/FluentAssertions/Equivalency/EquivalencyValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ public class EquivalencyValidator : IEquivalencyValidator
public void AssertEquality(Comparands comparands, EquivalencyValidationContext context)
{
using var scope = new AssertionScope();

scope.AssumeSingleCaller();
scope.AddReportable("configuration", context.Options.ToString());
scope.BecauseOf(context.Reason);

Expand Down
90 changes: 55 additions & 35 deletions Src/FluentAssertions/Execution/AssertionScope.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public sealed class AssertionScope : IAssertionScope
private Func<string> reason;

private static readonly AsyncLocal<AssertionScope> CurrentScope = new();
private Func<string> callerIdentityProvider = () => CallerIdentifier.DetermineCallerIdentity();
private AssertionScope parent;
private Func<string> expectation;
private string fallbackIdentifier = "object";
Expand All @@ -36,7 +37,7 @@ private sealed class DeferredReportable

public DeferredReportable(Func<string> valueFunc)
{
this.lazyValue = new(valueFunc);
lazyValue = new(valueFunc);
}

public override string ToString() => lazyValue.Value;
Expand All @@ -45,32 +46,15 @@ public DeferredReportable(Func<string> valueFunc)
#endregion

/// <summary>
/// Starts a new scope based on the given assertion strategy and parent assertion scope
/// </summary>
/// <param name="assertionStrategy">The assertion strategy for this scope.</param>
/// <param name="parent">The parent assertion scope for this scope.</param>
/// <exception cref="ArgumentNullException">Thrown when trying to use a null strategy.</exception>
internal AssertionScope(IAssertionStrategy assertionStrategy, AssertionScope parent)
{
this.assertionStrategy = assertionStrategy
?? throw new ArgumentNullException(nameof(assertionStrategy));
this.parent = parent;
}

/// <summary>
/// Starts a new scope based on the given assertion strategy.
/// Starts a named scope within which multiple assertions can be executed
/// and which will not throw until the scope is disposed.
/// </summary>
/// <param name="assertionStrategy">The assertion strategy for this scope.</param>
/// <exception cref="ArgumentNullException">Thrown when trying to use a null strategy.</exception>
public AssertionScope(IAssertionStrategy assertionStrategy)
: this(assertionStrategy, GetCurrentAssertionScope())
public AssertionScope(string context)
: this()
{
SetCurrentAssertionScope(this);

if (parent is not null)
if (!string.IsNullOrEmpty(context))
{
contextData.Add(parent.contextData);
Context = parent.Context;
Context = new Lazy<string>(() => context);
}
}

Expand All @@ -84,16 +68,14 @@ public AssertionScope()
}

/// <summary>
/// Starts a named scope within which multiple assertions can be executed
/// and which will not throw until the scope is disposed.
/// Starts a new scope based on the given assertion strategy.
/// </summary>
public AssertionScope(string context)
: this()
/// <param name="assertionStrategy">The assertion strategy for this scope.</param>
/// <exception cref="ArgumentNullException">Thrown when trying to use a null strategy.</exception>
public AssertionScope(IAssertionStrategy assertionStrategy)
: this(assertionStrategy, GetCurrentAssertionScope())
{
if (!string.IsNullOrEmpty(context))
{
Context = new Lazy<string>(() => context);
}
SetCurrentAssertionScope(this);
}

/// <summary>
Expand All @@ -106,6 +88,26 @@ public AssertionScope(Lazy<string> context)
Context = context;
}

/// <summary>
/// Starts a new scope based on the given assertion strategy and parent assertion scope
/// </summary>
/// <param name="assertionStrategy">The assertion strategy for this scope.</param>
/// <param name="parent">The parent assertion scope for this scope.</param>
/// <exception cref="ArgumentNullException">Thrown when trying to use a null strategy.</exception>
private AssertionScope(IAssertionStrategy assertionStrategy, AssertionScope parent)
{
this.assertionStrategy = assertionStrategy
?? throw new ArgumentNullException(nameof(assertionStrategy));
this.parent = parent;

if (parent is not null)
{
contextData.Add(parent.contextData);
Context = parent.Context;
callerIdentityProvider = parent.callerIdentityProvider;
}
}

/// <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 @@ -119,7 +121,10 @@ public AssertionScope(Lazy<string> context)
public static AssertionScope Current
{
#pragma warning disable CA2000 // AssertionScope should not be disposed here
get => GetCurrentAssertionScope() ?? new AssertionScope(new DefaultAssertionStrategy(), parent: null);
get
{
return GetCurrentAssertionScope() ?? new AssertionScope(new DefaultAssertionStrategy(), parent: null);
}
#pragma warning restore CA2000
private set => SetCurrentAssertionScope(value);
}
Expand Down Expand Up @@ -298,15 +303,19 @@ public Continuation FailWith(string message, params Func<object>[] argProviders)
private string GetIdentifier()
{
var identifier = Context?.Value;

if (string.IsNullOrEmpty(identifier))
{
identifier = CallerIdentifier.DetermineCallerIdentity();
identifier = CallerIdentity;
}

return identifier;
}

/// <summary>
/// Gets the identity of the caller associated with the current scope.
/// </summary>
public string CallerIdentity => callerIdentityProvider();

/// <summary>
/// Adds a pre-formatted failure message to the current scope.
/// </summary>
Expand Down Expand Up @@ -386,6 +395,17 @@ public AssertionScope WithDefaultIdentifier(string identifier)
return this;
}

/// <summary>
/// Allows the scope to assume that all assertions that happen within this scope are going to
/// be initiated by the same caller.
/// </summary>
public void AssumeSingleCaller()
{
// Since we know there's only one caller, we don't have to have every assertion determine the caller identity again
var provider = new Lazy<string>(() => CallerIdentifier.DetermineCallerIdentity());
callerIdentityProvider = () => provider.Value;
}

private static AssertionScope GetCurrentAssertionScope()
{
return CurrentScope.Value;
Expand Down
2 changes: 1 addition & 1 deletion Src/FluentAssertions/Numeric/ComparableTypeAssertions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ public AndConstraint<TAssertions> Be(T expected, string because = "", params obj
EquivalencyAssertionOptions<TExpectation> options = config(AssertionOptions.CloneDefaults<TExpectation>());

var context = new EquivalencyValidationContext(
Node.From<TExpectation>(() => CallerIdentifier.DetermineCallerIdentity()), options)
Node.From<TExpectation>(() => AssertionScope.Current.CallerIdentity), options)
{
Reason = new Reason(because, becauseArgs),
TraceWriter = options.TraceWriter
Expand Down
2 changes: 1 addition & 1 deletion Src/FluentAssertions/Primitives/ObjectAssertions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ public AndConstraint<TAssertions> NotBe(TSubject unexpected, string because = ""
EquivalencyAssertionOptions<TExpectation> options = config(AssertionOptions.CloneDefaults<TExpectation>());

var context = new EquivalencyValidationContext(Node.From<TExpectation>(() =>
CallerIdentifier.DetermineCallerIdentity()), options)
AssertionScope.Current.CallerIdentity), options)
{
Reason = new Reason(because, becauseArgs),
TraceWriter = options.TraceWriter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1226,6 +1226,7 @@ namespace FluentAssertions.Execution
public AssertionScope(FluentAssertions.Execution.IAssertionStrategy assertionStrategy) { }
public AssertionScope(System.Lazy<string> context) { }
public AssertionScope(string context) { }
public string CallerIdentity { get; }
public System.Lazy<string> Context { get; set; }
public FluentAssertions.Formatting.FormattingOptions FormattingOptions { get; }
public FluentAssertions.Execution.AssertionScope UsingLineBreaks { get; }
Expand All @@ -1234,6 +1235,7 @@ namespace FluentAssertions.Execution
public void AddPreFormattedFailure(string formattedFailureMessage) { }
public void AddReportable(string key, System.Func<string> valueFunc) { }
public void AddReportable(string key, string value) { }
public void AssumeSingleCaller() { }
public FluentAssertions.Execution.AssertionScope BecauseOf(FluentAssertions.Execution.Reason reason) { }
public FluentAssertions.Execution.AssertionScope BecauseOf(string because, params object[] becauseArgs) { }
public FluentAssertions.Execution.Continuation ClearExpectation() { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1226,6 +1226,7 @@ namespace FluentAssertions.Execution
public AssertionScope(FluentAssertions.Execution.IAssertionStrategy assertionStrategy) { }
public AssertionScope(System.Lazy<string> context) { }
public AssertionScope(string context) { }
public string CallerIdentity { get; }
public System.Lazy<string> Context { get; set; }
public FluentAssertions.Formatting.FormattingOptions FormattingOptions { get; }
public FluentAssertions.Execution.AssertionScope UsingLineBreaks { get; }
Expand All @@ -1234,6 +1235,7 @@ namespace FluentAssertions.Execution
public void AddPreFormattedFailure(string formattedFailureMessage) { }
public void AddReportable(string key, System.Func<string> valueFunc) { }
public void AddReportable(string key, string value) { }
public void AssumeSingleCaller() { }
public FluentAssertions.Execution.AssertionScope BecauseOf(FluentAssertions.Execution.Reason reason) { }
public FluentAssertions.Execution.AssertionScope BecauseOf(string because, params object[] becauseArgs) { }
public FluentAssertions.Execution.Continuation ClearExpectation() { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1226,6 +1226,7 @@ namespace FluentAssertions.Execution
public AssertionScope(FluentAssertions.Execution.IAssertionStrategy assertionStrategy) { }
public AssertionScope(System.Lazy<string> context) { }
public AssertionScope(string context) { }
public string CallerIdentity { get; }
public System.Lazy<string> Context { get; set; }
public FluentAssertions.Formatting.FormattingOptions FormattingOptions { get; }
public FluentAssertions.Execution.AssertionScope UsingLineBreaks { get; }
Expand All @@ -1234,6 +1235,7 @@ namespace FluentAssertions.Execution
public void AddPreFormattedFailure(string formattedFailureMessage) { }
public void AddReportable(string key, System.Func<string> valueFunc) { }
public void AddReportable(string key, string value) { }
public void AssumeSingleCaller() { }
public FluentAssertions.Execution.AssertionScope BecauseOf(FluentAssertions.Execution.Reason reason) { }
public FluentAssertions.Execution.AssertionScope BecauseOf(string because, params object[] becauseArgs) { }
public FluentAssertions.Execution.Continuation ClearExpectation() { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1179,6 +1179,7 @@ namespace FluentAssertions.Execution
public AssertionScope(FluentAssertions.Execution.IAssertionStrategy assertionStrategy) { }
public AssertionScope(System.Lazy<string> context) { }
public AssertionScope(string context) { }
public string CallerIdentity { get; }
public System.Lazy<string> Context { get; set; }
public FluentAssertions.Formatting.FormattingOptions FormattingOptions { get; }
public FluentAssertions.Execution.AssertionScope UsingLineBreaks { get; }
Expand All @@ -1187,6 +1188,7 @@ namespace FluentAssertions.Execution
public void AddPreFormattedFailure(string formattedFailureMessage) { }
public void AddReportable(string key, System.Func<string> valueFunc) { }
public void AddReportable(string key, string value) { }
public void AssumeSingleCaller() { }
public FluentAssertions.Execution.AssertionScope BecauseOf(FluentAssertions.Execution.Reason reason) { }
public FluentAssertions.Execution.AssertionScope BecauseOf(string because, params object[] becauseArgs) { }
public FluentAssertions.Execution.Continuation ClearExpectation() { }
Expand Down

0 comments on commit 577d555

Please sign in to comment.