Skip to content

Commit

Permalink
Ensured that nested assertion scopes produce a nested context
Browse files Browse the repository at this point in the history
Also fixed an inconsistency in which one overload of the assertion scope constructor would not actually affect AssertionScope.Current.
  • Loading branch information
dennisdoomen committed Mar 19, 2024
1 parent e969354 commit c5c777d
Show file tree
Hide file tree
Showing 3 changed files with 44 additions and 18 deletions.
44 changes: 26 additions & 18 deletions Src/FluentAssertions/Execution/AssertionScope.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ public sealed class AssertionScope : IAssertionScope

private static readonly AsyncLocal<AssertionScope> CurrentScope = new();
private Func<string> callerIdentityProvider = () => CallerIdentifier.DetermineCallerIdentity();
#pragma warning disable CA2213
private AssertionScope parent;
#pragma warning restore CA2213
private Func<string> expectation;
private string fallbackIdentifier = "object";
private bool? succeeded;
Expand All @@ -48,25 +50,23 @@ public DeferredReportable(Func<string> valueFunc)
#endregion

/// <summary>
/// Starts a named scope within which multiple assertions can be executed
/// Starts an unnamed scope within which multiple assertions can be executed
/// and which will not throw until the scope is disposed.
/// </summary>
public AssertionScope(string context)
: this()
public AssertionScope()
: this(new CollectingAssertionStrategy(), context: null)
{
if (!string.IsNullOrEmpty(context))
{
Context = new Lazy<string>(() => context);
}
SetCurrentAssertionScope(this);
}

/// <summary>
/// Starts an unnamed scope within which multiple assertions can be executed
/// Starts a named scope within which multiple assertions can be executed
/// and which will not throw until the scope is disposed.
/// </summary>
public AssertionScope()
: this(new CollectingAssertionStrategy())
public AssertionScope(string context)
: this(new CollectingAssertionStrategy(), new Lazy<string>(() => context))
{
SetCurrentAssertionScope(this);
}

/// <summary>
Expand All @@ -75,7 +75,7 @@ public AssertionScope()
/// <param name="assertionStrategy">The assertion strategy for this scope.</param>
/// <exception cref="ArgumentNullException"><paramref name="assertionStrategy"/> is <see langword="null"/>.</exception>
public AssertionScope(IAssertionStrategy assertionStrategy)
: this(assertionStrategy, GetCurrentAssertionScope())
: this(assertionStrategy, context: null)
{
SetCurrentAssertionScope(this);
}
Expand All @@ -85,32 +85,40 @@ public AssertionScope(IAssertionStrategy assertionStrategy)
/// and which will not throw until the scope is disposed.
/// </summary>
public AssertionScope(Lazy<string> context)
: this()
: this(new CollectingAssertionStrategy(), context)
{
Context = context;
SetCurrentAssertionScope(this);
}

/// <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"><paramref name="assertionStrategy"/> is <see langword="null"/>.</exception>
private AssertionScope(IAssertionStrategy assertionStrategy, AssertionScope parent)
private AssertionScope(IAssertionStrategy assertionStrategy, Lazy<string> context)
{
this.assertionStrategy = assertionStrategy
?? throw new ArgumentNullException(nameof(assertionStrategy));

this.parent = parent;
parent = GetCurrentAssertionScope();

if (parent is not null)
{
contextData.Add(parent.contextData);
Context = parent.Context;
reason = parent.reason;
callerIdentityProvider = parent.callerIdentityProvider;
FormattingOptions = parent.FormattingOptions.Clone();
Context = new Lazy<string>(() => JoinContext(parent.Context, context));
}
else
{
Context = context;
}
}

private static string JoinContext(params Lazy<string>[] contexts)
{
return string.Join("/", contexts.Where(ctx => ctx is not null).Select(x => x.Value));
}

/// <summary>
Expand All @@ -128,7 +136,7 @@ public static AssertionScope Current
#pragma warning disable CA2000 // AssertionScope should not be disposed here
get
{
return GetCurrentAssertionScope() ?? new AssertionScope(new DefaultAssertionStrategy(), parent: null);
return GetCurrentAssertionScope() ?? new AssertionScope(new DefaultAssertionStrategy(), context: null);
}
#pragma warning restore CA2000
private set => SetCurrentAssertionScope(value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,22 @@ public void Message_should_use_the_lazy_name_of_the_scope_as_context()
.WithMessage("Expected lazy foo to be equal to*");
}

[Fact]
public void Nested_scopes_use_the_name_of_their_outer_scope_as_context()
{
// Act
Action act = () =>
{
using var outerScope = new AssertionScope("outer");
using var innerScope = new AssertionScope("inner");
new[] { 1, 2, 3 }.Should().Equal(3, 2, 1);
};

// Assert
act.Should().Throw<XunitException>()
.WithMessage("Expected outer/inner to be equal to*");
}

[Fact]
public void Message_should_contain_each_unique_failed_assertion_seperately()
{
Expand Down
2 changes: 2 additions & 0 deletions docs/_pages/releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ sidebar:
* Fixed using `BeEquivalentTo` on an empty `ArraySegment` - [#2445](https://github.com/fluentassertions/fluentassertions/pull/2445), [#2511](https://github.com/fluentassertions/fluentassertions/pull/2511)
* `BeEquivalentTo` with a custom comparer can now handle null values - [#2489](https://github.com/fluentassertions/fluentassertions/pull/2489)
* Fixed incorrect treatment of "\\r\\n" as new line - [#2569](https://github.com/fluentassertions/fluentassertions/pull/2569)
* Ensured that nested calls to `AssertionScope(context)` create a chained context - [#2607](https://github.com/fluentassertions/fluentassertions/pull/2607)
* One overload of the `AssertionScope` constructor would not create an actual scope associated with the thread - [#2607](https://github.com/fluentassertions/fluentassertions/pull/2607)

### Breaking Changes (for users)
* Moved support for `DataSet`, `DataTable`, `DataRow` and `DataColumn` into a new package `FluentAssertions.DataSet` - [#2267](https://github.com/fluentassertions/fluentassertions/pull/2267)
Expand Down

0 comments on commit c5c777d

Please sign in to comment.