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 17, 2024
1 parent e969354 commit 5d87fdf
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
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())
{
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,34 +85,42 @@ 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>
/// 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 @@ -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
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_will_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
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 5d87fdf

Please sign in to comment.